diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7d57dcc059..65302caba0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,6 +17,11 @@ on: required: false default: true type: boolean + full: + description: Run the full Linux and Windows builds (off runs only the relinker builds and tests on Linux, Windows and macOS) + required: false + default: true + type: boolean permissions: contents: read @@ -47,29 +52,74 @@ jobs: register-context: ${{ steps.gate.outputs.register-context }} write-watch: ${{ steps.gate.outputs.write-watch }} full: ${{ steps.gate.outputs.full }} + relinker-hosts: ${{ steps.gate.outputs.relinker-hosts }} steps: - uses: actions/github-script@v8 id: gate env: PR_NUMBER: ${{ inputs.pr }} + LINUX_BUILD: ${{ inputs.linux }} + FULL_BUILD: ${{ inputs.full }} with: script: | + const { owner, repo } = context.repo; + const workflow = '.github/workflows/build.yml'; + const libc = 'core/libs/prx/libc/'; + const select = (paths, all) => { + const touches = (...prefixes) => all || + paths.some((path) => prefixes.some((prefix) => prefix.endsWith('/') ? path.startsWith(prefix) : path === prefix)); + return { + 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 + ), + registerContext: touches('core/libs/tests/register_context/', 'core/libs/tests/RegisterContext.cpp', libc, workflow), + writeWatch: touches('core/libs/tests/write_watch/', 'core/libs/tests/WriteWatch.cpp', libc, workflow), + }; + }; + const relinkerHosts = (full, linux) => JSON.stringify([ + ...(full && linux ? [] : [{ os: 'ubuntu-latest', compiler: 'g++' }]), + ...(full ? [] : [{ os: 'windows-latest', compiler: 'g++' }]), + { os: 'macos-latest', compiler: 'clang++' }, + ]); if (context.eventName === 'push') { if (context.ref !== 'refs/heads/main') { throw new Error('Automatic full builds only run on main'); } + let paths = []; + let all = true; + if (!/^0+$/.test(context.payload.before)) { + try { + const { data } = await github.rest.repos.compareCommitsWithBasehead({ + owner, repo, basehead: `${context.payload.before}...${context.sha}` + }); + const files = data.files ?? []; + all = files.length >= 300; + paths = files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean)); + } catch (error) { + core.warning(`Could not list the files of this push, so every test job runs: ${error.message}`); + } + } + const run = select(paths, all); core.setOutput('sha', context.sha); core.setOutput('pr', ''); core.setOutput('head', ''); - core.setOutput('relinker', true); - core.setOutput('register-context', true); - core.setOutput('write-watch', true); + core.setOutput('relinker', run.relinker); + core.setOutput('register-context', run.registerContext); + core.setOutput('write-watch', run.writeWatch); core.setOutput('full', true); - await core.summary.addRaw(`Building main at ${context.sha}: all builds and tests enabled`).write(); + core.setOutput('relinker-hosts', relinkerHosts(true, true)); + await core.summary.addRaw( + `Building main at ${context.sha}: full builds enabled, ` + + `macOS relinker ${run.relinker ? 'enabled' : 'skipped'}, ` + + `register context tests ${run.registerContext ? 'enabled' : 'skipped'}, write watch tests ${run.writeWatch ? 'enabled' : 'skipped'}` + ).write(); return; } - const { owner, repo } = context.repo; const manual = context.eventName === 'workflow_dispatch'; + const linux = process.env.LINUX_BUILD !== 'false'; + const full = manual && process.env.FULL_BUILD !== 'false'; if (manual) { for (const username of new Set([context.actor, process.env.GITHUB_TRIGGERING_ACTOR])) { const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username }); @@ -121,17 +171,10 @@ jobs: paths.push(...files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean))); pulls.push({ number: pull_number, head: pr.head.sha }); } - const touches = (...prefixes) => truncated || - 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 run = select(paths, truncated); + relinker = relinker && run.relinker; + const registerContext = run.registerContext; + const writeWatch = run.writeWatch; const { data: main } = await github.rest.repos.getBranch({ owner, repo, branch: 'main' }); core.setOutput('sha', main.commit.sha); core.setOutput('pr', pulls.map((pull) => pull.number).join(' ')); @@ -139,12 +182,13 @@ jobs: core.setOutput('relinker', relinker); core.setOutput('register-context', registerContext); core.setOutput('write-watch', writeWatch); - core.setOutput('full', manual); + core.setOutput('full', full); + core.setOutput('relinker-hosts', relinkerHosts(full, linux)); const merged = pulls.map((pull) => `PR #${pull.number} at ${pull.head} merged into main at ${main.commit.sha}`); merged.forEach((line) => core.info(line)); await core.summary.addRaw( - `${merged.join(', then ')}: full builds ${manual ? 'enabled' : 'disabled'}, ` + - `relinker ${relinker ? 'enabled' : 'skipped: no relinker file changed or the author has no commit in main'}, ` + + `${merged.join(', then ')}: full builds ${full ? 'enabled' : 'disabled'}, ` + + `relinker ${relinker ? (full ? (linux ? 'enabled on macOS (the full builds run the Linux and Windows relinker tests)' : 'enabled on Linux and macOS (the full Windows build runs the Windows relinker tests)') : '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(); @@ -156,13 +200,7 @@ jobs: strategy: fail-fast: false matrix: - include: - - os: ubuntu-latest - compiler: g++ - - os: windows-latest - compiler: g++ - - os: macos-latest - compiler: clang++ + include: ${{ fromJSON(needs.eligibility.outputs.relinker-hosts) }} env: WINLIBS_RELEASE: 15.2.0posix-14.0.0-ucrt-r7 WINLIBS_ZIP: winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip @@ -184,7 +222,7 @@ jobs: read -ra numbers <<< "$PR_NUMBER" read -ra heads <<< "$PR_HEAD" refspecs=() - for number in "${numbers[@]}"; do refspecs+=("refs/pull/$number/head:refs/remotes/pull/$number"); done + for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done git fetch --no-tags --unshallow origin "${refspecs[@]}" for i in "${!numbers[@]}"; do if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi @@ -266,7 +304,7 @@ jobs: read -ra numbers <<< "$PR_NUMBER" read -ra heads <<< "$PR_HEAD" refspecs=() - for number in "${numbers[@]}"; do refspecs+=("refs/pull/$number/head:refs/remotes/pull/$number"); done + for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done fetch_options=() if [ "$(git rev-parse --is-shallow-repository)" = true ]; then fetch_options+=(--unshallow); fi git fetch --no-tags "${fetch_options[@]}" origin "${refspecs[@]}" @@ -332,7 +370,7 @@ jobs: read -ra numbers <<< "$PR_NUMBER" read -ra heads <<< "$PR_HEAD" refspecs=() - for number in "${numbers[@]}"; do refspecs+=("refs/pull/$number/head:refs/remotes/pull/$number"); done + for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done git fetch --no-tags --unshallow origin "${refspecs[@]}" for i in "${!numbers[@]}"; do if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi diff --git a/.github/workflows/pr-overlap.yml b/.github/workflows/pr-overlap.yml index f6d437eebd..40a008035f 100644 --- a/.github/workflows/pr-overlap.yml +++ b/.github/workflows/pr-overlap.yml @@ -38,11 +38,12 @@ jobs: record() { jq --arg pr "$1" --arg h "$2" '.[$pr] = $h' "$state" > "$state.new" && mv "$state.new" "$state"; } shopt -s nullglob marker='' + unpinned() { sed -E 's#/blob/[0-9a-f]{40}/#/blob/main/#g'; } prev= for file in "$out"/*.md; do [ -z "$prev" ] || record "$prev" "$hash" pr=$(basename "$file" .md) - hash=$(sha256sum "$file" | cut -d' ' -f1) + hash=$(unpinned < "$file" | sha256sum | cut -d' ' -f1) prev= [ "$(jq -r --arg pr "$pr" '.[$pr] // empty' "$state")" = "$hash" ] && continue prev=$pr @@ -50,7 +51,7 @@ jobs: --jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | startswith(\"$marker\"))) | [.id, .body] | @json" | head -n1) id=$(jq -r '.[0] // empty' <<< "${comment:-null}") if [ -s "$file" ]; then - [ "$(jq -r '.[1]' <<< "${comment:-null}")" = "$(cat "$file")" ] && continue + [ "$(jq -r '.[1]' <<< "${comment:-null}" | unpinned)" = "$(unpinned < "$file")" ] && continue if [ -n "$id" ]; then gh api -X PATCH "repos/$GH_REPO/issues/comments/$id" -F body=@"$file" > /dev/null else diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml new file mode 100644 index 0000000000..e2b5aa9186 --- /dev/null +++ b/.github/workflows/web-tests.yml @@ -0,0 +1,31 @@ +name: Web tests + +on: + pull_request: + paths: + - .github/workflows/web-tests.yml + - demo/web/** + - tools/test_web.py + +permissions: + contents: read + +concurrency: + group: web-tests-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + output-directory: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Check browser module syntax + run: | + node --check demo/web/app.mjs + node --check demo/web/output-directory.mjs + node --check demo/web/worker.mjs + - name: Test folder output recovery + run: node --test demo/web/tests/output-directory.test.mjs diff --git a/CMakeLists.txt b/CMakeLists.txt index 5670ea8ce3..502a2cf90e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,6 +87,10 @@ if(BUILD_TESTING) set_tests_properties(request_serializer PROPERTIES TIMEOUT 20) find_package(Python3 QUIET COMPONENTS Interpreter) if(Python3_Interpreter_FOUND) + add_test(NAME shader_cache_native_paths COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/test_shader_cache_native_paths.py + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ShaderCacheVersion.cmake ${CMAKE_COMMAND}) + set_tests_properties(shader_cache_native_paths PROPERTIES TIMEOUT 20) add_test(NAME pr_overlap_encoding COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_encoding.py) add_test(NAME pr_overlap_dependencies COMMAND ${Python3_EXECUTABLE} @@ -115,6 +119,8 @@ if(BUILD_TESTING) ${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 conventions_doc_links COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_doc_links.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} @@ -142,11 +148,17 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py) add_test(NAME release_package COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_package_release.py) + add_test(NAME web_library_assets COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_build_web.py) + set_tests_properties(web_library_assets PROPERTIES TIMEOUT 20) add_test(NAME pad_input COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pad_input.py) set_tests_properties(pad_input PROPERTIES TIMEOUT 20) add_test(NAME trace_digest COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_trace_digest.py) + add_test(NAME conventions_bom_shebang COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_bom_shebang.py) + set_tests_properties(conventions_bom_shebang PROPERTIES TIMEOUT 20) add_test(NAME conventions_body_bom COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_body_bom.py) add_test(NAME conventions_blob_reads COMMAND ${Python3_EXECUTABLE} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b83c070c09..a9a443afc7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,6 +39,8 @@ 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` 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. +With `--field full=false`, the manual run skips the full builds and runs only the relinker matrix on Linux, Windows and macOS, if the pull request changes a file the relinker-only build uses, such as the Python tools and their tests in `tools/`. + 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 [technical debt](docs/dev/TechnicalDebt.md), the index or a file in `docs/dev/debt/`, and only UTF-8 text files. Run it locally before pushing: ``` diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 2b49528bdd..8385138261 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -58,6 +58,21 @@ if(MINGW) foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") endforeach() + + if(NOT EXISTS "${CMAKE_RANLIB}") + message(FATAL_ERROR "Stable import libraries require ranlib") + endif() + list(LENGTH CMAKE_CXX_CREATE_SHARED_LIBRARY linkCommandCount) + string(FIND "${CMAKE_CXX_CREATE_SHARED_LIBRARY}" "-Wl,--out-implib, " implibOption) + if(NOT linkCommandCount EQUAL 1 OR implibOption EQUAL -1) + message(FATAL_ERROR "Expected one shared library link command that writes -Wl,--out-implib,: ${CMAKE_CXX_CREATE_SHARED_LIBRARY}") + endif() + string(REPLACE "-Wl,--out-implib, " "-Wl,--out-implib,.new " linkCommand "${CMAKE_CXX_CREATE_SHARED_LIBRARY}") + set(CMAKE_CXX_CREATE_SHARED_LIBRARY + "\"${CMAKE_COMMAND}\" -E env SOURCE_DATE_EPOCH=0 ${linkCommand}" + "\"${CMAKE_RANLIB}\" -D .new" + "\"${CMAKE_COMMAND}\" -E copy_if_different .new " + ) endif() set(LIBC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc) @@ -87,6 +102,14 @@ set_target_properties(libc PROPERTIES set(LIBC_UNPATCHED ${UNPATCHED_DIR}/libc.prx) set(LIBC_PATCHED ${PATCHED_DIR}/libc.prx) +set(LIBC_EXPORT_LIST ${CMAKE_CURRENT_BINARY_DIR}/libc.exports) + +add_custom_command( + OUTPUT ${LIBC_EXPORT_LIST} + COMMAND $ --write-export-list ${LIBC_UNPATCHED} ${LIBC_EXPORT_LIST} + DEPENDS nid_patcher libc ${LIBC_UNPATCHED} + COMMENT "Listing libc exports" +) add_custom_command( OUTPUT ${LIBC_PATCHED} @@ -146,8 +169,8 @@ foreach(LIB_DIR ${LIBS_DIRS}) add_custom_command( OUTPUT ${PATCHED_FILE} COMMAND ${CMAKE_COMMAND} -E copy ${UNPATCHED_FILE} ${PATCHED_FILE} - COMMAND $ ${LIB_NAME} --preserve-exports "${LIBC_UNPATCHED}" "${PATCHED_FILE}" - DEPENDS nid_patcher ${LIB_NAME} ${UNPATCHED_FILE} ${LIBC_UNPATCHED} + COMMAND $ ${LIB_NAME} --preserve-export-list "${LIBC_EXPORT_LIST}" "${PATCHED_FILE}" + DEPENDS nid_patcher ${LIB_NAME} ${UNPATCHED_FILE} ${LIBC_EXPORT_LIST} COMMENT "Patching NIDs for ${LIB_NAME}" ) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 9ddb8391f4..0ebfa792f7 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1750,23 +1750,29 @@ struct JpegEncOutputInfo { }; struct JpegDecCreateParam { - std::uint32_t size; + std::uint32_t this_size; std::uint32_t attribute; + std::uint32_t max_image_width; }; struct JpegDecParseParam { const void* jpeg_mem_addr; std::uint32_t jpeg_mem_size; - std::uint32_t reserved0; + std::uint16_t decode_mode; + std::uint16_t down_scale; }; struct JpegDecDecodeParam { const void* jpeg_mem_addr; void* image_mem_addr; + void* coefficient_mem_addr; std::uint32_t jpeg_mem_size; std::uint32_t image_mem_size; + std::uint32_t coefficient_mem_size; + std::uint16_t decode_mode; + std::uint16_t down_scale; std::uint16_t pixel_format; - std::uint16_t reserved0; + std::uint16_t alpha_value; std::uint32_t image_pitch; }; @@ -1774,8 +1780,13 @@ struct JpegDecImageInfo { std::uint32_t image_width; std::uint32_t image_height; std::uint16_t color_space; - std::uint16_t bit_depth; - std::uint32_t image_flag; + std::uint16_t component_number; + std::uint8_t component_id[4]; + std::uint8_t sampling_factor[4]; + std::uint32_t coefficient_mem_size; + std::uint32_t suitable_csc_attribute; + std::uint32_t output_image_width; + std::uint32_t output_image_height; }; struct PlayGoInitParams { diff --git a/core/libs/nid/include/nid/ExportExclusions.hpp b/core/libs/nid/include/nid/ExportExclusions.hpp index 93442d156e..ae41e169c5 100644 --- a/core/libs/nid/include/nid/ExportExclusions.hpp +++ b/core/libs/nid/include/nid/ExportExclusions.hpp @@ -8,6 +8,8 @@ namespace Nid { std::string NormalizeExportName(const std::string& name); std::unordered_set ReadExportExclusions(const std::string& path); +std::unordered_set ReadExportList(const std::string& path); +bool WriteExportList(const std::string& referencePath, const std::string& path); } diff --git a/core/libs/nid/src/ExportExclusions.cpp b/core/libs/nid/src/ExportExclusions.cpp index 366e918cb5..273f517d3b 100644 --- a/core/libs/nid/src/ExportExclusions.cpp +++ b/core/libs/nid/src/ExportExclusions.cpp @@ -4,10 +4,13 @@ #include #include #include +#include #include #include #include +#include #include +#include namespace Nid { @@ -113,6 +116,24 @@ std::unordered_set ReadElfExports(const std::vector& return result; } +std::string ReadText(const std::string& path, std::ifstream& input) { + std::string text{std::istreambuf_iterator(input), {}}; + if (input.bad()) throw std::runtime_error("cannot read export list: " + path); + return text; +} + +std::string FormatExportList(const std::unordered_set& exports) { + std::vector names(exports.begin(), exports.end()); + std::ranges::sort(names); + std::string text; + for (const auto& name : names) { + if (name.find('\n') != std::string::npos) throw std::runtime_error("export name contains a line break: " + name); + text += name; + text += '\n'; + } + return text; +} + } std::string NormalizeExportName(const std::string& name) { @@ -153,4 +174,31 @@ std::unordered_set ReadExportExclusions(const std::string& path) { return result; } +std::unordered_set ReadExportList(const std::string& path) { + std::ifstream input(path, std::ios::binary); + if (!input) throw std::runtime_error("cannot open export list: " + path); + const auto text = ReadText(path, input); + if (text.empty() || text.back() != '\n') throw std::runtime_error("malformed export list: " + path); + std::unordered_set result; + for (std::size_t start = 0; start < text.size();) { + const auto end = text.find('\n', start); + if (end == start) throw std::runtime_error("empty name in export list: " + path); + auto name = text.substr(start, end - start); + if (!result.insert(name).second) throw std::runtime_error("duplicate name in export list: " + name); + start = end + 1; + } + return result; +} + +bool WriteExportList(const std::string& referencePath, const std::string& path) { + const auto text = FormatExportList(ReadExportExclusions(referencePath)); + if (std::ifstream existing(path, std::ios::binary); existing && ReadText(path, existing) == text) return false; + std::ofstream output(path, std::ios::binary | std::ios::trunc); + if (!output) throw std::runtime_error("cannot write export list: " + path); + output.write(text.data(), static_cast(text.size())); + output.close(); + if (!output) throw std::runtime_error("cannot write export list: " + path); + return true; +} + } diff --git a/core/libs/nid/src/main.cpp b/core/libs/nid/src/main.cpp index c07a4e8bfa..7e459dc99d 100644 --- a/core/libs/nid/src/main.cpp +++ b/core/libs/nid/src/main.cpp @@ -25,10 +25,25 @@ void _writeFile(const std::string& path, const std::vector& data) int Run(int argc, char* argv[]) { if (argc < 3) { - std::cerr << "Usage: nid_patcher [--preserve-exports ] [file2 ...]\n"; + std::cerr << "Usage: nid_patcher [--preserve-exports | --preserve-export-list ] [file2 ...]\n" + " nid_patcher --write-export-list \n"; return 1; } + if (std::string(argv[1]) == "--write-export-list") { + if (argc != 4) { + std::cerr << "FAIL: --write-export-list requires a reference library and an export list\n"; + return 2; + } + try { + std::cout << (Nid::WriteExportList(argv[2], argv[3]) ? "WROTE: " : "UNCHANGED: ") << argv[3] << "\n"; + } catch (const std::exception& e) { + std::cerr << "FAIL: " << argv[3] << ": " << e.what() << "\n"; + return 2; + } + return 0; + } + const std::string libraryName = argv[1]; std::unordered_set excludedExports; std::vector paths; @@ -36,10 +51,10 @@ int Run(int argc, char* argv[]) { bool hasReference = false; for (int i = 2; i < argc; ++i) { const std::string argument = argv[i]; - if (argument == "--preserve-exports") { - if (hasReference || i + 1 == argc) throw std::runtime_error("--preserve-exports requires exactly one reference library"); + if (argument == "--preserve-exports" || argument == "--preserve-export-list") { + if (hasReference || i + 1 == argc) throw std::runtime_error(argument + " requires exactly one reference"); hasReference = true; - excludedExports = Nid::ReadExportExclusions(argv[++i]); + excludedExports = argument == "--preserve-exports" ? Nid::ReadExportExclusions(argv[++i]) : Nid::ReadExportList(argv[++i]); } else { if (argument.starts_with("--")) throw std::runtime_error("unknown option: " + argument); paths.push_back(argument); diff --git a/core/libs/nid/tests/test_macho_nids.py b/core/libs/nid/tests/test_macho_nids.py index 1d5c118ced..a4a239fcbc 100644 --- a/core/libs/nid/tests/test_macho_nids.py +++ b/core/libs/nid/tests/test_macho_nids.py @@ -1,6 +1,7 @@ """Check nid_patcher on small byte-built Mach-O dylibs: the export trie, the chained imports and what it refuses.""" import hashlib +import os import struct import subprocess import sys @@ -211,6 +212,53 @@ def check_preserved(patcher, directory): assert exports_of(path.read_bytes()) == {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)}, exports_of(path.read_bytes()) +def write_export_list(patcher, reference, listing): + return subprocess.run([str(patcher), "--write-export-list", str(reference), str(listing)], capture_output=True, text=True, timeout=30) + + +def check_export_list(patcher, directory): + reference = Path(directory) / "listed.dylib" + listing = Path(directory) / "listed.exports" + reference.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit", 0, 0x108)], [])) + result = write_export_list(patcher, reference, listing) + assert result.returncode == 0, (result.stdout, result.stderr) + assert listing.read_bytes() == b"exit\nputs\n", listing.read_bytes() + + os.utime(listing, ns=(1_000_000_000_000_000_000, 1_000_000_000_000_000_000)) + reference.write_bytes(build_dylib([("_exit", 0, 0x110), ("_puts_nid_postfix", 0, 0x118)], [])) + result = write_export_list(patcher, reference, listing) + assert result.returncode == 0, (result.stdout, result.stderr) + assert listing.stat().st_mtime_ns == 1_000_000_000_000_000_000, "an unchanged export list was rewritten" + + reference.write_bytes(build_dylib([("_puts", 0, 0x100)], [])) + result = write_export_list(patcher, reference, listing) + assert result.returncode == 0, (result.stdout, result.stderr) + assert listing.read_bytes() == b"puts\n", listing.read_bytes() + assert listing.stat().st_mtime_ns != 1_000_000_000_000_000_000, "a changed export list kept its time" + + path = Path(directory) / "listed-preserve.dylib" + path.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit_nid_postfix", 0, 0x108)], [])) + result = subprocess.run([str(patcher), "libtest", "--preserve-export-list", str(listing), str(path)], capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (result.stdout, result.stderr) + assert exports_of(path.read_bytes()) == {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)}, exports_of(path.read_bytes()) + + malformed_lists = [ + ("unterminated", b"puts", "malformed export list"), + ("blank", b"puts\n\nexit\n", "empty name in export list"), + ("twice", b"puts\nputs\n", "duplicate name in export list"), + ("empty", b"", "malformed export list"), + ] + for name, content, message in malformed_lists: + malformed = Path(directory) / (name + ".exports") + malformed.write_bytes(content) + target = Path(directory) / (name + ".dylib") + target.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100)], [])) + result = subprocess.run([str(patcher), "libtest", "--preserve-export-list", str(malformed), str(target)], capture_output=True, text=True, timeout=30) + assert result.returncode != 0 and message in result.stdout + result.stderr, (name, result.stdout, result.stderr) + result = subprocess.run([str(patcher), "libtest", "--preserve-exports", str(reference), "--preserve-export-list", str(listing), str(path)], capture_output=True, text=True, timeout=30) + assert result.returncode != 0 and "exactly one reference" in result.stdout + result.stderr, (result.stdout, result.stderr) + + def check_refusals(patcher, directory): growing = [("_ab", 0, 0x100), ("_cd", 0, 0x108)] result, _ = patch(patcher, directory, "signed.dylib", build_dylib(growing, [], signed=True)) @@ -227,6 +275,7 @@ def main(): check_shrinking(patcher, directory) check_growing(patcher, directory) check_preserved(patcher, directory) + check_export_list(patcher, directory) check_refusals(patcher, directory) print("NID patcher Mach-O tests passed") diff --git a/core/libs/prx/libSceAgc/DcbState/src/Init.cpp b/core/libs/prx/libSceAgc/DcbState/src/Init.cpp index 27f4ec9738..f3acef34c5 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Init.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Init.cpp @@ -7,21 +7,18 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -static constexpr int SCE_AGC_ERROR_INVALID_ARGUMENT = static_cast(0x80890001); - extern "C" { -int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t version) { - Agc::Command::CheckAddress(reinterpret_cast(state), alignof(std::uint32_t), __func__); - Agc::Command::Require(version < 14, __func__, "unsupported AGC initialization version"); - return 0; -} - int APS5_VABI sceAgcInit(std::uint32_t version) { - if (version >= 14) return SCE_AGC_ERROR_INVALID_ARGUMENT; + if (version >= 14) return 0; Agc::Command::GetRegisterDefaults(version, false, __func__); Agc::Command::GetRegisterDefaults(version, true, __func__); return 0; } +int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t version) { + Agc::Command::CheckAddress(reinterpret_cast(state), alignof(std::uint32_t), __func__); + return sceAgcInit(version); +} + } diff --git a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp index 608c859ad8..1f3c3ac3c6 100644 --- a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp @@ -72,7 +72,7 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* throw std::runtime_error(std::string(fn) + ": input semantic count exceeds 32"); } - AgcDriver::ShaderPreparationScope transaction; + AgcDriver::ShaderPreparationScope transaction({ps != nullptr ? gs : nullptr, ps}); std::array values{}; auto* output = regs; regs = values.data(); diff --git a/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp b/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp index 1044b410f4..59a43354c8 100644 --- a/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp @@ -22,7 +22,7 @@ extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister* context, ShaderRegist // The simple vertex/pixel pipeline combines two primitive context registers // and 32 interpolant registers. Publish only after both helpers succeed. - AgcDriver::ShaderPreparationScope transaction; + AgcDriver::ShaderPreparationScope transaction({vertex, pixel}); std::array contextValues{}; std::array primitiveValues{}; sceAgcCreatePrimState(contextValues.data(), primitiveValues.data(), nullptr, vertex, primitiveType); diff --git a/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp b/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp index 587ae15a32..b62666315c 100644 --- a/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp @@ -8,6 +8,7 @@ #include "SceShaders.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp" #include "prx/libSceAgc/Shader/include/ShaderUtils.hpp" #include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" @@ -56,7 +57,9 @@ int APS5_VABI sceAgcCreatePrimState(ShaderRegister* cx_regs, ShaderRegister* uc_ const std::array stages{hs, gs}; const bool patch = prim_type == static_cast(ShaderRegs::PrimitiveType::Patch); if (patch == ((contextValues[0].value & ShaderRegs::VGT_SHADER_STAGES_HS_BIT) != 0)) { + AgcDriver::ShaderPreparationScope transaction({hs, gs}); AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span(stages).subspan(hs == nullptr ? 1u : 0u), contextValues, primitiveValues); + transaction.Commit(); } if (cx_regs != nullptr) std::copy(contextValues.begin(), contextValues.end(), cx_regs); if (uc_regs != nullptr) std::copy(primitiveValues.begin(), primitiveValues.end(), uc_regs); diff --git a/core/libs/prx/libSceAgc/Shader/src/ShaderUtils.cpp b/core/libs/prx/libSceAgc/Shader/src/ShaderUtils.cpp index 8a783ea261..5a67826f8f 100644 --- a/core/libs/prx/libSceAgc/Shader/src/ShaderUtils.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/ShaderUtils.cpp @@ -1,4 +1,5 @@ #include +#include #include "prx/libSceAgc/Shader/include/ShaderUtils.hpp" using namespace ShaderRegs; diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 6fc0d679e7..7c90e82644 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -52,8 +52,6 @@ extern "C" std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, namespace { -constexpr int SCE_AGC_ERROR_INVALID_ARGUMENT = static_cast(0x80890001); - void check(bool condition, const char* message) { if (!condition) { throw std::runtime_error(message); @@ -624,12 +622,12 @@ void testDefaults() { check(sceAgcInit_0090(&state, 8) == 0 && state == 0x12345678, "AGC initialization failed or modified caller state"); check(sceAgcInit_0090(&state, 13) == 0 && state == 0x12345678, "AGC version 13 initialization changed caller state"); expectFailure([] { sceAgcInit_0090(nullptr, 8); }); - expectFailure([&] { sceAgcInit_0090(&state, 14); }); + check(sceAgcInit_0090(&state, 14) == 0 && state == 0x12345678, "out-of-range AGC initialization version changed caller state"); check(sceAgcInit(8) == 0, "AGC version initialization failed"); check(sceAgcInit(13) == 0, "AGC version 13 initialization failed"); - check(sceAgcInit(14) == SCE_AGC_ERROR_INVALID_ARGUMENT, "unsupported AGC version did not return invalid argument"); - check(sceAgcInit(0xffffffffu) == SCE_AGC_ERROR_INVALID_ARGUMENT, "maximum AGC version did not return invalid argument"); - check(sceAgcInit(0xffffc278u) == SCE_AGC_ERROR_INVALID_ARGUMENT, "pointer-valued AGC argument did not return invalid argument"); + check(sceAgcInit(14) == 0, "out-of-range AGC version did not return success"); + check(sceAgcInit(0xffffffffu) == 0, "maximum AGC version did not return success"); + check(sceAgcInit(0xffffc278u) == 0, "pointer-valued AGC argument did not return success"); for (std::uint32_t version = 0; version < 14; ++version) { for (const bool internal : {false, true}) { auto* first = Agc::Command::GetRegisterDefaults(version, internal, __func__); diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index b798358b13..55ab8bb76c 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -832,6 +832,11 @@ target_link_libraries(agc_formatted_buffer_once_tests PRIVATE shader_recompiler_ set_target_properties(agc_formatted_buffer_once_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_formatted_buffer_once_tests) +add_test_executable(agc_fma_mix_once_tests EXCLUDE_FROM_ALL tests/FmaMixOnce.cpp) +target_link_libraries(agc_fma_mix_once_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_fma_mix_once_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_fma_mix_once_tests) + add_test_executable(agc_rect_list_position_invariant_tests EXCLUDE_FROM_ALL tests/RectListPositionInvariant.cpp) target_link_libraries(agc_rect_list_position_invariant_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_rect_list_position_invariant_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -1021,6 +1026,7 @@ configure_windows_unwind(agc_driver_recorder_tests) if(BUILD_TESTING) set_target_properties(agc_driver_recorder_tests PROPERTIES EXCLUDE_FROM_ALL OFF) add_test(NAME agc_pipeline_cache COMMAND agc_driver_recorder_tests --pipeline-cache-only) + add_test(NAME agc_pipeline_cache_file COMMAND agc_driver_recorder_tests --pipeline-cache-file-only) add_test(NAME agc_pipeline_library COMMAND agc_driver_recorder_tests --pipeline-library-only) add_test(NAME agc_pipeline_library_off COMMAND agc_driver_recorder_tests --pipeline-library-only) set_tests_properties(agc_pipeline_library agc_pipeline_library_off PROPERTIES SKIP_RETURN_CODE 77) @@ -1031,9 +1037,9 @@ if(BUILD_TESTING) set_tests_properties(agc_pipeline_library agc_pipeline_library_off agc_pipeline_library_fast_only PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - set_tests_properties(agc_pipeline_cache PROPERTIES SKIP_RETURN_CODE 77) + set_tests_properties(agc_pipeline_cache agc_pipeline_cache_file PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_pipeline_cache PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_pipeline_cache agc_pipeline_cache_file PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_unchanged_cpu_stamps COMMAND agc_driver_recorder_tests --unchanged-cpu-stamps-only) @@ -1042,6 +1048,12 @@ if(BUILD_TESTING) set_tests_properties(agc_unchanged_cpu_stamps PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_storage_kept_unit_settle COMMAND agc_driver_recorder_tests --kept-unit-settle-only) + set_tests_properties(agc_storage_kept_unit_settle PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_storage_kept_unit_settle PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_storage_baseline_settled COMMAND agc_driver_recorder_tests --settled-baseline-only) add_test(NAME agc_storage_integer_clear COMMAND agc_driver_recorder_tests --integer-clear-only) set_tests_properties(agc_storage_integer_clear PROPERTIES SKIP_RETURN_CODE 77) @@ -1067,6 +1079,12 @@ if(BUILD_TESTING) set_tests_properties(agc_storage_written_buffer_order PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_storage_split_writeback COMMAND agc_driver_recorder_tests --split-writeback-only) + set_tests_properties(agc_storage_split_writeback PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000") + if(WIN32) + set_tests_properties(agc_storage_split_writeback PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_copied_direct_ordering COMMAND agc_driver_recorder_tests --copied-direct-only) set_tests_properties(agc_copied_direct_ordering PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) @@ -1075,6 +1093,12 @@ if(BUILD_TESTING) endif() add_test(NAME agc_recorder_read_list COMMAND agc_driver_recorder_tests --read-list-only) set_tests_properties(agc_recorder_read_list PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_space_pending_writes COMMAND agc_driver_recorder_tests --space-pending-writes-only) + set_tests_properties(agc_space_pending_writes PROPERTIES SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_space_pending_writes PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + 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) @@ -1096,6 +1120,8 @@ if(BUILD_TESTING) set_tests_properties(agc_descriptor_pool_recycling PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_draw_snapshot_cap COMMAND agc_driver_recorder_tests --draw-snapshot-cap-only) set_tests_properties(agc_draw_snapshot_cap PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_draw_snapshot_epoch COMMAND agc_driver_recorder_tests --draw-snapshot-epoch-only) + set_tests_properties(agc_draw_snapshot_epoch PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_multisample_guest_access COMMAND agc_driver_recorder_tests --multisample-only) set_tests_properties(agc_multisample_guest_access PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_draw_inputs_in_place COMMAND agc_driver_recorder_tests --draw-inputs-in-place-only) @@ -1114,7 +1140,7 @@ if(BUILD_TESTING) add_test(NAME agc_metadata_pass COMMAND agc_driver_recorder_tests --metadata-pass-only) set_tests_properties(agc_metadata_pass PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_draw_snapshot_epoch agc_multisample_guest_access agc_draw_inputs_in_place agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only) set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION @@ -1382,8 +1408,12 @@ endforeach() target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber) +target_link_libraries(agc_driver_preparation_scope_settle_tests PRIVATE libSceAgc) + target_include_directories(agc_driver_alpha_to_coverage_tests PRIVATE $) +target_include_directories(agc_driver_sample_multisample_target_tests PRIVATE $) + if(BUILD_TESTING) add_test(NAME agc_driver_undithered_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --undithered-alpha-to-coverage-state) set_tests_properties(agc_driver_undithered_alpha_to_coverage_state PROPERTIES @@ -1476,6 +1506,11 @@ if(BUILD_TESTING) add_test(NAME agc_shader_srt_read_patching COMMAND agc_shader_memory_tests --srt-read-patching) set_tests_properties(agc_shader_srt_read_patching PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_frontend_pair COMMAND agc_shader_memory_tests --frontend-pair) + add_test(NAME agc_shader_quad_mask_no_demote COMMAND agc_shader_memory_tests --quad-mask-no-demote) + set_tests_properties(agc_shader_quad_mask_no_demote PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_shader_quad_mask_no_demote PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_shader_frontend_pair_off COMMAND agc_shader_memory_tests --frontend-pair) set_tests_properties(agc_shader_frontend_pair PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_shader_frontend_pair_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_FRONTEND_PAIR=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") @@ -1486,7 +1521,7 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - foreach(agcTest IN ITEMS agc_vertex_input_tests 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_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_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 agc_formatted_buffer_byte_stores_tests ${agcExecutionTests}) + foreach(agcTest IN ITEMS agc_vertex_input_tests 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_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_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 agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_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() @@ -1504,6 +1539,11 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(WIN32) + add_test(NAME agc_driver_untracked_snapshot_hint COMMAND agc_driver_partially_resident_snapshot_tests --untracked-hint) + set_tests_properties(agc_driver_untracked_snapshot_hint PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_sparse_buffer_holes COMMAND agc_driver_graphics_tests --sparse-buffer-holes) set_tests_properties(agc_sparse_buffer_holes PROPERTIES TIMEOUT 30) set_tests_properties(agc_driver_sparse_buffer_holes PROPERTIES TIMEOUT 30) @@ -1640,17 +1680,29 @@ if(BUILD_TESTING) "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_worker_failure_log COMMAND agc_driver_flip_tests failure-log) + set_tests_properties(agc_driver_worker_failure_log PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 20) 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 agc_driver_present_mode PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_flip_compute agc_driver_present_mode agc_driver_worker_failure_log 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:") + add_test(NAME agc_driver_draw_cache_reuse_verify COMMAND agc_driver_draw_cache_reuse_tests) + set_tests_properties(agc_driver_draw_cache_reuse_verify PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_VERIFY_DRAW_DATA_HITS=1" SKIP_RETURN_CODE 77) + set_tests_properties(agc_driver_draw_cache_reuse agc_driver_draw_cache_reuse_off agc_driver_draw_cache_reuse_verify PROPERTIES FAIL_REGULAR_EXPRESSION "FATAL:") + set_property(TEST agc_driver_draw_cache_reuse agc_driver_draw_cache_reuse_off APPEND PROPERTY ENVIRONMENT "APS5_VERIFY_DRAW_RECIPE=1" "APS5_VERIFY_REGISTER_KEYS=1") if(WIN32) - set_tests_properties(agc_driver_draw_cache_reuse_off PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_draw_cache_reuse_off agc_driver_draw_cache_reuse_verify PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test(NAME agc_driver_stencil_replay_no_gpl COMMAND agc_driver_stencil_replay_tests) + set_tests_properties(agc_driver_stencil_replay_no_gpl PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_GPL=1" TIMEOUT 30 SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_driver_stencil_replay_no_gpl PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() @@ -2104,6 +2156,14 @@ if(BUILD_TESTING) set_tests_properties(pixel_ancillary PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(interpolation_reference_range_tests ${LIBS_INCLUDE_DIR}/tests/InterpolationReferenceRange.cpp) + target_link_libraries(interpolation_reference_range_tests PRIVATE shader_recompiler_agc_driver libc) + configure_windows_unwind(interpolation_reference_range_tests) + add_test(NAME interpolation_reference_range COMMAND interpolation_reference_range_tests) + if(WIN32) + set_tests_properties(interpolation_reference_range PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_indexed_draw_tests ${LIBS_INCLUDE_DIR}/tests/GuestIndexedDraw.cpp) target_include_directories(guest_indexed_draw_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_indexed_draw_tests PRIVATE libSceAgc libSceAgcDriver libc) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/DeviceAccess.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/DeviceAccess.hpp index 910f6ba77d..3bd84a70db 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/DeviceAccess.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/DeviceAccess.hpp @@ -7,6 +7,7 @@ #include #include #include +#include "prx/libc/include/general/AtomicSharedPtr.hpp" namespace AgcDriver::DriverDetail { @@ -21,7 +22,7 @@ public: bool operator==(std::nullptr_t) const; private: - std::atomic> pointer; + AtomicSharedPtr pointer; }; class DeviceUseGate { diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp index 1591221fdf..110894795c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp @@ -17,6 +17,7 @@ #include #include #include +#include "prx/libc/include/general/AtomicSharedPtr.hpp" namespace AgcDriver::DriverDetail { @@ -31,7 +32,7 @@ struct DispatchVariant { std::shared_ptr shader; - std::atomic> recipe; + AtomicSharedPtr recipe; static constexpr std::uint32_t NoFlatBinding = std::numeric_limits::max(); std::vector dataPositions; 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 83797987c1..cf7e9a6a9f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp @@ -11,6 +11,7 @@ #include #include #include +#include "prx/libc/include/general/AtomicSharedPtr.hpp" namespace AgcDriver::DriverDetail { @@ -24,6 +25,11 @@ struct DrawProgram { std::shared_ptr snapshot; std::size_t codeOffset = 0; std::shared_ptr tailContinuation; + std::uint32_t resourceRegister = 0; + bool nullPixel = false; + bool merged = false; + bool mergedPointerRequired = false; + std::uint32_t mergedPointer = 0; }; struct DrawDecode { @@ -33,6 +39,8 @@ struct DrawDecode { std::vector roles; }; +void ReadUserWords(const QueueState& queue, DrawProgram& program, bool staticAbi); +void InitializeMerged(const QueueState& queue, DrawProgram& program, std::uint32_t pointerBase, bool pointerRequired, bool staticAbi); void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const ShaderRegistry& registry, bool staticAbi, bool includeFragment); struct PreparedGraphicsStage { @@ -58,9 +66,17 @@ struct DrawEntry { std::vector>> stages; - std::atomic>> recipes; + AtomicSharedPtr> recipes; std::uint64_t touched = 0; std::list::iterator order; + std::uint64_t shape = 0; +}; + +using DrawDataCandidates = std::vector, std::vector>>>; + +struct StageDataHit { + std::vector words; + std::shared_ptr compiled; }; enum class DrawMiss : std::size_t { FrontDiffering, FragmentDiffering, OtherDiffering, Layout, Gate, Stages, Count }; @@ -73,6 +89,10 @@ struct DrawEntryCounters { std::uint64_t registerKeyLookups = 0, registerKeyHits = 0, decodeSkipped = 0, decodePartial = 0, facadeMismatches = 0, verifyDecodes = 0, verifyDecodeMismatches = 0; double keyUs = 0; + std::uint64_t absentNewRegisters = 0, absentUserWords = 0, absentEvicted = 0, absentNeverInserted = 0, evictions = 0; + std::uint64_t differingSameRuns = 0, differingWords = 0, differingRunsChanged = 0; + std::uint64_t dataHits = 0, dataStagesReused = 0, dataStagesCompiled = 0, dataVerified = 0; + std::uint64_t stageDataHits = 0, stageDataWords = 0, stageDataVerified = 0, stageDataInserts = 0, stageDataPositions = 0; std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now(); }; @@ -93,6 +113,7 @@ enum class DrawVerdict { Drawn, Nothing, Rejected }; struct StageCapture { std::shared_ptr compiled; + std::shared_ptr capture; std::vector regions; std::uint64_t forgetSerial = 0; std::uint32_t pushOffset = 0; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp index 74a562c8b5..c96acb0762 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp @@ -5,7 +5,7 @@ namespace AgcDriver::DriverDetail { -std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords = false, bool incremental = true); +std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords = false, bool incremental = true, std::uint64_t* shapeKey = nullptr); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index fc2f6d1a7d..7da8e8022b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -36,6 +36,7 @@ #include #include #include +#include #include #include @@ -62,6 +63,7 @@ public: void ResolveShaderAbi(const Shader* shader, std::span context, std::span primitive); void ResolveGraphicsStagesAbi(std::span stages, std::span context, std::span primitive); void ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType); + void SettleShaderPreparations(std::span stages); static void FoldDrawOffsets(const ShaderRecompiler::RecompileResult& result, const DrawProgram& program, Pm4::DrawParameters& parameters); static std::optional ClassifyIndirectDraw(const ShaderRecompiler::RecompileResult& result, const Graphics::State& graphics, const DrawProgram& frontProgram, const std::shared_ptr& localDevice, Pm4::DrawParameters& drawParameters, bool traceIndirect); static std::string dumpRequest(std::uint64_t address, const ShaderRecompiler::RecompileRequest& request); @@ -116,25 +118,33 @@ private: void dispatch(QueueState& queue, std::span packet, const Submission& submission, std::uint64_t indirectArguments = 0); void verifyDataHit(const ShaderSnapshot& snapshot, std::size_t codeOffset, ShaderRecompiler::RecompileRequest request, std::span memory, std::uint64_t address, const DispatchVariant& variant, std::span liveWords, const ShaderRecompiler::RecompileResult& patched); static bool drawEntries(); + static bool drawDataHits(); + static bool verifyDrawDataHits(); + static bool drawStageDataHits(); static bool verifyDrawEntries(); static bool registerKeyEnabled(); static bool verifyDrawRecipe(); static std::size_t drawCacheEntries(); void accountDrawVariant(const DispatchVariant& variant, bool added); - void insertDrawEntry(std::uint64_t key, std::vector>& fresh, std::shared_ptr decode); - bool admitDrawKey(std::uint64_t key); + void insertDrawEntry(std::uint64_t key, std::vector>& fresh, std::shared_ptr decode, std::uint64_t shape = 0); + bool admitDrawKey(std::uint64_t key, std::uint64_t shape); void eraseDrawEntry(std::uint64_t key, const std::shared_ptr& expected); std::shared_ptr findDrawRecipe(std::uint64_t key, const std::vector>& stages); void attachDrawRecipe(std::uint64_t key, const std::vector>& stages, std::shared_ptr recipe); - void reportDrawCache(DrawEntryCounters& counters); - static std::uint64_t drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial); + static bool traceDrawCache(); + static void traceInsert(std::unordered_set& set, std::uint64_t key); + void maybeReportDrawCache(bool profile); + void reportDrawCache(DrawEntryCounters& counters, bool profile); + std::string profileDrawCacheLines(const DrawEntryCounters& counters); + static std::uint64_t drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial, std::uint64_t* shape = nullptr); static bool sameVertexInfo(const ShaderRecompiler::ShaderVertexStageInfo& a, const ShaderRecompiler::ShaderVertexStageInfo& b); static bool sameDecode(const DrawDecode& a, const DrawDecode& b); std::shared_ptr decodeDraw(const QueueState& queue, const Submission& submission); void resolveDrawDecode(const QueueState& queue, const Submission& submission, std::shared_ptr& decode, bool registerKey, std::uint64_t drawKey, bool profile); - void lookupDraw(const Submission& submission, const std::shared_ptr& localDevice, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector& programs, const std::vector& roles, const std::vector>& vertexInfos, bool useDrawEntries, bool registerKey, bool profile, std::uint64_t& drawKey, std::shared_ptr& entry, std::vector>& matched, std::vector>& matchedRegions, bool& drawHit, bool& verifyHit, DrawPhaseTiming& phaseTiming, std::array& phaseMs); + void lookupDraw(const Submission& submission, const std::shared_ptr& localDevice, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector& programs, const std::vector& roles, const std::vector>& vertexInfos, bool useDrawEntries, bool registerKey, bool profile, std::uint64_t& drawKey, std::shared_ptr& entry, std::vector>& matched, std::vector>& matchedRegions, bool& drawHit, bool& verifyHit, DrawPhaseTiming& phaseTiming, std::array& phaseMs, const std::shared_ptr& dataEntry, DrawDataCandidates& dataCandidates, std::vector& stageData); std::shared_ptr materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector>& vertexInfos, std::vector& memory, const std::vector& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr& localDevice, ShaderMemory& shaderMemory, std::vector& stageCaptures, std::vector& recompiled, bool drawHit, const std::vector>& matched, const std::vector>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array& phaseMs, std::string& rejected); - void cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional& indirectCpu, const std::vector& programs, const std::vector& stageCaptures, const std::vector>& vertexInfos, const std::vector>& decodeReads, bool verifyHit, const std::vector>& matched, std::vector>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr& decode, DrawPhaseTiming& phaseTiming); + static void recordStageDataPositions(DispatchVariant& variant, const ShaderRecompiler::ResourceCapture& capture, const std::vector& decodeReads); + void cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional& indirectCpu, const std::vector& programs, const std::vector& stageCaptures, const std::vector>& vertexInfos, const std::vector>& decodeReads, bool verifyHit, const std::vector>& matched, std::vector>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr& decode, DrawPhaseTiming& phaseTiming, const std::vector& reused, const std::shared_ptr& entry, std::uint64_t shapeKey); static bool drawPrecheck(); std::optional precheckDraw(const QueueState& queue, const Submission& submission, std::span packet, const Pm4::DrawParameters& drawParameters, std::string& rejected, bool& traceIndirect); static std::uint32_t drawUserWord(const DrawProgram& program, std::int32_t sgpr); @@ -207,6 +217,7 @@ private: bool captureStable(std::span captured); bool validateCaptured(std::uint64_t program, std::uint32_t queue, std::span captured, const ShaderRecompiler::RecompileResult& compiled, bool inPlace, const PendingView& view, bool* unmapped = nullptr, std::optional* sampling = nullptr, const DataMask* data = nullptr); static void appendEntryRegions(const DispatchVariant& variant, std::vector& regions, const std::vector* words = nullptr); + static std::shared_ptr patchDataWords(const DispatchVariant& variant, const std::vector& words); bool syncPendingRuns(std::uint64_t program, std::uint32_t queue, const ShaderRecompiler::RecompileResult& compiled, std::span regions, std::uint64_t& synced, PendingView& pending, std::optional& sampling); EntryOutcome validateVariant(std::uint64_t program, std::uint32_t queue, const DispatchVariant& variant, std::span regions, std::uint64_t& imagesFlushed, std::uint64_t& runsSynced, std::optional& sampling, std::vector>* live = nullptr); static int waitTimeoutMs(); @@ -316,10 +327,13 @@ private: std::unordered_map> priorValueSets; std::unordered_map> drawCache; + std::unordered_map drawShapes; + std::unordered_set traceShapes, traceKeysSeen, traceKeysEvicted; std::list drawOrder; std::mutex drawCacheMutex; std::uint64_t drawCacheHits = 0, drawCacheEvictions = 0, drawCacheVariants = 0, drawCacheVariantBytes = 0; std::optional drawKeySightings; + std::optional drawShapeSightings; DrawEntryCounters drawEntryCounters; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp index ad4da0a768..0c72dfd1f1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp @@ -1,9 +1,14 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_BUFFERCOPY_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_BUFFERCOPY_HPP +#include "Recompiler.hpp" +#include #include #include #include +#include +#include +#include namespace AgcDriver::DriverDetail { @@ -13,6 +18,17 @@ extern std::atomic knownValueEntries, knownValueReads, knownValue enum CopyPath { CopyCpu = 0, CopyGpu = 1, CopyNotImported = 2, CopyAliased = 3, CopyRecordPending = 4, CopyShape = 5, CopyOverlapping = 6, CopyInaccessible = 7, CopyOverGpuMax = 8, CopyPaths = 9 }; +extern std::array, CopyPaths> copyPathCounts; + +struct StreamCopy { + std::uint64_t source = 0; + std::uint64_t destination = 0; + std::size_t bytes = 0; +}; + +bool MatchesStreamCopyKernel(std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); +std::optional StreamCopyRange(const std::vector& userData, std::uint32_t groups); + } #endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp index f64ecfaa7f..e71457e0f3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include namespace AgcDriver::DriverDetail { 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 5ba7460078..9aa51c4f40 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -113,6 +113,7 @@ std::shared_ptr RegisteredProgram(const ShaderRegistry& re void PublishRegisteredShader(std::shared_ptr& registry, const std::shared_ptr& snapshot); void SubmitRegistrationPreparation(std::function task, const PreparedShaders* prepared = nullptr); void PrioritizeRegistrationPreparation(PreparedShaders& prepared); +void AwaitRegistrationPreparation(); void SubmitLibraryPrebuild(std::function task); void CloseRegistrationPreparation(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp index 435a8ef490..2a5314f9ff 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp @@ -162,6 +162,9 @@ public: std::span slices; std::vector totals; bool dropped = false; + std::uint64_t eventsLost = 0; + std::optional eventsFullAt; + bool totalsFull = false; }; inline static const std::atomic AlwaysBuffering{true}; @@ -195,7 +198,10 @@ public: if (scope == nullptr) continue; snapshot.totals.push_back({scope, entry.stage.load(std::memory_order_relaxed), Clock::duration(entry.length.load(std::memory_order_relaxed)), entry.count.load(std::memory_order_relaxed)}); } - snapshot.dropped = dropped.load(std::memory_order_relaxed); + snapshot.eventsLost = lost.load(std::memory_order_acquire); + if (snapshot.eventsLost != 0) snapshot.eventsFullAt = Clock::time_point(Clock::duration(firstLost.load(std::memory_order_relaxed))); + snapshot.totalsFull = totalsFull.load(std::memory_order_relaxed); + snapshot.dropped = snapshot.eventsLost != 0 || snapshot.totalsFull; return snapshot; } @@ -210,7 +216,9 @@ private: void append(const Slice& slice) { const auto index = used.load(std::memory_order_relaxed); if (index == capacity) { - dropped.store(true, std::memory_order_relaxed); + const auto count = lost.load(std::memory_order_relaxed); + if (count == 0) firstLost.store(slice.begin.time_since_epoch().count(), std::memory_order_relaxed); + lost.store(count + 1, std::memory_order_release); return; } slices[index] = slice; @@ -233,7 +241,7 @@ private: entry.count.store(entry.count.load(std::memory_order_relaxed) + 1, std::memory_order_relaxed); return; } - dropped.store(true, std::memory_order_relaxed); + totalsFull.store(true, std::memory_order_relaxed); } const std::string name; @@ -242,7 +250,9 @@ private: const std::unique_ptr slices; const std::size_t capacity; std::atomic used{0}; - std::atomic dropped{false}; + std::atomic lost{0}; + std::atomic firstLost{0}; + std::atomic totalsFull{false}; std::array entries; bool sampled = false; std::uint64_t sampledPresent = 0; @@ -344,6 +354,10 @@ public: } output += text; } + if (thread.eventsFullAt) { + event("trace buffer full", "i", *thread.eventsFullAt, tid); + output += ",\"s\":\"t\",\"args\":{\"events_lost\":" + std::to_string(thread.eventsLost) + "}}"; + } } for (std::size_t index = 0; index + 1 < presents.size(); ++index) { event("present", "i", presents[index], presenter + 1); @@ -423,8 +437,22 @@ public: bool firstWorker = true; std::map phases; bool dropped = false; + struct DroppedThread { + std::string name; + std::uint64_t eventsLost = 0; + std::optional eventsFullAt; + bool totalsFull = false; + }; + std::vector droppedList; for (const auto& thread : threads) { dropped = dropped || thread.dropped; + if (thread.eventsLost != 0 || thread.totalsFull) { + auto entry = std::find_if(droppedList.begin(), droppedList.end(), [&](const DroppedThread& candidate) { return candidate.name == thread.name; }); + if (entry == droppedList.end()) entry = droppedList.insert(droppedList.end(), DroppedThread{thread.name}); + entry->eventsLost += thread.eventsLost; + if (thread.eventsLost != 0 && thread.eventsFullAt && (!entry->eventsFullAt || *thread.eventsFullAt < *entry->eventsFullAt)) entry->eventsFullAt = thread.eventsFullAt; + entry->totalsFull = entry->totalsFull || thread.totalsFull; + } Clock::duration busy{}; for (const auto& total : thread.totals) { const std::string_view scope = total.scope; @@ -447,6 +475,20 @@ public: } output += "},\"trace_dropped\":"; output += dropped ? "true" : "false"; + if (!droppedList.empty()) { + std::string droppedThreads; + for (const auto& entry : droppedList) { + if (!droppedThreads.empty()) droppedThreads += ','; + AppendString(droppedThreads, entry.name); + droppedThreads += ":{"; + if (entry.eventsLost != 0) { + if (entry.eventsFullAt && !presents.empty() && *entry.eventsFullAt >= presents.front() && *entry.eventsFullAt <= presents.back()) droppedThreads += "\"events_full_after_s\":" + number(std::chrono::duration(*entry.eventsFullAt - presents.front()).count()) + ","; + droppedThreads += "\"events_lost\":" + std::to_string(entry.eventsLost) + ","; + } + droppedThreads += std::string("\"totals_full\":") + (entry.totalsFull ? "true" : "false") + "}"; + } + output += ",\"trace_dropped_threads\":{" + droppedThreads + "}"; + } output += ",\"trace\":"; AppendString(output, summary.trace); output += "}\n"; @@ -483,7 +525,7 @@ public: if (!buffering.load(std::memory_order_relaxed)) return; std::lock_guard lock(gpuMutex); if (gpuRanges.size() == gpuRanges.capacity()) { - gpuDropped = true; + ++gpuLost; return; } gpuRanges.push_back({name, beginNs, endNs, batch}); @@ -509,7 +551,7 @@ private: inline static thread_local TraceThread* local = nullptr; inline static std::mutex gpuMutex; inline static std::vector gpuRanges; - inline static bool gpuDropped = false; + inline static std::uint64_t gpuLost = 0; }; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp index 35c5467efb..58fde0469b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp @@ -194,6 +194,7 @@ std::vector DecodeStores(std::span packet); // packet made by reading them there (through the checked guest memory path, which waits for // recorded GPU work that writes them), and both steps in one for the packet. std::uint64_t DispatchArgumentAddress(std::span packet, const QueueState& queue); +void NoteDispatchRegisters(std::span packet, QueueState& queue); std::array ReadDispatchArguments(std::uint64_t arguments, std::uint32_t initiator); std::array PartialGroupThreads(const std::array& groups, const std::array& numThreads); std::array ResolveDispatch(std::span packet, const QueueState& queue); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp index 0c31480307..30fcf5b8f4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp @@ -1,11 +1,16 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERPREPARATIONSCOPE_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERPREPARATIONSCOPE_HPP +#include +#include + +struct Shader; + namespace AgcDriver::DriverDetail { class ShaderPreparationTransaction; } -extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix(); +extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix(const Shader* const* shaders, std::size_t count); extern "C" void AgcDriverCommitShaderPreparation_nid_postfix(AgcDriver::DriverDetail::ShaderPreparationTransaction* transaction); extern "C" void AgcDriverEndShaderPreparation_nid_postfix(AgcDriver::DriverDetail::ShaderPreparationTransaction* transaction) noexcept; @@ -13,7 +18,7 @@ namespace AgcDriver { class ShaderPreparationScope { public: - ShaderPreparationScope() : transaction(AgcDriverBeginShaderPreparation_nid_postfix()) {} + explicit ShaderPreparationScope(std::initializer_list shaders) : transaction(AgcDriverBeginShaderPreparation_nid_postfix(shaders.begin(), shaders.size())) {} ~ShaderPreparationScope() { AgcDriverEndShaderPreparation_nid_postfix(transaction); } ShaderPreparationScope(const ShaderPreparationScope&) = delete; ShaderPreparationScope& operator=(const ShaderPreparationScope&) = delete; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index f757498d0a..87800734a3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -25,6 +25,7 @@ class StorageTexture; // Stage A of a dispatch's resource build (see VulkanDevice::PrepareDispatch); opaque to callers. struct PreparedDispatch; +struct ComputePipelineObjects; // The unlocked pre-checks of a recipe hit (see VulkanDevice::PrepareRecipe); opaque to callers. struct RecipeHit; // The record tail shared by an ordinary dispatch and a recipe hit (VulkanDevice::recordDispatch). @@ -168,6 +169,10 @@ public: std::uint64_t aliased, created, noSource, shape, refused; }; static CopyAliasing CopyAliasCounts(); + struct ComputePipelineCreation { + std::uint64_t created, underGpuMutex; + }; + static ComputePipelineCreation ComputePipelineCounts(); void ResolveMemory(std::uint64_t address, std::size_t bytes, bool writable); void* Window() const; void Resize(std::uint32_t width, std::uint32_t height); @@ -234,7 +239,7 @@ public: // records. Null when nothing is prepared: the resource cache may serve the dispatch (its // Revalidate stays under the mutex), or APS5_LOCKED_BUILD=1 keeps the whole build under it as // before. `shader` and `snapshots` must outlive the dispatch. - std::shared_ptr PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span snapshots, std::uint64_t dispatchThreads = 0); + std::shared_ptr PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span snapshots, std::uint64_t dispatchThreads = 0, std::uint64_t programAddress = 0); // APS5_PROFILE_DRAW: the parts of a PrepareDispatch in milliseconds, in the order key, find, // precollect, presync, stage A (the driver's 'prepare:' rows). static std::span PreparePhaseMs(const PreparedDispatch& prepared); @@ -317,6 +322,7 @@ private: IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe); // The stage-A pre-sync over `surfaces` (see PrepareDispatch): the serial waited for, 0 none. std::uint64_t presync(std::span> surfaces); + std::shared_ptr computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress); // A DISPATCH_INDIRECT's argument path (see DispatchIndirect): GPU-side from the host import, or // the counts read on the CPU (the record then continues as a direct dispatch). void decideIndirect(RecordedDispatch& record, IndirectOutcome& outcome, char* groupsText); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index 374129a92b..c00249bc0c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" @@ -73,9 +74,12 @@ void Driver::dispatch(QueueState& queue, std::span packet, const bool partialLastGroup = partialGroups != std::array{}; if (resolving) { if (!partialLastGroup && matchesFillKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return; - if (!partialLastGroup && indirectArguments == 0 && matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return; + if (!partialLastGroup && indirectArguments == 0 && (matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) || MatchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute))) return; } else { - if (const auto htile = Graphics::HtileDepthClearAddress(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads); htile != 0) Graphics::NoteHtileDepthClear(htile); + if (const auto htile = Graphics::HtileDepthClearAddress(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads); htile != 0) { + const auto threads = (packet[4] & 0x20u) != 0 ? static_cast(packet[1]) : static_cast(packet[1]) * compute.numThreads[0]; + Graphics::NoteHtileDepthClear(htile, static_cast(threads * 16u)); + } if (indirectArguments == 0 && (packet[4] & 0x20u) == 0) { if (const auto fill = Graphics::MatchMemoryValueFill(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads, {packet[1], packet[2], packet[3]})) Graphics::NoteMemoryValueFill(*fill); } @@ -369,7 +373,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, std::shared_ptr prepared; if (recipeHit == nullptr || VulkanDevice::VerifyRecipes()) { try { - prepared = localDevice->PrepareDispatch(compiled, snapshots, dispatchThreads); + prepared = localDevice->PrepareDispatch(compiled, snapshots, dispatchThreads, address); } catch (const std::exception& error) { if (rethrow(error)) return; } 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 508bbbfcb1..441cbdf3c7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp @@ -155,6 +155,7 @@ void Driver::resolveGroupAhead(const Submission& submission, const QueueState& l try { if (opcode == 0x15) dispatch(scratch, packet, submission); else dispatchIndirect(scratch, packet, submission); + Pm4::NoteDispatchRegisters(packet, scratch); } catch (const std::exception&) { ++stats.failures; break; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchLookup.cpp index 47f52c0886..b0b37b7900 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchLookup.cpp @@ -123,12 +123,7 @@ void Driver::lookupDispatch(std::uint64_t address, const Submission& submission, for (const auto& [position, value] : liveData) liveWords[position] = value; regions.clear(); appendEntryRegions(*variant, regions, &liveWords); - auto patched = std::make_shared(*variant->compiled); - auto& descriptor = patched->bindings[variant->flatBinding].guestDescriptor; - for (std::size_t k = 0; k < variant->dataPositions.size(); ++k) { - if (variant->dataSlots[k] < descriptor.size()) descriptor[variant->dataSlots[k]] = liveWords[variant->dataPositions[k]]; - } - compiledResult = std::move(patched); + compiledResult = patchDataWords(*variant, liveWords); ++counters.dataHits; counters.dataWordsRefreshed += liveData.size(); ++counters.dataHitsByRank[rank]; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/VariantValidation.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/VariantValidation.cpp index 6c76ebb1a6..64d3fc89fe 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/VariantValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/VariantValidation.cpp @@ -17,6 +17,15 @@ void Driver::appendEntryRegions(const DispatchVariant& variant, std::vector Driver::patchDataWords(const DispatchVariant& variant, const std::vector& words) { + auto patched = std::make_shared(*variant.compiled); + auto& descriptor = patched->bindings[variant.flatBinding].guestDescriptor; + for (std::size_t k = 0; k < variant.dataPositions.size(); ++k) { + if (variant.dataSlots[k] < descriptor.size()) descriptor[variant.dataSlots[k]] = words[variant.dataPositions[k]]; + } + return patched; +} + bool Driver::syncPendingRuns(std::uint64_t program, std::uint32_t queue, const ShaderRecompiler::RecompileResult& compiled, std::span regions, std::uint64_t& synced, PendingView& pending, std::optional& sampling) { using Policy = ShaderMemory::PendingWrite; static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index 8858b58f2c..881c040c61 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -6,6 +6,7 @@ #include "Optimization/ResourceProgram.hpp" #include #include +#include namespace AgcDriver::DriverDetail { @@ -67,9 +68,11 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe std::uint64_t drawKey = 0; std::shared_ptr entry; std::shared_ptr decode; + std::shared_ptr dataEntry; + std::uint64_t shapeKey = 0; if (registerKey) { const auto keyStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - drawKey = drawRegisterKey(queue, *submission.shaders, localDevice->Serial()); + drawKey = drawRegisterKey(queue, *submission.shaders, localDevice->Serial(), &shapeKey); std::lock_guard cacheLock(drawCacheMutex); ++drawEntryCounters.lookups; ++drawEntryCounters.registerKeyLookups; @@ -80,7 +83,26 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe decode = entry->decode; } else { ++drawEntryCounters.absent; + if (traceDrawCache()) { + auto& counters = drawEntryCounters; + if (traceKeysEvicted.contains(drawKey)) ++counters.absentEvicted; + else if (traceKeysSeen.contains(drawKey)) ++counters.absentNeverInserted; + else if (traceShapes.contains(shapeKey)) ++counters.absentUserWords; + else ++counters.absentNewRegisters; + } + if (drawDataHits() && !drawParameters.indirect) { + if (const auto shape = drawShapes.find(shapeKey); shape != drawShapes.end()) { + const auto kept = drawCache.find(shape->second); + if (kept != drawCache.end() && kept->second->decode != nullptr && kept->second->shape == shapeKey) dataEntry = kept->second; + } + if (dataEntry != nullptr) ++drawEntryCounters.dataHits; + } } + if (traceDrawCache()) { + traceInsert(traceKeysSeen, drawKey); + traceInsert(traceShapes, shapeKey); + } + maybeReportDrawCache(profile); } if (decode != nullptr && !DecodeProgramsCurrent(*decode, *submission.shaders)) { std::lock_guard cacheLock(drawCacheMutex); @@ -89,8 +111,27 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe entry = nullptr; decode = nullptr; } + if (dataEntry != nullptr && !DecodeProgramsCurrent(*dataEntry->decode, *submission.shaders)) dataEntry = nullptr; phaseTiming.Phase(DrawRowKeyLookupValidate); + if (dataEntry != nullptr) { + auto refreshed = std::make_shared(*dataEntry->decode); + for (auto& program : refreshed->programs) { + ReadUserWords(queue, program, false); + if (program.merged) InitializeMerged(queue, program, program.mergedPointer, program.mergedPointerRequired, false); + } + if (verifyDrawDataHits()) { + const auto full = decodeDraw(queue, submission); + if (!sameDecode(*refreshed, *full)) { + std::size_t differing = 0; + while (differing < full->programs.size() && differing < refreshed->programs.size() && full->programs[differing].userData == refreshed->programs[differing].userData) ++differing; + char message[320]; + std::snprintf(message, sizeof(message), "APS5_VERIFY_DRAW_DATA_HITS: a data-only hit's decode differs from the full decode (key 0x%llx, shape 0x%llx, %zu / %zu programs, first with differing user words %zu, path %u / %u, target 0x%llx / 0x%llx)", static_cast(drawKey), static_cast(shapeKey), refreshed->programs.size(), full->programs.size(), differing, static_cast(refreshed->state.stages.path), static_cast(full->state.stages.path), static_cast(refreshed->state.color.address), static_cast(full->state.color.address)); + throw std::runtime_error(message); + } + } + decode = std::move(refreshed); + } resolveDrawDecode(queue, submission, decode, registerKey, drawKey, profile); const auto& graphics = decode->state; auto pixel = decode->pixel; @@ -171,9 +212,16 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe std::vector> fresh(programs.size()); std::vector recompiled(programs.size(), false); + std::vector, std::vector>>> dataCandidates(programs.size()); + std::vector reused(programs.size(), false); + std::vector stageData(programs.size()); + std::uint64_t dataStagesReused = 0, dataStagesCompiled = 0, dataVerified = 0; bool drawHit = false; bool verifyHit = false; - lookupDraw(submission, localDevice, graphics, pixel, programs, roles, vertexInfos, useDrawEntries, registerKey, profile, drawKey, entry, matched, matchedRegions, drawHit, verifyHit, phaseTiming, phaseMs); + const bool stageEntries = useDrawEntries && !(graphics.stages.tessellation && !drawParameters.indirect && drawParameters.instanceCount > 1u); + lookupDraw(submission, localDevice, graphics, pixel, programs, roles, vertexInfos, stageEntries, registerKey, profile, drawKey, entry, matched, matchedRegions, drawHit, verifyHit, phaseTiming, phaseMs, dataEntry, dataCandidates, stageData); + const bool patchedStages = std::any_of(stageData.begin(), stageData.end(), [](const StageDataHit& hit) { return hit.compiled != nullptr; }); + std::uint64_t stageDataVerified = 0; if (registerKey) { @@ -182,6 +230,11 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe if (matched[i]->vertexInfo != nullptr) vertexInfos[i] = *matched[i]->vertexInfo; continue; } + if (!dataCandidates[i].empty() && !verifyDrawDataHits()) { + const auto& info = dataCandidates[i].front().first->vertexInfo; + if (info != nullptr) vertexInfos[i] = *info; + continue; + } decodeVertexInfo(i); if (matched[i] != nullptr && verifyDrawRecipe() && (vertexInfos[i].has_value() != (matched[i]->vertexInfo != nullptr) || (vertexInfos[i] && !sameVertexInfo(*vertexInfos[i], *matched[i]->vertexInfo)))) { static std::atomic reports{0}; @@ -195,6 +248,46 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe const auto fold = [&](const ShaderRecompiler::RecompileResult& main, Pm4::DrawParameters& parameters) { FoldDrawOffsets(main, programs.front(), parameters); }; + const auto freshRuns = [&](std::size_t i, std::vector>& runs, std::vector& words) { + std::vector regions(stageCaptures[i].regions.begin(), stageCaptures[i].regions.end()); + for (const auto& read : decodeReads[i]) regions.push_back({read.address, std::as_bytes(std::span(read.bytes))}); + std::stable_sort(regions.begin(), regions.end(), [](const ShaderRecompiler::MemoryRegion& a, const ShaderRecompiler::MemoryRegion& b) { return a.guestAddress < b.guestAddress; }); + for (const auto& region : regions) { + runs.emplace_back(region.guestAddress, region.guestAddress + region.bytes.size()); + const auto count = region.bytes.size() / sizeof(std::uint32_t); + const auto offset = words.size(); + words.resize(offset + count); + std::memcpy(words.data() + offset, region.bytes.data(), count * sizeof(std::uint32_t)); + } + }; + const auto verifyDataStage = [&](std::size_t i, const DispatchVariant& kept) { + std::vector> runs; + std::vector words; + freshRuns(i, runs, words); + const auto& compiled = *stageCaptures[i].compiled; + const char* what = runs != kept.runs ? "runs" : words != kept.words ? "words" : vertexInfos[i].has_value() != (kept.vertexInfo != nullptr) || (vertexInfos[i] && !sameVertexInfo(*vertexInfos[i], *kept.vertexInfo)) ? "vertex stage info" : compiled.variantId != kept.compiled->variantId ? "variant" : compiled.pushConstants != kept.compiled->pushConstants ? "push constants" : nullptr; + ++dataVerified; + if (what == nullptr) return; + char message[320]; + std::snprintf(message, sizeof(message), "APS5_VERIFY_DRAW_DATA_HITS: stage %zu (program 0x%llx) of a data-only hit differs from its full build in its %s (%zu runs / %zu words kept, %zu / %zu fresh; variant 0x%llx / 0x%llx; key 0x%llx, shape 0x%llx)", i, static_cast(programs[i].binary.codeAddress), what, kept.runs.size(), kept.words.size(), runs.size(), words.size(), static_cast(kept.compiled->variantId), static_cast(compiled.variantId), static_cast(drawKey), static_cast(shapeKey)); + throw std::runtime_error(message); + }; + const auto verifyStageData = [&](std::size_t i) { + const auto& hit = stageData[i]; + const auto& kept = *matched[i]; + std::vector> runs; + std::vector words; + freshRuns(i, runs, words); + const auto& fresh = *stageCaptures[i].compiled; + const auto& patched = *hit.compiled; + const bool sameBindings = fresh.bindings.size() == patched.bindings.size() && std::equal(fresh.bindings.begin(), fresh.bindings.end(), patched.bindings.begin(), [](const auto& a, const auto& b) { return a.kind == b.kind && a.role == b.role && a.binding == b.binding && a.count == b.count && a.guestDescriptor == b.guestDescriptor; }); + const char* what = runs != kept.runs ? "runs" : words != hit.words ? "words" : fresh.variantId != patched.variantId ? "variant" : fresh.pushConstants != patched.pushConstants ? "push constants" : !sameBindings ? "bindings" : nullptr; + ++stageDataVerified; + if (what == nullptr) return; + char message[256]; + std::snprintf(message, sizeof(message), "APS5_VERIFY_DRAW_DATA_HITS: stage %zu (program 0x%llx) of a stage data hit differs from its full build in its %s (%zu data positions refreshed)", i, static_cast(programs[i].binary.codeAddress), what, kept.dataPositions.size()); + throw std::runtime_error(message); + }; std::optional indirectCpu; std::vector pushOffsets(programs.size(), 0); std::vector resultIndex(programs.size(), 0); @@ -205,19 +298,33 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe pushOffsets[i] = pushCursorBytes; if (drawHit) { - programResults[i] = matched[i]->compiled.get(); + programResults[i] = stageData[i].compiled != nullptr ? stageData[i].compiled.get() : matched[i]->compiled.get(); memory.insert(memory.end(), matchedRegions[i].begin(), matchedRegions[i].end()); } else { - if (program.binary.stage == ShaderRecompiler::ShaderStage::Fragment) { - for (auto previous = i; previous-- > 0;) { - if (programResults[previous] == nullptr) continue; - pixel = Graphics::DefaultUnexportedInputs(decode->pixel, programResults[previous]->parameterExports); - break; + const auto candidate = std::find_if(dataCandidates[i].begin(), dataCandidates[i].end(), [&](const auto& kept) { return kept.first->pushOffset == pushCursorBytes; }); + if (candidate != dataCandidates[i].end() && !verifyDrawDataHits()) { + matched[i] = candidate->first; + matchedRegions[i] = std::move(candidate->second); + reused[i] = true; + programResults[i] = stageData[i].compiled != nullptr ? stageData[i].compiled.get() : matched[i]->compiled.get(); + memory.insert(memory.end(), matchedRegions[i].begin(), matchedRegions[i].end()); + } else { + if (!dataCandidates[i].empty() && !verifyDrawDataHits()) decodeVertexInfo(i); + if (program.binary.stage == ShaderRecompiler::ShaderStage::Fragment) { + for (auto previous = i; previous-- > 0;) { + if (programResults[previous] == nullptr) continue; + pixel = Graphics::DefaultUnexportedInputs(decode->pixel, programResults[previous]->parameterExports); + break; + } } + compiledStages.push_back(materializeDrawStage(i, pushCursorBytes, queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected)); + if (!rejected.empty()) return DrawVerdict::Rejected; + programResults[i] = compiledStages.back().get(); + if (candidate != dataCandidates[i].end()) verifyDataStage(i, *candidate->first); + if (stageData[i].compiled != nullptr && verifyDrawDataHits()) verifyStageData(i); } - compiledStages.push_back(materializeDrawStage(i, pushCursorBytes, queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected)); - if (!rejected.empty()) return DrawVerdict::Rejected; - programResults[i] = compiledStages.back().get(); + if (dataEntry != nullptr && reused[i]) ++dataStagesReused; + else if (dataEntry != nullptr) ++dataStagesCompiled; } const auto& result = *programResults[i]; if (i == 0 && drawParameters.indirect) { @@ -231,7 +338,17 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe pushCursorBytes += static_cast(result.pushConstants.size()); } - cacheDrawStages(useDrawEntries, drawHit, drawParameters, indirectCpu, programs, stageCaptures, vertexInfos, decodeReads, verifyHit, matched, fresh, drawKey, registerKey, decode, phaseTiming); + if (stageDataVerified != 0) { + std::lock_guard cacheLock(drawCacheMutex); + drawEntryCounters.stageDataVerified += stageDataVerified; + } + if (dataEntry != nullptr) { + std::lock_guard cacheLock(drawCacheMutex); + drawEntryCounters.dataStagesReused += dataStagesReused; + drawEntryCounters.dataStagesCompiled += dataStagesCompiled; + drawEntryCounters.dataVerified += dataVerified; + } + cacheDrawStages(stageEntries, drawHit, drawParameters, indirectCpu, programs, stageCaptures, vertexInfos, decodeReads, verifyHit, matched, fresh, drawKey, registerKey, decode, phaseTiming, reused, entry, shapeKey); timing.Mark("shader_compile_and_link"); for (const auto& reads : decodeReads) { @@ -325,6 +442,11 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe patch(baseVertexWord, indirect.recordBytes == 20 ? arguments.vertexOffset : arguments.firstVertexOrIndex); patch(startInstanceWord, arguments.firstInstance); patch(drawIndexWord, record); + if (graphics.stages.tessellation) { + for (std::size_t i = 0; i < programs.size(); ++i) { + if (programs[i].binary.stage == Stage::TessellationControl || programs[i].binary.stage == Stage::TessellationEvaluation) patched.insert(i); + } + } Pm4::DrawParameters direct{0, arguments.count, 0, arguments.instances, drawParameters.flags, drawParameters.indexed, 0, 0}; if (drawParameters.indexed) { @@ -343,7 +465,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe auto& result = results[resultIndex[programIndex]]; const auto pushBytes = result.pushConstants.size(); decodeVertexInfo(programIndex); - result = *materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected); + result = *materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, programs[programIndex].binary.stage == Stage::TessellationControl || programs[programIndex].binary.stage == Stage::TessellationEvaluation ? direct : drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected); if (!rejected.empty()) return DrawVerdict::Rejected; require(result.pushConstants.size() == pushBytes, "patched program changed its push constant layout"); } @@ -365,7 +487,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe } std::vector> recipeStages; - if (registerKey && !drawParameters.indirect && Graphics::DrawRecipes()) { + if (registerKey && !drawParameters.indirect && !patchedStages && Graphics::DrawRecipes()) { recipeStages.reserve(programs.size()); for (std::size_t i = 0; i < programs.size(); ++i) recipeStages.push_back(drawHit ? matched[i] : fresh[i]); if (std::all_of(recipeStages.begin(), recipeStages.end(), [](const std::shared_ptr& variant) { return variant == nullptr; })) recipeStages.clear(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp index bf7cf368bf..e7b4e66382 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp @@ -2,6 +2,9 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp" #include #include +#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/WriteEvidence.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include namespace AgcDriver::DriverDetail { @@ -23,6 +26,21 @@ bool Driver::drawEntries() { return entries; } +bool Driver::drawDataHits() { + static const bool hits = std::getenv("APS5_NO_DRAW_DATA_HITS") == nullptr; + return hits; +} + +bool Driver::drawStageDataHits() { + static const bool hits = std::getenv("APS5_NO_DRAW_STAGE_DATA_HITS") == nullptr && !stampValidate(); + return hits; +} + +bool Driver::verifyDrawDataHits() { + static const bool verify = std::getenv("APS5_VERIFY_DRAW_DATA_HITS") != nullptr; + return verify; +} + bool Driver::verifyDrawEntries() { static const bool verify = std::getenv("APS5_VERIFY_DRAW_ENTRIES") != nullptr; return verify; @@ -80,14 +98,17 @@ bool DrawKeySightings::SeenBefore(std::uint64_t key) { return false; } -bool Driver::admitDrawKey(std::uint64_t key) { +bool Driver::admitDrawKey(std::uint64_t key, std::uint64_t shape) { std::lock_guard cacheLock(drawCacheMutex); if (drawCache.contains(key)) return true; if (!drawKeySightings) drawKeySightings.emplace(drawCacheEntries() * DrawKeySightings::Ways); - return drawKeySightings->SeenBefore(key); + const bool seen = drawKeySightings->SeenBefore(key); + if (seen || shape == 0 || drawShapes.contains(shape)) return seen; + if (!drawShapeSightings) drawShapeSightings.emplace(drawCacheEntries() * DrawKeySightings::Ways); + return drawShapeSightings->SeenBefore(shape); } -void Driver::insertDrawEntry(std::uint64_t key, std::vector>& fresh, std::shared_ptr decode) { +void Driver::insertDrawEntry(std::uint64_t key, std::vector>& fresh, std::shared_ptr decode, std::uint64_t shape) { std::lock_guard cacheLock(drawCacheMutex); auto& counters = drawEntryCounters; ++counters.inserts; @@ -95,6 +116,7 @@ void Driver::insertDrawEntry(std::uint64_t key, std::vector(); replacement->stages.resize(fresh.size()); replacement->decode = std::move(decode); + replacement->shape = shape; if (found != drawCache.end()) { if (found->second->stages.size() == fresh.size()) replacement->stages = found->second->stages; if (found->second->decode != nullptr) replacement->decode = found->second->decode; @@ -119,6 +141,7 @@ void Driver::insertDrawEntry(std::uint64_t key, std::vectortouched = drawCacheHits; + if (replacement->decode != nullptr && shape != 0) drawShapes[shape] = key; if (found == drawCache.end()) { drawOrder.push_front(key); replacement->order = drawOrder.begin(); @@ -133,9 +156,12 @@ void Driver::insertDrawEntry(std::uint64_t key, std::vectorsecond->stages) { for (const auto& variant : variants) accountDrawVariant(*variant, false); } + if (traceDrawCache()) traceInsert(traceKeysEvicted, last->first); + if (const auto shapeOf = drawShapes.find(last->second->shape); shapeOf != drawShapes.end() && shapeOf->second == last->first) drawShapes.erase(shapeOf); drawOrder.erase(last->second->order); drawCache.erase(last); ++drawCacheEvictions; + ++counters.evictions; } } @@ -145,6 +171,7 @@ void Driver::eraseDrawEntry(std::uint64_t key, const std::shared_ptr& for (const auto& variants : found->second->stages) { for (const auto& variant : variants) accountDrawVariant(*variant, false); } + if (const auto shapeOf = drawShapes.find(found->second->shape); shapeOf != drawShapes.end() && shapeOf->second == key) drawShapes.erase(shapeOf); drawOrder.erase(found->second->order); drawCache.erase(found); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp index 94d63e8297..da6d28175f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp @@ -1,20 +1,41 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include namespace AgcDriver::DriverDetail { -void Driver::reportDrawCache(DrawEntryCounters& counters) { +bool Driver::traceDrawCache() { + static const bool trace = std::getenv("APS5_TRACE_DRAW_CACHE") != nullptr; + return trace; +} + +void Driver::traceInsert(std::unordered_set& set, std::uint64_t key) { + if (set.size() >= (1u << 16u)) set.clear(); + set.insert(key); +} + +void Driver::maybeReportDrawCache(bool profile) { + auto& counters = drawEntryCounters; + if (!profile && !traceDrawCache()) return; + const auto now = std::chrono::steady_clock::now(); + if (now - counters.lastReport < std::chrono::seconds(10)) return; + reportDrawCache(counters, profile); + counters.lastReport = now; +} + +void Driver::reportDrawCache(DrawEntryCounters& counters, bool profile) { const auto count = [](std::uint64_t value) { return static_cast(value); }; - const auto validated = counters.lookups - counters.absent; const auto miss = [&](DrawMiss reason) { return count(counters.misses[static_cast(reason)]); }; - std::string ranks; - for (std::size_t rank = 0; rank < dispatchVariants(); ++rank) { - char text[32]; - std::snprintf(text, sizeof(text), "%s%llu", rank == 0 ? "" : " / ", count(counters.variantHitsByRank[rank])); - ranks += text; + std::string text; + if (traceDrawCache()) { + char line[1536]; + std::uint64_t misses = 0; + for (const auto value : counters.misses) misses += value; + std::snprintf(line, sizeof(line), "[draw-cache] 10 s: %llu lookups, %llu hits (%.1f%%), %llu stage misses, %llu without an entry, %llu data-only hits (stages: %llu reused, %llu compiled; %llu verified); no entry: new registers %llu, user words/pointers only %llu, evicted %llu, seen but never inserted %llu; stage misses: front differing %llu, fragment differing %llu, other differing %llu, layout %llu, gate %llu, stage count %llu; differing captures: same runs %llu (%.1f words changed each), other runs %llu; %zu entries, %llu evicted\n", count(counters.lookups), count(counters.hits), counters.lookups != 0 ? 100.0 * static_cast(counters.hits) / static_cast(counters.lookups) : 0.0, count(misses), count(counters.absent), count(counters.dataHits), count(counters.dataStagesReused), count(counters.dataStagesCompiled), count(counters.dataVerified), count(counters.absentNewRegisters), count(counters.absentUserWords), count(counters.absentEvicted), count(counters.absentNeverInserted), miss(DrawMiss::FrontDiffering), miss(DrawMiss::FragmentDiffering), miss(DrawMiss::OtherDiffering), miss(DrawMiss::Layout), miss(DrawMiss::Gate), miss(DrawMiss::Stages), count(counters.differingSameRuns), counters.differingSameRuns != 0 ? static_cast(counters.differingWords) / static_cast(counters.differingSameRuns) : 0.0, count(counters.differingRunsChanged), drawCache.size(), count(counters.evictions)); + text += line; } - AgcDriver::ProfilePrint_nid_no_patch( "[draw-cache] %llu lookups (10 s): %llu no entry, %llu validated in %.1f us each (key, lookup and every stage): %llu hits (every stage equal; stage hits by variant rank 1..k %s), misses by reason: front stage differing %llu, fragment differing %llu, other stage differing %llu, layout (push offset) %llu, gate %llu, stage count %llu; %llu stage validations (%llu equal, %.2f variants compared each); %llu inserts (%llu variants inserted, %llu evicted beyond k, %llu already present, %llu unstable), %zu entries (%llu variants, ~%.1f MiB), %llu evictions in total, %llu LRU moves; verify: %llu hits captured again, %llu stages differed\n", count(counters.lookups), count(counters.absent), count(validated), validated != 0 ? counters.validateUs / static_cast(validated) : 0.0, count(counters.hits), ranks.c_str(), miss(DrawMiss::FrontDiffering), miss(DrawMiss::FragmentDiffering), miss(DrawMiss::OtherDiffering), miss(DrawMiss::Layout), miss(DrawMiss::Gate), miss(DrawMiss::Stages), count(counters.stageValidations), count(counters.stageEqual), counters.stageValidations != 0 ? static_cast(counters.variantsCompared) / static_cast(counters.stageValidations) : 0.0, count(counters.inserts), count(counters.variantsInserted), count(counters.variantsEvicted), count(counters.present), count(counters.unstable), drawCache.size(), count(drawCacheVariants), static_cast(drawCacheVariantBytes) / (1024.0 * 1024.0), count(drawCacheEvictions), count(counters.touches), count(counters.verifyHits), count(counters.verifyMismatches)); - AgcDriver::ProfilePrint_nid_no_patch( "[draw-cache] register key (10 s): %llu lookups, %llu hits, key build %.1f us each; decode skipped %llu, partial (state/pixel/programs from the entry) %llu; facade log != table %llu; verify: %llu decodes compared, %llu differed\n", count(counters.registerKeyLookups), count(counters.registerKeyHits), counters.registerKeyLookups != 0 ? counters.keyUs / static_cast(counters.registerKeyLookups) : 0.0, count(counters.decodeSkipped), count(counters.decodePartial), count(counters.facadeMismatches), count(counters.verifyDecodes), count(counters.verifyDecodeMismatches)); + if (profile) text += profileDrawCacheLines(counters); + AgcDriver::ProfilePrint_nid_no_patch("%s", text.c_str()); counters = DrawEntryCounters{}; } @@ -23,4 +44,26 @@ DrawEntryCounters Driver::DrawCacheCounters() { return drawEntryCounters; } +std::string Driver::profileDrawCacheLines(const DrawEntryCounters& counters) { + std::string text; + const auto append = [&](const char* format, auto... values) { + char line[2048]; + std::snprintf(line, sizeof(line), format, values...); + text += line; + }; + const auto count = [](std::uint64_t value) { return static_cast(value); }; + const auto validated = counters.lookups > counters.absent ? counters.lookups - counters.absent : 0; + const auto miss = [&](DrawMiss reason) { return count(counters.misses[static_cast(reason)]); }; + std::string ranks; + for (std::size_t rank = 0; rank < dispatchVariants(); ++rank) { + char item[32]; + std::snprintf(item, sizeof(item), "%s%llu", rank == 0 ? "" : " / ", count(counters.variantHitsByRank[rank])); + ranks += item; + } + append("[draw-cache] %llu lookups (10 s): %llu no entry, %llu validated in %.1f us each (key, lookup and every stage): %llu hits (every stage equal; stage hits by variant rank 1..k %s), misses by reason: front stage differing %llu, fragment differing %llu, other stage differing %llu, layout (push offset) %llu, gate %llu, stage count %llu; %llu stage validations (%llu equal, %.2f variants compared each); %llu inserts (%llu variants inserted, %llu evicted beyond k, %llu already present, %llu unstable), %zu entries (%llu variants, ~%.1f MiB), %llu evictions in total, %llu LRU moves; verify: %llu hits captured again, %llu stages differed\n", count(counters.lookups), count(counters.absent), count(validated), validated != 0 ? counters.validateUs / static_cast(validated) : 0.0, count(counters.hits), ranks.c_str(), miss(DrawMiss::FrontDiffering), miss(DrawMiss::FragmentDiffering), miss(DrawMiss::OtherDiffering), miss(DrawMiss::Layout), miss(DrawMiss::Gate), miss(DrawMiss::Stages), count(counters.stageValidations), count(counters.stageEqual), counters.stageValidations != 0 ? static_cast(counters.variantsCompared) / static_cast(counters.stageValidations) : 0.0, count(counters.inserts), count(counters.variantsInserted), count(counters.variantsEvicted), count(counters.present), count(counters.unstable), drawCache.size(), count(drawCacheVariants), static_cast(drawCacheVariantBytes) / (1024.0 * 1024.0), count(drawCacheEvictions), count(counters.touches), count(counters.verifyHits), count(counters.verifyMismatches)); + append("[draw-cache] register key (10 s): %llu lookups, %llu hits, key build %.1f us each; decode skipped %llu, partial (state/pixel/programs from the entry) %llu; facade log != table %llu; verify: %llu decodes compared, %llu differed\n", count(counters.registerKeyLookups), count(counters.registerKeyHits), counters.registerKeyLookups != 0 ? counters.keyUs / static_cast(counters.registerKeyLookups) : 0.0, count(counters.decodeSkipped), count(counters.decodePartial), count(counters.facadeMismatches), count(counters.verifyDecodes), count(counters.verifyDecodeMismatches)); + append("[draw-cache] stage data hits (10 s): %llu stages (%llu words refreshed), verified %llu; stage inserts with data positions %llu (%.1f positions each)\n", count(counters.stageDataHits), count(counters.stageDataWords), count(counters.stageDataVerified), count(counters.stageDataInserts), counters.stageDataInserts != 0 ? static_cast(counters.stageDataPositions) / static_cast(counters.stageDataInserts) : 0.0); + return text; +} + } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp index 4f3db831ac..5c049b74a6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -7,6 +7,8 @@ #include "Optimization/ResourceProgram.hpp" #include #include +#include +#include namespace AgcDriver::DriverDetail { @@ -29,19 +31,35 @@ std::shared_ptr Driver::materializeDraw auto& stageCapture = stageCaptures[i]; stageCapture.forgetSerial = GuestMemory::ForgetSerial(); stageCapture.pushOffset = pushOffset; + const auto named = [&](const std::exception& error) { + static const bool dumpShaders = std::getenv("APS5_DUMP_SHADERS") != nullptr; + std::string dump; + if (dumpShaders) { + try { + dump = dumpRequest(program.binary.codeAddress, request); + } catch (...) {} + } + std::ostringstream where; + where << "shader 0x" << std::hex << program.binary.codeAddress << " stage " << std::dec << static_cast(program.binary.stage); + if (!dump.empty()) where << " (" << dump << ")"; + where << ": "; + return std::runtime_error(where.str() + error.what()); + }; const auto capture = [&] { const SampledReadScope sampling(evidenceReads); - return shaderMemory.Capture(invocation); + try { + return shaderMemory.Capture(invocation); + } catch (const std::exception& error) { + throw named(error); + } }(); stageCapture.regions = shaderMemory.TakeRecentRegions(); recompiled[i] = true; memory = shaderMemory.Regions(); - if (drawHit) { - for (std::size_t j = 0; j < programs.size(); ++j) { - if (matched[j] != nullptr && !recompiled[j]) memory.insert(memory.end(), matchedRegions[j].begin(), matchedRegions[j].end()); - } + for (std::size_t j = 0; j < programs.size(); ++j) { + if (matched[j] != nullptr && !recompiled[j] && (drawHit || j < i)) memory.insert(memory.end(), matchedRegions[j].begin(), matchedRegions[j].end()); } request.context.memory = memory; if (traceCapSync()) traceCapture("draw-capture", program.binary.codeAddress, submission.queue, memory, Graphics::Recorder::ThreadWaitedMs() - waitedBefore); @@ -75,18 +93,58 @@ std::shared_ptr Driver::materializeDraw phaseTiming.Phase(DrawRowCapture); timing.Mark("capture_resources"); - stageCapture.compiled = invocation.Materialize(*capture); + try { + stageCapture.compiled = invocation.Materialize(*capture); + } catch (const std::exception& error) { + throw named(error); + } + stageCapture.capture = capture; timing.Mark("materialize"); phaseTiming.Phase(DrawRowRecompile); return stageCapture.compiled; } -void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional& indirectCpu, const std::vector& programs, const std::vector& stageCaptures, const std::vector>& vertexInfos, const std::vector>& decodeReads, bool verifyHit, const std::vector>& matched, std::vector>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr& decode, DrawPhaseTiming& phaseTiming) { - if (useDrawEntries && !drawHit && !(drawParameters.indirect && indirectCpu) && (verifyHit || admitDrawKey(drawKey))) { +void Driver::recordStageDataPositions(DispatchVariant& variant, const ShaderRecompiler::ResourceCapture& capture, const std::vector& decodeReads) { + const auto& compiled = *variant.compiled; + for (std::size_t b = 0; b < compiled.bindings.size(); ++b) { + if (compiled.bindings[b].role != ShaderRecompiler::DescriptorRole::FlattenedSrt) continue; + variant.flatBinding = static_cast(b); + static_cast(DataWordPositions(variant.runs, capture.readTrace.leaves, capture.readTrace.otherReads, variant.words, compiled.bindings[b].guestDescriptor, variant.dataPositions, variant.dataSlots)); + break; + } + if (variant.dataPositions.empty() || decodeReads.empty()) return; + std::vector decoded(variant.words.size(), false); + std::size_t offset = 0; + for (const auto& [begin, end] : variant.runs) { + for (const auto& read : decodeReads) { + const auto from = std::max(begin, read.address); + const auto to = std::min(end, read.address + read.bytes.size()); + if (from >= to) continue; + for (auto word = (from - begin) / sizeof(std::uint32_t); word < (to - begin + sizeof(std::uint32_t) - 1) / sizeof(std::uint32_t); ++word) decoded[offset + static_cast(word)] = true; + } + offset += static_cast((end - begin) / sizeof(std::uint32_t)); + } + std::size_t kept = 0; + for (std::size_t k = 0; k < variant.dataPositions.size(); ++k) { + if (decoded[variant.dataPositions[k]]) continue; + variant.dataPositions[kept] = variant.dataPositions[k]; + variant.dataSlots[kept] = variant.dataSlots[k]; + ++kept; + } + variant.dataPositions.resize(kept); + variant.dataSlots.resize(kept); +} + +void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional& indirectCpu, const std::vector& programs, const std::vector& stageCaptures, const std::vector>& vertexInfos, const std::vector>& decodeReads, bool verifyHit, const std::vector>& matched, std::vector>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr& decode, DrawPhaseTiming& phaseTiming, const std::vector& reused, const std::shared_ptr& entry, std::uint64_t shapeKey) { + if (useDrawEntries && !drawHit && !(drawParameters.indirect && indirectCpu) && (verifyHit || admitDrawKey(drawKey, registerKey && drawDataHits() ? shapeKey : 0))) { phaseTiming.Phase(DrawRowVectors); - std::uint64_t unstable = 0, mismatches = 0; + std::uint64_t unstable = 0, mismatches = 0, differingSameRuns = 0, differingWords = 0, differingRunsChanged = 0; for (std::size_t i = 0; i < programs.size(); ++i) { const auto& stageCapture = stageCaptures[i]; + if (reused[i]) { + fresh[i] = matched[i]; + continue; + } if (stageCapture.compiled == nullptr || !CacheableResult(*stageCapture.compiled)) continue; auto variant = std::make_shared(); variant->compiled = stageCapture.compiled; @@ -105,6 +163,17 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP variant->words.resize(offset + count); std::memcpy(variant->words.data() + offset, region.bytes.data(), count * sizeof(std::uint32_t)); } + if (drawStageDataHits() && stageCapture.capture != nullptr && !stageCapture.capture->readTrace.leaves.empty()) recordStageDataPositions(*variant, *stageCapture.capture, decodeReads[i]); + if (traceDrawCache() && entry != nullptr && matched[i] == nullptr && i < entry->stages.size()) { + const auto& variants = entry->stages[i]; + const auto front = std::find_if(variants.begin(), variants.end(), [&](const std::shared_ptr& kept) { return kept->pushOffset == variant->pushOffset; }); + if (front != variants.end() && (*front)->runs == variant->runs && (*front)->words.size() == variant->words.size()) { + ++differingSameRuns; + for (std::size_t w = 0; w < variant->words.size(); ++w) differingWords += (*front)->words[w] != variant->words[w] ? 1u : 0u; + } else if (front != variants.end()) { + ++differingRunsChanged; + } + } if (verifyHit && matched[i] != nullptr && (matched[i]->runs != variant->runs || matched[i]->words != variant->words)) { ++mismatches; static std::atomic reports{0}; @@ -119,11 +188,22 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP } fresh[i] = std::move(variant); } - insertDrawEntry(drawKey, fresh, registerKey ? decode : nullptr); - if (unstable != 0 || mismatches != 0) { + insertDrawEntry(drawKey, fresh, registerKey ? decode : nullptr, registerKey && drawDataHits() ? shapeKey : 0); + std::uint64_t dataInserts = 0, dataPositions = 0; + for (std::size_t i = 0; i < programs.size(); ++i) { + if (fresh[i] == nullptr || reused[i] || fresh[i]->dataPositions.empty()) continue; + ++dataInserts; + dataPositions += fresh[i]->dataPositions.size(); + } + if (unstable != 0 || mismatches != 0 || differingSameRuns != 0 || differingRunsChanged != 0 || dataInserts != 0) { std::lock_guard cacheLock(drawCacheMutex); + drawEntryCounters.stageDataInserts += dataInserts; + drawEntryCounters.stageDataPositions += dataPositions; drawEntryCounters.unstable += unstable; drawEntryCounters.verifyMismatches += mismatches; + drawEntryCounters.differingSameRuns += differingSameRuns; + drawEntryCounters.differingWords += differingWords; + drawEntryCounters.differingRunsChanged += differingRunsChanged; } phaseTiming.Phase(DrawRowKeyLookupValidate); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index 0bc1b953b8..5b1840964e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -14,6 +14,39 @@ std::uint32_t ReadGraphicsRegister(const Registers& registers, std::uint32_t off } +void ReadUserWords(const QueueState& queue, DrawProgram& program, bool staticAbi) { + if (!program.nullPixel) Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, program.resourceRegister); + const auto resources = program.nullPixel ? 0u : ReadGraphicsRegister(queue.shader, program.resourceRegister); + const auto userCount = ((resources >> 1u) & 0x1fu) | (((resources >> 27u) & 1u) << 5u); + require(userCount <= 32, "graphics user SGPR count exceeds the register bank"); + program.userData.clear(); + for (std::uint32_t i = 0; i < userCount; ++i) { + Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, program.userDataBase + i); + program.userData.push_back(staticAbi ? ReadGraphicsRegister(queue.shader, program.userDataBase + i) : readUserData(queue.shader, program.userDataBase + i)); + } + if ((program.binary.stage == ShaderRecompiler::ShaderStage::Vertex || program.binary.stage == ShaderRecompiler::ShaderStage::Fragment) && program.snapshot->header.empty() && program.snapshot->userDataTailRegister) program.tailContinuation = ReadUserDataTailContinuation(program.binary.code, program.firstUserSgpr, program.userData); +} + +void InitializeMerged(const QueueState& queue, DrawProgram& program, std::uint32_t pointerBase, bool pointerRequired, bool staticAbi) { + program.merged = true; + program.mergedPointer = pointerBase; + program.mergedPointerRequired = pointerRequired; + program.firstUserSgpr = 0; + program.userData.insert(program.userData.begin(), 8, 0); + Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, pointerBase); + Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, pointerBase + 1); + if (staticAbi) return; + if (!pointerRequired && !queue.shader.contains(pointerBase) && !queue.shader.contains(pointerBase + 1)) return; + const auto low = ReadGraphicsRegister(queue.shader, pointerBase); + const auto high = ReadGraphicsRegister(queue.shader, pointerBase + 1); + const auto address = static_cast(low) | (static_cast(high) << 32u); + require(address != 0 || !pointerRequired, "merged shader user-data address is null"); + if (address == 0) return; + GuestMemory::CheckRange(reinterpret_cast(address), 8, 4); + program.userData[0] = low; + program.userData[1] = high; +} + void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const ShaderRegistry& registry, bool staticAbi, bool includeFragment) { using Stage = ShaderRecompiler::ShaderStage; using Role = ShaderRecompiler::ProgramRole; @@ -33,10 +66,6 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const require((address - snapshot.codeAddress) % sizeof(std::uint32_t) == 0, "graphics entry point is not dword aligned"); require(snapshot.header.empty() || snapshot.type == type, "graphics program refers to an incompatible shader binary type"); if (!nullPixel && snapshot.header.empty()) NoteRawGraphicsDraw(); - if (!nullPixel) Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, rsrc2); - const auto resources = nullPixel ? 0u : ReadGraphicsRegister(queue.shader, rsrc2); - const auto userCount = ((resources >> 1u) & 0x1fu) | (((resources >> 27u) & 1u) << 5u); - require(userCount <= 32, "graphics user SGPR count exceeds the register bank"); const auto codeOffset = static_cast((address - snapshot.codeAddress) / sizeof(std::uint32_t)); DrawProgram result{ {stage, address, std::span(snapshot.code).subspan(codeOffset), snapshot.headerAddress, snapshot.header}, @@ -47,11 +76,9 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const program, codeOffset }; - for (std::uint32_t i = 0; i < userCount; ++i) { - Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, userDataBase + i); - result.userData.push_back((staticAbi ? ReadGraphicsRegister(queue.shader, userDataBase + i) : readUserData(queue.shader, userDataBase + i))); - } - if ((stage == Stage::Vertex || stage == Stage::Fragment) && snapshot.header.empty() && snapshot.userDataTailRegister) result.tailContinuation = ReadUserDataTailContinuation(result.binary.code, result.firstUserSgpr, result.userData); + result.resourceRegister = rsrc2; + result.nullPixel = nullPixel; + ReadUserWords(queue, result, staticAbi); return result; }; { @@ -64,27 +91,11 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const programs.push_back(prepare(programAddress(base), type, stage, resources, users)); roles.push_back(role); }; - const auto initializeMerged = [&](DrawProgram& program, std::uint32_t pointerBase, bool pointerRequired) { - program.firstUserSgpr = 0; - program.userData.insert(program.userData.begin(), 8, 0); - Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, pointerBase); - Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, pointerBase + 1); - if (staticAbi) return; - if (!pointerRequired && !queue.shader.contains(pointerBase) && !queue.shader.contains(pointerBase + 1)) return; - const auto low = ReadGraphicsRegister(queue.shader, pointerBase); - const auto high = ReadGraphicsRegister(queue.shader, pointerBase + 1); - const auto address = static_cast(low) | (static_cast(high) << 32u); - require(address != 0 || !pointerRequired, "merged shader user-data address is null"); - if (address == 0) return; - GuestMemory::CheckRange(reinterpret_cast(address), 8, 4); - program.userData[0] = low; - program.userData[1] = high; - }; const auto& graphics = product->state; if (graphics.stages.path == Graphics::ShaderPath::Tessellation) { if (!staticAbi || programAddress(0x148) != 0) append(0x148, 5, Stage::Local, 0x10b, 0x10c, Role::Local); append(0x108, 7, Stage::TessellationControl, 0x10b, 0x10c, Role::Hull); - initializeMerged(programs.back(), 0x102, true); + InitializeMerged(queue, programs.back(), 0x102, true, staticAbi); append(0x0c8, 2, Stage::TessellationEvaluation, 0x08b, 0x08c, Role::Domain); } else if (graphics.stages.path == Graphics::ShaderPath::Geometry) { const auto frontAddress = programAddress(0xc8); @@ -92,7 +103,7 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const const auto type = front->header.empty() ? std::uint8_t{2} : front->type; require(type == 2 || type == 4, "invalid geometry front binary type"); append(0xc8, type, Stage::Mesh, 0x8b, 0x8c, Role::Main); - initializeMerged(programs.back(), 0x82, type == 4); + InitializeMerged(queue, programs.back(), 0x82, type == 4, staticAbi); if (type == 4) append(0x88, 6, Stage::Mesh, 0x8b, 0x8c, Role::GeometryBack); } else { append(0xc8, 2, Stage::Vertex, 0x8b, 0x8c, Role::Main); @@ -115,6 +126,8 @@ std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Su product->pixel.targetExportPacking = Graphics::ExportPackings(product->state); product->pixel.dualSourceBlend = product->state.dualSourceBlend; product->pixel.alphaToCoverage = product->state.alphaToCoverage; + product->pixel.quadPixelMask = Graphics::QuadPixelMask(product->state); + product->pixel.quadSampleMask = Graphics::QuadSampleMasks(product->state); if (product->pixel.ancillary) product->pixel.rasterPrimitive = Graphics::RasterPrimitiveOf(product->state); if (product->pixel.ancillary) product->pixel.layerExported = product->state.layerExports != 0; product->pixel.ditheredAlphaToCoverage = product->state.ditheredAlphaToCoverage; 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 40b6eeef9d..eb1ca2f599 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -4,7 +4,7 @@ namespace AgcDriver::DriverDetail { -void Driver::lookupDraw(const Submission& submission, const std::shared_ptr& localDevice, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector& programs, const std::vector& roles, const std::vector>& vertexInfos, bool useDrawEntries, bool registerKey, bool profile, std::uint64_t& drawKey, std::shared_ptr& entry, std::vector>& matched, std::vector>& matchedRegions, bool& drawHit, bool& verifyHit, DrawPhaseTiming& phaseTiming, std::array& phaseMs) { +void Driver::lookupDraw(const Submission& submission, const std::shared_ptr& localDevice, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector& programs, const std::vector& roles, const std::vector>& vertexInfos, bool useDrawEntries, bool registerKey, bool profile, std::uint64_t& drawKey, std::shared_ptr& entry, std::vector>& matched, std::vector>& matchedRegions, bool& drawHit, bool& verifyHit, DrawPhaseTiming& phaseTiming, std::array& phaseMs, const std::shared_ptr& dataEntry, DrawDataCandidates& dataCandidates, std::vector& stageData) { using Role = ShaderRecompiler::ProgramRole; if (useDrawEntries) { if (!registerKey) { @@ -62,7 +62,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(pixel.conservativeZExport)); mix(pixel.orderedPixelShader); mix(pixel.stencilRefExportEnable); @@ -73,17 +73,22 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(pixel.rasterPrimitive)); mix(pixel.ditheredAlphaToCoverage); + mix(pixel.quadPixelMask); + for (const auto mask : pixel.quadSampleMask) mix(mask); std::lock_guard cacheLock(drawCacheMutex); ++drawEntryCounters.lookups; const auto found = drawCache.find(drawKey); if (found != drawCache.end()) entry = found->second; else ++drawEntryCounters.absent; + maybeReportDrawCache(profile); } if (entry != nullptr) { const auto waitedBeforeValidate = profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0; std::optional miss; std::vector ranks(programs.size(), 0); - std::uint64_t stageValidations = 0, stageEqual = 0, compared = 0, imagesFlushed = 0, runsSynced = 0; + std::uint64_t stageValidations = 0, stageEqual = 0, compared = 0, imagesFlushed = 0, runsSynced = 0, dataStages = 0, dataWords = 0; + std::vector> liveData; + auto* live = drawStageDataHits() ? &liveData : nullptr; if (entry->stages.size() != programs.size()) miss = DrawMiss::Stages; std::uint32_t cursor = 0; @@ -105,11 +110,21 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptrwords; + for (const auto& [position, value] : liveData) hit.words[position] = value; + regions.clear(); + appendEntryRegions(*variant, regions, &hit.words); + hit.compiled = patchDataWords(*variant, hit.words); + ++dataStages; + dataWords += liveData.size(); + } matched[i] = variant; ranks[i] = rank; ++stageEqual; @@ -130,6 +145,8 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(); rotated->decode = entry->decode; rotated->stages = entry->stages; + rotated->shape = entry->shape; rotated->recipes.store(entry->recipes.load()); for (std::size_t i = 0; i < programs.size(); ++i) { if (ranks[i] == 0) continue; @@ -167,6 +185,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(*miss)]; if (registerKey && entry->decode != nullptr) ++counters.decodePartial; @@ -178,9 +197,21 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr std::chrono::seconds(10)) { - counters.lastReport = std::chrono::steady_clock::now(); - reportDrawCache(counters); + } + } else if (dataEntry != nullptr && graphics.stages.path == Graphics::ShaderPath::Vertex && dataEntry->stages.size() == programs.size() && dataEntry->decode->programs.size() == programs.size()) { + std::uint64_t imagesFlushed = 0, runsSynced = 0; + const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::DrawCache); + std::optional sampling; + for (std::size_t i = 0; i < programs.size(); ++i) { + if (roles[i] == Role::GeometryBack || programs[i].userData != dataEntry->decode->programs[i].userData) continue; + auto& candidates = dataCandidates[i]; + for (const auto& variant : dataEntry->stages[i]) { + if ((i == 0 && variant->pushOffset != 0) || std::any_of(candidates.begin(), candidates.end(), [&](const auto& candidate) { return candidate.first->pushOffset == variant->pushOffset; })) continue; + std::vector regions; + appendEntryRegions(*variant, regions); + auto result = validateVariant(programs[i].binary.codeAddress, submission.queue, *variant, regions, imagesFlushed, runsSynced, sampling); + if (gateRetry() && (result == EntryOutcome::PublishMoved || result == EntryOutcome::PendingMoved)) result = validateVariant(programs[i].binary.codeAddress, submission.queue, *variant, regions, imagesFlushed, runsSynced, sampling); + if (result == EntryOutcome::Equal) candidates.emplace_back(variant, std::move(regions)); } } } 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 59f20a139d..0ba464a4d8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -33,7 +33,7 @@ const auto& registerMasks() { } -std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords, bool incremental) { +std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords, bool incremental, std::uint64_t* shapeKey) { const auto& masks = registerMasks(); const auto fingerprint = [&](const Registers& registers, Graphics::RegisterBank bank) { const auto& mask = masks[static_cast(bank)]; @@ -42,6 +42,7 @@ std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords, bool auto shape = (0xcbf29ce484222325ull ^ fingerprint(queue.context, Graphics::RegisterBank::Context)) * 0x100000001b3ull; shape = (shape ^ fingerprint(queue.shader, Graphics::RegisterBank::Shader)) * 0x100000001b3ull; shape = (shape ^ fingerprint(queue.userConfig, Graphics::RegisterBank::UserConfig)) * 0x100000001b3ull; + if (shapeKey != nullptr) *shapeKey = shape; auto exact = shape; const auto words = [&](std::uint32_t base, std::uint32_t count) { for (auto it = queue.shader.lower_bound(base); it != queue.shader.end() && it->first < base + count; ++it) { @@ -60,14 +61,17 @@ std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords, bool } -std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial) { +std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial, std::uint64_t* shape) { static const bool allUserWords = std::getenv("APS5_DRAW_KEY_ALL_USER_WORDS") != nullptr; static const bool verify = std::getenv("APS5_VERIFY_REGISTER_KEYS") != nullptr; - auto key = RegisterStateKey(queue, allUserWords); + std::uint64_t shapeKey = 0; + auto key = RegisterStateKey(queue, allUserWords, true, &shapeKey); if (verify && key != RegisterStateKey(queue, allUserWords, false)) throw std::runtime_error("incremental register fingerprint disagrees with full state"); const auto mix = [&](std::uint64_t value) { key ^= value; key *= 0x100000001b3ull; + shapeKey ^= value; + shapeKey *= 0x100000001b3ull; }; mix(deviceSerial); for (const auto base : {0x008u, 0x088u, 0x0c8u, 0x108u, 0x148u}) { @@ -87,6 +91,7 @@ std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegis for (const auto& snapshot : it->second) mix(reinterpret_cast(snapshot.get())); mix(address - it->first); } + if (shape != nullptr) *shape = shapeKey; return key; } @@ -132,14 +137,15 @@ 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.stencilRefExportEnable != q.stencilRefExportEnable || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false; + if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.frontFaceAllBits != q.frontFaceAllBits || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.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]; const auto& y = b.programs[i]; if (x.binary.stage != y.binary.stage || x.binary.codeAddress != y.binary.codeAddress || x.userDataBase != y.userDataBase || x.firstUserSgpr != y.firstUserSgpr || x.userData != y.userData || x.snapshot != y.snapshot || x.codeOffset != y.codeOffset || x.tailContinuation != y.tailContinuation) return false; } - if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage) return false; + if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask) return false; + if (s.samples != t.samples || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ) return false; return true; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index 751da78ef4..8fdd044922 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #ifdef _WIN32 @@ -29,6 +30,17 @@ constexpr std::uintptr_t InlineImportStackBytes = 256 * 1024; std::atomic gpuMapImportThreads{0}; +void PrintFailure(std::exception_ptr error) { + try { + std::rethrow_exception(error); + } catch (const std::exception& cause) { + std::fprintf(stderr, "[gpu] driver failure: %s\n", cause.what()); + } catch (...) { + std::fprintf(stderr, "[gpu] driver failure with a non-standard exception\n"); + } + std::fflush(stderr); +} + bool StackHoldsImport() { #ifdef _WIN32 ULONG_PTR low = 0; @@ -168,9 +180,11 @@ void Driver::CheckFailure() { void Driver::ReportFailure(std::exception_ptr error) { require(error != nullptr, "null asynchronous failure"); + bool first = false; { std::lock_guard lock(mutex); - if (!failure) failure = error; + first = !failure; + if (first) failure = error; failed.store(true, std::memory_order_release); for (const auto& [handle, output] : outputs) output->Fail(failure); for (auto& [queue, worker] : workers) { @@ -181,6 +195,7 @@ void Driver::ReportFailure(std::exception_ptr error) { worker.unfinishedWrites.clear(); worker.queued.store(0, std::memory_order_release); } + if (first) PrintFailure(error); } changed.notify_all(); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp index dc96d10391..6a76867635 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include #include #include #include @@ -9,6 +10,7 @@ namespace AgcDriver::DriverDetail { std::atomic knownValueEntries{0}, knownValueReads{0}, knownValueVerified{0}, knownValueMismatches{0}; +std::array, CopyPaths> copyPathCounts{}; bool Driver::matchesCopyKernel(std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { static const bool enabled = std::getenv("APS5_NO_COPY_HLE") == nullptr; @@ -27,9 +29,34 @@ bool Driver::matchesCopyKernel(std::span code, const std::v return userData[3] == 0x00014004u && userData[7] == 0x00014004u && userData[11] == 0x0004dfacu && stride(1) == 4 && stride(5) == 4 && stride(9) == 16 && !swizzled(1) && !swizzled(5) && !swizzled(9) && userData[10] >= 1; } +bool MatchesStreamCopyKernel(std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { + static const bool enabled = std::getenv("APS5_NO_COPY_HLE") == nullptr; + if (!enabled || userData.size() != 8 || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1 || !compute.groupIdEnable[0]) return false; + static constexpr std::array kernel{0xd7460008u, 0x04010e08u, 0x8f6a8708u, 0xd76d0009u, 0x0401806au, 0xe00c2000u, 0x80000008u, 0xe00c2000u, 0x80000409u, 0xbf8c3f71u, 0xe01c2000u, 0x80010008u, 0xbf8c3f70u, 0xe01c2000u, 0x80010409u, 0xbf810000u}; + if (code.size() < kernel.size() || !std::equal(kernel.begin(), kernel.end(), code.begin())) return false; + const auto stride = [&](std::size_t word) { return (userData[word] >> 16u) & 0x3fffu; }; + const auto swizzled = [&](std::size_t word) { return ((userData[word] >> 31u) & 1u) != 0; }; + const auto dstSel = userData[3] & 0xfffu; + const bool addTid = ((userData[3] >> 23u) & 1u) != 0; + const auto outOfBounds = (userData[3] >> 28u) & 3u; + const auto type = userData[3] >> 30u; + const auto format = (userData[3] >> 12u) & 0x7fu; + return stride(1) == 16 && stride(5) == 16 && !swizzled(1) && !swizzled(5) && userData[3] == userData[7] && type == 0 && !addTid && outOfBounds == 0 && dstSel == 0xfacu && format == 0x4bu; +} + +std::optional StreamCopyRange(const std::vector& userData, std::uint32_t groups) { + const auto base = [&](std::size_t word) { return userData[word] | (static_cast(userData[word + 1] & 0xffffu) << 32u); }; + const auto elements = std::min(std::uint64_t{groups} * 128u, userData[6]); + const StreamCopy copy{base(0), base(4), static_cast(elements) * 16u}; + if (copy.bytes == 0 || copy.bytes > (16u << 20u) || userData[2] < elements || copy.source % 16 != 0 || copy.destination % 16 != 0) return std::nullopt; + return copy; +} + void Driver::countCopy(int path, std::size_t bytes, std::chrono::steady_clock::time_point started, std::chrono::steady_clock::time_point locked, const VulkanDevice::CopyOutcome* outcome) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; - if (!profile || path < 0 || path >= CopyPaths) return; + if (path < 0 || path >= CopyPaths) return; + copyPathCounts[path].fetch_add(1, std::memory_order_relaxed); + if (!profile) return; using Outcome = VulkanDevice::CopyOutcome; static std::mutex countsMutex; static std::uint64_t counts[CopyPaths] = {}; @@ -92,7 +119,8 @@ bool Driver::knownValueVerify() { } bool Driver::copyBuffer(QueueState& queue, std::uint32_t queueId, std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, const std::shared_ptr& localDevice, std::uint64_t programAddress) { - if (!matchesCopyKernel(code, userData, compute)) return false; + const bool stream = !matchesCopyKernel(code, userData, compute); + if (stream && !MatchesStreamCopyKernel(code, userData, compute)) return false; const auto started = std::chrono::steady_clock::now(); static const std::size_t cpuMax = [] { if (std::getenv("APS5_NO_CPU_COPY") != nullptr) return std::size_t{0}; @@ -106,7 +134,7 @@ bool Driver::copyBuffer(QueueState& queue, std::uint32_t queueId, std::span(userData[word + 1] & 0xffffu) << 32u); }; const auto source = base(0); const auto destination = base(4); - const auto record = base(8); + const auto record = stream ? std::uint64_t{0} : base(8); std::array groups{packet[1], packet[2], packet[3]}; if ((packet[4] & 0x20u) != 0) { for (std::uint32_t axis = 0; axis < 3; ++axis) { @@ -132,22 +160,32 @@ bool Driver::copyBuffer(QueueState& queue, std::uint32_t queueId, std::span(record), 8); + std::size_t bytes = 0; + if (stream) { + const auto range = StreamCopyRange(userData, groups[0]); + if (!range) { + countCopy(CopyShape, 0, started, locked); + return false; + } + bytes = range->bytes; + } else { + const bool recordAccessible = GuestMemory::Accessible(reinterpret_cast(record), 8); - bool published = false; - if (!recordAccessible || Graphics::Recorder::SnapshotWriteOverlaps(record, 8) || Graphics::StorageTexture::FlushPending(record, 8, nullptr, "buffer copy count", Graphics::PublishScope::Whole, &published) || published || labelPending(record, 8)) { - if (traceCopy()) traceCopyPending(recordAccessible ? "count record" : "inaccessible count record", record, 8, source, destination, 0, localDevice); - countCopy(CopyRecordPending, 0, started, locked); - return false; - } - std::uint32_t count = 0, period = 0; - std::memcpy(&count, reinterpret_cast(record), 4); - std::memcpy(&period, reinterpret_cast(record + 4), 4); - const auto bytes = static_cast(count) * 4u; + bool published = false; + if (!recordAccessible || Graphics::Recorder::SnapshotWriteOverlaps(record, 8) || Graphics::StorageTexture::FlushPending(record, 8, nullptr, "buffer copy count", Graphics::PublishScope::Whole, &published) || published || labelPending(record, 8)) { + if (traceCopy()) traceCopyPending(recordAccessible ? "count record" : "inaccessible count record", record, 8, source, destination, 0, localDevice); + countCopy(CopyRecordPending, 0, started, locked); + return false; + } + std::uint32_t count = 0, period = 0; + std::memcpy(&count, reinterpret_cast(record), 4); + std::memcpy(&period, reinterpret_cast(record + 4), 4); + bytes = static_cast(count) * 4u; - if (count == 0 || period < count || groups[0] != (count + 63u) / 64u || count > userData[2] || count > userData[6] || bytes > (16u << 20u) || source % 4 != 0 || destination % 4 != 0 || (record < destination + bytes && destination < record + 8)) { - countCopy(CopyShape, bytes, started, locked); - return false; + if (count == 0 || period < count || groups[0] != (count + 63u) / 64u || count > userData[2] || count > userData[6] || bytes > (16u << 20u) || source % 4 != 0 || destination % 4 != 0 || (record < destination + bytes && destination < record + 8)) { + countCopy(CopyShape, bytes, started, locked); + return false; + } } if (source < destination + bytes && destination < source + bytes) { countCopy(CopyOverlapping, bytes, started, locked); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 9340e0401c..ef2313522c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -257,6 +257,7 @@ void Driver::execute(const Submission& submission) { } if (opcode == 0x15) timed(&WorkerProfile::dispatchMs, [&] { dispatch(queue, packet, submission); }); else timed(&WorkerProfile::dispatchMs, [&] { dispatchIndirect(queue, packet, submission); }); + Pm4::NoteDispatchRegisters(packet, queue); Graphics::Recorder::CountRecordedWork(); finishDispatchPacket(opcode == 0x16); } else if (opcode == 0x3c || opcode == 0x93) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index 4f3eb61891..3609990f90 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -192,6 +192,12 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue } } + if (header == FlipPacketHeader) { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + static_cast(Graphics::StorageTexture::StoreSmallPending("flip")); + } + static const bool syncFlip = std::getenv("APS5_SYNC_FLIP") != nullptr; const bool drains = drainAll ? ((Pm4::AccessesMemory(header) && opcode != 0x16) || opcode == 0x42 || opcode == 0x46 || opcode == 0x58 || header == FlipPacketHeader) : (!wroteOnGpu && !orderedAlready && (opcode == 0x49 || opcode == 0x37 || opcode == 0x40 || opcode == 0x45 || opcode == 0x50 || opcode == 0x83 || sampleDump || (drawPacket && drawDrain) || (header == FlipPacketHeader && syncFlip))); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp index db3f64fa8d..3d428973f2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp @@ -298,6 +298,7 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue) { if (APS5_ENABLE_TIMING_LOG) submission.validatedAt = std::chrono::steady_clock::now(); waitForFlipRoom(submission); holdFlipBehindWorker(submission); + AwaitRegistrationPreparation(); if (APS5_ENABLE_TIMING_LOG) submission.roomReadyAt = std::chrono::steady_clock::now(); static const bool trace = std::getenv("APS5_TRACE_GPU") != nullptr; if (trace) std::fprintf(stderr, "[gpu] %.1f submit queue=0x%x dwords=%zu at %p\n", TraceMs(), queue, submission.commands.size(), static_cast(descriptor.addr)); 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 9a4a6e63eb..30a4685d05 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -29,9 +30,11 @@ #ifdef __GLIBC__ #include #endif +#include #include #include #include +#include "Optimization/ShaderStageInputInfo.hpp" #include "RdnaDecoder/RdnaInstructionDecoder.hpp" namespace AgcDriver::DriverDetail { @@ -538,14 +541,48 @@ std::vector ReadUserDataCallee(std::uint64_t address, std::uint32 throw std::runtime_error("its callee at " + hexText(address) + " does not return within " + std::to_string(limitWords * 4u) + " bytes"); } -std::vector LinkUserDataCallees(const ShaderRecompiler::RecompileRequest& request) { +std::vector ReadGuestBytes(std::uint64_t address, std::uint64_t bytes, const char* what) { + if (address > UINTPTR_MAX - bytes || !GuestMemory::Accessible(reinterpret_cast(static_cast(address)), static_cast(bytes))) { + throw std::runtime_error(std::string("its ") + what + " at " + hexText(address) + " + " + hexText(bytes) + " is not readable"); + } + std::vector data(static_cast(bytes)); + GuestMemory::Read(address, std::as_writable_bytes(std::span(data))); + return data; +} + +std::vector ReadTableCallees(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::TableCall& call) { + constexpr std::uint64_t limitBytes = 64u << 20u; + constexpr std::size_t limitCallees = 64u; + const auto slot = call.userDataRegister - request.context.userDataBaseRegister; + const auto pointer = static_cast(request.context.userData[slot]) | static_cast(request.context.userData[slot + 1u]) << 32u; + const auto words = ReadGuestBytes(pointer + call.descriptorOffset, 16u, "call table descriptor"); + ShaderRecompiler::ShaderBufferResource descriptor{}; + std::memcpy(descriptor.fields.data(), words.data(), words.size()); + const auto bytes = descriptor.GetSize(); + if (bytes > limitBytes) throw std::runtime_error("its call table of " + std::to_string(bytes) + " bytes is larger than the " + std::to_string(limitBytes) + " bytes that are searched for callees"); + const auto table = bytes == 0u ? std::vector{} : ReadGuestBytes(descriptor.Base48(), bytes, "call table"); + std::set targets; + for (std::uint64_t offset = call.entryOffset; offset + 8u <= bytes; offset += call.entryStride) { + std::uint64_t target = 0; + std::memcpy(&target, table.data() + offset, sizeof(target)); + if (target != 0u) targets.insert(target); + if (call.entryStride == 0u) break; + } + if (targets.size() > limitCallees) throw std::runtime_error("its call table holds " + std::to_string(targets.size()) + " distinct targets; at most " + std::to_string(limitCallees) + " are linked"); + std::vector callees; + for (const auto target : targets) callees.push_back({target, ReadUserDataCallee(target, call.linkRegister)}); + return callees; +} + +ShaderRecompiler::LinkedCalls LinkUserDataCallees(const ShaderRecompiler::RecompileRequest& request) { if (request.shader.stage != ShaderRecompiler::ShaderStage::Compute) return {}; const auto code = request.shader.code; if (std::none_of(code.begin(), code.end(), [](std::uint32_t word) { return (word & 0xff80ff00u) == 0xbe802100u; })) return {}; const auto program = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(code); - const ShaderRecompiler::SwappcInfo swappc{false, request.context.userDataBaseRegister, static_cast(request.context.userData.size())}; + const ShaderRecompiler::SwappcInfo swappc{false, request.context.userDataBaseRegister, static_cast(request.context.userData.size()), request.context.waveSize}; const auto calls = ShaderRecompiler::FindUserDataCalls(program, swappc); - if (calls.empty()) return {}; + const auto tableCalls = ShaderRecompiler::FindTableCalls(program, swappc); + if (calls.empty() && tableCalls.empty()) return {}; std::vector> callees; for (const auto& call : calls) { const auto slot = call.userDataRegister - swappc.userDataBaseRegister; @@ -557,7 +594,15 @@ std::vector LinkUserDataCallees(const ShaderRecompiler::Recompile std::to_string(call.userDataRegister + 1u) + "] at program counter " + hexText(call.programCounter) + ", but " + error.what()); } } - return ShaderRecompiler::LinkUserDataCalls(program, calls, callees); + std::vector> tableCallees; + for (const auto& call : tableCalls) { + try { + tableCallees.push_back(ReadTableCallees(request, call)); + } catch (const std::exception& error) { + throw std::runtime_error("AGC driver: compute shader " + hexText(request.shader.codeAddress) + " calls a target loaded from a table at program counter " + hexText(call.programCounter) + ", but " + error.what()); + } + } + return ShaderRecompiler::LinkCalls(program, calls, callees, tableCalls, tableCallees); } } @@ -567,9 +612,12 @@ ShaderRecompiler::PreparedShaderInvocation InvocationFor(const ShaderSnapshot& s const auto code = std::span(snapshot.code).subspan(codeOffset); require(request.shader.code.data() == code.data() && request.shader.code.size() == code.size(), "prepared invocation does not refer to registered code"); auto linked = LinkUserDataCallees(request); - if (linked.empty()) linked = LinkGraphicsUserDataTail(request, snapshot); + if (linked.code.empty()) linked.code = LinkGraphicsUserDataTail(request, snapshot); auto source = request; - if (!linked.empty()) source.shader.code = linked; + if (!linked.code.empty()) { + source.shader.code = linked.code; + source.context.linkedCallFallbacks = linked.fallbacks; + } auto invocationRequest = source; struct PreparedKeyStorage {}; auto& key = HostThreadLocal, PreparedKeyStorage>(); @@ -579,7 +627,7 @@ ShaderRecompiler::PreparedShaderInvocation InvocationFor(const ShaderSnapshot& s for (const auto& entry : snapshot.prepared->entries) { if (entry.codeOffset != codeOffset) continue; invocationRequest.shader.code = ShaderRecompiler::GetPreparedCode(*entry.handle); - if (!linked.empty() && !std::ranges::equal(invocationRequest.shader.code, linked)) continue; + if (!linked.code.empty() && !std::ranges::equal(invocationRequest.shader.code, linked.code)) continue; if (auto invocation = ShaderRecompiler::PreparedShaderInvocation::TryCreate(invocationRequest, entry.handle, key)) return std::move(*invocation); } ReportPreparedAtUse(snapshot, source); @@ -894,8 +942,10 @@ public: { std::unique_lock lock(mutex); if (threads != 0 && !closed) { - if (prepared != nullptr && prepared->demanded.load()) tasks.push_front({std::move(task), prepared}); - else tasks.push_back({std::move(task), prepared}); + const auto sequence = ++submitted; + if (prepared != nullptr && prepared->demanded.load()) tasks.push_front({std::move(task), prepared, sequence}); + else tasks.push_back({std::move(task), prepared, sequence}); + unfinished.insert(sequence); lock.unlock(); ready.notify_one(); return; @@ -912,6 +962,12 @@ public: std::rotate(tasks.begin(), found, std::next(found)); } + void AwaitSubmitted() { + std::unique_lock lock(mutex); + const auto ticket = submitted; + idle.wait(lock, [&] { return unfinished.empty() || *unfinished.begin() > ticket; }); + } + void Close() { std::unique_lock lock(mutex); closed = true; @@ -945,14 +1001,16 @@ private: pool.tasks.pop_front(); ++pool.running; } + const auto sequence = task.sequence; task.function(); task = {}; bool drained = false; { std::lock_guard lock(pool.mutex); --pool.running; + pool.unfinished.erase(sequence); drained = pool.tasks.empty() && pool.running == 0; - if (drained) pool.idle.notify_all(); + pool.idle.notify_all(); } #ifdef __GLIBC__ if (drained) trimFreedMemory(); @@ -964,6 +1022,7 @@ private: struct Task { std::function function; const PreparedShaders* prepared = nullptr; + std::uint64_t sequence = 0; }; std::mutex mutex; @@ -972,6 +1031,8 @@ private: std::deque tasks; unsigned threads = 0; unsigned running = 0; + std::uint64_t submitted = 0; + std::set unfinished; bool closed = false; }; @@ -1017,6 +1078,10 @@ void PrioritizeRegistrationPreparation(PreparedShaders& prepared) { if (auto* pool = RegistrationPreparePool::Existing()) pool->Prioritize(prepared); } +void AwaitRegistrationPreparation() { + if (auto* pool = RegistrationPreparePool::Existing()) pool->AwaitSubmitted(); +} + void SubmitLibraryPrebuild(std::function task) { RegistrationPreparePool::Prebuild().Submit(std::move(task)); } @@ -1271,6 +1336,22 @@ void Driver::ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std:: transaction.Commit(); } +void Driver::SettleShaderPreparations(std::span stages) { + std::vector> snapshots; + { + std::lock_guard lock(mutex); + if (shaders == nullptr) return; + for (const auto* shader : stages) { + if (shader == nullptr) continue; + if (auto snapshot = RegisteredHeader(*shaders, shader)) snapshots.push_back(std::move(snapshot)); + } + } + for (const auto& snapshot : snapshots) { + std::unique_lock lock(snapshot->prepared->mutex); + AwaitRegisteredPreparation(*snapshot->prepared, lock); + } +} + void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target) { ShaderPreparationTransaction transaction; require(fragment.get() != &front, "rectangle stages refer to the same shader"); @@ -1441,7 +1522,8 @@ void Driver::RegisterShader(const Shader* shader) { } -extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix() { +extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix(const Shader* const* shaders, std::size_t count) { + AgcDriver::DriverDetail::Driver::Get().SettleShaderPreparations({shaders, count}); return new AgcDriver::DriverDetail::ShaderPreparationTransaction(); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index e2a670e418..cd907d4a3d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -385,7 +385,7 @@ struct PageSpan { } std::uint8_t load(std::uintptr_t address) const { - return std::atomic_ref(pages[(address - base) / PageBytes]).load(std::memory_order_relaxed); + return std::atomic_ref(const_cast(pages[(address - base) / PageBytes])).load(std::memory_order_relaxed); } void store(std::uintptr_t address, std::uint8_t value) { @@ -398,11 +398,11 @@ struct PageSpan { const auto stop = std::min((std::min(limit, base + size) - base + PageBytes - 1) / PageBytes, size / PageBytes); const std::uint64_t wide = 0x0101010101010101ull * value; while (index < stop) { - if (index % 8 == 0 && index + 8 <= stop && std::atomic_ref(*reinterpret_cast(pages + index)).load(std::memory_order_relaxed) == wide) { + if (index % 8 == 0 && index + 8 <= stop && std::atomic_ref(*reinterpret_cast(const_cast(pages + index))).load(std::memory_order_relaxed) == wide) { index += 8; continue; } - if (std::atomic_ref(pages[index]).load(std::memory_order_relaxed) != value) break; + if (std::atomic_ref(const_cast(pages[index])).load(std::memory_order_relaxed) != value) break; ++index; } return std::min(limit, base + index * PageBytes); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp index 862c10d848..c913b43155 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp @@ -221,6 +221,7 @@ void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const { std::lock_guard gpu(gpuMutex); gpuRanges.clear(); + gpuLost = 0; gpuRanges.reserve(GpuRangeCapacity); } state.presents.push_back(now); @@ -256,7 +257,7 @@ void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const if (state.gpuClock && device != nullptr && device->CalibrateClock(clock.gpuNs, clock.cpu)) { std::lock_guard gpu(gpuMutex); gpuSlices = TraceFormat::GpuSlices(gpuRanges, *state.gpuClock, clock, state.presents.front()); - snapshots.push_back({"gpu", gpuSlices, {}, gpuDropped}); + snapshots.push_back({"gpu", gpuSlices, {}, gpuLost != 0, gpuLost}); } const auto process = processId(); auto summary = identity(width, height, device); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index e7b12a29d8..bdee46ee16 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -131,8 +131,11 @@ void copyMemory(std::uint64_t source, std::uint64_t destination, std::size_t byt GuestMemory::Write(destination, copySource(source, bytes, immediate)); } +constexpr std::uint32_t CopyDataRegisterSource = 0; constexpr std::uint32_t CopyDataCachePolicy = (3u << 13u) | (3u << 25u); +constexpr std::uint32_t ShaderRegisterBase = 0x2c00; constexpr std::uint32_t ContextRegisterBase = 0xa000; +constexpr std::uint32_t UserConfigRegisterBase = 0xc000; std::uint32_t copyDataCount(std::span packet) { return (packet[1] & 0x10000u) != 0 ? 2u : 1u; @@ -142,8 +145,27 @@ std::uint64_t gpuClockCount() { return static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count() / 10); } -std::vector copyDataSource(std::span packet, std::size_t bytes) { +std::uint32_t registerBankBase(std::uint32_t offset) { + if (offset - ShaderRegisterBase < 0x400u) return ShaderRegisterBase; + if (offset - ContextRegisterBase < 0x400u) return ContextRegisterBase; + if (offset - UserConfigRegisterBase < 0x4000u) return UserConfigRegisterBase; + return 0; +} + +std::vector copyDataSource(std::span packet, const QueueState& queue, std::size_t bytes) { const auto source = CopyDataSource(packet); + if (source == CopyDataRegisterSource) { + std::vector data(bytes); + for (std::size_t word = 0; word < bytes / 4; ++word) { + const auto offset = packet[2] + static_cast(word); + const auto base = registerBankBase(offset); + const auto& bank = base == ShaderRegisterBase ? queue.shader : base == ContextRegisterBase ? queue.context : queue.userConfig; + if (base == 0 || !bank.contains(offset - base)) throw std::runtime_error("COPY_DATA source register 0x" + ToHex(offset) + " has no known value"); + const auto value = bank.at(offset - base); + for (std::size_t i = 0; i < 4; ++i) data[word * 4 + i] = static_cast(value >> (i * 8)); + } + return data; + } if (source != CopyDataGpuClockSource) return copySource(address(packet[2], packet[3]), bytes, CopyDataImmediate(source)); const auto value = gpuClockCount(); std::vector data(bytes); @@ -332,10 +354,10 @@ void Validate(std::span packet, std::uint32_t queue) { require((packet[8] & 3u) == 0 && packet[8] >= (opcode == 0x2c ? 16u : 20u), "invalid indirect draw stride"); require((packet[9] & ~0x20u) == (opcode == 0x2c ? 2u : 0u), "unsupported indirect draw initiator"); break; - case 0x15: size(5); if ((packet[4] & ~0xa066u) != 0x1u || (packet[4] & 0x22u) == 0x22u) throw std::runtime_error("dispatch modifiers 0x" + ToHex(packet[4]) + " are not implemented"); break; + case 0x15: size(5); if ((packet[4] & ~0xa06eu) != 0x1u || (packet[4] & 0x22u) == 0x22u) throw std::runtime_error("dispatch modifiers 0x" + ToHex(packet[4]) + " are not implemented"); break; case 0x16: require(packet.size() == 3 || packet.size() == 4, "invalid indirect dispatch size"); - require((packet.back() & ~0xa064u) == 0x1u, "indirect dispatch modifiers are not implemented"); + require((packet.back() & ~0xa06cu) == 0x1u, "indirect dispatch modifiers are not implemented"); break; case 0x22: size(5); @@ -474,7 +496,8 @@ void Validate(std::span packet, std::uint32_t queue) { break; } require(destination == 2 || destination == 4 || destination == 10, "COPY_DATA GDS or performance-counter destination is not implemented"); - require(source == 2 || source == 4 || source == 5 || source == 10 || source == 11 || source == CopyDataGpuClockSource, "COPY_DATA register, GDS or reference-clock source is not implemented"); + require(source == 2 || source == 4 || source == 5 || source == 10 || source == 11 || source == CopyDataGpuClockSource || source == CopyDataRegisterSource, "COPY_DATA GDS or reference-clock source is not implemented"); + require(source != CopyDataRegisterSource || (packet[3] == 0 && registerBankBase(packet[2]) != 0 && registerBankBase(packet[2]) == registerBankBase(packet[2] + copyDataCount(packet) - 1)), "COPY_DATA from a register outside the shader, context and user config registers is not implemented"); require(source < 10 || source == CopyDataGpuClockSource || ((packet[1] & 0x10000u) == 0 && packet[3] == 0), "64-bit immediate COPY_DATA is not implemented"); break; } @@ -601,10 +624,11 @@ std::optional ResolveStore(std::span packet, co switch ((packet[0] >> 8u) & 0xffu) { case 0x40: { if (packet.size() < 6 || CopyDataToRegister(packet)) return std::nullopt; + if (CopyDataSource(packet) == CopyDataRegisterSource) return std::nullopt; const std::size_t bytes = CopyDataBytes(packet); const auto destination = address(packet[4], packet[5]); if (!fits(destination, bytes)) return std::nullopt; - return StoreWrite{destination, {}, copyDataSource(packet, bytes)}; + return StoreWrite{destination, {}, copyDataSource(packet, queue, bytes)}; } case 0x50: { if (packet.size() < 7 || dmaDestination(packet) == DmaSelectGds) return std::nullopt; @@ -899,6 +923,15 @@ std::vector ReadIndirectRegisters(std::span return pairs; } +void NoteDispatchRegisters(std::span packet, QueueState& queue) { + const bool direct = ((packet[0] >> 8u) & 0xffu) == 0x15u; + queue.shader.insert_or_assign(0x200, packet.back()); + for (std::uint32_t i = 0; i < 3; ++i) { + if (direct) queue.shader.insert_or_assign(0x201 + i, packet[1 + i]); + else queue.shader.erase(0x201 + i); + } +} + void ExecuteIndirectRegisters(std::span packet, std::span pairs, QueueState& queue) { const auto opcode = (packet[0] >> 8u) & 0xffu; require(packet.size() == 5 && IndirectRegisterOpcode(opcode), "expected indirect register packet"); @@ -1071,7 +1104,7 @@ void Execute(std::span packet, QueueState& queue) { return; } const auto destination = address(packet[4], packet[5]); - const auto data = copyDataSource(packet, CopyDataBytes(packet)); + const auto data = copyDataSource(packet, queue, CopyDataBytes(packet)); GuestMemory::CheckRange(reinterpret_cast(destination), data.size(), 1, true); GuestMemory::Write(destination, data); return; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp index 063205cbee..7dc7a4640c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp @@ -30,6 +30,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta const auto* barycentric = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_BARYCENTRIC_FEATURES_KHR); const auto* mesh = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_FEATURES_EXT); const auto* clock = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_CLOCK_FEATURES_KHR); + const auto* drawParameters = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES); const auto* maintenance8 = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_8_FEATURES_KHR); Require(target.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version, "shader device profile has an incompatible BDA ABI"); Require(bda != nullptr && bda->bufferDeviceAddress == VK_TRUE && core.shaderInt64 == VK_TRUE, "shader runtime requires enabled bufferDeviceAddress and shaderInt64"); @@ -64,6 +65,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta const bool meshEnabled = mesh != nullptr && mesh->meshShader == VK_TRUE; checkFeature(spv::CapabilityFragmentBarycentricKHR, barycentricEnabled, "fragmentShaderBarycentric"); checkFeature(spv::CapabilityMeshShadingEXT, meshEnabled, "meshShader"); + checkFeature(spv::CapabilityDrawParameters, drawParameters != nullptr && drawParameters->shaderDrawParameters == VK_TRUE, "shaderDrawParameters"); checkFeature(spv::CapabilityShaderClockKHR, clock != nullptr && clock->shaderSubgroupClock == VK_TRUE && clock->shaderDeviceClock == VK_TRUE, "shader clock"); Require(!target.fragmentShaderBarycentricEnabled || barycentricEnabled, "shader device profile exposes disabled barycentrics"); Require(!target.mesh || meshEnabled, "shader device profile exposes disabled mesh shaders"); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index a77efedf17..e4981a540a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -44,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -283,6 +284,8 @@ struct VulkanDevice::State { bool narrowSubgroupClock = false; bool float64DenormPreserve = false; bool float16DenormPreserve = false; + bool demoteToHelperInvocation = false; + bool earlyFragmentSampleMaskTestBeforeSampleCounting = false; // VK_EXT_descriptor_indexing with non-uniform image array indexing (bindless image tables in // graphics stages, and compute workgroups wider than a wave). bool descriptorIndexing = false; @@ -355,6 +358,7 @@ struct VulkanDevice::State { // references to its objects. std::mutex computePipelinesMutex; std::map> computePipelines; + std::map>> computePipelinesBuilding; std::unique_ptr colorTransfer; std::shared_ptr bufferPool; std::unique_ptr emptyBuffer; @@ -707,12 +711,14 @@ struct VulkanDevice::State { { std::lock_guard pipelines(computePipelinesMutex); computePipelines.clear(); + computePipelinesBuilding.clear(); } // Every ShaderResources (kept by the recorder or the resource cache) is gone now, so the // sets and samplers they borrowed can go. copiedWriters->clear(); resourceCache.Clear(); Graphics::ClearCachedTextures(device); + Graphics::ClearSnapshotCopies(device); Graphics::ClearImageMirrors(device); Graphics::ClearHostImports(device); patternBuffers.clear(); @@ -1144,6 +1150,20 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->narrowSubgroupClock = NarrowSubgroupClock(driver.driverID, state->properties.deviceID, state->properties.deviceName); APS5_LOG_OUT("Shader clock driver=%d narrowSubgroupClock=%d", static_cast(driver.driverID), state->narrowSubgroupClock ? 1 : 0); } + VkPhysicalDeviceShaderDemoteToHelperInvocationFeaturesEXT demoteFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DEMOTE_TO_HELPER_INVOCATION_FEATURES_EXT}; + if (hasExtension(VK_EXT_SHADER_DEMOTE_TO_HELPER_INVOCATION_EXTENSION_NAME)) { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &demoteFeatures}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); + state->demoteToHelperInvocation = demoteFeatures.shaderDemoteToHelperInvocation == VK_TRUE; + } + demoteFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DEMOTE_TO_HELPER_INVOCATION_FEATURES_EXT}; + demoteFeatures.shaderDemoteToHelperInvocation = VK_TRUE; + if (hasExtension(VK_KHR_MAINTENANCE_5_EXTENSION_NAME)) { + VkPhysicalDeviceMaintenance5PropertiesKHR maintenance5{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_PROPERTIES_KHR}; + VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &maintenance5}; + state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); + state->earlyFragmentSampleMaskTestBeforeSampleCounting = maintenance5.earlyFragmentSampleMaskTestBeforeSampleCounting == VK_TRUE; + } std::vector deviceExtensions; if (window != nullptr) deviceExtensions.assign(presentationExtensions.begin(), presentationExtensions.end()); if (window != nullptr && hasExtension(VK_EXT_HDR_METADATA_EXTENSION_NAME)) { @@ -1201,6 +1221,11 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->spirvExtensions.push_back("SPV_EXT_shader_viewport_index_layer"); state->viewportIndexLayer = true; } + if (state->demoteToHelperInvocation) { + deviceExtensions.push_back(VK_EXT_SHADER_DEMOTE_TO_HELPER_INVOCATION_EXTENSION_NAME); + state->capabilities.push_back(spv::CapabilityDemoteToHelperInvocation); + state->spirvExtensions.push_back("SPV_EXT_demote_to_helper_invocation"); + } deviceExtensions.push_back(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME); state->capabilities.push_back(spv::CapabilitySignedZeroInfNanPreserve); state->spirvExtensions.push_back("SPV_KHR_float_controls"); @@ -1415,6 +1440,14 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : enabled.tessellationShader = available.tessellationShader; state->tessellationShader = enabled.tessellationShader == VK_TRUE; if (state->tessellationShader) state->capabilities.push_back(3); + VkPhysicalDeviceShaderDrawParametersFeatures drawParametersFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES}; + { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &drawParametersFeatures}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); + } + const bool drawParameters = drawParametersFeatures.shaderDrawParameters == VK_TRUE; + drawParametersFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES, nullptr, VK_TRUE}; + if (drawParameters) state->capabilities.push_back(spv::CapabilityDrawParameters); APS5_LOG_OUT("Vulkan features tessellationAvailable=%u mesh=%u depthClip=%u depthRangeUnrestricted=%u pipelineLibrary=%u", static_cast(state->tessellationShader), static_cast(state->meshShader), static_cast(state->depthClipControl), static_cast(state->depthRangeUnrestricted), static_cast(state->graphicsPipelineLibrary)); require(available.samplerAnisotropy && available.textureCompressionBC, "device lacks sampler anisotropy or BC texture compression support required for texture sampling"); enabled.samplerAnisotropy = VK_TRUE; @@ -1598,6 +1631,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : imageAtomicInt64Features.pNext = byteFeatures.pNext; byteFeatures.pNext = &imageAtomicInt64Features; } + if (state->demoteToHelperInvocation) { + demoteFeatures.pNext = byteFeatures.pNext; + byteFeatures.pNext = &demoteFeatures; + } VkPhysicalDeviceImageRobustnessFeaturesEXT imageRobustnessFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_ROBUSTNESS_FEATURES_EXT, nullptr, VK_TRUE}; if (imageRobustness) { imageRobustnessFeatures.pNext = byteFeatures.pNext; @@ -1607,6 +1644,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : descriptorIndexingFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &descriptorIndexingFeatures; } + if (drawParameters) { + drawParametersFeatures.pNext = byteFeatures.pNext; + byteFeatures.pNext = &drawParametersFeatures; + } // Timeline semaphores let a queue worker wait for recorded batches without holding the GPU mutex // (see Recorder::WaitSerial). The instance is 1.1, so the KHR extension is used even on 1.2+ // devices. Debug aid: APS5_NO_TIMELINE=1 leaves it off (drains wait under the mutex as before). @@ -2267,6 +2308,15 @@ VulkanDevice::CopyVerification VulkanDevice::CopyVerifyCounts() { return {copiesVerified.load(std::memory_order_relaxed), copySourceChanged.load(std::memory_order_relaxed), copyDestinationChanged.load(std::memory_order_relaxed), copyReaderMissed.load(std::memory_order_relaxed)}; } +namespace { +std::atomic computePipelinesCreated{0}; +std::atomic computePipelinesCreatedLocked{0}; +} + +VulkanDevice::ComputePipelineCreation VulkanDevice::ComputePipelineCounts() { + return {computePipelinesCreated.load(std::memory_order_relaxed), computePipelinesCreatedLocked.load(std::memory_order_relaxed)}; +} + VulkanDevice::CopyAliasing VulkanDevice::CopyAliasCounts() { return {copiesAliased.load(std::memory_order_relaxed), copyAliasCreated.load(std::memory_order_relaxed), copyAliasNoSource.load(std::memory_order_relaxed), copyAliasShape.load(std::memory_order_relaxed), copyAliasRefused.load(std::memory_order_relaxed)}; } @@ -3414,6 +3464,8 @@ Graphics::Context VulkanDevice::buildContext() const { context.bufferInt64Atomics = state->bufferInt64Atomics; context.placeholderImages = state->placeholderImages.get(); context.viewportIndexLayer = state->viewportIndexLayer; + context.demoteToHelperInvocation = state->demoteToHelperInvocation; + context.earlyFragmentSampleMaskTestBeforeSampleCounting = state->earlyFragmentSampleMaskTestBeforeSampleCounting; context.primitiveListRestart = state->primitiveListRestart; context.imageViewMinLod = state->imageViewMinLod; context.pipelineExecutableInfo = state->pipelineExecutableInfo; @@ -3552,6 +3604,8 @@ struct PreparedDispatch { // The stage-A pre-sync (see PrepareDispatch): the newest recorder serial waited for without the // mutex, whose batches the dispatch reaps under it before stage B (0: nothing waited for). std::uint64_t presyncSerial = 0; + std::shared_ptr objects; + std::vector describedLayoutKey; }; struct RecipeHit { @@ -3990,7 +4044,7 @@ std::uint64_t VulkanDevice::presync(std::span VulkanDevice::PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span snapshots, std::uint64_t dispatchThreads) { +std::shared_ptr VulkanDevice::PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span snapshots, std::uint64_t dispatchThreads, std::uint64_t programAddress) { ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); // APS5_LOCKED_BUILD=1: the whole build under the mutex, as before the split. static const bool lockedBuild = std::getenv("APS5_LOCKED_BUILD") != nullptr; @@ -4044,6 +4098,18 @@ std::shared_ptr VulkanDevice::PrepareDispatch(const ShaderReco phase(PreparedDispatch::PreparePresync); } if (profile) prepared->prepareMs = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + const auto& built = cached != nullptr ? *cached : *prepared->resources; + const std::array shaders{{compute}}; + const auto pushStages = Graphics::PushConstantStages(shaders); + if (pushStages != 0 && state->properties.limits.maxPushConstantsSize < Graphics::ComputePushConstantBytes) return prepared; + if (built.Layout() != VK_NULL_HANDLE) { + const auto layouts = built.SetLayouts(); + prepared->objects = computePipeline(context, shader, layouts.main, layouts.mainKey, pushStages, programAddress); + } else if (context.descriptorCache != nullptr) { + const auto described = Graphics::DescribeSetLayouts(context, shaders); + prepared->describedLayoutKey = described.mainKey; + prepared->objects = computePipeline(context, shader, described.layouts.main, described.mainKey, pushStages, programAddress); + } return prepared; } @@ -4321,6 +4387,94 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { recordStep(PhaseRecordCompletion); } +std::shared_ptr VulkanDevice::computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress) { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + const std::uint64_t pipelineKey = shader.PipelineVariantId() != 0 ? (shader.PipelineVariantId() << 1u) | (pushStages != 0 ? 1u : 0u) : 0u; + std::promise> building; + if (pipelineKey != 0) { + std::shared_future> other; + { + std::lock_guard pipelines(state->computePipelinesMutex); + if (const auto found = state->computePipelines.find(pipelineKey); found != state->computePipelines.end()) return found->second; + if (const auto pending = state->computePipelinesBuilding.find(pipelineKey); pending != state->computePipelinesBuilding.end()) other = pending->second; + else state->computePipelinesBuilding.emplace(pipelineKey, building.get_future().share()); + } + if (other.valid()) return other.get(); + } + const auto finish = [&](std::shared_ptr built, std::exception_ptr error) { + if (pipelineKey == 0) return built; + { + std::lock_guard pipelines(state->computePipelinesMutex); + if (built != nullptr) built = state->computePipelines.try_emplace(pipelineKey, built).first->second; + state->computePipelinesBuilding.erase(pipelineKey); + } + if (error != nullptr) building.set_exception(error); + else building.set_value(built); + return built; + }; + std::shared_ptr objects; + try { + const auto start = std::chrono::steady_clock::now(); + objects = std::make_shared(); + objects->device = state->device; + objects->destroyModule = state->DeviceFunction("vkDestroyShaderModule"); + objects->destroyLayout = state->DeviceFunction("vkDestroyPipelineLayout"); + objects->destroyPipeline = state->DeviceFunction("vkDestroyPipeline"); + VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + moduleInfo.codeSize = shader.spirv.size() * sizeof(std::uint32_t); + moduleInfo.pCode = shader.spirv.data(); + check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects->module), "vkCreateShaderModule"); + const std::array setLayouts{setLayout, shader.workgroupMemoryDwords != 0 ? state->workgroupMemoryLayout : VK_NULL_HANDLE}; + const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, Graphics::ComputePushConstantBytes}; + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = shader.workgroupMemoryDwords != 0 ? ShaderRecompiler::WorkgroupMemoryDescriptorSet + 1 : 1; + layoutInfo.pSetLayouts = setLayouts.data(); + layoutInfo.pushConstantRangeCount = pushStages != 0 ? 1 : 0; + layoutInfo.pPushConstantRanges = pushStages != 0 ? &push : nullptr; + check(state->DeviceFunction("vkCreatePipelineLayout")(state->device, &layoutInfo, nullptr, &objects->layout), "vkCreatePipelineLayout"); + const VkPipelineCreateFlags pipelineFlags = (context.pipelineExecutableInfo ? VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR : 0) | VK_PIPELINE_CREATE_DISPATCH_BASE_BIT; + const Graphics::PipelineSpecialization specialization(shader); + const auto subgroupSize = state->computeWave32 && (shader.hostSubgroupSize == 32u || shader.hostSubgroupSize > state->maxComputeSubgroupSize) ? std::min(shader.hostSubgroupSize, state->maxComputeSubgroupSize) : 0u; + if (const char* directory = std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); directory != nullptr && *directory != '\0' && CaptureComputePipeline(shader.spirv.size())) { + ComputePipelineCapture capture; + capture.spirv = shader.spirv.Copy(); + ComputeDescriptorSetLayout resourcesLayout; + const bool updateAfterBind = layoutKey.size() % 4u == 1u; + resourcesLayout.flags = updateAfterBind ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0; + for (std::size_t index = 0; index + 4u <= layoutKey.size(); index += 4u) { + const auto type = static_cast(layoutKey[index + 1u]); + resourcesLayout.bindings.push_back({layoutKey[index], type, layoutKey[index + 2u], layoutKey[index + 3u], updateAfterBind && type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0u}); + } + capture.sets.push_back(std::move(resourcesLayout)); + if (shader.workgroupMemoryDwords != 0) capture.sets.push_back({0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}); + if (pushStages != 0) capture.pushConstants.push_back(push); + for (const auto& value : shader.specialization) capture.specialization.push_back({value.id, value.value}); + capture.requiredSubgroupSize = subgroupSize; + capture.flags = pipelineFlags; + capture.deviceName = state->properties.deviceName; + capture.vendorId = state->properties.vendorID; + capture.deviceId = state->properties.deviceID; + capture.driverVersion = state->properties.driverVersion; + capture.apiVersion = state->properties.apiVersion; + std::copy_n(state->properties.pipelineCacheUUID, VK_UUID_SIZE, capture.pipelineCacheUuid.begin()); + const auto path = capture.Write(directory); + std::fprintf(stderr, "[compute-capture] %s\n", path.string().c_str()); + } + if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] creating a pipeline for %zu SPIR-V words (program 0x%llx)\n", shader.spirv.size(), static_cast(programAddress)); + CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization); + objects->destroyModule(objects->device, objects->module, nullptr); + objects->module = VK_NULL_HANDLE; + computePipelinesCreated.fetch_add(1, std::memory_order_relaxed); + if (GuestMemory::GpuMutex().HeldByThisThread()) computePipelinesCreatedLocked.fetch_add(1, std::memory_order_relaxed); + Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline); + if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] created a pipeline for %zu SPIR-V words in %.0f ms (program 0x%llx)\n", shader.spirv.size(), std::chrono::duration(std::chrono::steady_clock::now() - start).count(), static_cast(programAddress)); + } catch (...) { + finish(nullptr, std::current_exception()); + throw; + } + return finish(std::move(objects), nullptr); +} + VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipeOut) { state->recorder->BoundKeptBytes(); ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); @@ -4344,8 +4498,8 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec // Pipelines are shared by dispatches of one compiled variant; descriptor set layouts built from the // same bindings are compatible, so the pipeline layout of the first dispatch serves them all. const std::uint64_t pipelineKey = shader.PipelineVariantId() != 0 ? (shader.PipelineVariantId() << 1u) | (pushStages != 0 ? 1u : 0u) : 0u; - std::shared_ptr objects; - if (pipelineKey != 0) { + std::shared_ptr objects = prepared != nullptr ? prepared->objects : nullptr; + if (objects == nullptr && pipelineKey != 0) { std::lock_guard pipelines(state->computePipelinesMutex); if (const auto found = state->computePipelines.find(pipelineKey); found != state->computePipelines.end()) objects = found->second; } @@ -4408,6 +4562,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec const auto before = profile ? resources->Timing() : Graphics::ShaderResources::BuildTiming{}; const auto completeStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; resources->Complete(); + require(prepared->describedLayoutKey.empty() || std::ranges::equal(resources->SetLayouts().mainKey, prepared->describedLayoutKey), "the set layout described for the dispatch's pipeline differs from its build"); if (profile) { const auto completeWall = std::chrono::duration(std::chrono::steady_clock::now() - completeStart).count(); timer.add(PhaseResourcesComplete, completeWall); @@ -4482,73 +4637,15 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec if (profile && std::chrono::steady_clock::now() - d.cacheReport > std::chrono::seconds(10)) { d.cacheReport = std::chrono::steady_clock::now(); AgcDriver::ProfilePrint_nid_no_patch("[rescache] %llu hits, %llu misses, %llu invalidated, %zu entries (dispatch + draw); template hits: %llu refreshed the data buffers (revalidate %.1f ms), %llu had the same words; Find calls %llu, Touch calls %llu\n", static_cast(d.cacheHits), static_cast(d.cacheMisses), static_cast(d.cacheInvalidated), state->resourceCache.Size(), static_cast(d.templateRefreshed), d.templateRevalidateMs, static_cast(d.templateSameWords), static_cast(ResourceCache::Finds()), static_cast(ResourceCache::Touches())); - AgcDriver::ProfilePrint_nid_no_patch("[vk] deviceProc lookups inside the device call: %llu (%.2f per dispatch); pre-dispatch barriers recorded %llu, skipped %llu\n", static_cast(d.procLookups), d.profiledDispatches != 0 ? static_cast(d.procLookups) / static_cast(d.profiledDispatches) : 0.0, static_cast(d.preBarriersRecorded), static_cast(d.preBarriersSkipped)); + AgcDriver::ProfilePrint_nid_no_patch("[vk] deviceProc lookups inside the device call: %llu (%.2f per dispatch); pre-dispatch barriers recorded %llu, skipped %llu; compute pipelines created %llu (%llu under the GPU mutex)\n", static_cast(d.procLookups), d.profiledDispatches != 0 ? static_cast(d.procLookups) / static_cast(d.profiledDispatches) : 0.0, static_cast(d.preBarriersRecorded), static_cast(d.preBarriersSkipped), static_cast(computePipelinesCreated.load(std::memory_order_relaxed)), static_cast(computePipelinesCreatedLocked.load(std::memory_order_relaxed))); reportRecipes(); } timing.Mark("shader_resources"); timer.phase(PhaseResources); if (objects == nullptr) { - objects = std::make_shared(); - objects->device = state->device; - objects->destroyModule = state->DeviceFunction("vkDestroyShaderModule"); - objects->destroyLayout = state->DeviceFunction("vkDestroyPipelineLayout"); - objects->destroyPipeline = state->DeviceFunction("vkDestroyPipeline"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = shader.spirv.size() * sizeof(std::uint32_t); - moduleInfo.pCode = shader.spirv.data(); - check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects->module), "vkCreateShaderModule"); - timing.Mark("validate_shader_module"); - const std::array setLayouts{resources->Layout(), shader.workgroupMemoryDwords != 0 ? state->workgroupMemoryLayout : VK_NULL_HANDLE}; - const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, Graphics::ComputePushConstantBytes}; - VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; - layoutInfo.setLayoutCount = shader.workgroupMemoryDwords != 0 ? ShaderRecompiler::WorkgroupMemoryDescriptorSet + 1 : 1; - layoutInfo.pSetLayouts = setLayouts.data(); - layoutInfo.pushConstantRangeCount = pushStages != 0 ? 1 : 0; - layoutInfo.pPushConstantRanges = pushStages != 0 ? &push : nullptr; - check(state->DeviceFunction("vkCreatePipelineLayout")(state->device, &layoutInfo, nullptr, &objects->layout), "vkCreatePipelineLayout"); - const VkPipelineCreateFlags pipelineFlags = (context.pipelineExecutableInfo ? VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR : 0) | VK_PIPELINE_CREATE_DISPATCH_BASE_BIT; - const Graphics::PipelineSpecialization specialization(shader); - const auto subgroupSize = state->computeWave32 && (shader.hostSubgroupSize == 32u || shader.hostSubgroupSize > state->maxComputeSubgroupSize) ? std::min(shader.hostSubgroupSize, state->maxComputeSubgroupSize) : 0u; - if (const char* directory = std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); directory != nullptr && *directory != '\0' && CaptureComputePipeline(shader.spirv.size())) { - ComputePipelineCapture capture; - capture.spirv = shader.spirv.Copy(); - ComputeDescriptorSetLayout resourcesLayout; - resourcesLayout.flags = resources->LayoutUpdateAfterBind() ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0; - const auto layoutBindings = resources->LayoutBindings(); - const auto bindingFlags = resources->LayoutBindingFlags(); - require(layoutBindings.size() == bindingFlags.size(), "compute capture binding flag count differs from layout"); - for (std::size_t index = 0; index < layoutBindings.size(); ++index) { - const auto& binding = layoutBindings[index]; - require(binding.pImmutableSamplers == nullptr, "compute capture does not support immutable samplers"); - resourcesLayout.bindings.push_back({binding.binding, binding.descriptorType, binding.descriptorCount, binding.stageFlags, bindingFlags[index]}); - } - capture.sets.push_back(std::move(resourcesLayout)); - if (shader.workgroupMemoryDwords != 0) capture.sets.push_back({0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}); - if (pushStages != 0) capture.pushConstants.push_back(push); - for (const auto& value : shader.specialization) capture.specialization.push_back({value.id, value.value}); - capture.requiredSubgroupSize = subgroupSize; - capture.flags = pipelineFlags; - capture.deviceName = state->properties.deviceName; - capture.vendorId = state->properties.vendorID; - capture.deviceId = state->properties.deviceID; - capture.driverVersion = state->properties.driverVersion; - capture.apiVersion = state->properties.apiVersion; - std::copy_n(state->properties.pipelineCacheUUID, VK_UUID_SIZE, capture.pipelineCacheUuid.begin()); - const auto path = capture.Write(directory); - std::fprintf(stderr, "[compute-capture] %s\n", path.string().c_str()); - } - if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] creating a pipeline for %zu SPIR-V words (program 0x%llx)\n", shader.spirv.size(), static_cast(programAddress)); - CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization); - objects->destroyModule(objects->device, objects->module, nullptr); - objects->module = VK_NULL_HANDLE; - Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline); + const auto layouts = resources->SetLayouts(); + objects = computePipeline(context, shader, layouts.main, layouts.mainKey, pushStages, programAddress); timing.Mark("pipeline_create"); - if (pipelineKey != 0) { - // Find-or-insert: objects another dispatch of the variant mapped meanwhile serve this - // one too (this call's go with the batch that keeps them). - std::lock_guard pipelines(state->computePipelinesMutex); - objects = state->computePipelines.try_emplace(pipelineKey, objects).first->second; - } timer.phase(PhasePipeline); } // Recorded into the device's open batch: the CPU moves on to the next command while the GPU diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 84e4cd5c2d..8f1e1dd6f4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -214,6 +214,8 @@ struct Context { bool bufferInt64Atomics = false; const PlaceholderImages* placeholderImages = nullptr; bool viewportIndexLayer = false; + bool demoteToHelperInvocation = false; + bool earlyFragmentSampleMaskTestBeforeSampleCounting = false; bool primitiveListRestart = false; bool imageViewMinLod = false; bool pipelineExecutableInfo = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index 052e452a62..1a8f3fa00c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -22,7 +22,7 @@ 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); +void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes); struct MemoryValueFill { std::uint64_t address; std::size_t bytes; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index e9b639cbeb..e5180f6213 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -4,8 +4,12 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "BdaAbi.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include +#include +#include #include #include +#include #include #include #include @@ -70,9 +74,15 @@ bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& // made outside the device lock (a sampled texture's path, a dispatch's pre-sync); the path taken // re-checks with HostImportFor when it binds the import. bool HostImportCovers(const Context& context, std::uint64_t address, std::size_t bytes); +struct RegisteredPiece { + std::uint64_t address; + std::uint64_t bytes; + bool writable; + bool gpu; +}; // Whether a readable registered allocation contains [address, address + bytes) right now (one // registry lease): a storage image whose memory was freed or re-registered has nothing to store to. -bool RegisteredReadableCovers(std::uint64_t address, std::size_t bytes); +bool RegisteredReadableCovers(std::uint64_t address, std::size_t bytes, std::vector* pieces = nullptr); std::uint64_t RegisteredReadableEnd(std::uint64_t address); // A persistent device copy of one registered range of the main guest image (which cannot be host @@ -80,6 +90,17 @@ std::uint64_t RegisteredReadableEnd(std::uint64_t address); struct ImageMirror; class Recorder; +template +void OverlappedRanges(std::span sorted, std::size_t first, std::span> writes, std::vector& indices) { + indices.clear(); + for (const auto& [begin, end] : writes) { + auto it = std::partition_point(sorted.begin() + static_cast(first), sorted.end(), [&](const TRange& range) { return range.end <= begin; }); + for (; it != sorted.end() && it->begin < end; ++it) indices.push_back(static_cast(it - sorted.begin())); + } + std::sort(indices.begin(), indices.end()); + indices.erase(std::unique(indices.begin(), indices.end()), indices.end()); +} + // Identity of the import serving [address, address + bytes): a serial unique for the life of one // import, 0 when no import serves the range. An equal serial later means the same VkBuffer still // backs the range (a re-import gets a new serial), which is what a descriptor set written against it @@ -167,6 +188,8 @@ struct MirrorStats { std::uint64_t sparseBinds = 0; }; MirrorStats MirrorCounters(); +void ClearSnapshotCopies(VkDevice device); +std::vector AddressTableKey(std::span> writes, std::span own); void ClearImageMirrors(VkDevice device); void ClearHostImports(VkDevice device); @@ -345,6 +368,7 @@ private: // drops the space: for a region that partially overlaps a base region, and for a build whose // imports changed under the space (`resolve`: the direct regions are re-resolved by UploadFinish). void dissolveSpace(bool resolve); + void completeSpaceWrites() const; // The region containing `address`: this build's own regions (sorted after UploadPrepare) or // the space's base regions, or null. const Region* owner(std::uint64_t address) const; @@ -392,8 +416,8 @@ private: // The cached address space this build maps through (its lease pins the ranges); `regions` then // holds only the regions outside it (V#s, snapshots, ranges copied per build). std::shared_ptr space; - mutable std::uint64_t writeTableSerial = 0; - mutable std::shared_ptr> writeTableRanges; + mutable std::uint64_t tableSerial = 0; + mutable std::shared_ptr> tableRanges; // Import registry epoch when `direct` pointers were taken at acquire time; they are reused while // no import was destroyed since. std::uint64_t importsEpoch = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp index 997e9dc7e7..e7447ef57b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp @@ -58,7 +58,8 @@ enum class TextureDimension { k2DArray, kCube, k3D, - k1DArray + k1DArray, + k2DMsaa }; struct GuestTextureResource { @@ -86,10 +87,12 @@ struct GuestTextureResource { bool dccPipeAligned = false; std::uint32_t minLod = 0; std::uint32_t allocatedMipCount = 0; + std::uint32_t samples = 1; }; float EffectiveMinLod(const GuestTextureResource& resource); +bool IsNullTextureResource(std::span words); GuestTextureResource DecodeTextureResource(std::span words); bool DescriptorSingleLevel(std::span words); bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, TextureDimension dimension); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index bfcc8fd534..6283261445 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -6,12 +6,15 @@ #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include #include +#include #include #include namespace AgcDriver::Graphics { class StorageTexture; +class Texture; +struct GuestTextureResource; class MultisampledCmask { public: @@ -49,6 +52,8 @@ std::uint64_t MultisampledColorBytes(const ColorTarget& target); bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, VkClearColorValue& clear); bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear); VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target); +std::shared_ptr MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components); +void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget& target); void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView); bool MultisampledColorFillAbsorbs(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index 0237e88e1c..a195283b06 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -123,8 +123,10 @@ bool ValidateSampleLocations(const Context& context, const State& state); void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment); bool ShadersWriteMemory(std::span shaders); +bool DeclaresEarlyFragmentTests(const ShaderRecompiler::RecompileResult& program); +void ValidateSampleMask(const Context& context, const State& state, 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, bool shaderClipDistance = false, bool shaderCullDistance = false, bool viewportIndexLayer = false, bool runtimeDescriptorArray = 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, bool shaderCullDistance = false, bool viewportIndexLayer = false, bool runtimeDescriptorArray = false, bool demoteToHelperInvocation = false); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp index 6398fde6d1..92506617e4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp @@ -4,8 +4,11 @@ #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include #include +#include #include #include +#include +#include #include #include @@ -24,6 +27,24 @@ public: PipelineCacheSynchronization& Synchronization() { return synchronization; } private: + struct FileHeader { + std::uint32_t magic; + std::uint32_t format; + std::uint64_t dataBytes; + std::uint64_t dataHash; + bool operator==(const FileHeader&) const = default; + }; + enum class FileState { Missing, Unreadable, Invalid, Valid }; + struct File { + FileState state = FileState::Missing; + const char* invalidReason = nullptr; + FileHeader header{}; + std::vector contents; + std::span Payload() const; + }; + + File readFile() const; + std::optional readHeader() const; void load(std::vector& initialData); void save(bool final); void run(); @@ -33,7 +54,9 @@ private: VkPipelineCache cache = VK_NULL_HANDLE; PipelineCacheSynchronization synchronization; std::filesystem::path path; - std::size_t savedBytes = 0; + std::size_t loadedBytes = 0; + std::size_t settledBytes = 0; + std::optional settledFile; std::mutex mutex; std::condition_variable wake; bool stopping = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 02d25125f5..18e72f40da 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include "prx/libSceAgcDriver/Graphics/include/PassHazards.hpp" +#include #include #include #include @@ -96,8 +97,11 @@ public: enum class SnapshotUse : std::uint8_t { Storage, Vertex, Index16, Index32, Index16Restart, Index32Restart }; static constexpr std::size_t DrawSnapshotBudget = std::size_t{1024} << 20u; static constexpr std::size_t DrawSnapshotEntries = 1024; + static constexpr std::size_t UnwrittenSnapshotEntries = 4096; static constexpr std::size_t DrawInputBudget = std::size_t{1024} << 20u; static constexpr std::size_t DrawInputEntries = 16384; + std::shared_ptr UnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp); + void NoteUnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp, std::shared_ptr buffer); 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; @@ -123,6 +127,7 @@ public: int value; }; std::vector PendingWritesOver(std::uint64_t address, std::size_t bytes) const; + void PendingWriteRanges(std::vector>& ranges) const; // Batch read tracking: the guest ranges the recorded work reads IN PLACE through a host import // and the GPU has not executed yet (a V# element bound in place, a region the GPU copies out of // an import, an address-based build's leased heaps, indirect arguments, a GPU-direct storage @@ -824,6 +829,14 @@ private: std::size_t bytes = 0; }; std::map drawSnapshots; + struct UnwrittenSnapshot { + std::size_t bytes; + std::shared_ptr buffer; + }; + std::unordered_map unwrittenSnapshots; + std::array unwrittenStamp{}; + std::size_t unwrittenBytes = 0; + void retireUnwrittenSnapshots(const std::array& stamp); std::array drawSnapshotPools; void eraseDrawSnapshot(std::map::iterator entry); struct IndexRange { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp index 6ae3f8ab60..b53834e9d8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp @@ -11,6 +11,7 @@ namespace AgcDriver::Graphics { ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, const std::array& exportMappings, bool nullProgram = false); +bool PixelEarlyZ(std::uint32_t shaderControl); ShaderRecompiler::ShaderPixelStageInfo DefaultUnexportedInputs(ShaderRecompiler::ShaderPixelStageInfo pixel, std::span parameterExports); ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& shader, std::span header); // A guest range DecodeVertexStageInfo read (an attribute word, a vertex V#) with the bytes as read: diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index a8d2c19dbe..9ae4012438 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -29,6 +29,7 @@ class Recorder; void FlushCachedTextures(VkDevice device); void ClearCachedTextures(VkDevice device); std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory); +std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported); std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes); bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::chrono::steady_clock::duration sinceReport); std::optional LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory); @@ -41,7 +42,7 @@ TextureCacheUse TextureCacheUsage(); std::uint64_t SampledTextureCacheBudget(const Context& context); std::shared_ptr CachedSampledTexture(const Context& context, std::span words); bool SampledTexturesShareEntry(std::span first, std::span second); -void CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes); +bool CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes); // The cached storage image of a surface (render targets use it as their resident image); brought up // to date with guest memory before it is returned. @@ -219,6 +220,7 @@ public: // ranges are fixed by the build), so a Revalidate's collects on the same worker are memo hits. void PrecollectSurfaces() const; bool Reusable() const { return reusable; } + const std::vector& MultisampleSources() const { return multisampleSources; } static bool NeverReusable(std::span shaders); const std::vector>& SampledTextures() const { return textures; } bool ChecksFaults() const { return bda != nullptr; } @@ -480,6 +482,7 @@ private: std::size_t maxDrawSnapshotBytes = MaxDrawSnapshotBytes(); std::unique_ptr bda; bool usesBda = false; + std::vector multisampleSources; bool usesFaultBuffer = false; bool faultChecks = false; VkDescriptorSetLayout _layout = VK_NULL_HANDLE; @@ -504,6 +507,15 @@ private: std::vector descriptorBuffers; std::vector descriptorImages; std::vector descriptorWrites; + struct MovedWordBuffer { + std::size_t allocation; + std::vector words; + std::size_t size; + std::shared_ptr buffer; + }; + mutable std::vector movedWordBuffers; + mutable std::size_t movedWordBufferNext = 0; + mutable std::vector drawPoolSizes; // Guest buffer elements bound read-only: each use of this object skips that many pending-write // notes (counted in MarkGpuWrites for the [buffers] line). std::size_t readOnlyBuffers = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index df145d6d23..1ad4b4f826 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -99,6 +99,7 @@ struct State { bool stencilTest = false; VkStencilOpState stencilFront{}; VkStencilOpState stencilBack{}; + std::optional stencilReplayReference; ColorTarget color; std::vector colors; std::vector blends; @@ -128,6 +129,8 @@ struct State { bool customSampleLocations = false; bool sampleLocationGrid = false; std::uint32_t paClVsOutCntl = 0; + std::array quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; + bool pixelEarlyZ = false; }; // PA_CL_VS_OUT_CNTL bits that place position exports (clip and cull distances, the misc and the two @@ -141,13 +144,18 @@ VkExtent2D SampleLocationGrid(const State& state); std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state); -ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot); +bool PartialSampleMask(const State& state); +bool UniformSampleMask(const State& state); +VkSampleMask HostSampleMask(const State& state); +std::uint8_t QuadPixelMask(const State& state); +std::array QuadSampleMasks(const State& state); +ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot, bool allowMultisampleDcc = false); std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height); std::uint32_t ColorWriteMask(const Registers& context); GuestTextureResource SurfaceForTarget(const ColorTarget& color); struct ColorMetadataPass { - enum class Mode { EliminateFastClear, DccDecompress, Resolve }; + enum class Mode { EliminateFastClear, DccDecompress, Resolve, FmaskDecompress }; Mode mode; std::vector targets; std::optional source; @@ -195,9 +203,9 @@ struct DrawKeyRange { inline constexpr std::array DrawKeyRegisters{{ {RegisterBank::Context, 0x000, 1}, {RegisterBank::Context, 0x002, 1}, {RegisterBank::Context, 0x005, 1}, {RegisterBank::Context, 0x007, 7}, {RegisterBank::Context, 0x010, 6}, {RegisterBank::Context, 0x01a, 5}, {RegisterBank::Context, 0x080, 4}, {RegisterBank::Context, 0x08c, 4}, {RegisterBank::Context, 0x090, 2}, {RegisterBank::Context, 0x094, 2}, {RegisterBank::Context, 0x0b4, 2}, {RegisterBank::Context, 0x105, 4}, {RegisterBank::Context, 0x10b, 3}, {RegisterBank::Context, 0x10f, 6}, - // SPI_PS_INPUT_CNTL_0..31, SPI_PS_INPUT_ENA/ADDR, SPI_PS_IN_CONTROL, SPI_SHADER_POS/Z/COL_FORMAT, + // SPI_PS_INPUT_CNTL_0..31, SPI_PS_INPUT_ENA/ADDR, SPI_PS_IN_CONTROL .. SPI_BARYC_CNTL, SPI_SHADER_POS/Z/COL_FORMAT, // CB_BLEND0..7_CONTROL, GE_MAX_OUTPUT_PER_SUBGROUP. - {RegisterBank::Context, 0x191, 32}, {RegisterBank::Context, 0x1b3, 2}, {RegisterBank::Context, 0x1b6, 1}, {RegisterBank::Context, 0x1c3, 3}, {RegisterBank::Context, 0x1e0, 8}, {RegisterBank::Context, 0x1ff, 1}, + {RegisterBank::Context, 0x191, 32}, {RegisterBank::Context, 0x1b3, 2}, {RegisterBank::Context, 0x1b6, 3}, {RegisterBank::Context, 0x1c3, 3}, {RegisterBank::Context, 0x1e0, 8}, {RegisterBank::Context, 0x1ff, 1}, // DB_DEPTH_CONTROL .. PA_CL_VS_OUT_CNTL, PA_SC_MODE_CNTL_0/1, VGT_GS_MODE, VGT_GS_VERT_ITEMSIZE, // PA_SU_VTX_CNTL, the sample masks, PA_SC_CONSERVATIVE_RASTERIZATION_CNTL. {RegisterBank::Context, 0x200, 8}, {RegisterBank::Context, 0x286, 2}, {RegisterBank::Context, 0x292, 2}, {RegisterBank::Context, 0x29b, 1}, {RegisterBank::Context, 0x2ab, 1}, {RegisterBank::Context, 0x2ce, 1}, {RegisterBank::Context, 0x2d5, 2}, {RegisterBank::Context, 0x2db, 2}, {RegisterBank::Context, 0x2de, 6}, {RegisterBank::Context, 0x2f8, 2}, {RegisterBank::Context, 0x2fe, 16}, {RegisterBank::Context, 0x30e, 2}, {RegisterBank::Context, 0x313, 1}, diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index f9a0bdd6b6..2a104e8a4d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -22,6 +22,7 @@ namespace AgcDriver::Graphics { +class Buffer; class ResidentColor; class CommandBatch; class StorageTexture; @@ -61,7 +62,7 @@ public: // compute pass writing it and the next pass sampling it share one image and copy nothing. // CanCopyFrom says whether the two descriptors address the same surface compatibly. Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components); - Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D); + Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D, std::uint32_t baseLayer = 0); struct DepthCopyTag {}; Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag); static VkFormat DepthCopyFormat(VkFormat colorFormat); @@ -194,6 +195,8 @@ 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 void SaveBeforeUnwatch(std::uint64_t address, std::size_t bytes); + static constexpr std::uint64_t SmallStoreBytes = 64 * 1024; + static std::size_t StoreSmallPending(const char* reason); static bool AnyPending(); static void FlushDumpSelected(const char* reason); static bool StoreAtFlipRequested(const char* value); @@ -373,6 +376,17 @@ private: // Drops the saved guest bytes of the units over [address, address + bytes) (another image's fill // or copy supersedes them there); the other units' bytes stay valid. void forgetSavedOver(std::uint64_t address, std::size_t bytes); + // A write-back stored on the GPU over memory outside the write watch: the stored units' bytes are + // copied from its retile scratch into `savedBytes` behind the store and become `original` once + // the batch completed (`savedTicket` names that write-back per unit; anything that replaces or + // drops a unit's saved bytes first clears it). + std::vector prepareStoredSave(const std::vector& layers, const std::vector& skippedLayer); + void commitStoredSave(const std::vector& units); + void applyStoredSave(std::uint64_t ticket, const std::vector& units); + void cancelStoredSave(std::uint32_t unit); + void cancelStoredSaves(); + // Lets the batches holding the saves of the units `layers` selects (every unit for none) complete. + void awaitStoredSave(const std::vector* layers); bool guestBytesSettled() const; // Tracked units as 64 KiB write-stamp blocks (`blockUnits`: a thin tiled surface at a 64 KiB // aligned base; APS5_NO_BLOCK_TRACKING=1 tracks array layers as above instead): a fill of one @@ -418,13 +432,20 @@ private: std::vector> importReads; }; PaddingSeeds paddingSeeds(const HostImport& import, std::vector copied) const; + struct ImportSegment { + const HostImport* import; + std::uint64_t begin; + std::uint64_t end; + }; + std::vector importSegments() const; + PaddingSeeds paddingSeeds(std::span segments, const std::vector& copied) const; // The direct upload and GPU-direct write-back of the runs through their windows (block units); // both return the bytes moved. The upload reads each unit from its unit shadow while fresh, // else from the import (`discard`: the image has no layout yet, a whole-surface first upload). // The write-back retiles into the import's unit shadow where a slab takes the piece // (`shadowed` receives those guest ranges, `imported` the pieces written to the import). std::uint64_t uploadWindows(const HostImport& import, std::span> runs, bool discard = false); - std::uint64_t writeBackWindows(const HostImport& import, std::span> keep, std::uint64_t firstStored, std::uint64_t lastStored, std::vector& shadowed, std::vector>& imported); + std::uint64_t writeBackWindows(std::span segments, std::span> keep, std::uint64_t firstStored, std::uint64_t lastStored, std::vector& shadowed, std::vector>& imported, Buffer* save = nullptr); // Per-layer validity (thin array surfaces whose layers are 64 KiB-aligned guest slices; every // other surface is one tracked layer): a layer's generation is the write generation its guest // bytes were last known to match the image at, and a pending layer holds results (a clear, a @@ -572,6 +593,9 @@ private: // `original` holds the guest bytes; false after a GPU-side clear, which never read them. std::vector layerOriginal; std::map keptAtUnwatch; + std::shared_ptr savedBytes; + std::vector savedTicket; + std::uint64_t lastSaveTicket = 0; // The storage cache let the image go (Flush): a lookup makes a new image of the surface, so // no fill may put results into this one (guarded by the live-image registry lock). bool released = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index f4f4750f14..148b5e4f55 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -275,6 +275,10 @@ void BdaResources::CheckFault() const { throw std::runtime_error(message.str()); } std::ostringstream message; + if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::UnlinkedCall) { + message << "shader stage " << report.stage << " called address 0x" << std::hex << report.address << " at program counter 0x" << report.instruction << ", which no linked callee of that call matches"; + throw std::runtime_error(message.str()); + } message << "BDA access failed: address=0x" << std::hex << report.address << " instruction=0x" << report.instruction << std::dec << " bytes=" << report.bytes << " stage=" << report.stage << " reason=" << static_cast(report.reason); if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::Permission && table != nullptr) { const auto bytes = table->Bytes(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index f5d3167840..4be4468a5b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -31,6 +31,13 @@ Recorder* RecorderFor(const Context& context) { return recorder != nullptr && recorder->Device() == context.device ? recorder : nullptr; } +bool storageFits(const DepthTarget& target, const GuestTextureResource& descriptor) { + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + const auto format = ResolveTextureFormat(descriptor.format); + const bool sized = d16 ? (format == VK_FORMAT_R16_UINT || format == VK_FORMAT_R16_UNORM || format == VK_FORMAT_R16_SINT || format == VK_FORMAT_R16_SNORM || format == VK_FORMAT_R16_SFLOAT) : (format == VK_FORMAT_R32_SFLOAT || format == VK_FORMAT_R32_UINT || format == VK_FORMAT_R32_SINT); + return sized && descriptor.width == target.extent.width && descriptor.height == target.extent.height && descriptor.mipCount == 1 && (descriptor.dimension == TextureDimension::k2D || descriptor.dimension == TextureDimension::k2DArray); +} + class DepthSurface { public: DepthSurface(const Context& context, const DepthTarget& target) : context(context), target(target), hostFormat(HostDepthFormat(context, target.format)) { @@ -213,7 +220,7 @@ public: if (plane.storage != nullptr) plane.replaced.push_back(plane.storage); plane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); } - copyPlane(plane, stencil, true); + copyPlane(plane, plane.storage, stencil, true); return plane.storage; } @@ -221,7 +228,7 @@ public: for (const bool stencil : {false, true}) { auto& plane = stencil ? stencilPlane : depthPlane; if (plane.storage.get() == &candidate) { - copyPlane(plane, stencil, false); + copyPlane(plane, plane.storage, stencil, false); written = ++depthWrites; return true; } @@ -233,6 +240,44 @@ public: return false; } + bool StoreBackCovering(const StorageTexture& candidate) { + for (const bool stencil : {false, true}) { + if (!CoversPlane(candidate, stencil)) continue; + auto& plane = stencil ? stencilPlane : depthPlane; + const auto source = std::const_pointer_cast(candidate.shared_from_this()); + copyPlane(plane, source, stencil, false); + written = ++depthWrites; + return true; + } + return false; + } + + void TakeStorage() { + if (target.samples != 1 || hostFormat != target.format) return; + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + const auto bytes = DepthSliceBytes(target.extent, d16 ? 2u : 4u); + auto storage = StorageTexture::FindPending(target.address, bytes); + if (storage == nullptr) storage = StorageTexture::FindLive(target.address, bytes); + if (storage == nullptr || !storageFits(target, storage->Descriptor())) return; + storage->Refresh(); + if (depthPlane.staging == nullptr) depthPlane.staging = std::make_unique(context, static_cast(target.extent.width) * target.extent.height * (d16 ? 2u : 4u), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); + auto* recorder = RecorderFor(context); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); + const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); + if (recorder != nullptr) recorder->Keep(storage); + VkBufferImageCopy depthRegion{0, 0, 0, {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}, {0, 0, 0}, {target.extent.width, target.extent.height, 1}}; + VkBufferImageCopy colorRegion = depthRegion; + colorRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + context.Function("vkCmdCopyImageToBuffer")(commands, storage->Image(), VK_IMAGE_LAYOUT_GENERAL, depthPlane.staging->Handle(), 1, &colorRegion); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdCopyBufferToImage")(commands, depthPlane.staging->Handle(), image, VK_IMAGE_LAYOUT_GENERAL, 1, &depthRegion); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); + if (batch) batch->SubmitAndWait(); + else Recorder::CountBarriers(Recorder::CommandClass::Copy, 3); + } + const Context context; const DepthTarget target; const VkFormat hostFormat; @@ -250,20 +295,30 @@ private: PlaneStorage depthPlane; PlaneStorage stencilPlane; - void copyPlane(PlaneStorage& plane, bool stencil, bool toStorage) { + bool CoversPlane(const StorageTexture& storage, bool stencil) const { + const auto& resource = storage.Descriptor(); + const auto address = stencil ? target.stencilAddress : target.address; + if (address == 0 || resource.baseAddress != address || resource.width != target.extent.width || resource.height != target.extent.height || resource.baseLevel != 0 || resource.mipCount != 1) return false; + if (resource.dimension != TextureDimension::k2D && (resource.dimension != TextureDimension::k2DArray || resource.baseArray != 0 || resource.depthOrLastArray != 0)) return false; + if (target.samples != 1 || (!stencil && hostFormat != target.format)) return false; + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + return ResolveTextureFormat(resource.format) == (stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT); + } + + void copyPlane(PlaneStorage& plane, const std::shared_ptr& storage, bool stencil, bool toStorage) { auto* recorder = Recorder::Active(); Require(recorder != nullptr, "depth surface storage copies need an active recorder"); const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto bytes = static_cast(target.extent.width) * target.extent.height * (stencil ? 1u : d16 ? 2u : 4u); if (plane.staging == nullptr) plane.staging = std::make_unique(context, bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); const auto commands = recorder->Commands(); - recorder->Keep(plane.storage); + recorder->Keep(storage); const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy); VkBufferImageCopy planeRegion{0, 0, 0, {stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}, {0, 0, 0}, {target.extent.width, target.extent.height, 1}}; VkBufferImageCopy colorRegion = planeRegion; colorRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - const auto sourceImage = toStorage ? image : plane.storage->Image(); - const auto destinationImage = toStorage ? plane.storage->Image() : image; + const auto sourceImage = toStorage ? image : storage->Image(); + const auto destinationImage = toStorage ? storage->Image() : image; RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); context.Function("vkCmdCopyImageToBuffer")(commands, sourceImage, VK_IMAGE_LAYOUT_GENERAL, plane.staging->Handle(), 1, toStorage ? &planeRegion : &colorRegion); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); @@ -316,7 +371,8 @@ public: allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory depth plane copy"); Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory depth plane copy"); - staging = std::make_unique(context, static_cast(sliceBytes()) * layers, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + stagingSize = static_cast(sliceBytes()) * layers; + staging = std::make_unique(context, stagingSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } catch (...) { release(); throw; @@ -326,10 +382,21 @@ public: DepthPlaneCopy(const DepthPlaneCopy&) = delete; DepthPlaneCopy& operator=(const DepthPlaneCopy&) = delete; - std::shared_ptr Refresh(std::span slices, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType) { + std::shared_ptr Refresh(std::span slices, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType, std::uint32_t baseLayer) { Require(slices.size() == layers, "depth plane copy slices do not match its layers"); const auto geometry = DescribeSurface(resource); - Require(geometry.layers == layers && geometry.sliceLinearBytes == sliceBytes() && !geometry.mips.empty(), "depth plane copy geometry does not match its layers"); + const bool guestFill = std::any_of(slices.begin(), slices.end(), [](const auto& slice) { return slice.image == VK_NULL_HANDLE; }); + if (guestFill && (geometry.layers != layers || geometry.sliceLinearBytes < sliceBytes() || geometry.mips.empty())) { + char text[320]; + std::snprintf(text, sizeof(text), "AGC graphics: depth plane copy geometry does not match its layers (copy %ux%u, %u layers, %llu bytes a slice; texture %ux%u guest format %u, dimension %d, tile mode %u, %u layers, %llu bytes a slice, %zu mips)", extent.width, extent.height, layers, static_cast(sliceBytes()), resource.width, resource.height, resource.format, static_cast(resource.dimension), resource.tileMode, geometry.layers, static_cast(geometry.sliceLinearBytes), geometry.mips.size()); + throw std::runtime_error(text); + } + const auto stagingBytes = static_cast(geometry.sliceLinearBytes) * layers; + if (stagingBytes > stagingSize) { + retiredStaging.push_back(std::move(staging)); + staging = std::make_unique(context, stagingBytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + stagingSize = stagingBytes; + } std::vector> uploads; std::vector uploadLayers; std::vector copiedLayers; @@ -356,10 +423,10 @@ public: held = current; } if (!copiedLayers.empty() || !uploads.empty()) record(slices, resource, geometry, copiedLayers, uploads, uploadLayers); - const std::array key{static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a), static_cast(viewType)}; + const std::array key{static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a), static_cast(viewType), baseLayer}; auto& texture = textures[key]; if (texture == nullptr) { - texture = std::make_shared(context, image, format, aspect(), components, viewType); + texture = std::make_shared(context, image, format, aspect(), components, viewType, baseLayer); texture->MarkRefreshedPerUse(); } return texture; @@ -382,7 +449,9 @@ private: VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; std::unique_ptr staging; - std::map, std::shared_ptr> textures; + std::size_t stagingSize = 0; + std::vector> retiredStaging; + std::map, std::shared_ptr> textures; std::vector guestLayers; std::vector surfaceLayers; bool filled = false; @@ -399,9 +468,11 @@ private: const auto barrier = context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier"); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); std::vector regions; + const auto& mip = geometry.mips.front(); const auto region = [&](std::uint32_t layer, VkImageAspectFlags aspectMask, std::uint32_t imageLayer) { VkBufferImageCopy copy{}; - copy.bufferOffset = sliceBytes() * layer; + copy.bufferOffset = geometry.LinearLayerOffset(layer) + mip.linearOffset; + copy.bufferRowLength = mip.pitchBytes / (format == VK_FORMAT_D32_SFLOAT ? 4u : 2u); copy.imageSubresource = {aspectMask, 0, imageLayer, 1}; copy.imageExtent = {extent.width, extent.height, 1}; return copy; @@ -617,11 +688,20 @@ DepthSurface* planeSurface(VkDevice device, std::uint64_t address) { return found == list.rend() ? nullptr : found->get(); } -std::set& clearedHtiles() { - static auto* cleared = new std::set(); +std::map& clearedHtiles() { + static auto* cleared = new std::map(); return *cleared; } +bool takeClearedHtile(const DepthTarget& target) { + if (target.htileAddress == 0) return false; + auto& cleared = clearedHtiles(); + const auto found = std::find_if(cleared.begin(), cleared.end(), [&](const auto& entry) { return HtileFillCovers(target.htileAddress, target.extent, entry.first, entry.second); }); + if (found == cleared.end()) return false; + cleared.erase(found); + return true; +} + const DepthSurface* drawnDepth = nullptr; std::atomic surfacesSerial{0}; @@ -677,8 +757,9 @@ float HostDepthBiasConstant(const Context& context, VkFormat format, float const } VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) { + std::lock_guard gpu(GuestMemory::GpuMutex()); std::lock_guard lock(surfacesMutex()); - const bool cleared = target.htileAddress != 0 && clearedHtiles().erase(target.htileAddress) != 0; + const bool cleared = takeClearedHtile(target); DepthSurface* surface = nullptr; for (const auto& candidate : surfaces()) { if (candidate->context.device == context.device && sameSurface(candidate->target, target)) { @@ -696,6 +777,7 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) surface->clearDepth = target.clearDepth; surface->clearStencil = target.clearStencil; takePlanes(*surface); + if (created && !inherits && target.htileAddress == 0) surface->TakeStorage(); if (cleared && (!created || inherits)) surface->pendingClear |= VK_IMAGE_ASPECT_DEPTH_BIT; surface->ApplyFastClear(); if (!created || inherits) surface->written = ++depthWrites; @@ -736,15 +818,15 @@ void NoteMemoryValueFill(const MemoryValueFill& fill) { NoteDepthMetadataFill(fill.address, fill.bytes, value); } -void NoteHtileDepthClear(std::uint64_t htileAddress) { +void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes) { std::lock_guard lock(surfacesMutex()); bool existing = false; for (const auto& surface : surfaces()) { - if (surface->target.htileAddress != htileAddress) continue; + if (!HtileFillCovers(surface->target.htileAddress, surface->target.extent, htileAddress, bytes)) continue; surface->pendingClear |= VK_IMAGE_ASPECT_DEPTH_BIT; existing = true; } - if (!existing) clearedHtiles().insert(htileAddress); + if (!existing) clearedHtiles()[htileAddress] = bytes; } void ClearDepthSurfaces(VkDevice device) { @@ -786,14 +868,15 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::spanhostFormat == base.format, "sampling several slices or an integer view of a depth plane that the device stores in another depth format is not implemented"); const auto copyFormat = integer ? VK_FORMAT_R16_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; const auto layers = cube ? 6u : layered ? resource.depthOrLastArray + 1u : 1u; - if ((format != copyFormat && !depthBits) || resource.width != base.extent.width || resource.height != base.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || resource.baseArray != 0 || (cube && resource.depthOrLastArray != 0 && resource.depthOrLastArray != 5) || (resource.dimension != TextureDimension::k2D && resource.dimension != TextureDimension::k2DArray && !cube)) { + if ((format != copyFormat && !depthBits) || resource.width != base.extent.width || resource.height != base.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || (resource.baseArray != 0 && (cube || !layered || resource.baseArray > resource.depthOrLastArray)) || (cube && resource.depthOrLastArray != 0 && resource.depthOrLastArray != 5) || (resource.dimension != TextureDimension::k2D && resource.dimension != TextureDimension::k2DArray && !cube)) { char text[320]; std::snprintf(text, sizeof(text), "AGC graphics: sampling the depth planes of depth surface 0x%llx (%ux%u, vk format %d) as a %ux%u texture of guest format %u (vk %d), dimension %d, %u slices, levels %u-%u, base slice %u is not implemented", static_cast(base.address), base.extent.width, base.extent.height, static_cast(base.format), resource.width, resource.height, resource.format, static_cast(format), static_cast(resource.dimension), layers, resource.baseLevel, resource.lastLevel, resource.baseArray); throw std::runtime_error(text); } std::vector slices; + const auto geometry = DescribeSurface(resource); for (std::uint32_t layer = 0; layer < layers; ++layer) { - const auto address = base.address + static_cast(layer) * DepthSliceBytes(base.extent, d16 ? 2u : 4u); + const auto address = base.address + geometry.GuestLayerOffset(layer); auto* slice = depthSlice(context.device, address, base); if (slice != nullptr) slice->ApplyFastClear(); if (slice == nullptr) slices.push_back({}); @@ -804,7 +887,7 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::span(context, base.extent, imageFormat, layers); } - return copy->Refresh(slices, resource, components, cube || resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D); + return copy->Refresh(slices, resource, components, cube || resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D, layered && !cube ? resource.baseArray : 0u); } bool DepthSurfaceHolds(const Context& context, const GuestTextureResource& resource, const Texture* texture) { @@ -917,6 +1000,9 @@ bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage for (const auto& surface : surfaces()) { if (surface->context.device == context.device && surface->StoreBack(storage)) return true; } + for (auto surface = surfaces().rbegin(); surface != surfaces().rend(); ++surface) { + if ((*surface)->context.device == context.device && (*surface)->StoreBackCovering(storage)) return true; + } return false; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index ee37b74a07..a4a0189916 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -700,7 +700,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, context.shaderClipDistance, context.shaderCullDistance, context.viewportIndexLayer, context.runtimeDescriptorArray); + outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics, context.shaderClipDistance, context.shaderCullDistance, context.viewportIndexLayer, context.runtimeDescriptorArray, context.demoteToHelperInvocation); } catch (const std::exception& error) { if (keyed) { std::lock_guard lock(validationMutex()); @@ -1059,6 +1059,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P // Viewport and scissor are dynamic pipeline state, so their limits are checked here per draw. ValidateViewport(context, state.viewport); ValidateDepthBounds(context, state); + ValidateSampleMask(context, state, shaders); timer.phase(PhaseValidate); inputs.maxIndex = draw.indexed ? 0u : draw.firstVertex + draw.indexCount - 1u; if (draw.indexed) { @@ -1327,7 +1328,7 @@ struct IndirectRecord { // The draw commands of one draw: the vertex and index buffer binds, then the direct draw, the // GPU-side indirect draw from `argumentBuffer` or the CPU-read records with the driver's rules. -void recordDrawCommands(const Context& context, VkCommandBuffer commands, const State& state, const Pm4::DrawParameters& draw, const DrawInputs& inputs, const IndirectRecord* indirect, VkBuffer argumentBuffer, VkDeviceSize argumentOffset) { +void recordDrawCommands(const Context& context, VkCommandBuffer commands, const State& state, const Pm4::DrawParameters& draw, const DrawInputs& inputs, const IndirectRecord* indirect, VkBuffer argumentBuffer, VkDeviceSize argumentOffset, std::uint32_t firstIndex = 0) { if (context.recorder != nullptr) context.recorder->NoteSampledDraw(commands); const auto* args = indirect != nullptr ? indirect->args : nullptr; if (state.stages.mesh && args != nullptr) { @@ -1342,7 +1343,7 @@ void recordDrawCommands(const Context& context, VkCommandBuffer commands, const if (!inputs.vertexHandles.empty()) context.Resolved(&DeviceFunctions::cmdBindVertexBuffers, "vkCmdBindVertexBuffers")(commands, 0, static_cast(inputs.vertexHandles.size()), inputs.vertexHandles.data(), inputs.vertexOffsets.data()); if (draw.indexed) context.Resolved(&DeviceFunctions::cmdBindIndexBuffer, "vkCmdBindIndexBuffer")(commands, inputs.indexHandle, inputs.indexOffset, draw.indexSize == 2 ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT32); if (args == nullptr) { - if (draw.indexed) context.Resolved(&DeviceFunctions::cmdDrawIndexed, "vkCmdDrawIndexed")(commands, draw.indexCount, draw.instanceCount, 0, static_cast(draw.firstVertex), draw.firstInstance); + if (draw.indexed) context.Resolved(&DeviceFunctions::cmdDrawIndexed, "vkCmdDrawIndexed")(commands, draw.indexCount, draw.instanceCount, firstIndex, static_cast(draw.firstVertex), draw.firstInstance); else context.Resolved(&DeviceFunctions::cmdDraw, "vkCmdDraw")(commands, draw.indexCount, draw.instanceCount, draw.firstVertex, draw.firstInstance); } else if (indirect->path == IndirectDrawPath::Gpu) { if (args->countIndirect) { @@ -1557,8 +1558,47 @@ struct RecordedDraw { bool waited = false; const std::array* pushBytes = nullptr; VkShaderStageFlags pushStages = 0; + std::vector>> stencilReplay; }; +void recordStencilReplay(const Context& context, VkCommandBuffer commands, const State& state, const Pm4::DrawParameters& draw, const DrawInputs& inputs, std::span>> replay, std::span shaders, const ShaderResources& resources, const std::array* bytes = nullptr, VkShaderStageFlags stages = 0) { + State selected = state; + auto block = bytes != nullptr ? *bytes : AssemblePushConstants(shaders); + if (bytes == nullptr) { + resources.PatchPushConstants(block); + stages = PushConstantStages(shaders); + } + const auto fragment = std::ranges::find_if(shaders, [](const CompiledShader& shader) { return shader.stage == ShaderRecompiler::ShaderStage::Fragment && !shader.program->stencilReferences.empty(); }); + Require(fragment != shaders.end(), "stencil replay fragment stage is missing"); + const auto referenceOffset = fragment->pushConstantOffset + fragment->program->stencilReferenceDword * sizeof(std::uint32_t); + Require(referenceOffset <= block.size() - sizeof(std::uint32_t), "stencil replay reference exceeds the push block"); + const auto run = [&](const Pm4::DrawParameters& piece, std::uint32_t firstIndex) { + for (const auto& [reference, pipeline] : replay) { + selected.stencilReplayReference = reference; + selected.stencilFront.reference = reference; + selected.stencilBack.reference = reference; + context.Resolved(&DeviceFunctions::cmdSetStencilReference, "vkCmdSetStencilReference")(commands, VK_STENCIL_FACE_FRONT_AND_BACK, reference); + std::memcpy(block.data() + referenceOffset, &reference, sizeof(reference)); + pipeline->PushConstants(commands, stages, block); + recordDrawCommands(context, commands, selected, piece, inputs, nullptr, VK_NULL_HANDLE, 0, firstIndex); + } + }; + if (replay.size() == 1u) { + run(draw, 0u); + return; + } + for (std::uint32_t instance = 0; instance < draw.instanceCount; ++instance) { + for (std::uint32_t first = 0; first < draw.indexCount - draw.indexCount % 3u; first += 3u) { + auto piece = draw; + piece.instanceCount = 1u; + piece.firstInstance = draw.firstInstance + instance; + piece.indexCount = 3u; + if (!draw.indexed) piece.firstVertex += first; + run(piece, draw.indexed ? first : 0u); + } + } +} + void pushDrawConstants(const Pipeline& pipeline, VkCommandBuffer commands, const State& state, const Pm4::DrawParameters& draw, std::span shaders, const ShaderResources& resources, const std::array* bytes, VkShaderStageFlags stages, VkDeviceAddress meshArguments = 0, bool restartTable = false) { auto block = bytes != nullptr ? *bytes : AssemblePushConstants(shaders); if (bytes == nullptr) { @@ -1775,7 +1815,11 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam APS5_LOG_CHARS_OUT_DEBUG("Resources bound"); pushDrawConstants(*record.pipeline, commands, state, draw, shaders, resources, record.pushBytes, record.pushStages, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0, !inputs.restartTable.empty()); APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded"); - recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset); + if (record.stencilReplay.empty()) recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset); + else { + recordStencilReplay(context, commands, state, draw, inputs, record.stencilReplay, shaders, resources, record.pushBytes, record.pushStages); + for (const auto& [reference, pipeline] : record.stencilReplay) recorder->Keep(pipeline); + } if (record.pipeline->SplitsFaces()) { record.pipeline->ContinueBackFaces(commands, state); recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset); @@ -1855,6 +1899,25 @@ std::optional KnownValidationFailure(const Context& context, std::s } void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots, std::shared_ptr* recipeOut) { + std::vector stencilReferences; + for (const auto& shader : shaders) { + if (shader.program != nullptr && !shader.program->stencilReferences.empty()) { + Require(shader.stage == ShaderRecompiler::ShaderStage::Fragment && stencilReferences.empty(), "stencil replay requires exactly one fragment stage"); + stencilReferences = shader.program->stencilReferences; + } + } + if (!stencilReferences.empty()) { + Require(!draw.indirect && !state.primitiveRestart && !state.rectList && !state.stages.mesh && !state.stages.tessellation && state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, "stencil replay supports direct triangle lists without restart, mesh or tessellation"); + Require(!state.depthBiasPerFace && !ShadersWriteMemory(shaders), "stencil replay refuses face splits and shader memory writes"); + Require(std::ranges::all_of(shaders, [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->stencilReplaySafe && (shader.stage == ShaderRecompiler::ShaderStage::Vertex || shader.stage == ShaderRecompiler::ShaderStage::Fragment); }), "stencil replay refuses stages with side effects, clocks, subgroup dependencies or primitive ID inputs"); + for (const auto& shader : shaders) { + if (shader.stage != ShaderRecompiler::ShaderStage::Vertex) continue; + Require((shader.program->hostSubgroupSize == 1u || shader.program->hostSubgroupSize == context.subgroup.subgroupSize) && shader.program->spirv.size() >= 5u && shader.program->spirv[1] >= 0x00010000u && shader.program->spirv[1] < 0x00010600u, "stencil replay vertex subgroup contract differs from the physical device"); + } + Require(static_cast(draw.firstInstance) + draw.instanceCount <= 0x100000000ull && (draw.indexed || static_cast(draw.firstVertex) + draw.indexCount <= 0x100000000ull), "stencil replay draw indices overflow"); + Require(state.samples == 1u && (!state.depth || HasStencil(state.depth->format)), "stencil replay requires single-sample rendering and an eight-bit stencil format"); + recipeOut = nullptr; + } if (context.recorder != nullptr) context.recorder->BoundKeptBytes(); PerformanceTimer timing("Graphics.Draw"); // APS5_PROFILE_DRAW prints the time of each phase of the draw (microseconds) and the [draws] totals. @@ -1992,9 +2055,34 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& timer.phase(PhasePrepare); const bool recordDraws = RecordDraws(); auto* recorder = Recorder::Active(); - const bool recordable = recordDraws && recorder != nullptr && dumpLimit == 0 && std::all_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.resident != nullptr || binding.multisampled; }); + bool recordable = recordDraws && recorder != nullptr && dumpLimit == 0 && std::all_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.resident != nullptr || binding.multisampled; }); auto resolved = resolveDrawResources(context, state, draw, shaders, snapshots, indexBytes, recordable, outcome, timer); auto& resources = resolved.resources; + if (!stencilReferences.empty() && resources->ChecksFaults()) recordable = false; + if (!stencilReferences.empty()) { + const auto readImages = resources->StorageImages(); + const auto inputOverlaps = [&](std::uint64_t address, std::uint64_t bytes) { + return std::ranges::any_of(inputs.inPlaceReads, [&](const auto& range) { return address < range.second && range.first < address + bytes; }); + }; + for (const auto& target : targets) { + const bool imageRead = target.resident != nullptr && std::ranges::any_of(readImages, [&](const auto& image) { return image.first == target.resident->Image(); }); + Require(!imageRead && !resources->ReadsImage(target.resident.get()) && !resources->ReadsOverlap(target.color.address, static_cast(target.color.bytes)) && !inputOverlaps(target.color.address, target.color.bytes), "stencil replay refuses attachment feedback reads"); + } + if (state.depth) { + const auto bytes = static_cast(state.depth->extent.width) * state.depth->extent.height * 4u; + Require(!resources->ReadsOverlap(state.depth->address, bytes) && !resources->ReadsOverlap(state.depth->stencilAddress, bytes) && !inputOverlaps(state.depth->address, bytes) && !inputOverlaps(state.depth->stencilAddress, bytes), "stencil replay refuses depth or stencil feedback reads"); + for (const auto& shader : shaders) { + for (const auto& binding : shader.program->bindings) { + if (binding.role != ShaderRecompiler::DescriptorRole::GuestImages && binding.role != ShaderRecompiler::DescriptorRole::ImageTable) continue; + for (std::size_t offset = 0; offset + 8u <= binding.guestDescriptor.size(); offset += 8u) { + const auto words = std::span(binding.guestDescriptor).subspan(offset, 8u); + const auto texture = DecodeTextureResource(words); + Require(!DepthSurfaceAt(context, words, texture), "stencil replay refuses sampled depth-surface feedback"); + } + } + } + } + } const auto& contentKey = resolved.contentKey; const bool cacheable = resolved.cacheable; built = resolved.built; @@ -2105,6 +2193,9 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& const bool lean = recorded && !drawTransitions; const bool multisampled = std::any_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.multisampled; }); Require(lean || !multisampled, "multisampled draws are only rendered as recorded draws"); + for (const auto& binding : targets) { + if (binding.multisampled) Require(std::find(resources->MultisampleSources().begin(), resources->MultisampleSources().end(), binding.color.surfaceAddress) == resources->MultisampleSources().end(), "sampling a multisampled color target the draw renders to is not implemented"); + } if (!lean && !resolved.moved.empty()) { resolved.resources = std::make_shared(context, shaders, state.color, static_cast(state.colors.size()), draw.indexAddress, static_cast(indexBytes), snapshots); resolved.moved.clear(); @@ -2126,7 +2217,17 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& 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.VertexLayout(), *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + std::vector>> stencilReplay; + for (const auto reference : stencilReferences) { + auto selected = pipelineState; + selected.stencilReplayReference = reference; + selected.stencilFront.reference = reference; + selected.stencilBack.reference = reference; + auto selectedPipeline = stencilReplay.empty() ? CachedPipeline(context, selected, inputs.VertexLayout(), *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) : stencilReplay.front().second; + Require(!selectedPipeline->SplitsFaces() && !selectedPipeline->SplitsSecondChannel(), "stencil replay refuses pipeline face or channel splits"); + stencilReplay.emplace_back(reference, std::move(selectedPipeline)); + } + auto pipeline = stencilReplay.empty() ? CachedPipeline(context, pipelineState, inputs.VertexLayout(), *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) : stencilReplay.front().second; // 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. std::vector> owners; @@ -2157,6 +2258,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& record.listed = listed; record.completion = completion; record.waited = outcome.waited; + record.stencilReplay = stencilReplay; recordDraw(context, state, draw, shaders, inputs, record, outcome, timer, ownWaitedMs); // The recipe for the caller's draw-cache entry (design_cpu_final M8, rule R3 for draws): // only a template the resource cache serves under this content key (reusable: no lease, @@ -2284,7 +2386,11 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& APS5_LOG_CHARS_OUT_DEBUG("Resources bound"); pushDrawConstants(*pipeline, commands, state, draw, shaders, *resources, nullptr, 0, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0, !inputs.restartTable.empty()); APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded"); - recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset); + if (stencilReplay.empty()) recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset); + else { + recordStencilReplay(context, commands, state, draw, inputs, stencilReplay, shaders, *resources); + if (recorded) for (const auto& [reference, selectedPipeline] : stencilReplay) recorder->Keep(selectedPipeline); + } if (pipeline->SplitsFaces()) { pipeline->ContinueBackFaces(commands, state); recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset); @@ -2369,6 +2475,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // The recorder sync SubmitAndWait makes is the draw's own wait, not a nested hook wait. const auto ownBefore = profile ? Recorder::ThreadWaitedMs() : 0.0; batch->SubmitAndWait(); + if (!stencilReferences.empty()) resources->CheckFaults(); if (profile) ownWaitedMs += Recorder::ThreadWaitedMs() - ownBefore; } if (checkRecords) checkRecords(); @@ -2570,6 +2677,10 @@ void RunColorMetadataPass(const Context& context, const ColorMetadataPass& pass) ResolveMultisampleTarget(context, *pass.source, *resident, resident->AttachmentView(destination.format, destination.mip)); return; } + if (pass.mode == ColorMetadataPass::Mode::FmaskDecompress) { + for (const auto& color : pass.targets) MaterializeMultisampleTargetClear(context, color); + return; + } for (const auto& color : pass.targets) { if (color.cmaskAddress != 0) { const auto resident = metadataPassResident(context, color); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 578128f2fe..c04d2bdf93 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -11,6 +11,7 @@ #include "prx/libc/include/CpuTopology.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" +#include "prx/libc/include/general/AtomicSharedPtr.hpp" #ifdef _WIN32 #include #else @@ -29,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -112,7 +114,13 @@ struct GuestBufferMemory::AddressSpace { std::vector ranges; std::shared_ptr>> directReads; mutable std::mutex tableMutex; - mutable std::map>, std::pair>>> writeTables; + struct Table { + std::uint64_t serial; + std::shared_ptr> ranges; + std::list*>::iterator recent; + }; + mutable std::map, Table> tables; + mutable std::list*> tableRecency; }; namespace { @@ -120,7 +128,7 @@ namespace { struct RetiredImport; struct AddressSpaceCache { - std::atomic> current; + AtomicSharedPtr current; std::atomic serials{0}; // Set by the waiter's drop, cleared by the next publish: the rebuild's reason. std::atomic droppedByWaiter{false}; @@ -1236,6 +1244,64 @@ ImageMirrors& Mirrors() { return mirrors; } +struct SnapshotCopies { + using Key = std::tuple; + static constexpr std::size_t Budget = std::size_t{16} << 20u; + static constexpr std::size_t MaxEntries = 1024; + std::mutex mutex; + VkDevice device = VK_NULL_HANDLE; + struct Entry { + std::shared_ptr buffer; + std::vector bytes; + std::list::iterator recent; + }; + std::map entries; + std::list recency; + std::size_t bytes = 0; + void clear() { + entries.clear(); + recency.clear(); + bytes = 0; + } +}; + +SnapshotCopies& SnapshotCopyCache() { + static SnapshotCopies copies; + return copies; +} + +std::shared_ptr snapshotCopy(const Context& context, std::uint64_t address, std::span bytes, VkBufferUsageFlags usage) { + auto& copies = SnapshotCopyCache(); + std::lock_guard lock(copies.mutex); + if (copies.device != context.device) { + copies.clear(); + copies.device = context.device; + } + const SnapshotCopies::Key key{address, bytes.size(), usage}; + if (const auto found = copies.entries.find(key); found != copies.entries.end()) { + if (std::memcmp(found->second.bytes.data(), bytes.data(), bytes.size()) == 0) { + copies.recency.splice(copies.recency.end(), copies.recency, found->second.recent); + return found->second.buffer; + } + copies.recency.erase(found->second.recent); + copies.bytes -= bytes.size(); + copies.entries.erase(found); + } + auto buffer = std::make_shared(context, bytes.size(), usage); + std::memcpy(buffer->Bytes().data(), bytes.data(), bytes.size()); + if (bytes.size() > SnapshotCopies::Budget) return buffer; + while (!copies.recency.empty() && (copies.bytes + bytes.size() > SnapshotCopies::Budget || copies.entries.size() >= SnapshotCopies::MaxEntries)) { + const auto oldest = copies.entries.find(copies.recency.front()); + copies.bytes -= std::get<1>(oldest->first); + copies.entries.erase(oldest); + copies.recency.pop_front(); + } + copies.recency.push_back(key); + copies.entries.emplace(key, SnapshotCopies::Entry{buffer, {bytes.begin(), bytes.end()}, std::prev(copies.recency.end())}); + copies.bytes += bytes.size(); + return buffer; +} + bool mirrorsEnabled() { static const bool disabled = std::getenv("APS5_NO_LEASE_MIRROR") != nullptr; return !disabled; @@ -2072,6 +2138,23 @@ MirrorStats MirrorCounters() { return stats; } +std::vector AddressTableKey(std::span> writes, std::span own) { + std::vector key; + key.reserve(writes.size() * 2u + 1u + own.size() * 4u); + for (const auto& [begin, end] : writes) key.insert(key.end(), {begin, end}); + key.push_back(std::numeric_limits::max()); + for (const auto& range : own) key.insert(key.end(), {range.begin, range.end, range.deviceAddress, (static_cast(range.reserved) << 32u) | range.permissions}); + return key; +} + +void ClearSnapshotCopies(VkDevice device) { + auto& copies = SnapshotCopyCache(); + std::lock_guard lock(copies.mutex); + if (copies.device != device) return; + copies.clear(); + copies.device = VK_NULL_HANDLE; +} + void ClearImageMirrors(VkDevice device) { auto space = Spaces().current.load(); if (space != nullptr && space->device == device) { @@ -2356,10 +2439,18 @@ bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& return true; } -bool RegisteredReadableCovers(std::uint64_t address, std::size_t bytes) { +bool RegisteredReadableCovers(std::uint64_t address, std::size_t bytes, std::vector* pieces) { + if (pieces != nullptr) pieces->clear(); if (bytes == 0 || bytes > std::numeric_limits::max() - address) return false; const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); - return containingRange(lease, address, address + bytes) != nullptr; + const auto end = address + bytes; + for (auto at = address; at < end;) { + const auto* range = containingRange(lease, at, at + 1); + if (range == nullptr) return false; + if (pieces != nullptr) pieces->push_back({range->address, range->bytes, range->writable, range->gpu}); + at = range->address + range->bytes; + } + return true; } std::uint64_t RegisteredReadableEnd(std::uint64_t address) { @@ -2879,7 +2970,25 @@ void GuestBufferMemory::AddSnapshot(const GuestMemorySnapshot& snapshot) { } const auto offset = static_cast(snapshot.address - owner->begin); const auto* source = live ? reinterpret_cast(snapshot.address) : owner->snapshot.data() + offset; - Require(std::memcmp(source, snapshot.bytes.data(), snapshot.bytes.size()) == 0, "guest snapshot differs from registered memory"); + if (std::memcmp(source, snapshot.bytes.data(), snapshot.bytes.size()) != 0) { + std::size_t first = 0; + while (first < snapshot.bytes.size() && source[first] == snapshot.bytes[first]) ++first; + char text[256]; + std::snprintf(text, sizeof(text), "guest snapshot differs from registered memory: snapshot 0x%llx+0x%zx (%s region 0x%llx+0x%llx%s%s%s) differs first at +0x%zx", static_cast(snapshot.address), snapshot.bytes.size(), live ? "live" : "snapshot-backed", static_cast(owner->begin), static_cast(owner->end - owner->begin), owner->writable ? ", writable" : "", owner->hostBacked ? ", host-backed" : "", owner->mirror != nullptr ? ", mirrored" : "", first); + static const bool fatal = std::getenv("APS5_SNAPSHOT_CHECK_FATAL") != nullptr; + if (!live || fatal) throw std::runtime_error(text); + static std::mutex reportMutex; + static std::uint64_t mismatches = 0; + static auto lastReport = std::chrono::steady_clock::now() - std::chrono::seconds(60); + std::lock_guard lock(reportMutex); + ++mismatches; + const auto now = std::chrono::steady_clock::now(); + if (now - lastReport >= std::chrono::seconds(10)) { + lastReport = now; + std::fprintf(stderr, "[gpu] %s; the live region serves the GPU (%llu mismatches so far; APS5_SNAPSHOT_CHECK_FATAL=1 fails the dispatch instead)\n", text, static_cast(mismatches)); + } + return; + } if (profile) Snapshots().checked.fetch_add(1, std::memory_order_relaxed); return; } @@ -3267,14 +3376,31 @@ void GuestBufferMemory::UploadPrepare(bool addressable) { } } +void GuestBufferMemory::completeSpaceWrites() const { + auto* recorder = Recorder::Active(); + if (recorder == nullptr || Recorder::InCompletion()) return; + const auto& base = space->base; + std::vector> writes; + std::vector candidates; + std::size_t next = 0; + while (next < base.size()) { + recorder->PendingWriteRanges(writes); + OverlappedRanges(std::span(base), next, writes, candidates); + next = base.size(); + for (const auto index : candidates) { + if (!CompletePendingCpuWrites(context, base[index].begin, static_cast(base[index].end - base[index].begin))) continue; + next = index + 1; + break; + } + } +} + void GuestBufferMemory::UploadFinish(bool addressable) { Require(prepared && !uploaded, "guest memory upload was not prepared"); static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const auto started = std::chrono::steady_clock::now(); for (const auto& region : regions) CompletePendingCpuWrites(context, region.begin, static_cast(region.end - region.begin)); - if (space != nullptr) { - for (const auto& region : space->base) CompletePendingCpuWrites(context, region.begin, static_cast(region.end - region.begin)); - } + if (space != nullptr) completeSpaceWrites(); uploaded = true; for (const auto& [begin, end] : writes) StorageTexture::FlushPending(begin, static_cast(end - begin), nullptr, "written buffer range"); if (context.hostImportAlignment != 0) { @@ -3514,7 +3640,9 @@ void GuestBufferMemory::copyRegion(Region& region, bool addressable) { } const auto allocated = region.bufferEnd != 0 ? region.bufferEnd - region.begin : bytes; // A buffer made by UploadPrepare for a GPU copy that fell back here (its import went) is kept. - if (region.buffer == nullptr) region.buffer = std::make_shared(context, static_cast(allocated), usage); + const bool shared = region.buffer == nullptr && !region.hostBacked && !region.writable && !region.sparse && region.bufferEnd == 0 && region.snapshot.size() == bytes; + if (shared) region.buffer = snapshotCopy(context, region.begin, region.snapshot, usage); + else if (region.buffer == nullptr) region.buffer = std::make_shared(context, static_cast(allocated), usage); const auto readStart = std::chrono::steady_clock::now(); if (profile) allocateUs.fetch_add(microsecondsSince(allocateStart), std::memory_order_relaxed); // Named for the [hooksync] attribution: the reads below go through the flush hook. @@ -3544,7 +3672,7 @@ void GuestBufferMemory::copyRegion(Region& region, bool addressable) { } } else { - std::memcpy(region.buffer->Bytes().data(), region.snapshot.data(), region.snapshot.size()); + if (!shared) std::memcpy(region.buffer->Bytes().data(), region.snapshot.data(), region.snapshot.size()); readBytes += region.snapshot.size(); } region.snapshot.clear(); @@ -3840,12 +3968,12 @@ std::vector GuestBufferMemory::AddressRanges() } std::optional GuestBufferMemory::CachedAddressTable() const { - if (space == nullptr || !regions.empty()) return std::nullopt; + if (space == nullptr) return std::nullopt; Require(uploaded && !committed, "guest GPU address ranges are not available"); - if (writes.empty()) return CachedTable{space->serial, &space->ranges}; - if (writeTableRanges != nullptr) return CachedTable{writeTableSerial, writeTableRanges.get()}; + if (writes.empty() && regions.empty()) return CachedTable{space->serial, &space->ranges}; + if (tableRanges != nullptr) return CachedTable{tableSerial, tableRanges.get()}; const auto& spaceRanges = space->ranges; - const bool inWritable = std::all_of(writes.begin(), writes.end(), [&](const auto& written) { + const bool inWritable = regions.empty() && std::all_of(writes.begin(), writes.end(), [&](const auto& written) { auto at = std::partition_point(spaceRanges.begin(), spaceRanges.end(), [&](const auto& range) { return range.end <= written.first; }); for (; at != spaceRanges.end() && at->begin < written.second; ++at) { if ((at->permissions & ShaderRecompiler::BdaAbi::Write) == 0u) return false; @@ -3853,18 +3981,32 @@ std::optional GuestBufferMemory::CachedAddressTa return true; }); if (inWritable) return CachedTable{space->serial, &space->ranges}; + std::vector own; + own.reserve(regions.size()); + for (const auto& region : regions) own.push_back(addressRange(region)); + auto key = AddressTableKey(writes, own); + static const bool verify = std::getenv("APS5_VERIFY_BDA_TABLES") != nullptr; std::lock_guard lock(space->tableMutex); - const auto found = space->writeTables.find(writes); - if (found != space->writeTables.end()) { - writeTableSerial = found->second.first; - writeTableRanges = found->second.second; + const auto found = space->tables.find(key); + if (found != space->tables.end()) { + space->tableRecency.splice(space->tableRecency.end(), space->tableRecency, found->second.recent); + tableSerial = found->second.serial; + tableRanges = found->second.ranges; + if (verify) { + const auto built = AddressRanges(); + Require(built.size() == tableRanges->size() && (built.empty() || std::memcmp(built.data(), tableRanges->data(), built.size() * sizeof(built.front())) == 0), "a cached BDA table differs from the ranges of the build that found it"); + } } else { - writeTableRanges = std::make_shared>(AddressRanges()); - writeTableSerial = Spaces().serials.fetch_add(1, std::memory_order_relaxed) + 1; - if (space->writeTables.size() >= 64u) space->writeTables.erase(space->writeTables.begin()); - space->writeTables.emplace(writes, std::pair{writeTableSerial, writeTableRanges}); + tableRanges = std::make_shared>(AddressRanges()); + tableSerial = Spaces().serials.fetch_add(1, std::memory_order_relaxed) + 1; + if (space->tables.size() >= 64u) { + space->tables.erase(*space->tableRecency.front()); + space->tableRecency.pop_front(); + } + const auto inserted = space->tables.emplace(std::move(key), AddressSpace::Table{tableSerial, tableRanges, {}}).first; + inserted->second.recent = space->tableRecency.insert(space->tableRecency.end(), &inserted->first); } - return CachedTable{writeTableSerial, writeTableRanges.get()}; + return CachedTable{tableSerial, tableRanges.get()}; } bool GuestBufferMemory::HasCopiedWrites() const { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 55ecd30e30..87c57c1278 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -44,12 +44,20 @@ TextureDimension resolveDimension(std::uint32_t raw) { case 11: return TextureDimension::kCube; case 12: return TextureDimension::k1DArray; case 13: return TextureDimension::k2DArray; + case 14: return TextureDimension::k2DMsaa; default: throw std::runtime_error("AGC graphics: guest texture descriptor uses an unsupported image type " + std::to_string(raw)); } } } +bool IsNullTextureResource(std::span words) { + Require(words.size() == 8, "guest texture descriptor must contain 8 dwords"); + const auto base40 = (static_cast(words[0]) | (static_cast(words[1]) << 32u)) & 0xffffffffffull; + const auto type = (words[3] >> 28u) & 0xfu; + return base40 == 0 || type == 0; +} + GuestTextureResource DecodeTextureResource(std::span words) { Require(words.size() == 8, "guest texture descriptor must contain 8 dwords"); @@ -76,7 +84,7 @@ GuestTextureResource DecodeTextureResource(std::span words) const auto baseArray = (words[4] >> 16u) & 0x1fffu; const auto arrayPitch = (words[5] >> 0u) & 0xfu; - const auto maxMip = (words[5] >> 4u) & 0xfu; + auto maxMip = (words[5] >> 4u) & 0xfu; const auto minLodWarn = (words[5] >> 8u) & 0xfffu; const auto cornerSample = ((words[5] >> 23u) & 0x1u) != 0; const auto mipStatsCntEn = ((words[5] >> 25u) & 0x1u) != 0; @@ -138,6 +146,13 @@ GuestTextureResource DecodeTextureResource(std::span words) } catch (const std::exception& error) { throw std::runtime_error(std::string(error.what()) + describe()); } + std::uint32_t samples = 1; + if (dimension == TextureDimension::k2DMsaa) { + Require(baseLevel == 0 && lastLevel >= 1u && lastLevel <= 4u && maxMip == lastLevel, "guest multisampled texture descriptor does not hold the same sample count of 2 to 16 in its last level and its last mip, which is not implemented" + describe()); + samples = 1u << lastLevel; + lastLevel = 0; + maxMip = 0; + } // Views may name levels past MAX_MIP (a 512x512 view through 1x1 over a 9-level surface); the // hardware never addresses them, so the view ends at the surface's last level. if (baseLevel <= maxMip) lastLevel = std::min(lastLevel, maxMip); @@ -161,6 +176,9 @@ GuestTextureResource DecodeTextureResource(std::span words) } Require(depth == 0 && baseArray == 0, "guest 2D texture descriptor has a nonzero depth or base array"); break; + case TextureDimension::k2DMsaa: + Require(depth == 0 && baseArray == 0, "guest multisampled 2D texture descriptor has a nonzero depth or base array"); + break; case TextureDimension::k3D: // word4 DEPTH holds the depth minus one; BASE_ARRAY has no meaning for volumes. Require(baseArray == 0, "guest 3D texture descriptor has a nonzero base array"); @@ -207,6 +225,7 @@ GuestTextureResource DecodeTextureResource(std::span words) result.dccAlphaOnMsb = dccAlphaPos; result.dccPipeAligned = metaPipeAligned; result.minLod = minLod; + result.samples = samples; if (baseLevel > maxMip) { Require(LevelsFitAllocation(result, lastLevel + 1u), "guest texture descriptor starts past the surface's last mip level at levels that would move the surface's own" + describe()); result.mipCount = lastLevel + 1u; @@ -231,6 +250,7 @@ bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, Texture case ShaderRecompiler::DescriptorImageShape::ImageCube: return dimension == TextureDimension::kCube; case ShaderRecompiler::DescriptorImageShape::Image3D: return dimension == TextureDimension::k3D; case ShaderRecompiler::DescriptorImageShape::Image1DArray: return dimension == TextureDimension::k1DArray; + case ShaderRecompiler::DescriptorImageShape::Image2DMultisample: return dimension == TextureDimension::k2DMsaa; } throw std::runtime_error("AGC graphics: MatchesGuestDimension encountered an unknown descriptor image shape"); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 792b1395a0..f7a7a60740 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -1,9 +1,11 @@ #include #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include @@ -12,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -54,8 +57,10 @@ public: VkFormatProperties properties{}; context.formatProperties(context.physical, target.format, &properties); Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0, "multisampled render-target format cannot be a color attachment"); - constexpr VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; VkImageFormatProperties supported{}; + sampleable = (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0 && context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage | VK_IMAGE_USAGE_SAMPLED_BIT, 0, &supported) == VK_SUCCESS && (supported.sampleCounts & samples) != 0; + if (sampleable) usage |= VK_IMAGE_USAGE_SAMPLED_BIT; Check(context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0, &supported), "vkGetPhysicalDeviceImageFormatProperties multisampled target"); Require((supported.sampleCounts & samples) != 0 && target.extent.width <= supported.maxExtent.width && target.extent.height <= supported.maxExtent.height, "multisampled render target exceeds device image limits"); try { @@ -120,6 +125,23 @@ public: }); } + std::shared_ptr Sampled(VkComponentMapping components) { + Require(sampleable, "the device cannot sample the multisampled color target's format at its sample count"); + Require(cmask.Refusal().empty(), cmask.Refusal()); + Require(cmask.Keys() != DccKeys::Clear0000, "sampling a multisampled color target whose CMASK fast clear no draw has applied is not implemented"); + Require(Recorder::Active() != nullptr, "a multisampled color target is only sampled by recorded draws and dispatches"); + ApplyFastClear(target); + recordCommands(context, [&](VkCommandBuffer commands, Recorder*) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + Recorder::CountBarriers(Recorder::CommandClass::Draw, 1); + }); + const std::array key{static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a)}; + if (const auto found = textures.find(key); found != textures.end()) return found->second; + auto texture = std::make_shared(context, image, target.format, VK_IMAGE_ASPECT_COLOR_BIT, components); + textures.emplace(key, texture); + return texture; + } + VkRenderPass ResolvePass() { if (resolvePass != VK_NULL_HANDLE) return resolvePass; std::array attachments{}; @@ -164,7 +186,11 @@ public: MultisampledColorFill colorFill; private: + bool sampleable = false; + std::map, std::shared_ptr> textures; + void release() noexcept { + textures.clear(); if (resolvePass) context.Function("vkDestroyRenderPass")(context.device, resolvePass, nullptr); if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); @@ -264,6 +290,55 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { return created; } +MultisampleTarget& createFilled(const Context& context, const GuestTextureResource& resource, VkFormat format) { + const auto refuse = [&](const std::string& reason) { + char text[160]; + std::snprintf(text, sizeof(text), "AGC graphics: sampling the %ux%u multisampled texture 0x%llx of %u samples, which no draw rendered to and ", resource.width, resource.height, static_cast(resource.baseAddress), resource.samples); + throw std::runtime_error(text + reason + ", is not implemented"); + }; + ColorTarget use{}; + switch (resource.tileMode) { + case TextureTileMode::kStandard4KB: use.tileMode = ColorTileMode::Standard4KB; break; + case TextureTileMode::kStandard64KB: use.tileMode = ColorTileMode::Standard64KB; break; + case TextureTileMode::kR64KBX: use.tileMode = ColorTileMode::RenderTarget; break; + default: refuse("whose tile mode " + std::to_string(static_cast(resource.tileMode)) + " has no modeled multisampled color footprint"); + } + use.address = resource.baseAddress; + use.surfaceAddress = resource.baseAddress; + use.extent = {resource.width, resource.height}; + use.surfaceExtent = use.extent; + use.format = format; + use.elementBytes = BytesPerElement(resource.format); + use.pipeBankXor = resource.pipeBankXor; + use.samples = resource.samples; + const auto bytes = MultisampledColorBytes(use); + if (bytes == 0) refuse("whose " + std::to_string(use.elementBytes) + "-byte elements have no modeled multisampled color footprint"); + use.bytes = ColorTargetLayout(use.extent.width, use.extent.height, use.tileMode, use.elementBytes).Bytes(); + char range[64]; + std::snprintf(range, sizeof(range), "0x%llx+0x%llx", static_cast(use.address), static_cast(bytes)); + RequireNoMultisampleTarget(use.address, static_cast(bytes), "sampling a multisampled texture no draw rendered to"); + GuestMemory::FlushGpuWrites(use.address, static_cast(bytes)); + if (!GuestMemory::Accessible(reinterpret_cast(use.address), static_cast(bytes))) refuse(std::string("whose color memory ") + range + " is not mapped"); + const auto* memory = reinterpret_cast(use.address); + std::uint32_t value = 0; + std::memcpy(&value, memory, sizeof(value)); + char held[16]; + std::snprintf(held, sizeof(held), "0x%08x", value); + const auto repeated = std::mismatch(memory + sizeof(value), memory + bytes, memory).first; + if (repeated != memory + bytes) { + char offset[32]; + std::snprintf(offset, sizeof(offset), "+0x%llx", static_cast(repeated - memory)); + refuse(std::string("whose color memory ") + range + " does not hold one repeated 32-bit value (" + held + " until " + offset + ")"); + } + VkClearColorValue clear{}; + if (!MultisampledFillClearColor(use, value, clear)) refuse(std::string("whose color memory ") + range + " holds " + held + ", which has no exact clear value in its format"); + targets().push_back(std::make_unique(context, use)); + auto& created = *targets().back(); + guard(context, use, created.active); + created.colorFill.NoteFill(use.address, static_cast(bytes), value); + return created; +} + } void MultisampledCmask::NoteFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern) { @@ -380,6 +455,31 @@ VkImageView MultisampleTargetView(const Context& context, const ColorTarget& tar return acquire(context, target).view; } +std::shared_ptr MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components) { + std::lock_guard lock(targetsMutex()); + const auto format = ResolveTextureFormat(resource.format); + const MultisampleTarget* other = nullptr; + for (const auto& target : targets()) { + if (!target->active->load(std::memory_order_acquire)) continue; + if (target->context.device != context.device || target->target.surfaceAddress != resource.baseAddress) continue; + const auto& rendered = target->target; + if (rendered.format == format && rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0) return target->Sampled(components); + other = target.get(); + } + if (other == nullptr) { + Require(Recorder::Active() != nullptr, "a multisampled texture is only sampled by recorded draws and dispatches"); + return createFilled(context, resource, format).Sampled(components); + } + char text[320]; + std::snprintf(text, sizeof(text), "AGC graphics: sampling multisampled color target 0x%llx (%ux%u, %u samples, vk format %d) as a %ux%u texture of %u samples and guest format %u (vk %d) is not implemented", static_cast(resource.baseAddress), other->target.extent.width, other->target.extent.height, other->target.samples, static_cast(other->target.format), resource.width, resource.height, resource.samples, resource.format, static_cast(format)); + throw std::runtime_error(text); +} + +void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget& target) { + std::lock_guard lock(targetsMutex()); + acquire(context, target); +} + void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView) { std::lock_guard lock(targetsMutex()); auto& target = acquire(context, source); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 6096ed0aeb..2b9f187a9c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -159,6 +159,20 @@ bool ShadersWriteMemory(std::span shaders) { return false; } +void ValidateSampleMask(const Context& context, const State& state, std::span shaders) { + if (!PartialSampleMask(state)) return; + for (const auto& shader : shaders) { + if (shader.stage != ShaderRecompiler::ShaderStage::Fragment) continue; + Require(shader.program != nullptr, "missing compiled fragment shader"); + if (UniformSampleMask(state)) { + Require(context.earlyFragmentSampleMaskTestBeforeSampleCounting || !DeclaresEarlyFragmentTests(*shader.program), "a sample mask with early fragment tests needs VkPhysicalDeviceMaintenance5Properties::earlyFragmentSampleMaskTestBeforeSampleCounting, which the device lacks"); + continue; + } + const bool tested = state.depth.has_value() && (state.depthTest || state.stencilTest || state.depthBoundsTest); + Require(!state.pixelEarlyZ || !tested || !ShadersWriteMemory(std::span(&shader, 1)), "a sample mask that differs between the pixels of a quad, from a pixel program with early depth or stencil tests that writes memory, is unsupported: the masked pixels are removed in the shader, after the tests"); + } +} + namespace { thread_local bool prebuilding = false; @@ -243,6 +257,8 @@ 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.fragmentShader, HostSampleMask(state)); + appendKey(keys.fragmentOutput, HostSampleMask(state)); if (!resources.split) { appendKey(keys.layout, pushStagesOf(context)); for (const auto word : resources.mainKey) appendKey(keys.layout, word); @@ -327,6 +343,12 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput specializations.emplace_back(shader); stages[i].pSpecializationInfo = specializations.back().Info(); } + if (std::ranges::any_of(shaders, [](const CompiledShader& shader) { return shader.program != nullptr && !shader.program->stencilReferences.empty(); })) { + for (std::size_t i = 0; i < shaders.size(); ++i) { + if (shaders[i].stage != ShaderRecompiler::ShaderStage::Vertex) continue; + Require(stages[i].flags == 0u && stages[i].pNext == nullptr, "stencil replay vertex stage allows varying subgroup size"); + } + } // A descriptor set layout with the same bindings as this one is compatible with the pipeline // layout, so later draws bind their own ShaderResources' set under it. const std::array setLayouts{resources.main, resources.fragment}; @@ -413,6 +435,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput PipelineLibraryKeys keys; libraries = libraryKeys(keys, context, state, vertexInput, resources, shaders); std::vector dynamicStates{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + if (state.stencilReplayReference) dynamicStates.push_back(VK_DYNAMIC_STATE_STENCIL_REFERENCE); if (depthBounds) dynamicStates.push_back(VK_DYNAMIC_STATE_DEPTH_BOUNDS); if (depthBias) dynamicStates.push_back(VK_DYNAMIC_STATE_DEPTH_BIAS); if (libraries) dynamicStates.assign(PipelineLibraryDynamicStates().begin(), PipelineLibraryDynamicStates().end()); @@ -454,6 +477,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput depthStencil.stencilTestEnable = state.stencilTest; depthStencil.front = state.stencilFront; depthStencil.back = state.stencilBack; + const VkSampleMask sampleMask = HostSampleMask(state); + if (PartialSampleMask(state) && UniformSampleMask(state)) samples.pSampleMask = &sampleMask; VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; const bool alternateAlpha = SplitsAlternateAlpha(state); std::vector firstChannels(state.blends.begin(), state.blends.end()); @@ -538,6 +563,7 @@ 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) { + if (std::ranges::any_of(shaders, [](const CompiledShader& shader) { return !shader.program->stencilReferences.empty(); })) return false; struct Scope { Scope() { prebuilding = true; prebuilt = false; } ~Scope() { prebuilding = false; } @@ -690,6 +716,7 @@ void Pipeline::Continue(VkCommandBuffer commands, const State& state) const { context.Resolved(&DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, best != VK_NULL_HANDLE ? best : pipeline); context.Resolved(&DeviceFunctions::cmdSetViewport, "vkCmdSetViewport")(commands, 0, 1, &state.viewport); context.Resolved(&DeviceFunctions::cmdSetScissor, "vkCmdSetScissor")(commands, 0, 1, &state.scissor); + if (state.stencilReplayReference) context.Resolved(&DeviceFunctions::cmdSetStencilReference, "vkCmdSetStencilReference")(commands, VK_STENCIL_FACE_FRONT_AND_BACK, *state.stencilReplayReference); if (libraries) { setLibraryState(commands, state); return; @@ -828,8 +855,12 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, state.depthBias); append(key, state.depthBiasPerFace); append(key, state.stencilTest); - append(key, state.stencilFront); - append(key, state.stencilBack); + auto front = state.stencilFront; + auto back = state.stencilBack; + if (state.stencilReplayReference) front.reference = back.reference = 0u; + append(key, state.stencilReplayReference.has_value()); + append(key, front); + append(key, back); } append(key, state.stages.mesh.has_value()); if (state.stages.mesh) { @@ -855,6 +886,7 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, tessellation.primitiveGroupSize); append(key, tessellation.breakAtInstance); } + append(key, HostSampleMask(state)); } std::uint64_t hashKey(const std::vector& key) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp index ec51c5a20b..8b5d426e59 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp @@ -1,9 +1,12 @@ #include "prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp" #include "ShaderCacheDirectory.hpp" +#include #include #include #include #include +#include +#include #include #include #include @@ -15,22 +18,25 @@ namespace { constexpr std::uint32_t FileMagic = 0x50565041u; constexpr std::uint32_t FileFormat = 1; -struct FileHeader { - std::uint32_t magic; - std::uint32_t format; - std::uint64_t dataBytes; - std::uint64_t dataHash; -}; -static_assert(sizeof(FileHeader) == 24); - bool profiling() { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; return profile; } +std::mutex& pathMutex(const std::filesystem::path& path) { + struct Registry { + std::mutex mutex; + std::map paths; + }; + static auto& registry = *new Registry; + std::lock_guard lock(registry.mutex); + return registry.paths[path]; +} + } PipelineCache::PipelineCache(const Context& context, const VkPhysicalDeviceProperties& properties) : context(context), properties(properties) { + static_assert(sizeof(FileHeader) == 24); const auto directory = ShaderRecompiler::ShaderCacheDirectory(); if (!directory.empty()) { std::string uuid; @@ -44,7 +50,10 @@ PipelineCache::PipelineCache(const Context& context, const VkPhysicalDevicePrope path = directory / name; } std::vector initialData; - if (!path.empty()) load(initialData); + if (!path.empty()) { + std::lock_guard pathLock(pathMutex(path)); + load(initialData); + } VkPipelineCacheCreateInfo info{VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO}; info.initialDataSize = initialData.size(); info.pInitialData = initialData.empty() ? nullptr : initialData.data(); @@ -54,7 +63,9 @@ PipelineCache::PipelineCache(const Context& context, const VkPhysicalDevicePrope std::fprintf(stderr, "[pipeline-cache] the driver refused %s (Vulkan result %d); starting empty\n", path.string().c_str(), static_cast(result)); info.initialDataSize = 0; info.pInitialData = nullptr; - savedBytes = 0; + loadedBytes = 0; + settledBytes = 0; + settledFile.reset(); result = create(context.device, &info, nullptr, &cache); } Check(result, "vkCreatePipelineCache"); @@ -74,19 +85,37 @@ PipelineCache::~PipelineCache() { context.Function("vkDestroyPipelineCache")(context.device, cache, nullptr); } -void PipelineCache::load(std::vector& initialData) { - std::vector file; - if (!ShaderRecompiler::ReadWholeFile(path, file)) return; - const auto reject = [&](const char* why) { - std::fprintf(stderr, "[pipeline-cache] ignoring %s: %s\n", path.string().c_str(), why); - }; - FileHeader header{}; - if (file.size() < sizeof(header)) return reject("truncated"); - std::memcpy(&header, file.data(), sizeof(header)); - if (header.magic != FileMagic || header.format != FileFormat) return reject("another format"); - if (header.dataBytes != file.size() - sizeof(header)) return reject("truncated"); - const auto data = std::span(file).subspan(sizeof(header)); - if (ShaderRecompiler::HashBytes(data) != header.dataHash) return reject("checksum mismatch"); +std::span PipelineCache::File::Payload() const { + return std::span(contents).subspan(sizeof(FileHeader)); +} + +PipelineCache::File PipelineCache::readFile() const { + File file; + if (!ShaderRecompiler::ReadWholeFile(path, file.contents)) { + std::error_code error; + const bool exists = std::filesystem::exists(path, error); + file.state = exists || error ? FileState::Unreadable : FileState::Missing; + return file; + } + file.state = FileState::Invalid; + if (file.contents.size() < sizeof(FileHeader)) { + file.invalidReason = "truncated"; + return file; + } + std::memcpy(&file.header, file.contents.data(), sizeof(FileHeader)); + if (file.header.magic != FileMagic || file.header.format != FileFormat) { + file.invalidReason = "another format"; + return file; + } + if (file.header.dataBytes != file.contents.size() - sizeof(FileHeader)) { + file.invalidReason = "truncated"; + return file; + } + const auto data = file.Payload(); + if (ShaderRecompiler::HashBytes(data) != file.header.dataHash) { + file.invalidReason = "checksum mismatch"; + return file; + } struct VulkanHeader { std::uint32_t headerSize; std::uint32_t headerVersion; @@ -95,35 +124,124 @@ void PipelineCache::load(std::vector& initialData) { std::uint8_t uuid[VK_UUID_SIZE]; } vulkan{}; static_assert(sizeof(VulkanHeader) == 32); - if (data.size() < sizeof(vulkan)) return reject("no Vulkan header"); + if (data.size() < sizeof(vulkan)) { + file.invalidReason = "no Vulkan header"; + return file; + } std::memcpy(&vulkan, data.data(), sizeof(vulkan)); - if (vulkan.headerSize < sizeof(vulkan) || vulkan.headerVersion != VK_PIPELINE_CACHE_HEADER_VERSION_ONE || vulkan.vendorID != properties.vendorID || vulkan.deviceID != properties.deviceID || std::memcmp(vulkan.uuid, properties.pipelineCacheUUID, VK_UUID_SIZE) != 0) return reject("made for another device or driver"); + if (vulkan.headerSize < sizeof(vulkan) || vulkan.headerVersion != VK_PIPELINE_CACHE_HEADER_VERSION_ONE || vulkan.vendorID != properties.vendorID || vulkan.deviceID != properties.deviceID || std::memcmp(vulkan.uuid, properties.pipelineCacheUUID, VK_UUID_SIZE) != 0) { + file.invalidReason = "made for another device or driver"; + return file; + } + file.state = FileState::Valid; + return file; +} + +std::optional PipelineCache::readHeader() const { + std::ifstream file(path, std::ios::binary); + FileHeader header{}; + if (!file.read(reinterpret_cast(&header), sizeof(header))) return std::nullopt; + return header; +} + +void PipelineCache::load(std::vector& initialData) { + const auto file = readFile(); + if (file.state == FileState::Missing) return; + if (file.state == FileState::Unreadable) { + std::fprintf(stderr, "[pipeline-cache] cannot read %s; starting empty\n", path.string().c_str()); + return; + } + if (file.state == FileState::Invalid) { + std::fprintf(stderr, "[pipeline-cache] ignoring %s: %s\n", path.string().c_str(), file.invalidReason); + return; + } + const auto data = file.Payload(); initialData.assign(data.begin(), data.end()); - savedBytes = initialData.size(); + loadedBytes = initialData.size(); + settledBytes = initialData.size(); + settledFile = file.header; std::fprintf(stderr, "[pipeline-cache] loaded %.1f KiB from %s\n", static_cast(initialData.size()) / 1024.0, path.string().c_str()); } void PipelineCache::save(bool final) { + std::lock_guard pathLock(pathMutex(path)); const auto getData = context.Function("vkGetPipelineCacheData"); + std::size_t liveBytes = 0; + { + auto synchronizationLock = synchronization.LockForSnapshot(); + if (getData(context.device, cache, &liveBytes, nullptr) != VK_SUCCESS || liveBytes == 0) return; + } + if (liveBytes == settledBytes && readHeader() == settledFile) return; + const auto started = std::chrono::steady_clock::now(); + const auto settle = [&](const std::optional& file) { + settledBytes = liveBytes; + settledFile = file; + }; + const auto disk = readFile(); + if (disk.state == FileState::Unreadable) { + std::fprintf(stderr, "[pipeline-cache] cannot read %s; not saving over it\n", path.string().c_str()); + settle(readHeader()); + return; + } + if (disk.state == FileState::Invalid) std::fprintf(stderr, "[pipeline-cache] replacing %s: %s\n", path.string().c_str(), disk.invalidReason); + std::size_t floor = 0; + VkPipelineCache merged = VK_NULL_HANDLE; + struct DestroyMerged { + const Context& context; + const VkPipelineCache& cache; + ~DestroyMerged() { + if (cache != VK_NULL_HANDLE) context.Function("vkDestroyPipelineCache")(context.device, cache, nullptr); + } + } destroyMerged{context, merged}; + if (disk.state == FileState::Valid) { + const auto data = disk.Payload(); + VkPipelineCacheCreateInfo info{VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO}; + info.initialDataSize = data.size(); + info.pInitialData = data.data(); + const auto result = context.Function("vkCreatePipelineCache")(context.device, &info, nullptr, &merged); + if (result == VK_SUCCESS) { + floor = std::max(loadedBytes, data.size()); + } else { + merged = VK_NULL_HANDLE; + std::fprintf(stderr, "[pipeline-cache] replacing %s: the driver refused it (Vulkan result %d)\n", path.string().c_str(), static_cast(result)); + } + } + if (merged != VK_NULL_HANDLE) { + auto synchronizationLock = synchronization.LockForSnapshot(); + const auto result = context.Function("vkMergePipelineCaches")(context.device, merged, 1, &cache); + if (result != VK_SUCCESS) { + std::fprintf(stderr, "[pipeline-cache] cannot merge into %s (Vulkan result %d); not saving over it\n", path.string().c_str(), static_cast(result)); + return; + } + } + const auto source = merged != VK_NULL_HANDLE ? merged : cache; std::size_t size = 0; std::vector file; { - auto synchronizationLock = synchronization.LockForSnapshot(); - if (getData(context.device, cache, &size, nullptr) != VK_SUCCESS || size == 0 || size == savedBytes) return; + auto synchronizationLock = source == cache ? synchronization.LockForSnapshot() : PipelineCacheSynchronization::SnapshotLock{}; + if (getData(context.device, source, &size, nullptr) != VK_SUCCESS || size == 0) return; + if (merged != VK_NULL_HANDLE && size == disk.header.dataBytes) { + settle(disk.header); + return; + } file.resize(sizeof(FileHeader) + size); - const auto result = getData(context.device, cache, &size, file.data() + sizeof(FileHeader)); + const auto result = getData(context.device, source, &size, file.data() + sizeof(FileHeader)); if (result != VK_SUCCESS && result != VK_INCOMPLETE) return; file.resize(sizeof(FileHeader) + size); } + if (size < floor) { + std::fprintf(stderr, "[pipeline-cache] keeping %s: the merged cache has %.1f KiB, less than the %.1f KiB on disk or loaded at start\n", path.string().c_str(), static_cast(size) / 1024.0, static_cast(floor) / 1024.0); + settle(disk.header); + return; + } const FileHeader header{FileMagic, FileFormat, size, ShaderRecompiler::HashBytes(std::span(file).subspan(sizeof(FileHeader)))}; std::memcpy(file.data(), &header, sizeof(header)); - const auto started = std::chrono::steady_clock::now(); if (!ShaderRecompiler::WriteFileAtomically(path, file)) { std::fprintf(stderr, "[pipeline-cache] cannot write %s\n", path.string().c_str()); return; } - savedBytes = size; - if (final || profiling()) std::fprintf(stderr, "[pipeline-cache] saved %.1f KiB to %s in %.1f ms%s\n", static_cast(size) / 1024.0, path.string().c_str(), std::chrono::duration(std::chrono::steady_clock::now() - started).count(), final ? " (teardown)" : ""); + settle(header); + if (final || profiling()) std::fprintf(stderr, "[pipeline-cache] saved %.1f KiB to %s in %.1f ms%s%s\n", static_cast(size) / 1024.0, path.string().c_str(), std::chrono::duration(std::chrono::steady_clock::now() - started).count(), merged != VK_NULL_HANDLE ? " (merged with the file)" : "", final ? " (teardown)" : ""); } void PipelineCache::run() { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 6c4f039b89..cf016e12ae 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -30,6 +30,7 @@ #include #include #include +#include "prx/libc/include/general/AtomicSharedPtr.hpp" namespace AgcDriver::Graphics { @@ -634,7 +635,7 @@ using WriteRanges = Recorder::WriteRanges; // work. A range leaves the snapshot only after its batch's completions (CPU write-backs) ran, so a // reader that sees no overlap either precedes the note (the queues are unordered then, as on the // GPU) or follows the write-back. -std::atomic> pendingWrites; +AtomicSharedPtr pendingWrites; bool HookSnapshotEnabled() { // Debug aid: APS5_NO_HOOK_SNAPSHOT=1 takes the GpuMutex on every access as before. @@ -2518,6 +2519,31 @@ void Recorder::eraseDrawSnapshot(std::map::iterat drawSnapshots.erase(entry); } +void Recorder::retireUnwrittenSnapshots(const std::array& stamp) { + if (stamp == unwrittenStamp) return; + unwrittenSnapshots.clear(); + unwrittenBytes = 0; + unwrittenStamp = stamp; +} + +std::shared_ptr Recorder::UnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp) { + retireUnwrittenSnapshots(stamp); + const auto found = unwrittenSnapshots.find(address); + if (found == unwrittenSnapshots.end() || found->second.bytes != bytes) return {}; + return found->second.buffer; +} + +void Recorder::NoteUnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp, std::shared_ptr buffer) { + retireUnwrittenSnapshots(stamp); + if (const auto found = unwrittenSnapshots.find(address); found != unwrittenSnapshots.end()) { + unwrittenBytes -= found->second.bytes; + unwrittenSnapshots.erase(found); + } + if (unwrittenSnapshots.size() >= UnwrittenSnapshotEntries || bytes > DrawSnapshotBudget - unwrittenBytes) return; + unwrittenSnapshots.emplace(address, UnwrittenSnapshot{bytes, std::move(buffer)}); + unwrittenBytes += bytes; +} + std::shared_ptr Recorder::ReusableDrawSnapshot(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::uint32_t* derived, std::uint64_t generation) { auto found = use == SnapshotUse::Vertex ? drawSnapshots.lower_bound({address, use, bytes}) : drawSnapshots.find({address, use, bytes}); if (found == drawSnapshots.end() || std::get<0>(found->first) != address || std::get<1>(found->first) != use) return {}; @@ -2843,6 +2869,12 @@ std::vector Recorder::PendingWritesOver(std::uint64_t ad return found; } +void Recorder::PendingWriteRanges(std::vector>& ranges) const { + ranges.clear(); + if (open != nullptr) ranges.insert(ranges.end(), open->writes.begin(), open->writes.end()); + for (const auto& batch : inFlight) ranges.insert(ranges.end(), batch->writes.begin(), batch->writes.end()); +} + bool Recorder::ReadTracking() { return ReadTrackingEnabled(); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index df4bfe5fe8..814b5179fd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -24,6 +24,7 @@ constexpr std::uint32_t spiPsInputCntl0 = 0x191; constexpr std::uint32_t spiPsInputEna = 0x1B3; constexpr std::uint32_t spiPsInputAddr = 0x1B4; constexpr std::uint32_t spiPsInControl = 0x1B6; +constexpr std::uint32_t spiBarycCntl = 0x1B8; constexpr std::uint32_t dbShaderControl = 0x203; constexpr std::uint32_t spiShaderColFormat = 0x1C5; constexpr std::uint32_t defaultPixelInputs = 0x2u; @@ -143,8 +144,6 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con const bool depthExportEnable = (shaderControl & 0x1u) != 0; const bool sampleMaskExportEnable = ((shaderControl >> 8u) & 0x1u) != 0; const bool stencilRefExportEnable = ((shaderControl >> 1u) & 0x1u) != 0; - const bool depthBeforeShader = ((shaderControl >> 12u) & 0x1u) != 0; - const auto zOrder = (shaderControl >> 4u) & 0x3u; const auto loaded = [&](PixelInput input) { return (activeInputs & PixelInputBit(input)) != 0; }; return ShaderRecompiler::ShaderPixelStageInfo{ .interpolatorCount = inputNum, @@ -158,6 +157,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con .posZ = loaded(PixelInput::PositionZ), .posW = loaded(PixelInput::PositionW), .frontFace = loaded(PixelInput::FrontFace), + .frontFaceAllBits = loaded(PixelInput::FrontFace) && ((read(context, spiBarycCntl, RegisterBank::Context) >> 24u) & 0x1u) != 0, .ancillary = loaded(PixelInput::Ancillary), .sampleShading = loaded(PixelInput::PerspectiveSample) && loaded(PixelInput::LinearSample), .noPerspective = loaded(PixelInput::LinearCenter), @@ -165,7 +165,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con .pixelKillEnable = pixelKillEnable, .depthExportEnable = depthExportEnable, .sampleMaskExportEnable = sampleMaskExportEnable, - .earlyZ = depthBeforeShader || ((zOrder & 1u) != 0u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable && !stencilRefExportEnable), + .earlyZ = PixelEarlyZ(shaderControl), .executeOnNoop = ((shaderControl >> 10u) & 0x1u) != 0, .conservativeZExport = static_cast(conservativeZExport), .orderedPixelShader = ((shaderControl >> 16u) & 0x1u) != 0, @@ -175,6 +175,16 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con }; } +bool PixelEarlyZ(std::uint32_t shaderControl) { + 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 bool depthBeforeShader = ((shaderControl >> 12u) & 0x1u) != 0; + const auto zOrder = (shaderControl >> 4u) & 0x3u; + return depthBeforeShader || ((zOrder & 1u) != 0u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable && !stencilRefExportEnable); +} + ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span header, std::uint64_t headerAddress, std::span userData, std::vector* reads, bool staticAbi, std::uint32_t hiddenUserWords) { if (header.empty()) return {}; if (header.size() < sizeof(Shader)) throw std::runtime_error("AGC graphics: shader header is smaller than the fixed AGC header"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index ea64c982c2..7a97bde178 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -115,6 +115,9 @@ struct CachedTexture { std::uint64_t accounted = 0; std::uint64_t present = 0; std::optional>> committed; + // The untrackedKey the snapshot was last read or compared equal at, for a surface outside the + // write watch; zero when none was taken. + std::array untracked{}; }; // Entries in use order (front = most recent) with a hash index by key: a lookup is O(1) and the @@ -340,6 +343,10 @@ struct TextureCounters { std::atomic ownRefreshes{0}; std::atomic tablesReused{0}; std::atomic tablesResolved{0}; + // Snapshot hits over memory outside the write watch: kept by an unchanged UntrackedHint key, or + // by a byte compare. + std::atomic untrackedKept{0}; + std::atomic untrackedCompared{0}; std::atomic lastReport{0}; }; @@ -356,7 +363,7 @@ void reportTextureCounters() { auto last = counters.lastReport.load(); if (nowMs - last < 10000 || !counters.lastReport.compare_exchange_strong(last, nowMs)) return; const auto count = [](const std::atomic& value) { return static_cast(value.load(std::memory_order_relaxed)); }; - AgcDriver::ProfilePrint_nid_no_patch("[textures] sampled created: %llu from storage images, %llu snapshots (%llu snapshot reads over recorded writes inside cachedTexture, %llu storage-path fallbacks); stage-A records %llu: %llu fast hits, %llu full lookups; storage images %llu hits, %llu created, %llu own-object refreshes; image tables %llu reused, %llu resolved\n", count(counters.fromStorage), count(counters.snapshots), count(counters.pendingReads), count(counters.storageFallbacks), count(counters.records), count(counters.fastHits), count(counters.fastMisses), count(counters.storageHits), count(counters.storageCreated), count(counters.ownRefreshes), count(counters.tablesReused), count(counters.tablesResolved)); + AgcDriver::ProfilePrint_nid_no_patch("[textures] sampled created: %llu from storage images, %llu snapshots (%llu snapshot reads over recorded writes inside cachedTexture, %llu storage-path fallbacks); stage-A records %llu: %llu fast hits, %llu full lookups; storage images %llu hits, %llu created, %llu own-object refreshes; image tables %llu reused, %llu resolved; untracked snapshots %llu kept by the write-watch hint, %llu compared\n", count(counters.fromStorage), count(counters.snapshots), count(counters.pendingReads), count(counters.storageFallbacks), count(counters.records), count(counters.fastHits), count(counters.fastMisses), count(counters.storageHits), count(counters.storageCreated), count(counters.ownRefreshes), count(counters.tablesReused), count(counters.tablesResolved), count(counters.untrackedKept), count(counters.untrackedCompared)); } // What the sampled-texture lookups on this thread proved their returned objects current against, @@ -484,7 +491,25 @@ std::uint64_t forcedCacheBudget() { std::uint64_t storageBudget(const Context& context) { const auto forced = forcedCacheBudget(); - return forced != 0 ? forced : TextureCacheBudget(context.memory); + if (forced != 0) return forced; + if (context.memoryProperties2 == nullptr || context.physical == VK_NULL_HANDLE) return TextureCacheBudget(context.memory); + static struct { + std::mutex mutex; + VkPhysicalDevice device{VK_NULL_HANDLE}; + std::chrono::steady_clock::time_point read{}; + std::uint64_t budget{0}; + } cached; + const auto now = std::chrono::steady_clock::now(); + std::lock_guard lock(cached.mutex); + if (cached.device == context.physical && now - cached.read < std::chrono::seconds(1)) return cached.budget; + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2}; + properties.pNext = &reported; + context.memoryProperties2(context.physical, &properties); + cached.device = context.physical; + cached.read = now; + cached.budget = StorageTextureBudget(context.memory, &reported); + return cached.budget; } std::uint64_t sampledBudget(const Context& context, TextureCache& cache) { @@ -503,7 +528,7 @@ std::uint64_t sampledBudget(const Context& context, TextureCache& cache) { if (const auto heap = LargestDeviceLocalHeap(context.memory); heap && (fresh || SampledBudgetReportDue(cache.reportedBudget, budget, now - cache.reportedAt))) { char was[48] = ""; if (!fresh) std::snprintf(was, sizeof(was), " (was %llu MiB)", static_cast(cache.reportedBudget >> 20u)); - std::fprintf(stderr, "[gpu] sampled texture cache budget %llu MiB%s: device-local heap %u has a VK_EXT_memory_budget budget of %llu MiB and uses %llu MiB, %llu MiB of it sampled textures (%llu MiB in the cache) and %llu MiB cached storage images; storage image cache budget %llu MiB\n", static_cast(budget >> 20u), was, *heap, static_cast(reported.heapBudget[*heap] >> 20u), static_cast(reported.heapUsage[*heap] >> 20u), static_cast(sampled >> 20u), static_cast(cache.bytes >> 20u), static_cast(storage >> 20u), static_cast(TextureCacheBudget(context.memory) >> 20u)); + std::fprintf(stderr, "[gpu] sampled texture cache budget %llu MiB%s: device-local heap %u has a VK_EXT_memory_budget budget of %llu MiB and uses %llu MiB, %llu MiB of it sampled textures (%llu MiB in the cache) and %llu MiB cached storage images; storage image cache budget %llu MiB\n", static_cast(budget >> 20u), was, *heap, static_cast(reported.heapBudget[*heap] >> 20u), static_cast(reported.heapUsage[*heap] >> 20u), static_cast(sampled >> 20u), static_cast(cache.bytes >> 20u), static_cast(storage >> 20u), static_cast(storageBudget(context) >> 20u)); cache.reportedBudget = budget; cache.reportedAt = now; } @@ -513,9 +538,27 @@ std::uint64_t sampledBudget(const Context& context, TextureCache& cache) { return budget; } +// A snapshot over memory outside the write watch is compared with guest memory again only when this +// key changed: a write-watch mark in the range, a driver store, a mapping change or a recorded GPU +// write (as StorageTexture::compareUntracked). Taken before the snapshot is read or compared, so a +// CPU store racing with that leaves a mark for the next key. Zero when the range has no hint. +std::array untrackedKey(std::uint64_t address, std::size_t bytes) { + if (GuestMemory::CollectEpoch() == 0) return {}; + const auto hint = GuestMemory::UntrackedHint(address, bytes); + if (hint == 0) return {}; + const auto* recorder = Recorder::Active(); + const std::array key{hint, GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetSerial(), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0}; + return (key[2] & 1u) == 0 ? key : std::array{}; +} + // `guestBytes` is the surface size when the caller described the surface already (0: described here). std::shared_ptr cachedTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components, std::uint64_t guestBytes = 0, bool depthCompare = false) { CaptureTrace::Log("sampled-lookup address=%llx width=%u height=%u dcc=%llx", static_cast(resource.baseAddress), resource.width, resource.height, static_cast(resource.dccAddress)); + if (resource.dimension == TextureDimension::k2DMsaa) { + auto target = MultisampleTargetTexture(context, resource, components); + logLookup({target.get(), resource, 0, DccKeys::Uncompressed, 0, nullptr, true}); + return target; + } RequireNoMultisampleTarget(resource.baseAddress, guestBytes != 0 ? static_cast(guestBytes) : 1u, "a texture read"); if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes(); if (auto depth = DepthSurfaceTexture(context, words, resource, components)) { @@ -644,6 +687,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span{}; auto& cache = Textures(); const auto key = MakeTextureKey(context.device, words, components, depthCompare); std::lock_guard lock(cache.mutex); @@ -676,9 +720,16 @@ std::shared_ptr cachedTexture(const Context& context, std::spancommitted != committed) return false; return committed.has_value() ? !Recorder::SnapshotWriteOverlaps(address, it->bytes.size()) && GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation); }; - if ((*keys != DccKeys::Uncompressed && *keys != DccKeys::ClearSingle) || unchanged() || equalsCommitted()) { + const auto kept = [&] { + if (untracked[0] == 0) return false; + const bool same = it->untracked == untracked && !Recorder::SnapshotWriteOverlaps(address, bytes); + (same ? counters.untrackedKept : counters.untrackedCompared).fetch_add(1, std::memory_order_relaxed); + return same; + }; + if ((*keys != DccKeys::Uncompressed && *keys != DccKeys::ClearSingle) || unchanged() || kept() || equalsCommitted()) { it->generation = snapshotGeneration; it->committed = std::move(committed); + it->untracked = untracked; touchTexture(cache, it); logLookup({it->texture.get(), resource, guestBytes, *keys, generation, nullptr}); reportTextureCounters(); @@ -707,8 +758,10 @@ std::shared_ptr cachedTexture(const Context& context, std::span cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& viewed, std::uint32_t mip, std::uint64_t guestBytes) { + Require(viewed.dimension != TextureDimension::k2DMsaa, "storage image access to a multisampled texture is not implemented"); RequireNoMultisampleTarget(viewed.baseAddress, guestBytes != 0 ? static_cast(guestBytes) : 1u, "a storage image access"); if (auto depth = DepthSurfaceStorage(context, words, viewed, mip)) return depth; static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; @@ -936,6 +990,12 @@ std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory) return std::max(2048ull << 20u, heap ? memory.memoryHeaps[*heap].size / 4u : 0u); } +std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported) { + const auto heap = LargestDeviceLocalHeap(memory); + if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory); + return std::max(512ull << 20u, std::min(TextureCacheBudget(memory), reported->heapBudget[*heap] / 4u)); +} + std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes) { const auto heap = LargestDeviceLocalHeap(memory); if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory); @@ -2003,7 +2063,7 @@ void ShaderResources::noteReusable() { captureValidation(); reusable = false; directRegions.clear(); - if (guestMemory.HasCopiedWrites() || HoldsLease() || usesBda || (bda != nullptr && LoopGuardEnabled())) return; + if (guestMemory.HasCopiedWrites() || HoldsLease() || usesBda || !multisampleSources.empty() || (bda != nullptr && LoopGuardEnabled())) return; if (std::any_of(textures.begin(), textures.end(), [](const std::shared_ptr& texture) { return texture != nullptr && texture->RefreshedPerUse(); })) return; if (TemplateDataRefresh() && std::any_of(allocations.begin(), allocations.end(), [](const Allocation& allocation) { return allocation.buffer != nullptr && !allocation.guest && allocation.role != ShaderRecompiler::DescriptorRole::ImageTableMap && allocation.size > MaxRefreshBytes; })) return; const auto regions = guestMemory.DirectRegions(); @@ -2671,7 +2731,7 @@ ShaderResources::OwnRefreshFallback ShaderResources::refreshOwnObjects(std::span if (binding.kind == ShaderRecompiler::DescriptorKind::SampledImage) { for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * 8u, 8u); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue; + if (IsNullTextureResource(words)) continue; if (textureIndex >= textures.size()) return OwnRefreshFallback::Uncached; const auto index = textureIndex++; const auto overlap = listed(index, false); @@ -2681,7 +2741,7 @@ ShaderResources::OwnRefreshFallback ShaderResources::refreshOwnObjects(std::span } else if (binding.kind == ShaderRecompiler::DescriptorKind::StorageImage) { for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * 8u, 8u); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue; + if (IsNullTextureResource(words)) continue; if (storageIndex >= storageTextures.size()) return OwnRefreshFallback::Uncached; const auto index = storageIndex++; if (listed(index, true) == overlapping.end()) continue; @@ -2702,21 +2762,23 @@ ShaderResources::OwnRefreshFallback ShaderResources::refreshOwnObjects(std::span return OwnRefreshFallback::Count; } -void CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes) { - if (bytes == 0 || Recorder::InCompletion()) return; +bool CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes) { + if (bytes == 0 || Recorder::InCompletion()) return false; auto* recorder = Recorder::Active(); - if (recorder == nullptr) return; + if (recorder == nullptr) return false; const auto drawWriters = DrawCopiedWriters(); const bool labels = Recorder::PendingCompletionLabels() != 0; - if ((context.copiedWriters == nullptr || context.copiedWriters->empty()) && drawWriters->empty() && !labels) return; - if (!recorder->PendingWriteOverlaps(address, bytes)) return; + if ((context.copiedWriters == nullptr || context.copiedWriters->empty()) && drawWriters->empty() && !labels) return false; + if (!recorder->PendingWriteOverlaps(address, bytes)) return false; const auto overlaps = [&](const auto& writers) { return std::any_of(writers.begin(), writers.end(), [&](const auto& writer) { return writer->WritesOverlap(address, bytes); }); }; if ((context.copiedWriters != nullptr && overlaps(*context.copiedWriters)) || overlaps(*drawWriters) || (labels && recorder->PendingLabelIn(address, bytes))) { Recorder::CountSync(4); recorder->SyncThrough(address, bytes); + return true; } + return false; } const ShaderRecompiler::DescriptorBinding* ShaderResources::tableImages(std::span shaders, ShaderRecompiler::ShaderStage stage) { @@ -2769,7 +2831,7 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR const auto elementWords = binding.count != 0 ? binding.guestDescriptor.size() / binding.count : 0; for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue; + if (IsNullTextureResource(words)) continue; const auto resource = DecodeTextureResource(words); const VkComponentMapping components = ViewComponents(resource); if (textureIndex >= textures.size() || cachedTexture(context, words, resource, components, 0, !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element)) != textures[textureIndex]) return false; @@ -2778,7 +2840,7 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR } else if (binding.kind == ShaderRecompiler::DescriptorKind::StorageImage) { for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * 8u, 8u); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue; + if (IsNullTextureResource(words)) continue; if (storageIndex >= storageTextures.size()) return false; std::shared_ptr expected; if (SameAsPreviousStorageElement(binding, element) && StorageDedupeEnabled()) expected = storageTextures[storageIndex - 1]; @@ -3754,7 +3816,7 @@ bool ShaderResources::precollectImages() { } ImageRecord record; record.sampled = sampled; - if (words[0] != 0u || (words[1] & 0xffu) != 0u) { + if (!IsNullTextureResource(words)) { const auto* memo = words.size() == 8 ? &decodedImage(context.device, words) : nullptr; record.resource = memo != nullptr ? memo->resource : DecodeTextureResource(words); record.guestBytes = memo != nullptr ? memo->guestBytes : DescribeSurface(record.resource).guestBytes; @@ -3863,7 +3925,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); const auto* record = nextRecord(); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) { + if (IsNullTextureResource(words)) { item.imageAllocations.push_back(std::numeric_limits::max()); --plannedSampledImages; continue; @@ -3885,6 +3947,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind (texture != nullptr ? counters.fastHits : counters.fastMisses).fetch_add(1, std::memory_order_relaxed); } if (texture == nullptr) texture = cachedTexture(context, words, resource, components, guestBytes, depthCompare); + if (resource.dimension == TextureDimension::k2DMsaa) multisampleSources.push_back(resource.baseAddress); bool baseLevel = false; if (element < binding.imageUnnormalized.size() && binding.imageUnnormalized[element]) { const auto range = texture->SampledViewRange(firstLayer); @@ -3932,7 +3995,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind if (sameAsPrevious) ++mipOffset; else mipOffset = 0; const auto* record = nextRecord(); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) { + if (IsNullTextureResource(words)) { item.imageAllocations.push_back(std::numeric_limits::max()); --plannedStorageImages; continue; @@ -4267,7 +4330,7 @@ std::vector> ShaderResources::PresyncSur // Stage A has not run (an address-based build, or the pass is off): decoded from the shader. if (deferredCompute.program == nullptr) return surfaces; forEachImageElement(*deferredCompute.program, [&](const ShaderRecompiler::DescriptorBinding& binding, std::uint32_t, std::span words) { - if (words[0] == 0u && (words[1] & 0xffu) == 0u) return; + if (IsNullTextureResource(words)) return; const auto resource = DecodeTextureResource(words); consider(resource.format, resource.baseAddress, DescribeSurface(resource).guestBytes, binding.kind == ShaderRecompiler::DescriptorKind::SampledImage); }); @@ -4389,14 +4452,26 @@ std::shared_ptr ShaderResources::PrepareDrawBindi std::size_t totalSnapshotBytes = 0; auto result = std::make_shared(); std::vector selected; + std::array stamp{}; + bool stamped = false; for (std::size_t index = 0; index < allocations.size(); ++index) { const auto& item = allocations[index]; const auto override = std::find_if(moved.begin(), moved.end(), [&](const MovedBuffer& entry) { return entry.allocation == index; }); if (override != moved.end() && !override->words.empty()) { - auto buffer = std::make_shared(context, override->size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); - std::memcpy(buffer->Bytes().data(), override->words.data(), override->size); - for (const auto& patch : dataPatches) { - if (patch.allocation == index && patch.byte < override->size) buffer->Bytes()[patch.byte] = static_cast(patch.adjustment); + constexpr std::size_t MovedWordBufferSlots = 8; + const auto held = std::find_if(movedWordBuffers.begin(), movedWordBuffers.end(), [&](const MovedWordBuffer& entry) { return entry.allocation == index && entry.size == override->size && entry.words == override->words; }); + std::shared_ptr buffer; + if (held != movedWordBuffers.end()) { + buffer = held->buffer; + } else { + buffer = std::make_shared(context, override->size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::memcpy(buffer->Bytes().data(), override->words.data(), override->size); + for (const auto& patch : dataPatches) { + if (patch.allocation == index && patch.byte < override->size) buffer->Bytes()[patch.byte] = static_cast(patch.adjustment); + } + MovedWordBuffer entry{index, override->words, override->size, buffer}; + if (movedWordBuffers.size() < MovedWordBufferSlots) movedWordBuffers.push_back(std::move(entry)); + else movedWordBuffers[movedWordBufferNext++ % MovedWordBufferSlots] = std::move(entry); } selected.push_back(index); result->snapshots.push_back({0, std::move(buffer)}); @@ -4416,12 +4491,29 @@ std::shared_ptr ShaderResources::PrepareDrawBindi const auto bytes = static_cast(GuestBufferMemory::ViewBytes(size, item.adjustment)); if (override == moved.end() && (bytes > maxSnapshotBytes || totalSnapshotBytes + bytes > maxSnapshotBytes)) continue; const auto registryGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); - const auto generation = GuestMemory::CollectWrites(begin, bytes); - auto buffer = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, generation); + if (!stamped) { + stamp = {GuestMemory::TrackerGeneration(), GuestMemory::CollectEpoch(), registryGeneration}; + stamped = true; + } + auto buffer = recorder.UnwrittenDrawSnapshot(begin, bytes, stamp); + static const bool verifySnapshots = std::getenv("APS5_VERIFY_DRAW_SNAPSHOTS") != nullptr; + if (verifySnapshots && buffer != nullptr) { + static std::atomic checked{0}, differing{0}; + const auto slow = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, GuestMemory::CollectWrites(begin, bytes)); + const bool same = slow == buffer || (slow != nullptr && slow->Bytes().size() == buffer->Bytes().size() && std::memcmp(slow->Bytes().data(), buffer->Bytes().data(), buffer->Bytes().size()) == 0) || (slow == nullptr && std::memcmp(buffer->Bytes().data(), reinterpret_cast(begin), bytes) == 0); + if (!same) differing.fetch_add(1); + if (checked.fetch_add(1) % 200000 == 199999) std::fprintf(stderr, "[draw-snapshots] verify: %llu fast answers checked, %llu differing%c", static_cast(checked.load()), static_cast(differing.load()), 10); + } if (buffer == nullptr) { - buffer = std::make_shared(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); - std::memcpy(buffer->Bytes().data(), reinterpret_cast(begin), bytes); - recorder.KeepDrawSnapshot(begin, bytes, generation, registryGeneration, buffer); + const auto generation = GuestMemory::CollectWrites(begin, bytes); + buffer = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, generation); + if (buffer == nullptr) { + buffer = std::make_shared(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::memcpy(buffer->Bytes().data(), reinterpret_cast(begin), bytes); + recorder.KeepDrawSnapshot(begin, bytes, generation, registryGeneration, buffer); + } + if (GuestMemory::TrackerGeneration() == stamp[0]) recorder.NoteUnwrittenDrawSnapshot(begin, bytes, stamp, buffer); + else stamped = false; } selected.push_back(index); result->snapshots.push_back({begin, std::move(buffer)}); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 5e16fd73f0..31f279d3b7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -140,18 +140,18 @@ struct Module { } const auto signature = Signature(type); if (vertex && storage == spv::StorageClassInput) { - Require((value == spv::BuiltInVertexIndex || value == spv::BuiltInInstanceIndex) && signature == "i32", "unsupported vertex built-in input"); + Require((value == spv::BuiltInVertexIndex || value == spv::BuiltInInstanceIndex || value == spv::BuiltInBaseInstance) && signature == "i32", "unsupported vertex built-in input"); } else if (vertex && storage == spv::StorageClassOutput) { if (value == spv::BuiltInLayer && layerOutput && (signature == "u32" || signature == "i32")) return; 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::BuiltInFragStencilRefEXT && signature == "i32") || (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") || (value == spv::BuiltInSampleMask && signature == "i32[1]"))) || (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, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray) { +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, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray, bool demoteToHelperInvocation) { using Stage = ShaderRecompiler::ShaderStage; Require(compiled.program != nullptr, "missing compiled shader"); const auto& shader = *compiled.program; @@ -263,6 +263,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi capability == spv::CapabilityImageGatherExtended || capability == spv::CapabilityImageQuery || ((vertex || evaluation || mesh) && shaderClipDistance && capability == spv::CapabilityClipDistance) || + (vertex && capability == spv::CapabilityDrawParameters) || (fragment && geometryShader && capability == spv::CapabilityGeometry) || (fragment && sampleRateShading && capability == spv::CapabilitySampleRateShading) || capability == spv::CapabilityImageMSArray || @@ -279,7 +280,8 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi capability == spv::CapabilityStorageBuffer8BitAccess || capability == spv::CapabilityPhysicalStorageBufferAddresses || capability == spv::CapabilitySampledImageArrayDynamicIndexing || - capability == spv::CapabilityStorageImageArrayDynamicIndexing; + capability == spv::CapabilityStorageImageArrayDynamicIndexing || + (fragment && demoteToHelperInvocation && capability == spv::CapabilityDemoteToHelperInvocation); // Bindless image tables index their slots non-uniformly in graphics stages // (VK_EXT_descriptor_indexing, enabled by the device setup when available). @@ -329,7 +331,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(fragment && fragmentShaderBarycentric, "SPV_KHR_fragment_shader_barycentric requires enabled fragmentShaderBarycentric in a fragment shader"); break; } - Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (viewportIndexLayer && extension == "SPV_EXT_shader_viewport_index_layer") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || ((descriptorIndexing || runtimeDescriptorArray) && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension"); + Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (viewportIndexLayer && extension == "SPV_EXT_shader_viewport_index_layer") || (fragment && demoteToHelperInvocation && extension == "SPV_EXT_demote_to_helper_invocation") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || ((descriptorIndexing || runtimeDescriptorArray) && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension"); break; } case spv::OpDecorateId: @@ -446,7 +448,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi const bool quads = state.rectList || state.stages.tessellation->domain == 2u; const bool counterClockwise = !state.rectList && state.stages.tessellation->outputTopology == 3u; mode(quads ? spv::ExecutionModeQuads : spv::ExecutionModeTriangles, {}); - mode(state.rectList || state.stages.tessellation->partitioning == 0u ? spv::ExecutionModeSpacingEqual : spv::ExecutionModeSpacingFractionalOdd, {}); + mode(state.rectList || state.stages.tessellation->partitioning == 0u ? spv::ExecutionModeSpacingEqual : state.stages.tessellation->partitioning == 3u ? spv::ExecutionModeSpacingFractionalEven : spv::ExecutionModeSpacingFractionalOdd, {}); mode(counterClockwise ? spv::ExecutionModeVertexOrderCcw : spv::ExecutionModeVertexOrderCw, {}); Require(module.modes.size() == 3, "unsupported tessellation-evaluation execution mode"); } else if (geometry) { @@ -599,7 +601,21 @@ 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, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray) { +bool DeclaresEarlyFragmentTests(const ShaderRecompiler::RecompileResult& program) { + const auto& words = program.spirv.Words(); + Require(words.size() >= 5 && words[0] == spv::MagicNumber, "the fragment shader is not a SPIR-V module"); + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + Require(count != 0 && count <= words.size() - cursor, "truncated SPIR-V instruction"); + const auto op = static_cast(words[cursor] & 0xffffu); + if (op == spv::OpFunction) return false; + if (op == spv::OpExecutionMode && count >= 3 && words[cursor + 2] == spv::ExecutionModeEarlyFragmentTests) return true; + cursor += count; + } + return false; +} + +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, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray, bool demoteToHelperInvocation) { using Stage = ShaderRecompiler::ShaderStage; const bool tessellation = state.stages.path == ShaderPath::Tessellation; const bool mesh = state.stages.path == ShaderPath::Geometry; @@ -620,7 +636,7 @@ std::set ValidateShaders(std::span shaders, Require(shaders[i].program != nullptr, "missing compiled shader"); Require(shaders[i].stage == expected, "graphics stage order disagrees"); for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == 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, shaderCullDistance, viewportIndexLayer, runtimeDescriptorArray); + const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance, viewportIndexLayer, runtimeDescriptorArray, demoteToHelperInvocation); 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 973a5a6fe6..6ba435e2d8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1,5 +1,6 @@ #include #include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" @@ -127,7 +128,7 @@ constexpr std::uint32_t DepthControlMask = ~0x007007f0u; // EXEC_ON_HIER_FAIL / EXEC_ON_NOOP / EXEC_IF_OVERLAPPED (bits 9, 10, 17) only force the pixel shader // to run, which it always does here. constexpr std::uint32_t ShaderControlMask = ~(0x0000f870u | 0x00020600u | 0x00010000u); -constexpr std::uint32_t PixelStageRunsMask = 0x00020747u; +constexpr std::uint32_t PixelStageRunsMask = 0x000207c7u; constexpr std::uint32_t AlphaToCoverageMask = ~0x0001ff00u; constexpr std::uint32_t AlphaToCoverageUndithered = 0x0000aa01u; constexpr std::uint32_t AlphaToCoverageDithered = 0x00018701u; @@ -292,7 +293,7 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx depth.clearDepth = readFloat(cx, 0x00b); depth.clearStencil = static_cast(read(cx, 0x00a) & 0xffu); if (const auto htile = find(cx, 0x005); zFormat != 0 && htile != cx.end() && htile->second != 0 && (read(cx, 0x010) & (1u << 29u)) != 0) { - depth.htileAddress = base(0x005, 0x01e); + depth.htileAddress = base(0x005, 0x01e) + static_cast(view & 0x1fffu) * HtileMetadataBytes(depth.extent); depth.htileStencil = stencil && (read(cx, 0x011) & (1u << 29u)) == 0; } result.depth = depth; @@ -410,6 +411,40 @@ bool coveredCentroidSupported(const QueueState& queue) { return ena != queue.context.end() && addr != queue.context.end() && (ena->second & addr->second & CentroidInputs) == 0; } +std::array sampleMaskFields(std::uint32_t top, std::uint32_t bottom) { + return {top & 0xffffu, top >> 16u, bottom & 0xffffu, bottom >> 16u}; +} + +std::string sampleMaskRejection(const QueueState& queue, std::uint32_t config, std::uint32_t top, std::uint32_t bottom) { + const auto log2 = config & 7u; + if (log2 > 3u) return {}; + const auto full = (1u << (1u << log2)) - 1u; + auto masks = sampleMaskFields(top, bottom); + for (auto& mask : masks) mask &= full; + if (std::ranges::all_of(masks, [&](std::uint32_t mask) { return mask == full; })) return {}; + const bool uniform = std::ranges::all_of(masks, [&](std::uint32_t mask) { return mask == masks[0]; }); + const bool wholePixels = std::ranges::all_of(masks, [&](std::uint32_t mask) { return mask == 0u || mask == full; }); + char detail[96]; + std::snprintf(detail, sizeof(detail), " (PA_SC_AA_MASK_X0Y0_X1Y0=0x%08x, PA_SC_AA_MASK_X0Y1_X1Y1=0x%08x)", top, bottom); + const auto conservative = find(queue.context, 0x313); + if (conservative != queue.context.end() && (conservative->second & ~0x001f6000u) != 0) return std::string("AGC graphics: sample masks with conservative rasterization are unsupported") + detail; + if (!wholePixels && uniform && !PixelProgramSkipped(queue)) { + const auto ena = find(queue.context, 0x1b3); + const auto addr = find(queue.context, 0x1b4); + if (ena != queue.context.end() && addr != queue.context.end() && (ena->second & addr->second & CentroidInputs) != 0) return std::string("AGC graphics: sample masks that remove some samples of a pixel with centroid interpolation are unsupported") + detail; + } + return {}; +} + +std::array decodeQuadSampleMask(const Registers& cx, std::uint32_t samples) { + const bool singleSample = (read(cx, 0x2f8) & 7u) == 0u; + const auto full = (1u << samples) - 1u; + const auto fields = sampleMaskFields(read(cx, 0x30e), read(cx, 0x30f)); + std::array masks{}; + for (std::size_t pixel = 0; pixel < masks.size(); ++pixel) masks[pixel] = static_cast(singleSample ? ((fields[pixel] & 1u) != 0u ? full : 0u) : fields[pixel] & full); + return masks; +} + std::uint32_t decodeSamples(const Registers& cx) { const auto config = read(cx, 0x2f8); zero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u | CoveredCentroidIsCenter), "coverage conversion or nonstandard multisampling"); @@ -666,7 +701,7 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { APS5_LOG_OUT_DEBUG("DecodeShaderStages value=0x%x primitive=%u path=%u vertexWave=%u", value, primitive, static_cast(result.path), result.vertexWaveSize); if (path == ShaderPath::Vertex) { validate((value & 0x2000u) != 0, "legacy vertex routing without PRIMGEN_EN is unsupported"); - validate((value & ~0x0247a010u) == 0, "unsupported vertex routing, scheduling or wave-ID state"); + validate((value & ~0x0247a010u) == 0 || value == 0x02002008u, "unsupported vertex routing, scheduling or wave-ID state"); } else if (path == ShaderPath::Tessellation) { validate((value & 0x00600020u) == 0, "wave32 tessellation or geometry amplification is unsupported"); validate((value & ~0x0207ed0du) == 0 && (value & 0x02002000u) == 0x02002000u && (value & 3u) == 1u && ((value >> 3u) & 3u) == 1u, "unsupported tessellation routing"); @@ -677,7 +712,7 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { validate(tess.inputControlPoints != 0 && tess.inputControlPoints <= 32 && tess.outputControlPoints != 0 && tess.outputControlPoints <= 32, "invalid tessellation control-point counts"); validate(tess.patchesPerGroup != 0, "VGT_LS_HS_CONFIG.NUM_PATCHES is zero"); if ((parameters & ~0x000600ffu) != 0) Require(false, zeroMessage(0x2db, parameters, "VGT_TF_PARAM donut, factor detection, memory type or reserved state")); - validate((tess.domain == 1 || tess.domain == 2) && (tess.partitioning == 0 || tess.partitioning == 2) && (tess.outputTopology == 2 || tess.outputTopology == 3), "only triangle and quad domains with integer or fractional-odd partitioning and triangle output are supported by the reference path"); + validate((tess.domain == 1 || tess.domain == 2) && (tess.partitioning == 0 || tess.partitioning == 2 || tess.partitioning == 3) && (tess.outputTopology == 2 || tess.outputTopology == 3), "only triangle and quad domains with integer, fractional-even or fractional-odd partitioning and triangle output are supported by the reference path"); result.minTessellationLevel = readFloat(queue.context, 0x287); result.maxTessellationLevel = readFloat(queue.context, 0x286); Require(result.minTessellationLevel >= 0.0f && result.minTessellationLevel <= 63.0f && result.minTessellationLevel <= result.maxTessellationLevel && result.maxTessellationLevel >= 1.0f, "tessellation level clamps need 0 <= VGT_HOS_MIN_TESS_LEVEL <= 63, VGT_HOS_MAX_TESS_LEVEL >= 1 and the minimum at most the maximum"); @@ -816,7 +851,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { Require(read(cx, 0x83) == 0xffffu, "clip rectangles are unsupported"); Require((read(cx, 0x8c) & 0xfu) == 0xau, "nonstandard triangle edge rules are unsupported"); Require(read(cx, 0x2f9) == 0x2du, "nonstandard pixel center or vertex quantization is unsupported"); - Require(read(cx, 0x30e) == 0xffffffffu && read(cx, 0x30f) == 0xffffffffu, "sample masks are unsupported"); + if (auto reason = sampleMaskRejection(queue, read(cx, 0x2f8), read(cx, 0x30e), read(cx, 0x30f)); !reason.empty()) throw std::runtime_error(reason); const auto viewportControl = read(cx, 0x206); if (viewportControl != 0x43fu) throw std::runtime_error(vteMessage(viewportControl)); zero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags"); @@ -903,6 +938,8 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { APS5_LOG_OUT_DEBUG("Render extent from screen=%ux%u", result.renderExtent.width, result.renderExtent.height); Require(result.renderExtent.width != 0 && result.renderExtent.height != 0, "empty framebuffer extent for a draw without color writes"); } + result.quadSampleMask = decodeQuadSampleMask(cx, result.samples); + result.pixelEarlyZ = !PixelProgramSkipped(queue) && PixelEarlyZ(read(cx, 0x203)); if ((read(cx, 0x207) & LayerExports & ~result.layerExports & ~(1u << 21u)) != 0) { static bool reported = false; if (!reported) { @@ -1037,6 +1074,43 @@ std::array ExportPackings(const State& return packings; } +bool PartialSampleMask(const State& state) { + const auto full = (1u << state.samples) - 1u; + return std::ranges::any_of(state.quadSampleMask, [&](std::uint16_t mask) { return (mask & full) != full; }); +} + +bool UniformSampleMask(const State& state) { + const auto full = (1u << state.samples) - 1u; + return std::ranges::all_of(state.quadSampleMask, [&](std::uint16_t mask) { return (mask & full) == (state.quadSampleMask[0] & full); }); +} + +VkSampleMask HostSampleMask(const State& state) { + const auto full = (1u << state.samples) - 1u; + return UniformSampleMask(state) ? state.quadSampleMask[0] & full : full; +} + +std::uint8_t QuadPixelMask(const State& state) { + if (!PartialSampleMask(state) || UniformSampleMask(state)) return 0xfu; + const auto full = (1u << state.samples) - 1u; + std::uint8_t pixels = 0; + for (std::uint32_t pixel = 0; pixel < state.quadSampleMask.size(); ++pixel) { + if ((state.quadSampleMask[pixel] & full) != 0u) pixels = static_cast(pixels | (1u << pixel)); + } + return pixels; +} + +std::array QuadSampleMasks(const State& state) { + std::array masks{0xffffu, 0xffffu, 0xffffu, 0xffffu}; + if (!PartialSampleMask(state) || UniformSampleMask(state)) return masks; + const auto full = (1u << state.samples) - 1u; + if (std::ranges::all_of(state.quadSampleMask, [&](std::uint16_t mask) { return (mask & full) == 0u || (mask & full) == full; })) return masks; + for (std::size_t pixel = 0; pixel < masks.size(); ++pixel) { + const auto mask = state.quadSampleMask[pixel] & full; + masks[pixel] = static_cast(mask == full ? 0xffffu : mask); + } + return masks; +} + ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state) { if (state.rectList) return ShaderRecompiler::RasterPrimitive::Rectangle; if (state.stages.mesh.has_value() || state.stages.tessellation.has_value()) return ShaderRecompiler::RasterPrimitive::Triangle; @@ -1107,7 +1181,7 @@ GuestTextureResource SurfaceForTarget(const ColorTarget& color) { return surface; } -ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { +ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allowMultisampleDcc) { const auto stride = slot * 0xfu; ColorTarget color{}; color.slot = slot; @@ -1123,7 +1197,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { Require(color.samples <= 8u, "color targets with more than eight samples are unsupported"); Require((1u << ((attrib >> 15u) & 3u)) == color.samples, "color targets with fewer fragments than samples (EQAA) are unsupported"); const auto multisampleInfo = color.samples > 1 ? 0x0c006000u : 0u; - Require(color.samples == 1 || (info & 0x10000000u) == 0, "DCC over a multisampled color target is unsupported"); + Require(allowMultisampleDcc || color.samples == 1 || (info & 0x10000000u) == 0, "DCC over a multisampled color target is unsupported"); // ROUND_MODE (bit 18) only affects unorm rounding. With DCC_ENABLE (bit 28) the target is written if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u | 0x00002000u | multisampleInfo)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")"); Require((info & 0x8000u) != 0 || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported"); @@ -1248,8 +1322,9 @@ std::optional DecodeColorMetadataPass(const QueueState& queue const auto sourceAttrib = find(cx, 0x31d); const bool sourceSamples = sourceAttrib != cx.end() && ((sourceAttrib->second >> 12u) & 7u) != 0; const bool resolve = mode == 3u && (rasterizerSamples || sourceSamples); - 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}; + const bool fmaskDecompress = mode == 5u; + if (mode != 2u && mode != 6u && !resolve && !fmaskDecompress) return std::nullopt; + ColorMetadataPass pass{resolve ? ColorMetadataPass::Mode::Resolve : fmaskDecompress ? ColorMetadataPass::Mode::FmaskDecompress : 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"); const auto value = [&](std::uint32_t offset) { const auto found = find(cx, offset); @@ -1299,8 +1374,13 @@ std::optional DecodeColorMetadataPass(const QueueState& queue const auto targetMask = read(cx, 0x8e); for (std::uint32_t slot = 0; slot < 8; ++slot) { if (((targetMask >> (4u * slot)) & 0xfu) == 0 || ((read(cx, 0x31c + slot * 0xfu) >> 2u) & 0x1fu) == 0) continue; - const auto target = DecodeColorBuffer(cx, slot); - Require(target.samples == 1, "CB metadata pass over a multisampled color target, whose texels are on the host, is unsupported (only a CB resolve is modeled)"); + const auto target = DecodeColorBuffer(cx, slot, fmaskDecompress); + if (fmaskDecompress) { + Require(target.samples > 1, "CB metadata pass over a single-sample target"); + Require(target.cmaskAddress != 0, "FMASK decompress of a target without a CMASK address"); + } else { + Require(target.samples == 1, "CB metadata pass over a multisampled color target, whose texels are on the host, is unsupported (only a CB resolve is modeled)"); + } Require((read(cx, 0x31c + slot * 0xfu) & 0x10000000u) == 0 || target.dccAddress != 0, "CB metadata pass over a mipmapped DCC color target, whose keys are not modeled"); requireCovered(target); pass.targets.push_back(target); @@ -1384,8 +1464,10 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (value(cx, 0x8c, word) && (word & 0xfu) != 0xau) return require(false, "nonstandard triangle edge rules are unsupported"); if (value(cx, 0x2f9, word) && word != 0x2du) return require(false, "nonstandard pixel center or vertex quantization is unsupported"); if (value(cx, 0x313, word) && (word & ~0x001f6000u) != 0 && word != 0x6001u) return conservativeMessage(word); - std::uint32_t other = 0; - if (value(cx, 0x30e, word) && value(cx, 0x30f, other) && (word != 0xffffffffu || other != 0xffffffffu)) return require(false, "sample masks are unsupported"); + std::uint32_t config = 0, other = 0; + if (value(cx, 0x2f8, config) && value(cx, 0x30e, word) && value(cx, 0x30f, other)) { + if (auto reason = sampleMaskRejection(queue, config, word, other); !reason.empty()) return reason; + } if (value(cx, 0x206, word) && word != 0x43fu) return vteMessage(word); if (auto reason = nonzero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags"); !reason.empty()) return reason; std::uint32_t targetMask = 0, shaderMask = 0; @@ -1408,10 +1490,11 @@ bool PixelProgramSkipped(const QueueState& queue) { const auto& cx = queue.context; const auto targetMask = find(cx, 0x8e); const auto shaderMask = find(cx, 0x8f); - const auto zFormat = find(cx, 0x1c4); const auto shaderControl = find(cx, 0x203); - if (targetMask == cx.end() || shaderMask == cx.end() || zFormat == cx.end() || shaderControl == cx.end()) return false; - return (targetMask->second & shaderMask->second) == 0 && zFormat->second == 0 && (shaderControl->second & PixelStageRunsMask) == 0; + if (targetMask == cx.end() || shaderMask == cx.end() || shaderControl == cx.end()) return false; + const auto alphaToMask = find(cx, 0x2dc); + if (alphaToMask != cx.end() && (alphaToMask->second & 1u) != 0) return false; + return (targetMask->second & shaderMask->second) == 0 && (shaderControl->second & PixelStageRunsMask) == 0; } std::string NullPixelProgramRejection(const QueueState& queue) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index a57623bdb1..ebd2feb5ad 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -211,6 +211,7 @@ VkImageViewType ViewTypeFor(TextureDimension dimension, [[maybe_unused]] std::ui case TextureDimension::kCube: return VK_IMAGE_VIEW_TYPE_2D_ARRAY; case TextureDimension::k3D: return VK_IMAGE_VIEW_TYPE_3D; case TextureDimension::k1DArray: return VK_IMAGE_VIEW_TYPE_1D_ARRAY; + case TextureDimension::k2DMsaa: break; } throw std::runtime_error("AGC graphics: Texture encountered an unknown guest texture dimension"); } @@ -779,7 +780,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& } } -Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType) : context(context) { +Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType, std::uint32_t baseLayer) : context(context) { layout = VK_IMAGE_LAYOUT_GENERAL; VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = depthImage; @@ -787,7 +788,7 @@ Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthForma viewInfo.format = depthFormat; viewFormat = depthFormat; viewInfo.components = components; - viewInfo.subresourceRange = {aspect, 0, 1, 0, viewType == VK_IMAGE_VIEW_TYPE_2D ? 1u : VK_REMAINING_ARRAY_LAYERS}; + viewInfo.subresourceRange = {aspect, 0, 1, baseLayer, viewType == VK_IMAGE_VIEW_TYPE_2D ? 1u : VK_REMAINING_ARRAY_LAYERS}; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth plane"); viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; if (viewType != VK_IMAGE_VIEW_TYPE_2D_ARRAY) return; @@ -1103,6 +1104,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, layerGeneration.assign(trackedLayers, 0); layerPending.assign(trackedLayers, false); layerOriginal.assign(trackedLayers, true); + savedTicket.assign(trackedLayers, 0); sliceLinearBytes = geometry.sliceLinearBytes; const auto linearBytes = sliceLinearBytes * arrayLayers; @@ -1786,6 +1788,7 @@ bool StorageTexture::Refresh() { stampedBlocks.assign(generations.size(), 0); cpuBlocks.assign(generations.size(), 0); tracked = GuestMemory::ChangedBlocks(descriptor.baseAddress, static_cast(guestBytes), generations, stampedBlocks, cpuBlocks); + if (!tracked) awaitStoredSave(nullptr); compared = !tracked && compareUntracked(descriptor.baseAddress, static_cast(guestBytes), stampedBlocks, true); if (compared) { for (const auto& [from, unitGeneration] : keptAtUnwatch) { @@ -2089,6 +2092,7 @@ void StorageTexture::captureGuestBytes(const std::vector* layers) { const auto offset = static_cast(layerBegin(layer) - descriptor.baseAddress); GuestMemory::ReadCommittedUnsynced(layerBegin(layer), std::span(original).subspan(offset, static_cast(layerBytes(layer)))); layerOriginal[layer] = true; + cancelStoredSave(layer); } } @@ -2180,8 +2184,12 @@ void StorageTexture::upload(const std::vector* layers) { // batch of its own submitted the recorder's work first and waited for all of it, 25-40 ms // under the GPU mutex at the movie stage. APS5_NO_RECORDED_CLEAR=1 waits as before. static_cast(HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes))); - if (GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) layerOriginal.assign(trackedLayers, false); - else captureGuestBytes(nullptr); + if (GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) { + layerOriginal.assign(trackedLayers, false); + cancelStoredSaves(); + } else { + captureGuestBytes(nullptr); + } forgetBorrowed(0, trackedLayers); stampLayers(false); static const bool recordClear = std::getenv("APS5_NO_RECORDED_CLEAR") == nullptr; @@ -2229,8 +2237,12 @@ void StorageTexture::upload(const std::vector* layers) { return; } if (const auto* import = uploadedKeys == DccKeys::Uncompressed ? HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes)) : nullptr) { - if (GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) layerOriginal.assign(trackedLayers, false); - else captureGuestBytes(layers); + if (GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) { + layerOriginal.assign(trackedLayers, false); + cancelStoredSaves(); + } else { + captureGuestBytes(layers); + } stampLayers(true); // A whole-surface upload of a block-unit image goes through the windows too when a unit // shadow holds part of the surface (the detile then reads the slabs; a new image has no @@ -2352,6 +2364,7 @@ void StorageTexture::upload(const std::vector* layers) { return; } layerOriginal.assign(trackedLayers, true); + cancelStoredSaves(); forgetBorrowed(0, trackedLayers); stampLayers(false); original.resize(static_cast(guestBytes)); @@ -2572,6 +2585,53 @@ StorageTexture::PaddingSeeds StorageTexture::paddingSeeds(const HostImport& impo return seeds; } +StorageTexture::PaddingSeeds StorageTexture::paddingSeeds(std::span segments, const std::vector& copied) const { + if (segments.size() == 1) return paddingSeeds(*segments.front().import, copied); + PaddingSeeds merged; + for (const auto& segment : segments) { + const auto segmentBegin = segment.begin - descriptor.baseAddress; + const auto segmentEnd = segment.end - descriptor.baseAddress; + std::vector inside; + for (const auto& range : copied) { + const auto begin = std::max(range.begin, segmentBegin); + const auto end = std::min(range.end, segmentEnd); + if (begin < end) inside.push_back({begin, end, range.scratch + (begin - range.begin)}); + } + if (inside.empty()) continue; + auto seeds = paddingSeeds(*segment.import, std::move(inside)); + for (auto& entry : seeds.copies) merged.copies.push_back(std::move(entry)); + for (auto& slab : seeds.slabs) merged.slabs.push_back(std::move(slab)); + merged.importReads.insert(merged.importReads.end(), seeds.importReads.begin(), seeds.importReads.end()); + } + return merged; +} + +std::vector StorageTexture::importSegments() const { + const auto begin = descriptor.baseAddress; + const auto end = begin + guestBytes; + const bool unitAligned = blockUnits && UnitShadowEnabled(); + const auto samePiece = [](const RegisteredPiece& a, const RegisteredPiece& b) { return a.address == b.address && a.bytes == b.bytes && a.writable == b.writable && a.gpu == b.gpu; }; + for (std::uint32_t attempt = 0; attempt < 2; ++attempt) { + std::vector pieces; + if (!RegisteredReadableCovers(begin, static_cast(guestBytes), &pieces) || pieces.size() < 2) return {}; + if (std::any_of(pieces.begin(), pieces.end(), [&](const RegisteredPiece& piece) { return piece.writable != pieces.front().writable || piece.gpu != pieces.front().gpu; })) return {}; + std::vector segments; + const auto proof = HostImportsIdentity(context); + for (const auto& piece : pieces) { + const auto from = std::max(piece.address, begin); + const auto to = std::min(piece.address + piece.bytes, end); + const auto* import = HostImportFor(context, from, static_cast(to - from)); + if (import == nullptr) return {}; + if (!segments.empty() && segments.back().import == import) segments.back().end = to; + else if (!segments.empty() && unitAligned && from % 65536 != 0) return {}; + else segments.push_back({import, from, to}); + } + std::vector still; + if (HostImportsUnchanged(context, proof) && RegisteredReadableCovers(begin, static_cast(guestBytes), &still) && std::equal(pieces.begin(), pieces.end(), still.begin(), still.end(), samePiece)) return segments; + } + return {}; +} + void StorageTexture::recordDirectUploadBarrier(VkCommandBuffer commands, bool discard) { const VkMemoryBarrier sourceReady{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT}; VkImageMemoryBarrier toWrite{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; @@ -2739,7 +2799,7 @@ std::uint64_t StorageTexture::uploadWindows(const HostImport& import, std::span< return uploadedBytes; } -std::uint64_t StorageTexture::writeBackWindows(const HostImport& import, std::span> keep, std::uint64_t firstStored, std::uint64_t lastStored, std::vector& shadowed, std::vector>& imported) { +std::uint64_t StorageTexture::writeBackWindows(std::span segments, std::span> keep, std::uint64_t firstStored, std::uint64_t lastStored, std::vector& shadowed, std::vector>& imported, Buffer* save) { const auto elementBytes = BytesPerElement(descriptor.format); std::vector> runs; std::uint64_t storedBytes = 0; @@ -2777,23 +2837,34 @@ std::uint64_t StorageTexture::writeBackWindows(const HostImport& import, std::sp std::shared_ptr pin; std::vector copies; }; + struct SeedUnit { + std::uint64_t begin; + std::uint64_t end; + std::shared_ptr slab; + const HostImport* import; + }; std::vector slabPieces; - std::vector importCopies; - std::vector seeds; + std::vector> importCopies(segments.size()); + std::vector seeds; const auto addPiece = [&](std::uint64_t scratchOffset, std::uint64_t guestBegin, std::uint64_t guestEnd) { while (guestBegin < guestEnd) { - const auto pieceEnd = UnitShadowEnabled() ? std::min(guestEnd, SlabBoundary(import, guestBegin)) : guestEnd; + std::size_t segment = 0; + while (segment + 1 < segments.size() && segments[segment + 1].begin <= guestBegin) ++segment; + Require(segments[segment].begin <= guestBegin && guestBegin < segments[segment].end, "storage image write-back piece lies outside its import segments"); + const auto& import = *segments[segment].import; + auto pieceEnd = std::min(guestEnd, segments[segment].end); + if (UnitShadowEnabled()) pieceEnd = std::min(pieceEnd, SlabBoundary(import, guestBegin)); const auto destination = ShadowDestinationFor(context, import, guestBegin, pieceEnd); if (destination.has_value()) { if (slabPieces.empty() || slabPieces.back().slab != destination->slab) slabPieces.push_back({destination->slab, destination->pin, {}}); slabPieces.back().copies.push_back({scratchOffset, destination->offset, pieceEnd - guestBegin}); for (const auto& [seedBegin, seedEnd] : destination->seedUnits) { - if (std::none_of(seeds.begin(), seeds.end(), [&](const ShadowedRange& seed) { return seed.begin == seedBegin; })) seeds.push_back({seedBegin, seedEnd, destination->slab}); + if (std::none_of(seeds.begin(), seeds.end(), [&](const SeedUnit& seed) { return seed.begin == seedBegin; })) seeds.push_back({seedBegin, seedEnd, destination->slab, &import}); } if (!shadowed.empty() && shadowed.back().end == guestBegin && shadowed.back().slab == destination->slab) shadowed.back().end = pieceEnd; else shadowed.push_back({guestBegin, pieceEnd, destination->slab}); } else { - importCopies.push_back({scratchOffset, guestBegin - import.base, pieceEnd - guestBegin}); + importCopies[segment].push_back({scratchOffset, guestBegin - import.base, pieceEnd - guestBegin}); if (!imported.empty() && imported.back().second == guestBegin) imported.back().second = pieceEnd; else imported.emplace_back(guestBegin, pieceEnd); } @@ -2812,7 +2883,7 @@ std::uint64_t StorageTexture::writeBackWindows(const HostImport& import, std::sp addPiece(scratchPositions[i] + (begin - window.tiledBegin), descriptor.baseAddress + begin, descriptor.baseAddress + end); } } - std::sort(seeds.begin(), seeds.end(), [](const ShadowedRange& a, const ShadowedRange& b) { return a.begin < b.begin; }); + std::sort(seeds.begin(), seeds.end(), [](const SeedUnit& a, const SeedUnit& b) { return a.begin < b.begin; }); std::vector copied; for (std::size_t i = 0; i < windows.size(); ++i) { const auto& window = windows[i]; @@ -2823,7 +2894,11 @@ std::uint64_t StorageTexture::writeBackWindows(const HostImport& import, std::sp if (begin < end) copied.push_back({begin, end, scratchPositions[i] + (begin - window.tiledBegin)}); } } - const auto padding = paddingSeeds(import, std::move(copied)); + std::vector saves; + if (save != nullptr) { + for (const auto& piece : copied) saves.push_back({piece.scratch, piece.begin, piece.end - piece.begin}); + } + const auto padding = paddingSeeds(segments, copied); // A queued label or key store inside a seeded unit lands before the seed copies the import. auto flushBegin = firstStored; auto flushEnd = lastStored; @@ -2882,10 +2957,10 @@ std::uint64_t StorageTexture::writeBackWindows(const HostImport& import, std::sp RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, transferAccess); Recorder::CountBarriers(Recorder::CommandClass::StorageWriteBack); } - for (const auto& [begin, end, slab] : seeds) { + for (const auto& [begin, end, slab, import] : seeds) { // The piece's own slab (a seeded unit is one the piece covers partly). - const VkBufferCopy seed{begin - import.base, SlabOffset(import, *slab, begin), end - begin}; - context.Resolved(&DeviceFunctions::cmdCopyBuffer, "vkCmdCopyBuffer")(commands, import.buffer, slab->buffer, 1, &seed); + const VkBufferCopy seed{begin - import->base, SlabOffset(*import, *slab, begin), end - begin}; + context.Resolved(&DeviceFunctions::cmdCopyBuffer, "vkCmdCopyBuffer")(commands, import->buffer, slab->buffer, 1, &seed); NoteShadowSeed(begin, end); } // The padding's current bytes, under the retile that follows (the import-ready barrier @@ -2931,7 +3006,10 @@ std::uint64_t StorageTexture::writeBackWindows(const HostImport& import, std::sp context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &scratchDone, 0, nullptr, 0, nullptr); const auto copyBuffer = context.Resolved(&DeviceFunctions::cmdCopyBuffer, "vkCmdCopyBuffer"); for (const auto& pieces : slabPieces) copyBuffer(commands, tiledScratch->Handle(), pieces.slab->buffer, static_cast(pieces.copies.size()), pieces.copies.data()); - if (!importCopies.empty()) copyBuffer(commands, tiledScratch->Handle(), import.buffer, static_cast(importCopies.size()), importCopies.data()); + for (std::size_t segment = 0; segment < segments.size(); ++segment) { + if (!importCopies[segment].empty()) copyBuffer(commands, tiledScratch->Handle(), segments[segment].import->buffer, static_cast(importCopies[segment].size()), importCopies[segment].data()); + } + if (!saves.empty()) copyBuffer(commands, tiledScratch->Handle(), save->Handle(), static_cast(saves.size()), saves.data()); } VkImageMemoryBarrier backToGeneral = toSource; backToGeneral.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; @@ -3234,13 +3312,16 @@ void StorageTexture::forgetSavedOver(std::uint64_t address, std::size_t bytes) { // pending there (the flush before the fill or copy stored only the units it overlaps) are // compared with it at their store when the memory is outside the write watch. for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { - if (layerBegin(unit) < address + bytes && address < layerBegin(unit) + layerBytes(unit)) layerOriginal[unit] = false; + if (layerBegin(unit) < address + bytes && address < layerBegin(unit) + layerBytes(unit)) { + layerOriginal[unit] = false; + cancelStoredSave(unit); + } } } void StorageTexture::saveBeforeUnwatch() { std::vector units(trackedLayers, false); - for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) units[unit] = layerPending[unit] && !layerOriginal[unit] && layerGeneration[unit] != 0; + for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) units[unit] = layerPending[unit] && !layerOriginal[unit]; const auto runs = unitRuns(units); if (runs.empty()) return; original.resize(static_cast(guestBytes)); @@ -3250,6 +3331,13 @@ void StorageTexture::saveBeforeUnwatch() { constexpr std::uint64_t block = 65536; for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { if (!units[unit]) continue; + if (layerGeneration[unit] == 0) { + // Without a write generation the watch stores the unit whole: the bytes before this + // moment are the ones its results replace. + layerOriginal[unit] = true; + cancelStoredSave(unit); + continue; + } const auto begin = layerBegin(unit); const auto end = begin + layerBytes(unit); const auto spanBegin = begin & ~(block - 1); @@ -3265,9 +3353,76 @@ void StorageTexture::saveBeforeUnwatch() { if (changedBlocks[static_cast((at - spanBegin) / block)] == GuestMemory::BlockWritten && (!edge || GuestMemory::StoredOver(from, static_cast(to - from), layerGeneration[unit])) && !unchangedSinceBaseline(from, to)) keptAtUnwatch.emplace(from, layerGeneration[unit]); } layerOriginal[unit] = true; + cancelStoredSave(unit); } } +std::vector StorageTexture::prepareStoredSave(const std::vector& layers, const std::vector& skippedLayer) { + std::vector units; + for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { + if (layers[unit] && !skippedLayer[unit]) units.push_back(unit); + } + if (!units.empty() && !savedBytes) savedBytes = std::make_shared(context, static_cast(guestBytes), VK_BUFFER_USAGE_TRANSFER_DST_BIT); + return units; +} + +void StorageTexture::commitStoredSave(const std::vector& units) { + if (units.empty()) return; + const auto ticket = ++lastSaveTicket; + for (const auto unit : units) savedTicket[unit] = ticket; + auto* recorder = Recorder::Active(); + if (recorder == nullptr) { + applyStoredSave(ticket, units); + return; + } + recorder->OnComplete([texture = weak_from_this(), bytes = savedBytes, ticket, units] { + if (auto alive = texture.lock()) alive->applyStoredSave(ticket, units); + }); +} + +void StorageTexture::applyStoredSave(std::uint64_t ticket, const std::vector& units) { + if (!savedBytes) return; + const auto stored = savedBytes->Bytes(); + for (const auto unit : units) { + if (savedTicket[unit] != ticket) continue; + if (original.size() != guestBytes) original.resize(static_cast(guestBytes)); + comparedGuestBytes = {}; + const auto offset = static_cast(layerBegin(unit) - descriptor.baseAddress); + std::memcpy(original.data() + offset, stored.data() + offset, static_cast(layerBytes(unit))); + layerOriginal[unit] = true; + savedTicket[unit] = 0; + } +} + +void StorageTexture::cancelStoredSave(std::uint32_t unit) { + if (unit < savedTicket.size()) savedTicket[unit] = 0; +} + +void StorageTexture::cancelStoredSaves() { + savedTicket.assign(trackedLayers, 0); +} + +void StorageTexture::awaitStoredSave(const std::vector* layers) { + std::uint64_t first = descriptor.baseAddress + guestBytes; + std::uint64_t last = descriptor.baseAddress; + for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { + if (savedTicket[unit] == 0 || (layers != nullptr && !(*layers)[unit])) continue; + first = std::min(first, layerBegin(unit)); + last = std::max(last, layerBegin(unit) + layerBytes(unit)); + } + if (last <= first) return; + auto* recorder = Recorder::Active(); + Require(recorder != nullptr, "a write-back's saved guest bytes are pending without a recorder"); + recorder->SyncThrough(first, static_cast(last - first)); + const auto outstanding = [&] { + for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { + if (savedTicket[unit] != 0 && (layers == nullptr || (*layers)[unit])) return true; + } + return false; + }; + if (outstanding()) recorder->Sync(); +} + bool StorageTexture::PublishShadowsOnly(std::uint64_t address, std::size_t bytes, PublishScope scope, const char* reason) { if (scope == PublishScope::None || !AnyShadowedOverlaps(address, bytes)) return false; std::lock_guard gpu(GuestMemory::GpuMutex()); @@ -3323,6 +3478,26 @@ void StorageTexture::FlushAllPending(const char* reason) { static_cast(FlushPending(0, std::numeric_limits::max(), nullptr, reason)); } +std::size_t StorageTexture::StoreSmallPending(const char* reason) { + std::vector> small; + { + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + for (auto* texture : pending.textures) { + if (texture->guestBytes > SmallStoreBytes) continue; + if (auto alive = texture->weak_from_this().lock()) small.push_back(std::move(alive)); + } + } + std::size_t stored = 0; + for (const auto& texture : small) { + const auto address = texture->descriptor.baseAddress; + const auto bytes = static_cast(texture->guestBytes); + if (!HostImportCovers(texture->context, address, bytes)) continue; + if (FlushPending(address, bytes, nullptr, reason)) ++stored; + } + return stored; +} + bool StorageTexture::AnyPending() { auto& pending = Pending(); std::lock_guard lock(pending.mutex); @@ -3549,6 +3724,7 @@ bool StorageTexture::evictStale(std::uint64_t address, std::size_t bytes, std::s if (!selected(layer)) continue; layerPending[layer] = false; layerOriginal[layer] = false; + cancelStoredSave(layer); } if (!anyLayerPending()) { ++dropped; @@ -3610,6 +3786,7 @@ std::size_t StorageTexture::DiscardPendingInside(std::uint64_t address, std::siz texture->dirty = false; texture->layerPending.assign(texture->trackedLayers, false); texture->layerOriginal.assign(texture->trackedLayers, false); + texture->cancelStoredSaves(); it = pending.textures.erase(it); ++discarded; } else if (texture != exempt && texture->blockUnits && texture->overlaps(address, bytes)) { @@ -3622,6 +3799,7 @@ std::size_t StorageTexture::DiscardPendingInside(std::uint64_t address, std::siz if (!texture->layerPending[unit] || unitBegin < address || unitBegin + texture->layerBytes(unit) > end) continue; texture->layerPending[unit] = false; texture->layerOriginal[unit] = false; + texture->cancelStoredSave(unit); unitsDropped.fetch_add(1, std::memory_order_relaxed); dropped = true; } @@ -3824,6 +4002,7 @@ bool StorageTexture::clearByKeysFill(DccKeys keys, std::uint8_t key) { context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); recorder->EndGpuTiming(timing, guestBytes); layerOriginal.assign(trackedLayers, false); + cancelStoredSaves(); uploadedKeys = keys; filledKeys = DccKeys::Uncompressed; keyProof = {}; @@ -3966,7 +4145,10 @@ bool StorageTexture::FillClear(std::span pattern, std::u // walk, the write-back's own walk would stamp them above this generation and keep their // blocks, and the fill's pattern would be lost in them. const bool watched = GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes)); - if (watched) layerOriginal.assign(trackedLayers, false); + if (watched) { + layerOriginal.assign(trackedLayers, false); + cancelStoredSaves(); + } uploadedKeys = DccKeys::Uncompressed; if (layer != WholeImage && trackedLayers > 1) { // The cleared layer's units alone: their stamps above lie below this generation, the other @@ -4101,8 +4283,12 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) { // As after a clear: the image holds the surface, guest memory follows at the write-back (which // keeps the 64 KiB blocks the CPU writes from now on; the collect is uncached for the reason // given in FillClear). - if (GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) layerOriginal.assign(trackedLayers, false); - else captureGuestBytes(nullptr); + if (GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) { + layerOriginal.assign(trackedLayers, false); + cancelStoredSaves(); + } else { + captureGuestBytes(nullptr); + } uploadedKeys = DccKeys::Uncompressed; layerGeneration.assign(trackedLayers, GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast(guestBytes))); refreshGeneration(); @@ -4384,6 +4570,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { const bool tracked = GuestMemory::ChangedBlocks(firstStored, static_cast(lastStored - firstStored), generations, changedBlocks); std::vector compared(trackedLayers, false); if (!tracked) { + awaitStoredSave(&layers); std::vector saved(trackedLayers, false); for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) saved[layer] = layers[layer] && layerOriginal[layer]; for (const auto& [begin, end] : unitRuns(saved)) { @@ -4395,7 +4582,9 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { } const auto forgetStored = [&] { for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { - if (layers[layer]) layerOriginal[layer] = false; + if (!layers[layer]) continue; + layerOriginal[layer] = false; + cancelStoredSave(layer); } }; bool unsavedOutsideWatch = false; @@ -4445,23 +4634,32 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { // own stamps, so every later import writer stamps newer. std::vector shadowed; std::vector> imported; - const HostImport* shadowImport = nullptr; + std::vector segments; + const auto markShadowed = [&](std::uint64_t generation) { + for (const auto& segment : segments) { + std::vector ranges; + for (const auto& range : shadowed) { + if (range.begin < segment.begin || range.begin >= segment.end) continue; + Require(range.end <= segment.end, "a shadowed storage write-back range crosses the end of its import segment"); + ranges.push_back(range); + } + if (!ranges.empty()) MarkShadowed(*segment.import, ranges, generation); + } + }; const auto settle = [&](bool memoizedCollect) { for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { if (layers[layer]) layerPending[layer] = false; } - const bool allKept = std::none_of(skippedLayer.begin(), skippedLayer.end(), [](bool kept) { return kept; }); - if (allKept || !adjacent.empty()) { - const auto now = memoizedCollect ? GuestMemory::CollectWrites(firstStored, static_cast(lastStored - firstStored)) : GuestMemory::CollectWritesUncached(firstStored, static_cast(lastStored - firstStored)); - for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { - if (layers[layer] && !skippedLayer[layer]) layerGeneration[layer] = now; - } - refreshGeneration(); - advanceAdjacent(adjacent, now, firstBlock, lastBlock); - if (!shadowed.empty()) MarkShadowed(*shadowImport, shadowed, now); - } else if (!shadowed.empty()) { - MarkShadowed(*shadowImport, shadowed, GuestMemory::TrackerGeneration()); + bool advances = false; + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) advances = advances || (layers[layer] && !skippedLayer[layer]); + if (!advances && adjacent.empty() && shadowed.empty()) return; + const auto now = memoizedCollect ? GuestMemory::CollectWrites(firstStored, static_cast(lastStored - firstStored)) : GuestMemory::CollectWritesUncached(firstStored, static_cast(lastStored - firstStored)); + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { + if (layers[layer] && !skippedLayer[layer]) layerGeneration[layer] = now; } + refreshGeneration(); + advanceAdjacent(adjacent, now, firstBlock, lastBlock); + if (!shadowed.empty()) markShadowed(now); }; if (keep.empty()) { // Every selected block was written by the CPU since the layer's generation: nothing to @@ -4472,12 +4670,15 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { settle(true); return; } - if (const auto* import = HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes))) { + if (const auto* import = HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes))) segments.push_back({import, descriptor.baseAddress, descriptor.baseAddress + guestBytes}); + else segments = importSegments(); + if (!segments.empty()) { Require(!unsavedOutsideWatch, "storing storage image results pending over memory outside the write watch without the guest bytes saved before them is not implemented"); + // Outside the write watch the stored bytes are what the next store compares with. + const auto saveUnits = GuestMemory::Watched(firstStored, static_cast(lastStored - firstStored)) ? std::vector{} : prepareStoredSave(layers, skippedLayer); if (blockUnits) { // The kept blocks alone pass through the retiler (windows of their slices). - shadowImport = import; - const auto storedBytes = writeBackWindows(*import, keep, firstStored, lastStored, shadowed, imported); + const auto storedBytes = writeBackWindows(segments, keep, firstStored, lastStored, shadowed, imported, saveUnits.empty() ? nullptr : savedBytes.get()); countStorageWriteBack(storedBytes, true); if (profile) Profile().storageGpu += timer.lap(); forgetStored(); @@ -4486,6 +4687,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { uploadedKeys = DccKeys::Uncompressed; for (const auto& [from, to] : keep) GuestMemory::MarkWritten(from, static_cast(to - from)); settle(true); + commitStoredSave(saveUnits); ++Profile().storageDirectWriteBacks; return; } @@ -4500,7 +4702,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { auto tiledScratch = std::make_shared(context, static_cast(guestBytes), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); std::vector copied; for (const auto& [from, to] : keep) copied.push_back({from - descriptor.baseAddress, to - descriptor.baseAddress, from - descriptor.baseAddress}); - const auto padding = paddingSeeds(*import, std::move(copied)); + const auto padding = paddingSeeds(segments, copied); detiler.BeginBatch(); auto* recorder = Recorder::Active(); std::unique_ptr batch; @@ -4527,7 +4729,6 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { batch = std::make_unique(context); commands = batch->Handle(); } - const auto importOffset = descriptor.baseAddress - import->base; // The tracked layers are the array layers when there are several; one tracked layer means // the whole surface. const auto* storedLayers = trackedLayers == arrayLayers && trackedLayers > 1 ? &layers : nullptr; @@ -4564,9 +4765,20 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { { const auto scratchDone = WholeBufferBarrier(tiledScratch->Handle(), VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &scratchDone, 0, nullptr); - std::vector copies; - for (const auto& [from, to] : keep) copies.push_back({from - descriptor.baseAddress, importOffset + (from - descriptor.baseAddress), to - from}); - if (!copies.empty()) context.Function("vkCmdCopyBuffer")(commands, tiledScratch->Handle(), import->buffer, static_cast(copies.size()), copies.data()); + for (const auto& segment : segments) { + std::vector copies; + for (const auto& [from, to] : keep) { + const auto begin = std::max(from, segment.begin); + const auto end = std::min(to, segment.end); + if (begin < end) copies.push_back({begin - descriptor.baseAddress, begin - segment.import->base, end - begin}); + } + if (!copies.empty()) context.Function("vkCmdCopyBuffer")(commands, tiledScratch->Handle(), segment.import->buffer, static_cast(copies.size()), copies.data()); + } + if (!saveUnits.empty()) { + std::vector saves; + for (const auto& [from, to] : keep) saves.push_back({from - descriptor.baseAddress, from - descriptor.baseAddress, to - from}); + context.Function("vkCmdCopyBuffer")(commands, tiledScratch->Handle(), savedBytes->Handle(), static_cast(saves.size()), saves.data()); + } } VkImageMemoryBarrier backToGeneral = toSource; backToGeneral.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; @@ -4597,6 +4809,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { // newer than this value when its own range is collected. No CPU wrote the pages here // (MarkWritten made the only stamps), so the memoized collect is exact. settle(true); + commitStoredSave(saveUnits); ++Profile().storageDirectWriteBacks; return; } @@ -4605,7 +4818,24 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { // the round trip through the CPU below would store stale texels over whatever the memory // holds now. Its selected units are dropped instead. APS5_NO_UNREGISTERED_DROP=1 stores. static const bool unregisteredDrop = std::getenv("APS5_NO_UNREGISTERED_DROP") == nullptr; + bool overhang = false; if (unregisteredDrop && !RegisteredReadableCovers(descriptor.baseAddress, static_cast(guestBytes))) { + const auto allocationEnd = RegisteredReadableEnd(descriptor.baseAddress); + const auto& level = mips.front(); + const auto rowBytes = std::uint64_t{level.height} * level.pitchBytes; + const auto lastRowTail = mips.size() == 1 && arrayLayers == 1 && !level.tail && level.tiledSize > rowBytes ? level.tiledSize - rowBytes : 0; + const auto end = descriptor.baseAddress + guestBytes; + if (allocationEnd > descriptor.baseAddress && end - allocationEnd <= lastRowTail) { + auto covered = allocationEnd; + while (covered < end) { + const auto next = RegisteredReadableEnd(covered); + if (next <= covered) break; + covered = next; + } + overhang = covered >= end; + } + } + if (unregisteredDrop && !overhang && !RegisteredReadableCovers(descriptor.baseAddress, static_cast(guestBytes))) { static std::atomic reports{0}; if (reports.fetch_add(1) < 4) std::fprintf(stderr, "[gpu] storage image 0x%llx+0x%llx: memory no longer registered; %zu pending ranges dropped\n", static_cast(descriptor.baseAddress), static_cast(guestBytes), keep.size()); unregisteredDropped.fetch_add(1, std::memory_order_relaxed); @@ -4708,7 +4938,9 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { // The store above is the only write to these pages, so `original` is current at a fresh // generation, unless blocks were kept for the CPU: then the image is stale there. for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { - if (layers[layer]) layerOriginal[layer] = !skippedLayer[layer]; + if (!layers[layer]) continue; + layerOriginal[layer] = !skippedLayer[layer]; + cancelStoredSave(layer); } // As in the GPU-direct path, but the store's memcpy dirtied this surface's pages in the write // watch: a memoized collect would leave them for the next walk to stamp newer than this value diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index ab8b8f3125..2fe20f100c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -1,4 +1,5 @@ #include "BdaTests.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libc/include/GuestAllocations.hpp" @@ -23,6 +24,7 @@ #include #include #include +#include #ifndef _WIN32 #include @@ -42,6 +44,118 @@ void reject(TAction action, const char* reason) { throw std::runtime_error(std::string("expected BDA rejection: ") + reason); } +void addressTableKeyTests() { + using ShaderRecompiler::BdaAbi::Range; + const std::array, 1> written{{{0x1000, 0x1040}}}; + const std::array, 1> otherWritten{{{0x1000, 0x1080}}}; + const std::array own{{{0x2000, 0x2100, 0x70000000, ShaderRecompiler::BdaAbi::Read, 0}}}; + const std::array moved{{{0x2000, 0x2100, 0x70010000, ShaderRecompiler::BdaAbi::Read, 0}}}; + const std::array, 2> ownAsWrites{{{0x2000, 0x2100}, {0x70000000, ShaderRecompiler::BdaAbi::Read}}}; + const auto plain = AddressTableKey({}, own); + Require(AddressTableKey({}, own) == plain, "one build's BDA table key is not stable"); + Require(AddressTableKey(written, own) != plain && AddressTableKey(written, own) != AddressTableKey(otherWritten, own), "BDA table keys of builds that differ only in their written ranges are equal"); + Require(AddressTableKey({}, moved) != plain, "BDA table keys of builds whose own region moved are equal"); + Require(AddressTableKey(ownAsWrites, {}) != plain, "written ranges and own regions with the same words give one BDA table key"); +} + +void snapshotTableTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t piece = 65536; + void* block = ::operator new(piece, std::align_val_t{piece}); + std::memset(block, 0x3c, piece); + auto importing = context; + importing.hostImportAlignment = piece; + const auto change = [&](bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, piece, true, true, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + change(true); + alignas(256) static std::array source{}; + const auto snapshotAddress = reinterpret_cast(source.data()); + std::array first{}; + std::array second{}; + first.fill(std::byte{0x11}); + second.fill(std::byte{0x22}); + struct Build { + std::unique_ptr memory; + std::unique_ptr bda; + std::uint64_t snapshot = 0; + VkBuffer table = VK_NULL_HANDLE; + VkBuffer view = VK_NULL_HANDLE; + }; + const auto build = [&](std::span bytes, bool written, bool addressable, std::uint64_t address) { + Build result; + result.memory = std::make_unique(importing); + auto& memory = *result.memory; + if (addressable) memory.AcquireRegistered(); + if (written) memory.AddWritable(reinterpret_cast(block) + 256, 64); + memory.AddSnapshot({address, bytes}); + memory.Upload(addressable); + std::uint32_t adjustment = 0; + result.view = memory.Descriptor(address, bytes.size(), adjustment).buffer; + if (!addressable) return result; + const auto ranges = memory.AddressRanges(); + const auto found = std::find_if(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin == address; }); + Require(found != ranges.end() && found->end == address + bytes.size(), "the snapshot has no BDA table entry of its own"); + result.snapshot = found->deviceAddress; + const auto held = access.addressBytes(result.snapshot); + Require(held.size() >= bytes.size() && std::memcmp(held.data(), bytes.data(), bytes.size()) == 0, "the snapshot's device copy does not hold its bytes"); + if (const auto cached = memory.CachedAddressTable()) { + Require(cached->ranges->size() == ranges.size() && std::memcmp(cached->ranges->data(), ranges.data(), ranges.size() * sizeof(ranges.front())) == 0, "a cached BDA table differs from the build's ranges"); + } + result.bda = std::make_unique(importing, memory); + result.table = result.bda->Table().buffer; + return result; + }; + const bool spaces = AddressSpaceCounters().enabled; + { + auto a = build(first, false, true, snapshotAddress); + auto plain = build(first, false, false, snapshotAddress); + auto d = build(first, true, true, snapshotAddress); + auto b = build(first, false, true, snapshotAddress); + auto c = build(second, false, true, snapshotAddress); + Require(a.snapshot == b.snapshot && a.view == b.view, "two builds of one snapshot with the same bytes do not share its device copy"); + Require(c.snapshot != a.snapshot, "a snapshot with other bytes took the device copy of the first"); + Require(plain.view != a.view && d.snapshot == a.snapshot, "a snapshot copy made for BDA serves a build bound by descriptor, or a build with written ranges did not share the snapshot copy"); + if (spaces) { + Require(a.memory->CachedAddressTable().has_value() && c.memory->CachedAddressTable().has_value() && d.memory->CachedAddressTable().has_value(), "a build with its own regions over the address space has no cached BDA table"); + Require(a.table == b.table, "two builds of the same space and snapshot do not share their BDA table"); + Require(c.table != a.table, "a build with other snapshot bytes took another build's BDA table"); + Require(a.memory->CachedAddressTable()->serial == b.memory->CachedAddressTable()->serial && c.memory->CachedAddressTable()->serial != a.memory->CachedAddressTable()->serial, "cached BDA table serials do not follow the build's ranges"); + } + ClearSnapshotCopies(importing.device); + auto e = build(first, false, true, snapshotAddress); + Require(e.snapshot != a.snapshot, "a snapshot copy outlived the teardown of its device's copies"); + for (auto* item : {&a, &b, &c, &d, &e, &plain}) item->memory->WriteBack(); + } + if (spaces) { + std::vector> variants(66); + for (std::size_t index = 0; index < variants.size(); ++index) { + variants[index].fill(std::byte{0x40}); + variants[index][0] = static_cast(index); + variants[index][1] = static_cast(index >> 8u); + } + alignas(256) static std::array sources{}; + std::vector builds; + const auto serial = [&](std::size_t index) { + builds.push_back(build(variants[index], false, true, reinterpret_cast(sources.data()) + index * 512)); + return builds.back().memory->CachedAddressTable()->serial; + }; + const auto kept = serial(0); + const auto evicted = serial(1); + for (std::size_t index = 2; index < 64; ++index) serial(index); + Require(serial(0) == kept, "a BDA table was dropped before 64 newer tables of its space"); + serial(64); + Require(serial(0) == kept, "the most recently used BDA table of a space was evicted"); + Require(serial(1) != evicted, "the least recently used BDA table of a space was kept past 64 tables"); + for (auto& item : builds) item.memory->WriteBack(); + } + ClearSnapshotCopies(importing.device); + change(false); + ::operator delete(block, std::align_val_t{piece}); +} + void importCrossingTests(const Context& context, const BdaTestAccess& access) { constexpr std::size_t half = 65536; void* block = ::operator new(2 * half, std::align_val_t{half}); @@ -1661,6 +1775,8 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { if (dropped.enabled) Require(dropped.waiterDrops == after.waiterDrops + 1 && LeaseCounters().cacheDrops == dropped.waiterDrops, "the free did not drop the cached address space"); } importCrossingTests(context, access); + addressTableKeyTests(); + snapshotTableTests(context, access); importSpanTests(context); failedImportRemapTests(context, access); spaceImportRunTests(context); @@ -1686,4 +1802,5 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { Require(AddressCopyOverflow({{0x1000, 0x3000, 0x2000, "uncommitted pages"}}, 0x2000).empty(), "copies within the limit were refused"); const auto copies = AddressCopyOverflow({{0x1000, 0x2000, 0x1000, "not mirrored"}, {0x10000, 0x30000, 0x18000, "uncommitted pages"}}, 0x2000); Require(!copies.empty() && copies.find("0x10000+0x20000 (0.1 MiB committed, uncommitted pages)") < copies.find("0x1000+0x1000"), "the copy limit does not name the largest copy first"); + ClearSnapshotCopies(context.device); } diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp index deec634f74..066f2eff6f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp @@ -39,11 +39,14 @@ THandle newHandle() { VkFormat lastImageFormat = VK_FORMAT_UNDEFINED; std::vector createdImages; std::vector> imageCopies; +VkImage lastImage = VK_NULL_HANDLE; +std::vector clearedImages; VKAPI_ATTR VkResult VKAPI_CALL createImage(VkDevice, const VkImageCreateInfo* info, const VkAllocationCallbacks*, VkImage* image) { lastImageFormat = info->format; *image = newHandle(); createdImages.push_back(*image); + lastImage = *image; return VK_SUCCESS; } @@ -101,7 +104,9 @@ VKAPI_ATTR void VKAPI_CALL destroyImageView(VkDevice, VkImageView, const VkAlloc VKAPI_ATTR void VKAPI_CALL destroyImage(VkDevice, VkImage, const VkAllocationCallbacks*) {} VKAPI_ATTR void VKAPI_CALL freeMemory(VkDevice, VkDeviceMemory, const VkAllocationCallbacks*) {} VKAPI_ATTR void VKAPI_CALL cmdPipelineBarrier(VkCommandBuffer, VkPipelineStageFlags, VkPipelineStageFlags, VkDependencyFlags, std::uint32_t, const VkMemoryBarrier*, std::uint32_t, const VkBufferMemoryBarrier*, std::uint32_t, const VkImageMemoryBarrier*) {} -VKAPI_ATTR void VKAPI_CALL cmdClearDepthStencilImage(VkCommandBuffer, VkImage, VkImageLayout, const VkClearDepthStencilValue*, std::uint32_t, const VkImageSubresourceRange*) {} +VKAPI_ATTR void VKAPI_CALL cmdClearDepthStencilImage(VkCommandBuffer, VkImage image, VkImageLayout, const VkClearDepthStencilValue*, std::uint32_t, const VkImageSubresourceRange*) { + clearedImages.push_back(image); +} VKAPI_ATTR void VKAPI_CALL cmdCopyImage(VkCommandBuffer, VkImage source, VkImageLayout, VkImage destination, VkImageLayout, std::uint32_t count, const VkImageCopy* regions) { for (std::uint32_t i = 0; i < count; ++i) imageCopies.emplace_back(source, destination, regions[i].dstSubresource.aspectMask); } @@ -307,6 +312,29 @@ void RunDepthSurfaceReuseTests() { imageCopies.clear(); const auto otherWithA = pairing(otherDepth, stencilA); Require(imageCopies.size() == 1 && imageCopies[0] == std::make_tuple(withA, otherWithA, static_cast(VK_IMAGE_ASPECT_STENCIL_BIT)), "a stencil plane paired with another depth plane must carry its stencil over"); + { + constexpr std::uint64_t arrayBase = 0x80000000; + constexpr std::uint64_t htile = 0x81000000; + constexpr VkExtent2D extent{1024, 512}; + const auto sliceHtile = HtileMetadataBytes(extent); + DepthTarget first{arrayBase, 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + first.htileAddress = htile; + DepthTarget second{arrayBase + DepthSliceBytes(extent, 4u), 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + second.htileAddress = htile + sliceHtile; + DepthSurfaceView(context, first); + const auto firstImage = lastImage; + DepthSurfaceView(context, second); + const auto secondImage = lastImage; + const auto clearsAfter = [&](std::size_t bytes) { + clearedImages.clear(); + NoteHtileDepthClear(htile, static_cast(bytes)); + DepthSurfaceView(context, first); + DepthSurfaceView(context, second); + return clearedImages; + }; + Require(clearsAfter(sliceHtile) == std::vector{firstImage}, "an HTILE clear of the first slice must clear that slice alone"); + Require(clearsAfter(2u * sliceHtile) == std::vector{firstImage, secondImage}, "an HTILE clear over both slices must clear both"); + } ClearDepthSurfaces(context.device); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp index c09560589b..fd1e548a00 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp @@ -9,12 +9,28 @@ #include #include #include +#include +#include +#include #include #include #include #include #include #include +#ifdef _WIN32 +#include +#define APS5_DUP _dup +#define APS5_DUP2 _dup2 +#define APS5_CLOSE _close +#define APS5_FILENO _fileno +#else +#include +#define APS5_DUP dup +#define APS5_DUP2 dup2 +#define APS5_CLOSE close +#define APS5_FILENO fileno +#endif namespace { @@ -323,6 +339,40 @@ void testFailure() { AgcDriverUnregisterVideoOutput_nid_postfix(7, output); } +void testFailureLog() { + const auto path = std::filesystem::temp_directory_path() / ("aps5_worker_failure_log_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); + std::fflush(stderr); + const int saved = APS5_DUP(APS5_FILENO(stderr)); + check(saved >= 0, "stderr could not be saved"); + check(std::freopen(path.string().c_str(), "wb", stderr) != nullptr, "stderr could not be redirected"); + std::string message; + std::string error; + try { + auto output = std::make_shared(); + output->state->fail = true; + AgcDriverRegisterVideoOutput_nid_postfix(7, output); + submitFlip(); + message = expectFailure([] { AgcDriverWaitIdle_nid_postfix(); }); + check(expectFailure([] { submitFlip(); }) == message, "the stored failure was not rethrown on the next driver call"); + AgcDriverUnregisterVideoOutput_nid_postfix(7, output); + } catch (const std::exception& caught) { + error = caught.what(); + } + std::fflush(stderr); + APS5_DUP2(saved, APS5_FILENO(stderr)); + APS5_CLOSE(saved); + std::string captured; + { + std::ifstream file(path, std::ios::binary); + captured.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); + } + std::filesystem::remove(path); + if (!error.empty()) throw std::runtime_error(error); + check(message == "intentional flip failure", "the worker failure was not rethrown"); + check(captured.find("[gpu] driver failure: intentional flip failure\n") != std::string::npos, "the worker failure was not printed"); + check(captured.find("driver failure") == captured.rfind("driver failure"), "the worker failure was printed more than once"); +} + void testReset(bool compute) { std::array registers{0xc0027600, 0x20c, 1, 0}; Packet packet{registers.data(), 4, 0, {}}; @@ -365,6 +415,13 @@ int main(int argc, char** argv) { std::puts("present mode selection tests passed"); return 0; } + if (argc == 2 && std::string(argv[1]) == "failure-log") { + testFailureLog(); + const auto shutdown = expectFailure([] { LibcRunShutdown_nid_postfix(); }); + check(shutdown == "intentional flip failure", "shutdown lost worker failure"); + std::puts("AGC worker failure log test passed"); + return 0; + } if (argc == 2) testReset(std::string(argv[1]) == "compute"); else { testFlipAndBoundary(); testFlipHold(); testComputeRenderingWait(); testFailure(); } const auto shutdown = expectFailure([] { LibcRunShutdown_nid_postfix(); }); diff --git a/core/libs/prx/libSceAgcDriver/tests/FmaMixOnce.cpp b/core/libs/prx/libSceAgcDriver/tests/FmaMixOnce.cpp new file mode 100644 index 0000000000..aa48f4f1e7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/FmaMixOnce.cpp @@ -0,0 +1,93 @@ +#include "BdaAbi.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void Check(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +SpirvTarget Target() { + static constexpr std::array capabilities{ + spv::CapabilityInt64, spv::CapabilityFloat64, 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", + }; + return {0x00401000u, 0x00010300u, 32, BdaAbi::Version, capabilities, extensions, false, + {1024, 1024, 64}, 1024, 32768, {}, {}}; +} + +struct Counts { + std::size_t words = 0; + std::size_t functions = 0; +}; + +Counts Count(std::span words) { + Check(words.size() >= 5 && words[0] == spv::MagicNumber, "fma mix once: missing SPIR-V module"); + Counts counts{words.size()}; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + Check(count != 0 && count <= words.size() - offset, "fma mix once: malformed SPIR-V instruction"); + if ((words[offset] & spv::OpCodeMask) == spv::OpFunction) ++counts.functions; + offset += count; + } + return counts; +} + +Counts Compile(std::uint32_t uses) { + std::vector code; + for (std::uint32_t load = 0; load < 3u; ++load) code.insert(code.end(), {0xe0300000u | (load * 4u), 0x80010000u | ((1u + load) << 8u)}); + code.push_back(0xbf8c3f70u); + std::uint32_t result = 1u; + for (std::uint32_t use = 0; use < uses; ++use) { + const std::uint32_t opcode = use % 2u == 0u ? 0x21u : 0x22u; + code.insert(code.end(), {0xcc000000u | (opcode << 16u) | 10u, (256u + result) | ((256u + 2u) << 9u) | ((256u + 3u) << 18u)}); + result = 10u; + } + code.insert(code.end(), {0xe0700000u, 0x80010000u | (result << 8u)}); + 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}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, reinterpret_cast(code.data()), words, 0, {}}; + request.target = Target(); + 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}; + request.context.memory = memory; + request.useCache = false; + return Count(Recompile(request).spirv.Words()); +} + +} + +int main() { + try { + const auto none = Compile(0); + const auto one = Compile(1); + const auto three = Compile(3); + Check(three.words - one.words < one.words - none.words, "fma mix once: 2 more v_fma_mixlo/mixhi_f16 added " + std::to_string(three.words - one.words) + " SPIR-V words, as many as the first one with its shared emulation (" + std::to_string(one.words - none.words) + ")"); + Check(one.functions == none.functions + 1u, "fma mix once: one v_fma_mixlo_f16 emitted " + std::to_string(one.functions - none.functions) + " functions more than none, expected one shared emulation function"); + Check(three.functions == one.functions, "fma mix once: two more v_fma_mixlo/mixhi_f16 in the same float mode emitted " + std::to_string(three.functions - one.functions) + " more functions, expected calls to the shared one"); + std::printf("fma mix once tests passed (%zu words without v_fma_mix, %zu for 1, %zu for 3)\n", none.words, one.words, three.words); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp index 5cd2842612..96d92473bd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp @@ -109,6 +109,7 @@ int main() { 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"); + Check(gpuOne.words < 40000u, "formatted buffer once: a formatted load from a GPU-selected descriptor takes " + std::to_string(gpuOne.words) + " SPIR-V words, expected under 40000 since every driver that inlines the shared function copies all of it"); 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) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index edb679bc59..ef6223d78a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -62,7 +62,7 @@ AgcDriver::QueueState makeState() { queue.userConfig[0x242] = 4; queue.context = { {0x2d5, 0x2000}, - {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x1b6, 0}, {0x1b8, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, @@ -187,6 +187,48 @@ void inactiveVertexPointSizeTests() { rejected(queue); } +void retainedPixelStageTests() { + using namespace AgcDriver::Graphics; + auto queue = makeState(); + queue.context[0x8e] = queue.context[0x8f] = queue.context[0x203] = 0; + for (const auto format : {0u, 1u, 2u, 3u, 9u}) { + queue.context[0x1c4] = format; + Require(PixelProgramSkipped(queue), "a retained export format forced an unused pixel program to run"); + Require(DrawRejection(queue, true).empty(), "an unused pixel program with a retained export format was rejected"); + for (const auto control : {1u, 2u, 4u, 0x40u, 0x80u, 0x100u, 0x200u, 0x400u, 0x20000u}) { + queue.context[0x203] = control; + Require(!PixelProgramSkipped(queue), "an enabled export or execution flag was skipped"); + } + queue.context[0x203] = 0; + queue.context[0x8e] = 0xf0000000u; + queue.context[0x8f] = 0x0f000000u; + Require(PixelProgramSkipped(queue), "disjoint color masks ran an unused pixel program"); + queue.context[0x8f] = 0xf0000000u; + Require(!PixelProgramSkipped(queue), "an enabled high render target lost its pixel program"); + queue.context[0x8e] = queue.context[0x8f] = 0; + } + queue.context[0x1b3] = queue.context[0x1b4] = 2; + for (const auto [format, control] : {std::pair{1u, 1u}, std::pair{2u, 2u}, std::pair{9u, 0x100u}, std::pair{9u, 0x103u}}) { + queue.context[0x1c4] = format; + queue.context[0x203] = control; + const auto state = DecodeState(queue); + Require(DrawRejection(queue, true).empty(), "a valid pixel export combination was rejected"); + const auto pixel = DecodePixelStageInfo(queue.context, ExportMappings(state), PixelProgramSkipped(queue)); + Require(pixel.depthExportEnable == ((control & 1u) != 0) && pixel.stencilRefExportEnable == ((control & 2u) != 0) && pixel.sampleMaskExportEnable == ((control & 0x100u) != 0), "decoding lost an enabled pixel export"); + } + queue.context[0x203] = 0; + for (const auto alpha : {0xaa00u, 0xaa01u}) { + queue.context[0x2dc] = alpha; + Require(PixelProgramSkipped(queue) == ((alpha & 1u) == 0), "alpha-to-coverage enable was not respected"); + } + queue.context[0x2dc] = 0xaa00u; + for (const auto offset : {0x8eu, 0x8fu, 0x203u}) { + auto missing = queue; + missing.context.erase(offset); + Require(!PixelProgramSkipped(missing), "incomplete execution state skipped a bound pixel program"); + } +} + void hdrComponentSwapTests() { using namespace AgcDriver::Graphics; for (const auto number : {0u, 1u, 4u, 7u}) { @@ -460,7 +502,18 @@ void stateTests() { Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "a pixel program with Z_EXPORT_ENABLE was skipped"); queue.context[0x203] = 0; queue.context[0x1c4] = 1; - Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "a pixel program exporting depth was skipped"); + Require(AgcDriver::Graphics::PixelProgramSkipped(queue), "a retained Z export format ran a pixel program with depth exports disabled"); + Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "a depth-only draw with a retained Z export format was rejected"); + for (const auto control : {1u, 2u, 4u, 0x40u, 0x80u, 0x100u, 0x200u, 0x400u, 0x20000u}) { + queue.context[0x203] = control; + Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "an enabled pixel export or execution flag was ignored with a retained Z format"); + } + queue.context[0x203] = 0; + queue.context[0x2dc] = 0xaa01; + Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "alpha-to-coverage skipped the pixel program"); + queue.context[0x203] = 0x800; + Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "the Vulkan alpha-to-coverage state lost its pixel program with DB's override set"); + queue.context[0x203] = queue.context[0x2dc] = 0; queue.context[0x1c4] = 0; queue.context[0x8e] = 0xf; Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "a pixel program writing color was skipped"); @@ -681,6 +734,22 @@ void ShaderStageTests() { } } } + { + auto vertex = makeState(); + vertex.context[0x2d5] = 0x02002008u; + const auto state = AgcDriver::Graphics::DecodeState(vertex); + Require(state.stages.path == AgcDriver::Graphics::ShaderPath::Vertex + && state.stages.registerValue == 0x02002008u, + "the reported vertex-routing state did not decode as vertex"); + vertex.context[0x2d5] |= 0x10u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(vertex); }, "reserved LS_EN, ES_EN or VS_EN"); + vertex.context[0x2d5] = 0x02002008u | 0x800000u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(vertex); }, "unsupported vertex routing"); + vertex.context[0x2d5] = 0x2008u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(vertex); }, "unsupported vertex routing"); + vertex.context.erase(0x2d5); + expectFailure([&] { AgcDriver::Graphics::DecodeState(vertex); }, "missing register"); + } queue = makeState(); queue.context[0x2d5] = 0x2020; queue.userConfig[0x25b] = (64u << 9u) | 21u; @@ -777,12 +846,14 @@ void ShaderStageTests() { Require(stages.path == AgcDriver::Graphics::ShaderPath::Tessellation && stages.tessellation, "tessellation routing changed"); const auto& tessellation = *stages.tessellation; Require(tessellation.inputControlPoints == 3 && tessellation.outputControlPoints == 3 && tessellation.domain == 1 && tessellation.partitioning == 2 && tessellation.outputTopology == 2 && stages.minTessellationLevel == 1.0f && stages.maxTessellationLevel == 64.0f && tessellation.patchesPerGroup == 85 && tessellation.primitiveGroupSize == 85 && !tessellation.breakAtInstance, "the passthrough triangle-patch configuration decoded differently"); - for (const auto [parameters, reason] : {std::pair{0x44049u, "VGT_TF_PARAM"}, std::pair{0x140049u, "VGT_TF_PARAM"}, std::pair{0x2040049u, "VGT_TF_PARAM"}, std::pair{0x40059u, "fractional-odd partitioning"}, std::pair{0x4004du, "fractional-odd partitioning"}, std::pair{0x40045u, "fractional-odd partitioning"}, std::pair{0x40048u, "fractional-odd partitioning"}, std::pair{0x40029u, "fractional-odd partitioning"}, std::pair{0x4004bu, "fractional-odd partitioning"}}) { + for (const auto [parameters, reason] : {std::pair{0x44049u, "VGT_TF_PARAM"}, std::pair{0x140049u, "VGT_TF_PARAM"}, std::pair{0x2040049u, "VGT_TF_PARAM"}, std::pair{0x40059u, "fractional-odd partitioning"}, std::pair{0x40045u, "fractional-odd partitioning"}, std::pair{0x40048u, "fractional-odd partitioning"}, std::pair{0x40029u, "fractional-odd partitioning"}, std::pair{0x4004bu, "fractional-odd partitioning"}}) { queue.context[0x2db] = parameters; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason); } queue.context[0x2db] = 0x40041; Require(AgcDriver::Graphics::DecodeState(queue).stages.tessellation->partitioning == 0, "integer-partitioned triangle patches decoded differently"); + queue.context[0x2db] = 0x4004d; + Require(AgcDriver::Graphics::DecodeState(queue).stages.tessellation->partitioning == 3, "fractional-even-partitioned triangle patches decoded differently"); queue.context[0x2d6] = 0x1043f; queue.context[0x2db] = 0x4006a; queue.context[0x286] = 0x41100000; @@ -1130,6 +1201,11 @@ void PixelInputLayoutTests() { Require(PixelInputVgpr(pixel.inputAddr, PixelInput::PerspectiveCenter) == 0u && PixelInputVgpr(pixel.inputAddr, PixelInput::PerspectiveCentroid) == 2u && PixelInputVgpr(pixel.inputAddr, PixelInput::LinearCenter) == 4u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionX) == 6u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionY) == 7u, "the centroid layout moved the inputs"); pixel = decode(0x1146u, 0x1146u); Require(pixel.linearCentroid && !pixel.noPerspective && PixelInputVgpr(pixel.inputAddr, PixelInput::LinearCentroid) == 4u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionX) == 6u && PixelInputVgpr(pixel.inputAddr, PixelInput::FrontFace) == 7u, "the linear centroid layout moved the inputs"); + Require(!pixel.frontFaceAllBits, "FRONT_FACE_ALL_BITS was decoded from a clear SPI_BARYC_CNTL"); + queue.context[0x1b8] = 1u << 24u; + Require(decode(0x1146u, 0x1146u).frontFaceAllBits, "SPI_BARYC_CNTL FRONT_FACE_ALL_BITS was not decoded"); + Require(!decode(0x326u, 0x326u).frontFaceAllBits, "FRONT_FACE_ALL_BITS was decoded for a pixel program without the front-face input"); + queue.context[0x1b8] = 0u; pixel = decode(0x506u, 0x7afu); Require(pixel.inputAddr == 0x7afu && !pixel.posY && pixel.posZ && PixelInputVgpr(pixel.inputAddr, PixelInput::PerspectiveCentroid) == 4u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionX) == 12u && PixelInputVgpr(pixel.inputAddr, PixelInput::PositionZ) == 14u, "ADDR-only inputs did not reserve their VGPRs"); for (const auto bit : {0x8u, 0x4000u}) { @@ -1447,6 +1523,25 @@ void DepthStencilTests() { const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); Require(rejection.empty(), "precheck rejected a stencil draw with a surface: " + rejection); Require(state.depth && state.depth->address == 0x10000 && state.depth->stencilAddress == 0x20000 && state.depth->format == VK_FORMAT_D32_SFLOAT_S8_UINT && state.depth->clearStencil == 7 && state.depth->htileAddress == 0x10000030000ull && !state.depth->htileStencil, "depth surface decode changed"); + for (const auto clear : {0u, 1u, 2u, 3u}) { + auto retained = queue; + retained.context[0x8e] = retained.context[0x8f] = retained.context[0x203] = 0; + retained.context[0x000] = clear; + retained.context[0x200] = 0x00700777u; + auto unbound = retained; + unbound.shader[0x008] = unbound.shader[0x009] = 0; + const auto expected = AgcDriver::Graphics::DecodeState(unbound); + for (const auto format : {1u, 2u, 3u, 9u}) { + retained.context[0x1c4] = format; + Require(AgcDriver::Graphics::PixelProgramSkipped(retained), "a retained format ran the pixel program during a depth/stencil pass"); + const auto actual = AgcDriver::Graphics::DecodeState(retained); + Require(actual.depth && expected.depth && actual.depth->address == expected.depth->address && actual.depth->stencilAddress == expected.depth->stencilAddress && actual.depth->format == expected.depth->format && actual.depth->clearDepth == expected.depth->clearDepth && actual.depth->clearStencil == expected.depth->clearStencil, "null substitution changed the depth/stencil surface"); + Require(actual.depthTest == expected.depthTest && actual.depthWrite == expected.depthWrite && actual.depthCompare == expected.depthCompare && actual.depthBoundsTest == expected.depthBoundsTest && actual.stencilTest == expected.stencilTest, "null substitution changed fixed-function depth/stencil tests"); + Require(std::memcmp(&actual.stencilFront, &expected.stencilFront, sizeof(VkStencilOpState)) == 0 && std::memcmp(&actual.stencilBack, &expected.stencilBack, sizeof(VkStencilOpState)) == 0, "null substitution changed stencil operations"); + Require(actual.viewport.minDepth == expected.viewport.minDepth && actual.viewport.maxDepth == expected.viewport.maxDepth && actual.renderExtent.width == expected.renderExtent.width && actual.renderExtent.height == expected.renderExtent.height, "null substitution changed depth coverage"); + Require(AgcDriver::Graphics::DrawRejection(retained, true).empty(), "a retained-format depth/stencil pass was rejected"); + } + } { queue.context[0x011] &= ~(1u << 29u); const auto stencilTiled = AgcDriver::Graphics::DecodeState(queue); @@ -1484,6 +1579,13 @@ void DepthStencilTests() { AgcDriver::Graphics::RegisterReadLog() = nullptr; for (const auto read : log) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks an HTILE register the decoder reads: " + std::to_string(read.offset)); Require(state.depth->htileAddress == (0x30000ull | (1ull << 40u)), "the HTILE address was not decoded from DB_HTILE_DATA_BASE and its high byte"); + { + const auto view = queue.context[0x002]; + queue.context[0x002] = 2u | (2u << 13u); + const auto slice = AgcDriver::Graphics::DecodeState(queue); + Require(slice.depth->htileAddress == (0x30000ull | (1ull << 40u)) + 2u * AgcDriver::Graphics::HtileMetadataBytes(slice.depth->extent), "slice 2 of a depth array must use the third slice of its HTILE"); + queue.context[0x002] = view; + } queue.context[0x010] &= ~0x20000000u; state = AgcDriver::Graphics::DecodeState(queue); Require(state.depth->htileAddress == 0, "a surface without TILE_SURFACE_ENABLE decoded an HTILE address"); @@ -2259,6 +2361,7 @@ VKAPI_ATTR VkResult VKAPI_CALL sampleLocationDepthProperties(VkPhysicalDevice, V void multisampleTests() { using AgcDriver::Graphics::ColorMetadataPass; + using AgcDriver::Graphics::DecodeColorBuffer; using AgcDriver::Graphics::DecodeColorMetadataPass; using AgcDriver::Graphics::DecodeState; const auto multisampled = [] { @@ -2475,6 +2578,10 @@ void multisampleTests() { queue.context[0x31f] = 0; expectFailure([&] { DecodeState(queue); }, "without a CMASK address"); queue = multisampled(); + queue.context[0x31c] |= 0x10000000u; + expectFailure([&] { DecodeState(queue); }, "DCC over a multisampled color target is unsupported"); + expectFailure([&] { DecodeColorBuffer(queue.context, 0); }, "DCC over a multisampled color target is unsupported"); + queue = multisampled(); queue.context[0x31c] |= 0x2000; setCmaskAddress(queue); std::vector reads; @@ -2505,6 +2612,9 @@ void multisampleTests() { queue = rasterizedResolve; queue.context[0x32b] = queue.context[0x31c] ^ (1u << 8u); expectFailure([&] { DecodeColorMetadataPass(queue); }, "different formats or extents"); + queue = rasterizedResolve; + queue.context[0x31c] |= 0x10000000u; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "DCC over a multisampled color target is unsupported"); queue = multisampled(); queue.context[0x0] = 0; queue.context[0x202] = 0xcc0030; @@ -2520,6 +2630,32 @@ void multisampleTests() { queue.context[0x0] = 0; queue.context[0x202] = 0xcc0020; expectFailure([&] { DecodeColorMetadataPass(queue); }, "multisampled color target"); + queue.context[0x31c] |= 0x10000000u; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "DCC over a multisampled color target is unsupported"); + queue = multisampled(); + queue.context[0x2f8] = 0; + queue.context[0x0] = 0; + queue.context[0x202] = 0x00cc0050; + queue.context[0x31c] = 0x1002e028; + queue.context[0x31d] = 0x00012000; + queue.context[0x325] = 0x2000; + queue.context[0x323] = 0x3f800000; + queue.context[0x324] = 0x3f800000; + setCmaskAddress(queue); + auto fmaskPass = DecodeColorMetadataPass(queue); + Require(fmaskPass.has_value() && fmaskPass->mode == ColorMetadataPass::Mode::FmaskDecompress && fmaskPass->targets.size() == 1 && fmaskPass->targets[0].samples == 4 && fmaskPass->targets[0].cmaskAddress == cmaskAddress && fmaskPass->targets[0].dccAddress != 0, "an FMASK decompress of a 4x multisampled target with CMASK and DCC did not decode"); + Require(fmaskPass->targets[0].clearWords[0] == 0x3f800000 && fmaskPass->targets[0].clearWords[1] == 0x3f800000, "fast clear words changed"); + queue = makeState(); + queue.context[0x0] = 0; + queue.context[0x202] = 0x00cc0050; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "over a single-sample target"); + queue = multisampled(); + queue.context[0x2f8] = 0; + queue.context[0x0] = 0; + queue.context[0x202] = 0x00cc0050; + queue.context[0x31c] = 0x00008028; + queue.context[0x31d] = 0x00012000; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "FMASK decompress of a target without a CMASK address"); using AgcDriver::Graphics::DccKeys; using AgcDriver::Graphics::MultisampledCmask; @@ -2539,6 +2675,7 @@ void multisampleTests() { MultisampledCmask seeded(0x30000, 0x1000); seeded.SeedKeys(DccKeys::Clear0000); Require(seeded.TakeClear(), "a fast-cleared seed did not clear its first use"); + Require(!seeded.TakeClear(), "a subsequent use applied the clear again"); MultisampledCmask mixedSeed(0x30000, 0x1000); mixedSeed.SeedKeys(DccKeys::Mixed); expectFailure([&] { mixedSeed.TakeClear(); }, "not modeled"); @@ -2643,6 +2780,152 @@ void framebufferLayerTests() { } } +void quadPixelMaskTests() { + using AgcDriver::Graphics::DecodeState; + using AgcDriver::Graphics::DrawRejection; + using AgcDriver::Graphics::HostSampleMask; + using AgcDriver::Graphics::PartialSampleMask; + using AgcDriver::Graphics::QuadPixelMask; + using AgcDriver::Graphics::UniformSampleMask; + using Masks = std::array; + const auto masked = [](std::uint32_t top, std::uint32_t bottom, std::uint32_t log2 = 0u) { + auto queue = makeState(); + queue.context[0x30e] = top; + queue.context[0x30f] = bottom; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + if (log2 != 0u) { + queue.context[0x2f8] = (log2 << 20u) | log2; + queue.context[0x292] = 3; + queue.context[0x3b8] = 0x0dc00000u; + queue.context[0x31d] = (log2 << 12u) | (log2 << 15u); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = log2 == 1u ? 0xcc44u : 0x622ae6aeu; + } + return queue; + }; + const auto accepted = [](const AgcDriver::QueueState& queue, const std::string& what) { + std::vector reads; + AgcDriver::Graphics::RegisterReadLog() = &reads; + const auto state = DecodeState(queue); + const auto rejection = DrawRejection(queue, false); + AgcDriver::Graphics::RegisterReadLog() = nullptr; + Require(rejection.empty(), what + " was refused by the precheck: " + rejection); + for (const auto read : reads) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a register decoded for " + what); + return state; + }; + const auto refused = [](const AgcDriver::QueueState& queue, std::string_view reason) { + expectFailure([&] { DecodeState(queue); }, reason); + Require(DrawRejection(queue, false).find(reason) != std::string::npos, "the precheck accepted a draw DecodeState refuses for " + std::string(reason)); + }; + auto state = accepted(masked(0x0000ffffu, 0u), "the X0Y0 single-sample mask"); + Require(state.quadSampleMask == Masks{1, 0, 0, 0} && PartialSampleMask(state) && !UniformSampleMask(state) && QuadPixelMask(state) == 0x1u, "PA_SC_AA_MASK 0x0000ffff/0 did not cover only the even-x, even-y pixel of each quad"); + state = accepted(masked(0xffff0000u, 0u), "the X1Y0 single-sample mask"); + Require(QuadPixelMask(state) == 0x2u, "PA_SC_AA_MASK 0xffff0000/0 did not cover only the odd-x, even-y pixel of each quad"); + state = accepted(masked(0xfffe0001u, 0x0001fffeu), "a single-sample mask with bits above sample 0"); + Require(state.quadSampleMask == Masks{1, 0, 0, 1} && QuadPixelMask(state) == 0x9u, "a single-sample mask did not keep only bit 0 of each pixel's field"); + state = accepted(masked(0x00010001u, 0x00010001u), "a single-sample mask with only sample 0 set"); + Require(!PartialSampleMask(state) && QuadPixelMask(state) == 0xfu && HostSampleMask(state) == 1u, "PA_SC_AA_MASK 0x00010001 at one sample is not a full mask"); + state = accepted(masked(0u, 0u), "an empty single-sample mask"); + Require(PartialSampleMask(state) && UniformSampleMask(state) && HostSampleMask(state) == 0u && QuadPixelMask(state) == 0xfu, "an empty mask equal for the whole quad did not become a host sample mask of 0"); + Require(AgcDriver::Graphics::DecodePixelStageInfo(masked(0x0000ffffu, 0u).context, {}).quadPixelMask == 0xfu, "the pixel stage decode read the sample mask registers instead of the decoded state"); + auto queue = masked(0x0000ffffu, 0u); + queue.context[0x203] = 0x10u; + Require(accepted(queue, "an early-Z masked draw").pixelEarlyZ, "DB_SHADER_CONTROL Z_ORDER EARLY_Z_THEN_LATE_Z did not decode as early Z"); + queue.context[0x203] = 0x50u; + Require(!accepted(queue, "a killing masked draw").pixelEarlyZ, "a pixel program with kill decoded as early Z"); + queue.context[0x313] = 0x6001u; + refused(queue, "sample masks with conservative rasterization"); + queue = masked(0xffffffffu, 0xffffffffu); + queue.context[0x313] = 0x6001u; + Require(!PartialSampleMask(accepted(queue, "a full mask with conservative rasterization")), "a full mask with conservative rasterization decoded as partial"); + state = accepted(masked(0x00050005u, 0x00050005u, 2u), "a 4x mask equal for the whole quad"); + Require(state.samples == 4 && PartialSampleMask(state) && UniformSampleMask(state) && HostSampleMask(state) == 0x5u && QuadPixelMask(state) == 0xfu, "a 4x mask 0x5 equal for the whole quad did not become the host sample mask 0x5"); + state = accepted(masked(0xfff5fff5u, 0xfff5fff5u, 2u), "a 4x mask with bits above sample 3"); + Require(HostSampleMask(state) == 0x5u && state.quadSampleMask == Masks{5, 5, 5, 5}, "a 4x mask kept bits of samples the rasterizer does not have"); + state = accepted(masked(0x0000000fu, 0u, 2u), "a 4x mask that covers whole pixels"); + Require(!UniformSampleMask(state) && QuadPixelMask(state) == 0x1u && state.quadSampleMask == Masks{0xf, 0, 0, 0}, "a 4x mask covering all samples of one quad pixel did not become a quad pixel mask"); + state = accepted(masked(0x00000003u, 0u, 1u), "a 2x mask that covers whole pixels"); + Require(QuadPixelMask(state) == 0x1u, "a 2x mask covering both samples of one quad pixel did not become a quad pixel mask"); + using AgcDriver::Graphics::QuadSampleMasks; + constexpr Masks NoSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; + state = accepted(masked(0x00020001u, 0x00080004u, 2u), "a 4x mask with one sample per quad pixel"); + Require(QuadPixelMask(state) == 0xfu && QuadSampleMasks(state) == Masks{1, 2, 4, 8}, "a 4x mask of sample q for quad pixel q did not become per-pixel sample masks"); + state = accepted(masked(0x0000000fu, 0x00030000u, 2u), "a 4x mask mixing full, empty and partial pixels"); + Require(QuadPixelMask(state) == 0x9u && QuadSampleMasks(state) == Masks{0xffff, 0, 0, 3}, "a 4x mask mixing full, empty and partial pixels did not keep full pixels as all samples"); + Require(QuadSampleMasks(accepted(masked(0x0000000fu, 0u, 2u), "a whole-pixel 4x mask")) == NoSampleMask && QuadSampleMasks(accepted(masked(0x00050005u, 0x00050005u, 2u), "a uniform 4x mask")) == NoSampleMask && QuadSampleMasks(accepted(masked(0x0000ffffu, 0u), "a single-sample mask")) == NoSampleMask, "a mask without partial pixels that differ produced per-pixel sample masks"); + for (const auto centroid : {4u, 64u}) { + queue = masked(0x00020001u, 0x00080004u, 2u); + queue.context[0x1b3] = 2u | centroid; + queue.context[0x1b4] = 2u | centroid; + Require(QuadSampleMasks(accepted(queue, "centroid interpolation under per-pixel sample masks")) == Masks{1, 2, 4, 8}, "centroid interpolation refused per-pixel sample masks"); + } + for (const auto centroid : {4u, 64u}) { + queue = masked(0x00050005u, 0x00050005u, 2u); + queue.context[0x1b3] = 2u | centroid; + queue.context[0x1b4] = 2u | centroid; + refused(queue, "sample masks that remove some samples of a pixel with centroid interpolation"); + queue.context[0x1b4] = 2u; + Require(HostSampleMask(accepted(queue, "an unaddressed centroid input")) == 0x5u, "an unaddressed centroid input refused a 4x mask"); + queue = masked(0x0000000fu, 0u, 2u); + queue.context[0x1b3] = 2u | centroid; + queue.context[0x1b4] = 2u | centroid; + Require(QuadPixelMask(accepted(queue, "centroid interpolation under a whole-pixel mask")) == 0x1u, "centroid interpolation refused a mask that covers whole pixels"); + } + queue = masked(0x0000ffffu, 0u); + queue.context[0x31d] = 0x12000; + state = accepted(queue, "a single-sample mask into a 4x target"); + Require(state.samples == 4 && state.quadSampleMask == Masks{0xf, 0, 0, 0} && QuadPixelMask(state) == 0x1u, "single-sample rasterization into a 4x target did not apply each pixel's sample 0 bit to all of its samples"); + queue.context[0x30e] = queue.context[0x30f] = 0xfffefffeu; + state = accepted(queue, "an empty single-sample mask into a 4x target"); + Require(UniformSampleMask(state) && HostSampleMask(state) == 0u, "an empty single-sample mask into a 4x target did not remove every sample"); + + AgcDriver::Graphics::Context context{}; + ShaderRecompiler::RecompileResult early; + early.spirv = std::vector{spv::MagicNumber, 0x10300u, 0u, 2u, 0u, (3u << 16u) | spv::OpExecutionMode, 1u, spv::ExecutionModeEarlyFragmentTests, (5u << 16u) | spv::OpFunction, 0u, 1u, 0u, 0u}; + ShaderRecompiler::RecompileResult late; + late.spirv = std::vector{spv::MagicNumber, 0x10300u, 0u, 2u, 0u, (3u << 16u) | spv::OpExecutionMode, 1u, spv::ExecutionModeOriginUpperLeft}; + Require(AgcDriver::Graphics::DeclaresEarlyFragmentTests(early) && !AgcDriver::Graphics::DeclaresEarlyFragmentTests(late), "early fragment tests were not read from the SPIR-V execution modes"); + using Stage = ShaderRecompiler::ShaderStage; + AgcDriver::Graphics::State uniform{}; + uniform.samples = 4; + uniform.quadSampleMask = {5, 5, 5, 5}; + const std::array earlyShaders{{{Stage::Fragment, &early, 0}}}; + const std::array lateShaders{{{Stage::Fragment, &late, 0}}}; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleMask(context, uniform, earlyShaders); }, "earlyFragmentSampleMaskTestBeforeSampleCounting"); + AgcDriver::Graphics::ValidateSampleMask(context, uniform, lateShaders); + context.earlyFragmentSampleMaskTestBeforeSampleCounting = true; + AgcDriver::Graphics::ValidateSampleMask(context, uniform, earlyShaders); + ShaderRecompiler::RecompileResult stores; + ShaderRecompiler::DescriptorBinding written{}; + written.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + written.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + written.count = 1; + written.bufferWritten = {true}; + stores.bindings.push_back(written); + const std::array storing{{{Stage::Vertex, &late, 0}, {Stage::Fragment, &stores, 0}}}; + const std::array vertexStoring{{{Stage::Vertex, &stores, 0}, {Stage::Fragment, &late, 0}}}; + AgcDriver::Graphics::State varying{}; + varying.quadSampleMask = {1, 0, 0, 0}; + varying.pixelEarlyZ = true; + varying.depth = AgcDriver::Graphics::DepthTarget{}; + varying.depthTest = true; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleMask(context, varying, storing); }, "differs between the pixels of a quad"); + AgcDriver::Graphics::ValidateSampleMask(context, varying, vertexStoring); + for (const auto change : {0, 1, 2}) { + auto relaxed = varying; + if (change == 0) relaxed.pixelEarlyZ = false; + if (change == 1) relaxed.depthTest = false; + if (change == 2) relaxed.quadSampleMask = {1, 1, 1, 1}; + AgcDriver::Graphics::ValidateSampleMask(context, relaxed, storing); + } + varying.depthTest = false; + varying.stencilTest = true; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleMask(context, varying, storing); }, "differs between the pixels of a quad"); + varying.stencilTest = false; + varying.depthBoundsTest = true; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleMask(context, varying, storing); }, "differs between the pixels of a quad"); +} + void DepthClipTests() { auto queue = makeState(); const auto direct = AgcDriver::Graphics::DecodeState(queue); @@ -4108,6 +4391,33 @@ void sampledTextureBudgetTests() { Require(SampledTextureBudget(memory, &reported, 0) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of sampled textures"); } +void storageTextureBudgetTests() { + using AgcDriver::Graphics::StorageTextureBudget; + constexpr std::uint64_t GiB = 1ull << 30u; + constexpr std::uint64_t MiB = 1ull << 20u; + VkPhysicalDeviceMemoryProperties memory{}; + memory.memoryHeapCount = 3; + memory.memoryHeaps[0] = {256 * MiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + memory.memoryHeaps[1] = {32 * GiB, 0}; + memory.memoryHeaps[2] = {16 * GiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + Require(StorageTextureBudget(memory, nullptr) == 4 * GiB, "without VK_EXT_memory_budget the storage image cache does not keep a quarter of the 16 GiB heap"); + reported.heapBudget[0] = 100 * GiB; + reported.heapBudget[1] = 100 * GiB; + Require(StorageTextureBudget(memory, &reported) == 4 * GiB, "a zero budget for the largest device-local heap did not fall back to a quarter of the heap"); + reported.heapBudget[2] = 10 * GiB; + Require(StorageTextureBudget(memory, &reported) == 2560 * MiB, "a 10 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget"); + reported.heapBudget[2] = 6 * GiB; + Require(StorageTextureBudget(memory, &reported) == 1536 * MiB, "a 6 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget"); + reported.heapBudget[2] = 1 * GiB; + Require(StorageTextureBudget(memory, &reported) == 512 * MiB, "a 1 GiB budget does not keep the 512 MiB floor"); + reported.heapBudget[2] = 40 * GiB; + Require(StorageTextureBudget(memory, &reported) == 4 * GiB, "a budget above the heap size raised the storage image cache past a quarter of the heap"); + memory.memoryHeaps[0].flags = 0; + memory.memoryHeaps[2].flags = 0; + Require(StorageTextureBudget(memory, &reported) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of storage images"); +} + void sampledBudgetReportTests() { using AgcDriver::Graphics::SampledBudgetReportDue; using std::chrono::seconds; @@ -4671,6 +4981,40 @@ void copiedWriterIndirectOverlapTests() { ::operator delete(copiedBlock, std::align_val_t{half}); } +void registeredReadableCoverTests() { + using namespace AgcDriver::Graphics; + + constexpr std::size_t block = 65536; + auto* memory = static_cast(::operator new(4 * block, std::align_val_t{block})); + const auto base = reinterpret_cast(memory); + const auto add = [&](std::size_t index, bool readable) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, memory + index * block, block, readable, readable, true); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + const auto remove = [&](std::size_t index) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, memory + index * block); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + + add(0, true); + add(1, true); + Require(RegisteredReadableCovers(base, block), "a registered allocation must be covered"); + Require(RegisteredReadableCovers(base + block / 2, block), "a range across two adjacent registered allocations must be covered"); + Require(RegisteredReadableCovers(base, 2 * block), "two adjacent registered allocations must be covered together"); + Require(!RegisteredReadableCovers(base + block, 2 * block), "a range past the last registered allocation must not be covered"); + add(3, true); + Require(!RegisteredReadableCovers(base, 4 * block), "a range over a gap between registered allocations must not be covered"); + add(2, false); + Require(!RegisteredReadableCovers(base + block, 2 * block), "a range over an unreadable registered allocation must not be covered"); + Require(RegisteredReadableCovers(base + 3 * block, block), "the readable allocation after an unreadable one must be covered"); + Require(!RegisteredReadableCovers(base, 0), "an empty range must not be covered"); + for (std::size_t index = 0; index < 4; ++index) remove(index); + Require(!RegisteredReadableCovers(base, block), "a removed allocation must not be covered"); + ::operator delete(memory, std::align_val_t{block}); +} + void rectListTests() { using namespace ShaderRecompiler; using namespace AgcDriver::Graphics; @@ -4907,6 +5251,11 @@ void earlyFragmentTestsTests() { request.context.pixel->alphaToCoverage = true; const auto replayed = serializer.Deserialize(serializer.Serialize(request)); Require(!declaresEarlyFragmentTests(request) && !declaresEarlyFragmentTests(replayed.request), "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with alpha-to-coverage declared early fragment tests"); + request = depthExportRequest(shaderControl); + request.context.pixel->quadPixelMask = 0x1u; + const auto masked = serializer.Deserialize(serializer.Serialize(request)); + Require(!declaresEarlyFragmentTests(request) && !declaresEarlyFragmentTests(masked.request), "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with a quad pixel mask declared early fragment tests"); + Require(AgcDriver::Graphics::DeclaresEarlyFragmentTests(ShaderRecompiler::Recompile(depthExportRequest(shaderControl))), "the driver did not read early fragment tests from a recompiled pixel shader"); } } @@ -5140,7 +5489,48 @@ void stencilRefExportTests() { 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"); + const auto emulated = ShaderRecompiler::Recompile(stencilRefExportRequest(0x52u, plain)); + Require(emulated.stencilReferences == std::vector{0u} && emulated.stencilReplaySafe && stencilRefShape(emulated.spirv.Words()).empty(), "a constant stencil reference was not lowered to replay"); + Require(emulated.stencilReferenceDword == 0u && emulated.pushConstants.size() == sizeof(std::uint32_t), "a stencil-only shader did not reserve dynamic reference data"); + auto replayRequest = stencilRefExportRequest(0x52u, plain); + const auto replayHandle = ShaderRecompiler::PrepareShader(replayRequest); + const auto replayCapture = ShaderRecompiler::CaptureResources(replayRequest, ShaderRecompiler::SrtRuntime{}, *replayHandle); + const auto firstReplay = ShaderRecompiler::MaterializeShader(replayRequest, *replayCapture, *replayHandle); + replayRequest.layout.pushConstantOffsetBytes = 128u; + Require(ShaderRecompiler::MatchesPreparedShader(replayRequest, *replayHandle), "moving the stencil replay stage slot invalidated the prepared artifact"); + const auto movedReplay = ShaderRecompiler::MaterializeShader(replayRequest, *replayCapture, *replayHandle); + Require(firstReplay->spirv != movedReplay->spirv && firstReplay->variantId == movedReplay->variantId && firstReplay->pushConstants == movedReplay->pushConstants, "prepared stencil replay did not move only the dynamic reference offset"); + replayRequest.layout.pushConstantSizeBytes = 0u; + Require(!ShaderRecompiler::MatchesPreparedShader(replayRequest, *replayHandle), "prepared stencil replay accepted an exhausted push range"); + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(replayRequest)); }, "unused push constant dword"); + const std::array pushedDepthStencil{0x7e000200u, 0x7e0e02f2u, 0xf8001883u, 0x700u, 0xbf810000u}; + const std::array pushedDepth{0x3f000000u}; + auto pushedRequest = stencilRefExportRequest(0x53u, plain, pushedDepthStencil); + pushedRequest.context.userData = pushedDepth; + pushedRequest.layout = {0u, 0u, 96u, 32u}; + const auto pushedHandle = ShaderRecompiler::PrepareShader(pushedRequest); + ShaderRecompiler::SrtRuntime pushedRuntime{}; + pushedRuntime.userData = pushedDepth; + const auto pushedCapture = ShaderRecompiler::CaptureResources(pushedRequest, pushedRuntime, *pushedHandle); + const auto pushedResult = ShaderRecompiler::MaterializeShader(pushedRequest, *pushedCapture, *pushedHandle); + Require(pushedResult->stencilReferenceDword == 1u && pushedResult->pushConstants.size() == 8u, "a nonaligned replay stage did not reserve its reference after existing push data"); + for (const auto [offset, bytes] : {std::pair{128u, 128u}, std::pair{120u, 8u}}) { + pushedRequest.layout.pushConstantOffsetBytes = offset; + pushedRequest.layout.pushConstantSizeBytes = bytes; + Require(ShaderRecompiler::MatchesPreparedShader(pushedRequest, *pushedHandle), "a replay payload could not move within a valid stage range"); + const auto moved = ShaderRecompiler::MaterializeShader(pushedRequest, *pushedCapture, *pushedHandle); + Require(moved->pushConstants == pushedResult->pushConstants && moved->spirv != pushedResult->spirv, "moving replay push data changed its payload or retained its old offset"); + } + pushedRequest.layout = {0u, 0u, 124u, 4u}; + Require(!ShaderRecompiler::MatchesPreparedShader(pushedRequest, *pushedHandle), "a full push budget reused a replay artifact"); + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(pushedRequest)); }, "unused push constant dword"); + pushedRequest.layout.pushConstantSizeBytes = 8u; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(pushedRequest)); }, "crosses a stage slot"); + const std::array noStencilExport{0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u}; + auto noStencilRequest = stencilRefExportRequest(0x52u, plain, noStencilExport); + noStencilRequest.layout.pushConstantSizeBytes = 0u; + const auto noStencil = ShaderRecompiler::Recompile(noStencilRequest); + Require(noStencil.stencilReferences.empty() && noStencil.stencilReferenceDword == 0xffffffffu && noStencil.pushConstants.empty(), "a stencil export flag without a remaining stencil output reserved replay state"); auto queue = stencilRefExportQueue(0x52u, 2u); queue.context[0x000] = 0; queue.context[0x002] = 0; @@ -6444,6 +6834,7 @@ int main(int argc, char** argv) { RunGuestLeaseWaitTests(); ResourceContentKeyTests(false); stateTests(); + retainedPixelStageTests(); pipelineLibrarySetTests(); hdrComponentSwapTests(); depthMaintenanceTests(); @@ -6483,6 +6874,7 @@ int main(int argc, char** argv) { clipDistanceTests(); framebufferLayerTests(); UnwrittenSampleLocationTests(); + quadPixelMaskTests(); ShaderStageTests(); NggVertexWorkgroupTests(); inactiveVertexPointSizeTests(); @@ -6510,6 +6902,7 @@ int main(int argc, char** argv) { debugBranchTests(); textureCacheBudgetTests(); sampledTextureBudgetTests(); + storageTextureBudgetTests(); sampledBudgetReportTests(); meshArgumentTests(); meshIndexBufferTests(); @@ -6528,6 +6921,7 @@ int main(int argc, char** argv) { floatControlsModeTests(); barycentricEmulationTests(); copiedWriterIndirectOverlapTests(); + registeredReadableCoverTests(); mock = MockVulkan{}; auto bdaContext = mockContext(); bdaContext.bufferDeviceAddress = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index bd5e36614f..ee2531011a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -96,6 +96,14 @@ void rejectFields(const Fields& f, std::string_view reason) { void RunGuestTextureResourceTests() { Fields base; + const auto baseWords = pack(base); + Require(!IsNullTextureResource(baseWords), "a nonzero 2D texture was classified as null"); + Fields nullBase = base; + nullBase.base40 = 0; + Require(IsNullTextureResource(pack(nullBase)), "a zero-base texture was not classified as null"); + Fields nullType = base; + nullType.typeRaw = 0; + Require(IsNullTextureResource(pack(nullType)), "a type-0 texture with a nonzero base was not classified as null"); auto result = DecodeTextureResource(pack(base)); Require(result.baseAddress == (base.base40 << 8u), "decoded base address changed"); Require(result.width == 16 && result.height == 16, "decoded width or height changed"); @@ -244,6 +252,37 @@ void RunGuestTextureResourceTests() { cubeNotMultiple.depth = 4; rejectFields(cubeNotMultiple, "multiple of 6"); + Fields multisampled = base; + multisampled.typeRaw = 14; + multisampled.width = 297; + multisampled.height = 297; + multisampled.tileModeRaw = 0x1b; + multisampled.lastLevel = 2; + multisampled.maxMip = 2; + result = DecodeTextureResource(pack(multisampled)); + Require(result.dimension == TextureDimension::k2DMsaa && result.samples == 4 && result.width == 297 && result.height == 297, "4x multisampled descriptor decoded incorrectly"); + Require(result.mipCount == 1 && result.allocatedMipCount == 1 && result.baseLevel == 0 && result.lastLevel == 0, "a multisampled descriptor's sample count was decoded as mip levels"); + Require(DecodeTextureResource(pack(base)).samples == 1, "a single-sample descriptor decoded with several samples"); + const std::array canvas{0x021f2000u, 0x03800000u, 0x804a004au, 0xe1b20facu, 0x00000000u, 0x00400020u, 0x00000000u, 0x00000000u}; + result = DecodeTextureResource(canvas); + Require(result.dimension == TextureDimension::k2DMsaa && result.samples == 4 && result.baseAddress == 0x21f200000ull && result.width == 297 && result.height == 297 && result.format == 56 && result.tileMode == TextureTileMode::kR64KBX, "the 4x canvas descriptor of PPSA99006 decoded incorrectly"); + Fields fewerFragments = multisampled; + fewerFragments.maxMip = 1; + rejectFields(fewerFragments, "same sample count"); + Fields singleSampleType = multisampled; + singleSampleType.lastLevel = 0; + singleSampleType.maxMip = 0; + rejectFields(singleSampleType, "same sample count"); + Fields offsetLevel = multisampled; + offsetLevel.baseLevel = 1; + rejectFields(offsetLevel, "same sample count"); + Fields multisampledSlice = multisampled; + multisampledSlice.depth = 1; + rejectFields(multisampledSlice, "nonzero depth or base array"); + Fields multisampledArray = multisampled; + multisampledArray.typeRaw = 15; + rejectFields(multisampledArray, "unsupported image type"); + Fields badType = base; badType.typeRaw = 0; rejectFields(badType, "unsupported image type"); @@ -438,6 +477,9 @@ void RunGuestTextureResourceTests() { Require(MatchesGuestDimension(Shape::Image2D, TextureDimension::k2D), "2D shape must match 2D dimension"); Require(!MatchesGuestDimension(Shape::Image2D, TextureDimension::k2DArray), "2D shape must not match 2D array dimension"); Require(MatchesGuestDimension(Shape::Image2DArray, TextureDimension::k2DArray), "2D array shape must match 2D array dimension"); + Require(MatchesGuestDimension(Shape::Image2DMultisample, TextureDimension::k2DMsaa), "multisampled shape must match the multisampled dimension"); + Require(!MatchesGuestDimension(Shape::Image2DMultisample, TextureDimension::k2D), "multisampled shape must not match 2D dimension"); + Require(!MatchesGuestDimension(Shape::Image2D, TextureDimension::k2DMsaa), "2D shape must not match the multisampled dimension"); Require(MatchesGuestDimension(Shape::Image2DArray, TextureDimension::kCube), "a cube must be readable as a 2D array of its faces"); Require(MatchesGuestDimension(Shape::ImageCube, TextureDimension::kCube), "cube shape must match cube dimension"); Require(!MatchesGuestDimension(Shape::ImageCube, TextureDimension::k1D), "cube shape must not match 1D dimension"); diff --git a/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp b/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp index 86fd98da3e..1e855b1d1a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp @@ -53,9 +53,8 @@ int main() { resources.controlFlow.emplace_back(); resources.controlFlow.back().condition = pinned[9]; resources.uniformFill.values[0] = pinned[10]; - auto external = std::make_unique(IrOpcode::Identity, IrType::U32, value.Id()); - external->AddArgument(pinned[11]); - resources.valueStorage.push_back(std::move(external)); + resources.valueStorage.Reserve(1); + resources.valueStorage.Create(IrOpcode::Identity, IrType::U32, value.Id())->AddArgument(pinned[11]); const auto removed = program.ReclaimUnusedValues(); if (removed != 3u) throw std::runtime_error("detached identity and cyclic garbage were not reclaimed"); if (value.Id() != id || value.ImmediateU32() != 42u || value.Uses().size() != 1u) throw std::runtime_error("a live instruction lost its value or retained an unused backlink"); diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index 175fdaf979..7b03f83944 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -1,5 +1,7 @@ #include "IntermediateRepresentation/IrProgram.hpp" +#include "IntermediateRepresentation/IrBuilder.hpp" #include "Optimization/MaskedSelectEliminator.hpp" +#include "StencilReplay.hpp" #include "RdnaDecoder/RdnaInstruction.hpp" #include #include @@ -26,6 +28,7 @@ struct Builder { IrBlock& newBlock() { auto& created = program.CreateBlock(); program.BlockOrder().push_back(&created); + program.Metadata().blockInfo.push_back(BlockInfo{created.Id()}); return created; } @@ -118,6 +121,25 @@ bool expect(const char* name, bool condition) { return condition; } +bool messageSafety(IrShaderStage stage, bool malformed = false, bool allocate = false) { + IrProgram program; + program.Resources().stage = stage; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + if (allocate) static_cast(builder.Emit(IrOpcode::MeshAllocate, IrType::Void, {&builder.Constant(0u)})); + else if (malformed) { + auto& message = program.CreateValue(IrOpcode::Sendmsg, IrType::Void); + message.AddArgument(&builder.Constant(0u)); + block.AppendInstruction(&message); + } + else static_cast(builder.Emit(IrOpcode::Sendmsg, IrType::Void, {})); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + return StencilReplaySafe(program); +} + bool run(const char* name, const std::function& test) { try { return expect(name, test()); @@ -767,6 +789,434 @@ int main() { return removed(written); }; + passed &= run("a vertex message emitted as void is replay-safe", [&] { return messageSafety(IrShaderStage::Vertex); }); + passed &= run("pixel message classification remains unchanged", [&] { return !messageSafety(IrShaderStage::Pixel); }); + passed &= run("compute message classification remains unchanged", [&] { return !messageSafety(IrShaderStage::Compute); }); + passed &= run("a message with operands is refused", [&] { return !messageSafety(IrShaderStage::Vertex, true); }); + passed &= run("mesh allocation remains refused", [&] { return !messageSafety(IrShaderStage::Vertex, false, true); }); + + passed &= run("guard implication preserves a mask within its whole-quad expansion", [&] { + Builder b; + auto& live = b.mask(16u); + auto& quads = wholeQuads(b, live); + return GuardImplies(b.program, &live, &quads) && !GuardImplies(b.program, &quads, &live); + }); + + passed &= run("guard implication intersects both paths through a phi", [&] { + Builder b; + auto& entry = *b.block; + auto& live = b.mask(16u); + auto& quads = wholeQuads(b, live); + auto& left = b.newBlock(); + auto& right = b.newBlock(); + auto& merge = b.newBlock(); + entry.AddBranch(&left); + entry.AddBranch(&right); + left.AddBranch(&merge); + right.AddBranch(&merge); + b.block = &merge; + auto& subset = b.phi(IrType::Bool, left, live); + subset.AddPhiOperand(&right, &b.logicalAnd(live, b.mask(8u))); + auto& wider = b.phi(IrType::Bool, left, live); + wider.AddPhiOperand(&right, &b.truth()); + return GuardImplies(b.program, &subset, &quads) && !GuardImplies(b.program, &wider, &quads); + }); + + passed &= run("guard implication handles whole-quad masks intersected with own active ballots", [&] { + Builder b; + auto& live = b.mask(16u); + const auto [low, high] = b.ballotWords(live); + auto& wqm = b.emit(IrOpcode::WqmU64, IrType::U64, {&b.emit(IrOpcode::CompositeConstructU64, IrType::U64, {low, high})}); + auto& wideLow = b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqm, &b.constant(0u)}); + auto& wideHigh = b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqm, &b.constant(1u)}); + const auto [allLow, allHigh] = b.ballotWords(b.truth()); + auto& guard = b.threadBit(b.wordAnd(wideLow, *allLow), b.wordAnd(*allHigh, wideHigh)); + auto& narrower = b.logicalAnd(live, b.mask(8u)); + return GuardImplies(b.program, &live, &guard) && GuardImplies(b.program, &narrower, &guard) && !GuardImplies(b.program, &guard, &live); + }); + + passed &= run("a paired mask phi with a wider incoming arm does not imply a narrower write mask", [] { + Builder b; + auto& entry = *b.block; + auto& live = b.mask(16u); + const auto [low, high] = b.ballotWords(live); + auto& left = b.newBlock(); + auto& right = b.newBlock(); + auto& merge = b.newBlock(); + entry.AddBranch(&left); + entry.AddBranch(&right); + left.AddBranch(&merge); + right.AddBranch(&merge); + b.block = &merge; + auto& lowPhi = b.phi(IrType::U32, left, *low); + lowPhi.AddPhiOperand(&right, &b.constant(0xffffffffu)); + auto& highPhi = b.phi(IrType::U32, left, *high); + highPhi.AddPhiOperand(&right, &b.constant(0xffffffffu)); + auto& merged = b.threadBit(lowPhi, highPhi); + return !GuardImplies(b.program, &merged, &live); + }); + + passed &= run("a reconverged active ballot dominates a later mask observation", [] { + Builder b; + auto& entry = *b.block; + auto& left = b.newBlock(); + auto& right = b.newBlock(); + auto& merge = b.newBlock(); + auto& exit = b.newBlock(); + entry.AddBranch(&left); + entry.AddBranch(&right); + left.AddBranch(&merge); + right.AddBranch(&merge); + merge.AddBranch(&exit); + b.block = &merge; + auto& active = b.ballotBit(b.truth()); + b.block = &exit; + auto& observed = b.mask(16u); + return GuardImplies(b.program, &observed, &active); + }); + + passed &= run("an active ballot from one branch does not dominate a merged observation", [] { + Builder b; + auto& entry = *b.block; + auto& left = b.newBlock(); + auto& right = b.newBlock(); + auto& merge = b.newBlock(); + entry.AddBranch(&left); + entry.AddBranch(&right); + left.AddBranch(&merge); + right.AddBranch(&merge); + b.block = &left; + auto& active = b.ballotBit(b.truth()); + b.block = &merge; + auto& observed = b.mask(16u); + return !GuardImplies(b.program, &observed, &active); + }); + + passed &= run("a later ballot in a reconverged block cannot justify an earlier observation", [] { + Builder b; + auto& entry = *b.block; + auto& left = b.newBlock(); + auto& right = b.newBlock(); + auto& merge = b.newBlock(); + entry.AddBranch(&left); + entry.AddBranch(&right); + left.AddBranch(&merge); + right.AddBranch(&merge); + b.block = &merge; + auto& observed = b.mask(16u); + auto& active = b.ballotBit(b.truth()); + return !GuardImplies(b.program, &observed, &active); + }); + + passed &= run("guard implication refuses a loop-carried condition", [] { + Builder b; + auto& entry = *b.block; + auto& header = b.newBlock(); + entry.AddBranch(&header); + header.AddBranch(&header); + b.block = &header; + auto& condition = b.mask(8u); + auto& consequence = b.mask(16u); + return !GuardImplies(b.program, &condition, &consequence); + }); + + passed &= run("stencil replay refuses neighbor-lane operations even without LDS storage", [] { + for (const auto opcode : {IrOpcode::PermuteU32, IrOpcode::BpermuteU32, IrOpcode::SwizzleU32, IrOpcode::ReadLane, IrOpcode::ReadFirstLane, IrOpcode::DppMoveU32, IrOpcode::DppUpdateU32, IrOpcode::Permlane16U32, IrOpcode::WqmU64}) { + Builder b; + b.emit(opcode, IrType::U32, {}); + if (StencilReplaySafe(b.program)) return false; + } + return true; + }); + + const auto exportUnder = [](Builder& b, IrValue& guard, bool stencil, bool live) { + ExportInfo info{}; + info.kind = stencil ? ExportTargetKind::MrtZ : ExportTargetKind::Mrt; + info.en = stencil ? 2u : 15u; + info.vm = live; + ExportFlags flags{static_cast(b.program.Metadata().exportInfo.size())}; + b.program.Metadata().exportInfo.push_back(info); + std::uint64_t bits = 0; + std::memcpy(&bits, &flags, sizeof(flags)); + auto& zero = b.constant(0u); + auto& value = b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&zero, &zero, &zero, &zero}); + b.emit(IrOpcode::SetAttribute, IrType::Void, {&value, &guard}, bits); + }; + + const auto pixelPredicate = [](Builder& b) -> IrValue& { + auto& value = b.emit(IrOpcode::GetBuiltin, IrType::U32, {&b.constant(static_cast(StageInputKind::FragCoord)), &b.constant(0u)}); + return b.emit(IrOpcode::INotEqual32, IrType::Bool, {&value, &b.constant(0u)}); + }; + + const auto helperPredicate = [](Builder& b, bool helper) -> IrValue& { + auto& value = b.emit(IrOpcode::GetBuiltin, IrType::U32, {&b.constant(static_cast(StageInputKind::HelperInvocation)), &b.constant(0u)}); + return b.emit(helper ? IrOpcode::INotEqual32 : IrOpcode::IEqual32, IrType::Bool, {&value, &b.constant(0u)}); + }; + + const auto stencilByMask = [](Builder& b, IrValue& mask) { + auto& zero = b.constant(0u); + auto& reference = b.select(mask, zero, b.constant(255u)); + ExportInfo info{}; + info.kind = ExportTargetKind::MrtZ; + info.en = 2u; + info.vm = true; + ExportFlags flags{static_cast(b.program.Metadata().exportInfo.size())}; + b.program.Metadata().exportInfo.push_back(info); + std::uint64_t bits = 0; + std::memcpy(&bits, &flags, sizeof(flags)); + auto& value = b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&zero, &reference, &zero, &zero}); + b.emit(IrOpcode::SetAttribute, IrType::Void, {&value, &b.truth()}, bits); + }; + + passed &= run("finite stencil references selected by an undefined value cannot be replayed", [] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& undefined = b.emit(IrOpcode::UndefU1, IrType::Bool, {}); + auto& zero = b.constant(0u); + auto& reference = b.select(undefined, zero, b.constant(255u)); + ExportInfo info{}; + info.kind = ExportTargetKind::MrtZ; + info.en = 2u; + info.vm = true; + b.program.Metadata().exportInfo.push_back(info); + ExportFlags flags{0u}; + std::uint64_t bits = 0; + std::memcpy(&bits, &flags, sizeof(flags)); + auto& value = b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&zero, &reference, &zero, &zero}); + b.emit(IrOpcode::SetAttribute, IrType::Void, {&value, &b.truth()}, bits); + return StencilReferenceValues(b.program) == std::vector{0u, 255u} && !StencilReplaySafe(b.program); + }); + + passed &= run("stencil replay admits own-lane masks and their whole-quad expansion as export guards", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& quads = wholeQuads(b, b.logicalAnd(pixelPredicate(b), helperPredicate(b, false))); + exportUnder(b, quads, true, true); + return StencilReplaySafe(b.program); + }); + + passed &= run("helper participation cannot choose a kept lane's stencil through whole-quad expansion", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + stencilByMask(b, wholeQuads(b, helperPredicate(b, true))); + return StencilReferenceValues(b.program) == std::vector{0u, 255u} && !StencilReplaySafe(b.program); + }); + + passed &= run("nested active ballots do not erase helper uncertainty before whole-quad expansion", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& active = b.ballotBit(b.truth()); + stencilByMask(b, wholeQuads(b, b.logicalAnd(active, helperPredicate(b, true)))); + return !StencilReplaySafe(b.program); + }); + + passed &= run("an uncovered helper coordinate predicate cannot choose a real stencil through a ballot", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& bits = b.emit(IrOpcode::GetBuiltin, IrType::U32, {&b.constant(static_cast(StageInputKind::FragCoord)), &b.constant(0u)}); + auto& coordinate = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&bits}); + auto& boundary = b.program.CreateValue(IrOpcode::Void, IrType::F32); + boundary.SetImmediateF32(1.0f); + auto& predicate = b.emit(IrOpcode::FPOrdGreaterThan32, IrType::Bool, {&coordinate, &boundary}); + stencilByMask(b, wholeQuads(b, predicate)); + return !StencilReplaySafe(b.program); + }); + + passed &= run("explicit helper exclusion is preserved through nested ballot mask plumbing", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& real = b.ballotBit(helperPredicate(b, false)); + stencilByMask(b, wholeQuads(b, b.logicalAnd(real, pixelPredicate(b)))); + return StencilReplaySafe(b.program); + }); + + passed &= run("only guarded read-only SRT diagnostics can be replayed", [] { + Builder b; + b.program.Info().usesFaultBuffer = true; + if (StencilReplaySafe(b.program)) return false; + b.program.Resources().guardedSrtSlots.push_back(0u); + if (!StencilReplaySafe(b.program)) return false; + b.program.Metadata().blockInfo.front().terminator.loopHeader = true; + if (StencilReplaySafe(b.program)) return false; + b.program.Metadata().blockInfo.front().terminator.loopHeader = false; + b.emit(IrOpcode::Loop, IrType::Void, {}); + return !StencilReplaySafe(b.program); + }); + + passed &= run("guarded SRT replay refuses an unannotated control-flow cycle", [] { + Builder b; + b.program.Info().usesFaultBuffer = true; + b.program.Resources().guardedSrtSlots.push_back(0u); + auto& entry = *b.block; + auto& cycle = b.newBlock(); + entry.AddBranch(&cycle); + cycle.AddBranch(&cycle); + auto& metadata = b.program.Metadata().blockInfo; + metadata[0u].terminator.kind = TerminatorKind::Branch; + metadata[0u].terminator.trueBlock = cycle.Id(); + metadata[1u].terminator.kind = TerminatorKind::Branch; + metadata[1u].terminator.trueBlock = cycle.Id(); + return !StencilReplaySafe(b.program); + }); + + passed &= run("guarded SRT replay refuses missing control-flow targets", [] { + Builder b; + b.program.Info().usesFaultBuffer = true; + b.program.Resources().guardedSrtSlots.push_back(0u); + auto& term = b.program.Metadata().blockInfo.front().terminator; + term.kind = TerminatorKind::Branch; + term.trueBlock = 1024u; + return !StencilReplaySafe(b.program); + }); + + passed &= run("a captured active ballot cannot provide helper texture coordinates", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& active = b.ballotBit(b.truth()); + auto& helperValue = b.emit(IrOpcode::GetBuiltin, IrType::U32, {&b.constant(static_cast(StageInputKind::HelperInvocation)), &b.constant(0u)}); + auto& helper = b.emit(IrOpcode::INotEqual32, IrType::Bool, {&helperValue, &b.constant(0u)}); + auto& coordinate = b.select(b.logicalAnd(active, helper), b.constant(1u), b.constant(0u)); + auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&coordinate}); + auto& image = b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}); + auto& sampler = b.emit(IrOpcode::GetSamplerResource, IrType::SamplerResource, {}); + b.emit(IrOpcode::ImageSampleRaw, IrType::U32x4, {&image, &sampler, &address}); + exportUnder(b, b.truth(), true, true); + return !StencilReplaySafe(b.program); + }); + + const auto optionalPixelBody = [&](bool derivativeConsumer, IrOpcode observer = IrOpcode::ImageSampleRaw) { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& entry = *b.block; + auto& predicate = pixelPredicate(b); + auto& zero = b.constant(0u); + auto& condition = b.emit(IrOpcode::LogicalNot, IrType::Bool, {&predicate}); + auto& body = b.newBlock(); + auto& merge = b.newBlock(); + entry.AddBranch(&body); + entry.AddBranch(&merge); + body.AddBranch(&merge); + auto& entryInfo = b.program.Metadata().blockInfo[0u]; + entryInfo.terminator.kind = TerminatorKind::ConditionalBranch; + entryInfo.terminator.condition = BranchCondition::ExecZero; + entryInfo.terminator.trueBlock = merge.Id(); + entryInfo.terminator.falseBlock = body.Id(); + entryInfo.condition = &condition; + auto& bodyInfo = b.program.Metadata().blockInfo[1u]; + bodyInfo.terminator.kind = TerminatorKind::Branch; + bodyInfo.terminator.trueBlock = merge.Id(); + b.block = &body; + auto& written = b.select(predicate, b.constant(42u), zero); + b.block = &merge; + auto& merged = b.phi(IrType::U32, entry, zero); + merged.AddPhiOperand(&body, &written); + if (derivativeConsumer) { + auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&merged}); + auto& image = b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}); + auto& sampler = b.emit(IrOpcode::GetSamplerResource, IrType::SamplerResource, {}); + b.emit(observer, IrType::U32x4, {&image, &sampler, &address}); + } else b.keep(merged); + exportUnder(b, b.truth(), true, true); + return StencilReplaySafe(b.program); + }; + + passed &= run("an optional wave body preserves inactive values consumed only within a lane", [&] { return optionalPixelBody(false); }); + passed &= run("an optional wave body cannot change helper coordinates for later implicit samples", [&] { return !optionalPixelBody(true); }); + passed &= run("an optional wave body cannot change helper coordinates for LOD queries", [&] { return !optionalPixelBody(true, IrOpcode::ImageQueryLod); }); + + passed &= run("varying attribute sources cannot be treated as uniform any-active predicates", [&] { + for (const auto opcode : {IrOpcode::GetAttribute, IrOpcode::GetInterpolationParameter, IrOpcode::GetInterpolationParameterF16, IrOpcode::GetTessellationAttribute}) { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& entry = *b.block; + auto& varying = b.emit(opcode, IrType::U32, {&b.constant(0u), &b.constant(0u), &b.constant(0u)}); + auto& predicate = b.emit(IrOpcode::INotEqual32, IrType::Bool, {&varying, &b.constant(0u)}); + auto& condition = b.emit(IrOpcode::LogicalNot, IrType::Bool, {&predicate}); + auto& left = b.newBlock(); + auto& right = b.newBlock(); + entry.AddBranch(&left); + entry.AddBranch(&right); + auto& info = b.program.Metadata().blockInfo[0u]; + info.terminator.kind = TerminatorKind::ConditionalBranch; + info.terminator.condition = BranchCondition::ExecZero; + info.terminator.trueBlock = left.Id(); + info.terminator.falseBlock = right.Id(); + info.condition = &condition; + b.block = &left; + exportUnder(b, b.truth(), true, true); + b.block = &right; + auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&varying}); + auto& image = b.emit(IrOpcode::GetImageResource, IrType::ImageResource, {}); + auto& sampler = b.emit(IrOpcode::GetSamplerResource, IrType::SamplerResource, {}); + b.emit(IrOpcode::ImageSampleRaw, IrType::U32x4, {&image, &sampler, &address}); + exportUnder(b, b.truth(), true, true); + if (StencilReplaySafe(b.program)) return false; + } + return true; + }); + + const auto anyActiveGate = [&](bool unrelatedLiveMask) { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + auto& entry = *b.block; + const auto [low, high] = b.ballotWords(pixelPredicate(b)); + auto& own = b.threadBit(*low, *high); + auto& words = b.emit(IrOpcode::BitwiseOr32, IrType::U32, {low, high}); + auto& any = b.emit(IrOpcode::INotEqual32, IrType::Bool, {&words, &b.constant(0u)}); + auto& condition = b.emit(IrOpcode::LogicalNot, IrType::Bool, {&any}); + auto& killed = b.newBlock(); + auto& kept = b.newBlock(); + entry.AddBranch(&killed); + entry.AddBranch(&kept); + auto& info = b.program.Metadata().blockInfo[0u]; + info.terminator.kind = TerminatorKind::ConditionalBranch; + info.terminator.condition = BranchCondition::SccZero; + info.terminator.trueBlock = killed.Id(); + info.terminator.falseBlock = kept.Id(); + info.condition = &condition; + b.block = &killed; + auto& falseValue = b.program.CreateValue(IrOpcode::Void, IrType::Bool); + falseValue.SetImmediateBool(false); + exportUnder(b, falseValue, true, true); + b.block = &kept; + exportUnder(b, unrelatedLiveMask ? b.truth() : own, true, true); + return StencilReplaySafe(b.program); + }; + + passed &= run("any-active early return is stable for the source mask's kept quads", [&] { return anyActiveGate(false); }); + passed &= run("an any-active early return cannot control unrelated kept invocations", [&] { return !anyActiveGate(true); }); + + passed &= run("kept invocations must be covered by the stencil export guard", [&] { + Builder b; + auto& written = b.mask(16u); + exportUnder(b, written, true, false); + exportUnder(b, b.logicalAnd(written, b.mask(8u)), false, true); + if (!StencilReferenceDefinedForKeptInvocations(b.program)) return false; + exportUnder(b, b.truth(), false, true); + return !StencilReferenceDefinedForKeptInvocations(b.program); + }); + + passed &= run("an export on one branch does not cover a kept return on another", [&] { + Builder b; + auto& entry = *b.block; + auto& left = b.newBlock(); + auto& right = b.newBlock(); + auto& merge = b.newBlock(); + entry.AddBranch(&left); + entry.AddBranch(&right); + left.AddBranch(&merge); + right.AddBranch(&merge); + auto& metadata = b.program.Metadata().blockInfo; + metadata[0].terminator = {TerminatorKind::ConditionalBranch, BranchCondition::Unknown, left.Id(), right.Id()}; + metadata[1].terminator = {TerminatorKind::Branch, BranchCondition::Always, merge.Id()}; + metadata[2].terminator = {TerminatorKind::Branch, BranchCondition::Always, merge.Id()}; + b.block = &left; + exportUnder(b, b.truth(), true, false); + b.block = &merge; + exportUnder(b, b.truth(), false, true); + return !StencilReferenceDefinedForKeptInvocations(b.program); + }); + passed &= run("an implicit-LOD pixel sample reads its quad's coordinates and keeps the select", [&] { return !sample(IrShaderStage::Pixel, 0u); }); diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp index 3f16b22de4..41824053d6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp @@ -57,9 +57,31 @@ void BufferCap() { for (int index = 0; index < 3; ++index) thread.Add("Driver.Packet", nullptr, origin + milliseconds(index), origin + milliseconds(index) + microseconds(30)); const auto snapshot = thread.Collect(); Check(snapshot.slices.size() == 2 && snapshot.dropped, "a full buffer did not stop and set the dropped flag"); + Check(snapshot.eventsLost == 1 && snapshot.eventsFullAt == origin + milliseconds(2) && !snapshot.totalsFull, "a full buffer did not report the lost slice and its time apart from the totals"); Check(snapshot.totals.size() == 1 && snapshot.totals[0].count == 3 && snapshot.totals[0].length == microseconds(90), "totals missed a slice past the cap"); } +void LostEventsCount() { + std::atomic presents{1}; + TraceThread thread("queue worker 0x0", presents, 2); + const auto origin = Clock::now(); + Check(!thread.Collect().eventsFullAt && thread.Collect().eventsLost == 0, "a thread that lost nothing reported a loss"); + for (int index = 0; index < 10; ++index) thread.Add("Driver.Packet", nullptr, origin + milliseconds(index), origin + milliseconds(index) + microseconds(30)); + const auto snapshot = thread.Collect(); + Check(snapshot.eventsLost == 8 && snapshot.eventsFullAt == origin + milliseconds(2), "the lost slices were not counted from the first one"); +} + +void TotalsOverflow() { + static char scopes[600]; + std::atomic presents{1}; + TraceThread thread("presenter", presents, 4096); + const auto origin = Clock::now(); + for (const auto& scope : scopes) thread.Add(&scope, nullptr, origin, origin + microseconds(30)); + const auto snapshot = thread.Collect(); + Check(snapshot.totalsFull && snapshot.dropped && snapshot.eventsLost == 0 && !snapshot.eventsFullAt, "a full totals table was not reported apart from the event buffer"); + Check(snapshot.totals.size() == 512 && snapshot.slices.size() == 600, "a full totals table stopped the event buffer or kept more than its entries"); +} + void CapacitySettings() { Check(TraceThread::ParseCapacity(nullptr) == TraceThread::DefaultCapacity, "the default event capacity changed"); Check(TraceThread::ParseCapacity("1") == 1, "the smallest event capacity was rejected"); @@ -152,6 +174,22 @@ void BatchIds() { Check(TraceFormat::GpuSlices(ranges, first, last, origin)[0].batch == 3, "a GPU range lost its batch id"); } +void FullBufferEvents() { + const auto origin = Clock::now(); + const std::vector slices{{"Driver.Packet", nullptr, origin + milliseconds(1), microseconds(30)}}; + TraceThread::Snapshot snapshot{"queue worker 0x0", slices, {}, true, 7, origin + milliseconds(3), false}; + const std::vector threads{snapshot}; + const std::vector presents{origin, origin + milliseconds(16)}; + const auto json = TraceFormat::Events(7, origin, threads, 0, presents, 5); + const std::string expected = + "{\"name\":\"trace buffer full\",\"ph\":\"i\",\"ts\":3000.000,\"pid\":7,\"tid\":1,\"s\":\"t\",\"args\":{\"events_lost\":7}},\n" + "{\"name\":\"present\""; + Check(json.find(expected) != std::string::npos, "the cut-off instant event is missing or has the wrong shape"); + snapshot.eventsLost = 0; + snapshot.eventsFullAt.reset(); + Check(TraceFormat::Events(7, origin, std::vector{snapshot}, 0, presents, 5).find("trace buffer full") == std::string::npos, "a thread that lost nothing got a cut-off event"); +} + void SummaryFields() { const auto origin = Clock::now(); AgcDriver::TraceSummary summary; @@ -184,6 +222,24 @@ void SummaryFields() { auto dropped = threads; dropped[1].dropped = true; Check(TraceFormat::Summary(summary, dropped, presents).find("\"trace_dropped\":true") != std::string::npos, "a dropped thread did not mark the summary"); + Check(line.find("trace_dropped_threads") == std::string::npos, "a summary without drops listed threads"); + dropped[0].dropped = true; + dropped[0].eventsLost = 2240000; + dropped[0].eventsFullAt = presents.front() + milliseconds(32); + dropped[1].totalsFull = true; + const auto detailed = TraceFormat::Summary(summary, dropped, presents); + Check(detailed.find("\"trace_dropped\":true,\"trace_dropped_threads\":{\"queue worker 0x0\":{\"events_full_after_s\":0.032,\"events_lost\":2240000,\"totals_full\":false},\"presenter\":{\"totals_full\":true}},\"trace\":") != std::string::npos, "the dropped threads were not listed with their reasons"); + auto shared = threads; + shared.push_back({"shader prepare", {}, {}, true}); + shared.push_back({"shader prepare", {}, {}, true}); + shared[2].eventsLost = 5; + shared[2].eventsFullAt = presents.front() + milliseconds(30); + shared[3].eventsLost = 7; + shared[3].eventsFullAt = presents.front() + milliseconds(10); + shared[3].totalsFull = true; + const auto merged = TraceFormat::Summary(summary, shared, presents); + Check(merged.find("\"trace_dropped_threads\":{\"shader prepare\":{\"events_full_after_s\":0.010,\"events_lost\":12,\"totals_full\":true}},\"trace\":") != std::string::npos, "threads with one name were not merged into one entry"); + Check(merged.find("shader prepare") == merged.rfind("shader prepare"), "threads with one name were listed twice"); } void GpuClock() { @@ -271,6 +327,25 @@ void IntervalSummaryFields() { Check(line == expected, "interval summary line changed"); } +void IntervalDroppedThreads() { + const auto origin = Clock::now(); + AgcDriver::TraceSummary summary; + summary.interval = std::make_pair(30.0, 60.0); + std::vector threads{{"queue worker 0x0", {}, {}, true, 5}}; + const std::vector presents{origin + milliseconds(100), origin + milliseconds(116)}; + threads[0].eventsFullAt = origin + milliseconds(40); + auto line = TraceFormat::Summary(summary, threads, presents); + Check(line.find("\"queue worker 0x0\":{\"events_lost\":5,\"totals_full\":false}") != std::string::npos, "a loss before the interval printed its time"); + threads[0].eventsFullAt = origin + milliseconds(104); + line = TraceFormat::Summary(summary, threads, presents); + Check(line.find("{\"events_full_after_s\":0.004,\"events_lost\":5,") != std::string::npos, "a loss inside the interval lost its time"); + threads[0].eventsFullAt = origin + milliseconds(120); + line = TraceFormat::Summary(summary, threads, presents); + Check(line.find("\"queue worker 0x0\":{\"events_lost\":5,\"totals_full\":false}") != std::string::npos && line.find("events_full_after_s") == std::string::npos, "a loss after the last present printed its time"); + line = TraceFormat::Summary(summary, threads, {}); + Check(line.find("\"queue worker 0x0\":{\"events_lost\":5,\"totals_full\":false}") != std::string::npos && line.find("events_full_after_s") == std::string::npos, "an interval without presents printed a loss time"); +} + void GzipFile() { std::string text; for (int index = 0; index < 20000; ++index) text += "{\"name\":\"Driver.Packet\",\"ph\":\"X\",\"ts\":" + std::to_string(index) + "}\n"; @@ -323,7 +398,10 @@ int main() { WindowBySeconds(); WindowRejects(); BufferCap(); + LostEventsCount(); + TotalsOverflow(); CapacitySettings(); + FullBufferEvents(); FoldShortSlices(); EscapeStrings(); EventsShape(); @@ -334,6 +412,7 @@ int main() { SummaryOnlyThread(); TotalsSince(); IntervalSummaryFields(); + IntervalDroppedThreads(); GzipFile(); AppendLines(); std::cout << "performance trace tests passed\n"; diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 44a2886d86..39131eb9ca 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -209,7 +209,8 @@ void testContextAndBases() { expectFailure([] { static_cast(AgcDriver::Pm4::PartialGroupThreads({0x10002, 1, 1}, {0x0001ffff, 0x00010001, 0x00010001})); }, "exceeds 32 bits"); expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x15, {1, 1, 1, 0x2063}, 2), 0); }, "dispatch modifiers"); expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x15, {1, 1, 1, 0x41}, 4), 0); }, "header flags"); - expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x15, {1, 1, 1, 0x49}), 0); }, "dispatch modifiers"); + AgcDriver::Pm4::Validate(makePacket(0x15, {1, 1, 1, 0x49}), 0); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x15, {1, 1, 1, 0x51}), 0); }, "dispatch modifiers"); execute(state, makePacket(0x13, {32})); execute(state, makePacket(0x26, {0x1000, 1})); execute(state, makePacket(0x2a, {1})); @@ -506,6 +507,29 @@ void testCopies() { const auto clockStore =AgcDriver::Pm4::ResolveStore(clockCopy(&clock[0]), state, 64); check(clockStore.has_value() && clockStore->Bytes().size() == 8, "GPU clock COPY_DATA did not resolve as an 8-byte store"); expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x40, {0x1020a, 0, 0, low(&clock[0]), high(&clock[0])}), 0); }, "reference-clock"); + alignas(8) std::array fromRegister{0xdeadbeef, 0xdeadbeef}; + const auto registerCopy = [&](std::uint32_t control, std::uint32_t offset) { return makePacket(0x40, {control, offset, 0, low(fromRegister.data()), high(fromRegister.data())}); }; + execute(state, makePacket(0x76, {0x20c, 0x02754800, 0x2})); + execute(state, registerCopy(0x00100200, 0x2e0c)); + check(fromRegister[0] == 0x02754800 && fromRegister[1] == 0xdeadbeef, "COPY_DATA from a shader register failed"); + execute(state, registerCopy(0x00110200, 0x2e0c)); + check(fromRegister[0] == 0x02754800 && fromRegister[1] == 0x2, "64-bit COPY_DATA from two shader registers failed"); + execute(state, makePacket(0x69, {0x10, 0xabc})); + execute(state, registerCopy(0x00100200, 0xa010)); + check(fromRegister[0] == 0xabc, "COPY_DATA from a context register failed"); + check(!AgcDriver::Pm4::ResolveStore(registerCopy(0x00100200, 0x2e0c), state, 64).has_value(), "COPY_DATA from a register resolved before its turn"); + AgcDriver::Pm4::NoteDispatchRegisters(makePacket(0x15, {4, 2, 1, 0x8041}), state); + execute(state, registerCopy(0x00100200, 0x2e00)); + check(fromRegister[0] == 0x8041, "COPY_DATA did not read the last dispatch initiator"); + execute(state, registerCopy(0x00100200, 0x2e01)); + check(fromRegister[0] == 4, "COPY_DATA did not read the last direct dispatch's group count"); + AgcDriver::Pm4::NoteDispatchRegisters(makePacket(0x16, {0, 0x8001}), state); + execute(state, registerCopy(0x00100200, 0x2e00)); + check(fromRegister[0] == 0x8001, "COPY_DATA did not read the last indirect dispatch initiator"); + expectFailure([&] { execute(state, registerCopy(0x00100200, 0x2e01)); }, "has no known value"); + expectFailure([&] { execute(state, registerCopy(0x00100200, 0x2e3f)); }, "has no known value"); + expectFailure([&] { AgcDriver::Pm4::Validate(registerCopy(0x00100200, 0x1000), 0); }, "outside the shader, context and user config"); + expectFailure([&] { AgcDriver::Pm4::Validate(registerCopy(0x00110200, 0x2fff), 0); }, "outside the shader, context and user config"); execute(state, makePacket(0x50, {0x60000000, low(source.data()), high(source.data()), low(destination.data()), high(destination.data()), 16})); check(source == destination, "DMA_DATA copy failed"); execute(state, makePacket(0x50, {0x40000000, 0x44332211, 0, low(destination.data()), high(destination.data()), 6})); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 916b9ca313..251be75a8e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -3,6 +3,7 @@ #include "Triangle_vert_spv.h" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" @@ -52,6 +53,8 @@ #include #include #include +#include +#include #include #include #include @@ -281,6 +284,11 @@ public: bool NullDescriptors() const { return nullDescriptors; } const Context& GetContext() const { return context; } bool Graphics() const { return graphics; } + VkPhysicalDeviceProperties Properties() const { + VkPhysicalDeviceProperties properties{}; + function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + return properties; + } void WaitQueue() const { Check(context.Function("vkQueueWaitIdle")(context.queue), "vkQueueWaitIdle"); } private: @@ -805,6 +813,116 @@ void readListTests(const Device& device, Recorder& recorder) { static_cast(device); } +struct TestRange { + std::uint64_t begin; + std::uint64_t end; +}; + +void overlappedRangeTests(Recorder& recorder) { + Require(recorder.Idle(), "the recorder is busy before the overlap tests"); + recorder.NotePendingWrite(0x210000, 0x100); + recorder.NotePendingWrite(0x210400, 0x10); + recorder.Submit(); + recorder.NotePendingWrite(0x210800, 0x200); + recorder.NotePendingWrite(0x210900, 0x200); + const std::array ranges{{{0x20f000, 0x210000}, {0x210000, 0x210080}, {0x210080, 0x210400}, {0x210400, 0x210410}, {0x210410, 0x210800}, {0x210800, 0x210801}, {0x210801, 0x210a00}, {0x210a00, 0x210b00}, {0x210b00, 0x211000}, {0x300000, 0x300010}}}; + std::vector> writes; + recorder.PendingWriteRanges(writes); + Require(writes.size() == 4, "the pending write ranges miss a batch's writes"); + std::vector indices; + std::size_t matched = 0; + for (const std::size_t first : {0u, 3u, 6u, 10u}) { + OverlappedRanges(std::span(ranges), first, writes, indices); + Require(std::is_sorted(indices.begin(), indices.end()) && std::adjacent_find(indices.begin(), indices.end()) == indices.end(), "the overlapped ranges are not sorted and unique"); + for (std::size_t index = 0; index < ranges.size(); ++index) { + const bool listed = std::find(indices.begin(), indices.end(), index) != indices.end(); + const bool pending = index >= first && recorder.PendingWriteOverlaps(ranges[index].begin, static_cast(ranges[index].end - ranges[index].begin)); + Require(listed == pending, "a range is listed as overlapped differently from PendingWriteOverlaps"); + matched += listed ? 1 : 0; + } + } + Require(matched == 12, "the overlap comparison covered too few overlapped ranges"); + recorder.Sync(); + recorder.PendingWriteRanges(writes); + Require(writes.empty(), "pending write ranges outlived their batches"); +} + +void spaceWriteSyncTests(const Device& device, Recorder& recorder) { + const auto& context = device.GetContext(); + if (context.hostImportAlignment == 0) { + std::cout << "host imports unavailable: address space pending writes not tested\n"; + return; + } + constexpr std::size_t bytes = 0x10000; + constexpr std::size_t count = 4; + void* block = ::operator new(2 * count * bytes, std::align_val_t{65536}); + std::memset(block, 0, 2 * count * bytes); + auto* memory = static_cast(block); + const auto registry = [&](bool add) { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < count; ++index) { + if (add) mutation.Add(memory + 2 * index * bytes, bytes, true, false, true); + else mutation.Remove(memory + 2 * index * bytes); + } + }; + const auto rangeAt = [&](std::size_t index) { return reinterpret_cast(memory + 2 * index * bytes); }; + const auto build = [&] { + GuestBufferMemory leased(context); + leased.AcquireRegistered(); + leased.AddReadable(rangeAt(0) + 64, 16); + leased.Upload(true); + const auto reads = leased.DeviceReads(); + leased.WriteBack(); + return reads.size(); + }; + registry(true); + Require(recorder.Idle(), "the recorder is busy before the address space tests"); + const auto regions = build(); + if (regions < count) { + registry(false); + ::operator delete(block, std::align_val_t{65536}); + std::cout << "the registered ranges are not served in place: address space pending writes not tested\n"; + return; + } + const std::array value{std::byte{0x5a}, std::byte{0xa5}, std::byte{0x5a}, std::byte{0xa5}}; + const auto landed = [&](std::uint64_t address) { return std::memcmp(reinterpret_cast(address), value.data(), value.size()) == 0; }; + const auto inside = rangeAt(2) + 0x100; + static_cast(recorder.Commands()); + recorder.AfterCompletions(inside, value, 7000, 0, false); + Require(Recorder::PendingCompletionLabels() == 1 && !landed(inside), "the label inside a registered range is not pending"); + build(); + Require(landed(inside) && Recorder::PendingCompletionLabels() == 0, "a build over the address space did not complete the label pending in one of its ranges"); + recorder.Sync(); + const auto gap = rangeAt(2) + bytes + 0x100; + static_cast(recorder.Commands()); + recorder.AfterCompletions(gap, value, 7001, 0, false); + build(); + Require(!landed(gap) && Recorder::PendingCompletionLabels() == 1, "a build over the address space completed a label outside its ranges"); + recorder.Sync(); + Require(landed(gap) && Recorder::PendingCompletionLabels() == 0, "the label outside the ranges did not land"); + static_cast(recorder.Commands()); + recorder.AfterCompletions(rangeAt(1) + 0x40, value, 7002, 0, false); + recorder.Submit(); + static_cast(recorder.Commands()); + recorder.AfterCompletions(rangeAt(3) + 0x40, value, 7003, 0, false); + build(); + Require(landed(rangeAt(1) + 0x40) && landed(rangeAt(3) + 0x40) && Recorder::PendingCompletionLabels() == 0, "a build over the address space missed a label behind an earlier sync"); + recorder.Sync(); + static_cast(recorder.Commands()); + recorder.AfterCompletions(rangeAt(1) + 0x80, value, 7004, 0, false); + bool noted = false; + recorder.OnComplete([&] { + static_cast(recorder.Commands()); + recorder.AfterCompletions(rangeAt(3) + 0x80, value, 7005, 0, false); + noted = true; + }); + build(); + Require(noted && landed(rangeAt(1) + 0x80) && landed(rangeAt(3) + 0x80) && Recorder::PendingCompletionLabels() == 0, "a build over the address space missed a label a completion noted during its sync"); + recorder.Sync(); + registry(false); + ::operator delete(block, std::align_val_t{65536}); +} + void writeSettledTests(const Device& device, Recorder& recorder) { Require(recorder.PendingWriteSettled(0x50000, 0x100), "an unwritten range is not settled"); recorder.NotePendingWrite(0x50000, 0x100); @@ -2565,6 +2683,83 @@ void drawSnapshotPatchTests(const Device& device) { } } +void drawSnapshotEpochTests(const Device& device) { + const auto& context = device.GetContext(); + constexpr std::size_t bytes = 4 * 65536; + std::unique_lock gpu(GpuMutex()); + void* block = context.hostImportAlignment != 0 ? AllocateWatched(bytes, 65536) : nullptr; + if (block == nullptr) { + std::cout << "host imports unavailable: draw snapshot reuse within a collect epoch not tested\n"; + return; + } + struct Release { + void* block; + ~Release() { ReleaseWatched(block, bytes); } + } release{block}; + auto* guest = static_cast(block); + for (std::size_t at = 0; at < bytes; ++at) guest[at] = static_cast(at * 7u + 3u); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, reinterpret_cast(block), bytes); + } + } unregister{context, block}; + if (HostImportFor(context, address, bytes) == nullptr) { + std::cout << "host import of the block refused: draw snapshot reuse within a collect epoch not tested\n"; + return; + } + constexpr std::size_t elementOffset = 4096; + constexpr std::size_t elementBytes = 2 * 65536; + const auto element = address + elementOffset; + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding binding; + binding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + binding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + binding.descriptorSet = 0; + binding.binding = 0; + binding.count = 1; + binding.guestDescriptor = {static_cast(element), static_cast(element >> 32u) & 0xffffu, static_cast(elementBytes), 0x31000000u}; + binding.bufferWritten = {false}; + program.bindings.push_back(binding); + const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + auto snapshotContext = context; + DescriptorCache cache(snapshotContext); + snapshotContext.descriptorCache = &cache; + Recorder snapshotRecorder(snapshotContext); + snapshotRecorder.Activate(); + { + ShaderResources resources(snapshotContext, compute); + const auto snapshot = [&] { + auto bindings = resources.PrepareDrawBindings(snapshotRecorder); + Require(bindings != nullptr && bindings->snapshots.size() == 1, "read-only draw input was not snapshotted"); + auto buffer = bindings->snapshots[0].buffer; + const auto contents = buffer->Bytes(); + Require(contents.size() == elementBytes && std::memcmp(contents.data(), reinterpret_cast(element), elementBytes) == 0, "a draw snapshot does not hold the guest bytes of its draw"); + return buffer; + }; + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto first = snapshot(); + Require(snapshot() == first, "a second draw in the same collect epoch copied its unchanged input again"); + guest[elementOffset + 65536 + 17] ^= 0xffu; + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto changed = snapshot(); + Require(changed != first, "a draw after a store and a new collect epoch reused the snapshot taken before the store"); + Require(snapshot() == changed, "a second draw after the store copied its input again"); + snapshotRecorder.Sync(); + } + std::cout << "draw snapshot reuse within a collect epoch: ok\n"; +} + void drawSnapshotCapTests(const Device& device, Recorder& recorder) { using namespace AgcDriver::GuestMemory; const auto& context = device.GetContext(); @@ -2761,6 +2956,24 @@ void drawSnapshotEvictionTests(const Device& device) { Require(cache.ReusableDrawSnapshot(address, 16) == nullptr && cache.ReusableDrawSnapshot(address, 16) == nullptr, "a snapshot outlived a CPU store"); } +void unwrittenSnapshotTests(const Device& device) { + Recorder cache(device.GetContext()); + const auto buffer = std::make_shared(device.GetContext(), 16, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + constexpr std::uint64_t address = 0x10000000; + const std::array stamp{1, 1, 1}; + const std::array next{1, 2, 1}; + cache.NoteUnwrittenDrawSnapshot(address, 16, stamp, buffer); + Require(cache.UnwrittenDrawSnapshot(address, 16, stamp) == buffer, "a noted unwritten snapshot is not found under its stamp"); + cache.NoteUnwrittenDrawSnapshot(address + 8192, Recorder::DrawSnapshotBudget, stamp, buffer); + Require(cache.UnwrittenDrawSnapshot(address + 8192, Recorder::DrawSnapshotBudget, stamp) == nullptr, "unwritten snapshots grew past the byte budget"); + cache.NoteUnwrittenDrawSnapshot(address, Recorder::DrawSnapshotBudget, stamp, buffer); + Require(cache.UnwrittenDrawSnapshot(address, Recorder::DrawSnapshotBudget, stamp) == buffer, "replacing an unwritten snapshot kept the bytes of the old one"); + Require(cache.UnwrittenDrawSnapshot(address, Recorder::DrawSnapshotBudget, next) == nullptr && cache.UnwrittenDrawSnapshot(address, Recorder::DrawSnapshotBudget, stamp) == nullptr, "an unwritten snapshot outlived a newer stamp"); + for (std::size_t entry = 0; entry < Recorder::UnwrittenSnapshotEntries; ++entry) cache.NoteUnwrittenDrawSnapshot(address + entry * 16, 1, next, buffer); + cache.NoteUnwrittenDrawSnapshot(address + Recorder::UnwrittenSnapshotEntries * 16, 1, next, buffer); + Require(cache.UnwrittenDrawSnapshot(address, 1, next) == buffer && cache.UnwrittenDrawSnapshot(address + Recorder::UnwrittenSnapshotEntries * 16, 1, next) == nullptr, "unwritten snapshots grew past the entry cap"); +} + void drawInputReuseTests(const Device& device, Recorder& recorder) { using namespace AgcDriver::GuestMemory; using Use = Recorder::SnapshotUse; @@ -3894,6 +4107,165 @@ void writeBackPaddingTests(const Device& device, Recorder& recorder, bool watche std::cout << "write-back padding" << (watched ? " (watched)" : "") << ": ok\n"; } +void splitWriteBackTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + if (base.hostImportAlignment == 0 || 4096 % base.hostImportAlignment != 0) { + std::cout << "host imports unavailable: the split write-back not tested\n"; + return; + } + struct Case { + const char* name; + TextureTileMode tileMode; + std::uint32_t mipCount; + std::uint64_t offset; + std::uint64_t split; + bool gpu; + bool direct; + bool twoAllocations; + bool secondStore; + }; + const Case cases[] = { + {"64 KiB R_X 200x150, 3 mips, ranges split at 128 KiB", TextureTileMode::kR64KBX, 3, 0, 131072, true, true, false, false}, + {"64 KiB R_X 200x150, 3 mips, off a 64 KiB boundary, ranges split at 132 KiB", TextureTileMode::kR64KBX, 3, 4096, 135168, true, true, false, false}, + {"linear 200x150, ranges split at 64 KiB", TextureTileMode::kLinear, 1, 0, 65536, true, true, false, false}, + {"64 KiB R_X 200x150, 3 mips, second range of another access", TextureTileMode::kR64KBX, 3, 0, 131072, false, false, false, false}, + {"64 KiB R_X 200x150, 3 mips, ranges split inside a 64 KiB unit", TextureTileMode::kR64KBX, 3, 0, 135168, true, !UnitShadowEnabled(), false, false}, + {"64 KiB R_X 200x150, 3 mips, two adjacent allocations split at 128 KiB", TextureTileMode::kR64KBX, 3, 0, 131072, true, true, true, false}, + {"64 KiB R_X 200x150, 3 mips, ranges split at 128 KiB, a second store outside the write watch", TextureTileMode::kR64KBX, 3, 0, 131072, true, true, false, true}, + {"linear 200x150, ranges split at 64 KiB, a second store outside the write watch", TextureTileMode::kLinear, 1, 0, 65536, true, true, false, true}, + }; + SetHostImportWindow(64ull << 20u); + struct Window { + ~Window() { SetHostImportWindow(0); } + } window; + const auto pattern = [](std::size_t i) { return static_cast(i * 131u + 7u); }; + constexpr std::array red{255, 0, 0, 255}; + std::string failures; + for (const auto& test : cases) { + GuestTextureResource resource{}; + resource.width = 200; + resource.height = 150; + resource.mipCount = test.mipCount; + resource.tileMode = test.tileMode; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + const auto geometry = DescribeSurface(resource); + const auto surfaceBytes = static_cast(geometry.guestBytes); + Require(test.offset < test.split && test.split < test.offset + surfaceBytes, "the split test's ranges do not divide its surface"); + std::size_t texelCount = 0; + for (const auto& mip : geometry.mips) texelCount += static_cast(mip.width) * mip.height; + const auto bytes = (static_cast(test.offset) + surfaceBytes + 65535) / 65536 * 65536 + 65536; +#ifdef _WIN32 + void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* block = std::aligned_alloc(65536, bytes); +#endif + Require(block != nullptr, "cannot allocate the split write-back block"); + auto* memory = static_cast(block); + for (std::size_t i = 0; i < bytes; ++i) memory[i] = pattern(i); + const auto blockAddress = reinterpret_cast(block); + const auto address = blockAddress + test.offset; + { + GuestAllocations::Mutation mutation; + if (test.twoAllocations) { + mutation.Add(block, static_cast(test.split), true, true, true); + mutation.Add(memory + test.split, static_cast(bytes - test.split), true, true, test.gpu); + } else { + mutation.Add(block, bytes, true, true, true); + mutation.Protect(memory + test.split, static_cast(bytes - test.split), true, true, test.gpu, [] {}); + } + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + std::size_t bytes; + void* second; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + if (second != nullptr) mutation.Remove(second); + } + HostImportFor(context, address, bytes); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + } unregister{base, block, blockAddress, bytes, test.twoAllocations ? static_cast(memory + test.split) : nullptr}; + Require(HostImportFor(base, address, surfaceBytes) == nullptr, std::string(test.name) + ": one import covers both ranges"); + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + resource.baseAddress = address; + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, test.mipCount, 0, 1}; + const auto commands = recorder.Commands(); + recorder.Keep(image); + const VkClearColorValue value{{1.0f, 0.0f, 0.0f, 1.0f}}; + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &value, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + image->WriteBack(); + const bool direct = !image->GuestSnapshotValid(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + std::size_t texels = 0, garbage = 0, outside = 0; + for (std::size_t i = 0; i < bytes; i += 4) { + const bool inside = i >= test.offset && i < test.offset + surfaceBytes; + bool isTexel = inside, isPattern = true; + for (std::size_t c = 0; c < 4; ++c) { + isTexel = isTexel && memory[i + c] == red[c]; + isPattern = isPattern && memory[i + c] == pattern(i + c); + } + if (isTexel) ++texels; + else if (!isPattern) ++(inside ? garbage : outside); + } + if (direct != test.direct) failures += std::string(test.name) + ": the write-back took the " + (direct ? "GPU-direct" : "CPU") + " path\n"; + if (direct && !(HostImportCovers(base, blockAddress, static_cast(test.split)) && HostImportCovers(base, blockAddress + test.split, static_cast(bytes - test.split)))) failures += std::string(test.name) + ": a range has no import after the GPU-direct write-back\n"; + if (garbage != 0 || outside != 0 || texels != texelCount) failures += std::string(test.name) + ": the write-back stored " + std::to_string(texels) + " texels of " + std::to_string(texelCount) + " and changed " + std::to_string(garbage) + " words no texel holds (" + std::to_string(outside) + " past the surface)\n"; + else if (test.secondStore) { + const auto cpuPage = static_cast(test.split) + 16384; + Require(cpuPage + 4096 <= test.offset + surfaceBytes, std::string(test.name) + ": the CPU's page lies past the surface"); + if (AgcDriver::GuestMemory::Watched(address, surfaceBytes)) failures += std::string(test.name) + ": the surface is write-watched\n"; + if (!image->GuestSnapshotValid()) failures += std::string(test.name) + ": the first write-back left no saved bytes\n"; + for (std::size_t i = cpuPage; i < cpuPage + 4096; ++i) memory[i] = static_cast(~pattern(i)); + const auto again = recorder.Commands(); + const VkClearColorValue greenValue{{0.0f, 1.0f, 0.0f, 1.0f}}; + RecordMemoryBarrier(context, again, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdClearColorImage")(again, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &greenValue, 1, &range); + RecordMemoryBarrier(context, again, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + image->WriteBack(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + constexpr std::array green{0, 255, 0, 255}; + std::size_t kept = 0, greenBelow = 0, greenAbove = 0; + for (std::size_t i = cpuPage; i < cpuPage + 4096; ++i) kept += memory[i] == static_cast(~pattern(i)) ? 1 : 0; + for (std::size_t i = test.offset; i < test.offset + surfaceBytes; i += 4) { + if (i >= cpuPage && i < cpuPage + 4096) continue; + if (std::equal(green.begin(), green.end(), memory + i)) ++(i < test.split ? greenBelow : greenAbove); + } + if (kept != 4096) failures += std::string(test.name) + ": the second write-back overwrote " + std::to_string(4096 - kept) + " of the 4096 bytes the CPU wrote after the first\n"; + if (greenBelow == 0 || greenAbove == 0) failures += std::string(test.name) + ": the second write-back stored " + std::to_string(greenBelow) + " texels below the split and " + std::to_string(greenAbove) + " above it\n"; + } + } + recorder.Sync(); + } + if (!failures.empty()) throw std::runtime_error("split write-back:\n" + failures); + std::cout << "split write-back: ok\n"; +} + void singlePassTests(const Device& device, Recorder& recorder, bool watched) { const auto& base = device.GetContext(); if (base.hostImportAlignment == 0 || !base.singlePassStorage) { @@ -4469,6 +4841,135 @@ void unchangedCpuStampTests(const Device& device, Recorder& recorder) { std::cout << "unchanged CPU stamps: ok\n"; } +void keptUnitSettleTests(const Device& device, Recorder& recorder) { + Require(AgcDriver::GuestMemory::WriteWatched(), "(k) kept-unit settle tests require host write watching"); + const auto& base = device.GetContext(); + constexpr std::uint32_t width = 1024; + constexpr std::uint32_t height = 512; + constexpr std::size_t surfaceBytes = std::size_t{width} * height * 4; + constexpr std::size_t unitBytes = 65536; + constexpr std::array red{255, 0, 0, 255}; + constexpr std::array blue{0, 0, 255, 255}; + for (const bool direct : {false, true}) { + const std::string what = direct ? "GPU-mapped direct memory" : "watched memory"; + if (direct && base.hostImportAlignment == 0) { + std::cout << "host imports unavailable: units stored next to a unit kept for the CPU in " << what << " not tested\n"; + continue; + } + struct Backing { + Recorder& recorder; + bool direct; + void* block = nullptr; + std::int64_t physical = -1; + ~Backing() { + try { + recorder.Sync(); + } catch (const std::exception& error) { + std::cerr << "(k) the recorded work did not finish, the test memory stays mapped: " << error.what() << '\n'; + return; + } + if (direct) { + if (block != nullptr) sceKernelMunmap(block, surfaceBytes); + if (physical >= 0) sceKernelReleaseDirectMemory(physical, surfaceBytes); + return; + } + if (block == nullptr) return; + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + ReleaseWatched(block, surfaceBytes); + } + } backing{recorder, direct}; + if (direct) { + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, surfaceBytes, unitBytes, 0, &backing.physical) == 0, "(k) allocate the direct memory"); + Require(sceKernelMapDirectMemory(&backing.block, surfaceBytes, 0x33, 0, backing.physical, unitBytes) == 0, "(k) map the direct memory for the GPU"); + } else { + backing.block = AllocateWatched(surfaceBytes, unitBytes); + Require(backing.block != nullptr, "(k) no watched block although write watching is on"); + GuestAllocations::Mutation mutation; + mutation.Add(backing.block, surfaceBytes, true, true, true); + } + auto* memory = static_cast(backing.block); + std::memset(memory, 0x5a, surfaceBytes); + const auto address = reinterpret_cast(backing.block); + AgcDriver::GuestMemory::CollectWritesUncached(address, surfaceBytes); + TextureDetiler detiler(base); + auto context = base; + if (!direct) context.hostImportAlignment = 0; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = width; + resource.height = height; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + Require(DescribeSurface(resource).guestBytes == surfaceBytes, "(k) unexpected surface size"); + if (direct && !AgcDriver::GuestMemory::Watched(address, surfaceBytes)) { + std::cout << "GPU-mapped direct memory is not write-watched here: units stored next to a unit kept for the CPU in " << what << " not tested\n"; + continue; + } + Require(AgcDriver::GuestMemory::Watched(address, surfaceBytes), "(k) " + what + " is not watched"); + if (direct && HostImportFor(context, address, surfaceBytes) == nullptr) { + std::cout << "host import of the direct memory refused: units stored next to a unit kept for the CPU in " << what << " not tested\n"; + continue; + } + if (direct && !AgcDriver::GuestMemory::Watched(address, surfaceBytes)) { + std::cout << "the host import of the direct memory took it out of the write watch (its imports are compared): units stored next to a unit kept for the CPU in " << what << " not tested\n"; + continue; + } + Require(direct || HostImportFor(context, address, surfaceBytes) == nullptr, "(k) watched memory was imported although host imports are off for it"); + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const auto clear = [&](VkClearColorValue value) { + const auto commands = recorder.Commands(); + recorder.Keep(image); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &value, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + }; + const auto store = [&] { + image->WriteBack(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + }; + const auto unitHolds = [&](std::size_t unit, const std::array& texel) { + std::vector read(unitBytes); + AgcDriver::GuestMemory::Read(address + unit * unitBytes, read); + for (std::size_t i = 0; i < unitBytes; ++i) { + if (std::to_integer(read[i]) != texel[i % 4]) return false; + } + return true; + }; + clear({{1.0f, 0.0f, 0.0f, 1.0f}}); + memory[16] = 0x77; + AgcDriver::GuestMemory::CollectWritesUncached(address, surfaceBytes); + store(); + std::vector head(unitBytes); + AgcDriver::GuestMemory::Read(address, head); + Require(std::to_integer(head[16]) == 0x77, "(k) " + what + ": the CPU store into unit 0 was overwritten by results it was newer than"); + for (std::size_t unit = 1; unit < surfaceBytes / unitBytes; ++unit) Require(unitHolds(unit, red), "(k) " + what + ": the first write-back did not store unit " + std::to_string(unit)); + clear({{0.0f, 0.0f, 1.0f, 1.0f}}); + store(); + for (std::size_t unit = 1; unit < surfaceBytes / unitBytes; ++unit) { + Require(unitHolds(unit, blue), "(k) " + what + ": unit " + std::to_string(unit) + " kept the results of the earlier write-back; that write-back's own store was taken for a CPU write because a neighbouring unit was kept for the CPU"); + } + AgcDriver::GuestMemory::Read(address, head); + Require(std::to_integer(head[16]) == 0x77, "(k) " + what + ": the second write-back stored over unit 0, which the CPU wrote after that unit's generation"); + } + std::cout << "units stored next to a unit kept for the CPU in " << what << ": ok\n"; + } +} + void settledBaselineTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); #ifdef _WIN32 @@ -5686,6 +6187,58 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { std::lock_guard gpu(GpuMutex()); ShaderResources resources(context, compute); }, "a texture read", "a texture read"); + const auto multisampledWords = [&](std::uint32_t width, std::uint32_t sampleShift, std::uint64_t base) { + return std::vector{ + static_cast(base >> 8u), + static_cast((base >> 40u) & 0xffu) | (56u << 20u) | (((width - 1u) & 3u) << 30u), + ((width - 1u) >> 2u) | ((side - 1u) << 14u), + 0xfacu | (sampleShift << 16u) | (14u << 28u), + 0u, + sampleShift << 4u, + 0u, + 0u, + }; + }; + const auto sampledProgram = [&](ShaderRecompiler::DescriptorKind kind, std::vector words) { + ShaderRecompiler::RecompileResult result; + auto element = binding; + element.kind = kind; + if (kind == ShaderRecompiler::DescriptorKind::StorageImage) element.binding = ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding; + element.imageShape = ShaderRecompiler::DescriptorImageShape::Image2DMultisample; + element.guestDescriptor = std::move(words); + result.bindings.push_back(std::move(element)); + return result; + }; + const auto refusedSample = [&](ShaderRecompiler::DescriptorKind kind, std::vector words, const std::string& expected, const std::string& what) { + const auto refusedProgram = sampledProgram(kind, std::move(words)); + const CompiledShader refusedCompute{ShaderRecompiler::ShaderStage::Compute, &refusedProgram, 0}; + std::string message; + try { + std::lock_guard gpu(GpuMutex()); + ShaderResources resources(context, refusedCompute); + } catch (const std::exception& error) { + message = error.what(); + } + Require(message.find(expected) != std::string::npos, what + " did not throw: " + message); + }; + { + const auto sampled = sampledProgram(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 2u, address)); + const CompiledShader sampledCompute{ShaderRecompiler::ShaderStage::Compute, &sampled, 0}; + std::lock_guard gpu(GpuMutex()); + ShaderResources first(context, sampledCompute); + ShaderResources second(context, sampledCompute); + Require(first.SampledTextures().size() == 1 && first.SampledTextures().front() != nullptr, "the multisampled target was not bound as a sampled texture"); + Require(first.SampledTextures().front()->Layout() == VK_IMAGE_LAYOUT_GENERAL && first.SampledTextures().front()->ViewFormat() == target.format, "the sampled multisampled target is not a view of the target's image in its layout"); + Require(second.SampledTextures().front() == first.SampledTextures().front(), "a second lookup of the multisampled target made another texture"); + Require(first.MultisampleSources() == std::vector{address} && !first.Reusable(), "resources sampling a multisampled target are kept for reuse without the barrier their lookup records"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + refusedSample(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 3u, address), "as a 64x64 texture of 8 samples", "sampling a 4x target as an 8x texture"); + refusedSample(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side / 2u, 2u, address), "as a 32x64 texture of 4 samples", "sampling a 4x target as a narrower texture"); + refusedSample(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 2u, address + 4u * 65536u), "which no draw rendered to and whose tile mode 0 has no modeled multisampled color footprint", "sampling a linear multisampled texture no draw rendered to"); + refusedSample(ShaderRecompiler::DescriptorKind::StorageImage, multisampledWords(side, 2u, address), "storage image access to a multisampled texture", "storage access to a multisampled texture"); refused([&] { AgcDriver::GuestMemory::Read(address + 3u * 65536u + 256u, texels); }, "a CPU read", "a CPU read in the padding of a later sample plane"); AgcDriver::GuestMemory::Read(address + 4u * 65536u, texels); clearTargets(); @@ -5735,12 +6288,19 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { target.surfaceExtent = target.extent; target.samples = 4; VkImageView previous = VK_NULL_HANDLE; + std::shared_ptr previousTexture; for (const bool removeAllocation : {false, true}) { VkImageView view = VK_NULL_HANDLE; + std::shared_ptr texture; { + const auto sampled = sampledProgram(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 2u, address)); + const CompiledShader sampledCompute{ShaderRecompiler::ShaderStage::Compute, &sampled, 0}; std::lock_guard gpu(GpuMutex()); view = MultisampleTargetView(context, target); Require(view != VK_NULL_HANDLE && view != previous, "a remapped allocation reused its retired multisampled image"); + ShaderResources resources(context, sampledCompute); + texture = resources.SampledTextures().front(); + Require(texture != nullptr && texture != previousTexture, "a remapped allocation sampled its retired multisampled image"); recorder.Submit(); device.WaitQueue(); recorder.Sync(); @@ -5767,12 +6327,114 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { } RequireNoMultisampleTarget(address, bytes, "a texture read"); AgcDriver::GuestMemory::Read(address + 256u, texels); + refusedSample(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 2u, address), "which no draw rendered to", "sampling a multisampled target whose allocation was unmapped"); previous = view; + previousTexture = texture; } clearTargets(); std::cout << "Multisampled target guest access tests passed\n"; } +void fmaskDecompressMsaaTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0 || base.hostImportAlignment == 0) { + std::cout << "4x color targets or host imports unavailable: FMASK decompress not tested\n"; + return; + } + constexpr std::uint32_t side = 128; + constexpr std::size_t targetBytes = 65536u; + constexpr std::size_t cmaskBytes = 4096u; + constexpr std::size_t destBytes = targetBytes; + constexpr std::size_t bytes = 5u * 65536u + cmaskBytes + 2u * destBytes; +#ifdef _WIN32 + void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* block = std::aligned_alloc(65536, bytes); +#endif + Require(block != nullptr, "cannot allocate the FMASK decompress block"); + std::memset(block, 0, bytes); + const auto address = reinterpret_cast(block); + const auto cmaskAddress = address + 5u * 65536u; + const auto destAddress = cmaskAddress + cmaskBytes; + const auto destAddress2 = destAddress + destBytes; + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + ~Unregister() { + ClearMultisampleTargets(context.device); + ClearCachedTextures(context.device); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + } unregister{base, block}; + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + ColorTarget source{}; + source.address = address; + source.surfaceAddress = address; + source.extent = {side, side}; + source.surfaceExtent = source.extent; + source.format = VK_FORMAT_R8G8B8A8_UNORM; + source.elementBytes = 4; + source.bytes = targetBytes; + source.samples = 4; + source.cmaskAddress = cmaskAddress; + source.cmaskBytes = cmaskBytes; + source.cmaskFastClear = true; + source.clearWords = {0x11223344u, 0}; + ColorTarget destination{}; + destination.address = destAddress; + destination.surfaceAddress = destAddress; + destination.extent = {side, side}; + destination.surfaceExtent = destination.extent; + destination.format = VK_FORMAT_R8G8B8A8_UNORM; + destination.elementBytes = 4; + destination.bytes = destBytes; + destination.samples = 1; + destination.tileMode = ColorTileMode::RenderTarget; + { + std::lock_guard gpu(GpuMutex()); + RunColorMetadataPass(context, {ColorMetadataPass::Mode::FmaskDecompress, {source}}); + auto recolored = source; + recolored.clearWords = {0x55667788u, 0}; + RunColorMetadataPass(context, {ColorMetadataPass::Mode::Resolve, {destination}, recolored}); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + std::vector resolved(destBytes / 4); + AgcDriver::GuestMemory::Read(destAddress, std::as_writable_bytes(std::span(resolved))); + Require(std::ranges::all_of(resolved, [](auto word) { return word == 0x11223344u; }), "FMASK decompress did not materialize the seeded clear to the host multisampled image"); + source.clearWords = {0x55667788u, 0}; + ColorTarget destination2 = destination; + destination2.address = destAddress2; + destination2.surfaceAddress = destAddress2; + { + std::lock_guard gpu(GpuMutex()); + RunColorMetadataPass(context, {ColorMetadataPass::Mode::FmaskDecompress, {source}}); + RunColorMetadataPass(context, {ColorMetadataPass::Mode::Resolve, {destination2}, source}); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + std::vector resolved2(destBytes / 4); + AgcDriver::GuestMemory::Read(destAddress2, std::as_writable_bytes(std::span(resolved2))); + Require(std::ranges::all_of(resolved2, [](auto word) { return word == 0x11223344u; }), "subsequent FMASK decompress re-cleared the multisampled target"); + std::cout << "FMASK decompress MSAA clear and resolve tests passed\n"; +} + void depthSurfaceProofTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); if (!device.NullDescriptors()) { @@ -6483,6 +7145,134 @@ void pipelineCacheTests(const Device& device, bool benchmark) { std::cout << "Pipeline cache key and retained lifetime tests passed\n"; } +int pipelineCacheFileTests(const Device& device) { + const auto& context = device.GetContext(); + const auto directory = std::filesystem::temp_directory_path() / ("aps5-pipeline-cache-file-test-" + std::to_string(std::random_device{}())); + struct RemoveDirectory { + std::filesystem::path directory; + ~RemoveDirectory() { + std::error_code error; + std::filesystem::remove_all(directory, error); + } + } removeDirectory{directory}; +#ifdef _WIN32 + _putenv_s("ANYPS5_NO_SHADER_CACHE", "0"); + _putenv_s("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str()); +#else + setenv("ANYPS5_NO_SHADER_CACHE", "0", 1); + setenv("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str(), 1); +#endif + const auto properties = device.Properties(); + struct Objects { + const Context& context; + VkDescriptorSetLayout setLayout = VK_NULL_HANDLE; + VkPipelineLayout layout = VK_NULL_HANDLE; + std::array modules{}; + ~Objects() { + for (const auto module : modules) { + if (module != VK_NULL_HANDLE) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + } + if (layout != VK_NULL_HANDLE) context.Function("vkDestroyPipelineLayout")(context.device, layout, nullptr); + if (setLayout != VK_NULL_HANDLE) context.Function("vkDestroyDescriptorSetLayout")(context.device, setLayout, nullptr); + } + } objects{context}; + const std::array bindings{VkDescriptorSetLayoutBinding{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, VkDescriptorSetLayoutBinding{1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}}; + VkDescriptorSetLayoutCreateInfo setLayoutInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + setLayoutInfo.bindingCount = static_cast(bindings.size()); + setLayoutInfo.pBindings = bindings.data(); + Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &setLayoutInfo, nullptr, &objects.setLayout), "vkCreateDescriptorSetLayout"); + const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(float) * 2}; + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = 1; + layoutInfo.pSetLayouts = &objects.setLayout; + layoutInfo.pushConstantRangeCount = 1; + layoutInfo.pPushConstantRanges = &push; + Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &objects.layout), "vkCreatePipelineLayout"); + const std::array, 2> codes{std::span(SAMPLE_LOD_SPV), std::span(SAMPLE_ARRAY_SPV)}; + for (std::size_t index = 0; index < codes.size(); ++index) { + VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + moduleInfo.codeSize = codes[index].size_bytes(); + moduleInfo.pCode = codes[index].data(); + Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &objects.modules[index]), "vkCreateShaderModule"); + } + const auto build = [&](VkPipelineCache cache, std::size_t index) { + VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + info.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_COMPUTE_BIT, objects.modules[index], "main", nullptr}; + info.layout = objects.layout; + VkPipeline pipeline = VK_NULL_HANDLE; + Check(context.Function("vkCreateComputePipelines")(context.device, cache, 1, &info, nullptr, &pipeline), "vkCreateComputePipelines"); + context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + }; + const auto bytes = [&](VkPipelineCache cache) { + std::size_t size = 0; + Check(context.Function("vkGetPipelineCacheData")(context.device, cache, &size, nullptr), "vkGetPipelineCacheData"); + return size; + }; + const auto holds = [&](VkPipelineCache cache, std::size_t index) { + const auto before = bytes(cache); + build(cache, index); + return bytes(cache) == before; + }; + const auto cacheFile = [&] { + std::vector found; + for (const auto& entry : std::filesystem::directory_iterator(directory)) found.push_back(entry.path()); + Require(found.size() == 1 && found.front().filename().string().starts_with("vk-pipelines-"), "the pipeline cache directory does not hold exactly one cache file"); + return found.front(); + }; + const auto contents = [](const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + return std::vector(std::istreambuf_iterator(stream), std::istreambuf_iterator()); + }; + { + VkPipelineCacheCreateInfo info{VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO}; + VkPipelineCache probe = VK_NULL_HANDLE; + Check(context.Function("vkCreatePipelineCache")(context.device, &info, nullptr, &probe), "vkCreatePipelineCache"); + const bool stored = !holds(probe, 0); + context.Function("vkDestroyPipelineCache")(context.device, probe, nullptr); + if (!stored) { + std::cout << "skipped, the device does not keep pipelines in a pipeline cache\n"; + return 77; + } + } + for (const bool firstSavesFirst : {true, false}) { + std::filesystem::remove_all(directory); + auto first = std::make_unique(context, properties); + auto second = std::make_unique(context, properties); + build(first->Handle(), 0); + build(second->Handle(), 1); + if (firstSavesFirst) { + first.reset(); + second.reset(); + } else { + second.reset(); + first.reset(); + } + PipelineCache third(context, properties); + Require(holds(third.Handle(), 0), firstSavesFirst ? "the cache file lost the pipeline of the object that saved first" : "the cache file lost the pipeline of the object that saved last"); + Require(holds(third.Handle(), 1), firstSavesFirst ? "the cache file lost the pipeline of the object that saved last" : "the cache file lost the pipeline of the object that saved first"); + } + const auto path = cacheFile(); + const auto stored = contents(path); + { + PipelineCache idle(context, properties); + } + Require(contents(path) == stored, "an object that added no pipelines rewrote the cache file"); + { + std::ofstream corrupt(path, std::ios::binary | std::ios::trunc); + corrupt << "not a pipeline cache"; + } + { + PipelineCache replacing(context, properties); + build(replacing.Handle(), 1); + } + { + PipelineCache reloaded(context, properties); + Require(holds(reloaded.Handle(), 1), "an invalid cache file was not replaced by the saving object's pipelines"); + } + std::cout << "Pipeline cache file merge tests passed\n"; + return 0; +} + void descriptorPoolTests(const Device& device) { DescriptorCache cache(device.GetContext()); const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 32, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}; @@ -7049,6 +7839,10 @@ int main(int argc, char** argv) { return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--pipeline-cache-file-only") { + return pipelineCacheFileTests(device); + } + if (argc == 2 && std::string_view(argv[1]) == "--descriptor-pool-only") { descriptorPoolTests(device); return 0; @@ -7094,6 +7888,10 @@ int main(int argc, char** argv) { unchangedCpuStampTests(device, recorder); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--kept-unit-settle-only") { + keptUnitSettleTests(device, recorder); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--integer-clear-only") { integerClearTests(device); return 0; @@ -7148,6 +7946,14 @@ int main(int argc, char** argv) { unitShadowTests(device, recorder); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--split-writeback-only") { + if (device.GetContext().hostImportAlignment == 0) { + std::cout << "skipped, host imports are unavailable\n"; + return 77; + } + splitWriteBackTests(device, recorder); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--cmask-only") { cmaskPassTests(device, recorder); std::cout << "CMASK copied clear, write-back and failure tests passed\n"; @@ -7182,6 +7988,7 @@ int main(int argc, char** argv) { } if (argc == 2 && std::string_view(argv[1]) == "--multisample-only") { multisampleGuestAccessTests(device, recorder); + fmaskDecompressMsaaTests(device, recorder); return 0; } if (argc == 2 && std::string_view(argv[1]) == "--draw-descriptors-only") { @@ -7189,11 +7996,22 @@ int main(int argc, char** argv) { std::cout << "Draw descriptor writes and snapshot overrides passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--space-pending-writes-only") { + overlappedRangeTests(recorder); + spaceWriteSyncTests(device, recorder); + std::cout << "Address space pending write tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--draw-snapshot-cap-only") { drawSnapshotCapTests(device, recorder); + unwrittenSnapshotTests(device); std::cout << "Draw snapshot cap tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--draw-snapshot-epoch-only") { + drawSnapshotEpochTests(device); + return 0; + } if (argc == 2 && (std::string_view(argv[1]) == "--vertex-target-range-copied" || std::string_view(argv[1]) == "--vertex-target-range-imported")) { return vertexTargetRangeTests(device, recorder, std::string_view(argv[1]) == "--vertex-target-range-imported") ? 0 : 77; @@ -7207,6 +8025,8 @@ int main(int argc, char** argv) { #endif readTrackingTests(device, recorder); readListTests(device, recorder); + overlappedRangeTests(recorder); + spaceWriteSyncTests(device, recorder); writeSettledTests(device, recorder); completionCountTests(device, recorder); afterRecordedWorkTests(device, recorder); @@ -7224,12 +8044,14 @@ int main(int argc, char** argv) { readWrittenStagingTests(device, recorder); depthSurfaceProofTests(device, recorder); multisampleGuestAccessTests(device, recorder); + fmaskDecompressMsaaTests(device, recorder); misalignedSnapshotTests(device, recorder); misalignedRegionTests(device, recorder); drawSnapshotReuseTests(device, recorder); drawDescriptorWriteTests(device, recorder); drawSnapshotCapTests(device, recorder); drawSnapshotEvictionTests(device); + unwrittenSnapshotTests(device); drawInputReuseTests(device, recorder); drawInputInPlaceTests(device, recorder); RunResidentPresentTests(device.GetContext()); @@ -7251,9 +8073,11 @@ int main(int argc, char** argv) { static_cast(writtenBufferOrderTests(device, recorder)); writeBackPaddingTests(device, recorder, false); writeBackPaddingTests(device, recorder, true); + splitWriteBackTests(device, recorder); singlePassTests(device, recorder, false); singlePassTests(device, recorder, true); unchangedCpuStampTests(device, recorder); + keptUnitSettleTests(device, recorder); settledBaselineTests(device, recorder); importWatchTests(device); staleGenerationTests(device, recorder); @@ -7274,6 +8098,7 @@ int main(int argc, char** argv) { gpu.unlock(); hostImportUnmapTests(device, recorder); drawSnapshotPatchTests(device); + drawSnapshotEpochTests(device); gpu.lock(); recorder.Activate(); std::cout << "Recorder read tracking and label tests passed\n"; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index aad1cc12cd..6d79855198 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -1,5 +1,6 @@ #include "ShaderDiskCache.hpp" #include "CacheKey.hpp" +#include "ShaderCachePack.hpp" #include "VertexInputSpecialization.hpp" #include "SpirvBackend/SpirvSpecialization.hpp" #include "Optimization/ResourceMaterializer.hpp" @@ -81,6 +82,9 @@ void requireSameArtifact(const CompiledShaderArtifact& left, const CompiledShade 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"); + require(left.stencilReferences == right.stencilReferences, prefix + "stencil reference set differs"); + require(left.stencilReplaySafe == right.stencilReplaySafe, prefix + "stencil replay proof differs"); + require(left.stencilReferenceDword == right.stencilReferenceDword, prefix + "stencil replay push offset differs"); require(left.fragmentParameters.size() == right.fragmentParameters.size(), prefix + "fragment parameter count differs"); for (std::size_t i = 0; i < left.fragmentParameters.size(); ++i) { const auto& a = left.fragmentParameters[i]; @@ -181,6 +185,9 @@ RecompileResult sampleResult() { result.instanceOffsetConflict = true; result.parameterExports = {0, 3, 7}; result.fragmentParameters = {{0, 1, true, false, true}, {2, 3, false, true}}; + result.stencilReferences = {0u, 17u, 255u}; + result.stencilReplaySafe = true; + result.stencilReferenceDword = 23u; result.poisonedSrtReads = 3; result.barycentricEmulation = {true, false, true}; result.variantId = 99; @@ -235,7 +242,9 @@ CompiledVariant sampleVariant() { image.constantSwizzle = true; image.constantSwizzleCompatible = false; image.flatVolumeCompatible = false; + image.singleMipPlaneBiasCompatible = true; image.flatLineCompatible = false; + image.flatVolumeLineCompatible = false; image.foldTexelOffsetsOnly = true; image.byElements = 4; image.byComponents = 1; @@ -270,6 +279,7 @@ CompiledVariant sampleVariant() { info.bindings.pushDataStartDword = 2; info.bindings.memoryOffsetDword = 1; info.bindings.memoryOffsetCount = 5; + info.bindings.stencilReferenceDword = 23u; info.bindings.userDataRegisters = {4, 5, 9}; info.bindings.descriptors = {{DescriptorBindingKind::Buffers, {0, 1}}, {DescriptorBindingKind::FlattenedSrt, {}}}; variant.bindings.layout = info.bindings; @@ -371,12 +381,183 @@ void verifyEntryRoundTrip() { auto previousVersion = file; previousVersion[4] = static_cast(ShaderDiskCache::FormatVersion - 1u); require(ShaderDiskCache::DecodeEntry(previousVersion, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, "an entry with the previous format version loads"); + auto priorStencilVersion = file; + priorStencilVersion[4] = std::byte{32}; + require(ShaderDiskCache::DecodeEntry(priorStencilVersion, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, "an artifact with the prior stencil format loads"); + auto upstreamVersion = file; + upstreamVersion[4] = std::byte{33}; + require(ShaderDiskCache::DecodeEntry(upstreamVersion, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, "a stencil artifact with the upstream resource plan format loads"); auto incompatible = variant; incompatible.artifact.runtimeAbiVersion = RuntimeAbi::Version + 1u; const auto incompatibleFile = ShaderDiskCache::EncodeEntry(key, incompatible); require(ShaderDiskCache::DecodeEntry(incompatibleFile, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, "an entry with an incompatible runtime ABI loads"); } +std::vector packBytes(const std::vector, std::vector>>& entries) { + using namespace ShaderRecompiler::ShaderDiskCache; + std::vector records; + std::uint64_t offset = sizeof(PackHeader) + entries.size() * sizeof(PackRecord); + for (const auto& [key, entry] : entries) { + records.push_back({PackKeyHash(key), offset, entry.size()}); + offset += entry.size(); + } + std::sort(records.begin(), records.end(), [](const PackRecord& left, const PackRecord& right) { return left.hash < right.hash; }); + PackHeader header; + header.sourceVersion = SourceVersion(); + header.fileBytes = offset; + header.entryCount = entries.size(); + header.indexHash = ShaderRecompiler::HashBytes(std::as_bytes(std::span(records))); + std::vector bytes(offset); + std::memcpy(bytes.data(), &header, sizeof(header)); + std::memcpy(bytes.data() + sizeof(header), records.data(), records.size() * sizeof(PackRecord)); + std::size_t at = sizeof(header) + records.size() * sizeof(PackRecord); + for (const auto& [key, entry] : entries) { + std::memcpy(bytes.data() + at, entry.data(), entry.size()); + at += entry.size(); + } + return bytes; +} + +void refreshIndexHash(std::vector& pack, std::size_t entries) { + using namespace ShaderRecompiler::ShaderDiskCache; + const auto index = std::span(pack).subspan(sizeof(PackHeader), entries * sizeof(PackRecord)); + const auto hash = ShaderRecompiler::HashBytes(index); + std::memcpy(pack.data() + offsetof(PackHeader, indexHash), &hash, sizeof(hash)); +} + +void verifyPrebuiltPack(const std::filesystem::path& directory) { + using namespace ShaderRecompiler::ShaderDiskCache; + SampleRequest sample; + const auto key = sample.Key(); + const auto variant = sampleVariant(); + const auto file = EncodeEntry(key, variant); + auto otherKey = key; + otherKey.back() ^= std::byte{1}; + const auto otherFile = EncodeEntry(otherKey, variant); + std::filesystem::create_directories(directory); + const auto writePack = [&](const char* name, const std::vector& bytes) { + const auto path = directory / name; + require(WriteFileAtomically(path, bytes), "cannot write the test pack"); + return path; + }; + PackLoadTimings timings; + CompiledVariant decoded; + { + CachePack pack(writePack("valid.pack", packBytes({{key, file}, {otherKey, otherFile}}))); + require(pack.Valid(), "a valid pack is rejected: " + pack.Error()); + require(pack.Count() == 2, "the pack does not hold both entries"); + require(pack.Load(key, decoded, timings) == LoadStatus::Loaded, "a pack entry does not load"); + requireSameVariant(variant, decoded, "pack round trip"); + auto missingKey = key; + missingKey.front() ^= std::byte{1}; + require(pack.Load(missingKey, decoded, timings) == LoadStatus::Absent, "an absent key loads from the pack"); + } + { + std::vector, std::vector>> mismatched; + mismatched.emplace_back(otherKey, file); + CachePack pack(writePack("mismatch.pack", packBytes(mismatched))); + require(pack.Valid(), "the key mismatch pack is rejected: " + pack.Error()); + require(pack.Load(otherKey, decoded, timings) != LoadStatus::Loaded, "an entry stored under another key loads"); + } + { + auto bytes = packBytes({{key, file}}); + const auto truncated = static_cast(file.size() / 2); + std::memcpy(bytes.data() + sizeof(PackHeader) + offsetof(PackRecord, size), &truncated, sizeof(truncated)); + refreshIndexHash(bytes, 1); + CachePack pack(writePack("truncated.pack", bytes)); + require(pack.Valid(), "the truncated-entry pack is rejected: " + pack.Error()); + require(pack.Load(key, decoded, timings) == LoadStatus::Rejected, "a truncated pack entry loads"); + } + { + auto bytes = packBytes({{key, file}}); + bytes[sizeof(PackHeader)] ^= std::byte{0x10}; + CachePack pack(writePack("corrupt-index.pack", bytes)); + require(!pack.Valid(), "a pack with a damaged index is accepted"); + } + { + auto bytes = packBytes({{key, file}}); + const auto outside = static_cast(bytes.size()) + 16; + std::memcpy(bytes.data() + sizeof(PackHeader) + offsetof(PackRecord, offset), &outside, sizeof(outside)); + refreshIndexHash(bytes, 1); + CachePack pack(writePack("corrupt-record.pack", bytes)); + require(!pack.Valid(), "a pack with an out-of-range record is accepted"); + } + { + auto bytes = packBytes({{key, file}}); + const auto wrong = SourceVersion() + 1; + std::memcpy(bytes.data() + offsetof(PackHeader, sourceVersion), &wrong, sizeof(wrong)); + CachePack pack(writePack("wrong-version.pack", bytes)); + require(!pack.Valid(), "a pack from another compiler source version is accepted"); + } + { + auto bytes = packBytes({{key, file}}); + bytes.front() ^= std::byte{0x10}; + CachePack pack(writePack("unknown-format.pack", bytes)); + require(!pack.Valid(), "a pack with an unknown format is accepted"); + } +} + +SampleRequest prebuiltSample() { + SampleRequest sample; + sample.code[3] ^= 0x5a5a5a5au; + return sample; +} + +int runPrebuiltLoadingProcess() { + using namespace ShaderRecompiler::ShaderDiskCache; + SampleRequest sample = prebuiltSample(); + const auto key = sample.Key(); + const auto expected = sampleVariant(); + CompiledVariant loaded; + require(Load(key, loaded), "the prebuilt pack entry does not load"); + requireSameVariant(expected, loaded, "prebuilt load"); + require(Totals().prebuiltHits == 1, "the prebuilt load was not counted"); + const auto afterPack = Totals(); + require(afterPack.prebuiltBytesRead > 0 && afterPack.prebuiltLoadNanoseconds > 0, "the prebuilt load was not timed"); + require(afterPack.hits == 0 && afterPack.loadFailures == 0 && afterPack.bytesRead == 0 && afterPack.loadNanoseconds == 0, "the prebuilt load reached the writable totals"); + auto looseKey = key; + looseKey.back() ^= std::byte{1}; + CompiledVariant loose; + require(Load(looseKey, loose), "the loose prebuilt entry does not load"); + requireSameVariant(expected, loose, "loose prebuilt load"); + require(Totals().prebuiltHits == 2, "the loose prebuilt load was not counted"); + require(Totals().hits == 0 && Totals().loadFailures == 0 && Totals().bytesRead == 0, "the loose prebuilt load reached the writable totals"); + auto corruptKey = looseKey; + corruptKey.front() ^= std::byte{1}; + CompiledVariant corrupt; + require(!Load(corruptKey, corrupt), "a corrupt loose prebuilt entry loads"); + require(Totals().prebuiltRejected >= 1, "the corrupt loose prebuilt entry was not counted as rejected"); + require(Totals().loadFailures == 0, "the corrupt loose prebuilt entry reached the writable failures"); + auto otherKey = looseKey; + otherKey.front() ^= std::byte{1}; + CompiledVariant other; + require(!Load(otherKey, other), "a key absent from prebuilt storage loads"); + Store(otherKey, std::make_shared(sampleVariant())); + Flush(); + require(Load(otherKey, other), "the writable cache fall-through does not load"); + std::cout << "prebuilt process loaded from the pack and fell through to the writable cache\n"; + return 0; +} + +void verifyPrebuiltAcrossProcesses(const char* self, const std::filesystem::path& directory) { + using namespace ShaderRecompiler::ShaderDiskCache; + SampleRequest sample = prebuiltSample(); + const auto prebuilt = directory / "prebuilt"; + std::filesystem::create_directories(prebuilt); + const auto bytes = packBytes({{sample.Key(), EncodeEntry(sample.Key(), sampleVariant())}}); + require(WriteFileAtomically(prebuilt / "test.pack", bytes), "cannot write the prebuilt pack"); + auto looseKey = sample.Key(); + looseKey.back() ^= std::byte{1}; + require(WriteFileAtomically(prebuilt / EntryName(looseKey), EncodeEntry(looseKey, sampleVariant())), "cannot write the loose prebuilt entry"); + auto corruptKey = looseKey; + corruptKey.front() ^= std::byte{1}; + auto corruptEntry = EncodeEntry(corruptKey, sampleVariant()); + corruptEntry.resize(corruptEntry.size() / 2); + require(WriteFileAtomically(prebuilt / EntryName(corruptKey), corruptEntry), "cannot write the corrupt loose prebuilt entry"); + const std::string command = "\"" + std::string(self) + "\" --load-prebuilt"; + require(std::system(command.c_str()) == 0, "the prebuilt loading process failed"); +} + void verifyArtifactStorageIsolation() { SampleRequest sample; const auto key = sample.Key(); @@ -1289,10 +1470,10 @@ std::vector planFixtures() { IrResourcePlan handBuiltPlan() { IrResourcePlan plan; + plan.valueStorage.Reserve(64); std::uint32_t ids = 0; const auto make = [&](IrOpcode opcode, IrType type) -> IrValue& { - plan.valueStorage.push_back(std::make_unique(opcode, type, ids++)); - return *plan.valueStorage.back(); + return *plan.valueStorage.Create(opcode, type, ids++); }; const auto constant = [&](std::uint32_t value) -> IrValue& { auto& immediate = make(IrOpcode::Void, IrType::U32); @@ -1418,14 +1599,14 @@ std::vector encodedPlan(const IrResourcePlan& plan) { } bool samePlan(const IrResourcePlan& left, const IrResourcePlan& right) { - if (left.valueStorage.size() != right.valueStorage.size() || left.blockStorage.size() != right.blockStorage.size()) return false; + if (left.valueStorage.Size() != right.valueStorage.Size() || left.blockStorage.size() != right.blockStorage.size()) return false; std::map leftValues; std::map rightValues; std::map leftBlocks; std::map rightBlocks; - for (std::size_t index = 0; index < left.valueStorage.size(); ++index) { - leftValues.emplace(left.valueStorage[index].get(), index); - rightValues.emplace(right.valueStorage[index].get(), index); + for (std::size_t index = 0; index < left.valueStorage.Size(); ++index) { + leftValues.emplace(&left.valueStorage[index], index); + rightValues.emplace(&right.valueStorage[index], index); } for (std::size_t index = 0; index < left.blockStorage.size(); ++index) { leftBlocks.emplace(left.blockStorage[index].get(), index); @@ -1445,9 +1626,9 @@ bool samePlan(const IrResourcePlan& left, const IrResourcePlan& right) { for (std::size_t index = 0; index < left.blockStorage.size(); ++index) { if (left.blockStorage[index]->Id() != right.blockStorage[index]->Id()) return false; } - for (std::size_t index = 0; index < left.valueStorage.size(); ++index) { - const auto& a = *left.valueStorage[index]; - const auto& b = *right.valueStorage[index]; + for (std::size_t index = 0; index < left.valueStorage.Size(); ++index) { + const auto& a = left.valueStorage[index]; + const auto& b = right.valueStorage[index]; if (a.Opcode() != b.Opcode() || a.Type() != b.Type() || a.Id() != b.Id() || a.Flags() != b.Flags() || !(a.Register() == b.Register()) || a.HasImmediate() != b.HasImmediate() || (a.HasImmediate() && a.ImmediateU64() != b.ImmediateU64())) return false; if (a.ArgumentCount() != b.ArgumentCount() || a.IsPhi() != b.IsPhi() || a.PhiBlockCount() != b.PhiBlockCount()) return false; for (std::size_t operand = 0; operand < a.ArgumentCount(); ++operand) { @@ -1499,7 +1680,7 @@ MaterializedPlan materializePlan(const IrResourcePlan& plan, SrtRuntime runtime) } catch (const std::exception& error) { outcome.error = error.what(); } - outcome.leaf = static_cast(std::find_if(plan.valueStorage.begin(), plan.valueStorage.end(), [&](const auto& value) { return value.get() == outcome.trace.leaf; }) - plan.valueStorage.begin()); + outcome.leaf = static_cast(std::find_if(plan.valueStorage.begin(), plan.valueStorage.end(), [&](const auto& value) { return &value == outcome.trace.leaf; }) - plan.valueStorage.begin()); return outcome; } @@ -1632,15 +1813,11 @@ void verifyMalformedPlans() { std::vector bytes; require(!ShaderDiskCache::EncodePlan(foreign, bytes), "a plan reading a value it does not own was encoded"); auto unblocked = handBuiltPlan(); - auto& phi = *unblocked.valueStorage.emplace_back(std::make_unique(IrOpcode::Phi, IrType::U32, 100u)); - phi.AddArgument(unblocked.valueStorage[0].get()); + auto& phi = *unblocked.valueStorage.Create(IrOpcode::Phi, IrType::U32, 100u); + phi.AddArgument(&unblocked.valueStorage[0]); unblocked.srtReads[0].value = φ bytes.clear(); require(!ShaderDiskCache::EncodePlan(unblocked, bytes), "a phi without predecessor blocks was encoded"); - auto emptied = handBuiltPlan(); - emptied.valueStorage.push_back(nullptr); - bytes.clear(); - require(!ShaderDiskCache::EncodePlan(emptied, bytes), "a plan with an empty value slot was encoded"); } void verifyPlanRoundTrip() { @@ -1648,7 +1825,7 @@ void verifyPlanRoundTrip() { const auto handBytes = encodedPlan(handBuilt); IrResourcePlan handDecoded; require(ShaderDiskCache::DecodePlan(handBytes, handDecoded), "the hand-built plan does not decode"); - require(encodedPlan(handDecoded) == handBytes && handDecoded.valueStorage.size() == handBuilt.valueStorage.size() && handDecoded.blockStorage.size() == handBuilt.blockStorage.size(), "the hand-built plan changed in a round trip"); + require(encodedPlan(handDecoded) == handBytes && handDecoded.valueStorage.Size() == handBuilt.valueStorage.Size() && handDecoded.blockStorage.size() == handBuilt.blockStorage.size(), "the hand-built plan changed in a round trip"); const auto& phi = *handDecoded.descriptorSources[0].dwords[2]; require(phi.IsPhi() && phi.PhiBlockCount() == 2u && phi.PhiBlock(0)->Id() == 3u && phi.PhiBlock(1)->Id() == 7u && phi.Argument(1)->ImmediateU32() == 0u, "the decoded phi lost its blocks or operands"); require(handDecoded.uniformFill.values[0] == handDecoded.uniformFill.values[1] && handDecoded.uniformFill.values[0] != nullptr && handDecoded.uniformFill.values[2] == nullptr, "the decoded uniform fill lost its shared or null values"); @@ -1674,13 +1851,13 @@ void verifyPlanRoundTrip() { require(ShaderDiskCache::DecodePlan(bytes, decoded), fixture.name + ": the plan does not decode"); require(encodedPlan(decoded) == bytes, fixture.name + ": the plan changed in a round trip"); require(samePlan(*plan, decoded), fixture.name + ": the decoded plan differs from the plan in a field or a value"); - require(decoded.valueStorage.size() == plan->valueStorage.size() && decoded.blockStorage.size() == plan->blockStorage.size(), fixture.name + ": the decoded plan holds another graph"); + require(decoded.valueStorage.Size() == plan->valueStorage.Size() && decoded.blockStorage.size() == plan->blockStorage.size(), fixture.name + ": the decoded plan holds another graph"); const auto succeeded = verifyRoundTripRuntimes(*plan, decoded, fixture, 0x5eedu + static_cast(index), fixture.name); require(succeeded != 0u, fixture.name + ": no runtime materialized"); std::vector key; ShaderDiskCache::BuildPlanKey(request, 32u, key); if (index % 4u == 0u) verifyPlanEntryDamage(key, *plan, fixture.name); - std::cout << "plan round trip: " << fixture.name << ", " << plan->valueStorage.size() << " values, " << ShaderDiskCache::EncodePlanEntry(key, *plan).size() << " bytes per entry, " << succeeded << " of 1000 runtimes materialized\n"; + std::cout << "plan round trip: " << fixture.name << ", " << plan->valueStorage.Size() << " values, " << ShaderDiskCache::EncodePlanEntry(key, *plan).size() << " bytes per entry, " << succeeded << " of 1000 runtimes materialized\n"; ++index; } const auto covers = [&](const std::function& predicate) { @@ -1690,7 +1867,7 @@ void verifyPlanRoundTrip() { require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.tableColumn.has_value() && !source.tableColumn->keyDomains.empty(); }); }), "no fixture plans an image table with a key domain"); require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.tableColumn.has_value() && source.tableColumn->address; }); }), "no fixture plans an image table through a pointer"); require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.controlFlow.begin(), plan.controlFlow.end(), [](const ResourceBlock& block) { return block.condition != nullptr; }); }), "no fixture plans a control-flow condition"); - require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.valueStorage.begin(), plan.valueStorage.end(), [](const auto& value) { return value->IsPhi(); }); }), "no fixture plans a phi"); + require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.valueStorage.begin(), plan.valueStorage.end(), [](const auto& value) { return value.IsPhi(); }); }), "no fixture plans a phi"); require(covers([](const IrResourcePlan& plan) { return !plan.guardedSrtSlots.empty(); }), "no fixture plans a guarded SRT read"); require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.pureFlatSlots.begin(), plan.pureFlatSlots.end(), [](std::uint8_t pure) { return pure != 0u; }); }), "no fixture plans a pure flat slot"); } @@ -1905,7 +2082,21 @@ void verifyDefaultDirectory(const char* self) { int main(int argc, char** argv) { try { + if (argc == 2 && std::string_view(argv[1]) == "--resource-plan-codec") { + verifyResultRoundTrip(); + verifyEntryRoundTrip(); + verifyPlanRoundTrip(); + std::cout << "resource plan codec tests passed\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--artifact-codec") { + verifyResultRoundTrip(); + verifyEntryRoundTrip(); + std::cout << "artifact codec tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess(); + if (argc == 2 && std::string_view(argv[1]) == "--load-prebuilt") return runPrebuiltLoadingProcess(); if (argc == 2 && std::string_view(argv[1]) == "--no-failure-memo") return runWithoutFailureMemo(); if (argc == 2 && std::string_view(argv[1]).starts_with("--load-plan")) return runPlanLoadingProcess(argv[1]); const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-disk-cache-test-" + std::to_string(std::random_device{}())); @@ -1916,6 +2107,7 @@ int main(int argc, char** argv) { verifyAtomicReplacement(directory); verifyResultRoundTrip(); verifyEntryRoundTrip(); + verifyPrebuiltPack(directory / "packs"); verifyArtifactStorageIsolation(); verifyKeySensitivity(); verifyFloatModeSerialization(); @@ -1924,6 +2116,7 @@ int main(int argc, char** argv) { verifyStore(); verifyPersist(); verifyAcrossProcesses(argv[0]); + verifyPrebuiltAcrossProcesses(argv[0], directory); verifyPlanAcrossProcesses(argv[0]); verifyEmissionFailureMemo(); verifyFailureMemoSwitch(argv[0]); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 59cf1d71b5..462bbcd8ff 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -525,6 +525,73 @@ void verifySignedMinimum() { } } +void verifyMayEvaluate() { + using namespace ShaderRecompiler; + IrResourcePlan plan; + std::vector> values; + const auto make = [&](IrOpcode opcode, IrType type, std::initializer_list arguments = {}) -> IrValue& { + values.push_back(std::make_unique(opcode, type, static_cast(values.size()))); + for (auto* argument : arguments) values.back()->AddArgument(argument); + return *values.back(); + }; + const auto constant = [&](std::uint32_t value) -> IrValue& { + auto& leaf = make(IrOpcode::Void, IrType::U32); + leaf.SetImmediateU32(value); + return leaf; + }; + auto& scalar = make(IrOpcode::Void, IrType::ScalarReg); + scalar.SetRegister({RegisterBank::Scalar, 0}); + auto& vector = make(IrOpcode::Void, IrType::VectorReg); + vector.SetRegister({RegisterBank::Vector, 0}); + auto& input = make(IrOpcode::GetUserData, IrType::U32, {&scalar}); + auto& lane = make(IrOpcode::GetRegister, IrType::U32, {&vector}); + auto& uniform = make(IrOpcode::IEqual32, IrType::Bool, {&input, &constant(4)}); + auto& varying = make(IrOpcode::IEqual32, IrType::Bool, {&input, &lane}); + auto& select = make(IrOpcode::SelectU32, IrType::U32, {&uniform, &constant(1), &lane}); + auto& unevaluated = make(IrOpcode::FPAdd32, IrType::F32, {&input, &input}); + auto& invariant = make(IrOpcode::Phi, IrType::Bool); + invariant.AddArgument(&uniform); + invariant.AddArgument(&uniform); + auto& divergent = make(IrOpcode::Phi, IrType::Bool); + divergent.AddArgument(&uniform); + divergent.AddArgument(&varying); + auto& counter = make(IrOpcode::Phi, IrType::U32); + auto& next = make(IrOpcode::CompositeExtractU32x2, IrType::U32, {&make(IrOpcode::IAddCarry32, IrType::U32x2, {&counter, &constant(1)}), &constant(0)}); + counter.AddArgument(&constant(0)); + counter.AddArgument(&next); + auto& latch = make(IrOpcode::ULessThan32, IrType::Bool, {&next, &constant(4)}); + auto& sum = make(IrOpcode::CompositeExtractU32x2, IrType::U32, {&make(IrOpcode::IAddCarry32, IrType::U32x2, {&input, &constant(1)}), &constant(0)}); + std::array data{4}; + SrtRuntime runtime{data}; + for (const auto& [value, expected, name] : std::array, 11>{{ + {&constant(3), true, "a constant"}, + {&input, true, "user data"}, + {&uniform, true, "a comparison of user data"}, + {&lane, false, "a vector register read"}, + {&varying, false, "a comparison with a vector register"}, + {&select, false, "a select with a vector register arm"}, + {&unevaluated, false, "an opcode the evaluator does not evaluate"}, + {&invariant, true, "a phi whose operands agree"}, + {&divergent, false, "a phi whose operands differ"}, + {&latch, false, "a scalar loop counter's latch condition"}, + {&sum, true, "the low word of a carry add of user data"}, + }}) { + require(Detail::Evaluator::MayEvaluate(plan, value) == expected, (std::string("MayEvaluate misjudged ") + name).c_str()); + Detail::Evaluator evaluator(plan, runtime); + std::uint32_t result = 0; + require(evaluator.Evaluate(value, result) == expected, (std::string("MayEvaluate disagreed with the evaluator on ") + name).c_str()); + } + const std::array takenAsEvaluable{IrOpcode::Identity, IrOpcode::Phi, IrOpcode::Void, IrOpcode::GetUserData, IrOpcode::GetShaderBase, IrOpcode::ReadFirstLane, IrOpcode::ReadConst, IrOpcode::LoadAddressU32, IrOpcode::ReadConstBuffer}; + for (std::uint16_t raw = 0; raw < static_cast(IrOpcode::Count); ++raw) { + const auto opcode = static_cast(raw); + if (std::ranges::find(takenAsEvaluable, opcode) != takenAsEvaluable.end()) continue; + auto& value = make(opcode, IrType::U32, {&constant(0), &constant(0), &constant(0), &constant(0)}); + Detail::Evaluator evaluator(plan, runtime); + std::uint32_t result = 0; + require(Detail::Evaluator::MayEvaluate(plan, &value) == evaluator.Evaluate(&value, result), (std::string("MayEvaluate disagreed with the evaluator on ") + std::string(IrOpcodeName(opcode)) + " over constants").c_str()); + } +} + void verifyEvaluatedValues() { using namespace ShaderRecompiler; std::vector> values; @@ -1648,6 +1715,57 @@ void verifyPointerImageTable() { #endif } +void verifyPointerTableOffsetCarry() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + constexpr std::size_t Low = 0x10000; + constexpr std::uint64_t High = 0xffff0000ull; + constexpr std::size_t HighBytes = 0x20000; + constexpr std::uint64_t Reserved = High + HighBytes; + static TableTexture textures[2]; + void* block = nullptr; +#ifdef _WIN32 + block = VirtualAlloc(nullptr, static_cast(Reserved), MEM_RESERVE, PAGE_NOACCESS); + require(block != nullptr && VirtualAlloc(block, Low, MEM_COMMIT, PAGE_READWRITE) == block && VirtualAlloc(static_cast(block) + High, HighBytes, MEM_COMMIT, PAGE_READWRITE) == static_cast(block) + High, "pointer table offset carry: cannot map the root"); +#else + block = mmap(nullptr, Reserved, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0); + require(block != MAP_FAILED && mprotect(block, Low, PROT_READ | PROT_WRITE) == 0 && mprotect(static_cast(block) + High, HighBytes, PROT_READ | PROT_WRITE) == 0, "pointer table offset carry: cannot map the root"); +#endif + auto* root = static_cast(block); + const auto base = static_cast(reinterpret_cast(root)); + static std::array keys{}; + static std::array output{}; + const auto keysV = TableV(keys.data(), 0u, static_cast(sizeof(keys))); + const auto outputV = TableV(output.data(), 0u, static_cast(sizeof(output))); + std::copy(keysV.begin(), keysV.end(), root); + std::copy(outputV.begin(), outputV.end(), root + 4); + const auto at = [&](std::uint64_t offset, std::array words) { + std::copy(words.begin(), words.end(), root + offset / 4u); + }; + at(0x100000010ull, TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D)); + at(0x50u, TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D)); + const std::array userData{static_cast(base), static_cast(base >> 32u)}; + const std::vector code{0xf4080200u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x7e200500u, 0x8f108210u, 0xf4200444u, + 0x20000000u, 0x8711ff11u, 0x000000ffu, 0x8f128511u, 0x8113ff12u, 0xffffff10u, 0x8114ff12u, 0xffffff20u, 0xf4080900u, 0x26000040u, 0xf4080a00u, + 0x28000040u, 0xf09c0f08u, 0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + auto request = TableRequest(code, userData); + request.shader.codeAddress = 0x36000u; + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "pointer table offset carry: a T# read by two loads of its record was not planned as a table"); + const auto& column = *plan->descriptorSources.at(plan->info.images[0].source).tableColumn; + require(column.address && column.stride == 32u && column.addend == 0xffffff10u && column.offset == 0x40u && column.maxKey == 0xffu && column.foldedAddend == 0xffffff10u && column.foldedCarry == 16u, "pointer table offset carry: the column is wrong"); + const auto compiled = CompileTable(request); + const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0); + require(snapshot.keys == 256u && snapshot.codes[7] == Abi::PoisonCode(static_cast(Abi::PoisonReason::OffsetCarry)), "pointer table offset carry: the record whose second load wraps past 32 bits is not OffsetCarry poison"); + require((snapshot.codes[6] & Abi::PoisonFlag) == 0u && (snapshot.codes[8] & Abi::PoisonFlag) == 0u && compiled.capture->snapshot.tables.words.at(snapshot.codes[6]).dwords[0] == root[0x100000010ull / 4u] && compiled.capture->snapshot.tables.words.at(snapshot.codes[8]).dwords[0] == root[0x50u / 4u] && root[0x100000010ull / 4u] != root[0x50u / 4u], "pointer table offset carry: the records on either side of the carry were not resolved"); + require(!TableTouched(compiled.regions, root + 0x100000030ull / 4u, 0x20u), "pointer table offset carry: the record whose second load wraps was read"); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + munmap(block, Reserved); +#endif +} + void verifyDescriptorPhis() { using namespace ShaderRecompiler; constexpr std::uint32_t Format8888UNorm = 56; @@ -2472,6 +2590,28 @@ void verifyMeshConfiguration() { RecompileCacheKey::Build(vertex, unclipped); require(clipped != unclipped, "PA_CL_VS_OUT_CNTL is not part of the recompile cache key"); } + { + RecompileRequest fragment{}; + fragment.shader = {ShaderStage::Fragment, 0x10000u, code, 0, {}}; + fragment.context.waveSize = 64; + ShaderPixelStageInfo info{}; + info.quadPixelMask = 0x2u; + fragment.context.pixel = info; + const auto back = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(fragment)); + require(back.request.context.pixel.has_value() && back.request.context.pixel->quadPixelMask == 0x2u, "the quad pixel mask was lost in serialization"); + std::vector masked; + RecompileCacheKey::Build(fragment, masked); + fragment.context.pixel->quadPixelMask = 0xfu; + std::vector full; + RecompileCacheKey::Build(fragment, full); + require(masked != full, "the quad pixel mask is not part of the recompile cache key"); + fragment.context.pixel->quadSampleMask = {0x1u, 0x2u, 0x0u, 0xffffu}; + const auto sampled = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(fragment)); + require(sampled.request.context.pixel.has_value() && sampled.request.context.pixel->quadSampleMask == std::array{0x1u, 0x2u, 0x0u, 0xffffu}, "the quad sample masks were lost in serialization"); + std::vector partial; + RecompileCacheKey::Build(fragment, partial); + require(partial != full, "the quad sample masks are not part of the recompile cache key"); + } std::vector key; RecompileCacheKey::Build(request, key); const auto first = key; @@ -2508,16 +2648,31 @@ void verifyMeshConfiguration() { RecompileCacheKey::Build(primitiveGrouped, key); require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on the primitive group size, which rebuilds their relative patch index"); } - auto shared = hull; - shared.graphics->tessellation->breakAtInstance = false; + auto instanced = hull; + instanced.graphics->tessellation->breakAtInstance = false; + instanced.graphics->draw.instanceCount = 2u; + instanced.graphics->draw.indexCount = 12u; + auto longer = instanced; + longer.graphics->draw.indexCount = 16u; + auto breaking = instanced; + breaking.graphics->tessellation->breakAtInstance = true; for (const auto stage : {ShaderStage::Local, ShaderStage::TessellationControl, ShaderStage::TessellationEvaluation}) { hull.shader.stage = stage; - shared.shader.stage = stage; + instanced.shader.stage = stage; + longer.shader.stage = stage; + breaking.shader.stage = stage; RecompileCacheKey::Build(hull, key); - const auto patchKey = key; - RecompileCacheKey::Build(shared, key); - require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on BREAK_WAVE_AT_EOI, which decides whether their relative patch index can be rebuilt"); + const auto single = key; + RecompileCacheKey::Build(instanced, key); + const auto shared = key; + RecompileCacheKey::Build(longer, key); + require((shared != single && key != shared) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on the patches per instance of an instanced draw"); + RecompileCacheKey::Build(breaking, key); + require(key == single, "an instanced draw whose instances start hull threadgroups (BREAK_WAVE_AT_EOI) changed the cache key"); } + breaking.shader.stage = ShaderStage::TessellationControl; + const auto breakingReplay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(breaking)); + require(breakingReplay.request.graphics && breakingReplay.request.graphics->tessellation && breakingReplay.request.graphics->tessellation->breakAtInstance, "BREAK_WAVE_AT_EOI was lost in serialization"); const std::array users{}; request.context.waveSize = 32u; request.context.userData = users; @@ -2737,7 +2892,7 @@ void verifyPixelRequestSerialization() { const auto fields = [](const ShaderPixelStageInfo& value) { return std::tie(value.interpolatorCount, value.interpolatorSettings, value.wave32, value.inputAddr, value.hasPerspectiveCenterVgpr, value.perspectiveCentroid, value.posX, value.posY, - value.posZ, value.posW, value.frontFace, value.ancillary, value.sampleShading, + value.posZ, value.posW, value.frontFace, value.frontFaceAllBits, 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.stencilRefExportEnable, value.rasterPrimitive, value.layerExported, value.ditheredAlphaToCoverage); @@ -2769,6 +2924,7 @@ void verifyPixelRequestSerialization() { .posZ = true, .posW = true, .frontFace = true, + .frontFaceAllBits = true, .ancillary = true, .sampleShading = true, .noPerspective = true, @@ -2836,7 +2992,13 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQRkAAAA=", "new requests did not use serialization version 25"); + require(requestPrefix(encoded, 8u) == "NVNQQR0AAAA=", "new requests did not use serialization version 29"); + const std::array fallbacks{0x40u, 0x1ac4u}; + auto withFallbacks = minimal; + withFallbacks.context.linkedCallFallbacks = fallbacks; + const auto fallbackRequest = serializer.Deserialize(serializer.Serialize(withFallbacks)); + require(std::ranges::equal(fallbackRequest.request.context.linkedCallFallbacks, fallbacks), "the linked call fallbacks did not round-trip"); + require(serializer.Deserialize(encoded).request.context.linkedCallFallbacks.empty(), "a request without linked call fallbacks gained some"); require(serializer.Deserialize(encoded).request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && serializer.Deserialize(encoded).request.context.pixel->ditheredAlphaToCoverage, "the pixel primitive class or dither mode did not round-trip"); require(serializer.Deserialize(encoded).request.context.pixel->layerExported, "the pixel layer export did not round-trip"); const auto legacy16 = serializer.Deserialize( @@ -2945,10 +3107,10 @@ void verifyPixelRequestSerialization() { require(RecompileCacheKey::ContextHash(legacy19.request) == RecompileCacheKey::ContextHash(upgraded19.request), "upgrading version19 changed shader identity"); } constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u; - for (std::size_t bytes = 0; bytes < 23u; ++bytes) { - expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export or dither flag was accepted"); + for (std::size_t bytes = 0; bytes < 41u; ++bytes) { + expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRoAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQR4AAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -3066,7 +3228,40 @@ void verifyVertexInfoVersion19() { std::size_t flagOffset = 0; while (flagOffset < legacyPixel.size() && legacyPixel[flagOffset] == exportedBytes[flagOffset]) ++flagOffset; require(flagOffset < legacyPixel.size(), "the pixel layer export flag is not serialized"); - legacyPixel.erase(legacyPixel.begin() + static_cast(flagOffset), legacyPixel.begin() + static_cast(flagOffset + 2u)); + auto allBitsPixel = pixel; + allBitsPixel.context.pixel->frontFaceAllBits = true; + const auto allBitsBytes = decodeBase64(serializer.Serialize(allBitsPixel)); + auto version26Pixel = decodeBase64(encodedPixel); + std::size_t allBitsOffset = 0; + while (allBitsOffset < version26Pixel.size() && version26Pixel[allBitsOffset] == allBitsBytes[allBitsOffset]) ++allBitsOffset; + require(allBitsOffset == flagOffset + 2u, "the front-face all-bits flag does not follow the dithered coverage flag"); + version26Pixel.erase(version26Pixel.begin() + static_cast(allBitsOffset), version26Pixel.begin() + static_cast(allBitsOffset + 2u + 16u)); + version26Pixel[4] = 26u; + const auto replay26 = serializer.Deserialize(encodeBase64(version26Pixel)); + require(replay26.request.context.pixel.has_value() && replay26.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && !replay26.request.context.pixel->frontFaceAllBits, "a version 26 request lost its pixel primitive class or read a front-face all-bits flag"); + require(serializer.Serialize(replay26.request) == encodedPixel, "a version 26 request misread the fields after its dithered coverage flag"); + auto maskedPixel = pixel; + maskedPixel.context.pixel->quadPixelMask = 0x2u; + const auto maskedBytes = decodeBase64(serializer.Serialize(maskedPixel)); + std::size_t maskOffset = 0; + while (maskOffset < legacyPixel.size() && legacyPixel[maskOffset] == maskedBytes[maskOffset]) ++maskOffset; + require(maskOffset == flagOffset + 3u && maskOffset < legacyPixel.size(), "the quad pixel mask is not the byte after the front-face all-bits flag"); + constexpr std::size_t sampleMaskBytes = 16u; + auto sampledPixel = maskedPixel; + sampledPixel.context.pixel->quadSampleMask = {0x1u, 0x2u, 0x4u, 0x8u}; + auto version28 = decodeBase64(serializer.Serialize(sampledPixel)); + require(std::equal(maskedBytes.begin(), maskedBytes.begin() + static_cast(maskOffset + 1u), version28.begin()) && version28[maskOffset + 1u] == 0x1u && version28[maskOffset + 5u] == 0x2u && version28[maskOffset + 9u] == 0x4u && version28[maskOffset + 13u] == 0x8u, "the quad sample masks are not the four words after the quad pixel mask"); + version28.erase(version28.begin() + static_cast(maskOffset + 1u), version28.begin() + static_cast(maskOffset + 1u + sampleMaskBytes)); + version28[4] = 28u; + const auto replay28 = serializer.Deserialize(encodeBase64(version28)); + require(replay28.request.context.pixel.has_value() && replay28.request.context.pixel->quadPixelMask == 0x2u && replay28.request.context.pixel->quadSampleMask == std::array{0xffffu, 0xffffu, 0xffffu, 0xffffu} && decodeBase64(serializer.Serialize(replay28.request)) == maskedBytes, "a version 28 request read quad sample masks or misread the fields after them"); + auto version27 = maskedBytes; + version27.erase(version27.begin() + static_cast(maskOffset), version27.begin() + static_cast(maskOffset + 1u + sampleMaskBytes)); + version27[4] = 27u; + const auto replay27 = serializer.Deserialize(encodeBase64(version27)); + require(replay27.request.context.pixel.has_value() && replay27.request.context.pixel->quadPixelMask == 0xfu && serializer.Serialize(replay27.request) == encodedPixel, "a version 27 request read a quad pixel mask or misread the fields after it"); + legacyPixel.erase(legacyPixel.begin() + static_cast(flagOffset), legacyPixel.begin() + static_cast(flagOffset + 4u + sampleMaskBytes)); + legacyPixel.resize(legacyPixel.size() - sizeof(std::uint64_t)); for (const auto version : {19u, 20u, 21u, 22u}) { auto versionPixel = legacyPixel; versionPixel[4] = static_cast(version); @@ -4725,6 +4920,59 @@ void verifyGuardedNullPointers() { #endif } +void verifyQuadMaskNeighbourReads() { + using namespace ShaderRecompiler; + constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic}; + constexpr std::array demoteCapabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic, spv::CapabilityDemoteToHelperInvocation}; + constexpr std::array demoteExtensions{"SPV_EXT_demote_to_helper_invocation"}; + constexpr std::array constant{0x7e0202f2u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + constexpr std::array explicitExec{0xbefe04c1u, 0x7e000500u, 0x7e020200u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + constexpr std::array constantWqm{0xbefe0ac1u, 0x7e000500u, 0x7e020200u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + constexpr std::array permute{0xbefe04c1u, 0x7e040284u, 0xdac80000u, 0x01000002u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + constexpr std::array comparisonBallot{0xbe8003ffu, 0x3f000000u, 0xbefe04c1u, 0x7d840000u, 0xbe80046au, 0x7e020200u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + const auto compile = [&](std::span code, bool masked, bool demote) { + RecompileRequest request{}; + request.shader = {ShaderStage::Fragment, 0x58000u, code, 0, {}}; + request.context.waveSize = 32u; + request.context.pixel = ShaderPixelStageInfo{}; + request.context.pixel->inputAddr = PixelInputBit(PixelInput::PositionX); + request.context.pixel->posX = true; + request.context.pixel->quadPixelMask = masked ? 0x2u : 0xfu; + request.context.pixel->targetOutputMode[0] = 9u; + request.context.pixel->targetExportMapping.fill(0xe4u); + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32u; + request.target.supportedCapabilities = demote ? std::span(demoteCapabilities) : std::span(capabilities); + if (demote) request.target.supportedExtensions = demoteExtensions; + request.layout.pushConstantSizeBytes = 128u; + request.useCache = false; + return Recompile(request); + }; + const auto declares = [](const RecompileResult& result, spv::Op opcode) { + const auto& words = result.spirv.Words(); + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + require(count != 0 && count <= words.size() - cursor, "quad mask: truncated SPIR-V instruction"); + if ((words[cursor] & 0xffffu) == static_cast(opcode)) return true; + cursor += count; + } + return false; + }; + require(declares(compile(constant, true, false), spv::OpKill), "a quad mask without neighbour reads did not retain its kill fallback"); + for (const auto code : {std::span(explicitExec), std::span(constantWqm), std::span(permute)}) { + const auto unmasked = compile(code, false, false); + require(declares(unmasked, spv::OpGroupNonUniformShuffle), "the neighbour-read test did not retain its cross-lane shuffle"); + expectFailure([&] { static_cast(compile(code, true, false)); }, "shaderDemoteToHelperInvocation", "a quad mask killed helper lanes before a cross-lane read with EXEC enabled for those lanes"); + const auto masked = compile(code, true, true); + require(declares(masked, spv::OpDemoteToHelperInvocation) && declares(masked, spv::OpGroupNonUniformShuffle), "a quad mask with demote did not preserve its cross-lane shuffle path"); + } + require(declares(compile(comparisonBallot, false, false), spv::OpGroupNonUniformBallot), "the comparison test did not retain its subgroup ballot"); + expectFailure([&] { static_cast(compile(comparisonBallot, true, false)); }, "shaderDemoteToHelperInvocation", "a quad mask killed helper lanes before a ballot with EXEC enabled for those lanes"); + const auto maskedBallot = compile(comparisonBallot, true, true); + require(declares(maskedBallot, spv::OpDemoteToHelperInvocation) && declares(maskedBallot, spv::OpGroupNonUniformBallot), "a quad mask with demote did not preserve its ballot path"); +} + void verifyCubeDepthCompareMode() { using namespace ShaderRecompiler; ImageResource image{}; @@ -4780,6 +5028,11 @@ void verifySrgbDepthCompareModes() { int main(int argc, char** argv) { try { using namespace ShaderRecompiler; + if (argc == 2 && std::string_view(argv[1]) == "--quad-mask-no-demote") { + verifyQuadMaskNeighbourReads(); + std::cout << "Quad masks preserve neighbour-lane helpers or refuse without demote\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--frontend-pair") { verifyFrontendPair(); std::cout << "Prepared frontend reuse preserves full and partial-thread programs\n"; @@ -4802,6 +5055,7 @@ int main(int argc, char** argv) { verifyImageTableRegistration(); verifyScalarAddressRule(); verifyPointerImageTable(); + verifyPointerTableOffsetCarry(); std::cout << "Image table keys, maps, records and registration passed\n"; return 0; } @@ -4818,6 +5072,7 @@ int main(int argc, char** argv) { verifySignedSrtComparison(); verifyOutOfRangeConstantReads(); verifySignedMinimum(); + verifyMayEvaluate(); verifyPureFlatSlots(); verifyGuardedDescriptorSources(); verifyImageTableKeys(); @@ -4825,6 +5080,7 @@ int main(int argc, char** argv) { verifyImageTableRegistration(); verifyScalarAddressRule(); verifyPointerImageTable(); + verifyPointerTableOffsetCarry(); verifyDescriptorPhis(); verifyUniformTableImage(); verifyLaneSelectedDescriptors(); @@ -4860,6 +5116,7 @@ int main(int argc, char** argv) { verifyCubeDepthCompareMode(); verifyVolumeArrayLoadMode(); verifySrgbDepthCompareModes(); + verifyQuadMaskNeighbourReads(); verifyCoherentGds(); #if ANYPS5_ENABLE_SPIRV_TOOLS const std::vector minimalSpirv{ diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp index b9c3911406..79616003ec 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp @@ -166,6 +166,8 @@ void testNarrowConversions(bool optimizationEnabled) { check(opcodeCount(normalized, spv::OpStore) == (vector ? 1u : 2u), label + ": optimizer lost buffer stores"); if (!optimizationEnabled) { check(output == input, label + ": none mode changed the shader"); + const auto specialized = ShaderRecompiler::ValidateAndOptimizeSpirv(input, Vulkan11, Spirv13, false, true, true); + check(specialized == input, label + ": none mode changed the specialized shader"); } else { check(opcodeCount(normalized, spv::OpIAdd) == 0, label + ": optimizer did not remove dead arithmetic"); if (arithmetic && !type.floating) { @@ -378,6 +380,77 @@ OpFunctionEnd } } +void testLongControlFlow(bool optimizationEnabled, bool large) { + const char* mode = std::getenv("APS5_SPIRV_OPT"); + const bool full = mode != nullptr && std::string(mode) == "full"; + const std::size_t blocks = large && optimizationEnabled && !full ? 512 : 8; + std::string source = R"(OpCapability Shader +OpCapability StorageBuffer8BitAccess +OpExtension "SPV_KHR_8bit_storage" +OpMemoryModel Logical GLSL450 +OpEntryPoint GLCompute %main "main" +OpExecutionMode %main LocalSize 1 1 1 +OpDecorate %array ArrayStride 4 +OpMemberDecorate %buffer 0 Offset 0 +OpDecorate %buffer Block +OpDecorate %data DescriptorSet 0 +OpDecorate %data Binding 0 +OpDecorate %byteArray ArrayStride 1 +OpMemberDecorate %byteBuffer 0 Offset 0 +OpDecorate %byteBuffer Block +OpDecorate %bytes DescriptorSet 0 +OpDecorate %bytes Binding 1 +%void = OpTypeVoid +%uint = OpTypeInt 32 0 +%boolean = OpTypeBool +%byte = OpTypeInt 8 0 +%byteArray = OpTypeRuntimeArray %byte +%byteBuffer = OpTypeStruct %byteArray +%byteBufferPointer = OpTypePointer StorageBuffer %byteBuffer +%bytePointer = OpTypePointer StorageBuffer %byte +%array = OpTypeRuntimeArray %uint +%buffer = OpTypeStruct %array +%bufferPointer = OpTypePointer StorageBuffer %buffer +%valuePointer = OpTypePointer StorageBuffer %uint +%function = OpTypeFunction %void +%data = OpVariable %bufferPointer StorageBuffer +%bytes = OpVariable %byteBufferPointer StorageBuffer +%zero = OpConstant %uint 0 +%one = OpConstant %uint 1 +%two = OpConstant %uint 2 +)"; + for (std::size_t i = 0; i < blocks * 4; ++i) source += "%index" + std::to_string(i) + " = OpConstant %uint " + std::to_string(i + 1) + "\n"; + source += "%main = OpFunction %void None %function\n%entry = OpLabel\n%input = OpAccessChain %valuePointer %data %zero %zero\n%seed = OpLoad %uint %input Volatile\n%condition = OpINotEqual %boolean %seed %zero\nOpBranch %block0\n"; + for (std::size_t i = 0; i < blocks; ++i) { + const auto number = std::to_string(i); + const auto previous = i == 0 ? "%seed" : "%next" + std::to_string(i - 1); + source += "%block" + number + " = OpLabel\n%sum" + number + " = OpIAdd %uint " + previous + " %one\n"; + source += "%duplicate" + number + " = OpIAdd %uint " + previous + " %one\n%shared" + number + " = OpIAdd %uint %seed %one\n"; + for (std::size_t j = 0; j < 3; ++j) { + const auto index = std::to_string(i * 4 + j); + source += "%slot" + index + " = OpAccessChain %valuePointer %data %zero %index" + index + "\n"; + source += "OpStore %slot" + index + " %" + (j == 0 ? "sum" : j == 1 ? "duplicate" : "shared") + number + " Volatile\n"; + } + const auto branchIndex = std::to_string(i * 4 + 3); + source += "%branchSlot" + number + " = OpAccessChain %valuePointer %data %zero %index" + branchIndex + "\n"; + source += "OpSelectionMerge %join" + number + " None\nOpBranchConditional %condition %left" + number + " %right" + number + "\n"; + source += "%left" + number + " = OpLabel\n%leftValue" + number + " = OpIAdd %uint %sum" + number + " %one\nOpStore %branchSlot" + number + " %leftValue" + number + " Volatile\nOpBranch %join" + number + "\n"; + source += "%right" + number + " = OpLabel\n%rightCopy" + number + " = OpIAdd %uint %sum" + number + " %one\nOpStore %branchSlot" + number + " %rightCopy" + number + " Volatile\n"; + source += "%rightValue" + number + " = OpIAdd %uint %sum" + number + " %two\nOpStore %branchSlot" + number + " %rightValue" + number + " Volatile\nOpBranch %join" + number + "\n"; + source += "%join" + number + " = OpLabel\n%next" + number + " = OpPhi %uint %leftValue" + number + " %left" + number + " %rightValue" + number + " %right" + number + "\n"; + if (i + 1 == blocks) source += "%byteSlot = OpAccessChain %bytePointer %bytes %zero %zero\n%truncated = OpUConvert %byte %next" + number + "\nOpStore %byteSlot %truncated Volatile\nOpReturn\n"; + else source += "OpBranch %block" + std::to_string(i + 1) + "\n"; + } + source += "OpFunctionEnd\n"; + const auto input = assemble(source); + const auto output = ShaderRecompiler::ValidateAndOptimizeSpirv(input, Vulkan11, Spirv13, false); + const auto stores = opcodeCount(output, spv::OpStore); + check(stores >= blocks * 4 + 1 && stores <= blocks * 6 + 1, "long control-flow optimization discarded observable stores"); + const auto adds = opcodeCount(output, spv::OpIAdd); + if (!optimizationEnabled) check(output == input, "disabled optimization changed the long control-flow module"); + else check(adds >= blocks && adds <= blocks * 4, "control-flow optimization lost arithmetic or retained redundant values"); +} + } int main() { @@ -388,6 +461,8 @@ int main() { testInvalidInputIsRejected(); testSpecializedSelectionExit(); testSpecializedDeadContinue(); + testLongControlFlow(optimizationEnabled, false); + testLongControlFlow(optimizationEnabled, true); if (failures != 0) { std::fprintf(stderr, "%d optimizer check(s) failed\n", failures); return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/WaveControlDecode.cpp b/core/libs/prx/libSceAgcDriver/tests/WaveControlDecode.cpp index e75cde7a9f..16faeba83f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WaveControlDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WaveControlDecode.cpp @@ -102,6 +102,8 @@ void verifyTranslation() { require(recompile(withStore({0xbf8d0000u})).empty(), "s_sethalt 0 is refused: " + recompile(withStore({0xbf8d0000u}))); const std::vector trailingLoop{0x7e020281u, 0xbf820003u, 0xe0700000u, 0x80000100u, 0xbf810000u, 0xbf82fffcu, 0xbf9f0000u, 0xbf9f0000u, 0xffffffffu}; require(recompile(trailingLoop).empty(), "a program padded with s_code_end after its last branch is refused: " + recompile(trailingLoop)); + const auto orderedAdd = withStore({0xd8fe0000u, 0x05000001u}); + require(recompile(orderedAdd).empty(), "ds_ordered_count adding to a GDS counter is refused: " + recompile(orderedAdd)); const std::vector, std::string_view>> refused{ {"s_sethalt 1", withStore({0xbf8d0001u}), "s_sethalt 1 at pc 0 halts the wave"}, @@ -114,7 +116,8 @@ void verifyTranslation() { {"ds_gws_sema_p", withStore({0xd8720000u, 0x00000000u}), "ds_gws_sema_p at pc 0: global wave sync"}, {"ds_gws_sema_release_all", withStore({0xd8620000u, 0x00000000u}), "ds_gws_sema_release_all at pc 0: global wave sync"}, {"ds_gws_barrier", withStore({0xd8760000u, 0x00000003u}), "ds_gws_barrier at pc 0: global wave sync"}, - {"ds_ordered_count", withStore({0xd8fe0000u, 0x05000001u}), "ds_ordered_count at pc 0: GDS counters"}, + {"ds_ordered_count swap", withStore({0xd8fe1000u, 0x05000001u}), "ds_ordered_count at pc 0: ordered swap is not modeled"}, + {"ds_ordered_count of two dwords", withStore({0xd8fe4000u, 0x05000001u}), "ds_ordered_count at pc 0: counting more than one dword is not modeled"}, }; for (const auto& [name, code, message] : refused) { const auto error = recompile(code); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp index bf04d4c6f0..7ca17147aa 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp @@ -125,6 +125,9 @@ void ScalarReads(AgcDriver::VulkanDevice& device) { static_cast(ShaderRecompiler::Recompile(request)); } catch (const std::runtime_error& error) { Require(std::string(error.what()).find("buffer descriptor uses an unsupported type") != std::string::npos, "vector buffer descriptor failed for another reason"); + char words[64]; + std::snprintf(words, sizeof(words), "(buffer 0 V# %08x %08x %08x %08x)", userData[0], userData[1], userData[2], userData[3]); + Require(std::string(error.what()).find(words) != std::string::npos, std::string("unsupported buffer descriptor error does not name ") + words + ": " + error.what()); continue; } throw std::runtime_error("a vector or mixed buffer access accepted a non-buffer descriptor type"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ComputePipelineOutsideGpuMutex.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ComputePipelineOutsideGpuMutex.cpp new file mode 100644 index 0000000000..95513fdc4b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ComputePipelineOutsideGpuMutex.cpp @@ -0,0 +1,123 @@ +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array ReaderCode{0xf4040100u, 0xfa000000u, 0xf4080201u, 0xfa000000u, 0xbf8cc07fu, 0xf4000182u, 0xfa000000u, 0xbf8cc07fu, 0x7e000206u, 0xe0700000u, 0x80020000u, 0xbf810000u}; +alignas(256) std::array NewReaderCode{0xbf800000u, 0xf4040100u, 0xfa000000u, 0xf4080201u, 0xfa000000u, 0xbf8cc07fu, 0xf4000182u, 0xfa000000u, 0xbf8cc07fu, 0x7e000206u, 0xe0700000u, 0x80020000u, 0xbf810000u}; + +struct Slot { + alignas(256) std::array value{}; + alignas(256) std::array table{}; + alignas(256) std::array output{}; + alignas(256) std::array outputDescriptor{}; + alignas(256) volatile std::uint32_t done = 0; +}; + +std::array Slots; + +std::uint32_t Low(const volatile void* pointer) { + return static_cast(reinterpret_cast(pointer)); +} + +std::uint32_t High(const volatile void* pointer) { + return static_cast(static_cast(reinterpret_cast(pointer)) >> 32u); +} + +void SetShaderRegister(std::vector& words, std::uint32_t offset, std::uint32_t value) { + words.insert(words.end(), {0xc0017600u, offset, value}); +} + +void Dispatch(std::vector& words, const void* code, std::initializer_list userData) { + const auto address = static_cast(reinterpret_cast(code)); + SetShaderRegister(words, 0x20c, static_cast(address >> 8u)); + SetShaderRegister(words, 0x20d, static_cast(address >> 40u) & 0xffu); + for (const std::uint32_t offset : {0x207u, 0x208u, 0x209u}) SetShaderRegister(words, offset, 1); + SetShaderRegister(words, 0x212, 0); + SetShaderRegister(words, 0x213, static_cast(userData.size()) << 1u); + std::uint32_t offset = 0x240; + for (const auto value : userData) SetShaderRegister(words, offset++, value); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); +} + +void WriteData(std::vector& words, volatile std::uint32_t* target, std::uint32_t value) { + words.insert(words.end(), {0xc0033700u, 0x00100200u, Low(target), High(target), value}); +} + +void Submit(std::uint32_t queue, Slot& slot, const void* code, std::uint32_t value) { + slot.value[0] = value; + slot.table = {Low(slot.value.data()), High(slot.value.data())}; + slot.outputDescriptor = {Low(slot.output.data()), High(slot.output.data()) & 0xffffu, static_cast(sizeof(slot.output)), 0x31016facu}; + std::vector words; + Dispatch(words, code, {Low(slot.table.data()), High(slot.table.data()), Low(slot.outputDescriptor.data()), High(slot.outputDescriptor.data())}); + WriteData(words, &slot.done, 1); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(queue, &packet) == 0, "compute submit failed"); +} + +void WaitDone(const Slot& slot, const char* what) { + const auto start = std::chrono::steady_clock::now(); + while (slot.done == 0) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(60), what); + std::this_thread::sleep_for(std::chrono::microseconds(200)); + } +} + +} + +int main() { + using AgcDriver::VulkanDevice; + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); + Submit(0x28, Slots[0], ReaderCode.data(), 0x1234u); + WaitDone(Slots[0], "the first dispatch did not finish"); + Require(Slots[0].output[0] == 0x1234u, "the first dispatch stored " + std::to_string(Slots[0].output[0])); + + const auto before = VulkanDevice::ComputePipelineCounts(); + { + std::unique_lock gpuLock(AgcDriver::GuestMemory::GpuMutex()); + Submit(0x20, Slots[1], NewReaderCode.data(), 0x5678u); + const auto start = std::chrono::steady_clock::now(); + while (VulkanDevice::ComputePipelineCounts().created == before.created) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(60), "a dispatch of a new program did not create its pipeline while another thread held the GPU mutex"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + Require(Slots[1].done == 0, "the dispatch finished while another thread held the GPU mutex"); + } + WaitDone(Slots[1], "the dispatch of the new program did not finish"); + Require(Slots[1].output[0] == 0x5678u, "the dispatch of the new program stored " + std::to_string(Slots[1].output[0])); + const auto after = VulkanDevice::ComputePipelineCounts(); + Require(after.underGpuMutex == before.underGpuMutex, "a compute pipeline was created under the GPU mutex"); + std::cout << "compute pipeline outside GPU mutex test passed\n"; + AgcDriverWaitIdle_nid_postfix(); + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ComputeTableCalls.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeTableCalls.cpp new file mode 100644 index 0000000000..6b3bb066da --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeTableCalls.cpp @@ -0,0 +1,164 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "SceShaders.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Untouched = 0xcafebabeu; + +alignas(256) constexpr std::array Caller{ + 0x7e020280u, 0xf4080400u, 0xfa000000u, 0x8f148402u, 0xbf8cc07fu, 0xf4280208u, 0x28000000u, 0xbf8cc07fu, + 0xbe8e0308u, 0xbe8f0309u, 0xbf068103u, 0xbe8e210eu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u, + 0xbf9f0000u, 0x30306c73u, 0x00000040u, 0x00000008u}; +alignas(256) constexpr std::array SelectSixteen{0x85159091u, 0x7e020215u, 0xbe80200eu}; +alignas(256) constexpr std::array SelectThirtyTwo{0x8515a0a1u, 0x7e020215u, 0xbe80200eu}; +alignas(16) std::array Table{}; +alignas(16) std::array Descriptors{}; +alignas(256) std::array Output{}; + +void Fill() { + const auto first = reinterpret_cast(SelectSixteen.data()); + const auto second = reinterpret_cast(SelectThirtyTwo.data()); + Table = {static_cast(first), static_cast(first >> 32u), 0u, 0u, static_cast(second), static_cast(second >> 32u), 0u, 0u}; + const auto table = reinterpret_cast(Table.data()); + Descriptors = {static_cast(table), static_cast((table >> 32u) & 0xffffu) | (16u << 16u), 2u, 0x31016facu}; +} + +std::array UserData(std::uint32_t index, bool scc) { + const auto descriptors = reinterpret_cast(Descriptors.data()); + const auto output = reinterpret_cast(Output.data()); + return {static_cast(descriptors), static_cast(descriptors >> 32u), index, scc ? 1u : 0u, + static_cast(output), static_cast((output >> 32u) & 0xffffu), static_cast(sizeof(Output)), 0x31016facu}; +} + +std::vector Commands(const std::array& data, bool wave32) { + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{Threads, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast(Caller.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array resource{static_cast(data.size()) << 1u}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u | (wave32 ? 0x8000u : 0u)}); + return words; +} + +class StderrCapture { +public: + StderrCapture() { + std::fflush(stderr); + file = std::tmpfile(); + Require(file != nullptr, "cannot open a temporary file for stderr"); +#ifdef _WIN32 + saved = _dup(_fileno(stderr)); + _dup2(_fileno(file), _fileno(stderr)); +#else + saved = dup(fileno(stderr)); + dup2(fileno(file), fileno(stderr)); +#endif + } + std::string Finish() { + std::fflush(stderr); +#ifdef _WIN32 + _dup2(saved, _fileno(stderr)); + _close(saved); +#else + dup2(saved, fileno(stderr)); + close(saved); +#endif + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t read = 0; (read = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, read); + std::fclose(file); + std::fwrite(text.data(), 1, text.size(), stderr); + return text; + } + +private: + std::FILE* file = nullptr; + int saved = -1; +}; + +std::string Dispatch(std::uint32_t index, bool scc, bool wave32) { + Output.fill(Untouched); + const auto words = Commands(UserData(index, scc), wave32); + Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; + StderrCapture log; + std::string failure; + try { + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + } catch (const std::exception& error) { + failure = error.what(); + } + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); + return log.Finish() + failure; +} + +void Check(std::uint32_t index, bool scc, bool wave32, std::uint32_t expected, const std::string& what) { + const auto log = Dispatch(index, scc, wave32); + Require(log.find("no linked callee") == std::string::npos, what + ": a linked callee was reported missing: " + log); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Require(Output[lane] == expected, what + (wave32 ? " (wave32)" : " (wave64)") + ": lane " + std::to_string(lane) + " is " + std::to_string(Output[lane]) + ", expected " + std::to_string(expected)); + } +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + Fill(); + for (const bool wave32 : {false, true}) { + Check(0u, false, wave32, 16u, "the first record's callee with SCC 0"); + Check(0u, true, wave32, 17u, "the first record's callee with SCC 1"); + Check(1u, false, wave32, 32u, "the second record's callee with SCC 0"); + Check(1u, true, wave32, 33u, "the second record's callee with SCC 1"); + const auto log = Dispatch(2u, false, wave32); + Require(log.find("called address 0x0 at program counter") != std::string::npos && log.find("which no linked callee of that call matches") != std::string::npos, + std::string("a record past the table did not report an unlinked call") + (wave32 ? " (wave32)" : " (wave64)") + ": " + log); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Require(Output[lane] == Untouched, "a lane stored after the unlinked call: lane " + std::to_string(lane) + " is " + std::to_string(Output[lane])); + } + } + AgcDriverShutdown_nid_postfix(); + std::puts("compute table call tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception&) { + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp index 797f086287..cdb94c5363 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp @@ -313,6 +313,76 @@ void RecordedUploads(const Context& context, const WatchedBlock& block) { recorder.Sync(); } +void RunCubeGather(const Context& base, const WatchedBlock& block) { + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.width = Side; + resource.height = Side; + resource.depthOrLastArray = 5; + resource.mipCount = 2; + resource.tileMode = TextureTileMode::kZ64KBX; + resource.dimension = TextureDimension::kCube; + resource.format = Format32Float; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + const auto geometry = DescribeSurface(resource); + Require(geometry.layerBytes != LayerBytes, "the cube fixture's per-layer footprint must not match one host depth slice"); + const auto address = block.Address(); + resource.baseAddress = address; + ClearDepthSurfaces(context.device); + for (std::uint32_t layer = 0; layer < 6; ++layer) { + DepthSurfaceView(context, DepthTarget{address + geometry.GuestLayerOffset(layer), 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); + } + imageToBufferCopies = 0; + bufferToImageCopies = 0; + const auto texture = DepthSurfaceTexture(context, words, resource, components); + Require(texture != nullptr, "a cube over six depth surfaces at the descriptor's layer offsets was not sampled through their depth planes"); + if (imageToBufferCopies.load() != 6 || bufferToImageCopies.load() != 1) { + throw std::runtime_error("sampling a cube over six depth surfaces recorded " + std::to_string(imageToBufferCopies.load()) + " depth surface copies and " + std::to_string(bufferToImageCopies.load()) + " plane copy fills, expected 6 and 1"); + } + ClearDepthSurfaces(context.device); +} + +void RunCubeTiledSlice(const Context& base, const WatchedBlock& block) { + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + constexpr VkExtent2D extent{64, 64}; + GuestTextureResource resource{}; + resource.width = extent.width; + resource.height = extent.height; + resource.depthOrLastArray = 5; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kZ64KBX; + resource.dimension = TextureDimension::kCube; + resource.format = Format32Float; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + const auto geometry = DescribeSurface(resource); + const auto slice = DepthSliceBytes(extent, 4u); + Require(geometry.layerBytes == slice && geometry.sliceLinearBytes != slice, "the tiled cube fixture must have a layer footprint equal to a depth slice but a different linear footprint"); + const auto address = block.Address(); + resource.baseAddress = address; + ClearDepthSurfaces(context.device); + for (std::uint32_t layer = 0; layer < 6; ++layer) { + DepthSurfaceView(context, DepthTarget{address + geometry.GuestLayerOffset(layer), 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); + } + imageToBufferCopies = 0; + bufferToImageCopies = 0; + const auto texture = DepthSurfaceTexture(context, words, resource, components); + Require(texture != nullptr, "a tiled cube whose linear layer footprint differs from a depth slice was not sampled through its depth planes"); + if (imageToBufferCopies.load() != 6 || bufferToImageCopies.load() != 1) { + throw std::runtime_error("sampling a tiled cube over six depth surfaces recorded " + std::to_string(imageToBufferCopies.load()) + " depth surface copies and " + std::to_string(bufferToImageCopies.load()) + " plane copy fills, expected 6 and 1"); + } + ClearDepthSurfaces(context.device); +} + } int main() { @@ -337,6 +407,8 @@ int main() { Require(AgcDriver::GuestMemory::Watched(block.Address(), BlockBytes), "depth plane guest layers: the guest block is not write-watched"); Run(context, block); RecordedUploads(context, block); + RunCubeGather(context, block); + RunCubeTiledSlice(context, block); } device.reset(); std::puts("depth plane guest layer tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp index 2cdf03f889..f43484e980 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp @@ -1,9 +1,13 @@ +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" #include "DepthFastClearHarness.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "Recompiler.hpp" #include "VulkanTestDevice.hpp" #ifdef _WIN32 @@ -16,6 +20,7 @@ #endif #include #include +#include #include #include #include @@ -26,6 +31,14 @@ #include #include +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + namespace { using AgcDriver::Graphics::Require; @@ -35,6 +48,7 @@ using DepthFastClearHarness::VertexCode; using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Format16Unorm = 7; constexpr std::uint32_t Format16Uint = 11; constexpr std::uint32_t Format32Float = 22; constexpr std::uint32_t Type2D = 9; @@ -43,7 +57,7 @@ constexpr std::uint32_t Type2DArray = 13; constexpr std::uint32_t Swizzle64KbS = 9; constexpr std::uint32_t Swizzle64KbZX = 24; constexpr std::uint64_t SliceBytes = 65536; -constexpr std::size_t BlockBytes = 2u << 20u; +constexpr std::size_t BlockBytes = 8u << 20u; constexpr std::size_t OutputOffset = 0; constexpr std::size_t ArrayOffset = 0x10000; constexpr std::size_t CubeOffset = 0x40000; @@ -51,17 +65,18 @@ constexpr std::size_t IntegerOffset = 0xa0000; constexpr std::size_t DepthBitsOffset = 0xb0000; constexpr std::size_t OneSliceOffset = 0xd0000; constexpr std::size_t FirstSliceOffset = 0xe0000; +constexpr std::size_t PaddedCubeOffset = 0x120000; +constexpr std::size_t SmallCubeOffset = 0x180000; +constexpr std::size_t LargeCubeOffset = 0x200000; constexpr float DrawnDepth = 0.25f; constexpr float DrawnD16Depth = 0.2f; constexpr std::uint32_t DrawnD16Bits = 13107; -// image_load of row 0, one texel per thread (x = thread id), slice / face from user data, stored as a dword per -// thread into the buffer at s[0:3]. The T# is s[4:11], the slice s12. constexpr std::array LoadDims{1u, 3u, 5u}; std::array LoadCode(std::uint32_t dim) { return { - 0x7e020280, // v_mov_b32 v1, 0 (y) + 0x7e02020d, 0x7e04020c, // v_mov_b32 v2, s12 (slice or face) 0xf0001100u | (dim << 3u), // image_load v3, v[0:2], s[4:11] dmask:0x1 unorm, dim 2D / CUBE / 2D_ARRAY 0x00010300, @@ -79,7 +94,7 @@ alignas(256) std::array Load2D = LoadCode(LoadDims[0]); alignas(256) std::array LoadCube = LoadCode(LoadDims[1]); alignas(256) std::array LoadArray = LoadCode(LoadDims[2]); -alignas(256) std::array Pixels{}; +alignas(256) std::array Pixels{}; class GuestBlock { public: @@ -196,7 +211,7 @@ void DrawDepth(AgcDriver::VulkanDevice& device, std::uint64_t address, VkExtent2 }}; AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; - state.color = {reinterpret_cast(Pixels.data()), extent, VK_FORMAT_R8G8B8A8_UNORM, static_cast(extent.width) * extent.height * 4u, 0xe4u}; + state.color = {reinterpret_cast(Pixels.data()), extent, VK_FORMAT_R8G8B8A8_UNORM, AgcDriver::Graphics::ColorTargetLayout(extent.width, extent.height, AgcDriver::Graphics::ColorTileMode::Linear).Bytes(), 0xe4u}; state.colors = {state.color}; state.hasColorTarget = true; state.depth = AgcDriver::Graphics::DepthTarget{address, 0, extent, format, 1.0f, 0}; @@ -218,9 +233,7 @@ void DrawDepth(AgcDriver::VulkanDevice& device, std::uint64_t address, VkExtent2 device.WaitIdle(); } -// Loads row 0 of `slice` of the texture `shape` describes at `texels` into `output` (one dword per thread). With -// `shaderImage2D` the shader declares a 2D image whatever the T#'s type is. -void Load(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Shape& shape, std::uint32_t slice, std::span output, bool shaderImage2D = false) { +void Load(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Shape& shape, std::uint32_t slice, std::span output, bool shaderImage2D = false, std::uint32_t row = 0) { const auto& code = shaderImage2D ? Load2D : shape.type == TypeCube ? LoadCube : shape.type == Type2DArray ? LoadArray : Load2D; std::vector userData(16, 0u); const auto out = std::array{static_cast(reinterpret_cast(output.data())), static_cast((reinterpret_cast(output.data()) >> 32u) & 0xffffu), static_cast(output.size_bytes()), 0x31016facu}; @@ -228,6 +241,7 @@ void Load(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Shape& sh std::copy(out.begin(), out.end(), userData.begin()); std::copy(texture.begin(), texture.end(), userData.begin() + 4); userData[12] = slice; + userData[13] = row; const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(std::span(code))}}}; const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ @@ -303,6 +317,13 @@ void DepthBitsArrayCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std: // A 2D shader image over a 2D-array T# reads the array's first slice: a one-slice array, which samples the depth // image itself, and a 3-slice array, which samples the plane copy. +void DepthBitsBaseSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + Shape shape{Format16Uint, 256, 128, Type2DArray, 1, Swizzle64KbZX}; + shape.baseArray = 1; + Load(device, block.At(DepthBitsOffset), shape, 0, output); + Expect(output, 0x1234u, "D16 depth-bits array from base slice 1"); +} + void FirstSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { auto* one = block.At(OneSliceOffset); DrawDepth(device, reinterpret_cast(one), {128, 128}, VK_FORMAT_D32_SFLOAT, DrawnDepth); @@ -316,20 +337,87 @@ void FirstSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::spa Expect(output, Bits(DrawnDepth), "3-slice D32 2D array through a 2D shader image"); } -// A plane array from a later base slice is still refused (its slice offsets are not modeled here). -void RefusalCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { - bool refused = false; - try { - Shape shape{Format32Float, 128, 128, Type2DArray, 2, Swizzle64KbZX}; - shape.baseArray = 1; - Load(device, block.At(ArrayOffset), shape, 0, output); - } catch (const std::runtime_error& error) { - refused = std::string(error.what()).find("sampling the depth planes of depth surface") != std::string::npos; - if (!refused) throw; +void PaddedCubeCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + auto* base = block.At(PaddedCubeOffset); + DrawDepth(device, reinterpret_cast(base), {128, 128}, VK_FORMAT_D16_UNORM, DrawnD16Depth); + for (std::uint32_t face = 1; face < 6u; ++face) FillSlice(base + face * SliceBytes, SliceBytes, 0x1000u + face, 2u); + const Shape shape{Format16Uint, 128, 128, TypeCube, 0, Swizzle64KbZX}; + for (std::uint32_t face = 0; face < 6u; ++face) { + Load(device, base, shape, face, output); + Expect(output, face == 0 ? DrawnD16Bits : 0x1000u + face, "D16 cube with padded rows, face " + std::to_string(face)); } - Require(refused, "a depth plane array from base slice 1 was not refused"); } +void SmallD16CubeCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + auto* base = block.At(SmallCubeOffset); + DrawDepth(device, reinterpret_cast(base), {32, 32}, VK_FORMAT_D16_UNORM, DrawnD16Depth); + for (std::uint32_t face = 1; face < 6u; ++face) FillSlice(base + face * SliceBytes, SliceBytes, 0x1230u + face, 2u); + const Shape shape{Format16Uint, 32, 32, TypeCube, 0, Swizzle64KbZX}; + for (std::uint32_t face = 0; face < 6u; ++face) { + Load(device, base, shape, face, output); + Expect(output, face == 0 ? DrawnD16Bits : 0x1230u + face, "small D16 cube face " + std::to_string(face)); + } +} + +void PaddedD16CubeCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + constexpr std::uint32_t side = 320; + const auto mips = AgcDriver::Graphics::ComputeElementMipLayout(AgcDriver::Graphics::TextureTileMode::kZ64KBX, 2u, side, side, 1u); + Require(mips.size() == 1 && mips.front().pitchBytes == 1024 && mips.front().linearSize == 327680 && mips.front().tiledSize == 393216, "the 320 D16 regression must have a padded row pitch and face stride"); + const auto stride = AgcDriver::Graphics::DepthSliceBytes({side, side}, 2u); + Require(stride == 393216 && LargeCubeOffset + 6u * stride <= BlockBytes, "the 320 D16 cube must fit its actual tiled face stride"); + auto* base = block.At(LargeCubeOffset); + DrawDepth(device, reinterpret_cast(base), {side, side}, VK_FORMAT_D16_UNORM, DrawnD16Depth); + for (std::uint32_t face = 1; face < 6; ++face) FillSlice(base + face * stride, stride, 0x1000u + face * 0x1000u, 2u); + const Shape shape{Format16Unorm, side, side, TypeCube, 5u, Swizzle64KbZX}; + const auto check = [&](std::uint32_t face, std::uint32_t row, bool rewritten) { + Load(device, base, shape, face, output, false, row); + const auto bits = face == 0 ? DrawnD16Bits : rewritten && face == 3 ? 0x7000u : 0x1000u + face * 0x1000u; + const auto expected = static_cast(bits) / 65535.0f; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + float actual = 0; + std::memcpy(&actual, &output[tid], sizeof actual); + char message[160]; + std::snprintf(message, sizeof message, "320 D16 cube face %u row %u texel %u: got %.8f, expected %.8f", face, row, tid, actual, expected); + Require(std::abs(actual - expected) <= 2.0f / 65535.0f, message); + } + }; + for (std::uint32_t face = 0; face < 6; ++face) { + for (const auto row : {0u, 1u, 127u, 319u}) check(face, row, false); + } + FillSlice(base + 3u * stride, stride, 0x7000u, 2u); + for (std::uint32_t face = 0; face < 6; ++face) check(face, 319u, true); +} + +void BaseSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + auto* base = block.At(ArrayOffset); + Shape shape{Format32Float, 128, 128, Type2DArray, 2, Swizzle64KbZX}; + shape.baseArray = 1; + Load(device, base, shape, 0, output); + Expect(output, Bits(0.5f), "D32 2D array from base slice 1, slice 0"); + Load(device, base, shape, 1, output); + Expect(output, Bits(0.75f), "D32 2D array from base slice 1, slice 1"); + Load(device, base, shape, 0, output, true); + Expect(output, Bits(0.5f), "D32 2D array from base slice 1 through a 2D shader image"); +} + +void ExpectRefusal(AgcDriver::VulkanDevice& device, GuestBlock& block, std::size_t offset, const Shape& shape, std::uint32_t baseArray, const std::string& expected, const std::string& what) { + const auto words = TextureDescriptor(block.At(offset), shape); + auto resource = AgcDriver::Graphics::DecodeTextureResource(words); + resource.baseArray = baseArray; + try { + static_cast(AgcDriver::Graphics::DepthSurfaceTexture(AgcDriver::VulkanDeviceTestAccess::ContextOf(device), words, resource, {})); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find(expected) != std::string::npos, what + " was refused with an unexpected message: " + error.what()); + return; + } + Require(false, what + " was not refused"); +} + +void RefusalCase(AgcDriver::VulkanDevice& device, GuestBlock& block) { + ExpectRefusal(device, block, ArrayOffset, {Format32Float, 128, 128, Type2DArray, 2, Swizzle64KbZX}, 3, "base slice 3 is not implemented", "a depth plane array whose base slice is past its last slice"); + ExpectRefusal(device, block, CubeOffset, {Format32Float, 128, 128, TypeCube, 5, Swizzle64KbZX}, 1, "base slice 1 is not implemented", "a depth plane cube from base slice 1"); + ExpectRefusal(device, block, ArrayOffset, {Format32Float, 128, 128, Type2D, 0, Swizzle64KbZX}, 1, "slice 1 is not implemented", "a 2D depth plane view with a base slice"); +} } int main() { @@ -342,8 +430,13 @@ int main() { CubeCase(*device, block, output); IntegerCase(*device, block, output); DepthBitsArrayCase(*device, block, output); + DepthBitsBaseSliceCase(*device, block, output); FirstSliceCase(*device, block, output); - RefusalCase(*device, block, output); + PaddedCubeCase(*device, block, output); + SmallD16CubeCase(*device, block, output); + PaddedD16CubeCase(*device, block, output); + BaseSliceCase(*device, block, output); + RefusalCase(*device, block); std::puts("depth plane sampling tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp index 0739bd6b86..f83e70edb5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp @@ -246,6 +246,52 @@ void Run(Context context, std::uint8_t* block) { ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "with a layer pitch of", "a two-layer 2D array view of the depth plane with a layer pitch other than the depth slice size was accepted"); layeredDepth.baseArray = 1; ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "with a layer pitch of", "a 2D array view of layer 1 of the depth plane with a layer pitch other than the depth slice size was accepted"); + const auto own = StorageTexture::Create(context, *context.detiler, PlaneResource(address, Format32Float), 0); + const std::array eighth{std::bit_cast(0.125f), std::bit_cast(0.125f), std::bit_cast(0.125f), std::bit_cast(0.125f)}; + Require(own->FillClear(eighth, StorageTexture::WholeImage, refusal), refusal != nullptr ? refusal : "the storage image could not be filled"); + Require(DepthSurfaceStoreBack(context, *own), "a storage image over the depth plane that the depth surface did not hand out was not stored back"); + submit(recorder); + const auto reread = DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float), 0); + Require(reread != nullptr, "the depth plane has no storage image"); + submit(recorder); + readBack(*reread); + Require(CountFloats(block, 0.125f) == Side * Side, "a storage image written over the depth plane did not reach the depth surface"); + Require(!DepthSurfaceStoreBack(context, *StorageTexture::Create(context, *context.detiler, PlaneResource(address, Format32Float, Side / 2), 0)), "a storage image of another extent over the depth memory was stored into the depth surface"); +} + +void RunPairings(Context context, std::uint8_t* block) { + const auto address = reinterpret_cast(block); + const auto submit = [&](Recorder& recorder) { + recorder.Submit(); + Check(context.Function("vkQueueWaitIdle")(context.queue), "vkQueueWaitIdle"); + }; + Recorder recorder(context); + recorder.Activate(); + const auto readBack = [&](StorageTexture& texture) { + texture.MarkDirty(); + texture.WriteBack(); + submit(recorder); + }; + const DepthTarget first{address, address + BlockBytes / 2, {Side, Side}, VK_FORMAT_D32_SFLOAT_S8_UINT, 0.25f, 7}; + const DepthTarget second{address, address + BlockBytes / 2 + 8192, {Side, Side}, VK_FORMAT_D32_SFLOAT_S8_UINT, 0.5f, 3}; + Require(DepthSurfaceView(context, first) != VK_NULL_HANDLE && DepthSurfaceView(context, second) != VK_NULL_HANDLE, "the two pairings of the depth plane were not created"); + const auto depth = DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float), 0); + Require(depth != nullptr, "the depth plane has no storage image"); + submit(recorder); + const std::array pattern{std::bit_cast(0.75f), std::bit_cast(0.75f), std::bit_cast(0.75f), std::bit_cast(0.75f)}; + const char* refusal = nullptr; + Require(depth->FillClear(pattern, StorageTexture::WholeImage, refusal), refusal != nullptr ? refusal : "the storage image could not be filled"); + Require(DepthSurfaceStoreBack(context, *depth), "the newest pairing's storage image was not stored back"); + submit(recorder); + const auto reread = DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float), 0); + Require(reread != nullptr, "the depth plane has no storage image"); + submit(recorder); + readBack(*reread); + Require(CountFloats(block, 0.75f) == Side * Side, "a storage image stored back went into an older pairing instead of the pairing that handed it out"); + auto layered = PlaneResource(address, Format32Float); + layered.dimension = TextureDimension::k2DArray; + Require(DepthSurfaceStorage(context, {}, layered, 0) != nullptr, "a one-layer 2D array view of the depth plane was not taken"); + ExpectFailure([&] { static_cast(DepthSurfaceStoreBack(context, *depth)); }, "through a view that another view of the plane", "a store back through a replaced view was taken by an older pairing that covers the plane"); } } @@ -269,6 +315,8 @@ int main() { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); Run(context, block); ClearDepthSurfaces(context.device); + RunPairings(context, block); + ClearDepthSurfaces(context.device); } GuestAllocations::Mutation().Remove(block); std::cout << "depth surface storage access tests passed\n"; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthSurfaceFromStorage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthSurfaceFromStorage.cpp new file mode 100644 index 0000000000..fdd4bbe36a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthSurfaceFromStorage.cpp @@ -0,0 +1,205 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.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 "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using DepthFastClearHarness::Covered; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::HtileBytes; +using DepthFastClearHarness::Require; +using DepthFastClearHarness::Width; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Format16Unorm = 7; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t TileDepth64KB = 0x18; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Type2DArray = 13; +constexpr std::size_t PlaneBytes = 65536; +constexpr std::size_t BlockBytes = 9 * PlaneBytes; +constexpr std::uint32_t RowPasses = 15; + +alignas(256) constexpr std::array StoreRowCode{ + 0x7e280d00, 0x102828ff, 0x3cf80000, 0x7e3c0300, 0x7e3e0280, 0xf0201108, 0x0001141e, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array StoreRowLayerCode{ + 0x7e280d00, 0x102828ff, 0x3cf80000, 0x7e3c0300, 0x7e3e0280, 0x7e400280, 0xf0201128, 0x0001141e, 0xbf810000, 0xbf810000, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, BlockBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast(aligned + BlockBytes), begin + alignment - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); + } +#endif + Require(block != nullptr, "depth surface from storage: cannot allocate the guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + AgcDriver::GuestMemory::CollectWritesUncached(reinterpret_cast(block), BlockBytes); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); +#else + munmap(block, BlockBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, BlockBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Plane(std::size_t index) { return block + index * PlaneBytes; } + +private: + std::uint8_t* block = nullptr; +}; + +std::uint64_t Address(const std::uint8_t* plane) { + return static_cast(reinterpret_cast(plane)); +} + +std::array StorageView(std::uint64_t address, std::uint32_t format, std::uint32_t layers) { + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + 0xfacu | (TileDepth64KB << 20u) | ((layers > 1u ? Type2DArray : Type2D) << 28u), + layers - 1u, 0u, 0u, 0u, + }; +} + +void Fill(std::uint8_t* plane, VkFormat format) { + if (format == VK_FORMAT_D16_UNORM) { + const std::uint16_t quarter = 0x4000u; + for (std::size_t offset = 0; offset < PlaneBytes; offset += sizeof(quarter)) std::memcpy(plane + offset, &quarter, sizeof(quarter)); + return; + } + const float quarter = 0.25f; + for (std::size_t offset = 0; offset < PlaneBytes; offset += sizeof(quarter)) std::memcpy(plane + offset, &quarter, sizeof(quarter)); +} + +void StoreRow(AgcDriver::VulkanDevice& device, std::uint8_t* plane, VkFormat format, std::uint32_t layers) { + std::vector userData(16, 0u); + const auto view = StorageView(Address(plane), format == VK_FORMAT_D16_UNORM ? Format16Unorm : Format32Float, layers); + std::copy(view.begin(), view.end(), userData.begin() + 4); + const std::span code = layers > 1u ? std::span(StoreRowLayerCode) : std::span(StoreRowCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto program = ShaderRecompiler::Recompile(request); + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +DepthFastClearHarness::Surface Plane(std::uint8_t* plane, VkFormat format, std::uint64_t htile = 0) { + return {Address(plane), 0, htile, false, format}; +} + +std::vector failures; + +void RequireCount(std::uint32_t covered, std::uint32_t expected, const std::string& what) { + if (covered != expected) failures.push_back(what + ": " + std::to_string(covered) + " pixels passed, expected " + std::to_string(expected)); +} + +void NewSurfaceCase(AgcDriver::VulkanDevice& device, const DepthFastClearHarness::Shaders& shaders, std::uint8_t* plane, VkFormat format, bool flushed, const std::string& what, std::uint32_t layers = 1) { + for (std::uint32_t layer = 0; layer < layers; ++layer) Fill(plane + layer * PlaneBytes, format); + StoreRow(device, plane, format, layers); + if (layers > 1u) { + Require(AgcDriver::Graphics::StorageTexture::FindPending(Address(plane), PlaneBytes) != nullptr && AgcDriver::Graphics::StorageTexture::FindLive(Address(plane), PlaneBytes) == nullptr, what + ": the stored row is not pending in a storage image larger than the plane, or a cached image of the plane's size holds it"); + } else if (flushed) { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(plane), PlaneBytes, nullptr, "test"); + device.WaitIdle(); + Require(AgcDriver::Graphics::StorageTexture::FindPending(Address(plane), PlaneBytes) == nullptr && AgcDriver::Graphics::StorageTexture::FindLive(Address(plane), PlaneBytes) != nullptr, what + ": the stored row is not held by a flushed, live storage image"); + } else { + Require(AgcDriver::Graphics::StorageTexture::FindPending(Address(plane), PlaneBytes) != nullptr, what + ": the stored row is not pending in a storage image"); + } + RequireCount(DepthFastClearHarness::Draw(device, shaders, Plane(plane, format), {}), RowPasses, what); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto shaders = DepthFastClearHarness::Compile(*device); + GuestBlock block; + NewSurfaceCase(*device, shaders, block.Plane(0), VK_FORMAT_D32_SFLOAT, false, "a new D32 depth surface over a pending R32F storage write"); + NewSurfaceCase(*device, shaders, block.Plane(1), VK_FORMAT_D16_UNORM, false, "a new D16 depth surface over a pending R16 unorm storage write"); + NewSurfaceCase(*device, shaders, block.Plane(2), VK_FORMAT_D32_SFLOAT, true, "a new D32 depth surface over a flushed R32F storage write"); + NewSurfaceCase(*device, shaders, block.Plane(7), VK_FORMAT_D32_SFLOAT, false, "a new D32 depth surface over a pending R32F storage write through a two-layer 2D array view", 2); + + auto* cleared = block.Plane(3); + const auto htile = Address(block.Plane(4)); + Fill(cleared, VK_FORMAT_D32_SFLOAT); + StoreRow(*device, cleared, VK_FORMAT_D32_SFLOAT, 1); + AgcDriver::Graphics::NoteHtileDepthClear(htile, DepthFastClearHarness::HtileBytes); + RequireCount(DepthFastClearHarness::Draw(*device, shaders, Plane(cleared, VK_FORMAT_D32_SFLOAT, htile), {}), Covered, "a new D32 depth surface whose HTILE was cleared after a storage write"); + + auto* stenciled = block.Plane(5); + const DepthFastClearHarness::Surface depthStencil{Address(stenciled), Address(block.Plane(6)), 0, false, VK_FORMAT_D32_SFLOAT_S8_UINT}; + constexpr VkStencilOpState equalZero{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, 0xffu, 0u, 0u}; + Fill(stenciled, VK_FORMAT_D32_SFLOAT); + StoreRow(*device, stenciled, VK_FORMAT_D32_SFLOAT, 1); + RequireCount(DepthFastClearHarness::Draw(*device, shaders, depthStencil, {}), RowPasses, "a new D32S8 depth surface over a pending R32F storage write"); + RequireCount(DepthFastClearHarness::Draw(*device, shaders, depthStencil, {.depthTest = false, .stencilTest = true, .stencil = equalZero}), Covered, "the stencil plane of a new D32S8 depth surface that took a storage write, tested EQUAL to its clear value 0"); + + if (!failures.empty()) { + std::string text; + for (const auto& failure : failures) text += failure + "\n"; + throw std::runtime_error(text); + } + std::puts("depth surface from storage 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/DrawCacheReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp index 23fbd3392b..516e85a915 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp @@ -1,6 +1,8 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "VulkanTestDevice.hpp" +#include #include #include #include @@ -21,8 +23,14 @@ using AgcDriver::DriverDetail::DrawMiss; using AgcDriver::DriverDetail::DrawKeySightings; alignas(256) constexpr std::array VertexCode{0x7e000208u, 0x7e020280u, 0x7e040280u, 0x7e0602f2u, 0xf80008cfu, 0x03020100u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array TriangleVertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, +}; alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array ConstantPixelCode{0xf4000080u, 0xfa000000u, 0xbf8cc07fu, 0x7e000202u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u}; alignas(256) std::array ColorMemory{}; +alignas(256) std::array PixelConstant{}; struct Header { Shader shader{}; @@ -31,6 +39,8 @@ struct Header { alignas(8) Header VertexHeader{}; alignas(8) Header PixelHeader{}; +alignas(8) Header TriangleVertexHeader{}; +alignas(8) Header ConstantPixelHeader{}; const Shader* Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { header.shader.file_header = 0x34333231u; @@ -49,10 +59,10 @@ void Append(std::vector& commands, std::uint32_t opcode, std::ini commands.insert(commands.end(), payload); } -std::vector DrawThrice() { +std::vector Draws(float scale, const std::uint32_t* vertexCode, std::uint32_t vertexUserSgprs, const std::uint32_t* pixelCode, std::uint32_t pixelUserSgprs, const std::vector>& pixelWords) { const auto color = reinterpret_cast(ColorMemory.data()); - const auto vertex = reinterpret_cast(VertexCode.data()); - const auto pixel = reinterpret_cast(PixelCode.data()); + const auto vertex = reinterpret_cast(vertexCode); + const auto pixel = reinterpret_cast(pixelCode); const std::vector> context{ {0x2d5, 0x2000}, {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, @@ -69,33 +79,57 @@ std::vector DrawThrice() { {0x90, 0x80000000}, {0x91, 0x40040}, {0x94, 0x80000000}, {0x95, 0x40040}, {0x318, static_cast(color >> 8u)}, {0x390, static_cast(color >> 40u)}, - {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x10f, std::bit_cast(scale)}, {0x110, std::bit_cast(scale)}, {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)}, + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, vertexUserSgprs << 1u}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, pixelUserSgprs << 1u}, }; 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}); - Append(commands, 0x2d, {3, 2}); - Append(commands, 0x2d, {3, 2}); - Append(commands, 0x2d, {3, 2}); + for (const auto& words : pixelWords) { + for (std::uint32_t k = 0; k < words.size(); ++k) Append(commands, 0x76, {0x00c + k, words[k]}); + Append(commands, 0x2d, {3, 2}); + } return commands; } +AgcDriver::DriverDetail::DrawEntryCounters Submit(std::vector commands) { + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + return AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); +} + +AgcDriver::DriverDetail::DrawEntryCounters Submit(float scale, std::initializer_list pixelWords) { + std::vector> words; + for (const auto word : pixelWords) words.push_back({std::bit_cast(word)}); + return Submit(Draws(scale, VertexCode.data(), 1, PixelCode.data(), 1, words)); +} + +AgcDriver::DriverDetail::DrawEntryCounters SubmitConstantDraws(std::size_t draws) { + const auto constant = reinterpret_cast(PixelConstant.data()); + const std::vector> words(draws, {static_cast(constant), static_cast(constant >> 32u)}); + return Submit(Draws(32.0f, TriangleVertexCode.data(), 0, ConstantPixelCode.data(), 2, words)); +} + +unsigned BrightestColorByte() { + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(ColorMemory.data()), ColorMemory.size()); + unsigned brightest = 0; + for (const auto value : ColorMemory) brightest = std::max(brightest, static_cast(value)); + return brightest; +} + void ThirdDrawHitsTheCache(bool libraries) { Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); - auto commands = DrawThrice(); - Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; - Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); - AgcDriverWaitIdle_nid_postfix(); - const auto counters = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + const auto counters = Submit(32.0f, {1.0f, 1.0f, 1.0f}); const auto mode = std::string(libraries ? "with" : "without") + " graphics pipeline libraries"; const auto layout = counters.misses[static_cast(DrawMiss::Layout)]; Require(counters.lookups == 3 && counters.absent == 2, mode + ": the three draws made " + std::to_string(counters.lookups) + " draw-cache lookups (" + std::to_string(counters.absent) + " without an entry), expected 3 lookups with 2 absent"); @@ -104,6 +138,84 @@ void ThirdDrawHitsTheCache(bool libraries) { Require(counters.hits == 1, mode + ": the repeated draw did not reuse the cached draw (" + std::to_string(counters.hits) + " hits)"); } +void UserDataDrawsReuseTheirShape(bool libraries) { + const auto mode = std::string(libraries ? "with" : "without") + " graphics pipeline libraries"; + const bool verify = std::getenv("APS5_VERIFY_DRAW_DATA_HITS") != nullptr; + const auto before = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + const auto known = Submit(32.0f, {2.0f, 3.0f, 4.0f}); + Require(known.lookups - before.lookups == 3 && known.absent - before.absent == 3, mode + ": three draws with new user words did not miss the draw key three times"); + Require(known.dataHits - before.dataHits == 3, mode + ": " + std::to_string(known.dataHits - before.dataHits) + " of three draws that differ from a cached draw only in user words reused its decode, expected 3"); + Require(known.inserts == before.inserts, mode + ": a new key of a shape that has an entry was cached at its first sight"); + const auto stages = known.dataStagesReused + known.dataStagesCompiled - before.dataStagesReused - before.dataStagesCompiled; + Require(stages == 6, mode + ": the three data-only hits accounted " + std::to_string(stages) + " stages, expected 6"); + Require(verify ? known.dataVerified - before.dataVerified == 3 : known.dataStagesReused - before.dataStagesReused == 3, mode + ": the data-only hits reused " + std::to_string(known.dataStagesReused - before.dataStagesReused) + " stages and verified " + std::to_string(known.dataVerified - before.dataVerified) + ", expected the 3 vertex stages, whose user words did not change"); + const auto fresh = Submit(16.0f, {5.0f, 6.0f, 7.0f}); + Require(fresh.inserts - known.inserts == 1, mode + ": three keys of a new shape inserted " + std::to_string(fresh.inserts - known.inserts) + " draw entries, expected 1: the shape's second key"); + Require(fresh.dataHits - known.dataHits == 1, mode + ": the third key of a new shape made " + std::to_string(fresh.dataHits - known.dataHits) + " data-only hits, expected 1"); +} + +void StageDataHitsRefreshTheirWords(bool libraries) { + const auto mode = std::string(libraries ? "with" : "without") + " graphics pipeline libraries"; + const bool verify = std::getenv("APS5_VERIFY_DRAW_DATA_HITS") != nullptr; + const bool enabled = std::getenv("APS5_NO_DRAW_STAGE_DATA_HITS") == nullptr; + Registered(TriangleVertexHeader, TriangleVertexCode.data(), sizeof(TriangleVertexCode), 2); + Registered(ConstantPixelHeader, ConstantPixelCode.data(), sizeof(ConstantPixelCode), 1); + ColorMemory.fill(std::byte{0}); + PixelConstant[0] = 0.25f; + const auto before = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + const auto cached = SubmitConstantDraws(3); + Require(cached.inserts - before.inserts == 1 && cached.hits - before.hits == 1, mode + ": three draws of a pixel shader reading a constant through its user words did not insert once and hit once"); + Require(!enabled || cached.stageDataInserts - before.stageDataInserts == 1, mode + ": the inserted draw recorded " + std::to_string(cached.stageDataInserts - before.stageDataInserts) + " stages with data positions, expected the fragment stage"); + Require(BrightestColorByte() == 64, mode + ": the draws of constant 0.25 wrote " + std::to_string(BrightestColorByte()) + ", expected 64"); + PixelConstant[0] = 1.0f; + const auto refreshed = SubmitConstantDraws(1); + const auto fragmentMisses = refreshed.misses[static_cast(DrawMiss::FragmentDiffering)] - cached.misses[static_cast(DrawMiss::FragmentDiffering)]; + if (!enabled) { + Require(fragmentMisses == 1, mode + ": with stage data hits off, a changed constant did not miss on the fragment stage"); + } else if (verify) { + Require(refreshed.stageDataVerified - cached.stageDataVerified == 1, mode + ": the stage data hit was verified " + std::to_string(refreshed.stageDataVerified - cached.stageDataVerified) + " times, expected once"); + } else { + Require(refreshed.hits - cached.hits == 1 && fragmentMisses == 0, mode + ": a draw whose constant changed missed the cached draw"); + Require(refreshed.stageDataHits - cached.stageDataHits == 1 && refreshed.stageDataWords - cached.stageDataWords == 1, mode + ": the hit refreshed " + std::to_string(refreshed.stageDataWords - cached.stageDataWords) + " words in " + std::to_string(refreshed.stageDataHits - cached.stageDataHits) + " stages, expected 1 word in the fragment stage"); + Require(refreshed.inserts == cached.inserts, mode + ": a stage data hit inserted a draw entry"); + } + Require(BrightestColorByte() == 255, mode + ": the draw of constant 1.0 wrote " + std::to_string(BrightestColorByte()) + ", expected 255"); +} + +void RetainedPixelCacheTransitions() { + auto commands = Draws(32.0f, VertexCode.data(), 1, PixelCode.data(), 1, {}); + Append(commands, 0x76, {0x00c, std::bit_cast(1.0f)}); + const auto draw = [&] { + for (unsigned i = 0; i < 3; ++i) Append(commands, 0x2d, {3, 2}); + }; + const auto skipped = [&] { + Append(commands, 0x69, {0x8e, 0, 0}); + Append(commands, 0x69, {0x203, 0}); + Append(commands, 0x69, {0x2dc, 0xaa00}); + draw(); + }; + Append(commands, 0x69, {0x1c4, 1}); + skipped(); + Append(commands, 0x69, {0x8e, 0xf, 0xf}); + draw(); + skipped(); + Append(commands, 0x69, {0x203, 0x400}); + draw(); + skipped(); + Append(commands, 0x69, {0x8e, 0xf, 0xf}); + Append(commands, 0x69, {0x2dc, 0xaa01}); + draw(); + skipped(); + const auto before = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "retained pixel cache transitions failed to submit"); + AgcDriverWaitIdle_nid_postfix(); + const auto after = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Require(after.lookups - before.lookups == 21 && after.absent - before.absent == 8 && after.inserts - before.inserts == 4 && after.hits - before.hits == 13, "color, execution or alpha-to-coverage transitions reused the wrong cached draw"); + if (std::getenv("APS5_VERIFY_DRAW_RECIPE") != nullptr) Require(after.verifyDecodes > before.verifyDecodes, "pixel cache transitions did not verify any cached decode"); + Require(after.verifyDecodeMismatches == before.verifyDecodeMismatches && after.facadeMismatches == before.facadeMismatches, "cached pixel transitions disagreed with fresh decoding"); +} + void SightingsKeepCollidingKeys() { DrawKeySightings sightings(DrawKeySightings::Ways); Require(!sightings.SeenBefore(0) && sightings.SeenBefore(0), "draw key 0 was not admitted at its second sight"); @@ -127,6 +239,9 @@ int main() { } if (!libraries && std::getenv("APS5_NO_GPL") == nullptr) return VulkanTestSkipped; ThirdDrawHitsTheCache(libraries); + UserDataDrawsReuseTheirShape(libraries); + StageDataHitsRefreshTheirWords(libraries); + RetainedPixelCacheTransitions(); AgcDriverShutdown_nid_postfix(); std::cout << "A repeated draw reused the cached draw " << (libraries ? "with" : "without") << " graphics pipeline libraries\n"; return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawUnsupportedBufferType.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawUnsupportedBufferType.cpp new file mode 100644 index 0000000000..2c4ef6537e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawUnsupportedBufferType.cpp @@ -0,0 +1,112 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +alignas(256) std::array Pixels{}; +alignas(256) std::array Source{}; +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, +}; +alignas(256) constexpr std::array PixelCode{ + 0x7e020280u, 0xe0301000u, 0x80000201u, 0xbf8c3f70u, 0x7e000280u, 0x7e0602f2u, 0xf800180fu, 0x03010002u, 0xbf810000u, +}; + +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); +} + +void Draw(const std::array& descriptor) { + 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}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {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, 0u}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 4u << 1u}, + {0x00c, descriptor[0]}, {0x00d, descriptor[1]}, {0x00e, descriptor[2]}, {0x00f, descriptor[3]}, + }; + 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 color = reinterpret_cast(Pixels.data()); + Append(commands, 0x69, {0x318, static_cast(color >> 8u)}); + Append(commands, 0x69, {0x390, static_cast(color >> 40u)}); + Append(commands, 0x2d, {3, 2}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "unsupported buffer type draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + +void NamesShaderAndDescriptor() { + const auto address = reinterpret_cast(Source.data()); + const std::array descriptor{static_cast(address), static_cast((address >> 32u) & 0xffffu), sizeof(Source), 0xa1b00facu}; + try { + Draw(descriptor); + } catch (const std::exception& error) { + const std::string message = error.what(); + char shader[64]; + std::snprintf(shader, sizeof(shader), "shader 0x%llx stage %u", static_cast(reinterpret_cast(PixelCode.data())), static_cast(ShaderRecompiler::ShaderStage::Fragment)); + char words[64]; + std::snprintf(words, sizeof(words), "(buffer 0 V# %08x %08x %08x %08x)", descriptor[0], descriptor[1], descriptor[2], descriptor[3]); + Require(message.find("buffer descriptor uses an unsupported type") != std::string::npos, "draw failed for another reason: " + message); + Require(message.find(shader) != std::string::npos, std::string("draw error does not name ") + shader + ": " + message); + Require(message.find(words) != std::string::npos, std::string("draw error does not name ") + words + ": " + message); + return; + } + throw std::runtime_error("a pixel shader read a buffer through a descriptor of type 2"); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + NamesShaderAndDescriptor(); + try { AgcDriverShutdown_nid_postfix(); } catch (const std::exception&) {} + std::puts("draw unsupported buffer type tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { AgcDriverShutdown_nid_postfix(); } catch (const std::exception&) {} + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FlipStoresSmallResults.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FlipStoresSmallResults.cpp new file mode 100644 index 0000000000..bbac636c99 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FlipStoresSmallResults.cpp @@ -0,0 +1,155 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +constexpr std::uint32_t Threads = 8; +constexpr std::uint32_t Side = 16; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t OutputHandle = 7; +constexpr std::size_t BlockBytes = 0x20000; +constexpr std::uint32_t Untouched = 0xdeadbeefu; +constexpr std::uint32_t Written = 0x12345678u; + +alignas(256) constexpr std::array StoreCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, Written, 0xf0200108, 0x00020102, 0xbf810000, +}; + +alignas(4) std::atomic Gate = 0; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::uint32_t Low(const void* data) { + return static_cast(AddressOf(data)); +} + +std::uint32_t High(const void* data) { + return static_cast(AddressOf(data) >> 32u); +} + +std::array TextureDescriptor(const std::uint32_t* texels) { + const auto address = AddressOf(texels); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +std::vector Frame(const std::uint32_t* texels) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(texels); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::vector words; + const auto registers = [&](const std::uint32_t first, const std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{Threads, 1, 1}; + registers(0x207, shape); + const std::array program{static_cast(AddressOf(StoreCode.data()) >> 8u), static_cast(AddressOf(StoreCode.data()) >> 40u)}; + registers(0x20c, program); + const std::array resource{static_cast(userData.size()) << 1u}; + registers(0x213, resource); + registers(0x240, userData); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u}); + words.insert(words.end(), {AgcDriver::FlipPacketHeader, OutputHandle, 0xfffffffeu, 1, 0, 0}); + words.insert(words.end(), {0xc0053c00u, 0x13u, Low(&Gate), High(&Gate), 1u, 0xffffffffu, 0x19u}); + return words; +} + +class Request final : public AgcDriver::IFlipRequest { +public: + explicit Request(std::atomic& ready) : ready(ready) {} + void GpuReady(const std::shared_ptr&) override { ++ready; } + void Fail(std::exception_ptr) noexcept override {} +private: + std::atomic& ready; +}; + +class Output final : public AgcDriver::IVideoOutput { +public: + std::shared_ptr Reserve(const AgcDriver::FlipInfo&) override { return std::make_shared(ready); } + void Fail(std::exception_ptr) noexcept override {} + std::atomic ready = 0; +}; + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + std::vector storage((BlockBytes + 0x10000u) / 4u); + auto* texels = reinterpret_cast((reinterpret_cast(storage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}); + std::fill(texels, texels + BlockBytes / 4u, Untouched); + { + GuestAllocations::Mutation mutation; + mutation.Add(texels, BlockBytes, true, true, true); + } + const auto bytes = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(texels))).guestBytes; + Require(bytes <= AgcDriver::Graphics::StorageTexture::SmallStoreBytes, "the test image is larger than an image the flip stores"); + const volatile std::uint32_t* cpu = texels; + const auto stored = [&] { + for (std::size_t index = 0; index < bytes / 4u; ++index) { + if (cpu[index] == Written) return true; + } + return false; + }; + auto output = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(OutputHandle, output); + + auto words = Frame(texels); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while ((output->ready.load() == 0 || !stored()) && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + const bool flipped = output->ready.load() == 1; + const bool seen = stored(); + Gate.store(1, std::memory_order_release); + + AgcDriverWaitIdle_nid_postfix(); + AgcDriverUnregisterVideoOutput_nid_postfix(OutputHandle, output); + AgcDriverShutdown_nid_postfix(); + { + GuestAllocations::Mutation mutation; + mutation.Remove(texels); + } + Require(flipped, "the flip did not reach the GPU"); + Require(seen, "a small image written before a flip did not reach guest memory while the queue kept working"); + std::puts("flip stores small results 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/FrontFaceAllBits.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FrontFaceAllBits.cpp new file mode 100644 index 0000000000..d3f9b23635 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FrontFaceAllBits.cpp @@ -0,0 +1,133 @@ +#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 Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t WaveSize = 64; +constexpr std::uint32_t Kept = 0x12345678u; +alignas(256) std::array Pixels{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0xf8001801, 0x00000000, 0xbf810000, +}; + +constexpr std::array, 6> Corners{{ + {1.0f, 1.0f}, {1.0f, 15.0f}, {15.0f, 1.0f}, + {17.0f, 1.0f}, {31.0f, 1.0f}, {17.0f, 15.0f}, +}}; + +alignas(256) std::array, Corners.size()> Vertices{}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void Draw(AgcDriver::VulkanDevice& device, bool allBits) { + Pixels.fill(Kept); + for (std::size_t index = 0; index < Corners.size(); ++index) { + Vertices[index] = {Corners[index][0] * 2.0f / Width - 1.0f, 1.0f - Corners[index][1] * 2.0f / Height, 0.5f, 1.0f}; + } + 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()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::FrontFace); + pixel.frontFace = true; + pixel.frontFaceAllBits = allBits; + pixel.targetOutputMode[0] = 1; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R32_UINT, sizeof(Pixels), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Vertices.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(std::uint32_t front, std::uint32_t back, std::string_view what) { + for (const auto& [x, y, isFront] : std::array, 2>{{{4, 4, true}, {20, 4, false}}}) { + const auto expected = isFront ? front : back; + const auto actual = Pixels[y * Width + x]; + char text[160]; + std::snprintf(text, sizeof(text), "%.*s: the %s-facing triangle read FRONT_FACE as 0x%08x, expected 0x%08x", static_cast(what.size()), what.data(), isFront ? "front" : "back", actual, expected); + Require(actual == expected, text); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Draw(*device, false); + Check(0x3f800000u, 0xbf800000u, "without FRONT_FACE_ALL_BITS"); + Draw(*device, true); + Check(0xffffffffu, 0u, "with FRONT_FACE_ALL_BITS"); + std::puts("front face all-bits 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/GdsAppendConsume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GdsAppendConsume.cpp index 0ee7d0c786..9ac9376530 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GdsAppendConsume.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GdsAppendConsume.cpp @@ -26,6 +26,8 @@ constexpr std::array Starts{1000, 2000, 3000, 4000}; constexpr std::uint32_t UntouchedAppend = 0x5eed0001u; constexpr std::uint32_t UntouchedConsume = 0x5eed0002u; constexpr std::size_t ActiveLanesLiteral = 12; +constexpr std::size_t OrderedM0Literal = 3; +constexpr std::array OrderedM0{0u, 7u}; alignas(256) std::array Output{}; alignas(16) std::array CounterReadback{}; @@ -44,6 +46,16 @@ alignas(256) auto Code = std::to_array({ 0xbf810000, }); +alignas(256) auto OrderedCode = std::to_array({ + 0x34060082, + 0x4a020083, + 0xbefc03ff, 0x0000ffff, + 0xd8fe0310, 0x05000001, + 0xbf8cc07f, + 0xe0702000, 0x80000503, + 0xbf810000, +}); + struct Workgroup { std::uint32_t threads; std::uint32_t waveSize; @@ -83,13 +95,11 @@ std::array ReadCounters(AgcDriver::QueueState& queue) { return CounterReadback; } -void Run(AgcDriver::VulkanDevice& device, const Workgroup& workgroup) { +void Run(AgcDriver::VulkanDevice& device, const Workgroup& workgroup, std::span code) { Output.fill(0xdeadbeefu); - Code[ActiveLanesLiteral] = workgroup.activeLanes; std::vector userData(4, 0u); const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); std::copy(output.begin(), output.end(), userData.begin()); - const std::span code(Code); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; const ShaderRecompiler::ShaderComputeStageInfo compute{{workgroup.threads, 1, 1}, 0, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ @@ -134,6 +144,28 @@ void CheckWaves(const Workgroup& workgroup, std::uint32_t result, std::uint32_t } } +void CheckOrdered(const Workgroup& workgroup, std::uint32_t m0, const std::array& counters) { + const auto waves = workgroup.threads / workgroup.waveSize; + const auto what = workgroup.Name() + " ds_ordered_count with M0 " + std::to_string(m0); + std::vector order(waves); + for (std::uint32_t wave = 0; wave < waves; ++wave) { + order[wave] = wave; + const auto first = wave * workgroup.waveSize; + for (std::uint32_t lane = first; lane < first + workgroup.waveSize; ++lane) { + const auto actual = Output[lane * Results]; + Require(actual == Output[first * Results], what + ": lane " + std::to_string(lane) + " returned " + std::to_string(actual) + ", its wave's first lane " + std::to_string(Output[first * Results])); + } + } + std::sort(order.begin(), order.end(), [&](std::uint32_t left, std::uint32_t right) { return Output[left * workgroup.waveSize * Results] < Output[right * workgroup.waveSize * Results]; }); + auto expected = Starts[0]; + for (const auto wave : order) { + const auto first = wave * workgroup.waveSize; + Require(Output[first * Results] == expected, what + ": wave " + std::to_string(wave) + " returned " + std::to_string(Output[first * Results]) + ", expected " + std::to_string(expected)); + expected += first + 3u; + } + Require(counters[0] == expected, what + ": GDS counter 0 is " + std::to_string(counters[0]) + ", expected " + std::to_string(expected)); +} + void Check(const Workgroup& workgroup, std::uint32_t pass, const std::array& counters) { const auto active = std::min(workgroup.activeLanes, workgroup.threads); const std::array steps{workgroup.threads, workgroup.threads, active, active}; @@ -164,10 +196,17 @@ int main() { for (const auto& workgroup : Workgroups) { SetCounters(queue); Require(ReadCounters(queue) == Starts, workgroup.Name() + ": the GDS counters did not read back as set"); + Code[ActiveLanesLiteral] = workgroup.activeLanes; for (std::uint32_t pass = 0; pass < 2; ++pass) { - Run(*device, workgroup); + Run(*device, workgroup, Code); Check(workgroup, pass, ReadCounters(queue)); } + for (const auto m0 : OrderedM0) { + SetCounters(queue); + OrderedCode[OrderedM0Literal] = m0; + Run(*device, workgroup, OrderedCode); + CheckOrdered(workgroup, m0, ReadCounters(queue)); + } } std::puts("gds append consume tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoads.cpp index 0c288dbb13..b13c0f3fb7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalLoads.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "Recompiler.hpp" @@ -16,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -143,7 +145,15 @@ void Dispatch(AgcDriver::VulkanDevice& device, std::span co }; request.useCache = false; const auto result = ShaderRecompiler::Recompile(request); - device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + const auto before = AgcDriver::VulkanDevice::ComputePipelineCounts(); + auto prepared = device.PrepareDispatch(result, {}, Threads, reinterpret_cast(code.data())); + const auto after = AgcDriver::VulkanDevice::ComputePipelineCounts(); + Require(after.created == before.created + 1 && after.underGpuMutex == before.underGpuMutex, "global loads: the address-table program's pipeline was not created before the GPU mutex"); + { + std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data()), std::move(prepared)); + } + Require(AgcDriver::VulkanDevice::ComputePipelineCounts().created == after.created, "global loads: the dispatch created a second pipeline"); device.WaitIdle(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GpuBufferFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GpuBufferFormat.cpp new file mode 100644 index 0000000000..423fc3d9a6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GpuBufferFormat.cpp @@ -0,0 +1,678 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Lanes = 2; +constexpr std::uint32_t Results = 12; +constexpr std::uint32_t OutputStride = 16; +constexpr std::uint32_t Untouched = 0xdeadbeefu; +constexpr std::size_t BlockBytes = 65536; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "gpu buffer format: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + const std::array initialData{ + 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0xc0490fdbu, 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0x00000001u + }; + std::copy(initialData.begin(), initialData.end(), Data().begin()); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::span Descriptor() { + return std::span(reinterpret_cast(block), 4); + } + + std::span Data() { + return std::span(reinterpret_cast(block + 256), 4 * Lanes); + } + + std::span Output() { + return std::span(reinterpret_cast(block + 512), OutputStride * Lanes); + } + +private: + std::uint8_t* block = nullptr; +}; + +alignas(256) constexpr std::array Code{ + 0xe0340000u, 0x80010200u, 0xbf8c3f70u, 0x7e140502u, 0x7e160503u, 0x34020084u, 0x34280086u, 0xe00c1000u, + 0x80020401u, 0xe0001000u, 0x80020801u, 0xe0041000u, 0x80020901u, 0xe0081000u, 0x80020b01u, 0xe20c1000u, + 0x80020e01u, 0xbf8c3f70u, 0xe0781000u, 0x80030414u, 0xe0701010u, 0x80030814u, 0xe0741014u, 0x80030914u, + 0xe07c101cu, 0x80030b14u, 0xe0741028u, 0x80030e14u, 0xbf810000u, +}; + +struct Case { + std::uint32_t records; + std::uint32_t word3; + std::array expected; +}; +constexpr std::array Cases{{ + {32u, 0x31001facu, { + 0x3b808081u, 0x3b808081u, 0x3b808081u, 0x3b808081u, 0x3b808081u, 0x3b808081u, + 0x3b808081u, 0x3b808081u, 0x3b808081u, 0x3b808081u, 0x1c041c04u, 0x1c041c04u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + }}, + {32u, 0x31002facu, { + 0x3c010204u, 0x3c010204u, 0x3c010204u, 0x3c010204u, 0x3c010204u, 0x3c010204u, + 0x3c010204u, 0x3c010204u, 0x3c010204u, 0x3c010204u, 0x20082008u, 0x20082008u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + }}, + {32u, 0x31003facu, { + 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, + 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3c003c00u, 0x3c003c00u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + }}, + {32u, 0x31004facu, { + 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, + 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x3c003c00u, 0x3c003c00u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + }}, + {32u, 0x31005facu, { + 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, + 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00010001u, 0x00010001u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + }}, + {32u, 0x31006facu, { + 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, + 0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u, 0x00010001u, 0x00010001u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + }}, + {32u, 0x31007facu, { + 0x3f7f01ffu, 0x3f7f01ffu, 0x3f7f01ffu, 0x3f7f01ffu, 0x3f7f01ffu, 0x3f7f01ffu, + 0x3f7f01ffu, 0x3f7f01ffu, 0x3f7f01ffu, 0x3f7f01ffu, 0x3bf83bf8u, 0x3bf83bf8u, + 0x3ef800f8u, 0x3ef800f8u, 0x3ef800f8u, 0x3ef800f8u, 0x3ef800f8u, 0x3ef800f8u, + 0x3ef800f8u, 0x3ef800f8u, 0x3ef800f8u, 0x3ef800f8u, 0x37c037c0u, 0x37c037c0u, + }}, + {32u, 0x31008facu, { + 0xbbff01feu, 0xbbff01feu, 0xbbff01feu, 0xbbff01feu, 0xbbff01feu, 0xbbff01feu, + 0xbbff01feu, 0xbbff01feu, 0xbbff01feu, 0xbbff01feu, 0x9ff89ff8u, 0x9ff89ff8u, + 0x3f7801f0u, 0x3f7801f0u, 0x3f7801f0u, 0x3f7801f0u, 0x3f7801f0u, 0x3f7801f0u, + 0x3f7801f0u, 0x3f7801f0u, 0x3f7801f0u, 0x3f7801f0u, 0x3bc03bc0u, 0x3bc03bc0u, + }}, + {32u, 0x31009facu, { + 0x477f0100u, 0x477f0100u, 0x477f0100u, 0x477f0100u, 0x477f0100u, 0x477f0100u, + 0x477f0100u, 0x477f0100u, 0x477f0100u, 0x477f0100u, 0x7bf87bf8u, 0x7bf87bf8u, + 0x46f80000u, 0x46f80000u, 0x46f80000u, 0x46f80000u, 0x46f80000u, 0x46f80000u, + 0x46f80000u, 0x46f80000u, 0x46f80000u, 0x46f80000u, 0x77c077c0u, 0x77c077c0u, + }}, + {32u, 0x3100afacu, { + 0xc37f0000u, 0xc37f0000u, 0xc37f0000u, 0xc37f0000u, 0xc37f0000u, 0xc37f0000u, + 0xc37f0000u, 0xc37f0000u, 0xc37f0000u, 0xc37f0000u, 0xdbf8dbf8u, 0xdbf8dbf8u, + 0x46f80000u, 0x46f80000u, 0x46f80000u, 0x46f80000u, 0x46f80000u, 0x46f80000u, + 0x46f80000u, 0x46f80000u, 0x46f80000u, 0x46f80000u, 0x77c077c0u, 0x77c077c0u, + }}, + {32u, 0x3100bfacu, { + 0x0000ff01u, 0x0000ff01u, 0x0000ff01u, 0x0000ff01u, 0x0000ff01u, 0x0000ff01u, + 0x0000ff01u, 0x0000ff01u, 0x0000ff01u, 0x0000ff01u, 0xff01ff01u, 0xff01ff01u, + 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, + 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x7c007c00u, 0x7c007c00u, + }}, + {32u, 0x3100cfacu, { + 0xffffff01u, 0xffffff01u, 0xffffff01u, 0xffffff01u, 0xffffff01u, 0xffffff01u, + 0xffffff01u, 0xffffff01u, 0xffffff01u, 0xffffff01u, 0xff01ff01u, 0xff01ff01u, + 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, + 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x7c007c00u, 0x7c007c00u, + }}, + {32u, 0x3100dfacu, { + 0xffe02000u, 0xffe02000u, 0xffe02000u, 0xffe02000u, 0xffe02000u, 0xffe02000u, + 0xffe02000u, 0xffe02000u, 0xffe02000u, 0xffe02000u, 0xff01ff01u, 0xff01ff01u, + 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, + 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7c007c00u, 0x7c007c00u, + }}, + {32u, 0x3100efacu, { + 0x3b808081u, 0x3f800000u, 0x3b808081u, 0x3f800000u, 0x3b808081u, 0x3b808081u, + 0x3f800000u, 0x3b808081u, 0x3f800000u, 0x3b808081u, 0x3c001c04u, 0x3c001c04u, + 0x00000000u, 0x3ef8f8f9u, 0x00000000u, 0x3ef8f8f9u, 0x00000000u, 0x00000000u, + 0x3ef8f8f9u, 0x00000000u, 0x3ef8f8f9u, 0x00000000u, 0x37c80000u, 0x37c80000u, + }}, + {32u, 0x3100ffacu, { + 0x3c010204u, 0xbc010204u, 0x3c010204u, 0xbc010204u, 0x3c010204u, 0x3c010204u, + 0xbc010204u, 0x3c010204u, 0xbc010204u, 0x3c010204u, 0xa0082008u, 0xa0082008u, + 0x00000000u, 0x3f79f3e8u, 0x00000000u, 0x3f79f3e8u, 0x00000000u, 0x00000000u, + 0x3f79f3e8u, 0x00000000u, 0x3f79f3e8u, 0x00000000u, 0x3bd00000u, 0x3bd00000u, + }}, + {32u, 0x31010facu, { + 0x3f800000u, 0x437f0000u, 0x3f800000u, 0x437f0000u, 0x3f800000u, 0x3f800000u, + 0x437f0000u, 0x3f800000u, 0x437f0000u, 0x3f800000u, 0x5bf83c00u, 0x5bf83c00u, + 0x00000000u, 0x42f80000u, 0x00000000u, 0x42f80000u, 0x00000000u, 0x00000000u, + 0x42f80000u, 0x00000000u, 0x42f80000u, 0x00000000u, 0x57c00000u, 0x57c00000u, + }}, + {32u, 0x31011facu, { + 0x3f800000u, 0xbf800000u, 0x3f800000u, 0xbf800000u, 0x3f800000u, 0x3f800000u, + 0xbf800000u, 0x3f800000u, 0xbf800000u, 0x3f800000u, 0xbc003c00u, 0xbc003c00u, + 0x00000000u, 0x42f80000u, 0x00000000u, 0x42f80000u, 0x00000000u, 0x00000000u, + 0x42f80000u, 0x00000000u, 0x42f80000u, 0x00000000u, 0x57c00000u, 0x57c00000u, + }}, + {32u, 0x31012facu, { + 0x00000001u, 0x000000ffu, 0x00000001u, 0x000000ffu, 0x00000001u, 0x00000001u, + 0x000000ffu, 0x00000001u, 0x000000ffu, 0x00000001u, 0x00ff0001u, 0x00ff0001u, + 0x00000000u, 0x0000007cu, 0x00000000u, 0x0000007cu, 0x00000000u, 0x00000000u, + 0x0000007cu, 0x00000000u, 0x0000007cu, 0x00000000u, 0x007c0000u, 0x007c0000u, + }}, + {32u, 0x31013facu, { + 0x00000001u, 0xffffffffu, 0x00000001u, 0xffffffffu, 0x00000001u, 0x00000001u, + 0xffffffffu, 0x00000001u, 0xffffffffu, 0x00000001u, 0xffff0001u, 0xffff0001u, + 0x00000000u, 0x0000007cu, 0x00000000u, 0x0000007cu, 0x00000000u, 0x00000000u, + 0x0000007cu, 0x00000000u, 0x0000007cu, 0x00000000u, 0x007c0000u, 0x007c0000u, + }}, + {32u, 0x31014facu, { + 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, + 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0xffffffffu, 0xffffffffu, + 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, + 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xffffffffu, 0xffffffffu, + }}, + {32u, 0x31015facu, { + 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, + 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x80008000u, 0x80008000u, + 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, + 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0x80008000u, 0x80008000u, + }}, + {32u, 0x31016facu, { + 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, + 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x80008000u, 0x80008000u, + 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, + 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfbfffbffu, 0xfbfffbffu, + }}, + {32u, 0x31017facu, { + 0x3f7f01ffu, 0x3f007f80u, 0x3f7f01ffu, 0x3f007f80u, 0x3f7f01ffu, 0x3f7f01ffu, + 0x3f007f80u, 0x3f7f01ffu, 0x3f007f80u, 0x3f7f01ffu, 0x38043bf8u, 0x38043bf8u, + 0x3ef800f8u, 0x3f7c00fcu, 0x3ef800f8u, 0x3f7c00fcu, 0x3ef800f8u, 0x3ef800f8u, + 0x3f7c00fcu, 0x3ef800f8u, 0x3f7c00fcu, 0x3ef800f8u, 0x3be037c0u, 0x3be037c0u, + }}, + {32u, 0x31018facu, { + 0xbbff01feu, 0xbf7f03feu, 0xbbff01feu, 0xbf7f03feu, 0xbbff01feu, 0xbbff01feu, + 0xbf7f03feu, 0xbbff01feu, 0xbf7f03feu, 0xbbff01feu, 0xbbf89ff8u, 0xbbf89ff8u, + 0x3f7801f0u, 0xbd000100u, 0x3f7801f0u, 0xbd000100u, 0x3f7801f0u, 0x3f7801f0u, + 0xbd000100u, 0x3f7801f0u, 0xbd000100u, 0x3f7801f0u, 0xa8003bc0u, 0xa8003bc0u, + }}, + {32u, 0x31019facu, { + 0x477f0100u, 0x47007f00u, 0x477f0100u, 0x47007f00u, 0x477f0100u, 0x477f0100u, + 0x47007f00u, 0x477f0100u, 0x47007f00u, 0x477f0100u, 0x78047bf8u, 0x78047bf8u, + 0x46f80000u, 0x477c0000u, 0x46f80000u, 0x477c0000u, 0x46f80000u, 0x46f80000u, + 0x477c0000u, 0x46f80000u, 0x477c0000u, 0x46f80000u, 0x7be077c0u, 0x7be077c0u, + }}, + {32u, 0x3101afacu, { + 0xc37f0000u, 0xc6ff0200u, 0xc37f0000u, 0xc6ff0200u, 0xc37f0000u, 0xc37f0000u, + 0xc6ff0200u, 0xc37f0000u, 0xc6ff0200u, 0xc37f0000u, 0xf7f8dbf8u, 0xf7f8dbf8u, + 0x46f80000u, 0xc4800000u, 0x46f80000u, 0xc4800000u, 0x46f80000u, 0x46f80000u, + 0xc4800000u, 0x46f80000u, 0xc4800000u, 0x46f80000u, 0xe40077c0u, 0xe40077c0u, + }}, + {32u, 0x3101bfacu, { + 0x0000ff01u, 0x0000807fu, 0x0000ff01u, 0x0000807fu, 0x0000ff01u, 0x0000ff01u, + 0x0000807fu, 0x0000ff01u, 0x0000807fu, 0x0000ff01u, 0x807fff01u, 0x807fff01u, + 0x00007c00u, 0x0000fc00u, 0x00007c00u, 0x0000fc00u, 0x00007c00u, 0x00007c00u, + 0x0000fc00u, 0x00007c00u, 0x0000fc00u, 0x00007c00u, 0xfc007c00u, 0xfc007c00u, + }}, + {32u, 0x3101cfacu, { + 0xffffff01u, 0xffff807fu, 0xffffff01u, 0xffff807fu, 0xffffff01u, 0xffffff01u, + 0xffff807fu, 0xffffff01u, 0xffff807fu, 0xffffff01u, 0x807fff01u, 0x807fff01u, + 0x00007c00u, 0xfffffc00u, 0x00007c00u, 0xfffffc00u, 0x00007c00u, 0x00007c00u, + 0xfffffc00u, 0x00007c00u, 0xfffffc00u, 0x00007c00u, 0xfc007c00u, 0xfc007c00u, + }}, + {32u, 0x3101dfacu, { + 0xffe02000u, 0xb6fe0000u, 0xffe02000u, 0xb6fe0000u, 0xffe02000u, 0xffe02000u, + 0xb6fe0000u, 0xffe02000u, 0xb6fe0000u, 0xffe02000u, 0x807fff01u, 0x807fff01u, + 0x7f800000u, 0xff800000u, 0x7f800000u, 0xff800000u, 0x7f800000u, 0x7f800000u, + 0xff800000u, 0x7f800000u, 0xff800000u, 0x7f800000u, 0xfc007c00u, 0xfc007c00u, + }}, + {32u, 0x3101efacu, { + 0x3f603c08u, 0x3f800000u, 0x3f006018u, 0x3f603c08u, 0x3f603c08u, 0x3f603c08u, + 0x3f800000u, 0x3f603c08u, 0x3f800000u, 0x3f006018u, 0x3c003b02u, 0x3b023803u, + 0x3f001002u, 0x3bf01e04u, 0x3f7c3f10u, 0x3f001002u, 0x3f001002u, 0x3f001002u, + 0x3bf01e04u, 0x3f001002u, 0x3bf01e04u, 0x3f7c3f10u, 0x1f813801u, 0x38013be2u, + }}, + {32u, 0x3101ffacu, { + 0xbe7f3fd0u, 0xba802008u, 0xbf800000u, 0xbe7f3fd0u, 0xbe7f3fd0u, 0xbe7f3fd0u, + 0xba802008u, 0xbe7f3fd0u, 0xba802008u, 0xbf800000u, 0x9401b3fau, 0xb3fabc00u, + 0xbf800000u, 0x3c703c0fu, 0xbd004020u, 0xbf800000u, 0xbf800000u, 0xbf800000u, + 0x3c703c0fu, 0xbf800000u, 0x3c703c0fu, 0xbd004020u, 0x2382bc00u, 0xbc00a802u, + }}, + {32u, 0x31020facu, { + 0x44e02000u, 0x44ffe000u, 0x44004000u, 0x44e02000u, 0x44e02000u, 0x44e02000u, + 0x44ffe000u, 0x44e02000u, 0x44ffe000u, 0x44004000u, 0x67ff6701u, 0x67016002u, + 0x44800000u, 0x41700000u, 0x447c0000u, 0x44800000u, 0x44800000u, 0x44800000u, + 0x41700000u, 0x44800000u, 0x41700000u, 0x447c0000u, 0x4b806400u, 0x640063e0u, + }}, + {32u, 0x31021facu, { + 0xc37f0000u, 0xbf800000u, 0xc3ff8000u, 0xc37f0000u, 0xc37f0000u, 0xc37f0000u, + 0xbf800000u, 0xc37f0000u, 0xbf800000u, 0xc3ff8000u, 0xbc00dbf8u, 0xdbf8dffcu, + 0xc4800000u, 0x41700000u, 0xc1800000u, 0xc4800000u, 0xc4800000u, 0xc4800000u, + 0x41700000u, 0xc4800000u, 0x41700000u, 0xc1800000u, 0x4b80e400u, 0xe400cc00u, + }}, + {32u, 0x31022facu, { + 0x00000701u, 0x000007ffu, 0x00000201u, 0x00000701u, 0x00000701u, 0x00000701u, + 0x000007ffu, 0x00000701u, 0x000007ffu, 0x00000201u, 0x07ff0701u, 0x07010201u, + 0x00000400u, 0x0000000fu, 0x000003f0u, 0x00000400u, 0x00000400u, 0x00000400u, + 0x0000000fu, 0x00000400u, 0x0000000fu, 0x000003f0u, 0x000f0400u, 0x040003f0u, + }}, + {32u, 0x31023facu, { + 0xffffff01u, 0xffffffffu, 0xfffffe01u, 0xffffff01u, 0xffffff01u, 0xffffff01u, + 0xffffffffu, 0xffffff01u, 0xffffffffu, 0xfffffe01u, 0xffffff01u, 0xff01fe01u, + 0xfffffc00u, 0x0000000fu, 0xfffffff0u, 0xfffffc00u, 0xfffffc00u, 0xfffffc00u, + 0x0000000fu, 0xfffffc00u, 0x0000000fu, 0xfffffff0u, 0x000ffc00u, 0xfc00fff0u, + }}, + {32u, 0x31024facu, { + 0x46020000u, 0x7ffe0000u, 0x40040000u, 0x46020000u, 0x46020000u, 0x46020000u, + 0x7ffe0000u, 0x46020000u, 0x7ffe0000u, 0x40040000u, 0x7ff07010u, 0x70104020u, + 0x40000000u, 0x37700000u, 0x7fc00000u, 0x40000000u, 0x40000000u, 0x40000000u, + 0x37700000u, 0x40000000u, 0x37700000u, 0x7fc00000u, 0x00f04000u, 0x40007e00u, + }}, + {32u, 0x31025facu, { + 0x3f40701cu, 0x3f800000u, 0x3f00700eu, 0x3f40701cu, 0x3f40701cu, 0x3f40701cu, + 0x3f800000u, 0x3f40701cu, 0x3f800000u, 0x3f00700eu, 0x3c003a04u, 0x3a043804u, + 0x00000000u, 0x3c781f04u, 0x3f7c1f84u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x3c781f04u, 0x00000000u, 0x3c781f04u, 0x3f7c1f84u, 0x23c10000u, 0x00003be1u, + }}, + {32u, 0x31026facu, { + 0xbeff7fc0u, 0xba802008u, 0xbf7f7fe0u, 0xbeff7fc0u, 0xbeff7fc0u, 0xbeff7fc0u, + 0xba802008u, 0xbeff7fc0u, 0xba802008u, 0xbf7f7fe0u, 0x9401b7fcu, 0xb7fcbbfcu, + 0x00000000u, 0x3cf83e10u, 0xbd002008u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x3cf83e10u, 0x00000000u, 0x3cf83e10u, 0xbd002008u, 0x27c20000u, 0x0000a801u, + }}, + {32u, 0x31027facu, { + 0x44404000u, 0x44ffe000u, 0x44806000u, 0x44404000u, 0x44404000u, 0x44404000u, + 0x44ffe000u, 0x44404000u, 0x44ffe000u, 0x44806000u, 0x67ff6202u, 0x62026403u, + 0x00000000u, 0x41f80000u, 0x44fc0000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x41f80000u, 0x00000000u, 0x41f80000u, 0x44fc0000u, 0x4fc00000u, 0x000067e0u, + }}, + {32u, 0x31028facu, { + 0xc37f0000u, 0xbf800000u, 0xc47f4000u, 0xc37f0000u, 0xc37f0000u, 0xc37f0000u, + 0xbf800000u, 0xc37f0000u, 0xbf800000u, 0xc47f4000u, 0xbc00dbf8u, 0xdbf8e3fau, + 0x00000000u, 0x41f80000u, 0xc2000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x41f80000u, 0x00000000u, 0x41f80000u, 0xc2000000u, 0x4fc00000u, 0x0000d000u, + }}, + {32u, 0x31029facu, { + 0x00000301u, 0x000007ffu, 0x00000403u, 0x00000301u, 0x00000301u, 0x00000301u, + 0x000007ffu, 0x00000301u, 0x000007ffu, 0x00000403u, 0x07ff0301u, 0x03010403u, + 0x00000000u, 0x0000001fu, 0x000007e0u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x0000001fu, 0x00000000u, 0x0000001fu, 0x000007e0u, 0x001f0000u, 0x000007e0u, + }}, + {32u, 0x3102afacu, { + 0xffffff01u, 0xffffffffu, 0xfffffc03u, 0xffffff01u, 0xffffff01u, 0xffffff01u, + 0xffffffffu, 0xffffff01u, 0xffffffffu, 0xfffffc03u, 0xffffff01u, 0xff01fc03u, + 0x00000000u, 0x0000001fu, 0xffffffe0u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x0000001fu, 0x00000000u, 0x0000001fu, 0xffffffe0u, 0x001f0000u, 0x0000ffe0u, + }}, + {32u, 0x3102bfacu, { + 0x44040000u, 0x7ffe0000u, 0x40060000u, 0x44040000u, 0x44040000u, 0x44040000u, + 0x7ffe0000u, 0x44040000u, 0x7ffe0000u, 0x40060000u, 0x7ff06020u, 0x60204030u, + 0x00000000u, 0x37f80000u, 0x7fc00000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x37f80000u, 0x00000000u, 0x37f80000u, 0x7fc00000u, 0x01f00000u, 0x00007e00u, + }}, + {32u, 0x3102cfacu, { + 0x3eaaaaabu, 0x3f703c0fu, 0x3f800000u, 0x3f006018u, 0x3eaaaaabu, 0x3eaaaaabu, + 0x3f703c0fu, 0x3eaaaaabu, 0x3f703c0fu, 0x3f800000u, 0x3b823555u, 0x38033c00u, + 0x00000000u, 0x3f40300cu, 0x3be0380eu, 0x3f7c3f10u, 0x00000000u, 0x00000000u, + 0x3f40300cu, 0x00000000u, 0x3f40300cu, 0x3be0380eu, 0x3a020000u, 0x3be21f02u, + }}, + {32u, 0x3102dfacu, { + 0x3f800000u, 0xbe004020u, 0xbb004020u, 0xbf800000u, 0x3f800000u, 0x3f800000u, + 0xbe004020u, 0x3f800000u, 0xbe004020u, 0xbb004020u, 0xb0023c00u, 0xbc009802u, + 0x00000000u, 0xbf004020u, 0x3c607038u, 0xbd004020u, 0x00000000u, 0x00000000u, + 0xbf004020u, 0x00000000u, 0xbf004020u, 0x3c607038u, 0xb8020000u, 0xa8022304u, + }}, + {32u, 0x3102efacu, { + 0x3f800000u, 0x44700000u, 0x447fc000u, 0x44004000u, 0x3f800000u, 0x3f800000u, + 0x44700000u, 0x3f800000u, 0x44700000u, 0x447fc000u, 0x63803c00u, 0x600263feu, + 0x00000000u, 0x44400000u, 0x40e00000u, 0x447c0000u, 0x00000000u, 0x00000000u, + 0x44400000u, 0x00000000u, 0x44400000u, 0x40e00000u, 0x62000000u, 0x63e04700u, + }}, + {32u, 0x3102ffacu, { + 0x3f800000u, 0xc2800000u, 0xbf800000u, 0xc3ff8000u, 0x3f800000u, 0x3f800000u, + 0xc2800000u, 0x3f800000u, 0xc2800000u, 0xbf800000u, 0xd4003c00u, 0xdffcbc00u, + 0x00000000u, 0xc3800000u, 0x40e00000u, 0xc1800000u, 0x00000000u, 0x00000000u, + 0xc3800000u, 0x00000000u, 0xc3800000u, 0x40e00000u, 0xdc000000u, 0xcc004700u, + }}, + {32u, 0x31030facu, { + 0x00000001u, 0x000003c0u, 0x000003ffu, 0x00000201u, 0x00000001u, 0x00000001u, + 0x000003c0u, 0x00000001u, 0x000003c0u, 0x000003ffu, 0x03c00001u, 0x020103ffu, + 0x00000000u, 0x00000300u, 0x00000007u, 0x000003f0u, 0x00000000u, 0x00000000u, + 0x00000300u, 0x00000000u, 0x00000300u, 0x00000007u, 0x03000000u, 0x03f00007u, + }}, + {32u, 0x31031facu, { + 0x00000001u, 0xffffffc0u, 0xffffffffu, 0xfffffe01u, 0x00000001u, 0x00000001u, + 0xffffffc0u, 0x00000001u, 0xffffffc0u, 0xffffffffu, 0xffc00001u, 0xfe01ffffu, + 0x00000000u, 0xffffff00u, 0x00000007u, 0xfffffff0u, 0x00000000u, 0x00000000u, + 0xffffff00u, 0x00000000u, 0xffffff00u, 0x00000007u, 0xff000000u, 0xfff00007u, + }}, + {32u, 0x31032facu, { + 0x3f40701cu, 0x3f800000u, 0x3be0380eu, 0x3f2aaaabu, 0x3f40701cu, 0x3f40701cu, + 0x3f800000u, 0x3f40701cu, 0x3f800000u, 0x3be0380eu, 0x3c003a04u, 0x39551f02u, + 0x00000000u, 0x3cf83e10u, 0x3f703c0fu, 0x3f800000u, 0x00000000u, 0x00000000u, + 0x3cf83e10u, 0x00000000u, 0x3cf83e10u, 0x3f703c0fu, 0x27c20000u, 0x3c003b82u, + }}, + {32u, 0x31033facu, { + 0xbeff7fc0u, 0xbb004020u, 0x3c607038u, 0xbf800000u, 0xbeff7fc0u, 0xbeff7fc0u, + 0xbb004020u, 0xbeff7fc0u, 0xbb004020u, 0x3c607038u, 0x9802b7fcu, 0xbc002304u, + 0x00000000u, 0x3d787c3eu, 0xbe004020u, 0xbf800000u, 0x00000000u, 0x00000000u, + 0x3d787c3eu, 0x00000000u, 0x3d787c3eu, 0xbe004020u, 0x2bc40000u, 0xbc00b002u, + }}, + {32u, 0x31034facu, { + 0x44404000u, 0x447fc000u, 0x40e00000u, 0x40000000u, 0x44404000u, 0x44404000u, + 0x447fc000u, 0x44404000u, 0x447fc000u, 0x40e00000u, 0x63fe6202u, 0x40004700u, + 0x00000000u, 0x41f80000u, 0x44700000u, 0x40400000u, 0x00000000u, 0x00000000u, + 0x41f80000u, 0x00000000u, 0x41f80000u, 0x44700000u, 0x4fc00000u, 0x42006380u, + }}, + {32u, 0x31035facu, { + 0xc37f0000u, 0xbf800000u, 0x40e00000u, 0xc0000000u, 0xc37f0000u, 0xc37f0000u, + 0xbf800000u, 0xc37f0000u, 0xbf800000u, 0x40e00000u, 0xbc00dbf8u, 0xc0004700u, + 0x00000000u, 0x41f80000u, 0xc2800000u, 0xbf800000u, 0x00000000u, 0x00000000u, + 0x41f80000u, 0x00000000u, 0x41f80000u, 0xc2800000u, 0x4fc00000u, 0xbc00d400u, + }}, + {32u, 0x31036facu, { + 0x00000301u, 0x000003ffu, 0x00000007u, 0x00000002u, 0x00000301u, 0x00000301u, + 0x000003ffu, 0x00000301u, 0x000003ffu, 0x00000007u, 0x03ff0301u, 0x00020007u, + 0x00000000u, 0x0000001fu, 0x000003c0u, 0x00000003u, 0x00000000u, 0x00000000u, + 0x0000001fu, 0x00000000u, 0x0000001fu, 0x000003c0u, 0x001f0000u, 0x000303c0u, + }}, + {32u, 0x31037facu, { + 0xffffff01u, 0xffffffffu, 0x00000007u, 0xfffffffeu, 0xffffff01u, 0xffffff01u, + 0xffffffffu, 0xffffff01u, 0xffffffffu, 0x00000007u, 0xffffff01u, 0xfffe0007u, + 0x00000000u, 0x0000001fu, 0xffffffc0u, 0xffffffffu, 0x00000000u, 0x00000000u, + 0x0000001fu, 0x00000000u, 0x0000001fu, 0xffffffc0u, 0x001f0000u, 0xffffffc0u, + }}, + {32u, 0x31038facu, { + 0x3b808081u, 0x3f800000u, 0x3efefeffu, 0x3f008081u, 0x3b808081u, 0x3b808081u, + 0x3f800000u, 0x3b808081u, 0x3f800000u, 0x3efefeffu, 0x3c001c04u, 0x380437f8u, + 0x00000000u, 0x3ef8f8f9u, 0x00000000u, 0x3f7cfcfdu, 0x00000000u, 0x00000000u, + 0x3ef8f8f9u, 0x00000000u, 0x3ef8f8f9u, 0x00000000u, 0x37c80000u, 0x3be80000u, + }}, + {32u, 0x31039facu, { + 0x3c010204u, 0xbc010204u, 0x3f800000u, 0xbf800000u, 0x3c010204u, 0x3c010204u, + 0xbc010204u, 0x3c010204u, 0xbc010204u, 0x3f800000u, 0xa0082008u, 0xbc003c00u, + 0x00000000u, 0x3f79f3e8u, 0x00000000u, 0xbd010204u, 0x00000000u, 0x00000000u, + 0x3f79f3e8u, 0x00000000u, 0x3f79f3e8u, 0x00000000u, 0x3bd00000u, 0xa8080000u, + }}, + {32u, 0x3103afacu, { + 0x3f800000u, 0x437f0000u, 0x42fe0000u, 0x43000000u, 0x3f800000u, 0x3f800000u, + 0x437f0000u, 0x3f800000u, 0x437f0000u, 0x42fe0000u, 0x5bf83c00u, 0x580057f0u, + 0x00000000u, 0x42f80000u, 0x00000000u, 0x437c0000u, 0x00000000u, 0x00000000u, + 0x42f80000u, 0x00000000u, 0x42f80000u, 0x00000000u, 0x57c00000u, 0x5be00000u, + }}, + {32u, 0x3103bfacu, { + 0x3f800000u, 0xbf800000u, 0x42fe0000u, 0xc3000000u, 0x3f800000u, 0x3f800000u, + 0xbf800000u, 0x3f800000u, 0xbf800000u, 0x42fe0000u, 0xbc003c00u, 0xd80057f0u, + 0x00000000u, 0x42f80000u, 0x00000000u, 0xc0800000u, 0x00000000u, 0x00000000u, + 0x42f80000u, 0x00000000u, 0x42f80000u, 0x00000000u, 0x57c00000u, 0xc4000000u, + }}, + {32u, 0x3103cfacu, { + 0x00000001u, 0x000000ffu, 0x0000007fu, 0x00000080u, 0x00000001u, 0x00000001u, + 0x000000ffu, 0x00000001u, 0x000000ffu, 0x0000007fu, 0x00ff0001u, 0x0080007fu, + 0x00000000u, 0x0000007cu, 0x00000000u, 0x000000fcu, 0x00000000u, 0x00000000u, + 0x0000007cu, 0x00000000u, 0x0000007cu, 0x00000000u, 0x007c0000u, 0x00fc0000u, + }}, + {32u, 0x3103dfacu, { + 0x00000001u, 0xffffffffu, 0x0000007fu, 0xffffff80u, 0x00000001u, 0x00000001u, + 0xffffffffu, 0x00000001u, 0xffffffffu, 0x0000007fu, 0xffff0001u, 0xff80007fu, + 0x00000000u, 0x0000007cu, 0x00000000u, 0xfffffffcu, 0x00000000u, 0x00000000u, + 0x0000007cu, 0x00000000u, 0x0000007cu, 0x00000000u, 0x007c0000u, 0xfffc0000u, + }}, + {32u, 0x3103efacu, { + 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x807fff01u, + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0xffffffffu, 0xffffffffu, + 0xfc007c00u, 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0xfc007c00u, 0xfc007c00u, + 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0xfc007c00u, 0xffffffffu, 0xffffffffu, + }}, + {32u, 0x3103ffacu, { + 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x807fff01u, + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x7fff8000u, 0x7fff8000u, + 0xfc007c00u, 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0xfc007c00u, 0xfc007c00u, + 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0x7fff8000u, + }}, + {32u, 0x31040facu, { + 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x807fff01u, + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x20058000u, 0x20058000u, + 0xfc007c00u, 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0xfc007c00u, 0xfc007c00u, + 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0xfc007c00u, 0x7ffcfbffu, 0x7ffcfbffu, + }}, + {32u, 0x31041facu, { + 0x3f7f01ffu, 0x3f007f80u, 0x3f3c00bcu, 0x3e7000f0u, 0x3f7f01ffu, 0x3f7f01ffu, + 0x3f007f80u, 0x3f7f01ffu, 0x3f007f80u, 0x3f3c00bcu, 0x38043bf8u, 0x338039e0u, + 0x3ef800f8u, 0x3f7c00fcu, 0x3f000080u, 0x3effff00u, 0x3ef800f8u, 0x3ef800f8u, + 0x3f7c00fcu, 0x3ef800f8u, 0x3f7c00fcu, 0x3f000080u, 0x3be037c0u, 0x38003800u, + }}, + {32u, 0x31042facu, { + 0xbbff01feu, 0xbf7f03feu, 0xbf080110u, 0x3ef001e0u, 0xbbff01feu, 0xbbff01feu, + 0xbf7f03feu, 0xbbff01feu, 0xbf7f03feu, 0xbf080110u, 0xbbf89ff8u, 0x3780b840u, + 0x3f7801f0u, 0xbd000100u, 0xbf800000u, 0x3f800000u, 0x3f7801f0u, 0x3f7801f0u, + 0xbd000100u, 0x3f7801f0u, 0xbd000100u, 0xbf800000u, 0xa8003bc0u, 0x3c00bc00u, + }}, + {32u, 0x31043facu, { + 0x477f0100u, 0x47007f00u, 0x473c0000u, 0x46700000u, 0x477f0100u, 0x477f0100u, + 0x47007f00u, 0x477f0100u, 0x47007f00u, 0x473c0000u, 0x78047bf8u, 0x738079e0u, + 0x46f80000u, 0x477c0000u, 0x47000000u, 0x46fffe00u, 0x46f80000u, 0x46f80000u, + 0x477c0000u, 0x46f80000u, 0x477c0000u, 0x47000000u, 0x7be077c0u, 0x78007800u, + }}, + {32u, 0x31044facu, { + 0xc37f0000u, 0xc6ff0200u, 0xc6880000u, 0x46700000u, 0xc37f0000u, 0xc37f0000u, + 0xc6ff0200u, 0xc37f0000u, 0xc6ff0200u, 0xc6880000u, 0xf7f8dbf8u, 0x7380f440u, + 0x46f80000u, 0xc4800000u, 0xc7000000u, 0x46fffe00u, 0x46f80000u, 0x46f80000u, + 0xc4800000u, 0x46f80000u, 0xc4800000u, 0xc7000000u, 0xe40077c0u, 0x7800f800u, + }}, + {32u, 0x31045facu, { + 0x0000ff01u, 0x0000807fu, 0x0000bc00u, 0x00003c00u, 0x0000ff01u, 0x0000ff01u, + 0x0000807fu, 0x0000ff01u, 0x0000807fu, 0x0000bc00u, 0x807fff01u, 0x3c00bc00u, + 0x00007c00u, 0x0000fc00u, 0x00008000u, 0x00007fffu, 0x00007c00u, 0x00007c00u, + 0x0000fc00u, 0x00007c00u, 0x0000fc00u, 0x00008000u, 0xfc007c00u, 0x7fff8000u, + }}, + {32u, 0x31046facu, { + 0xffffff01u, 0xffff807fu, 0xffffbc00u, 0x00003c00u, 0xffffff01u, 0xffffff01u, + 0xffff807fu, 0xffffff01u, 0xffff807fu, 0xffffbc00u, 0x807fff01u, 0x3c00bc00u, + 0x00007c00u, 0xfffffc00u, 0xffff8000u, 0x00007fffu, 0x00007c00u, 0x00007c00u, + 0xfffffc00u, 0x00007c00u, 0xfffffc00u, 0xffff8000u, 0xfc007c00u, 0x7fff8000u, + }}, + {32u, 0x31047facu, { + 0xffe02000u, 0xb6fe0000u, 0xbf800000u, 0x3f800000u, 0xffe02000u, 0xffe02000u, + 0xb6fe0000u, 0xffe02000u, 0xb6fe0000u, 0xbf800000u, 0x807fff01u, 0x3c00bc00u, + 0x7f800000u, 0xff800000u, 0x80000000u, 0x7fffe000u, 0x7f800000u, 0x7f800000u, + 0xff800000u, 0x7f800000u, 0xff800000u, 0x80000000u, 0xfc007c00u, 0x7fff8000u, + }}, + {32u, 0x31048facu, { + 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0x807fff01u, 0x807fff01u, 0x807fff01u, + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0xffffffffu, 0xffffffffu, + 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, + 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0xffffffffu, 0xffffffffu, + }}, + {32u, 0x31049facu, { + 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0x807fff01u, 0x807fff01u, 0x807fff01u, + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0x7fff8000u, 0x80007fffu, + 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, + 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0x7fff8000u, 0x80007fffu, + }}, + {32u, 0x3104afacu, { + 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0x807fff01u, 0x807fff01u, 0x807fff01u, + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0x20058000u, 0x80003c00u, + 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, + 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0x7ffcfbffu, 0xfbff7e00u, + }}, + {32u, 0x3104bfacu, { + 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0xc0490fdbu, 0x807fff01u, 0x807fff01u, + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0xffffffffu, 0xffffffffu, + 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0x00000001u, 0xfc007c00u, 0xfc007c00u, + 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0xffffffffu, 0x0001ffffu, + }}, + {32u, 0x3104cfacu, { + 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0xc0490fdbu, 0x807fff01u, 0x807fff01u, + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0x7fff8000u, 0x80007fffu, + 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0x00000001u, 0xfc007c00u, 0xfc007c00u, + 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0x7fff8000u, 0x00017fffu, + }}, + {32u, 0x3104dfacu, { + 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0xc0490fdbu, 0x807fff01u, 0x807fff01u, + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0x20058000u, 0xc2483c00u, + 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0x00000001u, 0xfc007c00u, 0xfc007c00u, + 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0x7fc00001u, 0x7ffcfbffu, 0x00007e00u, + }}, + {32u, 0x310389c8u, { + 0x00000000u, 0x3f800000u, 0x3f008081u, 0x3b808081u, 0x00000000u, 0x00000000u, + 0x3f800000u, 0x00000000u, 0x3f800000u, 0x3f008081u, 0x3c000000u, 0x1c043804u, + 0x00000000u, 0x3f800000u, 0x3f7cfcfdu, 0x00000000u, 0x00000000u, 0x00000000u, + 0x3f800000u, 0x00000000u, 0x3f800000u, 0x3f7cfcfdu, 0x3c000000u, 0x00003be8u, + }}, + {32u, 0x3103c9c8u, { + 0x00000000u, 0x00000001u, 0x00000080u, 0x00000001u, 0x00000000u, 0x00000000u, + 0x00000001u, 0x00000000u, 0x00000001u, 0x00000080u, 0x00010000u, 0x00010080u, + 0x00000000u, 0x00000001u, 0x000000fcu, 0x00000000u, 0x00000000u, 0x00000000u, + 0x00000001u, 0x00000000u, 0x00000001u, 0x000000fcu, 0x00010000u, 0x000000fcu, + }}, + {32u, 0x31040977u, { + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x3c00bc00u, + 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x80002005u, 0x80002005u, + 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0x7fff8000u, + 0xfc007c00u, 0x7fff8000u, 0xfc007c00u, 0x7fff8000u, 0xfbff7ffcu, 0xfbff7ffcu, + }}, + {32u, 0x31016977u, { + 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, + 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x807fff01u, 0x80008000u, 0x80008000u, + 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, + 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfc007c00u, 0xfbfffbffu, 0xfbfffbffu, + }}, + {22u, 0x3104dfacu, { + 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0xc0490fdbu, 0x807fff01u, 0x807fff01u, + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x3f800000u, 0x20058000u, 0xc2483c00u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xfc007c00u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + }}, + {22u, 0x31047facu, { + 0xffe02000u, 0xb6fe0000u, 0xbf800000u, 0x3f800000u, 0xffe02000u, 0xffe02000u, + 0xb6fe0000u, 0xffe02000u, 0xb6fe0000u, 0xbf800000u, 0x807fff01u, 0x3c00bc00u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x7f800000u, 0x7f800000u, + 0xff800000u, 0x7f800000u, 0xff800000u, 0x80000000u, 0x00000000u, 0x00000000u, + }}, + {22u, 0x31040facu, { + 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x807fff01u, + 0x3c00bc00u, 0x807fff01u, 0x3c00bc00u, 0x807fff01u, 0x20058000u, 0x20058000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xfc007c00u, 0x00000000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + }}, + {22u, 0x31038facu, { + 0x3b808081u, 0x3f800000u, 0x3efefeffu, 0x3f008081u, 0x3b808081u, 0x3b808081u, + 0x3f800000u, 0x3b808081u, 0x3f800000u, 0x3efefeffu, 0x3c001c04u, 0x380437f8u, + 0x00000000u, 0x3ef8f8f9u, 0x00000000u, 0x3f7cfcfdu, 0x00000000u, 0x00000000u, + 0x3ef8f8f9u, 0x00000000u, 0x3ef8f8f9u, 0x00000000u, 0x37c80000u, 0x3be80000u, + }}, +}}; + +constexpr std::array InvalidFormats{0x31000facu, 0x3104efacu}; + +constexpr std::array Names{ + "buffer_load_format_xyzw x", "buffer_load_format_xyzw y", "buffer_load_format_xyzw z", "buffer_load_format_xyzw w", + "buffer_load_format_x x", "buffer_load_format_xy x", "buffer_load_format_xy y", "buffer_load_format_xyz x", + "buffer_load_format_xyz y", "buffer_load_format_xyz z", "buffer_load_format_d16_xyzw xy", "buffer_load_format_d16_xyzw zw", +}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, GuestBlock& guest) { + const auto descriptor = reinterpret_cast(guest.Descriptor().data()); + const auto data = reinterpret_cast(guest.Data().data()); + const auto output = reinterpret_cast(guest.Output().data()); + const std::array userData{ + 0u, 0u, 0u, 0u, + static_cast(descriptor), static_cast((descriptor >> 32u) & 0xffffu), static_cast(guest.Descriptor().size_bytes()), 0x31016facu, + static_cast(data), static_cast((data >> 32u) & 0xffffu), 0u, 0u, + static_cast(output), static_cast((output >> 32u) & 0xffffu), static_cast(guest.Output().size_bytes()), 0x31016facu, + }; + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 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; + return ShaderRecompiler::Recompile(request); +} + +void Run(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& shader, GuestBlock& guest, std::uint32_t records, std::uint32_t word3) { + auto descriptor = guest.Descriptor(); + descriptor[0] = records; + descriptor[1] = word3; + descriptor[2] = 0u; + descriptor[3] = 0u; + std::fill(guest.Output().begin(), guest.Output().end(), Untouched); + device.Dispatch(shader, 1, 1, 1, {}, reinterpret_cast(Code.data())); + device.WaitIdle(); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock guest; + const auto shader = Compile(*device, guest); + for (const auto& test : Cases) { + Run(*device, shader, guest, test.records, test.word3); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + for (std::uint32_t index = 0; index < Results; ++index) { + const auto actual = guest.Output()[lane * OutputStride + index]; + const auto expected = test.expected[lane * Results + index]; + Require(actual == expected, "gpu buffer format: format " + std::to_string((test.word3 >> 12u) & 0x7fu) + " word3 " + Hex(test.word3) + " records " + std::to_string(test.records) + " lane " + std::to_string(lane) + " " + Names[index] + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } + } + for (const auto word3 : InvalidFormats) { + Run(*device, shader, guest, 32u, word3); + const auto output = guest.Output(); + Require(std::all_of(output.begin(), output.end(), [](std::uint32_t word) { return word == Untouched; }), "gpu buffer format: format " + std::to_string((word3 >> 12u) & 0x7fu) + " did not stop the invocation before its stores"); + } + std::puts("gpu buffer format 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/ImagePlaneAddressOnLine.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLine.cpp index 4614998ce7..465df1cabf 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLine.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLine.cpp @@ -22,6 +22,7 @@ using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Threads = 32; constexpr std::uint32_t Words = 32; constexpr std::uint32_t Size = 16; +constexpr std::uint32_t Mips = 5; constexpr std::uint32_t Format8888UNorm = 56; constexpr std::uint32_t Type1D = 8; constexpr std::uint32_t Type2D = 9; @@ -30,6 +31,7 @@ constexpr std::uint32_t LoadResult = 12; alignas(256) std::array Buffer{}; alignas(256) std::array Line{}; alignas(256) std::array Plane{}; +alignas(256) std::array MippedLine{}; alignas(256) constexpr std::array Code{ 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, @@ -39,11 +41,43 @@ alignas(256) constexpr std::array Code{ 0x80001201, 0xe070103c, 0x80001301, 0xbf810000, }; -alignas(256) constexpr std::array ExplicitLodCode{ +alignas(256) constexpr std::array ExplicitLodCode{ 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, - 0x80000501, 0xbf8c3f70, 0xf0900f08, 0x00610c02, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xbf810000, + 0x80000501, 0xbf8c3f70, 0xf0900f08, 0x00610c02, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, + 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xbf810000, }; +alignas(256) constexpr std::array LineLodCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301008, 0x80000301, 0xbf8c3f70, 0xf0900f00, 0x00610c02, 0xbf8c3f70, + 0xe0701020, 0x80000c01, 0xe0701024, 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xbf810000, +}; + +struct SamplerCase { + const char* name; + std::uint32_t clamp; + std::uint32_t filter; + std::uint32_t mipFilter; +}; + +constexpr std::uint32_t ClampWrap = 0; +constexpr std::uint32_t ClampEdge = 2; +constexpr std::uint32_t ClampBorder = 6; +constexpr std::uint32_t FilterPoint = 0; +constexpr std::uint32_t FilterLinear = 1; +constexpr std::uint32_t MipPoint = 1; +constexpr std::uint32_t MipLinear = 2; + +constexpr std::array SamplerCases{{ + {"point filtering, point mips, clamp to edge", ClampEdge, FilterPoint, MipPoint}, + {"point filtering, point mips, wrap", ClampWrap, FilterPoint, MipPoint}, + {"linear filtering, linear mips, clamp to edge", ClampEdge, FilterLinear, MipLinear}, + {"linear filtering, linear mips, wrap", ClampWrap, FilterLinear, MipLinear}, + {"linear filtering, linear mips, clamp to border", ClampBorder, FilterLinear, MipLinear}, +}}; + +constexpr std::array LodU{0.03125f, 0.33125f, 0.96875f, 0.0f, 1.0f, -0.2f, 1.3f, 0.6f}; +constexpr std::array Lods{0.0f, 1.0f, 2.0f, 1.25f, 2.75f, 3.6f, 6.0f, -1.0f}; + constexpr std::array LineV{0.0f, 0.5f, 1.0f, 1.5f, -0.25f, 3.0f, 0.25f, 0.75f}; constexpr std::array LineY{0u, 1u, 2u, 3u, 0xffffffffu, 7u, 0u, 5u}; @@ -81,6 +115,20 @@ std::array LineDescriptor() { }; } +std::array MippedLineDescriptor() { + const auto address = static_cast(reinterpret_cast(MippedLine.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + (Size - 1u) >> 2u, + 0xfacu | ((Mips - 1u) << 16u) | (Type1D << 28u), + 0u, + (Mips - 1u) << 4u, + 0u, + 0u, + }; +} + std::array PlaneDescriptor() { const auto address = static_cast(reinterpret_cast(Plane.data())); return { @@ -111,6 +159,25 @@ void FillTexture(std::span storage, const std::array MipTexel(std::uint32_t level, std::uint32_t x) { + return {10u + 40u * level, x * 12u + 3u, 200u - 30u * level, 255u - 7u * x - 11u * level}; +} + +void FillMippedLine() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(MippedLineDescriptor())); + Require(geometry.guestBytes <= MippedLine.size(), "image plane address on a line: the mipped texture does not fit the texel storage"); + Require(geometry.mips.size() == Mips, "image plane address on a line: the mipped texture has " + std::to_string(geometry.mips.size()) + " mips"); + std::fill(MippedLine.begin(), MippedLine.end(), 0xeeu); + for (std::uint32_t level = 0; level < Mips; ++level) { + const auto& mip = geometry.mips[level]; + for (std::uint32_t x = 0; x < (Size >> level); ++x) { + const auto value = MipTexel(level, x); + auto* texel = &MippedLine[geometry.GuestLayerOffset(0) + mip.tiledOffset + x * 4u]; + for (std::uint32_t component = 0; component < 4u; ++component) texel[component] = static_cast(value[component]); + } + } +} + 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}; @@ -120,10 +187,13 @@ std::array SamplerDescriptor() { return {0u, 0xfffu << 12u, 1u << 26u, 0u}; } -void Run(AgcDriver::VulkanDevice& device, std::span code, const std::array& texture) { +std::array SamplerDescriptor(const SamplerCase& sampler) { + return {sampler.clamp | (sampler.clamp << 3u) | (sampler.clamp << 6u), 0xfffu << 12u, (sampler.filter << 20u) | (sampler.filter << 22u) | (sampler.mipFilter << 26u), 0u}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, const std::array& texture, const std::array& sampler = SamplerDescriptor()) { std::vector userData(16, 0u); const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); - const auto sampler = SamplerDescriptor(); std::copy(buffer.begin(), buffer.end(), userData.begin()); std::copy(texture.begin(), texture.end(), userData.begin() + 4); std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); @@ -164,16 +234,68 @@ void CheckPlane() { } } -void RequireRefused(AgcDriver::VulkanDevice& device) { - std::string refusal; - try { - Run(device, ExplicitLodCode, LineDescriptor()); - } catch (const std::exception& error) { - refusal = error.what(); - } - Require(refusal.find("incompatible with the static runtime image interface") != std::string::npos, "image_sample_l 2d on a 1D texture was not refused: " + refusal); +float LaneU(std::uint32_t tid) { + return LodU[(tid + tid / 8u) % LodU.size()]; } +float LaneLod(std::uint32_t tid) { + return Lods[tid / 4u]; +} + +void FillLodInput() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::array words{Bits(LaneU(tid)), Bits(LineV[(tid / 2u) % LineV.size()]), Bits(LaneLod(tid)), 0u}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } +} + +std::array SampleResults() { + std::array results{}; + for (std::uint32_t tid = 0; tid < Threads; ++tid) std::copy_n(&Buffer[tid * Words + SampleResult], 4u, &results[tid * 4u]); + return results; +} + +std::uint32_t PointLevel(float lod) { + return static_cast(std::clamp(std::lround(lod), 0L, static_cast(Mips - 1u))); +} + +std::uint32_t PointTexel(float u, std::uint32_t level, std::uint32_t clamp) { + const auto width = static_cast(Size >> level); + const auto texel = static_cast(std::floor(u * static_cast(width))); + if (clamp == ClampWrap) return static_cast((texel % width + width) % width); + return static_cast(std::clamp(texel, 0, width - 1)); +} + +void CheckExplicitLod(AgcDriver::VulkanDevice& device) { + FillMippedLine(); + for (const auto& sampler : SamplerCases) { + const auto samplerWords = SamplerDescriptor(sampler); + FillLodInput(); + Run(device, LineLodCode, MippedLineDescriptor(), samplerWords); + const auto reference = SampleResults(); + FillLodInput(); + Run(device, ExplicitLodCode, MippedLineDescriptor(), samplerWords); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto u = LaneU(tid); + const auto lod = LaneLod(tid); + const auto what = std::string("image_sample_l 2d on a 1D texture with ") + sampler.name + ", u " + std::to_string(u) + ", v " + std::to_string(LineV[(tid / 2u) % LineV.size()]) + ", LOD " + std::to_string(lod); + for (std::uint32_t component = 0; component < 4u; ++component) { + const auto value = Buffer[tid * Words + SampleResult + component]; + const auto expected = reference[tid * 4u + component]; + Require(value == expected, what + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " has bits " + std::to_string(value) + ", image_sample_l 1d returned bits " + std::to_string(expected)); + } + if (sampler.filter == FilterPoint) { + const auto level = PointLevel(lod); + Expect(tid, SampleResult, MipTexel(level, PointTexel(u, level, sampler.clamp)), what); + } else if (sampler.clamp != ClampBorder) { + const auto mark = std::bit_cast(Buffer[tid * Words + SampleResult]) * 255.0f; + const auto expected = 10.0f + 40.0f * std::clamp(lod, 0.0f, static_cast(Mips - 1u)); + Require(std::fabs(mark - expected) <= 1.0f, what + ": thread " + std::to_string(tid) + " blends the levels to " + std::to_string(mark) + ", expected " + std::to_string(expected)); + } + } + } +} } int main() { @@ -188,7 +310,7 @@ int main() { FillInput(false); Run(*device, Code, PlaneDescriptor()); CheckPlane(); - RequireRefused(*device); + CheckExplicitLod(*device); std::puts("image plane address on line tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneBiasOnSingleLevelVolume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneBiasOnSingleLevelVolume.cpp new file mode 100644 index 0000000000..29875f7792 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneBiasOnSingleLevelVolume.cpp @@ -0,0 +1,244 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t Size = 16; +constexpr std::uint32_t Mips = 1; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type3D = 10; +constexpr float SecondCoordinateScale = 1.0f / static_cast(Size); +alignas(256) std::array Pixels{}; +alignas(256) std::array Volume{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array BiasPixelCode{ + 0x10040010, 0x06060300, 0x10060611, 0xf0940f08, 0x00610401, 0xbf8c3f70, 0xf800180f, 0x07060504, 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}}}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::array VolumeDescriptor() { + const auto address = static_cast(reinterpret_cast(Volume.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + 0xfacu | ((Mips - 1u) << 16u) | (Type3D << 28u), + 0u, + (Mips - 1u) << 4u, + 0u, + 0u, + }; +} + +std::array SamplerDescriptor() { + return {0u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +void FillTexture() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(VolumeDescriptor())); + Require(geometry.guestBytes <= Volume.size(), "single-level volume does not fit the texel storage"); + Require(geometry.mips.size() == 1u && geometry.imageDepth == 1u, "single-level volume geometry differs"); + std::fill(Volume.begin(), Volume.end(), 0xeeu); + const auto& mip = geometry.mips.front(); + for (std::uint32_t y = 0; y < Size; ++y) { + for (std::uint32_t x = 0; x < Size; ++x) { + auto* texel = &Volume[mip.tiledOffset + y * mip.pitchBytes + x * 4u]; + texel[0] = static_cast(x * 8u); + texel[1] = static_cast(y * 8u); + texel[2] = 99u; + texel[3] = 255u; + } + } +} + +void Draw(AgcDriver::VulkanDevice& device, std::array texture, std::array sampler, float bias, bool mixedSamplers = false) { + const auto& target = device.Target(); + alignas(256) std::array code{}; + std::copy_n(BiasPixelCode.begin(), 3u, code.begin()); + code[3] = 0x7e020212u; + std::copy(BiasPixelCode.begin() + 3, BiasPixelCode.end(), code.begin() + 4); + if (mixedSamplers) { + code[6] = 0xf0940f08u; + code[7] = 0x00a10801u; + code[8] = 0xbf8c3f70u; + code[9] = 0x06081104u; + code[10] = 0xf800180fu; + code[11] = 0x07060504u; + code[12] = 0xbf810000u; + } + const auto pixelCode = std::span(code).first(mixedSamplers ? 13u : 10u); + Pixels.fill(std::byte{0}); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.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, {}}, + {64, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = false; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + std::vector pixelUserData(24, 0u); + std::copy(texture.begin(), texture.end(), pixelUserData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), pixelUserData.begin() + 12); + pixelUserData[16] = std::bit_cast(1.0f / static_cast(Size)); + pixelUserData[17] = std::bit_cast(SecondCoordinateScale); + pixelUserData[18] = std::bit_cast(bias); + auto secondSampler = sampler; + secondSampler[0] &= ~(1u << 20u); + std::copy(secondSampler.begin(), secondSampler.end(), pixelUserData.begin() + 20); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(std::span(pixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {64, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(bool degamma = false) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + std::array expected{(x % Size) * 8u, ((x + y + 1u) % Size) * 8u, 99u, 255u}; + if (degamma) { + for (std::uint32_t component = 0; component < 3u; ++component) { + const float encoded = static_cast(expected[component]) / 255.0f; + const float linear = encoded <= 0.04045f ? encoded / 12.92f : std::pow((encoded + 0.055f) / 1.055f, 2.4f); + expected[component] = static_cast(std::lround(linear * 255.0f)); + } + } + for (std::uint32_t component = 0; component < 4u; ++component) { + const auto value = std::to_integer(Pixels[(y * Width + x) * 4u + component]); + Require(std::abs(static_cast(value) - static_cast(expected[component])) <= (degamma ? 1 : 0), "single-level volume bias: pixel " + std::to_string(x) + "," + std::to_string(y) + " component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component])); + } + } + } +} + +void RequireRefused(AgcDriver::VulkanDevice& device, std::array texture, std::array sampler, const char* expected) { + std::string refusal; + try { + Draw(device, texture, sampler, 0.0f); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find(expected) != std::string::npos, "single-level volume refusal differs: " + refusal); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillTexture(); + for (const float bias : {-16.0f, -0.5f, 0.0f, 0.5f, 16.0f}) { + Draw(*device, VolumeDescriptor(), SamplerDescriptor(), bias); + Check(); + } + for (const std::uint32_t reduction : {1u, 2u}) { + auto sampler = SamplerDescriptor(); + sampler[0] |= reduction << 29u; + Draw(*device, VolumeDescriptor(), sampler, 8.0f); + Check(); + } + auto integerTexture = VolumeDescriptor(); + integerTexture[1] = (integerTexture[1] & ~(0x1ffu << 20u)) | (60u << 20u); + auto forcedSampler = SamplerDescriptor(); + forcedSampler[0] |= 1u << 20u; + for (const float bias : {-16.0f, 0.0f, 16.0f}) { + Draw(*device, integerTexture, forcedSampler, bias); + Check(true); + } + std::string mixedRefusal; + try { + Draw(*device, integerTexture, forcedSampler, 0.0f, true); + } catch (const std::exception& error) { + mixedRefusal = error.what(); + } + Require(mixedRefusal.find("mixed sampler states") != std::string::npos, "mixed FORCE_DEGAMMA samplers were not refused: " + mixedRefusal); + auto texture = VolumeDescriptor(); + texture[4] = 1u; + RequireRefused(*device, texture, SamplerDescriptor(), "one slice and mip level zero only"); + texture = VolumeDescriptor(); + texture[3] |= 1u << 16u; + texture[5] |= 1u << 4u; + RequireRefused(*device, texture, SamplerDescriptor(), "one slice and mip level zero only"); + auto sampler = SamplerDescriptor(); + sampler[2] |= (1u << 20u) | (1u << 22u); + RequireRefused(*device, VolumeDescriptor(), sampler, "normalized point sampler"); + sampler = SamplerDescriptor(); + sampler[0] |= 1u << 15u; + RequireRefused(*device, VolumeDescriptor(), sampler, "normalized point sampler"); + std::puts("image plane bias on single-level volume 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/ImagePlaneCompareOnLine.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneCompareOnLine.cpp new file mode 100644 index 0000000000..56717c07df --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneCompareOnLine.cpp @@ -0,0 +1,155 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t Size = 16; +constexpr std::uint32_t Format16UNorm = 7; +constexpr std::uint32_t Type1D = 8; +constexpr std::uint32_t ReferenceWord = 6; +constexpr std::uint32_t Result = 8; +constexpr std::uint32_t Step = 4000; +constexpr std::uint32_t CompareLess = 1; +alignas(256) std::array Buffer{}; +alignas(256) std::array Line{}; + +alignas(256) constexpr std::array LevelZeroCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf0bc0108, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +alignas(256) constexpr std::array ImplicitLodCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf0a00108, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +constexpr std::array LineV{0.0f, 0.5f, 1.0f, 1.5f, -0.25f, 3.0f, 0.25f, 0.75f}; + +std::uint32_t TexelOf(std::uint32_t tid) { + return (tid * 7u + 3u) % Size; +} + +bool ReferenceBelow(std::uint32_t tid) { + return (tid & 1u) == 0u; +} + +std::uint32_t Bits(float value) { + return std::bit_cast(value); +} + +void FillInput() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = TexelOf(tid); + const float texel = static_cast((x + 1u) * Step) / 65535.0f; + const float half = static_cast(Step / 2u) / 65535.0f; + Buffer[tid * Words] = Bits((static_cast(x) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + 1u] = Bits(LineV[tid % LineV.size()]); + Buffer[tid * Words + ReferenceWord] = Bits(ReferenceBelow(tid) ? texel - half : texel + half); + } +} + +std::array LineDescriptor() { + const auto address = static_cast(reinterpret_cast(Line.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format16UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + (Size - 1u) >> 2u, + 0xfacu | (Type1D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void FillLine() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(LineDescriptor())); + Require(geometry.guestBytes <= Line.size(), "image plane compare on a line: the texture does not fit the texel storage"); + std::fill(Line.begin(), Line.end(), 0xeeu); + for (std::uint32_t x = 0; x < Size; ++x) { + const auto value = static_cast((x + 1u) * Step); + auto* texel = &Line[geometry.GuestLayerOffset(0) + geometry.mips.at(0).tiledOffset + x * 2u]; + texel[0] = static_cast(value); + texel[1] = static_cast(value >> 8u); + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array SamplerDescriptor() { + return {CompareLess << 12u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + const auto texture = LineDescriptor(); + const auto sampler = SamplerDescriptor(); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const std::string& op) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const float value = std::bit_cast(Buffer[tid * Words + Result]); + const float expected = ReferenceBelow(tid) ? 1.0f : 0.0f; + Require(value == expected, op + " 2d on a 1D texture with v " + std::to_string(LineV[tid % LineV.size()]) + ": thread " + std::to_string(tid) + " is " + std::to_string(value) + ", expected " + std::to_string(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillLine(); + FillInput(); + Run(*device, LevelZeroCode); + Check("image_sample_c_lz"); + FillInput(); + Run(*device, ImplicitLodCode); + Check("image_sample_c"); + std::puts("image plane compare on line 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/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index d5eb6a23e5..43f1799617 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -237,6 +237,22 @@ alignas(256) constexpr std::array ReadPointerCode{ 0xbf810000u, }; +alignas(256) constexpr std::array SplitRecordCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0x8f108202u, 0xf4200444u, + 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, 0xf4280a02u, 0x24000020u, 0x360200bfu, 0x7e020d01u, + 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c0f08u, 0x01090401u, 0x7e120202u, + 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) constexpr std::array IndirectRecordCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, 0xf4240b02u, 0x24000008u, 0xf40c0916u, 0xfa000020u, + 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c0f08u, + 0x01090401u, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) std::array Indirect{}; + constexpr std::array PointClamp{0x92u, (4u * 256u) << 12u, 0u, 0u}; constexpr std::array LinearWrap{0u, (4u * 256u) << 12u, (1u << 20u) | (1u << 22u), 0u}; constexpr std::array PointWrap{0u, (4u * 256u) << 12u, 1u << 24u, 0u}; @@ -407,6 +423,26 @@ Outcome RunPointer(AgcDriver::VulkanDevice& device, const std::array +Outcome RunIndirectRecords(AgcDriver::VulkanDevice& device, const std::array& code, std::uint32_t groups) { + const auto table = Buffer(Table.data(), static_cast(sizeof(Table))); + const auto keys = Buffer(Keys.data(), static_cast(sizeof(Keys))); + const auto output = Buffer(Output.data(), static_cast(sizeof(Output))); + std::copy(table.begin(), table.end(), Srt.begin()); + std::copy(keys.begin(), keys.end(), Srt.begin() + 4); + std::copy(output.begin(), output.end(), Srt.begin() + 8); + std::copy(PointClamp.begin(), PointClamp.end(), Srt.begin() + 12); + const std::span span(code); + const std::array memory{{ + {reinterpret_cast(span.data()), std::as_bytes(span)}, + {reinterpret_cast(Srt.data()), std::as_bytes(std::span(Srt))}, + {reinterpret_cast(Table.data()), std::as_bytes(std::span(Table))}, + {reinterpret_cast(Keys.data()), std::as_bytes(std::span(Keys))}, + {reinterpret_cast(Indirect.data()), std::as_bytes(std::span(Indirect))}, + }}; + return Execute(device, span, Srt.data(), memory, groups, Lanes, 64); +} + void SetEntry(std::uint32_t entry, const std::array& image) { auto* words = Root.data() + (PaletteOffset + entry * 32u) / 4u; std::fill(words, words + 8, 0u); @@ -1035,6 +1071,64 @@ void RunPointerTests(AgcDriver::VulkanDevice& device) { RequireRed(reads, 4, Lanes, [](std::uint32_t x) { return UintTexels[x]; }, "pointer image table read: key 0x1ff masked to entry 255"); } +void RunPointerRecordTests(AgcDriver::VulkanDevice& device) { + Fill(); + const auto floatImage = Image(FloatTexels.data(), Format32Float); + const auto uintImage = Image(UintTexels.data(), Format32UInt); + const auto sintImage = Image(SintTexels.data(), Format32SInt); + const std::array, 3> images{floatImage, uintImage, sintImage}; + const std::array, 3> texels{ + [](std::uint32_t x) { return std::bit_cast(FloatTexels[x]); }, + [](std::uint32_t x) { return UintTexels[x]; }, + [](std::uint32_t x) { return static_cast(SintTexels[x]); }, + }; + const std::array names{"float", "uint", "sint"}; + + Table = {}; + Indirect = {}; + Keys = {}; + for (std::uint32_t group = 0; group < 3; ++group) Keys[group] = 2u - group; + for (std::uint32_t record = 0; record < 3; ++record) { + const std::uint32_t entry = 2u - record; + std::copy(images[record].begin(), images[record].end(), Indirect.data() + entry * 16u + 8u); + const auto pointer = static_cast(reinterpret_cast(Indirect.data() + entry * 16u)); + Table[record * (Stride / 4u) + 2u] = static_cast(pointer); + Table[record * (Stride / 4u) + 3u] = static_cast(pointer >> 32u); + } + const auto indirect = RunIndirectRecords(device, IndirectRecordCode, 3); + Require(!Faulted(indirect), "image table: a T# behind a pointer in its record faulted:\n" + indirect.log); + Require(indirect.imageTableCount == 3u, "image table: T#s behind pointers in their records do not hold one element per distinct T# (" + std::to_string(indirect.imageTableCount) + ")"); + for (std::uint32_t group = 0; group < 3; ++group) RequireRed(indirect, group, Lanes, texels[2u - group], std::string(names[2u - group]) + " texels of a T# behind a pointer in its record"); + + const auto entry = Indirect.data() + 16u; + std::copy(floatImage.begin(), floatImage.end(), entry + 8u); + const auto changed = RunIndirectRecords(device, IndirectRecordCode, 3); + RequireRed(changed, 1, Lanes, texels[0], "float texels after the T# behind record 1's pointer changed"); +} + +void RunSplitRecordTests(AgcDriver::VulkanDevice& device) { + Fill(); + const auto floatImage = Image(FloatTexels.data(), Format32Float); + const auto uintImage = Image(UintTexels.data(), Format32UInt); + const auto sintImage = Image(SintTexels.data(), Format32SInt); + const std::array, 3> images{floatImage, uintImage, sintImage}; + const std::array, 3> texels{ + [](std::uint32_t x) { return std::bit_cast(FloatTexels[x]); }, + [](std::uint32_t x) { return UintTexels[x]; }, + [](std::uint32_t x) { return static_cast(SintTexels[x]); }, + }; + const std::array names{"float", "uint", "sint"}; + + Table = {}; + for (std::uint32_t record = 0; record < 3; ++record) SetRecord(record, PointClamp, images[record]); + Keys = {}; + for (std::uint32_t group = 0; group < 3; ++group) Keys[group] = 2u - group; + const auto split = Run(device, SplitRecordCode, 3, Lanes, 64); + Require(!Faulted(split), "image table: a T# read by two loads of its record faulted:\n" + split.log); + Require(split.imageTableCount == 3u, "image table: a T# read by two loads of its record does not hold one element per distinct T# (" + std::to_string(split.imageTableCount) + ")"); + for (std::uint32_t group = 0; group < 3; ++group) RequireRed(split, group, Lanes, texels[2u - group], std::string(names[2u - group]) + " texels of a T# read by two loads of its record"); +} + void RunPointerStorageWriteTests(AgcDriver::VulkanDevice& device) { alignas(256) static std::array records{}; alignas(256) static std::array otherRecords{}; @@ -1234,6 +1328,8 @@ int main() { RunModeTests(*device); RunStoreTests(*device); RunPointerTests(*device); + RunPointerRecordTests(*device); + RunSplitRecordTests(*device); RunPointerStorageWriteTests(*device); RunGraphicsTests(*device); std::puts("image table tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageVolumeAddressOnLine.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageVolumeAddressOnLine.cpp new file mode 100644 index 0000000000..b9027457bf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageVolumeAddressOnLine.cpp @@ -0,0 +1,233 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t Size = 16; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type1D = 8; +constexpr std::uint32_t Type3D = 10; +constexpr std::uint32_t Depth = 2; +constexpr std::uint32_t SampleResult = 8; +constexpr std::uint32_t LoadResult = 12; +alignas(256) std::array Buffer{}; +alignas(256) std::array Line{}; +alignas(256) std::array Volume{}; + +alignas(256) constexpr std::array Code{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xbf8c3f70, 0xf09c0f10, 0x00610c02, + 0xf0001f10, 0x00011005, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, 0x80000d01, 0xe0701028, + 0x80000e01, 0xe070102c, 0x80000f01, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, + 0x80001201, 0xe070103c, 0x80001301, 0xbf810000, +}; + +alignas(256) constexpr std::array ImplicitLodCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xbf8c3f70, 0xf0800f10, 0x00610c02, + 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, + 0x80000f01, 0xbf810000, +}; + +alignas(256) constexpr std::array ExplicitLodCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xbf8c3f70, 0xf0900f10, 0x00610c02, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xbf810000, +}; + +constexpr std::array LineV{0.0f, 0.5f, 1.0f, 1.5f, -0.25f, 3.0f, 0.25f, 0.75f}; +constexpr std::array LineR{0.0f, 0.5f, 1.0f, 1.5f, -0.25f, 3.0f, 0.25f, 0.75f}; +constexpr std::array LineY{0u, 1u, 2u, 3u, 0xffffffffu}; +constexpr std::array LineZ{0u, 1u, 0u, 2u, 3u, 0u, 0xffffffffu}; + +struct Lane { + std::uint32_t x; + std::uint32_t y; +}; + +Lane LaneOf(std::uint32_t tid) { + return {(tid * 7u + 3u) % Size, (tid * 5u + 1u) % Size}; +} + +std::uint32_t Bits(float value) { + return std::bit_cast(value); +} + +void FillInput(bool volume) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto lane = LaneOf(tid); + const auto u = Bits((static_cast(lane.x) + 0.5f) / static_cast(Size)); + const auto v = volume ? Bits((static_cast(lane.y) + 0.5f) / static_cast(Size)) : Bits(LineV[tid % LineV.size()]); + const auto r = volume ? Bits((static_cast(tid % Depth) + 0.5f) / static_cast(Depth)) : Bits(LineR[(tid + 3u) % LineR.size()]); + const auto y = volume ? lane.y : LineY[tid % LineY.size()]; + const auto z = volume ? tid % Depth : LineZ[tid % LineZ.size()]; + const std::array words{u, v, r, lane.x, y, z}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } +} + +std::uint8_t SliceMarker(std::uint32_t z) { + return static_cast(z * 100u + 7u); +} + +std::array LineDescriptor() { + const auto address = static_cast(reinterpret_cast(Line.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + (Size - 1u) >> 2u, + 0xfacu | (Type1D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +std::array VolumeDescriptor() { + const auto address = static_cast(reinterpret_cast(Volume.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + 0xfacu | (Type3D << 28u), + Depth - 1u, + 0u, + 0u, + 0u, + }; +} + +void FillTexture(std::span storage, const std::array& descriptor, std::uint32_t slices, std::uint32_t rows) { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(descriptor)); + Require(geometry.guestBytes <= storage.size(), "image volume address on a line: the texture does not fit the texel storage"); + const auto& mip = geometry.mips.at(0); + std::fill(storage.begin(), storage.end(), 0xeeu); + for (std::uint32_t z = 0; z < slices; ++z) { + for (std::uint32_t y = 0; y < rows; ++y) { + for (std::uint32_t x = 0; x < Size; ++x) { + auto* texel = &storage[geometry.GuestLayerOffset(z) + mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u]; + texel[0] = SliceMarker(z); + texel[1] = static_cast(x); + texel[2] = static_cast(y); + texel[3] = 255u; + } + } + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array SamplerDescriptor() { + return {0u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, const std::array& texture) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + const auto sampler = SamplerDescriptor(); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Expect(std::uint32_t tid, std::uint32_t result, const std::array& expected, const std::string& name) { + for (std::uint32_t component = 0; component < 4u; ++component) { + const float value = std::bit_cast(Buffer[tid * Words + result + component]) * 255.0f; + Require(std::lround(value) == static_cast(expected[component]), name + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component])); + } +} + +void CheckLine(const std::string& op) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::array texel{SliceMarker(0), LaneOf(tid).x, 0u, 255u}; + Expect(tid, SampleResult, texel, op + " 3d on a 1D texture with v " + std::to_string(LineV[tid % LineV.size()]) + " and r " + std::to_string(LineR[(tid + 3u) % LineR.size()])); + } +} + +void CheckLineLoad() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto z = LineZ[tid % LineZ.size()]; + const std::array texel = z == 0u ? std::array{SliceMarker(0), LaneOf(tid).x, 0u, 255u} : std::array{}; + Expect(tid, LoadResult, texel, "image_load 3d on a 1D texture with y " + std::to_string(static_cast(LineY[tid % LineY.size()])) + " and z " + std::to_string(static_cast(z))); + } +} + +void CheckVolume() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto lane = LaneOf(tid); + const std::array texel{SliceMarker(tid % Depth), lane.x, lane.y, 255u}; + Expect(tid, SampleResult, texel, "image_sample_lz 3d on a 3D texture"); + Expect(tid, LoadResult, texel, "image_load 3d on a 3D texture"); + } +} + +void RequireRefused(AgcDriver::VulkanDevice& device, std::span code, const std::string& name) { + std::string refusal; + try { + Run(device, code, LineDescriptor()); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find("incompatible with the static runtime image interface") != std::string::npos, name + " on a 1D texture was not refused: " + refusal); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillTexture(Line, LineDescriptor(), 1u, 1u); + FillInput(false); + Run(*device, Code, LineDescriptor()); + CheckLine("image_sample_lz"); + CheckLineLoad(); + FillInput(false); + Run(*device, ImplicitLodCode, LineDescriptor()); + CheckLine("image_sample"); + FillTexture(Volume, VolumeDescriptor(), Depth, Size); + FillInput(true); + Run(*device, Code, VolumeDescriptor()); + CheckVolume(); + RequireRefused(*device, ExplicitLodCode, "image_sample_l 3d"); + std::puts("image volume address on line 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/LdsAddF32.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsAddF32.cpp new file mode 100644 index 0000000000..abd04c9da1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsAddF32.cpp @@ -0,0 +1,184 @@ +#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 = 64; +constexpr std::uint32_t Results = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020083, 0xe0341000, 0x80000201, 0x34080082, 0xbf8c3f70, 0xd8340000, 0x00000204, 0xd8340400, + 0x00000204, 0xbf8cc07f, 0xd9540000, 0x05000304, 0xd8540400, 0x00000304, 0xbf8cc07f, 0xd8d80000, + 0x06000004, 0xd8d80400, 0x07000004, 0x34020084, 0xbf8cc07f, 0xe07c1000, 0x80010501, 0xbf810000, +}; + +constexpr std::array, Threads> Operands{{ + {0x7f800000, 0xff800000}, {0xff800000, 0x7f800000}, {0x3f800000, 0xbf800000}, {0x80000000, 0x80000000}, + {0x00000000, 0x80000000}, {0x7f7fffff, 0x7f7fffff}, {0x7fc00000, 0xffc00001}, {0x7f800001, 0x3f800000}, + {0x3f800000, 0xff812345}, {0xffc00001, 0x7f800001}, {0x00000001, 0x80000001}, {0x00800000, 0x80000001}, + {0x8110c65e, 0x8074b225}, {0x00d42774, 0x80d0391a}, {0x80800000, 0x00800001}, {0x80431a89, 0x01bcb83b}, + {0x01bec9b9, 0x81bab7f2}, {0x007fffff, 0x00400000}, {0x80000000, 0x80000001}, {0x80f9f2ca, 0x00cd2d23}, + {0x005921e2, 0x8187bc63}, {0x014e8a46, 0x0060b9ff}, {0x81cf89be, 0x805706c5}, {0x00e62d65, 0x80e62d68}, + {0x00a66021, 0x800b6944}, {0x802976a1, 0x0156a967}, {0x0197eca0, 0x803c270f}, {0x8027bc33, 0x00f88e02}, + {0x0104a791, 0x80c6fa02}, {0x001f9fb3, 0x01e94ee7}, {0x80000000, 0x007fffff}, {0x007fffff, 0x007fffff}, + {0x010d380e, 0x0076291d}, {0x80000001, 0x80000001}, {0x81dd408a, 0x8023bed7}, {0x00000001, 0x00400000}, + {0x8033deaa, 0x001a010b}, {0x003da089, 0x01a8d6f4}, {0x002eafb3, 0x01f74c53}, {0x01fd86b4, 0x00195d7b}, + {0x80476063, 0x00b49438}, {0x00800001, 0x80000001}, {0xe34217a6, 0x5b100f9a}, {0xff7fffff, 0x00800000}, + {0x00400000, 0xcb000001}, {0x4b000000, 0x807fffff}, {0xa63b9449, 0x2d91f2c3}, {0x819c564f, 0x802c240f}, + {0x81b5b8a2, 0x81a1681d}, {0xe640b291, 0x5766d41e}, {0x334bd7d6, 0x3d01a33b}, {0x01eeda5d, 0x80e7c8f6}, + {0x7f800001, 0x80000001}, {0xff812345, 0x80000000}, {0xffc00001, 0x7f7fffff}, {0xff812345, 0x7f7fffff}, + {0xffc00001, 0x00800001}, {0xff812345, 0x80800000}, {0x6ef2f0aa, 0xeef2feaa}, {0x019a87f3, 0x80efc545}, + {0x71223030, 0xf122303c}, {0xff7fffff, 0xff7fffff}, {0x80ec8133, 0x005f7c8c}, {0xe914c8da, 0x6914c8db}, +}}; + +constexpr std::array, 5> Measured{{ + { + 0xffc00000, 0xffc00000, 0x00000000, 0x80000000, 0x00000000, 0x7f800000, 0xffc00001, 0x7fc00001, + 0xffc12345, 0x7fc00001, 0x00000000, 0x00800000, 0x8110c65e, 0x00000000, 0x00000000, 0x01bcb83b, + 0x00000000, 0x00000000, 0x80000000, 0x80000000, 0x8187bc63, 0x014e8a46, 0x81cf89be, 0x80000000, + 0x00a66021, 0x0156a967, 0x0197eca0, 0x00f88e02, 0x00000000, 0x01e94ee7, 0x00000000, 0x00000000, + 0x010d380e, 0x80000000, 0x81dd408a, 0x00000000, 0x00000000, 0x01a8d6f4, 0x01f74c53, 0x01fd86b4, + 0x00b49438, 0x00800001, 0xe3421716, 0xff7fffff, 0xcb000001, 0x4b000000, 0x2d91f14c, 0x819c564f, + 0x822b9060, 0xe640b291, 0x3d01a348, 0x01b4e820, 0x7fc00001, 0xffc12345, 0xffc00001, 0xffc12345, + 0xffc00001, 0xffc12345, 0xe8e00000, 0x013d2d44, 0xe7400000, 0xff800000, 0x80ec8133, 0x5d800000, + }, + { + 0xffc00000, 0xffc00000, 0x00000000, 0x80000000, 0x00000000, 0x7f800000, 0xffc00001, 0x7fc00001, + 0xffc12345, 0x7fc00001, 0x00000000, 0x00000000, 0x814b1f70, 0x00000000, 0x00000000, 0x01abf199, + 0x00000000, 0x00bfffff, 0x80000000, 0x80000000, 0x8162e7d5, 0x017ee746, 0x81e54b6f, 0x80000000, + 0x009af6dd, 0x0141ee16, 0x0188e2dc, 0x00d0d1cf, 0x00000000, 0x01f136d4, 0x00000000, 0x00fffffe, + 0x01484c9c, 0x80000000, 0x81e63040, 0x00000000, 0x80000000, 0x01b83f16, 0x02017c20, 0x0201ef09, + 0x00000000, 0x00800000, 0xe3421716, 0xff7fffff, 0xcb000001, 0x4b000000, 0x2d91f14c, 0x81a75f53, + 0x822b9060, 0xe640b291, 0x3d01a348, 0x01b4e820, 0x7fc00001, 0xffc12345, 0xffc00001, 0xffc12345, + 0xffc00001, 0xffc12345, 0xe8e00000, 0x013d2d44, 0xe7400000, 0xff800000, 0x808d04a7, 0x5d800000, + }, + { + 0xffc00000, 0xffc00000, 0x00000000, 0x80000000, 0x00000000, 0x7f800000, 0xffc00001, 0x7fc00001, + 0xffc12345, 0x7fc00001, 0x00000000, 0x00800000, 0x8110c65e, 0x0003ee5a, 0x00000001, 0x01bcb83b, + 0x0010471c, 0x00000000, 0x80000000, 0x802cc5a7, 0x8187bc63, 0x014e8a46, 0x81cf89be, 0x80000003, + 0x00a66021, 0x0156a967, 0x0197eca0, 0x00f88e02, 0x00425520, 0x01e94ee7, 0x00000000, 0x00000000, + 0x010d380e, 0x80000000, 0x81dd408a, 0x00000000, 0x00000000, 0x01a8d6f4, 0x01f74c53, 0x01fd86b4, + 0x00b49438, 0x00800001, 0xe3421716, 0xff7fffff, 0xcb000001, 0x4b000000, 0x2d91f14c, 0x819c564f, + 0x822b9060, 0xe640b291, 0x3d01a348, 0x01b4e820, 0x7fc00001, 0xffc12345, 0xffc00001, 0xffc12345, + 0xffc00001, 0xffc12345, 0xe8e00000, 0x013d2d44, 0xe7400000, 0xff800000, 0x80ec8133, 0x5d800000, + }, + { + 0xffc00000, 0xffc00000, 0x00000000, 0x80000000, 0x00000000, 0x7f800000, 0xffc00001, 0x7fc00001, + 0xffc12345, 0x7fc00001, 0x00000000, 0x007fffff, 0x814b1f70, 0x0003ee5a, 0x00000001, 0x01abf199, + 0x0010471c, 0x00bfffff, 0x80000001, 0x802cc5a7, 0x8162e7d5, 0x017ee746, 0x81e54b6f, 0x80000003, + 0x009af6dd, 0x0141ee16, 0x0188e2dc, 0x00d0d1cf, 0x00425520, 0x01f136d4, 0x007fffff, 0x00fffffe, + 0x01484c9c, 0x80000002, 0x81e63040, 0x00400001, 0x8019dd9f, 0x01b83f16, 0x02017c20, 0x0201ef09, + 0x006d33d5, 0x00800000, 0xe3421716, 0xff7fffff, 0xcb000001, 0x4b000000, 0x2d91f14c, 0x81a75f53, + 0x822b9060, 0xe640b291, 0x3d01a348, 0x01b4e820, 0x7fc00001, 0xffc12345, 0xffc00001, 0xffc12345, + 0xffc00001, 0xffc12345, 0xe8e00000, 0x013d2d44, 0xe7400000, 0xff800000, 0x808d04a7, 0x5d800000, + }, + { + 0xffc00000, 0xffc00000, 0x00000000, 0x80000000, 0x00000000, 0x7f800000, 0xffc00001, 0x7fc00001, + 0xffc12345, 0x7fc00001, 0x00000000, 0x007fffff, 0x814b1f70, 0x0003ee5a, 0x00000001, 0x01abf199, + 0x0010471c, 0x00bfffff, 0x80000001, 0x802cc5a7, 0x8162e7d5, 0x017ee746, 0x81e54b6f, 0x80000003, + 0x009af6dd, 0x0141ee16, 0x0188e2dc, 0x00d0d1cf, 0x00425520, 0x01f136d4, 0x007fffff, 0x00fffffe, + 0x01484c9c, 0x80000002, 0x81e63040, 0x00400001, 0x8019dd9f, 0x01b83f16, 0x02017c20, 0x0201ef09, + 0x006d33d5, 0x00800000, 0xe3421716, 0xff7fffff, 0xcb000001, 0x4b000000, 0x2d91f14c, 0x81a75f53, + 0x822b9060, 0xe640b291, 0x3d01a348, 0x01b4e820, 0x7fc00001, 0xffc12345, 0xffc00001, 0xffc12345, + 0xffc00001, 0xffc12345, 0xe8e00000, 0x013d2d44, 0xe7400000, 0xff800000, 0x808d04a7, 0x5d800000, + }, +}}; + +constexpr std::array FloatModes{0x00u | (3u << 6u), 0x10u | (3u << 6u), 0x20u | (3u << 6u), 0x30u | (3u << 6u), 0x33u | (3u << 6u)}; + +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, std::uint32_t floatMode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * 2u] = Operands[tid][0]; + Input[tid * 2u + 1u] = Operands[tid][1]; + } + 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}, 2048u, {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{floatMode, true, false, false}; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + device.WaitIdle(); +} + +bool Denormal(std::uint32_t value) { + return (value & 0x7f800000u) == 0u && (value & 0x007fffffu) != 0u; +} + +bool HostDependent(std::uint32_t mode, std::uint32_t tid) { + const bool keepInputs = ((FloatModes[mode] >> 4u) & 1u) != 0u; + const bool keepResult = ((FloatModes[mode] >> 5u) & 1u) != 0u; + return (keepInputs && (Denormal(Operands[tid][0]) || Denormal(Operands[tid][1]))) || (keepResult && Denormal(Measured[mode][tid])); +} + +std::uint32_t Check(std::uint32_t mode) { + std::uint32_t checked = 0; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + if (HostDependent(mode, tid)) continue; + ++checked; + const std::uint32_t* out = &Output[tid * Results]; + const std::string row = "FLOAT_MODE " + Hex(FloatModes[mode]) + " lane " + std::to_string(tid) + " (" + Hex(Operands[tid][0]) + " + " + Hex(Operands[tid][1]) + ")"; + Require(out[0] == Operands[tid][0], "lds add f32: ds_add_rtn_f32 " + row + " returned " + Hex(out[0])); + Require(out[1] == Measured[mode][tid], "lds add f32: ds_add_rtn_f32 " + row + " stored " + Hex(out[1]) + ", expected " + Hex(Measured[mode][tid])); + Require(out[2] == Measured[mode][tid], "lds add f32: ds_add_f32 " + row + " stored " + Hex(out[2]) + ", expected " + Hex(Measured[mode][tid])); + } + return checked; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + std::uint32_t checked = 0; + for (std::uint32_t mode = 0; mode < FloatModes.size(); ++mode) { + Run(*device, FloatModes[mode]); + checked += Check(mode); + } + Require(checked >= 200u, "lds add f32: only " + std::to_string(checked) + " rows were independent of host denormal preservation"); + std::puts("lds add f32 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/PartiallyResidentSnapshot.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp index 4bdf081efe..005a1e506f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp @@ -262,6 +262,51 @@ void Run(const Context& context, ReservedSurface& surface) { AgcDriver::GuestMemory::SetFlushHook(nullptr); } +#ifdef _WIN32 +void UntrackedHint(const Context& context, ReservedSurface& surface) { + const auto address = surface.Address(); + const auto words = Descriptor(address); + surface.Commit(0, SurfaceBytes); + std::memset(surface.Data(), 0x5a, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + AgcDriver::GuestMemory::Unwatch(address, SurfaceBytes); + Require(!AgcDriver::GuestMemory::Watched(address, SurfaceBytes) && AgcDriver::GuestMemory::UntrackedHint(address, SurfaceBytes) != 0, "the surface did not leave the write watch with an untracked hint"); + surfaceBegin = address; + surfaceEnd = address + SurfaceBytes; + std::shared_ptr previous; + const auto sample = [&](const std::string& when, bool compared, bool same) { + AgcDriver::GuestMemory::BumpCollectEpoch(); + surfaceReads = 0; + auto texture = CachedSampledTexture(context, words); + Require(texture != nullptr && !texture->ViewsStorageImage(), "the untracked surface was not sampled through a snapshot " + when); + const auto reads = surfaceReads.load(); + if (compared && reads == 0) throw std::runtime_error("the cached snapshot of the untracked surface was not compared with guest memory " + when); + if (!compared && reads != 0) throw std::runtime_error("the cached snapshot of the untracked surface was compared with guest memory " + when + " although its untracked hint did not move (" + std::to_string(reads) + " reads)"); + if (previous != nullptr && same != (texture == previous)) throw std::runtime_error(std::string("sampling the untracked surface ") + when + (same ? " replaced the snapshot" : " kept the stale snapshot")); + previous = texture; + }; + AgcDriver::GuestMemory::SetFlushHook(&CountReads); + try { + sample("at its first use", true, false); + for (int epoch = 0; epoch < 50; ++epoch) sample("in a collect epoch without a store", false, true); + surface.Data()[0x100] = 0x11; + sample("after a CPU write", true, false); + sample("after the re-read of the written block", false, true); + auto* stored = static_cast(surface.Data()) + 2 * BlockBytes + 0x40; + *stored = *stored; + sample("after a CPU store of the bytes already there", true, true); + sample("after the compare that followed that store", false, true); + AgcDriver::GuestMemory::MarkWritten(address + 0x200, 4); + sample("after a driver store", true, true); + sample("after the compare that followed the driver store", false, true); + } catch (...) { + AgcDriver::GuestMemory::SetFlushHook(nullptr); + throw; + } + AgcDriver::GuestMemory::SetFlushHook(nullptr); +} +#endif + void PendingStores(const Context& context, ReservedSurface& surface) { const auto address = surface.Address(); const auto words = Descriptor(address); @@ -326,7 +371,14 @@ int main(int argc, char** argv) { ReservedSurface surface; std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); if (argc == 2 && std::string_view(argv[1]) == "--pending-stores") PendingStores(context, surface); - else Run(context, surface); + else if (argc == 2 && std::string_view(argv[1]) == "--untracked-hint") { +#ifdef _WIN32 + UntrackedHint(context, surface); +#else + std::puts("skipped, only Windows host imports leave the write watch with a hint"); + return Skipped; +#endif + } else Run(context, surface); ClearCachedTextures(context.device); } device.reset(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp new file mode 100644 index 0000000000..2233d80ba3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp @@ -0,0 +1,150 @@ +#include "VulkanTestDevice.hpp" +#include "SceShaders.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps); + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) const std::array VertexCode{0x7e0002ffu, 0, 0x7e0202ffu, 0, 0x7e0402ffu, 0, 0x7e0602ffu, 0x3f800000u, 0xf80008cfu, 0x03020100u, 0xbf810000u}; +alignas(256) const std::array PixelCode{0xbf810000u}; + +struct VertexHeader { + Shader shader{}; + std::array registers{}; + ShaderSpecialRegs specials{}; + ShaderUserData users{}; + std::array context{{{0x2d5, 0x2000}}}; + + VertexHeader() { + const auto address = reinterpret_cast(VertexCode.data()); + shader.file_header = 0x34333231u; + shader.version = 0x18; + shader.header_size = sizeof(VertexHeader); + shader.shader_size = sizeof(VertexCode); + shader.code = VertexCode.data(); + shader.type = 2; + registers = {{{0xc8, static_cast(address >> 8u)}, {0xc9, static_cast(address >> 40u)}, {0x8b, 0}, {0x8a, 0}, {0xca, 0}, {0xcb, 0}, {0xcc, 0}}}; + shader.sh_registers = registers.data(); + shader.num_sh_registers = registers.size(); + shader.cx_registers = context.data(); + shader.num_cx_registers = context.size(); + shader.specials = &specials; + shader.user_data = &users; + specials.dispatch_modifier = 0x8000; + } +}; + +struct PixelHeader { + Shader shader{}; + std::array registers{}; + std::array context{{{0x1b3, 2}, {0x1b4, 2}, {0x1b6, 1}}}; + + PixelHeader() { + const auto address = reinterpret_cast(PixelCode.data()); + shader.file_header = 0x34333231u; + shader.version = 0x18; + shader.header_size = sizeof(PixelHeader); + shader.shader_size = sizeof(PixelCode); + shader.code = PixelCode.data(); + shader.type = 1; + registers = {{{0x008, static_cast(address >> 8u)}, {0x009, static_cast(address >> 40u)}, {0x00a, 0}, {0x00b, 0}}}; + shader.sh_registers = registers.data(); + shader.num_sh_registers = registers.size(); + shader.cx_registers = context.data(); + shader.num_cx_registers = context.size(); + } +}; + +class HeldPreparationThreads { +public: + HeldPreparationThreads() { + for (unsigned index = 0; index < std::thread::hardware_concurrency() + 2u; ++index) { + AgcDriver::DriverDetail::SubmitRegistrationPreparation([state = state] { + std::unique_lock lock(state->mutex); + state->changed.wait(lock, [&] { return state->released; }); + }); + } + } + + ~HeldPreparationThreads() { + Release(); + } + + void Release() { + { + std::lock_guard lock(state->mutex); + state->released = true; + } + state->changed.notify_all(); + } + +private: + struct State { + std::mutex mutex; + std::condition_variable changed; + bool released = false; + }; + std::shared_ptr state = std::make_shared(); +}; + +void RegistrationBesidePendingLink() { + VertexHeader vertex; + VertexHeader other; + PixelHeader pixel; + AgcDriverRegisterShader_nid_postfix(&pixel.shader); + std::future link; + std::future registration; + bool registeredBeside = false; + std::array mapping{}; + { + HeldPreparationThreads held; + AgcDriverRegisterShader_nid_postfix(&vertex.shader); + link = std::async(std::launch::async, [&] { return sceAgcCreateInterpolantMapping_0100(mapping.data(), &vertex.shader, &pixel.shader); }); + std::this_thread::sleep_for(std::chrono::seconds(1)); + Require(link.wait_for(std::chrono::seconds(0)) == std::future_status::timeout, "the link did not wait for the vertex shader's registration preparation"); + registration = std::async(std::launch::async, [&] { AgcDriverRegisterShader_nid_postfix(&other.shader); }); + registeredBeside = registration.wait_for(std::chrono::seconds(5)) == std::future_status::ready; + held.Release(); + } + registration.get(); + Require(link.get() == 0, "the interpolant mapping failed"); + Require(registeredBeside, "a link waiting for a registration preparation blocked the registration of another shader"); + for (std::uint32_t index = 0; index < mapping.size(); ++index) Require(mapping[index].offset == 0x191u + index, "the interpolant mapping wrote another register"); +} + +} + +int main() { + try { + if (!OpenVulkanTestDevice()) return VulkanTestSkipped; + RegistrationBesidePendingLink(); + AgcDriverShutdown_nid_postfix(); + std::cout << "preparation scope settle tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp index 3464b69a22..b296b49beb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp @@ -394,6 +394,87 @@ void PrebuildPolicyReadback(AgcDriver::VulkanDevice& device, bool disabled) { Require(std::ranges::all_of(pixels, [](const auto value) { return value == 0xffffffffu; }), "prebuild policy changed native prepared-shader readback"); } +void RetainedPixelMatchesNullProgram(AgcDriver::VulkanDevice& device) { + using namespace AgcDriver::DriverDetail; + Fixture front; + Fixture retained; + front.Initialize(2u); + retained.Initialize(1u); + const auto null = std::make_shared(PrepareNullPixelProgram(device)); + ShaderRegistry registry; + for (const auto& snapshot : {front.snapshot, retained.snapshot, null}) registry.emplace(snapshot->codeAddress, std::vector>{snapshot}); + AgcDriver::QueueState queue{}; + queue.context[0x8e] = queue.context[0x8f] = queue.context[0x203] = 0; + queue.context[0x2dc] = 0xaa00; + queue.context[0x1c4] = 1; + front.Bind(queue, 0xc8u, 0x8bu); + queue.shader[0x8bu] = 0; + retained.Bind(queue, 0x008u, 0x00bu); + queue.shader[0x00bu] = (31u << 1u) | (1u << 27u); + for (std::uint32_t i = 0; i < 30; ++i) queue.shader[0x00cu + i] = 0xdead0000u + i; + queue.context[0x1b6] = queue.context[0x1b3] = queue.context[0x1b4] = queue.context[0x1c5] = 0xffffffffu; + auto captured = queue; + captured.shader[0x00bu] = 20u << 1u; + DrawDecode capturedDraw{}; + capturedDraw.state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; + DecodeGraphicsPrograms(capturedDraw, captured, registry, false, true); + Require(capturedDraw.programs.back().snapshot == null && capturedDraw.programs.back().userData.empty(), "a retained pixel program with a valid 20-word resource count consumed unrelated userdata"); + for (const auto wave : {64u, 32u}) { + DrawDecode draw{}; + draw.state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; + draw.state.stages.vertexWaveSize = 32u; + draw.state.stages.fragmentWaveSize = wave; + std::vector reads; + AgcDriver::Graphics::RegisterReadLog() = &reads; + try { + DecodeGraphicsPrograms(draw, queue, registry, false, true); + draw.pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, {}, AgcDriver::Graphics::PixelProgramSkipped(queue)); + } catch (...) { + AgcDriver::Graphics::RegisterReadLog() = nullptr; + throw; + } + AgcDriver::Graphics::RegisterReadLog() = nullptr; + Require(draw.roles.size() == 2 && draw.roles.back() == ShaderRecompiler::ProgramRole::Fragment, "null substitution changed stage linking"); + Require(draw.programs.back().snapshot == null && draw.programs.back().userData.empty(), "an unused retained pixel program consumed unrelated userdata"); + Require(draw.pixel.interpolatorCount == 0 && !draw.pixel.depthExportEnable && !draw.pixel.stencilRefExportEnable && !draw.pixel.sampleMaskExportEnable && !draw.pixel.pixelKillEnable, "the substituted pixel program inherited stale inputs or exports"); + for (const auto mode : draw.pixel.targetOutputMode) Require(mode == 0, "the substituted pixel program inherited stale color formats"); + for (const auto read : reads) { + Require(AgcDriver::Graphics::DrawKeyCovers(read), "a pixel substitution dependency is absent from the draw key"); + Require(read.bank != AgcDriver::Graphics::RegisterBank::Shader || read.offset < 0x00bu || read.offset > 0x02bu, "the substituted program read fragment resources or userdata"); + } + const auto stages = PrepareGraphicsStages(draw, device.Target()); + Require(stages.size() == 2 && stages.back().entry.handle != nullptr, "a retained-format null program was not prepared"); + std::vector linked; + std::vector memory; + for (std::size_t i = 0; i < draw.programs.size(); ++i) { + const auto& program = draw.programs[i]; + linked.push_back({draw.roles[i], program.binary, program.userDataBase, program.firstUserSgpr, program.userData}); + memory.insert(memory.end(), program.memory.begin(), program.memory.end()); + } + const auto& fragment = draw.programs.back(); + const auto offset = stages.back().pushOffset; + const auto room = AgcDriver::Graphics::PipelinePushSlotBytes - offset % AgcDriver::Graphics::PipelinePushSlotBytes; + ShaderRecompiler::RecompileRequest request{fragment.binary, {wave, 0, {}, {}, draw.pixel, {}, memory, RegisteredFloatMode(*null)}, device.Target(), {0, 0, offset, room}, ShaderRecompiler::GraphicsCompileContext{0, linked, {}, {}, {}}}; + static_cast(InvocationFor(*null, fragment.codeOffset, request)); + const auto handle = SourceHandleFor(*null, fragment.codeOffset, request); + const auto entries = null->prepared->entries.size(); + static_cast(InvocationFor(*null, fragment.codeOffset, request)); + Require(null->prepared->entries.size() == entries && SourceHandleFor(*null, fragment.codeOffset, request) == handle, "a repeated null program did not reuse its prepared variant"); + } + queue.shader[0x00bu] = 2; + queue.shader[0x00cu] = 0x12345678u; + for (const auto trigger : {0u, 1u, 2u}) { + auto active = queue; + if (trigger == 0) active.context[0x8e] = active.context[0x8f] = 0xf; + if (trigger == 1) active.context[0x203] = 0x400; + if (trigger == 2) active.context[0x2dc] = 0xaa01; + DrawDecode draw{}; + draw.state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; + DecodeGraphicsPrograms(draw, active, registry, false, true); + Require(draw.programs.back().snapshot == retained.snapshot && draw.programs.back().userData == std::vector{0x12345678u}, "an enabled consumer did not restore the retained pixel program"); + } +} + void NullPixelMatchesRegistration(AgcDriver::VulkanDevice& device) { using namespace AgcDriver::DriverDetail; Fixture front; @@ -449,6 +530,7 @@ int main(int argc, char** argv) { return 0; } const auto dump = argc == 2 ? std::filesystem::path(argv[1]) : std::filesystem::path{}; + RetainedPixelMatchesNullProgram(*device); NullPixelMatchesRegistration(*device); Check(*device, AgcDriver::Graphics::ShaderPath::Vertex, dump); Check(*device, AgcDriver::Graphics::ShaderPath::Geometry, dump); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index c4fb6eba1f..777dd733f4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -304,6 +304,31 @@ std::exception_ptr InjectedFailure() { } } +void AwaitedRegistrationPreparation() { + std::atomic ran{false}; + AgcDriver::DriverDetail::SubmitRegistrationPreparation([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + ran.store(true, std::memory_order_release); + }); + AgcDriver::DriverDetail::AwaitRegistrationPreparation(); + Require(ran.load(std::memory_order_acquire), "a submission did not wait for the registration preparation queued before it"); + std::atomic slow{false}; + std::atomic fast{false}; + AgcDriver::DriverDetail::SubmitRegistrationPreparation([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + slow.store(true, std::memory_order_release); + }); + auto waiter = std::async(std::launch::async, [&] { + AgcDriver::DriverDetail::AwaitRegistrationPreparation(); + return slow.load(std::memory_order_acquire); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + AgcDriver::DriverDetail::SubmitRegistrationPreparation([&] { fast.store(true, std::memory_order_release); }); + Require(waiter.get(), "a submission stopped waiting for an earlier preparation once a later one finished"); + AgcDriver::DriverDetail::AwaitRegistrationPreparation(); + Require(fast.load(std::memory_order_acquire), "the later preparation did not run"); +} + void PendingRegistrationPreparation(AgcDriver::VulkanDevice& device) { alignas(256) const std::array code{0xbf810000u}; std::array users{}; @@ -673,6 +698,7 @@ int main(int argc, char** argv) { RegisteredVertexWorkgroup(device->Target()); FailureCapture(*device); PendingRegistrationPreparation(*device); + AwaitedRegistrationPreparation(); Require(argc != 2 || std::string_view(argv[1]) != "--fail-before-registration", "injected failure before registration"); PrepareMultisampledStorage(*device); device.reset(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/QuadPixelMask.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/QuadPixelMask.cpp new file mode 100644 index 0000000000..613daa2001 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/QuadPixelMask.cpp @@ -0,0 +1,189 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 32; +constexpr std::uint32_t TextureWidth = Width * 4u; +constexpr std::uint32_t TextureHeight = Height * 4u; +constexpr std::uint32_t TextureLevels = 9; +constexpr std::uint32_t SampledLevel = 2; +constexpr std::uint32_t Format8888Unorm = 56; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t DemoteToHelperInvocation = 5379; +alignas(256) std::array Pixels{}; +alignas(4096) std::array Texels{}; +alignas(256) 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) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array ConstantPixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array SampledPixelCode{ + 0xbe8c047e, 0xbefe0a7e, 0x100000ff, 0x3c800000, 0x100202ff, 0x3d000000, 0xf0800f08, 0x00400200, + 0xbf8c3f70, 0x87fe0c7e, 0xf800180f, 0x05040302, 0xbf810000, +}; + +std::array LevelColor(std::uint32_t level) { + return {static_cast(32u * (level + 1u)), static_cast(255u - 16u * level), static_cast(0x40u + level), 255u}; +} + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::array TextureDescriptor() { + const auto address = static_cast(reinterpret_cast(Texels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888Unorm << 20u) | (((TextureWidth - 1u) & 3u) << 30u), + ((TextureWidth - 1u) >> 2u) | ((TextureHeight - 1u) << 14u), + 0xfacu | ((TextureLevels - 1u) << 16u) | (Type2D << 28u), + 0u, (TextureLevels - 1u) << 4u, 0u, 0u, + }; +} + +std::array SamplerDescriptor() { + return {0u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +void FillTexture() { + const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, Format8888Unorm, TextureWidth, TextureHeight, TextureLevels); + Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= Texels.size(), "the mipmapped test texture does not fit its storage"); + for (std::uint32_t level = 0; level < TextureLevels; ++level) { + const auto color = LevelColor(level); + for (std::uint64_t offset = mips[level].tiledOffset; offset < mips[level].tiledOffset + mips[level].tiledSize; offset += 4u) { + std::memcpy(Texels.data() + offset, color.data(), color.size()); + } + } +} + +void Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, std::uint8_t quadPixelMask) { + Pixels.fill(std::byte{0}); + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.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, {}}, + {64, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + pixel.quadPixelMask = quadPixelMask; + std::vector pixelUserData(12, 0u); + const auto texture = TextureDescriptor(); + const auto sampler = SamplerDescriptor(); + std::copy(texture.begin(), texture.end(), pixelUserData.begin()); + std::copy(sampler.begin(), sampler.end(), pixelUserData.begin() + 8); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {64, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(const std::string& what, std::uint8_t quadPixelMask, std::array color) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const bool covered = ((quadPixelMask >> ((x & 1u) | ((y & 1u) << 1u))) & 1u) != 0; + const auto* written = &Pixels[(y * Width + x) * 4u]; + for (std::uint32_t channel = 0; channel < 4u; ++channel) { + const auto expected = covered ? color[channel] : 0u; + const auto value = std::to_integer(written[channel]); + Require(value == expected, what + " with quad pixel mask " + std::to_string(quadPixelMask) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(value) + ", expected " + std::to_string(expected)); + } + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto capabilities = device->Target().supportedCapabilities; + const bool demote = std::find(capabilities.begin(), capabilities.end(), DemoteToHelperInvocation) != capabilities.end(); + FillTexture(); + for (const std::uint8_t mask : {std::uint8_t{0xf}, std::uint8_t{0x2}, std::uint8_t{0x9}}) { + Draw(*device, ConstantPixelCode, mask); + Check("constant color", mask, {255u, 255u, 255u, 255u}); + if (mask == 0xf || demote) { + Draw(*device, SampledPixelCode, mask); + Check("implicit-LOD sample", mask, LevelColor(SampledLevel)); + continue; + } + try { + Draw(*device, SampledPixelCode, mask); + } catch (const std::exception& error) { + Require(std::string(error.what()).find("shaderDemoteToHelperInvocation") != std::string::npos, std::string("an implicit-LOD sample under a quad pixel mask failed for another reason: ") + error.what()); + continue; + } + Require(false, "an implicit-LOD sample under quad pixel mask " + std::to_string(mask) + " was drawn without demote to helper invocation"); + } + std::printf("quad pixel mask tests passed (%s)\n", demote ? "demote to helper invocation" : "kill, derivatives refused"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskDepthStencil.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskDepthStencil.cpp new file mode 100644 index 0000000000..412ceb0a5d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskDepthStencil.cpp @@ -0,0 +1,215 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.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::uint32_t EarlyZThenLateZ = 0x10u; +constexpr std::uint8_t Reference = 0x5au; +constexpr std::byte Kept{0x40}; +alignas(256) std::array Pixels{}; +alignas(256) std::array DepthMemory{}; +alignas(256) std::array StencilMemory{}; +alignas(256) std::array Stores{}; + +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 StoringPixelCode{ + 0x7e040f00, 0x7e060f01, 0x34060686, 0x4a040702, 0x34040482, 0x7e080281, 0xe0705000, 0x80000402, + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +using Triangle = std::array, 3>; +alignas(256) constexpr Triangle Middle{{{-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) constexpr Triangle Near{{{-1.0f, -1.0f, 0.25f, 1.0f}, {3.0f, -1.0f, 0.25f, 1.0f}, {-1.0f, 3.0f, 0.25f, 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, stride == 0u ? 0x31016facu : 0x01016facu}; +} + +struct Pass { + std::array quadSampleMask{1, 1, 1, 1}; + const Triangle* triangle = &Middle; + std::span pixelCode = PlainPixelCode; + std::uint32_t shaderControl = EarlyZThenLateZ; + bool depthTest = false; + bool depthWrite = false; + VkCompareOp depthCompare = VK_COMPARE_OP_ALWAYS; + VkStencilOpState stencil{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0u, 0u}; +}; + +void Draw(AgcDriver::VulkanDevice& device, const Pass& pass) { + Pixels.fill(Kept); + Stores.fill(0); + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(pass.triangle->data(), 16u, static_cast(pass.triangle->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()); + + 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, 0.0f, 0}; + state.depthTest = pass.depthTest; + state.depthWrite = pass.depthWrite; + state.depthCompare = pass.depthCompare; + state.stencilTest = true; + state.stencilFront = pass.stencil; + state.stencilBack = pass.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 = {}; + state.quadSampleMask = pass.quadSampleMask; + state.pixelEarlyZ = AgcDriver::Graphics::PixelEarlyZ(pass.shaderControl); + + const AgcDriver::Registers context{{0x1b3, 0x300u}, {0x1b4, 0x300u}, {0x1b6, 0u}, {0x1c5, 9u}, {0x203, pass.shaderControl}}; + auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + Require(pixel.earlyZ == state.pixelEarlyZ, "the pixel stage and the decoded state disagree on early Z"); + pixel.quadPixelMask = AgcDriver::Graphics::QuadPixelMask(state); + std::vector pixelUserData(4, 0u); + const auto stores = BufferDescriptor(Stores.data(), 0u, static_cast(sizeof(Stores))); + std::copy(stores.begin(), stores.end(), pixelUserData.begin()); + const std::array pixelMemory{{{reinterpret_cast(pass.pixelCode.data()), std::as_bytes(pass.pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pass.pixelCode.data()), pass.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)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(pass.triangle->size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(const std::function& drawn, const std::string& 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, what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") is " + std::to_string(static_cast(actual)) + ", expected " + (drawn(x, y) ? "drawn" : "kept")); + } + } +} + +bool Covered(std::uint8_t quadPixelMask, std::uint32_t x, std::uint32_t y) { + return ((quadPixelMask >> ((x & 1u) | ((y & 1u) << 1u))) & 1u) != 0u; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const VkStencilOpState replace{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, Reference}; + const VkStencilOpState equal{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, 0xffu, 0u, Reference}; + const VkStencilOpState always{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0u, 0u}; + const VkStencilOpState zero{VK_STENCIL_OP_ZERO, VK_STENCIL_OP_ZERO, VK_STENCIL_OP_ZERO, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 0u}; + for (const std::uint8_t quadPixelMask : {std::uint8_t{0x1}, std::uint8_t{0x2}, std::uint8_t{0x9}}) { + const auto mask = "quad pixel mask " + std::to_string(quadPixelMask); + Pass reset; + reset.triangle = &Near; + reset.depthTest = true; + reset.depthWrite = true; + reset.stencil = zero; + Draw(*device, reset); + Check([](std::uint32_t, std::uint32_t) { return true; }, mask + ", the full-screen draw that resets depth to 0.25 and stencil to 0"); + Pass gbuffer; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) gbuffer.quadSampleMask[pixel] = static_cast((quadPixelMask >> pixel) & 1u); + gbuffer.depthTest = true; + gbuffer.depthWrite = true; + gbuffer.depthCompare = VK_COMPARE_OP_GREATER_OR_EQUAL; + gbuffer.stencil = replace; + Draw(*device, gbuffer); + Check([&](std::uint32_t x, std::uint32_t y) { return Covered(quadPixelMask, x, y); }, mask + ", the early-Z GEQUAL draw with depth writes and stencil REPLACE"); + Pass stencilProbe; + stencilProbe.stencil = equal; + Draw(*device, stencilProbe); + Check([&](std::uint32_t x, std::uint32_t y) { return Covered(quadPixelMask, x, y); }, mask + ", stencil equal to the replaced reference"); + Pass depthProbe; + depthProbe.triangle = &Near; + depthProbe.depthTest = true; + depthProbe.depthCompare = VK_COMPARE_OP_LESS; + depthProbe.stencil = always; + Draw(*device, depthProbe); + Check([&](std::uint32_t x, std::uint32_t y) { return Covered(quadPixelMask, x, y); }, mask + ", depth 0.25 less than the stored depth"); + depthProbe.depthCompare = VK_COMPARE_OP_GREATER_OR_EQUAL; + Draw(*device, depthProbe); + Check([&](std::uint32_t x, std::uint32_t y) { return !Covered(quadPixelMask, x, y); }, mask + ", depth 0.25 at least the stored depth"); + } + Pass storing; + storing.quadSampleMask = {1, 0, 0, 0}; + storing.pixelCode = StoringPixelCode; + storing.depthTest = true; + storing.depthCompare = VK_COMPARE_OP_ALWAYS; + storing.stencil = always; + try { + Draw(*device, storing); + } catch (const std::exception& error) { + Require(std::string(error.what()).find("differs between the pixels of a quad") != std::string::npos, std::string("a storing early-Z pixel program under a quad mask failed for another reason: ") + error.what()); + storing.shaderControl = 0u; + Draw(*device, storing); + Check([](std::uint32_t x, std::uint32_t y) { return Covered(0x1, x, y); }, "the storing late-Z pixel program under quad pixel mask 1"); + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + const auto expected = Covered(0x1, pixel % Width, pixel / Width) ? 1u : 0u; + Require(Stores[pixel] == expected, "the storing pixel program under quad pixel mask 1 stored " + std::to_string(Stores[pixel]) + " at pixel " + std::to_string(pixel) + ", expected " + std::to_string(expected)); + } + std::puts("quad sample mask depth and stencil tests passed"); + return 0; + } + Require(false, "a quad-masked early-Z pixel program that stores to memory was drawn with depth tests"); + return 1; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskDraw.cpp new file mode 100644 index 0000000000..867590f0cf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskDraw.cpp @@ -0,0 +1,213 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.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::Require; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t SetContextReg = 0x69u; +constexpr std::uint32_t SetShReg = 0x76u; +constexpr std::uint32_t SetUconfigReg = 0x79u; +constexpr std::uint32_t DrawIndexAuto = 0x2du; +constexpr std::size_t Block = 65536; +constexpr std::size_t ColorBytes = Width * Height * 4; +std::byte* ColorMemory = nullptr; +alignas(64) volatile std::uint32_t EndOfPipe = 0; + +std::byte* AllocateWatched(std::size_t bytes) { +#ifdef _WIN32 + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (raw == MAP_FAILED) throw std::runtime_error("cannot map the watched block"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast(aligned + bytes), begin + Block - aligned); + void* block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); +#endif + return static_cast(block); +} + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80020005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0002f2, 0x7e0202f2, 0x7e0402f2, 0x7e0602f2, 0xf800080f, 0x03020100, 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}, +}}; + +struct Step { + const char* name; + std::uint32_t top; + std::uint32_t bottom; + std::uint32_t shaderControl; + std::uint8_t quadPixels; +}; + +const std::array Steps{{ + {"PA_SC_AA_MASK 0x0000ffff/0 with early Z (GTA V, X0Y0)", 0x0000ffffu, 0u, 0x10u, 0x1u}, + {"PA_SC_AA_MASK 0x0000ffff/0", 0x0000ffffu, 0u, 0x800u, 0x1u}, + {"PA_SC_AA_MASK 0xffff0000/0 (X1Y0)", 0xffff0000u, 0u, 0x800u, 0x2u}, + {"PA_SC_AA_MASK 0/0xffff0000 (X1Y1)", 0u, 0xffff0000u, 0x800u, 0x8u}, + {"PA_SC_AA_MASK 0xfffe0001/0x0001fffe (X0Y0 and X1Y1 by bit 0)", 0xfffe0001u, 0x0001fffeu, 0x800u, 0x9u}, + {"PA_SC_AA_MASK 0x00010001/0x00010001 (full at one sample)", 0x00010001u, 0x00010001u, 0x800u, 0xfu}, + {"PA_SC_AA_MASK 0/0 (empty)", 0u, 0u, 0x800u, 0x0u}, + {"full PA_SC_AA_MASK", 0xffffffffu, 0xffffffffu, 0x800u, 0xfu}, +}}; + +struct RegisteredShader { + Shader shader; + ShaderUserData userData; +}; +RegisteredShader VertexShader{}; +RegisteredShader PixelShader{}; + +const std::vector> ContextBaseline{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, + {0x81, 0x80000000}, {0x90, 0x80000000}, {0x94, 0x80000000}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, {0x114, 0}, {0x113, std::bit_cast(1.0f)}, +}; + +void Register(RegisteredShader& registered, const void* code, std::uint32_t bytes, std::uint8_t type) { + registered.shader.file_header = 0x34333231u; + registered.shader.version = 0x18u; + registered.shader.user_data = ®istered.userData; + registered.shader.code = code; + registered.shader.header_size = sizeof(RegisteredShader); + registered.shader.shader_size = bytes; + registered.shader.type = type; + AgcDriverRegisterShader_nid_postfix(®istered.shader); +} + +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 SetRegister(std::vector& words, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) { + words.push_back(0xc0010000u | (opcode << 8u)); + words.push_back(offset); + words.push_back(value); +} + +std::vector Frame(const Step& step) { + const auto colorAddress = reinterpret_cast(ColorMemory); + const auto vertexAddress = reinterpret_cast(VertexCode.data()); + const auto pixelAddress = reinterpret_cast(PixelCode.data()); + std::vector words; + for (const auto& [offset, value] : ContextBaseline) SetRegister(words, SetContextReg, offset, value); + SetRegister(words, SetContextReg, 0x203, step.shaderControl); + SetRegister(words, SetContextReg, 0x30e, step.top); + SetRegister(words, SetContextReg, 0x30f, step.bottom); + SetRegister(words, SetContextReg, 0x318, static_cast(colorAddress >> 8u)); + SetRegister(words, SetContextReg, 0x390, static_cast(colorAddress >> 40u)); + SetRegister(words, SetContextReg, 0x3b0, ((Width - 1u) << 14u) | (Height - 1u)); + SetRegister(words, SetContextReg, 0x10f, std::bit_cast(Width / 2.0f)); + SetRegister(words, SetContextReg, 0x110, std::bit_cast(Width / 2.0f)); + SetRegister(words, SetContextReg, 0x111, std::bit_cast(-(Height / 2.0f))); + SetRegister(words, SetContextReg, 0x112, std::bit_cast(Height / 2.0f)); + const auto extent = (Height << 16u) | Width; + SetRegister(words, SetContextReg, 0xd, extent); + SetRegister(words, SetContextReg, 0x82, extent); + SetRegister(words, SetContextReg, 0x91, extent); + SetRegister(words, SetContextReg, 0x95, extent); + SetRegister(words, SetUconfigReg, 0x242, 4u); + SetRegister(words, SetShReg, 0x008, static_cast(pixelAddress >> 8u)); + SetRegister(words, SetShReg, 0x009, static_cast(pixelAddress >> 40u)); + SetRegister(words, SetShReg, 0x00b, 0u); + SetRegister(words, SetShReg, 0x0c8, static_cast(vertexAddress >> 8u)); + SetRegister(words, SetShReg, 0x0c9, static_cast(vertexAddress >> 40u)); + SetRegister(words, SetShReg, 0x08b, 4u << 1u); + const auto descriptor = BufferDescriptor(Vertices.data(), 16u, static_cast(Vertices.size())); + for (std::uint32_t i = 0; i < descriptor.size(); ++i) SetRegister(words, SetShReg, 0x08c + i, descriptor[i]); + words.insert(words.end(), {0xc0000000u | (1u << 16u) | (DrawIndexAuto << 8u), static_cast(Vertices.size()), 2u}); + const auto labelAddress = reinterpret_cast(&EndOfPipe); + words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), static_cast(labelAddress), static_cast(static_cast(labelAddress) >> 32u), 1u, 0u, 0u}); + return words; +} + +void Check(const Step& step, const std::vector& colors) { + const auto what = std::string("sample mask through the driver, ") + step.name; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto value = std::to_integer(colors[(y * Width + x) * 4u]); + const bool expected = ((step.quadPixels >> ((x & 1u) | ((y & 1u) << 1u))) & 1u) != 0u; + Require(value == (expected ? 255u : 0u), what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") stored " + std::to_string(value) + ", expected " + (expected ? "written" : "clear")); + } + } +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + ColorMemory = AllocateWatched(Block); + Register(VertexShader, VertexCode.data(), sizeof(VertexCode), 2); + Register(PixelShader, PixelCode.data(), sizeof(PixelCode), 1); + for (int round = 0; round < 2; ++round) { + for (const auto& step : Steps) { + std::fill_n(ColorMemory, ColorBytes, std::byte{0}); + EndOfPipe = 0; + auto words = Frame(step); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, std::string("DCB submission failed for ") + step.name); + AgcDriverWaitIdle_nid_postfix(); + Require(EndOfPipe == 1u, std::string("the end-of-pipe label was not written for ") + step.name); + std::vector colors(ColorBytes); + AgcDriver::GuestMemory::Read(static_cast(reinterpret_cast(ColorMemory)), colors); + Check(step, colors); + } + } + AgcDriverShutdown_nid_postfix(); + std::puts("sample mask draws through the driver passed"); + 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/SampleMaskMultisample.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskMultisample.cpp new file mode 100644 index 0000000000..7ec9bf0884 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskMultisample.cpp @@ -0,0 +1,209 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::PixelInput; +using ShaderRecompiler::PixelInputBit; +using ShaderRecompiler::ShaderStage; +using Masks = std::array; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t Samples = 4; +constexpr std::uint32_t WaveSize = 64; +constexpr std::uint32_t EarlyZThenLateZ = 0x10u; +constexpr Masks Full{0xf, 0xf, 0xf, 0xf}; +alignas(256) std::array Invocations{}; +alignas(256) std::array DepthMemory{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array SampleStoreCode{ + 0x7e100f04, 0x7e120f05, 0x34121286, 0x4a101308, 0x34101082, 0xd548000a, 0x02111106, 0x4a101508, + 0x34101082, 0x7e160281, 0xe0705000, 0x80000b08, 0xbf8c3f70, 0xf8001890, 0x00000000, 0xbf810000, + 0xbf810000, +}; + +alignas(256) constexpr std::array NullExportCode{0xf8001890, 0x00000000, 0xbf810000}; + +using Triangle = std::array, 3>; +alignas(256) constexpr Triangle Far{{{-1.0f, -1.0f, 0.25f, 1.0f}, {3.0f, -1.0f, 0.25f, 1.0f}, {-1.0f, 3.0f, 0.25f, 1.0f}}}; +alignas(256) constexpr Triangle Probe{{{-1.0f, -1.0f, 0.375f, 1.0f}, {3.0f, -1.0f, 0.375f, 1.0f}, {-1.0f, 3.0f, 0.375f, 1.0f}}}; +alignas(256) constexpr Triangle Middle{{{-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, stride == 0u ? 0x31016facu : 0x01016facu}; +} + +struct Pass { + Masks quadSampleMask = Full; + bool perSample = true; + std::uint32_t shaderControl = 0u; + const Triangle* triangle = &Middle; + bool depth = false; + bool depthWrite = false; + VkCompareOp depthCompare = VK_COMPARE_OP_ALWAYS; +}; + +void Draw(AgcDriver::VulkanDevice& device, const Pass& pass) { + Invocations.fill(0); + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(pass.triangle->data(), 16u, static_cast(pass.triangle->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()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; + state.hasColorTarget = false; + 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.blendConstants = {}; + state.samples = Samples; + state.quadSampleMask = pass.quadSampleMask; + state.pixelEarlyZ = AgcDriver::Graphics::PixelEarlyZ(pass.shaderControl); + if (pass.depth) { + state.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(DepthMemory.data()), 0u, {Width, Height}, VK_FORMAT_D32_SFLOAT, 0.0f, 0}; + state.depth->samples = Samples; + state.depthTest = true; + state.depthWrite = pass.depthWrite; + state.depthCompare = pass.depthCompare; + } + + const std::uint32_t inputs = pass.perSample ? PixelInputBit(PixelInput::PerspectiveSample) | PixelInputBit(PixelInput::LinearSample) | PixelInputBit(PixelInput::PositionX) | PixelInputBit(PixelInput::PositionY) | PixelInputBit(PixelInput::Ancillary) : PixelInputBit(PixelInput::PerspectiveCenter); + const AgcDriver::Registers context{{0x1b3, inputs}, {0x1b4, inputs}, {0x1b6, 0u}, {0x1c5, 0u}, {0x203, pass.shaderControl}}; + auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + Require(pixel.sampleShading == pass.perSample, "the pixel inputs did not select the requested shading rate"); + pixel.quadPixelMask = AgcDriver::Graphics::QuadPixelMask(state); + pixel.quadSampleMask = AgcDriver::Graphics::QuadSampleMasks(state); + std::vector pixelUserData(4, 0u); + const auto invocations = BufferDescriptor(Invocations.data(), 0u, static_cast(sizeof(Invocations))); + std::copy(invocations.begin(), invocations.end(), pixelUserData.begin()); + const std::span code = pass.perSample ? std::span(SampleStoreCode) : std::span(NullExportCode); + const std::array pixelMemory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(code.data()), code, 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)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(pass.triangle->size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(const Masks& quadSampleMask, const std::string& what) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto mask = quadSampleMask[(x & 1u) | ((y & 1u) << 1u)]; + for (std::uint32_t sample = 0; sample < Samples; ++sample) { + const auto expected = (mask >> sample) & 1u; + const auto ran = Invocations[(y * Width + x) * Samples + sample]; + Require(ran == expected, what + ": sample " + std::to_string(sample) + " of pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") " + (ran != 0u ? "stored" : "did not store") + " (" + std::to_string(ran) + "), the quad pixel's mask is " + std::to_string(mask)); + } + } + } +} + +void DepthWritten(AgcDriver::VulkanDevice& device, const Masks& mask, const std::string& what) { + Pass reset; + reset.perSample = false; + reset.triangle = &Far; + reset.depth = true; + reset.depthWrite = true; + Draw(device, reset); + Pass masked = reset; + masked.quadSampleMask = mask; + masked.triangle = &Middle; + Draw(device, masked); + Pass probe; + probe.shaderControl = EarlyZThenLateZ; + probe.triangle = &Probe; + probe.depth = true; + probe.depthCompare = VK_COMPARE_OP_LESS; + Draw(device, probe); + Check(mask, "the samples a pixel-rate draw with " + what + " wrote depth 0.5 into, probed by early-Z per-sample stores"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + struct Case { + const char* name; + Masks mask; + }; + const std::array cases{{ + {"no sample mask", Full}, + {"samples 0 and 2 of every pixel", {0x5, 0x5, 0x5, 0x5}}, + {"sample 3 of every pixel", {0x8, 0x8, 0x8, 0x8}}, + {"no sample of any pixel", {0x0, 0x0, 0x0, 0x0}}, + {"every sample of the even-x, even-y pixel of each quad", {0xf, 0x0, 0x0, 0x0}}, + {"every sample of the odd-x pixels of each quad", {0x0, 0xf, 0x0, 0xf}}, + {"sample q of quad pixel q", {0x1, 0x2, 0x4, 0x8}}, + {"all, none, none and samples 0 and 1", {0xf, 0x0, 0x0, 0x3}}, + {"samples 1 and 3, 0 and 2, 2 and 3, and 0", {0xa, 0x5, 0xc, 0x1}}, + }}; + for (const auto& test : cases) { + Pass pass; + pass.quadSampleMask = test.mask; + Draw(*device, pass); + Check(test.mask, std::string("4x per-sample shading with ") + test.name); + } + for (const auto& test : cases) DepthWritten(*device, test.mask, test.name); + Pass early; + early.quadSampleMask = {0x6, 0x6, 0x6, 0x6}; + early.shaderControl = EarlyZThenLateZ; + try { + Draw(*device, early); + } catch (const std::exception& error) { + Require(std::string(error.what()).find("earlyFragmentSampleMaskTestBeforeSampleCounting") != std::string::npos, std::string("a sample mask with early fragment tests failed for another reason: ") + error.what()); + std::puts("4x sample mask tests passed (early fragment tests refused: the device does not order the sample mask test before sample counting)"); + return 0; + } + Check(early.quadSampleMask, "4x per-sample shading with early fragment tests and samples 1 and 2 of every pixel"); + std::puts("4x sample mask 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/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp new file mode 100644 index 0000000000..f3d7e2bf44 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -0,0 +1,350 @@ +#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/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#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 Side = 16; +constexpr std::uint32_t Samples = 4; +constexpr std::uint32_t SampleShift = 2; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Float16x4Info = 0x0730u; +constexpr std::uint32_t MultisampledAttrib = (SampleShift << 12u) | (SampleShift << 15u); +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Format16161616Float = 71; +constexpr std::uint32_t Type2DMsaa = 14; +constexpr std::uint32_t TileRenderTarget = 0x1b; +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t LoadResult = 12; +constexpr std::array Drawn{0.25f, 0.5f, 0.125f, 1.0f}; +constexpr double EdgeMargin = 1.0 / 32.0; +constexpr std::uint32_t FillOnes = 0x3c003c00u; +constexpr std::array Ones{1.0f, 1.0f, 1.0f, 1.0f}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0802ff, 0x3e800000, 0x7e0a02ff, 0x3f000000, 0x7e0c02ff, 0x3e000000, 0x7e0e02ff, 0x3f800000, 0xf800180f, 0x07060504, 0xbf810000, +}; + +alignas(256) constexpr std::array LoadCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xbf8c3f70, 0xf0001f30, 0x00011005, + 0xbf8c3f70, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, 0x80001201, 0xe070103c, + 0x80001301, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {0.83f, -1.0f, 0.5f, 1.0f}, {-1.0f, 0.71f, 0.5f, 1.0f} +}}; + +alignas(256) std::array Buffer{}; + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes) { +#ifdef _WIN32 + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); +#endif + Require(data != nullptr, "cannot allocate the multisampled color target"); + std::memset(data, 0, bytes); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + VirtualFree(data, 0, MEM_RELEASE); +#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; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Float16x4Info; + cx[0x31d] = MultisampledAttrib; + cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u); + cx[0x3b8] = TiledAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R16G16B16A16_SFLOAT && color.samples == Samples && color.extent.width == Side && color.extent.height == Side, "the target did not decode as a 16x16 RGBA16F surface of 4 samples"); + return color; +} + +AgcDriver::Graphics::State TargetState(const AgcDriver::Graphics::ColorTarget& color) { + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Side, Side}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Side), static_cast(Side), -static_cast(Side), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Side, Side}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + state.samples = Samples; + return state; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& state) { + 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()); + const auto codeAddress = reinterpret_cast(PixelCode.data()); + const std::span pixelCode(PixelCode); + 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, {}), 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 invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, fragment); + const auto pixelResult = invocation.Materialize(*invocation.Capture(runtime)); + 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::array TargetDescriptor(const Block& block, std::uint32_t sampleShift) { + const auto address = block.Address(); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format16161616Float << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (sampleShift << 16u) | (TileRenderTarget << 20u) | (Type2DMsaa << 28u), + 0u, + sampleShift << 4u, + 0u, + 0u, + }; +} + +ShaderRecompiler::RecompileResult CompileLoad(AgcDriver::VulkanDevice& device, const std::array& texture, std::vector& userData) { + const auto address = reinterpret_cast(Buffer.data()); + const std::array buffer{static_cast(address), static_cast((address >> 32u) & 0xffffu), static_cast(Buffer.size() * 4u), 0x31016facu}; + userData.assign(16, 0u); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::span code(LoadCode); + 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; + return ShaderRecompiler::Recompile(request); +} + +std::vector> LoadSamples(AgcDriver::VulkanDevice& device, const Block& block) { + std::vector userData; + const auto program = CompileLoad(device, TargetDescriptor(block, SampleShift), userData); + std::vector> loaded(Side * Side * Samples); + for (std::uint32_t first = 0; first < loaded.size(); first += Threads) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto index = first + tid; + const auto pixel = index / Samples; + const std::array words{0u, 0u, 0u, pixel % Side, pixel / Side, index % Samples}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(LoadCode.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t component = 0; component < 4u; ++component) loaded[first + tid][component] = std::bit_cast(Buffer[tid * Words + LoadResult + component]); + } + } + return loaded; +} + +double EdgeDistance(double x, double y, std::size_t from) { + const auto pixels = [](const std::array& vertex) { + return std::array{(static_cast(vertex[0]) + 1.0) * 0.5 * Side, Side - (static_cast(vertex[1]) + 1.0) * 0.5 * Side}; + }; + const auto a = pixels(Triangle[from]); + const auto b = pixels(Triangle[(from + 1u) % Triangle.size()]); + const auto c = pixels(Triangle[(from + 2u) % Triangle.size()]); + const auto length = std::hypot(b[0] - a[0], b[1] - a[1]); + const auto side = [&](double px, double py) { return ((b[0] - a[0]) * (py - a[1]) - (b[1] - a[1]) * (px - a[0])) / length; }; + return side(c[0], c[1]) < 0 ? -side(x, y) : side(x, y); +} + +void CheckSamples(const AgcDriver::Graphics::State& state, const std::vector>& loaded, const std::array& background) { + const auto locations = AgcDriver::Graphics::SampleLocations(state); + std::array pixelsByCoverage{}; + std::array coveredBySample{}; + std::uint32_t checked = 0; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + std::uint32_t covered = 0; + for (std::uint32_t sample = 0; sample < Samples; ++sample) { + const auto& texel = loaded[(y * Side + x) * Samples + sample]; + const bool drawn = texel == Drawn; + const bool empty = texel == background; + char where[160]; + std::snprintf(where, sizeof(where), "sample %u of pixel (%u, %u) loaded (%g, %g, %g, %g)", sample, x, y, texel[0], texel[1], texel[2], texel[3]); + Require(drawn || empty, std::string(where) + ", neither the drawn color nor the cleared target"); + covered += drawn ? 1u : 0u; + coveredBySample[sample] += drawn ? 1u : 0u; + const auto px = x + static_cast(locations[sample].x); + const auto py = y + static_cast(locations[sample].y); + const auto distance = std::min({EdgeDistance(px, py, 0), EdgeDistance(px, py, 1), EdgeDistance(px, py, 2)}); + if (std::fabs(distance) < EdgeMargin) continue; + ++checked; + Require(drawn == (distance > 0), std::string(where) + (distance > 0 ? ", which the triangle covers" : ", which the triangle does not cover")); + } + ++pixelsByCoverage[covered]; + } + } + Require(checked >= Side * Side * Samples * 9u / 10u, "fewer than nine in ten samples are clear of the triangle's edges"); + Require(pixelsByCoverage[0] != 0 && pixelsByCoverage[Samples] != 0, "the triangle covers every pixel or none"); + Require(pixelsByCoverage[1] + pixelsByCoverage[2] + pixelsByCoverage[3] != 0, "no pixel is partly covered, so the samples of a pixel were not told apart"); + Require(std::adjacent_find(coveredBySample.begin(), coveredBySample.end(), std::not_equal_to<>()) != coveredBySample.end(), "every sample index has the same coverage, so the sample index was not told apart"); +} + +void CheckUniform(const std::vector>& loaded, const std::array& expected, const std::string& what) { + for (std::size_t index = 0; index < loaded.size(); ++index) { + const auto& texel = loaded[index]; + char where[160]; + std::snprintf(where, sizeof(where), "sample %zu of pixel (%zu, %zu) loaded (%g, %g, %g, %g)", index % Samples, index / Samples % Side, index / Samples / Side, texel[0], texel[1], texel[2], texel[3]); + Require(texel == expected, std::string(where) + " " + what); + } +} + +void StoreWords(const Block& block, std::uint64_t bytes, std::uint32_t word) { + for (std::uint64_t offset = 0; offset < bytes; offset += sizeof(word)) std::memcpy(block.data + offset, &word, sizeof(word)); +} + +void RequireRefused(AgcDriver::VulkanDevice& device, const std::array& texture, const std::string& expected, const std::string& what) { + std::string refusal; + try { + std::vector userData; + const auto program = CompileLoad(device, texture, userData); + Buffer.fill(0u); + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(LoadCode.data())); + device.WaitIdle(); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find(expected) != std::string::npos, what + " was not refused: " + refusal); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Block block(4u * BlockBytes); + Block other(BlockBytes); + const auto color = DecodeTarget(block); + const auto state = TargetState(color); + Draw(*device, state); + CheckSamples(state, LoadSamples(*device, block), {}); + const auto colorBytes = AgcDriver::Graphics::MultisampledColorBytes(color); + Require(colorBytes != 0 && colorBytes <= block.bytes, "the 4x target's color memory does not fit its block"); + AgcDriver::Graphics::NoteColorMetadataFill(block.Address(), static_cast(colorBytes), FillOnes); + CheckUniform(LoadSamples(*device, block), Ones, "after a fill of the target's color memory with half-float ones"); + RequireRefused(*device, TargetDescriptor(block, 3u), "texture of 8 samples", "loading a 4x target as an 8x texture"); + Require(colorBytes <= other.bytes, "the 4x target's color memory does not fit one block"); + CheckUniform(LoadSamples(*device, other), {}, "from zeroed color memory no draw rendered to"); + Block stored(BlockBytes); + StoreWords(stored, colorBytes, FillOnes); + CheckUniform(LoadSamples(*device, stored), Ones, "from color memory of half-float ones no draw rendered to"); + const auto storedState = TargetState(DecodeTarget(stored)); + Draw(*device, storedState); + CheckSamples(storedState, LoadSamples(*device, stored), Ones); + Block mixed(BlockBytes); + StoreWords(mixed, colorBytes, FillOnes); + const std::uint32_t odd = 0x3c000000u; + std::memcpy(mixed.data + colorBytes - sizeof(odd), &odd, sizeof(odd)); + RequireRefused(*device, TargetDescriptor(mixed, SampleShift), "does not hold one repeated 32-bit value", "loading a multisampled texture no draw rendered to whose color memory is not uniform"); + std::puts("every sample of a 4x color target loaded through its multisampled texture descriptor matches the triangle drawn into it, then the fill of its color memory; a surface no draw rendered to loads the value its color memory repeats and keeps it under a later draw"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp new file mode 100644 index 0000000000..d6a3055e6d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp @@ -0,0 +1,377 @@ +#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 +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +PFN_vkCmdBindPipeline NativeBind = nullptr; +std::atomic GraphicsBinds{0u}; + +VKAPI_ATTR void VKAPI_CALL CountBind(VkCommandBuffer commands, VkPipelineBindPoint point, VkPipeline pipeline) { + if (point == VK_PIPELINE_BIND_POINT_GRAPHICS) ++GraphicsBinds; + NativeBind(commands, point, pipeline); +} + +struct BindTrace { + AgcDriver::Graphics::DeviceFunctions* functions; + explicit BindTrace(const AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + functions = const_cast(context.functions); + Require(functions != nullptr && functions->cmdBindPipeline != nullptr, "native pipeline bind table is unavailable"); + NativeBind = functions->cmdBindPipeline; + functions->cmdBindPipeline = CountBind; + } + ~BindTrace() { functions->cmdBindPipeline = NativeBind; } +}; + +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, 6> Vertices{{ + {-1.0f, -1.0f, 0.25f, 1.0f}, {3.0f, -1.0f, 0.25f, 1.0f}, {-1.0f, 3.0f, 0.25f, 1.0f}, + {-1.0f, -1.0f, 0.75f, 1.0f}, {3.0f, -1.0f, 0.75f, 1.0f}, {-1.0f, 3.0f, 0.75f, 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, 0x700u}, {0x1b4, 0x700u}, {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, std::uint32_t vertices = 3u, std::uint32_t instances = 1u, bool depthFail = false, const std::function& alter = {}, const std::function& alterDraw = {}, std::span> vertexData = Vertices, std::span vertexCode = VertexCode, std::span pixelData = {}, std::span pixelSrt = {}, const std::function& alterVertex = {}) { + Pixels.fill(Kept); + auto target = device.Target(); + std::vector capabilities(target.supportedCapabilities.begin(), target.supportedCapabilities.end()); + std::erase(capabilities, static_cast(spv::CapabilityStencilExportEXT)); + target.supportedCapabilities = capabilities; + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(vertexData.data(), 16u, static_cast(vertexData.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(vertexCode.data()), std::as_bytes(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; + auto vertexResult = ShaderRecompiler::Recompile(vertex); + if (alterVertex) alterVertex(vertexResult); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + auto pixelUserData = pixelData.empty() ? std::vector(8, 0u) : std::vector(pixelData.begin(), pixelData.end()); + std::vector pixelMemory{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}; + pixelMemory.insert(pixelMemory.end(), pixelSrt.begin(), pixelSrt.end()); + 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); + if (!pixelResult.stencilReferences.empty()) { + Require(pixelResult.stencilReferenceDword * sizeof(std::uint32_t) + sizeof(std::uint32_t) == pixelResult.pushConstants.size(), "dynamic reference is not reserved after the stage payload"); + for (std::size_t cursor = 5; cursor < pixelResult.spirv.size();) { + const auto count = pixelResult.spirv[cursor] >> 16u; + Require(count != 0u && count <= pixelResult.spirv.size() - cursor, "invalid materialized replay module"); + Require((pixelResult.spirv[cursor] & 0xffffu) != spv::OpSpecConstant, "expected reference remains a pipeline specialization"); + cursor += count; + } + } + 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.depthTest = depthFail; + state.depthCompare = depthFail ? VK_COMPARE_OP_NEVER : VK_COMPARE_OP_ALWAYS; + 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 = {}; + if (alter) alter(state); + AgcDriver::Pm4::DrawParameters draw{0, vertices, 0, instances, 0, false}; + if (alterDraw) alterDraw(draw); + const auto bindsBefore = GraphicsBinds.load(); + device.Draw(state, draw, shaders); + device.WaitIdle(); + if (!pixelResult.stencilReferences.empty()) Require(GraphicsBinds.load() - bindsBefore == 1u, "stencil replay rebound its shader pipeline within an original draw"); +} + +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; + const BindTrace binds(*device); + constexpr std::uint32_t exported = 0x52u; + constexpr std::uint32_t plain = 0x50u; + constexpr std::array alternating{ + 0x7e040f00u, 0x36060481u, + 0x7c000000u | (0xc2u << 17u) | (3u << 9u) | 0x80u, + 0x7e0802ffu, 0xffu, + (1u << 25u) | (5u << 17u) | (4u << 9u) | 0x80u, + 0x7e0e02f2u, 0xf8000082u, 5u << 8u, + 0xf800180fu, 0x07070707u, 0xbf810000u + }; + constexpr std::array ordered{ + 0x7c000000u | (4u << 17u) | (2u << 9u) | 0xf0u, + 0x7e0802ffu, 0xffu, + (1u << 25u) | (5u << 17u) | (4u << 9u) | 0x80u, + 0x7e0e02f2u, 0xf8000082u, 5u << 8u, + 0xf800180fu, 0x07070707u, 0xbf810000u + }; + const auto replace = [](std::uint32_t reference = 0u, std::uint32_t mask = 0xffu) { return VkStencilOpState{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, mask, reference}; }; + const auto equal = [](std::uint32_t reference, std::uint32_t mask = 0xffu) { return VkStencilOpState{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, mask, 0u, reference}; }; + const auto all = [](std::uint32_t, std::uint32_t) { return true; }; + const auto even = [](std::uint32_t x, std::uint32_t) { return x % 2u == 0u; }; + const auto odd = [](std::uint32_t x, std::uint32_t) { return x % 2u != 0u; }; + bool rejectedContract = false; + try { + Draw(*device, alternating, exported, replace(), 3u, 1u, false, {}, {}, Vertices, VertexCode, {}, {}, [&](ShaderRecompiler::RecompileResult& result) { result.hostSubgroupSize = device->Target().subgroupSize == 64u ? 32u : 64u; }); + } catch (const std::exception& error) { + rejectedContract = std::string_view(error.what()).find("subgroup contract differs") != std::string_view::npos; + if (!rejectedContract) throw; + } + Require(rejectedContract, "a stale vertex subgroup-size contract was accepted"); + rejectedContract = false; + try { + Draw(*device, alternating, exported, replace(), 3u, 1u, false, {}, {}, Vertices, VertexCode, {}, {}, [](ShaderRecompiler::RecompileResult& result) { auto words = result.spirv.Copy(); words[1] = 0x00010600u; result.spirv = std::move(words); }); + } catch (const std::exception& error) { + rejectedContract = std::string_view(error.what()).find("subgroup contract differs") != std::string_view::npos; + if (!rejectedContract) throw; + } + Require(rejectedContract, "a varying-size module contract was accepted"); + Draw(*device, alternating, exported, replace()); + Check(all, "finite stencil export"); + Draw(*device, PlainPixelCode, plain, equal(255u)); + Check(even, "even columns exported 255"); + Draw(*device, PlainPixelCode, plain, equal(0u)); + Check(odd, "odd columns exported zero"); + Draw(*device, alternating, exported, equal(91u)); + Check(all, "exported references replace compare reference"); + Draw(*device, PlainPixelCode, plain, replace(0u)); + Draw(*device, ordered, exported, replace(), 6u); + Draw(*device, PlainPixelCode, plain, equal(0u)); + Check(all, "overlapping primitives retain original order"); + Draw(*device, PlainPixelCode, plain, replace(0u)); + const VkStencilOpState instanceOrder{VK_STENCIL_OP_INCREMENT_AND_WRAP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_GREATER, 0xffu, 0xffu, 0u}; + Draw(*device, ordered, exported, instanceOrder, 6u, 2u); + Draw(*device, PlainPixelCode, plain, equal(0u)); + Check(all, "primitive order within every instance"); + alignas(256) constexpr std::array indices{0u, 1u, 2u, 3u, 4u, 5u}; + alignas(256) std::array, 7> indexedVertices{}; + std::copy(Vertices.begin(), Vertices.end(), indexedVertices.begin() + 1); + Draw(*device, PlainPixelCode, plain, replace(0u)); + Draw(*device, ordered, exported, instanceOrder, 6u, 2u, false, {}, [&](AgcDriver::Pm4::DrawParameters& draw) { + draw.indexed = true; + draw.indexAddress = reinterpret_cast(indices.data()); + draw.indexSize = 4u; + draw.firstVertex = 1u; + draw.firstInstance = 7u; + }, indexedVertices); + Draw(*device, PlainPixelCode, plain, equal(0u)); + Check(all, "indexed primitives preserve base vertex and nonzero first instance"); + alignas(256) std::array, 72> instanceVertices{}; + std::copy(Vertices.begin(), Vertices.end(), instanceVertices.begin() + 56); + std::copy(Vertices.begin() + 3, Vertices.end(), instanceVertices.begin() + 64); + std::copy(Vertices.begin(), Vertices.begin() + 3, instanceVertices.begin() + 67); + constexpr std::array instanceVertexCode{0x34101083u, 0x4a0a0b08u, 0xe0382000u, 0x80000005u, 0xbf8c3f70u, 0xf80008cfu, 0x03020100u, 0xbf810000u}; + Draw(*device, PlainPixelCode, plain, replace(0u)); + Draw(*device, ordered, exported, instanceOrder, 6u, 2u, false, {}, [](AgcDriver::Pm4::DrawParameters& draw) { draw.firstInstance = 7u; }, instanceVertices, instanceVertexCode); + Check(all, "nonzero instance indices select live vertices"); + Draw(*device, PlainPixelCode, plain, equal(255u)); + Check(all, "each replayed instance preserves its observable instance index"); + Draw(*device, PlainPixelCode, plain, replace(0x55u)); + Draw(*device, alternating, exported, replace(0u, 0xaau)); + Draw(*device, PlainPixelCode, plain, equal(0xffu)); + Check(even, "stencil write mask preserves even columns"); + Draw(*device, PlainPixelCode, plain, equal(0x55u)); + Check(odd, "stencil write mask preserves odd columns"); + auto highBits = alternating; + highBits[4] = 0x1234ffu; + Draw(*device, highBits, exported, replace()); + Draw(*device, PlainPixelCode, plain, equal(255u)); + Check(even, "stencil reference truncates to eight attachment bits"); + Draw(*device, PlainPixelCode, plain, replace(0u)); + Draw(*device, PlainPixelCode, plain, replace(0u), 3u, 1u, false, [](AgcDriver::Graphics::State& state) { state.cullMode = VK_CULL_MODE_BACK_BIT; }); + const auto facing = Pixels[0] == std::byte{255} ? VK_FRONT_FACE_COUNTER_CLOCKWISE : VK_FRONT_FACE_CLOCKWISE; + for (const bool back : {false, true}) { + Draw(*device, PlainPixelCode, plain, replace(0u)); + Draw(*device, alternating, exported, replace(), 3u, 1u, false, [&](AgcDriver::Graphics::State& state) { + state.frontFace = back ? (facing == VK_FRONT_FACE_COUNTER_CLOCKWISE ? VK_FRONT_FACE_CLOCKWISE : VK_FRONT_FACE_COUNTER_CLOCKWISE) : facing; + state.stencilFront.writeMask = 0xaau; + state.stencilBack.writeMask = 0x55u; + state.stencilBack.passOp = VK_STENCIL_OP_INVERT; + }); + Draw(*device, PlainPixelCode, plain, equal(back ? 0x55u : 0xaau)); + Check(back ? all : even, "distinct front and back stencil operations and write masks"); + } + Draw(*device, PlainPixelCode, plain, replace(0x0fu)); + Draw(*device, alternating, exported, equal(1u, 0x0fu)); + Check(even, "stencil compare masks exported reference"); + const VkStencilOpState depthFailure{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 0u}; + Draw(*device, alternating, exported, depthFailure, 3u, 1u, true); + Check([](std::uint32_t, std::uint32_t) { return false; }, "depth failure rejects colors"); + Draw(*device, PlainPixelCode, plain, equal(255u)); + Check(even, "depth-fail replace uses exported reference"); + auto depthStencil = std::vector(alternating.begin(), alternating.end()); + const auto mrtz = std::find(depthStencil.begin(), depthStencil.end(), 0xf8000082u); + const auto mrtzOffset = static_cast(mrtz - depthStencil.begin()); + depthStencil.insert(mrtz, {0x7e0c02ffu, 0x3f400000u}); + depthStencil[mrtzOffset + 2u] = 0xf8000083u; + depthStencil[mrtzOffset + 3u] = (5u << 8u) | 6u; + Draw(*device, depthStencil, 0x53u, replace(), 3u, 1u, false, [](AgcDriver::Graphics::State& state) { + state.depthTest = true; + state.depthWrite = true; + state.depthCompare = VK_COMPARE_OP_ALWAYS; + }); + auto midDepth = Vertices; + for (auto& vertex : midDepth) vertex[2] = 0.5f; + Draw(*device, PlainPixelCode, plain, replace(), 3u, 1u, false, [](AgcDriver::Graphics::State& state) { + state.depthTest = true; + state.depthCompare = VK_COMPARE_OP_LESS; + }, {}, midDepth); + Check(all, "simultaneous depth and stencil exports preserve shader depth replacement"); + for (const bool restart : {false, true}) { + bool refused = false; + try { + Draw(*device, alternating, exported, replace(), 3u, 1u, false, [&](AgcDriver::Graphics::State& state) { if (restart) state.primitiveRestart = true; else state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; }); + } catch (const std::exception& error) { + refused = std::string(error.what()).find("direct triangle lists") != std::string::npos; + } + Require(refused, "unsafe primitive routing was not refused"); + } + bool unknownRefused = false; + try { Draw(*device, ColumnPixelCode, exported, replace()); } catch (const std::exception& error) { unknownRefused = std::string(error.what()).find("proven set") != std::string::npos; } + Require(unknownRefused, "unbounded stencil reference was not refused"); + bool earlyRefused = false; + try { Draw(*device, alternating, 0x1012u, replace()); } catch (const std::exception& error) { earlyRefused = std::string(error.what()).find("late tests") != std::string::npos; } + Require(earlyRefused, "early stencil testing was not refused"); + bool orderedRefused = false; + try { Draw(*device, alternating, 0x10052u, replace()); } catch (const std::exception& error) { orderedRefused = std::string(error.what()).find("late tests") != std::string::npos; } + Require(orderedRefused, "ordered fragment execution was not refused"); + bool indirectRefused = false; + try { Draw(*device, alternating, exported, replace(), 3u, 1u, false, {}, [](AgcDriver::Pm4::DrawParameters& draw) { draw.indirect.emplace(); }); } catch (const std::exception& error) { indirectRefused = std::string(error.what()).find("direct triangle lists") != std::string::npos; } + Require(indirectRefused, "an indirect replay draw was not refused"); + auto killed = std::vector(alternating.begin(), alternating.end()); + killed.insert(std::find(killed.begin(), killed.end(), 0xf800180fu), 0x7c000000u | (0xd2u << 17u) | (3u << 9u) | 0x80u); + Draw(*device, killed, exported, replace()); + Check(even, "shader kill remains effective before reference filtering"); + auto skippedExport = std::vector(alternating.begin(), alternating.end()); + skippedExport.insert(skippedExport.begin(), 0xbe94037eu); + skippedExport.insert(skippedExport.begin() + 7, 0x7c000000u | (0xd2u << 17u) | (3u << 9u) | 0x80u); + skippedExport.insert(std::find(skippedExport.begin(), skippedExport.end(), 0xf800180fu), 0xbefe0314u); + bool coverageRefused = false; + try { Draw(*device, skippedExport, exported, replace()); } catch (const std::exception& error) { coverageRefused = std::string(error.what()).find("every kept path") != std::string::npos; } + Require(coverageRefused, "a stencil export skipped by kept fragments was not refused"); + auto userColor = std::vector(alternating.begin(), alternating.end()); + *std::find(userColor.begin(), userColor.end(), 0x7e0e02f2u) = 0x7e0e0200u; + const std::array colorData{0x3f800000u}; + Draw(*device, userColor, exported, replace(), 3u, 1u, false, {}, {}, Vertices, VertexCode, colorData); + Check(all, "dynamic reference preserves the adjacent shader-data push payload"); + auto guarded = std::vector(alternating.begin(), alternating.end()); + *std::find(guarded.begin(), guarded.end(), 0x7e0e02f2u) = 0x7e0e020cu; + guarded.insert(guarded.begin(), {0xf4040280u, 0xfa000018u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu}); + alignas(256) std::array root{}; + const std::uint32_t payload = 0x3f800000u; + const auto rootAddress = static_cast(reinterpret_cast(root.data())); + const std::array rootData{static_cast(rootAddress), static_cast(rootAddress >> 32u)}; + const auto payloadAddress = static_cast(reinterpret_cast(&payload)); + root[6] = static_cast(payloadAddress); + root[7] = static_cast(payloadAddress >> 32u); + const std::array guardedMemory{{{rootAddress, std::as_bytes(std::span(root))}, {payloadAddress, std::as_bytes(std::span(&payload, 1))}}}; + Draw(*device, guarded, exported, replace(), 6u, 2u, false, {}, {}, Vertices, VertexCode, rootData, guardedMemory); + Check(all, "readonly guarded SRT survives repeated references, primitives and instances"); + root[6] = root[7] = 0u; + bool faultThrown = false; + try { Draw(*device, guarded, exported, replace(), 6u, 2u, false, {}, {}, Vertices, VertexCode, rootData, std::span(guardedMemory).first(1)); } + catch (const std::exception& error) { faultThrown = std::string(error.what()).find("BDA") != std::string::npos; } + Require(faultThrown, "poisoned readonly SRT replay did not throw a host fault"); + Require(std::ranges::all_of(Pixels, [](std::byte value) { return value == Kept; }), "poisoned replay published guest color results before its fault"); + std::puts("stencil replay 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/StorageWriteBackOverhang.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackOverhang.cpp new file mode 100644 index 0000000000..4f5f7f8c0b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackOverhang.cpp @@ -0,0 +1,170 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.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 "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Width = 128; +constexpr std::uint32_t Height = 120; +constexpr std::uint32_t FormatRgba16Float = 71; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Standard64KB = 0x09; +constexpr std::size_t SurfaceBytes = 0x20000; +constexpr std::size_t AllocationBytes = std::size_t{Width} * Height * 8u; +constexpr std::uint8_t Sentinel = 0xa5; + +alignas(256) constexpr std::array WriteCode{0xd7460004u, 0x0401060eu, 0xd7460005u, 0x0405060fu, 0x7e100d04u, 0x7e120d05u, 0x7e140c0du, 0x7e1602f2u, 0xf0200f08u, 0x00000804u, 0xbf810000u}; + +std::array TextureDescriptor(const void* texels) { + const auto address = reinterpret_cast(texels); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (FormatRgba16Float << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + 0xfacu | (Standard64KB << 20u) | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +void Write(std::uint8_t* texels) { + std::vector data; + const auto texture = TextureDescriptor(texels); + data.insert(data.end(), texture.begin(), texture.end()); + data.resize(14, 0u); + data[13] = 7u; + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{8, 8, 1}; + registers(0x207, shape); + const auto code = reinterpret_cast(WriteCode.data()); + const std::array program{static_cast(code >> 8u), static_cast(code >> 40u)}; + registers(0x20c, program); + const std::array resource{(static_cast(data.size()) << 1u) | (7u << 7u) | (1u << 11u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, Width / 8u, Height / 8u, 1u, 0x41u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::Graphics::StorageTexture::FlushAllPending("test"); + AgcDriverWaitIdle_nid_postfix(); +} + +class GuestSurface { +public: + explicit GuestSurface(bool split) { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, Alignment)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + void* mapped = mmap(nullptr, SurfaceBytes + Alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + Alignment - 1) & ~(static_cast(Alignment) - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + SurfaceBytes != begin + SurfaceBytes + Alignment) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + Alignment - aligned); + block = reinterpret_cast(aligned); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, SurfaceBytes); + } +#endif + Require(block != nullptr, "storage write-back overhang: cannot allocate the guest surface"); + std::memset(block, 0, AllocationBytes); + std::memset(block + AllocationBytes, Sentinel, SurfaceBytes - AllocationBytes); + GuestAllocations::Mutation mutation; + if (split) { + mutation.Add(block, AllocationBytes, true, true, true); + mutation.Add(block + AllocationBytes, SurfaceBytes - AllocationBytes, true, true, true); + } else { + mutation.Add(block, SurfaceBytes, true, true, true); + } + this->split = split; + } + + ~GuestSurface() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + if (split) mutation.Remove(block + AllocationBytes); + } +#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 + } + + GuestSurface(const GuestSurface&) = delete; + GuestSurface& operator=(const GuestSurface&) = delete; + + std::uint8_t* Data() { return block; } + +private: + static constexpr std::size_t Alignment = 65536; + std::uint8_t* block = nullptr; + bool split = false; +}; + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + GuestSurface whole(false); + GuestSurface split(true); + Write(whole.Data()); + Write(split.Data()); + const auto* expected = whole.Data(); + const auto* stored = split.Data(); + const auto texel = [](const std::uint8_t* at) { return !std::all_of(at, at + 8, [](std::uint8_t byte) { return byte == 0 || byte == Sentinel; }); }; + std::size_t written = 0; + std::size_t past = 0; + for (std::size_t at = 0; at < SurfaceBytes; at += 8) { + written += texel(expected + at); + past += at >= AllocationBytes && texel(expected + at); + } + Require(written != 0, "storage write-back overhang: the fully registered surface received no texels"); + Require(past != 0, "storage write-back overhang: no texel of the fully registered surface lies past pitch x height"); + const auto differs = std::mismatch(expected, expected + SurfaceBytes, stored); + Require(differs.first == expected + SurfaceBytes, "storage write-back overhang: byte 0x" + [&] { char text[32]; std::snprintf(text, sizeof(text), "%zx", static_cast(differs.first - expected)); return std::string(text); }() + " differs from the fully registered surface (" + std::to_string(written) + " texels, " + std::to_string(past) + " past pitch x height)"); + Require(std::any_of(stored + AllocationBytes, stored + SurfaceBytes, [](std::uint8_t byte) { return byte == Sentinel; }), "storage write-back overhang: the padding past the image's rows was overwritten"); + AgcDriverShutdown_nid_postfix(); + std::puts("storage write-back overhang tests passed"); + 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/StoreOverUntracked.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp index 99e31a350a..3641153174 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp @@ -44,11 +44,16 @@ constexpr std::size_t SurfaceBytes = 4 * Block; constexpr std::uint32_t Untouched = 0xdeadbeefu; constexpr std::uint32_t Stored = 0x12345678u; constexpr std::uint32_t Filled = 0x9abcdef0u; +constexpr std::uint32_t StoredAgain = 0x0badf00du; alignas(256) constexpr std::array StoreCode{ 0x7e040300, 0x7e060280, 0x7e0202ff, Stored, 0xf0200108, 0x00020102, 0xbf810000, }; +alignas(256) constexpr std::array StoreAgainCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, StoredAgain, 0xf0200108, 0x00020102, 0xbf810000, +}; + alignas(256) constexpr std::array FillCode{ 0xd7460004, 0x04010c08, 0x7e000204, 0x7e020205, 0x7e040206, 0x7e060207, 0xe01c2000, 0x80000004, 0xbf810000, }; @@ -91,11 +96,11 @@ void Dispatch(std::span code, std::uint32_t threads, std::u AgcDriver::Submit(&packet, 0); } -void Store(const std::uint32_t* texels, std::uint32_t side = Side) { +void Store(const std::uint32_t* texels, std::uint32_t side = Side, std::span code = StoreCode) { std::array userData{}; const auto texture = TextureDescriptor(texels, side); std::copy(texture.begin(), texture.end(), userData.begin() + 8); - Dispatch(StoreCode, 8, static_cast(userData.size()) << 1u, userData, 1); + Dispatch(code, 8, static_cast(userData.size()) << 1u, userData, 1); } void Fill(const std::uint32_t* texels) { @@ -219,11 +224,76 @@ struct Case { enum class Outcome { Kept, Lost, Missed }; +// The surface is outside the write watch from the start. Its first results are stored, which +// leaves their bytes saved for the next store to compare with; the CPU writes another block, then +// the next results land and are stored: the CPU's block was written after the first store, so it +// keeps the CPU's bytes as the write watch would keep them. +Outcome RunSecondStore(const char* name) { + Surface surface(true); + struct WaitForWork { + ~WaitForWork() { AgcDriverWaitIdle_nid_postfix(); } + } waitForWork; + const auto address = surface.Address(); + if (!AgcDriver::GuestMemory::Watched(address, SurfaceBytes)) { + std::fprintf(stderr, "[store over untracked] %s: not run, the surface is not write-watched\n", name); + return Outcome::Kept; + } + AgcDriver::GuestMemory::Unwatch(address, SurfaceBytes); + Require(!AgcDriver::GuestMemory::Watched(address, SurfaceBytes), "store over untracked: the surface is still write-watched"); + std::unique_lock hold(AgcDriver::GuestMemory::GpuMutex(), std::defer_lock); + const auto heldLive = [&](std::uint32_t result) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (true) { + hold.lock(); + const std::array, 1> range{{{address, address + SurfaceBytes}}}; + if (StorageTexture::FindLive(address, SurfaceBytes) != nullptr && StorageTexture::AnyPendingOverlaps(range)) return true; + hold.unlock(); + if (Holds(surface.Texels(), result)) return false; + Require(std::chrono::steady_clock::now() < deadline, "store over untracked: the result neither became pending nor reached guest memory"); + std::this_thread::yield(); + } + }; + Store(surface.Texels()); + if (!heldLive(Stored)) return Outcome::Missed; + StorageTexture::FlushAllPending("store over untracked fixture"); + hold.unlock(); + { + std::vector first(SurfaceBytes); + AgcDriver::GuestMemory::Read(address, first); + } + Require(static_cast(surface.Texels())[0] == Stored, "store over untracked: the first result was not stored"); + hold.lock(); + const auto image = StorageTexture::FindLive(address, SurfaceBytes); + const bool saved = image != nullptr && image->GuestSnapshotValid(); + hold.unlock(); + Require(image != nullptr, "store over untracked: the surface has no storage image"); + if (!saved) { + std::fprintf(stderr, "[store over untracked] %s: the store left no saved guest bytes\n", name); + return Outcome::Lost; + } + auto* written = surface.Bytes() + 2 * Block; + for (std::size_t offset = 0; offset < Page; ++offset) static_cast(written)[offset] = Pattern(offset); + Store(surface.Texels(), Side, StoreAgainCode); + if (!heldLive(StoredAgain)) return Outcome::Missed; + StorageTexture::FlushAllPending("store over untracked fixture"); + hold.unlock(); + std::vector whole(SurfaceBytes); + AgcDriver::GuestMemory::Read(address, whole); + Require(static_cast(surface.Texels())[0] == StoredAgain, "store over untracked: the second result did not reach a block the CPU left alone"); + const auto lost = LostBytes(written); + if (lost == 0) return Outcome::Kept; + std::fprintf(stderr, "[store over untracked] %s: of 0x%zx bytes the CPU wrote between the two stores, %zu were lost\n", name, Page, lost); + return Outcome::Lost; +} + Outcome Run(const Case& test) { std::optional source; if (test.kind == Kind::Copied || test.kind == Kind::NeighbourCopied) source.emplace(); const bool leaves = test.kind == Kind::LeftWatch || test.kind == Kind::WrittenThenLeftWatch || test.kind == Kind::ImportLeftWatch; Surface surface(leaves); + struct WaitForWork { + ~WaitForWork() { AgcDriverWaitIdle_nid_postfix(); } + } waitForWork; auto* bytes = surface.Bytes(); const auto address = surface.Address(); std::unique_lock hold(AgcDriver::GuestMemory::GpuMutex(), std::defer_lock); @@ -365,6 +435,13 @@ int main() { Require(outcome != Outcome::Missed, "store over untracked: the result was never held pending"); kept = outcome == Outcome::Kept && kept; } + { + constexpr auto name = "a second store over a surface outside the write watch"; + auto outcome = Outcome::Missed; + for (std::uint32_t attempt = 0; attempt < 256u && outcome == Outcome::Missed; ++attempt) outcome = RunSecondStore(name); + Require(outcome != Outcome::Missed, "store over untracked: the result was never held pending"); + kept = outcome == Outcome::Kept && kept; + } Require(kept, "a pending storage result was stored over bytes the CPU wrote to untracked memory"); AgcDriverShutdown_nid_postfix(); std::puts("store over untracked tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StreamCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StreamCopy.cpp new file mode 100644 index 0000000000..65bbde4890 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StreamCopy.cpp @@ -0,0 +1,177 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::DriverDetail::MatchesStreamCopyKernel; +using AgcDriver::DriverDetail::StreamCopyRange; + +alignas(256) constexpr std::array Code{0xd7460008u, 0x04010e08u, 0x8f6a8708u, 0xd76d0009u, 0x0401806au, 0xe00c2000u, 0x80000008u, 0xe00c2000u, 0x80000409u, 0xbf8c3f71u, 0xe01c2000u, 0x80010008u, 0xbf8c3f70u, 0xe01c2000u, 0x80010409u, 0xbf810000u}; +constexpr std::uint32_t Untouched = 0xcafebabeu; +constexpr std::uint32_t Format = 0x0004bfacu; +constexpr std::uint32_t LargeGroups = 8193; +constexpr std::size_t Words = (std::size_t{LargeGroups} * 128u + 64u) * 4u; +alignas(256) std::array Source{}; +alignas(256) std::array Destination{}; + +std::vector UserData(const std::uint32_t* source, std::uint32_t sourceRecords, const std::uint32_t* destination, std::uint32_t destinationRecords, std::uint32_t sourceStride = 16, std::uint32_t destinationStride = 16) { + const auto from = reinterpret_cast(source); + const auto to = reinterpret_cast(destination); + return {static_cast(from), static_cast((from >> 32u) & 0xffffu) | (sourceStride << 16u), sourceRecords, Format, + static_cast(to), static_cast((to >> 32u) & 0xffffu) | (destinationStride << 16u), destinationRecords, Format}; +} + +void MatchTests() { + const ShaderRecompiler::ShaderComputeStageInfo compute{{64, 1, 1}, 0, {true, false, false}, false, 1}; + const auto data = UserData(Source.data(), 512, Destination.data(), 512); + Require(MatchesStreamCopyKernel(Code, data, compute), "the stream copy kernel was not recognized"); + for (std::size_t word = 0; word < Code.size(); ++word) { + auto altered = Code; + altered[word] ^= 1u; + Require(!MatchesStreamCopyKernel(altered, data, compute), "a different shader matched the stream copy kernel (word " + std::to_string(word) + ")"); + } + Require(!MatchesStreamCopyKernel(std::span(Code).first(15), data, compute), "a truncated shader matched"); + Require(!MatchesStreamCopyKernel(Code, UserData(Source.data(), 512, Destination.data(), 512, 4), compute), "a source stride other than 16 matched"); + Require(!MatchesStreamCopyKernel(Code, UserData(Source.data(), 512, Destination.data(), 512, 16, 4), compute), "a destination stride other than 16 matched"); + auto swizzled = data; + swizzled[5] |= 0x80000000u; + Require(!MatchesStreamCopyKernel(Code, swizzled, compute), "a swizzled destination matched"); + auto formats = data; + formats[7] ^= 0x1000u; + Require(!MatchesStreamCopyKernel(Code, formats, compute), "descriptors of different formats matched"); + const auto both = [&](std::uint32_t word3) { + auto changed = data; + changed[3] = changed[7] = word3; + return MatchesStreamCopyKernel(Code, changed, compute); + }; + Require(!both((Format & ~0x7f000u) | (0x14u << 12u)), "a 32-bit format matched a copy of 16-byte elements"); + Require(!both((Format & ~0xfffu) | 0xc6cu), "a swapped destination select matched"); + Require(!both(Format | 0x00800000u), "an add-tid descriptor matched"); + Require(!both(Format | 0x30000000u), "a raw out-of-bounds mode matched"); + Require(!both(Format | 0x40000000u), "a descriptor of another type matched"); + auto extra = data; + extra.push_back(0); + Require(!MatchesStreamCopyKernel(Code, extra, compute), "a different user SGPR count matched (the group id moves)"); + auto wave32 = compute; + wave32.numThreads[0] = 32; + Require(!MatchesStreamCopyKernel(Code, data, wave32), "a group of 32 threads matched"); + auto noGroup = compute; + noGroup.groupIdEnable[0] = false; + Require(!MatchesStreamCopyKernel(Code, data, noGroup), "a dispatch without the group id matched"); + + const auto source = reinterpret_cast(Source.data()); + const auto destination = reinterpret_cast(Destination.data()); + const auto copy = StreamCopyRange(data, 4); + Require(copy && copy->source == source && copy->destination == destination && copy->bytes == 8192, "4 groups were not taken as a copy of 512 elements"); + const auto clamped = StreamCopyRange(UserData(Source.data(), 512, Destination.data(), 300), 4); + Require(clamped && clamped->bytes == 300u * 16u, "the copy was not clamped to the destination's records"); + const auto launched = StreamCopyRange(UserData(Source.data(), 600, Destination.data(), 600), 4); + Require(launched && launched->bytes == 8192, "the copy exceeded the launched threads"); + Require(!StreamCopyRange(UserData(Source.data(), 400, Destination.data(), 512), 4), "a source with fewer records than the copy was taken"); + Require(!StreamCopyRange(UserData(Source.data(), 512, Destination.data(), 0), 4), "a copy of 0 bytes was taken"); + Require(!StreamCopyRange(data, 0), "a copy of 0 groups was taken"); + Require(StreamCopyRange(UserData(Source.data(), 8192u * 128u, Destination.data(), 8192u * 128u), 8192).has_value(), "a copy of 16 MiB was refused"); + Require(!StreamCopyRange(UserData(Source.data(), LargeGroups * 128u, Destination.data(), LargeGroups * 128u), LargeGroups), "a copy above 16 MiB was taken"); + Require(!StreamCopyRange(UserData(Source.data() + 1, 512, Destination.data(), 512), 4), "a source not aligned to 16 bytes was taken"); + Require(!StreamCopyRange(UserData(Source.data(), 512, Destination.data() + 1, 512), 4), "a destination not aligned to 16 bytes was taken"); +} + +std::vector Commands(const std::vector& data, std::uint32_t groups) { + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{64, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast(Code.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array resource{(static_cast(data.size()) << 1u) | (1u << 7u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, groups, 1, 1, 0x41u}); + return words; +} + +std::uint64_t Taken() { + using namespace AgcDriver::DriverDetail; + return copyPathCounts[CopyCpu].load() + copyPathCounts[CopyGpu].load() + copyPathCounts[CopyAliased].load(); +} + +void ExecuteTests() { + using AgcDriver::DriverDetail::copyPathCounts; + using AgcDriver::DriverDetail::CopyShape; + struct Case { + const char* name; + std::uint32_t groups, sourceRecords, destinationRecords, sourceOffset, destinationOffset; + bool hle; + }; + constexpr std::array cases{{ + {"4 groups", 4, 512, 512, 0, 0, true}, + {"16 groups", 16, 2048, 2048, 0, 0, true}, + {"destination records below the launch", 4, 512, 300, 0, 0, true}, + {"records above the launch", 4, 600, 600, 0, 0, true}, + {"source records below the copy", 4, 400, 512, 0, 0, false}, + {"0 destination records", 4, 512, 0, 0, 0, false}, + {"above 16 MiB", LargeGroups, LargeGroups * 128u, LargeGroups * 128u, 0, 0, false}, + {"source not aligned to 16 bytes", 4, 512, 512, 1, 0, false}, + {"destination not aligned to 16 bytes", 4, 512, 512, 0, 1, false}, + }}; + const bool hleEnabled = std::getenv("APS5_NO_COPY_HLE") == nullptr; + for (const auto& item : cases) { + const bool hle = item.hle && hleEnabled; + for (std::size_t i = 0; i < Source.size(); ++i) Source[i] = static_cast(i) * 2654435761u + 1u; + Destination.fill(Untouched); + const auto taken = Taken(); + const auto refused = copyPathCounts[CopyShape].load(); + const auto words = Commands(UserData(Source.data() + item.sourceOffset, item.sourceRecords, Destination.data() + item.destinationOffset, item.destinationRecords), item.groups); + Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Destination.data()), sizeof(Destination)); + const std::string where = std::string(" (") + item.name + ")"; + Require(Taken() - taken == (hle ? 1u : 0u), (hle ? "the copy did not take the HLE path" : "the copy took the HLE path") + where); + Require(copyPathCounts[CopyShape].load() - refused == (hle || !hleEnabled ? 0u : 1u), (hle ? "the copy was refused for its shape" : "the copy was not refused for its shape") + where); + const auto elements = std::min(item.groups * 128u, item.destinationRecords); + for (std::size_t i = 0; i < Destination.size(); ++i) { + const bool inside = i >= item.destinationOffset && (i - item.destinationOffset) / 4u < elements; + const auto element = (i - item.destinationOffset) / 4u; + const auto expected = !inside ? Untouched : element < item.sourceRecords ? Source[item.sourceOffset + i - item.destinationOffset] : 0u; + Require(Destination[i] == expected, "stream copy mismatch at word " + std::to_string(i) + where + ": got " + std::to_string(Destination[i]) + ", expected " + std::to_string(expected)); + } + } +} + +} + +int main() { + try { + if (std::getenv("APS5_NO_COPY_HLE") == nullptr) MatchTests(); + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + ExecuteTests(); + AgcDriverShutdown_nid_postfix(); + std::puts("stream copy tests passed"); + 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/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index c016c7423a..9ed2c5c32c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -23,6 +23,7 @@ #include #include #include +#include namespace { @@ -117,6 +118,13 @@ alignas(256) constexpr std::array DomainCode{ 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, 0x14131214, 0xbf810000, }; +alignas(256) constexpr std::array DomainLoweredInterior{ + 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, + 0xe0341020, 0x04030e01, 0xbf8c3f70, 0x08040af2, 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, + 0x10221705, 0x3e221b06, 0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff, 0x40400000, 0x08222511, + 0x7e260280, 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, 0x14131214, 0xbf810000, +}; + alignas(256) constexpr std::array DomainRelativeData{ 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0x4a2a0e81, 0x7e2a0d15, 0x102a2aff, 0x3e000000, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, 0xe0341020, 0x04030e01, 0xbf8c3f70, 0x08040af2, @@ -359,6 +367,8 @@ struct PatchDraw { std::span indices; float minLevel = 1.0f; float maxLevel = 64.0f; + std::uint32_t instances = 1; + std::uint32_t firstInstance = 0; }; alignas(256) constexpr std::array NextInputIndices{ @@ -394,7 +404,7 @@ ShaderRecompiler::RecompileResult Compile(const ShaderRecompiler::SpirvTarget& t pixel = info; } else { vertex = ShaderRecompiler::ShaderVertexStageInfo{}; - graphics = ShaderRecompiler::GraphicsCompileContext{hull ? 0u : 8u, {}, std::nullopt, patches.configuration, {reinterpret_cast(patches.indices.data()), static_cast(patches.indices.size()), 2, 1}}; + graphics = ShaderRecompiler::GraphicsCompileContext{hull ? 0u : 8u, {}, std::nullopt, patches.configuration, {reinterpret_cast(patches.indices.data()), static_cast(patches.indices.size()), 2, patches.instances}}; } auto layout = stage == ShaderStage::Fragment ? PixelPushLayout(pushCursor, target) : ShaderRecompiler::BindingLayout{0, 0, pushCursor, AgcDriver::Graphics::PipelinePushSlotBytes - pushCursor}; if (layout.pushConstantOffsetBytes < AgcDriver::Graphics::PipelinePushSlotBytes) layout.pushConstantSizeBytes = ShaderRecompiler::TessellationLevelPushOffsetBytes - layout.pushConstantOffsetBytes; @@ -477,7 +487,7 @@ void Draw(AgcDriver::VulkanDevice& device, const Stages& stages, VkCullModeFlags state.blend.colorWriteMask = 15; state.blends = {state.blend}; state.blendConstants = {}; - device.Draw(state, {reinterpret_cast(patches.indices.data()), static_cast(patches.indices.size()), 2, 1, 0, true}, shaders); + device.Draw(state, {reinterpret_cast(patches.indices.data()), static_cast(patches.indices.size()), 2, patches.instances, 0, true, 0, patches.firstInstance}, shaders); device.WaitIdle(); } @@ -621,6 +631,33 @@ void RequireRelativePatches(AgcDriver::VulkanDevice& device, const ShaderRecompi } } +void RequireInstancedRelativePatches(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + const auto center = Row(-0.5f + 1.0f / 3.0f); + TessellationConfiguration restarting{3u, 3u, 1u, 2u, 2u, 4u, 8u}; + restarting.breakAtInstance = true; + for (const auto& [patches, first, last] : {std::tuple{PatchDraw{{3u, 3u, 1u, 2u, 2u, 4u, 8u}, Indices, 1.0f, 64.0f, 2u, 1u}, 128, 64}, std::tuple{PatchDraw{{3u, 3u, 1u, 2u, 2u, 3u, 3u}, Indices, 1.0f, 64.0f, 2u, 1u}, 32, 96}, std::tuple{PatchDraw{restarting, Indices, 1.0f, 64.0f, 2u, 1u}, 32, 96}}) { + std::uint32_t cursor = 0; + const Stages stages{ + Compile(target, ShaderStage::Local, LocalCode, cursor, patches), + Compile(target, ShaderStage::TessellationControl, HullRelativeData, cursor, patches), + Compile(target, ShaderStage::TessellationEvaluation, DomainPatchRead, cursor, patches), + Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches), + }; + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, patches); + for (const auto& [patch, expected] : {std::pair{0u, first}, std::pair{2u, last}}) { + const auto pixel = PixelAt(Column(patch, 0.25f), center); + Require(pixel[0] == 255 && std::abs(pixel[2] - expected) <= 1, "the patch in column " + std::to_string(patch) + " of an instanced draw with " + std::to_string(patches.configuration.patchesPerGroup) + " patches per group and " + std::to_string(patches.configuration.primitiveGroupSize) + " primitives per group" + (patches.configuration.breakAtInstance ? " and BREAK_WAVE_AT_EOI" : "") + " did not take its blue channel from the hull's relative patch index across instances, " + PixelText(pixel)); + } + } + const PatchDraw shared{{3u, 3u, 1u, 2u, 2u, 4u, 8u}, Indices, 1.0f, 64.0f, 2u, 1u}; + ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainRelativeData, "domain program uses its relative patch index as data in an instanced draw", shared); + std::vector capabilities(target.supportedCapabilities.begin(), target.supportedCapabilities.end()); + std::erase(capabilities, static_cast(spv::CapabilityDrawParameters)); + auto withoutDrawParameters = target; + withoutDrawParameters.supportedCapabilities = capabilities; + ExpectRefusal(withoutDrawParameters, ShaderStage::TessellationControl, HullRelativeData, "shaderDrawParameters", shared); +} + class GuestBlock { public: GuestBlock() { @@ -689,6 +726,32 @@ void RequireHullBdaReads(AgcDriver::VulkanDevice& device, const ShaderRecompiler address(0u); } +Stages CompileLoweredInterior(const ShaderRecompiler::SpirvTarget& target, const PatchDraw& patches) { + std::uint32_t cursor = 0; + return { + Compile(target, ShaderStage::Local, LocalCode, cursor, patches), + Compile(target, ShaderStage::TessellationControl, HullCode, cursor, patches), + Compile(target, ShaderStage::TessellationEvaluation, DomainLoweredInterior, cursor, patches), + Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches), + }; +} + +void RequireFractionalEvenPartitioning(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + const auto below = Row(-0.75f); + const PatchDraw integer{{3u, 3u, 1u, 0u, 2u}, Indices}; + Draw(device, CompileLoweredInterior(target, integer), VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, integer); + Require(CoveredPixels(0, false) > 100 && PixelAt(Column(0, 0.25f), below) == Background, "integer partitioning: the level-1 patch has a domain point inside it, " + PixelText(PixelAt(Column(0, 0.25f), below))); + Require(PixelAt(Column(2, 0.25f), below) != Background, "integer partitioning: the level-5 patch has no domain point inside it"); + const PatchDraw patches{{3u, 3u, 1u, 3u, 2u}, Indices}; + const auto stages = CompileLoweredInterior(target, patches); + Require(HasSpacing(stages.domain, spv::ExecutionModeSpacingFractionalEven) && !HasSpacing(stages.domain, spv::ExecutionModeSpacingFractionalOdd) && !HasSpacing(stages.domain, spv::ExecutionModeSpacingEqual), "fractional-even partitioning did not compile to fractional-even spacing"); + Require(HasLevelBound(stages.hull, 64.0f) && !HasLevelBound(stages.hull, 63.0f), "fractional-even partitioning did not bound its tessellation factors at 64"); + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, patches); + Require(CoveredPixels(0, false) > 100 && PixelAt(Column(0, 0.25f), below) != Background, "fractional-even partitioning: the level-1 patch was not raised to level 2 with a domain point inside it"); + Require(CoveredPixels(1, false) == 0 && PixelAt(Column(1, 0.25f), below) == Background, "fractional-even partitioning: the patch with level 0 was not culled"); + Require(CoveredPixels(2, false) > 100 && PixelAt(Column(2, 0.25f), below) != Background, "fractional-even partitioning: the level-5 patch has no domain point inside it"); +} + void RequireQuads(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { const auto center = Row(0.0f); std::array frontFacing{}; @@ -802,8 +865,6 @@ int main() { ExpectRefusal(target, ShaderStage::Local, LocalIndexAsData, "outside a tessellation ring or LDS address"); ExpectRefusal(target, ShaderStage::TessellationControl, HullRelativeData, "GE_CNTL.PRIM_GRP_SIZE is 0", RecordPatches); ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainRelativeData, "GE_CNTL.PRIM_GRP_SIZE is 0", RecordPatches); - ExpectRefusal(target, ShaderStage::TessellationControl, HullRelativeData, "GE_CNTL.BREAK_WAVE_AT_EOI is clear", {{3u, 3u, 1u, 2u, 2u, 3u, 3u}, Indices}); - ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainRelativeData, "GE_CNTL.BREAK_WAVE_AT_EOI is clear", {{3u, 3u, 1u, 2u, 2u, 3u, 3u}, Indices}); ExpectRefusal(target, ShaderStage::TessellationControl, HullCode, "outside its patch's quad factors", {{3u, 3u, 2u, 2u, 2u}, Indices}); ExpectRefusal(target, ShaderStage::TessellationControl, HullDescendingRecords, "not affine"); ExpectRefusal(target, ShaderStage::TessellationControl, HullDescendingRecords, "not affine", {{3u, 3u, 1u, 2u, 2u, 158u}, Indices}); @@ -819,9 +880,11 @@ int main() { RequireCounterClockwise(*device, target); RequireLevelClamps(*device, target); RequireIntegerPartitioning(*device, target); + RequireFractionalEvenPartitioning(*device, target); RequireQuads(*device, target); RequirePatchConstants(*device, target); RequireRelativePatches(*device, target); + RequireInstancedRelativePatches(*device, target); RequireHullBdaReads(*device, target); RequireCompareBranches(target); device.reset(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/TgSizeWaveId.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/TgSizeWaveId.cpp new file mode 100644 index 0000000000..79d60e331f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/TgSizeWaveId.cpp @@ -0,0 +1,94 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Threads = 256; + +alignas(256) constexpr std::array TgSizeWaveIdCode{ + 0x9384ff03, 0x00060006, 0x34020082, 0x7e040204, 0xdc708000, 0x00000201, 0xbf810000, 0xbf810000, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "TG_SIZE wave id: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + ~GuestBlock() { GuestAllocations::Mutation().Remove(block); } + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +void Run(AgcDriver::VulkanDevice& device, GuestBlock& guest, std::uint32_t waveSize) { + std::memset(guest.Data(), 0xff, BlockBytes); + const auto address = static_cast(reinterpret_cast(guest.Data())); + const std::vector userData{static_cast(address), static_cast(address >> 32u)}; + const std::span code(TgSizeWaveIdCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {true, false, false}, true, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 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(); + std::array waves{}; + std::memcpy(waves.data(), guest.Data(), sizeof(waves)); + for (std::uint32_t thread = 0; thread < Threads; ++thread) { + if (waves[thread] == thread / waveSize) continue; + Require(false, "wave" + std::to_string(waveSize) + " thread " + std::to_string(thread) + " read wave " + std::to_string(waves[thread]) + " from TG_SIZE[11:6], expected " + std::to_string(thread / waveSize)); + } + std::printf("wave%u: TG_SIZE[11:6] holds the wave index for all %u threads\n", waveSize, Threads); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock guest; + Run(*device, guest, 64); + Run(*device, guest, 32); + std::puts("TG_SIZE wave id 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/VCmpF32Denormals.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp index 54e132b6c6..22568a2f9a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp @@ -95,17 +95,9 @@ std::string Hex(std::uint32_t 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) { +ShaderRecompiler::RecompileResult Compile(const AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode mode, std::uint32_t compare, std::uint32_t capture) { 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)); @@ -122,8 +114,19 @@ std::uint32_t Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloat }; request.useCache = false; request.context.floatMode = mode; - const auto result = ShaderRecompiler::Recompile(request); - device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + return ShaderRecompiler::Recompile(request); +} + +std::uint32_t Run(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& program, std::uint32_t group) { + 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); + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(Code.data())); device.WaitIdle(); std::uint32_t mask = 0u; for (std::uint32_t lane = 0; lane < Threads; ++lane) { @@ -135,8 +138,9 @@ std::uint32_t Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloat 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"; + const auto program = Compile(device, mode, exec ? row.cmpx : row.cmp, exec ? CaptureExec : CaptureVcc); 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); + const std::uint32_t actual = Run(device, program, group); if (actual == expected[group]) continue; const std::uint32_t diff = actual ^ expected[group]; for (std::uint32_t lane = 0; lane < Threads; ++lane) { diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 4f17031b65..482edbfdaf 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -152,6 +152,9 @@ struct Instance { AVCodecContext* opus = nullptr; std::uint32_t opusChannels = 0; std::uint32_t opusMappingFamily = 0; + std::uint32_t opusStreams = 0; + std::uint32_t opusCoupled = 0; + bool opusLayoutPending = false; std::uint32_t opusSampleRate = 0; std::vector opusPending; AVCodecContext* aac = nullptr; @@ -401,12 +404,13 @@ std::int32_t InitializeInstance(Instance& instance, const std::uint8_t* paramete 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 && (mappingFamily != 255 || channels != 1)) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus mapping family other than 0, 1 or a mono stream's 255)"); + if (mappingFamily > 1 && mappingFamily != 255) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus mapping family other than 0, 1 or 255)"); if (mappingFamily == 0 && channels > 2) return AJM_RESULT_INVALID_PARAMETER; instance.opusChannels = channels; instance.opusMappingFamily = mappingFamily; instance.opusSampleRate = sampleRate; - OpenOpus(instance); + instance.opusLayoutPending = mappingFamily == 255 && channels > 1; + if (!instance.opusLayoutPending) OpenOpus(instance); instance.opusPending.clear(); instance.initialized = true; instance.totalDecodedSamples = 0; @@ -988,6 +992,21 @@ 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 == 255 && 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] = 255; + header[19] = static_cast(instance.opusStreams); + header[20] = static_cast(instance.opusCoupled); + for (std::uint32_t channel = 0; channel < instance.opusChannels; ++channel) header[21 + channel] = static_cast(channel); + } 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}; @@ -1034,6 +1053,80 @@ std::size_t OpusPacketSamples(const std::uint8_t* packet, std::size_t size) { return frames * frameSamples; } + +std::size_t OpusLength(const std::uint8_t* data, std::size_t size, std::size_t& at) { + if (at >= size) return SIZE_MAX; + const std::size_t first = data[at++]; + if (first < 252) return first; + if (at >= size) return SIZE_MAX; + return first + 4u * data[at++]; +} + +std::size_t OpusSelfDelimitedBytes(const std::uint8_t* data, std::size_t size) { + if (size < 1) return SIZE_MAX; + const std::uint32_t code = data[0] & 3u; + std::size_t at = 1; + if (code == 0) { + const std::size_t frame = OpusLength(data, size, at); + return frame == SIZE_MAX ? SIZE_MAX : at + frame; + } + if (code == 1) { + const std::size_t frame = OpusLength(data, size, at); + return frame == SIZE_MAX ? SIZE_MAX : at + 2 * frame; + } + if (code == 2) { + const std::size_t first = OpusLength(data, size, at); + const std::size_t second = OpusLength(data, size, at); + return first == SIZE_MAX || second == SIZE_MAX ? SIZE_MAX : at + first + second; + } + if (at >= size) return SIZE_MAX; + const std::uint8_t count = data[at++]; + const std::size_t frames = count & 0x3fu; + if (frames == 0) return SIZE_MAX; + std::size_t padding = 0; + if ((count & 0x40u) != 0) { + for (;;) { + if (at >= size) return SIZE_MAX; + const std::uint8_t value = data[at++]; + padding += value == 255 ? 254 : value; + if (value != 255) break; + } + } + std::size_t total = 0; + if ((count & 0x80u) != 0) { + for (std::size_t frame = 0; frame < frames; ++frame) { + const std::size_t bytes = OpusLength(data, size, at); + if (bytes == SIZE_MAX) return SIZE_MAX; + total += bytes; + } + } else { + const std::size_t bytes = OpusLength(data, size, at); + if (bytes == SIZE_MAX) return SIZE_MAX; + total = bytes * frames; + } + return at + total + padding; +} + +void OpenOpusFromPacket(Instance& instance, const std::uint8_t* data, std::size_t size) { + std::uint32_t streams = 0; + std::uint32_t coupled = 0; + std::size_t offset = 0; + while (offset < size) { + if ((data[offset] & 0x04u) != 0) ++coupled; + ++streams; + const std::uint32_t channels = coupled * 2 + (streams - coupled); + if (channels >= instance.opusChannels) break; + const std::size_t bytes = OpusSelfDelimitedBytes(data + offset, size - offset); + if (bytes == SIZE_MAX || bytes >= size - offset) break; + offset += bytes; + } + if (coupled * 2 + (streams - coupled) != instance.opusChannels) throw std::runtime_error("AJM Opus: a mapping family 255 packet's " + std::to_string(streams) + " streams (" + std::to_string(coupled) + " stereo) do not make its " + std::to_string(instance.opusChannels) + " channels"); + instance.opusStreams = streams; + instance.opusCoupled = coupled; + instance.opusLayoutPending = false; + OpenOpus(instance); +} + void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { const JobInput input(job, inputs); PcmOutputs pcmOutputs(outputs, job.outputCount); @@ -1083,6 +1176,7 @@ void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, if (frames == 0 && pcmOutputs.produced == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; break; } + if (instance.opusLayoutPending) OpenOpusFromPacket(instance, data, bytes); packet->data = const_cast(data); packet->size = static_cast(bytes); if (avcodec_send_packet(instance.opus, packet.get()) < 0) { diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index 6b0f691ad5..33093cc011 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(libSceAudioOut SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOutSdl.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOutShutdown.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2Context.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2PadMix.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2Port.cpp @@ -117,6 +118,15 @@ if(BUILD_TESTING) set_tests_properties(audio_out2_port_buffers PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(audio_out2_device_state_tests tests/AudioOut2DeviceState.cpp) + target_include_directories(audio_out2_device_state_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out2_device_state_tests PRIVATE libSceAudioOut libc) + configure_windows_unwind(audio_out2_device_state_tests) + add_test(NAME audio_out2_device_state COMMAND audio_out2_device_state_tests) + if(WIN32) + set_tests_properties(audio_out2_device_state PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(audio_out2_timing_tests tests/AudioOut2Timing.cpp) target_include_directories(audio_out2_timing_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(audio_out2_timing_tests PRIVATE libSceAudioOut libc) @@ -126,4 +136,17 @@ if(BUILD_TESTING) set_tests_properties(audio_out2_timing PROPERTIES RUN_SERIAL TRUE ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(audio_out_shutdown_tests tests/AudioOutShutdown.cpp) + target_include_directories(audio_out_shutdown_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out_shutdown_tests PRIVATE libSceAudioOut libkernel libc) + configure_windows_unwind(audio_out_shutdown_tests) + add_test(NAME audio_out2_shutdown COMMAND audio_out_shutdown_tests --audio-out2) + add_test(NAME audio_out2_waiting_push_shutdown COMMAND audio_out_shutdown_tests --audio-out2-waiting-push) + add_test(NAME audio_out2_advance_shutdown COMMAND audio_out_shutdown_tests --audio-out2-advance) + add_test(NAME audio_out_shutdown COMMAND audio_out_shutdown_tests --audio-out) + set_tests_properties(audio_out2_shutdown audio_out2_waiting_push_shutdown audio_out2_advance_shutdown audio_out_shutdown PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(audio_out2_shutdown audio_out2_waiting_push_shutdown audio_out2_advance_shutdown audio_out_shutdown PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index fcd7d359f8..2f63a8fb03 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -15,14 +15,17 @@ #include "SDL.h" #include "SceTypes.hpp" #include "prx/libc/include/PreciseWait.hpp" +#include "prx/libc/include/Shutdown.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include "prx/libSceAudioOut/include/AudioOutSdl.hpp" +#include "AudioOutShutdown.hpp" static constexpr int PORT_TYPE_MAIN = 0; static constexpr int PORT_TYPE_BGM = 1; static constexpr int PORT_TYPE_VOICE = 2; static constexpr int PORT_TYPE_PERSONAL = 3; static constexpr int PORT_TYPE_PADSPK = 4; +static constexpr int PORT_TYPE_EX = 6; static constexpr int PORT_TYPE_VIBRATION = 10; static constexpr int PORT_TYPE_AUDIO3D = 126; static constexpr int PORT_TYPE_AUX = 127; @@ -37,6 +40,12 @@ 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; static constexpr std::uint32_t EX_MONITOR_INFO_MIN_SIZE = 0xA0; +static constexpr std::uint32_t EX_OPEN_MODE_MAX = 10; +static constexpr std::uint32_t EX_OPEN_MODES = 0x61F; +static constexpr std::uint32_t EX_CONFIGURE_MODE_DEFAULT = 0xFF; +static constexpr std::uint32_t EX_CONFIGURE_TARGET_DEFAULT = 0xFF; +static constexpr int ERROR_EX_INVALID_MODE = -2144993259; +static constexpr int ERROR_EX_INVALID_CONTROL = -2144960503; enum class Format { Unknown, @@ -297,6 +306,34 @@ static Port* getPort(int handle) { return &g_ports[idx]; } +static int allocatePort(int type, std::uint32_t len, std::uint32_t freq, Format format) { + std::lock_guard lock(g_mutex); + for (int i = 0; i < PORTS_MAX; i++) { + if (!g_ports[i].used) { + Port& port = g_ports[i]; + port.used = true; + port.type = type; + port.samplesNum = len; + port.freq = freq; + port.format = format; + port.channels = channelsForFormat(format); + port.lastDataOutputTime = 0; + port.virtualQueueEnd = sceKernelGetProcessTime(); + port.mixLevel = type == PORT_TYPE_PADSPK ? DEFAULT_PADSPK_MIX_LEVEL : DEFAULT_VOLUME; + for (int c = 0; c < port.channels; c++) { + port.volume[c] = DEFAULT_VOLUME; + } + if (type != PORT_TYPE_VIBRATION && type != PORT_TYPE_EX) { + openDevice(port); + } + static const bool trace = std::getenv("APS5_TRACE_AUDIOOUT") != nullptr; + if (trace) std::fprintf(stderr, "[audioout] port %d: type %d, %u samples at %u Hz, format %d, device %u\n", i + 1, type, len, freq, static_cast(format), static_cast(port.device)); + return i + 1; + } + } + return -2144993275; +} + extern "C" { int APS5_VABI sceAudioOutInit() { @@ -333,31 +370,7 @@ int APS5_VABI sceAudioOutOpen(int userId, int type, int index, std::uint32_t len throw std::runtime_error("sceAudioOutOpen: unknown format param"); } - std::lock_guard lock(g_mutex); - for (int i = 0; i < PORTS_MAX; i++) { - if (!g_ports[i].used) { - Port& port = g_ports[i]; - port.used = true; - port.type = type; - port.samplesNum = len; - port.freq = freq; - port.format = format; - port.channels = channelsForFormat(format); - port.lastDataOutputTime = 0; - port.virtualQueueEnd = sceKernelGetProcessTime(); - port.mixLevel = type == PORT_TYPE_PADSPK ? DEFAULT_PADSPK_MIX_LEVEL : DEFAULT_VOLUME; - for (int c = 0; c < port.channels; c++) { - port.volume[c] = DEFAULT_VOLUME; - } - if (type != PORT_TYPE_VIBRATION) { - openDevice(port); - } - static const bool trace = std::getenv("APS5_TRACE_AUDIOOUT") != nullptr; - if (trace) std::fprintf(stderr, "[audioout] port %d: type %d, %u samples at %u Hz, format %d, device %u\n", i + 1, type, len, freq, static_cast(format), static_cast(port.device)); - return i + 1; - } - } - return -2144993275; + return allocatePort(type, len, freq, format); } int APS5_VABI sceAudioOutClose(int handle) { @@ -372,7 +385,8 @@ int APS5_VABI sceAudioOutClose(int handle) { return 0; } -int APS5_VABI sceAudioOutOutput(int handle, const void* ptr) { +int APS5_VABI sceAudioOutOutput(int handle, const void* ptr) try { + const AudioOutCall call; std::lock_guard outputLock(g_outputMutex); std::unique_lock lock(g_mutex); Port* port = getPort(handle); @@ -387,13 +401,16 @@ int APS5_VABI sceAudioOutOutput(int handle, const void* ptr) { } if (ptr != nullptr) port->lastDataOutputTime = sceKernelGetProcessTime(); return static_cast(port->samplesNum); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceAudioOutOutputs(AudioOutOutputParam* param, std::uint32_t num) { +int APS5_VABI sceAudioOutOutputs(AudioOutOutputParam* param, std::uint32_t num) try { if (param == nullptr || num == 0) { return -2144993276; } + const AudioOutCall call; std::lock_guard outputLock(g_outputMutex); std::unique_lock lock(g_mutex); @@ -419,6 +436,8 @@ int APS5_VABI sceAudioOutOutputs(AudioOutOutputParam* param, std::uint32_t num) } return static_cast(first.samplesNum); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } int APS5_VABI sceAudioOutSetVolume(int handle, std::uint32_t flag, int* vol) { @@ -554,13 +573,30 @@ int APS5_VABI sceAudioOutSetMixLevelPadSpk(int handle, int mixLevel) { return 0; } -int APS5_VABI sceAudioOutExClose() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAudioOutExClose(int handle) { + std::lock_guard outputLock(g_outputMutex); + std::lock_guard lock(g_mutex); + Port* port = getPort(handle); + if (port == nullptr || port->type != PORT_TYPE_EX) { + throw std::runtime_error("sceAudioOutExClose: handle " + std::to_string(handle) + " is not an open Ex port"); + } + *port = Port{}; return 0; } -int APS5_VABI sceAudioOutExConfigureOutput() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAudioOutExConfigureOutput(int control, std::uint64_t flags, std::uint32_t mode, std::uint32_t target, std::uint64_t option) { + if (control != 0) { + return ERROR_EX_INVALID_CONTROL; + } + if (flags != 0 || option != 0) { + throw std::runtime_error("sceAudioOutExConfigureOutput: flags " + std::to_string(flags) + " and option " + std::to_string(option) + " not supported"); + } + if (mode > EX_OPEN_MODE_MAX && mode != EX_CONFIGURE_MODE_DEFAULT) { + throw std::runtime_error("sceAudioOutExConfigureOutput: mode " + std::to_string(mode) + " not supported"); + } + if ((target < 1 || target > 3) && target != EX_CONFIGURE_TARGET_DEFAULT) { + throw std::runtime_error("sceAudioOutExConfigureOutput: target " + std::to_string(target) + " not supported"); + } return 0; } @@ -578,9 +614,13 @@ int APS5_VABI sceAudioOutExGetMonitorInfo(int type, int index, void* info, std:: return 0; } -int APS5_VABI sceAudioOutExOpen() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAudioOutExOpen(int userId, std::uint32_t mode) { + (void)userId; + if (mode > EX_OPEN_MODE_MAX || ((EX_OPEN_MODES >> mode) & 1) == 0) { + return ERROR_EX_INVALID_MODE; + } + const bool highRate = mode == 3 || mode == 4 || mode == 10; + return allocatePort(PORT_TYPE_EX, highRate ? 1024 : 256, highRate ? 192000 : 48000, Format::S16Stereo); } } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp index 96ca5c4273..74a4a2b1c4 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp @@ -13,21 +13,17 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/PreciseWait.hpp" +#include "prx/libc/include/Shutdown.hpp" #include "AudioOut2Internal.hpp" +#include "AudioOutShutdown.hpp" #include "prx/libSceAudioOut/include/AudioOutSdl.hpp" #include "prx/libScePad/include/PadState.hpp" -// An AudioOut2 context is the hardware output queue: every push appends a grain (num_grains samples) -// to a queue of queue_depth grains that plays in real time. Titles pace their mixer on that queue, so -// the level must follow the output clock. With an SDL device open, a grain's port buffers are read at -// the next push, the device callback plays the mixed grains behind a cushion and the level counts the -// grains beyond that cushion; otherwise each push is mixed at once and a wall-clock model of the -// queue stands in. - using Clock = std::chrono::steady_clock; static constexpr std::size_t CONTEXT_MEMORY = 0x10000; static constexpr std::uint32_t DEFAULT_MAX_PORTS = 16; +static constexpr std::size_t ADVANCED_GRAINS_MAX = 16; // Pushes, polls and advances traced individually before the trace falls back to the per-second summary. static constexpr std::uint64_t CALL_TRACE_FULL = 16; // Grains the device waits for before it starts playing (and again whenever it ran dry), so the @@ -262,16 +258,31 @@ static void TraceSummary(AudioOut2Context& context, Clock::time_point now) { extern "C" { -// The title's per-tick step between setting the ports' data and pushing. A push records the buffers the -// ports point at and the next push reads them, and the push paces the clock, so nothing is due here. -int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle ctx) { +int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle ctx) try { + const AudioOutCall call; auto* context = FromHandle(ctx); if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; std::lock_guard lock(context->lock); + if (context->device != 0) { + if (context->advancedGrains.size() >= ADVANCED_GRAINS_MAX) throw std::runtime_error("AudioOut2: more than 16 completed grains await a push"); + if (!context->advancedMode) { + while (!context->pendingGrains.empty()) { + Render(*context, context->pendingGrains.front()); + context->output.insert(context->output.end(), context->mix.begin(), context->mix.end()); + context->pendingGrains.pop_front(); + context->pendingSince.pop_front(); + } + context->advancedMode = true; + } + context->advancedGrains.push_back(AudioOut2CaptureGrain(*context, true)); + context->grainReady.notify_one(); + } context->advances++; context->summaryAdvances++; if (context->advances <= CALL_TRACE_FULL) AUDIOOUT2_TRACE("t=%.3f Advance ctx %p (pushes so far %llu)\n", AudioOut2TraceSeconds(), static_cast(context), static_cast(context->pushes)); return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam* params, void* buffer, size_t buffer_size, AudioOut2ContextHandle* ctx) { @@ -289,7 +300,8 @@ int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam* params, voi return 0; } -int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle ctx) { +int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle ctx) try { + const AudioOutCall call; auto* context = FromHandle(ctx); if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; AUDIOOUT2_TRACE("t=%.3f ContextDestroy ctx %p after %llu pushes\n", AudioOut2TraceSeconds(), static_cast(context), static_cast(context->pushes)); @@ -303,6 +315,8 @@ int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle ctx) { AudioOut2ReleasePorts(*context); delete context; return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } int APS5_VABI sceAudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint32_t* queue_level, uint32_t* available_queue) { @@ -318,7 +332,8 @@ int APS5_VABI sceAudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint3 return 0; } -int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t blocking) { +int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t blocking) try { + const AudioOutCall call; auto* context = FromHandle(ctx); if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; std::unique_lock lock(context->lock); @@ -336,13 +351,24 @@ int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t block lock.lock(); now = Clock::now(); } + if (context->advancedMode && context->advancedGrains.empty()) { + if (!blocking) throw std::runtime_error("AudioOut2: a non-blocking push without a rendered grain is not implemented"); + if (!context->grainReady.wait(lock, LibcShutdownToken_nid_postfix(), [&] { return !context->advancedGrains.empty(); })) throw ProcessShutdown{}; + now = Clock::now(); + Drain(*context, now); + } context->queued++; context->playHead += GrainDuration(*context); UpdatePadDevice(*context, now); - auto grain = AudioOut2CaptureGrain(*context); + auto grain = context->advancedMode ? std::move(context->advancedGrains.front()) : AudioOut2CaptureGrain(*context); + if (context->advancedMode) context->advancedGrains.pop_front(); const auto mixed = static_cast(grain.size()); if (context->device == 0) { Render(*context, grain); + } else if (context->advancedMode) { + Render(*context, grain); + if (PendingMs(*context) > MAX_QUEUED_MS) context->dropped++; + else context->output.insert(context->output.end(), context->mix.begin(), context->mix.end()); } else { context->pendingGrains.push_back(std::move(grain)); context->pendingSince.push_back(Clock::now()); @@ -373,6 +399,8 @@ int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t block } TraceSummary(*context, now); return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } int APS5_VABI sceAudioOut2ContextQueryMemory(const AudioOut2ContextParam* params, size_t* memory_size) { diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp index 1a6f8cc7e4..c58bd4c542 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp @@ -1,12 +1,15 @@ #ifndef CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUT2INTERNAL_HPP #define CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUT2INTERNAL_HPP +#include +#include #include #include #include #include #include #include +#include #include #include @@ -47,6 +50,8 @@ struct AudioOut2CapturedPort { std::size_t index = 0; std::uint64_t generation = 0; const void* data = nullptr; + std::vector pcm; + std::array volume{}; }; using AudioOut2Grain = std::vector; @@ -86,6 +91,9 @@ struct AudioOut2Context { // Mixed stereo frames the device has not played yet; it plays silence until the cushion is mixed. std::deque output; bool priming = true; + bool advancedMode = false; + std::condition_variable_any grainReady; + std::deque advancedGrains; std::deque pendingGrains; std::deque pendingSince; SDL_AudioDeviceID padDevice = 0; @@ -112,7 +120,7 @@ struct AudioOut2Context { // Mixes every port of the context that carries PCM data into out (stereo float, frames frames), summing // onto the zeroed buffer. Returns the number of ports mixed. std::uint32_t AudioOut2MixPorts(const AudioOut2Context& context, const AudioOut2Grain& grain, float* out, float* padOut, std::uint32_t frames); -AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context); +AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context, bool snapshot = false); bool AudioOut2HasPadPorts(const AudioOut2Context& context); // Forgets the ports of a context being destroyed. void AudioOut2ReleasePorts(const AudioOut2Context& context); diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp index bbb30e7968..14c9bc2c63 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp @@ -6,11 +6,14 @@ #include #include #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/Shutdown.hpp" #include "AudioOut2Internal.hpp" #include "AudioOut2PadMix.hpp" +#include "AudioOutShutdown.hpp" static constexpr std::uint16_t OUTPUT_MAIN = 1; static constexpr std::int16_t VOLUME_MAX = 127; @@ -74,15 +77,15 @@ static void ReadFrame(const AudioOut2Port& port, const void* data, std::uint32_t } } -static void AccumulatePadPort(const AudioOut2Port& port, const void* data, AudioOut2Route route, float* out, std::uint32_t frames) { +static void AccumulatePadPort(const AudioOut2Port& port, const void* data, AudioOut2Route route, float* out, std::uint32_t frames, const float* volume) { float in[AUDIO_OUT2_PORT_CHANNELS_MAX]; for (std::uint32_t frame = 0; frame < frames; frame++) { ReadFrame(port, data, frame, in); - AudioOut2AccumulatePadFrame(route, in, port.channels, port.volume, out + static_cast(frame) * AUDIO_OUT2_PAD_CHANNELS); + AudioOut2AccumulatePadFrame(route, in, port.channels, volume, out + static_cast(frame) * AUDIO_OUT2_PAD_CHANNELS); } } -static void AccumulatePort(const AudioOut2Port& port, const void* data, float* out, std::uint32_t frames) { +static void AccumulatePort(const AudioOut2Port& port, const void* data, float* out, std::uint32_t frames, const float* volume) { const auto& fold = *port.fold; float in[AUDIO_OUT2_PORT_CHANNELS_MAX]; for (std::uint32_t frame = 0; frame < frames; frame++) { @@ -90,7 +93,7 @@ static void AccumulatePort(const AudioOut2Port& port, const void* data, float* o float left = 0.0f; float right = 0.0f; for (std::uint32_t c = 0; c < port.channels; c++) { - const float sample = in[c] * port.volume[c]; + const float sample = in[c] * volume[c]; if (fold.left[c] != 0.0f) left += sample * fold.left[c]; if (fold.right[c] != 0.0f) right += sample * fold.right[c]; } @@ -99,7 +102,7 @@ static void AccumulatePort(const AudioOut2Port& port, const void* data, float* o } } -AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context) { +AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context, bool snapshot) { std::lock_guard lock(g_portsLock); AudioOut2Grain grain; for (std::size_t index = 0; index < g_ports.size(); index++) { @@ -110,7 +113,15 @@ AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context) { data = port.queuedData.front(); port.queuedData.pop_front(); } - if (data != nullptr) grain.push_back({index, port.generation, data}); + if (data != nullptr) { + AudioOut2CapturedPort captured{index, port.generation, data}; + if (snapshot) { + captured.pcm.resize(static_cast(context.grain) * port.channels * (port.int16 ? sizeof(std::int16_t) : sizeof(float))); + std::memcpy(captured.pcm.data(), data, captured.pcm.size()); + std::copy_n(port.volume, port.channels, captured.volume.begin()); + } + grain.push_back(std::move(captured)); + } } return grain; } @@ -118,13 +129,17 @@ AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context) { std::uint32_t AudioOut2MixPorts(const AudioOut2Context& context, const AudioOut2Grain& grain, float* out, float* padOut, std::uint32_t frames) { std::lock_guard lock(g_portsLock); std::uint32_t mixed = 0; - for (const auto& [index, generation, data] : grain) { + for (const auto& captured : grain) { + const auto index = captured.index; + const auto generation = captured.generation; + const void* data = captured.pcm.empty() ? captured.data : captured.pcm.data(); if (index >= g_ports.size()) continue; const auto& port = g_ports[index]; if (!port.used || port.generation != generation || port.context != &context || port.channels == 0) continue; const auto route = AudioOut2RouteForPort(port.type, port.channels); - if (route == AudioOut2Route::Main || (context.padDevice == 0 && route == AudioOut2Route::PadSpeaker)) AccumulatePort(port, data, out, frames); - else if (padOut != nullptr) AccumulatePadPort(port, data, route, padOut, frames); + const float* volume = captured.pcm.empty() ? port.volume : captured.volume.data(); + if (route == AudioOut2Route::Main || (context.padDevice == 0 && route == AudioOut2Route::PadSpeaker)) AccumulatePort(port, data, out, frames, volume); + else if (padOut != nullptr) AccumulatePadPort(port, data, route, padOut, frames, volume); mixed++; } return mixed; @@ -165,7 +180,8 @@ static void TraceAttribute(AudioOut2PortHandle handle, const AudioOut2Port& port extern "C" { -int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut2PortParam* params, AudioOut2PortHandle* port) { +int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut2PortParam* params, AudioOut2PortHandle* port) try { + const AudioOutCall call; if (!ctx) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; if (!params || !port) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT; const auto channels = (params->data_format >> FORMAT_CHANNELS_SHIFT) & FORMAT_CHANNELS_MASK; @@ -206,30 +222,39 @@ int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut2 AudioOut2TraceSeconds(), static_cast(ctx), static_cast(*port), params->port_type, params->data_format, entry.channels, params->sampling_freq, params->flags, static_cast(params->user_handle)); return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle port) { +int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle port) try { + const AudioOutCall call; std::lock_guard lock(g_portsLock); auto* entry = FromHandle(port); if (!entry) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; AUDIOOUT2_TRACE("t=%.3f PortDestroy port %llu (data sets %llu)\n", AudioOut2TraceSeconds(), static_cast(port), static_cast(entry->dataSets)); *entry = AudioOut2Port{}; return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceAudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortState* state) { +int APS5_VABI sceAudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortState* state) try { + const AudioOutCall call; std::lock_guard lock(g_portsLock); - if (!FromHandle(port)) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; + const auto* entry = FromHandle(port); + if (!entry) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; if (!state) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT; + *state = AudioOut2PortState{}; state->output = OUTPUT_MAIN; - state->num_channels = AUDIO_OUT2_OUTPUT_CHANNELS; + state->num_channels = static_cast(entry->channels); state->volume = VOLUME_MAX; - state->reroute_counter = 0; - state->flags = 0; return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle port, const AudioOut2Attribute* attributes, uint32_t num) { +int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle port, const AudioOut2Attribute* attributes, uint32_t num) try { + const AudioOutCall call; std::lock_guard lock(g_portsLock); auto* entry = FromHandle(port); if (!entry) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; @@ -255,6 +280,8 @@ int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle port, const Audi if (traces < ATTRIBUTE_TRACE_FULL || traces % ATTRIBUTE_TRACE_EVERY == 0) TraceAttribute(port, *entry, attribute, verdict); } return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOutShutdown.cpp b/core/libs/prx/libSceAudioOut/src/AudioOutShutdown.cpp new file mode 100644 index 0000000000..d980fd22fc --- /dev/null +++ b/core/libs/prx/libSceAudioOut/src/AudioOutShutdown.cpp @@ -0,0 +1,34 @@ +#include +#include +#include "prx/libc/include/Shutdown.hpp" +#include "AudioOutShutdown.hpp" + +namespace { + +std::atomic inside{0}; +std::atomic stopping{false}; + +void Leave() { + if (inside.fetch_sub(1) == 1) inside.notify_all(); +} + +void Stop() { + stopping.store(true); + for (auto count = inside.load(); count != 0; count = inside.load()) inside.wait(count); +} + +} + +AudioOutCall::AudioOutCall() { + static const bool registered = (LibcRegisterShutdown_nid_postfix(Stop), true); + static_cast(registered); + inside.fetch_add(1); + if (stopping.load() || LibcShutdownToken_nid_postfix().stop_requested()) { + Leave(); + throw ProcessShutdown{}; + } +} + +AudioOutCall::~AudioOutCall() { + Leave(); +} diff --git a/core/libs/prx/libSceAudioOut/src/AudioOutShutdown.hpp b/core/libs/prx/libSceAudioOut/src/AudioOutShutdown.hpp new file mode 100644 index 0000000000..39af8943ab --- /dev/null +++ b/core/libs/prx/libSceAudioOut/src/AudioOutShutdown.hpp @@ -0,0 +1,12 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUTSHUTDOWN_HPP +#define CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUTSHUTDOWN_HPP + +class AudioOutCall final { +public: + AudioOutCall(); + ~AudioOutCall(); + AudioOutCall(const AudioOutCall&) = delete; + AudioOutCall& operator=(const AudioOutCall&) = delete; +}; + +#endif diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2DeviceState.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2DeviceState.cpp new file mode 100644 index 0000000000..cfa5a7a062 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2DeviceState.cpp @@ -0,0 +1,137 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOut2ContextResetParam(AudioOut2ContextParam*); +int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam*, void*, std::size_t, AudioOut2ContextHandle*); +int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle); +int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle, const AudioOut2PortParam*, AudioOut2PortHandle*); +int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle); +int APS5_VABI sceAudioOut2PortGetState(AudioOut2PortHandle, AudioOut2PortState*); +int APS5_VABI sceAudioOut2GetSpeakerInfo(AudioOut2SpeakerInfo*, std::uint32_t); +int APS5_VABI sceAudioOut2GetSystemState(AudioOut2SystemState*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +namespace { + +constexpr int errorInvalidArgument = static_cast(0x80260502); +constexpr int errorInvalidHandle = static_cast(0x80260503); +constexpr std::uint16_t portTypeMain = 0; +constexpr std::uint16_t outputMain = 1; +constexpr std::int16_t volumeMax = 127; +constexpr std::uint32_t formatFloat = 0; +constexpr std::uint32_t formatS16 = 1; +constexpr std::uint32_t frequency = 48000; + +std::uint32_t Format(std::uint32_t channels, std::uint32_t type) { + return channels << 8 | type; +} + +void SetEnvironment(const char* name, const std::string& value) { +#ifdef _WIN32 + _putenv_s(name, value.c_str()); +#else + setenv(name, value.c_str(), 1); +#endif +} + +AudioOut2PortState StateOfPort(AudioOut2ContextHandle context, std::uint32_t format) { + AudioOut2PortParam params{}; + params.port_type = portTypeMain; + params.data_format = format; + params.sampling_freq = frequency; + AudioOut2PortHandle port = 0; + Require(sceAudioOut2PortCreate(context, ¶ms, &port) == 0); + AudioOut2PortState state; + std::memset(&state, 0xAB, sizeof(state)); + Require(sceAudioOut2PortGetState(port, &state) == 0); + Require(sceAudioOut2PortDestroy(port) == 0); + return state; +} + +void PortStateFollowsTheDataFormat(AudioOut2ContextHandle context) { + struct Case { + std::uint32_t channels; + std::uint32_t sampleType; + }; + for (const Case entry : {Case{1, formatFloat}, Case{2, formatFloat}, Case{2, formatS16}, Case{8, formatFloat}, Case{12, formatS16}}) { + const auto state = StateOfPort(context, Format(entry.channels, entry.sampleType)); + Require(state.output == outputMain); + Require(state.num_channels == entry.channels); + Require(state.volume == volumeMax); + Require(state.reroute_counter == 0); + Require(state.flags == 0); + Require(state.pad1 == 0 && state.pad2 == 0); + for (const auto word : state.reserved) Require(word == 0); + } +} + +void PortStateRejectsBadArguments(AudioOut2ContextHandle context) { + AudioOut2PortState state{}; + Require(sceAudioOut2PortGetState(0, &state) == errorInvalidHandle); + Require(sceAudioOut2PortGetState(0x7fff, &state) == errorInvalidHandle); + + AudioOut2PortParam params{}; + params.port_type = portTypeMain; + params.data_format = Format(2, formatFloat); + params.sampling_freq = frequency; + AudioOut2PortHandle port = 0; + Require(sceAudioOut2PortCreate(context, ¶ms, &port) == 0); + Require(sceAudioOut2PortGetState(port, nullptr) == errorInvalidArgument); + Require(sceAudioOut2PortDestroy(port) == 0); + Require(sceAudioOut2PortGetState(port, &state) == errorInvalidHandle); +} + +void SpeakerInfoReportsTheFrontPair() { + AudioOut2SpeakerInfo info; + std::memset(&info, 0xAB, sizeof(info)); + Require(sceAudioOut2GetSpeakerInfo(&info, 0) == 0); + Require(info.type == 0); + Require(info.available_bits == 0x3); + Require(info.flags == 0); + Require(info.speaker_angle[0].azimuth == -30 && info.speaker_angle[0].elevation == 0); + Require(info.speaker_angle[1].azimuth == 30 && info.speaker_angle[1].elevation == 0); + for (int index = 2; index < 16; index++) Require(info.speaker_angle[index].azimuth == 0 && info.speaker_angle[index].elevation == 0); + Require(sceAudioOut2GetSpeakerInfo(nullptr, 0) == errorInvalidArgument); +} + +void SystemStateIsSilentAndRejectsNull() { + AudioOut2SystemState state; + std::memset(&state, 0xAB, sizeof(state)); + Require(sceAudioOut2GetSystemState(&state) == 0); + Require(state.loudness == 0.0f); + Require(sceAudioOut2GetSystemState(nullptr) == errorInvalidArgument); +} + +} + +int main() { + const auto path = std::filesystem::temp_directory_path() / ("anyps5_audio_out2_device_state-" + std::to_string(std::random_device{}()) + ".raw"); + std::filesystem::remove(path); + SetEnvironment("SDL_DISKAUDIOFILE", path.string()); + + AudioOut2ContextParam params{}; + Require(sceAudioOut2ContextResetParam(¶ms) == 0); + params.num_grains = 256; + params.queue_depth = 1; + AudioOut2ContextHandle context = 0; + Require(sceAudioOut2ContextCreate(¶ms, nullptr, 0, &context) == 0); + + PortStateFollowsTheDataFormat(context); + PortStateRejectsBadArguments(context); + SpeakerInfoReportsTheFrontPair(); + SystemStateIsSilentAndRejectsNull(); + + Require(sceAudioOut2ContextDestroy(context) == 0); + std::filesystem::remove(path); + return 0; +} diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp index 54f467b334..cac79482e3 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp @@ -1,5 +1,7 @@ #include "SceTypes.hpp" +#include +#include #include #include #include @@ -7,15 +9,18 @@ #include #include #include +#include #include #include #include +#include #include extern "C" { int APS5_VABI sceAudioOut2ContextResetParam(AudioOut2ContextParam*); int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam*, void*, std::size_t, AudioOut2ContextHandle*); int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle); +int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle); int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle, std::uint32_t); int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle, const AudioOut2PortParam*, AudioOut2PortHandle*); int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle); @@ -49,7 +54,7 @@ void SetData(AudioOut2PortHandle port, const void* data) { Require(sceAudioOut2PortSetAttributes(port, &attribute, 1) == 0); } -std::vector Play(const std::function& feed) { +std::vector Play(const std::function& feed, bool advanced = false) { const auto path = std::filesystem::temp_directory_path() / ("anyps5_audio_out2_port_buffers-" + std::to_string(std::random_device{}()) + ".raw"); std::filesystem::remove(path); SetEnvironment("SDL_DISKAUDIOFILE", path.string()); @@ -67,7 +72,13 @@ std::vector Play(const std::function volume{0.5f, 0.5f}; + const AudioOut2Attribute attribute{1, 0, volume.data(), sizeof(volume)}; + Require(sceAudioOut2PortSetAttributes(port, &attribute, 1) == 0); + SetData(port, buffer.data()); + Require(sceAudioOut2ContextAdvance(context) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + }, true); + RequirePlayed(played, {&buffer, &reduced}); +} + +void TestPushingThreadWaitsForAdvance() { + const auto first = Grain(0.01f); + const auto second = Grain(0.02f); + const auto played = Play([&](AudioOut2ContextHandle context, AudioOut2PortHandle port) { + SetData(port, first.data()); + Require(sceAudioOut2ContextAdvance(context) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + std::promise entered; + std::promise done; + auto completed = done.get_future(); + std::thread pusher([&] { + entered.set_value(); + done.set_value(sceAudioOut2ContextPush(context, 1)); + }); + entered.get_future().wait(); + const bool waited = completed.wait_for(std::chrono::milliseconds(10)) == std::future_status::timeout; + SetData(port, second.data()); + Require(sceAudioOut2ContextAdvance(context) == 0); + Require(completed.get() == 0); + pusher.join(); + Require(waited); + }, true); + RequirePlayed(played, {&first, &second}); +} + } int main() { SetEnvironment("SDL_AUDIODRIVER", "disk"); + TestAdvanceRetainsRewrittenBuffer(); + TestAdvanceRetainsVolume(); + TestPushingThreadWaitsForAdvance(); TestBuffersSetBeforeOnePushPlayInOrder(); TestBurstThenGapKeepsEveryBuffer(); TestAdvancingPortWithoutNewBufferPlaysSilence(); diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutEx.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutEx.cpp index 22f70c9128..b96f9dd570 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOutEx.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutEx.cpp @@ -1,32 +1,66 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include -#include +#include extern "C" { -int APS5_VABI sceAudioOutExClose(); -int APS5_VABI sceAudioOutExConfigureOutput(); -int APS5_VABI sceAudioOutExOpen(); +int APS5_VABI sceAudioOutExOpen(int userId, std::uint32_t mode); +int APS5_VABI sceAudioOutExConfigureOutput(int control, std::uint64_t flags, std::uint32_t mode, std::uint32_t target, std::uint64_t option); +int APS5_VABI sceAudioOutExClose(int handle); +int APS5_VABI sceAudioOutOutput(int handle, const void* ptr); } -using ExFunction = int (APS5_VABI *)(); +static constexpr int SYSTEM_USER = 0xFF; +static constexpr int ERROR_EX_INVALID_MODE = static_cast(0x80260015u); +static constexpr int ERROR_EX_INVALID_CONTROL = static_cast(0x80268009u); -static bool ReportsNotImplemented(ExFunction function, const char* name) { +static bool failed = false; + +static void Require(bool condition, const char* what) { + if (!condition) { + std::fprintf(stderr, "failed: %s\n", what); + failed = true; + } +} + +template +static bool Throws(Call call) { try { - function(); - } catch (const std::runtime_error& error) { - if (std::string(error.what()) == std::string(name) + " not implemented") return true; - std::fprintf(stderr, "%s threw: %s\n", name, error.what()); - return false; + call(); + } catch (const std::runtime_error&) { + return true; } - std::fprintf(stderr, "%s returned instead of reporting that it is not implemented\n", name); return false; } int main() { - bool passed = ReportsNotImplemented(sceAudioOutExClose, "sceAudioOutExClose"); - passed = ReportsNotImplemented(sceAudioOutExConfigureOutput, "sceAudioOutExConfigureOutput") && passed; - passed = ReportsNotImplemented(sceAudioOutExOpen, "sceAudioOutExOpen") && passed; - return passed ? 0 : 1; + Require(sceAudioOutExOpen(SYSTEM_USER, 5) == ERROR_EX_INVALID_MODE, "mode 5 (Linear PCM) is refused"); + Require(sceAudioOutExOpen(SYSTEM_USER, 7) == ERROR_EX_INVALID_MODE, "mode 7 is outside mask 0x61F"); + Require(sceAudioOutExOpen(SYSTEM_USER, 11) == ERROR_EX_INVALID_MODE, "mode 11 is above 10"); + + const int ac3 = sceAudioOutExOpen(SYSTEM_USER, 0); + Require(ac3 > 0, "mode 0 opens a port"); + const std::vector burst(256 * 2, 0x1234); + Require(sceAudioOutOutput(ac3, burst.data()) == 256, "a mode 0 port takes 256 frames"); + Require(sceAudioOutOutput(ac3, nullptr) == 256, "a mode 0 port drains"); + + const int eac3 = sceAudioOutExOpen(SYSTEM_USER, 4); + Require(eac3 > 0 && eac3 != ac3, "mode 4 opens a second port"); + const std::vector wide(1024 * 2, 0); + Require(sceAudioOutOutput(eac3, wide.data()) == 1024, "a mode 4 port takes 1024 frames"); + + Require(sceAudioOutExConfigureOutput(1, 0, 0, 1, 0) == ERROR_EX_INVALID_CONTROL, "a nonzero first argument is refused"); + Require(sceAudioOutExConfigureOutput(0, 0, 0, 1, 0) == 0, "mode 0 to HDMI"); + Require(sceAudioOutExConfigureOutput(0, 0, 0, 0xFF, 0) == 0, "mode 0 to the default target"); + Require(sceAudioOutExConfigureOutput(0, 0, 0xFF, 0xFF, 0) == 0, "the reset"); + Require(Throws([] { sceAudioOutExConfigureOutput(0, 0, 11, 1, 0); }), "an unknown mode throws"); + Require(Throws([] { sceAudioOutExConfigureOutput(0, 0, 0, 4, 0); }), "an unknown target throws"); + Require(Throws([] { sceAudioOutExConfigureOutput(0, 1, 0, 1, 0); }), "nonzero flags throw"); + + Require(sceAudioOutExClose(ac3) == 0, "close the mode 0 port"); + Require(Throws([ac3] { sceAudioOutExClose(ac3); }), "a closed port throws"); + Require(sceAudioOutExClose(eac3) == 0, "close the mode 4 port"); + return failed ? 1 : 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutShutdown.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutShutdown.cpp new file mode 100644 index 0000000000..dd5390753d --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutShutdown.cpp @@ -0,0 +1,155 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/Shutdown.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOut2ContextResetParam(AudioOut2ContextParam*); +int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam*, void*, std::size_t, AudioOut2ContextHandle*); +int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle); +int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle, std::uint32_t); +int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle, const AudioOut2PortParam*, AudioOut2PortHandle*); +int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle, const AudioOut2Attribute*, std::uint32_t); +int APS5_VABI sceAudioOutOpen(int, int, int, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioOutOutput(int, const void*); +} + +namespace { + +constexpr std::uint32_t grain = 256; +constexpr std::uint32_t frequency = 48000; +constexpr std::uint16_t portTypeMain = 0; +constexpr std::uint32_t attributeData = 0; +constexpr std::uint32_t formatStereoFloat = 2u << 8; +constexpr int user = 0x10000000; +constexpr std::uint32_t formatS16Stereo = 1; +constexpr std::uint64_t startedOutputs = 3; +constexpr auto settle = std::chrono::milliseconds(100); +constexpr auto exitLimit = std::chrono::seconds(3); + +std::atomic outputs{0}; +std::vector pcm(static_cast(grain) * 2, 0.25f); + +void Fail(const char* message) { + std::fprintf(stderr, "%s\n", message); + std::fflush(stderr); + std::_Exit(1); +} + +void RequireParked() { + const auto before = outputs.load(); + std::this_thread::sleep_for(settle); + if (outputs.load() != before) Fail("the mixer thread kept outputting audio after the audio shutdown"); + std::puts("the mixer thread was parked before the audio tables were destroyed"); + std::fflush(stdout); +} + +void DelayExit() { + std::this_thread::sleep_for(settle); +} + +void ReportExit() { + std::puts("the exit finished while a push waited for an advance"); + std::fflush(stdout); +} + +void SetData(AudioOut2PortHandle port) { + const void* pointer = pcm.data(); + const AudioOut2Attribute attribute{attributeData, 0, &pointer, sizeof(pointer)}; + if (sceAudioOut2PortSetAttributes(port, &attribute, 1) != 0) Fail("a port attribute set returned an error"); +} + +void CreatePort(AudioOut2ContextHandle& context, AudioOut2PortHandle& port) { + AudioOut2ContextParam params{}; + if (sceAudioOut2ContextResetParam(¶ms) != 0) Fail("context parameters were not reset"); + params.num_grains = grain; + if (sceAudioOut2ContextCreate(¶ms, nullptr, 0, &context) != 0) Fail("context was not created"); + AudioOut2PortParam portParams{}; + portParams.port_type = portTypeMain; + portParams.data_format = formatStereoFloat; + portParams.sampling_freq = frequency; + if (sceAudioOut2PortCreate(context, &portParams, &port) != 0) Fail("port was not created"); +} + +void PushAudioOut2() { + AudioOut2ContextHandle context = 0; + AudioOut2PortHandle port = 0; + CreatePort(context, port); + std::thread([context, port] { + for (;;) { + SetData(port); + if (sceAudioOut2ContextPush(context, 1) != 0) Fail("a push during exit returned an error"); + outputs++; + } + }).detach(); +} + +void WaitInPushAudioOut2() { + AudioOut2ContextHandle context = 0; + AudioOut2PortHandle port = 0; + CreatePort(context, port); + SetData(port); + if (sceAudioOut2ContextAdvance(context) != 0) Fail("an advance returned an error"); + if (sceAudioOut2ContextPush(context, 1) != 0) Fail("a push returned an error"); + std::thread([context] { + outputs++; + sceAudioOut2ContextPush(context, 1); + Fail("a push waiting for an advance returned during exit"); + }).detach(); +} + +void AdvanceAtExit() { + AudioOut2ContextHandle context = 0; + AudioOut2PortHandle port = 0; + CreatePort(context, port); + SetData(port); + LibcRegisterShutdown_nid_postfix(DelayExit); + LibcRequestExit_nid_postfix(0); + sceAudioOut2ContextAdvance(context); + Fail("an advance after the exit request returned"); +} + +void OutputAudioOut() { + const int port = sceAudioOutOpen(user, 0, 0, grain, frequency, formatS16Stereo); + if (port <= 0) Fail("port was not opened"); + static std::vector data(static_cast(grain) * 2, 0x1000); + std::thread([port] { + for (;;) { + if (sceAudioOutOutput(port, data.data()) < 0) Fail("an output during exit returned an error"); + outputs++; + } + }).detach(); +} + +} + +int main(int argc, char** argv) { +#ifdef _WIN32 + _putenv_s("SDL_AUDIODRIVER", "dummy"); +#else + setenv("SDL_AUDIODRIVER", "dummy", 1); +#endif + const bool waiting = argc == 2 && std::strcmp(argv[1], "--audio-out2-waiting-push") == 0; + if (argc == 2 && std::strcmp(argv[1], "--audio-out2") == 0) PushAudioOut2(); + else if (argc == 2 && std::strcmp(argv[1], "--audio-out") == 0) OutputAudioOut(); + else if (waiting) WaitInPushAudioOut2(); + else if (argc == 2 && std::strcmp(argv[1], "--audio-out2-advance") == 0) AdvanceAtExit(); + else Fail("usage: audio_out_shutdown_tests --audio-out2 | --audio-out2-waiting-push | --audio-out2-advance | --audio-out"); + while (outputs.load() < (waiting ? 1 : startedOutputs)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (waiting) std::this_thread::sleep_for(settle); + std::thread([] { + std::this_thread::sleep_for(exitLimit); + Fail("the exit did not finish"); + }).detach(); + if (std::atexit(waiting ? ReportExit : RequireParked) != 0) Fail("atexit registration failed"); + LibcRequestExit_nid_postfix(0); + LibcAwaitExit_nid_postfix(); +} diff --git a/core/libs/prx/libSceFiber/CMakeLists.txt b/core/libs/prx/libSceFiber/CMakeLists.txt index d2ec7534ef..6aa8ddda2a 100644 --- a/core/libs/prx/libSceFiber/CMakeLists.txt +++ b/core/libs/prx/libSceFiber/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(libSceFiber SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceFiber PRIVATE ${LIBS_INCLUDE_DIR}) +target_compile_options(libSceFiber PRIVATE -fno-omit-frame-pointer) configure_windows_unwind(libSceFiber) if(BUILD_TESTING) diff --git a/core/libs/prx/libSceJpegDec/Export.cpp b/core/libs/prx/libSceJpegDec/Export.cpp index d38764bf71..0d92c485bb 100644 --- a/core/libs/prx/libSceJpegDec/Export.cpp +++ b/core/libs/prx/libSceJpegDec/Export.cpp @@ -5,14 +5,15 @@ #include #include #include +#include #include #include "Decoder/Jpeg.hpp" #include "SceTypes.hpp" -static_assert(sizeof(JpegDecCreateParam) == 0x8); +static_assert(sizeof(JpegDecCreateParam) == 0xC); static_assert(sizeof(JpegDecParseParam) == 0x10); -static_assert(sizeof(JpegDecDecodeParam) == 0x20); -static_assert(sizeof(JpegDecImageInfo) == 0x10); +static_assert(sizeof(JpegDecDecodeParam) == 0x30); +static_assert(sizeof(JpegDecImageInfo) == 0x24); namespace { @@ -24,10 +25,14 @@ constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_WORK_MEMORY = static_cast(0x80690010); constexpr std::int32_t SCE_JPEG_DEC_ERROR_DECODE_ERROR = static_cast(0x80690012); -constexpr std::uint32_t ATTRIBUTE_NONE = 0; +constexpr std::uint32_t ATTRIBUTE_CSC_ORDINARY = 1; +constexpr std::uint32_t ATTRIBUTE_CSC_FULL = 2; constexpr std::uint32_t MEMORY_SIZE = 0x20; constexpr std::uintptr_t HANDLE_ALIGNMENT = 8; +constexpr std::uint16_t DECODE_MODE_NORMAL = 0; +constexpr std::uint16_t DOWN_SCALE_NONE = 1; + constexpr std::uint16_t PIXEL_FORMAT_R8G8B8A8 = 0; constexpr std::uint16_t PIXEL_FORMAT_B8G8R8A8 = 1; @@ -45,11 +50,16 @@ static_assert(sizeof(Context) + HANDLE_ALIGNMENT - 1 <= MEMORY_SIZE); std::int32_t validateCreateParam(const JpegDecCreateParam* param) { if (!param) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; - if (param->size != sizeof(JpegDecCreateParam)) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; - if (param->attribute != ATTRIBUTE_NONE) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + if (param->this_size != sizeof(JpegDecCreateParam)) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; + if (param->attribute != ATTRIBUTE_CSC_ORDINARY && param->attribute != ATTRIBUTE_CSC_FULL) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; return 0; } +void requireNormalDecode(const char* function, std::uint16_t decodeMode, std::uint16_t downScale) { + if (decodeMode != DECODE_MODE_NORMAL) throw std::runtime_error(std::string(function) + ": decode mode " + std::to_string(decodeMode) + " is not implemented"); + if (downScale != DOWN_SCALE_NONE) throw std::runtime_error(std::string(function) + ": down scale " + std::to_string(downScale) + " is not implemented"); +} + Context* toContext(void* handle) { const auto address = reinterpret_cast(handle); if (address == 0 || address % HANDLE_ALIGNMENT != 0) return nullptr; @@ -58,11 +68,14 @@ Context* toContext(void* handle) { } void fillImageInfo(const Decoder::Jpeg::Header& header, JpegDecImageInfo* info) { + *info = {}; info->image_width = header.width; info->image_height = header.height; info->color_space = COLOR_SPACE_RGB; - info->bit_depth = 8; - info->image_flag = 0; + info->component_number = static_cast(header.channels); + info->suitable_csc_attribute = ATTRIBUTE_CSC_ORDINARY; + info->output_image_width = header.width; + info->output_image_height = header.height; } std::span toBytes(const void* data, std::uint32_t size) { @@ -95,6 +108,7 @@ int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param if (!param->jpeg_mem_addr || !param->image_mem_addr) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; if (param->jpeg_mem_size == 0 || param->image_mem_size == 0) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; if (param->pixel_format != PIXEL_FORMAT_R8G8B8A8 && param->pixel_format != PIXEL_FORMAT_B8G8R8A8) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + requireNormalDecode(__func__, param->decode_mode, param->down_scale); const std::span jpeg = toBytes(param->jpeg_mem_addr, param->jpeg_mem_size); const std::optional header = Decoder::Jpeg::ParseHeader(jpeg); @@ -109,6 +123,7 @@ int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param if (!image || image->width != header->width || image->height != header->height) return SCE_JPEG_DEC_ERROR_DECODE_ERROR; const bool swapRedBlue = param->pixel_format == PIXEL_FORMAT_B8G8R8A8; + const auto alpha = static_cast(param->alpha_value); auto* output = static_cast(param->image_mem_addr); for (std::uint32_t y = 0; y < image->height; ++y) { std::uint8_t* out = output + y * pitch; @@ -125,7 +140,7 @@ int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param pixel[2] = in[2]; } if (swapRedBlue) std::swap(pixel[0], pixel[2]); - pixel[3] = 0xFF; + pixel[3] = alpha; } } @@ -145,6 +160,7 @@ int32_t APS5_VABI sceJpegDecParseHeader(const JpegDecParseParam* param, JpegDecI if (!param) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; if (!param->jpeg_mem_addr || !image_info) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; if (param->jpeg_mem_size == 0) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; + requireNormalDecode(__func__, param->decode_mode, param->down_scale); const std::optional header = Decoder::Jpeg::ParseHeader(toBytes(param->jpeg_mem_addr, param->jpeg_mem_size)); if (!header) return SCE_JPEG_DEC_ERROR_INVALID_DATA; fillImageInfo(*header, image_info); diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index ba4bdf466c..adb79106c5 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -513,6 +513,8 @@ void APS5_VABI _ZN3sce4Json12MemAllocatorD2Ev(void* self) { *static_cast(self) = &JsonMemAllocatorVtable[2]; } +std::size_t _ZN3sce4Json6String4nposE = static_cast(-1); + void APS5_VABI _ZN3sce4Json6StringC1Ev(String* self) { Construct(*self, std::string()); } void APS5_VABI _ZN3sce4Json6StringC1EPKc(String* self, const char* text) { if (text == nullptr) throw std::invalid_argument("sce::Json::String: null text"); @@ -528,6 +530,25 @@ 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; } +void APS5_VABI _ZN3sce4Json6String5clearEv(String* self) { self->text->clear(); } +void APS5_VABI _ZN3sce4Json6String7reserveEm(String* self, std::size_t capacity) { self->text->reserve(capacity); } +String* APS5_VABI _ZN3sce4Json6String6appendERKS1_(String* self, const String* other) { + const std::string copy = *other->text; + self->text->append(copy); + return self; +} +String* APS5_VABI _ZN3sce4Json6String6appendEPKc(String* self, const char* text) { + if (text == nullptr) throw std::invalid_argument("sce::Json::String::append: null text"); + self->text->append(text); + return self; +} +String* APS5_VABI _ZNK3sce4Json6String6substrEmm(String* result, const String* self, std::size_t position, std::size_t length) { + if (position > self->text->size()) throw std::out_of_range("sce::Json::String::substr: position past the end"); + Construct(*result, self->text->substr(position, length)); + return result; +} +std::size_t APS5_VABI _ZNK3sce4Json6String4findEcm(const String* self, char character, std::size_t position) { return self->text->find(character, position); } +std::size_t APS5_VABI _ZNK3sce4Json6String5rfindEcm(const String* self, char character, std::size_t position) { return self->text->rfind(character, position); } 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); diff --git a/core/libs/prx/libSceMouse/Export.cpp b/core/libs/prx/libSceMouse/Export.cpp index f41c9eaf48..0fd4352a9f 100644 --- a/core/libs/prx/libSceMouse/Export.cpp +++ b/core/libs/prx/libSceMouse/Export.cpp @@ -19,7 +19,7 @@ int APS5_VABI sceMouseRead(std::int32_t handle, MouseData* data, std::int32_t nu return Mouse::Read(handle, data, num); } -void MousePublishInput_nid_postfix(const MouseInputEvent& event) { +void MousePublishInput_nid_no_patch(const MouseInputEvent& event) { Mouse::Publish(event); } @@ -28,7 +28,7 @@ int APS5_VABI sceMouseResetAbsolutePosition() { return 0; } -bool MouseIsOpen_nid_postfix() { +bool MouseIsOpen_nid_no_patch() { return Mouse::IsOpen(); } diff --git a/core/libs/prx/libSceMouse/include/MouseState.hpp b/core/libs/prx/libSceMouse/include/MouseState.hpp index 15c89831b6..5abc3c3e6a 100644 --- a/core/libs/prx/libSceMouse/include/MouseState.hpp +++ b/core/libs/prx/libSceMouse/include/MouseState.hpp @@ -24,7 +24,7 @@ int Read(std::int32_t handle, MouseData* data, std::int32_t num); void Publish(const MouseInputEvent& event); } -extern "C" void MousePublishInput_nid_postfix(const MouseInputEvent& event); -extern "C" bool MouseIsOpen_nid_postfix(); +extern "C" void MousePublishInput_nid_no_patch(const MouseInputEvent& event); +extern "C" bool MouseIsOpen_nid_no_patch(); #endif diff --git a/core/libs/prx/libSceMouse/tests/Mouse.cpp b/core/libs/prx/libSceMouse/tests/Mouse.cpp index e01f3f9b4f..2793ec0184 100644 --- a/core/libs/prx/libSceMouse/tests/Mouse.cpp +++ b/core/libs/prx/libSceMouse/tests/Mouse.cpp @@ -92,7 +92,7 @@ int main() { for (int i = 0; i < 70; ++i) { MouseInputEvent motion{}; motion.x = i; - MousePublishInput_nid_postfix(motion); + MousePublishInput_nid_no_patch(motion); } Require(sceMouseRead(MOUSE_HANDLE, data, 64) == 64); Require(data[0].x_axis == 6 && data[63].x_axis == 69); diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index e03f88e831..ac697a4120 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -249,6 +249,8 @@ struct Sock { int rcv_timeout_us = 0; int snd_timeout_us = 0; int reported_error = 0; + int snd_buffer = 0; + int rcv_buffer = 0; }; #ifdef _WIN32 @@ -595,6 +597,52 @@ bool SetSocketOptionForKernel(int descriptor, int level, int option, const void* const bool g_socketOptionSetterRegistered = (KernelSetSocketOptionSetter_nid_no_patch(&SetSocketOptionForKernel), true); +constexpr int GuestFcntlGetStatus = 3; +constexpr int GuestFcntlSetStatus = 4; +constexpr int GuestReadWrite = 2; +constexpr int GuestNonblocking = 4; +constexpr int GuestStatusFlagsMask = 7; + +bool ControlSocketStatusForKernel(int descriptor, int command, int flags, int* result, int* error) { + bool nonblock = false; + { + std::lock_guard lk(g_mutex); + const auto it = g_socks.find(descriptor); + if (it == g_socks.end()) return false; + nonblock = it->second.nonblock; + } + *result = 0; + *error = 0; + if (command == GuestFcntlGetStatus) { + *result = GuestReadWrite | (nonblock ? GuestNonblocking : 0); + return true; + } + if (command != GuestFcntlSetStatus) { + *error = NET_EINVAL; + return true; + } + if ((flags & ~GuestStatusFlagsMask) != 0) { + *error = NET_EOPNOTSUPP; + return true; + } + const int enabled = (flags & GuestNonblocking) != 0 ? 1 : 0; + if (sceNetSetsockopt(descriptor, NET_SOL_SOCKET, NET_SO_NBIO, &enabled, sizeof(enabled)) != 0) *error = *errno_slot(); + return true; +} + +const bool g_socketStatusControlRegistered = (KernelSetSocketStatusControl_nid_no_patch(&ControlSocketStatusForKernel), true); + +bool FindSocketNativeForKernel(int descriptor, std::shared_ptr* owner, std::uintptr_t* native) { + std::lock_guard lk(g_mutex); + const auto it = g_socks.find(descriptor); + if (it == g_socks.end() || !it->second.native) return false; + *native = static_cast(it->second.native->value); + *owner = it->second.native; + return true; +} + +const bool g_socketNativeLookupRegistered = (KernelSetSocketNativeLookup_nid_no_patch(&FindSocketNativeForKernel), true); + int APS5_VABI sceNetInit_nid_postfix(void) { std::lock_guard lk(g_mutex); g_net_inited = true; @@ -1038,6 +1086,8 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval if (optname == NET_SO_NBIO) it->second.nonblock = value != 0; if (optname == NET_SO_RCVTIMEO) it->second.rcv_timeout_us = value; if (optname == NET_SO_SNDTIMEO) it->second.snd_timeout_us = value; + if (optname == NET_SO_SNDBUF) it->second.snd_buffer = value; + if (optname == NET_SO_RCVBUF) it->second.rcv_buffer = value; } return 0; } @@ -1074,6 +1124,16 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint value = pending == 0 ? 0 : net_error(pending); break; } + case NET_SO_SNDBUF: + case NET_SO_RCVBUF: { + const bool send = optname == NET_SO_SNDBUF; + value = send ? socket.snd_buffer : socket.rcv_buffer; + if (value != 0) break; + NativeLength size = sizeof(value); + if (::getsockopt(socket.native->value, SOL_SOCKET, send ? SO_SNDBUF : SO_RCVBUF, reinterpret_cast(&value), &size) != 0) + return fail(native_error()); + break; + } default: return fail(NET_EOPNOTSUPP); } std::memcpy(optval, &value, sizeof(value)); diff --git a/core/libs/prx/libSceNetCtl/CMakeLists.txt b/core/libs/prx/libSceNetCtl/CMakeLists.txt index f221f21932..0083a712af 100644 --- a/core/libs/prx/libSceNetCtl/CMakeLists.txt +++ b/core/libs/prx/libSceNetCtl/CMakeLists.txt @@ -1,3 +1,17 @@ add_library(libSceNetCtl SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceNetCtl PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceNetCtl) + +if(BUILD_TESTING) + add_test_executable(guest_net_ctl_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetCtl.cpp) + target_include_directories(guest_net_ctl_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_ctl_tests PRIVATE libSceNetCtl libc) + configure_windows_unwind(guest_net_ctl_tests) + add_test(NAME guest_net_ctl COMMAND guest_net_ctl_tests) + set_tests_properties(guest_net_ctl PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77) + if(WIN32) + target_link_libraries(guest_net_ctl_tests PRIVATE ws2_32) + set_tests_properties(guest_net_ctl PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index c6645e18b1..27bdd35a7c 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -4,32 +4,58 @@ #include #include #else +#include #include #include +#include #include +#include +#include +#include #ifdef __linux__ #include #endif #endif #include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include +#include #include +#include #include #include -#ifdef _WIN32 #include -#endif // Network-control state reflects host interface addressing, not PSN sign-in status. static constexpr int SCE_NET_CTL_ERROR_CALLBACK_MAX = static_cast(0x80412103); static constexpr int SCE_NET_CTL_ERROR_INVALID_ID = static_cast(0x80412105); +static constexpr int SCE_NET_CTL_ERROR_INVALID_CODE = static_cast(0x80412106); static constexpr int SCE_NET_CTL_ERROR_INVALID_ADDR = static_cast(0x80412107); static constexpr int SCE_NET_CTL_ERROR_NOT_CONNECTED = static_cast(0x80412108); static constexpr int NET_CTL_STATE_DISCONNECTED = 0; static constexpr int NET_CTL_STATE_IPOBTAINED = 3; +static constexpr int NET_CTL_INFO_DEVICE = 1; +static constexpr int NET_CTL_INFO_ETHER_ADDR = 2; +static constexpr int NET_CTL_INFO_MTU = 3; +static constexpr int NET_CTL_INFO_LINK = 4; +static constexpr int NET_CTL_INFO_IP_CONFIG = 11; +static constexpr int NET_CTL_INFO_IP_ADDRESS = 14; +static constexpr int NET_CTL_INFO_NETMASK = 15; +static constexpr int NET_CTL_INFO_DEFAULT_ROUTE = 16; +static constexpr int NET_CTL_INFO_PRIMARY_DNS = 17; +static constexpr int NET_CTL_INFO_SECONDARY_DNS = 18; +static constexpr int NET_CTL_INFO_HTTP_PROXY_CONFIG = 19; +static constexpr int NET_CTL_INFO_HTTP_PROXY_PORT = 21; +static constexpr std::uint32_t NET_CTL_DEVICE_WIRED = 0; +static constexpr std::uint32_t NET_CTL_LINK_CONNECTED = 1; +static constexpr std::uint32_t NET_CTL_IP_DHCP = 0; +static constexpr std::uint32_t NET_CTL_HTTP_PROXY_OFF = 0; +static constexpr std::uint8_t NET_CTL_ETHER_ADDR[6] = {0x02, 0x50, 0x53, 0x35, 0x00, 0x01}; +static constexpr std::size_t NET_CTL_IPV4_ADDR_STR_LEN = 16; static constexpr int MAX_CALLBACKS = 8; static std::mutex g_callbackLock; @@ -105,6 +131,157 @@ static bool HostHasAddress(int family) { #endif } +struct HostIpv4Interface { + in_addr address{}; + in_addr netmask{}; + in_addr gateway{}; + std::uint32_t mtu = 0; + std::vector dns; +}; + +static bool UsableIpv4(in_addr address) { + const auto host = ntohl(address.s_addr); + return host != 0 && (host >> 24) != 127; +} + +#ifdef _WIN32 +static const sockaddr_in* Ipv4Of(const SOCKET_ADDRESS& address) { + if (!address.lpSockaddr || address.lpSockaddr->sa_family != AF_INET) return nullptr; + return reinterpret_cast(address.lpSockaddr); +} + +static std::uint32_t PrefixMask(unsigned length) { + if (length == 0) return 0; + return htonl(length >= 32 ? 0xFFFFFFFFu : 0xFFFFFFFFu << (32 - length)); +} +#else +struct HostDefaultRoute { + std::string name; + in_addr gateway{}; + unsigned long metric = 0; +}; + +static std::vector HostDefaultRoutes() { + std::ifstream table("/proc/net/route"); + if (!table) throw std::runtime_error("sceNetCtlGetInfo: the host routing table is unavailable"); + std::vector routes; + std::string line; + std::getline(table, line); + while (std::getline(table, line)) { + std::istringstream fields(line); + HostDefaultRoute route; + unsigned long destination = 0; + unsigned long gateway = 0; + unsigned long flags = 0; + unsigned long references = 0; + unsigned long uses = 0; + unsigned long mask = 0; + if (!(fields >> route.name >> std::hex >> destination >> gateway >> flags >> std::dec >> references >> uses >> route.metric >> std::hex >> mask)) continue; + if (destination != 0 || mask != 0 || !(flags & RTF_UP) || (flags & RTF_REJECT)) continue; + route.gateway.s_addr = static_cast(gateway); + routes.push_back(route); + } + return routes; +} + +static std::uint32_t HostInterfaceMtu(const char* name) { + const int descriptor = socket(AF_INET, SOCK_DGRAM, 0); + if (descriptor < 0) throw std::runtime_error("sceNetCtlGetInfo: no socket to read the host interface MTU"); + ifreq request{}; + std::strncpy(request.ifr_name, name, IFNAMSIZ - 1); + const int result = ioctl(descriptor, SIOCGIFMTU, &request); + close(descriptor); + if (result != 0) throw std::runtime_error(std::string("sceNetCtlGetInfo: no MTU for host interface ") + name); + return static_cast(request.ifr_mtu); +} + +static std::vector HostDnsServers() { + std::ifstream config("/etc/resolv.conf"); + std::vector servers; + std::string line; + while (std::getline(config, line)) { + std::istringstream fields(line); + std::string keyword; + std::string value; + in_addr server{}; + if (fields >> keyword >> value && keyword == "nameserver" && inet_pton(AF_INET, value.c_str(), &server) == 1) servers.push_back(server); + } + return servers; +} +#endif + +static bool FindHostIpv4Interface(HostIpv4Interface& found) { +#ifdef _WIN32 + ULONG size = 0; + const ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_INCLUDE_GATEWAYS; + if (GetAdaptersAddresses(AF_INET, flags, nullptr, nullptr, &size) != ERROR_BUFFER_OVERFLOW) return false; + std::vector storage(size); + auto* adapters = reinterpret_cast(storage.data()); + if (GetAdaptersAddresses(AF_INET, flags, nullptr, adapters, &size) != NO_ERROR) return false; + bool chosen = false; + bool chosenHasGateway = false; + ULONG chosenMetric = 0; + for (auto* adapter = adapters; adapter; adapter = adapter->Next) { + if (adapter->OperStatus != IfOperStatusUp || adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK) continue; + auto* unicast = adapter->FirstUnicastAddress; + while (unicast && !(Ipv4Of(unicast->Address) && UsableIpv4(Ipv4Of(unicast->Address)->sin_addr))) unicast = unicast->Next; + if (!unicast) continue; + auto* gateway = adapter->FirstGatewayAddress; + while (gateway && !Ipv4Of(gateway->Address)) gateway = gateway->Next; + if (chosen && !(gateway && (!chosenHasGateway || adapter->Ipv4Metric < chosenMetric))) continue; + HostIpv4Interface candidate; + candidate.address = Ipv4Of(unicast->Address)->sin_addr; + candidate.netmask.s_addr = PrefixMask(unicast->OnLinkPrefixLength); + if (gateway) candidate.gateway = Ipv4Of(gateway->Address)->sin_addr; + candidate.mtu = adapter->Mtu; + for (auto* server = adapter->FirstDnsServerAddress; server; server = server->Next) { + if (Ipv4Of(server->Address)) candidate.dns.push_back(Ipv4Of(server->Address)->sin_addr); + } + found = std::move(candidate); + chosen = true; + chosenHasGateway = gateway != nullptr; + chosenMetric = adapter->Ipv4Metric; + } + return chosen; +#else + ifaddrs* list = nullptr; + if (getifaddrs(&list) != 0) return false; + const std::unique_ptr interfaces(list, freeifaddrs); + const auto routes = HostDefaultRoutes(); + const ifaddrs* chosen = nullptr; + const HostDefaultRoute* chosenRoute = nullptr; + for (auto* item = list; item; item = item->ifa_next) { + if (!item->ifa_addr || item->ifa_addr->sa_family != AF_INET || !(item->ifa_flags & IFF_UP) || (item->ifa_flags & IFF_LOOPBACK)) continue; + if (!UsableIpv4(reinterpret_cast(item->ifa_addr)->sin_addr)) continue; + const HostDefaultRoute* route = nullptr; + for (const auto& entry : routes) { + if (entry.name == item->ifa_name && (!route || entry.metric < route->metric)) route = &entry; + } + if (chosen && !(route && (!chosenRoute || route->metric < chosenRoute->metric))) continue; + chosen = item; + chosenRoute = route; + } + if (!chosen) return false; + found.address = reinterpret_cast(chosen->ifa_addr)->sin_addr; + if (chosen->ifa_netmask) found.netmask = reinterpret_cast(chosen->ifa_netmask)->sin_addr; + if (chosenRoute) found.gateway = chosenRoute->gateway; + found.mtu = HostInterfaceMtu(chosen->ifa_name); + found.dns = HostDnsServers(); + return true; +#endif +} + +static void WriteInfoWord(NetCtlInfo* info, std::uint32_t value) { + std::memcpy(info->opaque, &value, sizeof(value)); +} + +static void WriteInfoAddress(NetCtlInfo* info, in_addr address) { + const auto* bytes = reinterpret_cast(&address.s_addr); + char text[NET_CTL_IPV4_ADDR_STR_LEN] = {}; + std::snprintf(text, sizeof(text), "%u.%u.%u.%u", static_cast(bytes[0]), static_cast(bytes[1]), static_cast(bytes[2]), static_cast(bytes[3])); + std::memcpy(info->opaque, text, sizeof(text)); +} + extern "C" std::uint64_t NetCtlReadByteCounter_nid_no_patch(const char* path) { std::ifstream file(path); std::uint64_t value = 0; @@ -156,9 +333,25 @@ int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat* stat) { } int APS5_VABI sceNetCtlGetInfo(int code, NetCtlInfo* info) { - (void)code; - (void)info; - return SCE_NET_CTL_ERROR_NOT_CONNECTED; + if (!info) return SCE_NET_CTL_ERROR_INVALID_ADDR; + if (code < NET_CTL_INFO_DEVICE || code > NET_CTL_INFO_HTTP_PROXY_PORT) return SCE_NET_CTL_ERROR_INVALID_CODE; + HostIpv4Interface host; + if (!FindHostIpv4Interface(host)) return SCE_NET_CTL_ERROR_NOT_CONNECTED; + switch (code) { + case NET_CTL_INFO_DEVICE: WriteInfoWord(info, NET_CTL_DEVICE_WIRED); break; + case NET_CTL_INFO_ETHER_ADDR: std::memcpy(info->opaque, NET_CTL_ETHER_ADDR, sizeof(NET_CTL_ETHER_ADDR)); break; + case NET_CTL_INFO_MTU: WriteInfoWord(info, host.mtu); break; + case NET_CTL_INFO_LINK: WriteInfoWord(info, NET_CTL_LINK_CONNECTED); break; + case NET_CTL_INFO_IP_CONFIG: WriteInfoWord(info, NET_CTL_IP_DHCP); break; + case NET_CTL_INFO_IP_ADDRESS: WriteInfoAddress(info, host.address); break; + case NET_CTL_INFO_NETMASK: WriteInfoAddress(info, host.netmask); break; + case NET_CTL_INFO_DEFAULT_ROUTE: WriteInfoAddress(info, host.gateway); break; + case NET_CTL_INFO_PRIMARY_DNS: WriteInfoAddress(info, host.dns.size() > 0 ? host.dns[0] : in_addr{}); break; + case NET_CTL_INFO_SECONDARY_DNS: WriteInfoAddress(info, host.dns.size() > 1 ? host.dns[1] : in_addr{}); break; + case NET_CTL_INFO_HTTP_PROXY_CONFIG: WriteInfoWord(info, NET_CTL_HTTP_PROXY_OFF); break; + default: throw std::runtime_error("sceNetCtlGetInfo: info code " + std::to_string(code) + " is not implemented"); + } + return 0; } int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo* nat_info) { diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index ca1b035a39..19e7cd543d 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -64,11 +64,13 @@ static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_EXIT_LOOP = 0 static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_PITCH = 0x40010005; static constexpr std::uint32_t SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP = 0x40020000; static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER = 0x40001300; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX = 0x40001d00; static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 = 0x40001f00; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MAX_MODULES = 24; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MAX_PORTS = 16; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_FILTER = 0x13; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_USER_FX = 0x1d; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2 = 0x1f; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE = 1; @@ -502,6 +504,15 @@ struct Ngs2CustomSubmixerVoiceSetupParam { }; static_assert(sizeof(Ngs2CustomSubmixerVoiceSetupParam) == 24); +struct Ngs2CustomVoiceUserFxParam { + Ngs2VoiceParamHeader header; + Ngs2UserFxProcessHandler handler; + std::uintptr_t user_data0; + std::uintptr_t user_data1; + std::uintptr_t user_data2; +}; +static_assert(sizeof(Ngs2CustomVoiceUserFxParam) == 40); + struct Ngs2CustomVoiceUserFx2Param { Ngs2VoiceParamHeader header; const void* data; diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp index af850cd2b8..5f163c25ee 100644 --- a/core/libs/prx/libSceNgs2.native/src/Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -24,6 +24,9 @@ void Ngs2CheckCustomRack(const Ngs2CustomRackOption& option) { } else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_FILTER) { if (info.option != nullptr || info.state_size != 0 || info.state_offset != 0) APS5_INVALID_ARG_EX; if (info.source_buffer_id != 0 || info.extra_buffer_id != 0 || info.dest_buffer_id != 0) APS5_INVALID_ARG_EX; + } else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX) { + if (info.source_buffer_id != 0 || info.dest_buffer_id != 0) APS5_INVALID_ARG_EX; + if (info.option != nullptr && info.option->size < sizeof(Ngs2CustomModuleOption)) APS5_INVALID_ARG_EX; } else { throw std::runtime_error("NGS2: custom module id " + Ngs2Hex(info.module_id) + " is not implemented"); } @@ -43,23 +46,32 @@ void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option) { rack.customModuleIds.resize(option.num_modules); rack.userFx.resize(option.num_modules); rack.userFxCommon.resize(option.num_modules); - for (auto& voice : rack.voices) voice.userFx.resize(option.num_modules); + for (auto& voice : rack.voices) { + voice.userFx.resize(option.num_modules); + voice.customUserFx.resize(option.num_modules); + } for (std::uint32_t m = 0; m < option.num_modules; m++) { rack.customModuleIds[m] = option.module[m].module_id; if (option.module[m].module_id == SCE_NGS2_CUSTOM_MODULE_ID_FILTER) continue; - const auto& module = *reinterpret_cast(option.module[m].option); - rack.userFx[m] = module; - rack.userFxCommon[m].resize(module.common_size); - for (std::uint32_t i = 0; i < rack.voices.size(); i++) { - auto& fx = rack.voices[i].userFx[m]; - fx.param.resize(module.param_size); - fx.work.resize(module.work_size); - fx.state.resize(option.module[m].state_size); - fx.flags = USER_FX2_FLAG_FIRST_PROCESS; - if (module.setup_handler == nullptr) continue; - auto context = UserFxContext(rack, m, i); - const int result = module.setup_handler(&context); - if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx2 setup handler failed with " + std::to_string(result)); + if (option.module[m].module_id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) { + const auto& module = *reinterpret_cast(option.module[m].option); + rack.userFx[m] = module; + rack.userFxCommon[m].resize(module.common_size); + for (std::uint32_t i = 0; i < rack.voices.size(); i++) { + auto& fx = rack.voices[i].userFx[m]; + fx.param.resize(module.param_size); + fx.work.resize(module.work_size); + fx.state.resize(option.module[m].state_size); + fx.flags = USER_FX2_FLAG_FIRST_PROCESS; + if (module.setup_handler == nullptr) continue; + auto context = UserFxContext(rack, m, i); + const int result = module.setup_handler(&context); + if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx2 setup handler failed with " + std::to_string(result)); + } + } else if (option.module[m].module_id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX) { + for (std::uint32_t i = 0; i < rack.voices.size(); i++) { + rack.voices[i].customUserFx[m].flags = 1; + } } } } @@ -93,6 +105,17 @@ static void SetUserFxParam(Ngs2Voice& voice, std::uint32_t index, const Ngs2Cust fx.flags |= USER_FX2_FLAG_PARAM_CHANGED; } +static void SetCustomUserFxParam(Ngs2Voice& voice, std::uint32_t index, const Ngs2CustomVoiceUserFxParam& param) { + if (index >= voice.customUserFx.size()) APS5_INVALID_ARG_EX; + if (index >= voice.rack->customModuleIds.size() || voice.rack->customModuleIds[index] != SCE_NGS2_CUSTOM_MODULE_ID_USER_FX) { + APS5_INVALID_ARG_EX; + } + auto& fx = voice.customUserFx[index]; + fx.handler = param.handler; + fx.data = {param.user_data0, param.user_data1, param.user_data2}; + fx.flags = 1; +} + void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { const auto rackId = voice.rack->rackId; if (param.id == SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP) { @@ -130,24 +153,41 @@ void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { SetUserFxParam(voice, param.id & 0x1f, ParamAs(param)); return; } + if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX) { + if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); + SetCustomUserFxParam(voice, param.id & 0x1f, ParamAs(param)); + return; + } if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); throw std::runtime_error("NGS2: voice param " + Ngs2Hex(param.id) + " is not implemented"); } void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate) { - if (voice.userFx.empty() || std::none_of(voice.rack->customModuleIds.begin(), voice.rack->customModuleIds.end(), - [](std::uint32_t id) { return id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2; })) return; + if (voice.userFx.empty() && voice.customUserFx.empty()) return; std::vector channels(voice.channels); for (std::uint32_t c = 0; c < voice.channels; c++) channels[c] = voice.samples.data() + static_cast(c) * grain; - for (std::size_t m = 0; m < voice.userFx.size(); m++) { - if (voice.rack->customModuleIds[m] != SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) continue; - auto& fx = voice.userFx[m]; - const auto& module = voice.rack->userFx[m]; - Ngs2UserFx2ProcessContext context{channels.data(), voice.rack->userFxCommon[m].data(), fx.param.data(), fx.work.data(), fx.state.data(), - module.user_data, fx.flags, voice.channels, voice.channels, grain, sampleRate, 0, {}}; - const int result = module.process_handler(&context); - if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx2 process handler failed with " + std::to_string(result)); - fx.flags = 0; + for (std::size_t m = 0; m < voice.rack->customModuleIds.size(); m++) { + if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) { + if (!voice.hasSamples) continue; + auto& fx = voice.userFx[m]; + const auto& module = voice.rack->userFx[m]; + if (module.process_handler == nullptr) continue; + Ngs2UserFx2ProcessContext context{channels.data(), voice.rack->userFxCommon[m].data(), fx.param.data(), fx.work.data(), fx.state.data(), + module.user_data, fx.flags, voice.channels, voice.channels, grain, sampleRate, 0, {}}; + const int result = module.process_handler(&context); + if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx2 process handler failed with " + std::to_string(result)); + fx.flags = 0; + } else if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX) { + if (m < voice.customUserFx.size() && voice.customUserFx[m].handler != nullptr) { + auto& fx = voice.customUserFx[m]; + Ngs2UserFxProcessContext context{channels.data(), fx.data[0], fx.data[1], fx.data[2], + fx.flags, voice.channels, grain, sampleRate}; + const int result = fx.handler(&context); + if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx process handler failed with " + std::to_string(result)); + fx.flags = 0; + voice.hasSamples = std::any_of(voice.samples.begin(), voice.samples.end(), [](float sample) { return sample != 0.0f; }); + } + } } } diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index 8dbe429dc9..b219666620 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -101,6 +101,12 @@ struct Ngs2UserFx2 { std::uint32_t flags = 0; }; +struct Ngs2VoiceCustomUserFx { + Ngs2UserFxProcessHandler handler = nullptr; + std::array data{}; + std::uint32_t flags = 0; +}; + struct Ngs2Voice { Ngs2Rack* rack = nullptr; Ngs2PlayState state = Ngs2PlayState::Empty; @@ -128,6 +134,7 @@ struct Ngs2Voice { float fbwLevel = 1.0f; float lfeLevel = 1.0f; std::vector userFx; + std::vector customUserFx; Ngs2UserFxProcessHandler userFxHandler = nullptr; std::array userFxData{}; std::uint32_t userFxFlags = 0; diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index bf14563e82..921df42551 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -249,7 +249,7 @@ static void RenderVoice(Ngs2Voice& voice, const std::vector& voices, } if (voice.rack->rackId == SCE_NGS2_RACK_ID_REVERB && voice.reverb) voice.hasSamples = Ngs2ProcessReverb(voice, grain, systemRate); Ngs2ProcessLegacyUserFx(voice, grain, systemRate); - if (voice.hasSamples) Ngs2ProcessUserFx(voice, grain, systemRate); + Ngs2ProcessUserFx(voice, grain, systemRate); } voice.rendered = true; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp index 3b21b794c2..ef5b874c41 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -346,10 +346,123 @@ static void TestCustomFilter() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } + +struct LegacyUserFxState { + std::uint32_t calls = 0; + std::uint32_t flags = 0; + std::int32_t result = SCE_NGS2_OK; + float val0 = 0.25f; + float val1 = 0.5f; +}; + +static std::int32_t APS5_VABI ProcessLegacy(Ngs2UserFxProcessContext* context) { + auto& state = *reinterpret_cast(context->user_data0); + state.calls++; + state.flags = context->flags; + if (state.result != SCE_NGS2_OK) return state.result; + for (std::uint32_t i = 0; i < context->num_grain_samples; i++) { + context->channel_data[0][i] = state.val0; + context->channel_data[1][i] = state.val1; + } + return SCE_NGS2_OK; +} + +static Ngs2CustomSubmixerRackOption LegacyRackOption(const Ngs2CustomModuleOption* option) { + Ngs2CustomSubmixerRackOption rack{}; + auto& common = rack.custom_rack_option.rack_option; + common.size = sizeof(rack); + common.max_grain_samples = 512; + common.max_voices = 2; + common.max_matrices = 1; + common.max_ports = 1; + rack.custom_rack_option.num_buffers = 1; + rack.custom_rack_option.num_modules = 1; + rack.custom_rack_option.module[0].option = option; + rack.custom_rack_option.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_USER_FX; + rack.custom_rack_option.module[0].state_size = 16; + rack.max_channels = 2; + rack.max_inputs = 1; + return rack; +} + +static void TestCustomLegacyUserFx() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 2); + const Ngs2CustomModuleOption header{sizeof(Ngs2CustomModuleOption)}; + const auto rack = CustomRack(system, LegacyRackOption(&header)); + const auto custom = Voice(rack); + Control(custom, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 2, 0, 0}); + Patch(custom, master); + Event(custom, SCE_NGS2_VOICE_EVENT_PLAY); + + LegacyUserFxState state{}; + Control(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 0, + Ngs2CustomVoiceUserFxParam{{}, ProcessLegacy, reinterpret_cast(&state), 123, 456}); + + auto out = Render(system); + Require(state.calls == 1 && state.flags == 1); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.25f && out[i * 2 + 1] == 0.5f); + + out = Render(system); + Require(state.calls == 2 && state.flags == 0); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.25f && out[i * 2 + 1] == 0.5f); + + struct BadSizeParam { + Ngs2VoiceParamHeader header; + std::uint8_t dummy[10]; + }; + Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 0, BadSizeParam{})); + Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 2, Ngs2CustomVoiceUserFxParam{{}, ProcessLegacy, 0, 0, 0})); + + Ngs2CustomModuleOption badSize{2}; + Require(CreateThrows(LegacyRackOption(&badSize))); + + struct ExtendedOption { + std::uint32_t size = 20; + std::uint32_t reserved = 0; + std::uint32_t channels = 4; + float param0 = 1000.0f; + float param1 = 0.0f; + } extendedOption; + const auto rackExtended = CustomRack(system, LegacyRackOption(reinterpret_cast(&extendedOption))); + Require(sceNgs2RackDestroy(rackExtended, nullptr) == SCE_NGS2_OK); + + state.result = -1; + bool failed = false; + try { + Render(system); + } catch (const std::runtime_error&) { + failed = true; + } + Require(failed); + state.result = SCE_NGS2_OK; + + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + + const auto rackNullOpt = CustomRack(system, LegacyRackOption(nullptr)); + const auto voiceNullOpt = Voice(rackNullOpt); + Control(voiceNullOpt, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 2, 0, 0}); + Patch(voiceNullOpt, master); + Event(voiceNullOpt, SCE_NGS2_VOICE_EVENT_PLAY); + + LegacyUserFxState state2{}; + state2.val0 = 0.125f; + state2.val1 = 0.75f; + Control(voiceNullOpt, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX | 0, + Ngs2CustomVoiceUserFxParam{{}, ProcessLegacy, reinterpret_cast(&state2), 123, 456}); + out = Render(system); + Require(state2.calls == 1 && state2.flags == 1); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.125f && out[i * 2 + 1] == 0.75f); + + Require(sceNgs2RackDestroy(rackNullOpt, nullptr) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + int main() { TestUserFxChain(); TestRackOptions(); TestCustomSampler(); TestCustomFilter(); + TestCustomLegacyUserFx(); return 0; } diff --git a/core/libs/prx/libSceNpCommerce/CMakeLists.txt b/core/libs/prx/libSceNpCommerce/CMakeLists.txt new file mode 100644 index 0000000000..ec34bf1230 --- /dev/null +++ b/core/libs/prx/libSceNpCommerce/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceNpCommerce SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpCommerce PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpCommerce) + +if(BUILD_TESTING) + add_test_executable(guest_np_commerce_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpCommerceDialog.cpp) + target_include_directories(guest_np_commerce_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_commerce_dialog_tests PRIVATE libSceNpCommerce libc) + configure_windows_unwind(guest_np_commerce_dialog_tests) + add_test(NAME guest_np_commerce_dialog COMMAND guest_np_commerce_dialog_tests) + if(WIN32) + set_tests_properties(guest_np_commerce_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpCommerce/Export.cpp b/core/libs/prx/libSceNpCommerce/Export.cpp index 525178f39e..39fecd0fc0 100644 --- a/core/libs/prx/libSceNpCommerce/Export.cpp +++ b/core/libs/prx/libSceNpCommerce/Export.cpp @@ -80,8 +80,8 @@ int APS5_VABI sceNpCommerceSetPsStoreIconLayout(int layout) { } int APS5_VABI sceNpCommerceDialogOpen2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; } } diff --git a/core/libs/prx/libScePad/CMakeLists.txt b/core/libs/prx/libScePad/CMakeLists.txt index 16919e233d..b979e5344c 100644 --- a/core/libs/prx/libScePad/CMakeLists.txt +++ b/core/libs/prx/libScePad/CMakeLists.txt @@ -17,3 +17,15 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_pad_vibration_mode_tests ${LIBS_INCLUDE_DIR}/tests/GuestPadVibrationMode.cpp) + target_include_directories(guest_pad_vibration_mode_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pad_vibration_mode_tests PRIVATE libScePad libc) + configure_windows_unwind(guest_pad_vibration_mode_tests) + add_test(NAME guest_pad_vibration_mode COMMAND guest_pad_vibration_mode_tests) + if(WIN32) + set_tests_properties(guest_pad_vibration_mode PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index bc8838e917..c65a5df868 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -251,8 +251,10 @@ int APS5_VABI scePadSetVibration(int handle, const PadVibrationParam* param) { } int APS5_VABI scePadSetVibrationMode(int handle, int mode) { + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + if (mode <= 0) return PAD_ERROR_INVALID_ARG; if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; - if (mode < 0 || mode > 3) return PAD_ERROR_INVALID_ARG; + if (mode > 3 && mode != 5) return PAD_ERROR_INVALID_ARG; Pad::SetVibrationMode(mode); return PAD_OK; } diff --git a/core/libs/prx/libScePad/include/Pad.hpp b/core/libs/prx/libScePad/include/Pad.hpp index b24a7a8f3c..8a4a7406a5 100644 --- a/core/libs/prx/libScePad/include/Pad.hpp +++ b/core/libs/prx/libScePad/include/Pad.hpp @@ -6,6 +6,7 @@ constexpr int PAD_OK = 0; constexpr int PAD_ERROR_INVALID_ARG = -2137915391; constexpr int PAD_ERROR_INVALID_HANDLE = -2137915389; +constexpr int PAD_ERROR_NOT_INITIALIZED = static_cast(0x80920005u); constexpr int PAD_PORT_TYPE_STANDARD = 0; constexpr int PAD_PORT_TYPE_SPECIAL = 2; diff --git a/core/libs/prx/libScePad/include/PadState.hpp b/core/libs/prx/libScePad/include/PadState.hpp index 9d267a0716..c8c7d44e46 100644 --- a/core/libs/prx/libScePad/include/PadState.hpp +++ b/core/libs/prx/libScePad/include/PadState.hpp @@ -52,6 +52,7 @@ struct PadOutputState { namespace Pad { void Initialize(); +bool IsInitialized(); PadData ReadState(); int Read(PadData* data, int num); void SetVibration(std::uint8_t large, std::uint8_t small); diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index 1288a0fa8b..a915e25c21 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -112,6 +112,11 @@ void Pad::Initialize() { initialized = true; } +bool Pad::IsInitialized() { + std::lock_guard lock(stateMutex); + return initialized; +} + namespace { PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uint64_t now, const Pad::InputCommandFile::Input& injected) { PadData data{}; diff --git a/core/libs/prx/libScePlayGo/Export.cpp b/core/libs/prx/libScePlayGo/Export.cpp index 64420b32d3..38d6a3621a 100644 --- a/core/libs/prx/libScePlayGo/Export.cpp +++ b/core/libs/prx/libScePlayGo/Export.cpp @@ -19,6 +19,7 @@ static constexpr int8_t PLAYGO_LOCUS_NOT_DOWNLOADED = 0; static constexpr int8_t PLAYGO_LOCUS_LOCAL_SLOW = 2; static constexpr int8_t PLAYGO_LOCUS_LOCAL_FAST = 3; static constexpr uint64_t PLAYGO_LANGUAGE_MASK_ALL = ~0ull; +static constexpr uint64_t PLAYGO_OPTIONAL_CHUNK_MASK_ALL = ~0ull; static constexpr int32_t PLAYGO_INSTALL_SPEED_FULL = 2; static int32_t g_installSpeed = PLAYGO_INSTALL_SPEED_FULL; @@ -87,7 +88,9 @@ static const ChunkTable& Chunks() { static bool IsKnownChunk(uint16_t id) { const auto& chunks = Chunks(); - return chunks.defined ? chunks.ids.contains(id) : chunks.scenario || id == 0; + if (chunks.defined) return chunks.ids.contains(id); + if (chunks.scenario || id == 0) return true; + throw std::runtime_error("scePlayGo: sce_sys/playgo-chunk.dat and sce_sys/playgo-scenario.json are missing, so chunk ID " + std::to_string(id) + " cannot be checked"); } static int ValidateChunks(const uint16_t* chunk_ids, uint32_t number_of_entries) { @@ -160,7 +163,7 @@ int APS5_VABI scePlayGoGetOptionalChunk(int handle, int32_t type, PlayGoOptional if (handle != PLAYGO_HANDLE) return SCE_PLAYGO_ERROR_BAD_HANDLE; (void)type; if (!option) return SCE_PLAYGO_ERROR_BAD_POINTER; - option->bitmask = 0; + option->bitmask = PLAYGO_OPTIONAL_CHUNK_MASK_ALL; return 0; } @@ -177,7 +180,7 @@ int APS5_VABI scePlayGoGetSupportedOptionalChunk(int handle, int32_t type, PlayG if (handle != PLAYGO_HANDLE) return SCE_PLAYGO_ERROR_BAD_HANDLE; (void)type; if (!option) return SCE_PLAYGO_ERROR_BAD_POINTER; - option->bitmask = 0; + option->bitmask = PLAYGO_OPTIONAL_CHUNK_MASK_ALL; return 0; } diff --git a/core/libs/prx/libScePsml_debug/Export.cpp b/core/libs/prx/libScePsml_debug/Export.cpp index bec0b00b8e..758ceaffcf 100644 --- a/core/libs/prx/libScePsml_debug/Export.cpp +++ b/core/libs/prx/libScePsml_debug/Export.cpp @@ -155,15 +155,13 @@ int APS5_VABI scePsmlMfsr2Init() { return 0; } -APS5_EXPORT("xNZM9Y5lf-M", scePsmlUnknown01); -int APS5_VABI scePsmlUnknown01(void) { - NotImplemented_nid_no_patch("xNZM9Y5lf-M"); +int APS5_VABI scePsmlMfsrEnableSpecularAA() { + NotImplemented_nid_no_patch(__func__); return 0; } -APS5_EXPORT("lcZD9tJraAw", scePsmlUnknown02); -int APS5_VABI scePsmlUnknown02(void) { - NotImplemented_nid_no_patch("lcZD9tJraAw"); +int APS5_VABI scePsmlMfsrEnableQuality8K() { + NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 00529793b7..3a75d7875f 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -145,7 +145,7 @@ SaveDataParam load_param(const std::string& real_path) { std::error_code ec; const auto written = std::filesystem::last_write_time(real_path, ec); if (!ec) { - const auto system = std::filesystem::file_time_type::clock::to_sys(written); + const auto system = std::chrono::file_clock::to_sys(written); param.mtime = std::chrono::duration_cast(system.time_since_epoch()).count(); } } @@ -893,9 +893,8 @@ int APS5_VABI sceSaveDataDirNameSearch2(const SaveDataDirNameSearchCond* cond, S return rc; } -APS5_EXPORT("RvID8BVeg+g", sceSaveDataUnknown_RvID8BVeg); -int APS5_VABI sceSaveDataUnknown_RvID8BVeg() { - NotImplemented_nid_no_patch("RvID8BVeg+g"); +int APS5_VABI sceSaveDataPrepareForTransferring() { + NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceShare.native/Export.cpp b/core/libs/prx/libSceShare.native/Export.cpp index 2d8f2cf361..fb34206c21 100644 --- a/core/libs/prx/libSceShare.native/Export.cpp +++ b/core/libs/prx/libSceShare.native/Export.cpp @@ -3,86 +3,8 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -// Capture and sharing are not emulated; policy and parameter calls are accepted. - extern "C" { -int APS5_VABI sceShareCaptureScreenshot(const void* param, int32_t* req_id) { - (void)param; - (void)req_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceShareCaptureVideoClip(const void* param, int32_t* req_id) { - (void)param; - (void)req_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceShareFeaturePermit(uint32_t feature_flags) { - (void)feature_flags; - return 0; -} - -int APS5_VABI sceShareFeatureProhibit(uint32_t feature_flags) { - (void)feature_flags; - return 0; -} - -int APS5_VABI sceShareGetCurrentStatus(uint32_t feature_flag, ShareCurrentStatus* status) { - (void)feature_flag; - (void)status; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceShareInitialize(size_t heap_size, int thread_priority, uint64_t affinity_mask) { - (void)heap_size; - (void)thread_priority; - (void)affinity_mask; - return 0; -} - -int APS5_VABI sceShareOpenMenuForContent(const void* content_id) { - (void)content_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceShareRegisterContentEventCallback(void* callback, void* user_data) { - (void)callback; - (void)user_data; - return 0; -} - -int APS5_VABI sceShareSetCaptureSource(uint32_t feature_flags, const void* tap_point) { - (void)feature_flags; - (void)tap_point; - return 0; -} - -int APS5_VABI sceShareSetContentParam(const char* content_param) { - (void)content_param; - return 0; -} - -int APS5_VABI sceShareSetScreenshotOverlayImage(const char* file_path, int32_t margin_x, int32_t margin_y, int32_t origin) { - (void)file_path; - (void)margin_x; - (void)margin_y; - (void)origin; - return 0; -} - -int APS5_VABI sceShareTerminate(void) { - return 0; -} - -int APS5_VABI sceShareUnregisterContentEventCallback(void* callback) { - (void)callback; - return 0; -} +APS5_DUMMY_FUN } diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index 36af909cc3..d7ff11174c 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceSystemService SHARED EXCLUDE_FROM_ALL Export.cpp) +add_library(libSceSystemService SHARED EXCLUDE_FROM_ALL Export.cpp LoadExec.cpp) target_include_directories(libSceSystemService PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceSystemService PRIVATE libSceAgcDriver) configure_windows_unwind(libSceSystemService) @@ -15,6 +15,19 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_library(guest_loadexec_host SHARED ${LIBS_INCLUDE_DIR}/tests/GuestLoadExecHost.cpp) + target_include_directories(guest_loadexec_host PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_loadexec_host PRIVATE libSceSystemService libc libkernel) + set_target_properties(guest_loadexec_host PROPERTIES PREFIX "" SUFFIX ".prx" BUILD_RPATH "$ORIGIN") + configure_windows_unwind(guest_loadexec_host) + find_package(Python3 COMPONENTS Interpreter QUIET) + if(Python3_FOUND) + add_test(NAME loadexec_arguments COMMAND ${Python3_EXECUTABLE} + ${CMAKE_SOURCE_DIR}/core/relinker/relinker/tests/test_loadexec_arguments.py + $ $ + $ $ $ $ ${CMAKE_CXX_COMPILER}) + set_tests_properties(loadexec_arguments PROPERTIES TIMEOUT 30) + endif() add_test_executable(guest_system_exit_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemExit.cpp) target_include_directories(guest_system_exit_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_system_exit_tests PRIVATE libSceSystemService libc) @@ -27,7 +40,8 @@ if(BUILD_TESTING) add_test(NAME guest_system_relaunch COMMAND guest_system_exit_tests --relaunch-driver) add_test(NAME guest_nested_exit COMMAND guest_system_exit_tests --nested-exit-driver) set_tests_properties(guest_system_exit PROPERTIES - PASS_REGULAR_EXPRESSION "Guest shutdown and atexit completed") + PASS_REGULAR_EXPRESSION "Guest shutdown completed" + FAIL_REGULAR_EXPRESSION "Guest exit handler ran|Guest thread resumed during exit") set_tests_properties(guest_quick_exit PROPERTIES PASS_REGULAR_EXPRESSION "Guest quick_exit handlers completed") set_tests_properties(guest_system_relaunch PROPERTIES diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 36fa503c9d..6b5bb3bb44 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -18,6 +18,7 @@ #endif #include "prx/libc/include/Shutdown.hpp" #include "SceTypes.hpp" +#include "LoadExec.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libSceSystemService/SystemService.hpp" @@ -201,8 +202,11 @@ extern "C" { int APS5_VABI sceSystemServiceLoadExec(const char* path, const char* const* arguments) { if (!path || !*path) return SYSTEM_SERVICE_ERROR_PARAMETER; - if (std::strcmp(path, "exit") == 0) LibcExit_nid_no_patch(0); + if (std::strcmp(path, "exit") == 0) LibcExitWithoutHandlers_nid_no_patch(0); auto launch = PrepareLaunch(path, arguments); +#if defined(_WIN32) || defined(__linux__) + LoadConvertedExecutable_nid_no_patch(std::filesystem::path(launch.executable), arguments); +#endif LibcRunShutdown_nid_postfix(); std::fflush(nullptr); StartHostExecutable(launch); diff --git a/core/libs/prx/libSceSystemService/LoadExec.cpp b/core/libs/prx/libSceSystemService/LoadExec.cpp new file mode 100644 index 0000000000..cc359f4b53 --- /dev/null +++ b/core/libs/prx/libSceSystemService/LoadExec.cpp @@ -0,0 +1,178 @@ +#include "LoadExec.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#elif defined(__linux__) +#include +#include +extern char** environ; +#endif + +namespace { + +#ifdef _WIN32 +using Handle = std::unique_ptr; + +std::wstring Wide(const std::string& text) { + if (text.size() > static_cast(std::numeric_limits::max())) + throw std::runtime_error("LoadExec: argument is too long"); + if (text.empty()) return {}; + const int size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast(text.size()), nullptr, 0); + if (!size) throw std::runtime_error("LoadExec: argument is not valid UTF-8"); + std::wstring wide(size, L'\0'); + if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast(text.size()), wide.data(), size) != size) + throw std::runtime_error("LoadExec: cannot convert argument to UTF-16"); + return wide; +} + +std::wstring Quote(const std::wstring& text) { + std::wstring result = L"\""; + std::size_t slashes = 0; + for (const wchar_t character : text) { + if (character == L'\\') { ++slashes; continue; } + result.append(character == L'"' ? slashes * 2 + 1 : slashes, L'\\'); + result += character; + slashes = 0; + } + result.append(slashes * 2, L'\\'); + return result + L'"'; +} + +Handle Duplicate(HANDLE source, DWORD access) { + HANDLE handle = nullptr; + if (!DuplicateHandle(GetCurrentProcess(), source, GetCurrentProcess(), &handle, access, TRUE, + access ? 0 : DUPLICATE_SAME_ACCESS)) + throw std::system_error(GetLastError(), std::system_category(), "LoadExec: duplicating a process handle"); + return Handle(handle, CloseHandle); +} + +[[noreturn]] void Replace(const std::filesystem::path& target, const std::vector& arguments) { + std::wstring command = Quote(target.wstring()); + for (const auto& argument : arguments) { + if (!command.empty()) command += L' '; + command += Quote(Wide(argument)); + } + if (command.size() >= 32767) throw std::runtime_error("LoadExec: command line is too long"); + auto parent = Duplicate(GetCurrentProcess(), SYNCHRONIZE); + std::vector standard; + std::vector handles{parent.get()}; + STARTUPINFOEXW startup{}; + startup.StartupInfo.cb = sizeof(startup); + startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES; + HANDLE* outputs[] = {&startup.StartupInfo.hStdInput, &startup.StartupInfo.hStdOutput, &startup.StartupInfo.hStdError}; + const DWORD identifiers[] = {STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE}; + for (std::size_t index = 0; index < 3; ++index) { + const HANDLE source = GetStdHandle(identifiers[index]); + if (source && source != INVALID_HANDLE_VALUE) { + standard.push_back(Duplicate(source, 0)); + *outputs[index] = standard.back().get(); + handles.push_back(*outputs[index]); + } + } + SIZE_T bytes = 0; + InitializeProcThreadAttributeList(nullptr, 1, 0, &bytes); + std::vector storage((bytes + sizeof(std::max_align_t) - 1) / sizeof(std::max_align_t)); + auto* attributes = reinterpret_cast(storage.data()); + if (!InitializeProcThreadAttributeList(attributes, 1, 0, &bytes)) + throw std::system_error(GetLastError(), std::system_category(), "LoadExec: creating process attributes"); + const std::unique_ptr<_PROC_THREAD_ATTRIBUTE_LIST, decltype(&DeleteProcThreadAttributeList)> cleanup(attributes, DeleteProcThreadAttributeList); + if (!UpdateProcThreadAttribute(attributes, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, handles.data(), handles.size() * sizeof(HANDLE), nullptr, nullptr)) + throw std::system_error(GetLastError(), std::system_category(), "LoadExec: selecting inherited handles"); + startup.lpAttributeList = attributes; + const std::unique_ptr inherited(GetEnvironmentStringsW(), FreeEnvironmentStringsW); + if (!inherited) throw std::runtime_error("LoadExec: cannot read process environment"); + constexpr wchar_t Variable[] = L"ANYPS5_LOADEXEC_PARENT="; + constexpr wchar_t Skip[] = L"ANYPS5_LOADEXEC_ARGV_SKIP="; + std::vector entries; + for (const wchar_t* at = inherited.get(); *at;) { + const std::wstring entry(at); + if (_wcsnicmp(entry.c_str(), Variable, std::size(Variable) - 1) != 0 && + _wcsnicmp(entry.c_str(), Skip, std::size(Skip) - 1) != 0) { + entries.push_back(entry); + } + at += entry.size() + 1; + } + std::wostringstream value; + value << Variable << std::hex << reinterpret_cast(parent.get()); + entries.push_back(value.str()); + entries.push_back(std::wstring(Skip) + L"1"); + std::sort(entries.begin(), entries.end(), [](const auto& left, const auto& right) { return _wcsicmp(left.c_str(), right.c_str()) < 0; }); + std::vector environment; + for (const auto& entry : entries) { + environment.insert(environment.end(), entry.begin(), entry.end()); + environment.push_back(0); + } + environment.push_back(0); + PROCESS_INFORMATION process{}; + if (!CreateProcessW(target.c_str(), command.data(), nullptr, nullptr, TRUE, + CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT, + environment.data(), target.parent_path().c_str(), &startup.StartupInfo, &process)) + throw std::system_error(GetLastError(), std::system_category(), "LoadExec: creating the converted executable"); + Handle child(process.hProcess, CloseHandle), thread(process.hThread, CloseHandle); + try { + LibcRunShutdown_nid_postfix(); + std::fflush(nullptr); + if (ResumeThread(thread.get()) == static_cast(-1)) + throw std::system_error(GetLastError(), std::system_category(), "LoadExec: starting the converted executable"); + } catch (...) { + TerminateProcess(child.get(), 1); + throw; + } + thread.reset(); + child.reset(); + parent.reset(); + LibcTerminate_nid_no_patch(0); +} +#elif defined(__linux__) +[[noreturn]] void Replace(const std::filesystem::path& target, const std::vector& arguments) { + const int descriptor = open(target.c_str(), O_RDONLY | O_CLOEXEC); + if (descriptor < 0) throw std::system_error(errno, std::generic_category(), "LoadExec: opening the converted executable"); + const auto close = [](int* fd) { ::close(*fd); delete fd; }; + const std::unique_ptr file(new int(descriptor), close); + std::array header{}; + if (pread(descriptor, header.data(), header.size(), 0) != static_cast(header.size()) || + header[0] != 0x7f || header[1] != 'E' || header[2] != 'L' || header[3] != 'F' || + header[4] != 2 || header[5] != 1 || (header[7] != 0 && header[7] != 3) || header[17] != 0 || (header[16] != 2 && header[16] != 3) || header[18] != 62 || header[19] != 0 || access(target.c_str(), X_OK) != 0) + throw std::runtime_error("LoadExec: target is not an executable x86-64 Linux ELF"); + std::vector pointers; + for (const auto& argument : arguments) pointers.push_back(const_cast(argument.c_str())); + pointers.push_back(nullptr); + LibcRunShutdown_nid_postfix(); + std::fflush(nullptr); + std::filesystem::current_path(target.parent_path()); + if ((descriptor > 3 && close_range(3, static_cast(descriptor - 1), 0) != 0) || + close_range(static_cast(std::max(3, descriptor + 1)), ~0u, 0) != 0) + throw std::system_error(errno, std::generic_category(), "LoadExec: closing the old executable's descriptors"); + fexecve(descriptor, pointers.data(), environ); + throw std::system_error(errno, std::generic_category(), "LoadExec: replacing the executable"); +} +#endif + +} + +extern "C" bool LoadConvertedExecutable_nid_no_patch(const std::filesystem::path& target, const char* const* arguments) { +#if defined(_WIN32) || defined(__linux__) + if (!arguments || !arguments[0]) return false; + std::vector copied; + for (std::size_t index = 0; arguments[index]; ++index) copied.emplace_back(arguments[index]); + Replace(target, copied); +#else + (void)target; + (void)arguments; + return false; +#endif +} diff --git a/core/libs/prx/libSceSystemService/LoadExec.hpp b/core/libs/prx/libSceSystemService/LoadExec.hpp new file mode 100644 index 0000000000..7b689c7745 --- /dev/null +++ b/core/libs/prx/libSceSystemService/LoadExec.hpp @@ -0,0 +1,8 @@ +#ifndef CORE_LIBS_PRX_LIBSCESYSTEMSERVICE_LOADEXEC_HPP +#define CORE_LIBS_PRX_LIBSCESYSTEMSERVICE_LOADEXEC_HPP + +#include + +extern "C" bool LoadConvertedExecutable_nid_no_patch(const std::filesystem::path& target, const char* const* arguments); + +#endif diff --git a/core/libs/prx/libSceUserService/CMakeLists.txt b/core/libs/prx/libSceUserService/CMakeLists.txt index ac0ab70e9a..449131a43a 100644 --- a/core/libs/prx/libSceUserService/CMakeLists.txt +++ b/core/libs/prx/libSceUserService/CMakeLists.txt @@ -23,3 +23,14 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_user_service_lifecycle_tests ${LIBS_INCLUDE_DIR}/tests/GuestUserServiceLifecycle.cpp) + target_include_directories(guest_user_service_lifecycle_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_user_service_lifecycle_tests PRIVATE libSceUserService) + add_test(NAME guest_user_service_lifecycle COMMAND guest_user_service_lifecycle_tests) + if(WIN32) + set_tests_properties(guest_user_service_lifecycle 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 4996849ae0..a2c906db17 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -6,6 +6,9 @@ #include #include "prx/libSceUserService/UserService.hpp" +static std::atomic initialized{false}; +static std::atomic loginReported{false}; + 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; @@ -55,12 +58,14 @@ int APS5_VABI sceUserServiceGetAgeLevel(int user_id, uint32_t* age_level) { return USER_SERVICE_OK; } -// The initial user is reported as logging in once; afterwards there are no user events. +// The initial user is reported as logging in once per initialization; afterwards there are no user events. int APS5_VABI sceUserServiceGetEvent(SceUserServiceEvent* event) { + if (!initialized.load()) { + return USER_SERVICE_ERROR_NOT_INITIALIZED; + } if (event == nullptr) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } - static std::atomic loginReported{false}; if (loginReported.exchange(true)) { return USER_SERVICE_ERROR_NO_EVENT; } @@ -97,6 +102,9 @@ int APS5_VABI sceUserServiceGetInitialUser(int* user_id) { } int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* user_id_list) { + if (!initialized.load()) { + return USER_SERVICE_ERROR_NOT_INITIALIZED; + } if (user_id_list == nullptr) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } @@ -132,14 +140,24 @@ int APS5_VABI sceUserServiceGetUserNumber(int user_id, int32_t* number) { int APS5_VABI sceUserServiceInitialize(const void* params) { (void)params; + if (initialized.exchange(true)) { + return USER_SERVICE_ERROR_ALREADY_INITIALIZED; + } + loginReported.store(false); return USER_SERVICE_OK; } int APS5_VABI sceUserServiceInitialize2(void) { + if (!initialized.exchange(true)) { + loginReported.store(false); + } return USER_SERVICE_OK; } int APS5_VABI sceUserServiceTerminate(void) { + if (!initialized.exchange(false)) { + return USER_SERVICE_ERROR_NOT_INITIALIZED; + } return USER_SERVICE_OK; } diff --git a/core/libs/prx/libSceUserService/UserService.hpp b/core/libs/prx/libSceUserService/UserService.hpp index 342b82052c..ae6468367d 100644 --- a/core/libs/prx/libSceUserService/UserService.hpp +++ b/core/libs/prx/libSceUserService/UserService.hpp @@ -5,6 +5,8 @@ #include constexpr int USER_SERVICE_OK = 0; +constexpr int USER_SERVICE_ERROR_NOT_INITIALIZED = static_cast(0x80960002u); +constexpr int USER_SERVICE_ERROR_ALREADY_INITIALIZED = static_cast(0x80960003u); 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; diff --git a/core/libs/prx/libSceVdecsw/Export.cpp b/core/libs/prx/libSceVdecsw/Export.cpp index d56d38b16b..26fdb475ef 100644 --- a/core/libs/prx/libSceVdecsw/Export.cpp +++ b/core/libs/prx/libSceVdecsw/Export.cpp @@ -116,6 +116,13 @@ struct Videodec2AvcPictureInfo { std::uint32_t picHeightInLumaSamples; }; +struct Videodec2Crop { + std::uint32_t left; + std::uint32_t right; + std::uint32_t top; + std::uint32_t bottom; +}; + struct DecoderMemoryInfo { std::uint64_t thisSize; std::uint64_t cpuMemorySize; @@ -141,6 +148,7 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const InputDa int APS5_VABI sceVideodec2Flush_nid_postfix(std::uint64_t handle, Videodec2FrameBuffer* frame, Videodec2OutputInfo* output); int APS5_VABI sceVideodec2Reset_nid_postfix(std::uint64_t handle); int APS5_VABI sceVideodec2GetAvcPictureInfo_nid_postfix(const Videodec2OutputInfo* output, Videodec2AvcPictureInfo* first, Videodec2AvcPictureInfo* second); +void Videodec2PictureCrop_nid_no_patch(std::uint64_t handle, const void* frameBuffer, Videodec2Crop* crop); } namespace { @@ -183,10 +191,12 @@ std::shared_ptr findDecoder(std::uint64_t handle) { return found->second; } -AvcPictureInfo pictureInfo(const Videodec2OutputInfo& output) { +AvcPictureInfo pictureInfo(std::uint64_t handle, const Videodec2OutputInfo& output) { Videodec2AvcPictureInfo source{sizeof(Videodec2AvcPictureInfo)}; sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &source, nullptr); if (!source.isValid) throw std::runtime_error("Vdecsw: decoded picture has no picture info"); + Videodec2Crop crop{}; + Videodec2PictureCrop_nid_no_patch(handle, output.frameBuffer, &crop); const auto widthInMbs = (output.frameWidth + 15u) / 16u; const auto heightInMbs = (output.frameHeight + 15u) / 16u; AvcPictureInfo info{}; @@ -201,9 +211,11 @@ AvcPictureInfo pictureInfo(const Videodec2OutputInfo& output) { info.picWidthInMbsMinus1 = widthInMbs - 1u; info.picHeightInMapUnitsMinus1 = heightInMbs - 1u; info.frameMbsOnlyFlag = 1; - info.frameCroppingFlag = widthInMbs * 16u != output.frameWidth || heightInMbs * 16u != output.frameHeight; - info.frameCropRightOffset = (widthInMbs * 16u - output.frameWidth) / 2u; - info.frameCropBottomOffset = (heightInMbs * 16u - output.frameHeight) / 2u; + info.frameCroppingFlag = crop.left != 0 || crop.right != 0 || crop.top != 0 || crop.bottom != 0; + info.frameCropLeftOffset = crop.left / 2u; + info.frameCropRightOffset = crop.right / 2u; + info.frameCropTopOffset = crop.top / 2u; + info.frameCropBottomOffset = crop.bottom / 2u; return info; } @@ -226,7 +238,7 @@ void advance(std::uint64_t handle, Decoder& decoder) { return; } if (output.isValid) { - const auto info = pictureInfo(output); + const auto info = pictureInfo(handle, output); decoder.pictures.push_back({std::move(pixels), output, info}); } } diff --git a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp index 8662d18e0b..2d9e303e1e 100644 --- a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp @@ -218,13 +218,32 @@ std::uint8_t stickByte(std::int32_t axis) { return static_cast(((axis + 32768) * 255 + 32767) / 65535); } +std::filesystem::path pathFromUtf8(std::string_view text) { + return std::filesystem::path(std::u8string(reinterpret_cast(text.data()), text.size())); +} + +std::string pathToUtf8(const std::filesystem::path& path) { + const auto text = path.u8string(); + return std::string(reinterpret_cast(text.data()), text.size()); +} + +std::filesystem::path configuredPath() { +#ifdef _WIN32 + const wchar_t* configured = _wgetenv(L"ANYPS5_INPUT_CONFIG"); + return configured != nullptr ? std::filesystem::path(configured) : std::filesystem::path{}; +#else + const char* configured = std::getenv("ANYPS5_INPUT_CONFIG"); + return configured != nullptr ? pathFromUtf8(configured) : std::filesystem::path{}; +#endif +} + [[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); + throw std::runtime_error("Pad: invalid input mapping " + pathToUtf8(path) + ":" + std::to_string(line) + ": " + reason); } std::filesystem::path defaultConfigPath() { std::unique_ptr basePath(SDL_GetBasePath(), SDL_free); - if (basePath) return std::filesystem::path(basePath.get()) / "anyps5-input.ini"; + if (basePath) return pathFromUtf8(basePath.get()) / "anyps5-input.ini"; return "anyps5-input.ini"; } @@ -248,13 +267,13 @@ std::array Pad::StickWithDeadzone(std::int16_t x, std::int16_t 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(); + const auto configured = configuredPath(); + const bool explicitPath = !configured.empty(); + const std::filesystem::path path = explicitPath ? configured : defaultConfigPath(); std::ifstream file(path); if (!file) { if (explicitPath || std::filesystem::exists(path)) { - throw std::runtime_error("Pad: cannot read input mapping '" + path.string() + "'"); + throw std::runtime_error("Pad: cannot read input mapping '" + pathToUtf8(path) + "'"); } return configuration; } @@ -305,7 +324,7 @@ Pad::InputConfiguration Pad::LoadInputMapping() { invalidLine(path, lineNumber, error.what()); } } - if (file.bad()) throw std::runtime_error("Pad: cannot read input mapping '" + path.string() + "'"); + if (file.bad()) throw std::runtime_error("Pad: cannot read input mapping '" + pathToUtf8(path) + "'"); return configuration; } diff --git a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp index c00aef3ff3..4286633290 100644 --- a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp @@ -54,7 +54,7 @@ void MouseInput::HandleEvent(const SDL_Event& event, unsigned windowId) { default: return; } - MousePublishInput_nid_postfix(input); + MousePublishInput_nid_no_patch(input); } void MouseInput::SetPresentation(std::uint32_t sourceWidth, std::uint32_t sourceHeight, std::uint32_t presentedWidth, std::uint32_t presentedHeight) { @@ -78,7 +78,7 @@ std::int32_t MouseInput::scaled(std::int32_t delta, double scale, double& remain } bool MouseInput::CaptureWanted(bool allowed) const { - return allowed && focused && MouseIsOpen_nid_postfix(); + return allowed && focused && MouseIsOpen_nid_no_patch(); } void MouseInput::UpdateCapture(bool allowed) { diff --git a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp index 5789bb5b96..0d85e5f293 100644 --- a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp @@ -122,6 +122,48 @@ void mouseLookReject() { require(rejects("MouseDecayMs = 20\nmousedecayms = 30\n"), "a mouse setting given twice is rejected"); } +void setConfigPath(const std::filesystem::path& path) { +#ifdef _WIN32 + require(_wputenv_s(L"ANYPS5_INPUT_CONFIG", path.wstring().c_str()) == 0, "set ANYPS5_INPUT_CONFIG"); +#else + require(::setenv("ANYPS5_INPUT_CONFIG", path.string().c_str(), 1) == 0, "set ANYPS5_INPUT_CONFIG"); +#endif +} + +void nonAsciiPaths() { + const std::u8string name = u8"anyps5-input-\u00fc-\u65e5\u672c-" + std::u8string(root.filename().u8string()); + const auto directory = root / std::filesystem::path(name); + std::filesystem::create_directories(directory); + const auto file = directory / "anyps5-input.ini"; + { + std::ofstream stream(file, std::ios::binary | std::ios::trunc); + stream << "MouseSensitivity = 3\n"; + require(stream.good(), "write the input mapping in a non-ASCII directory"); + } + setConfigPath(file); + require(Pad::LoadInputMapping().settings.mouseSensitivity == 3.0, "ANYPS5_INPUT_CONFIG may name a path with non-ASCII characters"); + { + std::ofstream stream(file, std::ios::binary | std::ios::trunc); + stream << "MouseSensitivity = fast\n"; + } + std::string message; + try { + Pad::LoadInputMapping(); + } catch (const std::runtime_error& error) { + message = error.what(); + } + const auto utf8Name = std::string(reinterpret_cast(name.data()), name.size()); + require(message.find(utf8Name) != std::string::npos, "an error names the path in UTF-8"); + setConfigPath(directory / "missing.ini"); + message.clear(); + try { + Pad::LoadInputMapping(); + } catch (const std::runtime_error& error) { + message = error.what(); + } + require(message.find(utf8Name) != std::string::npos, "a missing file names the path in UTF-8"); +} + 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); @@ -159,6 +201,7 @@ int main() try { mouseLookDefault(); mouseLookParse(); mouseLookReject(); + nonAsciiPaths(); deadzoneOffMatchesRawSticks(); deadzoneShapesSticks(); std::filesystem::remove_all(root); diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index 89b25e2b09..971db75652 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -267,12 +267,20 @@ std::vector ParseVp9Packet(std::span bytes, std::ar return result; } +struct Crop { + std::uint32_t left = 0; + std::uint32_t right = 0; + std::uint32_t top = 0; + std::uint32_t bottom = 0; +}; + struct Picture { std::uint64_t pts = 0; std::uint64_t dts = 0; std::uint64_t attached = 0; bool idr = false; Vp9PictureInfo vp9{}; + Crop crop{}; }; struct FrameDeleter { @@ -356,6 +364,7 @@ void OpenCodec(Decoder& decoder) { decoder.context->err_recognition = AV_EF_EXPLODE; decoder.context->thread_count = 0; decoder.context->thread_type = FF_THREAD_SLICE; + decoder.context->apply_cropping = 0; if (avcodec_open2(decoder.context, codec, nullptr) < 0) { avcodec_free_context(&decoder.context); throw std::runtime_error("Videodec2: cannot open decoder"); @@ -443,6 +452,7 @@ void EmitDecoded(std::uint64_t handle, Decoder& decoder, FrameBuffer* frame, Out auto timestamps = found->second; decoder.inputs.erase(found); timestamps.idr = (picture->flags & AV_FRAME_FLAG_KEY) != 0; + timestamps.crop = {static_cast(picture->crop_left), static_cast(picture->crop_right), static_cast(picture->crop_top), static_cast(picture->crop_bottom)}; decoder.width = static_cast(picture->width); decoder.height = static_cast(picture->height); if (decoder.context->profile > 0) decoder.profile = static_cast(decoder.context->profile) & 0xffu; @@ -674,4 +684,15 @@ int APS5_VABI sceVideodec2GetVp9PictureInfo_nid_postfix(const OutputInfo* output return 0; } +void Videodec2PictureCrop_nid_no_patch(std::uint64_t handle, const void* frameBuffer, Crop* crop) { + if (!crop) throw std::runtime_error("Videodec2: invalid argument pointer"); + const auto found = FindDecoder(handle); + if (!found) throw std::runtime_error("Videodec2: invalid decoder handle"); + auto& decoder = *found; + std::lock_guard guard(decoder.mutex); + const auto picture = decoder.pictures.find(frameBuffer); + if (picture == decoder.pictures.end()) throw std::runtime_error("Videodec2: unknown frame buffer"); + *crop = picture->second.crop; +} + } diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index c9bb506b2c..1fe621e9eb 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -543,6 +543,11 @@ if(BUILD_TESTING) target_include_directories(guest_exit_order_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_exit_order_tests PRIVATE libc) add_test(NAME guest_exit_order COMMAND guest_exit_order_tests) + add_test_executable(guest_path_lifetime_tests ${LIBS_INCLUDE_DIR}/tests/GuestPathLifetime.cpp) + target_include_directories(guest_path_lifetime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_path_lifetime_tests PRIVATE libc) + add_test(NAME guest_path_lifetime COMMAND guest_path_lifetime_tests) + set_tests_properties(guest_path_lifetime PROPERTIES TIMEOUT 20) if(NOT APPLE) enable_language(C) add_test_executable(exception_c_cleanup_tests ${LIBS_INCLUDE_DIR}/tests/ExceptionCCleanup.cpp ${LIBS_INCLUDE_DIR}/tests/ExceptionCCleanup.c) @@ -581,8 +586,9 @@ if(BUILD_TESTING) add_test(NAME application_heap_invalid_metadata COMMAND application_heap_tests invalid) add_test(NAME application_heap_default COMMAND application_heap_tests default) add_test(NAME application_heap_exit_order COMMAND application_heap_tests exit-order) + add_test(NAME application_heap_lazy COMMAND application_heap_tests lazy) if(WIN32) - set_tests_properties(application_heap_replacement application_heap_invalid_metadata application_heap_default application_heap_exit_order + set_tests_properties(application_heap_replacement application_heap_invalid_metadata application_heap_default application_heap_exit_order application_heap_lazy PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_allocator_failures_tests ${LIBS_INCLUDE_DIR}/tests/GuestAllocatorFailures.cpp) diff --git a/core/libs/prx/libc/include/GuestAllocations.hpp b/core/libs/prx/libc/include/GuestAllocations.hpp index ce42a0be61..bee10a9750 100644 --- a/core/libs/prx/libc/include/GuestAllocations.hpp +++ b/core/libs/prx/libc/include/GuestAllocations.hpp @@ -63,6 +63,7 @@ void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* poi void GuestAllocationsRequireAvailable_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); bool GuestAllocationsCovers_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); bool GuestAllocationsOverlaps_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); +bool GuestAllocationsForRegistered_nid_postfix(void* mutation, const void* pointer, std::size_t bytes, const std::function& visit); Range GuestAllocationsFind_nid_postfix(void* mutation, const void* pointer); void GuestAllocationsRemove_nid_postfix(void* mutation, const void* pointer); void GuestAllocationsProtect_nid_postfix(void* mutation, const void* pointer, std::size_t bytes, bool readable, bool writable, bool gpu, const std::function& apply); @@ -103,6 +104,7 @@ public: void RequireAvailable(const void* pointer, std::size_t bytes) const { GuestAllocationsRequireAvailable_nid_postfix(handle, pointer, bytes); } bool Covers(const void* pointer, std::size_t bytes) const { return GuestAllocationsCovers_nid_postfix(handle, pointer, bytes); } bool Overlaps(const void* pointer, std::size_t bytes) const { return GuestAllocationsOverlaps_nid_postfix(handle, pointer, bytes); } + bool ForRegistered(const void* pointer, std::size_t bytes, const std::function& visit) const { return GuestAllocationsForRegistered_nid_postfix(handle, pointer, bytes, visit); } Range Find(const void* pointer) const { return GuestAllocationsFind_nid_postfix(handle, pointer); } void Remove(const void* pointer) { GuestAllocationsRemove_nid_postfix(handle, pointer); } void Unmap(const void* pointer, std::size_t bytes, const std::function& apply) { GuestAllocationsUnmap_nid_postfix(handle, pointer, bytes, apply); } diff --git a/core/libs/prx/libc/include/GuestArena.hpp b/core/libs/prx/libc/include/GuestArena.hpp index 3c96845e4d..a1ad24d339 100644 --- a/core/libs/prx/libc/include/GuestArena.hpp +++ b/core/libs/prx/libc/include/GuestArena.hpp @@ -19,6 +19,7 @@ bool GuestArenaAvailable_nid_postfix(); bool GuestArenaContains_nid_postfix(const void* pointer, std::size_t bytes); void* GuestArenaAllocate_nid_postfix(std::size_t bytes, std::size_t alignment); void* GuestArenaAllocateAtOrAbove_nid_postfix(std::uintptr_t hint, std::size_t bytes, std::size_t alignment); +void* GuestArenaAllocateFromHint_nid_postfix(std::uintptr_t hint, std::size_t bytes, std::size_t alignment); std::uintptr_t GuestArenaHighestFree_nid_no_patch(std::size_t bytes, std::size_t alignment); void GuestArenaMarkUsed_nid_postfix(const void* pointer, std::size_t bytes); void GuestArenaRelease_nid_postfix(const void* pointer, std::size_t bytes); diff --git a/core/libs/prx/libc/include/Shutdown.hpp b/core/libs/prx/libc/include/Shutdown.hpp index a7618a0321..4bfe737f75 100644 --- a/core/libs/prx/libc/include/Shutdown.hpp +++ b/core/libs/prx/libc/include/Shutdown.hpp @@ -12,6 +12,7 @@ extern "C" void LibcRequestShutdown_nid_postfix(); extern "C" void LibcRequestExit_nid_postfix(int code); extern "C" [[noreturn]] void LibcAwaitExit_nid_postfix(); extern "C" [[noreturn]] void LibcExit_nid_no_patch(int code); +extern "C" [[noreturn]] void LibcExitWithoutHandlers_nid_no_patch(int code); extern "C" [[noreturn]] void LibcTerminate_nid_no_patch(int code); #endif diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 8df63459a7..56c8851fbe 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -24,8 +24,8 @@ public: enum class Collection { All, Armed, Fresh }; static WindowsMappings& Get() { - static WindowsMappings mappings; - return mappings; + static auto* mappings = new WindowsMappings; + return *mappings; } void* Reserve(void* address, std::size_t bytes) { @@ -334,6 +334,12 @@ public: } } else if (clear && !collectable(address, end)) return false; auto position = views.lower_bound(address & ~(pageBytes - 1)); + std::uintptr_t cleanStart = 0; + std::uintptr_t cleanEnd = 0; + const auto flushClean = [&] { + if (cleanEnd > cleanStart) rememberClean(cleanStart, cleanEnd); + cleanStart = cleanEnd = 0; + }; for (auto cursor = address; cursor < end;) { const auto nextClean = cleanRanges.upper_bound(cursor); if (nextClean != cleanRanges.begin()) { @@ -346,6 +352,7 @@ public: const auto base = cursor & ~(pageBytes - 1); const auto found = viewAt(base, position); if (found != views.end() && found->second.resident) { + flushClean(); if (!watchResident) return false; const auto available = capacity - *count; if (available == 0) return true; @@ -371,7 +378,10 @@ public: } } } - if (view.protection == PAGE_NOACCESS) return false; + if (view.protection == PAGE_NOACCESS) { + flushClean(); + return false; + } const bool pinned = view.page->pins != 0; std::uint8_t settled = 0; auto settledEnd = cursor; @@ -414,24 +424,25 @@ public: other.armed |= arming; } const auto tracked = static_cast(settled & ~untracked); - auto cleanFrom = cursor; - auto cleanTo = cursor; for (std::size_t sub = 0; sub < subpages; ++sub) { if ((tracked & (1u << sub)) == 0) continue; const auto from = std::max(cursor, base + sub * subpageBytes); const auto to = std::min(settledEnd, base + (sub + 1) * subpageBytes); if (from == base + sub * subpageBytes && to == base + (sub + 1) * subpageBytes) view.seen[sub] = view.page->generation[sub]; - if (from != cleanTo) { - if (cleanFrom != cleanTo) rememberClean(cleanFrom, cleanTo); - cleanFrom = from; + if (from != cleanEnd) { + flushClean(); + cleanStart = from; } - cleanTo = to; + cleanEnd = to; } - if (cleanFrom != cleanTo) rememberClean(cleanFrom, cleanTo); } - if (full) return true; + if (full) { + flushClean(); + return true; + } cursor = stop; } else { + flushClean(); if (collection == Collection::Fresh) { const auto next = views.lower_bound(cursor); cursor = next == views.end() ? end : std::min(end, next->first); @@ -450,6 +461,7 @@ public: cursor = stop; } } + flushClean(); return true; } diff --git a/core/libs/prx/libc/include/general/AtomicSharedPtr.hpp b/core/libs/prx/libc/include/general/AtomicSharedPtr.hpp new file mode 100644 index 0000000000..30054c527e --- /dev/null +++ b/core/libs/prx/libc/include/general/AtomicSharedPtr.hpp @@ -0,0 +1,50 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_ATOMICSHAREDPTR_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_ATOMICSHAREDPTR_HPP + +#include +#include +#include +#include + +#if defined(__cpp_lib_atomic_shared_ptr) +template +using AtomicSharedPtr = std::atomic>; +#else +template +class AtomicSharedPtr { +public: + AtomicSharedPtr() noexcept = default; + AtomicSharedPtr(std::shared_ptr desired) noexcept : value(std::move(desired)) {} + AtomicSharedPtr(const AtomicSharedPtr&) = delete; + AtomicSharedPtr& operator=(const AtomicSharedPtr&) = delete; + + std::shared_ptr load(std::memory_order = std::memory_order_seq_cst) const noexcept { + std::lock_guard lock(mutex); + return value; + } + void store(std::shared_ptr desired, std::memory_order order = std::memory_order_seq_cst) noexcept { + exchange(std::move(desired), order); + } + std::shared_ptr exchange(std::shared_ptr desired, std::memory_order = std::memory_order_seq_cst) noexcept { + std::lock_guard lock(mutex); + return std::exchange(value, std::move(desired)); + } + bool compare_exchange_strong(std::shared_ptr& expected, std::shared_ptr desired, std::memory_order = std::memory_order_seq_cst) noexcept { + std::lock_guard lock(mutex); + if (value == expected) { + value = std::move(desired); + return true; + } + expected = value; + return false; + } + operator std::shared_ptr() const noexcept { return load(); } + void operator=(std::shared_ptr desired) noexcept { store(std::move(desired)); } + +private: + mutable std::mutex mutex; + std::shared_ptr value; +}; +#endif + +#endif diff --git a/core/libs/prx/libc/src/ApplicationHeap.cpp b/core/libs/prx/libc/src/ApplicationHeap.cpp index e619429eee..6acd014d08 100644 --- a/core/libs/prx/libc/src/ApplicationHeap.cpp +++ b/core/libs/prx/libc/src/ApplicationHeap.cpp @@ -76,9 +76,17 @@ TValue read(const void* pointer, std::size_t offset) { return value; } +void ensureInitialized(std::unique_lock& lock) { + if (heapApi[0] != nullptr || heapFailure || heapFinalized) return; + lock.unlock(); + ApplicationHeapInitialize_nid_no_patch(ApplicationProcessParameters_nid_no_patch()); + lock.lock(); +} + template TCallback callback(std::size_t index) { - std::lock_guard lock(heapMutex); + std::unique_lock lock(heapMutex); + ensureInitialized(lock); if (heapFailure) std::rethrow_exception(heapFailure); if (heapFinalized) throw std::runtime_error("application heap: allocator has been finalized"); if (heapApi[index] == nullptr) throw std::runtime_error("application heap: allocator API is not registered"); @@ -202,7 +210,8 @@ void* ApplicationHeapRealign_nid_no_patch(void* pointer, std::size_t bytes, std: requireAlignment(alignment); Realign realign; { - std::lock_guard lock(heapMutex); + std::unique_lock lock(heapMutex); + ensureInitialized(lock); if (heapFailure) std::rethrow_exception(heapFailure); if (heapFinalized) throw std::runtime_error("application heap: allocator has been finalized"); if (heapApi[0] == nullptr) throw std::runtime_error("application heap: allocator API is not registered"); @@ -240,7 +249,8 @@ int ApplicationHeapPosixAlign_nid_no_patch(void** pointer, std::size_t alignment int ApplicationHeapStatsFast_nid_no_patch(void* stats) { Stats statsFast; { - std::lock_guard lock(heapMutex); + std::unique_lock lock(heapMutex); + ensureInitialized(lock); if (heapFailure) std::rethrow_exception(heapFailure); if (heapFinalized) throw std::runtime_error("application heap: allocator has been finalized"); if (heapApi[0] == nullptr) throw std::runtime_error("application heap: allocator API is not registered"); @@ -254,7 +264,8 @@ int ApplicationHeapStatsFast_nid_no_patch(void* stats) { std::size_t ApplicationHeapUsableSize_nid_no_patch(void* pointer) { UsableSize usableSize = nullptr; { - std::lock_guard lock(heapMutex); + std::unique_lock lock(heapMutex); + ensureInitialized(lock); if (heapFailure) std::rethrow_exception(heapFailure); if (heapFinalized) throw std::runtime_error("application heap: allocator has been finalized"); if (heapApi[0] == nullptr) throw std::runtime_error("application heap: allocator API is not registered"); diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 402da585ff..9e5a938d36 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -42,8 +42,8 @@ struct PathAliases { }; PathAliases& Aliases() { - static PathAliases aliases; - return aliases; + static auto* aliases = new PathAliases; + return *aliases; } std::string TrimSlashes(const char* path) { @@ -120,7 +120,10 @@ struct WorkingDirectory { const std::filesystem::path root = std::filesystem::canonical(std::filesystem::current_path()); std::filesystem::path current = root; }; -WorkingDirectory& Directories() { static WorkingDirectory state; return state; } +WorkingDirectory& Directories() { + static auto* state = new WorkingDirectory; + return *state; +} std::filesystem::path HostPath(WorkingDirectory& state, const std::filesystem::path& guest) { if (auto aliased = ResolveAlias(guest.relative_path().generic_string())) return *aliased; return ResolveHostPath(state.root, guest.relative_path()).make_preferred(); diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index c7b286af87..d6bc09c0af 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -41,8 +41,8 @@ struct Registry { }; Registry& registry() { - static Registry value; - return value; + static auto* value = new Registry; + return *value; } std::atomic generation{1}; @@ -378,6 +378,33 @@ bool GuestAllocationsOverlaps_nid_postfix(void*, const void* pointer, std::size_ return false; } +bool GuestAllocationsForRegistered_nid_postfix(void*, const void* pointer, std::size_t bytes, const std::function& visit) { + const auto address = reinterpret_cast(pointer); + require(address != 0 && bytes != 0 && bytes <= std::numeric_limits::max() - address, "invalid guest allocation range"); + const auto end = address + bytes; + const auto& ranges = registry().ranges; + auto it = ranges.upper_bound(address); + if (it != ranges.begin()) --it; + std::uintptr_t first = 0; + std::uintptr_t last = 0; + bool any = false; + for (; it != ranges.end() && it->first < end; ++it) { + const auto base = std::max(it->first, address); + const auto finish = std::min(it->first + it->second->bytes, end); + if (finish <= base) continue; + if (any && base == last) { + last = finish; + continue; + } + if (any) visit(reinterpret_cast(first), last - first); + first = base; + last = finish; + any = true; + } + if (any) visit(reinterpret_cast(first), last - first); + return any; +} + bool GuestAllocationsCovers_nid_postfix(void*, const void* pointer, std::size_t bytes) { const auto address = reinterpret_cast(pointer); require(address != 0 && bytes != 0 && bytes <= std::numeric_limits::max() - address, "invalid guest allocation range"); @@ -456,10 +483,10 @@ RangePatch replaceRange(const void* pointer, std::size_t bytes, bool remove, boo const auto insert = [&](std::uint64_t first, std::uint64_t last, bool canRead, bool canWrite, bool gpuMapped) { if (first < last) patch.inserted.emplace_back(first, std::make_shared(Range{first, static_cast(last - first), canRead, canWrite, range.allocationAddress, range.allocationBytes, range.releasable, gpuMapped})); }; - insert(base, std::max(base, address), range.readable, range.writable, range.gpu); - if (!remove) insert(std::max(base, address), std::min(finish, end), readable, writable, gpu); - insert(std::min(finish, end), finish, range.readable, range.writable, range.gpu); - cursor = std::min(finish, end); + insert(base, std::max(base, address), range.readable, range.writable, range.gpu); + if (!remove) insert(std::max(base, address), std::min(finish, end), readable, writable, gpu); + insert(std::min(finish, end), finish, range.readable, range.writable, range.gpu); + cursor = std::min(finish, end); } require(cursor == end, "guest protection or unmap range is not registered"); return patch; diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 5387ae0390..ca580fc5bd 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -49,8 +49,8 @@ std::uintptr_t alignUp(std::uintptr_t value, std::size_t alignment) { class Arena { public: static Arena& Get() { - static Arena arena; - return arena; + static auto* arena = new Arena; + return *arena; } bool Available() const { @@ -67,21 +67,18 @@ public: } void* AllocateAtOrAbove(std::uintptr_t hint, std::size_t bytes, std::size_t alignment) { - if (_base == 0) throw std::runtime_error("guest address space arena is unavailable"); - if (alignment == 0 || (alignment & (alignment - 1)) != 0) throw std::invalid_argument("invalid guest arena alignment"); + RequireAllocatable(alignment); std::lock_guard lock(_lock); if (hint >= _end) throw std::runtime_error("mapping address hint is above the guest address space arena"); - std::uintptr_t candidate = alignUp(std::max(_base, hint), alignment); - auto it = _used.upper_bound(candidate); - if (it != _used.begin() && std::prev(it)->second > candidate) --it; - for (; it != _used.end(); ++it) { - const auto& [start, end] = *it; - if (candidate <= _end && bytes <= _end - candidate && candidate + bytes <= start) break; - candidate = std::max(candidate, alignUp(end, alignment)); - } - if (candidate > _end || bytes > _end - candidate) throw std::runtime_error("guest address space arena exhausted"); - _used.emplace(candidate, candidate + bytes); - return reinterpret_cast(candidate); + return Claim(FindAtOrAbove(hint, bytes, alignment), bytes); + } + + void* AllocateFromHint(std::uintptr_t hint, std::size_t bytes, std::size_t alignment) { + RequireAllocatable(alignment); + std::lock_guard lock(_lock); + auto candidate = hint < _end ? FindAtOrAbove(hint, bytes, alignment) : 0; + if (candidate == 0 && hint > _base) candidate = FindAtOrAbove(_base, bytes, alignment); + return Claim(candidate, bytes); } std::uintptr_t HighestFree(std::size_t bytes, std::size_t alignment) { @@ -248,6 +245,30 @@ private: std::map _used; std::vector> _holes; std::vector> _hostRegions; + void RequireAllocatable(std::size_t alignment) const { + if (_base == 0) throw std::runtime_error("guest address space arena is unavailable"); + if (alignment == 0 || (alignment & (alignment - 1)) != 0) throw std::invalid_argument("invalid guest arena alignment"); + } + + std::uintptr_t FindAtOrAbove(std::uintptr_t hint, std::size_t bytes, std::size_t alignment) const { + std::uintptr_t candidate = alignUp(std::max(_base, hint), alignment); + auto it = _used.upper_bound(candidate); + if (it != _used.begin() && std::prev(it)->second > candidate) --it; + for (; it != _used.end(); ++it) { + const auto& [start, end] = *it; + if (candidate <= _end && bytes <= _end - candidate && candidate + bytes <= start) break; + candidate = std::max(candidate, alignUp(end, alignment)); + } + if (candidate > _end || bytes > _end - candidate) return 0; + return candidate; + } + + void* Claim(std::uintptr_t candidate, std::size_t bytes) { + if (candidate == 0) throw std::runtime_error("guest address space arena exhausted"); + _used.emplace(candidate, candidate + bytes); + return reinterpret_cast(candidate); + } + std::uintptr_t _base = 0; std::uintptr_t _end = 0; bool _writeWatched = false; @@ -284,6 +305,10 @@ void* GuestArenaAllocateAtOrAbove_nid_postfix(std::uintptr_t hint, std::size_t b return Arena::Get().AllocateAtOrAbove(hint, bytes, alignment); } +void* GuestArenaAllocateFromHint_nid_postfix(std::uintptr_t hint, std::size_t bytes, std::size_t alignment) { + return Arena::Get().AllocateFromHint(hint, bytes, alignment); +} + std::uintptr_t GuestArenaHighestFree_nid_no_patch(std::size_t bytes, std::size_t alignment) { return Arena::Get().HighestFree(bytes, alignment); } diff --git a/core/libs/prx/libc/src/GuestWriteWatch.cpp b/core/libs/prx/libc/src/GuestWriteWatch.cpp index 55f991f047..959d940c38 100644 --- a/core/libs/prx/libc/src/GuestWriteWatch.cpp +++ b/core/libs/prx/libc/src/GuestWriteWatch.cpp @@ -47,8 +47,8 @@ constexpr std::uintptr_t PageBytes = 4096; class Watch { public: static Watch& Get() { - static Watch watch; - return watch; + static auto* watch = new Watch; + return *watch; } bool Available() const { @@ -321,8 +321,8 @@ const bool g_opened = (Watch::Get(), true); class Watch { public: static Watch& Get() { - static Watch watch; - return watch; + static auto* watch = new Watch; + return *watch; } bool Available() const { diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index 76a702a2a6..d4bca7d3f2 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -96,6 +96,20 @@ extern "C" void LibcRunShutdown_nid_postfix() { if (error) std::rethrow_exception(error); } +namespace { + +void StartExit() { + exitRequested.store(true); + if (exitStarted.exchange(true)) { + if (!runningExit) LibcAwaitExit_nid_postfix(); + } else { + runningExit = true; + LibcRunShutdown_nid_postfix(); + } +} + +} + extern "C" { [[noreturn]] void APS5_VABI _Exit_nid_postfix(int code) { @@ -115,17 +129,17 @@ extern "C" { } [[noreturn]] void LibcExit_nid_no_patch(int code) { - exitRequested.store(true); - if (exitStarted.exchange(true)) { - if (!runningExit) LibcAwaitExit_nid_postfix(); - } else { - runningExit = true; - LibcRunShutdown_nid_postfix(); - } + StartExit(); CxaFinalize_nid_no_patch(nullptr); std::exit(code); } +[[noreturn]] void LibcExitWithoutHandlers_nid_no_patch(int code) { + StartExit(); + std::fflush(nullptr); + LibcTerminate_nid_no_patch(code); +} + void APS5_VABI exit_nid_postfix(int code) { static const bool trace = std::getenv("APS5_TRACE_EXIT") != nullptr; if (trace) { diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 3c678d19a7..fc45573cab 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -14,6 +14,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/File/src/OpenFileLimit.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/SharedMemory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/RandomDevice.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/File/src/HostStreams.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/Stdio.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libc/src/Rename.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Semaphore/src/Semaphore.cpp @@ -85,6 +86,16 @@ endif() configure_windows_unwind(libkernel) if(BUILD_TESTING) + if(NOT WIN32) + add_library(guest_memory_lifetime_module MODULE ${LIBS_INCLUDE_DIR}/tests/GuestMemoryLifetimeModule.cpp) + target_include_directories(guest_memory_lifetime_module PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_memory_lifetime_module PRIVATE libkernel libc) + add_test_executable(guest_memory_lifetime_tests ${LIBS_INCLUDE_DIR}/tests/GuestMemoryLifetime.cpp) + target_link_libraries(guest_memory_lifetime_tests PRIVATE ${CMAKE_DL_LIBS}) + add_dependencies(guest_memory_lifetime_tests guest_memory_lifetime_module) + add_test(NAME guest_memory_lifetime COMMAND guest_memory_lifetime_tests $) + set_tests_properties(guest_memory_lifetime PROPERTIES TIMEOUT 25) + endif() add_test_executable(download_data_mount_tests ${LIBS_INCLUDE_DIR}/tests/GuestDownloadDataMount.cpp) target_include_directories(download_data_mount_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(download_data_mount_tests PRIVATE libkernel libc) @@ -535,6 +546,16 @@ if(BUILD_TESTING) set_tests_properties(guest_thread_cpu_clock PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_socket_descriptor_flags_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketDescriptorFlags.cpp) + target_include_directories(guest_socket_descriptor_flags_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_descriptor_flags_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_socket_descriptor_flags_tests) + add_test(NAME guest_socket_descriptor_flags COMMAND guest_socket_descriptor_flags_tests) + set_tests_properties(guest_socket_descriptor_flags PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_socket_descriptor_flags PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_nanosleep_tests ${LIBS_INCLUDE_DIR}/tests/GuestNanosleep.cpp) target_include_directories(guest_nanosleep_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_nanosleep_tests PRIVATE libkernel libc) @@ -1135,6 +1156,7 @@ if(BUILD_TESTING) add_test(NAME guest_kqueue COMMAND guest_kqueue_tests) set_tests_properties(guest_kqueue PROPERTIES TIMEOUT 20) if(WIN32) + target_link_libraries(guest_kqueue_tests PRIVATE advapi32) set_tests_properties(guest_kqueue PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() @@ -1148,6 +1170,16 @@ if(BUILD_TESTING) set_tests_properties(guest_socket_descriptors PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_host_stream_tests ${LIBS_INCLUDE_DIR}/tests/GuestHostStreams.cpp) + target_include_directories(guest_host_stream_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_host_stream_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_host_stream_tests) + add_test(NAME guest_host_streams COMMAND guest_host_stream_tests) + set_tests_properties(guest_host_streams PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_host_streams PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_socket_event_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketEvents.cpp) target_include_directories(guest_socket_event_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_socket_event_tests PRIVATE libkernel libc) @@ -1209,6 +1241,16 @@ if(BUILD_TESTING) set_tests_properties(guest_memory guest_mmap_fixed_anonymous PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_mprotect_unmapped_tests ${LIBS_INCLUDE_DIR}/tests/GuestMprotectUnmapped.cpp) + target_include_directories(guest_mprotect_unmapped_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_mprotect_unmapped_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_mprotect_unmapped_tests) + add_test(NAME guest_mprotect_unmapped COMMAND guest_mprotect_unmapped_tests) + set_tests_properties(guest_mprotect_unmapped PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_mprotect_unmapped PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_flexible_memory_budget_tests ${LIBS_INCLUDE_DIR}/tests/GuestFlexibleMemoryBudget.cpp) target_include_directories(guest_flexible_memory_budget_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_flexible_memory_budget_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index c68e05455b..36f91b26f0 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -68,7 +68,7 @@ static void CommitArenaRange(void* addr, size_t len, DWORD winProt) { static void* mmap_aligned(size_t len, int prot, size_t alignment, std::uintptr_t hint = 0) { auto& arena = KernelArena::Get(); - void* result = GuestArena::GuestArenaAllocateAtOrAbove_nid_postfix(hint, len, alignment); + void* result = GuestArena::GuestArenaAllocateFromHint_nid_postfix(hint, len, alignment); if (prot != PROT_NONE) { try { CommitArenaRange(result, len, WinProtFromPosix(prot)); @@ -241,19 +241,19 @@ int PhysicalFile() { return file; } -std::vector g_physicalUsed(DIRECT_MEMORY_SIZE / PS5_PAGE_SIZE); +std::vector& PhysicalUsed(); void ZeroReusedPhysical(std::uint64_t start, std::size_t bytes) { const auto first = start / PS5_PAGE_SIZE; const auto count = bytes / PS5_PAGE_SIZE; for (std::size_t page = 0; page < count;) { - if (!g_physicalUsed[first + page]) { - g_physicalUsed[first + page] = true; + if (!PhysicalUsed()[first + page]) { + PhysicalUsed()[first + page] = true; ++page; continue; } std::size_t run = 0; - while (page + run < count && g_physicalUsed[first + page + run]) ++run; + while (page + run < count && PhysicalUsed()[first + page + run]) ++run; const auto offset = (first + page) * PS5_PAGE_SIZE; void* view = ::mmap(nullptr, run * PS5_PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, PhysicalFile(), static_cast(offset)); if (view == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map reused direct memory"); @@ -337,9 +337,6 @@ struct PhysicalPage { std::uint64_t offset; }; -std::mutex g_directLock; -std::map g_physPages; - struct DirectMapping { std::uintptr_t end; std::uint64_t phys; @@ -347,13 +344,47 @@ struct DirectMapping { std::shared_ptr backing; }; -std::map g_directMappings; struct PhysicalSpan { std::uint64_t end; std::vector bases; }; -std::map g_physicalSpans; +struct ProtectedRange { + std::uintptr_t end; + int prot; +}; + +struct DirectMemoryState { +#if defined(__linux__) + std::vector physicalUsed = std::vector(DIRECT_MEMORY_SIZE / PS5_PAGE_SIZE); +#endif + std::map physicalSpans; + std::mutex directLock; + std::map physPages; + std::map directMappings; + std::mutex protectionLock; + std::map protections; + std::mutex reservationLock; + std::map reservations; +}; + +DirectMemoryState& MemoryState() { + static auto* state = new DirectMemoryState; + return *state; +} + +#if defined(__linux__) +std::vector& PhysicalUsed() { return MemoryState().physicalUsed; } +#endif + +auto& g_directLock = MemoryState().directLock; +auto& g_physPages = MemoryState().physPages; +auto& g_directMappings = MemoryState().directMappings; +auto& g_physicalSpans = MemoryState().physicalSpans; +auto& g_protectionLock = MemoryState().protectionLock; +auto& g_protections = MemoryState().protections; +auto& g_reservationLock = MemoryState().reservationLock; +auto& g_reservations = MemoryState().reservations; void SplitPhysicalSpan(std::uint64_t at) { auto span = g_physicalSpans.upper_bound(at); @@ -444,7 +475,6 @@ void ErasePhysMappings(std::uint64_t first, std::uint64_t last) { } } - void ValidatePhysicalRange(std::uint64_t phys, std::size_t len) { for (std::size_t offset = 0; offset < len; offset += PS5_PAGE_SIZE) { if (!g_physPages.contains(phys + offset)) { @@ -701,7 +731,7 @@ void* MapPlaced(void* addr, size_t len, int prot, int flags, size_t alignment) { if (addr) { #if defined(__linux__) if (ArenaAvailable()) { - void* placed = GuestArena::GuestArenaAllocateAtOrAbove_nid_postfix( + void* placed = GuestArena::GuestArenaAllocateFromHint_nid_postfix( reinterpret_cast(addr), len, alignment); if (void* committed = ArenaCommit(placed, len, prot); committed != MAP_FAILED) return committed; ArenaForget(placed, len); @@ -749,13 +779,6 @@ void* MapPlaced(void* addr, size_t len, int prot, int flags, size_t alignment) { return aligned; } -struct ProtectedRange { - std::uintptr_t end; - int prot; -}; -std::mutex g_protectionLock; -std::map g_protections; - void EraseProtections(std::uintptr_t start, std::uintptr_t end) { auto it = g_protections.lower_bound(start); if (it != g_protections.begin() && std::prev(it)->second.end > start) --it; @@ -768,9 +791,6 @@ void EraseProtections(std::uintptr_t start, std::uintptr_t end) { } } -std::mutex g_reservationLock; -std::map g_reservations; - void EraseReservations(const void* addr, size_t len) { const auto start = reinterpret_cast(addr); const auto end = start + len; @@ -1047,15 +1067,22 @@ int DoMprotect(const void* addr, size_t len, int prot) { #else mutation.RegisterMainImage(); #endif - mutation.Protect(pointer, bytes, (prot & 3) != 0, (prot & 2) != 0, (prot & GuestProtGpuReadWrite) != 0, [&] { - if (mprotect(const_cast(pointer), bytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); - }); - RecordProtection(pointer, bytes, prot); + std::vector> spans; + mutation.ForRegistered(pointer, bytes, [&](const void* span, std::size_t spanBytes) { spans.emplace_back(span, spanBytes); }); + if (spans.empty() || reinterpret_cast(spans.front().first) != first) return SCE_KERNEL_ERROR_EINVAL; + for (const auto& [span, spanBytes] : spans) mutation.RequireUnpinned(span, spanBytes); + for (const auto& [span, spanBytes] : spans) { + mutation.Protect(span, spanBytes, (prot & 3) != 0, (prot & 2) != 0, (prot & GuestProtGpuReadWrite) != 0, [&] { + if (mprotect(const_cast(span), spanBytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); + }); + RecordProtection(span, spanBytes, prot); + } return 0; } int DoMtypeprotect(const void* addr, size_t len, int type, int prot) { const int result = DoMprotect(addr, len, prot); + if (result != 0) return result; const auto address = reinterpret_cast(addr); constexpr auto pageMask = static_cast(PS5_PAGE_SIZE - 1); const auto first = address & ~pageMask; diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp index 8fa22b5fbc..fc082fb8e9 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp @@ -52,8 +52,8 @@ bool PageSpan(uint64_t start, size_t len, uint64_t& lo, uint64_t& hi) { struct PhysicalMemoryPool { static PhysicalMemoryPool& Instance() { - static PhysicalMemoryPool inst; - return inst; + static auto* inst = new PhysicalMemoryPool; + return *inst; } int Alloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut) { diff --git a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp index d8e2080266..b6bbf66d7e 100644 --- a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp +++ b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp @@ -221,6 +221,24 @@ std::vector SnapshotHints(const Snapshot& before, const Snapshot& return hints; } +std::string hostPathText(const std::wstring& path) { + const int size = ::WideCharToMultiByte(CP_UTF8, 0, path.c_str(), -1, nullptr, 0, nullptr, nullptr); + if (size <= 0) { + return ""; + } + std::string text(static_cast(size), '\0'); + if (::WideCharToMultiByte(CP_UTF8, 0, path.c_str(), -1, text.data(), size, nullptr, nullptr) != size) { + return ""; + } + text.pop_back(); + return text; +} + +std::string changeNotificationFailure(const char* watcher, const std::wstring& path, DWORD error) { + return "EVFILT_VNODE: the " + std::string(watcher) + " watch of \"" + hostPathText(path) + + "\" failed, FindFirstChangeNotification error=" + std::to_string(error); +} + HANDLE WatchParent(const std::wstring& path) { const auto separator = path.find_last_of(L"\\/"); if (separator == std::wstring::npos) { @@ -230,16 +248,16 @@ HANDLE WatchParent(const std::wstring& path) { const HANDLE handle = ::FindFirstChangeNotificationW(parent.c_str(), FALSE, PARENT_CHANGES); const DWORD error = handle == INVALID_HANDLE_VALUE ? ::GetLastError() : ERROR_SUCCESS; if (handle == INVALID_HANDLE_VALUE && error != ERROR_INVALID_FUNCTION) { - throw std::runtime_error("EVFILT_VNODE: FindFirstChangeNotification failed, error=" + std::to_string(error)); + throw std::runtime_error(changeNotificationFailure("parent", parent, error)); } return handle; } -HANDLE WatchContents(const std::wstring& path, DWORD changes) { +HANDLE WatchContents(const std::wstring& path, DWORD changes, const char* watcher) { const HANDLE handle = ::FindFirstChangeNotificationW(path.c_str(), FALSE, changes); if (handle == INVALID_HANDLE_VALUE) { const DWORD error = ::GetLastError(); - throw std::runtime_error("EVFILT_VNODE: FindFirstChangeNotification failed, error=" + std::to_string(error)); + throw std::runtime_error(changeNotificationFailure(watcher, path, error)); } return handle; } @@ -352,8 +370,8 @@ int OpenHostWatch(int descriptor, std::uint32_t notes, HostWatch& host) { try { host.parent = WatchParent(host.last.path); if (host.directory) { - host.entries = WatchContents(host.last.path, FILE_NOTIFY_CHANGE_FILE_NAME); - host.subdirectories = WatchContents(host.last.path, FILE_NOTIFY_CHANGE_DIR_NAME); + host.entries = WatchContents(host.last.path, FILE_NOTIFY_CHANGE_FILE_NAME, "entries"); + host.subdirectories = WatchContents(host.last.path, FILE_NOTIFY_CHANGE_DIR_NAME, "subdirectories"); } } catch (...) { CloseHostWatch(host); diff --git a/core/libs/prx/libkernel/File/include/HostStreams.hpp b/core/libs/prx/libkernel/File/include/HostStreams.hpp new file mode 100644 index 0000000000..d40bcb1600 --- /dev/null +++ b/core/libs/prx/libkernel/File/include/HostStreams.hpp @@ -0,0 +1,10 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_HOSTSTREAMS_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_HOSTSTREAMS_HPP + +namespace File { + +bool IsHostStream(int fd); + +} + +#endif diff --git a/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp new file mode 100644 index 0000000000..849b88004e --- /dev/null +++ b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp @@ -0,0 +1,58 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_WINDOWSSYMBOLICLINK_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_WINDOWSSYMBOLICLINK_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace File { + +class WindowsSymbolicLink { + struct ReparseData { + ULONG tag; + USHORT dataLength; + USHORT reserved; + USHORT substituteNameOffset; + USHORT substituteNameLength; + USHORT printNameOffset; + USHORT printNameLength; + ULONG flags; + }; + static constexpr ULONG relativeFlag = 1; + +public: + static bool ReadRelative(const std::filesystem::path& native, std::string& target, const char* operation) { + 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(), std::string(operation) + ": 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(), std::string(operation) + ": FSCTL_GET_REPARSE_POINT"); + ReparseData header{}; + if (size < sizeof(header)) return false; + std::memcpy(&header, data.data(), sizeof(header)); + if (header.tag != IO_REPARSE_TAG_SYMLINK || (header.flags & relativeFlag) == 0) return false; + const std::size_t begin = sizeof(header) + header.substituteNameOffset; + if (begin + header.substituteNameLength > size) + throw std::runtime_error(std::string(operation) + ": 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 diff --git a/core/libs/prx/libkernel/File/src/HostStreams.cpp b/core/libs/prx/libkernel/File/src/HostStreams.cpp new file mode 100644 index 0000000000..0dbc0a53c9 --- /dev/null +++ b/core/libs/prx/libkernel/File/src/HostStreams.cpp @@ -0,0 +1,40 @@ +#include "prx/libkernel/File/include/HostStreams.hpp" +#include +#include +#include +#ifdef _WIN32 +#include +#include +#else +#include +#endif + +namespace { + +#ifdef _WIN32 +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); +void IgnoreInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} +#endif + +bool OpenAtStart(int fd) { +#ifdef _WIN32 + const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); + const bool open = _get_osfhandle(fd) != -1; + _set_thread_local_invalid_parameter_handler(previous); + return open; +#else + return ::fcntl(fd, F_GETFD) != -1; +#endif +} + +const std::array g_hostStreams = {OpenAtStart(0), OpenAtStart(1), OpenAtStart(2)}; + +} + +namespace File { + +bool IsHostStream(int fd) { + return fd >= 0 && fd < static_cast(g_hostStreams.size()) && g_hostStreams[static_cast(fd)]; +} + +} diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index 5e6e304a73..7950599933 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -8,14 +8,17 @@ #ifdef _WIN32 #include "prx/libkernel/File/include/WindowsFileTime.hpp" +#include "prx/libkernel/File/include/WindowsSymbolicLink.hpp" #include #include #include +constexpr unsigned short SymbolicLinkMode = 0120777; struct NativeStat : __stat64 { FILE_BASIC_INFO times{}; BY_HANDLE_FILE_INFORMATION identity{}; bool hasTimes = false; bool hasIdentity = false; + bool isSymbolicLink = false; }; static int ReadHandleInfo(HANDLE handle, NativeStat* st) { if (GetFileType(handle) != FILE_TYPE_DISK) return 0; @@ -45,9 +48,20 @@ static int DoFstat(int fd, NativeStat* st) { } static int DoLstat(const std::filesystem::path& p, NativeStat* st) { const auto attributes = GetFileAttributesW(p.c_str()); - if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) - NotImplemented_nid_no_patch("lstat of a Windows symbolic link"); - return DoStat(p, st); + if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return DoStat(p, st); + std::string target; + if (!File::WindowsSymbolicLink::ReadRelative(p, target, "lstat")) + NotImplemented_nid_no_patch("lstat of a Windows reparse point other than a relative symbolic link"); + const auto handle = CreateFileW(p.c_str(), FILE_READ_ATTRIBUTES, 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) return File::WindowsFileTime::Failure(GetLastError()); + const int result = ReadHandleInfo(handle, st); + CloseHandle(handle); + if (result != 0) return result; + st->st_mode = SymbolicLinkMode; + st->st_size = static_cast<__int64>(target.size()); + st->isSymbolicLink = true; + return 0; } #else #include @@ -68,7 +82,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); + if (st.hasIdentity && !st.isSymbolicLink) 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 4989903336..0f90fb1694 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -11,6 +11,7 @@ #include "prx/libkernel/File/include/FileLock.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" +#include "prx/libkernel/File/include/HostStreams.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "SceTypes.hpp" @@ -233,6 +234,7 @@ int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std } int APS5_VABI sceKernelClose(int d) { + if (File::IsHostStream(d)) return SCE_KERNEL_ERROR_EBADF; if (GuestSockets::IsOpen(d)) return GuestSockets::Close(d) == 0 ? 0 : SceKernelError(*__error_nid_postfix()); EqueueDescriptorClosed(d); File::ForgetRandomDevice(d); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 58f00fafaa..55cfd9736b 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -16,6 +16,7 @@ #include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/File/include/FileLock.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" +#include "prx/libkernel/File/include/HostStreams.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" @@ -565,6 +566,7 @@ int APS5_VABI chflags_nid_postfix(const char* path, std::uint64_t flags) { } int APS5_VABI close_nid_postfix(int d) { + if (File::IsHostStream(d)) return PosixFailure(GUEST_EBADF); int socketError = 0; if (KernelCloseSocket_nid_no_patch(d, &socketError)) { if (socketError == 0) return 0; diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index caeb5c291d..b7e180b1cf 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -310,8 +310,11 @@ PthreadPrivate::PthreadPrivate(const PthreadAttrPrivate* attr) : _finished(false const KernelCpumask inherited = parent ? parent->affinity.load(std::memory_order_relaxed) : registry.processAffinity; affinity.store(attr && attr->_affinityExplicit ? attr->_affinity : inherited, std::memory_order_relaxed); constexpr int ExplicitSched = 0; + const bool explicitSched = attr && attr->_inheritsched == ExplicitSched; const int inheritedPolicy = parent ? parent->schedPolicy.load(std::memory_order_relaxed) : schedPolicy.load(std::memory_order_relaxed); - schedPolicy.store(attr && attr->_inheritsched == ExplicitSched ? attr->_schedpolicy : inheritedPolicy, std::memory_order_relaxed); + schedPolicy.store(explicitSched ? attr->_schedpolicy : inheritedPolicy, std::memory_order_relaxed); + const int attrPriority = attr ? attr->_schedpriority : DEFAULT_THREAD_PRIORITY; + priority.store(explicitSched || !parent ? attrPriority : parent->priority.load(std::memory_order_relaxed), std::memory_order_relaxed); if (registry.nextThread > static_cast(CPU_CLOCK_ID_MASK)) throw std::runtime_error("Guest thread CPU clock identifiers are exhausted"); cpuClockThread = registry.nextThread++; @@ -683,7 +686,6 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread p->_detached = detached; 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) diff --git a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp index 622a17bd20..b21208982d 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include namespace KernelSocketPoll { constexpr short Readable = 0x1; @@ -18,6 +19,8 @@ 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); +using StatusControl = bool (*)(int descriptor, int command, int flags, int* result, int* error); +using NativeLookup = bool (*)(int descriptor, std::shared_ptr* owner, std::uintptr_t* native); } extern "C" void KernelSetSocketPoller_nid_no_patch(KernelSocketPoll::Poller poller); @@ -25,5 +28,9 @@ extern "C" void KernelSetSocketCloser_nid_no_patch(KernelSocketPoll::Closer clos 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); +extern "C" void KernelSetSocketStatusControl_nid_no_patch(KernelSocketPoll::StatusControl control); +extern "C" bool KernelControlSocketStatus_nid_no_patch(int descriptor, int command, int flags, int* result, int* error); +extern "C" void KernelSetSocketNativeLookup_nid_no_patch(KernelSocketPoll::NativeLookup lookup); +extern "C" bool KernelFindSocketNative_nid_no_patch(int descriptor, std::shared_ptr* owner, std::uintptr_t* native); extern "C" int KernelReserveSocketNumber_nid_no_patch(std::uintptr_t native); extern "C" void KernelReleaseSocketNumber_nid_no_patch(int number); diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index e01a8b7f34..8500256e25 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -35,6 +35,8 @@ struct GuestTimeval { std::atomic g_socketPoller{nullptr}; std::atomic g_socketCloser{nullptr}; std::atomic g_socketOptionSetter{nullptr}; +std::atomic g_socketStatusControl{nullptr}; +std::atomic g_socketNativeLookup{nullptr}; bool DescriptorSet(const void* set, int descriptor) { return set != nullptr && ((static_cast(set)[descriptor / 64] >> (descriptor % 64)) & 1u) != 0; @@ -153,6 +155,24 @@ bool KernelSetSocketOption_nid_no_patch(int descriptor, int level, int option, c return setter != nullptr && setter(descriptor, level, option, value, length, error); } +void KernelSetSocketStatusControl_nid_no_patch(KernelSocketPoll::StatusControl control) { + g_socketStatusControl.store(control); +} + +bool KernelControlSocketStatus_nid_no_patch(int descriptor, int command, int flags, int* result, int* error) { + const auto control = g_socketStatusControl.load(); + return control != nullptr && control(descriptor, command, flags, result, error); +} + +void KernelSetSocketNativeLookup_nid_no_patch(KernelSocketPoll::NativeLookup lookup) { + g_socketNativeLookup.store(lookup); +} + +bool KernelFindSocketNative_nid_no_patch(int descriptor, std::shared_ptr* owner, std::uintptr_t* native) { + const auto lookup = g_socketNativeLookup.load(); + return lookup != nullptr && lookup(descriptor, owner, native); +} + int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout) { const auto* limit = static_cast(timeout); if (nfds < 0 || nfds > SelectDescriptorLimit || (limit != nullptr && (limit->seconds < 0 || limit->microseconds < 0 || limit->microseconds >= 1000000))) { diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index e33cc71239..2c9d15da2a 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -54,13 +55,29 @@ using NativeSocket = int; constexpr auto Invalid = -1; #endif int Fail(int error) { *__error_nid_postfix() = error; return -1; } +constexpr int GuestPeek = 0x2; +constexpr int GuestDontWait = 0x80; constexpr int GuestNoSignal = 0x20000; +#ifdef _WIN32 +constexpr int GuestSendFlags = GuestNoSignal; +constexpr int GuestReceiveFlags = GuestPeek; +#else +constexpr int GuestSendFlags = GuestNoSignal | GuestDontWait; +constexpr int GuestReceiveFlags = GuestPeek | GuestDontWait; +#endif int NativeSendFlags(int flags) { #ifdef _WIN32 static_cast(flags); return 0; #else - return flags & GuestNoSignal ? MSG_NOSIGNAL : 0; + return (flags & GuestNoSignal ? MSG_NOSIGNAL : 0) | (flags & GuestDontWait ? MSG_DONTWAIT : 0); +#endif +} +int NativeReceiveFlags(int flags) { +#ifdef _WIN32 + return flags & GuestPeek ? MSG_PEEK : 0; +#else + return (flags & GuestPeek ? MSG_PEEK : 0) | (flags & GuestDontWait ? MSG_DONTWAIT : 0); #endif } int NativeError() { @@ -142,6 +159,7 @@ Waker& ThreadWaker() { } std::mutex socketsMutex; std::map> sockets; +std::set closeOnExecDescriptors; #ifdef _WIN32 int ReserveNumber(NativeSocket) { const HANDLE placeholder = ::CreateFileW(L"NUL", 0, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); @@ -281,6 +299,7 @@ int GuestSockets::Close(int descriptor) { if (found == sockets.end()) return Fail(9); released = std::move(found->second); sockets.erase(found); + closeOnExecDescriptors.erase(descriptor); } EqueueDescriptorClosed(descriptor); ReleaseNumber(descriptor); @@ -295,6 +314,7 @@ void GuestSockets::Forget(int descriptor) { if (found == sockets.end()) return; released = std::move(found->second); sockets.erase(found); + closeOnExecDescriptors.erase(descriptor); } EqueueDescriptorClosed(descriptor); } @@ -488,6 +508,7 @@ int GuestSockets::Duplicate(int descriptor) { if (duplicate < 0) return -1; try { sockets.insert_or_assign(duplicate, found->second); + closeOnExecDescriptors.erase(duplicate); } catch (const std::bad_alloc&) { ReleaseNumber(duplicate); return Fail(12); @@ -517,6 +538,10 @@ int GuestSockets::DuplicateTo(int descriptor, int target) { } } if (replaced) EqueueDescriptorClosed(target); + { + std::lock_guard lock(socketsMutex); + closeOnExecDescriptors.erase(target); + } return target; } @@ -739,11 +764,37 @@ int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { va_end(arguments); #endif } + int result = 0; + int netError = 0; + if (KernelControlSocketStatus_nid_no_patch(descriptor, command, static_cast(argument), &result, &netError)) { + return netError == 0 ? result : Fail(netError); + } return File::Fcntl(descriptor, command, argument); } const auto socket = Lookup(descriptor); if (!socket) return -1; std::lock_guard lock(socket->modeMutex); + if (command == 1) { + std::lock_guard registry(socketsMutex); + return closeOnExecDescriptors.count(descriptor) != 0 ? 1 : 0; + } + if (command == 2) { +#ifdef _WIN32 + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, command); + const int flags = __builtin_va_arg(arguments, int); + __builtin_sysv_va_end(arguments); +#else + std::va_list arguments; + va_start(arguments, command); + const int flags = va_arg(arguments, int); + va_end(arguments); +#endif + std::lock_guard registry(socketsMutex); + if ((flags & 1) != 0) closeOnExecDescriptors.insert(descriptor); + else closeOnExecDescriptors.erase(descriptor); + return 0; + } if (command == 3) return 2 | (socket->nonblocking ? 4 : 0); // F_GETFL, O_RDWR if (command != 4) return Fail(22); #ifdef _WIN32 @@ -812,7 +863,12 @@ int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { auto socket = std::make_shared(native, family, plainType); guard.value = Invalid; if (nonblocking && SetNonBlocking(*socket)) return -1; - return Install(std::move(socket)); + const int number = Install(std::move(socket)); + if (number >= 0 && (type & GuestCloseOnExec) != 0) { + std::lock_guard registry(socketsMutex); + closeOnExecDescriptors.insert(number); + } + return number; } catch (const std::bad_alloc&) { return Fail(12); } @@ -908,7 +964,7 @@ int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* l std::int64_t APS5_VABI send_nid_postfix(int descriptor, const void* buffer, std::uint64_t length, int flags) { const auto socket = Lookup(descriptor); if (!socket) return -1; - if ((flags & ~GuestNoSignal) != 0) return Fail(45); + if ((flags & ~GuestSendFlags) != 0) return Fail(45); 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)); @@ -917,13 +973,13 @@ std::int64_t APS5_VABI send_nid_postfix(int descriptor, const void* buffer, std: std::int64_t APS5_VABI recv_nid_postfix(int descriptor, void* buffer, std::uint64_t length, int flags) { const auto socket = Lookup(descriptor); if (!socket) return -1; - if ((flags & ~2) != 0) return Fail(45); + if ((flags & ~GuestReceiveFlags) != 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); + NativeReceiveFlags(flags)); #ifdef _WIN32 if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) return static_cast(length); #endif @@ -1005,7 +1061,7 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st int flags, const void* address, std::uint32_t addressLength) { const auto socket = Lookup(descriptor); if (!socket) return -1; - if ((flags & ~GuestNoSignal) != 0) return Fail(45); + if ((flags & ~GuestSendFlags) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if (!buffer && length) return Fail(14); int result; @@ -1026,7 +1082,7 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u int flags, void* address, std::uint32_t* addressLength) { const auto socket = Lookup(descriptor); if (!socket) return -1; - if ((flags & ~2) != 0) return Fail(45); + if ((flags & ~GuestReceiveFlags) != 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)); @@ -1034,7 +1090,7 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u sockaddr_storage native{}; socklen_t size = sizeof(native); auto result = ::recvfrom(socket->value, static_cast(buffer), static_cast(length), - flags & 2 ? MSG_PEEK : 0, reinterpret_cast(&native), &size); + NativeReceiveFlags(flags), reinterpret_cast(&native), &size); #ifdef _WIN32 if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(length); #endif @@ -1098,7 +1154,7 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, if (total < 0) return -1; const auto socket = Lookup(descriptor); if (!socket) return -1; - if ((flags & ~2) != 0) return Fail(45); + if ((flags & ~GuestReceiveFlags) != 0) return Fail(45); const bool records = socket->type == 2 || socket->type == 5; std::vector buffer; try { @@ -1109,7 +1165,7 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, sockaddr_storage native{}; socklen_t size = sizeof(native); std::int64_t received = ::recvfrom(socket->value, buffer.data(), static_cast(buffer.size()), - flags & 2 ? MSG_PEEK : 0, reinterpret_cast(&native), &size); + NativeReceiveFlags(flags), reinterpret_cast(&native), &size); if (received < 0) { #ifdef _WIN32 if (!records || WSAGetLastError() != WSAEMSGSIZE) return Fail(NativeError()); @@ -1224,7 +1280,7 @@ int APS5_VABI sendfile_nid_postfix(int file, int descriptor, std::int64_t offset int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t count, int timeout) { if (timeout < -1) return Fail(22); if (count && !descriptors) return Fail(14); - std::vector> held; + std::vector> held; std::vector native; std::vector owners; std::vector pipes; @@ -1235,8 +1291,12 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c if (entry.descriptor < 0) continue; if ((entry.events & ~GuestPollAccepted) != 0) throw std::runtime_error("poll: unsupported event flags " + std::to_string(entry.events)); - auto socket = Find(entry.descriptor); - if (!socket) { + std::shared_ptr owner; + std::uintptr_t value = 0; + if (auto socket = Find(entry.descriptor)) { + value = static_cast(socket->value); + owner = std::move(socket); + } else if (!KernelFindSocketNative_nid_no_patch(entry.descriptor, &owner, &value)) { if (!DescriptorIsOpen_nid_no_patch(entry.descriptor)) { entry.revents = GuestPollInvalid; ++ready; @@ -1253,12 +1313,12 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c continue; } NativePollDescriptor request{}; - request.fd = socket->value; + request.fd = static_cast(value); for (const auto& flag : RequestFlags) if (entry.events & flag.guest) request.events = static_cast(request.events | flag.native); native.push_back(request); owners.push_back(i); - held.push_back(std::move(socket)); + held.push_back(std::move(owner)); } #ifndef _WIN32 const std::size_t socketCount = native.size(); diff --git a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp index dda99c2edf..178c6459e0 100644 --- a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp +++ b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp @@ -59,12 +59,26 @@ bool IsAlignedAddress(std::uintptr_t address) { return address != 0 && address % alignof(TValue) == 0; } +template +void DeferringDelivery(TFunction function) { +#ifdef _WIN32 + { + TimedWait::DeliveryDeferral deferral; + function(); + } + TimedWait::DeliverDeferred(); +#else + function(); +#endif +} + template -bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional deadlineNanos, const void* caller) { +std::optional WaitOnAddressLocked(TValue* address, TValue expected, std::optional deadlineNanos, bool& woken) { const auto key = reinterpret_cast(address); std::unique_lock lock(g_waitersLock); if (std::atomic_ref(*address).load() != expected) { - return true; + woken = true; + return std::nullopt; } AddressWaiter waiter; @@ -79,15 +93,24 @@ bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional(std::chrono::steady_clock::now() - waitStart); - const bool woken = waiter.woken; + woken = waiter.woken; if (!woken) { queue.erase(position); if (queue.empty()) { g_waiters.erase(key); } } - lock.unlock(); - KernelTraceWait_nid_postfix("addr", caller, static_cast(waited.count()), !woken); + return waited; +} + +template +bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional deadlineNanos, const void* caller) { + bool woken = false; + std::optional waited; + DeferringDelivery([&] { waited = WaitOnAddressLocked(address, expected, deadlineNanos, woken); }); + if (waited) { + KernelTraceWait_nid_postfix("addr", caller, static_cast(waited->count()), !woken); + } return woken; } @@ -97,7 +120,7 @@ int WaitOnAddress(TValue* address, TValue expected, const KernelUseconds* timeou return WaitOnAddressUntil(address, expected, deadline, caller) ? SYNC_ON_ADDRESS_OK : SCE_KERNEL_ERROR_ETIMEDOUT; } -void WakeAddress(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { +void WakeAddressLocked(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { std::lock_guard lock(g_waitersLock); const auto entry = g_waiters.find(key); if (entry == g_waiters.end()) { @@ -115,6 +138,10 @@ void WakeAddress(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { } } +void WakeAddress(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { + DeferringDelivery([&] { WakeAddressLocked(key, count, wakesAtLeastOne); }); +} + std::uint64_t SaturatingNanos(std::int64_t seconds, std::int64_t nanos) { if (seconds < 0) { return 0; diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 925ac51567..d16c8c6055 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -81,6 +81,13 @@ public: throw std::system_error(GetLastError(), std::system_category(), "Reading process arguments"); for (int index = 0; index < count; ++index) arguments.push_back(toUtf8(values.get()[index])); + wchar_t skip[2]{}; + if (GetEnvironmentVariableW(L"ANYPS5_LOADEXEC_ARGV_SKIP", skip, 2) == 1 && skip[0] == L'1') { + if (arguments.size() < 2) throw std::runtime_error("LoadExec: missing guest process arguments"); + arguments.erase(arguments.begin()); + if (_wputenv_s(L"ANYPS5_LOADEXEC_ARGV_SKIP", L"") != 0 || !SetEnvironmentVariableW(L"ANYPS5_LOADEXEC_ARGV_SKIP", nullptr)) + throw std::runtime_error("LoadExec: cannot clear the argument marker"); + } #else std::ifstream stream("/proc/self/cmdline", std::ios::binary); if (!stream) diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 1432b9f07a..e25277e821 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -570,10 +571,51 @@ int APS5_VABI clock_settime_nid_postfix(int clockId, const KernelTimespec* tp) { // Moved as-is (not yet implemented) from the monolithic libkernel/Export.cpp. // --------------------------------------------------------------------------- -// The sce* clock calls share the POSIX implementations above. The console clock is kept in UTC: -// the time zone reported here and by gettimeofday has no offset and no daylight saving. static constexpr int SceKernelErrorEfault = static_cast(0x8002000e); +struct HostZone { + std::int64_t standardEast; + std::int64_t daylight; +}; + +static HostZone HostZoneAt(std::int64_t utcTime) { + std::tm local{}; +#ifdef _WIN32 + constexpr std::int64_t cycleSeconds = 146097LL * 86400LL; + constexpr std::int64_t lastHostSecond = 32535215999LL; + const std::int64_t cycles = utcTime >= 0 && utcTime <= lastHostSecond ? 0 : (utcTime >= 0 ? utcTime / cycleSeconds : -((-(utcTime + 1)) / cycleSeconds + 1)); + const std::time_t t = static_cast(utcTime - cycles * cycleSeconds); + _tzset(); + if (localtime_s(&local, &t) != 0) throw std::runtime_error(std::string(__func__) + ": no host local time for " + std::to_string(utcTime)); + long standardBias = 0; + long daylightBias = 0; + _get_timezone(&standardBias); + _get_dstbias(&daylightBias); + return {-static_cast(standardBias), local.tm_isdst > 0 ? -static_cast(daylightBias) : 0}; +#else + const std::time_t t = static_cast(utcTime); + ::tzset(); + if (localtime_r(&t, &local) == nullptr) throw std::runtime_error(std::string(__func__) + ": no host local time for " + std::to_string(utcTime)); + const std::int64_t daylight = local.tm_isdst > 0 ? static_cast(local.tm_gmtoff) + ::timezone : 0; + return {static_cast(local.tm_gmtoff) - daylight, daylight}; +#endif +} + +static std::int64_t HostDaylightAmount(std::int64_t utcTime) { + constexpr std::int64_t monthSeconds = 2629746; + for (int month = 0; month <= 11; ++month) { + for (const int direction : {1, -1}) { + const HostZone zone = HostZoneAt(utcTime + direction * month * monthSeconds); + if (zone.daylight != 0) return zone.daylight; + } + } + return 0; +} + +static KernelTimesec ZoneState(std::int64_t time, const HostZone& zone) { + return {time, static_cast(zone.standardEast), static_cast(zone.daylight)}; +} + int APS5_VABI sceKernelClockGetres(KernelClockid clock_id, KernelTimespec* tp) { if (tp == nullptr) return SceKernelErrorEfault; if (clock_getres_nid_postfix(static_cast(clock_id), tp) != 0) return SceKernelError(*__error_nid_postfix()); @@ -588,16 +630,19 @@ int APS5_VABI sceKernelClockGettime(KernelClockid clock_id, KernelTimespec* tp) int APS5_VABI sceKernelConvertLocaltimeToUtc(int64_t local_time, int64_t reserved, int64_t* utc_time, KernelTimesec* timezone, int32_t* dst_seconds) { (void)reserved; - if (utc_time != nullptr) *utc_time = local_time; - if (timezone != nullptr) *timezone = {local_time, 0u, 0u}; - if (dst_seconds != nullptr) *dst_seconds = 0; + const HostZone guess = HostZoneAt(local_time); + const HostZone zone = HostZoneAt(local_time - guess.standardEast - guess.daylight); + if (utc_time != nullptr) *utc_time = local_time - zone.standardEast - zone.daylight; + if (timezone != nullptr) *timezone = ZoneState(local_time, zone); + if (dst_seconds != nullptr) *dst_seconds = static_cast(HostDaylightAmount(local_time - zone.standardEast - zone.daylight)); return 0; } 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; + const HostZone zone = HostZoneAt(utc_time); + if (local_time != nullptr) *local_time = utc_time + zone.standardEast + zone.daylight; + if (st != nullptr) *st = ZoneState(utc_time, zone); + if (dst_sec != nullptr) *dst_sec = static_cast(zone.daylight); return 0; } diff --git a/core/libs/prx/libkernel/Time/TimedWait.cpp b/core/libs/prx/libkernel/Time/TimedWait.cpp index 8535253896..c4ae764f64 100644 --- a/core/libs/prx/libkernel/Time/TimedWait.cpp +++ b/core/libs/prx/libkernel/Time/TimedWait.cpp @@ -241,6 +241,18 @@ void Condition::NotifyAll() { } } +DeliveryDeferral::DeliveryDeferral() { + WaitState().bound->fetch_add(1, std::memory_order_seq_cst); +} + +DeliveryDeferral::~DeliveryDeferral() { + WaitState().bound->fetch_sub(1, std::memory_order_seq_cst); +} + +void DeliverDeferred() { + DrainApcs(); +} + namespace { struct alignas(64) ParkBucket { @@ -256,11 +268,6 @@ ParkBucket& ParkBucketFor(const void* key) { return parkBuckets[(reinterpret_cast(key) * 0x9E3779B97F4A7C15ULL) >> 56]; } -struct DeliveryDeferral { - DeliveryDeferral() { WaitState().bound->fetch_add(1, std::memory_order_seq_cst); } - ~DeliveryDeferral() { WaitState().bound->fetch_sub(1, std::memory_order_seq_cst); } -}; - void UnlinkParked(ParkBucket& bucket, Waiter* waiter) { if (waiter->previous) waiter->previous->next = waiter->next; else bucket.head = waiter->next; diff --git a/core/libs/prx/libkernel/Time/include/TimedWait.hpp b/core/libs/prx/libkernel/Time/include/TimedWait.hpp index 4f7ccc32b5..446b0c73f3 100644 --- a/core/libs/prx/libkernel/Time/include/TimedWait.hpp +++ b/core/libs/prx/libkernel/Time/include/TimedWait.hpp @@ -22,6 +22,15 @@ struct Waiter; #ifdef _WIN32 constexpr unsigned PARK_SPINS = 1000; +class DeliveryDeferral { +public: + DeliveryDeferral(); + ~DeliveryDeferral(); + DeliveryDeferral(const DeliveryDeferral&) = delete; + DeliveryDeferral& operator=(const DeliveryDeferral&) = delete; +}; + +void DeliverDeferred(); void SpinPause(); Waiter* BeginPark(const void* key); bool FinishPark(const void* key, Waiter* waiter, const std::uint64_t* deadlineNanos); diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 67c4823346..99809d6212 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -1,5 +1,4 @@ #include -#include #include #include #include @@ -30,6 +29,7 @@ #include #include #include +#include "prx/libkernel/File/include/WindowsSymbolicLink.hpp" #ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE #define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x2 #endif @@ -125,49 +125,12 @@ std::int64_t CopyLinkTarget(const std::string& target, char* buffer, std::size_t 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 - std::int64_t ReadNativeLink(const std::filesystem::path& native, char* buffer, std::size_t size) { #ifdef _WIN32 const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { std::string target; - if (!ReadRelativeSymbolicLink(native, target)) { + if (!File::WindowsSymbolicLink::ReadRelative(native, target, "readlink")) { NotImplemented_nid_no_patch("readlink of a Windows reparse point other than a relative symbolic link"); return -1; } diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index d7f755740f..a4c2856b60 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -8,8 +8,19 @@ #include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#elif defined(__linux__) +#include "prx/libc/include/specifics/linux/ElfTypes.hpp" +#include +#include +#endif extern "C" { +void APS5_VABI _init_env_nid_postfix(); 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); @@ -30,6 +41,12 @@ int APS5_VABI atexit_nid_postfix(void (APS5_VABI*)()); namespace { alignas(64) std::array storage{}; +alignas(16) std::array process{}; +alignas(16) std::array libc{}; +alignas(16) std::array replacement{}; +#ifdef _WIN32 +[[gnu::section(".proctst"), gnu::used]] alignas(4) std::uint32_t processParameterMetadata[2]{}; +#endif std::size_t lastSize = 0; std::size_t lastAlignment = 0; unsigned posixCalls = 0; @@ -43,6 +60,55 @@ void require(bool condition) { if (!condition) throw std::runtime_error("application heap test failed"); } +void installProcessParameters() { +#ifdef _WIN32 + auto* image = reinterpret_cast(GetModuleHandleW(nullptr)); + require(image != nullptr); + auto* dos = reinterpret_cast(image); + auto* nt = reinterpret_cast(image + dos->e_lfanew); + auto* sections = IMAGE_FIRST_SECTION(nt); + IMAGE_SECTION_HEADER* target = nullptr; + for (unsigned index = 0; index < nt->FileHeader.NumberOfSections; ++index) { + if (std::memcmp(sections[index].Name, ".proctst", 8) == 0) { + target = §ions[index]; + break; + } + } + require(target != nullptr); + processParameterMetadata[0] = static_cast(process.data() - image); + processParameterMetadata[1] = static_cast(process.size()); + DWORD oldProtect = 0; + require(VirtualProtect(target, sizeof(*target), PAGE_READWRITE, &oldProtect) != 0); + std::memcpy(target->Name, ".procpar", 8); + target->Misc.VirtualSize = sizeof(processParameterMetadata); + DWORD restored = 0; + VirtualProtect(target, sizeof(*target), oldProtect, &restored); +#elif defined(__linux__) + bool installed = false; + dl_iterate_phdr([](dl_phdr_info* image, std::size_t, void* data) { + if (image->dlpi_name != nullptr && image->dlpi_name[0] != '\0') return 0; + for (std::uint16_t index = 0; index < image->dlpi_phnum; ++index) { + const auto& header = image->dlpi_phdr[index]; + if (header.p_type == PT_LOAD || header.p_type == PT_GNU_EH_FRAME || header.p_type < 0x60000000u) continue; + auto* mutableHeader = const_cast(&header); + const auto pageSize = static_cast(sysconf(_SC_PAGESIZE)); + const auto pageStart = reinterpret_cast(mutableHeader) & ~(pageSize - 1); + const auto pageEnd = (reinterpret_cast(mutableHeader + 1) + pageSize - 1) & ~(pageSize - 1); + if (mprotect(reinterpret_cast(pageStart), pageEnd - pageStart, PROT_READ | PROT_WRITE) != 0) return 1; + mutableHeader->p_type = 0x61000001; + mutableHeader->p_vaddr = reinterpret_cast(process.data()) - image->dlpi_addr; + mutableHeader->p_paddr = mutableHeader->p_vaddr; + mutableHeader->p_filesz = process.size(); + mutableHeader->p_memsz = process.size(); + *static_cast(data) = true; + return 1; + } + return 1; + }, &installed); + require(installed); +#endif +} + template void reject(TAction action) { bool rejected = false; @@ -106,9 +172,6 @@ void write(std::array& data, std::size_t offset, TValue value) int main(int argc, char** argv) { reject([] { ApplicationHeapAllocate_nid_no_patch(64); }); reject([] { ApplicationHeapRegister_nid_no_patch(nullptr); }); - std::array process{}; - std::array libc{}; - std::array replacement{}; write(process, 0, std::uint64_t{0x40}); write(process, 8, std::uint32_t{0x4942524f}); write(process, 0x38, libc.data()); @@ -125,6 +188,16 @@ int main(int argc, char** argv) { write(replacement, 0x40, &align); write(replacement, 0x48, &realign); write(replacement, 0x50, &posixAlign); + if (argc > 1 && std::strcmp(argv[1], "lazy") == 0) { + installProcessParameters(); + require(initializes == 0); + require(_Znwm_nid_postfix(40) == storage.data() && lastSize == 40 && initializes == 1); + _ZdlPv_nid_postfix(storage.data()); + require(frees == 1); + _init_env_nid_postfix(); + require(initializes == 1); + return 0; + } if (argc > 1 && std::strcmp(argv[1], "exit-order") == 0) { require(atexit_nid_postfix(exitCallbackAllocates) == 0); ApplicationHeapInitialize_nid_no_patch(process.data()); diff --git a/core/libs/tests/Fiber.cpp b/core/libs/tests/Fiber.cpp index 43ce315163..248325f569 100644 --- a/core/libs/tests/Fiber.cpp +++ b/core/libs/tests/Fiber.cpp @@ -172,6 +172,25 @@ void APS5_VABI HostCallEntry(std::uint64_t, std::uint64_t) { for (;;) sceFiberReturnToThread(0, &received); } +alignas(16) std::array g_chainContext; +FiberStorage g_chainStorage; +bool g_chainEnded = false; + +void APS5_VABI FrameChainEntry(std::uint64_t, std::uint64_t) { + auto* frame = static_cast(__builtin_frame_address(0)); + const auto low = reinterpret_cast(frame); + const auto high = low + (1u << 20); + for (int depth = 0; depth < 100 && frame != nullptr; ++depth) { + const auto address = reinterpret_cast(frame); + Require(address >= low && address + 2 * sizeof(std::uintptr_t) <= high, "frame chain stays on the fiber's stack"); + frame = reinterpret_cast(frame[0]); + Require(frame == nullptr || reinterpret_cast(frame) > address, "frame chain climbs the stack"); + } + g_chainEnded = frame == nullptr; + std::uint64_t received = 0; + for (;;) sceFiberReturnToThread(0, &received); +} + void RunHostCallOnSmallContext() { auto* fiber = reinterpret_cast(&g_hostCallStorage); auto* context = g_hostCallArea.data() + g_hostCallArea.size() - HostCallContextSize; @@ -184,6 +203,14 @@ void RunHostCallOnSmallContext() { } } +void RunFrameChain() { + auto* fiber = reinterpret_cast(&g_chainStorage); + Require(_sceFiberInitializeImpl_nid_postfix(fiber, "chain", FrameChainEntry, 0, g_chainContext.data(), g_chainContext.size(), nullptr, 0) == 0, "initialize chain"); + Require(sceFiberRun_nid_postfix(fiber, 0, nullptr) == 0, "run chain"); + Require(g_chainEnded, "frame chain of a fiber ends with a null frame"); + Require(sceFiberFinalize(fiber) == 0, "finalize chain"); +} + [[gnu::noinline]] std::int32_t RunFirst(std::uint64_t* returned) { g_threadFramePointer = reinterpret_cast(__builtin_frame_address(0)); const std::int32_t result = sceFiberRun_nid_postfix(g_first, 100, returned); @@ -231,4 +258,5 @@ int main() { Require(sceFiberStopContextSizeCheck() == 0, "stop size check"); RunHostCallOnSmallContext(); + RunFrameChain(); } diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 54344eb553..5231993647 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -537,6 +537,69 @@ bool Refused(std::uint32_t context, const AjmBatchInfo& info) { return false; } +void AppendOpusLength(std::vector& out, std::size_t length) { + if (length < 252) { + out.push_back(static_cast(length)); + return; + } + out.push_back(static_cast(252 + (length - 252) % 4)); + out.push_back(static_cast((length - 252) / 4)); +} + +std::vector SelfDelimited(const std::uint8_t* packet, std::size_t bytes) { + const std::uint32_t code = packet[0] & 3u; + Require(code <= 1); + std::vector out{packet[0]}; + AppendOpusLength(out, code == 0 ? bytes - 1 : (bytes - 1) / 2); + out.insert(out.end(), packet + 1, packet + bytes); + return out; +} + +void TestOpusMultistreamFamily255(std::uint32_t context) { + const bool stereo = (OPUS_STEREO[2] & 0x04u) != 0; + const std::uint32_t channels = stereo ? 4 : 2; + std::uint32_t mono = 0; + std::uint32_t multi = 0; + const std::uint32_t parameters[2][3] = {{stereo ? 2u : 1u, 48000, 0}, {channels, 48000, 255}}; + std::uint32_t* instances[2] = {&mono, &multi}; + for (std::size_t index = 0; index < 2; ++index) { + Require(sceAjmInstanceCreate(context, 24, 0, instances[index]) == 0); + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t initResult[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, *instances[index], parameters[index], sizeof(parameters[index]), initResult) == 0); + Submit(context, info); + Require(initResult[0] == 0); + } + const std::uint32_t streamChannels = channels / 2; + std::size_t offset = 0; + for (int packet = 0; packet < 3; ++packet) { + const std::size_t bytes = OPUS_STEREO[offset] | (std::size_t{OPUS_STEREO[offset + 1]} << 8u); + const std::uint8_t* data = OPUS_STEREO + offset + 2; + std::vector joined = SelfDelimited(data, bytes); + joined.insert(joined.end(), data, data + bytes); + std::vector framed{static_cast(joined.size()), static_cast(joined.size() >> 8u)}; + framed.insert(framed.end(), joined.begin(), joined.end()); + std::vector single(960 * streamChannels, -12345); + std::vector both(960 * channels, -12345); + DecodeSideband sideband{}; + RunDecode(context, mono, OPUS_STEREO + offset, 2 + bytes, single.data(), single.size() * sizeof(std::int16_t), sideband); + Require(sideband.result == 0); + RunDecode(context, multi, framed.data(), framed.size(), both.data(), both.size() * sizeof(std::int16_t), sideband); + Require(sideband.result == 0 && static_cast(sideband.inputConsumed) == framed.size() && sideband.outputWritten == 960 * channels * 2); + for (std::size_t sample = 0; sample < 960; ++sample) { + for (std::uint32_t channel = 0; channel < streamChannels; ++channel) { + Require(both[sample * channels + channel] == single[sample * streamChannels + channel]); + Require(both[sample * channels + streamChannels + channel] == single[sample * streamChannels + channel]); + } + } + offset += 2 + bytes; + } + Require(sceAjmInstanceDestroy(context, mono) == 0); + Require(sceAjmInstanceDestroy(context, multi) == 0); +} + void TestOpusMonoFamily255(std::uint32_t context) { std::uint32_t instances[2] = {}; for (std::size_t index = 0; index < 2; ++index) { @@ -569,7 +632,7 @@ void TestOpusMonoFamily255(std::uint32_t context) { } Require(peak > 900 && peak < 1200); - const std::uint32_t refusedParameters[2][3] = {{2, 48000, 255}, {1, 48000, 2}}; + const std::uint32_t refusedParameters[1][3] = {{1, 48000, 2}}; for (const auto& parameters : refusedParameters) { std::uint32_t instance = 0; Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); @@ -1605,6 +1668,7 @@ int main() { TestOpus(context); TestOpus(context, 1); TestOpusMonoFamily255(context); + TestOpusMultistreamFamily255(context); TestOpusFamilyZeroChannels(context); TestOpusSurround(context, 3, 0); for (std::size_t stream = 0; stream < 2; ++stream) { diff --git a/core/libs/tests/GuestDup.cpp b/core/libs/tests/GuestDup.cpp index 04614f8062..4e0451ae85 100644 --- a/core/libs/tests/GuestDup.cpp +++ b/core/libs/tests/GuestDup.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 854cf0fe33..2c17a9dc9c 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -651,9 +651,25 @@ int main() { Require(std::filesystem::create_directory(targetDirectory)); const auto directoryLink = root / "directory-link"; const auto danglingLink = root / "dangling-link"; + const auto absoluteLink = root / "absolute-link"; 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); + Require(CreateSymbolicLinkW(danglingLink.c_str(), L"missing.txt", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + Require(CreateSymbolicLinkW(absoluteLink.c_str(), std::filesystem::absolute(present).c_str(), + SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + const auto checkRelativeLink = [](const std::filesystem::path& link, std::int64_t targetLength) { + FileStat own{}; + return lstat_nid_postfix(link.string().c_str(), &own) == 0 && (own.st_mode & 0170000) == 0120000 && + own.st_size == targetLength && own.st_nlink == 1; + }; + Require(checkRelativeLink(fileLink, 11)); + Require(checkRelativeLink(directoryLink, 16)); + Require(checkRelativeLink(danglingLink, 11)); + FileStat followed{}; + Require(lstat_nid_postfix(fileLink.string().c_str(), &linkStatus) == 0); + Require(stat_nid_postfix(fileLink.string().c_str(), &followed) == 0 && (followed.st_mode & 0170000) == 0100000); + Require(followed.st_ino == status.st_ino && linkStatus.st_ino != status.st_ino && linkStatus.st_dev == status.st_dev); + Require(stat_nid_postfix(directoryLink.string().c_str(), &followed) == 0 && (followed.st_mode & 0170000) == 0040000); const auto checkUnsupportedLink = [](const std::filesystem::path& link) { FileStat untouched; std::memset(&untouched, 0x5a, sizeof(untouched)); @@ -663,7 +679,7 @@ int main() { try { lstat_nid_postfix(link.string().c_str(), &untouched); } catch (const std::runtime_error& error) { - rejected = std::strcmp(error.what(), "lstat of a Windows symbolic link not implemented") == 0; + rejected = std::strcmp(error.what(), "lstat of a Windows reparse point other than a relative symbolic link not implemented") == 0; } const bool errorUnchanged = *__error_nid_postfix() == sentinelError; const auto* bytes = reinterpret_cast(&untouched); @@ -671,11 +687,9 @@ int main() { 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)); - Require(std::filesystem::remove(fileLink) && std::filesystem::remove(directoryLink) && - std::filesystem::remove(danglingLink) && std::filesystem::remove(targetDirectory)); + Require(checkUnsupportedLink(absoluteLink)); + Require(std::filesystem::remove(fileLink) && std::filesystem::remove(directoryLink) && DeleteFileW(danglingLink.c_str()) != 0 && + std::filesystem::remove(absoluteLink) && std::filesystem::remove(targetDirectory)); } #endif FileStat identity{}; diff --git a/core/libs/tests/GuestFreebsdClocks.cpp b/core/libs/tests/GuestFreebsdClocks.cpp index e1f1b5256a..e74dc2cd38 100644 --- a/core/libs/tests/GuestFreebsdClocks.cpp +++ b/core/libs/tests/GuestFreebsdClocks.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include extern "C" { @@ -40,6 +41,16 @@ static constexpr std::int64_t GIVE_UP_NANOS = 10 * NANOS_PER_SECOND; static void Require(bool value) { if (!value) std::abort(); } +static void SetTimeZone(const char* zone) { +#ifdef _WIN32 + Require(_putenv_s("TZ", zone) == 0); + _tzset(); +#else + Require(setenv("TZ", zone, 1) == 0); + tzset(); +#endif +} + static bool OmittedResolutionSucceeds() { constexpr int clocks[] = {0, 1, 2, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 3, 6, 9999, -1, INT_MIN, INT_MAX}; for (const int clock : clocks) { @@ -177,6 +188,7 @@ static void XtimeTicksAreGettimeofdayMicroseconds() { } static void CalendarConversionsInitializeTheirState() { + SetTimeZone("UTC0"); constexpr std::uint64_t sentinel = 0xa5a5a5a5a5a5a5a5ull; struct GuardedState { std::uint64_t before; @@ -242,11 +254,52 @@ static void SettimeValidatesLikeFreebsd() { Require(SettimeThrows(KernelTimespec{1, NANOS_PER_SECOND - 1}, "1.999999999")); } +static std::int32_t RuntimeDaylight(std::int32_t daylight) { +#ifdef _WIN32 + long bias = 0; + Require(_get_dstbias(&bias) == 0); + return daylight == 0 ? 0 : static_cast(-bias); +#else + return daylight; +#endif +} + +static void RequireZone(const char* zone, std::int64_t utc, std::int64_t standardEast, std::int32_t zoneDaylight, std::int32_t daylightInEffect) { + SetTimeZone(zone); + zoneDaylight = RuntimeDaylight(zoneDaylight); + const std::int32_t daylight = daylightInEffect == 0 ? 0 : zoneDaylight; + const std::int64_t east = standardEast + daylight; + std::int64_t local = -1; + KernelTimesec universal{-1, 0xa5a5a5a5u, 0xa5a5a5a5u}; + std::int32_t universalDst = -1; + Require(sceKernelConvertUtcToLocaltime(utc, &local, &universal, &universalDst) == SCE_OK); + Require(local == utc + east && universalDst == daylight); + Require(universal.t == utc && universal.west_sec == static_cast(east - daylight) && universal.dst_sec == static_cast(daylight)); + std::int64_t converted = -1; + KernelTimesec state{-1, 0xa5a5a5a5u, 0xa5a5a5a5u}; + std::int32_t dst = -1; + Require(sceKernelConvertLocaltimeToUtc(local, 0, &converted, &state, &dst) == SCE_OK); + Require(converted == utc && dst == zoneDaylight); + Require(state.t == local && state.west_sec == universal.west_sec && state.dst_sec == universal.dst_sec); +} + +static void CalendarConversionsFollowTheHostZone() { + constexpr std::int64_t winter = 1768478400; + constexpr std::int64_t summer = 1782907200; + RequireZone("UTC0", summer, 0, 0, 0); + RequireZone("IST-5:30", winter, 19800, 0, 0); + RequireZone("BRT+3", summer, -10800, 0, 0); + RequireZone("EST5EDT", winter, -18000, 3600, 0); + RequireZone("EST5EDT", summer, -18000, 3600, 3600); + SetTimeZone("UTC0"); +} + int main() { if (!OmittedResolutionSucceeds()) return 1; UnsupportedResolutionStillThrows(); SettimeValidatesLikeFreebsd(); CalendarConversionsInitializeTheirState(); + CalendarConversionsFollowTheHostZone(); SecondClockReportsWholeSeconds(); KernelTimezone zone{-1, -1}; Require(gettimeofday_nid_postfix(nullptr, nullptr) == SCE_OK); diff --git a/core/libs/tests/GuestHostStreams.cpp b/core/libs/tests/GuestHostStreams.cpp new file mode 100644 index 0000000000..e31c1dad51 --- /dev/null +++ b/core/libs/tests/GuestHostStreams.cpp @@ -0,0 +1,38 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +extern "C" { +int APS5_VABI dup_nid_postfix(int); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI sceKernelClose(int); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +constexpr int GuestEbadf = 9; +constexpr int SceKernelErrorEbadf = static_cast(0x80020009); + +static bool Open(int fd) { + const int duplicate = dup_nid_postfix(fd); + if (duplicate < 0) return false; + Require(close_nid_postfix(duplicate) == 0); + return true; +} + +int main() { + bool open[3]; + for (int fd = 0; fd < 3; ++fd) open[fd] = Open(fd); + for (int fd = 0; fd < 3; ++fd) { + if (!open[fd]) continue; + Require(close_nid_postfix(fd) == -1 && *__error_nid_postfix() == GuestEbadf); + Require(sceKernelClose(fd) == SceKernelErrorEbadf); + Require(Open(fd)); + } + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + for (const int fd : pipe) Require(fd >= 3 || !open[fd]); + Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0); + return 0; +} diff --git a/core/libs/tests/GuestJpegDec.cpp b/core/libs/tests/GuestJpegDec.cpp index 1268bd57d2..0d9e9dcf43 100644 --- a/core/libs/tests/GuestJpegDec.cpp +++ b/core/libs/tests/GuestJpegDec.cpp @@ -1,12 +1,28 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "Decoder/Jpeg.hpp" +#include #include #include #include +#include #include #include +static_assert(sizeof(JpegDecCreateParam) == 0xC); +static_assert(sizeof(JpegDecParseParam) == 0x10); +static_assert(offsetof(JpegDecParseParam, jpeg_mem_size) == 0x8 && offsetof(JpegDecParseParam, decode_mode) == 0xC && offsetof(JpegDecParseParam, down_scale) == 0xE); +static_assert(sizeof(JpegDecDecodeParam) == 0x30); +static_assert(offsetof(JpegDecDecodeParam, coefficient_mem_addr) == 0x10 && offsetof(JpegDecDecodeParam, jpeg_mem_size) == 0x18); +static_assert(offsetof(JpegDecDecodeParam, image_mem_size) == 0x1C && offsetof(JpegDecDecodeParam, coefficient_mem_size) == 0x20); +static_assert(offsetof(JpegDecDecodeParam, decode_mode) == 0x24 && offsetof(JpegDecDecodeParam, down_scale) == 0x26); +static_assert(offsetof(JpegDecDecodeParam, pixel_format) == 0x28 && offsetof(JpegDecDecodeParam, alpha_value) == 0x2A && offsetof(JpegDecDecodeParam, image_pitch) == 0x2C); +static_assert(sizeof(JpegDecImageInfo) == 0x24); +static_assert(offsetof(JpegDecImageInfo, color_space) == 0x8 && offsetof(JpegDecImageInfo, component_number) == 0xA); +static_assert(offsetof(JpegDecImageInfo, component_id) == 0xC && offsetof(JpegDecImageInfo, sampling_factor) == 0x10); +static_assert(offsetof(JpegDecImageInfo, coefficient_mem_size) == 0x14 && offsetof(JpegDecImageInfo, suitable_csc_attribute) == 0x18); +static_assert(offsetof(JpegDecImageInfo, output_image_width) == 0x1C && offsetof(JpegDecImageInfo, output_image_height) == 0x20); + extern "C" { std::int32_t APS5_VABI sceJpegDecQueryMemorySize(const JpegDecCreateParam*); std::int32_t APS5_VABI sceJpegDecCreate(const JpegDecCreateParam*, void*, std::uint32_t, void**); @@ -17,21 +33,35 @@ std::int32_t APS5_VABI sceJpegDecDecode(void*, const JpegDecDecodeParam*, JpegDe static void Require(bool value) { if (!value) std::abort(); } -static std::vector TestJpeg(std::uint32_t width, std::uint32_t height) { - std::vector rgb(static_cast(width) * height * 3); - for (std::size_t i = 0; i < rgb.size(); ++i) rgb[i] = static_cast(10 + i * 13); - return Decoder::Jpeg::Encode(rgb, width, height, 3, 90); +template +static bool ThrowsRuntimeError(TFunction function) { + try { + function(); + } catch (const std::runtime_error&) { + return true; + } + return false; } -static JpegDecDecodeParam DecodeParam(const std::vector& jpeg, std::vector& image) { +static std::vector TestJpeg(std::uint32_t width, std::uint32_t height, std::uint32_t channels) { + std::vector pixels(static_cast(width) * height * channels); + for (std::size_t i = 0; i < pixels.size(); ++i) pixels[i] = static_cast(10 + i * 13); + return Decoder::Jpeg::Encode(pixels, width, height, channels, 90); +} + +static JpegDecDecodeParam DecodeParam(const std::vector& jpeg, std::vector& image, std::uint32_t pitch) { JpegDecDecodeParam param{}; param.jpeg_mem_addr = jpeg.data(); param.image_mem_addr = image.data(); + param.coefficient_mem_addr = nullptr; param.jpeg_mem_size = static_cast(jpeg.size()); param.image_mem_size = static_cast(image.size()); + param.coefficient_mem_size = 0; + param.decode_mode = 0; + param.down_scale = 1; param.pixel_format = 0; - param.reserved0 = 0; - param.image_pitch = 0; + param.alpha_value = 0xFF; + param.image_pitch = pitch; return param; } @@ -43,62 +73,95 @@ int main() { constexpr std::int32_t invalidWorkMemory = static_cast(0x80690005); constexpr std::int32_t invalidData = static_cast(0x80690010); - const JpegDecCreateParam param{sizeof(JpegDecCreateParam), 0}; + const std::vector jpeg = TestJpeg(7, 5, 3); + JpegDecImageInfo info{}; + const JpegDecParseParam parseParam{jpeg.data(), static_cast(jpeg.size()), 0, 1}; + Require(sceJpegDecParseHeader(nullptr, &info) == invalidParam); + Require(sceJpegDecParseHeader(&parseParam, nullptr) == invalidAddr); + const JpegDecParseParam emptyParam{nullptr, 0, 0, 1}; + Require(sceJpegDecParseHeader(&emptyParam, &info) == invalidAddr); + const JpegDecParseParam downScaledParse{jpeg.data(), static_cast(jpeg.size()), 0, 2}; + Require(ThrowsRuntimeError([&] { sceJpegDecParseHeader(&downScaledParse, &info); })); + const std::vector grayJpeg = TestJpeg(3, 2, 1); + const JpegDecParseParam grayParse{grayJpeg.data(), static_cast(grayJpeg.size()), 0, 1}; + Require(sceJpegDecParseHeader(&grayParse, &info) == 0); + Require(info.image_width == 3 && info.image_height == 2 && info.component_number == 1); + Require(sceJpegDecParseHeader(&parseParam, &info) == 0); + Require(info.image_width == 7 && info.image_height == 5 && info.component_number == 3); + Require(info.output_image_width == 7 && info.output_image_height == 5); + Require(info.coefficient_mem_size == 0 && info.suitable_csc_attribute == 1); + + const JpegDecCreateParam param{sizeof(JpegDecCreateParam), info.suitable_csc_attribute, info.image_width}; const std::int32_t memorySize = sceJpegDecQueryMemorySize(¶m); Require(memorySize > 0); + const JpegDecCreateParam fullParam{sizeof(JpegDecCreateParam), 2, info.image_width}; + Require(sceJpegDecQueryMemorySize(&fullParam) == memorySize); Require(sceJpegDecQueryMemorySize(nullptr) == invalidParam); - const JpegDecCreateParam badSize{sizeof(JpegDecCreateParam) - 1, 0}; + const JpegDecCreateParam badSize{sizeof(JpegDecCreateParam) - 4, 1, 7}; Require(sceJpegDecQueryMemorySize(&badSize) == invalidSize); - const JpegDecCreateParam badAttribute{sizeof(JpegDecCreateParam), 1}; - Require(sceJpegDecQueryMemorySize(&badAttribute) == invalidParam); + for (const std::uint32_t attribute : {0u, 3u}) { + const JpegDecCreateParam badAttribute{sizeof(JpegDecCreateParam), attribute, 7}; + Require(sceJpegDecQueryMemorySize(&badAttribute) == invalidParam); + } std::vector memory(static_cast(memorySize) + 8); void* handle = nullptr; Require(sceJpegDecCreate(¶m, memory.data(), 0, &handle) == invalidWorkMemory); Require(sceJpegDecCreate(¶m, memory.data(), static_cast(memorySize), nullptr) == invalidAddr); - Require(sceJpegDecCreate(¶m, memory.data(), static_cast(memorySize), &handle) == 0); + Require(sceJpegDecCreate(&fullParam, memory.data(), static_cast(memorySize), &handle) == 0); Require(handle != nullptr); Require(sceJpegDecDelete(nullptr) == invalidHandle); Require(sceJpegDecDelete(handle) == 0); Require(sceJpegDecCreate(¶m, memory.data(), static_cast(memorySize), &handle) == 0); - const std::vector jpeg = TestJpeg(7, 5); - JpegDecImageInfo info{}; - const JpegDecParseParam parseParam{jpeg.data(), static_cast(jpeg.size()), 0}; - Require(sceJpegDecParseHeader(nullptr, &info) == invalidParam); - Require(sceJpegDecParseHeader(&parseParam, nullptr) == invalidAddr); - const JpegDecParseParam emptyParam{nullptr, 0, 0}; - Require(sceJpegDecParseHeader(&emptyParam, &info) == invalidAddr); - Require(sceJpegDecParseHeader(&parseParam, &info) == 0); - Require(info.image_width == 7); - Require(info.image_height == 5); + const std::optional reference = Decoder::Jpeg::Decode(jpeg); + Require(reference && reference->width == 7 && reference->height == 5 && reference->channels == 3); std::vector image(7 * 5 * 4, 0xCC); - JpegDecDecodeParam decodeParam = DecodeParam(jpeg, image); + JpegDecDecodeParam decodeParam = DecodeParam(jpeg, image, 7 * 4); Require(sceJpegDecDecode(nullptr, &decodeParam, &info) == invalidHandle); Require(sceJpegDecDecode(handle, nullptr, &info) == invalidParam); JpegDecDecodeParam badFormat = decodeParam; badFormat.pixel_format = 2; Require(sceJpegDecDecode(handle, &badFormat, &info) == invalidParam); - const std::int32_t packed = sceJpegDecDecode(handle, &decodeParam, &info); - Require(packed == static_cast(7 << 16 | 5)); - Require(info.image_width == 7); - Require(info.image_height == 5); - Require(image[3] == 0xFF); + JpegDecDecodeParam downScaled = decodeParam; + downScaled.down_scale = 2; + Require(ThrowsRuntimeError([&] { sceJpegDecDecode(handle, &downScaled, &info); })); + JpegDecDecodeParam otherMode = decodeParam; + otherMode.decode_mode = 1; + Require(ThrowsRuntimeError([&] { sceJpegDecDecode(handle, &otherMode, &info); })); + info = {}; + Require(sceJpegDecDecode(handle, &decodeParam, &info) == static_cast(7 << 16 | 5)); + Require(info.image_width == 7 && info.image_height == 5); + Require(info.output_image_width == 7 && info.output_image_height == 5); + for (std::size_t pixel = 0; pixel < 7 * 5; ++pixel) { + for (std::size_t c = 0; c < 3; ++c) Require(image[pixel * 4 + c] == reference->pixels[pixel * 3 + c]); + Require(image[pixel * 4 + 3] == 0xFF); + } - std::vector bgra(7 * 5 * 4, 0); - JpegDecDecodeParam bgraParam = DecodeParam(jpeg, bgra); + constexpr std::uint32_t pitch = 7 * 4 + 8; + std::vector bgra(4 * pitch + 7 * 4, 0xEE); + JpegDecDecodeParam bgraParam = DecodeParam(jpeg, bgra, pitch); bgraParam.pixel_format = 1; + bgraParam.alpha_value = 0x77; Require(sceJpegDecDecode(handle, &bgraParam, nullptr) == static_cast(7 << 16 | 5)); - Require(bgra[0] == image[2]); - Require(bgra[2] == image[0]); + for (std::size_t y = 0; y < 5; ++y) { + for (std::size_t x = 0; x < 7; ++x) { + const std::uint8_t* pixel = &bgra[y * pitch + x * 4]; + const std::uint8_t* rgba = &image[(y * 7 + x) * 4]; + Require(pixel[0] == rgba[2] && pixel[1] == rgba[1] && pixel[2] == rgba[0] && pixel[3] == 0x77); + } + } + for (std::size_t y = 0; y < 4; ++y) { + for (std::size_t x = 7 * 4; x < pitch; ++x) Require(bgra[y * pitch + x] == 0xEE); + } const std::vector garbage{0x00, 0x01, 0x02, 0x03}; - JpegDecParseParam garbageParam{garbage.data(), static_cast(garbage.size()), 0}; + const JpegDecParseParam garbageParam{garbage.data(), static_cast(garbage.size()), 0, 1}; Require(sceJpegDecParseHeader(&garbageParam, &info) == invalidData); std::vector garbageOut(64, 0); - JpegDecDecodeParam garbageDecode = DecodeParam(garbage, garbageOut); + const JpegDecDecodeParam garbageDecode = DecodeParam(garbage, garbageOut, 4 * 4); Require(sceJpegDecDecode(handle, &garbageDecode, &info) == invalidData); Require(sceJpegDecDelete(handle) == 0); diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 76d1beb472..a520e90225 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -61,6 +61,14 @@ 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*); +extern std::size_t _ZN3sce4Json6String4nposE; +void APS5_VABI _ZN3sce4Json6String5clearEv(String*); +void APS5_VABI _ZN3sce4Json6String7reserveEm(String*, std::size_t); +String* APS5_VABI _ZN3sce4Json6String6appendERKS1_(String*, const String*); +String* APS5_VABI _ZN3sce4Json6String6appendEPKc(String*, const char*); +String* APS5_VABI _ZNK3sce4Json6String6substrEmm(String*, const String*, std::size_t, std::size_t); +std::size_t APS5_VABI _ZNK3sce4Json6String4findEcm(const String*, char, std::size_t); +std::size_t APS5_VABI _ZNK3sce4Json6String5rfindEcm(const String*, char, std::size_t); bool APS5_VABI _ZNK3sce4Json6Object8iteratorneERKS2_(const Iterator*, const Iterator*); void APS5_VABI _ZN3sce4Json5ValueC1Ev(Value*); void APS5_VABI _ZN3sce4Json5ValueC1Eb(Value*, bool); @@ -430,6 +438,44 @@ static void ObjectFind() { _ZN3sce4Json6ObjectD1Ev(&object); } +static void StringMethods() { + Require(_ZN3sce4Json6String4nposE == static_cast(-1)); + const auto text = [](const String& string) { return std::string(_ZNK3sce4Json6String5c_strEv(&string), _ZNK3sce4Json6String6lengthEv(&string)); }; + String base{}; + String suffix{}; + _ZN3sce4Json6StringC1EPKc(&base, "a/b/c"); + _ZN3sce4Json6StringC1EPKc(&suffix, "-tail"); + Require(_ZNK3sce4Json6String4findEcm(&base, '/', 0) == 1); + Require(_ZNK3sce4Json6String4findEcm(&base, '/', 2) == 3); + Require(_ZNK3sce4Json6String4findEcm(&base, 'z', 0) == _ZN3sce4Json6String4nposE); + Require(_ZNK3sce4Json6String5rfindEcm(&base, '/', _ZN3sce4Json6String4nposE) == 3); + Require(_ZNK3sce4Json6String5rfindEcm(&base, '/', 2) == 1); + String part{}; + Require(_ZNK3sce4Json6String6substrEmm(&part, &base, 2, 3) == &part); + Require(text(part) == "b/c"); + _ZN3sce4Json6StringD1Ev(&part); + Require(_ZNK3sce4Json6String6substrEmm(&part, &base, 5, _ZN3sce4Json6String4nposE) == &part); + Require(text(part).empty()); + _ZN3sce4Json6StringD1Ev(&part); + bool refused = false; + try { + _ZNK3sce4Json6String6substrEmm(&part, &base, 6, 1); + } catch (const std::out_of_range&) { + refused = true; + } + Require(refused); + _ZN3sce4Json6String7reserveEm(&base, 64); + Require(_ZN3sce4Json6String6appendERKS1_(&base, &suffix) == &base); + Require(_ZN3sce4Json6String6appendEPKc(&base, "!") == &base); + Require(text(base) == "a/b/c-tail!"); + _ZN3sce4Json6String6appendERKS1_(&base, &base); + Require(text(base) == "a/b/c-tail!a/b/c-tail!"); + _ZN3sce4Json6String5clearEv(&base); + Require(text(base).empty()); + _ZN3sce4Json6StringD1Ev(&suffix); + _ZN3sce4Json6StringD1Ev(&base); +} + static void NullAccess(SetNullAccessCallback setCallback) { callbackCalls = 0; lastRequested = -1; @@ -724,6 +770,7 @@ int main() { ObjectPair(); ArrayIteratorCopies(); AllocatorNotifyError(); + StringMethods(); NullAccess(_ZN3sce4Json11Initializer27setGlobalNullAccessCallBackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_); NullAccess(_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_); StaticNullAccess(); diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 78bb97ca3e..8ac99378e5 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -1,11 +1,18 @@ #include "SceTypes.hpp" +#include +#include #include #include +#include #include #include #include #include #include +#ifdef _WIN32 +#include +#include +#endif extern "C" { int APS5_VABI kqueue_nid_postfix(); @@ -111,6 +118,15 @@ template static bool Throws(TCall call) { return false; } +template static std::string ThrownMessage(TCall call) { + try { + call(); + } catch (const std::runtime_error& error) { + return error.what(); + } + return {}; +} + static void CreateAndClose() { const int kq = kqueue_nid_postfix(); Require(kq >= 0 && kq < FirstSocket); @@ -420,6 +436,86 @@ static void DeleteWatchedFile() { Require(!std::filesystem::exists(path)); } +#ifdef _WIN32 +static std::string Utf8(const std::wstring& text) { + const int size = ::WideCharToMultiByte(CP_UTF8, 0, text.c_str(), -1, nullptr, 0, nullptr, nullptr); + Require(size > 0); + std::string narrow(static_cast(size), '\0'); + Require(::WideCharToMultiByte(CP_UTF8, 0, text.c_str(), -1, narrow.data(), size, nullptr, nullptr) == size); + narrow.pop_back(); + return narrow; +} + +static PACL DenyListing(const std::wstring& directory, PSECURITY_DESCRIPTOR& owner) { + wchar_t account[256]{}; + DWORD count = static_cast(sizeof(account) / sizeof(account[0])); + Require(::GetUserNameW(account, &count) != FALSE); + PACL original = nullptr; + Require(::GetNamedSecurityInfoW(directory.c_str(), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, + nullptr, nullptr, &original, nullptr, &owner) == ERROR_SUCCESS); + _EXPLICIT_ACCESS_W denied{}; + denied.grfAccessPermissions = FILE_LIST_DIRECTORY; + denied.grfAccessMode = DENY_ACCESS; + denied.grfInheritance = NO_INHERITANCE; + denied.Trustee.TrusteeForm = TRUSTEE_IS_NAME; + denied.Trustee.TrusteeType = TRUSTEE_IS_USER; + denied.Trustee.ptstrName = account; + PACL blocked = nullptr; + Require(::SetEntriesInAclW(1, &denied, original, &blocked) == ERROR_SUCCESS); + const DWORD applied = ::SetNamedSecurityInfoW(const_cast(directory.c_str()), SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, nullptr, nullptr, blocked, nullptr); + ::LocalFree(blocked); + Require(applied == ERROR_SUCCESS); + return original; +} + +static void RequireContext(const std::string& message, const std::string& context) { + if (message.find(context) == std::string::npos) { + std::fprintf(stderr, "the vnode watch error lacks \"%s\": %s\n", context.c_str(), message.c_str()); + std::abort(); + } +} + +static void RequireContextInsensitive(std::string message, std::string context) { + const auto lower = [](char character) { return static_cast(std::tolower(static_cast(character))); }; + std::transform(message.begin(), message.end(), message.begin(), lower); + std::transform(context.begin(), context.end(), context.begin(), lower); + if (message.find(context) == std::string::npos) { + std::fprintf(stderr, "the vnode watch error lacks \"%s\"\n", context.c_str()); + std::abort(); + } +} + +static void DeniedParentWatch() { + const auto path = TemporaryName("denied"); + Require(mkdir_nid_postfix(path.c_str(), 0755) == 0); + const int file = open_nid_postfix((path + "/watched.bin").c_str(), ReadWrite | Create, 0644); + Require(file >= 0); + PSECURITY_DESCRIPTOR owner = nullptr; + const auto directory = std::filesystem::absolute(path).wstring(); + const PACL original = DenyListing(directory, owner); + const int kq = kqueue_nid_postfix(); + KernelEvent events[4]{}; + const auto ident = static_cast(file); + const std::string message = ThrownMessage([kq, ident] { + Apply(kq, Change(ident, EvfiltVnode, EvAdd, NoteWrite)); + }); + const DWORD restored = ::SetNamedSecurityInfoW(const_cast(directory.c_str()), SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, nullptr, nullptr, original, nullptr); + Require(restored == ERROR_SUCCESS); + ::LocalFree(owner); + RequireContext(message, "parent"); + RequireContext(message, "error=5"); + RequireContextInsensitive(message, Utf8(directory)); + Require(Apply(kq, Change(ident, EvfiltVnode, EvAdd | EvClear, NoteWrite)) == 0); + Require(write_nid_postfix(file, "abc", 3) == 3); + Require(Wait(kq, events, 4) == 1 && events[0].ident == ident); + Require((events[0].fflags & NoteWrite) != 0); + Require(close_nid_postfix(file) == 0); + Require(close_nid_postfix(kq) == 0); + Require(std::filesystem::remove_all(path) == 2); +} +#endif static void Timeout() { const int kq = kqueue_nid_postfix(); KernelEvent event{}; @@ -446,6 +542,9 @@ int main() { #endif DirectoryWatch(); DeleteWatchedFile(); +#ifdef _WIN32 + DeniedParentWatch(); +#endif Timeout(); return 0; } diff --git a/core/libs/tests/GuestLoadExecHost.cpp b/core/libs/tests/GuestLoadExecHost.cpp new file mode 100644 index 0000000000..bba40568d0 --- /dev/null +++ b/core/libs/tests/GuestLoadExecHost.cpp @@ -0,0 +1,160 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +extern "C" int APS5_VABI sceSystemServiceLoadExec(const char*, const char* const*); +extern "C" int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI*)(void*), void*, void*); +extern "C" int APS5_VABI getargc_nid_postfix(); +extern "C" const char** APS5_VABI getargv_nid_postfix(); + +namespace { + +bool cleaned = false; + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "LoadExec fixture: %s\n", message); + std::fflush(stderr); + std::_Exit(98); + } +} + +std::filesystem::path Root() { +#ifdef _WIN32 + std::wstring value(32768, L'\0'); + const auto length = GetEnvironmentVariableW(L"ANYPS5_LOADEXEC_TEST_ROOT", value.data(), static_cast(value.size())); + Require(length > 0 && length < value.size(), "missing fixture directory"); + value.resize(length); + return value; +#else + const auto* value = std::getenv("ANYPS5_LOADEXEC_TEST_ROOT"); + Require(value != nullptr, "missing fixture directory"); + return std::filesystem::path(reinterpret_cast(value)); +#endif +} + +void RecordPid() { + std::ofstream file(Root() / "pids.txt", std::ios::app); +#ifdef _WIN32 + file << GetCurrentProcessId() << '\n'; +#else + file << getpid() << '\n'; +#endif + file.close(); + Require(!file.fail(), "writing the process ID file"); +} + +void Record(const char* value) { + std::ofstream file(Root() / "events.txt", std::ios::app); + file << value << '\n'; + file.close(); + Require(!file.fail(), "writing the event file"); +} + +std::string Events() { + std::ifstream file(Root() / "events.txt"); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +void Shutdown() { cleaned = true; Record("shutdown"); } +void APS5_VABI Finalize(void*) { Record("guest-exit"); } +void HostFinalize() { Record("host-exit"); } + +void Register() { + LibcRegisterShutdown_nid_postfix(Shutdown); + Require(__cxa_atexit_nid_postfix(Finalize, nullptr, nullptr) == 0, "guest exit registration"); + Require(std::atexit(HostFinalize) == 0, "host exit registration"); +} + +void Arguments(const std::uintptr_t* block, const std::vector& expected) { + Require(block[0] == expected.size(), "entry argument count"); + const auto* argv = reinterpret_cast(block + 1); + Require(getargc_nid_postfix() == static_cast(expected.size()), "getargc argument count"); + const auto* kernel = getargv_nid_postfix(); + for (std::size_t index = 0; index < expected.size(); ++index) { + Require(argv[index] && expected[index] == argv[index], "entry argument value"); + Require(kernel[index] && expected[index] == kernel[index], "getargv argument value"); + } + Require(argv[expected.size()] == nullptr && kernel[expected.size()] == nullptr, "argument terminator"); +} + +const char* FirstArgument() { + return std::getenv("ANYPS5_TEST_QUOTED_ARGV0") ? "first\"and\\slash\\ Кириллица 東京" : ""; +} + +void Reject(const char* path, const char* const* argv) { + bool rejected = false; + try { sceSystemServiceLoadExec(path, argv); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected && !cleaned, "rejected target ran shutdown"); +} + +} + +extern "C" void APS5_VABI LoadExecFirst_nid_no_patch(const std::uintptr_t* block) { + if (!Events().empty()) { + Arguments(block, {"back", "argument with spaces"}); + Require(std::filesystem::equivalent(std::filesystem::current_path(), Root()), "return working directory"); + RecordPid(); + Record("returned"); + LibcTerminate_nid_no_patch(37); + } + Require(std::filesystem::equivalent(std::filesystem::current_path(), Root()), "initial working directory"); + RecordPid(); + Record("first"); + Register(); + const char* const argv[] = {FirstArgument(), "argument with spaces", "quote\"and\\slash\\", "Кириллица 東京", nullptr}; + Reject("/app0/absent.bin", argv); + Reject("/other/second.bin", argv); + Reject("/app0/../second.bin", argv); +#ifdef _WIN32 + const auto second = Root() / L"picker.second.bin.exe"; +#else + const auto second = Root() / "picker.second.bin.elf"; +#endif + auto saved = second; + saved += ".saved"; + std::filesystem::rename(second, saved); + Reject("/app0/picker/second.bin", argv); + std::filesystem::rename(saved, second); + std::filesystem::current_path(Root() / "app0" / "working"); + sceSystemServiceLoadExec("/app0/picker/second.bin", argv); + Require(false, "LoadExec returned to the first executable"); +} + +extern "C" void APS5_VABI LoadExecSecond_nid_no_patch(const std::uintptr_t* block) { + Arguments(block, {FirstArgument(), "argument with spaces", "quote\"and\\slash\\", "Кириллица 東京"}); + Require(std::filesystem::equivalent(std::filesystem::current_path(), Root()), "secondary working directory"); + std::ifstream resource(ResolvePath_nid_no_patch("/app0/resource.txt")); + std::string text; + std::getline(resource, text); + Require(text == "resource survives", "secondary resource access"); + Require(Events() == "first\nshutdown\n", "old executable did not end after its shutdown alone"); +#ifdef _WIN32 + wchar_t value[32]{}; + Require(GetEnvironmentVariableW(L"ANYPS5_LOADEXEC_PARENT", value, 32) == 0, "parent wait variable was retained"); + Require(GetEnvironmentVariableW(L"ANYPS5_LOADEXEC_ARGV_SKIP", value, 32) == 0, "argument marker was retained"); +#endif + Require(!cleaned, "secondary inherited old runtime state"); + RecordPid(); + Record("second"); + Register(); + const char* const argv[] = {"back", "argument with spaces", nullptr}; + sceSystemServiceLoadExec("/app0/eboot.bin", argv); + Require(false, "LoadExec returned to the second executable"); +} diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index f9730f94c9..a7460faf47 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -737,6 +737,23 @@ static void CheckHintedVirtualReservation() { Require(sceKernelMunmap(placed, page * 2) == 0); } +static void CheckReservationHintWithoutRoomAbove() { + constexpr std::size_t page = 0x4000; + std::uintptr_t base = 0; + std::size_t size = 0; + GuestArena::GuestArenaRange_nid_postfix(&base, &size); + const auto end = base + size; + void* aboveTheArena = reinterpret_cast(end + 0x40000000); + Require(sceKernelReserveVirtualRange(&aboveTheArena, page * 2, 0, 0) == 0); + Require(GuestArena::GuestArenaContains_nid_postfix(aboveTheArena, page * 2)); + void* const lastPage = reinterpret_cast(end - page); + void* belowTheHint = lastPage; + Require(sceKernelReserveVirtualRange(&belowTheHint, page * 2, 0, 0) == 0); + Require(belowTheHint < lastPage && GuestArena::GuestArenaContains_nid_postfix(belowTheHint, page * 2)); + Require(sceKernelMunmap(belowTheHint, page * 2) == 0); + Require(sceKernelMunmap(aboveTheArena, page * 2) == 0); +} + static void CheckFixedVirtualReservation() { constexpr std::size_t page = 0x4000; void* probe = nullptr; @@ -2558,6 +2575,7 @@ int main(int argc, char** argv) { CheckGpuMapsAreObserved(); CheckClearingTheObserverWaitsForItsCalls(); CheckHintedVirtualReservation(); + CheckReservationHintWithoutRoomAbove(); CheckFixedVirtualReservation(); CheckReservedRangeIsNotCommitted(); CheckHintInsideReservation(); diff --git a/core/libs/tests/GuestMemoryLifetime.cpp b/core/libs/tests/GuestMemoryLifetime.cpp new file mode 100644 index 0000000000..80a0dec272 --- /dev/null +++ b/core/libs/tests/GuestMemoryLifetime.cpp @@ -0,0 +1,75 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +int fixtureDestroyed = 0; +using Exercise = int (*)(); +Exercise exercise = nullptr; + +void ProbeAfterStaticDestruction() { + if (fixtureDestroyed != 1 || exercise == nullptr) { + std::fputs("memory lifetime: fixture finalizer did not run before the exit probe\n", stderr); + std::_Exit(1); + } + if (exercise() != 0) std::_Exit(1); + std::fputs("memory lifetime: allocation, backing, mapping and explicit free survived native exit\n", stderr); +} + +int RunChild(const char* module) { + if (std::atexit(ProbeAfterStaticDestruction) != 0) return 2; + auto* handle = dlopen(module, RTLD_NOW | RTLD_LOCAL); + if (handle == nullptr) { + std::fprintf(stderr, "memory lifetime: load failed: %s\n", dlerror()); + std::_Exit(2); + } + const auto prepare = reinterpret_cast(dlsym(handle, "GuestMemoryLifetimePrepare")); + exercise = reinterpret_cast(dlsym(handle, "GuestMemoryLifetimeExercise")); + if (prepare == nullptr || exercise == nullptr || prepare(&fixtureDestroyed) != 0) std::_Exit(2); + std::exit(0); +} + +} + +int main(int argc, char** argv) { + if (argc == 3 && std::strcmp(argv[1], "--child") == 0) return RunChild(argv[2]); + if (argc != 2) return 2; + const auto child = fork(); + if (child == -1) return 2; + if (child == 0) { + execl(argv[0], argv[0], "--child", argv[1], static_cast(nullptr)); + std::_Exit(2); + } + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(20); + int status = 0; + for (;;) { + const auto result = waitpid(child, &status, WNOHANG); + if (result == child) break; + if (result < 0) { + if (errno == EINTR) continue; + kill(child, SIGKILL); + while (waitpid(child, &status, 0) < 0 && errno == EINTR) {} + return 2; + } + if (std::chrono::steady_clock::now() >= deadline) { + kill(child, SIGKILL); + while (waitpid(child, &status, 0) < 0 && errno == EINTR) {} + std::fputs("memory lifetime: exit probe timed out\n", stderr); + return 1; + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + std::fprintf(stderr, "memory lifetime: child failed during native exit (status=%d)\n", status); + return 1; + } + return 0; +} diff --git a/core/libs/tests/GuestMemoryLifetimeModule.cpp b/core/libs/tests/GuestMemoryLifetimeModule.cpp new file mode 100644 index 0000000000..173ac84e32 --- /dev/null +++ b/core/libs/tests/GuestMemoryLifetimeModule.cpp @@ -0,0 +1,126 @@ +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include +#include +#include +#include + +namespace { + +int* fixtureDestroyed = nullptr; +std::int64_t physical = -1; +void* mapping = nullptr; +bool watched = false; + +struct StaticExitMarker { + ~StaticExitMarker() { if (fixtureDestroyed != nullptr) *fixtureDestroyed = 1; } +} marker; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void CheckPhysical(std::int64_t expected) { + std::int64_t start = -1, end = -1; + int type = -1; + Require(DirectMemoryFind(expected, false, &start, &end, &type), "physical pool lost a live allocation during exit"); + Require(start == expected && end == expected + static_cast(PS5_PAGE_SIZE) && type == 0, "physical pool range changed during exit"); +} + +void CheckMapping(void* address, std::int64_t expected) { + std::uintptr_t start = 0, end = 0; + std::uint64_t offset = 0; + int type = -1; + Require(QueryDirectMapping(reinterpret_cast(address), &start, &end, &offset, &type), "direct backing map lost a live allocation during exit"); + Require(start == reinterpret_cast(address) && end == start + PS5_PAGE_SIZE && offset == static_cast(expected) && type == 0, "direct backing range changed during exit"); + GuestAllocations::Mutation mutation; + Require(mutation.Covers(address, PS5_PAGE_SIZE), "guest allocation registry lost a live mapping during exit"); + Require(mutation.Find(address).bytes == PS5_PAGE_SIZE, "guest allocation size changed during exit"); +} + +void CheckUnmapped(void* address) { + std::uintptr_t begin = 0, end = 0; + std::uint64_t offset = 0; + int type = -1; + Require(!QueryDirectMapping(reinterpret_cast(address), &begin, &end, &offset, &type), "explicit unmap did not remove the direct mapping"); + int protection = -1; + Require(!GuestProtection(reinterpret_cast(address), &protection), "explicit unmap did not remove protection metadata"); + GuestAllocations::Mutation mutation; + Require(!mutation.Overlaps(address, PS5_PAGE_SIZE), "explicit unmap did not remove the guest allocation"); +} + +void IgnoreWritten(void*, std::uintptr_t, std::uintptr_t) {} + +} + +extern "C" int GuestMemoryLifetimePrepare(int* destroyed) { + try { + fixtureDestroyed = destroyed; + Require(DirectMemoryAlloc(0, DIRECT_MEMORY_SIZE, PS5_PAGE_SIZE, PS5_PAGE_SIZE, 0, &physical) == 0, "prepare physical allocation failed"); + Require(DoMapDirect(&mapping, PS5_PAGE_SIZE, 3, 0, physical, PS5_PAGE_SIZE) == 0, "prepare direct mapping failed"); + CheckPhysical(physical); + CheckMapping(mapping, physical); + std::memset(mapping, 0x6d, PS5_PAGE_SIZE); + watched = GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix(); + if (watched) Require(GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(reinterpret_cast(mapping), PS5_PAGE_SIZE), "prepare write watch missing"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "memory lifetime prepare: %s\n", error.what()); + return 1; + } +} + +extern "C" int GuestMemoryLifetimeExercise() { + const char* stage = "physical allocation query"; + try { + CheckPhysical(physical); + stage = "backing mapping query"; + CheckMapping(mapping, physical); + Require(static_cast(mapping)[0] == 0x6d, "physical data changed during exit"); + stage = "write watch"; + if (watched) { + Require(GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(reinterpret_cast(mapping), PS5_PAGE_SIZE), "write watch lost a live mapping during exit"); + Require(GuestWriteWatch::GuestWriteWatchCollect_nid_postfix(reinterpret_cast(mapping), PS5_PAGE_SIZE, IgnoreWritten, nullptr), "write watch collection failed during exit"); + static_cast(mapping)[0] = 0x2a; + } + stage = "mapping protection"; + Require(DoMprotect(mapping, PS5_PAGE_SIZE, 1) == 0, "late read-only protection failed"); + int protection = -1; + Require(GuestProtection(reinterpret_cast(mapping), &protection) && protection == 1, "late protection registry failed"); + Require(DoMprotect(mapping, PS5_PAGE_SIZE, 3) == 0, "late read/write protection failed"); + stage = "explicit unmap and free"; + Require(DoMunmap(mapping, PS5_PAGE_SIZE) == 0, "late direct unmap failed"); + CheckUnmapped(mapping); + DirectMemoryFree(physical, PS5_PAGE_SIZE); + std::int64_t start = -1, end = -1; + int type = -1; + Require(!DirectMemoryFind(physical, false, &start, &end, &type), "explicit physical free did not remove the allocation"); + + stage = "physical offset reuse"; + std::int64_t reused = -1; + Require(DirectMemoryAlloc(physical, physical + PS5_PAGE_SIZE, PS5_PAGE_SIZE, PS5_PAGE_SIZE, 0, &reused) == 0 && reused == physical, "late physical allocation failed"); + mapping = nullptr; + Require(DoMapDirect(&mapping, PS5_PAGE_SIZE, 3, 0, reused, PS5_PAGE_SIZE) == 0, "late direct mapping failed"); + CheckPhysical(reused); + CheckMapping(mapping, reused); + const auto* bytes = static_cast(mapping); + for (std::size_t i = 0; i < PS5_PAGE_SIZE; ++i) Require(bytes[i] == 0, "reused physical backing was not zeroed"); + std::memset(mapping, 0x17, PS5_PAGE_SIZE); + Require(DoMunmap(mapping, PS5_PAGE_SIZE) == 0, "late reused mapping unmap failed"); + CheckUnmapped(mapping); + DirectMemoryFree(reused, PS5_PAGE_SIZE); + + stage = "late arena reservation"; + void* reserved = nullptr; + Require(DoReserveVirtual(&reserved, PS5_PAGE_SIZE, 0, PS5_PAGE_SIZE) == 0, "late arena reservation failed"); + std::uintptr_t begin = 0, finish = 0; + Require(GuestReservation(reinterpret_cast(reserved), &begin, &finish) && begin == reinterpret_cast(reserved) && finish == begin + PS5_PAGE_SIZE, "late reservation registry failed"); + Require(DoMunmap(reserved, PS5_PAGE_SIZE) == 0, "late reserved mapping unmap failed"); + Require(!GuestReservation(reinterpret_cast(reserved), &begin, &finish), "explicit unmap did not remove the reservation"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "memory lifetime exit probe (%s): %s\n", stage, error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestMlock.cpp b/core/libs/tests/GuestMlock.cpp index 71ed127626..8bad49dd13 100644 --- a/core/libs/tests/GuestMlock.cpp +++ b/core/libs/tests/GuestMlock.cpp @@ -42,7 +42,8 @@ static void RequireLockAllUnsupported(int flags) { Require(rejected); } -#if defined(__linux__) +#if defined(__linux__) && !defined(ANYPS5_ENABLE_ASAN) +#define HAS_LOCKED_KILOBYTES static std::size_t LockedKilobytes() { std::ifstream status("/proc/self/status"); std::string line; @@ -68,11 +69,11 @@ int main() { *__error_nid_postfix() = untouched; Require(mlock_nid_postfix(mapped, 0) == 0); Require(*__error_nid_postfix() == untouched); -#if defined(__linux__) +#ifdef HAS_LOCKED_KILOBYTES const auto lockedBefore = LockedKilobytes(); #endif Require(mlock_nid_postfix(bytes + 1, length - page) == 0); -#if defined(__linux__) +#ifdef HAS_LOCKED_KILOBYTES Require(LockedKilobytes() - lockedBefore == length / 1024); #endif Require(mlock_nid_postfix(mapped, length) == 0); @@ -83,7 +84,7 @@ int main() { Require(munlock_nid_postfix(mapped, 0) == 0); Require(*__error_nid_postfix() == untouched); Require(munlock_nid_postfix(bytes + 1, length - page) == 0); -#if defined(__linux__) +#ifdef HAS_LOCKED_KILOBYTES Require(LockedKilobytes() == lockedBefore); #elif defined(_WIN32) Require(!VirtualUnlock(mapped, length) && GetLastError() == ERROR_NOT_LOCKED); @@ -93,18 +94,18 @@ int main() { Require(*__error_nid_postfix() == untouched); bytes[0] = 7; Require(mlock_nid_postfix(mapped, page) == 0); -#if defined(__linux__) +#ifdef HAS_LOCKED_KILOBYTES Require(LockedKilobytes() - lockedBefore == page / 1024); #endif Require(munlock_nid_postfix(mapped, length) == 0); -#if defined(__linux__) +#ifdef HAS_LOCKED_KILOBYTES Require(LockedKilobytes() == lockedBefore); #elif defined(_WIN32) Require(!VirtualUnlock(mapped, page) && GetLastError() == ERROR_NOT_LOCKED); #endif Require(mlock_nid_postfix(mapped, page) == 0); Require(munlock_nid_postfix(mapped, page) == 0); -#if defined(__linux__) +#ifdef HAS_LOCKED_KILOBYTES Require(LockedKilobytes() == lockedBefore); #elif defined(_WIN32) Require(!VirtualUnlock(mapped, page) && GetLastError() == ERROR_NOT_LOCKED); @@ -119,18 +120,22 @@ int main() { void* reserved = nullptr; Require(sceKernelReserveVirtualRange(&reserved, page, 0, 0) == 0); +#ifndef ANYPS5_ENABLE_ASAN *__error_nid_postfix() = 0; Require(mlock_nid_postfix(reserved, page) == -1 && *__error_nid_postfix() == 12); +#endif *__error_nid_postfix() = untouched; Require(munlock_nid_postfix(reserved, page) == 0); Require(*__error_nid_postfix() == untouched); Require(sceKernelMunmap(reserved, page) == 0); Require(sceKernelMunmap(mapped, length) == 0); +#ifndef ANYPS5_ENABLE_ASAN *__error_nid_postfix() = 0; Require(mlock_nid_postfix(mapped, page) == -1 && *__error_nid_postfix() == 12); *__error_nid_postfix() = 0; Require(munlock_nid_postfix(mapped, page) == -1 && *__error_nid_postfix() == 12); +#endif RequireLockAllInvalid(0); RequireLockAllInvalid(4); diff --git a/core/libs/tests/GuestMprotectUnmapped.cpp b/core/libs/tests/GuestMprotectUnmapped.cpp new file mode 100644 index 0000000000..ed26e5bfa3 --- /dev/null +++ b/core/libs/tests/GuestMprotectUnmapped.cpp @@ -0,0 +1,82 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include + +extern "C" { +void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t) noexcept; +int APS5_VABI munmap_nid_postfix(void*, std::size_t) noexcept; +int APS5_VABI mprotect_nid_postfix(void*, std::size_t, int) noexcept; +int APS5_VABI sceKernelMprotect(const void*, std::size_t, int); +int APS5_VABI sceKernelMtypeprotect(const void*, std::size_t, int, int); +int APS5_VABI sceKernelVirtualQuery(const void*, int, VirtualQueryInfo*, std::uint64_t); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr std::size_t Page = 0x4000; +constexpr int ErrorInvalid = 22; +constexpr int ProtectRead = 1; +constexpr int ProtectReadWrite = 3; + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s\n", what); + std::abort(); +} + +int Protect(const void* address, std::size_t bytes, int protection) { + try { + return sceKernelMprotect(address, bytes, protection); + } catch (const std::exception& error) { + std::fprintf(stderr, "sceKernelMprotect threw: %s\n", error.what()); + std::abort(); + } +} + +int ProtectionAt(const unsigned char* address) { + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(address, 0, &info, sizeof(info)) == 0, "virtual query of a mapped page failed"); + return info.protection; +} + +unsigned char* Map(std::size_t pages) { + void* raw = mmap_nid_postfix(nullptr, Page * pages, ProtectReadWrite, 0x1002, -1, 0); + Require(raw != reinterpret_cast(static_cast(-1)), "mmap failed"); + return static_cast(raw); +} + +} + +int main() { + unsigned char* memory = Map(3); + Require(munmap_nid_postfix(memory + Page, Page) == 0, "munmap of the middle page failed"); + + Require(Protect(memory, Page * 3, ProtectRead) == 0, "a range with a gap in the middle must succeed for its mapped parts"); + Require(ProtectionAt(memory) == ProtectRead && ProtectionAt(memory + Page * 2) == ProtectRead, "the mapped parts on both sides of the gap must be protected"); + Require(mprotect_nid_postfix(memory, Page * 3, ProtectReadWrite) == 0, "mprotect over a range with a gap must succeed for its mapped parts"); + Require(ProtectionAt(memory) == ProtectReadWrite && ProtectionAt(memory + Page * 2) == ProtectReadWrite, "mprotect must change the mapped parts on both sides of the gap"); + Require(sceKernelMtypeprotect(memory, Page * 3, 0, ProtectRead) == 0, "sceKernelMtypeprotect over a range with a gap must succeed for its mapped parts"); + + Require(Protect(memory + Page - 8, 16, ProtectReadWrite) == 0, "an unaligned range that reaches into the unmapped page protects the mapped page"); + Require(Protect(memory + 100, Page - 200, ProtectReadWrite) == 0, "an unaligned range inside the first page must work"); + + Require(Protect(memory + Page, Page, ProtectRead) == SCE_KERNEL_ERROR_EINVAL, "a range that is completely unmapped must return EINVAL"); + Require(Protect(memory + Page * 2, Page, ProtectReadWrite) == 0, "protecting the last page alone must work"); + Require(Protect(memory + Page, Page * 2, ProtectRead) == SCE_KERNEL_ERROR_EINVAL, "a range whose start is unmapped must return EINVAL even when later pages are mapped"); + Require(ProtectionAt(memory + Page * 2) == ProtectReadWrite, "a refused range must not change the mapped pages behind its start"); + Require(mprotect_nid_postfix(memory + Page, Page, ProtectRead) == -1 && *__error_nid_postfix() == ErrorInvalid, "mprotect over a range that is completely unmapped must fail with EINVAL (22)"); + + Require(munmap_nid_postfix(memory, Page) == 0 && munmap_nid_postfix(memory + Page * 2, Page) == 0, "munmap of the remaining pages failed"); + + unsigned char* tail = Map(2); + Require(munmap_nid_postfix(tail + Page, Page) == 0, "munmap of the last page failed"); + Require(Protect(tail, Page * 2, ProtectRead) == 0, "a range that ends in an unmapped page must succeed for its mapped part"); + Require(ProtectionAt(tail) == ProtectRead, "the mapped part before a trailing gap must be protected"); + Require(munmap_nid_postfix(tail, Page) == 0, "munmap of the remaining page failed"); + return 0; +} diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp new file mode 100644 index 0000000000..f0196de5a7 --- /dev/null +++ b/core/libs/tests/GuestNetCtl.cpp @@ -0,0 +1,136 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#else +#include +#include +#include +#include +#endif +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNetCtlGetInfo(int, NetCtlInfo*); +int APS5_VABI sceNetCtlGetState(int*); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kErrInvalidCode = static_cast(0x80412106); +constexpr int kErrInvalidAddr = static_cast(0x80412107); +constexpr int kErrNotConnected = static_cast(0x80412108); +constexpr int kStateIpObtained = 3; +constexpr std::uint8_t kFill = 0xa5; +constexpr std::size_t kAddressLength = 16; + +NetCtlInfo Query(int code) { + NetCtlInfo info; + std::memset(&info, kFill, sizeof(info)); + Require(sceNetCtlGetInfo(code, &info) == 0); + return info; +} + +void RequireUntouchedFrom(const NetCtlInfo& info, std::size_t offset) { + for (std::size_t i = offset; i < sizeof(info.opaque); ++i) Require(info.opaque[i] == kFill); +} + +std::uint32_t QueryWord(int code) { + const auto info = Query(code); + std::uint32_t value = 0; + std::memcpy(&value, info.opaque, sizeof(value)); + RequireUntouchedFrom(info, sizeof(value)); + return value; +} + +in_addr QueryAddress(int code) { + const auto info = Query(code); + char text[kAddressLength]; + std::memcpy(text, info.opaque, sizeof(text)); + Require(std::memchr(text, '\0', sizeof(text)) != nullptr); + RequireUntouchedFrom(info, kAddressLength); + in_addr address{}; + Require(inet_pton(AF_INET, text, &address) == 1); + return address; +} + +bool Binds(in_addr address) { + sockaddr_in local{}; + local.sin_family = AF_INET; + local.sin_addr = address; +#ifdef _WIN32 + const SOCKET descriptor = socket(AF_INET, SOCK_DGRAM, 0); + Require(descriptor != INVALID_SOCKET); + const bool bound = bind(descriptor, reinterpret_cast(&local), sizeof(local)) == 0; + closesocket(descriptor); +#else + const int descriptor = socket(AF_INET, SOCK_DGRAM, 0); + Require(descriptor >= 0); + const bool bound = bind(descriptor, reinterpret_cast(&local), sizeof(local)) == 0; + close(descriptor); +#endif + return bound; +} + +} + +int main() { +#ifdef _WIN32 + WSADATA data{}; + Require(WSAStartup(MAKEWORD(2, 2), &data) == 0); +#endif + NetCtlInfo info{}; + Require(sceNetCtlGetInfo(0, &info) == kErrInvalidCode); + Require(sceNetCtlGetInfo(22, &info) == kErrInvalidCode); + Require(sceNetCtlGetInfo(14, nullptr) == kErrInvalidAddr); + + const int result = sceNetCtlGetInfo(14, &info); + if (result == kErrNotConnected) { + std::puts("skipped, the host has no IPv4 address"); + return 77; + } + Require(result == 0); + int state = -1; + Require(sceNetCtlGetState(&state) == 0 && state == kStateIpObtained); + + Require(QueryWord(1) == 0); + const auto ether = Query(2); + const std::uint8_t mac[6] = {0x02, 0x50, 0x53, 0x35, 0x00, 0x01}; + Require(std::memcmp(ether.opaque, mac, sizeof(mac)) == 0); + RequireUntouchedFrom(ether, sizeof(mac)); + const auto mtu = QueryWord(3); + Require(mtu >= 68 && mtu <= 65535); + Require(QueryWord(4) == 1); + Require(QueryWord(11) == 0); + Require(QueryWord(19) == 0); + + const auto address = QueryAddress(14); + const auto host = ntohl(address.s_addr); + Require(host != 0 && (host >> 24) != 127); + Require(Binds(address)); + const auto mask = ntohl(QueryAddress(15).s_addr); + Require(mask != 0 && (~mask & (~mask + 1)) == 0); + QueryAddress(16); + QueryAddress(17); + QueryAddress(18); + + for (const int code : {5, 6, 7, 8, 9, 10, 12, 13, 20, 21}) { + bool threw = false; + try { + sceNetCtlGetInfo(code, &info); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + } +} diff --git a/core/libs/tests/GuestNpCommerceDialog.cpp b/core/libs/tests/GuestNpCommerceDialog.cpp new file mode 100644 index 0000000000..7b7239959f --- /dev/null +++ b/core/libs/tests/GuestNpCommerceDialog.cpp @@ -0,0 +1,64 @@ +#include "SceTypes.hpp" + +#include +#include +#include + +extern "C" { +int APS5_VABI sceNpCommerceDialogInitialize(); +int APS5_VABI sceNpCommerceDialogOpen(const void* param); +int APS5_VABI sceNpCommerceDialogOpen2(void); +int APS5_VABI sceNpCommerceDialogUpdateStatus(void); +int APS5_VABI sceNpCommerceDialogGetStatus(void); +int APS5_VABI sceNpCommerceDialogGetResult(void* result); +int APS5_VABI sceNpCommerceDialogClose(void); +int APS5_VABI sceNpCommerceDialogTerminate(); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int statusNone = 0; +constexpr int statusInitialized = 1; +constexpr int statusFinished = 3; +constexpr std::int32_t resultUserCanceled = 1; + +void RequireCanceledResult() { + std::uint8_t result[0x20]; + std::memset(result, 0x33, sizeof(result)); + Require(sceNpCommerceDialogGetResult(result) == 0); + std::int32_t value = 0; + std::memcpy(&value, result, sizeof(value)); + Require(value == resultUserCanceled); + Require(result[4] == 0); + Require(result[5] == 0x33); +} + +} + +int main() { + Require(sceNpCommerceDialogGetStatus() == statusNone); + Require(sceNpCommerceDialogInitialize() == 0); + Require(sceNpCommerceDialogGetStatus() == statusInitialized); + + Require(sceNpCommerceDialogOpen2() == 0); + Require(sceNpCommerceDialogUpdateStatus() == statusFinished); + Require(sceNpCommerceDialogGetStatus() == statusFinished); + RequireCanceledResult(); + + Require(sceNpCommerceDialogClose() == 0); + Require(sceNpCommerceDialogGetStatus() == statusFinished); + Require(sceNpCommerceDialogTerminate() == 0); + Require(sceNpCommerceDialogGetStatus() == statusNone); + + alignas(8) std::uint8_t param[0x40] = {}; + Require(sceNpCommerceDialogInitialize() == 0); + Require(sceNpCommerceDialogOpen(param) == 0); + Require(sceNpCommerceDialogOpen2() == 0); + Require(sceNpCommerceDialogUpdateStatus() == statusFinished); + RequireCanceledResult(); + Require(sceNpCommerceDialogTerminate() == 0); + Require(sceNpCommerceDialogGetStatus() == statusNone); + return 0; +} diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 4cb68bd29e..f30bd4d91d 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -297,6 +297,8 @@ int main() { Require(scePadSetVibrationMode(handle, 2) == 0); Require(scePadSetVibrationMode(handle, 3) == 0); Require(scePadSetVibrationMode(handle, 4) == PAD_ERROR_INVALID_ARG); + Require(scePadSetVibrationMode(handle, 5) == 0); + Require(scePadSetVibrationMode(handle, 6) == PAD_ERROR_INVALID_ARG); Require(scePadSetVibrationMode(handle, -1) == PAD_ERROR_INVALID_ARG); Require(scePadGetHandle(0xff, 16, 0) == handle); Require(scePadGetHandle(user, 16, 0) == noHandle); diff --git a/core/libs/tests/GuestPadVibrationMode.cpp b/core/libs/tests/GuestPadVibrationMode.cpp new file mode 100644 index 0000000000..1d826fedb1 --- /dev/null +++ b/core/libs/tests/GuestPadVibrationMode.cpp @@ -0,0 +1,33 @@ +#include "SceTypes.hpp" +#include "prx/libScePad/include/Pad.hpp" +#include +#include + +extern "C" { +int APS5_VABI scePadInit_nid_postfix(void); +int APS5_VABI scePadOpen_nid_postfix(int userId, int type, int index, const void* param); +int APS5_VABI scePadClose_nid_postfix(int handle); +int APS5_VABI scePadSetVibrationMode(int handle, int mode); +} + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + for (int handle : {-1, 0, 1, 12345}) + for (int mode : {-1, 0, 1, 4}) + Require(scePadSetVibrationMode(handle, mode) == PAD_ERROR_NOT_INITIALIZED); + + Require(scePadInit_nid_postfix() == PAD_OK); + for (int handle : {-1, 0, 12345}) { + Require(scePadSetVibrationMode(handle, -1) == PAD_ERROR_INVALID_ARG); + Require(scePadSetVibrationMode(handle, 0) == PAD_ERROR_INVALID_ARG); + for (int mode : {1, 2, 3, 4, 5}) + Require(scePadSetVibrationMode(handle, mode) == PAD_ERROR_INVALID_HANDLE); + } + + const int handle = scePadOpen_nid_postfix(0x10000000, 0, 0, nullptr); + Require(handle > 0); + Require(scePadSetVibrationMode(handle, 0) == PAD_ERROR_INVALID_ARG); + Require(scePadSetVibrationMode(handle, 1) == PAD_OK); + Require(scePadClose_nid_postfix(handle) == PAD_OK); +} diff --git a/core/libs/tests/GuestPathLifetime.cpp b/core/libs/tests/GuestPathLifetime.cpp new file mode 100644 index 0000000000..44e8f59072 --- /dev/null +++ b/core/libs/tests/GuestPathLifetime.cpp @@ -0,0 +1,80 @@ +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" char* APS5_VABI getcwd_nid_postfix(char*, std::size_t); + +namespace { + +int events[2]; +char expectedRoot[4096]; + +void CheckPathsDuringExit() { + bool passed = false; + try { + std::thread worker([&] { + try { + const std::filesystem::path root(expectedRoot); + char cwd[32] = {}; + passed = getcwd_nid_postfix(cwd, sizeof(cwd)) == cwd && + std::strcmp(cwd, "/") == 0 && + ResolvePath_nid_no_patch(".").lexically_relative(root) == "." && + ResolvePath_nid_no_patch("/").lexically_relative(root) == "." && + ResolvePath_nid_no_patch("/lifetime_alias") == root; + RemovePathAlias_nid_no_patch("/lifetime_alias"); + AddPathAlias_nid_no_patch("/lifetime_alias", expectedRoot); + passed = passed && ResolvePath_nid_no_patch("/lifetime_alias") == root; + } catch (const std::exception& error) { + std::fprintf(stderr, "path services during exit: %s\n", error.what()); + passed = false; + } catch (...) { passed = false; } + }); + worker.join(); + } catch (...) { passed = false; } + const char result = passed ? 'P' : 'F'; + static_cast(!write(events[1], &result, 1)); +} + +bool PathsSurviveExit() { + if (pipe(events) != 0) return false; + const pid_t child = fork(); + if (child < 0) { + close(events[0]); + close(events[1]); + return false; + } + if (child == 0) { + close(events[0]); + const auto root = std::filesystem::canonical(std::filesystem::current_path()).string(); + if (root.size() >= sizeof(expectedRoot)) std::_Exit(2); + std::memcpy(expectedRoot, root.c_str(), root.size() + 1); + if (std::atexit(CheckPathsDuringExit) != 0) std::_Exit(2); + ResolvePath_nid_no_patch("/"); + AddPathAlias_nid_no_patch("/lifetime_alias", expectedRoot); + LibcExit_nid_no_patch(0); + } + close(events[1]); + char result = 0; + const ssize_t count = read(events[0], &result, 1); + close(events[0]); + int status = 0; + return waitpid(child, &status, 0) == child && WIFEXITED(status) && + WEXITSTATUS(status) == 0 && count == 1 && result == 'P'; +} + +} + +int main() { + if (!PathsSurviveExit()) { + std::fputs("guest path services must remain available during process exit\n", stderr); + return 1; + } + return 0; +} diff --git a/core/libs/tests/GuestPlayGo.cpp b/core/libs/tests/GuestPlayGo.cpp index cf93ef9eb2..77b345b58c 100644 --- a/core/libs/tests/GuestPlayGo.cpp +++ b/core/libs/tests/GuestPlayGo.cpp @@ -16,6 +16,8 @@ int APS5_VABI scePlayGoInitialize(const PlayGoInitParams*); int APS5_VABI scePlayGoOpen(int*, const void*); int APS5_VABI scePlayGoClose(int); int APS5_VABI scePlayGoGetLanguageMask(int, std::uint64_t*); +int APS5_VABI scePlayGoGetSupportedOptionalChunk(int, std::int32_t, PlayGoOptionalChunk*); +int APS5_VABI scePlayGoGetOptionalChunk(int, std::int32_t, PlayGoOptionalChunk*); int APS5_VABI scePlayGoSetToDoList(int, const PlayGoToDo*, std::uint32_t); int APS5_VABI scePlayGoGetToDoList(int, PlayGoToDo*, std::uint32_t, std::uint32_t*); int APS5_VABI scePlayGoGetLocus(int, const std::uint16_t*, std::uint32_t, std::int8_t*); @@ -24,6 +26,20 @@ int APS5_VABI scePlayGoGetChunkId(int, std::uint16_t*, std::uint32_t, std::uint3 static void Require(bool value) { if (!value) std::abort(); } +template +static void RequireMissingChunkMetadata(Operation&& operation) { + bool threw = false; + try { + operation(); + } catch (const std::runtime_error& error) { + const std::string_view message(error.what()); + threw = message.find("playgo-chunk.dat") != std::string_view::npos && + message.find("playgo-scenario.json") != std::string_view::npos && + message.find("chunk ID") != std::string_view::npos; + } + Require(threw); +} + static int CheckPackageChunkTable(const char* root) { constexpr int badChunkId = static_cast(0x80B2000C); std::filesystem::current_path(root); @@ -65,7 +81,6 @@ static int CheckNoPlayGoFile(const char* root) { constexpr int badPointer = static_cast(0x80B2000A); constexpr int badSize = static_cast(0x80B2000B); constexpr int badLocus = static_cast(0x80B20010); - constexpr int badChunkId = static_cast(0x80B2000C); PlayGoInitParams init{}; int handle = 0; @@ -79,6 +94,13 @@ static int CheckNoPlayGoFile(const char* root) { Require(scePlayGoGetLanguageMask(handle, nullptr) == badPointer); Require(scePlayGoGetLanguageMask(handle, &mask) == 0 && mask == ~0ull); + for (const auto getOptionalChunk : {scePlayGoGetSupportedOptionalChunk, scePlayGoGetOptionalChunk}) { + PlayGoOptionalChunk option{}; + Require(getOptionalChunk(handle + 1, 0, &option) == badHandle); + Require(getOptionalChunk(handle, 0, nullptr) == badPointer); + Require(getOptionalChunk(handle, 0, &option) == 0 && option.bitmask == ~0ull); + } + PlayGoToDo todo[2] = {{0, 3, 0}, {0, 0, 0}}; Require(scePlayGoSetToDoList(handle + 1, todo, 2) == badHandle); Require(scePlayGoSetToDoList(handle, nullptr, 2) == badPointer); @@ -87,9 +109,9 @@ static int CheckNoPlayGoFile(const char* root) { todo[1].locus = 1; Require(scePlayGoSetToDoList(handle, todo, 2) == badLocus); todo[1] = {1, 3, 0}; - Require(scePlayGoSetToDoList(handle, todo, 2) == badChunkId); + RequireMissingChunkMetadata([&] { scePlayGoSetToDoList(handle, todo, 2); }); todo[1] = {0xFFFF, 3, 0}; - Require(scePlayGoSetToDoList(handle, todo, 2) == badChunkId); + RequireMissingChunkMetadata([&] { scePlayGoSetToDoList(handle, todo, 2); }); std::uint32_t entries = 1; Require(scePlayGoGetToDoList(handle, todo, 2, &entries) == 0 && entries == 0); @@ -99,7 +121,7 @@ static int CheckNoPlayGoFile(const char* root) { Require(scePlayGoGetLocus(handle, &chunk, 1, &locus) == 0 && locus == 3); const std::uint16_t undeclared[2] = {1, 10}; std::int8_t loci[2] = {0, 0}; - Require(scePlayGoGetLocus(handle, undeclared, 2, loci) == badChunkId); + RequireMissingChunkMetadata([&] { scePlayGoGetLocus(handle, undeclared, 2, loci); }); const std::uint16_t onlyDeclared[2] = {0, 0}; Require(scePlayGoGetLocus(handle, onlyDeclared, 2, loci) == 0 && loci[0] == 3 && loci[1] == 3); std::uint32_t chunkCount = 0; diff --git a/core/libs/tests/GuestPngEnc.cpp b/core/libs/tests/GuestPngEnc.cpp index 7535dae853..7984c2fff5 100644 --- a/core/libs/tests/GuestPngEnc.cpp +++ b/core/libs/tests/GuestPngEnc.cpp @@ -68,7 +68,8 @@ int main() { encode.color_space = colorSpace; PngEncOutputInfo info{}; const int size = scePngEncEncode(handle, &encode, &info); - Require(size > 0 && info.data_size == static_cast(size) && info.processed_height == height); + Require(size > 1 && static_cast(size) < png.size() + && info.data_size == static_cast(size) && info.processed_height == height); const auto header = Decoder::Png::ParseHeader(std::span(png.data(), size)); Require(header.has_value()); Require(header->colorType == (colorSpace == 19 ? Decoder::Png::ColorType::Rgba : Decoder::Png::ColorType::Rgb)); @@ -83,6 +84,22 @@ int main() { Require(result[3] == (colorSpace == 19 ? source[3] : 0xFF)); } } + const std::vector expected(png.begin(), png.begin() + size); + for (const int capacity : {size - 1, size}) { + std::fill(png.begin(), png.end(), 0xA5); + encode.png_mem_size = static_cast(capacity); + info = {1, 1}; + const int result = scePngEncEncode(handle, &encode, &info); + if (capacity < size) { + Require(result == dataOverflow && info.data_size == 0 && info.processed_height == 0); + Require(std::all_of(png.begin(), png.end(), [](auto byte) { return byte == 0xA5; })); + } else { + Require(result == size && info.data_size == static_cast(size) && info.processed_height == height); + Require(std::equal(expected.begin(), expected.end(), png.begin())); + Require(std::all_of(png.begin() + size, png.end(), [](auto byte) { return byte == 0xA5; })); + } + } + encode.png_mem_size = static_cast(png.size()); } } diff --git a/core/libs/tests/GuestPthreadMutexattr.cpp b/core/libs/tests/GuestPthreadMutexattr.cpp index 410aa02ba0..f3a7257808 100644 --- a/core/libs/tests/GuestPthreadMutexattr.cpp +++ b/core/libs/tests/GuestPthreadMutexattr.cpp @@ -1,6 +1,7 @@ #include "SceTypes.hpp" #include #include +#include #include extern "C" { diff --git a/core/libs/tests/GuestPthreadSchedparam.cpp b/core/libs/tests/GuestPthreadSchedparam.cpp index 6b6b07b2b8..ddafd9090e 100644 --- a/core/libs/tests/GuestPthreadSchedparam.cpp +++ b/core/libs/tests/GuestPthreadSchedparam.cpp @@ -14,6 +14,7 @@ int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched); int APS5_VABI scePthreadAttrSetschedpolicy(PthreadAttr* attr, int policy); +int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param); } static constexpr int SCE_OK = 0; @@ -98,6 +99,46 @@ static int CreatedPolicy(int inheritSched, int policy) { return reported; } +static void* APS5_VABI ReportPriority(void* arg) { + int policy = -1; + KernelSchedParam param{}; + Require(scePthreadGetschedparam(scePthreadSelf(), &policy, ¶m) == SCE_OK); + *static_cast(arg) = param.sched_priority; + return nullptr; +} + +static int CreatedPriority(const PthreadAttr* attr) { + int reported = -1; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, attr, ReportPriority, &reported, "priority") == SCE_OK); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + return reported; +} + +static int CreatedPriority(int inheritSched, const KernelSchedParam* param) { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(scePthreadAttrSetinheritsched(&attr, inheritSched) == SCE_OK); + Require(scePthreadAttrSetschedpolicy(&attr, SCHED_RR) == SCE_OK); + if (param) Require(scePthreadAttrSetschedparam(&attr, param) == SCE_OK); + const int reported = CreatedPriority(&attr); + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + return reported; +} + +static void* APS5_VABI Spawn(void*) { + const KernelSchedParam param{300}; + for (int priority : {450, 600}) { + Require(SceSet(SCHED_FIFO, priority) == SCE_OK); + Require(CreatedPriority(INHERIT_SCHED, ¶m) == priority); + Require(CreatedPriority(INHERIT_SCHED, nullptr) == priority); + Require(CreatedPriority(nullptr) == priority); + Require(CreatedPriority(EXPLICIT_SCHED, ¶m) == 300); + Require(CreatedPriority(EXPLICIT_SCHED, nullptr) == 700); + } + return nullptr; +} + int main() { Pthread thread = nullptr; Require(scePthreadCreate(&thread, nullptr, Run, nullptr, "schedparam") == SCE_OK); @@ -105,4 +146,6 @@ int main() { Require(CreatedPolicy(EXPLICIT_SCHED, SCHED_RR) == SCHED_RR); Require(CreatedPolicy(EXPLICIT_SCHED, SCHED_FIFO) == SCHED_FIFO); Require(CreatedPolicy(INHERIT_SCHED, SCHED_RR) == SCHED_FIFO); + Require(scePthreadCreate(&thread, nullptr, Spawn, nullptr, "spawn") == SCE_OK); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); } diff --git a/core/libs/tests/GuestRaiseException.cpp b/core/libs/tests/GuestRaiseException.cpp index ee3bde7706..c1b6d0a298 100644 --- a/core/libs/tests/GuestRaiseException.cpp +++ b/core/libs/tests/GuestRaiseException.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -33,6 +34,7 @@ int APS5_VABI scePthreadRwlockDestroy(PthreadRwlock* rwlock); int APS5_VABI scePthreadRwlockWrlock(PthreadRwlock* rwlock); int APS5_VABI scePthreadRwlockUnlock(PthreadRwlock* rwlock); int APS5_VABI sceKernelSyncOnAddressWait(std::uint32_t* address, std::uint32_t expected, const KernelUseconds* timeout, const char* name); +int APS5_VABI sceKernelSyncOnAddressWake(void* address, std::int32_t count); } static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); @@ -402,6 +404,87 @@ static void NestedWaitKeepsOuterWake() { Require(sceKernelDeleteSema(sem) == 0); } +#ifdef _WIN32 +static constexpr int AddressRounds = 100; +static std::uint32_t addressWord = 0; +static std::uint32_t otherAddressWord = 0; +static std::atomic holdingEntered{false}; +static std::atomic holdingRelease{false}; +static std::atomic holdingLeft{false}; +static std::atomic holdingThread; + +static void APS5_VABI HoldingHandler(int signum, void*) { + Require(signum == SIGUSR1); + holdingThread.store(std::this_thread::get_id()); + holdingEntered.store(true); + while (!holdingRelease.load()) std::this_thread::yield(); + holdingLeft.store(true); +} + +static void* APS5_VABI AddressWaking(void* arg) { + auto& worker = *static_cast(arg); + worker.id = std::this_thread::get_id(); + worker.started.store(true); + while (!worker.stop.load()) Require(sceKernelSyncOnAddressWake(&addressWord, 1) == 0); + return nullptr; +} + +static void* APS5_VABI AddressWaiting(void* arg) { + auto& worker = *static_cast(arg); + worker.id = std::this_thread::get_id(); + worker.started.store(true); + while (!worker.stop.load()) { + KernelUseconds timeout = 20; + const int result = sceKernelSyncOnAddressWait(&addressWord, 0, &timeout, "address waiting"); + Require(result == 0 || result == SCE_KERNEL_ERROR_ETIMEDOUT); + } + return nullptr; +} + +static bool AwaitWithin(const std::atomic& flag, std::chrono::milliseconds limit) { + const auto deadline = std::chrono::steady_clock::now() + limit; + while (!flag.load()) { + if (std::chrono::steady_clock::now() >= deadline) return false; + std::this_thread::yield(); + } + return true; +} + +static void RequireInRound(bool value, const char* name, int round, const char* check) { + if (value) return; + std::fprintf(stderr, "%s round %d: %s\n", name, round, check); + std::fflush(stderr); + std::abort(); +} + +static void HandlerLeavesAddressLock(PthreadEntry entry, const char* name) { + Worker worker; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, entry, &worker, name) == 0); + while (!worker.started.load()) std::this_thread::yield(); + for (int round = 0; round < AddressRounds; ++round) { + holdingEntered.store(false); + holdingLeft.store(false); + holdingRelease.store(false); + RequireInRound(sceKernelRaiseException(thread, SIGUSR1) == 0, name, round, "the raise failed"); + RequireInRound(AwaitWithin(holdingEntered, std::chrono::seconds(5)), name, round, "the handler did not run within 5 s"); + RequireInRound(holdingThread.load() == worker.id, name, round, "the handler ran on another thread"); + std::atomic woke{false}; + std::thread waker([&] { + Require(sceKernelSyncOnAddressWake(&otherAddressWord, 1) == 0); + woke.store(true); + }); + const bool returned = AwaitWithin(woke, std::chrono::milliseconds(500)); + holdingRelease.store(true); + waker.join(); + RequireInRound(returned, name, round, "a wake on another address blocked for 500 ms while the handler ran"); + RequireInRound(AwaitWithin(holdingLeft, std::chrono::seconds(5)), name, round, "the handler did not return within 5 s of its release"); + } + worker.stop.store(true); + Require(scePthreadJoin(thread, nullptr) == 0); +} +#endif + static void ExpectDelivery(int before, std::thread::id thread) { for (int attempt = 0; attempt < 5000 && calls.load() == before; ++attempt) std::this_thread::sleep_for(std::chrono::milliseconds(1)); Require(calls.load() == before + 1); @@ -660,4 +743,11 @@ int main() { NestedWaitKeepsLaterWaiters(); NestedWaitKeepsOuterWake(); Require(sceKernelRemoveExceptionHandler(SIGUSR1) == 0); + +#ifdef _WIN32 + Require(sceKernelInstallExceptionHandler(SIGUSR1, reinterpret_cast(&HoldingHandler)) == 0); + HandlerLeavesAddressLock(AddressWaking, "address waking"); + HandlerLeavesAddressLock(AddressWaiting, "address waiting"); + Require(sceKernelRemoveExceptionHandler(SIGUSR1) == 0); +#endif } diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index c117778e49..06e4eb3fe7 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -74,6 +74,13 @@ int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI close_nid_postfix(int); int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +struct GuestPollEntry { + int descriptor; + short events; + short revents; +}; +int APS5_VABI poll_nid_postfix(GuestPollEntry*, std::uint32_t, int); 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); @@ -506,6 +513,26 @@ static void CheckKernelTimeouts() { Require(sceNetSocketClose(descriptor) == 0); } +static void CheckKernelStatusFlags() { + const int descriptor = sceNetSocket("kernel-status", 2, 1, 6); + Require(descriptor >= 0); + Require(fcntl_nid_postfix(descriptor, 3) == 2); + Require(fcntl_nid_postfix(descriptor, 4, 2 | 4) == 0); + Require(fcntl_nid_postfix(descriptor, 3) == (2 | 4)); + int enabled = 0; + std::uint32_t length = sizeof(enabled); + Require(sceNetGetsockopt(descriptor, 0xffff, 0x1200, &enabled, &length) == 0 && enabled == 1); + Require(fcntl_nid_postfix(descriptor, 4, 2) == 0); + Require(fcntl_nid_postfix(descriptor, 3) == 2); + length = sizeof(enabled); + Require(sceNetGetsockopt(descriptor, 0xffff, 0x1200, &enabled, &length) == 0 && enabled == 0); + *__error_nid_postfix() = 0; + Require(fcntl_nid_postfix(descriptor, 4, 0x100) == -1 && *__error_nid_postfix() == 45); + *__error_nid_postfix() = 0; + Require(fcntl_nid_postfix(descriptor, 1) == -1 && *__error_nid_postfix() == 22); + Require(close_nid_postfix(descriptor) == 0); +} + static void CheckAddressText(int family, const char* text) { std::array address{}; Require(sceNetInetPton(family, text, address.data()) == 1); @@ -671,6 +698,23 @@ static void CheckStreamAddressLengths() { for (const int descriptor : {accepted, client, listener}) Require(sceNetSocketClose(descriptor) == 0); } +static void CheckBufferSizes() { + const int socket = sceNetSocket("buffer-sizes", 2, 2, 17); + Require(socket >= 0); + for (const int option : {0x1001, 0x1002}) { + int value = -1; + std::uint32_t size = sizeof(value); + Require(sceNetGetsockopt(socket, 0xffff, option, &value, &size) == 0 && size == sizeof(value) && value > 0); + } + for (const auto [option, requested] : {std::array{0x1001, 32768}, std::array{0x1002, 65536}}) { + Require(sceNetSetsockopt(socket, 0xffff, option, &requested, sizeof(requested)) == 0); + int value = -1; + std::uint32_t size = sizeof(value); + Require(sceNetGetsockopt(socket, 0xffff, option, &value, &size) == 0 && size == sizeof(value) && value == requested); + } + Require(sceNetSocketClose(socket) == 0); +} + static void CheckUnspecifiedIpv6() { std::array unspecified{}; Require(sceNetInetPton(28, "::", unspecified.data()) == 1); @@ -844,6 +888,7 @@ int main() { Require(Failed(sceNetSocketClose(closed), 9)); CheckKernelTimeouts(); CheckSocketError(); + CheckKernelStatusFlags(); const int listener = sceNetSocket(nullptr, 2, 1, 6); Require(listener >= 0); @@ -876,6 +921,7 @@ int main() { CheckDatagramAddressLengths(2); CheckDatagramAddressLengths(28); CheckStreamAddressLengths(); + CheckBufferSizes(); const int epoll = sceNetEpollCreate("guest-sce-net", 0); Require(epoll >= 0); @@ -986,6 +1032,13 @@ int main() { Require(select_nid_postfix(std::max(kernel_socket, udp_sender) + 1, nullptr, mixed_writable, nullptr, wait_second) == 2); Require(((mixed_writable[kernel_socket / 64] >> (kernel_socket % 64)) & 1u) != 0); Require(((mixed_writable[udp_sender / 64] >> (udp_sender % 64)) & 1u) != 0); + GuestPollEntry net_read{udp_receiver, 1, 0}; + Require(poll_nid_postfix(&net_read, 1, 0) == 0 && net_read.revents == 0); + Require(sceNetSendto(udp_sender, datagram, sizeof(datagram), 0, address.data(), address.size()) == sizeof(datagram)); + Require(poll_nid_postfix(&net_read, 1, 1000) == 1 && net_read.revents == 1); + Require(sceNetRecvfrom(udp_receiver, datagram_result, sizeof(datagram_result), 0, nullptr, nullptr) == sizeof(datagram_result)); + GuestPollEntry mixed_poll[2]{{kernel_socket, 4, 0}, {udp_sender, 4, 0}}; + Require(poll_nid_postfix(mixed_poll, 2, 1000) == 2 && mixed_poll[0].revents == 4 && mixed_poll[1].revents == 4); Require(close_nid_postfix(kernel_socket) == 0); std::uint64_t stdio_readable[16]{1}; std::uint64_t stdio_writable[16]{6}; diff --git a/core/libs/tests/GuestSchedPriority.cpp b/core/libs/tests/GuestSchedPriority.cpp index a83bebd231..25393d9bcd 100644 --- a/core/libs/tests/GuestSchedPriority.cpp +++ b/core/libs/tests/GuestSchedPriority.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" { diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index 913fbe1ef1..d2fe283b8a 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include diff --git a/core/libs/tests/GuestSocketDescriptorFlags.cpp b/core/libs/tests/GuestSocketDescriptorFlags.cpp new file mode 100644 index 0000000000..a8c1ba10f2 --- /dev/null +++ b/core/libs/tests/GuestSocketDescriptorFlags.cpp @@ -0,0 +1,61 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI dup_nid_postfix(int); +int APS5_VABI dup2_nid_postfix(int, int); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int GetDescriptorFlags = 1; +constexpr int SetDescriptorFlags = 2; +constexpr int CloseOnExec = 1; +constexpr int Stream = 1; +constexpr int SocketCloseOnExec = 0x10000000; + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix()); + std::abort(); +} + +} + +int main() { + const int plain = socket_nid_postfix(2, Stream, 0); + Require(plain >= 0, "socket failed"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == 0, "a new socket must not be close-on-exec (F_GETFD 0)"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, CloseOnExec) == 0, "F_SETFD on a socket must succeed"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "F_GETFD must read back FD_CLOEXEC"); + const int copy = dup_nid_postfix(plain); + Require(copy >= 0 && fcntl_nid_postfix(copy, GetDescriptorFlags) == 0, "dup must clear FD_CLOEXEC on the copy"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "dup must leave the flag of the original"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0 && fcntl_nid_postfix(plain, GetDescriptorFlags) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); + Require(close_nid_postfix(copy) == 0, "close failed"); + + const int target = socket_nid_postfix(2, Stream, 0); + Require(target >= 0, "socket failed"); + Require(fcntl_nid_postfix(target, SetDescriptorFlags, CloseOnExec) == 0 && fcntl_nid_postfix(target, GetDescriptorFlags) == CloseOnExec, "F_SETFD on the dup2 target must succeed"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, CloseOnExec) == 0, "F_SETFD on a socket must succeed"); + Require(dup2_nid_postfix(plain, target) == target, "dup2 onto another socket must return the target"); + Require(fcntl_nid_postfix(target, GetDescriptorFlags) == 0, "dup2 must clear FD_CLOEXEC on the target"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "dup2 must leave the flag of the original"); + Require(dup2_nid_postfix(plain, plain) == plain && fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "dup2 onto the same descriptor must leave the flag"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); + Require(close_nid_postfix(target) == 0, "close failed"); + + const int marked = socket_nid_postfix(2, Stream | SocketCloseOnExec, 0); + Require(marked >= 0 && fcntl_nid_postfix(marked, GetDescriptorFlags) == CloseOnExec, "SOCK_CLOEXEC must set FD_CLOEXEC"); + Require(close_nid_postfix(marked) == 0, "close failed"); + const int reused = socket_nid_postfix(2, Stream, 0); + Require(reused >= 0 && fcntl_nid_postfix(reused, GetDescriptorFlags) == 0, "the flag of a closed socket must not reach the next socket"); + Require(close_nid_postfix(reused) == 0 && close_nid_postfix(plain) == 0, "close failed"); + return 0; +} diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index 2f712816f2..5242a51ef2 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libSceSystemService/SystemService.hpp" +#include #include #include #include @@ -26,14 +27,31 @@ using GuestQuickExitCallback = void (APS5_VABI*)(); extern "C" int APS5_VABI at_quick_exit_nid_postfix(GuestQuickExitCallback); extern "C" void APS5_VABI quick_exit_nid_postfix(int); namespace { -bool cleaned = false; -bool finalized = false; -void Cleanup() { cleaned = true; } -void APS5_VABI Finalize(void*) { finalized = true; } -void VerifyExit() { - if (!cleaned || !finalized) std::_Exit(1); - std::puts("Guest shutdown and atexit completed"); +std::atomic awaiting{false}; +void AwaitExit() { + awaiting = true; + awaiting.notify_one(); + try { + LibcAwaitExit_nid_postfix(); + } catch (const ProcessShutdown&) { + std::puts("Guest thread resumed during exit"); + std::fflush(stdout); + std::_Exit(6); + } } +void Cleanup() { + std::thread(AwaitExit).detach(); + awaiting.wait(false); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::puts("Guest shutdown completed"); + std::fflush(stdout); +} +void ExitHandlerRan() { + std::puts("Guest exit handler ran"); + std::fflush(stdout); + std::_Exit(5); +} +void APS5_VABI UnexpectedFinalizer(void*) { ExitHandlerRan(); } int quickOrder = 0; void APS5_VABI QuickSecond() { quickOrder = 1; } void APS5_VABI QuickFirst() { @@ -140,7 +158,8 @@ int DriveRelaunch() { } } int main(int argc, char** argv) { - if (argc > 1 && std::strcmp(argv[1], "--immediate") == 0) { + const int firstArgument = argc > 0 && std::strncmp(argv[0], "--", 2) == 0 ? 0 : 1; + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--immediate") == 0) { LibcRegisterShutdown_nid_postfix(UnexpectedCleanup); Require(std::atexit(UnexpectedCleanup) == 0); #ifdef _WIN32 @@ -150,54 +169,54 @@ int main(int argc, char** argv) { _Exit_nid_postfix(0); return 2; } - if (argc > 1 && std::strcmp(argv[1], "--return-from-main") == 0) { + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--return-from-main") == 0) { catchReturnFromMain_nid_postfix(0); return 2; } - if (argc > 1 && std::strcmp(argv[1], "--quick-exit") == 0) { + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--quick-exit") == 0) { Require(at_quick_exit_nid_postfix(QuickFirst) == 0); Require(at_quick_exit_nid_postfix(QuickSecond) == 0); quick_exit_nid_postfix(0); return 2; } - if (argc > 1 && std::strcmp(argv[1], "--nested-exit-driver") == 0) { + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--nested-exit-driver") == 0) { return DriveNestedExit(); } - if (argc > 1 && std::strcmp(argv[1], "--nested-exit") == 0) { + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--nested-exit") == 0) { Require(__cxa_atexit_nid_postfix(RemainingHandler, nullptr, nullptr) == 0); Require(__cxa_atexit_nid_postfix(ExitingHandler, nullptr, nullptr) == 0); exit_nid_postfix(0); return 2; } - if (argc > 1 && std::strcmp(argv[1], "--relaunch-driver") == 0) { + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--relaunch-driver") == 0) { return DriveRelaunch(); } - if (argc > 1 && std::strcmp(argv[1], "--relaunch") == 0) { + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--relaunch") == 0) { LibcRegisterShutdown_nid_postfix(MainShutdown); const char* const arguments[] = {"--secondary", "two words", "quote\"d\\", "", nullptr}; sceSystemServiceLoadExec("/app0/picker/picker.bin", arguments); return 2; } - if (argc > 1 && std::strcmp(argv[1], "--secondary") == 0) { + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--secondary") == 0) { Require(OwnExecutable().filename().string().starts_with("picker.picker.bin")); std::string received = "arguments:"; - for (int index = 1; index < argc; ++index) received += std::string(index == 1 ? " " : "|") + argv[index]; + for (int index = firstArgument; index < argc; ++index) received += std::string(index == firstArgument ? " " : "|") + argv[index]; AppendMarker(received); LibcRegisterShutdown_nid_postfix(SecondaryShutdown); const char* const arguments[] = {"--relaunched", nullptr}; sceSystemServiceLoadExec("/app0/eboot.bin", arguments); return 2; } - if (argc > 1 && std::strcmp(argv[1], "--relaunched") == 0) { + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--relaunched") == 0) { const char* main = std::getenv(MainExecutable); Require(main != nullptr && std::filesystem::equivalent(OwnExecutable(), main)); AppendMarker("relaunched"); return 0; } - if (argc > 1) { + if (argc > firstArgument) { LibcRegisterShutdown_nid_postfix(Cleanup); - Require(__cxa_atexit_nid_postfix(Finalize, nullptr, nullptr) == 0); - Require(std::atexit(VerifyExit) == 0); + Require(__cxa_atexit_nid_postfix(UnexpectedFinalizer, nullptr, nullptr) == 0); + Require(std::atexit(ExitHandlerRan) == 0); sceSystemServiceLoadExec("exit", nullptr); return 2; } @@ -206,7 +225,7 @@ int main(int argc, char** argv) { for (const char* path : {"/app0/another.bin", "/data/eboot.bin", "/app0/../eboot.bin", "/app0//eboot.bin"}) { bool rejected = false; try { sceSystemServiceLoadExec(path, nullptr); } - catch (const std::runtime_error&) { rejected = true; } + catch (const std::runtime_error& error) { rejected = std::string(error.what()).find(path) != std::string::npos; } Require(rejected); } #ifdef _WIN32 diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index d9da3cf349..e9ffebd9a7 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -19,6 +19,7 @@ int APS5_VABI getpeername_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 recv_nid_postfix(int, 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); @@ -98,9 +99,50 @@ static void CheckAddressLengths(int family) { Require(close_nid_postfix(sender) == 0 && close_nid_postfix(receiver) == 0); } +static void CheckDontWait() { + const int receiver = socket_nid_postfix(2, 2, 0); + const int sender = socket_nid_postfix(2, 2, 0); + Require(receiver >= 0 && sender >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(receiver, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(receiver, address.data(), &size) == 0); + char buffer[16]{}; + Iovec vector{buffer, sizeof(buffer)}; + Msghdr message{nullptr, 0, &vector, 1, nullptr, 0, 0}; +#ifdef _WIN32 + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 45); + Require(recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 0x80, nullptr, nullptr) == -1 && *__error_nid_postfix() == 45); + Require(recvmsg_nid_postfix(receiver, &message, 0x80) == -1 && *__error_nid_postfix() == 45); + Require(sendto_nid_postfix(sender, "x", 1, 0x80, address.data(), address.size()) == -1 && *__error_nid_postfix() == 45); +#else + const auto start = std::chrono::steady_clock::now(); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 35); + Require(recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 0x80, nullptr, nullptr) == -1 && *__error_nid_postfix() == 35); + Require(recvmsg_nid_postfix(receiver, &message, 0x80) == -1 && *__error_nid_postfix() == 35); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x82) == -1 && *__error_nid_postfix() == 35); + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(1)); + const char datagram[] = "nowait"; + Require(sendto_nid_postfix(sender, datagram, sizeof(datagram), 0x80, address.data(), address.size()) == sizeof(datagram)); + Require(recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 2, nullptr, nullptr) == sizeof(datagram)); + std::memset(buffer, 0, sizeof(buffer)); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x82) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + std::memset(buffer, 0, sizeof(buffer)); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + Require(connect_nid_postfix(sender, address.data(), address.size()) == 0); + Require(send_nid_postfix(sender, datagram, sizeof(datagram), 0x80 | 0x20000) == sizeof(datagram)); + std::memset(buffer, 0, sizeof(buffer)); + Require(recvmsg_nid_postfix(receiver, &message, 0) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 35); +#endif + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x1) == -1 && *__error_nid_postfix() == 45); + Require(close_nid_postfix(receiver) == 0 && close_nid_postfix(sender) == 0); +} + int main() { CheckAddressLengths(2); CheckAddressLengths(28); + CheckDontWait(); 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); diff --git a/core/libs/tests/GuestUserServiceLifecycle.cpp b/core/libs/tests/GuestUserServiceLifecycle.cpp new file mode 100644 index 0000000000..5603a52319 --- /dev/null +++ b/core/libs/tests/GuestUserServiceLifecycle.cpp @@ -0,0 +1,54 @@ +#include "SceTypes.hpp" +#include "prx/libSceUserService/UserService.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceUserServiceInitialize(const void* params); +int APS5_VABI sceUserServiceTerminate(void); +int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* user_id_list); +int APS5_VABI sceUserServiceGetEvent(SceUserServiceEvent* event); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +void RequireNotInitialized() { + UserServiceLoginUserIdList list{}; + Require(sceUserServiceGetLoginUserIdList(&list) == USER_SERVICE_ERROR_NOT_INITIALIZED); + SceUserServiceEvent event{}; + std::memset(&event, 0xAA, sizeof(event)); + Require(sceUserServiceGetEvent(&event) == USER_SERVICE_ERROR_NOT_INITIALIZED); + Require(event.user_id == static_cast(0xAAAAAAAAu)); + Require(sceUserServiceGetEvent(nullptr) == USER_SERVICE_ERROR_NOT_INITIALIZED); +} + +void RequireLoginOnce() { + SceUserServiceEvent event{}; + Require(sceUserServiceGetEvent(&event) == USER_SERVICE_OK); + Require(event.event_type == 0 && event.user_id == USER_SERVICE_INITIAL_USER_ID); + Require(sceUserServiceGetEvent(&event) == USER_SERVICE_ERROR_NO_EVENT); +} + +} + +int main() { + RequireNotInitialized(); + Require(sceUserServiceTerminate() == USER_SERVICE_ERROR_NOT_INITIALIZED); + + Require(sceUserServiceInitialize(nullptr) == USER_SERVICE_OK); + Require(sceUserServiceInitialize(nullptr) == USER_SERVICE_ERROR_ALREADY_INITIALIZED); + UserServiceLoginUserIdList list{}; + Require(sceUserServiceGetLoginUserIdList(&list) == USER_SERVICE_OK); + Require(list.user_id[0] == USER_SERVICE_INITIAL_USER_ID); + RequireLoginOnce(); + + Require(sceUserServiceTerminate() == USER_SERVICE_OK); + Require(sceUserServiceTerminate() == USER_SERVICE_ERROR_NOT_INITIALIZED); + RequireNotInitialized(); + + Require(sceUserServiceInitialize(nullptr) == USER_SERVICE_OK); + RequireLoginOnce(); + Require(sceUserServiceTerminate() == USER_SERVICE_OK); +} diff --git a/core/libs/tests/GuestVdecsw.cpp b/core/libs/tests/GuestVdecsw.cpp index 99a11fe967..4604a40034 100644 --- a/core/libs/tests/GuestVdecsw.cpp +++ b/core/libs/tests/GuestVdecsw.cpp @@ -188,13 +188,13 @@ void testDecode() { } check(pictures < PictureHashes.size(), "more pictures than access units"); check(output.isLastFrame == (pictures + 1 == PictureHashes.size()), "only the last picture of the finalized sequence is marked last"); - check(!output.isErrorFrame && !output.isDiscardedFrame && output.pictureCount == 1 && output.frameWidth == Width && output.frameHeight == Height && output.frameBuffer == buffers[pictures % 2].data(), "unexpected picture geometry"); + check(!output.isErrorFrame && !output.isDiscardedFrame && output.pictureCount == 1 && output.frameWidth == Width && output.frameHeight == CodedHeight && output.frameBuffer == buffers[pictures % 2].data(), "unexpected picture geometry"); AvcPictureInfo info{sizeof(AvcPictureInfo)}; check(sceVdecswGetAvcPictureInfo(&output, &info, nullptr) == 0 && info.isValid, "picture info missing"); const auto unit = DisplayOrderUnits[pictures]; check(info.ptsData == 1000 + unit && info.dtsData == unit && info.attachedData == 0xa0 + unit && info.idrPictureFlag == (pictures == 0 ? 1 : 0) && info.profileIdc == 100, "pictures out of display order"); - check(info.picWidthInMbsMinus1 == Width / 16 - 1 && info.picHeightInMapUnitsMinus1 == (Height + 15) / 16 - 1 && info.frameMbsOnlyFlag == 1 && info.frameCroppingFlag == 1 && info.frameCropRightOffset == 0 && info.frameCropBottomOffset == ((Height + 15) / 16 * 16 - Height) / 2, "picture geometry in the picture info is wrong"); - check(hashNv12(static_cast(output.frameBuffer), output.framePitch) == PictureHashes[pictures], "picture " + std::to_string(pictures) + " differs from the reference"); + check(info.picWidthInMbsMinus1 == Width / 16 - 1 && info.picHeightInMapUnitsMinus1 == CodedHeight / 16 - 1 && info.frameMbsOnlyFlag == 1 && info.frameCroppingFlag == 1 && info.frameCropLeftOffset == 0 && info.frameCropRightOffset == 0 && info.frameCropTopOffset == 0 && info.frameCropBottomOffset == (CodedHeight - Height) / 2, "picture geometry in the picture info is wrong"); + check(hashNv12(static_cast(output.frameBuffer), output.framePitch, output.frameHeight) == PictureHashes[pictures], "picture " + std::to_string(pictures) + " differs from the reference"); ++pictures; } }; diff --git a/core/libs/tests/GuestVideodec2.cpp b/core/libs/tests/GuestVideodec2.cpp index df41978f8c..9efbaa0456 100644 --- a/core/libs/tests/GuestVideodec2.cpp +++ b/core/libs/tests/GuestVideodec2.cpp @@ -389,7 +389,7 @@ void testClip(std::uint32_t codec, std::uint32_t profile, const std::vector{0x88}, secondBuffer, secondOutput, 9200); info = query(secondOutput); check(info.isValid && info.ptsData == 9200 && info.keyFrameFlag == 0 && info.frameWidth == Width, "VP9 existing reference metadata differs"); - check(hashNv12(secondBuffer.data(), secondOutput.framePitch) == Vp9Hashes[0], "VP9 existing reference pixels differ"); + check(hashNv12(secondBuffer.data(), secondOutput.framePitch, secondOutput.frameHeight) == Vp9Hashes[0], "VP9 existing reference pixels differ"); check(sceVideodec2Reset_nid_postfix(handle) == 0, "VP9 reset failed"); checkThrows([&] { decode(std::vector{0x88}, firstBuffer, firstOutput, 9300); }); checkThrows([&] { decode(std::vector{0}, firstBuffer, firstOutput, 9400); }); @@ -484,7 +484,7 @@ void testVp9PictureMetadata() { decode(resized, firstBuffer, firstOutput, 9600); info = query(firstOutput); check(info.isValid && info.bitDepth == 8 && info.colorRange == 1 && info.frameWidth == Width && info.frameHeight == Height && info.renderWidth == Width * 2 && info.renderHeight == Height * 2, "VP9 render size or color range metadata differs"); - check(hashNv12(firstBuffer.data(), firstOutput.framePitch) == Vp9Hashes[0], "VP9 render metadata changed coded pixels"); + check(hashNv12(firstBuffer.data(), firstOutput.framePitch, firstOutput.frameHeight) == Vp9Hashes[0], "VP9 render metadata changed coded pixels"); check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0 && !query(firstOutput).isValid, "VP9 delete retained picture ownership"); } @@ -521,7 +521,7 @@ void testDecode(bool lengthPrefixed) { check(output.isValid == frame.isAccepted, "frame buffer acceptance disagrees with the output"); if (!output.isValid) return false; check(pictures < PictureHashes.size(), "more pictures than access units"); - check(!output.isErrorFrame && output.frameWidth == Width && output.frameHeight == Height && output.framePitch == 256 && output.frameBuffer == buffer.data(), "unexpected picture geometry"); + check(!output.isErrorFrame && output.frameWidth == Width && output.frameHeight == CodedHeight && output.framePitch == 256 && output.frameBuffer == buffer.data(), "unexpected picture geometry"); AvcPictureInfo info{sizeof(AvcPictureInfo)}; check(sceVideodec2GetPictureInfo_nid_postfix(&output, &info, nullptr) == 0 && info.isValid, "picture info missing"); const auto unit = DisplayOrderUnits[pictures]; @@ -529,8 +529,8 @@ void testDecode(bool lengthPrefixed) { check(info.idrPictureFlag == (pictures == 0 ? 1 : 0) && info.profileIdc == 100, "picture flags mismatch"); AvcPictureInfo avc{sizeof(AvcPictureInfo)}; check(sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &avc, nullptr) == 0 && avc.isValid, "avc picture info missing"); - check(avc.ptsData == 1000 + unit && avc.dtsData == unit && avc.attachedData == 0xa0 + unit && avc.idrPictureFlag == (pictures == 0 ? 1 : 0) && avc.profileIdc == 100 && avc.picWidthInLumaSamples == Width && avc.picHeightInLumaSamples == Height, "avc picture info mismatch"); - check(hashNv12(buffer.data(), output.framePitch) == PictureHashes[pictures], "picture " + std::to_string(pictures) + " differs from the reference"); + check(avc.ptsData == 1000 + unit && avc.dtsData == unit && avc.attachedData == 0xa0 + unit && avc.idrPictureFlag == (pictures == 0 ? 1 : 0) && avc.profileIdc == 100 && avc.picWidthInLumaSamples == Width && avc.picHeightInLumaSamples == CodedHeight, "avc picture info mismatch"); + check(hashNv12(buffer.data(), output.framePitch, output.frameHeight) == PictureHashes[pictures], "picture " + std::to_string(pictures) + " differs from the reference"); ++pictures; return true; }; diff --git a/core/libs/tests/H264Fixture.hpp b/core/libs/tests/H264Fixture.hpp index f63718b676..20eab25442 100644 --- a/core/libs/tests/H264Fixture.hpp +++ b/core/libs/tests/H264Fixture.hpp @@ -9,6 +9,7 @@ namespace H264Fixture { inline constexpr std::uint32_t Width = 64; inline constexpr std::uint32_t Height = 40; +inline constexpr std::uint32_t CodedHeight = 48; inline constexpr std::array Stream{ 0x00, 0x00, 0x00, 0x01, 0x09, 0x10, 0x00, 0x00, 0x00, 0x01, 0x67, 0x64, 0x00, 0x0a, 0xac, 0xd9, @@ -107,20 +108,20 @@ inline constexpr std::array Stream{ }; inline constexpr std::array PictureHashes{ - 0x5871fdc85dfa274eull, 0x26ce4afb974cffc4ull, 0xd2e739633733d5baull, - 0x54feb3d290e028fdull, 0xfa1342a4ba1819c8ull, 0x870125dd7f079a4cull, + 0x51b6aa8b48a59d5full, 0x2d097708ed7c0f66ull, 0xdee989a52806e938ull, + 0x4bfd091eacb8679full, 0x290cf60ce8be58bcull, 0x809cb73819503632ull, }; inline constexpr std::array DisplayOrderUnits{0, 2, 3, 1, 5, 4}; -inline std::uint64_t hashNv12(const std::uint8_t* buffer, std::uint32_t pitch) { +inline std::uint64_t hashNv12(const std::uint8_t* buffer, std::uint32_t pitch, std::uint32_t height) { std::uint64_t hash = 0xcbf29ce484222325ull; const auto mix = [&](const std::uint8_t* row) { for (std::uint32_t column = 0; column < Width; ++column) hash = (hash ^ row[column]) * 0x100000001b3ull; }; - for (std::uint32_t row = 0; row < Height; ++row) mix(buffer + static_cast(row) * pitch); - const auto* chroma = buffer + static_cast(pitch) * Height; - for (std::uint32_t row = 0; row < Height / 2; ++row) mix(chroma + static_cast(row) * pitch); + for (std::uint32_t row = 0; row < height; ++row) mix(buffer + static_cast(row) * pitch); + const auto* chroma = buffer + static_cast(pitch) * height; + for (std::uint32_t row = 0; row < height / 2; ++row) mix(chroma + static_cast(row) * pitch); return hash; } diff --git a/core/libs/tests/InterpolationReferenceRange.cpp b/core/libs/tests/InterpolationReferenceRange.cpp new file mode 100644 index 0000000000..7bf9c80ad3 --- /dev/null +++ b/core/libs/tests/InterpolationReferenceRange.cpp @@ -0,0 +1,53 @@ +#include "IntermediateRepresentation/IrBuilder.hpp" +#include "Optimization/ShaderInfoCollector.hpp" +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +std::string Collect(std::uint32_t attribute, std::uint32_t component, std::uint32_t mode, std::uint32_t inputNum) { + IrProgram program; + program.Resources().stage = IrShaderStage::Pixel; + program.Resources().resourceTrackingComplete = true; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& parameter = builder.Emit(IrOpcode::GetInterpolationParameter, IrType::U32, {&builder.Constant(attribute), &builder.Constant(component), &builder.Constant(mode)}); + static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {¶meter})); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ShaderPixelInputInfo pixel {}; + pixel.inputNum = inputNum; + try { + ShaderInfoCollector().Collect(program, ShaderStageInputInfo {nullptr, &pixel, nullptr}); + } catch (const std::runtime_error& error) { + return error.what(); + } + return {}; +} + +} + +int main() { + int failures = 0; + const auto expect = [&](bool value, const char* name, const std::string& message) { + if (!value) { + std::fprintf(stderr, "%s: %s\n", name, message.c_str()); + ++failures; + } + }; + const auto attribute = Collect(5u, 1u, 0u, 2u); + expect(attribute.find("interpolation parameter reference is out of range (attribute 5 component 1 mode 0, 2 inputs)") != std::string::npos, "an attribute past the pixel inputs", attribute); + const auto component = Collect(0u, 4u, 2u, 1u); + expect(component.find("(attribute 0 component 4 mode 2, 1 inputs)") != std::string::npos, "a component past w", component); + const auto mode = Collect(1u, 3u, 3u, 3u); + expect(mode.find("(attribute 1 component 3 mode 3, 3 inputs)") != std::string::npos, "a reserved interpolation mode", mode); + const auto inRange = Collect(1u, 3u, 2u, 2u); + expect(inRange.find("interpolation parameter reference") == std::string::npos, "an in-range reference", inRange); + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/tests/ScalarCall.cpp b/core/libs/tests/ScalarCall.cpp index e797b7aeba..dda7b2fea4 100644 --- a/core/libs/tests/ScalarCall.cpp +++ b/core/libs/tests/ScalarCall.cpp @@ -151,10 +151,8 @@ static void CheckUserDataCalls() { Require(std::equal(addFive.begin(), addFive.end(), linked.begin() + 12) && std::equal(addSixtyFour.begin(), addSixtyFour.end(), linked.begin() + 14)); auto readsPc = caller; readsPc[1] = 0xbe941f00u; - ExpectThrow("cannot have user-data callees linked", [&] { - const auto readsPcProgram = RdnaInstructionDecoder{}.Decode(readsPc); - static_cast(LinkUserDataCalls(readsPcProgram, FindUserDataCalls(readsPcProgram, swappc), callees)); - }); + const auto readsPcProgram = RdnaInstructionDecoder{}.Decode(readsPc); + Require(LinkUserDataCalls(readsPcProgram, FindUserDataCalls(readsPcProgram, swappc), callees).size() == 17u); const auto linkedProgram = RdnaInstructionDecoder{}.Decode(linked); const auto cfg = GraphBuilder{}.Build(linkedProgram, &swappc); @@ -181,6 +179,101 @@ static void CheckUserDataCalls() { ExpectThrow("is shared with another call", [&] { static_cast(GraphBuilder{}.Build(RdnaInstructionDecoder{}.Decode(sameCopy), &swappc)); }); } +static std::vector TableCaller() { + return {0x8f148414u, 0xf4080406u, 0xfa000270u, 0xbf8cc07fu, 0xf4280208u, 0x28000000u, 0xbf8cc07fu, 0xbe8e0308u, 0xbe8f0309u, 0xbe8e210eu, 0xbf810000u}; +} + +static ControlFlowGraph BuildCode(std::span code, const SwappcInfo& swappc) { + return GraphBuilder{}.Build(RdnaInstructionDecoder{}.Decode(code), &swappc); +} + +static IrProgram TranslateCode(std::span code, const SwappcInfo& swappc) { + const auto program = RdnaInstructionDecoder{}.Decode(code); + const auto cfg = GraphBuilder{}.Build(program, &swappc); + ShaderComputeInputInfo computeInfo{}; + TranslateOptions options{}; + options.stage = ShaderStageKind::Compute; + options.inputInfo.compute = &computeInfo; + options.userDataCount = swappc.userDataCount; + return InstructionTranslator{}.Translate(program, cfg, options); +} + +static void CheckTableCalls() { + const SwappcInfo swappc{false, 0u, 16u}; + const auto caller = TableCaller(); + const auto program = RdnaInstructionDecoder{}.Decode(caller); + const auto calls = FindTableCalls(program, swappc); + Require(calls.size() == 1u); + Require(calls[0].programCounter == 0x24u && calls[0].linkRegister == 14u && calls[0].targetRegister == 14u && calls[0].userDataRegister == 12u); + Require(calls[0].descriptorOffset == 0x270u && calls[0].entryOffset == 0u && calls[0].entryStride == 16u); + Require(FindUserDataCalls(program, swappc).empty()); + const auto table = [&](std::size_t word, std::uint32_t value, std::uint32_t userDataCount = 16u) { + auto code = caller; + code[word] = value; + return FindTableCalls(RdnaInstructionDecoder{}.Decode(code), SwappcInfo{false, 0u, userDataCount}); + }; + Require(table(0u, 0x8f148114u).empty()); + Require(table(2u, 0x28000270u).empty()); + Require(table(8u, 0xbe8f030au).empty()); + Require(table(10u, 0xbf810000u, 12u).empty()); + Require(table(5u, 0x28000008u).size() == 1u && table(5u, 0x28000008u)[0].entryOffset == 8u); + Require(table(0u, 0xbe940380u).empty()); + auto readsLane = caller; + readsLane.insert(readsLane.begin(), {0xd4c2000bu, 0x00020300u}); + const auto readsLaneProgram = RdnaInstructionDecoder{}.Decode(readsLane); + Require(FindTableCalls(readsLaneProgram, SwappcInfo{false, 0u, 16u, 32u}).size() == 1u); + Require(FindTableCalls(readsLaneProgram, SwappcInfo{false, 0u, 16u, 64u}).empty()); + + ExpectThrow("not statically resolvable", [&] { static_cast(BuildCode(caller, swappc)); }); + const std::array fallback{0x24u}; + SwappcInfo listed = swappc; + listed.linkedCallFallbacks = fallback; + Require(BuildCode(caller, listed).unlinkedCallProgramCounters == std::vector{0x24u}); + const auto unlinked = TranslateCode(caller, listed); + Require(unlinked.Info().usesFaultBuffer && ProgramToString(unlinked).find("UnlinkedCall") != std::string::npos); + + const std::vector first{0xbf068000u, 0x4a020285u, 0xbe80200eu}; + const std::vector second{0x4a0202c0u, 0xbf068001u, 0xbe80200eu}; + const std::vector> callees{{{0x1122334455ull, first}, {0x66778899aaull, second}}}; + const auto linkedCalls = LinkCalls(program, {}, {}, calls, callees); + const auto& linked = linkedCalls.code; + Require(linkedCalls.fallbacks == std::vector{50u * 4u}); + ExpectThrow("is shared with another call", [&] { static_cast(BuildCode(linked, swappc)); }); + SwappcInfo linkedSwappc = swappc; + linkedSwappc.linkedCallFallbacks = linkedCalls.fallbacks; + Require(linked.size() == 59u && linked[9] == 0xbf820001u && linked[11] == 0xbf850010u && linked[58] == 0xbf9f0000u); + Require(linked[12] == 0xbf07ff0eu && linked[13] == 0x22334455u && linked[14] == 0xbf850004u && linked[15] == 0xbf07ff0fu && linked[16] == 0x11u && linked[17] == 0xbf850001u && linked[18] == 0xbf82001bu); + Require(linked[20] == 0x778899aau && linked[26] == 0xbf078080u && linked[27] == 0xbf820016u && linked[34] == 0xbf068080u && linked[35] == 0xbf82000au && linked[44] == 0xbf068080u && linked[45] == 0xbf820004u); + Require(linked[46] == 0xbb0e0005u && linked[47] == 0xbf82ffdau && linked[48] == 0xbb0e0006u && linked[49] == 0xbf82ffd8u && linked[50] == 0xbe8e210eu && linked[51] == 0xbf82ffd6u); + Require(std::equal(first.begin(), first.end(), linked.begin() + 52) && std::equal(second.begin(), second.end(), linked.begin() + 55)); + const auto cfg = BuildCode(linked, linkedSwappc); + const auto block = [&](std::uint32_t word) { return cfg.FindBlockByProgramCounter(word * 4u).id; }; + Require(cfg.FindBlockByProgramCounter(46u * 4u).terminator.trueBlock == block(52u)); + Require(cfg.FindBlockByProgramCounter(52u * 4u).terminator.trueBlock == block(47u)); + Require(cfg.FindBlockByProgramCounter(48u * 4u).terminator.trueBlock == block(55u)); + Require(cfg.FindBlockByProgramCounter(55u * 4u).terminator.trueBlock == block(49u)); + Require(cfg.unlinkedCallProgramCounters == std::vector{50u * 4u}); + static_cast(TranslateCode(linked, linkedSwappc)); + + std::vector large(0x8000u, 0xbf800000u); + const std::vector> far{{{0x10ull, large}, {0x20ull, first}}}; + ExpectThrow("out of s_call_b64 range", [&] { static_cast(LinkCalls(program, {}, {}, calls, far)); }); + + const std::vector jumpTable{ + 0xbe8c1f00u, 0x800cff0cu, 0x00000058u, 0x820d800du, 0x83928202u, 0xd4c40016u, 0x02000500u, 0x8f128212u, 0xf4000386u, 0x24000000u, + 0xbf8cc07fu, 0x7d820003u, 0x808c0e0cu, 0x828d800du, 0xbefd210cu, 0x7e020281u, 0xbf820003u, 0x7e020282u, 0xbf820001u, 0x7e020283u, + 0xe0700000u, 0x80000100u, 0xbf810000u, 0x00000020u, 0x00000018u, 0x00000010u}; + ExpectThrow("jumps through a table in its code", [&] { static_cast(LinkCalls(RdnaInstructionDecoder{}.Decode(jumpTable), {}, {}, {}, {})); }); + const std::array compare{0xbf068008u}; + const auto comparesLink = Build({DecodeRdnaSopc(0u, compare, 0u), Call(4u, 8u, 1u), Sopp(8u, 1u), Sop1(12u, 0u, 0x20u, 8u)}); + Require(comparesLink.FindBlockByProgramCounter(0u).terminator.trueBlock == comparesLink.FindBlockByProgramCounter(12u).id); + const auto readsFirst = Build({Sop1(0u, 0u, 3u, 8u), Call(4u, 8u, 1u), Sopp(8u, 1u), Sop1(12u, 0u, 0x20u, 8u)}); + Require(readsFirst.FindBlockByProgramCounter(0u).terminator.trueBlock == readsFirst.FindBlockByProgramCounter(12u).id); + ExpectThrow("escapes the constant-offset call/return model", [] { + Build({Sop1(0u, 0u, 3u, 8u), Call(4u, 8u, 2u), Sopp(8u, 2u, 0xfffdu), Sopp(12u, 1u), Sop1(16u, 0u, 0x20u, 8u)}); + }); +} + static void CheckRejections() { ExpectThrow("invalid instruction boundary", [] { Build({Call(0u, 8u, 16u), Sopp(4u, 1u)}); @@ -280,6 +373,7 @@ int main(int argc, char** argv) { CheckCallAfterEnd(); CheckRejections(); CheckUserDataCalls(); + CheckTableCalls(); CheckGuards(""); } else { Require(argc == 2); diff --git a/core/libs/tests/ScalarSwappc.cpp b/core/libs/tests/ScalarSwappc.cpp index 6ab8dc4f05..7776183052 100644 --- a/core/libs/tests/ScalarSwappc.cpp +++ b/core/libs/tests/ScalarSwappc.cpp @@ -135,7 +135,7 @@ static void CheckThrows() { ExpectThrow("escapes the constant-offset call/return model", [] { Build(Program({ Sop1(0x00u, 4u, 0x1fu, 0u), - Sop2Literal(0x04u, 4u, 0x00u, 4u, 0x18u), + Sop2Literal(0x04u, 4u, 0x00u, 4u, 0x20u), Sop2Literal(0x0cu, 5u, 0x04u, 5u, 0u), Sop1(0x14u, 8u, 0x21u, 4u), Sop1(0x18u, 8u, 0x03u, 8u), diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 0b57570996..4660de2490 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -484,6 +484,9 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_symbol_names.py $) add_test(NAME guest_registry COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_registry.py $) + add_test(NAME needed_libraries COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_needed_libraries.py $) + set_tests_properties(needed_libraries PROPERTIES TIMEOUT 30) add_test(NAME macos_output COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_output.py $) add_test(NAME guest_host_libc COMMAND ${Python3_EXECUTABLE} @@ -549,6 +552,10 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_magic.py $) add_test(NAME input_preservation COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_preservation.py $) + add_test(NAME output_collisions COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_output_collisions.py $) + add_test(NAME secondary_output_collisions COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_secondary_output_collisions.py $) add_test(NAME input_file COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_file.py $) add_test(NAME null_symbol_relocations COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/domain/include/domain/Types.hpp b/core/relinker/domain/include/domain/Types.hpp index 31405091c5..f505333ec7 100644 --- a/core/relinker/domain/include/domain/Types.hpp +++ b/core/relinker/domain/include/domain/Types.hpp @@ -101,6 +101,7 @@ struct SysVDynamicSection { std::map ImportModules; std::set FunctionImports; std::set UnmatchedImportModules; + std::vector NeededLibraries; }; struct CallRegistryEntry { @@ -112,6 +113,7 @@ struct CallRegistryEntry { FileByteOffset TargetOffset; std::vector CallSites; bool CallSitesResolved; + bool Weak = false; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp index ed2ad7e750..4f7ec0fee8 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp @@ -14,6 +14,7 @@ struct MacOsBind { std::string Symbol; std::uint32_t Rva; std::int64_t Addend = 0; + bool Weak = false; }; struct MacOsExport { @@ -60,6 +61,7 @@ std::uint64_t GuestTlsBlockSize(const Domain::ProgramHeader& tls); std::vector WriteMacOsImage(MacOsImageInput& input); std::string MachOLoadPath(const std::string& path, const std::string& origin); +std::vector MachORunPaths(const std::string& paths, const std::string& origin); std::string MachODependency(const std::string& name, const std::string& origin); diff --git a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp index b8c3f7b06f..bd5928a33a 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp @@ -106,7 +106,7 @@ std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestIma for (const auto& dependency : dependencies) input.Dylibs.push_back(MachODependency(dependency, "@loader_path")); if (std::find(input.Dylibs.begin(), input.Dylibs.end(), "@rpath/libkernel.prx") == input.Dylibs.end()) input.Dylibs.push_back("@rpath/libkernel.prx"); - input.RunPaths.push_back(MachOLoadPath(runPath, "@loader_path")); + input.RunPaths = MachORunPaths(runPath, "@loader_path"); for (const auto slot : guest.InitArray) input.InitArrayRvas.push_back(image.GetRva(slot, 8)); for (const auto slot : guest.FiniArray) input.FiniArrayRvas.push_back(image.GetRva(slot, 8)); input.InitRva = guest.Init == 0 ? 0 : image.GetRva(guest.Init); diff --git a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp index c8db078cd8..838d0d6c8b 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp @@ -59,6 +59,7 @@ constexpr std::uint8_t RebaseOpcodeDoRebaseImmTimes = 0x50; constexpr std::uint8_t BindTypePointer = 1; constexpr std::uint8_t BindSpecialDylibFlatLookup = 0x0e; +constexpr std::uint8_t BindSymbolFlagsWeakImport = 0x1; constexpr std::uint8_t BindOpcodeDone = 0x00; constexpr std::uint8_t BindOpcodeSetDylibSpecialImm = 0x30; constexpr std::uint8_t BindOpcodeSetSymbolTrailingFlagsImm = 0x40; @@ -609,7 +610,7 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { bind.push_back(BindOpcodeSetTypeImm | BindTypePointer); for (const auto& item : input.Binds) { const auto [segment, offset] = Locate(segments, item.Rva, 8); - bind.push_back(BindOpcodeSetSymbolTrailingFlagsImm); + bind.push_back(item.Weak ? BindOpcodeSetSymbolTrailingFlagsImm | BindSymbolFlagsWeakImport : BindOpcodeSetSymbolTrailingFlagsImm); AppendCString(bind, item.Symbol); bind.push_back(BindOpcodeSetAddendSleb); AppendSleb(bind, item.Addend); diff --git a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp index 9928d52fa6..0b36789a41 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp @@ -15,6 +15,19 @@ std::string MachOLoadPath(const std::string& path, const std::string& origin) { return path; } +std::vector MachORunPaths(const std::string& paths, const std::string& origin) { + std::vector result; + for (std::size_t begin = 0;;) { + const auto end = paths.find(':', begin); + const auto path = paths.substr(begin, end == std::string::npos ? end : end - begin); + if (path.empty()) throw Domain::RelinkerException("Empty macOS run path component is not supported"); + result.push_back(MachOLoadPath(path, origin)); + if (end == std::string::npos) break; + begin = end + 1; + } + return result; +} + std::vector ReadNeededLibraries(const Domain::SysVDynamicSection& dynamicSection) { const auto& bytes = dynamicSection.DynamicSegmentData; if (bytes.size() % 16 != 0) throw Domain::RelinkerException("Invalid dynamic segment size"); @@ -55,12 +68,12 @@ std::vector MacOsMachOPatcher::Patch(std::vector sou input.Image = ℑ input.Rebases = relocations.BaseRelocations; for (const auto& import : relocations.Imports) - input.Binds.push_back({"_" + import.Name, import.TargetRva, static_cast(import.Addend)}); + input.Binds.push_back({"_" + import.Name, import.TargetRva, static_cast(import.Addend), import.Weak}); for (const auto& library : ReadNeededLibraries(dynamicSection)) input.Dylibs.push_back(MachODependency(library, "@executable_path")); if (std::find(input.Dylibs.begin(), input.Dylibs.end(), "@rpath/libkernel.prx") == input.Dylibs.end()) input.Dylibs.push_back("@rpath/libkernel.prx"); - input.RunPaths.push_back(MachOLoadPath(runPath, "@executable_path")); + input.RunPaths = MachORunPaths(runPath, "@executable_path"); input.EntryRva = image.GetEntryRva(); input.Trampolines = &trampolines; return WriteMacOsImage(input); diff --git a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp index 6a90b28b7d..6506f2a745 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -97,6 +97,9 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con return rva; }; + const auto parentVariable = addWideString(u"ANYPS5_LOADEXEC_PARENT"); + const auto parentText = reserve(34); + const auto skipFirstArgument = reserve(4); const auto programPath = reserve(PathCapacity * 2); const auto modulePath = reserve(PathCapacity * 2); const auto argumentBlock = reserve(8); @@ -234,6 +237,8 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con (import.Library.empty() ? std::string{} : " from " + import.Library) + "\n"); const auto argumentError = errors.size(); errors.push_back("FAIL: cannot prepare command-line arguments\n"); + const auto parentError = errors.size(); + errors.push_back("FAIL: cannot wait for the previous LoadExec process\n"); const auto directoryError = errors.size(); errors.push_back("FAIL: cannot make the executable directory the current directory\n"); const auto traceError = errors.size(); @@ -396,6 +401,49 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con }; code.Emit({0x53, 0x55, 0x56, 0x57, 0x41, 0x54, 0x48, 0x83, 0xec, 0x60}); + code.Rip({0x48, 0x8d, 0x0d}, parentVariable); + code.Rip({0x48, 0x8d, 0x15}, parentText); + code.Emit({0x41, 0xb8, 17, 0, 0, 0}); + call("GetEnvironmentVariableW"); + code.Emit({0x85, 0xc0}); + const auto noParent = code.Branch({0x0f, 0x84}); + code.Emit({0x83, 0xf8, 16}); + const auto shortValue = code.Branch({0x0f, 0x86}); + fail(parentError, 1); + code.PatchBranch(shortValue, code.GetRva()); + code.Rip({0xc7, 0x05}, skipFirstArgument - 4); + code.U32(1); + code.Rip({0x48, 0x8d, 0x35}, parentText); + code.Emit({0x31, 0xff}); + const auto digit = code.GetRva(); + code.Emit({0x0f, 0xb7, 0x06, 0x85, 0xc0}); + const auto parsed = code.Branch({0x0f, 0x84}); + code.Emit({0x83, 0xe8, 0x30, 0x83, 0xf8, 9}); + const auto decimal = code.Branch({0x0f, 0x86}); + code.Emit({0x83, 0xe8, 0x31, 0x83, 0xf8, 5}); + const auto hex = code.Branch({0x0f, 0x86}); + fail(parentError, 1); + code.PatchBranch(hex, code.GetRva()); + code.Emit({0x83, 0xc0, 10}); + code.PatchBranch(decimal, code.GetRva()); + code.Emit({0x48, 0xc1, 0xe7, 4, 0x48, 0x09, 0xc7, 0x48, 0x83, 0xc6, 2}); + code.Rip({0xe9}, digit); + code.PatchBranch(parsed, code.GetRva()); + code.Emit({0x48, 0x89, 0xf9, 0xba, 0xff, 0xff, 0xff, 0xff}); + call("WaitForSingleObject"); + code.Emit({0x85, 0xc0}); + const auto waited = code.Branch({0x0f, 0x84}); + fail(parentError, 1); + code.PatchBranch(waited, code.GetRva()); + code.Emit({0x48, 0x89, 0xf9}); + call("CloseHandle"); + requireNonzero(parentError, 1); + code.Rip({0x48, 0x8d, 0x0d}, parentVariable); + code.Emit({0x31, 0xd2}); + call("SetEnvironmentVariableW"); + requireNonzero(parentError, 1); + code.PatchBranch(noParent, code.GetRva()); + code.Emit({0x31, 0xc9}); code.Rip({0x48, 0x8d, 0x15}, programPath); code.Emit({0x41, 0xb8}); @@ -621,6 +669,13 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con requireNonzero(argumentError, 0xc000000du); code.Rip({0x48, 0x89, 0x05}, wideArguments); code.Emit({0x48, 0x89, 0xc3}); + code.Rip({0x83, 0x3d}, skipFirstArgument - 1); + code.Emit({0}); + const auto retainProgramName = code.Branch({0x0f, 0x84}); + code.Emit({0x48, 0x83, 0xc3, 8}); + code.Rip({0x83, 0x2d}, argumentCount - 1); + code.Emit({1}); + code.PatchBranch(retainProgramName, code.GetRva()); code.Rip({0x8b, 0x2d}, argumentCount); code.Emit({0x8d, 0x45, 0xff, 0x3d}); code.U32(ArgumentCapacity - 1); diff --git a/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp index aabd25ac42..32775ce92c 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp @@ -38,6 +38,9 @@ void validatePng(const std::vector& bytes, const std::filesystem:: const auto length = readBigEndian(bytes, offset); if (length > bytes.size() - offset - 12) fail("truncated chunk data"); const auto type = readBigEndian(bytes, offset + 4); + for (std::size_t index = offset + 4; index < offset + 8; ++index) + if (!((bytes[index] >= 'A' && bytes[index] <= 'Z') || (bytes[index] >= 'a' && bytes[index] <= 'z'))) + fail("invalid chunk type"); std::uint32_t crc = 0xffffffffu; for (std::size_t index = offset + 4; index < offset + 8 + length; ++index) { crc ^= bytes[index]; diff --git a/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp index 71ce4f2bad..3901ef1c17 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp @@ -5,7 +5,7 @@ namespace Elfpatcher::Windows { WindowsImports WindowsImportBuilder::Build(const std::uint32_t sectionRva) const { - const std::vector names = {"CreateFileW", "ExitProcess", "FormatMessageA", "FreeLibrary", "GetCommandLineW", "GetFileAttributesW", "GetLastError", "GetModuleFileNameW", "GetModuleHandleA", "GetModuleHandleW", "GetProcAddress", "GetStdHandle", "GetSystemDirectoryW", "LoadLibraryExA", "LoadLibraryExW", "LocalFree", "MultiByteToWideChar", "RaiseException", "SetCurrentDirectoryW", "SetStdHandle", "VirtualAlloc", "WideCharToMultiByte", "WriteFile", "lstrcatA", "lstrcmpA", "lstrcmpiA", "lstrcpyA", "lstrlenA"}; + const std::vector names = {"CloseHandle", "CreateFileW", "ExitProcess", "FormatMessageA", "FreeLibrary", "GetCommandLineW", "GetEnvironmentVariableW", "GetFileAttributesW", "GetLastError", "GetModuleFileNameW", "GetModuleHandleA", "GetModuleHandleW", "GetProcAddress", "GetStdHandle", "GetSystemDirectoryW", "LoadLibraryExA", "LoadLibraryExW", "LocalFree", "MultiByteToWideChar", "RaiseException", "SetCurrentDirectoryW", "SetEnvironmentVariableW", "SetStdHandle", "VirtualAlloc", "WaitForSingleObject", "WideCharToMultiByte", "WriteFile", "lstrcatA", "lstrcmpA", "lstrcmpiA", "lstrcpyA", "lstrlenA"}; WindowsImports result{{".idata", sectionRva, SectionRead | SectionWrite | 0x40u, std::vector(40)}, {sectionRva, 40}, {}, {}}; auto& bytes = result.Section.Data; const auto lookupOffset = bytes.size(); diff --git a/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp b/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp index 772189083e..b7aaa68f04 100644 --- a/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp +++ b/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp @@ -70,6 +70,10 @@ void checkChunks() { const auto note = chunk("tEXt", {'n', 0, 'v'}); check("unknown-critical", {chunk("ABCD", {}), idat}, false); check("ancillary", {note, idat}, true); + check("private-ancillary", {chunk("vpAg", {}), idat}, true); + check("reserved-ancillary", {chunk("tExT", {}), idat}, true); + for (const auto& type : {"t1Xt", "tEX!", "tEX\x80"}) + check("invalid-chunk-type", {chunk(type, {}), idat}, false); const auto first = chunk("IDAT", {data.begin(), data.begin() + 6}); const auto last = chunk("IDAT", {data.begin() + 6, data.end()}); check("consecutive", {first, last}, true); diff --git a/core/relinker/io/src/FileWriter.cpp b/core/relinker/io/src/FileWriter.cpp index e80448f503..49df133411 100644 --- a/core/relinker/io/src/FileWriter.cpp +++ b/core/relinker/io/src/FileWriter.cpp @@ -1,28 +1,110 @@ #include #include #include +#include #include +#include +#ifndef _WIN32 +#include +#include +#include +#include +#include +#include +#endif namespace Io { -void FileWriter::Write(const std::string& path, const std::vector& data) { - std::ofstream f(NativePath(path), std::ios::binary); +namespace { + +bool writeFile(const std::filesystem::path& file, const std::string& path, const std::ios::openmode mode, const char* data, const std::size_t size) { + std::ofstream f(file, mode); if (!f) - throw Domain::RelinkerException("Cannot open output file: " + path); - f.write(reinterpret_cast(data.data()), static_cast(data.size())); + return false; + f.write(data, static_cast(size)); f.close(); if (!f) throw Domain::RelinkerException("Failed to write file: " + path); + return true; +} + +#ifndef _WIN32 +int createTemporary(const std::filesystem::path& target, const mode_t mode, std::filesystem::path& temporary) { + constexpr std::string_view characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + std::random_device device; + for (int attempt = 0; attempt < 100; ++attempt) { + std::string suffix = "."; + for (int index = 0; index < 6; ++index) + suffix += characters[device() % characters.size()]; + temporary = target; + temporary += suffix; + const int descriptor = open(temporary.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, mode); + if (descriptor >= 0 || errno != EEXIST) + return descriptor; + } + return -1; +} + +bool writeAll(const int descriptor, const char* data, std::size_t size) { + while (size != 0) { + const auto written = write(descriptor, data, size); + if (written < 0 && errno == EINTR) + continue; + if (written <= 0) + return false; + data += written; + size -= static_cast(written); + } + return true; +} + +bool replaceFile(const std::filesystem::path& target, const std::string& path, const char* data, const std::size_t size) { + std::error_code error; + const auto previous = std::filesystem::symlink_status(target, error); + const bool regular = std::filesystem::is_regular_file(previous); + if (!regular && previous.type() != std::filesystem::file_type::not_found) + return false; + const auto mode = regular ? static_cast(previous.permissions() & std::filesystem::perms::all) : static_cast(0666); + std::filesystem::path temporary; + const int descriptor = createTemporary(target, mode, temporary); + if (descriptor < 0) + return false; + if (regular && fchmod(descriptor, mode) != 0) { + close(descriptor); + std::filesystem::remove(temporary, error); + return false; + } + const bool written = writeAll(descriptor, data, size); + if (close(descriptor) != 0 || !written) { + std::filesystem::remove(temporary, error); + throw Domain::RelinkerException("Failed to write file: " + path); + } + std::filesystem::rename(temporary, target, error); + if (!error) + return true; + std::filesystem::remove(temporary, error); + return false; +} +#endif + +void writeOutput(const std::string& path, const std::ios::openmode mode, const char* data, const std::size_t size) { + const auto target = NativePath(path); +#ifndef _WIN32 + if (replaceFile(target, path, data, size)) + return; +#endif + if (!writeFile(target, path, mode, data, size)) + throw Domain::RelinkerException("Cannot open output file: " + path); +} + +} + +void FileWriter::Write(const std::string& path, const std::vector& data) { + writeOutput(path, std::ios::binary, reinterpret_cast(data.data()), data.size()); } void FileWriter::Write(const std::string& path, const std::string& content) { - std::ofstream f(NativePath(path)); - if (!f) - throw Domain::RelinkerException("Cannot open output file: " + path); - f << content; - f.close(); - if (!f) - throw Domain::RelinkerException("Failed to write file: " + path); + writeOutput(path, std::ios::out, content.data(), content.size()); } } diff --git a/core/relinker/io/tests/FileWriterTests.cpp b/core/relinker/io/tests/FileWriterTests.cpp index 8edc710d55..900518b16f 100644 --- a/core/relinker/io/tests/FileWriterTests.cpp +++ b/core/relinker/io/tests/FileWriterTests.cpp @@ -1,9 +1,19 @@ #include #include +#include +#include +#include #include +#include #include #include +#include #include +#include +#include +#include +#include +#include namespace { @@ -21,6 +31,141 @@ void requireWriteFailure(const TValue& data) { throw std::runtime_error("Write to /dev/full reported success"); } +std::string readFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +std::vector siblings(const std::filesystem::path& output) { + std::vector names; + for (const auto& entry : std::filesystem::directory_iterator(output.parent_path())) { + const auto name = entry.path().filename().string(); + if (name.starts_with(output.filename().string() + ".")) + names.push_back(name); + } + return names; +} + +template +void requireRunningReplacement(const std::filesystem::path& directory, const TValue& data, const bool leftover) { + const auto output = directory / (leftover ? "leftover" : "running"); + if (leftover) + std::ofstream(directory / "leftover.tmp", std::ios::binary) << "killed"; + std::filesystem::copy_file("/bin/sleep", output); + const auto permissions = std::filesystem::perms::owner_all | std::filesystem::perms::group_all; + std::filesystem::permissions(output, permissions); + const auto previous = readFile(output); + const pid_t child = fork(); + if (child < 0) + throw std::runtime_error("Cannot start the previous executable"); + if (child == 0) { + execl(output.c_str(), "sleep", "30", nullptr); + _exit(127); + } + std::error_code error; + bool started = false; + for (int attempt = 0; attempt < 2000 && !started; ++attempt) { + started = std::filesystem::read_symlink("/proc/" + std::to_string(child) + "/exe", error) == output; + if (!started) + usleep(1000); + } + const int descriptor = open(output.c_str(), O_RDONLY); + std::string failure; + if (started) { + const auto mask = umask(077); + try { + Io::FileWriter().Write(output.string(), data); + } catch (const std::exception& e) { + failure = e.what(); + } + umask(mask); + } + std::string kept(previous.size(), '\0'); + const bool keptPrevious = pread(descriptor, kept.data(), kept.size(), 0) == static_cast(kept.size()) && kept == previous; + close(descriptor); + const bool running = waitpid(child, nullptr, WNOHANG) == 0; + if (running) { + kill(child, SIGKILL); + waitpid(child, nullptr, 0); + } + if (!started) + throw std::runtime_error("The previous executable did not start"); + if (!failure.empty()) + throw std::runtime_error("Replacing a running executable failed: " + failure); + if (readFile(output) != std::string(data.begin(), data.end())) + throw std::runtime_error("Running executable was not replaced"); + if ((std::filesystem::status(output).permissions() & std::filesystem::perms::all) != permissions) + throw std::runtime_error("Replacement did not keep the previous permissions"); + if (!keptPrevious) + throw std::runtime_error("An open descriptor no longer reads the previous file"); + if (!running) + throw std::runtime_error("Running executable stopped"); + if (siblings(output) != (leftover ? std::vector{"leftover.tmp"} : std::vector{})) + throw std::runtime_error("Temporary file left behind"); + if (leftover && readFile(directory / "leftover.tmp") != "killed") + throw std::runtime_error("A file left by an earlier run was changed"); +} + +template +void requireFailedReplacement(const std::filesystem::path& directory, const TValue& data) { + const auto output = directory / "limited"; + std::ofstream(output, std::ios::binary) << "previous"; + rlimit limit{}; + getrlimit(RLIMIT_FSIZE, &limit); + rlimit lowered = limit; + lowered.rlim_cur = 1; + const auto handler = std::signal(SIGXFSZ, SIG_IGN); + setrlimit(RLIMIT_FSIZE, &lowered); + std::string failure; + try { + Io::FileWriter().Write(output.string(), data); + } catch (const Domain::RelinkerException& e) { + failure = e.what(); + } + setrlimit(RLIMIT_FSIZE, &limit); + std::signal(SIGXFSZ, handler); + if (failure != "Failed to write file: " + output.string()) + throw std::runtime_error("Write past the file size limit did not fail: " + failure); + if (readFile(output) != "previous" || !siblings(output).empty()) + throw std::runtime_error("Failed write did not keep the previous file"); +} + +template +void requireInPlaceWrites(const std::filesystem::path& directory, const TValue& data) { + const std::string expected(data.begin(), data.end()); + const auto target = directory / "target"; + const auto link = directory / "link"; + std::ofstream(target, std::ios::binary) << "previous"; + std::filesystem::create_symlink(target, link); + Io::FileWriter().Write(link.string(), data); + if (!std::filesystem::is_symlink(link) || readFile(target) != expected) + throw std::runtime_error("Symbolic link output was not written through"); + const auto reserved = directory / "reserved"; + std::ofstream(reserved, std::ios::binary) << "previous"; + std::ofstream(directory / "reserved.tmp", std::ios::binary) << "unrelated"; + Io::FileWriter().Write(reserved.string(), data); + if (readFile(reserved) != expected || readFile(directory / "reserved.tmp") != "unrelated") + throw std::runtime_error("Existing temporary file name was not left alone"); +} + +template +void requireReplacement(const TValue& data) { + const auto directory = std::filesystem::canonical(std::filesystem::temp_directory_path()) / ("anyps5-file-writer-" + std::to_string(getpid())); + std::error_code error; + std::filesystem::remove_all(directory, error); + std::filesystem::create_directory(directory); + try { + requireRunningReplacement(directory, data, false); + requireRunningReplacement(directory, data, true); + requireFailedReplacement(directory, data); + requireInPlaceWrites(directory, data); + } catch (...) { + std::filesystem::remove_all(directory, error); + throw; + } + std::filesystem::remove_all(directory, error); +} + } int main(int argc, char* argv[]) { @@ -28,12 +173,17 @@ int main(int argc, char* argv[]) { if (argc != 2) throw std::runtime_error("Expected binary or text"); const std::string mode = argv[1]; for (const std::size_t size : {std::size_t{3}, std::size_t{65536}}) { - if (mode == "binary") - requireWriteFailure(std::vector(size, 0x41)); - else if (mode == "text") - requireWriteFailure(std::string(size, 'A')); - else + if (mode == "binary") { + const std::vector data(size, 0x41); + requireWriteFailure(data); + requireReplacement(data); + } else if (mode == "text") { + const std::string data(size, 'A'); + requireWriteFailure(data); + requireReplacement(data); + } else { throw std::runtime_error("Unknown test mode: " + mode); + } } } catch (const std::exception& e) { std::cerr << e.what() << '\n'; diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 557840d16a..6b967356a5 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -40,7 +41,7 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, input.InputPath = std::filesystem::absolute(Io::NativePath(inputPath)).lexically_normal(); input.OutputPath = Io::NativePath(absPath); if (!args.skipSceModule) { - auto modules = Cli::ReadModules(input.InputPath, Io::NativePath(args.sceModulePath), args.excludedSceModules, args.moduleDirectories); + auto modules = Cli::ReadModules(std::filesystem::absolute(Io::NativePath(args.inputPath)).lexically_normal(), Io::NativePath(args.sceModulePath), args.excludedSceModules, args.moduleDirectories); input.ModuleRoot = std::move(modules.Root); input.Modules = std::move(modules.Modules); input.RuntimeDirectories = std::move(modules.RuntimeDirectories); @@ -69,6 +70,100 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, return {inputPath, absPath, std::move(converted.Analysis), std::move(converted.Modules), std::move(converted.Executable), std::move(converted.WindowsDiagnostics)}; } +#ifdef _WIN32 + +class OutputIdentityDirectory { +public: + OutputIdentityDirectory(const OutputIdentityDirectory&) = delete; + OutputIdentityDirectory& operator=(const OutputIdentityDirectory&) = delete; + + explicit OutputIdentityDirectory(const std::filesystem::path& parent) { + std::random_device random; + for (unsigned attempt = 0; attempt < 64; ++attempt) { + const auto candidate = parent / (".anyps5-output-identity-" + std::to_string(random()) + "-" + std::to_string(random())); + std::error_code error; + if (std::filesystem::create_directory(candidate, error)) { + directory = candidate; + return; + } + if (error && error != std::errc::file_exists) + throw std::filesystem::filesystem_error("Cannot create output identity directory", candidate, error); + } + throw Domain::RelinkerException("Cannot reserve an output identity directory in " + Io::Utf8Path(parent)); + } + + ~OutputIdentityDirectory() { + if (directory.empty()) return; + try { + Remove(); + } catch (const std::exception& error) { + std::cerr << "FAIL: Output identity cleanup: " << error.what() << '\n'; + } + } + + std::filesystem::path GetPath() const { return directory / "file"; } + const std::filesystem::path& GetDirectory() const { return directory; } + + void Remove() { + std::filesystem::remove(GetPath()); + std::filesystem::remove(directory); + directory.clear(); + } + +private: + std::filesystem::path directory; +}; + +#endif + +bool SameOutputFile(const std::filesystem::path& first, const std::filesystem::path& second) { + if (std::filesystem::equivalent(first, second)) return true; +#ifdef _WIN32 + if (std::filesystem::status(first).permissions() != std::filesystem::status(second).permissions() && + std::filesystem::hard_link_count(first) > 1 && std::filesystem::hard_link_count(second) > 1) { + for (const auto& path : {first, second}) { + if ((std::filesystem::status(path).permissions() & std::filesystem::perms::owner_write) == std::filesystem::perms::none) + throw Domain::RelinkerException("Cannot compare read-only output identities: " + Io::Utf8Path(path)); + } + OutputIdentityDirectory firstDirectory(first.parent_path()); + OutputIdentityDirectory secondDirectory(second.parent_path()); + std::filesystem::create_hard_link(first, firstDirectory.GetPath()); + std::filesystem::create_hard_link(second, secondDirectory.GetPath()); + const bool same = std::filesystem::equivalent(firstDirectory.GetPath(), secondDirectory.GetPath()); + secondDirectory.Remove(); + firstDirectory.Remove(); + return same; + } +#endif + return false; +} + +bool SameOutputDestination(const std::filesystem::path& first, const std::filesystem::path& second) { + const auto firstPath = std::filesystem::weakly_canonical(first); + const auto secondPath = std::filesystem::weakly_canonical(second); + if (firstPath == secondPath) return true; + if (std::filesystem::exists(first) && std::filesystem::exists(second)) return SameOutputFile(first, second); +#ifdef _WIN32 + auto firstName = Io::Utf8Path(firstPath.filename()); + auto secondName = Io::Utf8Path(secondPath.filename()); + for (auto* name : {&firstName, &secondName}) + for (auto& character : *name) + if (character >= 'A' && character <= 'Z') character = static_cast(character + ('a' - 'A')); + if (firstName == secondName && std::filesystem::exists(firstPath.parent_path()) && + std::filesystem::exists(secondPath.parent_path()) && std::filesystem::equivalent(firstPath.parent_path(), secondPath.parent_path())) { + OutputIdentityDirectory probe(firstPath.parent_path()); + auto alternateName = Io::Utf8Path(probe.GetDirectory().filename()); + for (auto& character : alternateName) + if (character >= 'a' && character <= 'z') character = static_cast(character - ('a' - 'A')); + const auto alternate = firstPath.parent_path() / Io::NativePath(alternateName); + const bool same = std::filesystem::exists(alternate) && std::filesystem::equivalent(probe.GetDirectory(), alternate); + probe.Remove(); + return same; + } +#endif + return false; +} + int Run(const int argc, char* argv[]) { Cli::Args args; try { @@ -170,6 +265,7 @@ int Run(const int argc, char* argv[]) { checkOutputInputs(std::filesystem::absolute("windows-diagnostics-imports.txt"), "Windows import diagnostics"); std::vector registryPaths; + std::vector neededPaths; if (args.writeRegistry) { for (const auto& executable : executables) { const std::filesystem::path outFsPath = Io::NativePath(executable.OutputPath); @@ -179,11 +275,35 @@ int Run(const int argc, char* argv[]) { 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"); + for (const auto& path : registryPaths) { + auto name = Io::Utf8Path(path.filename()); + name.resize(name.size() - std::string(".registry.json").size()); + neededPaths.push_back(path.parent_path() / Io::NativePath(name + ".needed.json")); + } + for (const auto& path : neededPaths) checkOutputInputs(path, "Needed libraries"); } checkOutputInputs(Io::NativePath(absPath), "Executable"); for (const auto& artifact : guestArtifacts) checkOutputInputs(artifact.Path, "Guest module"); + std::vector outputPaths; + for (const auto& executable : executables) outputPaths.push_back(Io::NativePath(executable.OutputPath)); + for (const auto& artifact : guestArtifacts) outputPaths.push_back(artifact.Path); + outputPaths.insert(outputPaths.end(), registryPaths.begin(), registryPaths.end()); + if (desktopEntry) { + outputPaths.push_back(desktopEntry->IconPath); + outputPaths.push_back(desktopEntry->EntryPath); + } + if (args.windowsDiagnostics) outputPaths.push_back(std::filesystem::absolute("windows-diagnostics-imports.txt")); + for (std::size_t index = 0; index < outputPaths.size(); ++index) { + for (std::size_t previous = 0; previous < index; ++previous) { + const auto& first = outputPaths[index]; + const auto& second = outputPaths[previous]; + if (SameOutputDestination(first, second)) + throw Domain::RelinkerException("Output paths collide: " + Io::Utf8Path(first) + " and " + Io::Utf8Path(second)); + } + } + if (args.windowsDiagnostics) fileWriter.Write("windows-diagnostics-imports.txt", executables.front().Diagnostics); const auto outputCount = guestArtifacts.size() + executables.size(); progress.Report({Domain::ProgressStage::Write, absPath, 0, outputCount}); @@ -199,10 +319,14 @@ int Run(const int argc, char* argv[]) { } if (args.writeRegistry) { const auto registryWriter = std::make_shared(); - for (std::size_t index = 0; index < executables.size(); ++index) + for (std::size_t index = 0; index < executables.size(); ++index) { fileWriter.Write(Io::Utf8Path(registryPaths[index]), registryWriter->WriteCallRegistry(executables[index].Result.RegistryEntries)); - for (std::size_t index = 0; index < guestArtifacts.size(); ++index) + fileWriter.Write(Io::Utf8Path(neededPaths[index]), registryWriter->WriteNeededLibraries(executables[index].Result.DynamicSection.NeededLibraries)); + } + for (std::size_t index = 0; index < guestArtifacts.size(); ++index) { fileWriter.Write(Io::Utf8Path(registryPaths[executables.size() + index]), registryWriter->WriteModuleImports(guestArtifacts[index].Imports)); + fileWriter.Write(Io::Utf8Path(neededPaths[executables.size() + index]), registryWriter->WriteNeededLibraries(guestArtifacts[index].NeededLibraries)); + } } progress.Finish(); if (desktopEntry) std::cout << "Desktop entry: " << Io::Utf8Path(desktopEntryWriter.Write(*desktopEntry)) << '\n'; diff --git a/core/relinker/relinker/include/relinker/domain/ICallRegistryWriter.hpp b/core/relinker/relinker/include/relinker/domain/ICallRegistryWriter.hpp index d528667e97..57f2e9c218 100644 --- a/core/relinker/relinker/include/relinker/domain/ICallRegistryWriter.hpp +++ b/core/relinker/relinker/include/relinker/domain/ICallRegistryWriter.hpp @@ -13,6 +13,7 @@ public: virtual std::string WriteCallRegistry(const std::vector& entries) = 0; virtual std::string WriteModuleImports(const std::vector& entries) = 0; + virtual std::string WriteNeededLibraries(const std::vector& libraries) = 0; }; } diff --git a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index bef96dbe10..31e5781a54 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -66,6 +66,7 @@ struct GuestArtifact { std::vector Bytes; std::vector Imports; std::filesystem::path SourcePath; + std::vector NeededLibraries; }; struct GuestModuleInput { diff --git a/core/relinker/relinker/include/relinker/output/CallRegistryWriter.hpp b/core/relinker/relinker/include/relinker/output/CallRegistryWriter.hpp index 79b196f869..5c16c9057b 100644 --- a/core/relinker/relinker/include/relinker/output/CallRegistryWriter.hpp +++ b/core/relinker/relinker/include/relinker/output/CallRegistryWriter.hpp @@ -9,6 +9,7 @@ class CallRegistryWriter : public ICallRegistryWriter { public: std::string WriteCallRegistry(const std::vector& entries) override; std::string WriteModuleImports(const std::vector& entries) override; + std::string WriteNeededLibraries(const std::vector& libraries) override; }; } diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index d7598f0a26..b9844c22cf 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -277,6 +277,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path for (const auto& name : neededNames) addHost(name); for (const auto& image : images) for (const auto& dependency : image.Dependencies) addHost(dependency); if (uniqueHosts.contains("libSceLibcInternal.prx") && uniqueHosts.insert("libc.prx").second) hostLibraries.push_back("libc.prx"); + dynamic.NeededLibraries = hostLibraries; dynamic.DynamicSegmentData.clear(); const auto addNeeded = [&](const std::string& name) { Io::AppendU64(dynamic.DynamicSegmentData, 1); @@ -356,7 +357,10 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } } dynamic.GuestModules.push_back(std::move(runtime)); - artifacts.push_back({target, std::move(output), std::move(imports), image.SourcePath}); + std::vector neededLibraries; + for (const auto& name : image.Dependencies) + if (findGuest(name) == guestNames.end()) neededLibraries.push_back(name); + artifacts.push_back({target, std::move(output), std::move(imports), image.SourcePath, std::move(neededLibraries)}); progress.Report({Domain::ProgressStage::Modules, Io::Utf8Path(image.SourcePath), artifacts.size(), order.size()}); } return artifacts; diff --git a/core/relinker/relinker/src/output/CallRegistryWriter.cpp b/core/relinker/relinker/src/output/CallRegistryWriter.cpp index e598056d6e..8fbd461e89 100644 --- a/core/relinker/relinker/src/output/CallRegistryWriter.cpp +++ b/core/relinker/relinker/src/output/CallRegistryWriter.cpp @@ -52,7 +52,8 @@ std::string CallRegistryWriter::WriteCallRegistry(const std::vector& libraries) { + std::ostringstream out; + out << "[\n"; + for (std::size_t index = 0; index < libraries.size(); ++index) { + out << " " << _jsonString(libraries[index]); + if (index + 1 < libraries.size()) out << ","; + out << "\n"; + } + out << "]\n"; + return out.str(); +} + } diff --git a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp index 32cb6523c4..0d8bdd763f 100644 --- a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp +++ b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp @@ -68,6 +68,7 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( std::uint32_t originalJmprelCount) { SysVDynamicSection result; + result.NeededLibraries = neededLibraries; result.DynStrData.push_back(0); std::vector neededOffsets; diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 4940918eb9..8e082aa3e9 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -525,6 +525,7 @@ RelinkResult RelinkerPipeline::Relink() { entry.TargetSection = ".got"; entry.TargetOffset = ref.RelocationAddress; entry.CallSitesResolved = false; + entry.Weak = ref.Weak; entries.push_back(std::move(entry)); } diff --git a/core/relinker/relinker/tests/test_call_registry.py b/core/relinker/relinker/tests/test_call_registry.py index c12ad56ff0..1bc1749afe 100644 --- a/core/relinker/relinker/tests/test_call_registry.py +++ b/core/relinker/relinker/tests/test_call_registry.py @@ -35,9 +35,31 @@ def main(): entries = json.loads(registry.read_text(encoding="utf-8")) expected = [{"nid": name, "library": "", "relocationType": "R_X86_64_GLOB_DAT", "relocationOffset": "0x700", "targetSection": ".got", "targetOffset": "0x300", - "callSites": ["0x200"], "callSitesResolved": True}] + "callSites": ["0x200"], "callSitesResolved": True, "weak": False}] if entries != expected: raise AssertionError((name, options, entries)) + needed = json.loads(output.with_suffix(".needed.json").read_text(encoding="utf-8")) + if needed != ["lib.so"]: + raise AssertionError((name, options, needed)) + strings = b"\0lib.so\0" + names[0].encode("utf-8") + b"\0" + image = fixture(str_size=len(strings), table_bytes=strings) + struct.pack_into(" bytearray: return image -def run_case(relinker: Path, work: Path, name: str, args, code: bytes, with_import=False): +def run_case(relinker: Path, work: Path, name: str, args, code: bytes, with_import=False, trailing_data=b""): source = work / (name + ".elf") output = work / (name + ".exe") - source.write_bytes(fixture(code, with_import)) + image = fixture(code, with_import) + end = 0x800 + len(code) + image[end:end + len(trailing_data)] = trailing_data + source.write_bytes(image) result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", *args, str(source), str(output)], capture_output=True, text=True, timeout=20) @@ -72,6 +75,21 @@ def main(): assert result.returncode == 2 and not output.exists(), (name, result.stdout, result.stderr) assert "Forbidden syscall instruction at code offset 0x1801" in result.stderr, (name, result.stderr) + for name, instruction in forbidden: + result, output = run_case(relinker, work, name + "-at-end", [], b"\x90" + instruction) + assert result.returncode == 2 and not output.exists(), (name, result.stdout, result.stderr) + assert "Forbidden syscall instruction at code offset 0x1801" in result.stderr, (name, result.stderr) + + for name, instruction in forbidden: + result, output = run_case(relinker, work, name + "-outside", [], b"\x90\xc3", + trailing_data=instruction) + assert result.returncode == 0 and output.exists(), (name, result.stdout, result.stderr) + + result, output = run_case(relinker, work, name + "-truncated", [], b"\x90" + instruction[:-1], + trailing_data=instruction[-1:]) + assert result.returncode == 2 and not output.exists(), (name, result.stdout, result.stderr) + assert "Cannot decode instruction" in result.stderr and "offset 0x1801" in result.stderr, (name, result.stderr) + result, output = run_case(relinker, work, "immediate-decoys", [], b"\x48\xb8\x0f\x05\xcd\x80\x0f\x34\x0f\x07\xc3") assert result.returncode == 0 and output.exists(), (result.stdout, result.stderr) diff --git a/core/relinker/relinker/tests/test_guest_module_args.py b/core/relinker/relinker/tests/test_guest_module_args.py index 509c98c23e..6c3de5f3a4 100644 --- a/core/relinker/relinker/tests/test_guest_module_args.py +++ b/core/relinker/relinker/tests/test_guest_module_args.py @@ -22,9 +22,10 @@ def args_module(): image = bytearray(0x3000) image[:16] = b'\x7fELF\x02\x01\x01' + bytes(9) struct.pack_into(' second -> first; entry/getargv arguments, Unicode, cwd/resources, ' + 'rejected targets, shutdown without guest or host exit handlers passed') + + +if __name__ == '__main__': + main() diff --git a/core/relinker/relinker/tests/test_macos_output.py b/core/relinker/relinker/tests/test_macos_output.py index d6c58adad2..dfbe252bca 100644 --- a/core/relinker/relinker/tests/test_macos_output.py +++ b/core/relinker/relinker/tests/test_macos_output.py @@ -14,6 +14,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent / "macos")) from import_fixture import fixture as import_fixture from nid import nid from tls_fixture import fixture as tls_fixture +from test_guest_module_directories import module_with_symbol MH_MAGIC_64 = 0xFEEDFACF CPU_TYPE_X86_64 = 0x01000007 @@ -145,12 +146,43 @@ def check_options(relinker, directory): check(result.returncode != 0 and "--windows conflicts with --macos" in result.stdout + result.stderr, "--macos together with --windows was accepted") +def check_runpath_lists(relinker, directory): + source = Path(directory) / "runpaths.elf" + source.write_bytes(bytes(import_fixture())) + modules = Path(directory) / "sce_module" + modules.mkdir() + (modules / "module.prx").write_bytes(module_with_symbol(True)) + for index, (configured, expected) in enumerate(( + ("$ORIGIN/first:$ORIGIN/second", ["@executable_path/first", "@executable_path/second"]), + ("$ORIGIN:$ORIGIN/libs:$ORIGIN", ["@executable_path", "@executable_path/libs", "@executable_path"]), + ("/first:/second", ["/first", "/second"]))): + output = Path(directory) / f"runpaths-{index}" + options = ["--skip-sce-module"] if configured.startswith("/") else [] + result = subprocess.run([str(relinker), "--macos", *options, "--rpath", configured, + str(source), str(output)], capture_output=True, text=True, timeout=20) + check(result.returncode == 0, f"run paths: {result.stdout}{result.stderr}") + actual = MachO(output.read_bytes()).rpaths + check(actual == expected, f"run paths: {actual}") + if not options: + guest = MachO((Path(directory) / "app0/sce_module/module.prx.guest.prx").read_bytes()).rpaths + expected_guest = [path.replace("@executable_path", "@loader_path/../..") for path in expected] + check(guest == expected_guest, f"guest run paths: {guest}") + for configured in (":$ORIGIN/libs", "$ORIGIN/libs:", "$ORIGIN/first::$ORIGIN/second"): + output = Path(directory) / "empty-runpaths" + result = subprocess.run([str(relinker), "--macos", "--skip-sce-module", "--rpath", configured, + str(source), str(output)], capture_output=True, text=True, timeout=20) + check(result.returncode != 0 and "Empty macOS run path component" in result.stdout + result.stderr, + f"empty run path accepted: {configured}") + check(not output.exists(), "empty run path created an output") + + def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-macos-output-") as directory: check_imports(relink(relinker, directory, "import", import_fixture())) check_tls(relink(relinker, directory, "tls", tls_fixture())) check_options(relinker, directory) + check_runpath_lists(relinker, directory) print("macOS output checks passed") diff --git a/core/relinker/relinker/tests/test_needed_libraries.py b/core/relinker/relinker/tests/test_needed_libraries.py new file mode 100644 index 0000000000..9bf39efb8f --- /dev/null +++ b/core/relinker/relinker/tests/test_needed_libraries.py @@ -0,0 +1,92 @@ +import json +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture +from test_guest_symbol_names import importing_executable + +sys.path.insert(0, str(Path(__file__).resolve().parents[4] / "tools")) +import import_audit + + +def named_module(tag): + image = guest_fixture(PLAIN_SITE) + strings = b"\0Party.prx\0" + image[0x800:0x800 + len(strings)] = strings + struct.pack_into(" 0, (symbol, stream) + opcodes[symbol] = stream[at - 1] + return opcodes + + def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix='anyps5-weak-imports-') as directory: @@ -68,6 +84,19 @@ def main(): run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) assert run.returncode == expected, (name, run.returncode, run.stdout, run.stderr) + weak_puts = macos_fixture() + weak_puts[0x5B00 + 24 + 4] = (STB_WEAK << 4) | 2 + for name, image, expected in (('macos-weak', weak_puts, {'puts': 0x41, 'exit': 0x40}), + ('macos-global', macos_fixture(), {'puts': 0x40, 'exit': 0x40})): + source = work / (name + '.elf') + source.write_bytes(bytes(image)) + output = work / name + result = subprocess.run([str(relinker), '--macos', '--skip-sce-module', str(source), str(output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (name, result.stdout, result.stderr) + opcodes = macho_bind_opcodes(output.read_bytes()) + assert opcodes == expected, (name, opcodes) + print('Weak import tests passed') diff --git a/core/shader/recompiler/BdaAbi.hpp b/core/shader/recompiler/BdaAbi.hpp index 0b055a525e..7a79928c76 100644 --- a/core/shader/recompiler/BdaAbi.hpp +++ b/core/shader/recompiler/BdaAbi.hpp @@ -28,7 +28,7 @@ struct Range { }; enum class FaultState : std::uint32_t { Empty, Writing, Ready }; -enum class FaultReason : std::uint32_t { Unmapped = 1, Permission, Overflow, InvalidTable, InvalidRectangle, LoopLimit, Unaligned, InvalidDescriptor, ImageTable }; +enum class FaultReason : std::uint32_t { Unmapped = 1, Permission, Overflow, InvalidTable, InvalidRectangle, LoopLimit, Unaligned, InvalidDescriptor, ImageTable, UnlinkedCall }; struct Fault { FaultState state; diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 3461dd0d21..810c99d92d 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -35,7 +35,9 @@ function(add_shader_recompiler target runtimeTarget) add_library(${target} STATIC EXCLUDE_FROM_ALL ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Recompiler.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/StencilReplay.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/ShaderDiskCache.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/ShaderCachePack.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/RectListShaders.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/BarycentricGeometryShader.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/RdnaDecoder/src/RdnaOpcode.cpp @@ -167,7 +169,7 @@ function(add_shader_recompiler target runtimeTarget) target_compile_features(${target} PUBLIC cxx_std_20) set(shaderCacheGeneratedDir "${CMAKE_CURRENT_BINARY_DIR}/${target}_generated") - set(shaderCacheVersionHeader "${shaderCacheGeneratedDir}/ShaderCacheVersion.hpp") + set(shaderCacheVersionSource "${shaderCacheGeneratedDir}/ShaderCacheVersion.cpp") file(GLOB_RECURSE shaderCacheRecompilerSources CONFIGURE_DEPENDS "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/*.cpp" "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/*.hpp" @@ -260,6 +262,8 @@ function(add_shader_recompiler target runtimeTarget) FindUserDataCalls UserDataCalleeLength LinkUserDataCalls + FindTableCalls + LinkCalls WriteFileAtomically ) set(shaderCacheManifest) @@ -282,21 +286,20 @@ function(add_shader_recompiler target runtimeTarget) set(shaderCacheListFile "${shaderCacheGeneratedDir}/sources.txt") file(CONFIGURE OUTPUT "${shaderCacheListFile}" CONTENT "${shaderCacheManifest}" @ONLY) add_custom_command( - OUTPUT "${shaderCacheVersionHeader}" + OUTPUT "${shaderCacheVersionSource}" COMMAND ${CMAKE_COMMAND} "-DROOT=${PROJECT_SOURCE_DIR}" "-DLIST_FILE=${shaderCacheListFile}" "-DRECOMPILER_DIR=${CMAKE_CURRENT_FUNCTION_LIST_DIR}" "-DGIT=${GIT_EXECUTABLE}" "-DEXTRA=${shaderCacheExtra}" - "-DOUTPUT=${shaderCacheVersionHeader}" + "-DOUTPUT=${shaderCacheVersionSource}" -P "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/ShaderCacheVersion.cmake" DEPENDS ${shaderCacheRecompilerSources} ${shaderCacheGuardSources} ${shaderCacheSubmoduleStamps} "${shaderCacheListFile}" "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/ShaderCacheVersion.cmake" COMMENT "Hashing the shader recompiler sources for the shader disk cache" VERBATIM ) - target_sources(${target} PRIVATE "${shaderCacheVersionHeader}") - target_include_directories(${target} PRIVATE "${shaderCacheGeneratedDir}") + target_sources(${target} PRIVATE "${shaderCacheVersionSource}") set_target_properties(${target} PROPERTIES POSITION_INDEPENDENT_CODE ON diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 33e899a818..9a05433674 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -52,6 +52,7 @@ public: append(key, request.context.pixel); append(key, request.context.vertex); append(key, request.context.floatMode); + appendFallbacks(key, request); appendMesh(key, request); appendTessellation(key, request); std::uint64_t hash = 0xcbf29ce484222325ull; @@ -84,6 +85,7 @@ private: append(key, request.context.pixel); append(key, request.context.vertex); append(key, request.context.floatMode); + appendFallbacks(key, request); appendMesh(key, request); appendTessellation(key, request); append(key, request.target); @@ -92,6 +94,11 @@ private: append(key, RayTracingMiss()); } + static void appendFallbacks(std::vector& key, const RecompileRequest& request) { + append(key, request.context.linkedCallFallbacks.size()); + for (const auto programCounter : request.context.linkedCallFallbacks) append(key, programCounter); + } + static void appendMesh(std::vector& key, const RecompileRequest& request) { if (request.shader.stage != ShaderStage::Mesh) return; const auto* mesh = request.graphics && request.graphics->mesh ? &*request.graphics->mesh : nullptr; @@ -109,7 +116,7 @@ private: if (request.shader.stage == ShaderStage::Local) return; append(key, tessellation->patchesPerGroup); append(key, tessellation->primitiveGroupSize); - append(key, tessellation->breakAtInstance); + append(key, TessellationInstancePatches(request)); } template @@ -169,6 +176,7 @@ private: append(key, value.posZ); append(key, value.posW); append(key, value.frontFace); + append(key, value.frontFaceAllBits); append(key, value.ancillary); append(key, value.sampleShading); append(key, value.noPerspective); @@ -183,6 +191,8 @@ private: append(key, value.alphaToCoverage); append(key, value.stencilRefExportEnable); append(key, value.ditheredAlphaToCoverage); + append(key, value.quadPixelMask); + append(key, value.quadSampleMask); append(key, value.targetOutputMode); } diff --git a/core/shader/recompiler/ControlFlow/include/ControlFlow/ControlFlowGraph.hpp b/core/shader/recompiler/ControlFlow/include/ControlFlow/ControlFlowGraph.hpp index d93905ba94..3199579688 100644 --- a/core/shader/recompiler/ControlFlow/include/ControlFlow/ControlFlowGraph.hpp +++ b/core/shader/recompiler/ControlFlow/include/ControlFlow/ControlFlowGraph.hpp @@ -107,6 +107,7 @@ struct ControlFlowGraph { std::vector components; std::vector codeTableLoadProgramCounters; std::vector codeTableLoads; + std::vector unlinkedCallProgramCounters; std::uint32_t entryBlock = InvalidControlFlowId; bool hasFetchCall = false; std::uint32_t fetchCallProgramCounter = 0; diff --git a/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp b/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp index 27bbdae67d..49d52a67f0 100644 --- a/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp +++ b/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp @@ -23,6 +23,8 @@ struct SwappcInfo { bool fetchCallAllowed = false; std::uint32_t userDataBaseRegister = 0; std::uint32_t userDataCount = 0; + std::uint32_t waveSize = 0; + std::span linkedCallFallbacks; }; struct SwappcCall { @@ -41,10 +43,33 @@ struct UserDataCall { std::uint32_t userDataRegister = 0; }; +struct TableCall { + std::uint32_t programCounter = 0; + std::uint32_t linkRegister = 0; + std::uint32_t targetRegister = 0; + std::uint32_t userDataRegister = 0; + std::uint32_t descriptorOffset = 0; + std::uint32_t entryOffset = 0; + std::uint32_t entryStride = 0; +}; + +struct TableCallee { + std::uint64_t target = 0; + std::vector code; +}; + [[nodiscard]] std::optional UnresolvableSwappcTarget(const RdnaProgram& program, const SwappcInfo* swappc); [[nodiscard]] std::vector FindUserDataCalls(const RdnaProgram& program, const SwappcInfo& swappc); [[nodiscard]] std::optional UserDataCalleeLength(std::span callee, std::uint32_t linkRegister); [[nodiscard]] std::vector LinkUserDataCalls(const RdnaProgram& program, std::span calls, std::span> callees); +[[nodiscard]] std::vector FindTableCalls(const RdnaProgram& program, const SwappcInfo& swappc); +struct LinkedCalls { + std::vector code; + std::vector fallbacks; +}; + +[[nodiscard]] LinkedCalls LinkCalls(const RdnaProgram& program, std::span userDataCalls, std::span> userDataCallees, + std::span tableCalls, std::span> tableCallees); class GraphBuilder { public: diff --git a/core/shader/recompiler/ControlFlow/include/ControlFlow/RequestSerializer.hpp b/core/shader/recompiler/ControlFlow/include/ControlFlow/RequestSerializer.hpp index 0f59c68558..bf76e576bd 100644 --- a/core/shader/recompiler/ControlFlow/include/ControlFlow/RequestSerializer.hpp +++ b/core/shader/recompiler/ControlFlow/include/ControlFlow/RequestSerializer.hpp @@ -33,6 +33,7 @@ namespace ShaderRecompiler { std::optional vertex; std::vector> memoryBytesStorage; std::vector memory; + std::vector linkedCallFallbacks; std::vector supportedCapabilities; std::vector supportedExtensionStorage; std::vector supportedExtensions; diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index 7a487915c8..a67f30d1ec 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -2,6 +2,7 @@ #include "ControlFlow/ControlFlowHelpers.hpp" #include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include +#include #include #include #include @@ -257,7 +258,7 @@ bool resolveJumpTable(const RdnaProgram& program, std::uint32_t index, BoundedJu return false; } -bool writesScalar(const RdnaInstruction& instruction, std::uint32_t index) { +bool writesScalar(const RdnaInstruction& instruction, std::uint32_t index, std::uint32_t waveSize = 0u) { std::uint32_t count = instruction.dataDwordCount; switch (instruction.family) { case RdnaInstructionFamily::SOP1: @@ -273,7 +274,7 @@ bool writesScalar(const RdnaInstruction& instruction, std::uint32_t index) { case RdnaInstructionFamily::VOP3: case RdnaInstructionFamily::VOP3P: case RdnaInstructionFamily::VOPC: - count = 2u; + count = instruction.op == RdnaOpcode::VReadlaneB32 || instruction.op == RdnaOpcode::VReadfirstlaneB32 || waveSize == 32u ? 1u : 2u; break; case RdnaInstructionFamily::SOPC: case RdnaInstructionFamily::SOPP: @@ -293,11 +294,11 @@ bool writesScalar(const RdnaInstruction& instruction, std::uint32_t index) { return covers(instruction.destination) || covers(instruction.destination2); } -bool findLastWriter(const RdnaProgram& program, std::uint32_t end, std::initializer_list indices, std::uint32_t& writer) { +bool findLastWriter(const RdnaProgram& program, std::uint32_t end, std::initializer_list indices, std::uint32_t& writer, std::uint32_t waveSize = 0u) { for (auto position = end; position-- > 0u;) { const auto& instruction = program.instructions[position]; if (IsDirectBranchOpcode(instruction.op) || instruction.op == RdnaOpcode::SSetpcB64 || instruction.op == RdnaOpcode::SEndpgm) return false; - if (std::ranges::any_of(indices, [&](std::uint32_t index) { return writesScalar(instruction, index); })) { + if (std::ranges::any_of(indices, [&](std::uint32_t index) { return writesScalar(instruction, index, waveSize); })) { writer = position; return true; } @@ -475,7 +476,298 @@ bool isFetchCall(const RdnaProgram& program, std::uint32_t index, const SwappcIn return swappc != nullptr && swappc->fetchCallAllowed && positional && (userDataPair || outsideProgram); } -std::vector analyzeSwappcCalls(const RdnaProgram& program, const SwappcInfo* swappc) { +bool overwritesScalar(const RdnaInstruction& instruction, std::uint32_t reg, std::uint32_t waveSize) { + const auto first = scalarIndex(instruction.destination); + switch (instruction.family) { + case RdnaInstructionFamily::SMEM: + return isScalarLoad(instruction.op) && first != NoScalarRegister && reg >= first && reg - first < instruction.dataDwordCount; + case RdnaInstructionFamily::SOP1: + switch (instruction.op) { + case RdnaOpcode::SMovreldB32: + case RdnaOpcode::SMovreldB64: + case RdnaOpcode::SMovrelsd2B32: + case RdnaOpcode::SCmovB32: + case RdnaOpcode::SCmovB64: + case RdnaOpcode::SBitset0B32: + case RdnaOpcode::SBitset0B64: + case RdnaOpcode::SBitset1B32: + case RdnaOpcode::SBitset1B64: + case RdnaOpcode::SSwappcB64: + case RdnaOpcode::SSetpcB64: return false; + case RdnaOpcode::SMovB64: + case RdnaOpcode::SGetpcB64: return first != NoScalarRegister && reg >= first && reg - first < 2u; + default: return first != NoScalarRegister && reg == first; + } + case RdnaInstructionFamily::SOP2: return first != NoScalarRegister && reg == first; + case RdnaInstructionFamily::SOPK: return instruction.op == RdnaOpcode::SMovkI32 && reg == first; + case RdnaInstructionFamily::VOP1: + case RdnaInstructionFamily::VOP2: + case RdnaInstructionFamily::VOP3: + case RdnaInstructionFamily::VOP3P: + case RdnaInstructionFamily::VOPC: { + const std::uint32_t count = instruction.op != RdnaOpcode::VReadlaneB32 && instruction.op != RdnaOpcode::VReadfirstlaneB32 && waveSize == 64u ? 2u : 1u; + return std::ranges::any_of(std::array{&instruction.destination, &instruction.destination2}, [&](const RdnaOperand* operand) { + const auto slot = scalarIndex(*operand); + return slot != NoScalarRegister && reg >= slot && reg - slot < count; + }); + } + default: return false; + } +} + +bool readsVectorSources64(RdnaOpcode op) { + switch (op) { + case RdnaOpcode::VMadU64U32: + case RdnaOpcode::VMadI64I32: + case RdnaOpcode::VFmaF64: + case RdnaOpcode::VAddF64: + case RdnaOpcode::VMulF64: + case RdnaOpcode::VMinF64: + case RdnaOpcode::VMaxF64: + case RdnaOpcode::VLdexpF64: + case RdnaOpcode::VCvtI32F64: + case RdnaOpcode::VCvtF64I32: + case RdnaOpcode::VCvtF32F64: + case RdnaOpcode::VCvtF64F32: + case RdnaOpcode::VCvtU32F64: + case RdnaOpcode::VCvtF64U32: + case RdnaOpcode::VTruncF64: + case RdnaOpcode::VCeilF64: + case RdnaOpcode::VRndneF64: + case RdnaOpcode::VFloorF64: + case RdnaOpcode::VFrexpExpI32F64: + case RdnaOpcode::VFrexpMantF64: + case RdnaOpcode::VFractF64: + case RdnaOpcode::VRcpF64: + case RdnaOpcode::VRsqF64: + case RdnaOpcode::VSqrtF64: + case RdnaOpcode::VTrigPreopF64: + case RdnaOpcode::VDivScaleF64: + case RdnaOpcode::VDivFmasF64: + case RdnaOpcode::VDivFixupF64: + case RdnaOpcode::VLshlrevB64: + case RdnaOpcode::VLshrrevB64: + case RdnaOpcode::VAshrrevI64: + case RdnaOpcode::VCmpEqI64: + case RdnaOpcode::VCmpLtU64: + case RdnaOpcode::VCmpEqU64: + case RdnaOpcode::VCmpGtU64: + case RdnaOpcode::VCmpNeU64: + case RdnaOpcode::VCmpxNeI64: + case RdnaOpcode::VCmpxNeU64: + case RdnaOpcode::VCmpFI64: + case RdnaOpcode::VCmpLtI64: + case RdnaOpcode::VCmpLeI64: + case RdnaOpcode::VCmpGtI64: + case RdnaOpcode::VCmpNeI64: + case RdnaOpcode::VCmpGeI64: + case RdnaOpcode::VCmpTI64: + case RdnaOpcode::VCmpxFI64: + case RdnaOpcode::VCmpxLtI64: + case RdnaOpcode::VCmpxEqI64: + case RdnaOpcode::VCmpxLeI64: + case RdnaOpcode::VCmpxGtI64: + case RdnaOpcode::VCmpxGeI64: + case RdnaOpcode::VCmpxTI64: + case RdnaOpcode::VCmpFU64: + case RdnaOpcode::VCmpLeU64: + case RdnaOpcode::VCmpGeU64: + case RdnaOpcode::VCmpTU64: + case RdnaOpcode::VCmpxFU64: + case RdnaOpcode::VCmpxLtU64: + case RdnaOpcode::VCmpxEqU64: + case RdnaOpcode::VCmpxLeU64: + case RdnaOpcode::VCmpxGtU64: + case RdnaOpcode::VCmpxGeU64: + case RdnaOpcode::VCmpxTU64: + case RdnaOpcode::VCmpFF64: + case RdnaOpcode::VCmpLtF64: + case RdnaOpcode::VCmpEqF64: + case RdnaOpcode::VCmpLeF64: + case RdnaOpcode::VCmpGtF64: + case RdnaOpcode::VCmpLgF64: + case RdnaOpcode::VCmpGeF64: + case RdnaOpcode::VCmpOF64: + case RdnaOpcode::VCmpUF64: + case RdnaOpcode::VCmpNgeF64: + case RdnaOpcode::VCmpNlgF64: + case RdnaOpcode::VCmpNgtF64: + case RdnaOpcode::VCmpNleF64: + case RdnaOpcode::VCmpNeqF64: + case RdnaOpcode::VCmpNltF64: + case RdnaOpcode::VCmpTruF64: + case RdnaOpcode::VCmpClassF64: + case RdnaOpcode::VCmpxFF64: + case RdnaOpcode::VCmpxLtF64: + case RdnaOpcode::VCmpxEqF64: + case RdnaOpcode::VCmpxLeF64: + case RdnaOpcode::VCmpxGtF64: + case RdnaOpcode::VCmpxLgF64: + case RdnaOpcode::VCmpxGeF64: + case RdnaOpcode::VCmpxOF64: + case RdnaOpcode::VCmpxUF64: + case RdnaOpcode::VCmpxNgeF64: + case RdnaOpcode::VCmpxNlgF64: + case RdnaOpcode::VCmpxNgtF64: + case RdnaOpcode::VCmpxNleF64: + case RdnaOpcode::VCmpxNeqF64: + case RdnaOpcode::VCmpxNltF64: + case RdnaOpcode::VCmpxTruF64: + case RdnaOpcode::VCmpxClassF64: + return true; + default: return false; + } +} + +bool readsScalarDword(const RdnaInstruction& instruction, std::uint32_t reg, std::uint32_t waveSize) { + if (instruction.op == RdnaOpcode::SMovrelsB32 || instruction.op == RdnaOpcode::SMovrelsB64) return true; + const auto overlaps = [&](const RdnaOperand& operand, std::uint32_t count) { + const auto slot = scalarIndex(operand); + return slot != NoScalarRegister && slot <= reg && reg - slot < count; + }; + std::uint32_t source0Count = 1u, source1Count = 1u, source2Count = 1u, source3Count = 1u; + switch (instruction.family) { + case RdnaInstructionFamily::SMEM: + switch (instruction.op) { + case RdnaOpcode::SBufferLoadDword: + case RdnaOpcode::SBufferLoadDwordx2: + case RdnaOpcode::SBufferLoadDwordx4: + case RdnaOpcode::SBufferLoadDwordx8: + case RdnaOpcode::SBufferLoadDwordx16: source0Count = 4u; break; + default: source0Count = 2u; break; + } + break; + case RdnaInstructionFamily::MUBUF: + case RdnaInstructionFamily::MTBUF: source1Count = 4u; break; + case RdnaInstructionFamily::MIMG: + source1Count = instruction.imageR128 ? 4u : 8u; + source2Count = 4u; + break; + case RdnaInstructionFamily::FLAT: + source0Count = 2u; + source1Count = 2u; + break; + case RdnaInstructionFamily::SOPK: + if (instruction.op != RdnaOpcode::SMovkI32 && instruction.op != RdnaOpcode::SCallB64 && overlaps(instruction.destination, 1u)) return true; + break; + case RdnaInstructionFamily::SOP1: + case RdnaInstructionFamily::SOP2: + case RdnaInstructionFamily::SOPC: + switch (instruction.op) { + case RdnaOpcode::SBcnt0I32B64: + case RdnaOpcode::SBcnt1I32B64: + case RdnaOpcode::SFf0I32B64: + case RdnaOpcode::SFf1I32B64: + case RdnaOpcode::SFlbitI32B64: + case RdnaOpcode::SFlbitI32I64: + case RdnaOpcode::SCmpEqU64: + case RdnaOpcode::SCmpLgU64: + case RdnaOpcode::SBitcmp0B64: + case RdnaOpcode::SBitcmp1B64: source0Count = source1Count = 2u; break; + default: source0Count = source1Count = instruction.family == RdnaInstructionFamily::SOPC ? 1u : std::max(instruction.dataDwordCount, 1u); break; + } + break; + case RdnaInstructionFamily::VOP1: + case RdnaInstructionFamily::VOP2: + case RdnaInstructionFamily::VOP3: + case RdnaInstructionFamily::VOP3P: + case RdnaInstructionFamily::VOPC: + source0Count = source1Count = source2Count = source3Count = readsVectorSources64(instruction.op) ? 2u : 1u; + switch (instruction.op) { + case RdnaOpcode::VCndmaskB32: + case RdnaOpcode::VAddcU32: + case RdnaOpcode::VSubCoCiU32: + case RdnaOpcode::VSubrevCoCiU32: + case RdnaOpcode::VDivFmasF32: + case RdnaOpcode::VDivFmasF64: source2Count = std::max(source2Count, waveSize == 32u ? 1u : 2u); break; + default: break; + } + break; + default: break; + } + switch (instruction.op) { + case RdnaOpcode::SBitset0B32: + case RdnaOpcode::SBitset0B64: + case RdnaOpcode::SBitset1B32: + case RdnaOpcode::SBitset1B64: + case RdnaOpcode::SCmovB32: + case RdnaOpcode::SCmovB64: + if (overlaps(instruction.destination, 2u)) return true; + break; + default: break; + } + return overlaps(instruction.source0, source0Count) || overlaps(instruction.source1, source1Count) || overlaps(instruction.source2, source2Count) || overlaps(instruction.source3, source3Count); +} + +template +void requireReturnAddressesStayInCalls(const RdnaProgram& program, const std::vector& calls, const std::vector& unlinked, std::uint32_t waveSize, const TError& escapeError) { + std::set links; + for (const auto& call : calls) { + if (!call.fetch) links.insert(call.linkRegister); + } + const auto size = static_cast(program.instructions.size()); + const auto successors = [&](std::uint32_t index) { + std::vector next; + const auto& instruction = program.instructions[index]; + if (instruction.op == RdnaOpcode::SEndpgm || instruction.op == RdnaOpcode::SCodeEnd) return next; + if (IsDirectBranchOpcode(instruction.op)) { + next.push_back(instructionIndexOfProgramCounter(program, instruction.branchTarget)); + if (IsConditionalBranchOpcode(instruction.op)) next.push_back(index + 1u); + } else if (instruction.op == RdnaOpcode::SSwappcB64 || instruction.op == RdnaOpcode::SCallB64) { + const SwappcCall* call = findCallAt(calls, index); + next.push_back(call == nullptr || call->fetch ? index + 1u : call->targetIndex); + } else if (instruction.op == RdnaOpcode::SSetpcB64) { + std::uint32_t target = 0; + if (const SwappcCall* call = findReturnAt(calls, index); call != nullptr) { + next.push_back(call->callIndex + 1u); + } else if (resolveSetpcTarget(program, index, target)) { + next.push_back(instructionIndexOfProgramCounter(program, target)); + } + } else { + next.push_back(index + 1u); + } + std::erase_if(next, [&](std::uint32_t successor) { return successor >= size; }); + return next; + }; + for (const auto link : links) { + const auto writesLink = [&](std::uint32_t index) { + const SwappcCall* call = findCallAt(calls, index); + return (call != nullptr && call->linkRegister == link) || (std::ranges::find(unlinked, index) != unlinked.end() && scalarIndex(program.instructions[index].destination) == link); + }; + std::vector holds(size, 0u); + std::vector work; + for (std::uint32_t index = 0; index < size; ++index) { + if (writesLink(index)) work.push_back(index); + } + const auto after = [&](std::uint32_t index) -> std::uint8_t { + if (writesLink(index)) return 3u; + std::uint8_t value = holds[index]; + if (overwritesScalar(program.instructions[index], link, waveSize)) value &= 2u; + if (overwritesScalar(program.instructions[index], link + 1u, waveSize)) value &= 1u; + return value; + }; + while (!work.empty()) { + const auto index = work.back(); + work.pop_back(); + const auto value = after(index); + for (const auto successor : successors(index)) { + if ((holds[successor] | value) == holds[successor]) continue; + holds[successor] |= value; + work.push_back(successor); + } + } + for (std::uint32_t index = 0; index < size; ++index) { + const auto& instruction = program.instructions[index]; + if (holds[index] == 0u || writesLink(index) || instruction.family == RdnaInstructionFamily::SOPC || findReturnAt(calls, index) != nullptr) continue; + const bool inside = std::ranges::any_of(calls, [&](const SwappcCall& call) { return !call.fetch && call.linkRegister == link && index >= call.targetIndex && index <= call.returnIndex; }); + if (inside || !(((holds[index] & 1u) != 0u && readsScalarDword(instruction, link, waveSize)) || ((holds[index] & 2u) != 0u && readsScalarDword(instruction, link + 1u, waveSize)))) continue; + const auto owner = std::ranges::find_if(calls, [&](const SwappcCall& call) { return !call.fetch && call.linkRegister == link; }); + throw escapeError(*owner, instruction); + } + } +} + +std::vector analyzeSwappcCalls(const RdnaProgram& program, const SwappcInfo* swappc, std::vector& unlinked) { std::vector calls; for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { const auto& instruction = program.instructions[index]; @@ -502,6 +794,10 @@ std::vector analyzeSwappcCalls(const RdnaProgram& program, const Swa continue; } if (!staticTarget || call.targetIndex == InvalidControlFlowId) { + if (swappc != nullptr && std::ranges::find(swappc->linkedCallFallbacks, instruction.programCounter) != swappc->linkedCallFallbacks.end()) { + unlinked.push_back(index); + continue; + } if (std::ranges::any_of(calls, [&](const SwappcCall& other) { return other.linkRegister == call.linkRegister; })) { throw std::invalid_argument("unclosable scalar call/return pairing at program counter " + toHexString(instruction.programCounter) + ": link register s[" + std::to_string(call.linkRegister) + "] is shared with another call"); @@ -574,7 +870,8 @@ std::vector analyzeSwappcCalls(const RdnaProgram& program, const Swa continue; } const bool outsideRegion = regionEnd != InvalidControlFlowId && (index < call.targetIndex || index > regionEnd); - if (touchesLinkRegister(instruction, call.linkRegister) && !(outsideRegion && onlyWritesLinkRegister(instruction, call.linkRegister))) { + if (outsideRegion) continue; + if (touchesLinkRegister(instruction, call.linkRegister)) { throw escapeError(call, instruction); } } @@ -609,14 +906,12 @@ std::vector analyzeSwappcCalls(const RdnaProgram& program, const Swa if (std::ranges::any_of(calls, [&](const SwappcCall& other) { return other.callIndex == index && other.linkRegister == call.linkRegister; })) continue; const auto& instruction = program.instructions[index]; const auto region = std::ranges::find_if(calls, [&](const SwappcCall& other) { return other.linkRegister == call.linkRegister && index >= other.targetIndex && index <= other.returnIndex; }); - if (region != calls.end()) { - if (index != region->returnIndex && touchesLinkRegister(instruction, call.linkRegister)) throw escapeError(*region, instruction); - } else if (touchesLinkRegister(instruction, call.linkRegister) && !onlyWritesLinkRegister(instruction, call.linkRegister)) { - throw escapeError(call, instruction); - } + if (region != calls.end() && index != region->returnIndex && touchesLinkRegister(instruction, call.linkRegister)) throw escapeError(*region, instruction); } } + requireReturnAddressesStayInCalls(program, calls, unlinked, swappc != nullptr ? swappc->waveSize : 0u, escapeError); + for (std::size_t first = 0; first < calls.size(); ++first) { if (!calls[first].fetch && calls[first].callIndex >= calls[first].targetIndex && calls[first].callIndex <= calls[first].returnIndex) { throw std::invalid_argument("recursive scalar call at program counter " + toHexString(program.instructions[calls[first].callIndex].programCounter) + @@ -664,7 +959,7 @@ std::vector analyzeSwappcCalls(const RdnaProgram& program, const Swa } if (instruction.op == RdnaOpcode::SSwappcB64 || instruction.op == RdnaOpcode::SCallB64) { const SwappcCall* call = findCallAt(calls, index); - transfers.push_back(Transfer{index, call->fetch ? index + 1u : call->targetIndex}); + transfers.push_back(Transfer{index, call == nullptr || call->fetch ? index + 1u : call->targetIndex}); continue; } if (instruction.op != RdnaOpcode::SEndpgm && instruction.op != RdnaOpcode::SCodeEnd) transfers.push_back(Transfer{index, index + 1u}); @@ -958,23 +1253,75 @@ std::optional UnresolvableSwappcTarget(const RdnaProgram& program return std::nullopt; } -std::vector FindUserDataCalls(const RdnaProgram& program, const SwappcInfo& swappc) { +namespace { + +std::set branchLabels(const RdnaProgram& program) { std::set labels; for (const auto& instruction : program.instructions) { if (IsDirectBranchOpcode(instruction.op) || instruction.op == RdnaOpcode::SCallB64) labels.insert(instruction.branchTarget); } - const auto userData = [&](std::uint32_t reg) { return reg >= swappc.userDataBaseRegister && reg - swappc.userDataBaseRegister < swappc.userDataCount; }; - const auto unchangedUntil = [&](std::uint32_t reg, std::uint32_t reader) { - for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { - if (!writesScalar(program.instructions[index], reg)) continue; - if (index <= reader) return false; - for (std::uint32_t later = index; later < program.instructions.size(); ++later) { - const auto& instruction = program.instructions[later]; - if ((IsDirectBranchOpcode(instruction.op) || instruction.op == RdnaOpcode::SCallB64) && instruction.branchTarget <= program.instructions[reader].programCounter) return false; - } + return labels; +} + +bool unchangedUntil(const RdnaProgram& program, std::uint32_t reg, std::uint32_t reader, std::uint32_t waveSize) { + for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { + if (!writesScalar(program.instructions[index], reg, waveSize)) continue; + if (index <= reader) return false; + for (std::uint32_t later = index; later < program.instructions.size(); ++later) { + const auto& instruction = program.instructions[later]; + if ((IsDirectBranchOpcode(instruction.op) || instruction.op == RdnaOpcode::SCallB64) && instruction.branchTarget <= program.instructions[reader].programCounter) return false; } - return true; - }; + } + return true; +} + +bool isScalarBufferLoad(RdnaOpcode op) { + switch (op) { + case RdnaOpcode::SBufferLoadDword: + case RdnaOpcode::SBufferLoadDwordx2: + case RdnaOpcode::SBufferLoadDwordx4: + case RdnaOpcode::SBufferLoadDwordx8: + case RdnaOpcode::SBufferLoadDwordx16: return true; + default: return false; + } +} + +bool loadedDword(const RdnaProgram& program, std::uint32_t reg, std::uint32_t before, std::uint32_t waveSize, std::uint32_t& load, std::uint32_t& dword) { + for (std::uint32_t copies = 0; copies < 4u; ++copies) { + std::uint32_t writer = 0; + if (!findLastWriter(program, before, {reg}, writer, waveSize)) return false; + const auto& instruction = program.instructions[writer]; + const auto destination = scalarIndex(instruction.destination); + const auto source = scalarIndex(instruction.source0); + if (instruction.op == RdnaOpcode::SMovB32 && destination == reg && source != NoScalarRegister) { + reg = source; + } else if (instruction.op == RdnaOpcode::SMovB64 && destination != NoScalarRegister && reg - destination < 2u && source != NoScalarRegister) { + reg = source + (reg - destination); + } else if (isScalarBufferLoad(instruction.op) && destination != NoScalarRegister && reg >= destination && reg - destination < instruction.dataDwordCount) { + load = writer; + dword = reg - destination; + return true; + } else { + return false; + } + before = writer; + } + return false; +} + +std::uint32_t branchWord(std::uint32_t opcode, std::size_t from, std::size_t to, const char* what) { + const auto offset = static_cast(to) - static_cast(from) - 1; + if (offset < -0x8000 || offset > 0x7fff) throw std::invalid_argument(std::string(what) + " lands out of the 16-bit branch range"); + return opcode | (static_cast(offset) & 0xffffu); +} + +} + + +std::vector FindUserDataCalls(const RdnaProgram& program, const SwappcInfo& swappc) { + const auto labels = branchLabels(program); + const auto userData = [&](std::uint32_t reg) { return reg >= swappc.userDataBaseRegister && reg - swappc.userDataBaseRegister < swappc.userDataCount; }; + const auto unchanged = [&](std::uint32_t reg, std::uint32_t reader) { return unchangedUntil(program, reg, reader, swappc.waveSize); }; std::vector calls; for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { const auto& instruction = program.instructions[index]; @@ -985,7 +1332,7 @@ std::vector FindUserDataCalls(const RdnaProgram& program, const Sw const auto source = scalarIndex(instruction.source0); if (link == NoScalarRegister || source == NoScalarRegister || source % 2u != 0u) continue; std::uint32_t pair = NoScalarRegister; - if (source != link && userData(source) && userData(source + 1u) && unchangedUntil(source, index) && unchangedUntil(source + 1u, index)) { + if (source != link && userData(source) && userData(source + 1u) && unchanged(source, index) && unchanged(source + 1u, index)) { pair = source; } else { std::uint32_t low = 0, high = 0; @@ -1003,7 +1350,7 @@ std::vector FindUserDataCalls(const RdnaProgram& program, const Sw from = scalarIndex(lowWriter.source0); if (from != NoScalarRegister && !isScalar(highWriter.source0, from + 1u)) from = NoScalarRegister; } - if (!sameBlock || from == NoScalarRegister || from % 2u != 0u || !userData(from) || !userData(from + 1u) || !unchangedUntil(from, low) || !unchangedUntil(from + 1u, high)) continue; + if (!sameBlock || from == NoScalarRegister || from % 2u != 0u || !userData(from) || !userData(from + 1u) || !unchanged(from, low) || !unchanged(from + 1u, high)) continue; pair = from; } calls.push_back({instruction.programCounter, link, pair}); @@ -1077,32 +1424,132 @@ std::optional UserDataCalleeLength(std::span } std::vector LinkUserDataCalls(const RdnaProgram& program, std::span calls, std::span> callees) { - if (calls.size() != callees.size()) throw std::logic_error("user-data calls and callees differ in number"); - for (const auto& instruction : program.instructions) { - if (instruction.op == RdnaOpcode::SGetpcB64) { - throw std::invalid_argument("a program that reads its program counter (s_getpc_b64 at program counter " + toHexString(instruction.programCounter) + ") cannot have user-data callees linked after it"); + return LinkCalls(program, calls, callees, {}, {}).code; +} + +std::vector FindTableCalls(const RdnaProgram& program, const SwappcInfo& swappc) { + const auto labels = branchLabels(program); + const auto userData = [&](std::uint32_t reg) { return reg >= swappc.userDataBaseRegister && reg - swappc.userDataBaseRegister < swappc.userDataCount; }; + const auto sameBlock = [&](std::uint32_t first, std::uint32_t last) { + return std::none_of(labels.begin(), labels.end(), [&](std::uint32_t label) { + return label > program.instructions[first].programCounter && label <= program.instructions[last].programCounter; + }); + }; + std::vector calls; + for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { + const auto& instruction = program.instructions[index]; + if (instruction.op != RdnaOpcode::SSwappcB64) continue; + std::uint32_t ignored = 0; + if (resolveSetpcTarget(program, index, ignored)) continue; + const auto link = scalarIndex(instruction.destination); + const auto target = scalarIndex(instruction.source0); + if (link == NoScalarRegister || link % 2u != 0u || link > 104u || target == NoScalarRegister || target % 2u != 0u || target > 104u) continue; + std::uint32_t lowLoad = 0, lowDword = 0, highLoad = 0, highDword = 0; + if (!loadedDword(program, target, index, swappc.waveSize, lowLoad, lowDword) || !loadedDword(program, target + 1u, index, swappc.waveSize, highLoad, highDword)) continue; + if (lowLoad != highLoad || highDword != lowDword + 1u || !sameBlock(lowLoad, index)) continue; + const auto& load = program.instructions[lowLoad]; + const auto descriptor = scalarIndex(load.source0); + if (load.source0.kind != RdnaOperandKind::ScalarRegister || descriptor % 4u != 0u || load.memoryOffset >= 0x100000u || (load.memoryOffset & 3u) != 0u) continue; + std::uint32_t descriptorLoad = 0; + if (!findLastWriter(program, lowLoad, {descriptor, descriptor + 1u, descriptor + 2u, descriptor + 3u}, descriptorLoad, swappc.waveSize) || !sameBlock(descriptorLoad, index)) continue; + const auto& table = program.instructions[descriptorLoad]; + const auto pointer = scalarIndex(table.source0); + if (table.op != RdnaOpcode::SLoadDwordx4 || !isScalar(table.destination, descriptor) || table.source0.kind != RdnaOperandKind::ScalarRegister || table.source1.kind != RdnaOperandKind::Null || + table.memoryOffset >= 0x100000u || (table.memoryOffset & 3u) != 0u || !userData(pointer) || !userData(pointer + 1u) || + !unchangedUntil(program, pointer, descriptorLoad, swappc.waveSize) || !unchangedUntil(program, pointer + 1u, descriptorLoad, swappc.waveSize)) continue; + std::uint32_t stride = 0; + if (load.source1.kind != RdnaOperandKind::Null) { + const auto offset = scalarIndex(load.source1); + std::uint32_t shift = 0, shiftIndex = 0; + if (offset == NoScalarRegister || !findLastWriter(program, lowLoad, {offset}, shiftIndex, swappc.waveSize) || !sameBlock(shiftIndex, index)) continue; + const auto& shifted = program.instructions[shiftIndex]; + if (shifted.op != RdnaOpcode::SLshlB32 || !isScalar(shifted.destination, offset) || !isImmediate(shifted.source1, shift) || shift < 2u || shift > 24u) continue; + stride = 1u << shift; + } + calls.push_back({instruction.programCounter, link, target, pointer, table.memoryOffset, load.memoryOffset + lowDword * 4u, stride}); + } + return calls; +} + +LinkedCalls LinkCalls(const RdnaProgram& program, std::span userDataCalls, std::span> userDataCallees, + std::span tableCalls, std::span> tableCallees) { + if (userDataCalls.size() != userDataCallees.size()) throw std::logic_error("user-data calls and callees differ in number"); + if (tableCalls.size() != tableCallees.size()) throw std::logic_error("table calls and callees differ in number"); + for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { + BoundedJumpTable table; + if (program.instructions[index].op == RdnaOpcode::SSetpcB64 && resolveBoundedJumpTable(program, index, table)) { + throw std::invalid_argument("a program that jumps through a table in its code (s_setpc_b64 at program counter " + toHexString(program.instructions[index].programCounter) + ") cannot have callees linked over the words after its last instruction"); } } const auto end = instructionEndProgramCounter(program.instructions.back()) / 4u; std::vector linked(program.code.begin(), program.code.begin() + end); - for (std::size_t call = 0; call < calls.size(); ++call) { - const auto index = instructionIndexOfProgramCounter(program, calls[call].programCounter); + std::vector fallbacks; + const auto callWord = [&](std::uint32_t programCounter, const char* kind) { + const auto index = instructionIndexOfProgramCounter(program, programCounter); if (index == InvalidControlFlowId || program.instructions[index].op != RdnaOpcode::SSwappcB64 || program.instructions[index].wordCount != 1u) { - throw std::logic_error("user-data call does not name an s_swappc_b64"); + throw std::logic_error(std::string(kind) + " call does not name an s_swappc_b64"); } - const auto word = calls[call].programCounter / 4u; + return programCounter / 4u; + }; + for (std::size_t call = 0; call < userDataCalls.size(); ++call) { + const auto word = callWord(userDataCalls[call].programCounter, "user-data"); const auto offset = static_cast(linked.size()) - static_cast(word) - 1; - if (offset > 0x7fff) throw std::invalid_argument("user-data callee of the call at program counter " + toHexString(calls[call].programCounter) + " lands out of s_call_b64 range"); - linked[word] = 0xbb000000u | (calls[call].linkRegister << 16u) | static_cast(offset); - linked.insert(linked.end(), callees[call].begin(), callees[call].end()); + if (offset > 0x7fff) throw std::invalid_argument("user-data callee of the call at program counter " + toHexString(userDataCalls[call].programCounter) + " lands out of s_call_b64 range"); + linked[word] = 0xbb000000u | (userDataCalls[call].linkRegister << 16u) | static_cast(offset); + linked.insert(linked.end(), userDataCallees[call].begin(), userDataCallees[call].end()); + } + for (std::size_t call = 0; call < tableCalls.size(); ++call) { + const auto& table = tableCalls[call]; + const auto& callees = tableCallees[call]; + const auto word = callWord(table.programCounter, "table"); + const auto at = "the table callees of the call at program counter " + toHexString(table.programCounter); + const auto original = linked[word]; + const auto count = callees.size(); + const auto dispatch = linked.size(); + const auto zero = dispatch + 1u; + const auto one = zero + count * 7u + 2u; + const auto calls = one + count * 8u + 2u; + const auto fault = calls + count * 2u; + const auto branch = [&](std::uint32_t opcode, std::size_t target) { linked.push_back(branchWord(opcode, linked.size(), target, at.c_str())); }; + linked[word] = branchWord(0xbf820000u, word, dispatch, at.c_str()); + branch(0xbf850000u, one); + for (const bool sccOne : {false, true}) { + for (std::size_t callee = 0; callee < count; ++callee) { + const auto next = linked.size() + (sccOne ? 8u : 7u); + linked.insert(linked.end(), {0xbf07ff00u | table.targetRegister, static_cast(callees[callee].target)}); + branch(0xbf850000u, next); + linked.insert(linked.end(), {0xbf07ff00u | (table.targetRegister + 1u), static_cast(callees[callee].target >> 32u)}); + branch(0xbf850000u, next); + if (sccOne) linked.push_back(0xbf068080u); + branch(0xbf820000u, calls + callee * 2u); + } + linked.push_back(sccOne ? 0xbf068080u : 0xbf078080u); + branch(0xbf820000u, fault); + } + for (std::size_t callee = 0; callee < count; ++callee) { + linked.push_back(0u); + branch(0xbf820000u, word + 1u); + } + fallbacks.push_back(static_cast(linked.size() * 4u)); + linked.push_back(original); + branch(0xbf820000u, word + 1u); + for (std::size_t callee = 0; callee < count; ++callee) { + const auto site = calls + callee * 2u; + const auto offset = static_cast(linked.size()) - static_cast(site) - 1; + if (offset > 0x7fff) throw std::invalid_argument(at + " land out of s_call_b64 range"); + linked[site] = 0xbb000000u | (table.linkRegister << 16u) | static_cast(offset); + linked.insert(linked.end(), callees[callee].code.begin(), callees[callee].code.end()); + } } linked.push_back(0xbf9f0000u); - return linked; + return {std::move(linked), std::move(fallbacks)}; } ControlFlowGraph GraphBuilder::Build(const RdnaProgram& program, const SwappcInfo* swappc) const { - const std::vector calls = analyzeSwappcCalls(program, swappc); + std::vector unlinked; + const std::vector calls = analyzeSwappcCalls(program, swappc, unlinked); ControlFlowGraph graph; + for (const auto index : unlinked) graph.unlinkedCallProgramCounters.push_back(program.instructions[index].programCounter); graph.blocks = splitIntoBlocks(program, calls); linkBlocks(graph.blocks, program, calls); for (const auto& call : calls) { diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 93b1024051..5699b039f3 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -339,6 +339,9 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteU8(static_cast(info.rasterPrimitive)); writer.WriteBool(info.layerExported); writer.WriteBool(info.ditheredAlphaToCoverage); + writer.WriteBool(info.frontFaceAllBits); + writer.WriteU8(info.quadPixelMask); + for (const std::uint16_t mask : info.quadSampleMask) writer.WriteU32(mask); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -391,6 +394,15 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { if (version >= 19u) info.rasterPrimitive = readRasterPrimitive(reader); if (version >= 23u) info.layerExported = reader.ReadBool(); if (version >= 25u) info.ditheredAlphaToCoverage = reader.ReadBool(); + if (version >= 27u) info.frontFaceAllBits = reader.ReadBool(); + if (version >= 28u) info.quadPixelMask = reader.ReadU8(); + if (version >= 29u) { + for (std::uint16_t& mask : info.quadSampleMask) { + const auto value = reader.ReadU32(); + if (value > 0xffffu) throw std::runtime_error("invalid recompile request quad sample mask"); + mask = static_cast(value); + } + } 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) | @@ -762,7 +774,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(25u); + writer.WriteU32(29u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -786,6 +798,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const writer.WriteU32(request.target.registrationWordLimit); writer.WriteBool(request.target.vertexSubgroups); writer.WriteBool(request.target.float16DenormPreserve); + writer.WriteU32Span(request.context.linkedCallFallbacks); return base64Encode(buffer); } @@ -794,7 +807,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 > 25u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 29u) 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); @@ -820,14 +833,17 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const if (version >= 18u) result.request.target.vertexSubgroups = reader.ReadBool(); if (version >= 21u) { if (version >= 22u) reader.Require(1u); - if (reader.Remaining() == 1u) { + if (reader.Remaining() != 0u) { const auto float16DenormPreserve = reader.ReadU8(); if (float16DenormPreserve > 1u) throw std::runtime_error("RequestSerializer: invalid float16DenormPreserve value"); result.request.target.float16DenormPreserve = float16DenormPreserve != 0u; - } else if (reader.Remaining() != 0u) { - throw std::runtime_error("RequestSerializer: unexpected trailing data"); } } + if (version >= 26u) { + result.linkedCallFallbacks = reader.ReadU32Vector(); + result.request.context.linkedCallFallbacks = result.linkedCallFallbacks; + } + if (version >= 21u && reader.Remaining() != 0u) throw std::runtime_error("RequestSerializer: unexpected trailing data"); return result; } diff --git a/core/shader/recompiler/ImageTableAbi.hpp b/core/shader/recompiler/ImageTableAbi.hpp index 8a07ef7cda..9b434141d8 100644 --- a/core/shader/recompiler/ImageTableAbi.hpp +++ b/core/shader/recompiler/ImageTableAbi.hpp @@ -75,6 +75,7 @@ enum class PoisonReason : std::uint32_t { HeapWritten, DriverRejected, UnsupportedOperation, + OffsetCarry, Count }; @@ -101,6 +102,7 @@ constexpr std::string_view PoisonReasonName(PoisonReason reason) { case PoisonReason::HeapWritten: return "table written by its dispatch"; case PoisonReason::DriverRejected: return "rejected by the driver"; case PoisonReason::UnsupportedOperation: return "unsupported operation"; + case PoisonReason::OffsetCarry: return "record offset carries past 32 bits"; case PoisonReason::Count: break; } return "unknown"; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp index f361365f4b..c402bbee7e 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp @@ -39,9 +39,14 @@ struct TableColumn { bool address = false; std::vector keyDomains; bool unkeyed = false; + bool indirect = false; + std::uint32_t indirectOffset = 0; + std::uint32_t indirectImmediate = 0; + std::uint32_t foldedAddend = 0; + std::uint32_t foldedCarry = 0; bool operator==(const TableColumn& other) const { - return heapSource == other.heapSource && stride == other.stride && addend == other.addend && offset == other.offset && dwordCount == other.dwordCount && sampler == other.sampler && address == other.address; + return heapSource == other.heapSource && stride == other.stride && addend == other.addend && offset == other.offset && dwordCount == other.dwordCount && sampler == other.sampler && address == other.address && indirect == other.indirect && indirectOffset == other.indirectOffset && indirectImmediate == other.indirectImmediate && foldedAddend == other.foldedAddend && foldedCarry == other.foldedCarry; } }; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp index 976a74f8e3..809c3e0932 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp @@ -39,6 +39,7 @@ struct IrDescriptorBinding { struct IrBindingLayout { std::uint32_t pushDataStartDword = PushData::NoStart; + std::uint32_t stencilReferenceDword = PushData::NoStart; std::uint32_t memoryOffsetDword = 0; std::uint32_t memoryOffsetCount = 0; bool dispatchThreadLimit = false; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp index 7c8d5d26d1..e340873ac7 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp @@ -51,6 +51,7 @@ struct MemoryInfo { bool coherent = false; bool gpuDescriptor = false; bool flushDenormals = false; + bool flushDenormalResults = false; bool operator==(const MemoryInfo& other) const = default; }; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp index 45a3e64956..c769521f18 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include #include @@ -77,13 +79,64 @@ struct UniformFillPlan { inline constexpr std::uint32_t NativePushSlotSize = sizeof(PushData); inline constexpr std::uint32_t NativePushConstantSize = 2u * NativePushSlotSize; +class IrValueArena { +public: + IrValueArena() = default; + IrValueArena(const IrValueArena&) = delete; + IrValueArena& operator=(const IrValueArena&) = delete; + IrValueArena(IrValueArena&& other) noexcept : values(std::exchange(other.values, nullptr)), count(std::exchange(other.count, 0)), capacity(std::exchange(other.capacity, 0)) {} + IrValueArena& operator=(IrValueArena&& other) noexcept { + if (this != &other) { + release(); + values = std::exchange(other.values, nullptr); + count = std::exchange(other.count, 0); + capacity = std::exchange(other.capacity, 0); + } + return *this; + } + ~IrValueArena() { release(); } + + void Reserve(std::size_t size) { + if (values != nullptr) throw std::runtime_error("IrValueArena::Reserve called twice"); + if (size == 0) return; + values = static_cast(::operator new(size * sizeof(IrValue), std::align_val_t{alignof(IrValue)})); + capacity = size; + } + IrValue* Create(IrOpcode opcode, IrType type, std::uint32_t id) { + if (count == capacity) throw std::runtime_error("IrValueArena::Create exceeds the reserved size"); + auto* value = new (values + count) IrValue(opcode, type, id); + ++count; + return value; + } + [[nodiscard]] std::size_t Size() const { return count; } + [[nodiscard]] IrValue& operator[](std::size_t index) { return values[index]; } + [[nodiscard]] const IrValue& operator[](std::size_t index) const { return values[index]; } + [[nodiscard]] IrValue* begin() { return values; } + [[nodiscard]] IrValue* end() { return values + count; } + [[nodiscard]] const IrValue* begin() const { return values; } + [[nodiscard]] const IrValue* end() const { return values + count; } + +private: + void release() { + if (values == nullptr) return; + for (std::size_t index = count; index > 0; --index) values[index - 1].~IrValue(); + ::operator delete(values, std::align_val_t{alignof(IrValue)}); + values = nullptr; + count = 0; + capacity = 0; + } + IrValue* values = nullptr; + std::size_t count = 0; + std::size_t capacity = 0; +}; + struct IrResourcePlan { IrShaderStage stage = IrShaderStage::Unknown; std::uint64_t shaderHash = 0; std::uint32_t userDataBase = 0; std::uint32_t userDataCount = 64; std::uint32_t srgbDecodeFormats = 0; - std::vector> valueStorage; + IrValueArena valueStorage; std::vector> blockStorage; std::vector memoryInfo; std::vector descriptorSources; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index db601d6231..e2ebd78e0a 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -89,7 +89,9 @@ struct ImageResource { bool constantSwizzle = false; bool constantSwizzleCompatible = true; bool flatVolumeCompatible = true; + bool singleMipPlaneBiasCompatible = false; bool flatLineCompatible = true; + bool flatVolumeLineCompatible = true; bool foldTexelOffsetsOnly = false; std::uint32_t byElements = 0; std::uint32_t byComponents = 0; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp index 96c7d51533..84ec2412d0 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp @@ -14,7 +14,9 @@ enum class TessellationAttribute { EvaluationInput, PatchOutput, Factor, - PatchInput + PatchInput, + InstanceOutput, + InstanceInput }; enum class TessellationBaseKind { @@ -46,6 +48,7 @@ enum class StageInputKind { HelperInvocation, RasterPrimitive, LayerExported, + BaseInstance, }; enum class StageOutputKind { diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp index 6ed27287ab..872aeae99e 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp @@ -73,6 +73,7 @@ enum class IrOpcode : std::uint16_t { Void, Reference, ReferenceU32, + UnlinkedCall, GetUserData, GetShaderBase, ShaderClock, @@ -431,6 +432,7 @@ enum class IrOpcode : std::uint16_t { SharedAtomicMskor64, DataAppend, DataConsume, + DataOrderedCount, SwizzleU32, ImageQueryDimensions, ImageQueryLod, diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp index baeba29a7a..d7adec321f 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp @@ -86,6 +86,7 @@ public: void SetImmediateU16(std::uint16_t value); void SetRegister(const GuestRegister& reg); void AddPhiOperand(IrBlock* predecessor, IrValue* value); + void SetDetachedOperands(std::vector arguments, std::vector phiBlocks); void ReplaceOpcode(IrOpcode opcode); void Invalidate(); void ReplaceUsesWith(IrValue* replacement, bool preserve); diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp index b61b111ffd..650a852f61 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp @@ -114,6 +114,7 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("Void", Void), makeMeta("Reference", Void, U1), makeMeta("ReferenceU32", Void, U32), + makeMeta("UnlinkedCall", Void, U32, U32, U32), makeMeta("GetUserData", U32, ScalarReg), makeMeta("GetShaderBase", U64), makeMeta("ShaderClock", U64), @@ -472,6 +473,7 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("SharedAtomicMskor64", U64, U32, U64, U64, U1), makeMeta("DataAppend", U32, U32, U1, U32, U32), makeMeta("DataConsume", U32, U32, U1, U32, U32), + makeMeta("DataOrderedCount", U32, U32, U1, U32, U32, U32), makeMeta("SwizzleU32", U32, U32, U32, U1), makeMeta("ImageQueryDimensions", U32x4, ImageResource, ImageAddress, U1), makeMeta("ImageQueryLod", U32x4, ImageResource, SamplerResource, ImageAddress, U1), @@ -695,6 +697,7 @@ SharedAccess SharedAccessOf(IrOpcode opcode) { case IrOpcode::SharedAtomicMskor64: return SharedAccess::Atomic; case IrOpcode::DataAppend: + case IrOpcode::DataOrderedCount: return SharedAccess::Append; case IrOpcode::DataConsume: return SharedAccess::Consume; @@ -888,6 +891,7 @@ bool IrOpcodeHasSideEffects(IrOpcode opcode) { case IrOpcode::Discard: case IrOpcode::Reference: case IrOpcode::ReferenceU32: + case IrOpcode::UnlinkedCall: case IrOpcode::SetTessellationAttribute: case IrOpcode::InterpolateHostP2: case IrOpcode::SetThreadBitScalarRegister: diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp index 7d0a7a6c98..3ea52ae5b0 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp @@ -222,7 +222,7 @@ std::size_t IrProgram::ReclaimUnusedValues() { }; keepBlocks(blocks); keepBlocks(resourcePlan.blockStorage); - for (const auto& value : resourcePlan.valueStorage) keep(value.get()); + for (auto& value : resourcePlan.valueStorage) keep(&value); for (const auto& source : resourcePlan.descriptorSources) for (IrValue* word : source.dwords) keep(word); for (const auto& read : resourcePlan.srtReads) keep(read.value); for (const auto& block : resourcePlan.controlFlow) keep(block.condition); diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp index 4a1a662b69..d7f234f908 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp @@ -188,6 +188,17 @@ void IrValue::AddPhiOperand(IrBlock* predecessor, IrValue* value) { AddArgument(value); } +void IrValue::SetDetachedOperands(std::vector arguments, std::vector phiBlocks) { + if (!this->arguments.empty() || !this->phiBlocks.empty()) { + throw std::runtime_error("IrValue::SetDetachedOperands called on a value with operands"); + } + if (IsPhi() ? phiBlocks.size() != arguments.size() : !phiBlocks.empty()) { + throw std::runtime_error("IrValue::SetDetachedOperands phi blocks do not match the arguments"); + } + this->arguments = std::move(arguments); + this->phiBlocks = std::move(phiBlocks); +} + void IrValue::ReplaceOpcode(IrOpcode opcode) { this->opcode = opcode; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp b/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp index 0944d23535..4b3755e656 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, bool registrationModules = false) const; + [[nodiscard]] BindingAllocationResult Allocate(IrProgram& program, const BindingLayout& layout, bool registrationModules = false, bool stencilReplay = false) const; [[nodiscard]] const IrDescriptorBinding& FindBinding(const IrBindingLayout& layout, DescriptorBindingKind kind) const; }; diff --git a/core/shader/recompiler/Optimization/include/Optimization/MaskedSelectEliminator.hpp b/core/shader/recompiler/Optimization/include/Optimization/MaskedSelectEliminator.hpp index 2e0b5bd5b5..e8b873610f 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/MaskedSelectEliminator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/MaskedSelectEliminator.hpp @@ -3,6 +3,7 @@ #include "IntermediateRepresentation/IrProgram.hpp" #include +#include namespace ShaderRecompiler { @@ -16,6 +17,9 @@ public: [[nodiscard]] MaskedSelectEliminationStats Eliminate(IrProgram& program) const; }; +[[nodiscard]] bool GuardImplies(const IrProgram& program, const IrValue* condition, const IrValue* consequence); +[[nodiscard]] bool GuardImpliesMaskOnPath(const IrProgram& program, const IrValue* condition, const IrValue* low, const IrValue* high, std::span path); + } #endif diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 77e52d36de..72fe2675ee 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -3,6 +3,7 @@ #include "IntermediateRepresentation/IrMetadata.hpp" #include "Recompiler.hpp" +#include #include #include #include @@ -115,6 +116,8 @@ struct ShaderTessellationInputInfo { std::uint32_t patchesPerGroup = 256; std::uint32_t primitiveGroupSize = 0; bool breakAtInstance = false; + std::uint32_t instancePatches = 0; + bool drawParameters = false; }; struct ShaderVertexInputInfo { @@ -165,6 +168,7 @@ struct ShaderPixelInputInfo { bool psPosZ = false; bool psPosW = false; bool psFrontFace = false; + bool psFrontFaceAllBits = false; bool psAncillary = false; bool psNoPerspective = false; bool psPixelKillEnable = false; @@ -178,12 +182,22 @@ struct ShaderPixelInputInfo { bool psOrderedPixelShader = false; bool psAlphaToCoverage = false; bool psDitheredAlphaToCoverage = false; + std::uint8_t quadPixelMask = 0xf; + std::array quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; ShaderStageRuntime stage; bool HasPositionInput() const { return psPosX || psPosY || psPosZ || psPosW; } + [[nodiscard]] bool QuadSampleMaskPartial() const { + return std::ranges::any_of(quadSampleMask, [](std::uint16_t mask) { return mask != 0xffffu; }); + } + + [[nodiscard]] bool QuadMasked() const { + return quadPixelMask != 0xf || QuadSampleMaskPartial(); + } + [[nodiscard]] bool InputIsDefault(std::uint32_t input) const { return input < inputNum && input < 32u && (interpolatorSettings[input] & 0x420u) == 0x20u; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp index 8332618a6b..1f0a417de6 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp @@ -75,6 +75,7 @@ class Evaluator { public: Evaluator(const IrResourcePlan& program, const SrtRuntime& runtime, std::span cleanFlatSlots = {}, Evaluator* cleanEvaluator = nullptr, IrValue* activeMask = nullptr) : _program(program), _runtime(runtime), _cleanFlatSlots(cleanFlatSlots), _cleanEvaluator(cleanEvaluator), _activeMask(activeMask != nullptr ? activeMask->Resolve() : nullptr) {} + static bool MayEvaluate(const IrResourcePlan& program, IrValue* value); bool Evaluate(IrValue* value, std::uint32_t& result); bool EvaluateWide(IrValue* raw, std::uint64_t& result); void ReportInaccessibleReads(InaccessibleRead* sink) { _inaccessible = sink; } diff --git a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp index e6f44d4687..1910bb1531 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, bool registrationModules) const { +BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const BindingLayout& layout, bool registrationModules, bool stencilReplay) const { IrProgramMetadata& metadata = program.Metadata(); if (!metadata.shaderInfoComplete || metadata.bindingLayoutComplete) { fail(metadata.shaderInfoComplete ? "shader binding layout failed: binding layout already allocated" @@ -115,6 +115,11 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin } const auto pushSize = layout.pushConstantSizeBytes / 4u; next.pushDataStartDword = next.runtimeImageCount == 0u && next.ShaderDataDwords() != 0u && next.ShaderDataDwords() <= pushSize ? layout.pushConstantOffsetBytes / 4u : PushData::NoStart; + if (stencilReplay) { + const auto offset = next.UsesPushData() ? next.ShaderDataDwords() : 0u; + if (offset >= pushSize) fail("stencil replay requires an unused push constant dword"); + next.stencilReferenceDword = offset; + } if (!info.buffers.empty()) { std::vector resources(info.buffers.size()); @@ -196,6 +201,10 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin result.pushConstantOffsetBytes = result.layout.pushDataStartDword * static_cast(sizeof(std::uint32_t)); result.pushConstantSizeBytes = result.layout.ShaderDataDwords() * static_cast(sizeof(std::uint32_t)); } + if (stencilReplay) { + result.pushConstantOffsetBytes = layout.pushConstantOffsetBytes; + result.pushConstantSizeBytes += sizeof(std::uint32_t); + } return result; } diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 2a437bc625..d988925a91 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -91,7 +91,10 @@ DescriptorRole RoleFor(DescriptorBindingKind kind) { DescriptorImageShape ImageShapeForResource(const ImageResource& image) { const RdnaImageDimensionInfo& info = RdnaImageDimensionInfoFor(image.dimension); if (info.multisampled != 0u) { - fail("DescriptorBindingBuilder::Populate multisampled image resources have no descriptor image shape"); + if (info.arrayed != 0u) { + fail("DescriptorBindingBuilder::Populate multisampled array image resources have no descriptor image shape"); + } + return DescriptorImageShape::Image2DMultisample; } if (info.spirvDimension == spv::Dim1D) { return info.arrayed != 0u ? DescriptorImageShape::Image1DArray : DescriptorImageShape::Image1D; @@ -181,6 +184,15 @@ UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapsh if (snapshot.samplers.at(r).dwordCount != 4u) fail("sampler descriptor must contain four dwords"); const auto& sampler = info.samplers[r]; if (sampler.table != NoTable) continue; + for (const auto& pair : info.sampledPairs) { + if (pair.sampler != r) continue; + const auto& image = info.images.at(pair.image); + if (!image.singleMipPlaneBiasCompatible || image.dimension != RdnaImageDimension::Dim2D) continue; + const auto mode = ResourceMaterializer::RuntimeImageMode(image, snapshot.images.at(pair.image), info.runtimeImageModes.at(pair.image)); + if (info.runtimeImageModes.at(pair.image).at(mode).dimension != RdnaImageDimension::Dim3D) continue; + const auto& words = snapshot.samplers.at(r).dwords; + if ((words[0] & ForceUnnormalizedBit) != 0u || ((words[0] >> 29u) & 3u) > 2u || ((words[2] >> 20u) & 0xfu) != 0u || ((words[2] >> 26u) & 3u) > 1u) fail("2D biased sampling of a one-slice volume requires a normalized point sampler"); + } if ((snapshot.samplers[r].dwords[0] & ForceUnnormalizedBit) == 0u) { if (sampler.foldTexelOffsetsOnly) { fail("DescriptorBindingBuilder::Populate normalized guest sampler is used with a texel offset that is not a constant, which image sampling takes only with VK_KHR_maintenance8 and shaderImageGatherExtended"); diff --git a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp index 61a65c21b4..7124e58f1b 100644 --- a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp @@ -781,6 +781,208 @@ MaskFamily familyOf(const std::vector& regions, const IrValue* mask, std return MaskFamily{mask->HasImmediate() || regionOf(regions, mask->Parent()) == Region::Once, wholeQuadSource(mask, waveSize) != nullptr}; } +class GuardProof { +public: + GuardProof(const IrProgram& program, const std::vector& regions, const BlockSet& uniform, const IrValue* observation, std::span path = {}) : program(program), regions(regions), uniform(uniform), observation(observation), path(path), waveSize(program.WaveSize()) {} + + bool ImpliesMask(const IrValue* condition, const IrValue* low, const IrValue* high) { + return within(condition, Words{low, high}, 0u); + } + + bool Implies(const IrValue* left, const IrValue* right, unsigned depth = 0) { + left = resolve(left); + right = resolve(right); + if (!follow(left, depth) || !follow(right, depth)) return false; + if (left == right || truth(left, false) || truth(right, true)) return true; + if (right->Opcode() == IrOpcode::LogicalAnd) return Implies(left, right->Argument(0), depth + 1u) && Implies(left, right->Argument(1), depth + 1u); + if (right->Opcode() == IrOpcode::LogicalOr) return Implies(left, right->Argument(0), depth + 1u) || Implies(left, right->Argument(1), depth + 1u); + if (left->Opcode() == IrOpcode::LogicalAnd) return Implies(left->Argument(0), right, depth + 1u) || Implies(left->Argument(1), right, depth + 1u); + if (left->Opcode() == IrOpcode::LogicalOr) return Implies(left->Argument(0), right, depth + 1u) && Implies(left->Argument(1), right, depth + 1u); + if (left->IsPhi() && merges(*left)) { + return std::all_of(left->Arguments().begin(), left->Arguments().end(), [&](const IrValue* incoming) { return Implies(incoming, right, depth + 1u); }); + } + if (left->Opcode() == IrOpcode::SelectU1) return Implies(left->Argument(1), right, depth + 1u) && Implies(left->Argument(2), right, depth + 1u); + if (const auto bit = threadBit(right, waveSize)) return within(left, Words{bit->low, bit->high}, depth + 1u); + if (const auto bit = threadBit(left, waveSize)) return fromWords(Words{bit->low, bit->high}, right, depth + 1u); + return false; + } + +private: + struct Words { + const IrValue* low; + const IrValue* high; + }; + + bool follow(const IrValue* value, unsigned depth) { + if (depth > 64u || remaining == 0u) return false; + --remaining; + return value->HasImmediate() || regionOf(regions, value->Parent()) == Region::Once; + } + + static bool truth(const IrValue* value, bool expected) { + return value->Type() == IrType::Bool && value->HasImmediate() && value->ImmediateBool() == expected; + } + + bool constant(Words words, std::uint32_t value) const { + return isImmediate(words.low, value) && (waveSize == 32u || isImmediate(words.high, value)); + } + + bool same(Words left, Words right) const { + return left.low == right.low && (waveSize == 32u || left.high == right.high); + } + + bool merges(const IrValue& phi) const { + if (regionOf(regions, phi.Parent()) != Region::Once || phi.ArgumentCount() == 0u || phi.ArgumentCount() != phi.PhiBlockCount()) return false; + return std::all_of(phi.Arguments().begin(), phi.Arguments().end(), [&](const IrValue* incoming) { return incoming->Resolve()->HasImmediate() || regionOf(regions, incoming->Resolve()->Parent()) == Region::Once; }); + } + + 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; + } + + const IrValue* resolve(const IrValue* value) const { + for (unsigned depth = 0; depth < 64u; ++depth) { + value = value->Resolve(); + if (!value->IsPhi()) return value; + const auto block = std::find(path.begin(), path.end(), value->Parent()); + if (block == path.end() || block == path.begin()) return value; + const IrValue* incoming = incomingFrom(*value, *(block - 1)); + if (incoming == nullptr) return value; + value = incoming; + } + return value; + } + + bool capturedHere(const IrValue* value) const { + if (uniform.contains(value->Parent())) return true; + const IrBlock* observedBlock = path.empty() ? observation->Parent() : path.back(); + if (!truth(value->Argument(0)->Resolve(), true) || observedBlock == nullptr || value->Parent() == nullptr) return false; + if (value->Parent() == observedBlock) { + if (!path.empty() && observation->Parent() != observedBlock) return true; + const auto& instructions = value->Parent()->Instructions(); + const auto captured = std::find(instructions.begin(), instructions.end(), value); + const auto observed = std::find(instructions.begin(), instructions.end(), observation); + return captured != instructions.end() && observed != instructions.end() && std::distance(instructions.begin(), captured) < std::distance(instructions.begin(), observed); + } + BlockSet visited; + std::vector pending{program.BlockOrder().front()}; + while (!pending.empty()) { + const IrBlock* block = pending.back(); + pending.pop_back(); + if (block == value->Parent() || !visited.insert(block).second) continue; + if (block == observedBlock) return false; + pending.insert(pending.end(), block->Successors().begin(), block->Successors().end()); + } + return true; + } + + const IrValue* ballot(const IrValue* word, std::uint32_t half) const { + if (word->Opcode() != IrOpcode::CompositeExtractU32x4 || !isImmediate(word->Argument(1)->Resolve(), half)) return nullptr; + const IrValue* value = word->Argument(0)->Resolve(); + return value->Opcode() == IrOpcode::Ballot ? value : nullptr; + } + + std::optional quadSource(Words words) const { + if (words.low->Opcode() != IrOpcode::CompositeExtractU64 || !isImmediate(words.low->Argument(1)->Resolve(), 0u)) return std::nullopt; + const IrValue* wqm = words.low->Argument(0)->Resolve(); + if (waveSize != 32u && (words.high->Opcode() != IrOpcode::CompositeExtractU64 || words.high->Argument(0)->Resolve() != wqm || !isImmediate(words.high->Argument(1)->Resolve(), 1u))) return std::nullopt; + if (wqm->Opcode() != IrOpcode::WqmU64) return std::nullopt; + const IrValue* source = wqm->Argument(0)->Resolve(); + if (source->Opcode() != IrOpcode::CompositeConstructU64 || source->ArgumentCount() != 2u) return std::nullopt; + return Words{source->Argument(0)->Resolve(), source->Argument(1)->Resolve()}; + } + + std::vector parts(Words words) const { + const bool lowAnd = words.low->Opcode() == IrOpcode::BitwiseAnd32; + const bool highAnd = waveSize != 32u && words.high->Opcode() == IrOpcode::BitwiseAnd32; + std::vector result; + if (!lowAnd && !highAnd) return result; + for (unsigned low = 0; low < (lowAnd ? 2u : 1u); ++low) { + for (unsigned high = 0; high < (highAnd ? 2u : 1u); ++high) { + result.push_back(Words{lowAnd ? words.low->Argument(low)->Resolve() : words.low, highAnd ? words.high->Argument(high)->Resolve() : words.high}); + } + } + return result; + } + + template + std::optional intersection(Words words, TProof proof) { + const bool lowAnd = words.low->Opcode() == IrOpcode::BitwiseAnd32; + const bool highAnd = waveSize != 32u && words.high->Opcode() == IrOpcode::BitwiseAnd32; + if (!lowAnd && !highAnd) return std::nullopt; + const auto part = [&](unsigned low, unsigned high) { + return Words{lowAnd ? words.low->Argument(low)->Resolve() : words.low, highAnd ? words.high->Argument(high)->Resolve() : words.high}; + }; + if (!lowAnd || !highAnd) return proof(part(0u, 0u)) && proof(part(lowAnd ? 1u : 0u, highAnd ? 1u : 0u)); + return (proof(part(0u, 0u)) && proof(part(1u, 1u))) || (proof(part(0u, 1u)) && proof(part(1u, 0u))); + } + + bool within(const IrValue* condition, Words words, unsigned depth) { + condition = resolve(condition); + words = Words{resolve(words.low), resolve(words.high)}; + if (!follow(condition, depth) || !follow(words.low, depth) || (waveSize != 32u && !follow(words.high, depth))) return false; + if (truth(condition, false) || constant(words, 0xffffffffu)) return true; + if (const auto bit = threadBit(condition, waveSize)) return subset(Words{bit->low, bit->high}, words, depth + 1u); + if (const auto result = intersection(words, [&](Words part) { return within(condition, part, depth + 1u); })) return *result; + if (const auto source = quadSource(words)) return within(condition, *source, depth + 1u); + const IrValue* lowBallot = ballot(words.low, 0u); + const IrValue* highBallot = waveSize == 32u ? lowBallot : ballot(words.high, 1u); + if (lowBallot && highBallot && capturedHere(lowBallot) && capturedHere(highBallot)) return Implies(condition, lowBallot->Argument(0), depth + 1u) && Implies(condition, highBallot->Argument(0), depth + 1u); + if (condition->Opcode() == IrOpcode::LogicalAnd) return within(condition->Argument(0), words, depth + 1u) || within(condition->Argument(1), words, depth + 1u); + if (condition->Opcode() == IrOpcode::LogicalOr) return within(condition->Argument(0), words, depth + 1u) && within(condition->Argument(1), words, depth + 1u); + if (condition->IsPhi() && merges(*condition)) return std::all_of(condition->Arguments().begin(), condition->Arguments().end(), [&](const IrValue* incoming) { return within(incoming, words, depth + 1u); }); + return false; + } + + bool fromWords(Words words, const IrValue* condition, unsigned depth) { + condition = resolve(condition); + words = Words{resolve(words.low), resolve(words.high)}; + if (!follow(condition, depth) || !follow(words.low, depth) || (waveSize != 32u && !follow(words.high, depth))) return false; + if (truth(condition, true) || constant(words, 0u)) return true; + if (const auto bit = threadBit(condition, waveSize)) return subset(words, Words{bit->low, bit->high}, depth + 1u); + if (condition->Opcode() == IrOpcode::LogicalAnd) return fromWords(words, condition->Argument(0), depth + 1u) && fromWords(words, condition->Argument(1), depth + 1u); + if (condition->Opcode() == IrOpcode::LogicalOr) return fromWords(words, condition->Argument(0), depth + 1u) || fromWords(words, condition->Argument(1), depth + 1u); + const auto intersections = parts(words); + if (!intersections.empty()) return std::any_of(intersections.begin(), intersections.end(), [&](Words part) { return fromWords(part, condition, depth + 1u); }); + const IrValue* lowBallot = ballot(words.low, 0u); + const IrValue* highBallot = waveSize == 32u ? lowBallot : ballot(words.high, 1u); + return lowBallot && highBallot && Implies(lowBallot->Argument(0), condition, depth + 1u) && Implies(highBallot->Argument(0), condition, depth + 1u); + } + + bool subset(Words left, Words right, unsigned depth) { + left = Words{resolve(left.low), resolve(left.high)}; + right = Words{resolve(right.low), resolve(right.high)}; + if (!follow(left.low, depth) || !follow(right.low, depth) || (waveSize != 32u && (!follow(left.high, depth) || !follow(right.high, depth)))) return false; + if (same(left, right) || constant(left, 0u) || constant(right, 0xffffffffu)) return true; + if (const auto result = intersection(right, [&](Words part) { return subset(left, part, depth + 1u); })) return *result; + const auto leftParts = parts(left); + if (!leftParts.empty() && std::any_of(leftParts.begin(), leftParts.end(), [&](Words part) { return subset(part, right, depth + 1u); })) return true; + if (const auto source = quadSource(right); source && subset(left, *source, depth + 1u)) return true; + if (left.low->IsPhi() && merges(*left.low) && (waveSize == 32u || (left.high->IsPhi() && left.high->Parent() == left.low->Parent() && merges(*left.high)))) { + for (std::size_t index = 0; index < left.low->PhiBlockCount(); ++index) { + const IrValue* highIncoming = waveSize == 32u ? left.high : incomingFrom(*left.high, left.low->PhiBlock(index)); + if (highIncoming == nullptr || !subset(Words{left.low->Argument(index)->Resolve(), highIncoming}, right, depth + 1u)) return false; + } + return true; + } + const IrValue* lowBallot = ballot(left.low, 0u); + const IrValue* highBallot = waveSize == 32u ? lowBallot : ballot(left.high, 1u); + if (lowBallot && highBallot && within(lowBallot->Argument(0), right, depth + 1u) && within(highBallot->Argument(0), right, depth + 1u)) return true; + lowBallot = ballot(right.low, 0u); + highBallot = waveSize == 32u ? lowBallot : ballot(right.high, 1u); + return lowBallot && highBallot && capturedHere(lowBallot) && capturedHere(highBallot) && fromWords(left, lowBallot->Argument(0), depth + 1u) && fromWords(left, highBallot->Argument(0), depth + 1u); + } + + const IrProgram& program; + const std::vector& regions; + const BlockSet& uniform; + const IrValue* observation; + std::span path; + std::uint32_t waveSize; + std::uint32_t remaining = 8192u; +}; + class UnreadLanes; using FirstLaneReads = std::array, 4>; @@ -1019,4 +1221,36 @@ MaskedSelectEliminationStats MaskedSelectEliminator::Eliminate(IrProgram& progra return stats; } +bool GuardImplies(const IrProgram& program, const IrValue* condition, const IrValue* consequence) { + if (condition == nullptr || consequence == nullptr) return false; + condition = condition->Resolve(); + consequence = consequence->Resolve(); + if (condition->Type() != IrType::Bool || consequence->Type() != IrType::Bool) return false; + if (condition == consequence || (condition->HasImmediate() && !condition->ImmediateBool()) || (consequence->HasImmediate() && consequence->ImmediateBool())) return true; + const auto regions = blockRegions(program); + if (std::ranges::find(regions, Region::Loop) != regions.end()) return false; + if ((!condition->HasImmediate() && regionOf(regions, condition->Parent()) != Region::Once) || (!consequence->HasImmediate() && regionOf(regions, consequence->Parent()) != Region::Once)) return false; + const BlockSet uniform = uniformEntryBlocks(program); + std::uint64_t steps = 0; + MaskSets sets(steps); + ImpliedMasks implied(regions, uniform, program.WaveSize(), familyOf(regions, consequence, program.WaveSize()), sets, steps); + implied.AddMask(consequence); + return sets.Contains(implied.Of(condition), consequence->Id()) || GuardProof(program, regions, uniform, condition).Implies(condition, consequence); +} + +bool GuardImpliesMaskOnPath(const IrProgram& program, const IrValue* condition, const IrValue* low, const IrValue* high, std::span path) { + if (condition == nullptr || low == nullptr || high == nullptr || path.empty() || program.BlockOrder().empty() || path.front() != program.BlockOrder().front()) return false; + if (condition->Resolve()->Type() != IrType::Bool || low->Resolve()->Type() != IrType::U32 || high->Resolve()->Type() != IrType::U32) return false; + BlockSet visited; + for (std::size_t index = 0; index < path.size(); ++index) { + if (path[index] == nullptr || !visited.insert(path[index]).second) return false; + if (index != 0u && std::ranges::find(path[index - 1u]->Successors(), path[index]) == path[index - 1u]->Successors().end()) return false; + } + if (!condition->Resolve()->HasImmediate() && std::ranges::find(path, condition->Resolve()->Parent()) == path.end()) return false; + const auto regions = blockRegions(program); + if (std::ranges::find(regions, Region::Loop) != regions.end()) return false; + const auto uniform = uniformEntryBlocks(program); + return GuardProof(program, regions, uniform, condition->Resolve(), path).ImpliesMask(condition, low, high); +} + } diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index e638726397..3ed3870d7b 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -1,5 +1,6 @@ #include "Optimization/ResourceMaterializer.hpp" #include "Optimization/SrtWalker/SrtFlatSlotClasses.hpp" +#include "Optimization/SrtWalker/SrtEvaluator.hpp" #include "Optimization/ShaderStageInputInfo.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "RdnaDecoder/RdnaImageOpDecoder.hpp" @@ -345,7 +346,7 @@ void reportTables() { static_cast(counters.narrowed.exchange(0, std::memory_order_relaxed)), static_cast(counters.narrowSkipped.exchange(0, std::memory_order_relaxed)), static_cast(counters.nanoseconds.exchange(0, std::memory_order_relaxed)) / 1e6); } -enum class RecordState : std::uint8_t { OutsideDomain, Read, Unmapped }; +enum class RecordState : std::uint8_t { OutsideDomain, Read, Unmapped, Carried }; struct ColumnRecords { std::uint64_t base = 0; @@ -555,7 +556,7 @@ private: } records.base = heap.Base48(); records.size = heap.GetSize(); - records.records = ImageTableAbi::ScalarBufferRecords(column.addend, column.stride, column.offset, records.size); + records.records = ImageTableAbi::ScalarBufferRecords(column.addend, column.stride, column.offset + (column.indirect ? 4u : 0u), records.size); if (records.size != 0u) tables.ranges.emplace_back(records.base, records.size); } records.records = std::min(records.records, ImageTableAbi::KeyRecords(column.maxKey, column.stride)); @@ -567,6 +568,38 @@ private: } records.words.assign(records.keys, 0u); const auto end = records.base + records.size; + if (column.indirect) { + for (std::uint32_t record = 0; record < records.keys; record++) { + if (records.states[record] != RecordState::Read) continue; + const auto offset = column.addend + record * column.stride; + const auto lowWord = ImageTableAbi::ScalarBufferDword(offset, column.offset, records.size); + const auto highWord = ImageTableAbi::ScalarBufferDword(offset, column.offset + 4u, records.size); + if (!lowWord.has_value() || !highWord.has_value() || !PageAccessible(records.base + *lowWord, records.base, end) || !PageAccessible(records.base + *highWord, records.base, end)) { + records.states[record] = RecordState::Unmapped; + continue; + } + const std::uint64_t pointer = (static_cast(Read(records.base + *highWord)) << 32u) | Read(records.base + *lowWord); + DescriptorValue value; + value.dwordCount = column.sampler ? 4u : 8u; + std::uint64_t first = std::numeric_limits::max(); + std::uint64_t past = 0; + for (std::uint32_t dword = 0; dword < column.dwordCount; dword++) { + auto address = ImageTableAbi::ScalarAddressDword(pointer, column.indirectOffset, column.indirectImmediate + dword * 4u); + if (address.has_value() && (*address & 3u) != 0u) address.reset(); + if (!address.has_value() || !AddressAccessible(*address)) { + records.states[record] = RecordState::Unmapped; + break; + } + value.dwords[dword] = Read(*address); + first = std::min(first, *address); + past = std::max(past, *address + sizeof(std::uint32_t)); + } + if (records.states[record] != RecordState::Read) continue; + records.words[record] = WordsIndex(value); + tables.ranges.emplace_back(first, past - first); + } + return records; + } constexpr auto Unread = std::numeric_limits::max(); std::vector whole(records.keys, Unread); std::vector>> split; @@ -611,6 +644,10 @@ private: for (std::uint32_t record = 0; record < records.keys; record++) { if (records.states[record] != RecordState::Read) continue; const auto offset = column.addend + record * column.stride; + if (offset >= column.foldedAddend && offset > 0xffffffffu - column.foldedCarry) { + records.states[record] = RecordState::Carried; + continue; + } const auto head = locate(offset, column.offset); const auto tail = locate(offset, column.offset + last); if (head.has_value() && tail.has_value() && (*head & 3u) == 0u && *tail > *head && *tail - *head == last && wholeAccessible(*head) && wholeAccessible(*tail)) { @@ -698,6 +735,10 @@ private: out.codes[key] = ImageTableAbi::PoisonCode(static_cast(PoisonReason::Unmapped)); counters.poison[static_cast(PoisonReason::Unmapped)].fetch_add(1, std::memory_order_relaxed); break; + case RecordState::Carried: + out.codes[key] = ImageTableAbi::PoisonCode(static_cast(PoisonReason::OffsetCarry)); + counters.poison[static_cast(PoisonReason::OffsetCarry)].fetch_add(1, std::memory_order_relaxed); + break; } trace.poison++; } @@ -798,6 +839,29 @@ void materializeSnapshot(const IrResourcePlan& plan, const SrtRuntime& runtime, } } +void materializeForcedDegamma(const IrResourcePlan& plan, ResourceSnapshot& snapshot) { + for (std::uint32_t index = 0; index < plan.info.images.size(); ++index) { + auto& descriptor = snapshot.images.at(index); + if (((descriptor.dwords[1] >> 20u) & 0x1ffu) != static_cast(IrBufferFormat::Format8_8_8_8UInt)) continue; + bool forced = false; + bool plain = false; + for (const auto& pair : plan.info.sampledPairs) { + if (pair.image != index) continue; + const auto& sampler = snapshot.samplers.at(pair.sampler); + if (sampler.dwordCount != 4u) throw std::runtime_error("FORCE_DEGAMMA requires four sampler descriptor words"); + const bool enabled = (sampler.dwords[0] & (1u << 20u)) != 0u; + forced = forced || enabled; + plain = plain || !enabled; + } + if (!forced) continue; + const auto& image = plan.info.images.at(index); + if (!image.singleMipPlaneBiasCompatible || image.dimension != RdnaImageDimension::Dim2D || image.table != NoTable || image.resourceClass != ImageResourceClass::Sampled || image.depthCompare || image.packed) throw std::runtime_error("FORCE_DEGAMMA on RGBA8_UINT requires a direct plain biased pixel sample"); + if (plain) throw std::runtime_error("FORCE_DEGAMMA on RGBA8_UINT with mixed sampler states is not implemented"); + if ((descriptor.dwords[3] >> 28u) != 10u || (descriptor.dwords[4] & 0x1fffu) != 0u || (descriptor.dwords[3] & 0xff000u) != 0u || (descriptor.dwords[5] & 0xf0u) != 0u) throw std::runtime_error("FORCE_DEGAMMA on RGBA8_UINT requires a one-slice volume at mip zero"); + descriptor.dwords[1] = (descriptor.dwords[1] & ~(0x1ffu << 20u)) | (static_cast(IrBufferFormat::Format8_8_8_8Srgb) << 20u); + } +} + std::uint32_t colorCompareReference(IrBufferFormat format) { switch (format) { case IrBufferFormat::Format16_16UNorm: @@ -889,7 +953,12 @@ void materializeTables(const IrResourcePlan& plan, ResourceSnapshot& snapshot) { snapshot.buffers.at(i).dwords[3] = 0u; } const auto decoded = decodeBufferDescriptor(snapshot.buffers.at(i)); - if (decoded.Type() != 0u) throw std::runtime_error("buffer descriptor uses an unsupported type"); + if (decoded.Type() != 0u) { + const auto& words = snapshot.buffers.at(i).dwords; + char detail[64]; + std::snprintf(detail, sizeof(detail), " (buffer %u V# %08x %08x %08x %08x)", i, words[0], words[1], words[2], words[3]); + throw std::runtime_error(std::string("buffer descriptor uses an unsupported type") + detail); + } if (plan.stage != IrShaderStage::Compute && decoded.AddTid()) throw std::runtime_error("buffer ADD_TID is only valid for compute shaders"); } if (snapshot.flattenedSrt.size() != plan.srtReads.size()) throw std::runtime_error("runtime SRT size differs from the static interface"); @@ -1142,7 +1211,8 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe modes.push_back(mode); const bool volumeLoad = (image.dimension == RdnaImageDimension::Dim2D || image.dimension == RdnaImageDimension::Dim2DArray) && image.fmaskCompatible; const bool volumeStore = image.dimension == RdnaImageDimension::Dim2DArray && storage && image.written && !image.read && !image.atomic && image.mipMode != ImageMipMode::DynamicStorage; - if ((volumeLoad || volumeStore) && !depth && packed == IrBufferFormat::Invalid && image.byElements == 0u) { + const bool volumePlaneBias = image.dimension == RdnaImageDimension::Dim2D && image.singleMipPlaneBiasCompatible && !storage && image.table == NoTable && conversion == IrBufferFormat::Invalid; + if ((volumeLoad || volumeStore || volumePlaneBias) && !depth && packed == IrBufferFormat::Invalid && image.byElements == 0u) { auto volume = mode; volume.dimension = RdnaImageDimension::Dim3D; modes.push_back(volume); @@ -1154,6 +1224,11 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe modes.push_back(plane); } } + if (image.dimension == RdnaImageDimension::Dim3D && image.flatVolumeLineCompatible && !storage && !depth && conversion == IrBufferFormat::Invalid && packed == IrBufferFormat::Invalid) { + auto line = mode; + line.dimension = RdnaImageDimension::Dim1D; + modes.push_back(line); + } if (image.dimension == RdnaImageDimension::Dim2D && storage && image.written && !image.read && !image.atomic && image.mipMode != ImageMipMode::DynamicStorage && !depth && packed == IrBufferFormat::Invalid && image.byElements == 0u) { auto line = mode; line.dimension = RdnaImageDimension::Dim1D; @@ -1321,6 +1396,9 @@ RuntimeImageModeMatch ResourceMaterializer::TryRuntimeImageMode(const ImageResou } } } + if (image.singleMipPlaneBiasCompatible && image.dimension == RdnaImageDimension::Dim2D && decoded.dimension == RdnaImageDimension::Dim3D) { + if ((descriptor.dwords[4] & 0x1fffu) != 0u || (descriptor.dwords[3] & 0xff000u) != 0u || (descriptor.dwords[5] & 0xf0u) != 0u) throw std::runtime_error("2D biased sampling of a volume requires one slice and mip level zero only"); + } for (std::uint32_t index = 0u; index < modes.size(); ++index) { const auto& mode = modes[index]; if (mode.constantSwizzle) continue; @@ -1445,13 +1523,13 @@ void ownPlanValues(IrResourcePlan& plan) { if (argument != nullptr) pending.push_back(argument); } } + plan.valueStorage.Reserve(order.size()); for (const auto* value : order) { - auto clone = std::make_unique(value->Opcode(), value->Type(), value->Id()); + auto* clone = plan.valueStorage.Create(value->Opcode(), value->Type(), value->Id()); clone->SetFlags(value->Flags()); if (value->HasImmediate()) clone->SetImmediateU64(value->ImmediateU64()); clone->SetRegister(value->Register()); - clones[value] = clone.get(); - plan.valueStorage.push_back(std::move(clone)); + clones[value] = clone; } std::unordered_map blocks; const auto blockFor = [&](const IrBlock* block) { @@ -1463,16 +1541,16 @@ void ownPlanValues(IrResourcePlan& plan) { return clone; }; for (const auto* value : order) { - auto* clone = clones.at(value); + std::vector arguments; + std::vector phiBlocks; + arguments.reserve(value->ArgumentCount()); + if (value->IsPhi()) phiBlocks.reserve(value->ArgumentCount()); for (std::size_t index = 0; index < value->ArgumentCount(); index++) { const auto* argument = value->Argument(index); - auto* mapped = argument == nullptr ? nullptr : clones.at(argument); - if (value->IsPhi()) { - clone->AddPhiOperand(blockFor(value->PhiBlock(index)), mapped); - } else { - clone->AddArgument(mapped); - } + arguments.push_back(argument == nullptr ? nullptr : clones.at(argument)); + if (value->IsPhi()) phiBlocks.push_back(blockFor(value->PhiBlock(index))); } + clones.at(value)->SetDetachedOperands(std::move(arguments), std::move(phiBlocks)); } for (auto* root : roots) { if (*root != nullptr) *root = clones.at(*root); @@ -1503,6 +1581,9 @@ IrResourcePlan ResourceMaterializer::ExtractPlan(const IrProgram& program) const plan.info = source.info; PrepareImageModes(plan.info); plan.uniformFill = source.uniformFill; + for (auto& block : plan.controlFlow) { + if (block.condition != nullptr && !Detail::Evaluator::MayEvaluate(source, block.condition)) block.condition = nullptr; + } ownPlanValues(plan); const auto addSource = [&plan](std::uint32_t index) { if (index >= plan.descriptorSources.size()) { @@ -1552,6 +1633,7 @@ void ResourceMaterializer::Materialize(const IrResourcePlan& program, const SrtR throw; } const auto started = MaterializeProfiled() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + materializeForcedDegamma(plan, nextSnapshot); materializeTables(plan, nextSnapshot); materializeSrtGuards(plan, nextSnapshot); if (MaterializeProfiled()) specializationNanoseconds.fetch_add(static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()), std::memory_order_relaxed); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 5209bd0cfb..d8634e78ca 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -539,6 +539,17 @@ private: return KeyDomain{sourceIndex, memory->offset, std::max(4u, 1u << zeros)}; } + static IrValue* SplitImmediateAddend(IrValue* value, std::uint32_t& addend) { + value = value->Resolve(); + addend = 0; + if (value->Opcode() == IrOpcode::IAdd32 && value->ArgumentCount() == 2u) { + if (immediateU32(value->Argument(0), addend)) return value->Argument(1)->Resolve(); + if (immediateU32(value->Argument(1), addend)) return value->Argument(0)->Resolve(); + } + addend = 0; + return value; + } + bool TryMakeTableColumn(IrValue& handle, bool sampler, bool r128, TableColumnPlan& plan) { const auto expected = sampler ? IrOpcode::GetSamplerResource : IrOpcode::GetImageResource; const std::uint32_t width = sampler ? 4u : 8u; @@ -558,6 +569,10 @@ private: } IrValue* heapHandle = nullptr; IrValue* offset = nullptr; + IrValue* base = nullptr; + std::uint32_t baseAddend = 0; + std::uint32_t carry = 0; + bool folded = false; std::uint32_t first = 0; bool address = false; for (std::uint32_t dword = 0; dword < words; dword++) { @@ -568,20 +583,30 @@ private: if (memory == nullptr || !MemoryIndexBelongsTo(memoryIndex, *read)) { return false; } + std::uint32_t readAddend = 0; + IrValue* readBase = SplitImmediateAddend(read->Argument(1), readAddend); if (dword == 0u) { first = memory->offset; address = addressRead; - } else if (addressRead != address || memory->offset != first + dword * static_cast(sizeof(std::uint32_t))) { + base = readBase; + baseAddend = readAddend; + } else if (addressRead != address || memory->offset + readAddend != first + baseAddend + dword * static_cast(sizeof(std::uint32_t))) { return false; + } else if (readAddend != baseAddend) { + if (readAddend < baseAddend || static_cast(memory->offset) + (readAddend - baseAddend) > 0x7fffffffu) { + return false; + } + folded = true; + carry = std::max(carry, readAddend - baseAddend); } IrValue* currentHandle = read->Argument(0)->Resolve(); - if (heapHandle != nullptr && currentHandle != heapHandle) { + if (heapHandle != nullptr && currentHandle != heapHandle && !EquivalentValue(m_program.Resources(), heapHandle, currentHandle)) { return false; } - heapHandle = currentHandle; + if (heapHandle == nullptr) heapHandle = currentHandle; if (dword == 0u) { offset = read->Argument(1)->Resolve(); - } else if (!EquivalentValue(m_program.Resources(), offset, read->Argument(1))) { + } else if (!EquivalentValue(m_program.Resources(), base, readBase)) { return false; } plan.memory.push_back(memoryIndex); @@ -599,6 +624,10 @@ private: std::uint32_t stride = 0; std::uint32_t addend = 0; bool masked = false; + std::uint32_t descriptorOffset = 0; + if (address && immediateU32(offset, descriptorOffset)) { + return TryMakeIndirectColumn(handle, *heapHandle, sampler, words, descriptorOffset, first, plan); + } if (!MatchAffineOffset(offset, key, stride, addend)) { if (!MatchMaskedOffset(offset, key, stride)) { return false; @@ -619,6 +648,8 @@ private: column.maxKey = possibleU32Bits(key); column.sampler = sampler; column.address = address; + column.foldedAddend = folded ? baseAddend : 0u; + column.foldedCarry = folded ? carry : 0u; const auto domain = masked ? std::optional{} : MatchKeyDomain(key); DescriptorSource columnSource = heapSource; columnSource.tableColumn = column; @@ -637,6 +668,68 @@ private: return true; } + bool TryMakeIndirectColumn(IrValue& handle, IrValue& pointer, bool sampler, std::uint32_t words, std::uint32_t descriptorOffset, std::uint32_t descriptorImmediate, TableColumnPlan& plan) { + if (pointer.Opcode() != IrOpcode::GetAddressResource || pointer.ArgumentCount() != 2u) { + return false; + } + IrValue* bufferHandle = nullptr; + IrValue* offset = nullptr; + std::uint32_t first = 0; + for (std::uint32_t dword = 0; dword < 2u; dword++) { + IrValue* read = pointer.Argument(dword)->Resolve(); + std::uint32_t memoryIndex = 0; + const MemoryInfo* memory = ScalarReadMemory(*read, memoryIndex); + if (memory == nullptr || !MemoryIndexBelongsTo(memoryIndex, *read)) { + return false; + } + if (dword == 0u) { + first = memory->offset; + bufferHandle = read->Argument(0)->Resolve(); + offset = read->Argument(1)->Resolve(); + } else if (memory->offset != first + static_cast(sizeof(std::uint32_t)) || read->Argument(0)->Resolve() != bufferHandle || !EquivalentValue(m_program.Resources(), offset, read->Argument(1))) { + return false; + } + } + IrValue* key = nullptr; + std::uint32_t stride = 0; + std::uint32_t addend = 0; + if (!MatchAffineOffset(offset, key, stride, addend)) { + return false; + } + DescriptorSource heapSource; + std::uint32_t heapIndex = 0; + if (!MakeRuntimeSource(*bufferHandle, IrOpcode::GetBufferResource, 4u, heapIndex, heapSource)) { + return false; + } + TableColumn column; + column.heapSource = heapIndex; + column.stride = stride; + column.addend = addend; + column.offset = first; + column.dwordCount = words; + column.maxKey = possibleU32Bits(key); + column.sampler = sampler; + column.indirect = true; + column.indirectOffset = descriptorOffset; + column.indirectImmediate = descriptorImmediate; + const auto domain = MatchKeyDomain(key); + DescriptorSource columnSource = heapSource; + columnSource.tableColumn = column; + plan.handle = &handle; + plan.source = InternSource(columnSource); + plan.keyDomain = domain; + auto& shared = *m_sources[plan.source].tableColumn; + shared.maxKey = std::max(shared.maxKey, column.maxKey); + if (!domain.has_value()) { + shared.unkeyed = true; + shared.keyDomains.clear(); + } else if (!shared.unkeyed && std::find(shared.keyDomains.begin(), shared.keyDomains.end(), *domain) == shared.keyDomains.end()) { + shared.keyDomains.push_back(*domain); + } + plan.offset = offset; + return true; + } + const TableColumnPlan* FindTableColumn(const IrValue& handle) const { const auto found = std::ranges::find_if(m_tableColumns, [&](const TableColumnPlan& plan) { return plan.handle == &handle; @@ -1376,6 +1469,7 @@ private: return std::numeric_limits::max(); } ImageResource image; + image.singleMipPlaneBiasCompatible = m_program.Resources().stage == IrShaderStage::Pixel; image.source = source; image.firstUsePc = pc; image.resourceClass = resourceClass; @@ -1406,6 +1500,7 @@ private: image.atomic = image.atomic || atomic; image.srgbDecodeCompatible = image.srgbDecodeCompatible && !ImageOpcodeInfoOf(op).needsSampler; image.fmaskCompatible = image.fmaskCompatible && op == IrOpcode::ImageRead && memory.dataBits == 32u; + image.singleMipPlaneBiasCompatible = image.singleMipPlaneBiasCompatible && op == IrOpcode::ImageSampleRaw && memory.imageSampleFlags == RdnaImageSampleFlagBias && memory.dataBits == 32u && !memory.imageR128 && !memory.imagePacked && memory.imageByElements == 0u; image.depthBitsCompatible = image.depthBitsCompatible && memory.dataBits == 32u; image.constantSwizzleCompatible = image.constantSwizzleCompatible && (op == IrOpcode::ImageSampleRaw || op == IrOpcode::ImageGatherRaw); const bool flatVolumeLoad = op == IrOpcode::ImageRead && memory.imageSampleFlags == 0u; @@ -1413,8 +1508,11 @@ private: const bool flatVolumeQuery = op == IrOpcode::ImageQueryDimensions; image.flatVolumeCompatible = image.flatVolumeCompatible && (flatVolumeLoad || flatVolumeSample || flatVolumeQuery) && !memory.imagePacked && memory.imageByElements == 0u; const bool flatLineLoad = op == IrOpcode::ImageRead && !memory.imageHasMip && memory.imageSampleFlags == 0u; - const bool flatLineSample = op == IrOpcode::ImageSampleRaw && (memory.imageSampleFlags == RdnaImageSampleFlagLevelZero || memory.imageSampleFlags == 0u); - image.flatLineCompatible = image.flatLineCompatible && (flatLineLoad || flatLineSample) && !memory.imagePacked && memory.imageByElements == 0u; + const auto lineSampleFlags = memory.imageSampleFlags & ~RdnaImageSampleFlagCompare; + const bool flatLineSample = op == IrOpcode::ImageSampleRaw && (lineSampleFlags == RdnaImageSampleFlagLevelZero || lineSampleFlags == 0u); + const bool flatLineExplicitLod = op == IrOpcode::ImageSampleRaw && memory.imageSampleFlags == RdnaImageSampleFlagLod; + image.flatLineCompatible = image.flatLineCompatible && (flatLineLoad || flatLineSample || flatLineExplicitLod) && !memory.imagePacked && memory.imageByElements == 0u; + image.flatVolumeLineCompatible = image.flatVolumeLineCompatible && (flatLineLoad || flatLineSample) && !memory.imagePacked && memory.imageByElements == 0u; if ((memory.imageSampleFlags & RdnaImageSampleFlagCompare) != 0u) { constexpr auto unsupported = RdnaImageSampleFlagLod | RdnaImageSampleFlagDerivative; if (op == IrOpcode::ImageGatherRaw || (memory.imageSampleFlags & unsupported) != 0u) image.emulatedCompare |= EmulatedCompare::Unsupported; diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 6ded41848d..fac95cb5d4 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -102,7 +102,7 @@ void ValidateValueReferences(const IrProgram& program, ShaderStageInputInfo inpu return Fail("interpolation parameter reference is invalid"); } if (input->ImmediateU32() >= inputInfo.pixel->inputNum || component->ImmediateU32() >= 4u || mode->ImmediateU32() >= 3u) { - return Fail("interpolation parameter reference is out of range"); + return Fail("interpolation parameter reference is out of range (attribute " + std::to_string(input->ImmediateU32()) + " component " + std::to_string(component->ImmediateU32()) + " mode " + std::to_string(mode->ImmediateU32()) + ", " + std::to_string(inputInfo.pixel->inputNum) + " inputs)"); } break; } @@ -151,6 +151,7 @@ void ValidateValueReferences(const IrProgram& program, ShaderStageInputInfo inpu break; case StageInputKind::VertexIndex: case StageInputKind::InstanceIndex: + case StageInputKind::BaseInstance: case StageInputKind::InvocationId: case StageInputKind::PrimitiveId: case StageInputKind::FrontFacing: @@ -236,7 +237,7 @@ bool IsPixelParameterFlat(const ShaderPixelInputInfo& pixel, std::uint32_t input } void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pixel, ShaderInfo& info) { - if (pixel->HasPositionInput() || pixel->psDitheredAlphaToCoverage) { + if (pixel->HasPositionInput() || pixel->psDitheredAlphaToCoverage || pixel->QuadMasked()) { AddInput(info, StageInputKind::FragCoord, 0, 4, "gl_FragCoord"); } if (pixel->psFrontFace) { @@ -343,6 +344,9 @@ void CollectBuiltinInputs(const IrProgram& program, ShaderInfo& info) { case StageInputKind::InstanceIndex: AddInput(info, kind, 0, 1, "gl_InstanceIndex"); break; + case StageInputKind::BaseInstance: + AddInput(info, kind, 0, 1, "gl_BaseInstance"); + break; case StageInputKind::InvocationId: AddInput(info, kind, 0, 1, "gl_InvocationID"); break; diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp index 36baa22dc5..7e8f767e2b 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp @@ -10,6 +10,10 @@ #include #include #include +#include +#include +#include +#include namespace ShaderRecompiler::Detail { @@ -178,6 +182,150 @@ bool Evaluator::EvaluateRawRead(IrValue& inst, std::uint64_t& result) { return true; } +namespace { + +std::optional evaluatedArguments(IrOpcode opcode) { + switch (opcode) { + case IrOpcode::BitCastU32F32: + case IrOpcode::BitCastF32U32: + case IrOpcode::ConvertF32U32: + case IrOpcode::ConvertU32F32: + case IrOpcode::FPTrunc32: + case IrOpcode::FPIsNan32: + case IrOpcode::BitwiseNot32: + case IrOpcode::LogicalNot: return 1u; + case IrOpcode::CompositeConstructU64: + case IrOpcode::IAdd32: + case IrOpcode::IAdd64: + case IrOpcode::ISub32: + case IrOpcode::ISub64: + case IrOpcode::IMul32: + case IrOpcode::IMul64: + case IrOpcode::SMin32: + case IrOpcode::UMin32: + case IrOpcode::FPMul32: + case IrOpcode::FPOrdLessThanEqual32: + case IrOpcode::FPOrdGreaterThanEqual32: + case IrOpcode::BitwiseAnd32: + case IrOpcode::BitwiseAnd64: + case IrOpcode::BitwiseOr32: + case IrOpcode::BitwiseXor32: + case IrOpcode::ShiftLeftLogical32: + case IrOpcode::ShiftLeftLogical64: + case IrOpcode::ShiftRightLogical32: + case IrOpcode::ShiftRightLogical64: + case IrOpcode::ShiftRightArithmetic32: + case IrOpcode::ShiftRightArithmetic64: + case IrOpcode::IEqual32: + case IrOpcode::INotEqual32: + case IrOpcode::SLessThan32: + case IrOpcode::ULessThan32: + case IrOpcode::UGreaterThan32: + case IrOpcode::LogicalAnd: + case IrOpcode::LogicalOr: + case IrOpcode::LogicalXor: return 2u; + case IrOpcode::BitFieldUExtract: + case IrOpcode::BitFieldSExtract: + case IrOpcode::SelectU32: + case IrOpcode::SelectU1: + case IrOpcode::SelectF32: return 3u; + case IrOpcode::BitFieldInsert: return 4u; + default: return std::nullopt; + } +} + +std::optional evaluableAlone(const IrResourcePlan& program, const IrValue& value) { + if (value.HasImmediate()) { + switch (value.Type()) { + case IrType::Bool: + case IrType::U8: + case IrType::U16: + case IrType::U32: + case IrType::U64: + case IrType::F32: return true; + default: return false; + } + } + switch (value.Opcode()) { + case IrOpcode::GetUserData: + case IrOpcode::GetShaderBase: + case IrOpcode::ReadFirstLane: + case IrOpcode::ReadConst: return true; + case IrOpcode::LoadAddressU32: + case IrOpcode::ReadConstBuffer: return IsRawRead(program, value); + case IrOpcode::Phi: return std::nullopt; + case IrOpcode::CompositeExtractU64: + case IrOpcode::CompositeExtractU32x2: { + if (value.ArgumentCount() < 2) return true; + const auto* index = value.Argument(1) != nullptr ? value.Argument(1)->Resolve() : nullptr; + if (index == nullptr || !index->HasImmediate() || index->Type() != IrType::U32 || index->ImmediateU32() >= 2u) return false; + if (value.Opcode() == IrOpcode::CompositeExtractU64) return std::nullopt; + const auto* source = value.Argument(0) != nullptr ? value.Argument(0)->Resolve() : nullptr; + if (source == nullptr || (source->Opcode() != IrOpcode::CompositeConstructU32x2 && source->Opcode() != IrOpcode::IAddCarry32)) return false; + return std::nullopt; + } + default: break; + } + const auto arguments = evaluatedArguments(value.Opcode()); + if (!arguments) return false; + if (value.ArgumentCount() < *arguments) return true; + return std::nullopt; +} + +} + +bool Evaluator::MayEvaluate(const IrResourcePlan& program, IrValue* value) { + std::unordered_map known; + const auto requirements = [&program](IrValue* current) { + std::vector required; + if (current->IsPhi()) { + required.push_back(ResolveInvariantPhi(program, current)); + return required; + } + const auto resolved = [](IrValue* value) { return value != nullptr ? value->Resolve() : nullptr; }; + if (current->Opcode() == IrOpcode::CompositeExtractU64) { + required.push_back(resolved(current->Argument(0))); + return required; + } + if (current->Opcode() == IrOpcode::CompositeExtractU32x2) { + auto* source = current->Argument(0)->Resolve(); + if (source->Opcode() == IrOpcode::CompositeConstructU32x2) { + required.push_back(resolved(source->Argument(current->Argument(1)->Resolve()->ImmediateU32()))); + } else { + required.push_back(resolved(source->Argument(0))); + required.push_back(resolved(source->Argument(1))); + } + return required; + } + const auto arguments = *evaluatedArguments(current->Opcode()); + for (std::size_t index = 0; index < arguments; ++index) { + auto* argument = current->Argument(index); + required.push_back(argument != nullptr ? argument->Resolve() : nullptr); + } + return required; + }; + std::vector> pending{{value->Resolve(), false}}; + while (!pending.empty()) { + const auto [current, expanded] = pending.back(); + pending.pop_back(); + if (known.contains(current)) continue; + if (const auto alone = evaluableAlone(program, *current)) { + known.emplace(current, *alone); + continue; + } + const auto required = requirements(current); + if (!expanded) { + pending.emplace_back(current, true); + for (auto* argument : required) { + if (argument != nullptr) pending.emplace_back(argument, false); + } + continue; + } + known.emplace(current, std::ranges::all_of(required, [&known](const IrValue* argument) { return argument != nullptr && known.at(argument); })); + } + return known.at(value->Resolve()); +} + bool Evaluator::EvaluateInst(IrValue& inst, std::uint64_t& result) { std::uint64_t a = 0; std::uint64_t b = 0; diff --git a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp index 5ca2dc0cc6..e1970d35c4 100644 --- a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp +++ b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp @@ -576,6 +576,7 @@ public: if (data) requireGroupedPatches(); for (IrValue* base : relative) base->ReplaceAllUsesWith(data ? &groupedPatch(*base) : &constant(0u)); deadCodeEliminator.Eliminate(program); + if (stage == IrShaderStage::Local && tessellation.drawParameters) writeInstance(); program.Metadata().tessellationLocalStride = localStride; program.Metadata().tessellationControlStride = controlStride; program.Metadata().tessellationPatchStride = patchStride; @@ -773,7 +774,9 @@ private: void requireGroupedPatches() const { if (stage == IrShaderStage::Local) Fail("the relative patch or vertex index is used outside a tessellation ring or LDS address", 0u); if (tessellation.primitiveGroupSize == 0u) Fail(stage == IrShaderStage::TessellationControl ? "the hull program uses its relative patch index as data, but GE_CNTL.PRIM_GRP_SIZE is 0 or not programmed" : "the domain program uses its relative patch index as data, but GE_CNTL.PRIM_GRP_SIZE is 0 or not programmed", 0u); - if (!tessellation.breakAtInstance) Fail(stage == IrShaderStage::TessellationControl ? "the hull program uses its relative patch index as data, but GE_CNTL.BREAK_WAVE_AT_EOI is clear, so the instances of a draw can share hull threadgroups" : "the domain program uses its relative patch index as data, but GE_CNTL.BREAK_WAVE_AT_EOI is clear, so the instances of a draw can share hull threadgroups", 0u); + if (tessellation.instancePatches == 0u || restartsPerInstance()) return; + if (stage == IrShaderStage::TessellationEvaluation) Fail("the domain program uses its relative patch index as data in an instanced draw whose instances share hull threadgroups (GE_CNTL.BREAK_WAVE_AT_EOI clear)", 0u); + if (!tessellation.drawParameters) Fail("the hull program uses its relative patch index as data in an instanced draw whose instances share hull threadgroups, which needs the device's shaderDrawParameters", 0u); } IrValue& groupedPatch(IrValue& base) { @@ -787,8 +790,14 @@ private: IrValue& count = constant(divisor); return emit(IrOpcode::ISub32, {&value, &emit(IrOpcode::IMul32, {&emit(IrOpcode::UDiv32, {&value, &count}), &count})}); }; - IrValue& primitive = emit(IrOpcode::GetBuiltin, {&constant(static_cast(StageInputKind::PrimitiveId)), &constant(0u)}); - return remainder(remainder(primitive, tessellation.primitiveGroupSize), tessellation.patchesPerGroup); + IrValue* primitive = &emit(IrOpcode::GetBuiltin, {&constant(static_cast(StageInputKind::PrimitiveId)), &constant(0u)}); + if (tessellation.instancePatches != 0u && !restartsPerInstance()) { + IrValue& always = program.CreateValue(IrOpcode::Void, IrType::Bool); + always.SetImmediateBool(true); + IrValue& instance = emit(IrOpcode::GetTessellationAttribute, {&constant(static_cast(TessellationAttribute::InstanceInput)), &constant(0u), &always}); + primitive = &emit(IrOpcode::IAdd32, {&emit(IrOpcode::IMul32, {&instance, &constant(tessellation.instancePatches)}), primitive}); + } + return remainder(remainder(*primitive, tessellation.primitiveGroupSize), tessellation.patchesPerGroup); } void removeUnobservedCycles() { @@ -833,6 +842,33 @@ private: return result; } + [[nodiscard]] bool restartsPerInstance() const { + const auto patches = tessellation.instancePatches; + const auto group = tessellation.primitiveGroupSize; + return group != 0u && (patches % group == 0u || (group % tessellation.patchesPerGroup == 0u && patches % tessellation.patchesPerGroup == 0u)); + } + + void writeInstance() { + IrBlock* entry = program.BlockOrder().front(); + IrValue* first = entry->Instructions().empty() ? nullptr : entry->Instructions().front(); + const auto place = [&](IrOpcode opcode, IrType type, std::initializer_list arguments) -> IrValue& { + IrValue& value = program.CreateValue(opcode, type); + for (IrValue* argument : arguments) value.AddArgument(argument); + if (first != nullptr) { + entry->InsertInstructionBefore(first, &value); + } else { + entry->AppendInstruction(&value); + } + return value; + }; + IrValue& index = place(IrOpcode::GetBuiltin, IrType::U32, {&constant(static_cast(StageInputKind::InstanceIndex)), &constant(0u)}); + IrValue& base = place(IrOpcode::GetBuiltin, IrType::U32, {&constant(static_cast(StageInputKind::BaseInstance)), &constant(0u)}); + IrValue& instance = place(IrOpcode::ISub32, IrType::U32, {&index, &base}); + IrValue& truth = program.CreateValue(IrOpcode::Void, IrType::Bool); + truth.SetImmediateBool(true); + static_cast(place(IrOpcode::SetTessellationAttribute, IrType::Void, {&constant(static_cast(TessellationAttribute::InstanceOutput)), &constant(0u), &instance, &truth})); + } + IrValue& insert(const Access& access, IrOpcode opcode, IrType type, std::initializer_list arguments) { IrValue& value = program.CreateValue(opcode, type); for (IrValue* argument : arguments) value.AddArgument(argument); diff --git a/core/shader/recompiler/PatchSpirvTools.cmake b/core/shader/recompiler/PatchSpirvTools.cmake index 2a6e17df0e..69cb4bcfc7 100644 --- a/core/shader/recompiler/PatchSpirvTools.cmake +++ b/core/shader/recompiler/PatchSpirvTools.cmake @@ -3,31 +3,101 @@ set(spirvToolsRedundancySource "${spirvToolsSourceDir}/redundancy_elimination.cp set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${spirvToolsRedundancySource}") file(READ "${spirvToolsRedundancySource}" spirvToolsRedundancy) +set(spirvToolsLocalRedundancySource "${spirvToolsSourceDir}/local_redundancy_elimination.cpp") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${spirvToolsLocalRedundancySource}") +file(READ "${spirvToolsLocalRedundancySource}" spirvToolsLocalRedundancy) +set(spirvToolsExpectedElimination [=[ + auto func = [this, &vnTable, &modified, value_to_ids](Instruction* inst) { + if (inst->result_id() == 0) { + return; + } + + uint32_t value = vnTable.GetValueNumber(inst); + + if (value == 0) { + return; + } + + auto candidate = value_to_ids->insert({value, inst->result_id()}); + if (!candidate.second) { + context()->KillNamesAndDecorates(inst); + context()->ReplaceAllUsesWith(inst->result_id(), candidate.first->second); + context()->KillInst(inst); + modified = true; + } + }; + block->ForEachInst(func); +]=]) +string(FIND "${spirvToolsLocalRedundancy}" "${spirvToolsExpectedElimination}" spirvToolsEliminationPosition) +if(spirvToolsEliminationPosition EQUAL -1) + message(FATAL_ERROR "SPIRV-Tools value elimination has changed; review the local traversal fix") +endif() + set(spirvToolsOldTraversal [=[ + struct State { + DominatorTreeNode* node; + std::map value_to_id_map; + }; + std::vector todo; + todo.push_back({bb, std::map()}); + bool modified = false; + for (size_t next_node = 0; next_node < todo.size(); next_node++) { + modified |= EliminateRedundanciesInBB(todo[next_node].node->bb_, vnTable, + &todo[next_node].value_to_id_map); for (DominatorTreeNode* child : todo[next_node].node->children_) { todo.push_back({child, todo[next_node].value_to_id_map}); } + } ]=]) set(spirvToolsNewTraversal [=[ - const auto& children = todo[next_node].node->children_; - for (size_t child_index = 0; child_index < children.size(); ++child_index) { - if (child_index + 1 == children.size()) { - todo.push_back({children[child_index], - std::move(todo[next_node].value_to_id_map)}); - } else { - todo.push_back({children[child_index], todo[next_node].value_to_id_map}); - } + struct State { + DominatorTreeNode* node; + size_t next_child; + size_t added_begin; + bool entered; + }; + std::map value_to_id_map; + std::vector added_values; + std::vector todo; + todo.push_back({bb, 0, 0, false}); + bool modified = false; + while (!todo.empty()) { + auto& state = todo.back(); + if (!state.entered) { + state.entered = true; + state.node->bb_->ForEachInst([&](Instruction* inst) { + if (inst->result_id() == 0) return; + const uint32_t value = vnTable.GetValueNumber(inst); + if (value == 0) return; + auto candidate = value_to_id_map.insert({value, inst->result_id()}); + if (candidate.second) { + added_values.push_back(value); + } else { + context()->KillNamesAndDecorates(inst); + context()->ReplaceAllUsesWith(inst->result_id(), candidate.first->second); + context()->KillInst(inst); + modified = true; + } + }); } - todo[next_node].value_to_id_map.clear(); + const auto& children = state.node->children_; + if (state.next_child != children.size()) { + auto* child = children[state.next_child++]; + todo.push_back({child, 0, added_values.size(), false}); + continue; + } + while (added_values.size() != state.added_begin) { + value_to_id_map.erase(added_values.back()); + added_values.pop_back(); + } + todo.pop_back(); + } ]=]) string(FIND "${spirvToolsRedundancy}" "${spirvToolsOldTraversal}" spirvToolsTraversalPosition) if(spirvToolsTraversalPosition EQUAL -1) message(FATAL_ERROR "SPIRV-Tools redundancy traversal has changed; review the local memory fix") endif() string(REPLACE "${spirvToolsOldTraversal}" "${spirvToolsNewTraversal}" spirvToolsRedundancy "${spirvToolsRedundancy}") -string(REPLACE "#include \"source/opt/redundancy_elimination.h\"\n" - "#include \"source/opt/redundancy_elimination.h\"\n\n#include \n" - spirvToolsRedundancy "${spirvToolsRedundancy}") set(spirvToolsPatchedDir "${CMAKE_BINARY_DIR}/3rdparty/SPIRV-Tools/patched") set(spirvToolsPatchedSource "${spirvToolsPatchedDir}/redundancy_elimination.cpp") diff --git a/core/shader/recompiler/PipelineSpecialization.hpp b/core/shader/recompiler/PipelineSpecialization.hpp index 071736b7a1..6e34cbab78 100644 --- a/core/shader/recompiler/PipelineSpecialization.hpp +++ b/core/shader/recompiler/PipelineSpecialization.hpp @@ -28,6 +28,7 @@ inline constexpr std::uint32_t ExportPackingBase = 8224u; inline constexpr std::uint32_t DualSourceBlend = 8232u; inline constexpr std::uint32_t RasterPrimitive = 8233u; inline constexpr std::uint32_t LayerExported = 8234u; +inline constexpr std::uint32_t StencilReferenceOffset = 8235u; inline constexpr std::uint32_t HeapCountBase = 8256u; inline constexpr std::uint32_t MipCountBase = 8320u; inline constexpr std::uint32_t DescriptorIndexBase = 16384u; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index d7574e7d41..326d926eca 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -1,4 +1,5 @@ #include "Recompiler.hpp" +#include "StencilReplay.hpp" #include #include #include @@ -108,7 +109,14 @@ RdnaProgram DecodeStageProgram(ShaderStageKind stage, std::spanmesh ? &*request.graphics->mesh : nullptr; - const auto* tessellation = request.graphics && request.graphics->tessellation ? &*request.graphics->tessellation : nullptr; + std::optional tessellationCopy; + if (request.graphics && request.graphics->tessellation) { + tessellationCopy = *request.graphics->tessellation; + tessellationCopy->instancePatches = TessellationInstancePatches(request); + const auto& capabilities = request.target.supportedCapabilities; + tessellationCopy->drawParameters = std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityDrawParameters)) != capabilities.end(); + } + const auto* tessellation = tessellationCopy ? &*tessellationCopy : nullptr; return BuildShaderStageInputInfo(toShaderStageKind(request.shader.stage), request.context, HostSubgroupSize(request), mesh, tessellation); } @@ -124,7 +132,7 @@ std::atomic frontEndBuilds{0}; struct PreparedControlFlow { PreparedControlFlow(const RecompileRequest& request, const SwappcInfo& swappcInfo) - : stage(request.shader.stage), swappc(swappcInfo), code(request.shader.code.begin(), request.shader.code.end()), + : stage(request.shader.stage), fallbacks(swappcInfo.linkedCallFallbacks.begin(), swappcInfo.linkedCallFallbacks.end()), swappc(WithFallbacks(swappcInfo, fallbacks)), code(request.shader.code.begin(), request.shader.code.end()), decoded(DecodeStageProgram(toShaderStageKind(request.shader.stage), code)), cfg(GraphBuilder{}.Build(decoded, &swappc)) { Structurizer{}.Structurize(cfg); } @@ -135,10 +143,16 @@ struct PreparedControlFlow { bool Matches(const RecompileRequest& request, const SwappcInfo& swappcInfo) const { return stage == request.shader.stage && swappc.fetchCallAllowed == swappcInfo.fetchCallAllowed && swappc.userDataBaseRegister == swappcInfo.userDataBaseRegister && swappc.userDataCount == swappcInfo.userDataCount && - std::ranges::equal(code, request.shader.code); + std::ranges::equal(fallbacks, swappcInfo.linkedCallFallbacks) && std::ranges::equal(code, request.shader.code); + } + + static SwappcInfo WithFallbacks(SwappcInfo info, const std::vector& fallbacks) { + info.linkedCallFallbacks = fallbacks; + return info; } ShaderStage stage; + std::vector fallbacks; SwappcInfo swappc; std::vector code; RdnaProgram decoded; @@ -147,7 +161,7 @@ struct PreparedControlFlow { std::shared_ptr ShaderPreparationContext::AcquireFrontend(const RecompileRequest& request) { const auto inputInfo = RequestInputInfo(request); - const SwappcInfo swappc{inputInfo.vertex != nullptr, request.context.userDataBaseRegister, static_cast(request.context.userData.size())}; + const SwappcInfo swappc{inputInfo.vertex != nullptr, request.context.userDataBaseRegister, static_cast(request.context.userData.size()), 0u, request.context.linkedCallFallbacks}; static const bool reuse = std::getenv("APS5_NO_PERF_FRONTEND_PAIR") == nullptr; if (reuse && frontend != nullptr && frontend->Matches(request, swappc)) { return frontend; @@ -310,6 +324,7 @@ struct SourceEntry : std::enable_shared_from_this { // accepted only when its code matches word for word. Owned here because the request's span // points into a registration the driver may replace while the entry lives on. std::vector code; + std::vector linkedCallFallbacks; std::shared_ptr plan; // A plan build that threw (an unsupported resource chain or control flow) is remembered and // rethrown: the front end ran every pass before failing, ~13 ms per dispatch of a shader the @@ -408,6 +423,7 @@ std::shared_ptr getSource(const RecompileRequest& request, ShaderPr if (source == nullptr) { source = std::make_shared(); source->code.assign(request.shader.code.begin(), request.shader.code.end()); + source->linkedCallFallbacks.assign(request.context.linkedCallFallbacks.begin(), request.context.linkedCallFallbacks.end()); auto& bucket = sources[key]; if (!bucket.empty()) { // A second entry under one key is a code hash collision (or the unhashed key with @@ -492,7 +508,8 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) constexpr BindingAllocator bindingAllocator; bool registration = request.target.registrationModules && (request.shader.stage == ShaderStage::Vertex || request.shader.stage == ShaderStage::Fragment); program.Metadata().bindingLayoutComplete = false; - auto bindings = bindingAllocator.Allocate(program, request.layout, registration); + const bool stencilReplay = request.shader.stage == ShaderStage::Fragment && std::ranges::any_of(program.Info().outputs, [](const StageOutput& output) { return output.kind == StageOutputKind::StencilRef; }) && std::ranges::find(request.target.supportedCapabilities, static_cast(spv::CapabilityStencilExportEXT)) == request.target.supportedCapabilities.end(); + auto bindings = bindingAllocator.Allocate(program, request.layout, registration, stencilReplay); SpirvTargetOptions targetOptions {}; targetOptions.vulkanVersion = request.target.vulkanVersion; @@ -515,11 +532,16 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) if (registration && result.spirv.size() > request.target.registrationWordLimit) { registration = false; program.Metadata().bindingLayoutComplete = false; - bindings = bindingAllocator.Allocate(program, request.layout, false); + bindings = bindingAllocator.Allocate(program, request.layout, false, stencilReplay); targetOptions.registrationModules = false; result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); } result.registrationModule = registration; + result.stencilReplaySafe = (request.shader.stage == ShaderStage::Vertex || stencilReplay) && StencilReplaySafe(program); + if (stencilReplay) { + result.stencilReferences = StencilReferenceValues(program); + result.stencilReferenceDword = bindings.layout.stencilReferenceDword; + } result.bdaAbiVersion = program.Info().usesDma || program.Info().usesFaultBuffer ? request.target.bdaAbiVersion : 0u; result.memoryOffsetDword = bindings.layout.memoryOffsetDword; @@ -599,15 +621,24 @@ std::shared_ptr buildSpecializedModule(const CompiledSh } std::map values; std::set specializedIds; + std::set pushTypes; + std::optional replaySlotBytes; const auto& words = source.Words(); if (words.size() < 5u || words[0] != spv::MagicNumber) throw std::runtime_error("invalid prepared specialization module"); for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; const auto op = static_cast(words[cursor] & 0xffffu); if (count == 0u || count > words.size() - cursor) throw std::runtime_error("truncated prepared specialization instruction"); + if (!artifact.stencilReferences.empty() && op == spv::OpTypePointer && count == 4u && words[cursor + 2u] == spv::StorageClassPushConstant) pushTypes.insert(words[cursor + 3u]); if (op == spv::OpDecorate && count == 4u && words[cursor + 2u] == spv::DecorationSpecId) { const auto found = supplied.find(words[cursor + 3u]); - if (found == supplied.end() || !values.emplace(words[cursor + 1u], found->second).second) throw std::runtime_error("missing or duplicate prepared specialization value"); + if (found == supplied.end()) throw std::runtime_error("missing prepared specialization value"); + auto value = found->second; + if (!artifact.stencilReferences.empty()) { + if (found->first == PipelineSpecialization::StencilReferenceOffset) replaySlotBytes = value / PushData::DwordCount * NativePushSlotSize; + if (found->first == PipelineSpecialization::StencilReferenceOffset || found->first == PipelineSpecialization::PushDataOffset) value %= PushData::DwordCount; + } + if (!values.emplace(words[cursor + 1u], value).second) throw std::runtime_error("duplicate prepared specialization value"); specializedIds.insert(found->first); } cursor += count; @@ -619,6 +650,10 @@ std::shared_ptr buildSpecializedModule(const CompiledSh if (op == spv::OpSpecConstant) { if (count != 4u || !values.contains(words[cursor + 2u])) throw std::runtime_error("invalid prepared specialization constant"); materialized.insert(materialized.end(), {(4u << 16u) | spv::OpConstant, words[cursor + 1u], words[cursor + 2u], values.at(words[cursor + 2u])}); + } else if (op == spv::OpMemberDecorate && count == 5u && words[cursor + 3u] == spv::DecorationOffset && pushTypes.contains(words[cursor + 1u])) { + if (!replaySlotBytes || words[cursor + 2u] != 0u || *replaySlotBytes >= NativePushConstantSize) throw std::runtime_error("invalid stencil replay push constant slot"); + materialized.insert(materialized.end(), words.begin() + cursor, words.begin() + cursor + count - 1u); + materialized.push_back(*replaySlotBytes); } else if (!(op == spv::OpDecorate && count == 4u && words[cursor + 2u] == spv::DecorationSpecId)) { materialized.insert(materialized.end(), words.begin() + cursor, words.begin() + cursor + count); } @@ -833,6 +868,9 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil .parameterExports = artifact.parameterExports, .fragmentParameters = artifact.fragmentParameters, .barycentricEmulation = artifact.barycentricEmulation, + .stencilReferences = artifact.stencilReferences, + .stencilReplaySafe = artifact.stencilReplaySafe, + .stencilReferenceDword = artifact.stencilReferenceDword, .variantId = artifact.variantId }; const auto plan = preparedBindingPlan(variant, snapshot, request.context.pixel ? std::span(request.context.pixel->targetExportMapping) : std::span{}, request.target.registrationModules); @@ -842,6 +880,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil auto& moduleKey = HostThreadLocal, LocalModuleKeyStorage>(); moduleKey.clear(); if (variant.bindings.layout.UsesPushData()) moduleKey.push_back(request.layout.pushConstantOffsetBytes / 4u); + if (!artifact.stencilReferences.empty()) moduleKey.push_back(request.layout.pushConstantOffsetBytes / 4u + artifact.stencilReferenceDword); if (request.context.pixel) { for (const auto packing : request.context.pixel->targetExportPacking) moduleKey.push_back(static_cast(packing)); moduleKey.push_back(request.context.pixel->dualSourceBlend ? 1u : 0u); @@ -890,6 +929,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil auto constants = plan->bindings.specialization; std::size_t index = 0; if (variant.bindings.layout.UsesPushData()) constants.push_back({PipelineSpecialization::PushDataOffset, moduleKey[index++]}); + if (!artifact.stencilReferences.empty()) constants.push_back({PipelineSpecialization::StencilReferenceOffset, moduleKey[index++]}); if (request.context.pixel) { for (std::uint32_t target = 0; target < request.context.pixel->targetExportPacking.size(); ++target) constants.push_back({PipelineSpecialization::ExportPackingBase + target, moduleKey[index++]}); constants.push_back({PipelineSpecialization::DualSourceBlend, moduleKey[index++]}); @@ -933,6 +973,10 @@ 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); + if (!artifact.stencilReferences.empty()) { + if (artifact.stencilReferenceDword * sizeof(std::uint32_t) != result.pushConstants.size() || result.pushConstants.size() + sizeof(std::uint32_t) > request.layout.pushConstantSizeBytes) throw std::runtime_error("stencil replay push constant lies outside the stage range"); + result.pushConstants.resize(result.pushConstants.size() + sizeof(std::uint32_t)); + } const auto& layout = variant.bindings.layout; if (artifact.registrationModule && request.shader.stage != ShaderStage::Fragment && layout.runtimeStateDwords != 0u) { const auto first = layout.RuntimeStateDword() + RuntimeAbi::StateVertexSelectors; @@ -1354,6 +1398,7 @@ bool MatchesPreparedShader(const RecompileRequest& request, const SourceHandle& const auto words = prepared.ShaderDataDwords(); const bool usesPush = prepared.runtimeImageCount == 0u && words != 0u && words <= layout.pushConstantSizeBytes / 4u; if (prepared.UsesPushData() != usesPush) return false; + if (!handle.artifact->artifact.stencilReferences.empty() && (prepared.stencilReferenceDword >= layout.pushConstantSizeBytes / 4u || prepared.stencilReferenceDword != (usesPush ? words : 0u))) return false; if (!std::ranges::equal(key, handle.staticKey) || handle.source->code.size() != request.shader.code.size()) return false; return handle.source->code.data() == request.shader.code.data() || std::ranges::equal(handle.source->code, request.shader.code); } @@ -1363,7 +1408,9 @@ std::span GetPreparedCode(const SourceHandle& handle) { return handle.source->code; } -PreparedShaderInvocation::PreparedShaderInvocation(const RecompileRequest& request, const std::shared_ptr& handle) : request(request), handle(handle) {} +PreparedShaderInvocation::PreparedShaderInvocation(const RecompileRequest& request, const std::shared_ptr& handle) : request(request), handle(handle) { + this->request.context.linkedCallFallbacks = handle->source->linkedCallFallbacks; +} std::optional PreparedShaderInvocation::TryCreate(const RecompileRequest& request, const std::shared_ptr& handle) { struct PreparedKeyStorage {}; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 8e77c62bd0..5ddc9dfb7d 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -133,6 +133,7 @@ struct ShaderPixelStageInfo { bool posZ; bool posW; bool frontFace; + bool frontFaceAllBits; bool ancillary; bool sampleShading; bool noPerspective; @@ -153,6 +154,8 @@ struct ShaderPixelStageInfo { RasterPrimitive rasterPrimitive = RasterPrimitive::Triangle; bool layerExported = false; bool ditheredAlphaToCoverage = false; + std::uint8_t quadPixelMask = 0xf; + std::array quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; }; struct ShaderVertexBufferResource { @@ -200,6 +203,7 @@ struct GuestContext { std::optional vertex; std::span memory; std::optional floatMode; + std::span linkedCallFallbacks; }; struct MeshTargetLimits { @@ -293,6 +297,8 @@ struct TessellationConfiguration { std::uint32_t patchesPerGroup = 256; std::uint32_t primitiveGroupSize = 0; bool breakAtInstance = false; + std::uint32_t instancePatches = 0; + bool drawParameters = false; }; inline constexpr std::uint32_t MeshDrawPushOffsetBytes = 104; @@ -333,6 +339,11 @@ struct RecompileRequest { bool useCache = true; }; +inline std::uint32_t TessellationInstancePatches(const RecompileRequest& request) { + if (!request.graphics || !request.graphics->tessellation || request.graphics->draw.instanceCount <= 1u || request.graphics->tessellation->breakAtInstance) return 0u; + return request.graphics->draw.indexCount / request.graphics->tessellation->inputControlPoints; +} + enum class DescriptorKind { UniformBuffer, StorageBuffer, @@ -349,7 +360,8 @@ enum class DescriptorImageShape { Image2DArray, ImageCube, Image3D, - Image1DArray + Image1DArray, + Image2DMultisample }; enum class DescriptorRole { @@ -527,6 +539,9 @@ struct CompiledShaderArtifact { std::vector parameterExports; std::vector fragmentParameters; BarycentricEmulation barycentricEmulation; + std::vector stencilReferences; + bool stencilReplaySafe = false; + std::uint32_t stencilReferenceDword = 0xffffffffu; std::uint64_t variantId = 0; }; diff --git a/core/shader/recompiler/ShaderCachePack.cpp b/core/shader/recompiler/ShaderCachePack.cpp new file mode 100644 index 0000000000..a9550483f5 --- /dev/null +++ b/core/shader/recompiler/ShaderCachePack.cpp @@ -0,0 +1,115 @@ +#include "ShaderCachePack.hpp" +#include "ShaderCacheDirectory.hpp" +#include +#include +#include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#else +#include +#include +#include +#include +#endif + +namespace ShaderRecompiler::ShaderDiskCache { +namespace { +using Clock = std::chrono::steady_clock; +std::uint64_t elapsed(Clock::time_point start) { + return std::chrono::duration_cast(Clock::now() - start).count(); +} +} + +std::array PackKeyHash(std::span key) { + return {HashBytes(key, 0x5eed0001ull), HashBytes(key, 0x5eed0002ull)}; +} + +std::vector PrebuiltCachePaths(const std::filesystem::path& cacheRoot) { + if (cacheRoot.empty()) return {}; + return {cacheRoot / "prebuilt"}; +} + +CachePack::CachePack(const std::filesystem::path& path) { + static_assert(std::endian::native == std::endian::little); +#ifdef _WIN32 + const auto handle = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (handle == INVALID_HANDLE_VALUE) { error = "cannot open"; return; } + LARGE_INTEGER size{}; + if (!GetFileSizeEx(handle, &size) || size.QuadPart < static_cast(sizeof(PackHeader)) || static_cast(size.QuadPart) > std::numeric_limits::max()) { + CloseHandle(handle); error = "invalid size"; return; + } + mapping = CreateFileMappingW(handle, nullptr, PAGE_READONLY, 0, 0, nullptr); + fileHandle = handle; + if (!mapping) { error = "cannot map"; return; } + const auto* data = static_cast(MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0)); + if (!data) { error = "cannot map view"; return; } + file = {data, static_cast(size.QuadPart)}; +#else + const auto fd = open(path.c_str(), O_RDONLY | O_CLOEXEC); + if (fd < 0) { error = "cannot open"; return; } + struct stat size{}; + if (fstat(fd, &size) != 0 || size.st_size < static_cast(sizeof(PackHeader)) || static_cast(size.st_size) > std::numeric_limits::max()) { + close(fd); error = "invalid size"; return; + } + auto* data = mmap(nullptr, size.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + if (data == MAP_FAILED) { error = "cannot map"; return; } + file = {static_cast(data), static_cast(size.st_size)}; +#endif + Validate(); +} + +CachePack::~CachePack() { +#ifdef _WIN32 + if (!file.empty()) UnmapViewOfFile(file.data()); + if (mapping) CloseHandle(mapping); + if (fileHandle) CloseHandle(fileHandle); +#else + if (!file.empty()) munmap(const_cast(file.data()), file.size()); +#endif +} + +void CachePack::Validate() { + PackHeader header; + std::memcpy(&header, file.data(), sizeof(header)); + if (header.magic != PackMagic) { error = "unknown pack format"; return; } + if (header.sourceVersion != SourceVersion()) { error = "different compiler source version"; return; } + if (header.fileBytes != file.size() || header.entryCount > (file.size() - sizeof(header)) / sizeof(PackRecord)) { error = "truncated index"; return; } + const auto indexBytes = static_cast(header.entryCount) * sizeof(PackRecord); + const auto index = file.subspan(sizeof(header), indexBytes); + if (HashBytes(index) != header.indexHash) { error = "index checksum mismatch"; return; } + const std::span checked(reinterpret_cast(index.data()), header.entryCount); + std::array previous{}; + for (const auto& record : checked) { + if (record.hash < previous || record.offset < sizeof(header) + indexBytes || record.offset > file.size() || record.size > file.size() - record.offset || record.size < 48) { error = "invalid index record"; return; } + previous = record.hash; + } + records = checked; +} + +LoadStatus CachePack::Load(std::span key, CompiledVariant& variant, PackLoadTimings& timings) const { + const auto start = Clock::now(); + const auto hash = PackKeyHash(key); + const auto first = std::lower_bound(records.begin(), records.end(), hash, [](const auto& record, const auto& value) { return record.hash < value; }); + timings.lookupNanoseconds += elapsed(start); + auto status = LoadStatus::Absent; + for (auto it = first; it != records.end() && it->hash == hash; ++it) { + const auto data = file.subspan(it->offset, it->size); + const auto decode = Clock::now(); + const auto result = DecodeEntry(data, key, variant); + timings.decodeNanoseconds += elapsed(decode); + timings.bytes += data.size(); + if (result == LoadStatus::Loaded) return result; + if (result == LoadStatus::Rejected) status = result; + } + return status; +} +} diff --git a/core/shader/recompiler/ShaderCachePack.hpp b/core/shader/recompiler/ShaderCachePack.hpp new file mode 100644 index 0000000000..24aff33610 --- /dev/null +++ b/core/shader/recompiler/ShaderCachePack.hpp @@ -0,0 +1,51 @@ +#pragma once +#include "ShaderDiskCache.hpp" +#include + +namespace ShaderRecompiler::ShaderDiskCache { + +inline constexpr std::uint64_t PackMagic = 0x314b434150535041ull; +struct PackHeader { + std::uint64_t magic = PackMagic; + std::uint64_t sourceVersion = 0; + std::uint64_t fileBytes = 0; + std::uint64_t entryCount = 0; + std::uint64_t indexHash = 0; +}; +struct PackRecord { + std::array hash{}; + std::uint64_t offset = 0; + std::uint64_t size = 0; +}; +static_assert(sizeof(PackHeader) == 40 && sizeof(PackRecord) == 32); + +struct PackLoadTimings { + std::uint64_t lookupNanoseconds = 0; + std::uint64_t decodeNanoseconds = 0; + std::uint64_t bytes = 0; +}; + +[[nodiscard]] std::array PackKeyHash(std::span key); +[[nodiscard]] std::vector PrebuiltCachePaths(const std::filesystem::path& cacheRoot); + +class CachePack { +public: + explicit CachePack(const std::filesystem::path& path); + ~CachePack(); + CachePack(const CachePack&) = delete; + CachePack& operator=(const CachePack&) = delete; + [[nodiscard]] bool Valid() const { return error.empty(); } + [[nodiscard]] const std::string& Error() const { return error; } + [[nodiscard]] std::size_t Count() const { return records.size(); } + [[nodiscard]] LoadStatus Load(std::span key, CompiledVariant& variant, PackLoadTimings& timings) const; + +private: + void Validate(); + std::span file; + std::span records; + std::string error; + void* mapping = nullptr; + void* fileHandle = nullptr; +}; + +} diff --git a/core/shader/recompiler/ShaderCacheSourceVersion.hpp b/core/shader/recompiler/ShaderCacheSourceVersion.hpp new file mode 100644 index 0000000000..d73de7db7f --- /dev/null +++ b/core/shader/recompiler/ShaderCacheSourceVersion.hpp @@ -0,0 +1,15 @@ +#ifndef CORE_SHADER_RECOMPILER_SHADERCACHESOURCEVERSION_HPP +#define CORE_SHADER_RECOMPILER_SHADERCACHESOURCEVERSION_HPP + +#include +#include +#include + +namespace ShaderRecompiler::ShaderDiskCache::Generated { + +extern const std::uint64_t SourceVersion; +extern const std::span RecompilerSwitches; + +} + +#endif diff --git a/core/shader/recompiler/ShaderCacheVersion.cmake b/core/shader/recompiler/ShaderCacheVersion.cmake index e952236aef..81be546106 100644 --- a/core/shader/recompiler/ShaderCacheVersion.cmake +++ b/core/shader/recompiler/ShaderCacheVersion.cmake @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.20) -file(STRINGS "${LIST_FILE}" entries) +file(STRINGS "${LIST_FILE}" entries ENCODING UTF-8) set(sources) set(guard) set(submodules) @@ -46,7 +46,7 @@ foreach(source IN LISTS sources) if(NOT source MATCHES "\\.(cpp|hpp|h)$") continue() endif() - file(STRINGS "${source}" lines REGEX "\"APS5_[A-Z0-9_]+\"") + file(STRINGS "${source}" lines ENCODING UTF-8 REGEX "\"APS5_[A-Z0-9_]+\"") foreach(line IN LISTS lines) string(REGEX MATCHALL "\"APS5_[A-Z0-9_]+\"" names "${line}") list(APPEND switches ${names}) @@ -56,7 +56,7 @@ foreach(source IN LISTS sources) continue() endif() get_filename_component(sourceDirectory "${source}" DIRECTORY) - file(STRINGS "${source}" lines REGEX "^[ \t]*#[ \t]*include[ \t]*\"[^\"]+\"") + file(STRINGS "${source}" lines ENCODING UTF-8 REGEX "^[ \t]*#[ \t]*include[ \t]*\"[^\"]+\"") foreach(line IN LISTS lines) string(REGEX REPLACE "^[ \t]*#[ \t]*include[ \t]*\"([^\"]+)\".*$" "\\1" included "${line}") set(resolved) @@ -109,7 +109,7 @@ foreach(source IN LISTS guard) continue() endif() file(RELATIVE_PATH relative "${ROOT}" "${source}") - file(STRINGS "${source}" lines REGEX "ShaderRecompiler|setenv|putenv") + file(STRINGS "${source}" lines ENCODING UTF-8 REGEX "ShaderRecompiler|setenv|putenv") foreach(line IN LISTS lines) if(line MATCHES "using[ \t]+namespace[ \t]+ShaderRecompiler" OR line MATCHES "namespace[ \t]+[A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*(::)?ShaderRecompiler[ \t]*;") list(APPEND failures "${relative} brings the ShaderRecompiler namespace in whole, which hides its calls from the shader disk cache key review: qualify them") @@ -140,23 +140,22 @@ list(REMOVE_DUPLICATES switches) list(SORT switches) list(JOIN switches ",\n " switchList) -set(content "#ifndef CORE_SHADER_RECOMPILER_SHADERCACHEVERSION_HPP -#define CORE_SHADER_RECOMPILER_SHADERCACHEVERSION_HPP - -#include -#include - +set(content "#include \"ShaderCacheSourceVersion.hpp\"\n") +string(APPEND content " namespace ShaderRecompiler::ShaderDiskCache::Generated { -inline constexpr std::uint64_t SourceVersion = 0x${version}ull; +namespace { -inline constexpr std::string_view RecompilerSwitches[] = { +constexpr std::string_view recompilerSwitches[] = { ${switchList} }; } -#endif +constinit const std::uint64_t SourceVersion = 0x${version}ull; +constinit const std::span RecompilerSwitches = recompilerSwitches; + +} ") if(EXISTS "${OUTPUT}") diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index ede17b76e5..c0a118aeb8 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -1,5 +1,6 @@ #include "ShaderDiskCache.hpp" #include "CacheKey.hpp" +#include "ShaderCachePack.hpp" #include "ShaderCacheDirectory.hpp" #include "ThreadOwned.hpp" #include "Optimization/ResourceMaterializer.hpp" @@ -20,7 +21,7 @@ #include #include #endif -#include "ShaderCacheVersion.hpp" +#include "ShaderCacheSourceVersion.hpp" #include #include #include @@ -83,14 +84,14 @@ std::filesystem::path ShaderCacheDirectory() { namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(CompiledShaderArtifact) == 232, "CompiledShaderArtifact changed: update the artifact encoder"); +static_assert(sizeof(CompiledShaderArtifact) == 264, "CompiledShaderArtifact changed: update the artifact encoder"); static_assert(sizeof(ShaderInvocation) == 200, "ShaderInvocation changed: update the invocation encoder"); -static_assert(sizeof(RecompileResult) == 440, "RecompileResult changed: update EncodeResult and DecodeResult"); +static_assert(sizeof(RecompileResult) == 472, "RecompileResult changed: update EncodeResult and DecodeResult"); static_assert(sizeof(DescriptorBinding) == 592, "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"); static_assert(sizeof(FragmentParameter) == 12, "FragmentParameter changed: update the parameter encoder"); -static_assert(sizeof(CompiledShaderInfo) == 344, "CompiledShaderInfo changed: update the info encoder"); +static_assert(sizeof(CompiledShaderInfo) == 352, "CompiledShaderInfo changed: update the info encoder"); static_assert(sizeof(ShaderInfo) == 232, "ShaderInfo changed: update the info encoder"); static_assert(sizeof(BufferResource) == 32, "BufferResource changed: update the info encoder"); static_assert(sizeof(ImageResource) == 88, "ImageResource changed: update the info encoder"); @@ -98,17 +99,17 @@ static_assert(sizeof(SamplerResource) == 24, "SamplerResource changed: update th static_assert(sizeof(SampledResourcePair) == 12, "SampledResourcePair changed: update the info encoder"); static_assert(sizeof(StageInput) == 56, "StageInput changed: update the info encoder"); static_assert(sizeof(StageOutput) == 48, "StageOutput changed: update the info encoder"); -static_assert(sizeof(IrBindingLayout) == 72, "IrBindingLayout changed: update the layout encoder"); +static_assert(sizeof(IrBindingLayout) == 80, "IrBindingLayout changed: update the layout encoder"); static_assert(sizeof(IrDescriptorBinding) == 32, "IrDescriptorBinding changed: update the layout encoder"); -static_assert(sizeof(BindingAllocationResult) == 152, "BindingAllocationResult changed: update the allocation encoder"); -static_assert(sizeof(CompiledBindingLayout) == 80, "CompiledBindingLayout changed: update the allocation encoder"); +static_assert(sizeof(BindingAllocationResult) == 160, "BindingAllocationResult changed: update the allocation encoder"); +static_assert(sizeof(CompiledBindingLayout) == 88, "CompiledBindingLayout changed: update the allocation encoder"); static_assert(sizeof(BindingLayout) == 16, "BindingLayout changed: update BuildKey"); static_assert(sizeof(IrResourcePlan) == 584, "IrResourcePlan changed: update EncodePlan and DecodePlan"); static_assert(sizeof(IrValue) == 152, "IrValue changed: update the plan value encoder"); static_assert(sizeof(GuestRegister) == 8, "GuestRegister changed: update the plan value encoder"); static_assert(sizeof(MemoryInfo) == 80, "MemoryInfo changed: update the plan encoder"); -static_assert(sizeof(DescriptorSource) == 144, "DescriptorSource changed: update the plan encoder"); -static_assert(sizeof(TableColumn) == 64, "TableColumn changed: update the plan encoder"); +static_assert(sizeof(DescriptorSource) == 160, "DescriptorSource changed: update the plan encoder"); +static_assert(sizeof(TableColumn) == 80, "TableColumn changed: update the plan encoder"); static_assert(sizeof(KeyDomain) == 16, "KeyDomain changed: update the plan encoder"); static_assert(sizeof(ResourceBlock) == 80, "ResourceBlock changed: update the plan encoder"); static_assert(sizeof(SrtRead) == 16, "SrtRead changed: update the plan encoder"); @@ -139,16 +140,16 @@ constexpr std::size_t fieldCount() { static_assert(fieldCount() == 23, "ShaderInfo changed: update the info encoder"); static_assert(fieldCount() == 12, "BufferResource changed: update the info encoder"); -static_assert(fieldCount() == 35, "ImageResource changed: update the info encoder"); +static_assert(fieldCount() == 37, "ImageResource changed: update the info encoder"); static_assert(fieldCount() == 8, "SamplerResource changed: update the info encoder"); static_assert(fieldCount() == 3, "SampledResourcePair changed: update the info encoder"); static_assert(fieldCount() == 5, "StageInput changed: update the info encoder"); static_assert(fieldCount() == 4, "StageOutput changed: update the info encoder"); static_assert(fieldCount() == 21, "IrResourcePlan changed: update EncodePlan and DecodePlan"); static_assert(fieldCount() == 2, "GuestRegister changed: update the plan value encoder"); -static_assert(fieldCount() == 30, "MemoryInfo changed: update the plan encoder"); +static_assert(fieldCount() == 31, "MemoryInfo changed: update the plan encoder"); static_assert(fieldCount() == 3, "DescriptorSource changed: update the plan encoder"); -static_assert(fieldCount() == 10, "TableColumn changed: update the plan encoder"); +static_assert(fieldCount() == 15, "TableColumn changed: update the plan encoder"); static_assert(fieldCount() == 4, "KeyDomain changed: update the plan encoder"); static_assert(fieldCount() == 4, "ResourceBlock changed: update the plan encoder"); static_assert(fieldCount() == 2, "SrtRead changed: update the plan encoder"); @@ -497,6 +498,9 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { writer.Value(result.barycentricEmulation.active); writer.Value(result.barycentricEmulation.smooth); writer.Value(result.barycentricEmulation.linear); + writer.Values(std::span(result.stencilReferences)); + writer.Value(result.stencilReplaySafe); + writer.Value(result.stencilReferenceDword); } void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { @@ -539,6 +543,9 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { reader.Value(result.barycentricEmulation.active); reader.Value(result.barycentricEmulation.smooth); reader.Value(result.barycentricEmulation.linear); + reader.Values(result.stencilReferences); + reader.Value(result.stencilReplaySafe); + reader.Value(result.stencilReferenceDword); result.variantId = 0; } @@ -619,6 +626,7 @@ void decodeInvocation(Reader& reader, ShaderInvocation& invocation) { void encodeLayout(Writer& writer, const IrBindingLayout& layout) { writer.Value(layout.pushDataStartDword); + writer.Value(layout.stencilReferenceDword); writer.Value(layout.memoryOffsetDword); writer.Value(layout.memoryOffsetCount); writer.Value(layout.dispatchThreadLimit); @@ -633,6 +641,7 @@ void encodeLayout(Writer& writer, const IrBindingLayout& layout) { void decodeLayout(Reader& reader, IrBindingLayout& layout) { reader.Value(layout.pushDataStartDword); + reader.Value(layout.stencilReferenceDword); reader.Value(layout.memoryOffsetDword); reader.Value(layout.memoryOffsetCount); reader.Value(layout.dispatchThreadLimit); @@ -690,7 +699,9 @@ void encodeShaderInfo(Writer& writer, const ShaderInfo& info) { out.Value(image.constantSwizzle); out.Value(image.constantSwizzleCompatible); out.Value(image.flatVolumeCompatible); + out.Value(image.singleMipPlaneBiasCompatible); out.Value(image.flatLineCompatible); + out.Value(image.flatVolumeLineCompatible); out.Value(image.foldTexelOffsetsOnly); out.Value(image.byElements); out.Value(image.byComponents); @@ -761,7 +772,7 @@ void decodeShaderInfo(Reader& reader, ShaderInfo& info) { in.Value(buffer.scalar); in.Value(buffer.typedAlignment); }); - reader.List(info.images, 75, [](Reader& in, ImageResource& image) { + reader.List(info.images, 76, [](Reader& in, ImageResource& image) { in.Value(image.source); in.Value(image.firstUsePc); in.Value(image.resourceClass); @@ -789,7 +800,9 @@ void decodeShaderInfo(Reader& reader, ShaderInfo& info) { in.Value(image.constantSwizzle); in.Value(image.constantSwizzleCompatible); in.Value(image.flatVolumeCompatible); + in.Value(image.singleMipPlaneBiasCompatible); in.Value(image.flatLineCompatible); + in.Value(image.flatVolumeLineCompatible); in.Value(image.foldTexelOffsetsOnly); in.Value(image.byElements); in.Value(image.byComponents); @@ -915,6 +928,7 @@ void encodeMemoryInfo(Writer& out, const MemoryInfo& memory) { out.Value(memory.coherent); out.Value(memory.gpuDescriptor); out.Value(memory.flushDenormals); + out.Value(memory.flushDenormalResults); } void decodeMemoryInfo(Reader& in, MemoryInfo& memory) { @@ -948,17 +962,18 @@ void decodeMemoryInfo(Reader& in, MemoryInfo& memory) { in.Value(memory.coherent); in.Value(memory.gpuDescriptor); in.Value(memory.flushDenormals); + in.Value(memory.flushDenormalResults); } bool encodePlan(Writer& writer, const IrResourcePlan& plan) { - if (std::ranges::any_of(plan.valueStorage, [](const auto& value) { return value == nullptr; }) || std::ranges::any_of(plan.blockStorage, [](const auto& block) { return block == nullptr; })) return false; + if (std::ranges::any_of(plan.blockStorage, [](const auto& block) { return block == nullptr; })) return false; std::unordered_map values; std::unordered_map blocks; - values.reserve(plan.valueStorage.size()); + values.reserve(plan.valueStorage.Size()); blocks.reserve(plan.blockStorage.size()); - for (std::size_t index = 0; index < plan.valueStorage.size(); ++index) values.emplace(plan.valueStorage[index].get(), static_cast(index)); + for (std::size_t index = 0; index < plan.valueStorage.Size(); ++index) values.emplace(&plan.valueStorage[index], static_cast(index)); for (std::size_t index = 0; index < plan.blockStorage.size(); ++index) blocks.emplace(plan.blockStorage[index].get(), static_cast(index)); - bool owned = values.size() == plan.valueStorage.size() && blocks.size() == plan.blockStorage.size(); + bool owned = values.size() == plan.valueStorage.Size() && blocks.size() == plan.blockStorage.size(); std::uint32_t current = 0; const auto valueIndex = [&](const IrValue* value) { if (value == nullptr) return NoPlanIndex; @@ -975,29 +990,31 @@ bool encodePlan(Writer& writer, const IrResourcePlan& plan) { writer.Value(plan.userDataCount); writer.Value(plan.srgbDecodeFormats); writer.List(plan.blockStorage, [](Writer& out, const std::unique_ptr& block) { out.Value(block->Id()); }); - writer.List(plan.valueStorage, [&](Writer& out, const std::unique_ptr& value) { + writer.Value(plan.valueStorage.Size()); + for (const auto& stored : plan.valueStorage) { + const auto* value = &stored; const auto bank = static_cast(value->Register().bank); if (bank > 0xfu) owned = false; - out.Compact(static_cast(value->Opcode())); - out.Compact(static_cast(value->Type())); - out.Compact(value->Id()); - out.Compact(value->Flags()); - out.Compact((static_cast(value->Register().index) << 4u) | (bank & 0xfu)); - out.Compact((static_cast(value->ArgumentCount()) << 1u) | (value->HasImmediate() ? 1u : 0u)); - if (value->HasImmediate()) out.Compact(value->ImmediateU64()); + writer.Compact(static_cast(value->Opcode())); + writer.Compact(static_cast(value->Type())); + writer.Compact(value->Id()); + writer.Compact(value->Flags()); + writer.Compact((static_cast(value->Register().index) << 4u) | (bank & 0xfu)); + writer.Compact((static_cast(value->ArgumentCount()) << 1u) | (value->HasImmediate() ? 1u : 0u)); + if (value->HasImmediate()) writer.Compact(value->ImmediateU64()); for (const auto* argument : value->Arguments()) { const auto index = valueIndex(argument); const auto delta = static_cast(index) - static_cast(current); - out.Compact(argument == nullptr ? 0u : ((static_cast(delta) << 1u) ^ static_cast(delta >> 63)) + 1u); + writer.Compact(argument == nullptr ? 0u : ((static_cast(delta) << 1u) ^ static_cast(delta >> 63)) + 1u); } if (value->IsPhi() && value->PhiBlockCount() != value->ArgumentCount()) owned = false; for (std::size_t operand = 0; value->IsPhi() && operand < value->ArgumentCount(); ++operand) { const auto found = operand < value->PhiBlockCount() ? blocks.find(value->PhiBlock(operand)) : blocks.end(); if (found == blocks.end()) owned = false; - out.Compact(found != blocks.end() ? found->second : 0u); + writer.Compact(found != blocks.end() ? found->second : 0u); } ++current; - }); + } writer.List(plan.memoryInfo, encodeMemoryInfo); writer.List(plan.descriptorSources, [&](Writer& out, const DescriptorSource& source) { for (const auto* dword : source.dwords) out.Value(valueIndex(dword)); @@ -1019,6 +1036,11 @@ bool encodePlan(Writer& writer, const IrResourcePlan& plan) { domains.Value(domain.value); }); out.Value(column.unkeyed); + out.Value(column.indirect); + out.Value(column.indirectOffset); + out.Value(column.indirectImmediate); + out.Value(column.foldedAddend); + out.Value(column.foldedCarry); }); writer.List(plan.controlFlow, [&](Writer& out, const ResourceBlock& block) { out.Value(valueIndex(block.condition)); @@ -1119,36 +1141,35 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { if (!reader.Ok()) return false; plan.blockStorage.reserve(blockIds.size()); for (const auto id : blockIds) plan.blockStorage.push_back(std::make_unique(id)); - plan.valueStorage.reserve(records.size()); + plan.valueStorage.Reserve(records.size()); for (const auto& record : records) { - auto value = std::make_unique(record.opcode, record.type, record.id); + auto* value = plan.valueStorage.Create(record.opcode, record.type, record.id); value->SetFlags(record.flags); if (record.hasImmediate) value->SetImmediateU64(record.immediate); value->SetRegister(record.registerValue); - plan.valueStorage.push_back(std::move(value)); } bool valid = true; const auto valueAt = [&](std::uint32_t index) -> IrValue* { if (index == NoPlanIndex) return nullptr; - if (index >= plan.valueStorage.size()) { + if (index >= plan.valueStorage.Size()) { valid = false; return nullptr; } - return plan.valueStorage[index].get(); + return &plan.valueStorage[index]; }; for (std::size_t index = 0; index < records.size(); ++index) { const auto& record = records[index]; - auto& value = *plan.valueStorage[index]; + auto& value = plan.valueStorage[index]; + std::vector arguments; + std::vector phiBlocks; + arguments.reserve(record.arguments.size()); for (std::size_t operand = 0; operand < record.arguments.size(); ++operand) { - auto* argument = valueAt(record.arguments[operand]); - if (value.IsPhi()) { - value.AddPhiOperand(plan.blockStorage[record.phiBlocks[operand]].get(), argument); - } else { - value.AddArgument(argument); - } + arguments.push_back(valueAt(record.arguments[operand])); + if (value.IsPhi()) phiBlocks.push_back(plan.blockStorage[record.phiBlocks[operand]].get()); } + value.SetDetachedOperands(std::move(arguments), std::move(phiBlocks)); } - reader.List(plan.memoryInfo, 78, decodeMemoryInfo); + reader.List(plan.memoryInfo, 79, decodeMemoryInfo); reader.List(plan.descriptorSources, 72, [&](Reader& in, DescriptorSource& source) { for (auto& dword : source.dwords) dword = valueAt(in.Get()); in.Value(source.dwordCount); @@ -1169,6 +1190,11 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { domains.Value(domain.value); }); in.Value(column.unkeyed); + in.Value(column.indirect); + in.Value(column.indirectOffset); + in.Value(column.indirectImmediate); + in.Value(column.foldedAddend); + in.Value(column.foldedCarry); source.tableColumn = table ? std::optional(std::move(column)) : std::nullopt; }); reader.List(plan.controlFlow, 28, [&](Reader& in, ResourceBlock& block) { @@ -1283,6 +1309,7 @@ public: if (root.empty()) return; this->root = root; directory = root / hex(SourceVersion()); + discoverPrebuilt(); std::thread([this] { run(); }).detach(); } @@ -1290,6 +1317,7 @@ public: [[nodiscard]] const std::filesystem::path& Directory() const { return directory; } bool Load(std::span key, CompiledVariant& variant) { + if (loadPrebuilt(key, variant)) return true; return load(key, [&](std::span file, std::shared_ptr owner) { return DecodeEntry(file, key, variant, std::move(owner)); }, hits, misses, loadFailures); } @@ -1339,16 +1367,107 @@ public: counters.planMisses = planMisses.load(std::memory_order_relaxed); counters.planWrites = planWrites.load(std::memory_order_relaxed); counters.planLoadFailures = planLoadFailures.load(std::memory_order_relaxed); + counters.prebuiltHits = prebuiltHits.load(std::memory_order_relaxed); + counters.prebuiltMisses = prebuiltMisses.load(std::memory_order_relaxed); + counters.prebuiltRejected = prebuiltRejected.load(std::memory_order_relaxed); + counters.lookupNanoseconds = lookupNanoseconds.load(std::memory_order_relaxed); + counters.readNanoseconds = readNanoseconds.load(std::memory_order_relaxed); + counters.decodeNanoseconds = decodeNanoseconds.load(std::memory_order_relaxed); + counters.loadNanoseconds = loadNanoseconds.load(std::memory_order_relaxed); + counters.maxLoadNanoseconds = maxLoadNanoseconds.load(std::memory_order_relaxed); + counters.prebuiltBytesRead = prebuiltBytesRead.load(std::memory_order_relaxed); + counters.prebuiltLoadNanoseconds = prebuiltLoadNanoseconds.load(std::memory_order_relaxed); return counters; } private: + void discoverPrebuilt() { + const auto start = std::chrono::steady_clock::now(); + std::size_t entries = 0; + const auto addPack = [&](const std::filesystem::path& path) { + auto pack = std::make_unique(path); + if (!pack->Valid()) { + std::fprintf(stderr, "[shader-prebuilt] ignored %s: %s\n", path.string().c_str(), pack->Error().c_str()); + return; + } + entries += pack->Count(); + packs.push_back(std::move(pack)); + }; + for (const auto& path : PrebuiltCachePaths(root)) { + std::error_code error; + if (std::filesystem::is_regular_file(path, error)) { addPack(path); continue; } + if (!std::filesystem::is_directory(path, error)) continue; + bool loose = false; + std::vector packPaths; + for (std::filesystem::directory_iterator it(path, error), end; !error && it != end; it.increment(error)) { + const auto extension = it->path().extension(); + if (extension == ".pack") packPaths.push_back(it->path()); + if (extension == ".bin" && it->path().stem().string().size() == 32) loose = true; + } + std::sort(packPaths.begin(), packPaths.end()); + for (const auto& item : packPaths) addPack(item); + if (loose) looseDirectories.push_back(path); + const auto versioned = path / hex(SourceVersion()); + if (std::filesystem::is_directory(versioned, error)) looseDirectories.push_back(versioned); + } + if (!packs.empty() || !looseDirectories.empty()) std::fprintf(stderr, "[shader-prebuilt] mapped %zu packs, %zu entries, %zu loose folders in %.3f ms; exact source/ABI/request keys required\n", packs.size(), entries, looseDirectories.size(), std::chrono::duration(std::chrono::steady_clock::now() - start).count()); + } + struct Job { std::vector key; std::shared_ptr variant; std::shared_ptr plan; }; + bool loadPrebuilt(std::span key, CompiledVariant& variant) { + if (packs.empty() && looseDirectories.empty()) return false; + const auto started = std::chrono::steady_clock::now(); + const auto elapsed = [](auto start) { return static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count()); }; + bool loaded = false; + PackLoadTimings timing; + for (const auto& pack : packs) { + const auto status = pack->Load(key, variant, timing); + if (status == LoadStatus::Loaded) { loaded = true; break; } + if (status == LoadStatus::Rejected) prebuiltRejected.fetch_add(1, std::memory_order_relaxed); + } + lookupNanoseconds.fetch_add(timing.lookupNanoseconds, std::memory_order_relaxed); + decodeNanoseconds.fetch_add(timing.decodeNanoseconds, std::memory_order_relaxed); + prebuiltBytesRead.fetch_add(timing.bytes, std::memory_order_relaxed); + if (!loaded) { + const auto name = EntryName(key); + thread_local std::vector* fileSlot = nullptr; + auto& file = ThreadOwned(fileSlot); + for (const auto& path : looseDirectories) { + const auto start = std::chrono::steady_clock::now(); + const auto read = ReadWholeFile(path / name, file); + readNanoseconds.fetch_add(elapsed(start), std::memory_order_relaxed); + if (!read) continue; + const auto decode = std::chrono::steady_clock::now(); + const auto status = DecodeEntry(file, key, variant); + decodeNanoseconds.fetch_add(elapsed(decode), std::memory_order_relaxed); + prebuiltBytesRead.fetch_add(file.size(), std::memory_order_relaxed); + if (status == LoadStatus::Rejected) { + const auto failures = prebuiltRejected.fetch_add(1, std::memory_order_relaxed); + if (failures < 8) std::fprintf(stderr, "[shader-prebuilt] rejected %s (%zu bytes): truncated, corrupt or another format; recompiling\n", name.c_str(), file.size()); + } + if (status == LoadStatus::Loaded) { loaded = true; break; } + } + if (file.capacity() > (4u << 20u)) std::vector().swap(file); + } + if (loaded) { + const auto previous = prebuiltHits.fetch_add(1, std::memory_order_relaxed); + if (previous == 0) std::fprintf(stderr, "[shader-prebuilt] first exact-key hit loaded in %.3f ms\n", elapsed(started) / 1e6); + } else prebuiltMisses.fetch_add(1, std::memory_order_relaxed); + if (loaded) { + const auto duration = elapsed(started); + prebuiltLoadNanoseconds.fetch_add(duration, std::memory_order_relaxed); + auto maximum = maxLoadNanoseconds.load(std::memory_order_relaxed); + while (maximum < duration && !maxLoadNanoseconds.compare_exchange_weak(maximum, duration, std::memory_order_relaxed)) {} + report(false); + } + return loaded; + } + template bool load(std::span key, TDecode&& decode, std::atomic& hitCount, std::atomic& missCount, std::atomic& failureCount) { const auto started = std::chrono::steady_clock::now(); @@ -1474,19 +1593,27 @@ private: const auto totals = Totals(); const auto loadNs = loadNanoseconds.load(std::memory_order_relaxed); const auto delta = [](std::uint64_t after, std::uint64_t before) { return static_cast(after - before); }; - if (totals.hits == reported.hits && totals.misses == reported.misses && totals.writes == reported.writes && totals.loadFailures == reported.loadFailures && totals.writeFailures == reported.writeFailures && totals.planHits == reported.planHits && totals.planMisses == reported.planMisses && totals.planWrites == reported.planWrites && totals.planLoadFailures == reported.planLoadFailures) return; - std::fprintf(stderr, "[shader-disk-cache] (10 s): %llu hits, %llu misses, %llu writes (%.1f MiB), %llu load failures, %llu write failures; resource plans: %llu hits, %llu misses, %llu writes, %llu load failures; %.1f MiB read in %.1f ms (totals: %llu hits, %llu misses, %llu writes; plans %llu hits, %llu misses)\n", + const auto baseChanged = totals.hits != reported.hits || totals.misses != reported.misses || totals.writes != reported.writes || totals.loadFailures != reported.loadFailures || totals.writeFailures != reported.writeFailures || totals.planHits != reported.planHits || totals.planMisses != reported.planMisses || totals.planWrites != reported.planWrites || totals.planLoadFailures != reported.planLoadFailures; + const auto prebuiltChanged = totals.prebuiltHits != reported.prebuiltHits || totals.prebuiltMisses != reported.prebuiltMisses || totals.prebuiltRejected != reported.prebuiltRejected; + if (!baseChanged && !prebuiltChanged) return; + if (baseChanged) std::fprintf(stderr, "[shader-disk-cache] (10 s): %llu hits, %llu misses, %llu writes (%.1f MiB), %llu load failures, %llu write failures; resource plans: %llu hits, %llu misses, %llu writes, %llu load failures; %.1f MiB read in %.1f ms (totals: %llu hits, %llu misses, %llu writes; plans %llu hits, %llu misses)\n", delta(totals.hits, reported.hits), delta(totals.misses, reported.misses), delta(totals.writes, reported.writes), static_cast(totals.bytesWritten - reported.bytesWritten) / (1024.0 * 1024.0), delta(totals.loadFailures, reported.loadFailures), delta(totals.writeFailures, reported.writeFailures), delta(totals.planHits, reported.planHits), delta(totals.planMisses, reported.planMisses), delta(totals.planWrites, reported.planWrites), delta(totals.planLoadFailures, reported.planLoadFailures), static_cast(totals.bytesRead - reported.bytesRead) / (1024.0 * 1024.0), static_cast(loadNs - reportedLoadNs) / 1e6, static_cast(totals.hits), static_cast(totals.misses), static_cast(totals.writes), static_cast(totals.planHits), static_cast(totals.planMisses)); + if (prebuiltChanged && (!packs.empty() || !looseDirectories.empty())) std::fprintf(stderr, "[shader-prebuilt] interval: %llu hits, %llu misses, %llu rejected; lookup %.3f ms, file read %.3f ms, decode %.3f ms; total load %.3f ms, max %.3f ms (lifetime hits %llu)\n", + delta(totals.prebuiltHits, reported.prebuiltHits), delta(totals.prebuiltMisses, reported.prebuiltMisses), delta(totals.prebuiltRejected, reported.prebuiltRejected), + (totals.lookupNanoseconds - reported.lookupNanoseconds) / 1e6, (totals.readNanoseconds - reported.readNanoseconds) / 1e6, (totals.decodeNanoseconds - reported.decodeNanoseconds) / 1e6, + (totals.prebuiltLoadNanoseconds - reported.prebuiltLoadNanoseconds) / 1e6, totals.maxLoadNanoseconds / 1e6, static_cast(totals.prebuiltHits)); reported = totals; reportedLoadNs = loadNs; } std::filesystem::path root; std::filesystem::path directory; + std::vector> packs; + std::vector looseDirectories; std::mutex mutex; std::condition_variable wake; std::condition_variable idle; @@ -1494,6 +1621,7 @@ private: bool writing = false; std::atomic hits{0}, misses{0}, writes{0}, loadFailures{0}, writeFailures{0}, bytesRead{0}, bytesWritten{0}, loadNanoseconds{0}; std::atomic planHits{0}, planMisses{0}, planWrites{0}, planLoadFailures{0}; + std::atomic prebuiltHits{0}, prebuiltMisses{0}, prebuiltRejected{0}, lookupNanoseconds{0}, readNanoseconds{0}, decodeNanoseconds{0}, maxLoadNanoseconds{0}, prebuiltBytesRead{0}, prebuiltLoadNanoseconds{0}; std::atomic lastReport{0}; std::mutex reportMutex; Counters reported; diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 09feddad59..9e59550c01 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 33; +inline constexpr std::uint32_t FormatVersion = 35; enum class LoadStatus { Loaded, @@ -34,6 +34,16 @@ struct Counters { std::uint64_t planMisses = 0; std::uint64_t planWrites = 0; std::uint64_t planLoadFailures = 0; + std::uint64_t prebuiltHits = 0; + std::uint64_t prebuiltMisses = 0; + std::uint64_t prebuiltRejected = 0; + std::uint64_t lookupNanoseconds = 0; + std::uint64_t readNanoseconds = 0; + std::uint64_t decodeNanoseconds = 0; + std::uint64_t loadNanoseconds = 0; + std::uint64_t maxLoadNanoseconds = 0; + std::uint64_t prebuiltBytesRead = 0; + std::uint64_t prebuiltLoadNanoseconds = 0; }; [[nodiscard]] std::uint64_t SourceVersion(); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp index 5c63224c63..1b1c53242b 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp @@ -15,6 +15,7 @@ struct SpirvRequirements { bool subgroupShuffle = false; bool subgroupLocalInvocationId = false; bool computeDerivatives = false; + bool neighbourLaneReads = false; bool imageGatherExtended = false; bool functionLds = false; bool ldsLock = false; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp index 47ffeb796c..58789e09fe 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp @@ -27,6 +27,7 @@ bool BdaInvocationsMayStop(const IrProgram& program); void StopBdaInvocationIf(SpirvEmitterState& state, std::uint32_t condition); std::uint32_t BdaInstructionPc(SpirvEmitterState& state, const IrValue& inst); std::uint32_t EmitBdaRead(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t bits); +std::uint32_t EmitBdaReadBytes(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t bytes); void EmitBdaWrite(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t value, std::uint32_t bits = 32u); void EmitBdaStore(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t value, std::uint32_t bits); void DefineBdaByteWriteFunctions(SpirvEmitterState& state); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp index 0ad1aefdb9..1a1beae999 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp @@ -220,6 +220,7 @@ std::uint32_t EmitFPNeg32(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitFPCmpClass32(SpirvEmitterState& state, std::uint32_t value, std::uint32_t mask); void EmitReference(SpirvValueEmitContext& context); void EmitReferenceU32(SpirvValueEmitContext& context); +void EmitUnlinkedCall(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t low, std::uint32_t high, std::uint32_t programCounter); void EmitControlNop(SpirvValueEmitContext& context); void EmitWaitcnt(SpirvValueEmitContext& context); void EmitSendmsg(SpirvValueEmitContext& context); @@ -346,6 +347,7 @@ std::uint32_t EmitSharedAtomicCmpstF64(SpirvValueEmitContext& ctx, const IrValue std::uint32_t EmitSharedAtomicMskor64(SpirvValueEmitContext& ctx, const IrValue& inst); std::uint32_t EmitDataAppend(SpirvValueEmitContext& ctx, const IrValue& inst); std::uint32_t EmitDataConsume(SpirvValueEmitContext& ctx, const IrValue& inst); +std::uint32_t EmitDataOrderedCount(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageQueryDimensions(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageQueryLod(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageRead(SpirvValueEmitContext& ctx, const IrValue& inst); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 0612a023fe..b85eb66e9f 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -93,7 +93,7 @@ struct SpirvEmitterState { SpirvModule module; const IrProgram& program; ShaderStageInputInfo inputInfo; - std::array tessVariables {}; + std::array tessVariables {}; std::uint32_t tessInnerVariable = 0; SpirvRequirements requirements; @@ -119,6 +119,7 @@ struct SpirvEmitterState { std::vector memoryByteOffsets; std::map, std::uint32_t> formattedBufferFunctions; std::map, std::uint32_t> formattedGpuBufferFunctions; + std::uint32_t bufferFormatTable = 0; std::uint32_t bdaPagetableVariable = 0; std::uint32_t faultBufferVariable = 0; std::uint32_t bdaPointerFunction = 0; @@ -134,6 +135,7 @@ struct SpirvEmitterState { bool nativeF16ModesEmitted = false; bool f16DenormPreserveEmitted = false; std::uint32_t dot2F32F16Function = 0; + std::map, std::uint32_t> interpolateFunctions; std::array, 2> bdaDwordReadFunctions {}; std::array, 2> bdaSpanReadFunctions {}; std::unordered_map> bdaGroupedLoads; @@ -177,6 +179,9 @@ struct SpirvEmitterState { std::uint32_t currentLabel = 0; const IrBlock* currentBlock = nullptr; std::uint32_t pixelValidMaskVariable = 0; + std::uint32_t quadPixelUncovered = 0; + std::uint32_t sampleMaskInputVariable = 0; + std::uint32_t quadSampleCoverage = 0; std::uint32_t subgroupLocalInvocationIdVariable = 0; std::uint32_t perVertexVariable = 0; std::uint32_t pointSizeVariable = 0; @@ -188,6 +193,7 @@ struct SpirvEmitterState { std::uint32_t depthVariable = 0; std::uint32_t sampleMaskVariable = 0; std::uint32_t stencilRefVariable = 0; + bool stencilReplay = false; std::vector inputs; std::vector outputs; std::vector interfaceVariables; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp index 21043b76c5..48870b3597 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp @@ -52,6 +52,7 @@ public: [[nodiscard]] std::uint32_t Import(const std::string& name); [[nodiscard]] std::uint32_t DefineGlobalVariable(std::uint32_t pointerType, std::uint32_t storageClass); void DefineGlobalVariable(std::uint32_t id, std::uint32_t pointerType, std::uint32_t storageClass); + [[nodiscard]] std::uint32_t DefineInitializedGlobalVariable(std::uint32_t pointerType, std::uint32_t storageClass, std::uint32_t initializer); void AddMemoryModel(std::uint32_t addressingModel, std::uint32_t memoryModel); void AddName(std::uint32_t target, const std::string& name); void AddFunction(std::span words); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp index 241522188c..592cb6a6d1 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include namespace ShaderRecompiler { namespace { @@ -806,12 +808,52 @@ std::uint32_t EmitFPDot2F32F16(SpirvEmitterState& state, std::uint32_t arg0, std }); } +namespace { + +std::uint32_t InterpolateFunction(SpirvEmitterState& state, bool halfResult, std::uint32_t mode) { + const auto key = std::make_pair(halfResult, mode); + if (const auto found = state.interpolateFunctions.find(key); found != state.interpolateFunctions.end()) return found->second; + const auto f32 = TypeF32(state); + const auto id = state.module.AllocateId(); + struct Restore { + SpirvEmitterState& state; + std::uint32_t label; + ~Restore() { + state.module.EndHelperFunction(); + state.currentLabel = label; + } + }; + state.module.BeginHelperFunction(); + const Restore restore{state, state.currentLabel}; + state.module.AddName(id, std::string(halfResult ? "interpolate_f16_mode_" : "interpolate_f32_mode_") + std::to_string(mode)); + state.module.AddFunction(spv::OpFunction, f32, id, spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, f32, f32, f32, f32)); + std::array parameters{}; + for (auto& parameter : parameters) { + parameter = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionParameter, f32, parameter); + } + EmitLabel(state, state.module.AllocateId()); + state.module.AddFunction(spv::OpReturnValue, Interpolate(state, parameters[0], parameters[1], parameters[2], halfResult, mode)); + state.module.AddFunction(spv::OpFunctionEnd); + state.interpolateFunctions.emplace(key, id); + return id; +} + +std::uint32_t CallInterpolate(SpirvEmitterState& state, std::uint32_t delta, std::uint32_t coordinate, std::uint32_t base, bool halfResult, std::uint32_t mode) { + const auto function = InterpolateFunction(state, halfResult, mode); + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, TypeF32(state), result, function, delta, coordinate, base); + return result; +} + +} + std::uint32_t EmitFPInterpolateF32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1, std::uint32_t arg2, const IrValue* mode) { - return Interpolate(state, arg0, arg1, arg2, false, mode->ImmediateU32()); + return CallInterpolate(state, arg0, arg1, arg2, false, mode->ImmediateU32()); } std::uint32_t EmitFPInterpolateF16(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1, std::uint32_t arg2, const IrValue* mode) { - return Interpolate(state, arg0, arg1, arg2, true, mode->ImmediateU32()); + return CallInterpolate(state, arg0, arg1, arg2, true, mode->ImmediateU32()); } std::uint32_t EmitConvertF32F64(SpirvEmitterState& state, std::uint32_t arg0) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index dd83663928..60295387c7 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -1,4 +1,5 @@ #include "SpirvBackend/SpirvAnalysis.hpp" +#include "RdnaDecoder/RdnaInstruction.hpp" #include #include #include @@ -9,6 +10,30 @@ namespace ShaderRecompiler { namespace { +bool ReadsNeighbourLanes(const IrProgram& program, const IrValue& inst) { + switch (inst.Opcode()) { + case IrOpcode::ImageQueryLod: + case IrOpcode::DppMoveU32: + case IrOpcode::DppUpdateU32: + case IrOpcode::SwizzleU32: + case IrOpcode::BpermuteU32: + case IrOpcode::PermuteU32: + case IrOpcode::Permlane16U32: + case IrOpcode::ReadLane: + case IrOpcode::ReadFirstLane: + case IrOpcode::Ballot: + case IrOpcode::WqmU64: + return true; + case IrOpcode::ImageSampleImplicitLod: + case IrOpcode::ImageSampleRaw: { + const auto flags = program.Resources().memoryInfo.at(inst.Flags().index).imageSampleFlags; + return (flags & (RdnaImageSampleFlagDerivative | RdnaImageSampleFlagLod | RdnaImageSampleFlagLevelZero)) == 0u; + } + default: + return false; + } +} + bool UniformSource(const IrValue& value) { switch (value.Opcode()) { case IrOpcode::Ballot: @@ -49,6 +74,7 @@ bool LaneSource(const IrProgram& program, const IrValue& value) { case IrOpcode::DataAppend: case IrOpcode::DataConsume: case IrOpcode::LoadVectorWindow: + case IrOpcode::DataOrderedCount: return true; case IrOpcode::GetBuiltin: { const auto* kind = value.Argument(0)->Resolve(); @@ -376,6 +402,7 @@ SpirvRequirements AnalyzeProgramRequirements(const IrProgram& program) { requirements.subgroupLocalInvocationId = true; } } + requirements.neighbourLaneReads = requirements.neighbourLaneReads || ReadsNeighbourLanes(program, *inst); switch (inst->Opcode()) { case IrOpcode::Ballot: requirements.subgroupBallot = true; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index b43f89f997..067d57d184 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -427,6 +427,17 @@ std::uint32_t EmitBdaRead(SpirvValueEmitContext& ctx, const IrValue& inst, std:: return EmitBdaBytes(state, address, bits / 8u, instruction, BdaAccessMask(state, inst)); } +std::uint32_t EmitBdaReadBytes(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t bytes) { + auto& state = ctx.state; + if (state.bdaPointerFunction == 0) ctx.Fail(inst, "BDA lookup function is missing"); + const auto instruction = BdaInstructionPc(state, inst); + const auto u64 = TypeScalarU64(state); + const auto overflow = Binary(state, spv::OpUGreaterThan, TypeBool(state), address, Binary(state, spv::OpISub, u64, BdaConstant(state, std::numeric_limits::max()), Unary(state, spv::OpUConvert, u64, bytes))); + EmitIfCondition(state, overflow, [&] { RecordBdaFault(state, address, bytes, instruction, BdaAbi::FaultReason::Overflow); }); + StopBdaInvocationIf(state, overflow); + return EmitBdaByteLoop(state, address, bytes, instruction, BdaAccessMask(state, inst)); +} + void EmitBdaWrite(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t value, std::uint32_t bits) { auto& state = ctx.state; if (state.bdaWritePointerFunction == 0 || state.bdaNoteWriteFunction == 0) ctx.Fail(inst, "BDA write functions are missing"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index 769ec4c329..5564c836c6 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -1,4 +1,5 @@ #include "SpirvBackend/SpirvFlowEmitter.hpp" +#include "PipelineSpecialization.hpp" #include "SpirvBackend/SpirvBda.hpp" #include "SpirvBackend/SpirvEmitterInstructions.hpp" #include "SpirvBackend/SpirvMemory/SpirvSubgroup.hpp" @@ -93,6 +94,18 @@ void EmitReturnTerminator(SpirvValueEmitContext& ctx) { state.module.AddFunction(spv::OpEndInvocationInterlockEXT); } EmitKillIfPixelValidMaskInactive(state); + if (state.stencilReplay) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeI32(state), value, state.stencilRefVariable); + const auto reference = Unary(state, spv::OpBitcast, TypeU32(state), value); + const auto masked = Binary(state, spv::OpBitwiseAnd, TypeU32(state), reference, ConstantU32(state, 0xffu)); + const auto offset = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::StencilReferenceOffset, state.program.Metadata().bindings.stencilReferenceDword); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePushConstantElementPointer(state), pointer, state.pushConstantVariable, ConstantU32(state, 0u), offset); + const auto expected = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), expected, pointer); + EmitKillIfBoolFalse(state, Binary(state, spv::OpIEqual, TypeBool(state), masked, expected)); + } state.module.AddFunction(spv::OpReturn); } @@ -293,6 +306,7 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::Void: return Invoke(EmitVoid, ctx, inst); case IrOpcode::Reference: return Invoke(EmitReference, ctx, inst); case IrOpcode::ReferenceU32: return Invoke(EmitReferenceU32, ctx, inst); + case IrOpcode::UnlinkedCall: return Invoke(EmitUnlinkedCall, ctx, inst); case IrOpcode::GetUserData: return Invoke(EmitGetUserData, ctx, inst); case IrOpcode::GetShaderBase: return Invoke(EmitGetShaderBase, ctx, inst); case IrOpcode::ShaderClock: return Invoke(EmitShaderClock, ctx, inst); @@ -651,6 +665,7 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::SharedAtomicMskor64: return Invoke(EmitSharedAtomicMskor64, ctx, inst); case IrOpcode::DataAppend: return Invoke(EmitDataAppend, ctx, inst); case IrOpcode::DataConsume: return Invoke(EmitDataConsume, ctx, inst); + case IrOpcode::DataOrderedCount: return Invoke(EmitDataOrderedCount, ctx, inst); case IrOpcode::SwizzleU32: return Invoke(EmitSwizzleU32, ctx, inst); case IrOpcode::ImageQueryDimensions: return Invoke(EmitImageQueryDimensions, ctx, inst); case IrOpcode::ImageQueryLod: return Invoke(EmitImageQueryLod, ctx, inst); @@ -874,6 +889,13 @@ void EmitReferenceU32(SpirvValueEmitContext& context) { EmitVoid(context); } +void EmitUnlinkedCall(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t low, std::uint32_t high, std::uint32_t programCounter) { + auto& state = ctx.state; + if (state.faultBufferVariable == 0) ctx.Fail(inst, "calls an unlinked target without a fault buffer"); + RecordBdaFaultWords(state, low, high, ConstantU32(state, 0u), programCounter, BdaAbi::FaultReason::UnlinkedCall); + StopBdaInvocationIf(state, ConstantBool(state, true)); +} + void EmitControlNop(SpirvValueEmitContext& context) { EmitVoid(context); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 07e7a11780..0328487362 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -965,7 +965,7 @@ void EmitReadOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { const auto& dimensionInfo = RdnaImageDimensionInfoFor(access.image.dimension); const auto numericClass = access.image.numericClass; auto condition = ctx.Arg(access.inst, 2); - if (access.mem.imageDimension == RdnaImageDimension::Dim3D && access.image.dimension == RdnaImageDimension::Dim2D) { + if (access.mem.imageDimension == RdnaImageDimension::Dim3D && (access.image.dimension == RdnaImageDimension::Dim2D || access.image.dimension == RdnaImageDimension::Dim1D)) { condition = Binary(state, spv::OpLogicalAnd, TypeBool(state), condition, Binary(state, spv::OpIEqual, TypeBool(state), AddressU32(ctx, access, 2u), ConstantU32(state, 0u))); } if (VolumeSliceAddress(access)) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index 44745b1c8b..9312b24e49 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -37,8 +37,8 @@ std::uint32_t EmitShaderDataDwordLoad(SpirvEmitterState& state, std::uint32_t dw const auto pointer = state.module.AllocateId(); const auto value = state.module.AllocateId(); if (layout.UsesPushData()) { - const auto base = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::PushDataOffset, layout.pushDataStartDword); - const auto index = Binary(state, spv::OpIAdd, TypeU32(state), Binary(state, spv::OpISub, TypeU32(state), base, ConstantU32(state, layout.PushSlotDword())), ConstantU32(state, dwordIndex)); + const auto base = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::PushDataOffset, state.stencilReplay ? layout.pushDataStartDword % PushData::DwordCount : layout.pushDataStartDword); + const auto index = Binary(state, spv::OpIAdd, TypeU32(state), Binary(state, spv::OpISub, TypeU32(state), base, ConstantU32(state, state.stencilReplay ? 0u : layout.PushSlotDword())), ConstantU32(state, dwordIndex)); state.module.AddFunction(spv::OpAccessChain, TypePushConstantElementPointer(state), pointer, state.pushConstantVariable, ConstantU32(state, 0), index); } else if (state.shaderDataStorageVariable != 0) { state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), pointer, state.shaderDataStorageVariable, ConstantU32(state, 0), ConstantU32(state, dwordIndex)); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index bd37156052..168803014c 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -1,6 +1,7 @@ #include "Optimization/ResourceMaterializer.hpp" #include "BdaAbi.hpp" #include "PipelineSpecialization.hpp" +#include "StencilReplay.hpp" #include "SpirvBackend/SpirvEmitterHelpers.hpp" #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" @@ -70,6 +71,7 @@ std::uint32_t BuiltInForInput(StageInputKind kind) { case StageInputKind::PrimitiveId: return spv::BuiltInPrimitiveId; case StageInputKind::TessCoord: return spv::BuiltInTessCoord; case StageInputKind::InstanceIndex: return spv::BuiltInInstanceIndex; + case StageInputKind::BaseInstance: return spv::BuiltInBaseInstance; case StageInputKind::FragCoord: return spv::BuiltInFragCoord; case StageInputKind::FrontFacing: return spv::BuiltInFrontFacing; case StageInputKind::HelperInvocation: return spv::BuiltInHelperInvocation; @@ -217,6 +219,7 @@ void DefineInputs(SpirvEmitterState& state) { case StageInputKind::InvocationId: case StageInputKind::PrimitiveId: case StageInputKind::InstanceIndex: + case StageInputKind::BaseInstance: case StageInputKind::Layer: case StageInputKind::SampleId: type = TypeI32(state); @@ -295,6 +298,11 @@ void DefineInputs(SpirvEmitterState& state) { state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationFlat); } } + if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->QuadSampleMaskPartial()) { + state.sampleMaskInputVariable = DefineInterfaceVariable(state, SampleMaskArrayType(state), spv::StorageClassInput, "gl_SampleMaskIn"); + state.module.AddAnnotation(spv::OpDecorate, state.sampleMaskInputVariable, spv::DecorationBuiltIn, spv::BuiltInSampleMask); + state.module.AddAnnotation(spv::OpDecorate, state.sampleMaskInputVariable, spv::DecorationFlat); + } } constexpr std::uint32_t MaxClipDistances = 8u; @@ -366,7 +374,18 @@ void DefineOutputs(SpirvEmitterState& state) { 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); + if (std::ranges::find(state.supportedCapabilities, static_cast(spv::CapabilityStencilExportEXT)) == state.supportedCapabilities.end()) { + const auto values = StencilReferenceValues(state.program); + if (values.empty()) throw std::runtime_error("stencil reference export emulation requires a proven set of at most 16 values"); + if (!StencilReferenceDefinedForKeptInvocations(state.program)) throw std::runtime_error("stencil reference export emulation requires a stencil export on every kept path"); + if (PixelInfo(state).psEarlyZ || PixelInfo(state).psOrderedPixelShader || !StencilReplaySafe(state.program)) throw std::runtime_error("stencil reference export emulation requires a replay-safe pixel shader with late tests"); + state.stencilReplay = true; + state.stencilRefVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPrivate, TypeI32(state)), spv::StorageClassPrivate); + if (state.spirvVersion >= 0x00010400u) state.interfaceVariables.push_back(state.stencilRefVariable); + binding.variableId = state.stencilRefVariable; + } else { + binding.variableId = BuiltIn(state.stencilRefVariable, TypeI32(state), "gl_FragStencilRefARB", spv::BuiltInFragStencilRefEXT); + } break; case StageOutputKind::Parameter: case StageOutputKind::Mrt: { @@ -385,12 +404,15 @@ void DefineOutputs(SpirvEmitterState& state) { 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); } + if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->QuadSampleMaskPartial()) { + BuiltIn(state.sampleMaskVariable, SampleMaskArrayType(state), "gl_SampleMask", spv::BuiltInSampleMask); + } } void DefineDescriptors(SpirvEmitterState& state) { const IrBindingLayout& layout = state.program.Metadata().bindings; const IrShaderStage stage = state.program.Resources().stage; - if (layout.UsesPushData() || stage == IrShaderStage::Mesh || stage == IrShaderStage::TessellationControl) { + if (layout.UsesPushData() || state.stencilReplay || stage == IrShaderStage::Mesh || stage == IrShaderStage::TessellationControl) { const auto type = PushConstantBlockType(state); state.pushConstantVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPushConstant, type), spv::StorageClassPushConstant); state.module.AddName(type, "BufferResource"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index c454eee2c5..b7ee109d7f 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -1250,7 +1250,7 @@ std::uint32_t AddressAtomic(SpirvValueEmitContext& ctx, const IrValue& inst) { }); } -std::uint32_t AppendConsume(SpirvValueEmitContext& ctx, const IrValue& inst, bool append) { +std::uint32_t AppendConsume(SpirvValueEmitContext& ctx, const IrValue& inst, bool append, bool ordered = false) { auto& state = ctx.state; if (ctx.half == 1u) { if (ctx.otherHalf == nullptr) { @@ -1261,9 +1261,10 @@ std::uint32_t AppendConsume(SpirvValueEmitContext& ctx, const IrValue& inst, boo const auto& mem = SharedMemory(ctx, inst); const bool wave64 = state.laneCount == 2u; const bool gds = mem.kind == ResourceKind::Gds; + const bool m0Address = gds && !ordered; std::uint32_t rawIndex = ConstantU32(state, mem.offset >> 2u); std::uint32_t m0Bounds = 0; - if (gds) { + if (m0Address) { const auto m0 = ctx.Arg(inst, 0); const auto base = Binary(state, spv::OpShiftRightLogical, TypeU32(state), m0, ConstantU32(state, 16u)); const auto size = Binary(state, spv::OpBitwiseAnd, TypeU32(state), m0, ConstantU32(state, 0xffffu)); @@ -1282,9 +1283,14 @@ std::uint32_t AppendConsume(SpirvValueEmitContext& ctx, const IrValue& inst, boo const auto count = Binary(state, spv::OpIAdd, TypeU32(state), Unary(state, spv::OpBitCount, TypeU32(state), low), Unary(state, spv::OpBitCount, TypeU32(state), high)); const auto first = ctx.FirstLane(ballot); const auto sourceLane = wave64 ? Binary(state, spv::OpBitwiseAnd, TypeU32(state), first, ConstantU32(state, 31u)) : first; + auto amount = count; + if (ordered) { + amount = state.module.AllocateId(); + state.module.AddFunction(spv::OpGroupNonUniformShuffle, TypeU32(state), amount, ConstantU32(state, spv::ScopeSubgroup), ctx.Arg(inst, 4), EmitHostSubgroupLane(state, sourceLane)); + } const auto isFirst = Binary(state, spv::OpIEqual, TypeBool(state), EmitSubgroupLocalInvocationId(state), sourceLane); const auto storageBounds = EmitMemoryElementInBounds(state, access, index); - const auto bounds = gds ? AndCondition(state, storageBounds, m0Bounds) : storageBounds; + const auto bounds = m0Address ? AndCondition(state, storageBounds, m0Bounds) : storageBounds; const auto lanesActive = wave64 ? Binary(state, spv::OpINotEqual, TypeBool(state), count, ConstantU32(state, 0u)) : exec; const auto condition = AndCondition(state, isFirst, AndCondition(state, lanesActive, bounds)); const auto atomic = EmitValueOrZeroIfCondition(state, condition, [&]() { @@ -1292,7 +1298,7 @@ std::uint32_t AppendConsume(SpirvValueEmitContext& ctx, const IrValue& inst, boo return UpdateFunctionElement(state, EmitMemoryElementPointer(state, access, index), [&](std::uint32_t old) { return Binary(state, append ? spv::OpIAdd : spv::OpISub, TypeU32(state), old, count); }); } const auto value = state.module.AllocateId(); - state.module.AddFunction(append ? spv::OpAtomicIAdd : spv::OpAtomicISub, TypeU32(state), value, EmitMemoryElementPointer(state, access, index), ConstantU32(state, gds ? spv::ScopeDevice : spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsMaskNone), count); + state.module.AddFunction(append ? spv::OpAtomicIAdd : spv::OpAtomicISub, TypeU32(state), value, EmitMemoryElementPointer(state, access, index), ConstantU32(state, gds ? spv::ScopeDevice : spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsMaskNone), amount); return value; }); const auto result = state.module.AllocateId(); @@ -1837,6 +1843,10 @@ std::uint32_t EmitDataConsume(SpirvValueEmitContext& ctx, const IrValue& inst) { return AppendConsume(ctx, inst, false); } +std::uint32_t EmitDataOrderedCount(SpirvValueEmitContext& ctx, const IrValue& inst) { + return AppendConsume(ctx, inst, true, true); +} + std::uint32_t EmitSharedAtomicRsub32(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto data = ctx.Arg(inst, 1); @@ -1847,10 +1857,21 @@ std::uint32_t EmitSharedAtomicRsub32(SpirvValueEmitContext& ctx, const IrValue& std::uint32_t EmitSharedAtomicFAdd32(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto data = ctx.Arg(inst, 1); + const auto& mem = SharedMemory(ctx, inst); + const bool flushInputs = mem.flushDenormals; + const bool flushResult = mem.flushDenormalResults; + const AtomicFloatBits dataBits{ctx.state, false}; + const auto dataOperand = Unary(ctx.state, spv::OpBitcast, TypeF32(ctx.state), dataBits.flush(data, flushInputs)); + const auto dataNan = dataBits.nan(data); + const auto quietData = dataBits.op(spv::OpBitwiseOr, data, dataBits.constant(dataBits.quiet())); return SharedAtomicUpdate(ctx, inst, [&](SpirvEmitterState& state, std::uint32_t old) { - const auto sum = Binary(state, spv::OpFAdd, TypeF32(state), Unary(state, spv::OpBitcast, TypeF32(state), old), Unary(state, spv::OpBitcast, TypeF32(state), data)); + const AtomicFloatBits bits{state, false}; + const auto sum = Binary(state, spv::OpFAdd, TypeF32(state), Unary(state, spv::OpBitcast, TypeF32(state), bits.flush(old, flushInputs)), dataOperand); state.module.AddAnnotation(spv::OpDecorate, sum, spv::DecorationNoContraction); - return Unary(state, spv::OpBitcast, TypeU32(state), sum); + auto result = bits.flush(Unary(state, spv::OpBitcast, TypeU32(state), sum), flushResult); + result = bits.select(bits.nan(result), bits.constant(0xffc00000u), result); + result = bits.select(bits.nan(old), bits.op(spv::OpBitwiseOr, old, bits.constant(bits.quiet())), result); + return bits.select(dataNan, quietData, result); }); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index f4c1da3f2b..7e4da050f6 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -137,9 +137,12 @@ void DefineModule(SpirvEmitterState& state) { state.module.EmitCapability(spv::CapabilityGroupNonUniformShuffle); } if (state.requirements.computeDerivatives && StageOf(state) == IrShaderStage::Compute) { - state.module.EmitCapability(spv::CapabilityComputeDerivativeGroupQuadsKHR); + state.module.EmitCapability(spv::CapabilityComputeDerivativeGroupLinearKHR); state.module.EmitExtension("SPV_KHR_compute_shader_derivatives"); } + if (std::any_of(state.inputs.begin(), state.inputs.end(), [](const SpirvInputBinding& input) { return input.kind == StageInputKind::BaseInstance; })) { + state.module.EmitCapability(spv::CapabilityDrawParameters); + } const bool fragmentBarycentric = StageOf(state) == IrShaderStage::Pixel && std::any_of(state.inputs.begin(), state.inputs.end(), [](const SpirvInputBinding& input) { return input.perVertex || input.kind == StageInputKind::BaryCoordSmooth || input.kind == StageInputKind::BaryCoordNoPerspective; }); @@ -170,10 +173,6 @@ void DefineModule(SpirvEmitterState& state) { localX = ((localX * localY * localZ + 63u) / 64u) * 32u; localY = 1u; localZ = 1u; - if (state.requirements.computeDerivatives) { - localY = localX / 2u; - localX = 2u; - } } state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeLocalSize, localX, localY, localZ); } @@ -189,7 +188,7 @@ void DefineModule(SpirvEmitterState& state) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDepthGreater); } } - if (state.stencilRefVariable != 0) { + if (state.stencilRefVariable != 0 && !state.stencilReplay) { 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"); } @@ -197,7 +196,7 @@ void DefineModule(SpirvEmitterState& state) { state.module.EmitExtension("SPV_EXT_shader_stencil_export"); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeStencilRefReplacingEXT); } - if (pixel.psEarlyZ && !pixel.psAlphaToCoverage) { + if (pixel.psEarlyZ && !pixel.psAlphaToCoverage && !pixel.QuadMasked()) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests); } if (pixel.psOrderedPixelShader) { @@ -210,7 +209,7 @@ void DefineModule(SpirvEmitterState& state) { } } if (state.requirements.computeDerivatives && StageOf(state) == IrShaderStage::Compute) { - state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDerivativeGroupQuadsKHR); + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDerivativeGroupLinearKHR); } state.module.AddName(state.mainFunc, "main"); if (state.requirements.functionLds) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModule.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModule.cpp index 3dbdbc3003..db387b77e3 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModule.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModule.cpp @@ -201,6 +201,12 @@ void SpirvModule::DefineGlobalVariable(std::uint32_t id, std::uint32_t pointerTy appendInstruction(declarations, spv::OpVariable, pointerType, id, storageClass); } +std::uint32_t SpirvModule::DefineInitializedGlobalVariable(std::uint32_t pointerType, std::uint32_t storageClass, std::uint32_t initializer) { + const auto id = AllocateId(); + appendInstruction(declarations, spv::OpVariable, pointerType, id, storageClass, initializer); + return id; +} + void SpirvModule::AddMemoryModel(std::uint32_t addressingModel, std::uint32_t memoryModel) { appendInstruction(this->memoryModel, spv::OpMemoryModel, addressingModel, memoryModel); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 395e365f6e..edabae4d45 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -81,13 +81,18 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std: return bits; } if (kind == StageInputKind::HelperInvocation) { - const auto value = state.module.AllocateId(); + auto value = state.module.AllocateId(); const auto bits = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeBool(state), value, variable); + if (state.quadPixelUncovered != 0u) { + const auto helper = state.module.AllocateId(); + state.module.AddFunction(spv::OpLogicalOr, TypeBool(state), helper, value, state.quadPixelUncovered); + value = helper; + } state.module.AddFunction(spv::OpSelect, TypeU32(state), bits, value, ConstantU32(state, 1u), ConstantU32(state, 0u)); return bits; } - if (kind == StageInputKind::VertexIndex || kind == StageInputKind::InstanceIndex || kind == StageInputKind::InvocationId || kind == StageInputKind::PrimitiveId || kind == StageInputKind::Layer || kind == StageInputKind::SampleId) { + if (kind == StageInputKind::VertexIndex || kind == StageInputKind::InstanceIndex || kind == StageInputKind::BaseInstance || kind == StageInputKind::InvocationId || kind == StageInputKind::PrimitiveId || kind == StageInputKind::Layer || kind == StageInputKind::SampleId) { const auto value = state.module.AllocateId(); const auto bits = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeI32(state), value, variable); @@ -283,6 +288,7 @@ bool AlphaToCoverage(const SpirvEmitterState& state) { } void StoreCoverage(SpirvEmitterState& state, std::uint32_t mask) { + if (state.quadSampleCoverage != 0u) mask = Binary(state, spv::OpBitwiseAnd, TypeI32(state), mask, state.quadSampleCoverage); const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeI32(state)), pointer, state.sampleMaskVariable, ConstantU32(state, 0u)); state.module.AddFunction(spv::OpStore, pointer, mask); @@ -579,9 +585,16 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins if (variable == 0u) { throw std::runtime_error("tessellation attribute has no interface variable"); } - const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput || kind == TessellationAttribute::PatchInput; + const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput || kind == TessellationAttribute::PatchInput || kind == TessellationAttribute::InstanceInput; const auto storage = input ? spv::StorageClassInput : spv::StorageClassOutput; + if (kind == TessellationAttribute::InstanceOutput) { + return variable; + } const auto pointer = state.module.AllocateId(); + if (kind == TessellationAttribute::InstanceInput) { + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, storage, TypeU32(state)), pointer, variable, ConstantU32(state, 0u)); + return pointer; + } if (kind == TessellationAttribute::Factor) { if (!inst.Argument(1)->HasImmediate()) { throw std::runtime_error("tessellation factor index must be immediate"); @@ -629,7 +642,7 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins } void DefineTessellationInterfaces(SpirvEmitterState& state) { - std::array used {}; + std::array used {}; for (const IrBlock* block : state.program.BlockOrder()) { for (const IrValue* inst : block->Instructions()) { if (inst->Opcode() != IrOpcode::GetTessellationAttribute && inst->Opcode() != IrOpcode::SetTessellationAttribute) { @@ -650,12 +663,15 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) { continue; } const auto kind = static_cast(index); - const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput || kind == TessellationAttribute::PatchInput; + const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput || kind == TessellationAttribute::PatchInput || kind == TessellationAttribute::InstanceInput; const bool patch = kind == TessellationAttribute::PatchOutput || kind == TessellationAttribute::PatchInput; const auto storage = input ? spv::StorageClassInput : spv::StorageClassOutput; + const bool instance = kind == TessellationAttribute::InstanceOutput || kind == TessellationAttribute::InstanceInput; std::uint32_t type; if (kind == TessellationAttribute::Factor) { type = array(TypeF32(state), 4u); + } else if (instance) { + type = kind == TessellationAttribute::InstanceInput ? array(TypeU32(state), tess.inputControlPoints) : TypeU32(state); } else if (patch) { type = array(TypeU32Vector(state, 4u), (state.program.Metadata().tessellationPatchStride + 15u) / 16u); } else { @@ -667,8 +683,10 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) { } } auto& variable = state.tessVariables.at(index); - variable = DefineInterfaceVariable(state, type, storage, "tess_attributes"); - if (kind == TessellationAttribute::Factor) { + variable = DefineInterfaceVariable(state, type, storage, instance ? "tess_instance" : "tess_attributes"); + if (instance) { + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationLocation, (state.program.Metadata().tessellationLocalStride + 15u) / 16u); + } else if (kind == TessellationAttribute::Factor) { state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBuiltIn, spv::BuiltInTessLevelOuter); state.tessInnerVariable = DefineInterfaceVariable(state, array(TypeF32(state), 2u), storage, "tess_inner"); state.module.AddAnnotation(spv::OpDecorate, state.tessInnerVariable, spv::DecorationBuiltIn, spv::BuiltInTessLevelInner); @@ -684,7 +702,7 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) { void DefineTessellationExecutionModes(SpirvEmitterState& state) { const auto& tess = state.inputInfo.vertex->tess; - if ((tess.domain != 1u && tess.domain != 2u) || (tess.partitioning != 0u && tess.partitioning != 2u) || (tess.outputTopology != 2u && tess.outputTopology != 3u)) { + if ((tess.domain != 1u && tess.domain != 2u) || (tess.partitioning != 0u && tess.partitioning != 2u && tess.partitioning != 3u) || (tess.outputTopology != 2u && tess.outputTopology != 3u)) { throw std::runtime_error("unsupported tessellation domain, partitioning, or output topology"); } state.module.EmitCapability(spv::CapabilityTessellation); @@ -692,7 +710,7 @@ void DefineTessellationExecutionModes(SpirvEmitterState& state) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeOutputVertices, tess.outputControlPoints); } else { state.module.AddExecutionMode(state.mainFunc, tess.domain == 2u ? spv::ExecutionModeQuads : spv::ExecutionModeTriangles); - state.module.AddExecutionMode(state.mainFunc, tess.partitioning == 0u ? spv::ExecutionModeSpacingEqual : spv::ExecutionModeSpacingFractionalOdd); + state.module.AddExecutionMode(state.mainFunc, tess.partitioning == 0u ? spv::ExecutionModeSpacingEqual : tess.partitioning == 3u ? spv::ExecutionModeSpacingFractionalEven : spv::ExecutionModeSpacingFractionalOdd); state.module.AddExecutionMode(state.mainFunc, tess.outputTopology == 3u ? spv::ExecutionModeVertexOrderCcw : spv::ExecutionModeVertexOrderCw); } } @@ -1168,6 +1186,55 @@ void EmitProgram(SpirvEmitterState& state) { state.module.AddFunction(spv::OpBeginInvocationInterlockEXT); } EmitMemoryOffsets(state); + if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->QuadMasked()) { + const auto& pixel = *state.inputInfo.pixel; + const auto coordinate = [&](std::uint32_t component) { + const auto value = Unary(state, spv::OpBitcast, TypeF32(state), EmitBuiltinU32(state, StageInputKind::FragCoord, component)); + return EmitBinaryU32(state, spv::OpBitwiseAnd, Unary(state, spv::OpConvertFToU, TypeU32(state), value), ConstantU32(state, 1u)); + }; + const auto quad = EmitBinaryU32(state, spv::OpBitwiseOr, coordinate(0u), EmitBinaryU32(state, spv::OpShiftLeftLogical, coordinate(1u), ConstantU32(state, 1u))); + auto bit = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, ConstantU32(state, pixel.quadPixelMask), quad), ConstantU32(state, 1u)); + if (pixel.QuadSampleMaskPartial()) { + const auto top = ConstantU32(state, static_cast(pixel.quadSampleMask[0]) | (static_cast(pixel.quadSampleMask[1]) << 16u)); + const auto bottom = ConstantU32(state, static_cast(pixel.quadSampleMask[2]) | (static_cast(pixel.quadSampleMask[3]) << 16u)); + const auto lower = EmitCompareU32Constant(state, spv::OpIEqual, EmitBinaryU32(state, spv::OpBitwiseAnd, quad, ConstantU32(state, 2u)), 2u); + const auto row = Select(state, TypeU32(state), lower, bottom, top); + const auto shift = EmitBinaryU32(state, spv::OpShiftLeftLogical, EmitBinaryU32(state, spv::OpBitwiseAnd, quad, ConstantU32(state, 1u)), ConstantU32(state, 4u)); + const auto covered = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, row, shift), ConstantU32(state, 0xffffu)); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeI32(state)), pointer, state.sampleMaskInputVariable, ConstantU32(state, 0u)); + const auto rasterized = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeI32(state), rasterized, pointer); + bit = EmitBinaryU32(state, spv::OpBitwiseAnd, Unary(state, spv::OpBitcast, TypeU32(state), rasterized), covered); + state.quadSampleCoverage = Unary(state, spv::OpBitcast, TypeI32(state), covered); + } + const auto mask = static_cast(pixel.quadPixelMask); + const bool demoteCapability = std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityDemoteToHelperInvocation)) != state.supportedCapabilities.end(); + const bool demoteCore = state.spirvVersion >= 0x00010600u; + const bool demoteExtension = std::find(state.supportedExtensions.begin(), state.supportedExtensions.end(), "SPV_EXT_demote_to_helper_invocation") != state.supportedExtensions.end(); + const bool demote = demoteCapability && (demoteCore || demoteExtension); + if (!demote && state.requirements.neighbourLaneReads) { + throw std::runtime_error("the PA_SC_AA_MASK quad pixel mask " + std::to_string(mask) + " leaves helper pixels whose lanes the pixel program reads (implicit-LOD sampling, whole-quad mode or cross-lane reads), which needs the device's shaderDemoteToHelperInvocation"); + } + const auto uncovered = state.module.AllocateId(); + state.module.AddFunction(spv::OpIEqual, TypeBool(state), uncovered, bit, ConstantU32(state, 0u)); + state.quadPixelUncovered = uncovered; + const auto killLabel = state.module.AllocateId(); + const auto mergeLabel = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelectionMerge, mergeLabel, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, uncovered, killLabel, mergeLabel); + EmitLabel(state, killLabel); + if (demote) { + state.module.EmitCapability(spv::CapabilityDemoteToHelperInvocation); + if (!demoteCore) state.module.EmitExtension("SPV_EXT_demote_to_helper_invocation"); + state.module.AddFunction(spv::OpDemoteToHelperInvocation); + state.module.AddFunction(spv::OpBranch, mergeLabel); + } else { + state.module.AddFunction(spv::OpKill); + } + EmitLabel(state, mergeLabel); + if (state.quadSampleCoverage != 0u) StoreCoverage(state, ConstantI32(state, -1)); + } if (program.BlockOrder().empty()) { if (OrderedPixelShader(state)) { state.module.AddFunction(spv::OpEndInvocationInterlockEXT); @@ -1345,7 +1412,7 @@ void EmitSetTessellationAttribute(SpirvValueEmitContext& ctx, const IrValue& ins const auto raised = state.module.AllocateId(); state.module.AddFunction(spv::OpExtInst, TypeF32(state), raised, GlslStd450(state), GLSLstd450FMax, floating, lowest); const auto clamped = state.module.AllocateId(); - state.module.AddFunction(spv::OpExtInst, TypeF32(state), clamped, GlslStd450(state), GLSLstd450FMin, raised, TessellationLevelBound(state, TessellationLevel(state, 1u), GLSLstd450FMin, state.inputInfo.vertex->tess.partitioning == 0u ? 64.0f : 63.0f)); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), clamped, GlslStd450(state), GLSLstd450FMin, raised, TessellationLevelBound(state, TessellationLevel(state, 1u), GLSLstd450FMin, state.inputInfo.vertex->tess.partitioning == 2u ? 63.0f : 64.0f)); value = state.module.AllocateId(); state.module.AddFunction(spv::OpSelect, TypeF32(state), value, inside, clamped, outer ? floating : lowest); } @@ -1429,10 +1496,8 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { if ((exp.en & 4u) != 0u && state.sampleMaskVariable != 0u) { const auto raw = ExportRawComponent(ctx, data, 2); const auto value = state.module.AllocateId(); - const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpBitcast, TypeI32(state), value, raw); - state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeI32(state)), pointer, state.sampleMaskVariable, ConstantU32(state, 0u)); - state.module.AddFunction(spv::OpStore, pointer, value); + StoreCoverage(state, value); } return; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp index a1b5e245b2..89702e7b2b 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp @@ -94,7 +94,7 @@ std::vector ValidateAndOptimizeSpirv(std::span(spirv.begin(), spirv.end()); + if (mode != nullptr && std::string(mode) == "none") return std::vector(spirv.begin(), spirv.end()); // The recompiler emits helpers (BDA lookup, fault reporting) as functions called from every memory // access. Exhaustive inlining multiplies module size by ~10x and optimization time by ~20x, so the // default pipeline keeps the performance passes that work per function and leaves inlining to the driver. diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index 7f0ae8c42a..ec559b5340 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -7,6 +7,7 @@ #include "PipelineSpecialization.hpp" #include #include +#include #include #include @@ -89,7 +90,7 @@ RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& inst return {base, context.Arg(*handle, 2u), stride, word3, both(state, nonzero(state, stride), nonzero(state, field(state, word1, 31u, 1u))), index, valid}; } -RuntimeAddress address(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t componentOffset, std::uint32_t bytes) { +RuntimeAddress byteAddress(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t offset, std::uint32_t bytes) { auto& state = context.state; const auto u32 = TypeU32(state); const auto boolean = TypeBool(state); @@ -98,9 +99,8 @@ RuntimeAddress address(SpirvValueEmitContext& context, const IrValue& instructio const auto mul = [&](std::uint32_t left, std::uint32_t right) { return Binary(state, spv::OpIMul, u32, left, right); }; const auto less = [&](std::uint32_t left, std::uint32_t right) { return Binary(state, spv::OpULessThan, boolean, left, right); }; const auto fits = [&](std::uint32_t offset, std::uint32_t size) { - return both(state, Binary(state, spv::OpUGreaterThanEqual, boolean, size, constant(bytes)), Binary(state, spv::OpULessThanEqual, boolean, offset, Binary(state, spv::OpISub, u32, size, constant(bytes)))); + return both(state, Binary(state, spv::OpUGreaterThanEqual, boolean, size, bytes), Binary(state, spv::OpULessThanEqual, boolean, offset, Binary(state, spv::OpISub, u32, size, bytes))); }; - const auto offset = resource.offset != 0u ? add(resource.offset, constant(componentOffset)) : add(context.Arg(instruction, 2u), constant(context.Memory(instruction).offset + componentOffset)); const auto soffset = resource.soffset != 0u ? resource.soffset : context.Arg(instruction, 3u); const auto indexShift = add(field(state, resource.word3, 21u, 2u), constant(3u)); const auto indices = Binary(state, spv::OpShiftLeftLogical, u32, constant(1u), indexShift); @@ -119,8 +119,15 @@ RuntimeAddress address(SpirvValueEmitContext& context, const IrValue& instructio auto inBounds = Select(state, boolean, equal(state, mode, constant(0u)), structured, indexInBounds); inBounds = Select(state, boolean, less(mode, constant(2u)), inBounds, Select(state, boolean, equal(state, mode, constant(2u)), nonzero(state, resource.records), raw)); inBounds = both(state, resource.valid, both(state, nonzero(state, field(state, resource.word3, 12u, 7u)), inBounds)); - if (!context.Memory(instruction).gpuDescriptor) return {byte, inBounds}; - return {AddBdaAddress(context, instruction, resource.base, Unary(state, spv::OpUConvert, TypeScalarU64(state), byte), false), inBounds}; + return {byte, inBounds}; +} + +RuntimeAddress address(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t componentOffset, std::uint32_t bytes) { + auto& state = context.state; + const auto offset = Binary(state, spv::OpIAdd, TypeU32(state), resource.offset != 0u ? resource.offset : context.Arg(instruction, 2u), ConstantU32(state, resource.offset != 0u ? componentOffset : context.Memory(instruction).offset + componentOffset)); + const auto bytesAddress = byteAddress(context, instruction, resource, offset, ConstantU32(state, bytes)); + if (!context.Memory(instruction).gpuDescriptor) return bytesAddress; + return {AddBdaAddress(context, instruction, resource.base, Unary(state, spv::OpUConvert, TypeScalarU64(state), bytesAddress.guest), false), bytesAddress.inBounds}; } MemoryResourceAccess directAccess(SpirvValueEmitContext& context, const IrValue& instruction, bool wide = false, bool plainLoad = false) { @@ -361,6 +368,131 @@ std::uint32_t formatted(SpirvValueEmitContext& context, const IrValue& instructi return composite(state, components, outputs); } +std::uint32_t formatTable(SpirvEmitterState& state, std::uint32_t formats) { + if (state.bufferFormatTable != 0u) return state.bufferFormatTable; + const auto u32 = TypeU32(state); + const auto arrayType = state.module.Type(spv::OpTypeArray, u32, ConstantU32(state, 3u * (formats + 1u))); + std::vector elements; + for (std::uint32_t index = 0; index <= formats; ++index) { + std::array entry{}; + if (index != 0u) { + const auto info = GetFormatInfo(static_cast(index)); + entry[0] = info.componentCount | (info.packedBitfield ? 8u : 0u) | (static_cast(info.type) << 4u); + for (std::uint32_t component = 0; component < info.componentCount; ++component) { + entry[0] |= info.componentBits[component] << (8u + component * 6u); + entry[1] |= GetFormatComponentByteOffset(info, component) << (component * 8u); + entry[2] |= info.componentBitOffset[component] << (component * 8u); + } + } + for (const auto word : entry) elements.push_back(ConstantU32(state, word)); + } + const auto table = state.module.Constant(spv::OpConstantComposite, arrayType, elements); + state.bufferFormatTable = state.module.DefineInitializedGlobalVariable(TypePointer(state, spv::StorageClassPrivate, arrayType), spv::StorageClassPrivate, table); + return state.bufferFormatTable; +} + +std::uint32_t genericFormattedLoad(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t components, std::uint32_t format, std::uint32_t formats) { + auto& state = context.state; + const auto& memory = context.Memory(instruction); + const auto u32 = TypeU32(state); + const auto boolean = TypeBool(state); + const auto constant = [&](std::uint32_t value) { return ConstantU32(state, value); }; + const auto table = formatTable(state, formats); + const auto entry = [&](std::uint32_t word) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassPrivate, u32), pointer, table, Binary(state, spv::OpIAdd, u32, Binary(state, spv::OpIMul, u32, format, constant(3u)), constant(word))); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, u32, value, pointer); + return value; + }; + const auto layout = entry(0u); + const auto byteOffsets = entry(1u); + const auto bitOffsets = entry(2u); + const auto componentCount = field(state, layout, 0u, 3u); + const auto packed = nonzero(state, field(state, layout, 3u, 1u)); + const auto type = field(state, layout, 4u, 4u); + const auto isType = [&](SpirvFormatComponentType value) { return equal(state, type, constant(static_cast(value))); }; + const auto signedType = Binary(state, spv::OpLogicalOr, boolean, isType(SpirvFormatComponentType::Sint), Binary(state, spv::OpLogicalOr, boolean, isType(SpirvFormatComponentType::Snorm), isType(SpirvFormatComponentType::Sscaled))); + const auto integer = Binary(state, spv::OpLogicalOr, boolean, isType(SpirvFormatComponentType::Uint), isType(SpirvFormatComponentType::Sint)); + std::array bits{}; + std::array active{}; + for (std::uint32_t component = 0; component < 4u; ++component) { + bits[component] = field(state, layout, 8u + component * 6u, 6u); + active[component] = Binary(state, spv::OpULessThan, boolean, constant(component), componentCount); + } + std::array selectors{}; + std::array selected{}; + for (std::uint32_t output = 0; output < components; ++output) { + selectors[output] = field(state, resource.word3, output * 3u, 3u); + selected[output] = Binary(state, spv::OpUMod, u32, Binary(state, spv::OpISub, u32, selectors[output], constant(4u)), componentCount); + } + std::array required{}; + std::array addresses{}; + auto inBounds = ConstantBool(state, true); + for (std::uint32_t component = 0; component < 4u; ++component) { + required[component] = ConstantBool(state, false); + for (std::uint32_t output = 0; output < components; ++output) { + const auto fromMemory = Binary(state, spv::OpUGreaterThanEqual, boolean, selectors[output], constant(4u)); + required[component] = Binary(state, spv::OpLogicalOr, boolean, required[component], both(state, fromMemory, equal(state, selected[output], constant(component)))); + } + required[component] = both(state, active[component], required[component]); + const auto componentOffset = field(state, byteOffsets, component * 8u, 8u); + const auto bytes = Select(state, u32, packed, constant(4u), Binary(state, spv::OpShiftRightLogical, u32, bits[component], constant(3u))); + const auto offset = Binary(state, spv::OpIAdd, u32, resource.offset != 0u ? resource.offset : Binary(state, spv::OpIAdd, u32, context.Arg(instruction, 2u), constant(memory.offset)), componentOffset); + addresses[component] = byteAddress(context, instruction, resource, offset, bytes); + if (memory.gpuDescriptor) { + const auto u64 = TypeScalarU64(state); + const auto guest = Binary(state, spv::OpIAdd, u64, resource.base, Unary(state, spv::OpUConvert, u64, addresses[component].guest)); + const auto overflow = both(state, active[component], Binary(state, spv::OpULessThan, boolean, guest, resource.base)); + EmitIfCondition(state, overflow, [&] { RecordBdaFault(state, resource.base, constant(0u), BdaInstructionPc(state, instruction), BdaAbi::FaultReason::Overflow); }); + StopBdaInvocationIf(state, overflow); + addresses[component].guest = guest; + } + inBounds = both(state, inBounds, Binary(state, spv::OpLogicalOr, boolean, Unary(state, spv::OpLogicalNot, boolean, required[component]), addresses[component].inBounds)); + } + std::array values{}; + for (std::uint32_t component = 0; component < 4u; ++component) { + values[component] = EmitValueOrZeroIfCondition(state, both(state, inBounds, required[component]), [&] { + const auto width = Select(state, u32, packed, constant(32u), bits[component]); + const auto dword = equal(state, width, constant(32u)); + const auto raw = EmitValueIfElse(state, dword, u32, [&] { return readBuffer(context, instruction, addresses[component].guest, 32u); }, [&] { + return EmitBdaReadBytes(context, instruction, addresses[component].guest, Binary(state, spv::OpShiftRightLogical, u32, width, constant(3u))); + }); + const auto bitOffset = Select(state, u32, packed, field(state, bitOffsets, component * 8u, 8u), constant(0u)); + const auto unsignedField = EmitBitFieldUExtract(state, raw, bitOffset, bits[component]); + const auto signedField = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitFieldSExtract, TypeI32(state), signedField, Unary(state, spv::OpBitcast, TypeI32(state), raw), bitOffset, bits[component]); + const auto extracted = Select(state, u32, equal(state, bits[component], constant(32u)), raw, Select(state, u32, signedType, Unary(state, spv::OpBitcast, u32, signedField), unsignedField)); + std::vector> conversions; + for (std::uint32_t index = 1; index <= formats; ++index) { + const auto info = GetFormatInfo(static_cast(index)); + if (component >= info.componentCount) continue; + const std::pair key{info.type, info.componentBits[component]}; + if (std::find(conversions.begin(), conversions.end(), key) == conversions.end()) conversions.push_back(key); + } + auto value = constant(0u); + for (const auto& [conversionType, conversionBits] : conversions) { + SpirvBufferFormatInfo info{}; + info.type = conversionType; + info.componentCount = 1u; + info.componentBits[0] = conversionBits; + const auto converted = memory.d16 ? EmitD16FormatComponent(state, info, 0u, extracted) : NormalizeFormatComponent(state, info, 0u, extracted); + value = Select(state, u32, both(state, isType(conversionType), equal(state, bits[component], constant(conversionBits))), converted, value); + } + return value; + }); + } + const auto one = Select(state, u32, integer, constant(1u), constant(memory.d16 ? 0x3c00u : 0x3f800000u)); + std::array outputs{}; + for (std::uint32_t output = 0; output < components; ++output) { + auto value = values[0]; + for (std::uint32_t component = 1; component < 4u; ++component) value = Select(state, u32, equal(state, selected[output], constant(component)), values[component], value); + value = Select(state, u32, equal(state, selectors[output], constant(1u)), one, value); + outputs[output] = Select(state, u32, equal(state, selectors[output], constant(0u)), constant(0u), value); + } + return composite(state, components, outputs); +} + std::uint32_t formatSwitch(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t components, bool store, std::uint32_t data); std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t components, bool store, std::uint32_t data); std::uint32_t formattedGpuCall(SpirvValueEmitContext& context, const IrValue& instruction, const RuntimeDescriptor& resource, std::uint32_t components, bool store, std::uint32_t data); @@ -395,9 +527,20 @@ std::uint32_t formatSwitch(SpirvValueEmitContext& context, const IrValue& instru auto& state = context.state; const auto& memory = context.Memory(instruction); const auto format = field(state, resource.word3, 12u, 7u); + constexpr std::uint32_t count = static_cast(IrBufferFormat::Format32_32_32_32Float); + if (!store && memory.gpuDescriptor) { + const auto type = components == 1u ? TypeU32(state) : TypeU32Composite(state, components); + const auto zero = components == 1u ? ConstantU32(state, 0u) : ConstantU32CompositeZero(state, components); + const auto unsupported = Binary(state, spv::OpLogicalOr, TypeBool(state), equal(state, format, ConstantU32(state, 0u)), Binary(state, spv::OpUGreaterThan, TypeBool(state), format, ConstantU32(state, count))); + const auto result = EmitValueIfElse(state, unsupported, type, [&] { + RecordBdaFault(state, resource.base, ConstantU32(state, 0u), BdaInstructionPc(state, instruction), BdaAbi::FaultReason::InvalidDescriptor); + return zero; + }, [&] { return genericFormattedLoad(context, instruction, resource, components, format, count); }); + StopBdaInvocationIf(state, unsupported); + return result; + } const auto merge = state.module.AllocateId(); const auto invalid = state.module.AllocateId(); - constexpr std::uint32_t count = static_cast(IrBufferFormat::Format32_32_32_32Float); std::array labels{}; std::vector branch{spv::OpSwitch, format, invalid}; for (std::uint32_t index = 0; index < count; ++index) { diff --git a/core/shader/recompiler/StencilReplay.cpp b/core/shader/recompiler/StencilReplay.cpp new file mode 100644 index 0000000000..f44d54f3d4 --- /dev/null +++ b/core/shader/recompiler/StencilReplay.cpp @@ -0,0 +1,744 @@ +#include "StencilReplay.hpp" +#include "Optimization/MaskedSelectEliminator.hpp" +#include "Optimization/SrtWalker/SrtInstructionPredicates.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace ShaderRecompiler { + +namespace { + +bool Boolean(const IrValue* value, bool expected) { + if (value == nullptr) return expected; + value = value->Resolve(); + return value->Type() == IrType::U1 && value->HasImmediate() && value->ImmediateBool() == expected; +} + +bool Implies(const IrValue* left, const IrValue* right, unsigned depth = 0) { + if (depth > 32u) return false; + if (left) left = left->Resolve(); + if (right) right = right->Resolve(); + if (left == right || Boolean(left, false) || Boolean(right, true)) return true; + if (!left || !right) return false; + if (left->Opcode() == IrOpcode::LogicalAnd) return Implies(left->Argument(0), right, depth + 1u) || Implies(left->Argument(1), right, depth + 1u); + if (right->Opcode() == IrOpcode::LogicalAnd) return Implies(left, right->Argument(0), depth + 1u) && Implies(left, right->Argument(1), depth + 1u); + if (left->Opcode() == IrOpcode::LogicalOr) return Implies(left->Argument(0), right, depth + 1u) && Implies(left->Argument(1), right, depth + 1u); + if (right->Opcode() == IrOpcode::LogicalOr) return Implies(left, right->Argument(0), depth + 1u) || Implies(left, right->Argument(1), depth + 1u); + return false; +} + +struct ReplayMask { + const IrValue* low = nullptr; + const IrValue* high = nullptr; +}; + +bool Immediate(const IrValue* value, std::uint32_t expected) { + value = value->Resolve(); + return value->HasImmediate() && value->Type() == IrType::U32 && value->ImmediateU32() == expected; +} + +bool FaultControlFlowAcyclic(const IrProgram& program) { + const auto& blocks = program.BlockOrder(); + const auto& metadata = program.Metadata().blockInfo; + if (blocks.empty() || blocks.size() != metadata.size() || blocks.size() > 4096u) return false; + std::map indices; + std::set owned; + for (std::size_t index = 0; index < blocks.size(); ++index) if (!blocks[index] || !owned.insert(blocks[index]).second || !indices.emplace(metadata[index].id, index).second) return false; + std::vector> successors(blocks.size()); + for (std::size_t index = 0; index < blocks.size(); ++index) { + const auto& term = metadata[index].terminator; + if (term.loopHeader) return false; + const auto edge = [&](std::uint32_t id) { + const auto found = indices.find(id); + if (found == indices.end()) return false; + successors[index].push_back(found->second); + return true; + }; + if (term.kind == TerminatorKind::Branch) { if (!edge(term.trueBlock)) return false; } + else if (term.kind == TerminatorKind::ConditionalBranch) { if (!metadata[index].condition || !edge(term.trueBlock) || !edge(term.falseBlock)) return false; } + else if (term.kind != TerminatorKind::Return) return false; + std::set expected; + for (const auto next : successors[index]) expected.insert(blocks[next]); + const std::set actual(blocks[index]->Successors().begin(), blocks[index]->Successors().end()); + if (actual != expected) return false; + } + std::vector colors(blocks.size()); + std::vector> pending{{0u, 0u}}; + colors[0u] = 1u; + while (!pending.empty()) { + auto& [block, edge] = pending.back(); + if (edge == successors[block].size()) { colors[block] = 2u; pending.pop_back(); continue; } + const auto next = successors[block][edge++]; + if (colors[next] == 1u) return false; + if (colors[next] == 2u) continue; + colors[next] = 1u; + pending.emplace_back(next, 0u); + } + return true; +} + +const IrValue* Other(const IrValue* value, std::uint32_t immediate) { + if (value->ArgumentCount() != 2u) return nullptr; + const IrValue* left = value->Argument(0)->Resolve(); + const IrValue* right = value->Argument(1)->Resolve(); + if (Immediate(left, immediate)) return right; + if (Immediate(right, immediate)) return left; + return nullptr; +} + +class MaskReplaySafety { +public: + explicit MaskReplaySafety(const IrProgram& program) : program(program), waveSize(program.WaveSize()) {} + + bool Check() { + if (program.Resources().stage != IrShaderStage::Pixel || (waveSize != 32u && waveSize != 64u) || !paths()) return false; + for (const auto* block : program.BlockOrder()) { + for (const auto* value : block->Instructions()) { + if (const auto mask = own(value)) { + if (!maskStable(*mask, 0u)) return false; + ownMasks.emplace(value, *mask); + } + } + } + for (const auto& info : program.Metadata().blockInfo) { + if (info.terminator.kind != TerminatorKind::ConditionalBranch) continue; + if (!control(info)) return false; + } + for (const auto& [value, mask] : ownMasks) if (helperObserved(value)) return false; + for (const auto* block : program.BlockOrder()) { + for (const auto* value : block->Instructions()) { + if ((value->Opcode() == IrOpcode::LaneId || value->Opcode() == IrOpcode::Ballot || value->Opcode() == IrOpcode::WqmU64) && !restricted.contains(value)) return false; + } + } + for (const auto* value : restricted) { + for (const auto& use : value->OperandUses()) { + if (restricted.contains(use.user) || ownMasks.contains(use.user) || use.user->Opcode() == IrOpcode::Reference || use.user->Opcode() == IrOpcode::ReferenceU32) continue; + return false; + } + } + return true; + } + +private: + enum class HelperMask : unsigned { Visiting, RealOnly, QuadStable, Uncertain }; + + void restrict(const IrValue* value) { + if (value && !value->HasImmediate()) restricted.insert(value); + } + + std::optional own(const IrValue* value) { + value = value->Resolve(); + if (value->Opcode() != IrOpcode::INotEqual32) return std::nullopt; + const IrValue* bit = Other(value, 0u); + if (!bit || bit->Opcode() != IrOpcode::BitwiseAnd32) return std::nullopt; + const IrValue* shifted = Other(bit, 1u); + if (!shifted || shifted->Opcode() != IrOpcode::ShiftRightLogical32 || shifted->ArgumentCount() != 2u) return std::nullopt; + const IrValue* amount = shifted->Argument(1)->Resolve(); + if (amount->Opcode() != IrOpcode::BitwiseAnd32) return std::nullopt; + const IrValue* lane = Other(amount, 31u); + if (!lane || lane->Opcode() != IrOpcode::LaneId) return std::nullopt; + const IrValue* word = shifted->Argument(0)->Resolve(); + ReplayMask result{word, word}; + if (word->Opcode() == IrOpcode::SelectU32 && word->ArgumentCount() == 3u) { + const IrValue* half = word->Argument(0)->Resolve(); + if (half->Opcode() != IrOpcode::ULessThan32 || half->ArgumentCount() != 2u || half->Argument(0)->Resolve()->Opcode() != IrOpcode::LaneId || !Immediate(half->Argument(1), 32u)) return std::nullopt; + restrict(half); + restrict(half->Argument(0)->Resolve()); + restrict(word); + result = ReplayMask{word->Argument(1)->Resolve(), word->Argument(2)->Resolve()}; + } else if (waveSize != 32u && !word->HasImmediate()) return std::nullopt; + restrict(bit); + restrict(shifted); + restrict(amount); + restrict(lane); + return result; + } + + const IrValue* ballotWord(const IrValue* word, std::uint32_t half) const { + if (word->Opcode() != IrOpcode::CompositeExtractU32x4 || word->ArgumentCount() != 2u || !Immediate(word->Argument(1), half)) return nullptr; + const IrValue* ballot = word->Argument(0)->Resolve(); + return ballot->Opcode() == IrOpcode::Ballot && ballot->ArgumentCount() == 1u ? ballot : nullptr; + } + + bool maskStable(ReplayMask mask, unsigned depth) { + if (depth > 128u) return false; + mask = ReplayMask{mask.low->Resolve(), mask.high->Resolve()}; + const auto key = std::pair{mask.low->Id(), waveSize == 32u ? mask.low->Id() : mask.high->Id()}; + if (const auto found = masks.find(key); found != masks.end()) return found->second == 2u; + masks[key] = 1u; + bool valid = false; + const auto finish = [&](bool accepted) { + masks[key] = accepted ? 2u : 3u; + if (accepted) { restrict(mask.low); if (waveSize != 32u) restrict(mask.high); } + return accepted; + }; + if ((Immediate(mask.low, 0u) && (waveSize == 32u || Immediate(mask.high, 0u))) || (Immediate(mask.low, 0xffffffffu) && (waveSize == 32u || Immediate(mask.high, 0xffffffffu)))) return finish(true); + const IrValue* lowBallot = ballotWord(mask.low, 0u); + const IrValue* highBallot = waveSize == 32u ? lowBallot : ballotWord(mask.high, 1u); + if (lowBallot && highBallot) { + valid = stable(lowBallot->Argument(0), depth + 1u) && stable(highBallot->Argument(0), depth + 1u); + if (valid) { restrict(lowBallot); restrict(highBallot); } + } else if (mask.low->Opcode() == IrOpcode::BitwiseAnd32 && (waveSize == 32u || mask.high->Opcode() == IrOpcode::BitwiseAnd32)) { + const auto pair = [&](unsigned low, unsigned high) { return ReplayMask{mask.low->Argument(low)->Resolve(), waveSize == 32u ? mask.low->Argument(low)->Resolve() : mask.high->Argument(high)->Resolve()}; }; + valid = (maskStable(pair(0u, 0u), depth + 1u) && maskStable(pair(1u, 1u), depth + 1u)) || (waveSize != 32u && maskStable(pair(0u, 1u), depth + 1u) && maskStable(pair(1u, 0u), depth + 1u)); + } else if (mask.low->Opcode() == IrOpcode::CompositeExtractU64 && mask.low->ArgumentCount() == 2u && Immediate(mask.low->Argument(1), 0u)) { + const IrValue* wqm = mask.low->Argument(0)->Resolve(); + const bool paired = waveSize == 32u || (mask.high->Opcode() == IrOpcode::CompositeExtractU64 && mask.high->ArgumentCount() == 2u && mask.high->Argument(0)->Resolve() == wqm && Immediate(mask.high->Argument(1), 1u)); + if (paired && wqm->Opcode() == IrOpcode::WqmU64 && wqm->ArgumentCount() == 1u) { + const IrValue* source = wqm->Argument(0)->Resolve(); + valid = source->Opcode() == IrOpcode::CompositeConstructU64 && source->ArgumentCount() == 2u && maskStable(ReplayMask{source->Argument(0)->Resolve(), source->Argument(1)->Resolve()}, depth + 1u); + if (valid) { + const auto provenance = helpers(ReplayMask{source->Argument(0)->Resolve(), source->Argument(1)->Resolve()}, depth + 1u); + valid = provenance == HelperMask::RealOnly || provenance == HelperMask::QuadStable; + } + if (valid) { restrict(wqm); restrict(source); } + } + } else if (mask.low->IsPhi() && mask.low->ArgumentCount() != 0u && mask.low->ArgumentCount() == mask.low->PhiBlockCount() && (waveSize == 32u || (mask.high->IsPhi() && mask.high->Parent() == mask.low->Parent() && mask.high->ArgumentCount() == mask.high->PhiBlockCount()))) { + valid = true; + for (std::size_t index = 0; index < mask.low->PhiBlockCount(); ++index) { + const IrValue* high = mask.low->Argument(index)->Resolve(); + if (waveSize != 32u) { + high = nullptr; + for (std::size_t highIndex = 0; highIndex < mask.high->PhiBlockCount(); ++highIndex) if (mask.high->PhiBlock(highIndex) == mask.low->PhiBlock(index)) high = mask.high->Argument(highIndex)->Resolve(); + } + if (!high || !maskStable(ReplayMask{mask.low->Argument(index)->Resolve(), high}, depth + 1u)) { valid = false; break; } + } + } + return finish(valid); + } + + std::optional onHelper(const IrValue* value, unsigned depth) { + if (depth > 128u) return std::nullopt; + value = value->Resolve(); + if (value->HasImmediate()) { + if (value->Type() == IrType::Bool) return value->ImmediateBool() ? 1u : 0u; + if (value->Type() == IrType::U32) return value->ImmediateU32(); + return std::nullopt; + } + if (const auto found = helperValues.find(value); found != helperValues.end()) return found->second; + helperValues.emplace(value, std::nullopt); + std::optional result; + const auto argument = [&](unsigned index) { return onHelper(value->Argument(index), depth + 1u); }; + if (value->Opcode() == IrOpcode::GetBuiltin && value->ArgumentCount() == 2u && Immediate(value->Argument(0), static_cast(StageInputKind::HelperInvocation)) && Immediate(value->Argument(1), 0u)) result = 1u; + else if (const auto mask = own(value)) { + if (helpers(*mask, depth + 1u) == HelperMask::RealOnly) result = 0u; + } else if (value->Opcode() == IrOpcode::LogicalNot && value->ArgumentCount() == 1u) { + if (const auto source = argument(0u)) result = *source == 0u ? 1u : 0u; + } else if (value->Opcode() == IrOpcode::LogicalAnd && value->ArgumentCount() == 2u) { + const auto left = argument(0u), right = argument(1u); + if ((left && *left == 0u) || (right && *right == 0u)) result = 0u; + else if (left && right) result = 1u; + } else if (value->Opcode() == IrOpcode::LogicalOr && value->ArgumentCount() == 2u) { + const auto left = argument(0u), right = argument(1u); + if ((left && *left != 0u) || (right && *right != 0u)) result = 1u; + else if (left && right) result = 0u; + } else if ((value->Opcode() == IrOpcode::IEqual32 || value->Opcode() == IrOpcode::INotEqual32) && value->ArgumentCount() == 2u) { + const auto left = argument(0u), right = argument(1u); + if (left && right) result = (*left == *right) == (value->Opcode() == IrOpcode::IEqual32) ? 1u : 0u; + } else if ((value->Opcode() == IrOpcode::SelectU1 || value->Opcode() == IrOpcode::SelectU32) && value->ArgumentCount() == 3u) { + if (const auto condition = argument(0u)) result = argument(*condition != 0u ? 1u : 2u); + else { + const auto left = argument(1u), right = argument(2u); + if (left && right && left == right) result = left; + } + } else if (value->IsPhi() && value->ArgumentCount() != 0u && value->ArgumentCount() == value->PhiBlockCount()) { + result = argument(0u); + for (unsigned index = 1u; result && index < value->ArgumentCount(); ++index) if (argument(index) != result) result = std::nullopt; + } + helperValues[value] = result; + return result; + } + + HelperMask helpers(ReplayMask mask, unsigned depth) { + if (depth > 128u) return HelperMask::Uncertain; + mask = ReplayMask{mask.low->Resolve(), mask.high->Resolve()}; + const auto key = std::pair{mask.low->Id(), waveSize == 32u ? mask.low->Id() : mask.high->Id()}; + if (const auto found = helperMasks.find(key); found != helperMasks.end()) return found->second; + helperMasks[key] = HelperMask::Visiting; + HelperMask result = HelperMask::Uncertain; + if (Immediate(mask.low, 0u) && (waveSize == 32u || Immediate(mask.high, 0u))) result = HelperMask::RealOnly; + else if (Immediate(mask.low, 0xffffffffu) && (waveSize == 32u || Immediate(mask.high, 0xffffffffu))) result = HelperMask::QuadStable; + else { + const IrValue* lowBallot = ballotWord(mask.low, 0u); + const IrValue* highBallot = waveSize == 32u ? lowBallot : ballotWord(mask.high, 1u); + if (lowBallot && highBallot) { + const auto low = onHelper(lowBallot->Argument(0), depth + 1u), high = onHelper(highBallot->Argument(0), depth + 1u); + if (low && high && *low == 0u && *high == 0u) result = HelperMask::RealOnly; + } else if (mask.low->Opcode() == IrOpcode::BitwiseAnd32 && (waveSize == 32u || mask.high->Opcode() == IrOpcode::BitwiseAnd32)) { + const auto pair = [&](unsigned low, unsigned high) { return ReplayMask{mask.low->Argument(low)->Resolve(), waveSize == 32u ? mask.low->Argument(low)->Resolve() : mask.high->Argument(high)->Resolve()}; }; + const auto combine = [&](unsigned firstHigh, unsigned secondHigh) { + const auto first = pair(0u, firstHigh), second = pair(1u, secondHigh); + if (!maskStable(first, depth + 1u) || !maskStable(second, depth + 1u)) return HelperMask::Uncertain; + const auto left = helpers(first, depth + 1u), right = helpers(second, depth + 1u); + if (left == HelperMask::RealOnly || right == HelperMask::RealOnly) return HelperMask::RealOnly; + if (left == HelperMask::QuadStable && right == HelperMask::QuadStable) return HelperMask::QuadStable; + return HelperMask::Uncertain; + }; + result = combine(0u, 1u); + if (waveSize != 32u && result != HelperMask::RealOnly) { + const auto swapped = combine(1u, 0u); + if (swapped == HelperMask::RealOnly || (swapped == HelperMask::QuadStable && result == HelperMask::Uncertain)) result = swapped; + } + } else if (mask.low->Opcode() == IrOpcode::CompositeExtractU64 && mask.low->ArgumentCount() == 2u && Immediate(mask.low->Argument(1), 0u)) { + const IrValue* wqm = mask.low->Argument(0)->Resolve(); + const bool paired = waveSize == 32u || (mask.high->Opcode() == IrOpcode::CompositeExtractU64 && mask.high->ArgumentCount() == 2u && mask.high->Argument(0)->Resolve() == wqm && Immediate(mask.high->Argument(1), 1u)); + if (paired && wqm->Opcode() == IrOpcode::WqmU64 && wqm->ArgumentCount() == 1u) { + const IrValue* source = wqm->Argument(0)->Resolve(); + if (source->Opcode() == IrOpcode::CompositeConstructU64 && source->ArgumentCount() == 2u) { + const auto input = helpers(ReplayMask{source->Argument(0)->Resolve(), source->Argument(1)->Resolve()}, depth + 1u); + if (input == HelperMask::RealOnly || input == HelperMask::QuadStable) result = HelperMask::QuadStable; + } + } + } else if (mask.low->IsPhi() && mask.low->ArgumentCount() != 0u && mask.low->ArgumentCount() == mask.low->PhiBlockCount() && (waveSize == 32u || (mask.high->IsPhi() && mask.high->Parent() == mask.low->Parent() && mask.high->ArgumentCount() == mask.high->PhiBlockCount()))) { + result = HelperMask::RealOnly; + for (std::size_t index = 0; index < mask.low->PhiBlockCount(); ++index) { + const IrValue* high = mask.low->Argument(index)->Resolve(); + if (waveSize != 32u) { + high = nullptr; + for (std::size_t highIndex = 0; highIndex < mask.high->PhiBlockCount(); ++highIndex) if (mask.high->PhiBlock(highIndex) == mask.low->PhiBlock(index)) high = mask.high->Argument(highIndex)->Resolve(); + } + const auto incoming = high ? helpers(ReplayMask{mask.low->Argument(index)->Resolve(), high}, depth + 1u) : HelperMask::Uncertain; + if (incoming == HelperMask::Uncertain || incoming == HelperMask::Visiting) { result = HelperMask::Uncertain; break; } + if (incoming == HelperMask::QuadStable) result = HelperMask::QuadStable; + } + } + } + helperMasks[key] = result; + return result; + } + + bool stable(const IrValue* value, unsigned depth) { + if (depth > 512u) return false; + value = value->Resolve(); + if (value->HasImmediate()) return true; + if (const auto found = stableValues.find(value); found != stableValues.end()) return found->second == 2u; + stableValues[value] = 1u; + bool accepted = false; + if (const auto mask = own(value)) { + accepted = maskStable(*mask, depth + 1u); + if (accepted) ownMasks.emplace(value, *mask); + } else if (value->Opcode() != IrOpcode::Ballot && value->Opcode() != IrOpcode::LaneId && value->Opcode() != IrOpcode::WqmU64) { + accepted = std::all_of(value->Arguments().begin(), value->Arguments().end(), [&](const IrValue* argument) { return stable(argument, depth + 1u); }); + } + stableValues[value] = accepted ? 2u : 3u; + return accepted; + } + + bool uniform(const IrValue* value, unsigned depth = 0) { + if (depth > 128u) return false; + value = value->Resolve(); + if (value->HasImmediate()) return true; + if (const auto found = uniformValues.find(value); found != uniformValues.end()) return found->second == 2u; + uniformValues[value] = 1u; + bool accepted = value->Opcode() == IrOpcode::GetUserData || value->Opcode() == IrOpcode::GetShaderBase || value->Opcode() == IrOpcode::GetSrtResource; + const bool transfer = Detail::IsRuntimeUniformOp(value->Opcode()) || value->Opcode() == IrOpcode::ReadConst || value->Opcode() == IrOpcode::FPOrdLessThan32 || value->Opcode() == IrOpcode::FPOrdGreaterThan32; + if (!accepted && transfer && !value->MayHaveSideEffects() && value->ArgumentCount() != 0u) { + accepted = std::all_of(value->Arguments().begin(), value->Arguments().end(), [&](const IrValue* argument) { return uniform(argument, depth + 1u); }); + } + uniformValues[value] = accepted ? 2u : 3u; + return accepted; + } + + bool paths() { + const auto& blocks = program.BlockOrder(); + const auto& metadata = program.Metadata().blockInfo; + if (blocks.empty() || blocks.size() != metadata.size()) return false; + for (std::size_t index = 0; index < blocks.size(); ++index) { + if (!indices.emplace(metadata[index].id, index).second) return false; + blockIndices.emplace(blocks[index], index); + } + for (std::size_t index = 0; index < blocks.size(); ++index) { + const auto& term = metadata[index].terminator; + if (term.loopHeader) return false; + std::set expected; + const auto target = [&](std::uint32_t id) { + const auto found = indices.find(id); + if (found == indices.end()) return false; + expected.insert(blocks[found->second]); + return true; + }; + if (term.kind == TerminatorKind::Branch) { if (!target(term.trueBlock)) return false; } + else if (term.kind == TerminatorKind::ConditionalBranch) { if (!metadata[index].condition || !target(term.trueBlock) || !target(term.falseBlock)) return false; } + else if (term.kind != TerminatorKind::Return) return false; + const std::set actual(blocks[index]->Successors().begin(), blocks[index]->Successors().end()); + if (actual != expected) return false; + } + std::vector path; + std::set visiting; + std::function visit = [&](std::size_t index) { + if (path.size() >= 128u || allPaths.size() >= 256u || !visiting.insert(blocks[index]).second) return false; + path.push_back(blocks[index]); + const auto& term = metadata[index].terminator; + bool valid = !term.loopHeader; + const auto edge = [&](std::uint32_t id) { const auto found = indices.find(id); return found != indices.end() && visit(found->second); }; + if (term.kind == TerminatorKind::Return) allPaths.push_back(path); + else if (term.kind == TerminatorKind::Branch) valid = valid && edge(term.trueBlock); + else if (term.kind == TerminatorKind::ConditionalBranch) valid = valid && metadata[index].condition && edge(term.trueBlock) && edge(term.falseBlock); + else valid = false; + path.pop_back(); + visiting.erase(blocks[index]); + return valid; + }; + return visit(0u); + } + + const IrValue* live(const std::vector& path) const { + const IrValue* result = nullptr; + for (const auto* block : path) { + for (const auto* instruction : block->Instructions()) { + if (instruction->Opcode() != IrOpcode::SetAttribute) continue; + const auto& info = program.Metadata().exportInfo.at(instruction->Flags().index); + if (info.vm) result = instruction->Argument(1)->Resolve(); + } + } + return result; + } + + bool guaranteed(const IrBlock* branch, ReplayMask mask, std::optional continuing = {}) { + for (const auto& path : allPaths) { + const auto found = std::find(path.begin(), path.end(), branch); + if (found == path.end()) continue; + const IrValue* kept = live(path); + if (Boolean(kept, false)) continue; + if (!kept || (continuing && (found + 1 == path.end() || *(found + 1) != *continuing)) || !GuardImpliesMaskOnPath(program, kept, mask.low, mask.high, path)) return false; + } + return true; + } + + std::optional any(const IrValue* value) { + value = value->Resolve(); + if (value->Opcode() != IrOpcode::INotEqual32) return std::nullopt; + const IrValue* words = Other(value, 0u); + if (!words || words->Opcode() != IrOpcode::BitwiseOr32 || words->ArgumentCount() != 2u) return std::nullopt; + ReplayMask mask{words->Argument(0)->Resolve(), words->Argument(1)->Resolve()}; + if (!maskStable(mask, 0u)) { + mask = ReplayMask{mask.high, mask.low}; + if (!maskStable(mask, 0u)) return std::nullopt; + } + restrict(words); + restrict(value); + return mask; + } + + bool sameValue(const IrValue* left, const IrValue* right) const { + left = left->Resolve(); + right = right->Resolve(); + return left == right || (left->HasImmediate() && right->HasImmediate() && left->Type() == IrType::U32 && right->Type() == IrType::U32 && left->ImmediateU32() == right->ImmediateU32()); + } + + bool inactiveEquals(const IrValue* value, const IrValue* other, const IrValue* predicate, unsigned depth = 0) { + if (depth > 32u) return false; + value = value->Resolve(); + if (sameValue(value, other)) return true; + if ((value->Opcode() == IrOpcode::SelectU32 || value->Opcode() == IrOpcode::SelectF32 || value->Opcode() == IrOpcode::SelectU1) && value->ArgumentCount() == 3u && (value->Argument(0)->Resolve() == predicate->Resolve() || GuardImplies(program, value->Argument(0), predicate))) return inactiveEquals(value->Argument(2), other, predicate, depth + 1u); + return false; + } + + bool quadObserved(const IrValue* value) const { + std::set visited; + std::vector pending{value}; + while (!pending.empty()) { + const IrValue* current = pending.back(); + pending.pop_back(); + if (!visited.insert(current).second) continue; + for (const auto& info : program.Metadata().blockInfo) if (info.condition && info.condition->Resolve() == current) return true; + for (const auto& use : current->OperandUses()) { + const auto opcode = use.user->Opcode(); + if (ImageOpcodeInfoOf(opcode).needsSampler || opcode == IrOpcode::Ballot || opcode == IrOpcode::WqmU64 || opcode == IrOpcode::ReadLane || opcode == IrOpcode::ReadFirstLane || opcode == IrOpcode::DppMoveU32 || opcode == IrOpcode::DppUpdateU32 || opcode == IrOpcode::SwizzleU32 || opcode == IrOpcode::PermuteU32 || opcode == IrOpcode::BpermuteU32 || opcode == IrOpcode::Permlane16U32 || opcode == IrOpcode::WriteLane || SharedAccessOf(opcode) != SharedAccess::None) return true; + if (opcode == IrOpcode::SetAttribute || opcode == IrOpcode::Reference || opcode == IrOpcode::ReferenceU32) continue; + pending.push_back(use.user); + } + } + return false; + } + + bool helperObserved(const IrValue* value) const { + std::set visited; + std::vector pending{value}; + while (!pending.empty()) { + const IrValue* current = pending.back(); + pending.pop_back(); + if (!visited.insert(current).second) continue; + for (const auto& info : program.Metadata().blockInfo) { + if (!info.condition || info.condition->Resolve() != current) continue; + const auto kind = info.terminator.condition; + if (kind != BranchCondition::ExecZero && kind != BranchCondition::ExecNonZero && kind != BranchCondition::VccZero && kind != BranchCondition::VccNonZero) return true; + } + for (const auto& use : current->OperandUses()) { + const auto opcode = use.user->Opcode(); + if (ImageOpcodeInfoOf(opcode).needsSampler) return true; + if (restricted.contains(use.user) || opcode == IrOpcode::SetAttribute || opcode == IrOpcode::Reference || opcode == IrOpcode::ReferenceU32) continue; + pending.push_back(use.user); + } + } + return false; + } + + bool optionalBody(const IrBlock* branch, const IrBlock* body, const IrBlock* merge, const IrValue* predicate) { + const auto& bodyInfo = program.Metadata().blockInfo[blockIndices.at(body)]; + if (bodyInfo.terminator.kind != TerminatorKind::Branch || indices.at(bodyInfo.terminator.trueBlock) != blockIndices.at(merge)) return false; + std::set merged; + for (const auto* instruction : merge->Instructions()) { + if (!instruction->IsPhi()) continue; + if (instruction->ArgumentCount() != 2u || instruction->PhiBlockCount() != 2u) return false; + const IrValue* skipped = nullptr; + const IrValue* taken = nullptr; + for (std::size_t index = 0; index < 2u; ++index) { + if (instruction->PhiBlock(index) == branch) skipped = instruction->Argument(index)->Resolve(); + if (instruction->PhiBlock(index) == body) taken = instruction->Argument(index)->Resolve(); + } + if (!skipped || !taken || !inactiveEquals(taken, skipped, predicate) || quadObserved(instruction)) return false; + merged.insert(instruction); + } + for (const auto* instruction : body->Instructions()) { + if (instruction->Opcode() == IrOpcode::SetAttribute || instruction->Opcode() == IrOpcode::Discard) return false; + for (const auto& use : instruction->OperandUses()) { + if (use.user->Parent() == body || merged.contains(use.user) || use.user->Opcode() == IrOpcode::Reference || use.user->Opcode() == IrOpcode::ReferenceU32) continue; + return false; + } + } + return !merged.empty(); + } + + bool control(const BlockInfo& info) { + const auto* branch = program.BlockOrder()[indices.at(info.id)]; + const auto* trueBlock = program.BlockOrder()[indices.at(info.terminator.trueBlock)]; + const auto* falseBlock = program.BlockOrder()[indices.at(info.terminator.falseBlock)]; + const IrValue* condition = info.condition->Resolve(); + const bool waveZero = info.terminator.condition == BranchCondition::ExecZero || info.terminator.condition == BranchCondition::VccZero; + const bool waveNonZero = info.terminator.condition == BranchCondition::ExecNonZero || info.terminator.condition == BranchCondition::VccNonZero; + if (waveZero || waveNonZero) { + const IrValue* predicate = condition; + if (waveZero) { + if (condition->Opcode() != IrOpcode::LogicalNot || condition->ArgumentCount() != 1u) return false; + predicate = condition->Argument(0)->Resolve(); + } + if (!stable(predicate, 0u)) return false; + if (uniform(predicate)) return true; + if (predicate->Opcode() == IrOpcode::LogicalAnd && predicate->ArgumentCount() == 2u) { + for (unsigned index = 0; index < 2u; ++index) { + const auto mask = own(predicate->Argument(index)); + if (mask && uniform(predicate->Argument(1u - index)) && guaranteed(branch, *mask)) return true; + } + } + const auto* body = waveZero ? falseBlock : trueBlock; + const auto* merge = waveZero ? trueBlock : falseBlock; + return optionalBody(branch, body, merge, predicate); + } + bool inverted = false; + if (condition->Opcode() == IrOpcode::LogicalNot && condition->ArgumentCount() == 1u) { condition = condition->Argument(0)->Resolve(); inverted = true; } + if (const auto mask = any(condition)) { + if (inverted) restrict(info.condition->Resolve()); + return guaranteed(branch, *mask, inverted ? falseBlock : trueBlock); + } + return stable(info.condition, 0u); + } + + const IrProgram& program; + std::uint32_t waveSize; + std::set restricted; + std::map ownMasks; + std::map, unsigned> masks; + std::map, HelperMask> helperMasks; + std::unordered_map> helperValues; + std::unordered_map stableValues; + std::unordered_map uniformValues; + std::map indices; + std::map blockIndices; + std::vector> allPaths; +}; + +std::optional> Values(const IrProgram& program, const IrValue* value, std::set& visiting, unsigned depth, std::optional component = {}, const IrValue* active = nullptr) { + if (value == nullptr || depth > 64u) return std::nullopt; + value = value->Resolve(); + if (!visiting.insert(value).second) return std::nullopt; + const auto finish = [&](std::optional> result) { + visiting.erase(value); + if (result && result->size() > 16u) return std::optional>{}; + return result; + }; + const auto child = [&](const IrValue* next, std::optional part = {}) { return Values(program, next, visiting, depth + 1u, part, active); }; + if (component) { + if (value->Opcode() == IrOpcode::CompositeConstructU32x4 && *component < value->ArgumentCount()) return finish(child(value->Argument(*component))); + return finish(std::nullopt); + } + if (value->HasImmediate() && value->Type() == IrType::U32) return finish(std::set{value->ImmediateU32() & 0xffu}); + if (value->Opcode() == IrOpcode::CompositeExtractU32x4 && value->ArgumentCount() == 2u && value->Argument(1)->Resolve()->HasImmediate()) return finish(child(value->Argument(0), value->Argument(1)->Resolve()->ImmediateU32())); + if (value->Opcode() == IrOpcode::SelectU32 || value->Opcode() == IrOpcode::Select) { + if (value->ArgumentCount() != 3u) return finish(std::nullopt); + if (active != nullptr && (Implies(active, value->Argument(0)) || GuardImplies(program, active, value->Argument(0)))) return finish(child(value->Argument(1))); + auto left = child(value->Argument(1)); + auto right = child(value->Argument(2)); + if (!left || !right) return finish(std::nullopt); + left->insert(right->begin(), right->end()); + return finish(std::move(left)); + } + if (value->Opcode() == IrOpcode::Phi) { + std::set result; + for (const auto* argument : value->Arguments()) { + const auto values = child(argument); + if (!values) return finish(std::nullopt); + result.insert(values->begin(), values->end()); + } + return finish(std::move(result)); + } + return finish(std::nullopt); +} + +} + +std::vector StencilReferenceValues(const IrProgram& program) { + std::set result; + for (const auto& block : program.Blocks()) { + for (const auto* instruction : block->Instructions()) { + if (instruction->Opcode() != IrOpcode::SetAttribute) continue; + const auto& info = program.Metadata().exportInfo.at(instruction->Flags().index); + if (info.kind != ExportTargetKind::MrtZ || (info.en & 2u) == 0u) continue; + std::set visiting; + const auto values = Values(program, instruction->Argument(0), visiting, 0u, 1u, instruction->Argument(1)); + if (!values) return {}; + result.insert(values->begin(), values->end()); + if (result.size() > 16u) return {}; + } + } + return {result.begin(), result.end()}; +} + +bool StencilReferenceDefinedForKeptInvocations(const IrProgram& program) { + const auto& blocks = program.BlockOrder(); + const auto& metadata = program.Metadata().blockInfo; + if (blocks.empty() || blocks.size() != metadata.size()) return false; + std::map indices; + for (std::size_t index = 0; index < metadata.size(); ++index) if (!indices.emplace(metadata[index].id, index).second) return false; + std::vector> successors(blocks.size()), predecessors(blocks.size()); + for (std::size_t index = 0; index < metadata.size(); ++index) { + const auto& term = metadata[index].terminator; + if (term.loopHeader || (term.kind != TerminatorKind::Branch && term.kind != TerminatorKind::ConditionalBranch && term.kind != TerminatorKind::Return)) return false; + const auto edge = [&](std::uint32_t id) { + const auto found = indices.find(id); + if (found == indices.end()) return false; + successors[index].push_back(found->second); + predecessors[found->second].push_back(index); + return true; + }; + if (term.kind == TerminatorKind::Branch && !edge(term.trueBlock)) return false; + if (term.kind == TerminatorKind::ConditionalBranch && (!edge(term.trueBlock) || !edge(term.falseBlock))) return false; + } + std::vector reached(blocks.size()); + std::function visit = [&](std::size_t index) { + if (reached[index] == 1u) return false; + if (reached[index] == 2u) return true; + reached[index] = 1u; + for (const auto next : successors[index]) if (!visit(next)) return false; + reached[index] = 2u; + return true; + }; + if (!visit(0u)) return false; + struct State { + std::set written; + const IrValue* live = nullptr; + bool liveKnown = true; + }; + std::vector> states(blocks.size()); + bool valid = true; + std::function resolve = [&](std::size_t index) -> const State& { + if (states[index]) return *states[index]; + State state; + bool first = true; + for (const auto previous : predecessors[index]) { + if (reached[previous] != 2u) continue; + const auto& incoming = resolve(previous); + if (first) { state = incoming; first = false; } + else { + std::set common; + std::set_intersection(state.written.begin(), state.written.end(), incoming.written.begin(), incoming.written.end(), std::inserter(common, common.begin())); + state.written = std::move(common); + state.liveKnown = state.liveKnown && incoming.liveKnown && state.live == incoming.live; + } + } + for (const auto* instruction : blocks[index]->Instructions()) { + if (instruction->Opcode() != IrOpcode::SetAttribute) continue; + const auto& info = program.Metadata().exportInfo.at(instruction->Flags().index); + const auto* active = instruction->Argument(1)->Resolve(); + if (info.kind == ExportTargetKind::MrtZ && (info.en & 2u) != 0u && !Boolean(active, false)) state.written.insert(Boolean(active, true) ? nullptr : active); + if (info.vm) { + state.live = Boolean(active, true) ? nullptr : active; + state.liveKnown = true; + } + } + if (metadata[index].terminator.kind == TerminatorKind::Return) { + const bool killed = state.liveKnown && Boolean(state.live, false); + const bool written = state.written.contains(nullptr) || (state.liveKnown && std::ranges::any_of(state.written, [&](const IrValue* guard) { return Implies(state.live, guard) || GuardImplies(program, state.live, guard); })); + if (!killed && !written) valid = false; + } + states[index] = std::move(state); + return *states[index]; + }; + for (std::size_t index = 0; index < blocks.size(); ++index) if (reached[index] == 2u) resolve(index); + return valid; +} + +bool StencilReplaySafe(const IrProgram& program) { + if (program.Info().sharedMemoryBytes != 0u || program.Info().bdaWrites || program.Info().usesDma || (program.Info().usesFaultBuffer && program.Resources().guardedSrtSlots.empty())) return false; + if (program.Info().usesFaultBuffer && !FaultControlFlowAcyclic(program)) return false; + for (const auto& input : program.Info().inputs) if (input.kind == StageInputKind::PrimitiveId) return false; + bool masks = false; + for (const auto& info : program.Metadata().blockInfo) { + if (info.terminator.loopHeader) return false; + if (info.terminator.kind != TerminatorKind::ConditionalBranch) continue; + const auto kind = info.terminator.condition; + masks = masks || kind == BranchCondition::ExecZero || kind == BranchCondition::ExecNonZero || kind == BranchCondition::VccZero || kind == BranchCondition::VccNonZero; + } + for (const auto& block : program.Blocks()) { + for (const auto* instruction : block->Instructions()) { + const auto opcode = instruction->Opcode(); + if (opcode == IrOpcode::Loop || opcode == IrOpcode::LoopMerge || opcode == IrOpcode::ImageBvhIntersectRay || BufferAccessOf(opcode) == BufferAccess::Atomic || AddressOpcodeInfoOf(opcode).access == AddressAccess::Write || AddressOpcodeInfoOf(opcode).access == AddressAccess::Atomic || SharedAccessOf(opcode) != SharedAccess::None || ImageOpcodeInfoOf(opcode).access == ImageAccess::Write || ImageOpcodeInfoOf(opcode).access == ImageAccess::Atomic) return false; + if (opcode == IrOpcode::UndefU1 || opcode == IrOpcode::UndefU8 || opcode == IrOpcode::UndefU16 || opcode == IrOpcode::UndefU32 || opcode == IrOpcode::UndefU64) return false; + if (opcode == IrOpcode::ShaderClock || opcode == IrOpcode::RealtimeClock || opcode == IrOpcode::ReadFirstLane || opcode == IrOpcode::ReadLane || opcode == IrOpcode::WriteLane || opcode == IrOpcode::DppMoveU32 || opcode == IrOpcode::DppUpdateU32 || opcode == IrOpcode::Permlane16U32 || opcode == IrOpcode::PermuteU32 || opcode == IrOpcode::BpermuteU32 || opcode == IrOpcode::SwizzleU32) return false; + masks = masks || opcode == IrOpcode::Ballot || opcode == IrOpcode::LaneId || opcode == IrOpcode::WqmU64; + if (!instruction->MayHaveSideEffects()) continue; + switch (opcode) { + case IrOpcode::Branch: + case IrOpcode::BranchConditional: + case IrOpcode::Return: + case IrOpcode::Unreachable: + case IrOpcode::Discard: + case IrOpcode::Reference: + case IrOpcode::ReferenceU32: + case IrOpcode::SetAttribute: + case IrOpcode::InterpolateHostP2: + case IrOpcode::SetRegister: + case IrOpcode::SetThreadBitScalarRegister: + case IrOpcode::SetScalarMaskTag: + case IrOpcode::SetScalarRegister: + case IrOpcode::SetVectorRegister: + case IrOpcode::StoreVectorWindow: + case IrOpcode::SetGotoVariable: + case IrOpcode::SetScc: + case IrOpcode::SetExec: + case IrOpcode::SetExecLo: + case IrOpcode::SetExecHi: + case IrOpcode::SetVcc: + case IrOpcode::SetVccLo: + case IrOpcode::SetVccHi: + case IrOpcode::SetM0: + case IrOpcode::Waitcnt: + case IrOpcode::InstPrefetch: + break; + case IrOpcode::Sendmsg: + if (program.Resources().stage != IrShaderStage::Vertex || instruction->ArgumentCount() != 0u) return false; + break; + default: + return false; + } + } + } + return !masks || MaskReplaySafety(program).Check(); +} + +} diff --git a/core/shader/recompiler/StencilReplay.hpp b/core/shader/recompiler/StencilReplay.hpp new file mode 100644 index 0000000000..2b45bf0cfd --- /dev/null +++ b/core/shader/recompiler/StencilReplay.hpp @@ -0,0 +1,15 @@ +#ifndef CORE_SHADER_RECOMPILER_STENCILREPLAY_HPP +#define CORE_SHADER_RECOMPILER_STENCILREPLAY_HPP + +#include "IntermediateRepresentation/IrProgram.hpp" +#include + +namespace ShaderRecompiler { + +[[nodiscard]] std::vector StencilReferenceValues(const IrProgram& program); +[[nodiscard]] bool StencilReplaySafe(const IrProgram& program); +[[nodiscard]] bool StencilReferenceDefinedForKeptInvocations(const IrProgram& program); + +} + +#endif diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index 711f219057..c174855b88 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -27,6 +27,7 @@ public: void SetNativeFloat16(bool enabled) { nativeFloat16 = enabled; } void AddBranchCondition(const BasicBlock& source, BlockInfo& info); void TranslateCodeTableLoad(const RdnaInstruction& instruction, const ControlFlowGraph::CodeTableLoad& table); + void TranslateUnlinkedCall(const RdnaInstruction& instruction); private: struct AddressOperands { @@ -152,6 +153,7 @@ private: bool dsAtomic64(const RdnaInstruction& inst, IrOpcode opcode, bool returnsValue); bool dsCondxchg32(const RdnaInstruction& inst); bool dsAppendConsume(const RdnaInstruction& inst, IrOpcode opcode); + bool dsOrderedCount(const RdnaInstruction& inst); bool dsAddtid(const RdnaInstruction& inst, bool write); bool globalAddtid(const RdnaInstruction& inst, bool write); bool dsSwizzleB32(const RdnaInstruction& inst); diff --git a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp index 18bb250332..9c67377bf0 100644 --- a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp @@ -283,6 +283,13 @@ void TranslationContext::sSwappcB64(const RdnaInstruction& inst) { writeU32Pair(inst.destination, extractU64(link)); } +void TranslationContext::TranslateUnlinkedCall(const RdnaInstruction& inst) { + const std::array target = readU32Pair(inst.source0); + (void)ir.Emit(IrOpcode::UnlinkedCall, IrType::Void, {&target[0].Value(), &target[1].Value(), &ir.Constant(inst.programCounter)}); + program.Info().usesFaultBuffer = true; + sSwappcB64(inst); +} + void TranslationContext::sCselectB32(const RdnaInstruction& inst) { const IrU32 trueValue = readU32(sourceAt(inst, 0u)); const IrU32 falseValue = readU32(sourceAt(inst, 1u)); diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index 8bd53ac8db..e29958a558 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -86,6 +86,10 @@ const ShaderWorkgroupInputInfo* shaderWorkgroupInput(ShaderStageKind stage, cons } } +bool isUnlinkedCall(const ControlFlowGraph& cfg, std::uint32_t programCounter) { + return std::find(cfg.unlinkedCallProgramCounters.begin(), cfg.unlinkedCallProgramCounters.end(), programCounter) != cfg.unlinkedCallProgramCounters.end(); +} + bool isCodeTableLoad(const ControlFlowGraph& cfg, std::uint32_t programCounter) { return std::find(cfg.codeTableLoadProgramCounters.begin(), cfg.codeTableLoadProgramCounters.end(), programCounter) != cfg.codeTableLoadProgramCounters.end(); } @@ -231,7 +235,7 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO const std::uint32_t waves = std::min((totalThreads + waveSize - 1u) / waveSize, 0x3fu); IrValue& localIndex = builtin(StageInputKind::LocalInvocationIndex); IrValue& waveId = entryIr.Emit(IrOpcode::UDiv32, IrOpcodeType(IrOpcode::UDiv32), {&localIndex, &entryIr.Constant(waveSize)}); - IrValue& waveBits = entryIr.ShiftLeftLogical(waveId, entryIr.Constant(20u)); + IrValue& waveBits = entryIr.BitwiseOr(entryIr.ShiftLeftLogical(waveId, entryIr.Constant(6u)), entryIr.ShiftLeftLogical(waveId, entryIr.Constant(20u))); IrValue& firstBit = entryIr.Select(entryIr.IEqual(waveId, entryIr.Constant(0u)), entryIr.Constant(0x80000000u), entryIr.Constant(0u)); entryIr.SetScalarReg(static_cast(cs->workgroupRegister + regOffset), entryIr.BitwiseOr(entryIr.BitwiseOr(waveBits, entryIr.Constant(waves)), firstBit)); } @@ -363,7 +367,9 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO setVector(static_cast(vgpr(PixelInput::PositionW)), entryIr.BitCastU32(w)); } if (loaded(PixelInput::FrontFace)) { - setVector(static_cast(vgpr(PixelInput::FrontFace)), builtin(StageInputKind::FrontFacing)); + // The front value under FRONT_FACE_ALL_BITS is not measured (docs/dev/debt/shader.md). + IrValue& facing = builtin(StageInputKind::FrontFacing); + setVector(static_cast(vgpr(PixelInput::FrontFace)), ps->psFrontFaceAllBits ? entryIr.Select(entryIr.IEqual(facing, entryIr.Constant(0x3f800000u)), entryIr.Constant(0xffffffffu), entryIr.Constant(0u)) : facing); } if (loaded(PixelInput::Ancillary)) { setVector(static_cast(vgpr(PixelInput::Ancillary)), builtin(StageInputKind::PackedAncillary)); @@ -534,6 +540,10 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con context.TranslateCodeTableLoad(instruction, *table); continue; } + if (isUnlinkedCall(cfg, instruction.programCounter)) { + context.TranslateUnlinkedCall(instruction); + continue; + } if (options.embeddedFetch != nullptr) { const auto load = std::ranges::find(options.embeddedFetch->loads, instruction.programCounter, &EmbeddedFetchLoad::programCounter); if (load != options.embeddedFetch->loads.end()) { diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryInstructionDispatch.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryInstructionDispatch.cpp index 276a3fbae4..b0680c202a 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryInstructionDispatch.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryInstructionDispatch.cpp @@ -502,7 +502,7 @@ bool TranslationContext::emitMemory(const RdnaInstruction& inst) { case RdnaOpcode::DsGwsBarrier: throw std::runtime_error(std::string(globalWaveSyncName(inst.op)) + " at pc " + std::to_string(inst.programCounter) + ": global wave sync barriers and semaphores shared by waves of different workgroups are not modeled"); case RdnaOpcode::DsOrderedCount: - throw std::runtime_error("ds_ordered_count at pc " + std::to_string(inst.programCounter) + ": GDS counters updated in wave launch order are not modeled"); + return dsOrderedCount(inst); case RdnaOpcode::DsWriteAddtidB32: return dsAddtid(inst, true); case RdnaOpcode::DsReadAddtidB32: diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp index 04115cbb32..b78f01f6d2 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp @@ -33,6 +33,7 @@ MemoryInfo sharedMemoryInfoFromInstruction(const RdnaInstruction& inst) { bool TranslationContext::dsAtomic(const RdnaInstruction& inst, IrOpcode opcode, bool returnsValue) { MemoryInfo memory = sharedMemoryInfoFromInstruction(inst); memory.flushDenormals = sharedFloatFlush(opcode); + memory.flushDenormalResults = opcode == IrOpcode::SharedAtomicFAdd32 && (!floatMode.has_value() || ((floatMode->floatMode >> 5u) & 1u) == 0u); const IrU32 address = readU32(inst.source0); const IrU32 value = readU32(inst.source1); IrValue& active = ir.GetExec(); @@ -217,6 +218,7 @@ bool TranslationContext::sharedFloatFlush(IrOpcode opcode) const { case IrOpcode::SharedAtomicFMin32: case IrOpcode::SharedAtomicFMax32: case IrOpcode::SharedAtomicCmpstF32: + case IrOpcode::SharedAtomicFAdd32: return !floatMode.has_value() || ((floatMode->floatMode >> 4u) & 1u) == 0u; case IrOpcode::SharedAtomicFMin64: case IrOpcode::SharedAtomicFMax64: @@ -293,6 +295,24 @@ bool TranslationContext::dsAppendConsume(const RdnaInstruction& inst, IrOpcode o return true; } +bool TranslationContext::dsOrderedCount(const RdnaInstruction& inst) { + const std::uint32_t flags = (inst.memoryOffset >> 8u) & 0xffu; + const auto location = "ds_ordered_count at pc " + std::to_string(inst.programCounter); + if (!inst.gds) throw std::runtime_error(location + " without GDS is not modeled"); + if ((flags & 0x10u) != 0u) throw std::runtime_error(location + ": ordered swap is not modeled"); + if (((flags >> 6u) & 3u) != 0u) throw std::runtime_error(location + ": counting more than one dword is not modeled"); + MemoryInfo memory = sharedMemoryInfoFromInstruction(inst); + memory.offset = inst.memoryOffset & 0xffu; + IrValue& address = ir.Constant(0u); + const IrU32 amount = readU32(inst.source0); + IrValue& active = ir.GetExec(); + IrValue& activeLo = ir.GetExecLo(); + IrValue& activeHi = ir.GetExecHi(); + IrValue& result = ir.Emit(IrOpcode::DataOrderedCount, IrOpcodeType(IrOpcode::DataOrderedCount), {&address, &active, &activeLo, &activeHi, &amount.Value()}, addMemoryInfo(memory, inst.programCounter)); + writeOperand(inst.destination, &result); + return true; +} + bool TranslationContext::dsAddtid(const RdnaInstruction& inst, bool write) { const MemoryInfo memory = sharedMemoryInfoFromInstruction(inst); IrValue& base = ir.BitwiseAnd(readU32(makeM0Operand()).Value(), ir.Constant(0xffffu)); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 3e1f574040..5de4543f7b 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -133,6 +133,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psPosZ = pixel.posZ; pixelStorage.psPosW = pixel.posW; pixelStorage.psFrontFace = pixel.frontFace; + pixelStorage.psFrontFaceAllBits = pixel.frontFaceAllBits; pixelStorage.psAncillary = pixel.ancillary; pixelStorage.psNoPerspective = pixel.noPerspective; pixelStorage.psPixelKillEnable = pixel.pixelKillEnable; @@ -147,6 +148,8 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues if (pixel.ditheredAlphaToCoverage && !pixel.alphaToCoverage) throw std::runtime_error("dithered alpha-to-coverage requires enabled coverage"); pixelStorage.psAlphaToCoverage = pixel.alphaToCoverage; pixelStorage.psDitheredAlphaToCoverage = pixel.ditheredAlphaToCoverage; + pixelStorage.quadPixelMask = pixel.quadPixelMask; + pixelStorage.quadSampleMask = pixel.quadSampleMask; ShaderStageInputInfo result; result.pixel = &pixelStorage; return result; @@ -184,7 +187,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues if (stage == ShaderStageKind::Local || stage == ShaderStageKind::TessellationControl || stage == ShaderStageKind::TessellationEvaluation) { if (tessellation == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: tessellation configuration is missing"); if (tessellation->inputControlPoints == 0u || tessellation->inputControlPoints > 32u || tessellation->outputControlPoints == 0u || tessellation->outputControlPoints > 32u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation control-point counts"); - if ((tessellation->domain != 1u && tessellation->domain != 2u) || (tessellation->partitioning != 0u && tessellation->partitioning != 2u) || (tessellation->outputTopology != 2u && tessellation->outputTopology != 3u)) throw std::runtime_error("ShaderInputInfoBuilder: unsupported tessellation configuration"); + if ((tessellation->domain != 1u && tessellation->domain != 2u) || (tessellation->partitioning != 0u && tessellation->partitioning != 2u && tessellation->partitioning != 3u) || (tessellation->outputTopology != 2u && tessellation->outputTopology != 3u)) throw std::runtime_error("ShaderInputInfoBuilder: unsupported tessellation configuration"); if (tessellation->patchesPerGroup == 0u || tessellation->patchesPerGroup > 256u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation patches per group"); auto& target = vertexStorage.tess; target.inputControlPoints = tessellation->inputControlPoints; @@ -195,6 +198,8 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues target.patchesPerGroup = tessellation->patchesPerGroup; target.primitiveGroupSize = tessellation->primitiveGroupSize; target.breakAtInstance = tessellation->breakAtInstance; + target.instancePatches = tessellation->instancePatches; + target.drawParameters = tessellation->drawParameters; } if (stage == ShaderStageKind::Mesh) { if (mesh == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: a mesh-stage program has no mesh configuration"); diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index c60d90b112..5433a1a88c 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -45,6 +45,10 @@ void Require(bool condition) { if (!condition) std::abort(); } +void InsertQuadMaskDefaults(std::vector& bytes) { + bytes.insert(bytes.begin() + 261u, {0x0fu, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u}); +} + std::vector DecodeBase64(std::string_view text) { constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; std::vector bytes; @@ -202,9 +206,11 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 25u; - if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u}); + upgraded[4] = 29u; + if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); + if (fixture.pixel) InsertQuadMaskDefaults(upgraded); if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 5u, 0u); + upgraded.insert(upgraded.end(), sizeof(std::uint64_t), 0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); auto truncated = bytes; truncated.pop_back(); @@ -239,9 +245,11 @@ int main() { layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; } Require(layerRejected); - layered[4] = 25u; - layered.insert(layered.begin() + 259u, 0u); + layered[4] = 29u; + layered.insert(layered.begin() + 259u, {0u, 0u}); + InsertQuadMaskDefaults(layered); if (fixture.tessellation) layered.insert(layered.end() - 35u, 5u, 0u); + layered.insert(layered.end(), sizeof(std::uint64_t), 0u); Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered)); } } @@ -262,8 +270,10 @@ int main() { Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); Require(replay.request.target.float16DenormPreserve == preserve); - bytes[4] = 25u; - bytes.insert(bytes.begin() + 259u, 0u); + bytes[4] = 29u; + bytes.insert(bytes.begin() + 259u, {0u, 0u}); + InsertQuadMaskDefaults(bytes); + bytes.insert(bytes.end(), sizeof(std::uint64_t), 0u); Require(serializer.Serialize(replay.request) == EncodeBase64(bytes)); auto truncated23 = DecodeBase64(encoded23); @@ -384,8 +394,10 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 25u; - if (fixture.pixel) upgraded.insert(upgraded.begin() + 259u, 0u); + upgraded[4] = 29u; + if (fixture.pixel) upgraded.insert(upgraded.begin() + 259u, {0u, 0u}); + if (fixture.pixel) InsertQuadMaskDefaults(upgraded); + upgraded.insert(upgraded.end(), sizeof(std::uint64_t), 0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); } } @@ -398,9 +410,9 @@ int main() { const auto replay = serializer.Deserialize(encoded); Require(replay.request.target.float16DenormPreserve == preserve); auto bytes = DecodeBase64(encoded); - Require(bytes.size() >= 8u && bytes[4] == 25u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + Require(bytes.size() >= 8u && bytes[4] == 29u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); auto future = bytes; - future[4] = 26u; + future[4] = 30u; bool futureRejected = false; try { static_cast(serializer.Deserialize(EncodeBase64(future))); diff --git a/core/shader/recompiler/tests/test_shader_cache_native_paths.py b/core/shader/recompiler/tests/test_shader_cache_native_paths.py new file mode 100644 index 0000000000..b2f1e8c57f --- /dev/null +++ b/core/shader/recompiler/tests/test_shader_cache_native_paths.py @@ -0,0 +1,78 @@ +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +SCRIPT = Path(sys.argv.pop(1)).resolve() +CMAKE = sys.argv.pop(1) + + +class ShaderCachePathTests(unittest.TestCase): + def fixture(self, root): + source = root / 'recompiler' / 'input.cpp' + source.parent.mkdir(parents=True) + source.write_text('const char* mode = "APS5_TEST_PATH";\n', encoding='utf-8') + listing = root / 'sources.txt' + listing.write_text(f'sources {source.as_posix()}\n', encoding='utf-8') + output = root / 'version.hpp' + return source, listing, output + + def run_script(self, root, listing, output): + return subprocess.run([CMAKE, f'-DROOT={root.as_posix()}', + f'-DRECOMPILER_DIR={(root / "recompiler").as_posix()}', + f'-DLIST_FILE={listing.as_posix()}', f'-DOUTPUT={output.as_posix()}', + '-P', str(SCRIPT)], capture_output=True, encoding='utf-8', errors='replace', timeout=10) + + def check_source_change(self, name): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) / name + source, listing, output = self.fixture(root) + result = self.run_script(root, listing, output) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + previous = output.read_bytes() + self.assertIn(b'APS5_TEST_PATH', previous) + source.write_text('const char* mode = "APS5_TEST_CHANGED";\n', encoding='utf-8') + result = self.run_script(root, listing, output) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertNotEqual(previous, output.read_bytes()) + self.assertIn(b'APS5_TEST_CHANGED', output.read_bytes()) + unchanged = output.stat().st_mtime_ns + result = self.run_script(root, listing, output) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(output.stat().st_mtime_ns, unchanged) + + def test_ascii(self): + self.check_source_change('plain path') + + def test_cyrillic(self): + self.check_source_change('\u0441\u0431\u043e\u0440\u043a\u0430 \u0438\u0433\u0440\u044b') + + def test_cjk(self): + self.check_source_change('\u6e2c\u8a66 path') + + def test_unicode_guard_rejects_unhashed_include(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) / '\u0441\u0431\u043e\u0440\u043a\u0430' + source, listing, output = self.fixture(root) + (root / 'shared.hpp').write_text('int example();\n', encoding='utf-8') + source.write_text('#include "../shared.hpp"\n', encoding='utf-8') + result = self.run_script(root, listing, output) + self.assertNotEqual(result.returncode, 0) + self.assertIn('does not hash', result.stdout + result.stderr) + self.assertFalse(output.exists()) + + def test_unicode_header_rejects_unhashed_include(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) / 'plain path' + source, listing, output = self.fixture(root) + name = '\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a.hpp' + (root / name).write_text('int example();\n', encoding='utf-8') + source.write_text(f'#include "../{name}"\n', encoding='utf-8') + result = self.run_script(root, listing, output) + self.assertNotEqual(result.returncode, 0) + self.assertIn('does not hash', result.stdout + result.stderr) + self.assertFalse(output.exists()) + + +unittest.main() diff --git a/demo/web/app.mjs b/demo/web/app.mjs index ca615cf880..79fc9f901d 100644 --- a/demo/web/app.mjs +++ b/demo/web/app.mjs @@ -1,11 +1,15 @@ +import { abortAndDiscardOutput, OutputDirectoryTransaction } from "./output-directory.mjs"; + const $ = id => document.getElementById(id); const stages = ["Converting executable", "Converting PRX modules", "Scanning syscalls", "Scanning instructions", "Converting instructions", "Analysing imports", "Building output image", "Writing output"]; let worker; let file; let summary; let working = false; +let failing = false; let sink; let outputDirectory; +let outputTransaction; let downloadUrl; let logs = []; let release; @@ -29,15 +33,20 @@ function update() { folderOption.disabled = !canStream || target === "windows"; folderOption.textContent = target === "windows" ? "Save to folder · unavailable for Windows (DLL restrictions)" : "Save to folder · uncompressed"; const libraryBytes = release?.platforms[target]?.expandedBytes; - const estimate = summary && libraryBytes !== undefined ? Math.ceil(summary.totalBytes * 1.02) + summary.selectedBytes * 4 + libraryBytes + summary.files * 1024 + 8 * 1024 * 1024 : null; + const selectedBytes = summary ? target === "windows" ? summary.windowsSelectedBytes : summary.selectedBytes : null; + const conversionTooLarge = selectedBytes !== null && selectedBytes > 256 * 1024 * 1024; + const iconTooLarge = target === "windows" && Boolean(summary?.windowsIconTooLarge); + const conversionBlocked = conversionTooLarge || iconTooLarge; + const estimate = summary && libraryBytes !== undefined ? Math.ceil(summary.totalBytes * 1.02) + selectedBytes * 4 + libraryBytes + summary.files * 1024 + 8 * 1024 * 1024 : null; const tooLarge = estimate !== null && estimate > 256 * 1024 * 1024; $("save").querySelector('[value="memory"]').disabled = Boolean(tooLarge); const memoryBlocked = $("save").value === "memory" && tooLarge; - $("save-note").textContent = $("save").value === "disk" ? "Creates a subfolder with the executable, libs and app0. No ZIP compression. Browsers may block DLL and other restricted files; use ZIP for those apps." : $("save").value === "zip" ? "Writes a compressed ZIP directly to disk without holding the whole archive in memory." : "Up to 256 MiB including expanded files and output overhead."; - $("browser-note").hidden = supported && canStream && canSaveZip && !memoryBlocked && !folderBlocked; - $("browser-note").textContent = !supported ? "This browser or connection does not support the converter. Use a current browser over HTTPS, or use the native relinker." : folderBlocked ? "Browsers block direct DLL creation. Windows output requires ZIP. Choose Save ZIP to disk or Download in memory." : memoryBlocked ? "This archive exceeds the memory-download limit. Choose Save ZIP to disk. If that option is unavailable, open this page in desktop Chrome or Edge." : "Some direct disk saving options are unavailable in this browser or context. Small apps can still be downloaded in memory. For larger apps, use desktop Chrome or Edge over HTTPS."; - $("convert").disabled = !supported || working || !summary || !release || memoryBlocked || folderBlocked || (summary.warnings.length > 0 && !$("acknowledge").checked); - $("ready-note").textContent = summary ? (folderBlocked ? "Choose a ZIP save mode for Windows." : memoryBlocked ? "A direct disk save is required for this archive." : release ? `Libraries: ${release.tag} · ${$("save").value === "disk" ? "Uncompressed folder" : "ZIP compression level 9"}` : "Loading release information…") : "Choose a ZIP or folder to get started."; + if (summary) $("archive-info").textContent = `${summary.files} files · ${bytes(summary.totalBytes)} expanded · ${summary.modules} modules · ${bytes(selectedBytes)} ${target} conversion input` + (summary.root ? ` · Root: ${summary.root}` : ""); + $("save-note").textContent = $("save").value === "disk" ? "Creates a subfolder with the executable, libs and app0. No ZIP compression. Incomplete output folders are removed after cancellation or failure. Browsers may block DLL and other restricted files; use ZIP for those apps." : $("save").value === "zip" ? "Writes a compressed ZIP directly to disk without holding the whole archive in memory." : "Up to 256 MiB including expanded files and output overhead."; + $("browser-note").hidden = supported && canStream && canSaveZip && !memoryBlocked && !folderBlocked && !conversionBlocked; + $("browser-note").textContent = !supported ? "This browser or connection does not support the converter. Use a current browser over HTTPS, or use the native relinker." : folderBlocked ? "Browsers block direct DLL creation. Windows output requires ZIP. Choose Save ZIP to disk or Download in memory." : iconTooLarge ? "The Windows icon exceeds 16 MiB. Choose Linux or use the native relinker." : conversionTooLarge ? `This ${target} conversion input exceeds 256 MiB. Use the native relinker for this app.` : memoryBlocked ? "This archive exceeds the memory-download limit. Choose Save ZIP to disk. If that option is unavailable, open this page in desktop Chrome or Edge." : "Some direct disk saving options are unavailable in this browser or context. Small apps can still be downloaded in memory. For larger apps, use desktop Chrome or Edge over HTTPS."; + $("convert").disabled = !supported || working || !summary || !release || conversionBlocked || memoryBlocked || folderBlocked || (summary.warnings.length > 0 && !$("acknowledge").checked); + $("ready-note").textContent = summary ? (folderBlocked ? "Choose a ZIP save mode for Windows." : iconTooLarge ? "The Windows icon exceeds 16 MiB. Choose Linux or use the native relinker." : conversionTooLarge ? `The ${target} conversion input exceeds 256 MiB. Use the native relinker.` : memoryBlocked ? "A direct disk save is required for this archive." : release ? `Libraries: ${release.tag} · ${$("save").value === "disk" ? "Uncompressed folder" : "ZIP compression level 9"}` : "Loading release information…") : "Choose a ZIP or folder to get started."; } function lock(value) { @@ -45,22 +54,30 @@ function lock(value) { } async function fail(message) { + if (failing) return; + failing = true; worker?.terminate(); worker = null; - working = false; + working = true; summary = null; - if (sink) { - const active = sink; - sink = null; - try { await active.abort(); } catch (error) { message += `\nCould not discard the output file: ${error.message}`; } + const transaction = outputTransaction; + outputTransaction = null; + outputDirectory = null; + if (transaction) { + $("status").hidden = false; + $("phase").textContent = "Removing incomplete output folder"; } - if (outputDirectory) { message += `\nIncomplete files remain in ${outputDirectory.name}/.`; outputDirectory = null; } + const activeSink = sink; + sink = null; + for (const failure of await abortAndDiscardOutput(transaction, activeSink)) message += `\n${failure}`; $("error").textContent = message; $("error").hidden = false; $("status").hidden = true; $("cancel").hidden = true; $("drop").hidden = false; $("file").value = ""; + working = false; + failing = false; lock(false); update(); } @@ -90,7 +107,6 @@ async function onMessage({ data }) { $("selection").hidden = false; $("drop").hidden = true; $("archive-name").textContent = file.name; - $("archive-info").textContent = `${summary.files} files · ${bytes(summary.totalBytes)} expanded · ${summary.modules} modules · ${bytes(summary.selectedBytes)} conversion input` + (summary.root ? ` · Root: ${summary.root}` : ""); $("warning-list").replaceChildren(...summary.warnings.map(message => { const item = document.createElement("li"); item.textContent = message; return item; })); $("warnings").hidden = !summary.warnings.length; $("status").hidden = true; @@ -137,11 +153,13 @@ async function onMessage({ data }) { } else if (data.mode === "zip") { $("result-info").textContent += " · ZIP saved to disk"; } else { - $("result-info").textContent += ` · Saved in ${outputDirectory.name}/`; + $("result-info").textContent += ` · Saved in ${outputTransaction.name}/`; if (document.querySelector('input[name="target"]:checked').value === "linux") $("result-info").textContent += ` · On Linux, run chmod +x "${data.outputName}" before launching.`; } $("result-note").hidden = data.mode === "disk"; outputDirectory = null; + outputTransaction?.commit(); + outputTransaction = null; summary = null; worker.terminate(); worker = null; @@ -202,11 +220,8 @@ $("convert").onclick = async () => { } else if ($("save").value === "disk") { const parent = await window.showDirectoryPicker({ mode: "readwrite" }); const name = file.name.replace(/\.zip$/iu, "") + "-" + document.querySelector('input[name="target"]:checked').value; - let exists = false; - try { await parent.getDirectoryHandle(name); exists = true; } - catch (error) { if (error.name !== "NotFoundError") throw error; } - if (exists) throw new Error(`The folder ${name} already exists. Choose another destination or rename the existing folder.`); - outputDirectory = await parent.getDirectoryHandle(name, { create: true }); + outputTransaction = await OutputDirectoryTransaction.create(parent, name); + outputDirectory = outputTransaction.directory; } $("status").hidden = false; $("cancel").hidden = false; diff --git a/demo/web/archive.mjs b/demo/web/archive.mjs index 20c9584823..f1a30d2e42 100644 --- a/demo/web/archive.mjs +++ b/demo/web/archive.mjs @@ -18,7 +18,7 @@ class BoundedBlobReader extends BlobReader { export function validPath(name) { if (!name || name.includes("\\") || /[\x00-\x1f<>:"|?*]/u.test(name)) throw new Error(`Unsafe ZIP path: ${name}`); const parts = name.replace(/\/$/u, "").split("/"); - if (parts.some(part => !part || part === "." || part === ".." || /[. ]$/u.test(part) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu.test(part))) throw new Error(`Unsafe ZIP path: ${name}`); + if (parts.some(part => !part || part === "." || part === ".." || /[. ]$/u.test(part) || /^(con|prn|aux|nul|com[1-9\u00b9\u00b2\u00b3]|lpt[1-9\u00b9\u00b2\u00b3])(?:\.|$)/iu.test(part))) throw new Error(`Unsafe ZIP path: ${name}`); return name; } @@ -27,8 +27,8 @@ function safeSize(value) { return value; } -async function signature(entry) { - if (entry.uncompressedSize < 4) return false; +async function headerBytes(entry) { + if (entry.uncompressedSize < 4) return null; const stop = new AbortController(); const header = new Uint8Array(4); let filled = 0; @@ -44,7 +44,20 @@ async function signature(entry) { } catch (error) { if (!stop.signal.aborted || filled !== 4) throw error; } - return header[0] === 0x7f && header[1] === 69 && header[2] === 76 && header[3] === 70; + return header; +} + +const SELF_MAGICS = [ + [0x4f, 0x15, 0x3d, 0x1d], + [0x54, 0x14, 0xf5, 0xee] +]; + +function isElf(header) { + return header !== null && header[0] === 0x7f && header[1] === 69 && header[2] === 76 && header[3] === 70; +} + +function isSelf(header) { + return header !== null && SELF_MAGICS.some(magic => magic.every((byte, index) => header[index] === byte)); } async function boundedBytes(entry, limit) { @@ -119,41 +132,60 @@ export class Archive { const files = this.entries.filter(entry => !entry.directory); while (files.every(entry => entry.filename.slice(prefix.length).includes("/"))) { const first = files[0].filename.slice(prefix.length).split("/")[0]; - if (["sce_sys", "sce_module"].includes(first) || !this.entries.every(entry => entry.filename === prefix || entry.filename.startsWith(prefix + first + "/"))) break; + if (["sce_sys", "sce_module"].includes(first) || !this.entries.every(entry => (entry.directory && prefix.startsWith(entry.filename)) || entry.filename.startsWith(prefix + first + "/"))) break; prefix += first + "/"; } this.prefix = prefix; this.files = this.entries.filter(entry => entry.filename.startsWith(prefix) && entry.filename !== prefix).map(entry => ({ entry, name: entry.filename.slice(prefix.length) })); const executables = []; this.modules = []; + const selfContainers = []; + const selfModules = []; for (let index = 0; index < this.files.length; index++) { const item = this.files[index]; report({ phase: "Inspecting files", input: item.name, completed: index, total: this.files.length }); if (item.entry.directory || item.name.startsWith("sce_sys/")) continue; if (!item.name.startsWith("sce_module/") && item.name.includes("/")) continue; if (item.name.startsWith("sce_module/") && item.name.endsWith(".guest.prx")) continue; - const elf = await signature(item.entry); + const header = await headerBytes(item.entry); if (item.name.startsWith("sce_module/")) { - if (elf) this.modules.push(item); - } else if (elf) executables.push(item); + if (isElf(header)) this.modules.push(item); + else if (isSelf(header)) selfModules.push(item.name); + } else if (isElf(header)) { + executables.push(item); + } else if (isSelf(header)) { + selfContainers.push(item.name); + } + } + if (executables.length !== 1) { + if (selfContainers.length > 0) throw new Error(`${selfContainers.join(", ")} is a SELF container, not an ELF: unwrap the app before converting it`); + throw new Error(`Expected exactly one executable ELF in the app root; found ${executables.length}`); } - if (executables.length !== 1) throw new Error(`Expected exactly one executable ELF in the app root; found ${executables.length}`); this.executable = executables[0]; if (this.executable.name.includes("/")) throw new Error("Place the executable next to sce_sys and sce_module, not inside another folder"); this.icon = this.files.find(item => item.name === "sce_sys/icon0.png" && !item.entry.directory); - if (this.icon && this.icon.entry.uncompressedSize > ICON_LIMIT) throw new Error("sce_sys/icon0.png exceeds 16 MiB"); - this.selectedBytes = [this.executable, ...this.modules, ...(this.icon ? [this.icon] : [])].reduce((sum, item) => sum + item.entry.uncompressedSize, 0); - if (this.selectedBytes > CONVERSION_LIMIT) throw new Error("Executable, modules and icon exceed the 256 MiB conversion limit. Use the native relinker for this app."); + this.selectedBytes = [this.executable, ...this.modules].reduce((sum, item) => sum + item.entry.uncompressedSize, 0); + this.windowsSelectedBytes = this.selectedBytes + (this.icon?.entry.uncompressedSize ?? 0); + if (this.selectedBytes > CONVERSION_LIMIT) throw new Error("Executable and modules exceed the 256 MiB conversion limit. Use the native relinker for this app."); const warnings = []; if (!this.files.some(item => item.name.startsWith("sce_sys/"))) warnings.push("sce_sys is missing. The Windows executable will have no app icon."); else if (!this.icon) warnings.push("sce_sys/icon0.png is missing. The Windows executable will have no app icon."); if (!this.files.some(item => item.name.startsWith("sce_module/"))) warnings.push("sce_module is missing. If the original app included this folder, leaving it out can prevent the app from starting."); + if (selfModules.length > 0) warnings.push(`${selfModules.join(", ")} is a SELF container and was skipped: unwrap it to include that module.`); this.totalBytes = total; - return { name: this.executable.name, files: files.length, modules: this.modules.length, totalBytes: total, selectedBytes: this.selectedBytes, root: prefix, warnings }; + return { name: this.executable.name, files: files.length, modules: this.modules.length, totalBytes: total, selectedBytes: this.selectedBytes, windowsSelectedBytes: this.windowsSelectedBytes, windowsIconTooLarge: (this.icon?.entry.uncompressedSize ?? 0) > ICON_LIMIT, root: prefix, warnings }; + } + + selectedBytesForTarget(target) { + if (target !== "linux" && target !== "windows") throw new Error(`Unsupported conversion target: ${target}`); + if (target === "windows" && (this.icon?.entry.uncompressedSize ?? 0) > ICON_LIMIT) throw new Error("sce_sys/icon0.png exceeds 16 MiB"); + return target === "windows" ? this.windowsSelectedBytes : this.selectedBytes; } async conversionInput(target, toIntel, report) { let remaining = CONVERSION_LIMIT; + const selectedBytes = this.selectedBytesForTarget(target); + if (selectedBytes > CONVERSION_LIMIT) throw new Error(`Selected ${target} conversion input exceeds the 256 MiB conversion limit. Use the native relinker for this app.`); const read = async item => { report({ phase: "Reading conversion input", input: item.name }); const bytes = await boundedBytes(item.entry, remaining); diff --git a/demo/web/index.html b/demo/web/index.html index c280516fb8..793792ab62 100644 --- a/demo/web/index.html +++ b/demo/web/index.html @@ -22,7 +22,7 @@ -

Memory downloads have a 256 MiB budget, checked against expanded files and output overhead. Larger results require desktop Chrome or Edge with direct disk saving. Executable, modules and icon are limited to 256 MiB combined; use the native relinker for larger conversion inputs.

+

Memory downloads have a 256 MiB budget, checked against expanded files and output overhead. Larger results require desktop Chrome or Edge with direct disk saving. Executable and modules are limited to 256 MiB combined; the icon is also limited to 16 MiB and counts toward the Windows conversion limit. Use the native relinker for larger conversion inputs.

Conversion does not guarantee that an app runs. Check the community compatibility list for known blockers and reports.

diff --git a/demo/web/output-directory.mjs b/demo/web/output-directory.mjs new file mode 100644 index 0000000000..ee199e1341 --- /dev/null +++ b/demo/web/output-directory.mjs @@ -0,0 +1,44 @@ +export class OutputDirectoryTransaction { + #state = "open"; + + constructor(parent, name, directory) { + this.parent = parent; + this.name = name; + this.directory = directory; + } + + static async create(parent, name) { + try { + await parent.getDirectoryHandle(name); + } catch (error) { + if (error.name !== "NotFoundError") throw error; + const directory = await parent.getDirectoryHandle(name, { create: true }); + return new OutputDirectoryTransaction(parent, name, directory); + } + throw new Error(`The folder ${name} already exists. Choose another destination or rename the existing folder.`); + } + + commit() { + if (this.#state !== "open") throw new Error("Output directory transaction is no longer active"); + this.#state = "committed"; + } + + async discard() { + if (this.#state !== "open") return; + await this.parent.removeEntry(this.name, { recursive: true }); + this.#state = "discarded"; + } +} + +export async function abortAndDiscardOutput(transaction, sink) { + const failures = []; + if (sink) { + try { await sink.abort(); } + catch (error) { failures.push(`Could not discard the output file: ${error instanceof Error ? error.message : String(error)}`); } + } + if (transaction) { + try { await transaction.discard(); } + catch (error) { failures.push(`Could not remove incomplete output folder ${transaction.name}/: ${error instanceof Error ? error.message : String(error)}`); } + } + return failures; +} diff --git a/demo/web/tests/archive-reserved.test.mjs b/demo/web/tests/archive-reserved.test.mjs new file mode 100644 index 0000000000..b04432d014 --- /dev/null +++ b/demo/web/tests/archive-reserved.test.mjs @@ -0,0 +1,31 @@ +import assert from "node:assert/strict"; +import { register } from "node:module"; +import { pathToFileURL } from "node:url"; +import path from "node:path"; + +register("./cdn-loader.mjs", import.meta.url, { data: { url: process.env.ANYPS5_TEST_ZIP_URL, path: process.env.ANYPS5_TEST_ZIP_PATH } }); +const { Archive, validPath } = await import(pathToFileURL(path.join(process.argv[2], "archive.mjs"))); +const { BlobWriter, Uint8ArrayReader, ZipWriter } = await import(process.env.ANYPS5_TEST_ZIP_URL); +const header = new Uint8Array([127, 69, 76, 70]); + +for (const prefix of ["COM", "lpt"]) { + for (const digit of ["\u00b9", "\u00b2", "\u00b3"]) { + for (const name of [prefix + digit, prefix + digit + ".txt", "content/" + prefix + digit + "/data.bin"]) { + assert.throws(() => validPath(name), /Unsafe ZIP path/); + const folder = new Archive([{ path: "main.elf", file: new Blob([header]) }, { path: name, file: new Blob(["resource"]) }]); + try { await assert.rejects(folder.inspect(() => {}), /Unsafe ZIP path/); } + finally { await folder.close(); } + } + } +} + +const writer = new ZipWriter(new BlobWriter(), { useWebWorkers: false, useCompressionStream: false }); +await writer.add("main.elf", new Uint8ArrayReader(header)); +await writer.add("content/COM\u00b9.txt", new Uint8ArrayReader(new Uint8Array([1]))); +const archive = new Archive(await writer.close()); +try { await assert.rejects(archive.inspect(() => {}), /Unsafe ZIP path/); } +finally { await archive.close(); } + +for (const name of ["COM10", "COM\u2074.txt", "COM\u00b9-extra", "content/ordinary.txt"]) { + assert.equal(validPath(name), name); +} diff --git a/demo/web/tests/archive-root.test.mjs b/demo/web/tests/archive-root.test.mjs new file mode 100644 index 0000000000..deba9ee898 --- /dev/null +++ b/demo/web/tests/archive-root.test.mjs @@ -0,0 +1,37 @@ +import assert from "node:assert/strict"; +import { register } from "node:module"; +import { pathToFileURL } from "node:url"; +import path from "node:path"; + +register("./cdn-loader.mjs", import.meta.url, { data: { url: process.env.ANYPS5_TEST_ZIP_URL, path: process.env.ANYPS5_TEST_ZIP_PATH } }); +const { Archive } = await import(pathToFileURL(path.join(process.argv[2], "archive.mjs"))); +const { BlobWriter, Uint8ArrayReader, ZipWriter } = await import(process.env.ANYPS5_TEST_ZIP_URL); +const header = new Uint8Array([127, 69, 76, 70]); + +async function zipped(entries) { + const writer = new ZipWriter(new BlobWriter(), { useWebWorkers: false, useCompressionStream: false }); + for (const [name, directory] of entries) await writer.add(name, directory ? undefined : new Uint8ArrayReader(header), { directory }); + return writer.close(); +} + +const parents = ["outer/", "outer/inner/", "outer/inner/title/"]; +for (const directories of [[], parents, [...parents].reverse()]) { + const archive = new Archive(await zipped([ + ...directories.map(name => [name, true]), ["outer/inner/title/main.elf", false] + ])); + try { + const summary = await archive.inspect(() => {}); + assert.equal(summary.root, "outer/inner/title/"); + assert.equal(summary.name, "main.elf"); + assert.equal(summary.files, 1); + assert.deepEqual((await archive.conversionInput("linux", false, () => {})).executable, header); + } finally { await archive.close(); } +} + +const folder = new Archive([{ path: "outer/inner/title/main.elf", file: new Blob([header]) }]); +try { assert.equal((await folder.inspect(() => {})).root, "outer/inner/title/"); } +finally { await folder.close(); } + +const siblings = new Archive(await zipped([["outer/", true], ["outer/main.elf", false], ["other/resource", false]])); +try { await assert.rejects(siblings.inspect(() => {}), /exactly one executable ELF/); } +finally { await siblings.close(); } diff --git a/demo/web/tests/archive.test.mjs b/demo/web/tests/archive.test.mjs index 4470741f74..c176ff2d38 100644 --- a/demo/web/tests/archive.test.mjs +++ b/demo/web/tests/archive.test.mjs @@ -7,8 +7,9 @@ import { register } from "node:module"; register("./cdn-loader.mjs", import.meta.url, { data: { url: process.env.ANYPS5_TEST_ZIP_URL, path: process.env.ANYPS5_TEST_ZIP_PATH } }); const site = name => pathToFileURL(path.join(process.argv[2], name)).href; -const { Archive, memoryEstimate, MEMORY_LIMIT, packageArchive } = await import(site("archive.mjs")); +const { Archive, memoryEstimate, MEMORY_LIMIT, ICON_LIMIT, packageArchive } = await import(site("archive.mjs")); const { BlobReader, BlobWriter, Uint8ArrayReader, Uint8ArrayWriter, ZipReader, ZipWriter } = await import(process.env.ANYPS5_TEST_ZIP_URL); +await import("./archive-reserved.test.mjs"); const { default: createRelinker } = await import(site("native/relinker_wasm.mjs")); const native = await createRelinker(); const executable = new Uint8Array(await readFile(path.join(process.argv[3], "main.elf"))); @@ -113,6 +114,8 @@ for (const target of ["windows", "linux"]) { for (const [entries, error] of [ [[["one", executable], ["two", executable]], /exactly one/u], [[["readme", new Uint8Array([1])]], /exactly one/u], + [[["eboot.bin", new Uint8Array([79, 21, 61, 29])]], /SELF container/u], + [[["eboot.bin", new Uint8Array([84, 20, 245, 238])]], /SELF container/u], [[["app", executable], ["..\/escape", resource]], /Unsafe ZIP/u], [[["app", executable], ["APP", resource]], /Duplicate ZIP/u], [[["app", executable], ["folder", resource], ["folder/file", resource]], /also used as a folder/u], @@ -125,17 +128,96 @@ for (const [entries, error] of [ const missing = new Archive(await zip([["any-name", executable]])); assert.equal((await missing.inspect(() => {})).warnings.length, 2); await missing.close(); +const selfModule = new Archive(await zip([["program.dat", executable],["sce_module/library.prx", new Uint8Array([84, 20, 245, 238])]])); +const selfModuleSummary = await selfModule.inspect(() => {}); +assert.equal(selfModuleSummary.warnings.length, 2); +assert.match(selfModuleSummary.warnings.join(" "), /SELF container/u); +await selfModule.close(); assert(memoryEstimate(200 * 1024 ** 3, 1024, 1024, 100) > MEMORY_LIMIT); const invalid = native.Convert({ target: "unsupported" }, () => {}); assert.equal(invalid.ok, false); console.log("Archive validation, missing-folder warnings and large-download budget verified"); +const iconBytes = new Uint8Array(1024 * 1024); +const iconArchive = new Archive([ + { path: "title/program.elf", file: new Blob([new Uint8Array([0x7f, 69, 76, 70])]) }, + { path: "title/sce_sys/icon0.png", file: new Blob([iconBytes]) } +]); +const iconSummary = await iconArchive.inspect(() => {}); +assert.equal(iconSummary.selectedBytes, 4); +assert.equal(iconSummary.windowsSelectedBytes, iconBytes.length + 4); +assert.equal((await iconArchive.conversionInput("linux", false, () => {})).icon, null); +assert.equal((await iconArchive.conversionInput("windows", false, () => {})).icon.length, iconBytes.length); +const librariesAtBudgetEdge = MEMORY_LIMIT + - memoryEstimate(iconSummary.totalBytes, iconSummary.selectedBytes, 0, iconSummary.files) + - iconBytes.length * 2; +assert(memoryEstimate(iconSummary.totalBytes, iconSummary.selectedBytes, librariesAtBudgetEdge, iconSummary.files) < MEMORY_LIMIT); +assert(memoryEstimate(iconSummary.totalBytes, iconSummary.windowsSelectedBytes, librariesAtBudgetEdge, iconSummary.files) > MEMORY_LIMIT); +await iconArchive.close(); +console.log("Linux conversion input and memory estimate exclude the Windows-only icon"); + +const oversizedIconArchive = new Archive([ + { path: "title/program.elf", file: new Blob([new Uint8Array([0x7f, 69, 76, 70])]) }, + { path: "title/sce_sys/icon0.png", file: new Blob([new Uint8Array(ICON_LIMIT + 1)]) } +]); +const oversizedIconSummary = await oversizedIconArchive.inspect(() => {}); +assert.equal(oversizedIconSummary.windowsIconTooLarge, true); +assert.equal(oversizedIconArchive.selectedBytesForTarget("linux"), 4); +assert.throws(() => oversizedIconArchive.selectedBytesForTarget("windows"), /16 MiB/u); +assert.equal((await oversizedIconArchive.conversionInput("linux", false, () => {})).icon, null); +await oversizedIconArchive.close(); +console.log("Oversized Windows-only icons do not block Linux conversion"); + const workerMessages = []; globalThis.self = { postMessage: message => workerMessages.push(message) }; -await import(site("worker.mjs")); +const worker = await import(site("worker.mjs")); +const libraryArchive = await zip([["libs/system.prx", new Uint8Array([1, 2, 3])]]); +const libraryBytes = new Uint8Array(await libraryArchive.arrayBuffer()); +const libraryPath = "libraries/windows-acde1234.zip"; +const librarySha256 = [...new Uint8Array(await crypto.subtle.digest("SHA-256", libraryBytes))] + .map(byte => byte.toString(16).padStart(2, "0")) + .join(""); +const release = { + tag: "v1", + platforms: { + windows: { + path: libraryPath, + size: libraryBytes.byteLength, + expandedBytes: 3, + sha256: librarySha256 + } + } +}; +const originalFetch = globalThis.fetch; +let latestResponse = new Response(null, { status: 403, headers: { "X-RateLimit-Remaining": "0" } }); +globalThis.fetch = async url => { + if (url === "./release.json") return new Response(JSON.stringify(release)); + if (url === "https://api.github.com/repos/boykopovar/AnyPS5/releases/latest") return latestResponse; + if (url === libraryPath) return new Response(libraryBytes); + throw new Error(`Unexpected fetch: ${url}`); +}; +try { + const downloaded = await worker.downloadLibraries("windows"); + assert.equal(downloaded.release.tag, release.tag); + assert.deepEqual(new Uint8Array(await downloaded.file.arrayBuffer()), libraryBytes); + assert(workerMessages.some(message => message.type === "progress" && /rate limit reached/u.test(message.message ?? ""))); + + latestResponse = new Response(null, { status: 429 }); + const rateLimitedDownload = await worker.downloadLibraries("windows"); + assert.deepEqual(new Uint8Array(await rateLimitedDownload.file.arrayBuffer()), libraryBytes); + + latestResponse = new Response(null, { status: 403 }); + await assert.rejects(worker.downloadLibraries("windows"), /Cannot verify the latest release \(403\)/u); + latestResponse = new Response(JSON.stringify({ tag_name: "v2" })); + await assert.rejects(worker.downloadLibraries("windows"), /A new release is being published/u); +} finally { + globalThis.fetch = originalFetch; + workerMessages.length = 0; +} await self.onmessage({ data: { type: "convert", target: "windows", mode: "disk" } }); assert.equal(workerMessages.length, 1); assert.equal(workerMessages[0].type, "error"); assert.match(workerMessages[0].message, /browsers block DLL creation/u); delete globalThis.self; +console.log("Rate-limited release checks use the verified site manifest; other release-check failures remain errors"); console.log("Windows folder output rejected before conversion or file writes"); diff --git a/demo/web/tests/output-directory.test.mjs b/demo/web/tests/output-directory.test.mjs new file mode 100644 index 0000000000..873572a738 --- /dev/null +++ b/demo/web/tests/output-directory.test.mjs @@ -0,0 +1,116 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { abortAndDiscardOutput, OutputDirectoryTransaction } from "../output-directory.mjs"; + +function missingDirectory() { + const error = new Error("not found"); + error.name = "NotFoundError"; + return error; +} + +test("creates a new output root only after confirming the destination is absent", async () => { + const directory = {}; + const calls = []; + const parent = { + async getDirectoryHandle(name, options) { + calls.push([name, options]); + if (!options) throw missingDirectory(); + return directory; + }, + async removeEntry() {} + }; + + const transaction = await OutputDirectoryTransaction.create(parent, "app-linux"); + + assert.equal(transaction.directory, directory); + assert.deepEqual(calls, [["app-linux", undefined], ["app-linux", { create: true }]]); +}); + +test("does not claim or delete a destination that already exists", async () => { + const parent = { + async getDirectoryHandle() { return {}; }, + async removeEntry() { assert.fail("existing destination must not be removed"); } + }; + + await assert.rejects( + OutputDirectoryTransaction.create(parent, "app-linux"), + /already exists/u + ); +}); + +test("discards its incomplete output recursively", async () => { + let removed; + const parent = { + async getDirectoryHandle(name, options) { + if (!options) throw missingDirectory(); + return { name }; + }, + async removeEntry(...args) { removed = args; } + }; + const transaction = await OutputDirectoryTransaction.create(parent, "app-linux"); + + await transaction.discard(); + await transaction.discard(); + + assert.deepEqual(removed, ["app-linux", { recursive: true }]); +}); + +test("keeps completed output and reports cleanup failures", async () => { + let removeCount = 0; + const parent = { + async getDirectoryHandle(name, options) { + if (!options) throw missingDirectory(); + return { name }; + }, + async removeEntry() { + removeCount++; + throw new Error("permission denied"); + } + }; + const completed = await OutputDirectoryTransaction.create(parent, "complete-linux"); + completed.commit(); + await completed.discard(); + assert.equal(removeCount, 0); + + const failed = await OutputDirectoryTransaction.create(parent, "partial-linux"); + await assert.rejects(failed.discard(), /permission denied/u); + assert.equal(removeCount, 1); +}); + +test("aborts an open file before removing the incomplete output tree", async () => { + const calls = []; + const parent = { + async getDirectoryHandle(name, options) { + if (!options) throw missingDirectory(); + return { name }; + }, + async removeEntry(name, options) { calls.push(["remove", name, options]); } + }; + const transaction = await OutputDirectoryTransaction.create(parent, "partial-linux"); + let finishAbort; + const sink = { abort() { calls.push(["abort"]); return new Promise(resolve => { finishAbort = resolve; }); } }; + + const cleanup = abortAndDiscardOutput(transaction, sink); + await Promise.resolve(); + assert.deepEqual(calls, [["abort"]]); + finishAbort(); + assert.deepEqual(await cleanup, []); + assert.deepEqual(calls, [["abort"], ["remove", "partial-linux", { recursive: true }]]); +}); + +test("returns sink and folder cleanup failures without hiding either", async () => { + const parent = { + async getDirectoryHandle(name, options) { + if (!options) throw missingDirectory(); + return { name }; + }, + async removeEntry() { throw new Error("directory permission denied"); } + }; + const transaction = await OutputDirectoryTransaction.create(parent, "partial-linux"); + const sink = { async abort() { throw new Error("file is busy"); } }; + + assert.deepEqual(await abortAndDiscardOutput(transaction, sink), [ + "Could not discard the output file: file is busy", + "Could not remove incomplete output folder partial-linux/: directory permission denied" + ]); +}); diff --git a/demo/web/worker.mjs b/demo/web/worker.mjs index 9a0017792f..d2cbdb4377 100644 --- a/demo/web/worker.mjs +++ b/demo/web/worker.mjs @@ -1,4 +1,4 @@ -import { Archive, MEMORY_LIMIT, memoryEstimate, packageArchive } from "./archive.mjs"; +import { Archive, CONVERSION_LIMIT, MEMORY_LIMIT, memoryEstimate, packageArchive } from "./archive.mjs"; let archive; let busy = false; @@ -15,7 +15,7 @@ function report(event) { self.postMessage({ type: "progress", ...event }); } -async function downloadLibraries(target) { +export async function downloadLibraries(target) { report({ phase: "Downloading system libraries" }); const response = await fetch("./release.json", { cache: "no-cache" }); if (!response.ok) throw new Error(`Cannot load release manifest (${response.status})`); @@ -23,9 +23,14 @@ async function downloadLibraries(target) { const asset = release.platforms[target]; if (!asset || !/^libraries\/(windows|linux)-[a-f0-9]+\.zip$/u.test(asset.path) || !/^[a-f0-9]{64}$/u.test(asset.sha256)) throw new Error("Invalid release manifest"); const latestResponse = await fetch("https://api.github.com/repos/boykopovar/AnyPS5/releases/latest", { headers: { Accept: "application/vnd.github+json" } }); - if (!latestResponse.ok) throw new Error(`Cannot verify the latest release (${latestResponse.status}). Try again later.`); - const latest = await latestResponse.json(); - if (latest.tag_name !== release.tag) throw new Error("A new release is being published to this site. Try again after the Pages build finishes."); + if (!latestResponse.ok) { + const rateLimited = latestResponse.status === 429 || (latestResponse.status === 403 && latestResponse.headers.get("x-ratelimit-remaining") === "0"); + if (!rateLimited) throw new Error(`Cannot verify the latest release (${latestResponse.status}). Try again later.`); + report({ phase: "Downloading system libraries", message: "GitHub API rate limit reached; using the libraries published with this site." }); + } else { + const latest = await latestResponse.json(); + if (latest.tag_name !== release.tag) throw new Error("A new release is being published to this site. Try again after the Pages build finishes."); + } const download = await fetch(asset.path); if (!download.ok) throw new Error(`Cannot download system libraries (${download.status})`); const bytes = await download.arrayBuffer(); @@ -54,8 +59,10 @@ self.onmessage = async ({ data }) => { } else if (data.type === "convert") { if (data.target === "windows" && data.mode === "disk") throw new Error("Windows folder output is unavailable because browsers block DLL creation. Choose a ZIP save mode."); if (!archive) throw new Error("Select a ZIP or folder first"); + const selectedBytes = archive.selectedBytesForTarget(data.target); + if (selectedBytes > CONVERSION_LIMIT) throw new Error(`Selected ${data.target} conversion input exceeds the 256 MiB conversion limit. Use the native relinker for this app.`); const libraries = await downloadLibraries(data.target); - const estimate = memoryEstimate(archive.totalBytes, archive.selectedBytes, libraries.expandedBytes, archive.entries.length); + const estimate = memoryEstimate(archive.totalBytes, selectedBytes, libraries.expandedBytes, archive.entries.length); if (data.mode === "memory" && estimate > MEMORY_LIMIT) throw new Error("This result exceeds the 256 MiB memory-download budget. Use Save ZIP to disk in desktop Chrome or Edge."); report({ phase: "Loading converter" }); const { default: createRelinker } = await import("./native/relinker_wasm.mjs"); diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index e73a415d40..ffbc3207e3 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -43,7 +43,7 @@ flowchart LR ``` - [`core/relinker/main.cpp`](../../core/relinker/main.cpp) runs the steps in this order. `--to-intel` is optional, see [USAGE.md](../user/USAGE.md). -- Each library in [`core/libs/prx`](../../core/libs/prx) builds as a shared library. After the build, `nid_patcher` ([`core/libs/nid`](../../core/libs/nid)) renames every export to its NID, computed from the function name. `APS5_EXPORT("", func)` sets the NID directly when the name is unknown. +- Each library in [`core/libs/prx`](../../core/libs/prx) builds as a shared library. After the build, `nid_patcher` ([`core/libs/nid`](../../core/libs/nid)) renames every export to its NID, computed from the function name. `APS5_EXPORT("", func)` sets the NID directly when the name is unknown. Every other library keeps the names libc exports; it reads them from `libc.exports`, which `nid_patcher --write-export-list` rewrites only when that list changes. On Windows the link writes each import library with `SOURCE_DATE_EPOCH=0` and `ranlib -D` and replaces it only when its bytes change. A source edit that keeps the exports therefore relinks and re-patches only its own library. ## Windows deferred module initialization @@ -94,7 +94,7 @@ flowchart LR ``` - [`Recompiler.cpp`](../../core/shader/recompiler/Recompiler.cpp) runs the stages in this order. With `ANYPS5_ENABLE_SPIRV_TOOLS`, the SPIR-V is also validated and optimized with SPIRV-Tools. -- `ShaderRecompiler::Recompile` keeps compiled variants in memory, and `ShaderDiskCache` stores them on disk so later runs reuse them. +- `ShaderRecompiler::Recompile` keeps compiled variants in memory, and `ShaderDiskCache` stores them on disk so later runs reuse them. Before the writable cache, a miss is also looked up in read-only prebuilt packs and loose entry folders under `shader_cache/prebuilt`; pack entries are ordinary encoded cache entries behind a sorted key-hash index, validated by magic, compiler source version and index checksum when mapped, and a miss or rejected entry falls through to the writable cache and the compiler. Cache format 35 also stores the finite stencil-reference set, its replay-safety proof and reserved push-data word in the artifact and binding layouts. Entries from formats 31 to 34 are rejected. - A compute shader whose LDS plus its lock dword exceeds the host's `maxComputeSharedMemorySize` is not given a `Workgroup` array. `Recompile` sets `ShaderInfo::sharedMemoryBytes` for it and the SPIR-V backend addresses the LDS in a coherent storage buffer at descriptor set 1 (`WorkgroupMemoryDescriptorSet`), with a slice of `RecompileResult::workgroupMemoryDwords` per workgroup, indexed by `WorkgroupID` and `NumWorkGroups`. `VulkanDevice` keeps one such buffer, sized from the dispatch dimensions, and binds it after the resource set; programs that fit keep using shared memory. - `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. @@ -128,6 +128,13 @@ A storage image over guest memory that the write watch does not cover (a host-po 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`. +### Resource plans + +A cached shader keeps an `IrResourcePlan` (`ResourceMaterializer::ExtractPlan` in [`ResourceMaterializer.cpp`](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp)) for as long as it is registered: a copy of the IR values its descriptor sources, SRT reads, uniform fill and control-flow conditions depend on, which every capture walks to read the shader's resources from guest memory. The copy is sized to what the walk can use: + +- A control-flow condition is kept only when `Detail::Evaluator::MayEvaluate` finds a runtime it could be evaluated for. A condition that depends on a vector register, or on an opcode the evaluator does not evaluate, takes both successors whatever the runtime holds, so the plan stores it as no condition and does not copy its operands. Such conditions are most of a shader's IR when the masked-lane code feeds its branches. +- The copied values are allocated in one `IrValueArena` and hold no use lists: the walk only follows arguments. + ### 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/BUILD.md b/docs/dev/BUILD.md index a3bd542283..12b552df7c 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -30,7 +30,7 @@ git submodule update --init --recursive - Linux display refresh queries require SDL's XRandR support (`libxrandr-dev` on Debian/Ubuntu; `libXrandr-devel` on Fedora). Bundled SDL configuration fails without it. Keep `SDL_X11_XRANDR` enabled. - Linux: GCC, G++, binutils. SDL's X11 backend requires X11 and Xext development headers (`libx11-dev` and `libxext-dev` on Debian/Ubuntu). SDL's Wayland backend requires the Wayland, xkbcommon and EGL development files (`libwayland-dev`, `libxkbcommon-dev` and `libegl-dev` on Debian/Ubuntu; `wayland-devel`, `libxkbcommon-devel` and `mesa-libEGL-devel` on Fedora), and `libdecor-0-dev` (`libdecor-devel`) for window decorations on GNOME. SDL loads both backends at run time and prefers Wayland when `WAYLAND_DISPLAY` is set; `SDL_VIDEODRIVER=x11` selects X11 (XWayland). Titles that bundle their own SDL read `SDL_VIDEODRIVER` too, and some of them fail to start with `x11`. Without the Wayland files configuration still succeeds, and the build uses X11 only. Audio output needs SDL's PipeWire, PulseAudio or ALSA backend, built from their development files (`libpipewire-0.3-dev`, `libpulse-dev` and `libasound2-dev` on Debian/Ubuntu; `pipewire-devel`, `pulseaudio-libs-devel` and `alsa-lib-devel` on Fedora). SDL loads them at run time. Without any of them SDL keeps only its silent dummy and disk drivers, so configuration fails. - Fedora: if CMake reports `Could NOT find Threads`, install `glibc-static` and `libstdc++-static` with `sudo dnf install glibc-static libstdc++-static`, then rerun configuration. The project enables static executable linking, and FFmpeg's thread check needs these libraries. -- Windows: only MinGW-w64 GCC 15.2.0 (WinLibs `x86_64-ucrt-posix-seh`, release `15.2.0posix-14.0.0-ucrt-r7`) is currently supported. Add its `mingw64/bin` directory to `PATH` before configuring. +- Windows: only MinGW-w64 GCC 15.2.0 (WinLibs `x86_64-ucrt-posix-seh`, release [`15.2.0posix-14.0.0-ucrt-r7`](https://github.com/brechtsanders/winlibs_mingw/releases/tag/15.2.0posix-14.0.0-ucrt-r7), archive `winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip`) is currently supported. Add its `mingw64/bin` directory to `PATH` before configuring. - Windows antivirus software can quarantine the executables that the relinker tests create in the temp directory, which fails `optional_plt`, `empty_tls`, `windows_address_space` and `windows_icon`. Set `TEMP` and `TMP` to a directory the scanner excludes before running `ctest`. - FFmpeg binaries are downloaded during configuration unless `FFMPEG_PREBUILT_DIR` is set. With the WinLibs CMake, the download fails with status 60 (`SSL peer certificate or SSH remote key was not OK`) unless `SSL_CERT_FILE` names a CA bundle, for example `C:\Program Files\Git\mingw64\etc\ssl\certs\ca-bundle.crt` from Git for Windows, as in CI. @@ -151,9 +151,9 @@ Set `APS5_TRACE_FRAMES=:` to record `` presents from presen Set `APS5_TRACE_EVENTS=` to change the queue-worker and presenter buffers for a frame or seconds window. The count is a whole number from 1 through 4,194,304; the default is 262,144. An invalid value fails when one of these threads first binds to the trace, before recording. Each buffer uses `count * sizeof(TraceThread::Slice)` bytes (currently 32 bytes per event): 8 MiB at the default, 96 MiB at `3145728`, and 128 MiB at the maximum, per thread. The buffers are allocated even while waiting for the window to open, and formatting the trace uses additional memory. Shader preparation keeps its explicitly sized 32,768-event buffers, and the GPU range limit stays 262,144. Summary-only sessions allocate no event buffers and do not read this setting. A larger buffer can still fill; use a shorter window or increase the count within the bound. -Inside the window the queue workers and the presenter record every `PerformanceTimer` scope and stage as a complete event named after its frame timing key: `Driver.WorkerWait`, `Driver.Packet`, `Driver.Packet.draw`, `Graphics.Draw`, `Driver.Present`, `Driver.Present.inflight_wait`, `Vulkan.Retire` and so on. A slice shorter than 20 us is only counted: its time is in the thread's counter ` folded` (milliseconds per present) and in the summary. A thread writes a present's counter sample at its first slice after the next present, so the last present of the window has no sample; its time is still in the summary. By default, each queue worker and presenter allocates a buffer for 262,144 events when it starts; a full buffer stops that thread's recording and sets `trace_dropped`. When the window closes, the presenter formats the trace once and a `ProfileOutput` writer thread writes `aps5-trace--.json.gz` (Chrome trace-event JSON, opened by ui.perfetto.dev or chrome://tracing) and appends one line to `aps5-summary.jsonl`, both in `APS5_TRACE_DIR` or the working directory. A window that is still open when the title exits writes nothing. +Inside the window the queue workers and the presenter record every `PerformanceTimer` scope and stage as a complete event named after its frame timing key: `Driver.WorkerWait`, `Driver.Packet`, `Driver.Packet.draw`, `Graphics.Draw`, `Driver.Present`, `Driver.Present.inflight_wait`, `Vulkan.Retire` and so on. A slice shorter than 20 us is only counted: its time is in the thread's counter ` folded` (milliseconds per present) and in the summary. A thread writes a present's counter sample at its first slice after the next present, so the last present of the window has no sample; its time is still in the summary. By default, each queue worker and presenter allocates a buffer for 262,144 events when it starts; a full buffer stops that thread's recording, sets `trace_dropped` and puts an instant event `trace buffer full` on the thread's track at the start of the first lost slice (slices are appended when their outer scope ends, so that can precede the last recorded slice by up to one outer-scope length), with the number of lost slices in `args.events_lost`. When the window closes, the presenter formats the trace once and a `ProfileOutput` writer thread writes `aps5-trace--.json.gz` (Chrome trace-event JSON, opened by ui.perfetto.dev or chrome://tracing) and appends one line to `aps5-summary.jsonl`, both in `APS5_TRACE_DIR` or the working directory. A window that is still open when the title exits writes nothing. -The summary line has `commit` and `dirty` of the build, `env` (every `APS5_*` variable; a value containing `/` or `\` is written as ``), `gpu`, `driver`, `os`, `resolution` (the video output's), `window`, `presents`, `frame_ms` (p50, p90, p99 and max of the intervals between presents), `gpu_busy_ms_per_present` (the batch timestamps behind `[present]`), `worker_busy_pct` (share of the window each queue worker spends in `Driver.Packet`, `Driver.Suspend`, `Driver.EndSubmission`, `Driver.Rewind` and `Driver.Completion`), `top_phases_ms_per_frame` (the eight largest scopes and stages, without `Driver.WorkerWait`), `trace_dropped` and `trace`. A trace holds timings, scope names, counts and thread names; no guest memory, paths or strings of the title. +The summary line has `commit` and `dirty` of the build, `env` (every `APS5_*` variable; a value containing `/` or `\` is written as ``), `gpu`, `driver`, `os`, `resolution` (the video output's), `window`, `presents`, `frame_ms` (p50, p90, p99 and max of the intervals between presents), `gpu_busy_ms_per_present` (the batch timestamps behind `[present]`), `worker_busy_pct` (share of the window each queue worker spends in `Driver.Packet`, `Driver.Suspend`, `Driver.EndSubmission`, `Driver.Rewind` and `Driver.Completion`), `top_phases_ms_per_frame` (the eight largest scopes and stages, without `Driver.WorkerWait`), `trace_dropped`, `trace_dropped_threads` and `trace`. `trace_dropped` is `true` when any thread lost a slice, any thread's totals table (512 scope and stage entries) was full, or the `gpu` track was full. `trace_dropped_threads` is present only then and has one entry per such thread name (threads that share a name, such as the shader prepare threads, share one entry: `events_lost` is summed, `events_full_after_s` is the earliest, `totals_full` is true if any is): `events_full_after_s` (seconds from the first present of the window to the start of the first lost slice; absent for `gpu` and, in every summary line (windowed `APS5_TRACE_FRAMES`/`APS5_TRACE_SECONDS` and `APS5_TRACE_SUMMARY_EVERY`), whenever the first lost slice starts outside the line's span of presents, including a slice whose scope began before the window opened or one lost after the window's last present), `events_lost` (slices not recorded; absent when 0; in `APS5_TRACE_SUMMARY_EVERY` lines it counts the thread buffer's losses since it started, not only those of the line's span) and `totals_full` (`true` when the thread's totals table overflowed, which makes the summary numbers incomplete; a thread with `events_lost` only has complete totals). A trace holds timings, scope names, counts and thread names; no guest memory, paths or strings of the title. The shader preparation threads are traced as well, `shader prepare`, with a buffer for 32,768 events each. A wait for the GPU mutex on a traced thread is a `GpuMutex.wait.` slice (`dispatch`, `draw`, `present`, `flush` and so on, as on the `[lock]` line). diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 3e40f44bba..2f721015f3 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -4,7 +4,6 @@ Entries about one library or component are in [debt](debt/), one file per librar ### Build -- The bundled [SPIRV-Tools build](../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy that releases processed traversal maps. Remove this patch when the pinned revision includes [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). System-dependency builds need the fix in their installed SPIRV-Tools package. - Building on Windows requires a specific version of mingw - MinGW-w64 GCC 15.2.0 (`winlibs-gcc15`, `x86_64-ucrt-posix-seh`) - Even compiled prx libraries on Windows require nearby (static linking of these dependencies causes conflicts): - libgcc_s_seh-1.dll diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 2a41360807..81b3f5e718 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -7,7 +7,7 @@ - [sceAgcDcbSetCfRegisterDirect, sceAgcDcbSetCfRegisterRangeDirect](../../../core/libs/prx/libSceAgc/DcbState/src/Registers.cpp) (libSceAgc) - signatures follow [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Agc/SceAgc.cs), which agree; the SET_CONFIG_REG (0x68) packet is OpenAGC's. The driver does not execute SET_CONFIG_REG, so a submitted packet throws - [Command buffer cursors](../../../core/libs/prx/libSceAgc/Command/src/Packet.cpp) (libSceAgc) - a null `cursor_down` is taken as a buffer without downward allocations, whose limit is `top`. PPSA16064 builds one-packet buffers of 8 dwords that way; the console's handling is unconfirmed - [sceAgcGetRegisterDefaultsInternal](../../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 0 internal table, as `sceAgcGetRegisterDefaults` returns the version 0 public one; [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns the table of the version passed to `sceAgcInit` instead, and null before it -- [sceAgcInit](../../../core/libs/prx/libSceAgc/DcbState/src/Init.cpp) (libSceAgc) - unsupported register-default versions return `0x80890001` following [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c); AnyPS5 keeps version 13 support because its register-default tables include it, while that reference accepts only versions 0–12. The upper version accepted by PS5 firmware is unverified +- [sceAgcInit](../../../core/libs/prx/libSceAgc/DcbState/src/Init.cpp) (libSceAgc) - versions 0–13 select their register-default tables and return 0; pointer-shaped or otherwise out-of-range values (>=14) return 0 without a register-default lookup. Console probe on firmware 13.60 ([#4808](https://github.com/boykopovar/AnyPS5/issues/4808#issuecomment-6149448503)): `sceAgcInit(&buf, 8)` (NID `kW3GLb7QfPg`) returns 0 and writes nothing; scalar versions 2–13 return `0x8A6C0004` there, while AnyPS5 still returns 0 for those scalars. The probe does not show that table 13 is selected for pointer-shaped calls. [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns `0x80890001` above 12 for `sceAgcInit`, and falls back to version 11 in `sceAgcGetRegisterDefaults2`. PPSA99011, PPSA99995, [ps5-vulkan](https://github.com/mpereiraesaa/ps5-vulkan) and [prospero-win](https://github.com/mpereiraesaa/prospero-win) call that NID with a pointer-shaped first argument. `sceAgcInit_0090` (NID `23LRUSvYu1M`) checks the state pointer and then calls `sceAgcInit`, as OpenAGC does - [sceAgcSetSubmitMode](../../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted - [sceAgcGetGsOversubscription](../../../core/libs/prx/libSceAgc/Shader/src/GsOversubscription.cpp) (libSceAgc) - signature and occupancy formula (base and expanded vertex/export capacities, budget shift, `GE_PC_ALLOC` / `SPI_SHADER_PGM_RSRC4_GS` encoding) follow KytyPS5 only, not confirmed on hardware or a title; a negative or NaN factor that gives a negative target, a missing GS context register or a zero `GE_MAX_OUTPUT_PER_SUBGROUP` throws - [sceAgcGetGsPrimPayload](../../../core/libs/prx/libSceAgc/Shader/src/GsPrimPayload.cpp) (libSceAgc) - the search does not stop at an `SPI_SHADER_IDX_FORMAT` (0x1C2) entry whose format is not `SPI_SHADER_2COMP`, as the analysis of the library declared in the pull request describes (its origin and firmware version are not identified), while [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1285-L1310) stops there; not verified on PS5 hardware. A null payload, a null shader or a register count with a null register array throws instead of faulting on the memory access diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 24455beaa3..720fc6ccbd 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -13,6 +13,7 @@ - [User data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp) (AGC driver) - a `SPI_SHADER_USER_DATA_*` or `COMPUTE_USER_DATA_*` register the title never wrote reads as 0 when a stage's RSRC2 count covers it, as Kyty (zeroed `UserSgprInfo`) and SharpEmu (missing registers read 0) do, and as AMD PAL and Mesa initialize them; the console's value is not confirmed - [SET_PREDICATION](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw - [NGG vertex programs on the mesh path](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - an unregistered vertex program with `PRIMGEN_EN` that uses LDS or `s_barrier` runs on the geometry (mesh) path. Each input primitive is assumed to yield at most one output primitive, the subgroup is bounded by `GE_MAX_OUTPUT_PER_SUBGROUP` as a vertex budget, and the GS/ES VGPR count checks are skipped, because the merged inputs keep their positions. Taken from the code Mesa's ACO emits for NGG culling and from PPSA59322 and PPSA99010 draws, not measured on hardware. Registered programs keep the vertex path +- [VGT_SHADER_STAGES_EN 0x02002008](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - this one value, `ES_EN` = 1 without `HS_EN` on a non-patch draw, is drawn as the vertex path. AMD's CIK register reference calls `ES_EN` = 1 `ES_STAGE_DS` (the ES stage runs the tessellation evaluation shader), which does not explain it on a non-patch draw. Seen only on PPSA29413 and not verified on the console; other vertex-path values with `ES_EN` set still throw. - [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. - [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 @@ -30,11 +31,13 @@ ### Functional - [Initial stencil state](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) uses zero for `DB_STENCIL_CONTROL`, `DB_STENCILREFMASK` and `DB_STENCILREFMASK_BF` (context `0x10b`-`0x10d`), including after `CLEAR_STATE`. The public V0, V4, V5 and V7-V11 [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) supply these values. Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h) agrees on the control register but uses `0x01000000` for the reference masks; the console reset values have not been measured. Planet Coaster (PPSA01736 v01.017) enables stencil tests without writing the control register. +- [Type-0 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 in dword 3) with a nonzero base are bound as null images, like a zero base. The behaviour comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault); it was not measured on the console's gfx1013. - [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. Retired Vulkan images remain allocated until device teardown to preserve outstanding references. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. +- [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` supports at most sixteen values proved from constant, select and phi exports, with an export proved on every kept path. It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions, since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware. - [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. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane, or of a later slice, is a storage image over the slices from 0 to its last (`LayeredDepthStorage`); only the slices from its base slice to its last are copied: a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, base slice, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, a view from another base slice, or a sampled copy) is a separate image, copied in before and out after each use, so views that are written before a flush and share a slice do not merge: the later copy-out wins, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. - [Collapsed color array views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) preserve the selected slice in the XOR address equation when the view is represented as a shifted 2D surface. Native Vulkan coverage checks R_X draws, array reads, publication and re-upload for slices 1-3; Lost Soul Aside PPSA26358 validates the RGBA16F cube mip chain on Windows/NVIDIA. Other devices and console comparisons remain unverified. Full-array views retain their layer addressing. -- [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. Planes with mips, a base slice other than 0, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). +- [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. A 2D-array T# whose `BASE_ARRAY` is past slice 0 reads slices `BASE_ARRAY` to `LAST_ARRAY` of the copy, which holds slices 0 to `LAST_ARRAY`, and a 2D binding of it reads slice `BASE_ARRAY`; that follows the colour 2D-array semantics and the titles that hit it (PPSA01556, PPSA03912), and was not measured on hardware for depth surfaces. Planes with mips, a cube or a 2D T# with a base slice other than 0, a `BASE_ARRAY` past `LAST_ARRAY`, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). - [SW_64KB_Z_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the texture equations and 64 KiB block geometry of `SW_64KB_Z_X`, whose addresses are checked against [AMD addrlib GFX10](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) in the texture tiling tests. Any 256-byte pipe/bank XOR below the block size is accepted, as for `SW_64KB_R_X` targets and Z_X textures; which of those bits the console honours, and the render results, are not measured. - [SW_64KB_D_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the existing texture block/mip geometry and equations from [AMD addrlib GFX10 non-RB+](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) for 16 pipes and 256-byte interleave. The address-reference tests use legal pipe/bank XOR bits for that configuration. Raw column bits outside the source configuration's pipe/bank mask 0xcf00 throw; their console meaning is unresolved. Console address equations and render results have not been measured. - [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. @@ -43,6 +46,8 @@ - [Separate alpha blend into 8_8 SWAP_ALT targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver): the second channel of a two-channel target with `COMP_SWAP` 1 holds the alpha export, but Vulkan blends it with the color equation of the R8G8 attachment, so a draw whose alpha equation differs from its color equation is recorded twice, the first channel with the color equation and the second with the alpha factors and operation moved into the color equation. The vertex stages run twice, so a vertex shader that writes memory writes twice, and occlusion counts include both passes. Draws that write depth or stencil, pixel shaders that write memory and factors of the destination alpha throw. That the hardware blends the second channel with the alpha equation is taken from Mesa's `si_translate_colorswap` and radeonsi's use of it for luminance-alpha formats, not measured on hardware. - [Unexported pixel inputs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver): a pixel input whose `SPI_PS_INPUT_CNTL` parameter the previous stage does not export is programmed as a default-value input (`OFFSET` bit 5, `DEFAULT_VAL` 0, FP16 high half too) and reads (0, 0, 0, 0), as radeonsi's `SI_PS_INPUT_CNTL_UNUSED` does and as rect-list draws already do. The console's value for an unwritten parameter cache entry is not measured on hardware. - [Per-sample pixel shading](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): `PA_SC_MODE_CNTL_1.PS_ITER_SAMPLE` (bit 16, [PS5_Mesa GFX10.3 registers](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/registers/gfx103.json)) on a multisampled draw enables Vulkan sample shading with a minimum rate of 1.0, so the pixel program runs once per sample. The draw throws when the device lacks `sampleRateShading`. On a single-sample draw the bit has no effect and is dropped. That the console runs the pixel program once per covered sample, and what it does with the other multisample registers in that mode, is taken from the register name and Vulkan's definition, not measured on hardware. +- [Unused pixel programs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): a bound pixel program is skipped when color masks have no intersection and DB export and execution enables are clear, regardless of retained SPI_SHADER_Z_FORMAT. This follows [AMD PAL DbShaderControl](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9AbiToPipelineRegisters.h) and its [GFX10 register definitions](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/chip/gfx9_plus_merged_registers.h), not a hardware measurement. Enabled DB_ALPHA_TO_MASK conservatively retains the program even with ALPHA_TO_MASK_DISABLE, because DecodeState currently enables Vulkan alpha-to-coverage without applying that override. Console behavior for these combinations has not been measured. +- [Sample masks](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `PA_SC_AA_MASK_X0Y0_X1Y0` and `PA_SC_AA_MASK_X0Y1_X1Y1` hold one 16-bit sample mask for each pixel of the aligned 2x2 quad ([Mesa gfx10.json](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/registers/gfx10.json#L13512)); only the bits of the `MSAA_NUM_SAMPLES` rasterized samples count, and at one sample bit 0 of a pixel's field covers every sample of the target the draw is raised to. A mask equal for the four pixels is the Vulkan `pSampleMask`, the case [radv](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_cmd_buffer.c#L13633) and [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/gallium/drivers/radeonsi/si_state.c#L4218) program; with early fragment tests it needs `earlyFragmentSampleMaskTestBeforeSampleCounting` ([radv](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_physical_device.c#L2079) reports it). A mask that differs between the pixels is applied in the pixel shader without early fragment tests: a pixel whose mask covers all or none of its samples is demoted to a helper invocation by `gl_FragCoord` parity, and otherwise the pixel's mask is ANDed into `gl_SampleMask` (with alpha-to-coverage and the mask export) after demoting invocations whose `SampleMask` input it leaves empty. A pixel program with early Z that writes memory under depth, stencil or depth-bounds tests is therefore refused. That XnYm is the pixel at (x mod 2, y mod 2) = (n, m) is read from the field names; that the mask removes samples before the depth and stencil tests and occlusion counting is inferred from radv implementing the Vulkan sample mask test with it; and that it takes no part in the centroid (`AA_MASK_CENTROID_DTMN` 0) is inferred from radv setting that bit only for conservative rasterization. None of it is measured on gfx1013. A mask equal for the four pixels that removes some but not all samples of a pixel is refused with active centroid inputs, since where Vulkan puts the centroid relative to `pSampleMask` is implementation-defined, and any partial mask is refused with conservative rasterization (radv sets `PREZ_AA_MASK_ENABLE` and `POSTZ_AA_MASK_ENABLE` [there](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_cmd_buffer.c#L13653), which 0x6001 leaves clear). - [Covered-centroid raster state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) accepts PA_SC_AA_CONFIG.COVERED_CENTROID_IS_CENTER only when the pixel program is skipped or SPI_PS_INPUT_ENA and SPI_PS_INPUT_ADDR have no active perspective or linear centroid input. Bit 29 follows [PS5_Mesa GFX10.3 registers](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/registers/gfx103.json); [RADV](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/radv_cmd_buffer.c) sets it on GFX10.3 and later. Active centroid interpolation and unspecified input registers still throw. Console covered-pixel centroid results were not measured. - [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. @@ -52,8 +57,8 @@ - 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. - [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`. - [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). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default. -- [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending, and ranges that leave the watch through a failed collect, have no saved bytes; storing such results throws. -- Windows host-pointer imports of the guest mapping itself are compared instead of write-watched: a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but disables snapshot reuse and unit shadows for excluded blocks; restoring tracking on import retirement without replacing the backing remains unimplemented. +- [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending, and ranges that leave the watch through a failed collect, have no saved bytes; storing such results throws. +- Windows host-pointer imports of the guest mapping itself are compared instead of write-watched: a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. - [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. 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. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. - [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when its offset in the backing file is not a multiple of the buffer's sparse block size (64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. @@ -77,7 +82,7 @@ - [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. - [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. - [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.STENCIL_TEST_VAL_EXPORT_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) becomes `FragStencilRefEXT` when available, 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 and `STENCIL_OP_VAL_EXPORT_ENABLE`. A device without the extension uses the bounded replay described above. - [DB_SHADER_CONTROL.DEPTH_BEFORE_SHADER](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `EarlyFragmentTests`, and so does `Z_ORDER` `EARLY_Z_THEN_LATE_Z` or `EARLY_Z_THEN_RE_Z` without kill, Z export, mask export or stencil export; with alpha-to-coverage the tests stay late. The meaning of the bit and of `Z_ORDER` 3 follows Mesa (`src/amd/registers/gfx10.json`; RADV sets the bit for `early_fragment_tests` in `src/amd/vulkan/radv_shader.c`). What the console does with `DEPTH_BEFORE_SHADER` and a Z export, a mask export, a stencil export or alpha-to-coverage together was not measured. - [PA_SC_CONSERVATIVE_RASTERIZATION_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 0x6001 uses Vulkan overestimation on devices that add at most 1/256 pixel and keep degenerate triangles; the console's uncertainty region and degenerate handling are unmeasured. A value with `OVER_RAST_ENABLE` and `UNDER_RAST_ENABLE` clear is decoded as disabled even when `UNCERTAINTY_REGION_MODE` or the `OUTER_`/`INNER_UNCERTAINTY_EDGERULE_OVERRIDE` bits (19:16) are set, as PPSA01474 writes 0xe6000; that these fields have no effect without over- or underestimation is assumed from their names, not measured. - [Descriptor pool chain](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - each pool holds two storage buffer descriptors more than a set can take, as a workaround: RADV (Mesa 26.0 and later) lets a `FREE_DESCRIPTOR_SET` pool place a set up to 32 bytes past the pool's buffer ([`util_vma_heap_init`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_descriptor_pool.c) is given `bo_size + RADV_POOL_HEAP_OFFSET` as a size), and with a page-multiple pool size those bytes are an unmapped page @@ -89,7 +94,8 @@ - [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. - [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 color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. A uniform compute fill over exactly the whole color memory with an exact clear value is not stored into guest memory, since only the host image holds the samples. A target is retired only when its whole guarded range is unmapped or its allocation removed, so if a title reuses that memory for other data while it stays mapped, a read the guard does not check (a buffer read through a V# or a pointer) sees the bytes from before the fill. -- [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 texture no draw rendered to](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - sampling a multisampled texture whose surface no draw rendered to creates the host target from the T# and clears it to the color a single 32-bit value repeated over the whole footprint encodes, padding up to whole tile blocks included. Inferred, not measured on the console: that the samples of such a surface are the guest color memory, as a title that clears its canvas on the CPU (ps5-opengl `ps5_clear_msaa4_color`) relies on. Memory that is not one repeated value, tile modes other than Standard4KB, Standard64KB and RenderTarget, unmapped memory and values with no exact clear color throw. CPU writes to the surface after its host target exists are not seen by the host. +- [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve and an FMASK decompress (`CB_COLOR_CONTROL` mode 5) are modeled. The FMASK decompress only applies the pending CMASK fast clear to the host image; it requires a CMASK address, and the target's DCC and FMASK contents are not interpreted. DCC over a multisampled target is accepted only in that pass and throws everywhere else. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. - [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. - [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off. - [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2, and throw on others. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each. 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. @@ -104,6 +110,7 @@ - [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. +- [COPY_DATA from a register](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 0) copies the value the queue holds for a shader, context or user config register (`COUNT_SEL` two consecutive ones) and throws for a register with no value. A dispatch records `COMPUTE_DISPATCH_INITIATOR` and, for `DISPATCH_DIRECT`, `COMPUTE_DIM_X/Y/Z` in the shader registers; `DISPATCH_INDIRECT` clears the three counts. Neither the register values the CP writes nor the copy's ordering against in-flight dispatches have been measured on hardware. - [INDEX_ATTRIBUTES_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x91) loads INDEX_BASE, INDEX_BUFFER_SIZE (in indices) and INDEX_TYPE from the 16-byte record at `attribute_base + attribute_index * 16`, read when the packet runs. The record layout (address low, address high, size, type) is PAL's [`IndexAttribIndirect`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9Chip.h#L60-L67) and the packet is the one [`BuildIndexAttributesIndirect`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9CmdUtil.cpp#L2586-L2613) writes; neither is measured on a PS5 or seen in a title. `indirect_mode` 1 (`addr_offset`, which only PAL's ExecuteIndirect path emits) throws, and so does a record whose index type is above 3, as INDEX_TYPE does. - [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 @@ -124,11 +131,14 @@ - [Table S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) DISABLE_CUBE_WRAP faults on selection without `VK_EXT_non_seamless_cube_map`: no runtime dimension check, including valid non-cube direct/table T#s. Unselected entries do not fault; with the feature this bit alone is accepted. Table T#/direct S# faults only for cubes needing the missing feature. - An [image table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) whose record range overlaps a buffer or storage image written by its own dispatch faults (`table written by its dispatch`) when any entry is selected, even if the write misses the selected record. - An [image table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) T# whose surface has no mapped byte faults when selected instead of becoming a zero texture as a direct T# does; what the console returns for it is not measured. -- [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. +- [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 from the recompiled code; in the shaders seen they are metadata or constants the caller reads relative to its program counter, and those reads go to the captured guest code, which linking does not change. A caller that jumps through a table among those words, which the control flow analysis reads from the recompiled code, 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. +- [Table callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is two consecutive dwords of an `s_buffer_load_dword*`, or a copy of them made with `s_mov_b32` or `s_mov_b64` in the same block, calls one of the targets that buffer holds when the load's buffer descriptor comes from an `s_load_dwordx4` without SOFFSET from a user-data pointer and the load's SOFFSET from an `s_lshl_b32` by 2 to 24 bits. At dispatch the descriptor and the buffer are read from guest memory, and every non-zero 64-bit value at the loaded offset plus a multiple of the shift's stride, below the descriptor's size (stride times num_records, or num_records bytes with stride 0), is read as a user-data callee and appended once per call. The call becomes a branch to a chain that compares the source pair with each target (`s_cmp_lg_u32` with a literal and `s_cbranch_scc1` per dword) and `s_call_b64`s the copy that matches; no match runs the original `s_swappc_b64` as an unlinked call. There is one chain for each value SCC has at the call, and the chain sets SCC back to that value (`s_cmp_eq_u32 0, 0` or `s_cmp_lg_u32 0, 0`) before the callee or the unlinked call runs. The searched range is the descriptor's whole size, which covers the range the load can reach whether it is checked in bytes or in strides; which one the PS5 uses for `s_buffer_load` was not measured. More than 64 distinct targets, a buffer above 64 MiB or a copy out of `s_call_b64` range throws. As with user-data callees, the buffer is read again only when a dispatch misses the dispatch cache, so a target a title adds to the buffer later is an unlinked call when a cached program runs. - [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. - [Comparison-sampled R32 float and R16 unorm textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) whose surface a storage image holds are sampled from a D32_SFLOAT or D16_UNORM image that the GPU refills from the storage image whenever its version moves, because Vulkan compares only depth formats. Only surfaces of at least 1 MiB (`APS5_DEPTH_COPY_MIN_KIB`) that are not 3D take the copy; smaller ones stay CPU snapshots compared against guest memory. With copies of small surfaces, one title hit an intermittent boot-time crash that was not explained. - [Null vertex buffer V#](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware. - [Descriptor pool recycling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) avoids individually freed sets: [RADV 26.2.2](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_descriptor_pool.c) initializes its free-set heap with 32 bytes beyond the backing allocation. An allocation-only RX 6600 test reproduces acceptance of a 4,128-byte set in a 4,096-byte pool; the path without individual frees rejects it. Pools reset after their last set is released. A cached set can retain unused slots in an exhausted pool until eviction; peak gameplay pool memory remains unmeasured. - [16-bit depth with stencil](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is stored as `D24_UNORM_S8_UINT`, or `D32_SFLOAT_S8_UINT`, on a device without `D16_UNORM_S8_UINT` as an attachment. Depth comparisons, clears and the depth image see the wider format's precision, so results at the 16-bit quantization boundary can differ from the console; the constant depth bias is scaled by 256 for D24, and a non-zero constant on the D32 float fallback throws. Not measured on console hardware. +- [Depth surfaces created over storage writes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth surface created without HTILE copies its depth plane from the storage image at the plane's address, a pending one first, when that image has the plane's extent, one level, a 2D view or a 2D array view (its layer 0 is copied) and a 16-bit format for a 16-bit plane or a 32-bit format for a 32-bit plane. A multisampled surface, a surface the device stores in another format, a surface with HTILE and a surface without such an image start from `DB_DEPTH_CLEAR`, and the stencil plane from `DB_STENCIL_CLEAR`; CPU writes to the plane's memory are not read. Not measured on the console, where the plane is that memory. - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; a title that waits only on release interrupts sees one extra event per flip, with `data` counting it. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less 2 GiB (imports and type-3 mirrors share it; #167's rule). A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A refused import is tried again at its next use. The 64 MiB window, the 256-lookup protection and the 2 GiB reserve are choices, not measured. +- [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index 21b00e1b01..0546015bae 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -20,3 +20,4 @@ - [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 - [AJM M4AAC decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 2) - Ghost of Yotei (PPSA26344) initializes it through `sceAjmBatchJobControl`. The initialize parameters (`u32` config type, 1 ADTS or 2 raw, then `u32` sampling frequency index), instance flags (channels in bits 3-6, `ENABLE_SBR_DECODE` bit 32, `ENABLE_NONDELAY_OUTPUT` bit 33, surround interleave order bits 34-35), `{heaac, reserved}` codec info and decoder error result (`CODEC_ERROR | FATAL`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/ajm/ajm_aac.cpp) (PS4, fdk-aac) and are unverified on PS5; decoding uses FFmpeg's AAC decoder. Without `ENABLE_NONDELAY_OUTPUT`, the first two frames after initialization or reset are consumed without output. Raw input takes its channel configuration from the instance (0 means stereo) and is decoded as one access unit per run job. The format reports the last frame's bitrate. Invalid config type or raw sampling frequency index above 11 returns invalid-parameter result 4. The implementation throws for ADTS without a frame header, profiles other than AAC LC, multiple raw data blocks per ADTS frame, unsupported channel configurations, more channels than the instance supports, HE-AAC without `ENABLE_SBR_DECODE`, raw input with `RUN_MULTIPLE_FRAMES`, output room smaller than the previous frame length, and unknown codec flags. - [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. +- [AJM Opus mapping family 255 with several channels](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): the initialization words carry no stream count or channel mapping, so the decoder opens on the first packet. It counts the self-delimited streams of that multistream packet ([RFC 6716 appendix B](https://datatracker.ietf.org/doc/html/rfc6716#appendix-B)) and takes each stream's stereo flag from its TOC byte as coupled, then decodes with an identity channel mapping. A packet whose streams do not add up to the channel count throws. The layout is inferred, not read from the console; unverified on hardware. diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 017fe07cf4..75a1c10469 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -2,8 +2,10 @@ ### Unknown function info -- [sceAudioOut2UserGetSupportedAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp) (libSceAudioOut) - the user handle and two uint32 output pointers follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. The masks describe this runtime: no context attributes and port attributes PCM (bit 0) and gain (bit 1). Null outputs and unknown or destroyed user handles throw; console validation order and error values are unknown. Users are tracked for this query, but port ownership and busy-user destruction are not implemented. The other PPSA23566 imports remain declared without parameters, with unknown signatures: [sceConvertKeycodeGetCharacterFromKeyboardData](../../../core/libs/prx/libSceConvertKeycode/Export.cpp), [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp), [sceNpCommerceDialogOpen2](../../../core/libs/prx/libSceNpCommerce/Export.cpp), [sceProprietaryVoiceChatHelper{Initialize,Terminate,SetVoiceChatState,GetVoiceChatUsageState}](../../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp), [sceVoice{GetResourceInfo,ResetPort,EnableChat,DisableChat}](../../../core/libs/prx/libSceVoice/Export.cpp). Names from the shadPS4 aerolib NID list -- [sceAudioOutExClose, sceAudioOutExConfigureOutput, 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 +- [sceAudioOut2ContextAdvance and sceAudioOut2ContextPush](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - with an SDL device, Advance snapshots each port's PCM and gain; Push consumes completed grains in order and a blocking Push waits for Advance when none is ready. Inferred from PPSA26358, which rewrites the same 512-frame, 12-channel float buffer on a producer thread and pushes on another thread. Console buffer ownership, snapshot timing and empty-queue errors are unverified. More than 16 completed grains awaiting Push and a nonblocking Push without a completed grain throw; the limit and errors are runtime restrictions, not console measurements. Context destruction concurrent with active calls remains unsupported. Without an SDL device the existing wall-clock path remains. + +- [sceAudioOut2UserGetSupportedAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp) (libSceAudioOut) - the user handle and two uint32 output pointers follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. The masks describe this runtime: no context attributes and port attributes PCM (bit 0) and gain (bit 1). Null outputs and unknown or destroyed user handles throw; console validation order and error values are unknown. Users are tracked for this query, but port ownership and busy-user destruction are not implemented. The other PPSA23566 imports remain declared without parameters, with unknown signatures: [sceConvertKeycodeGetCharacterFromKeyboardData](../../../core/libs/prx/libSceConvertKeycode/Export.cpp), [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp), [sceProprietaryVoiceChatHelper{Initialize,Terminate,SetVoiceChatState,GetVoiceChatUsageState}](../../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp), [sceVoice{GetResourceInfo,ResetPort,EnableChat,DisableChat}](../../../core/libs/prx/libSceVoice/Export.cpp). Names from the shadPS4 aerolib NID list +- [sceAudioOutExOpen, sceAudioOutExConfigureOutput, sceAudioOutExClose](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - the HDMI bitstream calls follow [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/docs/FINDINGS.md#L31-L80), which disassembles firmware 12.70's libSceAudioOut and logs the calls on a console. `sceAudioOutExOpen(user, mode)` returns `0x80260015` for a mode above 10 or outside mask `0x61F`; otherwise it opens a port of internal type 6 in S16 stereo, 256 frames at 48000 Hz for modes 0, 1, 2 and 9 and 1024 frames at 192000 Hz for modes 3, 4 and 10. `sceAudioOutExConfigureOutput(control, flags, mode, target, option)` returns `0x80268009` when `control` is not 0, takes modes 0 to 10 and 0xFF and targets 1 to 3 and 0xFF, and returned 0 on the console; `sceAudioOutExClose` returned 0. The argument registers match [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L26-L28) and PPSA99051's [bitstream_out.cpp](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/sample-app/src/platform/ps5/bitstream_out.cpp#L29-L32). Audio reaches the host through SDL as PCM and the host has no HDMI bitstream output, so an Ex port has no host device: `sceAudioOutOutput` paces it at its own rate and drops its IEC 61937 bursts, and `sceAudioOutExConfigureOutput` changes nothing. Nonzero `flags` or `option`, other modes or targets, `sceAudioOutExClose` of a handle that is not an Ex port, and `sceAudioOutGetPortState` on an Ex port throw, since the console's answers are unknown - [sceAudioOutExGetMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) reports a sink without audio descriptors: it zeroes the buffer and returns 0, as `sceAudioOutSysGetHdmiMonitorInfo` does, since audio reaches the host through SDL as PCM and there is no EDID to describe. The arguments `(0, 0, info, size)` and the descriptor count as a 32-bit value at +0x98, with 8-byte descriptors from +0xA0, follow [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L62-L117) (PPSA99505), which passes a 0x400-byte buffer and read that layout from one TV; with no descriptor listed it decodes the audio itself instead of opening a bitstream port. The meaning of the first two arguments and the structure's size are unknown: nonzero first or second arguments and sizes below 0xA0, which cannot hold the count, throw, and every byte of the declared size is written as 0. A null buffer returns `0x80260004` like the other AudioOut calls; the console's code is not measured - [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - assumed to take `(user_id, latency)`: KytyPS5 reads `(user_id, output, latency_us)`, but the known calls set just edi and esi; only `(0xFF, 2)` (PPSA26344) and `(0xFF, 1)` (PPSA14632) are accepted - [sceAudioOut2MasteringInit](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - flag values unknown; only 0 is accepted. Whether the console reports an error for a repeated `sceAudioOut2MasteringInit` or for `sceAudioOut2MasteringTerm` without a matching initialization is unknown; both return 0. PPSA01289 creates the audio device of its next world before it destroys the previous one, so it calls Init twice before Term twice, and only logs a non-zero result @@ -16,10 +18,11 @@ - [sceAudioOut2PortCreate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - the stereo fold is assumed, not the console's downmix: C at -3 dB into both sides, Ls/Rs, Lb/Rb and Ltf/Rtf at -3 dB and Ltb/Rtb at -6 dB into their own side, LFE dropped - [sceAudioOut2PortSetAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - each PCM buffer set (attribute 0) is taken as one grain: buffers set between two pushes are queued and played one per grain, at most 16 queued. A grain with no new buffer repeats the last one only when the port has always been given that same buffer (rewritten in place); a port whose buffers move on plays silence instead. Inferred from PPSA12544, whose object port (type 6) is given one grain of a 64-grain ring per set, often two or three between pushes and then none for a few; the console behaviour for several sets per push and for a push without a new buffer is not documented - [sceAudioOutDeviceIdOpen](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA01317, which fails to load without it; declared without parameters, signature unknown. It opens a port on a given audio device; which device ids exist and how they map to host outputs is unknown, so it throws +- [sceAudioOut2PortGetState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) and [sceAudioOut2GetSpeakerInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp) (libSceAudioOut) - the port state reports the channel count of the port's data format, output main, volume 127 and a zero reroute counter and flags; the speaker info reports a stereo front pair at -30 and +30 degrees. Taking the channel count from the data format follows [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs), another emulator, and no console measurement exists for either call: the output of a pad port, the volume after `sceAudioOut2PortSetAttributes` and the reroute counter on a device change are unverified, as are the speaker types other than stereo. ASTRO BOT and ASTRO's PLAYROOM reportedly stop on an assertion that depends on these values (#4317), but which field matters has not been isolated ### Functional - [sceAudioOut2EnableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) takes no arguments and returns 0, as declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L460-L464) ("Allows chat audio alongside the application's own"). The host mixes no chat audio and AudioOut2 has no chat port, so the call only records that chat audio is allowed. Whether the console fails it without a chat session, when repeated or before `sceAudioOut2Initialize` is unknown. [sceAudioOut2DisableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) is the counterpart, imported by PPSA18216: it takes no arguments, returns 0 and records that chat audio is no longer allowed; its signature is not confirmed by a console measurement, and whether the console fails it without a chat session or when repeated is unknown. - [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. -- [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. +- [AudioOut2 contexts](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device and no `sceAudioOut2ContextAdvance` calls read a grain's port buffers at the next push, at least 100 us after the grain was pushed, and play the mix behind a 40 ms cushion that the queue level leaves out. When the hardware reads the buffers is not known. The timing follows a title whose mixer fills a buffer after pushing it, reuses it two pushes later, and may still be writing it when the next push follows within microseconds. The last grain pushed before a title stops pushing is never read or played. - [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. diff --git a/docs/dev/debt/libSceJpegDec.md b/docs/dev/debt/libSceJpegDec.md new file mode 100644 index 0000000000..342b2df171 --- /dev/null +++ b/docs/dev/debt/libSceJpegDec.md @@ -0,0 +1,9 @@ +# libSceJpegDec technical debt + +### Unknown function info + +- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - structure layouts follow [OpenOrbis `jpeg.h`](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/_types/jpeg.h#L13-L72) and [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L20-L117); attributes 1 and 2 are the values [SharpProspero's `JpegImage.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Graphics/JpegImage.cs#L64-L74) passes. The error codes are libScePngDec's: no reference lists the JpegDec ones, and SharpProspero raises 0x80650020 on its JpegDec path where it raises libScePngDec's 0x80690020 on its PngDec path. Image info always reports color space 3, suitable CSC attribute 1 and zero component ids and sampling factors; the console's values, including which images it reports attribute 2 for, are unknown. The coefficient memory size is 0 because the host decoder needs no coefficient buffer; the size the console reports for progressive images is unknown. `max_image_width` is not checked, and the packed `width << 16 | height` decode result is libScePngDec's + +### Functional + +- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - decode modes other than 0 and down scales other than 1 throw diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md new file mode 100644 index 0000000000..4da9e097aa --- /dev/null +++ b/docs/dev/debt/libSceNetCtl.md @@ -0,0 +1,5 @@ +# libSceNetCtl technical debt + +### Unknown function info + +- [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface that holds an IPv4 address, preferring the one that carries the default route: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, whatever the host uses, and the Ethernet address is the one `sceNetGetMacAddress` reports. A gateway or DNS server the interface does not have is reported as `0.0.0.0`; what a console reports then is unknown. The Wi-Fi codes (5-10), the DHCP host name (12), the PPPoE user name (13) and the HTTP proxy server and port (20, 21) throw diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 5416c84150..430758814e 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -12,7 +12,7 @@ - [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. - [NGS2 port matrix](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a port without a matrix, or with a selected matrix whose levels were never set, mixes through a default channel map: mono reaches both stereo channels at -3 dB and the 5.1 and 7.1 centre, stereo folds to mono at -3 dB per side, and wider sources fold down with 0.5 and -3 dB gains. The map and the equivalence of an unset matrix with no matrix were measured from the PS4 firmware 11.00 `libSceNgs2.sprx`, not yet from the PS5 library. That library only accepts submixer and mastering voices with 1, 2, 6 or 8 channels, so the map into 3, 4, 5 or 7 channels throws. A matrix set with fewer levels than the destination needs still mixes only the rows it has -- [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - structure layouts from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`; module id 0x1f for UserFx2, the voice parameter id `0x40001f00 | module index`, the process flags (1 on the first process call after setup, 2 after a parameter change, 0 otherwise) and the call order (setup per voice at rack creation, process after the inputs are mixed, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5). Only UserFx2 modules on a single buffer are accepted: other module ids, more buffers, a module control handler, different input and output channel counts, setup flags and default rack options throw. Each module's state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` still throws for this rack. As in KytyPS5, process runs only on grains where an input produced samples, so an effect tail stops with the input and a pending flag 2 waits for the next audible grain +- [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - structure layouts from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`; module id 0x1f for UserFx2 and 0x1d for UserFx, voice parameter ids `0x40001f00 | module index` and `0x40001d00 | module index`, the process flags (1 on the first process call after setup, 2 after a parameter change, 0 otherwise) and the call order (setup per voice at rack creation, process after the inputs are mixed, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5) and shadPS4. Only UserFx and UserFx2 modules on a single buffer are accepted: other module ids, more buffers, a module control handler, different input and output channel counts, setup flags and default rack options throw. Each module's state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` still throws for this rack. As in KytyPS5, UserFx2 process runs only on grains where an input produced samples, so an effect tail stops with the input and a pending flag 2 waits for the next audible grain - [NGS2 reverb rack](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native, rack 0x2001) - follows the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output at 11.025 to 48 kHz, including parameter changes during playback (differences at float rounding level); not yet compared with the PS5 library. Structure layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_reverb.h` and the param ids 0x20010000 (setup) and 0x20010001 (I3DL2) from PPSA22520. Only the default `reverb_size` of 1 is handled, with its 14400-sample reflection buffer; other sizes and the library's half-rate mode throw. System sample rates below about 7.5 kHz, where an allpass delay would have no samples, throw. Setup only accepts equal input and output channel counts - [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` diff --git a/docs/dev/debt/libSceNpCommerce.md b/docs/dev/debt/libSceNpCommerce.md index a3a899c134..7a9d0ec642 100644 --- a/docs/dev/debt/libSceNpCommerce.md +++ b/docs/dev/debt/libSceNpCommerce.md @@ -3,3 +3,7 @@ ### Silent stubs - [libSceNpCommerce](../../../core/libs/prx/libSceNpCommerce/Export.cpp) - the PS Store icon show/hide calls and `sceNpCommerceSetPsStoreIconLayout` do nothing + +### Unknown function info + +- [sceNpCommerceDialogOpen2](../../../core/libs/prx/libSceNpCommerce/Export.cpp) (libSceNpCommerce) - declared without parameters; follows [prosper](https://github.com/mattias800/prosper/blob/73413cb445b0364bb49c4d2478e9ee1614b9f7d9/prosper/src/hle/np/np.cpp#L500-L504), which serves it with its `sceNpCommerceDialogOpen` handler: every argument is ignored, nothing is validated, it returns 0 and the dialog finishes at once, so `sceNpCommerceDialogGetResult` reports it cancelled. The parameters and the console's error results are unknown. Call of Duty: Black Ops (PPSA34501) calls it at Zombies > Online diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 04435c2849..b828591f2b 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -5,7 +5,7 @@ - [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per library rather than per handle since one pad handle exists - [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - special ports (type 2) return `DEVICE_NOT_CONNECTED` (0x80920007) because the host has no special controller, and `scePadGetHandle` has no handle for them; whether the console opens a handle and reports the device as disconnected later is unknown. scePadOpenExt handles only special ports. PPSA01317 passes a 16-byte param that names the device by vendor and product id (`b7 0e 04 0e ...` for 0x0eb7:0x0e04) and skips the user on any negative result. Other port types throw - [scePadVrControllerReadState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `iA-DdobUen8`) - the name hashes to the imported NID; the `(int handle, void* state)` signature follows [PS5VR](https://github.com/theghostonline/PS5VR/blob/0e408909ce7a39891a2e06eee3438d07a09e11e3/app/src/app_vr.c#L326-L369) (PPSA99177), which passes a PS VR2 Sense controller handle from `scePadOpen` type 3 or 4 and a 0x68-byte state it reads as buttons at 0, stick bytes at 4 and a connected byte at 0x40. The title calls it only after `sceHmd2Initialize` succeeds, which it does not here, so every call throws; the state layout is otherwise unknown -- [scePadSetVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 0 to 3 are accepted and stored; the meaning of mode 3 is unknown (PPSA21480 calls it with 3 right after `scePadOpen` and aborts on failure; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Pad/Pad.cs) names only 1 and 2). The stored mode has no other effect +- [scePadSetVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 1 to 3 and 5 are accepted and stored. On a PS5 (firmware 13.60) the call returns `0x80920005` before `scePadInit`, then rejects a mode of 0 or below with `INVALID_ARG` before it checks the handle, and returns `INVALID_HANDLE` for a bad handle with any higher mode; whether an open handle accepts modes above 3 was not measured, since the payload could not open a pad; 5 is accepted because PPSA25286 passes it on its open handle at start-up; the meanings of modes 3 and 5 are unknown (PPSA21480 calls it with 3 right after `scePadOpen` and aborts on failure; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Pad/Pad.cs) names only 1 and 2). The stored mode has no other effect - [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 - [scePadUnknown_zBsgF0q8DIM](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `zBsgF0q8DIM`) - PPSA03671 imports it and fails to load without it. The aerolib list has no name for the NID, and none of the pad names in SharpProspero, shadPS4 or OpenOrbis, nor the trigger effect, motion sensor, VR controller, HD and haptic names tried, hashes to it. It is declared without parameters and throws. The title's executable has an import stub for it but no call, jump or address reference to that stub, so the argument count and whether the result is used are unknown diff --git a/docs/dev/debt/libScePlayGo.md b/docs/dev/debt/libScePlayGo.md index ecf89fb032..b7dcfacd48 100644 --- a/docs/dev/debt/libScePlayGo.md +++ b/docs/dev/debt/libScePlayGo.md @@ -2,4 +2,5 @@ ### Unknown function info -- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - the chunk set comes from `playgo-chunkdefs.xml` (every listed chunk plus chunks 0 through the default chunk), otherwise from `sce_sys/playgo-chunk.dat`, whose `plgx` header is read as holding the chunk count as a 16-bit value at offset 0x0A and ids 0 to count - 1 are taken as valid. The field was identified on four dumps, where exactly that many 0x20-byte entries follow from offset 0x100 (RIDE 4 PPSA01599: 35, Persona 5 Royal PPSA05110: 25, LEGO Horizon Adventures PPSA14632 and Kena PPSA01802: 1); the rest of the format is not decoded and ids are assumed to be contiguous. Every listed chunk is reported as installed (`LOCAL_FAST`). Without either file, a package with `sce_sys/playgo-scenario.json` (PPSA01748, whose dump lists no chunks) accepts every chunk id as installed while `scePlayGoGetChunkId` lists only chunk 0, and a package with no `playgo-*` file in `sce_sys` (PPSA03001, PPSA03490 and PPSA01285 here) accepts only chunk 0 and returns `SCE_PLAYGO_ERROR_BAD_CHUNK_ID` for any other id; a `sce_sys` that holds other `playgo-*` files (`playgo-ficm.dat`, `playgo-hash-table.dat`) without `playgo-chunk.dat` or `playgo-scenario.json`, or a `playgo-chunk.dat` without a `plgx` header and a nonzero count, throws naming the file, because no such dump is known; the two answers above are unverified on a console, as is the console's answer for an id outside a package's chunk table. +- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - the chunk set comes from `playgo-chunkdefs.xml` (every listed chunk plus chunks 0 through the default chunk), otherwise from `sce_sys/playgo-chunk.dat`, whose `plgx` header is read as holding the chunk count as a 16-bit value at offset 0x0A and ids 0 to count - 1 are taken as valid. The field was identified on four dumps, where exactly that many 0x20-byte entries follow from offset 0x100 (RIDE 4 PPSA01599: 35, Persona 5 Royal PPSA05110: 25, LEGO Horizon Adventures PPSA14632 and Kena PPSA01802: 1); the rest of the format is not decoded and ids are assumed to be contiguous. Every listed chunk is reported as installed (`LOCAL_FAST`). Without either file, a package with `sce_sys/playgo-scenario.json` (PPSA01748, whose dump lists no chunks) accepts every chunk id as installed while `scePlayGoGetChunkId` lists only chunk 0. A package with no `playgo-*` file in `sce_sys` (PPSA03001, PPSA03490 and PPSA01285 here) accepts chunk 0; querying any other id throws with a message naming the missing `playgo-chunk.dat` and `playgo-scenario.json`, since the incomplete dump cannot establish whether that id exists. A `sce_sys` that holds other `playgo-*` files (`playgo-ficm.dat`, `playgo-hash-table.dat`) without `playgo-chunk.dat` or `playgo-scenario.json`, or a `playgo-chunk.dat` without a `plgx` header and a nonzero count, throws naming the file, because no such dump is known. These answers are unverified on a console, as is the console's answer for an id outside a package's chunk table. +- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - `scePlayGoGetSupportedOptionalChunk` and `scePlayGoGetOptionalChunk` report every bit set for any `type`, i.e. every optional chunk (language pack) supported and installed, like `scePlayGoGetLanguageMask`. The bit layout and the values of `type` are not known; PPSA01748 calls both with type 0 and hides every text language listed in `playgo-scenario.json` `chunkSupportedLanguages` when the bitmask is 0. The console's answer for a package that ships no optional chunks is unverified. diff --git a/docs/dev/debt/libScePsml_debug.md b/docs/dev/debt/libScePsml_debug.md index 9dd2ab63c4..3042611621 100644 --- a/docs/dev/debt/libScePsml_debug.md +++ b/docs/dev/debt/libScePsml_debug.md @@ -4,6 +4,6 @@ - [scePsmlMfsr2CreateContext, scePsmlMfsr2CreateSharedResources, scePsmlMfsr2GetContextInitRequirement, scePsmlMfsr2GetDispatchPackets, scePsmlMfsr2GetDispatchPacketsSizeInDwords, scePsmlMfsr2GetSharedResourcesInitRequirement, scePsmlMfsr2Init, scePsmlMfsr2ReleaseContext, scePsmlMfsr2ReleaseSharedResources](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - unknown signatures, imported by PPSA23566; declared without parameters - [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; only 0x8A810001 (not initialized) is returned +- [scePsmlMfsrEnableSpecularAA, scePsmlMfsrEnableQuality8K](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_specularAA and libScePsml_quality8K) - imported by PPSA01317, which fails to load without them; signatures unknown, so they throw. The names are not in the aerolib NID list; they hash to the imported NIDs `xNZM9Y5lf-M` and `lcZD9tJraAw` (`tools/nid_names.py`) - [scePsmlMfsrGetDispatchMfsrPacket1000](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - unknown signature, imported by PPSA25344; declared without parameters. libScePsml and libScePsml_debug are built from the same source - [scePsmlMfsrCreateContext1300, scePsmlMfsrGetDispatchMfsrPacket1300](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - unknown signatures, imported by PPSA28416 next to `scePsmlMfsrGetContextBufferRequirement1100`; declared without parameters. The names are not in the aerolib NID list; they hash to the imported NIDs `kLF7WtQ1-Dg` and `DDVUyPyYHTA` (`tools/nid_names.py`) -- [NIDs `xNZM9Y5lf-M` and `lcZD9tJraAw`](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_specularAA and libScePsml_quality8K, exported as `scePsmlUnknown01` and `scePsmlUnknown02`) - imported by PPSA01317, which fails to load without them; names and signatures unknown (not in the aerolib NID list), so they throw diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index 6759fd2a9e..ca4be61ba7 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -2,7 +2,7 @@ ### Unknown function info -- [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. +- [sceSaveDataPrepareForTransferring](../../../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. The name hashes to the NID but is not in aerolib; it was found by combining words from the libSceSaveData names in aerolib. Its arguments and behaviour are unknown. - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits - [sceSaveDataConvert](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the parameter block (user id at `+0x00`, `SceSaveDataDirName` pointer at `+0x08`, 48 opaque bytes after it) is read from PPSA28416's single call, not confirmed by KytyPS5, shadPS4, fpPS4 or SharpProspero, which have no body for it. Host saves have no size limit, so an existing save returns `SCE_OK` without touching its files and 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 - [sceSaveDataGetConvertProgress](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA16064; signature and error codes follow the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-get-convert-progress.md) (`float* progress`). Save data conversion on host filesystems completes synchronously, so the progress query returns `SAVE_DATA_OK` with the last conversion's progress (`0.0f` before any conversion or when conversion fails, `1.0f` after successful conversion), `SAVE_DATA_ERROR_PARAMETER` if `progress` is null, or `SAVE_DATA_ERROR_NOT_INITIALIZED` when the library is uninitialized; intermediate asynchronous conversion progress on console is unmeasured diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 17773650c4..3e5bcecac2 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -16,5 +16,5 @@ ### Functional -- [sceSystemServiceLoadExec](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - besides `"exit"`, it starts the title's own `/app0/eboot.bin` or another executable under `/app0` that the relinker converted with `--executable`, which is looked up beside the running one (`/app0/picker/picker.bin` as `picker.picker.bin` with the running executable's extension). The main executable is the one `APS5_EBOOT` names, which the first call sets to its own path. After the guest shutdown the new executable gets the argument list as its command line, after its own path (`CreateProcessW` on Windows, `execv` on Linux), and the old process ends without running host destructors and without waiting, so its direct memory is released before the new one allocates. [VLC for PS5](https://github.com/ZiZc3/VLC/blob/86bb7d474a303f62bb3c2fc12f728db4846085b8/src/platform_ps5.c#L565-L571) (PPSA85300) relaunches itself with no arguments; RetroArch (PPSA99169) starts its frontend picker with `{"", NULL}`, and the picker starts `eboot.bin` with `{"--ps5-mode=retroarch", NULL}` or `/app0/es-de/es-de.bin` ([picker.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/frontends/picker/picker.cpp#L444-L446)), so the list holds only arguments. What the console puts in `argv[0]` is not known; here it is the host path. On Windows the new process is a separate process, so a shell or script that started the title sees it end. Paths outside `/app0`, unconverted executables and macOS hosts throw +- [sceSystemServiceLoadExec](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - besides `"exit"`, it starts the title's own `/app0/eboot.bin` or another executable under `/app0` that the relinker converted with `--executable`, which is looked up beside the running one (`/app0/picker/picker.bin` as `picker.picker.bin` with the running executable's extension). The main executable is the one `APS5_EBOOT` names, which the first call sets to its own path. After the guest shutdown the old process ends without running guest exit handlers or host destructors. `"exit"` ends the process after the host shutdown without running the title's `atexit` and `__cxa_atexit` handlers, since the console's shell closes the title instead; a line the title writes just before through its own thread, such as a forwarded standard error line, may not be printed. [VLC for PS5](https://github.com/ZiZc3/VLC/blob/86bb7d474a303f62bb3c2fc12f728db4846085b8/src/platform_ps5.c#L565-L571) (PPSA85300) relaunches itself with no arguments; RetroArch (PPSA99169) starts its frontend picker with `{"", NULL}`, and the picker starts `eboot.bin` with `{"--ps5-mode=retroarch", NULL}` or `/app0/es-de/es-de.bin` ([picker.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/frontends/picker/picker.cpp#L444-L446)), so the list holds only arguments. A list with a first entry becomes the whole guest `argv`, with no program name in front ([LoadExec.cpp](../../../core/libs/prx/libSceSystemService/LoadExec.cpp)): a [PS5 RetroArch console transcript](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/evidence/loadexec-second-image/relaunch-test.jsonl) records a restart with one argument entering with `argc=1`, `argv[0]="--ps5-relaunch=1"`, and a launch from the home screen with `argc=1`, `argv[0]=""` ([relaunch_ps5.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/src/relaunch_ps5.cpp#L161-L164)). For such a list Linux replaces the process with `fexecve`: the PID and the standard streams stay, the other descriptors are closed and the working directory becomes the executable's. Windows starts a suspended process that inherits only the standard streams and a handle of the old process, and its entry stub waits for the old process to end before it loads the title's libraries, so a shell or script that started the title sees it end. A null list or one without a first entry keeps the earlier launch: the new executable gets only its own path as its command line (`CreateProcessW` on Windows, `execv` on Linux), so its `argv[0]` is the host path, and the old process ends without waiting for it. What the console passes for such a list is not known, nor are its argument limits and error codes. Paths outside `/app0`, unconverted executables and macOS hosts throw - [Running application ID](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - `sceSystemServiceGetAppIdOfRunningBigApp` reports 1 as the running title's app ID. The value is AnyPS5's own: the console's shell assigns app IDs and their values are not known here, so other calls that take an app ID must use the same one. `sceSystemServiceKillApp` with that ID and the arguments -1, 0 and 0, which [SwanStationPS5](https://github.com/darkxex/SwanStationPS5/blob/4cdc981d1a64ffb7f2af73f7a27a9e91a3cb5eb4/src/main.c#L1050-L1058) passes to close itself and [ps5-payload-dev's hbldr](https://github.com/ps5-payload-dev/websrv/blob/46374edee6627885abac717de24da7a1d73af6f1/src/ps5/hbldr.c#L425-L435) passes to close the running game, ends the title as its own `exit(0)` would: the guest's shutdown and `atexit` handlers run and the process exits with status 0. On the console the call returns and the shell then ends the title, and whether those handlers run there is unknown; here there is no shell, so the call does not return. Another app ID or other arguments throw. diff --git a/docs/dev/debt/libSceUserService.md b/docs/dev/debt/libSceUserService.md index fe81330fce..085b02a1a2 100644 --- a/docs/dev/debt/libSceUserService.md +++ b/docs/dev/debt/libSceUserService.md @@ -3,6 +3,7 @@ ### Unknown function info - [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` +- [sceUserServiceInitialize, sceUserServiceTerminate](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - measured on a PS5 (firmware 13.60): before `sceUserServiceInitialize`, and again after `sceUserServiceTerminate`, `sceUserServiceGetLoginUserIdList`, `sceUserServiceGetEvent` and `sceUserServiceTerminate` return `0x80960002`; a second initialization returns `0x80960003`, and a new initialization reports the login of the initial user again. The other calls were not measured before initialization and do not check it. `sceUserServiceInitialize2` was not measured: it initializes like `sceUserServiceInitialize` and returns success when already initialized - [sceUserServiceGetUserColor](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the default profile colour is assumed to be blue (0) - [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 - [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 diff --git a/docs/dev/debt/libSceVideodec2.md b/docs/dev/debt/libSceVideodec2.md index ee47bcc9aa..5d7b802774 100644 --- a/docs/dev/debt/libSceVideodec2.md +++ b/docs/dev/debt/libSceVideodec2.md @@ -9,3 +9,4 @@ - [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` is not exported; `sceVideodec2GetVp9PictureInfo` has its own VP9 layout - [sceVideodec2GetVp9PictureInfo](../../../core/libs/prx/libSceVideodec2/Export.cpp) uses the 88-byte layout and two-pointer signature from [SharpProspero Videodec2.cs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs). Frame metadata follows the [VP9 specification](https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.7-20170222-draft.pdf), sections 5.26 and 6.2: profile, key/intra-only flags, bit depth, color space/range, coded/render dimensions and superframe count are retained per emitted buffer. Level is 0 because the elementary stream does not signal a level; the console value is unverified. Null pointers, undersized structures and querying another codec throw; console error codes and support for other structure sizes are unknown. Existing NV12 output rejects pictures deeper than 8 bits. Superframes are submitted as individual coded frames to keep each output associated with its input metadata. +- [sceVideodec2Decode](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) and `sceVideodec2Flush` output the coded picture, with the chroma plane after its luma rows, as the console does for 1080p H.264 and HEVC streams coded at 1920x1088. An HEVC or VP9 picture whose size is not a multiple of 16, such as HEVC coded at 1920x1080, is output at that size; whether the console pads it is unverified. `sceVideodec2GetPictureInfo` reports no frame cropping diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6eaefa8c88..f16a03c5ab 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -22,6 +22,7 @@ - [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 - [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 +- [sceKernelConvertUtcToLocaltime, sceKernelConvertLocaltimeToUtc](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) convert with the host time zone at the given instant, as libc `localtime` does, instead of the console's time zone setting. `west_sec` holds the standard-time offset in seconds east of UTC, despite its name, and `dst_sec` the daylight saving seconds in effect. This follows PPSA99203 ([PS5SX2](https://github.com/Swordpdf/PS5SX2/blob/b4cf55e87c34e8a2a1cd9302df4373e5a94d28f4/ps5/frontend/fe_ps5.cpp#L54-L63)), whose author saw a console write 7200 and 3600 into the 16-byte block; which zone that console was set to is not recorded, so whether `west_sec` includes daylight saving is not settled. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/23c41333/src/core/libraries/kernel/time.cpp) also stores seconds east. PPSA99203 declares the last argument of `sceKernelConvertUtcToLocaltime` as `uint64_t*`; 32 bits are written. On Windows the standard offset and the daylight saving amount are the C runtime's current `_get_timezone` and `_get_dstbias` for every instant, as libc `localtime_s` uses them, so instants under a zone's earlier rules get today's offsets; a `TZ` variable does not set `_dstbias`, which keeps the value an earlier `_tzset` read from the system (0 when the system zone has no daylight saving), and summer time then adds nothing. In a repeated or skipped local hour, `sceKernelConvertLocaltimeToUtc` takes the offset in effect one standard offset earlier. Its last argument receives the zone's daylight saving amount whether or not it is in effect, found by sampling the host zone over the year around the result, while `dst_sec` is the amount in effect: the libc `mktime` that PPSA99109 bundles (text 0x70ebb-0x70ee5) subtracts that amount when `tm_isdst` is set outside summer time. That `mktime` passes `tm_isdst` as the second argument, which is ignored, so it does not choose the offset in a repeated hour. `sceKernelGettimezone` and the `gettimeofday` time zone still report no offset and no daylight saving. A host time the C runtime cannot convert throws - [getuid, geteuid, getgid, getegid and issetugid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) report the root identity (0) and a process that never changed credentials (`issetugid` 0), as a jailbroken process has; the host user and group are not exposed to the guest. The values a title sees on a console are unverified - [sceKernelGetAppInfo](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - only `app_id` and the title ID (offset 0x10, up to 9 characters plus a terminator) are written; callers declare the structure with different sizes, so the rest is left alone. `app_id` is 1, the running title's id in libSceSystemService, which is a guess, not something measured. The error codes follow shadPS4: `EPERM` for another process, and 0 without writing anything for a null pointer. - [AMPR object layout and compact gathers](../../../core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp) - the 24-byte object and offsets (type 0, cursor 4, count 8, capacity 12, pointer 16) follow [ampr_emu assertions](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr_emu_command_buffer_types.h#L138-L143) and its [C declaration](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr.h#L41-L47). AnyPS5 represents the 32-bit type word as low 16-bit type and high 16-bit recording flags. Command encoding is AnyPS5 internal: opcode 29 and three 32-bit words encode the high offset byte, length minus one and low offset word. The 12-byte size reduces HFW's 1 MiB buffer pressure; it is not a claim about console opcode encoding. Existing 40-bit offsets and lengths 1 through 2^32 are retained; console behavior outside those limits is unverified. @@ -60,6 +61,7 @@ - [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 - [Fixed anonymous mmap](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `MAP_PRIVATE | MAP_ANON | MAP_FIXED` replaces the requested range through the existing guest mapping machinery and zeroes the new memory. `MAP_EXCL` is checked under the allocation-registry mutation lock, including virtual reservations, and returns `ENOMEM` without changing an occupied range; without `MAP_FIXED` it is `EINVAL`. Lengths are rounded to 16 KiB, while fixed addresses must be aligned and the complete rounded range must lie inside the guest arena. This follows FreeBSD 11.4 [argument validation and rounding](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L226-L299), [fixed replacement](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1455-L1464), and [collision errno](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1604-L1613). The console behaviour has not been measured. AnyPS5 starts the guest arena at 0x200000000, so the fixed path rejects the low 0x400000 address requested by PICO-8 PPSA99808. The console address-space layout is unmeasured; executable placement outside the host arena is tracked in #5217. Static catalog disassembly finds `0x1012` in PPSA98001/PPSA99808 and `0x5012` in PPSA99808; these titles were not run. +- [Mapping hints with no room above them](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - a mapping without `MAP_FIXED` whose hint lies above the guest address space, or has no free range at or above it, is placed from the bottom of the guest arena, as FreeBSD 11.4's [`vm_mmap_object`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1569) retries through [`vm_map_find_min`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1555-L1584) from its minimum address. FreeBSD's minimum is the end of the largest possible data segment rather than the start of the arena, and the console's address-space layout is not measured. PS5CEMU-HAR (PPSA99360) gives `sceKernelReserveVirtualRange` a hint near its own code, which is a host address here because the relinked executable lies outside the guest arena, so its JIT gets memory more than 2 GiB from its code and does not start - [_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 - [tcgetattr, tcsetattr](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) - every file and pipe descriptor is taken as not a terminal (`ENOTTY`), and a socket gets `EADDRNOTAVAIL` for `AF_INET` and `EOPNOTSUPP` otherwise, as FreeBSD's `in_control` and `pru_control_notsupp` answer a termios ioctl; what descriptors 0 to 2 of a title are on the console, and whether any of them is a terminal there, is unknown - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - descriptor 0 is never reported readable and descriptors 1 and 2 are always writable, as for a console title with no input; reads from descriptor 0 still go to the host stdin. `/dev/random` and `/dev/urandom` are always readable and writable, as FreeBSD reports a seeded random device. Guest sockets and libSceNet sockets are polled; other descriptors are polled through the host on Linux, while on Windows pipes are checked every millisecond with the readiness test `poll` uses and any other descriptor throws. Dungeons (PPSA16064) selects on descriptor 0, and RetroArch (PPSA99169) selects on its web server's wake-up pipe next to its listening socket @@ -95,21 +97,24 @@ - [uDGXwUKqi8s](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature - [sceKernelGetCpuTemperature, sceKernelGetSocSensorTemperature](../../../core/libs/prx/libkernel/System/src/ThermalSensors.cpp) (libkernel) - read the host's thermal sensors: on Linux from `/sys/class/hwmon` and `/sys/class/thermal`, on Windows from the ACPI thermal zones (`MSAcpi_ThermalZoneTemperature` over WMI) and the storage temperature property of the first physical drive. Sensor 0 is the CPU/package value, 1 the first other value and 2 an NVMe value; a host that exposes none reports every sensor as unsupported (`EINVAL`). The console's SoC channel count and meanings are hardware-dependent and differ from the host's, the host sensors are not the console's, the ACPI reading is unavailable on many machines, and the storage temperature property is numbered 52 on Windows 10 2004 and later and 22 before it, so the mapping, the values and the unsupported-input error are unverified; the storage channel was checked against the drive's own SMART composite temperature on one NVMe host (`StorageDeviceTemperatureProperty`, 36 °C on a WD_BLACK SN7100). `sceKernelGetCpuTemperature` reports sensor 0. PPSA99058 and PPSA99420 call both - [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 -- [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` and `scePthreadSetschedparam` accept those two policies with a priority of 256 to 767 or 896 to 959 and keep the policy, as measured; a created thread takes the policy of explicit scheduling attributes, or its creator's when it inherits. When it inherits, it still takes the priority of its attributes, where the console gives it its creator's priority +- [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` and `scePthreadSetschedparam` accept those two policies with a priority of 256 to 767 or 896 to 959 and keep the policy, as measured; a created thread takes the policy and priority of explicit scheduling attributes, or its creator's when it inherits, as it does with a null attribute - [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 - [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) (libkernel, NID `LzoM-wVLJDE`) - no source gives the signature; `int (void)` is assumed from `sceKernelIsAddressSanitizerEnabled` and from PPSA23226's bundled `libSceJobManager_nosubmission.prx`, which passes no arguments and tests only the 32-bit result - [getdtablesize](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - throws: FreeBSD returns the smaller of the `RLIMIT_NOFILE` soft limit and `maxfilesperproc` ([`sys_getdtablesize`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L338-L355)), and neither value is known for the console, where `getrlimit(RLIMIT_NOFILE)` throws too. VLC (PPSA85300) imports it for a `closefrom` loop in the child of its spawn helper ([libc_stubs.c](https://github.com/ZiZc3/VLC/blob/d46be6a68f4244466b041c2dc1b640915ac1a983/ps5/compat/libc_stubs.c#L66-L72)), which never runs because its `fork` fails with `ENOSYS` +- [close, sceKernelClose](../../../core/libs/prx/libkernel/File/src/HostStreams.cpp) (libkernel) - descriptors 0 to 2 that are open when libkernel loads belong to the host process: closing one fails with `EBADF` and leaves it open. The console's result is unknown. PPSA21703 calls `sceKernelClose(0)` repeatedly; closing the host stream let the host C runtime give descriptor 0 to the next file opened, which the next call then closed ### Functional +- [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors is unsupported, as for `shm_open`. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws - [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters; other filters, `EV_DISABLE`, `EV_DISPATCH`, `EV_FORCEONESHOT`, `NOTE_LOWAT`, `EV_CLEAR` on a socket, `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro), any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) and a negative `nevents` throw. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop -- [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title +- [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. The three Windows notifications are told apart in the error, which names the `parent`, `entries` or `subdirectories` watch, the path it was given and `GetLastError`, but a `FindFirstChangeNotification` that fails still throws rather than returning an error to the guest: the EVFILT_VNODE attach path is [`vfs_kqfilter`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L4936), which returns `EINVAL`, `ENOMEM` or 0 after `fget` has turned a descriptor that is not open into `EBADF`, and consults no directory permission, so no FreeBSD errno stands for a host that refuses to notify, and what the console does on such a host is not measured. Only the `parent` watch can fail this way in practice: `entries` and `subdirectories` need the `FILE_LIST_DIRECTORY` that opening the watched directory already took. The parent handle produces no hint of its own, as `RunHostWatch` only calls `FindNextChangeNotification` on it, so a later change may leave the watch on its 50 ms poll instead of throwing; that is not done here. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title - [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. - [tcsendbreak, tcdrain, tcflush, tcflow, tcgetpgrp, tcsetpgrp](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) follow FreeBSD 11.4's [`termios.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/termios.c): `tcflush` answers `EINVAL` for a queue other than `TCIFLUSH`, `TCOFLUSH` or `TCIOFLUSH` and `tcflow` for an action outside `TCOOFF`..`TCION` before the descriptor is looked at, and every other call fails as its terminal ioctl does on a descriptor that is not a terminal, with the errors `tcgetattr` gives, so `tcsendbreak` never reaches its 0.4 s pause. FreeBSD's `pipe_ioctl` answers `TIOCGPGRP` and `TIOCSPGRP` on a pipe or FIFO with the descriptor's SIGIO owner, which is not modelled, so `tcgetpgrp` and `tcsetpgrp` throw on a host pipe or FIFO above descriptor 2; descriptors 0 to 2 are taken as non-terminals that are not pipes, whatever the host attaches to them, so a pipe the guest creates on a freed descriptor 0 to 2 gets `ENOTTY` there. The PS5 `libkernel` is assumed to use this FreeBSD code, and `tcdrain` is not a cancellation point here - [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond - [EVFILT_VNODE on macOS](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) registers `EVFILT_VNODE` on a duplicate of the descriptor in a host kqueue, whose notes have FreeBSD's values, and drops the `NOTE_LINK` that XNU adds to `NOTE_DELETE` when the watched file or directory is removed, since FreeBSD's `vop_remove_post` and `vop_rmdir_post` report `NOTE_DELETE` alone. As on Windows, a directory registered with `NOTE_EXTEND` is accepted but never receives it: XNU reports a move between directories as `NOTE_WRITE` alone, with `NOTE_LINK` for a subdirectory - [Windows file identity](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails +- [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target, a junction and any other reparse point throw, as for `readlink` - [setitimer](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `sys_setitimer` and `kern_setitimer`: `EINVAL` for timers other than `ITIMER_REAL`, `ITIMER_VIRTUAL` and `ITIMER_PROF`, and for a value, or the interval of a non-zero value, with a negative field, `tv_usec` of 1000000 or more, or more than `INT32_MAX / 2` seconds; a null value reads the timer as `getitimer` does, with `EFAULT` when the output is null too. `ITIMER_REAL` shares the timer thread of `alarm`, which clears its interval: a zero value disarms it, the previous value is returned, and each expiry re-arms it by `it_interval`. An expiry delivers nothing: with `SIG_IGN` it is dropped, with the default disposition the process exits with status 142, and with a handler, or with `SIGALRM` blocked, it throws, since guest `SIGALRM` handlers are never run and signals are never left pending. The value is not raised to `tick / 16` as `itimerfix` does, and an interval below 1 ms throws, since the console's `hz`, and so that rounding, is unknown (1 ms covers any `hz` of 63 or more). `ITIMER_VIRTUAL` and `ITIMER_PROF` throw when armed, as process CPU time is not accounted, and read and disarm as zero. PPSA99420 imports it and fails to load without it - On Linux, [direct memory](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [system](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified @@ -133,7 +138,7 @@ - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) does not consult the target thread's signal mask - [pthread_sigmask](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) shares the process-wide mask of `sigprocmask` and `_sigprocmask`, which keeps only the first 32 signals; FreeBSD keeps a mask per thread - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) reaches a thread that waits in an alertable `TimedWait` wait through an APC. A thread blocked anywhere else, including all waits under `APS5_COARSE_TIMED_WAITS` and host waits inside the emulator, is redirected and runs the handler only when that wait returns -- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs the handler wherever the target thread was interrupted. It waits only while the thread is inside winpthreads or restoring a context through `NtContinue`, and throws if that lasts 1 s, so a handler that calls into other host code the thread was interrupted in, such as the C runtime's heap or a lock of the emulator, can deadlock on it or reenter it +- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs the handler wherever the target thread was interrupted. It waits only while the thread is inside winpthreads or restoring a context through `NtContinue`, and throws if that lasts 1 s, so a handler that calls into other host code the thread was interrupted in, such as the C runtime's heap or a lock of the emulator, can deadlock on it or reenter it. A thread that holds the address-wait lock of `sceKernelSyncOnAddressWait`, `sceKernelSyncOnAddressWake` and `_umtx_op`, or the park queue lock of the guest mutexes, gets the raise as an APC instead, which runs once it has released the lock or while it waits; the other locks of the emulator are not deferred this way - [sigaltstack](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records each thread's alternate signal stack and reports it back, following FreeBSD's `kern_sigaltstack` (`EINVAL` for flags other than `SS_DISABLE`, `ENOMEM` below `MINSIGSTKSZ` 2048), but handlers never run on it, so `SS_ONSTACK` and `EPERM` are never reported - [sigpending](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) reports the intersection of the process-wide pending set of `sigtimedwait` and the blocked mask of `_sigprocmask`, as FreeBSD's `sys_sigpending` intersects the pending list with `td_sigmask`, and returns -1 with `EFAULT` for a null set. Because `_sigprocmask` keeps only the first 32 signals, signals 33 to 128 are never blocked and never appear in the set, and setting `SIG_IGN` (or `SIG_DFL` on a signal whose default action is to ignore) does not clear a signal that was already marked pending. PPSA99420 imports it - [scePthreadJoin, scePthreadCreate](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - on a PS5 (firmware 13.60) joining a thread a second time returns `ESRCH` (`0x80020003`); 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 @@ -155,11 +160,12 @@ - [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. - [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. +- [MSG_DONTWAIT](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `send`, `sendto`, `sendmsg`, `recv`, `recvfrom` and `recvmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`). Winsock has no per-call non-blocking flag, so on Windows these calls still fail with `EOPNOTSUPP` for it - [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. `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 - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number -- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket nor a pipe (files, stdio, libSceNet sockets) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked +- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket nor a pipe (files, 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`), an infinite timeout with no socket or pipe 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 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level only, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT`, `SO_TIMESTAMP` and `SO_NOSIGPIPE`, which FreeBSD stores and no title has been seen to set, the send and receive timeouts and low-water marks among them) and every other level (`IPPROTO_TCP`, `IPPROTO_IP`, `IPPROTO_IPV6`) fails with `ENOPROTOOPT`. None of this was measured on a console. - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had @@ -173,7 +179,8 @@ - [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` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console -- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `EINVAL` for every other command, `F_GETFD` and `F_SETFD` included). The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux, where `F_GETLK` with `F_UNLCK` throws. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. 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`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). 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, where `F_GETLK` with `F_UNLCK` throws. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console +- [fcntl on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answers `F_GETFD`, `F_SETFD`, `F_GETFL` and `F_SETFL` and fails every other command with `EINVAL`; FreeBSD 11.4's `kern_fcntl` also duplicates the descriptor (`F_DUPFD`, `F_DUPFD_CLOEXEC`) and handles `F_GETOWN` and `F_SETOWN` on a socket. `FD_CLOEXEC` is only recorded (there is no `exec`): `dup`, `dup2` and an accepted socket start without it, `SOCK_CLOEXEC` and `F_SETFD` set it. None of this was measured 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, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, 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 - [Regular file mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - on Linux `mmap` maps a regular file's descriptor shared or private like a shared memory object, with the same access checks; SQLite's WAL index (`-shm`) in Dusklight PPSA99305 needs it. FreeBSD 11 `mmap(2)` and shadPS4's `MemoryManager::MapFile` map regular files; the console's behaviour is not measured. Other descriptors (pipes, sockets, directories, devices) still fail with `EOPNOTSUPP`, and Windows maps no files. - [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 diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 92d3cea3b1..bbae61a42b 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -4,9 +4,12 @@ - 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. 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. +- The bundled [SPIRV-Tools build](../../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy. A depth-first traversal keeps one value map and removes each subtree's added values on return, avoiding growing map copies and preventing sibling values from leaking into one another. This extends the processed-map release fix in [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). Remove the patch when the pinned dependency has equivalent copy-free traversal; #6962 alone still copies maps for sibling branches. System-dependency builds need an equivalent fix in their installed package. Optimization passes and validation remain unchanged; wider shader and game performance measurement is needed. + ### Unknown function info - [Packed pixel ancillary word](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) (shader recompiler) - the sample index at bits 8-11 and the render target index at bits 16-28 follow #295. The sample index is the SampleId builtin, an equality inferred, not measured. The render target index is the Layer builtin when the draw exports it (a layered draw, `PA_CL_VS_OUT_CNTL` bit 18; the pipeline specialization constant `LayerExported`) and 0 otherwise, the first layer Vulkan renders when no pre-rasterization stage writes Layer. Reading the unwritten Layer builtin instead makes AMD's Windows driver drop the fragment's writes (#4800); with the constant at 0 the load is folded away. Mesa reads both fields from those bits, with the layer 14 bits wide only from GFX12 on ([ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c#L342-L368)). A value passed through a select, move or phi whose every read is an extract inside one of those fields is rebuilt from the two fields alone. Any other read the masked-select pass cannot drop gets the whole word: the primitive class at bits 0-1 (0 point, 1 line, 2 triangle, 3 rectangle), which the draw sets as a pipeline specialization constant ([RasterPrimitiveOf](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp)) in the shaders that read it, plus the sample field, with every other bit zero. That layout comes from the RDNA 3 and 3.5 ISA guides (prim type 1:0, VRS rates 3:2 and 5:4, eye id 7, sample 11:8, render target index 28:16). The GCN register guides give the same prim type and sample fields and put the render target index at 26:16, so the two sources agree for layers below 2048. AMD's LLPC reads the primitive class from bits 1:0 with the same encoding ([LowerGlCompatibility.cpp](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/llpc/lowering/LowerGlCompatibility.cpp#L830-L836)) and without a GFX-level check ([getPrimType](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/LowerInOut.cpp#L5307-L5315)). Nobody has checked it on gfx1013. In the whole word only bits 0-1 and 8-11 have a source. Bits 2-7, 12-15 and 29-31 are assumed zero, not measured: the VRS rates (2-5) and the eye id (7) because the driver supports neither variable-rate shading nor multiview, so a title that enables either would read other bits on the console, and bits 6, 12-15 and 29-31 because no source gives them a meaning. The sample field reads SampleId, which is always 0 here because the driver rejects MSAA and per-sample shading (`PA_SC_AA_CONFIG`, `PS_ITER_SAMPLE`). The whole word has the same 0 at bits 16-28. A change that makes draws export a layer must restore the Layer read in both places +- [Front-face input with `FRONT_FACE_ALL_BITS`](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (shader recompiler) - with `SPI_BARYC_CNTL.FRONT_FACE_ALL_BITS` set, a back face reads 0 and a front face 0xffffffff. Mesa 22.0 RADV sets the bit ([radv_pipeline.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-22.0.0/src/amd/vulkan/radv_pipeline.c#L5953)) and ACO tests the input with `v_cmp_lg_u32 0` ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-22.0.0/src/amd/compiler/aco_instruction_selection.cpp#L8123-L8128)), so only back = 0 and front ≠ 0 are sourced; PPSA05684's two-sided cloth shaders test it with `v_cmp_lt_u32 0`. The exact front value is not measured. Without the bit the input is ±1.0, as current Mesa reads it (`fgt 0`, `fsign`; [ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c)) - [Color comparison descriptor forms](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): emulated color comparison supports UNORM, SNORM and float formats for 2D, 2D-array and cube views. 8-bit sRGB and UNORM (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`, `Format8UNorm`, `Format8_8UNorm`, `Format8_8_8_8UNorm`) return 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255. Anisotropic filters are refused, except on those 8-bit formats, where every texel comparison is 0 (not measured with an anisotropic S#). Bilinear cube comparisons of float, snorm and the other unorm formats without `DISABLE_CUBE_WRAP` are refused; on that hardware a 4x4 R32 float cube blends across faces (46 of 90 edge rows differ from a per-face clamp; face 0, ref 0.1, u=3.9 v=2.5 is 0.6016 and u=1.5 v=0.1 is 0.3984), and with `DISABLE_CUBE_WRAP` the per-face clamp matches. An 8-bit sRGB or UNORM bilinear cube without that bit returns 0, since every texel comparison is already 0 for every compare function and the selected face cannot change the result. Cube-edge sampling behavior and console coverage on PS5's gfx1013 remain unverified. - [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 - [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 @@ -18,6 +21,7 @@ ### Functional +- [Unlinked calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) - the `s_swappc_b64` a table call's compare chain falls back to, when its target matches no linked copy, records the `UnlinkedCall` fault with the target and the program counter and stops the invocation, or continues after the call in programs whose faults do not stop invocations (see `bdaStopsInvocations`). The driver lists those calls in the request (`linkedCallFallbacks`), and it reports the fault like a BDA fault, naming the target: a dispatch that checks its faults when it completes throws, one whose check runs in a deferred write-back logs it and continues. The hardware would jump to the target, which is not modelled. Any other `s_swappc_b64` whose target is not resolved still throws when the shader is compiled. - [Cached source IR](../../../core/shader/recompiler/Recompiler.cpp) releases lazily allocated SSA construction tables after resource preparation and reclaims values unreachable from instructions, metadata and resource plans when retaining a pending source program. Immediate shader preparation skips reclamation because compilation consumes the program. Reclamation must account for every new IR pointer owner; a remaining external use throws. Live IDs are retained. Source and variant entries remain cached without an eviction bound. A pending source program is released after its first variant is obtained; memory beyond the measured title checkpoint and Linux performance remain unmeasured. - [Vertex user-data tail jumps](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) link a linear raw vertex prolog's terminal `s_setpc_b64` through an unchanged user-data pair to its continuation. The instruction semantics follow AMD RDNA2 ISA section 12.3; the prolog convention follows Mesa `aco_select_vs_prolog.cpp` at `0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8`. Console hardware has not been measured. Branching prologs, computed low pointers and continuations reading `s_getpc_b64` remain unsupported - [Raw fragment user-data tails](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) also link a terminal `s_setpc_b64` when the low half is unchanged user data and the last high-half writer is an unconditional immediate `s_mov_b32`. Other computed high halves remain unsupported. This follows AMD RDNA2 ISA section 12.3 and Mesa `aco_select_nir.cpp` (`create_fs_jump_to_epilog`), `aco_isel_helpers.cpp` (`convert_pointer_to_64_bit`) and `aco_lower_to_hw_instr.cpp` (`p_jump_to_epilog`) at `0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8`; console hardware has not been measured @@ -64,7 +68,8 @@ - A value read through a null or unmapped pointer still throws at record time if a [uniform fill or table base](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) needs it, or if the pointer comes from user data and is not null, even if the console skips the read. - A [V# or flat SRT word](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) read through a null pointer taken from user data is zero and counts as a [poisoned SRT read](../../../core/shader/recompiler/Recompiler.cpp), so a dispatch that reads it is not cached. The console faults if the program executes the read; PPSA28416 branches around it with `s_cbranch_execz`. The fault is not modeled. - A [V#, T# or S#](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) loaded through a null or unmapped pointer read from memory is zero when the work is recorded, and the capture throws if no flat SRT slot records that read. -- [2D accesses through a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read the texel the 1D addressing selects and ignore the second coordinate, as simonbbby measured on RDNA2 (gfx1036) in #2237 for `image_sample_lz` and mip-less `image_load` with point filtering and wrap, clamp and border addressing; the PS5's gfx1013 is not measured. A plain `image_sample` (implicit LOD) through such a pair takes its LOD from the derivatives of the first coordinate alone, measured on RDNA2 (gfx1036) with the [hardware oracle](../HW_ORACLE.md) on the quads of a compute kernel (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): on a 16-texel, 5-mip 32_UINT T# with point filters it picks the same mip and texel as DIM 1D for first-coordinate steps of 0, 1, 2, 4 and 8 texels per lane along x or along y, whatever the second coordinate does (normalised steps up to 16 per lane), while the same rows on a 16x16 2D T# move the mip with the second coordinate. Outside the pixel stage a plain `image_sample` is emitted with LOD 0 for every image type, so this pair reads mip 0 there, where the kernel measurement shows the quad's derivatives being used. Other 2D accesses through a 1D T# (storage, depth, packed, BY loads, mips, explicit LOD, bias, offsets, derivatives, comparison) still throw. +- [2D accesses through a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read the texel the 1D addressing selects and ignore the second coordinate, as simonbbby measured on RDNA2 (gfx1036) in #2237 for `image_sample_lz` and mip-less `image_load` with point filtering and wrap, clamp and border addressing; `image_sample_l` was measured the same way on gfx1036 against `image_sample_l` with DIM 1D through the same T#, for u inside, on and past the edges, v of 0, 0.5, 1, -0.25 and 3, LOD -1 to 6 on a 5-level R32_FLOAT texture, point, linear and linear-mip filtering, wrap, clamp and border addressing and HEIGHT or DEPTH 3: all 4725 rows matched. The PS5's gfx1013 is not measured. A plain `image_sample` (implicit LOD) through such a pair takes its LOD from the derivatives of the first coordinate alone, measured on RDNA2 (gfx1036) with the [hardware oracle](../HW_ORACLE.md) on the quads of a compute kernel (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): on a 16-texel, 5-mip 32_UINT T# with point filters it picks the same mip and texel as DIM 1D for first-coordinate steps of 0, 1, 2, 4 and 8 texels per lane along x or along y, whatever the second coordinate does (normalised steps up to 16 per lane), while the same rows on a 16x16 2D T# move the mip with the second coordinate. Outside the pixel stage a plain `image_sample` is emitted with LOD 0 for every image type, so this pair reads mip 0 there, where the kernel measurement shows the quad's derivatives being used. `image_sample_c_lz` and plain `image_sample_c` through such a pair compare against the texel the DIM 1D comparison with the same reference and first coordinate reads, measured on RDNA1 (gfx1010, RX 5700 XT) with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): 8-texel 32_FLOAT and 16_UNORM T#s, also with HEIGHT 3, point and bilinear filters, wrap, clamp and opaque-white border addressing, less and gequal functions, second coordinates 0, 0.5, 1, 1.5, -0.25 and 3; on a 64-texel, 7-mip 32_FLOAT T# `image_sample_c` picks its LOD from the first coordinate's steps alone (1, 4 and 16 texels per lane give mips 0, 2 and 4; second-coordinate steps up to 64 texels leave it unchanged). Only comparisons the runtime does natively (32_FLOAT, 16_UNORM) reach the 1D view; comparisons the runtime emulates for other color formats still throw through such a pair. Other 2D accesses through a 1D T# (storage, depth, packed, BY loads, mips other than explicit LOD, explicit LOD with comparison, bias, offsets, derivatives) still throw. +- [3D image accesses bound to a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) follow a [hardware oracle](../HW_ORACLE.md) measurement on RDNA1 (RX 5700 XT, gfx1010; wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even), not the PS5's gfx1013, on 8x1 R8G8B8A8_UNORM linear 1D T#s with HEIGHT and DEPTH 0 or 3 and point filters. `image_sample_lz` reads the texel u selects for v and r in {0, 0.5, 1, 1.5, -0.25, 3}, with wrap, clamp and border addressing, and v and r never produce the border; u wraps, clamps and borders as with DIM 1D. Mip-less `image_load` reads the x texel for any y in {0, 1, 2, 3, -1} when z is 0, returns zero for z in {1, 2, 3, -1}, whatever DEPTH says, and returns zero for x past the width. A plain `image_sample` (implicit LOD) on the quads of a compute kernel takes its texel and LOD from u alone: on a 64-texel, 7-mip 1D T# steps of 1, 4 and 16 texels per lane pick mips 0, 2 and 4, as DIM 1D `image_sample_l` does, and steps in v or r alone stay at mip 0. Outside the pixel stage a plain `image_sample` is emitted with LOD 0, as for 2D accesses through a 1D T#. Every other 3D access of such an image (`image_load` with mips, storage, explicit LOD, bias, derivatives, offsets, comparison, gathers, queries) keeps throwing. SONIC SUPERSTARS (PPSA06888) samples a 1x1 1D T# this way with plain `image_sample` and `image_load`. - [Emulated comparison of a 2D-array access through a 2D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) samples the 2D image and ignores the slice, as the native modes of a 2D-array access through a 2D T# already do. This follows the T# type deciding the addressing; it is not measured on hardware for comparison samples - [Flat aperture bases](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - the layout follows the gfx9+ one in the AMD KFD driver's `kfd_flat_memory.c` (`MAKE_LDS_APP_BASE_V9`, `MAKE_LDS_APP_LIMIT`): each aperture is a 4 GB window whose base is the high dword, so a flat address is routed by its whole high dword, as measured on RDNA2 (gfx1035: `src_shared_base` 0x00010000_00000000, `src_shared_limit` 0x00010000_ffffffff). The PS5's base values, high dwords 0x80000000 (shared, `SharedApertureHigh`) and 0x70000000 (private, `PrivateApertureHigh`), are inferred from GTA V's traversal addresses, not measured on a console or read from SH_MEM_BASES. The scalar aperture sources read the same two values - [Flat accesses outside the aperture routing](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - only compute stages route flat accesses, so graphics-stage flat accesses to the shared or private aperture still go to BDA. In compute, a 64-bit flat atomic whose address is in the shared aperture takes the workgroup lock (`ldsLock`) for its two-dword update, and every such atomic requests that lock, so a shader with one pays for it even on global addresses (the path is exercised by `agc_driver_global_atomics_float`). Inferred, not measured on LDS or scratch: an aperture access that is not inside one dword is done byte by byte, an unaligned aperture atomic records the BDA unaligned fault, and LDS or scratch accesses past their end are dropped or read zero. @@ -98,13 +103,14 @@ - [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. - [Tessellation rings](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) of local, hull and domain programs become Vulkan tessellation interfaces and are never written to guest memory; a title that reads the off-chip or factor ring back sees no data. - [Hull input control points](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a hull LDS read at a control-point index computed from data reads `gl_in` at that index clamped to the patch's last input control point. On the console an index past the patch reads the LDS slot of a neighbouring patch or of no patch; the host has no such memory, and Vulkan leaves an out-of-range `gl_in` index undefined. The lowering refuses a computed offset that is not a whole number of control points, but the index itself is only known when the program runs. -- [Inner tessellation factors](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) of integer or fractional-odd triangle patches that are NaN tessellate as 1, as in the D3D11 reference tessellator; the console is not measured. +- [Inner tessellation factors](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) of integer, fractional-even or fractional-odd triangle patches that are NaN tessellate as 1 (fractional-even then raises them to 2, as Vulkan does), as in the D3D11 reference tessellator; the console is not measured. - [Hull programs](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) start from the hull launch registers (user data, ring bases, v0, v1); on the console they start with what the local program leaves, and that it leaves them unchanged is not checked. - [Quad tessellation factors](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) are taken as outer[0..3] then inner[0..1], 24 bytes per patch, as [Mesa stores them](https://github.com/mihawk-99/PS5_Mesa/blob/b3588f78dd7fd46fbadc7ecd487e5f14d4213e4b/src/amd/common/nir/ac_nir_lower_tess_io_to_mem.c); unmeasured on the console. - [VGT_HOS_MIN/MAX_TESS_LEVEL](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) clamp positive tessellation factors (inner ones to at least 1); outer factors that are zero, negative or NaN still cull the patch and such inner factors become max(HOS_MIN, 1). The console's clamp and cull order is not measured. - [Patch constants](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) that hull programs write and domain programs read are placed by their off-chip address modulo the record stride, as the two compile apart; a domain read of a field the hull never writes, or of only a neighbouring patch's record, is not detected. -- [Integer partitioning](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (`VGT_TF_PARAM.PARTITIONING` 0) is drawn with Vulkan's equal spacing, which [radeonsi programs as `PART_INTEGER`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L539-540), with factors bounded at 64 instead of fractional-odd's 63; how the console rounds a clamped factor up to a whole number of segments is not measured. Power-of-two (1) and fractional-even (3) partitioning throw. -- The [relative patch index](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) that hull and domain programs use as data is (PrimitiveID mod PRIM_GRP_SIZE) mod NUM_PATCHES, as if each primitive group and each instance started new threadgroups; it throws when GE_CNTL.BREAK_WAVE_AT_EOI is clear. Unmeasured; PPSA26344 programs 64 primitives for 63 patches. +- [Integer partitioning](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (`VGT_TF_PARAM.PARTITIONING` 0) is drawn with Vulkan's equal spacing, which [radeonsi programs as `PART_INTEGER`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L539-540), with factors bounded at 64 instead of fractional-odd's 63; how the console rounds a clamped factor up to a whole number of segments is not measured. Power-of-two (1) partitioning throws. +- [Fractional-even partitioning](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (`VGT_TF_PARAM.PARTITIONING` 3) is drawn with Vulkan's fractional-even spacing, which [radeonsi programs as `PART_FRAC_EVEN`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L536-537), with factors bounded at 64. Vulkan raises every factor that is not culled to at least 2 after `VGT_HOS_MIN/MAX_TESS_LEVEL`, so a `VGT_HOS_MAX_TESS_LEVEL` below 2 still tessellates at 2; the D3D11 range for this mode is also [2, 64], but the console's order of the HOS clamp and that minimum is not measured. +- The [relative patch index](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) that hull and domain programs use as data is (PrimitiveID mod PRIM_GRP_SIZE) mod NUM_PATCHES, with the draw-relative instance times the patches per instance added when GE_CNTL.BREAK_WAVE_AT_EOI is clear and the instances share hull threadgroups; a domain that uses it in such a draw, and a hull without shaderDrawParameters, throw. Unmeasured on hardware; PPSA26344 programs 64 primitives for 63 patches. - The [output modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the shader's float mode as measured on RDNA2 (gfx1035): the hardware applies it only with IEEE mode off and output denormals of the result's precision flushed (bit 1 of the `FLOAT_MODE` denormal field clear), and ignores it otherwise. On f32 results it is applied as the hardware does with f32 denormals flushed; a shader without a known float mode is translated with IEEE mode off and f32 denormals flushed, the mode titles use. On f16 and f64 results it is ignored, and it throws in the modes where the hardware applies it. - The [clamp modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows `DX10_CLAMP`, measured on RDNA2 (gfx1035) for f32, f16, packed f16 and f64 results at IEEE=0 and IEEE=1: with `DX10_CLAMP`=1 (and in shaders without a known float mode) a NaN result clamps to +0, including the quieted NaN that `v_pk_min_f16` and `v_pk_max_f16` return for a signaling NaN operand with IEEE=1 (measured on RDNA2 gfx1036 at both `IEEE_MODE` and both `DX10_CLAMP` settings); with `DX10_CLAMP`=0 a NaN result is written unchanged and every other value clamps to [0, 1] as before. f64 results follow the same rule on hardware, but a clamped f64 result with `DX10_CLAMP`=0 throws until it has an execution test. With `DX10_CLAMP`=0 the NaN's bits are those the translation already produces for the instruction without clamp, so where those are left to the driver (f32 and scalar f16 arithmetic) the payload is the driver's, not the hardware's input propagation. `v_dot2_f32_f16` and the integer clamps are unchanged. - [FP16_OVFL](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows the shader's float mode as measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f16 denormals kept and flushed: with `FP16_OVFL`=1 an f16 result that rounds to ±infinity is written as ±0x7bff unless a source operand is infinite, so `v_rcp_f16`, `v_rsq_f16` and `v_log_f16` of ±0 and `v_exp_f16` of 16 or more are clamped while an infinite input passes through, and `v_div_fixup_f16` keeps the infinity it writes for a zero denominator or an infinite numerator and clamps any other infinite quotient, a NaN one included. `v_cvt_pkrtz_f16_f32` rounds toward zero and never overflows. The sources are checked as the f32 values the translation computes with; the rule is applied to `v_fma_mix*_f16` with f32 sources and to `v_interp_p2_f16` without a measurement of infinite f32 sources or parameters. Round modes other than nearest even are not modelled, so the overflow threshold is the nearest-even one (65520). @@ -114,7 +120,7 @@ - [Converted unorm images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view): samples, gathers, LOD queries, 16-bit data and a DST_SEL of the missing W channel throw. - [Converted unorm image loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) divide by 2^n - 1 with the buffer format path's `OpFDiv`, which Vulkan allows to be up to 2.5 ULP off, so some drivers are not bit-exact. - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. -- [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. The same bit rule serves the buffer float atomics (`AtomicFloatBits`). Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given), and `ds_add_f32`, which keeps the host's `OpFAdd`. +- [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. The same bit rule serves the buffer float atomics (`AtomicFloatBits`). Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. - [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes and `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as measured on gfx1035 (Linux, alignment mode UNALIGNED; the console's mode is unknown). Formatted accesses off their element's alignment and atomics throw. - [32-bit bitfield masks](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`S_BFM_B32`, `V_BFM_B32`) use the truncating 32-bit shift specified by the [RDNA 1.0 ISA](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna-shader-instruction-set-architecture.pdf), sections 12.1 (opcode 36) and 12.12 (opcode 867). Width and offset use their low five bits. This matches gfx1035 (Linux) for every width/offset pair, with and without upper operand bits set, and both leave SCC unchanged; not measured on the console. @@ -122,13 +128,16 @@ - [Raw `buffer_load_dwordx2`](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) is one 64-bit access on the hardware (with or without glc), so it never sees half of a `buffer_atomic_swap_x2`. Only a glc load that is 8-byte aligned and in bounds, in a program that has the 64-bit storage view (it uses 64-bit buffer atomics), is read with one 64-bit atomic load; every other dwordx2 load is two dword loads and can tear against a concurrent 64-bit write: loads without glc, and loads in a program without 64-bit buffer atomics while another shader writes the pair with `swap_x2`. - [Buffer loads without GLC or DLC](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) are not coherent in a program that has GLC or DLC buffer accesses and no `buffer_gl0_inv`, `buffer_gl1_inv`, `s_gl1_inv` or `s_dcache_inv`: only its GLC and DLC loads, stores and atomics use the `Coherent` buffer view, so a plain load can return a value cached before another work-group's write. This follows the [LLVM AMDGPU memory model for GFX10](https://llvm.org/docs/AMDGPUUsage.html#memory-model-gfx10-gfx11), in which a load-acquire is a GLC DLC load followed by `buffer_gl1_inv` and `buffer_gl0_inv`; it is not measured on the console or on RDNA hardware. - [DS src2 operations](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) (`ds_*_src2_*`, GFX10.1 only) read `DS[B]`, with `B = A + 4 * sext(offset1[6:0]:offset0)`, and then update `DS[A]` with the matching 32-bit atomic, so the read of `B` is not atomic with the update of `A`. The form that takes the offset from the address (`offset1[7]` set), the 64-bit forms and GDS throw: whether the LDS address keeps `A[31:17]` in that form, and whether the 64-bit forms scale the offset by 4 or 8, is not documented. `ds_min_src2_f32`, `ds_max_src2_f32` and `ds_add_src2_f32` throw: on RDNA2 (gfx1035, the oracle's hardware, which has no src2 opcodes) `ds_min_f32`, `ds_max_f32` and `ds_add_f32` ignore `IEEE_MODE` but depend on whether f32 denormals are flushed (28, 28 and 12 of 196 special rows differ), and that is not modelled for LDS float atomics yet (#981). -- [s_swappc_b64](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) writes the link pair with the same model as `s_getpc_b64`: the guest return address `pc + 4` as a byte offset over `GetShaderBase`. Any other use of the link pair (arithmetic, compare, hash, store) throws, apart from the overwrites and shared links that [static scalar calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) allow; constant-offset folding over the link is not modeled. The special encoding `s_swappc_b64` with SDST 125 still decodes as `s_setpc_b64`, which matches the hardware: measured on RDNA2 (gfx1036) in wave32 and wave64, it branches to the source pair and leaves a marker SGPR pair untouched, and with an SGPR destination the link is `PC + 4`. +- [`ds_ordered_count`](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) (GDS only) reads its operand fields as LLVM encodes them for `llvm.amdgcn.ds.ordered.add` ([SIISelLowering.cpp](https://github.com/llvm/llvm-project/blob/llvmorg-19.1.0/llvm/lib/Target/AMDGPU/SIISelLowering.cpp#L8844-L8894)): `offset0` is the counter's byte offset (index times 4) and the whole GDS address, `offset1` bit 0 wave release, bit 1 wave done, bits 2-3 the shader type, bit 4 swap instead of add and bits 6-7 the dword count less one. `M0` holds the ordered wave ID, not a GDS address as for `ds_append` (radeonsi passes `TG_SIZE` bits 6-17 as the intrinsic's pointer in [si_compute_prim_discard.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-20.3.0/src/gallium/drivers/radeonsi/si_compute_prim_discard.c) and `gs_tg_info` bits 0-11 in [gfx10_shader_ngg.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-21.3.0/src/gallium/drivers/radeonsi/gfx10_shader_ngg.c), with "The GDS address is always 0 with ordered append"); it only orders the waves, which is not modeled, so it is ignored. Each wave adds the value of its first active lane to the counter with one device-scope atomic and every active lane gets the counter's previous value (the `ORDERED_APPEND_ENBL` dispatch modifier is accepted for it). The hardware also serializes the waves of a dispatch in launch order, which is not modeled: the waves add in any order, so counters a title reads back as ordered offsets give the same totals but may hand out ranges in another order. Which lane's value is added is not documented; the first active lane is assumed and not measured on hardware. Swap, counts of more than one dword and LDS forms throw. Covered by [GdsAppendConsume.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/GdsAppendConsume.cpp). +- [s_swappc_b64](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) writes the link pair with the same model as `s_getpc_b64`: the guest return address `pc + 4` as a byte offset over `GetShaderBase`. Any other use of the link pair (arithmetic, hash, store) throws, apart from the overwrites, SOPC compares and shared links that [static scalar calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) allow; constant-offset folding over the link is not modeled. The special encoding `s_swappc_b64` with SDST 125 still decodes as `s_setpc_b64`, which matches the hardware: measured on RDNA2 (gfx1036) in wave32 and wave64, it branches to the source pair and leaves a marker SGPR pair untouched, and with an SGPR destination the link is `PC + 4`. +- [2D biased sampling through a one-slice, single-level 3D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) supplies a zero third coordinate and retains the bias operation, with normalized point minification and magnification and no linear mip filtering. Measured on RDNA2 (AMD integrated gfx1036, driver 32.0.21036.18, Windows 11) by running an OpenCL-loaded native kernel with `image_sample_b` DIM 2D against 16x16x1 R32_UINT and RGBA8_UNORM 3D images: biases -16, -0.5, 0, 0.5 and 16 read the (u,v) texel in slice zero, including wrapped coordinates, point min/max reduction and the title's sampler with Z border addressing. Wave32, IEEE off, DX10 clamp on, denormals preserved, round to nearest even, FP16 overflow off; wave64 and the PS5's gfx1013 are not measured. Pixel-stage, direct, non-depth, unpacked samples whose only use is the plain 32-bit bias form are accepted; extra slices, any nonzero mip range, linear or anisotropic filtering and unnormalized coordinates throw. Vulkan readback is checked on NVIDIA RTX 5070 and AMD integrated graphics for positive and negative bias and point weighted-average/min/max reduction. +- [FORCE_DEGAMMA on RGBA8_UINT](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) materializes a direct image sampled only by plain biased pixel accesses as RGBA8_SRGB when every paired sampler forces degamma. Measured on AMD integrated RDNA2 gfx1036 (driver 32.0.21036.18, Windows 11, wave32, IEEE off, DX10 clamp on, denormals preserved, round to nearest even, FP16 overflow off): a native `image_sample_b` DIM 2D on a 16x16x1 RGBA8_UINT 3D T# returns integer channels without the bit, but the same floating-point decoded values as RGBA8_UNORM with it, including point min/max reduction and the title's exact sampler. The metadata and image view must both become floating-point sRGB; ignoring the sampler bit or retaining a UINT heap is incorrect. The accepted case requires one slice, mip zero, normalized point sampling and uniform FORCE_DEGAMMA across its samplers. Other image accesses, mixed sampler states, indirect descriptors and other integer formats remain refused. Wave64, other filtering modes and the PS5's gfx1013 are not measured. - 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, scalar source codes 235 to 238) read as the 64-bit apertures the flat routing uses: a base is the aperture's high dword (`SharedApertureHigh`, `PrivateApertureHigh`) over a low dword of 0, a limit is the same high dword over 0xffffffff. A 64-bit read (`s_mov_b64`, a 64-bit VALU source, a wave64 lane mask) gets the whole value and a 32-bit read gets the low dword. This follows LLVM's `SIRegisterInfo.td` (aperture registers are 64-bit, their low 32 bits are always 0 for a base and -1 for a limit) and `getSegmentAperture` (`AMDGPULegalizerInfo.cpp`, `SIISelLowering.cpp`: a 64-bit read whose high half is the aperture), the KFD's `MAKE_LDS_APP_LIMIT` (limit = base | 0xffffffff) and the gfx1030 and gfx1035 readouts on [#1190](https://github.com/boykopovar/AnyPS5/pull/1190) and [#3803](https://github.com/boykopovar/AnyPS5/issues/3803). Not measured on a console: the PS5's high dwords are the inferred values of the flat aperture bases entry, and the wave is taken to be in 64-bit address mode (PTR32 clear) - [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. - [PERSP_SAMPLE and LINEAR_SAMPLE](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) hold the pixel-centre barycentrics, like the centroid pairs. With one sample this is the sample position ([Vulkan standard sample locations](https://docs.vulkan.org/spec/latest/chapters/primsrast.html#primsrast-multisampling): (0.5, 0.5)); with more samples the console's per-sample weights are not computed and were not measured. - [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 -- [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. +- [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 is free to use wherever no return address can reach: a dataflow over the decoded instructions follows each dword of the pair from the calls that write it until a scalar load, SALU or VALU result is known to overwrite it (a VALU mask counts as one SGPR unless the request says wave64), and an instruction there that may read a dword still holding a return address throws; SOPC compares are exempt, as they only set SCC. 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. 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. A whole-word use that the masked-select pass keeps gets the composed word described under the packed pixel ancillary word. 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. - [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. - [BDA span reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) (shader recompiler) - `read_bda_span` is one selection: one arm calls `load_bda_span`, a `DontInline` function that holds the in-place dword loads and calls nothing, and the other arm is the per-dword byte fallback. Two other shapes of the same reads each hang a group of NVIDIA drivers, where a dispatch never finishes and the device is lost. With the loads emitted in the arm itself, a dispatch whose lanes took both arms hung on an RTX 3060 and an RTX 4060 Laptop GPU (Windows, driver 591.86) and an RTX 5060 (Linux, driver 590.48.01) ([#3291](https://github.com/boykopovar/AnyPS5/issues/3291)). With the loads and the fallback as two selections, the first under the span's condition and the second under its negation, it hung on an RTX 4070 Laptop GPU (Windows, drivers 595.97 and 596.36), an RTX 5080 (Windows, driver 610.62) and an RTX 4090 (Linux, driver 610.57.04) ([#4975](https://github.com/boykopovar/AnyPS5/issues/4975)). The current shape is measured on the RTX 4090 with driver 610.57.04 only, where it does not hang with or without SPIRV-Tools; a shape with the loads in their own `DontInline` function was reported passing on driver 591.86 in #3291. The cause inside the driver is not known. diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index ad5b146e65..2b594277b8 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -1,8 +1,8 @@ # Controller, keyboard and mouse input -AnyPS5 accepts SDL-mapped game controllers as well as its built-in keyboard and mouse bindings. Xbox-style controllers and PlayStation controllers recognized by SDL use the standard PS button layout; sticks and analog triggers are passed through, and controllers can be connected or disconnected while the game is running. The first recognized controller is used. AnyPS5 uses its built-in keyboard and mouse bindings when no configuration file is present. To change selected keyboard and mouse bindings, create `anyps5-input.ini` beside the generated game executable. Set `ANYPS5_INPUT_CONFIG` to use a file at another path. +AnyPS5 accepts SDL-mapped game controllers as well as its built-in keyboard and mouse bindings. Xbox-style controllers and PlayStation controllers recognized by SDL use the standard PS button layout; sticks and analog triggers are passed through, and controllers can be connected or disconnected while the game is running. The first recognized controller is used. AnyPS5 uses its built-in keyboard and mouse bindings when no configuration file is present. To change selected keyboard and mouse bindings, create `anyps5-input.ini` beside the generated game executable. Set `ANYPS5_INPUT_CONFIG` to use a file at another path. Both the game directory and that path may contain any characters, such as `ü` or `日本`. -For an Xbox-layout controller, A/B/X/Y map to Cross/Circle/Square/Triangle, LB/RB map to L1/R1, and LT/RT map to analog L2/R2. Start maps to Options. D-pad and stick-click buttons are supported. On a PlayStation controller, the touchpad click button is forwarded; finger touch coordinates are not implemented. +For an Xbox-layout controller, A/B/X/Y map to Cross/Circle/Square/Triangle, LB/RB map to L1/R1, and LT/RT map to analog L2/R2. Start maps to Options. D-pad and stick-click buttons are supported. On a PlayStation controller, touchpad clicks and the positions of up to two fingers on the touchpad are forwarded. On other controllers, View (Back) presses the touchpad at its center. Each non-empty line has the form `Action = Type:Value`. Action names are case-insensitive. A `#` or `;` starts a comment. The first line for an action replaces its built-in bindings; later lines for the same action add alternate inputs. Duplicate entries for the same action are ignored. Actions omitted from the file keep their built-in bindings. UTF-8 files with or without a byte-order mark are supported. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 29f34b4fd0..65a2a146b5 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -2,6 +2,10 @@ [**Online relinker**](https://boykopovar.github.io/AnyPS5/) - files stay on your device. +### Browser folder output + +The online converter can save Linux output as an uncompressed folder in supported browsers. It does not overwrite an existing destination folder. If conversion fails or is cancelled, it aborts the active file and attempts to remove the newly created output folder. If the browser prevents cleanup, the error names the folder that may contain incomplete files. + ## Input and conversion Use a clean ELF executable. Place its bundled ELF modules in `sce_module/`, `sce_modules/`, or `prx/` beside the input executable, or under the parent directory selected with `--sce-module-path `. The option accepts absolute paths and paths relative to the current working directory; do not include the `sce_module` directory itself. Only files directly inside the module directories are processed. SELF/fSELF containers are skipped with a warning; unwrap them before relinking if they need to be included. `tools/unwrap_self.py ` does that for a decrypted container: it places the segments at the offsets of the SELF segment records and restores `p_offset % p_align == p_vaddr % p_align`, and it refuses a container whose load segments all look encrypted. `prx/` can coexist with either `sce_module/` or `sce_modules/`. Both `sce_module/` and `sce_modules/` present, or all three absent, is an error. @@ -94,11 +98,14 @@ libs/ *.prx app0/ + eboot.bin sce_module/ ``` -Use `sce_modules/` or `prx/` instead of `sce_module/` if that is the input directory name. Relinker preserves each module's directory under `app0/` and prints its exact path. Place app resources in `app0/` separately. Copy the built system libraries from `build/core/libs/libs/*.prx` into `libs/`; use libraries built for the target OS. A custom `--rpath` changes the system library location. +Use `sce_modules/` or `prx/` instead of `sce_module/` if that is the input directory name. Relinker preserves each module's directory under `app0/` and prints its exact path. Place app resources in `app0/` separately, including the title's original `eboot.bin`: titles test `/app0/eboot.bin` to find their own directory, as it is always present on the console (ProsperoTV, ProsperoRadio, ProsperoLight and ProsperoLichess fall back to an empty `/data/homebrew/` and fail to load their fonts without it). Only the converted executable runs. Copy the built system libraries from `build/core/libs/libs/*.prx` into `libs/`; use libraries built for the target OS. A custom `--rpath` changes the system library location. + +Some applications also need their original `app0/sce_sys/param.json` at runtime. AnyPS5 reads this file for the title ID and display name; it must contain a non-empty `titleId` string and a non-empty `localizedParameters` object with a language entry that has a non-empty `titleName`. It selects the entry named by `defaultLanguage` (inside `localizedParameters`, or at the top level); if that entry is absent, it tries `en-US`, then the first object-valued language entry. Preserve the values from the title's own metadata and place the file under the same `app0/` directory as the other app resources. At startup, its presence in the working directory creates `data/`; a non-zero `downloadDataSize` also creates `download0/`. Without the file, neither directory is created. If it is missing or invalid, metadata calls such as title/window setup can fail with a `param.json` error. Use the generated files printed as `Guest module:` for bundled title modules. `libs/` is for AnyPS5 system libraries, not the original PS5 `.prx` files. Placing an original PS5 module in `libs/` on Windows makes Windows try to load it as a DLL and can fail with error 193 (not a valid Win32 application). @@ -161,10 +168,16 @@ $env:VK_LOADER_DRIVERS_DISABLE = '*dzn*' Do not disable Dozen if it is the only Vulkan driver available; the application would have no device to use. `VK_LOADER_DRIVERS_DISABLE` matches Vulkan driver manifest filenames and requires a Vulkan loader built with headers 1.3.234 or later ([loader documentation](https://github.com/KhronosGroup/Vulkan-Loader/blob/main/docs/LoaderInterfaceArchitecture.md#filter-environment-variables)). +On Windows, NVIDIA driver 610.47 can crash in its shader compiler while pipelines are created: the log ends in `FATAL: unhandled exception 0xc0000005` with `nvgpucomp64.dll` in `rip`, on a driver thread. PPSA25344 on an RTX 3060 stopped this way in 3 of 3 New Game loads, also with `APS5_NO_PIPELINE_LTO=1`; with driver 617.42 the same build and scene ran for 40 minutes. Update the driver when the crash is in `nvgpucomp64.dll`. + ### HDR output HDR output is available on Windows only. Titles see it when the monitor showing the window runs in HDR mode (Settings > Display > Use HDR), the window's Vulkan surface offers an HDR10 (`VK_COLOR_SPACE_HDR10_ST2084_EXT`) swapchain in a 10-bit format, the instance has `VK_EXT_swapchain_colorspace` and the device `VK_EXT_hdr_metadata`. The monitor's luminance comes from DXGI; a monitor that reports no peak luminance is taken as 1000 nits peak and full-frame, 0 nits black. On Linux and other hosts the output is always SDR, because the surface's formats do not show whether the display is in HDR. A title's BT.2100 PQ display buffers then reach the display unchanged; without HDR output they are converted to SDR. +### Shader cache + +Recompiled shaders and the Vulkan pipeline cache are kept in a `shader_cache/` directory beside the output executable, so later runs skip recompilation. Set `ANYPS5_SHADER_CACHE_DIR` to use another directory, or set `ANYPS5_NO_SHADER_CACHE` to any non-empty value other than `0` to disable both caches. Shaders are stored in a subdirectory named after the recompiler's source version; subdirectories of other versions unused for 14 days are removed the first time a run looks up a shader. The pipeline cache is one `vk-pipelines-*.bin` file per GPU and driver version. Deleting the directory is safe: it is rebuilt on the next run. + ### Storing GPU results at each flip GPU results in storage images and render targets stay on the GPU until something reads their memory. A title that frees such an image after waiting for its GPU work and reuses the memory can have its new data overwritten when the results are stored later ([TechnicalDebt](../dev/TechnicalDebt.md)). `APS5_STORE_AT_FLIP=1` stores every pending result when a display buffer is flipped, at the cost of one write-back per pending image and frame. Any other value stops the title at its first flip. @@ -231,9 +244,11 @@ python3 tools/import_audit.py app.registry.json app.libc.prx.guest.prx.registry. | `absent` | No built library exports the NID: the loader fails. | | `module` | The import names a file found in `--modules` (the title's own library), which is trusted to provide it. | -Each `--modules` file is also audited itself: its undefined symbols are classified against the built libraries and the other modules' exports, with the module file named in the report, and the libraries it needs must have a file in `--libs` or `--modules`. The audit reads dynamic tables from ELF section headers or, for sectionless files, from `PT_DYNAMIC` and a SysV or GNU hash table. +ELF modules in `--modules` are audited regardless of filename: their undefined symbols are classified against the built libraries and the other modules' exports, with the module file named in the report, and the libraries they need must have a file in `--libs` or `--modules`. Ordinary non-module sidecars are ignored; malformed ELF, PE/SELF containers and non-ELF files with module extensions remain errors. The audit reads dynamic tables from ELF section headers or, for sectionless files, from `PT_DYNAMIC` and a SysV or GNU hash table. -The report also lists needed libraries that have no file in `--libs` or `--modules`, and imports exported only by a library other than the one they name; those resolve on Linux and can fail on Windows. +Each registry has a companion with the same prefix and the suffix `.needed.json`, listing its host library dependencies, including declared `DT_NEEDED` entries with no imports. Dependencies supplied by converted bundled modules, including their aliases, are omitted. The executable's companion includes host dependencies of its bundled modules. The audit reads these companions automatically; keep them beside the registries. Older registries without companions still work, but cannot report dependencies that have no imports; regenerate them with `--registry` to include those dependencies. + +The report also lists needed libraries that have no file in `--libs` or `--modules`, including dependencies with zero imports. These do not increase the import or reference counts. Windows can skip a missing dependency when no import needs it; Linux still needs the file. The audit also lists imports exported only by a library other than the one they name; those resolve on Linux and can fail on Windows. | Option | Effect | |--------|--------| diff --git a/tools/build_web.py b/tools/build_web.py index 849042afd4..0d8bb92408 100644 --- a/tools/build_web.py +++ b/tools/build_web.py @@ -62,9 +62,14 @@ def publish_libraries(output): files = archive.infolist() if not files or len(files) > 4096: raise ValueError("Invalid library archive") + names = set() for file in files: if not file.filename.startswith("libs/") or any(part in ("..", ".", "") for part in file.filename.rstrip("/").split("/")) or "\\" in file.filename: raise ValueError("Invalid library archive layout") + name = file.filename.rstrip("/").lower() + if name in names: + raise ValueError(f"Duplicate library archive path: {file.filename}") + names.add(name) corrupt = archive.testzip() if corrupt: raise ValueError(f"Corrupt release entry: {corrupt}") diff --git a/tools/check_conventions.py b/tools/check_conventions.py index ee3f7d0527..de1af96502 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -32,7 +32,10 @@ RULES = { CPP = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".inl", ".ipp"} IMAGES = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg", ".webp", ".ico", ".tga", ".dds"} NOTES = {".md", ".markdown", ".rst", ".log", ".patch", ".diff"} -AGENT_FILES = {"agents.md", "claude.md", "gemini.md", ".cursorrules", "copilot-instructions.md"} +AGENT_FILES = { + "agents.md", "claude.md", "gemini.md", ".cursorrules", "copilot-instructions.md", + ".windsurfrules", "windsurf.md", "chatgpt.md", ".chatgptrules", ".aider.conf.yml", +} DEBT = "docs/dev/TechnicalDebt.md" DEBT_DIR = "docs/dev/debt/" DEBT_TARGET = re.compile(r"\]\((?:\.\./)+(core/[^)#\s]+)") @@ -46,7 +49,9 @@ STUB_BODY = re.compile(r"^(?:\(void\)\s*\w+\s*;|static_cast<void>\(\s*\w+\s*\)\s SYSTEM_DEPENDENCY = re.compile(r"\b(find_package\s*\((?!\s*(Python3|Threads|Git)\b)|pkg_check_modules\b|pkg_search_module\b|find_(library|path|program|file)\b)", re.I) 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]+)") +LINK = re.compile(r"\[[^\]]+\]\(([^)\s]+)") +FENCE = re.compile(r"^ {0,3}(```|~~~).*?^ {0,3}\1[ \t]*$|^ {0,3}(?:```|~~~).*\Z", re.S | re.M) +INLINE = re.compile(r"`[^`\n]*`") SECTION = re.compile(r"^###(?!#)\s*(.+?)\s*$", re.M) KEY = re.compile(r"^( *)([A-Za-z0-9_-]+|'[^']*'|\"[^\"]*\"):(?:\s+(.*?))?\s*$") @@ -71,17 +76,23 @@ class Check: self.blobs = {} self.tree = set(self.modes) self.dirs = {str(parent) for path in self.tree for parent in PurePosixPath(path).parents} - statuses = git("diff", "--name-status", "--no-renames", "-z", self.base, head).split("\0") - for status, path in zip(statuses[::2], statuses[1::2]): - self.status[path] = status - for line in git("diff", "--numstat", "--no-renames", "-z", self.base, head).split("\0"): - if not line: - continue + diff = ("--find-renames", "-l0", self.base, head) + statuses = iter(git("diff", "--name-status", "-z", *diff).split("\0")[:-1]) + for status in statuses: + path = next(statuses) + if status.startswith("R"): + path = next(statuses) + self.status[path] = status[0] + statistics = iter(git("diff", "--numstat", "-z", *diff).split("\0")[:-1]) + for line in statistics: added, deleted, path = line.split("\t", 2) + if not path: + next(statistics) + path = next(statistics) self.numstat[path] = (added, deleted) - paths = iter(self.status) + paths = iter([path for path, status in self.status.items() for _ in range(2 if status == "T" else 1)]) path, number = None, None - for line in git("diff", "-U0", "--no-color", "--no-renames", self.base, head).split("\n"): + for line in git("diff", "-U0", "--no-color", *diff).split("\n"): if line.startswith("diff --git "): path, number = next(paths), None elif line.startswith("@@"): @@ -149,7 +160,8 @@ class Check: self.duplicates(exports) for path, lines in self.files(lambda p: p.suffix in (".py", ".cmake") or p.name == "CMakeLists.txt"): for number, text in lines: - if "#" in STRINGS.sub('""', text) and not text.startswith("#!"): + shebang = text.startswith("#!") or (number == 1 and text.startswith("\ufeff#!")) + if "#" in STRINGS.sub('""', text) and not shebang: self.report("comment", path, number) if any(is_debt(path) for path in self.status): self.findings = [finding for finding in self.findings if finding[0] != "comment"] @@ -200,15 +212,22 @@ class Check: suffix = PurePosixPath(path).suffix.lower() if self.status.get(path) != "D" and (added == "-" or suffix in IMAGES or not utf8(self.blob(path))): self.report("binary", path, 0) - if self.status.get(path) == "A" and allowed_notes(path) is False: + if self.status.get(path) in ("A", "R") and allowed_notes(path) is False: self.report("notes-file", path, 0) def docs(self): - for path, lines in self.files(lambda p: p.suffix == ".md"): + files = dict(self.files(lambda p: p.suffix == ".md")) + for path, status in self.status.items(): + if status == "R" and not path.startswith("3rdparty/") and PurePosixPath(path).suffix == ".md": + files[path] = list(enumerate(self.show(path).split("\n"), 1)) + for path, lines in files.items(): + fenced = fenced_lines(self.show(path)) for number, text in lines: name = debt_name(text) if is_debt(path) and text.startswith("- ") else None if name and path != f"{DEBT_DIR}{name}.md": self.report("debt-file", path, number, f"{DEBT_DIR}{name}.md") + if number in fenced: + continue for target, resolved in links(path, text): if resolved not in self.tree and resolved not in self.dirs: self.report("doc-link", path, number, target) @@ -258,8 +277,17 @@ def utf8(data): return True +def fenced_lines(text): + inside = set() + for match in FENCE.finditer(text): + first = text.count("\n", 0, match.start()) + 1 + inside.update(range(first, first + match.group(0).count("\n") + 1)) + return inside + + def links(path, text): result = [] + text = INLINE.sub("", FENCE.sub("", text)) for target in LINK.findall(text): if re.match(r"[A-Za-z][A-Za-z0-9+.-]*:|//|#", target): continue diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index 7b00d94d3e..a496b7eada 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -1,11 +1,14 @@ import argparse import re import subprocess +import sys from pathlib import Path TEST_OUTPUT = r"tests[\\/]" COMMAND = re.compile(r"^\s*(add_test|set_tests_properties|subdirs)\s*\(", re.M | re.I) ARGUMENT = re.compile(r'"(?:\\.|[^"\\])*"|\[(?P<equals>=*)\[.*?\](?P=equals)\]|[^\s()]+', re.S) +LEXICAL = re.compile(r'\\.|"(?:\\.|[^"\\])*"|\[(?P<literal_equals>=*)\[.*?\](?P=literal_equals)\]' + r'|(?P<comment>\#\[(?P<comment_equals>=*)\[.*?\](?P=comment_equals)\]|\#[^\n]*)', re.S) KNOWN_UNRUN = set() @@ -21,8 +24,16 @@ def built(build): raise ValueError(f"test registration audit requires a Ninja generator, got {generator!r}") if not program or program.endswith("-NOTFOUND"): raise FileNotFoundError(f"CMAKE_MAKE_PROGRAM is not set in {Path(build) / 'CMakeCache.txt'}") + encoding = "utf-8" + if sys.platform == "win32": + codepage = subprocess.run([program, "-t", "wincodepage"], capture_output=True, text=True, + encoding="ascii", check=True).stdout.strip() + encodings = {"Build file encoding: UTF-8": "utf-8", "Build file encoding: ANSI": "mbcs"} + if codepage not in encodings: + raise ValueError(f"unsupported Ninja build-file encoding: {codepage!r}") + encoding = encodings[codepage] listed = subprocess.run([program, "-C", str(build), "-t", "targets", "all"], - capture_output=True, text=True, check=True).stdout + capture_output=True, text=True, encoding=encoding, check=True).stdout names = set() for line in listed.splitlines(): path, _, rule = line.partition(": ") @@ -40,6 +51,8 @@ def built(build): def commands(content): + content = LEXICAL.sub(lambda match: re.sub(r"[^\n]", " ", match.group()) + if match.group("comment") is not None else match.group(), content) parsed_until = 0 for command in COMMAND.finditer(content): if command.start() < parsed_until: diff --git a/tools/debt_split.py b/tools/debt_split.py index e7341fefe1..5c27562a4e 100644 --- a/tools/debt_split.py +++ b/tools/debt_split.py @@ -43,7 +43,7 @@ def key(kind, text): def relink(text, source, destination): def replace(match): target = match.group(1) - if re.match(r"^[a-z][a-z0-9+.-]*:", target): + if re.match(r"^[A-Za-z][A-Za-z0-9+.-]*:|//", target): return match.group(0) path, _, anchor = target.partition("#") resolved = posixpath.normpath(posixpath.join(posixpath.dirname(source), path)) if path else source diff --git a/tools/import_audit.py b/tools/import_audit.py index b3809b786c..bc819fc9bf 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -50,6 +50,8 @@ def elf_sections(path, data): ident, section_offset, section_size, section_count = header[0], header[6], header[11], header[12] if ident[4] != 2 or ident[5] != 1 or section_size != 64: raise AuditError(f"{path}: only little-endian ELF64 with 64-byte section headers is supported (class {ident[4]}, data {ident[5]}, shentsize {section_size})") + if ident[6] != 1 or header[3] != 1: + raise AuditError(f"{path}: unsupported ELF version (identification {ident[6]}, object {header[3]})") if not section_offset: raise AuditError(f"{path}: ELF file without a section header table is not supported") if not section_count: @@ -61,6 +63,8 @@ def elf_sections(path, data): def elf_program_dynamic_symbols(path, data): header = unpack("<16sHHIQQQIHHHHHH", data, 0, path, "ELF header") ident, program_offset, program_size, program_count = header[0], header[5], header[9], header[10] + if ident[6] != 1 or header[3] != 1: + raise AuditError(f"{path}: unsupported ELF version (identification {ident[6]}, object {header[3]})") if ident[4] != 2 or ident[5] != 1 or program_size != 56 or not program_offset or not program_count: raise AuditError(f"{path}: sectionless ELF needs little-endian ELF64 program headers") take(data, program_offset, program_count * program_size, path, "program header table") @@ -104,11 +108,12 @@ def elf_program_dynamic_symbols(path, data): if 4 in dynamic: hash_offset = file_offset(dynamic[4], 8, "SysV hash table") buckets, chains = unpack("<II", data, hash_offset, path, "SysV hash header") - take(data, hash_offset, 8 + 4 * (buckets + chains), path, "SysV hash table") + file_offset(dynamic[4], 8 + 4 * (buckets + chains), "SysV hash table") symbol_count = chains elif 0x6ffffef5 in dynamic: hash_offset = file_offset(dynamic[0x6ffffef5], 16, "GNU hash header") buckets, first_symbol, bloom_count, _ = unpack("<IIII", data, hash_offset, path, "GNU hash header") + file_offset(dynamic[0x6ffffef5], 16 + bloom_count * 8 + buckets * 4, "GNU hash prefix") bucket_offset = hash_offset + 16 + bloom_count * 8 bucket_data = take(data, bucket_offset, buckets * 4, path, "GNU hash buckets") chain_offset = bucket_offset + buckets * 4 @@ -208,6 +213,8 @@ def elf_imports(path, data): take(data, strings_offset, strings_size, path, "dynamic string table") for position in range(0, size, 16): tag, value = unpack("<qQ", data, offset + position, path, "dynamic entry") + if tag == 0: + break if tag == 1: needed.add(cstring(data, strings_offset + value, strings_offset + strings_size, path, "needed library name")) return symbols, needed @@ -320,12 +327,31 @@ def built_libraries(directories): return {name: sorted(files) for name, files in exports.items()}, libraries +def module_files(directory): + for file in directory_files(directory, "*"): + with file.open("rb") as handle: + magic = handle.read(4) + if magic == b"\x7fELF": + yield file + elif (file.suffix.lower() in (".prx", ".sprx", ".suprx", ".debug_prx", ".elf") + or magic.startswith(b"MZ") or magic in (b"\x4f\x15\x3d\x1d", b"\x54\x14\xf5\xee")): + raise AuditError(f"{file}: expected ELF module (first bytes {magic!r})") + + def read_modules(directories): imports = {} exports = defaultdict(set) + images = {} for directory in directories: - for file in directory_files(directory, "*"): + for file in module_files(directory): data = file.read_bytes() + digest = hashlib.sha256(data).digest() + if file.name in images: + previous, fingerprint = images[file.name] + if digest != fingerprint: + raise AuditError(f"ambiguous guest module {file.name}: {previous} and {file} have different contents") + continue + images[file.name] = (file, digest) own = elf_imports(file, data) symbols, needed = own imports[file.name] = (sorted(symbols), sorted(needed)) @@ -385,10 +411,24 @@ def read_registry(path): nid = entry["nid"].split("#", 1)[0] if not NID.fullmatch(nid): raise AuditError(f"{path}: entry {index} has an invalid NID {entry['nid']!r}") - imports.append((nid, entry["library"])) + imports.append((nid, entry["library"], bool(entry.get("weak", False)))) return imports +def needed_registry_path(path): + return path.with_name(path.name.removesuffix(".registry.json") + ".needed.json") + + +def read_needed_registry(path): + try: + libraries = json.loads(path.read_text(encoding="utf-8-sig")) + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error: + raise AuditError(f"{path}: cannot read needed libraries: {error}") from error + if not isinstance(libraries, list) or any(not isinstance(library, str) or not library for library in libraries): + raise AuditError(f"{path}: needed libraries must be a JSON list of nonempty strings") + return libraries + + def check_conservation(references, records): classified = sum(record["references"] for record in records) unknown = sorted({record["class"] for record in records} - set(CLASSES)) @@ -406,14 +446,15 @@ def classify(nid, library, providers, stubs, modules): return "implemented" -def audit(imports, exports, stubs, libraries, modules, module_imports=None, module_exports=None): +def audit(imports, exports, stubs, libraries, modules, module_imports=None, module_exports=None, *, needed_libraries=()): records = {} - for nid, library in imports: - record = records.setdefault((nid, library), {"nid": nid, "library": library, "references": 0}) + for nid, library, weak in imports: + record = records.setdefault((nid, library), {"nid": nid, "library": library, "references": 0, "weak": weak}) record["references"] += 1 + record["weak"] = weak for module, (symbols, needed) in (module_imports or {}).items(): for nid in symbols: - record = records.setdefault((nid, None, module), {"nid": nid, "library": None, "module": module, "references": 0}) + record = records.setdefault((nid, None, module), {"nid": nid, "library": None, "module": module, "references": 0, "weak": False}) record["references"] += 1 for record in records.values(): nid, library = record["nid"], record["library"] @@ -428,6 +469,9 @@ def audit(imports, exports, stubs, libraries, modules, module_imports=None, modu ordered = sorted(records.values(), key=lambda record: (record["class"], record["library"] or "", record.get("module") or "", record["nid"])) check_conservation(len(imports) + sum(len(symbols) for symbols, _ in (module_imports or {}).values()), ordered) missing = defaultdict(int) + for library in needed_libraries: + if library not in libraries and library not in modules: + missing[library] = 0 for record in ordered: library = record["library"] if library and library not in libraries and library not in modules: @@ -440,9 +484,11 @@ def audit(imports, exports, stubs, libraries, modules, module_imports=None, modu def current_stop(imports, records): - """The first import the loader cannot resolve, in registry order.""" + """The first import the loader cannot resolve, in registry order; a weak import resolves to null instead.""" classes = {(record["nid"], record["library"]): record for record in records} - for nid, library in imports: + for nid, library, weak in imports: + if weak: + continue record = classes.get((nid, library)) if record is not None and record["class"] == "absent": return record @@ -536,6 +582,8 @@ def render(summary, records, missing, stops): lines.append("") lines.append(f"Needed libraries with no file in --libs or --modules ({len(missing)})") lines.extend(f" {library}: {count} imports" for library, count in missing.items()) + if any(count == 0 for count in missing.values()): + lines.append(" Libraries with 0 imports are declared dependencies; Windows can skip them if no import needs them") if summary["library_mismatch"]: lines.append("") lines.append(f"{summary['library_mismatch']} imports are exported only by a library other than the one they name: they resolve on Linux and can fail on Windows") @@ -553,8 +601,10 @@ def main(argv=None): parser.add_argument("--strict-providers", action="store_true", help="fail when a named import is provided only by another library; retains the existing libc and title-module rules") args = parser.parse_args(argv) try: + dependency_registries = [needed_registry_path(path) for path in args.registries if path.name.endswith(".registry.json")] + dependency_registries = [path for path in dependency_registries if path.exists()] if args.json: - inputs = args.registries + ([Path(args.names)] if args.names else []) + inputs = args.registries + dependency_registries + ([Path(args.names)] if args.names else []) inputs += [file for directory in args.libs for file in directory_files(directory, "*.prx")] inputs += [file for directory in args.modules for file in directory_files(directory, "*")] inputs += list(Path(args.source).rglob("*.cpp")) @@ -563,9 +613,10 @@ def main(argv=None): raise AuditError(f"{args.json}: JSON output would overwrite an input file") registries = [(registry, read_registry(registry)) for registry in args.registries] imports = [entry for _, entries in registries for entry in entries] + needed = sorted({library for path in dependency_registries for library in read_needed_registry(path)}) exports, libraries = built_libraries(args.libs) module_imports, module_exports = read_modules(args.modules) - records, missing = audit(imports, exports, source_stubs(args.source), libraries, set(module_imports), module_imports, module_exports) + records, missing = audit(imports, exports, source_stubs(args.source), libraries, set(module_imports), module_imports, module_exports, needed_libraries=needed) if args.names: attach_names(records, load_names(args.names)) summary = summarize(records, sum(record["references"] for record in records), missing) @@ -576,7 +627,7 @@ def main(argv=None): "nid": entry["stop"]["nid"] if entry["stop"] else None, "library": entry["stop"]["library"] if entry["stop"] else None, "name": entry["stop"]["name"] if entry["stop"] else None} for entry in stops] - result = {"source_commit": commit, "source_dirty": dirty, "registries": [str(path) for path in args.registries], **summary, "stops": json_stops, "imports": records} + result = {"source_commit": commit, "source_dirty": dirty, "registries": [str(path) for path in args.registries], "dependency_registries": [str(path) for path in dependency_registries], "needed_libraries": needed, **summary, "stops": json_stops, "imports": records} write_json(args.json, result) except AuditError as error: print(f"FAIL: {error}", file=sys.stderr) diff --git a/tools/package_release.py b/tools/package_release.py index c825539e1d..72c23905fe 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -1,6 +1,8 @@ import argparse +import os import re import shutil +import stat import tarfile import tempfile import zipfile @@ -41,6 +43,8 @@ def package(platform, build, output, version): destination = output / name if destination.exists() and not destination.is_file(): raise RuntimeError(f"Release destination is not a file: {destination}") + if os.name == "nt" and os.path.lexists(destination) and destination.lstat().st_file_attributes & stat.FILE_ATTRIBUTE_READONLY: + raise RuntimeError(f"Release destination is read-only: {destination}") output.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix=".release-", dir=output) as temporary: staging = Path(temporary) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index 4a1a4b4720..393b5586a8 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -17,7 +17,7 @@ FETCH_REFS = 100 def git(*args): - return subprocess.run(["git", *args], capture_output=True, text=True, encoding="utf-8", errors="replace", check=True).stdout + return subprocess.run(["git", "-c", "core.quotePath=false", *args], capture_output=True, text=True, encoding="utf-8", errors="replace", check=True).stdout def own(prs, sha): @@ -79,7 +79,7 @@ def fetch(base, prs): def merge(*args): - result = subprocess.run(["git", "merge-tree", "--write-tree", "--name-only", "--no-messages", *args], + result = subprocess.run(["git", "-c", "core.quotePath=false", "merge-tree", "--write-tree", "--name-only", "--no-messages", *args], capture_output=True, text=True, encoding="utf-8", errors="replace") lines = result.stdout.splitlines() if result.returncode not in (0, 1) or not lines: @@ -126,7 +126,11 @@ def scan(pr): def hunks(pr, path): diff = git("diff", "-U0", "--no-color", "refs/pr/base", pr["tree"], "--", path) - return [(int(start), int(start) + max(int(count or 1), 1) - 1) for start, count in HUNK.findall(diff)] + found = [] + for start, count in HUNK.findall(diff): + start = max(int(start), 1) + found.append((start, start + max(int(count or 1), 1) - 1)) + return found def clashes(a, b, path): diff --git a/tools/progress.py b/tools/progress.py index cc18c35023..8a4f5995f3 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -20,6 +20,8 @@ SOURCE = f'https://github.com/{os.environ.get("GITHUB_REPOSITORY", "boykopovar/A DEFINITION = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\([^;{]*\)\s*(?:noexcept\s*)?(?:try\s*)?\{") STUB = "NotImplemented_nid_no_patch" CALL = re.compile(r"\b(\w+)\s*\(") +KEYWORDS = r"return|else|do|throw|co_return|co_await|co_yield|and|or|not|xor|bitand|bitor|compl" +QUALIFIED = re.compile(rf"(?:\.|->|(?:\b(?!(?:{KEYWORDS})\b)\w+|>)\s*::)\s*$") CMAKE_SHARED = re.compile(r'^\s*include\(\s*"?\$\{CMAKE_CURRENT_SOURCE_DIR\}/\.\./([\w.-]+)/[\w.-]+\.cmake"?\s*\)', re.M) SOURCE_SHARED = re.compile(r'^\s*#\s*include\s*"prx/([\w.-]+)/[\w./-]+\.cpp"', re.M) STUB_WRAPPER = re.compile( @@ -101,12 +103,16 @@ def body_end(text, start): def stub_calls(text): - calls = {STUB} + helpers = {} for match in STUB_WRAPPER.finditer(text): body = text[match.end() - 1:body_end(text, match.end() - 1)] - if any(call.group(1) == STUB for call in CALL.finditer(body)): - calls.add(match.group(1)) - return calls + helpers.setdefault(match.group(1), set()).update(call.group(1) for call in CALL.finditer(body)) + calls = {STUB} + while True: + found = {name for name, targets in helpers.items() if name not in calls and targets & calls} + if not found: + return calls + calls |= found def statements(body): @@ -167,7 +173,8 @@ def scan_library(path): if name.endswith(("_nid_no_patch", "_nid_no_patch_cut")): continue body = text[match.end() - 1:body_end(text, match.end() - 1)] - if any(call.group(1) in calls for call in CALL.finditer(body)): + if any(call.group(1) in calls and not QUALIFIED.search(body, 0, call.start()) + for call in CALL.finditer(body)): todo.add(name) continue done.add(name) diff --git a/tools/progress_history.py b/tools/progress_history.py index 22818d0cd3..83b9bc76a2 100644 --- a/tools/progress_history.py +++ b/tools/progress_history.py @@ -35,7 +35,11 @@ def measure(sha): with tempfile.TemporaryDirectory() as directory: root = Path(directory) try: - tarfile.open(fileobj=io.BytesIO(git("archive", sha, "--", *paths))).extractall(root, filter="data") + archive = tarfile.open(fileobj=io.BytesIO(git("archive", sha, "--", *paths))) + if hasattr(tarfile, "data_filter"): + archive.extractall(root, filter="data") + else: + archive.extractall(root) except subprocess.CalledProcessError: return None saved = progress.PRX, progress.OPCODES diff --git a/tools/stubgen.py b/tools/stubgen.py index 2c296b9298..6fd72e6cef 100644 --- a/tools/stubgen.py +++ b/tools/stubgen.py @@ -197,7 +197,11 @@ def write_outputs(outputs): temporary.replace(path) finally: for temporary, path in staged: - temporary.unlink(missing_ok=True) + try: + temporary.unlink(missing_ok=True) + except PermissionError: + temporary.chmod(0o600) + temporary.unlink(missing_ok=True) def main(argv=None): diff --git a/tools/test_web.py b/tools/test_web.py index c6c66fe237..f96c3d5f4f 100644 --- a/tools/test_web.py +++ b/tools/test_web.py @@ -17,6 +17,7 @@ def main(): parser = argparse.ArgumentParser() parser.add_argument("--build", type=Path, required=True) args = parser.parse_args() + subprocess.run(["node", "--test", str(ROOT / "demo/web/tests/output-directory.test.mjs")], check=True) with tempfile.TemporaryDirectory(prefix="anyps5-web-") as directory: fixture = Path(directory) (fixture / "main.elf").write_bytes(main_fixture()) @@ -27,6 +28,7 @@ def main(): with urllib.request.urlopen(url, timeout=30) as response: library.write_bytes(response.read()) environment = dict(os.environ, ANYPS5_TEST_ZIP_URL=url, ANYPS5_TEST_ZIP_PATH=library.as_uri()) + subprocess.run(["node", str(ROOT / "demo/web/tests/archive-root.test.mjs"), str(args.build.resolve() / "web")], env=environment, check=True) subprocess.run(["node", str(ROOT / "demo/web/tests/archive.test.mjs"), str(args.build.resolve() / "web"), str(fixture)], env=environment, check=True) diff --git a/tools/tests/test_build_web.py b/tools/tests/test_build_web.py new file mode 100644 index 0000000000..872c9f03eb --- /dev/null +++ b/tools/tests/test_build_web.py @@ -0,0 +1,91 @@ +import contextlib +import hashlib +import io +import json +from pathlib import Path +import sys +import tempfile +import unittest +from unittest.mock import patch +import warnings +import zipfile + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import build_web + + +def library_zip(entries): + buffer = io.BytesIO() + with warnings.catch_warnings(), zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED) as archive: + warnings.simplefilter("ignore", UserWarning) + for name, data in entries: + archive.writestr(name, data) + return buffer.getvalue() + + +class LibraryPublicationTests(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.output = Path(temporary.name) + self.libraries = self.output / "libraries" + self.libraries.mkdir() + self.manifest = self.output / "release.json" + self.previous = b'{"tag":"previous complete release"}\n' + self.manifest.write_bytes(self.previous) + self.existing = self.libraries / "previous.zip" + self.existing.write_bytes(b"previous archive") + + def publish(self, windows, linux): + assets = [] + responses = {} + for platform, data in (("windows", windows), ("linux", linux)): + url = f"https://example.invalid/{platform}.zip" + assets.append({"name": f"prx-{platform}-v1.zip", "size": len(data), + "digest": "sha256:" + hashlib.sha256(data).hexdigest(), "browser_download_url": url}) + responses[url] = data + release = {"tag_name": "v1", "html_url": "https://example.invalid/v1", "assets": assets} + responses["https://api.github.com/repos/boykopovar/AnyPS5/releases/latest"] = json.dumps(release).encode() + with patch.object(build_web, "request", side_effect=lambda url: io.BytesIO(responses[url])), \ + contextlib.redirect_stdout(io.StringIO()): + build_web.publish_libraries(self.output) + + def test_colliding_paths_do_not_publish_a_release(self): + valid = library_zip([("libs/libc.prx", b"valid archive")]) + for first, second in (("libs/libc.prx", "libs/libc.prx"), + ("libs/libc.prx", "libs/LIBC.prx"), + ("libs/extra/", "libs/EXTRA")): + with self.subTest(first=first, second=second): + invalid = library_zip([(first, b"" if first.endswith("/") else b"first"), (second, b"second")]) + with self.assertRaisesRegex(ValueError, "Duplicate library archive path"): + self.publish(invalid, valid) + self.assertEqual(self.manifest.read_bytes(), self.previous) + self.assertEqual(self.existing.read_bytes(), b"previous archive") + self.assertEqual(list(self.libraries.iterdir()), [self.existing]) + + def test_collision_in_second_platform_preserves_previous_manifest(self): + valid = library_zip([("libs/libc.prx", b"valid archive")]) + invalid = library_zip([("libs/libc.prx", b"first"), ("libs/LIBC.prx", b"second")]) + with self.assertRaisesRegex(ValueError, "Duplicate library archive path"): + self.publish(valid, invalid) + self.assertEqual(self.manifest.read_bytes(), self.previous) + self.assertEqual(self.existing.read_bytes(), b"previous archive") + self.assertFalse(any(file.suffix == ".download" for file in self.libraries.iterdir())) + + def test_distinct_full_paths_publish_both_platforms(self): + payload = library_zip([("libs/", b""), ("libs/libc.prx", b"first"), ("libs/extra/libc.prx", b"second")]) + self.publish(payload, payload) + manifest = json.loads(self.manifest.read_text(encoding="utf-8")) + self.assertEqual(manifest["tag"], "v1") + self.assertEqual(set(manifest["platforms"]), {"windows", "linux"}) + for platform, asset in manifest["platforms"].items(): + digest = hashlib.sha256(payload).hexdigest() + self.assertEqual(asset, {"path": f"libraries/{platform}-{digest}.zip", "size": len(payload), + "expandedBytes": 11, "sha256": digest}) + self.assertEqual((self.output / asset["path"]).read_bytes(), payload) + self.assertEqual(self.existing.read_bytes(), b"previous archive") + self.assertFalse(any(file.suffix == ".download" for file in self.libraries.iterdir())) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_check_conventions_agent_files.py b/tools/tests/test_check_conventions_agent_files.py index 48a975aa1b..87704e1226 100644 --- a/tools/tests/test_check_conventions_agent_files.py +++ b/tools/tests/test_check_conventions_agent_files.py @@ -9,7 +9,11 @@ from check_conventions import allowed_notes class AgentFileTests(unittest.TestCase): def test_agent_files_are_rejected_in_any_case(self): - for name in ("CLAUDE.md", "claude.md", "Claude.md", "AGENTS.md", "Agents.md", "GEMINI.md", "Copilot-Instructions.md", ".CursorRules"): + for name in ( + "CLAUDE.md", "claude.md", "Claude.md", "AGENTS.md", "Agents.md", "GEMINI.md", + "Copilot-Instructions.md", ".CursorRules", ".WindsurfRules", "windsurf.md", + "ChatGPT.md", ".ChatGPTRules", ".aider.conf.yml", + ): with self.subTest(name=name): self.assertIs(allowed_notes(f"docs/dev/{name}"), False) self.assertIs(allowed_notes(name), False) diff --git a/tools/tests/test_check_conventions_bom_shebang.py b/tools/tests/test_check_conventions_bom_shebang.py new file mode 100644 index 0000000000..6add18b3cf --- /dev/null +++ b/tools/tests/test_check_conventions_bom_shebang.py @@ -0,0 +1,48 @@ +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +class BomShebangTests(unittest.TestCase): + def check(self, header): + checker = Path(__file__).resolve().parents[1] / "check_conventions.py" + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + + def git(*args): + return subprocess.run( + ["git", "-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("Example\n", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "docs: add readme") + script = root / "tools/example.py" + script.parent.mkdir() + script.write_text("\ufeff" + header + '\nprint("ok")\n', encoding="utf-8") + executed = subprocess.run([sys.executable, str(script)], capture_output=True, text=True) + self.assertEqual((executed.returncode, executed.stdout.strip()), (0, "ok"), executed.stderr) + git("add", ".") + git("commit", "-qm", "fix: add Python tool") + environment = {key: value for key, value in os.environ.items() + if key not in ("GITHUB_EVENT_PATH", "GITHUB_STEP_SUMMARY", "GITHUB_ACTIONS")} + return subprocess.run([sys.executable, str(checker), "--base", "HEAD~1"], + cwd=root, capture_output=True, text=True, env=environment) + + def test_initial_bom_shebang_is_accepted(self): + result = self.check("#!/usr/bin/env python3") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_initial_bom_does_not_exempt_an_ordinary_comment(self): + result = self.check("# ordinary comment") + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("[comment]", result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index 25e0b1f871..867c3d1a0d 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -498,6 +498,80 @@ class RepositoryChecksTests(unittest.TestCase): check.code() self.assertIn(("comment", "core/example.cpp", 3, ""), check.findings) + def test_unchanged_move_preserves_existing_comments_and_extensions(self): + source = self.root / "core/tests/Existing.cpp" + source.parent.mkdir(parents=True) + source.write_text("// existing explanation\n__attribute__((noinline)) int Existing() { return 1; }\n", encoding="utf-8") + self.commit() + destination = self.root / "core/tests/group/Café example.cpp" + destination.parent.mkdir() + source.rename(destination) + (self.root / "a.bin").write_bytes(b"binary\x00data") + (self.root / "README.md").unlink() + self.commit() + check = Check("HEAD~1", "HEAD") + check.code() + check.repository() + name = destination.relative_to(self.root).as_posix() + self.assertEqual(check.status[name], "R") + self.assertEqual(check.numstat[name], ("0", "0")) + self.assertNotIn(name, check.added) + self.assertEqual(check.findings, [("binary", "a.bin", 0, "")]) + + def test_edited_move_checks_new_lines_at_the_destination(self): + source = self.root / "core/tests/Existing.cpp" + source.parent.mkdir(parents=True) + original = "// existing explanation\n__attribute__((noinline)) int Existing() { return 1; }\n" + original += "".join(f"int Existing{index}() {{ return {index}; }}\n" for index in range(12)) + source.write_text(original, encoding="utf-8") + self.commit() + destination = self.root / "core/tests/group/Edited.cpp" + destination.parent.mkdir() + source.rename(destination) + destination.write_text(original + "// new explanation\n__declspec(noinline) int Added() { return 2; }\n", encoding="utf-8") + self.commit() + check = Check("HEAD~1", "HEAD") + check.code() + name = destination.relative_to(self.root).as_posix() + self.assertEqual(check.status[name], "R") + self.assertEqual(check.numstat[name], ("2", "0")) + self.assertEqual(check.findings, [("comment", name, 15, ""), ("extension", name, 16, "__declspec")]) + + def test_copied_code_is_still_checked_as_an_addition(self): + source = self.root / "core/tests/Existing.cpp" + source.parent.mkdir(parents=True) + source.write_text("// existing explanation\n__attribute__((noinline)) int Existing() { return 1; }\n", encoding="utf-8") + self.commit() + destination = self.root / "core/tests/Copy.cpp" + destination.write_bytes(source.read_bytes()) + self.commit() + check = Check("HEAD~1", "HEAD") + check.code() + name = destination.relative_to(self.root).as_posix() + self.assertEqual(check.status[name], "A") + self.assertEqual(check.findings, [("comment", name, 1, ""), ("extension", name, 2, "__attribute__")]) + + def test_moving_a_document_to_a_forbidden_path_is_rejected(self): + (self.root / "README.md").rename(self.root / "investigation.md") + self.commit() + check = Check("HEAD~1", "HEAD") + check.repository() + self.assertEqual(check.findings, [("notes-file", "investigation.md", 0, "")]) + + def test_moving_a_document_rechecks_relative_links(self): + source = self.root / "docs/dev/guide.md" + source.parent.mkdir(parents=True) + source.write_text("[reference](reference.md)\n", encoding="utf-8") + (source.parent / "reference.md").write_text("Reference\n", encoding="utf-8") + self.commit() + destination = self.root / "docs/dev/group/guide.md" + destination.parent.mkdir() + source.rename(destination) + self.commit() + check = Check("HEAD~1", "HEAD") + check.docs() + self.assertEqual(check.findings, [("doc-link", "docs/dev/group/guide.md", 1, "reference.md")]) + def test_unicode_raw_literals_keep_git_patch_and_grep_records_intact(self): literals = 'R"text(foo\u2028+tail\u2029+tail)text"' fixtures = ( @@ -536,6 +610,21 @@ class RepositoryChecksTests(unittest.TestCase): self.assertEqual(check.findings, [("duplicate-export", "core/libs/prx/libSceB/Export.cpp", 1, "sceFooBar is also in libSceA")]) + def test_type_change_does_not_shift_later_files(self): + (self.root / "a.txt").write_text("text\n", encoding="utf-8") + self.commit() + target = self.root / "target" + target.write_text("README.md", encoding="utf-8") + blob = self.git("hash-object", "-w", "target") + target.unlink() + self.git("update-index", "--cacheinfo", "120000", blob, "a.txt") + (self.root / "z.cpp").write_text("int z;\n// debt\n", encoding="utf-8") + self.git("add", "z.cpp") + self.git("commit", "-qm", "test: replace a file with a link") + check = Check("HEAD~1", "HEAD") + check.code() + self.assertIn(("comment", "z.cpp", 2, ""), check.findings) + def test_submodule_outside_thirdparty_reports_without_crashing(self): head = self.git("rev-parse", "HEAD") self.git("update-index", "--add", "--cacheinfo", "160000", head, "vendor") diff --git a/tools/tests/test_check_conventions_doc_links.py b/tools/tests/test_check_conventions_doc_links.py new file mode 100644 index 0000000000..d230f88a30 --- /dev/null +++ b/tools/tests/test_check_conventions_doc_links.py @@ -0,0 +1,29 @@ +from pathlib import Path +import sys +import unittest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import check_conventions + +FENCE = "`" * 3 + + +class DocLinkTests(unittest.TestCase): + def test_code_blocks_and_inline_code_have_no_links(self): + text = f"{FENCE}cpp\nauto f = [](int a) {{ return a; }};\n{FENCE}\n~~~\nauto g = [](int b) {{}};\n~~~\nuse `[](auto)` here\n" + self.assertEqual(check_conventions.links("docs/x.md", text), []) + + def test_real_link_outside_block_is_kept(self): + text = f"{FENCE}\n[](int a)\n{FENCE}\nsee [guide](../README.md)\n" + self.assertEqual(check_conventions.links("docs/x.md", text), [("../README.md", "README.md")]) + + def test_empty_link_text_is_ignored(self): + self.assertEqual(check_conventions.links("docs/x.md", "![](img.png) and [](empty.md)"), []) + + def test_fenced_lines(self): + text = f"a\n{FENCE}\n[](int a)\n{FENCE}\nb\n" + self.assertEqual(check_conventions.fenced_lines(text), {2, 3, 4}) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_debt_split.py b/tools/tests/test_debt_split.py index 9bdceffa98..8ffc27c4ca 100644 --- a/tools/tests/test_debt_split.py +++ b/tools/tests/test_debt_split.py @@ -59,6 +59,20 @@ class DebtSplitTests(unittest.TestCase): self.git("add", "-A") self.git("commit", "-qm", "docs: split technical debt") + def test_split_preserves_external_urls_and_rewrites_local_links(self): + links = ["HTTPS://example.test/guide#intro", "//example.test/guide#intro", + "https://example.test/guide", "MAILTO:author@example.test"] + references = " ".join(f"[reference]({target})" for target in links) + self.write(INDEX, ORIGINAL.replace(A_STUB, A_STUB + " " + references + + " [local](ARCHITECTURE.md#layout)")) + self.split() + moved = self.read("docs/dev/debt/libSceA.md") + for target in links: + with self.subTest(target=target): + self.assertIn(f"[reference]({target})", moved) + self.assertIn("[local](../ARCHITECTURE.md#layout)", moved) + self.assertIn("../../../core/libs/prx/libSceA/Export.cpp", moved) + def test_split_moves_entries_by_their_first_core_link(self): self.split() self.assertEqual(self.read("docs/dev/debt/libSceA.md"), diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index f441d78c61..2b6bf042f6 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -143,7 +143,13 @@ def write_source(root, library, text): def write_registry(path, entries, encoding="utf-8"): - path.write_text(json.dumps([{"nid": nid, "library": library, "relocationType": "R_X86_64_GLOB_DAT"} for nid, library in entries]), encoding=encoding) + records = [] + for entry in entries: + record = {"nid": entry[0], "library": entry[1], "relocationType": "R_X86_64_GLOB_DAT"} + if len(entry) > 2: + record["weak"] = entry[2] + records.append(record) + path.write_text(json.dumps(records), encoding=encoding) class Workspace: @@ -159,9 +165,9 @@ class Workspace: def close(self): self._directory.cleanup() - def run(self, registry, *extra): + def run(self, registry, *extra, registries=()): out, err = io.StringIO(), io.StringIO() - arguments = [str(registry), "--libs", str(self.libs), "--modules", str(self.modules), "--source", str(self.source), *extra] + arguments = [str(registry), *map(str, registries), "--libs", str(self.libs), "--modules", str(self.modules), "--source", str(self.source), *extra] with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err): code = import_audit.main(arguments) return code, out.getvalue(), err.getvalue() @@ -383,6 +389,103 @@ class ImportAuditTests(unittest.TestCase): self.assertIn("[library not named]", out) self.assertIn("EEEEEEEEEEE (imported by main.prx)", out) + def test_module_sidecars_do_not_change_the_audit(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + (work.modules / "provider.prx").write_bytes(exporting("BBBBBBBBBBB")) + (work.modules / "consumer.prx").write_bytes(build_elf([ + ("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 0, GLOBAL_FUNCTION)], + ["libSceA.prx", "provider.prx"])) + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, [("BBBBBBBBBBB", "provider.prx")]) + code, before_out, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (0, "")) + before = json.loads(result.read_text()) + for name, data in (("README", b"module notes\n"), ("notes.txt", b"EEEEEEEEEEE\n"), + ("provider.registry.json", b'[{"nid":"EEEEEEEEEEE","library":"missing.prx"}]'), + (".DS_Store", b"\x00\xffmetadata"), ("empty.txt", b"")): + (work.modules / name).write_bytes(data) + code, out, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (0, "")) + self.assertEqual(out, before_out) + self.assertEqual(json.loads(result.read_text()), before) + write_registry(registry, [("EEEEEEEEEEE", "notes.txt")]) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (1, "")) + data = json.loads(result.read_text()) + record = next(record for record in data["imports"] if record["nid"] == "EEEEEEEEEEE") + self.assertEqual((record["class"], record["providers"]), ("absent", [])) + self.assertEqual(data["missing_libraries"], {"notes.txt": 1}) + + def test_elf_modules_are_audited_regardless_of_filename(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + (work.modules / "provider.data").write_bytes(build_elf([ + ("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 1, GLOBAL_FUNCTION)], ["libSceA.prx"])) + (work.modules / "consumer.prx").write_bytes(build_elf([ + ("BBBBBBBBBBB", 0, GLOBAL_FUNCTION)], ["provider.data"])) + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, [("BBBBBBBBBBB", "provider.data")]) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (0, "")) + data = json.loads(result.read_text()) + self.assertEqual(data["references"], 3) + self.assertEqual(data["unique_by_class"], {"implemented": 2, "stub": 0, "absent": 0, "module": 1}) + self.assertEqual(data["missing_libraries"], {}) + records = {(record["nid"], record.get("module")): record for record in data["imports"]} + self.assertEqual(records[("AAAAAAAAAAA", "provider.data")]["providers"], ["libSceA.prx"]) + self.assertEqual(records[("BBBBBBBBBBB", "consumer.prx")]["providers"], ["provider.data"]) + + def test_bad_named_modules_and_malformed_elf_are_not_ignored(self): + work = self.work + (work.libs / "libc.prx").write_bytes(exporting()) + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, []) + previous = b'{"previous":"report"}\n' + invalid_class = bytearray(exporting()) + invalid_class[4] = 1 + candidates = [("guest" + suffix, b"module placeholder\n") + for suffix in (".prx", ".sprx", ".suprx", ".debug_prx", ".elf", ".PRX")] + candidates += [("truncated.data", b"\x7fELF"), ("unsupported.data", invalid_class)] + for name, image in candidates: + module = work.modules / name + module.write_bytes(image) + for exists in (False, True): + with self.subTest(name=name, existing_report=exists): + if exists: + result.write_bytes(previous) + else: + result.unlink(missing_ok=True) + code, out, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, out), (2, "")) + self.assertIn(name, err) + self.assertIn("ELF", err) + if exists: + self.assertEqual(result.read_bytes(), previous) + else: + self.assertFalse(result.exists()) + module.unlink() + + def test_pe_and_self_modules_are_not_ignored_as_sidecars(self): + work = self.work + (work.libs / "libc.prx").write_bytes(exporting()) + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, []) + previous = b'{"previous":"report"}\n' + for image in (build_pe(["alpha"]), b"\x4f\x15\x3d\x1d" + bytes(64), + b"\x54\x14\xf5\xee" + bytes(64)): + for name in ("guest.prx", "metadata.txt"): + module = work.modules / name + module.write_bytes(image) + with self.subTest(name=name, magic=bytes(image[:4])): + result.write_bytes(previous) + code, out, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, out), (2, "")) + self.assertIn(name, err) + self.assertIn("ELF", err) + self.assertEqual(result.read_bytes(), previous) + module.unlink() + def test_module_needed_library_without_a_file_is_missing(self): work = self.work (work.libs / "libc.prx").write_bytes(exporting()) @@ -397,6 +500,66 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual(data["missing_libraries"], {"libSceMissing.prx": 1}) self.assertIn("libSceMissing.prx", out) + def test_conflicting_module_files_preserve_or_leave_no_json_report(self): + work = self.work + first, second = work.modules, work.root / "other-modules" + second.mkdir() + (work.libs / "libc.prx").write_bytes(exporting()) + (first / "main.prx").write_bytes(build_elf([ + ("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 1, GLOBAL_FUNCTION)], ["libSceMissing.prx"])) + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, []) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (1, "")) + data = json.loads(result.read_text()) + self.assertEqual(data["unique_by_class"]["absent"], 1) + self.assertEqual(data["missing_libraries"], {"libSceMissing.prx": 1}) + (second / "main.prx").write_bytes(exporting("CCCCCCCCCCC")) + previous = b'{"previous": "complete report"}\n' + for directories in ((first, second), (second, first)): + for exists in (False, True): + with self.subTest(directories=directories, existing_report=exists): + if exists: + result.write_bytes(previous) + else: + result.unlink(missing_ok=True) + with patch.object(work, "modules", directories[0]): + code, out, err = work.run(registry, "--modules", str(directories[1]), "--json", str(result)) + self.assertEqual((code, out), (2, "")) + self.assertIn("ambiguous guest module main.prx", err) + self.assertIn(str(first / "main.prx"), err) + self.assertIn(str(second / "main.prx"), err) + if exists: + self.assertEqual(result.read_bytes(), previous) + else: + self.assertFalse(result.exists()) + + def test_identical_module_files_are_audited_once(self): + work = self.work + first, second = work.modules, work.root / "other-modules" + second.mkdir() + (work.libs / "libc.prx").write_bytes(exporting()) + image = build_elf([ + ("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 1, GLOBAL_FUNCTION)], ["libSceMissing.prx"]) + for directory in (first, second): + (directory / "main.prx").write_bytes(image) + (first / "consumer.prx").write_bytes(build_elf([("BBBBBBBBBBB", 0, GLOBAL_FUNCTION)])) + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, []) + for directories in ((first, second, first), (second, first, second)): + with self.subTest(directories=directories), patch.object(work, "modules", directories[0]): + extra = [argument for directory in directories[1:] for argument in ("--modules", str(directory))] + code, _, err = work.run(registry, *extra, "--json", str(result)) + self.assertEqual((code, err), (1, "")) + data = json.loads(result.read_text()) + self.assertEqual((data["references"], data["unique_imports"]), (2, 2)) + self.assertEqual(data["unique_by_class"], {"implemented": 1, "stub": 0, "absent": 1, "module": 0}) + self.assertEqual(data["references_by_class"], data["unique_by_class"]) + self.assertEqual(data["missing_libraries"], {"libSceMissing.prx": 1}) + records = {record["nid"]: record for record in data["imports"]} + self.assertEqual((records["AAAAAAAAAAA"]["class"], records["AAAAAAAAAAA"]["references"]), ("absent", 1)) + self.assertEqual((records["BBBBBBBBBBB"]["class"], records["BBBBBBBBBBB"]["providers"]), ("implemented", ["main.prx"])) + def test_module_files_must_be_elf(self): work = self.work (work.libs / "libc.prx").write_bytes(exporting()) @@ -432,6 +595,31 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual(import_audit.elf_program_dynamic_symbols(Path("module.prx"), image), ({"AAAAAAAAAAA"}, {"BBBBBBBBBBB"}, {"libSceA.prx"})) + def test_sectionless_hash_metadata_must_be_file_backed(self): + for gnu_hash in (False, True): + image = bytearray(build_sectionless_elf([("AAAAAAAAAAA", 1, GLOBAL_FUNCTION)], gnu_hash=gnu_hash)) + load = list(struct.unpack_from("<IIQQQQQQ", image, 64)) + dynamic = list(struct.unpack_from("<IIQQQQQQ", image, 120)) + tags = dict(struct.unpack_from("<qQ", image, position) + for position in range(dynamic[2], dynamic[2] + dynamic[5], 16)) + address = tags[0x6ffffef5 if gnu_hash else 4] + offset = address - load[3] + load[2] + load[5] = load[6] = offset + (24 if gnu_hash else 8) + if gnu_hash: + struct.pack_into("<I", image, offset + 24, 0) + dynamic[3] += 0x1000 + dynamic[4] = dynamic[3] + second = [1, 4, dynamic[2], dynamic[3], dynamic[4], dynamic[5], dynamic[5], 1] + table = len(image) + for header in (load, second, dynamic): + image.extend(struct.pack("<IIQQQQQQ", *header)) + struct.pack_into("<Q", image, 32, table) + struct.pack_into("<H", image, 56, 3) + for reader in (import_audit.elf_exports, import_audit.elf_imports): + with self.subTest(gnu_hash=gnu_hash, reader=reader.__name__): + with self.assertRaisesRegex(import_audit.AuditError, "hash"): + reader(Path("unmapped-hash.prx"), image) + def test_module_symbol_suffix_is_stripped(self): work = self.work exported = nid_names.compute_nid("sceRealName") @@ -485,6 +673,17 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual(data["stops"], [{"registry": str(registry), "nid": None, "library": None, "name": None}]) self.assertIn(f" {registry}: none", out) + def test_a_weak_unresolved_import_is_not_a_stop(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry = work.root / "game.json" + write_registry(registry, [("BBBBBBBBBBB", "libSceA.prx", True), ("CCCCCCCCCCC", "libSceA.prx")]) + result = work.root / "audit.json" + code, out, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (1, "")) + data = json.loads(result.read_text()) + self.assertEqual(data["stops"], [{"registry": str(registry), "nid": "CCCCCCCCCCC", "library": "libSceA.prx", "name": None}]) + def test_removing_an_export_flips_the_class(self): work = self.work registry = work.root / "game.json" @@ -496,6 +695,73 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual(code, 1) self.assertIn("AAAAAAAAAAA", out) + def test_needed_library_without_imports_is_reported(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry = work.root / "game.registry.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) + needed = work.root / "game.needed.json" + needed.write_text(json.dumps(["libSceA.prx", "Party.prx", "Party.prx"])) + result = work.root / "audit.json" + code, out, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (1, "")) + data = json.loads(result.read_text()) + self.assertEqual(data["missing_libraries"], {"Party.prx": 0}) + self.assertEqual((data["references"], data["unique_imports"]), (1, 1)) + self.assertIn("Party.prx: 0 imports", out) + self.assertEqual(data["needed_libraries"], ["Party.prx", "libSceA.prx"]) + (work.modules / "Party.prx").write_bytes(exporting()) + self.assertEqual(work.run(registry)[0], 0) + (work.modules / "Party.prx").unlink() + (work.libs / "Party.prx").write_bytes(exporting()) + self.assertEqual(work.run(registry)[0], 0) + + def test_needed_libraries_from_empty_and_module_registries(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting()) + registry = work.root / "game.registry.json" + module_registry = work.root / "game.engine.prx.guest.prx.registry.json" + write_registry(registry, []) + write_registry(module_registry, []) + (work.root / "game.needed.json").write_text('["Party.prx"]') + (work.root / "game.engine.prx.guest.prx.needed.json").write_text('["Party.prx", "libSceOther.prx"]') + result = work.root / "audit.json" + code, out, err = work.run(registry, "--json", str(result), registries=[module_registry]) + self.assertEqual((code, err), (1, "")) + data = json.loads(result.read_text()) + self.assertEqual(data["missing_libraries"], {"Party.prx": 0, "libSceOther.prx": 0}) + self.assertEqual((data["references"], data["unique_imports"], data["imports"]), (0, 0, [])) + self.assertIn("0 references, 0 unique imports", out) + + def test_invalid_needed_registry_fails_instead_of_ignoring_it(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting()) + registry = work.root / "game.registry.json" + write_registry(registry, []) + needed = work.root / "game.needed.json" + for text in ('{', '{}', '[42]', '[""]'): + with self.subTest(text=text): + needed.write_text(text) + code, out, err = work.run(registry) + self.assertEqual((code, out), (2, "")) + self.assertIn(str(needed), err) + + def test_json_output_cannot_overwrite_needed_registry(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting()) + registry = work.root / "game.registry.json" + write_registry(registry, []) + needed = work.root / "game.needed.json" + needed.write_text('["Party.prx"]') + alias = work.root / "needed-alias.json" + os.link(needed, alias) + for output in (needed, alias): + with self.subTest(output=output): + code, out, err = work.run(registry, "--json", str(output)) + self.assertEqual((code, out), (2, "")) + self.assertIn("overwrite an input", err) + self.assertEqual(needed.read_text(), '["Party.prx"]') + def test_source_stubs(self): source = self.work.source write_source(source, "libSceTwin", STUB_SOURCE.format(name="sceTwinCall") + STUB_SOURCE.format(name="printf_nid_postfix")) @@ -517,11 +783,11 @@ class ImportAuditTests(unittest.TestCase): nid = nid_names.compute_nid("sceTwinCall") stubs = import_audit.source_stubs(source) exports = {nid: ["libA.prx", "libB.prx"]} - records, _ = import_audit.audit([(nid, "libB.prx")], {nid: ["libB.prx"]}, stubs, {"libB.prx"}, set()) + records, _ = import_audit.audit([(nid, "libB.prx", False)], {nid: ["libB.prx"]}, stubs, {"libB.prx"}, set()) self.assertEqual((records[0]["class"], records[0]["name"]), ("stub", "sceTwinCall")) - records, _ = import_audit.audit([(nid, "libA.prx")], {nid: ["libA.prx"]}, stubs, {"libA.prx"}, set()) + records, _ = import_audit.audit([(nid, "libA.prx", False)], {nid: ["libA.prx"]}, stubs, {"libA.prx"}, set()) self.assertEqual((records[0]["class"], records[0]["name"]), ("implemented", None)) - records, _ = import_audit.audit([(nid, "libB.prx")], exports, stubs, {"libA.prx", "libB.prx"}, set()) + records, _ = import_audit.audit([(nid, "libB.prx", False)], exports, stubs, {"libA.prx", "libB.prx"}, set()) self.assertEqual(records[0]["class"], "implemented") def test_cmake_shared_library_preserves_stub_names_and_implemented_exports(self): @@ -636,6 +902,29 @@ class ImportAuditTests(unittest.TestCase): def test_nid_anchor_matches_the_known_pair(self): self.assertEqual(nid_names.compute_nid(MESSAGE_NAME), MESSAGE_NID) + def test_unsupported_elf_versions_preserve_the_audit_report(self): + work = self.work + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")]) + library = work.libs / "libSceA.prx" + images = (exporting("AAAAAAAAAAA"), + build_sectionless_elf([("AAAAAAAAAAA", 1, GLOBAL_FUNCTION)]), + build_sectionless_elf([("AAAAAAAAAAA", 1, GLOBAL_FUNCTION)], gnu_hash=True)) + previous = b'{"previous":"complete report"}\n' + for index, valid in enumerate(images): + for offset, layout in ((6, "<B"), (20, "<I")): + for version in (0, 2): + with self.subTest(image=index, offset=offset, version=version): + image = bytearray(valid) + struct.pack_into(layout, image, offset, version) + library.write_bytes(image) + result.write_bytes(previous) + code, out, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, out), (2, "")) + self.assertIn("ELF version", err) + self.assertIn(str(library), err) + self.assertEqual(result.read_bytes(), previous) + def test_elf_reader(self): image = build_elf([("one", 1, GLOBAL_FUNCTION), ("weak", 1, 0x22), ("undefined", 0, GLOBAL_FUNCTION), ("local", 1, LOCAL_FUNCTION)]) path = Path("lib.prx") @@ -756,6 +1045,23 @@ class ImportAuditTests(unittest.TestCase): struct.pack_into("<H", image, 0x200 + 56 + 2, 0) self.assertEqual(import_audit.pe_exports(Path("lib.prx"), image), {"alpha", "beta"}) + def test_module_dependencies_stop_at_dynamic_terminator(self): + work = self.work + (work.libs / "libc.prx").write_bytes(exporting()) + image = bytearray(build_elf([], ["libc.prx", "libSceGhost.prx"])) + sections = struct.unpack_from("<Q", image, 40)[0] + dynamic = struct.unpack_from("<Q", image, sections + 192 + 24)[0] + struct.pack_into("<qQ", image, dynamic, 0, 0) + (work.modules / "module.prx").write_bytes(image) + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, []) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (0, "")) + self.assertEqual(json.loads(result.read_text())["missing_libraries"], {}) + struct.pack_into("<qQ", image, dynamic, 1, 1) + struct.pack_into("<qQ", image, dynamic + 16, 0, 0) + self.assertEqual(import_audit.elf_imports("module.prx", image)[1], {"libc.prx"}) + def test_pe32_uses_its_own_directory_layout(self): image = bytearray(build_pe(["alpha"])) struct.pack_into("<H", image, 0x58, 0x10b) @@ -889,7 +1195,7 @@ class ImportAuditTests(unittest.TestCase): with self.assertRaisesRegex(import_audit.AuditError, r"entry 1 has an invalid NID 'bad\\x01nid'"): import_audit.read_registry(path) write_registry(path, [("AAAAAAAAAAA#A#B", "libSceA.prx")], encoding="utf-8-sig") - self.assertEqual(import_audit.read_registry(path), [("AAAAAAAAAAA", "libSceA.prx")]) + self.assertEqual(import_audit.read_registry(path), [("AAAAAAAAAAA", "libSceA.prx", False)]) def test_names_never_download(self): work = self.work diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index ce95a7bcd0..6c7b5f76cc 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -1,5 +1,7 @@ import os import json +import ctypes +import stat import sys import tarfile import tempfile @@ -145,6 +147,95 @@ class ReleasePackageTests(unittest.TestCase): self.assertEqual(destination.read_bytes(), b"old " + name.encode()) self.assertEqual((output / "unrelated.txt").read_bytes(), b"unrelated") + @unittest.skipUnless(os.name == "nt", "Windows read-only replacement semantics") + def test_readonly_destinations_preserve_all_existing_assets(self): + names = ["prx-linux-v1.zip", "prx-linux-v1.tar.gz", "relinker-v1"] + for index, blocked in enumerate(names): + with self.subTest(blocked=blocked): + output = self.output / str(index) + output.mkdir(parents=True) + previous = {name: b"old " + name.encode() for name in names} + previous["unrelated.txt"] = b"unrelated" + for name, content in previous.items(): + (output / name).write_bytes(content) + destination = output / blocked + destination.chmod(stat.S_IREAD) + try: + with self.assertRaises((OSError, RuntimeError)) as error: + package_release.package("linux", self.build, output, "v1") + self.assertEqual({path.name: path.read_bytes() for path in output.iterdir()}, previous) + self.assertIn("read-only", str(error.exception)) + self.assertFalse(destination.stat().st_mode & stat.S_IWRITE) + finally: + destination.chmod(stat.S_IREAD | stat.S_IWRITE) + + @unittest.skipIf(os.name == "nt", "POSIX allows replacement in a writable directory") + def test_posix_readonly_destination_can_be_replaced(self): + self.output.mkdir() + destination = self.output / "prx-linux-v1.zip" + destination.write_bytes(b"old archive") + destination.chmod(stat.S_IREAD) + try: + package_release.package("linux", self.build, self.output, "v1") + with zipfile.ZipFile(destination) as archive: + self.assertEqual(archive.read("libs/libc.prx"), b"library bytes") + finally: + destination.chmod(stat.S_IREAD | stat.S_IWRITE) + + def file_link(self, link, target): + try: + link.symlink_to(target) + except OSError as error: + self.skipTest(f"File symlinks unavailable: {error}") + + def link_attributes(self, link, attributes): + setter = ctypes.WinDLL("kernel32", use_last_error=True).SetFileAttributesW + setter.argtypes = [ctypes.c_wchar_p, ctypes.c_uint32] + setter.restype = ctypes.c_int + if not setter(str(link), attributes): + raise ctypes.WinError(ctypes.get_last_error()) + + @unittest.skipUnless(os.name == "nt", "Windows read-only symlink semantics") + def test_readonly_dangling_link_preserves_existing_assets(self): + self.output.mkdir() + tar = self.output / "prx-linux-v1.tar.gz" + binary = self.output / "relinker-v1" + tar.write_bytes(b"old tar") + binary.write_bytes(b"old executable") + link = self.output / "prx-linux-v1.zip" + self.file_link(link, "missing.zip") + self.link_attributes(link, stat.FILE_ATTRIBUTE_READONLY) + try: + self.assertFalse(link.exists()) + with self.assertRaisesRegex(RuntimeError, "read-only"): + package_release.package("linux", self.build, self.output, "v1") + self.assertEqual(tar.read_bytes(), b"old tar") + self.assertEqual(binary.read_bytes(), b"old executable") + self.assertEqual(link.readlink(), Path("missing.zip")) + self.assertTrue(link.lstat().st_file_attributes & stat.FILE_ATTRIBUTE_READONLY) + self.assertEqual({path.name for path in self.output.iterdir()}, {tar.name, binary.name, link.name}) + finally: + self.link_attributes(link, stat.FILE_ATTRIBUTE_NORMAL) + + @unittest.skipUnless(os.name == "nt", "Windows link and target attributes are distinct") + def test_writable_link_to_readonly_target_can_be_replaced(self): + self.output.mkdir() + target = self.root / "original.zip" + target.write_bytes(b"original target") + link = self.output / "prx-linux-v1.zip" + self.file_link(link, target) + target.chmod(stat.S_IREAD) + try: + self.assertFalse(link.lstat().st_file_attributes & stat.FILE_ATTRIBUTE_READONLY) + package_release.package("linux", self.build, self.output, "v1") + self.assertFalse(link.is_symlink()) + self.assertEqual(target.read_bytes(), b"original target") + self.assertFalse(target.stat().st_mode & stat.S_IWRITE) + with zipfile.ZipFile(link) as archive: + self.assertEqual(archive.read("libs/libc.prx"), b"library bytes") + finally: + target.chmod(stat.S_IREAD | stat.S_IWRITE) + def test_invalid_tag_creates_no_assets(self): with self.assertRaises(ValueError): package_release.package("linux", self.build, self.output, "../v1.0") diff --git a/tools/tests/test_pr_overlap.py b/tools/tests/test_pr_overlap.py index ed4f4e4b25..b6471f1054 100644 --- a/tools/tests/test_pr_overlap.py +++ b/tools/tests/test_pr_overlap.py @@ -1,5 +1,9 @@ import json +import os +import shutil +import subprocess import sys +import tempfile import unittest from pathlib import Path from unittest.mock import patch @@ -77,6 +81,23 @@ class OpenPullRequestsTests(unittest.TestCase): pr_overlap.open_prs("main") +class TopInsertTests(unittest.TestCase): + def test_top_of_file_insertions_clash_on_line_one(self): + pr = {"tree": "t", "files": {"f.cpp"}, "added": set()} + with patch.object(pr_overlap, "git", return_value="@@ -0,0 +1 @@\n+x\n"): + self.assertEqual(pr_overlap.hunks(pr, "f.cpp"), [(1, 1)]) + self.assertEqual(pr_overlap.clashes(pr, pr, "f.cpp"), [(1, 1)]) + + def test_top_of_file_span_links_to_line_one(self): + self.assertEqual(pr_overlap.spans({"f.cpp": [(1, 1)]}, "https://x/blob/sha"), + "`f.cpp` [L1](https://x/blob/sha/f.cpp#L1)") + + def test_ordinary_hunk_is_unchanged(self): + pr = {"tree": "t"} + with patch.object(pr_overlap, "git", return_value="@@ -3,2 +3,2 @@\n-a\n-b\n+c\n+d\n"): + self.assertEqual(pr_overlap.hunks(pr, "f.cpp"), [(3, 4)]) + + class FetchTests(unittest.TestCase): def test_fetches_each_head_commit_the_api_reported(self): prs = { @@ -117,5 +138,101 @@ class ConflictTests(unittest.TestCase): merge.assert_called_once_with("first-tree", "second-tree") +class UnicodePathTests(unittest.TestCase): + def test_scans_and_reports_conflicts_with_git_path_quoting_enabled(self): + previous = os.getcwd() + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + + def git(*args): + return subprocess.run( + ["git", "-c", "user.name=Overlap Test", "-c", "user.email=test@example.com", + "-c", "commit.gpgsign=false", *args], cwd=root, + capture_output=True, text=True, encoding="utf-8", check=True).stdout.strip() + + filename = "core/libs/prx/libSceExample/caf\u00e9.cpp" + source = root / filename + source.parent.mkdir(parents=True) + source.write_text("int value = 0;\n", encoding="utf-8") + git("init", "-q") + git("config", "core.quotePath", "true") + git("add", ".") + git("commit", "-qm", "test: create base") + base = git("rev-parse", "HEAD") + git("update-ref", "refs/pr/base", base) + for branch, value in (("left", 1), ("right", 2)): + git("checkout", "-qb", branch, base) + source.write_text(f"int value = {value};\n", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "fix: change value") + try: + os.chdir(root) + left, right = {"ref": "left"}, {"ref": "right"} + pr_overlap.scan(left) + pr_overlap.scan(right) + self.assertEqual(left["files"], {filename}) + self.assertEqual(right["files"], {filename}) + self.assertEqual(pr_overlap.conflicts(left, right), {filename: [(1, 1)]}) + finally: + os.chdir(previous) + + +class CommentStateTests(unittest.TestCase): + def setUp(self): + if os.name == "nt" or not all(shutil.which(tool) for tool in ("bash", "jq", "sed", "sha256sum")): + self.skipTest("the workflow step runs in bash with jq, sed and sha256sum on Linux") + lines = (Path(__file__).resolve().parents[2] / ".github/workflows/pr-overlap.yml").read_text(encoding="utf-8").splitlines() + start = next(index for index, line in enumerate(lines) if "tools/pr_overlap.py" in line) + indent = len(lines[start]) - len(lines[start].lstrip()) + while start > 0 and len(lines[start - 1]) - len(lines[start - 1].lstrip()) == indent: + start -= 1 + step = [] + for line in lines[start:]: + if line.strip() and len(line) - len(line.lstrip()) < indent: + break + step.append(line[indent:]) + self.step = "\n".join(step).replace("${{ github.event.repository.default_branch }}", "main") + self.temp = tempfile.TemporaryDirectory() + self.root = Path(self.temp.name) + (self.root / "overlap").mkdir() + + def tearDown(self): + self.temp.cleanup() + + def report(self, commit, lines): + found = {7: {"conflicts": {"core/a.cpp": [lines]}, "added": set()}} + return pr_overlap.report(found, f"https://github.com/owner/project/blob/{commit}") + + def run_step(self, report, comment): + (self.root / "overlap" / "42.md").write_text(report, encoding="utf-8", newline="\n") + (self.root / "comment").write_text(json.dumps([5, comment]) + "\n" if comment else "", encoding="utf-8", newline="\n") + calls = self.root / "calls" + calls.write_text("", encoding="utf-8") + script = "\n".join([ + "set -eo pipefail", + "python3() { :; }", + 'gh() { printf \'%s\\n\' "$*" >> "$RUNNER_TEMP/calls"; case "$*" in "api repos/"*) cat "$RUNNER_TEMP/comment";; esac; }', + self.step, + ]) + (self.root / "step.sh").write_text(script, encoding="utf-8", newline="\n") + environment = dict(os.environ, RUNNER_TEMP=str(self.root), GH_REPO="owner/project") + result = subprocess.run(["bash", "step.sh"], cwd=self.root, env=environment, capture_output=True, text=True, timeout=20) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + return [call.split(" ")[:3] for call in calls.read_text(encoding="utf-8").splitlines()] + + def test_new_main_commit_alone_makes_no_requests(self): + first = self.report("a" * 40, (3, 4)) + self.assertEqual(self.run_step(first, None), [["api", "repos/owner/project/issues/42/comments", "--paginate"], ["pr", "comment", "42"]]) + self.assertEqual(self.run_step(self.report("b" * 40, (3, 4)), first), []) + + def test_lost_state_reads_the_comment_without_rewriting_it(self): + (self.root / "overlap-state.json").write_text("{}", encoding="utf-8") + self.assertEqual(self.run_step(self.report("b" * 40, (3, 4)), self.report("a" * 40, (3, 4))), [["api", "repos/owner/project/issues/42/comments", "--paginate"]]) + + def test_changed_lines_still_update_the_comment(self): + first = self.report("a" * 40, (3, 4)) + self.run_step(first, None) + self.assertEqual(self.run_step(self.report("b" * 40, (5, 6)), first), [["api", "repos/owner/project/issues/42/comments", "--paginate"], ["api", "-X", "PATCH"]]) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_progress.py b/tools/tests/test_progress.py index bf1ea28953..e2ac037b3f 100644 --- a/tools/tests/test_progress.py +++ b/tools/tests/test_progress.py @@ -280,5 +280,19 @@ class ProgressTests(unittest.TestCase): self.assertNotIn("<td>0</td>", html) + def test_qualified_calls_match_only_the_global_wrapper(self): + wrapper = "static void Fail() { NotImplemented_nid_no_patch(__func__); }\n" + cases = {"obj->Fail();": "done", "obj.Fail();": "done", "X::Fail();": "done", + "Foo<int>::Fail();": "done", "::Fail();": "todo", ":: Fail ();": "todo", "Fail();": "todo", + "return ::Fail();": "todo", "else ::Fail();": "todo", "if (obj) ::Fail();": "todo"} + for call, expected in cases.items(): + with self.subTest(call=call), tempfile.TemporaryDirectory() as directory: + library = Path(directory) / "libSceExample" + library.mkdir() + (library / "Export.cpp").write_text(wrapper + "int APS5_VABI Run(Obj* obj) { " + call + " return 0; }\n") + group = progress.scan_library(library) + self.assertEqual(group[expected + "_names"], ["Run"]) + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_progress_encoding.py b/tools/tests/test_progress_encoding.py index 289389fc50..a0e1b46852 100644 --- a/tools/tests/test_progress_encoding.py +++ b/tools/tests/test_progress_encoding.py @@ -29,7 +29,7 @@ class ProgressEncodingTests(unittest.TestCase): path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") env = {**os.environ, "PYTHONIOENCODING": encoding, "PYTHONUTF8": "0"} result = subprocess.run([sys.executable, "-X", "utf8=0", str(PROGRESS), "--compare", *map(str, paths)], - env=env, capture_output=True, timeout=10) + env=env, capture_output=True, timeout=60) self.assertEqual(result.returncode, 0, result.stderr.decode("utf-8", errors="replace")) return result.stdout.decode("utf-8").replace("\r\n", "\n") diff --git a/tools/tests/test_progress_wrappers.py b/tools/tests/test_progress_wrappers.py index 356e46aafb..3897bd125c 100644 --- a/tools/tests/test_progress_wrappers.py +++ b/tools/tests/test_progress_wrappers.py @@ -40,6 +40,37 @@ class WrapperTests(unittest.TestCase): def test_static_helper_still_works(self): self.assertTodo("static int Helper() " + STUB_BODY + CALLER) + def test_nested_helpers_are_pending_in_either_source_order(self): + for helpers in ( + "int Last() { NotImplemented_nid_no_patch(__func__); return 0; }\n" + "int Middle() { return Last(); }\nint Helper() { return Middle(); }\n", + "int Helper() { return Middle(); }\nint Middle() { return Last(); }\n" + "int Last() { NotImplemented_nid_no_patch(__func__); return 0; }\n", + ): + with self.subTest(helpers=helpers): + self.assertTodo(helpers + CALLER) + + def test_recursive_helper_chain_reaching_stub_is_pending(self): + self.assertTodo("int Helper() { return Middle(); }\n" + "int Middle() { if (false) return Helper(); " + "NotImplemented_nid_no_patch(__func__); return 0; }\n" + CALLER) + + def test_recursive_helpers_without_stub_stay_done(self): + group = scan("int Helper() { return Middle(); }\n" + "int Middle() { return Helper(); }\n" + CALLER) + self.assertEqual(group["done_names"], ["sceExampleCall"]) + self.assertEqual(group["todo_names"], []) + + def test_helper_chain_uses_whole_call_identifiers(self): + group = scan("int Last() { NotImplemented_nid_no_patch(__func__); return 0; }\n" + "int Helper() { return NotLast(); }\n" + CALLER) + self.assertEqual(group["done_names"], ["sceExampleCall"]) + self.assertEqual(group["todo_names"], []) + + def test_overloaded_helper_keeps_each_stub_path(self): + self.assertTodo("int Helper() { NotImplemented_nid_no_patch(__func__); return 0; }\n" + "int Helper(int value) { return value; }\n" + CALLER) + def test_unrelated_helper_stays_done(self): group = scan("namespace {\nint Other() { return 1; }\nint Helper() " + STUB_BODY + "}\n" "int APS5_VABI sceExampleCall() { return Other(); }\n") diff --git a/tools/tests/test_stubgen.py b/tools/tests/test_stubgen.py index 66ab9bd1b3..cfda701a6b 100644 --- a/tools/tests/test_stubgen.py +++ b/tools/tests/test_stubgen.py @@ -1,6 +1,7 @@ import contextlib import io import os +import stat import tempfile from pathlib import Path import sys @@ -121,5 +122,43 @@ class StubGeneratorTests(unittest.TestCase): self.assertEqual(code.stat().st_mode & 0o777, 0o666 & ~mask) +class ReadOnlyOutputTests(unittest.TestCase): + def test_later_staging_failure_preserves_original_error_and_cleans_readonly_temporary(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + nids, output = root / "nids.txt", root / "Export.cpp" + nids.write_text("AAAAAAAAAAA\n", encoding="utf-8") + original = "existing source\n" + output.write_text(original, encoding="utf-8") + output.chmod(0o444) + errors = io.StringIO() + arguments = ["--module", "libSceExample", "--nids", str(nids), "--out", str(output), + "--td-out", str(root / "absent" / "debt.txt")] + with contextlib.redirect_stderr(errors), self.assertRaises(SystemExit) as error: + stubgen.main(arguments) + self.assertEqual(error.exception.code, 2) + self.assertIn("absent", errors.getvalue()) + self.assertNotIn("Export.cpp.", errors.getvalue()) + self.assertEqual(output.read_text(encoding="utf-8"), original) + self.assertFalse(output.stat().st_mode & stat.S_IWRITE) + self.assertEqual(sorted(path.name for path in root.iterdir()), ["Export.cpp", "nids.txt"]) + + @unittest.skipUnless(os.name == "nt", "Windows denies replacing read-only output files") + def test_denied_replace_preserves_readonly_output_without_leaking_temporary(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + nids, output = root / "nids.txt", root / "Export.cpp" + nids.write_text("AAAAAAAAAAA\n", encoding="utf-8") + original = "existing source\n" + output.write_text(original, encoding="utf-8") + output.chmod(0o444) + with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as error: + stubgen.main(["--module", "libSceExample", "--nids", str(nids), "--out", str(output)]) + self.assertEqual(error.exception.code, 2) + self.assertEqual(output.read_text(encoding="utf-8"), original) + self.assertFalse(output.stat().st_mode & stat.S_IWRITE) + self.assertEqual(sorted(path.name for path in root.iterdir()), ["Export.cpp", "nids.txt"]) + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index e9862c2bc3..59056d06dd 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -18,6 +18,28 @@ class RegistrationTests(unittest.TestCase): (build / "CTestTestfile.cmake").write_text(content, encoding="utf-8") return check_test_registration.registered(build) + def test_bracket_comments_do_not_register_tests(self): + for equals in ("", "=", "==="): + with self.subTest(equals=equals): + content = (f"#[{equals}[\nadd_test(ghost /build/tests/missing_tests)\n]{equals}]\n" + 'add_test("real" "/build/tests/real_tests")\n') + self.assertEqual(self.registered(content), {"real_tests"}) + + def test_bracket_comment_does_not_disable_a_registered_test(self): + content = ('add_test("real" "/build/tests/real_tests")\n' + '#[=[\nset_tests_properties("real" PROPERTIES DISABLED TRUE)\n]=]\n') + self.assertEqual(self.registered(content), {"real_tests"}) + + def test_bracket_comment_inside_arguments_does_not_end_the_command(self): + content = ('add_test("real" #[=[ )\nadd_test(ghost /build/tests/missing_tests)\n]=]\n' + '"/build/tests/real_tests")\n') + self.assertEqual(self.registered(content), {"real_tests"}) + + def test_comment_markers_inside_literals_remain_arguments(self): + content = ('add_test("real" "/build/tests/real_tests" "#[[" [=[# comment]=] "\\\\#")\n') + self.assertEqual(list(check_test_registration.commands(content)), + [("add_test", ["real", "/build/tests/real_tests", "#[[", "# comment", "\\#"])]) + def test_test_name_does_not_register_executable(self): names = self.registered('add_test("missing_tests" "/usr/bin/python3" "unrelated.py")\n') self.assertNotIn("missing_tests", names) @@ -41,7 +63,8 @@ class RegistrationTests(unittest.TestCase): with tempfile.TemporaryDirectory() as directory: build = Path(directory) (build / "CMakeCache.txt").write_text("CMAKE_MAKE_PROGRAM:FILEPATH=/configured/ninja\nCMAKE_GENERATOR:INTERNAL=Ninja\n", encoding="utf-8") - with patch.object(check_test_registration.subprocess, "run", return_value=SimpleNamespace(stdout=output)) as run: + with patch.object(check_test_registration.subprocess, "run", side_effect=lambda command, **kwargs: SimpleNamespace( + stdout="Build file encoding: UTF-8\n" if command[-1] == "wincodepage" else output)) as run: self.assertEqual(check_test_registration.built(build), {"buffer-bounds_tests"}) self.assertEqual(run.call_args[0][0][0], "/configured/ninja") @@ -50,7 +73,8 @@ class RegistrationTests(unittest.TestCase): with tempfile.TemporaryDirectory() as directory: build = Path(directory) (build / "CMakeCache.txt").write_text("CMAKE_MAKE_PROGRAM:FILEPATH=/configured/ninja\nCMAKE_GENERATOR:INTERNAL=Ninja Multi-Config\n", encoding="utf-8") - with patch.object(check_test_registration.subprocess, "run", return_value=SimpleNamespace(stdout=output)) as run: + with patch.object(check_test_registration.subprocess, "run", side_effect=lambda command, **kwargs: SimpleNamespace( + stdout="Build file encoding: UTF-8\n" if command[-1] == "wincodepage" else output)) as run: self.assertEqual(check_test_registration.built(build), {"buffer_bounds_tests"}) self.assertEqual(run.call_args[0][0][0], "/configured/ninja") @@ -151,5 +175,39 @@ class RegistrationTests(unittest.TestCase): check_test_registration.built(build) run.assert_not_called() + @unittest.skipIf(shutil.which("ninja") is None, "requires a local Ninja to exercise its real output encoding") + def test_ninja_output_encoding_with_a_cp1252_default(self): + program = shutil.which("ninja") + actual_run = check_test_registration.subprocess.run + for encoding in (("utf-8", "mbcs") if sys.platform == "win32" else ("utf-8",)): + with self.subTest(encoding=encoding), tempfile.TemporaryDirectory() as directory: + build = Path(directory) + name = "caf\u00e9_tests".encode(encoding, errors="ignore").decode(encoding) + (build / "CMakeCache.txt").write_text( + f"CMAKE_MAKE_PROGRAM:FILEPATH={program}\nCMAKE_GENERATOR:INTERNAL=Ninja\n", encoding="utf-8") + (build / "build.ninja").write_text( + "rule CXX_EXECUTABLE_LINKER\n command = true\n" + f"build tests/{name}: CXX_EXECUTABLE_LINKER\n", encoding=encoding) + + def run(command, **kwargs): + if command[-1] == "wincodepage": + return SimpleNamespace(stdout="Build file encoding: " + ("ANSI" if encoding == "mbcs" else "UTF-8")) + kwargs.setdefault("encoding", "cp1252") + return actual_run(command, **kwargs) + + with patch.object(check_test_registration.subprocess, "run", side_effect=run): + self.assertEqual(check_test_registration.built(build), {name}) + + def test_unknown_windows_ninja_encoding_is_rejected(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + (build / "CMakeCache.txt").write_text( + "CMAKE_MAKE_PROGRAM:FILEPATH=/configured/ninja\nCMAKE_GENERATOR:INTERNAL=Ninja\n", encoding="utf-8") + with patch.object(check_test_registration.sys, "platform", "win32"), patch.object( + check_test_registration.subprocess, "run", return_value=SimpleNamespace(stdout="unknown")) as run: + with self.assertRaisesRegex(ValueError, "unsupported Ninja build-file encoding"): + check_test_registration.built(build) + run.assert_called_once() + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index 7af55af49b..9f81711170 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -165,6 +165,39 @@ class TraceDigestTests(unittest.TestCase): trace_digest.main([str(self.trace), "--json"]) self.assertEqual(json.loads(output.getvalue())["frames"], 3) + def test_fractional_adjacent_slices_remain_separate_in_command_output(self): + for first, second in (("Driver.Packet", "Driver.Completion"), + ("Driver.WorkerWait", "Driver.CompletionWait")): + with self.subTest(first=first): + events = [EVENTS[1], EVENTS[-3], + complete(1, first, 10000.1, 20000.2), + complete(1, second, 30000.3, 20000.0)] + with gzip.open(self.trace, "wt", encoding="utf-8") as handle: + json.dump({"traceEvents": events}, handle) + summary = dict(SUMMARY, window=dict(SUMMARY["window"], ms=50.0003)) + self.trace.with_name("aps5-summary.jsonl").write_text(json.dumps(summary), encoding="utf-8") + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(trace_digest.main([str(self.trace), "--json"]), 0) + result = json.loads(output.getvalue()) + thread = next(thread for thread in result["threads"] if thread["thread"] == "queue worker 0x0") + field = "wait" if trace_digest.is_wait(first) else "busy" + self.assertAlmostEqual(thread[field + "_ms_per_frame"], 40.0002) + self.assertAlmostEqual(thread[field + "_pct"], 100 * 40.0002 / 50.0003) + self.assertAlmostEqual(thread["untraced_ms_per_frame"], 10.0001) + if field == "busy": + self.assertEqual({entry["name"] for entry in thread["top"]}, {first, second}) + else: + self.assertEqual({entry["name"] for entry in result["longest_waits"]}, {first, second}) + + def test_one_nanosecond_nested_wait_remains_nested(self): + trace = trace_digest.Trace({"traceEvents": [ + complete(1, "Driver.WorkerWait", 10000.1, 20000.2), + complete(1, "Driver.InnerWait", 30000.299, 0.001), + complete(1, "Driver.Completion", 30000.3, 20000.0)]}) + self.assertEqual([(entry[3], entry[4]) for entry in trace.slices[1]], + [(0, False), (1, True), (0, False)]) + def interval_line(start, end, p99, gpu_ms, **changes): line = dict(SUMMARY, window={"spec": "summary_every 30", "first_present": 0, "ms": (end - start) * 1000.0}, interval_start_s=start, interval_end_s=end, diff --git a/tools/trace_digest.py b/tools/trace_digest.py index e7e9bc4caf..44af0d4c92 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -49,7 +49,8 @@ class Trace: slices.sort(key=lambda entry: (entry[0], -entry[1])) stack = [] for entry in slices: - while stack and entry[0] >= stack[-1][0] + stack[-1][1]: + start = round(entry[0] * 1000) + while stack and start >= round(stack[-1][0] * 1000) + round(stack[-1][1] * 1000): stack.pop() entry[3] = len(stack) entry[4] = any(is_wait(parent[2]) for parent in stack)