diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index fec7af47a..94ee308e6 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -13,4 +13,5 @@ - [ ] New third-party code is a submodule built from source - [ ] Generic behaviour, not specific to one title - [ ] Depends on: +- [ ] Unblocks: - [ ] AI-assisted: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bf9aa23b4..bcc95b201 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,6 +39,8 @@ jobs: pr: ${{ steps.gate.outputs.pr }} head: ${{ steps.gate.outputs.head }} relinker: ${{ steps.gate.outputs.relinker }} + register-context: ${{ steps.gate.outputs.register-context }} + write-watch: ${{ steps.gate.outputs.write-watch }} full: ${{ steps.gate.outputs.full }} steps: - uses: actions/github-script@v8 @@ -55,6 +57,8 @@ jobs: core.setOutput('pr', ''); core.setOutput('head', ''); core.setOutput('relinker', true); + core.setOutput('register-context', true); + core.setOutput('write-watch', true); core.setOutput('full', true); await core.summary.addRaw(`Building main at ${context.sha}: all builds and tests enabled`).write(); return; @@ -97,16 +101,32 @@ jobs: }); relinker = commits.length > 0; } + const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 }); + const paths = files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean)); + const touches = (...prefixes) => manual || files.length >= 3000 || + paths.some((path) => prefixes.some((prefix) => prefix.endsWith('/') ? path.startsWith(prefix) : path === prefix)); + const workflow = '.github/workflows/build.yml'; + const libc = 'core/libs/prx/libc/'; + relinker = relinker && touches( + 'core/relinker/', libc, 'core/libs/prx/libkernel/AppMetadata/', 'core/libs/tests/ParamJsonParser.cpp', + 'core/shader/recompiler/tests/CacheFileReadTests.cpp', 'core/shader/recompiler/ShaderCacheDirectory.hpp', + 'tools/', 'cmake/', 'CMakeLists.txt', workflow + ); + const registerContext = touches('core/libs/tests/register_context/', 'core/libs/tests/RegisterContext.cpp', libc, workflow); + const writeWatch = touches('core/libs/tests/write_watch/', 'core/libs/tests/WriteWatch.cpp', libc, workflow); const { data: main } = await github.rest.repos.getBranch({ owner, repo, branch: 'main' }); core.setOutput('sha', main.commit.sha); core.setOutput('pr', String(pull_number)); core.setOutput('head', pr.head.sha); core.setOutput('relinker', relinker); + core.setOutput('register-context', registerContext); + core.setOutput('write-watch', writeWatch); core.setOutput('full', manual); core.info(`PR #${pull_number} at ${pr.head.sha} merged into main at ${main.commit.sha}`); await core.summary.addRaw( `PR #${pull_number} at ${pr.head.sha} merged into main at ${main.commit.sha}: full builds ${manual ? 'enabled' : 'disabled'}, ` + - `relinker ${relinker ? 'enabled' : 'skipped because the author has no commit in main'}` + `relinker ${relinker ? 'enabled' : 'skipped: no relinker file changed or the author has no commit in main'}, ` + + `register context tests ${registerContext ? 'enabled' : 'skipped'}, write watch tests ${writeWatch ? 'enabled' : 'skipped'}` ).write(); relinker: @@ -162,6 +182,8 @@ jobs: - run: ctest --test-dir build-relinker --output-on-failure --timeout 120 register-context: + needs: eligibility + if: ${{ needs.eligibility.outputs.register-context == 'true' }} strategy: fail-fast: false matrix: @@ -176,6 +198,8 @@ jobs: - run: ctest --test-dir build/register-context --output-on-failure --repeat until-fail:3 --no-tests=error write-watch: + needs: eligibility + if: ${{ needs.eligibility.outputs.write-watch == 'true' }} strategy: fail-fast: false matrix: diff --git a/CMakeLists.txt b/CMakeLists.txt index bc2733e64..39349094f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,10 +66,16 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress.py) add_test(NAME progress_encoding COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_encoding.py) + add_test(NAME progress_render_sliver COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_render_sliver.py) add_test(NAME import_audit COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_import_audit.py) add_test(NAME conventions_function_bodies COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_comments.py) + add_test(NAME conventions_wrapped_declarations COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_wrapped_declarations.py) + add_test(NAME conventions_ci_gate COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_ci_gate.py) add_test(NAME test_registration COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_test_registration.py) add_test(NAME check_skips COMMAND ${Python3_EXECUTABLE} @@ -80,6 +86,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/hw-oracle/test_hw_oracle.py) add_test(NAME pr_overlap_exports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_exports.py) + add_test(NAME pr_overlap_wrapped_exports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_wrapped_exports.py) add_test(NAME nid_database_cache COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py) add_test(NAME release_package COMMAND ${Python3_EXECUTABLE} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 900ea736e..85fc90d0e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,7 +37,7 @@ For pull requests, full Linux and Windows builds and tests run only when a colla gh workflow run build.yml --repo boykopovar/AnyPS5 --ref main --field pr=1234 ``` -This manual run also builds and tests the relinker on Linux, Windows and macOS, regardless of the author's previous contributions or the changed files. The relinker matrix also runs automatically when a pull request targeting `main` is opened, updated or reopened, if its author already has a commit in `main`; full builds never run automatically for pull requests. All build jobs build the selected pull request merged into the current `main` (GitHub's merge commit), so a pull request that no longer merges cleanly must be rebased first. New commits require another manual run for full builds. +This manual run also builds and tests the relinker on Linux, Windows and macOS, regardless of the author's previous contributions or the changed files. The relinker matrix also runs automatically when a pull request targeting `main` is opened, updated or reopened, if its author already has a commit in `main` and it changes a file the relinker-only build uses; full builds never run automatically for pull requests. Every job that runs automatically for pull requests runs only when the pull request changes its files: a `paths` filter on the `pull_request` trigger, or `needs: eligibility` with an `if` on the files output it sets in `build.yml`. The Conventions check fails a workflow change that adds a job without one. All build jobs build the selected pull request merged into the current `main` (GitHub's merge commit), so a pull request that no longer merges cleanly must be rebased first. New commits require another manual run for full builds. The Conventions check runs automatically for every contributor, including first-time contributors. It uses the base branch checker and reads pull request Git objects without executing pull request code. The Conventions check runs on every pull request and fails when a rule on this page is broken. It accepts code comments only when the pull request also changes [TechnicalDebt](docs/dev/TechnicalDebt.md), and only UTF-8 text files. Run it locally before pushing: @@ -54,6 +54,9 @@ The [pull request template](.github/pull_request_template.md) is the checklist f - Before starting, check that no open pull request already implements the same functions. - Keep the branch up to date with `main` and resolve conflicts yourself; rebasing and force-pushing is fine. - If a pull request needs another one first, say so on the checklist line (`Depends on: #N`; `none` otherwise). +- If a pull request fixes a stop or a wrong result seen on a title, name it on the checklist line with the title ID and its [Game bring-up](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up) discussion (`Unblocks: PPSA01325 #3232`; `none` otherwise). Reviewers review these first. +- Fixes a title needs go to `main` as pull requests. A Game bring-up discussion lists the open pull requests and local changes the title still needs; update the list when one is merged, closed or replaced. +- The PR overlap comment lists open pull requests that change the same lines or exports; agree with their authors which one goes first and declare it with `Depends on`. - Run the tests before opening the pull request and describe what was tested (OS, title or homebrew). - Investigation notes, reports, screenshots and logs go in the pull request, not in the repository. Images for documentation go in the [gist](https://gist.github.com/boykopovar/0e53f2e1426f29ecd41e3b51540b8a90) comments. - Say whether the change was written with AI assistance. The author of the pull request is responsible for every line of it. diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 23a160828..d094f39cf 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -26,6 +26,13 @@ target_compile_features(nid_patcher PRIVATE cxx_std_20) if(MINGW) target_link_options(nid_patcher PRIVATE -static -static-libgcc -static-libstdc++) + include(CheckCXXSourceCompiles) + set(CMAKE_REQUIRED_FLAGS -fno-asynchronous-unwind-tables) + check_cxx_source_compiles("#ifdef __USING_SJLJ_EXCEPTIONS__\n#error\n#endif\nint main() { return 0; }" ANYPS5_MINGW_DWARF_EXCEPTIONS) + unset(CMAKE_REQUIRED_FLAGS) + if(NOT ANYPS5_MINGW_DWARF_EXCEPTIONS) + message(FATAL_ERROR "${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION} selects SjLj exceptions with -fno-asynchronous-unwind-tables; the libraries need DWARF unwinding. Use the MinGW-w64 toolchain listed in docs/dev/BUILD.md.") + endif() add_compile_options(-fno-asynchronous-unwind-tables) endif() @@ -290,6 +297,11 @@ if(BUILD_TESTING) endif() add_test(NAME host_thread_local COMMAND host_thread_local_tests) set_tests_properties(host_thread_local PROPERTIES TIMEOUT 15) + if(MINGW) + add_test(NAME prx_thread_local_destructors COMMAND ${CMAKE_COMMAND} -DNM=${CMAKE_NM} -DOBJDUMP=${CMAKE_OBJDUMP} + -DMODULES=$ -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/ThreadLocalDestructors.cmake) + set_tests_properties(prx_thread_local_destructors PROPERTIES TIMEOUT 300) + endif() add_test_executable(savedata_core_write_replacement_tests tests/SaveDataWriteReplacement.cpp) target_include_directories(savedata_core_write_replacement_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 51d48be50..29dddbc5a 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1448,7 +1448,12 @@ struct SaveDataMountInfo { struct SceSaveDataTitleId { char data[10]; char pad[2]; }; struct SceSaveDataDirName { char data[32]; }; -struct SaveDataSearchInfo { std::uint8_t opaque[128]; }; +struct SaveDataSearchInfo { + std::uint64_t blocks; + std::uint64_t free_blocks; + std::uint8_t reserved[32]; +}; +static_assert(sizeof(SaveDataSearchInfo) == 48); struct SaveDataMemoryData { void* buf; std::size_t buf_size; std::size_t offset; }; struct SaveDataMount3 { @@ -1571,6 +1576,13 @@ struct SaveDataBackup { std::uint8_t reserved[32]; }; +struct SaveDataConvert { + std::int32_t user_id; + std::int32_t pad; + const SceSaveDataDirName* dir_name; + std::uint8_t unknown[48]; +}; + struct AppContentInitParam { char reserved[32]; }; struct AppContentBootParam { @@ -1590,6 +1602,19 @@ struct ContentExportInitParam2 { std::int64_t reserved1; }; +struct ContentExportParam { + char title[257]; + char comment[257]; + char content_type[65]; +}; +static_assert(sizeof(ContentExportParam) == 579); + +struct ContentExportCallbackParam { + std::size_t content_length; + void* func; + void* user_data; +}; + struct ContentSearchInitParam { std::size_t memory_size; }; struct PngEncCreateParam { diff --git a/core/libs/prx/libSceAgc/Acb/src/Control.cpp b/core/libs/prx/libSceAgc/Acb/src/Control.cpp index de20362b0..cfacb3c60 100644 --- a/core/libs/prx/libSceAgc/Acb/src/Control.cpp +++ b/core/libs/prx/libSceAgc/Acb/src/Control.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgc/Command/include/Memory.hpp" #include "prx/libSceAgc/Command/include/Packet.hpp" #include "prx/libSceAgc/DcbState/include/Marker.hpp" +#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" #include #include #include "SceTypes.hpp" @@ -11,10 +12,15 @@ extern "C" { -// unknown signature -void* APS5_VABI sceAgcAcbAtomicGds_0900(void) { - NotImplemented_nid_no_patch(__func__); - return nullptr; +std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData) { + const auto operation = (control & 0xffu) | ((control & 0x200u) << 7u) | + ((control & 0x20u) << 13u) | ((control & 0x40u) << 14u) | + ((control & 0x20u) == 0 && mode == 1 ? 0x20000u : 0u); + return Agc::Command::Emit(buf, 0x1du, { + operation, ((gdsOp & 1u) << 8u) | (src & 0x3fu), offset, index, + mask & 0x00ff00ffu, data, static_cast(compareData), + static_cast(compareData >> 32u), static_cast(extraData), + static_cast(extraData >> 32u)}, __func__); } std::uint32_t* APS5_VABI sceAgcAcbJump(CommandBuffer* buf, std::uint8_t cachePolicy, const std::uint32_t* target, std::uint32_t sizeInDwords) { @@ -46,11 +52,12 @@ std::uint32_t APS5_VABI sceAgcAcbRewindGetSize() { } std::uint32_t* APS5_VABI sceAgcAcbWaitUntilSafeForRendering(CommandBuffer* buf, std::uint32_t videoOutHandle, std::uint32_t displayBufferIndex) { - (void)buf; - (void)videoOutHandle; - (void)displayBufferIndex; - NotImplemented_nid_no_patch(__func__); - return nullptr; + auto* packet = Agc::Command::Allocate(buf, AgcDriver::RenderingWaitPacketWords, __func__); + packet[0] = AgcDriver::RenderingWaitPacketHeader; + packet[1] = videoOutHandle; + packet[2] = displayBufferIndex; + packet[3] = 0; + return packet; } std::uint32_t* APS5_VABI sceAgcAcbSetFlip(CommandBuffer* buf, std::uint32_t videoOutHandle, std::int32_t displayBufferIndex, std::uint32_t flipMode, std::int64_t flipArg) { diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index 3d44c03db..130df0566 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -68,7 +68,7 @@ endforeach() add_test_executable(agc_command_tests EXCLUDE_FROM_ALL tests/Commands.cpp) target_include_directories(agc_command_tests PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(agc_command_tests PRIVATE libSceAgc libc) +target_link_libraries(agc_command_tests PRIVATE libSceAgc libSceAgcDriver libc) configure_windows_unwind(agc_command_tests) if(BUILD_TESTING) set_target_properties(agc_command_tests PROPERTIES EXCLUDE_FROM_ALL OFF) diff --git a/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp b/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp index 2ce29cfe2..5db03f98a 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp @@ -1,28 +1,59 @@ #include "prx/libSceAgc/DcbState/include/Workload.hpp" #include "prx/libSceAgc/Command/include/Packet.hpp" +#include "prx/libSceAgcDriver/Resource/include/Registration.hpp" #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr std::uint32_t OpcodeNop = 0x10; +constexpr std::uint32_t ActivePacketWords = 18; +constexpr std::uint32_t CompletePacketWords = 12; +constexpr std::uint32_t MaxWorkloadId = 63; +constexpr std::uint32_t MaxActiveCount = 63; + +uint32_t* NewPacket(CommandBuffer* buf, std::uint32_t words, const char* function) { + auto* packet = Agc::Command::Allocate(buf, words, function); + std::memset(packet, 0, words * sizeof(std::uint32_t)); + packet[0] = Agc::Command::Header(OpcodeNop, words, 0); + return packet; +} + +} + extern "C" { uint32_t* APS5_VABI sceAgcDcbSetWorkloadComplete(CommandBuffer* buf, uint32_t stream_id, uint32_t workload_id) { - (void)buf; - (void)stream_id; - (void)workload_id; - NotImplemented_nid_no_patch(__func__); - return nullptr; + if (buf == nullptr || workload_id > MaxWorkloadId || !AgcDriverWorkloadStreamRegistered_nid_postfix(stream_id)) return nullptr; + const std::uint64_t clearMask = ~(std::uint64_t{1} << workload_id); + auto* packet = NewPacket(buf, CompletePacketWords, __func__); + packet[1] = stream_id; + packet[2] = workload_id; + packet[3] = static_cast(clearMask); + packet[4] = static_cast(clearMask >> 32); + return packet; } uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t stream_id, const uint32_t* workload_ids, uint32_t workload_count) { - (void)buf; - (void)stream_id; - (void)workload_ids; - (void)workload_count; - NotImplemented_nid_no_patch(__func__); - return nullptr; + if (buf == nullptr || workload_ids == nullptr || workload_count == 0 || workload_count > MaxActiveCount) return nullptr; + std::uint64_t mask = 0; + for (std::uint32_t index = 0; index < workload_count; ++index) { + const auto id = workload_ids[index]; + if (id > MaxWorkloadId) return nullptr; + const std::uint64_t bit = std::uint64_t{1} << id; + if ((mask & bit) != 0) return nullptr; + mask |= bit; + } + if (!AgcDriverWorkloadStreamRegistered_nid_postfix(stream_id)) return nullptr; + auto* packet = NewPacket(buf, ActivePacketWords, __func__); + packet[1] = stream_id; + packet[2] = static_cast(mask); + packet[3] = static_cast(mask >> 32); + return packet; } } diff --git a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp index b3b0da8f4..608c859ad 100644 --- a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp @@ -53,10 +53,21 @@ std::uint32_t CreateInterpolantValueSplitF16(std::uint32_t psWord, const ShaderS : CreateInterpolantValue(psWord, gsSemantic); } -int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* gs, const Shader* ps, CreateInterpolantValueFn createValue) { +std::uint32_t UpdateInterpolantValue(std::uint32_t psWord, const ShaderSemantic* gsSemantic) { + const std::uint32_t f16 = (psWord >> 20u) & 0x3u; + if (f16 != 0 && f16 != InterpolantF16HiOnly && ((psWord >> 28u) & 0x3u) != ((psWord >> 30u) & 0x3u)) { + throw std::runtime_error(std::string(__func__) + ": DEFAULT_VAL of an f16 input with different low and high defaults is unknown"); + } + return CreateInterpolantValueSplitF16(psWord, gsSemantic); +} + +int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* gs, const Shader* ps, CreateInterpolantValueFn createValue, bool fillIdentityTail) { if (regs == nullptr) { throw std::runtime_error(std::string(fn) + ": regs is null"); } + if (ps == nullptr && !fillIdentityTail) { + throw std::runtime_error(std::string(fn) + ": ps is null"); + } if (ps != nullptr && ps->num_input_semantics > 32) { throw std::runtime_error(std::string(fn) + ": input semantic count exceeds 32"); } @@ -66,13 +77,14 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* auto* output = regs; regs = values.data(); if (ps == nullptr || ps->num_input_semantics == 0) { - FillIdentityInterpolants(regs, 0); + const std::uint32_t count = fillIdentityTail ? InterpolantRegisterCount : 0; + if (fillIdentityTail) FillIdentityInterpolants(regs, 0); if (ps != nullptr) { - AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, 32}, {}); + AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, count}, {}); if (gs != nullptr) AgcDriverResolveGraphicsAbi_nid_postfix(gs, ps, 0); } transaction.Commit(); - std::copy(values.begin(), values.end(), output); + std::copy_n(values.begin(), count, output); return 0; } @@ -98,11 +110,12 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* SetInterpolantRegister(regs, i, createValue(ShaderSemanticWord(psSemantic), gsSemantic)); } - FillIdentityInterpolants(regs, ps->num_input_semantics); - AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, 32}, {}); + const std::uint32_t count = fillIdentityTail ? InterpolantRegisterCount : ps->num_input_semantics; + if (fillIdentityTail) FillIdentityInterpolants(regs, ps->num_input_semantics); + AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, count}, {}); AgcDriverResolveGraphicsAbi_nid_postfix(gs, ps, 0); transaction.Commit(); - std::copy(values.begin(), values.end(), output); + std::copy_n(values.begin(), count, output); return 0; } @@ -111,12 +124,17 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* extern "C" { int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps) { - return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValue); + return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValue, true); } APS5_EXPORT("dbOlWdppb4o", sceAgcUnknownCreateInterpolantMapping); int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps) { - return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16); + return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16, true); +} + +APS5_EXPORT("vieBRwlh1Lw", sceAgcUnknown_vieBRwlh1Lw); +int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(ShaderRegister* regs, const Shader* gs, const Shader* ps) { + return CreateInterpolantMapping(__func__, regs, gs, ps, UpdateInterpolantValue, false); } } diff --git a/core/libs/prx/libSceAgc/Unimplemented.cpp b/core/libs/prx/libSceAgc/Unimplemented.cpp index 860242c9d..0b85ef7ff 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -90,10 +90,4 @@ int APS5_VABI sceAgcGetShaderInstrumentation() { return 0; } -APS5_EXPORT("vieBRwlh1Lw", sceAgcUnknown_vieBRwlh1Lw); -int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index fd59ffa94..128553fa4 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -13,6 +13,7 @@ extern "C" std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std::uint32_t op, std::uint32_t state); extern "C" std::uint32_t* APS5_VABI sceAgcDcbClearState(CommandBuffer* buf, std::uint32_t command); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetFlip(CommandBuffer* buf, std::uint32_t handle, std::int32_t index, std::uint32_t mode, std::int64_t argument); +extern "C" std::uint32_t* APS5_VABI sceAgcAcbWaitUntilSafeForRendering(CommandBuffer* buf, std::uint32_t handle, std::uint32_t index); extern "C" int APS5_VABI sceAgcSuspendPoint(); extern "C" int APS5_VABI sceAgcInit(std::uint32_t version); extern "C" void* APS5_VABI sceAgcGetRegisterDefaults(); @@ -29,6 +30,10 @@ extern "C" std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuff extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateOp_0100(CommandBuffer* buf, std::uint32_t operation); extern "C" std::uint32_t* APS5_VABI sceAgcDcbPushMarker(CommandBuffer* buf, const char* str, std::uint32_t color); extern "C" std::uint32_t* APS5_VABI sceAgcDcbPopMarker(CommandBuffer* buf); +extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetWorkloadComplete(CommandBuffer* buf, std::uint32_t stream_id, std::uint32_t workload_id); +extern "C" int APS5_VABI sceAgcDriverRegisterWorkloadStream(std::uint32_t stream_id, const void* stream); +extern "C" int APS5_VABI sceAgcDriverUnregisterWorkloadStream(std::uint32_t stream_id); +extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, std::uint32_t stream_id, const std::uint32_t* workload_ids, std::uint32_t workload_count); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetMarker(CommandBuffer* buf, const char* str, std::uint32_t color); extern "C" std::uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, std::uint32_t color); extern "C" std::uint32_t* APS5_VABI sceAgcAcbPopMarker(CommandBuffer* buf); @@ -37,6 +42,7 @@ extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetIndexBuffer(CommandBuffer* buf, extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetBaseIndirectArgs(CommandBuffer* buf, std::uint32_t shaderType, const volatile void* indirectBaseAddr); extern "C" std::uint32_t* APS5_VABI sceAgcSetNop(CommandBuffer* buf, std::uint32_t sizeDw); extern "C" int APS5_VABI sceAgcDebugRaiseException(std::uint32_t exceptionId); +extern "C" std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData); namespace { @@ -206,6 +212,38 @@ void testMarkers() { expectFailure([] { sceAgcAcbSetMarker(nullptr, "frame", 0); }); } +void testWorkloads() { + Storage dcb; + const std::uint32_t ids[2] = {3, 4}; + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, ids, 2) == nullptr, "workloads active packet for an unregistered stream"); + check(sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 3) == nullptr, "workload complete packet for an unregistered stream"); + check(dcb.buffer.cursor_up == dcb.words.data(), "unregistered stream advanced the cursor"); + const std::uint32_t record[8] = {}; + check(sceAgcDriverRegisterWorkloadStream(1, record) == 0, "workload stream registration failed"); + const auto* active = sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, ids, 2); + check(active == dcb.words.data() && active[0] == Agc::Command::Header(0x10, 18, 0) && active[1] == 1 && active[2] == 0x18 && active[3] == 0, "workloads active packet"); + for (std::uint32_t word = 4; word < 18; ++word) check(active[word] == 0, "workloads active packet tail is not zero"); + const auto* complete = sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 33); + check(complete == dcb.words.data() + 18 && complete[0] == Agc::Command::Header(0x10, 12, 0) && complete[1] == 1 && complete[2] == 33 && complete[3] == 0xffffffffu && complete[4] == 0xfffffffdu, "workload complete packet"); + for (std::uint32_t word = 5; word < 12; ++word) check(complete[word] == 0, "workload complete packet tail is not zero"); + check(dcb.buffer.cursor_up == dcb.words.data() + 30, "incorrect workload cursor advance"); + const std::uint32_t duplicate[2] = {5, 5}; + const std::uint32_t outOfRange[1] = {64}; + const std::uint32_t top[1] = {63}; + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, duplicate, 2) == nullptr, "duplicate workload ids accepted"); + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, outOfRange, 1) == nullptr, "workload id 64 accepted"); + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, top, 1) != nullptr, "workload id 63 rejected"); + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, ids, 0) == nullptr, "workload count 0 accepted"); + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, nullptr, 2) == nullptr, "null workload ids accepted"); + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 0, ids, 2) == nullptr, "stream id 0 accepted"); + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 32, ids, 2) == nullptr, "stream id 32 accepted"); + check(sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 64) == nullptr, "workload complete id 64 accepted"); + check(sceAgcDcbSetWorkloadsActive(nullptr, 1, ids, 2) == nullptr, "null command buffer accepted"); + check(sceAgcDcbSetWorkloadComplete(nullptr, 1, 3) == nullptr, "null command buffer accepted for complete"); + check(sceAgcDriverUnregisterWorkloadStream(1) == 0, "workload stream unregistration failed"); + check(sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 3) == nullptr, "workload complete packet after unregistration"); +} + void testIndexBuffer() { Storage storage; alignas(4) std::uint16_t indices[2]{}; @@ -316,6 +354,17 @@ void testFlip() { exhausted.buffer.cursor_down = exhausted.words.data() + 5; expectFailure([&] { sceAgcDcbSetFlip(&exhausted.buffer, 1, 0, 1, 0); }); check(exhausted.buffer.cursor_up == exhausted.words.data(), "failed flip allocation advanced cursor"); + Storage acb; + acb.words.fill(0xdeadbeefu); + auto* wait = sceAgcAcbWaitUntilSafeForRendering(&acb.buffer, 0xfedcba98u, 2); + const std::array expectedWait{0xc0021018u, 0xfedcba98u, 2, 0}; + check(wait == acb.words.data() && std::equal(expectedWait.begin(), expectedWait.end(), wait), "compute rendering wait packet mismatch"); + check(acb.buffer.cursor_up == wait + 4 && wait[4] == 0xdeadbeefu, "compute rendering wait overran allocation"); + expectFailure([] { sceAgcAcbWaitUntilSafeForRendering(nullptr, 1, 0); }); + Storage full; + full.buffer.cursor_down = full.words.data() + 3; + expectFailure([&] { sceAgcAcbWaitUntilSafeForRendering(&full.buffer, 1, 0); }); + check(full.buffer.cursor_up == full.words.data(), "failed compute rendering wait advanced cursor"); check(sceAgcSuspendPoint() == 0, "empty suspend failed"); } @@ -404,6 +453,101 @@ void testMemory() { check(storage.words == beforeMask, "invalid mask patch modified packet memory"); } +struct AtomicGdsAllocation { + Storage storage; + std::uint32_t capacity = 15; + std::uint32_t calls = 0; + bool accepted = true; +}; + +bool APS5_VABI growAtomicGds(CommandBuffer* buffer, std::uint32_t count, void* userData) { + auto& allocation = *static_cast(userData); + check(count == 15, "atomic GDS allocation omitted packet or reserved words"); + ++allocation.calls; + if (!allocation.accepted) return false; + buffer->bottom = allocation.storage.words.data(); + buffer->top = buffer->bottom + allocation.capacity; + buffer->cursor_up = buffer->bottom; + buffer->cursor_down = buffer->top; + return true; +} + +void testAtomicGds() { + Storage storage; + storage.words.fill(0xdeadbeefu); + storage.buffer.cursor_up += 2; + auto* packet = sceAgcAcbAtomicGds_0900(&storage.buffer, 0xffffffffu, 1, 0xffffffffu, 0xffffffffu, + 0xabcd, 0xfedc, 0x12345678u, 0x87654321u, 0xfedcba9876543210ull, 0x0123456789abcdefull); + const std::array expected{ + 0xc0091d00u, 0x001500ffu, 0x13fu, 0xabcdu, 0xfedcu, 0x00340078u, + 0x87654321u, 0x76543210u, 0xfedcba98u, 0x89abcdefu, 0x01234567u}; + check(packet == storage.words.data() + 2, "atomic GDS returned the wrong packet address"); + check(std::equal(expected.begin(), expected.end(), packet), "atomic GDS packet or guest ABI mismatch"); + check(storage.buffer.cursor_up == packet + expected.size(), "atomic GDS did not advance by eleven words"); + check(storage.words[0] == 0xdeadbeefu && storage.words[1] == 0xdeadbeefu && + std::all_of(storage.words.begin() + 13, storage.words.end(), [](auto word) { return word == 0xdeadbeefu; }), + "atomic GDS wrote outside its packet"); + + const std::array, 9> controls{{ + {0, 0, 0}, {0, 1, 0x00020000u}, {0, 2, 0}, {0, 0xffffffffu, 0}, + {0x20u, 1, 0x00040020u}, {0x40u, 1, 0x00120040u}, + {0x200u, 1, 0x00030000u}, {0xfffffd00u, 0, 0}, {0xffu, 1, 0x001400ffu}}}; + for (const auto& entry : controls) { + Storage controlStorage; + auto* controlPacket = sceAgcAcbAtomicGds_0900(&controlStorage.buffer, entry[0], entry[1], + 0xfffffffeu, 0xffffffc0u, 0, 0, 0xff00ff00u, 0, 0, 0); + std::array controlExpected{}; + controlExpected[0] = 0xc0091d00u; + controlExpected[1] = entry[2]; + check(std::equal(controlExpected.begin(), controlExpected.end(), controlPacket), "atomic GDS control or field masking mismatch"); + } + + auto emit = [](CommandBuffer* buffer) { + return sceAgcAcbAtomicGds_0900(buffer, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + }; + Storage exact; + exact.buffer.cursor_down = exact.words.data() + 15; + exact.buffer.reserved_dw = 4; + check(emit(&exact.buffer) == exact.words.data() && exact.buffer.cursor_up == exact.words.data() + 11, + "atomic GDS rejected an exact fit with reserved space"); + const auto exactBefore = exact.words; + expectFailure([&] { emit(&exact.buffer); }); + check(exact.words == exactBefore && exact.buffer.cursor_up == exact.words.data() + 11, + "atomic GDS overwrote reserved space"); + Storage exhausted; + exhausted.buffer.cursor_down = exhausted.words.data() + 10; + const auto before = exhausted.words; + expectFailure([&] { emit(&exhausted.buffer); }); + check(exhausted.words == before && exhausted.buffer.cursor_up == exhausted.words.data(), + "failed atomic GDS allocation modified the buffer"); + expectFailure([&] { emit(nullptr); }); + + Storage noDown; + noDown.buffer.top = noDown.words.data() + 11; + noDown.buffer.cursor_down = nullptr; + check(emit(&noDown.buffer) == noDown.words.data() && noDown.buffer.cursor_up == noDown.buffer.top, + "atomic GDS did not honor a buffer with no down cursor"); + + AtomicGdsAllocation allocation; + CommandBuffer growing{nullptr, nullptr, nullptr, nullptr, growAtomicGds, &allocation, 4}; + check(emit(&growing) == allocation.storage.words.data() && allocation.calls == 1 && + growing.cursor_up == allocation.storage.words.data() + 11, "atomic GDS did not use the guest allocation callback"); + const std::array zeroPacket{0xc0091d00u}; + check(std::equal(zeroPacket.begin(), zeroPacket.end(), allocation.storage.words.begin()), + "atomic GDS callback allocation produced the wrong packet"); + for (const bool accepted : {false, true}) { + AtomicGdsAllocation failure; + failure.accepted = accepted; + failure.capacity = 14; + const auto failureBefore = failure.storage.words; + CommandBuffer failed{nullptr, nullptr, nullptr, nullptr, growAtomicGds, &failure, 4}; + expectFailure([&] { emit(&failed); }); + check(failure.calls == 1 && failure.storage.words == failureBefore && + failed.cursor_up == (accepted ? failure.storage.words.data() : nullptr), + "atomic GDS wrote a packet after failed or insufficient allocation"); + } +} + void testDebugRaiseException() { int result = -1; try { @@ -451,6 +595,7 @@ int main(int argc, char** argv) { testClearState(); testIndexedIndirectDraws(); testMarkers(); + testWorkloads(); testIndexBuffer(); testIndirectBase(); testContextState(); @@ -459,6 +604,7 @@ int main(int argc, char** argv) { testRegisterRange(); testPacketPayloadAddress(); testMemory(); + testAtomicGds(); testDebugRaiseException(); testDefaults(); LibcRunShutdown_nid_postfix(); diff --git a/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp index 1ea1d6ca2..b176809a4 100644 --- a/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp @@ -13,12 +13,14 @@ extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps); extern "C" int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps); +extern "C" int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(ShaderRegister* regs, const Shader* gs, const Shader* ps); namespace { using Registers = std::array; const Shader* mappedPixel = nullptr; Registers preparedMapping{}; +std::size_t preparedCount = 0; unsigned mappings = 0; unsigned links = 0; @@ -65,6 +67,11 @@ void testIdentity() { regs = filled(); check(sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, &ps) == 0, "mapping without inputs failed"); checkIdentity(regs, 0, "mapping without inputs is not the identity"); + regs = filled(); + check(sceAgcUnknown_vieBRwlh1Lw(regs.data(), nullptr, &ps) == 0, "update without inputs failed"); + check(preparedCount == 0u, "update without inputs prepared registers"); + const auto untouched = filled(); + check(std::memcmp(regs.data(), untouched.data(), sizeof(regs)) == 0, "update without inputs wrote registers"); } void testMapping() { @@ -99,6 +106,15 @@ void testMapping() { check(split[4].value == 0x03180002u, "full f16 interpolant mapping changed"); checkIdentity(split, static_cast(inputs.size()), "unused interpolants are not the identity"); + auto update = filled(); + const auto untouched = filled(); + const std::size_t written = inputs.size() * sizeof(ShaderRegister); + check(sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps) == 0, "update mapping failed"); + check(preparedCount == inputs.size(), "update prepared registers past its inputs"); + check(std::memcmp(update.data(), preparedMapping.data(), written) == 0, "prepared update differs from published registers"); + check(std::memcmp(update.data(), split.data(), written) == 0, "update mapping differs from the split f16 mapping"); + check(std::memcmp(update.data() + inputs.size(), untouched.data(), sizeof(update) - written) == 0, "update wrote past its inputs"); + auto regular = filled(); check(sceAgcCreateInterpolantMapping_0100(regular.data(), &gs, &ps) == 0, "mapping failed"); check(std::memcmp(regular.data(), preparedMapping.data(), sizeof(regular)) == 0, "prepared mapping differs from published registers"); @@ -106,6 +122,51 @@ void testMapping() { checkIdentity(regular, static_cast(inputs.size()), "unused interpolants are not the identity"); } +void testUpdateMapping() { + std::array inputs{semantic(1, 0, 0), semantic(2, 0, 1), semantic(3, 0, 2), semantic(4, 0, 3)}; + inputs[0].is_custom = 1; + inputs[0].default_value = 2; + inputs[1].default_value = 1; + inputs[1].default_value_hi = 1; + inputs[2].default_value_hi = 2; + inputs[3].is_custom = 1; + inputs[3].default_value = 3; + inputs[3].default_value_hi = 3; + std::array outputs{semantic(1, 5, 0), semantic(2, 7, 1), semantic(4, 9, 3)}; + Shader gs{}; + gs.output_semantics = outputs.data(); + gs.num_output_semantics = static_cast(outputs.size()); + Shader ps{}; + ps.input_semantics = inputs.data(); + ps.num_input_semantics = static_cast(inputs.size()); + + auto update = filled(); + const auto untouched = filled(); + check(sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps) == 0, "update mapping failed"); + check(update[0].value == 0x00000625u, "custom interpolant update changed"); + check(update[1].value == 0x01380107u, "low f16 half update changed"); + check(update[2].value == 0x02480220u, "unmatched high f16 half update changed"); + check(update[3].value == 0x03680709u, "full f16 interpolant update changed"); + for (std::uint32_t i = 0; i < inputs.size(); ++i) { + check(update[i].offset == ShaderRegs::SPI_PS_INPUT_CNTL_0 + i, "updated register offset changed"); + } + const std::size_t written = inputs.size() * sizeof(ShaderRegister); + check(std::memcmp(update.data() + inputs.size(), untouched.data(), sizeof(update) - written) == 0, "update wrote past its inputs"); + + auto create = filled(); + check(sceAgcUnknownCreateInterpolantMapping(create.data(), &gs, &ps) == 0, "split f16 mapping failed"); + check(std::memcmp(create.data(), update.data(), written) == 0, "update differs from the split f16 mapping"); + checkIdentity(create, static_cast(inputs.size()), "unused interpolants are not the identity"); + + for (const std::uint32_t index : {1u, 3u}) { + inputs[index].default_value = 0; + update = filled(); + expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps); }); + check(std::memcmp(update.data(), untouched.data(), sizeof(update)) == 0, "rejected update wrote registers"); + inputs[index].default_value = inputs[index].default_value_hi; + } +} + void testRejections() { std::array inputs{}; Shader gs{}; @@ -117,18 +178,23 @@ void testRejections() { const auto saved = regs; expectFailure([&] { sceAgcCreateInterpolantMapping_0100(regs.data(), &gs, &ps); }); expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(regs.data(), &gs, &ps); }); + expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), &gs, &ps); }); check(std::memcmp(regs.data(), saved.data(), sizeof(regs)) == 0, "rejected mapping wrote registers"); ps.num_input_semantics = 1; expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, &ps); }); expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(nullptr, &gs, &ps); }); + expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), nullptr, &ps); }); + expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(nullptr, &gs, &ps); }); + expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), &gs, nullptr); }); } } extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span context, std::span primitive) { - check(shader != nullptr && context.size() == preparedMapping.size() && primitive.empty(), "invalid mapping preparation call"); + check(shader != nullptr && context.size() <= preparedMapping.size() && primitive.empty(), "invalid mapping preparation call"); mappedPixel = shader; std::copy(context.begin(), context.end(), preparedMapping.begin()); + preparedCount = context.size(); ++mappings; } @@ -141,8 +207,9 @@ int main() { try { testIdentity(); testMapping(); + testUpdateMapping(); testRejections(); - check(mappings == 3u && links == 2u, "mapping preparation call count changed"); + check(mappings == 7u && links == 5u, "mapping preparation call count changed"); LibcRunShutdown_nid_postfix(); std::puts("AGC interpolant mapping tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index e77f027d1..86d62006f 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -595,6 +595,20 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_device_memory_fallback_tests EXCLUDE_FROM_ALL tests/DeviceMemoryFallback.cpp) +target_include_directories(agc_device_memory_fallback_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +target_link_libraries(agc_device_memory_fallback_tests PRIVATE libSceAgcDriver libc) +set_target_properties(agc_device_memory_fallback_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_device_memory_fallback_tests) +if(BUILD_TESTING) + set_target_properties(agc_device_memory_fallback_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_device_memory_fallback COMMAND agc_device_memory_fallback_tests) + if(WIN32) + set_tests_properties(agc_device_memory_fallback PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_display_buffer_dcc_tests EXCLUDE_FROM_ALL tests/DisplayBufferDcc.cpp) target_include_directories(agc_display_buffer_dcc_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_display_buffer_dcc_tests PRIVATE libSceAgcDriver libc) @@ -657,6 +671,7 @@ add_test_executable(agc_driver_flip_tests EXCLUDE_FROM_ALL target_include_directories(agc_driver_flip_tests PRIVATE ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include ) target_link_libraries(agc_driver_flip_tests PRIVATE @@ -811,6 +826,8 @@ if(BUILD_TESTING) endif() add_test(NAME agc_host_import_unmap COMMAND agc_driver_recorder_tests --host-import-unmap-only) set_tests_properties(agc_host_import_unmap PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_parked_import_unmap COMMAND agc_driver_recorder_tests --parked-import-unmap-only) + set_tests_properties(agc_parked_import_unmap PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000") add_test(NAME agc_unit_shadow COMMAND agc_driver_recorder_tests --unit-shadow-only) set_tests_properties(agc_unit_shadow PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000") add_test(NAME agc_descriptor_pool_recycling COMMAND agc_driver_recorder_tests --descriptor-pool-only) @@ -819,8 +836,10 @@ if(BUILD_TESTING) set_tests_properties(agc_draw_snapshot_cap PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_multisample_guest_access COMMAND agc_driver_recorder_tests --multisample-only) set_tests_properties(agc_multisample_guest_access PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_draw_inputs_in_place COMMAND agc_driver_recorder_tests --draw-inputs-in-place-only) + set_tests_properties(agc_draw_inputs_in_place PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_host_import_unmap agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() @@ -988,6 +1007,8 @@ target_include_directories(agc_driver_swap_alt_blend_tests PRIVATE $) +target_include_directories(agc_driver_color1555_target_tests PRIVATE $) + foreach(stage IN ITEMS vert frag) set(agcProvokingShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/ProvokingVertex.${stage}) set(agcProvokingShaderSpv ${agcTestShaderGeneratedRoot}/ProvokingVertex_${stage}.spv) @@ -1008,6 +1029,8 @@ target_include_directories(agc_driver_provoking_vertex_tests PRIVATE ${agcTestSh target_include_directories(agc_driver_prepared_exports_tests PRIVATE $) +target_include_directories(agc_driver_buffer_descriptor_runtime_tests PRIVATE $) + target_include_directories(agc_driver_prepared_publication_tests PRIVATE $) target_include_directories(agc_driver_prepared_graphics_tests PRIVATE $) @@ -1021,6 +1044,12 @@ target_include_directories(agc_driver_prepared_shaders_tests PRIVATE if(BUILD_TESTING) set_target_properties(agc_shader_mode_register_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_driver_readonly_resummarize COMMAND agc_driver_tests --readonly-resummarize) + set_tests_properties(agc_driver_readonly_resummarize PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(agc_driver_readonly_resummarize PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_shader_mode_register COMMAND agc_shader_mode_register_tests) set_tests_properties(agc_shader_mode_register PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 20) if(WIN32) @@ -1038,7 +1067,7 @@ if(BUILD_TESTING) set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_frontend_pair agc_shader_frontend_pair_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() foreach(agcTest IN ITEMS agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_vector_window_tests agc_constant_bit_test_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests ${agcExecutionTests}) - if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests") + if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests") continue() endif() string(REGEX REPLACE "_tests$" "" agcTestName "${agcTest}") @@ -1067,6 +1096,28 @@ if(BUILD_TESTING) set_tests_properties(agc_shared_write_partial_collect agc_shared_write_partial_collect_two_pass PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + set_target_properties(agc_driver_pipeline_optimized_readback_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + foreach(pipelineReadbackCase IN ITEMS d32 d16 color32 color64) + foreach(pipelineReadbackMode IN ITEMS default fast) + set(pipelineReadbackTest agc_driver_pipeline_optimized_readback_${pipelineReadbackCase}) + set(pipelineReadbackEnvironment "ANYPS5_NO_SHADER_CACHE=1") + if(pipelineReadbackMode STREQUAL "fast") + string(APPEND pipelineReadbackTest "_fast") + list(APPEND pipelineReadbackEnvironment "APS5_NO_PIPELINE_LTO=1") + endif() + add_test(NAME ${pipelineReadbackTest} COMMAND agc_driver_pipeline_optimized_readback_tests ${pipelineReadbackCase}) + set_tests_properties(${pipelineReadbackTest} PROPERTIES ENVIRONMENT "${pipelineReadbackEnvironment}" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(${pipelineReadbackTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() + add_test(NAME agc_vector_window_stage_inputs COMMAND agc_vector_window_tests --stage-inputs-only) + set_tests_properties(agc_vector_window_stage_inputs PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_vector_window_stage_inputs agc_driver_relative_stage_inputs PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_vector_window_stage_inputs PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_f64_denorm_preserve_spirv COMMAND agc_driver_f64_fma_subnormals_tests --verify-spv-denorm-mode) set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) @@ -1123,14 +1174,16 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_pm4_priority_refused PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_THREAD_PRIORITY=high;DBUS_SYSTEM_BUS_ADDRESS=unix:path=/nonexistent/aps5-no-rtkit" SKIP_RETURN_CODE 77) endif() + add_test(NAME agc_driver_present_mode COMMAND agc_driver_flip_tests present-mode) add_test(NAME agc_driver_flip_compute COMMAND agc_driver_flip_tests compute) set_tests_properties(agc_driver_flip_compute PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_driver_flip_compute PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_flip_compute agc_driver_present_mode PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_draw_cache_reuse_off COMMAND agc_driver_draw_cache_reuse_tests) set_tests_properties(agc_driver_draw_cache_reuse_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_GPL=1" SKIP_RETURN_CODE 77) + set_tests_properties(agc_driver_draw_cache_reuse agc_driver_draw_cache_reuse_off PROPERTIES FAIL_REGULAR_EXPRESSION "FATAL:") if(WIN32) set_tests_properties(agc_driver_draw_cache_reuse_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") @@ -1686,3 +1739,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +add_test_executable(agc_constant_phi_tests EXCLUDE_FROM_ALL tests/ConstantPhi.cpp) +target_link_libraries(agc_constant_phi_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_constant_phi_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_constant_phi_tests) +if(BUILD_TESTING) + set_target_properties(agc_constant_phi_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_constant_phi COMMAND agc_constant_phi_tests) + if(WIN32) + set_tests_properties(agc_constant_phi PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp index 06d954d3b..5fa283462 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp @@ -37,6 +37,9 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const struct PreparedGraphicsStage { std::shared_ptr snapshot; PreparedShaders::Entry entry; + ShaderRecompiler::ShaderStage stage = ShaderRecompiler::ShaderStage::Vertex; + std::uint32_t pushOffset = 0; + std::shared_ptr registration; }; std::vector PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index f0ee74af4..ad90c2db7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -103,6 +103,7 @@ std::shared_ptr RegisteredHeader(const ShaderRegistry& reg std::shared_ptr RegisteredProgram(const ShaderRegistry& registry, std::uint64_t address, std::initializer_list types); void PublishRegisteredShader(std::shared_ptr& registry, const std::shared_ptr& snapshot); void SubmitRegistrationPreparation(std::function task); +void SubmitLibraryPrebuild(std::function task); void CloseRegistrationPreparation(); void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index 5321424ce..b118da7bf 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -101,6 +101,7 @@ std::uint64_t CollectWritesUncached(std::uint64_t address, std::size_t bytes); // tracker mutex, so every collect that bumped before it has finished its walk and every later // collect stamps newer. std::uint64_t TrackerGeneration(); +std::uint64_t NewestStamp(std::uint64_t address, std::size_t bytes); // Whether no CPU write touched the range since `generation`: an uncached resetting collect of the // range's pages first (a game store not collected yet becomes a block stamp), then the compare over // the stamps collects make (dirty pages), not the MarkWritten stamps of the driver's own GPU label diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PresentMode.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PresentMode.hpp new file mode 100644 index 000000000..cb448bbc4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PresentMode.hpp @@ -0,0 +1,27 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PRESENTMODE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PRESENTMODE_HPP + +#include +#include +#include +#include +#include + +namespace AgcDriver { + +inline VkPresentModeKHR SelectPresentMode(const char* requested, std::span supported) { + VkPresentModeKHR mode = VK_PRESENT_MODE_FIFO_KHR; + if (requested != nullptr) { + const std::string_view name(requested); + if (name == "mailbox") mode = VK_PRESENT_MODE_MAILBOX_KHR; + else if (name == "immediate") mode = VK_PRESENT_MODE_IMMEDIATE_KHR; + else if (name == "fifo_latest_ready") mode = VK_PRESENT_MODE_FIFO_LATEST_READY_KHR; + else if (name != "fifo") throw std::runtime_error("Vulkan presentation: ANYPS5_PRESENT_MODE must be fifo, fifo_latest_ready, mailbox or immediate"); + } + if (std::find(supported.begin(), supported.end(), mode) == supported.end()) throw std::runtime_error("Vulkan presentation: the surface does not support the requested present mode"); + return mode; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 433582775..2027874af 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -299,9 +299,11 @@ public: }; IndirectDrawSupport DrawIndirectSupport() const; void EnqueueDraw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots = {}); + bool PrebuildGraphicsLibraries(const Graphics::ShaderStages& stages, std::span shaders); private: VulkanDevice(const PresentationWindow* window, bool compileOnly); + friend struct VulkanDeviceTestAccess; // The device's Graphics::Context: a copy of the one built at setup (its instance functions // resolved then, its function table filled then), or with APS5_NO_CONTEXT_CACHE=1 built anew. Graphics::Context graphicsContext() const; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp index 60d370b08..1ca05db5c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" +#include "ThreadOwned.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" @@ -10,8 +11,8 @@ namespace AgcDriver::DriverDetail { std::map& Driver::resolvedAhead() { - static thread_local std::map resolved; - return resolved; + static thread_local std::map* resolved = nullptr; + return ShaderRecompiler::ThreadOwned(resolved); } Driver::GroupCaptureStats& Driver::groupCaptureStats() { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index 14090d411..7b959293e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -64,6 +64,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(pixel.conservativeZExport)); mix(pixel.orderedPixelShader); + mix(pixel.stencilRefExportEnable); for (const auto value : pixel.targetOutputMode) mix(value); for (const auto value : pixel.targetExportMapping) mix(value); for (const auto value : pixel.targetExportPacking) mix(static_cast(value)); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp index 03a3f4533..267b6cf70 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp @@ -23,6 +23,7 @@ std::optional Driver::precheckDraw(const QueueState& queue, const S localDevice->ColorMetadataPass(*pass); return DrawVerdict::Drawn; } + if (Graphics::DepthResummarizeDrawsNothing(queue)) return DrawVerdict::Nothing; rejected = Graphics::DepthMaintenanceRejection(queue); if (!rejected.empty()) return DrawVerdict::Rejected; static const bool traceIndirectEnabled = std::getenv("APS5_TRACE_INDIRECT_DRAWS") != nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index 3e1fd6a77..6dec41300 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -98,7 +98,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { if (std::memcmp(&s.viewport, &t.viewport, sizeof(VkViewport)) != 0 || std::memcmp(&s.scissor, &t.scissor, sizeof(VkRect2D)) != 0) return false; const auto& p = a.pixel; const auto& q = b.pixel; - if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.orderedPixelShader != q.orderedPixelShader || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false; + if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.orderedPixelShader != q.orderedPixelShader || p.stencilRefExportEnable != q.stencilRefExportEnable || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false; if (a.roles != b.roles || a.programs.size() != b.programs.size()) return false; for (std::size_t i = 0; i < a.programs.size(); ++i) { const auto& x = a.programs[i]; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index 1dbe1cfd1..fa645dd13 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -55,6 +55,7 @@ void Driver::stop() { } Driver::Driver() { + static_cast(GuestMemory::TrackerGeneration()); ShaderMemory::SetWaitedMsProvider(&Graphics::Recorder::ThreadWaitedMs); try { LibcRegisterShutdown_nid_postfix([] { Driver::Get().Shutdown(); }); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 580d119af..55c09e78c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -19,9 +19,12 @@ #include #include #include +#include #include #include +#include #include +#include #include #include #include "RdnaDecoder/RdnaInstructionDecoder.hpp" @@ -29,13 +32,25 @@ namespace AgcDriver::DriverDetail { std::shared_ptr PrepareShaderWithDiagnostics(const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::ShaderPreparationContext* preparation) { + std::exception_ptr failure; + std::string dump; try { - return ShaderRecompiler::PrepareShader(request, preparation); + auto handle = ShaderRecompiler::PrepareShader(request, preparation); + return handle; } catch (...) { + failure = std::current_exception(); try { - if (std::getenv("APS5_DUMP_SHADERS") != nullptr) static_cast(Driver::dumpRequest(request.shader.codeAddress, request)); + if (std::getenv("APS5_DUMP_SHADERS") != nullptr) dump = Driver::dumpRequest(request.shader.codeAddress, request); } catch (...) {} - throw; + } + try { + std::rethrow_exception(failure); + } catch (const std::exception& error) { + std::ostringstream where; + where << "shader 0x" << std::hex << request.shader.codeAddress << " stage " << static_cast(request.shader.stage); + if (!dump.empty()) where << " (" << dump << ")"; + where << ": "; + throw std::runtime_error(where.str() + error.what()); } } @@ -571,6 +586,7 @@ struct RegisteredPreparation { std::array memory{}; std::optional request; std::vector abiKey; + std::shared_ptr prebuildDevice; }; std::unique_ptr PlanRegistered(const ShaderSnapshot& snapshot, const VulkanDevice& device, const QueueState& state, bool registration, bool* deferred = nullptr) { @@ -669,6 +685,18 @@ std::unique_ptr PlanRegistered(const ShaderSnapshot& snap return plan; } +void PrebuildStageLibrary(const std::shared_ptr& device, const Graphics::ShaderStages& stages, ShaderRecompiler::ShaderStage stage, std::shared_ptr result, std::uint32_t pushOffset) { + if (result == nullptr || (stage != ShaderRecompiler::ShaderStage::Vertex && stage != ShaderRecompiler::ShaderStage::Fragment)) return; + SubmitLibraryPrebuild([device, stages, stage, result = std::move(result), pushOffset] { + const std::array shaders{{{stage, result.get(), pushOffset}}}; + try { + static_cast(device->PrebuildGraphicsLibraries(stages, shaders)); + } catch (const std::exception& error) { + APS5_LOG_OUT("shader library prebuild skipped: %s", error.what()); + } + }); +} + std::vector PrepareRegistered(RegisteredPreparation& plan) { auto& request = *plan.request; std::vector entries; @@ -678,6 +706,13 @@ std::vector PrepareRegistered(RegisteredPreparation& pla entries.push_back({plan.codeOffset, PrepareShaderWithDiagnostics(request, &preparation)}); }; append(); + if (plan.prebuildDevice != nullptr && request.target.registrationModules && request.shader.stage == ShaderRecompiler::ShaderStage::Fragment) { + try { + PrebuildStageLibrary(plan.prebuildDevice, {Graphics::ShaderPath::Vertex, 0u, 32u, request.context.waveSize, {}, {}}, request.shader.stage, ShaderRecompiler::RegistrationResult(request, *entries.front().handle), request.layout.pushConstantOffsetBytes); + } catch (const std::exception& error) { + APS5_LOG_OUT("shader library prebuild skipped: %s", error.what()); + } + } if (request.context.compute) { request.context.compute->partialThreads = {1, 1, 1}; append(); @@ -696,10 +731,32 @@ std::vector PrepareRegistered(const ShaderSnapshot& snap class RegistrationPreparePool { public: static RegistrationPreparePool& Instance() { - static auto* pool = new RegistrationPreparePool(); + static auto* pool = new RegistrationPreparePool(std::max(2u, std::thread::hardware_concurrency() / 2u)); return *pool; } + static RegistrationPreparePool& Prebuild() { + static auto* pool = [] { + const char* text = std::getenv("APS5_PREBUILD_THREADS"); + auto* created = new RegistrationPreparePool(text != nullptr ? std::max(1u, static_cast(std::strtoul(text, nullptr, 10))) : 1u); + PrebuildSlot().store(created); + return created; + }(); + return *pool; + } + + static RegistrationPreparePool* ExistingPrebuild() { + return PrebuildSlot().load(); + } + +private: + static std::atomic& PrebuildSlot() { + static std::atomic slot{nullptr}; + return slot; + } + +public: + void Submit(std::function task) { { std::unique_lock lock(mutex); @@ -720,8 +777,7 @@ public: } private: - RegistrationPreparePool() { - const auto count = std::max(2u, std::thread::hardware_concurrency() / 2u); + explicit RegistrationPreparePool(unsigned count) { pthread_attr_t attributes; pthread_attr_init(&attributes); pthread_attr_setstacksize(&attributes, 64u << 20u); @@ -802,8 +858,13 @@ void SubmitRegistrationPreparation(std::function task) { RegistrationPreparePool::Instance().Submit(std::move(task)); } +void SubmitLibraryPrebuild(std::function task) { + RegistrationPreparePool::Prebuild().Submit(std::move(task)); +} + void CloseRegistrationPreparation() { RegistrationPreparePool::Instance().Close(); + if (auto* prebuild = RegistrationPreparePool::ExistingPrebuild()) prebuild->Close(); } std::vector PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target) { @@ -860,7 +921,15 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod } const auto bytes = handle->artifact->bindings.pushConstantSizeBytes; require(bytes <= room, "prepared graphics stages exceed the push constant block"); - prepared.push_back({program.snapshot, {program.codeOffset, handle}}); + std::shared_ptr registration; + if (target.registrationModules) { + try { + registration = ShaderRecompiler::RegistrationResult(request, *handle); + } catch (const std::exception& error) { + APS5_LOG_OUT("shader library prebuild skipped: %s", error.what()); + } + } + prepared.push_back({program.snapshot, {program.codeOffset, handle}, program.binary.stage, offset, std::move(registration)}); pushOffset += bytes; } transaction.Commit(); @@ -930,6 +999,11 @@ void Driver::ResolveGraphicsStagesAbi(std::span stages, std if (stage.snapshot->type == 1) continue; ResolvePreparedGraphics(*stage.snapshot, {}, primitiveType == state.userConfig.end() ? 0u : primitiveType->second, localDevice->Target()); } + if (decoded.state.stages.path == Graphics::ShaderPath::Vertex) { + for (const auto& stage : prepared) { + if (stage.registration != nullptr) PrebuildStageLibrary(localDevice, decoded.state.stages, stage.stage, stage.registration, stage.pushOffset); + } + } PreparedShaderState::GraphicsAbi abi{key, registry, {}}; for (const auto& stage : prepared) abi.stages.push_back(stage.snapshot); auto& graphicsAbis = transaction.Edit(*owner).graphicsAbis; @@ -970,14 +1044,20 @@ void Driver::ResolveShaderAbi(const Shader* shader, std::span, RegisteredAbiKeyStorage>(); BuildRegisteredAbiKey(state, *localDevice, key); + const auto prepare = [&] { + const auto plan = PlanRegistered(*snapshot, *localDevice, state, false); + if (plan == nullptr) return std::vector{}; + plan->prebuildDevice = localDevice; + return PrepareRegistered(*plan); + }; const bool committedAbi = CommittedAbi(*snapshot, key); - auto entries = committedAbi ? std::vector{} : PrepareRegistered(*snapshot, *localDevice, state, false); + auto entries = committedAbi ? std::vector{} : prepare(); const auto& registeredAbis = transaction.Read(*snapshot).registeredAbis; if (std::ranges::find(registeredAbis, key) != registeredAbis.end()) { transaction.Commit(); return; } - if (committedAbi) entries = PrepareRegistered(*snapshot, *localDevice, state, false); + if (committedAbi) entries = prepare(); auto& prepared = transaction.Edit(*snapshot); for (auto& entry : entries) { const auto duplicate = std::ranges::any_of(prepared.entries, [&](const auto& existing) { return existing.handle->artifact == entry.handle->artifact; }); @@ -1164,6 +1244,7 @@ void Driver::RegisterShader(const Shader* shader) { std::shared_ptr preparation; if (AsyncRegistrationPrepare()) { preparation = PlanRegistered(snapshot, *localDevice, registered, true, &snapshot.prepared->deferred); + if (preparation != nullptr) preparation->prebuildDevice = localDevice; if (preparation != nullptr && (snapshot.type == 0 || snapshot.type == 1)) BuildRegisteredAbiKey(registered, *localDevice, preparation->abiKey); snapshot.prepared->pending = preparation != nullptr; } else { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp index 1475997dd..9d15cf04b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include #include #include @@ -54,8 +55,8 @@ bool Driver::traceLateLabels() { } std::vector& Driver::pendingWaits() { - static thread_local std::vector waits; - return waits; + struct WaitsTag {}; + return HostThreadLocal, WaitsTag>(); } void Driver::notePendingWait(std::span packet, std::uint32_t queue, std::uint64_t received) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index b764c499d..834dccfce 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -423,6 +423,17 @@ struct PageStates { arena.forget(address, bytes); image.forget(address, bytes); } + + void record(std::uintptr_t base, std::uintptr_t regionEnd, bool readable, bool writable, std::uint64_t generation) { + for (PageSpan* span : {&arena, &image}) { + if (!readable || span->size == 0 || regionEnd <= span->base || base >= span->base + span->size) continue; + const std::uint8_t value = PageReadable | (writable ? PageWritable : 0u); + const auto first = std::max(base, span->base); + const auto last = std::min(regionEnd, span->base + span->size); + for (auto at = first; at < last; at += PageBytes) span->store(at, value); + if (GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation) span->forget(first, static_cast(last - first)); + } + } }; PageStates& Pages() { @@ -533,18 +544,16 @@ bool describePages(std::uintptr_t address, std::size_t bytes, Emit&& emit) { const bool committed = memory.State == MEM_COMMIT && (memory.Protect & (PAGE_GUARD | PAGE_NOACCESS)) == 0; const bool readable = committed && readableProtection(protection); const bool writable = readable && writableProtection(protection); - for (PageSpan* span : {&pages.arena, &pages.image}) { - if (!readable || span->size == 0 || regionEnd <= span->base || base >= span->base + span->size) continue; - const std::uint8_t value = PageReadable | (writable ? PageWritable : 0u); - const auto first = std::max(base, span->base); - const auto last = std::min(regionEnd, span->base + span->size); - for (auto at = first; at < last; at += PageBytes) span->store(at, value); - if (GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation) span->forget(first, static_cast(last - first)); - } + pages.record(base, regionEnd, readable, writable, generation); const auto next = std::min(end, regionEnd); if (!emit(PageRun{cursor, next, readable, writable})) return true; cursor = next; #else + const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + const auto recordRun = [&](std::uintptr_t next, bool readable, bool writable) { + constexpr auto mask = static_cast(PageBytes - 1); + pages.record(cursor & ~mask, (next + mask) & ~mask, readable, writable, generation); + }; if (const int fd = HostMapping::QueryFd(); fd >= 0) { procmap_query query{}; query.size = sizeof(query); @@ -559,8 +568,10 @@ bool describePages(std::uintptr_t address, std::size_t bytes, Emit&& emit) { continue; } const bool readable = (query.vma_flags & PROCMAP_QUERY_VMA_READABLE) != 0; + const bool writable = readable && (query.vma_flags & PROCMAP_QUERY_VMA_WRITABLE) != 0; const auto next = std::min(end, query.vma_end); - if (!emit(PageRun{cursor, next, readable, readable && (query.vma_flags & PROCMAP_QUERY_VMA_WRITABLE) != 0})) return true; + recordRun(next, readable, writable); + if (!emit(PageRun{cursor, next, readable, writable})) return true; cursor = next; continue; } @@ -589,8 +600,11 @@ bool describePages(std::uintptr_t address, std::size_t bytes, Emit&& emit) { found = true; break; } + const bool readable = permissions[0] == 'r'; + const bool writable = readable && permissions[1] == 'w'; const auto next = std::min(end, last); - if (!emit(PageRun{cursor, next, permissions[0] == 'r', permissions[0] == 'r' && permissions[1] == 'w'})) return true; + recordRun(next, readable, writable); + if (!emit(PageRun{cursor, next, readable, writable})) return true; cursor = next; found = true; break; @@ -780,7 +794,7 @@ struct WriteTracker { bool covers(std::uint64_t address, std::size_t bytes) const { #ifdef _WIN32 - return coverage.Covers(address, bytes); + return coverage.Covers(address, bytes) && !GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(static_cast(address), bytes); #else return address + bytes <= LeafCount * LeafBlocks * WriteBlockBytes && GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(static_cast(address), bytes); #endif @@ -859,6 +873,40 @@ struct WriteTracker { #endif } + static constexpr std::size_t UnstampedSlots = 1u << 16; + std::vector unstampedStores; + + void noteUnstampedStore(std::uint64_t address, std::uint64_t end) { + if (unstampedStores.empty()) unstampedStores.assign(UnstampedSlots, 0); + const auto first = address / WriteBlockBytes; + const auto last = (end - 1) / WriteBlockBytes; + if (last - first >= UnstampedSlots) { + std::fill(unstampedStores.begin(), unstampedStores.end(), generation); + return; + } + for (auto block = first; block <= last; ++block) unstampedStores[block % UnstampedSlots] = generation; + } + + std::uint32_t newestStamp(std::uint64_t address, std::uint64_t end) const { + std::uint32_t newest = 0; + if (!unstampedStores.empty()) { + const auto first = address / WriteBlockBytes; + const auto last = (end - 1) / WriteBlockBytes; + if (last - first >= UnstampedSlots) newest = *std::max_element(unstampedStores.begin(), unstampedStores.end()); + else for (auto block = first; block <= last; ++block) newest = std::max(newest, unstampedStores[block % UnstampedSlots]); + } + if (!watched) return newest; +#ifdef _WIN32 + address = std::max(address, base); + end = std::min(end, base + size); +#else + end = std::min(end, LeafCount * LeafBlocks * WriteBlockBytes); +#endif + if (address >= end) return newest; + for (auto block = blockOf(address); block <= blockOf(end - 1); ++block) newest = std::max(newest, stampOf(block)); + return newest; + } + bool unchanged(std::uint64_t first, std::uint64_t last, std::uint64_t since) const { if (first == last) return stampOf(first) <= since; const auto newer = [since](std::uint32_t stamp) { return stamp > since; }; @@ -1059,9 +1107,6 @@ bool walkWrites(WriteTracker& tracker, std::uint64_t first, std::uint64_t stop, std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoized) { if (bytes == 0 || bytes > std::numeric_limits::max() - address) return 0; -#ifdef _WIN32 - if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return 0; -#endif auto& tracker = Tracker(); // Whole pages, so a page shared with the next range is collected with either. constexpr std::uint64_t page = 4096; @@ -1190,9 +1235,6 @@ bool WriteWatched() { bool Watched(std::uint64_t address, std::size_t bytes) { if (bytes == 0 || bytes > std::numeric_limits::max() - address) return false; -#ifdef _WIN32 - if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return false; -#endif auto& tracker = Tracker(); const auto lock = lockTracker(tracker); tracker.initialize(); @@ -1201,9 +1243,6 @@ bool Watched(std::uint64_t address, std::size_t bytes) { bool UnchangedSince(std::uint64_t address, std::size_t bytes, std::uint64_t generation) { if (bytes == 0 || bytes > std::numeric_limits::max() - address) return false; -#ifdef _WIN32 - if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return false; -#endif auto& tracker = Tracker(); const auto lock = lockTracker(tracker); tracker.initialize(); @@ -1220,9 +1259,6 @@ bool UnchangedSinceAll(std::span queries) { if (!tracker.watched) return false; for (const auto& [address, bytes, generation] : queries) { if (generation == 0 || bytes == 0 || !tracker.covers(address, bytes)) return false; -#ifdef _WIN32 - if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return false; -#endif const auto first = tracker.blockOf(address); const auto last = tracker.blockOf(address + bytes - 1); if (!tracker.unchanged(first, last, generation)) return false; @@ -1235,7 +1271,10 @@ std::uint64_t storeOwn(std::uint64_t address, std::size_t bytes, const std::func const auto stampStored = [](WriteTracker& tracker, std::pair stored) -> std::uint64_t { if (stored.second <= stored.first) return 0; ++tracker.generation; - if (!tracker.watched || !tracker.covers(stored.first, static_cast(stored.second - stored.first))) return 0; + if (!tracker.watched || !tracker.covers(stored.first, static_cast(stored.second - stored.first))) { + tracker.noteUnstampedStore(stored.first, stored.second); + return 0; + } for (auto block = tracker.blockOf(stored.first); block <= tracker.blockOf(stored.second - 1); ++block) { tracker.stamp(block, tracker.generation, StampKind::Driver); tracker.noteDriverStore(block, stored.first, stored.second, tracker.generation); @@ -1249,9 +1288,6 @@ std::uint64_t storeOwn(std::uint64_t address, std::size_t bytes, const std::func constexpr std::uint64_t page = 4096; const auto first = address & ~(page - 1); const auto stop = (address + bytes + page - 1) & ~(page - 1); -#ifdef _WIN32 - if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(first, static_cast(stop - first))) return stampStored(tracker, store()); -#endif if (!walkWrites(tracker, first, stop, StampKind::Cpu)) return stampStored(tracker, store()); const auto stored = store(); walkWrites(tracker, first, stop, StampKind::Driver); @@ -1280,7 +1316,10 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { tracker.initialize(); if (bytes == 0) return 0; ++tracker.generation; - if (!tracker.watched || !tracker.covers(address, bytes)) return 0; + if (!tracker.watched || !tracker.covers(address, bytes)) { + tracker.noteUnstampedStore(address, address + bytes); + return 0; + } #ifndef _WIN32 { const auto blockFirst = address & ~(std::uint64_t{WriteBlockBytes} - 1); @@ -1288,6 +1327,7 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { StampRuns runs{tracker, StampKind::Fresh}; if (!GuestWriteWatch::GuestWriteWatchCollectFresh_nid_postfix(static_cast(blockFirst), static_cast(blockStop - blockFirst), &stampWrittenRun, &runs)) { unwatchLocked(tracker, blockFirst, static_cast(blockStop - blockFirst)); + tracker.noteUnstampedStore(address, address + bytes); return 0; } if (runs.stamped != 0) ++tracker.generation; @@ -1307,6 +1347,9 @@ bool StoredOver(std::uint64_t address, std::size_t bytes, std::uint64_t generati const auto lock = lockTracker(tracker); tracker.initialize(); if (!tracker.watched || generation == 0 || bytes == 0 || !tracker.covers(address, bytes)) return true; +#ifdef _WIN32 + if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return true; +#endif const auto end = address + bytes; const auto first = tracker.blockOf(address); const auto last = tracker.blockOf(end - 1); @@ -1344,6 +1387,13 @@ std::uint64_t TrackerGeneration() { return tracker.generation; } +std::uint64_t NewestStamp(std::uint64_t address, std::size_t bytes) { + auto& tracker = Tracker(); + const auto lock = lockTracker(tracker); + tracker.initialize(); + return bytes == 0 ? 0 : tracker.newestStamp(address, address + bytes); +} + bool UnchangedSinceCollected(std::uint64_t address, std::size_t bytes, std::uint64_t generation) { if (CollectWritesUncached(address, bytes) == 0) return false; auto& tracker = Tracker(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index 9b7b06e0a..2fdd6b930 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -249,7 +249,7 @@ void Validate(std::span packet, std::uint32_t queue) { require((packet[1] & 0xffff0000u) != 0x68750000u, "typed user-data and legacy flip markers are not implemented"); break; case 0x09: size(2); break; - case 0x06: graphics(); size(4); require(packet[3] == 0, "unsupported rendering wait mode"); break; + case 0x06: size(4); require(packet[3] == 0, "unsupported rendering wait mode"); break; case 0x0b: { const auto data = std::as_bytes(packet.subspan(1)); require(std::find(data.begin(), data.end(), std::byte{}) != data.end(), "unterminated marker text"); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp index 3d3794279..0e55dd5ea 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp @@ -47,6 +47,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta checkFeature(spv::CapabilityFloat64, core.shaderFloat64 == VK_TRUE, "shaderFloat64"); checkFeature(spv::CapabilityTessellation, core.tessellationShader == VK_TRUE, "tessellationShader"); checkFeature(spv::CapabilityImageGatherExtended, core.shaderImageGatherExtended == VK_TRUE, "shaderImageGatherExtended"); + checkFeature(spv::CapabilityClipDistance, core.shaderClipDistance == VK_TRUE, "shaderClipDistance"); checkFeature(spv::CapabilityMinLod, core.shaderResourceMinLod == VK_TRUE, "shaderResourceMinLod"); checkFeature(spv::CapabilityStorageImageMultisample, core.shaderStorageImageMultisample == VK_TRUE, "shaderStorageImageMultisample"); checkFeature(spv::CapabilityStorageImageReadWithoutFormat, core.shaderStorageImageReadWithoutFormat == VK_TRUE, "shaderStorageImageReadWithoutFormat"); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 8a1600be3..aae60dba2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -13,6 +13,7 @@ #include "prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp" #include "prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp" +#include "prx/libSceAgcDriver/Execution/include/PresentMode.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" @@ -241,6 +242,7 @@ struct VulkanDevice::State { VkExtent2D extent{}; std::vector images; VkFence acquireFence = VK_NULL_HANDLE; + VkPresentModeKHR presentMode = VK_PRESENT_MODE_FIFO_KHR; struct RetiredSwapchain { VkSwapchainKHR swapchain = VK_NULL_HANDLE; std::vector rendered; @@ -262,6 +264,7 @@ struct VulkanDevice::State { bool fragmentShaderBarycentric = false; bool geometryShader = false; bool sampleRateShading = false; + bool shaderClipDistance = false; bool shaderClock = false; bool narrowSubgroupClock = false; bool float64DenormPreserve = false; @@ -284,6 +287,7 @@ struct VulkanDevice::State { bool maintenance8 = false; std::uint32_t srgbDecodeFormats = 0; bool storageImageWithoutFormat = false; + VkSampleCountFlags sampleLocationSampleCounts = 0; bool depthClamp = false; bool depthBounds = false; bool depthBiasClamp = false; @@ -590,13 +594,19 @@ struct VulkanDevice::State { void WriteFrameDump(const PresentSlot& slot) { require(slot.dumpBuffer != nullptr, "frame dump was not recorded"); - std::vector full(slot.dumpBuffer->Bytes().begin(), slot.dumpBuffer->Bytes().end()); + const auto full = slot.dumpBuffer->Bytes(); const auto index = slot.dumpIndex; - const auto width = slot.dumpWidth; - const auto height = slot.dumpHeight; + const auto scale = slot.dumpScale; + require(scale != 0 && full.size() == static_cast(slot.dumpWidth) * slot.dumpHeight * 4, "invalid frame dump pixels"); + const auto width = (slot.dumpWidth + scale - 1) / scale; + const auto height = (slot.dumpHeight + scale - 1) / scale; + std::vector pixels(static_cast(width) * height * 4); + for (std::uint32_t y = 0; y < height; ++y) { + for (std::uint32_t x = 0; x < width; ++x) std::memcpy(pixels.data() + (static_cast(y) * width + x) * 4, full.data() + (static_cast(y) * scale * slot.dumpWidth + static_cast(x) * scale) * 4, 4); + } static const auto start = std::chrono::steady_clock::now(); - std::fprintf(stderr, "[gpu] frame_%03d.bmp: %ux%u display read back on the GPU at %.1f s\n", index, width, height, std::chrono::duration(std::chrono::steady_clock::now() - start).count()); - dumpWriter.Enqueue(index, width, height, slot.dumpScale, std::move(full)); + std::fprintf(stderr, "[gpu] frame_%03d.bmp: %ux%u display read back on the GPU at %.1f s\n", index, slot.dumpWidth, slot.dumpHeight, std::chrono::duration(std::chrono::steady_clock::now() - start).count()); + dumpWriter.Enqueue(index, width, height, 1, std::move(pixels)); } // Blocks on the fence, or polls it every 50 us with APS5_PRESENT_POLL_FENCE=1 (the difference @@ -1093,6 +1103,11 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->capabilities.push_back(spv::CapabilityFragmentShaderPixelInterlockEXT); state->spirvExtensions.push_back("SPV_EXT_fragment_shader_interlock"); } + if (hasExtension(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME)) { + deviceExtensions.push_back(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME); + state->capabilities.push_back(spv::CapabilityStencilExportEXT); + state->spirvExtensions.push_back("SPV_EXT_shader_stencil_export"); + } if (state->shaderClock) { deviceExtensions.push_back(VK_KHR_SHADER_CLOCK_EXTENSION_NAME); state->capabilities.push_back(spv::CapabilityShaderClockKHR); @@ -1150,6 +1165,31 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->spirvExtensions.push_back("SPV_KHR_physical_storage_buffer"); state->depthRangeUnrestricted = hasExtension(VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME); if (state->depthRangeUnrestricted) deviceExtensions.push_back(VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME); + if (hasExtension(VK_EXT_SAMPLE_LOCATIONS_EXTENSION_NAME)) { + VkPhysicalDeviceSampleLocationsPropertiesEXT locations{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLE_LOCATIONS_PROPERTIES_EXT}; + VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &locations}; + state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); + if (locations.sampleLocationSubPixelBits >= 4 && locations.sampleLocationCoordinateRange[0] <= 0.0f && locations.sampleLocationCoordinateRange[1] >= 15.0f / 16.0f) { + const auto multisampleProperties = state->InstanceFunction("vkGetPhysicalDeviceMultisamplePropertiesEXT"); + for (const auto samples : {VK_SAMPLE_COUNT_2_BIT, VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT}) { + if ((locations.sampleLocationSampleCounts & samples) == 0) continue; + VkMultisamplePropertiesEXT multisample{VK_STRUCTURE_TYPE_MULTISAMPLE_PROPERTIES_EXT}; + multisampleProperties(selected, samples, &multisample); + if (multisample.maxSampleLocationGridSize.width != 0 && multisample.maxSampleLocationGridSize.height != 0) state->sampleLocationSampleCounts |= samples; + } + } + if (state->sampleLocationSampleCounts != 0) deviceExtensions.push_back(VK_EXT_SAMPLE_LOCATIONS_EXTENSION_NAME); + } + const char* fifoLatestReadyExtension = hasExtension(VK_KHR_PRESENT_MODE_FIFO_LATEST_READY_EXTENSION_NAME) ? VK_KHR_PRESENT_MODE_FIFO_LATEST_READY_EXTENSION_NAME : hasExtension("VK_EXT_present_mode_fifo_latest_ready") ? "VK_EXT_present_mode_fifo_latest_ready" : nullptr; + const char* requestedPresentMode = std::getenv("ANYPS5_PRESENT_MODE"); + VkPhysicalDevicePresentModeFifoLatestReadyFeaturesKHR fifoLatestReadyFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_MODE_FIFO_LATEST_READY_FEATURES_KHR}; + bool fifoLatestReady = false; + if (requestedPresentMode != nullptr && std::strcmp(requestedPresentMode, "fifo_latest_ready") == 0 && fifoLatestReadyExtension != nullptr) { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &fifoLatestReadyFeatures}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); + fifoLatestReady = fifoLatestReadyFeatures.presentModeFifoLatestReady == VK_TRUE; + if (fifoLatestReady) deviceExtensions.push_back(fifoLatestReadyExtension); + } // Guest dispatches cover whole thread groups past the edge of small images; robust image access // drops those writes instead of faulting the device. const bool imageRobustness = hasExtension(VK_EXT_IMAGE_ROBUSTNESS_EXTENSION_NAME); @@ -1348,6 +1388,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->sampleRateShading = enabled.sampleRateShading == VK_TRUE; if (enabled.geometryShader) state->capabilities.push_back(spv::CapabilityGeometry); enabled.shaderClipDistance = available.shaderClipDistance; + state->shaderClipDistance = enabled.shaderClipDistance == VK_TRUE; + if (enabled.shaderClipDistance) state->capabilities.push_back(spv::CapabilityClipDistance); if (enabled.shaderStorageImageWriteWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageWriteWithoutFormat); if (enabled.shaderStorageImageReadWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageReadWithoutFormat); state->storageImageWithoutFormat = enabled.shaderStorageImageWriteWithoutFormat == VK_TRUE && enabled.shaderStorageImageReadWithoutFormat == VK_TRUE; @@ -1417,6 +1459,12 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : minLodFeatures.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &minLodFeatures; } + fifoLatestReadyFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_MODE_FIFO_LATEST_READY_FEATURES_KHR}; + fifoLatestReadyFeatures.presentModeFifoLatestReady = VK_TRUE; + if (fifoLatestReady) { + fifoLatestReadyFeatures.pNext = const_cast(deviceInfo.pNext); + deviceInfo.pNext = &fifoLatestReadyFeatures; + } if (state->maintenance8) { maintenance8Features.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &maintenance8Features; @@ -1593,6 +1641,13 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : std::vector formats(formatCount); check(getFormats(selected, state->surface, &formatCount, formats.data()), "vkGetPhysicalDeviceSurfaceFormatsKHR"); require(std::any_of(formats.begin(), formats.end(), [](const auto& format) { return format.format == VK_FORMAT_B8G8R8A8_UNORM && format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; }), "BGRA8 sRGB-nonlinear surface format is unavailable"); + std::uint32_t presentModeCount = 0; + auto getPresentModes = state->InstanceFunction("vkGetPhysicalDeviceSurfacePresentModesKHR"); + check(getPresentModes(selected, state->surface, &presentModeCount, nullptr), "vkGetPhysicalDeviceSurfacePresentModesKHR"); + std::vector presentModes(presentModeCount); + check(getPresentModes(selected, state->surface, &presentModeCount, presentModes.data()), "vkGetPhysicalDeviceSurfacePresentModesKHR"); + presentModes.resize(presentModeCount); + state->presentMode = SelectPresentMode(std::getenv("ANYPS5_PRESENT_MODE"), presentModes); VkSwapchainCreateInfoKHR swapchain{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR}; swapchain.surface = state->surface; swapchain.minImageCount = surface.minImageCount; @@ -1604,7 +1659,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : swapchain.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; swapchain.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; swapchain.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; - swapchain.presentMode = VK_PRESENT_MODE_FIFO_KHR; + swapchain.presentMode = state->presentMode; swapchain.clipped = VK_FALSE; check(state->DeviceFunction("vkCreateSwapchainKHR")(state->device, &swapchain, nullptr, &state->swapchain), "vkCreateSwapchainKHR"); std::uint32_t imageCount = 0; @@ -2283,7 +2338,7 @@ void VulkanDevice::Resize(std::uint32_t width, std::uint32_t height) { create.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; create.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; create.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; - create.presentMode = VK_PRESENT_MODE_FIFO_KHR; + create.presentMode = state->presentMode; create.oldSwapchain = state->swapchain; state->retiredSwapchains.reserve(state->retiredSwapchains.size() + 1); VkSwapchainKHR replacement = VK_NULL_HANDLE; @@ -2469,6 +2524,7 @@ FrameDumps& Dumps() { bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { static const bool storeAtFlip = Graphics::StorageTexture::StoreAtFlipRequested(std::getenv("APS5_STORE_AT_FLIP")); if (storeAtFlip) Graphics::StorageTexture::FlushAllPending("flip"); + else Graphics::StorageTexture::FlushDumpSelected("flip"); if (buffer.tilingMode == 1) { const auto pixels = ReadDisplayBuffer(buffer); return present(buffer.width, buffer.height, true, pixels); @@ -2907,6 +2963,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.narrowSubgroupClock = state->narrowSubgroupClock; target.float64DenormPreserve = state->float64DenormPreserve; target.fixedPushSlots = state->graphicsPipelineLibrary; + target.registrationModules = state->graphicsPipelineLibrary; target.srgbDecodeFormats = state->srgbDecodeFormats; if (state->meshShader) { const auto& mesh = state->meshLimits; @@ -3012,6 +3069,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.imageInt64Atomics = state->imageInt64Atomics; context.geometryShader = state->geometryShader; context.sampleRateShading = state->sampleRateShading; + context.shaderClipDistance = state->shaderClipDistance; context.nullDescriptors = state->shaderProfile != nullptr && state->shaderProfile->NullDescriptors(); context.bufferInt64Atomics = state->bufferInt64Atomics; context.placeholderImages = state->placeholderImages.get(); @@ -3019,6 +3077,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.imageViewMinLod = state->imageViewMinLod; context.pipelineExecutableInfo = state->pipelineExecutableInfo; context.srgbDecodeFormats = state->srgbDecodeFormats; + context.sampleLocationSampleCounts = state->sampleLocationSampleCounts; context.memoryProperties2 = state->memoryBudget ? state->InstanceFunction("vkGetPhysicalDeviceMemoryProperties2") : nullptr; context.singlePassStorage = state->storageImageWithoutFormat; return context; @@ -3036,6 +3095,44 @@ void VulkanDevice::ColorMetadataPass(const Graphics::ColorMetadataPass& pass) { Graphics::RunColorMetadataPass(graphicsContext(), pass); } +bool VulkanDevice::PrebuildGraphicsLibraries(const Graphics::ShaderStages& stages, std::span shaders) { + PerformanceTimer timing("Vulkan.GraphicsPipelinePrebuild"); + const auto context = graphicsContext(); + if (!context.graphicsPipelineLibrary || context.descriptorCache == nullptr) return false; + Graphics::State state{}; + state.stages = stages; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.color.format = VK_FORMAT_R8G8B8A8_UNORM; + state.color.exportIndex = 0; + state.colors.push_back(state.color); + state.hasColorTarget = true; + state.blend.colorWriteMask = 15; + state.blends.push_back(state.blend); + state.renderExtent = {1, 1}; + std::vector group(shaders.begin(), shaders.end()); + if (group.size() == 1u) { + const bool vertex = group.front().stage == ShaderRecompiler::ShaderStage::Fragment; + static const auto stub = [](bool vertexStub) { + auto result = std::make_shared(); + const std::uint32_t model = vertexStub ? 0u : 4u; + std::vector words{0x07230203u, 0x00010000u, 0u, 6u, 0u, 0x00020011u, 1u, 0x0003000eu, 0u, 1u, 0x0005000fu, model, 4u, 0x6e69616du, 0u}; + if (!vertexStub) words.insert(words.end(), {0x00030010u, 4u, 7u}); + words.insert(words.end(), {0x00020013u, 2u, 0x00030021u, 3u, 2u, 0x00050036u, 2u, 4u, 0u, 3u, 0x000200f8u, 5u, 0x000100fdu, 0x00010038u}); + result->spirv = std::move(words); + result->variantId = vertexStub ? ~std::uint64_t{0} - 1u : ~std::uint64_t{0} - 2u; + result->runtimeAbiVersion = ShaderRecompiler::RuntimeAbi::Version; + return result; + }; + static const auto vertexStub = stub(true); + static const auto fragmentStub = stub(false); + if (vertex) group.insert(group.begin(), {ShaderRecompiler::ShaderStage::Vertex, vertexStub.get(), 0u}); + else group.push_back({ShaderRecompiler::ShaderStage::Fragment, fragmentStub.get(), Graphics::PipelinePushSlotBytes}); + } + const auto layouts = Graphics::DescribeSetLayouts(context, group); + return Graphics::Pipeline::Prebuild(context, state, Graphics::VertexInputLayout{}, layouts.View(), group); +} + void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots, std::shared_ptr* recipe) { for (const auto& shader : shaders) { require(shader.program != nullptr, "missing compiled shader"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp index 575de2335..d5a82a6ed 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_COLORTARGETLAYOUT_HPP #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include #include #include @@ -33,6 +34,7 @@ constexpr TextureTileMode ColorTextureTileMode(ColorTileMode mode) { } ColorTileMode DecodeColorTileMode(std::uint32_t attrib3); +TileMipLayout ComputeColorTargetMipLayout(ColorTileMode mode, std::uint32_t bytesPerElement, std::uint32_t surfaceWidth, std::uint32_t surfaceHeight, std::uint32_t mipCount, std::uint32_t mipLevel); class ColorTargetLayout { public: diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 38049d9e6..6ca5f6c43 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -5,7 +5,9 @@ #define VK_NO_PROTOTYPES #endif #include +#include #include +#include #include #include #include @@ -158,6 +160,7 @@ struct Context { bool imageInt64Atomics = false; bool geometryShader = false; bool sampleRateShading = false; + bool shaderClipDistance = false; bool nullDescriptors = false; bool bufferInt64Atomics = false; const PlaceholderImages* placeholderImages = nullptr; @@ -170,6 +173,7 @@ struct Context { bool provokingVertexLast = false; bool provokingVertexModePerPipeline = false; bool singlePassStorage = false; + VkSampleCountFlags sampleLocationSampleCounts = 0; template TFunction Function(const char* name) const { @@ -193,6 +197,28 @@ struct Context { } throw std::runtime_error("AGC graphics: required Vulkan memory type is unavailable"); } + + std::uint32_t DeviceMemoryType(std::uint32_t mask, VkDeviceSize bytes, const char* what) const { + const auto type = MemoryType(mask, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + if (memoryProperties2 == nullptr || physical == VK_NULL_HANDLE) return type; + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2}; + properties.pNext = &reported; + memoryProperties2(physical, &properties); + const auto heap = memory.memoryTypes[type].heapIndex; + const auto budget = reported.heapBudget[heap]; + const auto usage = reported.heapUsage[heap]; + if (budget == 0 || (usage < budget && bytes <= budget - usage)) return type; + for (std::uint32_t i = 0; i < memory.memoryTypeCount; ++i) { + if ((mask & (1u << i)) == 0 || (memory.memoryHeaps[memory.memoryTypes[i].heapIndex].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0) continue; + static std::atomic placements{0}, placedBytes{0}; + const auto count = placements.fetch_add(1) + 1; + const auto total = placedBytes.fetch_add(bytes) + bytes; + if (count <= 5 || (count & (count - 1)) == 0) std::fprintf(stderr, "[gpu] %s: device-local heap %u uses %llu of its %llu MiB budget, %llu bytes placed in host memory type %u; %llu such allocations, %llu MiB\n", what, heap, static_cast(usage >> 20u), static_cast(budget >> 20u), static_cast(bytes), i, static_cast(count), static_cast(total >> 20u)); + return i; + } + return type; + } }; inline void EndRenderPass(const Context& context, VkCommandBuffer commands) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index f5b6cad4a..4b42880ec 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -13,9 +13,11 @@ namespace AgcDriver::Graphics { class Texture; VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target); +bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std::uint32_t samples); std::uint64_t DepthSliceBytes(VkExtent2D extent, std::uint32_t bytesPerTexel); void ClearDepthSurfaces(VkDevice device); bool DepthSurfaceAt(std::uint64_t address); +bool DepthSurfaceAt(const Context& context, std::span words, const GuestTextureResource& resource); std::uint64_t HtileDepthClearAddress(std::span code, std::span userData, const std::array& numThreads); void NoteHtileDepthClear(std::uint64_t htileAddress); bool DepthSurfaceHolds(const Context& context, const GuestTextureResource& resource, const Texture* texture); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp index e59172f7c..d852d6743 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp @@ -32,6 +32,11 @@ DrawInputCopy CopyDrawInput(const Context& context, Recorder* recorder, std::uin DrawInputCopy CopyZeroPaddedDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t validBytes); void KeepDrawInput(Recorder* recorder, std::uint64_t address, const DrawInputCopy& copy, Recorder::SnapshotUse use, std::uint32_t derived); const HostImport* InPlaceDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t alignment); +struct InPlaceIndices { + std::optional highest; + bool reused = false; +}; +InPlaceIndices InPlaceIndexRange(Recorder& recorder, std::uint64_t address, std::size_t bytes, std::uint32_t indexSize, bool skipRestart); std::array MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index bb817d437..6e6ca7fdf 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -41,7 +41,9 @@ public: // before, during and after the pass (GENERAL for resident targets, which then need no // transitions). Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); ~Pipeline(); + static bool Prebuild(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders); Pipeline(const Pipeline&) = delete; Pipeline& operator=(const Pipeline&) = delete; VkPipelineLayout Layout() const; @@ -87,6 +89,7 @@ private: VkFormat depthFormat = VK_FORMAT_UNDEFINED; VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_UNDEFINED; std::vector colorSlots; + bool sampleLocationsEnabled = false; std::vector framebuffers; }; @@ -97,6 +100,15 @@ private: // only evicted once no recorded draw holds them. Debug aid: APS5_NO_PIPELINE_CACHE=1 builds one per // draw as before. std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); +std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); +struct DescribedSetLayouts { + PipelineSetLayouts layouts; + std::vector mainKey; + std::vector fragmentKey; + PipelineSetLayouts View() const { return {layouts.main, layouts.fragment, layouts.split, mainKey, fragmentKey}; } +}; +DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span shaders); +bool SplitsSets(const Context& context, std::span shaders); // Destroys the cached pipelines of a device; to be called before the device goes away. Without it, // entries of a gone device are recognised by their buffer pool (made and reset with the device, so // it tells device instances apart when the loader reuses a VkDevice handle) and forgotten unused. @@ -106,11 +118,12 @@ bool HasStencil(VkFormat format); void ValidateViewport(const Context& context, const VkViewport& viewport); void ValidateDepthBounds(const Context& context, const State& state); void ValidateProvokingVertex(const Context& context, const State& state, std::span shaders = {}); +bool ValidateSampleLocations(const Context& context, const State& state); void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment); bool ShadersWriteMemory(std::span shaders); // Returns the color attachment locations the pixel shader writes. -std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false); +std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false, bool shaderClipDistance = false); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp index f480042a3..1d340829c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp @@ -19,6 +19,8 @@ struct PipelineLibraryKeys { std::vector fragmentOutput; std::vector renderPass; std::vector layout; + std::vector preRasterizationLayout; + std::vector fragmentLayout; }; std::span PipelineLibraryDynamicStates(); @@ -31,7 +33,7 @@ struct OptimizedPipeline { void Release() noexcept; }; -VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized = nullptr, std::span modules = {}); +VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized = nullptr, bool partsOnly = false, std::span modules = {}); void WaitForOptimizedPipelines(VkDevice device); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 7a863048d..2954c20f7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -99,6 +99,10 @@ public: static constexpr std::size_t DrawInputEntries = 16384; std::shared_ptr ReusableDrawSnapshot(std::uint64_t address, std::size_t bytes, SnapshotUse use = SnapshotUse::Storage, std::uint32_t* derived = nullptr, std::uint64_t generation = 0); void KeepDrawSnapshot(std::uint64_t address, std::size_t bytes, std::uint64_t generation, std::uint64_t registryGeneration, std::shared_ptr buffer, SnapshotUse use = SnapshotUse::Storage, std::uint32_t derived = 0); + static constexpr std::size_t IndexRangeEntries = 16384; + static constexpr std::size_t KeptIndexRangeIndices = 4096; + bool KnownHighestIndex(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::optional& highest); + void KeepHighestIndex(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::uint64_t generation, std::uint64_t registryGeneration, std::optional highest); void OnComplete(std::function action); void NotePendingWrite(std::uint64_t address, std::size_t bytes); void NotePendingFill(std::uint64_t address, std::size_t bytes, std::uint8_t value); @@ -808,6 +812,14 @@ private: std::map drawSnapshots; std::array drawSnapshotPools; void eraseDrawSnapshot(std::map::iterator entry); + struct IndexRange { + std::uint64_t generation; + std::uint64_t registryGeneration; + std::list::iterator recent; + std::optional highest; + }; + std::map indexRanges; + std::list indexRangeRecency; bool refreshDrawSnapshot(DrawSnapshot& entry, std::uint64_t address, std::size_t bytes); }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 014a06035..7b29f6df0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -51,6 +51,14 @@ bool StorageImageServesKeys(const StorageTexture& image, std::uint64_t dccAddres // a storage image other than `except` has results pending in [address, address + bytes). bool PendingStorageOverlaps(std::uint64_t address, std::size_t bytes, const StorageTexture* except); +struct PipelineSetLayouts { + VkDescriptorSetLayout main = VK_NULL_HANDLE; + VkDescriptorSetLayout fragment = VK_NULL_HANDLE; + bool split = false; + std::span mainKey; + std::span fragmentKey; +}; + class DescriptorCache { public: explicit DescriptorCache(const Context& context); @@ -117,6 +125,9 @@ public: std::vector LayoutBindings() const; bool LayoutUpdateAfterBind() const; std::span LayoutBindingFlags() const { return layoutBindingFlags; } + VkDescriptorSetLayout FragmentLayout() const { return fragment.layout; } + bool SplitSets() const { return splitSets; } + PipelineSetLayouts SetLayouts() const { return {_layout, fragment.layout, splitSets, layoutKey, fragmentLayoutKey}; } void Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const; struct DrawBindings { DrawBindings() = default; @@ -127,6 +138,7 @@ public: std::shared_ptr buffer; }; DescriptorCache* cache = nullptr; + DescriptorCache::SetAllocation fragmentAllocation; DescriptorCache::SetAllocation allocation; std::vector snapshots; ~DrawBindings(); @@ -165,6 +177,7 @@ public: bool ReadsImage(const StorageTexture* image) const; bool HasCachedStorageImages() const; const std::vector& LayoutKey() const { return layoutKey; } + const std::vector& FragmentLayoutKey() const { return fragmentLayoutKey; } // Debug aid: each bound guest resource with the fraction of sampled bytes that are nonzero. std::string Describe() const; @@ -428,6 +441,17 @@ private: VkDescriptorPool pool = VK_NULL_HANDLE; // The cache pool the set was allocated from, freed back to it on release. DescriptorCache::SetAllocation cacheAllocation; + struct SetObjects { + VkDescriptorSetLayout layout = VK_NULL_HANDLE; + bool ownsLayout = false; + VkDescriptorSet set = VK_NULL_HANDLE; + VkDescriptorPool pool = VK_NULL_HANDLE; + DescriptorCache::SetAllocation cacheAllocation; + }; + bool splitSets = false; + std::vector fragmentLayoutKey; + SetObjects fragment; + std::uint32_t setOf(const Binding& binding) const; std::vector allocations; std::vector descriptorBuffers; std::vector descriptorImages; @@ -479,6 +503,7 @@ private: // the descriptor counts the set was sized for, and the compute stage of a deferred build. std::vector bindings; std::vector layoutBindingFlags; + std::vector fragmentBindingFlags; std::vector refreshResourceKey; struct DeferredImages { const ShaderRecompiler::DescriptorBinding* binding; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index bb18791e6..36a8348cc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -116,10 +116,13 @@ struct State { VkPipelineColorBlendAttachmentState blend; std::array blendConstants; std::uint32_t samples = 1; + std::array sampleLocations{}; + bool customSampleLocations = false; }; ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup = false); State DecodeState(const QueueState& queue, bool vertexWorkgroup = false); +std::array SampleLocations(const State& state); std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot); @@ -134,6 +137,7 @@ struct ColorMetadataPass { }; std::optional DecodeColorMetadataPass(const QueueState& queue); std::string DepthMaintenanceRejection(const QueueState& queue); +bool DepthResummarizeDrawsNothing(const QueueState& queue); // The message DecodeState (or the pixel stage decode after it) would throw for the register rules // this precheck covers, evaluated without exceptions before the draw is decoded; empty when they // pass (DecodeState still checks everything). A register a rule needs that is absent is no verdict. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index a2d84f038..a01cd7efc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -178,6 +178,7 @@ public: static bool FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except = nullptr, const char* reason = "memory access", PublishScope scope = PublishScope::Whole, bool* published = nullptr); static void FlushAllPending(const char* reason); static bool AnyPending(); + static void FlushDumpSelected(const char* reason); static bool StoreAtFlipRequested(const char* value); // See PendingSerial: a change of a surface's source outside the registry (a unit shadow // retile) moves it too. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp index 0bc1a7c21..c5de807ad 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp @@ -227,6 +227,45 @@ inline VertexCopyPlan PlanVertexCopies(std::span fetches) { return plan; } +inline std::optional ZeroFetchVertex(std::span attributes) { + std::uint32_t zeroFrom = 0; + for (const auto& attribute : attributes) { + const auto stride = (attribute.resource.fields[1] >> 16u) & 0x3fffu; + if (NullVertexDescriptor(attribute) || attribute.fetchIndex != 0 || stride == 0) continue; + if (VertexBufferOutOfBoundsSelect(attribute) > 1u) return std::nullopt; + zeroFrom = std::max(zeroFrom, attribute.resource.fields[2]); + } + return zeroFrom; +} + +inline std::optional ClampDrawIndices(std::span indices, std::uint32_t indexSize, bool skipRestart, std::uint32_t limit) { + Require(indexSize == 2 || indexSize == 4, "unsupported index size"); + const auto restartIndex = indexSize == 2 ? 0xffffu : 0xffffffffu; + std::optional highest; + for (std::size_t offset = 0; offset + indexSize <= indices.size(); offset += indexSize) { + std::uint32_t index = 0; + if (indexSize == 2) { + std::uint16_t value = 0; + std::memcpy(&value, indices.data() + offset, sizeof(value)); + index = value; + } else { + std::memcpy(&index, indices.data() + offset, sizeof(index)); + } + if (skipRestart && index == restartIndex) continue; + if (index > limit) { + index = limit; + if (indexSize == 2) { + const auto value = static_cast(index); + std::memcpy(indices.data() + offset, &value, sizeof(value)); + } else { + std::memcpy(indices.data() + offset, &index, sizeof(index)); + } + } + highest = std::max(highest.value_or(0u), index); + } + return highest; +} + inline std::optional HighestDrawIndex(std::span indices, std::uint32_t indexSize, bool skipRestart) { Require(indexSize == 2 || indexSize == 4, "unsupported index size"); const auto restartIndex = indexSize == 2 ? 0xffffu : 0xffffffffu; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp index c406e4b6c..832305195 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" #include #include @@ -52,6 +53,19 @@ ColorTileMode DecodeColorTileMode(std::uint32_t attrib3) { return static_cast(mode); } +TileMipLayout ComputeColorTargetMipLayout(ColorTileMode mode, std::uint32_t bytesPerElement, std::uint32_t surfaceWidth, std::uint32_t surfaceHeight, std::uint32_t mipCount, std::uint32_t mipLevel) { + auto mips = ComputeElementMipLayout(ColorTextureTileMode(mode), bytesPerElement, surfaceWidth, surfaceHeight, mipCount); + require(mipLevel < mips.size(), "AGC graphics: color target mip is outside its surface"); + auto mip = mips[mipLevel]; + const ColorTargetLayout target(mip.width, mip.height, mode, bytesPerElement); + require(mip.tiledSize == target.Bytes(), "AGC graphics: color target mip tiling differs from its surface layout"); + mip.tiledOffset = 0; + mip.linearOffset = 0; + mip.pitchBytes = mip.width * bytesPerElement; + mip.linearSize = target.LinearBytes(); + return mip; +} + namespace { struct SwizzleTables { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 0b5d5eb79..406470b23 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -34,6 +34,7 @@ public: const VkImageAspectFlags aspects = VK_IMAGE_ASPECT_DEPTH_BIT | (target.stencilAddress != 0 ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); try { VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + if (SampleLocationsCompatibleDepth(context, target.format, target.samples)) info.flags = VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT; info.imageType = VK_IMAGE_TYPE_2D; info.format = target.format; info.extent = {target.extent.width, target.extent.height, 1}; @@ -187,6 +188,16 @@ std::uint64_t DepthSliceBytes(VkExtent2D extent, std::uint32_t bytesPerTexel) { return width * height * bytesPerTexel; } +bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std::uint32_t samples) { + if (samples <= 1 || (context.sampleLocationSampleCounts & samples) == 0) return false; + VkImageFormatProperties properties{}; + const auto result = context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, + VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT, &properties); + if (result == VK_ERROR_FORMAT_NOT_SUPPORTED) return false; + Check(result, "vkGetPhysicalDeviceImageFormatProperties sample locations depth"); + return (properties.sampleCounts & samples) != 0; +} + VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) { std::lock_guard lock(surfacesMutex()); const bool cleared = target.htileAddress != 0 && clearedHtiles().erase(target.htileAddress) != 0; @@ -284,6 +295,22 @@ void NoteDepthMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32 } } +bool DepthSurfaceAt(const Context& context, std::span words, const GuestTextureResource& resource) { + std::lock_guard lock(surfacesMutex()); + const auto& list = surfaces(); + const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { + return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); + }); + if (found == list.rend()) return false; + const auto& target = (*found)->target; + if (resource.width != target.extent.width || resource.height != target.extent.height) return false; + const bool stencil = target.stencilAddress != 0 && target.stencilAddress == resource.baseAddress; + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; + const bool depthBits = !stencil && words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u); + return ResolveTextureFormat(resource.format) == expected || depthBits; +} + bool DepthSurfaceAt(std::uint64_t address) { std::lock_guard lock(surfacesMutex()); return std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& surface) { return surface->target.address == address || surface->target.stencilAddress == address; }); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index b1fe81c22..3cb6a005c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -50,6 +50,11 @@ std::uint64_t DrawRenderPassKey(const Context& context, const State& state, std: } mix(state.renderExtent.width); mix(state.renderExtent.height); + mix(state.samples); + mix(state.customSampleLocations); + if (state.customSampleLocations) { + for (const auto location : state.sampleLocations) mix(location); + } if (!context.provokingVertexModePerPipeline) mix(state.provokingVertexMode); return key; } @@ -732,7 +737,7 @@ std::set CachedFragmentOutputs(const Context& context, std::span< } std::set outputs; try { - outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics); + outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics, context.shaderClipDistance); } catch (const std::exception& error) { if (keyed) { std::lock_guard lock(validationMutex()); @@ -971,6 +976,25 @@ const HostImport* InPlaceDrawInput(const Context& context, std::uint64_t address return import; } +InPlaceIndices InPlaceIndexRange(Recorder& recorder, std::uint64_t address, std::size_t bytes, std::uint32_t indexSize, bool skipRestart) { + const std::span range(reinterpret_cast(address), bytes); + InPlaceIndices indices; + if (bytes < Recorder::KeptIndexRangeIndices * indexSize) { + indices.highest = HighestDrawIndex(range, indexSize, skipRestart); + return indices; + } + const auto use = indexSize == 2 ? (skipRestart ? Recorder::SnapshotUse::Index16Restart : Recorder::SnapshotUse::Index16) : (skipRestart ? Recorder::SnapshotUse::Index32Restart : Recorder::SnapshotUse::Index32); + const auto registryGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + const auto generation = GuestMemory::CollectWrites(address, bytes); + if (generation != 0 && recorder.KnownHighestIndex(address, bytes, use, indices.highest)) { + indices.reused = true; + return indices; + } + indices.highest = HighestDrawIndex(range, indexSize, skipRestart); + recorder.KeepHighestIndex(address, bytes, use, generation, registryGeneration, indices.highest); + return indices; +} + namespace { // The draw's inputs before its resources (prepareDrawInputs): the validated parameters, the index @@ -1077,10 +1101,11 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P if (import == nullptr) copy = CopyDrawInput(context, context.recorder, draw.indexAddress, static_cast(indexBytes), draw.indexSize, use); else GuestMemory::CheckRange(reinterpret_cast(draw.indexAddress), static_cast(indexBytes), draw.indexSize); std::optional highest; - if (!copy.reused) { - const auto bytes = import != nullptr ? std::span(reinterpret_cast(draw.indexAddress), static_cast(indexBytes)) : copy.buffer->Bytes().first(static_cast(indexBytes)); - highest = HighestDrawIndex(bytes, draw.indexSize, skipRestart); - if (import == nullptr) KeepDrawInput(context.recorder, draw.indexAddress, copy, use, skipRestart ? (highest ? *highest + 1u : 0u) : highest.value_or(0u)); + if (import != nullptr) { + highest = InPlaceIndexRange(*context.recorder, draw.indexAddress, static_cast(indexBytes), draw.indexSize, skipRestart).highest; + } else if (!copy.reused) { + highest = HighestDrawIndex(copy.buffer->Bytes().first(static_cast(indexBytes)), draw.indexSize, skipRestart); + KeepDrawInput(context.recorder, draw.indexAddress, copy, use, skipRestart ? (highest ? *highest + 1u : 0u) : highest.value_or(0u)); } else if (!skipRestart) { highest = copy.derived; } else if (copy.derived != 0) { @@ -1090,6 +1115,24 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P inputs.nothing = true; return inputs; } + constexpr std::uint32_t ZeroFetchSlack = 0x10000u; + if (const auto zeroFrom = state.stages.mesh ? std::nullopt : ZeroFetchVertex(shaders.front().program->vertexAttributes)) { + const auto limit = *zeroFrom > draw.firstVertex ? *zeroFrom - draw.firstVertex : 0u; + if (*highest >= limit && *highest - limit >= ZeroFetchSlack) { + GuestMemory::FlushGpuWrites(draw.indexAddress, static_cast(indexBytes)); + auto clamped = std::make_shared(context, static_cast(indexBytes), VK_BUFFER_USAGE_INDEX_BUFFER_BIT); + const auto bytes = clamped->Bytes().first(static_cast(indexBytes)); + GuestMemory::Read(draw.indexAddress, bytes, draw.indexSize); + highest = ClampDrawIndices(bytes, draw.indexSize, skipRestart, limit); + import = nullptr; + copy = DrawInputCopy{}; + copy.buffer = std::move(clamped); + if (!highest) { + inputs.nothing = true; + return inputs; + } + } + } Require(*highest <= context.limits.maxDrawIndexedIndexValue, "index exceeds the device's indexed draw limit"); Require(!fanGeometry || !state.primitiveRestart || *highest != (draw.indexSize == 2 ? 0xffffu : 0xffffffffu), "primitive restart in a triangle fan geometry draw is unsupported"); inputs.maxIndex = *highest; @@ -1739,7 +1782,9 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam APS5_LOG_CHARS_OUT_DEBUG("Pipeline Begin OK"); if (drawBindings != nullptr) { const auto set = drawBindings->allocation.set; - context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout(), 0, 1, &set, 0, nullptr); + const auto fragmentSet = drawBindings->fragmentAllocation.set; + if (set != VK_NULL_HANDLE) context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout(), 0, 1, &set, 0, nullptr); + if (fragmentSet != VK_NULL_HANDLE) context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout(), ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet, 1, &fragmentSet, 0, nullptr); } else { resources.Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout()); } @@ -1924,11 +1969,11 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& continue; } materializeCmaskClear(context, color, nullptr); - Require(!color.mipTail, "rendering into a packed mip tail needs the resident image of its surface"); + Require(!color.mipTail || binding.gpuTiling, "rendering into a packed mip tail requires the GPU tiled fallback"); Require(color.depth == 1, "rendering into a 3D color target needs the resident image of its surface"); Require(color.arraySlice == 0 || !ColorTileModeIsXor(color.tileMode), "rendering into a slice of a 2D array color target with an XOR tile mode needs the resident image of its array"); if (binding.gpuTiling) { - binding.mip = ColorTargetMip(color, colorLayout); + binding.mip = color.mipTail ? ComputeColorTargetMipLayout(color.tileMode, color.elementBytes, color.surfaceExtent.width, color.surfaceExtent.height, color.mipCount, color.mip) : ColorTargetMip(color, colorLayout); binding.tiled = std::make_unique(context, colorLayout.Bytes(), copies); binding.tiledDevice = std::make_unique(context, colorLayout.Bytes(), copies | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); binding.linearDevice = std::make_unique(context, colorLayout.LinearBytes(), copies | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); @@ -2091,6 +2136,12 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& masked->blends.at(state.colors[index].exportIndex).colorWriteMask &= VK_COLOR_COMPONENT_R_BIT; } const State& pipelineState = masked.has_value() ? *masked : state; + static const bool verifySetLayouts = std::getenv("APS5_VERIFY_SET_LAYOUTS") != nullptr; + if (verifySetLayouts && context.descriptorCache != nullptr) { + const auto described = DescribeSetLayouts(context, shaders); + const auto built = resources->SetLayouts(); + Require(described.layouts.main == built.main && described.layouts.fragment == built.fragment && described.layouts.split == built.split && std::ranges::equal(described.mainKey, built.mainKey) && std::ranges::equal(described.fragmentKey, built.fragmentKey), "set layouts described from the shaders differ from the draw's"); + } auto pipeline = CachedPipeline(context, pipelineState, inputs.vertexInput, *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); // Resident targets' views are stable while their storage image lives, so the framebuffer is // reused with the pipeline; a per-draw RenderTarget gets a framebuffer of its own. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 73e1015e5..17d00a1be 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -142,6 +142,12 @@ bool addressSpaceCacheEnabled() { return !disabled; } +struct ImportFailure { + std::uint64_t base; + std::uint64_t bytes; + GuestAllocations::Mapped ranges; +}; + // Imports persist across draws, keyed by allocation base, and are dropped when their allocation leaves // the registered set; a dropped import is destroyed once the recorded work that may read it completed. struct HostImports { @@ -152,7 +158,7 @@ struct HostImports { std::map imports; std::map>> parked; std::uint64_t liveBytes = 0; - std::set failed; + std::map failed; std::set unaliasedSpans; // Registry generation the imports were last reconciled with. std::uint64_t refreshedGeneration = 0; @@ -219,6 +225,26 @@ void retireImport(VkDevice device, HostImports& state, std::map> released; + { + std::lock_guard lock(state.mutex); + for (auto device = state.parked.begin(); device != state.parked.end();) { + auto& list = device->second; + for (auto it = list.begin(); it != list.end();) { + if ((*it)->entry.base < end && begin < (*it)->entry.base + (*it)->entry.bytes) { + released.push_back(std::move(*it)); + it = list.erase(it); + } else { + ++it; + } + } + device = list.empty() ? state.parked.erase(device) : std::next(device); + } + } + return !released.empty(); +} + const char* bindImport(const Context& context, HostImport& entry, VkExternalMemoryHandleTypeFlagBits handleType, const void* import, std::uint32_t importTypes, bool& allocated, VkResult& failure) { const auto failed = [&](const char* step, VkResult result) -> const char* { if (entry.buffer) context.Function("vkDestroyBuffer")(context.device, entry.buffer, nullptr); @@ -595,7 +621,8 @@ bool runCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::u return !run.ranges.empty() && end <= run.base + run.bytes; } -bool sameRun(const HostImport& entry, const ImportRun& run) { +template +bool sameRun(const Entry& entry, const ImportRun& run) { if (run.base != entry.base || run.bytes != entry.bytes || run.ranges.size() != entry.ranges.size()) return false; for (std::size_t index = 0; index < run.ranges.size(); ++index) { if (entry.ranges[index].owner_before(run.ranges[index]) || run.ranges[index].owner_before(entry.ranges[index])) return false; @@ -629,7 +656,11 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s } if (auto it = state.imports.lower_bound(base); it != state.imports.begin() && std::prev(it)->second.base + std::prev(it)->second.bytes > base) retireImport(context.device, state, std::prev(it), lease); const auto alignment = context.hostImportAlignment; - if (alignment == 0 || base % alignment != 0 || bytes % alignment != 0 || state.failed.contains(base)) return nullptr; + if (alignment == 0 || base % alignment != 0 || bytes % alignment != 0) return nullptr; + if (const auto failed = state.failed.find(base); failed != state.failed.end()) { + if (sameRun(failed->second, run)) return nullptr; + state.failed.erase(failed); + } // Pinned imports count against the driver's system memory budget; past it ordinary host // allocations fail, so imports stop at APS5_HOST_IMPORT_MIB (default 6 GiB, which covers the // registered memory of address-based shaders; past it they copy gigabytes per dispatch). @@ -661,7 +692,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s for (std::uint64_t cursor = base; cursor < base + bytes;) { MEMORY_BASIC_INFORMATION info{}; if (VirtualQuery(reinterpret_cast(cursor), &info, sizeof(info)) == 0 || info.State != MEM_COMMIT || (info.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) { - state.failed.insert(base); + state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); return nullptr; } const auto protection = info.Protect & 0xffu; @@ -673,7 +704,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s cursor = reinterpret_cast(info.BaseAddress) + info.RegionSize; } if (!writable && readOnly) { - state.failed.insert(base); + state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); return nullptr; } if (!writable) { @@ -692,7 +723,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s } #else if (!GuestMemory::Accessible(reinterpret_cast(base), static_cast(bytes), true)) { - state.failed.insert(base); + state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); return nullptr; } #endif @@ -712,7 +743,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s #ifdef _WIN32 GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias); #endif - state.failed.insert(base); + state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx failed at %s (%d); falling back to copies\n", static_cast(base), static_cast(bytes), step, static_cast(result)); #ifdef _WIN32 static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr; @@ -800,7 +831,7 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc retireImport(context.device, state, it, lease); it = next; } - for (auto it = state.failed.begin(); it != state.failed.end();) it = leasedRangeAt(lease, *it) != nullptr ? std::next(it) : state.failed.erase(it); + for (auto it = state.failed.begin(); it != state.failed.end();) it = sameRun(it->second, importRun(lease, it->first)) ? std::next(it) : state.failed.erase(it); } const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end) { @@ -1534,6 +1565,7 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) noexcept if (recorder != nullptr) recorder->FlushDeferredReleases(); const auto end = static_cast(address) + bytes; auto& imports = Imports(); + if (releaseParkedImports(imports, address, end)) retiredImport = true; const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); { std::lock_guard importsLock(imports.mutex); @@ -1585,6 +1617,7 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) noexcept const auto end = static_cast(address) + bytes; auto& imports = Imports(); + if (releaseParkedImports(imports, address, end)) retiredImport = true; bool overlapsImport = false; { std::lock_guard importsLock(imports.mutex); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 8fea5aec0..8e854983e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include #include #include @@ -152,6 +153,12 @@ GuestTextureResource DecodeTextureResource(std::span words) Require(height == 1 && depth == 0 && baseArray == 0, "guest 1D texture descriptor has a nonzero height, depth or base array"); break; case TextureDimension::k2D: + if (depth != 0 && tileMode == TextureTileMode::kLinear) { + const auto elementsPerPitchUnit = 256u / BytesPerElement(format); + const auto defaultPitch = (width + elementsPerPitchUnit - 1u) / elementsPerPitchUnit * elementsPerPitchUnit; + Require(!IsBlockCompressed(format) && maxMip == 0 && baseArray == 0 && depth + 1u == defaultPitch, "guest 2D linear texture descriptor sets a custom pitch other than its default pitch, which is not implemented" + describe()); + depth = 0; + } Require(depth == 0 && baseArray == 0, "guest 2D texture descriptor has a nonzero depth or base array"); break; case TextureDimension::k3D: diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 27bfd7ee9..069809184 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include @@ -94,7 +95,13 @@ void ValidateDepthBounds(const Context& context, const State& state) { void ValidateProvokingVertex(const Context& context, const State& state, std::span shaders) { Require(state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT || state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT, "invalid provoking vertex mode"); - if (state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT) return; + if (state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT) { + for (const auto& shader : shaders) { + if (shader.stage != ShaderRecompiler::ShaderStage::Fragment || shader.program == nullptr) continue; + Require(std::none_of(shader.program->fragmentParameters.begin(), shader.program->fragmentParameters.end(), [](const ShaderRecompiler::FragmentParameter& parameter) { return parameter.flatP10; }), "v_interp_mov p10 of a flat input with the first provoking vertex is not measured"); + } + return; + } Require(context.provokingVertexLast, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); Require(!state.rectList && state.stages.path == ShaderPath::Vertex && !state.stages.mesh && !state.stages.tessellation, "last provoking vertex is unsupported for generated primitive pipelines"); Require(state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "last provoking vertex is unsupported for this primitive topology"); @@ -106,6 +113,14 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp } } +bool ValidateSampleLocations(const Context& context, const State& state) { + const bool extensionSupportsSamples = (context.sampleLocationSampleCounts & state.samples) != 0; + Require(!state.customSampleLocations || extensionSupportsSamples, "custom sample locations require VK_EXT_sample_locations support for the draw's sample count"); + const bool enabled = extensionSupportsSamples && (!state.depth || SampleLocationsCompatibleDepth(context, state.depth->format, state.depth->samples)); + Require(!state.customSampleLocations || enabled, "custom sample locations require a compatible depth format and sample count"); + return enabled; +} + void ValidateViewport(const Context& context, const VkViewport& viewport) { Require(std::isfinite(viewport.minDepth) && std::isfinite(viewport.maxDepth), "non-finite viewport depth range"); Require(context.depthRangeUnrestricted || (viewport.minDepth >= 0 && viewport.minDepth <= 1 && viewport.maxDepth >= 0 && viewport.maxDepth <= 1), "viewport depth outside [0, 1] requires VK_EXT_depth_range_unrestricted"); @@ -143,6 +158,9 @@ bool ShadersWriteMemory(std::span shaders) { namespace { +thread_local bool prebuilding = false; +thread_local bool prebuilt = false; + VkShaderStageFlags pushStagesOf(const Context& context) { return PreRasterizationPushStages(context.meshShader, context.tessellationShader, context.geometryShader); } @@ -154,7 +172,7 @@ void appendKey(std::vector& key, const TValue& value) { key.insert(key.end(), bytes.begin(), bytes.end()); } -bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& state, const VertexInputLayout& input, const ShaderResources& resources, std::span shaders) { +bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& state, const VertexInputLayout& input, const PipelineSetLayouts& resources, std::span shaders) { using Stage = ShaderRecompiler::ShaderStage; if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || SplitsAlternateAlpha(state)) return false; keys = {}; @@ -188,6 +206,17 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& if (state.stages.tessellation) appendKey(keys.preRasterization, state.stages.tessellation->inputControlPoints); appendKey(keys.fragmentShader, state.samples); appendKey(keys.fragmentOutput, state.samples); + const auto sampleLocationsEnabled = ValidateSampleLocations(context, state); + appendKey(keys.fragmentShader, sampleLocationsEnabled); + appendKey(keys.fragmentOutput, sampleLocationsEnabled); + appendKey(keys.fragmentShader, state.customSampleLocations); + appendKey(keys.fragmentOutput, state.customSampleLocations); + if (state.customSampleLocations) { + for (std::uint32_t sample = 0; sample < state.samples; ++sample) { + appendKey(keys.fragmentShader, state.sampleLocations[sample]); + appendKey(keys.fragmentOutput, state.sampleLocations[sample]); + } + } for (const auto& blend : state.blends) appendKey(keys.fragmentOutput, blend); for (const auto value : state.blendConstants) appendKey(keys.fragmentOutput, value); appendKey(keys.renderPass, state.blends.size()); @@ -197,15 +226,38 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& } appendKey(keys.renderPass, state.depth.has_value()); if (state.depth) appendKey(keys.renderPass, state.depth->format); - appendKey(keys.layout, pushStagesOf(context)); - for (const auto word : resources.LayoutKey()) appendKey(keys.layout, word); + if (!resources.split) { + appendKey(keys.layout, pushStagesOf(context)); + for (const auto word : resources.mainKey) appendKey(keys.layout, word); + keys.preRasterizationLayout = keys.fragmentLayout = keys.layout; + return true; + } + const auto splitKey = [&](std::vector& key, std::uint32_t sets) { + appendKey(key, ~0u); + appendKey(key, sets); + appendKey(key, pushStagesOf(context)); + if ((sets & 1u) != 0u) { + appendKey(key, resources.mainKey.size()); + for (const auto word : resources.mainKey) appendKey(key, word); + } + if ((sets & 2u) != 0u) { + appendKey(key, resources.fragmentKey.size()); + for (const auto word : resources.fragmentKey) appendKey(key, word); + } + }; + splitKey(keys.layout, 3u); + splitKey(keys.preRasterizationLayout, 1u); + splitKey(keys.fragmentLayout, 2u); return true; } } -Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()), depthBounds(state.depth.has_value() && state.depthBoundsTest), depthBias(state.depth.has_value() && state.depthBias), dynamicRendering(context.graphicsPipelineLibrary) { +Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) : Pipeline(context, state, vertexInput, resources.SetLayouts(), shaders, attachmentLayout) {} + +Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()), depthBounds(state.depth.has_value() && state.depthBoundsTest), depthBias(state.depth.has_value() && state.depthBias), dynamicRendering(context.graphicsPipelineLibrary) { PerformanceTimer timing("Vulkan.GraphicsPipeline"); + sampleLocationsEnabled = ValidateSampleLocations(context, state); // A cached pipeline may outlive its device's teardown (see ClearCachedPipelines); it must not keep // the buffer pool, which is reset with the device, alive past it. this->context.bufferPool.reset(); @@ -259,12 +311,13 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput } // A descriptor set layout with the same bindings as this one is compatible with the pipeline // layout, so later draws bind their own ShaderResources' set under it. - const auto setLayout = resources.Layout(); + const std::array setLayouts{resources.main, resources.fragment}; const VkPushConstantRange push{pushStages, 0, PipelinePushSlotBytes}; const std::array slots{{{pushStagesOf(context), 0, PipelinePushSlotBytes}, {VK_SHADER_STAGE_FRAGMENT_BIT, PipelinePushSlotBytes, PipelinePushSlotBytes}}}; VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; - layoutInfo.setLayoutCount = 1; - layoutInfo.pSetLayouts = &setLayout; + layoutInfo.flags = resources.split ? VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT : 0; + layoutInfo.setLayoutCount = resources.split ? 2u : 1u; + layoutInfo.pSetLayouts = setLayouts.data(); layoutInfo.pushConstantRangeCount = dynamicRendering ? static_cast(slots.size()) : pushStages != 0 ? 1 : 0; layoutInfo.pPushConstantRanges = dynamicRendering ? slots.data() : pushStages != 0 ? &push : nullptr; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &layout), "vkCreatePipelineLayout graphics"); @@ -367,6 +420,11 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput } VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; samples.rasterizationSamples = static_cast(state.samples); + const auto locations = SampleLocations(state); + VkPipelineSampleLocationsStateCreateInfoEXT sampleLocations{VK_STRUCTURE_TYPE_PIPELINE_SAMPLE_LOCATIONS_STATE_CREATE_INFO_EXT}; + sampleLocations.sampleLocationsEnable = VK_TRUE; + sampleLocations.sampleLocationsInfo = {VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, samples.rasterizationSamples, {1, 1}, state.samples, locations.data()}; + if (sampleLocationsEnabled) samples.pNext = &sampleLocations; VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; depthStencil.depthTestEnable = state.depthTest; depthStencil.depthWriteEnable = state.depthWrite; @@ -420,7 +478,19 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput pipelineInfo.renderPass = renderPass; if (dynamicRendering) pipelineInfo.pNext = &rendering; timing.Mark("modules_and_state"); - if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, _modules); + if (prebuilding && !libraries) { + for (const auto module : _modules) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + _modules.clear(); + return; + } + if (prebuilding) { + static_cast(LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, true, _modules)); + for (const auto module : _modules) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + _modules.clear(); + prebuilt = true; + return; + } + if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, false, _modules); else Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines"); if (alternateAlpha) { blend.pAttachments = secondChannels.data(); @@ -440,6 +510,15 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput } } +bool Pipeline::Prebuild(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders) { + struct Scope { + Scope() { prebuilding = true; prebuilt = false; } + ~Scope() { prebuilding = false; } + } scope; + const Pipeline pipeline(context, state, vertexInput, resources, shaders); + return prebuilt; +} + Pipeline::~Pipeline() { release(); } @@ -563,6 +642,16 @@ void Pipeline::Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, V begin.renderPass = renderPass; begin.framebuffer = framebuffer.Handle(); begin.renderArea = {{0, 0}, extent}; + const auto locations = SampleLocations(state); + const VkSampleLocationsInfoEXT info{VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, static_cast(state.samples), {1, 1}, state.samples, locations.data()}; + const VkAttachmentSampleLocationsEXT initial{static_cast(state.colors.size()), info}; + const VkSubpassSampleLocationsEXT post{0, info}; + VkRenderPassSampleLocationsBeginInfoEXT sampleLocations{VK_STRUCTURE_TYPE_RENDER_PASS_SAMPLE_LOCATIONS_BEGIN_INFO_EXT}; + sampleLocations.attachmentInitialSampleLocationsCount = state.depth ? 1 : 0; + sampleLocations.pAttachmentInitialSampleLocations = state.depth ? &initial : nullptr; + sampleLocations.postSubpassSampleLocationsCount = 1; + sampleLocations.pPostSubpassSampleLocations = &post; + if (sampleLocationsEnabled) begin.pNext = &sampleLocations; context.Resolved(&DeviceFunctions::cmdBeginRenderPass, "vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); Continue(commands, state); } @@ -639,7 +728,7 @@ void append(std::vector& key, const TValue& value) { // (the recompiler could not identify it, so nothing else may share its pipeline). The rect-list // control and evaluation stages are generated from the vertex and fragment results, which the key // already names, so they carry no id of their own. -void pipelineKey(std::vector& key, const Context& context, const State& state, const VertexInputLayout& input, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) { +void pipelineKey(std::vector& key, const Context& context, const State& state, const VertexInputLayout& input, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) { using Stage = ShaderRecompiler::ShaderStage; key.clear(); append(key, context.device); @@ -671,8 +760,10 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, attribute.format); append(key, attribute.offset); } - append(key, resources.LayoutKey().size()); - for (const auto word : resources.LayoutKey()) append(key, word); + append(key, resources.mainKey.size()); + for (const auto word : resources.mainKey) append(key, word); + append(key, resources.fragmentKey.size()); + for (const auto word : resources.fragmentKey) append(key, word); append(key, state.hasColorTarget); append(key, state.rectList); append(key, state.topology); @@ -687,6 +778,8 @@ void pipelineKey(std::vector& key, const Context& context, const Stat for (const auto& blend : state.blends) append(key, blend); for (const auto value : state.blendConstants) append(key, value); append(key, state.samples); + append(key, state.customSampleLocations); + if (state.customSampleLocations) append(key, state.sampleLocations); append(key, state.colors.size()); for (const auto& color : state.colors) append(key, color.format); if (state.blends.size() != state.colors.size()) { @@ -790,6 +883,10 @@ void reportPipelines(PipelineStore& store) { } std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) { + return CachedPipeline(context, state, vertexInput, resources.SetLayouts(), shaders, attachmentLayout); +} + +std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) { static const bool disabled = std::getenv("APS5_NO_PIPELINE_CACHE") != nullptr; if (disabled) return std::make_shared(context, state, vertexInput, resources, shaders, attachmentLayout); auto& store = Pipelines(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp index edb90c69e..db9134959 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace AgcDriver::Graphics { @@ -38,9 +39,9 @@ struct DeviceLibraries { PFN_vkDestroyPipeline destroyPipeline = nullptr; PFN_vkDestroyPipelineLayout destroyLayout = nullptr; PFN_vkDestroyShaderModule destroyModule = nullptr; + std::vector modules; std::map layouts; std::array, 4> parts; - std::vector modules; PipelineLibraryCounters counters; }; @@ -101,22 +102,32 @@ std::span PipelineLibraryDynamicStates() { return dynamicStates; } -VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized, std::span modules) { +VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized, bool partsOnly, std::span modules) { Require(info.renderPass == VK_NULL_HANDLE && info.pDynamicState != nullptr, "pipeline library parts need dynamic rendering and the library dynamic states"); auto& store = Libraries(); - std::lock_guard lock(store.mutex); + std::unique_lock lock(store.mutex); auto& device = store.devices[context.device]; if (device.destroyPipeline == nullptr) { device.destroyPipeline = context.Function("vkDestroyPipeline"); device.destroyLayout = context.Function("vkDestroyPipelineLayout"); device.destroyModule = context.Function("vkDestroyShaderModule"); } - auto pipelineLayout = device.layouts.find(keys.layout); - if (pipelineLayout == device.layouts.end()) { + const auto layoutFor = [&](const Key& key, std::optional set) { + auto found = device.layouts.find(key); + if (found != device.layouts.end()) return found->second; + auto info = layout; + std::array sets{VK_NULL_HANDLE, VK_NULL_HANDLE}; + if (set) { + sets.at(*set) = layout.pSetLayouts[*set]; + info.pSetLayouts = sets.data(); + } VkPipelineLayout handle = VK_NULL_HANDLE; - Check(context.Function("vkCreatePipelineLayout")(context.device, &layout, nullptr, &handle), "vkCreatePipelineLayout library"); - pipelineLayout = device.layouts.emplace(keys.layout, handle).first; - } + Check(context.Function("vkCreatePipelineLayout")(context.device, &info, nullptr, &handle), "vkCreatePipelineLayout library"); + return device.layouts.emplace(key, handle).first->second; + }; + const bool split = (layout.flags & VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT) != 0; + const auto fullLayout = layoutFor(keys.layout, std::nullopt); + const std::array partLayouts{VK_NULL_HANDLE, split ? layoutFor(keys.preRasterizationLayout, 0u) : fullLayout, split ? layoutFor(keys.fragmentLayout, 1u) : fullLayout, VK_NULL_HANDLE}; std::vector preRasterization; const VkPipelineShaderStageCreateInfo* fragment = nullptr; bool mesh = false; @@ -135,7 +146,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip }; const std::array partKeys{&keys.vertexInput, &keys.preRasterization, &keys.fragmentShader, &keys.fragmentOutput}; const Key none; - const std::array contextKeys{&none, &keys.layout, &keys.layout, &keys.renderPass}; + const std::array contextKeys{&none, &keys.preRasterizationLayout, &keys.fragmentLayout, &keys.renderPass}; const VkPipelineRenderingCreateInfoKHR shaderRendering{VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR}; VkPipelineDepthStencilStateCreateInfo dynamicDepth{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; static const bool backgroundLinking = std::getenv("APS5_NO_PIPELINE_LTO") == nullptr; @@ -155,7 +166,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &library}; create.flags = VK_PIPELINE_CREATE_LIBRARY_BIT_KHR | VK_PIPELINE_CREATE_RETAIN_LINK_TIME_OPTIMIZATION_INFO_BIT_EXT; create.pDynamicState = info.pDynamicState; - create.layout = pipelineLayout->second; + create.layout = partLayouts[part]; switch (flags[part]) { case VK_GRAPHICS_PIPELINE_LIBRARY_VERTEX_INPUT_INTERFACE_BIT_EXT: create.pVertexInputState = info.pVertexInputState; @@ -182,7 +193,15 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip break; } VkPipeline handle = VK_NULL_HANDLE; - Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle), "vkCreateGraphicsPipelines library"); + if (partsOnly) lock.unlock(); + const auto created = context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle); + if (partsOnly) lock.lock(); + Check(created, "vkCreateGraphicsPipelines library"); + if (const auto raced = cache.find(key); raced != cache.end()) { + device.destroyPipeline(context.device, handle, nullptr); + libraries.push_back(raced->second); + continue; + } cache.emplace(key, handle); if (optimize) { for (std::uint32_t stage = 0; stage < create.stageCount; ++stage) { @@ -196,6 +215,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip ++device.counters.built[part]; libraries.push_back(handle); } + if (partsOnly) return VK_NULL_HANDLE; PerformanceTimer timing("Vulkan.GraphicsPipelineLink"); VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR}; linked.libraryCount = static_cast(libraries.size()); @@ -212,7 +232,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip *optimized = slot; const auto createPipelines = context.Function("vkCreateGraphicsPipelines"); const auto cache = context.pipelineCache; - const auto libraryLayout = pipelineLayout->second; + const auto libraryLayout = fullLayout; enqueue(store, context.device, [slot, createPipelines, cache, libraryLayout, libraries, &store, devicePointer = &device] { if (slot->released.load()) return; VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR}; @@ -255,8 +275,8 @@ void ClearPipelineLibraries(VkDevice device) { for (auto& part : libraries.parts) { for (const auto& [key, handle] : part) libraries.destroyPipeline(device, handle, nullptr); } - for (const auto module : libraries.modules) libraries.destroyModule(device, module, nullptr); for (const auto& [key, handle] : libraries.layouts) libraries.destroyLayout(device, handle, nullptr); + for (const auto module : libraries.modules) libraries.destroyModule(device, module, nullptr); store.devices.erase(found); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index c788418b9..cfcb1c010 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -2506,11 +2506,19 @@ bool Recorder::refreshDrawSnapshot(DrawSnapshot& entry, std::uint64_t address, s const auto aligned = address / block * block; const auto end = address + bytes; const auto count = static_cast((end - 1) / block - address / block + 1); - thread_local std::vector generations; - thread_local std::vector changed; - thread_local std::vector> differing; - thread_local std::vector spans; - thread_local std::vector equal; + struct Scratch { + std::vector generations; + std::vector changed; + std::vector> differing; + std::vector spans; + std::vector equal; + }; + auto& scratch = HostThreadLocal(); + auto& generations = scratch.generations; + auto& changed = scratch.changed; + auto& differing = scratch.differing; + auto& spans = scratch.spans; + auto& equal = scratch.equal; generations.assign(count, entry.generation); changed.assign(count, GuestMemory::BlockWritten); differing.clear(); @@ -2561,6 +2569,40 @@ void Recorder::KeepDrawSnapshot(std::uint64_t address, std::size_t bytes, std::u pool.bytes += bytes; } +bool Recorder::KnownHighestIndex(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::optional& highest) { + const auto found = indexRanges.find({address, use, bytes}); + if (found == indexRanges.end()) return false; + if (found->second.registryGeneration != GuestAllocations::GuestAllocationsGeneration_nid_postfix() || !GuestMemory::UnchangedSince(address, bytes, found->second.generation)) { + indexRangeRecency.erase(found->second.recent); + indexRanges.erase(found); + return false; + } + indexRangeRecency.splice(indexRangeRecency.end(), indexRangeRecency, found->second.recent); + highest = found->second.highest; + return true; +} + +void Recorder::KeepHighestIndex(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::uint64_t generation, std::uint64_t registryGeneration, std::optional highest) { + Require(use != SnapshotUse::Storage && use != SnapshotUse::Vertex, "a highest index belongs to an index buffer"); + if (generation == 0) return; + const DrawSnapshotKey key{address, use, bytes}; + if (const auto found = indexRanges.find(key); found != indexRanges.end()) { + indexRangeRecency.erase(found->second.recent); + indexRanges.erase(found); + } + while (indexRanges.size() >= IndexRangeEntries) { + indexRanges.erase(indexRangeRecency.front()); + indexRangeRecency.pop_front(); + } + indexRangeRecency.push_back(key); + try { + indexRanges.emplace(key, IndexRange{generation, registryGeneration, std::prev(indexRangeRecency.end()), highest}); + } catch (...) { + indexRangeRecency.pop_back(); + throw; + } +} + void Recorder::OnComplete(std::function action) { ensureOpen(); open->completions.push_back(std::move(action)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index 3e89ca766..61cfa6b42 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -142,6 +142,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con const bool pixelKillEnable = ((shaderControl >> 6u) & 0x1u) != 0; const bool depthExportEnable = (shaderControl & 0x1u) != 0; const bool sampleMaskExportEnable = ((shaderControl >> 8u) & 0x1u) != 0; + const bool stencilRefExportEnable = ((shaderControl >> 1u) & 0x1u) != 0; const auto zOrder = (shaderControl >> 4u) & 0x3u; const auto loaded = [&](PixelInput input) { return (activeInputs & PixelInputBit(input)) != 0; }; return ShaderRecompiler::ShaderPixelStageInfo{ @@ -163,12 +164,13 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con .pixelKillEnable = pixelKillEnable, .depthExportEnable = depthExportEnable, .sampleMaskExportEnable = sampleMaskExportEnable, - .earlyZ = zOrder == 1u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable, + .earlyZ = zOrder == 1u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable && !stencilRefExportEnable, .executeOnNoop = ((shaderControl >> 10u) & 0x1u) != 0, .conservativeZExport = static_cast(conservativeZExport), .orderedPixelShader = ((shaderControl >> 16u) & 0x1u) != 0, .targetOutputMode = targetOutputMode, - .targetExportMapping = nullProgram ? std::array{} : exportMappings + .targetExportMapping = nullProgram ? std::array{} : exportMappings, + .stencilRefExportEnable = stencilRefExportEnable }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 0383b7571..63bd7b18e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2,10 +2,12 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "ThreadOwned.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include #include @@ -278,9 +280,10 @@ struct ImageMemoEntry { }; constexpr std::size_t ImageMemoSlots = 1024; -thread_local std::unique_ptr> imageMemo; +struct ImageMemoTag {}; ImageMemoEntry& imageMemoSlot(std::span words) { + auto& imageMemo = HostThreadLocal>, ImageMemoTag>(); if (imageMemo == nullptr) imageMemo = std::make_unique>(); const auto hash = hashWords(14695981039346656037ull, words.data(), words.size()); return (*imageMemo)[(hash ^ (hash >> 29u)) % ImageMemoSlots]; @@ -834,9 +837,9 @@ GuestTextureResource StorageSurface(const Context& context, const GuestTextureRe // `guestBytes` is the surface size when the caller described the surface already (0: described here). std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& viewed, std::uint32_t mip, std::uint64_t guestBytes) { RequireNoMultisampleTarget(viewed.baseAddress, guestBytes != 0 ? static_cast(guestBytes) : 1u, "a storage image access"); - if (DepthSurfaceAt(viewed.baseAddress)) { - char text[112]; - std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented", static_cast(viewed.baseAddress)); + if (DepthSurfaceAt(context, words, viewed)) { + char text[256]; + std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented (storage=%ux%u guest format=%u mip=%u dimension=%d tile=%d)", static_cast(viewed.baseAddress), viewed.width, viewed.height, viewed.format, mip, static_cast(viewed.dimension), static_cast(viewed.tileMode)); throw std::runtime_error(text); } static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; @@ -1164,6 +1167,14 @@ VkDescriptorSetLayout CreateSetLayout(const Context& context, std::span PoolSizes(std::span bindings) { + std::map counts; + for (const auto& binding : bindings) counts[binding.descriptorType] += binding.descriptorCount; + std::vector sizes; + for (const auto& [type, count] : counts) sizes.push_back({type, count}); + return sizes; +} + VkDescriptorPool CreateSetPool(const Context& context, std::span sizes, bool updateAfterBind) { VkDescriptorPoolCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; info.flags = updateAfterBind ? VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT : 0; @@ -1337,7 +1348,7 @@ void ShaderResources::buildPrepare(std::span shaders, cons updateAfterBind = true; }; for (const auto& binding : shader.program->bindings) { - Require(binding.descriptorSet == 0, "unexpected descriptor set: every shader resource must use descriptor set zero"); + Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shader.stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "unexpected descriptor set: a shader resource must use its stage's descriptor set"); Require(occupied.insert(binding.binding).second, "duplicate shader binding"); const bool addressRole = binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable || binding.role == ShaderRecompiler::DescriptorRole::FaultBuffer; const bool bufferRole = addressRole || binding.role == ShaderRecompiler::DescriptorRole::GuestBuffers || binding.role == ShaderRecompiler::DescriptorRole::ShaderData || binding.role == ShaderRecompiler::DescriptorRole::FlattenedSrt || binding.role == ShaderRecompiler::DescriptorRole::Gds; @@ -1414,23 +1425,62 @@ void ShaderResources::buildPrepare(std::span shaders, cons if (drawBuild && !usesBda && std::all_of(allocations.begin(), allocations.end(), [&](const Allocation& allocation) { return !allocation.guest || allocation.pushByte >= 0 || (allocation.dataAllocation >= 0 && allocation.dataByte < allocations[static_cast(allocation.dataAllocation)].size); })) guestMemory.AllowAdjustedRegions(); guestMemory.UploadPrepare(usesBda); timing.uploadMs = phase(BuildPhase::Upload); - std::vector description; + splitSets = SplitsSets(context, shaders); + std::array, 2> descriptions; for (const auto& binding : bindings) { - description.push_back(binding.layout); - layoutBindingFlags.push_back(updateAfterBind && binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0); - layoutKey.insert(layoutKey.end(), {binding.layout.binding, static_cast(binding.layout.descriptorType), binding.layout.descriptorCount, binding.layout.stageFlags}); + const auto set = setOf(binding); + descriptions[set].push_back(binding.layout); + (set == 0 ? layoutBindingFlags : fragmentBindingFlags).push_back(updateAfterBind && binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0); + auto& key = set == 0 ? layoutKey : fragmentLayoutKey; + key.insert(key.end(), {binding.layout.binding, static_cast(binding.layout.descriptorType), binding.layout.descriptorCount, binding.layout.stageFlags}); + } + if (updateAfterBind) { + layoutKey.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); + if (splitSets) fragmentLayoutKey.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); } - if (updateAfterBind) layoutKey.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); static const bool noLayoutCache = std::getenv("APS5_NO_LAYOUT_CACHE") != nullptr; static const bool noPoolCache = std::getenv("APS5_NO_POOL_CACHE") != nullptr; - if (context.descriptorCache != nullptr && !noLayoutCache) { - _layout = context.descriptorCache->Layout(layoutKey, description, updateAfterBind, layoutBindingFlags); - } else { + const auto createLayout = [&](const std::vector& key, const std::vector& description, const std::vector& flags, bool& owns) { + if (context.descriptorCache != nullptr && !noLayoutCache) return context.descriptorCache->Layout(key, description, updateAfterBind, flags); ValidateDescriptorLayout(context, description, updateAfterBind); - _layout = CreateSetLayout(context, description, updateAfterBind, layoutBindingFlags); - ownsLayout = true; + owns = true; + return CreateSetLayout(context, description, updateAfterBind, flags); + }; + _layout = createLayout(layoutKey, descriptions[0], layoutBindingFlags, ownsLayout); + if (splitSets) { + fragment.layout = createLayout(fragmentLayoutKey, descriptions[1], fragmentBindingFlags, fragment.ownsLayout); + { + const auto sizes = PoolSizes(descriptions[1]); + if (context.descriptorCache != nullptr && !noPoolCache) { + fragment.cacheAllocation = context.descriptorCache->Allocate(fragment.layout, sizes, updateAfterBind); + fragment.set = fragment.cacheAllocation.set; + } + if (fragment.set == VK_NULL_HANDLE) { + fragment.pool = CreateSetPool(context, sizes, updateAfterBind); + VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + allocation.descriptorPool = fragment.pool; + allocation.descriptorSetCount = 1; + allocation.pSetLayouts = &fragment.layout; + Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &fragment.set), "vkAllocateDescriptorSets"); + } + } } - if (!bindings.empty()) { + if (splitSets) { + const auto sizes = PoolSizes(descriptions[0]); + if (context.descriptorCache != nullptr && !noPoolCache) { + cacheAllocation = context.descriptorCache->Allocate(_layout, sizes, updateAfterBind); + _set = cacheAllocation.set; + } + if (_set == VK_NULL_HANDLE) { + pool = CreateSetPool(context, sizes, updateAfterBind); + VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + allocation.descriptorPool = pool; + allocation.descriptorSetCount = 1; + allocation.pSetLayouts = &_layout; + Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &_set), "vkAllocateDescriptorSets"); + } + } + if (!splitSets && !bindings.empty()) { // The set is sized from the plan: every image element becomes exactly one descriptor // when stage B looks it up. std::vector sizes; @@ -1463,6 +1513,48 @@ void ShaderResources::buildPrepare(std::span shaders, cons } } +bool SplitsSets(const Context& context, std::span shaders) { + const auto fragmentSet = [](const CompiledShader& shader) { return shader.program != nullptr && std::any_of(shader.program->bindings.begin(), shader.program->bindings.end(), [](const auto& binding) { return binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet; }); }; + const auto fragmentStage = [](const CompiledShader& shader) { return shader.stage == ShaderRecompiler::ShaderStage::Fragment; }; + return std::any_of(shaders.begin(), shaders.end(), fragmentSet) || (context.graphicsPipelineLibrary && std::any_of(shaders.begin(), shaders.end(), fragmentStage)); +} + +DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span shaders) { + Require(context.descriptorCache != nullptr, "described set layouts need the descriptor cache"); + DescribedSetLayouts result; + std::array, 2> descriptions; + const bool split = SplitsSets(context, shaders); + bool updateAfterBind = false; + std::uint64_t storageBuffers = 0; + for (const auto& shader : shaders) { + Require(shader.program != nullptr, "missing compiled shader"); + const VkShaderStageFlags flags = VulkanStage(shader.stage); + std::uint64_t stageStorageBuffers = 0; + std::uint64_t stageResources = 0; + for (const auto& binding : shader.program->bindings) { + const bool image = binding.role == ShaderRecompiler::DescriptorRole::GuestImages || binding.role == ShaderRecompiler::DescriptorRole::GuestSamplers; + const auto type = !image ? VK_DESCRIPTOR_TYPE_STORAGE_BUFFER : binding.kind == ShaderRecompiler::DescriptorKind::StorageImage ? VK_DESCRIPTOR_TYPE_STORAGE_IMAGE : binding.kind == ShaderRecompiler::DescriptorKind::SampledImage ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE : VK_DESCRIPTOR_TYPE_SAMPLER; + if (type != VK_DESCRIPTOR_TYPE_SAMPLER) stageResources += binding.count; + if (type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) { + stageStorageBuffers += binding.count; + storageBuffers += binding.count; + } + if (stageStorageBuffers > context.limits.maxPerStageDescriptorStorageBuffers || stageResources > context.limits.maxPerStageResources) updateAfterBind = true; + descriptions[binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet ? 1 : 0].push_back({binding.binding, type, binding.count, flags, nullptr}); + } + } + if (storageBuffers > context.limits.maxDescriptorSetStorageBuffers) updateAfterBind = true; + for (std::size_t set = 0; set < descriptions.size(); ++set) { + auto& key = set == 0 ? result.mainKey : result.fragmentKey; + for (const auto& binding : descriptions[set]) key.insert(key.end(), {binding.binding, static_cast(binding.descriptorType), binding.descriptorCount, binding.stageFlags}); + if (updateAfterBind && (set == 0 || split)) key.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); + } + result.layouts.split = split; + result.layouts.main = context.descriptorCache->Layout(result.mainKey, descriptions[0], updateAfterBind); + if (split) result.layouts.fragment = context.descriptorCache->Layout(result.fragmentKey, descriptions[1], updateAfterBind); + return result; +} + void ShaderResources::buildComplete() { PerformanceTimer frameTiming("Resources.Complete"); Require(!completed, "cannot update a published descriptor heap"); @@ -1490,7 +1582,7 @@ void ShaderResources::buildComplete() { if (usesBda) bda = std::make_unique(context, guestMemory); else if (usesFaultBuffer) bda = std::make_unique(context); phase(BuildPhase::Bda); - if (_set != VK_NULL_HANDLE) { + if (_set != VK_NULL_HANDLE || fragment.set != VK_NULL_HANDLE) { // One update call for the whole set: the info arrays are sized up front so every write's // pointer into them stays valid until the call. std::size_t bufferCount = 0; @@ -1512,7 +1604,7 @@ void ShaderResources::buildComplete() { }; for (const auto& binding : bindings) { VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; - write.dstSet = _set; + write.dstSet = setOf(binding) == 0 ? _set : fragment.set; write.dstBinding = binding.layout.binding; write.descriptorCount = binding.layout.descriptorCount; write.descriptorType = binding.layout.descriptorType; @@ -3332,10 +3424,19 @@ void ShaderResources::release() noexcept { if (cacheAllocation.pool && context.descriptorCache != nullptr) context.descriptorCache->Free(cacheAllocation); if (pool) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); if (_layout && ownsLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, _layout, nullptr); + if (fragment.cacheAllocation.pool && context.descriptorCache != nullptr) context.descriptorCache->Free(fragment.cacheAllocation); + if (fragment.pool) context.Function("vkDestroyDescriptorPool")(context.device, fragment.pool, nullptr); + if (fragment.layout && fragment.ownsLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, fragment.layout, nullptr); cacheAllocation = {}; pool = VK_NULL_HANDLE; _set = VK_NULL_HANDLE; _layout = VK_NULL_HANDLE; + fragment = {}; +} + +std::uint32_t ShaderResources::setOf(const Binding& binding) const { + constexpr auto count = static_cast(ShaderRecompiler::RuntimeAbi::Binding::Count); + return splitSets && binding.layout.binding / count == static_cast(ShaderRecompiler::RuntimeAbi::Stage::Fragment) ? 1u : 0u; } VkDescriptorSetLayout ShaderResources::Layout() const { @@ -3354,11 +3455,12 @@ bool ShaderResources::LayoutUpdateAfterBind() const { ShaderResources::DrawBindings::~DrawBindings() { if (cache != nullptr && allocation.set != VK_NULL_HANDLE) cache->Free(allocation); + if (cache != nullptr && fragmentAllocation.set != VK_NULL_HANDLE) cache->Free(fragmentAllocation); } std::optional> ShaderResources::MovedReadOnlyBuffers(std::span shaders, Recorder& recorder) const { std::vector moved; - if (_set == VK_NULL_HANDLE || usesBda) return moved; + if ((_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) || usesBda) return moved; for (const auto& shader : shaders) { if (shader.program == nullptr) return std::nullopt; for (const auto& binding : shader.program->bindings) { @@ -3411,7 +3513,7 @@ std::size_t ShaderResources::MaxDrawSnapshotBytes() { } std::shared_ptr ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span moved) const { - if (_set == VK_NULL_HANDLE || usesBda) return {}; + if ((_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) || usesBda) return {}; const auto reads = guestMemory.InPlaceReads(); const auto maxSnapshotBytes = MaxDrawSnapshotBytes(); std::size_t totalSnapshotBytes = 0; @@ -3458,13 +3560,17 @@ std::shared_ptr ShaderResources::PrepareDrawBindi } if (selected.empty()) return {}; Require(context.descriptorCache != nullptr, "draw snapshots require a descriptor cache"); - std::map counts; - for (const auto& binding : bindings) counts[binding.layout.descriptorType] += binding.layout.descriptorCount; - std::vector sizes; - for (const auto& [type, count] : counts) sizes.push_back({type, count}); + std::array, 2> descriptions; + for (const auto& binding : bindings) descriptions[setOf(binding)].push_back(binding.layout); result->cache = context.descriptorCache; - result->allocation = result->cache->Allocate(_layout, sizes); - Require(result->allocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed"); + if (!descriptions[0].empty() || splitSets) { + result->allocation = result->cache->Allocate(_layout, PoolSizes(descriptions[0])); + Require(result->allocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed"); + } + if (!descriptions[1].empty() || splitSets) { + result->fragmentAllocation = result->cache->Allocate(fragment.layout, PoolSizes(descriptions[1])); + Require(result->fragmentAllocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed"); + } auto buffers = descriptorBuffers; auto writes = descriptorWrites; std::vector snapshotIndices(allocations.size(), selected.size()); @@ -3472,7 +3578,7 @@ std::shared_ptr ShaderResources::PrepareDrawBindi for (std::size_t index = 0; index < bindings.size(); ++index) { const auto& binding = bindings[index]; auto& write = writes[index]; - write.dstSet = result->allocation.set; + write.dstSet = setOf(binding) == 0 ? result->allocation.set : result->fragmentAllocation.set; if (binding.allocations.empty()) continue; const auto offset = static_cast(write.pBufferInfo - descriptorBuffers.data()); write.pBufferInfo = buffers.data() + offset; @@ -3490,8 +3596,8 @@ std::shared_ptr ShaderResources::PrepareDrawBindi } void ShaderResources::Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const { - if (_set == VK_NULL_HANDLE) return; - context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, bindPoint, layout, 0, 1, &_set, 0, nullptr); + if (_set != VK_NULL_HANDLE) context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, bindPoint, layout, 0, 1, &_set, 0, nullptr); + if (fragment.set != VK_NULL_HANDLE) context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, bindPoint, layout, ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet, 1, &fragment.set, 0, nullptr); } namespace { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index c5d9168d1..4022508ff 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -43,6 +43,7 @@ struct Module { std::map outputs; std::optional secondSource; bool position = false; + bool clipDistance = false; bool primitiveIndices = false; bool fragmentBarycentric = false; std::map> modes; @@ -109,6 +110,11 @@ struct Module { if (!input) position = true; return; } + if (value == spv::BuiltInClipDistance && !input && (vertex || stage == Stage::TessellationEvaluation)) { + Require(!clipDistance && Signature(type) == "f32[1]", "invalid or duplicate clip distance output"); + clipDistance = true; + return; + } if (stage == Stage::TessellationControl || stage == Stage::TessellationEvaluation || stage == Stage::Mesh) { const auto signature = Signature(type); if (!input && value == spv::BuiltInPosition) { @@ -131,12 +137,12 @@ struct Module { Require(value == spv::BuiltInPosition && signature == "f32x4" && !position, "unsupported or duplicate vertex built-in output"); position = true; } else { - Require((storage == spv::StorageClassInput && ((value == spv::BuiltInFragCoord && signature == "f32x4") || ((value == spv::BuiltInFrontFacing || value == spv::BuiltInHelperInvocation) && signature == "bool") || ((value == spv::BuiltInSampleId || value == spv::BuiltInLayer) && signature == "i32"))) || (storage == spv::StorageClassOutput && ((value == spv::BuiltInFragDepth && signature == "f32") || (value == spv::BuiltInSampleMask && (signature == "i32[1]" || signature == "u32[1]")))), "unsupported fragment built-in"); + Require((storage == spv::StorageClassInput && ((value == spv::BuiltInFragCoord && signature == "f32x4") || ((value == spv::BuiltInFrontFacing || value == spv::BuiltInHelperInvocation) && signature == "bool") || ((value == spv::BuiltInSampleId || value == spv::BuiltInLayer) && signature == "i32"))) || (storage == spv::StorageClassOutput && ((value == spv::BuiltInFragDepth && signature == "f32") || (value == spv::BuiltInFragStencilRefEXT && signature == "i32") || (value == spv::BuiltInSampleMask && (signature == "i32[1]" || signature == "u32[1]")))), "unsupported fragment built-in"); } } }; -Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics) { +Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance) { using Stage = ShaderRecompiler::ShaderStage; Require(compiled.program != nullptr, "missing compiled shader"); const auto& shader = *compiled.program; @@ -226,12 +232,17 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi fragment && capability == spv::CapabilityFragmentShaderPixelInterlockEXT; + const bool isStencilExportCapability = + fragment && + capability == spv::CapabilityStencilExportEXT; + // Enabled unconditionally or by the device setup in VulkanDevice. const bool isFeatureCapability = capability == spv::CapabilitySampled1D || capability == spv::CapabilityImage1D || capability == spv::CapabilityImageGatherExtended || capability == spv::CapabilityImageQuery || + ((vertex || evaluation) && shaderClipDistance && capability == spv::CapabilityClipDistance) || (fragment && geometryShader && capability == spv::CapabilityGeometry) || (fragment && sampleRateShading && capability == spv::CapabilitySampleRateShading) || capability == spv::CapabilityImageMSArray || @@ -266,6 +277,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi isMeshCapability || isGeometryCapability || isInterlockCapability || + isStencilExportCapability || isFeatureCapability || isDescriptorIndexingCapability || ((imageInt64Atomics || bufferInt64Atomics) && capability == spv::CapabilityInt64Atomics) || @@ -286,6 +298,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(fragment, "SPV_EXT_fragment_shader_interlock requires a fragment shader"); break; } + if (extension == "SPV_EXT_shader_stencil_export") { + Require(fragment, "SPV_EXT_shader_stencil_export requires a fragment shader"); + break; + } if (extension == "SPV_KHR_fragment_shader_barycentric") { Require(fragment && fragmentShaderBarycentric, "SPV_KHR_fragment_shader_barycentric requires enabled fragmentShaderBarycentric in a fragment shader"); break; @@ -413,7 +429,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi mode(spv::ExecutionModeOutputVertices, {3u}); Require(module.modes.size() == 4, "unsupported geometry execution mode"); } else if (fragment) { - for (const auto& [name, operands] : module.modes) Require(operands.empty() && (name == spv::ExecutionModeOriginUpperLeft || name == spv::ExecutionModeEarlyFragmentTests || name == spv::ExecutionModeDepthReplacing || name == spv::ExecutionModeDepthLess || name == spv::ExecutionModeDepthGreater || name == spv::ExecutionModePixelInterlockOrderedEXT), "unsupported fragment execution mode"); + for (const auto& [name, operands] : module.modes) Require(operands.empty() && (name == spv::ExecutionModeOriginUpperLeft || name == spv::ExecutionModeEarlyFragmentTests || name == spv::ExecutionModeDepthReplacing || name == spv::ExecutionModeDepthLess || name == spv::ExecutionModeDepthGreater || name == spv::ExecutionModePixelInterlockOrderedEXT || name == spv::ExecutionModeStencilRefReplacingEXT), "unsupported fragment execution mode"); } else Require(module.modes.empty(), "unsupported vertex execution mode"); std::set> descriptors; bool push = false; @@ -547,7 +563,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi } -std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics) { +std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance) { using Stage = ShaderRecompiler::ShaderStage; const bool tessellation = state.stages.path == ShaderPath::Tessellation; const bool mesh = state.stages.path == ShaderPath::Geometry; @@ -567,8 +583,8 @@ std::set ValidateShaders(std::span shaders, const auto expected = state.rectList ? (i == 0 ? Stage::Vertex : tessStages[i]) : tessellation ? tessStages[i] : geometry && i == 1 ? Stage::Geometry : i == shaders.size() - 1 ? Stage::Fragment : state.stages.path == ShaderPath::Geometry ? Stage::Mesh : Stage::Vertex; Require(shaders[i].program != nullptr, "missing compiled shader"); Require(shaders[i].stage == expected, "graphics stage order disagrees"); - for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == 0, "graphics resource uses a descriptor set other than zero"); - const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics); + for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shaders[i].stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "graphics resource uses a descriptor set other than its stage's"); + const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance); if (i != 0) { for (const auto& [location, signature] : current.inputs) { const auto output = previous.outputs.find(location); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 3316fd778..691bb1ff2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -119,9 +119,11 @@ bool zFormatSupported(std::uint32_t format) { std::uint32_t shaderControlMask(std::uint32_t zFormat) { constexpr std::uint32_t zExportEnable = 0x1u; + constexpr std::uint32_t stencilTestValExportEnable = 0x2u; constexpr std::uint32_t maskExportEnable = 0x100u; auto mask = ShaderControlMask; if (zFormat != 0u && zFormatSupported(zFormat)) mask &= ~zExportEnable; + if (zFormat == 2u || zFormat == 9u) mask &= ~stencilTestValExportEnable; if (zFormat == 9u) mask &= ~maskExportEnable; return mask; } @@ -154,7 +156,7 @@ bool depthPlanesAbsent(const Registers& cx) { return z != cx.end() && stencil != cx.end() && (z->second & 3u) == 0 && (stencil->second & 1u) == 0; } -VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint32_t refMask, bool readOnly) { +VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint32_t refMask, bool readOnly, bool testValExport) { VkStencilOpState face{}; face.compareOp = static_cast(compare); face.compareMask = (refMask >> 8u) & 0xffu; @@ -172,6 +174,10 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint case 2: case 3: case 4: { + if (testValExport) { + if (op != 3) throw std::runtime_error("AGC graphics: stencil replacement by a value other than the test value is unsupported with a pixel shader that exports the stencil test value"); + return VK_STENCIL_OP_REPLACE; + } const auto value = op == 2 ? 0xffu : op == 3 ? test : opValue; if (((reference ^ value) & fixed & face.writeMask) != 0) { std::snprintf(detail, sizeof(detail), "AGC graphics: stencil replacement 0x%02x against test value 0x%02x (masks 0x%02x/0x%02x) is unsupported", value, test, face.compareMask, face.writeMask); @@ -203,7 +209,7 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint return face; } -void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) { +void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValExport, State& result) { zero(cx, 0x000, 0x00001f9cu, "depth copy, resummarize or decompress draws (DB_RENDER_CONTROL)"); const bool depthClear = (read(cx, 0x000) & 1u) != 0; const bool stencilClear = (read(cx, 0x000) & 2u) != 0; @@ -223,6 +229,7 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) const bool depthReadOnly = (view & 0x01000000u) != 0; const bool stencilReadOnly = (view & 0x02000000u) != 0; Require(!stencilClear || (stencil && !stencilReadOnly), "stencil clear requires a writable stencil plane"); + Require(!stencilClear || !testValExport, "stencil clear with a pixel shader that exports the stencil test value is unsupported"); Require(!depthClear || (zFormat != 0 && !depthReadOnly), "depth clear requires a writable depth plane"); DepthTarget depth{}; depth.samples = 1u << ((read(cx, 0x010) >> 2u) & 3u); @@ -274,8 +281,8 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) result.stencilBack = clear; } else if (result.stencilTest) { const auto ops = read(cx, 0x10b); - result.stencilFront = stencilFace((depthControl >> 8u) & 7u, ops, read(cx, 0x10c), stencilReadOnly); - result.stencilBack = (depthControl & 0x80u) != 0 ? stencilFace((depthControl >> 20u) & 7u, ops >> 12u, read(cx, 0x10d), stencilReadOnly) : result.stencilFront; + result.stencilFront = stencilFace((depthControl >> 8u) & 7u, ops, read(cx, 0x10c), stencilReadOnly, testValExport); + result.stencilBack = (depthControl & 0x80u) != 0 ? stencilFace((depthControl >> 20u) & 7u, ops >> 12u, read(cx, 0x10d), stencilReadOnly, testValExport) : result.stencilFront; } } @@ -317,6 +324,36 @@ constexpr std::array, 2> StandardLocations2{{{4, 4}, constexpr std::array, 4> StandardLocations4{{{-2, -6}, {6, -2}, {-6, 2}, {2, 6}}}; constexpr std::array, 8> StandardLocations8{{{1, -3}, {-1, 3}, {5, 1}, {-3, -5}, {-5, 5}, {-7, -1}, {3, 7}, {7, -7}}}; +std::string singleSampleMsaaRejection(const QueueState& queue) { + const auto& cx = queue.context; + const auto word = [](const Registers& registers, std::uint32_t offset, RegisterBank bank = RegisterBank::Context) -> std::optional { + const auto it = find(registers, offset, bank); + if (it == registers.end()) return std::nullopt; + return it->second; + }; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + const auto offset = 0x2feu + pixel * 4u; + if (const auto locations = word(cx, offset); locations && (*locations & 0xffu) != 0) return zeroMessage(offset, *locations, "MSAA_ENABLE on a single-sample target whose sample is off the pixel center"); + } + if (const auto alphaToMask = word(cx, 0x2dc); alphaToMask && (*alphaToMask & 1u) != 0) return zeroMessage(0x2dc, *alphaToMask, "MSAA_ENABLE on a single-sample target with alpha-to-coverage"); + if (const auto conservative = word(cx, 0x313); conservative && (*conservative & ~0x001f6000u) != 0) return zeroMessage(0x313, *conservative, "MSAA_ENABLE on a single-sample target with conservative rasterization"); + constexpr const char* primitives = "MSAA_ENABLE on a single-sample target for primitives other than triangles"; + const auto stages = word(cx, 0x2d5); + if (!stages) return {}; + if ((*stages & 0x20u) != 0) { + if (const auto output = word(cx, 0x29b); output && ((*output & 0x80000000u) != 0 || (*output & 0x3fu) != 2u)) return zeroMessage(0x29b, *output, primitives); + } else if ((*stages & 4u) != 0) { + if (const auto parameters = word(cx, 0x2db)) { + const auto domain = *parameters & 3u; + const auto topology = (*parameters >> 5u) & 7u; + if ((domain != 1u && domain != 2u) || (topology != 2u && topology != 3u)) return zeroMessage(0x2db, *parameters, primitives); + } + } else if (const auto primitive = word(queue.userConfig, 0x242, RegisterBank::UserConfig); primitive && *primitive != 4u && *primitive != 5u && *primitive != 6u) { + return zeroMessage(0x242, *primitive, primitives); + } + return {}; +} + std::uint32_t decodeSamples(const Registers& cx) { const auto config = read(cx, 0x2f8); zero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u), "coverage conversion or nonstandard multisampling"); @@ -327,7 +364,12 @@ std::uint32_t decodeSamples(const Registers& cx) { } Require(log2 <= 3u, "more than eight samples are unsupported"); Require(((config >> 20u) & 7u) == log2, "exposing fewer samples than are rasterized is unsupported"); - const auto samples = 1u << log2; + return 1u << log2; +} + +void decodeSampleLocations(const Registers& cx, State& state) { + const auto samples = state.samples; + if (samples == 1) return; const std::span> standard = samples == 2 ? std::span>(StandardLocations2) : samples == 4 ? std::span>(StandardLocations4) : std::span>(StandardLocations8); for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { for (std::uint32_t sample = 0; sample < samples; ++sample) { @@ -335,10 +377,14 @@ std::uint32_t decodeSamples(const Registers& cx) { const auto bits = (word >> (8u * (sample % 4u))) & 0xffu; const auto x = static_cast(static_cast(bits << 4u)) >> 4; const auto y = static_cast(static_cast(bits & 0xf0u)) >> 4; - Require(x == standard[sample][0] && y == standard[sample][1], "nonstandard sample locations are unsupported"); + if (pixel == 0) { + state.sampleLocations[sample] = static_cast(bits); + state.customSampleLocations |= x != standard[sample][0] || y != standard[sample][1]; + } else { + Require(bits == state.sampleLocations[sample], "nonstandard sample locations varying between pixels are unsupported"); + } } } - return samples; } bool colorControlSupported(std::uint32_t colorControl, bool hasColorTarget) { @@ -495,6 +541,9 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, if (number == floating) return {VK_FORMAT_R32G32B32A32_SFLOAT, 16}; if (number == uint) return {VK_FORMAT_R32G32B32A32_UINT, 16}; return fail(); + case 17: + if (swap != 0 || number != unorm) return fail(); + return {VK_FORMAT_A1R5G5B5_UNORM_PACK16, 2, 0xc6u}; default: return fail(); } @@ -519,6 +568,23 @@ void intersect(VkRect2D& result, const Registers& registers, std::uint32_t offse } +std::array SampleLocations(const State& state) { + Require(state.samples == 1 || state.samples == 2 || state.samples == 4 || state.samples == 8, "invalid sample count"); + std::array locations{}; + for (std::uint32_t sample = 0; sample < state.samples; ++sample) { + std::array position{}; + if (state.customSampleLocations) { + const auto bits = state.sampleLocations[sample]; + position = {static_cast(bits & 7u) - static_cast(bits & 8u), static_cast((bits >> 4u) & 7u) - static_cast((bits >> 4u) & 8u)}; + } else if (state.samples != 1) { + const auto& standard = state.samples == 2 ? StandardLocations2[sample] : state.samples == 4 ? StandardLocations4[sample] : StandardLocations8[sample]; + position = {standard[0], standard[1]}; + } + locations[sample] = {0.5f + position[0] / 16.0f, 0.5f + position[1] / 16.0f}; + } + return locations; +} + ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { const auto value = read(queue.context, 0x2d5); const auto validate = [&](bool condition, const char* reason) { @@ -576,7 +642,22 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { return result; } +bool DepthResummarizeDrawsNothing(const QueueState& queue) { + const auto& cx = queue.context; + const auto control = find(cx, 0x000); + const auto depthControl = find(cx, 0x200); + const auto zInfo = find(cx, 0x010); + const auto stencilInfo = find(cx, 0x011); + const auto targetMask = find(cx, 0x8e); + const auto shaderMask = find(cx, 0x8f); + if (control == cx.end() || control->second != 0x10u || depthControl == cx.end() || zInfo == cx.end() || stencilInfo == cx.end() || targetMask == cx.end() || shaderMask == cx.end()) return false; + const auto depth = depthControl->second; + const auto format = zInfo->second & 3u; + return (depth & 2u) != 0 && (depth & 0xc000000cu) == 0 && depthPassThrough(depth) && (format == 1u || format == 3u) && (stencilInfo->second & 1u) == 0 && ColorWriteMask(cx) == 0 && PixelProgramSkipped(queue); +} + std::string DepthMaintenanceRejection(const QueueState& queue) { + if (DepthResummarizeDrawsNothing(queue)) return {}; const auto control = find(queue.context, 0x000); if (control == queue.context.end() || (control->second & ~0x2063u) == 0) return {}; const auto depthControl = find(queue.context, 0x200); @@ -619,13 +700,14 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } zero(cx, 0x207, ~LayerExports, "clip distances, layer, viewport or auxiliary vertex exports"); result.samples = decodeSamples(cx); + decodeSampleLocations(cx, result); { const auto depthControl = read(cx, 0x200); const auto renderControl = find(cx, 0x000); const bool clear = renderControl != cx.end() && (renderControl->second & 3u) != 0; const auto tests = planeTests(cx, depthControl); if (clear || (tests != 0 && depthSurfaceBound(cx))) { - decodeDepth(cx, depthControl, result); + decodeDepth(cx, depthControl, (read(cx, 0x203) & 2u) != 0 && !PixelProgramSkipped(queue), result); } else if (depthPassThrough(depthControl) || ((depthControl & 0xbu) != 0 && (tests == 0 || depthPlanesAbsent(cx)))) { static bool reported = false; if (!reported) { @@ -648,8 +730,12 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { result.alphaToCoverage = read(cx, 0x2dc) == AlphaToCoverageUndithered; if (!result.alphaToCoverage) zero(cx, 0x2dc, AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); Require(!result.depth || result.depth->samples == result.samples, "the depth target's sample count differs from the rasterizer's"); - zero(cx, 0x292, result.samples > 1 ? ScanModeMask & ~1u : ScanModeMask, "scan conversion mode"); + zero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode"); Require(result.samples == 1 || (read(cx, 0x292) & 1u) != 0, "multisampled targets rasterized without MSAA_ENABLE are unsupported"); + if (result.samples == 1 && (read(cx, 0x292) & 1u) != 0) { + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) static_cast(read(cx, 0x2feu + pixel * 4u)); + if (auto reason = singleSampleMsaaRejection(queue); !reason.empty()) throw std::runtime_error(reason); + } zero(cx, 0x293, ScanControlMask, "sample iteration, primitive discard or out-of-order rasterization"); zero(cx, 0x80, ~0u, "window offset"); zero(cx, 0x8d, ScreenOffsetMask, "reserved PA_SU_HARDWARE_SCREEN_OFFSET bits"); @@ -1004,7 +1090,7 @@ std::optional DecodeColorMetadataPass(const QueueState& queue if (mode != 2u && mode != 6u && !resolve) return std::nullopt; ColorMetadataPass pass{resolve ? ColorMetadataPass::Mode::Resolve : mode == 2u ? ColorMetadataPass::Mode::EliminateFastClear : ColorMetadataPass::Mode::DccDecompress, {}, std::nullopt}; Require((control->second & ~0x70u) == 0xcc0000u, "CB metadata pass with a nonstandard ROP, dual quads disabled or degamma"); - Require((read(cx, 0x200) & 0xfu) == 0 && (read(cx, 0x0) & 0xfu) == 0, "CB metadata pass with depth or stencil work"); + Require(depthPlanesAbsent(cx) || ((read(cx, 0x200) & 0xfu) == 0 && (read(cx, 0x0) & 0xfu) == 0), "CB metadata pass with depth or stencil work"); const auto samples = resolve ? decodeSamples(cx) : 1u; if (!resolve) zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); zero(cx, 0x80, ~0u, "window offset"); @@ -1100,7 +1186,10 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { } if (auto reason = nonzero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u), "coverage conversion or nonstandard multisampling"); !reason.empty()) return reason; const bool multisampled = value(cx, 0x2f8, word) && (word & 7u) != 0; - if (auto reason = nonzero(cx, 0x292, multisampled ? ScanModeMask & ~1u : ScanModeMask, "scan conversion mode"); !reason.empty()) return reason; + if (auto reason = nonzero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode"); !reason.empty()) return reason; + if (!multisampled && value(cx, 0x292, word) && (word & 1u) != 0) { + if (auto reason = singleSampleMsaaRejection(queue); !reason.empty()) return reason; + } if (auto reason = nonzero(cx, 0x293, ScanControlMask, "sample iteration, primitive discard or out-of-order rasterization"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x80, ~0u, "window offset"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x8d, ScreenOffsetMask, "reserved PA_SU_HARDWARE_SCREEN_OFFSET bits"); !reason.empty()) return reason; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 2753e91fc..b6072c675 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -286,7 +286,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; allocation.allocationSize = requirements.size; allocationBytes = requirements.size; - allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "texture"); Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &owned->memory), "vkAllocateMemory texture"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory"); @@ -940,7 +940,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; allocation.allocationSize = requirements.size; memoryBytes = requirements.size; - allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "storage texture"); Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory storage texture"); Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory storage"); } catch (...) { @@ -1909,7 +1909,7 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, const auto first = address / blockBytes; Require(changed.size() == (end - 1) / blockBytes - first + 1, "texture comparison block count differs"); const auto* recorder = Recorder::Active(); - const std::array stamp{GuestMemory::CollectEpoch(), GuestMemory::TrackerGeneration(), GuestMemory::ForgetSerial(), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0}; + const std::array stamp{GuestMemory::CollectEpoch(), GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetSerial(), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0}; const bool cacheable = memoize && stamp[0] != 0 && (stamp[2] & 1u) == 0 && address == descriptor.baseAddress && bytes == guestBytes; if (cacheable && comparedGuestBytes == stamp) { std::fill(changed.begin(), changed.end(), GuestMemory::BlockUnchanged); @@ -2976,6 +2976,51 @@ bool StorageTexture::PublishShadowsOnly(std::uint64_t address, std::size_t bytes return PublishShadow(address, bytes, scope, PublishReasonFor(reason)) != 0; } +namespace { + +struct StorageDumpSelector { + std::uint64_t address = 0; + std::uint32_t width = 0, height = 0, layers = 0, format = 0; + bool anyFormat = true; +}; + +const StorageDumpSelector& StorageDumpSelection() { + static const StorageDumpSelector selector = [] { + StorageDumpSelector parsed; + const char* text = std::getenv("APS5_DUMP_STORAGE"); + if (text == nullptr) return parsed; + if (const int fields = std::sscanf(text, "%ux%ux%u:%u", &parsed.width, &parsed.height, &parsed.layers, &parsed.format); fields >= 3) { + parsed.anyFormat = fields < 4; + return parsed; + } + parsed.address = std::strtoull(text, nullptr, 16); + return parsed; + }(); + return selector; +} + +bool StorageDumpSelects(const GuestTextureResource& descriptor) { + const auto& selector = StorageDumpSelection(); + if (selector.address != 0) return descriptor.baseAddress == selector.address; + return selector.width != 0 && descriptor.width == selector.width && descriptor.height == selector.height && descriptor.depthOrLastArray + 1 == selector.layers && (selector.anyFormat || descriptor.format == selector.format); +} + +} + +void StorageTexture::FlushDumpSelected(const char* reason) { + const auto& selector = StorageDumpSelection(); + if (selector.address == 0 && selector.width == 0) return; + std::vector> ranges; + { + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + for (const auto* texture : pending.textures) { + if (StorageDumpSelects(texture->descriptor)) ranges.emplace_back(texture->descriptor.baseAddress, static_cast(texture->guestBytes)); + } + } + for (const auto& [address, bytes] : ranges) static_cast(FlushPending(address, bytes, nullptr, reason)); +} + void StorageTexture::FlushAllPending(const char* reason) { static_cast(FlushPending(0, std::numeric_limits::max(), nullptr, reason)); } @@ -4257,15 +4302,15 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { detiler.Dispatch(commands, descriptor.tileMode, elementBytes, linear.Handle(), geometry.LinearLayerOffset(layer) + mip.linearOffset, tiled.Handle(), geometry.GuestLayerOffset(layer) + mip.tiledOffset, mip, true, layer, geometry.thick, {.pipeBankXor = descriptor.pipeBankXor}); } } - // Debug aid: APS5_DUMP_STORAGE= saves that storage image's first mip after each of - // its first 8 write-backs as storage_
_.raw (u32 width, height, VkFormat, then rows). - static const std::uint64_t dumpAddress = [] { const char* text = std::getenv("APS5_DUMP_STORAGE"); return text ? std::strtoull(text, nullptr, 16) : 0ull; }(); + const bool dumpSelected = StorageDumpSelects(descriptor); static int dumps = 0; std::unique_ptr dump; - if (dumpAddress != 0 && descriptor.baseAddress == dumpAddress && dumps < 8) { - dump = std::make_unique(context, static_cast(mips[0].linearSize), VK_BUFFER_USAGE_TRANSFER_DST_BIT); + if (dumpSelected && dumps < 8) { + dump = std::make_unique(context, static_cast(mips[0].linearSize * arrayLayers), VK_BUFFER_USAGE_TRANSFER_DST_BIT); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); - CopyBuffer(context, commands, linear.Handle(), mips[0].linearOffset, dump->Handle(), 0, mips[0].linearSize); + for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { + if (geometry.HasLayer(0, layer)) CopyBuffer(context, commands, linear.Handle(), geometry.LinearLayerOffset(layer) + mips[0].linearOffset, dump->Handle(), layer * mips[0].linearSize, mips[0].linearSize); + } } const auto toCopy = WholeBufferBarrier(tiled.Handle(), VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); VkImageMemoryBarrier backToGeneral = toSource; diff --git a/core/libs/prx/libSceAgcDriver/PostCompile.cmake b/core/libs/prx/libSceAgcDriver/PostCompile.cmake index 3cabf5ccd..761824014 100644 --- a/core/libs/prx/libSceAgcDriver/PostCompile.cmake +++ b/core/libs/prx/libSceAgcDriver/PostCompile.cmake @@ -28,7 +28,7 @@ foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_gra endif() endforeach() -target_sources(agc_driver_bda_device_tests PRIVATE tests/ColorTransferTests.cpp Graphics/src/ColorTargetLayout.cpp Execution/src/GuestMemory.cpp) +target_sources(agc_driver_bda_device_tests PRIVATE tests/ColorTransferTests.cpp Graphics/src/ColorTargetLayout.cpp Graphics/src/TextureTiling.cpp Graphics/src/TextureFormat.cpp Execution/src/GuestMemory.cpp) target_include_directories(agc_driver_bda_device_tests PRIVATE ${agcTextureDetileGeneratedRoot}) if(TARGET agc_driver_visual_test) diff --git a/core/libs/prx/libSceAgcDriver/Resource/include/Registration.hpp b/core/libs/prx/libSceAgcDriver/Resource/include/Registration.hpp index bcf672d32..d30f27839 100644 --- a/core/libs/prx/libSceAgcDriver/Resource/include/Registration.hpp +++ b/core/libs/prx/libSceAgcDriver/Resource/include/Registration.hpp @@ -1,4 +1,8 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_RESOURCE_INCLUDE_REGISTRATION_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_RESOURCE_INCLUDE_REGISTRATION_HPP +#include + +extern "C" bool AgcDriverWorkloadStreamRegistered_nid_postfix(std::uint32_t streamId); + #endif diff --git a/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp index 9fe7f3639..fe08199be 100644 --- a/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp +++ b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp @@ -2,15 +2,27 @@ #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" // Resource registration feeds GPU debugging tools, none of which are attached; the game treats this code as benign. static constexpr int SCE_AGC_ERROR_RESOURCE_REGISTRATION_UNAVAILABLE = static_cast(0x8A6C9018); static constexpr uint32_t RESOURCE_REGISTRATION_MAX_NAME_LENGTH = 0xfc; +static constexpr uint32_t WORKLOAD_STREAM_MIN_ID = 1; +static constexpr uint32_t WORKLOAD_STREAM_MAX_ID = 31; + +static std::mutex g_workloadStreamLock; +static uint32_t g_workloadStreams = 0; extern "C" { +bool AgcDriverWorkloadStreamRegistered_nid_postfix(uint32_t streamId) { + if (streamId < WORKLOAD_STREAM_MIN_ID || streamId > WORKLOAD_STREAM_MAX_ID) return false; + std::lock_guard lock(g_workloadStreamLock); + return (g_workloadStreams & (1u << streamId)) != 0; +} + int APS5_VABI sceAgcDriverRegisterOwner(uint32_t* owner_handle, const char* name) { (void)owner_handle; (void)name; @@ -37,8 +49,10 @@ int APS5_VABI sceAgcDriverRegisterMultipleResources(uint32_t* resourceHandle, ui } int APS5_VABI sceAgcDriverRegisterWorkloadStream(uint32_t stream_id, const void* stream) { - (void)stream_id; - (void)stream; + if (stream != nullptr && stream_id >= WORKLOAD_STREAM_MIN_ID && stream_id <= WORKLOAD_STREAM_MAX_ID) { + std::lock_guard lock(g_workloadStreamLock); + g_workloadStreams |= 1u << stream_id; + } return 0; } @@ -69,7 +83,10 @@ int APS5_VABI sceAgcDriverUnregisterAllResourcesForOwner(uint32_t owner_handle) } int APS5_VABI sceAgcDriverUnregisterWorkloadStream(uint32_t stream_id) { - (void)stream_id; + if (stream_id >= WORKLOAD_STREAM_MIN_ID && stream_id <= WORKLOAD_STREAM_MAX_ID) { + std::lock_guard lock(g_workloadStreamLock); + g_workloadStreams &= ~(1u << stream_id); + } return 0; } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 777c3fa77..392eab435 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -119,6 +119,62 @@ void importSpanTests(const Context& context) { ::operator delete(block, std::align_val_t{piece}); } +void failedImportRemapTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t piece = 65536; + auto* block = static_cast(::operator new(4 * piece, std::align_val_t{piece})); + auto importing = context; + importing.hostImportAlignment = piece; + const auto address = reinterpret_cast(block); + const auto add = [&](std::size_t index) { + GuestAllocations::Mutation mutation; + mutation.Add(block + index * piece, piece, true, true, true); + }; + const auto remove = [&](std::size_t index) { + GuestAllocations::Mutation mutation; + mutation.Remove(block + index * piece); + }; + SetHostImportWindow(0); + for (std::size_t replaced = 0; replaced < 2; ++replaced) { + add(0); + add(1); + std::weak_ptr owner; + { + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + const auto found = std::find_if(lease.begin(), lease.end(), [&](const auto& range) { return range->address == address + replaced * piece; }); + Require(found != lease.end(), "failed import fixture has no registered owner"); + owner = *found; + } + access.limitMemory(0); + const auto attempts = access.allocationAttempts(); + Require(HostImportFor(importing, address, 2 * piece) == nullptr && access.allocationAttempts() == attempts + 1, "host import failure was not exercised"); + Require(owner.use_count() == 1, "a failed import pins its owner"); + Require(HostImportFor(importing, address, 2 * piece) == nullptr && access.allocationAttempts() == attempts + 1, "an unchanged failed import was retried"); + add(3); + Require(HostImportFor(importing, address, 2 * piece) == nullptr && access.allocationAttempts() == attempts + 1, "an unrelated mapping retried a failed import"); + remove(replaced); + Require(owner.expired(), "a failed import kept the replaced owner alive"); + add(replaced); + access.limitMemory(std::nullopt); + const auto* imported = HostImportFor(importing, address, 2 * piece); + Require(imported != nullptr && imported->base == address && imported->bytes == 2 * piece && access.allocationAttempts() == attempts + 2, "same-address remap retained a failed import decision"); + remove(0); + remove(1); + remove(3); + ClearHostImports(importing.device); + } + add(0); + add(1); + access.limitMemory(0); + Require(HostImportFor(importing, address, 2 * piece) == nullptr, "span failure was not exercised"); + remove(1); + access.limitMemory(std::nullopt); + const auto* shortened = HostImportFor(importing, address, piece); + Require(shortened != nullptr && shortened->bytes == piece, "a failed span suppressed an import of its remaining range"); + remove(0); + ClearHostImports(importing.device); + ::operator delete(block, std::align_val_t{piece}); +} + void importWindowTests(const Context& context) { constexpr std::size_t piece = 65536; constexpr std::size_t window = 2 * piece; @@ -1105,6 +1161,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { } importCrossingTests(context, access); importSpanTests(context); + failedImportRemapTests(context, access); importWindowTests(context); heapMirrorTests(context, access); importedHeapMirrorTests(context, access); diff --git a/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp b/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp new file mode 100644 index 000000000..1a811755b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp @@ -0,0 +1,91 @@ +#include "IntermediateRepresentation/IrBuilder.hpp" +#include "Optimization/ConstantFolder.hpp" +#include "Optimization/DeadCodeEliminator.hpp" +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void CheckDiamond(unsigned mode) { + IrProgram program; + for (std::uint32_t id = 0; id < 4u; ++id) { + auto& block = program.CreateBlock(); + program.BlockOrder().push_back(&block); + BlockInfo info; + info.id = id; + info.terminator.kind = TerminatorKind::Return; + program.Metadata().blockInfo.push_back(info); + } + auto& entry = *program.BlockOrder()[0]; + auto& left = *program.BlockOrder()[1]; + auto& right = *program.BlockOrder()[2]; + auto& merge = *program.BlockOrder()[3]; + program.SetEntryBlock(entry); + entry.AddBranch(&left); + entry.AddBranch(&right); + left.AddBranch(&merge); + right.AddBranch(&merge); + auto& entryInfo = program.Metadata().blockInfo[0]; + entryInfo.terminator.kind = TerminatorKind::ConditionalBranch; + entryInfo.terminator.trueBlock = 1u; + entryInfo.terminator.falseBlock = 2u; + for (std::uint32_t id = 1; id < 3u; ++id) { + program.Metadata().blockInfo[id].terminator.kind = TerminatorKind::Branch; + program.Metadata().blockInfo[id].terminator.trueBlock = 3u; + } + IrBuilder builder(program); + builder.SetInsertionPoint(entry); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + auto& limit = builder.Constant(16u); + auto& condition = builder.Emit(IrOpcode::ULessThan32, IrType::Bool, {&lane, &limit}); + entryInfo.condition = &condition; + auto& one = builder.Constant(1u); + auto& two = builder.Constant(2u); + IrValue* first = &lane; + IrValue* second = &lane; + if (mode == 0u || mode == 1u) { + builder.SetInsertionPoint(left); + first = mode == 0u ? &builder.Emit(IrOpcode::SelectU32, IrType::U32, {&condition, &one, &two}) : &builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + builder.SetInsertionPoint(right); + second = mode == 0u ? &builder.Emit(IrOpcode::SelectU32, IrType::U32, {&condition, &one, &two}) : &builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + } else if (mode == 3u || mode == 4u) { + first = &one; + second = &program.CreateValue(IrOpcode::Void, IrType::U32); + second->SetImmediateU32(mode == 3u ? 1u : 2u); + } + builder.SetInsertionPoint(merge); + auto& phi = builder.Emit(IrOpcode::Phi, IrType::U32, {}); + phi.AddPhiOperand(&left, first); + phi.AddPhiOperand(&right, second); + auto& reference = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&phi}); + ValidateProgram(program, true); + ConstantFolder{}.Fold(program); + DeadCodeEliminator{}.RemoveIdentities(program); + DeadCodeEliminator{}.Eliminate(program); + ValidateProgram(program, true); + const auto* result = reference.Argument(0)->Resolve(); + if (mode == 0u || mode == 1u || mode == 4u) { + Require(result == &phi && phi.Argument(0) == first && phi.Argument(1) == second, "phi lost distinct branch definitions or constants"); + } else { + Require(result == first, "phi did not fold identical definitions or equal constants"); + } +} + +} + +int main() { + try { + for (unsigned mode = 0; mode < 5u; ++mode) CheckDiamond(mode); + std::puts("constant phi dominance tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index 030af961d..96708260f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -566,6 +566,16 @@ done: buffer_store_dword v1, off, s[0:3], 0 s_endpgm)", Store({0x7d880090u, 0xbf860004u, 0x7d880088u, 0xbf870003u, 0x4a020081u, 0xbf820005u, 0x4a020082u, 0x7d880084u, 0xbf860001u, 0x4a020285u, 0x4a020283u}), Split::Clone}, + {"memory access in a single-block loop latch", R"( + s_mov_b32 s8, 0 + v_mov_b32 v1, 0 +loop: + buffer_store_dword v1, off, s[0:3], 0 + s_add_u32 s8, s8, 1 + s_cmp_lt_u32 s8, 4 + s_cbranch_scc1 loop + s_endpgm)", + {0xbe880380u, 0x7e020280u, 0xe0700000u, 0x80000100u, 0x80088108u, 0xbf0a8408u, 0xbf85fffbu, 0xbf810000u}, Split::None}, {"shared tail in a loop", R"( s_mov_b32 s8, 0 v_mov_b32 v1, 0 diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp index 63ddb1ae1..bf83e852c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp @@ -186,5 +186,26 @@ void RunDepthSurfaceReuseTests() { Require(lookup(context, {stencil, FormatR8G8B8A8Unorm, 320, 180, TileRenderTarget64KB}) == nullptr, "an RGBA8 view of a stencil plane's own extent must be read as reused memory"); Require(lookup(context, {stencil, FormatR32Float, 320, 180}) == nullptr, "an R32F view of a stencil plane's own extent must be read as reused memory"); + const auto storageAliasesDepth = [&](const Context& owner, const View& view) { + const auto words = tsharp(view); + return DepthSurfaceAt(owner, words, DecodeTextureResource(words)); + }; + Require(storageAliasesDepth(context, {depth32, FormatR32Float, 384, 384}), "storage access to the actual D32 plane must remain rejected"); + Require(storageAliasesDepth(context, {depth16, FormatR16Uint, 256, 256}), "raw D16 storage access must remain rejected"); + Require(!storageAliasesDepth(context, {depth32, FormatR32Float, 128, 128}), "temporary memory reused at a different extent is not a depth storage alias"); + Require(!storageAliasesDepth(context, {depth32, FormatR11G11B10Float, 384, 384}), "temporary memory reused for a color format is not a depth storage alias"); + Require(!storageAliasesDepth(context, {depth32, FormatR32Uint, 384, 384, TileRenderTarget64KB}), "raw integer color memory without a depth layout is not a depth storage alias"); + auto otherDevice = context; + static int otherDeviceTag = 0; + otherDevice.device = reinterpret_cast(&otherDeviceTag); + Require(!storageAliasesDepth(otherDevice, {depth32, FormatR32Float, 384, 384}), "a depth surface on another device must not block a storage lookup"); + + constexpr std::uint64_t stencil16 = 0x60000000; + DepthSurfaceView(context, {depth16, stencil16, {128, 128}, VK_FORMAT_D16_UNORM_S8_UINT, 1.0f, 0}); + Require(!storageAliasesDepth(context, {depth16, FormatR16Unorm, 256, 256}), "an older depth extent at the same address must not override the newest surface"); + Require(storageAliasesDepth(context, {depth16, FormatR16Unorm, 128, 128}), "the newest depth extent must still reject actual storage access"); + Require(storageAliasesDepth(context, {stencil16, FormatR8Uint, 128, 128}), "storage access to the actual stencil plane must remain rejected"); + Require(!storageAliasesDepth(context, {stencil16, FormatR11G11B10Float, 128, 128}), "a stencil address reused for color is not a stencil storage alias"); + ClearDepthSurfaces(context.device); } diff --git a/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp b/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp new file mode 100644 index 000000000..c139312bf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp @@ -0,0 +1,71 @@ +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include +#include +#include +#include + +using namespace AgcDriver::Graphics; + +namespace { + +int failures = 0; + +void Expect(bool condition, const std::string& what) { + if (condition) return; + std::fprintf(stderr, "FAIL: %s\n", what.c_str()); + ++failures; +} + +constexpr std::uint32_t HostType = 0; +constexpr std::uint32_t DeviceType = 1; +constexpr std::uint64_t MiB = 1ull << 20u; + +std::uint64_t reportedBudget = 0; +std::uint64_t reportedUsage = 0; + +VKAPI_ATTR void VKAPI_CALL mockMemoryProperties2(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) { + auto* reported = static_cast(properties->pNext); + reported->heapBudget[0] = reportedBudget; + reported->heapUsage[0] = reportedUsage; +} + +Context mockContext(bool budgetExtension) { + Context context{}; + context.physical = reinterpret_cast(static_cast(1)); + context.memoryProperties2 = budgetExtension ? mockMemoryProperties2 : nullptr; + context.memory.memoryHeapCount = 2; + context.memory.memoryHeaps[0] = {4096 * MiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + context.memory.memoryHeaps[1] = {16384 * MiB, 0}; + context.memory.memoryTypeCount = 2; + context.memory.memoryTypes[HostType] = {0, 1}; + context.memory.memoryTypes[DeviceType] = {VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0}; + return context; +} + +void Choose(const char* what, bool budgetExtension, std::uint64_t budget, std::uint64_t usage, std::uint32_t mask, std::uint32_t expected) { + reportedBudget = budget; + reportedUsage = usage; + const auto type = mockContext(budgetExtension).DeviceMemoryType(mask, 64 * MiB, "test"); + Expect(type == expected, std::string(what) + ": chose memory type " + std::to_string(type) + ", expected " + std::to_string(expected)); +} + +} + +int main() { + try { + const std::uint32_t both = (1u << HostType) | (1u << DeviceType); + Choose("no VK_EXT_memory_budget", false, 0, 0, both, DeviceType); + Choose("budget not reported", true, 0, 4000 * MiB, both, DeviceType); + Choose("room in the device-local budget", true, 3000 * MiB, 2000 * MiB, both, DeviceType); + Choose("allocation fills the budget exactly", true, 3000 * MiB, 2936 * MiB, both, DeviceType); + Choose("allocation exceeds the budget", true, 3000 * MiB, 2937 * MiB, both, HostType); + Choose("usage already over the budget", true, 2952 * MiB, 3633 * MiB, both, HostType); + Choose("no host type allowed", true, 3000 * MiB, 3000 * MiB, 1u << DeviceType, DeviceType); + } catch (const std::exception& error) { + std::fprintf(stderr, "FAIL: %s\n", error.what()); + return 1; + } + if (failures != 0) return 1; + std::puts("device memory placement tests passed"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp index 4ce4faa60..c09560589 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/PresentMode.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" @@ -265,6 +266,46 @@ void testFlipHold() { AgcDriverUnregisterVideoOutput_nid_postfix(7, output); } +void testComputeRenderingWait() { + auto output = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(7, output); + std::array wait{0xc0021018, 8, 0, 0}; + Packet packet{wait.data(), 4, 0, {}}; + check(expectFailure([&] { sceAgcDriverSubmitAcb(0x20, &packet); }).find("unregistered video output") != std::string::npos, "a compute rendering wait on an unregistered output was accepted"); + wait = {0xc0021018, 7, 0, 1}; + check(expectFailure([&] { sceAgcDriverSubmitAcb(0x20, &packet); }).find("rendering wait mode") != std::string::npos, "a compute rendering wait with a mode was accepted"); + { + std::lock_guard lock(output->state->mutex); + output->state->gate = true; + } + struct GateRelease { + std::shared_ptr state; + ~GateRelease() { + { + std::lock_guard lock(state->mutex); + state->gate = false; + } + state->changed.notify_all(); + } + } gateRelease{output->state}; + wait = {0xc0021018, 7, 0, 0}; + check(sceAgcDriverSubmitAcb(0x20, &packet) == 0, "compute rendering wait submission failed"); + { + std::unique_lock lock(output->state->mutex); + check(output->state->changed.wait_for(lock, std::chrono::seconds(5), [&] { return output->state->waiting; }), "compute worker did not reach the rendering wait"); + } + auto idle = std::async(std::launch::async, [] { AgcDriverWaitIdle_nid_postfix(); }); + check(idle.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout, "the driver went idle past a closed compute rendering wait"); + { + std::lock_guard lock(output->state->mutex); + output->state->gate = false; + } + output->state->changed.notify_all(); + check(idle.wait_for(std::chrono::seconds(5)) == std::future_status::ready, "a compute rendering wait stayed blocked after release"); + idle.get(); + AgcDriverUnregisterVideoOutput_nid_postfix(7, output); +} + void testFailure() { auto output = std::make_shared(); output->state->fail = true; @@ -298,10 +339,34 @@ void testReset(bool compute) { } +void testPresentMode() { + using AgcDriver::SelectPresentMode; + const std::array wayland{VK_PRESENT_MODE_FIFO_KHR, VK_PRESENT_MODE_IMMEDIATE_KHR}; + const std::array all{VK_PRESENT_MODE_IMMEDIATE_KHR, VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_FIFO_KHR}; + check(SelectPresentMode(nullptr, wayland) == VK_PRESENT_MODE_FIFO_KHR, "the default present mode is not FIFO"); + check(SelectPresentMode("fifo", wayland) == VK_PRESENT_MODE_FIFO_KHR, "fifo was not selected"); + check(SelectPresentMode("immediate", wayland) == VK_PRESENT_MODE_IMMEDIATE_KHR, "immediate was not selected"); + check(SelectPresentMode("mailbox", all) == VK_PRESENT_MODE_MAILBOX_KHR, "mailbox was not selected"); + const std::array latest{VK_PRESENT_MODE_FIFO_KHR, VK_PRESENT_MODE_FIFO_LATEST_READY_KHR}; + check(SelectPresentMode("fifo_latest_ready", latest) == VK_PRESENT_MODE_FIFO_LATEST_READY_KHR, "fifo_latest_ready was not selected"); + check(SelectPresentMode(nullptr, latest) == VK_PRESENT_MODE_FIFO_KHR, "the default changed on a surface with fifo_latest_ready"); + check(expectFailure([&] { SelectPresentMode("fifo_latest_ready", wayland); }).find("does not support") != std::string::npos, "fifo_latest_ready was accepted on a surface without it"); + check(expectFailure([&] { SelectPresentMode("mailbox", wayland); }).find("does not support") != std::string::npos, "an unsupported present mode was accepted"); + check(expectFailure([&] { SelectPresentMode("Immediate", all); }).find("must be fifo, fifo_latest_ready, mailbox or immediate") != std::string::npos, "an unknown present mode name was accepted"); + check(expectFailure([&] { SelectPresentMode("", all); }).find("must be fifo, fifo_latest_ready, mailbox or immediate") != std::string::npos, "an empty present mode name was accepted"); + const std::array none{}; + check(expectFailure([&] { SelectPresentMode(nullptr, none); }).find("does not support") != std::string::npos, "FIFO was selected on a surface without it"); +} + int main(int argc, char** argv) { try { + if (argc == 2 && std::string(argv[1]) == "present-mode") { + testPresentMode(); + std::puts("present mode selection tests passed"); + return 0; + } if (argc == 2) testReset(std::string(argv[1]) == "compute"); - else { testFlipAndBoundary(); testFlipHold(); testFailure(); } + else { testFlipAndBoundary(); testFlipHold(); testComputeRenderingWait(); testFailure(); } const auto shutdown = expectFailure([] { LibcRunShutdown_nid_postfix(); }); check(shutdown.find(argc == 2 ? (std::string(argv[1]) == "compute" ? "not readable" : "required shader register") : "intentional flip failure") != std::string::npos, "shutdown lost worker failure"); std::puts("AGC flip and suspend tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp index d4ae978ef..24ed571cd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp @@ -19,8 +19,8 @@ void Check(bool condition, const std::string& message) { } SpirvTarget Target() { - static constexpr std::array capabilities{ - spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, + static constexpr std::array capabilities{ + spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance, }; static constexpr std::array extensions{ "SPV_EXT_descriptor_indexing", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", "SPV_KHR_physical_storage_buffer", @@ -35,6 +35,7 @@ struct Counts { std::size_t calls = 0; std::size_t functions = 0; std::size_t notInlined = 0; + std::size_t compareExchanges = 0; }; Counts Count(std::span words) { @@ -46,6 +47,7 @@ Counts Count(std::span words) { const auto opcode = words[offset] & spv::OpCodeMask; if (opcode == spv::OpSwitch) ++counts.switches; if (opcode == spv::OpFunctionCall) ++counts.calls; + if (opcode == spv::OpAtomicCompareExchange) ++counts.compareExchanges; if (opcode == spv::OpFunction) { ++counts.functions; if (count > 3 && (words[offset + 3] & spv::FunctionControlDontInlineMask) != 0) ++counts.notInlined; @@ -55,24 +57,27 @@ Counts Count(std::span words) { return counts; } -Counts Compile(std::size_t loads, bool formattedStores = false, bool gpu = false) { +Counts Compile(std::size_t loads, bool formattedStores = false, bool gpu = false, bool vertex = false, bool registration = false) { std::vector code; if (gpu) code.push_back(0x7e160500u); for (std::uint32_t load = 0; load < loads; ++load) code.insert(code.end(), {0xe0000000u | (load * 4u), 0x80020000u | ((10u + load) << 8u)}); code.push_back(0xbf8c3f70u); for (std::uint32_t load = 0; load < loads; ++load) code.insert(code.end(), {(formattedStores ? 0xe0100000u : 0xe0700000u) | (load * 4u), 0x80010000u | ((10u + load) << 8u)}); + if (vertex) code.insert(code.end(), {0xf80008cfu, 0x03020100u}); code.push_back(0xbf810000u); const std::span words(code); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(words)}}}; const std::array userData{0u, 0u, 0u, 0u, 0x10000000u, 0u, 0x1000u, 0x31016facu, 0x20000000u, 0u, 0x1000u, 0x31038facu}; RecompileRequest request{}; - request.shader = {ShaderStage::Compute, reinterpret_cast(code.data()), words, 0, {}}; + request.shader = {vertex ? ShaderStage::Vertex : ShaderStage::Compute, reinterpret_cast(code.data()), words, 0, {}}; request.target = Target(); + request.target.registrationModules = registration; request.layout = {0, 0, 0, 128}; request.context.waveSize = 32; request.context.userDataBaseRegister = 0; request.context.userData = userData; - request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + if (vertex) request.context.vertex = ShaderVertexStageInfo{}; + else request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; request.context.memory = memory; request.useCache = false; return Count(Recompile(request).spirv.Words()); @@ -85,6 +90,12 @@ int main() { const auto none = Compile(0); const auto one = Compile(1); const auto three = Compile(3); + const auto gpuOne = Compile(1, false, true); + const auto vertexSpecialized = Compile(1, false, false, true, false); + const auto vertexRegistration = Compile(1, false, false, true, true); + Check(vertexRegistration.compareExchanges == vertexSpecialized.compareExchanges, "formatted buffer once: in registration form a store through an aligned V# kept the unaligned compare-exchange path (" + std::to_string(vertexRegistration.compareExchanges) + " compare-exchanges, " + std::to_string(vertexSpecialized.compareExchanges) + " specialized)"); + Check(vertexRegistration.words < vertexSpecialized.words + vertexSpecialized.words / 2u, "formatted buffer once: in registration form a formatted load from a known descriptor kept every format's decoder (" + std::to_string(vertexRegistration.words) + " words, " + std::to_string(vertexSpecialized.words) + " specialized)"); + Check(one.words < gpuOne.words / 2u, "formatted buffer once: a formatted load from a known descriptor kept every format's decoder (" + std::to_string(one.words) + " words, a GPU-selected descriptor needs " + std::to_string(gpuOne.words) + ")"); Check(one.calls == none.calls + 1 && one.functions == none.functions + 1, "formatted buffer once: one formatted load emitted " + std::to_string(one.calls - none.calls) + " calls and " + std::to_string(one.functions - none.functions) + " functions more than none, expected one call to one shared function"); Check(three.calls == one.calls + 2 && three.functions == one.functions, "formatted buffer once: two more formatted loads from the same descriptor emitted " + std::to_string(three.calls - one.calls) + " calls and " + std::to_string(three.functions - one.functions) + " functions more, expected two calls and no function"); Check(three.switches == one.switches, "formatted buffer once: two more formatted loads from the same descriptor emitted " + std::to_string(three.switches - one.switches) + " more format switches"); @@ -94,12 +105,11 @@ int main() { Check(storeOne.functions == none.functions + 2 && storeOne.calls == none.calls + 2, "formatted buffer once: a formatted load and a formatted store to another resource emitted " + std::to_string(storeOne.functions - none.functions) + " functions and " + std::to_string(storeOne.calls - none.calls) + " calls more than none, expected one function and one call each"); Check(storeThree.functions == storeOne.functions && storeThree.calls == storeOne.calls + 4 && storeThree.switches == storeOne.switches, "formatted buffer once: two more formatted loads and stores emitted " + std::to_string(storeThree.functions - storeOne.functions) + " functions, " + std::to_string(storeThree.calls - storeOne.calls) + " calls and " + std::to_string(storeThree.switches - storeOne.switches) + " switches more, expected four calls only"); Check(three.words - one.words < one.words / 10u, "formatted buffer once: 2 more formatted loads added " + std::to_string(three.words - one.words) + " SPIR-V words to a " + std::to_string(one.words) + "-word module"); - const auto gpuOne = Compile(1, false, true); const auto gpuThree = Compile(3, false, true); Check(gpuThree.functions == gpuOne.functions && gpuThree.calls > gpuOne.calls, "formatted buffer once: two more formatted loads from a GPU-selected descriptor emitted " + std::to_string(gpuThree.functions - gpuOne.functions) + " more functions, expected calls to the shared one"); Check(gpuThree.switches == gpuOne.switches, "formatted buffer once: two more formatted loads from a GPU-selected descriptor emitted " + std::to_string(gpuThree.switches - gpuOne.switches) + " more format switches"); Check(gpuThree.words - gpuOne.words < gpuOne.words / 10u, "formatted buffer once: 2 more formatted loads from a GPU-selected descriptor added " + std::to_string(gpuThree.words - gpuOne.words) + " SPIR-V words to a " + std::to_string(gpuOne.words) + "-word module"); - std::printf("formatted buffer once tests passed (%zu words for 1 load, %zu for 3; GPU-selected %zu and %zu)\n", one.words, three.words, gpuOne.words, gpuThree.words); + std::printf("formatted buffer once tests passed (%zu words for 1 load, %zu for 3; GPU-selected %zu and %zu; vertex %zu specialized, %zu registration)\n", one.words, three.words, gpuOne.words, gpuThree.words, vertexSpecialized.words, vertexRegistration.words); return 0; } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 0103b5aec..f1d04fa21 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -450,6 +450,73 @@ void hardwareScreenOffsetTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); } +void MsaaEnableSingleSampleTests() { + using AgcDriver::Graphics::DecodeState; + using AgcDriver::Graphics::DrawRejection; + const auto enabled = [] { + auto queue = makeState(); + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + queue.context[0x292] = 3; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0; + return queue; + }; + const auto refused = [](const AgcDriver::QueueState& queue, std::string_view reason) { + expectFailure([&] { DecodeState(queue); }, reason); + const auto rejection = DrawRejection(queue, false); + Require(rejection.find(reason) != std::string::npos, "the precheck did not refuse MSAA_ENABLE on a single-sample target (" + std::string(reason) + "): " + rejection); + }; + auto queue = enabled(); + auto clear = queue; + clear.context[0x292] = 2; + const auto baseline = DecodeState(clear); + for (const auto locations : {0x00000000u, 0xcc44cc00u}) { + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = locations; + const auto state = DecodeState(queue); + Require(state.samples == 1 && state.colors.size() == 1 && state.colors[0].samples == 1 && !state.alphaToCoverage, "MSAA_ENABLE on a single-sample target did not decode as single-sampled"); + Require(state.topology == baseline.topology && state.conservativeRasterization == baseline.conservativeRasterization && state.cullMode == baseline.cullMode && state.frontFace == baseline.frontFace, "MSAA_ENABLE on a single-sample target changed the rasterizer state"); + Require(state.scissor.offset.x == baseline.scissor.offset.x && state.scissor.offset.y == baseline.scissor.offset.y && state.scissor.extent.width == baseline.scissor.extent.width && state.scissor.extent.height == baseline.scissor.extent.height, "MSAA_ENABLE on a single-sample target changed the scissor"); + Require(state.colors[0].address == baseline.colors[0].address && state.colors[0].format == baseline.colors[0].format && state.blend.colorWriteMask == baseline.blend.colorWriteMask, "MSAA_ENABLE on a single-sample target changed the color target"); + const auto rejection = DrawRejection(queue, false); + Require(rejection.empty(), "the precheck refused MSAA_ENABLE on a single-sample target: " + rejection); + } + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + for (const auto location : {0x04u, 0x40u, 0xc4u}) { + queue = enabled(); + queue.context[0x2fe + pixel * 4u] = location; + refused(queue, "off the pixel center"); + } + } + queue = enabled(); + queue.context.erase(0x306); + expectFailure([&] { DecodeState(queue); }, "missing register"); + queue = enabled(); + queue.context[0x2dc] = 0xaa01; + refused(queue, "MSAA_ENABLE on a single-sample target with alpha-to-coverage"); + queue = enabled(); + queue.context[0x313] = 0x6001; + refused(queue, "MSAA_ENABLE on a single-sample target with conservative rasterization"); + for (const auto primitive : {2u, 7u, 17u}) { + queue = enabled(); + queue.userConfig[0x242] = primitive; + refused(queue, "primitives other than triangles"); + } + queue = enabled(); + queue.userConfig[0x242] = 1; + Require(DrawRejection(queue, false).find("primitives other than triangles") != std::string::npos, "the precheck accepted MSAA_ENABLE on a single-sample target for points"); + for (const auto primitive : {5u, 6u}) { + queue = enabled(); + queue.userConfig[0x242] = primitive; + Require(DecodeState(queue).samples == 1 && DrawRejection(queue, false).empty(), "MSAA_ENABLE on a single-sample target was refused for a triangle fan or strip"); + } + queue = enabled(); + queue.context[0x292] = 7; + refused(queue, "scan conversion mode"); + clear.context[0x2fe] = 0x44; + clear.context[0x2dc] = 0xaa01; + Require(DecodeState(clear).alphaToCoverage && DrawRejection(clear, false).empty(), "a single-sample target without MSAA_ENABLE was refused over its sample locations or alpha-to-coverage"); +} + void ShaderStageTests() { auto queue = makeState(); for (const auto routing : {0x2000u, 0x2010u, 0x02002000u, 0x02002010u}) { @@ -1021,6 +1088,26 @@ void ReversedComponentOrderTests() { } } +void Color1555TargetTests() { + auto queue = makeState(); + queue.context[0x31c] = (queue.context[0x31c] & ~((0x1fu << 2u) | (7u << 8u) | (3u << 11u))) | (17u << 2u); + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.colors.size() == 1 && state.colors[0].format == VK_FORMAT_A1R5G5B5_UNORM_PACK16 && state.colors[0].elementBytes == 2 && state.colors[0].componentMapping == 0xc6u, "a 1_5_5_5 target did not render through A1R5G5B5 with X and Z exchanged"); + Require(AgcDriver::Graphics::ExportMappings(state)[0] == 0xc6u && state.blends[0].colorWriteMask == 0xfu, "a 1_5_5_5 target did not write all four channels through its export mapping"); + queue.context[0x8e] = 0x1u; + Require(AgcDriver::Graphics::DecodeState(queue).blends[0].colorWriteMask == VK_COLOR_COMPONENT_B_BIT, "a 1_5_5_5 target writing only X did not write the A1R5G5B5 channel in its low bits"); + queue.context[0x8e] = 0xfu; + for (std::uint32_t i = 0; i < 4; ++i) queue.context[0x105 + i] = 0; + queue.context[0x1e0] = 0x40000504u; + const auto blended = AgcDriver::Graphics::DecodeState(queue); + Require(blended.blends[0].blendEnable && blended.blends[0].srcColorBlendFactor == VK_BLEND_FACTOR_SRC_ALPHA && blended.blends[0].dstColorBlendFactor == VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, "alpha blending into a 1_5_5_5 target was not decoded"); + queue.context[0x1e0] = 0; + for (const auto& [number, swap] : {std::pair{1u, 0u}, std::pair{4u, 0u}, std::pair{0u, 1u}}) { + queue.context[0x31c] = (queue.context[0x31c] & ~((7u << 8u) | (3u << 11u))) | (number << 8u) | (swap << 11u); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported color format 17 number type " + std::to_string(number) + " component swap " + std::to_string(swap)); + } +} + void CompactedExportTests() { alignas(256) static std::array slotFourMemory{}; const auto slotFour = reinterpret_cast(slotFourMemory.data()); @@ -1299,6 +1386,32 @@ void depthMaintenanceTests() { auto absent = makeState(); absent.context.erase(0x000); Require(AgcDriver::Graphics::DepthMaintenanceRejection(absent).empty(), "an absent depth control produced a maintenance verdict"); + auto readOnly = makeState(); + readOnly.context[0x000] = 0x10; + readOnly.context[0x200] = 0x007007f3; + readOnly.context[0x010] = 0xa0000183; + readOnly.context[0x011] = 0x20000180; + readOnly.context[0x8e] = 0xffffffff; + readOnly.context[0x8f] = 0; + readOnly.shader.erase(0x8); + readOnly.shader.erase(0x9); + Require(AgcDriver::Graphics::DepthMaintenanceRejection(readOnly).empty(), "a resummarize draw with pass-through read-only depth and no stencil plane was rejected"); + for (const auto [offset, value] : {std::pair{0x200u, 0x007007f7u}, {0x200u, 0x007007fbu}, {0x200u, 0x007007e3u}, {0x011u, 0x20000181u}, {0x8fu, 0xfu}, {0x000u, 0x11u}, {0x010u, 0xa0000182u}}) { + auto changed = readOnly; + changed.context[offset] = value; + Require(!AgcDriver::Graphics::DepthMaintenanceRejection(changed).empty(), "a resummarize draw with surface work or unsupported depth passed"); + } + auto pixel = readOnly; + pixel.shader[0x8] = 1; + pixel.shader[0x9] = 0; + pixel.context[0x1c4] = 0; + pixel.context[0x203] = 0x40; + Require(!AgcDriver::Graphics::DepthMaintenanceRejection(pixel).empty(), "a resummarize draw with a running pixel stage passed"); + for (const auto offset : {0x200u, 0x010u, 0x011u, 0x8eu, 0x8fu}) { + auto missing = readOnly; + missing.context.erase(offset); + Require(!AgcDriver::Graphics::DepthMaintenanceRejection(missing).empty(), "a resummarize draw without a required register passed"); + } } // SPI_SHADER_Z_FORMAT (0x1c4) and the export enables of DB_SHADER_CONTROL (0x203): Z export needs a @@ -1434,6 +1547,23 @@ void metadataPassTests() { queue.context[0x202] = 0xcc0060; queue.context[0x200] = 2; expectFailure([&] { DecodeColorMetadataPass(queue); }, "depth or stencil work"); + queue.context[0x10] = 0x80000180; + queue.context[0x11] = 0x20000180; + queue.context[0x200] = 0x700763; + queue.context[0x0] = 0x20; + Require(DecodeColorMetadataPass(queue).has_value(), "depth and stencil state without depth or stencil planes refused the pass"); + queue.context[0x10] = 0x80000183; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "depth or stencil work"); + queue.context[0x10] = 0x80000180; + queue.context[0x11] = 0x20000181; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "depth or stencil work"); + queue.context[0x11] = 0x20000180; + queue.context.erase(0x200); + queue.context.erase(0x0); + Require(DecodeColorMetadataPass(queue).has_value(), "unwritten depth and render control refused a pass without depth or stencil planes"); + queue.context.erase(0x10); + queue.context.erase(0x11); + queue.context[0x0] = 0; queue.context[0x200] = 0x70; Require(DecodeColorMetadataPass(queue).has_value(), "an always-pass depth function without a test refused the pass"); queue.context[0x91] = 0x40020; @@ -1721,6 +1851,18 @@ void uint16ExportTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "color export format 8"); } +VkSampleCountFlags depthSampleCounts = VK_SAMPLE_COUNT_4_BIT; +VkResult depthSampleResult = VK_SUCCESS; +std::uint32_t depthSampleQueries = 0; + +VKAPI_ATTR VkResult VKAPI_CALL sampleLocationDepthProperties(VkPhysicalDevice, VkFormat format, VkImageType type, VkImageTiling tiling, VkImageUsageFlags usage, VkImageCreateFlags flags, VkImageFormatProperties* properties) { + Require(format == VK_FORMAT_D32_SFLOAT && type == VK_IMAGE_TYPE_2D && tiling == VK_IMAGE_TILING_OPTIMAL, "sample location query changed the depth image format or type"); + Require(usage == (VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT) && flags == VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT, "sample location query differs from depth image creation"); + ++depthSampleQueries; + properties->sampleCounts = depthSampleCounts; + return depthSampleResult; +} + void multisampleTests() { using AgcDriver::Graphics::ColorMetadataPass; using AgcDriver::Graphics::DecodeColorMetadataPass; @@ -1729,13 +1871,19 @@ void multisampleTests() { auto queue = makeState(); queue.context[0x2f8] = 0x00100001; queue.context[0x292] = 3; + queue.context[0x3b8] = 0x0dc00000u; for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0xcc44; queue.context[0x31d] = 0x9000; - queue.context[0x3b8] |= (0x18u << 19u) | 0x4000000u; queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; return queue; }; + const auto cmaskAddress = reinterpret_cast(cmaskMemory.data()); + const auto setCmaskAddress = [&](auto& target, std::uint32_t slot = 0u) { + const auto stride = slot * 0xfu; + target.context[0x31f + stride] = static_cast(cmaskAddress >> 8u); + target.context[0x398 + slot] = static_cast(cmaskAddress >> 40u); + }; auto queue = multisampled(); auto state = DecodeState(queue); Require(state.samples == 2 && state.colors.size() == 1 && state.colors[0].samples == 2 && state.colors[0].cmaskAddress == 0, "2x multisampling did not decode"); @@ -1752,6 +1900,75 @@ void multisampleTests() { } state = DecodeState(queue); Require(state.samples == 8 && state.colors.size() == 1 && state.colors[0].samples == 8, "the standard 8x pattern (sample 7 at (7, -7), PPSA99902's registers) did not decode as standard"); + queue.context[0x2f8] = 0x00200002; + queue.context[0x31d] = 0x12000; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x22eea66a; + state = DecodeState(queue); + Require(state.samples == 4 && state.customSampleLocations, "uniform custom sample locations were not decoded"); + const auto locations = AgcDriver::Graphics::SampleLocations(state); + const std::array expected{{{0.125f, 0.875f}, {0.875f, 0.125f}, {0.375f, 0.375f}, {0.625f, 0.625f}}}; + for (std::size_t sample = 0; sample < expected.size(); ++sample) Require(locations[sample].x == expected[sample].x && locations[sample].y == expected[sample].y, "custom sample coordinates or indices changed"); + AgcDriver::Graphics::Context sampleContext{}; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "VK_EXT_sample_locations"); + sampleContext.sampleLocationSampleCounts = VK_SAMPLE_COUNT_2_BIT; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "sample count"); + sampleContext.sampleLocationSampleCounts |= VK_SAMPLE_COUNT_4_BIT; + AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); + sampleContext.imageFormatProperties = sampleLocationDepthProperties; + using AgcDriver::Graphics::SampleLocationsCompatibleDepth; + Require(SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 4), "supported custom sample depth format was refused"); + state.depth = AgcDriver::Graphics::DepthTarget{}; + state.depth->format = VK_FORMAT_D32_SFLOAT; + state.depth->samples = 4; + Require(AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state), "supported custom depth sample locations were disabled"); + depthSampleCounts = VK_SAMPLE_COUNT_2_BIT; + Require(!SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 4), "unsupported custom sample depth count was accepted"); + expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "compatible depth format and sample count"); + depthSampleCounts = VK_SAMPLE_COUNT_4_BIT; + depthSampleResult = VK_ERROR_FORMAT_NOT_SUPPORTED; + Require(!SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 4), "unsupported custom sample depth format was accepted"); + expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "compatible depth format and sample count"); + state.customSampleLocations = false; + Require(!AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state), "standard locations did not fall back for an incompatible depth format"); + depthSampleResult = VK_ERROR_OUT_OF_HOST_MEMORY; + expectFailure([&] { SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 4); }, "Vulkan result"); + depthSampleResult = VK_SUCCESS; + const auto queries = depthSampleQueries; + Require(!SampleLocationsCompatibleDepth({}, VK_FORMAT_D32_SFLOAT, 4) && !SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 1), "sample location depth compatibility requires the extension and multisampling"); + Require(depthSampleQueries == queries, "ineligible sample location depth formats were queried"); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x622ae6ae; + state = DecodeState(queue); + Require(!state.customSampleLocations, "standard sample locations require the extension"); + const auto standard = AgcDriver::Graphics::SampleLocations(state); + const std::array expectedStandard{{{0.375f, 0.125f}, {0.875f, 0.375f}, {0.125f, 0.625f}, {0.625f, 0.875f}}}; + for (std::size_t sample = 0; sample < expectedStandard.size(); ++sample) Require(standard[sample].x == expectedStandard[sample].x && standard[sample].y == expectedStandard[sample].y, "standard sample coordinate scale or Y direction changed"); + AgcDriver::Graphics::ValidateSampleLocations({}, state); + for (const auto samples : {2u, 8u}) { + queue = multisampled(); + const auto log2 = samples == 2 ? 1u : 3u; + queue.context[0x2f8] = (log2 << 20u) | log2; + queue.context[0x31d] = (log2 << 12u) | (log2 << 15u); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + queue.context[0x2fe + pixel * 4u] = 0x77887788; + queue.context[0x2ff + pixel * 4u] = 0x77887788; + } + state = DecodeState(queue); + Require(state.customSampleLocations && state.samples == samples, "a custom 2x or 8x pattern did not decode"); + const auto endpoints = AgcDriver::Graphics::SampleLocations(state); + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto expected = sample % 2u == 0 ? 0.0f : 15.0f / 16.0f; + Require(endpoints[sample].x == expected && endpoints[sample].y == expected, "signed sample coordinate endpoints changed"); + } + } + queue.context[0x2f8] = 0x00300003; + queue.context[0x31d] = 0x1b000; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + queue.context[0x2fe + pixel * 4u] = 0xbd153fd1u; + queue.context[0x2ff + pixel * 4u] = 0x9773f95bu; + } + state = DecodeState(queue); + Require(state.samples == 8 && !state.customSampleLocations && state.colors.size() == 1 && state.colors[0].samples == 8, "the standard 8x pattern (sample 7 at (7, -7), PPSA99902's registers) did not decode as standard"); + AgcDriver::Graphics::ValidateSampleLocations({}, state); queue = multisampled(); queue.context[0x31d] = 0x1000; expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples"); @@ -1765,28 +1982,28 @@ void multisampleTests() { expectFailure([&] { DecodeState(queue); }, "coverage conversion"); queue = multisampled(); queue.context[0x31c] |= 0x6000; - queue.context[0x31f] = 0x1234; - Require(DecodeState(queue).colors[0].cmaskAddress == 0x123400, "the CMASK address of a multisampled target did not decode"); + setCmaskAddress(queue); + Require(DecodeState(queue).colors[0].cmaskAddress == cmaskAddress, "the CMASK address of a multisampled target did not decode"); queue = makeState(); queue.context[0x31c] |= 0x4000; expectFailure([&] { DecodeState(queue); }, "color compression"); queue = multisampled(); queue.context[0x31c] |= 0x2000; - queue.context[0x31f] = 0x1234; + setCmaskAddress(queue); const auto fastCleared = DecodeState(queue).colors[0]; - Require(fastCleared.cmaskAddress == 0x123400 && fastCleared.cmaskFastClear && fastCleared.cmaskBytes == 0x1000, "a multisampled FAST_CLEAR target did not decode its CMASK at the metablock size"); + Require(fastCleared.cmaskAddress == cmaskAddress && fastCleared.cmaskFastClear && fastCleared.cmaskBytes == 0x1000, "a multisampled FAST_CLEAR target did not decode its CMASK at the metablock size"); queue = multisampled(); queue.context[0x31c] |= 0x4000; - queue.context[0x31f] = 0x1234; + setCmaskAddress(queue); const auto compressed = DecodeState(queue).colors[0]; - Require(compressed.cmaskAddress == 0x123400 && !compressed.cmaskFastClear && compressed.cmaskBytes == 0x1000, "a multisampled COMPRESSION target did not decode its CMASK at the metablock size"); + Require(compressed.cmaskAddress == cmaskAddress && !compressed.cmaskFastClear && compressed.cmaskBytes == 0x1000, "a multisampled COMPRESSION target did not decode its CMASK at the metablock size"); queue = multisampled(); queue.context[0x31c] |= 0x2000; queue.context[0x31f] = 0; expectFailure([&] { DecodeState(queue); }, "without a CMASK address"); queue = multisampled(); queue.context[0x31c] |= 0x2000; - queue.context[0x31f] = 0x1234; + setCmaskAddress(queue); std::vector reads; AgcDriver::Graphics::RegisterReadLog() = &reads; static_cast(DecodeState(queue)); @@ -1800,6 +2017,8 @@ void multisampleTests() { queue.context[0x32c] = 0; queue.context[0x3b1] = queue.context[0x3b0]; queue.context[0x3b9] = queue.context[0x3b8]; + setCmaskAddress(queue); + setCmaskAddress(queue, 1); auto pass = DecodeColorMetadataPass(queue); Require(pass.has_value() && pass->mode == ColorMetadataPass::Mode::Resolve && pass->source.has_value() && pass->source->samples == 2 && pass->targets.size() == 1 && pass->targets[0].samples == 1, "a CB resolve of a multisampled target did not decode"); queue.context[0x32b] = queue.context[0x31c] ^ (1u << 8u); @@ -1812,8 +2031,7 @@ void multisampleTests() { queue.context[0x3b1] = queue.context[0x3b0]; queue.context[0x3b9] = queue.context[0x3b8]; queue.context[0x32b] = queue.context[0x31c] | 0x2000u; - queue.context[0x32e] = static_cast(reinterpret_cast(cmaskMemory.data()) >> 8u); - queue.context[0x399] = static_cast(reinterpret_cast(cmaskMemory.data()) >> 40u); + setCmaskAddress(queue, 1); expectFailure([&] { DecodeColorMetadataPass(queue); }, "fast-clear color target"); queue = multisampled(); queue.context[0x2f8] = 0; @@ -3574,6 +3792,131 @@ void orderedPixelShaderTests() { expectFailure([&] { static_cast(ShaderRecompiler::Recompile(orderedPixelRequest(0x30600u, plain))); }, "fragmentShaderPixelInterlock"); } +constexpr std::array StencilRefExportCode{0x7e0e02f2u, 0xf8001882u, 0x700u, 0xbf810000u}; +constexpr std::array DepthStencilRefExportCode{0x7e0e02f2u, 0xf8001883u, 0x707u, 0xbf810000u}; + +AgcDriver::QueueState stencilRefExportQueue(std::uint32_t shaderControl, std::uint32_t zFormat) { + auto queue = makeState(); + queue.context[0x1b3] = 0x300u; + queue.context[0x1b4] = 0x300u; + queue.context[0x1c4] = zFormat; + queue.context[0x203] = shaderControl; + return queue; +} + +ShaderRecompiler::RecompileRequest stencilRefExportRequest(std::uint32_t shaderControl, std::span capabilities, std::span code = StencilRefExportCode) { + const auto queue = stencilRefExportQueue(shaderControl, 2u); + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x30000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports); + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 64; + request.target.supportedCapabilities = capabilities; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return request; +} + +std::string stencilRefShape(std::span words) { + std::string shape; + std::uint32_t variable = 0; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto op = static_cast(words[at] & 0xffffu); + if (op == spv::OpCapability && words[at + 1] == spv::CapabilityStencilExportEXT) shape += "capability "; + if (op == spv::OpExtension && std::string_view(reinterpret_cast(&words[at + 1])) == "SPV_EXT_shader_stencil_export") shape += "extension "; + if (op == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeEarlyFragmentTests) shape += "early "; + if (op == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeDepthReplacing) shape += "depth "; + if (op == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeStencilRefReplacingEXT) shape += "replacing "; + if (op == spv::OpDecorate && words[at + 2] == spv::DecorationBuiltIn && words[at + 3] == spv::BuiltInFragStencilRefEXT) { + variable = words[at + 1]; + shape += "builtin "; + } + if (op == spv::OpStore && variable != 0 && words[at + 1] == variable) shape += "store "; + } + return shape; +} + +void stencilRefExportTests() { + for (const auto shaderControl : {0x52u, 0x12u, 0x02u, 0x03u}) { + for (const auto zFormat : {2u, 9u}) { + const auto queue = stencilRefExportQueue(shaderControl, zFormat); + static_cast(AgcDriver::Graphics::DecodeState(queue)); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with Z format " + std::to_string(zFormat)); + const auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports); + Require(pixel.stencilRefExportEnable, "STENCIL_TEST_VAL_EXPORT_ENABLE did not reach the pixel stage"); + Require(!pixel.earlyZ, "a pixel shader that exports the stencil reference was given early tests"); + } + for (const auto zFormat : {0u, 1u, 3u, 7u}) { + const auto queue = stencilRefExportQueue(shaderControl, zFormat); + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "depth export, shader coverage or ordered fragment execution"); + Require(!AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck accepted a stencil reference export with Z format " + std::to_string(zFormat)); + } + } + for (const auto shaderControl : {0x54u, 0x56u}) { + const auto queue = stencilRefExportQueue(shaderControl, 2u); + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "depth export, shader coverage or ordered fragment execution"); + Require(!AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck accepted STENCIL_OP_VAL_EXPORT_ENABLE in DB_SHADER_CONTROL " + std::to_string(shaderControl)); + } + Require(!AgcDriver::Graphics::DecodePixelStageInfo(stencilRefExportQueue(0x50u, 2u).context, IdentityExports).stencilRefExportEnable, "a pixel stage without STENCIL_TEST_VAL_EXPORT_ENABLE exports the stencil reference"); + const std::array stencilExport{spv::CapabilityShader, spv::CapabilityStencilExportEXT}; + const std::array plain{spv::CapabilityShader}; + const ShaderRecompiler::RequestSerializer serializer; + std::set> keys; + const std::array, 4> cases{{{0x52u, "capability extension replacing builtin store "}, {0x12u, "capability extension replacing builtin store "}, {0x50u, ""}, {0x10u, "early "}}}; + for (const auto& [shaderControl, expected] : cases) { + const auto request = stencilRefExportRequest(shaderControl, stencilExport); + const auto replayed = serializer.Deserialize(serializer.Serialize(request)); + Require(replayed.request.context.pixel->stencilRefExportEnable == ((shaderControl & 2u) != 0), "the serialized request lost STENCIL_TEST_VAL_EXPORT_ENABLE"); + const auto result = ShaderRecompiler::Recompile(request); + Require(stencilRefShape(result.spirv.Words()) == expected && stencilRefShape(ShaderRecompiler::Recompile(replayed.request).spirv.Words()) == expected, "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " declared the wrong stencil reference export: " + stencilRefShape(result.spirv.Words())); + std::vector key; + ShaderRecompiler::RecompileCacheKey::Build(request, key); + keys.insert(key); + AgcDriver::Graphics::State state{}; + state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = makeModule({}); + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &result, 0}}}; + AgcDriver::Graphics::ValidateShaders(shaders, state, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false); + } + Require(keys.size() == cases.size(), "the recompile cache key ignores STENCIL_TEST_VAL_EXPORT_ENABLE"); + const auto both = stencilRefShape(ShaderRecompiler::Recompile(stencilRefExportRequest(0x53u, stencilExport, DepthStencilRefExportCode)).spirv.Words()); + Require(both == "capability extension depth replacing builtin store ", "a depth and stencil reference export declared the wrong outputs: " + both); + const auto depthOnly = stencilRefShape(ShaderRecompiler::Recompile(stencilRefExportRequest(0x51u, stencilExport, DepthStencilRefExportCode)).spirv.Words()); + Require(depthOnly == "depth ", "an MRTZ stencil channel without STENCIL_TEST_VAL_EXPORT_ENABLE declared the wrong outputs: " + depthOnly); + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(stencilRefExportRequest(0x52u, plain))); }, "VK_EXT_shader_stencil_export"); + auto queue = stencilRefExportQueue(0x52u, 2u); + queue.context[0x000] = 0; + queue.context[0x002] = 0; + queue.context[0x007] = (1u << 16u) | 3u; + queue.context[0x00a] = 7; + queue.context[0x00b] = 0; + queue.context[0x010] = 0x22900983; + queue.context[0x011] = 0x20000181; + for (const auto offset : {0x012u, 0x014u}) queue.context[offset] = 0x100; + for (const auto offset : {0x013u, 0x015u}) queue.context[offset] = 0x200; + queue.context[0x10b] = 0x00030030; + queue.context[0x10c] = 0x05ffff02; + queue.context[0x10d] = 0x05ffff02; + queue.context[0x200] = 0x00200211; + auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilTest && state.stencilFront.compareOp == VK_COMPARE_OP_EQUAL && state.stencilFront.passOp == VK_STENCIL_OP_REPLACE, "a stencil reference export changed the REPLACE_TEST decode"); + for (const auto ops : {0x00020020u, 0x00040040u, 0x00000200u, 0x00000004u}) { + queue.context[0x10b] = ops; + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "exports the stencil test value"); + } + queue.context[0x10b] = 0x00050050; + queue.context[0x10c] = 0x01ffff02; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilFront.passOp == VK_STENCIL_OP_INCREMENT_AND_CLAMP, "a stencil reference export changed the increment decode"); + queue.context[0x000] = 2; + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "stencil clear with a pixel shader that exports the stencil test value"); + queue.context[0x203] = 0x50u; + static_cast(AgcDriver::Graphics::DecodeState(queue)); +} + void ProvokingVertexTests() { using namespace AgcDriver::Graphics; constexpr std::uint32_t lastVertex = 1u << 19u; @@ -3622,6 +3965,14 @@ void ProvokingVertexTests() { Require(flatPixel.fragmentParameters.size() == 1 && flatPixel.fragmentParameters[0].flat && !flatPixel.fragmentParameters[0].perVertex, "flat v_interp_mov P0 has explicit per-vertex metadata"); const std::array flatShaders{{{ShaderRecompiler::ShaderStage::Fragment, &flatPixel, 0}}}; ValidateProvokingVertex(supported, last, flatShaders); + const std::array flatP10Color{0xc8120000u, 0xc8160100u, 0xc81a0200u, 0xc81e0300u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + const auto flatP10Pixel = recompilePixel({0x400u}, flatP10Color); + const auto p10Inputs = locatedInputs(flatP10Pixel.spirv.Words()); + Require(p10Inputs.size() == 1 && p10Inputs[0].location == 0 && p10Inputs[0].flat && !p10Inputs[0].perVertex, "flat v_interp_mov P10 did not compile to a flat input"); + Require(flatP10Pixel.fragmentParameters.size() == 1 && flatP10Pixel.fragmentParameters[0].flatP10 && !flatP10Pixel.fragmentParameters[0].perVertex && !flatPixel.fragmentParameters[0].flatP10, "flat v_interp_mov P10 was not recorded apart from P0"); + const std::array flatP10Shaders{{{ShaderRecompiler::ShaderStage::Fragment, &flatP10Pixel, 0}}}; + ValidateProvokingVertex(supported, last, flatP10Shaders); + expectFailure([&] { ValidateProvokingVertex(supported, first, flatP10Shaders); }, "v_interp_mov p10 of a flat input with the first provoking vertex is not measured"); ShaderRecompiler::RecompileResult fragment{}; const std::array shaders{{{ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN}) { @@ -4101,6 +4452,26 @@ void validationTests() { expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "metadata disagrees"); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 2, 1) == 80, "incorrect strided vertex range"); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 3, 1) == 112, "an index past the records still spans its fetch"); + { + auto wider = attribute; + wider.resource.fields[2] = 9; + auto instanced = attribute; + instanced.fetchIndex = 1; + instanced.resource.fields[2] = 1000; + auto constant = attribute; + constant.resource.fields[1] = 0; + const std::array attributes{attribute, wider, instanced, constant}; + Require(AgcDriver::Graphics::ZeroFetchVertex(attributes) == 9u, "the first vertex reading zeros is not past the largest per-vertex record count"); + auto clamped = attribute; + clamped.resource.fields[3] |= 2u << 28u; + const std::array selective{attribute, clamped}; + Require(!AgcDriver::Graphics::ZeroFetchVertex(selective).has_value(), "an attribute with out-of-bounds select 2 was taken to read zeros past its records"); + std::array indices{0xffffffffu, 2u, 0x12345u, 0xffffffffu}; + Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(indices)), 4, true, 9u) == 9u && indices == std::array{0xffffffffu, 2u, 9u, 0xffffffffu}, "clamping kept an index past the limit or moved a restart index"); + Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(indices)), 4, false, 9u) == 9u && indices == std::array{9u, 2u, 9u, 9u}, "clamping without restart kept a 0xffffffff index"); + std::array narrow{0xffffu, 1u}; + Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(narrow)), 2, false, 4u) == 4u && narrow[0] == 4u && narrow[1] == 1u, "clamping 16-bit indices failed"); + } attribute.fetchIndex = 1; Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 48, "instance attributes used the vertex index"); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2, 1) == 80, "first instance was ignored"); @@ -4274,7 +4645,7 @@ void validationTests() { { auto vertex = pushed({.push = true, .bufferArray = 2}); vertex.bindings.front().descriptorSet = 1; - expectFailure([&] { validate(vertex, 8); }, "descriptor set other than zero"); + expectFailure([&] { validate(vertex, 8); }, "descriptor set other than its stage's"); } { auto vertex = pushed({.push = true, .bufferArray = 2}); @@ -4591,11 +4962,13 @@ int main(int argc, char** argv) { DepthBoundsBiasTests(); conservativeZExportTests(); orderedPixelShaderTests(); + stencilRefExportTests(); ProvokingVertexTests(); ProvokingVertexRenderPassTests(); ConservativeRasterizationTests(); DisabledColorTests(); OpaqueDestinationAlphaTests(); + Color1555TargetTests(); CompactedExportTests(); ReversedComponentOrderTests(); ShadersWriteMemoryTests(); @@ -4605,6 +4978,7 @@ int main(int argc, char** argv) { uint16ExportTests(); uint8x4TargetTests(); multisampleTests(); + MsaaEnableSingleSampleTests(); ShaderStageTests(); NggVertexWorkgroupTests(); TuningFieldTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp index f4f02db6a..ad2314a04 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp @@ -1,5 +1,6 @@ #include "BdaTests.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestHeap.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" @@ -187,5 +188,36 @@ void RunUnmappedGapTests() { Require(!GuestMemory::Accessible(reinterpret_cast(std::uintptr_t{0x18}), 4), "a near-null address counts as accessible"); munmap(block, page); munmap(block + 2 * page, page); + if (!GuestArena::GuestArenaAvailable_nid_postfix()) return; + auto* guest = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(2 * page, page)); + Require(mmap(guest, 2 * page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == guest, "cannot map the arena page test pages"); + { + GuestAllocations::Mutation mutation; + mutation.Add(guest, 2 * page, true, true, true); + } + Require(GuestMemory::Accessible(guest, page, true), "a mapped arena page is not writable"); + Require(mprotect(guest, page, PROT_NONE) == 0, "cannot revoke the arena test page behind the registry"); + Require(GuestMemory::Accessible(guest, page, true), "a checked arena page was queried again instead of read from the page table"); + Require(mprotect(guest, page, PROT_READ | PROT_WRITE) == 0, "cannot restore the arena test page behind the registry"); + Require(mprotect(guest + page, page, PROT_READ) == 0, "cannot protect the arena test's second page"); + Require(!GuestMemory::Accessible(guest + page, page, true), "a check stored arena pages outside its range"); + { + GuestAllocations::Mutation mutation; + mutation.Protect(guest + page, page, true, true, true, [&] { Require(mprotect(guest + page, page, PROT_READ | PROT_WRITE) == 0, "cannot restore the arena test's second page"); }); + } + Require(GuestMemory::Accessible(guest, 2 * page, true), "mapped arena pages are not writable"); + { + GuestAllocations::Mutation mutation; + mutation.Protect(guest, page, true, false, true, [&] { Require(mprotect(guest, page, PROT_READ) == 0, "cannot protect the arena test page"); }); + } + Require(!GuestMemory::Accessible(guest, page, true) && GuestMemory::Accessible(guest, page) && GuestMemory::Accessible(guest + page, page, true), "an arena page protected through the registry kept its old access"); + { + GuestAllocations::Mutation mutation; + const auto release = [](const void* pointer, std::size_t bytes, const void*, bool) { Require(munmap(const_cast(pointer), bytes) == 0, "cannot unmap an arena test page"); }; + mutation.Unmap(guest, page, release); + mutation.Unmap(guest + page, page, release); + } + Require(!GuestMemory::Accessible(guest, page) && !GuestMemory::Accessible(guest + page, page), "unmapped arena pages are still accessible"); + GuestArena::GuestArenaRelease_nid_postfix(guest, 2 * page); #endif } diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index 0539db1b3..a605c6cd6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -160,7 +160,26 @@ void RunGuestTextureResourceTests() { Fields twoD = base; twoD.depth = 1; + rejectFields(twoD, "custom pitch other than its default pitch"); + twoD.tileModeRaw = 0x09; rejectFields(twoD, "nonzero depth or base array"); + Fields pitched = base; + pitched.width = 72; + pitched.height = 72; + pitched.depth = 127; + result = DecodeTextureResource(pack(pitched)); + Require(result.dimension == TextureDimension::k2D && result.width == 72 && result.depthOrLastArray == 0, "linear 2D descriptor with its default pitch in DEPTH decoded incorrectly"); + pitched.width = 8; + pitched.height = 8; + pitched.depth = 63; + result = DecodeTextureResource(pack(pitched)); + Require(result.width == 8 && result.depthOrLastArray == 0, "linear 2D descriptor with a one-unit default pitch decoded incorrectly"); + pitched.depth = 127; + rejectFields(pitched, "custom pitch other than its default pitch"); + pitched.depth = 63; + pitched.maxMip = 1; + pitched.lastLevel = 1; + rejectFields(pitched, "custom pitch other than its default pitch"); twoD = base; twoD.baseArray = 1; rejectFields(twoD, "nonzero depth or base array"); diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index 02a244738..175fdaf97 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -8,6 +8,7 @@ #include #include #include +#include using namespace ShaderRecompiler; @@ -209,7 +210,7 @@ int main() { passed &= run("cross-lane reads must keep the select", [] { bool ok = true; - for (const IrOpcode opcode : {IrOpcode::ReadFirstLane, IrOpcode::DppMoveU32, IrOpcode::BpermuteU32, IrOpcode::Permlane16U32, IrOpcode::SwizzleU32}) { + for (const IrOpcode opcode : {IrOpcode::DppMoveU32, IrOpcode::BpermuteU32, IrOpcode::Permlane16U32, IrOpcode::SwizzleU32}) { Builder b; auto& exec = b.mask(16u); auto& old = b.lane(); @@ -227,6 +228,132 @@ int main() { return ok && b.eliminate() == 0u && !removed(written); }); + passed &= run("a first-lane read guarded at both ends loses the source select", [] { + Builder b; + auto& exec = b.mask(16u); + auto& narrow = b.logicalAnd(exec, b.mask(8u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& read = b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&written, &narrow}); + b.keep(b.select(narrow, read, old)); + return b.eliminate() == 1u && removed(written); + }); + + passed &= run("an unguarded first-lane result keeps the source select for empty exec", [] { + Builder b; + auto& exec = b.emit(IrOpcode::INotEqual32, IrType::Bool, {&b.lane(), &b.constant(0u)}); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& empty = b.logicalAnd(exec, b.mask(0u)); + b.keep(b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&written, &empty})); + return b.eliminate() == 0u && !removed(written); + }); + + passed &= run("a first-lane result read under the write mask keeps the select when read exec is narrower", [] { + Builder b; + auto& exec = b.emit(IrOpcode::INotEqual32, IrType::Bool, {&b.lane(), &b.constant(0u)}); + auto& empty = b.logicalAnd(exec, b.mask(0u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& read = b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&written, &empty}); + b.keep(b.select(exec, read, old)); + return b.eliminate() == 0u && !removed(written); + }); + + passed &= run("a first-lane read under a wider exec keeps the select", [] { + Builder b; + auto& wide = b.mask(32u); + auto& exec = b.logicalAnd(wide, b.mask(16u)); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + auto& read = b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&written, &wide}); + b.keep(b.select(wide, read, old)); + return b.eliminate() == 0u && !removed(written); + }); + + const auto selectedExec = [](bool negate, bool widerArm) { + Builder b; + auto& wide = b.mask(32u); + auto& exec = b.logicalAnd(wide, b.mask(16u)); + auto& empty = b.logicalAnd(exec, b.mask(0u)); + auto& selector = negate ? b.emit(IrOpcode::LogicalNot, IrType::Bool, {&exec}) : exec; + auto& later = b.emit(IrOpcode::SelectU1, IrType::Bool, {&selector, negate ? (widerArm ? &wide : &empty) : &wide, negate ? &wide : (widerArm ? &wide : &empty)}); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + b.keep(b.select(later, b.add(written, 2u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("selected boolean exec implies the write mask through its selector", [&] { + for (bool negate : {false, true}) { + const auto [count, gone] = selectedExec(negate, false); + if (count != 1u || !gone) return false; + } + return true; + }); + + passed &= run("selected boolean exec with a wider unguarded arm keeps the select", [&] { + for (bool negate : {false, true}) { + const auto [count, gone] = selectedExec(negate, true); + if (count != 0u || gone) return false; + } + return true; + }); + + const auto rebuiltExec = [](bool mismatchedHigh) { + Builder b; + const auto [low, high] = b.ballotWords(b.mask(16u)); + const auto [wideLow, wideHigh] = b.ballotWords(b.mask(48u)); + auto& exec = b.threadBit(*low, *high); + auto& later = b.threadBit(*low, mismatchedHigh ? *wideHigh : *high); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + b.keep(b.select(later, b.add(written, 2u), old)); + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("an exec rebuilt from the same words implies the write mask", [&] { + const auto [count, gone] = rebuiltExec(false); + return count >= 1u && gone; + }); + + passed &= run("matching low mask words alone do not imply a wave64 write mask", [&] { + const auto [count, gone] = rebuiltExec(true); + return count == 0u && !gone; + }); + + const auto firstLaneInLoop = [](bool carriedResult) { + Builder b; + auto& exec = b.mask(16u); + auto& old = b.lane(); + auto& written = b.select(exec, b.add(old, 1u), old); + IrBlock& entry = *b.block; + IrBlock& loop = b.newBlock(); + entry.AddBranch(&loop); + loop.AddBranch(&loop); + b.block = &loop; + auto& loopExec = b.logicalAnd(exec, b.mask(8u)); + auto& read = b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&written, &loopExec}); + if (carriedResult) { + auto& carried = b.phi(IrType::U32, entry, old); + carried.AddPhiOperand(&loop, &read); + b.keep(b.select(loopExec, carried, old)); + } else { + b.keep(b.select(loopExec, read, old)); + } + return std::pair{b.eliminate(), removed(written)}; + }; + + passed &= run("a loop-local first-lane result guarded in its own iteration loses the source select", [&] { + const auto [count, gone] = firstLaneInLoop(false); + return count == 1u && gone; + }); + + passed &= run("a first-lane result carried to another loop iteration keeps the source select", [&] { + const auto [count, gone] = firstLaneInLoop(true); + return count == 0u && !gone; + }); + passed &= run("a branch condition derived from the write keeps the select", [] { Builder b; auto& exec = b.mask(16u); @@ -413,25 +540,35 @@ int main() { return count == 0u && !gone; }); - const auto chained = [](bool wideRoot) { + const auto chained = [](bool wideRoot, std::uint32_t kills) { Builder b; auto& wide = b.mask(48u); auto& exec = b.logicalAnd(wide, b.mask(32u)); auto& old = b.lane(); auto& written = b.select(exec, b.add(old, 1u), old); IrValue* later = wideRoot ? &wide : &exec; - for (std::uint32_t kill = 0; kill < 24u; ++kill) later = &b.logicalAnd(*later, b.mask(31u - kill)); + for (std::uint32_t kill = 0; kill < kills; ++kill) later = &b.logicalAnd(*later, b.mask(31u - kill % 24u)); b.keep(b.select(*later, b.add(written, 2u), old)); return std::pair{b.eliminate(), removed(written)}; }; passed &= run("an exec narrowed by a long chain of kills still implies the write's exec", [&] { - const auto [count, gone] = chained(false); + const auto [count, gone] = chained(false, 24u); return count == 1u && gone; }); passed &= run("a long chain of kills rooted at a wider exec keeps the select", [&] { - const auto [count, gone] = chained(true); + const auto [count, gone] = chained(true, 24u); + return count == 0u && !gone; + }); + + passed &= run("an exec narrowed by hundreds of kills still implies the write's exec", [&] { + const auto [count, gone] = chained(false, 300u); + return count == 1u && gone; + }); + + passed &= run("hundreds of kills rooted at a wider exec keep the select", [&] { + const auto [count, gone] = chained(true, 300u); return count == 0u && !gone; }); @@ -765,5 +902,42 @@ int main() { return b.eliminate() == 0u && !removed(written); }); + const auto shaded = [](std::uint32_t units) { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + MemoryInfo memory; + memory.kind = ResourceKind::Image; + memory.imageSampleFlags = RdnaImageSampleFlagLevelZero; + b.program.Resources().memoryInfo.push_back(memory); + MemoryFlags flags{0u, 0u}; + std::uint64_t bits = 0; + std::memcpy(&bits, &flags, sizeof(flags)); + auto& exec = b.mask(48u); + auto& image = b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}); + auto& sampler = b.emit(IrOpcode::GetSamplerResource, IrType::SamplerResource, {}); + IrValue* color = &b.lane(); + IrValue* address = nullptr; + std::uint32_t kept = 0; + std::vector compared; + for (std::uint32_t unit = 0; unit < units; ++unit) { + auto& sum = b.add(*color, unit); + auto& low = b.emit(IrOpcode::ULessThan32, IrType::Bool, {&sum, &b.constant(100u + unit)}); + compared.push_back(&b.select(low, sum, b.add(*color, 1u))); + address = &b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {compared.back(), color}); + auto& gathered = b.emit(IrOpcode::ImageGatherRaw, IrType::U32x4, {&image, &sampler, address}, bits); + color = &b.select(exec, b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&gathered, &b.constant(0u)}), *color); + } + b.emit(IrOpcode::ImageWrite, IrType::Void, {&image, address, &b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {color, color, color, color}), &exec}); + const auto stats = MaskedSelectEliminator{}.Eliminate(b.program); + for (const IrValue* select : compared) kept += removed(*select) ? 0u : 1u; + return std::pair{stats, kept}; + }; + + passed &= run("selects that each reach the rest of the program are settled in steps that grow with the program, not with its square", [&] { + const auto [small, smallKept] = shaded(500u); + const auto [large, largeKept] = shaded(4000u); + return small.removedSelects == 500u && smallKept == 500u && large.removedSelects == 4000u && largeKept == 4000u && small.steps != 0u && large.steps <= 9u * small.steps; + }); + return passed ? 0 : 1; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp index c92bd7fad..29891d598 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp @@ -2,6 +2,7 @@ #include "Recompiler.hpp" #include "execution/VulkanTestDevice.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include #include #include #include @@ -133,6 +134,26 @@ struct MeshDraw { VkCullModeFlags cull = VK_CULL_MODE_NONE; }; +bool ReadsNumWorkgroups(const std::vector& words) { + constexpr std::uint32_t OpDecorate = 71; + constexpr std::uint32_t OpAccessChain = 65; + constexpr std::uint32_t OpLoad = 61; + constexpr std::uint32_t DecorationBuiltIn = 11; + constexpr std::uint32_t BuiltInNumWorkgroups = 24; + std::uint32_t variable = 0; + std::vector pointers; + for (std::size_t at = 5; at < words.size();) { + const auto length = words[at] >> 16u; + const auto opcode = words[at] & 0xffffu; + if (length == 0 || at + length > words.size()) return false; + if (opcode == OpDecorate && length == 4 && words[at + 2] == DecorationBuiltIn && words[at + 3] == BuiltInNumWorkgroups) variable = words[at + 1]; + if (opcode == OpAccessChain && length >= 4 && variable != 0 && words[at + 3] == variable) pointers.push_back(words[at + 2]); + if (opcode == OpLoad && length >= 4 && variable != 0 && (words[at + 3] == variable || std::find(pointers.begin(), pointers.end(), words[at + 3]) != pointers.end())) return true; + at += length; + } + return false; +} + void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { const auto target = device.Target(); std::vector userData(12, 0u); @@ -149,6 +170,7 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { ShaderRecompiler::GraphicsCompileContext{0, {}, setup.mesh, std::nullopt, {setup.draw.indexAddress, setup.draw.indexCount, setup.draw.indexSize, setup.draw.instanceCount}} }; const auto meshResult = ShaderRecompiler::Recompile(geometry); + Require(ReadsNumWorkgroups(meshResult.spirv), "the mesh program does not read NumWorkgroups, so RADV can keep a previous vertex pipeline's user SGPR count for its dispatch"); const auto meshPush = static_cast(meshResult.pushConstants.size()); Require((meshPush == 0) == (setup.geometryPushBytes == 0), "the geometry program's push data does not follow its layout"); @@ -162,13 +184,13 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, {64, 0, {}, std::nullopt, pixel, std::nullopt, pixelMemory}, target, - {0, 0, meshPush, ShaderRecompiler::MeshDrawPushOffsetBytes - meshPush}, + target.fixedPushSlots ? PixelPushLayout(meshPush, target) : ShaderRecompiler::BindingLayout{0, 0, meshPush, ShaderRecompiler::MeshDrawPushOffsetBytes - meshPush}, std::nullopt }; const auto pixelResult = ShaderRecompiler::Recompile(fragment); const std::array shaders{{ {ShaderStage::Mesh, &meshResult, 0}, - {ShaderStage::Fragment, &pixelResult, meshPush} + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(meshPush, target)} }}; AgcDriver::Graphics::State state{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/PositionInvariance.cpp b/core/libs/prx/libSceAgcDriver/tests/PositionInvariance.cpp index 2c9b168bb..5b6d1b3be 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PositionInvariance.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PositionInvariance.cpp @@ -36,6 +36,8 @@ void requireInvariantPositions(const ShaderRecompiler::RecompileResult& result, constexpr std::array PositionExport{0xf80008cfu, 0u, 0xbf810000u}; +constexpr std::array VertexCapabilities{spv::CapabilityClipDistance}; + ShaderRecompiler::RecompileRequest baseRequest(ShaderRecompiler::ShaderStage stage) { using namespace ShaderRecompiler; RecompileRequest request{}; @@ -45,6 +47,7 @@ ShaderRecompiler::RecompileRequest baseRequest(ShaderRecompiler::ShaderStage sta request.target.vulkanVersion = 0x00401000u; request.target.spirvVersion = 0x00010300u; request.target.subgroupSize = 64; + request.target.supportedCapabilities = VertexCapabilities; request.layout.pushConstantSizeBytes = 128; request.useCache = false; return request; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 5484435e9..938b00102 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2081,6 +2081,45 @@ void drawInputInPlaceTests(const Device& device, Recorder& recorder) { Require(InPlaceDrawInput(unrecorded, vertices, 256, 4) == nullptr, "a draw without a recorder bound its input in place"); recorder.Sync(); Require(!recorder.PendingReadOverlaps(vertices, 256), "an in-place draw input's read outlived its batch"); + if (!AgcDriver::GuestMemory::Watched(address, bytes)) { + std::cout << "host imports are compared, not watched: kept highest indices not tested\n"; + return; + } + const auto indices = address + 20480; + constexpr std::size_t kept = Recorder::KeptIndexRangeIndices * 2; + std::vector first(Recorder::KeptIndexRangeIndices * 2, 1); + first[0] = 3; + first[1] = 9; + first[2] = 0xffff; + std::memcpy(reinterpret_cast(indices), first.data(), kept * 2); + auto range = InPlaceIndexRange(recorder, indices, kept, 2, false); + Require(!range.reused && range.highest == 0xffffu, "an in-place index range was not scanned at its first use"); + range = InPlaceIndexRange(recorder, indices, kept, 2, false); + Require(range.reused && range.highest == 0xffffu, "an unchanged in-place index range was scanned again"); + range = InPlaceIndexRange(recorder, indices, kept, 2, true); + Require(!range.reused && range.highest == 9u, "an in-place index range served a scan made under the other restart rule"); + Require(!InPlaceIndexRange(recorder, indices, kept + 2, 2, true).reused, "an in-place index range served a scan of another size"); + const std::uint16_t raised = 12; + std::memcpy(reinterpret_cast(indices + 2), &raised, sizeof(raised)); + range = InPlaceIndexRange(recorder, indices, kept, 2, true); + Require(!range.reused && range.highest == 12u, "an in-place index range kept its highest index over a guest write"); + range = InPlaceIndexRange(recorder, indices, kept, 2, true); + Require(range.reused && range.highest == 12u, "a rescanned in-place index range was not kept"); + range = InPlaceIndexRange(recorder, indices, kept - 2, 2, true); + Require(!range.reused && range.highest == 12u, "an in-place index range below the kept size was not scanned"); + range = InPlaceIndexRange(recorder, indices, kept - 2, 2, true); + Require(!range.reused && range.highest == 12u, "an in-place index range below the kept size was kept"); + range = InPlaceIndexRange(recorder, indices, kept * 2 - 4, 4, false); + Require(!range.reused && range.highest == 0x000c0003u, "a 32-bit in-place index range below the kept size was not scanned"); + Require(!InPlaceIndexRange(recorder, indices, kept * 2 - 4, 4, false).reused, "a 32-bit in-place index range below the kept size was kept"); + Require(!InPlaceIndexRange(recorder, indices, kept * 2, 4, false).reused, "a 32-bit in-place index range was not scanned at its first use"); + range = InPlaceIndexRange(recorder, indices, kept * 2, 4, false); + Require(range.reused && range.highest == 0x000c0003u, "an unchanged 32-bit in-place index range was scanned again"); + const std::vector restarts(Recorder::KeptIndexRangeIndices, 0xffff); + std::memcpy(reinterpret_cast(indices + 20480), restarts.data(), kept); + Require(!InPlaceIndexRange(recorder, indices + 20480, kept, 2, true).highest.has_value(), "an index range of restart indices has a highest index"); + range = InPlaceIndexRange(recorder, indices + 20480, kept, 2, true); + Require(range.reused && !range.highest.has_value(), "an empty in-place index range was scanned again"); } // Unit shadows (UnitShadow.hpp) over a host import of write-watched arena memory: a retile piece's @@ -2528,6 +2567,71 @@ void hostImportUnmapTests(const Device& device, Recorder& recorder) { std::cout << "host import unmap lifetime: ok\n"; } +void parkedImportUnmapTests(const Device& device) { + const auto& context = device.GetContext(); + if (context.hostImportAlignment == 0) { + std::cout << "host imports unavailable: parked import unmap not tested\n"; + return; + } + const auto alignment = static_cast(std::max(65536u, context.hostImportAlignment)); + const auto bytes = alignment * 4u; +#ifdef _WIN32 + void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* block = std::aligned_alloc(alignment, bytes); +#endif + Require(block != nullptr, "cannot allocate the parked import unmap block"); + const auto release = [&] { +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + }; + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + std::unique_ptr replacement; + bool imported = false; + { + std::lock_guard gpu(GpuMutex()); + imported = HostImportFor(context, address, bytes) != nullptr; + } + if (imported) { + replacement = std::make_unique(); + std::lock_guard gpu(GpuMutex()); + imported = HostImportFor(replacement->GetContext(), address, bytes) != nullptr; + } + if (!imported) { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + if (replacement) ClearHostImports(replacement->GetContext().device); + release(); + std::cout << "host import refused: parked import unmap not tested\n"; + return; + } + bool unmapped = false; + try { + GuestAllocations::Mutation mutation; + mutation.Unmap(block, bytes, [&](const void*, std::size_t, const void*, bool) { unmapped = true; }); + } catch (const std::exception& error) { + ClearHostImports(replacement->GetContext().device); + ClearHostImports(context.device); + GuestAllocations::Mutation mutation; + mutation.Remove(block); + release(); + throw std::runtime_error(std::string("guest unmap after a device replacement: ") + error.what()); + } + ClearHostImports(replacement->GetContext().device); + ClearImageMirrors(replacement->GetContext().device); + replacement.reset(); + release(); + Require(unmapped, "guest unmap of a range a replaced device imported did not run"); + std::cout << "parked import unmap: ok\n"; +} + void importWatchTests(const Device& device) { using namespace AgcDriver::GuestMemory; const auto& context = device.GetContext(); @@ -5292,6 +5396,37 @@ void pipelineLibraryTests(const Device& device) { state.samples = 1; state.colors.front().samples = 1; Require(lookup() != multisampled && counters().built == std::array{2, 2, 4, 4}, "the 1x pipeline after a 4x one built its libraries again"); + if ((context.sampleLocationSampleCounts & VK_SAMPLE_COUNT_4_BIT) != 0) { + state.samples = 4; + state.colors.front().samples = 4; + state.customSampleLocations = true; + state.sampleLocations = {0x10, 0x21, 0x32, 0x43}; + const auto pattern = lookup(); + const auto beforePatternChange = counters().built; + state.sampleLocations[0] = 0x20; + const auto otherPattern = lookup(); + Require(otherPattern != pattern && counters().built[2] == beforePatternChange[2] + 1u && counters().built[3] == beforePatternChange[3] + 1u && counters().built[0] == beforePatternChange[0] && counters().built[1] == beforePatternChange[1], "a custom sample pattern did not rebuild exactly the fragment shader and fragment output libraries"); + state.sampleLocations[0] = 0x10; + Require(lookup() == pattern && counters().built[2] == beforePatternChange[2] + 1u && counters().built[3] == beforePatternChange[3] + 1u, "a cached custom sample pattern was not reused"); + + std::optional compatibleDepth, incompatibleDepth; + state.customSampleLocations = false; + for (const auto format : {VK_FORMAT_D32_SFLOAT, VK_FORMAT_D16_UNORM, VK_FORMAT_D24_UNORM_S8_UINT, VK_FORMAT_D32_SFLOAT_S8_UINT}) { + state.depth = DepthTarget{}; + state.depth->format = format; + state.depth->samples = state.samples; + (ValidateSampleLocations(context, state) ? compatibleDepth : incompatibleDepth) = format; + } + if (compatibleDepth && incompatibleDepth) { + state.depth->format = *compatibleDepth; + const auto compatible = lookup(); + const auto beforeDepthChange = counters().built; + state.depth->format = *incompatibleDepth; + const auto incompatible = lookup(); + Require(incompatible != compatible && counters().built[2] == beforeDepthChange[2] + 1u && counters().built[3] == beforeDepthChange[3] + 1u, "depth-format sample-location enablement did not rebuild both fragment libraries"); + } + state.customSampleLocations = true; + } WaitForOptimizedPipelines(context.device); const auto lto = std::getenv("APS5_NO_PIPELINE_LTO") == nullptr && context.pipelineLinkTimeOptimization; Require(counters().optimized == (lto ? counters().linked : 0u), "a linked pipeline was not optimized in the background"); @@ -5361,6 +5496,11 @@ int main(int argc, char** argv) { hostImportUnmapTests(device, recorder); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--parked-import-unmap-only") { + gpu.unlock(); + parkedImportUnmapTests(device); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--unit-shadow-only") { if (!AgcDriver::Graphics::UnitShadowEnabled() || device.GetContext().hostImportAlignment == 0) { std::cout << "skipped, unit shadows or host imports are unavailable\n"; @@ -5374,6 +5514,11 @@ int main(int argc, char** argv) { std::cout << "CMASK copied clear, write-back and failure tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--draw-inputs-in-place-only") { + drawInputInPlaceTests(device, recorder); + std::cout << "In-place draw input tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--multisample-only") { multisampleGuestAccessTests(device, recorder); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index b0822a499..063d4a499 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -71,6 +71,7 @@ void requireSameArtifact(const CompiledShaderArtifact& left, const CompiledShade require(left.hostSubgroupSize == right.hostSubgroupSize, prefix + "host subgroup size differs"); require(left.vertexInputs == right.vertexInputs, prefix + "vertex inputs differ"); require(left.vertexInputPatches == right.vertexInputPatches, prefix + "vertex type patches differ"); + require(left.registrationModule == right.registrationModule, prefix + "registration module flags differ"); require(left.vertexOffsetSgpr == right.vertexOffsetSgpr && left.instanceOffsetSgpr == right.instanceOffsetSgpr, prefix + "offset SGPRs differ"); require(left.vertexOffsetShared == right.vertexOffsetShared && left.instanceOffsetShared == right.instanceOffsetShared && left.vertexOffsetConflict == right.vertexOffsetConflict && left.instanceOffsetConflict == right.instanceOffsetConflict, prefix + "offset flags differ"); require(left.parameterExports == right.parameterExports, prefix + "parameter exports differ"); @@ -153,6 +154,7 @@ RecompileResult sampleResult() { result.workgroupMemoryDwords = 32769u; result.vertexInputs = {{1, 4, 2}, {5, 2, 0}}; result.vertexInputPatches = {{1, 20, {7, 8, 9}}, {5, 40, {10, 11, 12}}}; + result.registrationModule = true; result.specialization = {{512, 4}, {516, 0x3f800000u}, {517, 0}}; result.vertexAttributes = {{1, 4, {{0x1000u, 0x20000u, 0x30u, 0x4u}}, 2}, {5, 2, {{9u, 8u, 7u, 6u}}, 0}}; result.vertexInputs[0].outputMask = 5u; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 2a86162fa..f650a2b59 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -50,7 +50,7 @@ namespace { ShaderRecompiler::SpirvTarget BufferTarget() { - static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityImageGatherExtended, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityImageGatherExtended, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance}; static constexpr std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; ShaderRecompiler::SpirvTarget target{}; target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; @@ -1202,7 +1202,7 @@ void verifyLanesOutsideHostSubgroup() { void verifyHalfWaveReduction() { using namespace ShaderRecompiler; - static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic}; + static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic, spv::CapabilityClipDistance}; const auto pixel = [](std::span code, std::uint32_t subgroupSize, bool arithmetic) { ShaderPixelStageInfo info{}; info.inputAddr = PixelInputBit(PixelInput::PositionX); @@ -1341,6 +1341,13 @@ void verifyHalfWaveReduction() { divergent.insert(divergent.begin() + 3, 0x88fe1a7eu); divergent.insert(divergent.begin() + 2, {0xbe860480u, 0x7d821688u, 0xbe9a246au, 0xbf880001u}); require(reductions(pixel(divergent, 32u, true), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: keys masked inside a divergent branch did not read lane 31 as a subgroup UMin"); + auto windowed = umin; + windowed.insert(windowed.begin() + 23, 0x7e008514u); + require(reductions(pixel(windowed, 32u, true), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: a UMin scan of registers in a v_movreld_b32 window did not read lane 31 as a subgroup UMin on 32 lanes"); + require(halves(pixel(windowed, 64u, true), spv::OpGroupNonUniformUMin, 0xffffffffu), "half-wave reduction: a UMin scan of registers in a v_movreld_b32 window did not read lanes 31 and 63 as reductions of host invocations 0-31 and 32-63 on 64 lanes"); + auto overwritten = umin; + overwritten.insert(overwritten.begin() + 17, {0x7ef80500u, 0x7e008514u}); + expectFailure([&] { static_cast(pixel(overwritten, 32u, true)); }, lane63, "half-wave reduction: a scan of window registers read lane 63 after a v_movreld_b32 at a runtime M0 that may overwrite them"); const char* arithmetic = "v_readlane_b32 of lane 31 of a wave64 half-wave reduction scan needs subgroup arithmetic"; expectFailure([&] { static_cast(pixel(umin, 32u, false)); }, arithmetic, "half-wave reduction: a device without subgroup arithmetic read lane 31 on 32 lanes"); expectFailure([&] { static_cast(pixel(umin, 64u, false)); }, arithmetic, "half-wave reduction: a device without subgroup arithmetic read lane 31 on 64 lanes"); @@ -1364,7 +1371,7 @@ void verifyHalfWaveReduction() { void verifyNarrowVertexWave() { using namespace ShaderRecompiler; - static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic}; + static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic, spv::CapabilityClipDistance}; static constexpr std::array userData{0u, 0u, 0u, 0x2020u}; const auto vertex = [](std::span code, std::uint32_t waveSize, std::uint32_t subgroupSize, std::uint32_t userDataBase) { RecompileRequest request{}; @@ -1411,7 +1418,7 @@ void verifyNarrowVertexWave() { const auto wide64 = prepared(culling, 64u); require(!wide64.empty() && reductions(wide64) == 0u, "narrow vertex wave: the NGG culling scan, keyed on lanes below 64 on 64 lanes, did not keep its DPP scan"); require(prepares(patched({{6, 0x80060015u}}), 32u), "narrow vertex wave: the scan keyed on lanes below the vertex count in s3 did not prepare on 32 lanes"); - static constexpr std::array noArithmetic{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle}; + static constexpr std::array noArithmetic{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityClipDistance}; const auto otherKeys = patched({{7, 0x360e0a82u}}); auto withoutArithmetic = vertex(otherKeys, 64u, 32u, 8u); withoutArithmetic.target.supportedCapabilities = noArithmetic; @@ -1542,7 +1549,7 @@ void verifyPixelRequestSerialization() { value.posZ, value.posW, value.frontFace, value.ancillary, value.sampleShading, value.noPerspective, value.linearCentroid, value.pixelKillEnable, value.depthExportEnable, value.sampleMaskExportEnable, value.earlyZ, value.executeOnNoop, value.conservativeZExport, value.orderedPixelShader, - value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage); + value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage, value.stencilRefExportEnable); }; const std::array code{0xbf810000u}; const std::array userData{0x12345678u, 0xabcdef01u, 0x87654321u}; @@ -1586,6 +1593,7 @@ void verifyPixelRequestSerialization() { }; for (std::uint32_t i = 0; i < pixel.interpolatorSettings.size(); ++i) pixel.interpolatorSettings[i] = 0x10101010u + i; pixel.alphaToCoverage = true; + pixel.stencilRefExportEnable = true; const std::array, 3> mappings{{ {0x00u, 0xe4u, 0xc6u, 0x1bu, 0xffu, 0x80u, 0x55u, 0xaau}, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}, @@ -1614,6 +1622,10 @@ void verifyPixelRequestSerialization() { withoutCoverage.context.pixel->alphaToCoverage = false; RecompileCacheKey::Build(withoutCoverage, replayKey); require(key != replayKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(withoutCoverage), "alpha-to-coverage did not change the static shader identity"); + auto withoutStencilRef = request; + withoutStencilRef.context.pixel->stencilRefExportEnable = false; + RecompileCacheKey::Build(withoutStencilRef, replayKey); + require(key != replayKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(withoutStencilRef), "the stencil reference export did not change the static shader identity"); } request.context.pixel.reset(); request.shader.stage = ShaderStage::Vertex; @@ -1625,12 +1637,12 @@ void verifyPixelRequestSerialization() { minimal.context.waveSize = 64; minimal.context.pixel = ShaderPixelStageInfo{}; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQRAAAAA=", "new requests did not use serialization version 16"); + require(requestPrefix(encoded, 8u) == "NVNQQREAAAA=", "new requests did not use serialization version 17"); constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u; - for (std::size_t bytes = 0; bytes < 18u; ++bytes) { - expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source or alpha-to-coverage flag was accepted"); + for (std::size_t bytes = 0; bytes < 19u; ++bytes) { + expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage or stencil reference export flag was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQREAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRIAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } } @@ -1720,12 +1732,13 @@ void verifyLegacyPixelRequests() { require(pixel.targetExportPacking == std::array{}, "a legacy request packed a color export"); require(!pixel.dualSourceBlend, "a legacy request enabled dual-source blending"); require(!pixel.alphaToCoverage, "a legacy request enabled alpha-to-coverage"); + require(!pixel.stencilRefExportEnable, "a legacy request exported a stencil reference"); require(pixel.inputAddr == 2u && pixel.hasPerspectiveCenterVgpr && pixel.targetOutputMode[0] == 9u, "legacy pixel layout was misread"); require(pixel.conservativeZExport == (version >= 7u ? ConservativeZExport::GreaterThanZ : ConservativeZExport::AnyZ), "legacy conservative Z layout was misread"); require(!pixel.orderedPixelShader, "a legacy request became a primitive-ordered pixel shader"); require(request.shader.stage == ShaderStage::Fragment && request.shader.code.size() == 1u && request.shader.code[0] == 0xbf810000u && !request.context.vertex.has_value() && request.context.memory.empty(), "legacy guest context was misaligned"); require(request.target.vulkanVersion == 0x00401000u && request.target.spirvVersion == 0x00010300u && request.target.subgroupSize == 64u && request.layout.firstBinding == 11u && request.layout.pushConstantSizeBytes == 128u, "legacy target or binding layout was misaligned"); - require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u && !request.target.narrowSubgroupClock && !request.target.float64DenormPreserve, "legacy request trailer was misread"); + require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u && !request.target.narrowSubgroupClock && !request.target.float64DenormPreserve && !request.target.registrationModules, "legacy request trailer was misread"); const auto upgraded = serializer.Deserialize(serializer.Serialize(request)); require(upgraded.request.context.pixel->targetExportMapping == pixel.targetExportMapping && RecompileCacheKey::ContextHash(upgraded.request) == RecompileCacheKey::ContextHash(request), "upgrading a legacy capture changed its pixel mapping"); } @@ -3218,6 +3231,16 @@ int main(int argc, char** argv) { auto float64Denormals = uncached; float64Denormals.target.float64DenormPreserve = true; require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(float64Denormals)).request.target.float64DenormPreserve, "f64 denormal preservation was lost in serialization"); + auto registrationModules = uncached; + registrationModules.target.registrationModules = true; + registrationModules.target.registrationWordLimit = 1234u; + const auto roundTrip = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(registrationModules)).request.target; + require(roundTrip.registrationModules && roundTrip.registrationWordLimit == 1234u, "registration modules were lost in serialization"); + std::vector plainKey; + std::vector registrationKey; + RecompileCacheKey::Build(uncached, plainKey); + RecompileCacheKey::Build(registrationModules, registrationKey); + require(plainKey != registrationKey, "registration modules did not change the cache key"); auto changedLayout = request; changedLayout.layout.pushConstantSizeBytes = 64; require(!Recompile(changedLayout).cacheHit, "binding layout change reused an incompatible variant"); diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp index daa5ac849..ae99defb9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp @@ -50,8 +50,8 @@ void CheckStorageOnlyModule(const RecompileResult& result, std::string_view name } SpirvTarget StorageOnlyTarget() { - static constexpr std::array capabilities{ - spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, + static constexpr std::array capabilities{ + spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance, }; static constexpr std::array extensions{ "SPV_EXT_descriptor_indexing", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", "SPV_KHR_physical_storage_buffer", diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index da1c9ba0b..ec50e84c9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #ifdef _WIN32 @@ -111,6 +112,17 @@ void testValidation() { AgcDriverWaitIdle_nid_postfix(); } +void testReadOnlyResummarize() { + std::vector commands; + for (const auto [offset, value] : {std::pair{0x000u, 0x10u}, {0x200u, 0x007007f3u}, {0x010u, 0xa0000183u}, {0x011u, 0x20000180u}, {0x8eu, 0xffffffffu}, {0x8fu, 0u}, {0x1c4u, 0u}, {0x203u, 0u}}) { + commands.insert(commands.end(), {0xc0016900u, offset, value}); + } + commands.insert(commands.end(), {0xc0012d00u, 3u, 2u}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&packet) == 0, "read-only resummarize submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + void testClearState() { AgcDriver::QueueState graphics{{{0x20c, 1}}, {{0x10, 17}, {0x11, 23}}, {{0x242, 5}}}; const auto shader = graphics.shader; @@ -671,8 +683,14 @@ void testWorkerFailure() { } -int main() { +int main(int argc, char** argv) { try { + if (argc == 2 && std::string_view(argv[1]) == "--readonly-resummarize") { + testReadOnlyResummarize(); + LibcRunShutdown_nid_postfix(); + std::puts("Read-only resummarize submission passed"); + return 0; + } const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; alignas(256) std::array rawCode{}; @@ -766,6 +784,7 @@ int main() { testHeaderWithoutProgramAddress(); testCopyIntoHostMemory(); testRegisteredFloatMode(); + testReadOnlyResummarize(); testWorkerFailure(); check(expectFailure([] { LibcRunShutdown_nid_postfix(); }).find("required shader register") != std::string::npos, "shutdown lost worker failure"); std::puts("AGC driver submit tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp index 1b9c4784e..fe008dfb1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureDetiler.cpp @@ -1,4 +1,5 @@ #include "GraphicsTests.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include #include @@ -171,6 +172,18 @@ void RunTextureDetilerTests(const Context& context, const TextureDetilerTestAcce Require(decodePush(access.lastDispatch().pushConstants).pipeBankXor == 0xa00u, "the window's pipe/bank XOR did not reach the detiling shader"); Require(equationSpecialization[0] == 4 && equationSpecialization[1] == 4096 && equationSpecialization[2] == 2, "SW_4KB_D_X detiling must select the equation family over 4 KiB blocks"); + { + const auto mip = ComputeColorTargetMipLayout(ColorTileMode::RenderTarget, 4, 1024, 1024, 11, 4); + Require(mip.width == 64 && mip.height == 64 && mip.tail, "the first packed render-target mip has the wrong extent"); + Require(mip.tiledOffset == 0 && mip.tiledSize == 65536 && mip.tailX == 64 && mip.tailY == 0, "the first packed render-target mip lost its shared-block origin"); + Require(mip.pitchBytes == 256 && mip.linearSize == 16384, "packed render-target mips must use tightly packed transfer rows"); + const auto next = ComputeColorTargetMipLayout(ColorTileMode::RenderTarget, 4, 1024, 1024, 11, 5); + Require(next.width == 32 && next.height == 32 && next.tail && next.tiledOffset == 0 && next.tiledSize == 65536 && next.tailX == 0 && next.tailY == 64, "the next packed render-target mip lost its shared-block origin"); + const auto regular = ComputeColorTargetMipLayout(ColorTileMode::RenderTarget, 4, 1024, 1024, 11, 3); + Require(!regular.tail && regular.width == 128 && regular.height == 128 && regular.tiledOffset == 0 && regular.tiledSize == 65536, "a regular render-target mip must be relative to its own target buffer"); + reject([] { ComputeColorTargetMipLayout(ColorTileMode::RenderTarget, 4, 1024, 1024, 11, 11); }, "outside its surface"); + } + reject([&] { detiler.Dispatch(VK_NULL_HANDLE, TextureTileMode::kStandard4KB, 4, source, 0, destination, 0, layout, 0); }, "active command buffer"); reject([&] { detiler.Dispatch(commands, TextureTileMode::kStandard4KB, 4, VK_NULL_HANDLE, 0, destination, 0, layout, 0); }, "source and destination buffers"); reject([&] { detiler.Dispatch(commands, TextureTileMode::kStandard4KB, 4, source, 0, VK_NULL_HANDLE, 0, layout, 0); }, "source and destination buffers"); diff --git a/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp index 3e852184b..7f622c956 100644 --- a/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp @@ -1,9 +1,13 @@ #include "Recompiler.hpp" +#include "ControlFlow/GraphBuilder.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include "Translation/InstructionTranslator.hpp" #include #include #include #include #include +#include #include #include #include @@ -15,11 +19,12 @@ namespace { using namespace ShaderRecompiler; constexpr std::uint32_t MoveM0Two = 0xbefc0382u; +constexpr std::uint32_t MoveM0GroupX = 0xbefc0304u; constexpr std::uint32_t MoveV4One = 0x7e0802f2u; constexpr std::uint32_t MoveV5Two = 0x7e0a02f4u; constexpr std::uint32_t MoveV6Four = 0x7e0c02f6u; constexpr std::uint32_t MovrelsV1V4 = 0x7e028704u; -constexpr std::uint32_t MovreldV4V1 = 0x7e088501u; +constexpr std::uint32_t MovreldV4V0 = 0x7e088500u; constexpr std::uint32_t MoveV1V5 = 0x7e020305u; constexpr std::uint32_t EndProgram = 0xbf810000u; @@ -36,14 +41,14 @@ void Require(bool condition, const std::string& message) { std::vector RecompileProgram(std::span code) { const std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformVote, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses}; - const std::array userData{}; + const std::array userData{0x10000000u, 0u, 0x40u, 0x31016facu}; const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; RecompileRequest request{}; request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; request.context.waveSize = 32; request.context.userDataBaseRegister = 0; request.context.userData = userData; - request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 4u, {false, false, false}, false, 1u}; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 4u, {true, false, false}, false, 1u}; request.target.vulkanVersion = 0x00403000u; request.target.spirvVersion = 0x00010600u; request.target.subgroupSize = 32; @@ -65,6 +70,7 @@ Shape Scan(std::span words) { std::vector> constants; std::vector> arrays; std::vector> functionPointers; + std::vector windowVariables; for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> spv::WordCountShift; const auto op = static_cast(words[cursor] & spv::OpCodeMask); @@ -78,19 +84,162 @@ Shape Scan(std::span words) { for (const auto& [array, length] : arrays) { if (array != pointee) continue; ++shape.functionArrays; + windowVariables.push_back(words[cursor + 2]); for (const auto& [constant, value] : constants) { if (constant == length) shape.arrayLength = value; } } } } - if (op == spv::OpAccessChain) ++shape.accessChains; + if (op == spv::OpAccessChain) { + for (const auto variable : windowVariables) { + if (words[cursor + 3] == variable) ++shape.accessChains; + } + } if (op == spv::OpSelect) ++shape.selects; cursor += count; } return shape; } +IrProgram TranslateStage(ShaderStageKind stage, std::uint32_t waveSize, std::uint32_t base = 0u, std::uint32_t fetchStart = 0u, std::uint32_t fetchCount = 0u) { + ShaderComputeInputInfo compute{}; + compute.threadIdsNum = 3; + compute.threadsNum[0] = waveSize; + ShaderVertexInputInfo vertex{}; + vertex.mesh.threadsNum[0] = waveSize; + vertex.mesh.inputPrimitive = 1u; + vertex.mesh.primitivesPerGroup = 1u; + vertex.mesh.verticesPerGroup = 1u; + vertex.mesh.esgsItemSize = 1u; + ShaderPixelInputInfo pixel{}; + std::uint32_t next = 0u; + for (const auto input : {PixelInput::PerspectiveCenter, PixelInput::PerspectiveCentroid, PixelInput::LinearCenter, PixelInput::LinearCentroid, + PixelInput::PositionX, PixelInput::PositionY, PixelInput::PositionZ, PixelInput::PositionW, + PixelInput::FrontFace, PixelInput::Ancillary, PixelInput::LineStipple, PixelInput::PositionFixedPoint}) { + pixel.psInputVgpr[static_cast(input)] = next; + next += PixelInputVgprCount(input); + } + std::vector code{MoveM0Two}; + if (fetchCount != 0u) { + code.push_back(0xbe802102u); + code.push_back(0x7e008700u | base); + code.push_back(0x7e1002f2u); + vertex.resourcesNum = 1; + vertex.resourcesDst[0] = {static_cast(fetchStart), static_cast(fetchCount), 0, 0u}; + } + code.push_back(0x7e008700u | base); + code.push_back(EndProgram); + const auto decoded = RdnaInstructionDecoder{}.Decode(code); + if (fetchCount != 0u) { + const auto& call = decoded.instructions.at(1); + Require(call.op == RdnaOpcode::SSwappcB64 && call.family == RdnaInstructionFamily::SOP1 && call.opcodeId == 0x21u && + call.destination.kind == RdnaOperandKind::ScalarRegister && call.destination.reg == 0u && + call.source0.kind == RdnaOperandKind::ScalarRegister && call.source0.reg == 2u, + "external fetch fixture did not decode s_swappc_b64 s[0:1], s[2:3]"); + } + const SwappcInfo swappc{fetchCount != 0u, 0u, 8u}; + const auto graph = GraphBuilder{}.Build(decoded, &swappc); + Require(graph.hasFetchCall == (fetchCount != 0u), "stage fixture did not identify its external fetch call"); + TranslateOptions options{}; + options.stage = stage; + options.waveSize = waveSize; + options.userDataCount = 8u; + options.inputInfo = {&vertex, &pixel, &compute}; + return InstructionTranslator{}.Translate(decoded, graph, options); +} + +void RequireWindow(const IrProgram& program, std::uint32_t base, std::uint32_t limit, std::span initialized) { + Require(program.Metadata().vectorWindowBase == base && program.Metadata().vectorWindowSize == limit - base, "relative window does not include exactly the initialized stage registers"); + std::array stored{}; + for (const auto* instruction : program.BlockOrder().front()->Instructions()) { + if (instruction->Opcode() == IrOpcode::StoreVectorWindow) { + Require(instruction->Argument(0)->HasImmediate(), "stage input uses a dynamic window index"); + const auto index = base + instruction->Argument(0)->ImmediateU32(); + Require(index < limit, "stage input lies past the window"); + stored[index] = true; + } else if (instruction->Opcode() == IrOpcode::SetVectorRegister) { + const auto index = instruction->Argument(0)->Register().index; + Require(index < base, "stage input escaped the relative window into SSA"); + stored[index] = true; + } + } + for (const auto index : initialized) Require(stored[index], "stage input v" + std::to_string(index) + " was not initialized"); + bool bounded = false; + for (const auto& block : program.Blocks()) { + for (const auto* instruction : block->Instructions()) { + if (instruction->Opcode() == IrOpcode::ULessThan32 && instruction->Argument(1)->HasImmediate() && instruction->Argument(1)->ImmediateU32() == limit - base) bounded = true; + } + } + Require(bounded, "relative access did not use the final stage window bound"); +} + +template +bool RunStage(const char* name, TAction action) { + try { + action(); + std::printf("vector window stage inputs: %s passed\n", name); + return true; + } catch (const std::exception& error) { + std::fprintf(stderr, "vector window stage inputs: %s: %s\n", name, error.what()); + return false; + } +} + +bool StageInputs() { + bool passed = true; + const auto stage = [&](const char* name, ShaderStageKind kind, std::uint32_t limit, std::initializer_list initialized) { + passed &= RunStage(name, [&] { + for (const auto wave : {32u, 64u}) RequireWindow(TranslateStage(kind, wave), 0u, limit, {initialized.begin(), initialized.size()}); + }); + }; + stage("compute", ShaderStageKind::Compute, 3u, {0u, 1u, 2u}); + stage("vertex", ShaderStageKind::Vertex, 9u, {5u, 8u}); + stage("local", ShaderStageKind::Local, 6u, {2u, 3u, 5u}); + stage("tessellation control", ShaderStageKind::TessellationControl, 2u, {0u, 1u}); + stage("tessellation evaluation", ShaderStageKind::TessellationEvaluation, 9u, {0u, 5u, 6u, 7u, 8u}); + stage("mesh", ShaderStageKind::Mesh, 9u, {0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u}); + stage("pixel", ShaderStageKind::Pixel, 16u, {0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u, 11u, 12u, 13u, 14u, 15u}); + passed &= RunStage("SSA prefix", [] { + const std::array local{2u, 3u, 5u}; + const std::array vertex{5u, 8u}; + for (const auto wave : {32u, 64u}) { + RequireWindow(TranslateStage(ShaderStageKind::Local, wave, 4u), 4u, 6u, local); + RequireWindow(TranslateStage(ShaderStageKind::Vertex, wave, 16u), 16u, 17u, vertex); + } + }); + passed &= RunStage("external fetch", [] { + const std::array vertex{5u, 8u}; + for (const auto wave : {32u, 64u}) { + for (const auto start : {20u, 252u}) { + for (const auto count : {1u, 2u, 3u, 4u}) { + const auto program = TranslateStage(ShaderStageKind::Vertex, wave, 0u, start, count); + RequireWindow(program, 0u, start + count, vertex); + std::uint32_t fetched = 0u; + std::uint32_t boundedReads = 0u; + for (const auto& block : program.Blocks()) { + for (const auto* instruction : block->Instructions()) { + if (instruction->Opcode() == IrOpcode::StoreVectorWindow) { + const auto* value = instruction->Argument(1); + if (value->Opcode() == IrOpcode::SelectU32) value = value->Argument(1); + if (value->Opcode() == IrOpcode::GetAttribute) { + Require(instruction->Argument(0)->ImmediateU32() == start + fetched && value->Argument(1)->ImmediateU32() == fetched, + "external fetch did not initialize its entire window span"); + ++fetched; + } + } + if (instruction->Opcode() == IrOpcode::ULessThan32 && instruction->Argument(1)->HasImmediate() && instruction->Argument(1)->ImmediateU32() == start + count) ++boundedReads; + } + } + Require(fetched == count && boundedReads == 2u, "relative accesses to the external fetch disagree about the window"); + } + } + } + }); + if (passed) std::puts("Vector window stage input and fetch tests passed"); + return passed; +} + bool run(const char* name, bool (*test)()) { try { if (test()) return true; @@ -103,21 +252,25 @@ bool run(const char* name, bool (*test)()) { } -int main() { +int main(int argc, char** argv) { + if (argc == 2 && std::string_view(argv[1]) == "--stage-inputs-only") return StageInputs() ? 0 : 1; + if (argc != 1) return 2; bool passed = true; - passed &= run("a relative read and write index a function-scope window instead of selecting over the registers", [] { - const std::array code{MoveM0Two, MoveV4One, MoveV5Two, MoveV6Four, MovrelsV1V4, MovreldV4V1, MoveV1V5, EndProgram}; + passed &= run("observable relative reads and writes index a function-scope window", [] { + const std::array code{MoveM0GroupX, MoveV4One, MoveV5Two, MoveV6Four, MovrelsV1V4, + 0xe0700000u, 0x80000100u, MovreldV4V0, MoveV1V5, 0xe0700004u, 0x80000100u, EndProgram}; const auto shape = Scan(RecompileProgram(code)); - if (shape.functionArrays != 1 || shape.arrayLength != 3 || shape.accessChains < 2 || shape.selects > 4) std::fprintf(stderr, " window shape: arrays %zu length %u access chains %zu selects %zu\n", shape.functionArrays, shape.arrayLength, shape.accessChains, shape.selects); - return shape.functionArrays == 1 && shape.arrayLength == 3 && shape.accessChains >= 2 && shape.selects <= 4; + if (shape.functionArrays != 1 || shape.arrayLength != 3 || shape.accessChains < 2) std::fprintf(stderr, " window shape: arrays %zu length %u access chains %zu selects %zu\n", shape.functionArrays, shape.arrayLength, shape.accessChains, shape.selects); + return shape.functionArrays == 1 && shape.arrayLength == 3 && shape.accessChains >= 2; }); passed &= run("a program without relative addressing keeps its registers in SSA values", [] { - const std::array code{MoveV4One, MoveV5Two, MoveV6Four, MoveV1V5, EndProgram}; + const std::array code{MoveV4One, MoveV5Two, MoveV6Four, MoveV1V5, 0xe0700000u, 0x80000100u, EndProgram}; const auto shape = Scan(RecompileProgram(code)); return shape.functionArrays == 0 && shape.accessChains == 0; }); + if (passed) std::puts("Vector window shape and SSA control tests passed"); return passed ? 0 : 1; } diff --git a/core/libs/prx/libSceAgcDriver/tests/VertexBarrier.cpp b/core/libs/prx/libSceAgcDriver/tests/VertexBarrier.cpp index 8d591fe05..453fbbab7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/VertexBarrier.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/VertexBarrier.cpp @@ -12,6 +12,8 @@ namespace { constexpr std::array BarrierExport{0xbf8a0000u, 0xf80008cfu, 0u, 0xbf810000u}; constexpr std::array BarrierLdsExport{0xd8340000u, 0u, 0xbf8a0000u, 0xf80008cfu, 0u, 0xbf810000u}; +constexpr std::array VertexCapabilities{spv::CapabilityClipDistance}; + ShaderRecompiler::RecompileRequest vertexRequest(std::span code) { using namespace ShaderRecompiler; RecompileRequest request{}; @@ -21,6 +23,7 @@ ShaderRecompiler::RecompileRequest vertexRequest(std::span request.target.vulkanVersion = 0x00401000u; request.target.spirvVersion = 0x00010300u; request.target.subgroupSize = 32; + request.target.supportedCapabilities = VertexCapabilities; request.layout.pushConstantSizeBytes = 128; request.useCache = false; return request; diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index b924f0d5c..8528450d4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -152,6 +152,32 @@ void CheckUnwatch() { Require(!UnchangedSince(base, Block, adjacent), "unwatch hid a CPU edit in unrelated memory"); } +void CheckNewestStamp() { + void* memory = AllocateWatched(4 * Block); + const auto base = reinterpret_cast(memory); + const auto first = base + Block; + const auto second = base + 2 * Block; + std::memset(memory, 0x11, 4 * Block); + BumpCollectEpoch(); + CollectWrites(base, 4 * Block); + Unwatch(first, 2 * Block); + const auto firstBefore = NewestStamp(first, Block); + const auto secondBefore = NewestStamp(second, Block); + MarkWritten(second + 16, 4); + Require(NewestStamp(second, Block) > secondBefore, "a driver store into unwatched memory did not advance its stamp"); + Require(NewestStamp(first, Block) == firstBefore, "a driver store advanced the stamp of other unwatched memory"); + const auto generation = TrackerGeneration(); + static_cast(memory)[0] = 0x22; + CollectWritesUncached(base, Block); + Require(TrackerGeneration() > generation && NewestStamp(first, Block) == firstBefore, "a CPU store into watched memory advanced the stamp of unwatched memory"); + StoreOwnBytes(first, 1, [&] { *reinterpret_cast(first) = 0x42; }); + Require(NewestStamp(first, Block) > firstBefore, "an own store into unwatched memory did not advance its stamp"); + const auto partial = NewestStamp(base + 3 * Block - 8, Block + 8); + static_cast(memory)[3 * Block] = 0x33; + CollectWritesUncached(base + 3 * Block, Block); + Require(NewestStamp(base + 3 * Block - 8, Block + 8) > partial, "a CPU store into the watched part of a partly watched range did not advance its stamp"); +} + #ifdef _WIN32 void CheckPrivateMappingReuse() { void* memory = AllocateWatched(3 * Block); @@ -184,6 +210,67 @@ void CheckPrivateMappingReuse() { GuestArena::GuestArenaReset_nid_postfix(replacement, 3 * Block); GuestArena::GuestArenaRelease_nid_postfix(replacement, 3 * Block); } + +std::uint64_t ArenaHostHole() { + std::uintptr_t arena = 0; + std::size_t arenaBytes = 0; + GuestArena::GuestArenaRange_nid_postfix(&arena, &arenaBytes); + for (auto cursor = arena; cursor < arena + arenaBytes;) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(reinterpret_cast(cursor), &info, sizeof(info)) == sizeof(info), "cannot enumerate arena host holes"); + if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(cursor, 1)) return cursor; + const auto next = reinterpret_cast(info.BaseAddress) + info.RegionSize; + Require(next > cursor, "arena enumeration did not advance"); + cursor = next; + } + return 0; +} + +void CheckHostHole() { + constexpr std::size_t bytes = 64; + const auto hole = ArenaHostHole(); + if (hole == 0) { + std::cout << "host-hole query checks skipped: the arena contains no host hole\n"; + return; + } + const auto before = TrackerGeneration(); + Require(!Watched(hole, bytes), "an arena host hole reads as watched"); + Require(CollectWrites(hole, bytes) == 0 && CollectWritesUncached(hole, bytes) == 0, "an arena host hole was collected"); + auto* watched = static_cast(AllocateWatched(Block, true)); + struct Cleanup { + void* memory; + ~Cleanup() { GuestArena::GuestArenaRelease_nid_postfix(memory, Block); } + } cleanup{watched}; + const auto base = reinterpret_cast(watched); + for (unsigned epoch = 0; epoch < 2; ++epoch) { + BumpCollectEpoch(); + watched[7] ^= 1; + Require(CollectWrites(base + 7, 1) != 0, "a warm host-hole check could not collect its watched range"); + for (unsigned repeat = 0; repeat < 3; ++repeat) { + Require(CollectWrites(base + 7, 1) != 0, "a warm host-hole check lost its watched range"); + Require(CollectWrites(hole, bytes) == 0, "a repeated host-hole query hit a collect memo"); + Require(CollectWrites(hole + 7, 1) == 0, "a page-rounded host-hole query hit a collect memo"); + } + } + Require(StoredOver(hole, bytes, before), "an arena host hole reads as not stored over"); + Require(!UnchangedSince(hole, bytes, before), "an arena host hole reads as unchanged"); + const std::array queries{UnchangedQuery{hole, bytes, before}}; + Require(!UnchangedSinceAll(queries), "an arena host hole reads as unchanged in a batch"); + Require(!UnchangedSinceCollected(hole, bytes, before), "an arena host hole reads as unchanged since a collect"); + Require(MarkWritten(hole, bytes) == 0, "a driver store into an arena host hole is stamped"); + Require(StoreOwnBytes(hole, bytes, [] {}) == 0, "an own store into an arena host hole is stamped"); + Require(!WrittenSince(hole, bytes, before), "an arena host hole reads its stamps as written"); + std::array generations{before}; + std::array changed{}; + std::array cpu{}; + Require(!ChangedBlocks(hole, bytes, generations, changed, cpu) && changed[0] == BlockWritten && cpu[0] == 1, "an arena host hole uses block stamps"); + bool imported = false; + Require(ImportWatched(hole, bytes, [&] { + imported = true; + return true; + }) && imported, "an import into an arena host hole did not run"); + Require(!Watched(hole, bytes), "an import made an arena host hole watched"); +} #endif void CheckVersionRanges() { @@ -310,8 +397,10 @@ int main() { CheckSharedBlock(); CheckOwnStore(); CheckUnwatch(); + CheckNewestStamp(); #ifdef _WIN32 CheckPrivateMappingReuse(); + CheckHostHole(); #endif CheckVersionRanges(); CheckCollectBoundaries(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp new file mode 100644 index 000000000..45364ef78 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp @@ -0,0 +1,111 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Lanes = 32; +constexpr std::uint32_t InputBytes = 256; +constexpr std::uint32_t Fill = 0xdeadbeefu; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020082, 0xe0301000, 0x80000201, 0xbf8c3f70, 0xe0701000, 0x80010201, 0xbf810000, +}; + +struct Case { + const char* name; + std::uint32_t baseOffset; + std::uint32_t records; + bool present; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +ShaderRecompiler::RecompileRequest Request(std::span userData, const ShaderRecompiler::SpirvTarget& target, std::span memory) { + const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0u, {false, false, false}, false, 1}; + return { + {ShaderStage::Compute, reinterpret_cast(Code.data()), Code, 0, {}}, + {Lanes, 0, userData, compute, std::nullopt, std::nullopt, memory}, + target, + {0, 0, 0, 128} + }; +} + +void Run(AgcDriver::VulkanDevice& device, bool registrationModules) { + const std::string mode = registrationModules ? "registration modules" : "specialized modules"; + const std::array memory{{{reinterpret_cast(Code.data()), std::as_bytes(std::span(Code))}}}; + std::array userData{}; + auto target = device.ComputeTarget(Lanes); + target.registrationModules = registrationModules; + const auto request = Request(userData, target, memory); + const auto handle = ShaderRecompiler::PrepareShader(request); + const std::array cases{{ + {"aligned", 0, 128, true}, + {"base +1", 1, 128, true}, + {"base +2", 2, 128, true}, + {"base +3", 3, 128, true}, + {"records 37", 0, 37, true}, + {"records 64 base +2", 2, 64, true}, + {"absent", 0, 128, false}, + }}; + for (const auto& test : cases) { + Output.fill(Fill); + const auto input = test.present ? BufferDescriptor(Input.data() + test.baseOffset, test.records) : std::array{0u, 0u, 0u, 0x31016facu}; + const auto output = BufferDescriptor(Output.data(), sizeof(Output)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + ShaderRecompiler::SrtRuntime runtime{}; + runtime.userData = userData; + const auto capture = ShaderRecompiler::CaptureResources(request, runtime, *handle); + const auto result = ShaderRecompiler::MaterializeShader(request, *capture, *handle); + Require(!result->registrationModule, "buffer descriptor runtime, " + mode + ", " + test.name + ": a compute shader was emitted in registration form"); + device.Dispatch(*result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + device.WaitIdle(); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + std::uint32_t expected = 0; + if (test.present && lane * 4u + 4u <= test.records) std::memcpy(&expected, Input.data() + test.baseOffset + lane * 4u, sizeof(expected)); + Require(Output[lane] == expected, "buffer descriptor runtime, " + mode + ", " + test.name + ": lane " + std::to_string(lane) + " is " + Hex(Output[lane]) + ", expected " + Hex(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (std::uint32_t index = 0; index < InputBytes; ++index) Input[index] = static_cast(index * 41u + 3u + (index >> 3u)); + Run(*device, false); + Run(*device, true); + std::puts("buffer descriptor runtime tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Color1555Target.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Color1555Target.cpp new file mode 100644 index 000000000..e4cf4c69b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Color1555Target.cpp @@ -0,0 +1,299 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t ScissorWidth = 128; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Color1555Info = 0x8044u; +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32Export = 9; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 16> Programs{}; +std::size_t programCount = 0; + +using Color = std::array; + +std::uint16_t Texel(const Color& color) { + const auto unorm = [](float value, std::uint32_t maximum) { return static_cast(std::lround(std::fmin(std::fmax(static_cast(value), 0.0), 1.0) * maximum)); }; + return static_cast(unorm(color[0], 31) | (unorm(color[1], 31) << 5u) | (unorm(color[2], 31) << 10u) | (unorm(color[3], 1) << 15u)); +} + +std::span Program(const Color& color) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + code[at++] = 0x7e0002ffu | ((4u + channel) << 17u); + code[at++] = std::bit_cast(color[channel]); + } + code[at++] = 0xf800180fu; + code[at++] = 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t attrib3) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Color1555Info; + cx[0x31d] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = attrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_A1R5G5B5_UNORM_PACK16 && color.elementBytes == 2 && color.bytes == block.bytes, "the 1_5_5_5 unorm target did not decode as an A1R5G5B5 surface"); + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t exportFormat, std::span pixelCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(Triangle.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {ScissorWidth, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, exportFormat}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + const auto handle = ShaderRecompiler::PrepareShader(fragment); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}}; + snapshot.prepared->entries.push_back({0, handle}); + fragment.shader.code = snapshot.code; + fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + const ShaderRecompiler::SrtRuntime runtime{}; + const auto materialize = [&](const ShaderRecompiler::RecompileRequest& request) { + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request); + return invocation.Materialize(*invocation.Capture(runtime)); + }; + const auto pixelResult = materialize(fragment); + Require(pixelResult->variantId == ShaderRecompiler::GetPreparedArtifact(*handle).variantId, "the draw did not use the registered pixel shader"); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, const Block& block) { + AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(block.bytes / 2u); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +std::vector Fill(Block& block) { + std::vector halves(block.bytes / 2u); + for (std::size_t index = 0; index < halves.size(); ++index) halves[index] = static_cast((index * 0x9e3779b9u) >> 16u); + std::memcpy(block.data, halves.data(), block.bytes); + return halves; +} + +void ExpectSurface(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, std::uint16_t texel, const std::string& what) { + const auto stored = ReadBack(device, block); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 2u; + const bool inside = x < ScissorWidth; + const auto expected = inside ? texel : before[index]; + if (stored[index] == expected) continue; + char detail[160]; + std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) %s the scissor holds 0x%04x, expected 0x%04x", x, y, inside ? "inside" : "outside", stored[index], expected); + throw std::runtime_error(what + detail); + } + } +} + +void TargetTests(AgcDriver::VulkanDevice& device, Block& block, std::uint32_t attrib3, ColorTileMode mode, const char* kind) { + const ColorTargetLayout layout(Width, Height, mode, 2); + Require(layout.Bytes() == block.bytes, "the test surface size disagrees with its layout"); + const auto color = DecodeTarget(block, attrib3); + auto before = Fill(block); + struct Case { + Color color; + std::uint16_t texel; + }; + const std::array cases{{ + {{1.0f, 0.0f, 0.0f, 1.0f}, 0x801f}, + {{0.0f, 1.0f, 0.0f, 0.0f}, 0x03e0}, + {{0.0f, 0.0f, 1.0f, 0.0f}, 0x7c00}, + {{0.0f, 0.0f, 0.0f, 1.0f}, 0x8000}, + {{1.0f, 0.0f, 1.0f, 0.0f}, 0x7c1f}, + {{1.0f, 1.0f, 1.0f, 1.0f}, 0xffff}, + {{0.0f, 0.0f, 0.0f, 0.0f}, 0x0000}, + {{3.0f / 31.0f, 10.0f / 31.0f, 21.0f / 31.0f, 1.0f}, (3u) | (10u << 5u) | (21u << 10u) | 0x8000u}, + {{21.0f / 31.0f, 3.0f / 31.0f, 10.0f / 31.0f, 0.0f}, (21u) | (3u << 5u) | (10u << 10u)}, + {{1.0f / 31.0f, 30.0f / 31.0f, 2.0f / 31.0f, 0.75f}, (1u) | (30u << 5u) | (2u << 10u) | 0x8000u}, + {{2.0f, -1.0f, 10.0f / 31.0f, 0.25f}, (31u) | (0u << 5u) | (10u << 10u)}, + }}; + for (const auto& test : cases) { + Require(Texel(test.color) == test.texel, "the test's expected texel disagrees with the 1_5_5_5 field layout"); + Draw(device, color, Float32Export, Program(test.color)); + char what[160]; + std::snprintf(what, sizeof(what), "a 32_ABGR export of (%.9g, %.9g, %.9g, %.9g) into a %s 1_5_5_5 unorm target", test.color[0], test.color[1], test.color[2], test.color[3], kind); + ExpectSurface(device, block, layout, before, test.texel, what); + before = ReadBack(device, block); + programCount = 0; + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Block tiled(ColorTargetLayout(Width, Height, ColorTileMode::RenderTarget, 2).Bytes()); + Block linear(ColorTargetLayout(Width, Height, ColorTileMode::Linear, 2).Bytes()); + TargetTests(*device, tiled, TiledAttrib3, ColorTileMode::RenderTarget, "tiled"); + TargetTests(*device, linear, LinearAttrib3, ColorTileMode::Linear, "linear"); + std::puts("1_5_5_5 unorm color target tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp index 18d3083b4..1e4e5bcb3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" #include #include #include @@ -96,7 +97,7 @@ inline Shaders Compile(AgcDriver::VulkanDevice& device) { {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, {32, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, target, - {0, 0, shaders.vertexPush, 128 - shaders.vertexPush} + PixelPushLayout(shaders.vertexPush, target) }; fragment.useCache = false; shaders.pixel = ShaderRecompiler::Recompile(fragment); @@ -107,7 +108,7 @@ inline std::uint32_t Draw(AgcDriver::VulkanDevice& device, const Shaders& compil Pixels.fill(std::byte{0}); const std::array shaders{{ {ShaderStage::Vertex, &compiled.vertex, 0}, - {ShaderStage::Fragment, &compiled.pixel, compiled.vertexPush} + {ShaderStage::Fragment, &compiled.pixel, PixelPushOffset(compiled.vertexPush, device.Target())} }}; AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageReuse.cpp new file mode 100644 index 000000000..91db433d8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageReuse.cpp @@ -0,0 +1,78 @@ +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::Width; + +alignas(4096) std::array Depth{}; +alignas(256) std::array Output{}; +alignas(256) constexpr std::array LoadCode{ + 0x7e3c0280u, 0x7e3e0280u, 0x7e140280u, 0xf0001108u, 0x00010a1eu, + 0xbf8c3f70u, 0xf0201108u, 0x00010a1eu, 0x34060082u, 0xe0701000u, 0x80000a03u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array SampleCode{ + 0x7e3c02ffu, 0x3f000000u, 0x7e3e02ffu, 0x3f000000u, 0xf09c0108u, + 0x00610a1eu, 0x34060082u, 0xe0701000u, 0x80000a03u, 0xbf810000u, +}; + +void Read(AgcDriver::VulkanDevice& device, std::uint32_t format, std::uint32_t width, bool sampled, float expected) { + Output.fill(0xdeadbeefu); + const auto output = reinterpret_cast(Output.data()); + const auto depth = reinterpret_cast(Depth.data()); + const std::array data{ + static_cast(output), static_cast(output >> 32u), sizeof(Output), 0x31016facu, + static_cast(depth >> 8u), static_cast(depth >> 40u) | (format << 20u) | (((width - 1u) & 3u) << 30u), + ((width - 1u) >> 2u) | ((Height - 1u) << 14u), 0x90000facu, 0u, 0u, 0u, 0u, + 0x92u, 0u, 0u, 0u, + }; + const auto code = sampled ? std::span(SampleCode) : std::span(LoadCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{32, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, data, compute, std::nullopt, std::nullopt, memory}, device.Target(), {0, 0, 0, 128}, + }; + request.useCache = false; + const auto shader = ShaderRecompiler::Recompile(request); + device.Dispatch(shader, 1, 1, 1); + device.WaitIdle(); + for (std::size_t lane = 0; lane < 32; ++lane) { + Require((sampled && format == 7u ? std::abs(std::bit_cast(Output[lane]) - expected) <= 1.0f / 65535.0f : Output[lane] == std::bit_cast(expected)), "depth reuse returned the wrong GPU value at lane " + std::to_string(lane) + ": got " + std::to_string(Output[lane]) + ", expected " + std::to_string(std::bit_cast(expected))); + } + Require(std::all_of(Depth.begin(), Depth.end(), [](float value) { return value == 0.25f; }), "depth reuse changed the guest bytes"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto shaders = DepthFastClearHarness::Compile(*device); + const auto address = reinterpret_cast(Depth.data()); + for (const auto format : {VK_FORMAT_D32_SFLOAT, VK_FORMAT_D16_UNORM}) { + AgcDriver::Graphics::ClearDepthSurfaces(device->Device()); + Depth.fill(0.25f); + const DepthFastClearHarness::Surface surface{address, 0, 0, false, format}; + Require(DepthFastClearHarness::Draw(*device, shaders, surface, {.depthWrite = true}) == DepthFastClearHarness::Covered, "depth producer did not cover the surface"); + Read(*device, format == VK_FORMAT_D16_UNORM ? 7u : 22u, Width, true, 0.5f); + Read(*device, 22u, format == VK_FORMAT_D16_UNORM ? Width : Width / 2u, false, 0.25f); + } + std::puts("depth storage reuse GPU tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F16DenormalFlush.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F16DenormalFlush.cpp new file mode 100644 index 000000000..60e0f3fac --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F16DenormalFlush.cpp @@ -0,0 +1,353 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 1; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +struct alignas(256) Shader { + std::array words; +}; + +struct Vector { + std::uint32_t half; + std::uint32_t expected[4]; +}; + +struct Case { + const char* name; + std::uint32_t word; + std::span rows; +}; + +constexpr Vector CeilRows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a3c00u, 0x5a5a0000u, 0x5a5a3c00u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a3c00u, 0x5a5a0000u, 0x5a5a3c00u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a3c00u, 0x5a5a0000u, 0x5a5a3c00u}}, + {0x8001u, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u}}, + {0x8200u, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u}}, + {0x83ffu, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u}}, + {0x0400u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x8400u, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0xbc00u, {0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u}}, + {0x3800u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, +}; + +constexpr Vector CvtF32Rows[] = { + {0x0001u, {0x00000000u, 0x33800000u, 0x00000000u, 0x33800000u}}, + {0x0200u, {0x00000000u, 0x38000000u, 0x00000000u, 0x38000000u}}, + {0x03ffu, {0x00000000u, 0x387fc000u, 0x00000000u, 0x387fc000u}}, + {0x8001u, {0x80000000u, 0xb3800000u, 0x80000000u, 0xb3800000u}}, + {0x8200u, {0x80000000u, 0xb8000000u, 0x80000000u, 0xb8000000u}}, + {0x83ffu, {0x80000000u, 0xb87fc000u, 0x80000000u, 0xb87fc000u}}, + {0x0400u, {0x38800000u, 0x38800000u, 0x38800000u, 0x38800000u}}, + {0x8400u, {0xb8800000u, 0xb8800000u, 0xb8800000u, 0xb8800000u}}, + {0x0000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x3c00u, {0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u}}, + {0xbc00u, {0xbf800000u, 0xbf800000u, 0xbf800000u, 0xbf800000u}}, + {0x3800u, {0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3f000000u}}, +}; + +constexpr Vector CvtNormI16Rows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a0001u, 0x5a5a0000u, 0x5a5a0001u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a0002u, 0x5a5a0000u, 0x5a5a0002u}}, + {0x8001u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x8200u, {0x5a5a0000u, 0x5a5affffu, 0x5a5a0000u, 0x5a5affffu}}, + {0x83ffu, {0x5a5a0000u, 0x5a5afffeu, 0x5a5a0000u, 0x5a5afffeu}}, + {0x0400u, {0x5a5a0002u, 0x5a5a0002u, 0x5a5a0002u, 0x5a5a0002u}}, + {0x8400u, {0x5a5afffeu, 0x5a5afffeu, 0x5a5afffeu, 0x5a5afffeu}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a7fffu, 0x5a5a7fffu, 0x5a5a7fffu, 0x5a5a7fffu}}, + {0xbc00u, {0x5a5a8001u, 0x5a5a8001u, 0x5a5a8001u, 0x5a5a8001u}}, + {0x3800u, {0x5a5a4000u, 0x5a5a4000u, 0x5a5a4000u, 0x5a5a4000u}}, + {0xb800u, {0x5a5ac000u, 0x5a5ac000u, 0x5a5ac000u, 0x5a5ac000u}}, +}; + +constexpr Vector CvtNormU16Rows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a0002u, 0x5a5a0000u, 0x5a5a0002u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a0004u, 0x5a5a0000u, 0x5a5a0004u}}, + {0x8001u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x8200u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x83ffu, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x0400u, {0x5a5a0004u, 0x5a5a0004u, 0x5a5a0004u, 0x5a5a0004u}}, + {0x8400u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5affffu, 0x5a5affffu, 0x5a5affffu, 0x5a5affffu}}, + {0xbc00u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3800u, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u}}, + {0xb800u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, +}; + +constexpr Vector ExpRows[] = { + {0x0001u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x0200u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x03ffu, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x8001u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x8200u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x83ffu, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x0400u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x8400u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x0000u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0x3c00u, {0x5a5a4000u, 0x5a5a4000u, 0x5a5a4000u, 0x5a5a4000u}}, + {0xbc00u, {0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u}}, + {0xcb01u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a03fau, 0x5a5a03fau}}, + {0xcca0u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a002du, 0x5a5a002du}}, + {0xce3cu, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0001u, 0x5a5a0001u}}, + {0xc800u, {0x5a5a1c00u, 0x5a5a1c00u, 0x5a5a1c00u, 0x5a5a1c00u}}, + {0xc400u, {0x5a5a2c00u, 0x5a5a2c00u, 0x5a5a2c00u, 0x5a5a2c00u}}, +}; + +constexpr Vector FloorRows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x8001u, {0x5a5a8000u, 0x5a5abc00u, 0x5a5a8000u, 0x5a5abc00u}}, + {0x8200u, {0x5a5a8000u, 0x5a5abc00u, 0x5a5a8000u, 0x5a5abc00u}}, + {0x83ffu, {0x5a5a8000u, 0x5a5abc00u, 0x5a5a8000u, 0x5a5abc00u}}, + {0x0400u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x8400u, {0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0xbc00u, {0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u}}, + {0xb800u, {0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u}}, +}; + +constexpr Vector FractRows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0001u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0200u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a03ffu}}, + {0x8001u, {0x5a5a0000u, 0x5a5a3bffu, 0x5a5a0000u, 0x5a5a3bffu}}, + {0x8200u, {0x5a5a0000u, 0x5a5a3bffu, 0x5a5a0000u, 0x5a5a3bffu}}, + {0x83ffu, {0x5a5a0000u, 0x5a5a3bffu, 0x5a5a0000u, 0x5a5a3bffu}}, + {0x0400u, {0x5a5a0400u, 0x5a5a0400u, 0x5a5a0400u, 0x5a5a0400u}}, + {0x8400u, {0x5a5a3bffu, 0x5a5a3bffu, 0x5a5a3bffu, 0x5a5a3bffu}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0xbc00u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0xb800u, {0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u}}, + {0x4900u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, +}; + +constexpr Vector FrexpExpRows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5affe9u, 0x5a5a0000u, 0x5a5affe9u}}, + {0x0200u, {0x5a5a0000u, 0x5a5afff2u, 0x5a5a0000u, 0x5a5afff2u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5afff2u, 0x5a5a0000u, 0x5a5afff2u}}, + {0x8001u, {0x5a5a0000u, 0x5a5affe9u, 0x5a5a0000u, 0x5a5affe9u}}, + {0x8200u, {0x5a5a0000u, 0x5a5afff2u, 0x5a5a0000u, 0x5a5afff2u}}, + {0x83ffu, {0x5a5a0000u, 0x5a5afff2u, 0x5a5a0000u, 0x5a5afff2u}}, + {0x0400u, {0x5a5afff3u, 0x5a5afff3u, 0x5a5afff3u, 0x5a5afff3u}}, + {0x8400u, {0x5a5afff3u, 0x5a5afff3u, 0x5a5afff3u, 0x5a5afff3u}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a0001u, 0x5a5a0001u, 0x5a5a0001u, 0x5a5a0001u}}, + {0xbc00u, {0x5a5a0001u, 0x5a5a0001u, 0x5a5a0001u, 0x5a5a0001u}}, + {0x4900u, {0x5a5a0004u, 0x5a5a0004u, 0x5a5a0004u, 0x5a5a0004u}}, +}; + +constexpr Vector FrexpMantRows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a3800u, 0x5a5a0000u, 0x5a5a3800u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a3800u, 0x5a5a0000u, 0x5a5a3800u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a3bfeu, 0x5a5a0000u, 0x5a5a3bfeu}}, + {0x8001u, {0x5a5a8000u, 0x5a5ab800u, 0x5a5a8000u, 0x5a5ab800u}}, + {0x8200u, {0x5a5a8000u, 0x5a5ab800u, 0x5a5a8000u, 0x5a5ab800u}}, + {0x83ffu, {0x5a5a8000u, 0x5a5abbfeu, 0x5a5a8000u, 0x5a5abbfeu}}, + {0x0400u, {0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u}}, + {0x8400u, {0x5a5ab800u, 0x5a5ab800u, 0x5a5ab800u, 0x5a5ab800u}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u}}, + {0xbc00u, {0x5a5ab800u, 0x5a5ab800u, 0x5a5ab800u, 0x5a5ab800u}}, + {0x4900u, {0x5a5a3900u, 0x5a5a3900u, 0x5a5a3900u, 0x5a5a3900u}}, +}; + +constexpr Vector LogRows[] = { + {0x0001u, {0x5a5afc00u, 0x5a5ace00u, 0x5a5afc00u, 0x5a5ace00u}}, + {0x0200u, {0x5a5afc00u, 0x5a5acb80u, 0x5a5afc00u, 0x5a5acb80u}}, + {0x03ffu, {0x5a5afc00u, 0x5a5acb00u, 0x5a5afc00u, 0x5a5acb00u}}, + {0x8001u, {0x5a5afc00u, 0x5a5afe00u, 0x5a5afc00u, 0x5a5afe00u}}, + {0x8200u, {0x5a5afc00u, 0x5a5afe00u, 0x5a5afc00u, 0x5a5afe00u}}, + {0x83ffu, {0x5a5afc00u, 0x5a5afe00u, 0x5a5afc00u, 0x5a5afe00u}}, + {0x0400u, {0x5a5acb00u, 0x5a5acb00u, 0x5a5acb00u, 0x5a5acb00u}}, + {0x8400u, {0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u}}, + {0x0000u, {0x5a5afc00u, 0x5a5afc00u, 0x5a5afc00u, 0x5a5afc00u}}, + {0x3c00u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0xbc00u, {0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u}}, + {0x4900u, {0x5a5a42a5u, 0x5a5a42a5u, 0x5a5a42a5u, 0x5a5a42a5u}}, + {0x0801u, {0x5a5aca80u, 0x5a5aca80u, 0x5a5aca80u, 0x5a5aca80u}}, +}; + +constexpr Vector RcpRows[] = { + {0x0001u, {0x5a5a7c00u, 0x5a5a7c00u, 0x5a5a7c00u, 0x5a5a7c00u}}, + {0x0200u, {0x5a5a7c00u, 0x5a5a7800u, 0x5a5a7c00u, 0x5a5a7800u}}, + {0x03ffu, {0x5a5a7c00u, 0x5a5a7401u, 0x5a5a7c00u, 0x5a5a7401u}}, + {0x8001u, {0x5a5afc00u, 0x5a5afc00u, 0x5a5afc00u, 0x5a5afc00u}}, + {0x8200u, {0x5a5afc00u, 0x5a5af800u, 0x5a5afc00u, 0x5a5af800u}}, + {0x83ffu, {0x5a5afc00u, 0x5a5af401u, 0x5a5afc00u, 0x5a5af401u}}, + {0x0400u, {0x5a5a7400u, 0x5a5a7400u, 0x5a5a7400u, 0x5a5a7400u}}, + {0x8400u, {0x5a5af400u, 0x5a5af400u, 0x5a5af400u, 0x5a5af400u}}, + {0x0000u, {0x5a5a7c00u, 0x5a5a7c00u, 0x5a5a7c00u, 0x5a5a7c00u}}, + {0x3c00u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0xbc00u, {0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u, 0x5a5abc00u}}, + {0x7bffu, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0100u, 0x5a5a0100u}}, + {0x7401u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a03ffu, 0x5a5a03ffu}}, + {0xfbffu, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8100u, 0x5a5a8100u}}, + {0xf404u, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a83fcu, 0x5a5a83fcu}}, + {0x7800u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0200u, 0x5a5a0200u}}, +}; + +constexpr Vector RsqRows[] = { + {0x0001u, {0x5a5a7c00u, 0x5a5a6c00u, 0x5a5a7c00u, 0x5a5a6c00u}}, + {0x0200u, {0x5a5a7c00u, 0x5a5a59a8u, 0x5a5a7c00u, 0x5a5a59a8u}}, + {0x03ffu, {0x5a5a7c00u, 0x5a5a5801u, 0x5a5a7c00u, 0x5a5a5801u}}, + {0x8001u, {0x5a5afc00u, 0x5a5afe00u, 0x5a5afc00u, 0x5a5afe00u}}, + {0x8200u, {0x5a5afc00u, 0x5a5afe00u, 0x5a5afc00u, 0x5a5afe00u}}, + {0x83ffu, {0x5a5afc00u, 0x5a5afe00u, 0x5a5afc00u, 0x5a5afe00u}}, + {0x0400u, {0x5a5a5800u, 0x5a5a5800u, 0x5a5a5800u, 0x5a5a5800u}}, + {0x8400u, {0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u}}, + {0x0000u, {0x5a5a7c00u, 0x5a5a7c00u, 0x5a5a7c00u, 0x5a5a7c00u}}, + {0x3c00u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0xbc00u, {0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u}}, + {0x4400u, {0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u, 0x5a5a3800u}}, +}; + +constexpr Vector SinRows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0006u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a0a48u, 0x5a5a0000u, 0x5a5a0a48u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a0e47u, 0x5a5a0000u, 0x5a5a0e47u}}, + {0x8001u, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u, 0x5a5a8006u}}, + {0x8200u, {0x5a5a8000u, 0x5a5a8a48u, 0x5a5a8000u, 0x5a5a8a48u}}, + {0x83ffu, {0x5a5a8000u, 0x5a5a8e47u, 0x5a5a8000u, 0x5a5a8e47u}}, + {0x0400u, {0x5a5a0e48u, 0x5a5a0e48u, 0x5a5a0e48u, 0x5a5a0e48u}}, + {0x8400u, {0x5a5a8e48u, 0x5a5a8e48u, 0x5a5a8e48u, 0x5a5a8e48u}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0xbc00u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x00a0u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a03edu}}, + {0x80a0u, {0x5a5a8000u, 0x5a5a8000u, 0x5a5a8000u, 0x5a5a83edu}}, + {0x3800u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, +}; + +constexpr Vector SqrtRows[] = { + {0x0001u, {0x5a5a0000u, 0x5a5a0c00u, 0x5a5a0000u, 0x5a5a0c00u}}, + {0x0200u, {0x5a5a0000u, 0x5a5a1da8u, 0x5a5a0000u, 0x5a5a1da8u}}, + {0x03ffu, {0x5a5a0000u, 0x5a5a1fffu, 0x5a5a0000u, 0x5a5a1fffu}}, + {0x8001u, {0x5a5a8000u, 0x5a5afe00u, 0x5a5a8000u, 0x5a5afe00u}}, + {0x8200u, {0x5a5a8000u, 0x5a5afe00u, 0x5a5a8000u, 0x5a5afe00u}}, + {0x83ffu, {0x5a5a8000u, 0x5a5afe00u, 0x5a5a8000u, 0x5a5afe00u}}, + {0x0400u, {0x5a5a2000u, 0x5a5a2000u, 0x5a5a2000u, 0x5a5a2000u}}, + {0x8400u, {0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u}}, + {0x0000u, {0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u, 0x5a5a0000u}}, + {0x3c00u, {0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u, 0x5a5a3c00u}}, + {0xbc00u, {0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u, 0x5a5afe00u}}, + {0x4400u, {0x5a5a4000u, 0x5a5a4000u, 0x5a5a4000u, 0x5a5a4000u}}, +}; + +constexpr Case Cases[] = { + {"v_ceil_f16", 0x7e14b904u, CeilRows}, + {"v_cvt_f32_f16", 0x7e141704u, CvtF32Rows}, + {"v_cvt_norm_i16_f16", 0x7e14c704u, CvtNormI16Rows}, + {"v_cvt_norm_u16_f16", 0x7e14c904u, CvtNormU16Rows}, + {"v_exp_f16", 0x7e14b104u, ExpRows}, + {"v_floor_f16", 0x7e14b704u, FloorRows}, + {"v_fract_f16", 0x7e14bf04u, FractRows}, + {"v_frexp_exp_i16_f16", 0x7e14b504u, FrexpExpRows}, + {"v_frexp_mant_f16", 0x7e14b304u, FrexpMantRows}, + {"v_log_f16", 0x7e14af04u, LogRows}, + {"v_rcp_f16", 0x7e14a904u, RcpRows}, + {"v_rsq_f16", 0x7e14ad04u, RsqRows}, + {"v_sin_f16", 0x7e14c104u, SinRows}, + {"v_sqrt_f16", 0x7e14ab04u, SqrtRows}, +}; + +constexpr std::uint32_t CaseCount = std::size(Cases); +std::array Shaders{}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, const Case& item, const Shader& shader, std::uint32_t denorm16, bool ieee) { + Input.fill(0u); + for (std::uint32_t row = 0; row < item.rows.size(); ++row) { + Input[row * Inputs] = item.rows[row].half; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(shader.words); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{(3u << 4u) | (denorm16 << 6u), true, ieee, false}; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const Case& item, std::uint32_t denorm16, bool ieee) { + for (std::uint32_t row = 0; row < item.rows.size(); ++row) { + const auto actual = Output[row]; + const auto expected = item.rows[row].expected[denorm16]; + Require(actual == expected, std::string(item.name) + " denorm16 " + std::to_string(denorm16) + (ieee ? " ieee" : " no ieee") + " input " + + Hex(item.rows[row].half) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (std::uint32_t index = 0; index < CaseCount; ++index) { + Shaders[index].words = {0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e1402ffu, 0x5a5a5a5au, Cases[index].word, + 0xe0701000u, 0x80010a02u, 0xbf810000u}; + for (std::uint32_t denorm16 = 0u; denorm16 < 4u; ++denorm16) { + for (const bool ieee : {false, true}) { + Run(*device, Cases[index], Shaders[index], denorm16, ieee); + Check(Cases[index], denorm16, ieee); + } + } + } + std::puts("f16 denormal flush tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MsaaEnableSingleSample.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MsaaEnableSingleSample.cpp new file mode 100644 index 000000000..d7500ebc2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MsaaEnableSingleSample.cpp @@ -0,0 +1,132 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::byte Kept{0x40}; + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; +alignas(256) std::array ClearMemory{}; +alignas(256) std::array EnabledMemory{}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +std::vector DrawBoth() { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + const auto draw = [&](std::array& memory, std::uint32_t scanMode) { + const auto color = reinterpret_cast(memory.data()); + Append(commands, 0x69, {0x318, static_cast(color >> 8u)}); + Append(commands, 0x69, {0x390, static_cast(color >> 40u)}); + Append(commands, 0x69, {0x292, scanMode}); + Append(commands, 0x2d, {3, 2}); + }; + draw(ClearMemory, 2); + draw(EnabledMemory, 3); + return commands; +} + +void MsaaEnableDrawsLikeDisabled() { + ClearMemory.fill(Kept); + EnabledMemory.fill(Kept); + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + auto commands = DrawBoth(); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + const auto kept = static_cast(std::count(ClearMemory.begin(), ClearMemory.end(), Kept)); + Require(kept != 0 && kept != ClearMemory.size(), "the triangle without MSAA_ENABLE covered " + std::to_string(ClearMemory.size() - kept) + " of " + std::to_string(ClearMemory.size()) + " bytes, expected an edge through the target"); + for (std::size_t index = 0; index < ClearMemory.size(); ++index) { + Require(EnabledMemory[index] == ClearMemory[index], "byte " + std::to_string(index) + " is " + std::to_string(static_cast(EnabledMemory[index])) + " with MSAA_ENABLE on a single-sample target and " + std::to_string(static_cast(ClearMemory[index])) + " without it"); + } +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + MsaaEnableDrawsLikeDisabled(); + AgcDriverShutdown_nid_postfix(); + std::puts("MSAA_ENABLE on a single-sample target drew the same pixels as the state without it"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDispatch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDispatch.cpp index e6bf9cfdf..068e0f31c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDispatch.cpp @@ -181,7 +181,7 @@ int main() { } else if (device->WorkGroupCountLimit()[0] < Groups) { CheckSplitCoverage(*device); } else { - std::printf("oversized dispatch split not tested: the device's %u workgroup limit never splits a %ux1x1 dispatch\n", device->WorkGroupCountLimit()[0], Groups); + std::printf("oversized dispatch split: skipped, the device reports a %u workgroup limit, which never splits a %ux1x1 dispatch\n", device->WorkGroupCountLimit()[0], Groups); return VulkanTestSkipped; } AgcDriverShutdown_nid_postfix(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp index 092a17fd7..e1455fa12 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp @@ -148,7 +148,7 @@ int main() { return std::ranges::find(target.supportedCapabilities, static_cast(capability)) != target.supportedCapabilities.end(); }; if (!supports(spv::CapabilityFloat16) || !supports(spv::CapabilityRoundingModeRTE) || !supports(spv::CapabilityDenormPreserve)) { - std::puts("packed f16 native: the device has no native f16 with RTE rounding and kept denormals, skipped"); + std::puts("packed f16 native: skipped, the device has no native f16 with RTE rounding and kept denormals"); return VulkanTestSkipped; } std::vector withoutFloat16; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedMipTailTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedMipTailTarget.cpp new file mode 100644 index 000000000..a8a85b621 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedMipTailTarget.cpp @@ -0,0 +1,257 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +using AgcDriver::Graphics::TextureTileMode; +using AgcDriver::Graphics::TileMipLayout; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Side = 256; +constexpr std::uint32_t Levels = 9; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t BlockSide = 128; +constexpr std::size_t SurfaceBytes = BlockBytes + Side * Side * 4u + BlockBytes; +constexpr std::uint32_t Color32FloatInfo = (4u << 2u) | (7u << 8u); +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Initial = 0x5a5a5a5au; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, Levels> PixelCodes{}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::string Hex(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%llx", static_cast(value)); + return text; +} + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, BlockBytes)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + void* mapped = mmap(nullptr, SurfaceBytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + SurfaceBytes != begin + SurfaceBytes + BlockBytes) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + BlockBytes - aligned); + block = reinterpret_cast(aligned); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, SurfaceBytes); + } +#endif + Require(block != nullptr, "packed mip tail target: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, SurfaceBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes); +#else + munmap(block, SurfaceBytes); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, SurfaceBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t level, const TileMipLayout& mip) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(address >> 8u); + cx[0x31b] = level << 26u; + cx[0x31c] = Color32FloatInfo; + cx[0x31d] = 0; + cx[0x3b0] = ((Levels - 1u) << 28u) | ((Side - 1u) << 14u) | (Side - 1u); + cx[0x3b8] = TiledAttrib3; + cx[0x390] = static_cast(address >> 40u); + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R32_SFLOAT && color.tileMode == ColorTileMode::RenderTarget, "the COLOR_32 float view did not decode as an SW_64KB_R_X R32 target"); + Require(color.mip == level && color.mipCount == Levels && color.mipTail == mip.tail, "the color view did not keep its mip of the surface"); + Require(color.extent.width == mip.width && color.extent.height == mip.height && color.address == address + mip.tiledOffset, "the color view did not decode the extent and address of mip " + std::to_string(level)); + return color; +} + +void DrawLevel(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t level, const TileMipLayout& mip, std::uint32_t value) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + auto& pixelWords = PixelCodes[level]; + std::size_t at = 0; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + pixelWords[at++] = 0x7e0002ffu | ((4u + channel) << 17u); + pixelWords[at++] = value; + } + pixelWords[at++] = 0xf800180fu; + pixelWords[at++] = 0x07060504u; + pixelWords[at++] = 0xbf810000u; + const std::span pixelCode(pixelWords.data(), at); + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(Triangle.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + const auto color = DecodeTarget(surface, level, mip); + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = color.extent; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(mip.height), static_cast(mip.width), -static_cast(mip.height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, color.extent}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, Float32Export}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Expect(std::vector& expected, const TileMipLayout& mip, std::uint32_t value) { + const ColorTargetLayout block(BlockSide, BlockSide, ColorTileMode::RenderTarget, 4); + const ColorTargetLayout own(mip.width, mip.height, ColorTileMode::RenderTarget, 4); + for (std::uint32_t y = 0; y < mip.height; ++y) { + for (std::uint32_t x = 0; x < mip.width; ++x) { + const auto offset = mip.tail ? block.Offset(x + mip.tailX, y + mip.tailY) : mip.tiledOffset + own.Offset(x, y); + Require(expected.at(offset / 4u) == Initial, "the expected texels of two mips overlap at byte " + Hex(offset)); + expected[offset / 4u] = value; + } + } +} + +void Check(const std::uint8_t* texels, const std::vector& expected, const std::string& what) { + const auto* words = reinterpret_cast(texels); + for (std::size_t index = 0; index < expected.size(); ++index) { + Require(words[index] == expected[index], what + ": surface byte " + Hex(index * 4u) + " holds " + Hex(words[index]) + ", expected " + Hex(expected[index])); + } +} + +void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels) { + const auto mips = AgcDriver::Graphics::ComputeElementMipLayout(TextureTileMode::kR64KBX, 4, Side, Side, Levels); + Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) == SurfaceBytes, "the 256x256 R32 surface with 9 mips is not one tail block, mip 0 and mip 1"); + Require(!mips[1].tail && mips[2].tail && mips[3].tail && mips[2].width == 64 && mips[3].width == 32, "mips 2 and 3 of the 256x256 R32 surface are not its first packed tail mips"); + const auto surface = AddressOf(texels); + std::fill_n(reinterpret_cast(texels), SurfaceBytes / 4u, Initial); + AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + std::vector expected(SurfaceBytes / 4u, Initial); + const std::array, 3> draws{{{2u, 0.25f}, {3u, 0.75f}, {1u, 0.5f}}}; + for (const auto& [level, drawn] : draws) { + const auto value = std::bit_cast(drawn); + DrawLevel(device, surface, level, mips[level], value); + Require(StorageTexture::FindPending(surface, SurfaceBytes) == nullptr, "the draw into mip " + std::to_string(level) + " rendered into a resident image instead of the guest-memory fallback"); + Expect(expected, mips[level], value); + Check(texels, expected, "after the draw into mip " + std::to_string(level)); + } + AgcDriver::Graphics::ClearCachedTextures(device.Device()); +} + +} + +int main() { + try { +#ifdef _WIN32 + _putenv_s("APS5_NO_RESIDENT_TARGETS", "1"); +#else + setenv("APS5_NO_RESIDENT_TARGETS", "1", 1); +#endif + GuestBlock block; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device, block.Data()); + std::puts("packed mip tail target tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp new file mode 100644 index 000000000..e4e66f07f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp @@ -0,0 +1,276 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using namespace AgcDriver::Graphics; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::byte Kept{0x40}; +alignas(256) std::array Pixels{}; +alignas(256) std::array DepthMemory{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; +alignas(256) constexpr std::array MaskedPixelCode{ + 0x7e040f00, 0x36060481, 0x7da40680, 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; +alignas(256) constexpr std::array FullPixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; +constexpr std::array, 12> DepthTriangles{{ + {-1.0f, -1.0f, 0.25f, 1.0f}, {0.0f, -1.0f, 0.25f, 1.0f}, {-1.0f, 1.0f, 0.25f, 1.0f}, + {-1.0f, 1.0f, 0.25f, 1.0f}, {0.0f, -1.0f, 0.25f, 1.0f}, {0.0f, 1.0f, 0.25f, 1.0f}, + {0.0f, -1.0f, 0.75f, 1.0f}, {1.0f, -1.0f, 0.75f, 1.0f}, {0.0f, 1.0f, 0.75f, 1.0f}, + {0.0f, 1.0f, 0.75f, 1.0f}, {1.0f, -1.0f, 0.75f, 1.0f}, {1.0f, 1.0f, 0.75f, 1.0f} +}}; + +struct PipelineTrace { + PFN_vkGetDeviceProcAddr resolve = nullptr; + PFN_vkCreateGraphicsPipelines create = nullptr; + PFN_vkCmdBindPipeline bind = nullptr; + PFN_vkCmdDraw draw = nullptr; + DeviceFunctions functions{}; + std::mutex mutex; + std::condition_variable released; + bool allowOptimization = false; + std::vector fast; + std::vector optimized; + VkCommandBuffer boundCommands = VK_NULL_HANDLE; + VkPipeline bound = VK_NULL_HANDLE; + std::vector drawn; + + void ReleaseOptimization() { + { + std::lock_guard lock(mutex); + allowOptimization = true; + } + released.notify_all(); + } +}; + +PipelineTrace* Trace = nullptr; + +VKAPI_ATTR VkResult VKAPI_CALL CreatePipelines(VkDevice device, VkPipelineCache cache, std::uint32_t count, const VkGraphicsPipelineCreateInfo* infos, const VkAllocationCallbacks* allocator, VkPipeline* pipelines) { + const bool optimizing = std::any_of(infos, infos + count, [](const auto& info) { return (info.flags & VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT) != 0; }); + if (optimizing) { + std::unique_lock lock(Trace->mutex); + Trace->released.wait(lock, [] { return Trace->allowOptimization; }); + } + const auto result = Trace->create(device, cache, count, infos, allocator, pipelines); + std::lock_guard lock(Trace->mutex); + for (std::uint32_t index = 0; index < count; ++index) { + if (pipelines[index] == VK_NULL_HANDLE || (infos[index].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) != 0) continue; + auto& handles = (infos[index].flags & VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT) != 0 ? Trace->optimized : Trace->fast; + handles.push_back(pipelines[index]); + } + return result; +} + +VKAPI_ATTR void VKAPI_CALL BindPipeline(VkCommandBuffer commands, VkPipelineBindPoint point, VkPipeline pipeline) { + if (point == VK_PIPELINE_BIND_POINT_GRAPHICS) { + Trace->boundCommands = commands; + Trace->bound = pipeline; + } + Trace->bind(commands, point, pipeline); +} + +VKAPI_ATTR void VKAPI_CALL DrawCommands(VkCommandBuffer commands, std::uint32_t vertices, std::uint32_t instances, std::uint32_t firstVertex, std::uint32_t firstInstance) { + Trace->drawn.push_back(Trace->boundCommands == commands ? Trace->bound : VK_NULL_HANDLE); + Trace->draw(commands, vertices, instances, firstVertex, firstInstance); +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL Resolve(VkDevice device, const char* name) { + if (std::strcmp(name, "vkCreateGraphicsPipelines") == 0) return reinterpret_cast(CreatePipelines); + if (std::strcmp(name, "vkCmdBindPipeline") == 0) return reinterpret_cast(BindPipeline); + if (std::strcmp(name, "vkCmdDraw") == 0) return reinterpret_cast(DrawCommands); + return Trace->resolve(device, name); +} + +struct OptimizationDrain { + VkDevice device; + ~OptimizationDrain() { + Trace->ReleaseOptimization(); + WaitForOptimizedPipelines(device); + } +}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void CheckPixels(bool masked, std::string_view phase) { + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + const bool drawn = masked ? (pixel % Width) % 2u == 0u : pixel % Width < Width / 2u; + const auto expected = drawn ? std::byte{255} : Kept; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto actual = Pixels[pixel * 4u + channel]; + Require(actual == expected, std::string(phase) + ": pixel " + std::to_string(pixel) + " channel " + std::to_string(channel) + " is " + std::to_string(static_cast(actual)) + ", expected " + std::to_string(static_cast(expected))); + } + } +} + +void CheckDrawn(PipelineTrace& trace, bool optimized) { + std::lock_guard lock(trace.mutex); + const auto& expected = optimized ? trace.optimized : trace.fast; + Require(trace.drawn.size() == 1, "pipeline readback did not observe exactly one real draw"); + Require(std::find(expected.begin(), expected.end(), trace.drawn.front()) != expected.end(), optimized ? "readback draw did not bind an optimized pipeline" : "readback draw did not bind a fast-linked pipeline"); +} + +void Run(AgcDriver::VulkanDevice& device, Context context, PipelineTrace& trace, std::uint32_t waveSize, VkFormat depthFormat, bool optimize) { + const bool masked = depthFormat == VK_FORMAT_UNDEFINED; + const auto target = device.Target(); + const auto vertices = masked ? std::span>(Triangle) : std::span>(DepthTriangles); + const auto vertexBuffer = BufferDescriptor(vertices.data(), 16u, static_cast(vertices.size())); + const std::vector vertexUserData(vertexBuffer.begin(), vertexBuffer.end()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + const auto pixelCode = masked ? std::span(MaskedPixelCode) : std::span(FullPixelCode); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = waveSize == 32u; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + State state{}; + state.stages = {ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + if (!masked) { + state.depth = DepthTarget{reinterpret_cast(DepthMemory.data()), 0, {Width, Height}, depthFormat, 0.5f, 0}; + state.depthTest = true; + state.depthCompare = VK_COMPARE_OP_LESS; + } + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(vertices.size()), 0, 1, 0, false}; + const auto render = [&](bool optimized, std::string_view phase) { + Pixels.fill(Kept); + trace.drawn.clear(); + trace.bound = VK_NULL_HANDLE; + trace.boundCommands = VK_NULL_HANDLE; + AgcDriver::Graphics::Draw(context, state, draw, shaders); + device.WaitIdle(); + CheckDrawn(trace, optimized); + CheckPixels(masked, phase); + }; + render(false, "fast link"); + trace.ReleaseOptimization(); + WaitForOptimizedPipelines(context.device); + Require(PipelineLibraryCountersOf(context.device).linked == 1, "the readback pipeline was not linked from graphics libraries"); + Require(trace.optimized.size() == (optimize ? 1u : 0u), "the optimized pipeline creation count differs from the requested mode"); + render(optimize, "completed link"); + std::printf("pipeline readback passed: fast draws %u, optimized draws %u, optimized creations %zu\n", optimize ? 1u : 2u, optimize ? 1u : 0u, trace.optimized.size()); +} + +} + +int main(int argc, char** argv) { + try { + std::string_view mode = "d32"; + bool force = false; + for (int index = 1; index < argc; ++index) { + const std::string_view argument = argv[index]; + if (argument == "--force-lto") force = true; + else { + Require(argument == "color32" || argument == "color64" || argument == "d32" || argument == "d16", "unknown pipeline readback case"); + mode = argument; + } + } + PipelineTrace trace; + Trace = &trace; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if (!context.graphicsPipelineLibrary) { + std::puts("skipped, graphics pipeline libraries are unavailable or disabled"); + return VulkanTestSkipped; + } + const bool masked = mode == "color32" || mode == "color64"; + if (masked && device->Target().subgroupSize < 32u) { + std::puts("skipped, the masked pixel shader requires at least 32 subgroup lanes"); + return VulkanTestSkipped; + } + if (force) context.pipelineLinkTimeOptimization = true; + const bool optimize = context.pipelineLinkTimeOptimization && std::getenv("APS5_NO_PIPELINE_LTO") == nullptr; + std::printf("pipeline readback: %s, %.*s, LTO %s%s\n", device->DeviceName().c_str(), static_cast(mode.size()), mode.data(), optimize ? "enabled" : "disabled", force ? " (explicit test override)" : " (production policy)"); + trace.resolve = context.deviceProc; + trace.create = context.Function("vkCreateGraphicsPipelines"); + trace.bind = context.Function("vkCmdBindPipeline"); + trace.draw = context.Function("vkCmdDraw"); + trace.functions = context.functions != nullptr ? *context.functions : DeviceFunctions{}; + trace.functions.cmdBindPipeline = BindPipeline; + trace.functions.cmdDraw = DrawCommands; + context.functions = &trace.functions; + context.deviceProc = Resolve; + OptimizationDrain drain{context.device}; + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Run(*device, context, trace, mode == "color32" ? 32u : 64u, masked ? VK_FORMAT_UNDEFINED : (mode == "d16" ? VK_FORMAT_D16_UNORM : VK_FORMAT_D32_SFLOAT), optimize); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelWaveReduction.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelWaveReduction.cpp index 83f21e2b2..7c007900a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelWaveReduction.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelWaveReduction.cpp @@ -194,13 +194,16 @@ int main() { }}; for (const auto& layout : layouts) { for (const auto& reduction : reductions) { - const auto code = PixelCode(reduction); - std::uint32_t partial = 0; - for (std::uint32_t pass = 0; pass < 4; ++pass) { - Draw(*device, code, layout); - partial += Check(reduction, target.subgroupSize); + for (const bool windowed : {false, true}) { + auto code = PixelCode(reduction); + if (windowed) code.insert(std::find(code.begin(), code.end(), 0x7e0e02f2u), 0x7e008514u); + std::uint32_t partial = 0; + for (std::uint32_t pass = 0; pass < 4; ++pass) { + Draw(*device, code, layout); + partial += Check(reduction, target.subgroupSize); + } + std::printf("%s%s, laid out for %u lanes on %u-lane subgroups: %u partial waves over 4 draws\n", reduction.name, windowed ? " in a v_movreld_b32 window" : "", layout.subgroupSize, target.subgroupSize, partial); } - std::printf("%s, laid out for %u lanes on %u-lane subgroups: %u partial waves over 4 draws\n", reduction.name, layout.subgroupSize, target.subgroupSize, partial); } } std::puts("pixel wave reduction tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedExports.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedExports.cpp index b0b85a7b7..bdbfd1362 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedExports.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedExports.cpp @@ -29,7 +29,7 @@ void Run(AgcDriver::VulkanDevice& device, bool integer) { const std::array values = integer ? std::array{11, 22, 33, 44} : std::array{0, 0x3e800000u, 0x3f000000u, 0x3f800000u}; const std::array code{0x7e0002ffu, values[0], 0x7e0202ffu, values[1], 0x7e0402ffu, values[2], 0x7e0602ffu, values[3], 0xf800100fu, 0x03020100u, 0xf800183fu, 0x03020100u, 0xbf810000u}; AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, integer ? 0x7007u : 0x9009u}}; - RecompileRequest request{{ShaderStage::Fragment, 0x20000, code, 0, {}}, {32, 0, {}, {}, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), {}, {}}, device.Target(), {0, 0, 0, 128}}; + RecompileRequest request{{ShaderStage::Fragment, 0x20000, code, 0, {}}, {32, 0, {}, {}, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), {}, {}}, device.Target(), PixelPushLayout(0u, device.Target())}; const auto handle = PrepareShader(request); const auto& artifact = GetPreparedArtifact(*handle); std::uint32_t scalarType = 0; @@ -85,6 +85,10 @@ void Run(AgcDriver::VulkanDevice& device, bool integer) { request.useCache = cached; const auto materialized = MaterializeShader(request, *capture, *handle); Require(materialized->spirv.data() == pixel->spirv.data() && materialized->PipelineVariantId() == pixel->PipelineVariantId(), "export mapping did not reuse its specialized module"); + if (device.Target().registrationModules) { + Require(materialized->PipelineVariantId() == first->PipelineVariantId(), "registration modules: the export mapping specialized the fragment module"); + continue; + } for (std::size_t cursor = 5; cursor < materialized->spirv.size();) { const auto count = materialized->spirv[cursor] >> 16u; Require(count != 0u && count <= materialized->spirv.size() - cursor, "invalid specialized fragment instruction"); @@ -94,7 +98,7 @@ void Run(AgcDriver::VulkanDevice& device, bool integer) { } request.useCache = true; if (integer) continue; - const std::array shaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::Fragment, pixel.get(), 0}}}; + const std::array shaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::Fragment, pixel.get(), PixelPushOffset(0u, device.Target())}}}; pixels.fill(0xdeadbeef); otherPixels.fill(0xdeadbeef); device.Draw(state, draw, shaders); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp index 5d456afdf..e405f2c16 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" #include "VulkanTestDevice.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp" #include "SceShaders.hpp" @@ -130,6 +131,30 @@ void Check(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ShaderPath path } const auto stages = PrepareGraphicsStages(prepared, target); Require(stages.size() == (tessellation ? 4u : 2u), "graphics preparation compiled the wrong stages"); + if (!mesh && !tessellation && target.registrationModules) { + const auto prebuild = [&](const std::vector& group, std::initializer_list picked) { + std::vector shaders; + for (const auto index : picked) { + Require(group.at(index).registration != nullptr, "registration modules: a prepared stage has no registration result"); + shaders.push_back({group[index].stage, group[index].registration.get(), group[index].pushOffset}); + } + const auto before = AgcDriver::Graphics::PipelineLibraryCountersOf(device.Device()); + Require(device.PrebuildGraphicsLibraries(prepared.state.stages, shaders), "registration modules: the shader libraries were not prebuilt"); + const auto after = AgcDriver::Graphics::PipelineLibraryCountersOf(device.Device()); + Require(after.linked == before.linked, "registration modules: a prebuild linked a pipeline"); + return std::pair{after.built[1] - before.built[1], after.built[2] - before.built[2]}; + }; + const auto vertexOnly = prebuild(stages, {0u}); + Require(vertexOnly.first == 1u, "registration modules: the vertex stage alone did not build its pre-rasterization library"); + const auto pixelOnly = prebuild(stages, {1u}); + Require(pixelOnly.second == 1u, "registration modules: the pixel stage alone did not build its fragment library"); + const auto pair = prebuild(stages, {0u, 1u}); + Require(pair.first == 0u && pair.second == 0u, "registration modules: the pair of stages did not find the libraries prebuilt one stage at a time"); + const auto again = PrepareGraphicsStages(prepared, target); + Require(again.front().registration != stages.front().registration, "registration modules: the second preparation reused the first result object"); + const auto repeated = prebuild(again, {0u, 1u}); + Require(repeated.first == 0u && repeated.second == 0u, "registration modules: another materialization of the same stages did not find the prebuilt libraries"); + } for (const auto& stage : stages) stage.snapshot->prepared->entries.push_back(stage.entry); const auto& pixelArtifact = GetPreparedArtifact(*stages.back().entry.handle); Require(!pixelArtifact.fragmentParameters.empty() && pixelArtifact.fragmentParameters.front().sourceLocation == 3u, "prepared fragment lost interpolant mapping"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 4aa54af7d..e34258828 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -200,7 +200,11 @@ void FailureCapture(AgcDriver::VulkanDevice& device) { #else Require(unsetenv("APS5_DUMP_SHADERS") == 0, "cannot disable shader capture"); #endif - Require(failure([&] { static_cast(AgcDriver::DriverDetail::PrepareShaderWithDiagnostics(request)); }) == expected, "disabled shader capture changed the preparation failure"); + std::ostringstream prefix; + prefix << "shader 0x" << std::hex << request.shader.codeAddress << " stage " << static_cast(request.shader.stage) << ": "; + const auto diagnosed = failure([&] { static_cast(AgcDriver::DriverDetail::PrepareShaderWithDiagnostics(request)); }); + Require(diagnosed.rfind(prefix.str(), 0) == 0, "diagnosed failure names no shader address and stage"); + Require(diagnosed.substr(prefix.str().size()) == expected, "diagnosed failure changed the reason"); Require(!std::ifstream(path, std::ios::binary).is_open(), "disabled shader capture created a request"); #ifdef _WIN32 Require(_putenv_s("APS5_DUMP_SHADERS", "1") == 0, "cannot enable shader capture"); @@ -208,12 +212,14 @@ void FailureCapture(AgcDriver::VulkanDevice& device) { Require(setenv("APS5_DUMP_SHADERS", "1", 1) == 0, "cannot enable shader capture"); #endif const auto actual = failure([&] { static_cast(AgcDriver::DriverDetail::PrepareShaderWithDiagnostics(request)); }); + std::ostringstream dumpedPrefix; + dumpedPrefix << "shader 0x" << std::hex << request.shader.codeAddress << " stage " << static_cast(request.shader.stage) << " (" << path << "): "; + Require(actual == dumpedPrefix.str() + expected, "enabled shader capture changed the diagnosed failure"); #ifdef _WIN32 Require(_putenv_s("APS5_DUMP_SHADERS", saved.c_str()) == 0, "cannot restore shader capture"); #else Require((previous != nullptr ? setenv("APS5_DUMP_SHADERS", saved.c_str(), 1) : unsetenv("APS5_DUMP_SHADERS")) == 0, "cannot restore shader capture"); #endif - Require(actual == expected, "shader capture changed the preparation failure"); std::ifstream file(path, std::ios::binary); std::ostringstream text; text << file.rdbuf(); @@ -531,7 +537,7 @@ void RegisteredVertexWorkgroup(const ShaderRecompiler::SpirvTarget& target, bool return; } alignas(256) static constexpr std::array code{ - 0xd8340000u, 0u, 0xd7600000u, 0x00017f00u, 0x7e020200u, 0xf80008cfu, 0x04030201u, 0xbf810000u + 0xd8340000u, 0u, 0xd7600000u, 0x00017f05u, 0x7e020200u, 0xf80008cfu, 0x04030201u, 0xbf810000u }; struct Header { Shader shader{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ProvokingVertex.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ProvokingVertex.cpp index f1651a590..2654a113b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ProvokingVertex.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ProvokingVertex.cpp @@ -50,7 +50,7 @@ ShaderRecompiler::RecompileResult GuestPixel(const ShaderRecompiler::SpirvTarget request.context.waveSize = 64; request.context.pixel = pixel; request.target = target; - request.layout.pushConstantSizeBytes = 128; + request.layout = PixelPushLayout(0u, target); request.useCache = false; auto result = ShaderRecompiler::Recompile(request); result.variantId = 3; @@ -130,7 +130,7 @@ int main() { const auto guestPixel = GuestPixel(device->Target()); const std::array guestShaders{{ {ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, - {ShaderRecompiler::ShaderStage::Fragment, &guestPixel, 0} + {ShaderRecompiler::ShaderStage::Fragment, &guestPixel, PixelPushOffset(0u, device->Target())} }}; for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP}) Run(*device, topology, guestShaders, true, "guest P0"); std::puts("provoking vertex rendering tests passed: GLSL and guest P0, triangle list and strip, first/last/first"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/RecordedKeptBytes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RecordedKeptBytes.cpp index a41fe4456..f6e0f04fd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/RecordedKeptBytes.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RecordedKeptBytes.cpp @@ -200,13 +200,13 @@ void Draw(AgcDriver::VulkanDevice& device, GuestBuffer& pixels, GuestBuffer& ver {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, {64, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, target, - {0, 0, vertexPush, 128 - vertexPush} + PixelPushLayout(vertexPush, target) }; fragment.useCache = false; const auto pixelResult = ShaderRecompiler::Recompile(fragment); const std::array shaders{{ {ShaderStage::Vertex, &vertexResult, 0}, - {ShaderStage::Fragment, &pixelResult, vertexPush} + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} }}; AgcDriver::Graphics::State state{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/RelativeAncillary.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeAncillary.cpp new file mode 100644 index 000000000..98d2d1b89 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeAncillary.cpp @@ -0,0 +1,148 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; +constexpr std::uint32_t Extent = 64u; +alignas(256) std::array pixels{}; +alignas(256) constexpr std::array, 6> positions{{ + {0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f, 1.0f}, + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; +constexpr std::array vertexCode{ + 0xe0382000u, 0x80000005u, 0xbf8c3f70u, 0xf80008cfu, 0x03020100u, 0xbf810000u +}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void Draw(AgcDriver::VulkanDevice& device, std::span fragmentCode, std::uint32_t offset, std::uint32_t waveSize, std::byte expected, bool reject = false, bool implicitInputs = false) { + pixels.fill(std::byte{0x40}); + const auto target = device.Target(); + const auto userData = BufferDescriptor(positions.data(), 16u, positions.size()); + const auto code = std::span(vertexCode); + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(code.data()), code, 0, {}}, + {32u, 0u, userData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, {}}, + target, {0u, 0u, 0u, 64u} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto pushBytes = static_cast(vertexResult.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = waveSize == 32u; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); + pixel.ancillary = true; + if (implicitInputs) { + pixel.inputAddr |= ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionZ) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionW); + pixel.posX = pixel.posY = pixel.posZ = pixel.posW = true; + } + pixel.targetOutputMode[0] = 9u; + pixel.targetExportMapping.fill(0xe4u); + const std::array pixelData{offset}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(fragmentCode.data()), fragmentCode, 0, {}}, + {waveSize, 0u, pixelData, std::nullopt, pixel, std::nullopt, {}}, target, PixelPushLayout(pushBytes, target) + }; + fragment.useCache = false; + const auto pixelResult = [&] { + try { + auto result = ShaderRecompiler::Recompile(fragment); + Require(!reject, "an unsupported ancillary word was accepted"); + return result; + } catch (const std::exception& error) { + if (!reject || std::string(error.what()).find("packed pixel ancillary input has an unsupported live use") == std::string::npos) throw; + return ShaderRecompiler::RecompileResult{}; + } + }(); + if (reject) return; + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0u}, {ShaderStage::Fragment, &pixelResult, PixelPushOffset(pushBytes, target)} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(pixels.data()), {Extent, Extent}, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Extent, Extent}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0.0f, static_cast(Extent), static_cast(Extent), -static_cast(Extent), 0.0f, 1.0f}; + state.scissor = {{0, 0}, {Extent, Extent}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15u; + state.blends = {state.blend}; + AgcDriver::Pm4::DrawParameters draw{0u, 3u, 0u, 1u, 0u, false}; + draw.firstVertex = 3u; + draw.firstInstance = 2u; + device.Draw(state, draw, shaders); + device.WaitIdle(); + for (const auto value : pixels) Require(value == expected, "relative ancillary draw produced an incorrect pixel: " + std::to_string(std::to_integer(value))); +} + +} // namespace + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const auto waveSize : {32u, 64u}) { + const std::array read{ + 0xbefc0300u, 0x7e028700u, 0xd5480001u, 0x02111101u, + 0x7e020d01u, 0x060202f2u, 0xf800180fu, 0x01010101u + }; + auto code = std::vector(read.begin(), read.end()); + code.push_back(0xbf810000u); + std::cerr << "wave " << waveSize << " packed read\n"; + Draw(*device, code, 0u, waveSize, std::byte{255}); + Draw(*device, code, 4u, waveSize, std::byte{255}, false, true); + const std::array prefix{ + 0xd5480000u, 0x02111100u, 0x7e000d00u, 0x7e020280u, + 0x7e0402f2u, 0xbefc0300u, 0x7e008502u, 0x7e008702u + }; + code.assign(prefix.begin(), prefix.end()); + code.insert(code.end(), {0xf800180fu, 0x01010101u, 0xbf810000u}); + std::cerr << "in-range\n"; + Draw(*device, code, 1u, waveSize, std::byte{255}); + std::cerr << "out-of-range\n"; + Draw(*device, code, 8u, waveSize, std::byte{0}); + code[6] = 0x7e02d100u; + code.erase(code.begin() + 7); + std::cerr << "in-range\n"; + Draw(*device, code, 0x00010001u, waveSize, std::byte{255}); + std::cerr << "out-of-range\n"; + Draw(*device, code, 0x00010008u, waveSize, std::byte{0}); + code.assign(prefix.begin(), prefix.begin() + 6); + code.insert(code.end(), {0xbefe0480u, 0x7e008502u, 0xbefe04c1u, 0xf800180fu, 0x01010101u, 0xbf810000u}); + std::cerr << "inactive write\n"; + Draw(*device, code, 1u, waveSize, std::byte{0}); + for (const auto modifier : {0x0000e402u, 0xff00e402u}) { + code.assign(prefix.begin(), prefix.begin() + 6); + code.insert(code.end(), {0x7e0084fau, modifier, 0xf800180fu, 0x01010101u, 0xbf810000u}); + std::cerr << "DPP write " << modifier << '\n'; + Draw(*device, code, 1u, waveSize, modifier == 0xff00e402u ? std::byte{255} : std::byte{0}); + Draw(*device, code, 8u, waveSize, std::byte{0}); + } + const std::array unsupported{0xbefc0300u, 0x7e028700u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + Draw(*device, unsupported, 0u, waveSize, std::byte{0}, true); + Draw(*device, unsupported, 4u, waveSize, std::byte{0}, true, true); + } + std::cout << "relative ancillary GPU readback tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/RelativeStageInputs.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeStageInputs.cpp new file mode 100644 index 000000000..ae310f425 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeStageInputs.cpp @@ -0,0 +1,148 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Extent = 64u; +alignas(256) std::array pixels{}; +alignas(256) std::array output{}; +alignas(256) constexpr std::array, 6> positions{{ + {0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f, 1.0f}, + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +constexpr std::array relativeVertexCode{ + 0xbefc0385u, 0x7e028700u, 0xbefc0388u, 0x7e008700u, 0x4a000300u, + 0xe0382000u, 0x80000000u, 0xbf8c3f70u, 0xf80008cfu, 0x03020100u, 0xbf810000u +}; +constexpr std::array vertexCode{ + 0xe0382000u, 0x80000005u, 0xbf8c3f70u, 0xf80008cfu, 0x03020100u, 0xbf810000u +}; +constexpr std::array relativePixelCode{ + 0xbefc0383u, 0x7e008700u, 0xf800180fu, 0x00000000u, 0xbf810000u +}; +constexpr std::array pixelCode{ + 0x7e0002f2u, 0xf800180fu, 0x00000000u, 0xbf810000u +}; +constexpr std::array computeCode{ + 0xbefc0382u, 0x7e028700u, 0x34020283u, 0x4a000300u, 0x34000082u, + 0x7e028700u, 0xe0701000u, 0x80000100u, 0xbf810000u +}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, stride == 0u ? 0x31016facu : 0x01016facu}; +} + +void Draw(AgcDriver::VulkanDevice& device, bool relativeVertex) { + pixels.fill(std::byte{0x40}); + const auto target = device.Target(); + const auto userData = BufferDescriptor(positions.data(), 16u, positions.size()); + const auto code = relativeVertex ? std::span(relativeVertexCode) : std::span(vertexCode); + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(code.data()), code, 0, {}}, + {32u, 0u, userData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, {}}, + target, {0u, 0u, 0u, 64u} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto pushBytes = static_cast(vertexResult.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = true; + if (!relativeVertex) { + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) + | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY) + | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionZ) + | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionW); + pixel.posX = pixel.posY = pixel.posZ = pixel.posW = true; + } + pixel.targetOutputMode[0] = 9u; + pixel.targetExportMapping.fill(0xe4u); + const auto fragmentCode = relativeVertex ? std::span(pixelCode) : std::span(relativePixelCode); + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(fragmentCode.data()), fragmentCode, 0, {}}, + {32u, 0u, {}, std::nullopt, pixel, std::nullopt, {}}, target, PixelPushLayout(pushBytes, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0u}, {ShaderStage::Fragment, &pixelResult, PixelPushOffset(pushBytes, target)} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(pixels.data()), {Extent, Extent}, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Extent, Extent}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0.0f, static_cast(Extent), static_cast(Extent), -static_cast(Extent), 0.0f, 1.0f}; + state.scissor = {{0, 0}, {Extent, Extent}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15u; + state.blends = {state.blend}; + AgcDriver::Pm4::DrawParameters draw{0u, 3u, 0u, 1u, 0u, false}; + draw.firstVertex = relativeVertex ? 1u : 3u; + draw.firstInstance = 2u; + device.Draw(state, draw, shaders); + device.WaitIdle(); + for (const auto value : pixels) Require(value == std::byte{255}, "relative stage input draw did not cover every pixel in white"); +} + +void Dispatch(AgcDriver::VulkanDevice& device) { + output.fill(0xdeadbeefu); + const auto userData = BufferDescriptor(output.data(), 0u, sizeof(output)); + const ShaderRecompiler::ShaderComputeStageInfo compute{{8u, 1u, 2u}, 0u, {false, false, false}, false, 3u}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(computeCode.data()), computeCode, 0, {}}, + {32u, 0u, userData, compute, std::nullopt, std::nullopt, {}}, device.Target(), {0u, 0u, 0u, 128u} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1u, 1u, 1u); + device.WaitIdle(); + for (std::size_t index = 0; index < output.size(); ++index) { + Require(output[index] == index / 8u, "relative stage input dispatch did not preserve the implicit Z input at output " + std::to_string(index) + + ": got " + std::to_string(output[index]) + ", expected " + std::to_string(index / 8u)); + } +} + +template +bool Run(const char* name, TAction action) { + try { + action(); + std::cout << "relative stage inputs: " << name << " GPU readback passed\n"; + return true; + } catch (const std::exception& error) { + std::cerr << "relative stage inputs: " << name << ": " << error.what() << '\n'; + return false; + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + bool passed = Run("vertex", [&] { Draw(*device, true); }); + passed &= Run("pixel", [&] { Draw(*device, false); }); + passed &= Run("compute", [&] { Dispatch(*device); }); + if (!passed) return 1; + std::cout << "relative stage input tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 0cf28122d..9d1978dbb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -42,6 +42,8 @@ constexpr std::uint32_t Type2D = 9; constexpr std::size_t SurfaceBytes = std::size_t{Side} * Side * 4u; constexpr std::size_t Surfaces = 12; constexpr std::size_t BlockBytes = SurfaceBytes * Surfaces; +constexpr std::size_t RegistrationGapBytes = 65536; +constexpr std::size_t AllocationBytes = BlockBytes + RegistrationGapBytes; constexpr std::uint64_t Budget = 1ull << 20u; constexpr std::uint64_t Ceiling = Budget + Budget / 4u; @@ -164,9 +166,9 @@ class Block { public: explicit Block(const Context& context) : context(context) { #ifdef _WIN32 - memory = VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + memory = VirtualAlloc(nullptr, AllocationBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); #else - memory = std::aligned_alloc(65536, BlockBytes); + memory = std::aligned_alloc(65536, AllocationBytes); #endif Require(memory != nullptr, "cannot allocate the texture block"); std::memset(memory, 0x5a, BlockBytes); @@ -449,7 +451,9 @@ void viewEvictionTests(const Context& context) { const std::weak_ptr view = Sampled(context, imported.Surface(0)); Require(!view.expired() && TextureCacheUsage().sampledBytes == 0, "the view of a cached storage image did not enter the sampled texture cache at 0 bytes"); Sampled(context, snapshots.Surface(0)); - const auto capacity = static_cast(Budget / TextureCacheUsage().sampledBytes); + const auto snapshotBytes = TextureCacheUsage().sampledBytes; + Require(snapshotBytes != 0, "the snapshot fixture unexpectedly became a view of a cached storage image"); + const auto capacity = static_cast(Budget / snapshotBytes); for (std::size_t surface = 1; surface <= capacity; ++surface) Sampled(context, snapshots.Surface(surface)); Require(!view.expired(), "the sampled texture cache evicted a view of a cached storage image, which frees no memory"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilRefExport.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilRefExport.cpp new file mode 100644 index 000000000..ac0318165 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilRefExport.cpp @@ -0,0 +1,159 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t WaveSize = 64; +constexpr std::byte Kept{0x40}; +alignas(256) std::array Pixels{}; +alignas(256) std::array DepthMemory{}; +alignas(256) std::array StencilMemory{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PlainPixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array ColumnPixelCode{ + 0x7e040f00, 0x7e0e02f2, 0xf8000082, 0x00000200, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array RowPixelCode{ + 0x7e040f01, 0x7e0e02f2, 0xf8000082, 0x00000200, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array, 3> Vertices{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}, +}}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +ShaderRecompiler::ShaderPixelStageInfo PixelInfo(std::uint32_t shaderControl) { + const AgcDriver::Registers context{ + {0x1b3, 0x300u}, {0x1b4, 0x300u}, {0x1b6, 0u}, {0x1c5, 9u}, {0x203, shaderControl}, + }; + return AgcDriver::Graphics::DecodePixelStageInfo(context, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}); +} + +void Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, std::uint32_t shaderControl, const VkStencilOpState& stencil) { + Pixels.fill(Kept); + const auto target = device.Target(); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Vertices.data(), 16u, static_cast(Vertices.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, PixelInfo(shaderControl), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(DepthMemory.data()), reinterpret_cast(StencilMemory.data()), {Width, Height}, VK_FORMAT_D32_SFLOAT_S8_UINT, 1.0f, 0}; + state.stencilTest = true; + state.stencilFront = stencil; + state.stencilBack = stencil; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Vertices.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(const std::function& drawn, std::string_view what) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto expected = drawn(x, y) ? std::byte{255} : Kept; + const auto actual = Pixels[(y * Width + x) * 4u]; + Require(actual == expected, std::string(what) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") is " + std::to_string(static_cast(actual)) + ", expected " + (drawn(x, y) ? "drawn" : "stencil-rejected")); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!TargetHasCapability(device->Target(), spv::CapabilityStencilExportEXT)) { + std::puts("skipped, the device lacks VK_EXT_shader_stencil_export"); + return VulkanTestSkipped; + } + constexpr std::uint32_t exported = 0x52u; + constexpr std::uint32_t plain = 0x50u; + const VkStencilOpState replace{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 0u}; + const auto equal = [](std::uint32_t reference) { return VkStencilOpState{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, 0xffu, 0u, reference}; }; + Draw(*device, ColumnPixelCode, exported, replace); + Check([](std::uint32_t, std::uint32_t) { return true; }, "the draw that replaces the stencil with the exported column"); + for (const auto column : {0u, 5u, 37u, 63u}) { + Draw(*device, PlainPixelCode, plain, equal(column)); + Check([&](std::uint32_t x, std::uint32_t) { return x == column; }, "stencil equal to " + std::to_string(column) + " after the exported column was stored"); + } + Draw(*device, ColumnPixelCode, exported, equal(200u)); + Check([](std::uint32_t, std::uint32_t) { return true; }, "the exported column tested against the stored column"); + Draw(*device, RowPixelCode, exported, equal(200u)); + Check([](std::uint32_t x, std::uint32_t y) { return x == y; }, "the exported row tested against the stored column"); + Draw(*device, ColumnPixelCode, plain, equal(9u)); + Check([](std::uint32_t x, std::uint32_t) { return x == 9u; }, "a column export without STENCIL_TEST_VAL_EXPORT_ENABLE"); + std::puts("stencil reference export tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp new file mode 100644 index 000000000..4944237df --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp @@ -0,0 +1,209 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Columns = 9; +constexpr std::uint32_t Ones = 0xffffffffu; +constexpr std::uint32_t Marker = 0x5a5a5a5au; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array ExecKeptCode{ + 0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, + 0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000a, 0x80148114, 0x4a141481, + 0x7e160214, 0x7e180226, 0xbe96047e, 0x7e1a0216, 0x7e1c0217, 0xbf098414, 0xbf850001, 0xbe26fff6, + 0xbe98047e, 0xbefe04c1, 0x7e1e0226, 0x7e200218, 0x7e220219, 0x7e240214, 0xe0701000, 0x80010a01, + 0xe0701100, 0x80010b01, 0xe0701200, 0x80010c01, 0xe0701300, 0x80010d01, 0xe0701400, 0x80010e01, + 0xe0701500, 0x80010f01, 0xe0701600, 0x80011001, 0xe0701700, 0x80011101, 0xe0701800, 0x80011201, + 0xbf810000, +}; + +alignas(256) constexpr std::array ExecResetCode{ + 0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, + 0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000b, 0x80148114, 0x4a141481, + 0x7e160214, 0x7e180226, 0xbe96047e, 0x7e1a0216, 0x7e1c0217, 0xbf098414, 0xbf850002, 0xbefe04c1, + 0xbe26fff5, 0xbe98047e, 0xbefe04c1, 0x7e1e0226, 0x7e200218, 0x7e220219, 0x7e240214, 0xe0701000, + 0x80010a01, 0xe0701100, 0x80010b01, 0xe0701200, 0x80010c01, 0xe0701300, 0x80010d01, 0xe0701400, + 0x80010e01, 0xe0701500, 0x80010f01, 0xe0701600, 0x80011001, 0xe0701700, 0x80011101, 0xe0701800, + 0x80011201, 0xbf810000, +}; + +constexpr std::array Names{"passes", "last pass", "s38 in the pass", "exec lo in the pass", "exec hi in the pass", "s38 after the loop", "exec lo after the loop", "exec hi after the loop", "total passes"}; + +struct Group { + std::uint32_t first; + std::uint32_t last; + std::array values; +}; + +struct Case { + const char* name; + std::uint64_t mask; + std::vector groups; +}; + +const std::vector ExecKeptCases{ + {"all lanes", ~0ull, { + {0, 31, {1, 1, Ones, Ones, 0, Ones, Ones, Ones, 2}}, + {32, 63, {1, 2, Ones, 0, Ones, Ones, Ones, Ones, 2}}, + }}, + {"low half", 0xffffffffull, { + {0, 31, {1, 1, 0, Ones, 0, 0, Ones, 0, 1}}, + {32, 63, {0, 0, 0, 0, 0, 0, Ones, 0, 1}}, + }}, + {"high half", 0xffffffffull << 32, { + {0, 31, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {32, 63, {1, 1, 0, 0, Ones, 0, 0, Ones, 1}}, + }}, + {"no lane", 0ull, { + {0, 63, {0, 0, 0, 0, 0, Marker, 0, 0, 0}}, + }}, + {"lanes 3 and 40", (1ull << 3) | (1ull << 40), { + {0, 63, {0, 0, 0, 0, 0, 8, 8, 0x100u, 2}}, + {3, 3, {1, 1, 0x100u, 8, 0, 8, 8, 0x100u, 2}}, + {40, 40, {1, 2, 8, 0, 0x100u, 8, 8, 0x100u, 2}}, + }}, + {"lane 3", 1ull << 3, { + {0, 63, {0, 0, 0, 0, 0, 0, 8, 0, 1}}, + {3, 3, {1, 1, 0, 8, 0, 0, 8, 0, 1}}, + }}, + {"lane 40", 1ull << 40, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, 0x100u, 1}}, + {40, 40, {1, 1, 0, 0, 0x100u, 0, 0, 0x100u, 1}}, + }}, +}; + +const std::vector ExecResetCases{ + {"all lanes", ~0ull, { + {0, 63, {0, 0, 0, 0, 0, Ones, Ones, Ones, 1}}, + {0, 31, {1, 1, Ones, Ones, 0, Ones, Ones, Ones, 1}}, + }}, + {"low half", 0xffffffffull, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {0, 31, {1, 1, 0, Ones, 0, 0, 0, Ones, 1}}, + }}, + {"high half", 0xffffffffull << 32, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {32, 63, {1, 1, 0, 0, Ones, 0, 0, Ones, 1}}, + }}, + {"no lane", 0ull, { + {0, 63, {0, 0, 0, 0, 0, Marker, 0, 0, 0}}, + }}, + {"lanes 3 and 40", (1ull << 3) | (1ull << 40), { + {0, 63, {0, 0, 0, 0, 0, 0x100u, 0x100u, Ones, 1}}, + {3, 3, {1, 1, 0x100u, 8, 0, 0x100u, 0x100u, Ones, 1}}, + }}, + {"lane 3", 1ull << 3, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {3, 3, {1, 1, 0, 8, 0, 0, 0, Ones, 1}}, + }}, + {"lane 40", 1ull << 40, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {40, 40, {1, 1, 0, 0, 0x100u, 0, 0, Ones, 1}}, + }}, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, std::uint64_t mask) { + for (std::uint32_t lane = 0; lane < Threads; ++lane) Input[lane] = ((mask >> lane) & 1u) != 0u ? 1u : 0u; + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {64, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const char* variant, const Case& testCase) { + std::array, Threads> expected{}; + for (const auto& group : testCase.groups) { + for (std::uint32_t lane = group.first; lane <= group.last; ++lane) expected[lane] = group.values; + } + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + for (std::uint32_t column = 0; column < Columns; ++column) { + const auto actual = Output[column * Threads + lane]; + Require(actual == expected[lane][column], std::string("subvector loop ") + variant + ", initial exec " + testCase.name + ": lane " + std::to_string(lane) + " " + Names[column] + " is " + std::to_string(actual) + ", expected " + std::to_string(expected[lane][column])); + } + } +} + +void CheckWave32Throws(AgcDriver::VulkanDevice& device, std::span code) { + std::vector userData(8, 0u); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{32, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.ComputeTarget(32), + {0, 0, 0, 128} + }; + request.useCache = false; + try { + static_cast(ShaderRecompiler::Recompile(request)); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("s_subvector_loop_begin in a wave32 shader is not measured") != std::string::npos, std::string("wave32 subvector loop threw an unexpected error: ") + error.what()); + return; + } + Require(false, "a wave32 subvector loop was recompiled"); +} + +void RunCases(AgcDriver::VulkanDevice& device, const char* variant, std::span code, const std::vector& cases) { + for (const auto& testCase : cases) { + Run(device, code, testCase.mask); + Check(variant, testCase); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + CheckWave32Throws(*device, ExecKeptCode); + if (device->Target().subgroupSize < 32u) { + std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + RunCases(*device, "exec kept by the body", ExecKeptCode, ExecKeptCases); + RunCases(*device, "body resets exec", ExecResetCode, ExecResetCases); + std::puts("subvector loop tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index 5453e3c05..0e991c307 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -317,7 +317,7 @@ int main() { if (!device) return VulkanTestSkipped; const auto target = device->Target(); if (!target.tessellation.has_value()) { - std::puts("Tessellation tests skipped: the device has no tessellation shaders"); + std::puts("tessellation: skipped, the device has no tessellation shaders"); return VulkanTestSkipped; } ExpectRefusal(target, ShaderStage::TessellationControl, HullUnguardedFactors, "not written by control point 0 alone"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp new file mode 100644 index 000000000..54e132b6c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp @@ -0,0 +1,173 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t ValueCount = 16; +constexpr std::uint32_t Groups = ValueCount * ValueCount / Threads; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +constexpr std::uint32_t Values[ValueCount] = { + 0x00000000u, 0x80000000u, 0x00000001u, 0x80000001u, 0x007fffffu, 0x807fffffu, 0x00800000u, 0x80800000u, + 0x3f800000u, 0xbf800000u, 0x7f800000u, 0xff800000u, 0x7fc00000u, 0x7f800001u, 0xffc00000u, 0x7f7fffffu, +}; + +constexpr std::uint32_t CaptureExec = 0xbe95037eu; +constexpr std::uint32_t CaptureVcc = 0xbe95036au; + +alignas(256) std::array Code{ + 0x34020084u, 0x34060082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xbe94037eu, 0x7c240b04u, + 0xbe95037eu, 0xbefe0314u, 0xd501000au, 0x00550280u, 0xe0701000u, 0x80010a03u, 0xbf810000u, +}; + +struct Row { + const char* condition; + std::uint32_t cmpx; + std::uint32_t cmp; + std::uint32_t kept[Groups]; + std::uint32_t flushed[Groups]; +}; + +constexpr std::uint32_t RowCount = 12; +constexpr Row Rows[RowCount] = { + {"eq", 0x7c240b04u, 0x7c040b04u, + {0x00030003u, 0x00080004u, 0x00200010u, 0x00800040u, 0x02000100u, 0x08000400u, 0x00000000u, 0x80000000u}, + {0x003f003fu, 0x003f003fu, 0x003f003fu, 0x00800040u, 0x02000100u, 0x08000400u, 0x00000000u, 0x80000000u}}, + {"ge", 0x7c2c0b04u, 0x7c0c0b04u, + {0x0aab0aabu, 0x0aa80aafu, 0x0aa00abfu, 0x0a800affu, 0x0a000bffu, 0x08008fffu, 0x00000000u, 0x8bff0000u}, + {0x0abf0abfu, 0x0abf0abfu, 0x0abf0abfu, 0x0a800affu, 0x0a000bffu, 0x08008fffu, 0x00000000u, 0x8bff0000u}}, + {"gt", 0x7c280b04u, 0x7c080b04u, + {0x0aa80aa8u, 0x0aa00aabu, 0x0a800aafu, 0x0a000abfu, 0x08000affu, 0x00008bffu, 0x00000000u, 0x0bff0000u}, + {0x0a800a80u, 0x0a800a80u, 0x0a800a80u, 0x0a000abfu, 0x08000affu, 0x00008bffu, 0x00000000u, 0x0bff0000u}}, + {"le", 0x7c260b04u, 0x7c060b04u, + {0x85578557u, 0x855f8554u, 0x857f8550u, 0x85ff8540u, 0x87ff8500u, 0x8fff0400u, 0x00000000u, 0x84000000u}, + {0x857f857fu, 0x857f857fu, 0x857f857fu, 0x85ff8540u, 0x87ff8500u, 0x8fff0400u, 0x00000000u, 0x84000000u}}, + {"lg", 0x7c2a0b04u, 0x7c0a0b04u, + {0x8ffc8ffcu, 0x8ff78ffbu, 0x8fdf8fefu, 0x8f7f8fbfu, 0x8dff8effu, 0x87ff8bffu, 0x00000000u, 0x0fff0000u}, + {0x8fc08fc0u, 0x8fc08fc0u, 0x8fc08fc0u, 0x8f7f8fbfu, 0x8dff8effu, 0x87ff8bffu, 0x00000000u, 0x0fff0000u}}, + {"lt", 0x7c220b04u, 0x7c020b04u, + {0x85548554u, 0x85578550u, 0x855f8540u, 0x857f8500u, 0x85ff8400u, 0x87ff0000u, 0x00000000u, 0x04000000u}, + {0x85408540u, 0x85408540u, 0x85408540u, 0x857f8500u, 0x85ff8400u, 0x87ff0000u, 0x00000000u, 0x04000000u}}, + {"neq", 0x7c3a0b04u, 0x7c1a0b04u, + {0xfffcfffcu, 0xfff7fffbu, 0xffdfffefu, 0xff7fffbfu, 0xfdfffeffu, 0xf7fffbffu, 0xffffffffu, 0x7fffffffu}, + {0xffc0ffc0u, 0xffc0ffc0u, 0xffc0ffc0u, 0xff7fffbfu, 0xfdfffeffu, 0xf7fffbffu, 0xffffffffu, 0x7fffffffu}}, + {"nge", 0x7c320b04u, 0x7c120b04u, + {0xf554f554u, 0xf557f550u, 0xf55ff540u, 0xf57ff500u, 0xf5fff400u, 0xf7ff7000u, 0xffffffffu, 0x7400ffffu}, + {0xf540f540u, 0xf540f540u, 0xf540f540u, 0xf57ff500u, 0xf5fff400u, 0xf7ff7000u, 0xffffffffu, 0x7400ffffu}}, + {"ngt", 0x7c360b04u, 0x7c160b04u, + {0xf557f557u, 0xf55ff554u, 0xf57ff550u, 0xf5fff540u, 0xf7fff500u, 0xffff7400u, 0xffffffffu, 0xf400ffffu}, + {0xf57ff57fu, 0xf57ff57fu, 0xf57ff57fu, 0xf5fff540u, 0xf7fff500u, 0xffff7400u, 0xffffffffu, 0xf400ffffu}}, + {"nle", 0x7c380b04u, 0x7c180b04u, + {0x7aa87aa8u, 0x7aa07aabu, 0x7a807aafu, 0x7a007abfu, 0x78007affu, 0x7000fbffu, 0xffffffffu, 0x7bffffffu}, + {0x7a807a80u, 0x7a807a80u, 0x7a807a80u, 0x7a007abfu, 0x78007affu, 0x7000fbffu, 0xffffffffu, 0x7bffffffu}}, + {"nlg", 0x7c340b04u, 0x7c140b04u, + {0x70037003u, 0x70087004u, 0x70207010u, 0x70807040u, 0x72007100u, 0x78007400u, 0xffffffffu, 0xf000ffffu}, + {0x703f703fu, 0x703f703fu, 0x703f703fu, 0x70807040u, 0x72007100u, 0x78007400u, 0xffffffffu, 0xf000ffffu}}, + {"nlt", 0x7c3c0b04u, 0x7c1c0b04u, + {0x7aab7aabu, 0x7aa87aafu, 0x7aa07abfu, 0x7a807affu, 0x7a007bffu, 0x7800ffffu, 0xffffffffu, 0xfbffffffu}, + {0x7abf7abfu, 0x7abf7abfu, 0x7abf7abfu, 0x7a807affu, 0x7a007bffu, 0x7800ffffu, 0xffffffffu, 0xfbffffffu}}, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode mode, std::uint32_t compare, std::uint32_t capture, std::uint32_t group) { + Code[6] = compare; + Code[7] = capture; + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + const std::uint32_t pair = group * Threads + lane; + Input[lane * Inputs] = Values[pair / ValueCount]; + Input[lane * Inputs + 1u] = Values[pair % ValueCount]; + Input[lane * Inputs + 2u] = 0u; + Input[lane * Inputs + 3u] = 0u; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + std::uint32_t mask = 0u; + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Require(Output[lane] <= 1u, "v_cmp f32 denormals: lane " + std::to_string(lane) + " stored " + Hex(Output[lane])); + mask |= Output[lane] << lane; + } + return mask; +} + +void Check(AgcDriver::VulkanDevice& device, const char* modeName, ShaderRecompiler::ShaderFloatMode mode, const Row& row, bool exec, const std::uint32_t* expected) { + const std::string name = std::string(exec ? "v_cmpx_" : "v_cmp_") + row.condition + "_f32"; + for (std::uint32_t group = 0; group < Groups; ++group) { + const std::uint32_t actual = Run(device, mode, exec ? row.cmpx : row.cmp, exec ? CaptureExec : CaptureVcc, group); + if (actual == expected[group]) continue; + const std::uint32_t diff = actual ^ expected[group]; + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + if ((diff >> lane & 1u) == 0u) continue; + const std::uint32_t pair = group * Threads + lane; + Require(false, + name + " " + modeName + " of " + Hex(Values[pair / ValueCount]) + " and " + + Hex(Values[pair % ValueCount]) + " is " + std::to_string(actual >> lane & 1u) + ", expected " + + std::to_string(expected[group] >> lane & 1u)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const Row& row : Rows) { + for (const bool exec : {true, false}) { + Check(*device, "denormals kept, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, row, exec, row.kept); + Check(*device, "denormals kept, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, false, false, false}, row, exec, row.kept); + Check(*device, "denormals flushed, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}, row, exec, row.flushed); + Check(*device, "denormals flushed, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xc0u, false, false, false}, row, exec, row.flushed); + } + } + std::puts("v_cmp and v_cmpx f32 denormals tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp new file mode 100644 index 000000000..d7a66c2b4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp @@ -0,0 +1,136 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 16; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 4; +constexpr std::uint32_t RowCount = 16; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) std::array Code{ + 0x34020084u, 0x34060084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, + 0x7e1402ffu, 0x5a5a5a5au, 0xd5d3000au, 0x00000104u, + 0xe0781000u, 0x80010a03u, 0xbf810000u, +}; + +struct Variant { + const char* modifiers; + std::uint32_t clamp; + std::uint32_t omod; +}; + +constexpr Variant Variants[] = { + {"mul:2", 0, 1}, + {"mul:4", 0, 2}, + {"div:2", 0, 3}, + {"clamp", 1, 0}, + {"clamp mul:2", 1, 1}, +}; + +constexpr std::uint32_t Rows[RowCount] = { + 0x00003c00u, + 0x0000bc00u, + 0x0000c100u, + 0x00004200u, + 0x00006400u, + 0x00005640u, + 0x0000d640u, + 0x00007bffu, + 0x0000fbffu, + 0x00007e00u, + 0xbeef3c00u, + 0xbeefc100u, + 0x00007c00u, + 0x0000fc00u, + 0x00007c01u, + 0x00003e00u, +}; + +constexpr std::uint32_t Expected[RowCount] = { + 0x5a5a0001u, 0x5a5affffu, 0x5a5afffeu, 0x5a5a0003u, + 0x5a5a0400u, 0x5a5a0064u, 0x5a5aff9cu, 0x5a5a7fffu, + 0x5a5a8000u, 0x5a5a0000u, 0x5a5a0001u, 0x5a5afffeu, + 0x5a5a7fffu, 0x5a5a8000u, 0x5a5a0000u, 0x5a5a0001u, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, const ShaderRecompiler::ShaderFloatMode& floatMode, const Variant& variant) { + Code[7] = 0xd5d3000au | variant.clamp << 15u; + Code[8] = 0x00000104u | variant.omod << 27u; + Input.fill(0u); + for (std::uint32_t tid = 0; tid < RowCount; ++tid) Input[tid * Inputs] = Rows[tid]; + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = floatMode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const char* mode, const Variant& variant) { + for (std::uint32_t tid = 0; tid < RowCount; ++tid) { + const std::uint32_t actual = Output[tid * Results]; + Require(actual == Expected[tid], std::string("v_cvt_i16_f16 ") + variant.modifiers + " " + mode + ": lane " + std::to_string(tid) + + " of " + Hex(Rows[tid]) + " is " + Hex(actual) + ", expected " + Hex(Expected[tid])); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const Variant& variant : Variants) { + Run(*device, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, variant); + Check("M1", variant); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, variant); + Check("M2", variant); + } + std::puts("v_cvt_i16_f16 output modifiers tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp index 4baa2d05b..14df999eb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexFetch.cpp @@ -53,8 +53,9 @@ void Run(AgcDriver::VulkanDevice& device) { std::uint32_t format; bool moved; std::uint32_t offset; + bool specializedPixel = false; }; - constexpr std::array cases{{{16u, 77u, false, 1u}, {32u, 77u, true, 0u}, {8u, 71u, false, 1u}, {16u, 71u, true, 0u}}}; + constexpr std::array cases{{{16u, 77u, false, 1u}, {32u, 77u, true, 0u}, {8u, 71u, false, 1u}, {16u, 71u, true, 0u}, {16u, 77u, false, 1u, true}}}; for (const auto& value : cases) { auto& storage = value.moved ? movedVertices : vertices; storage.fill(std::byte{0}); @@ -85,9 +86,11 @@ void Run(AgcDriver::VulkanDevice& device) { pixelInfo.wave32 = true; pixelInfo.targetOutputMode[0] = 9u; pixelInfo.targetExportMapping.fill(0xe4u); - ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), {0u, 0u, pushBytes, 128u - pushBytes}}; + ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), PixelPushLayout(pushBytes, device.Target())}; + if (value.specializedPixel) pixelRequest.target.registrationWordLimit = 1u; const auto pixel = ShaderRecompiler::Recompile(pixelRequest); - const std::array shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, pushBytes}}}; + if (device.Target().registrationModules) Require(vertex.registrationModule && pixel.registrationModule != value.specializedPixel, "registration modules: the word limit did not choose the module mode"); + const std::array shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, PixelPushOffset(pushBytes, device.Target())}}}; AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; state.color = {reinterpret_cast(pixels.data()), {Extent, Extent}, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp new file mode 100644 index 000000000..db8010acb --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp @@ -0,0 +1,137 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Extent = 64u; +alignas(256) std::array pixels{}; +alignas(256) std::array vertices{}; +constexpr std::array vertexCode{0xf4080100u, 0xfa000000u, 0x4a0a0a02u, 0x4a0a0b08u, 0x4a0a0a03u, 0xe00c2000u, 0x80010005u, 0xbf8c3f70u, 0xf80008cfu, 0x03020100u, 0xbf810000u}; +constexpr std::array pixelCode{0x7e0002f2u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u}; + +using Position = std::array; + +struct Counts { + unsigned total = 0; + unsigned upper = 0; + std::array, 16> map{}; +}; + +Counts RunCase(AgcDriver::VulkanDevice& device, const char* name, std::uint32_t records, const std::vector& positions, std::size_t stored, VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) { + vertices.fill(std::byte{0}); + constexpr std::uint32_t stride = 16u; + for (std::size_t index = 0; index < stored; ++index) std::memcpy(vertices.data() + index * stride, positions[index].data(), 16u); + std::array descriptor{}; + const auto tableAddress = reinterpret_cast(descriptor.data()); + std::array userData{static_cast(tableAddress), static_cast(tableAddress >> 32u), 0u, 0u}; + const std::array memory{{{tableAddress, std::as_bytes(std::span(descriptor))}}}; + ShaderRecompiler::ShaderVertexStageInfo vertexInfo{}; + vertexInfo.fetchEmbedded = true; + vertexInfo.resourcesNum = 1u; + vertexInfo.resourcesDst[0] = {0, 4, 0, 0}; + ShaderRecompiler::RecompileRequest request{{ShaderStage::Vertex, reinterpret_cast(vertexCode.data()), vertexCode, 0, {}}, {32u, 0u, userData, std::nullopt, std::nullopt, vertexInfo, memory}, device.Target(), {0u, 0u, 0u, 96u}}; + const auto address = reinterpret_cast(vertices.data()); + descriptor = {static_cast(address), static_cast(address >> 32u) | (stride << 16u), records, 0x01000facu | (77u << 12u)}; + request.context.vertex->resources[0].fields = descriptor; + const auto vertex = ShaderRecompiler::Recompile(request); + const auto pushBytes = static_cast(vertex.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixelInfo{}; + pixelInfo.wave32 = true; + pixelInfo.targetOutputMode[0] = 9u; + pixelInfo.targetExportMapping.fill(0xe4u); + ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), PixelPushLayout(pushBytes, device.Target())}; + const auto pixel = ShaderRecompiler::Recompile(pixelRequest); + const std::array shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, PixelPushOffset(pushBytes, device.Target())}}}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(pixels.data()), {Extent, Extent}, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Extent, Extent}; + state.topology = topology; + state.viewport = {0.0f, static_cast(Extent), static_cast(Extent), -static_cast(Extent), 0.0f, 1.0f}; + state.scissor = {{0, 0}, {Extent, Extent}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15u; + state.blends = {state.blend}; + AgcDriver::Pm4::DrawParameters draw{0u, static_cast(positions.size()), 0u, 1u, 0u, false}; + draw.firstInstance = 0u; + pixels.fill(std::byte{0}); + device.Draw(state, draw, shaders); + device.WaitIdle(); + Counts counts; + for (std::uint32_t y = 0; y < Extent; ++y) { + for (std::uint32_t x = 0; x < Extent; ++x) { + const auto index = (y * Extent + x) * 4u; + if (pixels[index] == std::byte{0} && pixels[index + 3u] == std::byte{0}) continue; + ++counts.total; + const float ndcX = (static_cast(x) + 0.5f) / Extent * 2.0f - 1.0f; + const float ndcY = 1.0f - (static_cast(y) + 0.5f) / Extent * 2.0f; + if (ndcX + ndcY > -1.0f) ++counts.upper; + ++counts.map[y / 4u][x / 4u]; + } + } + std::cout << "case " << name << ": records=" << records << " total=" << counts.total << " outsideFirstTriangle(x+y>-1)=" << counts.upper << "\n"; + for (const auto& row : counts.map) { + std::string line; + for (const auto count : row) line += count == 0u ? '.' : count == 16u ? '#' : '+'; + std::cout << " " << line << "\n"; + } + return counts; +} + +void Run(AgcDriver::VulkanDevice& device) { + std::cout << "device: " << device.DeviceName() << "\n"; + const Position v0{-1.0f, -1.0f, 0.0f, 1.0f}; + const Position v1{0.0f, -1.0f, 0.0f, 1.0f}; + const Position v2{-1.0f, 0.0f, 0.0f, 1.0f}; + const auto make = [&](const Position& last) { return std::vector{v0, v1, v2, last}; }; + const auto a = RunCase(device, "A out-of-range vertex 3", 3u, make({0.0f, 0.0f, 0.0f, 0.0f}), 3u); + const auto b = RunCase(device, "B in-range (0,0,0,0)", 4u, make({0.0f, 0.0f, 0.0f, 0.0f}), 4u); + const auto c = RunCase(device, "C repeat of v1", 4u, make(v1), 4u); + const auto d = RunCase(device, "D (0,0,0,1) at the centre", 4u, make({0.0f, 0.0f, 0.0f, 1.0f}), 4u); + const auto e = RunCase(device, "E (1,1,0,1) real triangle", 4u, make({1.0f, 1.0f, 0.0f, 1.0f}), 4u); + const Position zero{0.0f, 0.0f, 0.0f, 0.0f}; + const auto f = RunCase(device, "F negative zeros", 4u, make({-0.0f, 0.0f, -0.0f, -0.0f}), 4u); + const auto g = RunCase(device, "G triangle list, second triangle has (0,0,0,0)", 6u, {v0, v1, v2, v1, v2, zero}, 6u, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST); + const auto h = RunCase(device, "H strip with two (0,0,0,0) vertices", 5u, {v0, v1, v2, zero, zero}, 5u); + const auto p = RunCase(device, "P point list, (-0.5,-0.5,0,1) and (0,0,0,0)", 2u, {{-0.5f, -0.5f, 0.0f, 1.0f}, zero}, 2u, VK_PRIMITIVE_TOPOLOGY_POINT_LIST); + Require(e.upper > 0u, "probe cannot see the second triangle"); + Require(c.total > 0u && c.upper == 0u, "the first triangle is not drawn alone"); + Require(a.upper == 0u && a.total == c.total, "a vertex fetched past num_records must clip the primitive away"); + Require(b.upper == 0u && b.total == c.total, "a stored (0,0,0,0) vertex must clip the primitive away"); + Require(f.upper == 0u && f.total == c.total, "a stored (-0,0,-0,-0) vertex must clip the primitive away"); + Require(g.upper == 0u && g.total == c.total, "a triangle list with a (0,0,0,0) vertex must keep only the first triangle"); + Require(h.upper == 0u && h.total == c.total, "a strip with two (0,0,0,0) vertices must keep only the first triangle"); + Require(d.upper == 496u, "a (0,0,0,1) vertex must not be clipped away"); + Require(p.total == 1u, "a point at (0,0,0,0) must not be drawn"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp index 50e546f07..a308a4063 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp @@ -37,11 +37,14 @@ struct Case { std::uint32_t records; bool inRange; const char* what; + std::uint32_t selectors = 0xfacu; }; +std::vector registrationModule; + std::array VertexBufferDescriptor(const Case& value) { const auto address = reinterpret_cast(Element.data()); - return {static_cast(address), static_cast((address >> 32u) & 0xffffu), value.records, 0x0104dfacu | (value.select << 28u)}; + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), value.records, (0x0104dfacu & ~0xfffu) | value.selectors | (value.select << 28u)}; } std::array OutputDescriptor() { @@ -74,6 +77,10 @@ void Draw(AgcDriver::VulkanDevice& device, const Case& value) { vertexRequest.useCache = false; const auto vertexResult = ShaderRecompiler::Recompile(vertexRequest); Require(vertexResult.vertexAttributes.size() == 1u, std::string(value.what) + ": the fetch did not become a vertex attribute"); + if (device.Target().registrationModules) { + if (registrationModule.empty()) registrationModule = vertexResult.spirv.Words(); + Require(vertexResult.spirv.Words() == registrationModule, std::string(value.what) + ": registration modules: the V# specialized the vertex module"); + } const auto vertexPush = static_cast(vertexResult.pushConstants.size()); ShaderRecompiler::ShaderPixelStageInfo pixel{}; @@ -120,7 +127,9 @@ void Draw(AgcDriver::VulkanDevice& device, const Case& value) { void Check(const Case& value) { for (std::uint32_t vertex = 0; vertex < Vertices; ++vertex) { for (std::uint32_t component = 0; component < 4u; ++component) { - const auto expected = value.inRange ? std::bit_cast(Element[component]) : 0u; + const auto selector = (value.selectors >> (component * 3u)) & 7u; + const auto selected = selector >= 4u ? std::bit_cast(Element[selector - 4u]) : selector == 1u ? std::bit_cast(1.0f) : 0u; + const auto expected = value.inRange || selector < 4u ? selected : 0u; const auto actual = Output[vertex * 4u + component]; Require(actual == expected, std::string(value.what) + ": vertex " + std::to_string(vertex) + " v" + std::to_string(8u + component) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected)); } @@ -133,12 +142,14 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - constexpr std::array cases{{ + constexpr std::array cases{{ {2u, 1u, true, "OOB_SELECT 2, one record"}, {2u, 0u, false, "OOB_SELECT 2, no records"}, {3u, 16u, true, "OOB_SELECT 3, whole element in range"}, {3u, 0u, false, "OOB_SELECT 3, no records"}, {0u, 16u, true, "OOB_SELECT 0, whole element in range"}, + {3u, 16u, true, "OOB_SELECT 3, selectors W Z 1 0", 7u | (6u << 3u) | (1u << 6u) | (0u << 9u)}, + {3u, 0u, false, "OOB_SELECT 3, no records, selectors W Z 1 0", 7u | (6u << 3u) | (1u << 6u) | (0u << 9u)}, }}; for (const auto& value : cases) { Draw(*device, value); diff --git a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp index b62ee64a7..524575eef 100644 --- a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp @@ -41,6 +41,7 @@ bool g_disassemble = false; bool g_assembly = false; bool g_memory = false; bool g_spirv = false; +bool g_prepare = false; bool g_graph = false; bool g_maintenance8 = false; bool g_code = false; @@ -113,6 +114,16 @@ bool Replay(const char* path) { std::printf(" resource analysis failed: %s\n", FirstLine(error.what()).c_str()); return false; } + if (g_prepare) { + try { + const auto handle = ShaderRecompiler::PrepareShader(request.request); + std::printf(" prepared: %zu SPIR-V words\n", ShaderRecompiler::GetPreparedArtifact(*handle).spirv.Words().size()); + return true; + } catch (const std::exception& error) { + std::printf(" prepare failed: %s\n", FirstLine(error.what()).c_str()); + return false; + } + } try { if (g_maintenance8) request.request.target.nonConstantImageOffsets = true; const auto result = ShaderRecompiler::Recompile(request.request); @@ -152,7 +163,7 @@ bool Replay(const char* path) { int main(int argc, char** argv) { if (argc < 2) { - std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--cfg] [--mem] [--spv] [--code] [--maintenance8] ...\n the driver writes shader_
.req files when APS5_DUMP_SHADERS is set\n"); + std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--cfg] [--mem] [--spv] [--prepare] [--code] [--maintenance8] ...\n the driver writes shader_
.req files when APS5_DUMP_SHADERS is set\n"); return 2; } int failures = 0; @@ -181,6 +192,10 @@ int main(int argc, char** argv) { g_code = true; continue; } + if (std::string(argv[i]) == "--prepare") { + g_prepare = true; + continue; + } if (std::string(argv[i]) == "--spv") { g_spirv = true; continue; diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 4d7cd26fa..8dc5f2ad7 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -6,6 +6,7 @@ extern "C" { #include #include +#include } #include #include @@ -134,6 +135,7 @@ struct Instance { std::uint32_t mp3Bitrate = 0; AVCodecContext* opus = nullptr; std::uint32_t opusChannels = 0; + std::uint32_t opusMappingFamily = 0; std::uint32_t opusSampleRate = 0; std::vector opusPending; Resampler resampler; @@ -369,10 +371,13 @@ std::int32_t InitializeInstance(Instance& instance, const std::uint8_t* paramete std::memcpy(&channels, parameters, sizeof(channels)); std::memcpy(&sampleRate, parameters + 4, sizeof(sampleRate)); if (channels == 0 || channels > 8 || sampleRate == 0) return AJM_RESULT_INVALID_PARAMETER; - std::uint32_t third = 0; - if (size >= 12) std::memcpy(&third, parameters + 8, sizeof(third)); - if (sampleRate != 48000 || third != 0) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus sample rate other than 48000 or nonzero third parameter word)"); + std::uint32_t mappingFamily = 0; + if (size >= 12) std::memcpy(&mappingFamily, parameters + 8, sizeof(mappingFamily)); + if (sampleRate != 48000) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus sample rate other than 48000)"); + if (mappingFamily > 1) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus mapping family other than 0 or 1)"); + if (mappingFamily == 0 && channels > 2) return AJM_RESULT_INVALID_PARAMETER; instance.opusChannels = channels; + instance.opusMappingFamily = mappingFamily; instance.opusSampleRate = sampleRate; OpenOpus(instance); instance.opusPending.clear(); @@ -948,6 +953,29 @@ void OpenOpus(Instance& instance) { if (!instance.opus) throw std::runtime_error("AJM: cannot allocate the FFmpeg Opus decoder"); av_channel_layout_default(&instance.opus->ch_layout, static_cast(instance.opusChannels)); instance.opus->sample_rate = 48000; + if (instance.opusMappingFamily == 1) { + static constexpr std::uint8_t streams[8] = {1, 1, 2, 2, 3, 4, 4, 5}; + static constexpr std::uint8_t coupled[8] = {0, 1, 1, 2, 2, 2, 3, 3}; + static constexpr std::uint8_t mapping[8][8] = { + {0}, {0, 1}, {0, 2, 1}, {0, 1, 2, 3}, + {0, 4, 1, 2, 3}, {0, 4, 1, 2, 3, 5}, + {0, 4, 1, 2, 3, 5, 6}, {0, 6, 1, 2, 3, 4, 5, 7} + }; + const std::size_t index = instance.opusChannels - 1; + instance.opus->extradata_size = static_cast(21 + instance.opusChannels); + instance.opus->extradata = static_cast(av_mallocz(instance.opus->extradata_size + AV_INPUT_BUFFER_PADDING_SIZE)); + if (!instance.opus->extradata) throw std::bad_alloc(); + auto* header = instance.opus->extradata; + std::memcpy(header, "OpusHead", 8); + header[8] = 1; + header[9] = static_cast(instance.opusChannels); + header[12] = 0x80; + header[13] = 0xbb; + header[18] = 1; + header[19] = streams[index]; + header[20] = coupled[index]; + std::memcpy(header + 21, mapping[index], instance.opusChannels); + } if (avcodec_open2(instance.opus, codec, nullptr) < 0) throw std::runtime_error("AJM: cannot open the FFmpeg Opus decoder"); } diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index fb835934f..db2eb580c 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -218,16 +218,8 @@ static int RecordWriteAddressFromCounterPair(Apr::CommandBufferObject* buffer, v return AppendCommand(buffer, Apr::Opcode::WriteAddressFromCounterPair, Apr::WriteAddressFromCounterCommand{{}, reinterpret_cast(address), counter, counter + 1u}); } -struct Xtime { - std::int64_t sec; - std::int64_t nsec; -}; - static constexpr int ThrdSuccess = 0; static constexpr int ThrdError = 4; -static constexpr int GuestClockRealtime = 0; -static constexpr std::int64_t NanosPerSecond = 1000000000; -static constexpr std::int64_t LongestSleepSeconds = 86400; static std::atomic_flag sharedPtrSpinLock; @@ -238,22 +230,6 @@ int APS5_VABI _Cnd_init_nid_postfix(void** handle); int SignalCondition_nid_no_patch(void** handle); } -static KernelTimespec XtimeDeadline(const Xtime& time) { - constexpr auto largest = std::numeric_limits::max(); - constexpr auto smallest = std::numeric_limits::min(); - const auto carry = time.nsec / NanosPerSecond; - auto nanos = time.nsec % NanosPerSecond; - auto seconds = time.sec; - if ((carry > 0 && seconds > largest - carry) || (carry < 0 && seconds < smallest - carry)) throw std::overflow_error("_Thrd_sleep: xtime seconds overflow"); - seconds += carry; - if (nanos < 0) { - if (seconds == smallest) throw std::overflow_error("_Thrd_sleep: xtime seconds overflow"); - nanos += NanosPerSecond; - --seconds; - } - return {seconds, nanos}; -} - extern "C" { int APS5_VABI sceAmprCommandBufferWriteAddressOnCompletion(Apr::CommandBufferObject* buffer, volatile std::uint64_t* address, std::uint64_t value) { @@ -822,6 +798,11 @@ int APS5_VABI sceAmprAmmGetVirtualAddressRanges(std::uint64_t* start, std::uint6 return 0; } +int APS5_VABI sceAmprAmmSetPageTablePoolOccupancyNotificationThreshold(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceAmprAmmSubmitCommandBuffer(void* base, std::uint32_t offset, std::uint32_t priority) { if (priority > 2u) return SCE_KERNEL_ERROR_EINVAL; if (!base) return SCE_KERNEL_ERROR_EPERM; @@ -943,22 +924,6 @@ void APS5_VABI _Lock_shared_ptr_spin_lock_nid_postfix(void) { while (sharedPtrSpinLock.test_and_set(std::memory_order_acquire)) std::this_thread::yield(); } -int APS5_VABI _Thrd_sleep_nid_postfix(const Xtime* target, Xtime*) { - if (!target) APS5_INVALID_ARG_EX; - const auto deadline = XtimeDeadline(*target); - for (;;) { - KernelTimespec now{}; - if (clock_gettime_nid_postfix(GuestClockRealtime, &now) != 0) throw std::runtime_error("_Thrd_sleep: reading the realtime clock failed"); - if (now.tv_sec > deadline.tv_sec || (now.tv_sec == deadline.tv_sec && now.tv_nsec >= deadline.tv_nsec)) return 0; - KernelTimespec remaining{deadline.tv_sec - now.tv_sec, deadline.tv_nsec - now.tv_nsec}; - if (remaining.tv_nsec < 0) { - remaining.tv_nsec += NanosPerSecond; - --remaining.tv_sec; - } - remaining.tv_sec = std::min(remaining.tv_sec, LongestSleepSeconds); - if (nanosleep_nid_postfix(&remaining, nullptr) != 0) throw std::runtime_error("_Thrd_sleep: nanosleep failed"); - } -} int APS5_VABI wcstombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit) { std::array state{}; @@ -1029,4 +994,9 @@ APS5_EXPORT("qlWiRfOJx1A", sceAmprFpclassifyd); int APS5_VABI sceAmprFpclassifyd(double value) { return SonyFpclassifyd(value); } + +int APS5_VABI sceAmprAmmGetUsageStatsData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index 968a17129..bc11e8e5b 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -57,6 +57,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(audio_out_sys_hdmi_monitor_info_tests tests/AudioOutSysHdmiMonitorInfo.cpp) + target_include_directories(audio_out_sys_hdmi_monitor_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out_sys_hdmi_monitor_info_tests PRIVATE libSceAudioOut libc) + configure_windows_unwind(audio_out_sys_hdmi_monitor_info_tests) + add_test(NAME audio_out_sys_hdmi_monitor_info COMMAND audio_out_sys_hdmi_monitor_info_tests) + if(WIN32) + set_tests_properties(audio_out_sys_hdmi_monitor_info PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(audio_out2_latency_tests tests/AudioOut2Latency.cpp) target_include_directories(audio_out2_latency_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(audio_out2_latency_tests PRIVATE libSceAudioOut libc) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index ddddb77fb..c8adb77b2 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -32,6 +33,8 @@ static constexpr std::uint32_t FORMAT_MASK = 0xFFu; static constexpr std::uint64_t TARGET_LATENCY_US = 40000; static constexpr std::uint64_t DRAIN_TIMEOUT_US = 200000; static constexpr std::uint64_t DRAIN_SLEEP_US = 1000; +static constexpr int MONITOR_TYPE_HDMI = 1; +static constexpr std::uint32_t HDMI_MONITOR_INFO_SIZE = 0x180; enum class Format { Unknown, @@ -506,8 +509,17 @@ int APS5_VABI sceAudioOutSysConfigureOutput() { return 0; } -int APS5_VABI sceAudioOutSysGetHdmiMonitorInfo() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAudioOutSysGetHdmiMonitorInfo(int type, void* info, std::uint32_t size) { + if (type != MONITOR_TYPE_HDMI) { + throw std::runtime_error("sceAudioOutSysGetHdmiMonitorInfo: monitor type " + std::to_string(type) + " not supported"); + } + if (size != HDMI_MONITOR_INFO_SIZE) { + throw std::runtime_error("sceAudioOutSysGetHdmiMonitorInfo: size " + std::to_string(size) + " not supported"); + } + if (info == nullptr) { + return -2144993276; + } + std::memset(info, 0, HDMI_MONITOR_INFO_SIZE); return 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutSysHdmiMonitorInfo.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutSysHdmiMonitorInfo.cpp new file mode 100644 index 000000000..ce23a664d --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutSysHdmiMonitorInfo.cpp @@ -0,0 +1,71 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOutSysGetHdmiMonitorInfo(int, void*, std::uint32_t); +} + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +namespace { + +constexpr int monitorTypeHdmi = 1; +constexpr std::uint32_t infoSize = 0x180; +constexpr std::size_t descriptorCountOffset = 0x94; +constexpr std::size_t guardSize = 16; +constexpr std::uint8_t untouched = 0xA5; +constexpr int invalidPointer = static_cast(0x80260004); + +bool Throws(int type, std::uint32_t size, const std::string& expected) { + std::array info{}; + try { + sceAudioOutSysGetHdmiMonitorInfo(type, info.data(), size); + } catch (const std::runtime_error& error) { + if (std::string(error.what()) == expected) return true; + std::fprintf(stderr, "threw: %s\n", error.what()); + return false; + } + return false; +} + +} + +int main() { + std::array buffer{}; + buffer.fill(untouched); + Require(sceAudioOutSysGetHdmiMonitorInfo(monitorTypeHdmi, buffer.data(), infoSize) == 0, + "an HDMI query with a 0x180-byte buffer must succeed"); + for (std::uint32_t i = 0; i < infoSize; i++) { + Require(buffer[i] == 0, "the monitor info must describe a sink without a name or audio descriptors"); + } + std::uint32_t descriptors = 0xFFFFFFFFu; + std::memcpy(&descriptors, buffer.data() + descriptorCountOffset, sizeof(descriptors)); + Require(descriptors == 0, "the audio descriptor count must be 0"); + for (std::size_t i = infoSize; i < buffer.size(); i++) { + Require(buffer[i] == untouched, "nothing past 0x180 bytes may be written"); + } + + Require(sceAudioOutSysGetHdmiMonitorInfo(monitorTypeHdmi, nullptr, infoSize) == invalidPointer, + "a null buffer must report an invalid pointer"); + + Require(Throws(monitorTypeHdmi, infoSize - 1, + "sceAudioOutSysGetHdmiMonitorInfo: size 383 not supported"), "a short buffer must throw"); + Require(Throws(monitorTypeHdmi, infoSize + 1, + "sceAudioOutSysGetHdmiMonitorInfo: size 385 not supported"), "a long buffer must throw"); + Require(Throws(0x10, infoSize, + "sceAudioOutSysGetHdmiMonitorInfo: monitor type 16 not supported"), "monitor type 0x10 must throw"); + Require(Throws(0, infoSize, + "sceAudioOutSysGetHdmiMonitorInfo: monitor type 0 not supported"), "monitor type 0 must throw"); + return 0; +} diff --git a/core/libs/prx/libSceAvPlayer/CMakeLists.txt b/core/libs/prx/libSceAvPlayer/CMakeLists.txt index e2e599427..bdc296e7e 100644 --- a/core/libs/prx/libSceAvPlayer/CMakeLists.txt +++ b/core/libs/prx/libSceAvPlayer/CMakeLists.txt @@ -2,6 +2,9 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/AvPlayerLibrary.cmake) add_sce_avplayer_library(libSceAvPlayer) if(BUILD_TESTING) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() add_test_executable(guest_avplayer_tests ${LIBS_INCLUDE_DIR}/tests/GuestAvPlayer.cpp) target_include_directories(guest_avplayer_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_avplayer_tests PRIVATE libSceAvPlayer libc) diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index a2649662d..489bc94b3 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -13,18 +13,45 @@ constexpr int CES_ERROR_UNASSIGNED_CODE = static_cast(0x805C0020); constexpr int CES_ERROR_INVALID_DST_BUFFER = static_cast(0x805C0030); constexpr int CES_ERROR_DST_BUFFER_END = static_cast(0x805C0031); -constexpr uint8_t CP1252_PROFILE = 0; +constexpr uint8_t PROFILES[2] = {0, 1}; +constexpr const uint8_t* CP1252_PROFILE = &PROFILES[0]; +constexpr const uint8_t* CP1251_PROFILE = &PROFILES[1]; constexpr uint16_t CP1252_HIGH[32] = { 0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x008D, 0x017D, 0x008F, 0x0090, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x009D, 0x017E, 0x0178, }; -uint32_t cp1252ToUnicode(uint8_t sbc) { +constexpr uint16_t CP1251_HIGH[128] = { + 0x0402, 0x0403, 0x201A, 0x0453, 0x201E, 0x2026, 0x2020, 0x2021, 0x20AC, 0x2030, 0x0409, 0x2039, 0x040A, 0x040C, 0x040B, 0x040F, + 0x0452, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x0098, 0x2122, 0x0459, 0x203A, 0x045A, 0x045C, 0x045B, 0x045F, + 0x00A0, 0x040E, 0x045E, 0x0408, 0x00A4, 0x0490, 0x00A6, 0x00A7, 0x0401, 0x00A9, 0x0404, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x0407, + 0x00B0, 0x00B1, 0x0406, 0x0456, 0x0491, 0x00B5, 0x00B6, 0x00B7, 0x0451, 0x2116, 0x0454, 0x00BB, 0x0458, 0x0405, 0x0455, 0x0457, + 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F, + 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F, + 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F, + 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, 0x0448, 0x0449, 0x044A, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F, +}; + +uint32_t sbcToUnicode(const uint8_t* profile, uint8_t sbc) { + if (profile == CP1251_PROFILE) return sbc >= 0x80 ? CP1251_HIGH[sbc - 0x80] : sbc; return sbc >= 0x80 && sbc < 0xA0 ? CP1252_HIGH[sbc - 0x80] : sbc; } -bool unicodeToCp1252(uint32_t code, uint8_t& sbc) { +bool unicodeToSbc(const uint8_t* profile, uint32_t code, uint8_t& sbc) { + if (profile == CP1251_PROFILE) { + if (code < 0x80) { + sbc = static_cast(code); + return true; + } + for (uint32_t i = 0; i < 128; ++i) { + if (CP1251_HIGH[i] == code) { + sbc = static_cast(0x80 + i); + return true; + } + } + return false; + } if (code < 0x80 || (code >= 0xA0 && code <= 0xFF)) { sbc = static_cast(code); return true; @@ -81,15 +108,25 @@ uint32_t encodeUtf8(uint32_t code, uint8_t* utf8) { extern "C" { const uint8_t* APS5_VABI sceCesRefersUcsProfileCp1252(void) { - return &CP1252_PROFILE; + return CP1252_PROFILE; +} + +const uint8_t* APS5_VABI sceCesRefersUcsProfileCp1251(void) { + return CP1251_PROFILE; +} + +int APS5_VABI sceCesMbcsUcsContextInit(void* context, const void* profile) { + (void)profile; + if (!context) return CES_ERROR_INVALID_PARAMETER; + return 0; } int APS5_VABI sceCesSbcToUtf8(const uint8_t* profile, uint8_t sbc, uint8_t* utf8, uint32_t utf8max, uint32_t* utf8_len) { - if (profile != &CP1252_PROFILE) return CES_ERROR_INVALID_PARAMETER; + if (profile != CP1252_PROFILE && profile != CP1251_PROFILE) return CES_ERROR_INVALID_PARAMETER; if (!utf8) return CES_ERROR_INVALID_DST_BUFFER; uint8_t encoded[3]; - const uint32_t length = encodeUtf8(cp1252ToUnicode(sbc), encoded); + const uint32_t length = encodeUtf8(sbcToUnicode(profile, sbc), encoded); if (utf8max < length) return CES_ERROR_DST_BUFFER_END; for (uint32_t i = 0; i < length; ++i) utf8[i] = encoded[i]; if (utf8_len) *utf8_len = length; @@ -97,7 +134,7 @@ int APS5_VABI sceCesSbcToUtf8(const uint8_t* profile, uint8_t sbc, uint8_t* utf8 } int APS5_VABI sceCesUtf8ToSbc(const uint8_t* utf8, uint32_t utf8max, uint32_t* utf8_len, const uint8_t* profile, uint8_t* sbc) { - if (profile != &CP1252_PROFILE) return CES_ERROR_INVALID_PARAMETER; + if (profile != CP1252_PROFILE && profile != CP1251_PROFILE) return CES_ERROR_INVALID_PARAMETER; if (!utf8) return CES_ERROR_INVALID_SRC_BUFFER; if (utf8max == 0) return CES_ERROR_SRC_BUFFER_END; if (!sbc) return CES_ERROR_INVALID_DST_BUFFER; @@ -105,7 +142,7 @@ int APS5_VABI sceCesUtf8ToSbc(const uint8_t* utf8, uint32_t utf8max, uint32_t* u uint32_t length = 0; uint32_t code = 0; if (const int result = decodeUtf8(utf8, utf8max, length, code); result != 0) return result; - if (!unicodeToCp1252(code, *sbc)) return CES_ERROR_UNASSIGNED_CODE; + if (!unicodeToSbc(profile, code, *sbc)) return CES_ERROR_UNASSIGNED_CODE; if (utf8_len) *utf8_len = length; return 0; } diff --git a/core/libs/prx/libSceContentExport/CMakeLists.txt b/core/libs/prx/libSceContentExport/CMakeLists.txt index 7efbba37e..59e62f64b 100644 --- a/core/libs/prx/libSceContentExport/CMakeLists.txt +++ b/core/libs/prx/libSceContentExport/CMakeLists.txt @@ -15,7 +15,7 @@ if(BUILD_TESTING) add_test_executable(guest_content_unimplemented_tests ${LIBS_INCLUDE_DIR}/tests/GuestContentUnimplemented.cpp) target_include_directories(guest_content_unimplemented_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_content_unimplemented_tests PRIVATE libSceContentExport libSceContentSearch libSceContentDelete libc) + target_link_libraries(guest_content_unimplemented_tests PRIVATE libSceContentSearch libSceContentDelete libc) configure_windows_unwind(guest_content_unimplemented_tests) add_test(NAME guest_content_unimplemented COMMAND guest_content_unimplemented_tests) if(WIN32) diff --git a/core/libs/prx/libSceContentExport/Export.cpp b/core/libs/prx/libSceContentExport/Export.cpp index a626f6a42..f618daeea 100644 --- a/core/libs/prx/libSceContentExport/Export.cpp +++ b/core/libs/prx/libSceContentExport/Export.cpp @@ -3,11 +3,23 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include +#include +#include +#include #include #include namespace { +constexpr std::size_t MaxExecution = 10; std::atomic g_initialized{false}; +std::mutex g_sessionMutex; +std::set g_sessions; +std::int32_t g_lastSessionId = 0; + +void RequireSession(const char* function, std::int32_t expId) { + if (!g_initialized.load()) throw std::logic_error(std::string(function) + ": not initialized"); + if (!g_sessions.contains(expId)) throw std::invalid_argument(std::string(function) + ": unknown export session"); +} } extern "C" { @@ -21,9 +33,11 @@ int APS5_VABI sceContentExportInit2(const ContentExportInitParam2* init_param) { return 0; } -int APS5_VABI sceContentExportFinish(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentExportFinish(std::int32_t expId) { + std::lock_guard lock(g_sessionMutex); + RequireSession(__func__, expId); + g_sessions.erase(expId); + return 0; } int APS5_VABI sceContentExportFromData(void) { @@ -42,26 +56,36 @@ int APS5_VABI sceContentExportFromFileWithThumbnail(void) { } int APS5_VABI sceContentExportStart(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_sessionMutex); + if (!g_initialized.load()) throw std::logic_error(std::string(__func__) + ": not initialized"); + if (g_sessions.size() >= MaxExecution) throw std::runtime_error(std::string(__func__) + ": behaviour beyond 10 live export sessions is unknown"); + if (g_lastSessionId == std::numeric_limits::max()) throw std::runtime_error(std::string(__func__) + ": export session ids exhausted"); + g_sessions.insert(++g_lastSessionId); + return g_lastSessionId; } -int APS5_VABI sceContentExportGetProgress(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentExportGetProgress(std::int32_t expId) { + std::lock_guard lock(g_sessionMutex); + RequireSession(__func__, expId); + throw std::runtime_error(std::string(__func__) + ": progress of a session with no running export is unknown"); } -int APS5_VABI sceContentExportCancel(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentExportCancel(std::int32_t expId) { + std::lock_guard lock(g_sessionMutex); + RequireSession(__func__, expId); + throw std::runtime_error(std::string(__func__) + ": cancelling a session with no running export is unknown"); } -int APS5_VABI sceContentExportFromDataWithThumbnail(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const ContentExportParam* param, const ContentExportCallbackParam* contentCallbackParam, + const char* thumbnailType, const ContentExportCallbackParam* thumbnailCallbackParam, char* path, std::size_t pathBufferLength) { + std::lock_guard lock(g_sessionMutex); + RequireSession(__func__, expId); + throw std::runtime_error(std::string(__func__) + ": the host has no media library to export into"); } int APS5_VABI sceContentExportTerm(void) { + std::lock_guard lock(g_sessionMutex); + if (!g_sessions.empty()) throw std::runtime_error(std::string(__func__) + ": terminating with live export sessions is unknown"); bool expected = true; if (!g_initialized.compare_exchange_strong(expected, false)) throw std::logic_error(std::string(__func__) + ": not initialized"); return 0; diff --git a/core/libs/prx/libSceFont/src/SystemFont.cpp b/core/libs/prx/libSceFont/src/SystemFont.cpp index bf1281380..f164d8646 100644 --- a/core/libs/prx/libSceFont/src/SystemFont.cpp +++ b/core/libs/prx/libSceFont/src/SystemFont.cpp @@ -6,6 +6,8 @@ #include #ifdef _WIN32 #include +#elif defined(__APPLE__) +#include #endif #include "prx/libSceFont/include/FontInternal.hpp" @@ -91,6 +93,15 @@ std::filesystem::path ExecutableDirectory() { const auto length = GetModuleFileNameW(nullptr, module, MAX_PATH); if (length == 0 || length == MAX_PATH) throw std::runtime_error("libSceFont: cannot locate the executable"); return std::filesystem::path(module).parent_path(); +#elif defined(__APPLE__) + std::uint32_t size = 0; + _NSGetExecutablePath(nullptr, &size); + std::vector executable(size); + std::error_code error; + std::filesystem::path path; + if (_NSGetExecutablePath(executable.data(), &size) == 0) path = std::filesystem::canonical(executable.data(), error); + if (path.empty()) throw std::runtime_error("libSceFont: cannot locate the executable"); + return path.parent_path(); #else return std::filesystem::read_symlink("/proc/self/exe").parent_path(); #endif diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index 9cfafc35a..a6ca6f3ad 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -16,6 +16,21 @@ std::int32_t APS5_VABI sceHmd2Initialize(const void* param) { return SCE_HMD2_ERROR_UNSUPPORTED_FEATURE; } +int APS5_VABI sceHmd2GetFieldOfView() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2GetDeviceInformationByHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2Terminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceHmd2Close() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceHttp2/Export.cpp b/core/libs/prx/libSceHttp2/Export.cpp index f037bf7f9..786bf3099 100644 --- a/core/libs/prx/libSceHttp2/Export.cpp +++ b/core/libs/prx/libSceHttp2/Export.cpp @@ -356,6 +356,13 @@ int APS5_VABI sceHttp2CreateCookieBox(int lib_http2_ctx_id) { return id; } +int APS5_VABI sceHttp2DeleteCookieBox(int cookie_box_id) { + std::lock_guard lock(g_handlesMutex); + if (FindHandle(cookie_box_id, Http2Kind::CookieBox) == nullptr) return ERROR_INVALID_ID; + g_handles.erase(cookie_box_id); + return 0; +} + int APS5_VABI sceHttp2SetCookieBox(int id, int cookie_box_id) { (void)id; (void)cookie_box_id; diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 19b25beb8..ea9bc796d 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -527,6 +528,10 @@ const char* APS5_VABI _ZNK3sce4Json6String5c_strEv(const String* self) { return bool APS5_VABI _ZNK3sce4Json6String5emptyEv(const String* self) { return self->text->empty(); } std::size_t APS5_VABI _ZNK3sce4Json6String6lengthEv(const String* self) { return self->text->size(); } bool APS5_VABI _ZNK3sce4Json6StringeqEPKc(const String* self, const char* text) { return text != nullptr && *self->text == text; } +int APS5_VABI _ZNK3sce4Json6String7compareEPKc(const String* self, const char* text) { + if (text == nullptr) throw std::invalid_argument("sce::Json::String::compare: null text"); + return self->text->compare(text); +} void APS5_VABI _ZN3sce4Json5ArrayC1Ev(Array* self) { Construct(*self); } void APS5_VABI _ZN3sce4Json5ArrayC1ERKS1_(Array* self, const Array* other) { CopyArray(*self, *other); } @@ -582,6 +587,11 @@ ObjectIterator* APS5_VABI _ZNK3sce4Json6Object3endEv(ObjectIterator* result, con void APS5_VABI _ZN3sce4Json6Object8iteratorD1Ev(ObjectIterator* self) { (void)self; } ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iteratorppEv(ObjectIterator* self) { ++self->it; return self; } Pair* APS5_VABI _ZNK3sce4Json6Object8iteratordeEv(const ObjectIterator* self) { return &*self->it; } +Pair* APS5_VABI _ZNK3sce4Json6Object8iteratorptEv(const ObjectIterator* self) { return &*self->it; } +ObjectIterator* APS5_VABI _ZNK3sce4Json6Object4findERKNS0_6StringE(ObjectIterator* result, const Object* self, const String* key) { + result->it = std::find_if(self->items->begin(), self->items->end(), [&](const Pair& pair) { return *pair.key.text == *key->text; }); + return result; +} bool APS5_VABI _ZNK3sce4Json6Object8iteratorneERKS2_(const ObjectIterator* self, const ObjectIterator* other) { return self->it != other->it; } bool APS5_VABI _ZNK3sce4Json6Object5emptyEv(const Object* self) { return self->items->empty(); } std::size_t APS5_VABI _ZNK3sce4Json6Object4sizeEv(const Object* self) { return self->items->size(); } @@ -798,30 +808,53 @@ int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value* out, const char* } -int APS5_VABI _ZN3sce4Json17InitParameterRttiC1EPNS0_14AllocParamRttiEPvm(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +struct InitParameterRtti { + void* allocParam; + void* userData; + std::size_t fileBufferSize; +}; + +void APS5_VABI _ZN3sce4Json17InitParameterRttiC1EPNS0_14AllocParamRttiEPvm(InitParameterRtti* self, void* allocParam, void* userData, std::size_t fileBufferSize) { + self->allocParam = allocParam; + self->userData = userData; + self->fileBufferSize = fileBufferSize; } + struct InitParameter2 { void* allocator; void* userData; std::size_t fileBufferSize; + std::int32_t specialFloatFormatType; + std::uint32_t reserved[3]; }; -static_assert(sizeof(InitParameter2) <= 40); +static_assert(sizeof(InitParameter2) == 0x28 && offsetof(InitParameter2, specialFloatFormatType) == 0x18); void APS5_VABI _ZN3sce4Json14InitParameter2C1Ev(InitParameter2* self) { *self = {}; } +void APS5_VABI _ZN3sce4Json14InitParameter2C2Ev(InitParameter2* self) { + _ZN3sce4Json14InitParameter2C1Ev(self); +} + void APS5_VABI _ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv(InitParameter2* self, void* allocator, void* userData) { self->allocator = allocator; self->userData = userData; } +void APS5_VABI _ZN3sce4Json18InitParameterRtti216setAllocatorRttiEPNS0_14AllocParamRttiEPv(InitParameter2* self, void* allocParam, void* userData) { + self->allocator = allocParam; + self->userData = userData; +} + void APS5_VABI _ZN3sce4Json14InitParameter217setFileBufferSizeEm(InitParameter2* self, std::size_t size) { self->fileBufferSize = size; } +void APS5_VABI _ZN3sce4Json14InitParameter225setSpecialFloatFormatTypeENS0_22SpecialFloatFormatTypeE(InitParameter2* self, std::int32_t type) { + self->specialFloatFormatType = type; +} + int APS5_VABI _ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(void* self, const InitParameter2* parameter) { (void)self; if (parameter == nullptr) throw std::invalid_argument("sce::Json::Initializer::initialize: null parameter"); @@ -838,15 +871,6 @@ int APS5_VABI _ZN3sce4Json12MemAllocator11notifyErrorEimPv(void) { return 0; } -void APS5_VABI _ZN3sce4Json14InitParameter2C2Ev(InitParameter2* self) { - _ZN3sce4Json14InitParameter2C1Ev(self); -} - -int APS5_VABI _ZN3sce4Json18InitParameterRtti216setAllocatorRttiEPNS0_14AllocParamRttiEPv(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZN3sce4Json5Value21setNullAccessCallBackEPFRKS1_NS0_9ValueTypeEPS2_PvES6_() { NotImplemented_nid_no_patch(__func__); return 0; @@ -872,4 +896,33 @@ int APS5_VABI _ZNK3sce4Json5ValuecvbEv(void) { return 0; } +int APS5_VABI _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce4Json6Object4PairD1Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce4Json6Object6insertERKNS1_4PairE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce4Json5Array8iteratorppEi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce4Json5Array8iteratoraSERKS2_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce4Json5Array8iteratorC1Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceMouse/Export.cpp b/core/libs/prx/libSceMouse/Export.cpp index 9d820d245..157e3afcc 100644 --- a/core/libs/prx/libSceMouse/Export.cpp +++ b/core/libs/prx/libSceMouse/Export.cpp @@ -1,4 +1,5 @@ #include "prx/libSceMouse/include/MouseState.hpp" +#include "prx/libc/include/General.hpp" extern "C" { @@ -22,4 +23,8 @@ void MousePublishInput_nid_postfix(const MouseInputEvent& event) { Mouse::Publish(event); } +int APS5_VABI sceMouseResetAbsolutePosition() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index bcf7a8ccf..98776d4b5 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -15,6 +15,19 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + if(WIN32) + add_test_executable(guest_socket_host_write_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketHostWrite.cpp) + target_include_directories(guest_socket_host_write_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_host_write_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_socket_host_write_tests) + foreach(api IN ITEMS recv recvfrom read sceKernelRead sceNetRecv sceNetRecvfrom) + add_test(NAME guest_socket_host_write_${api}_udp COMMAND guest_socket_host_write_tests ${api}) + add_test(NAME guest_socket_host_write_${api}_tcp COMMAND guest_socket_host_write_tests ${api} tcp) + set_tests_properties(guest_socket_host_write_${api}_udp guest_socket_host_write_${api}_tcp + PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endforeach() + endif() add_test_executable(guest_sce_net_tests ${LIBS_INCLUDE_DIR}/tests/GuestSceNet.cpp) target_include_directories(guest_sce_net_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_sce_net_tests PRIVATE libSceNet libSceNetCtl libkernel libc) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 3bfaa1229..79214e8e4 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -32,6 +32,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" // Guest socket descriptors retain PS5 semantics while their transport is backed by host sockets. @@ -70,10 +71,17 @@ constexpr int NET_AF_INET6 = 28; constexpr int NET_SOCK_STREAM = 1; constexpr int NET_SOCK_DGRAM = 2; constexpr int NET_SOCK_RAW = 3; +constexpr int NET_EPOLL_CTL_ADD = 1; +constexpr int NET_EPOLL_CTL_MOD = 2; +constexpr int NET_EPOLL_CTL_DEL = 3; constexpr int NET_SOL_SOCKET = 0xFFFF; constexpr int NET_SO_SNDTIMEO = 0x1005; constexpr int NET_SO_RCVTIMEO = 0x1006; constexpr int NET_SO_NBIO = 0x1200; +constexpr int NET_SO_REUSEADDR = 0x4; +constexpr int NET_SO_BROADCAST = 0x20; +constexpr int NET_SO_SNDBUF = 0x1001; +constexpr int NET_SO_RCVBUF = 0x1002; constexpr int NET_MSG_PEEK = 0x2; constexpr int NET_MSG_TRUNC = 0x10; constexpr int NET_UIO_MAXIOV = 1024; @@ -167,7 +175,7 @@ bool guest_to_native_address(const void* address, std::uint32_t length, sockaddr_storage& native, NativeLength& native_length) { if (!address || length < 2) return false; const auto* bytes = static_cast(address); - if (bytes[1] == NET_AF_INET && bytes[0] == 16 && length >= 16) { + if (bytes[1] == NET_AF_INET && length >= 16) { auto& v4 = reinterpret_cast(native); v4.sin_family = AF_INET; std::memcpy(&v4.sin_port, bytes + 2, 2); @@ -175,7 +183,7 @@ bool guest_to_native_address(const void* address, std::uint32_t length, native_length = sizeof(v4); return true; } - if (bytes[1] == NET_AF_INET6 && bytes[0] == 28 && length >= 28) { + if (bytes[1] == NET_AF_INET6 && length >= 28) { auto& v6 = reinterpret_cast(native); v6.sin6_family = AF_INET6; std::memcpy(&v6.sin6_port, bytes + 2, 2); @@ -301,8 +309,15 @@ int* errno_slot() { return &e; } -int fail(int err) { +extern "C" int* APS5_VABI __error_nid_postfix(); + +void set_errno(int err) { *errno_slot() = err; + *__error_nid_postfix() = err; +} + +int fail(int err) { + set_errno(err); return NET_ERROR_BASE | err; } @@ -364,6 +379,15 @@ std::int64_t message_length(const NetMsghdr* message) { return static_cast(total); } +int receive_error(const Sock& socket) { +#ifdef _WIN32 + if (WSAGetLastError() == WSAETIMEDOUT && socket.type != NET_SOCK_STREAM && socket.rcv_timeout_us > 0) return NET_EAGAIN; +#else + (void)socket; +#endif + return native_error(); +} + } // namespace extern "C" { @@ -439,6 +463,32 @@ int PollSockets(KernelSocketPoll::Entry* entries, int count, int timeoutMillisec const bool g_socketPollerRegistered = (KernelSetSocketPoller_nid_no_patch(&PollSockets), true); +extern "C" int APS5_VABI sceNetSocketClose(int s); + +bool CloseSocketForKernel(int descriptor, int* error) { + { + std::lock_guard lk(g_mutex); + if (g_socks.count(descriptor) == 0) return false; + } + *error = sceNetSocketClose(descriptor) == 0 ? 0 : *errno_slot(); + return true; +} + +const bool g_socketCloserRegistered = (KernelSetSocketCloser_nid_no_patch(&CloseSocketForKernel), true); + +extern "C" int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval, uint32_t optlen); + +bool SetSocketOptionForKernel(int descriptor, int level, int option, const void* value, std::uint32_t length, int* error) { + { + std::lock_guard lk(g_mutex); + if (g_socks.count(descriptor) == 0) return false; + } + *error = sceNetSetsockopt(descriptor, level, option, value, length) == 0 ? 0 : *errno_slot(); + return true; +} + +const bool g_socketOptionSetterRegistered = (KernelSetSocketOptionSetter_nid_no_patch(&SetSocketOptionForKernel), true); + int APS5_VABI sceNetInit_nid_postfix(void) { std::lock_guard lk(g_mutex); g_net_inited = true; @@ -644,12 +694,14 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } + const GuestArena::HostWrite destination(len == 0 ? nullptr : buf, len); + if (!destination.Open()) return fail(NET_EFAULT); const int native_flags = flags == 2 ? MSG_PEEK : 0; const int result = ::recv(socket.native->value, static_cast(buf), static_cast(len), native_flags); #ifdef _WIN32 if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) return static_cast(len); #endif - return result >= 0 ? result : fail(native_error()); + return result >= 0 ? result : fail(receive_error(socket)); } int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* from, uint32_t* fromlen) { @@ -664,6 +716,8 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } + const GuestArena::HostWrite destination(len == 0 ? nullptr : buf, len); + if (!destination.Open()) return fail(NET_EFAULT); sockaddr_storage peer{}; NativeLength peer_length = sizeof(peer); const int native_flags = flags == 2 ? MSG_PEEK : 0; @@ -672,7 +726,7 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* #ifdef _WIN32 if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(len); #endif - if (result < 0) return fail(native_error()); + if (result < 0) return fail(receive_error(socket)); if (from && !native_to_guest_address(peer, from, fromlen)) return fail(NET_EAFNOSUPPORT); return result; } @@ -760,7 +814,7 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { flags == NET_MSG_PEEK ? MSG_PEEK : 0, reinterpret_cast(&peer), &peer_length); if (received < 0) { #ifdef _WIN32 - if (!datagram || WSAGetLastError() != WSAEMSGSIZE) return fail(native_error()); + if (!datagram || WSAGetLastError() != WSAEMSGSIZE) return fail(receive_error(socket)); received = static_cast(buffer.size()); #else return fail(native_error()); @@ -831,6 +885,10 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval if (::setsockopt(socket.native->value, SOL_SOCKET, native_option, &timeout, sizeof(timeout)) != 0) return fail(native_error()); #endif + } else if (optname == NET_SO_REUSEADDR || optname == NET_SO_BROADCAST || optname == NET_SO_SNDBUF || optname == NET_SO_RCVBUF) { + const int native_option = optname == NET_SO_REUSEADDR ? SO_REUSEADDR : optname == NET_SO_BROADCAST ? SO_BROADCAST : optname == NET_SO_SNDBUF ? SO_SNDBUF : SO_RCVBUF; + if (::setsockopt(socket.native->value, SOL_SOCKET, native_option, reinterpret_cast(&value), sizeof(value)) != 0) + return fail(native_error()); } else { return fail(NET_EOPNOTSUPP); } @@ -958,13 +1016,13 @@ int APS5_VABI sceNetEpollControl(int eid, int op, int id, const NetEpollEvent* e if (g_epolls.count(eid) == 0) { return fail(NET_EBADF); } - if (op < 1 || op > 3) return fail(NET_EINVAL); - if (op != 2 && !event) return fail(NET_EINVAL); - if (op != 2 && g_socks.count(id) == 0) return fail(NET_EBADF); + if (op != NET_EPOLL_CTL_ADD && op != NET_EPOLL_CTL_MOD && op != NET_EPOLL_CTL_DEL) return fail(NET_EINVAL); + if (op != NET_EPOLL_CTL_DEL && !event) return fail(NET_EINVAL); + if (op != NET_EPOLL_CTL_DEL && g_socks.count(id) == 0) return fail(NET_EBADF); auto& registrations = g_epoll_socks[eid]; - if (op == 1) { + if (op == NET_EPOLL_CTL_ADD) { if (!registrations.emplace(id, *event).second) return fail(NET_EADDRINUSE); - } else if (op == 2) { + } else if (op == NET_EPOLL_CTL_DEL) { if (registrations.erase(id) == 0) return fail(NET_ENOENT); } else { auto registration = registrations.find(id); @@ -1091,11 +1149,11 @@ int APS5_VABI sceNetInetPton(int af, const char* src, void* dst) { const char* APS5_VABI sceNetInetNtop(int af, const void* src, char* dst, uint32_t size) { if (src == nullptr || dst == nullptr) { - *errno_slot() = NET_EINVAL; + set_errno(NET_EINVAL); return nullptr; } if (af != NET_AF_INET && af != NET_AF_INET6) { - *errno_slot() = NET_EAFNOSUPPORT; + set_errno(NET_EAFNOSUPPORT); return nullptr; } char text[INET6_ADDRSTRLEN]; @@ -1105,12 +1163,12 @@ const char* APS5_VABI sceNetInetNtop(int af, const void* src, char* dst, uint32_ const auto* result = ::inet_ntop(af == NET_AF_INET ? AF_INET : AF_INET6, src, text, sizeof(text)); #endif if (!result) { - *errno_slot() = native_error(); + set_errno(native_error()); return nullptr; } const auto length = std::strlen(text) + 1; if (length > size) { - *errno_slot() = NET_ENOSPC; + set_errno(NET_ENOSPC); return nullptr; } std::memcpy(dst, text, length); @@ -1163,7 +1221,7 @@ int lookup_ipv4(int rid, const char* hostname, const char* func, std::vector(&address), sizeof(address), hostname, static_cast(len), nullptr, 0, NI_NAMEREQD); if (result != 0) { - *errno_slot() = result == EAI_AGAIN ? NET_ETIMEDOUT : NET_ENOENT; + set_errno(result == EAI_AGAIN ? NET_ETIMEDOUT : NET_ENOENT); log_soft(__func__, "host reverse DNS lookup failed"); set_resolver_error(rid, NET_ERROR_RESOLVER_ENODNS); return NET_ERROR_RESOLVER_ENODNS; diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index 79d43e8ae..ca10bd524 100644 --- a/core/libs/prx/libSceNgs2.native/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2.native/CMakeLists.txt @@ -2,7 +2,7 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/Ngs2Library.cmake) add_sce_ngs2_library(libSceNgs2.native) if(BUILD_TESTING) - foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx Report) + foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx Report Geom) string(TOLOWER ${ngs2Test} testName) add_test_executable(ngs2_${testName}_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Ngs2${ngs2Test}.cpp) target_include_directories(ngs2_${testName}_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) diff --git a/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake b/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake index 06a20f0d4..877276b0b 100644 --- a/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake +++ b/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake @@ -3,6 +3,7 @@ function(add_sce_ngs2_library target) add_library(${target} SHARED EXCLUDE_FROM_ALL ${ngs2Dir}/src/Atrac9.cpp ${ngs2Dir}/src/Custom.cpp + ${ngs2Dir}/src/Geom.cpp ${ngs2Dir}/src/Pan.cpp ${ngs2Dir}/src/Rack.cpp ${ngs2Dir}/src/Render.cpp diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index c79d3ad7c..fd9cfd40f 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -36,6 +36,7 @@ static constexpr std::uint32_t SCE_NGS2_VOICE_STATE_FLAG_INUSE = 1; static constexpr std::uint32_t SCE_NGS2_VOICE_STATE_FLAG_PLAYING = 2; static constexpr std::uint32_t SCE_NGS2_VOICE_STATE_FLAG_PAUSED = 4; static constexpr std::uint32_t SCE_NGS2_VOICE_STATE_FLAG_STOPPED = 8; +static constexpr std::uint32_t SCE_NGS2_VOICE_STATE_FLAG_EMPTY = 0x20; static constexpr std::uint32_t SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS = 0x0001; static constexpr std::uint32_t SCE_NGS2_VOICE_PARAM_PORT_VOLUME = 0x0002; @@ -606,20 +607,79 @@ struct Ngs2PanWork { }; static_assert(sizeof(Ngs2PanWork) == 40); -struct Ngs2GeomListenerParam { - std::uint32_t reserved[32]; +struct Ngs2GeomVector { + float x; + float y; + float z; }; +static_assert(sizeof(Ngs2GeomVector) == 12); + +struct Ngs2GeomCone { + float inner_level; + float inner_angle; + float outer_level; + float outer_angle; +}; +static_assert(sizeof(Ngs2GeomCone) == 16); + +struct Ngs2GeomRolloff { + std::uint32_t model; + float max_distance; + float rolloff_factor; + float reference_distance; +}; +static_assert(sizeof(Ngs2GeomRolloff) == 16); + +struct Ngs2GeomListenerParam { + Ngs2GeomVector position; + Ngs2GeomVector orient_front; + Ngs2GeomVector orient_up; + Ngs2GeomVector velocity; + float sound_speed; + std::uint32_t reserved[2]; +}; +static_assert(sizeof(Ngs2GeomListenerParam) == 60); struct Ngs2GeomListenerWork { - std::uint32_t reserved[64]; + float matrix[4][4]; + Ngs2GeomVector velocity; + float sound_speed; + std::uint32_t coordinate; + std::uint32_t reserved[3]; }; +static_assert(sizeof(Ngs2GeomListenerWork) == 96); struct Ngs2GeomSourceParam { - std::uint32_t reserved[32]; + Ngs2GeomVector position; + Ngs2GeomVector velocity; + Ngs2GeomVector direction; + Ngs2GeomCone cone; + Ngs2GeomRolloff rolloff; + float doppler_factor; + float fbw_level; + float lfe_level; + float max_level; + float min_level; + float radius; + std::uint32_t num_speakers; + std::uint32_t matrix_format; + std::uint32_t reserved[2]; }; +static_assert(sizeof(Ngs2GeomSourceParam) == 108); + +struct Ngs2GeomA3dAttribute { + Ngs2GeomVector position; + float volume; + std::uint32_t reserved[4]; +}; +static_assert(sizeof(Ngs2GeomA3dAttribute) == 32); struct Ngs2GeomAttribute { - std::uint32_t reserved[32]; + float pitch_ratio; + float level[64]; + Ngs2GeomA3dAttribute a3d_attrib; + std::uint32_t reserved[4]; }; +static_assert(sizeof(Ngs2GeomAttribute) == 308); #endif diff --git a/core/libs/prx/libSceNgs2.native/src/Geom.cpp b/core/libs/prx/libSceNgs2.native/src/Geom.cpp new file mode 100644 index 000000000..c5e3624b3 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/src/Geom.cpp @@ -0,0 +1,34 @@ +#include + +#include "Ngs2Internal.hpp" + +static constexpr float DEFAULT_SOUND_SPEED = 343.0f; + +int Ngs2GeomResetListenerParam(Ngs2GeomListenerParam* out_listener_param) { + if (out_listener_param == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + *out_listener_param = {}; + out_listener_param->orient_front.z = 1.0f; + out_listener_param->orient_up.y = 1.0f; + out_listener_param->sound_speed = DEFAULT_SOUND_SPEED; + return SCE_NGS2_OK; +} + +int Ngs2GeomResetSourceParam(Ngs2GeomSourceParam* out_source_param) { + if (out_source_param == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + *out_source_param = {}; + out_source_param->direction.z = 1.0f; + out_source_param->cone.inner_level = 1.0f; + out_source_param->cone.inner_angle = 360.0f; + out_source_param->cone.outer_level = 1.0f; + out_source_param->cone.outer_angle = 360.0f; + out_source_param->rolloff.max_distance = 1000000.0f; + out_source_param->rolloff.rolloff_factor = 1.0f; + out_source_param->rolloff.reference_distance = 1.0f; + out_source_param->doppler_factor = 1.0f; + out_source_param->fbw_level = 1.0f; + out_source_param->lfe_level = 1.0f; + out_source_param->max_level = 1.0f; + out_source_param->num_speakers = 2; + out_source_param->matrix_format = 2; + return SCE_NGS2_OK; +} diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index 9bed17834..65829bf2f 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -203,5 +203,7 @@ void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo float Ngs2DefaultLevel(std::uint32_t sourceChannels, std::uint32_t source, std::uint32_t destChannels, std::uint32_t dest); int Ngs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle, uint32_t num_speakers); int Ngs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* params, uint32_t num_params, uint32_t matrix_format, float* out_volume_matrix); +int Ngs2GeomResetListenerParam(Ngs2GeomListenerParam* out_listener_param); +int Ngs2GeomResetSourceParam(Ngs2GeomSourceParam* out_source_param); #endif diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index c93902179..6cf69f714 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -314,6 +314,7 @@ void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo for (auto* voice : voices) { if (voice->state == Ngs2PlayState::Stopped) voice->state = Ngs2PlayState::Empty; voice->stateFlags = static_cast(voice->state); + if (voice->state == Ngs2PlayState::Playing && voice->rack->rackId == SCE_NGS2_RACK_ID_SAMPLER && voice->blocks.empty() && voice->acceptsBlocks) voice->stateFlags |= SCE_NGS2_VOICE_STATE_FLAG_EMPTY; } system.renderCount++; } diff --git a/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp b/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp index 3287f78d9..9cb4cd441 100644 --- a/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp @@ -25,18 +25,6 @@ int APS5_VABI sceNgs2GeomCalcListener(const Ngs2GeomListenerParam* param, Ngs2Ge return 0; } -int APS5_VABI sceNgs2GeomResetListenerParam(Ngs2GeomListenerParam* out_listener_param) { - (void)out_listener_param; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNgs2GeomResetSourceParam(Ngs2GeomSourceParam* out_source_param) { - (void)out_source_param; - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNgs2ReportRegisterHandler(uint32_t report_type, void* handler, uintptr_t user_data, Ngs2Handle* out_handle) { (void)report_type; (void)handler; diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index 008c1cd26..bb45dc5b2 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -486,6 +486,14 @@ int APS5_VABI sceNgs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* p return Ngs2PanGetVolumeMatrix(work, params, num_params, matrix_format, out_volume_matrix); } +int APS5_VABI sceNgs2GeomResetListenerParam(Ngs2GeomListenerParam* out_listener_param) { + return Ngs2GeomResetListenerParam(out_listener_param); +} + +int APS5_VABI sceNgs2GeomResetSourceParam(Ngs2GeomSourceParam* out_source_param) { + return Ngs2GeomResetSourceParam(out_source_param); +} + } #pragma GCC visibility pop diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp new file mode 100644 index 000000000..ca194eea7 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp @@ -0,0 +1,57 @@ +#include "Ngs2Test.hpp" + +#include +#include + +static constexpr float SoundSpeed = 343.0f; + +static void TestLayouts() { + Require(sizeof(Ngs2GeomVector) == 12); + Require(sizeof(Ngs2GeomCone) == 16); + Require(sizeof(Ngs2GeomRolloff) == 16); + Require(sizeof(Ngs2GeomListenerParam) == 60); + Require(sizeof(Ngs2GeomListenerWork) == 96); + Require(sizeof(Ngs2GeomSourceParam) == 108); + Require(sizeof(Ngs2GeomA3dAttribute) == 32); + Require(sizeof(Ngs2GeomAttribute) == 308); +} + +static void TestResetListenerParam() { + Ngs2GeomListenerParam param; + std::memset(¶m, 0x5a, sizeof(param)); + Require(sceNgs2GeomResetListenerParam(¶m) == SCE_NGS2_OK); + Require(param.position.x == 0.0f && param.position.y == 0.0f && param.position.z == 0.0f); + Require(param.velocity.x == 0.0f && param.velocity.y == 0.0f && param.velocity.z == 0.0f); + Require(param.orient_front.x == 0.0f && param.orient_front.y == 0.0f && param.orient_front.z == 1.0f); + Require(param.orient_up.x == 0.0f && param.orient_up.y == 1.0f && param.orient_up.z == 0.0f); + Require(param.sound_speed == SoundSpeed); + Require(param.reserved[0] == 0u && param.reserved[1] == 0u); + Require(sceNgs2GeomResetListenerParam(nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); +} + +static void TestResetSourceParam() { + Ngs2GeomSourceParam param; + std::memset(¶m, 0x5a, sizeof(param)); + Require(sceNgs2GeomResetSourceParam(¶m) == SCE_NGS2_OK); + Require(param.position.x == 0.0f && param.position.y == 0.0f && param.position.z == 0.0f); + Require(param.velocity.x == 0.0f && param.velocity.y == 0.0f && param.velocity.z == 0.0f); + Require(param.direction.x == 0.0f && param.direction.y == 0.0f && param.direction.z == 1.0f); + Require(param.cone.inner_level == 1.0f && param.cone.inner_angle == 360.0f); + Require(param.cone.outer_level == 1.0f && param.cone.outer_angle == 360.0f); + Require(param.rolloff.model == 0u); + Require(param.rolloff.max_distance == 1000000.0f); + Require(param.rolloff.rolloff_factor == 1.0f); + Require(param.rolloff.reference_distance == 1.0f); + Require(param.doppler_factor == 1.0f && param.fbw_level == 1.0f && param.lfe_level == 1.0f); + Require(param.max_level == 1.0f && param.min_level == 0.0f && param.radius == 0.0f); + Require(param.num_speakers == 2u && param.matrix_format == 2u); + Require(param.reserved[0] == 0u && param.reserved[1] == 0u); + Require(sceNgs2GeomResetSourceParam(nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); +} + +int main() { + TestLayouts(); + TestResetListenerParam(); + TestResetSourceParam(); + return 0; +} diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp index af1e0b06e..726cf9148 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp @@ -158,10 +158,37 @@ static void TestSilenceBlock() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static void TestDrainedStreamReportsEmpty() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 1); + const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + const auto voice = StreamVoice(rack, 0, master); + const std::vector pcm(4, 16384); + constexpr std::uint32_t playing = SCE_NGS2_VOICE_STATE_FLAG_INUSE | SCE_NGS2_VOICE_STATE_FLAG_PLAYING; + + AddBlock(voice, pcm.data(), pcm.size() * 2, 4, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET | SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + RenderI16(system); + Require(Flags(voice) == (playing | SCE_NGS2_VOICE_STATE_FLAG_EMPTY)); + + const std::vector longer(Grain * 2, 8192); + AddBlock(voice, longer.data(), longer.size() * 2, Grain * 2, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE); + const auto out = RenderI16(system); + Require(out[0] == 8192); + Require(Flags(voice) == playing); + + AddBlock(voice, pcm.data(), pcm.size() * 2, 4, 0); + RenderI16(system); + RenderI16(system); + Require((Flags(voice) & SCE_NGS2_VOICE_STATE_FLAG_EMPTY) == 0); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + int main() { TestStreamEndsAtDeclaredSamples(); TestStreamClosedBeforeDeclaredSamples(); TestStreamRejections(); TestSilenceBlock(); + TestDrainedStreamReportsEmpty(); return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp index 51a50d512..6312953af 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp @@ -39,6 +39,8 @@ int APS5_VABI sceNgs2VoiceGetState(uintptr_t, Ngs2VoiceState*, size_t); int APS5_VABI sceNgs2VoiceGetStateFlags(uintptr_t, uint32_t*); int APS5_VABI sceNgs2VoiceGetPortInfo(uintptr_t, uint32_t, Ngs2VoicePortInfo*, size_t); int APS5_VABI sceNgs2VoiceQueryInfo(uintptr_t, uint32_t, void*, size_t); +int APS5_VABI sceNgs2GeomResetListenerParam(Ngs2GeomListenerParam*); +int APS5_VABI sceNgs2GeomResetSourceParam(Ngs2GeomSourceParam*); } inline void Check(bool value, int line) { diff --git a/core/libs/prx/libSceNpAuth/Export.cpp b/core/libs/prx/libSceNpAuth/Export.cpp index 1ef3f03a6..c17210d9f 100644 --- a/core/libs/prx/libSceNpAuth/Export.cpp +++ b/core/libs/prx/libSceNpAuth/Export.cpp @@ -32,6 +32,11 @@ int APS5_VABI sceNpAuthDeleteRequest(int req_id) { return 0; } +int APS5_VABI sceNpAuthGetAuthorizedAppCode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceNpAuthGetAuthorizationCodeV3(int req_id, const void* param, void* auth_code, int* issuer_id) { (void)req_id; (void)issuer_id; diff --git a/core/libs/prx/libSceNpAuthAuthorizedAppDialog/CMakeLists.txt b/core/libs/prx/libSceNpAuthAuthorizedAppDialog/CMakeLists.txt new file mode 100644 index 000000000..3d65cfe79 --- /dev/null +++ b/core/libs/prx/libSceNpAuthAuthorizedAppDialog/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libSceNpAuthAuthorizedAppDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpAuthAuthorizedAppDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpAuthAuthorizedAppDialog) + +if(BUILD_TESTING) + add_test_executable(guest_np_auth_authorized_app_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpAuthAuthorizedAppDialog.cpp) + target_include_directories(guest_np_auth_authorized_app_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_auth_authorized_app_dialog_tests PRIVATE libSceNpAuthAuthorizedAppDialog) + add_test(NAME guest_np_auth_authorized_app_dialog COMMAND guest_np_auth_authorized_app_dialog_tests) + if(WIN32) + set_tests_properties(guest_np_auth_authorized_app_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp b/core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp new file mode 100644 index 000000000..6a722de5a --- /dev/null +++ b/core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp @@ -0,0 +1,70 @@ +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int COMMON_DIALOG_STATUS_NONE = 0; +constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; +constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; +constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; +constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; + +std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; + +} + +extern "C" { + +int APS5_VABI sceNpAuthAuthorizedAppDialogInitialize(void) { + int expected = COMMON_DIALOG_STATUS_NONE; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + return 0; +} + +int APS5_VABI sceNpAuthAuthorizedAppDialogOpen(const void* param) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; +} + +int APS5_VABI sceNpAuthAuthorizedAppDialogUpdateStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceNpAuthAuthorizedAppDialogGetStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceNpAuthAuthorizedAppDialogGetResult(void* result) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + const std::int32_t canceled = COMMON_DIALOG_RESULT_USER_CANCELED; + std::memcpy(result, &canceled, sizeof(canceled)); + return 0; +} + +int APS5_VABI sceNpAuthAuthorizedAppDialogClose(void) { + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + return 0; +} + +int APS5_VABI sceNpAuthAuthorizedAppDialogTerminate(void) { + if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + return 0; +} + +} diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index 90aeb0b06..aecaa928b 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -12,6 +12,8 @@ #include #ifdef _WIN32 #include +#elif defined(__APPLE__) +#include #endif static constexpr int SCE_NP_ENTITLEMENT_ACCESS_ERROR_PARAMETER = static_cast(0x80558003); @@ -28,6 +30,15 @@ std::filesystem::path EntitlementsPath() { const auto length = GetModuleFileNameW(nullptr, module, MAX_PATH); if (length == 0 || length == MAX_PATH) throw std::runtime_error("NpEntitlementAccess: cannot locate the executable"); return std::filesystem::path(module).parent_path() / "anyps5-entitlements.ini"; +#elif defined(__APPLE__) + std::uint32_t size = 0; + _NSGetExecutablePath(nullptr, &size); + std::vector executable(size); + std::error_code error; + std::filesystem::path path; + if (_NSGetExecutablePath(executable.data(), &size) == 0) path = std::filesystem::canonical(executable.data(), error); + if (path.empty()) throw std::runtime_error("NpEntitlementAccess: cannot locate the executable"); + return path.parent_path() / "anyps5-entitlements.ini"; #else return std::filesystem::read_symlink("/proc/self/exe").parent_path() / "anyps5-entitlements.ini"; #endif diff --git a/core/libs/prx/libSceNpWebApi2/Export.cpp b/core/libs/prx/libSceNpWebApi2/Export.cpp index 75c959187..806accdb3 100644 --- a/core/libs/prx/libSceNpWebApi2/Export.cpp +++ b/core/libs/prx/libSceNpWebApi2/Export.cpp @@ -36,6 +36,11 @@ int APS5_VABI sceNpWebApi2AddHttpRequestHeader(int64_t request_id, const char* f return 0; } +int APS5_VABI sceNpWebApi2AddWebTraceTag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + void APS5_VABI sceNpWebApi2CheckTimeout(void) { } diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 10f597db2..cba221154 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -35,6 +35,21 @@ int APS5_VABI scePadClose_nid_postfix(int handle) { return PAD_OK; } +int APS5_VABI scePadVrControllerSetVibration() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVrControllerSetVibrationMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVrControllerReadCombinedState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI scePadDeviceClassGetExtendedInformation(int handle, PadDeviceClassExtendedInformation* info) { if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; if (info == nullptr) return PAD_ERROR_INVALID_ARG; diff --git a/core/libs/prx/libScePad/include/InputCommandFile.hpp b/core/libs/prx/libScePad/include/InputCommandFile.hpp index 8915531f4..7433fd9f4 100644 --- a/core/libs/prx/libScePad/include/InputCommandFile.hpp +++ b/core/libs/prx/libScePad/include/InputCommandFile.hpp @@ -1,6 +1,8 @@ #ifndef CORE_LIBS_PRX_LIBSCEPAD_INPUTCOMMANDFILE_HPP #define CORE_LIBS_PRX_LIBSCEPAD_INPUTCOMMANDFILE_HPP +#include +#include #include #include #include @@ -9,28 +11,38 @@ namespace Pad { class InputCommandFile { public: + struct Input { + std::uint32_t buttons = 0; + bool hasSticks = false; + std::array sticks{}; + + bool operator==(const Input&) const = default; + }; + explicit InputCommandFile(const char* path) : path(path != nullptr ? path : "") { if (!this->path.empty()) lastSequence = read().sequence; } - std::uint32_t Buttons(std::uint64_t now) { - if (path.empty()) return 0; + Input Poll(std::uint64_t now) { + if (path.empty()) return {}; if (now >= nextPoll) { nextPoll = now + 100000; const auto command = read(); if (command.sequence > lastSequence) { lastSequence = command.sequence; - buttons = command.buttons; + input = command.input; until = now + command.durationMs * 1000ull; } } - return now < until ? buttons : 0; + return now < until ? input : Input{}; } + std::uint32_t Buttons(std::uint64_t now) { return Poll(now).buttons; } + private: struct Command { std::uint64_t sequence = 0; - std::uint32_t buttons = 0; + Input input; std::uint32_t durationMs = 0; }; @@ -41,10 +53,27 @@ private: const std::string text(line); if (text.find('-') != std::string::npos) return {}; std::istringstream fields(text); + std::string tokens[8]; + int count = 0; + while (count < 8 && fields >> tokens[count]) ++count; + if (count != 3 && count != 7) return {}; + std::uint64_t values[7]{}; + for (int i = 0; i < count; ++i) { + const char* first = tokens[i].data() + (tokens[i].front() == '+' ? 1 : 0); + const char* last = tokens[i].data() + tokens[i].size(); + const auto parsed = std::from_chars(first, last, values[i]); + if (first == last || parsed.ec != std::errc{} || parsed.ptr != last) return {}; + } + if (values[0] == 0 || (values[1] & ~0x10fffeull) != 0 || values[2] == 0 || values[2] > 5000) return {}; Command command; - std::string extra; - if (!(fields >> command.sequence >> command.buttons >> command.durationMs) || fields >> extra) return {}; - if (command.sequence == 0 || (command.buttons & ~0x10fffeu) != 0 || command.durationMs == 0 || command.durationMs > 5000) return {}; + command.sequence = values[0]; + command.input.buttons = static_cast(values[1]); + command.durationMs = static_cast(values[2]); + for (int i = 3; i < count; ++i) { + if (values[i] > 255) return {}; + command.input.sticks[i - 3] = static_cast(values[i]); + } + command.input.hasSticks = count == 7; return command; } @@ -52,7 +81,7 @@ private: std::uint64_t lastSequence = 0; std::uint64_t nextPoll = 0; std::uint64_t until = 0; - std::uint32_t buttons = 0; + Input input; }; } diff --git a/core/libs/prx/libScePad/include/InputMapping.hpp b/core/libs/prx/libScePad/include/InputMapping.hpp index ea3513278..21cccc0ae 100644 --- a/core/libs/prx/libScePad/include/InputMapping.hpp +++ b/core/libs/prx/libScePad/include/InputMapping.hpp @@ -13,7 +13,20 @@ namespace Pad { inline constexpr int MousePollIntervalMs = 33; inline constexpr int WheelPressDurationMs = 80; -inline constexpr double MouseSensitivity = 1.0; +inline constexpr double DefaultMouseSensitivity = 1.0; +inline constexpr double MaxMouseSensitivity = 10.0; +inline constexpr int MaxStickDeadzonePercent = 90; + +struct InputSettings { + int leftStickDeadzonePercent = 0; + int rightStickDeadzonePercent = 0; + double mouseSensitivity = DefaultMouseSensitivity; +}; + +struct InputConfiguration { + std::vector bindings; + InputSettings settings; +}; inline constexpr std::array InputMapping{ InputBinding{SDL_SCANCODE_F11, MouseButton::None, InputControl::ToggleFullscreen}, @@ -51,7 +64,8 @@ inline constexpr std::array InputMapping{ InputBinding{SDL_SCANCODE_UNKNOWN, MouseButton::None, InputControl::Button, PadButton::Down, -1} }; -std::vector LoadInputMapping(); +InputConfiguration LoadInputMapping(); +std::array StickWithDeadzone(std::int16_t x, std::int16_t y, int deadzonePercent); } diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index 1f40fc26a..9d8adf4c3 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -109,15 +109,19 @@ void Pad::Initialize() { } namespace { -PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uint64_t now, std::uint32_t injected) { +PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uint64_t now, const Pad::InputCommandFile::Input& injected) { PadData data{}; - data.buttons = state.buttons | injected; - data.left_stick_x = state.sticks[0]; - data.left_stick_y = state.sticks[1]; - data.right_stick_x = state.sticks[2]; - data.right_stick_y = state.sticks[3]; - data.analog_buttons_l2 = std::max(state.analogButtonsL2, (data.buttons & 0x100) != 0 ? 255 : 0); - data.analog_buttons_r2 = std::max(state.analogButtonsR2, (data.buttons & 0x200) != 0 ? 255 : 0); + data.buttons = state.buttons | injected.buttons; + const auto& sticks = injected.hasSticks ? injected.sticks : state.sticks; + data.left_stick_x = sticks[0]; + data.left_stick_y = sticks[1]; + data.right_stick_x = sticks[2]; + data.right_stick_y = sticks[3]; + const auto trigger = [&](std::uint8_t analog, std::uint32_t bit) -> std::uint8_t { + return (injected.buttons & bit) != 0 || (analog == 0 && (state.buttons & bit) != 0) ? 255 : analog; + }; + data.analog_buttons_l2 = trigger(state.analogButtonsL2, 0x100); + data.analog_buttons_r2 = trigger(state.analogButtonsR2, 0x200); const bool live = state.hasMotion && output.motionEnabled; const std::array rest{0.0f, 9.80665f, 0.0f}; @@ -172,10 +176,10 @@ PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uin return data; } -std::uint32_t InjectedButtons(std::uint64_t now) { +Pad::InputCommandFile::Input InjectedInput(std::uint64_t now) { static Pad::InputCommandFile commands(std::getenv("APS5_PAD_INPUT_FILE")); - static std::uint32_t lastInjected = 0; - const auto injected = commands.Buttons(now); + static Pad::InputCommandFile::Input lastInjected; + const auto injected = commands.Poll(now); if (injected != lastInjected) { lastInjected = injected; timestamp = now; @@ -189,7 +193,7 @@ PadData Pad::ReadState() { if (failure) std::rethrow_exception(failure); if (!initialized) throw std::runtime_error("Pad: read before initialization"); const std::uint64_t now = sceKernelGetProcessTime(); - const std::uint32_t injected = InjectedButtons(now); + const auto injected = InjectedInput(now); return BuildData(state, timestamp, now, injected); } @@ -199,7 +203,7 @@ int Pad::Read(PadData* data, int num) { if (failure) std::rethrow_exception(failure); if (!initialized) throw std::runtime_error("Pad: read before initialization"); const std::uint64_t now = sceKernelGetProcessTime(); - const std::uint32_t injected = InjectedButtons(now); + const auto injected = InjectedInput(now); if (queue.empty()) { data[0] = BuildData(state, timestamp, now, injected); return 1; diff --git a/core/libs/prx/libScePosixForWebKit/Export.cpp b/core/libs/prx/libScePosixForWebKit/Export.cpp index 0c3b4b758..4ff202b86 100644 --- a/core/libs/prx/libScePosixForWebKit/Export.cpp +++ b/core/libs/prx/libScePosixForWebKit/Export.cpp @@ -1,10 +1,9 @@ #include "prx/libc/include/general/VabiMacros.hpp" -#include +#include "prx/libc/include/HostEntropy.hpp" #include #include #include #include -#include #include namespace { @@ -172,12 +171,7 @@ char* APS5_VABI strcasestr_nid_postfix(const char* text, const char* needle) { } void APS5_VABI arc4random_buf_nid_postfix(void* buffer, std::size_t size) { - static thread_local std::random_device device; - auto* bytes = static_cast(buffer); - for (std::size_t offset = 0; offset < size; offset += sizeof(unsigned int)) { - const unsigned int value = device(); - std::memcpy(bytes + offset, &value, std::min(sizeof(value), size - offset)); - } + HostEntropy_nid_no_patch(buffer, size); } std::uint32_t APS5_VABI arc4random_nid_postfix() { diff --git a/core/libs/prx/libScePosixForWebKit/Resolver.cpp b/core/libs/prx/libScePosixForWebKit/Resolver.cpp index 7c5254fee..6441693f4 100644 --- a/core/libs/prx/libScePosixForWebKit/Resolver.cpp +++ b/core/libs/prx/libScePosixForWebKit/Resolver.cpp @@ -8,6 +8,7 @@ #include #endif #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include #include #include @@ -129,7 +130,8 @@ GuestResolver::HostEntry* APS5_VABI gethostbyname_nid_postfix(const char* name) return nullptr; } #endif - static thread_local HostLookup lookup; + struct LookupTag {}; + auto& lookup = HostThreadLocal(); lookup.name = native->h_name; lookup.aliasNames.clear(); lookup.aliases.clear(); @@ -284,13 +286,13 @@ int APS5_VABI getnameinfo_nid_postfix(const void* address, std::uint32_t length, const char* APS5_VABI gai_strerror_nid_postfix(int error) { static const char* messages[] = { "Success", "Address family for hostname not supported", "Temporary failure in name resolution", - "Invalid flags", "Name resolution failed", "Address family not supported", - "Memory allocation failed", "No address for hostname", "Name or service not known", - "Service not supported for socket type", "Socket type not supported", "System error", - "Invalid hints", "Unknown protocol", "Buffer too small" + "Invalid value for ai_flags", "Non-recoverable failure in name resolution", "ai_family not supported", + "Memory allocation failure", "No address associated with hostname", "hostname nor servname provided, or not known", + "servname not supported for ai_socktype", "ai_socktype not supported", "System error returned in errno", + "Invalid value for hints", "Resolved protocol is unknown", "Argument buffer overflow" }; if (error < 0 || error >= static_cast(sizeof(messages) / sizeof(messages[0]))) - return "Unknown address resolution error"; + return "Unknown error"; return messages[error]; } } diff --git a/core/libs/prx/libSceRandom/Export.cpp b/core/libs/prx/libSceRandom/Export.cpp index 4e9ef2674..c61ffc4d5 100644 --- a/core/libs/prx/libSceRandom/Export.cpp +++ b/core/libs/prx/libSceRandom/Export.cpp @@ -1,10 +1,8 @@ #include #include -#include -#include -#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/HostEntropy.hpp" namespace { @@ -17,12 +15,7 @@ extern "C" { int APS5_VABI sceRandomGetRandomNumber(void* buf, size_t size) { if (!buf || size > SCE_RANDOM_MAX_SIZE) return SCE_RANDOM_ERROR_INVALID; - static thread_local std::random_device device; - auto* bytes = static_cast(buf); - for (std::size_t offset = 0; offset < size; offset += sizeof(unsigned int)) { - const unsigned int value = device(); - std::memcpy(bytes + offset, &value, std::min(sizeof(value), size - offset)); - } + HostEntropy_nid_no_patch(buf, size); return 0; } diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index e1c13629d..b0f3a5603 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -234,6 +234,8 @@ bool parseZoneName(const char* cursor, std::int64_t& offsetMinutes) { const char* name; std::int64_t minutes; } zones[] = { + {"WETDST", 60}, {"METDST", 120}, {"EETDST", 180}, {"SWT", 60}, {"MEZ", 60}, {"AESST", 660}, {"ACSST", 630}, + {"CAST", 570}, {"CADT", 630}, {"SAST", 570}, {"SADT", 630}, {"WST", 480}, {"WDT", 540}, {"GMT", 0}, {"WET", 0}, {"BST", 60}, {"WAT", -60}, {"AST", -240}, {"ADT", -180}, {"EST", -300}, {"EDT", -240}, {"CST", -360}, {"CDT", -300}, {"MST", -420}, {"MDT", -360}, {"PST", -480}, {"PDT", -420}, {"YST", -540}, {"YDT", -480}, {"HDT", -540}, {"CAT", -600}, {"AHST", -600}, {"NST", -150}, {"NDT", -90}, {"IDLW", -720}, {"CET", 60}, {"MET", 60}, diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index cff5949a7..bbd31b09a 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -25,6 +25,7 @@ constexpr int RUDP_POOL_MIN_SIZE = 0xC58; constexpr std::uintptr_t RUDP_POOL_ALIGNMENT = 8; constexpr std::size_t RUDP_STATUS_SIZE = 0xF8; constexpr std::size_t RUDP_STATUS_CURRENT_CONTEXTS = 0x64; +constexpr uint16_t RUDP_MAX_SEGMENT_SIZE = 1410; using GuestRudpEventHandler = void(APS5_VABI*)(int, int, int, void*); @@ -273,8 +274,15 @@ int APS5_VABI sceRudpNetReceived(void) { return 0; } -int APS5_VABI sceRudpGetMaxSegmentSize(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceRudpGetMaxSegmentSize(uint16_t* size) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (size == nullptr) { + return RUDP_ERROR_INVALID_ARGUMENT; + } + *size = RUDP_MAX_SEGMENT_SIZE; return 0; } diff --git a/core/libs/prx/libSceSaveData.native/CMakeLists.txt b/core/libs/prx/libSceSaveData.native/CMakeLists.txt index 3b596955e..6b03a2c86 100644 --- a/core/libs/prx/libSceSaveData.native/CMakeLists.txt +++ b/core/libs/prx/libSceSaveData.native/CMakeLists.txt @@ -22,6 +22,15 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_savedata_convert_tests ${LIBS_INCLUDE_DIR}/tests/GuestSaveDataConvert.cpp) + target_include_directories(guest_savedata_convert_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_savedata_convert_tests PRIVATE libSceSaveData.native libc) + add_test(NAME guest_savedata_convert COMMAND guest_savedata_convert_tests) + if(WIN32) + set_tests_properties(guest_savedata_convert PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_savedata_mount_status_tests ${LIBS_INCLUDE_DIR}/tests/GuestSaveDataMountStatus.cpp) target_include_directories(guest_savedata_mount_status_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_savedata_mount_status_tests PRIVATE libSceSaveData.native libc) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 4f96392ed..29494d4f1 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -270,13 +270,22 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS } result->hit_num = 0; result->set_num = 0; + const std::uint32_t key = cond->key; + if ((key > SAVE_DATA_SORT_KEY_MTIME && key != SAVE_DATA_SORT_KEY_FREE_BLOCKS) || cond->order > SAVE_DATA_SORT_ORDER_DESCENT) { + return SAVE_DATA_ERROR_PARAMETER; + } const char* pattern = (cond->dir_name != nullptr) ? cond->dir_name->data : nullptr; const std::string root = save_root(); if (!std::filesystem::is_directory(root)) { return SAVE_DATA_OK; } - std::uint32_t hit = 0; - std::uint32_t set = 0; + struct Match { + std::string name; + std::string path; + SaveDataParam param; + }; + const bool sortByParam = key == SAVE_DATA_SORT_KEY_USER_PARAM || key == SAVE_DATA_SORT_KEY_MTIME; + std::vector matches; for (const auto& entry : std::filesystem::directory_iterator(root)) { if (!entry.is_directory()) { continue; @@ -285,16 +294,32 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS if (!dir_name_match(name.c_str(), pattern)) { continue; } - hit++; - if (result->dir_names != nullptr && set < result->dir_names_num) { - std::snprintf(result->dir_names[set].data, sizeof(result->dir_names[set].data), "%s", name.c_str()); - if (result->params != nullptr) { - result->params[set] = load_param(entry.path().string()); - } - set++; + Match match{name, entry.path().string(), {}}; + if (sortByParam) { + match.param = load_param(match.path); + } + matches.push_back(std::move(match)); + } + std::sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.name < b.name; }); + if (key == SAVE_DATA_SORT_KEY_USER_PARAM) { + std::stable_sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.param.user_param < b.param.user_param; }); + } else if (key == SAVE_DATA_SORT_KEY_MTIME) { + std::stable_sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.param.mtime < b.param.mtime; }); + } + if (cond->order == SAVE_DATA_SORT_ORDER_DESCENT) { + std::reverse(matches.begin(), matches.end()); + } + const std::uint32_t set = result->dir_names == nullptr ? 0 : static_cast(std::min(result->dir_names_num, matches.size())); + for (std::uint32_t i = 0; i < set; i++) { + std::snprintf(result->dir_names[i].data, sizeof(result->dir_names[i].data), "%s", matches[i].name.c_str()); + if (result->params != nullptr) { + result->params[i] = sortByParam ? matches[i].param : load_param(matches[i].path); + } + if (result->infos != nullptr) { + result->infos[i] = SaveDataSearchInfo{SAVE_DATA_BLOCKS_MAX, SAVE_DATA_BLOCKS_MAX, {}}; } } - result->hit_num = hit; + result->hit_num = static_cast(matches.size()); result->set_num = set; return SAVE_DATA_OK; } @@ -625,22 +650,30 @@ static int setupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, SaveDat if (!have) { existed = 0; } + const bool needParam = (setup_param->init_param != nullptr && (setup_param->option & 1u) != 0); + const std::string paramPath = mem_path(setup_param->user_id, setup_param->slot_id, "param"); + std::size_t paramSize = 0; + const bool haveParam = file_size_of(paramPath, ¶mSize); // First run: create a zero-filled blob and report existed size 0 so the title treats it as a new save. - if (!have || existed < setup_param->memory_size) { + if (!have || existed < setup_param->memory_size || (needParam && !haveParam)) { std::error_code ec; std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec); - std::vector data; - if (have && !read_file_all(path, data)) { - return SAVE_DATA_ERROR_INTERNAL; - } - data.resize(setup_param->memory_size, 0); - if (!write_file_replace(path, data)) { - return SAVE_DATA_ERROR_INTERNAL; - } - if (setup_param->init_param != nullptr && (setup_param->option & 1u) != 0) { + if (needParam) { std::vector pd(sizeof(SaveDataParam)); std::memcpy(pd.data(), setup_param->init_param, sizeof(SaveDataParam)); - write_file_replace(mem_path(setup_param->user_id, setup_param->slot_id, "param"), pd); + if (!write_file_replace(paramPath, pd)) { + return SAVE_DATA_ERROR_INTERNAL; + } + } + if (!have || existed < setup_param->memory_size) { + std::vector data; + if (have && !read_file_all(path, data)) { + return SAVE_DATA_ERROR_INTERNAL; + } + data.resize(setup_param->memory_size, 0); + if (!write_file_replace(path, data)) { + return SAVE_DATA_ERROR_INTERNAL; + } } } if (result != nullptr) { @@ -740,9 +773,28 @@ int APS5_VABI sceSaveDataDirNameSearchPs4(const SaveDataDirNameSearchCond* cond, return rc; } -int APS5_VABI sceSaveDataConvert() { - NotImplemented_nid_no_patch(__func__); - return 0; +static int convert(const SaveDataConvert* param) { + if (param == nullptr || param->dir_name == nullptr) { + throw std::runtime_error("sceSaveDataConvert: null argument"); + } + const auto* nameEnd = static_cast(std::memchr(param->dir_name->data, '\0', sizeof(param->dir_name->data))); + if (nameEnd == nullptr) { + return SAVE_DATA_ERROR_PARAMETER; + } + const std::string dirName(param->dir_name->data, static_cast(nameEnd - param->dir_name->data)); + if (dirName.empty() || dirName == "." || dirName == ".." || dirName.find_first_of("/\\:") != std::string::npos) { + return SAVE_DATA_ERROR_PARAMETER; + } + if (!std::filesystem::is_directory(save_root() + "/" + dirName)) { + return SAVE_DATA_ERROR_NOT_FOUND; + } + return SAVE_DATA_OK; +} + +int APS5_VABI sceSaveDataConvert(const SaveDataConvert* param) { + const int rc = convert(param); + SAVEDATA_TRACE("convert user=%d dir=%s -> 0x%08x", param != nullptr ? param->user_id : -1, param != nullptr && param->dir_name != nullptr ? param->dir_name->data : "(null)", static_cast(rc)); + return rc; } int APS5_VABI sceSaveDataGetConvertProgress() { @@ -754,4 +806,16 @@ int APS5_VABI sceSaveDataCancel() { NotImplemented_nid_no_patch(__func__); return 0; } + +int APS5_VABI sceSaveDataDirNameSearch2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +APS5_EXPORT("RvID8BVeg+g", sceSaveDataUnknown_RvID8BVeg); +int APS5_VABI sceSaveDataUnknown_RvID8BVeg() { + NotImplemented_nid_no_patch("RvID8BVeg+g"); + return 0; +} + } diff --git a/core/libs/prx/libSceSaveData.native/SaveData.hpp b/core/libs/prx/libSceSaveData.native/SaveData.hpp index 20d02ad5a..ceffb09c0 100644 --- a/core/libs/prx/libSceSaveData.native/SaveData.hpp +++ b/core/libs/prx/libSceSaveData.native/SaveData.hpp @@ -33,6 +33,14 @@ constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_DETAIL = 3; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_USER_PARAM = 4; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_MTIME = 5; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_DIRNAME = 0; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_USER_PARAM = 1; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_BLOCKS = 2; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_MTIME = 3; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_FREE_BLOCKS = 5; +constexpr std::uint32_t SAVE_DATA_SORT_ORDER_ASCENT = 0; +constexpr std::uint32_t SAVE_DATA_SORT_ORDER_DESCENT = 1; + constexpr std::size_t SAVE_DATA_MOUNT_SLOTS = 16; struct MountSlot { diff --git a/core/libs/prx/libSceSysmodule/CMakeLists.txt b/core/libs/prx/libSceSysmodule/CMakeLists.txt index 5f204ccf4..b03a2433c 100644 --- a/core/libs/prx/libSceSysmodule/CMakeLists.txt +++ b/core/libs/prx/libSceSysmodule/CMakeLists.txt @@ -12,9 +12,11 @@ if(BUILD_TESTING) add_custom_target(guest_sysmodule_runtime COMMAND "${CMAKE_COMMAND}" -E copy_if_different "$" "${guestSysmoduleRun}/libs/libSceFiber.prx" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "$" "${guestSysmoduleRun}/libs/libSceAudioPropagation.prx" VERBATIM ) - add_dependencies(guest_sysmodule_runtime libSceFiber) + add_dependencies(guest_sysmodule_runtime libSceFiber libSceAudioPropagation) add_dependencies(guest_sysmodule_tests guest_sysmodule_runtime) add_test(NAME guest_sysmodule COMMAND guest_sysmodule_tests) set_tests_properties(guest_sysmodule PROPERTIES WORKING_DIRECTORY ${guestSysmoduleRun}) diff --git a/core/libs/prx/libSceSysmodule/ModuleTable.hpp b/core/libs/prx/libSceSysmodule/ModuleTable.hpp index 14642ee07..67003e4f3 100644 --- a/core/libs/prx/libSceSysmodule/ModuleTable.hpp +++ b/core/libs/prx/libSceSysmodule/ModuleTable.hpp @@ -149,12 +149,15 @@ inline const std::unordered_map kModuleTable = { {0x00000109, "libSceHdrScopes"}, {0x0000010c, "libSceCesCs"}, {0x0000010d, "libScePlayerInvitationDialog"}, + {0x0000010f, "libSceShare"}, {0x00000110, "libSceNpTrophy2"}, {0x00000112, "libSceNpSessionSignaling"}, {0x00000113, "libSceNpEntitlementAccess"}, + {0x00000114, "libSceVoiceChat"}, {0x00000115, "libSceNpCppWebApi"}, {0x00000116, "libSceHubAppUtil"}, {0x0000011a, "libSceNpPartner001"}, + {0x00000120, "libSceAudioPropagation"}, {0x0000012f, "libSceFontGs"}, {0x00000135, "libSceFontGsm"}, {0x00000138, "libSceNpPartnerSubscription"}, diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index da7831be4..fc2b46ac9 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -92,7 +92,8 @@ int APS5_VABI sceSystemServiceParamGetInt(int paramId, int* value) { return SYSTEM_SERVICE_ERROR_PARAMETER; } switch (paramId) { - case SYSTEM_SERVICE_PARAM_ID_LANG: *value = ConsoleLanguage(); break; + case SYSTEM_SERVICE_PARAM_ID_LANG: + case SYSTEM_SERVICE_PARAM_ID_400: *value = ConsoleLanguage(); break; case SYSTEM_SERVICE_PARAM_ID_DATE_FORMAT: *value = SYSTEM_SERVICE_PARAM_DATE_FORMAT_DDMMYYYY; break; case SYSTEM_SERVICE_PARAM_ID_TIME_FORMAT: *value = SYSTEM_SERVICE_PARAM_TIME_FORMAT_24HOUR; break; case SYSTEM_SERVICE_PARAM_ID_TIME_ZONE: *value = 0; break; diff --git a/core/libs/prx/libSceSystemService/SystemService.hpp b/core/libs/prx/libSceSystemService/SystemService.hpp index 01f6f34f2..99b73cc0b 100644 --- a/core/libs/prx/libSceSystemService/SystemService.hpp +++ b/core/libs/prx/libSceSystemService/SystemService.hpp @@ -17,6 +17,7 @@ constexpr int SYSTEM_SERVICE_PARAM_ID_TIME_ZONE = 4; constexpr int SYSTEM_SERVICE_PARAM_ID_SUMMERTIME = 5; constexpr int SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME = 6; constexpr int SYSTEM_SERVICE_PARAM_ID_GAME_PARENTAL_LEVEL = 7; +constexpr int SYSTEM_SERVICE_PARAM_ID_400 = 400; constexpr int SYSTEM_SERVICE_PARAM_ID_ENTER_BUTTON_ASSIGN = 1000; constexpr std::size_t SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH = 65; diff --git a/core/libs/prx/libSceUserService/CMakeLists.txt b/core/libs/prx/libSceUserService/CMakeLists.txt index 444c378c2..ac0ab70e9 100644 --- a/core/libs/prx/libSceUserService/CMakeLists.txt +++ b/core/libs/prx/libSceUserService/CMakeLists.txt @@ -12,3 +12,14 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_user_service_user_name_tests ${LIBS_INCLUDE_DIR}/tests/GuestUserServiceUserName.cpp) + target_include_directories(guest_user_service_user_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_user_service_user_name_tests PRIVATE libSceUserService) + add_test(NAME guest_user_service_user_name COMMAND guest_user_service_user_name_tests) + if(WIN32) + set_tests_properties(guest_user_service_user_name PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index 2a54f7c9f..4996849ae 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -6,61 +6,52 @@ #include #include "prx/libSceUserService/UserService.hpp" +static int ReportAccessibilitySetting(int user_id, int32_t* setting, int32_t value) { + if (user_id != USER_SERVICE_INITIAL_USER_ID) { + return USER_SERVICE_ERROR_NOT_LOGGED_IN; + } + if (setting == nullptr) { + return USER_SERVICE_ERROR_INVALID_ARGUMENT; + } + *setting = value; + return USER_SERVICE_OK; +} + extern "C" { int APS5_VABI sceUserServiceGetAccessibilityChatTranscription(int user_id, int32_t* chat_transcription) { - if (chat_transcription == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { - return USER_SERVICE_ERROR_INVALID_ARGUMENT; - } - *chat_transcription = 0; - return USER_SERVICE_OK; + return ReportAccessibilitySetting(user_id, chat_transcription, 0); } int APS5_VABI sceUserServiceGetAccessibilityPressAndHoldDelay(int user_id, int32_t* press_and_hold_delay) { - if (press_and_hold_delay == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { - return USER_SERVICE_ERROR_INVALID_ARGUMENT; - } - *press_and_hold_delay = 0; - return USER_SERVICE_OK; + return ReportAccessibilitySetting(user_id, press_and_hold_delay, 0); } int APS5_VABI sceUserServiceGetAccessibilityTriggerEffect(int user_id, int32_t* trigger_effect) { - if (trigger_effect == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { - return USER_SERVICE_ERROR_INVALID_ARGUMENT; - } - *trigger_effect = 0; - return USER_SERVICE_OK; + return ReportAccessibilitySetting(user_id, trigger_effect, 1); } int APS5_VABI sceUserServiceGetAccessibilityVibration(int user_id, int32_t* vibration) { - if (vibration == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { - return USER_SERVICE_ERROR_INVALID_ARGUMENT; - } - *vibration = 0; - return USER_SERVICE_OK; + return ReportAccessibilitySetting(user_id, vibration, 1); } int APS5_VABI sceUserServiceGetAccessibilityZoomEnabled(int user_id, int32_t* zoom_enabled) { - if (zoom_enabled == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { - return USER_SERVICE_ERROR_INVALID_ARGUMENT; - } - *zoom_enabled = 0; - return USER_SERVICE_OK; + return ReportAccessibilitySetting(user_id, zoom_enabled, 0); } int APS5_VABI sceUserServiceGetAccessibilityZoomFollowFocus(int user_id, int32_t* zoom_follow_focus) { - if (zoom_follow_focus == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { - return USER_SERVICE_ERROR_INVALID_ARGUMENT; - } - *zoom_follow_focus = 0; - return USER_SERVICE_OK; + return ReportAccessibilitySetting(user_id, zoom_follow_focus, 1); } int APS5_VABI sceUserServiceGetAgeLevel(int user_id, uint32_t* age_level) { - if (age_level == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { + if (user_id != USER_SERVICE_INITIAL_USER_ID) { + return USER_SERVICE_ERROR_NOT_LOGGED_IN; + } + if (age_level == nullptr) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } - *age_level = 0; + constexpr std::uint32_t Unrestricted = 0xFFFFFFFFu; + *age_level = Unrestricted; return USER_SERVICE_OK; } @@ -117,10 +108,14 @@ int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* user_ } int APS5_VABI sceUserServiceGetUserName(int user_id, char* name, size_t size) { - if (name == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { + if (name == nullptr) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } + if (user_id != USER_SERVICE_INITIAL_USER_ID) { + return USER_SERVICE_ERROR_UNKNOWN_USER; + } if (size < USER_SERVICE_MAX_USER_NAME_LENGTH + 1) { + std::memset(name, 0, size); return USER_SERVICE_ERROR_BUFFER_TOO_SHORT; } std::strncpy(name, USER_SERVICE_INITIAL_USER_NAME, size); diff --git a/core/libs/prx/libSceUserService/UserService.hpp b/core/libs/prx/libSceUserService/UserService.hpp index dcc140dad..342b82052 100644 --- a/core/libs/prx/libSceUserService/UserService.hpp +++ b/core/libs/prx/libSceUserService/UserService.hpp @@ -7,9 +7,11 @@ constexpr int USER_SERVICE_OK = 0; constexpr int USER_SERVICE_ERROR_INVALID_ARGUMENT = -2137653243; constexpr int USER_SERVICE_ERROR_NO_EVENT = -2137653241; +constexpr int USER_SERVICE_ERROR_NOT_LOGGED_IN = -2137653239; constexpr int USER_SERVICE_USER_ID_INVALID = -1; constexpr int USER_SERVICE_INITIAL_USER_ID = 0x10000000; constexpr int USER_SERVICE_ERROR_BUFFER_TOO_SHORT = -2137653238; +constexpr int USER_SERVICE_ERROR_UNKNOWN_USER = static_cast(0x80960105u); constexpr std::size_t USER_SERVICE_MAX_USER_NAME_LENGTH = 16; constexpr const char* USER_SERVICE_INITIAL_USER_NAME = "Player"; diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index fe35a3262..042deeed6 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -58,7 +58,7 @@ if(BUILD_TESTING) "SDL video driver for VideoOut flip tests") set_target_properties(video_out_flip_tests PROPERTIES EXCLUDE_FROM_ALL OFF) - foreach(flipMode IN ITEMS lifetime reopen decode controls overlay) + foreach(flipMode IN ITEMS lifetime reopen decode controls overlay unopened) if(flipMode STREQUAL "lifetime") add_test(NAME video_out_flip_${flipMode} COMMAND video_out_flip_tests) else() @@ -72,10 +72,30 @@ if(BUILD_TESTING) endforeach() endif() +if(BUILD_TESTING AND NOT WIN32) + add_test_executable(video_out_frame_dump_queue_tests tests/FrameDumpQueue.cpp) + target_include_directories(video_out_frame_dump_queue_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) + target_link_libraries(video_out_frame_dump_queue_tests PRIVATE libSceVideoOut libSceAgcDriver libkernel libc) + set_target_properties(video_out_frame_dump_queue_tests PROPERTIES CXX_EXTENSIONS OFF) + add_test(NAME video_out_frame_dump_queue COMMAND video_out_frame_dump_queue_tests) + set_tests_properties(video_out_frame_dump_queue PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 + ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER};APS5_DUMP_FRAMES=4;APS5_DUMP_SCALE=4") +endif() + if(BUILD_TESTING) add_test_executable(pad_sdl_tests tests/PadSdl.cpp) target_link_libraries(pad_sdl_tests PRIVATE SDL2-static) add_test(NAME pad_sdl COMMAND pad_sdl_tests) + add_test_executable(pad_input_mapping_tests tests/InputMapping.cpp src/InputMapping.cpp) + target_include_directories(pad_input_mapping_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include) + target_link_libraries(pad_input_mapping_tests PRIVATE SDL2-static libc) + set_target_properties(pad_input_mapping_tests PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(pad_input_mapping_tests) + add_test(NAME pad_input_mapping COMMAND pad_input_mapping_tests) + if(WIN32) + set_tests_properties(pad_input_mapping PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() if(BUILD_TESTING) diff --git a/core/libs/prx/libSceVideoOut/include/PadInput.hpp b/core/libs/prx/libSceVideoOut/include/PadInput.hpp index f0cbea89c..ce5188877 100644 --- a/core/libs/prx/libSceVideoOut/include/PadInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/PadInput.hpp @@ -19,6 +19,7 @@ public: void Update(); private: + explicit PadInput(Pad::InputConfiguration configuration); void publish(); void setMouseMode(bool enabled); void openFirstAvailableController(); @@ -29,6 +30,7 @@ private: void enableSensors(); std::vector bindings; + Pad::InputSettings settings; std::vector pressed; std::vector wheelReleaseTimes; std::array mouseStick{128, 128}; diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index 567481a0d..8cfcc7299 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -173,6 +173,7 @@ struct FlipRequest final : AgcDriver::IFlipRequest, std::enable_shared_from_this uint32_t height = 0; VideoOutBuffer buffer; BufferAttributeGroup group; + bool groupCaptured = false; bool reserved = false; bool unregistered = false; bool ready = false; diff --git a/core/libs/prx/libSceVideoOut/src/Buffer.cpp b/core/libs/prx/libSceVideoOut/src/Buffer.cpp index 965e37b3d..82b62b6c2 100644 --- a/core/libs/prx/libSceVideoOut/src/Buffer.cpp +++ b/core/libs/prx/libSceVideoOut/src/Buffer.cpp @@ -134,7 +134,6 @@ int APS5_VABI sceVideoOutSubmitChangeBufferAttribute2(int handle, int setIndex, group.attribute = *attribute; for (int i = 0; i < VIDEO_OUT_BUFFER_NUM_MAX; ++i) { if (cfg->buffers[i].groupIndex != setIndex) continue; - if (cfg->bufferPending[i] != 0) throw std::runtime_error("VideoOut: buffer attributes are in use by a pending flip"); const auto display = DescribeVideoOutBuffer(cfg->buffers[i], group); AgcDriverCheckGuestMemory_nid_postfix(reinterpret_cast(display.address), AgcDriverDisplayBufferSize_nid_postfix(display), 65536); } diff --git a/core/libs/prx/libSceVideoOut/src/Event.cpp b/core/libs/prx/libSceVideoOut/src/Event.cpp index a4853cd0a..a2a425f24 100644 --- a/core/libs/prx/libSceVideoOut/src/Event.cpp +++ b/core/libs/prx/libSceVideoOut/src/Event.cpp @@ -18,6 +18,7 @@ static std::vector* getEventList(VideoOutConfig& cfg, int16_t } static int registerVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind, void* udata) { + if (!VideoOutDriver::Get().HasConfig(handle)) return VIDEO_OUT_ERROR_INVALID_HANDLE; auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); diff --git a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp index d3d3d7275..b407bfeb2 100644 --- a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include @@ -121,6 +123,63 @@ Pad::InputBinding parseBinding(const InputAction& action, std::string_view sourc throw std::runtime_error("source type must be KEY, MOUSE or WHEEL"); } +enum class InputSetting { + LeftStickDeadzone, + RightStickDeadzone, + MouseSensitivity +}; + +struct InputSettingName { + std::string_view name; + InputSetting setting; +}; + +constexpr auto settingNames = std::array{ + InputSettingName{"LeftStickDeadzone", InputSetting::LeftStickDeadzone}, + InputSettingName{"RightStickDeadzone", InputSetting::RightStickDeadzone}, + InputSettingName{"MouseSensitivity", InputSetting::MouseSensitivity} +}; + +const InputSettingName* findSetting(std::string_view name) { + const auto normalized = upper(name); + for (const auto& setting : settingNames) { + if (upper(setting.name) == normalized) return &setting; + } + return nullptr; +} + +int parseDeadzonePercent(std::string_view value) { + int parsed = 0; + const auto end = value.data() + value.size(); + const auto result = std::from_chars(value.data(), end, parsed); + if (result.ec != std::errc{} || result.ptr != end || parsed < 0 || parsed > Pad::MaxStickDeadzonePercent) { + throw std::runtime_error("deadzone must be a whole percentage from 0 to " + std::to_string(Pad::MaxStickDeadzonePercent)); + } + return parsed; +} + +double parseMouseSensitivity(std::string_view value) { + double parsed = 0.0; + const auto end = value.data() + value.size(); + const auto result = std::from_chars(value.data(), end, parsed); + if (result.ec != std::errc{} || result.ptr != end || !(parsed > 0.0) || parsed > Pad::MaxMouseSensitivity) { + throw std::runtime_error("mouse sensitivity must be a number greater than 0 and at most 10"); + } + return parsed; +} + +void applySetting(Pad::InputSettings& settings, InputSetting setting, std::string_view value) { + switch (setting) { + case InputSetting::LeftStickDeadzone: settings.leftStickDeadzonePercent = parseDeadzonePercent(value); break; + case InputSetting::RightStickDeadzone: settings.rightStickDeadzonePercent = parseDeadzonePercent(value); break; + case InputSetting::MouseSensitivity: settings.mouseSensitivity = parseMouseSensitivity(value); break; + } +} + +std::uint8_t stickByte(std::int32_t axis) { + return static_cast(((axis + 32768) * 255 + 32767) / 65535); +} + [[noreturn]] void invalidLine(const std::filesystem::path& path, std::size_t line, const std::string& reason) { throw std::runtime_error("Pad: invalid input mapping " + path.string() + ":" + std::to_string(line) + ": " + reason); } @@ -133,8 +192,24 @@ std::filesystem::path defaultConfigPath() { } -std::vector Pad::LoadInputMapping() { - std::vector bindings(InputMapping.begin(), InputMapping.end()); +std::array Pad::StickWithDeadzone(std::int16_t x, std::int16_t y, int deadzonePercent) { + if (deadzonePercent < 0 || deadzonePercent > MaxStickDeadzonePercent) throw std::invalid_argument("Pad: stick deadzone out of range"); + if (deadzonePercent == 0) return {stickByte(x), stickByte(y)}; + const double normalizedX = std::max(x / 32767.0, -1.0); + const double normalizedY = std::max(y / 32767.0, -1.0); + const double magnitude = std::hypot(normalizedX, normalizedY); + const double deadzone = deadzonePercent / 100.0; + if (magnitude <= deadzone) return {stickByte(0), stickByte(0)}; + const double scale = (magnitude - deadzone) / (1.0 - deadzone) / magnitude; + const auto axis = [scale](double value) { + return stickByte(static_cast(std::lround(std::clamp(value * scale, -1.0, 1.0) * 32767.0))); + }; + return {axis(normalizedX), axis(normalizedY)}; +} + +Pad::InputConfiguration Pad::LoadInputMapping() { + InputConfiguration configuration{{InputMapping.begin(), InputMapping.end()}, {}}; + auto& bindings = configuration.bindings; const char* configuredPath = std::getenv("ANYPS5_INPUT_CONFIG"); const bool explicitPath = configuredPath != nullptr && configuredPath[0] != '\0'; const std::filesystem::path path = explicitPath ? configuredPath : defaultConfigPath(); @@ -143,10 +218,11 @@ std::vector Pad::LoadInputMapping() { if (explicitPath || std::filesystem::exists(path)) { throw std::runtime_error("Pad: cannot read input mapping '" + path.string() + "'"); } - return bindings; + return configuration; } std::unordered_set overriddenActions; + std::array configuredSettings{}; std::string line; std::size_t lineNumber = 0; while (std::getline(file, line)) { @@ -160,6 +236,17 @@ std::vector Pad::LoadInputMapping() { const auto actionName = trim(content.substr(0, separator)); const auto source = trim(content.substr(separator + 1)); + if (const auto* setting = findSetting(actionName)) { + auto& configured = configuredSettings[static_cast(setting->setting)]; + if (configured) invalidLine(path, lineNumber, std::string(setting->name) + " is set more than once"); + configured = true; + try { + applySetting(configuration.settings, setting->setting, source); + } catch (const std::runtime_error& error) { + invalidLine(path, lineNumber, error.what()); + } + continue; + } const auto* action = findAction(actionName); if (action == nullptr) invalidLine(path, lineNumber, "unknown action '" + std::string(actionName) + "'"); if (source.empty()) invalidLine(path, lineNumber, "source is empty"); @@ -182,5 +269,5 @@ std::vector Pad::LoadInputMapping() { } if (file.bad()) throw std::runtime_error("Pad: cannot read input mapping '" + path.string() + "'"); - return bindings; + return configuration; } diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index ab3aa11a8..5e5f7690a 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "SDL.h" #include "prx/libSceVideoOut/include/PadInput.hpp" @@ -13,8 +14,11 @@ #include "prx/libScePad/include/PadInputTypes.hpp" #include "prx/libc/include/General.hpp" -PadInput::PadInput() - : bindings(Pad::LoadInputMapping()), pressed(bindings.size()), wheelReleaseTimes(bindings.size()) { +PadInput::PadInput() : PadInput(Pad::LoadInputMapping()) { +} + +PadInput::PadInput(Pad::InputConfiguration configuration) + : bindings(std::move(configuration.bindings)), settings(configuration.settings), pressed(bindings.size()), wheelReleaseTimes(bindings.size()) { openFirstAvailableController(); } @@ -153,16 +157,12 @@ PadInputState PadInput::sampleController() const { if (result.analogButtonsL2 != 0) result.buttons |= static_cast(Pad::PadButton::L2); if (result.analogButtonsR2 != 0) result.buttons |= static_cast(Pad::PadButton::R2); - const auto stickValue = [this](SDL_GameControllerAxis axis) { - const auto value = static_cast(SDL_GameControllerGetAxis(controller, axis)) + 32768; - return static_cast((value * 255 + 32767) / 65535); - }; - result.sticks = { - stickValue(SDL_CONTROLLER_AXIS_LEFTX), - stickValue(SDL_CONTROLLER_AXIS_LEFTY), - stickValue(SDL_CONTROLLER_AXIS_RIGHTX), - stickValue(SDL_CONTROLLER_AXIS_RIGHTY) + const auto stick = [this](SDL_GameControllerAxis xAxis, SDL_GameControllerAxis yAxis, int deadzonePercent) { + return Pad::StickWithDeadzone(SDL_GameControllerGetAxis(controller, xAxis), SDL_GameControllerGetAxis(controller, yAxis), deadzonePercent); }; + const auto left = stick(SDL_CONTROLLER_AXIS_LEFTX, SDL_CONTROLLER_AXIS_LEFTY, settings.leftStickDeadzonePercent); + const auto right = stick(SDL_CONTROLLER_AXIS_RIGHTX, SDL_CONTROLLER_AXIS_RIGHTY, settings.rightStickDeadzonePercent); + result.sticks = {left[0], left[1], right[0], right[1]}; switch (SDL_GameControllerGetType(controller)) { case SDL_CONTROLLER_TYPE_PS5: result.deviceKind = 1; break; case SDL_CONTROLLER_TYPE_PS4: result.deviceKind = 2; break; @@ -292,7 +292,7 @@ void PadInput::Update() { mouseStick = {128, 128}; if (deltaX != 0 || deltaY != 0) { const double distance = std::hypot(deltaX, deltaY); - const double scale = std::clamp(distance * Pad::MouseSensitivity + 16.0, 64.0, 128.0) / distance; + const double scale = std::clamp(distance * settings.mouseSensitivity + 16.0, 64.0, 128.0) / distance; const auto mapAxis = [scale](int delta) { return static_cast(std::clamp(128L + std::lround(delta * scale), 0L, 255L)); }; mouseStick = {mapAxis(deltaX), mapAxis(deltaY)}; } diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index bd1c9456c..f605f9dbb 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -84,7 +84,7 @@ void resolveFlipBuffer(FlipRequest& request, const VideoOutConfig& cfg) { return; } require(request.buffer.groupIndex < VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX, "invalid buffer group"); - request.group = cfg.groups[request.buffer.groupIndex]; + if (!request.groupCaptured) request.group = cfg.groups[request.buffer.groupIndex]; require(request.group.occupied, "buffer group is not registered"); require(request.buffer.dataAddress != 0, "null registered buffer address"); static_cast(DescribeVideoOutBuffer(request.buffer, request.group)); @@ -152,6 +152,10 @@ public: ++queue->reservations; ++cfg->flipStatus.flipPendingNum; if (info.index >= 0) ++cfg->bufferPending[info.index]; + if (info.index >= 0 && cfg->buffers[info.index].Occupied() && cfg->buffers[info.index].groupIndex < VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX && cfg->groups[cfg->buffers[info.index].groupIndex].occupied) { + request->group = cfg->groups[cfg->buffers[info.index].groupIndex]; + request->groupCaptured = true; + } request->reserved = true; return request; } diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index c2edb3103..0e25c5fd1 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -86,8 +86,16 @@ void testLifetime(bool reopen) { std::lock_guard lock(cfg->mutex); check(cfg->flipStatus.flipPendingNum == 1 && cfg->flipStatus.count == 0, "reservation status is wrong"); } + auto smaller = attribute; + smaller.width = 32; + smaller.height = 32; + check(sceVideoOutSubmitChangeBufferAttribute2(handle, 0, &smaller, nullptr) == 0, "attributes of a buffer with a pending flip could not be changed"); + { + std::lock_guard lock(cfg->mutex); + check(cfg->groups[0].attribute.width == 32 && cfg->groups[0].attribute.height == 32 && cfg->flipStatus.flipPendingNum == 1, "the attribute change did not reach later flips or dropped the pending one"); + } attribute.dcc_control = 1; - check(expectFailure([&] { sceVideoOutSubmitChangeBufferAttribute2(handle, 0, &attribute, nullptr); }).find("pending flip") != std::string::npos, "pending attributes were changed"); + check(expectFailure([&] { sceVideoOutSubmitChangeBufferAttribute2(handle, 0, &attribute, nullptr); }).find("DCC") != std::string::npos, "an invalid attribute change was accepted"); check(expectFailure([&] { sceVideoOutUnregisterBuffers(handle, 0); }).find("pending flip") != std::string::npos, "pending buffer was unregistered"); check(sceVideoOutWaitVblank(handle) == 0 && sceVideoOutWaitVblank(handle) == 0, "vblank stalled behind the GPU queue"); std::shared_ptr replacement; @@ -649,6 +657,19 @@ void testFlipAfterGpuWork() { LibcRunShutdown_nid_postfix(); } +void testUnopenedEventRegistration() { + KernelEqueue eq = 0; + check(sceKernelCreateEqueue(&eq, "unopened") == 0, "the event queue was not created"); + check(sceVideoOutAddFlipEvent(eq, 0, nullptr) == VIDEO_OUT_ERROR_INVALID_HANDLE, "flip event on an unopened port was accepted"); + check(sceVideoOutAddVblankEvent(eq, 1, nullptr) == VIDEO_OUT_ERROR_INVALID_HANDLE, "vblank event on an unopened port was accepted"); + const int handle = sceVideoOutOpen(255, 0, 0, nullptr); + check(sceVideoOutAddFlipEvent(eq, handle, nullptr) == 0, "flip event on an open port was refused"); + sceVideoOutClose(handle); + check(sceVideoOutAddFlipEvent(eq, handle, nullptr) == VIDEO_OUT_ERROR_INVALID_HANDLE, "flip event on a closed port was accepted"); + check(sceKernelDeleteEqueue(eq) == 0, "the event queue was not deleted"); + LibcRunShutdown_nid_postfix(); +} + void testOneDevice() { const int handle = sceVideoOutOpen(255, 0, 0, nullptr); auto cfg = VideoOutDriver::Get().GetConfig(handle); @@ -694,6 +715,7 @@ int main(int argc, char** argv) { else if (argc == 2 && std::string(argv[1]) == "aftergpu") testFlipAfterGpuWork(); else if (argc == 2 && std::string(argv[1]) == "unavailable") testUnavailable(); else if (argc == 2 && std::string(argv[1]) == "onedevice") testOneDevice(); + else if (argc == 2 && std::string(argv[1]) == "unopened") testUnopenedEventRegistration(); else if (argc == 2 && std::string(argv[1]) == "overlay") testOverlay(); else if (argc == 2 && std::string(argv[1]) == "deferred") testDeferredBuffer(); else if (argc == 2 && std::string(argv[1]) == "unregistered") testUnregisteredBuffer(); diff --git a/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp b/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp new file mode 100644 index 000000000..2c5b74ad1 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp @@ -0,0 +1,160 @@ +#include "prx/libSceVideoOut/include/VideoOutDriver.hpp" +#include "prx/libSceVideoOut/include/Buffer.hpp" +#include "prx/libSceVideoOut/include/Output.hpp" +#include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "tests/VideoOutTestEnvironment.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef main + +namespace { + +constexpr std::uint32_t Width = 3840; +constexpr std::uint32_t Height = 2160; +constexpr std::uint32_t Scale = 4; +constexpr std::uint32_t DumpWidth = Width / Scale; +constexpr std::uint32_t DumpHeight = Height / Scale; +constexpr std::size_t DumpBytes = 54 + static_cast(DumpWidth) * DumpHeight * 4; +constexpr int Frames = 4; + +void check(bool condition, const char* reason) { + if (!condition) throw std::runtime_error(reason); +} + +std::string frameName(int index) { + char name[32]; + std::snprintf(name, sizeof(name), "frame_%03d.bmp", index); + return name; +} + +class HeldDump final { + int descriptor; + +public: + explicit HeldDump(int index) { + const auto name = frameName(index); + check(mkfifo(name.c_str(), 0600) == 0, "a frame dump pipe could not be created"); + descriptor = open(name.c_str(), O_RDWR); + check(descriptor >= 0, "a frame dump pipe could not be opened"); + } + + ~HeldDump() { close(descriptor); } + + HeldDump(const HeldDump&) = delete; + HeldDump& operator=(const HeldDump&) = delete; + + std::vector Release() const { + std::vector dump(DumpBytes); + for (std::size_t done = 0; done < dump.size();) { + pollfd waiting{descriptor, POLLIN, 0}; + check(poll(&waiting, 1, 15000) == 1, "a queued frame dump was not written"); + const auto count = read(descriptor, dump.data() + done, dump.size() - done); + check(count > 0, "a frame dump pipe could not be read"); + done += static_cast(count); + } + return dump; + } +}; + +std::vector readDump(int index) { + std::ifstream file(frameName(index), std::ios::binary); + check(file.is_open(), "a frame dump is missing"); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +void checkDump(std::span dump, std::span decoded) { + check(dump.size() == DumpBytes, "a frame dump has the wrong size"); + std::uint32_t extent[2]; + std::memcpy(extent, dump.data() + 18, sizeof(extent)); + check(extent[0] == DumpWidth && extent[1] == static_cast(-static_cast(DumpHeight)), "a frame dump has the wrong extent"); + for (std::uint32_t y = 0; y < DumpHeight; ++y) { + for (std::uint32_t x = 0; x < DumpWidth; ++x) { + const auto dumped = dump.data() + 54 + (static_cast(y) * DumpWidth + x) * 4; + const auto source = decoded.data() + (static_cast(y) * Scale * Width + static_cast(x) * Scale) * 4; + check(std::memcmp(dumped, source, 4) == 0, "a frame dump pixel differs from the display buffer"); + } + } +} + +void testDumpQueue() { + const int handle = sceVideoOutOpen(255, 0, 0, nullptr); + auto cfg = VideoOutDriver::Get().GetConfig(handle); + std::vector allocation(static_cast((Width + 127) / 128) * ((Height + 127) / 128) * 65536 + 65535); + const auto address = reinterpret_cast(allocation.data()); + const std::span storage{allocation.data() + ((65536u - (address & 65535u)) & 65535u), allocation.size() - 65535u}; + const AgcDriver::DisplayBuffer display{reinterpret_cast(storage.data()), 0x8000000000000000ull, Width, Height}; + check(AgcDriver::DisplayBufferSize(display) == storage.size(), "incorrect padded display footprint"); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto offset = AgcDriver::DisplayBufferOffset(display, x, y); + storage[offset] = static_cast(x & 255u); + storage[offset + 1] = static_cast(y & 255u); + storage[offset + 2] = static_cast((x >> 8u) | ((y >> 8u) << 4u)); + storage[offset + 3] = std::byte{255}; + } + } + const auto decoded = AgcDriver::ReadDisplayBuffer(display); + VideoOutBuffers buffer{storage.data(), nullptr, {nullptr, nullptr}}; + VideoOutBufferAttribute2 attribute{}; + sceVideoOutSetBufferAttribute2(&attribute, display.pixelFormat, 0, Width, Height, 0, 0, 0); + check(sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, 0, nullptr) == 0, "buffer registration failed"); + const HeldDump first(0); + const HeldDump last(Frames - 1); + std::exception_ptr failure; + try { + for (int frame = 0; frame < Frames; ++frame) { + check(sceVideoOutSubmitFlip(handle, 0, 1, frame) == 0, "the flip was not accepted"); + std::unique_lock lock(cfg->mutex); + check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return (cfg->failure && cfg->flipStatus.flipPendingNum == 0) || cfg->flipStatus.count == static_cast(frame) + 1; }), "the flip did not complete"); + if (cfg->failure) std::rethrow_exception(cfg->failure); + } + } catch (...) { + failure = std::current_exception(); + } + const auto firstDump = first.Release(); + if (failure) std::rethrow_exception(failure); + checkDump(firstDump, decoded); + const auto lastDump = last.Release(); + checkDump(lastDump, decoded); + for (int frame = 1; frame < Frames - 1; ++frame) checkDump(readDump(frame), decoded); + sceVideoOutUnregisterBuffers(handle, 0); + sceVideoOutClose(handle); + LibcRunShutdown_nid_postfix(); +} + +} + +int main() { + VideoOutTestEnvironment environment; + try { + testDumpQueue(); + std::puts("VideoOut frame dump queue test passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + try { LibcRunShutdown_nid_postfix(); } + catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); } + if (std::string(error.what()).find("Vulkan support") != std::string::npos && !std::getenv("ANYPS5_REQUIRE_DISPLAY")) { + std::printf("skipped, no display or Vulkan device: %s\n", error.what()); + return 77; + } + return 1; + } +} diff --git a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp new file mode 100644 index 000000000..0a67672ea --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp @@ -0,0 +1,119 @@ +#include "prx/libScePad/include/InputMapping.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +std::filesystem::path root; + +void useConfig(const std::string& text) { + const auto path = root / "anyps5-input.ini"; + { + std::ofstream file(path, std::ios::binary | std::ios::trunc); + file << text; + require(file.good(), "write the input mapping"); + } +#ifdef _WIN32 + require(_putenv_s("ANYPS5_INPUT_CONFIG", path.string().c_str()) == 0, "set ANYPS5_INPUT_CONFIG"); +#else + require(::setenv("ANYPS5_INPUT_CONFIG", path.string().c_str(), 1) == 0, "set ANYPS5_INPUT_CONFIG"); +#endif +} + +bool rejects(const std::string& text) { + useConfig(text); + try { + Pad::LoadInputMapping(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +std::uint8_t plainStickByte(std::int16_t axis) { + return static_cast(((static_cast(axis) + 32768) * 255 + 32767) / 65535); +} + +void settingsDefault() { + useConfig("Cross = KEY:F\n"); + const auto configuration = Pad::LoadInputMapping(); + require(configuration.settings.leftStickDeadzonePercent == 0 && configuration.settings.rightStickDeadzonePercent == 0, "deadzones default to 0"); + require(configuration.settings.mouseSensitivity == Pad::DefaultMouseSensitivity, "mouse sensitivity defaults to 1"); +} + +void settingsParse() { + useConfig("leftstickdeadzone = 12 ; worn stick\nRightStickDeadzone=5\nMouseSensitivity = 2.5\nCross = KEY:F\n"); + const auto configuration = Pad::LoadInputMapping(); + require(configuration.settings.leftStickDeadzonePercent == 12, "left deadzone is read case-insensitively"); + require(configuration.settings.rightStickDeadzonePercent == 5, "right deadzone is read"); + require(configuration.settings.mouseSensitivity == 2.5, "mouse sensitivity is read"); + bool crossOnF = false; + for (const auto& binding : configuration.bindings) crossOnF = crossOnF || (binding.control == Pad::InputControl::Button && binding.button == Pad::PadButton::Cross && binding.key == SDL_SCANCODE_F); + require(crossOnF, "bindings are read alongside settings"); +} + +void settingsReject() { + require(rejects("LeftStickDeadzone = 91\n"), "a deadzone above 90 is rejected"); + require(rejects("LeftStickDeadzone = -1\n"), "a negative deadzone is rejected"); + require(rejects("LeftStickDeadzone = 7.5\n"), "a fractional deadzone is rejected"); + require(rejects("RightStickDeadzone =\n"), "an empty deadzone is rejected"); + require(rejects("MouseSensitivity = 0\n"), "a zero mouse sensitivity is rejected"); + require(rejects("MouseSensitivity = 10.5\n"), "a mouse sensitivity above 10 is rejected"); + require(rejects("MouseSensitivity = fast\n"), "a non-numeric mouse sensitivity is rejected"); + require(rejects("MouseSensitivity = 1\nMouseSensitivity = 2\n"), "a setting given twice is rejected"); +} + +void deadzoneOffMatchesRawSticks() { + for (const std::int16_t axis : {std::int16_t{-32768}, std::int16_t{-20000}, std::int16_t{-1}, std::int16_t{0}, std::int16_t{1}, std::int16_t{1234}, std::int16_t{32767}}) { + const auto stick = Pad::StickWithDeadzone(axis, axis, 0); + require(stick[0] == plainStickByte(axis) && stick[1] == plainStickByte(axis), "a 0 deadzone keeps the raw mapping"); + } +} + +void deadzoneShapesSticks() { + require(Pad::StickWithDeadzone(3000, -3000, 15) == std::array{128, 128}, "a small deflection inside the deadzone is centered"); + require(Pad::StickWithDeadzone(32767, 0, 15) == std::array{255, 128}, "a full deflection still reaches the edge"); + require(Pad::StickWithDeadzone(-32768, 0, 15) == std::array{0, 128}, "a full negative deflection still reaches the edge"); + require(Pad::StickWithDeadzone(32767, 32767, 15) == std::array{255, 255}, "a full diagonal still reaches the corner"); + const auto justOutside = Pad::StickWithDeadzone(5100, 0, 15); + require(justOutside[0] >= 128 && justOutside[0] <= 130 && justOutside[1] == 128, "the output starts at the center just outside the deadzone"); + const auto half = Pad::StickWithDeadzone(16384, 0, 20); + require(half[0] > 128 && half[0] < plainStickByte(16384), "a deflection outside the deadzone is rescaled"); + bool rejected = false; + try { + Pad::StickWithDeadzone(0, 0, 91); + } catch (const std::invalid_argument&) { + rejected = true; + } + require(rejected, "an out-of-range deadzone is rejected"); +} + +} + +int main() try { + root = std::filesystem::temp_directory_path() / ("anyps5_input_mapping-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(root); + settingsDefault(); + settingsParse(); + settingsReject(); + deadzoneOffMatchesRawSticks(); + deadzoneShapesSticks(); + std::filesystem::remove_all(root); + std::cout << "input mapping tests passed\n"; + return 0; +} catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; +} diff --git a/core/libs/prx/libSceVoiceQoS/Export.cpp b/core/libs/prx/libSceVoiceQoS/Export.cpp index 4ab9fb411..e8213885a 100644 --- a/core/libs/prx/libSceVoiceQoS/Export.cpp +++ b/core/libs/prx/libSceVoiceQoS/Export.cpp @@ -9,6 +9,7 @@ namespace { constexpr std::int32_t VOICE_QOS_APP_TYPE_GAME = 0x20000000; constexpr std::int32_t VOICE_QOS_APP_TYPE_10000000 = 0x10000000; constexpr std::uint32_t VOICE_QOS_MEMORY_SIZE = 0x40000; +constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_NOT_INIT = static_cast(0x804E0901); constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID = static_cast(0x804E0902); constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_INITIALIZED = static_cast(0x804E0905); @@ -28,8 +29,10 @@ int APS5_VABI sceVoiceQoSInit(void* mem_block, uint32_t mem_size, int32_t app_ty } int APS5_VABI sceVoiceQoSEnd() { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_VOICE_ERROR_LIBVOICEQOS_NOT_INIT; + g_initialized = false; + return 0; } int APS5_VABI sceVoiceQoSCreateLocalEndpoint() { @@ -62,6 +65,11 @@ int APS5_VABI sceVoiceQoSDisconnect() { return 0; } +int APS5_VABI sceVoiceQoSSetConnectionAttribute() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceVoiceQoSSetLocalEndpointAttribute() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index 75a4fce48..42b258980 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -10,6 +10,11 @@ int APS5_VABI sceVrTracker2Finalize() { return 0; } +int APS5_VABI sceVrTracker2ResetLocalCoordinate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceVrTracker2GetPlayAreaOrientedBoundingBox() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index d68768c32..f631dc835 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -14,6 +14,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/StringExtensions.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Math.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Random.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/HostEntropy.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Time.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FileAndHeap.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/StdioExtras.cpp @@ -57,6 +58,16 @@ if(WIN32) endif() if(BUILD_TESTING) + add_test_executable(guest_array_callbacks_tests ${LIBS_INCLUDE_DIR}/tests/GuestArrayCallbacks.cpp) + target_include_directories(guest_array_callbacks_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_array_callbacks_tests PRIVATE libc) + configure_windows_unwind(guest_array_callbacks_tests) + add_test(NAME guest_array_callbacks COMMAND guest_array_callbacks_tests) + set_tests_properties(guest_array_callbacks PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_array_callbacks PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_time_conversion_tests ${LIBS_INCLUDE_DIR}/tests/GuestTimeConversion.cpp) target_include_directories(guest_time_conversion_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_time_conversion_tests PRIVATE libc) @@ -67,6 +78,16 @@ if(BUILD_TESTING) set_tests_properties(guest_time_conversion PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_mktime_tests ${LIBS_INCLUDE_DIR}/tests/GuestMktime.cpp) + target_include_directories(guest_mktime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_mktime_tests PRIVATE libc) + configure_windows_unwind(guest_mktime_tests) + add_test(NAME guest_mktime COMMAND guest_mktime_tests) + set_tests_properties(guest_mktime PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_mktime PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(precise_wait_tests ${LIBS_INCLUDE_DIR}/tests/PreciseWait.cpp) target_include_directories(precise_wait_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(precise_wait_tests PRIVATE libc) @@ -190,6 +211,16 @@ if(BUILD_TESTING) set_tests_properties(guest_libc_threads PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_libc_static_imports_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibcStaticImports.cpp) + target_include_directories(guest_libc_static_imports_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libc_static_imports_tests PRIVATE libc) + configure_windows_unwind(guest_libc_static_imports_tests) + add_test(NAME guest_libc_static_imports COMMAND guest_libc_static_imports_tests) + set_tests_properties(guest_libc_static_imports PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_libc_static_imports PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_fiopen_tests ${LIBS_INCLUDE_DIR}/tests/GuestFiopen.cpp) target_include_directories(guest_fiopen_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_fiopen_tests PRIVATE libc) diff --git a/core/libs/prx/libc/Export.cpp b/core/libs/prx/libc/Export.cpp index be68a6cab..cf23ee77b 100644 --- a/core/libs/prx/libc/Export.cpp +++ b/core/libs/prx/libc/Export.cpp @@ -32,4 +32,8 @@ std::uint64_t APS5_VABI libcCyberUnknown08(void) { return 0; } +APS5_EXPORT("H2e8t5ScQGc", libc___cxa_finalize); +void APS5_VABI libc___cxa_finalize(void* dsoHandle) { + CxaFinalize_nid_no_patch(dsoHandle); +} } diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 7a1b467be..f93727c73 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -42,7 +42,7 @@ class FileStream { std::byte _reserved[256 - sizeof(GuestFilePrefix)]{}; std::FILE* _handle; bool _dynamic; - bool encodingError = false; + bool guestError = false; public: explicit FileStream(std::FILE* handle, bool dynamic = false) : _handle(handle), _dynamic(dynamic) { @@ -72,7 +72,7 @@ public: bool Reopen(const std::filesystem::path& filename, const char* mode) { auto* previous = GetHandle(); _guest = {}; - encodingError = false; + guestError = false; #ifdef _WIN32 std::wstring wideMode; for (const char character : std::string_view(mode)) wideMode.push_back(static_cast(character)); @@ -94,7 +94,7 @@ public: _guest.readRemaining = 0; _guest.writeRemaining = 0; _guest.flags = static_cast((_guest.flags & ~0x60) | - (std::feof(GetHandle()) ? 0x20 : 0) | (std::ferror(GetHandle()) || encodingError ? 0x40 : 0)); + (std::feof(GetHandle()) ? 0x20 : 0) | (std::ferror(GetHandle()) || guestError ? 0x40 : 0)); } void SyncUnlockedStatus() { @@ -108,25 +108,26 @@ public: const bool error = ::ferror_unlocked(GetHandle()) != 0; #endif _guest.flags = static_cast((_guest.flags & ~0x60) | - (endOfFile ? 0x20 : 0) | (error || encodingError ? 0x40 : 0)); + (endOfFile ? 0x20 : 0) | (error || guestError ? 0x40 : 0)); } - void SetEncodingError() { - encodingError = true; + void SetError() { + guestError = true; SyncStatus(); } void ClearError() { std::clearerr(GetHandle()); - encodingError = false; + guestError = false; SyncStatus(); } - void Close() { + int Close() { GetHandle(); - if (std::fclose(std::exchange(_handle, nullptr)) != 0) throw std::runtime_error("FileStream: close failed"); + const int result = std::fclose(std::exchange(_handle, nullptr)); _guest.flags = 0; _guest.descriptor = -1; + return result; } }; static_assert(std::is_standard_layout_v); diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 2a2a9ed92..7de9ed967 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -24,6 +24,9 @@ extern "C" void RecordWrittenPath_nid_no_patch(const std::filesystem::path& path extern "C" std::vector WrittenPaths_nid_no_patch(); extern "C" void SyncWrittenPaths_nid_no_patch(); extern "C" void BlockPathAlias_nid_no_patch(const char* guestPrefix); +#ifdef _WIN32 +extern "C" int WindowsOpenError_nid_no_patch(unsigned long error); +#endif #define APS5_INVALID_ARG_EX throw std::invalid_argument(std::string(__func__) + ": invalid argument") diff --git a/core/libs/prx/libc/include/HostEntropy.hpp b/core/libs/prx/libc/include/HostEntropy.hpp new file mode 100644 index 000000000..3380f9023 --- /dev/null +++ b/core/libs/prx/libc/include/HostEntropy.hpp @@ -0,0 +1,8 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_HOSTENTROPY_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_HOSTENTROPY_HPP + +#include + +extern "C" void HostEntropy_nid_no_patch(void* buffer, std::size_t size); + +#endif diff --git a/core/libs/prx/libc/include/PreciseWait.hpp b/core/libs/prx/libc/include/PreciseWait.hpp index e9fc32421..73249384b 100644 --- a/core/libs/prx/libc/include/PreciseWait.hpp +++ b/core/libs/prx/libc/include/PreciseWait.hpp @@ -6,6 +6,7 @@ #ifdef _WIN32 #include +#include "prx/libc/include/HostThreadLocal.hpp" #endif inline constexpr unsigned long long kSubTickWaitUs = 2000; @@ -24,7 +25,7 @@ inline void PreciseSleepUs(unsigned long long micros) { } } }; - thread_local Timer timer; + auto& timer = HostThreadLocal(); if (timer.handle != nullptr) { LARGE_INTEGER due{}; due.QuadPart = -static_cast(micros * 10ULL); diff --git a/core/libs/prx/libc/include/SonyCrt.hpp b/core/libs/prx/libc/include/SonyCrt.hpp index 2016da156..12500b15d 100644 --- a/core/libs/prx/libc/include/SonyCrt.hpp +++ b/core/libs/prx/libc/include/SonyCrt.hpp @@ -125,10 +125,11 @@ inline int SonyWcscpyS(std::uint16_t* dest, std::size_t size, const std::uint16_ inline std::size_t SonyMbstowcs(std::uint16_t* dest, const char* src, std::size_t size) { if (!src) return 0; + if (!dest) return std::strlen(src); std::size_t converted = 0; while (converted < size) { const auto value = static_cast(src[converted]); - if (dest) dest[converted] = value; + dest[converted] = value; if (value == 0) return converted; ++converted; } @@ -194,6 +195,19 @@ inline unsigned __int128 SonyUdivti3(unsigned __int128 dividend, unsigned __int1 return quotient; } +inline __int128 SonyDivti3(__int128 dividend, __int128 divisor) { + if (divisor == 0) throw std::runtime_error("__divti3: integer divide by zero"); + const bool negative = (dividend < 0) != (divisor < 0); + const unsigned __int128 magnitudeDividend = dividend < 0 + ? (unsigned __int128)0 - (unsigned __int128)dividend + : (unsigned __int128)dividend; + const unsigned __int128 magnitudeDivisor = divisor < 0 + ? (unsigned __int128)0 - (unsigned __int128)divisor + : (unsigned __int128)divisor; + const unsigned __int128 magnitude = SonyUdivti3(magnitudeDividend, magnitudeDivisor); + return (__int128)(negative ? (unsigned __int128)0 - magnitude : magnitude); +} + } #endif diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 9d33b9b0b..acb82476a 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -82,6 +82,9 @@ public: split(cursor + done, pageBytes); void* page = reinterpret_cast(cursor + done); if (!map(section, GetCurrentProcess(), page, offset + done, pageBytes, MEM_REPLACE_PLACEHOLDER, PAGE_EXECUTE_READWRITE, nullptr, 0)) fail("map shared guest page"); + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(page, &memory, sizeof(memory)) != sizeof(memory)) fail("query shared guest page"); + if (memory.State == MEM_RESERVE && !VirtualAlloc(page, pageBytes, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) fail("commit shared guest page"); DWORD previous; if (!VirtualProtect(page, pageBytes, protection, &previous)) fail("protect shared guest page"); const auto key = std::make_pair(reinterpret_cast(section), offset + done); diff --git a/core/libs/prx/libc/src/AllocatingStrings.cpp b/core/libs/prx/libc/src/AllocatingStrings.cpp index 74779c475..2e91cc215 100644 --- a/core/libs/prx/libc/src/AllocatingStrings.cpp +++ b/core/libs/prx/libc/src/AllocatingStrings.cpp @@ -1,20 +1,15 @@ #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" +#include "SceTypes.hpp" #include #include -#include -#include #include -#include #include -#include - -#ifdef _WIN32 -#include "prx/libc/include/WindowsFormatting.hpp" -#endif extern "C" { +int APS5_VABI vasprintf_nid_postfix(char** destination, const char* format, VaList* args); + char* APS5_VABI strndup_nid_postfix(const char* source, std::size_t limit) { std::size_t length = 0; while (length < limit && source[length] != '\0') ++length; @@ -35,9 +30,6 @@ char* APS5_VABI strdup_nid_postfix(const char* source) { } int APS5_VABI asprintf_nid_postfix(char** destination, const char* format, ...) { - if (!destination) { errno = 22; return -1; } - *destination = nullptr; - if (!format) { errno = 22; return -1; } #ifdef _WIN32 __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); @@ -45,36 +37,7 @@ int APS5_VABI asprintf_nid_postfix(char** destination, const char* format, ...) std::va_list args; va_start(args, format); #endif - int result = -1; - try { -#ifdef _WIN32 - std::string text; - const int count = LibcDetail::FormatWindows(nullptr, 0, format, args, &text); - const char* source = text.c_str(); -#else - char* text = nullptr; - const int count = ::vasprintf(&text, format, args); - std::unique_ptr owner(text, std::free); - const char* source = text; -#endif - if (count >= 0) { - auto* output = static_cast(ApplicationHeapAllocate_nid_no_patch(static_cast(count) + 1)); - if (output) { - std::memcpy(output, source, static_cast(count) + 1); - *destination = output; - result = count; - } else errno = 12; - } - } catch (const std::bad_alloc&) { - errno = 12; - } catch (...) { -#ifdef _WIN32 - __builtin_sysv_va_end(args); -#else - va_end(args); -#endif - throw; - } + const int result = vasprintf_nid_postfix(destination, format, reinterpret_cast(args)); #ifdef _WIN32 __builtin_sysv_va_end(args); #else diff --git a/core/libs/prx/libc/src/ApplicationHeap.cpp b/core/libs/prx/libc/src/ApplicationHeap.cpp index f05e94132..e619429ee 100644 --- a/core/libs/prx/libc/src/ApplicationHeap.cpp +++ b/core/libs/prx/libc/src/ApplicationHeap.cpp @@ -176,7 +176,7 @@ void ApplicationHeapFree_nid_no_patch(void* pointer) { } void* ApplicationHeapReallocate_nid_no_patch(void* pointer, std::size_t bytes) { - if (bytes == 0) { + if (bytes == 0 && pointer != nullptr) { ApplicationHeapFree_nid_no_patch(pointer); return nullptr; } diff --git a/core/libs/prx/libc/src/Environment.cpp b/core/libs/prx/libc/src/Environment.cpp index 080c8b110..03996644b 100644 --- a/core/libs/prx/libc/src/Environment.cpp +++ b/core/libs/prx/libc/src/Environment.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -14,9 +15,9 @@ extern char** environ; namespace { struct EnvironmentEntry { - std::string owned; + std::unique_ptr owned; char* borrowed = nullptr; - char* Data() { return borrowed ? borrowed : owned.data(); } + char* Data() { return borrowed ? borrowed : owned->data(); } bool Matches(std::string_view name) { const char* data = Data(); const char* separator = std::strchr(data, '='); @@ -35,7 +36,7 @@ std::vector& Environment() { #endif if (source) for (; *source; ++source) { if (**source && **source != '=' && std::strchr(*source, '=')) - result.push_back({*source, nullptr}); + result.push_back({std::make_unique(*source), nullptr}); } return result; }(); @@ -49,7 +50,7 @@ int EnvironmentError(int error) { errno = error; return -1; } extern "C" { char* APS5_VABI getenv_nid_postfix(const char* name) { - if (!ValidEnvironmentName(name)) return nullptr; + if (!ValidEnvironmentName(name)) { errno = 22; return nullptr; } try { std::lock_guard lock(environmentMutex); for (auto& entry : Environment()) @@ -66,7 +67,7 @@ int APS5_VABI setenv_nid_postfix(const char* name, const char* value, int overwr auto found = std::find_if(entries.begin(), entries.end(), [name](auto& entry) { return entry.Matches(name); }); if (found != entries.end() && !overwrite) return 0; - EnvironmentEntry replacement{std::string(name) + '=' + value, nullptr}; + EnvironmentEntry replacement{std::make_unique(std::string(name) + '=' + value), nullptr}; if (found != entries.end()) *found = std::move(replacement); else entries.push_back(std::move(replacement)); return 0; @@ -93,7 +94,7 @@ int APS5_VABI putenv_nid_postfix(char* text) { const std::string_view name(text, separator - text); auto found = std::find_if(entries.begin(), entries.end(), [name](auto& entry) { return entry.Matches(name); }); - EnvironmentEntry replacement{{}, text}; + EnvironmentEntry replacement{nullptr, text}; if (found != entries.end()) *found = std::move(replacement); else entries.push_back(std::move(replacement)); return 0; diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index d6e255918..aba269af5 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -6,9 +6,19 @@ #include #include #include +#include +#include #include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#endif #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/ApplicationHeap.hpp" @@ -43,9 +53,29 @@ static std::optional ParseFileMode(const char* mode) { static std::FILE* OpenNative(const std::filesystem::path& path, const std::string& nativeMode) { #ifdef _WIN32 - std::wstring wideMode; - for (const char character : nativeMode) wideMode.push_back(static_cast(character)); - return ::_wfopen(path.c_str(), wideMode.c_str()); + const char access = nativeMode[0]; + const bool update = nativeMode.find('+') != std::string::npos; + const DWORD desired = update ? GENERIC_READ | GENERIC_WRITE : access == 'r' ? GENERIC_READ : GENERIC_WRITE; + const DWORD disposition = access == 'r' ? OPEN_EXISTING + : nativeMode.find('x') != std::string::npos ? CREATE_NEW : access == 'w' ? CREATE_ALWAYS : OPEN_ALWAYS; + SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; + const auto handle = CreateFileW(path.c_str(), desired, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &security, + disposition, FILE_ATTRIBUTE_NORMAL, nullptr); + if (handle == INVALID_HANDLE_VALUE) { + errno = WindowsOpenError_nid_no_patch(GetLastError()); + return nullptr; + } + const int descriptor = ::_open_osfhandle(reinterpret_cast(handle), access == 'a' ? _O_APPEND : 0); + if (descriptor < 0) { + CloseHandle(handle); + return nullptr; + } + std::wstring streamMode; + for (const char character : nativeMode) + if (character != 'x') streamMode.push_back(static_cast(character)); + std::FILE* stream = ::_wfdopen(descriptor, streamMode.c_str()); + if (!stream) ::_close(descriptor); + return stream; #else return std::fopen(path.c_str(), nativeMode.c_str()); #endif @@ -58,18 +88,17 @@ extern "C" { FileStream* APS5_VABI fdopen_nid_postfix(int descriptor, const char* mode) { if (descriptor < 0) { errno = 9; return nullptr; } if (!mode) { errno = 22; return nullptr; } - const char* supported[] = {"r", "w", "a", "rb", "wb", "ab", "r+", "w+", "a+", - "rb+", "wb+", "ab+", "r+b", "w+b", "a+b"}; - bool valid = false; - for (const auto* candidate : supported) if (std::strcmp(mode, candidate) == 0) valid = true; - if (!valid) { errno = 22; return nullptr; } + const auto parsed = ParseFileMode(mode); + if (!parsed) { errno = 22; return nullptr; } try { + auto nativeMode = parsed->native; + std::erase(nativeMode, 'x'); const auto release = [](void* pointer) { ::operator delete(pointer); }; std::unique_ptr storage(::operator new(sizeof(FileStream)), release); #ifdef _WIN32 - auto* native = ::_fdopen(descriptor, mode); + auto* native = ::_fdopen(descriptor, nativeMode.c_str()); #else - auto* native = ::fdopen(descriptor, mode); + auto* native = ::fdopen(descriptor, nativeMode.c_str()); #endif if (!native) return nullptr; auto* stream = new (storage.get()) FileStream(native, true); @@ -148,7 +177,9 @@ FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode) int APS5_VABI fopen_s_nid_postfix(FileStream** result, const char* filename, const char* mode) { constexpr int GuestEinval = 22; constexpr int GuestEnoent = 2; - if (!result || !filename || !mode) return GuestEinval; + if (!result) return GuestEinval; + *result = nullptr; + if (!filename || !mode) return GuestEinval; *result = fopen_nid_postfix(filename, mode); return *result ? 0 : GuestEnoent; } @@ -156,8 +187,7 @@ int APS5_VABI fopen_s_nid_postfix(FileStream** result, const char* filename, con int APS5_VABI fclose_nid_postfix(FileStream* stream) { GetNativeStream(stream); std::unique_ptr owner(stream->IsDynamic() ? stream : nullptr); - stream->Close(); - return 0; + return stream->Close(); } int APS5_VABI fseek_nid_postfix(FileStream* stream, std::int64_t offset, int origin); @@ -294,6 +324,17 @@ void* APS5_VABI realloc_nid_postfix(void* ptr, size_t newSize) { return ApplicationHeapReallocate_nid_no_patch(ptr, newSize); } +void* APS5_VABI reallocf_nid_postfix(void* ptr, size_t newSize) { + try { + return ApplicationHeapReallocate_nid_no_patch(ptr, newSize); + } catch (const std::bad_alloc&) { + } catch (const std::length_error&) { + } + ApplicationHeapFree_nid_no_patch(ptr); + errno = 12; + return nullptr; +} + void* APS5_VABI memalign_nid_postfix(size_t alignment, size_t size) { return ApplicationHeapAlign_nid_no_patch(alignment, size); } diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index b59bbb2c8..7982d342a 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -35,6 +35,24 @@ bool HasGuestWideFormat(const char* format) { } return false; } + +void RejectGuestWideScan(const char* format) { + if (format == nullptr) return; + while ((format = std::strchr(format, '%')) != nullptr) { + ++format; + if (*format == '%') { ++format; continue; } + while (*format && std::strchr("-+ #0'0123456789.*$", *format)) ++format; + if (*format == 'S' || *format == 'C' || (*format == 'l' && format[1] != '\0' && std::strchr("sc[", format[1]))) + NotImplemented_nid_no_patch("scanf 16-bit wide conversion on Linux"); + if (*format == '[') { + ++format; + if (*format == '^') ++format; + if (*format == ']') ++format; + while (*format && *format != ']') ++format; + } + if (*format) ++format; + } +} #endif std::string HostLength(const std::string& length, char conversion) { @@ -236,6 +254,7 @@ int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...) { const int result = LibcDetail::ScanFileWindows_nid_no_patch(native, format, args); __builtin_sysv_va_end(args); #else + RejectGuestWideScan(format); std::va_list args; va_start(args, format); const int result = std::vfscanf(native, format, args); @@ -319,6 +338,7 @@ int APS5_VABI vsscanf_nid_postfix(const char* input, const char* format, VaList* #ifdef _WIN32 return LibcDetail::ScanWindows(input, format, args); #else + RejectGuestWideScan(format); std::va_list copy; va_copy(copy, *reinterpret_cast(args)); const int result = std::vsscanf(input, format, copy); @@ -337,6 +357,7 @@ int APS5_VABI sscanf_nid_postfix(const char* input, const char* format, ...) { } #else int APS5_VABI sscanf_nid_postfix(const char* input, const char* format, ...) { + RejectGuestWideScan(format); std::va_list args; va_start(args, format); const int result = std::vsscanf(input, format, args); @@ -358,6 +379,7 @@ int APS5_VABI sscanf_s_nid_postfix(const char* buffer, const char* format, ...) #else int APS5_VABI sscanf_s_nid_postfix(const char* buffer, const char* format, ...) { + RejectGuestWideScan(format); std::va_list args; va_start(args, format); const int result = ScanGuest(buffer, format, true, [&] { return va_arg(args, void*); }, [&] { return va_arg(args, unsigned int); }); @@ -436,6 +458,7 @@ int APS5_VABI printf_s_nid_postfix(const char* format, ...) { #else int APS5_VABI vsscanf_s_nid_postfix(const char* buffer, const char* format, VaList* args) { + RejectGuestWideScan(format); LibcDetail::FormatArguments arguments(args); return ScanGuest(buffer, format, true, [&] { return arguments.Next(); }, [&] { return arguments.Next(); }); } diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index fefc33af4..a2a57370b 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -401,6 +401,41 @@ extern "C" char* APS5_VABI getcwd_nid_postfix(char* buffer, std::size_t size) { catch (const std::filesystem::filesystem_error& error) { errno = DirectoryFailure(error.code()); return nullptr; } } +#ifdef _WIN32 +extern "C" int WindowsOpenError_nid_no_patch(unsigned long error) { + switch (error) { + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + case ERROR_INVALID_DRIVE: + case ERROR_BAD_NETPATH: + case ERROR_BAD_NET_NAME: + case ERROR_BAD_PATHNAME: + case ERROR_FILENAME_EXCED_RANGE: + return ENOENT; + case ERROR_ACCESS_DENIED: + case ERROR_SHARING_VIOLATION: + case ERROR_LOCK_VIOLATION: + case ERROR_WRITE_PROTECT: + case ERROR_NETWORK_ACCESS_DENIED: + case ERROR_CANNOT_MAKE: + case ERROR_DRIVE_LOCKED: + return EACCES; + case ERROR_FILE_EXISTS: + case ERROR_ALREADY_EXISTS: + return EEXIST; + case ERROR_TOO_MANY_OPEN_FILES: + return EMFILE; + case ERROR_NOT_ENOUGH_MEMORY: + case ERROR_OUTOFMEMORY: + return ENOMEM; + case ERROR_DISK_FULL: + return ENOSPC; + default: + return EINVAL; + } +} +#endif + extern "C" void NotImplemented_nid_no_patch(const char* funcName) { throw std::runtime_error(std::string(funcName) + " not implemented"); } diff --git a/core/libs/prx/libc/src/HostEntropy.cpp b/core/libs/prx/libc/src/HostEntropy.cpp new file mode 100644 index 000000000..d87362a22 --- /dev/null +++ b/core/libs/prx/libc/src/HostEntropy.cpp @@ -0,0 +1,33 @@ +#ifdef _WIN32 +#define _CRT_RAND_S +#endif +#include "prx/libc/include/HostEntropy.hpp" + +#include +#include +#include +#include +#include +#ifdef __APPLE__ +#include +#elif !defined(_WIN32) +#include +#endif + +void HostEntropy_nid_no_patch(void* buffer, std::size_t size) { + auto* bytes = static_cast(buffer); +#ifdef _WIN32 + for (std::size_t offset = 0; offset < size; offset += sizeof(unsigned int)) { + unsigned int value = 0; + if (const errno_t error = rand_s(&value); error != 0) + throw std::system_error(error, std::generic_category(), "HostEntropy: rand_s failed"); + std::memcpy(bytes + offset, &value, std::min(sizeof(value), size - offset)); + } +#else + constexpr std::size_t MaxChunk = 256; + for (std::size_t offset = 0; offset < size; offset += MaxChunk) { + if (getentropy(bytes + offset, std::min(MaxChunk, size - offset)) != 0) + throw std::system_error(errno, std::generic_category(), "HostEntropy: getentropy failed"); + } +#endif +} diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index d0a70841a..a21ce8227 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include #include @@ -323,8 +323,15 @@ mbstate_t* APS5_VABI _Getpwcstate_nid_postfix() { return &state; } -wint_t APS5_VABI _Towctrans_nid_postfix(wint_t c, wctrans_t desc) { - return std::towctrans(c, desc); +std::uint32_t APS5_VABI _Towctrans_nid_postfix(std::uint32_t c, std::uint64_t desc) { + constexpr std::uint64_t ToLower = 1; + constexpr std::uint64_t ToUpper = 2; + if (desc != ToLower && desc != ToUpper) { + errno = 22; + return c; + } + if (c > UCHAR_MAX) return c; + return static_cast((desc == ToLower ? g_lowerTable : g_upperTable)[c + 1]); } // Locale ids and stream objects the title imports besides the ones above; the streams are zeroed diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index f6fcf1608..9ecf79e14 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include "prx/libc/include/General.hpp" @@ -24,6 +25,106 @@ std::int64_t RoundToInt64(TFloat value) { return static_cast(rounded); } +float SinhfExpm1(float x) { + constexpr float Ln2High = 6.9313812256e-01f, Ln2Low = 9.0580006145e-06f, InverseLn2 = 1.4426950216e+00f; + constexpr float Q1 = -3.3333212137e-2f, Q2 = 1.5807170421e-3f; + const std::uint32_t magnitude = std::bit_cast(x); + int k = 0; + float c = 0.0f; + if (magnitude > 0x3eb17218u) { + float high; + float low; + if (magnitude < 0x3f851592u) { + high = x - Ln2High; + low = Ln2Low; + k = 1; + } else { + k = static_cast(InverseLn2 * x + 0.5f); + const float t = static_cast(k); + high = x - t * Ln2High; + low = t * Ln2Low; + } + x = high - low; + c = (high - x) - low; + } + const float halfX = 0.5f * x; + const float halfSquare = x * halfX; + const float r1 = 1.0f + halfSquare * (Q1 + halfSquare * Q2); + const float t = 3.0f - r1 * halfX; + float e = halfSquare * ((r1 - t) / (6.0f - x * t)); + if (k == 0) return x - (x * e - halfSquare); + e = x * (e - c) - c; + e -= halfSquare; + if (k == 1) { + if (x < -0.25f) return -2.0f * (e - (x + 0.5f)); + return 1.0f + 2.0f * (x - e); + } + const float twoPowK = std::bit_cast(static_cast(0x7f + k) << 23); + const float oneMinusTwoPowMinusK = std::bit_cast(0x3f800000u - (0x1000000u >> k)); + return (oneMinusTwoPowMinusK - (e - x)) * twoPowK; +} + +float SinhfExp(float x) { + constexpr float Ln2High = 6.9314575195e-01f, Ln2Low = 1.4286067653e-06f, InverseLn2 = 1.4426950216e+00f; + constexpr float P1 = 1.6666625440e-1f, P2 = -2.7667332906e-3f; + const int k = static_cast(InverseLn2 * x + 0.5f); + const float kFloat = static_cast(k); + const float high = x - kFloat * Ln2High; + const float low = kFloat * Ln2Low; + x = high - low; + const float t = x * x; + const float c = x - t * (P1 + t * P2); + const float y = 1.0f - ((low - (x * c) / (2.0f - c)) - high); + if (k == 128) return y * 2.0f * 0x1p127f; + return y * std::bit_cast(static_cast(0x7f + k) << 23); +} + +static_assert(std::numeric_limits::digits == 64, "guest long double is the x87 80-bit format"); + +struct ExtendedParts { + std::uint64_t mantissa; + std::uint16_t signExponent; +}; + +ExtendedParts SplitExtended(long double value) { + unsigned char bytes[sizeof(long double)]; + std::memcpy(bytes, &value, sizeof(value)); + ExtendedParts parts; + std::memcpy(&parts.mantissa, bytes, sizeof(parts.mantissa)); + std::memcpy(&parts.signExponent, bytes + sizeof(parts.mantissa), sizeof(parts.signExponent)); + return parts; +} + +long double ClearLowMantissaWord(long double value) { + unsigned char bytes[sizeof(long double)]; + std::memcpy(bytes, &value, sizeof(value)); + std::memset(bytes, 0, sizeof(std::uint32_t)); + std::memcpy(&value, bytes, sizeof(value)); + return value; +} + +constexpr long double AsinNumeratorCoefficients[] = { + 1.66666666666666666631e-01L, -4.16313987993683104320e-01L, 3.69068046323246813704e-01L, -1.36213932016738603108e-01L, + 1.78324189708471965733e-02L, -2.19216428382605211588e-04L, -7.10526623669075243183e-06L, +}; +constexpr long double AsinDenominatorCoefficients[] = { + -2.94788392796209867269e+00L, 3.27309890266528636716e+00L, -1.68285799854822427013e+00L, 3.90699412641738801874e-01L, + -3.14365703596053263322e-02L, +}; +constexpr long double HalfPiHigh = 1.57079632679489661926e+00L; +constexpr long double HalfPiLow = -2.50827880633416601173e-20L; +constexpr long double QuarterPiHigh = 7.85398163397448309628e-01L; + +long double AsinNumerator(long double t) { + const auto& p = AsinNumeratorCoefficients; + return t * (p[0] + t * (p[1] + t * (p[2] + t * (p[3] + t * (p[4] + t * (p[5] + t * p[6])))))); +} + +long double AsinDenominator(long double t) { + const auto& q = AsinDenominatorCoefficients; + return 1.0L + t * (q[0] + t * (q[1] + t * (q[2] + t * (q[3] + t * q[4])))); +} + } extern "C" { @@ -59,7 +160,12 @@ double APS5_VABI hypot_nid_postfix(double x, double y) { float APS5_VABI tanf_nid_postfix(float x) { return std::tan(x); } float APS5_VABI log10f_nid_postfix(float x) { return std::log10(x); } float APS5_VABI logbf_nid_postfix(float x) { return std::logb(x); } -double APS5_VABI exp2_nid_postfix(double x) { return std::exp2(x); } +double APS5_VABI exp2_nid_postfix(double x) { + int saved_errno = errno; + double result = std::exp2(x); + errno = saved_errno; + return result; +} double APS5_VABI ldexp_nid_postfix(double x, int exponent) { return std::ldexp(x, exponent); } double APS5_VABI scalbn_nid_postfix(double x, int exponent) { return std::scalbn(x, exponent); } float APS5_VABI scalbnf_nid_postfix(float x, int exponent) { return std::scalbn(x, exponent); } @@ -74,6 +180,7 @@ int APS5_VABI __isfinitef_nid_postfix(float x) { return std::isfinite(x) ? 1 : 0 int APS5_VABI __isnormal_nid_postfix(double x) { return std::isnormal(x) ? 1 : 0; } int APS5_VABI __isnormalf_nid_postfix(float x) { return std::isnormal(x) ? 1 : 0; } int APS5_VABI __isinff_nid_postfix(float x) { return std::isinf(x) ? 1 : 0; } +float APS5_VABI nearbyintf_nid_postfix(float x) { return std::nearbyintf(x); } double APS5_VABI cbrt_nid_postfix(double x) { return std::cbrt(x); } double APS5_VABI asin_nid_postfix(double x) { @@ -92,6 +199,36 @@ double APS5_VABI trunc_nid_postfix(double x) { return std::trunc(x); } double APS5_VABI fma_nid_postfix(double x, double y, double z) { return std::fma(x, y, z); } long double APS5_VABI acosl_nid_postfix(long double x) { return std::acos(x); } +long double APS5_VABI asinl_nid_postfix(long double x) { + const ExtendedParts parts = SplitExtended(x); + const int exponent = parts.signExponent & 0x7fff; + if (exponent >= 0x3fff) { + if (exponent == 0x3fff && (parts.mantissa & 0x7fffffffffffffffull) == 0) return x * HalfPiHigh + x * HalfPiLow; + std::feraiseexcept(FE_INVALID); + errno = EDOM; + return (x - x) / (x - x); + } + if (exponent < 0x3ffe) { + if (exponent < 0x3fff - 32 && 1.0e300L + x > 1.0L) return x; + const long double t = x * x; + return x + x * (AsinNumerator(t) / AsinDenominator(t)); + } + const long double t = (1.0L - std::fabs(x)) * 0.5L; + const long double p = AsinNumerator(t); + const long double q = AsinDenominator(t); + const long double s = std::sqrt(t); + long double result; + if ((parts.mantissa >> 32) >= 0xf3333333u) { + result = HalfPiHigh - (2.0L * (s + s * (p / q)) - HalfPiLow); + } else { + const long double w = ClearLowMantissaWord(s); + const long double c = (t - w * w) / (s + w); + const long double r = p / q; + result = QuarterPiHigh - ((2.0L * s * r - (HalfPiLow - 2.0L * c)) - (QuarterPiHigh - 2.0L * w)); + } + return (parts.signExponent & 0x8000) != 0 ? -result : result; +} + long double APS5_VABI frexpl_nid_postfix(long double x, int* exponent) { if (!std::isfinite(x)) return x; return std::frexp(x, exponent); @@ -118,6 +255,27 @@ double APS5_VABI cos_nid_postfix(double x) { return std::cos(x); } double APS5_VABI sinh_nid_postfix(double x) { return std::sinh(x); } double APS5_VABI cosh_nid_postfix(double x) { return std::cosh(x); } float APS5_VABI coshf_nid_postfix(float x) { return std::cosh(x); } + +float APS5_VABI sinhf_nid_postfix(float x) { + const std::uint32_t magnitude = std::bit_cast(x) & 0x7fffffffu; + if (magnitude >= 0x7f800000u) return x + x; + const float half = std::signbit(x) ? -0.5f : 0.5f; + const float absolute = std::fabs(x); + if (magnitude < 0x41100000u) { + if (magnitude < 0x39800000u && 1.0e37f + x > 1.0f) return x; + const float t = SinhfExpm1(absolute); + if (magnitude < 0x3f800000u) return half * (2.0f * t - t * t / (t + 1.0f)); + return half * (t + t / (t + 1.0f)); + } + if (magnitude < 0x42b17217u) return half * SinhfExp(absolute); + if (magnitude <= 0x42b2d4fcu) { + const float w = SinhfExp(0.5f * absolute); + return half * w * w; + } + errno = ERANGE; + return x * 1.0e37f; +} + double APS5_VABI asinh_nid_postfix(double x) { return std::asinh(x); } float APS5_VABI asinhf_nid_postfix(float x) { return std::asinh(x); } double APS5_VABI acosh_nid_postfix(double x) { return std::acosh(x); } @@ -151,7 +309,12 @@ float APS5_VABI logf_nid_postfix(float x) { return std::log(x); } float APS5_VABI log2f_nid_postfix(float x) { return std::log2(x); } double APS5_VABI log10_nid_postfix(double x) { return std::log10(x); } float APS5_VABI ldexpf_nid_postfix(float x, int exp) { return std::ldexp(x, exp); } -double APS5_VABI fmod_nid_postfix(double x, double y) { return std::fmod(x, y); } +double APS5_VABI fmod_nid_postfix(double x, double y) { + int saved_errno = errno; + double result = std::fmod(x, y); + errno = saved_errno; + return result; +} float APS5_VABI roundf_nid_postfix(float x) { return std::round(x); } double APS5_VABI round_nid_postfix(double x) { return std::round(x); } float APS5_VABI cbrtf_nid_postfix(float x) { return std::cbrt(x); } @@ -174,6 +337,22 @@ double APS5_VABI modf_nid_postfix(double x, double* integral) { return std::modf float APS5_VABI modff_nid_postfix(float x, float* integral) { return std::modf(x, integral); } double APS5_VABI tanh_nid_postfix(double x) { return std::tanh(x); } float APS5_VABI tanhf_nid_postfix(float x) { return std::tanh(x); } +long double APS5_VABI atanl_nid_postfix(long double x) { return std::atan(x); } +long double APS5_VABI sinl_nid_postfix(long double x) { return std::sin(x); } +long double APS5_VABI cosl_nid_postfix(long double x) { return std::cos(x); } +long double APS5_VABI tanl_nid_postfix(long double x) { return std::tan(x); } +long double APS5_VABI sinhl_nid_postfix(long double x) { return std::sinh(x); } +long double APS5_VABI coshl_nid_postfix(long double x) { return std::cosh(x); } +long double APS5_VABI tanhl_nid_postfix(long double x) { return std::tanh(x); } +long double APS5_VABI expl_nid_postfix(long double x) { return std::exp(x); } +long double APS5_VABI exp2l_nid_postfix(long double x) { return std::exp2(x); } +long double APS5_VABI logl_nid_postfix(long double x) { return std::log(x); } +long double APS5_VABI log10l_nid_postfix(long double x) { return std::log10(x); } +long double APS5_VABI powl_nid_postfix(long double base, long double exp) { return std::pow(base, exp); } +long double APS5_VABI ldexpl_nid_postfix(long double x, int exp) { return std::ldexp(x, exp); } +long double APS5_VABI truncl_nid_postfix(long double x) { return std::trunc(x); } +double APS5_VABI logb_nid_postfix(double x) { return std::logb(x); } +double APS5_VABI remainder_nid_postfix(double x, double y) { return std::remainder(x, y); } float APS5_VABI _FSinh_nid_postfix(float x, float y) { return y * std::sinh(x); } float APS5_VABI _FCosh_nid_postfix(float x, float y) { return y * std::cosh(x); } @@ -193,6 +372,7 @@ short APS5_VABI _FDtest_nid_postfix(const float* value) { return Finite; } int APS5_VABI __isnanf_nid_postfix(float x) { return std::isnan(x) ? 1 : 0; } +int APS5_VABI isnanf_nid_postfix(float x) { return __isnanf_nid_postfix(x); } int APS5_VABI __fpclassifyf_nid_postfix(float x) { constexpr int Infinite = 0x01, NotANumber = 0x02, Normal = 0x04, Subnormal = 0x08, Zero = 0x10; std::uint32_t bits; @@ -204,6 +384,10 @@ int APS5_VABI __fpclassifyf_nid_postfix(float x) { return Normal; } int APS5_VABI __signbitf_nid_postfix(float x) { return std::signbit(x) ? 1 : 0; } +int APS5_VABI __signbitl_nid_postfix(long double x) { + static_assert(sizeof(long double) == 16, "guest long double requires x87 extended precision storage"); + return std::signbit(x) ? 1 : 0; +} static std::mutex g_randLock; static std::uint32_t g_randState = 1; diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index 0e2341b02..fd810843e 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -18,6 +18,8 @@ #include "prx/libc/include/General.hpp" #include "SceTypes.hpp" +extern "C" int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *func)(void*), void* arg, void* dsoHandle); + namespace { std::mutex shutdownMutex; @@ -31,17 +33,9 @@ std::stop_source shutdownSource; std::atomic exitRequested{false}; std::atomic exitStarted{false}; using GuestExitCallback = void (APS5_VABI*)(); -std::mutex exitCallbackMutex; -std::vector exitCallbacks; -void runGuestExitCallback() { - GuestExitCallback callback; - { - std::lock_guard lock(exitCallbackMutex); - callback = exitCallbacks.back(); - exitCallbacks.pop_back(); - } - callback(); +void APS5_VABI RunGuestExitCallback(void* callback) { + reinterpret_cast(callback)(); } } @@ -166,11 +160,7 @@ int* APS5_VABI __error_nid_postfix() { int APS5_VABI atexit_nid_postfix(GuestExitCallback func) { if (func == nullptr) return 0; - std::lock_guard lock(exitCallbackMutex); - exitCallbacks.push_back(func); - const int result = std::atexit(runGuestExitCallback); - if (result != 0) exitCallbacks.pop_back(); - return result; + return __cxa_atexit_nid_postfix(RunGuestExitCallback, reinterpret_cast(func), nullptr); } namespace { diff --git a/core/libs/prx/libc/src/RuntimeSupport.cpp b/core/libs/prx/libc/src/RuntimeSupport.cpp index 69be0d848..0d83c1743 100644 --- a/core/libs/prx/libc/src/RuntimeSupport.cpp +++ b/core/libs/prx/libc/src/RuntimeSupport.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #ifdef _WIN32 @@ -20,6 +19,7 @@ #include "prx/libc/include/SonyCrt.hpp" #include "prx/libc/include/specifics/gcc/AtomicOps.hpp" #include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/HostEntropy.hpp" namespace { @@ -123,10 +123,9 @@ std::uintmax_t APS5_VABI strtoumax_nid_postfix(const char* str, char** endptr, i } unsigned int APS5_VABI _ZSt14_Random_devicev_nid_postfix() { - static std::random_device device; - static std::mutex mutex; - std::lock_guard lock(mutex); - return device(); + unsigned int value = 0; + HostEntropy_nid_no_patch(&value, sizeof(value)); + return value; } [[noreturn]] void APS5_VABI _ZSt14_Throw_C_errori_nid_postfix(int code) { @@ -181,6 +180,10 @@ void APS5_VABI _Unlocksyslock_nid_postfix() { g_sysLock.unlock(); } +__int128 APS5_VABI __divti3_nid_postfix(__int128 dividend, __int128 divisor) { + return SonyDivti3(dividend, divisor); +} + unsigned __int128 APS5_VABI __udivti3_nid_postfix(unsigned __int128 dividend, unsigned __int128 divisor) { return SonyUdivti3(dividend, divisor); } diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index 1d51cdb37..69e24de20 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -27,7 +27,7 @@ std::int32_t APS5_VABI ungetwc_nid_postfix(std::int32_t value, FileStream* strea if (value == -1) return -1; if (value < 0 || value > 255) { errno = 86; - stream->SetEncodingError(); + stream->SetError(); return -1; } const int result = std::ungetc(value, GetNativeStream(stream)); @@ -69,6 +69,11 @@ int APS5_VABI ungetc_nid_postfix(int value, FileStream* stream) { return result; } char* APS5_VABI fgets_nid_postfix(char* buffer, int size, FileStream* stream) { + if (size <= 0) { + stream->SetError(); + errno = 22; + return nullptr; + } auto* result = std::fgets(buffer, size, GetNativeStream(stream)); stream->SyncStatus(); return result; diff --git a/core/libs/prx/libc/src/StringExtensions.cpp b/core/libs/prx/libc/src/StringExtensions.cpp index eaa15adc5..8ea34819c 100644 --- a/core/libs/prx/libc/src/StringExtensions.cpp +++ b/core/libs/prx/libc/src/StringExtensions.cpp @@ -46,27 +46,25 @@ char* APS5_VABI dirname_nid_postfix(const char* path) { } auto length = std::strlen(path); - if (length >= sizeof(buffer)) { - errno = 63; - return nullptr; - } - std::memcpy(buffer, path, length + 1); - while (length > 0 && buffer[length - 1] == '/') --length; + while (length > 0 && path[length - 1] == '/') --length; if (length == 0) { buffer[0] = '/'; buffer[1] = '\0'; return buffer; } - buffer[length] = '\0'; - auto separator = length; - while (separator > 0 && buffer[separator - 1] != '/') --separator; + while (separator > 0 && path[separator - 1] != '/') --separator; if (separator == 0) { buffer[0] = '.'; buffer[1] = '\0'; return buffer; } - while (separator > 1 && buffer[separator - 1] == '/') --separator; + while (separator > 1 && path[separator - 1] == '/') --separator; + if (separator >= sizeof(buffer)) { + errno = 63; + return nullptr; + } + std::memcpy(buffer, path, separator); buffer[separator] = '\0'; return buffer; } diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 564458ace..ebb046b03 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -45,6 +45,14 @@ auto ParseAsciiPrefix(const char16_t* text, char16_t** end, TParse parse) { return value; } +template +TFloat ParseFloat(const char* text, char** end, TFloat (*parse)(const char*, char**)) { + char* parsedEnd = nullptr; + const TFloat value = parse(text, &parsedEnd); + if (end != nullptr) *end = parsedEnd; + return parsedEnd == text ? TFloat{0} : value; +} + } extern "C" { @@ -165,14 +173,16 @@ unsigned long long APS5_VABI strtoull_nid_postfix(const char* str, char** endptr } double APS5_VABI strtod_nid_postfix(const char* str, char** endptr) { - return std::strtod(str, endptr); + return ParseFloat(str, endptr, std::strtod); } -double APS5_VABI atof_nid_postfix(const char* str) { return std::atof(str); } -float APS5_VABI strtof_nid_postfix(const char* str, char** endptr) { return std::strtof(str, endptr); } +double APS5_VABI atof_nid_postfix(const char* str) { return strtod_nid_postfix(str, nullptr); } +float APS5_VABI strtof_nid_postfix(const char* str, char** endptr) { + return ParseFloat(str, endptr, std::strtof); +} long double APS5_VABI strtold_nid_postfix(const char* str, char** endptr) { static_assert(sizeof(long double) == 16, "Guest long double requires x87 extended precision storage"); - return std::strtold(str, endptr); + return ParseFloat(str, endptr, std::strtold); } int APS5_VABI atoi_nid_postfix(const char* str) { @@ -221,24 +231,21 @@ char16_t* APS5_VABI wmemmove_nid_postfix(char16_t* dest, const char16_t* src, si extern "C" { int APS5_VABI strcasecmp_nid_postfix(const char* s1, const char* s2) { - while (*s1 && *s2) { - unsigned char a = static_cast(std::tolower(static_cast(*s1))); - unsigned char b = static_cast(std::tolower(static_cast(*s2))); - if (a != b) return a - b; - ++s1; ++s2; + for (;; ++s1, ++s2) { + const int a = std::tolower(static_cast(*s1)); + const int b = std::tolower(static_cast(*s2)); + if (a != b || *s1 == 0) return a - b; } - return static_cast(*s1) - static_cast(*s2); } int APS5_VABI strncasecmp_nid_postfix(const char* s1, const char* s2, size_t n) { - while (n && *s1 && *s2) { - unsigned char a = static_cast(std::tolower(static_cast(*s1))); - unsigned char b = static_cast(std::tolower(static_cast(*s2))); + for (; n != 0; ++s1, ++s2, --n) { + const int a = std::tolower(static_cast(*s1)); + const int b = std::tolower(static_cast(*s2)); if (a != b) return a - b; - ++s1; ++s2; --n; + if (*s1 == 0) break; } - if (!n) return 0; - return static_cast(*s1) - static_cast(*s2); + return 0; } int APS5_VABI bcmp_nid_postfix(const void* s1, const void* s2, size_t n) { @@ -452,17 +459,17 @@ char16_t* APS5_VABI wmemset_nid_postfix(char16_t* s, char16_t c, size_t n) { } double APS5_VABI wcstod_nid_postfix(const char16_t* str, char16_t** endptr) { - return ParseAsciiPrefix(str, endptr, [](const char* text, char** end) { return std::strtod(text, end); }); + return ParseAsciiPrefix(str, endptr, [](const char* text, char** end) { return ParseFloat(text, end, std::strtod); }); } float APS5_VABI wcstof_nid_postfix(const char16_t* str, char16_t** endptr) { - return ParseAsciiPrefix(str, endptr, [](const char* text, char** end) { return std::strtof(text, end); }); + return ParseAsciiPrefix(str, endptr, [](const char* text, char** end) { return ParseFloat(text, end, std::strtof); }); } long double APS5_VABI wcstold_nid_postfix(const char16_t* str, char16_t** endptr) { static_assert(sizeof(long double) == 16); static_assert(std::numeric_limits::digits == 64); - return ParseAsciiPrefix(str, endptr, [](const char* text, char** end) { return std::strtold(text, end); }); + return ParseAsciiPrefix(str, endptr, [](const char* text, char** end) { return ParseFloat(text, end, std::strtold); }); } long long APS5_VABI wcstol_nid_postfix(const char16_t* str, char16_t** endptr, int base) { diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index 4565d8331..44db68abe 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -1,5 +1,8 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/General.hpp" +#include #include +#include #include #include #include @@ -9,9 +12,36 @@ #include #include #include +#include namespace { +struct Xtime { + std::int64_t sec; + std::int64_t nsec; +}; + +constexpr std::int64_t nanosPerSecond = 1000000000; +constexpr std::int64_t longestSleepSeconds = 86400; + +timespec XtimeDeadline(const Xtime& time) { + constexpr auto largest = std::numeric_limits::max(); + constexpr auto smallest = std::numeric_limits::min(); + const auto carry = time.nsec / nanosPerSecond; + auto nanos = time.nsec % nanosPerSecond; + auto seconds = time.sec; + if ((carry > 0 && seconds > largest - carry) || (carry < 0 && seconds < smallest - carry)) + throw std::overflow_error("_Thrd_sleep: xtime seconds overflow"); + seconds += carry; + if (nanos < 0) { + if (seconds == smallest) + throw std::overflow_error("_Thrd_sleep: xtime seconds overflow"); + nanos += nanosPerSecond; + --seconds; + } + return {static_cast(seconds), static_cast(nanos)}; +} + constexpr int threadSuccess = 0; constexpr int threadNomem = 1; constexpr int threadBusy = 3; @@ -111,6 +141,24 @@ int APS5_VABI _Mtx_lock_nid_postfix(void** handle) { return threadSuccess; } +int APS5_VABI _Mtx_trylock_nid_postfix(void** handle) { + auto* mutex = ResolveMutex(handle, "_Mtx_trylock"); + const auto self = std::this_thread::get_id(); + if (mutex->owner.load(std::memory_order_acquire) == self) { + if ((mutex->type & mutexRecursive) == 0) + return threadBusy; + if (mutex->count == std::numeric_limits::max()) + throw std::overflow_error("_Mtx_trylock: recursive lock count overflow"); + ++mutex->count; + return threadSuccess; + } + if (!mutex->native.try_lock()) + return threadBusy; + mutex->owner.store(self, std::memory_order_release); + mutex->count = 1; + return threadSuccess; +} + int APS5_VABI _Mtx_unlock_nid_postfix(void** handle) { auto* mutex = ResolveMutex(handle, "_Mtx_unlock"); if (!IsOwnedByCaller(mutex)) @@ -178,4 +226,28 @@ int SignalCondition_nid_no_patch(void** handle) { return threadSuccess; } +int APS5_VABI _Thrd_sleep_nid_postfix(const Xtime* target, Xtime*) { + if (!target) + APS5_INVALID_ARG_EX; + const auto deadline = XtimeDeadline(*target); + for (;;) { + timespec now{}; + if (clock_gettime(CLOCK_REALTIME, &now) != 0) + throw std::runtime_error("_Thrd_sleep: reading the realtime clock failed"); + if (now.tv_sec > deadline.tv_sec || (now.tv_sec == deadline.tv_sec && now.tv_nsec >= deadline.tv_nsec)) + return threadSuccess; + timespec remaining{static_cast(deadline.tv_sec - now.tv_sec), static_cast(deadline.tv_nsec - now.tv_nsec)}; + if (remaining.tv_nsec < 0) { + remaining.tv_nsec += nanosPerSecond; + --remaining.tv_sec; + } + remaining.tv_sec = std::min(remaining.tv_sec, longestSleepSeconds); + if (nanosleep(&remaining, nullptr) != 0) { + if (errno == EINTR) + continue; + throw std::runtime_error("_Thrd_sleep: nanosleep failed"); + } + } +} + } diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index b730a9066..7bc8c87c1 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -17,6 +17,9 @@ namespace { +constexpr const char* WEEKDAY_NAMES[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; +constexpr const char* MONTH_NAMES[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; +constexpr std::size_t ASCTIME_SIZE = sizeof("Sun Jan-2147483648 -2147483648:-2147483648:-2147483648 -2147483648\n"); constexpr std::int64_t SECONDS_PER_DAY = 86400; constexpr std::int64_t DAYS_FROM_MARCH_TO_YEAR_END = 306; @@ -107,24 +110,41 @@ bool isFreeBsdConversion(char specifier) { return specifier == 'k' || specifier == 'l' || specifier == 's' || specifier == 'v' || specifier == '+'; } -bool isModifiedPercent(const char* afterPercent) { - return (afterPercent[0] == 'E' || afterPercent[0] == 'O') && afterPercent[1] == '%'; -} +struct FormatConversion { + std::size_t length; + char specifier; + bool literal; + bool padded; +}; -std::size_t freeBsdConversionLength(const char* afterPercent, char& conversion) { +FormatConversion parseConversion(const char* afterPercent) { const char* cursor = afterPercent; - if ((*cursor == 'E' || *cursor == 'O') && isFreeBsdConversion(cursor[1])) ++cursor; - if (!isFreeBsdConversion(*cursor)) return 0; - conversion = *cursor; - return static_cast(cursor - afterPercent) + 1; + bool alternative = false; + bool padded = false; + while (*cursor != '\0') { + if (*cursor == 'E' || *cursor == 'O') { + if (alternative) break; + alternative = true; + } else if (*cursor == '-' || *cursor == '_' || *cursor == '0') { + if (padded) break; + padded = true; + } else { + break; + } + ++cursor; + } + if (*cursor == '\0') + return {static_cast(cursor - afterPercent), cursor[-1], true, padded}; + const bool literal = *cursor == '%' || std::strchr("aAbBcCdDeFgGhHIjklmMnprRsStTuUVvWwXxyYzZ+", *cursor) == nullptr; + return {static_cast(cursor - afterPercent) + 1, *cursor, literal, padded}; } -bool hasFreeBsdConversion(const char* format) { +bool requiresFreeBsdExpansion(const char* format) { while (*format != '\0') { - if (*format++ != '%' || *format == '\0') continue; - char conversion = 0; - if (freeBsdConversionLength(format, conversion) != 0) return true; - format += isModifiedPercent(format) ? 2 : 1; + if (*format++ != '%') continue; + const auto conversion = parseConversion(format); + if (conversion.literal || (!conversion.padded && isFreeBsdConversion(conversion.specifier))) return true; + format += conversion.length; } return false; } @@ -142,26 +162,25 @@ std::string expandFreeBsdConversions(const char* format, const GuestTm& guest) { std::string result; while (*format != '\0') { const char next = *format++; - if (next != '%' || *format == '\0') { + if (next != '%') { result += next; continue; } - if (isModifiedPercent(format)) { - result += '%'; - result += *format++; - result += *format++; + const auto conversion = parseConversion(format); + if (conversion.literal) { + if (conversion.specifier == '%') result += '%'; + result += conversion.specifier; + format += conversion.length; continue; } - char conversion = 0; - const std::size_t length = freeBsdConversionLength(format, conversion); - if (length == 0) { - result += '%'; - result += *format++; + if (conversion.padded || !isFreeBsdConversion(conversion.specifier)) { + result.append(format - 1, conversion.length + 1); + format += conversion.length; continue; } - format += length; + format += conversion.length; char text[32]; - switch (conversion) { + switch (conversion.specifier) { case 'k': std::snprintf(text, sizeof(text), "%2d", guest.tm_hour); result += text; @@ -231,11 +250,12 @@ int64_t APS5_VABI _Xtime_get_ticks_nid_postfix() { } double APS5_VABI libc_difftime_nid_postfix(int64_t time1, int64_t time0) { - return std::difftime(static_cast(time1), static_cast(time0)); + if (time1 >= time0) return static_cast(static_cast(time1) - static_cast(time0)); + return -static_cast(static_cast(time0) - static_cast(time1)); } double APS5_VABI difftime_nid_postfix(int64_t time1, int64_t time0) { - return std::difftime(static_cast(time1), static_cast(time0)); + return libc_difftime_nid_postfix(time1, time0); } GuestTm* APS5_VABI libc_gmtime_nid_postfix(const int64_t* timer) { @@ -289,8 +309,9 @@ GuestTm* APS5_VABI gmtime_nid_postfix(const int64_t* timer) { int64_t APS5_VABI libc_mktime_nid_postfix(GuestTm* timeptr) { std::tm host = toHostTm(*timeptr); + host.tm_wday = -1; const std::time_t t = std::mktime(&host); - if (t == static_cast(-1)) return -1; + if (t == static_cast(-1) && host.tm_wday == -1) return -1; const int64_t time = static_cast(t); localtime_s_nid_postfix(&time, timeptr); return time; @@ -302,14 +323,18 @@ int64_t APS5_VABI mktime_nid_postfix(GuestTm* timeptr) { size_t APS5_VABI libc_strftime_nid_postfix(char* str, size_t count, const char* format, const GuestTm* timeptr) { const std::tm host = toHostTm(*timeptr); - if (!hasFreeBsdConversion(format)) return std::strftime(str, count, format, &host); + if (!requiresFreeBsdExpansion(format)) return std::strftime(str, count, format, &host); const std::string expanded = expandFreeBsdConversions(format, *timeptr); return std::strftime(str, count, expanded.c_str(), &host); } char* APS5_VABI asctime_nid_postfix(const GuestTm* timeptr) { - const std::tm host = toHostTm(*timeptr); - return std::asctime(&host); + static thread_local char result[ASCTIME_SIZE]; + const char* weekday = timeptr->tm_wday >= 0 && timeptr->tm_wday < 7 ? WEEKDAY_NAMES[timeptr->tm_wday] : "???"; + const char* month = timeptr->tm_mon >= 0 && timeptr->tm_mon < 12 ? MONTH_NAMES[timeptr->tm_mon] : "???"; + std::snprintf(result, sizeof(result), "%s %s%3d %.2d:%.2d:%.2d %lld\n", weekday, month, timeptr->tm_mday, + timeptr->tm_hour, timeptr->tm_min, timeptr->tm_sec, 1900LL + timeptr->tm_year); + return result; } char* APS5_VABI ctime_nid_postfix(const int64_t* timer) { diff --git a/core/libs/prx/libc/src/exception/ArraySupport.cpp b/core/libs/prx/libc/src/exception/ArraySupport.cpp index 8132ade49..6b5302ac9 100644 --- a/core/libs/prx/libc/src/exception/ArraySupport.cpp +++ b/core/libs/prx/libc/src/exception/ArraySupport.cpp @@ -2,13 +2,13 @@ #include extern "C" { -void APS5_VABI __cxa_vec_cleanup_nid_postfix(void* array, std::size_t count, std::size_t size, void (*destroy)(void*)) { +void APS5_VABI __cxa_vec_cleanup_nid_postfix(void* array, std::size_t count, std::size_t size, void (APS5_VABI *destroy)(void*)) { if (!destroy) return; try { while (count) destroy(static_cast(array) + --count * size); } catch (...) { LibcException::Terminate(); } } -void APS5_VABI __cxa_vec_ctor_nid_postfix(void* array, std::size_t count, std::size_t size, void (*construct)(void*), void (*destroy)(void*)) { +void APS5_VABI __cxa_vec_ctor_nid_postfix(void* array, std::size_t count, std::size_t size, void (APS5_VABI *construct)(void*), void (APS5_VABI *destroy)(void*)) { if (!construct) return; std::size_t complete = 0; try { @@ -18,7 +18,7 @@ void APS5_VABI __cxa_vec_ctor_nid_postfix(void* array, std::size_t count, std::s throw; } } -void APS5_VABI __cxa_vec_cctor_nid_postfix(void* destination, void* source, std::size_t count, std::size_t size, void (*copy)(void*, void*), void (*destroy)(void*)) { +void APS5_VABI __cxa_vec_cctor_nid_postfix(void* destination, void* source, std::size_t count, std::size_t size, void (APS5_VABI *copy)(void*, void*), void (APS5_VABI *destroy)(void*)) { if (!copy) return; std::size_t complete = 0; try { @@ -29,7 +29,7 @@ void APS5_VABI __cxa_vec_cctor_nid_postfix(void* destination, void* source, std: throw; } } -void APS5_VABI __cxa_vec_dtor_nid_postfix(void* array, std::size_t count, std::size_t size, void (*destroy)(void*)) { +void APS5_VABI __cxa_vec_dtor_nid_postfix(void* array, std::size_t count, std::size_t size, void (APS5_VABI *destroy)(void*)) { if (!destroy) return; bool unwinding = LibcException::globals.uncaught != 0; try { @@ -53,8 +53,8 @@ void* ArrayAllocate(std::size_t size) { if (!result) _ZSt11_Xbad_allocv_nid_postfix(); return result; } -template -void* NewArray(std::size_t count, std::size_t size, std::size_t padding, void (*construct)(void*), void (*destroy)(void*), void* (*allocate)(std::size_t), Deallocate deallocate) { +template +void* NewArray(std::size_t count, std::size_t size, std::size_t padding, void (APS5_VABI *construct)(void*), void (APS5_VABI *destroy)(void*), TAllocate allocate, TDeallocate deallocate) { std::size_t total = ArraySize(count, size, padding); auto* storage = static_cast(allocate(total)); if (!storage) return nullptr; @@ -64,8 +64,8 @@ void* NewArray(std::size_t count, std::size_t size, std::size_t padding, void (* catch (...) { deallocate(storage, total); throw; } return array; } -template -void DeleteArray(void* array, std::size_t size, std::size_t padding, void (*destroy)(void*), Deallocate deallocate) { +template +void DeleteArray(void* array, std::size_t size, std::size_t padding, void (APS5_VABI *destroy)(void*), TDeallocate deallocate) { if (!array) return; auto* storage = static_cast(array) - padding; std::size_t count = 0; @@ -81,22 +81,22 @@ void DeleteArray(void* array, std::size_t size, std::size_t padding, void (*dest } extern "C" { -void* APS5_VABI __cxa_vec_new_nid_postfix(std::size_t n, std::size_t s, std::size_t p, void (*ctor)(void*), void (*dtor)(void*)) { +void* APS5_VABI __cxa_vec_new_nid_postfix(std::size_t n, std::size_t s, std::size_t p, void (APS5_VABI *ctor)(void*), void (APS5_VABI *dtor)(void*)) { return LibcException::NewArray(n, s, p, ctor, dtor, LibcException::ArrayAllocate, [](void* ptr, std::size_t) { std::free(ptr); }); } -void* APS5_VABI __cxa_vec_new2_nid_postfix(std::size_t n, std::size_t s, std::size_t p, void (*ctor)(void*), void (*dtor)(void*), void* (*alloc)(std::size_t), void (*dealloc)(void*)) { +void* APS5_VABI __cxa_vec_new2_nid_postfix(std::size_t n, std::size_t s, std::size_t p, void (APS5_VABI *ctor)(void*), void (APS5_VABI *dtor)(void*), void* (APS5_VABI *alloc)(std::size_t), void (APS5_VABI *dealloc)(void*)) { return LibcException::NewArray(n, s, p, ctor, dtor, alloc, [dealloc](void* ptr, std::size_t) { dealloc(ptr); }); } -void* APS5_VABI __cxa_vec_new3_nid_postfix(std::size_t n, std::size_t s, std::size_t p, void (*ctor)(void*), void (*dtor)(void*), void* (*alloc)(std::size_t), void (*dealloc)(void*, std::size_t)) { +void* APS5_VABI __cxa_vec_new3_nid_postfix(std::size_t n, std::size_t s, std::size_t p, void (APS5_VABI *ctor)(void*), void (APS5_VABI *dtor)(void*), void* (APS5_VABI *alloc)(std::size_t), void (APS5_VABI *dealloc)(void*, std::size_t)) { return LibcException::NewArray(n, s, p, ctor, dtor, alloc, dealloc); } -void APS5_VABI __cxa_vec_delete_nid_postfix(void* array, std::size_t s, std::size_t p, void (*dtor)(void*)) { +void APS5_VABI __cxa_vec_delete_nid_postfix(void* array, std::size_t s, std::size_t p, void (APS5_VABI *dtor)(void*)) { LibcException::DeleteArray(array, s, p, dtor, [](void* ptr, std::size_t) { std::free(ptr); }); } -void APS5_VABI __cxa_vec_delete2_nid_postfix(void* array, std::size_t s, std::size_t p, void (*dtor)(void*), void (*dealloc)(void*)) { +void APS5_VABI __cxa_vec_delete2_nid_postfix(void* array, std::size_t s, std::size_t p, void (APS5_VABI *dtor)(void*), void (APS5_VABI *dealloc)(void*)) { LibcException::DeleteArray(array, s, p, dtor, [dealloc](void* ptr, std::size_t) { dealloc(ptr); }); } -void APS5_VABI __cxa_vec_delete3_nid_postfix(void* array, std::size_t s, std::size_t p, void (*dtor)(void*), void (*dealloc)(void*, std::size_t)) { +void APS5_VABI __cxa_vec_delete3_nid_postfix(void* array, std::size_t s, std::size_t p, void (APS5_VABI *dtor)(void*), void (APS5_VABI *dealloc)(void*, std::size_t)) { LibcException::DeleteArray(array, s, p, dtor, dealloc); } } diff --git a/core/libs/prx/libc/src/exception/Runtime.cpp b/core/libs/prx/libc/src/exception/Runtime.cpp index 7b18999b7..9725d7125 100644 --- a/core/libs/prx/libc/src/exception/Runtime.cpp +++ b/core/libs/prx/libc/src/exception/Runtime.cpp @@ -1,6 +1,8 @@ #include "prx/libc/include/exceptions/Runtime.hpp" +#include #include #include +#include #include #ifdef _WIN32 #include @@ -10,6 +12,21 @@ namespace LibcException { // Writes straight to the stderr handle: terminate paths must not wait on the CRT stream lock, // which another (possibly suspended) thread may hold. +static void RawWrite(const char* data, std::size_t size) { +#ifdef _WIN32 + const HANDLE handle = GetStdHandle(STD_ERROR_HANDLE); + while (size != 0) { + DWORD written = 0; + const auto chunk = static_cast(std::min(size, std::numeric_limits::max())); + if (!WriteFile(handle, data, chunk, &written, nullptr) || written == 0) return; + data += written; + size -= written; + } +#else + std::fwrite(data, 1, size, stderr); +#endif +} + static void RawLog(const char* format, ...) { char buffer[1024]; va_list args; @@ -18,12 +35,7 @@ static void RawLog(const char* format, ...) { va_end(args); if (length <= 0) return; if (length > static_cast(sizeof(buffer)) - 1) length = sizeof(buffer) - 1; -#ifdef _WIN32 - DWORD written = 0; - WriteFile(GetStdHandle(STD_ERROR_HANDLE), buffer, static_cast(length), &written, nullptr); -#else - std::fwrite(buffer, 1, length, stderr); -#endif + RawWrite(buffer, static_cast(length)); } [[noreturn]] static void DefaultTerminate() { @@ -47,9 +59,11 @@ static void RawLog(const char* format, ...) { int status = 0; char* demangled = abi::__cxa_demangle(typeName, nullptr, nullptr, &status); const char* displayName = (status == 0 && demangled) ? demangled : typeName; - if (what) - RawLog("terminate called after throwing an instance of '%s'\n what(): %s\n", displayName, what); - else + if (what) { + RawLog("terminate called after throwing an instance of '%s'\n what(): ", displayName); + RawWrite(what, std::strlen(what)); + RawWrite("\n", 1); + } else RawLog("terminate called after throwing an instance of '%s'\n", displayName); std::free(demangled); } else { diff --git a/core/libs/prx/libkernel/Aio/src/Aio.cpp b/core/libs/prx/libkernel/Aio/src/Aio.cpp index a89f42561..c16167088 100644 --- a/core/libs/prx/libkernel/Aio/src/Aio.cpp +++ b/core/libs/prx/libkernel/Aio/src/Aio.cpp @@ -6,10 +6,10 @@ #include #include #include -#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/PreciseWait.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" @@ -273,7 +273,7 @@ int APS5_VABI sceKernelAioWaitRequests(int32_t* id, int32_t num, int32_t* state, const auto elapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); if (elapsed > static_cast(*usec)) return SCE_KERNEL_ERROR_ETIMEDOUT; } - std::this_thread::sleep_for(std::chrono::microseconds(10)); + PreciseSleepUs(10); } } @@ -298,7 +298,7 @@ int APS5_VABI sceKernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec return SCE_KERNEL_ERROR_ETIMEDOUT; } } - std::this_thread::sleep_for(std::chrono::microseconds(10)); + PreciseSleepUs(10); } } diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp index c51228d71..769ff1efc 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -160,9 +160,12 @@ std::string parseJsonString(const std::string& text, std::size_t& position) { return result; } -JsonValue parseJsonValue(const std::string& text, std::size_t& position); +constexpr std::size_t MaxNestingDepth = 512; -JsonValue parseJsonObject(const std::string& text, std::size_t& position) { +JsonValue parseJsonValue(const std::string& text, std::size_t& position, std::size_t depth); + +JsonValue parseJsonObject(const std::string& text, std::size_t& position, std::size_t depth) { + if (depth >= MaxNestingDepth) throw std::runtime_error("param.json is nested too deeply"); expectChar(text, position, '{'); JsonValue value; value.type = JsonType::Object; @@ -173,7 +176,7 @@ JsonValue parseJsonObject(const std::string& text, std::size_t& position) { std::string key = parseJsonString(text, position); skipWhitespace(text, position); expectChar(text, position, ':'); - JsonValue member = parseJsonValue(text, position); + JsonValue member = parseJsonValue(text, position, depth + 1); value.objectValue.emplace_back(std::move(key), std::move(member)); skipWhitespace(text, position); const char next = peekChar(text, position); @@ -184,14 +187,15 @@ JsonValue parseJsonObject(const std::string& text, std::size_t& position) { return value; } -JsonValue parseJsonArray(const std::string& text, std::size_t& position) { +JsonValue parseJsonArray(const std::string& text, std::size_t& position, std::size_t depth) { + if (depth >= MaxNestingDepth) throw std::runtime_error("param.json is nested too deeply"); expectChar(text, position, '['); JsonValue value; value.type = JsonType::Array; skipWhitespace(text, position); if (peekChar(text, position) == ']') { ++position; return value; } while (true) { - JsonValue element = parseJsonValue(text, position); + JsonValue element = parseJsonValue(text, position, depth + 1); value.arrayValue.push_back(std::move(element)); skipWhitespace(text, position); const char next = peekChar(text, position); @@ -230,11 +234,11 @@ JsonValue parseJsonNumber(const std::string& text, std::size_t& position) { return value; } -JsonValue parseJsonValue(const std::string& text, std::size_t& position) { +JsonValue parseJsonValue(const std::string& text, std::size_t& position, std::size_t depth) { skipWhitespace(text, position); const char c = peekChar(text, position); - if (c == '{') return parseJsonObject(text, position); - if (c == '[') return parseJsonArray(text, position); + if (c == '{') return parseJsonObject(text, position, depth); + if (c == '[') return parseJsonArray(text, position, depth); if (c == '"') { JsonValue value; value.type = JsonType::String; value.stringValue = parseJsonString(text, position); return value; } if (c == 't') { expectLiteral(text, position, "true"); JsonValue value; value.type = JsonType::Boolean; value.boolValue = true; return value; } if (c == 'f') { expectLiteral(text, position, "false"); JsonValue value; value.type = JsonType::Boolean; value.boolValue = false; return value; } @@ -245,7 +249,7 @@ JsonValue parseJsonValue(const std::string& text, std::size_t& position) { JsonValue parseJsonDocument(const std::string& text) { std::size_t position = 0; - JsonValue root = parseJsonValue(text, position); + JsonValue root = parseJsonValue(text, position, 0); skipWhitespace(text, position); if (position != text.size()) throw std::runtime_error("unexpected trailing data in param.json"); return root; diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 8e307d014..631f5553e 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -77,7 +77,7 @@ bool _fileSize(const std::filesystem::path& path, std::uint64_t& bytes) { const HANDLE find = FindFirstFileExW((path.parent_path() / L"*").c_str(), FindExInfoBasic, &entry, FindExSearchNameMatch, nullptr, FIND_FIRST_EX_LARGE_FETCH); if (find != INVALID_HANDLE_VALUE) { do { - if ((entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) continue; + if ((entry.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) != 0) continue; const auto size = (static_cast(entry.nFileSizeHigh) << 32u) | entry.nFileSizeLow; sizes.emplace(_foldCase(std::filesystem::path(entry.cFileName).string()), size); } while (FindNextFileW(find, &entry)); @@ -101,8 +101,11 @@ bool _fileSize(const std::filesystem::path& path, std::uint64_t& bytes) { } // Not in the listing (created since, or an unusual name): ask the file system directly. std::error_code error; - bytes = std::filesystem::file_size(path, error); - return !error; + if (!std::filesystem::is_regular_file(path, error)) return false; + FileStat stat{}; + File::FillFileStat(path, &stat); + bytes = static_cast(stat.st_size); + return true; } int _fail(int guestErrno) { @@ -882,4 +885,29 @@ int APS5_VABI sceKernelAprResolveFilepathsWithPrefixToIdsAndFileSizesForEach(con return _resolveForEach(prefix, paths, count, ids, sizes, results); } +bool _validWriteRange(std::uint64_t address, std::uint64_t size) { + constexpr std::uint64_t pageMask = 0x3FFFu; + return address != 0u && size != 0u && ((address | size) & pageMask) == 0u && address + size > address; +} + +bool _validWriteProtection(std::uint64_t protection) { + return (protection & ~0x3F3ULL) == 0u; +} + +int APS5_VABI sceKernelWriteModifyMtypeProtectCommand(void* dst, std::uint64_t address, std::uint64_t size, std::uint64_t type, std::uint64_t protection, std::uint64_t mask, std::uint64_t* outSize) { + if (!_validWriteRange(address, size) || !_validWriteProtection(protection) || !_validWriteProtection(mask)) return SCE_KERNEL_ERROR_EINVAL; + const Apr::AmmProtectCommand command{{Apr::Opcode::AmmModifyMtypeProtect, sizeof(command)}, address, size, static_cast(type), static_cast(protection), static_cast(mask), 0}; + if (dst) std::memcpy(dst, &command, sizeof(command)); + if (outSize) *outSize = sizeof(command); + return 0; +} + +int APS5_VABI sceKernelWriteRemapIntoPrtCommand(std::uint32_t* dst, std::uint64_t address, std::uint64_t source, std::uint64_t size, int protection, int opcode, std::uint64_t* outSize) { + if (!_validWriteRange(address, size) || !_validWriteRange(source, size) || !_validWriteProtection(static_cast(protection))) return SCE_KERNEL_ERROR_EINVAL; + const Apr::AmmRemapCommand command{{Apr::Opcode::AmmRemapIntoPrt, sizeof(command)}, address, source, size, protection, opcode ? opcode : 1011}; + if (dst) std::memcpy(dst, &command, sizeof(command)); + if (outSize) *outSize = sizeof(command); + return 0; +} + } diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 9347227eb..5ab912cf3 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -3,6 +3,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Apr/src/Apr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/Equeue.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/src/EventAccessors.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/File/src/DescriptorControl.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/DirectoryDescriptor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/FileLock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/NativeStat.cpp @@ -36,6 +37,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Once.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Tsd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Sem.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Spinlock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Socket.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Udp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/AddressConversion.cpp @@ -53,6 +55,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Process.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Sandbox.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SystemConfiguration.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Login.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SoftwareVersion.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Signals.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SignalSets.cpp @@ -69,7 +72,7 @@ target_include_directories(libkernel PRIVATE ${LIBS_INCLUDE_DIR}/nid/include) target_link_libraries(libkernel PRIVATE ${CMAKE_DL_LIBS}) target_link_libraries(libkernel PRIVATE libc) if(WIN32) - target_link_libraries(libkernel PRIVATE ws2_32 shell32) + target_link_libraries(libkernel PRIVATE ws2_32 shell32 advapi32) target_compile_definitions(libkernel PRIVATE _WIN32_WINNT=0x0602) endif() configure_windows_unwind(libkernel) @@ -251,6 +254,9 @@ if(BUILD_TESTING) configure_windows_unwind(guest_aio_tests) add_test(NAME guest_aio COMMAND guest_aio_tests) if(WIN32) + add_test(NAME guest_aio_bad_descriptors COMMAND guest_aio_tests bad-descriptors) + set_tests_properties(guest_aio_bad_descriptors PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") set_tests_properties(guest_aio PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() @@ -314,6 +320,16 @@ if(BUILD_TESTING) set_tests_properties(guest_thread_self PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_spin_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadSpin.cpp) + target_include_directories(guest_pthread_spin_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_spin_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_spin_tests) + add_test(NAME guest_pthread_spin COMMAND guest_pthread_spin_tests) + set_tests_properties(guest_pthread_spin PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_spin PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_getname_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadGetname.cpp) target_include_directories(guest_pthread_getname_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_getname_tests PRIVATE libkernel libc) @@ -344,6 +360,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_mutexattr PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_mutex_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadMutexErrors.cpp) + target_include_directories(guest_pthread_mutex_errors_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_mutex_errors_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_mutex_errors_tests) + add_test(NAME guest_pthread_mutex_errors COMMAND guest_pthread_mutex_errors_tests) + set_tests_properties(guest_pthread_mutex_errors PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_mutex_errors PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_start_tests ${LIBS_INCLUDE_DIR}/tests/PthreadStart.cpp) target_include_directories(guest_pthread_start_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_start_tests PRIVATE libkernel libc) @@ -354,6 +380,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_start PROPERTIES TIMEOUT 60 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_attr_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadAttrErrors.cpp) + target_include_directories(guest_pthread_attr_errors_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_attr_errors_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_attr_errors_tests) + add_test(NAME guest_pthread_attr_errors COMMAND guest_pthread_attr_errors_tests) + set_tests_properties(guest_pthread_attr_errors PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_attr_errors PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_stack_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadStack.cpp) target_include_directories(guest_pthread_stack_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_stack_tests PRIVATE libkernel libc) @@ -496,6 +532,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_rwlock PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_join_self_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadJoinSelf.cpp) + target_include_directories(guest_pthread_join_self_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_join_self_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_join_self_tests) + add_test(NAME guest_pthread_join_self COMMAND guest_pthread_join_self_tests) + set_tests_properties(guest_pthread_join_self PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_join_self PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_key_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadKey.cpp) target_include_directories(guest_pthread_key_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_key_tests PRIVATE libkernel libc) @@ -701,6 +747,25 @@ if(BUILD_TESTING) set_tests_properties(guest_getrusage PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_getlogin_tests ${LIBS_INCLUDE_DIR}/tests/GuestGetlogin.cpp) + target_include_directories(guest_getlogin_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_getlogin_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_getlogin_tests) + add_test(NAME guest_getlogin COMMAND guest_getlogin_tests) + set_tests_properties(guest_getlogin PROPERTIES TIMEOUT 20) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_getlogin_no_session_tests + ${LIBS_INCLUDE_DIR}/tests/GuestGetloginNoSession.cpp + System/src/Login.cpp) + target_include_directories(guest_getlogin_no_session_tests PRIVATE ${LIBS_INCLUDE_DIR}) + add_test(NAME guest_getlogin_no_session COMMAND guest_getlogin_no_session_tests) + set_tests_properties(guest_getlogin_no_session PROPERTIES TIMEOUT 20) + endif() + if(WIN32) + target_link_libraries(guest_getlogin_tests PRIVATE advapi32) + set_tests_properties(guest_getlogin PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_sched_priority_tests ${LIBS_INCLUDE_DIR}/tests/GuestSchedPriority.cpp) target_include_directories(guest_sched_priority_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_sched_priority_tests PRIVATE libkernel libc) @@ -711,6 +776,15 @@ if(BUILD_TESTING) set_tests_properties(guest_sched_priority PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_operation_mode_tests ${LIBS_INCLUDE_DIR}/tests/GuestOperationMode.cpp) + target_include_directories(guest_operation_mode_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_operation_mode_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_operation_mode_tests) + add_test(NAME guest_operation_mode COMMAND guest_operation_mode_tests) + if(WIN32) + set_tests_properties(guest_operation_mode PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_waitpid_tests ${LIBS_INCLUDE_DIR}/tests/GuestWaitpid.cpp) target_include_directories(guest_waitpid_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_waitpid_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 8d1916f7b..47befded4 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -270,7 +270,7 @@ public: #ifdef _WIN32 static_cast(start); const auto size = static_cast(bytes); - section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, static_cast(size >> 32), static_cast(size), nullptr); + section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, static_cast(size >> 32), static_cast(size), nullptr); if (!section) { const auto error = static_cast(GetLastError()); char message[96]; @@ -348,15 +348,77 @@ struct DirectMapping { std::map g_directMappings; +#if defined(__linux__) +struct PhysicalSpan { + std::uint64_t end; + std::vector bases; +}; + +std::map g_physicalSpans; + +void SplitPhysicalSpan(std::uint64_t at) { + auto span = g_physicalSpans.upper_bound(at); + if (span == g_physicalSpans.begin()) return; + --span; + if (span->first == at || span->second.end <= at) return; + g_physicalSpans.emplace_hint(std::next(span), at, PhysicalSpan{span->second.end, span->second.bases}); + span->second.end = at; +} +#endif + +std::map::iterator InsertMapping(std::uintptr_t base, DirectMapping mapping) { + const auto [inserted, added] = g_directMappings.emplace(base, std::move(mapping)); + if (!added) throw std::runtime_error("direct mapping inserted over a live one"); +#if defined(__linux__) + const auto first = inserted->second.phys; + const auto last = first + (inserted->second.end - base); + SplitPhysicalSpan(first); + SplitPhysicalSpan(last); + auto span = g_physicalSpans.lower_bound(first); + for (auto cursor = first; cursor < last; ++span) { + if (span == g_physicalSpans.end() || span->first > cursor) span = g_physicalSpans.emplace_hint(span, cursor, PhysicalSpan{span == g_physicalSpans.end() ? last : std::min(last, span->first), {}}); + auto& bases = span->second.bases; + bases.insert(std::upper_bound(bases.begin(), bases.end(), base), base); + cursor = span->second.end; + } +#endif + return inserted; +} + +std::map::iterator RemoveMapping(std::map::iterator mapping) { +#if defined(__linux__) + const auto base = mapping->first; + const auto first = mapping->second.phys; + const auto last = first + (mapping->second.end - base); + for (auto span = g_physicalSpans.lower_bound(first); span != g_physicalSpans.end() && span->first < last;) { + auto& bases = span->second.bases; + bases.erase(std::lower_bound(bases.begin(), bases.end(), base)); + span = bases.empty() ? g_physicalSpans.erase(span) : std::next(span); + } + for (auto span = g_physicalSpans.lower_bound(first); span != g_physicalSpans.end() && span->first <= last;) { + if (span != g_physicalSpans.begin()) { + const auto before = std::prev(span); + if (before->second.end == span->first && before->second.bases == span->second.bases) { + before->second.end = span->second.end; + span = g_physicalSpans.erase(span); + continue; + } + } + ++span; + } +#endif + return g_directMappings.erase(mapping); +} + void EraseMappings(std::uintptr_t start, std::uintptr_t end) { auto it = g_directMappings.lower_bound(start); if (it != g_directMappings.begin() && std::prev(it)->second.end > start) --it; while (it != g_directMappings.end() && it->first < end) { const auto base = it->first; const auto mapping = it->second; - it = g_directMappings.erase(it); - if (base < start) g_directMappings.emplace(base, DirectMapping{start, mapping.phys, mapping.memoryType, mapping.backing}); - if (mapping.end > end) it = g_directMappings.emplace(end, DirectMapping{mapping.end, mapping.phys + end - base, mapping.memoryType, mapping.backing}).first; + it = RemoveMapping(it); + if (base < start) InsertMapping(base, DirectMapping{start, mapping.phys, mapping.memoryType, mapping.backing}); + if (mapping.end > end) it = InsertMapping(end, DirectMapping{mapping.end, mapping.phys + end - base, mapping.memoryType, mapping.backing}); } } @@ -369,14 +431,14 @@ void ErasePhysMappings(std::uint64_t first, std::uint64_t last) { ++it; continue; } - it = g_directMappings.erase(it); + it = RemoveMapping(it); if (mapping.phys < first) { const auto keep = first - mapping.phys; - g_directMappings.emplace(base, DirectMapping{base + keep, mapping.phys, mapping.memoryType, mapping.backing}); + InsertMapping(base, DirectMapping{base + keep, mapping.phys, mapping.memoryType, mapping.backing}); } if (mappingLast > last) { const auto skip = last - mapping.phys; - it = g_directMappings.emplace(base + skip, DirectMapping{mapping.end, last, mapping.memoryType, mapping.backing}).first; + it = InsertMapping(base + skip, DirectMapping{mapping.end, last, mapping.memoryType, mapping.backing}); } } } @@ -396,10 +458,16 @@ void ValidatePhysicalRange(std::uint64_t phys, std::size_t len) { void WatchMapping(std::uintptr_t address, std::size_t len) { GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(reinterpret_cast(address), len); const auto end = address + len; + std::vector sharing; for (auto added = g_directMappings.lower_bound(address); added != g_directMappings.end() && added->first < end; ++added) { const auto addedPhys = added->second.phys; const auto addedPhysEnd = addedPhys + (added->second.end - added->first); - for (const auto& [base, other] : g_directMappings) { + sharing.clear(); + for (auto span = g_physicalSpans.lower_bound(addedPhys); span != g_physicalSpans.end() && span->first < addedPhysEnd; ++span) sharing.insert(sharing.end(), span->second.bases.begin(), span->second.bases.end()); + std::sort(sharing.begin(), sharing.end()); + sharing.erase(std::unique(sharing.begin(), sharing.end()), sharing.end()); + for (const auto base : sharing) { + const auto& other = g_directMappings.at(base); if ((base >= address && base < end) || other.backing != added->second.backing) continue; const auto first = std::max(addedPhys, other.phys); const auto last = std::min(addedPhysEnd, other.phys + (other.end - base)); @@ -481,7 +549,7 @@ void AddMapping(std::uintptr_t address, std::size_t len, std::uint64_t phys, int bytes += PS5_PAGE_SIZE; } page.backing->Map(address + offset, bytes, page.offset, nativeProt); - g_directMappings.emplace(address + offset, DirectMapping{address + offset + bytes, phys + offset, page.backing->MemoryType(), page.backing}); + InsertMapping(address + offset, DirectMapping{address + offset + bytes, phys + offset, page.backing->MemoryType(), page.backing}); offset += bytes; } #if defined(__linux__) @@ -921,10 +989,6 @@ int DoMprotect(const void* addr, size_t len, int prot) { else if (IsGuestModuleImage(memory.AllocationBase)) mutation.RegisterImage(memory.AllocationBase); else throw std::invalid_argument("Memory protection of a foreign image is not supported"); } - if (memory.Type == MEM_PRIVATE && memory.State == MEM_COMMIT && memory.RegionSize >= bytes && !mutation.Overlaps(pointer, bytes)) { - if (mprotect(const_cast(pointer), bytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); - return 0; - } #else mutation.RegisterMainImage(); #endif @@ -949,13 +1013,13 @@ int DoMtypeprotect(const void* addr, size_t len, int type, int prot) { while (it != g_directMappings.end() && it->first < end) { const auto base = it->first; const auto mapping = it->second; - it = g_directMappings.erase(it); - if (base < first) g_directMappings.emplace(base, DirectMapping{first, mapping.phys, mapping.memoryType, mapping.backing}); + it = RemoveMapping(it); + if (base < first) InsertMapping(base, DirectMapping{first, mapping.phys, mapping.memoryType, mapping.backing}); const auto low = std::max(base, first); const auto high = std::min(mapping.end, end); - g_directMappings.emplace(low, DirectMapping{high, mapping.phys + low - base, type, mapping.backing}); + InsertMapping(low, DirectMapping{high, mapping.phys + low - base, type, mapping.backing}); physical.emplace_back(mapping.phys + low - base, high - low); - if (mapping.end > end) it = g_directMappings.emplace(end, DirectMapping{mapping.end, mapping.phys + end - base, mapping.memoryType, mapping.backing}).first; + if (mapping.end > end) it = InsertMapping(end, DirectMapping{mapping.end, mapping.phys + end - base, mapping.memoryType, mapping.backing}); } } for (const auto& [phys, bytes] : physical) DirectMemoryRetype(static_cast(phys), bytes, type); @@ -966,6 +1030,7 @@ int DoMunmap(void* addr, size_t len) { Trace("unmap %p+0x%zx", addr, len); if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0 || !addr) return SCE_KERNEL_ERROR_EINVAL; GuestAllocations::Mutation mutation; + if (!mutation.Overlaps(addr, len)) return 0; UnmapRegistered(mutation, addr, len); return 0; } diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 7deed55ab..cf0fe1203 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -171,6 +171,10 @@ size_t APS5_VABI sceKernelGetDirectMemorySize(void) { return DIRECT_MEMORY_SIZE; } +int APS5_VABI sceKernelEnableDmemAliasing(void) { + return 0; +} + int APS5_VABI sceKernelMapDirectMemory(void** addr, size_t len, int prot, int flags, int64_t direct_memory_start, size_t alignment) { (void)alignment; return DoMapDirect(addr, len, prot, flags, direct_memory_start, alignment); diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp index 27076063a..7f15f884a 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPoolExport.cpp @@ -23,9 +23,9 @@ PoolState& Pool() { bool PageAligned(uint64_t v) { return (v & (PS5_PAGE_SIZE - 1)) == 0; } -int PoolCommit(void* addr, uint64_t len, int prot) { +int PoolCommit(void* addr, uint64_t len, int type, int prot) { if (!addr || len == 0 || !PageAligned(reinterpret_cast(addr)) || !PageAligned(len)) return SCE_KERNEL_ERROR_EINVAL; - const int ret = DoMprotect(addr, static_cast(len), prot); + const int ret = DoMtypeprotect(addr, static_cast(len), type, prot); if (ret == 0) { PoolState& pool = Pool(); std::lock_guard lock(pool.mutex); @@ -67,10 +67,10 @@ int APS5_VABI sceKernelMemoryPoolBatch(const KernelMemoryPoolBatchEntry* entries const KernelMemoryPoolBatchEntry& e = entries[done]; ret = Guarded([&] { switch (e.op) { - case 1: return PoolCommit(e.commit.addr, e.commit.len, e.commit.prot); + case 1: return PoolCommit(e.commit.addr, e.commit.len, e.commit.type, e.commit.prot); case 2: return PoolDecommit(e.decommit.addr, e.decommit.len); case 3: return DoMprotect(e.protect.addr, static_cast(e.protect.len), e.protect.prot); - case 4: return DoMprotect(e.type_protect.addr, static_cast(e.type_protect.len), e.type_protect.prot); + case 4: return DoMtypeprotect(e.type_protect.addr, static_cast(e.type_protect.len), e.type_protect.type, e.type_protect.prot); default: return SCE_KERNEL_ERROR_EINVAL; } }); @@ -81,9 +81,8 @@ int APS5_VABI sceKernelMemoryPoolBatch(const KernelMemoryPoolBatchEntry* entries } int APS5_VABI sceKernelMemoryPoolCommit(void* addr, size_t len, int type, int prot, int flags) { - (void)type; (void)flags; - return Guarded([&] { return PoolCommit(addr, len, prot); }); + return Guarded([&] { return PoolCommit(addr, len, type, prot); }); } int APS5_VABI sceKernelMemoryPoolDecommit(void* addr, size_t len, int flags) { diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp new file mode 100644 index 000000000..cb327dff5 --- /dev/null +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -0,0 +1,20 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_DESCRIPTORCONTROL_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_DESCRIPTORCONTROL_HPP + +#include + +namespace File { + +enum class FcntlArgument { None, Int, Pointer }; + +FcntlArgument FcntlArgumentOf(int command); +int Fcntl(int fd, int command, std::intptr_t argument); +int Ioctl(int fd, std::uint64_t request, void* argument); +int DuplicateFrom(int fd, int start); +void RememberDescriptor(int fd, int openFlags); +void ShareDescriptor(int from, int to); +void ForgetDescriptor(int fd); + +} + +#endif diff --git a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp index 2031b6c4c..a8ce99649 100644 --- a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp +++ b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp @@ -6,6 +6,7 @@ #include extern "C" const char* GetDirectoryDescriptorPath_nid_no_patch(int fd, int* error); +extern "C" int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std::uint16_t mode); namespace File { diff --git a/core/libs/prx/libkernel/File/include/FileFlags.hpp b/core/libs/prx/libkernel/File/include/FileFlags.hpp index f97ccc9d2..2423ccdac 100644 --- a/core/libs/prx/libkernel/File/include/FileFlags.hpp +++ b/core/libs/prx/libkernel/File/include/FileFlags.hpp @@ -9,6 +9,7 @@ constexpr std::int32_t SCE_KERNEL_O_RDWR = 0x00000002; constexpr std::int32_t SCE_KERNEL_O_ACCMODE = 0x00000003; constexpr std::int32_t SCE_KERNEL_O_APPEND = 0x00000008; constexpr std::int32_t SCE_KERNEL_O_FSYNC = 0x00000080; +constexpr std::int32_t SCE_KERNEL_O_NOFOLLOW = 0x00000100; constexpr std::int32_t SCE_KERNEL_O_SYNC = 0x00000080; constexpr std::int32_t SCE_KERNEL_O_CREAT = 0x00000200; constexpr std::int32_t SCE_KERNEL_O_TRUNC = 0x00000400; diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp new file mode 100644 index 000000000..7bdd8c8bc --- /dev/null +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -0,0 +1,510 @@ +#include "prx/libkernel/File/include/DescriptorControl.hpp" +#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/RandomDevice.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#else +#include +#include +#include +#include +#endif + +extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI dup2_nid_postfix(int from, int to); +#ifdef _WIN32 +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); +#endif + +namespace { + +constexpr int GuestEintr = 4; +constexpr int GuestEbadf = 9; +constexpr int GuestEdeadlk = 11; +constexpr int GuestEfault = 14; +constexpr int GuestEinval = 22; +constexpr int GuestEnotty = 25; +constexpr int GuestEagain = 35; +constexpr int GuestEnolck = 77; +constexpr int GuestEoverflow = 84; + +constexpr int DuplicateCommand = 0; +constexpr int GetDescriptorFlags = 1; +constexpr int SetDescriptorFlags = 2; +constexpr int GetStatusFlags = 3; +constexpr int SetStatusFlags = 4; +constexpr int GetOwner = 5; +constexpr int SetOwner = 6; +constexpr int OldGetLock = 7; +constexpr int OldSetLock = 8; +constexpr int OldSetLockWait = 9; +constexpr int DuplicateToCommand = 10; +constexpr int GetLock = 11; +constexpr int SetLock = 12; +constexpr int SetLockWait = 13; +constexpr int SetLockRemote = 14; +constexpr int ReadAhead = 15; +constexpr int DarwinReadAhead = 16; +constexpr int DuplicateCloseOnExec = 17; +constexpr int DuplicateToCloseOnExec = 18; + +constexpr int CloseOnExec = 1; +constexpr int OpenCloseOnExec = 0x00100000; + +constexpr int ReadOnly = 0; +constexpr int WriteOnly = 1; +constexpr int ReadWrite = 2; +constexpr int Nonblocking = 0x0004; +constexpr int Append = 0x0008; +constexpr int Asynchronous = 0x0040; +constexpr int Synchronous = 0x0080; +constexpr int ReadAheadFlag = 0x0200; +constexpr int Direct = 0x00010000; +constexpr int OpenStatusFlags = Nonblocking | Append | Asynchronous | Synchronous | Direct; +constexpr int SettableStatusFlags = OpenStatusFlags | ReadAheadFlag; + +constexpr std::int16_t ReadLock = 1; +constexpr std::int16_t Unlock = 2; +constexpr std::int16_t WriteLock = 3; +constexpr std::int16_t FromCurrent = 1; + +constexpr std::uint32_t NonblockingRequest = 0x8004667e; +constexpr std::uint32_t ReadableRequest = 0x4004667f; +constexpr std::uint32_t RequestIn = 0x80000000u; +constexpr std::uint32_t TerminalGroup = 't'; + +struct GuestLock { + std::int64_t start; + std::int64_t length; + std::int32_t pid; + std::int16_t type; + std::int16_t whence; + std::int32_t system; +}; +static_assert(sizeof(GuestLock) == 32); +static_assert(offsetof(GuestLock, type) == 20); +constexpr std::size_t OldGuestLockSize = 24; + +enum class Kind { File, Directory, Pipe, Device, RandomDevice }; + +struct Entry { + bool closeOnExec = false; + std::shared_ptr status; +}; + +std::mutex g_mutex; +std::map g_entries; + +int Fail(int error) { + *__error_nid_postfix() = error; + return -1; +} + +const char* KindName(Kind kind) { + switch (kind) { + case Kind::File: return "file"; + case Kind::Directory: return "directory"; + case Kind::Pipe: return "pipe"; + case Kind::Device: return "character device"; + case Kind::RandomDevice: return "random device"; + } + return "descriptor"; +} + +[[noreturn]] void Unsupported(const char* function, const std::string& what, Kind kind, int fd) { + throw std::runtime_error(std::string(function) + ": " + what + " on a " + KindName(kind) + " is not implemented, fd=" + std::to_string(fd)); +} + +#ifdef _WIN32 +void IgnoreInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} + +HANDLE NativeHandle(int fd) { + const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); + const auto handle = ::_get_osfhandle(fd); + _set_thread_local_invalid_parameter_handler(previous); + return reinterpret_cast(handle); +} + +std::optional Classify(int fd, const char* function) { + if (fd < 0 || !DescriptorIsOpen_nid_no_patch(fd)) return std::nullopt; + if (File::DirectoryDescriptorPath(fd)) return Kind::Directory; + if (File::IsRandomDevice(fd)) return Kind::RandomDevice; + switch (::GetFileType(NativeHandle(fd))) { + case FILE_TYPE_DISK: return Kind::File; + case FILE_TYPE_PIPE: return Kind::Pipe; + case FILE_TYPE_CHAR: return Kind::Device; + default: throw std::runtime_error(std::string(function) + ": descriptor " + std::to_string(fd) + " has an unknown Windows file type, error=" + std::to_string(::GetLastError())); + } +} + +int AccessMode(int fd, const char* function) { + using QueryObject = NTSTATUS(NTAPI*)(HANDLE, OBJECT_INFORMATION_CLASS, PVOID, ULONG, PULONG); + static const auto query = reinterpret_cast(reinterpret_cast(::GetProcAddress(::GetModuleHandleW(L"ntdll.dll"), "NtQueryObject"))); + PUBLIC_OBJECT_BASIC_INFORMATION information{}; + const NTSTATUS status = query == nullptr ? -1 : query(NativeHandle(fd), ObjectBasicInformation, &information, sizeof(information), nullptr); + if (status < 0) + throw std::runtime_error(std::string(function) + ": cannot read the access of descriptor " + std::to_string(fd) + ", status=" + std::to_string(status)); + const bool read = (information.GrantedAccess & FILE_READ_DATA) != 0; + const bool write = (information.GrantedAccess & (FILE_WRITE_DATA | FILE_APPEND_DATA)) != 0; + if (read && write) return ReadWrite; + if (write) return WriteOnly; + if (read) return ReadOnly; + throw std::runtime_error(std::string(function) + ": descriptor " + std::to_string(fd) + " grants neither read nor write access"); +} + +DWORD PipeState(int fd, const char* function) { + DWORD state = 0; + if (!::GetNamedPipeHandleStateW(NativeHandle(fd), &state, nullptr, nullptr, nullptr, nullptr, 0)) + throw std::runtime_error(std::string(function) + ": cannot read the mode of pipe descriptor " + std::to_string(fd) + ", error=" + std::to_string(::GetLastError())); + return state; +} + +void SetPipeNonblocking(int fd, bool nonblocking, const char* function) { + DWORD mode = (PipeState(fd, function) & PIPE_READMODE_MESSAGE) | (nonblocking ? PIPE_NOWAIT : PIPE_WAIT); + if (!::SetNamedPipeHandleState(NativeHandle(fd), &mode, nullptr, nullptr)) + throw std::runtime_error(std::string(function) + ": cannot change the mode of pipe descriptor " + std::to_string(fd) + ", error=" + std::to_string(::GetLastError())); +} + +Entry InitialEntry(int fd) { + int status = 0; + if (::GetFileType(NativeHandle(fd)) == FILE_TYPE_PIPE && (PipeState(fd, "fcntl") & PIPE_NOWAIT) != 0) status |= Nonblocking; + return {false, std::make_shared(status)}; +} + +void ApplyHostStatus(int fd, Kind kind, int changed, int next, const char* function) { + if ((changed & Append) && kind == Kind::File) Unsupported(function, "changing O_APPEND", kind, fd); + if ((changed & Nonblocking) && kind == Kind::Device) Unsupported(function, "changing O_NONBLOCK", kind, fd); + if ((changed & Nonblocking) && kind == Kind::Pipe) SetPipeNonblocking(fd, (next & Nonblocking) != 0, function); +} + +int Readable(int fd, Kind kind, const char* function) { + if (kind == Kind::File) { + const auto size = ::_filelengthi64(fd); + const auto offset = ::_telli64(fd); + if (size < 0 || offset < 0) + throw std::runtime_error(std::string(function) + ": cannot read the size and offset of descriptor " + std::to_string(fd) + ", errno=" + std::to_string(errno)); + return static_cast(size - offset); + } + if (AccessMode(fd, function) == WriteOnly) return 0; + DWORD available = 0; + if (!::PeekNamedPipe(NativeHandle(fd), nullptr, 0, nullptr, &available, nullptr)) { + const DWORD error = ::GetLastError(); + if (error == ERROR_BROKEN_PIPE) return 0; + throw std::runtime_error(std::string(function) + ": cannot peek pipe descriptor " + std::to_string(fd) + ", error=" + std::to_string(error)); + } + return static_cast(available); +} +#else +std::optional Classify(int fd, const char* function) { + if (fd < 0) return std::nullopt; + struct stat status{}; + if (::fstat(fd, &status) != 0) { + if (errno == EBADF) return std::nullopt; + throw std::runtime_error(std::string(function) + ": fstat failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); + } + if (File::IsRandomDevice(fd)) return Kind::RandomDevice; + if (S_ISREG(status.st_mode)) return Kind::File; + if (S_ISDIR(status.st_mode)) return Kind::Directory; + if (S_ISFIFO(status.st_mode)) return Kind::Pipe; + if (S_ISCHR(status.st_mode)) return Kind::Device; + throw std::runtime_error(std::string(function) + ": descriptor " + std::to_string(fd) + " is neither a file, a directory, a pipe nor a character device"); +} + +int HostStatus(int fd, const char* function) { + const int flags = ::fcntl(fd, F_GETFL); + if (flags < 0) throw std::runtime_error(std::string(function) + ": host F_GETFL failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); + return flags; +} + +int AccessMode(int fd, const char* function) { + return HostStatus(fd, function) & O_ACCMODE; +} + +Entry InitialEntry(int fd) { + const int descriptorFlags = ::fcntl(fd, F_GETFD); + if (descriptorFlags < 0) throw std::runtime_error("fcntl: host F_GETFD failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); + const int host = HostStatus(fd, "fcntl"); + int status = 0; + if (host & O_NONBLOCK) status |= Nonblocking; + if (host & O_APPEND) status |= Append; + if (host & O_ASYNC) status |= Asynchronous; + if ((host & O_SYNC) == O_SYNC) status |= Synchronous; + if (host & O_DIRECT) status |= Direct; + return {(descriptorFlags & FD_CLOEXEC) != 0, std::make_shared(status)}; +} + +void ApplyHostStatus(int fd, Kind, int changed, int next, const char* function) { + if ((changed & (Nonblocking | Append)) == 0) return; + const int host = HostStatus(fd, function); + const int wanted = (host & ~(O_NONBLOCK | O_APPEND)) | ((next & Nonblocking) ? O_NONBLOCK : 0) | ((next & Append) ? O_APPEND : 0); + if (::fcntl(fd, F_SETFL, wanted) != 0) + throw std::runtime_error(std::string(function) + ": host F_SETFL failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); +} + +int Readable(int fd, Kind kind, const char* function) { + if (kind == Kind::File) { + struct stat status{}; + const auto offset = ::lseek(fd, 0, SEEK_CUR); + if (::fstat(fd, &status) != 0 || offset < 0) + throw std::runtime_error(std::string(function) + ": cannot read the size and offset of descriptor " + std::to_string(fd) + ", errno=" + std::to_string(errno)); + return static_cast(status.st_size - offset); + } + if (AccessMode(fd, function) == WriteOnly) return 0; + int available = 0; + if (::ioctl(fd, FIONREAD, &available) != 0) + throw std::runtime_error(std::string(function) + ": host FIONREAD failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); + return available; +} + +int LockError(int error) { + switch (error) { + case EACCES: + case EAGAIN: return GuestEagain; + case EDEADLK: return GuestEdeadlk; + case EINTR: return GuestEintr; + case ENOLCK: return GuestEnolck; + case EBADF: return GuestEbadf; + case EINVAL: return GuestEinval; + case EOVERFLOW: return GuestEoverflow; + default: throw std::runtime_error("fcntl: host record lock failed, errno=" + std::to_string(error)); + } +} +#endif + +Entry& Materialize(int fd) { + const auto found = g_entries.find(fd); + if (found != g_entries.end()) return found->second; + return g_entries.emplace(fd, InitialEntry(fd)).first->second; +} + +int ChangeStatus(int fd, Kind kind, int mask, int value, const char* function) { + std::lock_guard lock(g_mutex); + auto& status = *Materialize(fd).status; + const int next = (status & ~mask) | (value & mask); + const int changed = status ^ next; + if ((changed & Asynchronous) && (kind == Kind::Pipe || kind == Kind::Device)) Unsupported(function, "O_ASYNC", kind, fd); + if ((changed & Synchronous) && kind == Kind::File) Unsupported(function, "changing O_FSYNC", kind, fd); + ApplyHostStatus(fd, kind, changed, next, function); + status = next; + return 0; +} + +int CurrentStatus(int fd) { + std::lock_guard lock(g_mutex); + return *Materialize(fd).status; +} + +void SetCloseOnExec(int fd, bool value) { + std::lock_guard lock(g_mutex); + Materialize(fd).closeOnExec = value; +} + +int RecordLock(int fd, int command, std::intptr_t argument) { + const bool old = command <= OldSetLockWait; + const int current = old ? command + (GetLock - OldGetLock) : command; + const std::size_t size = old ? OldGuestLockSize : sizeof(GuestLock); + auto* pointer = reinterpret_cast(argument); + if (pointer == nullptr) return Fail(GuestEfault); + GuestLock lock{}; + std::memcpy(&lock, pointer, size); + const auto kind = Classify(fd, "fcntl"); + if (!kind || *kind == Kind::Pipe) return Fail(GuestEbadf); + if (*kind != Kind::File && *kind != Kind::Directory) Unsupported("fcntl", "a record lock", *kind, fd); +#ifdef _WIN32 + static_cast(current); + throw std::runtime_error("fcntl: record locks are not implemented on Windows, command=" + std::to_string(command) + ", fd=" + std::to_string(fd)); +#else + struct flock host{}; + if (lock.type == ReadLock) host.l_type = F_RDLCK; + else if (lock.type == WriteLock) host.l_type = F_WRLCK; + else if (lock.type == Unlock) host.l_type = F_UNLCK; + else return Fail(GuestEinval); + if (current == GetLock && lock.type == Unlock) + throw std::runtime_error("fcntl: F_GETLK with F_UNLCK is not implemented, fd=" + std::to_string(fd)); + host.l_whence = lock.whence; + host.l_start = static_cast(lock.start); + host.l_len = static_cast(lock.length); + const int hostCommand = current == GetLock ? F_GETLK : current == SetLock ? F_SETLK : F_SETLKW; + if (::fcntl(fd, hostCommand, &host) != 0) return Fail(LockError(errno)); + if (current != GetLock) return 0; + if (host.l_type == F_UNLCK) { + if (lock.whence == FromCurrent) { + const auto offset = ::lseek(fd, 0, SEEK_CUR); + if (offset < 0) throw std::runtime_error("fcntl: cannot read the offset of descriptor " + std::to_string(fd) + ", errno=" + std::to_string(errno)); + lock.start += offset; + } + lock.type = Unlock; + } else { + lock.type = host.l_type == F_RDLCK ? ReadLock : WriteLock; + lock.whence = SEEK_SET; + lock.start = host.l_start; + lock.length = host.l_len; + lock.pid = host.l_pid; + lock.system = 0; + } + const GuestArena::HostWrite destination(pointer, size); + if (!destination.Open()) return Fail(GuestEfault); + std::memcpy(pointer, &lock, size); + return 0; +#endif +} + +int ReadAheadControl(int fd, int command, std::intptr_t argument) { + const auto kind = Classify(fd, "fcntl"); + if (!kind || *kind == Kind::Pipe) return Fail(GuestEbadf); + if (*kind != Kind::File) return Fail(GuestEnotty); + std::lock_guard lock(g_mutex); + auto& status = *Materialize(fd).status; + if (command == DarwinReadAhead || argument >= 0) status |= ReadAheadFlag; + else status &= ~ReadAheadFlag; + return 0; +} + +} + +namespace File { + +FcntlArgument FcntlArgumentOf(int command) { + switch (command) { + case DuplicateCommand: + case SetDescriptorFlags: + case SetStatusFlags: + case SetOwner: + case DuplicateToCommand: + case ReadAhead: + case DarwinReadAhead: + case DuplicateCloseOnExec: + case DuplicateToCloseOnExec: + return FcntlArgument::Int; + case OldGetLock: + case OldSetLock: + case OldSetLockWait: + case GetLock: + case SetLock: + case SetLockWait: + case SetLockRemote: + return FcntlArgument::Pointer; + default: + return FcntlArgument::None; + } +} + +int Fcntl(int fd, int command, std::intptr_t argument) { + switch (command) { + case DuplicateCommand: + case DuplicateCloseOnExec: { + if (fd < 0) return Fail(GuestEbadf); + if (static_cast(argument) < 0) return Fail(GuestEinval); + if (!Classify(fd, "fcntl")) return Fail(GuestEbadf); + const int duplicate = DuplicateFrom(fd, static_cast(argument)); + if (duplicate >= 0 && command == DuplicateCloseOnExec) SetCloseOnExec(duplicate, true); + return duplicate; + } + case DuplicateToCommand: + case DuplicateToCloseOnExec: { + const int duplicate = dup2_nid_postfix(fd, static_cast(argument)); + if (duplicate >= 0 && command == DuplicateToCloseOnExec) SetCloseOnExec(duplicate, true); + return duplicate; + } + case GetDescriptorFlags: { + if (!Classify(fd, "fcntl")) return Fail(GuestEbadf); + std::lock_guard lock(g_mutex); + return Materialize(fd).closeOnExec ? CloseOnExec : 0; + } + case SetDescriptorFlags: + if (!Classify(fd, "fcntl")) return Fail(GuestEbadf); + SetCloseOnExec(fd, (argument & CloseOnExec) != 0); + return 0; + case GetStatusFlags: { + if (!Classify(fd, "fcntl")) return Fail(GuestEbadf); + const int access = AccessMode(fd, "fcntl"); + return access | CurrentStatus(fd); + } + case SetStatusFlags: { + const auto kind = Classify(fd, "fcntl"); + if (!kind) return Fail(GuestEbadf); + return ChangeStatus(fd, *kind, SettableStatusFlags, static_cast(argument), "fcntl"); + } + case GetOwner: + case SetOwner: { + const auto kind = Classify(fd, "fcntl"); + if (!kind) return Fail(GuestEbadf); + Unsupported("fcntl", command == GetOwner ? "F_GETOWN" : "F_SETOWN", *kind, fd); + } + case OldGetLock: + case OldSetLock: + case OldSetLockWait: + case GetLock: + case SetLock: + case SetLockWait: + return RecordLock(fd, command, argument); + case SetLockRemote: + throw std::runtime_error("fcntl: F_SETLK_REMOTE needs a privilege whose state on the console is not known, fd=" + std::to_string(fd)); + case ReadAhead: + case DarwinReadAhead: + return ReadAheadControl(fd, command, argument); + default: + return Fail(GuestEinval); + } +} + +int Ioctl(int fd, std::uint64_t request, void* argument) { + const auto command = static_cast(request); + const std::uint32_t length = (command >> 16) & 0x1fff; + if ((command & RequestIn) && length != 0 && argument == nullptr) return Fail(GuestEfault); + const auto kind = Classify(fd, "ioctl"); + if (!kind) return Fail(GuestEbadf); + if (command == NonblockingRequest) { + const bool enable = *static_cast(argument) != 0; + return ChangeStatus(fd, *kind, Nonblocking, enable ? Nonblocking : 0, "ioctl"); + } + if (command == ReadableRequest) { + if (*kind == Kind::RandomDevice) return Fail(GuestEnotty); + if (*kind != Kind::File && *kind != Kind::Pipe) Unsupported("ioctl", "FIONREAD", *kind, fd); + const int available = Readable(fd, *kind, "ioctl"); + if (argument == nullptr) return Fail(GuestEfault); + const GuestArena::HostWrite destination(argument, sizeof(available)); + if (!destination.Open()) return Fail(GuestEfault); + std::memcpy(argument, &available, sizeof(available)); + return 0; + } + if (((command >> 8) & 0xff) == TerminalGroup) return Fail(GuestEnotty); + char hex[11]; + std::snprintf(hex, sizeof(hex), "0x%08X", command); + Unsupported("ioctl", std::string("request ") + hex, *kind, fd); +} + +void RememberDescriptor(int fd, int openFlags) { + std::lock_guard lock(g_mutex); + g_entries[fd] = Entry{(openFlags & OpenCloseOnExec) != 0, std::make_shared(openFlags & OpenStatusFlags)}; +} + +void ShareDescriptor(int from, int to) { + std::lock_guard lock(g_mutex); + auto status = Materialize(from).status; + g_entries[to] = Entry{false, std::move(status)}; +} + +void ForgetDescriptor(int fd) { + std::lock_guard lock(g_mutex); + g_entries.erase(fd); +} + +} diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index 9bbe6569b..de24d4eac 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -1,6 +1,7 @@ #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/KernelErrors.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include #include @@ -126,6 +127,10 @@ int ReadDirectoryDescriptor(int fd, char* buf, int nbytes) { std::size_t used = 0; while (state.cursor < state.entries.size()) { const auto& [name, type] = state.entries[state.cursor]; + if (name.size() > 255) { + if (used == 0) return SCE_KERNEL_ERROR_ENAMETOOLONG; + break; + } const std::size_t record = (8 + name.size() + 1 + 3) & ~std::size_t{3}; if (used + record > static_cast(nbytes)) { if (used == 0) return SCE_KERNEL_ERROR_EINVAL; @@ -177,7 +182,8 @@ extern "C" const char* GetDirectoryDescriptorPath_nid_no_patch(int fd, int* erro *error = ENOTDIR; return nullptr; } - static thread_local std::string path; + struct PathTag {}; + auto& path = HostThreadLocal(); const auto utf8Path = directory->u8string(); path.assign(reinterpret_cast(utf8Path.data()), utf8Path.size()); *error = 0; diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index 5454db47d..4495fb59f 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -68,6 +68,7 @@ static void CopyNativeStat(const NativeStat& st, FileStat* sb) { sb->st_mode = static_cast(st.st_mode); sb->st_size = static_cast(st.st_size); #ifdef _WIN32 + if (st.hasIdentity) sb->st_size = static_cast((static_cast(st.identity.nFileSizeHigh) << 32u) | st.identity.nFileSizeLow); sb->st_dev = static_cast(st.st_dev); sb->st_ino = static_cast(st.st_ino); sb->st_nlink = static_cast(st.st_nlink); diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index bfcf004c5..b5acafc34 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -6,6 +6,7 @@ #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/File/include/FileLock.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" @@ -23,38 +24,6 @@ #include #include #include -static int OpenError(DWORD error) { - switch (error) { - case ERROR_FILE_NOT_FOUND: - case ERROR_PATH_NOT_FOUND: - case ERROR_INVALID_DRIVE: - case ERROR_BAD_NETPATH: - case ERROR_BAD_NET_NAME: - case ERROR_BAD_PATHNAME: - case ERROR_FILENAME_EXCED_RANGE: - return ENOENT; - case ERROR_ACCESS_DENIED: - case ERROR_SHARING_VIOLATION: - case ERROR_LOCK_VIOLATION: - case ERROR_WRITE_PROTECT: - case ERROR_NETWORK_ACCESS_DENIED: - case ERROR_CANNOT_MAKE: - case ERROR_DRIVE_LOCKED: - return EACCES; - case ERROR_FILE_EXISTS: - case ERROR_ALREADY_EXISTS: - return EEXIST; - case ERROR_TOO_MANY_OPEN_FILES: - return EMFILE; - case ERROR_NOT_ENOUGH_MEMORY: - case ERROR_OUTOFMEMORY: - return ENOMEM; - case ERROR_DISK_FULL: - return ENOSPC; - default: - return EINVAL; - } -} static int NativeOpen(const std::filesystem::path& p, int nativeFlags, std::uint16_t mode) { const int access = nativeFlags & (_O_RDONLY | _O_WRONLY | _O_RDWR); const DWORD desired = access == _O_RDONLY ? GENERIC_READ : access == _O_WRONLY ? GENERIC_WRITE : GENERIC_READ | GENERIC_WRITE; @@ -65,7 +34,7 @@ static int NativeOpen(const std::filesystem::path& p, int nativeFlags, std::uint SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; const auto handle = CreateFileW(p.c_str(), desired, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &security, disposition, attributes, nullptr); if (handle == INVALID_HANDLE_VALUE) { - errno = OpenError(GetLastError()); + errno = WindowsOpenError_nid_no_patch(GetLastError()); return -1; } const int descriptor = ::_open_osfhandle(reinterpret_cast(handle), nativeFlags & (_O_APPEND | _O_RDONLY | _O_BINARY)); @@ -80,14 +49,19 @@ static int NativeRead(int fd, void* buf, std::size_t n) { if (n > static_cast(std::numeric_limits::max())) { throw std::runtime_error("sceKernelRead: nbytes exceeds platform limit"); } - return ::_read(fd, buf, static_cast(n)); + const int result = ::_read(fd, buf, static_cast(n)); + if (result < 0 && _doserrno == ERROR_NO_DATA) errno = EAGAIN; + return result; } static int NativeWrite(int fd, const void* buf, std::size_t n) { if (n > static_cast(std::numeric_limits::max())) { throw std::runtime_error("sceKernelWrite: nbytes exceeds platform limit"); } - const auto result = File::WriteSharedMemory_nid_no_patch(fd, buf, n); - return result == -2 ? ::_write(fd, buf, static_cast(n)) : static_cast(result); + const auto shared = File::WriteSharedMemory_nid_no_patch(fd, buf, n); + if (shared != -2) return static_cast(shared); + const int result = ::_write(fd, buf, static_cast(n)); + if (result < 0 && errno == ENOSPC && _doserrno == 0 && ::GetFileType(reinterpret_cast(::_get_osfhandle(fd))) == FILE_TYPE_PIPE) errno = EAGAIN; + return result; } extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); static void IgnoreInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} @@ -185,18 +159,7 @@ static int SceIoErrorFromErrno(int error) { return static_cast(0x80020000u | static_cast(guest)); } -extern "C" { - -int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { - if (path != nullptr && *path == '\0') return SCE_KERNEL_ERROR_ENOENT; - APS5_LOG_OUT("path=%s flags=0x%X nativeFlags=0x%X mode=0%o", path, flags, MapFlags(flags), mode); - if (path != nullptr && File::IsRandomDevicePath(path)) { - if ((flags & SCE_KERNEL_O_ACCMODE) != SCE_KERNEL_O_RDONLY) throw std::runtime_error(std::string(__func__) + ": writing to " + path + " is not implemented"); - const int fd = File::OpenRandomDevice(); - return fd < 0 ? SceErrorFromErrno(errno) : fd; - } - std::filesystem::path native; - if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); +static int OpenNative(const std::filesystem::path& native, int flags, std::uint16_t mode) { #ifdef _WIN32 if ((flags & (SCE_KERNEL_O_DIRECTORY | SCE_KERNEL_O_CREAT)) == SCE_KERNEL_O_DIRECTORY) { std::error_code error; @@ -204,6 +167,11 @@ int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { if (std::filesystem::exists(status) && !std::filesystem::is_directory(status)) return SceErrorFromErrno(ENOTDIR); } #endif + if ((flags & SCE_KERNEL_O_NOFOLLOW) != 0 && (flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL)) != (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL)) { + constexpr int GuestEmlink = 31; + std::error_code error; + if (std::filesystem::is_symlink(native, error)) return SceErrorFromErrno(GuestEmlink); + } int fd = NativeOpen(native, MapFlags(flags), mode); #ifdef _WIN32 if (fd < 0 && errno != ENOENT) { @@ -220,11 +188,38 @@ int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { } if ((flags & SCE_KERNEL_O_ACCMODE) != SCE_KERNEL_O_RDONLY || (flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_TRUNC))) RecordWrittenPath_nid_no_patch(native); + File::RememberDescriptor(fd, flags); return fd; } +extern "C" { + +int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { + if (path != nullptr && *path == '\0') return SCE_KERNEL_ERROR_ENOENT; + APS5_LOG_OUT("path=%s flags=0x%X nativeFlags=0x%X mode=0%o", path, flags, MapFlags(flags), mode); + if (path != nullptr && File::IsRandomDevicePath(path)) { + if ((flags & SCE_KERNEL_O_ACCMODE) != SCE_KERNEL_O_RDONLY) throw std::runtime_error(std::string(__func__) + ": writing to " + path + " is not implemented"); + const int fd = File::OpenRandomDevice(); + if (fd < 0) return SceErrorFromErrno(errno); + File::RememberDescriptor(fd, flags); + return fd; + } + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); + return OpenNative(native, flags, mode); +} + +int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std::uint16_t mode) { + int error = 0; + const char* base = GetDirectoryDescriptorPath_nid_no_patch(directory, &error); + if (base == nullptr) return SceErrorFromErrno(error); + return OpenNative(std::filesystem::path(reinterpret_cast(base)) / std::filesystem::path(reinterpret_cast(path)), flags, mode); +} + int APS5_VABI sceKernelClose(int d) { + if (d >= GuestSockets::FirstDescriptor) return GuestSockets::Close(d) == 0 ? 0 : SceKernelError(*__error_nid_postfix()); File::ForgetRandomDevice(d); + File::ForgetDescriptor(d); #ifdef _WIN32 File::ForgetDirectoryDescriptor(d); File::ForgetFileLock(d); @@ -321,27 +316,27 @@ int APS5_VABI sceKernelUnlink(const char* path) { } int APS5_VABI sceKernelFcntl(int d, int command, ...) { - if (d < GuestSockets::FirstDescriptor) { - if (!DescriptorIsOpen_nid_no_patch(d)) return SCE_KERNEL_ERROR_EBADF; - throw std::runtime_error(std::string(__func__) + ": file descriptors are not implemented, fd=" + std::to_string(d)); - } - int result; - if (command == GuestSetFlags) { + const auto type = File::FcntlArgumentOf(command); + std::intptr_t argument = 0; + if (type != File::FcntlArgument::None) { #ifdef _WIN32 __builtin_sysv_va_list arguments; __builtin_sysv_va_start(arguments, command); - const int flags = __builtin_va_arg(arguments, int); + argument = type == File::FcntlArgument::Int ? __builtin_va_arg(arguments, int) + : reinterpret_cast(__builtin_va_arg(arguments, void*)); __builtin_sysv_va_end(arguments); #else std::va_list arguments; va_start(arguments, command); - const int flags = va_arg(arguments, int); + argument = type == File::FcntlArgument::Int ? va_arg(arguments, int) + : reinterpret_cast(va_arg(arguments, void*)); va_end(arguments); #endif - result = fcntl_nid_postfix(d, command, flags); - } else { - result = fcntl_nid_postfix(d, command); } + int result; + if (d < GuestSockets::FirstDescriptor) result = File::Fcntl(d, command, argument); + else if (command == GuestSetFlags) result = fcntl_nid_postfix(d, command, static_cast(argument)); + else result = fcntl_nid_postfix(d, command); return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; } diff --git a/core/libs/prx/libkernel/File/src/RandomDevice.cpp b/core/libs/prx/libkernel/File/src/RandomDevice.cpp index 1edaddd69..1f4cfdda9 100644 --- a/core/libs/prx/libkernel/File/src/RandomDevice.cpp +++ b/core/libs/prx/libkernel/File/src/RandomDevice.cpp @@ -1,9 +1,7 @@ #include "prx/libkernel/File/include/RandomDevice.hpp" +#include "prx/libc/include/HostEntropy.hpp" -#include -#include #include -#include #include #ifdef _WIN32 @@ -47,12 +45,7 @@ bool IsRandomDevice(int fd) { bool ReadRandomDevice(int fd, void* buf, std::size_t nbytes) { std::lock_guard lock(g_mutex); if (!g_descriptors.contains(fd)) return false; - static std::random_device device; - auto* bytes = static_cast(buf); - for (std::size_t offset = 0; offset < nbytes; offset += sizeof(std::uint32_t)) { - const std::uint32_t value = device(); - std::memcpy(bytes + offset, &value, nbytes - offset < sizeof(value) ? nbytes - offset : sizeof(value)); - } + HostEntropy_nid_no_patch(buf, nbytes); return true; } diff --git a/core/libs/prx/libkernel/File/src/SharedMemory.cpp b/core/libs/prx/libkernel/File/src/SharedMemory.cpp index 8202db42e..c5b133d94 100644 --- a/core/libs/prx/libkernel/File/src/SharedMemory.cpp +++ b/core/libs/prx/libkernel/File/src/SharedMemory.cpp @@ -473,6 +473,7 @@ extern "C" int APS5_VABI shm_unlink_nid_postfix(const char* path) { #ifdef _WIN32 (void)path; NotImplemented_nid_no_patch("shm_unlink of a named object"); + return 0; #else int validationError = 0; const auto hostName = NamedHostPath(path, validationError); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index e7e0eeee2..055624dbc 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -12,10 +12,12 @@ #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libkernel/File/include/KernelStatfs.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/File/include/FileLock.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" +#include "prx/libkernel/Socket/include/SocketPoll.hpp" #include #include #include @@ -46,6 +48,14 @@ static constexpr std::uint64_t KERNEL_MNT_NOSUID = 0x8; #include #include #include "prx/libkernel/File/include/WindowsFileTime.hpp" +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); +static void IgnoreDescriptorParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} +template static auto WithoutParameterHandler(TCall call) { + const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreDescriptorParameter); + const auto result = call(); + _set_thread_local_invalid_parameter_handler(previous); + return result; +} static int NativeRmdir(const std::filesystem::path& path) { return ::_wrmdir(path.wstring().c_str()); } @@ -164,7 +174,7 @@ std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_ const auto result = File::PwriteSharedMemory_nid_no_patch(descriptor, buf, nbytes, offset); if (result != -2) return result; } - const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + const auto handle = reinterpret_cast(WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor); })); if (handle == INVALID_HANDLE_VALUE) { errno = EBADF; return -1; @@ -410,6 +420,8 @@ extern "C" int APS5_VABI pipe_nid_postfix(int* descriptors) { const int result = ::pipe(native); #endif if (result != 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); + File::RememberDescriptor(native[0], 0); + File::RememberDescriptor(native[1], 0); std::memcpy(descriptors, native, sizeof(native)); return 0; } @@ -420,14 +432,6 @@ static int PathError(const char* path) { } #ifdef _WIN32 -extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); -static void IgnoreDescriptorParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} -template static int WithoutParameterHandler(TCall call) { - const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreDescriptorParameter); - const int result = call(); - _set_thread_local_invalid_parameter_handler(previous); - return result; -} bool DescriptorIsOpen_nid_no_patch(int descriptor) { return descriptor >= 0 && WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor) == -1 ? -1 : 0; }) == 0; } @@ -454,6 +458,28 @@ static bool HasNonDirectoryPrefix(const std::filesystem::path& native) { return false; } +extern "C" int APS5_VABI dup_nid_postfix(int d); +extern "C" int APS5_VABI close_nid_postfix(int d); + +int File::DuplicateFrom(int fd, int start) { +#ifdef _WIN32 + std::vector below; + int duplicate; + while ((duplicate = dup_nid_postfix(fd)) >= 0 && duplicate < start) below.push_back(duplicate); + const int error = *__error_nid_postfix(); + for (const int descriptor : below) close_nid_postfix(descriptor); + return duplicate < 0 ? PosixFailure(error) : duplicate; +#else + const int duplicate = ::fcntl(fd, F_DUPFD, start); + if (duplicate < 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); + if (duplicate >= GuestSockets::FirstDescriptor) + throw std::runtime_error("fcntl: host descriptor reached the guest socket range"); + if (File::IsRandomDevice(fd)) File::RememberRandomDevice(duplicate); + File::ShareDescriptor(fd, duplicate); + return duplicate; +#endif +} + extern "C" { int APS5_VABI chmod_nid_postfix(const char* path, int mode) { @@ -466,11 +492,18 @@ int APS5_VABI chmod_nid_postfix(const char* path, int mode) { int APS5_VABI close_nid_postfix(int d) { if (d >= GuestSockets::FirstDescriptor) return GuestSockets::Close(d); + int socketError = 0; + if (KernelCloseSocket_nid_no_patch(d, &socketError)) { + if (socketError == 0) return 0; + *__error_nid_postfix() = socketError; + return -1; + } File::ForgetRandomDevice(d); + File::ForgetDescriptor(d); #ifdef _WIN32 File::ForgetDirectoryDescriptor(d); File::ForgetFileLock(d); - return _close(d); + return WithoutParameterHandler([d] { return ::_close(d); }); #else return ::close(d); #endif @@ -493,6 +526,7 @@ int APS5_VABI dup_nid_postfix(int d) { if (duplicate >= GuestSockets::FirstDescriptor) throw std::runtime_error(std::string(__func__) + ": host descriptor reached the guest socket range"); if (File::IsRandomDevice(d)) File::RememberRandomDevice(duplicate); + File::ShareDescriptor(d, duplicate); return duplicate; } @@ -517,6 +551,7 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { File::ForgetFileLock(to); File::ForgetRandomDevice(to); if (sourceIsRandom) File::RememberRandomDevice(to); + File::ShareDescriptor(from, to); return to; #else if (from == to) { @@ -527,6 +562,7 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { if (result < 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); File::ForgetRandomDevice(to); if (sourceIsRandom) File::RememberRandomDevice(to); + File::ShareDescriptor(from, to); return result; #endif } @@ -731,11 +767,13 @@ int APS5_VABI sceKernelFstat(int d, FileStat* sb) { } int APS5_VABI sceKernelFsync(int fd) { + if (fd >= GuestSockets::FirstDescriptor) return SceErrorFromErrno(GuestSockets::IsOpen(fd) ? GUEST_EINVAL : GUEST_EBADF); #ifdef _WIN32 - return NativeSync(fd); + if (NativeSync(fd) != 0) return SceErrorFromErrno(errno); #else - return ::fsync(fd); + if (::fsync(fd) != 0) return SceErrorFromErrno(errno); #endif + return 0; } int APS5_VABI sceKernelFdatasync(int fd) { @@ -1047,6 +1085,7 @@ int APS5_VABI sceKernelRmdir(const char* path) { std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); std::error_code error; + if (std::filesystem::is_symlink(native, error)) return SceErrorFromErrno(GUEST_ENOTDIR); if (!std::filesystem::is_directory(native, error)) { const bool blockedByFile = HasNonDirectoryPrefix(native) || std::filesystem::exists(WithoutTrailingSeparator(native), error); return SceErrorFromErrno(blockedByFile ? GUEST_ENOTDIR : GUEST_ENOENT); @@ -1141,8 +1180,7 @@ int APS5_VABI futimes_nid_postfix(int d, const KernelTimeval* times) { } int APS5_VABI fsync_nid_postfix(int fd) { - if (sceKernelFsync(fd) != 0) return PosixFailure(errno); - return 0; + return PosixResult(sceKernelFsync(fd)); } int APS5_VABI fdatasync_nid_postfix(int fd) { diff --git a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp index 0a889b3fd..ea6ce0c0b 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "../include/Pthread.hpp" @@ -33,6 +35,7 @@ int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr) p->_schedpriority = thread->priority.load(std::memory_order_relaxed); p->_affinity = thread->affinity.load(std::memory_order_relaxed); p->_affinityExplicit = true; + p->_scope = thread->scope; return 0; } @@ -48,6 +51,13 @@ int APS5_VABI pthread_attr_getguardsize_nid_postfix(const PthreadAttr* attr, siz return 0; } +int APS5_VABI pthread_attr_getinheritsched_nid_postfix(const PthreadAttr* attr, int* inherit_sched) { + if (!Valid(attr)) return PosixThread::GUEST_EINVAL; + if (!inherit_sched) throw std::runtime_error(std::string(__func__) + ": null output"); + *inherit_sched = (*attr)->_inheritsched; + return 0; +} + int APS5_VABI pthread_attr_getschedparam_nid_postfix(const PthreadAttr* attr, KernelSchedParam* param) { if (!Valid(attr) || !param) return PosixThread::GUEST_EINVAL; param->sched_priority = (*attr)->_schedpriority; @@ -60,6 +70,12 @@ int APS5_VABI pthread_attr_getschedpolicy_nid_postfix(const PthreadAttr* attr, i return 0; } +int APS5_VABI pthread_attr_getscope_nid_postfix(const PthreadAttr* attr, int* scope) { + if (!Valid(attr) || !scope) return PosixThread::GUEST_EINVAL; + *scope = (*attr)->_scope; + return 0; +} + int APS5_VABI pthread_attr_getstack_nid_postfix(const PthreadAttr* __restrict attr, void** __restrict stack_addr, size_t* __restrict stack_size) { if (!Valid(attr) || !stack_addr || !stack_size) return PosixThread::GUEST_EINVAL; *stack_addr = (*attr)->stackAddress; @@ -67,6 +83,12 @@ int APS5_VABI pthread_attr_getstack_nid_postfix(const PthreadAttr* __restrict at return 0; } +int APS5_VABI pthread_attr_getstackaddr_nid_postfix(const PthreadAttr* __restrict attr, void** __restrict stack_addr) { + if (!Valid(attr) || !stack_addr) return PosixThread::GUEST_EINVAL; + *stack_addr = (*attr)->stackAddress; + return 0; +} + int APS5_VABI pthread_attr_getstacksize_nid_postfix(const PthreadAttr* attr, size_t* stack_size) { if (!Valid(attr) || !stack_size) return PosixThread::GUEST_EINVAL; *stack_size = (*attr)->_stacksize; @@ -92,6 +114,7 @@ int APS5_VABI pthread_attr_setguardsize_nid_postfix(PthreadAttr* attr, size_t gu int APS5_VABI pthread_attr_setinheritsched_nid_postfix(PthreadAttr* attr, int inherit_sched) { if (!Valid(attr)) return PosixThread::GUEST_EINVAL; + if (inherit_sched != GUEST_INHERIT_SCHED && inherit_sched != GUEST_EXPLICIT_SCHED) return PosixThread::GUEST_ENOTSUP; (*attr)->_inheritsched = inherit_sched; return 0; } @@ -108,6 +131,12 @@ int APS5_VABI pthread_attr_setschedpolicy_nid_postfix(PthreadAttr* attr, int pol return 0; } +int APS5_VABI pthread_attr_setscope_nid_postfix(PthreadAttr* attr, int scope) { + if (!Valid(attr) || (scope != GUEST_SCOPE_PROCESS && scope != GUEST_SCOPE_SYSTEM)) return PosixThread::GUEST_EINVAL; + (*attr)->_scope = scope; + return 0; +} + int APS5_VABI pthread_attr_setstacksize_nid_postfix(PthreadAttr* attr, size_t stack_size) { if (!Valid(attr)) return PosixThread::GUEST_EINVAL; return PosixThread::ToErrno(scePthreadAttrSetstacksize(attr, stack_size)); diff --git a/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp b/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp index 140b6900e..4d03696ff 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp @@ -86,7 +86,7 @@ int APS5_VABI pthread_barrier_wait_nid_postfix(PthreadBarrierPrivate** barrier) return -1; } value.changed.wait(lock, [&] { return value.generation != generation; }); - --value.active; + if (--value.active == 0) value.changed.notify_all(); return 0; } @@ -94,8 +94,9 @@ int APS5_VABI pthread_barrier_destroy_nid_postfix(PthreadBarrierPrivate** barrie if (!barrier || !*barrier) return 22; auto* value = *barrier; { - std::lock_guard lock(value->mutex); - if (value->active != 0) return 16; + std::unique_lock lock(value->mutex); + if (value->arrived != 0) return 16; + value->changed.wait(lock, [&] { return value->active == 0; }); } delete value; *barrier = nullptr; diff --git a/core/libs/prx/libkernel/Pthread/Posix/Common.hpp b/core/libs/prx/libkernel/Pthread/Posix/Common.hpp index cf95f707e..b89859ae7 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Common.hpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Common.hpp @@ -13,6 +13,7 @@ namespace PosixThread { constexpr int GUEST_ESRCH = 3; constexpr int GUEST_EFAULT = 14; constexpr int GUEST_EINVAL = 22; +constexpr int GUEST_ENOTSUP = 45; constexpr int GUEST_ETIMEDOUT = 60; inline int ToErrno(int sceResult) { @@ -24,10 +25,18 @@ inline bool RelativeMicroseconds(int clockId, const KernelTimespec* abstime, Ker if (!abstime || abstime->tv_nsec < 0 || abstime->tv_nsec >= 1000000000) return false; KernelTimespec now{}; clock_gettime_nid_postfix(clockId, &now); - const auto deadline = std::chrono::seconds(abstime->tv_sec) + std::chrono::nanoseconds(abstime->tv_nsec); - const auto current = std::chrono::seconds(now.tv_sec) + std::chrono::nanoseconds(now.tv_nsec); - const auto remaining = std::chrono::duration_cast(deadline - current).count(); - *usec = remaining <= 0 ? 0 : remaining >= std::numeric_limits::max() ? std::numeric_limits::max() : static_cast(remaining); + constexpr auto maximum = std::numeric_limits::max(); + if (abstime->tv_sec < now.tv_sec - 1) { + *usec = 0; + return true; + } + const std::int64_t seconds = abstime->tv_sec - now.tv_sec; + if (seconds > static_cast(maximum / 1000000) + 1) { + *usec = maximum; + return true; + } + const std::int64_t remaining = (seconds * 1000000000 + abstime->tv_nsec - now.tv_nsec) / 1000; + *usec = remaining <= 0 ? 0 : remaining >= maximum ? maximum : static_cast(remaining); return true; } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp index b9a7b211c..5c5e64d18 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp @@ -41,6 +41,10 @@ int APS5_VABI pthread_cond_timedwait_nid_postfix(PthreadCond* cond, PthreadMutex return toPosix(CondOperations::AbsoluteTimedwait(cond, mutex, abstime)); } +int APS5_VABI pthread_cond_reltimedwait_np_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, KernelUseconds usec) { + return toPosix(scePthreadCondTimedwait(cond, mutex, usec)); +} + int APS5_VABI pthread_cond_wait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex) { return toPosix(scePthreadCondWait(cond, mutex)); } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp index ee0695d43..7ab97118f 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp @@ -6,14 +6,13 @@ #include "../include/Mutex.hpp" #include "Common.hpp" #include -#include -#include #include #include namespace { constexpr int POSIX_EINVAL = 22; +constexpr int GUEST_REALTIME_CLOCK = 0; constexpr int POSIX_PRIO_PROTECT = 2; constexpr int POSIX_SCHED_RR = 3; constexpr std::uintptr_t POSIX_ADAPTIVE_MUTEX_INITIALIZER = 1; @@ -62,12 +61,10 @@ int APS5_VABI pthread_mutex_lock_nid_postfix(PthreadMutex* mutex) { int APS5_VABI pthread_mutex_timedlock_nid_postfix(PthreadMutex* mutex, const KernelTimespec* abstime) { if (!abstime) throw std::runtime_error("pthread_mutex_timedlock: null abstime"); - if (abstime->tv_nsec < 0 || abstime->tv_nsec >= 1000000000) return POSIX_EINVAL; _initializeStatic(mutex, __func__); - const auto deadline = std::chrono::seconds(abstime->tv_sec) + std::chrono::nanoseconds(abstime->tv_nsec); - const auto now = std::chrono::system_clock::now().time_since_epoch(); - const auto remaining = std::chrono::duration_cast(deadline - now).count(); - const auto usec = remaining <= 0 ? 0 : remaining >= std::numeric_limits::max() ? std::numeric_limits::max() : static_cast(remaining); + if (scePthreadMutexTrylock(mutex) == 0) return 0; + KernelUseconds usec = 0; + if (!PosixThread::RelativeMicroseconds(GUEST_REALTIME_CLOCK, abstime, &usec)) return POSIX_EINVAL; return PosixThread::ToErrno(scePthreadMutexTimedlock(mutex, usec)); } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Once.cpp b/core/libs/prx/libkernel/Pthread/Posix/Once.cpp index 32ed2ee79..6919e008b 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Once.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Once.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/Pthread/include/Cancel.hpp" #include #include #include @@ -14,6 +15,14 @@ struct GuestOnce { static_assert(offsetof(GuestOnce, mutex) == 8 && sizeof(GuestOnce) == 16); std::mutex onceMutex; std::condition_variable onceChanged; + +void APS5_VABI ResetOnce(void* control) { + { + std::lock_guard lock(onceMutex); + static_cast(control)->state = 0; + } + onceChanged.notify_all(); +} } extern "C" int APS5_VABI pthread_once_nid_postfix( @@ -27,15 +36,17 @@ extern "C" int APS5_VABI pthread_once_nid_postfix( lock.unlock(); // Initializers may initialize other once controls. Never run guest code // under the shared lock. The lock publishes all initializer writes. + CleanupHandler reset{}; + void* const mark = ThreadCancel::CleanupMark(); + __pthread_cleanup_push_imp_nid_postfix(ResetOnce, control, &reset); try { initialize(); } catch (...) { - lock.lock(); - control->state = 0; - lock.unlock(); - onceChanged.notify_all(); + ThreadCancel::CleanupRestore(mark); + ResetOnce(control); throw; } + ThreadCancel::CleanupRestore(mark); lock.lock(); control->state = 1; lock.unlock(); diff --git a/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp b/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp index e71072e2a..d11bda07a 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp @@ -84,6 +84,14 @@ int APS5_VABI sem_trywait_nid_postfix(void* sem) { int APS5_VABI sem_timedwait_nid_postfix(void* sem, const KernelTimespec* abstime) { auto* semaphore = Get(sem); if (!semaphore) return Fail(PosixThread::GUEST_EINVAL); + { + std::lock_guard lock(semaphore->lock); + if (semaphore->count > 0) { + --semaphore->count; + return 0; + } + } + if (!abstime) return WaitFor(semaphore, nullptr); KernelUseconds usec = 0; if (!PosixThread::RelativeMicroseconds(GUEST_REALTIME_CLOCK, abstime, &usec)) return Fail(PosixThread::GUEST_EINVAL); return WaitFor(semaphore, &usec); diff --git a/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp b/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp new file mode 100644 index 000000000..3990f0d29 --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp @@ -0,0 +1,76 @@ +#include "SceTypes.hpp" +#include "Common.hpp" +#include +#include +#include +#include + +struct PthreadSpinlockPrivate { + std::atomic owner{}; +}; + +namespace { + +constexpr int GUEST_EPERM = 1; +constexpr int GUEST_ENOMEM = 12; +constexpr int GUEST_EBUSY = 16; +constexpr int PROCESS_PRIVATE = 0; +constexpr int PROCESS_SHARED = 1; +constexpr int SPIN_COUNT = 100000; + +bool tryAcquire(PthreadSpinlockPrivate* spinlock) { + std::thread::id unowned{}; + return spinlock->owner.compare_exchange_strong(unowned, std::this_thread::get_id(), std::memory_order_acquire, std::memory_order_relaxed); +} + +} + +extern "C" { + +int APS5_VABI pthread_spin_init_nid_postfix(PthreadSpinlockPrivate** lock, int pshared) { + if (!lock) return PosixThread::GUEST_EINVAL; + if (pshared != PROCESS_PRIVATE && pshared != PROCESS_SHARED) return PosixThread::GUEST_EINVAL; + auto* spinlock = new (std::nothrow) PthreadSpinlockPrivate; + if (!spinlock) return GUEST_ENOMEM; + *lock = spinlock; + return 0; +} + +int APS5_VABI pthread_spin_destroy_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + delete *lock; + *lock = nullptr; + return 0; +} + +int APS5_VABI pthread_spin_trylock_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + return tryAcquire(*lock) ? 0 : GUEST_EBUSY; +} + +int APS5_VABI pthread_spin_lock_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + auto* spinlock = *lock; + if (spinlock->owner.load(std::memory_order_relaxed) == std::this_thread::get_id()) + throw std::runtime_error("pthread_spin_lock: the calling thread already holds the lock and would spin forever"); + int count = SPIN_COUNT; + while (!tryAcquire(spinlock)) { + while (spinlock->owner.load(std::memory_order_relaxed) != std::thread::id{}) { + if (--count <= 0) { + count = SPIN_COUNT; + std::this_thread::yield(); + } + } + } + return 0; +} + +int APS5_VABI pthread_spin_unlock_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + auto owner = std::this_thread::get_id(); + if (!(*lock)->owner.compare_exchange_strong(owner, std::thread::id{}, std::memory_order_release, std::memory_order_relaxed)) + return GUEST_EPERM; + return 0; +} + +} diff --git a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp index 26a7c7176..6a3dad661 100644 --- a/core/libs/prx/libkernel/Pthread/include/Cancel.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Cancel.hpp @@ -12,6 +12,13 @@ inline constexpr int GUEST_CANCEL_DEFERRED = 0; inline constexpr int GUEST_CANCEL_ASYNCHRONOUS = 2; inline void* const GUEST_CANCELED = reinterpret_cast(std::uintptr_t{1}); +struct CleanupHandler { + CleanupHandler* previous; + void (APS5_VABI* routine)(void*); + void* argument; + int onHeap; +}; + extern "C" { void APS5_VABI __pthread_cleanup_push_imp_nid_postfix(void (APS5_VABI* routine)(void*), void* argument, void* info); void APS5_VABI __pthread_cleanup_pop_imp_nid_postfix(int execute); @@ -21,6 +28,8 @@ namespace ThreadCancel { bool Requested(); void Check(); +void* CleanupMark(); +void CleanupRestore(void* mark); void BeginWait(TimedWait::Condition* condition); void EndWait(); diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index 05177ac38..5ddf36a9f 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -83,6 +83,10 @@ static constexpr int DEFAULT_THREAD_PRIORITY = 700; static constexpr std::uint32_t CPU_CLOCK_BIT = 0x80000000u; static constexpr std::uint32_t CPU_CLOCK_PROCESS_BIT = 0x40000000u; static constexpr std::uint32_t CPU_CLOCK_ID_MASK = ~(CPU_CLOCK_BIT | CPU_CLOCK_PROCESS_BIT); +static constexpr int GUEST_EXPLICIT_SCHED = 0; +static constexpr int GUEST_INHERIT_SCHED = 4; +static constexpr int GUEST_SCOPE_PROCESS = 0; +static constexpr int GUEST_SCOPE_SYSTEM = 2; inline int GuestCpuFromHost(unsigned hostCpu, KernelCpumask threadAffinity) { KernelCpumask mask = threadAffinity & DEFAULT_THREAD_AFFINITY; @@ -102,6 +106,7 @@ struct PthreadAttrPrivate { bool _affinityExplicit = false; std::size_t _guardsize = 0x1000; int _solosched = 0; + int _scope = GUEST_SCOPE_SYSTEM; }; inline std::atomic nextThreadId{100000}; @@ -123,6 +128,7 @@ struct PthreadPrivate { std::atomic waitCount{0}; std::atomic affinity{DEFAULT_THREAD_AFFINITY}; std::atomic priority{DEFAULT_THREAD_PRIORITY}; + int scope = GUEST_SCOPE_SYSTEM; int cpuClockThread = 0; #ifndef _WIN32 clockid_t hostCpuClock{}; diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index ff5cba730..9d2f03274 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -33,29 +33,31 @@ int APS5_VABI scePthreadAttrInit(PthreadAttr* attr) { } int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; delete *attr; *attr = nullptr; return SCE_OK; } int APS5_VABI scePthreadAttrSetdetachstate(PthreadAttr* attr, int detachstate) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); - if (detachstate != DETACH_JOINABLE && detachstate != DETACH_DETACHED) - throw std::runtime_error(std::string(__func__) + ": invalid state"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + if (detachstate != DETACH_JOINABLE && detachstate != DETACH_DETACHED) return SCE_KERNEL_ERROR_EINVAL; (*attr)->_detachstate = detachstate; return SCE_OK; } int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param) { - if (!attr || !*attr || !param) - throw std::runtime_error(std::string(__func__) + ": null arg"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + constexpr int HighestPriority = 256; + constexpr int LowestPriority = 767; + if (!param || param->sched_priority < HighestPriority || param->sched_priority > LowestPriority) + return SCE_KERNEL_ERROR_EOPNOTSUPP; (*attr)->_schedpriority = param->sched_priority; return SCE_OK; } int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stacksize) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; if (stacksize < MIN_STACK_SIZE) return SCE_KERNEL_ERROR_EINVAL; #ifdef _WIN32 if (stacksize > std::numeric_limits::max() - 0xffffu) @@ -84,18 +86,19 @@ int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr) { (*attr)->_inheritsched = 4; (*attr)->_affinity = thread->affinity.load(std::memory_order_relaxed); (*attr)->_affinityExplicit = true; + (*attr)->_scope = thread->scope; return SCE_OK; } int APS5_VABI scePthreadAttrGetaffinity(const PthreadAttr* attr, KernelCpumask* mask) { - if (!attr || !*attr || !mask) throw std::runtime_error("scePthreadAttrGetaffinity: null argument"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + if (!mask) throw std::runtime_error("scePthreadAttrGetaffinity: null argument"); *mask = (*attr)->_affinity; return SCE_OK; } int APS5_VABI scePthreadAttrGetdetachstate(const PthreadAttr* attr, int* state) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); - if (!state) throw std::runtime_error(std::string(__func__) + ": null output"); + if (!attr || !*attr || !state) return SCE_KERNEL_ERROR_EINVAL; *state = (*attr)->_detachstate; return SCE_OK; } @@ -108,14 +111,13 @@ int APS5_VABI scePthreadAttrGetguardsize(const PthreadAttr* attr, size_t* guard_ } int APS5_VABI scePthreadAttrGetschedparam(const PthreadAttr* attr, KernelSchedParam* param) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); - if (!param) throw std::runtime_error(std::string(__func__) + ": null output"); + if (!attr || !*attr || !param) return SCE_KERNEL_ERROR_EINVAL; param->sched_priority = (*attr)->_schedpriority; return SCE_OK; } int APS5_VABI scePthreadAttrGetsolosched(const PthreadAttr* attr, int* solosched) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; if (!solosched) throw std::runtime_error(std::string(__func__) + ": null output"); *solosched = (*attr)->_solosched; return SCE_OK; @@ -129,14 +131,13 @@ int APS5_VABI scePthreadAttrGetstackaddr(const PthreadAttr* attr, void** stack_a } int APS5_VABI scePthreadAttrGetstacksize(const PthreadAttr* attr, size_t* stack_size) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); - if (!stack_size) throw std::runtime_error(std::string(__func__) + ": null output"); + if (!attr || !*attr || !stack_size) return SCE_KERNEL_ERROR_EINVAL; *stack_size = (*attr)->_stacksize; return SCE_OK; } int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr* attr, KernelCpumask mask) { - if (!attr || !*attr) throw std::runtime_error("scePthreadAttrSetaffinity: null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; (*attr)->_affinity = mask; (*attr)->_affinityExplicit = true; return SCE_OK; @@ -149,19 +150,27 @@ int APS5_VABI scePthreadAttrSetguardsize(PthreadAttr* attr, size_t guard_size) { } int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inherit_sched) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + constexpr int ExplicitSched = 0; + constexpr int InheritSched = 4; + if (inherit_sched != ExplicitSched && inherit_sched != InheritSched) return SCE_KERNEL_ERROR_EOPNOTSUPP; (*attr)->_inheritsched = inherit_sched; return SCE_OK; } int APS5_VABI scePthreadAttrSetschedpolicy(PthreadAttr* attr, int policy) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + constexpr int SchedRr = 3; + if (policy != SCHED_FIFO_PS5 && policy != SchedRr) return SCE_KERNEL_ERROR_EOPNOTSUPP; (*attr)->_schedpolicy = policy; return SCE_OK; } int APS5_VABI scePthreadAttrSetsolosched(PthreadAttr* attr, int solosched) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + constexpr int SoloSchedOff = 0; + constexpr int SoloSchedOn = 16; + if (solosched != SoloSchedOff && solosched != SoloSchedOn) return SCE_KERNEL_ERROR_EOPNOTSUPP; (*attr)->_solosched = solosched; return SCE_OK; } diff --git a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp index 605874cd0..4cf22db6e 100644 --- a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp @@ -111,7 +111,7 @@ int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr) { int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr) { if (!attr || !*attr) - throw std::invalid_argument("Mutex attributes are not initialized"); + return SCE_KERNEL_ERROR_EINVAL; delete *attr; *attr = nullptr; return 0; @@ -119,7 +119,7 @@ int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr) { int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type) { if (!attr || !*attr) - throw std::invalid_argument("Mutex attributes are not initialized"); + return SCE_KERNEL_ERROR_EINVAL; switch (type) { case 1: (*attr)->type = MutexType::ErrorCheck; break; case 2: (*attr)->type = MutexType::Recursive; break; @@ -132,21 +132,19 @@ int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type) { int APS5_VABI scePthreadMutexattrSetprotocol(PthreadMutexattr* attr, int protocol) { if (!attr || !*attr) - throw std::invalid_argument("Mutex attributes are not initialized"); + return SCE_KERNEL_ERROR_EINVAL; constexpr int PrioNone = 0; - constexpr int PrioInherit = 1; - if (protocol != PrioNone && protocol != PrioInherit) - throw std::invalid_argument("Mutex priority protection is unsupported"); + constexpr int PrioProtect = 2; + if (protocol < PrioNone || protocol > PrioProtect) + return SCE_KERNEL_ERROR_EINVAL; return 0; } int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char*) { if (!mutex) throw std::invalid_argument("Mutex pointer is null"); - if (attr && !*attr) - throw std::invalid_argument("Mutex attributes are not initialized"); auto replacement = std::make_unique(); - if (attr) + if (attr && *attr) replacement->_type = (*attr)->type; std::lock_guard lock(initializationMutex); std::atomic_ref(*mutex).store(replacement.release(), std::memory_order_release); @@ -158,7 +156,7 @@ int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex) { throw std::invalid_argument("Mutex pointer is null"); std::lock_guard lock(initializationMutex); if (*mutex == destroyedMutex()) - throw std::runtime_error("Mutex has already been destroyed"); + return SCE_KERNEL_ERROR_EINVAL; if (*mutex == adaptiveInitializer()) { std::atomic_ref(*mutex).store(destroyedMutex(), std::memory_order_release); return 0; diff --git a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp index cd8890dcd..3ff9b3088 100644 --- a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp @@ -2,6 +2,7 @@ #include "../include/Rwlock.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" +#include #include #include #include @@ -12,10 +13,22 @@ static constexpr int SCE_KERNEL_ERROR_ENOMEM = 0x8002000C; static constexpr int SCE_KERNEL_ERROR_EDEADLK = 0x8002000B; static constexpr int SCE_KERNEL_ERROR_EBUSY = 0x80020010; static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = 0x8002003C; +static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; + +static PthreadRwlockPrivate* const DestroyedRwlock = reinterpret_cast(std::uintptr_t{1}); static PthreadRwlockPrivate* RequireRwlock(PthreadRwlock* rwlock, const char* funcName) { - if (!rwlock || !*rwlock) throw std::runtime_error(std::string(funcName) + ": null rwlock"); - return *rwlock; + if (!rwlock) throw std::runtime_error(std::string(funcName) + ": null rwlock"); + std::atomic_ref slot(*rwlock); + PthreadRwlock current = slot.load(std::memory_order_acquire); + if (current == nullptr) { + auto* created = new PthreadRwlockPrivate(); + if (slot.compare_exchange_strong(current, created, std::memory_order_acq_rel, std::memory_order_acquire)) + current = created; + else + delete created; + } + return current == DestroyedRwlock ? nullptr : current; } static bool OwnsWrite(const PthreadRwlockPrivate* lock) { @@ -25,12 +38,16 @@ static bool OwnsWrite(const PthreadRwlockPrivate* lock) { extern "C" { int APS5_VABI scePthreadRwlockDestroy(PthreadRwlock* rwlock) { - auto* lock = RequireRwlock(rwlock, __func__); - if (lock->_writer.load(std::memory_order_acquire) != std::thread::id{} || - lock->_readers.load(std::memory_order_acquire) != 0) - return SCE_KERNEL_ERROR_EBUSY; - delete lock; - *rwlock = nullptr; + if (!rwlock) throw std::runtime_error("scePthreadRwlockDestroy: null rwlock"); + std::atomic_ref slot(*rwlock); + PthreadRwlock lock = slot.load(std::memory_order_acquire); + do { + if (lock == nullptr) return SCE_OK; + if (lock == DestroyedRwlock) return SCE_KERNEL_ERROR_EINVAL; + } while (!slot.compare_exchange_weak(lock, DestroyedRwlock, std::memory_order_acq_rel, std::memory_order_acquire)); + if (lock->_writer.load(std::memory_order_acquire) == std::thread::id{} && + lock->_readers.load(std::memory_order_acquire) == 0) + delete lock; return SCE_OK; } @@ -46,6 +63,7 @@ int APS5_VABI scePthreadRwlockInit(PthreadRwlock* rwlock, const PthreadRwlockatt int APS5_VABI scePthreadRwlockRdlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; #ifdef _WIN32 while (!lock->_lock.try_lock_shared()) TimedWait::SleepNanos(1000000); @@ -58,6 +76,7 @@ int APS5_VABI scePthreadRwlockRdlock(PthreadRwlock* rwlock) { int APS5_VABI scePthreadRwlockTryrdlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EBUSY; if (!lock->_lock.try_lock_shared()) return SCE_KERNEL_ERROR_EBUSY; lock->_readers.fetch_add(1, std::memory_order_release); @@ -66,6 +85,7 @@ int APS5_VABI scePthreadRwlockTryrdlock(PthreadRwlock* rwlock) { int APS5_VABI scePthreadRwlockTimedrdlock(PthreadRwlock* rwlock, KernelUseconds usec) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; const bool locked = TimedWait::AcquireUntil(TimedWait::DeadlineNanos(usec), [&] { return lock->_lock.try_lock_shared(); }, [&](std::uint64_t micros) { return lock->_lock.try_lock_shared_for(std::chrono::microseconds(micros)); }); if (!locked) return SCE_KERNEL_ERROR_ETIMEDOUT; @@ -75,6 +95,7 @@ int APS5_VABI scePthreadRwlockTimedrdlock(PthreadRwlock* rwlock, KernelUseconds int APS5_VABI scePthreadRwlockWrlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; #ifdef _WIN32 while (!lock->_lock.try_lock()) TimedWait::SleepNanos(1000000); @@ -87,6 +108,7 @@ int APS5_VABI scePthreadRwlockWrlock(PthreadRwlock* rwlock) { int APS5_VABI scePthreadRwlockTrywrlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock) || !lock->_lock.try_lock()) return SCE_KERNEL_ERROR_EBUSY; lock->_writer.store(std::this_thread::get_id(), std::memory_order_release); return SCE_OK; @@ -94,6 +116,7 @@ int APS5_VABI scePthreadRwlockTrywrlock(PthreadRwlock* rwlock) { int APS5_VABI scePthreadRwlockTimedwrlock(PthreadRwlock* rwlock, KernelUseconds usec) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; const bool locked = TimedWait::AcquireUntil(TimedWait::DeadlineNanos(usec), [&] { return lock->_lock.try_lock(); }, [&](std::uint64_t micros) { return lock->_lock.try_lock_for(std::chrono::microseconds(micros)); }); if (!locked) return SCE_KERNEL_ERROR_ETIMEDOUT; @@ -103,6 +126,7 @@ int APS5_VABI scePthreadRwlockTimedwrlock(PthreadRwlock* rwlock, KernelUseconds int APS5_VABI scePthreadRwlockUnlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) { lock->_writer.store(std::thread::id{}, std::memory_order_release); lock->_lock.unlock(); diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index fada8e72a..34e9ea435 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -53,12 +53,6 @@ struct ThreadArgs { static std::atomic threadDtors{nullptr}; static std::atomic threadAtexitCount{nullptr}; static std::atomic threadAtexitReport{nullptr}; -struct CleanupHandler { - CleanupHandler* previous; - void (APS5_VABI* routine)(void*); - void* argument; - int onHeap; -}; struct GuestThreadState { PthreadPrivate* current = nullptr; @@ -371,6 +365,14 @@ void ThreadCancel::Check() { if (Requested()) scePthreadExit(GUEST_CANCELED); } +void* ThreadCancel::CleanupMark() { + return CurrentGuestState().cleanupHandlers; +} + +void ThreadCancel::CleanupRestore(void* mark) { + CurrentGuestState().cleanupHandlers = static_cast(mark); +} + void ThreadCancel::BeginWait(TimedWait::Condition* condition) { if (!CurrentGuestState().current) return; std::lock_guard lock(CurrentGuestState().current->cancelLock); @@ -533,7 +535,7 @@ extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name) { if (!thread || !entry) throw std::runtime_error("scePthreadCreate: null arg"); - if (attr && !*attr) throw std::runtime_error("scePthreadCreate: null attributes"); + if (attr && !*attr) attr = nullptr; auto p = std::make_unique(attr ? *attr : nullptr); bool detached = false; if (attr && *attr) detached = ((*attr)->_detachstate == DETACH_DETACHED); @@ -541,6 +543,12 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread p->stackSize = attr ? (*attr)->_stacksize : DEFAULT_STACK_SIZE; if (attr) { p->priority.store((*attr)->_schedpriority, std::memory_order_relaxed); + if ((*attr)->_inheritsched != GUEST_INHERIT_SCHED) + p->scope = (*attr)->_scope; + else if (CurrentGuestState().current) + p->scope = CurrentGuestState().current->scope; + } else if (CurrentGuestState().current) { + p->scope = CurrentGuestState().current->scope; } if (name) p->name = name; std::promise start; @@ -618,10 +626,9 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread } int APS5_VABI scePthreadJoin(Pthread thread, void** retval) { - if (!thread) throw std::runtime_error("scePthreadJoin: null thread"); + if (!thread) return SCE_KERNEL_ERROR_EINVAL; if (thread->_detached) return SCE_KERNEL_ERROR_EINVAL; - if (thread == CurrentGuestState().current) - throw std::runtime_error("scePthreadJoin: cannot join current thread"); + if (thread == CurrentGuestState().current) return SCE_KERNEL_ERROR_EDEADLK; ThreadCancel::Check(); { std::unique_lock lock(thread->_join_mtx); @@ -642,7 +649,7 @@ int APS5_VABI scePthreadJoin(Pthread thread, void** retval) { } int APS5_VABI scePthreadDetach(Pthread thread) { - if (!thread) throw std::runtime_error("scePthreadDetach: null thread"); + if (!thread) return SCE_KERNEL_ERROR_EINVAL; if (thread->_detached) return SCE_KERNEL_ERROR_EINVAL; thread->_detached = true; #ifdef _WIN32 @@ -814,20 +821,36 @@ int APS5_VABI scePthreadSetprio(Pthread thread, int prio) { return SCE_OK; } +static void APS5_VABI ResetOnce(void* once) { + constexpr int32_t Never = 0; + std::atomic_ref state(*static_cast(once)); + state.store(Never, std::memory_order_release); + state.notify_all(); +} + int APS5_VABI scePthreadOnce(int32_t* once, void (APS5_VABI* init)(void)) { if (!once || !init) return SCE_KERNEL_ERROR_EINVAL; constexpr int32_t Never = 0; constexpr int32_t Done = 1; constexpr int32_t Running = 2; std::atomic_ref state(*once); - int32_t expected = Never; - if (state.compare_exchange_strong(expected, Running, std::memory_order_acq_rel)) { - init(); - state.store(Done, std::memory_order_release); - state.notify_all(); - return SCE_OK; + for (int32_t expected = Never; !state.compare_exchange_strong(expected, Running, std::memory_order_acq_rel); expected = Never) { + if (expected != Running) return SCE_OK; + state.wait(Running, std::memory_order_acquire); } - while ((expected = state.load(std::memory_order_acquire)) == Running) state.wait(Running, std::memory_order_acquire); + CleanupHandler reset{}; + void* const mark = ThreadCancel::CleanupMark(); + __pthread_cleanup_push_imp_nid_postfix(ResetOnce, once, &reset); + try { + init(); + } catch (...) { + ThreadCancel::CleanupRestore(mark); + ResetOnce(once); + throw; + } + ThreadCancel::CleanupRestore(mark); + state.store(Done, std::memory_order_release); + state.notify_all(); return SCE_OK; } diff --git a/core/libs/prx/libkernel/Pthread/src/Tsd.cpp b/core/libs/prx/libkernel/Pthread/src/Tsd.cpp index f9fa4ecc6..03c706c20 100644 --- a/core/libs/prx/libkernel/Pthread/src/Tsd.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Tsd.cpp @@ -64,6 +64,11 @@ struct ThreadValues { } }; +static int KeySlot(PthreadKey key) { + const unsigned slot = static_cast(key) - 1u; + return slot < static_cast(MAX_KEYS) ? static_cast(slot) : -1; +} + static ThreadValues& Values() { #ifdef _WIN32 return WindowsThreadLocal::Get(); @@ -86,7 +91,7 @@ int APS5_VABI scePthreadKeyCreate(PthreadKey* key, pthread_key_destructor_func_t if (g_keys[index].sequence.load(std::memory_order_relaxed) == FREE_KEY) { g_keys[index].destructor = reinterpret_cast(destructor); g_keys[index].sequence.store(++g_lastSequence, std::memory_order_release); - *key = index; + *key = index + 1; return SCE_OK; } } @@ -94,25 +99,28 @@ int APS5_VABI scePthreadKeyCreate(PthreadKey* key, pthread_key_destructor_func_t } int APS5_VABI scePthreadKeyDelete(PthreadKey key) { - if (key < 0 || key >= MAX_KEYS) return SCE_KERNEL_ERROR_EINVAL; + const int slot = KeySlot(key); + if (slot < 0) return SCE_KERNEL_ERROR_EINVAL; std::lock_guard lock(g_keyLock); - if (g_keys[key].sequence.load(std::memory_order_relaxed) == FREE_KEY) return SCE_KERNEL_ERROR_EINVAL; - g_keys[key].sequence.store(FREE_KEY, std::memory_order_release); - g_keys[key].destructor = nullptr; + if (g_keys[slot].sequence.load(std::memory_order_relaxed) == FREE_KEY) return SCE_KERNEL_ERROR_EINVAL; + g_keys[slot].sequence.store(FREE_KEY, std::memory_order_release); + g_keys[slot].destructor = nullptr; return SCE_OK; } void* APS5_VABI scePthreadGetspecific(PthreadKey key) { - if (key < 0 || key >= MAX_KEYS) return nullptr; - const ThreadValue& entry = Values().values[key]; - return entry.sequence == g_keys[key].sequence.load(std::memory_order_acquire) ? entry.value : nullptr; + const int slot = KeySlot(key); + if (slot < 0) return nullptr; + const ThreadValue& entry = Values().values[slot]; + return entry.sequence == g_keys[slot].sequence.load(std::memory_order_acquire) ? entry.value : nullptr; } int APS5_VABI scePthreadSetspecific(PthreadKey key, void* value) { - if (key < 0 || key >= MAX_KEYS) return SCE_KERNEL_ERROR_EINVAL; - const std::uint64_t sequence = g_keys[key].sequence.load(std::memory_order_acquire); + const int slot = KeySlot(key); + if (slot < 0) return SCE_KERNEL_ERROR_EINVAL; + const std::uint64_t sequence = g_keys[slot].sequence.load(std::memory_order_acquire); if (sequence == FREE_KEY) return SCE_KERNEL_ERROR_EINVAL; - Values().values[key] = {value, sequence}; + Values().values[slot] = {value, sequence}; return SCE_OK; } diff --git a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp index 8210cdb99..7375ae7ff 100644 --- a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp +++ b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp @@ -88,7 +88,9 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time) return KERNEL_SEMA_OK; } - const bool acquired = sem->condition.WaitUntil(lock, TimedWait::DeadlineNanos(*time), released); + const std::uint64_t deadline = TimedWait::DeadlineNanos(*time); + const bool acquired = sem->condition.WaitUntil(lock, deadline, released); + *time = static_cast(TimedWait::RemainingMicros(deadline)); traceWait(!acquired); if (sem->deleted) { return SCE_KERNEL_ERROR_EACCES; diff --git a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp index 71d5f62e7..c990c6686 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp @@ -16,6 +16,12 @@ struct Entry { }; using Poller = int (*)(Entry* entries, int count, int timeoutMilliseconds); +using Closer = bool (*)(int descriptor, int* error); +using OptionSetter = bool (*)(int descriptor, int level, int option, const void* value, std::uint32_t length, int* error); } extern "C" void KernelSetSocketPoller_nid_no_patch(KernelSocketPoll::Poller poller); +extern "C" void KernelSetSocketCloser_nid_no_patch(KernelSocketPoll::Closer closer); +extern "C" bool KernelCloseSocket_nid_no_patch(int descriptor, int* error); +extern "C" void KernelSetSocketOptionSetter_nid_no_patch(KernelSocketPoll::OptionSetter setter); +extern "C" bool KernelSetSocketOption_nid_no_patch(int descriptor, int level, int option, const void* value, std::uint32_t length, int* error); diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index 3f71f4f1c..f92f2d013 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -31,6 +31,8 @@ struct GuestTimeval { }; std::atomic g_socketPoller{nullptr}; +std::atomic g_socketCloser{nullptr}; +std::atomic g_socketOptionSetter{nullptr}; bool DescriptorSet(const void* set, int descriptor) { return set != nullptr && ((static_cast(set)[descriptor / 64] >> (descriptor % 64)) & 1u) != 0; @@ -105,6 +107,24 @@ void KernelSetSocketPoller_nid_no_patch(KernelSocketPoll::Poller poller) { g_socketPoller.store(poller); } +void KernelSetSocketCloser_nid_no_patch(KernelSocketPoll::Closer closer) { + g_socketCloser.store(closer); +} + +bool KernelCloseSocket_nid_no_patch(int descriptor, int* error) { + const auto closer = g_socketCloser.load(); + return closer != nullptr && closer(descriptor, error); +} + +void KernelSetSocketOptionSetter_nid_no_patch(KernelSocketPoll::OptionSetter setter) { + g_socketOptionSetter.store(setter); +} + +bool KernelSetSocketOption_nid_no_patch(int descriptor, int level, int option, const void* value, std::uint32_t length, int* error) { + const auto setter = g_socketOptionSetter.load(); + return setter != nullptr && setter(descriptor, level, option, value, length, error); +} + int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout) { const auto* limit = static_cast(timeout); if (nfds < 0 || nfds > SelectDescriptorLimit || (limit != nullptr && (limit->seconds < 0 || limit->microseconds < 0 || limit->microseconds >= 1000000))) { @@ -186,6 +206,7 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* waitMilliseconds = static_cast(std::clamp(roundedMilliseconds, 0, 100)); } if (devicesReady) waitMilliseconds = 0; +#ifndef _WIN32 if (!hosts.empty() && !sockets.empty()) { if (poller == nullptr) UnknownDescriptor(sockets.front().descriptor, false); const auto sliceEnd = std::chrono::steady_clock::now() + std::chrono::milliseconds(waitMilliseconds); @@ -214,7 +235,9 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* return -1; } for (std::size_t index = 0; index < hosts.size(); ++index) requested[hostSlots[index]].revents = hosts[index].revents; - } else if (!sockets.empty()) { + } else +#endif + if (!sockets.empty()) { if (poller == nullptr) UnknownDescriptor(sockets.front().descriptor, false); for (auto& socket : sockets) socket.revents = 0; const int result = poller(sockets.data(), static_cast(sockets.size()), waitMilliseconds); diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 52f37a428..8787a302f 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -14,10 +14,13 @@ #endif #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libkernel/Socket/include/SocketError.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" +#include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" +#include "prx/libkernel/Socket/include/SocketPoll.hpp" #include #include #include @@ -60,6 +63,14 @@ int NativeError() { return GuestSockets::GuestErrorFromHost(errno); #endif } +int SendError(int type) { +#ifdef _WIN32 + if (type == 2 && WSAGetLastError() == WSAENOTCONN) return 39; +#else + static_cast(type); +#endif + return NativeError(); +} struct Socket { NativeSocket value; int family; @@ -110,8 +121,8 @@ struct Waker { } }; Waker& ThreadWaker() { - thread_local Waker waker; - return waker; + struct WakerTag {}; + return HostThreadLocal(); } std::mutex socketsMutex; std::map> sockets; @@ -171,8 +182,9 @@ void GuestAddress(const sockaddr_storage& native, void* output, std::uint32_t* l std::memcpy(bytes + 8, &v6.sin6_addr, 16); std::memcpy(bytes + 24, &v6.sin6_scope_id, 4); } - std::memcpy(output, bytes, std::min(*length, bytes[0])); - *length = bytes[0]; + const auto copied = std::min(*length, bytes[0]); + std::memcpy(output, bytes, copied); + *length = copied; } } @@ -434,6 +446,26 @@ bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* e extern "C" { int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { + if (descriptor < GuestSockets::FirstDescriptor) { + const auto type = File::FcntlArgumentOf(command); + std::intptr_t argument = 0; + if (type != File::FcntlArgument::None) { +#ifdef _WIN32 + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, command); + argument = type == File::FcntlArgument::Int ? __builtin_va_arg(arguments, int) + : reinterpret_cast(__builtin_va_arg(arguments, void*)); + __builtin_sysv_va_end(arguments); +#else + std::va_list arguments; + va_start(arguments, command); + argument = type == File::FcntlArgument::Int ? va_arg(arguments, int) + : reinterpret_cast(va_arg(arguments, void*)); + va_end(arguments); +#endif + } + return File::Fcntl(descriptor, command, argument); + } const auto socket = Lookup(descriptor); if (!socket) return -1; std::lock_guard lock(socket->modeMutex); @@ -463,6 +495,10 @@ int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { } int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, const void* value, std::uint32_t length) { + int netError = 0; + if (descriptor < GuestSockets::FirstDescriptor && KernelSetSocketOption_nid_no_patch(descriptor, level, option, value, length, &netError)) { + return netError == 0 ? 0 : Fail(netError); + } const auto socket = Lookup(descriptor); if (!socket) return -1; const int nativeOption = Option(option); @@ -487,9 +523,11 @@ int APS5_VABI getsockopt_nid_postfix(int descriptor, int level, int option, return 0; } int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { + const bool nonblocking = (type & GuestNonBlocking) != 0; + const int plainType = type & ~(GuestCloseOnExec | GuestNonBlocking); if (family != 2 && family != 28) return Fail(47); - const int nativeType = type == 1 ? SOCK_STREAM : type == 2 ? SOCK_DGRAM : -1; - const int expectedProtocol = type == 1 ? 6 : 17; + const int nativeType = plainType == 1 ? SOCK_STREAM : plainType == 2 ? SOCK_DGRAM : -1; + const int expectedProtocol = plainType == 1 ? 6 : 17; if (nativeType == -1 || (protocol != 0 && protocol != expectedProtocol)) return Fail(43); #ifdef _WIN32 static const int startup = [] { WSADATA data{}; return WSAStartup(MAKEWORD(2, 2), &data); }(); @@ -497,10 +535,11 @@ int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { #endif const auto native = ::socket(family == 2 ? AF_INET : AF_INET6, nativeType, protocol); if (native == Invalid) return Fail(NativeError()); - Socket guard(native, family, type); + Socket guard(native, family, plainType); try { - auto socket = std::make_shared(native, family, type); + auto socket = std::make_shared(native, family, plainType); guard.value = Invalid; + if (nonblocking && SetNonBlocking(*socket)) return -1; std::lock_guard lock(socketsMutex); if (nextDescriptor == INT_MAX) return Fail(24); const int descriptor = nextDescriptor++; @@ -597,7 +636,7 @@ std::int64_t APS5_VABI send_nid_postfix(int descriptor, const void* buffer, std: if (length > INT_MAX) return Fail(40); if (!buffer && length) return Fail(14); const auto result = ::send(socket->value, static_cast(buffer), static_cast(length), NativeSendFlags(flags)); - return result < 0 ? Fail(NativeError()) : result; + return result < 0 ? Fail(SendError(socket->type)) : result; } std::int64_t APS5_VABI recv_nid_postfix(int descriptor, void* buffer, std::uint64_t length, int flags) { const auto socket = Lookup(descriptor); @@ -605,6 +644,8 @@ std::int64_t APS5_VABI recv_nid_postfix(int descriptor, void* buffer, std::uint6 if ((flags & ~2) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if (!buffer && length) return Fail(14); + const GuestArena::HostWrite destination(length == 0 ? nullptr : buffer, static_cast(length)); + if (!destination.Open()) return Fail(14); const auto result = ::recv(socket->value, static_cast(buffer), static_cast(length), flags & 2 ? MSG_PEEK : 0); #ifdef _WIN32 @@ -649,6 +690,7 @@ int APS5_VABI getpeername_nid_postfix(int descriptor, void* address, std::uint32 return 0; } int APS5_VABI ioctl_nid_postfix(int descriptor, std::uint64_t request, void* argument) { + if (descriptor < GuestSockets::FirstDescriptor) return File::Ioctl(descriptor, request, argument); const auto socket = Lookup(descriptor); if (!socket) return -1; if (!argument) return Fail(14); @@ -690,7 +732,7 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st result = static_cast(::sendto(socket->value, static_cast(buffer), static_cast(length), NativeSendFlags(flags), reinterpret_cast(&native), size)); } - return result < 0 ? Fail(NativeError()) : result; + return result < 0 ? Fail(SendError(socket->type)) : result; } std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::uint64_t length, int flags, void* address, std::uint32_t* addressLength) { @@ -699,6 +741,8 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u if ((flags & ~2) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if ((!buffer && length) || (address && !addressLength)) return Fail(14); + const GuestArena::HostWrite destination(length == 0 ? nullptr : buffer, static_cast(length)); + if (!destination.Open()) return Fail(14); sockaddr_storage native{}; socklen_t size = sizeof(native); auto result = ::recvfrom(socket->value, static_cast(buffer), static_cast(length), @@ -711,7 +755,10 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u if (socket->type == 1) *addressLength = 0; else UnnamedAddress(address, addressLength); return result; } - if (address) GuestAddress(native, address, addressLength); + if (address) { + if (native.ss_family == AF_INET || native.ss_family == AF_INET6) GuestAddress(native, address, addressLength); + else *addressLength = 0; + } return result; } struct GuestIovec { diff --git a/core/libs/prx/libkernel/System/src/Login.cpp b/core/libs/prx/libkernel/System/src/Login.cpp new file mode 100644 index 000000000..80f9eaabc --- /dev/null +++ b/core/libs/prx/libkernel/System/src/Login.cpp @@ -0,0 +1,53 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#else +#include +#include +#endif + +namespace { +int ReadLogin(char* output, int size) { +#ifdef _WIN32 + std::array name{}; + DWORD count = static_cast(name.size()); + if (!GetUserNameW(name.data(), &count)) + throw std::system_error(GetLastError(), std::system_category(), "getlogin_r: GetUserNameW"); + std::array utf8{}; + const int length = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, name.data(), + static_cast(count), utf8.data(), static_cast(utf8.size()), nullptr, nullptr); + if (length == 0) + throw std::system_error(GetLastError(), std::system_category(), "getlogin_r: UTF-8 conversion"); + if (length > size) return 34; + std::memcpy(output, utf8.data(), static_cast(length)); + return 0; +#else + std::array storage{}; + passwd entry{}; + passwd* account = nullptr; + const int error = ::getpwuid_r(::geteuid(), &entry, storage.data(), storage.size(), &account); + if (error != 0) + throw std::system_error(error, std::generic_category(), "getlogin_r: getpwuid_r"); + if (account == nullptr || account->pw_name == nullptr || account->pw_name[0] == '\0') + throw std::runtime_error("getlogin_r: host effective user has no account name"); + const std::size_t length = std::strlen(account->pw_name) + 1; + if (length > static_cast(size)) return 34; + std::memcpy(output, account->pw_name, length); + return 0; +#endif +} +} + +extern "C" int APS5_VABI getlogin_r_nid_postfix(char* output, int size) { + if (size <= 0) return 34; + if (output == nullptr) return 14; + const int saved = errno; + const int result = ReadLogin(output, size); + errno = saved; + return result; +} diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index f4d546837..11565f330 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -13,11 +13,13 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/HostEntropy.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" +#include #include #include #include @@ -29,7 +31,6 @@ #include #include #include -#include #include #include #include @@ -44,7 +45,11 @@ namespace { constexpr int sceInvalidArgument = static_cast(0x80020016u); constexpr int errnoNoChild = 10; +constexpr int errnoBadAddress = 14; constexpr int errnoInvalidArgument = 22; +constexpr int freebsdGroupsMax = 1023; +std::mutex supplementaryGroupsMutex; +std::vector supplementaryGroups{0}; constexpr unsigned freebsdWaitOptions = 0x8000003Fu; std::atomic gpoBits{0}; @@ -107,14 +112,22 @@ ProcessArguments& getProcessArguments() { return arguments; } -void validateSchedulingPolicy(int policy) { - if (policy < 1 || policy > 3) - throw std::invalid_argument("Unsupported guest scheduling policy"); +bool hasPriorityRange(int policy) { + return policy == 1 || policy == 3; } constexpr int guestFault = 14; constexpr int guestInvalid = 22; +constexpr int guestRlimitCpu = 0; +constexpr int guestRlimitFsize = 1; constexpr int guestRlimitData = 2; +constexpr int guestRlimitCore = 4; +constexpr int guestRlimitSbsize = 9; +constexpr int guestRlimitVmem = 10; +constexpr int guestRlimitNpts = 11; +constexpr int guestRlimitSwap = 12; +constexpr int guestRlimitKqueues = 13; +constexpr int guestRlimitUmtxp = 14; constexpr int guestRlimitCount = 15; constexpr std::int64_t guestRlimitInfinity = std::numeric_limits::max(); @@ -287,6 +300,10 @@ int APS5_VABI waitpid_nid_postfix(int pid, int* status, int options) { return -1; } +int APS5_VABI wait_nid_postfix(int* status) { + return waitpid_nid_postfix(-1, status, 0); +} + int APS5_VABI execvp_nid_postfix(const char* file, char* const* arguments) { (void)file; (void)arguments; @@ -342,11 +359,8 @@ void* APS5_VABI sceKernelGetProcParam(void) { int APS5_VABI sceKernelUuidCreate(std::uint32_t* uuid) { if (!uuid) return sceInvalidArgument; - static thread_local std::random_device device; - std::uniform_int_distribution distribution; std::array value; - for (auto& word : value) - word = distribution(device); + HostEntropy_nid_no_patch(value.data(), sizeof(value)); value[1] = (value[1] & 0x0fffffffu) | 0x40000000u; value[2] = (value[2] & 0xffffff3fu) | 0x80u; std::memcpy(uuid, value.data(), sizeof(value)); @@ -362,18 +376,28 @@ void APS5_VABI sync_nid_postfix(void) { } int APS5_VABI sched_get_priority_max_nid_postfix(int policy) { - validateSchedulingPolicy(policy); - return 256; + if (!hasPriorityRange(policy)) { + *__error_nid_postfix() = guestInvalid; + return -1; + } + return 767; } int APS5_VABI sched_get_priority_min_nid_postfix(int policy) { - validateSchedulingPolicy(policy); - return 767; + if (!hasPriorityRange(policy)) { + *__error_nid_postfix() = guestInvalid; + return -1; + } + return 256; } int APS5_VABI getrusage_nid_postfix(int who, GuestResourceUsage* usage) { if (usage == nullptr) throw std::invalid_argument("getrusage: usage is null"); + if (who == -1) { + *usage = {}; + return 0; + } if (who != 0 && who != 1) throw std::invalid_argument("getrusage: unsupported who"); #ifdef _WIN32 @@ -446,32 +470,47 @@ int APS5_VABI getrlimit_nid_postfix(int resource, GuestResourceLimit* limit) { *__error_nid_postfix() = guestFault; return -1; } - if (resource != guestRlimitData) - throw std::runtime_error("getrlimit: unsupported resource " + std::to_string(resource)); + switch (resource) { + case guestRlimitData: { #ifdef _WIN32 - const std::int64_t memory = hostMemoryLimit(); - limit->rlim_cur = memory; - limit->rlim_max = memory; + const std::int64_t memory = hostMemoryLimit(); + limit->rlim_cur = memory; + limit->rlim_max = memory; #else - rlimit native{}; - if (::getrlimit(RLIMIT_DATA, &native) != 0) - throw std::system_error(errno, std::generic_category(), "getrlimit: getrlimit failed"); - limit->rlim_cur = toGuestLimit(native.rlim_cur); - limit->rlim_max = toGuestLimit(native.rlim_max); + rlimit native{}; + if (::getrlimit(RLIMIT_DATA, &native) != 0) + throw std::system_error(errno, std::generic_category(), "getrlimit: getrlimit failed"); + limit->rlim_cur = toGuestLimit(native.rlim_cur); + limit->rlim_max = toGuestLimit(native.rlim_max); #endif - return 0; + return 0; + } + case guestRlimitCpu: + case guestRlimitFsize: + case guestRlimitCore: + case guestRlimitSbsize: + case guestRlimitVmem: + case guestRlimitNpts: + case guestRlimitSwap: + case guestRlimitKqueues: + case guestRlimitUmtxp: + limit->rlim_cur = guestRlimitInfinity; + limit->rlim_max = guestRlimitInfinity; + return 0; + default: + throw std::runtime_error("getrlimit: unsupported resource " + std::to_string(resource)); + } } int APS5_VABI sceKernelIsTrinityMode(void) { return 0; } -int APS5_VABI sceKernelGetOperationMode(int* mode, int* submode) { +void APS5_VABI sceKernelGetOperationMode(int* mode, int* submode) { if (!mode || !submode) throw std::invalid_argument("sceKernelGetOperationMode: null output"); - *mode = 0; + *mode = 2; *submode = 0; - return 0; } int APS5_VABI seteuid_nid_postfix(std::uint32_t euid) { @@ -486,4 +525,55 @@ int APS5_VABI setegid_nid_postfix(std::uint32_t egid) { return 0; } +int APS5_VABI setuid_nid_postfix(std::uint32_t uid) { + if (uid != 0) + throw std::runtime_error("setuid: unsupported user id " + std::to_string(uid)); + return 0; +} + +int APS5_VABI setreuid_nid_postfix(std::uint32_t ruid, std::uint32_t euid) { + constexpr std::uint32_t unchanged = 0xffffffffu; + if (ruid != 0 && ruid != unchanged) + throw std::runtime_error("setreuid: unsupported real user id " + std::to_string(ruid)); + if (euid != 0 && euid != unchanged) + throw std::runtime_error("setreuid: unsupported effective user id " + std::to_string(euid)); + return 0; +} + +int APS5_VABI setgroups_nid_postfix(int count, const std::uint32_t* groups) { + if (count < 0 || count > freebsdGroupsMax) { + *__error_nid_postfix() = errnoInvalidArgument; + return -1; + } + if (count > 0 && groups == nullptr) { + *__error_nid_postfix() = errnoBadAddress; + return -1; + } + if (count > 0 && groups[0] != 0) + throw std::runtime_error("setgroups: unsupported effective group id " + std::to_string(groups[0])); + std::lock_guard lock(supplementaryGroupsMutex); + if (count == 0) { + supplementaryGroups.resize(1); + return 0; + } + supplementaryGroups.assign(groups, groups + count); + return 0; +} + +int APS5_VABI getgroups_nid_postfix(int capacity, std::uint32_t* groups) { + std::lock_guard lock(supplementaryGroupsMutex); + const int count = static_cast(supplementaryGroups.size()); + if (capacity == 0) return count; + if (capacity < count) { + *__error_nid_postfix() = errnoInvalidArgument; + return -1; + } + if (groups == nullptr) { + *__error_nid_postfix() = errnoBadAddress; + return -1; + } + std::copy(supplementaryGroups.begin(), supplementaryGroups.end(), groups); + return count; +} + } diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index abbd8d0ea..c5c0f2220 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -16,6 +16,7 @@ #endif extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI getpid_nid_postfix(); struct SceKernelSystemSwVersion { std::uint64_t size; @@ -90,19 +91,61 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void *__error_nid_postfix() = 14; return -1; } - if (nameLength != 2 || name[0] != 6 || name[1] != 3) { - std::string mib; - for (std::uint32_t i = 0; i < nameLength; ++i) mib += (i == 0 ? "" : ".") + std::to_string(name[i]); - throw std::runtime_error(std::string(__func__) + ": unsupported MIB " + mib); + if (nameLength == 2 && name[0] == 6 && name[1] == 3) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + const std::int64_t processors = sysconf_nid_postfix(58); + if (processors <= 0) return -1; + const int value = static_cast(processors); + return CopyOut(&value, sizeof(value), oldValue, oldLength); } - if (newValue != nullptr) { - *__error_nid_postfix() = 1; - return -1; + if (nameLength == 2 && name[0] == 6 && name[1] == 5) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + const std::int64_t pages = sysconf_nid_postfix(121); + if (pages <= 0) return -1; + const std::uint64_t physmem = static_cast(pages) * PS5_PAGE_SIZE; + return CopyOut(&physmem, sizeof(physmem), oldValue, oldLength); } - const std::int64_t processors = sysconf_nid_postfix(58); - if (processors <= 0) return -1; - const int value = static_cast(processors); - return CopyOut(&value, sizeof(value), oldValue, oldLength); + if (nameLength >= 3 && name[0] == 1 && name[1] == 14 && name[2] == 12) { + if (nameLength != 4) { + *__error_nid_postfix() = 22; + return -1; + } + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + const int pid = name[3]; + if (pid != -1 && pid != getpid_nid_postfix()) { + *__error_nid_postfix() = 3; + return -1; + } + static constexpr char path[] = "/app0/eboot.bin"; + return CopyOut(path, sizeof(path), oldValue, oldLength); + } + if (nameLength == 2 && name[0] == 1 && name[1] == 37) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + if (oldLength == nullptr) return 0; + const std::size_t count = std::min(*oldLength, static_cast(256)); + if (oldValue != nullptr) { + static std::random_device device; + auto* bytes = static_cast(oldValue); + for (std::size_t i = 0; i < count; ++i) bytes[i] = static_cast(device()); + } + *oldLength = count; + return 0; + } + std::string mib; + for (std::uint32_t i = 0; i < nameLength; ++i) mib += (i == 0 ? "" : ".") + std::to_string(name[i]); + throw std::runtime_error(std::string(__func__) + ": unsupported MIB " + mib); } int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::size_t* oldLength, const void* newValue, std::size_t newLength) { @@ -110,6 +153,18 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si *__error_nid_postfix() = 14; return -1; } + if (std::strcmp(name, "machdep.cacheline_size") == 0) { + *__error_nid_postfix() = 2; + return -1; + } + if (std::strcmp(name, "kern.proc.pathname") == 0) { + *__error_nid_postfix() = 22; + return -1; + } + if (std::strcmp(name, "kern.arandom") == 0) { + static constexpr int mib[] = {1, 37}; + return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + } if (std::strcmp(name, "kern.rng_pseudo") == 0) { if (newValue != nullptr) { *__error_nid_postfix() = 1; @@ -121,9 +176,23 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si for (std::size_t i = 0; i < *oldLength; ++i) bytes[i] = static_cast(device()); return 0; } - if (std::strcmp(name, "hw.ncpu") != 0) throw std::runtime_error(std::string(__func__) + ": unsupported name " + name); - static constexpr int mib[] = {6, 3}; - return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + if (std::strcmp(name, "kern.sdk_version") == 0) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + static constexpr std::uint32_t sdkVersion = 0x01000000; + return CopyOut(&sdkVersion, sizeof(sdkVersion), oldValue, oldLength); + } + if (std::strcmp(name, "hw.ncpu") == 0) { + static constexpr int mib[] = {6, 3}; + return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + } + if (std::strcmp(name, "hw.physmem") == 0) { + static constexpr int mib[] = {6, 5}; + return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + } + throw std::runtime_error(std::string(__func__) + ": unsupported name " + name); } int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(SceKernelSystemSwVersion* version) { diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 0f86dbf4d..95c573feb 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -251,7 +251,7 @@ int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds) { return 0; } -static int SleepForRequest(const KernelTimespec* rqtp, KernelTimespec* rmtp) { +static int SleepForRequest(const KernelTimespec* rqtp) { constexpr int guestFault = 14; constexpr int guestInvalid = 22; if (rqtp == nullptr) return guestFault; @@ -260,20 +260,16 @@ static int SleepForRequest(const KernelTimespec* rqtp, KernelTimespec* rmtp) { CancellableSleep(static_cast(rqtp->tv_sec) * 1000000000ULL + static_cast(rqtp->tv_nsec)); } - if (rmtp != nullptr) { - rmtp->tv_sec = 0; - rmtp->tv_nsec = 0; - } return 0; } int APS5_VABI sceKernelNanosleep(const KernelTimespec* rqtp, KernelTimespec* rmtp) { - const int error = SleepForRequest(rqtp, rmtp); + const int error = SleepForRequest(rqtp); return error == 0 ? 0 : SceKernelError(error); } int APS5_VABI nanosleep_nid_postfix(const KernelTimespec* rqtp, KernelTimespec* rmtp) { - const int error = SleepForRequest(rqtp, rmtp); + const int error = SleepForRequest(rqtp); if (error == 0) return 0; *__error_nid_postfix() = error; return -1; @@ -564,12 +560,14 @@ static constexpr int SceKernelErrorEfault = static_cast(0x8002000e); int APS5_VABI sceKernelClockGetres(KernelClockid clock_id, KernelTimespec* tp) { if (tp == nullptr) return SceKernelErrorEfault; - return clock_getres_nid_postfix(static_cast(clock_id), tp); + if (clock_getres_nid_postfix(static_cast(clock_id), tp) != 0) return SceKernelError(*__error_nid_postfix()); + return 0; } int APS5_VABI sceKernelClockGettime(KernelClockid clock_id, KernelTimespec* tp) { if (tp == nullptr) return SceKernelErrorEfault; - return clock_gettime_nid_postfix(static_cast(clock_id), tp); + if (clock_gettime_nid_postfix(static_cast(clock_id), tp) != 0) return SceKernelError(*__error_nid_postfix()); + return 0; } int APS5_VABI sceKernelConvertLocaltimeToUtc(int64_t local_time, int64_t reserved, int64_t* utc_time, KernelTimesec* timezone, int32_t* dst_seconds) { @@ -580,7 +578,7 @@ int APS5_VABI sceKernelConvertLocaltimeToUtc(int64_t local_time, int64_t reserve return 0; } -int APS5_VABI sceKernelConvertUtcToLocaltime(int64_t utc_time, int64_t* local_time, KernelTimesec* st, uint64_t* dst_sec) { +int APS5_VABI sceKernelConvertUtcToLocaltime(int64_t utc_time, int64_t* local_time, KernelTimesec* st, int32_t* dst_sec) { if (local_time != nullptr) *local_time = utc_time; if (st != nullptr) *st = {utc_time, 0u, 0u}; if (dst_sec != nullptr) *dst_sec = 0; diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index 9fa34427b..eb776741a 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -10,6 +10,11 @@ int APS5_VABI sceCoredumpWriteUserData() { return 0; } +int APS5_VABI sceKernelGetCpuTemperature(int *out_celcius) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI __tls_get_addr_nid_postfix(void) { NotImplemented_nid_no_patch("vNe1w4diLCs"); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index f3554d51e..a50b7c0bb 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include #include #include @@ -17,6 +19,7 @@ #include "prx/libc/include/FilesystemError.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libc/include/CreationMask.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX @@ -43,6 +46,7 @@ constexpr int GuestEloop = 62; constexpr int GuestEnametoolong = 63; constexpr int GuestEnotempty = 66; constexpr int GuestEoverflow = 84; +constexpr int GuestAtFdcwd = -100; std::int64_t Fail(int error) { *__error_nid_postfix() = error; @@ -60,6 +64,50 @@ int GuestError(const std::error_code& error) { const int value = condition.value(); return value > 0 && value <= 34 ? value : GuestEio; } + +std::int64_t CopyLinkTarget(const std::string& target, char* buffer, std::size_t size) { + const auto count = std::min(size, target.size()); + if (count != 0 && buffer == nullptr) return Fail(GuestEfault); + if (count != 0) std::memcpy(buffer, target.data(), count); + return static_cast(count); +} + +#ifdef _WIN32 +struct SymbolicLinkReparseData { + ULONG tag; + USHORT dataLength; + USHORT reserved; + USHORT substituteNameOffset; + USHORT substituteNameLength; + USHORT printNameOffset; + USHORT printNameLength; + ULONG flags; +}; +constexpr ULONG SymbolicLinkRelative = 1; + +bool ReadRelativeSymbolicLink(const std::filesystem::path& native, std::string& target) { + const HANDLE handle = CreateFileW(native.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "readlink: CreateFileW"); + std::array data{}; + DWORD size = 0; + const BOOL read = DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, nullptr, 0, data.data(), static_cast(data.size()), &size, nullptr); + const DWORD code = GetLastError(); + CloseHandle(handle); + if (!read) throw std::system_error(static_cast(code), std::system_category(), "readlink: FSCTL_GET_REPARSE_POINT"); + SymbolicLinkReparseData header{}; + if (size < sizeof(header)) return false; + std::memcpy(&header, data.data(), sizeof(header)); + if (header.tag != IO_REPARSE_TAG_SYMLINK || (header.flags & SymbolicLinkRelative) == 0) return false; + const std::size_t begin = sizeof(header) + header.substituteNameOffset; + if (begin + header.substituteNameLength > size) throw std::runtime_error("readlink: malformed symbolic link reparse data"); + std::wstring name(header.substituteNameLength / sizeof(wchar_t), L'\0'); + std::memcpy(name.data(), data.data() + begin, name.size() * sizeof(wchar_t)); + target = std::filesystem::path(name).generic_string(); + return true; +} +#endif } extern "C" { @@ -92,8 +140,12 @@ std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std: #ifdef _WIN32 const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { - NotImplemented_nid_no_patch("readlink of a Windows symbolic link"); - return -1; + std::string target; + if (!ReadRelativeSymbolicLink(native, target)) { + NotImplemented_nid_no_patch("readlink of a Windows reparse point other than a relative symbolic link"); + return -1; + } + return CopyLinkTarget(target, buffer, size); } #endif std::error_code error; @@ -103,10 +155,7 @@ std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std: if (!std::filesystem::is_symlink(status)) return Fail(GuestEinval); const auto target = std::filesystem::read_symlink(native, error).generic_string(); if (error) return Fail(GuestError(error)); - const auto count = std::min(size, target.size()); - if (count != 0 && buffer == nullptr) return Fail(GuestEfault); - if (count != 0) std::memcpy(buffer, target.data(), count); - return static_cast(count); + return CopyLinkTarget(target, buffer, size); } int APS5_VABI link_nid_postfix(const char* existing, const char* created) { @@ -180,4 +229,29 @@ int APS5_VABI symlink_nid_postfix(const char* target, const char* created) { return 0; } +int APS5_VABI open_nid_postfix(const char* path, int flags, int mode); + +int APS5_VABI openat_nid_postfix(int fd, const char* path, int flags, ...) { + int mode = 0; + if (flags & SCE_KERNEL_O_CREAT) { +#ifdef _WIN32 + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, flags); + mode = __builtin_va_arg(arguments, int); + __builtin_sysv_va_end(arguments); +#else + std::va_list arguments; + va_start(arguments, flags); + mode = va_arg(arguments, int); + va_end(arguments); +#endif + } + if (path == nullptr) return static_cast(Fail(GuestEfault)); + if (*path == '\0') return static_cast(Fail(GuestEnoent)); + if (fd == GuestAtFdcwd || *path == '/') return open_nid_postfix(path, flags, mode); + const int result = OpenInDirectory_nid_no_patch(fd, path, flags, static_cast(mode)); + if (result < 0) return static_cast(Fail(result & 0xffff)); + return result; +} + } diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index 6cd531139..d7f755740 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -11,6 +11,7 @@ extern "C" { int APS5_VABI posix_memalign_nid_postfix(void**, std::size_t, std::size_t); +void* APS5_VABI reallocf_nid_postfix(void*, std::size_t); void* APS5_VABI _Znwm_nid_postfix(std::size_t); void* APS5_VABI _ZnwmRKSt9nothrow_t_nid_postfix(std::size_t, const void*) noexcept; void* APS5_VABI _ZnamRKSt9nothrow_t_nid_postfix(std::size_t, const void*) noexcept; @@ -148,6 +149,30 @@ int main(int argc, char** argv) { pointer = static_cast(ApplicationHeapReallocate_nid_no_patch(nullptr, 32)); require(pointer != nullptr); ApplicationHeapFree_nid_no_patch(pointer); + pointer = static_cast(reallocf_nid_postfix(nullptr, 24)); + require(pointer != nullptr && ApplicationHeapUsableSize_nid_no_patch(pointer) == 24); + std::memset(pointer, 0x3c, 24); + pointer = static_cast(reallocf_nid_postfix(pointer, 200)); + require(pointer != nullptr && ApplicationHeapUsableSize_nid_no_patch(pointer) == 200); + for (unsigned i = 0; i < 24; ++i) require(pointer[i] == 0x3c); + pointer = static_cast(reallocf_nid_postfix(pointer, 9)); + require(pointer != nullptr && ApplicationHeapUsableSize_nid_no_patch(pointer) == 9); + for (unsigned i = 0; i < 9; ++i) require(pointer[i] == 0x3c); + *__error_nid_postfix() = 0; + require(reallocf_nid_postfix(pointer, 0) == nullptr && *__error_nid_postfix() == 0); + reject([pointer] { ApplicationHeapUsableSize_nid_no_patch(pointer); }); + pointer = static_cast(reallocf_nid_postfix(nullptr, 40)); + require(pointer != nullptr); + require(reallocf_nid_postfix(pointer, SIZE_MAX - 4096) == nullptr && *__error_nid_postfix() == 12); + reject([pointer] { ApplicationHeapUsableSize_nid_no_patch(pointer); }); + *__error_nid_postfix() = 0; + require(reallocf_nid_postfix(nullptr, SIZE_MAX) == nullptr && *__error_nid_postfix() == 12); + void* empty = ApplicationHeapAllocate_nid_no_patch(0); + require(empty != nullptr); + ApplicationHeapFree_nid_no_patch(empty); + empty = ApplicationHeapReallocate_nid_no_patch(nullptr, 0); + require(empty != nullptr); + ApplicationHeapFree_nid_no_patch(empty); for (std::size_t alignment : {16, 64, 4096}) { auto* aligned = ApplicationHeapAlign_nid_no_patch(alignment, 37); require(reinterpret_cast(aligned) % alignment == 0); @@ -211,7 +236,14 @@ int main(int argc, char** argv) { require(frees == 6); _ZdlPvmSt11align_val_t_nid_postfix(nullptr, 37, 64); require(frees == 6); + require(reallocf_nid_postfix(storage.data(), 80) == storage.data() && lastSize == 80 && frees == 6); + require(reallocf_nid_postfix(storage.data(), 16) == storage.data() && lastSize == 16 && frees == 6); + require(reallocf_nid_postfix(storage.data(), 0) == nullptr && lastSize == 16 && frees == 7); fail = true; + *__error_nid_postfix() = 0; + require(reallocf_nid_postfix(storage.data(), 48) == nullptr && lastSize == 48 && frees == 8 && *__error_nid_postfix() == 12); + reject([] { ApplicationHeapReallocate_nid_no_patch(storage.data(), 48); }); + require(frees == 8); require(_ZnwmRKSt9nothrow_t_nid_postfix(8, nullptr) == nullptr); require(_ZnamRKSt9nothrow_t_nid_postfix(8, nullptr) == nullptr); reject([] { _Znwm_nid_postfix(8); }); diff --git a/core/libs/tests/ExceptionRuntime.cpp b/core/libs/tests/ExceptionRuntime.cpp index c55a2cb15..a2dd29806 100644 --- a/core/libs/tests/ExceptionRuntime.cpp +++ b/core/libs/tests/ExceptionRuntime.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include #include @@ -160,6 +162,27 @@ void CheckTerminate(void (*function)()) { assert(waitpid(child, &status, 0) == child); assert(WIFEXITED(status) && WEXITSTATUS(status) == 61); } +void CheckDefaultTerminateMessage() { + const std::string message = std::string(3000, 'w') + " end of message"; + int channel[2]; + assert(pipe(channel) == 0); + pid_t child = fork(); + assert(child >= 0); + if (child == 0) { + close(channel[0]); + dup2(channel[1], 2); + throw std::runtime_error(message); + } + close(channel[1]); + std::string output; + char buffer[4096]; + for (ssize_t count; (count = read(channel[0], buffer, sizeof(buffer))) > 0;) output.append(buffer, static_cast(count)); + close(channel[0]); + int status; + assert(waitpid(child, &status, 0) == child); + assert(WIFSIGNALED(status) && WTERMSIG(status) == SIGABRT); + assert(output.find(" what(): " + message + "\n") != std::string::npos); +} struct ForcedGuard { ~ForcedGuard() { ++destroyed; } }; [[gnu::noinline]] void ForceUnwind() { ForcedGuard guard; @@ -231,6 +254,7 @@ int main() { CheckThreads(); CheckTerminate(UncaughtThrow); CheckTerminate(NoexceptThrow); + CheckDefaultTerminateMessage(); CheckForcedUnwind(); int frames = 0; auto backtrace = _Unwind_Backtrace_nid_postfix([](_Unwind_Context* context, void* argument) { diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index aa4b41128..c06b0a06a 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -19,6 +19,8 @@ int APS5_VABI fseek_nid_postfix(FileStream* stream, std::int64_t offset, int ori std::int64_t APS5_VABI ftell_nid_postfix(FileStream* stream); int APS5_VABI fputs_nid_postfix(const char* str, FileStream* stream); int APS5_VABI fflush_nid_postfix(FileStream* stream); +int APS5_VABI remove_nid_postfix(const char* path); +int APS5_VABI rename_nid_postfix(const char* from, const char* to); } static void Require(bool condition) { @@ -91,5 +93,26 @@ int main(int argc, char** argv) { Require(fclose_nid_postfix(stream) == 0); Require(std::filesystem::file_size(hostName) == 0); std::filesystem::remove(hostName); + const std::string heldName = std::string(argv[1]) + ".held"; + const std::string movedName = std::string(argv[1]) + ".moved"; + stream = fopen_nid_postfix(heldName.c_str(), "w+b"); + Require(stream != nullptr && fwrite_nid_postfix(payload, 1, sizeof(payload), stream) == sizeof(payload)); + Require(fflush_nid_postfix(stream) == 0); + Require(rename_nid_postfix(heldName.c_str(), movedName.c_str()) == 0 && !std::filesystem::exists(heldName)); + Require(remove_nid_postfix(movedName.c_str()) == 0 && !std::filesystem::exists(movedName)); + Require(fseek_nid_postfix(stream, 0, SEEK_SET) == 0); + std::array heldBuffer{}; + Require(fread_nid_postfix(heldBuffer.data(), 1, heldBuffer.size(), stream) == heldBuffer.size()); + Require(std::strcmp(heldBuffer.data(), payload) == 0 && fclose_nid_postfix(stream) == 0); + stream = fopen_nid_postfix(heldName.c_str(), "ab"); + Require(stream != nullptr && fputs_nid_postfix("ab", stream) >= 0 && fclose_nid_postfix(stream) == 0); + stream = fopen_nid_postfix(heldName.c_str(), "ab"); + Require(stream != nullptr && fputs_nid_postfix("cd", stream) >= 0 && fclose_nid_postfix(stream) == 0); + errno = 0; + Require(fopen_nid_postfix(heldName.c_str(), "wbx") == nullptr && errno == EEXIST); + stream = fopen_nid_postfix(heldName.c_str(), "rb"); + std::array appended{}; + Require(stream != nullptr && fread_nid_postfix(appended.data(), 1, 4, stream) == 4 && std::strcmp(appended.data(), "abcd") == 0); + Require(fclose_nid_postfix(stream) == 0 && remove_nid_postfix(heldName.c_str()) == 0); std::cout << "PASS: stream objects, file operations, EOF and error handling\n"; } diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index 0b83a58c1..b841dece1 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -1,5 +1,9 @@ #include "SceTypes.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/GuestHeap.hpp" #include +#include +#include #include #include #include @@ -15,6 +19,8 @@ int APS5_VABI vsnprintf_nid_postfix(char*, size_t, const char*, VaList*); int APS5_VABI vsnprintf_s_nid_postfix(char*, size_t, const char*, VaList*); int APS5_VABI vsscanf_s_nid_postfix(const char*, const char*, VaList*); int APS5_VABI vprintf_nid_postfix(const char*, VaList*); +int APS5_VABI asprintf_nid_postfix(char**, const char*, ...); +void APS5_VABI free_nid_postfix(void*); } static void Require(bool condition) { @@ -208,6 +214,20 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { Require(sscanf_nid_postfix("", "%*d", &scanned) == EOF); Require(sscanf_nid_postfix(" ", "%*d%d", &scanned) == EOF); Require(sscanf_nid_postfix("", "%d", &scanned) == EOF); +#else + char16_t wide[4] = {u'!', u'!', u'!', u'!'}; + const auto rejects = [&](const char* format) { + try { sscanf_nid_postfix("ab cd", format, wide); } catch (const std::runtime_error&) { return wide[0] == u'!'; } + return false; + }; + Require(rejects("%ls") && rejects("%*s %3lc") && rejects("%l[a-z]") && rejects("%S") && rejects("%2$C") && rejects("%'ls")); + const auto secureRejects = [&](const char* format) { + try { ScanListS("ab cd", format, wide, 4u); } catch (const std::runtime_error&) { return wide[0] == u'!'; } + return false; + }; + Require(secureRejects("%ls") && secureRejects("%lc")); + char narrow[8] = {}; + Require(sscanf_nid_postfix("ab%", "%7[^%ls]", narrow) == 1 && std::strcmp(narrow, "ab") == 0); #endif long long count = -1; int smallCount = -1; @@ -228,4 +248,17 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { std::puts("Formatting checks passed: 10000 iterations"); } -int main() { RunChecks(); return CheckWidePrecision() ? 0 : 1; } +static void* APS5_VABI Allocate(std::size_t bytes) { return GuestHeap::GuestHeapAllocate_nid_postfix(bytes); } +static void APS5_VABI Release(void* pointer) { GuestHeap::GuestHeapFree_nid_postfix(pointer); } +static void* APS5_VABI Unused() { std::abort(); } + +static void CheckAllocatingFormat() { + const std::array api{reinterpret_cast(&Allocate), reinterpret_cast(&Release), reinterpret_cast(&Unused), + reinterpret_cast(&Unused), reinterpret_cast(&Unused), reinterpret_cast(&Unused), reinterpret_cast(&Unused)}; + ApplicationHeapRegister_nid_no_patch(api.data()); + char* allocated = nullptr; + Require(asprintf_nid_postfix(&allocated, "%ls:%S:%lc:%d", u"\u00e9", u"ab", 0x20ac, 7) == 11 && std::strcmp(allocated, "\xc3\xa9:ab:\xe2\x82\xac:7") == 0); + free_nid_postfix(allocated); +} + +int main() { RunChecks(); CheckAllocatingFormat(); return CheckWidePrecision() ? 0 : 1; } diff --git a/core/libs/tests/GuestAio.cpp b/core/libs/tests/GuestAio.cpp index e1a51c74e..96d803063 100644 --- a/core/libs/tests/GuestAio.cpp +++ b/core/libs/tests/GuestAio.cpp @@ -15,9 +15,15 @@ #include #include #include +#ifdef _WIN32 +#include +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); +#endif extern "C" { int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode); int APS5_VABI sceKernelClose(int d); +std::int64_t APS5_VABI sceKernelPread(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI sceKernelPwrite(int, const void*, std::size_t, std::int64_t); std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence); int APS5_VABI sceKernelAioDeleteRequest(std::int32_t id, std::int32_t* ret); int APS5_VABI sceKernelAioInitializeImpl(void* param, std::int32_t size); @@ -42,7 +48,50 @@ static void Require(bool value, int line) { } } #define Check(value) Require((value), __LINE__) -int main() { +#ifdef _WIN32 +static int g_parameterReports = 0; +static void CountParameterReport(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) { + ++g_parameterReports; +} +static void CheckBadPositionedDescriptors() { + const auto file = std::filesystem::path("anyps5-aio-bad-descriptor-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + { std::ofstream stream(file, std::ios::binary); stream << "data"; Check(static_cast(stream)); } + const int closed = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDWR, 0); + Check(closed >= 0 && sceKernelClose(closed) == 0); + const auto previous = _set_thread_local_invalid_parameter_handler(CountParameterReport); + for (int descriptor : {-1, closed}) { + for (std::size_t count : {std::size_t{0}, std::size_t{1}}) { + char buffer = 'x'; + Check(sceKernelPread(descriptor, &buffer, count, 0) == SCE_KERNEL_ERROR_EBADF); + Check(sceKernelPwrite(descriptor, &buffer, count, 0) == SCE_KERNEL_ERROR_EBADF); + for (bool write : {false, true}) { + KernelAioResult result{-1, 0}; + KernelAioRwRequest request{0, count, &buffer, &result, descriptor}; + std::int32_t id = 0; + Check((write ? sceKernelAioSubmitWriteCommands(&request, 1, 0, &id) + : sceKernelAioSubmitReadCommands(&request, 1, 0, &id)) == 0); + Check(result.state == 4 && result.return_value == SCE_KERNEL_ERROR_EBADF); + std::int32_t removed = -1; + Check(sceKernelAioDeleteRequest(id, &removed) == 0); + } + Check(buffer == 'x'); + Check(g_parameterReports == 0); + } + } + Check(::_get_osfhandle(closed) == -1); + Check(g_parameterReports == 1); + Check(_set_thread_local_invalid_parameter_handler(previous) == CountParameterReport); + Check(std::filesystem::remove(file)); +} +#endif +int main(int argc, char** argv) { +#ifdef _WIN32 + if (argc > 1 && std::strcmp(argv[1], "bad-descriptors") == 0) { + CheckBadPositionedDescriptors(); + return 0; + } +#endif const auto root = std::filesystem::path("anyps5-aio-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Check(std::filesystem::create_directory(root)); diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index c35ca5e78..b03e0a14f 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -131,10 +131,10 @@ void RunDecode(std::uint32_t context, std::uint32_t instance, const std::uint8_t Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0 && sceAjmBatchWait(context, id, 0, &error) == 0); } -void TestOpus(std::uint32_t context) { +void TestOpus(std::uint32_t context, std::uint32_t mappingFamily = 0) { std::uint32_t instance = 0; Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); - const std::uint32_t parameters[3] = {2, 48000, 0}; + const std::uint32_t parameters[3] = {2, 48000, mappingFamily}; std::vector batch(4096); AjmBatchInfo info{}; std::int64_t initResult[2] = {-1, -1}; @@ -171,6 +171,80 @@ void TestOpus(std::uint32_t context) { Require(sceAjmInstanceDestroy(context, instance) == 0); } +void TestOpusSurround(std::uint32_t context, std::uint32_t channels, std::size_t activeStream) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); + const std::uint32_t parameters[3] = {channels, 48000, 1}; + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t initResult[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, parameters, sizeof(parameters), initResult) == 0); + std::uint32_t id = 0; + AjmBatchError error{}; + Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0 && sceAjmBatchWait(context, id, 0, &error) == 0); + Require(initResult[0] == 0); + const std::size_t streamCount = channels == 3 || channels == 4 ? 2 : channels == 5 ? 3 : channels == 8 ? 5 : 4; + const std::size_t coupledCount = channels == 3 ? 1 : channels >= 7 ? 3 : 2; + std::size_t offset = 0; + for (std::size_t packetIndex = 0; packetIndex < 3; ++packetIndex) { + const std::size_t bytes = OPUS_STEREO[offset] | (std::size_t{OPUS_STEREO[offset + 1]} << 8u); + Require((OPUS_STEREO[offset + 2] & 3u) == 0 && bytes < 252); + std::vector packet{0, 0}; + for (std::size_t stream = 0; stream < streamCount; ++stream) { + if (stream == activeStream) { + packet.push_back(OPUS_STEREO[offset + 2]); + if (stream + 1 < streamCount) packet.push_back(static_cast(bytes - 1)); + packet.insert(packet.end(), OPUS_STEREO + offset + 3, OPUS_STEREO + offset + 2 + bytes); + } else { + packet.push_back(stream < coupledCount ? 0xfc : 0xf8); + if (stream + 1 < streamCount) packet.push_back(2); + packet.push_back(0xff); + packet.push_back(0xfe); + } + } + const std::size_t payload = packet.size() - 2; + packet[0] = static_cast(payload); + packet[1] = static_cast(payload >> 8u); + std::vector pcm(512 * channels, -12345); + DecodeSideband first{}; + RunDecode(context, instance, packet.data(), packet.size(), pcm.data(), pcm.size() * sizeof(std::int16_t), first); + Require(first.result == 0 && static_cast(first.inputConsumed) == packet.size() && first.outputWritten == static_cast(pcm.size() * sizeof(std::int16_t))); + std::vector rest(448 * channels, -12345); + DecodeSideband remaining{}; + RunDecode(context, instance, packet.data() + packet.size(), 0, rest.data(), rest.size() * sizeof(std::int16_t), remaining); + Require(remaining.result == 0 && remaining.inputConsumed == 0 && remaining.outputWritten == static_cast(rest.size() * sizeof(std::int16_t))); + Require(remaining.totalDecodedSamples == (packetIndex + 1) * 960); + pcm.insert(pcm.end(), rest.begin(), rest.end()); + for (std::size_t channel = 0; channel < channels; ++channel) { + int peak = 0; + for (std::size_t sample = channel; sample < pcm.size(); sample += channels) peak = std::max(peak, std::abs(static_cast(pcm[sample]))); + const std::size_t activeChannel = activeStream == 0 ? 0 : channels == 4 ? 2 : channels == 5 ? 3 : channels == 6 ? 4 : channels == 7 ? activeStream == 1 ? 5 : 2 : activeStream == 1 ? 6 : 4; + const std::size_t secondChannel = channels == 7 && activeStream == 2 ? 4 : activeChannel + 1; + const bool active = channel == activeChannel || channel == secondChannel; + Require(active ? packetIndex == 0 || peak > 900 : peak <= 2); + } + offset += 2 + bytes; + } + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +void TestOpusFamilyZeroChannels(std::uint32_t context) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); + const std::uint32_t parameters[3] = {4, 48000, 0}; + std::vector batch(4096); + AjmBatchInfo info{}; + std::int32_t result[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, parameters, sizeof(parameters), result) == 0); + std::uint32_t id = 0; + AjmBatchError error{}; + Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0 && sceAjmBatchWait(context, id, 0, &error) == 0); + Require(result[0] == 4 && result[1] == 0); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + void TestOpusInputBeyondStatedData(std::uint32_t context, bool multipleFrames) { #ifdef _WIN32 SYSTEM_INFO system{}; @@ -1170,6 +1244,18 @@ int main() { TestMp3ParseOfl(); TestMp3(context); TestOpus(context); + TestOpus(context, 1); + TestOpusFamilyZeroChannels(context); + TestOpusSurround(context, 3, 0); + for (std::size_t stream = 0; stream < 2; ++stream) { + TestOpusSurround(context, 4, stream); + TestOpusSurround(context, 5, stream); + TestOpusSurround(context, 6, stream); + } + for (std::size_t stream = 0; stream < 3; ++stream) { + TestOpusSurround(context, 7, stream); + TestOpusSurround(context, 8, stream); + } TestOpusInputBeyondStatedData(context, false); TestOpusInputBeyondStatedData(context, true); TestDecodeSingle(context); diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index d2a646ac1..37bc4c9df 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -9,12 +9,16 @@ #include #include #include +#include #include #include #include #include #include #include +#ifdef _WIN32 +#include +#endif extern "C" { int APS5_VABI sceAmprCommandBufferConstructor(Apr::CommandBufferObject*); @@ -108,6 +112,8 @@ int APS5_VABI sceKernelMunmap(void*, std::size_t); int APS5_VABI sceAmprAmmCommandBufferMapAsPrt(Apr::CommandBufferObject*, std::uint64_t, std::uint64_t); int APS5_VABI sceAmprAmmCommandBufferAllocatePaForPrt(Apr::CommandBufferObject*, std::uint64_t, std::uint64_t, std::int32_t, std::int32_t); int APS5_VABI sceAmprAmmCommandBufferRemapIntoPrt(Apr::CommandBufferObject*, std::uint64_t, std::uint64_t, std::uint64_t, std::int32_t, std::uint32_t); +int APS5_VABI sceKernelWriteModifyMtypeProtectCommand(void*, std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t*); +int APS5_VABI sceKernelWriteRemapIntoPrtCommand(std::uint32_t*, std::uint64_t, std::uint64_t, std::uint64_t, int, int, std::uint64_t*); int APS5_VABI sceAmprAmmCommandBufferUnmapToPrt(Apr::CommandBufferObject*, std::uint64_t, std::uint64_t); std::int64_t APS5_VABI sceAmprAmmMeasureAmmCommandSizeMapAsPrt(std::uint64_t, std::uint64_t); std::int64_t APS5_VABI sceAmprAmmMeasureAmmCommandSizeAllocatePaForPrt(std::uint64_t, std::uint64_t, std::int32_t, std::int32_t); @@ -669,6 +675,31 @@ void TestMalformedCommandSizes() { Require(rejected); } +void TestResolveSymbolicLink() { + const std::filesystem::path directory = "ampr_resolve_links"; + std::filesystem::remove_all(directory); + Require(std::filesystem::create_directory(directory)); + { std::ofstream file(directory / "target.bin", std::ios::binary); file << "linked data"; } + const auto link = directory / "link.bin"; +#ifdef _WIN32 + if (CreateSymbolicLinkW(link.c_str(), L"target.bin", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) == 0) { + Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD); + std::fprintf(stderr, "APR symbolic link check skipped: creating symbolic links needs Developer Mode or administrator rights\n"); + std::filesystem::remove_all(directory); + return; + } +#else + std::filesystem::create_symlink("target.bin", link); +#endif + const std::string linkPath = link.generic_string(); + const char* path = linkPath.c_str(); + std::uint32_t id = 0; + std::uint64_t size = 0; + std::uint32_t failed = 0; + Require(sceKernelAprResolveFilepathsToIdsAndFileSizes(&path, 1, &id, &size, &failed) == 0 && size == 11); + std::filesystem::remove_all(directory); +} + void TestGatherScatter() { const char* path = "ampr_gather_scatter.bin"; { @@ -1122,6 +1153,87 @@ void TestAmmPrt() { Require(sceAmprAmmMeasureAmmCommandSizeAllocatePaForPrt(prt + 8, page, 0, cpuReadWrite) == ammRejected); } +void TestKernelWriteCommands() { + std::uint64_t start = 0; + std::uint64_t end = 0; + std::uint64_t multimapStart = 0; + std::uint64_t multimapEnd = 0; + Require(sceAmprAmmGetVirtualAddressRanges(&start, &end, &multimapStart, &multimapEnd) == 0); + std::int64_t pool = -1; + Require(sceAmprAmmGiveDirectMemory(0, static_cast(sceKernelGetDirectMemorySize()), 4 * page, page, 1, &pool) == 0); + + const std::uint64_t target = start + 0x400000; + const std::uint64_t source = start + 0x440000; + const std::uint64_t prt = start + 0x480000; + + Recorder setup; + Require(sceAmprAmmCommandBufferMap(&setup.buffer, target, 2 * page, 0, cpuReadWrite) == 0); + Require(sceAmprAmmCommandBufferMap(&setup.buffer, source, page, 0, cpuReadWrite) == 0); + Require(sceAmprAmmCommandBufferMapAsPrt(&setup.buffer, prt, 2 * page) == 0); + SubmitAmm(setup); + At(source) = 0x9C; + + std::uint64_t measured = 0; + Require(sceKernelWriteModifyMtypeProtectCommand(nullptr, target, 2 * page, 5, 0x13, 0x10, &measured) == 0); + Require(measured == sizeof(Apr::AmmProtectCommand)); + std::array written{}; + std::uint64_t reported = 0; + Require(sceKernelWriteModifyMtypeProtectCommand(written.data(), target, 2 * page, 5, 0x13, 0x10, &reported) == 0); + Require(reported == sizeof(Apr::AmmProtectCommand)); + Apr::CommandHeader header; + std::memcpy(&header, written.data(), sizeof(header)); + Require(header.opcode == Apr::Opcode::AmmModifyMtypeProtect && header.bytes == sizeof(Apr::AmmProtectCommand)); + Recorder sibling; + Require(sceAmprAmmCommandBufferModifyMtypeProtect(&sibling.buffer, target, 2 * page, 5, 0x13, 0x10) == 0); + Require(std::memcmp(written.data(), sibling.memory.data(), sizeof(Apr::AmmProtectCommand)) == 0); + Require(sceKernelWriteModifyMtypeProtectCommand(written.data(), target, 2 * page, 5, 0x13, 0x10, nullptr) == 0); + std::uint32_t id = 0; + Require(sceAmprAmmSubmitCommandBuffer3(written.data(), static_cast(reported), 0, &id) == 0); + Require(sceAmprAmmWaitCommandBufferCompletion(id) == 0); + Require(Protection(target) == 0x13 && Protection(target + page) == 0x13); + At(target) = 0x9D; + Require(At(target) == 0x9D); + + std::uint64_t prtMeasured = 0; + Require(sceKernelWriteRemapIntoPrtCommand(nullptr, prt + page, source, page, cpuReadWrite, 0, &prtMeasured) == 0); + Require(prtMeasured == sizeof(Apr::AmmRemapCommand)); + std::array prtWritten{}; + Require(sceKernelWriteRemapIntoPrtCommand(reinterpret_cast(prtWritten.data()), prt + page, source, page, cpuReadWrite, 0, &prtMeasured) == 0); + Apr::AmmRemapCommand remap; + std::memcpy(&remap, prtWritten.data(), sizeof(remap)); + Require(remap.header.opcode == Apr::Opcode::AmmRemapIntoPrt && remap.header.bytes == sizeof(remap)); + Require(remap.address == prt + page && remap.source == source && remap.size == page && remap.protection == cpuReadWrite && remap.reserved == 1011); + Require(sceKernelWriteRemapIntoPrtCommand(reinterpret_cast(prtWritten.data()), prt + page, source, page, cpuReadWrite, 42, &prtMeasured) == 0); + std::memcpy(&remap, prtWritten.data(), sizeof(remap)); + Require(remap.reserved == 42); + Recorder prtSibling; + Require(sceAmprAmmCommandBufferRemapIntoPrt(&prtSibling.buffer, prt + page, source, page, cpuReadWrite, 0) == 0); + std::array prtSubmit{}; + Require(sceKernelWriteRemapIntoPrtCommand(reinterpret_cast(prtSubmit.data()), prt + page, source, page, cpuReadWrite, 0, nullptr) == 0); + Require(std::memcmp(prtSubmit.data(), prtSibling.memory.data(), sizeof(Apr::AmmRemapCommand)) == 0); + Require(sceAmprAmmSubmitCommandBuffer3(prtSubmit.data(), static_cast(sizeof(Apr::AmmRemapCommand)), 0, &id) == 0); + Require(sceAmprAmmWaitCommandBufferCompletion(id) == 0); + Require(At(prt + page) == 0x9C); + + std::array untouched{}; + untouched.fill(0xCC); + std::uint64_t sentinel = 0xDDDDDDDDDDDDDDDDULL; + Require(sceKernelWriteModifyMtypeProtectCommand(untouched.data(), target + 8, page, 0, 0x03, 0x03, &sentinel) == invalidArgument); + Require(sceKernelWriteModifyMtypeProtectCommand(untouched.data(), target, 0, 0, 0x03, 0x03, &sentinel) == invalidArgument); + Require(sceKernelWriteModifyMtypeProtectCommand(untouched.data(), 0, page, 0, 0x03, 0x03, &sentinel) == invalidArgument); + Require(sceKernelWriteModifyMtypeProtectCommand(untouched.data(), target, page, 0, 0x04, 0x03, &sentinel) == invalidArgument); + Require(sceKernelWriteModifyMtypeProtectCommand(untouched.data(), target, page, 0, 0x03, 0x04, &sentinel) == invalidArgument); + Require(sceKernelWriteModifyMtypeProtectCommand(untouched.data(), ~std::uint64_t(0), page, 0, 0x03, 0x03, &sentinel) == invalidArgument); + Require(sceKernelWriteModifyMtypeProtectCommand(untouched.data(), target, page, 0, 0x100000000ULL, 0x03, &sentinel) == invalidArgument); + Require(sceKernelWriteRemapIntoPrtCommand(reinterpret_cast(untouched.data()), prt, source + 8, page, cpuReadWrite, 0, &sentinel) == invalidArgument); + Require(sceKernelWriteRemapIntoPrtCommand(reinterpret_cast(untouched.data()), prt, source, 0, cpuReadWrite, 0, &sentinel) == invalidArgument); + Require(sceKernelWriteRemapIntoPrtCommand(reinterpret_cast(untouched.data()), prt, source, page, 0x04, 0, &sentinel) == invalidArgument); + Require(sentinel == 0xDDDDDDDDDDDDDDDDULL); + for (const auto byte : untouched) Require(byte == 0xCC); + Require(sceKernelWriteModifyMtypeProtectCommand(nullptr, target, page, 0, 0x03, 0x03, nullptr) == 0); + Require(sceKernelWriteRemapIntoPrtCommand(nullptr, prt, source, page, cpuReadWrite, 0, nullptr) == 0); +} + } int main() { @@ -1147,10 +1259,12 @@ int main() { TestZeroFilledBuffer(); TestResolveFailures(); TestMalformedCommandSizes(); + TestResolveSymbolicLink(); TestGatherScatter(); TestAmmRangesAboveHintedMaps(); TestAmm(); TestAmmRemapAndProtect(); TestAmmPrt(); + TestKernelWriteCommands(); return 0; } diff --git a/core/libs/tests/GuestAppInfo.cpp b/core/libs/tests/GuestAppInfo.cpp index e39662613..e65d891a0 100644 --- a/core/libs/tests/GuestAppInfo.cpp +++ b/core/libs/tests/GuestAppInfo.cpp @@ -4,15 +4,18 @@ #include #include #include +#include #include #include extern "C" { int APS5_VABI sceKernelGetAppInfo(int pid, void* info); int APS5_VABI getpid_nid_postfix(void); +int APS5_VABI sceKernelUuidCreate(std::uint32_t* uuid); } static constexpr int SCE_KERNEL_ERROR_EPERM = static_cast(0x80020001); +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); static void Require(bool value) { if (!value) std::abort(); } @@ -35,8 +38,11 @@ int main(int argc, char** argv) { if (argc == 3 && std::string_view(argv[1]) == "--without-param-json") { std::filesystem::current_path(argv[2]); AppInfo info{}; - sceKernelGetAppInfo(getpid_nid_postfix(), &info); - return 0; + try { sceKernelGetAppInfo(getpid_nid_postfix(), &info); } + catch (const std::runtime_error& error) { + return std::string_view(error.what()) == "param.json not found" ? 0 : 1; + } + return 1; } const auto previous = std::filesystem::current_path(); const auto root = EnterTemporaryRoot("guest_app_info_"); @@ -63,6 +69,13 @@ int main(int argc, char** argv) { #else const std::string command = arguments; #endif - Require(std::system(command.c_str()) != 0); + Require(std::system(command.c_str()) == 0); std::filesystem::remove_all(empty); + + Require(sceKernelUuidCreate(nullptr) == SCE_KERNEL_ERROR_EINVAL); + std::uint32_t first[4]{}; + std::uint32_t second[4]{}; + Require(sceKernelUuidCreate(first) == 0 && sceKernelUuidCreate(second) == 0); + for (const std::uint32_t* uuid : {first, second}) Require((uuid[1] >> 28) == 4u && (uuid[2] & 0xc0u) == 0x80u); + Require(std::memcmp(first, second, sizeof(first)) != 0); } diff --git a/core/libs/tests/GuestArrayCallbacks.cpp b/core/libs/tests/GuestArrayCallbacks.cpp new file mode 100644 index 000000000..92bfdfdef --- /dev/null +++ b/core/libs/tests/GuestArrayCallbacks.cpp @@ -0,0 +1,201 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +using ElementCallback = void (APS5_VABI *)(void*); +using CopyCallback = void (APS5_VABI *)(void*, void*); +using AllocateCallback = void* (APS5_VABI *)(std::size_t); +using FreeCallback = void (APS5_VABI *)(void*); +using SizedFreeCallback = void (APS5_VABI *)(void*, std::size_t); + +extern "C" { +void APS5_VABI __cxa_vec_ctor_nid_postfix(void*, std::size_t, std::size_t, ElementCallback, ElementCallback); +void APS5_VABI __cxa_vec_cctor_nid_postfix(void*, void*, std::size_t, std::size_t, CopyCallback, ElementCallback); +void APS5_VABI __cxa_vec_dtor_nid_postfix(void*, std::size_t, std::size_t, ElementCallback); +void APS5_VABI __cxa_vec_cleanup_nid_postfix(void*, std::size_t, std::size_t, ElementCallback); +void* APS5_VABI __cxa_vec_new_nid_postfix(std::size_t, std::size_t, std::size_t, ElementCallback, ElementCallback); +void* APS5_VABI __cxa_vec_new2_nid_postfix(std::size_t, std::size_t, std::size_t, ElementCallback, ElementCallback, AllocateCallback, FreeCallback); +void* APS5_VABI __cxa_vec_new3_nid_postfix(std::size_t, std::size_t, std::size_t, ElementCallback, ElementCallback, AllocateCallback, SizedFreeCallback); +void APS5_VABI __cxa_vec_delete_nid_postfix(void*, std::size_t, std::size_t, ElementCallback); +void APS5_VABI __cxa_vec_delete2_nid_postfix(void*, std::size_t, std::size_t, ElementCallback, FreeCallback); +void APS5_VABI __cxa_vec_delete3_nid_postfix(void*, std::size_t, std::size_t, ElementCallback, SizedFreeCallback); +} + +namespace { + +constexpr std::size_t Count = 4; +constexpr std::size_t ElementBytes = 16; +constexpr std::size_t Padding = 16; +constexpr std::size_t TotalBytes = Count * ElementBytes + Padding; +alignas(16) std::array storage{}; +alignas(16) std::array source{}; +std::byte* destination = nullptr; +std::size_t constructed = 0; +std::size_t copied = 0; +std::size_t destroyed = 0; +std::size_t allocated = 0; +std::size_t freed = 0; +bool failAllocation = false; +bool failConstruct = false; +bool failCopy = false; +bool failDestroy = false; + +void require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +void reset(std::byte* first) { + destination = first; + constructed = copied = destroyed = allocated = freed = 0; + failAllocation = false; + failConstruct = failCopy = failDestroy = false; +} + +void APS5_VABI construct(void* pointer) { + require(constructed < Count && pointer == destination + constructed * ElementBytes, "constructor received the wrong element"); + if (failConstruct && constructed == 2) throw 73; + std::memset(pointer, static_cast(++constructed), ElementBytes); +} + +void APS5_VABI copy(void* target, void* original) { + require(copied < Count && target == destination + copied * ElementBytes, "copy constructor received the wrong destination"); + require(original == source.data() + copied * ElementBytes, "copy constructor received the wrong source"); + if (failCopy && copied == 2) throw 73; + std::memcpy(target, original, ElementBytes); + ++copied; +} + +void APS5_VABI destroy(void* pointer) { + const auto count = copied != 0 ? copied : constructed; + require(destroyed < count && pointer == destination + (count - destroyed - 1) * ElementBytes, "destructor received the wrong element or order"); + require(*static_cast(pointer) == std::byte(count - destroyed), "destructor received the wrong element contents"); + ++destroyed; + if (failDestroy && destroyed == 1) throw 73; +} + +void* APS5_VABI allocate(std::size_t bytes) { + require(bytes == TotalBytes, "allocator received the wrong size"); + ++allocated; + return failAllocation ? nullptr : storage.data(); +} + +void APS5_VABI release(void* pointer) { + require(pointer == storage.data(), "deallocator received the wrong storage"); + require(destroyed == constructed, "deallocator ran before element destruction finished"); + ++freed; +} + +void APS5_VABI releaseSized(void* pointer, std::size_t bytes) { + require(bytes == TotalBytes, "sized deallocator received the wrong size"); + release(pointer); +} + +void checkCookie(const void* array) { + std::size_t count = 0; + std::memcpy(&count, static_cast(array) - sizeof(count), sizeof(count)); + require(count == Count, "array cookie has the wrong count"); +} + +void checkElements() { + reset(source.data()); + __cxa_vec_ctor_nid_postfix(source.data(), Count, ElementBytes, construct, destroy); + require(constructed == Count, "constructor count is wrong"); + __cxa_vec_dtor_nid_postfix(source.data(), Count, ElementBytes, destroy); + require(destroyed == Count, "destructor count is wrong"); + reset(storage.data()); + __cxa_vec_cctor_nid_postfix(storage.data(), source.data(), Count, ElementBytes, copy, destroy); + require(copied == Count && std::memcmp(storage.data(), source.data(), source.size()) == 0, "copy construction failed"); + __cxa_vec_cleanup_nid_postfix(storage.data(), Count, ElementBytes, destroy); + require(destroyed == Count, "cleanup count is wrong"); + __cxa_vec_ctor_nid_postfix(nullptr, Count, ElementBytes, nullptr, nullptr); + __cxa_vec_cctor_nid_postfix(nullptr, nullptr, Count, ElementBytes, nullptr, nullptr); + __cxa_vec_dtor_nid_postfix(nullptr, Count, ElementBytes, nullptr); + __cxa_vec_cleanup_nid_postfix(nullptr, Count, ElementBytes, nullptr); +} + +void checkAllocation(bool sized) { + reset(storage.data() + Padding); + void* array = sized + ? __cxa_vec_new3_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, releaseSized) + : __cxa_vec_new2_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, release); + require(array == destination && constructed == Count && allocated == 1, "array allocation failed"); + checkCookie(array); + if (sized) __cxa_vec_delete3_nid_postfix(array, ElementBytes, Padding, destroy, releaseSized); + else __cxa_vec_delete2_nid_postfix(array, ElementBytes, Padding, destroy, release); + require(destroyed == Count && freed == 1, "array deletion failed"); + reset(storage.data() + Padding); + failAllocation = true; + array = sized + ? __cxa_vec_new3_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, releaseSized) + : __cxa_vec_new2_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, release); + require(array == nullptr && allocated == 1 && constructed == 0 && freed == 0, "failed allocation invoked element callbacks"); + if (sized) __cxa_vec_delete3_nid_postfix(nullptr, ElementBytes, Padding, destroy, releaseSized); + else __cxa_vec_delete2_nid_postfix(nullptr, ElementBytes, Padding, destroy, release); + require(freed == 0, "null array invoked deallocation"); +} + +void checkDefaultAllocation() { + void* array = __cxa_vec_new_nid_postfix(Count, ElementBytes, Padding, nullptr, nullptr); + require(array != nullptr, "default array allocation failed"); + checkCookie(array); + reset(static_cast(array)); + __cxa_vec_ctor_nid_postfix(array, Count, ElementBytes, construct, destroy); + __cxa_vec_delete_nid_postfix(array, ElementBytes, Padding, destroy); + require(constructed == Count && destroyed == Count, "default array deletion failed"); + __cxa_vec_delete_nid_postfix(nullptr, ElementBytes, Padding, destroy); +} + +template +void expectException(TAction action) { + bool caught = false; + try { action(); } + catch (int value) { require(value == 73, "callback exception changed"); caught = true; } + require(caught, "callback exception was swallowed"); +} + +void checkRollback(bool sized) { + reset(storage.data() + Padding); + failConstruct = true; + expectException([&] { + if (sized) __cxa_vec_new3_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, releaseSized); + else __cxa_vec_new2_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, release); + }); + require(constructed == 2 && destroyed == 2 && allocated == 1 && freed == 1, "constructor rollback failed"); + reset(storage.data() + Padding); + void* array = sized + ? __cxa_vec_new3_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, releaseSized) + : __cxa_vec_new2_nid_postfix(Count, ElementBytes, Padding, construct, destroy, allocate, release); + failDestroy = true; + expectException([&] { + if (sized) __cxa_vec_delete3_nid_postfix(array, ElementBytes, Padding, destroy, releaseSized); + else __cxa_vec_delete2_nid_postfix(array, ElementBytes, Padding, destroy, release); + }); + require(constructed == Count && destroyed == Count && allocated == 1 && freed == 1, "destructor rollback failed"); +} + +void checkCopyRollback() { + reset(storage.data()); + failCopy = true; + expectException([] { + __cxa_vec_cctor_nid_postfix(storage.data(), source.data(), Count, ElementBytes, copy, destroy); + }); + require(copied == 2 && destroyed == 2, "copy constructor rollback failed"); +} + +} + +int main() { + checkElements(); + checkAllocation(false); + checkAllocation(true); + checkDefaultAllocation(); + checkRollback(false); + checkRollback(true); + checkCopyRollback(); + std::puts("guest array callback arguments and lifecycle: PASS"); +} diff --git a/core/libs/tests/GuestCes.cpp b/core/libs/tests/GuestCes.cpp index 985713bcb..55bd86742 100644 --- a/core/libs/tests/GuestCes.cpp +++ b/core/libs/tests/GuestCes.cpp @@ -5,6 +5,8 @@ extern "C" { const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1252(void); +const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1251(void); +int APS5_VABI sceCesMbcsUcsContextInit(void*, const void*); int APS5_VABI sceCesSbcToUtf8(const std::uint8_t*, std::uint8_t, std::uint8_t*, std::uint32_t, std::uint32_t*); int APS5_VABI sceCesUtf8ToSbc(const std::uint8_t*, std::uint32_t, std::uint32_t*, const std::uint8_t*, std::uint8_t*); } @@ -80,4 +82,28 @@ int main() { Require(Utf8ToSbc(profile, "\xED\xA0\x80", 3) == invalidEncode); Require(Utf8ToSbc(profile, "\xE3\x81\x82", 3) == unassignedCode); Require(Utf8ToSbc(profile, "\xF0\x9F\x98\x80", 4) == unassignedCode); + + const std::uint8_t* cyrillic = sceCesRefersUcsProfileCp1251(); + Require(cyrillic != nullptr && cyrillic == sceCesRefersUcsProfileCp1251() && cyrillic != profile); + RequireSbcToUtf8(cyrillic, 'A', "A"); + RequireSbcToUtf8(cyrillic, 0x88, "\xE2\x82\xAC"); + RequireSbcToUtf8(cyrillic, 0xC0, "\xD0\x90"); + RequireSbcToUtf8(cyrillic, 0xFF, "\xD1\x8F"); + RequireSbcToUtf8(cyrillic, 0xA8, "\xD0\x81"); + RequireSbcToUtf8(cyrillic, 0xB9, "\xE2\x84\x96"); + RequireUtf8ToSbc(cyrillic, "\xD0\x9F", 0xCF); + RequireUtf8ToSbc(cyrillic, "\xD1\x91", 0xB8); + for (int sbc = 1; sbc < 256; ++sbc) { + std::uint8_t cp[4] = {}; + std::uint32_t cpLength = 0; + std::uint8_t back = 0; + Require(sceCesSbcToUtf8(cyrillic, static_cast(sbc), cp, sizeof(cp), &cpLength) == 0); + Require(sceCesUtf8ToSbc(cp, cpLength, &cpLength, cyrillic, &back) == 0 && back == sbc); + } + Require(Utf8ToSbc(cyrillic, "\xC3\xA9", 2) == unassignedCode); + Require(Utf8ToSbc(profile, "\xD0\x90", 2) == unassignedCode); + + char context[256] = {}; + Require(sceCesMbcsUcsContextInit(context, cyrillic) == 0); + Require(sceCesMbcsUcsContextInit(nullptr, cyrillic) == invalidParameter); } diff --git a/core/libs/tests/GuestCondTimedwait.cpp b/core/libs/tests/GuestCondTimedwait.cpp index 310812871..5a9190e1e 100644 --- a/core/libs/tests/GuestCondTimedwait.cpp +++ b/core/libs/tests/GuestCondTimedwait.cpp @@ -1,5 +1,6 @@ #include "SceTypes.hpp" #include +#include #include #include @@ -10,9 +11,12 @@ int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* a int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex); int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char* name); int APS5_VABI scePthreadCondDestroy(PthreadCond* cond); int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, unsigned int usec); +int APS5_VABI scePthreadCondSignal(PthreadCond* cond); +int APS5_VABI pthread_cond_reltimedwait_np_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, unsigned int usec); int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *)(void*), void*, void*); void APS5_VABI __pthread_cxa_finalize_nid_postfix(void*); unsigned int APS5_VABI sceKernelSleep(unsigned int seconds); @@ -23,6 +27,9 @@ static void APS5_VABI Cleanup(void*) { cleanupAt = 0; } static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); +static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); +static constexpr int POSIX_EPERM = 1; +static constexpr int POSIX_ETIMEDOUT = 60; static void Require(bool value) { if (!value) std::abort(); } @@ -39,7 +46,67 @@ static void* APS5_VABI Contender(void* arg) { return nullptr; } +struct Signalled { + PthreadMutex mutex = nullptr; + PthreadCond cond = nullptr; + bool ready = false; + int trylock = SCE_OK; +}; + +static void* APS5_VABI Trylocker(void* arg) { + auto& signalled = *static_cast(arg); + signalled.trylock = scePthreadMutexTrylock(&signalled.mutex); + return nullptr; +} + +static void* APS5_VABI Signaller(void* arg) { + auto& signalled = *static_cast(arg); + Require(scePthreadMutexLock(&signalled.mutex) == SCE_OK); + signalled.ready = true; + Require(scePthreadCondSignal(&signalled.cond) == SCE_OK); + Require(scePthreadMutexUnlock(&signalled.mutex) == SCE_OK); + return nullptr; +} + +static void RequireHeldByCaller(Signalled& signalled) { + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Trylocker, &signalled, nullptr) == SCE_OK); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + Require(signalled.trylock == SCE_KERNEL_ERROR_EBUSY); +} + +static void RelativeWait() { + Signalled signalled; + Require(scePthreadMutexInit(&signalled.mutex, nullptr, nullptr) == SCE_OK); + Require(scePthreadCondInit(&signalled.cond, nullptr, nullptr) == SCE_OK); + + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 1000) == POSIX_EPERM); + + Require(scePthreadMutexLock(&signalled.mutex) == SCE_OK); + const auto start = std::chrono::steady_clock::now(); + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 50000) == POSIX_ETIMEDOUT); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40)); + RequireHeldByCaller(signalled); + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 0) == POSIX_ETIMEDOUT); + RequireHeldByCaller(signalled); + + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Signaller, &signalled, nullptr) == SCE_OK); + const auto waitStart = std::chrono::steady_clock::now(); + while (!signalled.ready) + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 10000000) == 0); + Require(std::chrono::steady_clock::now() - waitStart < std::chrono::seconds(5)); + RequireHeldByCaller(signalled); + Require(scePthreadMutexUnlock(&signalled.mutex) == SCE_OK); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + + Require(scePthreadCondDestroy(&signalled.cond) == SCE_OK); + Require(scePthreadMutexDestroy(&signalled.mutex) == SCE_OK); +} + int main() { + RelativeWait(); + Context context; PthreadCond cond = nullptr; Require(scePthreadMutexInit(&context.mutex, nullptr, nullptr) == SCE_OK); diff --git a/core/libs/tests/GuestContentExport.cpp b/core/libs/tests/GuestContentExport.cpp index 12038531a..ef295d6ec 100644 --- a/core/libs/tests/GuestContentExport.cpp +++ b/core/libs/tests/GuestContentExport.cpp @@ -1,25 +1,41 @@ #include "SceTypes.hpp" +#include #include +#include #include extern "C" int APS5_VABI sceContentExportInit2(const ContentExportInitParam2* init_param); extern "C" int APS5_VABI sceContentExportTerm(void); +extern "C" int APS5_VABI sceContentExportStart(void); +extern "C" int APS5_VABI sceContentExportFinish(std::int32_t expId); +extern "C" int APS5_VABI sceContentExportGetProgress(std::int32_t expId); +extern "C" int APS5_VABI sceContentExportCancel(std::int32_t expId); +extern "C" int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const ContentExportParam* param, const ContentExportCallbackParam* contentCallbackParam, + const char* thumbnailType, const ContentExportCallbackParam* thumbnailCallbackParam, char* path, std::size_t pathBufferLength); namespace { void Require(bool value) { if (!value) std::abort(); } -template +template bool Throws(TAction action) { try { action(); - } catch (const std::logic_error&) { + } catch (const TException&) { return true; } return false; } int allocator = 0; +char path[1024]{}; + +void Export(std::int32_t expId) { + ContentExportParam param{}; + ContentExportCallbackParam content{}; + ContentExportCallbackParam thumbnail{}; + sceContentExportFromDataWithThumbnail(expId, ¶m, &content, "image/png", &thumbnail, path, sizeof(path)); +} } @@ -27,6 +43,8 @@ int main() { ContentExportInitParam2 param{&allocator, &allocator, nullptr, 0x10000, 0, 0}; Require(Throws([] { sceContentExportInit2(nullptr); })); Require(Throws([] { sceContentExportTerm(); })); + Require(Throws([] { sceContentExportStart(); })); + Require(Throws([] { sceContentExportFinish(1); })); ContentExportInitParam2 missing = param; missing.free_func = nullptr; Require(Throws([&] { sceContentExportInit2(&missing); })); @@ -38,5 +56,33 @@ int main() { Require(sceContentExportTerm() == 0); Require(Throws([] { sceContentExportTerm(); })); Require(sceContentExportInit2(¶m) == 0); + + std::set sessions; + for (int i = 0; i < 10; ++i) { + int session = sceContentExportStart(); + Require(session > 0); + Require(sessions.insert(session).second); + } + Require(Throws([] { sceContentExportStart(); })); + Require(Throws([] { sceContentExportTerm(); })); + int first = *sessions.begin(); + Require(Throws([&] { sceContentExportGetProgress(first); })); + Require(Throws([&] { sceContentExportCancel(first); })); + Require(Throws([&] { Export(first); })); + Require(path[0] == 0); + Require(sceContentExportFinish(first) == 0); + Require(Throws([&] { sceContentExportFinish(first); })); + Require(Throws([&] { sceContentExportGetProgress(first); })); + Require(Throws([&] { sceContentExportCancel(first); })); + Require(Throws([&] { Export(first); })); + Require(Throws([] { sceContentExportFinish(0); })); + Require(Throws([] { sceContentExportFinish(-1); })); + sessions.erase(first); + int reopened = sceContentExportStart(); + Require(reopened > 0 && reopened != first); + Require(sessions.insert(reopened).second); + for (int session : sessions) Require(sceContentExportFinish(session) == 0); Require(sceContentExportTerm() == 0); + Require(Throws([] { sceContentExportStart(); })); + Require(Throws([&] { sceContentExportFinish(reopened); })); } diff --git a/core/libs/tests/GuestContentUnimplemented.cpp b/core/libs/tests/GuestContentUnimplemented.cpp index 00a291f52..549946300 100644 --- a/core/libs/tests/GuestContentUnimplemented.cpp +++ b/core/libs/tests/GuestContentUnimplemented.cpp @@ -6,9 +6,6 @@ extern "C" { int APS5_VABI sceContentDeleteById(); -int APS5_VABI sceContentExportGetProgress(); -int APS5_VABI sceContentExportCancel(); -int APS5_VABI sceContentExportFromDataWithThumbnail(); int APS5_VABI sceContentSearchGetContentLastUpdateId(); int APS5_VABI sceContentSearchOpenMetadataByContentId(); int APS5_VABI sceContentSearchGetTotalContentSize(); @@ -34,9 +31,6 @@ bool ReportsNotImplemented(const char* name, int (APS5_VABI *function)()) { int main() { bool correct = true; correct &= ReportsNotImplemented("sceContentDeleteById", sceContentDeleteById); - correct &= ReportsNotImplemented("sceContentExportGetProgress", sceContentExportGetProgress); - correct &= ReportsNotImplemented("sceContentExportCancel", sceContentExportCancel); - correct &= ReportsNotImplemented("sceContentExportFromDataWithThumbnail", sceContentExportFromDataWithThumbnail); correct &= ReportsNotImplemented("sceContentSearchGetContentLastUpdateId", sceContentSearchGetContentLastUpdateId); correct &= ReportsNotImplemented("sceContentSearchOpenMetadataByContentId", sceContentSearchOpenMetadataByContentId); correct &= ReportsNotImplemented("sceContentSearchGetTotalContentSize", sceContentSearchGetTotalContentSize); diff --git a/core/libs/tests/GuestCredentials.cpp b/core/libs/tests/GuestCredentials.cpp index aa2c8544f..99e003528 100644 --- a/core/libs/tests/GuestCredentials.cpp +++ b/core/libs/tests/GuestCredentials.cpp @@ -1,14 +1,25 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include + extern "C" { int APS5_VABI seteuid_nid_postfix(std::uint32_t); int APS5_VABI setegid_nid_postfix(std::uint32_t); +int APS5_VABI setuid_nid_postfix(std::uint32_t); +int APS5_VABI setreuid_nid_postfix(std::uint32_t, std::uint32_t); +int APS5_VABI setgroups_nid_postfix(int, const std::uint32_t*); +int APS5_VABI getgroups_nid_postfix(int, std::uint32_t*); +int APS5_VABI getuid_nid_postfix(void); +int APS5_VABI geteuid_nid_postfix(void); +int APS5_VABI getegid_nid_postfix(void); int* APS5_VABI __error_nid_postfix(); } -using SetId = int (APS5_VABI *)(std::uint32_t); + static void Require(bool value) { if (!value) std::abort(); } +using SetId = int (APS5_VABI *)(std::uint32_t); + static bool Throws(SetId function, std::uint32_t id) { try { function(id); @@ -17,6 +28,16 @@ static bool Throws(SetId function, std::uint32_t id) { } return false; } + +constexpr int GuestEfault = 14; +constexpr int GuestEinval = 22; +constexpr std::uint32_t Unchanged = 0xffffffffu; + +static void RequireFailure(int result, int error) { + Require(result == -1); + Require(*__error_nid_postfix() == error); +} + int main() { *__error_nid_postfix() = 13; Require(seteuid_nid_postfix(0) == 0); @@ -24,8 +45,35 @@ int main() { Require(*__error_nid_postfix() == 13); Require(Throws(seteuid_nid_postfix, 1000)); Require(Throws(setegid_nid_postfix, 1000)); - Require(Throws(seteuid_nid_postfix, 0xFFFFFFFFu)); - Require(Throws(setegid_nid_postfix, 0xFFFFFFFFu)); - Require(seteuid_nid_postfix(0) == 0); - Require(setegid_nid_postfix(0) == 0); + Require(Throws(seteuid_nid_postfix, Unchanged)); + Require(Throws(setegid_nid_postfix, Unchanged)); + + Require(setuid_nid_postfix(0) == 0); + Require(getuid_nid_postfix() == 0 && geteuid_nid_postfix() == 0); + Require(setreuid_nid_postfix(Unchanged, Unchanged) == 0); + Require(setreuid_nid_postfix(0, Unchanged) == 0); + Require(setreuid_nid_postfix(Unchanged, 0) == 0); + Require(setreuid_nid_postfix(0, 0) == 0); + + std::array groups{}; + Require(getgroups_nid_postfix(0, nullptr) == 1); + Require(getgroups_nid_postfix(static_cast(groups.size()), groups.data()) == 1 && groups[0] == 0); + Require(groups[0] == static_cast(getegid_nid_postfix())); + + const std::array wanted{0, 5, 7}; + Require(setgroups_nid_postfix(static_cast(wanted.size()), wanted.data()) == 0); + Require(getgroups_nid_postfix(0, nullptr) == 3); + RequireFailure(getgroups_nid_postfix(2, groups.data()), GuestEinval); + RequireFailure(getgroups_nid_postfix(3, nullptr), GuestEfault); + Require(getgroups_nid_postfix(static_cast(groups.size()), groups.data()) == 3); + Require(groups[0] == 0 && groups[1] == 5 && groups[2] == 7); + + Require(setgroups_nid_postfix(0, nullptr) == 0); + Require(getgroups_nid_postfix(static_cast(groups.size()), groups.data()) == 1 && groups[0] == 0); + + RequireFailure(setgroups_nid_postfix(-1, wanted.data()), GuestEinval); + RequireFailure(setgroups_nid_postfix(1024, wanted.data()), GuestEinval); + RequireFailure(setgroups_nid_postfix(1, nullptr), GuestEfault); + Require(getgroups_nid_postfix(0, nullptr) == 1); + return 0; } diff --git a/core/libs/tests/GuestCrt.cpp b/core/libs/tests/GuestCrt.cpp index 7bd72ca01..4aeeabd1e 100644 --- a/core/libs/tests/GuestCrt.cpp +++ b/core/libs/tests/GuestCrt.cpp @@ -31,8 +31,10 @@ std::size_t APS5_VABI mbstowcs_nid_postfix(std::uint16_t* dest, const char* src, char* APS5_VABI strtok_s_nid_postfix(char* str, const char* delim, char** context); int APS5_VABI __fpclassifyd_nid_postfix(double value); unsigned __int128 APS5_VABI __udivti3_nid_postfix(unsigned __int128 dividend, unsigned __int128 divisor); +__int128 APS5_VABI __divti3_nid_postfix(__int128 dividend, __int128 divisor); SonyLconv* APS5_VABI localeconv_nid_postfix(void); int APS5_VABI sceAmprFpclassifyd(double value); +unsigned int APS5_VABI _ZSt14_Random_devicev_nid_postfix(); } #define Require(condition) do { if (!(condition)) { std::fprintf(stderr, "guest_crt: line %d: %s\n", __LINE__, #condition); std::abort(); } } while (0) @@ -84,6 +86,29 @@ int main() { } Require(trapped); + Require(__divti3_nid_postfix(100, 3) == 33); + Require(__divti3_nid_postfix(-100, 3) == -33); + Require(__divti3_nid_postfix(100, -3) == -33); + Require(__divti3_nid_postfix(-100, -3) == 33); + Require(SonyDivti3(0, 5) == 0); + const __int128 large = (__int128)1 << 100; + Require(SonyDivti3(large, 3) == large / 3); + Require(SonyDivti3(-large, 3) == -(large / 3)); + Require(SonyDivti3(large, -3) == -(large / 3)); + Require(SonyDivti3(-large, -3) == large / 3); + const __int128 minimum = (__int128)((unsigned __int128)1 << 127); + const __int128 maximum = (__int128)(((unsigned __int128)1 << 127) - 1); + Require(SonyDivti3(minimum, 1) == minimum); + Require(SonyDivti3(minimum, -1) == minimum); + Require(SonyDivti3(maximum, -1) == -maximum); + trapped = false; + try { + SonyDivti3(1, 0); + } catch (const std::exception&) { + trapped = true; + } + Require(trapped); + void* previous = sceAmprSetConstraintHandlerS(reinterpret_cast(&CountingHandler)); Require(previous != nullptr); Require(previous != reinterpret_cast(&CountingHandler)); @@ -143,6 +168,13 @@ int main() { Require(sceAmprMbstowcs(wide, "ABC", 8) == 3); Require(wide[0] == 65 && wide[1] == 66 && wide[2] == 67 && wide[3] == 0); Require(sceAmprMbstowcs(nullptr, "ABC", 8) == 3); + Require(sceAmprMbstowcs(nullptr, "ABC", 0) == 3 && sceAmprMbstowcs(nullptr, "ABC", 1) == 3); + Require(mbstowcs_nid_postfix(nullptr, "ABC", 0) == 3 && mbstowcs_nid_postfix(nullptr, "ABC", 1) == 3); Require(sceAmprMbstowcs(wide, "ABC", 2) == 2); Require(mbstowcs_nid_postfix(wide, "ABC", 8) == 3); + + const unsigned int firstRandom = _ZSt14_Random_devicev_nid_postfix(); + bool randomChanged = false; + for (int draw = 0; draw < 8; ++draw) randomChanged |= _ZSt14_Random_devicev_nid_postfix() != firstRandom; + Require(randomChanged); } diff --git a/core/libs/tests/GuestDirectoryBuffer.cpp b/core/libs/tests/GuestDirectoryBuffer.cpp index 9f2bf60d0..6d1e6f4c3 100644 --- a/core/libs/tests/GuestDirectoryBuffer.cpp +++ b/core/libs/tests/GuestDirectoryBuffer.cpp @@ -17,6 +17,9 @@ int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI sceKernelClose(int); int APS5_VABI sceKernelGetdents(int, char*, int); int APS5_VABI sceKernelGetdirentries(int, char*, int, std::int64_t*); +int APS5_VABI getdents_nid_postfix(int, char*, int); +int APS5_VABI getdirentries_nid_postfix(int, char*, int, std::int64_t*); +int* APS5_VABI __error_nid_postfix(); } static void Check(bool value, int line) { @@ -27,15 +30,15 @@ static void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) -int main() { +static void CheckName(const std::u8string& utf8Name) { const auto root = std::filesystem::temp_directory_path() / ("anyps5-directory-buffer-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(root)); - const std::string name = "a-long-directory-entry-name.bin"; - { std::ofstream file(root / name); Require(static_cast(file)); } + const std::string name(utf8Name.begin(), utf8Name.end()); + { std::ofstream file(root / std::filesystem::path(utf8Name)); Require(static_cast(file)); } const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); Require(directory >= 0); - std::array buffer; + std::array buffer; buffer.fill('x'); std::int64_t base = -1; Require(sceKernelGetdirentries(directory, buffer.data(), 8, &base) == SCE_KERNEL_ERROR_EINVAL); @@ -54,9 +57,60 @@ int main() { const int recordSize = static_cast((8 + name.size() + 1 + 3) & ~std::size_t{3}); Require(sceKernelGetdirentries(directory, buffer.data(), recordSize, &base) == recordSize); Require(std::strcmp(buffer.data() + 8, name.c_str()) == 0); + Require(static_cast(buffer[7]) == name.size()); + std::uint16_t storedSize = 0; + std::memcpy(&storedSize, buffer.data() + 4, sizeof(storedSize)); + Require(storedSize == recordSize); + const auto rootUtf8 = root.u8string(); + const std::string enumeratedPath = std::string(rootUtf8.begin(), rootUtf8.end()) + '/' + (buffer.data() + 8); + const int file = sceKernelOpen(enumeratedPath.c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(file >= 0 && sceKernelClose(file) == 0); Require(sceKernelGetdents(directory, buffer.data(), 8) == 0); Require(sceKernelGetdents(directory, buffer.data(), 256) == 0); Require(sceKernelClose(directory) == 0); Require(sceKernelGetdents(-1, buffer.data(), 256) == SCE_KERNEL_ERROR_ENOTDIR); std::filesystem::remove_all(root); } + +static void CheckOversizedName() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-directory-name-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + std::u8string name; + for (int i = 0; i < 86; ++i) name += u8"\u30bb"; + { std::ofstream file(root / std::filesystem::path(name)); Require(static_cast(file)); } + const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(directory >= 0); + std::array buffer; + buffer.fill('x'); + Require(sceKernelGetdents(directory, buffer.data(), static_cast(buffer.size())) == 24); + Require(std::strcmp(buffer.data() + 8, ".") == 0); + Require(std::strcmp(buffer.data() + 20, "..") == 0); + Require(std::all_of(buffer.begin() + 24, buffer.end(), [](char byte) { return byte == 'x'; })); + for (int attempt = 0; attempt < 2; ++attempt) { + buffer.fill('x'); + Require(sceKernelGetdents(directory, buffer.data(), static_cast(buffer.size())) == SCE_KERNEL_ERROR_ENAMETOOLONG); + Require(std::all_of(buffer.begin(), buffer.end(), [](char byte) { return byte == 'x'; })); + } + *__error_nid_postfix() = 0; + Require(getdents_nid_postfix(directory, buffer.data(), static_cast(buffer.size())) == -1); + Require(*__error_nid_postfix() == 63); + *__error_nid_postfix() = 0; + std::int64_t base = -1; + Require(getdirentries_nid_postfix(directory, buffer.data(), static_cast(buffer.size()), &base) == -1); + Require(*__error_nid_postfix() == 63); + Require(std::all_of(buffer.begin(), buffer.end(), [](char byte) { return byte == 'x'; })); + Require(sceKernelClose(directory) == 0); + std::filesystem::remove_all(root); +} + +int main() { + CheckName(u8"a-long-directory-entry-name.bin"); + CheckName(u8"caf\u00e9-music.bin"); + CheckName(u8"\u30bb\u30fc\u30d6-music.bin"); + CheckName(u8"\U0001f3ae-music.bin"); + std::u8string longest; + for (int i = 0; i < 85; ++i) longest += u8"\u30bb"; + CheckName(longest); + CheckOversizedName(); +} diff --git a/core/libs/tests/GuestDup.cpp b/core/libs/tests/GuestDup.cpp index 1e3683f92..e4241dc52 100644 --- a/core/libs/tests/GuestDup.cpp +++ b/core/libs/tests/GuestDup.cpp @@ -15,6 +15,7 @@ int APS5_VABI socketpair_nid_postfix(int, int, int, int*); std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI fcntl_nid_postfix(int, int, ...); int* APS5_VABI __error_nid_postfix(); } @@ -81,6 +82,10 @@ static void Dup2ReplacesTarget() { for (const int descriptor : {first[0], first[1], second[0], second[1]}) Require(close_nid_postfix(descriptor) == 0); } +#ifdef _WIN32 +static bool g_invalidParameter = false; +static void RecordInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned, std::uintptr_t) { g_invalidParameter = true; } +#endif static void BadDescriptors() { int pipe[2]; Require(pipe_nid_postfix(pipe) == 0); @@ -94,6 +99,15 @@ static void BadDescriptors() { RequireFailure(dup2_nid_postfix(-1, pipe[0]), GuestEbadf); RequireFailure(dup_nid_postfix(FirstSocket + 0x0ffffff0), GuestEbadf); RequireFailure(dup2_nid_postfix(FirstSocket + 0x0ffffff0, FirstSocket + 0x0ffffff1), GuestEbadf); +#ifdef _WIN32 + const auto previous = _set_invalid_parameter_handler(RecordInvalidParameter); +#endif + RequireFailure(close_nid_postfix(closed), GuestEbadf); + RequireFailure(close_nid_postfix(-1), GuestEbadf); +#ifdef _WIN32 + _set_invalid_parameter_handler(previous); + Require(!g_invalidParameter); +#endif Require(close_nid_postfix(pipe[0]) == 0); } @@ -165,10 +179,39 @@ static void RandomDeviceDuplicates() { for (const int descriptor : {random, dupRandom, pipe[1], secondRandom}) Require(close_nid_postfix(descriptor) == 0); } +static void DuplicatesShareStatusFlags() { + constexpr int GetDescriptorFlags = 1; + constexpr int SetDescriptorFlags = 2; + constexpr int GetFlags = 3; + constexpr int SetFlags = 4; + constexpr int NonBlocking = 4; + constexpr int GuestEagain = 35; + int pipe[2]; + int other[2]; + Require(pipe_nid_postfix(pipe) == 0); + Require(pipe_nid_postfix(other) == 0); + Require(fcntl_nid_postfix(pipe[0], SetDescriptorFlags, 1) == 0); + const int duplicate = dup_nid_postfix(pipe[0]); + Require(duplicate >= 0); + Require(fcntl_nid_postfix(duplicate, GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(duplicate, SetFlags, NonBlocking) == 0); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == NonBlocking); + Require(dup2_nid_postfix(pipe[0], other[0]) == other[0]); + Require(fcntl_nid_postfix(other[0], GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(other[0], GetFlags) == NonBlocking); + char buffer[4] = {}; + RequireFailure(static_cast(read_nid_postfix(other[0], buffer, sizeof(buffer))), GuestEagain); + Require(fcntl_nid_postfix(pipe[0], SetFlags, 0) == 0); + Require(fcntl_nid_postfix(duplicate, GetFlags) == 0); + Require(fcntl_nid_postfix(pipe[0], GetDescriptorFlags) == 1); + for (const int descriptor : {pipe[0], pipe[1], other[0], other[1], duplicate}) Require(close_nid_postfix(descriptor) == 0); +} + int main() { DupOutlivesOriginal(); Dup2ReplacesTarget(); BadDescriptors(); SocketDuplicates(); RandomDeviceDuplicates(); + DuplicatesShareStatusFlags(); } diff --git a/core/libs/tests/GuestEnvironment.cpp b/core/libs/tests/GuestEnvironment.cpp index df1910cf4..7bd0c7fb8 100644 --- a/core/libs/tests/GuestEnvironment.cpp +++ b/core/libs/tests/GuestEnvironment.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include extern "C" { char* APS5_VABI getenv_nid_postfix(const char*); int APS5_VABI setenv_nid_postfix(const char*, const char*, int); @@ -36,6 +37,13 @@ int main() { Require(std::strcmp(getenv_nid_postfix(key), "two") == 0); Require(setenv_nid_postfix("bad=name", "x", 1) == -1); Require(*__error_nid_postfix() == 22); + const char* const invalidNames[] = {nullptr, "", "bad=name"}; + for (const char* name : invalidNames) { + *__error_nid_postfix() = 0; + Require(getenv_nid_postfix(name) == nullptr && *__error_nid_postfix() == 22); + } + *__error_nid_postfix() = 34; + Require(getenv_nid_postfix("ANYPS5_GUEST_ENV_ABSENT_4C27") == nullptr && *__error_nid_postfix() == 34); Require(unsetenv_nid_postfix("") == -1); char invalid[] = "no-separator"; Require(putenv_nid_postfix(invalid) == -1); @@ -43,4 +51,11 @@ int main() { Require(getenv_nid_postfix(key) == nullptr); Require(unsetenv_nid_postfix(key) == 0); Require(unsetenv_nid_postfix("anyps5_guest_env_test_4c27") == 0); + Require(setenv_nid_postfix("Q7A", "a", 1) == 0); + Require(setenv_nid_postfix("Q7B", "b", 1) == 0); + const char* kept = getenv_nid_postfix("Q7B"); + Require(unsetenv_nid_postfix("Q7A") == 0); + for (int index = 0; index < 64; ++index) + Require(setenv_nid_postfix(("ANYPS5_GUEST_ENV_GROW_4C27_" + std::to_string(index)).c_str(), "x", 1) == 0); + Require(getenv_nid_postfix("Q7B") == kept && std::strcmp(kept, "b") == 0); } diff --git a/core/libs/tests/GuestExitOrder.cpp b/core/libs/tests/GuestExitOrder.cpp index 067bea90f..889e6f0f2 100644 --- a/core/libs/tests/GuestExitOrder.cpp +++ b/core/libs/tests/GuestExitOrder.cpp @@ -6,6 +6,7 @@ #include extern "C" int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *func)(void*), void* arg, void* dsoHandle); +extern "C" int APS5_VABI atexit_nid_postfix(void (APS5_VABI *func)()); namespace { @@ -19,14 +20,16 @@ void APS5_VABI GuestHandler(void*) { Record('G'); } void HostHandler() { Record('H'); } -bool GuestHandlersRunFirst() { +void APS5_VABI AtexitHandler() { Record('A'); } + +template +bool ExitOrder(Setup setup, const char* expected) { if (pipe(events) != 0) return false; const pid_t child = fork(); if (child < 0) return false; if (child == 0) { close(events[0]); - __cxa_atexit_nid_postfix(GuestHandler, nullptr, nullptr); - std::atexit(HostHandler); + setup(); LibcExit_nid_no_patch(0); } close(events[1]); @@ -39,15 +42,19 @@ bool GuestHandlersRunFirst() { } close(events[0]); int status = 0; - return waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0 && length == 2 && order[0] == 'G' && order[1] == 'H'; + return waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0 && length == 2 && order[0] == expected[0] && order[1] == expected[1]; } } int main() { - if (!GuestHandlersRunFirst()) { + if (!ExitOrder([] { __cxa_atexit_nid_postfix(GuestHandler, nullptr, nullptr); std::atexit(HostHandler); }, "GH")) { std::fputs("guest exit handlers must run before the host exit handlers\n", stderr); return 1; } + if (!ExitOrder([] { __cxa_atexit_nid_postfix(GuestHandler, nullptr, nullptr); atexit_nid_postfix(AtexitHandler); }, "AG")) { + std::fputs("atexit and __cxa_atexit handlers must run in reverse order of registration\n", stderr); + return 1; + } return 0; } diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index ded45210e..79d20523f 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -30,6 +30,7 @@ int APS5_VABI sceKernelFutimes(int, const KernelTimeval*); int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI sceKernelFsync(int); int APS5_VABI fdatasync_nid_postfix(int); +int APS5_VABI fsync_nid_postfix(int); int APS5_VABI sceKernelFdatasync(int); int APS5_VABI sceKernelWriteThrottlingStatus(std::uint64_t*); int APS5_VABI sceKernelFtruncate(int, long long); @@ -358,6 +359,8 @@ int main() { Require(futimes_nid_postfix(descriptor, nullptr) == -1 && *__error_nid_postfix() == 9); Require(fdatasync_nid_postfix(descriptor) == -1 && *__error_nid_postfix() == 9); Require(sceKernelFdatasync(descriptor) == static_cast(0x80020009u)); + Require(fsync_nid_postfix(descriptor) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelFsync(descriptor) == static_cast(0x80020009u)); const int socket = socket_nid_postfix(2, 2, 0); Require(socket >= 0); Require(sceKernelFchmod(socket, 0600) == static_cast(0x80020016u)); @@ -365,12 +368,17 @@ int main() { Require(futimes_nid_postfix(socket, nullptr) == -1 && *__error_nid_postfix() == 22); Require(fdatasync_nid_postfix(socket) == -1 && *__error_nid_postfix() == 22); Require(sceKernelFdatasync(socket) == static_cast(0x80020016u)); + Require(fsync_nid_postfix(socket) == -1 && *__error_nid_postfix() == 22); + Require(sceKernelFsync(socket) == static_cast(0x80020016u)); Require(close_nid_postfix(socket) == 0); Require(fchmod_nid_postfix(socket, 0600) == -1 && *__error_nid_postfix() == 9); Require(futimes_nid_postfix(socket, nullptr) == -1 && *__error_nid_postfix() == 9); Require(fdatasync_nid_postfix(socket) == -1 && *__error_nid_postfix() == 9); Require(sceKernelFdatasync(socket) == static_cast(0x80020009u)); + Require(fsync_nid_postfix(socket) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelFsync(socket) == static_cast(0x80020009u)); Require(sceKernelFdatasync(0x7fffffff) == static_cast(0x80020009u)); + Require(sceKernelFsync(0x7fffffff) == static_cast(0x80020009u)); Require(remove_nid_postfix(sized.string().c_str()) == 0); const auto present = root / "present.txt"; const auto presentName = present.string(); @@ -420,10 +428,20 @@ int main() { std::filesystem::create_symlink("present.txt", link); Require(lstat_nid_postfix(link.string().c_str(), &linkStatus) == 0); Require((linkStatus.st_mode & 0170000) == 0120000 && linkStatus.st_size == 11); - Require(stat_nid_postfix(link.string().c_str(), &status) == 0 && (status.st_mode & 0170000) == 0100000); + Require(stat_nid_postfix(link.string().c_str(), &status) == 0 && (status.st_mode & 0170000) == 0100000 && status.st_size == 5); const auto dangling = root / "dangling"; std::filesystem::create_symlink("missing.txt", dangling); Require(lstat_nid_postfix(dangling.string().c_str(), &linkStatus) == 0 && (linkStatus.st_mode & 0170000) == 0120000); + constexpr int noFollow = 0x100; + Require(sceKernelOpen(link.string().c_str(), noFollow, 0) == static_cast(0x8002001Fu)); + Require(open_nid_postfix(link.string().c_str(), noFollow, 0) == -1 && *__error_nid_postfix() == 31); + Require(open_nid_postfix(dangling.string().c_str(), noFollow | 0x201, 0600) == -1 && *__error_nid_postfix() == 31); + Require(!std::filesystem::exists(root / "missing.txt")); + Require(open_nid_postfix(dangling.string().c_str(), noFollow | 0xA01, 0600) == -1 && *__error_nid_postfix() == 17); + const int followed = open_nid_postfix(link.string().c_str(), 0, 0); + Require(followed >= 0 && close_nid_postfix(followed) == 0); + const int plain = open_nid_postfix(presentName.c_str(), noFollow, 0); + Require(plain >= 0 && close_nid_postfix(plain) == 0); Require(stat_nid_postfix(dangling.string().c_str(), &status) == -1 && *__error_nid_postfix() == 2); Require(lstat_nid_postfix((root / "present.txt" / "child").string().c_str(), &linkStatus) == -1 && *__error_nid_postfix() == 20); const auto viaMissing = rootName + "/missing/../present.txt"; @@ -435,7 +453,7 @@ int main() { Require(unlink_nid_postfix(link.string().c_str()) == 0 && unlink_nid_postfix(dangling.string().c_str()) == 0); #else const auto fileLink = root / "file-link"; - if (CreateSymbolicLinkW(fileLink.c_str(), present.c_str(), SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) == 0) { + if (CreateSymbolicLinkW(fileLink.c_str(), L"present.txt", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) == 0) { Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD); std::fprintf(stderr, "Windows lstat link checks skipped: creating symbolic links needs Developer Mode or administrator rights\n"); } else { @@ -443,7 +461,7 @@ int main() { Require(std::filesystem::create_directory(targetDirectory)); const auto directoryLink = root / "directory-link"; const auto danglingLink = root / "dangling-link"; - Require(CreateSymbolicLinkW(directoryLink.c_str(), targetDirectory.c_str(), + Require(CreateSymbolicLinkW(directoryLink.c_str(), L"target-directory", SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); Require(CreateSymbolicLinkW(danglingLink.c_str(), missing.c_str(), SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); const auto checkUnsupportedLink = [](const std::filesystem::path& link) { @@ -462,6 +480,7 @@ int main() { const bool statusUnchanged = std::all_of(bytes, bytes + sizeof(untouched), [](unsigned char value) { return value == 0x5a; }); return rejected && errorUnchanged && statusUnchanged; }; + Require(stat_nid_postfix(fileLink.string().c_str(), &status) == 0 && status.st_size == 5); Require(checkUnsupportedLink(fileLink)); Require(checkUnsupportedLink(directoryLink)); Require(checkUnsupportedLink(danglingLink)); @@ -619,6 +638,20 @@ int main() { Require(rmdir_nid_postfix((missingName + "/").c_str()) == -1 && *__error_nid_postfix() == 2); Require(sceKernelRmdir((presentName + "/").c_str()) == static_cast(0x80020014u)); Require(sceKernelRmdir((missingName + "/").c_str()) == static_cast(0x80020002u)); +#ifndef _WIN32 + const auto linkedDirectory = root / "linked-directory"; + const auto directoryLink = root / "directory-link"; + const auto danglingDirectoryLink = root / "dangling-directory-link"; + Require(std::filesystem::create_directory(linkedDirectory)); + std::filesystem::create_directory_symlink("linked-directory", directoryLink); + std::filesystem::create_directory_symlink("missing-directory", danglingDirectoryLink); + Require(rmdir_nid_postfix(directoryLink.string().c_str()) == -1 && *__error_nid_postfix() == 20); + Require(sceKernelRmdir(directoryLink.string().c_str()) == static_cast(0x80020014u)); + Require(rmdir_nid_postfix(danglingDirectoryLink.string().c_str()) == -1 && *__error_nid_postfix() == 20); + Require(std::filesystem::is_symlink(directoryLink) && std::filesystem::is_directory(linkedDirectory)); + Require(unlink_nid_postfix(directoryLink.string().c_str()) == 0 && unlink_nid_postfix(danglingDirectoryLink.string().c_str()) == 0); + Require(rmdir_nid_postfix(linkedDirectory.string().c_str()) == 0 && !std::filesystem::exists(linkedDirectory)); +#endif const auto dotDirectory = root / "dot-directory"; Require(std::filesystem::create_directory(dotDirectory)); const auto dotDirectoryName = dotDirectory.string() + "/."; diff --git a/core/libs/tests/GuestFiopen.cpp b/core/libs/tests/GuestFiopen.cpp index 6c2636ae3..66ab93791 100644 --- a/core/libs/tests/GuestFiopen.cpp +++ b/core/libs/tests/GuestFiopen.cpp @@ -10,6 +10,7 @@ extern "C" { FileStream* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const char*, int, int); FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*); +int APS5_VABI fopen_s_nid_postfix(FileStream**, const char*, const char*); int APS5_VABI fclose_nid_postfix(FileStream*); std::int64_t APS5_VABI ftello_nid_postfix(FileStream*); } @@ -24,6 +25,8 @@ static void Check(bool value, int line) { constexpr int In = 0x01, Out = 0x02, Ate = 0x04, App = 0x08, Trunc = 0x10, Nocreate = 0x20, Noreplace = 0x40, Binary = 0x80; +constexpr int GuestEinval = 22; +constexpr int GuestEnoent = 2; static FileStream* Open(const char* name, int mode) { return _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(name, mode, 0x1b6); @@ -66,6 +69,21 @@ int main() { Write(Open(file, Out), "abc"); Require(Contents(file) == "abc"); + FileStream* checkedStream = &_Stdout_nid_postfix; + Require(fopen_s_nid_postfix(&checkedStream, nullptr, "rb") == GuestEinval); + Require(checkedStream == nullptr); + checkedStream = &_Stderr_nid_postfix; + Require(fopen_s_nid_postfix(&checkedStream, file, nullptr) == GuestEinval); + Require(checkedStream == nullptr); + Require(fopen_s_nid_postfix(nullptr, file, "rb") == GuestEinval); + checkedStream = &_Stdout_nid_postfix; + Require(fopen_s_nid_postfix(&checkedStream, file, "rb") == 0); + Require(checkedStream != nullptr); + Require(fclose_nid_postfix(checkedStream) == 0); + checkedStream = &_Stdout_nid_postfix; + Require(fopen_s_nid_postfix(&checkedStream, missing, "rb") == GuestEnoent); + Require(checkedStream == nullptr); + auto* stream = Open(file, In); Require(stream != nullptr && std::fgetc(stream->GetHandle()) == 'a'); fclose_nid_postfix(stream); @@ -131,6 +149,14 @@ int main() { Require(stream != nullptr && std::fgetc(stream->GetHandle()) == 'k'); fclose_nid_postfix(stream); +#ifndef _WIN32 + auto* full = std::fopen("/dev/full", "wb"); + Require(full != nullptr); + auto* fullStream = new FileStream(full, true); + Require(std::fwrite("buffered", 1, 8, full) == 8); + Require(fclose_nid_postfix(fullStream) == EOF); +#endif + std::filesystem::remove_all(dir); return 0; } diff --git a/core/libs/tests/GuestFreebsdClocks.cpp b/core/libs/tests/GuestFreebsdClocks.cpp index d75ca2ec4..9353846f0 100644 --- a/core/libs/tests/GuestFreebsdClocks.cpp +++ b/core/libs/tests/GuestFreebsdClocks.cpp @@ -16,7 +16,7 @@ int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds); int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* tv, KernelTimezone* tz); std::int64_t APS5_VABI _Xtime_get_ticks_nid_postfix(); int APS5_VABI sceKernelConvertLocaltimeToUtc(std::int64_t, std::int64_t, std::int64_t*, KernelTimesec*, std::int32_t*); -int APS5_VABI sceKernelConvertUtcToLocaltime(std::int64_t, std::int64_t*, KernelTimesec*, std::uint64_t*); +int APS5_VABI sceKernelConvertUtcToLocaltime(std::int64_t, std::int64_t*, KernelTimesec*, std::int32_t*); } static constexpr int SCE_OK = 0; @@ -179,6 +179,11 @@ static void CalendarConversionsInitializeTheirState() { KernelTimesec state; std::uint64_t after; }; + struct GuardedDst { + std::int32_t seconds; + std::uint32_t after; + }; + static_assert(sizeof(GuardedDst) == 8); for (const auto seconds : std::array{-86400, 0, 1767225600, 2147483648}) { GuardedState local{sentinel, {-1, 0xa5a5a5a5u, 0xa5a5a5a5u}, sentinel}; std::int64_t utc = -1; @@ -189,9 +194,10 @@ static void CalendarConversionsInitializeTheirState() { Require(local.before == sentinel && local.after == sentinel); GuardedState universal{sentinel, {-1, 0xa5a5a5a5u, 0xa5a5a5a5u}, sentinel}; std::int64_t converted = -1; - std::uint64_t universalDst = sentinel; - Require(sceKernelConvertUtcToLocaltime(utc, &converted, &universal.state, &universalDst) == SCE_OK); - Require(converted == seconds && universalDst == 0); + GuardedDst universalDst{-1, 0xa5a5a5a5u}; + Require(sceKernelConvertUtcToLocaltime(utc, &converted, &universal.state, &universalDst.seconds) == SCE_OK); + Require(converted == seconds && universalDst.seconds == 0); + Require(universalDst.after == 0xa5a5a5a5u); Require(universal.state.t == seconds && universal.state.west_sec == 0 && universal.state.dst_sec == 0); Require(universal.before == sentinel && universal.after == sentinel); } diff --git a/core/libs/tests/GuestGetlogin.cpp b/core/libs/tests/GuestGetlogin.cpp new file mode 100644 index 000000000..b006a283f --- /dev/null +++ b/core/libs/tests/GuestGetlogin.cpp @@ -0,0 +1,81 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#else +#include +#include +#endif + +extern "C" int APS5_VABI getlogin_r_nid_postfix(char*, int); +extern "C" int* APS5_VABI __error_nid_postfix(); + +static void Check(bool condition, int line) { + if (!condition) { + std::fprintf(stderr, "getlogin_r check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static std::string HostLogin() { +#ifdef _WIN32 + wchar_t name[UNLEN + 1]; + DWORD count = UNLEN + 1; + Require(GetUserNameW(name, &count) != 0); + std::array output{}; + Require(WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, name, -1, + output.data(), static_cast(output.size()), nullptr, nullptr) > 0); + return output.data(); +#else + const passwd* account = ::getpwuid(::geteuid()); + Require(account != nullptr && account->pw_name != nullptr && account->pw_name[0] != '\0'); + return account->pw_name; +#endif +} + +static void RequireLogin(const std::string& expected) { + std::vector output(expected.size() + 3, '\x5A'); + *__error_nid_postfix() = 12345; + Require(getlogin_r_nid_postfix(output.data() + 1, static_cast(expected.size() + 1)) == 0); + Require(*__error_nid_postfix() == 12345); + Require(output.front() == '\x5A' && output.back() == '\x5A'); + Require(std::memcmp(output.data() + 1, expected.c_str(), expected.size() + 1) == 0); + output.assign(output.size(), '\x5A'); + const auto saved = output; + Require(getlogin_r_nid_postfix(output.data() + 1, static_cast(expected.size())) == 34); + Require(*__error_nid_postfix() == 12345 && output == saved); +} + +int main() { + std::array output; + output.fill('\x5A'); + const auto untouched = output; + for (int size : {0, -1, std::numeric_limits::min()}) { + *__error_nid_postfix() = 12345; + Require(getlogin_r_nid_postfix(output.data(), size) == 34); + Require(*__error_nid_postfix() == 12345 && output == untouched); + } + Require(getlogin_r_nid_postfix(nullptr, 1) == 14); + Require(*__error_nid_postfix() == 12345); + const std::string expected = HostLogin(); + *__error_nid_postfix() = 12345; + Require(getlogin_r_nid_postfix(output.data(), static_cast(output.size())) == 0); + Require(*__error_nid_postfix() == 12345); + Require(std::strcmp(output.data(), expected.c_str()) == 0); + RequireLogin(expected); + std::array readers; + for (auto& reader : readers) reader = std::thread([&] { + for (int i = 0; i != 20; ++i) RequireLogin(expected); + }); + for (auto& reader : readers) reader.join(); +} diff --git a/core/libs/tests/GuestGetloginNoSession.cpp b/core/libs/tests/GuestGetloginNoSession.cpp new file mode 100644 index 000000000..c9f29f551 --- /dev/null +++ b/core/libs/tests/GuestGetloginNoSession.cpp @@ -0,0 +1,39 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI getlogin_r_nid_postfix(char*, int); + +static int loginError = ENXIO; + +extern "C" int getlogin_r(char* output, std::size_t size) { + if (loginError != 0) return loginError; + constexpr char sessionName[] = "anyps5-other-login-session"; + if (size < sizeof(sessionName)) return ERANGE; + std::memcpy(output, sessionName, sizeof(sessionName)); + return 0; +} + +int main() { + const passwd* account = ::getpwuid(::geteuid()); + if (account == nullptr || account->pw_name == nullptr || account->pw_name[0] == '\0') + std::abort(); + const std::string expected(account->pw_name); + for (int error : {ENXIO, ENOENT, 0}) { + loginError = error; + std::array output{}; + errno = 12345; + const int result = getlogin_r_nid_postfix(output.data(), static_cast(output.size())); + if (result != 0 || errno != 12345 || output.data() != expected) { + std::fprintf(stderr, "getlogin_r changed with host login-session result %d: guest result %d\n", + error, result); + return 1; + } + } +} diff --git a/core/libs/tests/GuestGetrlimit.cpp b/core/libs/tests/GuestGetrlimit.cpp index 598ae8ed9..73b23df87 100644 --- a/core/libs/tests/GuestGetrlimit.cpp +++ b/core/libs/tests/GuestGetrlimit.cpp @@ -17,9 +17,21 @@ extern "C" int* APS5_VABI __error_nid_postfix(); static constexpr int GUEST_EFAULT = 14; static constexpr int GUEST_EINVAL = 22; +static constexpr int GUEST_RLIMIT_CPU = 0; +static constexpr int GUEST_RLIMIT_FSIZE = 1; static constexpr int GUEST_RLIMIT_DATA = 2; +static constexpr int GUEST_RLIMIT_STACK = 3; +static constexpr int GUEST_RLIMIT_CORE = 4; +static constexpr int GUEST_RLIMIT_RSS = 5; +static constexpr int GUEST_RLIMIT_MEMLOCK = 6; +static constexpr int GUEST_RLIMIT_NPROC = 7; static constexpr int GUEST_RLIMIT_NOFILE = 8; +static constexpr int GUEST_RLIMIT_SBSIZE = 9; static constexpr int GUEST_RLIMIT_AS = 10; +static constexpr int GUEST_RLIMIT_NPTS = 11; +static constexpr int GUEST_RLIMIT_SWAP = 12; +static constexpr int GUEST_RLIMIT_KQUEUES = 13; +static constexpr int GUEST_RLIMIT_UMTXP = 14; static constexpr int GUEST_RLIM_NLIMITS = 15; static constexpr std::int64_t GUEST_RLIM_INFINITY = std::numeric_limits::max(); @@ -80,11 +92,35 @@ int main() { RequireHostLimit(GUEST_RLIMIT_DATA, RLIMIT_DATA); LoweredHostLimitIsReported(); #endif + for (int resource : { + GUEST_RLIMIT_CPU, + GUEST_RLIMIT_FSIZE, + GUEST_RLIMIT_CORE, + GUEST_RLIMIT_SBSIZE, + GUEST_RLIMIT_AS, + GUEST_RLIMIT_NPTS, + GUEST_RLIMIT_SWAP, + GUEST_RLIMIT_KQUEUES, + GUEST_RLIMIT_UMTXP, + }) { + const ResourceLimit guest = GuestLimit(resource); + Require(guest.rlim_cur == GUEST_RLIM_INFINITY); + Require(guest.rlim_max == GUEST_RLIM_INFINITY); + RequireError(resource, nullptr, GUEST_EFAULT); + } ResourceLimit limit{}; RequireError(GUEST_RLIM_NLIMITS, &limit, GUEST_EINVAL); RequireError(-1, &limit, GUEST_EINVAL); RequireError(GUEST_RLIM_NLIMITS, nullptr, GUEST_EINVAL); + RequireError(-1, nullptr, GUEST_EINVAL); RequireError(GUEST_RLIMIT_DATA, nullptr, GUEST_EFAULT); - RequireUnsupported(GUEST_RLIMIT_NOFILE); - RequireUnsupported(GUEST_RLIMIT_AS); + for (int resource : { + GUEST_RLIMIT_STACK, + GUEST_RLIMIT_RSS, + GUEST_RLIMIT_MEMLOCK, + GUEST_RLIMIT_NPROC, + GUEST_RLIMIT_NOFILE, + }) { + RequireUnsupported(resource); + } } diff --git a/core/libs/tests/GuestGetrusage.cpp b/core/libs/tests/GuestGetrusage.cpp index a549808cf..da998f3e4 100644 --- a/core/libs/tests/GuestGetrusage.cpp +++ b/core/libs/tests/GuestGetrusage.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include struct Timeval { @@ -28,6 +29,11 @@ static void SpinUntil(int who, std::int64_t micros) { } } int main() { + ResourceUsage children; + std::memset(&children, 0xff, sizeof(children)); + Require(getrusage_nid_postfix(-1, &children) == 0); + const ResourceUsage empty{}; + Require(std::memcmp(&children, &empty, sizeof(children)) == 0); SpinUntil(0, 400000); const std::int64_t mainThread = CpuMicros(1); Require(mainThread >= 200000); diff --git a/core/libs/tests/GuestHttp2.cpp b/core/libs/tests/GuestHttp2.cpp index e64d3053f..ffce322a3 100644 --- a/core/libs/tests/GuestHttp2.cpp +++ b/core/libs/tests/GuestHttp2.cpp @@ -8,6 +8,7 @@ int APS5_VABI sceHttp2Init(int, int, std::size_t, int); int APS5_VABI sceHttp2CreateTemplate(int, const char*, int, int); int APS5_VABI sceHttp2CreateRequestWithURL(int, const char*, const char*, std::uint64_t); int APS5_VABI sceHttp2CreateCookieBox(int); +int APS5_VABI sceHttp2DeleteCookieBox(int); int APS5_VABI sceHttp2SetCookieBox(int, int); int APS5_VABI sceHttp2CookieFlush(int); int APS5_VABI sceHttp2SetRequestNoContentLength(int); @@ -71,6 +72,11 @@ int main() { const int box = sceHttp2CreateCookieBox(context); const int other = sceHttp2CreateCookieBox(context); Require(box > 0 && other > 0 && box != other && box != context); + const int discarded = sceHttp2CreateCookieBox(context); + Require(discarded > 0 && discarded != box && discarded != other); + Require(sceHttp2DeleteCookieBox(discarded) == 0); + Require(sceHttp2DeleteCookieBox(discarded) == InvalidId); + Require(sceHttp2DeleteCookieBox(context) == InvalidId); const int tmpl = sceHttp2CreateTemplate(context, "agent", 2, 0); Require(tmpl > 0 && tmpl != box && tmpl != other); Require(sceHttp2SetResolveRetry(tmpl, 3) == 0); @@ -119,6 +125,7 @@ int main() { Require(sceHttp2Term(doomedContext) == 0); RequireSslSetters(doomedContext, InvalidId); RequireSslSetters(doomedContextBox, InvalidId); + Require(sceHttp2DeleteCookieBox(doomedContextBox) == InvalidId); RequireSslSetters(doomedContextTemplate, InvalidId); RequireSslSetters(doomedContextRequest, InvalidId); Require(sceHttp2Term(doomedContext) == InvalidId); @@ -153,6 +160,7 @@ int main() { Require(Throws([&] { sceHttp2WaitAsync(request, &result, nullptr, nullptr); })); Require(sceHttp2DeleteRequest(request) == 0); Require(sceKernelDeleteEqueue(eq) == 0); + Require(sceHttp2DeleteCookieBox(box) == 0 && sceHttp2DeleteCookieBox(other) == 0); Require(sceHttp2Term(context) == 0); KernelEqueue templateEqueue = 0; diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 6b7309123..eb29e781e 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -57,6 +57,9 @@ Iterator* APS5_VABI _ZNK3sce4Json6Object3endEv(Iterator*, const Object*); void APS5_VABI _ZN3sce4Json6Object8iteratorD1Ev(Iterator*); Iterator* APS5_VABI _ZN3sce4Json6Object8iteratorppEv(Iterator*); Pair* APS5_VABI _ZNK3sce4Json6Object8iteratordeEv(const Iterator*); +Pair* APS5_VABI _ZNK3sce4Json6Object8iteratorptEv(const Iterator*); +Iterator* APS5_VABI _ZNK3sce4Json6Object4findERKNS0_6StringE(Iterator*, const Object*, const String*); +int APS5_VABI _ZNK3sce4Json6String7compareEPKc(const String*, const char*); bool APS5_VABI _ZNK3sce4Json6Object8iteratorneERKS2_(const Iterator*, const Iterator*); void APS5_VABI _ZN3sce4Json5ValueC1Ev(Value*); void APS5_VABI _ZN3sce4Json5ValueC1Eb(Value*, bool); @@ -92,6 +95,9 @@ void APS5_VABI _ZN3sce4Json14InitParameter2C2Ev(void*); void APS5_VABI _ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv(void*, void*, void*); void APS5_VABI _ZN3sce4Json14InitParameter217setFileBufferSizeEm(void*, std::size_t); int APS5_VABI _ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(void*, const void*); +void APS5_VABI _ZN3sce4Json18InitParameterRtti216setAllocatorRttiEPNS0_14AllocParamRttiEPv(void*, void*, void*); +void APS5_VABI _ZN3sce4Json14InitParameter225setSpecialFloatFormatTypeENS0_22SpecialFloatFormatTypeE(void*, std::int32_t); +void APS5_VABI _ZN3sce4Json17InitParameterRttiC1EPNS0_14AllocParamRttiEPvm(void*, void*, void*, std::size_t); } static void Check(bool value, int line) { @@ -295,6 +301,56 @@ static void ObjectsAndArrays() { _ZN3sce4Json5ArrayD1Ev(&array); } +static void ObjectFind() { + Object object{}; + _ZN3sce4Json6ObjectC1Ev(&object); + String alpha{}, beta{}, gamma{}; + _ZN3sce4Json6StringC1EPKc(&alpha, "alpha"); + _ZN3sce4Json6StringC1EPKc(&beta, "beta"); + _ZN3sce4Json6StringC1EPKc(&gamma, "gamma"); + Iterator found{}, end{}; + _ZNK3sce4Json6Object3endEv(&end, &object); + _ZNK3sce4Json6Object4findERKNS0_6StringE(&found, &object, &alpha); + Require(!_ZNK3sce4Json6Object8iteratorneERKS2_(&found, &end)); + _ZN3sce4Json6Object8iteratorD1Ev(&found); + _ZN3sce4Json6Object8iteratorD1Ev(&end); + + Require(_ZN3sce4Json5Value3setEl(_ZN3sce4Json6ObjectixERKNS0_6StringE(&object, &alpha), 1) == 0); + Value* second = _ZN3sce4Json6ObjectixERKNS0_6StringE(&object, &beta); + Require(_ZN3sce4Json5Value3setEPKc(second, "b") == 0); + _ZNK3sce4Json6Object3endEv(&end, &object); + Iterator* returned = _ZNK3sce4Json6Object4findERKNS0_6StringE(&found, &object, &beta); + Require(returned == &found); + Require(_ZNK3sce4Json6Object8iteratorneERKS2_(&found, &end)); + const Pair* pair = _ZNK3sce4Json6Object8iteratorptEv(&found); + Require(pair == _ZNK3sce4Json6Object8iteratordeEv(&found)); + Require(&pair->value == second); + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&pair->key), "beta") == 0); + _ZN3sce4Json6Object8iteratorppEv(&found); + Require(!_ZNK3sce4Json6Object8iteratorneERKS2_(&found, &end)); + _ZNK3sce4Json6Object4findERKNS0_6StringE(&found, &object, &gamma); + Require(!_ZNK3sce4Json6Object8iteratorneERKS2_(&found, &end)); + Require(_ZNK3sce4Json6Object4sizeEv(&object) == 2); + _ZN3sce4Json6Object8iteratorD1Ev(&found); + _ZN3sce4Json6Object8iteratorD1Ev(&end); + + Require(_ZNK3sce4Json6String7compareEPKc(&beta, "beta") == 0); + Require(_ZNK3sce4Json6String7compareEPKc(&beta, "betb") < 0); + Require(_ZNK3sce4Json6String7compareEPKc(&beta, "bet") > 0); + Require(_ZNK3sce4Json6String7compareEPKc(&beta, "alpha") > 0); + bool threw = false; + try { + static_cast(_ZNK3sce4Json6String7compareEPKc(&beta, nullptr)); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); + _ZN3sce4Json6StringD1Ev(&alpha); + _ZN3sce4Json6StringD1Ev(&beta); + _ZN3sce4Json6StringD1Ev(&gamma); + _ZN3sce4Json6ObjectD1Ev(&object); +} + static void NullAccess(SetNullAccessCallback setCallback) { callbackCalls = 0; lastRequested = -1; @@ -422,6 +478,69 @@ static void InitParameter2BaseConstructor() { Require(_ZN3sce4Json11InitializerD1Ev(initializer) == 0); } +struct StoredInitParameter2 { + void* allocator; + void* userData; + std::size_t fileBufferSize; + std::int32_t specialFloatFormatType; + std::uint32_t reserved[3]; +}; +static_assert(sizeof(StoredInitParameter2) == 40); + +static StoredInitParameter2 Stored(const std::uint8_t* parameter) { + StoredInitParameter2 stored{}; + std::memcpy(&stored, parameter, sizeof(stored)); + return stored; +} + +static bool Untouched(const std::uint8_t* parameter, std::size_t from) { + for (std::size_t i = from; i < 40; ++i) + if (parameter[i] != 0xff) return false; + return true; +} + +static void InitParameters() { + using Constructor = void (APS5_VABI*)(void*); + for (Constructor construct : {_ZN3sce4Json14InitParameter2C1Ev, _ZN3sce4Json14InitParameter2C2Ev}) { + alignas(16) std::uint8_t parameter[40]; + std::memset(parameter, 0xff, sizeof(parameter)); + construct(parameter); + StoredInitParameter2 stored = Stored(parameter); + Require(stored.allocator == nullptr && stored.userData == nullptr && stored.fileBufferSize == 0); + Require(stored.specialFloatFormatType == 0); + Require(stored.reserved[0] == 0 && stored.reserved[1] == 0 && stored.reserved[2] == 0); + + int allocParam = 0; + int userData = 0; + _ZN3sce4Json14InitParameter217setFileBufferSizeEm(parameter, 8192); + _ZN3sce4Json14InitParameter225setSpecialFloatFormatTypeENS0_22SpecialFloatFormatTypeE(parameter, 2); + _ZN3sce4Json18InitParameterRtti216setAllocatorRttiEPNS0_14AllocParamRttiEPv(parameter, &allocParam, &userData); + stored = Stored(parameter); + Require(stored.allocator == &allocParam && stored.userData == &userData && stored.fileBufferSize == 8192); + Require(stored.specialFloatFormatType == 2); + Require(stored.reserved[0] == 0 && stored.reserved[1] == 0 && stored.reserved[2] == 0); + + _ZN3sce4Json14InitParameter225setSpecialFloatFormatTypeENS0_22SpecialFloatFormatTypeE(parameter, 0); + Require(Stored(parameter).specialFloatFormatType == 0 && Stored(parameter).fileBufferSize == 8192); + + alignas(16) std::uint8_t initializer[16]{}; + Require(_ZN3sce4Json11InitializerC1Ev(initializer) == 0); + Require(_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(initializer, parameter) == 0); + Require(_ZN3sce4Json11Initializer9terminateEv(initializer) == 0); + Require(_ZN3sce4Json11InitializerD1Ev(initializer) == 0); + } + + alignas(16) std::uint8_t rttiParameter[40]; + std::memset(rttiParameter, 0xff, sizeof(rttiParameter)); + int allocParam = 0; + int userData = 0; + _ZN3sce4Json17InitParameterRttiC1EPNS0_14AllocParamRttiEPvm(rttiParameter, &allocParam, &userData, 4096); + void* rttiStored[3]{}; + std::memcpy(rttiStored, rttiParameter, sizeof(rttiStored)); + Require(rttiStored[0] == &allocParam && rttiStored[1] == &userData && reinterpret_cast(rttiStored[2]) == 4096); + Require(Untouched(rttiParameter, sizeof(rttiStored))); +} + static void ValueClear() { Value value{}; _ZN3sce4Json5ValueC1Ev(&value); @@ -445,6 +564,7 @@ static void ValueClear() { _ZN3sce4Json6StringC1EPKc(&key, "prop"); Value* item = _ZN3sce4Json6ObjectixERKNS0_6StringE(&obj, &key); _ZN3sce4Json5Value3setEl(item, 999); + _ZN3sce4Json5ValueD1Ev(&value); _ZN3sce4Json5ValueC1ERKNS0_6ObjectE(&value, &obj); Require(_ZNK3sce4Json5Value7getTypeEv(&value) == TypeObject); _ZN3sce4Json5Value5clearEv(&value); @@ -494,10 +614,12 @@ int main() { ParseAndRoundTrip(); NestingDepth(); ObjectsAndArrays(); + ObjectFind(); NullAccess(_ZN3sce4Json11Initializer27setGlobalNullAccessCallBackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_); NullAccess(_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_); ValueAccess(); InitParameter2BaseConstructor(); + InitParameters(); ValueClear(); StreamParse(); } diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp index 5c14f7fca..6a76e356e 100644 --- a/core/libs/tests/GuestKernelErrors.cpp +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -90,13 +90,7 @@ int main() { Require(scePthreadMutexattrSettype(&attr, 5) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadMutexattrSetprotocol(&attr, PRIO_NONE) == SCE_OK); Require(scePthreadMutexattrSetprotocol(&attr, PRIO_INHERIT) == SCE_OK); - bool protectionRejected = false; - try { - scePthreadMutexattrSetprotocol(&attr, PRIO_PROTECT); - } catch (const std::invalid_argument&) { - protectionRejected = true; - } - Require(protectionRejected); + Require(scePthreadMutexattrSetprotocol(&attr, PRIO_PROTECT) == SCE_OK); PthreadMutex mutex = nullptr; Require(scePthreadMutexInit(&mutex, &attr, nullptr) == SCE_OK); Require(scePthreadMutexattrDestroy(&attr) == SCE_OK); diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 0c80bb893..c2c6030b3 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -9,19 +9,73 @@ extern "C" { int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI close_nid_postfix(int); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +int APS5_VABI ioctl_nid_postfix(int, std::uint64_t, void*); +std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); +std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); +int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceKernelFcntl(int, int, ...); int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); } static void Require(bool value) { if (!value) std::abort(); } +static constexpr int DuplicateCommand = 0; +static constexpr int GetDescriptorFlags = 1; +static constexpr int SetDescriptorFlags = 2; static constexpr int GetFlags = 3; static constexpr int SetFlags = 4; +static constexpr int GetLock = 11; +static constexpr int SetLock = 12; +static constexpr int ReadAhead = 15; +static constexpr int DarwinReadAhead = 16; +static constexpr int DuplicateCloseOnExec = 17; +static constexpr int ReadOnly = 0; +static constexpr int WriteOnly = 1; static constexpr int ReadWrite = 2; static constexpr int NonBlocking = 4; +static constexpr int Append = 8; +static constexpr int ReadAheadFlag = 0x200; +static constexpr int CreateReadWrite = 0x202; +static constexpr int CreateWriteAppend = 0x209; +static constexpr int GuestEbadf = 9; +static constexpr int GuestEfault = 14; +static constexpr int GuestEinval = 22; +static constexpr int GuestEnotty = 25; +static constexpr int GuestEagain = 35; +static constexpr std::uint64_t NonblockingRequest = 0x8004667e; +static constexpr std::uint64_t ReadableRequest = 0x4004667f; +static constexpr std::uint64_t WindowSizeRequest = 0x40087468; +static constexpr std::uint64_t AsyncRequest = 0x8004667d; static constexpr int SceEbadf = static_cast(0x80020009u); static constexpr int SceEinval = static_cast(0x80020016u); static constexpr int SceEnotsup = static_cast(0x8002002du); -int main() { + +struct GuestLock { + std::int64_t start; + std::int64_t length; + std::int32_t pid; + std::int16_t type; + std::int16_t whence; + std::int32_t system; +}; + +static void RequireFailure(int result, int error) { + Require(result == -1); + Require(*__error_nid_postfix() == error); +} + +template static bool ThrowsWith(TCall call, const std::string& prefix) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::string(error.what()).rfind(prefix, 0) == 0; + } + return false; +} + +static void SocketsKeepTheirPath() { const int socket = socket_nid_postfix(2, 2, 0); Require(socket >= 0); Require(sceKernelFcntl(socket, GetFlags) == ReadWrite); @@ -33,21 +87,156 @@ int main() { Require(sceKernelFcntl(socket, GetFlags) == ReadWrite); Require(sceKernelFcntl(socket, 5) == SceEinval); Require(sceKernelFcntl(socket + 4096, GetFlags) == SceEbadf); + int queued = -1; + Require(ioctl_nid_postfix(socket, ReadableRequest, &queued) == 0 && queued == 0); Require(close_nid_postfix(socket) == 0); Require(sceKernelFcntl(socket, GetFlags) == SceEbadf); Require(sceKernelFcntl(-1, GetFlags) == SceEbadf); Require(sceKernelFcntl(12345, GetFlags) == SceEbadf); - const int file = sceKernelOpen("kernel_fcntl_probe.bin", 0x202, 0644); +} + +static void FileFlagsRoundTrip() { + const int file = sceKernelOpen("kernel_fcntl_probe.bin", CreateReadWrite, 0644); Require(file >= 0); - bool rejected = false; - try { - sceKernelFcntl(file, GetFlags); - } catch (const std::runtime_error& error) { - rejected = std::string(error.what()).rfind("sceKernelFcntl: file descriptors are not implemented", 0) == 0; - } - Require(rejected); - Require(sceKernelClose(file) == 0); + Require(sceKernelFcntl(file, GetFlags) == ReadWrite); + Require(fcntl_nid_postfix(file, SetFlags, ReadWrite | NonBlocking | 0x8000) == 0); + Require(fcntl_nid_postfix(file, GetFlags) == (ReadWrite | NonBlocking)); + Require(fcntl_nid_postfix(file, SetFlags, 0) == 0); + Require(fcntl_nid_postfix(file, GetFlags) == ReadWrite); + Require(fcntl_nid_postfix(file, GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(file, SetDescriptorFlags, 1) == 0); + Require(fcntl_nid_postfix(file, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(file, SetDescriptorFlags, 0) == 0); + Require(sceKernelFcntl(file, GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(file, DarwinReadAhead, 1) == 0); + Require(fcntl_nid_postfix(file, GetFlags) == (ReadWrite | ReadAheadFlag)); + Require(fcntl_nid_postfix(file, ReadAhead, -1) == 0); + Require(fcntl_nid_postfix(file, GetFlags) == ReadWrite); + Require(sceKernelFcntl(file, ReadAhead, 4096) == 0); + Require(fcntl_nid_postfix(file, GetFlags) == (ReadWrite | ReadAheadFlag)); + Require(sceKernelFcntl(file, ReadAhead, -1) == 0); + Require(sceKernelFcntl(file, GetFlags) == ReadWrite); + Require(fcntl_nid_postfix(file, DarwinReadAhead, 1) == 0); + + const int duplicate = fcntl_nid_postfix(file, DuplicateCommand, 20); + Require(duplicate >= 20); + Require(fcntl_nid_postfix(duplicate, GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(file, SetFlags, NonBlocking) == 0); + Require(fcntl_nid_postfix(duplicate, GetFlags) == (ReadWrite | NonBlocking)); + const int closing = fcntl_nid_postfix(file, DuplicateCloseOnExec, duplicate); + Require(closing > duplicate); + Require(fcntl_nid_postfix(closing, GetDescriptorFlags) == 1); + Require(fcntl_nid_postfix(file, GetDescriptorFlags) == 0); + + Require(write_nid_postfix(file, "abcde", 5) == 5); + Require(sceKernelLseek(file, 2, 0) == 2); + int readable = -1; + Require(ioctl_nid_postfix(file, ReadableRequest, &readable) == 0 && readable == 3); + char size[8] = {}; + RequireFailure(ioctl_nid_postfix(file, WindowSizeRequest, size), GuestEnotty); + RequireFailure(ioctl_nid_postfix(file, NonblockingRequest, nullptr), GuestEfault); + Require(ThrowsWith([file] { int on = 1; ioctl_nid_postfix(file, AsyncRequest, &on); }, "ioctl: request 0x8004667D on a file is not implemented")); + + RequireFailure(fcntl_nid_postfix(file, DuplicateCommand, -1), GuestEinval); + Require(sceKernelFcntl(file, DuplicateCommand, -1) == SceEinval); + RequireFailure(fcntl_nid_postfix(file, 99), GuestEinval); + RequireFailure(fcntl_nid_postfix(file, GetLock, nullptr), GuestEfault); +#ifdef _WIN32 + GuestLock lock{0, 0, 0, 3, 0, 0}; + Require(ThrowsWith([file, &lock] { fcntl_nid_postfix(file, SetLock, &lock); }, "fcntl: record locks are not implemented on Windows")); +#else + GuestLock lock{0, 0, 0, 3, 0, 0}; + Require(fcntl_nid_postfix(file, SetLock, &lock) == 0); + GuestLock probe{0, 0, 0, 1, 0, 0}; + Require(fcntl_nid_postfix(file, GetLock, &probe) == 0 && probe.type == 2); + lock.type = 2; + Require(fcntl_nid_postfix(file, SetLock, &lock) == 0); + lock.type = 7; + RequireFailure(fcntl_nid_postfix(file, SetLock, &lock), GuestEinval); +#endif + + for (const int descriptor : {closing, duplicate, file}) Require(close_nid_postfix(descriptor) == 0); + RequireFailure(fcntl_nid_postfix(file, GetDescriptorFlags), GuestEbadf); + RequireFailure(fcntl_nid_postfix(file, DuplicateCommand, 0), GuestEbadf); + RequireFailure(ioctl_nid_postfix(file, ReadableRequest, &readable), GuestEbadf); Require(sceKernelFcntl(file, GetFlags) == SceEbadf); + + const int appending = sceKernelOpen("kernel_fcntl_probe.bin", CreateWriteAppend, 0644); + Require(appending >= 0); + Require(fcntl_nid_postfix(appending, GetFlags) == (WriteOnly | Append)); + Require(fcntl_nid_postfix(appending, SetFlags, Append | NonBlocking) == 0); + Require(fcntl_nid_postfix(appending, GetFlags) == (WriteOnly | Append | NonBlocking)); +#ifdef _WIN32 + Require(ThrowsWith([appending] { fcntl_nid_postfix(appending, SetFlags, 0); }, "fcntl: changing O_APPEND on a file is not implemented")); +#else + Require(fcntl_nid_postfix(appending, SetFlags, 0) == 0); + Require(fcntl_nid_postfix(appending, GetFlags) == WriteOnly); +#endif + Require(ThrowsWith([appending] { fcntl_nid_postfix(appending, SetFlags, Append | 0x80); }, "fcntl: changing O_FSYNC on a file is not implemented")); + Require(sceKernelClose(appending) == 0); std::remove("kernel_fcntl_probe.bin"); +} + +static void PipeFlagsRoundTrip() { + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == ReadOnly); + Require(fcntl_nid_postfix(pipe[1], GetFlags) == WriteOnly); + Require(fcntl_nid_postfix(pipe[1], SetDescriptorFlags, 1) == 0); + Require(fcntl_nid_postfix(pipe[1], GetDescriptorFlags) == 1); + Require(fcntl_nid_postfix(pipe[0], GetDescriptorFlags) == 0); + + Require(fcntl_nid_postfix(pipe[0], SetFlags, fcntl_nid_postfix(pipe[0], GetFlags) | NonBlocking) == 0); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == (ReadOnly | NonBlocking)); + char buffer[8] = {}; + RequireFailure(static_cast(read_nid_postfix(pipe[0], buffer, sizeof(buffer))), GuestEagain); + Require(write_nid_postfix(pipe[1], "abc", 3) == 3); + int readable = -1; + Require(ioctl_nid_postfix(pipe[0], ReadableRequest, &readable) == 0 && readable == 3); + Require(ioctl_nid_postfix(pipe[1], ReadableRequest, &readable) == 0 && readable == 0); + Require(read_nid_postfix(pipe[0], buffer, sizeof(buffer)) == 3); + RequireFailure(static_cast(read_nid_postfix(pipe[0], buffer, sizeof(buffer))), GuestEagain); + + int off = 0; + Require(ioctl_nid_postfix(pipe[0], NonblockingRequest, &off) == 0); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == ReadOnly); + int on = 1; + Require(ioctl_nid_postfix(pipe[0], NonblockingRequest, &on) == 0); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == (ReadOnly | NonBlocking)); + + Require(fcntl_nid_postfix(pipe[1], SetFlags, NonBlocking) == 0); + char block[1024] = {}; + std::int64_t written = 0; + std::int64_t result = 0; + while (written < (1 << 22) && (result = write_nid_postfix(pipe[1], block, sizeof(block))) > 0) written += result; + Require(written > 0); + RequireFailure(static_cast(result), GuestEagain); + + RequireFailure(fcntl_nid_postfix(pipe[0], DarwinReadAhead, 1), GuestEbadf); + GuestLock lock{0, 0, 0, 3, 0, 0}; + RequireFailure(fcntl_nid_postfix(pipe[1], SetLock, &lock), GuestEbadf); + char size[8] = {}; + RequireFailure(ioctl_nid_postfix(pipe[0], WindowSizeRequest, size), GuestEnotty); + Require(ThrowsWith([&pipe] { fcntl_nid_postfix(pipe[0], SetFlags, NonBlocking | 0x40); }, "fcntl: O_ASYNC on a pipe is not implemented")); + for (const int descriptor : pipe) Require(close_nid_postfix(descriptor) == 0); +} + +static void StandardInputFlags() { + const int flags = fcntl_nid_postfix(0, GetFlags); + Require(flags >= 0); + Require((flags & 3) != WriteOnly); + Require(fcntl_nid_postfix(0, SetFlags, fcntl_nid_postfix(0, GetFlags) & ~NonBlocking) == 0); + Require(fcntl_nid_postfix(0, GetFlags) == (flags & ~NonBlocking)); + Require(fcntl_nid_postfix(0, SetDescriptorFlags, 1) == 0); + Require(fcntl_nid_postfix(0, GetDescriptorFlags) == 1); + Require(fcntl_nid_postfix(0, SetDescriptorFlags, 0) == 0); + Require(fcntl_nid_postfix(0, GetDescriptorFlags) == 0); +} + +int main() { + SocketsKeepTheirPath(); + FileFlagsRoundTrip(); + PipeFlagsRoundTrip(); + StandardInputFlags(); return 0; } diff --git a/core/libs/tests/GuestKernelStackProtect.cpp b/core/libs/tests/GuestKernelStackProtect.cpp index 861844474..66aed4047 100644 --- a/core/libs/tests/GuestKernelStackProtect.cpp +++ b/core/libs/tests/GuestKernelStackProtect.cpp @@ -9,6 +9,7 @@ #include #else #include +#include #endif extern "C" { @@ -21,6 +22,7 @@ int APS5_VABI mprotect_nid_postfix(void* address, std::size_t length, int protec static constexpr int SCE_OK = 0; static constexpr std::uintptr_t Page = 0x4000; static constexpr std::size_t GuardBytes = 0x10000; +static constexpr std::size_t HostBytes = 0x40000; static void Require(bool value) { if (!value) std::abort(); } @@ -54,6 +56,23 @@ static PageState QueryPage(std::uintptr_t address) { #endif } +static void* MapHostPages() { +#ifdef _WIN32 + return VirtualAlloc(nullptr, HostBytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); +#else + void* mapped = mmap(nullptr, HostBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + return mapped == MAP_FAILED ? nullptr : mapped; +#endif +} + +static void UnmapHostPages(void* pages) { +#ifdef _WIN32 + Require(VirtualFree(pages, 0, MEM_RELEASE) != 0); +#else + Require(munmap(pages, HostBytes) == 0); +#endif +} + static void* APS5_VABI Worker(void* arg) { auto& passed = *static_cast*>(arg); int local = 0; @@ -82,6 +101,18 @@ static void* APS5_VABI Worker(void* arg) { static_cast(start)[0] = 3; const auto outside = reinterpret_cast(start) & ~(Page - 1); if (outside >= Page) Require(mprotect_nid_postfix(reinterpret_cast(outside - Page), 2 * Page, 3) == -1); + if (outside >= 2 * Page) Require(mprotect_nid_postfix(reinterpret_cast(outside - 2 * Page), Page, 3) == -1); + const auto stackBytes = reinterpret_cast(end) - reinterpret_cast(start); + Require(mprotect_nid_postfix(start, stackBytes, 0xc2) == 0); + Require(QueryPage(low).writable); + static_cast(start)[0] = 4; + Require(mprotect_nid_postfix(start, stackBytes, 3) == 0); + void* host = MapHostPages(); + Require(host != nullptr); + Require(mprotect_nid_postfix(host, HostBytes, 1) == -1); + Require(QueryPage(reinterpret_cast(host)).writable); + static_cast(host)[HostBytes - 1] = 5; + UnmapHostPages(host); passed.store(true); return nullptr; } diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 2603b7db9..8483b1be8 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -8,9 +8,13 @@ #include #include #include +#include #include #ifdef _WIN32 #include +#ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE +#define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x2 +#endif #endif #ifdef __linux__ #include @@ -31,6 +35,8 @@ int APS5_VABI sceKernelClose(int); int APS5_VABI setsid_nid_postfix(void); int APS5_VABI link_nid_postfix(const char*, const char*); int APS5_VABI symlink_nid_postfix(const char*, const char*); +int APS5_VABI openat_nid_postfix(int, const char*, int, ...); +std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t); int* APS5_VABI __error_nid_postfix(); } @@ -159,6 +165,33 @@ int main() { Require(symlink_nid_postfix("missing.bin", dangling.c_str()) == 0); Require((GetFileAttributesW(std::filesystem::path(dangling).wstring().c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) != 0); Require(symlink_nid_postfix("missing.bin", dangling.c_str()) == -1 && Error() == 17); + Require(readlink_nid_postfix(symbolic.c_str(), buffer, sizeof(buffer)) == 12 && std::memcmp(buffer, "original.bin", 12) == 0); + Require(readlink_nid_postfix(dangling.c_str(), buffer, sizeof(buffer)) == 11 && std::memcmp(buffer, "missing.bin", 11) == 0); + const auto nested = (root / "nested").string(); + Require(symlink_nid_postfix("sub/target.txt", nested.c_str()) == 0); + std::memset(buffer, '#', sizeof(buffer)); + Require(readlink_nid_postfix(nested.c_str(), buffer, sizeof(buffer)) == 14); + Require(std::memcmp(buffer, "sub/target.txt", 14) == 0 && buffer[14] == '#'); + Require(readlink_nid_postfix(nested.c_str(), buffer, 3) == 3 && std::memcmp(buffer, "sub", 3) == 0); + Require(readlink_nid_postfix(nested.c_str(), nullptr, 0) == 0); + Require(readlink_nid_postfix(nested.c_str(), nullptr, sizeof(buffer)) == -1 && Error() == 14); + const auto accented = (root / "accented").string(); + Require(symlink_nid_postfix("a\xc3\xa7\xc3\xa3o.bin", accented.c_str()) == 0); + Require(readlink_nid_postfix(accented.c_str(), buffer, sizeof(buffer)) == 10 && std::memcmp(buffer, "a\xc3\xa7\xc3\xa3o.bin", 10) == 0); + Require(std::filesystem::create_directory(root / "sub")); + const auto directoryLink = (root / "directory-link").string(); + Require(symlink_nid_postfix("sub", directoryLink.c_str()) == 0); + Require(readlink_nid_postfix(directoryLink.c_str(), buffer, sizeof(buffer)) == 3 && std::memcmp(buffer, "sub", 3) == 0); + const auto absolute = std::filesystem::absolute(root / "absolute"); + Require(CreateSymbolicLinkW(absolute.wstring().c_str(), std::filesystem::absolute(original).wstring().c_str(), + SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + bool refused = false; + try { + readlink_nid_postfix((root / "absolute").string().c_str(), buffer, sizeof(buffer)); + } catch (const std::runtime_error& failure) { + refused = std::string(failure.what()) == "readlink of a Windows reparse point other than a relative symbolic link not implemented"; + } + Require(refused); #else Require(std::filesystem::is_symlink(std::filesystem::symlink_status(symbolic))); Require(readlink_nid_postfix(symbolic.c_str(), buffer, sizeof(buffer)) == 12 && std::memcmp(buffer, "original.bin", 12) == 0); @@ -175,6 +208,30 @@ int main() { Require(symlink_nid_postfix("original.bin", (root / "missing" / "symbolic").string().c_str()) == -1 && Error() == 2); Require(symlink_nid_postfix(nullptr, symbolic.c_str()) == -1 && Error() == 14); Require(symlink_nid_postfix("original.bin", "") == -1 && Error() == 2); + const int base = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(base >= 0); + const int relative = openat_nid_postfix(base, "file.txt", SCE_KERNEL_O_RDONLY); + char byte = 0; + Require(relative >= 0 && sceKernelRead(relative, &byte, 1) == 1 && byte == 'x'); + Require(sceKernelClose(relative) == 0); + const int made = openat_nid_postfix(base, "made.bin", SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL, 0644); + Require(made >= 0 && std::filesystem::is_regular_file(root / "made.bin")); + Require(sceKernelClose(made) == 0); + Require(openat_nid_postfix(base, "made.bin", SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL, 0644) == -1 && Error() == 17); + Require(openat_nid_postfix(base, "missing.bin", SCE_KERNEL_O_RDONLY) == -1 && Error() == 2); + const auto guestFile = "/" + root.filename().string() + "/file.txt"; + const int absolute = openat_nid_postfix(-1, guestFile.c_str(), SCE_KERNEL_O_RDONLY); + Require(absolute >= 0 && sceKernelClose(absolute) == 0); + const int current = openat_nid_postfix(-100, file.c_str(), SCE_KERNEL_O_RDONLY); + Require(current >= 0 && sceKernelClose(current) == 0); + const int plain = sceKernelOpen(file.c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(plain >= 0); + Require(openat_nid_postfix(plain, "file.txt", SCE_KERNEL_O_RDONLY) == -1 && Error() == 20); + Require(sceKernelClose(plain) == 0); + Require(openat_nid_postfix(-1, "file.txt", SCE_KERNEL_O_RDONLY) == -1 && Error() == 9); + Require(openat_nid_postfix(base, nullptr, SCE_KERNEL_O_RDONLY) == -1 && Error() == 14); + Require(openat_nid_postfix(base, "", SCE_KERNEL_O_RDONLY) == -1 && Error() == 2); + Require(sceKernelClose(base) == 0); std::filesystem::remove_all(root); return 0; } diff --git a/core/libs/tests/GuestLibcStaticImports.cpp b/core/libs/tests/GuestLibcStaticImports.cpp new file mode 100644 index 000000000..a8068575d --- /dev/null +++ b/core/libs/tests/GuestLibcStaticImports.cpp @@ -0,0 +1,169 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +struct Xtime { + std::int64_t sec; + std::int64_t nsec; +}; + +extern "C" { +float APS5_VABI nearbyintf_nid_postfix(float x); +int APS5_VABI _Mtx_init_nid_postfix(void** handle, int type); +void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); +int APS5_VABI _Mtx_lock_nid_postfix(void** handle); +int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); +int APS5_VABI _Mtx_trylock_nid_postfix(void** handle); +int APS5_VABI _Thrd_sleep_nid_postfix(const Xtime* target, Xtime* remaining); +} + +namespace { + +void Require(bool value) { + if (!value) + std::abort(); +} + +template +void RequireThrows(TCall call) { + try { + call(); + } catch (const TException&) { + return; + } + std::abort(); +} + +constexpr int threadSuccess = 0; +constexpr int threadBusy = 3; +constexpr int mutexTryable = 0x02; +constexpr int mutexRecursive = 0x100; +constexpr std::int64_t nanosPerSecond = 1000000000LL; + +void TestNearbyintf() { + const float half = nearbyintf_nid_postfix(0.5f); + Require(half == 0.0f); + Require(!std::signbit(half)); + + const float negHalf = nearbyintf_nid_postfix(-0.5f); + Require(negHalf == 0.0f); + Require(std::signbit(negHalf)); + + Require(nearbyintf_nid_postfix(1.5f) == 2.0f); + Require(nearbyintf_nid_postfix(-1.5f) == -2.0f); + + Require(nearbyintf_nid_postfix(2.5f) == 2.0f); + Require(nearbyintf_nid_postfix(-2.5f) == -2.0f); + + const float negZero = nearbyintf_nid_postfix(-0.0f); + Require(negZero == 0.0f); + Require(std::signbit(negZero)); + + const float inf = std::numeric_limits::infinity(); + const float posInf = nearbyintf_nid_postfix(inf); + Require(std::isinf(posInf) && posInf > 0.0f); + + const float negInf = nearbyintf_nid_postfix(-inf); + Require(std::isinf(negInf) && negInf < 0.0f); + + const float nan = std::numeric_limits::quiet_NaN(); + Require(std::isnan(nearbyintf_nid_postfix(nan))); +} + +void TestMtxTrylock() { + void* mutex = nullptr; + Require(_Mtx_init_nid_postfix(&mutex, mutexTryable) == threadSuccess && mutex != nullptr); + + Require(_Mtx_trylock_nid_postfix(&mutex) == threadSuccess); + Require(_Mtx_trylock_nid_postfix(&mutex) == threadBusy); + Require(_Mtx_unlock_nid_postfix(&mutex) == threadSuccess); + + std::atomic held{false}; + std::atomic release{false}; + std::thread other([&] { + Require(_Mtx_lock_nid_postfix(&mutex) == threadSuccess); + held.store(true, std::memory_order_release); + while (!release.load(std::memory_order_acquire)) + std::this_thread::yield(); + Require(_Mtx_unlock_nid_postfix(&mutex) == threadSuccess); + }); + + while (!held.load(std::memory_order_acquire)) + std::this_thread::yield(); + + Require(_Mtx_trylock_nid_postfix(&mutex) == threadBusy); + + release.store(true, std::memory_order_release); + other.join(); + + Require(_Mtx_trylock_nid_postfix(&mutex) == threadSuccess); + Require(_Mtx_unlock_nid_postfix(&mutex) == threadSuccess); + _Mtx_destroy_nid_postfix(&mutex); + + void* recMutex = nullptr; + Require(_Mtx_init_nid_postfix(&recMutex, mutexTryable | mutexRecursive) == threadSuccess && recMutex != nullptr); + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadSuccess); + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadSuccess); + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadSuccess); + + std::thread([&] { + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadBusy); + }).join(); + + Require(_Mtx_unlock_nid_postfix(&recMutex) == threadSuccess); + std::thread([&] { + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadBusy); + }).join(); + + Require(_Mtx_unlock_nid_postfix(&recMutex) == threadSuccess); + std::thread([&] { + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadBusy); + }).join(); + + Require(_Mtx_unlock_nid_postfix(&recMutex) == threadSuccess); + + std::thread([&] { + Require(_Mtx_trylock_nid_postfix(&recMutex) == threadSuccess); + Require(_Mtx_unlock_nid_postfix(&recMutex) == threadSuccess); + }).join(); + + _Mtx_destroy_nid_postfix(&recMutex); +} + +void TestThrdSleep() { + timespec before{}; + Require(clock_gettime(CLOCK_REALTIME, &before) == 0); + + const std::int64_t sleepNanos = 20000000LL; + const std::int64_t targetTotal = static_cast(before.tv_sec) * nanosPerSecond + before.tv_nsec + sleepNanos; + const Xtime target{targetTotal / nanosPerSecond, targetTotal % nanosPerSecond}; + + Require(_Thrd_sleep_nid_postfix(&target, nullptr) == 0); + + timespec after{}; + Require(clock_gettime(CLOCK_REALTIME, &after) == 0); + const std::int64_t afterTotal = static_cast(after.tv_sec) * nanosPerSecond + after.tv_nsec; + Require(afterTotal >= targetTotal); + + const Xtime past{before.tv_sec - 1, before.tv_nsec}; + Require(_Thrd_sleep_nid_postfix(&past, nullptr) == 0); + + RequireThrows([] { + _Thrd_sleep_nid_postfix(nullptr, nullptr); + }); +} + +} + +int main() { + TestNearbyintf(); + TestMtxTrylock(); + TestThrdSleep(); + return 0; +} diff --git a/core/libs/tests/GuestLocale.cpp b/core/libs/tests/GuestLocale.cpp index 632670181..93cf43ff1 100644 --- a/core/libs/tests/GuestLocale.cpp +++ b/core/libs/tests/GuestLocale.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/GuestLocale.hpp" #include +#include #include #include #include @@ -23,6 +24,8 @@ int APS5_VABI _Mbtowcx_nid_postfix(std::uint16_t* dst, const char* src, std::siz int APS5_VABI _Wctombx_nid_postfix(char* dst, std::uint16_t src, std::mbstate_t* st); void APS5_VABI _Locksyslock_nid_postfix(); void APS5_VABI _Unlocksyslock_nid_postfix(); +std::uint32_t APS5_VABI _Towctrans_nid_postfix(std::uint32_t wc, std::uint64_t desc); +int* APS5_VABI __error_nid_postfix(); } @@ -144,12 +147,35 @@ static void CheckStreamDestruction() { Require(object.stream.locale == &_ZSt21_sceLibcClassicLocale_nid_postfix); } +static void CheckWideCaseTranslation() { + const auto* lower = _Getptolower_nid_postfix(); + const auto* upper = _Getptoupper_nid_postfix(); + Require(_Towctrans_nid_postfix(u'A', 1) == u'a' && _Towctrans_nid_postfix(u'a', 2) == u'A'); + *__error_nid_postfix() = 0; + for (std::uint32_t value = 0; value < 256; ++value) { + Require(_Towctrans_nid_postfix(value, 1) == static_cast(lower[value])); + Require(_Towctrans_nid_postfix(value, 2) == static_cast(upper[value])); + } + const std::uint32_t uncased[] = {0x100u, 0x391u, 0x3b1u, 0xffffu, 0xffffffffu}; + for (const std::uint32_t value : uncased) { + Require(_Towctrans_nid_postfix(value, 1) == value && _Towctrans_nid_postfix(value, 2) == value); + } + Require(*__error_nid_postfix() == 0); + const std::uint64_t invalidDescriptors[] = {0, 3}; + for (const std::uint64_t descriptor : invalidDescriptors) { + *__error_nid_postfix() = 0; + Require(_Towctrans_nid_postfix(u'A', descriptor) == u'A' && _Towctrans_nid_postfix(u'7', descriptor) == u'7'); + Require(*__error_nid_postfix() == 22); + } +} + int main() { CheckGuestCalls(); CheckLocinfoAlignment(); CheckCharacterTables(); CheckCharacterConversions(); CheckStreamDestruction(); + CheckWideCaseTranslation(); _Locksyslock_nid_postfix(); _Locksyslock_nid_postfix(); _Unlocksyslock_nid_postfix(); diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 409a96b94..5a8e85f5f 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -11,6 +11,10 @@ #include extern "C" { double APS5_VABI atof_nid_postfix(const char*); +double APS5_VABI strtod_nid_postfix(const char*, char**); +double APS5_VABI wcstod_nid_postfix(const char16_t*, char16_t**); +float APS5_VABI wcstof_nid_postfix(const char16_t*, char16_t**); +long double APS5_VABI wcstold_nid_postfix(const char16_t*, char16_t**); float APS5_VABI strtof_nid_postfix(const char*, char**); long double APS5_VABI strtold_nid_postfix(const char*, char**); std::int64_t APS5_VABI strtol_nid_postfix(const char*, char**, int); @@ -31,13 +35,17 @@ int APS5_VABI abs_nid_postfix(int); double APS5_VABI trunc_nid_postfix(double); double APS5_VABI fma_nid_postfix(double, double, double); long double APS5_VABI acosl_nid_postfix(long double); +long double APS5_VABI asinl_nid_postfix(long double); long double APS5_VABI frexpl_nid_postfix(long double, int*); struct LibcFloatConstant { std::uint32_t bits[4]; }; extern LibcFloatConstant _FInf_nid_postfix; extern LibcFloatConstant _FNan_nid_postfix; short APS5_VABI _FDtest_nid_postfix(const float*); int APS5_VABI __fpclassifyf_nid_postfix(float); +int APS5_VABI isnanf_nid_postfix(float); +int APS5_VABI __signbitl_nid_postfix(long double); float APS5_VABI fmodf_nid_postfix(float, float); +double APS5_VABI fmod_nid_postfix(double, double); float APS5_VABI asinf_nid_postfix(float); float APS5_VABI acosf_nid_postfix(float); double APS5_VABI asin_nid_postfix(double); @@ -66,6 +74,7 @@ std::div_t APS5_VABI div_nid_postfix(int, int); double APS5_VABI sinh_nid_postfix(double); double APS5_VABI cosh_nid_postfix(double); float APS5_VABI coshf_nid_postfix(float); +float APS5_VABI sinhf_nid_postfix(float); double APS5_VABI asinh_nid_postfix(double); float APS5_VABI asinhf_nid_postfix(float); double APS5_VABI acosh_nid_postfix(double); @@ -83,6 +92,22 @@ std::lldiv_t APS5_VABI lldiv_nid_postfix(long long, long long); std::lldiv_t APS5_VABI ldiv_nid_postfix(std::int64_t, std::int64_t); float APS5_VABI nanf_nid_postfix(const char*); double APS5_VABI nan_nid_postfix(const char*); +long double APS5_VABI atanl_nid_postfix(long double); +long double APS5_VABI sinl_nid_postfix(long double); +long double APS5_VABI cosl_nid_postfix(long double); +long double APS5_VABI tanl_nid_postfix(long double); +long double APS5_VABI sinhl_nid_postfix(long double); +long double APS5_VABI coshl_nid_postfix(long double); +long double APS5_VABI tanhl_nid_postfix(long double); +long double APS5_VABI expl_nid_postfix(long double); +long double APS5_VABI exp2l_nid_postfix(long double); +long double APS5_VABI logl_nid_postfix(long double); +long double APS5_VABI log10l_nid_postfix(long double); +long double APS5_VABI powl_nid_postfix(long double, long double); +long double APS5_VABI ldexpl_nid_postfix(long double, int); +long double APS5_VABI truncl_nid_postfix(long double); +double APS5_VABI logb_nid_postfix(double); +double APS5_VABI remainder_nid_postfix(double, double); } static void Require(bool value) { if (!value) std::abort(); } @@ -136,6 +161,15 @@ static void CheckAsinfErrno() { Require(errno == 0); } +static void CheckExp2Errno() { + errno = 0; + std::feclearexcept(FE_ALL_EXCEPT); + double result = exp2_nid_postfix(1024.0); + Require(errno == 0); + Require(std::isinf(result) && result > 0); + Require(std::fetestexcept(FE_OVERFLOW | FE_INEXACT) == (FE_OVERFLOW | FE_INEXACT)); +} + static void CheckLongDouble() { Require(ExtendedBits(acosl_nid_postfix(-1.0L), 0xc90fdaa22168c235ull, 0x4000)); Require(ExtendedBits(acosl_nid_postfix(0.0L), 0xc90fdaa22168c235ull, 0x3fff)); @@ -158,6 +192,56 @@ static void CheckLongDouble() { Require(std::isnan(frexpl_nid_postfix(NAN, &exponent)) && exponent == 77); } +static void CheckFmodErrno() { + errno = 0; + (void)fmod_nid_postfix(1.0, 0.0); + Require(errno == 0); + Require(fmod_nid_postfix(1e300, 3.0) == std::fmod(1e300, 3.0)); +} + +static long double MakeExtended(std::uint64_t mantissa, std::uint16_t signExponent) { + unsigned char bytes[sizeof(long double)]{}; + std::memcpy(bytes, &mantissa, 8); + std::memcpy(bytes + 8, &signExponent, 2); + long double value = 0; + std::memcpy(&value, bytes, sizeof(value)); + return value; +} + +static void CheckAsinl() { + const struct { std::uint64_t inputMantissa; std::uint16_t inputSignExponent; std::uint64_t outputMantissa; std::uint16_t outputSignExponent; } cases[] = { + {0, 0x0000, 0, 0x0000}, {0, 0x8000, 0, 0x8000}, + {0x8000000000000000ull, 0x3fd7, 0x8000000000000000ull, 0x3fd7}, {0x8000000000000000ull, 0xbfd7, 0x8000000000000000ull, 0xbfd7}, + {0x8000000000000000ull, 0x3ffd, 0x815f4e705c3e68f3ull, 0x3ffd}, {0x8000000000000000ull, 0xbffd, 0x815f4e705c3e68f3ull, 0xbffd}, + {0xcccccccccccccccdull, 0x3ffb, 0xcd2493b01bc41e02ull, 0x3ffb}, + {0x8000000000000000ull, 0x3ffe, 0x860a91c16b9b2c23ull, 0x3ffe}, {0x8000000000000000ull, 0xbffe, 0x860a91c16b9b2c23ull, 0xbffe}, + {0xc000000000000000ull, 0xbffe, 0xd91a98ae3406e041ull, 0xbffe}, {0xe666666666666666ull, 0x3ffe, 0x8f549b82fd0b8fa3ull, 0x3fff}, + {0xf333333333333333ull, 0x3ffe, 0xa06a08acf9a04f78ull, 0x3fff}, {0xfd70a3d70a3d70a4ull, 0xbffe, 0xb6f1e3799ea8ecc3ull, 0xbfff}, + {0xffffffffffffffffull, 0x3ffe, 0xc90fdaa16c63cf01ull, 0x3fff}, + {0x8000000000000000ull, 0x3fff, 0xc90fdaa22168c235ull, 0x3fff}, {0x8000000000000000ull, 0xbfff, 0xc90fdaa22168c235ull, 0xbfff}, + }; + for (const auto& test : cases) { + *__error_nid_postfix() = 12345; + const long double result = asinl_nid_postfix(MakeExtended(test.inputMantissa, test.inputSignExponent)); + if (!ExtendedBits(result, test.outputMantissa, test.outputSignExponent) || *__error_nid_postfix() != 12345) { + std::fprintf(stderr, "Guest asinl failed for %016llx:%04x\n", static_cast(test.inputMantissa), test.inputSignExponent); + std::abort(); + } + } + Require(std::fabs(asinl_nid_postfix(0.5L) - 0.523598775598298873077107230547L) <= 0x1p-64L); + const long double domainErrors[] = { + 1.5L, -2.0L, MakeExtended(0x8000000000000001ull, 0x3fff), MakeExtended(0x8000000000000001ull, 0xbfff), + std::numeric_limits::max(), std::numeric_limits::infinity(), -std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN(), + }; + for (const long double input : domainErrors) { + std::feclearexcept(FE_ALL_EXCEPT); + *__error_nid_postfix() = 0; + Require(std::isnan(asinl_nid_postfix(input)) && *__error_nid_postfix() == EDOM && std::fetestexcept(FE_INVALID) != 0); + } + *__error_nid_postfix() = 0; +} + static void CheckIntegerConversions() { for (const long long numerator : {4294967301LL, -4294967301LL}) { for (const long long denominator : {3LL, -3LL}) { @@ -319,6 +403,40 @@ static void CheckAsin() { Require(!std::fetestexcept(FE_INVALID)); } +static void CheckExtendedMath() { + Require(ExtendedBits(atanl_nid_postfix(INFINITY), 0xc90fdaa22168c235ull, 0x3fff)); + Require(std::fabs(atanl_nid_postfix(1.0L) - 0.785398163397448309616L) < 1e-18L); + Require(std::fabs(sinl_nid_postfix(0.5L) - 0.479425538604203000273L) < 1e-18L); + Require(std::fabs(cosl_nid_postfix(0.5L) - 0.877582561890372716116L) < 1e-18L); + Require(std::fabs(tanl_nid_postfix(0.5L) - 0.546302489843790513255L) < 1e-18L); + Require(std::fabs(sinhl_nid_postfix(1.0L) - 1.17520119364380145688L) < 1e-18L); + Require(std::fabs(coshl_nid_postfix(1.0L) - 1.54308063481524377848L) < 1e-18L); + Require(std::fabs(tanhl_nid_postfix(0.5L) - 0.462117157260009758502L) < 1e-18L); + Require(std::fabs(expl_nid_postfix(1.0L) - 2.71828182845904523536L) < 1e-18L); + Require(std::fabs(logl_nid_postfix(2.0L) - 0.693147180559945309417L) < 1e-18L); + Require(sinl_nid_postfix(0.0L) == 0.0L && std::signbit(sinl_nid_postfix(-0.0L)) && cosl_nid_postfix(0.0L) == 1.0L); + Require(std::isnan(sinl_nid_postfix(INFINITY)) && std::isnan(cosl_nid_postfix(-INFINITY)) && std::isnan(tanl_nid_postfix(INFINITY))); + Require(sinhl_nid_postfix(-INFINITY) == -INFINITY && coshl_nid_postfix(-INFINITY) == INFINITY && tanhl_nid_postfix(INFINITY) == 1.0L); + Require(sinhl_nid_postfix(11358.0L) == INFINITY && std::isfinite(sinhl_nid_postfix(11356.0L)) && std::isfinite(coshl_nid_postfix(11356.0L))); + Require(expl_nid_postfix(0.0L) == 1.0L && expl_nid_postfix(-INFINITY) == 0.0L && expl_nid_postfix(11357.0L) == INFINITY); + Require(exp2l_nid_postfix(10.0L) == 1024.0L && exp2l_nid_postfix(-16446.0L) == 0.0L && exp2l_nid_postfix(16384.0L) == INFINITY); + Require(ExtendedBits(exp2l_nid_postfix(-16445.0L), 1, 0) && ExtendedBits(exp2l_nid_postfix(-16444.0L), 2, 0)); + Require(logl_nid_postfix(1.0L) == 0.0L && logl_nid_postfix(0.0L) == -INFINITY && std::isnan(logl_nid_postfix(-1.0L))); + Require(std::fabs(log10l_nid_postfix(1000.0L) - 3.0L) < 1e-18L && log10l_nid_postfix(1.0L) == 0.0L); + Require(log10l_nid_postfix(0.0L) == -INFINITY && std::isnan(log10l_nid_postfix(-1.0L))); + Require(powl_nid_postfix(2.0L, 64.0L) == 18446744073709551616.0L && powl_nid_postfix(-2.0L, 3.0L) == -8.0L); + Require(powl_nid_postfix(0.0L, -1.0L) == INFINITY && powl_nid_postfix(1.0L, NAN) == 1.0L && std::isnan(powl_nid_postfix(-2.0L, 0.5L))); + Require(ldexpl_nid_postfix(1.0L, 16383) == 0x1p16383L && ldexpl_nid_postfix(1.0L, 16384) == INFINITY && ldexpl_nid_postfix(3.0L, -2) == 0.75L); + Require(ExtendedBits(ldexpl_nid_postfix(1.0L, -16445), 1, 0) && ldexpl_nid_postfix(1.0L, -16446) == 0.0L); + Require(truncl_nid_postfix(2.7L) == 2.0L && truncl_nid_postfix(-2.7L) == -2.0L && ExtendedBits(truncl_nid_postfix(-0.5L), 0, 0x8000)); + Require(truncl_nid_postfix(9223372036854775807.5L) == 9223372036854775807.0L && truncl_nid_postfix(-INFINITY) == -INFINITY); + Require(logb_nid_postfix(8.0) == 3.0 && logb_nid_postfix(0.75) == -1.0 && logb_nid_postfix(0x1p-1074) == -1074.0); + Require(logb_nid_postfix(0.0) == -INFINITY && logb_nid_postfix(-INFINITY) == INFINITY && std::isnan(logb_nid_postfix(NAN))); + Require(remainder_nid_postfix(5.0, 3.0) == -1.0 && remainder_nid_postfix(7.0, 2.0) == -1.0 && remainder_nid_postfix(5.0, 2.0) == 1.0); + Require(std::signbit(remainder_nid_postfix(-4.0, 2.0)) && remainder_nid_postfix(1.0, INFINITY) == 1.0); + Require(std::isnan(remainder_nid_postfix(1.0, 0.0)) && std::isnan(remainder_nid_postfix(INFINITY, 1.0))); +} + static void CheckHyperbolic() { const double infinity = std::numeric_limits::infinity(); Require(sinh_nid_postfix(0.) == 0. && std::signbit(sinh_nid_postfix(-0.))); @@ -341,6 +459,38 @@ static void CheckHyperbolic() { Require(Near(acosh_nid_postfix(1e300), 691.4686750787736, 1e-15)); } +static void CheckSinhf() { + const float infinity = std::numeric_limits::infinity(); + const struct { std::uint32_t input; std::uint32_t output; } cases[] = { + {0x00000000u, 0x00000000u}, {0x80000000u, 0x80000000u}, {0x00000001u, 0x00000001u}, {0x39000000u, 0x39000000u}, + {0x3e800000u, 0x3e815667u}, {0xbf000000u, 0xbf056680u}, {0x3f800000u, 0x3f966cfeu}, {0xbf800000u, 0xbf966cfeu}, + {0x40000000u, 0x40681e7bu}, {0x41100000u, 0x457d38acu}, {0xc1a00000u, 0xcd675844u}, {0x42b17217u, 0x7effff84u}, + {0x42b20000u, 0x7f28e166u}, {0xc2b20000u, 0xff28e166u}, {0x42b2d4fcu, 0x7f7fffecu}, + {0x7f800000u, 0x7f800000u}, {0xff800000u, 0xff800000u}, + }; + for (const auto& test : cases) { + *__error_nid_postfix() = 12345; + const float result = sinhf_nid_postfix(std::bit_cast(test.input)); + if (std::bit_cast(result) != test.output || *__error_nid_postfix() != 12345) { + std::fprintf(stderr, "Guest sinhf failed for %08x\n", test.input); + std::abort(); + } + } + Require(std::isnan(sinhf_nid_postfix(std::numeric_limits::quiet_NaN())) && *__error_nid_postfix() == 12345); + Require(Near(sinhf_nid_postfix(1.f), 1.1752011936438014, 1e-6) && Near(sinhf_nid_postfix(-2.f), -3.626860407847019, 1e-6)); + Require(Near(sinhf_nid_postfix(0.25f), 0.25261231680816831, 1e-6) && Near(sinhf_nid_postfix(20.f), 242582597.70489514, 1e-6)); + for (const std::uint32_t input : {0x42b2d4fdu, 0x42c80000u, 0x7f7fffffu}) { + for (const std::uint32_t sign : {0u, 0x80000000u}) { + std::feclearexcept(FE_ALL_EXCEPT); + *__error_nid_postfix() = 0; + const float result = sinhf_nid_postfix(std::bit_cast(input | sign)); + Require(result == (sign != 0 ? -infinity : infinity) && *__error_nid_postfix() == ERANGE); + Require(std::fetestexcept(FE_OVERFLOW) != 0); + } + } + *__error_nid_postfix() = 0; +} + static void CheckFloatClassification() { Require(_FInf_nid_postfix.bits[0] == 0x7f800000u && _FNan_nid_postfix.bits[0] == 0x7fc00000u); for (int word = 1; word < 4; ++word) Require(_FInf_nid_postfix.bits[word] == 0 && _FNan_nid_postfix.bits[word] == 0); @@ -366,6 +516,21 @@ static void CheckFloatClassification() { Require(_FDtest_nid_postfix(reinterpret_cast(&_FNan_nid_postfix)) == 2); } +static void CheckMissingClassificationExports() { + const float infinity = std::numeric_limits::infinity(); + const float nan = std::numeric_limits::quiet_NaN(); + Require(isnanf_nid_postfix(nan) == 1); + Require(isnanf_nid_postfix(infinity) == 0 && isnanf_nid_postfix(-infinity) == 0); + Require(isnanf_nid_postfix(0.f) == 0 && isnanf_nid_postfix(-1.f) == 0); + + const long double negativeNan = std::copysign(std::numeric_limits::quiet_NaN(), -1.L); + Require(__signbitl_nid_postfix(0.L) == 0 && __signbitl_nid_postfix(-0.L) == 1); + Require(__signbitl_nid_postfix(1.L) == 0 && __signbitl_nid_postfix(-1.L) == 1); + Require(__signbitl_nid_postfix(std::numeric_limits::infinity()) == 0); + Require(__signbitl_nid_postfix(-std::numeric_limits::infinity()) == 1); + Require(__signbitl_nid_postfix(negativeNan) == 1); +} + static void CheckDoubleRounding(double value, std::int64_t expected, bool invalid) { for (const bool useLlround : {false, true}) { std::feclearexcept(FE_ALL_EXCEPT); @@ -388,9 +553,52 @@ static void CheckFloatRounding(float value, std::int64_t expected, bool invalid) } } +static void CheckNoConversionIsPositiveZero() { + for (const char* text : {"", " ", "-", "+", "-.", "+.", "-x", "-.e1", "-e5", "- 1", " -."}) { + char* end = nullptr; + const double parsed = strtod_nid_postfix(text, &end); + Require(parsed == 0. && !std::signbit(parsed) && end == text); + const float parsedFloat = strtof_nid_postfix(text, &end); + Require(parsedFloat == 0.f && !std::signbit(parsedFloat) && end == text); + const long double parsedLong = strtold_nid_postfix(text, &end); + Require(parsedLong == 0.L && !std::signbit(parsedLong) && end == text); + Require(!std::signbit(atof_nid_postfix(text))); + } + char* end = nullptr; + const char negativeZero[] = "-0.0x"; + const double parsed = strtod_nid_postfix(negativeZero, &end); + Require(parsed == 0. && std::signbit(parsed) && end == negativeZero + 4); + for (const char* text : {"-0", "-.0", "-0e5", "-0x0p0", "-0x"}) { + const double keptDouble = strtod_nid_postfix(text, &end); + Require(keptDouble == 0. && std::signbit(keptDouble) && end != text); + const float keptFloat = strtof_nid_postfix(text, &end); + Require(keptFloat == 0.f && std::signbit(keptFloat) && end != text); + const long double keptLong = strtold_nid_postfix(text, nullptr); + Require(keptLong == 0.L && std::signbit(keptLong)); + } + Require(strtod_nid_postfix("-inf", nullptr) == -std::numeric_limits::infinity()); + Require(std::isnan(atof_nid_postfix("nan"))); + const double noEndPointer = strtod_nid_postfix("-.", nullptr); + Require(noEndPointer == 0. && !std::signbit(noEndPointer)); + const char16_t wide[] = u"-."; + char16_t* wideEnd = nullptr; + const char16_t wideKept[] = u" -0.0x"; + const double keptWide = wcstod_nid_postfix(wideKept, &wideEnd); + Require(keptWide == 0. && std::signbit(keptWide) && wideEnd == wideKept + 5); + const double parsedWide = wcstod_nid_postfix(wide, &wideEnd); + Require(parsedWide == 0. && !std::signbit(parsedWide) && wideEnd == wide); + const float parsedWideFloat = wcstof_nid_postfix(wide, &wideEnd); + Require(parsedWideFloat == 0.f && !std::signbit(parsedWideFloat) && wideEnd == wide); + const long double parsedWideLong = wcstold_nid_postfix(wide, &wideEnd); + Require(parsedWideLong == 0.L && !std::signbit(parsedWideLong) && wideEnd == wide); +} + int main() { CheckFloatClassification(); + CheckNoConversionIsPositiveZero(); + CheckMissingClassificationExports(); CheckIntegerConversions(); + CheckExtendedMath(); Require(atof_nid_postfix(" -12.5tail") == -12.5); char* end = nullptr; const char input[] = "0x1.8p+2 remainder"; @@ -463,8 +671,10 @@ int main() { Require(exp2_nid_postfix(-3.) == 0.125); Require(ldexp_nid_postfix(0.75, 4) == 12.); CheckSquareRoot(); + CheckExp2Errno(); CheckExactOperations(); CheckLongDouble(); + CheckAsinl(); Require(scalbn_nid_postfix(0.75, -2) == 0.1875); Require(scalbnf_nid_postfix(0.75f, 4) == 12.f); Require(nextafterf_nid_postfix(1.f, 2.f) == 0x1.000002p0f && nextafterf_nid_postfix(1.f, 0.f) == 0x1.fffffep-1f); @@ -547,5 +757,7 @@ int main() { Require(std::isinf(__powisf2_nid_postfix(-0.0f, -3)) && std::signbit(__powisf2_nid_postfix(-0.0f, -3))); Require(__powisf2_nid_postfix(2.0f, -127) == 0x1p-127f); CheckHyperbolic(); + CheckSinhf(); CheckHypotSpecialValue(); + CheckFmodErrno(); } diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 427a6d064..fb33dafc1 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #endif #ifdef _WIN32 @@ -53,16 +54,19 @@ int APS5_VABI sceKernelReleaseFlexibleMemory(void*, std::size_t); int APS5_VABI sceKernelMprotect(const void*, std::size_t, int); int APS5_VABI sceKernelMtypeprotect(const void*, std::size_t, int, int); int APS5_VABI sceKernelBatchMap2(KernelBatchMapEntry*, int, int*, int); +int APS5_VABI sceKernelMemoryPoolBatch(const KernelMemoryPoolBatchEntry*, int, int*, int); int APS5_VABI sceKernelVirtualQuery(const void*, int, VirtualQueryInfo*, std::uint64_t); int APS5_VABI sceKernelSetVirtualRangeName(const void*, std::uint64_t, const char*); int APS5_VABI sceKernelClearVirtualRangeName(const void*, std::uint64_t); int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); int APS5_VABI sceKernelMapDirectMemory(void**, std::size_t, int, int, std::int64_t, std::size_t); +int APS5_VABI sceKernelEnableDmemAliasing(); int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); int APS5_VABI sceKernelAvailableDirectMemorySize(std::int64_t, std::int64_t, std::size_t, std::int64_t*, std::size_t*); int APS5_VABI sceKernelCheckedReleaseDirectMemory(std::int64_t, std::size_t); int APS5_VABI sceKernelReserveVirtualRange(void**, std::size_t, int, std::size_t); int APS5_VABI sceKernelMemoryPoolReserve(void*, std::size_t, std::size_t, int, void**); +int APS5_VABI sceKernelMemoryPoolCommit(void*, std::size_t, int, int, int); int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI sceKernelClose(int); std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); @@ -250,6 +254,7 @@ static void CheckDirectMemoryFollowsPhysicalPages() { Require(sceKernelMapDirectMemory(&first, page * 2, 3, 0, phys, 0) == 0); static_cast(first)[0] = 11; static_cast(first)[page + 5] = 22; + Require(sceKernelEnableDmemAliasing() == 0); void* alias = nullptr; Require(sceKernelMapDirectMemory(&alias, page, 3, 0, phys + page, 0) == 0); Require(alias != first && static_cast(alias)[5] == 22); @@ -289,6 +294,34 @@ static void CheckDirectMemoryFollowsPhysicalPages() { Require(sceKernelReleaseDirectMemory(again, page * 2) == 0); } +static void CheckSparseDirectMemoryBacking() { + constexpr std::size_t page = 0x4000; + constexpr std::size_t size = 256u * 1024u * 1024u; + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, size, 0, 0, &physical) == 0); + void* first = nullptr; + void* last = nullptr; + Require(sceKernelMapDirectMemory(&first, page, 3, 0, physical, 0) == 0); + Require(sceKernelMapDirectMemory(&last, page, 3, 0, physical + size - page, 0) == 0); + auto* firstBytes = static_cast(first); + auto* lastBytes = static_cast(last); + Require(firstBytes[0] == 0 && firstBytes[page - 1] == 0 && lastBytes[0] == 0 && lastBytes[page - 1] == 0); + firstBytes[0] = 17; + lastBytes[page - 1] = 29; + void* alias = nullptr; + Require(sceKernelMapDirectMemory(&alias, page, 3, 0, physical + size - page, 0) == 0); + auto* aliasBytes = static_cast(alias); + Require(aliasBytes[page - 1] == 29 && aliasBytes[0] == 0); + aliasBytes[page - 1] = 41; + Require(lastBytes[page - 1] == 41 && firstBytes[0] == 17); + Require(sceKernelMunmap(last, page) == 0 && sceKernelMunmap(alias, page) == 0); + last = nullptr; + Require(sceKernelMapDirectMemory(&last, page, 3, 0, physical + size - page, 0) == 0); + Require(static_cast(last)[page - 1] == 41); + Require(sceKernelMunmap(first, page) == 0 && sceKernelMunmap(last, page) == 0); + Require(sceKernelReleaseDirectMemory(physical, size) == 0); +} + static void CheckReleaseDirectMemoryClearsMappings() { constexpr std::size_t page = 0x4000; std::int64_t phys = 0; @@ -436,6 +469,29 @@ static void CheckMtypeprotect() { check(0, 0, 3); check(1, 3, 1); check(2, 5, 3); + KernelMemoryPoolBatchEntry poolEntry{}; + poolEntry.op = 4; + poolEntry.type_protect.addr = bytes + page * 2; + poolEntry.type_protect.len = page; + poolEntry.type_protect.prot = 1; + poolEntry.type_protect.type = 4; + processed = -1; + Require(sceKernelMemoryPoolBatch(&poolEntry, 1, &processed, 0) == 0 && processed == 1); + check(1, 3, 1); + check(2, 4, 1); + Require(sceKernelMemoryPoolCommit(bytes + page * 2, page, 6, 3, 0) == 0); + check(2, 6, 3); + check(1, 3, 1); + KernelMemoryPoolBatchEntry commitEntry{}; + commitEntry.op = 1; + commitEntry.commit.addr = bytes + page * 2; + commitEntry.commit.len = page; + commitEntry.commit.prot = 1; + commitEntry.commit.type = 7; + processed = -1; + Require(sceKernelMemoryPoolBatch(&commitEntry, 1, &processed, 0) == 0 && processed == 1); + check(1, 3, 1); + check(2, 7, 1); Require(sceKernelMtypeprotect(bytes, page * 3, 2, 3) == 0); VirtualQueryInfo whole{}; Require(sceKernelVirtualQuery(bytes, 0, &whole, sizeof(whole)) == 0); @@ -1593,6 +1649,90 @@ static void CheckDirectMemoryWriteWatch() { Require(sceKernelMunmap(flexible, page * 3) == 0); Require(sceKernelReleaseDirectMemory(phys, page * 3) == 0); } + +static void CheckDirectMemoryWriteWatchFollowsRemovedMappings() { + if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { + std::puts("write watch unavailable: removed direct mappings not tested"); + return; + } + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 4, 0, 0, &phys) == 0); + void* whole = nullptr; + Require(sceKernelMapDirectMemory(&whole, page * 4, 3, 0, phys, 0) == 0); + const auto address = reinterpret_cast(whole); + const auto watched = [](const void* pointer, std::size_t offset) { return GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(reinterpret_cast(pointer) + offset, page); }; + void* pair = nullptr; + Require(sceKernelMapDirectMemory(&pair, page * 2, 3, 0, phys + page, 0) == 0); + Require(watched(whole, 0) && !watched(whole, page) && !watched(whole, page * 2) && watched(whole, page * 3)); + Require(!watched(pair, 0) && !watched(pair, page)); + Require(sceKernelMunmap(pair, page) == 0); + void* single = nullptr; + Require(sceKernelMapDirectMemory(&single, page, 3, 0, phys + page, 0) == 0); + Require(!watched(single, 0) && watched(whole, 0) && watched(whole, page * 3)); + Require(sceKernelMunmap(single, page) == 0); + Require(sceKernelMunmap(static_cast(pair) + page, page) == 0); + void* replaced = whole; + Require(sceKernelMapDirectMemory(&replaced, page, 3, 0x10, phys + page * 3, 0) == 0 && replaced == whole); + Require(!watched(whole, 0) && !watched(whole, page * 3)); + std::uintptr_t start = 0; + std::uintptr_t end = 0; + std::uint64_t offset = 0; + int type = 0; + Require(QueryDirectMapping(address, &start, &end, &offset, &type) && start == address && end == address + page && offset == static_cast(phys) + page * 3); + void* alone = nullptr; + Require(sceKernelMapDirectMemory(&alone, page, 3, 0, phys, 0) == 0); + Require(watched(alone, 0)); + void* again = nullptr; + Require(sceKernelMapDirectMemory(&again, page, 3, 0, phys, 0) == 0); + Require(!watched(alone, 0) && !watched(again, 0)); + Require(sceKernelMunmap(again, page) == 0); + Require(sceKernelMunmap(alone, page) == 0); + Require(sceKernelMunmap(whole, page * 4) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 4) == 0); +} + +static double MapAcrossBackingsSeconds(std::int64_t phys, std::size_t backings) { + constexpr std::size_t page = 0x4000; + const auto threadSeconds = [] { + timespec now{}; + Require(clock_gettime(CLOCK_THREAD_CPUTIME_ID, &now) == 0); + return static_cast(now.tv_sec) + static_cast(now.tv_nsec) * 1e-9; + }; + auto best = std::numeric_limits::max(); + for (int attempt = 0; attempt < 3; ++attempt) { + void* mapped = nullptr; + const auto before = threadSeconds(); + Require(sceKernelMapDirectMemory(&mapped, page * backings, 3, 0, phys, 0) == 0); + best = std::min(best, threadSeconds() - before); + const auto last = reinterpret_cast(mapped) + page * (backings - 1); + std::uintptr_t start = 0; + std::uintptr_t end = 0; + std::uint64_t offset = 0; + int type = 0; + Require(QueryDirectMapping(last, &start, &end, &offset, &type) && start == last && end == last + page); + Require(sceKernelMunmap(mapped, page * backings) == 0); + } + return best; +} + +static void CheckMappingCostFollowsTheMappingCount() { + constexpr std::size_t page = 0x4000; + constexpr std::size_t few = 1024; + constexpr std::size_t many = few * 16; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * many, 0, 0, &phys) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * many) == 0); + for (std::size_t index = 0; index < many; ++index) { + const auto wanted = phys + static_cast(page * index); + std::int64_t piece = 0; + Require(sceKernelAllocateDirectMemory(wanted, wanted + static_cast(page), page, 0, 0, &piece) == 0 && piece == wanted); + } + const auto small = MapAcrossBackingsSeconds(phys, few); + const auto large = MapAcrossBackingsSeconds(phys, many); + Require(large <= small * 48); + Require(sceKernelReleaseDirectMemory(phys, page * many) == 0); +} #endif static void CheckVirtualQuerySplitFlexibleRanges() { @@ -1698,35 +1838,39 @@ static void CheckAvailableDirectMemoryIsLargestFreeBlock() { Require(sceKernelAvailableDirectMemorySize(top, top + window, 0, &start, &size) == 0); Require(start == top && size == static_cast(window)); } -#ifdef _WIN32 -static void CheckMprotectOfHostMemoryOutsideTheGuestRegistry() { - constexpr std::size_t length = 0x40000; - void* host = VirtualAlloc(nullptr, length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); - Require(host != nullptr); - Require(sceKernelMprotect(host, length, 0x3) == 0); - static_cast(host)[length - 1] = 7; - Require(static_cast(host)[length - 1] == 7); - Require(sceKernelMprotect(host, length, 0x1) == 0); - MEMORY_BASIC_INFORMATION info{}; - Require(VirtualQuery(host, &info, sizeof(info)) == sizeof(info) && info.Protect == PAGE_READONLY); - Require(sceKernelMprotect(host, length, 0x3) == 0); - Require(VirtualQuery(host, &info, sizeof(info)) == sizeof(info) && info.Protect == PAGE_READWRITE); - Require(VirtualFree(host, 0, MEM_RELEASE) != 0); + +static void CheckMunmapOverUnmappedPages() { + constexpr std::size_t page = 0x4000; + const auto failed = reinterpret_cast(static_cast(-1)); + void* raw = mmap_nid_postfix(nullptr, page * 3, 3, 0x1002, -1, 0); + Require(raw != failed); + auto* memory = static_cast(raw); + memory[0] = 1; + memory[page * 2] = 2; + Require(munmap_nid_postfix(memory + page, page) == 0); + Require(munmap_nid_postfix(memory, page * 3) == 0); + { + GuestAllocations::Mutation mutation; + Require(!mutation.Overlaps(memory, page * 3)); + } + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(memory, 0, &info, sizeof(info)) == SCE_KERNEL_ERROR_EACCES); + Require(sceKernelVirtualQuery(memory + page * 2, 0, &info, sizeof(info)) == SCE_KERNEL_ERROR_EACCES); + Require(munmap_nid_postfix(memory, page * 3) == 0); + Require(sceKernelMunmap(memory, page * 3) == 0); + Require(sceKernelMunmap(memory, 0) == SCE_KERNEL_ERROR_EINVAL); } -#endif int main() { #ifdef _WIN32 _putenv_s("APS5_PIN_WAIT_MS", "1000"); #endif CheckAvailableDirectMemoryIsLargestFreeBlock(); -#ifdef _WIN32 - CheckMprotectOfHostMemoryOutsideTheGuestRegistry(); -#endif CheckVirtualQuerySplitFlexibleRanges(); CheckVirtualQueryForNonReadableGuestRange(); CheckReleaseFlexibleMemory(); CheckVirtualQueryPartialMunmap(); + CheckMunmapOverUnmappedPages(); CheckNamedAndHintedMappings(); CheckInternalNamedFlexibleMapping(); CheckGuestModuleUsesFlexibleMemory(); @@ -1734,6 +1878,7 @@ int main() { CheckCheckedReleaseDirectMemory(); CheckAudioCoprocessorProtection(); CheckDirectMemoryFollowsPhysicalPages(); + CheckSparseDirectMemoryBacking(); CheckReleaseDirectMemoryClearsMappings(); CheckReleaseDirectMemoryRejectsInvalidRanges(); CheckFixedMappingReplacesPartialOverlap(); @@ -1774,6 +1919,8 @@ int main() { CheckWriteWatchArmedCollect(); CheckWriteWatchFreshCollect(); CheckDirectMemoryWriteWatch(); + CheckDirectMemoryWriteWatchFollowsRemovedMappings(); + CheckMappingCostFollowsTheMappingCount(); CheckDirectMemoryBackingNeedsNoFilesystem(); CheckDirectMemorySharedBacking(); #endif @@ -1840,7 +1987,7 @@ int main() { Require(munmap_nid_postfix(memory, page) == 0); Require(memory[page * 2] == 73); Require(munmap_nid_postfix(memory + page * 2, page) == 0); - Require(munmap_nid_postfix(memory, page) == -1); + Require(munmap_nid_postfix(memory, page) == 0); Require(madvise_nid_postfix(memory, page, 4) == 0); *__error_nid_postfix() = 0; Require(mprotect_nid_postfix(memory, page, 1) == -1 && *__error_nid_postfix() == 22); diff --git a/core/libs/tests/GuestMktime.cpp b/core/libs/tests/GuestMktime.cpp new file mode 100644 index 000000000..0a81573f9 --- /dev/null +++ b/core/libs/tests/GuestMktime.cpp @@ -0,0 +1,105 @@ +#include "prx/libc/include/GuestTime.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +std::int64_t APS5_VABI libc_mktime_nid_postfix(GuestTm*); +std::int64_t APS5_VABI mktime_nid_postfix(GuestTm*); +GuestTm* APS5_VABI localtime_s_nid_postfix(const std::int64_t*, GuestTm*); +void APS5_VABI tzset_nid_postfix(); +} + +using Converter = std::int64_t (APS5_VABI *)(GuestTm*); + +static void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +static bool Equal(const GuestTm& left, const GuestTm& right) { + return left.tm_sec == right.tm_sec && left.tm_min == right.tm_min && left.tm_hour == right.tm_hour + && left.tm_mday == right.tm_mday && left.tm_mon == right.tm_mon && left.tm_year == right.tm_year + && left.tm_wday == right.tm_wday && left.tm_yday == right.tm_yday && left.tm_isdst == right.tm_isdst + && left.tm_gmtoff == right.tm_gmtoff && left.tm_zone != nullptr && right.tm_zone != nullptr + && std::strcmp(left.tm_zone, right.tm_zone) == 0; +} + +static void CheckNormalization(Converter convert, std::int64_t timer) { + GuestTm expected{}; + Require(localtime_s_nid_postfix(&timer, &expected) == &expected, "Local conversion failed"); + GuestTm input = expected; + input.tm_sec += 60; + --input.tm_min; + input.tm_wday = -1; + input.tm_yday = -1; + input.tm_isdst = -1; + input.tm_gmtoff = -1; + input.tm_zone = nullptr; + errno = EOVERFLOW; + Require(convert(&input) == timer, "mktime returned the wrong timestamp"); + Require(Equal(input, expected), "mktime did not normalize a valid timestamp"); +} + +static void CheckFailure(Converter convert) { + GuestTm input{}; + input.tm_year = std::numeric_limits::max(); + input.tm_mon = std::numeric_limits::max(); + input.tm_mday = 1; + input.tm_wday = 4; + input.tm_yday = 12; + input.tm_isdst = -1; + input.tm_gmtoff = 123; + input.tm_zone = "unchanged"; + const GuestTm original = input; + Require(convert(&input) == -1, "mktime accepted an unrepresentable calendar"); + Require(Equal(input, original), "Failed mktime changed the guest calendar"); +#ifdef _WIN32 + input = {}; + input.tm_year = 69; + input.tm_mon = 11; + input.tm_mday = 31; + input.tm_hour = 23; + input.tm_min = 59; + input.tm_sec = 59; + input.tm_zone = "unchanged"; + const GuestTm beforeEpoch = input; + Require(convert(&input) == -1, "Windows mktime accepted a time before the epoch"); + Require(Equal(input, beforeEpoch), "Failed Windows mktime changed the guest calendar"); +#endif +} + +static void CheckZone(const char* zone) { +#ifdef _WIN32 + Require(_putenv_s("TZ", zone) == 0, "Could not set the time zone"); +#else + Require(setenv("TZ", zone, 1) == 0, "Could not set the time zone"); +#endif + tzset_nid_postfix(); + constexpr std::array converters{libc_mktime_nid_postfix, mktime_nid_postfix}; + constexpr std::array timers{-86401, -2, -1, 0, 1, 59, 60, 86400}; + for (auto convert : converters) { + for (const auto timer : timers) { +#ifdef _WIN32 + if (timer < 0) continue; +#endif + CheckNormalization(convert, timer); + } + CheckFailure(convert); + } +} + +int main() { + CheckZone("UTC0"); + CheckZone("UTC-2"); +#ifndef _WIN32 + CheckZone("UTC5"); +#endif +} diff --git a/core/libs/tests/GuestNanosleep.cpp b/core/libs/tests/GuestNanosleep.cpp index db2e4ee8b..c97b6c1f1 100644 --- a/core/libs/tests/GuestNanosleep.cpp +++ b/core/libs/tests/GuestNanosleep.cpp @@ -85,6 +85,22 @@ static void SleepsForTheRequest(Nanosleep sleep) { Require(MonotonicNanos() - start >= SLEEP_NANOS - EARLY_WAKE_MARGIN_NANOS); } +static void CompletedSleepLeavesTheRemainingTimeAlone(Nanosleep sleep) { + const KernelTimespec request{0, NANOS_PER_MILLISECOND}; + KernelTimespec remaining{7, 9}; + Require(sleep(&request, &remaining) == SCE_OK); + Require(remaining.tv_sec == 7 && remaining.tv_nsec == 9); + KernelTimespec shared = request; + for (int i = 0; i < 3; ++i) { + Require(sleep(&shared, &shared) == SCE_OK); + Require(shared.tv_sec == request.tv_sec && shared.tv_nsec == request.tv_nsec); + } + const KernelTimespec invalid{0, NANOS_PER_SECOND}; + remaining = {7, 9}; + Require(sleep(&invalid, &remaining) != SCE_OK); + Require(remaining.tv_sec == 7 && remaining.tv_nsec == 9); +} + int main() { PosixRejectsInvalidRequests(nanosleep_nid_postfix); PosixRejectsInvalidRequests(_nanosleep_nid_postfix); @@ -97,4 +113,8 @@ int main() { SleepsForTheRequest(nanosleep_nid_postfix); SleepsForTheRequest(_nanosleep_nid_postfix); SleepsForTheRequest(sceKernelNanosleep); + + CompletedSleepLeavesTheRemainingTimeAlone(nanosleep_nid_postfix); + CompletedSleepLeavesTheRemainingTimeAlone(_nanosleep_nid_postfix); + CompletedSleepLeavesTheRemainingTimeAlone(sceKernelNanosleep); } diff --git a/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp b/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp new file mode 100644 index 000000000..e8a0a3afc --- /dev/null +++ b/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp @@ -0,0 +1,62 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceNpAuthAuthorizedAppDialogInitialize(void); +int APS5_VABI sceNpAuthAuthorizedAppDialogOpen(const void* param); +int APS5_VABI sceNpAuthAuthorizedAppDialogUpdateStatus(void); +int APS5_VABI sceNpAuthAuthorizedAppDialogGetStatus(void); +int APS5_VABI sceNpAuthAuthorizedAppDialogGetResult(void* result); +int APS5_VABI sceNpAuthAuthorizedAppDialogClose(void); +int APS5_VABI sceNpAuthAuthorizedAppDialogTerminate(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast(0x80B80003); +constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); +constexpr int kErrNotFinished = static_cast(0x80B80005); +constexpr int kErrArgNull = static_cast(0x80B8000D); +constexpr int kResultUserCanceled = 1; + +} + +int main() { + std::int64_t param = 0; + std::int32_t result[2] = {-1, -1}; + + Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusNone); + Require(sceNpAuthAuthorizedAppDialogTerminate() == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogClose() == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogGetResult(result) == kErrNotInitialized); + + Require(sceNpAuthAuthorizedAppDialogInitialize() == 0); + Require(sceNpAuthAuthorizedAppDialogInitialize() == kErrAlreadyInitialized); + Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusInitialized); + Require(sceNpAuthAuthorizedAppDialogUpdateStatus() == kStatusInitialized); + + Require(sceNpAuthAuthorizedAppDialogOpen(nullptr) == kErrArgNull); + Require(sceNpAuthAuthorizedAppDialogGetResult(result) == kErrNotFinished); + + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == 0); + Require(sceNpAuthAuthorizedAppDialogUpdateStatus() == kStatusFinished); + Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusFinished); + + Require(sceNpAuthAuthorizedAppDialogGetResult(nullptr) == kErrArgNull); + Require(sceNpAuthAuthorizedAppDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == -1); + + Require(sceNpAuthAuthorizedAppDialogClose() == 0); + Require(sceNpAuthAuthorizedAppDialogTerminate() == 0); + Require(sceNpAuthAuthorizedAppDialogTerminate() == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusNone); + return 0; +} diff --git a/core/libs/tests/GuestOnce.cpp b/core/libs/tests/GuestOnce.cpp index b82ec22ef..3e60f58ec 100644 --- a/core/libs/tests/GuestOnce.cpp +++ b/core/libs/tests/GuestOnce.cpp @@ -1,4 +1,4 @@ -#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" #include #include #include @@ -9,7 +9,17 @@ struct GuestOnce { std::int32_t state; void* mutex; }; static_assert(sizeof(GuestOnce) == 16 && offsetof(GuestOnce, mutex) == 8); using Initializer = void (APS5_VABI *)(); -extern "C" int APS5_VABI pthread_once_nid_postfix(GuestOnce*, Initializer); +extern "C" { +int APS5_VABI pthread_once_nid_postfix(GuestOnce*, Initializer); +int APS5_VABI scePthreadOnce(std::int32_t* once, Initializer init); +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +void APS5_VABI scePthreadExit(void* retval); +int APS5_VABI scePthreadCancel(Pthread thread); +void APS5_VABI scePthreadTestcancel(); +Pthread APS5_VABI scePthreadSelf(); +void APS5_VABI __pthread_cleanup_push_imp_nid_postfix(void (APS5_VABI* routine)(void*), void* argument, void* info); +} static void Require(bool value) { if (!value) std::abort(); } static GuestOnce control{}, nested{}, retry{}; static std::atomic calls{0}, arrived{0}; @@ -24,6 +34,35 @@ static void APS5_VABI Initialize() { static void APS5_VABI Retry() { if (++attempts == 1) throw 7; } +static GuestOnce exited{}, cancelled{}; +static std::int32_t sceExited = 0, sceCancelled = 0; +static int retried = 0; +static void APS5_VABI ExitInsideInitializer() { scePthreadExit(nullptr); } +static void APS5_VABI CancelInsideInitializer() { + Require(scePthreadCancel(scePthreadSelf()) == 0); + scePthreadTestcancel(); +} +static void APS5_VABI CountRetry() { ++retried; } +static void APS5_VABI AbortCleanup(void*) { std::abort(); } +static void APS5_VABI PushAndThrow() { + std::uintptr_t info[8]; + __pthread_cleanup_push_imp_nid_postfix(AbortCleanup, nullptr, info); + throw 7; +} +static GuestOnce pushed{}; +static std::int32_t scePushed = 0; +struct OnceCall { GuestOnce* control; std::int32_t* sceControl; Initializer initialize; }; +static void* APS5_VABI CallOnce(void* argument) { + const auto& call = *static_cast(argument); + if (call.control) pthread_once_nid_postfix(call.control, call.initialize); + else scePthreadOnce(call.sceControl, call.initialize); + return nullptr; +} +static void LeaveInsideInitializer(OnceCall call) { + Pthread thread{}; + Require(scePthreadCreate(&thread, nullptr, CallOnce, &call, nullptr) == 0); + Require(scePthreadJoin(thread, nullptr) == 0); +} int main() { std::array workers; for (auto& worker : workers) worker = std::thread([] { @@ -41,4 +80,22 @@ int main() { Require(pthread_once_nid_postfix(&control, nullptr) == 22); GuestOnce invalid{9, nullptr}; Require(pthread_once_nid_postfix(&invalid, Initialize) == 22); + LeaveInsideInitializer({&exited, nullptr, ExitInsideInitializer}); + Require(exited.state == 0); + Require(pthread_once_nid_postfix(&exited, CountRetry) == 0 && exited.state == 1 && retried == 1); + LeaveInsideInitializer({&cancelled, nullptr, CancelInsideInitializer}); + Require(cancelled.state == 0); + Require(pthread_once_nid_postfix(&cancelled, CountRetry) == 0 && cancelled.state == 1 && retried == 2); + LeaveInsideInitializer({nullptr, &sceExited, ExitInsideInitializer}); + Require(sceExited == 0); + Require(scePthreadOnce(&sceExited, CountRetry) == 0 && sceExited == 1 && retried == 3); + LeaveInsideInitializer({nullptr, &sceCancelled, CancelInsideInitializer}); + Require(sceCancelled == 0); + Require(scePthreadOnce(&sceCancelled, CountRetry) == 0 && sceCancelled == 1 && retried == 4); + try { pthread_once_nid_postfix(&pushed, PushAndThrow); std::abort(); } + catch (int value) { Require(value == 7 && pushed.state == 0); } + Require(pthread_once_nid_postfix(&pushed, CountRetry) == 0 && pushed.state == 1 && retried == 5); + try { scePthreadOnce(&scePushed, PushAndThrow); std::abort(); } + catch (int value) { Require(value == 7 && scePushed == 0); } + Require(scePthreadOnce(&scePushed, CountRetry) == 0 && scePushed == 1 && retried == 6); } diff --git a/core/libs/tests/GuestOperationMode.cpp b/core/libs/tests/GuestOperationMode.cpp new file mode 100644 index 000000000..248ec491a --- /dev/null +++ b/core/libs/tests/GuestOperationMode.cpp @@ -0,0 +1,20 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +extern "C" { +void APS5_VABI sceKernelGetOperationMode(int* mode, int* submode); +} +static void Require(bool value) { if (!value) std::abort(); } +static bool Throws(int* mode, int* submode) { + try { sceKernelGetOperationMode(mode, submode); } catch (const std::invalid_argument&) { return true; } + return false; +} +int main() { + int out[4] = {-1, -1, -1, -1}; + sceKernelGetOperationMode(&out[0], &out[2]); + Require(out[0] == 2 && out[1] == -1); + Require(out[2] == 0 && out[3] == -1); + int value = -1; + Require(Throws(nullptr, &value) && Throws(&value, nullptr)); + Require(value == -1); +} diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 5a7f7fab9..4eb1d8b0b 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -57,8 +57,34 @@ static void CheckInputCommands() { Require(commands.Buttons(1001000) == 0); write("2 16384 200"); Require(commands.Buttons(1101000) == 0); + write("5 0 300 0 128 255 64"); + const auto tilted = commands.Poll(1201000); + Require(tilted.buttons == 0 && tilted.hasSticks); + Require(tilted.sticks[0] == 0 && tilted.sticks[1] == 128 && tilted.sticks[2] == 255 && tilted.sticks[3] == 64); + Require(!commands.Poll(1501000).hasSticks); + write("6 16384 300"); + const auto pressed = commands.Poll(1601000); + Require(pressed.buttons == 0x4000 && !pressed.hasSticks); + const Pad::InputCommandFile::Input none; + write("7 16384 300 128 128"); + Require(commands.Poll(1901000) == none); + write("7 16384 300 128 128 128 256"); + Require(commands.Poll(2001000) == none); + write("7 16384 300 128 128 128 128 128"); + Require(commands.Poll(2101000) == none); + write("7 16384 300 128 128 128 up"); + Require(commands.Poll(2201000) == none); + write("7 16384 300 +"); + Require(commands.Poll(2301000) == none); + write("7 16384 300 4294967296"); + Require(commands.Poll(2401000) == none); + write("7 16384 300 128 128 128 128x"); + Require(commands.Poll(2501000) == none); + write("7 8192 300 1 2 3 4"); + const auto accepted = commands.Poll(2601000); + Require(accepted.buttons == 0x2000 && accepted.hasSticks && accepted.sticks[3] == 4); std::filesystem::remove(path); - Require(commands.Buttons(1201000) == 0); + Require(commands.Buttons(3001000) == 0); Pad::InputCommandFile disabled(nullptr); Require(disabled.Buttons(0) == 0); } @@ -101,6 +127,23 @@ static void CheckTouchContact() { Pad::ReadState(); } +static void CheckPartialTrigger() { + PadInputState partial; + partial.buttons = static_cast(Pad::PadButton::L2) | static_cast(Pad::PadButton::R2); + partial.analogButtonsL2 = 1; + partial.analogButtonsR2 = 73; + PadPublishInput_nid_postfix(partial); + const auto data = Pad::ReadState(); + Require(data.buttons == partial.buttons); + Require(data.analog_buttons_l2 == 1 && data.analog_buttons_r2 == 73); + PadInputState digital; + digital.buttons = static_cast(Pad::PadButton::L2); + PadPublishInput_nid_postfix(digital); + Require(Pad::ReadState().analog_buttons_l2 == 255); + PadPublishInput_nid_postfix(PadInputState{}); + Pad::ReadState(); +} + static void CheckReadQueue(int handle) { PadData drained[64]{}; scePadRead_nid_postfix(handle, drained, 64); @@ -156,6 +199,24 @@ static void CheckInjectedState(const std::filesystem::path& path) { const auto released = Pad::ReadState(); Require(released.buttons == 0x2000 && released.analog_buttons_l2 == 0); Require(released.timestamp > pressed.timestamp); + physical.sticks[3] = 17; + PadPublishInput_nid_postfix(physical); + const auto still = Pad::ReadState(); + std::ofstream(path) << "2 0 300 200 0 128 128\n"; + PadData moved{}; + const auto stickDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + do { + moved = Pad::ReadState(); + if (moved.left_stick_x == 200) break; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } while (std::chrono::steady_clock::now() < stickDeadline); + Require(moved.left_stick_x == 200 && moved.left_stick_y == 0); + Require(moved.right_stick_x == 128 && moved.right_stick_y == 128); + Require(moved.buttons == 0x2000 && moved.timestamp > still.timestamp); + std::this_thread::sleep_for(std::chrono::milliseconds(310)); + const auto centred = Pad::ReadState(); + Require(centred.left_stick_x == 42 && centred.right_stick_y == 17); + Require(centred.timestamp > moved.timestamp); PadPublishInput_nid_postfix(PadInputState{}); } @@ -193,6 +254,7 @@ int main() { Require(scePadGetHandle(user, 0, 0) == handle); CheckTiltCorrection(handle); CheckTouchContact(); + CheckPartialTrigger(); CheckReadQueue(handle); CheckReadStateHandle(handle); CheckRemoteController(handle); diff --git a/core/libs/tests/GuestPosixSem.cpp b/core/libs/tests/GuestPosixSem.cpp index aae90c1ae..f6a296312 100644 --- a/core/libs/tests/GuestPosixSem.cpp +++ b/core/libs/tests/GuestPosixSem.cpp @@ -1,16 +1,20 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include +#include #include #include #include #include #include +#include extern "C" { int APS5_VABI sem_init_nid_postfix(void*, int, unsigned int); int APS5_VABI sem_destroy_nid_postfix(void*); int APS5_VABI sem_post_nid_postfix(void*); int APS5_VABI sem_trywait_nid_postfix(void*); +int APS5_VABI sem_timedwait_nid_postfix(void*, const KernelTimespec*); int APS5_VABI sem_getvalue_nid_postfix(void*, int*); int* APS5_VABI __error_nid_postfix(); } @@ -58,4 +62,41 @@ int main(int argc, char** argv) { Require(sem_trywait_nid_postfix(&sem) == 0, "normal wait must succeed"); Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "normal wait must decrement"); Require(sem_destroy_nid_postfix(&sem) == 0, "normal destroy must succeed"); + + KernelTimespec invalidTimeout{0, 1000000000}; + Require(sem_init_nid_postfix(&sem, 0, 1) == 0, "available semaphore must initialize"); + Require(sem_timedwait_nid_postfix(&sem, nullptr) == 0, "available semaphore must ignore null timeout"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "null-timeout wait must consume the available count"); + Require(sem_destroy_nid_postfix(&sem) == 0, "null-timeout semaphore destroy must succeed"); + + Require(sem_init_nid_postfix(&sem, 0, 1) == 0, "invalid-timeout semaphore must initialize"); + Require(sem_timedwait_nid_postfix(&sem, &invalidTimeout) == 0, "available semaphore must ignore invalid timeout"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "invalid-timeout wait must consume the available count"); + Require(sem_destroy_nid_postfix(&sem) == 0, "invalid-timeout semaphore destroy must succeed"); + + Require(sem_init_nid_postfix(&sem, 0, 0) == 0, "blocked semaphore must initialize"); + std::atomic entered = false; + std::atomic completed = false; + int waitResult = -2; + std::thread waiter([&] { + entered.store(true, std::memory_order_release); + waitResult = sem_timedwait_nid_postfix(&sem, nullptr); + completed.store(true, std::memory_order_release); + }); + while (!entered.load(std::memory_order_acquire)) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + Require(!completed.load(std::memory_order_acquire), "blocked null-timeout wait must remain blocked"); + Require(sem_post_nid_postfix(&sem) == 0, "post must release a null-timeout waiter"); + waiter.join(); + Require(waitResult == 0, "null-timeout waiter must succeed after a post"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "null-timeout waiter must consume the posted count"); + + Require(sem_init_nid_postfix(&sem, 0, 0) == 0, "invalid-timeout blocked semaphore must initialize"); + *__error_nid_postfix() = 0; + Require(sem_timedwait_nid_postfix(&sem, &invalidTimeout) == -1 && *__error_nid_postfix() == 22, "blocked invalid-timeout wait must report EINVAL"); + KernelTimespec expiredTimeout{0, 0}; + *__error_nid_postfix() = 0; + Require(sem_timedwait_nid_postfix(&sem, &expiredTimeout) == -1 && *__error_nid_postfix() == 60, "blocked expired-timeout wait must report ETIMEDOUT"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "failed timed waits must preserve the count"); + Require(sem_destroy_nid_postfix(&sem) == 0, "blocked semaphore destroy must succeed"); } diff --git a/core/libs/tests/GuestPthreadAttr.cpp b/core/libs/tests/GuestPthreadAttr.cpp index ee24e0000..12c5f1b9b 100644 --- a/core/libs/tests/GuestPthreadAttr.cpp +++ b/core/libs/tests/GuestPthreadAttr.cpp @@ -22,11 +22,19 @@ int APS5_VABI scePthreadAttrGetdetachstate(const PthreadAttr* attr, int* state); int APS5_VABI scePthreadAttrGetsolosched(const PthreadAttr* attr, int* solosched); int APS5_VABI pthread_attr_setstacksize_nid_postfix(PthreadAttr* attr, std::size_t stackSize); int APS5_VABI pthread_attr_setsolosched_np_nid_postfix(PthreadAttr* attr, int solosched); +int APS5_VABI pthread_attr_getinheritsched_nid_postfix(const PthreadAttr* attr, int* inherit_sched); +int APS5_VABI pthread_attr_setinheritsched_nid_postfix(PthreadAttr* attr, int inherit_sched); +int APS5_VABI pthread_attr_getscope_nid_postfix(const PthreadAttr* attr, int* scope); +int APS5_VABI pthread_attr_setscope_nid_postfix(PthreadAttr* attr, int scope); +int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr); } static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); static constexpr int EXPLICIT_SCHED = 0; +static constexpr int INHERIT_SCHED = 4; +static constexpr int SCOPE_PROCESS = 0; +static constexpr int SCOPE_SYSTEM = 2; static constexpr int DETACH_JOINABLE = 0; static constexpr int CREATION_PRIORITY = 256; static constexpr int UPDATED_PRIORITY = 767; @@ -35,6 +43,7 @@ static constexpr KernelCpumask UPDATED_AFFINITY = 0x1000; static constexpr std::size_t STACK_SIZE = 2u << 20; static constexpr std::size_t MIN_STACK_SIZE = 16384; static constexpr int GUEST_EINVAL = 22; +static constexpr int GUEST_ENOTSUP = 45; static void Require(bool value) { if (!value) std::abort(); } @@ -48,6 +57,7 @@ struct ReportedAttributes { extern "C" { int APS5_VABI pthread_attr_setstack_nid_postfix(PthreadAttr* attr, void* addr, std::size_t size); int APS5_VABI pthread_attr_getstack_nid_postfix(const PthreadAttr* attr, void** addr, std::size_t* size); +int APS5_VABI pthread_attr_getstackaddr_nid_postfix(const PthreadAttr* attr, void** addr); } static ReportedAttributes Query(Pthread thread) { @@ -78,6 +88,8 @@ static void CheckStackSizeLimit() { Require(scePthreadAttrDestroy(&attr) == SCE_OK); } +extern "C" int APS5_VABI scePthreadAttrSetsolosched(PthreadAttr* attr, int solosched); + static void CheckSoloSched() { PthreadAttr attr = nullptr; Require(pthread_attr_setsolosched_np_nid_postfix(nullptr, 1) == GUEST_EINVAL); @@ -85,10 +97,21 @@ static void CheckSoloSched() { Require(scePthreadAttrInit(&attr) == SCE_OK); int soloSched = -1; Require(scePthreadAttrGetsolosched(&attr, &soloSched) == SCE_OK && soloSched == 0); - Require(pthread_attr_setsolosched_np_nid_postfix(&attr, 1) == 0); - Require(scePthreadAttrGetsolosched(&attr, &soloSched) == SCE_OK && soloSched == 1); + Require(pthread_attr_setsolosched_np_nid_postfix(&attr, 16) == 0); + Require(scePthreadAttrGetsolosched(&attr, &soloSched) == SCE_OK && soloSched == 16); + constexpr int GuestEopnotsupp = 45; + constexpr int SceKernelErrorEopnotsupp = static_cast(0x8002002Du); + for (int rejected : {-1, 1, 2, 4, 8, 15, 17, 32, 100, 0x7fffffff}) { + Require(pthread_attr_setsolosched_np_nid_postfix(&attr, rejected) == GuestEopnotsupp); + Require(scePthreadAttrSetsolosched(&attr, rejected) == SceKernelErrorEopnotsupp); + Require(scePthreadAttrGetsolosched(&attr, &soloSched) == SCE_OK && soloSched == 16); + } Require(pthread_attr_setsolosched_np_nid_postfix(&attr, 0) == 0); Require(scePthreadAttrGetsolosched(&attr, &soloSched) == SCE_OK && soloSched == 0); + PthreadAttr unset = nullptr; + Require(scePthreadAttrSetsolosched(&unset, 16) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetsolosched(nullptr, 16) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetsolosched(&unset, &soloSched) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadAttrDestroy(&attr) == SCE_OK); Require(pthread_attr_setsolosched_np_nid_postfix(&attr, 1) == GUEST_EINVAL); } @@ -98,9 +121,107 @@ static void* APS5_VABI Worker(void* arg) { return nullptr; } +static void CheckSchedulingAttributes() { + int inherit = -1; + int scope = -1; + Require(pthread_attr_getinheritsched_nid_postfix(nullptr, &inherit) == GUEST_EINVAL); + Require(pthread_attr_setinheritsched_nid_postfix(nullptr, EXPLICIT_SCHED) == GUEST_EINVAL); + Require(pthread_attr_getscope_nid_postfix(nullptr, &scope) == GUEST_EINVAL); + Require(pthread_attr_setscope_nid_postfix(nullptr, SCOPE_SYSTEM) == GUEST_EINVAL); + + PthreadAttr attr = nullptr; + Require(pthread_attr_getinheritsched_nid_postfix(&attr, &inherit) == GUEST_EINVAL); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, EXPLICIT_SCHED) == GUEST_EINVAL); + Require(pthread_attr_getscope_nid_postfix(&attr, &scope) == GUEST_EINVAL); + Require(pthread_attr_setscope_nid_postfix(&attr, SCOPE_SYSTEM) == GUEST_EINVAL); + + Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(pthread_attr_getinheritsched_nid_postfix(&attr, &inherit) == 0 && inherit == INHERIT_SCHED); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, 1) == GUEST_ENOTSUP); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, -1) == GUEST_ENOTSUP); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, EXPLICIT_SCHED) == 0); + Require(pthread_attr_getinheritsched_nid_postfix(&attr, &inherit) == 0 && inherit == EXPLICIT_SCHED); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, INHERIT_SCHED) == 0); + Require(pthread_attr_getinheritsched_nid_postfix(&attr, &inherit) == 0 && inherit == INHERIT_SCHED); + + Require(pthread_attr_getscope_nid_postfix(&attr, &scope) == 0 && scope == SCOPE_SYSTEM); + Require(pthread_attr_setscope_nid_postfix(&attr, 1) == GUEST_EINVAL); + Require(pthread_attr_setscope_nid_postfix(&attr, 3) == GUEST_EINVAL); + Require(pthread_attr_setscope_nid_postfix(&attr, -1) == GUEST_EINVAL); + Require(pthread_attr_setscope_nid_postfix(&attr, SCOPE_PROCESS) == 0); + Require(pthread_attr_getscope_nid_postfix(&attr, &scope) == 0 && scope == SCOPE_PROCESS); + Require(pthread_attr_setscope_nid_postfix(&attr, SCOPE_SYSTEM) == 0); + Require(pthread_attr_getscope_nid_postfix(&attr, &scope) == 0 && scope == SCOPE_SYSTEM); + + Require(pthread_attr_getscope_nid_postfix(&attr, nullptr) == GUEST_EINVAL); + + bool threw = false; + try { + pthread_attr_getinheritsched_nid_postfix(&attr, nullptr); + } catch (const std::exception&) { + threw = true; + } + Require(threw); + + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + Require(pthread_attr_getinheritsched_nid_postfix(&attr, &inherit) == GUEST_EINVAL); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, EXPLICIT_SCHED) == GUEST_EINVAL); + Require(pthread_attr_getscope_nid_postfix(&attr, &scope) == GUEST_EINVAL); + Require(pthread_attr_setscope_nid_postfix(&attr, SCOPE_SYSTEM) == GUEST_EINVAL); +} + +static void CheckThreadScope() { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, EXPLICIT_SCHED) == 0); + Require(pthread_attr_setscope_nid_postfix(&attr, SCOPE_PROCESS) == 0); + + std::promise release; + auto released = release.get_future(); + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, &attr, Worker, &released, nullptr) == SCE_OK); + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + + PthreadAttr queryAttr = nullptr; + Require(scePthreadAttrInit(&queryAttr) == SCE_OK); + Require(scePthreadAttrGet(thread, &queryAttr) == SCE_OK); + int scope = -1; + Require(pthread_attr_getscope_nid_postfix(&queryAttr, &scope) == 0 && scope == SCOPE_PROCESS); + + Require(pthread_attr_get_np_nid_postfix(thread, &queryAttr) == 0); + Require(pthread_attr_getscope_nid_postfix(&queryAttr, &scope) == 0 && scope == SCOPE_PROCESS); + Require(scePthreadAttrDestroy(&queryAttr) == SCE_OK); + + release.set_value(); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + + Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(pthread_attr_setinheritsched_nid_postfix(&attr, INHERIT_SCHED) == 0); + Require(pthread_attr_setscope_nid_postfix(&attr, SCOPE_PROCESS) == 0); + + std::promise releaseInherited; + auto releasedInherited = releaseInherited.get_future(); + Pthread inheritedThread = nullptr; + Require(scePthreadCreate(&inheritedThread, &attr, Worker, &releasedInherited, nullptr) == SCE_OK); + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + + Require(scePthreadAttrInit(&queryAttr) == SCE_OK); + Require(scePthreadAttrGet(inheritedThread, &queryAttr) == SCE_OK); + scope = -1; + Require(pthread_attr_getscope_nid_postfix(&queryAttr, &scope) == 0 && scope == SCOPE_SYSTEM); + Require(pthread_attr_get_np_nid_postfix(inheritedThread, &queryAttr) == 0); + Require(pthread_attr_getscope_nid_postfix(&queryAttr, &scope) == 0 && scope == SCOPE_SYSTEM); + Require(scePthreadAttrDestroy(&queryAttr) == SCE_OK); + + releaseInherited.set_value(); + Require(scePthreadJoin(inheritedThread, nullptr) == SCE_OK); +} + int main() { CheckStackSizeLimit(); CheckSoloSched(); + CheckSchedulingAttributes(); + CheckThreadScope(); PthreadAttr attr = nullptr; Require(scePthreadAttrInit(&attr) == SCE_OK); Require(scePthreadAttrSetinheritsched(&attr, EXPLICIT_SCHED) == SCE_OK); @@ -141,16 +262,23 @@ int main() { PthreadAttr stackAttr = nullptr; char stackMarker; + void* reportedAddress = nullptr; + std::size_t reportedSize = 0; Require(pthread_attr_setstack_nid_postfix(nullptr, &stackMarker, STACK_SIZE) == 22); Require(pthread_attr_setstack_nid_postfix(&stackAttr, &stackMarker, STACK_SIZE) == 22); + Require(pthread_attr_getstackaddr_nid_postfix(nullptr, &reportedAddress) == 22); + Require(pthread_attr_getstackaddr_nid_postfix(&stackAttr, &reportedAddress) == 22); Require(scePthreadAttrInit(&stackAttr) == SCE_OK); + Require(pthread_attr_getstackaddr_nid_postfix(&stackAttr, nullptr) == 22); + Require(pthread_attr_getstackaddr_nid_postfix(&stackAttr, &reportedAddress) == 0 && reportedAddress == nullptr); Require(pthread_attr_setstack_nid_postfix(&stackAttr, &stackMarker, STACK_SIZE) == 0); Require(pthread_attr_setstack_nid_postfix(&stackAttr, nullptr, STACK_SIZE) == 22); Require(pthread_attr_setstack_nid_postfix(&stackAttr, &stackMarker, 16383) == 22); - void* reportedAddress = nullptr; - std::size_t reportedSize = 0; Require(pthread_attr_getstack_nid_postfix(&stackAttr, &reportedAddress, &reportedSize) == 0); Require(reportedAddress == &stackMarker && reportedSize == STACK_SIZE); + reportedAddress = nullptr; + Require(pthread_attr_getstackaddr_nid_postfix(&stackAttr, &reportedAddress) == 0 && reportedAddress == &stackMarker); Require(scePthreadAttrDestroy(&stackAttr) == SCE_OK); Require(pthread_attr_setstack_nid_postfix(&stackAttr, &stackMarker, STACK_SIZE) == 22); + Require(pthread_attr_getstackaddr_nid_postfix(&stackAttr, &reportedAddress) == 22); } diff --git a/core/libs/tests/GuestPthreadAttrErrors.cpp b/core/libs/tests/GuestPthreadAttrErrors.cpp new file mode 100644 index 000000000..259ee7789 --- /dev/null +++ b/core/libs/tests/GuestPthreadAttrErrors.cpp @@ -0,0 +1,85 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); +int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stackSize); +int APS5_VABI scePthreadAttrGetstacksize(const PthreadAttr* attr, std::size_t* stackSize); +int APS5_VABI scePthreadAttrSetdetachstate(PthreadAttr* attr, int state); +int APS5_VABI scePthreadAttrGetdetachstate(const PthreadAttr* attr, int* state); +int APS5_VABI scePthreadAttrSetschedpolicy(PthreadAttr* attr, int policy); +int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched); +int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param); +int APS5_VABI scePthreadAttrGetschedparam(const PthreadAttr* attr, KernelSchedParam* param); +int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr* attr, KernelCpumask mask); +int APS5_VABI scePthreadAttrGetaffinity(const PthreadAttr* attr, KernelCpumask* mask); +} + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016u); +static constexpr int SCE_KERNEL_ERROR_EOPNOTSUPP = static_cast(0x8002002Du); + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK); + + for (std::size_t size : {std::size_t{0}, std::size_t{1}, std::size_t{16383}}) + Require(scePthreadAttrSetstacksize(&attr, size) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetstacksize(&attr, 16384) == SCE_OK); + + for (int state : {-1, 2, 3, 100}) Require(scePthreadAttrSetdetachstate(&attr, state) == SCE_KERNEL_ERROR_EINVAL); + for (int state : {0, 1}) Require(scePthreadAttrSetdetachstate(&attr, state) == SCE_OK); + + for (int policy : {-1, 0, 2, 4}) Require(scePthreadAttrSetschedpolicy(&attr, policy) == SCE_KERNEL_ERROR_EOPNOTSUPP); + for (int policy : {1, 3}) Require(scePthreadAttrSetschedpolicy(&attr, policy) == SCE_OK); + + for (int inherit : {-1, 1, 2, 8}) Require(scePthreadAttrSetinheritsched(&attr, inherit) == SCE_KERNEL_ERROR_EOPNOTSUPP); + for (int inherit : {0, 4}) Require(scePthreadAttrSetinheritsched(&attr, inherit) == SCE_OK); + + KernelSchedParam param{}; + for (int priority : {-1, 0, 255, 768, 900, 960}) { + param.sched_priority = priority; + Require(scePthreadAttrSetschedparam(&attr, ¶m) == SCE_KERNEL_ERROR_EOPNOTSUPP); + } + Require(scePthreadAttrSetschedparam(&attr, nullptr) == SCE_KERNEL_ERROR_EOPNOTSUPP); + for (int priority : {256, 767}) { + param.sched_priority = priority; + Require(scePthreadAttrSetschedparam(&attr, ¶m) == SCE_OK); + KernelSchedParam read{}; + Require(scePthreadAttrGetschedparam(&attr, &read) == SCE_OK && read.sched_priority == priority); + } + for (KernelCpumask mask : {KernelCpumask{0}, KernelCpumask{1}, ~KernelCpumask{0}}) + Require(scePthreadAttrSetaffinity(&attr, mask) == SCE_OK); + + std::size_t size = 0; + int state = 0; + KernelCpumask mask = 0; + Require(scePthreadAttrGetstacksize(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetdetachstate(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetschedparam(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); + + for (PthreadAttr* missing : {static_cast(nullptr), static_cast(&attr)}) { + PthreadAttr unset = nullptr; + PthreadAttr* target = missing ? &unset : nullptr; + Require(scePthreadAttrSetstacksize(target, 16384) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetdetachstate(target, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetschedpolicy(target, 1) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetinheritsched(target, 0) == SCE_KERNEL_ERROR_EINVAL); + param.sched_priority = 700; + Require(scePthreadAttrSetschedparam(target, ¶m) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetaffinity(target, 1) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetstacksize(target, &size) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetdetachstate(target, &state) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetschedparam(target, ¶m) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetaffinity(target, &mask) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrDestroy(target) == SCE_KERNEL_ERROR_EINVAL); + } + + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + Require(scePthreadAttrDestroy(&attr) == SCE_KERNEL_ERROR_EINVAL); +} diff --git a/core/libs/tests/GuestPthreadBarrier.cpp b/core/libs/tests/GuestPthreadBarrier.cpp index 64d67e201..6686ebc01 100644 --- a/core/libs/tests/GuestPthreadBarrier.cpp +++ b/core/libs/tests/GuestPthreadBarrier.cpp @@ -42,6 +42,13 @@ static void* APS5_VABI Waiter(void* arg) { return nullptr; } +static void* APS5_VABI DestroyingWaiter(void* arg) { + auto& context = *static_cast(arg); + if (pthread_barrier_wait_nid_postfix(&context.barrier) == BARRIER_SERIAL_THREAD) + context.serial = pthread_barrier_destroy_nid_postfix(&context.barrier) == 0 && context.barrier == nullptr ? 1 : -1; + return nullptr; +} + static void RunWaiters(Context& context) { std::array threads{}; for (auto& thread : threads) Require(scePthreadCreate(&thread, nullptr, Waiter, &context, nullptr) == 0); @@ -101,6 +108,15 @@ int main() { RunWaiters(withoutAttr); Require(pthread_barrier_destroy_nid_postfix(&withoutAttr.barrier) == 0); + for (int round = 0; round < 50; ++round) { + Context destroyed; + Require(pthread_barrier_init_nid_postfix(&destroyed.barrier, nullptr, WAITERS) == 0); + std::array threads{}; + for (auto& thread : threads) Require(scePthreadCreate(&thread, nullptr, DestroyingWaiter, &destroyed, nullptr) == 0); + for (auto& thread : threads) Require(scePthreadJoin(thread, nullptr) == 0); + Require(destroyed.serial == 1); + } + GuestBarrier* single = nullptr; Require(pthread_barrierattr_init_nid_postfix(&attr) == 0); Require(pthread_barrier_init_nid_postfix(&single, &attr, 1) == 0); diff --git a/core/libs/tests/GuestPthreadJoinSelf.cpp b/core/libs/tests/GuestPthreadJoinSelf.cpp new file mode 100644 index 000000000..742fa9d73 --- /dev/null +++ b/core/libs/tests/GuestPthreadJoinSelf.cpp @@ -0,0 +1,39 @@ +#include "SceTypes.hpp" +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +Pthread APS5_VABI scePthreadSelf(void); +int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** retval); +} + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EDEADLK = static_cast(0x8002000Bu); +static constexpr int GUEST_EDEADLK = 11; + +static void Require(bool value) { if (!value) std::abort(); } + +static void* APS5_VABI JoinSelf(void*) { + void* result = reinterpret_cast(std::uintptr_t{0x55}); + Require(scePthreadJoin(scePthreadSelf(), &result) == SCE_KERNEL_ERROR_EDEADLK); + Require(pthread_join_nid_postfix(scePthreadSelf(), &result) == GUEST_EDEADLK); + Require(result == reinterpret_cast(std::uintptr_t{0x55})); + return reinterpret_cast(std::uintptr_t{0x1234}); +} + +int main() { + Pthread thread = nullptr; + void* result = nullptr; + Require(scePthreadCreate(&thread, nullptr, JoinSelf, nullptr, "join-self") == SCE_OK); + Require(scePthreadJoin(thread, &result) == SCE_OK); + Require(result == reinterpret_cast(std::uintptr_t{0x1234})); + + PthreadAttr unset = nullptr; + thread = nullptr; + result = nullptr; + Require(scePthreadCreate(&thread, &unset, JoinSelf, nullptr, "unset-attr") == SCE_OK); + Require(scePthreadJoin(thread, &result) == SCE_OK); + Require(result == reinterpret_cast(std::uintptr_t{0x1234})); +} diff --git a/core/libs/tests/GuestPthreadKey.cpp b/core/libs/tests/GuestPthreadKey.cpp index e9bdd1653..ae87651a6 100644 --- a/core/libs/tests/GuestPthreadKey.cpp +++ b/core/libs/tests/GuestPthreadKey.cpp @@ -10,6 +10,8 @@ int APS5_VABI scePthreadKeyCreate(PthreadKey* key, pthread_key_destructor_func_t int APS5_VABI scePthreadKeyDelete(PthreadKey key); void* APS5_VABI scePthreadGetspecific(PthreadKey key); int APS5_VABI scePthreadSetspecific(PthreadKey key, void* value); +int APS5_VABI pthread_key_create_nid_postfix(PthreadKey* key, pthread_key_destructor_func_t destructor); +int APS5_VABI pthread_key_delete_nid_postfix(PthreadKey key); } static constexpr int SCE_OK = 0; @@ -62,8 +64,16 @@ static void* APS5_VABI HoldAcrossReuse(void*) { } int main() { + PthreadKey posixKey = 0; + Require(pthread_key_create_nid_postfix(&posixKey, nullptr) == 0 && posixKey != 0); + Require(scePthreadSetspecific(0, &first) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadGetspecific(0) == nullptr); + Require(scePthreadKeyDelete(0) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_key_delete_nid_postfix(posixKey) == 0); + PthreadKey key = -1; Require(scePthreadKeyCreate(&key, nullptr) == SCE_OK); + Require(key != 0); Require(scePthreadGetspecific(key) == nullptr); Require(scePthreadSetspecific(key, &first) == SCE_OK); Require(scePthreadGetspecific(key) == &first); diff --git a/core/libs/tests/GuestPthreadMutexErrors.cpp b/core/libs/tests/GuestPthreadMutexErrors.cpp new file mode 100644 index 000000000..064469d42 --- /dev/null +++ b/core/libs/tests/GuestPthreadMutexErrors.cpp @@ -0,0 +1,50 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr); +int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr); +int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type); +int APS5_VABI scePthreadMutexattrSetprotocol(PthreadMutexattr* attr, int protocol); +int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name); +int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); +} + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016u); +static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010u); + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + PthreadMutexattr attr = nullptr; + Require(scePthreadMutexattrInit(&attr) == SCE_OK); + for (int type : {-1, 0, 5}) Require(scePthreadMutexattrSettype(&attr, type) == SCE_KERNEL_ERROR_EINVAL); + for (int type : {1, 2, 3, 4}) Require(scePthreadMutexattrSettype(&attr, type) == SCE_OK); + for (int protocol : {-1, 3}) Require(scePthreadMutexattrSetprotocol(&attr, protocol) == SCE_KERNEL_ERROR_EINVAL); + for (int protocol : {0, 1, 2}) Require(scePthreadMutexattrSetprotocol(&attr, protocol) == SCE_OK); + + PthreadMutexattr unset = nullptr; + Require(scePthreadMutexattrSettype(&unset, 1) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrSetprotocol(&unset, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrDestroy(&unset) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrSettype(nullptr, 1) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrDestroy(nullptr) == SCE_KERNEL_ERROR_EINVAL); + + PthreadMutex mutex = nullptr; + Require(scePthreadMutexInit(&mutex, &unset, "unset") == SCE_OK); + Require(scePthreadMutexLock(&mutex) == SCE_OK); + Require(scePthreadMutexDestroy(&mutex) == SCE_KERNEL_ERROR_EBUSY); + Require(scePthreadMutexUnlock(&mutex) == SCE_OK); + Require(scePthreadMutexDestroy(&mutex) == SCE_OK); + Require(mutex == reinterpret_cast(std::uintptr_t{2})); + Require(scePthreadMutexDestroy(&mutex) == SCE_KERNEL_ERROR_EINVAL); + + Require(scePthreadMutexInit(&mutex, &attr, "attr") == SCE_OK); + Require(scePthreadMutexDestroy(&mutex) == SCE_OK); + Require(scePthreadMutexattrDestroy(&attr) == SCE_OK); +} diff --git a/core/libs/tests/GuestPthreadMutexattr.cpp b/core/libs/tests/GuestPthreadMutexattr.cpp index 410fb354e..410aa02ba 100644 --- a/core/libs/tests/GuestPthreadMutexattr.cpp +++ b/core/libs/tests/GuestPthreadMutexattr.cpp @@ -1,4 +1,5 @@ #include "SceTypes.hpp" +#include #include #include @@ -14,6 +15,9 @@ int APS5_VABI pthread_mutex_init_nid_postfix(PthreadMutex* mutex, const PthreadM int APS5_VABI pthread_mutex_lock_nid_postfix(PthreadMutex* mutex); int APS5_VABI pthread_mutex_unlock_nid_postfix(PthreadMutex* mutex); int APS5_VABI pthread_mutex_destroy_nid_postfix(PthreadMutex* mutex); +int APS5_VABI pthread_mutex_timedlock_nid_postfix(PthreadMutex* mutex, const KernelTimespec* abstime); +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); int APS5_VABI sched_get_priority_max_nid_postfix(int policy); } @@ -35,6 +39,25 @@ static bool Throws(Call call) { return false; } +static constexpr KernelTimespec INVALID_DEADLINE{0, 1000000000}; + +static void* APS5_VABI TimedlockInvalidDeadline(void* arg) { + auto* mutex = static_cast(arg); + return reinterpret_cast(static_cast(pthread_mutex_timedlock_nid_postfix(mutex, &INVALID_DEADLINE))); +} + +static void CheckTimedlockInvalidDeadline() { + PthreadMutex mutex = nullptr; + Require(pthread_mutex_init_nid_postfix(&mutex, nullptr) == 0); + Require(pthread_mutex_timedlock_nid_postfix(&mutex, &INVALID_DEADLINE) == 0); + Pthread thread{}; + void* result = nullptr; + Require(scePthreadCreate(&thread, nullptr, TimedlockInvalidDeadline, &mutex, nullptr) == 0); + Require(scePthreadJoin(thread, &result) == 0 && reinterpret_cast(result) == GUEST_EINVAL); + Require(pthread_mutex_unlock_nid_postfix(&mutex) == 0); + Require(pthread_mutex_destroy_nid_postfix(&mutex) == 0); +} + static void CheckType() { PthreadMutexattr attr = nullptr; Require(pthread_mutexattr_init_nid_postfix(&attr) == 0); @@ -55,6 +78,7 @@ static void CheckType() { int main() { CheckType(); + CheckTimedlockInvalidDeadline(); PthreadMutexattr attr = nullptr; Require(pthread_mutexattr_init_nid_postfix(&attr) == 0); int ceiling = -1; diff --git a/core/libs/tests/GuestPthreadRwlock.cpp b/core/libs/tests/GuestPthreadRwlock.cpp index ff27c538e..503e2dc69 100644 --- a/core/libs/tests/GuestPthreadRwlock.cpp +++ b/core/libs/tests/GuestPthreadRwlock.cpp @@ -23,12 +23,16 @@ int APS5_VABI pthread_rwlock_timedrdlock_nid_postfix(PthreadRwlock* rwlock, cons int APS5_VABI pthread_rwlock_timedwrlock_nid_postfix(PthreadRwlock* rwlock, const KernelTimespec* abstime); int APS5_VABI scePthreadRwlockInit(PthreadRwlock* rwlock, const PthreadRwlockattr* attr, const char* name); int APS5_VABI scePthreadRwlockDestroy(PthreadRwlock* rwlock); +int APS5_VABI scePthreadRwlockRdlock(PthreadRwlock* rwlock); +int APS5_VABI scePthreadRwlockWrlock(PthreadRwlock* rwlock); +int APS5_VABI scePthreadRwlockTryrdlock(PthreadRwlock* rwlock); +int APS5_VABI scePthreadRwlockUnlock(PthreadRwlock* rwlock); } using TimedLock = int (APS5_VABI *)(PthreadRwlock*, const KernelTimespec*); static constexpr int SCE_OK = 0; -static constexpr int SCE_KERNEL_ERROR_EBUSY = 0x80020010; +static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; static constexpr int GUEST_EPERM = 1; static constexpr int GUEST_EDEADLK = 11; static constexpr int GUEST_EBUSY = 16; @@ -39,6 +43,8 @@ static constexpr std::int64_t NANOS_PER_SECOND = 1000000000; static void Require(bool value) { if (!value) std::abort(); } +static const PthreadRwlock DestroyedRwlock = reinterpret_cast(std::uintptr_t{1}); + static KernelTimespec After(std::int64_t millis) { KernelTimespec now{}; Require(clock_gettime_nid_postfix(GUEST_REALTIME_CLOCK, &now) == 0); @@ -151,19 +157,44 @@ static void CheckWriterHandoff() { Require(scePthreadCreate(&state.firstThread, nullptr, HandoffFirst, &state, nullptr) == SCE_OK); Require(scePthreadCreate(&state.secondThread, nullptr, HandoffSecond, &state, nullptr) == SCE_OK); while (!state.firstHeld.load()) std::this_thread::yield(); - Require(pthread_rwlock_destroy_nid_postfix(&state.rwlock) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&state.rwlock) == SCE_KERNEL_ERROR_EBUSY); Require(state.rwlock != nullptr); state.firstRelease.store(true); Require(scePthreadJoin(state.firstThread, nullptr) == SCE_OK); while (!state.secondAcquired.load()) std::this_thread::yield(); - Require(pthread_rwlock_destroy_nid_postfix(&state.rwlock) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&state.rwlock) == SCE_KERNEL_ERROR_EBUSY); Require(state.rwlock != nullptr); state.secondRelease.store(true); Require(scePthreadJoin(state.secondThread, nullptr) == SCE_OK); Require(pthread_rwlock_destroy_nid_postfix(&state.rwlock) == 0); - Require(state.rwlock == nullptr); + Require(state.rwlock == DestroyedRwlock); +} + +static void CheckDestroyLifecycle() { + PthreadRwlock held = nullptr; + Require(scePthreadRwlockInit(&held, nullptr, nullptr) == SCE_OK); + Require(scePthreadRwlockWrlock(&held) == SCE_OK); + Require(scePthreadRwlockDestroy(&held) == SCE_OK); + Require(held == DestroyedRwlock); + Require(scePthreadRwlockUnlock(&held) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadRwlockRdlock(&held) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadRwlockTryrdlock(&held) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadRwlockDestroy(&held) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_rwlock_rdlock_nid_postfix(&held) == GUEST_EINVAL); + Require(held == DestroyedRwlock); + Require(scePthreadRwlockInit(&held, nullptr, nullptr) == SCE_OK); + Require(held != nullptr && held != DestroyedRwlock); + Require(scePthreadRwlockRdlock(&held) == SCE_OK); + Require(scePthreadRwlockRdlock(&held) == SCE_OK); + Require(scePthreadRwlockDestroy(&held) == SCE_OK); + Require(held == DestroyedRwlock); + + PthreadRwlock zero = nullptr; + Require(scePthreadRwlockDestroy(&zero) == SCE_OK); + Require(zero == nullptr); + Require(scePthreadRwlockRdlock(&zero) == SCE_OK); + Require(zero != nullptr && zero != DestroyedRwlock); + Require(scePthreadRwlockUnlock(&zero) == SCE_OK); + Require(scePthreadRwlockDestroy(&zero) == SCE_OK); + Require(zero == DestroyedRwlock); } static void CheckFailedLockAttempts() { @@ -172,27 +203,39 @@ static void CheckFailedLockAttempts() { const KernelTimespec timeout = After(10); Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, &timeout) == GUEST_EDEADLK); Require(pthread_rwlock_tryrdlock_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&rwlock) == SCE_KERNEL_ERROR_EBUSY); Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); - Require(rwlock == nullptr); + Require(rwlock == DestroyedRwlock); + rwlock = nullptr; Holder reader{&rwlock, false}; Start(reader); const KernelTimespec past = After(-1000); Require(pthread_rwlock_timedwrlock_nid_postfix(&rwlock, &past) == GUEST_ETIMEDOUT); Require(pthread_rwlock_trywrlock_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&rwlock) == SCE_KERNEL_ERROR_EBUSY); reader.release.store(true); Require(scePthreadJoin(reader.thread, nullptr) == SCE_OK); Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); - Require(rwlock == nullptr); + Require(rwlock == DestroyedRwlock); +} + +static void CheckDeadlinesBeyondTheClockRange() { + PthreadRwlock rwlock = nullptr; + Holder reader{&rwlock, false}; + Start(reader); + const KernelTimespec farPast{INT64_MIN, 0}; + Require(pthread_rwlock_timedwrlock_nid_postfix(&rwlock, &farPast) == GUEST_ETIMEDOUT); + reader.release.store(true); + const KernelTimespec farFuture{INT64_MAX, 0}; + Require(pthread_rwlock_timedwrlock_nid_postfix(&rwlock, &farFuture) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + Require(scePthreadJoin(reader.thread, nullptr) == SCE_OK); + Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); } int main() { CheckConcurrentFirstWrlockExcludes(); + CheckDeadlinesBeyondTheClockRange(); const KernelTimespec invalid{0, NANOS_PER_SECOND}; PthreadRwlock fresh[4] = {}; Require(pthread_rwlock_tryrdlock_nid_postfix(&fresh[0]) == 0); @@ -238,8 +281,6 @@ int main() { Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, &invalid) == 0); Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); Require(pthread_rwlock_trywrlock_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&rwlock) == SCE_KERNEL_ERROR_EBUSY); ExpectTimeout(pthread_rwlock_timedwrlock_nid_postfix, &rwlock); deadline = After(5000); reader.release.store(true); @@ -252,8 +293,6 @@ int main() { Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == GUEST_EPERM); Require(pthread_rwlock_tryrdlock_nid_postfix(&rwlock) == GUEST_EBUSY); Require(pthread_rwlock_trywrlock_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&rwlock) == SCE_KERNEL_ERROR_EBUSY); ExpectTimeout(pthread_rwlock_timedrdlock_nid_postfix, &rwlock); ExpectTimeout(pthread_rwlock_timedwrlock_nid_postfix, &rwlock); deadline = After(5000); @@ -268,58 +307,51 @@ int main() { catch (const std::runtime_error&) { rejected = true; } Require(rejected); Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); - Require(rwlock == nullptr); + Require(rwlock == DestroyedRwlock); PthreadRwlock unheld = nullptr; Require(scePthreadRwlockInit(&unheld, nullptr, nullptr) == SCE_OK); Require(unheld != nullptr); Require(pthread_rwlock_destroy_nid_postfix(&unheld) == 0); - Require(unheld == nullptr); + Require(unheld == DestroyedRwlock); PthreadRwlock rd = nullptr; Require(pthread_rwlock_rdlock_nid_postfix(&rd) == 0); - Require(pthread_rwlock_destroy_nid_postfix(&rd) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&rd) == SCE_KERNEL_ERROR_EBUSY); - Require(rd != nullptr); - Require(pthread_rwlock_unlock_nid_postfix(&rd) == 0); - Require(pthread_rwlock_tryrdlock_nid_postfix(&rd) == 0); - Require(pthread_rwlock_unlock_nid_postfix(&rd) == 0); Require(pthread_rwlock_destroy_nid_postfix(&rd) == 0); - Require(rd == nullptr); + Require(rd == DestroyedRwlock); + Require(pthread_rwlock_unlock_nid_postfix(&rd) == GUEST_EINVAL); + Require(pthread_rwlock_tryrdlock_nid_postfix(&rd) == GUEST_EINVAL); + Require(pthread_rwlock_destroy_nid_postfix(&rd) == GUEST_EINVAL); + Require(rd == DestroyedRwlock); PthreadRwlock wr = nullptr; Require(pthread_rwlock_wrlock_nid_postfix(&wr) == 0); - Require(pthread_rwlock_destroy_nid_postfix(&wr) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&wr) == SCE_KERNEL_ERROR_EBUSY); - Require(wr != nullptr); - Require(pthread_rwlock_unlock_nid_postfix(&wr) == 0); - Require(pthread_rwlock_trywrlock_nid_postfix(&wr) == 0); - Require(pthread_rwlock_unlock_nid_postfix(&wr) == 0); Require(pthread_rwlock_destroy_nid_postfix(&wr) == 0); - Require(wr == nullptr); + Require(wr == DestroyedRwlock); + Require(pthread_rwlock_unlock_nid_postfix(&wr) == GUEST_EINVAL); + Require(pthread_rwlock_trywrlock_nid_postfix(&wr) == GUEST_EINVAL); + Require(pthread_rwlock_destroy_nid_postfix(&wr) == GUEST_EINVAL); + Require(wr == DestroyedRwlock); PthreadRwlock remoteRd = nullptr; Holder bgReader{&remoteRd, false}; Start(bgReader); - Require(pthread_rwlock_destroy_nid_postfix(&remoteRd) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&remoteRd) == SCE_KERNEL_ERROR_EBUSY); Require(remoteRd != nullptr); bgReader.release.store(true); Require(scePthreadJoin(bgReader.thread, nullptr) == SCE_OK); Require(pthread_rwlock_destroy_nid_postfix(&remoteRd) == 0); - Require(remoteRd == nullptr); + Require(remoteRd == DestroyedRwlock); PthreadRwlock remoteWr = nullptr; Holder bgWriter{&remoteWr, true}; Start(bgWriter); - Require(pthread_rwlock_destroy_nid_postfix(&remoteWr) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&remoteWr) == SCE_KERNEL_ERROR_EBUSY); Require(remoteWr != nullptr); bgWriter.release.store(true); Require(scePthreadJoin(bgWriter.thread, nullptr) == SCE_OK); Require(pthread_rwlock_destroy_nid_postfix(&remoteWr) == 0); - Require(remoteWr == nullptr); + Require(remoteWr == DestroyedRwlock); CheckWriterHandoff(); CheckFailedLockAttempts(); + CheckDestroyLifecycle(); } diff --git a/core/libs/tests/GuestPthreadSpin.cpp b/core/libs/tests/GuestPthreadSpin.cpp new file mode 100644 index 000000000..9783df2dd --- /dev/null +++ b/core/libs/tests/GuestPthreadSpin.cpp @@ -0,0 +1,122 @@ +#include "SceTypes.hpp" +#include +#include + +struct PthreadSpinlockPrivate; +using PthreadSpinlock = PthreadSpinlockPrivate*; + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI pthread_spin_init_nid_postfix(PthreadSpinlock* lock, int pshared); +int APS5_VABI pthread_spin_destroy_nid_postfix(PthreadSpinlock* lock); +int APS5_VABI pthread_spin_lock_nid_postfix(PthreadSpinlock* lock); +int APS5_VABI pthread_spin_trylock_nid_postfix(PthreadSpinlock* lock); +int APS5_VABI pthread_spin_unlock_nid_postfix(PthreadSpinlock* lock); +} + +static constexpr int GUEST_EPERM = 1; +static constexpr int GUEST_EBUSY = 16; +static constexpr int GUEST_EINVAL = 22; +static constexpr int PTHREAD_PROCESS_PRIVATE = 0; +static constexpr int PTHREAD_PROCESS_SHARED = 1; +static constexpr int Increments = 200000; + +static void Require(bool value) { if (!value) std::abort(); } + +struct Shared { + PthreadSpinlock lock = nullptr; + long counter = 0; + int trylock = -1; + int unlock = -1; +}; + +static void* APS5_VABI Contender(void* arg) { + auto& shared = *static_cast(arg); + shared.trylock = pthread_spin_trylock_nid_postfix(&shared.lock); + shared.unlock = pthread_spin_unlock_nid_postfix(&shared.lock); + return nullptr; +} + +static void* APS5_VABI Incrementer(void* arg) { + auto& shared = *static_cast(arg); + for (int i = 0; i < Increments; ++i) { + Require(pthread_spin_lock_nid_postfix(&shared.lock) == 0); + ++shared.counter; + Require(pthread_spin_unlock_nid_postfix(&shared.lock) == 0); + } + return nullptr; +} + +static void ArgumentErrors() { + Require(pthread_spin_init_nid_postfix(nullptr, PTHREAD_PROCESS_PRIVATE) == GUEST_EINVAL); + PthreadSpinlock lock = nullptr; + Require(pthread_spin_init_nid_postfix(&lock, 2) == GUEST_EINVAL); + Require(pthread_spin_init_nid_postfix(&lock, -1) == GUEST_EINVAL); + Require(lock == nullptr); + Require(pthread_spin_lock_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_trylock_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_unlock_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_destroy_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_lock_nid_postfix(&lock) == GUEST_EINVAL); + Require(pthread_spin_trylock_nid_postfix(&lock) == GUEST_EINVAL); + Require(pthread_spin_unlock_nid_postfix(&lock) == GUEST_EINVAL); + Require(pthread_spin_destroy_nid_postfix(&lock) == GUEST_EINVAL); +} + +static void SingleThread(int pshared) { + PthreadSpinlock lock = nullptr; + Require(pthread_spin_init_nid_postfix(&lock, pshared) == 0); + Require(lock != nullptr); + Require(pthread_spin_unlock_nid_postfix(&lock) == GUEST_EPERM); + Require(pthread_spin_lock_nid_postfix(&lock) == 0); + Require(pthread_spin_trylock_nid_postfix(&lock) == GUEST_EBUSY); + bool threw = false; + try { + pthread_spin_lock_nid_postfix(&lock); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + Require(pthread_spin_unlock_nid_postfix(&lock) == 0); + Require(pthread_spin_unlock_nid_postfix(&lock) == GUEST_EPERM); + Require(pthread_spin_trylock_nid_postfix(&lock) == 0); + Require(pthread_spin_unlock_nid_postfix(&lock) == 0); + Require(pthread_spin_destroy_nid_postfix(&lock) == 0); + Require(lock == nullptr); + Require(pthread_spin_destroy_nid_postfix(&lock) == GUEST_EINVAL); +} + +static void HeldByAnotherThread() { + Shared shared; + Require(pthread_spin_init_nid_postfix(&shared.lock, PTHREAD_PROCESS_PRIVATE) == 0); + Require(pthread_spin_lock_nid_postfix(&shared.lock) == 0); + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Contender, &shared, nullptr) == 0); + Require(scePthreadJoin(thread, nullptr) == 0); + Require(shared.trylock == GUEST_EBUSY); + Require(shared.unlock == GUEST_EPERM); + Require(pthread_spin_unlock_nid_postfix(&shared.lock) == 0); + Require(pthread_spin_destroy_nid_postfix(&shared.lock) == 0); +} + +static void MutualExclusion() { + Shared shared; + Require(pthread_spin_init_nid_postfix(&shared.lock, PTHREAD_PROCESS_PRIVATE) == 0); + Pthread first = nullptr; + Pthread second = nullptr; + Require(scePthreadCreate(&first, nullptr, Incrementer, &shared, nullptr) == 0); + Require(scePthreadCreate(&second, nullptr, Incrementer, &shared, nullptr) == 0); + Require(scePthreadJoin(first, nullptr) == 0); + Require(scePthreadJoin(second, nullptr) == 0); + Require(shared.counter == 2L * Increments); + Require(pthread_spin_destroy_nid_postfix(&shared.lock) == 0); +} + +int main() { + ArgumentErrors(); + SingleThread(PTHREAD_PROCESS_PRIVATE); + SingleThread(PTHREAD_PROCESS_SHARED); + HeldByAnotherThread(); + MutualExclusion(); +} diff --git a/core/libs/tests/GuestResolver.cpp b/core/libs/tests/GuestResolver.cpp index 33bbed3d4..20f02e1d1 100644 --- a/core/libs/tests/GuestResolver.cpp +++ b/core/libs/tests/GuestResolver.cpp @@ -113,8 +113,13 @@ int main() { Require(getnameinfo_nid_postfix(v4.data(), 2, host, sizeof(host), nullptr, 0, 10) == 5); Require(getnameinfo_nid_postfix(v4.data(), v4.size(), host, sizeof(host), nullptr, 0, 32) == 3); Require(getnameinfo_nid_postfix(v4.data(), v4.size(), host, 1, nullptr, 0, 10) == 14); - Require(std::strlen(gai_strerror_nid_postfix(14)) > 0); - Require(std::strstr(gai_strerror_nid_postfix(-1), "Unknown") != nullptr); + Require(std::strcmp(gai_strerror_nid_postfix(0), "Success") == 0); + Require(std::strcmp(gai_strerror_nid_postfix(3), "Invalid value for ai_flags") == 0); + Require(std::strcmp(gai_strerror_nid_postfix(8), "hostname nor servname provided, or not known") == 0); + Require(std::strcmp(gai_strerror_nid_postfix(10), "ai_socktype not supported") == 0); + Require(std::strcmp(gai_strerror_nid_postfix(14), "Argument buffer overflow") == 0); + Require(std::strcmp(gai_strerror_nid_postfix(-1), "Unknown error") == 0); + Require(std::strcmp(gai_strerror_nid_postfix(15), "Unknown error") == 0); GuestResolver::AddressInfo hints{}; hints.flags = 12; // numeric host and service: no external DNS dependency hints.socketType = 1; diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 252649a32..d82c887ad 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -299,6 +299,35 @@ int main() { Require(tick.tick == dateTime.local - static_cast(dateTime.minutes * 60000000)); } + const struct { + const char* name; + std::int64_t minutes; + } additionalZones[] = { + {"SWT", 60}, {"MEZ", 60}, {"WETDST", 60}, {"METDST", 120}, {"EETDST", 180}, + {"AESST", 660}, {"ACSST", 630}, {"CAST", 570}, {"CADT", 630}, {"SAST", 570}, {"SADT", 630}, {"WST", 480}, {"WDT", 540}, + }; + for (const auto& zone : additionalZones) { + char text[80]; + std::snprintf(text, sizeof(text), "Thu, 29 Feb 2024 12:34:56 %s", zone.name); + tick.tick = untouched; + Require(sceRtcParseDateTime(&tick, text) == 0); + Require(tick.tick == leapDayTick - 789000ull - static_cast(zone.minutes * 60000000)); + char lower[16]{}; + for (std::size_t i = 0; zone.name[i] != 0; ++i) lower[i] = static_cast(zone.name[i] + ('a' - 'A')); + std::snprintf(text, sizeof(text), "Thu, 29 Feb 2024 12:34:56 %s", lower); + tick.tick = untouched; + Require(sceRtcParseDateTime(&tick, text) == 0); + Require(tick.tick == leapDayTick - 789000ull - static_cast(zone.minutes * 60000000)); + std::snprintf(text, sizeof(text), "Thu, 29 Feb 2024 12:34:56 %sextra", zone.name); + tick.tick = untouched; + Require(sceRtcParseDateTime(&tick, text) == 0); + Require(tick.tick == leapDayTick - 789000ull - static_cast(zone.minutes * 60000000)); + std::snprintf(text, sizeof(text), "Wed, 01 Mar 2000 08:09:10 %s", zone.name); + tick.tick = untouched; + Require(sceRtcParseDateTime(&tick, text) == 0); + Require(tick.tick == at(2000, 3, 1, 8, 9, 10) - static_cast(zone.minutes * 60000000)); + } + RtcTick source{leapDayTick}; RtcTick result{}; Require(sceRtcTickAddSeconds(&result, &source, 3600) == 0 && result.tick == leapDayTick + 3600000000ull); diff --git a/core/libs/tests/GuestRudpPump.cpp b/core/libs/tests/GuestRudpPump.cpp index 36cf63cb4..62a2bd9c8 100644 --- a/core/libs/tests/GuestRudpPump.cpp +++ b/core/libs/tests/GuestRudpPump.cpp @@ -11,7 +11,7 @@ int APS5_VABI sceRudpInit_nid_postfix(void*, int); int APS5_VABI sceRudpTerminate(); int APS5_VABI sceRudpProcessEvents(); int APS5_VABI sceRudpNetFlush(); -int APS5_VABI sceRudpGetMaxSegmentSize(); +int APS5_VABI sceRudpGetMaxSegmentSize(std::uint16_t*); int APS5_VABI sceRudpNetReceived(); } @@ -25,23 +25,36 @@ static void Require(bool value, int line) { int main() { constexpr int notInitialized = static_cast(0x80770001); + constexpr int invalidArgument = static_cast(0x80770004); + std::uint16_t segmentSize = 0x5a5a; CHECK(sceRudpProcessEvents() == notInitialized); CHECK(sceRudpNetFlush() == notInitialized); + CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == notInitialized); + CHECK(segmentSize == 0x5a5a); + CHECK(sceRudpGetMaxSegmentSize(nullptr) == notInitialized); alignas(8) static unsigned char pool[0xC58]; CHECK(sceRudpInit_nid_postfix(pool, sizeof(pool)) == 0); CHECK(sceRudpProcessEvents() == 0); CHECK(sceRudpNetFlush() == 0); + CHECK(sceRudpGetMaxSegmentSize(nullptr) == invalidArgument); + CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == 0); + CHECK(segmentSize == 1410); + std::uint16_t segment[3] = {0, 0, 0x5a5a}; + CHECK(sceRudpGetMaxSegmentSize(&segment[1]) == 0); + CHECK(segment[0] == 0 && segment[1] == 1410 && segment[2] == 0x5a5a); CHECK(sceRudpTerminate() == 0); - for (const auto call : {&sceRudpNetReceived, &sceRudpGetMaxSegmentSize}) { - bool threw = false; - try { - call(); - } catch (const std::runtime_error&) { - threw = true; - } - CHECK(threw); + segmentSize = 0x5a5a; + CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == notInitialized); + CHECK(segmentSize == 0x5a5a); + + bool threw = false; + try { + sceRudpNetReceived(); + } catch (const std::runtime_error&) { + threw = true; } + CHECK(threw); return 0; } diff --git a/core/libs/tests/GuestSaveDataConvert.cpp b/core/libs/tests/GuestSaveDataConvert.cpp new file mode 100644 index 000000000..8df9b84da --- /dev/null +++ b/core/libs/tests/GuestSaveDataConvert.cpp @@ -0,0 +1,73 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceSaveDataConvert(const SaveDataConvert*); +int APS5_VABI sceSaveDataInitialize3(const void*); +} + +namespace { + +constexpr int SaveDataErrorParameter = -2137063424; +constexpr int SaveDataErrorNotFound = -2137063416; + +int failures = 0; + +void Check(bool condition, const std::string& what) { + if (condition) return; + std::fprintf(stderr, "savedata convert check failed: %s\n", what.c_str()); + ++failures; +} + +int Convert(const char* data, std::size_t size) { + SceSaveDataDirName name{}; + std::memcpy(name.data, data, size); + SaveDataConvert convert{}; + convert.user_id = 0x10000000; + convert.dir_name = &name; + return sceSaveDataConvert(&convert); +} + +std::vector Read(const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + return {std::istreambuf_iterator(stream), std::istreambuf_iterator()}; +} + +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / ("anyps5-savedata-convert-" + std::to_string(std::random_device{}())); + const auto work = root / "work"; + const auto kept = work / "_sd" / "kept" / "data.bin"; + std::filesystem::create_directories(kept.parent_path()); + std::ofstream(kept) << "data"; + const auto previous = std::filesystem::current_path(); + std::filesystem::current_path(work); + + Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes"); + Check(Convert("kept", sizeof("kept")) == 0, "an existing save converts"); + Check(Read(kept) == std::vector{'d', 'a', 't', 'a'}, "the converted save keeps its files"); + Check(Convert("missing", sizeof("missing")) == SaveDataErrorNotFound, "a missing save is not found"); + for (const char* invalid : {"../kept", "", ".", "..", "a\\b", "c:d"}) { + Check(Convert(invalid, std::strlen(invalid) + 1) == SaveDataErrorParameter, std::string("\"") + invalid + "\" is rejected"); + } + char unterminated[sizeof(SceSaveDataDirName::data)]; + std::memset(unterminated, 'a', sizeof(unterminated)); + Check(Convert(unterminated, sizeof(unterminated)) == SaveDataErrorParameter, "an unterminated name is rejected"); + + std::filesystem::current_path(previous); + std::error_code error; + std::filesystem::remove_all(root, error); + if (failures != 0) return 1; + std::printf("savedata convert tests passed\n"); + return 0; +} diff --git a/core/libs/tests/GuestSaveDataSearch.cpp b/core/libs/tests/GuestSaveDataSearch.cpp index cc24049af..601783fe8 100644 --- a/core/libs/tests/GuestSaveDataSearch.cpp +++ b/core/libs/tests/GuestSaveDataSearch.cpp @@ -8,17 +8,79 @@ #include #include #include +#include extern "C" { int APS5_VABI sceSaveDataDirNameSearch(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); int APS5_VABI sceSaveDataMount3(const SaveDataMount3*, SaveDataMountResult*); int APS5_VABI sceSaveDataUmount2(std::uint32_t, const SaveDataMountPoint*); +int APS5_VABI sceSaveDataSetParam(const SaveDataMountPoint*, std::uint32_t, const void*, std::size_t); } +static constexpr int SaveDataErrorParameter = -2137063424; + static void Require(bool value, const char* message) { if (!value) throw std::runtime_error(message); } +static std::vector Search(std::uint32_t key, std::uint32_t order, std::uint32_t capacity, std::uint32_t* hits) { + SceSaveDataDirName pattern{}; + std::snprintf(pattern.data, sizeof(pattern.data), "SORT_%%"); + SaveDataDirNameSearchCond cond{}; + cond.user_id = 1; + cond.dir_name = &pattern; + cond.key = key; + cond.order = order; + std::array names{}; + SaveDataDirNameSearchResult result{}; + result.dir_names = names.data(); + result.dir_names_num = capacity; + Require(sceSaveDataDirNameSearch(&cond, &result) == 0, "sorted search failed"); + *hits = result.hit_num; + std::vector found; + for (std::uint32_t i = 0; i < result.set_num; ++i) found.emplace_back(names[i].data); + return found; +} + +static void SortByKey() { + struct Save { + const char* name; + std::uint32_t userParam; + std::int64_t mtime; + }; + const std::array saves{{{"SORT_B", 1, 1000}, {"SORT_A", 3, 2000}, {"SORT_C", 2, 3000}}}; + for (const auto& save : saves) { + std::filesystem::create_directories(std::filesystem::path("_sd") / save.name); + SceSaveDataDirName dirName{}; + std::snprintf(dirName.data, sizeof(dirName.data), "%s", save.name); + SaveDataMount3 mount{}; + mount.user_id = 1; + mount.dir_name = &dirName; + mount.mount_mode = 2; + SaveDataMountResult mounted{}; + Require(sceSaveDataMount3(&mount, &mounted) == 0, "mount for parameters failed"); + Require(sceSaveDataSetParam(&mounted.mount_point, 4, &save.userParam, sizeof(save.userParam)) == 0, "user parameter was not set"); + Require(sceSaveDataSetParam(&mounted.mount_point, 5, &save.mtime, sizeof(save.mtime)) == 0, "mtime was not set"); + Require(sceSaveDataUmount2(0, &mounted.mount_point) == 0, "unmount failed"); + } + using Names = std::vector; + std::uint32_t hits = 0; + Require(Search(0, 0, 3, &hits) == Names{"SORT_A", "SORT_B", "SORT_C"} && hits == 3, "directory names must sort ascending"); + Require(Search(0, 1, 3, &hits) == Names{"SORT_C", "SORT_B", "SORT_A"}, "directory names must sort descending"); + Require(Search(1, 0, 3, &hits) == Names{"SORT_B", "SORT_C", "SORT_A"}, "user parameters must sort ascending"); + Require(Search(1, 1, 3, &hits) == Names{"SORT_A", "SORT_C", "SORT_B"}, "user parameters must sort descending"); + Require(Search(3, 0, 3, &hits) == Names{"SORT_B", "SORT_A", "SORT_C"}, "modification times must sort ascending"); + Require(Search(3, 1, 1, &hits) == Names{"SORT_C"} && hits == 3, "the newest save must come first in descending modification time"); + SaveDataDirNameSearchCond cond{}; + cond.user_id = 1; + cond.key = 6; + SaveDataDirNameSearchResult result{}; + Require(sceSaveDataDirNameSearch(&cond, &result) == SaveDataErrorParameter, "an unknown sort key must be rejected"); + cond.key = 0; + cond.order = 2; + Require(sceSaveDataDirNameSearch(&cond, &result) == SaveDataErrorParameter, "an unknown sort order must be rejected"); +} + static void SearchAndMount() { const std::array names{std::string(31, 'A'), std::string(31, 'B')}; for (const auto& name : names) std::filesystem::create_directories(std::filesystem::path("_sd") / name); @@ -30,11 +92,23 @@ static void SearchAndMount() { SaveDataDirNameSearchResult result{}; result.dir_names = reinterpret_cast(buffer.data()); result.dir_names_num = 2; + alignas(SaveDataSearchInfo) std::array infos; + infos.fill(0x21); + result.infos = reinterpret_cast(infos.data()); Require(sceSaveDataDirNameSearch(&cond, &result) == 0, "search failed"); Require(result.hit_num == 2 && result.set_num == 2, "search must return both saves"); for (std::size_t i = 64; i < buffer.size(); ++i) { Require(buffer[i] == '!', "directory search overran two 32-byte guest records"); } + for (std::size_t i = 96; i < infos.size(); ++i) { + Require(infos[i] == 0x21, "directory search overran two 48-byte search infos"); + } + for (std::size_t i = 0; i < 2; ++i) { + SaveDataSearchInfo info{}; + std::memcpy(&info, infos.data() + i * 48, sizeof(info)); + Require(info.blocks == 32768 && info.free_blocks == 32768, "search info must report the save's blocks and free blocks"); + for (const auto byte : info.reserved) Require(byte == 0, "search info reserved bytes must be zero"); + } std::array found{}; for (int i = 0; i < 2; ++i) { const char* record = buffer.data() + i * 32; @@ -64,6 +138,7 @@ int main() { int status = 0; try { SearchAndMount(); + SortByKey(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); status = 1; diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 1579e6645..993e40205 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -56,6 +56,7 @@ int APS5_VABI sceNetCtlGetState(int*); int APS5_VABI select_nid_postfix(int, void*, void*, void*, const void*); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI close_nid_postfix(int); +int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceNetInetPton(int, const char*, void*); const char* APS5_VABI sceNetInetNtop(int, const void*, char*, std::uint32_t); @@ -148,6 +149,24 @@ static void CheckEmptyEpoll() { } } +static void CheckReceiveTimeout(int receiver, int sender, const std::array& address) { + const int timeout = 50000; + Require(sceNetSetsockopt(receiver, 0xffff, 0x1006, &timeout, sizeof(timeout)) == 0); + char buffer[8]{}; + std::array source{}; + std::uint32_t size = source.size(); + NetIovec in[] = {{buffer, sizeof(buffer)}}; + NetMsghdr receive{source.data(), static_cast(source.size()), in, 1, nullptr, 0, 0}; + Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), 0), 35)); + Require(Failed(sceNetRecvfrom(receiver, buffer, sizeof(buffer), 0, source.data(), &size), 35)); + Require(Failed(sceNetRecvmsg(receiver, &receive, 0), 35)); + const char datagram[] = "late"; + Require(sceNetSendto(sender, datagram, sizeof(datagram), 0, address.data(), address.size()) == sizeof(datagram)); + Require(sceNetRecv(receiver, buffer, sizeof(buffer), 0) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + const int disabled = 0; + Require(sceNetSetsockopt(receiver, 0xffff, 0x1006, &disabled, sizeof(disabled)) == 0); +} + static void CheckMessages(int receiver, int sender, const std::array& address) { char head[] = "scatter "; char tail[] = "gather"; @@ -311,6 +330,9 @@ static void CheckUnspecifiedIpv6() { Require(sceNetRecvfrom(receiver, received, sizeof(received), 0, peer.data(), &size) == sizeof(received)); Require(std::memcmp(received, payload, sizeof(payload)) == 0 && size == peer.size() && peer[1] == 28); Require(std::memcmp(peer.data() + 8, address.data() + 8, 16) == 0); + address[0] = 0; + Require(sceNetSendto(sender, payload, sizeof(payload), 0, address.data(), address.size()) == sizeof(payload)); + Require(sceNetRecvfrom(receiver, received, sizeof(received), 0, nullptr, nullptr) == sizeof(received)); Require(std::memcmp(sce_net_in6addr_any, unspecified.data(), unspecified.size()) == 0); Require(sceNetSocketClose(sender) == 0); Require(sceNetSocketClose(receiver) == 0); @@ -338,6 +360,15 @@ int main() { CheckLinkLocalAllNodesIpv6(); CheckUnspecifiedIpv6(); + const int closed = sceNetSocket("closed-by-kernel", 2, 2, 17); + Require(closed >= 0); + const int enabled = 1; + Require(setsockopt_nid_postfix(closed, 0xffff, 0x20, &enabled, sizeof(enabled)) == 0); + Require(sceNetSetsockopt(closed, 0xffff, 0x20, &enabled, sizeof(enabled)) == 0); + Require(sceNetSetsockopt(closed, 0xffff, 0x4, &enabled, sizeof(enabled)) == 0); + Require(close_nid_postfix(closed) == 0); + Require(Failed(sceNetSocketClose(closed), 9)); + const int listener = sceNetSocket(nullptr, 2, 1, 6); Require(listener >= 0); std::array address{16, 2, 0, 0, 127, 0, 0, 1}; @@ -400,6 +431,12 @@ int main() { Require(sceNetEpollWait(epoll, &ready, 1, 1000000) == 1 && (ready.events & 1) && ready.ident == static_cast(accepted)); Require(sceNetShutdown(accepted, 1) == 0); Require(Failed(sceNetSend(accepted, request, sizeof(request), 0), 32)); + registration.data.u32 = 78; + Require(sceNetEpollControl(epoll, 2, accepted, ®istration) == 0); + Require(Failed(sceNetEpollControl(epoll, 2, accepted, nullptr), 22)); + Require(sceNetEpollControl(epoll, 3, accepted, nullptr) == 0); + Require(Failed(sceNetEpollControl(epoll, 3, accepted, nullptr), 2)); + Require(Failed(sceNetEpollControl(epoll, 2, accepted, ®istration), 2)); Require(sceNetEpollDestroy(epoll) == 0); Require(sceNetSocketClose(accepted) == 0); bool send_failed = false; @@ -410,7 +447,9 @@ int main() { Require(send_failed); Require(sceNetSocketClose(client) == 0); Require(sceNetSocketClose(listener) == 0); + *__error_nid_postfix() = 0; Require(sceNetSocketClose(listener) == static_cast(0x80410109) && *sceNetErrnoLoc() == 9); + Require(*__error_nid_postfix() == 9); const int udp_receiver = sceNetSocket(nullptr, 2, 2, 17); const int udp_sender = sceNetSocket(nullptr, 2, 2, 17); @@ -427,6 +466,10 @@ int main() { Require(sceNetRecvfrom(udp_receiver, datagram_result, sizeof(datagram_result), 0, source.data(), &address_size) == sizeof(datagram_result)); Require(std::strcmp(datagram, datagram_result) == 0 && source[1] == 2); + std::array unsized = address; + unsized[0] = 0; + Require(sceNetSendto(udp_sender, datagram, sizeof(datagram), 0, unsized.data(), unsized.size()) == sizeof(datagram)); + Require(sceNetRecvfrom(udp_receiver, datagram_result, sizeof(datagram_result), 0, nullptr, nullptr) == sizeof(datagram_result)); std::uint64_t readable[16]{}; readable[udp_receiver / 64] |= std::uint64_t{1} << (udp_receiver % 64); const std::int64_t poll_now[2]{0, 0}; @@ -459,6 +502,7 @@ int main() { Require(stdio_readable[0] == 0 && stdio_writable[0] == 6); stdio_readable[0] = 1; Require(select_nid_postfix(1, stdio_readable, nullptr, nullptr, poll_now) == 0 && stdio_readable[0] == 0); + CheckReceiveTimeout(udp_receiver, udp_sender, address); CheckMessages(udp_receiver, udp_sender, address); Require(sceNetSocketClose(udp_sender) == 0); Require(sceNetSocketClose(udp_receiver) == 0); diff --git a/core/libs/tests/GuestSchedPriority.cpp b/core/libs/tests/GuestSchedPriority.cpp index 7f135f5ef..0e9d418d1 100644 --- a/core/libs/tests/GuestSchedPriority.cpp +++ b/core/libs/tests/GuestSchedPriority.cpp @@ -1,19 +1,23 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" { int APS5_VABI sched_get_priority_max_nid_postfix(int policy); int APS5_VABI sched_get_priority_min_nid_postfix(int policy); } static void Require(bool value) { if (!value) std::abort(); } static bool Rejects(int policy) { - try { sched_get_priority_max_nid_postfix(policy); } catch (const std::invalid_argument&) { return true; } - return false; + *__error_nid_postfix() = 0; + const bool max = sched_get_priority_max_nid_postfix(policy) == -1 && *__error_nid_postfix() == 22; + *__error_nid_postfix() = 0; + return max && sched_get_priority_min_nid_postfix(policy) == -1 && *__error_nid_postfix() == 22; } int main() { - for (int policy : {1, 2, 3}) { - Require(sched_get_priority_max_nid_postfix(policy) == 256); - Require(sched_get_priority_min_nid_postfix(policy) == 767); + Require(Rejects(-1) && Rejects(2) && Rejects(5)); + for (int policy : {1, 3}) { + Require(sched_get_priority_max_nid_postfix(policy) == 767); + Require(sched_get_priority_min_nid_postfix(policy) == 256); } Require(Rejects(0) && Rejects(4)); } diff --git a/core/libs/tests/GuestSemaphore.cpp b/core/libs/tests/GuestSemaphore.cpp index fd90d1c80..5392fa160 100644 --- a/core/libs/tests/GuestSemaphore.cpp +++ b/core/libs/tests/GuestSemaphore.cpp @@ -18,6 +18,7 @@ static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); static constexpr int SCE_KERNEL_ERROR_ECANCELED = static_cast(0x80020055); +static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); static void Require(bool value) { if (!value) std::abort(); } @@ -75,6 +76,12 @@ int main() { Require(sceKernelCancelSema(sem, 0, &waiters) == SCE_OK && waiters == 0); Require(sceKernelPollSema(sem, 1) == SCE_KERNEL_ERROR_EBUSY); + KernelUseconds remaining = 1000; + Require(sceKernelWaitSema(sem, 1, &remaining) == SCE_KERNEL_ERROR_ETIMEDOUT && remaining == 0); + Require(sceKernelSignalSema(sem, 1) == SCE_OK); + remaining = 5000000; + Require(sceKernelWaitSema(sem, 1, &remaining) == SCE_OK && remaining > 0 && remaining <= 5000000); + Require(sceKernelSignalSema(sem, 1) == SCE_OK); Require(sceKernelWaitSema(sem, 1, nullptr) == SCE_OK); Require(sceKernelDeleteSema(sem) == SCE_OK); diff --git a/core/libs/tests/GuestSocketHostWrite.cpp b/core/libs/tests/GuestSocketHostWrite.cpp new file mode 100644 index 000000000..9624ce806 --- /dev/null +++ b/core/libs/tests/GuestSocketHostWrite.cpp @@ -0,0 +1,305 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI listen_nid_postfix(int, int); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +std::int64_t APS5_VABI sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); +std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); +std::int64_t APS5_VABI recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*); +std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); +std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI sceNetSocket(const char*, int, int, int); +int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); +int APS5_VABI sceNetListen(int, int); +int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); +int APS5_VABI sceNetAccept(int, void*, std::uint32_t*); +int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); +std::int64_t APS5_VABI sceNetSendto(int, const void*, std::size_t, int, const void*, std::uint32_t); +std::int64_t APS5_VABI sceNetRecv(int, void*, std::size_t, int); +std::int64_t APS5_VABI sceNetRecvfrom(int, void*, std::size_t, int, void*, std::uint32_t*); +int APS5_VABI sceNetSocketClose(int); +int* APS5_VABI sceNetErrnoLoc(); +int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); +int APS5_VABI sceKernelMapDirectMemory(void**, std::size_t, int, int, std::int64_t, std::size_t); +int APS5_VABI sceKernelMprotect(void*, std::size_t, int); +int APS5_VABI sceKernelMunmap(void*, std::size_t); +int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); +} + +static void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::exit(1); + } +} + +static DWORD Protection(const void* address) { + MEMORY_BASIC_INFORMATION information{}; + Require(VirtualQuery(address, &information, sizeof(information)) != 0, "query host protection"); + return information.Protect; +} + +int main(int argc, char** argv) { + constexpr std::size_t page = 0x4000; + constexpr std::size_t capacity = 32; + constexpr const char* names[] = {"recv", "recvfrom", "read", "sceKernelRead", "sceNetRecv", "sceNetRecvfrom"}; + Require(argc == 2 || argc == 3, "receive API argument required"); + Require(argc != 3 || std::strcmp(argv[2], "tcp") == 0, "unknown transport"); + const bool stream = argc == 3; + const auto selected = std::find_if(std::begin(names), std::end(names), [&](const char* name) { + return std::strcmp(name, argv[1]) == 0; + }); + Require(selected != std::end(names), "unknown receive API"); + const auto api = selected - std::begin(names); + const bool net = api >= 4; + const bool from = api == 1 || api == 5; + const bool peek = api != 2 && api != 3; + const auto create = [&] { + return net ? sceNetSocket(nullptr, 2, stream ? 1 : 2, stream ? 6 : 17) : + socket_nid_postfix(2, stream ? 1 : 2, stream ? 6 : 17); + }; + int receiver = create(); + const int sender = create(); + Require(receiver >= 0 && sender >= 0, "create sockets"); + std::array destination{16, 2, 0, 0, 127, 0, 0, 1}; + Require((net ? sceNetBind_nid_postfix(receiver, destination.data(), destination.size()) : + bind_nid_postfix(receiver, destination.data(), destination.size())) == 0, "bind receiver"); + auto senderAddress = destination; + Require((net ? sceNetBind_nid_postfix(sender, senderAddress.data(), senderAddress.size()) : + bind_nid_postfix(sender, senderAddress.data(), senderAddress.size())) == 0, "bind sender"); + std::uint32_t size = destination.size(); + Require((net ? sceNetGetsockname(receiver, destination.data(), &size) : + getsockname_nid_postfix(receiver, destination.data(), &size)) == 0, "receiver address"); + size = senderAddress.size(); + Require((net ? sceNetGetsockname(sender, senderAddress.data(), &size) : + getsockname_nid_postfix(sender, senderAddress.data(), &size)) == 0, "sender address"); + if (stream) { + Require((net ? sceNetListen(receiver, 1) : listen_nid_postfix(receiver, 1)) == 0, "listen on loopback"); + Require((net ? sceNetConnect(sender, destination.data(), destination.size()) : + connect_nid_postfix(sender, destination.data(), destination.size())) == 0, "connect sender"); + const int accepted = net ? sceNetAccept(receiver, nullptr, nullptr) : accept_nid_postfix(receiver, nullptr, nullptr); + Require(accepted >= 0, "accept sender"); + Require((net ? sceNetSocketClose(receiver) : close_nid_postfix(receiver)) == 0, "close listener"); + receiver = accepted; + } + const auto nonblocking = [&](bool enabled) { + const int value = enabled; + Require((net ? sceNetSetsockopt(receiver, 0xffff, 0x1200, &value, sizeof(value)) : + fcntl_nid_postfix(receiver, 4, enabled ? 4 : 0)) == 0, "set receiver mode"); + }; + nonblocking(true); + const char payload[] = "watched socket data"; + const auto send = [&] { + Require((net ? sceNetSendto(sender, payload, sizeof(payload), 0, stream ? nullptr : destination.data(), + stream ? 0 : destination.size()) : sendto_nid_postfix(sender, payload, sizeof(payload), 0, + stream ? nullptr : destination.data(), stream ? 0 : destination.size())) == sizeof(payload), + "send packet"); + }; + const auto receive = [&](void* buffer, std::size_t bytes, int flags, void* address, std::uint32_t* addressLength) { + switch (api) { + case 0: return recv_nid_postfix(receiver, buffer, bytes, flags); + case 1: return recvfrom_nid_postfix(receiver, buffer, bytes, flags, address, addressLength); + case 2: return read_nid_postfix(receiver, buffer, bytes); + case 3: return sceKernelRead(receiver, buffer, bytes); + case 4: return sceNetRecv(receiver, buffer, bytes, flags); + default: return sceNetRecvfrom(receiver, buffer, bytes, flags, address, addressLength); + } + }; + const auto waitForPacket = [&] { + std::array buffer{}; + int* error = net ? sceNetErrnoLoc() : __error_nid_postfix(); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + std::int64_t result; + do { + result = net ? sceNetRecv(receiver, buffer.data(), buffer.size(), 2) : + recv_nid_postfix(receiver, buffer.data(), buffer.size(), 2); + if (result == sizeof(payload) || (result < 0 && *error != 35)) break; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } while (std::chrono::steady_clock::now() < deadline); + Require(result == sizeof(payload), "wait for complete packet"); + }; + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &physical) == 0, "allocate direct memory"); + void* mapping = nullptr; + Require(sceKernelMapDirectMemory(&mapping, page * 2, 3, 0, physical, 0) == 0, "map read/write memory"); + auto* buffer = static_cast(mapping) + page - 7; + const auto collect = [&] { + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(mapping), page * 2, + pages.data(), &count, true), "collect tracked writes"); + return count; + }; + const auto reset = [&] { + const GuestArena::HostWrite destination(buffer - 1, capacity + 2); + Require(destination.Open(), "reset writable receive buffer"); + std::memset(buffer - 1, '!', capacity + 2); + }; + int* error = net ? sceNetErrnoLoc() : __error_nid_postfix(); + for (const std::size_t bytes : {std::size_t{0}, std::size_t{4}, sizeof(payload), capacity}) { + send(); + waitForPacket(); + for (const int flags : {2, 0}) { + if (flags == 2 && !peek) continue; + reset(); + collect(); + Require(Protection(buffer) == PAGE_READONLY && Protection(buffer + capacity - 1) == PAGE_READONLY, + "write tracking arms both guest pages"); + std::array address{}; + size = address.size(); + *error = 123; + const auto result = receive(buffer, bytes, flags, stream ? nullptr : address.data(), stream ? nullptr : &size); + const auto expected = std::min(bytes, sizeof(payload)); + if (result != static_cast(expected)) { + std::fprintf(stderr, "%s capacity=%zu flags=%d result=%lld errno=%d\n", argv[1], bytes, flags, + static_cast(result), *error); + return 1; + } + Require(*error == 123, "successful receive preserves errno"); + Require(buffer[-1] == '!' && std::memcmp(buffer, payload, expected) == 0 && + std::all_of(buffer + expected, buffer + capacity + 1, [](char value) { return value == '!'; }), + "received data and buffer guards"); + if (from && !stream) Require(size == senderAddress.size() && std::memcmp(address.data(), senderAddress.data(), size) == 0, + "sender address preserved"); + if (flags == 0 && ((bytes == 0 && !peek) || (stream && expected < sizeof(payload)))) { + std::array retained{}; + Require(receive(retained.data(), retained.size(), 0, nullptr, nullptr) == sizeof(payload) - expected && + std::memcmp(retained.data(), payload + expected, sizeof(payload) - expected) == 0, + "short stream receive or zero-byte read retains unread data"); + } + if (bytes != 0) Require(collect() != 0, "receive reported as a tracked write"); + collect(); + Require(Protection(buffer) == PAGE_READONLY && Protection(buffer + capacity - 1) == PAGE_READONLY, + "receive releases the host write guard"); + } + } + if (from && !stream) { + std::int64_t outputPhysical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &outputPhysical) == 0, + "allocate sender output memory"); + void* outputMapping = nullptr; + Require(sceKernelMapDirectMemory(&outputMapping, page * 2, 3, 0, outputPhysical, 0) == 0, + "map sender output memory"); + const auto collectOutputs = [&] { + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(outputMapping), page * 2, + pages.data(), &count, true), "collect sender output writes"); + return count; + }; + for (const int mask : {1, 2, 3}) { + std::array hostAddress{}; + std::uint32_t hostLength = hostAddress.size(); + auto* address = (mask & 1) ? static_cast(outputMapping) + 16 : hostAddress.data(); + auto* length = (mask & 2) ? reinterpret_cast(static_cast(outputMapping) + page + 16) : + &hostLength; + { + const GuestArena::HostWrite addressDestination(address, hostAddress.size()); + const GuestArena::HostWrite lengthDestination(length, sizeof(*length)); + Require(addressDestination.Open() && lengthDestination.Open(), "initialize sender outputs"); + std::memset(address, 0xa5, hostAddress.size()); + *length = hostAddress.size(); + } + collectOutputs(); + Require(collectOutputs() == 0, "sender outputs initially clean"); + if (mask & 1) Require(Protection(address) == PAGE_READONLY, "sender address is watched and armed"); + if (mask & 2) Require(Protection(length) == PAGE_READONLY, "sender length is watched and armed"); + send(); + waitForPacket(); + reset(); + collect(); + *error = 123; + Require(receive(buffer, capacity, 0, address, length) == sizeof(payload) && *error == 123 && + std::memcmp(buffer, payload, sizeof(payload)) == 0, "receive into watched sender outputs"); + Require(*length == senderAddress.size() && std::memcmp(address, senderAddress.data(), *length) == 0, + "watched sender outputs are correct"); + const auto expectedPages = static_cast(((mask & 1) != 0) + ((mask & 2) != 0)) * (page / 4096); + Require(collectOutputs() == expectedPages, "each watched sender output is tracked"); + Require(collectOutputs() == 0, "sender output writes cleared"); + if (mask & 1) Require(Protection(address) == PAGE_READONLY, "sender address rearmed"); + if (mask & 2) Require(Protection(length) == PAGE_READONLY, "sender length rearmed"); + } + Require(sceKernelMunmap(outputMapping, page * 2) == 0, "unmap sender outputs"); + Require(sceKernelReleaseDirectMemory(outputPhysical, page * 2) == 0, "release sender output memory"); + } + reset(); + collect(); + *error = 0; + const auto empty = receive(buffer, capacity, 0, nullptr, nullptr); + const auto expectedAgain = api == 3 ? static_cast(0x80020023u) : + net ? static_cast(0x80410123u) : -1; + Require(empty == expectedAgain && *error == 35, "empty nonblocking receive reports EAGAIN"); + collect(); + Require(Protection(buffer) == PAGE_READONLY && Protection(buffer + capacity - 1) == PAGE_READONLY, + "failed receive releases the host write guard"); + Require(sceKernelMprotect(mapping, page * 2, 1) == 0, "make guest memory read-only"); + send(); + waitForPacket(); + *error = 0; + const auto rejected = receive(buffer, capacity, 0, nullptr, nullptr); + const auto expectedError = api == 3 ? static_cast(0x8002000eu) : + net ? static_cast(0x8041010eu) : -1; + Require(rejected == expectedError && *error == 14, "read-only destination fails with EFAULT"); + Require(std::all_of(buffer - 1, buffer + capacity + 1, [](char value) { return value == '!'; }), + "read-only destination unchanged"); + std::array recovery{}; + Require(receive(recovery.data(), recovery.size(), 0, nullptr, nullptr) == sizeof(payload) && + std::memcmp(recovery.data(), payload, sizeof(payload)) == 0, "rejected write preserves queued packet"); + send(); + waitForPacket(); + *error = 123; + Require(receive(buffer, 0, peek ? 2 : 0, nullptr, nullptr) == 0 && *error == 123, + "zero-byte receive does not require a writable destination"); + Require(Protection(buffer) == PAGE_READONLY && Protection(buffer + capacity - 1) == PAGE_READONLY, + "zero-byte receive preserves read-only protection"); + Require(receive(recovery.data(), recovery.size(), 0, nullptr, nullptr) == sizeof(payload) && + std::memcmp(recovery.data(), payload, sizeof(payload)) == 0, "zero-byte peek or read retains queued data"); + Require(sceKernelMprotect(mapping, page * 2, 3) == 0, "restore guest write permission"); + reset(); + collect(); + nonblocking(false); + std::int64_t result = -1; + int workerError = 0; + std::thread worker([&] { + int* slot = net ? sceNetErrnoLoc() : __error_nid_postfix(); + *slot = 123; + result = receive(buffer, capacity, 0, nullptr, nullptr); + workerError = *slot; + }); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (Protection(buffer) != PAGE_READWRITE && std::chrono::steady_clock::now() < deadline) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(Protection(buffer) == PAGE_READWRITE, "blocking receive opens its destination"); + collect(); + Require(Protection(buffer) == PAGE_READWRITE && Protection(buffer + capacity - 1) == PAGE_READWRITE, + "collection does not rearm an in-flight receive"); + send(); + worker.join(); + Require(result == sizeof(payload) && workerError == 123 && std::memcmp(buffer, payload, sizeof(payload)) == 0, + "blocking receive completes and preserves errno"); + Require(collect() != 0, "completion remains dirty after an in-flight collection"); + Require(Protection(buffer) == PAGE_READONLY, "completed receive can be rearmed"); + Require((net ? sceNetSocketClose(sender) : close_nid_postfix(sender)) == 0, "close sender"); + Require((net ? sceNetSocketClose(receiver) : close_nid_postfix(receiver)) == 0, "close receiver"); + Require(sceKernelMunmap(mapping, page * 2) == 0, "unmap direct memory"); + Require(sceKernelReleaseDirectMemory(physical, page * 2) == 0, "release direct memory"); +} diff --git a/core/libs/tests/GuestSocketReadWrite.cpp b/core/libs/tests/GuestSocketReadWrite.cpp index b89370b8c..5a8d48781 100644 --- a/core/libs/tests/GuestSocketReadWrite.cpp +++ b/core/libs/tests/GuestSocketReadWrite.cpp @@ -30,6 +30,7 @@ int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*); int APS5_VABI fcntl_nid_postfix(int, int, ...); int APS5_VABI poll_nid_postfix(PollDescriptor*, std::uint32_t, int); int APS5_VABI close_nid_postfix(int); +int APS5_VABI sceKernelClose(int); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); std::int64_t APS5_VABI _read_nid_postfix(int, void*, std::size_t); @@ -167,6 +168,8 @@ int main() { Require(pwrite_nid_disambig1_nid_postfix(server, request, 1, 0) == -1 && *__error_nid_postfix() == 9); Require(preadv_nid_postfix(server, positionedIov, 1, 0) == -1 && *__error_nid_postfix() == 9); Require(pwritev_nid_postfix(server, positionedIov, 1, 0) == -1 && *__error_nid_postfix() == 9); - Require(close_nid_postfix(listener) == 0); + Require(sceKernelClose(listener) == 0); + Require(sceKernelClose(listener) == KernelBadDescriptor); + Require(close_nid_postfix(listener) == -1 && *__error_nid_postfix() == 9); return 0; } diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 5de08e2cb..67a4a57b3 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -121,7 +122,7 @@ static void CheckUnlockedStreams() { Require(std::ungetc('Q', stream.GetHandle()) == 'Q'); Require(getc_unlocked_nid_postfix(&stream) == 'Q' && !(guest.flags & 0x20)); std::rewind(stream.GetHandle()); - stream.SetEncodingError(); + stream.SetError(); Require(putc_unlocked_nid_postfix('R', &stream) == 'R' && (guest.flags & 0x40)); clearerr_nid_postfix(&stream); Require(!(guest.flags & 0x40)); @@ -229,6 +230,27 @@ static void CheckUnlockedStreamLock(bool write) { stream.Close(); } +static void CheckFgetsSize() { + FileStream stream(std::tmpfile()); + Require(fwrite_nid_postfix("line\n", 1, 5, &stream) == 5); + rewind_nid_postfix(&stream); + for (const int size : {0, -1, std::numeric_limits::min()}) { + char buffer[8]; + std::memset(buffer, '!', sizeof(buffer)); + *__error_nid_postfix() = 0; + Require(fgets_nid_postfix(buffer, size, &stream) == nullptr); + Require(*__error_nid_postfix() == 22 && ferror_nid_postfix(&stream) != 0 && feof_nid_postfix(&stream) == 0); + Require(buffer[0] == '!' && ftello_nid_postfix(&stream) == 0); + Require(ferror_nid_postfix(&stream) != 0); + clearerr_nid_postfix(&stream); + Require(ferror_nid_postfix(&stream) == 0); + } + char line[8]{}; + Require(fgets_nid_postfix(line, sizeof(line), &stream) == line && std::strcmp(line, "line\n") == 0); + Require(ferror_nid_postfix(&stream) == 0); + stream.Close(); +} + static void CheckRewind() { FileStream stream(std::tmpfile()); Require(fwrite_nid_postfix("ab", 1, 2, &stream) == 2); @@ -239,7 +261,7 @@ static void CheckRewind() { rewind_nid_postfix(&stream); Require(fgetc_nid_postfix(&stream) == 'a' && fgetc_nid_postfix(&stream) == 'b' && fgetc_nid_postfix(&stream) == EOF); Require(feof_nid_postfix(&stream) != 0); - stream.SetEncodingError(); + stream.SetError(); Require(ferror_nid_postfix(&stream) != 0); rewind_nid_postfix(&stream); Require(feof_nid_postfix(&stream) == 0 && ferror_nid_postfix(&stream) == 0 && fgetc_nid_postfix(&stream) == 'a'); @@ -312,6 +334,35 @@ static bool CheckBinaryModes() { return correct; } +static int DuplicateDescriptor(std::FILE* stream) { +#ifdef _WIN32 + return _dup(_fileno(stream)); +#else + return ::dup(::fileno(stream)); +#endif +} + +static void CheckDescriptorModes(std::FILE* original) { + for (const char* mode : {"rt", "r+t", "re", "rbe", "rw", "r+x"}) { + const int descriptor = DuplicateDescriptor(original); + Require(descriptor >= 0); + auto* wrapped = fdopen_nid_postfix(descriptor, mode); + Require(wrapped != nullptr && fseek_nid_postfix(wrapped, 0, SEEK_SET) == 0); + char contents[32]{}; + Require(fgets_nid_postfix(contents, sizeof(contents), wrapped) == contents && std::strcmp(contents, "retained") == 0); + Require(fclose_nid_postfix(wrapped) == 0); + } + const int descriptor = DuplicateDescriptor(original); + Require(descriptor >= 0); + *__error_nid_postfix() = 0; + Require(fdopen_nid_postfix(descriptor, "rx") == nullptr && *__error_nid_postfix() == 22); +#ifdef _WIN32 + Require(_close(descriptor) == 0); +#else + Require(::close(descriptor) == 0); +#endif +} + int main() { Require(_Getmbcurmax_nid_postfix() == 1 && _Getmbcurmax_nid_postfix() == ___mb_cur_max_nid_postfix()); Require(fdopen_nid_postfix(-1, "rb") == nullptr && *__error_nid_postfix() == 9); @@ -339,6 +390,7 @@ int main() { #else Require(::close(descriptor) == -1); #endif + CheckDescriptorModes(original); Require(std::fclose(original) == 0); char stringOutput[256]; std::memset(stringOutput, '!', sizeof(stringOutput)); @@ -511,6 +563,7 @@ int main() { Require(fgetc_nid_postfix(&locked) == 'A' && fgetc_nid_postfix(&locked) == 'B'); locked.Close(); CheckRewind(); + CheckFgetsSize(); CheckUnlockedStreams(); CheckUnlockedStreamLock(false); CheckUnlockedStreamLock(true); diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index 3f9efbb6c..ec0b4f01d 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -11,6 +11,8 @@ char* APS5_VABI basename_nid_postfix(const char*); char* APS5_VABI __inet_ntoa_nid_postfix(unsigned int); char* APS5_VABI dirname_nid_postfix(const char*); int* APS5_VABI __error_nid_postfix(); +int APS5_VABI strcasecmp_nid_postfix(const char*, const char*); +int APS5_VABI strncasecmp_nid_postfix(const char*, const char*, std::size_t); std::size_t APS5_VABI strnlen_nid_postfix(const char*, std::size_t); std::size_t APS5_VABI strnlen_s_nid_postfix(const char*, std::size_t); char* APS5_VABI strncat_nid_postfix(char*, const char*, std::size_t); @@ -98,6 +100,18 @@ static void CheckBoundsCheckedFunctions() { Require(snprintf_s_nid_postfix(formatted, sizeof(formatted), "%d-%s", 42, "x") == 4 && std::strcmp(formatted, "42-x") == 0); } +static void CheckCaseInsensitiveComparison() { + Require(strcasecmp_nid_postfix("", "") == 0 && strcasecmp_nid_postfix("AbC", "aBc") == 0); + Require(strcasecmp_nid_postfix("A", "b") == 'a' - 'b' && strcasecmp_nid_postfix("b", "A") == 'b' - 'a'); + Require(strcasecmp_nid_postfix("ab", "A") == 'b' && strcasecmp_nid_postfix("AB", "a") == 'b'); + Require(strcasecmp_nid_postfix("A", "aB") == -'b' && strcasecmp_nid_postfix("a", "AB") == -'b'); + Require(strcasecmp_nid_postfix("", "Z") == -'z' && strcasecmp_nid_postfix("Z", "") == 'z'); + Require(strncasecmp_nid_postfix("abc", "ABD", 2) == 0 && strncasecmp_nid_postfix("abc", "ABD", 3) == 'c' - 'd'); + Require(strncasecmp_nid_postfix("Ab", "a", 2) == 'b' && strncasecmp_nid_postfix("a", "AB", 5) == -'b'); + Require(strncasecmp_nid_postfix("Q", "", 1) == 'q' && strncasecmp_nid_postfix("", "Q", 1) == -'q'); + Require(strncasecmp_nid_postfix("x", "y", 0) == 0 && strncasecmp_nid_postfix("same", "SAME", 99) == 0); +} + static void CheckSscanfS() { #ifndef _WIN32 int number = 0; @@ -119,8 +133,54 @@ static void CheckSscanfS() { #endif } +static bool CheckDirnameLengths() { + const std::string longLeaf(4096, 'x'); + const std::string longestParent(1023, 'p'); + const std::string tooLongParent(1024, 'q'); + struct Case { + std::string path; + const char* expected; + }; + const Case cases[] = { + {std::string(1023, 'x'), "."}, + {std::string(1024, 'x'), "."}, + {longLeaf, "."}, + {"/" + longLeaf, "/"}, + {"one/" + longLeaf, "one"}, + {"/one/" + longLeaf, "/one"}, + {"one//" + longLeaf + "///", "one"}, + {"/one//" + longLeaf + "///", "/one"}, + {longLeaf + "///", "."}, + {std::string(4096, '/'), "/"}, + {longestParent + "/leaf", longestParent.c_str()}, + {longestParent + "//leaf///", longestParent.c_str()}, + {longestParent + "/", "."}, + {tooLongParent + "/leaf", nullptr}, + {tooLongParent + "//leaf///", nullptr}, + {longLeaf + "/leaf", nullptr}, + }; + bool passed = true; + for (const auto& test : cases) { + const std::string original = test.path; + *__error_nid_postfix() = 77; + const char* result = dirname_nid_postfix(test.path.c_str()); + const bool matches = test.expected + ? result != nullptr && std::strcmp(result, test.expected) == 0 && *__error_nid_postfix() == 77 + : result == nullptr && *__error_nid_postfix() == 63; + if (!matches || test.path != original) { + std::fprintf(stderr, "dirname input length %zu: expected output length %zu, received %s, errno %d\n", + test.path.size(), test.expected ? std::strlen(test.expected) : 0, + result ? "a directory" : "null", *__error_nid_postfix()); + passed = false; + } + } + return passed; +} + int main() { + if (!CheckDirnameLengths()) return 1; CheckBoundsCheckedFunctions(); + CheckCaseInsensitiveComparison(); CheckSscanfS(); Require(std::strcmp(dirname_nid_postfix(nullptr), ".") == 0); char emptyPath[] = ""; @@ -142,8 +202,6 @@ int main() { Require(std::strcmp(dirname_nid_postfix(rootChild), "/") == 0); char onlySlashes[] = "////"; Require(std::strcmp(dirname_nid_postfix(onlySlashes), "/") == 0); - const std::string longDirnamePath(1024, 'x'); - Require(dirname_nid_postfix(longDirnamePath.c_str()) == nullptr && *__error_nid_postfix() == 63); Require(std::strcmp(basename_nid_postfix(nullptr), ".") == 0); Require(std::strcmp(basename_nid_postfix(""), ".") == 0); Require(std::strcmp(basename_nid_postfix("////"), "/") == 0); diff --git a/core/libs/tests/GuestSysmodule.cpp b/core/libs/tests/GuestSysmodule.cpp index 77ec859f5..a5383a863 100644 --- a/core/libs/tests/GuestSysmodule.cpp +++ b/core/libs/tests/GuestSysmodule.cpp @@ -44,6 +44,7 @@ constexpr int kInvalidValue = static_cast(0x805A1000); constexpr int kModuleUnloaded = static_cast(0x805A1001); constexpr std::uint32_t kInternalFlag = 0x80000000u; constexpr std::uint16_t kTrophy2ModuleId = 0x0110; +constexpr std::uint16_t kVoiceChatModuleId = 0x0114; constexpr std::uint32_t kAgcModuleId = 0x80000094; #ifdef __linux__ @@ -131,6 +132,13 @@ int main() { Require(sceSysmoduleUnloadModule(kUltModuleId) == 0); Require(sceSysmoduleIsLoaded(kUltModuleId) == kModuleUnloaded); + constexpr std::uint16_t kShareModuleId = 0x010f; + Require(sceSysmoduleIsLoaded(kShareModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModule(kShareModuleId) == 0); + Require(sceSysmoduleIsLoaded(kShareModuleId) == 0); + Require(sceSysmoduleUnloadModule(kShareModuleId) == 0); + Require(sceSysmoduleIsLoaded(kShareModuleId) == kModuleUnloaded); + Require(sceSysmoduleIsLoaded(kTrophy2ModuleId) == kModuleUnloaded); Require(sceSysmoduleLoadModuleInternal(kTrophy2ModuleId) == 0); Require(sceSysmoduleIsLoaded(kTrophy2ModuleId) == 0); @@ -141,6 +149,13 @@ int main() { Require(sceSysmoduleIsLoaded(kTrophy2ModuleId) == kModuleUnloaded); Require(sceSysmoduleUnloadModuleInternal(kTrophy2ModuleId) == kModuleUnloaded); + Require(sceSysmoduleIsLoaded(kVoiceChatModuleId) == kModuleUnloaded); + Require(sceSysmoduleUnloadModule(kVoiceChatModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModule(kVoiceChatModuleId) == 0); + Require(sceSysmoduleIsLoaded(kVoiceChatModuleId) == 0); + Require(sceSysmoduleUnloadModule(kVoiceChatModuleId) == 0); + Require(sceSysmoduleIsLoaded(kVoiceChatModuleId) == kModuleUnloaded); + Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == kModuleUnloaded); Require(sceSysmoduleLoadModuleInternal(kAgcModuleId) == 0); Require(sceSysmoduleLoadModuleInternal(kAgcModuleId) == 0); @@ -170,4 +185,22 @@ int main() { Require(symbol != nullptr); Require(sceSysmoduleUnloadModule(kFiberModuleId) == 0); Require(sceSysmoduleGetModuleHandleInternal(kFiberModuleId, &handle) == kModuleUnloaded); + + constexpr std::uint16_t kAudioPropagationModuleId = 0x0120; + std::int32_t audioPropagationHandle = -1; + Require(sceSysmoduleGetModuleHandleInternal(kAudioPropagationModuleId, &audioPropagationHandle) == kModuleUnloaded); + Require(audioPropagationHandle == -1); + + Require(sceSysmoduleIsLoaded(kAudioPropagationModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModule(kAudioPropagationModuleId) == 0); + Require(sceSysmoduleIsLoaded(kAudioPropagationModuleId) == 0); + Require(sceSysmoduleGetModuleHandleInternal(kAudioPropagationModuleId, &audioPropagationHandle) == 0); + Require(audioPropagationHandle >= 0); + void* audioPropagationSymbol = nullptr; + Require(sceKernelDlsym(audioPropagationHandle, "sceAudioPropagationSystemCreate", &audioPropagationSymbol) == 0); + Require(audioPropagationSymbol != nullptr); + + Require(sceSysmoduleUnloadModule(kAudioPropagationModuleId) == 0); + Require(sceSysmoduleIsLoaded(kAudioPropagationModuleId) == kModuleUnloaded); + Require(sceSysmoduleGetModuleHandleInternal(kAudioPropagationModuleId, &audioPropagationHandle) == kModuleUnloaded); } diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index ffba40d37..069ef2767 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -21,6 +21,7 @@ int* APS5_VABI __error_nid_postfix(); int APS5_VABI sysctl_nid_postfix(const int*, std::uint32_t, void*, std::size_t*, const void*, std::size_t); int APS5_VABI sysctlbyname_nid_postfix(const char*, void*, std::size_t*, const void*, std::size_t); int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(SceKernelSystemSwVersion*); +int APS5_VABI getpid_nid_postfix(); extern char** environ_nid_postfix; } static void Require(bool value) { if (!value) std::abort(); } @@ -56,7 +57,7 @@ static void CheckProcessorCountSysctl() { } Require(threw); threw = false; - const int unknown[] = {6, 5}; + const int unknown[] = {6, 99}; try { sysctl_nid_postfix(unknown, 2, &value, &length, nullptr, 0); } catch (const std::runtime_error&) { @@ -64,6 +65,77 @@ static void CheckProcessorCountSysctl() { } Require(threw); } +static void CheckPhysicalMemorySysctl() { + const int mib[] = {6, 5}; + const std::uint64_t expected = static_cast(sysconf_nid_postfix(121)) * 0x4000; + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == sizeof(std::uint64_t)); + std::uint64_t value = 0; + length = sizeof(value); + Require(sysctl_nid_postfix(mib, 2, &value, &length, nullptr, 0) == 0 && value == expected && length == sizeof(std::uint64_t)); + value = 0; + length = 16; + Require(sysctlbyname_nid_postfix("hw.physmem", &value, &length, nullptr, 0) == 0 && value == expected && length == sizeof(std::uint64_t)); + Require(*__error_nid_postfix() == 13); + unsigned char bytes[8] = {0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa}; + length = 4; + Require(sysctlbyname_nid_postfix("hw.physmem", bytes, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 4 && bytes[4] == 0xaa); + length = sizeof(value); + Require(sysctl_nid_postfix(mib, 2, &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); + Require(sysctlbyname_nid_postfix("hw.physmem", &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); +} +static void CheckMachdepCachelineSizeSysctl() { + *__error_nid_postfix() = 13; + int value = 0x5a5a5a5a; + std::size_t length = sizeof(value); + Require(sysctlbyname_nid_postfix("machdep.cacheline_size", &value, &length, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 2); + Require(value == 0x5a5a5a5a); +} +static void CheckPathnameSysctl() { + *__error_nid_postfix() = 13; + char buffer[32] = {}; + std::size_t length = sizeof(buffer); + Require(sysctlbyname_nid_postfix("kern.proc.pathname", buffer, &length, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 22); + + const int mibTooShort[] = {1, 14, 12}; + Require(sysctl_nid_postfix(mibTooShort, 3, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); + + const int mibTooLong[] = {1, 14, 12, -1, 0}; + Require(sysctl_nid_postfix(mibTooLong, 5, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); + + const int mibSelf[] = {1, 14, 12, -1}; + length = 0; + Require(sysctl_nid_postfix(mibSelf, 4, nullptr, &length, nullptr, 0) == 0 && length == sizeof("/app0/eboot.bin")); + + std::memset(buffer, 0xaa, sizeof(buffer)); + length = sizeof(buffer); + Require(sysctl_nid_postfix(mibSelf, 4, buffer, &length, nullptr, 0) == 0); + Require(length == sizeof("/app0/eboot.bin")); + Require(std::strcmp(buffer, "/app0/eboot.bin") == 0); + + const int callerPid = getpid_nid_postfix(); + const int mibPid[] = {1, 14, 12, callerPid}; + std::memset(buffer, 0xaa, sizeof(buffer)); + length = sizeof(buffer); + Require(sysctl_nid_postfix(mibPid, 4, buffer, &length, nullptr, 0) == 0); + Require(length == sizeof("/app0/eboot.bin")); + Require(std::strcmp(buffer, "/app0/eboot.bin") == 0); + + const int mibForeign[] = {1, 14, 12, callerPid + 1000}; + Require(sysctl_nid_postfix(mibForeign, 4, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 3); + + length = 5; + std::memset(buffer, 0xaa, sizeof(buffer)); + Require(sysctl_nid_postfix(mibSelf, 4, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 5 && buffer[4] == '0' && buffer[5] == static_cast(0xaa)); + + length = sizeof(buffer); + Require(sysctl_nid_postfix(mibSelf, 4, buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); +} static void CheckSystemSwVersion() { *__error_nid_postfix() = 13; Require(sceKernelGetSystemSwVersion_nid_postfix(nullptr) == SCE_KERNEL_ERROR_EFAULT && *__error_nid_postfix() == 13); @@ -107,10 +179,65 @@ static void CheckPseudoRandomSysctl() { } Require(threw); } +static void CheckSdkVersionSysctl() { + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctlbyname_nid_postfix("kern.sdk_version", nullptr, &length, nullptr, 0) == 0 && length == sizeof(std::uint32_t)); + std::uint32_t version = 0; + length = sizeof(version); + Require(sysctlbyname_nid_postfix("kern.sdk_version", &version, &length, nullptr, 0) == 0 && version == 0x01000000 && length == sizeof(std::uint32_t)); + Require(*__error_nid_postfix() == 13); + unsigned char bytes[4] = {0xaa, 0xaa, 0xaa, 0xaa}; + length = 2; + Require(sysctlbyname_nid_postfix("kern.sdk_version", bytes, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 2 && bytes[2] == 0xaa && bytes[3] == 0xaa); + length = sizeof(version); + Require(sysctlbyname_nid_postfix("kern.sdk_version", &version, &length, &version, sizeof(version)) == -1 && *__error_nid_postfix() == 1); +} +static void CheckArandomSysctl() { + const int mib[] = {1, 37}; + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == 0); + length = 64; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == 64); + length = 300; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == 256); + Require(sysctl_nid_postfix(mib, 2, nullptr, nullptr, nullptr, 0) == 0); + Require(*__error_nid_postfix() == 13); + unsigned char first[64] = {}; + unsigned char second[64] = {}; + length = sizeof(first); + Require(sysctl_nid_postfix(mib, 2, first, &length, nullptr, 0) == 0 && length == sizeof(first)); + length = sizeof(second); + Require(sysctl_nid_postfix(mib, 2, second, &length, nullptr, 0) == 0 && length == sizeof(second)); + Require(std::memcmp(first, second, sizeof(first)) != 0); + Require(*__error_nid_postfix() == 13); + unsigned char named[64] = {}; + length = sizeof(named); + Require(sysctlbyname_nid_postfix("kern.arandom", named, &length, nullptr, 0) == 0 && length == sizeof(named)); + Require(*__error_nid_postfix() == 13); + length = sizeof(first); + Require(sysctl_nid_postfix(mib, 2, first, &length, first, sizeof(first)) == -1 && *__error_nid_postfix() == 1); + *__error_nid_postfix() = 13; + unsigned char big[300] = {}; + length = sizeof(big); + Require(sysctl_nid_postfix(mib, 2, big, &length, nullptr, 0) == 0 && length == 256); + unsigned char small[16] = {}; + length = sizeof(small); + Require(sysctl_nid_postfix(mib, 2, small, &length, nullptr, 0) == 0 && length == sizeof(small)); + Require(sysctl_nid_postfix(mib, 2, first, nullptr, nullptr, 0) == 0); + Require(*__error_nid_postfix() == 13); +} int main() { CheckProcessorCountSysctl(); + CheckPhysicalMemorySysctl(); + CheckMachdepCachelineSizeSysctl(); + CheckPathnameSysctl(); CheckSystemSwVersion(); CheckPseudoRandomSysctl(); + CheckSdkVersionSysctl(); + CheckArandomSysctl(); *__error_nid_postfix() = 13; Require(sysconf_nid_postfix(47) == 0x4000); Require(getpagesize_nid_postfix() == sysconf_nid_postfix(47)); diff --git a/core/libs/tests/GuestSystemServiceLanguage.cpp b/core/libs/tests/GuestSystemServiceLanguage.cpp index 4b0e67f4d..efd37f499 100644 --- a/core/libs/tests/GuestSystemServiceLanguage.cpp +++ b/core/libs/tests/GuestSystemServiceLanguage.cpp @@ -21,20 +21,27 @@ void SetLanguage(const char* value) { int Language() { int value = -1; Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_LANG, &value) == SYSTEM_SERVICE_OK); + int sameValue = -1; + Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_400, &sameValue) == SYSTEM_SERVICE_OK); + Require(sameValue == value); return value; } -bool Rejected(const char* value) { - SetLanguage(value); +bool ParameterRejected(int paramId) { int language = -1; try { - static_cast(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_LANG, &language)); + static_cast(sceSystemServiceParamGetInt(paramId, &language)); } catch (const std::runtime_error& error) { return language == -1 && std::string(error.what()).find("ANYPS5_LANGUAGE") != std::string::npos; } return false; } +bool Rejected(const char* value) { + SetLanguage(value); + return ParameterRejected(SYSTEM_SERVICE_PARAM_ID_LANG) && ParameterRejected(SYSTEM_SERVICE_PARAM_ID_400); +} + } int main() { @@ -54,6 +61,7 @@ int main() { SetLanguage("arabic"); Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_LANG, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); + Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_400, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); int enterButton = -1; Require(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_ENTER_BUTTON_ASSIGN, &enterButton) == SYSTEM_SERVICE_OK); Require(enterButton == SYSTEM_SERVICE_PARAM_ENTER_BUTTON_CROSS); diff --git a/core/libs/tests/GuestTcp.cpp b/core/libs/tests/GuestTcp.cpp index d1f70d97d..5afeaa98e 100644 --- a/core/libs/tests/GuestTcp.cpp +++ b/core/libs/tests/GuestTcp.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -15,6 +16,7 @@ std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); int APS5_VABI getpeername_nid_postfix(int, void*, std::uint32_t*); int APS5_VABI shutdown_nid_postfix(int, int); +std::int64_t APS5_VABI recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*); int APS5_VABI close_nid_postfix(int); int* APS5_VABI __error_nid_postfix(); } @@ -23,7 +25,31 @@ static void Require(bool condition) { if (!condition) std::abort(); } +static void StreamReceiveReportsNoSender() { + const int listener = socket_nid_postfix(2, 1, 0); + Require(listener >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0 && listen_nid_postfix(listener, 4) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &size) == 0); + const int client = socket_nid_postfix(2, 1, 0); + Require(connect_nid_postfix(client, address.data(), address.size()) == 0); + const int accepted = accept_nid_postfix(listener, nullptr, nullptr); + Require(accepted >= 0); + const char text[] = "stream"; + Require(send_nid_postfix(client, text, sizeof(text), 0) == sizeof(text)); + std::array sender; + sender.fill(0xaa); + char received[sizeof(text)]{}; + std::uint32_t senderSize = sender.size(); + Require(recvfrom_nid_postfix(accepted, received, sizeof(received), 0, sender.data(), &senderSize) == sizeof(text)); + Require(std::strcmp(text, received) == 0); + Require(senderSize == 0 && sender[0] == 0xaa && sender[1] == 0xaa); + for (const int descriptor : {client, accepted, listener}) Require(close_nid_postfix(descriptor) == 0); +} + int main() { + StreamReceiveReportsNoSender(); const int listener = socket_nid_postfix(2, 1, 6); Require(listener >= 0); std::array address{16, 2, 0, 0, 127, 0, 0, 1}; @@ -48,6 +74,29 @@ int main() { Require(getpeername_nid_postfix(accepted, connected_peer.data(), &address_size) == 0); Require(address_size == 16 && connected_peer[1] == 2); + for (auto query : {getsockname_nid_postfix, getpeername_nid_postfix}) { + std::array expected{}; + address_size = expected.size(); + Require(query(accepted, expected.data(), &address_size) == 0 && address_size == expected.size()); + for (std::uint32_t capacity : {0u, 1u, 2u, 15u, 16u, 28u, UINT32_MAX}) { + std::array actual; + actual.fill(0xa5); + address_size = capacity; + const auto copied = std::min(capacity, std::uint32_t{16}); + Require(query(accepted, actual.data(), &address_size) == 0 && address_size == copied); + Require(std::memcmp(actual.data(), expected.data(), copied) == 0); + Require(std::all_of(actual.begin() + copied, actual.end(), [](auto byte) { return byte == 0xa5; })); + } + } + const int shortClient = socket_nid_postfix(2, 1, 0); + Require(shortClient >= 0 && connect_nid_postfix(shortClient, address.data(), address.size()) == 0); + peer.fill(0xa5); + address_size = 1; + const int shortAccepted = accept_nid_postfix(listener, peer.data(), &address_size); + Require(shortAccepted >= 0 && address_size == 1 && peer[0] == 16); + Require(std::all_of(peer.begin() + 1, peer.end(), [](auto byte) { return byte == 0xa5; })); + Require(close_nid_postfix(shortAccepted) == 0 && close_nid_postfix(shortClient) == 0); + const char request[] = "guest TCP loopback"; char received[sizeof(request)]{}; Require(send_nid_postfix(client, request, sizeof(request), 0x1) == -1 && *__error_nid_postfix() == 45); @@ -66,4 +115,4 @@ int main() { Require(close_nid_postfix(client) == 0); Require(close_nid_postfix(listener) == 0); Require(close_nid_postfix(listener) == -1 && *__error_nid_postfix() == 9); -} \ No newline at end of file +} diff --git a/core/libs/tests/GuestThreadCpuClock.cpp b/core/libs/tests/GuestThreadCpuClock.cpp index 058ed0969..15946c4ae 100644 --- a/core/libs/tests/GuestThreadCpuClock.cpp +++ b/core/libs/tests/GuestThreadCpuClock.cpp @@ -12,11 +12,13 @@ int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp); int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res); int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI sceKernelClockGettime(KernelClockid clockId, KernelTimespec* tp); int* APS5_VABI __error_nid_postfix(); } static constexpr int GUEST_EFAULT = 14; static constexpr int GUEST_EINVAL = 22; +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); static constexpr int GUEST_CLOCK_MONOTONIC = 4; static constexpr int GUEST_CLOCK_THREAD_CPUTIME_ID = 14; static constexpr std::int64_t NANOS_PER_SECOND = 1000000000LL; @@ -100,6 +102,9 @@ static void OtherThreadClockReadsThatThread() { *__error_nid_postfix() = 0; Require(clock_gettime_nid_postfix(clockId, &time) == -1); Require(*__error_nid_postfix() == GUEST_EINVAL); + *__error_nid_postfix() = 0; + Require(sceKernelClockGettime(clockId, &time) == SCE_KERNEL_ERROR_EINVAL); + Require(*__error_nid_postfix() == GUEST_EINVAL); } int main() { diff --git a/core/libs/tests/GuestThreadSelf.cpp b/core/libs/tests/GuestThreadSelf.cpp index 57413d0a9..9c59471e9 100644 --- a/core/libs/tests/GuestThreadSelf.cpp +++ b/core/libs/tests/GuestThreadSelf.cpp @@ -7,6 +7,8 @@ extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI scePthreadJoin(Pthread thread, void** retval); int APS5_VABI scePthreadDetach(Pthread thread); +int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** value); +int APS5_VABI pthread_detach_nid_postfix(Pthread thread); void APS5_VABI scePthreadExit(void* retval); Pthread APS5_VABI scePthreadSelf(); int APS5_VABI scePthreadGetthreadid(void); @@ -46,6 +48,9 @@ static bool StackContains(Pthread thread, const void* address) { static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; static constexpr int SCE_KERNEL_ERROR_EPERM = 0x80020001; +static constexpr int SCE_KERNEL_ERROR_EDEADLK = 0x8002000b; +static constexpr int GUEST_EDEADLK = 11; +static constexpr int GUEST_EINVAL = 22; static constexpr int MUTEX_TYPE_RECURSIVE = 2; static constexpr int PTHREAD_CANCEL_ENABLE = 0; static constexpr int PTHREAD_CANCEL_ASYNCHRONOUS = 2; @@ -69,6 +74,8 @@ struct WorkerContext { PthreadMutex* mutex = nullptr; int unlockResult = 0; bool testcancelReturned = false; + int sceJoinSelf = 0; + int posixJoinSelf = 0; }; static void* APS5_VABI Worker(void* arg) { @@ -80,6 +87,8 @@ static void* APS5_VABI Worker(void* arg) { int local = 0; context.workerStackReported = StackContains(context.selfFromWorker, &local); context.unlockResult = scePthreadMutexUnlock(context.mutex); + context.sceJoinSelf = scePthreadJoin(context.selfFromWorker, nullptr); + context.posixJoinSelf = pthread_join_nid_postfix(context.selfFromWorker, nullptr); int oldState = -1; int oldType = -1; context.testcancelReturned = scePthreadSetcancelstate(PTHREAD_CANCEL_ENABLE, &oldState) == SCE_OK && @@ -103,6 +112,10 @@ int main() { Require(StackContains(mainSelf, &local)); Require(scePthreadJoin(mainSelf, nullptr) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadDetach(mainSelf) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadJoin(nullptr, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadDetach(nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_join_nid_postfix(nullptr, nullptr) == GUEST_EINVAL); + Require(pthread_detach_nid_postfix(nullptr) == GUEST_EINVAL); PthreadMutexattr attr = nullptr; Require(scePthreadMutexattrInit(&attr) == SCE_OK); @@ -129,6 +142,8 @@ int main() { Require(context.workerThreadId == static_cast(context.workerTid)); Require(context.workerPosixThreadId == context.workerThreadId && context.workerPosixThreadId != static_cast(mainTid)); Require(context.unlockResult == SCE_KERNEL_ERROR_EPERM); + Require(context.sceJoinSelf == SCE_KERNEL_ERROR_EDEADLK); + Require(context.posixJoinSelf == GUEST_EDEADLK); Require(context.workerStackReported); Require(context.testcancelReturned); diff --git a/core/libs/tests/GuestTimeConversion.cpp b/core/libs/tests/GuestTimeConversion.cpp index e0027fb55..bdf1486d4 100644 --- a/core/libs/tests/GuestTimeConversion.cpp +++ b/core/libs/tests/GuestTimeConversion.cpp @@ -19,11 +19,14 @@ GuestTm* APS5_VABI libc_localtime_nid_postfix(const std::int64_t*); GuestTm* APS5_VABI localtime_nid_postfix(const std::int64_t*); GuestTm* APS5_VABI gmtime_s_nid_postfix(const std::int64_t*, GuestTm*); GuestTm* APS5_VABI localtime_s_nid_postfix(const std::int64_t*, GuestTm*); +char* APS5_VABI asctime_nid_postfix(const GuestTm*); std::int64_t APS5_VABI mktime_nid_postfix(GuestTm*); std::size_t APS5_VABI strftime_nid_postfix(char*, std::size_t, const char*, const GuestTm*); void APS5_VABI tzset_nid_postfix(void); char* APS5_VABI ctime_nid_postfix(const std::int64_t*); int* APS5_VABI __error_nid_postfix(); +double APS5_VABI libc_difftime_nid_postfix(std::int64_t, std::int64_t); +double APS5_VABI difftime_nid_postfix(std::int64_t, std::int64_t); } using Converter = GuestTm* (APS5_VABI *)(const std::int64_t*); @@ -68,6 +71,17 @@ static void CheckConcurrent(Converter convert, BufferedConverter buffered, const Require(convert(&invalid) == nullptr, "Unrepresentable time was accepted"); } +static GuestTm UtcTime(std::int64_t timer) { + GuestTm utc{}; + Require(gmtime_s_nid_postfix(&timer, &utc) == &utc, "UTC conversion failed"); + return utc; +} + +static void CheckAsctime(const GuestTm& brokenDown, const char* expected) { + const char* text = asctime_nid_postfix(&brokenDown); + Require(text != nullptr && std::strcmp(text, expected) == 0, "asctime did not format the broken-down time"); +} + struct UtcCase { std::int64_t timer; int year; @@ -92,6 +106,17 @@ static void CheckUtc(const UtcCase& expected) { Require(utc.tm_zone != nullptr && std::strcmp(utc.tm_zone, "UTC") == 0, "UTC conversion did not set tm_zone"); } +struct DifferenceCase { + std::int64_t time1; + std::int64_t time0; + double seconds; +}; + +static void CheckDifference(const DifferenceCase& expected) { + Require(libc_difftime_nid_postfix(expected.time1, expected.time0) == expected.seconds + && difftime_nid_postfix(expected.time1, expected.time0) == expected.seconds, "difftime did not return time1 - time0"); +} + static void CheckFormat(const GuestTm& guest, const char* format, const char* expected) { char buffer[96]{}; const std::size_t size = strftime_nid_postfix(buffer, sizeof(buffer), format, &guest); @@ -180,6 +205,53 @@ static void CheckFreeBsdConversions() { Require(strftime_nid_postfix(tooSmall, sizeof(tooSmall), "%k:%l", &guest) == 0, "strftime accepted an undersized buffer"); } +static void CheckLiteralConversions() { + const std::int64_t timer = 1700000000; + GuestTm guest{}; + Require(gmtime_s_nid_postfix(&timer, &guest) == &guest, "UTC conversion failed"); + struct FormatCase { + const char* format; + const char* expected; + }; + constexpr FormatCase cases[]{ + {"%Q", "Q"}, + {"%P|%f|%N|%1|%#Y", "P|f|N|1|#Y"}, + {"[%Q] %Y-%m-%d", "[Q] 2023-11-14"}, + {"%EQ|%OQ|%_Q|%-Q|%0Q", "Q|Q|Q|Q|Q"}, + {"%E_Q|%_EQ|%O0Q|%-OQ", "Q|Q|Q|Q"}, + {"%EEY|%EOY|%OEY|%OOY", "EY|OY|EY|OY"}, + {"%EE%Y|%OO%k", "E2023|O22"}, + {"%--Y|%__Y|%00Y|%_0Y|%0-Y", "-Y|_Y|0Y|0Y|-Y"}, + {"%E_EY|%_EOY|%O0OY", "EY|OY|OY"}, + {"%", "%"}, + {"text%", "text%"}, + {"%E", "E"}, + {"%O", "O"}, + {"%_", "_"}, + {"%-", "-"}, + {"%0", "0"}, + {"%E_", "_"}, + {"%_E", "E"}, + {"%Q%", "Q%"}, + {"%k%", "22%"}, + {"%%Q|%%%Q|%%%%Q", "%Q|%Q|%%Q"}, + {"%E%Q|%O%Q|%_%Q|%0%Q|%-%Q", "%Q|%Q|%Q|%Q|%Q"}, + {"%E%%Q|%O%%%Q", "%Q|%%Q"}, + {"%Q|%Ek|%Ol|%Ev|%Y", "Q|22|10|14-Nov-2023|2023"}, + {"%\xc3\xa5", "\xc3\xa5"}, + {"%%", "%"}, + {"literal text", "literal text"}, + }; + for (const auto& test : cases) { + CheckFormat(guest, test.format, test.expected); + CheckBufferSize(guest, test.format, std::strlen(test.expected)); + } + CheckFormat(guest, "", ""); + char buffer[2]{'x', 'y'}; + Require(strftime_nid_postfix(buffer, 0, "%Q", &guest) == 0, "strftime accepted a zero-size buffer"); + Require(buffer[0] == 'x' && buffer[1] == 'y', "strftime wrote to a zero-size buffer"); +} + int main() { #ifdef _WIN32 _putenv_s("TZ", "UTC-2"); @@ -202,6 +274,14 @@ int main() { GuestTm scratch{}; Require(gmtime_s_nid_postfix(&underflow, &scratch) == nullptr, "Unrepresentable negative time was accepted"); + CheckAsctime(UtcTime(0), "Thu Jan 1 00:00:00 1970\n"); + CheckAsctime(UtcTime(-2208988801), "Sun Dec 31 23:59:59 1899\n"); + CheckAsctime(UtcTime(-30610224000), "Wed Jan 1 00:00:00 1000\n"); + GuestTm pastMonthEnd = UtcTime(983404800); + pastMonthEnd.tm_mon = 1; + pastMonthEnd.tm_mday = 29; + CheckAsctime(pastMonthEnd, "Thu Feb 29 00:00:00 2001\n"); + const std::int64_t epoch = 0; GuestTm local{}; std::memset(&local, 0xAA, sizeof(local)); @@ -226,6 +306,21 @@ int main() { Require(std::strcmp(ctime_nid_postfix(&underflow), "??? ??? ?? ??:??:?? ????\n") == 0 && *__error_nid_postfix() == 22, "ctime of an unrepresentable time did not report it"); + constexpr std::int64_t latest = std::numeric_limits::max(); + constexpr std::int64_t earliest = std::numeric_limits::min(); + constexpr std::array differenceCases{{ + {10, 3, 7.0}, + {3, 10, -7.0}, + {0, -1, 1.0}, + {-1, 0, -1.0}, + {-86400, -172800, 86400.0}, + {latest, latest - 1, 1.0}, + {earliest + 1, earliest, 1.0}, + {latest, earliest, 0x1p64}, + {earliest, latest, -0x1p64}, + }}; + for (const auto& differenceCase : differenceCases) CheckDifference(differenceCase); + GuestTm utc{}; Require(gmtime_s_nid_postfix(&epoch, &utc) == &utc, "UTC epoch conversion failed"); char formatted[64]{}; @@ -233,6 +328,7 @@ int main() { && std::strcmp(formatted, "1970-01-01 00:00:00") == 0, "strftime did not read the guest tm"); CheckFreeBsdConversions(); + CheckLiteralConversions(); CheckConcurrent(libc_gmtime_nid_postfix, gmtime_s_nid_postfix, "Concurrent libc_gmtime returned another thread's date"); CheckConcurrent(gmtime_nid_postfix, gmtime_s_nid_postfix, "Concurrent gmtime returned another thread's date"); diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index 2f69a2201..5b4dc2694 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -15,6 +16,7 @@ int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); int APS5_VABI ioctl_nid_postfix(int, std::uint64_t, void*); int APS5_VABI _ioctl_nid_postfix(int, std::uint64_t, void*); +std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); std::int64_t APS5_VABI recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*); int APS5_VABI close_nid_postfix(int); @@ -42,7 +44,61 @@ std::int64_t APS5_VABI sceKernelReadv(int, const Iovec*, int); std::int64_t APS5_VABI sceKernelWritev(int, const Iovec*, int); } static void Require(bool value) { if (!value) std::abort(); } + +static void CheckAddressLengths(int family) { + const std::uint32_t addressSize = family == 2 ? 16 : 28; + const int receiver = socket_nid_postfix(family, 2, 0); + if (receiver < 0 && family == 28 && *__error_nid_postfix() == 47) return; + Require(receiver >= 0); + const int sender = socket_nid_postfix(family, 2, 0); + Require(sender >= 0); + std::array destination{}; + destination[0] = static_cast(addressSize); + destination[1] = static_cast(family); + if (family == 2) { destination[4] = 127; destination[7] = 1; } + else destination[23] = 1; + const int bound = bind_nid_postfix(receiver, destination.data(), addressSize); + if (bound < 0 && family == 28 && *__error_nid_postfix() == 49) { + Require(close_nid_postfix(sender) == 0 && close_nid_postfix(receiver) == 0); + return; + } + Require(bound == 0); + std::uint32_t size = destination.size(); + Require(getsockname_nid_postfix(receiver, destination.data(), &size) == 0 && size == addressSize); + Require(connect_nid_postfix(sender, destination.data(), addressSize) == 0); + std::array expected{}; + size = expected.size(); + Require(getsockname_nid_postfix(sender, expected.data(), &size) == 0 && size == addressSize); + for (std::uint32_t capacity : {0u, 1u, 2u, addressSize - 1, addressSize, 32u, UINT32_MAX}) { + const auto copied = std::min(capacity, addressSize); + std::array address; + address.fill(0xa5); + size = capacity; + Require(getsockname_nid_postfix(receiver, address.data(), &size) == 0 && size == copied); + Require(std::memcmp(address.data(), destination.data(), copied) == 0); + Require(std::all_of(address.begin() + copied, address.end(), [](auto byte) { return byte == 0xa5; })); + const char payload[] = "address length"; + char received[sizeof(payload)]{}; + Require(sendto_nid_postfix(sender, payload, sizeof(payload), 0, nullptr, 0) == sizeof(payload)); + address.fill(0xa5); + size = capacity; + Require(recvfrom_nid_postfix(receiver, received, sizeof(received), 2, address.data(), &size) == sizeof(payload)); + Require(size == copied && std::memcmp(address.data(), expected.data(), copied) == 0); + Require(std::all_of(address.begin() + copied, address.end(), [](auto byte) { return byte == 0xa5; })); + address.fill(0xa5); + Iovec vector{received, sizeof(received)}; + Msghdr message{address.data(), capacity, &vector, 1, nullptr, 0, -1}; + Require(recvmsg_nid_postfix(receiver, &message, 0) == sizeof(payload)); + Require(message.nameLength == copied && std::memcmp(received, payload, sizeof(payload)) == 0); + Require(std::memcmp(address.data(), expected.data(), copied) == 0); + Require(std::all_of(address.begin() + copied, address.end(), [](auto byte) { return byte == 0xa5; })); + } + Require(close_nid_postfix(sender) == 0 && close_nid_postfix(receiver) == 0); +} + int main() { + CheckAddressLengths(2); + CheckAddressLengths(28); const int receiver = socket_nid_postfix(2, 2, 0); const int sender = socket_nid_postfix(2, 2, 17); Require(receiver >= 0 && sender >= 0 && receiver != sender); @@ -68,13 +124,13 @@ int main() { Require(getsockopt_nid_postfix(sender, 0xffff, 0x20, &option, &optionSize) == 0 && option != 0); Require(setsockopt_nid_postfix(sender, 0xffff, 12345, &enabled, sizeof(enabled)) == -1); Require(*__error_nid_postfix() == 42); -#ifndef _WIN32 const int unconnected = socket_nid_postfix(2, 2, 0); Require(unconnected >= 0); + Require(send_nid_postfix(unconnected, "x", 1, 0) == -1); + Require(*__error_nid_postfix() == 39); Require(sendto_nid_postfix(unconnected, "x", 1, 0, nullptr, 0) == -1); Require(*__error_nid_postfix() == 39); Require(close_nid_postfix(unconnected) == 0); -#endif const int unsized = socket_nid_postfix(2, 2, 0); std::array unsizedAddress{0, 2, 0, 0, 127, 0, 0, 1}; Require(unsized >= 0 && bind_nid_postfix(unsized, unsizedAddress.data(), unsizedAddress.size()) == 0); @@ -163,4 +219,14 @@ int main() { const int stream = socket_nid_postfix(2, 1, 0); Require(stream >= 0); Require(close_nid_postfix(stream) == 0); + const int nonblocking = socket_nid_postfix(2, 2 | 0x20000000, 0); + Require(nonblocking >= 0 && fcntl_nid_postfix(nonblocking, 3) == 6); + std::array nonblockingAddress{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(nonblocking, nonblockingAddress.data(), nonblockingAddress.size()) == 0); + Require(recvfrom_nid_postfix(nonblocking, buffer, sizeof(buffer), 0, nullptr, nullptr) == -1 && *__error_nid_postfix() == 35); + Require(close_nid_postfix(nonblocking) == 0); + const int closeOnExec = socket_nid_postfix(2, 1 | 0x10000000, 6); + Require(closeOnExec >= 0 && fcntl_nid_postfix(closeOnExec, 3) == 2); + Require(close_nid_postfix(closeOnExec) == 0); + Require(socket_nid_postfix(2, 0x20000000, 0) == -1 && *__error_nid_postfix() == 43); } diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index 823c0536a..83d7afc73 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -8,6 +8,13 @@ int APS5_VABI sceUserServiceGetForegroundUser(int* user_id); int APS5_VABI sceUserServiceGetRegisteredUserIdList(UserServiceRegisteredUserIdList* user_id_list); int APS5_VABI sceUserServiceGetUserColor(int user_id, int* color); int APS5_VABI sceUserServiceGetNpAccountId(int user_id, std::uint64_t* account_id); +int APS5_VABI sceUserServiceGetAgeLevel(int user_id, std::uint32_t* age_level); +int APS5_VABI sceUserServiceGetAccessibilityChatTranscription(int user_id, std::int32_t* value); +int APS5_VABI sceUserServiceGetAccessibilityPressAndHoldDelay(int user_id, std::int32_t* value); +int APS5_VABI sceUserServiceGetAccessibilityTriggerEffect(int user_id, std::int32_t* value); +int APS5_VABI sceUserServiceGetAccessibilityVibration(int user_id, std::int32_t* value); +int APS5_VABI sceUserServiceGetAccessibilityZoomEnabled(int user_id, std::int32_t* value); +int APS5_VABI sceUserServiceGetAccessibilityZoomFollowFocus(int user_id, std::int32_t* value); } namespace { @@ -41,4 +48,39 @@ int main() { Require(sceUserServiceGetNpAccountId(123, &account_id) == USER_SERVICE_ERROR_INVALID_ARGUMENT); Require(sceUserServiceGetNpAccountId(USER_SERVICE_INITIAL_USER_ID, &account_id) == USER_SERVICE_OK); Require(account_id == 0); + + std::uint32_t age_level = 0x5a5a5a5au; + Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + const int absent_users[] = {0, USER_SERVICE_USER_ID_INVALID, 0xff, 0x12345678}; + for (int user_id : absent_users) { + Require(sceUserServiceGetAgeLevel(user_id, &age_level) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + Require(age_level == 0x5a5a5a5au); + Require(sceUserServiceGetAgeLevel(user_id, nullptr) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + } + Require(sceUserServiceGetAgeLevel(USER_SERVICE_INITIAL_USER_ID, &age_level) == USER_SERVICE_OK); + Require(age_level == 0xffffffffu); + + struct Setting { + int (APS5_VABI *get)(int, std::int32_t*); + std::int32_t value; + }; + const Setting settings[] = { + {sceUserServiceGetAccessibilityChatTranscription, 0}, + {sceUserServiceGetAccessibilityPressAndHoldDelay, 0}, + {sceUserServiceGetAccessibilityTriggerEffect, 1}, + {sceUserServiceGetAccessibilityVibration, 1}, + {sceUserServiceGetAccessibilityZoomEnabled, 0}, + {sceUserServiceGetAccessibilityZoomFollowFocus, 1}, + }; + for (const Setting& setting : settings) { + std::int32_t value = 0x5a5a5a5a; + Require(setting.get(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + for (int user_id : absent_users) { + Require(setting.get(user_id, &value) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + Require(value == 0x5a5a5a5a); + Require(setting.get(user_id, nullptr) == USER_SERVICE_ERROR_NOT_LOGGED_IN); + } + Require(setting.get(USER_SERVICE_INITIAL_USER_ID, &value) == USER_SERVICE_OK); + Require(value == setting.value); + } } diff --git a/core/libs/tests/GuestUserServiceUserName.cpp b/core/libs/tests/GuestUserServiceUserName.cpp new file mode 100644 index 000000000..f75516297 --- /dev/null +++ b/core/libs/tests/GuestUserServiceUserName.cpp @@ -0,0 +1,49 @@ +#include "SceTypes.hpp" +#include "prx/libSceUserService/UserService.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceUserServiceGetUserName(int user_id, char* name, std::size_t size); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +bool Filled(const char* buffer, std::size_t from, std::size_t to, char value) { + for (std::size_t i = from; i < to; ++i) { + if (buffer[i] != value) return false; + } + return true; +} + +} + +int main() { + char name[32]; + const std::size_t length = std::strlen(USER_SERVICE_INITIAL_USER_NAME); + + for (std::size_t size = 0; size <= USER_SERVICE_MAX_USER_NAME_LENGTH; ++size) { + std::memset(name, 'A', sizeof(name)); + Require(sceUserServiceGetUserName(USER_SERVICE_INITIAL_USER_ID, name, size) == USER_SERVICE_ERROR_BUFFER_TOO_SHORT); + Require(Filled(name, 0, size, 0) && Filled(name, size, sizeof(name), 'A')); + } + for (std::size_t size : {USER_SERVICE_MAX_USER_NAME_LENGTH + 1, sizeof(name)}) { + std::memset(name, 'A', sizeof(name)); + Require(sceUserServiceGetUserName(USER_SERVICE_INITIAL_USER_ID, name, size) == USER_SERVICE_OK); + Require(std::strcmp(name, USER_SERVICE_INITIAL_USER_NAME) == 0); + Require(Filled(name, length, size, 0) && Filled(name, size, sizeof(name), 'A')); + } + + for (int user : {0, USER_SERVICE_USER_ID_INVALID, 1, 0xff, 0x12345678}) { + std::memset(name, 'A', sizeof(name)); + Require(sceUserServiceGetUserName(user, name, sizeof(name)) == USER_SERVICE_ERROR_UNKNOWN_USER); + Require(sceUserServiceGetUserName(user, name, 1) == USER_SERVICE_ERROR_UNKNOWN_USER); + Require(Filled(name, 0, sizeof(name), 'A')); + Require(sceUserServiceGetUserName(user, nullptr, sizeof(name)) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + } + Require(sceUserServiceGetUserName(USER_SERVICE_INITIAL_USER_ID, nullptr, sizeof(name)) == USER_SERVICE_ERROR_INVALID_ARGUMENT); +} diff --git a/core/libs/tests/GuestVoiceQoS.cpp b/core/libs/tests/GuestVoiceQoS.cpp index bf8b3467c..44aa99634 100644 --- a/core/libs/tests/GuestVoiceQoS.cpp +++ b/core/libs/tests/GuestVoiceQoS.cpp @@ -5,6 +5,7 @@ extern "C" { int APS5_VABI sceVoiceQoSInit(void*, std::uint32_t, std::int32_t); +int APS5_VABI sceVoiceQoSEnd(); } static void Require(bool value) { if (!value) std::abort(); } @@ -13,8 +14,10 @@ int main(int argc, char** argv) { Require(argc == 2); constexpr int argumentInvalid = static_cast(0x804E0902); constexpr int initialized = static_cast(0x804E0905); + constexpr int notInitialized = static_cast(0x804E0901); const auto appType = static_cast(std::strtoul(argv[1], nullptr, 16)); std::vector memory(0x40000, 0xAA); + Require(sceVoiceQoSEnd() == notInitialized); Require(sceVoiceQoSInit(nullptr, 0x40000, appType) == argumentInvalid); Require(sceVoiceQoSInit(nullptr, 0, appType) == argumentInvalid); for (std::uint32_t size : {0u, 1u, 0x100u, 0x3FFFFu, 0x40001u, 0x80000u, 0xFFFFFFFFu}) Require(sceVoiceQoSInit(memory.data(), size, appType) == argumentInvalid); @@ -25,4 +28,11 @@ int main(int argc, char** argv) { Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == initialized); Require(sceVoiceQoSInit(nullptr, 0, 0) == initialized); + Require(sceVoiceQoSEnd() == 0); + Require(sceVoiceQoSEnd() == notInitialized); + Require(sceVoiceQoSInit(nullptr, 0x40000, appType) == argumentInvalid); + Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); + Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == initialized); + Require(sceVoiceQoSEnd() == 0); + Require(sceVoiceQoSEnd() == notInitialized); } diff --git a/core/libs/tests/GuestWaitpid.cpp b/core/libs/tests/GuestWaitpid.cpp index 235c57d91..8ef90e7b1 100644 --- a/core/libs/tests/GuestWaitpid.cpp +++ b/core/libs/tests/GuestWaitpid.cpp @@ -2,6 +2,7 @@ #include #include extern "C" { +int APS5_VABI wait_nid_postfix(int*); int APS5_VABI waitpid_nid_postfix(int, int*, int); int APS5_VABI getpid_nid_postfix(void); int* APS5_VABI __error_nid_postfix(); @@ -28,4 +29,12 @@ int main() { for (int options : {0x40, 0x41, 0x80, 0x100, 0x40000000, static_cast(0x80000040u), static_cast(0xffffffffu)}) RequireFailure(pid, options, invalidArgument); } + int status = untouched; + *__error_nid_postfix() = invalidArgument; + Require(wait_nid_postfix(&status) == -1); + Require(*__error_nid_postfix() == noChild); + Require(status == untouched); + *__error_nid_postfix() = invalidArgument; + Require(wait_nid_postfix(nullptr) == -1); + Require(*__error_nid_postfix() == noChild); } diff --git a/core/libs/tests/ParamJsonParser.cpp b/core/libs/tests/ParamJsonParser.cpp index 54a7fd500..1606b7f87 100644 --- a/core/libs/tests/ParamJsonParser.cpp +++ b/core/libs/tests/ParamJsonParser.cpp @@ -49,6 +49,23 @@ static void CheckUserDefinedParams(const std::filesystem::path& path) { } } +static void CheckNesting(const std::filesystem::path& path) { + const std::string prefix = R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}},"extra":)"; + const auto nested = [&](std::size_t arrays) { + return prefix + std::string(arrays, '[') + std::string(arrays, ']') + "}"; + }; + Require(ParseText(path, nested(511)).title == "Example", "Nesting within the limit rejected"); + for (const std::size_t arrays : {std::size_t{512}, std::size_t{513}, std::size_t{100000}}) { + bool rejected = false; + try { + ParseText(path, nested(arrays)); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).find("nested too deeply") != std::string::npos; + } + Require(rejected, "Excessive nesting not rejected: " + std::to_string(arrays)); + } +} + static void CheckLanguages(const std::filesystem::path& path) { const auto nested = ParseText(path, "{\r\n \"localizedParameters\": {\r\n \"defaultLanguage\": \"en-GB\",\r\n" " \"en-GB\": { \"titleName\": \"British\" }\r\n },\r\n \"titleId\": \"PPSA00001\"\r\n}\r\n"); @@ -153,6 +170,7 @@ int main() { CheckLanguages(path); CheckUserDefinedParams(path); CheckOutputPermission(path); + CheckNesting(path); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; result = 1; diff --git a/core/libs/tests/PixelAncillary.cpp b/core/libs/tests/PixelAncillary.cpp index 15740c514..69ce4d7c2 100644 --- a/core/libs/tests/PixelAncillary.cpp +++ b/core/libs/tests/PixelAncillary.cpp @@ -427,6 +427,74 @@ static void HelperLoopRefused(LoopRead read) { } Require(false); } +enum class StartingQuad { Kept, BallotAfterBranch, BallotOfOtherLanes, ComputeStage }; + +static IrValue& StartingQuadMasked(IrProgram& program, StartingQuad shape) { + auto& entry = Begin(program); + if (shape == StartingQuad::ComputeStage) program.Resources().stage = IrShaderStage::Compute; + IrBlock* tail = &entry; + if (shape == StartingQuad::BallotAfterBranch) { + auto& body = program.CreateBlock(); + auto& merge = program.CreateBlock(); + program.BlockOrder().push_back(&body); + program.BlockOrder().push_back(&merge); + entry.AddBranch(&body); + entry.AddBranch(&merge); + body.AddBranch(&merge); + tail = &merge; + } + IrBuilder builder(program); + builder.SetInsertionPoint(entry); + IrValue& ancillary = AncillaryBuiltin(builder); + IrValue& helper = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast(StageInputKind::HelperInvocation)), &builder.Constant(0u)}); + IrValue& started = shape == StartingQuad::BallotOfOtherLanes ? FrontFacingBuiltin(builder) : builder.IEqual(helper, builder.Constant(0u)); + IrValue& live = builder.Emit(IrOpcode::Ballot, IrType::Vec4U32, {&started}); + builder.SetInsertionPoint(*tail); + IrValue& active = builder.Emit(IrOpcode::Ballot, IrType::Vec4U32, {&builder.ConstantBool(true)}); + IrValue& low = builder.BitwiseAnd(builder.CompositeExtract(live, 0u), builder.CompositeExtract(active, 0u)); + IrValue& high = builder.BitwiseAnd(builder.CompositeExtract(live, 1u), builder.CompositeExtract(active, 1u)); + IrValue& quads = builder.Emit(IrOpcode::WqmU64, IrType::U64, {&builder.Emit(IrOpcode::CompositeConstructU64, IrType::U64, {&low, &high})}); + IrValue& lane = LaneBit(builder, builder.Emit(IrOpcode::CompositeExtractU64, IrType::U32, {&quads, &builder.Constant(0u)}), builder.Emit(IrOpcode::CompositeExtractU64, IrType::U32, {&quads, &builder.Constant(1u)})); + IrValue& kept = builder.Select(lane, builder.Constant(SelectOther), ancillary); + End(builder, kept); + return kept; +} +static const IrValue* StartingQuadExported(IrProgram& program, StartingQuad shape) { + static_cast(StartingQuadMasked(program, shape)); + if (shape == StartingQuad::ComputeStage) { + ConstantFolder().Fold(program); + DeadCodeEliminator().Eliminate(program); + } else { + Lower(program); + } + for (const IrBlock* block : program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + if (inst->Opcode() == IrOpcode::ReferenceU32) return inst->Argument(0)->Resolve(); + } + } + return nullptr; +} +static void StartingQuadOnly() { + IrProgram program; + const IrValue* value = StartingQuadExported(program, StartingQuad::Kept); + Require(value != nullptr && value->HasImmediate() && value->ImmediateU32() == SelectOther); +} +static void StartingQuadOutsidePixel() { + IrProgram program; + const IrValue* value = StartingQuadExported(program, StartingQuad::ComputeStage); + Require(value != nullptr && value->Opcode() == IrOpcode::SelectU32); +} +static void StartingQuadRefused(StartingQuad shape) { + IrProgram program; + static_cast(StartingQuadMasked(program, shape)); + try { + Lower(program); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("unsupported live use") != std::string::npos); + return; + } + Require(false); +} int main() { Extract(IrOpcode::BitFieldUExtract, 8u, 4u, StageInputKind::SampleId, 0u); Extract(IrOpcode::BitFieldUExtract, 9u, 2u, StageInputKind::SampleId, 1u); @@ -476,4 +544,8 @@ int main() { HelperLoopOnly(LoopRead::ExportInside); HelperLoopOnly(LoopRead::UniformLoop); HelperLoopRefused(LoopRead::RevivedExportInside); + StartingQuadOnly(); + StartingQuadRefused(StartingQuad::BallotAfterBranch); + StartingQuadRefused(StartingQuad::BallotOfOtherLanes); + StartingQuadOutsidePixel(); } diff --git a/core/libs/tests/SaveDataMemoryGrowth.cpp b/core/libs/tests/SaveDataMemoryGrowth.cpp index 95d5225a3..87d06e2a3 100644 --- a/core/libs/tests/SaveDataMemoryGrowth.cpp +++ b/core/libs/tests/SaveDataMemoryGrowth.cpp @@ -82,10 +82,10 @@ int main(int argc, char**) { std::fprintf(stderr, "Read denial unavailable for this user\n"); exitCode = 77; } else { - Require(Read(path) == original); Require(status == static_cast(0x809F000Bu)); + Require(Read(path) == original); Require(result.existed_memory_size == 123); - Require(!std::filesystem::exists("_sd_mem/u7531/slot0.param")); + Require(std::filesystem::is_regular_file("_sd_mem/u7531/slot0.param")); Require(!std::filesystem::exists(path.string() + ".tmp")); } } else { diff --git a/core/libs/tests/SaveDataMemoryMetadata.cpp b/core/libs/tests/SaveDataMemoryMetadata.cpp index 70724bd18..8f40cccf5 100644 --- a/core/libs/tests/SaveDataMemoryMetadata.cpp +++ b/core/libs/tests/SaveDataMemoryMetadata.cpp @@ -19,6 +19,7 @@ extern "C" { int APS5_VABI sceSaveDataInitialize3(const void*); int APS5_VABI sceSaveDataTerminate(); int APS5_VABI sceSaveDataSetSaveDataMemory2(const SaveDataMemorySet2*); +int APS5_VABI sceSaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2*, SaveDataMemorySetupResult*); } static void Check(bool value, int line) { @@ -115,6 +116,176 @@ int main() { Require(sceSaveDataSetSaveDataMemory2(&set) == 0); std::copy(first.begin(), first.end(), memoryExpected.begin() + 4); Require(Read(memoryPath) == memoryExpected); + const auto setupParamPath = std::filesystem::path("_sd_mem/u42/slot0.param"); + const auto setupBinPath = std::filesystem::path("_sd_mem/u42/slot0.bin"); + const auto setupParamTemp = setupParamPath.string() + ".tmp"; + Require(std::filesystem::create_directories(setupParamPath.parent_path())); + Require(std::filesystem::create_directory(setupParamTemp)); + SaveDataParam setupParam{}; + setupParam.user_param = 100; + SaveDataMemorySetup2 setup2{}; + setup2.user_id = 42; + setup2.slot_id = 0; + setup2.memory_size = 128; + setup2.option = 1; + setup2.init_param = &setupParam; + SaveDataMemorySetupResult setupResult{}; + setupResult.existed_memory_size = 555; + Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == static_cast(0x809F000Bu)); + Require(setupResult.existed_memory_size == 555); + Require(!std::filesystem::exists(setupBinPath)); + Require(!std::filesystem::exists(setupParamPath)); + Require(!std::filesystem::exists(setupParamTemp)); + Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); + Require(setupResult.existed_memory_size == 0); + Require(Read(setupBinPath) == std::vector(128, 0)); + const auto* setupBytes = reinterpret_cast(&setupParam); + Require(Read(setupParamPath) == std::vector(setupBytes, setupBytes + sizeof(setupParam))); + std::vector seededBin(128); + for (std::size_t i = 0; i < seededBin.size(); ++i) { + seededBin[i] = static_cast(static_cast(i * 3 + 7)); + } + { + std::ofstream seededFile(setupBinPath, std::ios::binary); + seededFile.write(seededBin.data(), static_cast(seededBin.size())); + Require(static_cast(seededFile)); + } + const auto growthParamTemp = setupParamPath.string() + ".tmp"; + Require(std::filesystem::create_directory(growthParamTemp)); + SaveDataParam previousParam = setupParam; + setupParam.user_param = 200; + setup2.memory_size = 256; + setupResult.existed_memory_size = 999; + Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == static_cast(0x809F000Bu)); + Require(setupResult.existed_memory_size == 999); + Require(Read(setupBinPath) == seededBin); + const auto* previousBytes = reinterpret_cast(&previousParam); + Require(Read(setupParamPath) == std::vector(previousBytes, previousBytes + sizeof(previousParam))); + Require(!std::filesystem::exists(growthParamTemp)); + Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); + Require(setupResult.existed_memory_size == 128); + std::vector expectedGrownBin = seededBin; + expectedGrownBin.resize(256, 0); + Require(Read(setupBinPath) == expectedGrownBin); + const auto* updatedBytes = reinterpret_cast(&setupParam); + Require(Read(setupParamPath) == std::vector(updatedBytes, updatedBytes + sizeof(setupParam))); + const auto growthBinTemp = setupBinPath.string() + ".tmp"; + Require(std::filesystem::create_directory(growthBinTemp)); + setupParam.user_param = 300; + setup2.memory_size = 512; + setupResult.existed_memory_size = 888; + Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == static_cast(0x809F000Bu)); + Require(setupResult.existed_memory_size == 888); + Require(Read(setupBinPath) == expectedGrownBin); + const auto* param300Bytes = reinterpret_cast(&setupParam); + Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); + Require(!std::filesystem::exists(growthBinTemp)); + Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); + Require(setupResult.existed_memory_size == 256); + std::vector expectedBlob512 = expectedGrownBin; + expectedBlob512.resize(512, 0); + Require(Read(setupBinPath) == expectedBlob512); + Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); + const auto freshFailBinPath = std::filesystem::path("_sd_mem/u55/slot0.bin"); + const auto freshFailParamPath = std::filesystem::path("_sd_mem/u55/slot0.param"); + Require(std::filesystem::create_directories(freshFailBinPath.parent_path())); + const auto freshFailBinTemp = freshFailBinPath.string() + ".tmp"; + Require(std::filesystem::create_directory(freshFailBinTemp)); + SaveDataParam freshFailParam{}; + freshFailParam.user_param = 55; + SaveDataMemorySetup2 freshFailSetup{}; + freshFailSetup.user_id = 55; + freshFailSetup.slot_id = 0; + freshFailSetup.memory_size = 64; + freshFailSetup.option = 1; + freshFailSetup.init_param = &freshFailParam; + SaveDataMemorySetupResult freshFailResult{}; + freshFailResult.existed_memory_size = 777; + Require(sceSaveDataSetupSaveDataMemory2(&freshFailSetup, &freshFailResult) == static_cast(0x809F000Bu)); + Require(freshFailResult.existed_memory_size == 777); + Require(!std::filesystem::exists(freshFailBinPath)); + const auto* freshFailBytes = reinterpret_cast(&freshFailParam); + Require(Read(freshFailParamPath) == std::vector(freshFailBytes, freshFailBytes + sizeof(freshFailParam))); + Require(!std::filesystem::exists(freshFailBinTemp)); + Require(sceSaveDataSetupSaveDataMemory2(&freshFailSetup, &freshFailResult) == 0); + Require(freshFailResult.existed_memory_size == 0); + Require(Read(freshFailBinPath) == std::vector(64, 0)); + Require(Read(freshFailParamPath) == std::vector(freshFailBytes, freshFailBytes + sizeof(freshFailParam))); + SaveDataParam noParam{}; + noParam.user_param = 5; + SaveDataMemorySetup2 noParamSetup{}; + SaveDataMemorySetupResult noParamResult{}; + noParamSetup.user_id = 56; + noParamSetup.slot_id = 0; + noParamSetup.memory_size = 16; + noParamSetup.option = 1; + noParamSetup.init_param = nullptr; + Require(sceSaveDataSetupSaveDataMemory2(&noParamSetup, &noParamResult) == 0); + Require(Read("_sd_mem/u56/slot0.bin") == std::vector(16, 0)); + Require(!std::filesystem::exists("_sd_mem/u56/slot0.param")); + noParamSetup.user_id = 57; + noParamSetup.option = 0; + noParamSetup.init_param = &noParam; + Require(sceSaveDataSetupSaveDataMemory2(&noParamSetup, &noParamResult) == 0); + Require(Read("_sd_mem/u57/slot0.bin") == std::vector(16, 0)); + Require(!std::filesystem::exists("_sd_mem/u57/slot0.param")); + SaveDataParam userA{}; + userA.user_param = 1; + SaveDataParam userB{}; + userB.user_param = 2; + SaveDataMemorySetup2 isoSetup{}; + SaveDataMemorySetupResult isoResult{}; + isoSetup.user_id = 60; + isoSetup.slot_id = 0; + isoSetup.memory_size = 8; + isoSetup.option = 1; + isoSetup.init_param = &userA; + Require(sceSaveDataSetupSaveDataMemory2(&isoSetup, &isoResult) == 0); + isoSetup.slot_id = 1; + isoSetup.init_param = &userB; + Require(sceSaveDataSetupSaveDataMemory2(&isoSetup, &isoResult) == 0); + const auto* userABytes = reinterpret_cast(&userA); + const auto* userBBytes = reinterpret_cast(&userB); + Require(Read("_sd_mem/u60/slot0.param") == std::vector(userABytes, userABytes + sizeof(userA))); + Require(Read("_sd_mem/u60/slot1.param") == std::vector(userBBytes, userBBytes + sizeof(userB))); + isoSetup.user_id = 61; + isoSetup.slot_id = 0; + Require(sceSaveDataSetupSaveDataMemory2(&isoSetup, &isoResult) == 0); + Require(Read("_sd_mem/u60/slot0.param") == std::vector(userABytes, userABytes + sizeof(userA))); + Require(Read("_sd_mem/u61/slot0.param") == std::vector(userBBytes, userBBytes + sizeof(userB))); + const auto orphanParamPath = std::filesystem::path("_sd_mem/u99/slot0.param"); + const auto orphanBinPath = std::filesystem::path("_sd_mem/u99/slot0.bin"); + Require(std::filesystem::create_directories(orphanBinPath.parent_path())); + const std::vector orphanBin(64, 'k'); + { + std::ofstream orphanFile(orphanBinPath, std::ios::binary); + orphanFile.write(orphanBin.data(), static_cast(orphanBin.size())); + Require(static_cast(orphanFile)); + } + SaveDataParam orphanParam{}; + orphanParam.user_param = 42; + SaveDataMemorySetup2 orphanSetup{}; + orphanSetup.user_id = 99; + orphanSetup.slot_id = 0; + orphanSetup.memory_size = 64; + orphanSetup.option = 1; + orphanSetup.init_param = &orphanParam; + SaveDataMemorySetupResult orphanResult{}; + orphanResult.existed_memory_size = 333; + Require(sceSaveDataSetupSaveDataMemory2(&orphanSetup, &orphanResult) == 0); + Require(orphanResult.existed_memory_size == 64); + Require(Read(orphanBinPath) == orphanBin); + const auto* orphanBytes = reinterpret_cast(&orphanParam); + Require(Read(orphanParamPath) == std::vector(orphanBytes, orphanBytes + sizeof(orphanParam))); + Require(sceSaveDataTerminate() == 0); + Require(Read(setupBinPath) == expectedBlob512); + Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); + Require(sceSaveDataInitialize3(nullptr) == 0); + setupResult.existed_memory_size = 111; + Require(sceSaveDataSetupSaveDataMemory2(&setup2, &setupResult) == 0); + Require(setupResult.existed_memory_size == 512); + Require(Read(setupBinPath) == expectedBlob512); + Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); Require(sceSaveDataTerminate() == 0); std::filesystem::current_path(previous); std::filesystem::remove_all(root); diff --git a/core/libs/tests/ThreadLocalDestructors.cmake b/core/libs/tests/ThreadLocalDestructors.cmake new file mode 100644 index 000000000..b87abde07 --- /dev/null +++ b/core/libs/tests/ThreadLocalDestructors.cmake @@ -0,0 +1,38 @@ +file(GLOB modules "${MODULES}/*.prx") +if(NOT modules) + message(FATAL_ERROR "no modules in ${MODULES}") +endif() +set(explicit "__cxa_thread_atexit_impl_nid_postfix|LibcInternalExtCxaThreadAtexit_nid_postfix") +set(failures "") +foreach(module IN LISTS modules) + get_filename_component(name "${module}" NAME) + execute_process(COMMAND "${NM}" --defined-only "${module}" OUTPUT_VARIABLE symbols RESULT_VARIABLE result ERROR_QUIET) + if(NOT result EQUAL 0 OR symbols STREQUAL "") + list(APPEND failures "${name}: no symbol table to inspect") + continue() + endif() + if(NOT symbols MATCHES " T __mingw_cxa_thread_atexit\n") + continue() + endif() + execute_process(COMMAND "${OBJDUMP}" -d --no-show-raw-insn "${module}" OUTPUT_VARIABLE code RESULT_VARIABLE result ERROR_QUIET) + if(NOT result EQUAL 0) + list(APPEND failures "${name}: cannot disassemble") + continue() + endif() + string(REGEX MATCHALL "\n[0-9a-f]+ <[^>\n]+>:|(call|jmp)[^\n]*<__cxa_thread_atexit>" lines "${code}") + set(function "") + foreach(line IN LISTS lines) + if(line MATCHES "^\n[0-9a-f]+ <([^>\n]+)>:$") + set(function "${CMAKE_MATCH_1}") + elseif(NOT function MATCHES "^(${explicit})$") + list(APPEND failures "${name}: ${function} registers a thread_local destructor") + endif() + endforeach() +endforeach() +if(failures) + list(REMOVE_DUPLICATES failures) + list(JOIN failures "\n " text) + message(FATAL_ERROR "On Windows a thread_local destructor in a module runs after emulated TLS freed the object; keep such objects in HostThreadLocal:\n ${text}") +endif() +list(LENGTH modules count) +message(STATUS "${count} modules register no thread_local destructors") diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 095915729..1f710b3fd 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -53,6 +53,8 @@ add_executable(relinker ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ClzeroLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalLowering.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/parsing/ElfReader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/SyscallScanner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp @@ -141,6 +143,8 @@ if(WIN32) ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ClzeroLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalLowering.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp ) target_include_directories(amd64_only_windows_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include ${CMAKE_CURRENT_SOURCE_DIR}/domain/include ${CMAKE_CURRENT_SOURCE_DIR}/io/include ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include ${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libc/src/specifics/windows) if(BUILD_TESTING) @@ -161,6 +165,22 @@ if(BUILD_TESTING) add_test(NAME elf_reader_bounds COMMAND elf_reader_bounds_tests) set_tests_properties(elf_reader_bounds PROPERTIES TIMEOUT 20) + add_test_executable(call_site_index_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/CallSiteIndexTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp + ) + target_include_directories(call_site_index_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ) + add_test(NAME call_site_index COMMAND call_site_index_tests) + set_tests_properties(call_site_index PROPERTIES TIMEOUT 20) + add_test_executable(autorun_exit_tests ${CMAKE_CURRENT_SOURCE_DIR}/cli/tests/AutorunExitTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp @@ -288,6 +308,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ClzeroLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalLowering.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxElfPatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/SegmentFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/EntryStubBuilder.cpp @@ -303,6 +325,18 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/io/include ) add_test(NAME amd64_only_converter COMMAND amd64_only_converter_tests) + add_test_executable(call_site_resolver_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/CallSiteResolverTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp + ) + target_include_directories(call_site_resolver_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ) + add_test(NAME call_site_resolver COMMAND call_site_resolver_tests) add_test_executable(decoder_length_tests ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/DecoderLengthTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp @@ -325,11 +359,14 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_rela_size.py $) add_test(NAME none_relocations COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_none_relocations.py $) + add_test(NAME guest_none_relocations COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_none_relocations.py $) + set_tests_properties(guest_none_relocations PROPERTIES TIMEOUT 20) add_test(NAME optional_plt COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $) add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_empty_tls.py $) - set(tlsCoverageGroups metadata padding register_loads displacement_bounds alu_execution register_address_loads rejected) + set(tlsCoverageGroups metadata padding register_loads displacement_bounds alu_execution register_address_loads moved_branch rejected) foreach(register IN ITEMS 0 1 2 3 5 6 7 8 9 10 11 12 13 14 15) list(APPEND tlsCoverageGroups displacement_r${register}) endforeach() @@ -365,6 +402,9 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_gui.py $) add_test(NAME windows_working_directory COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_working_directory.py $) + add_test(NAME windows_runpath COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_runpath.py $) + set_tests_properties(windows_runpath PROPERTIES TIMEOUT 30) add_test(NAME windows_address_space COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_address_space.py $) add_test(NAME windows_tls_thread_pointer COMMAND ${Python3_EXECUTABLE} @@ -452,6 +492,21 @@ if(BUILD_TESTING) else() message(STATUS "Python3 interpreter not found; skipping Python relinker tests") endif() + add_test_executable(unwind_record_bounds_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/UnwindRecordBoundsTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp + ) + target_include_directories(unwind_record_bounds_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ) + add_test(NAME unwind_record_bounds COMMAND unwind_record_bounds_tests) + set_tests_properties(unwind_record_bounds PROPERTIES TIMEOUT 20) + add_test_executable(windows_icon_resource_tests ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/WindowsIconResourceTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp @@ -475,6 +530,20 @@ if(BUILD_TESTING) ) add_test(NAME entry_stub COMMAND entry_stub_tests) + add_test_executable(control_flow_graph_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/ControlFlowGraphTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp + ) + target_include_directories(control_flow_graph_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ) + add_test(NAME control_flow_graph COMMAND control_flow_graph_tests) + set_tests_properties(control_flow_graph PROPERTIES TIMEOUT 20) + add_test_executable(strict_nid_filter_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/StrictNidFilterTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index eef10daad..21d055d6b 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -71,6 +71,8 @@ Args ParseArgs(int argc, char* argv[]) { args.windowsGui = true; } else if (arg.rfind("--", 0) == 0 || arg == "unused-filter") { throw std::runtime_error("unknown option: " + arg); + } else if (arg.empty()) { + throw std::runtime_error("input and output paths must be nonempty"); } else if (args.inputPath.empty()) { args.inputPath = arg; } else if (args.outputPath.empty()) { diff --git a/core/relinker/cli/tests/CliArgsTests.cpp b/core/relinker/cli/tests/CliArgsTests.cpp index ec27e771a..3cc339e55 100644 --- a/core/relinker/cli/tests/CliArgsTests.cpp +++ b/core/relinker/cli/tests/CliArgsTests.cpp @@ -163,6 +163,13 @@ void unknownOption() { expectThrows({"--", "input.elf", "output.elf"}, "unknown option: --"); } +void emptyPositionals() { + for (const auto& arguments : std::vector>{ + {"", "input.elf", "output.elf"}, {"input.elf", "", "output.elf"}, + {"", "output.elf"}, {"input.elf", ""}}) + expectThrows(arguments, "input and output paths must be nonempty"); +} + void tooManyPositionals() { expectThrows({"input.elf", "output.elf", "extra.elf"}, "unexpected argument: extra.elf"); } @@ -211,6 +218,7 @@ int main() { unusedFilterRejectsDuplicates(); unusedFilterBareWordIsUnknown(); unknownOption(); + emptyPositionals(); tooManyPositionals(); windowsDiagnosticsRequiresWindows(); windowsGuiRequiresWindows(); diff --git a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp index b7ee6f081..7f09e114a 100644 --- a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp +++ b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp @@ -41,6 +41,7 @@ inline constexpr Entry kMcommit = {"MCOMMIT", nullptr, 0}; inline constexpr Entry kRdseed = {"RDSEED", nullptr, 0}; inline constexpr Entry kRdpid = {"RDPID", nullptr, 0}; inline constexpr Entry kClwb = {"CLWB", nullptr, 0}; +inline constexpr Entry kWideStore = {"256-bit store", nullptr, 0}; inline constexpr Entry kExtrq = {"EXTRQ", nullptr, 0}; inline constexpr Entry kInsertq = {"INSERTQ", nullptr, 0}; inline constexpr Entry kExtrqRegisterForm = {"EXTRQ register form", nullptr, 0}; @@ -49,6 +50,8 @@ inline constexpr Entry kMovntss = {"MOVNTSS", nullptr, 0}; inline constexpr Entry kMovntsd = {"MOVNTSD", nullptr, 0}; inline constexpr Entry kVrcpps = {"VRCPPS", nullptr, 0}; inline constexpr Entry kVrsqrtps = {"VRSQRTPS", nullptr, 0}; +inline constexpr Entry kRcpps = {"RCPPS", nullptr, 0}; +inline constexpr Entry kRsqrtps = {"RSQRTPS", nullptr, 0}; inline constexpr Entry kSha256rnds2 = {"SHA256RNDS2", nullptr, 0}; inline constexpr Entry kSha256msg1 = {"SHA256MSG1", nullptr, 0}; inline constexpr Entry kSha256msg2 = {"SHA256MSG2", nullptr, 0}; diff --git a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp index bcb5df816..e2f594642 100644 --- a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp +++ b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp @@ -35,6 +35,7 @@ struct Amd64OnlyMatch { std::size_t ReturnBranchOffset; bool Optional = false; std::vector Relocations = {}; + bool RelocatableWindow = false; }; struct Amd64OnlySubstitutionReport { diff --git a/core/relinker/codegen/include/codegen/x86/IAmd64OnlyInstructionMatcher.hpp b/core/relinker/codegen/include/codegen/x86/IAmd64OnlyInstructionMatcher.hpp index df17eb175..9986e7928 100644 --- a/core/relinker/codegen/include/codegen/x86/IAmd64OnlyInstructionMatcher.hpp +++ b/core/relinker/codegen/include/codegen/x86/IAmd64OnlyInstructionMatcher.hpp @@ -2,6 +2,7 @@ #define CODEGEN_X86_IAMD64ONLYINSTRUCTIONMATCHER_HPP #include +#include #include #include #include @@ -23,6 +24,11 @@ public: std::span> instructions, std::span trailing ) const = 0; + + virtual void EmitOutOfLine( + StubBodyBuilder& body, + std::span instruction + ) const = 0; }; std::unique_ptr MakeAmd64OnlyInstructionMatcher(Amd64OnlyTarget target = Amd64OnlyTarget::Intel); diff --git a/core/relinker/codegen/include/codegen/x86/ReciprocalOperands.hpp b/core/relinker/codegen/include/codegen/x86/ReciprocalOperands.hpp index 31102844e..ca3820623 100644 --- a/core/relinker/codegen/include/codegen/x86/ReciprocalOperands.hpp +++ b/core/relinker/codegen/include/codegen/x86/ReciprocalOperands.hpp @@ -16,9 +16,10 @@ struct ReciprocalOperands { ReciprocalOperation Operation; std::uint8_t Destination; std::uint8_t Source; + bool Vex; }; -[[nodiscard]] std::optional DecodeVexReciprocal(const std::uint8_t* data, std::size_t length); +[[nodiscard]] std::optional DecodeReciprocal(const std::uint8_t* data, std::size_t length); } diff --git a/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp b/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp index 212eeea54..f2ff3c365 100644 --- a/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp +++ b/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp @@ -1,6 +1,7 @@ #ifndef CODEGEN_X86_STUBBODYBUILDER_HPP #define CODEGEN_X86_STUBBODYBUILDER_HPP +#include #include #include #include @@ -53,6 +54,10 @@ public: void Restore(std::uint8_t reg); void Raw(std::span bytes); void Move(std::span instruction, std::optional ripDisplacementOffset); + void RipRelative(std::span instruction, std::size_t displacementOffset, std::int64_t target); + void MoveControlFlow(std::span instruction, const DecodedInstructionInfo& info); + void JumpTo(std::int64_t siteTarget); + void Seek(std::int64_t siteOffset); void Advance(std::size_t length); [[nodiscard]] LoweredBody Finish(); @@ -63,12 +68,15 @@ private: std::size_t ConstantIndex; }; + void _rel32(std::initializer_list opcode, std::int64_t siteTarget); + void _pushReturnAddress(std::int64_t siteTarget); + std::vector _bytes; std::vector _fixups; std::vector _constants; std::vector _relocations; std::size_t _stackDepth = 0; - std::size_t _siteOffset = 0; + std::int64_t _siteOffset = 0; }; } diff --git a/core/relinker/codegen/include/codegen/x86/WideStoreLowering.hpp b/core/relinker/codegen/include/codegen/x86/WideStoreLowering.hpp new file mode 100644 index 000000000..7e27e2796 --- /dev/null +++ b/core/relinker/codegen/include/codegen/x86/WideStoreLowering.hpp @@ -0,0 +1,16 @@ +#ifndef CODEGEN_X86_WIDESTORELOWERING_HPP +#define CODEGEN_X86_WIDESTORELOWERING_HPP + +#include +#include + +namespace Codegen { + +class WideStoreLowering { +public: + void EmitOutOfLine(StubBodyBuilder& body, const WideStoreOperands& operands) const; +}; + +} + +#endif diff --git a/core/relinker/codegen/include/codegen/x86/WideStoreOperands.hpp b/core/relinker/codegen/include/codegen/x86/WideStoreOperands.hpp new file mode 100644 index 000000000..1716af37b --- /dev/null +++ b/core/relinker/codegen/include/codegen/x86/WideStoreOperands.hpp @@ -0,0 +1,26 @@ +#ifndef CODEGEN_X86_WIDESTOREOPERANDS_HPP +#define CODEGEN_X86_WIDESTOREOPERANDS_HPP + +#include +#include +#include +#include + +namespace Codegen { + +struct WideStoreHalf { + std::vector Bytes; + std::optional RipDisplacementOffset; + std::int64_t RipTarget = 0; +}; + +struct WideStoreOperands { + WideStoreHalf Low; + WideStoreHalf High; +}; + +[[nodiscard]] std::optional DecodeVexWideStore(const std::uint8_t* data, std::size_t length); + +} + +#endif diff --git a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp index 889d0c447..7a9a14a36 100644 --- a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp +++ b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp @@ -170,6 +170,7 @@ inline constexpr std::uint8_t TwoByteGrp6 = 0x00; inline constexpr std::uint8_t TwoByteLar = 0x02; inline constexpr std::uint8_t TwoByteLsl = 0x03; inline constexpr std::uint8_t TwoBytePrefetchw = 0x0D; +inline constexpr std::uint8_t TwoByte3DNow = 0x0F; inline constexpr std::uint8_t TwoByteMovCrDrMin = 0x20; inline constexpr std::uint8_t TwoByteMovCrDrMax = 0x23; inline constexpr std::uint8_t TwoBytePopcnt = 0xB8; diff --git a/core/relinker/codegen/src/Amd64OnlyConverter.cpp b/core/relinker/codegen/src/Amd64OnlyConverter.cpp index 201630464..2f57bd9fd 100644 --- a/core/relinker/codegen/src/Amd64OnlyConverter.cpp +++ b/core/relinker/codegen/src/Amd64OnlyConverter.cpp @@ -3,9 +3,13 @@ #include #include #include +#include #include +#include #include +#include #include +#include #include #include #include @@ -24,6 +28,181 @@ auto _atFileOffset(const Domain::FileByteOffset base, const TOperation& operatio } } +constexpr std::int64_t kShortJumpSize = 2; +constexpr std::uint64_t kIslandSearchBytes = 0x100; + +struct Placement { + std::vector Sites; + std::vector Claimed; + std::vector Consumed; + std::size_t ReplacementLength = 0; +}; + +class WindowPlacer { +public: + WindowPlacer( + std::vector& seg, + const std::vector& matches, + const Domain::ProgramHeader& ph, + const std::set& branchTargets, + const std::set& pending, + const std::set& claimed, + const IAmd64OnlyInstructionMatcher& matcher + ) : _seg(seg), _matches(matches), _ph(ph), _branchTargets(branchTargets), _pending(pending), _claimed(claimed), _matcher(matcher) {} + + [[nodiscard]] std::optional Window(const std::size_t index) const { + if (index + 1 < _matches.size() && (_pending.contains(index + 1) || _movable(index + 1, true))) { + if (auto placement = _relocate(index, index, index + 1)) + return placement; + } + if (index > 0 && _movable(index - 1, false)) + return _relocate(index, index - 1, index); + return std::nullopt; + } + + [[nodiscard]] std::optional Island(const std::size_t index) { + const auto store = _address(index); + for (std::size_t distance = 1;; ++distance) { + bool inReach = false; + for (const bool before : {true, false}) { + if (before ? distance > index : index + distance >= _matches.size()) + continue; + const auto start = before ? index - distance : index + distance; + if (_address(start) + kIslandSearchBytes < store || _address(start) > store + kIslandSearchBytes) + continue; + inReach = true; + if (auto placement = _islandAt(index, start)) + return placement; + } + if (!inReach) + return std::nullopt; + } + } + +private: + std::vector& _seg; + const std::vector& _matches; + const Domain::ProgramHeader& _ph; + const std::set& _branchTargets; + const std::set& _pending; + const std::set& _claimed; + const IAmd64OnlyInstructionMatcher& _matcher; + X64InstructionDecoder _decoder; + + [[nodiscard]] std::uint64_t _address(const std::size_t index) const { return _ph.MappedAddress + _matches[index].Offset; } + [[nodiscard]] std::uint64_t _end(const std::size_t index) const { return _address(index) + _matches[index].Length; } + [[nodiscard]] std::span _bytes(const std::size_t index) const { return {_seg.data() + _matches[index].Offset, _matches[index].Length}; } + [[nodiscard]] DecodedInstructionInfo _info(const std::size_t index) const { return _decoder.DecodeInstruction(_seg.data() + _matches[index].Offset, _matches[index].Length); } + + [[nodiscard]] bool _entered(const std::uint64_t start, const std::uint64_t end) const { + const auto hit = _branchTargets.upper_bound(start); + return hit != _branchTargets.end() && *hit < end; + } + + [[nodiscard]] bool _blocksFollowing(const std::size_t index) const { + const auto kind = _info(index).FlowKind; + return kind != ControlFlowKind::Sequential && kind != ControlFlowKind::ConditionalBranch; + } + + [[nodiscard]] bool _movable(const std::size_t index, const bool last) const { + return !_claimed.contains(index) && !_pending.contains(index) && (last || !_blocksFollowing(index)); + } + + [[nodiscard]] TrampolineSite _site(const std::uint64_t start, const std::size_t length, LoweredBody body) const { + const auto offset = static_cast(start - _ph.MappedAddress); + return { + static_cast(_ph.Offset + offset), + static_cast(start), + length, + std::vector(_seg.begin() + static_cast(offset), _seg.begin() + static_cast(offset + length)), + std::move(body.Bytes), + body.ReturnBranchOffset, + std::move(body.Relocations) + }; + } + + [[nodiscard]] std::optional _relocate(const std::size_t index, const std::size_t first, const std::size_t last) const { + if (_entered(_address(first), _end(last))) + return std::nullopt; + StubBodyBuilder body; + Placement placement; + try { + for (auto k = first; k <= last; ++k) { + if (_pending.contains(k)) { + _matcher.EmitOutOfLine(body, _bytes(k)); + if (k != index) + placement.Consumed.push_back(k); + } else { + body.MoveControlFlow(_bytes(k), _info(k)); + } + placement.Claimed.push_back(k); + } + } catch (const CodegenException&) { + return std::nullopt; + } + placement.Sites.push_back(_site(_address(first), static_cast(_end(last) - _address(first)), body.Finish())); + placement.ReplacementLength = placement.Sites.back().Body.size(); + return placement; + } + + [[nodiscard]] std::optional _islandAt(const std::size_t index, const std::size_t start) { + using Amd64OnlySubstitutionTable::kJmpRel32; + std::size_t head = 0; + for (auto last = start; last < _matches.size(); ++last) { + if (!_movable(last, true) || _entered(_address(start), _end(last))) + return std::nullopt; + if (head == 0 && _end(last) - _address(start) >= kJmpRel32.Size) + head = static_cast(_end(last) - _address(start)); + const auto island = _address(start) + head; + const auto reach = static_cast(island) - static_cast(_address(index) + kShortJumpSize); + if (head != 0 && (reach < std::numeric_limits::min() || reach > std::numeric_limits::max())) + return std::nullopt; + if (head != 0 && _end(last) >= island + kJmpRel32.Size) { + try { + return _buildIsland(index, start, last, head); + } catch (const CodegenException&) { + return std::nullopt; + } + } + if (_blocksFollowing(last)) + return std::nullopt; + } + return std::nullopt; + } + + [[nodiscard]] Placement _buildIsland(const std::size_t index, const std::size_t start, const std::size_t last, const std::size_t head) { + using Amd64OnlySubstitutionTable::kJmpRel32; + const auto windowStart = _address(start); + const auto windowEnd = _end(last); + const auto island = windowStart + head; + const auto store = _address(index); + Placement placement; + + StubBodyBuilder moved; + for (auto k = start; k <= last; ++k) { + moved.MoveControlFlow(_bytes(k), _info(k)); + placement.Claimed.push_back(k); + } + if (!_blocksFollowing(last)) + moved.JumpTo(static_cast(windowEnd - windowStart)); + placement.Sites.push_back(_site(windowStart, head, moved.Finish())); + + StubBodyBuilder lowered; + lowered.Seek(static_cast(store) - static_cast(island)); + _matcher.EmitOutOfLine(lowered, _bytes(index)); + lowered.JumpTo(static_cast(_end(index)) - static_cast(island)); + placement.Sites.push_back(_site(island, kJmpRel32.Size, lowered.Finish())); + placement.ReplacementLength = placement.Sites.back().Body.size(); + placement.Claimed.push_back(index); + + const auto offset = _matches[index].Offset; + std::fill_n(_seg.begin() + static_cast(offset), _matches[index].Length, Amd64OnlySubstitutionTable::kTrapFill); + _seg[offset] = X64OpcodeConstants::OneByteJmpRel8; + _seg[offset + 1] = static_cast(static_cast(static_cast(island) - static_cast(store + kShortJumpSize))); + return placement; + } +}; + class Amd64OnlyConverter : public IAmd64OnlyConverter { public: explicit Amd64OnlyConverter(const Amd64OnlyTarget target) : _matcher(MakeAmd64OnlyInstructionMatcher(target)) {} @@ -106,6 +285,10 @@ void Amd64OnlyConverter::_convertSegment( }); const auto branchTargets = needsBranchTargets ? _collectBranchTargets(seg, matches, ph) : std::set{}; + std::set pendingIndices; + for (const auto& item : pending) + pendingIndices.insert(item.Index); + std::set claimed; std::set consumed; for (const auto& item : pending) { if (consumed.contains(item.Index)) @@ -126,6 +309,20 @@ void Amd64OnlyConverter::_convertSegment( break; } case Amd64OnlyLowering::Trampoline: { + if (substitution.RelocatableWindow && match.Length < Amd64OnlySubstitutionTable::kJmpRel32.Size) { + WindowPlacer placer(seg, matches, ph, branchTargets, pendingIndices, claimed, *_matcher); + auto placement = placer.Window(item.Index); + if (!placement) + placement = placer.Island(item.Index); + if (!placement) + throw CodegenException("No room for a jump to the stub of " + substitution.InstructionName + " within its neighbours or a nearby island", fileOffset); + for (auto& site : placement->Sites) + result.Trampolines.push_back(std::move(site)); + claimed.insert(placement->Claimed.begin(), placement->Claimed.end()); + consumed.insert(placement->Consumed.begin(), placement->Consumed.end()); + replacementLength = placement->ReplacementLength; + break; + } std::vector taken; try { std::size_t siteLength = match.Length; @@ -172,6 +369,8 @@ void Amd64OnlyConverter::_convertSegment( }); replacementLength = stub.StubBody.size(); consumed.insert(taken.begin(), taken.end()); + for (auto k = item.Index; k < matches.size() && matches[k].Offset < match.Offset + siteLength; ++k) + claimed.insert(k); } catch (const CodegenException&) { if (!substitution.Optional) throw; diff --git a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp index 35b1e0cc8..edf59c514 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include #include #include @@ -41,6 +43,8 @@ const Entry& _sse4aEntry(const Sse4aOperands& operands) { } const Entry& _reciprocalEntry(const ReciprocalOperands& operands) { + if (!operands.Vex) + return operands.Operation == ReciprocalOperation::ReciprocalSquareRoot ? kRsqrtps : kRcpps; return operands.Operation == ReciprocalOperation::ReciprocalSquareRoot ? kVrsqrtps : kVrcpps; } @@ -70,8 +74,8 @@ const Entry& _sha1Entry(const Sha1Operands& operands) { return kSha1rnds4; } -Amd64OnlyMatch _trampoline(std::string name, const std::size_t length, LoweredBody body, const bool optional = false) { - return Amd64OnlyMatch{std::move(name), length, Amd64OnlyLowering::Trampoline, {}, std::move(body.Bytes), body.ReturnBranchOffset, optional, std::move(body.Relocations)}; +Amd64OnlyMatch _trampoline(std::string name, const std::size_t length, LoweredBody body, const bool optional = false, const bool window = false) { + return Amd64OnlyMatch{std::move(name), length, Amd64OnlyLowering::Trampoline, {}, std::move(body.Bytes), body.ReturnBranchOffset, optional, std::move(body.Relocations), window}; } void _moveTrailing(StubBodyBuilder& body, std::span trailing) { @@ -140,6 +144,11 @@ public: std::span trailing ) const override; + void EmitOutOfLine( + StubBodyBuilder& body, + std::span instruction + ) const override; + private: Amd64OnlyTarget _target; Sse4aLowering _lowering; @@ -147,6 +156,7 @@ private: Sha1Lowering _sha1Lowering; ClzeroLowering _clzeroLowering; ReciprocalLowering _reciprocalLowering; + WideStoreLowering _wideStoreLowering; [[nodiscard]] Amd64OnlyMatch _matchMovnts(const DecodedInstruction& instr, const Entry& entry) const; [[nodiscard]] Amd64OnlyMatch _matchSse4a(const DecodedInstruction& instr, const Entry& entry, const Entry& registerFormEntry, std::span trailing) const; @@ -154,6 +164,7 @@ private: [[nodiscard]] Amd64OnlyMatch _matchSha1(const DecodedInstruction& instr, std::span trailing) const; [[nodiscard]] Amd64OnlyMatch _matchClzero(const DecodedInstruction& instr, std::span trailing) const; [[nodiscard]] std::optional _matchRosetta(const DecodedInstruction& instr, std::span trailing) const; + [[nodiscard]] const char* _emitOutOfLine(StubBodyBuilder& body, const DecodedInstruction& instr) const; }; Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchMovnts(const DecodedInstruction& instr, const Entry& entry) const { @@ -206,9 +217,52 @@ std::optional Amd64OnlyInstructionMatcher::_matchRosetta(const D return _inPlace(kClwb, instr.Length, {}); if (instr.IsRdpid()) return _trampoline(kRdpid.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { body.Raw(_rdpidReplacement(instr)); }), true); + if (const auto wide = DecodeVexWideStore(instr.Data, instr.Length)) + return _trampoline(kWideStore.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { _wideStoreLowering.EmitOutOfLine(body, *wide); }), false, true); return std::nullopt; } +const char* Amd64OnlyInstructionMatcher::_emitOutOfLine(StubBodyBuilder& body, const DecodedInstruction& instr) const { + if (instr.IsExtrq() || instr.IsInsertq()) { + const auto operands = DecodeSse4a(instr.Data, instr.Length); + _lowering.EmitOutOfLine(body, operands); + return _sse4aEntry(operands).Name; + } + if (instr.IsSha256()) { + const auto operands = DecodeSha256(instr.Data, instr.Length); + _sha256Lowering.EmitOutOfLine(body, operands); + return _sha256Entry(operands).Name; + } + if (instr.IsSha1()) { + const auto operands = DecodeSha1(instr.Data, instr.Length); + _sha1Lowering.EmitOutOfLine(body, operands); + return _sha1Entry(operands).Name; + } + if (instr.IsClzero()) { + _clzeroLowering.EmitOutOfLine(body, DecodeClzero(instr.Data, instr.Length)); + return kClzero.Name; + } + if (const auto reciprocal = DecodeReciprocal(instr.Data, instr.Length)) { + _reciprocalLowering.EmitOutOfLine(body, *reciprocal); + return _reciprocalEntry(*reciprocal).Name; + } + if (_target == Amd64OnlyTarget::Rosetta && instr.IsRdpid()) { + body.Raw(_rdpidReplacement(instr)); + return kRdpid.Name; + } + if (const auto wide = _target == Amd64OnlyTarget::Rosetta ? DecodeVexWideStore(instr.Data, instr.Length) : std::nullopt) { + _wideStoreLowering.EmitOutOfLine(body, *wide); + return kWideStore.Name; + } + return nullptr; +} + +void Amd64OnlyInstructionMatcher::EmitOutOfLine(StubBodyBuilder& body, const std::span instruction) const { + if (_emitOutOfLine(body, DecodedInstruction{instruction.data(), instruction.size()}) == nullptr) + throw CodegenException("AMD-only instruction has no out-of-line lowering"); + body.Advance(instruction.size()); +} + std::optional Amd64OnlyInstructionMatcher::MatchSequence( std::span> instructions, std::span trailing @@ -219,42 +273,16 @@ std::optional Amd64OnlyInstructionMatcher::MatchSequence( const char* name = nullptr; for (const auto& instruction : instructions) { const DecodedInstruction instr{instruction.data(), instruction.size()}; - if (instr.IsExtrq() || instr.IsInsertq()) { - const auto operands = DecodeSse4a(instr.Data, instr.Length); - if (name == nullptr) - name = _sse4aEntry(operands).Name; - _lowering.EmitOutOfLine(body, operands); - } else if (instr.IsSha256()) { - const auto operands = DecodeSha256(instr.Data, instr.Length); - if (name == nullptr) - name = _sha256Entry(operands).Name; - _sha256Lowering.EmitOutOfLine(body, operands); - } else if (instr.IsSha1()) { - const auto operands = DecodeSha1(instr.Data, instr.Length); - if (name == nullptr) - name = _sha1Entry(operands).Name; - _sha1Lowering.EmitOutOfLine(body, operands); - } else if (instr.IsClzero()) { - const auto operands = DecodeClzero(instr.Data, instr.Length); - if (name == nullptr) - name = kClzero.Name; - _clzeroLowering.EmitOutOfLine(body, operands); - } else if (const auto reciprocal = DecodeVexReciprocal(instr.Data, instr.Length)) { - if (name == nullptr) - name = _reciprocalEntry(*reciprocal).Name; - _reciprocalLowering.EmitOutOfLine(body, *reciprocal); - } else if (_target == Amd64OnlyTarget::Rosetta && instr.IsRdpid()) { - if (name == nullptr) - name = kRdpid.Name; - body.Raw(_rdpidReplacement(instr)); - } else { + const auto emitted = _emitOutOfLine(body, instr); + if (emitted == nullptr) return std::nullopt; - } + if (name == nullptr) + name = emitted; body.Advance(instr.Length); } _moveTrailing(body, trailing); const DecodedInstruction first{instructions.front().data(), instructions.front().size()}; - const bool optional = DecodeVexReciprocal(first.Data, first.Length).has_value() || (_target == Amd64OnlyTarget::Rosetta && first.IsRdpid()); + const bool optional = DecodeReciprocal(first.Data, first.Length).has_value() || (_target == Amd64OnlyTarget::Rosetta && first.IsRdpid()); return _trampoline(name, instructions.front().size(), body.Finish(), optional); } @@ -289,7 +317,7 @@ std::optional Amd64OnlyInstructionMatcher::Match( if (instr.IsMwaitx()) return _validWait(instr) ? _inPlace(kMwaitx, length, std::vector(kPause.Bytes, kPause.Bytes + kPause.Size)) : _unsupported(kMwaitx, length); - if (const auto reciprocal = DecodeVexReciprocal(data, length)) { + if (const auto reciprocal = DecodeReciprocal(data, length)) { return _trampoline(_reciprocalEntry(*reciprocal).Name, length, _outOfLine(length, trailing, [&](StubBodyBuilder& body) { _reciprocalLowering.EmitOutOfLine(body, *reciprocal); }), true); } diff --git a/core/relinker/codegen/src/x86/ReciprocalLowering.cpp b/core/relinker/codegen/src/x86/ReciprocalLowering.cpp index 8f6d794b4..d01588ae8 100644 --- a/core/relinker/codegen/src/x86/ReciprocalLowering.cpp +++ b/core/relinker/codegen/src/x86/ReciprocalLowering.cpp @@ -46,11 +46,15 @@ void ReciprocalLowering::EmitOutOfLine(StubBodyBuilder& body, const ReciprocalOp body.SsePlain({0x0F, kSqrtps}, scratch, operands.Source); body.RipOperand({0x0F, kMovaps}, dst, _ones()); body.SsePlain({0x0F, kDivps}, dst, scratch); - _vexMovaps(body, dst, dst); + if (operands.Vex) + _vexMovaps(body, dst, dst); } else { body.RipOperand({0x0F, kMovaps}, scratch, _ones()); body.SsePlain({0x0F, kDivps}, scratch, operands.Source); - _vexMovaps(body, dst, scratch); + if (operands.Vex) + _vexMovaps(body, dst, scratch); + else + body.SsePlain({0x0F, kMovaps}, dst, scratch); } body.Restore(scratch); } diff --git a/core/relinker/codegen/src/x86/ReciprocalOperands.cpp b/core/relinker/codegen/src/x86/ReciprocalOperands.cpp index c9e203768..a985003b5 100644 --- a/core/relinker/codegen/src/x86/ReciprocalOperands.cpp +++ b/core/relinker/codegen/src/x86/ReciprocalOperands.cpp @@ -12,14 +12,15 @@ constexpr std::uint8_t kOpcodeRcpps = 0x53; } -std::optional DecodeVexReciprocal(const std::uint8_t* data, const std::size_t length) { +std::optional DecodeReciprocal(const std::uint8_t* data, const std::size_t length) { using namespace X64OpcodeConstants; - if (length < 4) + if (length < 3) return std::nullopt; bool registerExtension = false; bool rmExtension = false; std::uint8_t payload = 0; std::size_t opcodeOffset = 0; + bool vex = true; if (data[0] == OneByteVex2 && length == 4) { registerExtension = (data[1] & 0x80) == 0; payload = data[1]; @@ -31,13 +32,21 @@ std::optional DecodeVexReciprocal(const std::uint8_t* data, rmExtension = (data[1] & 0x20) == 0; payload = data[2]; opcodeOffset = 3; + } else if (data[0] == TwoByteOpcodeEscape && length == 3) { + opcodeOffset = 1; + vex = false; + } else if (data[0] >= RexMin && data[0] <= RexMax && length == 4 && data[1] == TwoByteOpcodeEscape) { + registerExtension = (data[0] & 0x04) != 0; + rmExtension = (data[0] & 0x01) != 0; + opcodeOffset = 2; + vex = false; } else { return std::nullopt; } const auto vvvv = static_cast((payload >> 3) & 0x0F); const bool wide = (payload & 0x04) != 0; const auto pp = static_cast(payload & 0x03); - if (vvvv != kVexUnusedRegister || wide || pp != 0) + if (vex && (vvvv != kVexUnusedRegister || wide || pp != 0)) return std::nullopt; const auto opcode = data[opcodeOffset]; if (opcode != kOpcodeRsqrtps && opcode != kOpcodeRcpps) @@ -49,6 +58,7 @@ std::optional DecodeVexReciprocal(const std::uint8_t* data, operands.Operation = opcode == kOpcodeRsqrtps ? ReciprocalOperation::ReciprocalSquareRoot : ReciprocalOperation::Reciprocal; operands.Destination = static_cast(((modrm >> ModRmRegShift) & ModRmRegMask) | (registerExtension ? 8 : 0)); operands.Source = static_cast((modrm & ModRmRmMask) | (rmExtension ? 8 : 0)); + operands.Vex = vex; return operands; } diff --git a/core/relinker/codegen/src/x86/StubBodyBuilder.cpp b/core/relinker/codegen/src/x86/StubBodyBuilder.cpp index a1b6e44b6..e68805215 100644 --- a/core/relinker/codegen/src/x86/StubBodyBuilder.cpp +++ b/core/relinker/codegen/src/x86/StubBodyBuilder.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include namespace Codegen { @@ -150,7 +151,7 @@ void StubBodyBuilder::Load(const std::uint8_t reg, const MemoryOperand& operand) if (operand.Rm == ModRmRmSibPresent) _bytes.push_back(operand.Sib); if (operand.RipRelative) { - _relocations.push_back({_bytes.size(), _bytes.size() + Disp32Size, static_cast(_siteOffset + operand.InstructionLength) + operand.Displacement}); + _relocations.push_back({_bytes.size(), _bytes.size() + Disp32Size, _siteOffset + static_cast(operand.InstructionLength) + operand.Displacement}); _bytes.insert(_bytes.end(), Disp32Size, 0); return; } @@ -189,13 +190,82 @@ void StubBodyBuilder::Move(const std::span instruction, cons value |= static_cast(instruction[*ripDisplacementOffset + index]) << (index * 8); _bytes[start + *ripDisplacementOffset + index] = 0; } - _relocations.push_back({start + *ripDisplacementOffset, start + instruction.size(), static_cast(_siteOffset + instruction.size()) + static_cast(value)}); + _relocations.push_back({start + *ripDisplacementOffset, start + instruction.size(), _siteOffset + static_cast(instruction.size()) + static_cast(value)}); } Advance(instruction.size()); } +void StubBodyBuilder::RipRelative(const std::span instruction, const std::size_t displacementOffset, const std::int64_t target) { + using namespace X64OpcodeConstants; + if (displacementOffset > instruction.size() || instruction.size() - displacementOffset < Disp32Size) + throw CodegenException("RIP-relative displacement lies outside its instruction"); + const auto start = _bytes.size(); + Raw(instruction); + std::fill_n(_bytes.begin() + static_cast(start + displacementOffset), Disp32Size, std::uint8_t{0}); + _relocations.push_back({start + displacementOffset, start + instruction.size(), _siteOffset + target}); +} + +void StubBodyBuilder::MoveControlFlow(const std::span instruction, const DecodedInstructionInfo& info) { + using namespace X64OpcodeConstants; + const auto end = _siteOffset + static_cast(instruction.size()); + if (info.FlowKind == ControlFlowKind::IndirectCall) + throw CodegenException("A register or memory call cannot move into a stub"); + if (info.FlowKind == ControlFlowKind::Call && info.HasRipRelativeDisp) { + _pushReturnAddress(end); + std::vector jump(instruction.begin(), instruction.end()); + auto& modrm = jump[info.RipRelativeDispOffset - 1]; + modrm = static_cast((modrm & ~(ModRmRegMask << ModRmRegShift)) | (4u << ModRmRegShift)); + Move(jump, info.RipRelativeDispOffset); + return; + } + if (!info.HasBranchTarget || info.HasRipRelativeDisp) { + Move(instruction, info.HasRipRelativeDisp ? std::optional(info.RipRelativeDispOffset) : std::nullopt); + return; + } + for (std::size_t index = 0; index < info.OpcodeOffset; ++index) { + const auto prefix = instruction[index]; + if (prefix != PrefixSegCs && prefix != PrefixSegDs && prefix != PrefixRepne && prefix != PrefixAddressSize && (prefix & 0xF0) != kRexBase) + throw CodegenException("A relative branch with this prefix cannot move into a stub"); + } + const auto target = end + info.BranchDisp; + if (info.IsTwoByteOpcode) { + _rel32({TwoByteOpcodeEscape, info.Opcode}, target); + } else if (info.Opcode == OneByteCallRel32) { + _pushReturnAddress(end); + _rel32({OneByteJmpRel32}, target); + } else if (info.Opcode == OneByteJmpRel32 || info.Opcode == OneByteJmpRel8) { + _rel32({OneByteJmpRel32}, target); + } else if (info.Opcode >= OneByteJccRel8Min && info.Opcode <= OneByteJccRel8Max) { + _rel32({TwoByteOpcodeEscape, static_cast(TwoByteJccRel32Min | (info.Opcode & 0x0F))}, target); + } else { + throw CodegenException("A LOOP or JRCXZ branch cannot move into a stub"); + } + Advance(instruction.size()); +} + +void StubBodyBuilder::JumpTo(const std::int64_t siteTarget) { + _rel32({X64OpcodeConstants::OneByteJmpRel32}, siteTarget); +} + +void StubBodyBuilder::Seek(const std::int64_t siteOffset) { + _siteOffset = siteOffset; +} + void StubBodyBuilder::Advance(const std::size_t length) { - _siteOffset += length; + _siteOffset += static_cast(length); +} + +void StubBodyBuilder::_rel32(const std::initializer_list opcode, const std::int64_t siteTarget) { + _bytes.insert(_bytes.end(), opcode); + const auto displacement = _bytes.size(); + _bytes.insert(_bytes.end(), X64OpcodeConstants::Disp32Size, std::uint8_t{0}); + _relocations.push_back({displacement, _bytes.size(), siteTarget}); +} + +void StubBodyBuilder::_pushReturnAddress(const std::int64_t siteTarget) { + _bytes.insert(_bytes.end(), {0x48, 0x8D, 0x64, 0x24, 0xF8, 0x50}); + _rel32({0x48, 0x8D, 0x05}, siteTarget); + _bytes.insert(_bytes.end(), {0x48, 0x89, 0x44, 0x24, 0x08, 0x58}); } LoweredBody StubBodyBuilder::Finish() { diff --git a/core/relinker/codegen/src/x86/WideStoreLowering.cpp b/core/relinker/codegen/src/x86/WideStoreLowering.cpp new file mode 100644 index 000000000..40e22c9a3 --- /dev/null +++ b/core/relinker/codegen/src/x86/WideStoreLowering.cpp @@ -0,0 +1,21 @@ +#include + +namespace Codegen { + +namespace { + +void _emitHalf(StubBodyBuilder& body, const WideStoreHalf& half) { + if (half.RipDisplacementOffset) + body.RipRelative(half.Bytes, *half.RipDisplacementOffset, half.RipTarget); + else + body.Raw(half.Bytes); +} + +} + +void WideStoreLowering::EmitOutOfLine(StubBodyBuilder& body, const WideStoreOperands& operands) const { + _emitHalf(body, operands.Low); + _emitHalf(body, operands.High); +} + +} diff --git a/core/relinker/codegen/src/x86/WideStoreOperands.cpp b/core/relinker/codegen/src/x86/WideStoreOperands.cpp new file mode 100644 index 000000000..478c4108e --- /dev/null +++ b/core/relinker/codegen/src/x86/WideStoreOperands.cpp @@ -0,0 +1,133 @@ +#include +#include +#include +#include + +namespace Codegen { + +namespace { + +constexpr std::uint8_t kVexMap0F = 0x01; +constexpr std::uint8_t kVexLength256 = 0x04; +constexpr std::uint8_t kVexUnusedRegister = 0x0F; +constexpr std::uint8_t kPpNone = 0x00; +constexpr std::uint8_t kPp66 = 0x01; +constexpr std::uint8_t kPpF3 = 0x02; +constexpr std::uint8_t kVextractf128 = 0x19; +constexpr std::uint8_t kVextractf128Payload = 0x7D; +constexpr std::uint8_t kUpperHalf = 0x01; +constexpr std::int64_t kHalfBytes = 16; + +bool _unalignedStore(const std::uint8_t pp, const std::uint8_t opcode) { + return ((pp == kPpNone || pp == kPp66) && opcode == 0x11) || (pp == kPpF3 && opcode == 0x7F); +} + +bool _legacyPrefixAllowed(const std::uint8_t prefix) { + using namespace X64OpcodeConstants; + return prefix == PrefixSegCs || prefix == PrefixSegSs || prefix == PrefixSegDs || prefix == PrefixSegEs || prefix == PrefixSegFs || prefix == PrefixSegGs || prefix == PrefixAddressSize; +} + +void _appendU32(std::vector& bytes, const std::uint32_t value) { + for (std::size_t index = 0; index < X64OpcodeConstants::Disp32Size; ++index) + bytes.push_back(static_cast(value >> (index * 8))); +} + +} + +std::optional DecodeVexWideStore(const std::uint8_t* data, const std::size_t length) { + using namespace X64OpcodeConstants; + std::size_t pos = 0; + while (pos < length && _legacyPrefixAllowed(data[pos])) + ++pos; + const auto vex = pos; + if (vex + 2 > length) + return std::nullopt; + std::uint8_t inverted = 0; + std::uint8_t payload = 0; + if (data[vex] == OneByteVex2) { + inverted = static_cast((data[vex + 1] & 0x80) | 0x60); + payload = data[vex + 1]; + pos = vex + Vex2PrefixLength; + } else if (data[vex] == OneByteVex3) { + if (vex + Vex3PrefixLength > length || (data[vex + 1] & Vex3MapMask) != kVexMap0F) + return std::nullopt; + inverted = static_cast(data[vex + 1] & 0xE0); + payload = data[vex + 2]; + pos = vex + Vex3PrefixLength; + } else { + return std::nullopt; + } + if ((payload & kVexLength256) == 0 || ((payload >> 3) & 0x0F) != kVexUnusedRegister) + return std::nullopt; + if (pos + 2 > length || !_unalignedStore(static_cast(payload & 0x03), data[pos])) + return std::nullopt; + const auto modRmOffset = pos + 1; + const auto modrm = data[modRmOffset]; + const auto mod = static_cast((modrm >> ModRmModShift) & ModRmModMask); + const auto rm = static_cast(modrm & ModRmRmMask); + if (mod == ModRmModRegister) + return std::nullopt; + auto cursor = modRmOffset + 1; + std::optional sib; + if (rm == ModRmRmSibPresent) { + if (cursor >= length) + return std::nullopt; + sib = data[cursor++]; + } + const bool ripRelative = mod == ModRmModIndirect && rm == ModRmRmRipRelative; + const bool absolute = mod == ModRmModIndirect && sib.has_value() && (*sib & SibBaseMask) == SibBaseDisp32; + const std::size_t displacementSize = mod == ModRmModDisp8 ? Disp8Size : (mod == ModRmModDisp32 || ripRelative || absolute) ? Disp32Size : 0; + if (cursor + displacementSize != length) + return std::nullopt; + std::int64_t displacement = 0; + if (displacementSize == Disp8Size) { + displacement = static_cast(data[cursor]); + } else if (displacementSize == Disp32Size) { + std::uint32_t value = 0; + for (std::size_t index = 0; index < Disp32Size; ++index) + value |= static_cast(data[cursor + index]) << (index * 8); + displacement = static_cast(value); + } + + WideStoreOperands operands; + operands.Low.Bytes.assign(data, data + length); + operands.Low.Bytes[data[vex] == OneByteVex2 ? vex + 1 : vex + 2] &= static_cast(~kVexLength256); + if (ripRelative) { + operands.Low.RipDisplacementOffset = cursor; + operands.Low.RipTarget = static_cast(length) + displacement; + } + + auto& high = operands.High.Bytes; + high.assign(data, data + vex); + high.insert(high.end(), {OneByteVex3, static_cast(inverted | Vex3Map0F3A), kVextractf128Payload, kVextractf128}); + const auto reg = static_cast(modrm & (ModRmRegMask << ModRmRegShift)); + std::int64_t highDisplacement = displacement + kHalfBytes; + std::uint8_t highMod = mod; + std::size_t highDisplacementSize = displacementSize; + if (!ripRelative && !absolute) { + if (mod == ModRmModIndirect || (mod == ModRmModDisp8 && highDisplacement > std::numeric_limits::max())) { + highMod = highDisplacement > std::numeric_limits::max() ? ModRmModDisp32 : ModRmModDisp8; + highDisplacementSize = highMod == ModRmModDisp8 ? Disp8Size : Disp32Size; + } + } + if (!ripRelative && highDisplacement > std::numeric_limits::max()) + throw CodegenException("256-bit store displacement leaves no room for its upper half"); + high.push_back(static_cast((highMod << ModRmModShift) | reg | rm)); + if (sib) + high.push_back(*sib); + const auto highDisplacementOffset = high.size(); + if (highDisplacementSize == Disp8Size) + high.push_back(static_cast(static_cast(highDisplacement))); + else if (ripRelative) + _appendU32(high, 0); + else if (highDisplacementSize == Disp32Size) + _appendU32(high, static_cast(static_cast(highDisplacement))); + high.push_back(kUpperHalf); + if (ripRelative) { + operands.High.RipDisplacementOffset = highDisplacementOffset; + operands.High.RipTarget = static_cast(length) + displacement + kHalfBytes; + } + return operands; +} + +} diff --git a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp index b894332bd..4f9ff5b3b 100644 --- a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp @@ -242,7 +242,7 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t hasModRm = true; } else if (opcode >= TwoByteJccRel32Min && opcode <= TwoByteJccRel32Max) { immediateSize = ImmSize32; - } else if (opcode == TwoByteMovImm8ModRm) { + } else if (opcode == TwoByteMovImm8ModRm || opcode == TwoByte3DNow) { hasModRm = true; immediateSize = ImmSize8; } else if (opcode >= TwoByteShiftImm8Min && opcode <= TwoByteShiftImm8Max) { @@ -556,7 +556,7 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( op == TwoByteImulRmModRm || op == TwoByteGrp7 || op == TwoByteGrp15 || op == TwoByteXadd || op == TwoByteGrp9 || op == TwoByteNopModRm || - op == TwoByteEndbr || op == TwoByteMovImm8ModRm || + op == TwoByteEndbr || op == TwoByteMovImm8ModRm || op == TwoByte3DNow || op == ThreeByteEscape38 || op == ThreeByteEscape3A || (op >= TwoByteShiftImm8Min && op <= TwoByteShiftImm8Max) || op == TwoByteShldImm8 || op == TwoByteShrdImm8 || diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index dba57fd9f..16e891044 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -670,6 +671,221 @@ void rosettaSubstitutions() { require(rdpid->ReturnBranchOffset == 6 && Bytes(rdpid->StubBody.begin(), rdpid->StubBody.begin() + 7) == Bytes{0x41, 0xBB, 0, 0, 0, 0, 0xE9}, "RDPID r11 stub does not load 0 before returning"); } +void wideStoreSplit() { + struct Case { + Bytes Original; + Bytes Low; + Bytes High; + const char* Name; + }; + const std::vector cases = { + {{0xC5, 0xFC, 0x11, 0x07}, {0xC5, 0xF8, 0x11, 0x07}, {0xC4, 0xE3, 0x7D, 0x19, 0x47, 0x10, 0x01}, "vmovups (%rdi)"}, + {{0xC4, 0x41, 0x7E, 0x7F, 0x4C, 0x87, 0x70}, {0xC4, 0x41, 0x7A, 0x7F, 0x4C, 0x87, 0x70}, {0xC4, 0x43, 0x7D, 0x19, 0x8C, 0x87, 0x80, 0x00, 0x00, 0x00, 0x01}, "vmovdqu 0x70(%r15,%rax,4)"}, + {{0xC5, 0xFC, 0x11, 0x4C, 0x24, 0xF8}, {0xC5, 0xF8, 0x11, 0x4C, 0x24, 0xF8}, {0xC4, 0xE3, 0x7D, 0x19, 0x4C, 0x24, 0x08, 0x01}, "vmovups -8(%rsp)"}, + {{0xC5, 0xFE, 0x7F, 0x93, 0x78, 0x56, 0x34, 0x12}, {0xC5, 0xFA, 0x7F, 0x93, 0x78, 0x56, 0x34, 0x12}, {0xC4, 0xE3, 0x7D, 0x19, 0x93, 0x88, 0x56, 0x34, 0x12, 0x01}, "vmovdqu 0x12345678(%rbx)"}, + {{0x64, 0xC5, 0xFC, 0x11, 0x04, 0x25, 0x40, 0x00, 0x00, 0x00}, {0x64, 0xC5, 0xF8, 0x11, 0x04, 0x25, 0x40, 0x00, 0x00, 0x00}, {0x64, 0xC4, 0xE3, 0x7D, 0x19, 0x04, 0x25, 0x50, 0x00, 0x00, 0x00, 0x01}, "vmovups %fs:0x40"}, + {{0xC4, 0x41, 0x7C, 0x11, 0x65, 0x00}, {0xC4, 0x41, 0x78, 0x11, 0x65, 0x00}, {0xC4, 0x43, 0x7D, 0x19, 0x65, 0x10, 0x01}, "vmovups (%r13)"}, + {{0xC4, 0xC1, 0x7C, 0x11, 0x47, 0x68}, {0xC4, 0xC1, 0x78, 0x11, 0x47, 0x68}, {0xC4, 0xC3, 0x7D, 0x19, 0x47, 0x78, 0x01}, "vmovups 0x68(%r15)"}}; + for (const auto& item : cases) { + const auto split = Codegen::DecodeVexWideStore(item.Original.data(), item.Original.size()); + require(split && split->Low.Bytes == item.Low && split->High.Bytes == item.High && !split->Low.RipDisplacementOffset && !split->High.RipDisplacementOffset, (std::string("256-bit store was not split into its 128-bit halves: ") + item.Name).c_str()); + } + const Bytes rip = {0xC5, 0xFD, 0x11, 0x1D, 0x00, 0x01, 0x00, 0x00}; + const auto ripSplit = Codegen::DecodeVexWideStore(rip.data(), rip.size()); + require(ripSplit && ripSplit->Low.Bytes == Bytes{0xC5, 0xF9, 0x11, 0x1D, 0x00, 0x01, 0x00, 0x00} && ripSplit->High.Bytes == Bytes{0xC4, 0xE3, 0x7D, 0x19, 0x1D, 0x00, 0x00, 0x00, 0x00, 0x01}, "RIP-relative 256-bit store was not split"); + require(ripSplit->Low.RipDisplacementOffset == 4 && ripSplit->Low.RipTarget == 8 + 0x100 && ripSplit->High.RipDisplacementOffset == 5 && ripSplit->High.RipTarget == 8 + 0x110, "RIP-relative halves do not address the original operand and the one 16 bytes above it"); + for (const auto& other : {Bytes{0xC5, 0xF8, 0x11, 0x07}, Bytes{0xC5, 0xFC, 0x11, 0xC1}, Bytes{0xC5, 0xFC, 0x10, 0x07}, Bytes{0xC4, 0xE2, 0x75, 0x2E, 0x07}, Bytes{0xC5, 0xF4, 0x11, 0x07}, Bytes{0xC4, 0xE3, 0x7D, 0x19, 0x07, 0x01}, Bytes{0xC5, 0xFC, 0x11, 0x04}, Bytes{0xC5, 0xFC, 0x11, 0x47}}) + require(!Codegen::DecodeVexWideStore(other.data(), other.size()).has_value(), "An instruction other than a plain 256-bit store was split"); + for (const auto& aligned : {Bytes{0xC5, 0xFC, 0x29, 0x4C, 0x24, 0xF8}, Bytes{0xC5, 0xFD, 0xE7, 0x93, 0x78, 0x56, 0x34, 0x12}, Bytes{0xC5, 0xFD, 0x7F, 0x1F}, Bytes{0xC5, 0xFC, 0x2B, 0x27}}) + require(!Codegen::DecodeVexWideStore(aligned.data(), aligned.size()).has_value(), "An aligned 256-bit store, which cannot cross a page, was split"); + const Bytes farRip = {0xC5, 0xFC, 0x11, 0x05, 0xF8, 0xFF, 0xFF, 0x7F}; + const auto farRipSplit = Codegen::DecodeVexWideStore(farRip.data(), farRip.size()); + require(farRipSplit && farRipSplit->High.RipTarget == std::int64_t{8} + 0x7FFFFFF8 + 0x10, "A RIP-relative 256-bit store near the end of its displacement range was not split"); + const Bytes farBase = {0xC5, 0xFC, 0x11, 0x80, 0xF8, 0xFF, 0xFF, 0x7F}; + requireFailure([&] { (void)Codegen::DecodeVexWideStore(farBase.data(), farBase.size()); }, "A 256-bit store whose upper half has no 32-bit displacement was kept"); + requireFailure([] { Codegen::StubBodyBuilder body; body.RipRelative(Bytes{0x90}, 0, 0); }, "A RIP-relative displacement outside its instruction was relocated"); + const auto rosetta = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes store = {0xC4, 0xC1, 0x7C, 0x11, 0x47, 0x68}; + const auto lowered = rosetta->Match(store.data(), store.size()); + require(lowered && lowered->Lowering == Codegen::Amd64OnlyLowering::Trampoline && !lowered->Optional && lowered->RelocatableWindow && lowered->InstructionName == "256-bit store", "A 256-bit store was not lowered through a required stub that may move its neighbours for Rosetta"); + Bytes expectedBody = cases.back().Low; + expectedBody.insert(expectedBody.end(), cases.back().High.begin(), cases.back().High.end()); + require(Bytes(lowered->StubBody.begin(), lowered->StubBody.begin() + static_cast(lowered->ReturnBranchOffset)) == expectedBody, "The 256-bit store stub is not the two halves"); + const std::vector> pair = {std::span(cases[0].Original), std::span(rip)}; + const auto sequence = rosetta->MatchSequence(pair, {}); + require(sequence && sequence->Relocations.size() == 2 && sequence->Relocations[0].SiteTarget == 4 + 8 + 0x100 && sequence->Relocations[1].SiteTarget == 4 + 8 + 0x110, "A RIP-relative 256-bit store after another is not relocated from its place in the site"); + require(!Codegen::MakeAmd64OnlyInstructionMatcher()->Match(store.data(), store.size()).has_value(), "A 256-bit store was split for an Intel host"); + const std::vector> single = {std::span(store)}; + require(!Codegen::MakeAmd64OnlyInstructionMatcher()->MatchSequence(single, {}).has_value(), "A 256-bit store was split in a sequence for an Intel host"); +} + +void converterWideStoreWindows() { + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes store = {0xC5, 0xFC, 0x11, 0x07}; + Bytes halves = {0xC5, 0xF8, 0x11, 0x07, 0xC4, 0xE3, 0x7D, 0x19, 0x47, 0x10, 0x01}; + const auto convert = [&](const Bytes& text) { + Bytes file(0x300 + text.size(), 0xCC); + std::copy(text.begin(), text.end(), file.begin() + 0x200); + return converter->Convert(file, {segmentHeader(text.size())}); + }; + const auto body = [](const Codegen::TrampolineSite& site) { + return Bytes(site.Body.begin(), site.Body.begin() + static_cast(site.ReturnBranchOffset)); + }; + const auto withStore = [&](Bytes before, const Bytes& after) { + before.insert(before.end(), store.begin(), store.end()); + before.insert(before.end(), after.begin(), after.end()); + return before; + }; + + const auto returned = convert(withStore({}, {0xC3})); + Bytes expected = halves; + expected.push_back(0xC3); + require(returned.Trampolines.size() == 1 && returned.KeptCount == 0 && returned.Reports.size() == 1 && returned.Reports[0].InstructionName == "256-bit store", "A 256-bit store followed by a return was not moved"); + require(returned.Trampolines[0].Offset == 0x200 && returned.Trampolines[0].Length == 5 && body(returned.Trampolines[0]) == expected, "A 256-bit store followed by a return did not end its stub with the return"); + + const auto called = convert(withStore({}, {0xE8, 0x10, 0x00, 0x00, 0x00})); + require(called.Trampolines.size() == 1 && called.Trampolines[0].Offset == 0x200 && called.Trampolines[0].Length == 9, "A 256-bit store followed by a call did not take the call"); + const auto& call = called.Trampolines[0]; + expected = halves; + expected.insert(expected.end(), {0x48, 0x8D, 0x64, 0x24, 0xF8, 0x50, 0x48, 0x8D, 0x05, 0, 0, 0, 0, 0x48, 0x89, 0x44, 0x24, 0x08, 0x58, 0xE9, 0, 0, 0, 0}); + require(body(call) == expected, "A moved call does not push its original return address and jump"); + require(call.Relocations.size() == 2 && call.Relocations[0].DisplacementOffset == halves.size() + 9 && call.Relocations[0].SiteTarget == 9 && call.Relocations[1].DisplacementOffset == halves.size() + 20 && call.Relocations[1].SiteTarget == 9 + 0x10, "A moved call returns to or calls the wrong address"); + + const auto branched = convert(withStore({}, {0x75, 0x10, 0x90})); + require(branched.Trampolines.size() == 1 && branched.Trampolines[0].Length == 6, "A 256-bit store followed by a short conditional branch did not take the branch"); + const auto& jcc = branched.Trampolines[0]; + expected = halves; + expected.insert(expected.end(), {0x0F, 0x85, 0, 0, 0, 0}); + require(body(jcc) == expected && jcc.Relocations.size() == 1 && jcc.Relocations[0].SiteTarget == 6 + 0x10, "A moved short conditional branch was not re-encoded with a 32-bit displacement to its target"); + + const auto jumped = convert(withStore({}, {0xEB, 0x10, 0x90})); + expected = halves; + expected.insert(expected.end(), {0xE9, 0, 0, 0, 0}); + require(jumped.Trampolines.size() == 1 && body(jumped.Trampolines[0]) == expected && jumped.Trampolines[0].Relocations.size() == 1 && jumped.Trampolines[0].Relocations[0].SiteTarget == 6 + 0x10, "A moved short jump was not re-encoded with a 32-bit displacement to its target"); + + const auto longBranched = convert(withStore({}, {0x0F, 0x85, 0x10, 0x00, 0x00, 0x00})); + expected = halves; + expected.insert(expected.end(), {0x0F, 0x85, 0, 0, 0, 0}); + require(longBranched.Trampolines.size() == 1 && longBranched.Trampolines[0].Length == 10 && body(longBranched.Trampolines[0]) == expected && longBranched.Trampolines[0].Relocations.size() == 1 && longBranched.Trampolines[0].Relocations[0].SiteTarget == 10 + 0x10, "A moved conditional branch with a 32-bit displacement does not reach its target"); + + const auto entered = convert(withStore({0x48, 0x83, 0xC1, 0x01}, {0xC3, 0xEB, 0xFD})); + expected = {0x48, 0x83, 0xC1, 0x01}; + expected.insert(expected.end(), halves.begin(), halves.end()); + require(entered.Trampolines.size() == 1 && entered.Trampolines[0].Offset == 0x200 && entered.Trampolines[0].Length == 8 && body(entered.Trampolines[0]) == expected, "A 256-bit store before a branch target did not take room from the instruction before it"); + + Bytes adds; + for (int index = 0; index < 4; ++index) + adds.insert(adds.end(), {0x48, 0x83, 0xC1, 0x01}); + Bytes after = {0xC3, 0xEB, 0xFD}; + after.insert(after.end(), adds.begin(), adds.end()); + const auto island = convert(withStore({0xE8, 0x00, 0x00, 0x00, 0x00}, after)); + require(island.Trampolines.size() == 2 && island.KeptCount == 0, "A 256-bit store with no movable neighbours was not lowered through an island"); + const auto& moved = island.Trampolines[0]; + const auto& lowered = island.Trampolines[1]; + expected = adds; + expected.insert(expected.end(), {0xE9, 0, 0, 0, 0}); + require(moved.Offset == 0x20C && moved.Length == 8 && body(moved) == expected && moved.Relocations.size() == 1 && moved.Relocations[0].SiteTarget == 16, "The instructions under the island were not moved and continued after it"); + expected = halves; + expected.insert(expected.end(), {0xE9, 0, 0, 0, 0}); + require(lowered.Offset == 0x214 && lowered.Length == 5 && body(lowered) == expected && lowered.Relocations.size() == 1 && lowered.Relocations[0].SiteTarget == -11, "The island does not store both halves and return after the original store"); + require(Bytes(island.Bytes.begin() + 0x205, island.Bytes.begin() + 0x209) == Bytes{0xEB, 0x0D, 0xCC, 0xCC}, "The 256-bit store was not replaced by a short jump to its island"); + + requireFailure([&] { (void)convert(withStore({0xE8, 0x00, 0x00, 0x00, 0x00}, {0xC3, 0xEB, 0xFD})); }, "A 256-bit store with no room for a stub jump was kept"); + + const auto paired = convert(withStore({}, {0xC5, 0xFC, 0x11, 0x0E, 0xC3})); + expected = halves; + expected.insert(expected.end(), {0xC5, 0xF8, 0x11, 0x0E, 0xC4, 0xE3, 0x7D, 0x19, 0x4E, 0x10, 0x01}); + require(paired.Trampolines.size() == 1 && paired.Trampolines[0].Length == 8 && body(paired.Trampolines[0]) == expected && paired.Reports.size() == 1, "A 256-bit store followed by another was not lowered with it in one stub"); + + const Bytes push = {0x48, 0x8D, 0x64, 0x24, 0xF8, 0x50, 0x48, 0x8D, 0x05, 0, 0, 0, 0, 0x48, 0x89, 0x44, 0x24, 0x08, 0x58}; + const auto ripCalled = convert(withStore({}, {0xFF, 0x15, 0x10, 0x00, 0x00, 0x00})); + expected = halves; + expected.insert(expected.end(), push.begin(), push.end()); + expected.insert(expected.end(), {0xFF, 0x25, 0, 0, 0, 0}); + const auto& ripCall = ripCalled.Trampolines[0]; + require(ripCalled.Trampolines.size() == 1 && ripCall.Length == 10 && body(ripCall) == expected, "A moved call through a RIP-relative pointer does not push its original return address and jump through the pointer"); + require(ripCall.Relocations.size() == 2 && ripCall.Relocations[0].SiteTarget == 10 && ripCall.Relocations[1].DisplacementOffset == halves.size() + 21 && ripCall.Relocations[1].SiteTarget == 10 + 0x10, "A moved call through a RIP-relative pointer returns to or reads the wrong address"); + + const auto ripJumped = convert(withStore({}, {0xFF, 0x25, 0x10, 0x00, 0x00, 0x00})); + expected = halves; + expected.insert(expected.end(), {0xFF, 0x25, 0, 0, 0, 0}); + require(ripJumped.Trampolines.size() == 1 && body(ripJumped.Trampolines[0]) == expected && ripJumped.Trampolines[0].Relocations.size() == 1 && ripJumped.Trampolines[0].Relocations[0].SiteTarget == 10 + 0x10, "A moved jump through a RIP-relative pointer reads the wrong address"); + + const auto relaxed = convert(withStore({}, {0x67, 0xE8, 0x10, 0x00, 0x00, 0x00})); + expected = halves; + expected.insert(expected.end(), push.begin(), push.end()); + expected.insert(expected.end(), {0xE9, 0, 0, 0, 0}); + require(relaxed.Trampolines.size() == 1 && relaxed.Trampolines[0].Length == 10 && body(relaxed.Trampolines[0]) == expected && relaxed.Trampolines[0].Relocations.size() == 2 && relaxed.Trampolines[0].Relocations[1].SiteTarget == 10 + 0x10, "A call with an address-size prefix was not moved like a call"); + + const Bytes add = {0x48, 0x83, 0xC1, 0x01}; + for (const auto& follower : {Bytes{0xFF, 0xD0}, Bytes{0xE3, 0x10}, Bytes{0x66, 0x75, 0x10}, Bytes{0x66, 0x0F, 0xAE, 0x37}}) { + const auto preceded = convert(withStore(add, follower)); + expected = add; + expected.insert(expected.end(), halves.begin(), halves.end()); + require(!preceded.Trampolines.empty() && preceded.Trampolines[0].Offset == 0x200 && preceded.Trampolines[0].Length == 8 && body(preceded.Trampolines[0]) == expected, "A 256-bit store moved a following instruction that cannot leave its place instead of the one before it"); + } + + Bytes throwing = {0xC3, 0xEB, 0xFD}; + for (int index = 0; index < 3; ++index) + throwing.insert(throwing.end(), add.begin(), add.end()); + throwing.insert(throwing.end(), {0xFF, 0xD0}); + throwing.insert(throwing.end(), adds.begin(), adds.end()); + const auto farther = convert(withStore({0xE8, 0x00, 0x00, 0x00, 0x00}, throwing)); + require(farther.Trampolines.size() == 2 && farther.Trampolines[0].Offset == 0x21A && farther.Trampolines[1].Offset == 0x222, "An island that would move a register call was not passed over for the next one"); + require(Bytes(farther.Bytes.begin() + 0x205, farther.Bytes.begin() + 0x207) == Bytes{0xEB, 0x1B}, "The 256-bit store does not jump to the island after the register call"); + + Bytes reentered = {0xFF, 0xD0, 0xEB, 0xF8}; + reentered.insert(reentered.end(), adds.begin(), adds.end()); + const auto beyondStore = convert(withStore(Bytes(adds.begin(), adds.begin() + 8), reentered)); + require(beyondStore.Trampolines.size() == 2 && beyondStore.Trampolines[0].Offset == 0x210 && beyondStore.Trampolines[1].Offset == 0x218, "An island before a 256-bit store that is a branch target took the store with it"); + require(Bytes(beyondStore.Bytes.begin() + 0x208, beyondStore.Bytes.begin() + 0x20A) == Bytes{0xEB, 0x0E}, "The 256-bit store that is a branch target does not jump to the island after it"); + + const Bytes farCall = {0xE8, 0x40, 0x00, 0x00, 0x00}; + Bytes claimedFirst = {0xFF, 0xD0}; + claimedFirst.insert(claimedFirst.end(), adds.begin(), adds.end()); + const auto afterSequence = convert(withStore({0xC5, 0xF8, 0x53, 0xC1, 0x48, 0x83, 0xC1, 0x01}, claimedFirst)); + require(afterSequence.Trampolines.size() == 3 && afterSequence.Trampolines[0].Offset == 0x200 && afterSequence.Trampolines[0].Length == 8 && afterSequence.Trampolines[1].Offset == 0x20E && afterSequence.Trampolines[2].Offset == 0x216, "A 256-bit store moved an instruction already moved into the stub of the instruction before it"); + + Bytes pendingRun = {0xFF, 0xD0, 0x48, 0x83, 0xC1, 0x01, 0xC5, 0xFC, 0x11, 0x47, 0x10}; + pendingRun.insert(pendingRun.end(), adds.begin(), adds.end()); + const auto pendingIsland = convert(withStore(farCall, pendingRun)); + require(pendingIsland.Trampolines.size() == 3 && pendingIsland.Trampolines[0].Offset == 0x214 && pendingIsland.Trampolines[1].Offset == 0x21C && pendingIsland.Trampolines[2].Offset == 0x20F && pendingIsland.Trampolines[2].Length == 5, "An island moved the call before a 256-bit store or another store that still needs its own stub"); + + Bytes enteredRun = {0xFF, 0xD0}; + for (int index = 0; index < 6; ++index) + enteredRun.insert(enteredRun.end(), add.begin(), add.end()); + enteredRun.insert(enteredRun.end(), {0xEB, 0xEE}); + const auto enteredIsland = convert(withStore(farCall, enteredRun)); + require(enteredIsland.Trampolines.size() == 2 && enteredIsland.Trampolines[0].Offset == 0x213 && enteredIsland.Trampolines[1].Offset == 0x21B, "An island was placed over a branch target"); + + Bytes returning = {0xFF, 0xD0}; + for (int index = 0; index < 3; ++index) + returning.insert(returning.end(), add.begin(), add.end()); + returning.push_back(0xC3); + const auto returnIsland = convert(withStore(farCall, returning)); + expected = Bytes(adds.begin(), adds.begin() + 12); + expected.push_back(0xC3); + require(returnIsland.Trampolines.size() == 2 && returnIsland.Trampolines[0].Offset == 0x20B && body(returnIsland.Trampolines[0]) == expected, "Instructions moved for an island that end in a return jump back after it"); + + const auto afterBranch = convert(withStore({0x75, 0x10}, {0xFF, 0xD0})); + expected = {0x0F, 0x85, 0, 0, 0, 0}; + expected.insert(expected.end(), halves.begin(), halves.end()); + require(afterBranch.Trampolines.size() == 1 && afterBranch.Trampolines[0].Offset == 0x200 && afterBranch.Trampolines[0].Length == 6 && body(afterBranch.Trampolines[0]) == expected && afterBranch.Trampolines[0].Relocations[0].SiteTarget == 2 + 0x10, "A 256-bit store after a conditional branch did not take the branch"); + + Bytes far = adds; + far.resize(8); + for (int index = 0; index < 60; ++index) + far.insert(far.end(), {0xFF, 0xD0}); + far.insert(far.end(), adds.begin(), adds.end()); + for (int index = 0; index < 70; ++index) + far.insert(far.end(), {0xFF, 0xD0}); + Bytes tail; + for (int index = 0; index < 11; ++index) + tail.insert(tail.end(), {0xFF, 0xD0}); + tail.insert(tail.end(), {0x90, 0x90}); + requireFailure([&] { (void)convert(withStore(far, tail)); }, "A 256-bit store whose movable neighbours are all beyond a short jump was kept"); +} + void converterRosetta() { const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); Bytes file(0x300, 0xCC); @@ -700,17 +916,23 @@ void converterRosetta() { } void reciprocalOperands() { - const auto vex2 = Codegen::DecodeVexReciprocal(Bytes{0xC5, 0xF8, 0x52, 0xD5}.data(), 4); + const auto vex2 = Codegen::DecodeReciprocal(Bytes{0xC5, 0xF8, 0x52, 0xD5}.data(), 4); require(vex2 && vex2->Operation == Codegen::ReciprocalOperation::ReciprocalSquareRoot && vex2->Destination == 2 && vex2->Source == 5, "VEX2 VRSQRTPS was not decoded"); - const auto vex2High = Codegen::DecodeVexReciprocal(Bytes{0xC5, 0x78, 0x53, 0xC1}.data(), 4); + const auto vex2High = Codegen::DecodeReciprocal(Bytes{0xC5, 0x78, 0x53, 0xC1}.data(), 4); require(vex2High && vex2High->Operation == Codegen::ReciprocalOperation::Reciprocal && vex2High->Destination == 8 && vex2High->Source == 1, "VEX2 VRCPPS with a high destination was not decoded"); - const auto vex3 = Codegen::DecodeVexReciprocal(Bytes{0xC4, 0x41, 0x78, 0x52, 0xC9}.data(), 5); + const auto vex3 = Codegen::DecodeReciprocal(Bytes{0xC4, 0x41, 0x78, 0x52, 0xC9}.data(), 5); require(vex3 && vex3->Destination == 9 && vex3->Source == 9, "VEX3 VRSQRTPS with high registers was not decoded"); - require(!Codegen::DecodeVexReciprocal(Bytes{0xC5, 0xFC, 0x52, 0xD5}.data(), 4), "256-bit VRSQRTPS must stay native"); - require(!Codegen::DecodeVexReciprocal(Bytes{0xC5, 0xFA, 0x52, 0xD5}.data(), 4), "VRSQRTSS must stay native"); - require(!Codegen::DecodeVexReciprocal(Bytes{0xC5, 0xF8, 0x52, 0x10}.data(), 4), "Memory form must stay native"); - require(!Codegen::DecodeVexReciprocal(Bytes{0xC5, 0xF8, 0x51, 0xD5}.data(), 4), "VSQRTPS is not a reciprocal"); - require(!Codegen::DecodeVexReciprocal(Bytes{0x0F, 0x52, 0xD5}.data(), 3), "Legacy RSQRTPS must stay native"); + require(!Codegen::DecodeReciprocal(Bytes{0xC5, 0xFC, 0x52, 0xD5}.data(), 4), "256-bit VRSQRTPS must stay native"); + require(!Codegen::DecodeReciprocal(Bytes{0xC5, 0xFA, 0x52, 0xD5}.data(), 4), "VRSQRTSS must stay native"); + require(!Codegen::DecodeReciprocal(Bytes{0xC5, 0xF8, 0x52, 0x10}.data(), 4), "Memory form must stay native"); + require(!Codegen::DecodeReciprocal(Bytes{0xC5, 0xF8, 0x51, 0xD5}.data(), 4), "VSQRTPS is not a reciprocal"); + require(vex2->Vex && vex2High->Vex && vex3->Vex, "VEX reciprocals must clear the upper vector bits"); + const auto legacy = Codegen::DecodeReciprocal(Bytes{0x0F, 0x52, 0xD5}.data(), 3); + require(legacy && !legacy->Vex && legacy->Operation == Codegen::ReciprocalOperation::ReciprocalSquareRoot && legacy->Destination == 2 && legacy->Source == 5, "Legacy RSQRTPS was not decoded"); + const auto legacyHigh = Codegen::DecodeReciprocal(Bytes{0x45, 0x0F, 0x53, 0xC9}.data(), 4); + require(legacyHigh && !legacyHigh->Vex && legacyHigh->Operation == Codegen::ReciprocalOperation::Reciprocal && legacyHigh->Destination == 9 && legacyHigh->Source == 9, "Legacy RCPPS with high registers was not decoded"); + for (const Bytes& unsupported : {Bytes{0xF3, 0x0F, 0x52, 0xD5}, Bytes{0xF3, 0x0F, 0x53, 0xD5}, Bytes{0x0F, 0x52, 0x10}, Bytes{0x0F, 0x53, 0x10}, Bytes{0x0F, 0x51, 0xD5}, Bytes{0x0F, 0x52}, Bytes{0x45, 0x0F, 0x53}}) + require(!Codegen::DecodeReciprocal(unsupported.data(), unsupported.size()), "An unsupported legacy reciprocal encoding was accepted"); } void converterReciprocal() { @@ -744,12 +966,33 @@ void converterReciprocal() { require(moved.Trampolines.size() == 1 && moved.KeptCount == 0 && moved.Bytes == ripRelative && moved.Trampolines[0].Length == 10, "VRSQRTPS did not absorb the following RIP-relative load"); const auto& movedSite = moved.Trampolines[0]; require(movedSite.Relocations.size() == 1 && movedSite.Relocations[0].DisplacementOffset == movedSite.ReturnBranchOffset - 4 && movedSite.Relocations[0].InstructionEnd == movedSite.ReturnBranchOffset && movedSite.Relocations[0].SiteTarget == 10 + 0x10, "Absorbed RIP-relative load has the wrong relocation"); + for (const std::uint8_t opcode : {std::uint8_t{0x52}, std::uint8_t{0x53}}) { + const std::string name = opcode == 0x52 ? "RSQRTPS" : "RCPPS"; + for (const Bytes& instruction : {Bytes{0x0F, opcode, 0xD5}, Bytes{0x45, 0x0F, opcode, 0xC9}}) { + auto legacyFile = file; + Bytes legacyText = instruction; + legacyText.insert(legacyText.end(), {0x90, 0x90, 0xC3}); + std::copy(legacyText.begin(), legacyText.end(), legacyFile.begin() + 0x200); + const auto legacy = converter->Convert(legacyFile, {segmentHeader(legacyText.size())}); + require(legacy.Trampolines.size() == 1 && legacy.KeptCount == 0 && legacy.Reports[0].InstructionName == name && legacy.Trampolines[0].Length == 5, "Legacy reciprocal did not absorb enough instructions for a jump"); + legacyText = instruction; + legacyText.push_back(0xC3); + std::copy(legacyText.begin(), legacyText.end(), legacyFile.begin() + 0x200); + const auto kept = converter->Convert(legacyFile, {segmentHeader(legacyText.size())}); + require(kept.Trampolines.empty() && kept.KeptCount == 1 && kept.Bytes == legacyFile && kept.Reports[0].InstructionName == name, "Legacy reciprocal before a return must stay native"); + } + } + auto sequenceFile = file; + const Bytes sequenceText = {0x0F, 0x52, 0xD5, 0xC5, 0xF8, 0x53, 0xC1, 0xC3}; + std::copy(sequenceText.begin(), sequenceText.end(), sequenceFile.begin() + 0x200); + const auto sequence = converter->Convert(sequenceFile, {segmentHeader(sequenceText.size())}); + require(sequence.Trampolines.size() == 1 && sequence.KeptCount == 0 && sequence.Trampolines[0].Length == 7 && sequence.Reports[0].InstructionName == "RSQRTPS", "Mixed legacy and VEX reciprocals were not lowered together"); } void converterStrayRex() { const auto converter = Codegen::MakeAmd64OnlyConverter(); const auto convert = [&](const Bytes& text) { - Bytes file(0x300, 0xCC); + Bytes file(0x300 + text.size(), 0xCC); std::copy(text.begin(), text.end(), file.begin() + 0x200); return std::pair{file, converter->Convert(file, {segmentHeader(text.size())})}; }; @@ -1037,10 +1280,10 @@ void reciprocalExecution() { for (const auto& lanes : inputs) { const std::uint64_t source[2] = {packLanes(lanes, 0), packLanes(lanes, 2)}; const std::uint64_t destination[2] = {0x1111111111111111ull, 0x2222222222222222ull}; - for (const bool squareRoot : {true, false}) { + for (const bool squareRoot : {true, false}) for (const bool vex : {false, true}) { const std::uint8_t opcode = squareRoot ? 0x52 : 0x53; - const Bytes distinct = {0xC5, 0xF8, opcode, 0xD5}; - const Bytes same = {0xC5, 0xF8, opcode, 0xD2}; + const Bytes distinct = vex ? Bytes{0xC5, 0xF8, opcode, 0xD5} : Bytes{0x0F, opcode, 0xD5}; + const Bytes same = vex ? Bytes{0xC5, 0xF8, opcode, 0xD2} : Bytes{0x0F, opcode, 0xD2}; const auto distinctOut = runRegisterFormStub(distinct, destination, source); const auto sameOut = runRegisterFormStub(same, source, source); float distinctLanes[4]; @@ -1049,8 +1292,8 @@ void reciprocalExecution() { std::memcpy(sameLanes, sameOut.data(), sizeof(sameLanes)); for (std::size_t lane = 0; lane < 4; ++lane) { const float expected = squareRoot ? 1.0f / std::sqrt(lanes[lane]) : 1.0f / lanes[lane]; - require(sameLane(expected, distinctLanes[lane]), "VRSQRTPS/VRCPPS stub is not correctly rounded"); - require(sameLane(expected, sameLanes[lane]), "VRSQRTPS/VRCPPS stub with equal operands is not correctly rounded"); + require(sameLane(expected, distinctLanes[lane]), "Packed reciprocal stub is not correctly rounded"); + require(sameLane(expected, sameLanes[lane]), "Packed reciprocal stub with equal operands is not correctly rounded"); } } } @@ -1392,6 +1635,8 @@ int main() { converterClzero(); rosettaSubstitutions(); converterRosetta(); + wideStoreSplit(); + converterWideStoreWindows(); converterReciprocal(); converterStrayRex(); rewriterStrayRex(); diff --git a/core/relinker/codegen/tests/DecoderLengthCases.hpp b/core/relinker/codegen/tests/DecoderLengthCases.hpp index 561f3a64e..10747c9a6 100644 --- a/core/relinker/codegen/tests/DecoderLengthCases.hpp +++ b/core/relinker/codegen/tests/DecoderLengthCases.hpp @@ -33,6 +33,12 @@ inline constexpr DecoderLengthCase kDecoderLengthCases[] = { {"0f0b", 2}, {"0f0d846511223344", 8}, {"0f0e", 2}, + {"0f0fc196", 4}, + {"0f0f08b7", 4}, + {"0f0f4f80b4", 5}, + {"0f0f846511223344b4", 9}, + {"0f0f0d11223344a6", 8}, + {"410f0f1c24bf", 6}, {"0f10846511223344", 8}, {"0f11846511223344", 8}, {"0f12846511223344", 8}, diff --git a/core/relinker/codegen/tests/DecoderLengthTests.cpp b/core/relinker/codegen/tests/DecoderLengthTests.cpp index d119b3b32..e141e1539 100644 --- a/core/relinker/codegen/tests/DecoderLengthTests.cpp +++ b/core/relinker/codegen/tests/DecoderLengthTests.cpp @@ -77,6 +77,10 @@ int main() { } catch (const Codegen::CodegenException&) { } } + const auto threeDNow = parse("0f0f0d11223344a6"); + const auto threeDNowInfo = decoder.DecodeInstruction(threeDNow.data(), threeDNow.size()); + require(threeDNowInfo.HasModRm && threeDNowInfo.ModRmByte == 0x0d, "3DNow! ModRM was not reported"); + require(threeDNowInfo.HasRipRelativeDisp && threeDNowInfo.RipRelativeDispOffset == 3, "3DNow! RIP-relative displacement was not reported"); std::cout << "Decoder length tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/relinker/domain/include/domain/ImportModule.hpp b/core/relinker/domain/include/domain/ImportModule.hpp index ecc49bd39..37bab4c73 100644 --- a/core/relinker/domain/include/domain/ImportModule.hpp +++ b/core/relinker/domain/include/domain/ImportModule.hpp @@ -3,15 +3,21 @@ #include #include +#include #include #include namespace Domain { +inline constexpr std::array ModuleSuffixes = {".prx", ".sprx", ".suprx", ".debug_prx"}; + +inline std::string_view ModuleSuffix(const std::string_view name) { + const auto suffix = std::find_if(ModuleSuffixes.begin(), ModuleSuffixes.end(), [&](const auto candidate) { return name.ends_with(candidate); }); + return suffix == ModuleSuffixes.end() ? std::string_view{} : *suffix; +} + inline std::string ImportModuleName(std::string name) { - if (name.ends_with(".prx")) name.resize(name.size() - 4); - else if (name.ends_with(".sprx")) name.resize(name.size() - 5); - else if (name.ends_with(".suprx")) name.resize(name.size() - 6); + name.resize(name.size() - ModuleSuffix(name).size()); if (name.ends_with("-module")) name.resize(name.size() - 7); std::replace(name.begin(), name.end(), '.', '_'); return name; @@ -44,7 +50,7 @@ inline std::string ImportModule(const std::string& symbol, const std::map dependencies = {}) { auto name = DeclaredImportModule(symbol, modules); if (name.empty()) return {}; - if (!name.ends_with(".prx") && !name.ends_with(".sprx") && !name.ends_with(".suprx")) name += ".prx"; + if (ModuleSuffix(name).empty()) name += ".prx"; if (std::find(dependencies.begin(), dependencies.end(), name) != dependencies.end()) return name; std::string matched; for (const auto& dependency : dependencies) { diff --git a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp index 83281042f..a5ea5b7e3 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -26,6 +26,8 @@ std::string normalizeRunPath(std::string path) { path = "."; if (path.starts_with("$ORIGIN\\")) path.erase(0, 8); + if (path.empty()) + path = "."; if (path.find('$') != std::string::npos) throw Domain::RelinkerException("Unsupported variable in Windows run path"); if (path.back() != '\\') diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index df1ce0fea..e37c631dc 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -23,6 +23,8 @@ struct MovedInstruction { std::uint32_t Rva; std::vector Bytes; std::optional RipDisplacement; + std::optional BranchTarget; + std::size_t BranchDisplacementOffset = 0; }; struct TlsAccess { @@ -145,9 +147,28 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co if (length >= header.FileSize - offset) throw Domain::RelinkerException("Short guest TLS instruction ends its segment", header.Offset + offset); const auto next = decoder.DecodeInstruction(bytes + length, header.FileSize - offset - length); - if (next.FlowKind != Codegen::ControlFlowKind::Sequential || next.HasBranchTarget || next.SegmentPrefix != 0) + const bool relativeBranch = + (next.FlowKind == Codegen::ControlFlowKind::ConditionalBranch && (next.IsTwoByteOpcode ? next.Opcode >= 0x80 && next.Opcode <= 0x8f : next.Opcode >= 0x70 && next.Opcode <= 0x7f)) || + (next.FlowKind == Codegen::ControlFlowKind::UnconditionalJump && !next.HasRipRelativeDisp && !next.IsTwoByteOpcode && (next.Opcode == 0xe9 || next.Opcode == 0xeb)); + if (!relativeBranch && (next.FlowKind != Codegen::ControlFlowKind::Sequential || next.HasBranchTarget || next.SegmentPrefix != 0)) throw Domain::RelinkerException("Short guest TLS instruction is followed by an instruction that cannot move", header.Offset + offset + length); - moved.push_back({CheckedRva(rva + length), std::vector(bytes + length, bytes + length + next.Length), next.HasRipRelativeDisp ? std::optional(next.RipRelativeDispOffset) : std::nullopt}); + std::vector movedBytes(bytes + length, bytes + length + next.Length); + std::optional branchTarget; + std::size_t branchDisplacementOffset = 0; + if (relativeBranch) { + const auto target = static_cast(rva) + static_cast(length + next.Length) + next.BranchDisp; + if (target < 0 || target > std::numeric_limits::max()) + throw Domain::RelinkerException("Short guest TLS instruction is followed by a branch beyond the address space", header.Offset + offset + length); + branchTarget = static_cast(target); + if (!next.IsTwoByteOpcode && next.Opcode != 0xe9) { + movedBytes = next.Opcode == 0xeb ? std::vector{0xe9} : std::vector{0x0f, static_cast(0x80 | (next.Opcode & 0xf))}; + branchDisplacementOffset = movedBytes.size(); + movedBytes.insert(movedBytes.end(), 4, 0); + } else { + branchDisplacementOffset = next.Length - 4; + } + } + moved.push_back({CheckedRva(rva + length), std::move(movedBytes), next.HasRipRelativeDisp ? std::optional(next.RipRelativeDispOffset) : std::nullopt, branchTarget, branchDisplacementOffset}); length += next.Length; } accesses.push_back({rva, header.Offset + offset, length, false, 0, addressRegister, 0, isAlu ? bytes[position] : std::uint8_t{0}, *address, wide, std::move(moved)}); @@ -357,6 +378,12 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co throw Domain::RelinkerException("Moved RIP-relative instruction exceeds rel32 range from its TLS stub", access.FileOffset); Io::WriteU32(bytes, *instruction.RipDisplacement, static_cast(static_cast(displacement))); } + if (instruction.BranchTarget) { + const auto displacement = static_cast(*instruction.BranchTarget) - (static_cast(code.GetRva()) + static_cast(bytes.size())); + if (displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) + throw Domain::RelinkerException("Moved branch exceeds rel32 range from its TLS stub", access.FileOffset); + Io::WriteU32(bytes, instruction.BranchDisplacementOffset, static_cast(static_cast(displacement))); + } for (const auto byte : bytes) code.Emit({byte}); } diff --git a/core/relinker/elfpatcher/tests/Amd64OnlyWindowsTests.cpp b/core/relinker/elfpatcher/tests/Amd64OnlyWindowsTests.cpp index f88d3d93f..c726b2958 100644 --- a/core/relinker/elfpatcher/tests/Amd64OnlyWindowsTests.cpp +++ b/core/relinker/elfpatcher/tests/Amd64OnlyWindowsTests.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +60,7 @@ struct alignas(16) State { std::uint64_t RspBefore; std::uint64_t RspAfter; std::uint8_t RedZone[128]; + std::uint8_t Ymm[16][32]; }; constexpr std::uint64_t kCanary = 0xA5C3E17B9D24F608ull; @@ -94,6 +96,11 @@ void movdquXmmRsp(Bytes& code, const std::uint8_t reg, const bool store, const s emitU32(code, offset); } +void vmovdquYmmRbx(Bytes& code, const std::uint8_t reg, const bool store) { + emit(code, {0xC4, static_cast(reg >= 8 ? 0x61 : 0xE1), 0x7E, static_cast(store ? 0x7F : 0x6F), static_cast(0x83 | ((reg & 7) << 3))}); + emitU32(code, static_cast(offsetof(State, Ymm)) + reg * 32); +} + class Harness { public: Harness() : _memory(VirtualAlloc(nullptr, kSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)) { @@ -104,7 +111,7 @@ public: VirtualFree(_memory, 0, MEM_RELEASE); } - void Run(const Bytes& body, const std::size_t returnBranchOffset, State& state) const { + void Run(const Bytes& body, const std::size_t returnBranchOffset, State& state, const bool wide = false) const { Bytes code; emit(code, {0x53, 0x55, 0x56, 0x57, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57, 0x48, 0x81, 0xEC, 0xA8, 0x00, 0x00, 0x00}); for (std::uint8_t reg = 6; reg < 16; ++reg) movdquXmmRsp(code, reg, true, (reg - 6) * 16); @@ -115,7 +122,12 @@ public: for (int slot = 0; slot < 16; ++slot) emit(code, {0x48, 0x89, 0x44, 0x24, static_cast(-128 + slot * 8)}); emit(code, {0x48, 0x89, 0xA3}); emitU32(code, kRspBeforeOffset); - for (std::uint8_t reg = 0; reg < 16; ++reg) movdquXmmRbx(code, reg, false); + for (std::uint8_t reg = 0; reg < 16; ++reg) { + if (wide) + vmovdquYmmRbx(code, reg, false); + else + movdquXmmRbx(code, reg, false); + } code.push_back(0xE9); const auto jumpOffset = code.size(); emitU32(code, 0); @@ -129,6 +141,8 @@ public: emit(code, {0x9C, 0x8F, 0x83}); emitU32(code, kFlagsOutOffset); for (std::uint8_t reg = 0; reg < 16; ++reg) movdquXmmRbx(code, reg, true); + if (wide) + for (std::uint8_t reg = 0; reg < 16; ++reg) vmovdquYmmRbx(code, reg, true); for (std::uint8_t reg = 6; reg < 16; ++reg) movdquXmmRsp(code, reg, false, (reg - 6) * 16); emit(code, {0x48, 0x81, 0xC4, 0xA8, 0x00, 0x00, 0x00, 0x41, 0x5F, 0x41, 0x5E, 0x41, 0x5D, 0x41, 0x5C, 0x5F, 0x5E, 0x5D, 0x5B, 0xC3}); code.resize(Io::AlignUp(code.size(), std::size_t{16}), 0xCC); @@ -219,6 +233,69 @@ void requireEnvironment(const State& input, const State& state, const std::uint1 } } +void reciprocalExecution() { + const Harness harness; + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(); + const bool wide = __builtin_cpu_supports("avx"); + const float inputs[][4] = { + {1.0f, 4.0f, 0.25f, 2.0f}, + {0.0f, -0.0f, INFINITY, -1.0f}, + {1.00000012f, 0.99999994f, 3.0e-38f, 1.0e30f}, + {NAN, -INFINITY, -4.0f, 16.0f}}; + for (const bool vex : {false, true}) { + if (vex && !wide) + continue; + for (const std::uint8_t opcode : {std::uint8_t{0x52}, std::uint8_t{0x53}}) { + for (const std::uint8_t destination : {std::uint8_t{0}, std::uint8_t{2}, std::uint8_t{9}, std::uint8_t{15}}) { + for (const std::uint8_t source : {destination, std::uint8_t{0}, std::uint8_t{5}, std::uint8_t{14}}) { + Bytes site; + if (vex) { + emit(site, {0xC4, static_cast((destination >= 8 ? 0 : 0x80) | 0x40 | (source >= 8 ? 0 : 0x20) | 1), 0x78}); + } else { + const auto rex = static_cast(0x40 | (destination >= 8 ? 4 : 0) | (source >= 8 ? 1 : 0)); + if (rex != 0x40) + site.push_back(rex); + site.push_back(0x0F); + } + emit(site, {opcode, static_cast(0xC0 | ((destination & 7) << 3) | (source & 7))}); + const auto match = matcher->Match(site.data(), site.size()); + require(match && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline, "Packed reciprocal was not lowered"); + for (const auto& lanes : inputs) { + State input{}; + for (std::size_t reg = 0; reg < 16; ++reg) { + for (std::size_t byte = 0; byte < 16; ++byte) { + input.Xmm[reg][byte] = static_cast(reg * 16 + byte); + input.Ymm[reg][byte + 16] = static_cast(reg * 16 + byte + 1); + } + } + std::memcpy(input.Xmm[source], lanes, sizeof(lanes)); + for (std::size_t reg = 0; reg < 16; ++reg) + std::memcpy(input.Ymm[reg], input.Xmm[reg], 16); + input.FlagsIn = 0xAC7; + State state = input; + harness.Run(match->StubBody, match->ReturnBranchOffset, state, wide); + ++g_executions; + float actual[4]; + std::memcpy(actual, state.Xmm[destination], sizeof(actual)); + for (std::size_t lane = 0; lane < 4; ++lane) { + const float expected = opcode == 0x52 ? 1.0f / std::sqrt(lanes[lane]) : 1.0f / lanes[lane]; + require(std::isnan(expected) ? std::isnan(actual[lane]) : std::memcmp(&expected, &actual[lane], sizeof(float)) == 0, "Packed reciprocal result differs from division and square root"); + } + requireEnvironment(input, state, static_cast(1u << destination), "packed reciprocal"); + if (wide) { + const std::uint8_t zero[16] = {}; + for (std::size_t reg = 0; reg < 16; ++reg) { + const auto* expected = vex && reg == destination ? zero : input.Ymm[reg] + 16; + require(std::memcmp(state.Ymm[reg] + 16, expected, 16) == 0, "Packed reciprocal changed the wrong upper YMM bits"); + } + } + } + } + } + } + } +} + void executeCase(const Harness& harness, const Codegen::IAmd64OnlyInstructionMatcher& matcher, const Case& item, std::mt19937_64& random, const int images) { const auto site = encode(item); const auto match = matcher.Match(site.data(), site.size()); @@ -643,6 +720,7 @@ void peBuilder() { int main() { try { cpuExecution(); + reciprocalExecution(); peBuilder(); std::cout << "AMD64-only Windows tests passed (" << g_executions << " lowered sequences executed)\n"; } catch (const std::exception& error) { diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 0e747853d..824d8d656 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -71,10 +71,11 @@ int Run(const int argc, char* argv[]) { sourceBytes = std::move(converted.Bytes); trampolines = std::move(converted.Trampolines); std::map stubsByName; + const auto counted = [](const std::string& name) { return name == "VRSQRTPS" || name == "VRCPPS" || name == "256-bit store"; }; for (const auto& report : converted.Reports) { if (report.Lowering == Codegen::Amd64OnlyLowering::Kept) std::cout << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) kept: no room for a jump\n"; - else if (report.InstructionName == "VRSQRTPS" || report.InstructionName == "VRCPPS") + else if (counted(report.InstructionName)) ++stubsByName[report.InstructionName]; else std::cout << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) -> " << (report.Lowering == Codegen::Amd64OnlyLowering::InPlace ? "in place " : "stub ") << report.ReplacementLength << " bytes\n"; @@ -127,19 +128,16 @@ int Run(const int argc, char* argv[]) { checkOutputInputs(desktopEntry->IconPath, "Icon"); checkOutputInputs(desktopEntry->EntryPath, "Desktop entry"); } + if (args.windowsDiagnostics) + checkOutputInputs(std::filesystem::absolute("windows-diagnostics-imports.txt"), "Windows import diagnostics"); + std::vector registryPaths; if (args.writeRegistry) { const std::filesystem::path outFsPath = Io::NativePath(absPath); - std::vector registryPaths{ - outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + ".registry.json") - }; + registryPaths.push_back(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + ".registry.json")); for (const auto& artifact : guestArtifacts) registryPaths.push_back(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + "." + Io::Utf8Path(artifact.Path.filename()) + ".registry.json")); for (const auto& path : registryPaths) checkOutputInputs(path, "Registry"); - const auto registryWriter = std::make_shared(); - fileWriter.Write(Io::Utf8Path(registryPaths.front()), registryWriter->WriteCallRegistry(result.RegistryEntries)); - for (std::size_t index = 0; index < guestArtifacts.size(); ++index) - fileWriter.Write(Io::Utf8Path(registryPaths[index + 1]), registryWriter->WriteModuleImports(guestArtifacts[index].Imports)); } auto byteWriter = std::make_shared(); @@ -174,6 +172,12 @@ int Run(const int argc, char* argv[]) { std::cout << "Guest module: " << Io::Utf8Path(artifact.Path) << '\n'; } fileWriter.Write(absPath, executableBytes); + if (args.writeRegistry) { + const auto registryWriter = std::make_shared(); + fileWriter.Write(Io::Utf8Path(registryPaths.front()), registryWriter->WriteCallRegistry(result.RegistryEntries)); + for (std::size_t index = 0; index < guestArtifacts.size(); ++index) + fileWriter.Write(Io::Utf8Path(registryPaths[index + 1]), registryWriter->WriteModuleImports(guestArtifacts[index].Imports)); + } if (desktopEntry) std::cout << "Desktop entry: " << Io::Utf8Path(desktopEntryWriter.Write(*desktopEntry)) << '\n'; std::cout << "External prx references: " << result.RegistryEntries.size() << "\nOutput file: " << absPath << '\n'; std::cout << "Expected runtime layout (relative to the output executable):\n" diff --git a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp index 6f08ab26f..fbd29c405 100644 --- a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp +++ b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp @@ -4,7 +4,8 @@ #include #include #include -#include +#include +#include namespace Relinker { @@ -27,7 +28,8 @@ bool isRipRelativeFF(const std::vector& text, const Codegen::Instr std::uint8_t modrm = text[cur + 1]; std::uint8_t mod = (modrm >> ModRmModShift) & ModRmModMask; std::uint8_t rm = modrm & ModRmRmMask; - return mod == ModRmModIndirect && rm == ModRmRmRipRelative; + const auto operation = (modrm >> ModRmRegShift) & ModRmRegMask; + return mod == ModRmModIndirect && rm == ModRmRmRipRelative && (operation >= 2 && operation <= 6); } break; } @@ -70,7 +72,7 @@ public: std::vector ResolveCallSites(const std::vector& textSection, FileByteOffset textSectionVAddr, VirtualAddress targetGotOrPltAddress, ByteCount targetGotOrPltSize) override; private: const std::vector* _cachedTextPtr = nullptr; - std::multimap _cachedTargetToInstr; + std::vector> _cachedTargetToInstr; FileByteOffset _cachedTextVAddr = 0; }; @@ -82,6 +84,10 @@ std::vector CallSiteResolver::ResolveCallSites(const std::vector auto scanner = Codegen::MakeInstructionScanner(); auto instructions = scanner->ScanCodeSection(textSection, 0, textSection.size()); _cachedTargetToInstr.clear(); + const auto referenceCount = std::count_if(instructions.begin(), instructions.end(), [&](const auto& match) { + return isRipRelativeFF(textSection, match) || isRipRelativeMov64(textSection, match); + }); + _cachedTargetToInstr.reserve(static_cast(referenceCount)); for (const auto& m : instructions) { std::size_t dispOff = 0; if (isRipRelativeFF(textSection, m)) @@ -94,15 +100,20 @@ std::vector CallSiteResolver::ResolveCallSites(const std::vector std::int32_t disp = readDisp32(textSection, dispOff); VirtualAddress instrEnd = textSectionVAddr + m.Offset + static_cast(m.Length); VirtualAddress target = static_cast(static_cast(instrEnd) + disp); - _cachedTargetToInstr.emplace(target, m.Offset); + _cachedTargetToInstr.emplace_back(target, m.Offset); } + std::sort(_cachedTargetToInstr.begin(), _cachedTargetToInstr.end()); _cachedTextPtr = &textSection; _cachedTextVAddr = textSectionVAddr; } std::vector sites; - auto lower = _cachedTargetToInstr.lower_bound(targetGotOrPltAddress); - auto upper = _cachedTargetToInstr.lower_bound(targetGotOrPltAddress + targetGotOrPltSize); + const auto findTarget = [&](VirtualAddress target) { + return std::lower_bound(_cachedTargetToInstr.begin(), _cachedTargetToInstr.end(), + std::pair{target, 0}); + }; + auto lower = findTarget(targetGotOrPltAddress); + auto upper = findTarget(targetGotOrPltAddress + targetGotOrPltSize); for (auto it = lower; it != upper; ++it) sites.push_back(textSectionVAddr + it->second); return sites; diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index 3d78157a2..876fa53ea 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -181,7 +181,8 @@ std::set CodeInstructionCollector::Collect(const std::ve const auto index = info >> 32; const auto addend = Io::ReadU64(bytes, offset + 16); if (type == 8) { - if (index != 0 || !pointers.Values.emplace(slot, addend).second) throw Domain::RelinkerException("Code analysis: invalid relative relocation", slot); + const auto zeroSectionSymbol = index != 0 && index < symbolCount && (bytes[symbols + index * 24 + 4] & 15) == 3 && Io::ReadU64(bytes, symbols + index * 24 + 8) == 0; + if ((index != 0 && !zeroSectionSymbol) || !pointers.Values.emplace(slot, addend).second) throw Domain::RelinkerException("Code analysis: invalid relative relocation", slot); addRoot(addend); } else if (type == 1 || type == 6 || type == 7) { if (index == 0 || index >= symbolCount) throw Domain::RelinkerException("Code analysis: invalid relocation symbol", slot); diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp index e4301940e..18842fd8c 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp @@ -74,87 +74,88 @@ void buildGraph( VirtualAddress va = worklist.front(); worklist.pop(); - if (va < textVaddr || va >= textVaddr + static_cast(text.size())) - throw RelinkerException("CFG: jump target outside text segment", va); + while (va >= textVaddr && va - textVaddr < text.size() && !reachable.contains(va)) { + std::size_t bufOff = static_cast(va - textVaddr); + std::size_t available = text.size() - bufOff; + if (available == 0) throw RelinkerException("CFG: zero available bytes at target", va); - std::size_t bufOff = static_cast(va - textVaddr); - std::size_t available = text.size() - bufOff; - if (available == 0) throw RelinkerException("CFG: zero available bytes at target", va); - - Codegen::DecodedInstructionInfo info; - if (speculative) { - try { + Codegen::DecodedInstructionInfo info; + if (speculative) { + try { + info = decoder.DecodeInstruction(text.data() + bufOff, available); + } catch (const Codegen::CodegenException&) { + break; + } + } else { info = decoder.DecodeInstruction(text.data() + bufOff, available); - } catch (const Codegen::CodegenException&) { - continue; } - } else { - info = decoder.DecodeInstruction(text.data() + bufOff, available); - } - reachable.insert(va); + reachable.insert(va); + visited.erase(va); - VirtualAddress nextVaddr = va + static_cast(info.Length); + VirtualAddress nextVaddr = va + static_cast(info.Length); - if (followCodeAddresses && info.HasRipRelativeDisp && text[bufOff + info.OpcodeOffset] == 0x8d && (info.RexPrefix & 8)) { - std::int32_t displacement = 0; - std::memcpy(&displacement, text.data() + bufOff + info.RipRelativeDispOffset, 4); - enqueue(static_cast(static_cast(nextVaddr) + displacement)); - } + if (followCodeAddresses && info.HasRipRelativeDisp && text[bufOff + info.OpcodeOffset] == 0x8d && (info.RexPrefix & 8)) { + std::int32_t displacement = 0; + std::memcpy(&displacement, text.data() + bufOff + info.RipRelativeDispOffset, 4); + enqueue(static_cast(static_cast(nextVaddr) + displacement)); + } - if (data != nullptr && info.HasRipRelativeDisp && !info.IsTwoByteOpcode && info.Opcode == 0x8D) { - std::int32_t displacement = 0; - std::memcpy(&displacement, text.data() + bufOff + info.RipRelativeDispOffset, 4); - const auto base = static_cast(static_cast(nextVaddr) + displacement); - takeAddress(base); - for (VirtualAddress target : ReadRelativeTableTargets(*data, base, textVaddr, text.size())) takeAddress(target); - } + if (data != nullptr && info.HasRipRelativeDisp && !info.IsTwoByteOpcode && info.Opcode == 0x8D) { + std::int32_t displacement = 0; + std::memcpy(&displacement, text.data() + bufOff + info.RipRelativeDispOffset, 4); + const auto base = static_cast(static_cast(nextVaddr) + displacement); + takeAddress(base); + for (VirtualAddress target : ReadRelativeTableTargets(*data, base, textVaddr, text.size())) takeAddress(target); + } - switch (info.FlowKind) { - using enum Codegen::ControlFlowKind; - case Sequential: - enqueue(nextVaddr); - break; - case ConditionalBranch: - enqueue(nextVaddr); - if (info.HasBranchTarget) - enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); - break; - case UnconditionalJump: - if (info.HasRipRelativeDisp) { - std::int32_t disp = 0; - std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); - VirtualAddress slotVaddr = static_cast( - static_cast(nextVaddr) + disp - ); - auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); - if (resolved.has_value()) - enqueue(*resolved); - } else if (info.HasBranchTarget) { - enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); - } - break; - case Call: - if (info.HasRipRelativeDisp) { - std::int32_t disp = 0; - std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); - VirtualAddress slotVaddr = static_cast( - static_cast(nextVaddr) + disp - ); - auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); - if (resolved.has_value()) - enqueue(*resolved); - } else if (info.HasBranchTarget) { - enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); - } - enqueue(nextVaddr); - break; - case IndirectCall: - enqueue(nextVaddr); - break; - case Return: - case IndirectJump: - case Trap: - break; + bool fallThrough = true; + switch (info.FlowKind) { + using enum Codegen::ControlFlowKind; + case Sequential: + break; + case ConditionalBranch: + if (info.HasBranchTarget) + enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); + break; + case UnconditionalJump: + fallThrough = false; + if (info.HasRipRelativeDisp) { + std::int32_t disp = 0; + std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); + VirtualAddress slotVaddr = static_cast( + static_cast(nextVaddr) + disp + ); + auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); + if (resolved.has_value()) + enqueue(*resolved); + } else if (info.HasBranchTarget) { + enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); + } + break; + case Call: + if (info.HasRipRelativeDisp) { + std::int32_t disp = 0; + std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); + VirtualAddress slotVaddr = static_cast( + static_cast(nextVaddr) + disp + ); + auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); + if (resolved.has_value()) + enqueue(*resolved); + } else if (info.HasBranchTarget) { + enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); + } + break; + case IndirectCall: + break; + case Return: + case IndirectJump: + case Trap: + fallThrough = false; + break; + } + if (!fallThrough) break; + va = nextVaddr; } } } diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp index 84d8b64d2..8ce7c297b 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp @@ -1,6 +1,7 @@ #include #include #include +#include namespace Relinker::UnusedNidFilter { @@ -37,7 +38,7 @@ public: for (std::uint64_t index = 0; index < count; ++index) { const auto begin = encoded(position, tableEncoding, segment.MappedAddress); const auto fde = encoded(position, tableEncoding, segment.MappedAddress); - if (position > segment.MappedAddress + segment.FileSize) throw RelinkerException("Strict filter: EH frame table exceeds segment", position); + if (position < segment.MappedAddress || position - segment.MappedAddress > segment.FileSize) throw RelinkerException("Strict filter: EH frame table exceeds segment", position); result.push_back(readFde(begin, fde)); } } @@ -51,11 +52,38 @@ private: const std::set& importSlots; std::map cies; + VirtualAddress checkedRangeEnd(VirtualAddress address, std::uint64_t size) const { + if (size > std::numeric_limits::max() - address) + throw RelinkerException("Strict filter: unwind range overflowed", address); + const auto end = address + size; + while (address < end) { + bool found = false; + for (const auto& segment : headers) { + if (segment.Type != 1 || address < segment.MappedAddress) continue; + const auto relative = address - segment.MappedAddress; + if (relative >= segment.FileSize) continue; + const auto chunk = std::min(end - address, segment.FileSize - relative); + if (segment.Offset > bytes.size() || relative > bytes.size() - segment.Offset || + chunk > bytes.size() - segment.Offset - relative) + throw RelinkerException("Strict filter: unwind data exceeds file", address); + address += chunk; + found = true; + break; + } + if (!found) throw RelinkerException("Strict filter: unwind range is not file-backed", address); + } + return end; + } + template TValue read(VirtualAddress& address) const { for (const auto& segment : headers) { if (segment.Type != 1 || address < segment.MappedAddress || address - segment.MappedAddress > segment.FileSize || sizeof(TValue) > segment.FileSize - (address - segment.MappedAddress)) continue; - const auto offset = segment.Offset + address - segment.MappedAddress; + const auto relative = address - segment.MappedAddress; + if (segment.Offset > bytes.size() || relative > bytes.size() - segment.Offset || + sizeof(TValue) > std::numeric_limits::max() - address) + throw RelinkerException("Strict filter: unwind data range overflowed", address); + const auto offset = segment.Offset + relative; if (offset > bytes.size() || sizeof(TValue) > bytes.size() - offset) throw RelinkerException("Strict filter: unwind data exceeds file", address); TValue value; std::memcpy(&value, bytes.data() + offset, sizeof(value)); @@ -118,7 +146,7 @@ private: auto position = address; const auto length = read(position); if (length == 0xFFFFFFFF || length < 8) throw RelinkerException("Strict filter: unsupported CIE record size", address); - const auto end = position + length; + const auto end = checkedRangeEnd(position, length); if (read(position) != 0) throw RelinkerException("Strict filter: invalid CIE identifier", address); const auto version = read(position); if (version != 1 && version != 3 && version != 4) throw RelinkerException("Strict filter: unsupported CIE version", address); @@ -140,8 +168,9 @@ private: if (augmentation.front() != 'z') throw RelinkerException("Strict filter: unsupported CIE augmentation", address); cie.Augmented = true; const auto size = leb(position); + if (position > end || size > end - position) + throw RelinkerException("Strict filter: CIE augmentation exceeds record", address); const auto augmentationEnd = position + size; - if (augmentationEnd > end) throw RelinkerException("Strict filter: CIE augmentation exceeds record", address); for (std::size_t index = 1; index < augmentation.size(); ++index) { switch (augmentation[index]) { case 'L': cie.LsdaEncoding = read(position); break; @@ -166,7 +195,7 @@ private: auto position = address; const auto length = read(position); if (length == 0xFFFFFFFF || length < 8) throw RelinkerException("Strict filter: unsupported FDE record size", address); - const auto end = position + length; + const auto end = checkedRangeEnd(position, length); const auto cieField = position; const auto cieOffset = read(position); if (cieOffset > cieField) throw RelinkerException("Strict filter: invalid FDE CIE offset", address); @@ -179,6 +208,8 @@ private: if (cie.Personality != 0) region.ExtraTargets.push_back(cie.Personality); if (cie.Augmented) { const auto augmentationSize = leb(position); + if (position > end || augmentationSize > end - position) + throw RelinkerException("Strict filter: FDE augmentation exceeds record", address); const auto augmentationEnd = position + augmentationSize; if (augmentationEnd > end) throw RelinkerException("Strict filter: FDE augmentation exceeds record", address); if (cie.LsdaEncoding != 0xFF) { @@ -201,7 +232,7 @@ private: if ((callEncoding & 0xF0) != 0) throw RelinkerException("Strict filter: unsupported LSDA call-site encoding", address); const auto tableSize = leb(position); if (tableSize > bytes.size()) throw RelinkerException("Strict filter: invalid LSDA table size", address); - const auto tableEnd = position + tableSize; + const auto tableEnd = checkedRangeEnd(position, tableSize); while (position < tableEnd) { encoded(position, callEncoding); encoded(position, callEncoding); diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp index cf163ac55..3eede671e 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp @@ -79,7 +79,12 @@ public: const auto slot = read(*rela + offset); const auto info = read(*rela + offset + 8); if (static_cast(info) != 8) continue; - if (info >> 32) throw RelinkerException("Strict filter: relative relocation has a symbol", slot); + if (const auto symbol = info >> 32; symbol != 0) { + const auto symbols = table(6, 0x61000039); + const auto size = tags.find(0x6100003F); + if (!symbols || size == tags.end() || symbol >= size->second / 24 || (read(*symbols + symbol * 24 + 4) & 15) != 3 || read(*symbols + symbol * 24 + 8) != 0) + throw RelinkerException("Strict filter: relative relocation has a symbol", slot); + } const auto target = read(*rela + offset + 16); if (input.ImportSlots.contains(slot) || !input.Pointers.emplace(slot, target).second) throw RelinkerException("Strict filter: overlapping pointer relocations", slot); diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index e6c3545a7..52cf793e5 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -262,13 +262,15 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector const auto size = get(sizeTag, sceSizeTag); if (size % 24 != 0) fail("Invalid relocation table size"); const auto offset = table(addressTag, sceAddressTag, size); - output.assign(bytes.begin() + offset, bytes.begin() + offset + size); + if (plt && size != 0 && get(20, 0x6100002b) != 7) fail("Unsupported PLT relocation format"); + output.reserve(size); for (std::uint64_t index = 0; index < size; index += 24) { - const auto info = Io::ReadU64(output, index + 8); + const auto info = Io::ReadU64(bytes, offset + index + 8); const auto relocation = static_cast(info); + if (relocation == 0) continue; if ((info >> 32) >= image.Symbols.size() || (plt && relocation != 7) || (relocation != 1 && relocation != 6 && relocation != 7 && relocation != 8 && relocation != 16 && relocation != 17 && relocation != 18)) fail("Unsupported relocation or symbol index"); - const auto target = Io::ReadU64(output, index); - const auto addend = Io::ReadU64(output, index + 16); + const auto target = Io::ReadU64(bytes, offset + index); + const auto addend = Io::ReadU64(bytes, offset + index + 16); mapped(target, 8, 2); const auto next = relocationTargets.lower_bound(target); if ((next != relocationTargets.end() && next->first < target + 8) || (next != relocationTargets.begin() && std::prev(next)->second > target)) fail("Overlapping guest relocations"); @@ -283,6 +285,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector const auto& symbol = image.Symbols[info >> 32]; if (((info >> 32) == 0 && tls == nullptr) || ((info >> 32) != 0 && (symbol.Info & 15) != 6) || (relocation == 16 && addend != 0)) fail("Invalid guest TLS relocation"); } + output.insert(output.end(), bytes.begin() + offset + index, bytes.begin() + offset + index + 24); } }; if (tags.contains(7) || tags.contains(0x6100002f)) { @@ -290,7 +293,6 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector } copyRelocations(7, 0x6100002f, 8, 0x61000031, image.Dynamic.RelaData, false); copyRelocations(23, 0x61000029, 2, 0x6100002d, image.Dynamic.RelaPltData, true); - if (!image.Dynamic.RelaPltData.empty() && get(20, 0x6100002b) != 7) fail("Unsupported PLT relocation format"); if (tags.contains(3) || tags.contains(0x61000027)) image.Got = get(3, 0x61000027); if (tags.contains(12)) image.Init = tags.at(12); if (tags.contains(13)) image.Fini = tags.at(13); diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index d51c4c160..bbcecbca4 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -44,7 +45,7 @@ std::string FoldFilename(std::string name) { std::string ModuleStem(std::string name, const bool windows) { if (windows) name = FoldFilename(std::move(name)); - for (const std::string_view suffix : {".debug_prx", ".sprx", ".prx"}) { + for (const std::string_view suffix : {".debug_prx", ".suprx", ".sprx", ".prx"}) { if (name.size() > suffix.size() && name.ends_with(suffix)) return name.substr(0, name.size() - suffix.size()); } return {}; @@ -102,6 +103,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path continue; } if (!entry.is_regular_file()) continue; + if (std::filesystem::equivalent(entry.path(), inputPath)) continue; if (isElf(entry.path())) paths.push_back(entry.path()); } } @@ -119,9 +121,8 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path Io::FileReader reader; std::map discovered; const auto matchesIdentity = [](const std::string& name, const std::vector& identities) { - return std::any_of(identities.begin(), identities.end(), [&](const auto& identity) { - return name == identity || name == identity + ".prx" || name == identity + ".sprx" || name == identity + ".suprx"; - }); + const auto stem = std::string_view(name).substr(0, name.size() - Domain::ModuleSuffix(name).size()); + return std::any_of(identities.begin(), identities.end(), [&](const auto& identity) { return name == identity || stem == identity; }); }; for (const auto& path : paths) discovered.emplace(path, GuestImageReader().Read(path, reader.Read(Io::Utf8Path(path)), windows)); if (!unmatchedExclusions.empty()) throw Domain::RelinkerException("Excluded guest module file not found: " + *unmatchedExclusions.begin()); @@ -288,6 +289,10 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path const auto deferred = [&](const std::size_t index) { return !required[index] && std::find(runtimeDirectories.begin(), runtimeDirectories.end(), images[index].SourcePath.parent_path().lexically_normal()) != runtimeDirectories.end(); }; + const auto appDirectory = [&](const GuestImage& image) { + const auto directory = image.SourcePath.parent_path().lexically_relative(root).generic_string(); + return directory == "." ? std::string("app0") : "app0/" + directory; + }; if (!windows) { for (auto& image : images) { for (std::size_t index = 1; index < image.Symbols.size(); ++index) { @@ -324,9 +329,9 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path Io::AppendU64(dynamic.DynamicSegmentData, dynamic.DynStrData.size()); Io::AppendString(dynamic.DynStrData, name); }; - for (const auto index : order) if (!windows && !deferred(index)) addNeeded("$ORIGIN/app0/" + images[index].SourcePath.parent_path().lexically_relative(root).generic_string() + "/" + images[index].OutputName); + for (const auto index : order) if (!windows && !deferred(index)) addNeeded("$ORIGIN/" + appDirectory(images[index]) + "/" + images[index].OutputName); for (const auto& name : hostLibraries) addNeeded(name); - if (!windows && runPath != "$ORIGIN" && !runPath.starts_with("$ORIGIN/") && !std::filesystem::path(runPath).is_absolute()) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); + if (!windows && runPath != "$ORIGIN" && !runPath.starts_with("$ORIGIN/") && !runPath.starts_with("$ORIGIN:") && !std::filesystem::path(runPath).is_absolute()) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); const auto outputDirectory = std::filesystem::absolute(outputPath).parent_path().lexically_normal(); std::vector artifacts; for (const auto index : order) { @@ -350,7 +355,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path image.Bytes[header.Offset + offset + 1] = 0x0B; } } - const auto relativeDirectory = "app0/" + image.SourcePath.parent_path().lexically_relative(root).generic_string(); + const auto relativeDirectory = appDirectory(image); const auto destination = outputDirectory / relativeDirectory; const auto target = destination / image.OutputName; if (target.lexically_normal() == std::filesystem::absolute(outputPath).lexically_normal()) throw Domain::RelinkerException("Guest output collides with the executable output"); @@ -365,9 +370,12 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (windows) output = Elfpatcher::GuestModuleWriter().WriteWindows(image, runtime); else { std::string guestRunPath = runPath; - if (runPath == "$ORIGIN" || runPath.starts_with("$ORIGIN/")) { - const auto relativeRoot = outputDirectory.lexically_relative(destination.lexically_normal()).generic_string(); - guestRunPath = "$ORIGIN/" + relativeRoot + runPath.substr(7); + const auto relativeRoot = outputDirectory.lexically_relative(destination.lexically_normal()).generic_string(); + for (std::size_t position = 0; (position = guestRunPath.find("$ORIGIN", position)) != std::string::npos;) { + position += 7; + if (position != guestRunPath.size() && guestRunPath[position] != '/' && guestRunPath[position] != ':') continue; + guestRunPath.insert(position, "/" + relativeRoot); + position += relativeRoot.size() + 1; } std::vector needed; for (const auto dependency : dependencies[index]) { diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index c6af1ec10..4b0e8560d 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -86,6 +86,8 @@ ElfHeader ElfReader::ReadHeader() const { } if (_fileBuffer[4] != 2 || _fileBuffer[5] != 1 || _fileBuffer[6] != 1) throw RelinkerException("Expected little-endian ELF64 version 1"); + if (_readU16At(0x12) != 62) + throw RelinkerException("Expected an x86-64 ELF"); ElfHeader header{}; header.Machine = _readU16At(0x12); diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index a486d3d64..d1ff243f4 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -219,6 +219,15 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf return relaOff + off; }; + auto isZeroSectionSymbol = [&](const std::uint32_t symIdx) { + const FileByteOffset symOff = dynSymTabOffset + static_cast(symIdx) * symEntSize; + if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < symEntSize) + throw RelinkerException("Symbol table entry out of bounds", symOff); + std::uint64_t value = 0; + std::memcpy(&value, raw.data() + symOff + 8, sizeof(value)); + return (raw[symOff + 4] & 0xf) == 3 && value == 0; + }; + auto extractRela = [&](const FileByteOffset relaOff, const ByteCount relaSize) { for (ByteCount off = 0; relaSize >= relaEntSize && off <= relaSize - relaEntSize; off += relaEntSize) { const FileByteOffset pos = relaEntryPos(relaOff, off); @@ -236,7 +245,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf continue; if (relType == 8) { - if (symIdx != 0) + if (symIdx != 0 && !isZeroSectionSymbol(symIdx)) throw RelinkerException("RELATIVE relocation has a nonzero symbol index", pos); continue; } @@ -405,9 +414,8 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::memcpy(&rOffset, raw.data() + pos, 8); std::memcpy(&rInfo, raw.data() + pos + 8, 8); std::memcpy(&rAddend, raw.data() + pos + 16, 8); - const std::uint32_t symIdx = static_cast(rInfo >> 32); const std::uint32_t relType = static_cast(rInfo & 0xffffffff); - if (symIdx == 0 && relType == R_X86_64_RELATIVE) + if (relType == R_X86_64_RELATIVE) appendRela(dynSection.RelaData, rOffset, static_cast(R_X86_64_RELATIVE), rAddend); } }; diff --git a/core/relinker/relinker/tests/CallSiteIndexTests.cpp b/core/relinker/relinker/tests/CallSiteIndexTests.cpp new file mode 100644 index 000000000..fceff1c26 --- /dev/null +++ b/core/relinker/relinker/tests/CallSiteIndexTests.cpp @@ -0,0 +1,37 @@ +#include +#include +#include +#include +#include + +static void Require(bool ok) { if (!ok) throw std::runtime_error("Incorrect call-site index"); } + +int main() { + try { + std::vector code; + const std::vector targets{0x2000, 0x1000, 0x2000, 0x3000, 0x1008}; + for (const auto target : targets) { + const auto offset = code.size(); + code.insert(code.end(), {0xFF, 0x15, 0, 0, 0, 0}); + const auto disp = static_cast(target - (0x100 + offset + 6)); + std::memcpy(code.data() + offset + 2, &disp, 4); + } + auto resolver = Relinker::MakeCallSiteResolver(); + Require(resolver->ResolveCallSites(code, 0x100, 0x2000, 8) == std::vector{0x100, 0x10C}); + Require(resolver->ResolveCallSites(code, 0x100, 0x1000, 8) == std::vector{0x106}); + Require(resolver->ResolveCallSites(code, 0x100, 0x1000, 9) == std::vector{0x106, 0x118}); + Require(resolver->ResolveCallSites(code, 0x100, 0x2000, 0).empty()); + Require(resolver->ResolveCallSites(code, 0x100, 0x4000, 8).empty()); + auto copy = code; + Require(resolver->ResolveCallSites(copy, 0x100, 0x2000, 8) == std::vector{0x100, 0x10C}); + Require(resolver->ResolveCallSites(copy, 0x200, 0x2100, 8) == std::vector{0x200, 0x20C}); + std::vector mov{0x48, 0x8B, 0x05, 0xF9, 0x0F, 0, 0}; + Require(resolver->ResolveCallSites(mov, 0x1000, 0x2000, 8) == std::vector{0x1000}); + Require(resolver->ResolveCallSites({}, 0, 0, 1).empty()); + bool rejected = false; + try { resolver->ResolveCallSites(code, 0, std::numeric_limits::max(), 1); } + catch (const std::exception&) { rejected = true; } + Require(rejected); + std::cout << "10 call-site index cases passed\n"; + } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } +} diff --git a/core/relinker/relinker/tests/CallSiteResolverTests.cpp b/core/relinker/relinker/tests/CallSiteResolverTests.cpp new file mode 100644 index 000000000..f780307eb --- /dev/null +++ b/core/relinker/relinker/tests/CallSiteResolverTests.cpp @@ -0,0 +1,41 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void require(bool condition, const std::string& name) { + if (!condition) throw std::runtime_error(name); +} + +void check(const std::string& name, std::initializer_list instruction, bool expected) { + auto resolver = Relinker::MakeCallSiteResolver(); + const std::vector text(instruction); + const auto sites = resolver->ResolveCallSites(text, 0x1000, 0x1100, 8); + require(sites == (expected ? std::vector{0x1000} : std::vector{}), name); + require(resolver->ResolveCallSites(text, 0x1000, 0x1200, 8).empty(), name + " unexpected address"); +} + +} + +int main() { + try { + check("indirect call", {0xff, 0x15, 0xfa, 0, 0, 0}, true); + check("indirect tail jump", {0xff, 0x25, 0xfa, 0, 0, 0}, true); + check("far indirect call", {0xff, 0x1d, 0xfa, 0, 0, 0}, true); + check("far indirect jump", {0xff, 0x2d, 0xfa, 0, 0, 0}, true); + check("RIP-relative function pointer load", {0x48, 0x8b, 0x05, 0xf9, 0, 0, 0}, true); + check("increment memory", {0xff, 0x05, 0xfa, 0, 0, 0}, false); + check("decrement memory", {0xff, 0x0d, 0xfa, 0, 0, 0}, false); + check("push memory", {0xff, 0x35, 0xfa, 0, 0, 0}, true); + std::cout << "Call-site resolver tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << "call_site_resolver_tests: " << error.what() << '\n'; + return 1; + } +} diff --git a/core/relinker/relinker/tests/ControlFlowGraphTests.cpp b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp new file mode 100644 index 000000000..5870e3c5e --- /dev/null +++ b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp @@ -0,0 +1,171 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace Relinker::UnusedNidFilter; +using Relinker::VirtualAddress; +using Bytes = std::vector; +constexpr VirtualAddress TextAddress = 0xec00001000; + +class Pointers : public IRelativeRelocationIndex { +public: + std::map Slots; + + std::optional TargetOfSlot(VirtualAddress slot) const override { + const auto position = Slots.find(slot); + if (position == Slots.end()) return std::nullopt; + return position->second; + } + + std::vector Targets() const override { + std::vector targets; + for (const auto& [slot, target] : Slots) targets.push_back(target); + return targets; + } +}; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +void RequireOffsets(const IControlFlowGraph& graph, std::initializer_list offsets) { + std::unordered_set expected; + for (const auto offset : offsets) expected.insert(TextAddress + offset); + Require(graph.ReachableVaddrs() == expected, "Unexpected reachable instruction set"); + for (const auto address : expected) Require(graph.IsReachable(address), "Missing reachable instruction"); + Require(!graph.IsReachable(TextAddress - 1), "Address before code is reachable"); +} + +void SetDisplacement(Bytes& text, std::size_t offset, std::size_t next, VirtualAddress target) { + const auto displacement = static_cast(target - (TextAddress + next)); + std::memcpy(text.data() + offset, &displacement, sizeof(displacement)); +} + +void CheckFallthrough() { + const Pointers pointers; + const Bytes text{0x90, 0x48, 0x89, 0xc0, 0xc3, 0x90}; + const auto graph = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*graph, {0, 1, 4}); + const auto end = BuildControlFlowGraph(Bytes{0x90, 0x90}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*end, {0, 1}); + const auto empty = BuildControlFlowGraph(Bytes{}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*empty, {}); + const auto outside = BuildControlFlowGraph(text, TextAddress, TextAddress - 1, {TextAddress + text.size()}, pointers); + RequireOffsets(*outside, {}); +} + +void CheckBranches() { + const Pointers pointers; + const Bytes text{0x75, 0x03, 0x90, 0xeb, 0x03, 0x90, 0xeb, 0xfa, 0xc3, 0x90}; + const auto graph = BuildControlFlowGraph(text, TextAddress, TextAddress, {TextAddress, TextAddress + 2}, pointers); + RequireOffsets(*graph, {0, 2, 3, 5, 6, 8}); + const auto self = BuildControlFlowGraph(Bytes{0xeb, 0xfe, 0x90}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*self, {0}); + const auto outside = BuildControlFlowGraph(Bytes{0xeb, 0x7f, 0x90}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*outside, {0}); + const auto trap = BuildControlFlowGraph(Bytes{0x0f, 0x0b, 0x90}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*trap, {0}); +} + +void CheckCalls() { + const Pointers pointers; + const Bytes text{0xe8, 0x03, 0, 0, 0, 0x90, 0xc3, 0x90, 0xe8, 0xf3, 0xff, 0xff, 0xff, 0xc3}; + const auto graph = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*graph, {0, 5, 6, 8, 13}); + const auto indirect = BuildControlFlowGraph(Bytes{0xff, 0xd0, 0x90, 0xff, 0xe0, 0xc3}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*indirect, {0, 2, 3}); +} + +void CheckRelocatedTransfers() { + constexpr auto slot = TextAddress + 0x1000; + Bytes text{0xff, 0x15, 0, 0, 0, 0, 0x90, 0xc3, 0xff, 0x25, 0, 0, 0, 0, 0x90, 0x90, 0xc3}; + SetDisplacement(text, 2, 6, slot); + SetDisplacement(text, 10, 14, slot + 8); + Pointers pointers; + pointers.Slots = {{slot, TextAddress + 8}, {slot + 8, TextAddress + 16}}; + const auto graph = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*graph, {0, 6, 7, 8, 16}); + const Pointers missing; + const auto unresolved = BuildControlFlowGraph(text, TextAddress, TextAddress, {TextAddress + 8}, missing); + RequireOffsets(*unresolved, {0, 6, 7, 8}); +} + +void CheckCodeAddresses() { + Bytes text(17, 0x90); + text[0] = 0x48; + text[1] = 0x8d; + text[2] = 0x05; + SetDisplacement(text, 3, 7, TextAddress + 16); + text[7] = text[16] = 0xc3; + const Pointers pointers; + const auto ordinary = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*ordinary, {0, 7}); + const auto addresses = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers, true); + RequireOffsets(*addresses, {0, 7, 16}); +} + +void CheckSpeculativeRoots() { + constexpr auto table = TextAddress + 0x1000; + Bytes text(32, 0xc3); + text[0] = 0x48; + text[1] = 0x8d; + text[2] = 0x05; + SetDisplacement(text, 3, 7, table); + text[16] = 0x90; + text[31] = 0x0f; + Bytes entries(12); + const std::int32_t targets[]{static_cast(TextAddress + 16 - table), + static_cast(TextAddress + 31 - table), 0}; + std::memcpy(entries.data(), targets, sizeof(targets)); + Pointers pointers; + pointers.Slots = {{table + 16, TextAddress + 24}, {table + 24, TextAddress + 31}}; + const std::vector data{{table, entries}}; + const auto graph = BuildAddressTakenControlFlowGraph(text, TextAddress, TextAddress, {}, pointers, data); + RequireOffsets(*graph, {0, 7, 16, 17, 24}); + bool threw = false; + try { + BuildAddressTakenControlFlowGraph(text, TextAddress, TextAddress, {TextAddress + 31}, pointers, data); + } catch (const Codegen::CodegenException&) { + threw = true; + } + Require(threw, "Malformed mandatory root was treated as speculative"); +} + +void CheckBorrowedReachability() { + const Pointers pointers; + const Bytes text{0x90, 0x90, 0xc3, 0x90, 0xc3}; + std::unordered_set reachable{TextAddress + 1}; + const auto first = BuildControlFlowGraph(std::span(text), TextAddress, TextAddress, {}, pointers, false, reachable); + RequireOffsets(*first, {0, 1}); + Require(&first->ReachableVaddrs() == &reachable, "Borrowed graph copied the result set"); + const auto second = BuildControlFlowGraph(std::span(text), TextAddress, TextAddress + 3, + {TextAddress + 3, TextAddress + 4}, pointers, false, reachable); + RequireOffsets(*second, {0, 1, 3, 4}); + RequireOffsets(*first, {0, 1, 3, 4}); +} + +} + +int main() { + try { + CheckFallthrough(); + CheckBranches(); + CheckCalls(); + CheckRelocatedTransfers(); + CheckCodeAddresses(); + CheckSpeculativeRoots(); + CheckBorrowedReachability(); + std::cout << "Control-flow graph tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << "control_flow_graph_tests: " << error.what() << '\n'; + return 1; + } +} diff --git a/core/relinker/relinker/tests/StrictNidFilterTests.cpp b/core/relinker/relinker/tests/StrictNidFilterTests.cpp index f62ec3f09..4d3f25755 100644 --- a/core/relinker/relinker/tests/StrictNidFilterTests.cpp +++ b/core/relinker/relinker/tests/StrictNidFilterTests.cpp @@ -576,6 +576,32 @@ void sceDynlibDataOffsets() { "Duplicate PT_SCE_DYNLIBDATA segments were accepted"); requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, duplicate, input.Text, input.TextVaddr); }, "Strict filter accepted duplicate PT_SCE_DYNLIBDATA segments"); + + auto sectionRelative = bytes; + write(sectionRelative, 208, 0x70); + write(sectionRelative, 216, 0x70); + write(sectionRelative, 0x330, 0x61000039); + write(sectionRelative, 0x338, 0x100); + write(sectionRelative, 0x340, 0x6100003B); + write(sectionRelative, 0x348, 24); + write(sectionRelative, 0x350, 0x6100003F); + write(sectionRelative, 0x358, 48); + write(sectionRelative, 0x61C, 3); + write(sectionRelative, 0x908, (std::uint64_t{1} << 32) | 8); + require(Relinker::MakeStrictUnusedNidFilter()->Filter(references, sectionRelative, input.Text, input.TextVaddr).size() == 1, + "Strict filter rejected a RELATIVE relocation against a zero-value section symbol"); + auto valuedSection = sectionRelative; + write(valuedSection, 0x620, 0x10); + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, valuedSection, input.Text, input.TextVaddr); }, + "Strict filter accepted a RELATIVE relocation against a section symbol with a value"); + auto untyped = sectionRelative; + write(untyped, 0x61C, 0); + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, untyped, input.Text, input.TextVaddr); }, + "Strict filter accepted a RELATIVE relocation against a non-section symbol"); + auto outside = sectionRelative; + write(outside, 0x908, (std::uint64_t{2} << 32) | 8); + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, outside, input.Text, input.TextVaddr); }, + "Strict filter accepted a RELATIVE relocation symbol outside the symbol table"); } } diff --git a/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp b/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp new file mode 100644 index 000000000..0210073af --- /dev/null +++ b/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp @@ -0,0 +1,47 @@ +#include +#include +#include +#include +#include + +static void Require(bool ok) { if (!ok) throw std::runtime_error("Unwind record validation failed"); } + +static std::vector Fixture() { + std::vector bytes(128, 0); + bytes[0] = 1; bytes[1] = 0; bytes[2] = 3; bytes[3] = 0; + Io::WriteU64(bytes, 4, 0x1040); Io::WriteU32(bytes, 12, 1); + Io::WriteU64(bytes, 16, 0x2000); Io::WriteU64(bytes, 24, 0x1060); + Io::WriteU32(bytes, 64, 9); bytes[72] = 1; bytes[73] = 0; bytes[74] = 1; bytes[75] = 0x78; bytes[76] = 16; + Io::WriteU32(bytes, 96, 20); Io::WriteU32(bytes, 100, 36); + Io::WriteU64(bytes, 104, 0x2000); Io::WriteU64(bytes, 112, 0x10); + return bytes; +} + +int main() { + try { + Domain::ProgramHeader load{}; load.Type = 1; load.MappedAddress = 0x1000; load.FileSize = 128; + auto eh = load; eh.Type = 0x6474E550; eh.FileSize = 32; + const auto regions = Relinker::UnusedNidFilter::ReadExceptionFunctions(Fixture(), {load, eh}, {}, {}); + Require(regions.size() == 1 && regions[0].Begin == 0x2000 && regions[0].End == 0x2010); + auto second = load; second.MappedAddress = 0x1048; second.Offset = 72; second.FileSize = 56; + auto first = load; first.FileSize = 72; + Require(Relinker::UnusedNidFilter::ReadExceptionFunctions(Fixture(), {first, second, eh}, {}, {}).size() == 1); + for (int kind = 0; kind < 7; ++kind) { + auto bytes = Fixture(); auto segment = load; + switch (kind) { + case 0: Io::WriteU32(bytes, 64, 0x1000); break; + case 1: Io::WriteU32(bytes, 96, 0x1000); break; + case 2: Io::WriteU32(bytes, 64, 100); break; + case 3: Io::WriteU32(bytes, 96, 32); bytes.resize(120); break; + case 4: segment.Offset = std::numeric_limits::max() - 4095; segment.MappedAddress = 0; segment.FileSize = 0x1100; break; + case 5: segment.FileSize = 119; break; + case 6: Io::WriteU32(bytes, 64, 8); break; + } + bool rejected = false; + try { Relinker::UnusedNidFilter::ReadExceptionFunctions(bytes, {segment, eh}, {}, {}); } + catch (const std::exception&) { rejected = true; } + Require(rejected); + } + std::cout << "9 unwind record cases passed\n"; + } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } +} diff --git a/core/relinker/relinker/tests/test_elf_header.py b/core/relinker/relinker/tests/test_elf_header.py index 553665bbd..d31486831 100644 --- a/core/relinker/relinker/tests/test_elf_header.py +++ b/core/relinker/relinker/tests/test_elf_header.py @@ -8,17 +8,21 @@ from test_guest_intel_trampolines import main_fixture def identification_cases(relinker, directory): image = main_fixture() - cases = [("sysv", image, False)] + cases = [("sysv", image, None)] freebsd = bytearray(image) freebsd[7] = 9 - cases.append(("freebsd", freebsd, False)) + cases.append(("freebsd", freebsd, None)) for field, values in ((4, (0, 1, 255)), (5, (0, 2, 255)), (6, (0, 2, 255))): for value in values: data = bytearray(image) data[field] = value - cases.append((f"ident-{field}-{value}", data, True)) + cases.append((f"ident-{field}-{value}", data, "Expected little-endian ELF64 version 1")) + for value in (0, 3, 183): + data = bytearray(image) + data[18:20] = value.to_bytes(2, "little") + cases.append((f"machine-{value}", data, "Expected an x86-64 ELF")) for mode, flags in enumerate(([], ["--windows"], ["--to-intel"], ["--windows", "--to-intel"])): - for name, data, invalid in cases: + for name, data, error in cases: source = Path(directory) / f"{mode}-{name}.elf" output = source.with_suffix(".out") registry = output.with_name(output.stem + ".registry.json") @@ -26,8 +30,8 @@ def identification_cases(relinker, directory): result = subprocess.run([str(relinker), "--skip-sce-module", "--registry", *flags, str(source), str(output)], capture_output=True, text=True, timeout=20) assert source.read_bytes() == data, (mode, name, "source changed") - if invalid: - assert result.returncode == 2 and "Expected little-endian ELF64 version 1" in result.stderr, (mode, name, result) + if error: + assert result.returncode == 2 and error in result.stderr, (mode, name, result) assert not output.exists() and not registry.exists(), (mode, name, "output written for unsupported ELF identification") else: assert result.returncode == 0 and registry.is_file(), (mode, name, result) @@ -39,7 +43,7 @@ def main(): cases = [ ("tiny", bytes(10), "File too small for ELF header"), ("truncated", b"\x7fELF\x02\x01\x01" + bytes(25), "File too small for ELF header"), - ("header-only", b"\x7fELF\x02\x01\x01" + bytes(57), "No PT_DYNAMIC segment found"), + ("header-only", b"\x7fELF\x02\x01\x01" + bytes(11) + b"\x3e" + bytes(45), "No PT_DYNAMIC segment found"), ("bad-magic", bytes(64), "Invalid ELF magic number"), ] with tempfile.TemporaryDirectory(prefix="anyps5-elf-header-") as directory: diff --git a/core/relinker/relinker/tests/test_guest_module_dir.py b/core/relinker/relinker/tests/test_guest_module_dir.py index 4ab73d8af..b333399ac 100644 --- a/core/relinker/relinker/tests/test_guest_module_dir.py +++ b/core/relinker/relinker/tests/test_guest_module_dir.py @@ -91,6 +91,35 @@ def main(): else: assert "$ORIGIN/app0/Media/Modules/needed.prx.guest.prx" in needed_libraries(output.read_bytes()), needed_libraries(output.read_bytes()) + case = work / f"{windows}-root" + case.mkdir() + (case / "root.prx").write_bytes(module_with_symbol(True)) + (case / "readme.txt").write_text("not ELF") + result, output = convert(case, windows, ["--module-dir", "."]) + assert result.returncode == 0, (result.stdout, result.stderr) + artifact = case / "app0" / "root.prx.guest.prx" + assert list((case / "app0").rglob("*.guest.prx")) == [artifact], list(case.rglob("*.guest.prx")) + assert artifact.read_bytes().startswith(b"MZ" if windows else b"\x7fELF"), artifact + assert " root.prx.guest.prx\n" in result.stdout, result.stdout + if windows: + assert "__aps5_guest_initialize" in pe_export_names(artifact.read_bytes()) + else: + assert not needed_libraries(output.read_bytes()), needed_libraries(output.read_bytes()) + if windows and os.name == "nt": + run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + assert run.returncode == 42, (run.returncode, run.stdout, run.stderr) + + case = work / f"{windows}-root-needed" + case.mkdir() + (case / "needed.prx").write_bytes(module_with_symbol(True)) + result, output = convert(case, windows, ["--module-dir", "."], executable_with_needed()) + assert result.returncode == 0, (result.stdout, result.stderr) + needed = case / "app0" / "needed.prx.guest.prx" + if windows: + assert "__aps5_guest_initialize" not in pe_export_names(needed.read_bytes()) + else: + assert "$ORIGIN/app0/needed.prx.guest.prx" in needed_libraries(output.read_bytes()), needed_libraries(output.read_bytes()) + case = work / f"{windows}-plugins" for rejected, message in ((str(plugins), "inside the guest module parent directory"), ("../outside", "inside the guest module parent directory"), diff --git a/core/relinker/relinker/tests/test_guest_module_identity.py b/core/relinker/relinker/tests/test_guest_module_identity.py index 211b7a6cc..2a03efd3f 100644 --- a/core/relinker/relinker/tests/test_guest_module_identity.py +++ b/core/relinker/relinker/tests/test_guest_module_identity.py @@ -147,7 +147,7 @@ def main(): for windows in (True, False): for tag in (0x6100000d, 0x61000043): - for suffix in ('.suprx', '.prx', '.sprx'): + for suffix in ('.suprx', '.prx', '.sprx', '.debug_prx'): request = 'libGuest' + suffix result, output = convert(f'{windows}-{tag}-{suffix}', windows, request, providers={'renamed.prx': declared_provider(22, ('libGuest',), tag=tag)}, guest_request=request) @@ -161,6 +161,20 @@ def main(): child = output.parent / 'app0/sce_module/consumer.prx.guest.prx' assert needed_libraries(child.read_bytes()) == ['$ORIGIN/renamed.prx.guest.prx'] + for suffix in ('.suprx', '.prx', '.sprx', '.debug_prx'): + providers = {'renamed.prx': declared_provider(22, ('libGuest',)), + 'other.prx': declared_provider(33, ('libOther',)), + 'own.prx': declared_provider(22, ('libOwn',), symbol='own'), + 'consumer.prx': consumer('libGuest' + suffix, module_name='libGuest')} + result, output = convert(f'{windows}-shared-{suffix}', windows, 'libOwn.prx', module_name='libOwn', + symbol='own#A#B', providers=providers) + assert result.returncode == 0, result.stderr + if windows: + run(output) + else: + child = output.parent / 'app0/sce_module/consumer.prx.guest.prx' + assert needed_libraries(child.read_bytes()) == ['$ORIGIN/renamed.prx.guest.prx'] + providers = {'one.prx': declared_provider(22, ('libGuest',)), 'two.prx': declared_provider(33, ('libGuest',), symbol='other')} result, output = convert(f'{windows}-ambiguous', windows, 'libGuest.suprx', providers=providers) diff --git a/core/relinker/relinker/tests/test_guest_needed_modules.py b/core/relinker/relinker/tests/test_guest_needed_modules.py index adc9b97da..d15b96ac9 100644 --- a/core/relinker/relinker/tests/test_guest_needed_modules.py +++ b/core/relinker/relinker/tests/test_guest_needed_modules.py @@ -142,6 +142,24 @@ def main(): needed = needed_libraries(output.read_bytes()) assert needed == [f"$ORIGIN/app0/{directory}/needed.prx.guest.prx"], needed + for requested, physical in ((b"needed.suprx", "needed.prx"), + (b"needed.prx", "needed.suprx"), + (b"needed.sprx", "needed.suprx"), + (b"needed.debug_prx", "needed.suprx")): + case = work / f"{windows}-suprx-alias-{requested.decode()}-{physical}" + (case / "sce_module").mkdir(parents=True) + (case / "sce_module" / physical).write_bytes(module_with_symbol(True)) + result, output = convert(case, windows, requested) + assert result.returncode == 0, (result.stdout, result.stderr) + artifact = case / "app0" / "sce_module" / (physical + ".guest.prx") + assert artifact.is_file(), (requested, physical, result.stdout) + if not windows: + needed = needed_libraries(output.read_bytes()) + assert needed == [f"$ORIGIN/app0/sce_module/{physical}.guest.prx"], needed + elif os.name == "nt": + run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + assert run.returncode == 42, (run.returncode, run.stdout, run.stderr) + if windows: case = work / "debug-name-case" (case / "prx").mkdir(parents=True) diff --git a/core/relinker/relinker/tests/test_guest_none_relocations.py b/core/relinker/relinker/tests/test_guest_none_relocations.py new file mode 100644 index 000000000..f11f79669 --- /dev/null +++ b/core/relinker/relinker/tests/test_guest_none_relocations.py @@ -0,0 +1,189 @@ +from pathlib import Path +import platform +import struct +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import main_fixture, pe_sections +from test_guest_module_directories import module_with_symbol +from test_guest_symbol_names import dynamic_tags, file_offset +from test_macos_output import MachO + + +TARGET = 0x2500 +FUNCTION = 0x1000 +UNCHANGED = 0x8877665544332211 +NOOP = (0, 0, 0) +IGNORED = (0xffffffffffffffff, 0xffffffff00000000, -1) +OVERLAP = (TARGET + 1, 0xffffffff00000000, -1) +RELATIVE = (TARGET, 8, FUNCTION) +JUMP_SLOT = (TARGET, (1 << 32) | 7, 0) + + +def module(rela=(), plt=(), sce=False, lifecycle=False, initial=UNCHANGED): + image = module_with_symbol(True) + image.extend(bytes(0xE00 - len(image))) + struct.pack_into("= 0, (tags, strings) actual = strings[offset:end].decode() configured = run_path if run_path is not None else "$ORIGIN/libs" - expected = Path(configured.replace("$ORIGIN", str(output.parent))).resolve() - resolved = Path(actual.replace("$ORIGIN", str(artifact.parent))).resolve() + configured_paths = configured.split(":") if label.startswith("list") else [configured] + actual_paths = actual.split(":") if label.startswith("list") else [actual] + expected = [Path(path.replace("$ORIGIN", str(output.parent))).resolve() for path in configured_paths] + resolved = [Path(path.replace("$ORIGIN", str(artifact.parent))).resolve() for path in actual_paths] if resolved != expected or (label == "absolute" and actual != run_path): failures.append((str(relative), label, actual, str(resolved), str(expected))) checked += 1 diff --git a/core/relinker/relinker/tests/test_input_preservation.py b/core/relinker/relinker/tests/test_input_preservation.py index 2114abbe3..3ec4114b3 100644 --- a/core/relinker/relinker/tests/test_input_preservation.py +++ b/core/relinker/relinker/tests/test_input_preservation.py @@ -8,6 +8,72 @@ from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture, main_fixture from test_linux_desktop_entry_output import png +def check_diagnostic_inputs(relinker, work): + filename = "windows-diagnostics-imports.txt" + for bundled in (False, True): + for alias in ("direct", "hardlink", "symlink"): + for registry in (False, True): + case = work / f"diagnostics-{bundled}-{alias}-{registry}" + modules = case / "sce_module" + modules.mkdir(parents=True) + diagnostics = (modules if bundled and alias == "direct" else case) / filename + source = diagnostics if not bundled and alias == "direct" else case / "input.elf" + original = bytes(main_fixture()) + source.write_bytes(original) + module = diagnostics if bundled and alias == "direct" else modules / "sample.prx" + module_original = bytes(guest_fixture(PLAIN_SITE)) + module.write_bytes(module_original) + protected = module if bundled else source + if alias == "hardlink": + os.link(protected, diagnostics) + elif alias == "symlink": + try: + diagnostics.symlink_to(protected) + except OSError as error: + if os.name != "nt" or error.winerror != 1314: + raise + print("Diagnostic symbolic-link case skipped: Windows symlink privilege unavailable") + continue + output = case / "output.exe" + previous_output = b"previous executable" + output.write_bytes(previous_output) + registry_path = case / "output.registry.json" + previous_registry = b"previous registry" + registry_path.write_bytes(previous_registry) + options = [] if bundled else ["--skip-sce-module"] + if registry: + options.append("--registry") + result = subprocess.run([str(relinker), "--windows", "--windows-diagnostics", *options, + str(source), str(output)], cwd=diagnostics.parent, + capture_output=True, text=True, timeout=20) + owner = "an input module" if bundled else "the input executable" + assert result.returncode == 2 and f"Windows import diagnostics output would overwrite {owner}" in result.stderr, (result.stdout, result.stderr) + assert str(diagnostics) in result.stderr, result.stderr + assert source.read_bytes() == original and module.read_bytes() == module_original + assert diagnostics.read_bytes() == (module_original if bundled else original) + assert output.read_bytes() == previous_output and registry_path.read_bytes() == previous_registry + assert not (case / "app0").exists() + + for enabled in (False, True): + for existing in (False, True): + case = work / f"diagnostics-valid-{enabled}-{existing}" + case.mkdir() + diagnostics = case / filename + source = case / "input.elf" if enabled else diagnostics + original = bytes(main_fixture()) + source.write_bytes(original) + if enabled and existing: + diagnostics.write_bytes(b"previous diagnostics") + output = case / "output.exe" + options = ["--windows-diagnostics"] if enabled else [] + result = subprocess.run([str(relinker), "--windows", "--skip-sce-module", *options, + str(source), str(output)], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 0 and output.read_bytes().startswith(b"MZ"), (result.stdout, result.stderr) + assert source.read_bytes() == original + assert diagnostics.read_bytes() == (b"" if enabled else original) + + def check_registry_inputs(relinker, work, windows): options = ["--windows"] if windows else [] for module_registry in (False, True): @@ -53,6 +119,29 @@ def check_registry_inputs(relinker, work, windows): assert not (case / "app0").exists() +def check_registry_after_failed_patch(relinker, work): + case = work / "registry-failed-patch" + (case / "sce_module").mkdir(parents=True) + (case / "sce_module" / "sample.prx").write_bytes(guest_fixture(PLAIN_SITE)) + (case / "sce_sys").mkdir() + (case / "sce_sys" / "icon0.png").write_bytes(b"\x89PNG\r\n\x1a\n" + bytes(40)) + source = case / "input.elf" + source.write_bytes(main_fixture()) + output = case / "output.exe" + previous_output = b"previous executable" + output.write_bytes(previous_output) + registries = [case / "output.registry.json", case / "output.sample.prx.guest.prx.registry.json"] + previous_registry = b"previous registry" + for registry in registries: + registry.write_bytes(previous_registry) + result = subprocess.run([str(relinker), "--windows", "--registry", str(source), str(output)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Invalid Windows icon PNG" in result.stderr, (result.stdout, result.stderr) + assert output.read_bytes() == previous_output + assert all(registry.read_bytes() == previous_registry for registry in registries) + assert not (case / "app0").exists() + + def check_desktop_inputs(relinker, work): for desktop in (False, True): for bundled in (False, True): @@ -112,7 +201,9 @@ def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-input-preservation-") as directory: work = Path(directory) + check_diagnostic_inputs(relinker, work) check_desktop_inputs(relinker, work) + check_registry_after_failed_patch(relinker, work) for windows in (False, True): options = ["--windows"] if windows else [] check_registry_inputs(relinker, work, windows) diff --git a/core/relinker/relinker/tests/test_none_relocations.py b/core/relinker/relinker/tests/test_none_relocations.py index 5a0d06fc3..cbf3d4cb3 100644 --- a/core/relinker/relinker/tests/test_none_relocations.py +++ b/core/relinker/relinker/tests/test_none_relocations.py @@ -79,13 +79,37 @@ def main(): if target == "windows" and os.name == "nt" and not has_import: run = subprocess.run([str(output)], capture_output=True, timeout=20) assert run.returncode == 42, (name, level, run.returncode, run.stderr) - source = work / "invalid-relative.elf" - source.write_bytes(image_with_relocations([noop, (0x300, (1 << 32) | 8, 0x210)], False, False)) - output = work / "invalid-relative.exe" - result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", str(source), str(output)], - capture_output=True, text=True, timeout=20) - assert result.returncode == 2 and "RELATIVE relocation has a nonzero symbol index" in result.stderr - assert not output.exists() + for sce in (False, True): + image = image_with_relocations([noop, (0x300, (1 << 32) | 8, 0x210)], sce, False) + struct.pack_into("= 0, (name, stub.hex()) + displacement = struct.unpack_from("= 15u) info.dualSourceBlend = reader.ReadBool(); if (version >= 16u) info.alphaToCoverage = reader.ReadBool(); + if (version >= 17u) info.stencilRefExportEnable = reader.ReadBool(); if (version < 5u) { const auto input = [](PixelInput value, bool present) { return present ? PixelInputBit(value) : 0u; }; info.inputAddr = input(PixelInput::PerspectiveSample, info.hasPerspectiveCenterVgpr && inputAddrOrCenterVgpr == 2u) | input(PixelInput::PerspectiveCenter, info.hasPerspectiveCenterVgpr) | @@ -734,7 +736,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(16u); + writer.WriteU32(17u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -754,6 +756,8 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const if (request.context.compute.has_value()) writer.WriteU32(request.context.compute->scratchDwords); writer.WriteBool(request.target.narrowSubgroupClock); writer.WriteBool(request.target.float64DenormPreserve); + writer.WriteBool(request.target.registrationModules); + writer.WriteU32(request.target.registrationWordLimit); return base64Encode(buffer); } @@ -762,7 +766,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 16u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 17u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); @@ -783,6 +787,8 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const if (version >= 10u && result.request.context.compute.has_value()) result.request.context.compute->scratchDwords = reader.ReadU32(); if (version >= 12u) result.request.target.narrowSubgroupClock = reader.ReadBool(); if (version >= 16u) result.request.target.float64DenormPreserve = reader.ReadBool(); + if (version >= 17u) result.request.target.registrationModules = reader.ReadBool(); + if (version >= 17u) result.request.target.registrationWordLimit = reader.ReadU32(); return result; } diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index 36a72be9c..a5134b561 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -1734,7 +1734,9 @@ void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph, bool routeL const bool latchInInnerLoop = routeSharedExits && latches.size() == 1u && std::any_of(graph.naturalLoops.begin(), graph.naturalLoops.end(), [&](const NaturalLoop& inner) { return inner.headerBlock != loop.headerBlock && contains(loop.blocks, inner.headerBlock) && contains(inner.blocks, latches.front()); }); - if (latches.size() <= 1u && !latchInInnerLoop) { + const auto& header = graph.FindBlock(loop.headerBlock); + const bool semanticSelfLoop = latches.size() == 1u && latches.front() == header.id && header.instructionBegin != header.instructionEnd; + if (latches.size() <= 1u && !latchInInnerLoop && !semanticSelfLoop) { continue; } diff --git a/core/shader/recompiler/Deserialize.cpp b/core/shader/recompiler/Deserialize.cpp index b4e12b4b5..6f1b3a0ca 100644 --- a/core/shader/recompiler/Deserialize.cpp +++ b/core/shader/recompiler/Deserialize.cpp @@ -226,7 +226,7 @@ int main(int argc, char** argv) { std::cout << "ShaderInfoCollector::Collect OK\n"; BindingAllocator bindingAllocator; - const auto bindings = bindingAllocator.Allocate(program, request.layout); + const auto bindings = bindingAllocator.Allocate(program, request.layout, request.target.registrationModules); std::cout << "BindingAllocator::Allocate OK, bindings=" << bindings.bindings.size() << "\n"; std::cout << "ALL STAGES OK\n"; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp index c1a9500b7..976a74f8e 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp @@ -43,6 +43,7 @@ struct IrBindingLayout { std::uint32_t memoryOffsetCount = 0; bool dispatchThreadLimit = false; std::uint32_t runtimeImageCount = 0; + std::uint32_t runtimeStateDwords = 0; std::vector userDataRegisters; std::vector descriptors; @@ -52,9 +53,12 @@ struct IrBindingLayout { [[nodiscard]] std::uint32_t ImageMetadataDword() const { return DispatchThreadLimitDword() + (dispatchThreadLimit ? 3u : 0u); } - [[nodiscard]] std::uint32_t ShaderDataDwords() const { + [[nodiscard]] std::uint32_t RuntimeStateDword() const { return ImageMetadataDword() + runtimeImageCount * (sizeof(RuntimeAbi::ResourceMetadata) / sizeof(std::uint32_t)); } + [[nodiscard]] std::uint32_t ShaderDataDwords() const { + return RuntimeStateDword() + runtimeStateDwords; + } [[nodiscard]] bool UsesPushData() const { return pushDataStartDword != PushData::NoStart; } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp index 4046804ef..2c5b7c379 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp @@ -51,6 +51,7 @@ enum class StageOutputKind { Mrt, Depth, SampleMask, + StencilRef, PointSize, ClipDistance, CullDistance, diff --git a/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp b/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp index 120de5358..0944d2353 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp @@ -23,7 +23,7 @@ struct BindingAllocationResult : CompiledBindingLayout { class BindingAllocator { public: - [[nodiscard]] BindingAllocationResult Allocate(IrProgram& program, const BindingLayout& layout) const; + [[nodiscard]] BindingAllocationResult Allocate(IrProgram& program, const BindingLayout& layout, bool registrationModules = false) const; [[nodiscard]] const IrDescriptorBinding& FindBinding(const IrBindingLayout& layout, DescriptorBindingKind kind) const; }; diff --git a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp index 01ece57e4..84341598a 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp @@ -14,6 +14,7 @@ struct PreparedDescriptorBinding { std::vector resources; std::vector identityImageSwizzle; std::vector samplerFilterElements; + std::vector inactive; std::size_t writtenBuffers = 0; std::size_t readOnlyBuffers = 0; }; @@ -28,12 +29,13 @@ struct DescriptorBindingPlan { std::vector bindings; std::vector imageMetadata; std::vector specialization; + std::vector runtimeState; bool storageData = false; }; class DescriptorBindingBuilder { public: - DescriptorBindingPlan Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings = {}) const; + DescriptorBindingPlan Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings = {}, bool registrationModules = false, bool fragmentDescriptorSet = false) const; DescriptorBindingPlan Select(const DescriptorBindingPlan& plan, std::span bindings) const; void Populate(BindingAllocationResult& allocation, const CompiledBindingLayout& compiled, const DescriptorBindingPlan& plan, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) const; void Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot, const std::array& partialThreads, std::span exportMappings = {}) const; diff --git a/core/shader/recompiler/Optimization/include/Optimization/MaskedSelectEliminator.hpp b/core/shader/recompiler/Optimization/include/Optimization/MaskedSelectEliminator.hpp index fad7eed9c..2e0b5bd5b 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/MaskedSelectEliminator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/MaskedSelectEliminator.hpp @@ -8,6 +8,7 @@ namespace ShaderRecompiler { struct MaskedSelectEliminationStats { std::uint32_t removedSelects = 0; + std::uint64_t steps = 0; }; class MaskedSelectEliminator { diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp index f23d1b8f6..909f3cea1 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp @@ -70,6 +70,7 @@ private: void BuildPreparedShaderKey(const RecompileRequest& request, std::vector& key); [[nodiscard]] bool MatchesPreparedShader(const RecompileRequest& request, const SourceHandle& handle, std::span key); [[nodiscard]] const CompiledShaderArtifact& GetPreparedArtifact(const SourceHandle& handle); +[[nodiscard]] std::shared_ptr RegistrationResult(const RecompileRequest& request, const SourceHandle& handle); [[nodiscard]] std::shared_ptr MaterializeShader(const RecompileRequest& request, const ResourceCapture& capture, const SourceHandle& handle); [[nodiscard]] std::shared_ptr ResolveSource(const RecompileRequest& request); [[nodiscard]] std::shared_ptr CaptureResources(const RecompileRequest& request, const SrtRuntime& runtime, const SourceHandle& handle); diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 32300b030..ba65e3242 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -167,6 +167,7 @@ struct ShaderPixelInputInfo { bool psPixelKillEnable = false; bool psDepthExportEnable = false; bool psSampleMaskExportEnable = false; + bool psStencilRefExportEnable = false; bool psSampleShading = false; bool psEarlyZ = false; bool psExecuteOnNoop = false; diff --git a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp index f99616a96..ceaca33ff 100644 --- a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp +++ b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp @@ -68,7 +68,7 @@ bool usesGds(const IrProgram& program) { } -BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const BindingLayout& layout) const { +BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const BindingLayout& layout, bool registrationModules) const { IrProgramMetadata& metadata = program.Metadata(); if (!metadata.shaderInfoComplete || metadata.bindingLayoutComplete) { fail(metadata.shaderInfoComplete ? "shader binding layout failed: binding layout already allocated" @@ -102,7 +102,17 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin next.dispatchThreadLimit = info.dispatchThreadLimit; if (info.buffers.size() > RuntimeAbi::BufferCapacity || info.images.size() > RuntimeAbi::ImageCapacity || info.samplers.size() > RuntimeAbi::SamplerHeapCapacity) fail("shader binding layout exceeds runtime metadata capacity"); if (std::ranges::any_of(next.userDataRegisters, [](auto reg) { return reg >= RuntimeAbi::UserDataCapacity; })) fail("shader user data exceeds runtime ABI capacity"); - if (std::ranges::any_of(info.images, [](const auto& image) { return image.indirectRoot != ImageResource::NoIndirectImage; })) next.runtimeImageCount = static_cast(info.images.size()); + if (registrationModules || std::ranges::any_of(info.images, [](const auto& image) { return image.indirectRoot != ImageResource::NoIndirectImage; })) next.runtimeImageCount = static_cast(info.images.size()); + if (registrationModules && program.Resources().stage == IrShaderStage::Pixel) next.runtimeStateDwords = RuntimeAbi::StateDwords; + if (registrationModules && program.Resources().stage != IrShaderStage::Pixel) { + std::uint32_t attributes = 0u; + for (const auto& block : program.Blocks()) { + for (const IrValue* inst : block->Instructions()) { + if (inst->Opcode() == IrOpcode::GetAttribute && inst->Flags() == 1u) attributes = std::max(attributes, inst->Argument(0)->Resolve()->ImmediateU32() + 1u); + } + } + next.runtimeStateDwords = (attributes + RuntimeAbi::VertexSelectorsPerDword - 1u) / RuntimeAbi::VertexSelectorsPerDword; + } const auto pushSize = layout.pushConstantSizeBytes / 4u; next.pushDataStartDword = next.runtimeImageCount == 0u && next.ShaderDataDwords() != 0u && next.ShaderDataDwords() <= pushSize ? layout.pushConstantOffsetBytes / 4u : PushData::NoStart; diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index 0020b87cb..b70fbab7b 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -1,5 +1,6 @@ #include "Optimization/ConstantFolder.hpp" #include "IntermediateRepresentation/IrBuilder.hpp" +#include "IntermediateRepresentation/IrMetadata/StageIO.hpp" #include #include @@ -76,6 +77,83 @@ bool foldConstantBitTest(IrBuilder& builder, IrValue& inst) { return true; } +bool isLaneIndex(const IrValue& value) { + return value.Opcode() == IrOpcode::LaneId; +} + +bool isImmediateU32(const IrValue& value, std::uint32_t immediate) { + return isImmediate(value, IrType::U32) && value.ImmediateU32() == immediate; +} + +bool runsAllStartingLanes(const IrProgram& program, const IrBlock* target) { + if (program.BlockOrder().empty()) return false; + const IrBlock* block = program.BlockOrder().front(); + for (std::size_t steps = 0; block != nullptr && steps < program.BlockOrder().size(); ++steps) { + if (block == target) return true; + if (block->Successors().size() != 1u) return false; + block = block->Successors().front(); + if (block->Predecessors().size() != 1u) return false; + } + return false; +} + +bool holdsStartingLanes(const IrProgram& program, const IrValue& word, std::uint32_t index, std::uint32_t depth = 0) { + if (depth > 8u) return false; + if (word.Opcode() == IrOpcode::BitwiseAnd32) { + return holdsStartingLanes(program, resolveArg(word, 0), index, depth + 1u) && holdsStartingLanes(program, resolveArg(word, 1), index, depth + 1u); + } + if (word.Opcode() == IrOpcode::BitwiseOr32) { + return holdsStartingLanes(program, resolveArg(word, 0), index, depth + 1u) || holdsStartingLanes(program, resolveArg(word, 1), index, depth + 1u); + } + if (word.Opcode() != IrOpcode::CompositeExtractU32x4 || !isImmediateU32(resolveArg(word, 1), index)) return false; + const auto& ballot = resolveArg(word, 0); + if (ballot.Opcode() != IrOpcode::Ballot || !runsAllStartingLanes(program, ballot.Parent())) return false; + const auto& condition = resolveArg(ballot, 0); + if (isImmediate(condition, IrType::U1)) return condition.ImmediateBool(); + if (condition.Opcode() != IrOpcode::IEqual32) return false; + const auto helper = [](const IrValue& value) { + if (value.Opcode() != IrOpcode::GetBuiltin) return false; + const auto& kind = resolveArg(value, 0); + return isImmediate(kind, IrType::U32) && static_cast(kind.ImmediateU32()) == StageInputKind::HelperInvocation; + }; + const auto& lhs = resolveArg(condition, 0); + const auto& rhs = resolveArg(condition, 1); + return (helper(lhs) && isImmediateU32(rhs, 0u)) || (helper(rhs) && isImmediateU32(lhs, 0u)); +} + +bool foldStartingQuadBitTest(const IrProgram& program, IrBuilder& builder, IrValue& inst) { + if (program.Resources().stage != IrShaderStage::Pixel) return false; + auto& left = resolveArg(inst, 0); + auto& right = resolveArg(inst, 1); + if (!isImmediateU32(right, 0u) || left.Opcode() != IrOpcode::BitwiseAnd32) return false; + IrValue* shifted = isImmediateU32(resolveArg(left, 1), 1u) ? &resolveArg(left, 0) : isImmediateU32(resolveArg(left, 0), 1u) ? &resolveArg(left, 1) : nullptr; + if (shifted == nullptr || shifted->Opcode() != IrOpcode::ShiftRightLogical32) return false; + const auto& amount = resolveArg(*shifted, 1); + if (amount.Opcode() != IrOpcode::BitwiseAnd32) return false; + const bool laneAmount = (isLaneIndex(resolveArg(amount, 0)) && isImmediateU32(resolveArg(amount, 1), 31u)) || (isLaneIndex(resolveArg(amount, 1)) && isImmediateU32(resolveArg(amount, 0), 31u)); + if (!laneAmount) return false; + const auto& word = resolveArg(*shifted, 0); + const IrValue* low = &word; + const IrValue* high = nullptr; + if (word.Opcode() == IrOpcode::SelectU32) { + const auto& half = resolveArg(word, 0); + if (half.Opcode() != IrOpcode::ULessThan32 || !isLaneIndex(resolveArg(half, 0)) || !isImmediateU32(resolveArg(half, 1), 32u)) return false; + low = &resolveArg(word, 1); + high = &resolveArg(word, 2); + } else if (program.WaveSize() != 32u) { + return false; + } + if (low->Opcode() != IrOpcode::CompositeExtractU64 || !isImmediateU32(resolveArg(*low, 1), 0u)) return false; + const auto& wqm = resolveArg(*low, 0); + if (high != nullptr && (high->Opcode() != IrOpcode::CompositeExtractU64 || &resolveArg(*high, 0) != &wqm || !isImmediateU32(resolveArg(*high, 1), 1u))) return false; + if (wqm.Opcode() != IrOpcode::WqmU64) return false; + const auto& lanes = resolveArg(wqm, 0); + if (lanes.Opcode() != IrOpcode::CompositeConstructU64 || !holdsStartingLanes(program, resolveArg(lanes, 0), 0u)) return false; + if (high != nullptr && !holdsStartingLanes(program, resolveArg(lanes, 1), 1u)) return false; + replaceWith(inst, builder.ConstantBool(true)); + return true; +} + template bool foldU32Compare(IrBuilder& builder, IrValue& inst, TFunction function) { auto& lhs = resolveArg(inst, 0); auto& rhs = resolveArg(inst, 1); @@ -144,7 +222,7 @@ bool foldPhi(IrValue& inst) { } if (same == nullptr) { same = value; - } else if (!(*same == *value)) { + } else if (same != value && !(same->HasImmediate() && value->HasImmediate() && *same == *value)) { return false; } } @@ -652,7 +730,7 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { return foldU32(builder, value, [](std::uint32_t a, std::uint32_t b) { return std::max(a, b); }); } case IrOpcode::IEqual32: return foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a == b; }); - case IrOpcode::INotEqual32: return foldConstantBitTest(builder, value) || foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a != b; }); + case IrOpcode::INotEqual32: return foldConstantBitTest(builder, value) || foldStartingQuadBitTest(program, builder, value) || foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a != b; }); case IrOpcode::ULessThan32: return foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a < b; }); case IrOpcode::ULessThanEqual32: return foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a <= b; }); case IrOpcode::UGreaterThan32: return foldU32Compare(builder, value, [](std::uint32_t a, std::uint32_t b) { return a > b; }); diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 6b68b292a..5951e7877 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -208,7 +208,7 @@ UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapsh return proof; } -std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads, const std::vector& imageMetadata) { +std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads, const std::vector& imageMetadata, std::span runtimeState) { std::vector result(layout.ShaderDataDwords(), 0u); for (std::size_t i = 0; i < layout.userDataRegisters.size(); ++i) { const auto reg = layout.userDataRegisters[i]; @@ -226,6 +226,8 @@ std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, st std::copy_n(descriptor.dwords.begin(), metadata.descriptor.size(), metadata.descriptor.begin()); std::memcpy(result.data() + entry.offset, &metadata, sizeof(metadata)); } + if (runtimeState.size() != layout.runtimeStateDwords) fail("runtime state does not match its shader data layout"); + std::copy(runtimeState.begin(), runtimeState.end(), result.begin() + layout.RuntimeStateDword()); return result; } @@ -250,7 +252,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con allocation.specialization = std::move(plan.specialization); } -DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings) const { +DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span exportMappings, bool registrationModules, bool fragmentDescriptorSet) const { if (stage == IrShaderStage::Pixel && exportMappings.size() != 8u) fail("fragment runtime export mappings are missing"); if (info.images.size() > ShaderInfo::MaxImages) fail("runtime image count exceeds the static capacity"); DescriptorBindingPlan plan; @@ -264,7 +266,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l const auto compare = compareStates[index] = ResourceMaterializer::EmulatedCompareState(info, snapshot, index); const auto first = PipelineSpecialization::CompareBase + index * PipelineSpecialization::CompareWords; const std::array values{EmulatedCompare::Function(compare), (compare & EmulatedCompare::Linear) != 0u, EmulatedCompare::AddressX(compare), EmulatedCompare::AddressY(compare), EmulatedCompare::Reference(compare), (compare & EmulatedCompare::BorderWhite) != 0u}; - for (std::uint32_t word = 0; word < values.size(); ++word) plan.specialization.push_back({first + word, values[word]}); + for (std::uint32_t word = 0; word < values.size() && !registrationModules; ++word) plan.specialization.push_back({first + word, values[word]}); } std::vector imageModes(info.images.size()); for (std::size_t index = 0; index < info.images.size(); ++index) imageModes[index] = ResourceMaterializer::RuntimeImageMode(info.images[index], snapshot.images.at(index), info.runtimeImageModes.at(index)); @@ -280,11 +282,15 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l auto alignment = static_cast(buffer.typedAlignment); if (buffer.descriptorFormatted && format != 0u) alignment = std::max(alignment, std::min(GetFormatInfo(static_cast(format)).byteSize, 4u)); if (alignment == 0u || misalignment % alignment != 0u) fail("is a formatted buffer access through a V# whose base is not aligned to its element"); - const auto first = PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords; - plan.specialization.push_back({first, descriptor.dwords[1] & 0xffff0000u}); - plan.specialization.push_back({first + 1u, descriptor.dwords[3]}); - plan.specialization.push_back({first + 2u, present ? 1u : 0u}); - plan.specialization.push_back({first + 3u, misalignment}); + if (registrationModules && buffer.descriptorFormatted) plan.specialization.push_back({PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords + 1u, descriptor.dwords[3]}); + if (registrationModules) plan.specialization.push_back({PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords + 3u, misalignment}); + if (!registrationModules) { + const auto first = PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords; + plan.specialization.push_back({first, descriptor.dwords[1] & 0xffff0000u}); + plan.specialization.push_back({first + 1u, descriptor.dwords[3]}); + plan.specialization.push_back({first + 2u, present ? 1u : 0u}); + plan.specialization.push_back({first + 3u, misalignment}); + } if (buffer.formattedReadMask != 0u) { for (std::uint32_t component = 0; component < 4u; ++component) { if ((buffer.formattedReadMask & (1u << component)) == 0u) continue; @@ -293,13 +299,13 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l } } } - for (std::uint32_t index = 0; index < info.images.size(); ++index) { + for (std::uint32_t index = 0; index < info.images.size() && !registrationModules; ++index) { if (info.images[index].indirectRoot != ImageResource::NoIndirectImage) continue; const auto first = PipelineSpecialization::ImageBase + index * PipelineSpecialization::ImageWords; plan.specialization.push_back({first, imageModes[index]}); for (std::uint32_t component = 0; component < 4u; ++component) plan.specialization.push_back({first + 1u + component, (snapshot.images.at(index).dwords[3] >> (component * 3u)) & 7u}); } - for (std::uint32_t target = 0; target < exportMappings.size(); ++target) { + for (std::uint32_t target = 0; target < exportMappings.size() && !registrationModules; ++target) { for (std::uint32_t component = 0; component < 4u; ++component) plan.specialization.push_back({PipelineSpecialization::ExportBase + target * 4u + component, (exportMappings[target] >> (component * 2u)) & 3u}); } std::vector samplerModes(info.samplers.size(), 0u); @@ -325,7 +331,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l const auto first = (word >> 12u) & 0xfu; const auto last = (word >> 16u) & 0xfu; if (last < first || last - first >= RuntimeAbi::StorageMipSlots) fail("invalid dynamic storage mip range"); - plan.specialization.push_back({PipelineSpecialization::MipCountBase + index, last - first + 1u}); + if (!registrationModules) plan.specialization.push_back({PipelineSpecialization::MipCountBase + index, last - first + 1u}); } for (std::uint32_t resource = 0; resource < info.images.size(); ++resource) { const auto& image = info.images[resource]; @@ -338,7 +344,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l if (mode != imageModes[slot]) fail("indirect image mode numbering disagrees with its root"); active[mode] = true; } - for (std::uint32_t mode = 0; mode < modes.size(); ++mode) plan.specialization.push_back({PipelineSpecialization::ImageModeBase + resource * PipelineSpecialization::ImageModeStride + mode, active[mode] ? 1u : 0u}); + for (std::uint32_t mode = 0; mode < modes.size() && !registrationModules; ++mode) plan.specialization.push_back({PipelineSpecialization::ImageModeBase + resource * PipelineSpecialization::ImageModeStride + mode, active[mode] ? 1u : 0u}); } plan.bindings.reserve(layout.descriptors.size()); for (const auto& logical : layout.descriptors) { @@ -346,6 +352,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l const bool samplerHeap = logical.kind == DescriptorBindingKind::Samplers; IrDescriptorBinding compact{logical.kind, {}}; std::vector originals; + std::vector inactive; std::uint32_t previous = UINT32_MAX; std::uint32_t mip = 0; for (std::uint32_t element = 0; element < logical.resources.size(); ++element) { @@ -363,17 +370,18 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l } else if (samplerHeap) { active = (samplerModes.at(resource) & (1u << (element & 1u))) != 0u; } - if (imageHeap || samplerHeap) plan.specialization.push_back({PipelineSpecialization::DescriptorIndex(static_cast(logical.kind), element), active ? static_cast(compact.resources.size()) : 0u}); - if (active) { + if ((imageHeap || samplerHeap) && !registrationModules) plan.specialization.push_back({PipelineSpecialization::DescriptorIndex(static_cast(logical.kind), element), active ? static_cast(compact.resources.size()) : 0u}); + if (active || ((imageHeap || samplerHeap) && registrationModules)) { compact.resources.push_back(resource); originals.push_back(element); + inactive.push_back(!active); } } - if (imageHeap || samplerHeap) plan.specialization.push_back({PipelineSpecialization::HeapCountBase + static_cast(logical.kind), std::max(1u, static_cast(compact.resources.size()))}); + if ((imageHeap || samplerHeap) && !registrationModules) plan.specialization.push_back({PipelineSpecialization::HeapCountBase + static_cast(logical.kind), std::max(1u, static_cast(compact.resources.size()))}); if ((imageHeap || samplerHeap) && compact.resources.empty()) continue; PreparedDescriptorBinding entry{}; auto& physical = entry.descriptor; - physical.descriptorSet = RuntimeAbi::DescriptorSet; + physical.descriptorSet = fragmentDescriptorSet && stage == IrShaderStage::Pixel ? RuntimeAbi::FragmentDescriptorSet : RuntimeAbi::DescriptorSet; physical.binding = NativeBinding(stage, compact.kind); physical.count = compact.resources.empty() ? 1u : static_cast(compact.resources.size()); physical.kind = PhysicalKindFor(compact.kind); @@ -404,7 +412,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l physical.imageUnnormalizedBaseLevel.push_back(unnormalized.imagesAtBaseLevel.at(resource)); physical.imageSamplers.push_back(0u); physical.imageAtomic64.push_back(image.atomic64); - if (layout.runtimeImageCount == 0u || image.indirectRoot == ImageResource::NoIndirectImage) continue; + if (layout.runtimeImageCount == 0u || image.indirectRoot == ImageResource::NoIndirectImage || inactive[element]) continue; if (resource >= layout.runtimeImageCount) fail("runtime image metadata exceeds compact layout"); const auto& descriptor = snapshot.images.at(resource); RuntimeAbi::ResourceMetadata metadata{}; @@ -412,7 +420,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l metadata.binding = static_cast(compact.kind); metadata.firstElement = static_cast(element); metadata.elementCount = 1u; - metadata.flags = imageModes[resource] << 1u; + metadata.flags = imageModes[resource] << 1u | (registrationModules ? (compareStates[resource] & RuntimeAbi::CompareStateMask) << RuntimeAbi::CompareStateShift : 0u); if (descriptor.dwords[0] == 0u && (descriptor.dwords[1] & 0xffu) == 0u) metadata.flags |= 1u; const auto offset = layout.ImageMetadataDword() + resource * static_cast(sizeof(metadata) / sizeof(std::uint32_t)); plan.imageMetadata.push_back({resource, offset, metadata}); @@ -426,20 +434,39 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l physical.samplerUnnormalized.push_back(unnormalized.samplers.at(resource)); const auto mode = 1u << (originals[element] & 1u); physical.samplerIntegerBorder.push_back((samplerIntegerModes.at(resource) & mode) != 0u && (samplerFloatModes.at(resource) & mode) == 0u); - if ((originals[element] & 1u) != 0u) entry.samplerFilterElements.push_back(element); + if ((originals[element] & 1u) != 0u && !inactive[element]) entry.samplerFilterElements.push_back(element); } } else if (physical.role == DescriptorRole::ShaderData && layout.UsesPushData()) { fail("DescriptorBindingBuilder::Populate shader-data binding must not exist when push data is used"); } plan.storageData |= physical.role == DescriptorRole::ShaderData; entry.resources = std::move(compact.resources); + if (registrationModules && (imageHeap || samplerHeap)) entry.inactive = std::move(inactive); plan.bindings.push_back(std::move(entry)); } + for (std::uint32_t resource = 0; resource < info.images.size() && registrationModules && layout.runtimeImageCount != 0u; ++resource) { + if (info.images[resource].indirectRoot != ImageResource::NoIndirectImage) continue; + if (resource >= layout.runtimeImageCount) fail("runtime image metadata exceeds compact layout"); + const auto& descriptor = snapshot.images.at(resource); + RuntimeAbi::ResourceMetadata metadata{}; + if (descriptor.dwordCount != metadata.descriptor.size()) fail("invalid runtime image descriptor width"); + metadata.elementCount = 1u; + if (imageModes[resource] > RuntimeAbi::ImageModeMask) fail("runtime image mode exceeds its metadata field"); + metadata.flags = imageModes[resource] << 1u | (registrationModules ? (compareStates[resource] & RuntimeAbi::CompareStateMask) << RuntimeAbi::CompareStateShift : 0u); + if (descriptor.dwords[0] == 0u && (descriptor.dwords[1] & 0xffu) == 0u) metadata.flags |= 1u; + plan.imageMetadata.push_back({resource, layout.ImageMetadataDword() + resource * static_cast(sizeof(metadata) / sizeof(std::uint32_t)), metadata}); + } + if (layout.runtimeStateDwords != 0u) { + plan.runtimeState.assign(layout.runtimeStateDwords, 0u); + for (std::uint32_t target = 0; target < exportMappings.size(); ++target) plan.runtimeState.at(RuntimeAbi::StateExportMappings + target / 4u) |= static_cast(exportMappings[target]) << (target % 4u * 8u); + } std::vector samplerMasks(info.samplers.size()); for (const auto& binding : plan.bindings) { if (binding.descriptor.role != DescriptorRole::GuestSamplers) continue; if (binding.resources.size() > ShaderInfo::MaxSamplers) fail("prepared sampler mask capacity exceeded"); - for (std::uint32_t element = 0; element < binding.resources.size(); ++element) samplerMasks.at(binding.resources[element]) |= 1u << element; + for (std::uint32_t element = 0; element < binding.resources.size(); ++element) { + if (binding.inactive.empty() || !binding.inactive[element]) samplerMasks.at(binding.resources[element]) |= 1u << element; + } } for (auto& binding : plan.bindings) { if (binding.descriptor.role != DescriptorRole::GuestImages) continue; @@ -470,13 +497,14 @@ DescriptorBindingPlan DescriptorBindingBuilder::Select(const DescriptorBindingPl } } selected.imageMetadata = plan.imageMetadata; + selected.runtimeState = plan.runtimeState; return selected; } void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, const CompiledBindingLayout& compiled, const DescriptorBindingPlan& plan, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) const { const auto& layout = compiled.layout; const bool needsShaderData = layout.UsesPushData() || plan.storageData; - auto shaderData = needsShaderData ? ShaderDataDwordsFor(layout, userDataBase, snapshot, partialThreads, plan.imageMetadata) : std::vector{}; + auto shaderData = needsShaderData ? ShaderDataDwordsFor(layout, userDataBase, snapshot, partialThreads, plan.imageMetadata, plan.runtimeState) : std::vector{}; std::vector bindings; bindings.reserve(plan.bindings.size()); std::size_t writtenHere = 0; @@ -494,6 +522,10 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con for (std::size_t element = 0; element < entry.resources.size(); ++element) { const auto& descriptor = snapshot.images.at(entry.resources[element]); if (descriptor.dwordCount != 8u) fail("invalid runtime image descriptor width"); + if (!entry.inactive.empty() && entry.inactive[element]) { + physical.guestDescriptor.insert(physical.guestDescriptor.end(), 8u, 0u); + continue; + } const auto offset = physical.guestDescriptor.size(); physical.guestDescriptor.insert(physical.guestDescriptor.end(), descriptor.dwords.begin(), descriptor.dwords.begin() + 8); if (entry.identityImageSwizzle[element]) physical.guestDescriptor[offset + 3u] = (physical.guestDescriptor[offset + 3u] & ~0xfffu) | ShaderImageIdentitySwizzle; diff --git a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp index 51ec69506..61a65c21b 100644 --- a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp @@ -2,6 +2,12 @@ #include "Optimization/DeadCodeEliminator.hpp" #include "RdnaDecoder/RdnaInstruction.hpp" #include +#include +#include +#include +#include +#include +#include #include #include #include @@ -11,8 +17,6 @@ namespace ShaderRecompiler { namespace { -constexpr std::size_t MinimumWalkLimit = 4096; -constexpr std::size_t WalkLimitPerInstruction = 2; constexpr std::uint32_t DepthLimit = 8; bool isSelect(IrOpcode opcode) { @@ -239,6 +243,7 @@ const IrValue* otherArgument(const IrValue* value, std::uint32_t immediate) { struct ThreadBit { const IrValue* low = nullptr; const IrValue* high = nullptr; + const IrValue* word = nullptr; }; bool isLane(const IrValue* value) { @@ -257,11 +262,11 @@ std::optional threadBit(const IrValue* value, std::uint32_t waveSize) const IrValue* lane = otherArgument(amount, 31u); if (lane == nullptr || !isLane(lane)) return std::nullopt; const IrValue* word = shifted->Argument(0)->Resolve(); - if (waveSize == 32u || word->HasImmediate()) return ThreadBit{word, word}; + if (waveSize == 32u || word->HasImmediate()) return ThreadBit{word, word, word}; if (word->Opcode() != IrOpcode::SelectU32) return std::nullopt; const IrValue* half = word->Argument(0)->Resolve(); if (half->Opcode() != IrOpcode::ULessThan32 || !isLane(half->Argument(0)) || !isImmediate(half->Argument(1)->Resolve(), 32u)) return std::nullopt; - return ThreadBit{word->Argument(1)->Resolve(), word->Argument(2)->Resolve()}; + return ThreadBit{word->Argument(1)->Resolve(), word->Argument(2)->Resolve(), word}; } bool alwaysTrue(const IrValue* value, std::uint32_t waveSize, std::uint32_t depth = 0) { @@ -326,11 +331,6 @@ const IrValue* wholeQuadSource(const IrValue* mask, std::uint32_t waveSize) { return source->Opcode() == IrOpcode::CompositeConstructU64 ? source : nullptr; } -bool withinQuadsOf(const IrValue* condition, const IrValue* mask, std::uint32_t waveSize, const BlockSet& uniform) { - const IrValue* source = wholeQuadSource(mask, waveSize); - return source != nullptr && withinWords(condition, source->Argument(0)->Resolve(), source->Argument(1)->Resolve(), waveSize, uniform, 0u); -} - enum class Region : std::uint8_t { Unreached, Once, Loop }; Region regionOf(const std::vector& regions, const IrBlock* block) { @@ -342,151 +342,6 @@ bool followable(const std::vector& regions, const IrBlock* block, bool a return region == Region::Once || (acrossLoops && region == Region::Loop); } -struct ImplicationKey { - const IrValue* first = nullptr; - const IrValue* second = nullptr; - - bool operator==(const ImplicationKey&) const = default; -}; - -struct ImplicationKeyHash { - std::size_t operator()(const ImplicationKey& key) const { - return std::hash{}(key.first) * 31u ^ std::hash{}(key.second); - } -}; - -class Implications { -public: - Implications(const std::vector& regions, const BlockSet& uniform, std::uint32_t waveSize, std::size_t& work) : regions(regions), uniform(uniform), waveSize(waveSize), work(work) {} - - void Reset(const IrValue* writeMask, bool acrossLoops) { - mask = writeMask; - loops = acrossLoops; - always = alwaysTrue(writeMask, waveSize); - results.clear(); - proven.clear(); - assumptionReads = 0; - } - - bool Implies(const IrValue* condition, std::uint32_t depth = 0) { - condition = condition->Resolve(); - if (condition == mask || always) return true; - if (condition->HasImmediate()) return condition->Type() == IrType::Bool && !condition->ImmediateBool(); - const ImplicationKey key{condition, nullptr}; - if (const auto known = lookup(key)) return *known; - if (depth > ImplicationDepthLimit || work == 0) return false; - --work; - const Attempt attempt = assume(key); - bool result = false; - if (withinQuadsOf(condition, mask, waveSize, uniform)) { - result = true; - } else if (condition->Opcode() == IrOpcode::LogicalAnd) { - result = Implies(condition->Argument(0), depth + 1u) || Implies(condition->Argument(1), depth + 1u); - } else if (condition->IsPhi()) { - result = merges(*condition); - for (std::size_t index = 0; result && index < condition->ArgumentCount(); ++index) { - result = Implies(condition->Argument(index), depth + 1u); - } - } else if (const auto bit = threadBit(condition, waveSize)) { - result = wordsImply(bit->low, bit->high, depth + 1u); - } - return settle(key, attempt, result); - } - -private: - static constexpr std::uint32_t ImplicationDepthLimit = 64; - - enum class Proof : std::uint8_t { Assumed, Implied, NotImplied }; - - struct Attempt { - std::size_t proven = 0; - std::size_t assumptionReads = 0; - }; - - std::optional lookup(const ImplicationKey& key) { - const auto known = results.find(key); - if (known == results.end()) return std::nullopt; - if (known->second == Proof::Assumed) ++assumptionReads; - return known->second != Proof::NotImplied; - } - - Attempt assume(const ImplicationKey& key) { - results.emplace(key, Proof::Assumed); - return Attempt{proven.size(), assumptionReads}; - } - - bool settle(const ImplicationKey& key, const Attempt& attempt, bool implied) { - if (!implied && assumptionReads != attempt.assumptionReads) { - for (std::size_t index = attempt.proven; index < proven.size(); ++index) results.erase(proven[index]); - proven.resize(attempt.proven); - } - results[key] = implied ? Proof::Implied : Proof::NotImplied; - if (implied) proven.push_back(key); - return implied; - } - - bool merges(const IrValue& phi) const { - if (!followable(regions, phi.Parent(), loops) || phi.ArgumentCount() != phi.PhiBlockCount()) return false; - for (std::size_t index = 0; index < phi.ArgumentCount(); ++index) { - const IrValue* incoming = phi.Argument(index)->Resolve(); - if (!incoming->HasImmediate() && !followable(regions, incoming->Parent(), loops)) return false; - } - return true; - } - - static const IrValue* incomingFrom(const IrValue& phi, const IrBlock* predecessor) { - for (std::size_t index = 0; index < phi.PhiBlockCount(); ++index) { - if (phi.PhiBlock(index) == predecessor) return phi.Argument(index)->Resolve(); - } - return nullptr; - } - - bool wordsImply(const IrValue* low, const IrValue* high, std::uint32_t depth) { - if (isImmediate(low, 0u) && (waveSize == 32u || isImmediate(high, 0u))) return true; - const ImplicationKey key{low, high}; - if (const auto known = lookup(key)) return *known; - if (depth > ImplicationDepthLimit || work == 0) return false; - --work; - const Attempt attempt = assume(key); - bool result = false; - if (low->Opcode() == IrOpcode::CompositeExtractU32x4 && isImmediate(low->Argument(1)->Resolve(), 0u)) { - const IrValue* ballot = low->Argument(0)->Resolve(); - const bool sameBallot = waveSize == 32u || (high->Opcode() == IrOpcode::CompositeExtractU32x4 && high->Argument(0)->Resolve() == ballot && isImmediate(high->Argument(1)->Resolve(), 1u)); - result = ballot->Opcode() == IrOpcode::Ballot && sameBallot && Implies(ballot->Argument(0), depth + 1u); - } - if (!result && low->Opcode() == IrOpcode::BitwiseAnd32) { - if (waveSize == 32u) { - result = wordsImply(low->Argument(0)->Resolve(), high, depth + 1u) || wordsImply(low->Argument(1)->Resolve(), high, depth + 1u); - } else if (high->Opcode() == IrOpcode::BitwiseAnd32) { - for (std::size_t lowIndex = 0; !result && lowIndex < 2u; ++lowIndex) { - for (std::size_t highIndex = 0; !result && highIndex < 2u; ++highIndex) { - result = wordsImply(low->Argument(lowIndex)->Resolve(), high->Argument(highIndex)->Resolve(), depth + 1u); - } - } - } - } - if (!result && low->IsPhi() && merges(*low) && (waveSize == 32u || (high->IsPhi() && high->Parent() == low->Parent() && merges(*high)))) { - result = true; - for (std::size_t index = 0; result && index < low->PhiBlockCount(); ++index) { - const IrValue* highIncoming = waveSize == 32u ? high : incomingFrom(*high, low->PhiBlock(index)); - result = highIncoming != nullptr && wordsImply(low->Argument(index)->Resolve(), highIncoming, depth + 1u); - } - } - return settle(key, attempt, result); - } - - const std::vector& regions; - const BlockSet& uniform; - std::uint32_t waveSize; - std::size_t& work; - const IrValue* mask = nullptr; - bool loops = false; - bool always = false; - std::unordered_map results; - std::vector proven; - std::size_t assumptionReads = 0; -}; - std::vector blockRegions(const IrProgram& program) { const auto& order = program.BlockOrder(); if (order.empty()) return {}; @@ -529,99 +384,637 @@ std::vector blockRegions(const IrProgram& program) { return result; } -std::size_t walkLimit(const IrProgram& program) { - std::size_t instructions = 0; - for (const IrBlock* block : program.BlockOrder()) instructions += block->Instructions().size(); - return std::max(MinimumWalkLimit, WalkLimitPerInstruction * instructions); -} +using MaskSet = std::uint32_t; -class MaskedLaneWalk { +class MaskSets { public: - MaskedLaneWalk(const IrProgram& program, const std::vector& regions) : program(program), regions(regions), uniform(uniformEntryBlocks(program)), limit(walkLimit(program)), implications(regions, uniform, program.WaveSize(), work) {} + static constexpr MaskSet None = 0; + static constexpr MaskSet All = 1; - bool UnobservedWhereMasked(const IrValue& select, const IrValue* mask) { - const bool acrossLoops = mask->HasImmediate() || regionOf(regions, mask->Parent()) == Region::Once; - work = limit; - implications.Reset(mask, acrossLoops); - const bool quadMasked = program.Resources().stage == IrShaderStage::Pixel && wholeQuadSource(mask, program.WaveSize()) != nullptr; - startVisits(); - pending.clear(); - pending.push_back(&select); - static_cast(firstVisit(select)); - while (!pending.empty()) { - const IrValue* value = pending.back(); - pending.pop_back(); - for (const IrUse& use : value->OperandUses()) { - const IrValue* user = use.user; - if (!followable(regions, user->Parent(), acrossLoops)) return false; - if (isSelect(user->Opcode()) && use.operand == 1u && implications.Implies(user->Argument(0))) continue; - if (user->Opcode() == IrOpcode::LogicalAnd && use.operand < 2u && implications.Implies(user->Argument(1u - use.operand))) continue; - if (readsOnlyWhereActive(user->Opcode()) && use.operand + 1u < user->ArgumentCount() && implications.Implies(user->Argument(user->ArgumentCount() - 1u))) continue; - if (!user->IsPhi() && !isLaneLocal(user->Opcode()) && !isExplicitLodSample(program, *user) && !(quadMasked && user->Opcode() == IrOpcode::ImageSampleRaw)) return false; - if (firstVisit(*user)) { - if (work == 0) return false; - --work; - pending.push_back(user); + explicit MaskSets(std::uint64_t& steps) : steps(steps), sets(2) {} + + MaskSet Single(std::uint32_t mask) { + sets.push_back({mask}); + return static_cast(sets.size() - 1u); + } + + MaskSet Of(std::vector&& masks) { + if (masks.empty()) return None; + std::sort(masks.begin(), masks.end()); + sets.push_back(std::move(masks)); + return static_cast(sets.size() - 1u); + } + + MaskSet Union(MaskSet left, MaskSet right) { + if (left == right || right == None) return left; + if (left == None) return right; + if (left == All || right == All) return All; + std::vector merged; + merged.reserve(sets[left].size() + sets[right].size()); + std::set_union(sets[left].begin(), sets[left].end(), sets[right].begin(), sets[right].end(), std::back_inserter(merged)); + return keep(left, right, std::move(merged)); + } + + MaskSet Intersection(MaskSet left, MaskSet right) { + if (left == right || right == All) return left; + if (left == All) return right; + if (left == None || right == None) return None; + std::vector common; + std::set_intersection(sets[left].begin(), sets[left].end(), sets[right].begin(), sets[right].end(), std::back_inserter(common)); + return keep(left, right, std::move(common)); + } + + [[nodiscard]] bool Contains(MaskSet set, std::uint32_t mask) const { + return set == All || (set != None && std::binary_search(sets[set].begin(), sets[set].end(), mask)); + } + + [[nodiscard]] bool Same(MaskSet left, MaskSet right) const { + return left == right || (left > All && right > All && sets[left] == sets[right]); + } + +private: + MaskSet keep(MaskSet left, MaskSet right, std::vector&& result) { + steps += sets[left].size() + sets[right].size(); + if (result.empty()) return None; + if (result.size() == sets[left].size()) return left; + if (result.size() == sets[right].size()) return right; + sets.push_back(std::move(result)); + return static_cast(sets.size() - 1u); + } + + std::uint64_t& steps; + std::vector> sets; +}; + +class Components { +public: + template + void From(std::uint32_t root, TSuccessors&& successors, TVisit&& visit) { + if (seen(root)) return; + open(root, successors); + while (!frames.empty()) { + Frame& frame = frames.back(); + if (frame.next != frame.end) { + const std::uint32_t successor = edges[frame.next++]; + if (successor == frame.node) frame.cyclic = true; + if (!seen(successor)) { + open(successor, successors); + } else if (low[successor] != Closed) { + low[frame.node] = std::min(low[frame.node], order[successor]); } + continue; } + const std::uint32_t node = frame.node; + const bool cyclic = frame.cyclic; + edges.resize(frame.begin); + frames.pop_back(); + if (!frames.empty()) low[frames.back().node] = std::min(low[frames.back().node], low[node]); + if (low[node] != order[node]) continue; + members.clear(); + do { + members.push_back(pending.back()); + pending.pop_back(); + low[members.back()] = Closed; + } while (members.back() != node); + visit(members, cyclic || members.size() != 1u); + } + } + +private: + static constexpr std::uint32_t Closed = 0xffffffffu; + + struct Frame { + std::uint32_t node = 0; + std::size_t begin = 0; + std::size_t next = 0; + std::size_t end = 0; + bool cyclic = false; + }; + + [[nodiscard]] bool seen(std::uint32_t node) const { + return node < order.size() && order[node] != 0u; + } + + template + void open(std::uint32_t node, TSuccessors& successors) { + if (node >= order.size()) { + order.resize(node + 1u, 0u); + low.resize(node + 1u, 0u); + } + order[node] = low[node] = ++opened; + pending.push_back(node); + const std::size_t begin = edges.size(); + successors(node, edges); + frames.push_back(Frame{node, begin, begin, edges.size(), false}); + } + + std::vector order; + std::vector low; + std::vector pending; + std::vector edges; + std::vector members; + std::vector frames; + std::uint32_t opened = 0; +}; + +struct MaskFamily { + bool acrossLoops = false; + bool wholeQuads = false; + + bool operator==(const MaskFamily&) const = default; +}; + +enum class Implication : std::uint8_t { Condition, Words, SelectedTrue, SelectedFalse }; + +struct ImplicationKey { + const IrValue* first = nullptr; + const IrValue* second = nullptr; + Implication kind = Implication::Condition; + + bool operator==(const ImplicationKey&) const = default; +}; + +struct ImplicationKeyHash { + std::size_t operator()(const ImplicationKey& key) const { + return std::hash{}(key.first) * 31u ^ std::hash{}(key.second); + } +}; + +class ImpliedMasks { +public: + ImpliedMasks(const std::vector& regions, const BlockSet& uniform, std::uint32_t waveSize, MaskFamily family, MaskSets& sets, std::uint64_t& steps) : regions(regions), uniform(uniform), waveSize(waveSize), family(family), sets(sets), steps(steps) {} + + void AddMask(const IrValue* mask) { + if (!mark(masks, mask)) return; + const auto bit = threadBit(mask, waveSize); + if (bit) { + mark(followed, bit->word); + sameWords[wordsKey(bit->low, bit->high)].masks.push_back(mask->Id()); + } + if (!family.wholeQuads) return; + mark(followed, mask); + const IrValue* source = wholeQuadSource(mask, waveSize); + const auto known = std::find_if(quads.begin(), quads.end(), [&](const Quads& entry) { return entry.source == source; }); + if (known == quads.end()) { + quads.push_back(Quads{source, sets.Single(mask->Id())}); + } else { + known->masks = sets.Union(known->masks, sets.Single(mask->Id())); + } + } + + MaskSet Of(const IrValue* guard) { + const std::uint32_t root = condition(guard); + components.From(root, [this](std::uint32_t node, std::vector& out) { expand(node, out); }, [this](const std::vector& members, bool cyclic) { settle(members, cyclic); }); + return nodes[root].implied; + } + + [[nodiscard]] bool Follows(const IrValue& value) const { + return value.Id() < followed.size() && followed[value.Id()] != 0u; + } + +private: + struct Node { + ImplicationKey key; + MaskSet own = MaskSets::None; + MaskSet implied = MaskSets::All; + bool every = false; + bool queued = false; + std::uint32_t group = 0; + std::vector inputs; + std::vector outputs; + }; + + struct Quads { + const IrValue* source = nullptr; + MaskSet masks = MaskSets::None; + }; + + struct SameWords { + std::vector masks; + MaskSet set = MaskSets::None; + }; + + static bool mark(std::vector& marks, const IrValue* value) { + if (value->Id() >= marks.size()) marks.resize(value->Id() + 1u, 0u); + return std::exchange(marks[value->Id()], std::uint8_t{1}) == 0u; + } + + std::uint32_t add(const ImplicationKey& key) { + nodes.push_back(Node{key}); + mark(followed, key.first); + if (key.second != nullptr) mark(followed, key.second); + return static_cast(nodes.size() - 1u); + } + + [[nodiscard]] ImplicationKey wordsKey(const IrValue* low, const IrValue* high) const { + return ImplicationKey{low, waveSize == 32u ? low : high, Implication::Words}; + } + + std::uint32_t words(const IrValue* low, const IrValue* high) { + const ImplicationKey key = wordsKey(low, high); + const auto known = wordNodes.find(key); + return known != wordNodes.end() ? known->second : wordNodes.emplace(key, add(key)).first->second; + } + + std::uint32_t condition(const IrValue* value) { + value = value->Resolve(); + if (value->Id() >= conditionNodes.size()) conditionNodes.resize(value->Id() + 1u, Unknown); + if (conditionNodes[value->Id()] == Unknown) conditionNodes[value->Id()] = add(ImplicationKey{value}); + return conditionNodes[value->Id()]; + } + + bool merges(const IrValue& phi) const { + if (!followable(regions, phi.Parent(), family.acrossLoops) || phi.ArgumentCount() != phi.PhiBlockCount()) return false; + for (std::size_t index = 0; index < phi.ArgumentCount(); ++index) { + const IrValue* incoming = phi.Argument(index)->Resolve(); + if (!incoming->HasImmediate() && !followable(regions, incoming->Parent(), family.acrossLoops)) return false; } return true; } -private: - void startVisits() { - if (++epoch != 0u) return; - std::fill(visits.begin(), visits.end(), 0u); - epoch = 1u; + static const IrValue* incomingFrom(const IrValue& phi, const IrBlock* predecessor) { + for (std::size_t index = 0; index < phi.PhiBlockCount(); ++index) { + if (phi.PhiBlock(index) == predecessor) return phi.Argument(index)->Resolve(); + } + return nullptr; } - bool firstVisit(const IrValue& value) { - const std::uint32_t id = value.Id(); - if (id >= visits.size()) visits.resize(id + 1u, 0u); - if (visits[id] == epoch) return false; - visits[id] = epoch; - return true; + void expand(std::uint32_t node, std::vector& out) { + const ImplicationKey key = nodes[node].key; + Node shape{key}; + if (key.kind == Implication::Condition) { + expandCondition(key.first, shape); + } else if (key.kind == Implication::Words) { + expandWords(key.first, key.second, shape); + } else { + expandSelected(key.first, key.kind == Implication::SelectedTrue, shape); + } + out.insert(out.end(), shape.inputs.begin(), shape.inputs.end()); + for (const std::uint32_t input : shape.inputs) nodes[input].outputs.push_back(node); + shape.outputs = std::move(nodes[node].outputs); + nodes[node] = std::move(shape); } - const IrProgram& program; + void expandCondition(const IrValue* value, Node& shape) { + if (value->Id() < masks.size() && masks[value->Id()] != 0u && !threadBit(value, waveSize)) shape.own = sets.Single(value->Id()); + if (value->HasImmediate()) { + if (value->Type() == IrType::Bool && !value->ImmediateBool()) shape.own = MaskSets::All; + return; + } + for (const Quads& entry : quads) { + ++steps; + if (withinWords(value, entry.source->Argument(0)->Resolve(), entry.source->Argument(1)->Resolve(), waveSize, uniform, 0u)) shape.own = sets.Union(shape.own, entry.masks); + } + if (value->Opcode() == IrOpcode::LogicalAnd) { + shape.inputs = {condition(value->Argument(0)), condition(value->Argument(1))}; + } else if (value->Opcode() == IrOpcode::SelectU1) { + shape.every = true; + shape.inputs = {add(ImplicationKey{value, nullptr, Implication::SelectedTrue}), add(ImplicationKey{value, nullptr, Implication::SelectedFalse})}; + } else if (value->IsPhi()) { + if (!merges(*value)) return; + shape.every = true; + for (std::size_t index = 0; index < value->ArgumentCount(); ++index) shape.inputs.push_back(condition(value->Argument(index))); + } else if (const auto bit = threadBit(value, waveSize)) { + mark(followed, bit->word); + shape.inputs = {words(bit->low, bit->high)}; + } + } + + void expandSelected(const IrValue* select, bool whenTrue, Node& shape) { + const IrValue* selector = select->Argument(0)->Resolve(); + shape.inputs = {condition(select->Argument(whenTrue ? 1u : 2u))}; + if (whenTrue) { + shape.inputs.push_back(condition(selector)); + } else if (selector->Opcode() == IrOpcode::LogicalNot) { + shape.inputs.push_back(condition(selector->Argument(0))); + } + } + + void expandWords(const IrValue* low, const IrValue* high, Node& shape) { + if (isImmediate(low, 0u) && (waveSize == 32u || isImmediate(high, 0u))) { + shape.own = MaskSets::All; + return; + } + if (const auto same = sameWords.find(wordsKey(low, high)); same != sameWords.end()) { + if (same->second.set == MaskSets::None) same->second.set = sets.Of(std::move(same->second.masks)); + shape.own = same->second.set; + } + if (low->Opcode() == IrOpcode::CompositeExtractU32x4 && isImmediate(low->Argument(1)->Resolve(), 0u)) { + const IrValue* ballot = low->Argument(0)->Resolve(); + const bool sameBallot = waveSize == 32u || (high->Opcode() == IrOpcode::CompositeExtractU32x4 && high->Argument(0)->Resolve() == ballot && isImmediate(high->Argument(1)->Resolve(), 1u)); + if (ballot->Opcode() == IrOpcode::Ballot && sameBallot) shape.inputs = {condition(ballot->Argument(0))}; + } else if (low->Opcode() == IrOpcode::BitwiseAnd32) { + if (waveSize == 32u) { + shape.inputs = {words(low->Argument(0)->Resolve(), high), words(low->Argument(1)->Resolve(), high)}; + } else if (high->Opcode() == IrOpcode::BitwiseAnd32) { + for (std::size_t lowIndex = 0; lowIndex < 2u; ++lowIndex) { + for (std::size_t highIndex = 0; highIndex < 2u; ++highIndex) shape.inputs.push_back(words(low->Argument(lowIndex)->Resolve(), high->Argument(highIndex)->Resolve())); + } + } + } else if (low->IsPhi() && merges(*low) && (waveSize == 32u || (high->IsPhi() && high->Parent() == low->Parent() && merges(*high)))) { + for (std::size_t index = 0; index < low->PhiBlockCount(); ++index) { + const IrValue* highIncoming = waveSize == 32u ? high : incomingFrom(*high, low->PhiBlock(index)); + if (highIncoming == nullptr) { + shape.inputs.clear(); + return; + } + shape.inputs.push_back(words(low->Argument(index)->Resolve(), highIncoming)); + } + shape.every = true; + } + } + + MaskSet evaluate(const Node& node) { + MaskSet result = node.every ? MaskSets::All : MaskSets::None; + for (const std::uint32_t input : node.inputs) { + ++steps; + result = node.every ? sets.Intersection(result, nodes[input].implied) : sets.Union(result, nodes[input].implied); + } + return sets.Union(result, node.own); + } + + void settle(const std::vector& members, bool cyclic) { + if (!cyclic) { + nodes[members.front()].implied = evaluate(nodes[members.front()]); + return; + } + ++groups; + queue.assign(members.rbegin(), members.rend()); + for (const std::uint32_t member : members) { + nodes[member].queued = true; + nodes[member].group = groups; + } + while (!queue.empty()) { + const std::uint32_t member = queue.back(); + queue.pop_back(); + nodes[member].queued = false; + const MaskSet result = evaluate(nodes[member]); + if (sets.Same(result, nodes[member].implied)) continue; + nodes[member].implied = result; + for (const std::uint32_t output : nodes[member].outputs) { + if (nodes[output].group == groups && !std::exchange(nodes[output].queued, true)) queue.push_back(output); + } + } + } + + static constexpr std::uint32_t Unknown = 0xffffffffu; + const std::vector& regions; - BlockSet uniform; - std::size_t limit; - std::size_t work = 0; - Implications implications; - std::vector pending; - std::vector visits; - std::uint32_t epoch = 0; + const BlockSet& uniform; + std::uint32_t waveSize; + MaskFamily family; + MaskSets& sets; + std::uint64_t& steps; + std::vector masks; + std::vector followed; + std::vector quads; + std::vector nodes; + std::vector conditionNodes; + std::unordered_map wordNodes; + std::unordered_map sameWords; + std::vector queue; + std::uint32_t groups = 0; + Components components; }; +MaskFamily familyOf(const std::vector& regions, const IrValue* mask, std::uint32_t waveSize) { + return MaskFamily{mask->HasImmediate() || regionOf(regions, mask->Parent()) == Region::Once, wholeQuadSource(mask, waveSize) != nullptr}; +} + +class UnreadLanes; + +using FirstLaneReads = std::array, 4>; + +class UnreadLanes { +public: + UnreadLanes(IrProgram& program, const std::vector& regions, const BlockSet& uniform, MaskFamily family, std::uint64_t& steps, FirstLaneReads* shared = nullptr) + : program(program), regions(regions), uniform(uniform), family(family), sampledByQuad(family.wholeQuads && program.Resources().stage == IrShaderStage::Pixel), firstLane(shared != nullptr), sameBlock(firstLane && !family.acrossLoops), steps(steps), sets(steps), implied(regions, uniform, program.WaveSize(), family, sets, steps), firstLanes(firstLane ? *shared : owned) { + if (!firstLane) return; + for (const IrBlock* block : program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + if (inst->Opcode() != IrOpcode::ReadFirstLane) continue; + const IrValue* mask = inst->Argument(1)->Resolve(); + if (familyOf(regions, mask, program.WaveSize()) == family) implied.AddMask(mask); + } + } + } + + bool UnreadOutsideItsMask(IrValue& read) { + if (solving) return false; + solving = true; + static_cast(readerOf(read)); + components.From(read.Id(), [this](std::uint32_t node, std::vector& out) { expand(node, out); }, [this](const std::vector& members, bool cyclic) { settle(members, cyclic); }); + solving = false; + return sets.Contains(unread[read.Id()], read.Argument(1)->Resolve()->Id()); + } + + [[nodiscard]] bool Follows(const IrValue& value) const { + return implied.Follows(value) || (value.Id() < readers.size() && readers[value.Id()] != Reader::Unknown); + } + + std::uint32_t Remove() { + std::vector order; + for (auto blockIt = program.BlockOrder().rbegin(); blockIt != program.BlockOrder().rend(); ++blockIt) { + const bool single = regionOf(regions, *blockIt) == Region::Once; + const auto& instructions = (*blockIt)->Instructions(); + for (auto it = instructions.rbegin(); it != instructions.rend(); ++it) { + IrValue* inst = *it; + order.push_back(inst); + if (!single || !isSelect(inst->Opcode()) || inst->ArgumentCount() != 3u || !inst->HasUses()) continue; + const IrValue* mask = inst->Argument(0)->Resolve(); + if (familyOf(regions, mask, program.WaveSize()) != family) continue; + implied.AddMask(mask); + slot(writes, inst->Id(), static_cast(nullptr)) = mask; + found = true; + } + } + if (!found) return 0; + for (IrValue* inst : order) { + if (halted) break; + static_cast(readerOf(*inst)); + components.From(inst->Id(), [this](std::uint32_t node, std::vector& out) { expand(node, out); }, [this](const std::vector& members, bool cyclic) { settle(members, cyclic); }); + } + if (removed != 0u) { + for (IrBlock* block : program.BlockOrder()) std::erase_if(block->Instructions(), [](const IrValue* inst) { return inst->Parent() == nullptr; }); + } + return removed; + } + +private: + static constexpr std::uint32_t Queued = 0xffffffffu; + + template + static TValue& slot(std::vector& table, std::uint32_t id, TValue fill) { + if (id >= table.size()) table.resize(id + 1u, fill); + return table[id]; + } + + enum class Reader : std::uint8_t { Unknown, Outside, Reads, Passes }; + + Reader readerOf(IrValue& user) { + Reader& known = slot(readers, user.Id(), Reader::Unknown); + if (known != Reader::Unknown) return known; + slot(values, user.Id(), static_cast(nullptr)) = &user; + slot(unread, user.Id(), MaskSets::All); + slot(groups, user.Id(), std::uint32_t{0}); + if (sameBlock ? user.IsPhi() : !followable(regions, user.Parent(), family.acrossLoops)) return known = Reader::Outside; + const bool passes = user.IsPhi() || isLaneLocal(user.Opcode()) || isExplicitLodSample(program, user) || (sampledByQuad && user.Opcode() == IrOpcode::ImageSampleRaw); + return known = passes ? Reader::Passes : Reader::Reads; + } + + [[nodiscard]] bool leaves(const IrValue& value, const IrValue& user) const { + return sameBlock && user.Parent() != value.Parent(); + } + + [[nodiscard]] bool readsFirstLane(const IrUse& use) const { + return use.user->Opcode() == IrOpcode::ReadFirstLane && use.operand == 0u; + } + + [[nodiscard]] bool solvedHere(const IrValue& read) const { + return firstLane && familyOf(regions, read.Argument(1)->Resolve(), program.WaveSize()) == family; + } + + bool unreadOutsideItsMask(IrValue& read) { + const IrValue* mask = read.Argument(1)->Resolve(); + if (solvedHere(read)) return sets.Contains(unread[read.Id()], mask->Id()); + const MaskFamily readFamily = familyOf(regions, mask, program.WaveSize()); + auto& reads = firstLanes[(readFamily.acrossLoops ? 2u : 0u) + (readFamily.wholeQuads ? 1u : 0u)]; + if (!reads) reads = std::make_unique(program, regions, uniform, readFamily, steps, &firstLanes); + return reads->UnreadOutsideItsMask(read); + } + + void expand(std::uint32_t node, std::vector& out) { + for (const IrUse& use : values[node]->OperandUses()) { + const Reader reader = readerOf(*use.user); + if (reader == Reader::Outside || leaves(*values[node], *use.user)) continue; + if (reader == Reader::Passes || (readsFirstLane(use) && solvedHere(*use.user))) out.push_back(use.user->Id()); + } + } + + MaskSet unreadBy(const IrValue& value, const IrUse& use) { + IrValue* user = use.user; + const Reader reader = readerOf(*user); + if (reader == Reader::Outside || leaves(value, *user)) return MaskSets::None; + MaskSet guarded = MaskSets::None; + if (isSelect(user->Opcode()) && use.operand == 1u) { + guarded = implied.Of(user->Argument(0)); + } else if (user->Opcode() == IrOpcode::LogicalAnd && use.operand < 2u) { + guarded = implied.Of(user->Argument(1u - use.operand)); + } else if (readsOnlyWhereActive(user->Opcode()) && use.operand + 1u < user->ArgumentCount()) { + guarded = implied.Of(user->Argument(user->ArgumentCount() - 1u)); + } else if (readsFirstLane(use)) { + guarded = implied.Of(user->Argument(1)); + if (guarded != MaskSets::None && !unreadOutsideItsMask(*user)) guarded = MaskSets::None; + } + if (guarded == MaskSets::All || reader != Reader::Passes) return guarded; + return sets.Union(guarded, unread[user->Id()]); + } + + MaskSet evaluate(std::uint32_t node) { + MaskSet result = MaskSets::All; + for (const IrUse& use : values[node]->OperandUses()) { + if (result == MaskSets::None) break; + ++steps; + result = sets.Intersection(result, unreadBy(*values[node], use)); + } + return result; + } + + void settle(const std::vector& members, bool cyclic) { + if (halted) return; + if (!cyclic) { + unread[members.front()] = evaluate(members.front()); + remove(members.front()); + return; + } + ++group; + queue.assign(members.rbegin(), members.rend()); + for (const std::uint32_t member : members) groups[member] = Queued; + while (!queue.empty()) { + const std::uint32_t member = queue.back(); + queue.pop_back(); + groups[member] = group; + const MaskSet result = evaluate(member); + if (sets.Same(result, unread[member])) continue; + unread[member] = result; + for (const IrValue* argument : values[member]->Arguments()) { + if (argument == nullptr || argument->Id() >= groups.size() || groups[argument->Id()] != group) continue; + groups[argument->Id()] = Queued; + queue.push_back(argument->Id()); + } + } + } + + void remove(std::uint32_t node) { + IrValue* select = values[node]; + const IrValue* mask = node < writes.size() ? writes[node] : nullptr; + if (mask == nullptr || !select->HasUses() || select->Argument(0)->Resolve() != mask || !sets.Contains(unread[node], mask->Id())) return; + halted = implied.Follows(*select) || std::any_of(owned.begin(), owned.end(), [&](const auto& reads) { return reads && reads->Follows(*select); }); + select->ReplaceAllUsesWith(select->Argument(1)); + select->Invalidate(); + select->SetParent(nullptr); + ++removed; + } + + IrProgram& program; + const std::vector& regions; + const BlockSet& uniform; + MaskFamily family; + bool sampledByQuad; + bool firstLane; + bool sameBlock; + std::uint64_t& steps; + MaskSets sets; + ImpliedMasks implied; + FirstLaneReads owned; + FirstLaneReads& firstLanes; + Components components; + std::vector values; + std::vector writes; + std::vector unread; + std::vector readers; + std::vector groups; + std::vector queue; + std::uint32_t group = 0; + std::uint32_t removed = 0; + bool found = false; + bool halted = false; + bool solving = false; +}; + +std::uint32_t removeFullWaveSelects(IrProgram& program) { + std::uint32_t removed = 0; + for (auto blockIt = program.BlockOrder().rbegin(); blockIt != program.BlockOrder().rend(); ++blockIt) { + auto& instructions = (*blockIt)->Instructions(); + auto it = instructions.end(); + while (it != instructions.begin()) { + --it; + IrValue* inst = *it; + if (!isSelect(inst->Opcode()) || inst->ArgumentCount() != 3u || !inst->HasUses() || !alwaysTrue(inst->Argument(0), program.WaveSize())) continue; + inst->ReplaceAllUsesWith(inst->Argument(1)); + inst->Invalidate(); + inst->SetParent(nullptr); + it = instructions.erase(it); + ++removed; + } + } + return removed; +} + } MaskedSelectEliminationStats MaskedSelectEliminator::Eliminate(IrProgram& program) const { MaskedSelectEliminationStats stats; const auto regions = blockRegions(program); - MaskedLaneWalk walk(program, regions); - bool changed = true; - while (changed) { - changed = false; - for (auto blockIt = program.BlockOrder().rbegin(); blockIt != program.BlockOrder().rend(); ++blockIt) { - const bool single = regionOf(regions, *blockIt) == Region::Once; - auto& instructions = (*blockIt)->Instructions(); - auto it = instructions.end(); - while (it != instructions.begin()) { - --it; - IrValue* inst = *it; - if (!isSelect(inst->Opcode()) || inst->ArgumentCount() != 3u || !inst->HasUses()) continue; - const IrValue* mask = inst->Argument(0)->Resolve(); - if (!alwaysTrue(mask, program.WaveSize()) && (!single || !walk.UnobservedWhereMasked(*inst, mask))) continue; - inst->ReplaceAllUsesWith(inst->Argument(1)); - inst->Invalidate(); - inst->SetParent(nullptr); - it = instructions.erase(it); - ++stats.removedSelects; - changed = true; - } + const BlockSet uniform = uniformEntryBlocks(program); + std::uint32_t removed = 1; + while (removed != 0u) { + removed = removeFullWaveSelects(program); + for (const MaskFamily family : {MaskFamily{true, false}, MaskFamily{true, true}, MaskFamily{false, false}, MaskFamily{false, true}}) { + removed += UnreadLanes(program, regions, uniform, family, stats.steps).Remove(); } - if (changed) DeadCodeEliminator{}.Eliminate(program); + stats.removedSelects += removed; + if (removed != 0u) DeadCodeEliminator{}.Eliminate(program); } return stats; } diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 00bde0c18..e9481485b 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -257,7 +257,7 @@ void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pi const auto input = inst->Argument(0)->Resolve()->ImmediateU32(); const auto mode = inst->Argument(2)->Resolve()->ImmediateU32(); read[input] = true; - perVertex[input] = perVertex[input] || mode < 2u || !IsPixelParameterFlat(*pixel, input); + perVertex[input] = perVertex[input] || mode == 1u || !IsPixelParameterFlat(*pixel, input); } else if (inst->Opcode() == IrOpcode::GetInterpolationParameterF16) { const auto input = inst->Argument(0)->Resolve()->ImmediateU32(); const bool high = inst->Argument(3)->Resolve()->ImmediateU32() != 0u; @@ -408,6 +408,9 @@ void CollectOutputs(const IrProgram& program, ShaderStageInputInfo inputInfo, Sh if (program.Resources().stage == IrShaderStage::Pixel && (exportInfo.en & 0x4u) != 0u && inputInfo.pixel->psSampleMaskExportEnable) { AddOutput(info, StageOutputKind::SampleMask, 0, 0, "gl_SampleMask"); } + if (program.Resources().stage == IrShaderStage::Pixel && (exportInfo.en & 0x2u) != 0u && inputInfo.pixel->psStencilRefExportEnable) { + AddOutput(info, StageOutputKind::StencilRef, 0, 0, "gl_FragStencilRefARB"); + } continue; } if (exportInfo.en == 0u) { diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp index 4703ce48f..8cbadf9cc 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -2066,8 +2066,9 @@ void checkNativeVop3Modifiers(RdnaOpcode opcode, bool permlane, bool carryInOut, const bool sourceModifiers = supportsNativeVop3SourceModifiers(opcode); const bool resultModifiers = supportsNativeVop3ResultModifiers(opcode); const bool clampModifier = supportsNativeVop3Clamp(opcode); + const bool ignoresOutputModifiers = opcode == RdnaOpcode::VCvtI16F16; if (sourceModifiers) { - if (opSel != 0u || (omod != 0u && !resultModifiers) || (clamp != 0u && !clampModifier)) { + if (opSel != 0u || (omod != 0u && !resultModifiers && !ignoresOutputModifiers) || (clamp != 0u && !clampModifier && !ignoresOutputModifiers)) { throw std::invalid_argument("VOP3 source modifiers are not implemented"); } return; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index c37ec986c..7b74fc9ce 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -1,5 +1,6 @@ #include "Recompiler.hpp" #include +#include #include #include #include @@ -12,6 +13,7 @@ #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "ShaderDiskCache.hpp" #include +#include #include #include #include @@ -221,6 +223,16 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat } if (maskedSelectEliminator.Eliminate(program).removedSelects != 0u) { + const bool liveAncillary = std::ranges::any_of(program.Blocks(), [](const auto& block) { + return std::ranges::any_of(block->Instructions(), [](const IrValue* value) { + return value->Opcode() == IrOpcode::GetBuiltin && value->HasUses() && value->Argument(0)->Resolve()->ImmediateU32() == static_cast(StageInputKind::PackedAncillary); + }); + }); + if (liveAncillary) { + constantFolder.Fold(program); + ResolveControlFlowIdentities(program); + deadCodeEliminator.RemoveIdentities(program); + } deadCodeEliminator.Eliminate(program); } }; @@ -292,6 +304,14 @@ struct SourceEntry { namespace { +class PrimedSharedMutex : public std::shared_mutex { +public: + PrimedSharedMutex() { + lock(); + unlock(); + } +}; + struct ResourceProgram { explicit ResourceProgram(const RecompileRequest& request, ShaderPreparationContext* preparation = nullptr) : program(std::make_unique(PrepareResourceProgram(request, preparation))), plan(std::make_shared(ResourceMaterializer{}.ExtractPlan(*program))) {} @@ -320,7 +340,7 @@ bool FailureMemo() { } std::shared_ptr getSource(const RecompileRequest& request, ShaderPreparationContext* preparation = nullptr) { - static std::shared_mutex mutex; + static PrimedSharedMutex mutex; // Entries whose code hashes alike share a bucket; the code comparison picks the right one. static std::unordered_map, std::vector>, SourceKeyHash> sources; struct SourceKeyStorage {}; @@ -398,7 +418,8 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra shaderInfoCollector.Collect(program, inputInfo); constexpr BindingAllocator bindingAllocator; - auto bindings = bindingAllocator.Allocate(program, request.layout); + bool registration = request.target.registrationModules && (request.shader.stage == ShaderStage::Vertex || request.shader.stage == ShaderStage::Fragment); + auto bindings = bindingAllocator.Allocate(program, request.layout, registration); SpirvTargetOptions targetOptions {}; targetOptions.vulkanVersion = request.target.vulkanVersion; @@ -410,11 +431,21 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra targetOptions.nonConstantImageOffsets = request.target.nonConstantImageOffsets; targetOptions.narrowSubgroupClock = request.target.narrowSubgroupClock; targetOptions.float64DenormPreserve = request.target.float64DenormPreserve; + targetOptions.registrationModules = registration; + targetOptions.fragmentDescriptorSet = request.target.registrationModules; constexpr SpirvEmitter spirvEmitter; CompiledShaderArtifact result; result.variantId = nextVariantId(); result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); + if (registration && result.spirv.size() > request.target.registrationWordLimit) { + registration = false; + program.Metadata().bindingLayoutComplete = false; + bindings = bindingAllocator.Allocate(program, request.layout, false); + targetOptions.registrationModules = false; + result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); + } + result.registrationModule = registration; result.bdaAbiVersion = program.Info().usesDma || program.Info().usesFaultBuffer ? request.target.bdaAbiVersion : 0u; result.memoryOffsetDword = bindings.layout.memoryOffsetDword; @@ -589,11 +620,43 @@ struct PreparedBindingPlan { std::once_flag ready; std::exception_ptr failure; DescriptorBindingPlan bindings; - std::shared_mutex mutex; + PrimedSharedMutex mutex; std::map, std::shared_ptr> modules; }; -std::shared_ptr preparedBindingPlan(const CompiledVariant& variant, const ResourceSnapshot& snapshot, std::span exports) { +struct RegistrationModuleEntry { + std::once_flag ready; + std::exception_ptr failure; + std::shared_ptr module; +}; + +std::shared_ptr registrationModule(std::uint64_t variantId, std::span moduleKey, const std::function()>& specialize) { + std::vector key{variantId}; + key.insert(key.end(), moduleKey.begin(), moduleKey.end()); + static std::shared_mutex mutex; + static std::map, std::shared_ptr> modules; + std::shared_ptr entry; + { + std::shared_lock lock(mutex); + if (const auto found = modules.find(key); found != modules.end()) entry = found->second; + } + if (entry == nullptr) { + std::unique_lock lock(mutex); + const auto found = modules.find(key); + entry = found != modules.end() ? found->second : modules.emplace(key, std::make_shared()).first->second; + } + std::call_once(entry->ready, [&] { + try { + entry->module = specialize(); + } catch (...) { + entry->failure = std::current_exception(); + } + }); + if (entry->failure) std::rethrow_exception(entry->failure); + return entry->module; +} + +std::shared_ptr preparedBindingPlan(const CompiledVariant& variant, const ResourceSnapshot& snapshot, std::span exports, bool fragmentDescriptorSet) { struct BindingPlanKeyStorage {}; auto& key = HostThreadLocal, BindingPlanKeyStorage>(); key.clear(); @@ -620,7 +683,7 @@ std::shared_ptr preparedBindingPlan(const CompiledVariant& } key.push_back(exports.size()); for (const auto mapping : exports) key.push_back(mapping); - static std::shared_mutex mutex; + static PrimedSharedMutex mutex; static std::map, std::shared_ptr> plans; std::shared_ptr plan; { @@ -634,7 +697,7 @@ std::shared_ptr preparedBindingPlan(const CompiledVariant& } std::call_once(plan->ready, [&] { try { - plan->bindings = DescriptorBindingBuilder{}.Prepare(variant.bindings.layout, variant.info.info, variant.info.stage, snapshot, exports); + plan->bindings = DescriptorBindingBuilder{}.Prepare(variant.bindings.layout, variant.info.info, variant.info.stage, snapshot, exports, variant.artifact.registrationModule, fragmentDescriptorSet); } catch (...) { plan->failure = std::current_exception(); } @@ -649,7 +712,7 @@ std::shared_ptr specializeModule(const CompiledShaderAr key.clear(); key.push_back(artifact.variantId); for (const auto& constant : constants) key.push_back((static_cast(constant.id) << 32u) | constant.value); - static std::shared_mutex mutex; + static PrimedSharedMutex mutex; static std::map, std::shared_ptr> modules; std::shared_ptr entry; { @@ -672,7 +735,7 @@ std::shared_ptr specializeModule(const CompiledShaderAr return entry->module; } -RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot) { +RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot, bool registrationDefaults = false) { RecompileResult result; const auto& artifact = variant.artifact; static_cast(result) = { @@ -681,6 +744,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil .imageMetadataDword = artifact.imageMetadataDword, .runtimeImageCount = artifact.runtimeImageCount, .runtimeImageResources = artifact.runtimeImageResources, + .registrationModule = artifact.registrationModule, .bdaAbiVersion = artifact.bdaAbiVersion, .runtimeAbiVersion = artifact.runtimeAbiVersion, .vertexInputs = artifact.vertexInputs, @@ -696,7 +760,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil .barycentricEmulation = artifact.barycentricEmulation, .variantId = artifact.variantId }; - const auto plan = preparedBindingPlan(variant, snapshot, request.context.pixel ? std::span(request.context.pixel->targetExportMapping) : std::span{}); + const auto plan = preparedBindingPlan(variant, snapshot, request.context.pixel ? std::span(request.context.pixel->targetExportMapping) : std::span{}, request.target.registrationModules); const DescriptorBindingPlan* bindingPlan = &plan->bindings; std::shared_ptr entry; struct LocalModuleKeyStorage {}; @@ -709,6 +773,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil } result.vertexAttributes.reserve(result.vertexInputs.size()); std::array vertexClasses{}; + std::array runtimeSelectors{}; for (const auto& input : result.vertexInputs) { if (!request.context.vertex || input.location >= request.context.vertex->resourcesNum || input.location >= request.context.vertex->resources.size()) throw std::runtime_error("Shader cache: invalid vertex attribute metadata"); const auto& vertex = *request.context.vertex; @@ -716,7 +781,8 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil const auto fetchIndex = vertex.fetchEmbedded ? vertex.resourcesDst[input.location].fetchIndex : input.fetchIndex; result.vertexAttributes.push_back({input.location, input.components, resource, fetchIndex}); if (!artifact.vertexInputPatches.empty()) { - const auto numeric = VertexInputNumericClass(static_cast((resource.fields[3] >> 12u) & 0x7fu)); + const auto format = static_cast((resource.fields[3] >> 12u) & 0x7fu); + const auto numeric = registrationDefaults && format == IrBufferFormat::Invalid ? IrTextureNumericClass::Float : VertexInputNumericClass(format); if (numeric == IrTextureNumericClass::Unsupported) throw std::runtime_error("unsupported prepared vertex format"); const auto kind = numeric == IrTextureNumericClass::Float ? 0u : numeric == IrTextureNumericClass::Sint ? 1u : 2u; vertexClasses.at(input.location) = kind; @@ -729,7 +795,8 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil if (selector >= 4u) formatComponents = std::max(formatComponents, selector - 3u); selectors |= selector << (component * 3u); } - moduleKey.push_back(selectors); + moduleKey.push_back(artifact.registrationModule ? selectors & ~0xfffu : selectors); + if (artifact.registrationModule) runtimeSelectors.at(input.location / RuntimeAbi::VertexSelectorsPerDword) |= (selectors & 0xfffu) << (input.location % RuntimeAbi::VertexSelectorsPerDword * 16u); } } if (!plan->bindings.specialization.empty() || variant.bindings.layout.UsesPushData() || !artifact.vertexInputPatches.empty() || request.context.pixel) { @@ -760,7 +827,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil constants.push_back({first + 5u, kind}); } } - const auto module = specializeModule(variant.artifact, vertexClasses, constants, request.target); + const auto module = artifact.registrationModule && plan->bindings.specialization.empty() ? registrationModule(artifact.variantId, moduleKey, [&] { return specializeModule(variant.artifact, vertexClasses, constants, request.target); }) : specializeModule(variant.artifact, vertexClasses, constants, request.target); if (variant.bindings.layout.UsesPushData() && !module->pushData) throw std::runtime_error("specialization removed the prepared push constant interface"); auto selected = DescriptorBindingBuilder{}.Select(plan->bindings, module->bindings); entry->bindings = std::move(selected); @@ -786,6 +853,19 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil result.workgroupMemoryDwords = WorkgroupMemoryStrideDwords(variant.info.info); result.bindings = std::move(bindings.bindings); result.pushConstants = std::move(bindings.pushConstants); + const auto& layout = variant.bindings.layout; + if (artifact.registrationModule && request.shader.stage != ShaderStage::Fragment && layout.runtimeStateDwords != 0u) { + const auto first = layout.RuntimeStateDword() + RuntimeAbi::StateVertexSelectors; + if (layout.runtimeStateDwords > runtimeSelectors.size()) throw std::runtime_error("vertex selector state exceeds its capacity"); + if (layout.UsesPushData()) { + if ((first + layout.runtimeStateDwords) * sizeof(std::uint32_t) > result.pushConstants.size()) throw std::runtime_error("vertex selector state lies outside the push data"); + std::memcpy(result.pushConstants.data() + first * sizeof(std::uint32_t), runtimeSelectors.data(), layout.runtimeStateDwords * sizeof(std::uint32_t)); + } else { + const auto data = std::ranges::find_if(result.bindings, [](const DescriptorBinding& binding) { return binding.role == DescriptorRole::ShaderData; }); + if (data == result.bindings.end() || first + layout.runtimeStateDwords > data->guestDescriptor.size()) throw std::runtime_error("vertex selector state lies outside the shader data"); + std::copy_n(runtimeSelectors.begin(), layout.runtimeStateDwords, data->guestDescriptor.begin() + first); + } + } result.poisonedSrtReads = static_cast(snapshot.srtPoison.size()) + snapshot.nullRootReads; return result; } @@ -1174,6 +1254,20 @@ const CompiledShaderArtifact& GetPreparedArtifact(const SourceHandle& handle) { return handle.artifact->artifact; } +std::shared_ptr RegistrationResult(const RecompileRequest& request, const SourceHandle& handle) { + if (handle.artifact == nullptr || !MatchesPreparedShader(request, handle)) throw std::runtime_error("ShaderRecompiler: prepared artifact does not match the static ABI"); + if (!request.target.registrationModules || !handle.artifact->artifact.registrationModule) return nullptr; + const auto& info = handle.artifact->info.info; + if (std::ranges::any_of(info.buffers, [](const BufferResource& buffer) { return buffer.descriptorFormatted; }) || std::ranges::any_of(info.samplers, [](const SamplerResource& sampler) { return FoldsTexelOffsets(sampler); })) return nullptr; + ResourceSnapshot snapshot; + snapshot.buffers.assign(info.buffers.size(), {{}, 4u}); + snapshot.images.assign(info.images.size(), {{}, 8u}); + snapshot.samplers.assign(info.samplers.size(), {{}, 4u}); + snapshot.userData.assign(RuntimeAbi::UserDataCapacity, 0u); + snapshot.flattenedSrt.assign(1u, 0u); + return std::make_shared(materializeResult(*handle.artifact, request, snapshot, true)); +} + std::shared_ptr MaterializeShader(const RecompileRequest& request, const ResourceCapture& capture, const SourceHandle& handle) { if (!MatchesPreparedShader(request, handle)) throw std::runtime_error("ShaderRecompiler: prepared artifact does not match the static ABI"); if (capture.source != handle.source || capture.plan != handle.source->plan) throw std::runtime_error("ShaderRecompiler: resource capture belongs to another prepared shader"); diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 85f9689e8..121ce4fce 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -141,6 +141,7 @@ struct ShaderPixelStageInfo { std::array targetExportPacking; bool dualSourceBlend; bool alphaToCoverage; + bool stencilRefExportEnable; }; struct ShaderVertexBufferResource { @@ -229,6 +230,8 @@ struct SpirvTarget { bool narrowSubgroupClock = false; bool fixedPushSlots = false; bool float64DenormPreserve = false; + bool registrationModules = false; + std::uint32_t registrationWordLimit = 200000u; }; struct BindingLayout { @@ -388,6 +391,7 @@ struct FragmentParameter { bool flat; bool perVertex; bool custom = false; + bool flatP10 = false; }; struct BarycentricEmulation { @@ -475,6 +479,7 @@ struct CompiledShaderArtifact { std::uint32_t imageMetadataDword = 0; std::uint32_t runtimeImageCount = 0; std::vector runtimeImageResources; + bool registrationModule = false; std::uint32_t bdaAbiVersion = 0; std::uint32_t runtimeAbiVersion = RuntimeAbi::Version; std::vector vertexInputs; diff --git a/core/shader/recompiler/RuntimeAbi.hpp b/core/shader/recompiler/RuntimeAbi.hpp index 131372e68..eb5ce8cfc 100644 --- a/core/shader/recompiler/RuntimeAbi.hpp +++ b/core/shader/recompiler/RuntimeAbi.hpp @@ -11,6 +11,7 @@ namespace ShaderRecompiler::RuntimeAbi { inline constexpr std::uint32_t Version = 11u; inline constexpr std::uint32_t DescriptorSet = 0u; +inline constexpr std::uint32_t FragmentDescriptorSet = 1u; inline constexpr std::uint32_t StageCount = 4u; inline constexpr std::uint32_t PushConstantDwords = 32u; inline constexpr std::uint32_t FirstImageBinding = 1u; @@ -39,6 +40,13 @@ inline constexpr std::uint32_t BindlessTableSlots = 16u; inline constexpr std::uint32_t StorageMipSlots = 4u; inline constexpr std::uint32_t StorageHeapCapacity = 16u; inline constexpr std::uint32_t SamplerHeapCapacity = 32u; +inline constexpr std::uint32_t StateExportMappings = 0u; +inline constexpr std::uint32_t StateDwords = 2u; +inline constexpr std::uint32_t StateVertexSelectors = 0u; +inline constexpr std::uint32_t VertexSelectorsPerDword = 2u; +inline constexpr std::uint32_t ImageModeMask = 0xffu; +inline constexpr std::uint32_t CompareStateShift = 9u; +inline constexpr std::uint32_t CompareStateMask = 0x1fffu; struct ResourceMetadata { std::uint32_t binding; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 203770688..1d85b6668 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -64,9 +64,9 @@ std::filesystem::path ShaderCacheDirectory() { namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(CompiledShaderArtifact) == 192, "CompiledShaderArtifact changed: update the artifact encoder"); +static_assert(sizeof(CompiledShaderArtifact) == 200, "CompiledShaderArtifact changed: update the artifact encoder"); static_assert(sizeof(ShaderInvocation) == 112, "ShaderInvocation changed: update the invocation encoder"); -static_assert(sizeof(RecompileResult) == 312, "RecompileResult changed: update EncodeResult and DecodeResult"); +static_assert(sizeof(RecompileResult) == 320, "RecompileResult changed: update EncodeResult and DecodeResult"); static_assert(sizeof(DescriptorBinding) == 568, "DescriptorBinding changed: update the binding encoder"); static_assert(sizeof(VertexAttribute) == 32, "VertexAttribute changed: update the attribute encoder"); static_assert(sizeof(VertexInput) == 16, "VertexInput changed: update the vertex input encoder"); @@ -297,6 +297,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { writer.Value(result.imageMetadataDword); writer.Value(result.runtimeImageCount); writer.Values(std::span(result.runtimeImageResources)); + writer.Value(result.registrationModule); writer.List(result.vertexInputPatches, [](Writer& out, const VertexInputPatch& patch) { out.Value(patch.location); out.Value(patch.word); @@ -322,6 +323,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { out.Value(parameter.flat); out.Value(parameter.perVertex); out.Value(parameter.custom); + out.Value(parameter.flatP10); }); writer.Value(result.barycentricEmulation.active); writer.Value(result.barycentricEmulation.smooth); @@ -339,6 +341,7 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { reader.Value(result.imageMetadataDword); reader.Value(result.runtimeImageCount); reader.Values(result.runtimeImageResources); + reader.Value(result.registrationModule); reader.List(result.vertexInputPatches, 20, [](Reader& in, VertexInputPatch& patch) { in.Value(patch.location); in.Value(patch.word); @@ -364,6 +367,7 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { in.Value(parameter.flat); in.Value(parameter.perVertex); in.Value(parameter.custom); + in.Value(parameter.flatP10); }); reader.Value(result.barycentricEmulation.active); reader.Value(result.barycentricEmulation.smooth); @@ -416,6 +420,7 @@ void encodeLayout(Writer& writer, const IrBindingLayout& layout) { writer.Value(layout.memoryOffsetCount); writer.Value(layout.dispatchThreadLimit); writer.Value(layout.runtimeImageCount); + writer.Value(layout.runtimeStateDwords); writer.Values(std::span(layout.userDataRegisters)); writer.List(layout.descriptors, [](Writer& out, const IrDescriptorBinding& descriptor) { out.Value(descriptor.kind); @@ -429,6 +434,7 @@ void decodeLayout(Reader& reader, IrBindingLayout& layout) { reader.Value(layout.memoryOffsetCount); reader.Value(layout.dispatchThreadLimit); reader.Value(layout.runtimeImageCount); + reader.Value(layout.runtimeStateDwords); reader.Values(layout.userDataRegisters); reader.List(layout.descriptors, 12, [](Reader& in, IrDescriptorBinding& descriptor) { in.Value(descriptor.kind); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index 0d18fb6e5..65cb02117 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -21,6 +21,8 @@ struct SpirvTargetOptions { bool nonConstantImageOffsets = false; bool narrowSubgroupClock = false; bool float64DenormPreserve = false; + bool registrationModules = false; + bool fragmentDescriptorSet = false; }; class SpirvEmitter { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index a876a4062..a26c20a54 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -90,6 +91,8 @@ struct SpirvEmitterState { bool splitSubgroup = false; bool narrowSubgroupClock = false; bool float64DenormPreserve = false; + bool registrationModules = false; + bool fragmentDescriptorSet = false; std::unordered_set sharedLaneValues; std::uint32_t laneHalf = 0; // The target's SPIR-V version and what the device accepts, for capabilities an emitter adds @@ -164,11 +167,13 @@ struct SpirvEmitterState { std::uint32_t perVertexVariable = 0; std::uint32_t pointSizeVariable = 0; std::uint32_t clipDistanceVariable = 0; + std::optional zeroPositionClipIndex; std::uint32_t cullDistanceVariable = 0; std::uint32_t layerVariable = 0; std::uint32_t viewportIndexVariable = 0; std::uint32_t depthVariable = 0; std::uint32_t sampleMaskVariable = 0; + std::uint32_t stencilRefVariable = 0; std::vector inputs; std::vector outputs; std::vector interfaceVariables; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp index 1650714e3..7f726e995 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp @@ -7,6 +7,7 @@ namespace ShaderRecompiler { +std::uint32_t RuntimeImageDword(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t member); std::uint32_t ResourceForDescriptor(const SpirvEmitterState& state, DescriptorBindingKind kind, std::uint32_t resource); std::uint32_t DescriptorElementPointer(SpirvEmitterState& state, std::uint32_t resultPtrType, std::uint32_t variableId, std::uint32_t arrayIndex, DescriptorBindingKind kind, std::uint32_t resource, const char* variableName); std::uint32_t ImageScalarType(SpirvEmitterState& state, IrTextureNumericClass numericClass); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index 2e2d30558..217c580b0 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -1,5 +1,6 @@ #include "SpirvBackend/SpirvEmitterInstructions.hpp" #include "SpirvBackend/SpirvMemory/SpirvSubgroup.hpp" +#include "IntermediateRepresentation/IrBlock.hpp" #include "IntermediateRepresentation/IrValue.hpp" #include #include @@ -227,8 +228,31 @@ struct HalfWaveScan { const IrValue* source = nullptr; }; +const IrValue* StoredWindowValue(const IrValue* load) { + const auto* slot = load->Argument(0)->Resolve(); + const auto* block = load->Parent(); + if (slot == nullptr || !slot->HasImmediate() || block == nullptr) return nullptr; + const auto& instructions = block->Instructions(); + auto at = std::find(instructions.begin(), instructions.end(), load); + if (at == instructions.end()) return nullptr; + while (at != instructions.begin()) { + const auto* previous = *--at; + if (previous->HasImmediate() || previous->Opcode() != IrOpcode::StoreVectorWindow) continue; + const auto* stored = previous->Argument(0)->Resolve(); + if (stored == nullptr || !stored->HasImmediate()) return nullptr; + if (stored->ImmediateU32() == slot->ImmediateU32()) return previous->Argument(1)->Resolve(); + } + return nullptr; +} + const IrValue* Resolved(const IrValue* value) { - return value != nullptr ? value->Resolve() : nullptr; + value = value != nullptr ? value->Resolve() : nullptr; + while (value != nullptr && !value->HasImmediate() && value->Opcode() == IrOpcode::LoadVectorWindow) { + const auto* stored = StoredWindowValue(value); + if (stored == nullptr) break; + value = stored; + } + return value; } bool Is(const IrValue* value, IrOpcode opcode) { @@ -859,6 +883,31 @@ std::uint32_t EmitFPLog2(SpirvEmitterState& state, std::uint32_t arg0) { return Select(state, TypeF32(state), Binary(state, spv::OpLogicalAnd, TypeBool(state), near, far), series, value); } +std::uint32_t SinTurnSeries(SpirvEmitterState& state, std::uint32_t turns) { + const auto square = Exact(state, Binary(state, spv::OpFMul, TypeF32(state), turns, turns)); + auto polynomial = ConstantF32(state, 0xc17183a8u); + for (const std::uint32_t coefficient : {0x42283c1au, 0xc2996966u, 0x42a335e3u, 0xc2255de7u, 0x40c90fdbu}) { + polynomial = Exact(state, EmitExt(state, TypeF32(state), GLSLstd450Fma, {polynomial, square, ConstantF32(state, coefficient)})); + } + return Exact(state, Binary(state, spv::OpFMul, TypeF32(state), turns, polynomial)); +} + +std::uint32_t CosTurnSeries(SpirvEmitterState& state, std::uint32_t turns) { + const auto square = Exact(state, Binary(state, spv::OpFMul, TypeF32(state), turns, turns)); + auto polynomial = ConstantF32(state, 0x40fce9c5u); + for (const std::uint32_t coefficient : {0xc1d368f9u, 0x4270fa83u, 0xc2aae9e4u, 0x4281e0f8u, 0xc19de9e6u, 0x3f800000u}) { + polynomial = Exact(state, EmitExt(state, TypeF32(state), GLSLstd450Fma, {polynomial, square, ConstantF32(state, coefficient)})); + } + return polynomial; +} + +std::uint32_t SeriesWhenFar(SpirvEmitterState& state, std::uint32_t value, std::uint32_t series) { + const auto valueBits = Unary(state, spv::OpBitcast, TypeU32(state), value); + const auto seriesBits = Unary(state, spv::OpBitcast, TypeU32(state), series); + const auto distance = EmitExt(state, TypeU32(state), GLSLstd450UMin, {Binary(state, spv::OpISub, TypeU32(state), valueBits, seriesBits), Binary(state, spv::OpISub, TypeU32(state), seriesBits, valueBits)}); + return Select(state, TypeF32(state), Binary(state, spv::OpUGreaterThan, TypeBool(state), distance, ConstantU32(state, 4u)), series, value); +} + std::uint32_t TrigLinearNearZero(SpirvEmitterState& state, std::uint32_t value, std::uint32_t distance, std::uint32_t linearConstant) { const auto linear = Binary(state, spv::OpFMul, TypeF32(state), distance, linearConstant); const auto magnitude = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Unary(state, spv::OpBitcast, TypeU32(state), distance), ConstantU32(state, 0x7fffffffu)); @@ -870,9 +919,11 @@ std::uint32_t EmitFPSin(SpirvEmitterState& state, std::uint32_t arg0) { const auto cycle = EmitTrigCycleF32(state, arg0, true); const auto absolute = EmitExt(state, TypeF32(state), GLSLstd450FAbs, {cycle}); const auto signBits = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Unary(state, spv::OpBitcast, TypeU32(state), cycle), ConstantU32(state, 0x80000000u)); - const auto value = EmitExt(state, TypeF32(state), GLSLstd450Sin, {Binary(state, spv::OpFMul, TypeF32(state), absolute, ConstantF32(state, 0x40c90fdbu))}); const auto nearHalf = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), absolute, ConstantF32(state, 0x3e800000u)); - const auto distance = Select(state, TypeF32(state), nearHalf, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3f000000u), absolute), absolute); + const auto distance = Select(state, TypeF32(state), nearHalf, Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3f000000u), absolute)), absolute); + const auto pastEighth = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), distance, ConstantF32(state, 0x3e000000u)); + const auto series = Select(state, TypeF32(state), pastEighth, CosTurnSeries(state, Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3e800000u), distance))), SinTurnSeries(state, distance)); + const auto value = SeriesWhenFar(state, EmitExt(state, TypeF32(state), GLSLstd450Sin, {Binary(state, spv::OpFMul, TypeF32(state), absolute, ConstantF32(state, 0x40c90fdbu))}), series); const auto linearConstant = Select(state, TypeF32(state), nearHalf, ConstantF32(state, 0x40c90fdbu), ConstantF32(state, 0x40c90fd5u)); const auto result = TrigLinearNearZero(state, value, distance, linearConstant); return Unary(state, spv::OpBitcast, TypeF32(state), Binary(state, spv::OpBitwiseOr, TypeU32(state), Unary(state, spv::OpBitcast, TypeU32(state), result), signBits)); @@ -881,8 +932,14 @@ std::uint32_t EmitFPSin(SpirvEmitterState& state, std::uint32_t arg0) { std::uint32_t EmitFPCos(SpirvEmitterState& state, std::uint32_t arg0) { const auto cycle = EmitTrigCycleF32(state, arg0, false); const auto absolute = EmitExt(state, TypeF32(state), GLSLstd450FAbs, {cycle}); - const auto value = EmitExt(state, TypeF32(state), GLSLstd450Cos, {Binary(state, spv::OpFMul, TypeF32(state), absolute, ConstantF32(state, 0x40c90fdbu))}); - const auto distance = Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3e800000u), absolute); + const auto distance = Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3e800000u), absolute)); + const auto low = CosTurnSeries(state, absolute); + const auto middle = SinTurnSeries(state, distance); + const auto high = Unary(state, spv::OpFNegate, TypeF32(state), CosTurnSeries(state, Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3f000000u), absolute)))); + const auto pastEighth = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), absolute, ConstantF32(state, 0x3e000000u)); + const auto pastThreeEighths = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), absolute, ConstantF32(state, 0x3ec00000u)); + const auto series = Select(state, TypeF32(state), pastThreeEighths, high, Select(state, TypeF32(state), pastEighth, middle, low)); + const auto value = SeriesWhenFar(state, EmitExt(state, TypeF32(state), GLSLstd450Cos, {Binary(state, spv::OpFMul, TypeF32(state), absolute, ConstantF32(state, 0x40c90fdbu))}), series); return TrigLinearNearZero(state, value, distance, ConstantF32(state, 0x40c90fdbu)); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 8a0951153..a3be534ad 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -250,6 +250,8 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.nonConstantImageOffsets = target.nonConstantImageOffsets; state.narrowSubgroupClock = target.narrowSubgroupClock; state.float64DenormPreserve = target.float64DenormPreserve; + state.registrationModules = target.registrationModules; + state.fragmentDescriptorSet = target.fragmentDescriptorSet; state.hostSubgroupSize = target.subgroupSize; state.splitSubgroup = program.WaveSize() == 32u && target.subgroupSize > 32u; if (state.splitSubgroup && (state.requirements.subgroupBallot || state.requirements.subgroupShuffle)) state.requirements.subgroupLocalInvocationId = true; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 5e209712a..d380b33fc 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -4,6 +4,7 @@ #include "SpirvBackend/SpirvBda.hpp" #include "SpirvBackend/SpirvBufferFormat.hpp" #include "SpirvBackend/SpirvEmitterInstructions.hpp" +#include "SpirvBackend/SpirvMemory/SpirvDescriptors.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "RdnaDecoder/RdnaImageOpDecoder.hpp" #include @@ -46,18 +47,13 @@ struct SampleSetup { std::uint32_t coord; }; -std::uint32_t RuntimeImageDword(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t member) { - const auto index = state.runtimeImageMetadata != 0u ? state.runtimeImageMetadata : ConstantU32(state, resource); - const auto dword = Binary(state, spv::OpIAdd, TypeU32(state), ConstantU32(state, state.program.Metadata().bindings.ImageMetadataDword() + member), Binary(state, spv::OpIMul, TypeU32(state), index, ConstantU32(state, sizeof(RuntimeAbi::ResourceMetadata) / sizeof(std::uint32_t)))); - const auto pointer = state.module.AllocateId(); - state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), pointer, state.shaderDataStorageVariable, ConstantU32(state, 0u), dword); - const auto value = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); - return value; +std::uint32_t RuntimeImageMode(SpirvEmitterState& state, std::uint32_t resource) { + const auto mode = Binary(state, spv::OpShiftRightLogical, TypeU32(state), RuntimeImageDword(state, resource, offsetof(RuntimeAbi::ResourceMetadata, flags) / sizeof(std::uint32_t)), ConstantU32(state, 1u)); + return state.registrationModules ? Binary(state, spv::OpBitwiseAnd, TypeU32(state), mode, ConstantU32(state, RuntimeAbi::ImageModeMask)) : mode; } std::uint32_t RuntimeImageSwizzle(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t component) { - if (state.program.Info().images.at(resource).indirectRoot == ImageResource::NoIndirectImage) return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + resource * PipelineSpecialization::ImageWords + 1u + component, component + 4u); + if (state.program.Info().images.at(resource).indirectRoot == ImageResource::NoIndirectImage && !state.registrationModules) return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + resource * PipelineSpecialization::ImageWords + 1u + component, component + 4u); const auto descriptor = RuntimeImageDword(state, resource, offsetof(RuntimeAbi::ResourceMetadata, descriptor) / sizeof(std::uint32_t) + 3u); return Binary(state, spv::OpBitwiseAnd, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeU32(state), descriptor, ConstantU32(state, component * 3u)), ConstantU32(state, 7u)); } @@ -1273,7 +1269,12 @@ void EmitEmulatedCompareSample(SpirvValueEmitContext& ctx, const ImageEmitAccess auto& state = ctx.state; const auto& mem = access.mem; const auto& image = access.image; - const auto parameter = [&](std::uint32_t word) { return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::CompareBase + mem.resource * PipelineSpecialization::CompareWords + word, 0u); }; + const auto compare = state.registrationModules ? EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), RuntimeImageDword(state, mem.resource, offsetof(RuntimeAbi::ResourceMetadata, flags) / sizeof(std::uint32_t)), ConstantU32(state, RuntimeAbi::CompareStateShift)), RuntimeAbi::CompareStateMask) : 0u; + const auto parameter = [&](std::uint32_t word) { + if (!state.registrationModules) return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::CompareBase + mem.resource * PipelineSpecialization::CompareWords + word, 0u); + constexpr std::array, 6> fields{{{EmulatedCompare::FunctionShift, 7u}, {4u, 1u}, {EmulatedCompare::ClampXShift, 3u}, {EmulatedCompare::ClampYShift, 3u}, {EmulatedCompare::ReferenceShift, 3u}, {12u, 1u}}}; + return EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), compare, ConstantU32(state, fields.at(word).first)), fields.at(word).second); + }; const auto equal = [&](std::uint32_t value, std::uint32_t literal) { return Binary(state, spv::OpIEqual, TypeBool(state), value, ConstantU32(state, literal)); }; if (access.slot != 0) ctx.Fail(access.inst, "compares against a color texture through a bindless image table, which is not implemented"); if (HasFlag(mem, RdnaImageSampleFlagDerivative) || HasFlag(mem, RdnaImageSampleFlagLod)) ctx.Fail(access.inst, "compares against a color texture with gradients or an explicit LOD, which is not implemented"); @@ -1653,7 +1654,7 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) { state.runtimeImageMetadata = 0u; return; } - const auto selector = base.indirectRoot == ImageResource::NoIndirectImage ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + memory.resource * PipelineSpecialization::ImageWords, 0u) : Binary(state, spv::OpShiftRightLogical, TypeU32(state), RuntimeImageDword(state, memory.resource, offsetof(RuntimeAbi::ResourceMetadata, flags) / sizeof(std::uint32_t)), ConstantU32(state, 1u)); + const auto selector = base.indirectRoot == ImageResource::NoIndirectImage && !state.registrationModules ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + memory.resource * PipelineSpecialization::ImageWords, 0u) : RuntimeImageMode(state, memory.resource); const bool returnsValue = inst.Opcode() != IrOpcode::ImageWrite; const auto resultId = returnsValue ? ctx.Result(inst) : 0u; ctx.definitions.erase(&inst); @@ -1677,7 +1678,7 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) { for (std::uint32_t index = 0u; index < modes.size(); ++index) { EmitLabel(state, labels[index]); std::uint32_t modeMerge = 0u; - if (base.indirectRoot != ImageResource::NoIndirectImage) { + if (base.indirectRoot != ImageResource::NoIndirectImage && !state.registrationModules) { const auto enabled = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageModeBase + memory.resource * PipelineSpecialization::ImageModeStride + index, 1u); const auto active = Binary(state, spv::OpINotEqual, TypeBool(state), enabled, ConstantU32(state, 0u)); const auto activeLabel = state.module.AllocateId(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp index b1a37bb57..4989ed437 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp @@ -50,11 +50,28 @@ std::uint32_t DescriptorElementPointer(SpirvEmitterState& state, std::uint32_t r } const auto pointer = state.module.AllocateId(); const bool heap = kind == DescriptorBindingKind::Samplers || ImageBindingResourceClass(kind) != ImageResourceClass::None; - const auto index = heap ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::DescriptorIndex(static_cast(kind), arrayIndex), arrayIndex) : ConstantU32(state, arrayIndex); + const auto index = heap && !state.registrationModules ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::DescriptorIndex(static_cast(kind), arrayIndex), arrayIndex) : ConstantU32(state, arrayIndex); state.module.AddFunction(spv::OpAccessChain, resultPtrType, pointer, variableId, index); return pointer; } +std::uint32_t RuntimeImageDword(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t member) { + const auto index = state.runtimeImageMetadata != 0u ? state.runtimeImageMetadata : ConstantU32(state, resource); + const auto dword = Binary(state, spv::OpIAdd, TypeU32(state), ConstantU32(state, state.program.Metadata().bindings.ImageMetadataDword() + member), Binary(state, spv::OpIMul, TypeU32(state), index, ConstantU32(state, sizeof(RuntimeAbi::ResourceMetadata) / sizeof(std::uint32_t)))); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), pointer, state.shaderDataStorageVariable, ConstantU32(state, 0u), dword); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); + return value; +} + +std::uint32_t RuntimeMipCount(SpirvEmitterState& state, std::uint32_t resource) { + const auto word = RuntimeImageDword(state, resource, offsetof(RuntimeAbi::ResourceMetadata, descriptor) / sizeof(std::uint32_t) + 3u); + const auto first = EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), word, ConstantU32(state, 12u)), 0xfu); + const auto last = EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), word, ConstantU32(state, 16u)), 0xfu); + return Binary(state, spv::OpIAdd, TypeU32(state), Binary(state, spv::OpISub, TypeU32(state), last, first), ConstantU32(state, 1u)); +} + std::uint32_t TableImageIndex(SpirvEmitterState& state, DescriptorBindingKind kind, std::uint32_t resource) { const bool storage = ImageBindingResourceClass(kind) == ImageResourceClass::Storage; const auto supported = [&](std::uint32_t capability) { @@ -240,6 +257,7 @@ void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std if (image.mipCount == 0u) { ExitDescriptorBindingFailure(state, kind, resource, "dynamic storage image has no mip descriptors"); } + const auto count = state.registrationModules ? RuntimeMipCount(state, resource) : state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::MipCountBase + resource, image.mipCount); const auto mergeLabel = state.module.AllocateId(); std::vector labels(image.mipCount); std::vector words{spv::OpSwitch, mipLod, mergeLabel}; @@ -252,7 +270,6 @@ void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std state.module.AddFunction(std::span(words)); for (std::uint32_t mip = 0; mip < image.mipCount; mip++) { EmitLabel(state, labels[mip]); - const auto count = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::MipCountBase + resource, image.mipCount); const auto active = Binary(state, spv::OpULessThan, TypeBool(state), ConstantU32(state, mip), count); const auto writeLabel = state.module.AllocateId(); const auto nextLabel = state.module.AllocateId(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 3cd67e86d..30fd199ca 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -291,6 +291,8 @@ void DefineInputs(SpirvEmitterState& state) { } } +constexpr std::uint32_t MaxClipDistances = 8u; + void DefineOutputs(SpirvEmitterState& state) { state.outputs.reserve(state.program.Info().outputs.size()); std::uint32_t clipDistanceCount = 0u; @@ -307,6 +309,18 @@ void DefineOutputs(SpirvEmitterState& state) { DefineMeshOutputs(state); return; } + const auto stage = state.program.Resources().stage; + const bool exportsPosition = std::ranges::any_of(state.outputs, [](const SpirvOutputBinding& output) { return output.kind == StageOutputKind::Position; }); + if (exportsPosition && (stage == IrShaderStage::Vertex || stage == IrShaderStage::TessellationEvaluation)) { + if (std::ranges::find(state.supportedCapabilities, spv::CapabilityClipDistance) == state.supportedCapabilities.end()) { + FailEmit("clipping a primitive with an all-zero clip position needs shaderClipDistance on the target device"); + } + state.zeroPositionClipIndex = clipDistanceCount; + clipDistanceCount += 1u; + if (clipDistanceCount > MaxClipDistances) { + FailEmit("guest clip distances plus the all-zero clip position distance exceed " + std::to_string(MaxClipDistances) + " clip distances"); + } + } const auto BuiltIn = [&](std::uint32_t& variable, std::uint32_t type, const char* name, std::uint32_t builtin) { if (variable == 0u) { variable = DefineInterfaceVariable(state, type, spv::StorageClassOutput, name); @@ -345,6 +359,9 @@ void DefineOutputs(SpirvEmitterState& state) { case StageOutputKind::SampleMask: binding.variableId = BuiltIn(state.sampleMaskVariable, SampleMaskArrayType(state), "gl_SampleMask", spv::BuiltInSampleMask); break; + case StageOutputKind::StencilRef: + binding.variableId = BuiltIn(state.stencilRefVariable, TypeI32(state), "gl_FragStencilRefARB", spv::BuiltInFragStencilRefEXT); + break; case StageOutputKind::Parameter: case StageOutputKind::Mrt: { const bool uintOutput = binding.kind == StageOutputKind::Mrt && state.program.Resources().stage == IrShaderStage::Pixel && binding.index < std::size(PixelInfo(state).targetOutputMode) && PixelInfo(state).targetOutputMode[binding.index] == 7u; @@ -355,6 +372,9 @@ void DefineOutputs(SpirvEmitterState& state) { } } } + if (state.zeroPositionClipIndex) { + BuiltIn(state.clipDistanceVariable, F32ArrayType(state, clipDistanceCount), "gl_ClipDistance", spv::BuiltInClipDistance); + } if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->psAlphaToCoverage) { if (state.inputInfo.pixel->psSampleMaskExportEnable) throw std::runtime_error("alpha-to-coverage with a sample-mask export is unsupported"); BuiltIn(state.sampleMaskVariable, SampleMaskArrayType(state), "gl_SampleMask", spv::BuiltInSampleMask); @@ -374,14 +394,14 @@ void DefineDescriptors(SpirvEmitterState& state) { const auto Define = [&](std::uint32_t type, const char* name, std::uint32_t storage = spv::StorageClassStorageBuffer) { const auto variable = state.module.DefineGlobalVariable(TypePointer(state, storage, type), storage); state.module.AddName(variable, name); - state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationDescriptorSet, 0u); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationDescriptorSet, state.fragmentDescriptorSet && stage == IrShaderStage::Pixel ? RuntimeAbi::FragmentDescriptorSet : RuntimeAbi::DescriptorSet); state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBinding, NativeBinding(stage, binding.kind)); return variable; }; const auto ArrayType = [&](std::uint32_t type) { const bool heap = binding.kind == DescriptorBindingKind::Samplers || ImageBindingResourceClass(binding.kind) != ImageResourceClass::None; const auto count = static_cast(binding.resources.size()); - const auto length = heap ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::HeapCountBase + static_cast(binding.kind), count) : ConstantU32(state, count); + const auto length = heap && !state.registrationModules ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::HeapCountBase + static_cast(binding.kind), count) : ConstantU32(state, count); return state.module.Type(spv::OpTypeArray, type, length); }; switch (binding.kind) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp index 2c3eb07f5..a7a465d86 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp @@ -48,11 +48,25 @@ namespace ShaderRecompiler if (inputInfo.pixel == nullptr) FailEmit("pixel input info is missing"); const auto& pixel = *inputInfo.pixel; std::vector result; + std::array readsP10{}; + for (const auto& block : program.Blocks()) { + for (const auto* instruction : block->Instructions()) { + if (instruction->Opcode() != IrOpcode::GetInterpolationParameter || instruction->Argument(2)->Resolve()->ImmediateU32() != 0u) continue; + const auto attribute = instruction->Argument(0)->Resolve()->ImmediateU32(); + if (attribute < readsP10.size()) readsP10[attribute] = true; + } + } for (const auto& input : program.Info().inputs) { if (input.kind != StageInputKind::Parameter) continue; const auto location = PixelInputLocation(pixel, input.location); - if (std::any_of(result.begin(), result.end(), [&](const auto& parameter) { return parameter.location == location; })) continue; - result.push_back({location, location, PixelInputIsFlat(pixel, input.location), input.perVertex, PixelInputIsCustom(pixel, input.location)}); + const bool flat = PixelInputIsFlat(pixel, input.location); + const bool flatP10 = flat && !input.perVertex && input.location < readsP10.size() && readsP10[input.location]; + const auto existing = std::find_if(result.begin(), result.end(), [&](const auto& parameter) { return parameter.location == location; }); + if (existing != result.end()) { + existing->flatP10 = existing->flatP10 || flatP10; + continue; + } + result.push_back({location, location, flat, input.perVertex, PixelInputIsCustom(pixel, input.location), flatP10}); } return result; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 699290232..a6ada9eba 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -185,7 +185,15 @@ void DefineModule(SpirvEmitterState& state) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDepthGreater); } } - if (pixel.psEarlyZ && !pixel.psPixelKillEnable && !pixel.psDepthExportEnable && !pixel.psSampleMaskExportEnable && !pixel.psAlphaToCoverage) { + if (state.stencilRefVariable != 0) { + if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityStencilExportEXT)) == state.supportedCapabilities.end()) { + throw std::runtime_error("a pixel shader that exports the stencil reference needs VK_EXT_shader_stencil_export, which the device lacks"); + } + state.module.EmitCapability(spv::CapabilityStencilExportEXT); + state.module.EmitExtension("SPV_EXT_shader_stencil_export"); + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeStencilRefReplacingEXT); + } + if (pixel.psEarlyZ && !pixel.psPixelKillEnable && !pixel.psDepthExportEnable && !pixel.psSampleMaskExportEnable && !pixel.psStencilRefExportEnable && !pixel.psAlphaToCoverage) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests); } if (pixel.psOrderedPixelShader) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 6124580ce..236724c00 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -417,6 +417,32 @@ void EmitAuxPositionExport(SpirvValueEmitContext& ctx, std::uint32_t data, const } } +void StoreZeroPositionClipDistance(SpirvEmitterState& state, std::uint32_t position) { + const auto bits = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitcast, TypeU32Vector(state, 4u), bits, position); + std::uint32_t combined = 0u; + for (std::uint32_t component = 0; component < 4u; component++) { + const auto raw = state.module.AllocateId(); + const auto magnitude = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), raw, bits, component); + state.module.AddFunction(spv::OpBitwiseAnd, TypeU32(state), magnitude, raw, ConstantU32(state, 0x7fffffffu)); + if (component == 0u) { + combined = magnitude; + continue; + } + const auto merged = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitwiseOr, TypeU32(state), merged, combined, magnitude); + combined = merged; + } + const auto allZero = state.module.AllocateId(); + const auto distance = state.module.AllocateId(); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpIEqual, TypeBool(state), allZero, combined, ConstantU32(state, 0u)); + state.module.AddFunction(spv::OpSelect, TypeF32(state), distance, allZero, ConstantF32Value(state, -1.0f), ConstantF32Value(state, 0.0f)); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeF32(state)), pointer, state.clipDistanceVariable, ConstantU32(state, *state.zeroPositionClipIndex)); + state.module.AddFunction(spv::OpStore, pointer, distance); +} + std::uint32_t ConvertClipCoordinate(SpirvEmitterState& state, std::uint32_t coordinate, float scale, float offset, float halfExtent) { const auto window = state.module.AllocateId(); const auto biased = state.module.AllocateId(); @@ -642,6 +668,10 @@ void DefineMeshOutputs(SpirvEmitterState& state) { state.module.AddAnnotation(spv::OpDecorate, state.meshPrimitives, spv::DecorationBuiltIn, spv::BuiltInPrimitiveTriangleIndicesEXT); state.module.AddAnnotation(spv::OpDecorate, state.meshCull, spv::DecorationBuiltIn, spv::BuiltInCullPrimitiveEXT); state.module.AddAnnotation(spv::OpDecorate, state.meshCull, spv::DecorationPerPrimitiveEXT); + if (state.numWorkgroupsVariable == 0u) { + state.numWorkgroupsVariable = DefineInterfaceVariable(state, TypeU32Vector(state, 3u), spv::StorageClassInput, "gl_NumWorkGroups"); + state.module.AddAnnotation(spv::OpDecorate, state.numWorkgroupsVariable, spv::DecorationBuiltIn, spv::BuiltInNumWorkgroups); + } } void EmitMeshEntryPoint(SpirvEmitterState& state) { @@ -666,7 +696,15 @@ void EmitMeshEntryPoint(SpirvEmitterState& state) { state.module.AddFunction(spv::OpExtInst, TypeU32(state), result, GlslStd450(state), GLSLstd450UMin, value, ConstantU32(state, limit)); return result; }; - const auto vertices = clamp(MeshLoad(state, state.meshAllocation, spv::StorageClassWorkgroup, TypeU32(state), ConstantU32(state, 0u)), mesh.maxVertices); + const auto groupsX = state.module.AllocateId(); + const auto groupsXPointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeU32(state)), groupsXPointer, state.numWorkgroupsVariable, ConstantU32(state, 0u)); + state.module.AddFunction(spv::OpLoad, TypeU32(state), groupsX, groupsXPointer); + const auto noGroups = state.module.AllocateId(); + state.module.AddFunction(spv::OpIEqual, TypeBool(state), noGroups, groupsX, ConstantU32(state, 0u)); + const auto allocatedVertices = clamp(MeshLoad(state, state.meshAllocation, spv::StorageClassWorkgroup, TypeU32(state), ConstantU32(state, 0u)), mesh.maxVertices); + const auto vertices = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelect, TypeU32(state), vertices, noGroups, ConstantU32(state, 0u), allocatedVertices); const auto primitives = clamp(MeshLoad(state, state.meshAllocation, spv::StorageClassWorkgroup, TypeU32(state), ConstantU32(state, 1u)), mesh.maxPrimitives); state.module.AddFunction(spv::OpSetMeshOutputsEXT, vertices, primitives); const auto anyVertex = state.module.AllocateId(); @@ -1212,7 +1250,7 @@ std::uint32_t EmitGetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) const auto component = inst.Argument(1)->ImmediateU32(); if (state.inputInfo.vertex == nullptr || !state.inputInfo.vertex->fetchEmbedded || component >= 4u) ctx.Fail(inst, "invalid prepared vertex attribute"); const auto first = PipelineSpecialization::VertexBase + attribute * PipelineSpecialization::VertexWords; - const auto selector = state.module.SpecializationConstant(TypeU32(state), first + component, component + 4u); + const auto selector = state.registrationModules ? EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), EmitShaderDataDwordLoad(state, state.program.Metadata().bindings.RuntimeStateDword() + RuntimeAbi::StateVertexSelectors + attribute / RuntimeAbi::VertexSelectorsPerDword), ConstantU32(state, attribute % RuntimeAbi::VertexSelectorsPerDword * 16u + component * 3u)), 7u) : state.module.SpecializationConstant(TypeU32(state), first + component, component + 4u); const auto one = state.module.SpecializationConstant(TypeU32(state), first + 4u, 0x3f800000u); auto value = Select(state, TypeU32(state), Binary(state, spv::OpIEqual, TypeBool(state), selector, ConstantU32(state, 1u)), one, ConstantU32(state, 0u)); for (std::uint32_t channel = 0; channel < 4u; ++channel) value = Select(state, TypeU32(state), Binary(state, spv::OpIEqual, TypeBool(state), selector, ConstantU32(state, channel + 4u)), EmitAttributeValue(state, attribute, channel), value); @@ -1260,6 +1298,12 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { state.module.AddFunction(spv::OpBitcast, TypeF32(state), f32, raw); state.module.AddFunction(spv::OpStore, state.depthVariable, f32); } + if ((exp.en & 2u) != 0u && state.stencilRefVariable != 0u) { + const auto raw = ExportRawComponent(ctx, data, 1); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitcast, TypeI32(state), value, raw); + state.module.AddFunction(spv::OpStore, state.stencilRefVariable, value); + } if ((exp.en & 4u) != 0u && state.sampleMaskVariable != 0u) { const auto raw = ExportRawComponent(ctx, data, 2); const auto value = state.module.AllocateId(); @@ -1281,7 +1325,7 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { if (exp.index >= 8u) throw std::runtime_error("fragment export target exceeds the runtime mapping table"); std::array components{}; for (std::uint32_t component = 0; component < components.size(); ++component) { - const auto index = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ExportBase + exp.index * 4u + component, component); + const auto index = state.registrationModules ? EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), EmitShaderDataDwordLoad(state, state.program.Metadata().bindings.RuntimeStateDword() + RuntimeAbi::StateExportMappings + exp.index / 4u), ConstantU32(state, exp.index % 4u * 8u + component * 2u)), 3u) : state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ExportBase + exp.index * 4u + component, component); components[component] = state.module.AllocateId(); state.module.AddFunction(spv::OpVectorExtractDynamic, uintOutput ? TypeU32(state) : TypeF32(state), components[component], value, index); } @@ -1300,6 +1344,9 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { value = ConvertPositionToClipSpace(state, value); } } + if (exp.kind == ExportTargetKind::Position && state.zeroPositionClipIndex) { + StoreZeroPositionClipDistance(state, value); + } if (state.program.Resources().stage == IrShaderStage::Mesh) { const auto kind = exp.kind == ExportTargetKind::Position ? StageOutputKind::Position : StageOutputKind::Parameter; state.module.AddFunction(spv::OpStore, MeshOutputPointer(state, kind, exp.index), value); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index 2e6a1b0db..2218a608a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -57,9 +57,11 @@ RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& inst const auto* handle = instruction.Argument(0)->Resolve(); if (handle->Opcode() != IrOpcode::GetBufferResource || handle->ArgumentCount() != 4u) context.Fail(instruction, "requires a runtime four-dword V#"); const auto& memory = context.Memory(instruction); + const bool runtime = memory.gpuDescriptor || state.registrationModules; const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords; - const auto word1 = memory.gpuDescriptor ? context.Arg(*handle, 1u) : state.module.SpecializationConstant(TypeU32(state), first, 0u); - const auto word3 = memory.gpuDescriptor ? context.Arg(*handle, 3u) : state.module.SpecializationConstant(TypeU32(state), first + 1u, 0u); + const auto word1 = runtime ? context.Arg(*handle, 1u) : state.module.SpecializationConstant(TypeU32(state), first, 0u); + const bool runtimeFormat = memory.gpuDescriptor || (state.registrationModules && !state.program.Info().buffers.at(memory.resource).descriptorFormatted); + const auto word3 = runtimeFormat ? context.Arg(*handle, 3u) : state.module.SpecializationConstant(TypeU32(state), first + 1u, 0u); const auto u32 = TypeU32(state); auto base = ConstantU32(state, 0u); if (memory.gpuDescriptor || BufferAccessOf(instruction.Opcode()) == BufferAccess::Atomic) { @@ -82,7 +84,8 @@ RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& inst } if (memory.gpuDescriptor) faultIf(context, instruction, invalid, base, 0u, BdaAbi::FaultReason::InvalidDescriptor); const auto stride = field(state, word1, 16u, 14u); - const auto valid = memory.gpuDescriptor ? both(state, Unary(state, spv::OpLogicalNot, TypeBool(state), invalid), Binary(state, spv::OpINotEqual, TypeBool(state), base, BdaConstant(state, 0u))) : nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u)); + const auto present = runtime ? both(state, nonzero(state, context.Arg(*handle, 2u)), Binary(state, spv::OpLogicalOr, TypeBool(state), nonzero(state, context.Arg(*handle, 0u)), nonzero(state, field(state, word1, 0u, 16u)))) : nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u)); + const auto valid = memory.gpuDescriptor ? both(state, Unary(state, spv::OpLogicalNot, TypeBool(state), invalid), Binary(state, spv::OpINotEqual, TypeBool(state), base, BdaConstant(state, 0u))) : present; return {base, context.Arg(*handle, 2u), stride, word3, both(state, nonzero(state, stride), nonzero(state, field(state, word1, 31u, 1u))), index, valid}; } @@ -140,8 +143,8 @@ MemoryResourceAccess directAccess(SpirvValueEmitContext& context, const IrValue& std::uint32_t baseMisalignment(SpirvValueEmitContext& context, const IrValue& instruction) { const auto& memory = context.Memory(instruction); if (memory.kind == ResourceKind::ScalarBuffer) return ConstantU32(context.state, 0u); - const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords; - return context.state.module.SpecializationConstant(TypeU32(context.state), first + 3u, 0u); + auto& state = context.state; + return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords + 3u, 0u); } std::uint32_t directWordIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t address) { @@ -401,7 +404,7 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr auto function = state.formattedBufferFunctions.find(key); if (function == state.formattedBufferFunctions.end()) { const auto id = state.module.AllocateId(); - const auto functionType = store ? state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32, valueType) : state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32); + const auto functionType = store ? state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32, u32, u32, u32, u32, valueType) : state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32, u32, u32, u32, u32); const auto label = state.currentLabel; const auto byteOffset = state.memoryByteOffsets.at(element); struct Restore { @@ -418,21 +421,26 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr state.module.BeginHelperFunction(); const Restore restore{state, element, label, byteOffset}; state.module.AddFunction(spv::OpFunction, resultType, id, spv::FunctionControlDontInlineMask, functionType); - std::array parameters{}; - for (std::uint32_t index = 0; index < (store ? 6u : 5u); ++index) { + std::array parameters{}; + for (std::uint32_t index = 0; index < (store ? 10u : 9u); ++index) { parameters[index] = state.module.AllocateId(); - state.module.AddFunction(spv::OpFunctionParameter, index == 5u ? valueType : u32, parameters[index]); + state.module.AddFunction(spv::OpFunctionParameter, index == 9u ? valueType : u32, parameters[index]); } EmitLabel(state, state.module.AllocateId()); state.memoryByteOffsets.at(element) = parameters[4]; const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords; - const auto word1 = state.module.SpecializationConstant(u32, first, 0u); - const auto word3 = state.module.SpecializationConstant(u32, first + 1u, 0u); - const auto stride = field(state, word1, 16u, 14u); - const auto swizzle = both(state, nonzero(state, stride), nonzero(state, field(state, word1, 31u, 1u))); - const auto valid = nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u)); - const RuntimeDescriptor shared{ConstantU32(state, 0u), parameters[3], stride, word3, swizzle, parameters[0], valid, parameters[1], parameters[2]}; - const auto value = formatSwitch(context, instruction, shared, components, store, store ? parameters[5] : 0u); + RuntimeDescriptor shared{}; + if (state.registrationModules) { + const auto word3 = state.program.Info().buffers.at(memory.resource).descriptorFormatted ? state.module.SpecializationConstant(u32, first + 1u, 0u) : parameters[6]; + shared = {ConstantU32(state, 0u), parameters[3], parameters[5], word3, nonzero(state, parameters[7]), parameters[0], nonzero(state, parameters[8]), parameters[1], parameters[2]}; + } else { + const auto word1 = state.module.SpecializationConstant(u32, first, 0u); + const auto stride = field(state, word1, 16u, 14u); + const auto swizzle = both(state, nonzero(state, stride), nonzero(state, field(state, word1, 31u, 1u))); + const auto valid = nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u)); + shared = {ConstantU32(state, 0u), parameters[3], stride, state.module.SpecializationConstant(u32, first + 1u, 0u), swizzle, parameters[0], valid, parameters[1], parameters[2]}; + } + const auto value = formatSwitch(context, instruction, shared, components, store, store ? parameters[9] : 0u); if (store) state.module.AddFunction(spv::OpReturn); else state.module.AddFunction(spv::OpReturnValue, value); state.module.AddFunction(spv::OpFunctionEnd); @@ -440,8 +448,11 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr } const auto offset = Binary(state, spv::OpIAdd, u32, context.Arg(instruction, 2u), ConstantU32(state, memory.offset)); const auto result = state.module.AllocateId(); - if (store) state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element), data); - else state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element)); + const auto flag = [&](std::uint32_t condition) { return Select(state, u32, condition, ConstantU32(state, 1u), ConstantU32(state, 0u)); }; + const auto swizzle = flag(resource.swizzle); + const auto valid = flag(resource.valid); + if (store) state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element), resource.stride, resource.word3, swizzle, valid, data); + else state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element), resource.stride, resource.word3, swizzle, valid); return store ? ConstantU32(state, 0u) : result; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp index 188cae8f5..6d8d41477 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp @@ -417,6 +417,12 @@ private: } return true; }; + std::set loopHeaders; + for (const auto& [label, indices] : blocks) { + for (const auto index : indices) { + if (Opcode(instructions[index]) == spv::OpLoopMerge) loopHeaders.insert(label); + } + } std::map> edges; for (const auto& [label, indices] : blocks) { auto& terminal = instructions[indices.back()]; @@ -425,7 +431,7 @@ private: bool loop = false; for (const auto index : indices) loop |= Opcode(instructions[index]) == spv::OpLoopMerge; if (!loop && op == spv::OpBranchConditional) { - if (const auto condition = value(terminal.at(1))) target = terminal.at(*condition != 0u ? 2u : 3u); + if (const auto condition = value(terminal.at(1)); condition.has_value() && !loopHeaders.contains(terminal.at(*condition != 0u ? 3u : 2u))) target = terminal.at(*condition != 0u ? 2u : 3u); } else if (!loop && op == spv::OpSwitch) { if (const auto selector = value(terminal.at(1))) { target = terminal.at(2); diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index 7a9d6fc8f..7d9fbb0c3 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -61,6 +61,10 @@ private: void write16Bits(const RdnaOperand& operand, IrU32 value); void writeF16(const RdnaOperand& operand, IrF32 value, std::initializer_list sources); bool directedF16Rounding() const; + IrU32 flushF16Bits(IrU32 bits, std::uint32_t keepBit); + IrU32 flushF16Input(IrU32 bits); + IrU32 flushF16Result(IrU32 bits); + IrF32 flushF16InputValue(IrF32 value); IrU32 convertF32ToF16Bits(IrF32 value, std::initializer_list sources); void writeF16Conversion(const RdnaOperand& operand, IrF32 value, std::initializer_list sources); IrU32 readU32(const RdnaOperand& operand); @@ -161,6 +165,7 @@ private: void emitInteger16Compare(const RdnaInstruction& inst, IrOpcode opcode, bool signedValue, bool cmpx); void emitFloatCompare(const RdnaInstruction& inst, IrOpcode opcode, bool half, bool cmpx, bool swap = false); IrU1 floatCompareWithConstant(IrOpcode opcode, IrU32 variableBits, std::uint32_t constant); + IrU1 floatCompareKeepingDenormals(IrOpcode opcode, IrU32 lhsBits, IrU32 rhsBits); void emitInteger64Order(const RdnaInstruction& inst, bool signedValue, bool swap, bool negate, bool cmpx); void emitFloatOrderedCompare(const RdnaInstruction& inst, bool ordered, bool half, bool cmpx); void emitFloatClassCompare(const RdnaInstruction& inst, bool cmpx); diff --git a/core/shader/recompiler/Translation/src/CompareInstructions.cpp b/core/shader/recompiler/Translation/src/CompareInstructions.cpp index eac2f2a94..50bdf556c 100644 --- a/core/shader/recompiler/Translation/src/CompareInstructions.cpp +++ b/core/shader/recompiler/Translation/src/CompareInstructions.cpp @@ -50,6 +50,29 @@ bool unorderedInfinityCompareIsTrue(IrOpcode opcode, bool constantIsLhs, std::ui return false; } +struct OrderedRelation { + IrOpcode integerOpcode; + bool unordered; +}; + +OrderedRelation orderedRelationOf(IrOpcode opcode) { + switch (opcode) { + case IrOpcode::FPOrdEqual32: return {IrOpcode::IEqual32, false}; + case IrOpcode::FPUnordEqual32: return {IrOpcode::IEqual32, true}; + case IrOpcode::FPOrdNotEqual32: return {IrOpcode::INotEqual32, false}; + case IrOpcode::FPUnordNotEqual32: return {IrOpcode::INotEqual32, true}; + case IrOpcode::FPOrdLessThan32: return {IrOpcode::SLessThan32, false}; + case IrOpcode::FPUnordLessThan32: return {IrOpcode::SLessThan32, true}; + case IrOpcode::FPOrdLessThanEqual32: return {IrOpcode::SLessThanEqual32, false}; + case IrOpcode::FPUnordLessThanEqual32: return {IrOpcode::SLessThanEqual32, true}; + case IrOpcode::FPOrdGreaterThan32: return {IrOpcode::SGreaterThan32, false}; + case IrOpcode::FPUnordGreaterThan32: return {IrOpcode::SGreaterThan32, true}; + case IrOpcode::FPOrdGreaterThanEqual32: return {IrOpcode::SGreaterThanEqual32, false}; + case IrOpcode::FPUnordGreaterThanEqual32: return {IrOpcode::SGreaterThanEqual32, true}; + default: throw std::runtime_error("f32 compare keeping denormals: unexpected compare opcode"); + } +} + } void TranslateCompareInstruction(IrBuilder& builder, const RdnaInstruction& instruction) { @@ -113,6 +136,10 @@ void TranslationContext::emitFloatCompare(const RdnaInstruction& inst, IrOpcode emitCompareResult(inst, floatCompareWithConstant(oriented, IrU32(lhsConstant.has_value() ? *rhsBits : *lhsBits), lhsConstant.has_value() ? *lhsConstant : *rhsConstant), false, cmpx); return; } + if ((f32DenormalFlush & 1u) == 0u) { + emitCompareResult(inst, floatCompareKeepingDenormals(opcode, IrU32(*lhsBits), IrU32(*rhsBits)), false, cmpx); + return; + } lhs = &ir.BitCastF32(flushF32Denormal(IrU32(*lhsBits)).Value()); rhs = &ir.BitCastF32(flushF32Denormal(IrU32(*rhsBits)).Value()); emitCompareResult(inst, IrU1(ir.Emit(opcode, IrType::U1, {lhs, rhs})), false, cmpx); @@ -122,6 +149,20 @@ void TranslationContext::emitFloatCompare(const RdnaInstruction& inst, IrOpcode emitCompareResult(inst, IrU1(ir.Emit(opcode, IrType::U1, {lhs, rhs})), false, cmpx); } +IrU1 TranslationContext::floatCompareKeepingDenormals(IrOpcode opcode, IrU32 lhsBits, IrU32 rhsBits) { + const auto magnitude = [&](IrU32 bits) -> IrValue& { return ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)); }; + const auto order = [&](IrU32 bits) -> IrValue& { + IrValue& absolute = magnitude(bits); + IrValue& negative = ir.INotEqual(ir.BitwiseAnd(bits.Value(), ir.Constant(0x80000000u)), ir.Constant(0u)); + return ir.Select(negative, ir.ISub(ir.Constant(0u), absolute), absolute); + }; + const auto isNan = [&](IrU32 bits) -> IrValue& { return ir.UGreaterThan(magnitude(bits), ir.Constant(0x7f800000u)); }; + const OrderedRelation relation = orderedRelationOf(opcode); + IrValue& ordering = ir.Emit(relation.integerOpcode, IrType::U1, {&order(lhsBits), &order(rhsBits)}); + IrValue& eitherNan = ir.LogicalOr(isNan(lhsBits), isNan(rhsBits)); + return IrU1(relation.unordered ? ir.LogicalOr(eitherNan, ordering) : ir.LogicalAnd(ir.LogicalNot(eitherNan), ordering)); +} + IrU1 TranslationContext::floatCompareWithConstant(IrOpcode opcode, IrU32 variableBits, std::uint32_t constant) { IrValue& value = ir.BitCastF32(variableBits.Value()); const auto compare = [&](IrOpcode compareOpcode, IrValue& lhs, std::uint32_t rhs) { return IrU1(ir.Emit(compareOpcode, IrType::U1, {&lhs, &ir.BitCastF32(ir.Constant(rhs))})); }; diff --git a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp index b1ac114b6..b2168d116 100644 --- a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp @@ -13,7 +13,40 @@ void TranslateControlFlowInstruction(IrBuilder& builder, const RdnaInstruction& } void TranslationContext::sSubvectorLoop(const RdnaInstruction& inst, bool begin) { - throw std::runtime_error("GNM subvector loop instructions are not supported by this translator"); + if (program.WaveSize() != 64u) { + throw std::runtime_error(std::string(begin ? "s_subvector_loop_begin" : "s_subvector_loop_end") + " in a wave" + std::to_string(program.WaveSize()) + " shader is not measured, only wave64 subvector loops are supported"); + } + if (inst.destination.kind != RdnaOperandKind::ScalarRegister) { + throw std::runtime_error(std::string(begin ? "s_subvector_loop_begin" : "s_subvector_loop_end") + " with a destination that is not an SGPR is not measured"); + } + const IrU32 execLo(ir.GetExecLo()); + const IrU32 execHi(ir.GetExecHi()); + const IrU32 counter = readRawU32(inst.destination); + const IrU32 zero(ir.Constant(0u)); + IrU32 newExecLo = execLo; + IrU32 newExecHi = execHi; + IrU32 newCounter = counter; + if (begin) { + const IrU1 lowZero(ir.IEqual(execLo.Value(), zero.Value())); + const IrU1 highZero(ir.IEqual(execHi.Value(), zero.Value())); + const IrU1 execZero(ir.LogicalAnd(lowZero.Value(), highZero.Value())); + const IrU32 entered(ir.Select(lowZero.Value(), execLo.Value(), execHi.Value())); + newCounter = IrU32(ir.Select(execZero.Value(), counter.Value(), entered.Value())); + newExecHi = IrU32(ir.Select(ir.LogicalOr(execZero.Value(), lowZero.Value()), execHi.Value(), zero.Value())); + instructionBranchCondition = execZero; + } else { + const IrU1 highSet(ir.INotEqual(execHi.Value(), zero.Value())); + const IrU1 counterZero(ir.IEqual(counter.Value(), zero.Value())); + const IrU1 stay(ir.LogicalOr(highSet.Value(), counterZero.Value())); + newExecLo = IrU32(ir.Select(highSet.Value(), counter.Value(), ir.Select(counterZero.Value(), execLo.Value(), zero.Value()))); + newExecHi = IrU32(ir.Select(stay.Value(), execHi.Value(), counter.Value())); + newCounter = IrU32(ir.Select(stay.Value(), counter.Value(), execLo.Value())); + instructionBranchCondition = IrU1(ir.LogicalNot(stay.Value())); + } + writeRawU32(inst.destination, newCounter); + ir.SetExecLo(newExecLo.Value()); + ir.SetExecHi(newExecHi.Value()); + ir.SetExec(threadBit({newExecLo, newExecHi}).Value()); } // s_*_saveexec reads its source before it writes the old exec into the destination, so @@ -298,8 +331,12 @@ struct RelativeWindowAccess { IrValue* inRange = nullptr; }; -RelativeWindowAccess relativeWindowAccess(IrBuilder& ir, const IrProgram& program, std::uint32_t reg, IrValue& offset, const char* name) { +RelativeWindowAccess relativeWindowAccess(IrBuilder& ir, const IrProgram& program, std::uint32_t reg, IrValue& offset, const char* name, std::uint32_t vectorLimit) { const auto& metadata = program.Metadata(); + if (metadata.vectorWindowSize == 0u) { + if (reg >= vectorLimit) throw std::runtime_error(std::string(name) + " addresses a register outside the relative-addressing span"); + return {nullptr, &ir.ULessThan(offset, ir.Constant(vectorLimit - reg))}; + } if (metadata.vectorWindowSize == 0u || reg < metadata.vectorWindowBase) throw std::runtime_error(std::string(name) + " addresses a register outside the relative-addressing window"); const auto first = reg - metadata.vectorWindowBase; IrValue& index = ir.Emit(IrOpcode::UMin32, IrType::U32, {&ir.IAdd(ir.Constant(first), offset), &ir.Constant(metadata.vectorWindowSize - 1u)}); @@ -310,7 +347,15 @@ IrU32 TranslationContext::readRelativeVector(const RdnaOperand& base, IrValue& o if (base.kind != RdnaOperandKind::VectorRegister) { throw std::runtime_error(std::string(name) + " source is not a vector register"); } - const auto access = relativeWindowAccess(ir, program, base.reg, offset, name); + const auto access = relativeWindowAccess(ir, program, base.reg, offset, name, currentVectorLimit); + if (access.index == nullptr) { + IrU32 result(ir.GetVectorReg(static_cast(0))); + for (std::uint32_t reg = base.reg; reg < currentVectorLimit; ++reg) { + IrValue& hit = ir.IEqual(offset, ir.Constant(reg - base.reg)); + result = IrU32(ir.Select(hit, ir.GetVectorReg(static_cast(reg)), result.Value())); + } + return applyBitSourceModifiers(base, result); + } IrValue& loaded = ir.Emit(IrOpcode::LoadVectorWindow, IrType::U32, {access.index}); return applyBitSourceModifiers(base, IrU32(ir.Select(*access.inRange, loaded, ir.GetVectorReg(static_cast(0))))); } @@ -320,7 +365,21 @@ void TranslationContext::writeRelativeVector(const RdnaOperand& base, IrValue& o if (destination.kind != RdnaOperandKind::VectorRegister) { throw std::runtime_error(std::string(name) + " destination is not a vector register"); } - const auto access = relativeWindowAccess(ir, program, destination.reg, offset, name); + const auto access = relativeWindowAccess(ir, program, destination.reg, offset, name, currentVectorLimit); + if (access.index == nullptr) { + for (std::uint32_t reg = destination.reg; reg < currentVectorLimit; ++reg) { + IrValue& previous = ir.GetVectorReg(static_cast(reg)); + IrValue* merged = nullptr; + if (destination.dpp) { + const DppMoveFlags flags{destination.dpp8 ? DppMoveFlags::Lanes8 | destination.dppCtrl : destination.dppCtrl, static_cast(destination.dppRowMask), static_cast(destination.dppBankMask), destination.dppFetchInactive, destination.dppBoundCtrl}; + merged = &ir.Emit(IrOpcode::DppUpdateU32, IrType::U32, {&value.Value(), &previous, &ir.GetExec()}, flags); + } else merged = &ir.Select(ir.GetExec(), value.Value(), previous); + IrValue* written = &ir.IEqual(offset, ir.Constant(reg - destination.reg)); + if (enable != nullptr) written = &ir.LogicalAnd(*written, *enable); + ir.SetVectorReg(static_cast(reg), ir.Select(*written, *merged, previous)); + } + return; + } IrValue& previous = ir.Emit(IrOpcode::LoadVectorWindow, IrType::U32, {access.index}); IrValue* merged = nullptr; if (destination.dpp) { @@ -354,7 +413,7 @@ void TranslationContext::vMovrelsdB32(const RdnaInstruction& inst, bool split, b return; } const IrU32 previous = readRelativeVector(plainOperand(inst.destination), destinationOffset, name); - IrValue& bothInRange = ir.LogicalAnd(*relativeWindowAccess(ir, program, sourceAt(inst, 0u).reg, sourceOffset, name).inRange, *relativeWindowAccess(ir, program, inst.destination.reg, destinationOffset, name).inRange); + IrValue& bothInRange = ir.LogicalAnd(*relativeWindowAccess(ir, program, sourceAt(inst, 0u).reg, sourceOffset, name, currentVectorLimit).inRange, *relativeWindowAccess(ir, program, inst.destination.reg, destinationOffset, name, currentVectorLimit).inRange); writeRelativeVector(inst.destination, destinationOffset, value, name, &bothInRange); writeRelativeVector(sourceAt(inst, 0u), sourceOffset, previous, name, &bothInRange); } diff --git a/core/shader/recompiler/Translation/src/ConvertInstructions.cpp b/core/shader/recompiler/Translation/src/ConvertInstructions.cpp index 880e7def8..6b76cb8f9 100644 --- a/core/shader/recompiler/Translation/src/ConvertInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ConvertInstructions.cpp @@ -123,7 +123,7 @@ void TranslationContext::vCvtF16F32(const RdnaInstruction& inst) { } void TranslationContext::vCvtF32F16(const RdnaInstruction& inst) { - const IrF32 value = readF16AsF32(sourceAt(inst, 0u)); + const IrF32 value = flushF16InputValue(readF16AsF32(sourceAt(inst, 0u))); writeOperand(inst.destination, &value.Value()); } diff --git a/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp b/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp index 7bc692922..1e2e2d5fc 100644 --- a/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp +++ b/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp @@ -342,6 +342,8 @@ bool writesExec(const RdnaInstruction& inst) { case RdnaOpcode::SOrn1SaveexecB64: case RdnaOpcode::SAndn1WrexecB64: case RdnaOpcode::SAndn2WrexecB64: + case RdnaOpcode::SSubvectorLoopBegin: + case RdnaOpcode::SSubvectorLoopEnd: return true; default: return false; diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index b19d72c94..e09885cb3 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -104,12 +104,13 @@ bool TranslationContext::packedFloat16(const RdnaInstruction& inst, IrOpcode opc bool TranslationContext::float16Unary(const RdnaInstruction& inst, IrOpcode opcode) { const RdnaOperand& operand = sourceAt(inst, 0u); - const IrF32 argument = readF16AsF32(operand); + const IrF32 argument = flushF16InputValue(readF16AsF32(operand)); const auto [result, invalid] = unaryFloatSpecials(opcode, argument, IrF32(ir.Emit(opcode, IrType::F32, {&argument.Value()}))); IrU32 bits = packHalf2x16(applyF16ResultModifiers(inst.destination, clampF16Overflow(result, {&argument.Value()})), IrF32(ir.ConstantF32(0.0f))); if (opcode == IrOpcode::FPFract32) { bits = IrU32(ir.Select(ir.IEqual(bits.Value(), ir.Constant(0x3c00u)), ir.Constant(0x3bffu), bits.Value())); } + bits = flushF16Result(bits); const IrU32 half = readF16Bits(operand); const IrU1 nan(ir.UGreaterThan(ir.BitwiseAnd(half.Value(), ir.Constant(0x7fffu)), ir.Constant(0x7c00u))); const IrU32 special(ir.Select(nan.Value(), quietNan16(half).Value(), ir.Constant(0xfe00u))); @@ -344,11 +345,7 @@ IrU32 TranslationContext::normF32(IrU32 bits, bool signedValue) { bool TranslationContext::vLdexpF16(const RdnaInstruction& inst) { const IrU32 source = readF16Bits(sourceAt(inst, 0u)); - IrU32 bits = source; - if (floatMode.has_value() && (floatMode->floatMode & 0x40u) == 0u) { - const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), ir.Constant(0x0400u))); - bits = IrU32(ir.Select(tiny.Value(), ir.BitwiseAnd(bits.Value(), ir.Constant(0x8000u)), bits.Value())); - } + const IrU32 bits = flushF16Input(source); const IrF16 half(ir.Emit(IrOpcode::BitCastF16U16, IrType::F16, {&ir.Emit(IrOpcode::ConvertU16U32, IrType::U16, {&bits.Value()})})); const IrF32 value(ir.Emit(IrOpcode::ConvertF32F16, IrType::F32, {&half.Value()})); const RdnaOperand& exponentSource = sourceAt(inst, 1u); @@ -368,8 +365,8 @@ bool TranslationContext::vLdexpF16(const RdnaInstruction& inst) { } bool TranslationContext::vFrexpF16(const RdnaInstruction& inst, bool exponent) { - const IrU32 bits = readF16Bits(sourceAt(inst, 0u)); - const IrF32 value = readF16AsF32(sourceAt(inst, 0u)); + const IrU32 bits = flushF16Input(readF16Bits(sourceAt(inst, 0u))); + const IrF32 value = flushF16InputValue(readF16AsF32(sourceAt(inst, 0u))); const IrU32 word(ir.BitCastU32(value.Value())); const IrU1 special(ir.LogicalOr(ir.IEqual(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), ir.Constant(0u)), ir.Emit(IrOpcode::UGreaterThanEqual32, IrType::U1, {&ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), &ir.Constant(0x7c00u)}))); @@ -388,7 +385,7 @@ bool TranslationContext::vFrexpF16(const RdnaInstruction& inst, bool exponent) { } bool TranslationContext::vCvtNormF16(const RdnaInstruction& inst, bool signedValue) { - write16Bits(inst.destination, normF16(readF16Bits(sourceAt(inst, 0u)), signedValue)); + write16Bits(inst.destination, normF16(flushF16Input(readF16Bits(sourceAt(inst, 0u))), signedValue)); return true; } diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index f99b87af0..04d50e26f 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -90,7 +90,8 @@ bool isCodeTableLoad(const ControlFlowGraph& cfg, std::uint32_t programCounter) return std::find(cfg.codeTableLoadProgramCounters.begin(), cfg.codeTableLoadProgramCounters.end(), programCounter) != cfg.codeTableLoadProgramCounters.end(); } -void translateExternalFetch(TranslationContext& context, const RdnaInstruction& call, const ShaderVertexInputInfo* input) { +template +void forEachExternalFetchLoad(const RdnaInstruction& call, const ShaderVertexInputInfo* input, TAction action) { if (input == nullptr || input->resourcesNum <= 0) { throw std::runtime_error("s_swappc_b64 fetch-shader call at program counter " + std::to_string(call.programCounter) + " has no parsed fetch-shader plan to inline"); } @@ -108,8 +109,9 @@ void translateExternalFetch(TranslationContext& context, const RdnaInstruction& fetch.programCounter = call.programCounter; fetch.destination.kind = RdnaOperandKind::VectorRegister; fetch.destination.reg = static_cast(destination.registerStart); + fetch.dataDwordCount = static_cast(destination.registersNum); fetch.formatted = true; - context.TranslateEmbeddedFetch(fetch, static_cast(index), static_cast(destination.registersNum)); + action(static_cast(index), fetch); } } @@ -161,6 +163,15 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO IrBuilder entryIr(program); entryIr.SetInsertionPoint(entryBlock); + const auto setVector = [&entryIr, &program](VectorReg reg, IrValue& value) { + auto& metadata = program.Metadata(); + const auto index = RegIndex(reg); + if (metadata.vectorWindowSize != 0u && index >= metadata.vectorWindowBase) { + metadata.vectorWindowSize = std::max(metadata.vectorWindowSize, index - metadata.vectorWindowBase + 1u); + } + entryIr.SetVectorReg(reg, value); + }; + const auto builtin = [&entryIr](StageInputKind kind, std::uint32_t component = 0u) -> IrValue& { return entryIr.Emit(IrOpcode::GetBuiltin, IrOpcodeType(IrOpcode::GetBuiltin), {&entryIr.Constant(static_cast(kind)), &entryIr.Constant(component)}); }; @@ -207,7 +218,7 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO const auto* cs = options.inputInfo.compute; const std::uint32_t threadIds = cs->threadIdsNum > 0 ? std::min(static_cast(cs->threadIdsNum), 3u) : 0u; for (std::uint32_t index = 0; index < threadIds; index++) { - entryIr.SetVectorReg(static_cast(index), builtin(StageInputKind::LocalInvocationId, index)); + setVector(static_cast(index), builtin(StageInputKind::LocalInvocationId, index)); } std::uint32_t regOffset = 0; for (std::uint32_t index = 0; index < 3u; index++) { @@ -279,11 +290,11 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO IrValue& first = fan ? entryIr.IMul(entryIr.IAdd(vertex, u32(1u)), item) : unlessRestarted(entryIr.IMul(entryIr.IAdd(vertex, parity), item)); IrValue& second = fan ? entryIr.IMul(entryIr.IAdd(vertex, u32(2u)), item) : size >= 2u ? unlessRestarted(entryIr.IMul(entryIr.ISub(entryIr.IAdd(vertex, u32(1u)), parity), item)) : u32(0u); IrValue& third = fan ? u32(0u) : size == 3u ? unlessRestarted(entryIr.IMul(entryIr.IAdd(vertex, u32(2u)), item)) : u32(0u); - entryIr.SetVectorReg(static_cast(0), entryIr.BitwiseOr(entryIr.BitwiseAnd(first, u32(0xffffu)), entryIr.ShiftLeftLogical(second, u32(16u)))); - entryIr.SetVectorReg(static_cast(1), entryIr.BitwiseAnd(third, u32(0xffffu))); - entryIr.SetVectorReg(static_cast(2), entryIr.IAdd(firstPrimitive, local)); - entryIr.SetVectorReg(static_cast(3), u32(0u)); - entryIr.SetVectorReg(static_cast(4), u32(0u)); + setVector(static_cast(0), entryIr.BitwiseOr(entryIr.BitwiseAnd(first, u32(0xffffu)), entryIr.ShiftLeftLogical(second, u32(16u)))); + setVector(static_cast(1), entryIr.BitwiseAnd(third, u32(0xffffu))); + setVector(static_cast(2), entryIr.IAdd(firstPrimitive, local)); + setVector(static_cast(3), u32(0u)); + setVector(static_cast(4), u32(0u)); if (options.userDataBaseRegister != 0u || options.userDataCount < 8u) { throw std::runtime_error("mesh shader translation requires the merged program's eight hidden user words"); } @@ -301,30 +312,30 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO } IrValue& restartIndex = entryIr.Select(entryIr.IEqual(indexBytes, u32(2u)), u32(0xffffu), u32(0xffffffffu)); IrValue& fetchedIndex = entryIr.Select(entryIr.LogicalAnd(restartTable, entryIr.IEqual(index, restartIndex)), u32(0u), index); - entryIr.SetVectorReg(static_cast(5), entryIr.IAdd(draw(1u), entryIr.Select(indexed, fetchedIndex, inputVertex))); - entryIr.SetVectorReg(static_cast(6), u32(0u)); - entryIr.SetVectorReg(static_cast(7), u32(0u)); - entryIr.SetVectorReg(static_cast(8), entryIr.IAdd(draw(2u), builtin(StageInputKind::WorkgroupId, 1u))); + setVector(static_cast(5), entryIr.IAdd(draw(1u), entryIr.Select(indexed, fetchedIndex, inputVertex))); + setVector(static_cast(6), u32(0u)); + setVector(static_cast(7), u32(0u)); + setVector(static_cast(8), entryIr.IAdd(draw(2u), builtin(StageInputKind::WorkgroupId, 1u))); } else if (options.stage == ShaderStageKind::Local) { entryIr.SetScalarReg(static_cast(3), entryIr.Constant(64u)); - entryIr.SetVectorReg(static_cast(2), drawIndex(StageInputKind::VertexIndex)); - entryIr.SetVectorReg(static_cast(3), tessellationBase(TessellationBaseKind::RelativeIndex)); - entryIr.SetVectorReg(static_cast(5), drawIndex(StageInputKind::InstanceIndex)); + setVector(static_cast(2), drawIndex(StageInputKind::VertexIndex)); + setVector(static_cast(3), tessellationBase(TessellationBaseKind::RelativeIndex)); + setVector(static_cast(5), drawIndex(StageInputKind::InstanceIndex)); } else if (options.stage == ShaderStageKind::TessellationControl) { const auto& tess = options.inputInfo.vertex->tess; entryIr.SetScalarReg(static_cast(2), tessellationBase(TessellationBaseKind::OffChip)); entryIr.SetScalarReg(static_cast(4), tessellationBase(TessellationBaseKind::Factor)); entryIr.SetScalarReg(static_cast(3), entryIr.Constant(0x81010000u | tess.inputControlPoints | (tess.outputControlPoints << 8u))); - entryIr.SetVectorReg(static_cast(0), builtin(StageInputKind::PrimitiveId)); - entryIr.SetVectorReg(static_cast(1), entryIr.BitwiseOr(entryIr.ShiftLeftLogical(builtin(StageInputKind::InvocationId), entryIr.Constant(8u)), tessellationBase(TessellationBaseKind::RelativeIndex))); + setVector(static_cast(0), builtin(StageInputKind::PrimitiveId)); + setVector(static_cast(1), entryIr.BitwiseOr(entryIr.ShiftLeftLogical(builtin(StageInputKind::InvocationId), entryIr.Constant(8u)), tessellationBase(TessellationBaseKind::RelativeIndex))); } else if (options.stage == ShaderStageKind::TessellationEvaluation) { entryIr.SetScalarReg(static_cast(3), entryIr.Constant(64u)); entryIr.SetScalarReg(static_cast(4), tessellationBase(TessellationBaseKind::OffChip)); - entryIr.SetVectorReg(static_cast(0), tessellationBase(TessellationBaseKind::PassthroughPrimitive)); - entryIr.SetVectorReg(static_cast(5), builtin(StageInputKind::TessCoord, 0u)); - entryIr.SetVectorReg(static_cast(6), builtin(StageInputKind::TessCoord, 1u)); - entryIr.SetVectorReg(static_cast(7), tessellationBase(TessellationBaseKind::RelativeIndex)); - entryIr.SetVectorReg(static_cast(8), builtin(StageInputKind::PrimitiveId)); + setVector(static_cast(0), tessellationBase(TessellationBaseKind::PassthroughPrimitive)); + setVector(static_cast(5), builtin(StageInputKind::TessCoord, 0u)); + setVector(static_cast(6), builtin(StageInputKind::TessCoord, 1u)); + setVector(static_cast(7), tessellationBase(TessellationBaseKind::RelativeIndex)); + setVector(static_cast(8), builtin(StageInputKind::PrimitiveId)); } else if (options.stage == ShaderStageKind::Pixel) { const auto* ps = options.inputInfo.pixel; const auto vgpr = [&](PixelInput input) { return ps->psInputVgpr[static_cast(input)]; }; @@ -332,44 +343,44 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO for (const auto [input, kind] : {std::pair{PixelInput::PerspectiveCenter, StageInputKind::BaryCoordSmooth}, std::pair{PixelInput::PerspectiveCentroid, StageInputKind::BaryCoordSmooth}, std::pair{PixelInput::LinearCenter, StageInputKind::BaryCoordNoPerspective}, std::pair{PixelInput::LinearCentroid, StageInputKind::BaryCoordNoPerspective}}) { if (!loaded(input)) continue; - entryIr.SetVectorReg(static_cast(vgpr(input)), builtin(kind, 0u)); - entryIr.SetVectorReg(static_cast(vgpr(input) + 1u), builtin(kind, 1u)); + setVector(static_cast(vgpr(input)), builtin(kind, 0u)); + setVector(static_cast(vgpr(input) + 1u), builtin(kind, 1u)); } if (loaded(PixelInput::PositionX)) { - entryIr.SetVectorReg(static_cast(vgpr(PixelInput::PositionX)), builtin(StageInputKind::FragCoord, 0u)); + setVector(static_cast(vgpr(PixelInput::PositionX)), builtin(StageInputKind::FragCoord, 0u)); } if (loaded(PixelInput::PositionY)) { - entryIr.SetVectorReg(static_cast(vgpr(PixelInput::PositionY)), builtin(StageInputKind::FragCoord, 1u)); + setVector(static_cast(vgpr(PixelInput::PositionY)), builtin(StageInputKind::FragCoord, 1u)); } if (loaded(PixelInput::PositionZ)) { - entryIr.SetVectorReg(static_cast(vgpr(PixelInput::PositionZ)), builtin(StageInputKind::FragCoord, 2u)); + setVector(static_cast(vgpr(PixelInput::PositionZ)), builtin(StageInputKind::FragCoord, 2u)); } if (loaded(PixelInput::PositionW)) { IrValue& reciprocalW = entryIr.BitCastF32(builtin(StageInputKind::FragCoord, 3u)); IrValue& w = entryIr.Emit(IrOpcode::FPRecip32, IrOpcodeType(IrOpcode::FPRecip32), {&reciprocalW}); - entryIr.SetVectorReg(static_cast(vgpr(PixelInput::PositionW)), entryIr.BitCastU32(w)); + setVector(static_cast(vgpr(PixelInput::PositionW)), entryIr.BitCastU32(w)); } if (loaded(PixelInput::FrontFace)) { - entryIr.SetVectorReg(static_cast(vgpr(PixelInput::FrontFace)), builtin(StageInputKind::FrontFacing)); + setVector(static_cast(vgpr(PixelInput::FrontFace)), builtin(StageInputKind::FrontFacing)); } if (loaded(PixelInput::Ancillary)) { - entryIr.SetVectorReg(static_cast(vgpr(PixelInput::Ancillary)), builtin(StageInputKind::PackedAncillary)); + setVector(static_cast(vgpr(PixelInput::Ancillary)), builtin(StageInputKind::PackedAncillary)); } if (loaded(PixelInput::LineStipple)) { - entryIr.SetVectorReg(static_cast(vgpr(PixelInput::LineStipple)), entryIr.Constant(0u)); + setVector(static_cast(vgpr(PixelInput::LineStipple)), entryIr.Constant(0u)); } if (loaded(PixelInput::PositionFixedPoint)) { IrValue& x = entryIr.Emit(IrOpcode::ConvertU32F32, IrType::U32, {&entryIr.BitCastF32(builtin(StageInputKind::FragCoord, 0u))}); IrValue& y = entryIr.Emit(IrOpcode::ConvertU32F32, IrType::U32, {&entryIr.BitCastF32(builtin(StageInputKind::FragCoord, 1u))}); - entryIr.SetVectorReg(static_cast(vgpr(PixelInput::PositionFixedPoint)), entryIr.BitwiseOr(entryIr.BitwiseAnd(x, entryIr.Constant(0xffffu)), entryIr.ShiftLeftLogical(y, entryIr.Constant(16u)))); + setVector(static_cast(vgpr(PixelInput::PositionFixedPoint)), entryIr.BitwiseOr(entryIr.BitwiseAnd(x, entryIr.Constant(0xffffu)), entryIr.ShiftLeftLogical(y, entryIr.Constant(16u)))); } } else if (options.stage == ShaderStageKind::Vertex) { if (options.userDataBaseRegister >= 8u) { const std::uint32_t lanes = options.waveSize == 64u && options.inputInfo.vertex->hostSubgroupSize == 32u ? 32u : options.waveSize; entryIr.SetScalarReg(static_cast(3), entryIr.Constant(lanes | (lanes << 8u))); } - entryIr.SetVectorReg(static_cast(5), drawIndex(StageInputKind::VertexIndex)); - entryIr.SetVectorReg(static_cast(8), drawIndex(StageInputKind::InstanceIndex)); + setVector(static_cast(5), drawIndex(StageInputKind::VertexIndex)); + setVector(static_cast(8), drawIndex(StageInputKind::InstanceIndex)); } } @@ -392,6 +403,11 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con continue; } includeInstructionVectorRegisters(instruction, vectorLimit); + if (cfg.hasFetchCall && instruction.programCounter == cfg.fetchCallProgramCounter) { + forEachExternalFetchLoad(instruction, options.inputInfo.vertex, [&](std::uint32_t, const RdnaInstruction& fetch) { + vectorLimit = std::min(NumVectorRegs, std::max(vectorLimit, fetch.destination.reg + fetch.dataDwordCount)); + }); + } } } @@ -419,7 +435,8 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con } } } - if (windowBase < vectorLimit) { + const bool ancillaryWindow = options.stage == ShaderStageKind::Pixel && options.inputInfo.pixel != nullptr && options.inputInfo.pixel->psAncillary && options.inputInfo.pixel->psInputVgpr[static_cast(PixelInput::Ancillary)] >= windowBase; + if (windowBase < vectorLimit && !ancillaryWindow) { program.Metadata().vectorWindowBase = windowBase; program.Metadata().vectorWindowSize = vectorLimit - windowBase; } @@ -482,6 +499,16 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con blocks.front()->AddBranch(blocks[blockIndices.at(cfg.blocks.front().id)]); emitEntryPrologue(program, *blocks.front(), options); + if (program.Metadata().vectorWindowSize != 0u) { + vectorLimit = program.Metadata().vectorWindowBase + program.Metadata().vectorWindowSize; + } + if (ancillaryWindow) { + for (const IrValue* instruction : blocks.front()->Instructions()) { + if (instruction->Opcode() == IrOpcode::SetVectorRegister) { + vectorLimit = std::max(vectorLimit, instruction->Argument(0)->Register().index + 1u); + } + } + } for (const auto& cfgBlock : cfg.blocks) { const auto typedIndex = blockIndices.at(cfgBlock.id); @@ -511,7 +538,9 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con } context.TranslateInstruction(instruction); if (cfg.hasFetchCall && instruction.programCounter == cfg.fetchCallProgramCounter) { - translateExternalFetch(context, instruction, options.inputInfo.vertex); + forEachExternalFetchLoad(instruction, options.inputInfo.vertex, [&](std::uint32_t attribute, const RdnaInstruction& fetch) { + context.TranslateEmbeddedFetch(fetch, attribute, fetch.dataDwordCount); + }); } } context.AddBranchCondition(cfgBlock, blockInfos[typedIndex]); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 79791905b..ce067c491 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -136,6 +136,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psPixelKillEnable = pixel.pixelKillEnable; pixelStorage.psDepthExportEnable = pixel.depthExportEnable; pixelStorage.psSampleMaskExportEnable = pixel.sampleMaskExportEnable; + pixelStorage.psStencilRefExportEnable = pixel.stencilRefExportEnable; pixelStorage.psSampleShading = pixel.sampleShading; pixelStorage.psEarlyZ = pixel.earlyZ; pixelStorage.psExecuteOnNoop = pixel.executeOnNoop; diff --git a/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp b/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp index e4e3de5c6..8347975fc 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp @@ -160,6 +160,27 @@ bool TranslationContext::directedF16Rounding() const { return floatMode.has_value() && ((floatMode->floatMode >> 2u) & 3u) != 0u && ((floatMode->floatMode >> 6u) & 3u) == 3u; } +IrU32 TranslationContext::flushF16Bits(IrU32 bits, std::uint32_t keepBit) { + if (!floatMode.has_value() || (floatMode->floatMode & keepBit) != 0u) return bits; + const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), ir.Constant(0x0400u))); + return IrU32(ir.Select(tiny.Value(), ir.BitwiseAnd(bits.Value(), ir.Constant(0x8000u)), bits.Value())); +} + +IrU32 TranslationContext::flushF16Input(IrU32 bits) { + return flushF16Bits(bits, 0x40u); +} + +IrU32 TranslationContext::flushF16Result(IrU32 bits) { + return flushF16Bits(bits, 0x80u); +} + +IrF32 TranslationContext::flushF16InputValue(IrF32 value) { + if (!floatMode.has_value() || (floatMode->floatMode & 0x40u) != 0u) return value; + const IrU32 bits(ir.BitCastU32(value.Value())); + const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)), ir.Constant(0x38800000u))); + return IrF32(ir.BitCastF32(ir.Select(tiny.Value(), ir.BitwiseAnd(bits.Value(), ir.Constant(0x80000000u)), bits.Value()))); +} + IrU32 TranslationContext::convertF32ToF16Bits(IrF32 value, std::initializer_list sources) { const IrF16 half(ir.Emit(IrOpcode::ConvertF16F32, IrType::F16, {&clampF16Overflow(value, sources).Value()})); const IrU32 native(ir.Emit(IrOpcode::ConvertU32U16, IrType::U32, {&ir.Emit(IrOpcode::BitCastU16F16, IrType::U16, {&half.Value()})})); diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 203602f2b..a1d0da4e4 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -91,11 +91,23 @@ flowchart LR - `Driver::RegisterShader` splits shader preparation in two. `PlanRegistered` runs on the registering guest thread: it validates the registers and entry point, decodes the code and builds the recompile request, so invalid registrations still fail there. The RDNA to IR translation and resource plan (`PrepareShader`) run on a background pool of `hardware_concurrency() / 2` detached threads (at least 2, 64 MiB stacks). - The shader registry maps a code address to every header registered for that code, in registration order. Static, graphics and rectangle ABI resolution pick the header at the bound address (or one equal to it, `SameHeader`, which also takes a hull header fused by `sceAgcFuseShaderHalves` (type 3) as the registered back half (type 7) it copies, the fused registers aside; graphics ABI resolution then uses the fused header's shader registers); draws and dispatches pick the program by the stage's binary type (`RegisteredProgram`), and a front entry matching both type 2 and type 4 throws. Within a type the last registration wins; re-registering an identical earlier header makes its snapshot current again without preparing it again, and changed code drops the older headers. - The snapshot is published as pending. `SourceHandleFor`, `InvocationFor` and `ShaderPreparationTransaction::Edit` / `Read` wait for the pending job before reading `PreparedShaders`, and rethrow its failure. `APS5_SYNC_SHADER_PREPARE=1` prepares on the registering thread instead. +- On devices with graphics pipeline libraries (`SpirvTarget::registrationModules`), a vertex or pixel artifact is emitted in registration form: the buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors it used to specialize on per draw are read at run time (image `ResourceMetadata` and a few state dwords in the shader data), descriptor heaps keep every element in place (an unused one gets a null or placeholder descriptor), and the pixel stage's bindings live in descriptor set 1 so each stage's library depends only on its own set layout (`INDEPENDENT_SETS`). An artifact above `registrationWordLimit` SPIR-V words is re-emitted specialized, and `CompiledShaderArtifact::registrationModule` records which form it has. For artifacts in registration form, `RegistrationResult` materializes the module once, and `PrebuildStageLibrary` builds its pre-rasterization or fragment-shader library on a separate one-thread pool (`APS5_PREBUILD_THREADS`) with a fixed default state, pairing it with a stub stage. A later draw with that state finds both libraries in the store and only links. - A draw or dispatch whose state matches no artifact prepared at registration (a title that builds its static ABI state itself, without `sceAgcDriverResolve*Abi`) gets one prepared at use by `SourceHandleFor` or `InvocationFor`, logged once per artifact and kept in the snapshot for the next use. - A compute shader whose `s_swappc_b64` calls a user-data pointer cannot be linked at registration, because the callee's address is only known at dispatch. `InvocationFor` in [`ShaderRegistry.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) reads each callee from guest memory, appends one copy per call after the caller (`LinkUserDataCalls`) and prepares that linked code, so a different callee gets its own prepared entry. - A draw's `Graphics::Pipeline` comes from `CachedPipeline`. When the device has `VK_EXT_graphics_pipeline_library` with fast linking, `VK_EXT_extended_dynamic_state` and `VK_KHR_dynamic_rendering`, draws render with `vkCmdBeginRenderingKHR` instead of render passes and the pipeline is linked from four libraries (vertex input, pre-rasterization shaders, fragment shader, fragment output) kept per device in [`PipelineLibrary.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp). A library built for one pipeline is reused by the next: another blend, vertex layout or target format builds only that part (the shader libraries name no attachment format), and cull mode, front face, depth and stencil state are dynamic, so changing them only links. The fast link is what the draw waits for; a background thread then links the same libraries with link-time optimization and the pipeline binds that one from the next draw on. Push constants then use a 256-byte block with a 128-byte slot per side: the pre-rasterization stages stack from byte 0, the pixel shader always starts at byte 128 (`StagePushOffset`), so its library does not depend on how much push data its vertex shader uses. Pipelines with per-face depth bias, a separate alpha blend into an 8_8 target with the alternate component order (both draw twice), rect lists, geometry shaders or a stage without a variant id are created whole, still with dynamic rendering; devices without the extensions (or with `APS5_NO_GPL=1`) keep render passes and whole pipelines. - A draw whose `PA_SC_AA_CONFIG` selects 2, 4 or 8 samples renders into host-only multisampled images. [`MultisampleTarget.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) keeps one image per device, surface address, format, extent and sample count (`MultisampleTargetView`); the pipeline, render pass and library keys carry `State::samples`, and a multisampled depth target is a multisampled depth surface. The guest sample planes and FMASK are never read or written, so the registry also guards the target's guest range: a CPU read (`GuestMemory::SetReadGuard`), a DMA copy, and a sampled or storage image read of it throw. A `CB_COLOR_CONTROL` resolve mode over a multisampled CB0 runs as a render pass whose resolve attachment is CB1's resident image (`ResolveMultisampleTarget`). A zero fill of the target's CMASK (`NoteColorMetadataFill`, from fill kernels and CP DMA fills) clears the image at its next use. +### Untracked storage image comparisons + +A storage image over guest memory that the write watch does not cover (a host-pointer import on a driver that drops its write protection, or an unwatched range) has no block stamps to tell a CPU edit apart, so `StorageTexture::compareUntracked` in [`Texture.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) compares the guest bytes with the image's snapshot. A whole-surface comparison that found no change is remembered under a stamp of: + +- the calling queue worker's collect epoch (`GuestMemory::CollectEpoch`), which advances at every ordering point where a CPU store may have landed (submission start, a satisfied `WAIT_REG_MEM`, a drain); a thread that never bumps it gets epoch 0 and is never memoized; +- `GuestMemory::NewestStamp` of the surface: the newest block stamp of its watched part, and the generation of the newest driver store (`MarkWritten`, `StoreOwnBytes`) into its unwatched part; +- `GuestMemory::ForgetSerial`, which changes when guest memory is released or remapped; +- the recorder's newest pending GPU write note over the surface. + +The comparison runs again when any of them changes. A CPU store into unwatched memory is therefore seen at the next ordering point, the same bound the collect memo gives watched memory. Driver stores into unwatched memory are recorded per 64 KiB block in a fixed table of 65,536 slots indexed by block number modulo the table size, each slot holding the newest generation stored to any of its blocks. A collision can only raise a surface's stamp, which costs one comparison and never skips one. `agc_driver_dcc_keys_first_write` (`RunUntrackedReuse`) checks that a CPU edit to an unwatched image is uploaded after the next epoch, and `agc_write_tracking` (`CheckNewestStamp`) checks which stores advance `NewestStamp`. + ### Shader MODE register access `GuestContext::floatMode` carries the initial shader mode through `Recompile` and `TranslateOptions` into each block's `TranslationContext`. `s_getreg_b32` reads confined to MODE bits 0–3 return the selected initial rounding bits in the destination's low bits. Bits 0–1 select f32 rounding; bits 2–3 select f16/f64 rounding. Missing mode metadata retains the legacy round-to-nearest-even assumption. Reading other MODE fields or hardware registers throws. diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 6955e86cd..85bf4ecca 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -7,7 +7,11 @@ - libgcc_s_seh-1.dll - libstdc++-6.dll - libwinpthread-1.dll -- winpthreads initializes a statically initialized rwlock on its first lock and fails a concurrent first lock with `EINVAL`, which `std::shared_mutex` and `std::shared_timed_mutex` ignore, so that lock does not exclude. Guest rwlocks ([Pthread.hpp](../../core/libs/prx/libkernel/Pthread/include/Pthread.hpp)) are locked once when created. The host `std::shared_mutex` objects in [Recompiler.cpp](../../core/shader/recompiler/Recompiler.cpp) and [GuestWriteWatch.cpp](../../core/libs/prx/libc/src/GuestWriteWatch.cpp) are not, and can still race on their first contended use. +- winpthreads initializes a statically initialized rwlock on its first lock and fails a concurrent first lock with `EINVAL`, which `std::shared_mutex` and `std::shared_timed_mutex` ignore, so that lock does not exclude. Guest rwlocks ([Pthread.hpp](../../core/libs/prx/libkernel/Pthread/include/Pthread.hpp)) and the host `std::shared_mutex` objects in [Recompiler.cpp](../../core/shader/recompiler/Recompiler.cpp) are locked once when created. A new host `std::shared_mutex` used on Windows needs the same. + +### Direct memory + +- Windows direct-memory backing reserves a paging-file section and commits each mapped guest page before setting its protection. Committed physical contents remain in the section across unmap/remap and are released when the backing and its remaining views are destroyed; commitment is not recovered merely by unmapping a page. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified. ### Silent stubs @@ -18,7 +22,9 @@ Throughout the project, every function at every stage either **does exactly what - [libSceErrorDialog](../../core/libs/prx/libSceErrorDialog/Export.cpp) - the error dialog runs the state machine but shows nothing; the error code passed to `sceErrorDialogOpen` is only logged - [libSceLoginDialog](../../core/libs/prx/libSceLoginDialog/Export.cpp) - the login dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled - [libSceCdlgG2P](../../core/libs/prx/libSceCdlgG2P/Export.cpp) - the G2P dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled. The signatures (`Open` and `GetResult` take a pointer, the others none) and the leading `int32` result are assumed from the other common dialogs, not verified for G2P; PPSA01289 imports it +- [libSceNpAuthAuthorizedAppDialog](../../core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp) - the authorized-app dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as there is no PSN sign-in to authorize an app against. The signatures follow the other common dialogs (`Open` takes a parameter block, `GetResult` writes a 32-bit result); Minecraft (PPSA17221) imports the seven functions and fails to load without them. - [libSceVrSetupDialog](../../core/libs/prx/libSceVrSetupDialog/Export.cpp) - the VR setup dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as no headset is supported (see libSceHmd2). The signatures are assumed from the other common dialogs; PPSA12544 imports it +- [scePthreadMutexattrSetprotocol](../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) - accepts 0 to 2 as a PS5 (firmware 13.60) does and returns `EINVAL` otherwise, but the protocol is not stored or applied: priority inheritance and protection are not modelled. On the console a null or uninitialized mutex attribute returns `EINVAL` from the SCE attribute calls, `scePthreadMutexInit` takes an uninitialized attribute as the default one, and a destroyed mutex returns `EINVAL` from `scePthreadMutexDestroy`; a null mutex pointer crashed the payload and still throws - [libSceHmd](../../core/libs/prx/libSceHmd/Export.cpp) implements only the disconnected-headset path: initialization succeeds, device queries report `NotDetected`, and opening a device returns `DeviceDisconnected`. Headset support, tracking, rendering and additional HMD exports are not implemented; SDK-level ABI compatibility and in-game behaviour remain unverified. - [pthread_mutexattr_setprotocol, pthread_mutexattr_setprioceiling, pthread_mutexattr_getprioceiling](../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) - the protocol and the priority ceiling are stored and reported as in FreeBSD's [libthr](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutexattr.c) (the ceiling is set or read only under `PTHREAD_PRIO_PROTECT`, and setting a protocol resets it to the highest `SCHED_RR` priority), but mutexes do not apply them: a priority-inherit or priority-protect mutex locks like a normal one and does not change its owner's priority - [libSceHmd2](../../core/libs/prx/libSceHmd2/Export.cpp) has no PS VR2 runtime: `sceHmd2Initialize` fails, and every other export it has (and every [libSceVrTracker2](../../core/libs/prx/libSceVrTracker2/Export.cpp) export) throws. PPSA01317 skips its whole VR set-up when initialization fails @@ -40,6 +46,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceHttp2](../../core/libs/prx/libSceHttp2/Export.cpp) - no request reaches the network, so `sceHttp2SetMinSslVersion` has no TLS connection to restrict and its value is ignored, as for `sceHttp2SslEnableOption` and `sceHttp2SslDisableOption`. The three setters validate `id` against the live handle registry and return `SCE_HTTP2_ERROR_INVALID_ID` (`0x817B1100`) for a handle that was never allocated, was deleted, or belonged to a terminated context, following [prosper](https://github.com/mattias800/prosper). Whether the console restricts the object kind these setters accept, and what it does with an id from a previous init/term cycle, is unverified, so any live handle kind is accepted and ids are never reused. `sceHttp2Term`, `sceHttp2DeleteTemplate`, `sceHttp2DeleteRequest`, `sceHttp2CreateTemplate`, `sceHttp2CreateRequestWithURL`, `sceHttp2CreateCookieBox` and `sceHttp2SendRequestAsync` also return the invalid-id error for a handle of the wrong kind or no longer live, following prosper (not measured on a console) - [libSceNpTrophy2](../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as no trophy is ever unlocked, `sceNpTrophy2UnregisterUnlockCallback` only returns success, and `sceNpTrophy2ShowTrophyList` returns success without showing a list (there is no system trophy UI; seen on PPSA15552). - [sceAvPlayerSetTrickSpeed](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) with a negative speed runs the clock backwards but delivers no frames; when a forward speed is set again, playback resumes from the rewound time. +- [sceCesMbcsUcsContextInit](../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns success without writing to the context, and returns the invalid-parameter error for a null context. The signature `int sceCesMbcsUcsContextInit(union* context, const union* mbcsUcsPrf)` follows the [Orbital IDA type database](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json); the context layout and its size are unknown, so nothing is stored. PPSA15246 calls it and imports no other function that takes the context; no conversion through an initialized context exists. `sceCesRefersUcsProfileCp1251` returns a profile accepted by `sceCesSbcToUtf8` and `sceCesUtf8ToSbc` only, with the Windows-1251 table of Microsoft's [bestfit1251.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1251.txt), where byte 0x98 (unassigned) maps to U+0098 like the unassigned Cp1252 bytes; the console behavior for that byte is not verified - [sceRudpProcessEvents and sceRudpNetFlush](../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host, and every `sceRudpWrite`, `sceRudpRead` and `sceRudpGetStatus` call here reports a reset or idle context without queueing data, so no context ever holds pending data to process or send; both calls return success after `sceRudpInit` and `RUDP_ERROR_NOT_INITIALIZED` before it, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) returns OK from its stubs. PPSA03490 calls `sceRudpProcessEvents(0)` followed by `sceRudpNetFlush()` from its P2P service thread and ignores the first result; whether the real calls take parameters is unknown - [sceRudpActivate](../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host to activate, so it returns success without doing anything, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) does for its own `sceRudpActivate`. PPSA01325 calls it with no arguments during start-up; what the console activates, and whether it reports an error before `sceRudpInit`, is unverified - [ulobjmgr](../../core/libs/prx/ulobjmgr/Export.cpp) registers no object: `_sceUlobjmgrRegisterObject` always hands out id 0 and `_sceUlobjmgrUnregisterObject` releases nothing, as shadPS4 does @@ -47,18 +54,30 @@ Throughout the project, every function at every stage either **does exactly what - [sceFontGlyphDefineAttribute](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) checks the glyph and ignores the attribute and its value, whose meaning is unknown. PPSA01325 sets attribute 0x11 to 0 on a glyph from `sceFontGenerateCharGlyph` that it deletes unused, and draws the character with `sceFontRenderCharGlyphImageHorizontal` - [sceAudioInInit](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - returns 0 without library-wide setup, as `sceAudioOutInit` does; each `sceAudioInOpen` opens its own SDL capture device - [sceRazorCpuPushMarker](../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signature (label, color, flags) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), the same as sceRazorCpuPushMarkerStatic's except that the label is copied; it does nothing without a capture and returns 0 +- [sceKernelEnableDmemAliasing](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) returns 0 and changes nothing: direct memory can always be mapped more than once here, as `DoMapDirect` in [DirectMemory.cpp](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) only checks that the physical pages are allocated, so there is no aliasing to turn on. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L39-L43) (PS4) sets a flag there that lifts the `EBUSY` its [`sceKernelMapDirectMemory2`](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L299-L301), and [`sceKernelMapDirectMemory` for SDK 2.50 and later](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L237-L242), return for direct memory that is [already mapped](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/memory.cpp#L548-L553); that check is not applied here. The no-argument `int` signature follows shadPS4 and ProsperoEden (PPSA99008), which calls it before mapping the same direct memory again. Whether the PS5 refuses such a mapping without the call is unverified ### Unknown function info +- [getlogin_r](../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel) returns the host effective account name: `GetUserNameW` as UTF-8 on Windows and `getpwuid_r(geteuid())` on POSIX. A valid account returns a name even without a login session; host `getlogin_r`, `USER` and `LOGNAME` are not consulted. The console's login name and behaviour without a session are unknown. Account lookup or encoding failures, a missing or empty account name, and POSIX account records exceeding the 16 KiB lookup buffer throw; there is no replacement name or retry. A short guest buffer returns `ERANGE` without modifying it, and a null output returns `EFAULT`. + - [sceHttpUriBuild](../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) retains `//` for a non-opaque URI independently of the selected fields, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a8a70da595da2a96e0410309472ba4a7eb558144/src/core/libraries/network/http.cpp#L3551-L3558). Black Myth: Wukong builds its `Scheme` header with option 1 and expects `http://` or `https://`. Other partial field combinations follow the PS4 reference and remain unverified on PS5 hardware. +- [_Mtx_trylock](../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `k6pGNMwJB08`) - returns busy for a non-recursive mutex already owned by the caller, assumed to match `_Mtx_lock`'s ownership handling; exact console behavior is unverified. +- [nearbyintf](../../core/libs/prx/libc/src/Math.cpp) (libc, NID `c+4r-T-tEIc`) uses the host rounding mode, not the console's; console rounding mode state remains unverified. +- [_Thrd_sleep](../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `jfRI3snge3o`) follows the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`xtime.c`) and C11 Annex K, without console verification. It reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep`, retrying on `EINTR`, so it is a cancellation point; a null xtime or one whose normalization overflows throws. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. - [sceCoredumpGetThreadContextInfo](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `ShChva57wIM`) - declared as a throwing stub. The pointer and size arguments and integer return type follow [SharpProspero's PS5 bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs). The context structure and console behavior are unverified; the stub does not read or write caller memory. PPSA01736 imports it +- [sceNpAuthGetAuthorizedAppCode](../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - declared without parameters as a throwing stub; Minecraft (PPSA17221) imports it and fails to load without it. The request parameter and code output are not known, and there is no PSN sign-in for it to succeed against. +- [sceSaveDataUnknown_RvID8BVeg](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native, NID `RvID8BVeg+g`) - declared without parameters as a throwing stub; Minecraft (PPSA17221) imports it beside `sceSaveDataTransferringMountPs4` and `sceSaveDataDirNameSearchPs4` and fails to load without it. No name in aerolib or among the PS4-migration variants tried with `tools/nid_names.py` hashes to it. - [sceNpWebApi2PushEventAbortHandle](../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2, NID `1OLgvahaSco`) is a throwing stub. The two signed 32-bit handle arguments and return type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/np/np_web_api2/np_web_api2.cpp) (PS4); the PS5 signature and abort behavior are unverified. PPSA01736 imports it. - [sceNpGetSandboxInfo](../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `ZVZ++68nNI8`) - the name hashes to the imported NID (found with nidhunt, confirmed with `tools/nid_names.py`). STAR WARS: Galactic Racer (PPSA28416) imports it; its OnlineSubsystemPS5 calls it with one pointer to a 0x50-byte stack buffer and reads the buffer as a string (discussions/3086). The returned string, the exact parameter struct and console errors are unknown, so every call throws without reading or writing caller memory. A working implementation belongs with that title's bring-up. - [sceKernelGetSystemSwVersion](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000). The 0x28-byte layout (`uint64_t size` at 0, `versionString` at 8 and `version` at 0x24) matches [ProsperoStore's caller](https://github.com/blackbearreloaded/ProsperoStore/blob/20584011/src/diag/trace.cpp#L20-L26), [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d1/src/core/libraries/kernel/kernel.h#L45-L49), [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6/kernel/ps4_libkernel.pas#L502-L506) and [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4/include/orbis/_types/kernel.h#L256-L260). These sources support the structure layout, but not the reported version value: the console firmware version remains unverified. The checks are unverified too: a null pointer returns `SCE_KERNEL_ERROR_EFAULT` and a `size` other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL`, while shadPS4 returns success for null and never reads `size`. - [kern.rng_pseudo](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` fills the whole output buffer with bytes from the host's `std::random_device` and leaves the length unchanged; a write returns -1 with `EPERM` and a call without an output buffer throws. The only caller seen is mbedTLS's `mbedtls_hardware_poll` in the catalog's ProsperoLight boilerplate, which asks for 64-byte blocks and fails if fewer come back. The console's generator, its per-call maximum and its answer to a size query are unknown +- [kern.sdk_version](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the system software version as the 4-byte value `0x01000000` (1.00), the value `sceKernelGetSystemSwVersion` reports; a write returns -1 with `EPERM`. The major version is byte 3 and the minor version byte 2, in hex digits that read as decimal (13.60 is `0x1360xxxx`), as [Capy Mobile](https://github.com/khalifa007/CapyMobile/blob/2758c016/src/ps5/platform.c#L64-L71) (PPSA92280), [remote_lua_loader](https://github.com/n0llptr/remote_lua_loader/blob/7c2840e475/savedata/misc.lua#L195-L209) and the [lapse payload](https://github.com/NetflixNHack/Netflix-N-Hack/blob/605eb93ba5/PS4/payloads/lapse.js#L436-L450) read it. Capy Mobile only displays it. The console's firmware version is not modeled and the low two bytes are unknown - PPSA01530, PPSA23827 and PPSA13356 imports declared without parameters, signatures unknown, added without an implementation because the titles fail to load without them: [sceNetShowIfconfig](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet), [sce::Json::Value::setNullAccessCallBack](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [kqueue and kevent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel), and [scePadUnknown_n3kSX62fgNo](../../core/libs/prx/libScePad/Export.cpp) (libScePad), whose real name is unknown. [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured. +- [sceVoiceQoSSetConnectionAttribute](../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS, NID `KCGOxg8iX7s`) is a throwing stub declared without parameters, like the other endpoint calls of the library: PPSA04159 fails to load without it and its signature is unknown. The name hashes to the imported NID (`tools/nid_names.py`) +- [Indices far past every vertex fetch](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - when a draw's highest index is at least 64Ki vertices past the largest per-vertex record count, every index past that count is moved to it in a copy of the indices, so the zero-padded vertex copies are not sized to the original index (PPSA04159 draws a list of `0xffffffff` with primitive restart off, which asked for 160 GiB). Each attribute fetched by vertex index reads zeros there as at the original index (out-of-bounds select 0 or 1); a draw with another out-of-bounds select keeps its indices. The vertex index the shader sees changes for the moved indices, which a shader that reads it directly would notice - [sceVideoOutGetOutputStatusWithVrr](../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - the name hashes to the imported NID. PPSA26344 version 02.002.000 passes a VideoOut handle and an output pointer at three call sites. It compares a 32-bit value at byte 4 with 2, tests bit 0x10 at byte 16, and, when set, compares low 16-bit values at bytes 48, 50 and 52 with 48, 60 and 120. The full output layout, size, field meanings and console errors are unknown. Every call throws without reading or writing caller memory. - [sceHttp2WebSocketSetPingInterval](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2, NID `YfaXPnc8PJE`) - imported by `libdiscord_partner_sdk.prx` of PPSA23126 (issue 2648). The parameters, the unit of the interval and the console errors are unknown. Every call throws. +- [sceHttp2DeleteCookieBox](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - imported by PPSA35943's Unity player. The `(int cookieBoxId)` signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http2.cs#L215-L217). A box `sceHttp2CreateCookieBox` returned is discarded; an unknown or already deleted id returns the library's invalid-id error (0x817B1100), as [prosper](https://github.com/mattias800/prosper/blob/b1bec57186a0/prosper/src/hle/net/hle_http2.cpp#L512-L523) does; not measured on a console - [sceKernelGetProsperoSystemSwVersion](../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports system software version `0x01000000` ("01.000.000"); the console's firmware version is not modeled. The 0x28-byte block (a 64-bit size at 0, the version string at 8 and the packed version at 0x24) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which Prospero Multi Tools (PPSA99110) and Prospero Explorer (PPSA99109) are built with and which sets the size to 40, and the same fields in [etaHEN](https://github.com/etaHEN/etaHEN/blob/main/Source%20Code/daemon/include/ipc.hpp) and [shsrv](https://github.com/ps5-payload-dev/shsrv/blob/master/sh.c). A size other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL` without writing; whether the console checks the size is unverified, since etaHEN and shsrv declare a 0x30-byte block and do not set it - [sceKernelGetAllowedSdkVersionOnSystem](../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports the system software version with the 12 patch bits set, `0x01000fff`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/98fd0689/src/core/libraries/kernel/kernel.cpp) does on the PS4 and as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs) describes it, which Prospero Explorer (PPSA99109) reads it through; a null pointer returns `SCE_KERNEL_ERROR_EINVAL`. The console's value and its errors are unverified - [sceSystemServiceLaunchApp](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 runs one title and cannot start another, so every call throws without reading its arguments. The (title ID, null-terminated argv, launch parameters) signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L141-L148), whose `AppLauncher` Prospero Explorer (PPSA99109) and Prospero Multi Tools (PPSA99110) import it through and which passes a 0x20-byte launch context or null; the console's argument checks and errors are unknown @@ -72,6 +91,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceNpEntitlementAccessGetEntitlementKey](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - the three-argument signature, 16-byte key and errors `0x817D0002`/`0x817D0007` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp); PPSA01521 passes a service label, entitlement label and output pointer. Only the no-key path is implemented: local entitlement labels carry no keys, and the output is untouched on failure. Console error precedence and key provisioning are unverified - [sceNpEntitlementAccess{Request,Poll}{Unified,Service}EntitlementInfo{,List}](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - signatures unknown; no reference implements them, so they are declared without parameters. The single-item variants copy the behaviour of the list ones, with no source of their own. Like the consume requests in the same library, they report the signed-out NP state with `SCE_NP_ERROR_SIGNED_OUT` (`0x80550006`) and write nothing. PPSA15657 calls the unified request during start-up and continues past the error. Whether the console fails at the request or at the poll when signed out is unverified - [sceRudpGetStatus](../../core/libs/prx/libSceRudp/Export.cpp) - PPSA01521 passes an output pointer and byte count `0xF8`. Measured on a PS5 (firmware 13.60): the status is 0xF8 bytes; before `sceRudpInit` every call returns `RUDP_ERROR_NOT_INITIALIZED`, and after it a null pointer or a byte count of 0 or above 0xF8 returns `RUDP_ERROR_INVALID_ARGUMENT`. The layout matches `SceRudpStatus` in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp) with `currentContexts` at 0x64 and `allocs`, `frees`, `memCurrent` and `memPeak` at 0xC8 to 0xD4. Only `currentContexts` is reported: the traffic counters stay 0 because nothing is sent, and the allocator counters stay 0 because the library does not allocate from the title's pool (the console reports 6 allocations and 0x1260 bytes right after `sceRudpInit`, and 4 more allocations and 0x3F0 bytes per context). The console also writes `currentContexts` and the allocator counters when the byte count is smaller than their offset; only the requested bytes are written here. +- [sceRudpGetMaxSegmentSize](../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - measured on a PS5 (firmware 13.60) in #3073: before `sceRudpInit` it returns `RUDP_ERROR_NOT_INITIALIZED` and leaves the value untouched, also for a null pointer; after it a null pointer returns `RUDP_ERROR_INVALID_ARGUMENT` and a valid one gets 1410 with result 0. The library keeps no configurable segment size (there is no `sceRudpSetMaxSegmentSize`), so 1410 is always reported. The `uint16_t*` parameter is inferred: the measurement does not show how many bytes the console writes, PPSA03490 passes a pointer to a 16-bit value and [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.cpp) declares the PS3 `cellRudpSetMaxSegmentSize(u16 mss)`, so only 16 bits are written - [sceHttpSetCookieEnabled](../../core/libs/prx/libSceHttp/Export.cpp) - the two-argument signature and 0/1 enable values follow [VitaSDK](https://github.com/vitasdk/vita-headers/blob/master/include/psp2/net/http.h) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp); PPSA01521 passes 0 to disable cookies. Disabling succeeds because the offline HTTP backend has no cookie engine; enabling throws. Invalid enable values use the same error as `sceHttpSetInflateGZIPEnabled`. Like the existing offline HTTP setters, the handle is not validated; requests still fail with the network error. Console behavior is unverified - [sceHmd2Initialize](../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the error the console returns when the PS VR2 runtime is unavailable is unknown; it returns libSceHmd's `UNSUPPORTED_FEATURE` (0x81110016). PPSA01317 passes a pointer to 16 zeroed bytes and only tests the result for non-zero @@ -109,11 +129,12 @@ Throughout the project, every function at every stage either **does exactly what - [sceVideoOutGetResolutionStatus](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the 48-byte layout follows the declarations in ps5-opengl and EVO-PLAYER-PS5 (#1578), which agree; not confirmed on a console. `screenSizeInInch` is 0, the value EDID uses for an undefined size, since the host display's size is not read; `flags` is 0, its bits are unknown. The pane equals the full size because `sceVideoOutSetWindowModeMargins` applies no margins - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown - [s_setkill](../../core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp) (core/shader/recompiler) - decoded as `s_endpgm` for any immediate: measured on RDNA2 hardware (gfx1035), a store before it is visible and nothing after it runs ([#771](https://github.com/boykopovar/AnyPS5/pull/771)); whether the KILL status bit it sets is observable differently from `s_endpgm` is unverified -- [sceRtcParseDateTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console's zone name table is only known from the names measured on a PS5 (firmware 13.60); any other name is assumed to take the military letter offset of its first character, as every measured name outside the table does (`HST`, `MSK`, `XYZ`), so a name the console's table holds but nobody measured gets the wrong offset +- [sceRtcParseDateTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console's zone name table is only known from the names measured on PS5 firmware 13.60 and 13.42; any other name is assumed to take the military letter offset of its first character, as every measured name outside the table does (`HST`, `MSK`, `XYZ`), so a name the console's table holds but nobody measured gets the wrong offset - [sceAudio3dPortOpen](../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - buffer modes 0 and 1 (also selected by the 0x10 and 0x18 parameter sizes) and 3 beds depend on an SDK version check in the module ([prosper](https://github.com/mattias800/prosper)) and throw. A repeated `sceAudio3dInitialize` (`NOT_READY` in shadPS4, 0 in prosper) and `sceAudio3dPortAdvance` on a full queue (`NOT_READY` in shadPS4, unhandled in KytyPS5) throw too - [sceAudiodecDecode](../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - a raw M4AAC stream carries no channel configuration, so the decoder tries the mono/stereo configuration and then configurations 3 to 7 within `ui_max_channels` until one decodes the first access unit, and keeps that one. No reference documents how the library picks the layout (shadPS4 maps the channel count straight to the configuration; SharpProspero only describes the maximum). With the stereo configuration a mono raw stream decodes as 2 channels, so 2 is reported, not 1 - [sceAudio3dObjectReserve](../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - object ids, the `max_objects` limit and the checks of `sceAudio3dObjectReserve` and `sceAudio3dObjectUnreserve` follow [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4), the only reference with a body: ids count up from 1 per port, skipping 0, 0xFFFFFFFF and ids in use, and a failed reserve writes 0xFFFFFFFF - [s_sethalt](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) (core/shader/recompiler) - with SIMM16[0] clear it is a no-op: the RDNA 2 ISA guide describes that value as resuming a halted wave, and a wave that executes it is not halted; measured on a gfx1035 (RDNA 2) wave: `s_sethalt 0` followed by a VALU op completes and gives the normal result. Only SIMM16[0] is read; with it set the instruction throws +- [s_subvector_loop_begin and s_subvector_loop_end](../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge, which SPIR-V requires exactly once per loop header (SPIRV-Tools rejects the module and the NVIDIA driver 615.78.08 crashes compiling it); a loop whose back-edge block becomes unreachable in another way is not handled - [AMPR WriteAddressFromCounterPairOnCompletion](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order - [AMPR command sizes](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_AMPR_ERROR_BUFFER_FULL` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes - [AMPR object layout and compact gathers](../../core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp) - the 24-byte object and offsets (type 0, cursor 4, count 8, capacity 12, pointer 16) follow [ampr_emu assertions](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr_emu_command_buffer_types.h#L138-L143) and its [C declaration](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr.h#L41-L47). AnyPS5 represents the 32-bit type word as low 16-bit type and high 16-bit recording flags. Command encoding is AnyPS5 internal: opcode 29 and three 32-bit words encode the high offset byte, length minus one and low offset word. The 12-byte size reduces HFW's 1 MiB buffer pressure; it is not a claim about console opcode encoding. Existing 40-bit offsets and lengths 1 through 2^32 are retained; console behavior outside those limits is unverified. @@ -124,6 +145,7 @@ Throughout the project, every function at every stage either **does exactly what - [AMM remap, multimap and protection changes](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection and mask checks taken from ampr_emu; the effects are inferred from the names, not confirmed on a title. A remap moves every page of a fully mapped range to the new address with the given protection and leaves the old range reserved; a multimap maps the same pages at a second address, anywhere in the AMM ranges, and a pool page goes back to the pool once its last mapping is unmapped; a multimap onto its own source range throws. A protection change sets the masked bits on every page of a fully mapped range; the memory type is recorded and ignored. A command on a range that is not fully mapped throws - [sceVideodec2GetAvcPictureInfo](../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) - runs the `sceVideodec2GetPictureInfo` body (KytyPS5 binds both NIDs to one function; SharpProspero gives both the same `outputInfo, first, second` signature); the second (bottom field) picture info is never written, since only progressive H.264 output is modelled. prosper calls the AVC layout unestablished (no title observed calling it) - [AMM PRT ranges](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures, checks and their order taken from ampr_emu, where `MapAsPrt` is a map with the PRT flag and protection 0 and `AllocatePaForPrt` a memory type and protection change with mask 1019, both checking the buffer memory after its size; the effects are inferred from the names, not confirmed on a title. Unbacked PRT pages are zero-filled pages with GPU read access, so CPU reads see zeros instead of faulting and GPU writes fault instead of being dropped. `AllocatePaForPrt` backs each unbacked page with a pool page and sets the type and protection of backed ones; `RemapIntoPrt` moves a fully mapped range into a PRT range, its opcode argument (1011 when 0, as in ampr_emu) is recorded and ignored; `UnmapToPrt` returns the pages to the pool and zero-fills them again. A plain map, unmap or remap over a PRT range ends it; `AllocatePaForPrt`, `RemapIntoPrt` or `UnmapToPrt` outside a PRT range throws +- [AMM kernel write commands](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - signatures of `sceKernelWriteModifyMtypeProtectCommand` and `sceKernelWriteRemapIntoPrtCommand` taken from [ampr_emu's libkernel hook targets](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr_libkernel_hook_targets.h), which forward to the real kernel; the measure mode (`dst` null reports the size, `outSize` null skips the report) follows ampr_emu's measure-then-record calls, not confirmed on a title. The range, protection and mask checks mirror the `libSceAmpr` siblings, and the recorded bytes match them exactly so the submitted buffer executes on the existing paths - [APR map begin and end](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - as in ampr_emu, `MapBegin`/`MapDirectBegin` map when they run and `MapEnd` only closes the region; while a region is open a second begin and every completion write return `EPERM`, the at-start `_04_00` writes are accepted - [AMPR markers](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - signatures taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: `sceAmprCommandBufferSetMarkerWithColor` takes the color by pointer, the push and measure variants by value. Colors are not recorded - [sceFontGetRenderScaledKerning](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - arguments, the cleared output and the bound renderer check taken from `cellFontGetRenderScaledKerning` in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellFont.cpp) (PS3 predecessor of the library), not confirmed on a title. The kerning is the `sceFontGetKerning` one at the render scale @@ -135,11 +157,12 @@ Throughout the project, every function at every stage either **does exactly what - [sceFontWritingRefersRenderStepCharacter](../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the second argument is an out pointer to the writing's letter step, as in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1882-L1896) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings. Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): exactly 8 bytes are written, the address of a step stored inside the writing, and a null writing writes null; shadPS4 and prosper treat the argument as a struct, which the measurement contradicts. The letter step lives in the writing, so the pointer stays valid as long as the writing does, and its contents are rebuilt by each call of this function. On the PS4 library repeated calls on one step walk the characters it covers and then return null; here every call returns the step's one character, so the second call differs. Assumed: each step covers one character and one glyph. PPSA06699 (Hot Shots Golf 2) dereferences the returned pointer for its character counts. - [sceFontWritingLineGetOrderingSpace](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - the four outputs are written in the order the PS4 11.00 `libSceFont.sprx` writes them (line advance, leading space, space between spaced runs, trailing space); the parameter names in the [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings order them differently (head, inline, tail, advance), and the PS5 library was not measured. PPSA01325's only call uses them in the PS4 order: it computes first - second - fourth - 0.25 * third and compares that, divided by a scale, with its width limit. The PS4 library writes the trailing space through its pointer without a null check whenever the leading-space pointer is set; that call throws here - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits -- [sceCoredumpWriteUserString](../../core/libs/prx/libkernel/System/src/Coredump.cpp) and [sceSaveDataConvert, sceSaveDataGetConvertProgress](../../core/libs/prx/libSceSaveData.native/Export.cpp) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signatures +- [sceCoredumpWriteUserString](../../core/libs/prx/libkernel/System/src/Coredump.cpp) and [sceSaveDataGetConvertProgress](../../core/libs/prx/libSceSaveData.native/Export.cpp) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signatures +- [sceSaveDataConvert](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the parameter block (user id at `+0x00`, `SceSaveDataDirName` pointer at `+0x08`, 48 opaque bytes after it) is read from PPSA28416's single call, not confirmed by KytyPS5, shadPS4, fpPS4 or SharpProspero, which have no body for it. Host saves have no size limit, so an existing save returns `SCE_OK` without touching its files and no event is queued; what the console returns for a save that is mounted, already converted or needs no conversion is unknown. The directory name is validated as `sceSaveDataDelete` does - [sceSaveDataCancel](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA23126, which fails to load without it; added without an implementation because its signature is unknown. Every save data call here finishes before it returns, so nothing is ever in flight to cancel, but what the console returns then is unknown - [sceAudioOutExClose, sceAudioOutExConfigureOutput, sceAudioOutExGetMonitorInfo, sceAudioOutExOpen](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99505, which fails to load without them; added without an implementation and declared without parameters. PPSA99505 calls them only for its HDMI bitstream output, an option that is off by default, to read the formats the TV or receiver lists, switch the HDMI output mode and open a port for IEC 61937 bursts ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L110-L179)); [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp) also calls `sceAudioOutExOpen`, `sceAudioOutExConfigureOutput` and `sceAudioOutExClose` for HDMI bitstream output. The two declare the arguments of `sceAudioOutExConfigureOutput` differently and neither is confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no encoded HDMI output to switch to - [sceOpusCeltDecCreateEx, sceOpusCeltDecDecode, sceOpusCeltDecDestroy, sceOpusCeltDecGetSize, sceOpusCeltDecInitialize, sceOpusCeltDecTerminate](../../core/libs/prx/libSceOpusCeltDec/Export.cpp) (libSceOpusCeltDec) - imported by PPSA99001, which fails to load without the module; added without an implementation. Their signatures match libSceOpusDec's in [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). PPSA99001 opens this decoder only as a fallback when libSceOpusDec reports a retryable error for a CELT packet, which [libSceOpusDec](../../core/libs/prx/libSceOpusDec/Export.cpp) here never returns, so the fallback is not reached -- [sceContentDeleteById](../../core/libs/prx/libSceContentDelete/Export.cpp) (libSceContentDelete), [sceContentExportGetProgress, sceContentExportCancel, sceContentExportFromDataWithThumbnail](../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport) and [sceContentSearchGetContentLastUpdateId, sceContentSearchOpenMetadataByContentId, sceContentSearchGetTotalContentSize, sceContentSearchGetNumOfContent](../../core/libs/prx/libSceContentSearch/Export.cpp) (libSceContentSearch) - imported by PPSA06465, which fails to load without them; added without an implementation because their signatures are unknown +- [sceContentDeleteById](../../core/libs/prx/libSceContentDelete/Export.cpp) (libSceContentDelete) and [sceContentSearchGetContentLastUpdateId, sceContentSearchOpenMetadataByContentId, sceContentSearchGetTotalContentSize, sceContentSearchGetNumOfContent](../../core/libs/prx/libSceContentSearch/Export.cpp) (libSceContentSearch) - imported by PPSA06465, which fails to load without them; added without an implementation because their signatures are unknown - [sce::Json::Parser::parse with a read callback](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the callback's contract is taken from PPSA04540's: it returns 0 with the next character, and at the end of its data writes `'\0'` and returns -1. Characters are read until the callback returns non-zero or gives `'\0'`, and the text read is then parsed as by the `(text, size)` overload, so trailing characters after the value are rejected as there. Whether the console stops reading at the end of the top-level value, and what it returns when the callback fails before the end, are unknown - [sce::Json::Parser::parse](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown - [sceUserServiceGetNpAccountId](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the account id for a user with no linked PSN account is assumed to be 0 with success; libSceNpManager's `sceNpGetAccountIdA` answers the same question with `SCE_NP_ERROR_SIGNED_OUT` @@ -148,6 +171,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceGameLiveStreamingGetCurrentStatus2](../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - the status layout (user id, on-air flag, spectator, text and command message counts, broadcast resolution, 48 reserved bytes) is taken from shadPS4's `OrbisGameLiveStreamingStatus2`; the call reports that no stream is on air, with an invalid user id - [Depth clear draws](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a draw with `DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE` is taken to write `DB_DEPTH_CLEAR` to every pixel it covers whatever the depth test, depth bounds and depth bias say: it is drawn with an always-passing depth write and the viewport depth range pinned to the clear value. The draw is rasterized as submitted, so its coverage is the draw's own. Whether the DB still applies the depth test or HiZ to such a draw is not measured - [Alpha-to-coverage](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - only `DB_ALPHA_TO_MASK = 0xaa01` is accepted: the enable bit with the four quad offsets at 2 and no `OFFSET_ROUND`, the undithered setting [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/gallium/drivers/radeonsi/si_state.c#L371-L381) programs. Draws are single-sampled, so the pixel shader [writes](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `SampleMask` 0 when its MRT0 alpha is below one half or NaN and all ones otherwise, and runs with late fragment tests. That rule was measured on a Radeon 660M (gfx1035, RADV with `RADV_DEBUG=noatocdithering`, which programs the same register) for 0, -0, 0.25, the two floats below one half, one half, the two above, 0.75, 1, 2, -1, both infinities and both NaNs, and is pinned in [AlphaToCoverage.cpp](../../core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp); it is not measured on gfx1013. Vulkan's `alphaToCoverageEnable` is not used: the NVIDIA driver 615.78.08 dithers single-sample coverage (an alpha of 0.25 writes half the pixels of an RTX 3050 Laptop target). Other offsets, `OFFSET_ROUND`, a sample-mask export, and an MRT0 that is missing, swizzled, packed or exported without a floating-point alpha throw +- [MSAA_ENABLE on a single-sample target](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_MODE_CNTL_0.MSAA_ENABLE` with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES = 0` is drawn like the same state without the bit when sample 0 of the four `PA_SC_AA_SAMPLE_LOCS_PIXEL_*_0` registers is at the pixel center and the draw rasterizes filled triangles with alpha-to-coverage and conservative rasterization off. [RADV](https://gitlab.freedesktop.org/mesa/mesa/-/blob/745709a000482a4a24c91faef5c5a40bd653a863/src/amd/vulkan/radv_cmd_buffer.c#L13606-13609) programs this state for one-sample custom sample locations on GFX10 and later, where the bit makes the rasterizer use the programmed location of sample 0 ([061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)); that the bit changes nothing else for such a draw is inferred from that and is not measured on hardware. Other locations of sample 0, absent location registers, points, lines, rectangles, alpha-to-coverage and conservative rasterization throw - [GPU flips](../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - a command-buffer flip of a buffer index with no registered buffer when the GPU reaches it presents black and completes like any flip (flip event, count, `flipPendingNum`). The console's behaviour is unknown; PPSA05684 requires that it completes: its renderer submits a flip of buffer 0 while another thread waits for `sceVideoOutIsFlipPending` to reach 0 before registering the buffers, and `flipPendingNum` counts that flip from its submission as in shadPS4 - [sceAgcDriverGetEqContextId](../../core/libs/prx/libSceAgcDriver/Eq/src/Query.cpp) (libSceAgcDriver) - returns `ident`: an end-of-pipe interrupt is delivered only to the registration whose id equals the raising queue, so `ident` is that context's id (titles register 0 or 1 for graphics and `0x20 + 8 * pipe + queue` for compute). The console's value is not known; KytyPS5 returns the event's `data`, which it fills with the interrupt's context id, whereas `data` here is the EV_CLEAR trigger count. Other event filters throw - [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted @@ -160,6 +184,7 @@ Throughout the project, every function at every stage either **does exactly what - [Strided buffer overhang](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a strided buffer descriptor that ends less than one stride past the end of the readable registered allocation holding its base is bound only up to that allocation's end, even when another readable allocation starts right there. This assumes the bytes past the end are the rounded-up `num_records` of the last record (RADV rounds it up), which no fetch inside the buffer reads; a guest that really keeps a vertex split across two adjacent allocations would read zeros for the cut tail. Taken from the RetroArch Dolphin core case in #2912, not measured on hardware. - [Zero-stride vertex fetch range](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - for a stride-0 V# with OOB_SELECT 2 the whole fetch reads memory unless NUM_RECORDS is 0; with OOB_SELECT 3 it reads zero when NUM_RECORDS is 0 and throws when the fetch only partly fits. Rules taken from the GFX10 OOB_SELECT table in Mesa [ac_descriptors.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/ac_descriptors.c), not measured on hardware. Whether OOB_SELECT 3 checks each dword of a multi-dword fetch separately is unknown. OOB_SELECT 0 and 1 keep the structured record checks. - [sceAgcCbCondWrite](../../core/libs/prx/libSceAgc/Cb/src/Branch.cpp) (libSceAgc) - argument order and the 9-dword COND_WRITE layout follow the clean-room [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Agc/AgcExports.CommandPackets.cs), which agree on them; SharpProspero lists another order. The write space is accepted only as 1 (memory), the one value both encode the same way, and other values throw. +- [sceAgcAcbAtomicGds_0900](../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - signature, control/mode bit mapping and the 11-dword ATOMIC_GDS (0x1d) layout follow the clean-room [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, which derives them from the library's disassembly; KytyPS5 and shadPS4 have no body for it. Not confirmed on hardware or a title (PPSA07286 imports it, nothing was run), and the driver has no ATOMIC_GDS handler (opcode 0x1d is not executed as an atomic GDS operation) - [sceAgcGetIsTrinityMode](../../core/libs/prx/libSceAgc/Misc/src/Platform.cpp) (libSceAgc) - signature from the only PPSA26344 call (a pointer to a one-byte flag, result ignored); a null pointer throws because its error code is unknown - [sceAgcDcbAtomicGds_0900](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - name from its NID (`zARR5aCmkoY`), the DCB counterpart of `sceAgcAcbAtomicGds_0900`, signature unknown - [sceAgcDcbContextStateOpGetSize](../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - sizes from KytyPS5 only (5, 27, 27 and 32 dwords), equal to what `sceAgcDcbContextStateOp_0100` writes; KytyPS5 returns 0 for an operation above 3, here it throws @@ -185,12 +210,13 @@ Throughout the project, every function at every stage either **does exactly what - [_umtx_op](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE` and `UMTX_OP_NWAKE_PRIVATE` follow FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): a timeout whose size argument is at most 16 is a relative `timespec`, a larger one an `_umtx_time`, and a wake of 0 or fewer wakes one waiter. `UMTX_OP_NWAKE_PRIVATE` reads the value as an `int` count of pointers at the object and wakes every waiter on each one; a count of 0 or fewer reads nothing, and a null list fails with `EFAULT`, where FreeBSD's `copyin` would also fail for any other unreadable list. Waiters share the queue of `sceKernelSyncOnAddressWait`. Operation 0 fails with `EOPNOTSUPP` and operations above 26 with `EINVAL`, as in FreeBSD 11; `UMTX_OP_WAIT_UINT` with a value that does not fit in 32 bits returns 0 at once, as FreeBSD's zero-extended compare does. Operation 1 throws, since PPSA12544's call_once uses it in a way FreeBSD rejects; the other operations throw, and so does a misaligned object. PPSA12544's `libc.prx` passes `UMTX_OP_WAIT` and `UMTX_OP_WAIT_UINT` a pointer to a 4-byte microsecond timeout with size argument 0, which FreeBSD reads as a `timespec`, and waits for a running call_once with operation 1; its `PS5Util.prx` passes a `timespec`. Whether the console reads a microsecond timeout there, and what operation 1 does on it, is unverified. A libc call cannot be told apart from a `timespec` call by the size argument alone, so a timeout with size argument 0 is read as a `timespec` and only throws when it is not a valid one (negative seconds, nanoseconds outside [0, 1e9)), which is where FreeBSD would return `EINVAL`; a microsecond value that happens to read as a valid `timespec` is waited on as one - [sceKernelSyncOnAddressWait](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed -- [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - a rwlock held for reading or writing returns `EBUSY` and keeps its handle, as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does; FreeBSD's [libthr](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_rwlock.c) and shadPS4 free it without checking, and the console's answer is unverified. Destroying a rwlock while another thread is acquiring it is not guarded +- [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded - [scePthreadCondWait / scePthreadCondTimedwait](../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written +- [scePthreadAttr setters and getters](../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs - [sceKernelGetAvailableCpumask](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - returns the default thread affinity, 0x1FFF (CPUs 0 to 12), as KytyPS5 does. Titles pin threads within it (PPSA02664 passes 0x1FFB, Hades II PPSA36082 0x3 and 0x3F); whether CPU 13 is available to a title is not confirmed - [sceKernelGetCurrentCpu](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - guest affinities are not applied to host threads, so the host CPU index is mapped into the calling thread's affinity clipped to `sceKernelGetAvailableCpumask` (the available mask when they do not overlap): host CPU n reports the (n modulo the CPU count)-th CPU of that mask, so a thread pinned to one CPU reports that CPU, as a FreeBSD `cpuset` guarantees on the console. Which CPU the console reports for a thread allowed on several is not known; shadPS4 returns 0 -- [pthread_attr_setsolosched_np](../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) takes `(attr, int)` and stores the value through `scePthreadAttrSetsolosched`, returning `EINVAL` (22) for a null or uninitialized attribute like the other POSIX attribute setters. Only the name is public (aerolib `2+pVfgiEd7A`); the signature and behaviour follow the SCE twin, which [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/pthread.cpp#L2188-L2199) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs#L891-L913) store as an `int` without a range check, and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/71de7681f06c5db59a1120a3bb554c86780c3f5f/src/hle/libs/kernel_threading.zig#L1396-L1398) implements this alias over it. Any value is accepted and threads created from the attribute are not scheduled differently; what solo scheduling does on the console and which values it rejects are unverified. Returnal (PPSA01285 v03.000.005) stopped on it with `what(): pthread_attr_setsolosched_np_nid_postfix not implemented` (#2872, #1155); LEGO Horizon Adventures (PPSA14632, #734) and Palworld (PPSA23126, #2598) import it +- [pthread_attr_setsolosched_np](../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) takes `(attr, int)` and stores the value through `scePthreadAttrSetsolosched`, returning `EINVAL` (22) for a null or uninitialized attribute like the other POSIX attribute setters. Only the name is public (aerolib `2+pVfgiEd7A`); the signature and behaviour follow the SCE twin, which [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/pthread.cpp#L2188-L2199) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs#L891-L913) store as an `int` without a range check, and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/71de7681f06c5db59a1120a3bb554c86780c3f5f/src/hle/libs/kernel_threading.zig#L1396-L1398) implements this alias over it. On a PS5 (firmware 13.60) only 0 and 16 are accepted; every other value returns `ENOTSUP` (45, `0x8002002D` from the SCE call) and keeps the stored one, and the SCE calls return `EINVAL` for a null or uninitialized attribute. PPSA15878 passes 16. Threads created from the attribute are not scheduled differently; what solo scheduling does on the console is unknown. Returnal (PPSA01285 v03.000.005) stopped on it with `what(): pthread_attr_setsolosched_np_nid_postfix not implemented` (#2872, #1155); LEGO Horizon Adventures (PPSA14632, #734) and Palworld (PPSA23126, #2598) import it - [sceKernelMapNamedFlexibleMemoryInternal](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - flag 0x8000 is accepted and ignored; its meaning is not established. KytyPS5 (`GUEST_MAP_UNKNOWN_8000`) and sharpemu accept it without reading it. Every known caller is the SDK 9 `libc.prx` heap (Astro Bot PPSA21564, PPSA22520), which passes it with read/write protection and uses the mapping as an older `libc.prx` uses `sceKernelMapNamedFlexibleMemory` with flags 0 - [sceKernelMtypeprotect](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - sets the memory type of the direct mappings in the page-rounded range and of the direct memory behind them, as shadPS4 and prosper do (KytyPS5 ignores the type); the type is not validated, flexible pages in the range only get the protection, and a later map of the retyped direct memory reports the type it was allocated with. `sceKernelBatchMap2` operation 4 takes the same path with the entry type - [Map hints inside a reservation](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMapDirectMemory` and the flexible and anonymous maps place a mapping without `MAP_FIXED` at its hint address when the whole range lies in a range still reserved by `sceKernelReserveVirtualRange`, as if `MAP_FIXED` were set. Taken from PPSA01474, which reserves 64 GiB and then maps its heaps inside it with hints and no `MAP_FIXED`, and uses the hint addresses afterwards; not measured on the console. Hints outside a reservation, or over a range that is partly mapped, stay hints @@ -198,11 +224,13 @@ Throughout the project, every function at every stage either **does exactly what - [sceLibcInternalBacktraceForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature - [sceLibcInternalHeapErrorReportForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature - [malloc_usable_size](../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` +- [reallocf](../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 frees the block and returns null as `realloc` does here, so the block is not freed twice; when `realloc` fails because the application allocator returns null or the default heap rejects the size, the block is freed through the same allocator, `errno` is `ENOMEM` and null is returned. Other failures (an exhausted guest arena, an unknown pointer) throw, as they do for `realloc` - [_sceLibcInternalThreadAtexit, _sceLibcInternalThreadDtors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - FreeBSD `__cxa_thread_atexit` semantics assumed; a null or non-image destructor and an allocation failure throw - [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in the [fpPS4](https://github.com/red-prig/fpPS4) reimplementation, Increment takes a reference on the loaded image containing the address and Decrement releases one; the variants without the leading underscore are assumed to match - [sceImeGetPanelPositionAndForm](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills; it is never written, since no panel can be open. That a closed panel is reported before a null output is assumed by analogy with `sceImeSetCaret`, not confirmed on a PS5 title - [_sceLibcInternalForceTlsDestructor](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - behaviour follows `sceLibcForceTlsDestructor(int)` in a title's `libc.prx` (disassembly): the calling thread's destructors whose dso belongs to the module run newest first, only if the dso's module is loaded and the dso points to itself, in up to 4 passes. The return type is unknown (that function returns nothing), 0 is returned. The module of each dso is looked up when the destructors are forced, not when they are registered as `libc.prx` does - [acosl](../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, like the other libc math functions here, not by the console's (FreeBSD msun ld80 code in Sony's `libc.prx`): results can differ from the console's in the last bit, and on Linux glibc sets `errno` to `EDOM` for `acosl` outside [-1, 1] where msun only raises the floating-point exception. `long double` is the x87 80-bit format, as Sony's `frexpl` uses it (`fldt 0x8(%rsp)`, result in `st(0)`) +- [atanl, sinl, cosl, tanl, sinhl, coshl, tanhl, expl, exp2l, logl, log10l and powl](../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, as `acosl` is, not by the console's FreeBSD msun ld80 code: results can differ from the console's in the last bit (glibc's `log10l(1000)` is not exactly 3), and on Linux glibc sets `errno` to `EDOM` or `ERANGE` where msun only raises the floating-point exception. `ldexpl` and `truncl` are exact operations and give the console's result; `logb` and `remainder` give the console's value too, but on Linux glibc sets `errno` (`ERANGE` for `logb(0)`, `EDOM` for `remainder` by zero or of an infinity) where msun only raises the floating-point exception - [std_execute_once](../../core/libs/prx/libc/Export.cpp) (libc) - assumed to be the plain-named twin of `std::_Execute_once` (`_ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1_`, the callee of Dinkumware `call_once`): it delegates there and gets the [thread.once.callonce] semantics already verified for that symbol (one invocation per flag, concurrent callers block until it completes, and a failing or throwing invocation leaves the flag unready so the next caller runs it). The name equivalence is not confirmed on a title, and the callback convention (called with (null, arg, null), non-zero return meaning success) is the twin's, unverified for this symbol - [Dinkumware mutexes and conditions](../../core/libs/prx/libc/src/Threads.cpp) (libc) use pointer-to-handle arguments, matching the console aerolib signatures reported in [#3079](https://github.com/boykopovar/AnyPS5/pull/3079). The title's `_Mtx_lock` maps `0` to `_Thrd_success` (0), `0x8002000b` to `_Thrd_busy` (3), and other failures to `_Thrd_error` (4); its internal mutex init recognizes recursive flag `0x100`, while every other type uses default attributes. The observed relock failure for a plain mutex suggests those defaults are error-checking, but that has not been confirmed on hardware. The title's `_Mtx_unlock` and `_Cnd_wait` return success regardless of their internal result. The host implementation treats invalid ownership preconditions as no-ops, and throws for invalid handles, host wait errors and destroying a locked mutex as debug guards; those cases are not confirmed against the console. - [random, srandom, initstate, setstate](../../core/libs/prx/libc/src/Random.cpp) (libc) follow FreeBSD 11 [random.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdlib/random.c): the additive feedback generator on a default 128-byte `TYPE_3` table, `good_rand` seeding, state sizes rounded down to 8, 32, 64, 128 or 256 bytes, and `setstate` returning the previous state or null for a rear position past the degree. `initstate` below 8 bytes returns null without setting errno, as that file and its manual do (glibc sets `EINVAL`). FreeBSD does not lock the generator; here every call is serialised by one lock, so concurrent callers draw the serial sequence between them. A null state throws, where FreeBSD faults. That the console's `libSceLibcInternal` uses this FreeBSD code and not another generator is unverified @@ -211,7 +239,7 @@ Throughout the project, every function at every stage either **does exactly what - [select](../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - a descriptor the socket poller reports as unknown that is below `GuestSockets::FirstDescriptor` is polled on the host instead of throwing, except on Windows where it still throws; an invalid host descriptor fails with `EBADF`, and mixed host and socket sets are polled in short alternating slices so neither side waits out the full slice alone. Wobbly Life (PPSA29413) selects on file descriptors while streaming its bundles - [__progname](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export - [environ](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - data export pointing to a fixed empty environment so titles that import it load; it is not the environment libc's `getenv` reads, so it lists none of those variables and `setenv`/`putenv` changes do not appear in it -- [seteuid, setegid](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid` and `sys_setegid`, id 0 is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change the effective id, which is not modelled, and throws. The ids and privileges a title has on a console are unverified +- [seteuid, setegid, setuid, setreuid, setgroups, getgroups](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid`, `sys_setegid`, `sys_setuid` and `sys_setreuid`, id 0 (or -1 for an unchanged `setreuid` id) is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change an id, which is not modelled, and throws. `setgroups` keeps the supplementary group list as `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 would change the effective group and throws) and `getgroups` reports it. The ids and privileges a title has on a console are unverified - [pthread_barrierattr_setpshared](../../core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp) (libkernel) - PTHREAD_PROCESS_SHARED is accepted on the assumption that the console follows FreeBSD 11.0 libthr, which accepts it (9.0 rejects it with EINVAL); unverified on hardware. Guest code runs in one process, so a shared barrier behaves as a private one - [pthread_getname_np](../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - a thread that has already been joined returns `ESRCH` (3) on a PS5 (firmware 13.60), but its object is freed after the join here, so the pointer cannot be told apart from a live thread and its name is read as for a live one - [sceSslClose](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature @@ -222,7 +250,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAudioOutSetMixLevelPadSpk](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - effect of a negative mix level unknown; only 0 to 32768 (0 dB) is accepted - [sceAudioOutSetMixLevelPadSpk](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - no source applies the level (shadPS4 only stores it); it is assumed to be a linear gain on the port's own output, and the -9 dB default (11626) is applied to every pad speaker port, including those of titles that never call the function - [sceAudioOutGetLastOutputTime](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - signature and behaviour taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [fpPS4](https://github.com/red-prig/fpPS4) reimplementations of the PS4 library, not confirmed on a PS5 title: the time is the process time at which `sceAudioOutOutput` or `sceAudioOutOutputs` accepted the port's last block, not the time the block reached the device -- [sceAudioOutSysClose, sceAudioOutSysConfigureOutput, sceAudioOutSysGetHdmiMonitorInfo, sceAudioOutSysOpen](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99051 and PPSA99997, which fail to load without them; added without an implementation and declared without parameters. [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp#L166-L240), whose audio code both titles share, calls `sceAudioOutSysGetHdmiMonitorInfo` to read the formats the TV or receiver lists ([bitstream.cpp](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/bitstream.cpp#L71)) and `sceAudioOutSysOpen`, `sceAudioOutSysConfigureOutput` and `sceAudioOutSysClose` only to open a port for HDMI bitstream passthrough. Its declared arguments are not confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no HDMI sink to describe or switch +- [sceAudioOutSysClose, sceAudioOutSysConfigureOutput, sceAudioOutSysOpen](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99051 and PPSA99997, which fail to load without them; added without an implementation and declared without parameters. [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp#L166-L240), whose audio code both titles share, calls `sceAudioOutSysOpen`, `sceAudioOutSysConfigureOutput` and `sceAudioOutSysClose` only to open a port for HDMI bitstream passthrough. Its declared arguments are not confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no HDMI output to switch - [sceAudioOut2PortCreate](../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - MAIN (type 0) format validation follows PS5 firmware 13.60 measurements: bits 8-11 accept 1, 2, 8 or 12, bits 0-6 accept 0 or 1, bit 7 requires 8 channels, and bits 12-31 do not affect validation. The sample types are still read as float and int16; bit 7's layout, the PCM effect of bits 12-31 and the port `flags` are unverified. Other port types retain unverified format validation, including 6 channels and rejection of bits 12-31 - [sceAudioOut2PortCreate](../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - 12 channels are assumed to be L R C LFE Ls Rs Lb Rb Ltf Rtf Ltb Rtb as in KytyPS5's 12-to-8 fold, and 6 and 8 channels to be its first 6 and 8; no title or SDK header confirms the order - [sceAudioOut2PortCreate](../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - the stereo fold is assumed, not the console's downmix: C at -3 dB into both sides, Ls/Rs, Lb/Rb and Ltf/Rtf at -3 dB and Ltb/Rtb at -6 dB into their own side, LFE dropped @@ -235,10 +263,10 @@ Throughout the project, every function at every stage either **does exactly what - [sceVoiceQoSInit](../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. The app type and the block are not used, as no endpoint is implemented - AudioIn and NpSessionSignaling exports added without an implementation have assumed signatures - [sceNpSessionSignalingGetConnectionFromPeerAddress](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - imported by PPSA01289 and added without an implementation; unknown signature -- [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Unimplemented.cpp) (libSceAgc) - unknown name, signature +- [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - unknown name; the `(regs, gs, ps)` arguments of `sceAgcCreateInterpolantMapping_0100` come from OpenAGC only - [scePadIsRemoteController](../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the name hashes to NID `fCWdlnmB1Ks`; the signature `(int handle, bool* remote)` comes from PPSA16064, which zeroes a byte, passes its address after `scePadGetControllerInformation` and ignores the result. The error codes follow the other pad calls. Controllers are local devices, so the answer is always false - [scePadSetAngularVelocityBiasCorrectionState](../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled -- [sceKernelGetOperationMode](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call; the values are unknown and 0 is reported for both, as in the [prosper](https://github.com/mattias800/prosper/pull/4153) reimplementation. Null outputs throw +- [sceKernelGetOperationMode](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call and ignores the result. A homebrew payload on a PS5 (firmware 13.60) reads mode 2 and submode 0, nothing else is written, and the result register holds no status, so the function is taken to return nothing. Whether a title process reads the same values is unverified. Null outputs throw - Font, Http2, Net, Ssl, SystemService and libkernel exports imported by PPSA12544 and added without an implementation have unknown signatures - [sceHttp2GetMemoryPoolStats, sceSslGetMemoryPoolStats, sceNetGetMemoryPoolStats](../../core/libs/prx/libSceNet/Export.cpp) (libSceHttp2, libSceSsl, libSceNet) - the signatures `(int id, stats*)`, the layouts (pool size, peak and current use, and for Http2 and Ssl a 32-bit reserved field) and the error codes were measured on a PS5 (firmware 13.60). The pool size is the one given at init or pool creation; nothing is allocated from these pools, so the peak and current use are 0, where the console already reports use right after initialization (0x15C0 bytes for Ssl, 0x1640 for Http2 and 4 for a Net pool) - [sceNpWebApi2GetMemoryPoolStats](../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - imported by PPSA21703's Unity PSN plugin. The signature `(int libCtxId, stats*)` and the layout of the Http2 statistics above (measured on a PS5 for Http2, Ssl and Net) are assumed for this library, not measured. The pool size is the one given to `sceNpWebApi2Initialize`, and the peak and current use are 0. The error codes are unknown, so a null output or an unknown context throws @@ -255,6 +283,7 @@ Throughout the project, every function at every stage either **does exactly what - [NGS2 silent waveform blocks](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native): flag `0x10` (named `SILENCE` here; no public reference names it) lets a waveform block declare samples without data. PPSA04867 adds one block with flags `0x11`, `data_size` 0 and `num_samples` 16385, then another with flags `0x11` and a fully specified 0x800-byte, 1024-sample mono block, which takes the ordinary path. A dataless block under the flag outputs `num_samples` frames of silence, reports them through `decoded_data_size`, and finishes like a complete block; a dataless block without the flag still throws as not fitting its data. The flag's real name, its interaction with ATRAC9 and repeats, and the console's `waveform_data` for silent blocks are unverified on hardware - [NGS2 streaming PCM waveforms](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - PPSA21402 streams its music by adding one block whose `num_samples` is the whole track and whose `data_size` is a 16 KiB first piece, with `CONTINUE`, and then adds each following piece with flag `0x2` (called `APPEND` here) and the same `num_samples`. No reference describes flag `0x2`. A 16-bit PCM block without skipped samples or repeats whose `num_samples` exceed its data is assumed to start such a stream; flag `0x2` is assumed to append the block's data to the last streaming block, ignoring its `num_samples`; the voice is assumed to output silence without advancing while it has played all appended data and `CONTINUE` is set; and the waveform is assumed to end at `num_samples` or, once `CONTINUE` is cleared, at the end of the appended data. `sceNgs2VoiceGetState` reports the bytes played as `decoded_data_size`, which the title uses to release its queued data, and the read position as `waveform_data`. Appending to a block that is not a stream, or other formats, skipped samples, repeats and partial frames, throw - [NGS2 matrix levels](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - levels are clamped to -4..4 and NaN is kept, as the PS4 firmware 11.00 `libSceNgs2.sprx` stores them; not yet confirmed on the PS5 library. That library rejects a level count of 0 and keeps the matrix; the PS5 error code is unknown, so a count of 0 throws. `sceNgs2VoiceRunCommands` is not in that library, so its matrix levels command is assumed to clamp the same way +- [NGS2 sampler voice state flag EMPTY](../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a playing sampler voice that still accepts blocks (`CONTINUE`) and has played all queued blocks reports `INUSE|PLAYING|EMPTY` (0x20) and keeps playing, so blocks added later play on. PPSA18391 (engine: Clickteam Fusion) reads `state_flags & 0x22 != 2` from `sceNgs2VoiceGetState` as the end of a sound and only then reuses the voice. When the console sets and clears EMPTY, and whether it also reports it for other voice types, is unverified on hardware. - [sceAvPlayerStartEx](../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - start info layout unknown; it is ignored and playback starts as with `sceAvPlayerStart` - [sceAvPlayerInit / sceAvPlayerInitEx](../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - behaviour without a memory replacement unknown; frame and sample buffers then come from the guest heap - [sceAvPlayerInitEx](../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - the PS5 init data layout (`this_size` 0x230: auto start at 0x74, nine 0x30-byte thread parameter blocks from 0x78, output video frame buffer count at 0x228) follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/avPlayer.cpp), [prosper](https://github.com/mattias800/prosper/blob/main/prosper/src/hle/video/avplayer.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Media/AvPlayer.cs) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs), which agree on it. PPSA25063 fills the same fields, writes auto start 0 and starts playback with `sceAvPlayerStart`; a title that sets auto start has not been seen. A smaller `this_size` is read with the PS4 offsets. The thread parameters are ignored @@ -267,7 +296,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag, VBRI, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description - [NGS2 port matrix](../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a port without a matrix, or with a selected matrix whose levels were never set, mixes through a default channel map: mono reaches both stereo channels at -3 dB and the 5.1 and 7.1 centre, stereo folds to mono at -3 dB per side, and wider sources fold down with 0.5 and -3 dB gains. The map and the equivalence of an unset matrix with no matrix were measured from the PS4 firmware 11.00 `libSceNgs2.sprx`, not yet from the PS5 library. That library only accepts submixer and mastering voices with 1, 2, 6 or 8 channels, so the map into 3, 4, 5 or 7 channels throws. A matrix set with fewer levels than the destination needs still mixes only the rows it has - [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts -- [AJM Opus decoder](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout (`u32 channels, u32 sample rate, u32`, seen as `2, 48000, 0`) and the little-endian `u16` byte count before each packet come from one title (The Smurfs Dreams); only 48000 Hz and a zero third word are accepted +- [AJM Opus decoder](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout and little-endian `u16` packet byte count come from The Smurfs Dreams (`2, 48000, 0`) and Lost Soul Aside (`4, 48000, 1`). The third word is interpreted as the mapping family, as in [KytyPS5's initialize structure](https://github.com/KytyPS5/KytyPS5/blob/984cfcebd141b211aa9e0a8cde63b0ebe652d07a/src/libs/ajm/opus_decoder.h#L11-L15). Family 1 uses the conventional stream counts, coupled counts and channel map from [libopus 1.5.2's surround layout](https://github.com/xiph/opus/blob/v1.5.2/src/opus_multistream_encoder.c#L47-L55), passed to FFmpeg through an [RFC 7845 section 5.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1) header with zero pre-skip and gain; gapless skipping remains controlled by AJM sideband parameters. FFmpeg returns PCM in its native speaker order. The implicit stream layout, PCM channel order and invalid family-0 channel count result are not verified on a console. Only 48000 Hz and mapping families 0 and 1 are implemented; other rates and families throw - [sceAjmBatchJobSetResampleParametersEx](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `(info, instance, float ratio_start, float ratio_change_per_sample, flags, result)` signature is inferred from the name and from `sceAjmBatchJobSetResampleParameters`; with a zero change per sample it sets the ratio as `sceAjmBatchJobSetResampleParameters` does (Watch Dogs: Legion, PPSA01500, calls it so). How a non-zero change ramps the ratio is unknown and throws - [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET (bit 13), INITIALIZE (bit 14) and the sideband input order (gapless decode, then initialize parameters) come from [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4); other flags throw - [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET is assumed to clear the context as sceAjmBatchJobClearContext does @@ -286,18 +315,21 @@ Throughout the project, every function at every stage either **does exactly what - [sceHttpSetRedirectCallback, sceHttpSetCookieRecvCallback, sceHttpsSetSslVersion, sceHttpsGetSslError](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) (PS4), which only logs and returns 0; a null `errNum` or `detail` in `sceHttpsGetSslError` is assumed to return `SCE_HTTP_ERROR_INVALID_VALUE` like `sceHttpGetLastErrno` - [sceHttpSetAuthInfoCallback](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature and callback type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h) (PS4); no PS5 reference has them. The `id` is not checked, as in the other libSceHttp setters - [libSceHttp2 cookie boxes](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - `sceHttp2CreateCookieBox` is taken to take the library context id first, as in the [prosper](https://github.com/mattias800/prosper) reimplementation; further arguments are unknown and ignored. `sceHttp2SetRequestNoContentLength` is assumed to take only the request id. Cookies are never stored, so `sceHttp2CookieFlush` has nothing to discard +- [sceUserServiceGetUserName](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - `0x80960105`, which a PS5 (firmware 13.60) returns for a user id it does not know, has no known name; it is `USER_SERVICE_ERROR_UNKNOWN_USER` here. On the console a registered user who is not logged in still gets their name; here only the initial user exists - [sceNpSessionSignalingGetMemoryInfo](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature - [sceNpSessionSignalingGetConnectionStatistics](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature -- [sceUserServiceGetAgeLevel](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the meaning of the value is unknown; 0 is reported, as for the unset game presets. PPSA12544 (Unity) only stores it for its user profile +- [sceUserServiceGetAgeLevel](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - on a PS5 (firmware 13.60) the value is the age set in the user's parental restriction (0, 7, 12 and 15 were measured) and `0xFFFFFFFF` without one; a user who is not logged in returns `0x80960009` with or without an output. Parental restrictions are not modelled, so the initial user is always reported as unrestricted. PPSA12544 (Unity) only stores the value for its user profile +- [sceUserServiceGetAccessibilityChatTranscription, PressAndHoldDelay, TriggerEffect, Vibration, ZoomEnabled, ZoomFollowFocus](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - report 0, 0, 1, 1, 0 and 1, what a PS5 (firmware 13.60) reports for a user whose accessibility settings are all at their defaults; the console reports the user's own settings (a changed vibration intensity read 3), which are not modelled. What each number means is unknown. A user who is not logged in returns `0x80960009` with or without an output - [sceGameUpdateGetAddcontLatestVersion](../../core/libs/prx/libSceGameUpdate/Export.cpp) (libSceGameUpdate) - a null `entitlement_label` throws: KytyPS5 accepts it, prosper returns `0x80412803`. Bytes after `found` are zeroed as in KytyPS5; prosper leaves them untouched - [sceSystemServiceParamGetString](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw -- [sceSystemServiceParamGetInt](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format, time format, time zone, summer time and enter button stay fixed +- [sceSystemServiceParamGetInt](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Parameter 400 returns the same number as parameter 1: PPSA01325 asks for 400 first, asks for 1 when that fails, and uses either answer as an index into the same 31 languages; in the PS5 10.01 and 12.60 `libSceSystemService` modules parameter 400 reads the same registry value as parameter 1, without parameter 1's mapping of newer language numbers by SDK version, and returns 0x80a10003 when an imported check that is not identified returns 1; the 4.03 module has no parameter 400. Neither the mapping nor that error is modelled, and the parameter's name is not known. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format, time format, time zone, summer time and enter button stay fixed - [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp) (libSceNgs2.native, NIDs `uBIN24Tv2MI`, `nPzb7Ly-VjE`) are throwing stubs. The signatures (report type, handler, user data, out handle) and (report handle) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_report.h`. No report is produced yet, so registering a handler cannot be honoured. PPSA04540 imports both and registers a CPU load handler (type 5) - [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. Lock and unlock take the same lock as `sceNgs2RackLock` - [sceNgs2RackGetInfo](../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - field order from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2RackInfo` with the 64-byte name of the other info structures (216 bytes); `type` is assumed to be the rack id divided by 0x1000, `min_grain_samples` 64 (the system minimum) and `state_flags` 1; `uid` comes from a per-process rack counter, `render_count` is the owning system's render count and `active_voice_count` counts voices that are not empty (paused and stopped voices included). The last process ratio and tick and `active_channel_work_count` are left zero - [sceNgs2VoiceQueryInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw - [sceNgs2VoiceGetPortInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - layout from shadPS4; the delay and destination input are zero because ports have neither yet - [sceNgs2ParseWaveformFile](../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw +- [sceSaveDataDirNameSearch](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. Every native save reports `SAVE_DATA_BLOCKS_MAX` blocks, as `sceSaveDataGetMountInfo` does, so the two block keys keep directory-name order. The search infos (blocks, free blocks and 32 reserved bytes per returned name, following shadPS4's `OrbisSaveDataSearchInfo`) report `SAVE_DATA_BLOCKS_MAX` for both. Keys other than 0, 1, 2, 3 and 5 (including 4) and orders above 1 return `SCE_SAVE_DATA_ERROR_PARAMETER`; shadPS4 and KytyPS5 only reject keys above 5, so key 4 is rejected here because no source names it - [sceSaveDataInitialize3](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a repeated initialize succeeds: PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin fails on any error. Initializations are counted and `sceSaveDataTerminate` ends the session at the last one; how the console pairs them is unknown - [sceTextToSpeech2GetSystemStatus](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - unknown signature; returns `EOPNOTSUPP` like `sceTextToSpeech2Initialize` - [sceCoredumpAttachUserMemoryFile](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - unknown signature @@ -329,6 +361,7 @@ Throughout the project, every function at every stage either **does exactly what - [pnz21I6VjDs](../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature - [qk043QPsrZU](../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature - [uDGXwUKqi8s](../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature +- [sceKernelGetCpuTemperature](../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - imported by PPSA99902, which fails to load without it; added without an implementation because its behavior is unverified. - [LinuxProtFromSce](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - protection bits 0x100 and 0x200 are accepted and assumed not to affect host CPU access; host protection is derived only from the read, write and execute bits - [_sceUlobjmgrRegisterObject](../../core/libs/prx/ulobjmgr/Export.cpp) (ulobjmgr) - parameters taken from shadPS4 (PS4): a nonzero 64-bit object, a nonzero 32-bit kind and a 32-bit id output; the meaning of the first two and the PS5 signature are unverified. The id bound of `_sceUlobjmgrUnregisterObject` (below 0x4000) and the raw `EINVAL` (22) return also come from shadPS4 - [sceShareGetCurrentStatus](../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - validation (feature flag non-zero, status non-null) and the all-zero 16-byte status taken from the [KytyPS5](https://github.com/KytyPS5/KytyPS5) reimplementation, not confirmed on a PS5 title; the meaning of the recording status values is unknown @@ -337,6 +370,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceSharePlayInitialize](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - the library keeps no state, so the heap is not used. A null heap is accepted: PPSA11386 passes a null heap and 0x1800 bytes and stops on any non-zero result. The error for a zero size is unknown, so it throws - [sceAgcDcbSetZPassPredicationEnableGetSize, sceAgcDcbSetPredicationDisableGetSize, sceAgcDcbSetBoolPredicationEnableGetSize](../../core/libs/prx/libSceAgc/DcbState/src/Predication.cpp) (libSceAgc) - the builders are inline in the SDK and not exported; the size is assumed to be that of the SET_PREDICATION packet `sceAgcDcbSetPredication` writes (16 bytes) - [Resource registration](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified. `sceAgcDriverGetResourceRegistrationMaxNameLength` writes 0xfc and returns 0, as [prosper](https://github.com/mattias800/prosper) does from a title's registration configuration; the `uint32_t*` parameter matches the SDK wrapper `sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*)`. sharpemu writes 256 and KytyPS5 returns `RESOURCE_REGISTRATION_UNAVAILABLE`, so the value is unverified; a null output throws +- [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) follows [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, not confirmed on hardware: the `dbOlWdppb4o` transform without the identity tail, so it writes one register per pixel shader input and none for a pixel shader without inputs, and a null pixel shader throws. For an `is_f16` 1 or 3 input whose `default_value` and `default_value_hi` differ it throws: OpenAGC takes `DEFAULT_VAL` from `default_value_hi`, while KytyPS5, sharpemu and `dbOlWdppb4o` here take it from `default_value` - [sceAgcDriverRegisterMultipleResources](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) uses the existing unavailable-registration state and leaves caller memory untouched. Its four arguments (uint32 handle output, uint32 owner, descriptor pointer, uint32 count) follow the original PPSA34502 / 01.010.000 codzm call. The caller builds 40-byte descriptors and explicitly accepts 0x8A6C9018 with an invalid output handle. Enabled registration and console argument-validation precedence are unverified. - [Shader registration](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a shader header is accepted at any address: PPSA26344 passes headers 4 and 1 bytes past an 8-byte boundary; whether the console refuses an alignment is unknown - [NGG vertex workgroups](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - type-2 PRIMGEN programs using DS instructions or `s_barrier` are prepared with the draw's routing and primitive registers on the existing mesh path. The workgroup requirement follows [Mesa's NGG lowering](https://github.com/mihawk-99/PS5_Mesa/blob/b3588f78dd7fd46fbadc7ecd487e5f14d4213e4b/src/amd/common/nir/ac_nir_lower_ngg.c#L1135); the mesh path supplies LDS and two guest lanes per host invocation for wave64 on a 32-lane host subgroup. Console routing has not been measured. Passthrough and tessellation keep their existing paths; unsupported mesh assembly and subgroup budgets throw. @@ -348,36 +382,46 @@ Throughout the project, every function at every stage either **does exactly what - [sceNgs2VoiceControl sampler filter](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason - [sceRtcGetDosTime, sceRtcSetDosTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date -- [sce::Json::InitParameter2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layout inferred, not known: assumed to start with the allocator, its user data and the file buffer size, in that order, and the whole struct is assumed to fit in 40 bytes; `Initializer::initialize(InitParameter2)` ignores them as `initialize(InitParameter)` does +- [sce::Json::InitParameter2, InitParameterRtti and InitParameterRtti2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layouts inferred, not known. `InitParameter2` is taken as the 40-byte object of [SharpEmu](https://github.com/sharpemu/sharpemu/blob/1811cef94435db7655209fb39f65ddf258b93fd6/src/SharpEmu.Libs/Json/JsonExports.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/eda6ba37ece127ab0396378404d99182e3e8a530/src/libs/libJson2.cpp): the allocator, its user data, the file buffer size, the 32-bit special float format type at 0x18 and 12 bytes both zero, and both constructors zero all of it. `InitParameterRtti2::setAllocatorRtti` writes the allocation parameters and user data where `setAllocator` writes the allocator, as both references do; whether the console also marks the object as using RTTI allocation is unknown. `InitParameterRtti`'s constructor writes the allocation parameters, user data and file buffer size to the first 24 bytes, an order inferred from its mangled arguments. The values of `SpecialFloatFormatType` and what they select are unknown, so `setSpecialFloatFormatType` stores any value and nothing reads it: `Initializer::initialize(InitParameter2)` ignores every field as `initialize(InitParameter)` does, serializing a non-finite real throws whatever the type, and whether the type lets the parser accept NaN or infinity is unknown. The setters return nothing; whether the console's return `this` is unknown - [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty - [scePlayerReviewDialogOpen, scePlayerReviewDialogGetResult](../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) - the PS5-only dialog has no public reference: the parameter is not read and the result is assumed to start with the 32-bit common dialog result, as `sceWebBrowserDialogGetResult` assumes - [sceHttpWaitRequest](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the timeout is assumed to be in microseconds and a negative timeout to wait without limit; the invalid-value error for a null handle or event array or a non-positive event count is assumed, not measured. No request is ever pending, so a wait returns 0 events when its timeout passes - [sceVideoOutAddPreVblankStartEvent](../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut) - how long before the vblank the console signals pre-vblank start is unknown. The event is triggered on each emulated vblank tick immediately before the vblank event, with its own count as the event data -- [sched_get_priority_max, sched_get_priority_min](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `SCHED_OTHER` (2) reports the same 256 to 767 range as `SCHED_FIFO` and `SCHED_RR`; the console's answer is unverified. FreeBSD accepts `SCHED_OTHER` with its own range, and shadPS4 rejects it with `EINVAL`. `pthread_setschedparam` ignores the policy, so a priority taken from this range applies +- [sched_get_priority_max, sched_get_priority_min](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - measured on a PS5 (firmware 13.60): `SCHED_FIFO` (1) and `SCHED_RR` (3) report a maximum of 767 and a minimum of 256, and every other policy, `SCHED_OTHER` (2) included, returns -1 with `EINVAL`. `pthread_setschedparam` ignores the policy and does not check the priority, where the console rejects `SCHED_OTHER` and priorities below 256 with `EINVAL` - [inet_pton, inet_ntop](../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp) (libkernel) - the text forms are assumed to be those of FreeBSD 11.4 and are unverified on a console: `inet_pton` accepts only four decimal parts without leading zeros for `AF_INET` and the RFC 4291 forms for `AF_INET6` (one `::` standing for at least one group, a dotted-quad tail only as the last 32 bits, no zone id), and `inet_ntop` prints lowercase hex with the first longest run of two or more zero groups compressed, and a dotted tail only for `::a.b.c.d` and `::ffff:a.b.c.d`. They no longer depend on the host resolver library - [gethostbyname](../../core/libs/prx/libScePosixForWebKit/Resolver.cpp) (libScePosixForWebKit) - numeric IPv4 forms, trailing whitespace and 64-bit accumulation follow FreeBSD 11.4 [inet_aton](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/inet/inet_addr.c); clearing the resolver error on numeric success follows [fakeaddr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/net/gethostnamadr.c). Exact PS5 behavior is unverified; hostname lookup still uses the host resolver - [sceAppContentGetAddcontDownloadProgress](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - imported by PPSA08755, which fails to load without it; added without an implementation because its behavior is unverified. [SharpProspero](https://github.com/SvenGDK/SharpProspero) lists `(uint32_t serviceLabel, entitlementLabel, SceAppContentAddcontDownloadProgress*)`, but no PS5 reference gives the progress layout or the error codes +- [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomCalcListener` and `sceNgs2GeomApply` stay unimplemented - [_sceFiberGetThreadFramePointerAddress](../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber, NID `0dy4JtMUcMQ`) forwards to `sceFiberGetThreadFramePointerAddress`; that the argument list matches is inferred from the export without the underscore, as for the underscore Ult exports, not measured +- [sceAgcAcbWaitUntilSafeForRendering](../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - `(buffer, videoOutHandle, displayBufferIndex)`, the declared arguments, which PPSA12544 (01.000.010) passes inside `waitUntilSafeForRendering` and `waitUntilSafeForRendering VR` markers on an asynchronous compute command buffer, ignoring the result. It writes the same rendering-wait packet as `sceAgcDcbWaitUntilSafeForRendering`, which the driver now accepts on compute queues too; the wait blocks only that queue's worker. Whether the console encodes the compute variant differently is not known ### Functional +- [Reused depth storage](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. Matching depth/stencil storage access still throws. + - [sceAudioInHqOpen, sceAudioInAsyncOpen](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) open the same port as `sceAudioInOpen`, so `sceAudioInInput`, `sceAudioInGetSilentState` and `sceAudioInClose` accept their handles. The signatures agree across [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/fc5d2cc219c43c8c8f354153e7b3d0c04a5d506e/src/core/libraries/audio/audioin.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp). HqOpen is limited to 128 samples at 48 kHz; AsyncOpen rejects sizes above 384 and throws for currently unverified inputs. Host capture pacing and console behavior remain unverified. + +- [sem_timedwait](../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. +- [MaskedSelectEliminator](../../core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp) can discard inactive source lanes through `ReadFirstLane` only when its read mask implies the write mask and its result is unobserved outside the read mask. A loop-local read mask requires result uses in the same block without phi transport. The empty-EXEC lane-zero fallback follows AMD's [Vega ISA, section 12.8, V_READFIRSTLANE_B32, page 127](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/vega-shader-instruction-set-architecture.pdf) and the existing SPIR-V emitter. The optimization is covered by synthetic IR tests but has not been compared with console hardware; a proof that needs a read mask of a kind that is already being solved is refused. + - [getc_unlocked, putc_unlocked](../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. - [ftrylockfile](../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - tries the host CRT stream lock that `flockfile`, `funlockfile` and each locking host stdio call on the stream take, so it returns -1 while another thread holds the stream through `flockfile` or is inside such a call, and 0 when it acquires the lock or already owns it (recursively, released by as many `funlockfile` calls), as FreeBSD 11 [`_ftrylockfile`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/_flock_stub.c#L89-L104) does. A libc export holds the lock only for each host call it makes, not across several host calls or while it updates the guest `FILE` prefix. Linux uses glibc `ftrylockfile`. UCRT has no try variant of `_lock_file`, so Windows calls `TryEnterCriticalSection` on the `CRITICAL_SECTION` that `_lock_file` enters, found through the private UCRT stream layout (`__crt_stdio_stream_data` in `ucrt/inc/corecrt_internal_stdio.h`, unchanged from Windows SDK 10.0.19041 to 10.0.26100); another CRT or a change of that layout breaks it. A null or closed stream throws where FreeBSD dereferences it - [libSceOpusDec](../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for 48000 Hz mono/stereo signed-16 decoding. The six signatures, 640-byte opaque state allocation and byte-count return follow [public PS5 decoder research](https://github.com/blackbearreloaded/ps5-hardware-audio-decoding/blob/26be5a12f92b2cdc67f8a9e082fd69ae0e9a35a2/docs/DECODING.md#direct-opus-abi) and [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). Console PCM equivalence and error codes are unverified. Other sample rates, packet loss concealment, invalid inputs and termination with live decoders throw. Context identifiers and host decoder state are private registries; the firmware's opaque state layout and error recovery are unknown. - [mkstemp and isatty](../../core/libs/prx/libScePosixForWebKit/Filesystem.cpp) (libScePosixForWebKit) follow FreeBSD's [temporary-file API](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/stdio/mktemp.c) and [terminal check](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/gen/isatty.c), without console verification. `mkstemp` replaces all trailing `X` characters, accepts a fixed name, and creates files through the guest path resolver with exclusive read/write access and mode 0600. Windows file permissions follow the existing kernel file implementation. `isatty` tests Windows console handles rather than all character devices; files, pipes and guest sockets return `ENOTTY`, closed descriptors return `EBADF`. -- [arc4random](../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) returns four bytes from the same thread-local host `std::random_device` as `arc4random_buf`, uniform over the 32-bit range like FreeBSD 11.4's RC4 keystream in [arc4random.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/arc4random.c). The console's generator and its reseeding are unknown; `arc4random_uniform`, `arc4random_stir` and `arc4random_addrandom` are not in the module's export set and are absent +- [arc4random](../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) returns four bytes of host OS entropy (`getentropy`, `rand_s` on Windows), as `arc4random_buf` does, uniform over the 32-bit range like FreeBSD 11.4's RC4 keystream in [arc4random.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/arc4random.c). The console's generator and its reseeding are unknown; `arc4random_uniform`, `arc4random_stir` and `arc4random_addrandom` are not in the module's export set and are absent - [fnmatch](../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) follows FreeBSD 11 [fnmatch.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/gen/fnmatch.c) in the C locale: each byte is one character, `FNM_CASEFOLD` folds ASCII letters only, and bracket ranges compare byte values. `[:class:]` names are not character classes: as in FreeBSD 11 through 14, `[[:digit:]]` is the set of `[`, `:`, `d`, `i`, `g`, `t` followed by a literal `]` (classes were added to `fnmatch.c` after 14). Multibyte characters and locale collation are not modelled. A null pattern or string throws -- [Dinkumware thread exports](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`, `xtime.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. `_Thrd_t` is taken to be the guest `pthread_t`, as the `std::_Pad::_Launch(pthread**)` export suggests: `_Thrd_join` returns `_Thrd_error` (4) when `pthread_join` fails and stores the thread's return value truncated to `int`. `_Thrd_sleep` reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep`, so it is a cancellation point; a null xtime or one whose normalization overflows throws. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. `_Thrd_join` (32-bit store of the result, 0 or 4) and the spin lock (byte exchange loop, no yield) match that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning +- [Dinkumware thread exports](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. `_Thrd_t` is taken to be the guest `pthread_t`, as the `std::_Pad::_Launch(pthread**)` export suggests: `_Thrd_join` returns `_Thrd_error` (4) when `pthread_join` fails and stores the thread's return value truncated to `int`. `_Thrd_join` (32-bit store of the result, 0 or 4) and the spin lock (byte exchange loop, no yield) match that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning. - [_Cnd_signal, _Cnd_init_with_name](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - take a pointer to the handle slot like the libc `_Cnd_*` exports from [#3079](https://github.com/boykopovar/AnyPS5/pull/3079) and act on the same conditions. `_Cnd_signal` wakes one waiter and returns `_Thrd_success` (0), as `_Cnd_broadcast` does; whether the console maps a failing internal signal to `_Thrd_error` is not known. The signature `_Cnd_init_with_name(_Cnd_t*, const char* name)` is inferred from [fpPS4 `ps4_mtx_internal.pas`](https://github.com/red-prig/fpPS4/blob/trunk/src/libcinternal/ps4_mtx_internal.pas), where the PS4 `_Mtx_init` is `_Mtx_init_with_name` with the name `SceLibcI_Mtx` and the name only reaches the pthread object; here it is `_Cnd_init` and the name, which may be null, is ignored. Neither is confirmed on a PS5 `libc.prx`. `_Thrd_id` still throws: microsoft/STL returns an `unsigned int` thread id, but whether the PS5 returns the `pthread_t` or a kernel thread id, and at what width, is not known - [libSceZlib](../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports one outstanding request, null-timeout waits and one result retrieval. Timed waits, multiple outstanding requests, repeated retrieval and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. - [Windows file timestamps](../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes` and `futimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond - [fchdir](../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) changes the guest working directory to the descriptor's directory: on Windows through the path recorded when the descriptor was opened, on Linux through the `/proc/self/fd` link target. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. FreeBSD `change_dir`'s search-permission check (`EACCES`) is not modelled, and a directory renamed after it was opened is still reached through the recorded path. Console behaviour is unverified +- [openat](../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `open`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. As with `open`, `O_NOFOLLOW` and `O_NONBLOCK` are ignored. PPSA99003 opens directory entries this way. Console behaviour is unverified +- [readlink](../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target and a junction throw, as a host path has no guest form. Any other reparse point throws too, where FreeBSD reports `EINVAL` for a file that is not a link - [Windows file identity](../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails - On Linux, [direct memory](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [Per-face depth bias](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): Vulkan has one depth bias per draw, so a draw that rasterizes both faces with different front and back bias (PA_SU_SC_MODE_CNTL, PA_SU_POLY_OFFSET_FRONT/BACK_*) is recorded twice, front faces with the back faces culled and then back faces with the front faces culled. The vertex stages run twice, so a vertex shader that writes memory writes twice, and all front faces land before all back faces instead of in primitive order, which differs only for blending or stencil updates that depend on the order. @@ -387,6 +431,8 @@ Throughout the project, every function at every stage either **does exactly what - [Compute LDS beyond the device's shared-memory limit](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords per workgroup (the last dword is the atomic lock), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. - [AGC buffer allocations](../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request. - [Graphics pipeline libraries](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline +- [Shader modules built at registration](../../core/shader/recompiler/Recompiler.cpp) (shader recompiler): on devices with graphics pipeline libraries a vertex or pixel module reads buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors at run time instead of specializing on them per draw, so its libraries can be built when the shader is registered. A module that would exceed `SpirvTarget::registrationWordLimit` (200000 SPIR-V words) is emitted specialized as before: on RADV (Radeon 660M, Mesa 26.2.4) a sample of 60 Astro's Playroom pixel shaders compiled 1.9 to 2.2 times slower in registration form below 141k words and 3.8 to 5.7 times slower above 280k words (none in between), so the limit sits in that gap; it was not measured on other drivers. Texel-offset folding, the format word of a buffer accessed with the descriptor's format (`buffer_load_format_*`; as runtime values every format's decoder is compiled, which made CI's lavapipe JIT ten times slower) and the vertex attribute numeric class stay specialized, so shaders with the first two, and pixel shaders whose image needs a multisampled descriptor, get no registration result; these, and every draw whose fixed-function state differs from the prebuild's, still build their libraries at the draw. Compute and the other stages stay specialized: in registration form the `agc_driver_dword_pattern_fill` compute shader hung CI's lavapipe (Mesa 25.2.8) when built with `ANYPS5_ENABLE_SPIRV_TOOLS=ON`, not on RADV, and the cause was not found. +- [Prebuilt pipeline libraries](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver): each registered vertex or pixel shader in registration form gets its pre-rasterization or fragment-shader library built on one background thread, with a fixed state the draw may not use: triangle list, counter-clockwise front face, no depth clamp, depth bias or conservative rasterization, first provoking vertex, one sample. Libraries are kept until the device is torn down; on Astro's Playroom 1948 of them added 0.44 GB of peak memory on RADV. - [sceAvPlayerGetVideoData](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) in sync mode `SCE_AVPLAYER_AV_SYNC_MODE_NONE` skips queued frames that the audio clock has already passed while the title pulls audio, so a title that requests video slower than the film's frame rate keeps the picture with the sound. Whether the console hands out every frame in this mode is unknown. PPSA12544 requests 7-19 frames per second for a 30 fps cinematic - [`APS5_THREAD_PRIORITY=rt`](../../core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp) (libSceAgcDriver) lowers the process's hard `RLIMIT_RTTIME` when it is above `APS5_THREAD_RTTIME_US` or rtkit's `RTTimeUSecMax`, because [rtkit](https://github.com/heftig/rtkit/blob/c295fa849f52b487be6433e69e08b46251950399/rtkit-daemon.c#L592-L637) (`verify_process_rttime`) reads the hard limit from `/proc//limits` and refuses real-time scheduling when it is higher. An unprivileged process cannot raise the hard limit again, so it stays lowered for the process and its threads even when rtkit then refuses the request (burst limit, user or start-time checks) and the thread throws. rtkit's `MaxRealtimePriority` and `RTTimeUSecMax` are read first, so an unreachable rtkit or a priority above its maximum leaves the limit untouched. The lowered limit only matters for threads that later run under a real-time policy - On a device without VK_EXT_robustness2's `nullDescriptor`, such as MoltenVK, a null image descriptor in a typed heap is a [placeholder image](../../core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp) of that heap's type, 1×1 and cleared to zero, which every null descriptor of the heap shares. Reads give zeros, as `nullDescriptor` allows, but stores and atomics to it are kept, so a later read of a null storage image can return them, and a depth comparison compares against depth 0. A heap whose placeholder the device cannot create, for example 1D or 3D depth, 64-bit atomics without 64-bit image atomics, or multisampled storage without `shaderStorageImageMultisample`, throws when a null descriptor reaches it. @@ -394,16 +440,19 @@ Throughout the project, every function at every stage either **does exactly what - [sceHttpAbortWaitRequest, sceHttpWaitRequest](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the abort takes only the epoll handle, as in [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); behaviour and error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) (PS4), not confirmed on a PS5 title or hardware. An abort stays set: every later wait on the handle, a poll included, returns `SCE_HTTP_ERROR_ABORTED` (0x80431080) until the handle is destroyed. A wait woken by `sceHttpDestroyEpoll`, or by the `sceHttpTerm` that ends the last context, returns 0 events. The epoll calls return `BEFORE_INIT` (0x80431001) while no `sceHttpInit` context is alive and `INVALID_ID` (0x80431100) for an unknown context or handle, and `sceHttpTerm` returns the same errors instead of succeeding for any id. Handles are pointers, so a stale handle whose address a newer epoll reuses is taken as that epoll - [system](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown -- [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies -- [/dev/random and /dev/urandom](../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled from `std::random_device`; both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, a `dup`ed descriptor reads as an empty file, `fstat`, `lseek`, `mmap` and `select` see the null device, and opening them for writing throws +- [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. Names whose UTF-8 encoding exceeds the one-byte `d_namlen` limit are not representable in this guest directory-record layout: `getdents` returns `ENAMETOOLONG` without consuming the entry. Repeated calls therefore report the same error and entries after that name cannot be reached through this descriptor; FreeBSD's `getdents` behavior for host filenames that exceed its `MAXNAMLEN` is not verified. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies +- [/dev/random and /dev/urandom](../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, a `dup`ed descriptor reads as an empty file, `fstat`, `lseek`, `mmap` and `select` see the null device, and opening them for writing throws - [Wide-character conversions](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [nan and nanf](../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. - [Draw input snapshots](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. +- [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing +- [sinhf](../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed +- [asinl](../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. The result needs the x87 precision control at 64 bits, as FreeBSD amd64 starts threads (control word `0x37f`), but guest threads do not set it: on Windows a thread started by `_beginthreadex` runs at 53 bits (`0x27f`), where `asinl(0.75)` returns mantissa `0xd91a98ae3406e000` instead of `0xd91a98ae3406e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code - [SPIRV-Tools optimization](../../core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp) skips CCP and Simplification for modules with storage-only 8- or 16-bit types because the pinned folder produces illegal narrow constants. This also disables unrelated constant folding; the restricted full pipeline mirrors the pinned performance recipe. - [SRT signed comparison](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) evaluates `SLessThan32` as signed 32-bit less-than, matching the existing SPIR-V backend and [SPIR-V `OpSLessThan`](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpSLessThan). Signed boundary descriptor expressions are tested on the host; native RDNA2/PS5 execution and title behavior remain unverified. - On macOS the [write watch](../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: - - a system call that writes into a protected page fails with `EFAULT` instead of faulting. `GuestArena::HostWrite` lifts the protection for the calls it brackets (the file reads); a host call that writes guest memory without it (sockets, `clock_gettime` and the like into a watched page) fails; + - a system call that writes into a protected page fails with `EFAULT` instead of faulting. `GuestArena::HostWrite` lifts the protection for the calls it brackets (file reads and socket receives); a host call that writes guest memory without it (`clock_gettime` and the like into a watched page) fails; - code that changes the protection of watched pages, or maps over them, has to call `GuestWriteWatchProtectionChanged`, and code that asks the kernel for a page's protection has to ask `GuestWriteWatchProtection` first, since a watched page is read-only between a collection and the next write. Nothing calls either yet: the macOS memory paths are not in the tree; - the handler is installed when libc loads and passes other faults to the handler installed before it. A handler installed later has to do the same. - [Pending storage-image results](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). `APS5_STORE_AT_FLIP=1` stores every pending result at each flip; storing them at the title's GPU waits (labels, fences) is unimplemented. @@ -411,8 +460,11 @@ Throughout the project, every function at every stage either **does exactly what - [Storage-image units under a Linux import window](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) that the window stamped as a possible CPU store (`BlockMaybeWritten`) are compared with the bytes the image was uploaded from or written back as, and are unchanged when those still match. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Images tracked by layer instead of by 64 KiB unit still read the window as a change and upload again. - [Shared memory](../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the socket's send buffer size (`SO_SNDBUF`) in `data`, not the free space left in it, as Windows has no query for that space. Only guest sockets are supported; other descriptors and a low watermark above 1 throw +- [sceNetSetsockopt](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. On Windows a receive or send on a stream socket that times out fails with `ETIMEDOUT`, where FreeBSD returns `EAGAIN` or the partial count: Winsock leaves the connection in an indeterminate state after a timeout and reports no partial count ([SOL_SOCKET options](https://learn.microsoft.com/en-us/windows/win32/winsock/sol-socket-socket-options)), so FreeBSD's behaviour needs the timeout waited for without the host option. A receive on a datagram socket that times out fails with `EAGAIN` - [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported +- [close](../../core/libs/prx/libkernel/File/src/Stdio.cpp) and [setsockopt](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) reach a [libSceNet](../../core/libs/prx/libSceNet/Export.cpp) socket through hooks libSceNet registers, as `select` polls them; the other libkernel socket calls do not. libSceNet numbers its sockets from 32 in its own table, so a CRT file descriptor and a socket can share a number; `close` then closes the socket. On the console both are one kernel descriptor space - [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries +- [Content export sessions](../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport): `sceContentExportStart` hands out positive session ids, at most 10 live, which `sceContentExportFinish` closes. The signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) and the limit `MaxExecution` [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Content/ContentExport.cs); the parameter layouts follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/content_export/content_export.h). The host has no media library, so `sceContentExportFromDataWithThumbnail` throws once the session is checked, before calling either data callback, and no export ever runs. `sceContentExportGetProgress` and `sceContentExportCancel` throw for a live session, as their result with no running export is unknown. An eleventh `sceContentExportStart` and `sceContentExportTerm` with live sessions throw: whether the console answers `EXECUTION_MAX` or `BUSY` there, or ends the sessions, is unverified. Session id values and error precedence are unverified - [Runtime-loaded modules](../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) that another module needs are initialized at startup with no start arguments, before the module that needs them is loaded; the console is assumed to start them when that module is loaded. - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console. Handles that are not disk files return `ESPIPE`. `readv` on a pipe or console handle stops after the first iovec read, a short read the console is assumed to allow; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done - [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console @@ -420,6 +472,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceKernelGetFsSandboxRandomWord](../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist - [sceKernelSync](../../core/libs/prx/libc/src/General.cpp) (libkernel) flushes only the host files and directories recorded through `RecordWrittenPath_nid_no_patch` (the libc, libkernel, save data and app content file functions) and their changed parent directories. A host write in a library that does not record its path is not flushed by `sceKernelSync` - [sceKernelSync](../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. On Linux, it skips a recorded file it cannot open for reading. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way +- [`fopen` streams](../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows share delete access, like the descriptors [`sceKernelOpen`](../../core/libs/prx/libkernel/File/src/Open.cpp) opens, so a file the guest holds open can be removed or renamed. A stream reopened with `freopen` still goes through `_wfreopen`, which does not share delete access - [getcontext and setcontext](../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`) - [makecontext](../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) follows FreeBSD 11's `lib/libc/amd64/gen/makecontext.c`: at most six arguments, a stack of at least `MINSIGSTKSZ` (2048) bytes, and `exit(0)` when the function returns with no `uc_link`. The console's version is not verified - [getcontext and setcontext](../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them @@ -436,6 +489,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAgcDebugRaiseException](../../core/libs/prx/libSceAgc/Misc/src/Debug.cpp) (libSceAgc) - logs the exception id and returns 0, as retail hardware does nothing per [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c); the trap (`ud2`) OpenAGC describes for development hardware is not reproduced. Neither is verified on a console. The `uint32` exception id is [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/6f0cddd4404ec5203e71fea77e75fc373299ba3f/src/libs/agc.cpp)'s claim; OpenAGC declares no arguments, so the logged id may be leftover register contents - [sigaltstack](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records each thread's alternate signal stack and reports it back, following FreeBSD's `kern_sigaltstack` (`EINVAL` for flags other than `SS_DISABLE`, `ENOMEM` below `MINSIGSTKSZ` 2048), but handlers never run on it, so `SS_ONSTACK` and `EPERM` are never reported - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; its existing size formula and the 512-byte object layouts remain unverified. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. +- [scePthreadJoin, scePthreadCreate](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - on a PS5 (firmware 13.60) joining a thread a second time returns `ESRCH` (`0x80020003`); here a joined thread is released, so a second join reads freed memory. A null entry crashed the payload and still throws. Joining the calling thread returns `EDEADLK` and a zeroed attribute creates a default thread, as on the console - [Thread cancellation](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) follows FreeBSD libthr (states and types 0/1 and 0/2, `PTHREAD_CANCELED` 1, the `_pthread_cleanup_info` node layout), not measured on the console. A request is acted on only in `scePthreadTestcancel`, the condition variable, `sem` and `scePthreadSem` waits, `scePthreadJoin` and the sleeps, and their POSIX names; file and socket calls are not cancellation points. A sleep is not interrupted: the thread exits when it ends. The asynchronous type acts like the deferred one, except at the two set calls. - [cpuset_setaffinity](../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - only the calling thread or the calling process is handled. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set - [cpuset_getaffinity](../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - only the calling thread or the calling process is handled. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified @@ -445,6 +499,9 @@ Throughout the project, every function at every stage either **does exactly what - [Packed horizontal gathers](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_gather4h_pck`, MIMG opcode 98): the four window texels are read raw and concatenated into a little-endian bitstream, with DMASK bit i writing stream word i to the next destination register; measured on RDNA2 (gfx1031) at every edge: the anchor is floor(x-0.5) with elements at -1, 0, +1 and +2 clamped horizontally into the width, the row is floor(y), negative rows clamp to row 0, rows at or past the height return zeros, sampler addressing modes do not affect the window, and formats wider than 32 bits per texel write zeros in every register. The emitter fetches each window texel at LOD 0 and packs its bits like `image_load_pck`, so the sampler's filter, addressing and LOD state are not used; formats whose bits are not recoverable from the view and D16 data throw. The anchor and row multiply the normalized coordinate by the image size without contraction. The stream rule was measured for R8_UNORM, RG8_UNORM and RGBA8; only RGBA8 has an execution test. `image_gather8h_pck` (opcode 99) never returns texel data on this hardware - across dmasks 0x1 to 0xFF including the upper nibble, RGBA8, RGBA16 and RGBA32 formats, 1D, 2D and 3D descriptors and normalized and unnormalized sampling it only zero-fills popcount(dmask) registers (sentinel-prefilled destinations) - so it decodes but throws at translation; its eight-texel semantics remain unmeasured. - [waitpid](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns -1 with `ECHILD` (`EINVAL` for unknown options): nothing here creates a child process; child processes would need a real `waitpid`. - [execv](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported, the same as `execvp`. PPSA97358 imports it and fails to load without it. +- [kern.proc.pathname](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. +- [machdep.cacheline_size](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns -1 with `ENOENT`, matching FreeBSD amd64 where this sysctl OID is not defined. +- [getrlimit](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `RLIMIT_CPU`, `RLIMIT_FSIZE`, `RLIMIT_CORE`, `RLIMIT_SBSIZE`, `RLIMIT_VMEM`/`AS`, `RLIMIT_NPTS`, `RLIMIT_SWAP`, `RLIMIT_KQUEUES` and `RLIMIT_UMTXP` report infinity as in FreeBSD's process-0 defaults, and `RLIMIT_DATA` mirrors the host limit. `RLIMIT_STACK`, `RLIMIT_RSS`, `RLIMIT_MEMLOCK`, `RLIMIT_NPROC` and `RLIMIT_NOFILE` throw because no console values are established. - [execvp](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported. PPSA85300 imports it and fails to load without it; its `fork` and `vfork` fail with `ENOSYS` ([libc_missing.c](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/ps5/compat/libc_missing.c#L290-L292)), so the child process that would call it is never created. - [fork](../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask - [MIMG BY2/BY4 loads](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_load[_mip]_by2/by4`, opcodes 0x42, 0x43, 0x4a and 0x4b) were measured on RDNA2 (gfx1035) with the [hardware oracle](HW_ORACLE.md) for 2D images with identity swizzle. They read N texels (BY2: 2, BY4: 4) from `x & ~(N - 1)` on row y, texel by texel and component by component within each texel, with each texel converted like `image_load`. Every register is zero when the mip level is past the last level, `x + N - 1` is outside the level's width (unaligned x, unsigned), or y is outside its height. Data only comes back when the N texels fit in one dword: R8 and R16 (DMASK 0x3) or RG8 (0xf) for BY2, and R8 (0xf) for BY4. With 32-bit texels (R32, RG32, RG16, RGBA8) the hardware returns zeros. Other formats, masks, dimensions, swizzles, D16/A16 and indirect descriptors throw. The stores (0x52, 0x53, 0x5a and 0x5b) decode but throw at translation. Measured on hardware, they write from the unaligned x up to the end of its dword, which is not implemented. @@ -483,6 +540,7 @@ Throughout the project, every function at every stage either **does exactly what - Without fetch-inactive, [`v_permlane16_b32` and `v_permlanex16_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take a source lane that the host subgroup does not hold as inactive and keep the old value (0 with bound_ctrl); the console reads that lane's VGPR. - On a host subgroup narrower than the wave, [`v_readlane_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) of a constant lane outside it builds only as lane 63 of a half-wave scan (DPP row_shr 1/2/4/8, `v_permlanex16_b32`, EXEC -1) 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. - [NGG mesh entry](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) uses the guest wave size (32 or 64) for wave indices, per-wave vertex and primitive counts, and the workgroup wave count in `s3`. The four-bit wave fields follow Mesa's [get_wave_id_in_tg, get_tgsize and get_thread_id_in_tg](https://chromium.googlesource.com/chromiumos/third_party/mesa/+/refs/heads/stabilize-13982.70.B-chromeos-amd/src/gallium/drivers/radeonsi/gfx10_shader_ngg.c); partial waves and groups exceeding 15 waves throw. Indexed and non-indexed wave32 mesh rendering with 32, 64 and 128 threads was checked by pixel readback on Windows with an NVIDIA RTX 4070 Laptop GPU. The guest register values have not been compared with console hardware. +- [Mesh shader grid size](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - every mesh shader loads `NumWorkgroups.x` and declares no vertices when it is 0, which no dispatch produces. The read is a workaround for RADV (Mesa 26.2.4, seen on an RX 9070 XT, GFX12): binding a mesh pipeline whose shader reads neither `NumWorkgroups` nor `DrawIndex` keeps the user SGPR count and draw-id flag of the vertex pipeline bound before it in the same command buffer (`radv_bind_pre_rast_shader` updates them only when the shader has the `AC_UD_VS_BASE_VERTEX_START_INSTANCE` slot), so `radv_emit_userdata_mesh` opens a 3-register `SET_SH_REG` and writes one value, the CP takes the following `DISPATCH_MESH_DIRECT` as register data and the GPU hangs (gfx ring timeout, MODE1 reset). Kena: Bridge of Spirits (PPSA01802) hits it on its layered volume draws. Reading `NumWorkgroups` gives every mesh shader the grid-size slot, so the count and the values match; drop the read once a fixed Mesa is the minimum - [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx). - [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric` and throw without it, as fixed-function interpolation cannot split a 32-bit attribute. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64, so these instructions need the Float64 capability. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the arithmetic in every mode against the hardware rows on any device with Float64. - Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it: tessellation, mesh and rect-list pipelines, primitives other than triangles, more than 32 fragment input locations and interfaces beyond the device's geometry limits throw. @@ -506,14 +564,15 @@ Throughout the project, every function at every stage either **does exactly what - [v_fma_mixlo_f16 and v_fma_mixhi_f16](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) round the exact product-sum once to f16 with the f64 arithmetic of v_interp_p2_f16, so they need the Float64 capability. On RDNA2 (gfx1036) they follow the `FLOAT_MODE` f16 round and denormal fields, not the f32 round field: -1.5 * 2^-24 + 2^-126 from f32 sources (0xbfc00000, 0x33800000, 0x00800000) gives 0x8001 with f16 denormals kept and round to nearest even, 0x8000 with f16 denormals flushed and 0x8002 with f16 round toward -infinity. With f16 denormals kept, every f16 rounding mode is translated: the exact product-sum is rounded once in the mode's direction. All four modes match RDNA2 (gfx1036) on the 2,233 rows of the #2631 sweep, including `FLOAT_MODE` 0xcc (toward zero, used by ps5-opengl) and the exact cancellations toward -infinity; the PS5's gfx1013 is not measured. A result beyond 65504 becomes infinity or 65504 as IEEE 754 gives for the mode. Flushed f16 denormals still throw, and packed f16 still throws in every mode but round to nearest even with f16 denormals kept. `FP16_OVFL` set is not measured. - [Signaling NaN inputs](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) of the f32 and f16 unary instructions (`v_sqrt/rcp/rsq/log/exp/sin/cos/fract/floor/ceil/trunc/rndne/rcp_iflag/frexp_mant`), `v_ldexp_f32/f16`, `v_frexp_mant_f16`, `v_div_fixup_f32/f16` and `v_mullit_f32` are returned quieted only with IEEE=1 and unchanged with IEEE=0, measured on RDNA2 (gfx1035) in both modes, and so are those of the f64 forms (`v_sqrt/rcp/rsq/fract/floor/ceil/trunc/rndne/frexp_mant/ldexp/div_fixup_f64`) and `v_cvt_f64_f32`, measured on RDNA2 (gfx1036) in both modes; shaders without a known float mode follow IEEE=0. `v_pack_b32_f16` quiets each signaling NaN half by the same rule, measured on RDNA2 (gfx1036) in both modes. A signaling NaN result of `v_cubesc/cubetc/cubema_f32` is quieted by the same rule and keeps the sign the instruction gives it, measured on RDNA2 (gfx1036) in both modes. `v_add/sub/subrev/mul/fma/fmac/fmamk/fmaak/mul_legacy_f32` and `v_fma_mix_f32` with f32 sources choose their NaN result on the bits, measured over 1000 operand triples in both modes: the first NaN source in operand order after its modifiers (the subtrahend negated, as the hardware adds its negation), quieted only with IEEE=1, or the default NaN 0xffc00000 for an invalid operation; in the fma forms an invalid product (inf * 0) gives the default NaN even when src2 is a NaN, and in the legacy forms a zero factor makes the product 0 even when the other factor is a NaN (a NaN src2 still wins). `v_mad_legacy_f32` and `v_mac_legacy_f32` follow the same rule with their separately rounded product, as measured on gfx1010 (#1281, #1290): a NaN source quieted only with IEEE=1, inf - inf giving 0xffc00000, the zero rule hiding a NaN factor and a NaN src2 winning. The f16 and f64 arithmetic, `v_fma_mix_f32` with f16 sources and the separately rounded mad/mac forms (not measurable on RDNA2) follow the same rule on hardware but their NaN bits are left to the driver. `v_cvt_f16_f32`, `v_cvt_f32_f64`, `v_dot2_f32_f16`, `v_fma_mixlo_f16` and `v_fma_mixhi_f16` always quiet and `v_div_scale_f32` never does: the f32 signaling NaN 0x7f812345 as src0 or src2 gives 0x7e09 from mixlo and mixhi at IEEE=0 and IEEE=1 on RDNA2 (gfx1036), where `v_fma_mix_f32` gives 0x7f812345 at IEEE=0 and 0x7fc12345 at IEEE=1. The payload of an f16 NaN source of mixlo and mixhi is left to the driver's f16 to f32 conversion, which NVIDIA replaces. - [Scalar f16 add, sub, subrev and mul denormals](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field at nearest-even: bit 0 preserves inputs and bit 1 preserves the result; otherwise a denormal is flushed to its signed zero. Measured on RDNA2 (gfx1036) with IEEE=0/1 and FP16_OVFL=0/1, over 32 operand pairs in all four denormal modes, including signed zeros, denormals, minimum normals, infinities and NaNs. Multiply detects tininess after rounding to 11 significant bits with an unbounded exponent, before gradual half packing: `0x3bff * 0x0400` flushes, while `0x3c01 * 0x03ff` rounds to `0x0400` and is kept. Directed rounding and ternary arithmetic keep their previous denormal lowering. Shaders without a known float mode keep the previous lowering. Scalar `v_sub_f16`/`v_subrev_f16` share `float16Binary` with add/mul and were measured on gfx1036 over the same operand set as add (1156 non-NaN-input rows per mode); NaN result bits still depend on the driver as recorded above. +- [f16 unary, frexp and conversion denormals](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_ceil_f16`, `v_floor_f16`, `v_fract_f16`, `v_rcp_f16`, `v_rsq_f16`, `v_sqrt_f16`, `v_log_f16`, `v_exp_f16`, `v_sin_f16`, `v_frexp_mant_f16`, `v_frexp_exp_i16_f16`, `v_cvt_norm_i16_f16`, `v_cvt_norm_u16_f16` and `v_cvt_f32_f16` flush a denormal half input to its signed zero when bit 0 is clear (0 and 2), and the f16-result ones flush a denormal half result to its signed zero when bit 1 is clear (0 and 1), after the host rounds it to half. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with denorm16 0..3 over every non-NaN f16 input (63,490 rows per mode and instruction): the only mismatches are the 1-ULP driver differences of `v_exp_f16` (2 inputs) and `v_sin_f16` (14), which are the same with denormals kept. NaN inputs are not compared. `v_ldexp_f16` shares the input rule. `v_cvt_pknorm_i16_f16` and `v_cvt_pknorm_u16_f16` are not measured and keep their previous lowering. Shaders without a known float mode keep the previous lowering. - [Packed f16 NaN results](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding or that flushes f16 denormals throws. -- [f32 denormals](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; f16 inputs retain their previous conversion and denormal handling. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` flush both operands by the field, see the LDS float atomics entry. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` is the exception: it preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. +- [f32 denormals](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; f16 inputs retain their previous conversion and denormal handling. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` flush both operands by the field, see the LDS float atomics entry. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. - The executable file that [relinker](../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens the console when launched, which is inconvenient for playability. - [Overlay output bus](../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - flips on a port opened with `SCE_VIDEO_OUT_BUS_TYPE_OVERLAY` complete and signal their events like main-bus flips, but their buffers are not shown: the overlay is not composited over the main output. Presenting them as whole frames made the window alternate between the two buses - [PA_CL_CLIP_CNTL](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) clear is drawn like bit 24 set, with Vulkan's clipping of `NoPerspective` outputs; what a clear bit changes on the console is undocumented. - [10_11_11 unorm color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export, rounded to nearest even like format-30 image stores, which is assumed to match the CB's conversion. -- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. Last mode throws without `VK_EXT_provoking_vertex`, for generated rectangle, mesh or tessellation primitives, explicit per-vertex flat inputs, and triangle strips with per-vertex inputs, whose [ordering depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html). -- [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. +- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. Last mode throws without `VK_EXT_provoking_vertex`, for generated rectangle, mesh or tessellation primitives, explicit per-vertex flat inputs, and triangle strips with per-vertex inputs, whose [ordering depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html). A FLAT_SHADE input without pass-through that is read through `v_interp_mov_f32` P10 and not P20 [returns the provoking vertex's value](../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp), like P0. ps5-opengl reads every flat input that way, and its driver passes the GL 4.6 CTS on a PS5 (#1413). The lane itself was not measured on the console, so with the first provoking vertex such a shader throws. +- [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. Outside those bands, a driver result more than 4 ULP from a Taylor series in turns of the exact cycle is replaced by the series, which is within 2 ULP of the true sine and cosine; the hardware is up to 9 ULP from them (gfx1035, 243,092 f32 inputs), so results stay within 13 ULP of the hardware on any driver. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's own sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP; the series replaces those results. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. - [v_log_f32 and v_log_f16](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. - [Dual-source blending](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. @@ -522,6 +581,7 @@ Throughout the project, every function at every stage either **does exactly what - The length-changing path of the [instruction rewriter](../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. - In executables with a `PT_SCE_DYNLIBDATA` segment, the PS4 layout, the executable segment also holds read-only data after the code (CUSA00900: 37 MiB after its last function). The [relinker pipeline](../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp) therefore scans for syscalls and import call sites only up to the end of the PLT that follows the last function of the unwind table; code after the PLT without an unwind entry is not scanned. `--to-intel` and the `unused-filter=1` and `unused-filter=2` analyses are not limited this way. - [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. +- [DB_SHADER_CONTROL.STENCIL_TEST_VAL_EXPORT_ENABLE](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) becomes `FragStencilRefEXT`, taken from the second MRTZ channel of the 32_GR and 32_ABGR formats as Mesa's AMD drivers write it ([`emit_ps_mrtz_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L194)); the console was not measured. The host value is the test value of both faces and what `REPLACE_TEST` writes, so a stencil operation that writes all ones or the operation value throws with it, as do a stencil clear draw, the UINT16_ABGR layout, `STENCIL_OP_VAL_EXPORT_ENABLE` and a device without `VK_EXT_shader_stencil_export` (NVIDIA's driver 610.57 offers none). - [DB_SHADER_CONTROL.PRIMITIVE_ORDERED_PIXEL_SHADER](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the host pixel interlock spans the whole shader, not only up to `s_sendmsg` ORDERED_PS_DONE. - [POPS collision SGPRs](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the collision wave ID and intrawave collision mask read 0, so a title skips its POPS wait and runs its per-collision loop once. - [Image loads](../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) (shader recompiler) - an image_load reads a sampled binding, also of an image the shader stores to, so in a primitive-ordered pixel shader it misses an earlier overlapping primitive's image_store in the same draw. @@ -539,6 +599,7 @@ Throughout the project, every function at every stage either **does exactly what - [Multisampled CMASK state](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. - [Multisampled targets in CB passes](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve is modeled. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. - [Multisampled target guest access](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, DMA copy, texture or storage image read) is taken as `max(CB bytes * samples, width * height * element bytes * samples)` from its address, an upper bound on the sample planes of a target laid out in 64 KiB blocks (inferred: the single-sample layout size times the sample count; the exact fragment-aware footprint of addrlib is not computed, so a few bytes past the real footprint can throw too). Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. +- [Custom MSAA sample locations](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid throw. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. - [DCC display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw. - [Storage image write-back](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value. - [16-bit float display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0xC001000622000000` and `0xC001000600000000` are taken as R16G16B16A16 and B16G16R16A16 float, eight bytes per texel, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps them and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgba16Float`, `Bgra16Float`); PPSA11386 registers 3840x2160 tiled buffers of the first. The tiled layout is assumed to be the 8-byte SW_64KB_R_X equation of `TextureSwizzleEquations.hpp` applied to full coordinates, as addrlib applies it, by analogy with the 32-bit scanout; `ColorTargetLayout` applies the equations to in-block coordinates, which for 8-byte texels differs in every odd 64-row block row (address bit 11 takes y bit 6) and for 16-byte texels also differs, so a float display buffer written through `WriteColorTarget` and presented from guest memory would show those rows misplaced. The values are assumed to be linear light and are encoded with the sRGB curve for the 8-bit output. Bits 32-39 of these formats are `0x06`, as in the 10-bit formats SharpProspero lists without sRGB transfer, while its sRGB formats have `0x00`, its 10-bit PQ formats `0x04` (with `0x07` in bits 40-47) and its float BT.2100 PQ formats `0x07`: they show only that these formats are not the sRGB ones. NaN and negative values give 0, values of 1 and above give 255, alpha is linear. Neither the layout nor the transfer is verified against console or title frames, and HDR output is not modelled. DCC metadata on a float buffer throws. @@ -547,16 +608,18 @@ Throughout the project, every function at every stage either **does exactly what - [Comp-to-single DCC keys](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - key 0x10 (`GFX9_DCC_CLEAR_SINGLE` in Mesa `ac_descriptors.h`) is expanded only by a CB DCC decompress pass over uniform keys of a non-linear target: every texel takes the element at the start of its 256-byte block, the top-left texel of the block where radv's `build_clear_dcc_comp_to_single_shader` stores the clear color. A fast-clear eliminate over such keys and mixed keys throw; textures and color targets that read them use their stored texels. "Uniform" is judged over the key bytes the driver reads, one per 256 surface bytes (see DCC key stores above), so a pipe-aligned surface whose keys differ past that range is expanded as if all were comp-to-single, as for the other clear codes. Not verified on hardware - [8_SRGB color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) on a device that cannot attach and blend R8_SRGB render into an RGBA8_SRGB copy of the target, so the sRGB encode, decode and blend rounding are the device's; the console's are not measured. - [8_SRGB color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. +- [1_5_5_5 color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. - [COPY_DATA from the GPU clock](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect. - [16-bit RGBA color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support the standard and the alternate (`COMP_SWAP` 1) component order. Alternate swaps red and blue in shader exports, write masks and constant-color blend factors, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c). The reverse orders (`COMP_SWAP` 2 and 3) keep throwing when blending, and simultaneous targets requiring conflicting blend constants throw. Console equivalence has not been measured on hardware. - [UINT16_ABGR color exports](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - export format 7 is accepted only into an unsigned integer target, and each 16-bit value reaches the target unchanged; a value beyond a narrower target's range (`R8_UINT`) is undefined in Vulkan, and whether the console clamps or truncates it is not confirmed. SINT16_ABGR (8) throws - `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. - `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). Rosetta runs AVX2, FMA, BMI1/2, F16C and MOVBE but reports none of them through CPUID, so code that checks CPUID takes its baseline path. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB; the same value reaches RDTSCP's ECX, which is not converted. ADCX/ADOX fault under Rosetta and are not converted: compilers do not emit them, and code that checks CPUID sees no ADX. The conversions are covered by the converter tests; that the converted code runs under Rosetta was checked on an M5 Max only, outside this repository's CI. -- `--to-intel` replaces the approximate VEX.128 register forms of VRSQRTPS and VRCPPS with [correctly rounded](../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). The 256-bit, scalar, legacy and memory forms keep the native instruction, as does a 4-byte site with no movable instruction after it for the jump (5 sites in PPSA21564, counted before a following RIP-relative instruction could move into the stub; not recounted). +- `--to-intel` replaces the approximate legacy SSE and VEX.128 register forms of RSQRTPS/RCPPS and VRSQRTPS/VRCPPS with [correctly rounded](../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). Legacy SSE forms preserve the destination's upper YMM bits; VEX forms clear them. The 256-bit, scalar and memory forms keep the native instruction, as does a site shorter than 5 bytes with no movable instruction after it for the jump (5 sites in PPSA21564, counted before a following RIP-relative instruction could move into the stub; not recounted). - [sceKeyboardGetKey2Char](../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) (libSceKeyboard) translates the 101-key (US) arrangement and the alphanumeric layer of the 106-key (Japanese) one, which follows the JIS layout of the HID usages: the Yen (0x89) and Ro (0x87) keys return `\` (0x5C) unshifted, as JIS X 0201 places the yen sign there; whether the console returns U+00A5 instead is unverified. With the Kana LED (bit 4, the HID position) set the 106-key arrangement throws, as the kana the console returns are unknown. Ctrl and Alt do not change the character. - [sceAudioOut2Set3DLatency](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) and [sceAudioOut2MasteringInit, sceAudioOut2MasteringTerm and sceAudioOut2MasteringSetParam](../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) have no effect: there is no 3D rendering or mastering stage. - [sceImeKeyboardGetInfo](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) returns `0x80bc0023` (no resource id) for every id while a keyboard is open, since the IME keyboard never reports a connected device. [sceImeKeyboardSetMode](../../core/libs/prx/libSceIme/Export.cpp) validates the mode bits but the mode has no effect, as no IME keyboard events are delivered. - [AudioOut2 contexts](../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device read a grain's port buffers at the next push, at least 100 us after the grain was pushed, and play the mix behind a 40 ms cushion that the queue level leaves out. When the hardware reads the buffers is not known. The timing follows a title whose mixer fills a buffer after pushing it, reuses it two pushes later, and may still be writing it when the next push follows within microseconds. The last grain pushed before a title stops pushing is never read or played. +- [sceAudioOutSysGetHdmiMonitorInfo](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) reports an HDMI sink without a monitor name or audio descriptors: it zeroes the 0x180 bytes and returns 0, since audio reaches the host through SDL as PCM and there is no EDID to describe. The arguments `(type, info, size)`, the required size 0x180 and the fields titles read (monitor name at +0x0B, 13 bytes; descriptor count as a 32-bit value at +0x94; 8-byte descriptors `[coding type, channels, rate mask, 0, extra, 0, 0, 0]` from +0x98) come from [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/c3be9c2afcb8f7838784ead0fe1c12a2ceea965a/docs/FINDINGS.md) (firmware 12.70, one monitor). What a console without an HDMI audio sink returns, an error or no descriptors, is unknown. The other bytes, among them the header at +0x90 (`01 09 4f 00` on that monitor), have no known meaning and are written as 0. No LPCM descriptor is reported for the host device, as the rate mask and extra byte of one are not known. A null buffer returns `0x80260004` like the other AudioOut calls; the console's code is not measured. Monitor type 0x10, which the console also accepts, other types and sizes other than 0x180 throw: their results and error codes are unknown. - [libSceAudiodec](../../core/libs/prx/libSceAudiodec/Export.cpp) throws for the 24-bit PCM word size (`iBwPcm` 0): its sample layout is unknown. ATRAC9 decoding is covered only by configuration and error tests, as no ATRAC9 encoder is available for a fixture. - [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) throws when the original file length is requested (`parse_ofl` non-zero) for a layer other than III, for a CRC-protected frame, or for a VBRI header outside MPEG-1 stereo: shadPS4 looks for the tag after the CRC and after the side information, while LAME and FFmpeg place it without the CRC and VBRI always at byte 36. - [ATRAC9 gapless segments](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a run job stops after the gapless total of output samples counted from the start of the segment, leaving the rest of its input unconsumed, and the next run starts the next segment with the decoder as it is and without a second skip. FMOD in PPSA29141 sets a total shorter than a sound's frames and reads the frames after it on later runs; with the output held at the total its stream reader waited for them forever and the last buffer repeated. The console's handling of the frames after the total is not documented; a reset (`SetGaplessDecode` with the reset flag, `ClearContext`, initialization) starts a new segment. An ATRAC9 run job without `RUN_MULTIPLE_FRAMES` decodes its whole input as one with it: FMOD in PPSA29141 puts two run jobs in one batch with the second's input right after the whole of the first's. Whether the console stops after one frame there is not verified; MP3 and Opus run jobs still stop after one frame @@ -591,9 +654,11 @@ Throughout the project, every function at every stage either **does exactly what - [The narrow printf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, so a narrow `%p` there still follows the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`); `%m` and argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter - [collate::_Getcat](../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size, and for results of 4095 characters or more, whose string storage uses the guest's over-aligned large allocation. - [The scanf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) hands `%i` to glibc on Linux, which since 2.38 reads the C23 `0b`/`0B` binary prefix when the library is built as GNU C++: `sscanf("0b101", "%i", &value)` stores 5 where C17 stores 0 and stops at the `b`. The strtol family stops at the prefix as C17 does +- [The scanf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, on Linux throws for the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`): glibc would store a 32-bit `wchar_t` per character into the guest's 16-bit buffer. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits - [The scanf family](../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` - [socketpair](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLHUP` without `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, where the console reports `POLLIN`, with `POLLHUP` only after `close` - [UDP recv/recvfrom](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) and [sceNetRecv/sceNetRecvfrom](../../core/libs/prx/libSceNet/Export.cpp) translate Windows WSAEMSGSIZE into the copied byte count for datagram sockets, including MSG_PEEK, and retain the sender address. This follows FreeBSD 11.4 recv(2) and Winsock's documented truncated-datagram behavior; console behavior and existing title impact are unverified. +- [send, sendto](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - send and sendto without an address on an unconnected datagram socket map Windows `WSAENOTCONN` to FreeBSD's `EDESTADDRREQ` (39). - [read, write and vector I/O](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. Guest socket descriptors start at `0x10000000` and are allocated upward, so the descriptor range selects the guest socket table or the host descriptor table. `EPIPE` from a write raises the host's `SIGPIPE` on Linux, which the guest can neither ignore nor suppress: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; `SO_NOSIGPIPE` is not an option here, and raises nothing on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor - [poll](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits only on guest sockets, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A descriptor below the socket range (files, pipes, stdio) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), `POLLPRI` on Windows, which `WSAPoll` rejects, and an infinite timeout with no socket to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked - [Socket error numbers](../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 @@ -606,6 +671,7 @@ Throughout the project, every function at every stage either **does exactly what - The [s_swappc_b64 fetch-shader call at vertex/local/tessellation/mesh entry](../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) assumes the called fetch-shader blob ends in `s_setpc_b64` to the link pair (the GNM fetch-shader shape shadPS4's `EmitFetch` copies); the blob is not part of the shader program, so its return closes the pairing by assumption and any in-program reader of the link pair throws. Attribute fetches are inlined from the request's parsed fetch-shader plan through the same `GetAttribute` path as the embedded fetch analyzer, honoring the buffer resource `dst_sel`; an attribute writing more than four registers, or an entry call without a parsed plan, throws. The vertex/instance offset accumulator inside the fetch blob is not replayed for external blobs. - The scalar aperture sources [src_shared_base, src_shared_limit, src_private_base, src_private_limit](../../core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp) (core/shader/recompiler) decode to their own operand kinds (scalar source codes 235 to 238, named as in LLVM's `SIRegisterInfo.td`), but their value is not modelled: reading one refuses with the source it refused named, because the wave's shared and private aperture bases and limits are unknown. Both the scalar-code read and the raw-operand read refuse; a shader that only carries the value into an address still prepares, one that reads it does not - [VertexBufferReadSize](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) and the [vertex staging copy](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - a vertex fetch whose index is at or past the descriptor's `num_records` no longer throws for `OOB_SELECT` 0 and 1. The copy still spans the whole fetch, but the bytes from `num_records * stride` onward read as zeros, so the out-of-range vertex is `(0, 0, 0, 0)`. Measured with `buffer_load_format_xyzw` (`idxen`) through the [hardware oracle](HW_ORACLE.md) on RDNA2 gfx1036 for PPSA01325's word 3 `0x0004dfac` (`OOB_SELECT` 0) in #2241: indices 3, 4 and 5 over 3 records each return `00000000 00000000 00000000 00000000`, W included, unlike Vulkan `robustBufferAccess`, which returns `(0, 0, 0, 1)`. The zeros come from the record check, not from memory: stored values placed past the records are ignored, and a `num_records` 6 control returns them. `RESOURCE_LEVEL` 1, `OOB_SELECT` 1 and FORMAT 74 `32_32_32_FLOAT` give the same zeros, in wave32 and wave64; gfx1013 (the console) is not measured. `OOB_SELECT` 2 returns the stored values past the records and `OOB_SELECT` 3 compares `num_records` against the byte offset, both measured on the same setup in #2241; the two keep throwing past `num_records` here, since each needs its own rule. Zero-stride descriptors (`stride` 0) keep their separate byte-count check and are not covered here. Each out-of-range fetch is copied alone and outside the draw-snapshot cache, which is neither read nor written for it: a reused snapshot would carry another draw's bytes into the zero region, and a stored zero-padded copy would hand zeros to a later in-range draw at the same address. The valid prefix is still flushed for GPU writes before it is read. +- [EmitSetAttribute](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a primitive with a vertex whose clip position is (0,0,0,0) is clipped away through an extra `gl_ClipDistance` element on vertex and tessellation evaluation shaders, matching what a Radeon 610M (RDNA2, RADV) and llvmpipe rasterise; NVIDIA 615.78.08 without it draws the vertex at the viewport centre. Measured for triangles (strip and list) and for a point at (0,0,0,0), which the Radeon 610M does not draw, NVIDIA draws at the viewport centre and llvmpipe at the top-left corner; lines with such a vertex are clipped by the same element and were not measured. Mesh, geometry and the generated rect-list and barycentric stages do not write it, a device without `shaderClipDistance` cannot compile vertex shaders (the compile throws), and guest clip distances are still refused by the pipeline validator. The PS5 rasteriser was not measured. - [sceImeKeyboardGetResourceId](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) reports that no USB keyboard is connected (`0x80bc0004`, as [shadPS4](https://github.com/shadps4-emu/shadPS4) does) because IME keyboard events are not delivered to the handler, although [libSceKeyboard](../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) exposes the host keyboard - [Player review dialog](../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) shows nothing: `scePlayerReviewDialogOpen` finishes at once and the result reports that the user canceled, as `sceWebBrowserDialogOpen` does - [NP reachability callbacks](../../core/libs/prx/libSceNpManager/Export.cpp) retain one callback and its user data; duplicate registration and unregistering an absent callback return the errors used by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/np/np_manager.cpp). No callback is delivered because the existing NP model remains signed out with reachability unavailable. @@ -614,13 +680,13 @@ Throughout the project, every function at every stage either **does exactly what - [Temporary app-content storage](../../core/libs/prx/libSceAppContent/Export.cpp) tracks one `/temp0` mount, preserves its files on unmount, and rejects new path resolutions until remount. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); the pointer ABI is also declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). Existing open host descriptors remain usable after unmount; console behaviour with open files is unverified. Host filesystem failures still throw. - [Guest path resolution](../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation. - [localtime, mktime](../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule; outside that range `localtime` returns null and `mktime` returns -1, where the console converts. `gmtime` converts every time whose year fits an `int` on both hosts. -- [strftime](../../core/libs/prx/libc/src/Time.cpp) (libc) expands FreeBSD's `%k`, `%l`, `%s`, `%v` and `%+` itself, ignoring a leading `E` or `O` as FreeBSD does, and passes every other conversion to the host's `strftime`. On Windows the conversions the CRT lacks (`%P` and the `%-`, `%_` and `%0` padding flags) still produce nothing, and the padding flags are not supported on the five new conversions at all (`%-k`, `%_k` and `%0k` go to the host), an unknown conversion, a doubled `E` or `O` modifier (`%EE`) or a trailing lone `%` returns 0 where FreeBSD prints `%` and the character, and a standalone `%z` or `%Z` uses the host's zone instead of the broken-down time's `tm_zone`. `%s` follows FreeBSD 9 and 11 and is the local-time `mktime` of the broken-down time (current FreeBSD uses `tm_gmtoff`), which is -1 on Windows outside the range above. `%+` is the C locale's `%a %b %e %H:%M:%S %Z %Y` with `%Z` taken from `tm_zone`, or from `tzname[tm_isdst != 0]` when it is null. +- [strftime](../../core/libs/prx/libc/src/Time.cpp) (libc) expands FreeBSD's `%k`, `%l`, `%s`, `%v` and `%+` itself, ignoring a leading `E` or `O` as FreeBSD does. Unknown conversions, repeated modifiers and incomplete conversions emit literals as in FreeBSD 11; recognized conversions otherwise go to the host's `strftime`. On Windows the `%-`, `%_` and `%0` padding flags still produce nothing, including on the five expanded conversions (`%-k`, `%_k` and `%0k` go to the host), and a standalone `%z` or `%Z` uses the host's zone instead of the broken-down time's `tm_zone`. `%s` follows FreeBSD 9 and 11 and is the local-time `mktime` of the broken-down time (current FreeBSD uses `tm_gmtoff`), which is -1 on Windows outside the range above. `%+` is the C locale's `%a %b %e %H:%M:%S %Z %Y` with `%Z` taken from `tm_zone`, or from `tzname[tm_isdst != 0]` when it is null. - [LINE_STIPPLE_TEX](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) reads as 0: line stipple is not emulated. RADV enables the input only as the required pixel input of a shader that reads no interpolants, because it initializes the fewest VGPRs ([Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_shader.c) `radv_compute_spi_ps_input`), and never reads it - [Draw snapshot buffer descriptors](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) rewrite each affected array from element zero, preserving unchanged descriptors. MoltenVK 1.4.2 writes auxiliary buffer lengths at index `i` instead of `start + i` for partial array updates ([MVKDescriptorSet.mm, BufferAuxSize](https://github.com/KhronosGroup/MoltenVK/blob/v1.4.2/MoltenVK/MoltenVK/GPUObjects/MVKDescriptorSet.mm#L1051)). Per-element writes therefore let one buffer length replace another even when Vulkan buffer handles and ranges are correct. - [Static scalar calls](../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. A pair serves one call, or several calls whose regions are disjoint, each closed by the first return reading the pair after its target. Outside every region of its calls the pair may only be overwritten whole, by `s_mov_b32`, `s_mov_b64`, an SOP2 result or a scalar load, as the caller of a user-data callee does before each call. Regions must nest or be disjoint; recursion, other shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented. -- [Relative vector addressing](../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) keeps every vector register from the lowest base register the program addresses relatively up to the last register the program names in a zero-initialised function-scope array per invocation (`LoadVectorWindow`/`StoreVectorWindow`, one element per register), so an indexed access is one load or store instead of a compare-and-select chain over the window; the host compiler decides whether that array stays in registers or spills to scratch, which is not measured. A register in the window the program never wrote reads 0, as an unwritten SSA register does; the console's value is undefined. M0 is unsigned: an index at or past the window reads `v0` and drops its write, and `v_swaprel_b32` writes neither side when either is out of range, as measured on RDNA2 (gfx1035, `agc_driver_relative_indexing`). The window ends at the last register the program names rather than at the shader's VGPR allocation, which the recompiler does not receive, so a relative write past the named registers but inside the allocation is dropped where the hardware keeps it for a later relative read. Killzone: Liberation's PSP vertex program (PPSA11142, 3176 ISA words, 104 relative accesses over 154 registers) went from 649,515 to 392,333 SPIR-V words with this. +- [Relative vector addressing](../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) keeps every vector register from the lowest base register the program addresses relatively up to the last register the program names in a zero-initialised function-scope array per invocation (`LoadVectorWindow`/`StoreVectorWindow`, one element per register), so an indexed access is one load or store instead of a compare-and-select chain over the window; the host compiler decides whether that array stays in registers or spills to scratch, which is not measured. A register in the window the program never wrote reads 0, as an unwritten SSA register does; the console's value is undefined. M0 is unsigned: an index at or past the window reads `v0` and drops its write, and `v_swaprel_b32` writes neither side when either is out of range, as measured on RDNA2 (gfx1035, `agc_driver_relative_indexing`). The window ends at the last register the program names rather than at the shader's VGPR allocation, which the recompiler does not receive, so a relative write past the named registers but inside the allocation is dropped where the hardware keeps it for a later relative read. Killzone: Liberation's PSP vertex program (PPSA11142, 3176 ISA words, 104 relative accesses over 154 registers) went from 649,515 to 392,333 SPIR-V words with this. Pixel programs whose relative span contains the packed ancillary input instead keep that span in SSA and implement indexed operations with compare/select chains. This keeps ancillary definitions visible to field lowering and masked-write proofs; storing the unmodelled whole word in a private array would require composing unknown bits. A fold after masked-select elimination lowers fields whose consumers become visible only after redundant copies are removed. Whole-word uses still throw. Both representations include implicit entry inputs in the inferred register limit and retain unsigned M0, inactive-write and DPP update rules; wave32/wave64 native Vulkan readbacks cover relative reads, writes, split-offset swaps, inactive writes, DPP masks and out-of-range accesses, but console comparison and the performance cost of selecting over this span are unverified. - [User-data callees](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is a user-data pair, or a copy of one made with `s_mov_b32` or `s_mov_b64` in the same block, calls the code at the address that user data holds. At dispatch the callee is read from guest memory up to its `s_setpc_b64` on the link pair, one copy of it is appended per call after the caller's last instruction, and the call is rewritten to `s_call_b64` to that copy. The words after the caller's last instruction are dropped; in the shaders seen they are metadata, and a caller that reads its program counter, which could address them, throws. A copy runs at another program counter than the callee, so a callee that reads its program counter, makes a call, ends the program, jumps through another pair, returns before code it branches to, spans more than 64 KiB or is not readable throws. Callees are read again whenever a dispatch misses the dispatch cache; a dispatch that hits it reuses the linked program, so a title that rewrote a callee in place at the same address would keep running the old code. -- [DB_RENDER_CONTROL resummarize draws](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - HTILE is not modeled, so a RESUMMARIZE_ENABLE draw without depth or stencil tests and without color writes changes no modeled state and draws nothing; any other resummarize, copy or decompress draw throws +- [DB_RENDER_CONTROL resummarize draws](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - HTILE is not modeled, so a RESUMMARIZE_ENABLE draw without depth or stencil tests and without color writes changes no modeled state and draws nothing. A pure resummarize with pass-through D16/D32 depth, no depth writes or bounds test, no stencil plane, no color writes and no running pixel stage also draws nothing; other resummarize, copy or decompress draws throw. - [s_call_b64](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) saves `PC + 4` over `GetShaderBase` and branches to `PC + 4 + signext(SIMM16) * 4`, following [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) sections 3.6.3, 12.2 and 12.3. Forward callees after `s_endpgm` are decoded. Measured on RDNA2 (gfx1036) in wave32 and wave64 with SIMM16 = 3: the link pair receives `PC + 4`, the branch goes to `PC + 4 + signext(SIMM16) * 4`, and `s_setpc_b64` of the link returns to the instruction after the call. Execution on the PS5 (gfx1013) and title compatibility for these bounded scalar calls remain unverified; `s_rfe_b64` still throws in translation. - [sceVideodec2CreateDecoder](../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) decodes HEVC (codec type 974921) and VP9 (2382845) through FFmpeg, besides H.264. The tags come from SharpProspero's `Videodec2CodecType` and EVO-PLAYER-PS5's `sce_videodec2.h`, which records them from hardware runs. HEVC input is converted from length-prefixed NAL units to Annex B as for H.264, and VP9 frames are passed through. Output is the same 8-bit NV12 for every codec; a decoder for HEVC Main10 or VP9 Profile 2 is created, but a frame deeper than 8 bits throws, since the console's 10-bit output layout is unknown, and `QueryDecoderMemoryInfo` reports the 8-bit frame size for it. `sceVideodec2GetPictureInfo` and `sceVideodec2GetAvcPictureInfo` throw for HEVC and VP9 pictures, which use other layouts (`SceVideodec2HevcPictureInfo`, `SceVideodec2Vp9PictureInfo`), and `sceVideodec2GetHevcPictureInfo` and `sceVideodec2GetVp9PictureInfo` are not exported - [syslog](../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER`, and errno is preserved. `setlogmask` follows [FreeBSD 11](https://github.com/freebsd/freebsd-src/blob/8d2708de5d6112a492744d367cfdfb8fd219227a/lib/libc/gen/syslog.c): the process-wide mask defaults to 0xff, zero queries it, and nonzero values replace it; masked messages are skipped before formatting, including the unknown-priority diagnostic. The console's mask behaviour is unverified. `openlog` and `closelog` are not implemented, so their identity and options are not applied @@ -630,19 +696,27 @@ Throughout the project, every function at every stage either **does exactly what - [getsid](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: process sessions are not modelled. PPSA74126 imports it and fails to load without it; the title made no call to it in a 60 s run - [sigaction](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` of an `SA_SIGINFO` handler on a signal `signal` delivers throws instead of terminating, and `raise` and `kill` of the other signals still fail with `EINVAL`, recorded handler or not - [kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported -- [sceKernelFcntl](../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) handles socket descriptors through the POSIX `fcntl` export (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`) and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use; the command semantics follow FreeBSD's [fcntl](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_descrip.c), and a closed descriptor returns `SCE_KERNEL_ERROR_EBADF`. File descriptors throw: their open flags are not tracked, and `F_DUPFD`, `F_GETFD` and `F_SETFD` are unsupported on all descriptors. The return values and error codes are unverified on a console -- [ioctl, _ioctl](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket. Any other request on a socket fails with `ENOTTY`, and any request on a file or pipe descriptor fails with `EBADF`, where FreeBSD answers `FIONBIO` and `FIONREAD` there too and fails other requests, termios ones included, with `ENOTTY`. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests +- [sceKernelFcntl](../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console +- [fcntl](../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `EINVAL` for every other command, `F_GETFD` and `F_SETFD` included). The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and throw on Windows, and `F_GETLK` with `F_UNLCK` throws. The return values and error codes are unverified on a console +- [ioctl, _ioctl](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other request throws. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests - [sendfile](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A socket as the file fails with `EINVAL`, as on FreeBSD, but any other descriptor `sceKernelPread` reads, such as `/dev/urandom`, is sent where FreeBSD requires a regular file or shared memory, and a file descriptor as the socket fails with `EBADF`, as for `send`, where FreeBSD answers `ENOTSOCK`. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it - [pthread_kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. Signal 0 reports any other thread handle as existing, without checking that the thread is still alive. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread throws, since guest signals are not delivered to a chosen thread - [select](../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) validates `tv_usec` but saturates a valid timeout beyond the remaining `steady_clock` range at `time_point::max()`, so the wait can continue effectively indefinitely. The console's behavior for a valid timeout outside the host clock's range is unknown. - [AJM](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix. +- PPSA30601 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [six sce::Json::Object::Pair, sce::Json::Object and sce::Json::Array::iterator members](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [sceAmprAmmGetUsageStatsData](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr), [sceMouseResetAbsolutePosition](../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) and [sceSaveDataDirNameSearch2](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - PPSA07411 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceSslCreateConnection, sceSslDeleteConnection, sceSslEnableVerifyOption, sceSslConnect, sceSslRead, sceSslWrite](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl), [four sce::Json::Value members (setElementAccessFailureHandler, setSpecialFloatHandler, setTypeMismatchHandler, operator bool)](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [sceConvertKeycodeGetCharacter](../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) and [sceNpSessionSignalingGetGroupFromSessionId](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - [Comparison-sampled R32 float and R16 unorm textures](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) whose surface a storage image holds are sampled from a D32_SFLOAT or D16_UNORM image that the GPU refills from the storage image whenever its version moves, because Vulkan compares only depth formats. Only surfaces of at least 1 MiB (`APS5_DEPTH_COPY_MIN_KIB`) that are not 3D take the copy; smaller ones stay CPU snapshots compared against guest memory. With copies of small surfaces, one title hit an intermittent boot-time crash that was not explained. - [Null vertex buffer V#](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware. +- PPSA07411 PS VR2 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceHmd2GetDeviceInformationByHandle, sceHmd2GetFieldOfView and sceHmd2Terminate](../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2), [scePadVrControllerSetVibration, scePadVrControllerSetVibrationMode and scePadVrControllerReadCombinedState](../../core/libs/prx/libScePad/Export.cpp) (libScePad) and [sceVrTracker2ResetLocalCoordinate](../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2) - [Descriptor pool recycling](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) avoids individually freed sets: [RADV 26.2.2](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_descriptor_pool.c) initializes its free-set heap with 32 bytes beyond the backing allocation. An allocation-only RX 6600 test reproduces acceptance of a 4,128-byte set in a 4,096-byte pool; the path without individual frees rejects it. Pools reset after their last set is released. A cached set can retain unused slots in an exhausted pool until eviction; peak gameplay pool memory remains unmeasured. - [sce::Json::InitParameter2 base-object constructor](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - forwards to the complete-object constructor and uses its inferred layout and default field values; constructor equivalence and initialization bytes remain unverified on the console - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [fstatvfs](../../core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp) (libSceLibcInternal) converts `_fstatfs` to the 88-byte `struct statvfs` as FreeBSD 9.2's `sfs2svfs` does (`f_bsize` from the transfer size, `f_frsize` from the block size, `f_favail` equal to `f_ffree`, `f_fsid` 0, `ST_RDONLY` and `ST_NOSUID` only). Differences: `f_namemax` is the `statfs` value, not `fpathconf(_PC_NAME_MAX)`; negative `f_bavail` and `f_ffree` wrap to unsigned without `EOVERFLOW`, as the same-width copy in FreeBSD does; inode counts are 0 on Windows, inherited from `_fstatfs`; a null output returns `EFAULT` after the descriptor is validated, where FreeBSD would fault. Layout and conversion are unverified against the PS5 library (PS5MC imports `fstatvfs` but was not run) +- [sceVoiceQoSEnd](../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0901` (not initialized) before any initialization was measured on a PS5 (firmware 13.60, #1407); ending an initialized library was not, because a valid `sceVoiceQoSInit` never returns inside a homebrew payload. PPSA14632 calls it at shutdown and PPSA26344 after a nonzero `sceVoiceQoSInit`. After an initialization it returns 0 and clears the init flag, so the library can be initialized again. Whether the console also ends the libSceVoice instance that its `sceVoiceQoSInit` starts ([prosper](https://github.com/mattias800/prosper)'s reading of the module), and what it answers while endpoints exist, is unknown; no endpoint is implemented - [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. +- [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) and [sceAgcDriverRegisterWorkloadStream](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - the driver call records only the stream id (1 to 31; it returns 0 for any argument and keeps no stream record), and the DCB calls write the packets [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/agc.cpp) writes: an 18-dword (active) or 12-dword (complete) `IT_NOP` packet with the stream id, and for active the 64-bit mask of the workload ids, for complete the workload id and the 64-bit clear mask of that id. Both return null for a null command buffer, an unregistered or out-of-range stream, a workload id above 63, and for active a duplicate id or a count of 0 or above 63. The layouts and checks come from KytyPS5 alone; the console oracle in [prosper](https://github.com/mattias800/prosper/blob/main/prosper/tests/data/console_oracle/agcbuild.golden.tsv) measures the null return only for unregistered streams, not the packet contents of a registered one. The error codes the console driver returns for an invalid or repeated registration are unknown, so registration never fails here. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed. - [sceAgcCreatePrimState](../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well +- [pthread_spin_init, pthread_spin_destroy, pthread_spin_lock, pthread_spin_trylock, pthread_spin_unlock](../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) follow FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) and the owner checks of its [`thr_umtx.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_umtx.h#L80-L175), unverified on the console: `EINVAL` for a null or destroyed lock and a `pshared` other than 0 or 1, `EBUSY` from `trylock` while any thread holds it, `EPERM` from `unlock` by a thread that does not hold it. `PTHREAD_PROCESS_SHARED` allocates a private lock, as guest code runs in one process. libthr spins forever when the holder locks again; here that call throws +- [pthread_cond_reltimedwait_np](../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) takes `(cond, mutex, usec)`, a relative timeout in microseconds, and is `scePthreadCondTimedwait` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadCondTimedwait` ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/condvar.cpp#L229-L234)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `sem_reltimedwait_np`. Unverified on the console +- [sysctl kern.arandom](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) serves FreeBSD 11's MIB {1, 37} (CTL_KERN, KERN_ARANDOM) and the "kern.arandom" sysctlbyname lookup through std::random_device, returning min(*oldLength, 256) random bytes. Following FreeBSD's sysctl_kern_arnd, a null buffer reports min(*oldLength, 256) without writing, a null length returns 0 without writing, and requests with a new value return EPERM (1) as the node is read-only. Cryptographic quality depends on the host's random device implementation. diff --git a/docs/user/COMPATIBILITY.md b/docs/user/COMPATIBILITY.md index a6eb265f4..ee2e485fd 100644 --- a/docs/user/COMPATIBILITY.md +++ b/docs/user/COMPATIBILITY.md @@ -11,6 +11,8 @@ DOOM Shareware is a public homebrew, [lowbit/ps5-doom](https://github.com/lowbit - Windows 11, RTX 4070 SUPER (v1.1.1, `main` `2edaf7f4` plus open PRs, reported in [#690](https://github.com/boykopovar/AnyPS5/pull/690)): further maps, weapon switching, new games, level select, save/load round trip and audible audio. - Linux, Ubuntu 24.04 container, Mesa llvmpipe, no hardware GPU (v1.1.1, `main` `0e173a46` plus [#1278](https://github.com/boykopovar/AnyPS5/pull/1278), [#1279](https://github.com/boykopovar/AnyPS5/pull/1279) and [#1307](https://github.com/boykopovar/AnyPS5/pull/1307), relinked with `--exclude-sce-module libc.prx`, see [#1497](https://github.com/boykopovar/AnyPS5/issues/1497)): E1M1, 35 FPS in game; audio and extended gameplay not tested. The `/download0` directory must exist beside the executable. - Linux, CachyOS, NVIDIA RTX 3080 Ti, driver 610.57.04, KDE Plasma Wayland (v1.1.1, `main` `a11c8f7f7` without extra PRs; `eboot.bin` and `sce_module/libc.prx` unwrapped from fSELF before relinking, see [#2599](https://github.com/boykopovar/AnyPS5/issues/2599); the title's `libc.prx` converted as a guest module, no `--exclude-sce-module`): launcher at 60 FPS, E1M1 at 35 FPS in game; new game from the menu, movement, turning, firing, an item pickup and the automap checked with controller input from `APS5_PAD_INPUT_FILE`. Audio output opened at 48000 Hz but was not listened to; later maps and save/load not tested. The `/download0` directory did not need to exist before the first run. +- Linux, CachyOS, AMD Ryzen 5 PRO 5650U integrated graphics (RADV RENOIR), Mesa 26.2.4, kernel 7.2.9, KDE Plasma 6.7.5 Wayland (v1.1.1, `main` `a11c8f7f7` without extra PRs; `eboot.bin` and `sce_module/libc.prx` unwrapped from fSELF before relinking, see [#2599](https://github.com/boykopovar/AnyPS5/issues/2599); the title's `libc.prx` converted as a guest module, no `--exclude-sce-module`): launcher at 60 FPS, E1M1 and E1M2 completed at 35 FPS in game with a DualShock 4 through SDL (both sticks, D-pad, touchpad, options menu), audio heard, save/load round trip in `/download0`. The first run without a shader cache stayed on AGC compute presentation. `/dev/udmabuf` was accessible to the user, so shared direct memory was imported as dma-buf. +- Windows 11 Pro 10.0.26300, AMD Radeon RX 9060 XT (RDNA 4, driver 32.0.31041.1004) and the Ryzen 9 7950X integrated graphics (RDNA 2, driver 32.0.21045.5002), selected with `ANYPS5_GPU` (v1.1.1, `main` `b42ef24e0` without extra PRs; `eboot.bin` and `sce_module/libc.prx` unwrapped from their SELF and fSELF containers before relinking, see [#2599](https://github.com/boykopovar/AnyPS5/issues/2599); the title's `libc.prx` converted as a guest module, no `--exclude-sce-module`): on both devices, menus, a new game, movement, item pickups and firing in E1M1 with an Xbox One controller through SDL, audio heard. The first run without a shader cache was choppy and later runs were smooth; the frame rate was not measured. Later maps and save/load not tested. The `/download0` directory was created before the first run. - On the first run without a shader cache, the presentation kernel is compiled at its first dispatch, frame 1 does not complete in time and the title switches to the CPU scaler. Later runs with the cached kernel stay on AGC compute presentation (measured on Windows 11, RTX 3080, `main` `d70b8998`). -Not verified: Windows on current `main` without extra PRs, Linux with AMD or Intel GPUs. +Not verified: Linux with Intel GPUs. diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index 56509963d..83876a67d 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -30,4 +30,17 @@ R2 = MOUSE:Right ToggleMouse = MOUSE:Middle ``` +The same file also accepts these settings, each at most once, in the form `Setting = Value`: + +| Setting | Value | Default | +|---------|-------|---------| +| `LeftStickDeadzone`, `RightStickDeadzone` | Whole percentage from 0 to 90 of the controller stick's travel that reads as centered. Outside it the travel is rescaled, so the stick still reaches its full range. Keyboard and mouse stick bindings are not affected. | `0` | +| `MouseSensitivity` | Number greater than 0 and at most 10 that scales mouse movement on the right stick while the mouse is captured (`ToggleMouse`). | `1` | + +```ini +LeftStickDeadzone = 12 +RightStickDeadzone = 8 +MouseSensitivity = 2.5 +``` + An invalid line reports the file and line number and stops input initialization. If the configured file does not exist or cannot be read, AnyPS5 reports an error. With no `anyps5-input.ini` and no `ANYPS5_INPUT_CONFIG`, the built-in mapping is used. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 3ff968d17..9dbe5d410 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -33,9 +33,9 @@ macOS output (x86-64 Mach-O, which runs under Rosetta on Apple silicon): relinker --macos source/input.elf eboot ``` -Add `--to-intel` for Intel hosts, or `--to-rosetta` for Rosetta; a title that runs under Rosetta on Apple silicon is relinked with `--macos --to-rosetta`. The output format defaults to Linux ELF regardless of the filename; `.exe` alone does not select Windows. +Add `--to-intel` for Intel hosts, or `--to-rosetta` for Rosetta; a title that runs under Rosetta on Apple silicon is relinked with `--macos --to-rosetta`. `--to-intel` rewrites AMD-specific instructions found in the executable and bundled modules; it cannot translate AMD-specific instructions generated by a title at runtime, so such titles require an AMD host. The output format defaults to Linux ELF regardless of the filename; `.exe` alone does not select Windows. -The executable, registry, icon and desktop entry output files must not refer to the input executable or a bundled module being converted, including through a hard link or symbolic link. These collisions fail before any output is written. An existing output file can be replaced if it is separate from those inputs. +The executable, registry, Windows import diagnostics, icon and desktop entry output files must not refer to the input executable or a bundled module being converted, including through a hard link or symbolic link. These collisions fail before any output is written. An existing output file can be replaced if it is separate from those inputs. ## Options @@ -56,7 +56,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--rpath ` | Set the system library search path. Quote `$ORIGIN` to prevent shell expansion, for example `--rpath '$ORIGIN/libs'` in Bash or PowerShell. Linux guest modules require an absolute path or a path beginning with `$ORIGIN`. Windows requires a nonempty ASCII path and supports `$ORIGIN` as the executable directory. | | `--autorun` | Run the output after conversion, print its exit code, and wait for Enter. Adds executable permissions for Linux output. Requires the target OS and prepared runtime layout. | | `--sce-module-path ` | Parent of the module directories, not the module directory itself. Default: input ELF parent; relative input paths resolve from the process working directory. Explicit absolute path: used directly. Explicit relative path (including `.` and `..`): relative to the process working directory. Output path has no effect. Requires one nonempty value; repeats, nonexistent or nondirectory paths, access failures, and combination with `--skip-sce-module` are errors. | -| `--module-dir ` | Relink the ELF modules of a directory under the module parent directory, besides `sce_module`/`sce_modules`/`prx`; outputs keep the relative path. A module the executable or another module needs is initialized at startup, any other when the title loads it with `sceKernelLoadStartModule`, with its start arguments (for example Unity plugins in `Media/Plugins`). Repeat for multiple directories. Conflicts with `--skip-sce-module`. | +| `--module-dir ` | Relink the ELF modules of a directory under the module parent directory, besides `sce_module`/`sce_modules`/`prx`; outputs keep the relative path. A module the executable or another module needs is initialized at startup, any other when the title loads it with `sceKernelLoadStartModule`, with its start arguments (for example Unity plugins in `Media/Plugins`). Repeatable; `.` is the parent itself, without the input ELF. Conflicts with `--skip-sce-module`. | | `--skip-sce-module` | Deprecated. Disable bundled module processing and module directory checks. Conflicts with explicit `--sce-module-path`. | | `--exclude-sce-module ` | Deprecated. Exclude a bundled module by exact filename, not path. Repeat for multiple files; a missing filename is an error. Conflicts with `--skip-sce-module`. | | `--skip-syscall-check` | Deprecated. Disable syscall scanning in the executable and bundled modules. | @@ -115,9 +115,11 @@ APS5_PAD_INPUT_FILE=/tmp/anyps5-pad ./app.elf python3 tools/pad_input.py /tmp/anyps5-pad cross python3 tools/pad_input.py /tmp/anyps5-pad down --hold-ms 500 python3 tools/pad_input.py /tmp/anyps5-pad release +python3 tools/pad_input.py /tmp/anyps5-pad release --left-stick 128 0 --hold-ms 2000 +python3 tools/pad_input.py /tmp/anyps5-pad cross --left-stick 255 128 --right-stick 0 128 ``` -Run the helper in a second terminal. Commands are polled at most every 100 ms during controller reads and add to physical controller input. A press lasts 300 ms by default, starting when the application reads it, and releases automatically; `--hold-ms` accepts 1–5000 ms. Wait for the application to observe a release before sending another press. The file holds only the latest command; rapid commands can replace one another. Commands left over when the application first reads the controller are ignored. Without the environment variable, file input is disabled. +Run the helper in a second terminal. Commands are polled at most every 100 ms during controller reads, and their buttons add to physical controller input. A press lasts 300 ms by default, starting when the application reads it, and releases automatically; `--hold-ms` accepts 1–5000 ms. `--left-stick X Y` and `--right-stick X Y` hold the sticks for the same time: each axis is 0-255, 128 is the centre, X grows to the right and Y grows downward. While such a command is active both sticks come from it instead of the physical controller, and a stick that was not named stays centred. Wait for the application to observe a release before sending another press. The file holds only the latest command; rapid commands can replace one another. Commands left over when the application first reads the controller are ignored. Without the environment variable, file input is disabled. ### System fonts @@ -131,6 +133,8 @@ Games ask for the console's language with `sceSystemServiceParamGetInt` and show The game runs on the first Vulkan 1.1 device with graphics and compute queues and swapchain presentation, preferring a discrete GPU over an integrated one. Set `ANYPS5_GPU` to a part of a device name, compared without regard to case, to run on another device; the names are printed at start-up in the `Physical device candidate` lines. When no usable device contains the text, the start fails and the error lists the device names. +The window presents with vertical sync (`fifo`). Set `ANYPS5_PRESENT_MODE` to `fifo_latest_ready`, `mailbox` or `immediate` to keep the game from waiting for the display: `fifo_latest_ready` and `mailbox` still show whole frames at the display's refresh and drop the ones in between, `immediate` presents at once and can tear. The start fails on any other value and when the surface does not offer the mode (`fifo_latest_ready` needs `VK_KHR_present_mode_fifo_latest_ready` or its EXT form). Flips stay paced at 59.94 Hz in every mode. A Wayland compositor can stop delivering frames to a window it does not show, for example one on another workspace: on Hyprland with the NVIDIA driver a `fifo` swapchain then presents about once per second, while `fifo_latest_ready` and `immediate` keep the game at its normal rate. + On Windows, the OpenCL, OpenGL, and Vulkan Compatibility Pack can install Mesa Dozen, which exposes D3D12 devices through Vulkan. Some D3D12 drivers can crash while the Vulkan loader enumerates adapters, even when AnyPS5 would select a native Vulkan device. If this happens and the system has another Vulkan driver, disable Dozen for the current PowerShell session before launching AnyPS5: ```powershell diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 961b82c97..50fbd61dd 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -18,6 +18,7 @@ RULES = { "extension": ("error", "CONTRIBUTING.md#code", "Avoid non-standard extensions where standard C++ is enough"), "notes-file": ("error", "CONTRIBUTING.md#branches-and-pull-requests", "Notes, investigation and agent files go in the pull request, not in the repository"), "binary": ("error", "docs/dev/CONVENTIONS.md", "Only UTF-8 text files in the repository; images go in the gist comments"), + "ci-gate": ("error", "CONTRIBUTING.md#build-and-test", "A job that runs for pull requests needs a file filter: paths on the pull_request trigger, or needs eligibility and an if on its outputs"), "doc-link": ("error", "CONTRIBUTING.md#documentation", "Relative link to a file that does not exist"), "commit-subject": ("error", "docs/dev/CONVENTIONS.md", "Commit subject is not Conventional Commits"), "pr-title": ("error", "docs/dev/CONVENTIONS.md", "Pull request title is not Conventional Commits"), @@ -41,6 +42,7 @@ TITLE = re.compile(r"\b(PPSA|CUSA)\d{5}\b") EXTENSION = re.compile(r"__attribute__|__declspec|#\s*pragma\s+(?!once\b)") LINK = re.compile(r"\]\(([^)\s]+)") SECTION = re.compile(r"^###\s*(.+?)\s*$", re.M) +KEY = re.compile(r"^( *)([A-Za-z0-9_-]+|'[^']*'|\"[^\"]*\"):(?:\s+(.*?))?\s*$") def git(*args, text=True): @@ -122,14 +124,14 @@ class Check: def duplicates(self, exports): if not exports: return - pattern = r"\bAPS5_VABI\s+(" + "|".join(exports) + r")\s*\(" + pattern = r"(^|[^[:alnum:]_])APS5_VABI[[:space:]]+(" + "|".join(exports) + r")[[:space:]]*\(" result = subprocess.run(["git", "grep", "-nE", pattern, self.head, "--", "core/libs/prx"], capture_output=True, encoding="utf-8", errors="replace") for line in result.stdout.splitlines(): - _, path, _, text = line.split(":", 3) + _, path, number, text = line.split(":", 3) name = EXPORT.search(text).group(1) - own, number = exports[name] - if library(path) != library(own) and not text.rstrip().endswith(";"): - self.report("duplicate-export", own, number, f"{name} is also in {library(path)}") + own, own_number = exports[name] + if library(path) != library(own) and function_body(self.show(path).splitlines(), int(number) - 1) is not None: + self.report("duplicate-export", own, own_number, f"{name} is also in {library(path)}") def build(self): for path, lines in self.files(lambda p: p.name == "CMakeLists.txt" or p.suffix == ".cmake"): @@ -143,7 +145,18 @@ class Check: if match and not match.group(1).startswith("3rdparty/"): self.report("system-dependency", ".gitmodules", number, match.group(1)) + def submodules(self): + result = subprocess.run(["git", "config", "--no-includes", "-z", "--blob", f"{self.head}:.gitmodules", "--list"], + capture_output=True, encoding="utf-8", errors="replace") + values = {key: value for key, _, value in (entry.partition("\n") for entry in result.stdout.split("\0") if entry)} + return {path for key, path in values.items() if key.endswith(".path") and values.get(f"{key[:-5]}.url")} + def repository(self): + gitlinks = {path for path, mode in self.modes.items() if mode == "160000" and path.startswith("3rdparty/")} + checked = gitlinks if ".gitmodules" in self.status else gitlinks & self.status.keys() + if checked: + for path in sorted(checked - self.submodules()): + self.report("system-dependency", path, 0, "submodule without a url in .gitmodules") for path, (added, deleted) in self.numstat.items(): if path.startswith("3rdparty/"): continue @@ -163,6 +176,20 @@ class Check: if resolved not in self.tree and resolved not in self.dirs: self.report("doc-link", path, number, target) + def workflows(self): + for path in sorted(self.status): + if self.status[path] == "D" or not re.fullmatch(r"\.github/workflows/[^/]+\.ya?ml", path): + continue + source = self.show(path).splitlines() + lines = [(number, KEY.match(re.sub(r"(^|\s)#.*", "", text))) for number, text in enumerate(source, 1)] + lines = [(number, len(match[1]), match[2].strip("'\""), match[3] or "") for number, match in lines if match] + triggers = [entry for entry in lines if entry[1] == 0 and entry[2] == "on"] + if not triggers or pull_request_filtered(lines, triggers[0]): + continue + for number, name, needs, condition in jobs(lines, source): + if name != "eligibility" and ("eligibility" not in needs or "needs.eligibility.outputs." not in condition): + self.report("ci-gate", path, number, name) + def commits(self): for line in git("log", "--no-merges", "--format=%h %s", f"{self.base}..{self.head}").splitlines(): sha, subject = line.split(" ", 1) @@ -221,6 +248,48 @@ def silent_stub_debt(text): return debt +def block(lines, start): + _, indent, _, _ = lines[start] + end = start + 1 + while end < len(lines) and lines[end][1] > indent: + end += 1 + return lines[start + 1:end] + + +def pull_request_filtered(lines, trigger): + _, _, _, value = trigger + if value: + return "pull_request" not in re.findall(r"[A-Za-z_]+", value) + triggers = block(lines, lines.index(trigger)) + for index, (_, indent, name, value) in enumerate(triggers): + if name == "pull_request" and indent == triggers[0][1]: + return any(entry[2] in ("paths", "paths-ignore") for entry in block(triggers, index)) + return True + + +def field(source, entry): + number, indent, _, value = entry + for line in (line for line in source[number:] if line.strip()): + if len(line) - len(line.lstrip(" ")) <= indent: + break + value += " " + line.strip() + return value + + +def jobs(lines, source): + top = [index for index, entry in enumerate(lines) if entry[1] == 0 and entry[2] == "jobs"] + if not top: + return [] + entries = block(lines, top[0]) + result = [] + for index, (number, indent, name, _) in enumerate(entries): + if indent != entries[0][1]: + continue + fields = {entry[2]: field(source, entry) for entry in block(entries, index) if entry[1] == indent + 2} + result.append((number, name, re.findall(r"[A-Za-z0-9_-]+", fields.get("needs", "")), fields.get("if", ""))) + return result + + def listed(debt, name, path): own = PurePosixPath(path).parts[:4] for text, targets in debt: @@ -303,6 +372,7 @@ if __name__ == "__main__": check.build() check.repository() check.docs() + check.workflows() check.commits() pr = json.load(open(args.event, encoding="utf-8")).get("pull_request") if args.event else None if pr: diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index 83311cb20..0fb47521a 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -4,7 +4,7 @@ import subprocess from pathlib import Path TEST_OUTPUT = r"tests[\\/]" -COMMAND = re.compile(r"^\s*(add_test|set_tests_properties)\s*\(", re.M | re.I) +COMMAND = re.compile(r"^\s*(add_test|set_tests_properties|subdirs)\s*\(", re.M | re.I) ARGUMENT = re.compile(r'"(?:\\.|[^"\\])*"|\[(?P=*)\[.*?\](?P=equals)\]|[^\s()]+', re.S) KNOWN_UNRUN = set() @@ -63,10 +63,19 @@ def commands(content): def registered(build): names = set() - for file in Path(build).rglob("CTestTestfile.cmake"): + pending = [Path(build).resolve()] + visited = set() + while pending: + directory = pending.pop().resolve() + file = directory / "CTestTestfile.cmake" + if directory in visited or not file.is_file(): + continue + visited.add(directory) tests, disabled = {}, {} for command, arguments in commands(file.read_text(encoding="utf-8", errors="replace")): - if command == "add_test" and arguments: + if command == "subdirs": + pending.extend(directory / argument for argument in arguments) + elif command == "add_test" and arguments: tests[arguments[0]] = arguments[1:] elif command == "set_tests_properties" and "PROPERTIES" in arguments: properties = arguments.index("PROPERTIES") diff --git a/tools/hw-oracle/hw_oracle.py b/tools/hw-oracle/hw_oracle.py index 16cc7e87a..46b59c202 100644 --- a/tools/hw-oracle/hw_oracle.py +++ b/tools/hw-oracle/hw_oracle.py @@ -1,5 +1,7 @@ import argparse +import hashlib import os +import shlex import shutil import struct import subprocess @@ -33,15 +35,23 @@ def tool(name): def oracle(): - binary = CACHE / "oracle" source = HERE / "oracle.c" - if binary.exists() and binary.stat().st_mtime >= source.stat().st_mtime: + compiler = shlex.split(os.environ.get("CC", "cc")) + if not compiler: + raise ValueError("CC must name a compiler command") + root = rocm_root() + if root is not None: + root = root.resolve() + configuration = repr((compiler, str(root))).encode("utf-8") + key = hashlib.sha256(source.read_bytes() + configuration).hexdigest() + directory = CACHE / key + binary = directory / "oracle" + if binary.is_file(): return binary - CACHE.mkdir(parents=True, exist_ok=True) - with tempfile.TemporaryDirectory(dir=CACHE) as tmp: + directory.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(dir=directory) as tmp: output = Path(tmp) / "oracle" - command = [os.environ.get("CC", "cc"), "-O1", str(source), "-o", str(output)] - root = rocm_root() + command = [*compiler, "-O1", str(source), "-o", str(output)] if root: lib = root / "lib" command += [f"-I{root / 'include'}", f"-L{lib}", f"-Wl,-rpath,{lib}"] @@ -50,6 +60,8 @@ def oracle(): return binary + + @cache def target(): return os.environ.get("HW_ORACLE_TARGET") or subprocess.run([oracle(), "--target"], capture_output=True, text=True, check=True).stdout.strip() diff --git a/tools/hw-oracle/test_hw_oracle.py b/tools/hw-oracle/test_hw_oracle.py index 606517706..7de671b40 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -1,7 +1,10 @@ import contextlib import io import os +import shlex +import shutil import subprocess +import sys import tempfile import unittest from pathlib import Path @@ -38,24 +41,26 @@ class OracleCacheTests(unittest.TestCase): with self.assertRaises(subprocess.CalledProcessError): hw_oracle.oracle() self.assertEqual(binary.read_bytes() if binary.exists() else None, previous) - self.assertEqual(list(cache.iterdir()), [binary] if previous is not None else []) + self.assertEqual([file for file in cache.rglob("*") if file.is_file()], [binary] if previous is not None else []) compiler.side_effect = succeed - self.assertEqual(hw_oracle.oracle(), binary) - self.assertEqual(binary.read_bytes(), b"complete executable") - self.assertEqual(hw_oracle.oracle(), binary) + built = hw_oracle.oracle() + self.assertEqual(built.read_bytes(), b"complete executable") + self.assertEqual(hw_oracle.oracle(), built) self.assertEqual(compiler.call_count, 2) - self.assertEqual(list(cache.iterdir()), [binary]) + self.assertEqual(set(file for file in cache.rglob("*") if file.is_file()), {built, binary} if previous is not None else {built}) + if previous is not None: + self.assertEqual(binary.read_bytes(), previous) def test_overlapping_builds_do_not_expose_partial_output(self): with tempfile.TemporaryDirectory() as tmp: cache = Path(tmp) / "cache" - binary = cache / "oracle" outputs = [] def compile(command, *, check): self.assertTrue(check) output = Path(command[command.index("-o") + 1]) outputs.append(output) + binary = output.parent.parent / "oracle" output.write_bytes(b"partial executable") if len(outputs) == 1: self.assertFalse(binary.exists()) @@ -64,11 +69,11 @@ class OracleCacheTests(unittest.TestCase): with patch.object(hw_oracle, "CACHE", cache), patch.object(hw_oracle, "rocm_root", return_value=None), \ patch.object(hw_oracle.subprocess, "run", side_effect=compile) as compiler: - self.assertEqual(hw_oracle.oracle(), binary) + binary = hw_oracle.oracle() self.assertEqual(binary.read_bytes(), b"complete executable") self.assertEqual(compiler.call_count, 2) self.assertNotEqual(outputs[0], outputs[1]) - self.assertEqual(list(cache.iterdir()), [binary]) + self.assertEqual([file for file in cache.rglob("*") if file.is_file()], [binary]) class FloatModeTests(unittest.TestCase): @@ -122,6 +127,108 @@ class FloatModeTests(unittest.TestCase): self.assertEqual(assemble.call_args.kwargs, MODES | {"lds": 4096}) +class OracleConfigurationTests(unittest.TestCase): + @unittest.skipUnless(sys.platform.startswith("linux"), "Linux compiler and shared-library fixture") + def test_cache_and_compiler_command_with_host_toolchain(self): + compiler = shutil.which("cc") + if compiler is None: + self.skipTest("C compiler not found") + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + runtime = root / "runtime" + (runtime / "lib").mkdir(parents=True) + stub = root / "runtime.c" + stub.write_text("void runtime_stub(void) {}\n", encoding="utf-8") + subprocess.run([compiler, "-shared", "-fPIC", str(stub), "-o", str(runtime / "lib/libhsa-runtime64.so")], check=True) + source = root / "source" + source.mkdir() + source_file = source / "oracle.c" + source_file.write_text('#include \nint main(void) { printf("%d", VALUE); }\n', encoding="utf-8") + os.utime(source_file, (0, 0)) + wrapper = root / "compiler wrapper" + wrapper.write_text("#!/bin/sh\nexec " + shlex.quote(compiler) + ' "$@"\n', encoding="utf-8") + wrapper.chmod(0o755) + with patch.object(hw_oracle, "HERE", source), patch.object(hw_oracle, "CACHE", root / "cache"), \ + patch.object(hw_oracle, "rocm_root", return_value=runtime): + with patch.dict(os.environ, {"CC": shlex.quote(str(wrapper)) + " -DVALUE=7"}): + first = hw_oracle.oracle() + self.assertEqual(subprocess.check_output([first]), b"7") + self.assertEqual(hw_oracle.oracle(), first) + with patch.dict(os.environ, {"CC": shlex.quote(str(wrapper)) + " -DVALUE=8"}): + second = hw_oracle.oracle() + self.assertNotEqual(second, first) + self.assertEqual(subprocess.check_output([second]), b"8") + source_file.write_text('#include \nint main(void) { printf("%d", VALUE + 1); }\n', encoding="utf-8") + os.utime(source_file, (0, 0)) + third = hw_oracle.oracle() + self.assertNotEqual(third, second) + self.assertEqual(subprocess.check_output([third]), b"9") + + def test_compiler_command_supports_wrapper_and_flags(self): + with tempfile.TemporaryDirectory() as tmp: + def compile(command, *, check): + self.assertEqual(command[:3], ["ccache", "cc", "-fno-omit-frame-pointer"]) + Path(command[command.index("-o") + 1]).write_bytes(b"executable") + + with patch.object(hw_oracle, "CACHE", Path(tmp)), patch.object(hw_oracle, "rocm_root", return_value=None), \ + patch.dict(os.environ, {"CC": "ccache cc -fno-omit-frame-pointer"}), \ + patch.object(hw_oracle.subprocess, "run", side_effect=compile): + self.assertEqual(hw_oracle.oracle().read_bytes(), b"executable") + + def test_source_change_with_preserved_timestamp_invalidates_cache(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + source = root / "source" + source.mkdir() + source_file = source / "oracle.c" + source_file.write_text("first", encoding="utf-8") + os.utime(source_file, (0, 0)) + + def compile(command, *, check): + Path(command[command.index("-o") + 1]).write_bytes(source_file.read_bytes()) + + with patch.object(hw_oracle, "HERE", source), patch.object(hw_oracle, "CACHE", root / "cache"), \ + patch.object(hw_oracle, "rocm_root", return_value=None), \ + patch.object(hw_oracle.subprocess, "run", side_effect=compile) as compiler: + self.assertEqual(hw_oracle.oracle().read_bytes(), b"first") + source_file.write_text("second", encoding="utf-8") + os.utime(source_file, (0, 0)) + self.assertEqual(hw_oracle.oracle().read_bytes(), b"second") + self.assertEqual(compiler.call_count, 2) + + def test_compiler_change_invalidates_cache(self): + with tempfile.TemporaryDirectory() as tmp: + def compile(command, *, check): + Path(command[command.index("-o") + 1]).write_bytes(command[0].encode()) + + with patch.object(hw_oracle, "CACHE", Path(tmp)), patch.object(hw_oracle, "rocm_root", return_value=None), \ + patch.object(hw_oracle.subprocess, "run", side_effect=compile) as compiler: + with patch.dict(os.environ, {"CC": "cc"}): + self.assertEqual(hw_oracle.oracle().read_bytes(), b"cc") + with patch.dict(os.environ, {"CC": "clang"}): + self.assertEqual(hw_oracle.oracle().read_bytes(), b"clang") + self.assertEqual(compiler.call_count, 2) + + def test_runtime_path_change_invalidates_cache(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + + def compile(command, *, check): + library_path = next(option for option in command if option.startswith("-L")) + Path(command[command.index("-o") + 1]).write_bytes(library_path.encode()) + + with patch.object(hw_oracle, "CACHE", root / "cache"), \ + patch.object(hw_oracle.subprocess, "run", side_effect=compile) as compiler: + with patch.object(hw_oracle, "rocm_root", return_value=root / "one"): + first = hw_oracle.oracle() + self.assertEqual(first.read_bytes(), ("-L" + str((root / "one").resolve() / "lib")).encode()) + with patch.object(hw_oracle, "rocm_root", return_value=root / "two"): + second = hw_oracle.oracle() + self.assertNotEqual(second, first) + self.assertEqual(second.read_bytes(), ("-L" + str((root / "two").resolve() / "lib")).encode()) + self.assertEqual(compiler.call_count, 2) + + class GroupSegmentTests(unittest.TestCase): def test_wave64_dispatches_at_most_512_rows(self): rows = [(index, 0, 0, 0) for index in range(1025)] diff --git a/tools/import_audit.py b/tools/import_audit.py index a6c0f6b0d..276ce39f8 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -14,7 +14,6 @@ import progress ROOT = Path(__file__).resolve().parent.parent PRX = ROOT / "core" / "libs" / "prx" NID = re.compile(r"[A-Za-z0-9+\-]{11}") -EXPORT_ALIAS = re.compile(r'APS5_EXPORT\("([A-Za-z0-9+\-]{11})",\s*(\w+)\)') NID_POSTFIX = "_nid_postfix" CLASSES = ("implemented", "stub", "absent", "module") PE_DIRECTORY_OFFSET = {0x20b: 112, 0x10b: 96} @@ -208,7 +207,7 @@ def source_stubs(source): for library in sorted(item for item in Path(source).iterdir() if item.is_dir()): aliases = {} for file in library.rglob("*.cpp"): - for nid, name in EXPORT_ALIAS.findall(file.read_text(errors="ignore")): + for nid, name in nid_names.source_aliases(file.read_text(errors="ignore")): aliases[name] = nid group = progress.scan_library(library) done = {nid_of(name, aliases) for name in group["done_names"]} diff --git a/tools/nid_names.py b/tools/nid_names.py index 8dd9382e7..a633f94dd 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -81,7 +81,7 @@ def source_aliases(text): continue if re.fullmatch(r'"[A-Za-z0-9+\-]{11}"', literal) is None: continue - if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name) and "unknown" in name.lower(): + if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name): yield literal[1:-1], name @@ -97,7 +97,8 @@ def collect_unknowns(): for path in source_files(PRX): text = path.read_text(encoding="utf-8") for nid, name in source_aliases(text): - found.setdefault(nid, "%s:%s" % (path.relative_to(ROOT).as_posix(), name)) + if "unknown" in name.lower(): + found.setdefault(nid, "%s:%s" % (path.relative_to(ROOT).as_posix(), name)) return found diff --git a/tools/package_release.py b/tools/package_release.py index 476314e0e..52f0b3ffc 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -18,7 +18,7 @@ def package(platform, build, output, version): if missing: raise RuntimeError(f"Missing patched libraries: {', '.join(sorted(missing))}") executable = "relinker.exe" if platform == "windows" else "relinker" - files = list(libraries) + files = [library for library in libraries if library.name in expected] if platform == "windows": runtime = Path("C:/winlibs/mingw64/bin") files.extend(runtime / name for name in ("libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll")) diff --git a/tools/pad_input.py b/tools/pad_input.py index 8e1bed63d..e68f3185b 100644 --- a/tools/pad_input.py +++ b/tools/pad_input.py @@ -19,19 +19,27 @@ def main(): parser.add_argument("path", type=Path) parser.add_argument("button", choices=BUTTONS) parser.add_argument("--hold-ms", type=int, default=300) + parser.add_argument("--left-stick", type=int, nargs=2, metavar=("X", "Y")) + parser.add_argument("--right-stick", type=int, nargs=2, metavar=("X", "Y")) args = parser.parse_args() if not 1 <= args.hold_ms <= 5000: parser.error("--hold-ms must be between 1 and 5000") + sticks = "" + if args.left_stick is not None or args.right_stick is not None: + axes = (args.left_stick or [128, 128]) + (args.right_stick or [128, 128]) + if not all(0 <= axis <= 255 for axis in axes): + parser.error("stick axes must be between 0 and 255") + sticks = "".join(f" {axis}" for axis in axes) temporary = None try: with tempfile.NamedTemporaryFile(mode="w", dir=args.path.parent, delete=False) as output: temporary = Path(output.name) - output.write(f"{time.monotonic_ns()} {BUTTONS[args.button]} {args.hold_ms}\n") + output.write(f"{time.monotonic_ns()} {BUTTONS[args.button]} {args.hold_ms}{sticks}\n") os.replace(temporary, args.path) finally: if temporary is not None: temporary.unlink(missing_ok=True) - print(f"Queued {args.button} for {args.hold_ms} ms in {args.path}") + print(f"Queued {args.button}{' with sticks' + sticks if sticks else ''} for {args.hold_ms} ms in {args.path}") if __name__ == "__main__": diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index 279006dba..ee4e97a6a 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -3,7 +3,7 @@ import json import os import re import subprocess -from itertools import chain, combinations +from itertools import combinations from pathlib import Path from urllib.parse import urlencode @@ -37,9 +37,9 @@ def depends_on(body): def open_prs(base): query = urlencode({"state": "open", "base": base, "per_page": 100}) - out = gh("api", "--paginate", "--slurp", f"repos/{REPO}/pulls?{query}") + out = gh("api", "--paginate", f"repos/{REPO}/pulls?{query}") prs = {} - for pr in chain.from_iterable(json.loads(out)): + for pr in json.loads(out): prs[pr["number"]] = { "ref": f"refs/pr/{pr['number']}", "sha": pr["head"]["sha"], @@ -70,9 +70,18 @@ def commit(tree): def exports(diff): found = set() - for line in diff.splitlines(): + lines = diff.splitlines() + for i, line in enumerate(lines): m = EXPORT.match(line) - if m and not m.group(1).endswith("_nid_no_patch") and not line.rstrip().endswith(";"): + if not m or m.group(1).endswith("_nid_no_patch"): + continue + text = line[1:] + for nxt in lines[i + 1:]: + if ";" in text or "{" in text or not nxt.startswith("+") or nxt.startswith("+++"): + break + text += nxt[1:] + semi, brace = text.find(";"), text.find("{") + if semi < 0 or 0 <= brace < semi: found.add(m.group(1)) return found @@ -115,14 +124,26 @@ def conflicts(a, b): return {path: [] if path in a["added"] else clashes(a, b, path) for path in files} +def dependencies(prs, number): + found, pending = set(), [number] + while pending: + for n in prs[pending.pop()]["depends"]: + if n in prs and n not in found: + found.add(n) + pending.append(n) + return found + + def overlaps(prs, a, b): pa, pb = prs[a], prs[b] - if b in pa["depends"] or a in pb["depends"]: + da, db = dependencies(prs, a), dependencies(prs, b) + if b in da or a in db: return None - found = {"added": pa["added"] & pb["added"]} + shared = [prs[n] for n in da & db] + found = {"added": (pa["added"] & pb["added"]).difference(*(pr["added"] for pr in shared))} if pa["author"] != pb["author"]: found["conflicts"] = conflicts(pa, pb) - found["exports"] = pa["exports"] & pb["exports"] + found["exports"] = (pa["exports"] & pb["exports"]).difference(*(pr["exports"] for pr in shared)) return found if any(found.values()) else None diff --git a/tools/progress.py b/tools/progress.py index e3b281bb8..d2222e720 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -258,7 +258,7 @@ def squarify(values, x, y, w, h): def cells(count, x, y, w, h): - if not count: + if not count or w <= 0 or h <= 0: return [] rows = max(1, min(count, round((count * h / w) ** 0.5))) result = [] diff --git a/tools/tests/test_check_conventions_ci_gate.py b/tools/tests/test_check_conventions_ci_gate.py new file mode 100644 index 000000000..33f7d9450 --- /dev/null +++ b/tools/tests/test_check_conventions_ci_gate.py @@ -0,0 +1,103 @@ +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +CHECKER = Path(__file__).resolve().parents[1] / "check_conventions.py" +WORKFLOWS = Path(__file__).resolve().parents[2] / ".github/workflows" + +GATE = """ eligibility: + runs-on: ubuntu-latest + outputs: + tests: ${{ steps.gate.outputs.tests }} + steps: + - run: echo +""" + + +def workflow(trigger, job): + return f"name: Test\n\non:\n{trigger}\njobs:\n{GATE}{job}" + + +PULL_REQUEST = " pull_request:\n branches: [main]\n" + + +class CiGateTests(unittest.TestCase): + def check(self, files): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + + def git(*args): + return subprocess.run( + ["git", "-c", f"safe.directory={root}", "-c", "user.name=Conventions Test", + "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], + cwd=root, capture_output=True, text=True, check=True, + ) + + git("init", "-q") + (root / "README.md").write_text("readme\n", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "docs: add readme") + for name, text in files.items(): + path = root / ".github/workflows" / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + git("add", ".") + git("commit", "-qm", "ci: add workflow") + env = {key: value for key, value in os.environ.items() + if key not in ("GITHUB_EVENT_PATH", "GITHUB_STEP_SUMMARY", "GITHUB_ACTIONS")} + env.update(GIT_CONFIG_COUNT="1", GIT_CONFIG_KEY_0="safe.directory", GIT_CONFIG_VALUE_0=str(root)) + return subprocess.run( + [sys.executable, "-I", str(CHECKER), "--base", "HEAD~1", "--head", "HEAD"], + cwd=root, capture_output=True, text=True, env=env, timeout=20, + ) + + def assertGated(self, text): + result = self.check({"build.yml": text}) + self.assertNotIn("ci-gate", result.stdout, result.stdout + result.stderr) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def assertUngated(self, text, job): + result = self.check({"build.yml": text}) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("[ci-gate]", result.stdout) + self.assertIn(job, result.stdout) + + def test_job_without_gate_fails(self): + self.assertUngated(workflow(PULL_REQUEST, " tests:\n runs-on: ubuntu-latest\n steps:\n - run: make\n"), "tests") + + def test_job_needing_the_gate_without_if_fails(self): + self.assertUngated(workflow(PULL_REQUEST, " tests:\n needs: eligibility\n runs-on: ubuntu-latest\n"), "tests") + + def test_if_on_another_condition_fails(self): + self.assertUngated(workflow(PULL_REQUEST, " tests:\n needs: eligibility\n if: github.event_name == 'pull_request'\n"), "tests") + + def test_gated_job_passes(self): + self.assertGated(workflow(PULL_REQUEST, " tests:\n needs: eligibility\n if: ${{ needs.eligibility.outputs.tests == 'true' }}\n")) + + def test_gate_written_over_several_lines_passes(self): + self.assertGated(workflow(PULL_REQUEST, " tests:\n needs:\n - other\n - eligibility\n if: >-\n github.event_name == 'push' ||\n needs.eligibility.outputs.tests == 'true'\n")) + + def test_paths_filter_passes(self): + self.assertGated(workflow(" pull_request:\n paths:\n - 'core/**'\n", " tests:\n runs-on: ubuntu-latest\n")) + + def test_paths_of_another_trigger_do_not_count(self): + self.assertUngated(workflow(" push:\n paths:\n - 'core/**'\n pull_request:\n branches: [main]\n", " tests:\n runs-on: ubuntu-latest\n"), "tests") + + def test_inline_trigger_list_fails(self): + self.assertUngated("name: Test\non: [push, pull_request]\njobs:\n tests:\n runs-on: ubuntu-latest\n", "tests") + + def test_other_triggers_pass(self): + self.assertGated(workflow(" pull_request_target:\n types: [opened]\n push:\n branches: [main]\n", " tests:\n runs-on: ubuntu-latest\n")) + + def test_repository_workflows_pass(self): + files = {path.name: path.read_text(encoding="utf-8") for path in WORKFLOWS.glob("*.y*ml")} + self.assertIn("build.yml", files) + result = self.check(files) + self.assertNotIn("ci-gate", result.stdout, result.stdout + result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index 89df82af5..44616324e 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -295,6 +295,14 @@ class RepositoryChecksTests(unittest.TestCase): self.git("add", ".") self.git("commit", "-qm", "test: add convention fixture") + def add_thirdparty_submodule(self, gitmodules=None): + head = self.git("rev-parse", "HEAD") + if gitmodules is not None: + (self.root / ".gitmodules").write_text(gitmodules, encoding="utf-8") + self.git("add", ".gitmodules") + self.git("update-index", "--add", "--cacheinfo", "160000", head, "3rdparty/vendor") + self.git("commit", "-qm", "build: add submodule") + def test_quoted_utf8_paths_are_checked(self): name = "docs/dev/測試guide.md" if os.name == "nt" else "docs/dev/測試\tguide.md" path = self.root / name @@ -337,13 +345,30 @@ class RepositoryChecksTests(unittest.TestCase): self.assertIn(("system-dependency", "vendor", 0, "submodule outside 3rdparty/"), check.findings) def test_thirdparty_submodule_remains_allowed(self): - head = self.git("rev-parse", "HEAD") - self.git("update-index", "--add", "--cacheinfo", "160000", head, "3rdparty/vendor") - self.git("commit", "-qm", "build: add allowed submodule") + self.add_thirdparty_submodule('[submodule "vendor"]\n\tpath = 3rdparty/vendor\n' + "\turl = https://example.com/vendor.git\n") check = Check("HEAD~1", "HEAD") check.repository() self.assertEqual(check.findings, []) + def test_thirdparty_submodule_without_gitmodules_entry_is_reported(self): + self.add_thirdparty_submodule() + check = Check("HEAD~1", "HEAD") + check.repository() + self.assertEqual(check.findings, + [("system-dependency", "3rdparty/vendor", 0, "submodule without a url in .gitmodules")]) + + def test_dropping_a_submodule_url_is_reported(self): + self.add_thirdparty_submodule('[submodule "vendor"]\n\tpath = 3rdparty/vendor\n' + "\turl = https://example.com/vendor.git\n") + (self.root / ".gitmodules").write_text('[submodule "vendor"]\n\tpath = 3rdparty/vendor\n', encoding="utf-8") + self.git("add", ".gitmodules") + self.git("commit", "-qm", "build: drop submodule url") + check = Check("HEAD~1", "HEAD") + check.repository() + self.assertEqual(check.findings, + [("system-dependency", "3rdparty/vendor", 0, "submodule without a url in .gitmodules")]) + def test_relative_link_url_decoding(self): self.assertEqual(links("docs/dev/index.md", "[guide](./space%20name.md#details)"), [("./space%20name.md", "docs/dev/space name.md")]) diff --git a/tools/tests/test_check_conventions_wrapped_declarations.py b/tools/tests/test_check_conventions_wrapped_declarations.py new file mode 100644 index 000000000..57df9623e --- /dev/null +++ b/tools/tests/test_check_conventions_wrapped_declarations.py @@ -0,0 +1,57 @@ +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +CHECKER = Path(__file__).resolve().parents[1] / "check_conventions.py" +DEFINITION = "int APS5_VABI sceFooBar(int a, int b) { return a + b; }\n" + + +class WrappedDeclarationTests(unittest.TestCase): + def check(self, first_file, first_text): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + + def git(*args): + return subprocess.run( + ["git", "-c", f"safe.directory={root}", "-c", "user.name=Conventions Test", + "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], + cwd=root, capture_output=True, text=True, check=True, + ) + + git("init", "-q") + first = root / "core/libs/prx/libSceA" / first_file + first.parent.mkdir(parents=True) + first.write_text(first_text, encoding="utf-8") + other = root / "core/libs/prx/libSceA/Other.cpp" + other.write_text("int APS5_VABI sceOther() { return 1; }\n", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "feat: add library a") + second = root / "core/libs/prx/libSceB/Export.cpp" + second.parent.mkdir(parents=True) + second.write_text(DEFINITION, encoding="utf-8") + git("add", ".") + git("commit", "-qm", "feat: add library b") + env = {key: value for key, value in os.environ.items() + if key not in ("GITHUB_EVENT_PATH", "GITHUB_STEP_SUMMARY", "GITHUB_ACTIONS")} + env.update(GIT_CONFIG_COUNT="1", GIT_CONFIG_KEY_0="safe.directory", GIT_CONFIG_VALUE_0=str(root)) + return subprocess.run( + [sys.executable, "-I", str(CHECKER), "--base", "HEAD~1", "--head", "HEAD"], + cwd=root, capture_output=True, text=True, env=env, timeout=20, + ) + + def test_wrapped_declaration_is_not_a_duplicate(self): + result = self.check("Export.h", "int APS5_VABI sceFooBar(int a,\n int b);\n") + self.assertNotIn("duplicate-export", result.stdout, result.stdout + result.stderr) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_wrapped_definition_is_a_duplicate(self): + result = self.check("Export.cpp", "int APS5_VABI sceFooBar(int a,\n int b) {\n return 0;\n}\n") + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("[duplicate-export]", result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index 02ab77f59..d8ba15c75 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -13,6 +13,7 @@ LIMITATIONS = [ "skipped, the device lacks fragmentShaderPixelInterlock", "skipped, the device lacks geometryShader", "skipped, the device lacks shaderSubgroupClock", + "skipped, the device lacks VK_EXT_shader_stencil_export", "skipped, the device does not import guest memory", "skipped, the device imports no host memory", "skipped, the device flushes f64 subnormals without DenormPreserve", diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 98084bf52..5534297e0 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -191,6 +191,48 @@ class ImportAuditTests(unittest.TestCase): records, _ = import_audit.audit([(nid, "libB.prx")], exports, stubs, {"libA.prx", "libB.prx"}, set()) self.assertEqual(records[0]["class"], "implemented") + def test_formatted_export_aliases_remain_stubs(self): + work = self.work + nid = "AAAAAAAAAAA" + (work.libs / "libSceAlias.prx").write_bytes(exporting(nid)) + registry = work.root / "game.json" + result = work.root / "audit.json" + write_registry(registry, [(nid, "libSceAlias.prx")]) + aliases = [ + 'APS5_EXPORT (\n "AAAAAAAAAAA",\n NAME\n);\n', + 'APS5_EXPORT(/* nid */ "AAAAAAAAAAA", /* name */ NAME);\n', + 'APS5_EX\\\nPORT("AAAAAAAAAAA", NAME);\n', + ] + for name in ("sceAliasUnknown", "sceAliasedCall"): + for alias in aliases: + with self.subTest(name=name, alias=alias): + write_source(work.source, "libSceAlias", alias.replace("NAME", name) + STUB_SOURCE.format(name=name)) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (0, "")) + record, = json.loads(result.read_text())["imports"] + self.assertEqual((record["class"], record["name"]), ("stub", name)) + + def test_comments_and_literals_do_not_override_export_aliases(self): + work = self.work + nid, name = "AAAAAAAAAAA", "sceAliasUnknown" + (work.libs / "libSceAlias.prx").write_bytes(exporting(nid)) + registry = work.root / "game.json" + result = work.root / "audit.json" + write_registry(registry, [(nid, "libSceAlias.prx")]) + source = 'APS5_EXPORT("AAAAAAAAAAA", sceAliasUnknown);\n' + STUB_SOURCE.format(name=name) + decoys = [ + '// APS5_EXPORT("BBBBBBBBBBB", sceAliasUnknown);\n', + '/* APS5_EXPORT("BBBBBBBBBBB", sceAliasUnknown); */\n', + 'auto example = R"text(APS5_EXPORT("BBBBBBBBBBB", sceAliasUnknown);)text";\n', + ] + for decoy in decoys: + with self.subTest(decoy=decoy): + write_source(work.source, "libSceAlias", source + decoy) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (0, "")) + record, = json.loads(result.read_text())["imports"] + self.assertEqual((record["class"], record["name"]), ("stub", name)) + @unittest.skipUnless((import_audit.ROOT / ".git").exists(), "needs a git checkout") def test_json_is_stamped_with_the_source_commit(self): work = self.work diff --git a/tools/tests/test_nid_names.py b/tools/tests/test_nid_names.py index 0ac509da5..66b18f5df 100644 --- a/tools/tests/test_nid_names.py +++ b/tools/tests/test_nid_names.py @@ -161,6 +161,11 @@ int APS5_VABI sceReal() { return 0; } self.write_source('APS5_EXPORT (\n "AAAAAAAAAAA" ,\n exampleUnknown00\n );\n') self.assertEqual(nid_names.collect_unknowns(), {"AAAAAAAAAAA": "core/libs/prx/libSceExample/Export.cpp:exampleUnknown00"}) + def test_known_aliases_are_not_unknown_exports(self): + self.write_source('APS5_EXPORT("AAAAAAAAAAA", sceKnownCall);\n' + 'APS5_EXPORT("BBBBBBBBBBB", exampleUnknown00);\n') + self.assertEqual(nid_names.collect_unknowns(), {"BBBBBBBBBBB": "core/libs/prx/libSceExample/Export.cpp:exampleUnknown00"}) + def test_windows_source_paths_use_portable_separators(self): self.write_source('APS5_EXPORT("AAAAAAAAAAA", exampleUnknown00);\n') relative = PureWindowsPath("core/libs/prx/libSceExample/Export.cpp") diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index 2724ef6e4..a969a1692 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -56,6 +56,18 @@ class ReleasePackageTests(unittest.TestCase): with zipfile.ZipFile(self.output / "prx-linux-v1.0.zip") as archive: self.assertEqual(archive.namelist(), ["libs/libc.prx"]) + def test_removed_libraries_are_not_packaged_from_a_reused_build(self): + stale = self.library.with_name("libRemoved.prx") + stale.write_bytes(b"obsolete library") + package_release.package("linux", self.build, self.output, "v1.0") + with zipfile.ZipFile(self.output / "prx-linux-v1.0.zip") as archive: + self.assertEqual(archive.namelist(), ["libs/libc.prx"]) + self.assertEqual(archive.read("libs/libc.prx"), b"library bytes") + with tarfile.open(self.output / "prx-linux-v1.0.tar.gz") as archive: + self.assertEqual(archive.getnames(), ["libs/libc.prx"]) + self.assertEqual(archive.extractfile("libs/libc.prx").read(), b"library bytes") + self.assertEqual(stale.read_bytes(), b"obsolete library") + def test_tar_and_zip_include_the_same_symlink_contents(self): target = self.library.with_name("libc.target") self.library.rename(target) diff --git a/tools/tests/test_pr_overlap.py b/tools/tests/test_pr_overlap.py index f37570b78..31488bb0c 100644 --- a/tools/tests/test_pr_overlap.py +++ b/tools/tests/test_pr_overlap.py @@ -19,8 +19,7 @@ class OpenPullRequestsTests(unittest.TestCase): def response(*args): if args[0] == "api": - return json.dumps([pull_requests[start:start + 100] - for start in range(0, len(pull_requests), 100)]) + return json.dumps(pull_requests) limit = int(args[args.index("-L") + 1]) return json.dumps([ {"number": pr["number"], "author": pr["user"], "headRefOid": pr["head"]["sha"], @@ -38,18 +37,18 @@ class OpenPullRequestsTests(unittest.TestCase): def test_base_branch_is_encoded_in_the_paginated_request(self): with patch.object(pr_overlap, "REPO", "owner/project", create=True), \ - patch.object(pr_overlap, "gh", return_value="[[]]") as gh: + patch.object(pr_overlap, "gh", return_value="[]") as gh: self.assertEqual(pr_overlap.open_prs("topic/one&two"), {}) gh.assert_called_once_with( - "api", "--paginate", "--slurp", + "api", "--paginate", "repos/owner/project/pulls?state=open&base=topic%2Fone%26two&per_page=100", ) def test_single_page_preserves_author_head_and_dependencies(self): - pages = [[{"number": 7, "user": {"login": "contributor"}, "head": {"sha": "head-sha"}, - "body": "Depends on: none\nUnrelated issue #42"}]] + pull_requests = [{"number": 7, "user": {"login": "contributor"}, "head": {"sha": "head-sha"}, + "body": "Depends on: none\nUnrelated issue #42"}] with patch.object(pr_overlap, "REPO", "owner/project", create=True), \ - patch.object(pr_overlap, "gh", return_value=json.dumps(pages)): + patch.object(pr_overlap, "gh", return_value=json.dumps(pull_requests)): found = pr_overlap.open_prs("main") self.assertEqual(found, { 7: {"ref": "refs/pr/7", "sha": "head-sha", "author": "contributor", "depends": set()}, diff --git a/tools/tests/test_pr_overlap_dependencies.py b/tools/tests/test_pr_overlap_dependencies.py index f93bb828c..bcff60153 100644 --- a/tools/tests/test_pr_overlap_dependencies.py +++ b/tools/tests/test_pr_overlap_dependencies.py @@ -12,10 +12,27 @@ def pull_requests(body): response = [{"number": number, "user": {"login": "contributor"}, "head": {"sha": str(number) * 40}, "body": body if number == 1 else "Depends on: none"} for number in (1, 2)] with patch.object(pr_overlap, "REPO", "owner/repo", create=True), \ - patch.object(pr_overlap, "gh", return_value=json.dumps([response])): + patch.object(pr_overlap, "gh", return_value=json.dumps(response)): return pr_overlap.open_prs("main") +STACK = { + 1: (set(), {"one.cpp"}, {"sceFooInit"}), + 2: ({1, 999}, {"one.cpp", "two.cpp"}, {"sceFooInit"}), + 3: ({2}, {"one.cpp", "two.cpp", "three.cpp"}, {"sceFooInit"}), + 4: ({1}, {"one.cpp", "four.cpp"}, {"sceFooInit", "sceFooTerm"}), + 5: ({1}, {"one.cpp", "four.cpp"}, {"sceFooInit", "sceFooTerm"}), + 6: (set(), {"one.cpp"}, {"sceFooInit"}), + 7: ({8}, {"one.cpp", "seven.cpp"}, {"sceFooInit"}), + 8: ({7, 1}, {"one.cpp", "seven.cpp"}, {"sceFooInit"}), +} + + +def stacked_pull_requests(): + return {number: {"author": f"author{number}", "depends": depends, "added": added, "exports": exports, "tree": None} + for number, (depends, added, exports) in STACK.items()} + + class DependencyTests(unittest.TestCase): def test_documented_form_without_colon(self): for body in ("Depends on #2.", "Depends on #2, #3.", "Depends on\t#2 and #3.", @@ -95,5 +112,30 @@ class DependencyTests(unittest.TestCase): self.assertEqual(pr_overlap.overlaps(prs, 1, 2), {"added": {"shared.cpp"}}) +class StackedDependencyTests(unittest.TestCase): + def test_transitive_dependency_suppresses_overlap(self): + prs = stacked_pull_requests() + self.assertIsNone(pr_overlap.overlaps(prs, 1, 3)) + self.assertIsNone(pr_overlap.overlaps(prs, 3, 1)) + + def test_changes_inherited_from_a_shared_dependency_are_not_overlap(self): + prs = stacked_pull_requests() + self.assertIsNone(pr_overlap.overlaps(prs, 3, 4)) + self.assertIsNone(pr_overlap.overlaps(prs, 2, 4)) + + def test_overlap_beyond_a_shared_dependency_is_kept(self): + self.assertEqual(pr_overlap.overlaps(stacked_pull_requests(), 4, 5), + {"added": {"four.cpp"}, "conflicts": {}, "exports": {"sceFooTerm"}}) + + def test_independent_pull_request_keeps_inherited_overlap(self): + self.assertEqual(pr_overlap.overlaps(stacked_pull_requests(), 3, 6), + {"added": {"one.cpp"}, "conflicts": {}, "exports": {"sceFooInit"}}) + + def test_dependency_cycle_terminates(self): + prs = stacked_pull_requests() + self.assertIsNone(pr_overlap.overlaps(prs, 7, 8)) + self.assertIsNone(pr_overlap.overlaps(prs, 8, 2)) + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_pr_overlap_wrapped_exports.py b/tools/tests/test_pr_overlap_wrapped_exports.py new file mode 100644 index 000000000..96dbd2232 --- /dev/null +++ b/tools/tests/test_pr_overlap_wrapped_exports.py @@ -0,0 +1,28 @@ +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import pr_overlap + +DECL = "+int APS5_VABI sceFooBar(int a,\n+ int b);\n" + + +class WrappedExportTests(unittest.TestCase): + def test_wrapped_declaration_is_not_export(self): + self.assertEqual(pr_overlap.exports(DECL), set()) + + def test_wrapped_definition_is_export(self): + diff = "+int APS5_VABI sceFooBar(int a,\n+ int b) {\n+ return a + b;\n+}\n" + self.assertEqual(pr_overlap.exports(diff), {"sceFooBar"}) + + def test_declaration_then_definition_returns_only_definition(self): + diff = DECL + "+int APS5_VABI sceBaz(int a) { return a; }\n" + self.assertEqual(pr_overlap.exports(diff), {"sceBaz"}) + + def test_declaration_at_end_of_diff(self): + self.assertEqual(pr_overlap.exports("+int APS5_VABI sceFooBar(int a, int b);"), set()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_progress_render_sliver.py b/tools/tests/test_progress_render_sliver.py new file mode 100644 index 000000000..4fa8b2194 --- /dev/null +++ b/tools/tests/test_progress_render_sliver.py @@ -0,0 +1,24 @@ +import importlib.util +import unittest +from pathlib import Path + +SPEC = importlib.util.spec_from_file_location("progress", Path(__file__).resolve().parents[1] / "progress.py") +progress = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(progress) + + +class ProgressRenderSliverTests(unittest.TestCase): + def test_render_with_sliver_group(self): + libraries = {"done": 301, "total": 301, "percent": 100, "groups": [ + {"name": "libA", "label": "libA", "done": 300, "todo": 0}, + {"name": "libB", "label": "libB", "done": 1, "todo": 0}]} + shaders = {"done": 0, "total": 0, "percent": 0, "groups": []} + self.assertIn("