diff options
| author | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-11-01 16:21:45 -0500 |
|---|---|---|
| committer | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-11-01 16:21:45 -0500 |
| commit | a41d11108b6478ad862fc7fcac36ab51c83b8602 (patch) | |
| tree | 2c16c13764e6ef2b48c7327055fde64d7af2cd37 | |
| parent | 01effa81ab0e151f8cfd674bf4da952d24645943 (diff) | |
| parent | ac22c4087d9a6380c1937337530a5d4e54808ad1 (diff) | |
Merge remote-tracking branch 'origin/nx2' into merge/nx2-experimental-1101
# Conflicts:
# src/core/loader/deconstructed_rom_directory.cpp
119 files changed, 4667 insertions, 418 deletions
diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index e3314cedb2..20aa57f7aa 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -11,10 +11,27 @@ on: branches: - nx2 - nx2-experimental + # A binary only changes when build inputs change; docs-only pushes skip the build. + paths: + - "src/**" + - "CMakeLists.txt" + - "CMakeModules/**" + - "externals/**" + - "dist/**" + - "tools/**" + - ".patch/**" + - "cpmfile.json" + - ".github/workflows/release-binaries.yml" permissions: contents: read +# Clang major the Linux PGO build uses, from apt.llvm.org. Eden's profile is written by a current +# clang and an older reader refuses it, so this has to stay at or above whatever produced +# eden.profdata; bump it when a refreshed profile needs a newer LLVM. +env: + PGO_LLVM_VERSION: '23' + concurrency: group: release-binaries-${{ github.ref }} cancel-in-progress: true @@ -23,21 +40,42 @@ jobs: build: name: ${{ matrix.platform }} binary runs-on: ${{ matrix.runner }} + env: + CCACHE_DIR: ${{ github.workspace }}/.ccache + CCACHE_MAXSIZE: 1500M + SCCACHE_DIR: ${{ github.workspace }}/.sccache strategy: fail-fast: false matrix: include: - platform: linux-x86_64 runner: ubuntu-24.04 + # The two -pgo builds are the same programs compiled by clang against Eden's periodically + # updated profile (https://github.com/Eden-CI/PGO). Windows uses MSYS2's MinGW clang like + # Eden does; MSVC cannot read an LLVM .profdata file. + - platform: linux-x86_64-pgo + runner: ubuntu-24.04 + pgo: true - platform: windows-x86_64 runner: windows-2022 + - platform: windows-x86_64-pgo + runner: windows-2022 + pgo: true + mingw: true + - platform: linux-aarch64 + runner: ubuntu-24.04-arm + - platform: linux-aarch64-pgo + runner: ubuntu-24.04-arm + pgo: true + - platform: macos-arm64 + runner: macos-15 steps: - name: Check out source uses: actions/checkout@v4 - name: Install Linux dependencies - if: matrix.platform == 'linux-x86_64' + if: startsWith(matrix.platform, 'linux') shell: bash run: | sudo apt-get update @@ -49,10 +87,21 @@ jobs: libswscale-dev pkg-config zlib1g-dev libva-dev libvdpau-dev \ spirv-tools spirv-headers libusb-1.0-0-dev \ libboost-dev libboost-fiber-dev libboost-context-dev \ - libopus-dev libasound2-dev + libopus-dev libasound2-dev ccache + + # The packages docs/Deps.md lists for macOS, plus ccache. + - name: Install macOS dependencies + if: startsWith(matrix.platform, 'macos') + shell: bash + env: + HOMEBREW_NO_INSTALL_CLEANUP: 1 + run: | + brew install autoconf automake boost ffmpeg fmt glslang hidapi libtool libusb lz4 ninja \ + nlohmann-json openssl pkg-config qt@6 sdl3 speexdsp zlib zstd cmake catch2 molten-vk \ + vulkan-loader spirv-tools vulkan-utility-libraries ccache - - name: Install Qt on Linux - if: matrix.platform == 'linux-x86_64' + - name: Install Qt on Linux x86_64 + if: startsWith(matrix.platform, 'linux') && runner.arch != 'ARM64' uses: jurplel/install-qt-action@v4 with: version: '6.8.3' @@ -62,6 +111,46 @@ jobs: modules: qtcharts cache: true + - name: Install Qt on Linux ARM64 + if: startsWith(matrix.platform, 'linux') && runner.arch == 'ARM64' + uses: jurplel/install-qt-action@v4 + with: + version: '6.8.3' + host: linux_arm64 + target: desktop + arch: linux_gcc_arm64 + modules: qtcharts + cache: true + + # Ubuntu's own clang 18 cannot read a profile from a current LLVM, so take clang from the LLVM + # apt repo. The versioned packages are named explicitly: that repo's unversioned clang/lld + # metapackages can lag the binaries they depend on and then resolve to nothing. lld-23 only + # ships ld.lld under /usr/lib/llvm-23/bin, so -B points the linker search there. + - name: Install clang for PGO + if: startsWith(matrix.platform, 'linux') && matrix.pgo + shell: bash + run: | + set -euo pipefail + curl -fsSL https://apt.llvm.org/llvm-snapshot.gpg.key | + sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc > /dev/null + echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-$PGO_LLVM_VERSION main" | + sudo tee /etc/apt/sources.list.d/llvm.list > /dev/null + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + "clang-$PGO_LLVM_VERSION" "lld-$PGO_LLVM_VERSION" + "clang-$PGO_LLVM_VERSION" --version + + # Fetched into the source tree so both the Linux and MSYS2 configure steps can read it from a + # path their clang understands. + - name: Download PGO profile + if: matrix.pgo + shell: bash + run: | + set -euo pipefail + curl -fsSL -o eden.profdata \ + https://github.com/Eden-CI/PGO/releases/latest/download/eden.profdata + test -s eden.profdata + - name: Set up MSVC if: matrix.platform == 'windows-x86_64' uses: ilammy/msvc-dev-cmd@v1 @@ -77,6 +166,18 @@ jobs: modules: qtcharts cache: true + # MSVC is not something ccache handles well, so that job uses sccache. The other jobs use + # ccache, which CMake enables through USE_CCACHE (externals/cmake-modules/UseCcache.cmake). + - name: Install sccache on Windows + if: matrix.platform == 'windows-x86_64' + shell: pwsh + run: | + $zip = Join-Path $env:RUNNER_TEMP 'sccache.zip' + Invoke-WebRequest -OutFile $zip https://github.com/mozilla/sccache/releases/download/v0.19.0/sccache-v0.19.0-x86_64-pc-windows-msvc.zip + Expand-Archive $zip -DestinationPath (Join-Path $env:RUNNER_TEMP 'sccache') + $bin = (Get-ChildItem -Recurse -Filter sccache.exe (Join-Path $env:RUNNER_TEMP 'sccache')).DirectoryName + $bin | Out-File -Append -Encoding utf8 $env:GITHUB_PATH + - name: Install Vulkan SDK on Windows if: matrix.platform == 'windows-x86_64' shell: pwsh @@ -87,6 +188,24 @@ jobs: shell: pwsh run: glslangValidator --version + # Eden builds its Windows PGO binaries with MSYS2's clang toolchain, so we do the same. The + # package list is docs/Deps.md (MSYS2) plus what the bundled FFmpeg needs to compile. + - name: Set up MSYS2 + if: matrix.mingw + uses: msys2/setup-msys2@v2 + with: + msystem: UCRT64 + update: true + install: >- + git make autoconf libtool automake-wrapper patch jq pactoys ninja + pacboy: >- + toolchain:p clang:p lld:p cmake:p python-pip:p + qt6-base:p qt6-charts:p qt6-tools:p qt6-translations:p qt6-svg:p + openssl:p boost:p fmt:p lz4:p nlohmann-json:p zlib:p zstd:p libiconv:p + vulkan-memory-allocator:p vulkan-devel:p glslang:p enet:p opus:p + libusb:p unordered_dense:p jbigkit:p xz:p libdeflate:p bzip2:p + lerc:p graphite2:p libwebp:p libtiff:p brotli:p ccache:p + - name: Cache CPM downloads uses: actions/cache@v4 with: @@ -95,24 +214,52 @@ jobs: restore-keys: | cpm-${{ runner.os }}- + # The sha in the key makes every run save a fresh cache (actions/cache never saves over an + # exact hit), and restore-keys seeds it from the newest one of the same platform. + - name: Cache compiler objects + uses: actions/cache@v4 + with: + path: | + .ccache + .sccache + key: objects-${{ matrix.platform }}-${{ github.sha }} + restore-keys: | + objects-${{ matrix.platform }}- + - name: Configure Linux - if: matrix.platform == 'linux-x86_64' + if: startsWith(matrix.platform, 'linux') shell: bash - run: >- - cmake -S . -B build/release -G Ninja - -DCMAKE_BUILD_TYPE=Release - -DBUILD_TESTING=OFF - -DYUZU_TESTS=OFF - -DYUZU_CMD=ON - -DYUZU_ROOM=OFF - -DYUZU_ROOM_STANDALONE=OFF - -DYUZU_USE_BUNDLED_QT=OFF - -DCMAKE_PREFIX_PATH="$QT_ROOT_DIR" - -DYUZU_USE_BUNDLED_SDL3=ON - -DVulkanHeaders_FORCE_BUNDLED=ON - -DVulkanUtilityLibraries_FORCE_BUNDLED=ON - -DENABLE_UPDATE_CHECKER=OFF - -DUSE_DISCORD_PRESENCE=OFF + run: | + set -euo pipefail + flags=( + -DCMAKE_BUILD_TYPE=Release + -DBUILD_TESTING=OFF + -DYUZU_TESTS=OFF + -DYUZU_CMD=ON + -DYUZU_ROOM=OFF + -DYUZU_ROOM_STANDALONE=OFF + -DYUZU_USE_BUNDLED_QT=OFF + "-DCMAKE_PREFIX_PATH=$QT_ROOT_DIR" + -DYUZU_USE_BUNDLED_SDL3=ON + -DVulkanHeaders_FORCE_BUNDLED=ON + -DVulkanUtilityLibraries_FORCE_BUNDLED=ON + -DENABLE_UPDATE_CHECKER=OFF + -DUSE_DISCORD_PRESENCE=OFF + -DUSE_CCACHE=ON + ) + if [ '${{ matrix.pgo }}' = 'true' ]; then + # Same flags Eden's CI passes for its clang PGO build. + profile="-fprofile-use=$PWD/eden.profdata -Wno-backend-plugin \ + -Wno-profile-instr-unprofiled -Wno-profile-instr-out-of-date" + flags+=( + "-DCMAKE_C_COMPILER=clang-$PGO_LLVM_VERSION" + "-DCMAKE_CXX_COMPILER=clang++-$PGO_LLVM_VERSION" + "-DCMAKE_EXE_LINKER_FLAGS=-B/usr/lib/llvm-$PGO_LLVM_VERSION/bin -fuse-ld=lld" + "-DCMAKE_C_FLAGS=$profile" + "-DCMAKE_CXX_FLAGS=$profile" + ) + fi + cmake -S . -B build/release -G Ninja "${flags[@]}" - name: Configure Windows if: matrix.platform == 'windows-x86_64' @@ -129,16 +276,94 @@ jobs: -DYUZU_USE_BUNDLED_SDL3=ON -DENABLE_UPDATE_CHECKER=OFF -DUSE_DISCORD_PRESENCE=OFF + -DCMAKE_C_COMPILER_LAUNCHER=sccache + -DCMAKE_CXX_COMPILER_LAUNCHER=sccache + + - name: Configure macOS + if: startsWith(matrix.platform, 'macos') + shell: bash + run: | + set -euo pipefail + cmake -S . -B build/release -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTING=OFF \ + -DYUZU_TESTS=OFF \ + -DYUZU_CMD=ON \ + -DYUZU_ROOM=OFF \ + -DYUZU_ROOM_STANDALONE=OFF \ + -DYUZU_USE_BUNDLED_QT=OFF \ + "-DCMAKE_PREFIX_PATH=$(brew --prefix qt@6);$(brew --prefix zlib)" \ + "-DOPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" \ + -DVulkanHeaders_FORCE_BUNDLED=ON \ + -DVulkanUtilityLibraries_FORCE_BUNDLED=ON \ + -DENABLE_UPDATE_CHECKER=OFF \ + -DUSE_DISCORD_PRESENCE=OFF \ + -DUSE_CCACHE=ON + + # MinGW build: system Qt and libraries from MSYS2, clang plus Eden's PGO profile. The profile + # path is converted to its Windows form because clang runs with the MSYS mount underneath. + - name: Configure MinGW PGO + if: matrix.mingw + shell: msys2 {0} + run: | + set -euo pipefail + profile="-fprofile-use=$(cygpath -m "$PWD/eden.profdata") -Wno-backend-plugin \ + -Wno-profile-instr-unprofiled -Wno-profile-instr-out-of-date" + cmake -S . -B build/release -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTING=OFF \ + -DYUZU_TESTS=OFF \ + -DYUZU_CMD=ON \ + -DYUZU_ROOM=OFF \ + -DYUZU_ROOM_STANDALONE=OFF \ + -DYUZU_USE_BUNDLED_QT=OFF \ + -DYUZU_USE_BUNDLED_SDL3=ON \ + -DYUZU_USE_BUNDLED_FFMPEG=ON \ + -DVulkanHeaders_FORCE_BUNDLED=ON \ + -DVulkanUtilityLibraries_FORCE_BUNDLED=ON \ + -DENABLE_UPDATE_CHECKER=OFF \ + -DUSE_DISCORD_PRESENCE=OFF \ + -DUSE_CCACHE=ON \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DCMAKE_EXE_LINKER_FLAGS=-fuse-ld=lld \ + "-DCMAKE_C_FLAGS=$profile" \ + "-DCMAKE_CXX_FLAGS=$profile" - name: Build + if: ${{ ! matrix.mingw }} shell: bash - run: cmake --build build/release --target yuzu yuzu-cmd --parallel + run: | + cmake --build build/release --target yuzu yuzu-cmd --parallel + if command -v ccache > /dev/null; then ccache --show-stats; else sccache --show-stats; fi + + - name: Build MinGW PGO + if: matrix.mingw + shell: msys2 {0} + run: | + cmake --build build/release --target yuzu yuzu-cmd --parallel + ccache --show-stats + + # Plain tar.gz with the two programs. GitHub artifacts drop the executable bit, so a tar is + # what keeps it. This one is not self-contained: it needs Qt 6 and the usual libraries + # installed on the system. + - name: Package Linux tar.gz + if: startsWith(matrix.platform, 'linux') + shell: bash + run: | + set -euo pipefail + test -x build/release/bin/eglantine + test -x build/release/bin/eden-cli + strip build/release/bin/eglantine build/release/bin/eden-cli + tar -czf "eglantine-${GITHUB_SHA}-${{ matrix.platform }}.tar.gz" \ + -C build/release/bin eglantine eden-cli + ls -la "eglantine-${GITHUB_SHA}-${{ matrix.platform }}.tar.gz" # A self-contained AppImage: linuxdeploy bundles every library the two programs need except # the base system ones (glibc, X11/Wayland client libraries, the GPU driver and Vulkan loader), # and its Qt plugin adds the Qt libraries and platform plugins. Runs on glibc 2.39+ systems. - name: Package Linux AppImage - if: matrix.platform == 'linux-x86_64' + if: startsWith(matrix.platform, 'linux') shell: bash run: | set -euo pipefail @@ -149,19 +374,21 @@ jobs: libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxcb-xinerama0 libxcb-xkb1 tools="$RUNNER_TEMP/linuxdeploy" mkdir -p "$tools" - curl -fsSL -o "$tools/linuxdeploy-x86_64.AppImage" \ - https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage - curl -fsSL -o "$tools/linuxdeploy-plugin-qt-x86_64.AppImage" \ - https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage + arch=$(uname -m) # aarch64 or x86_64 + curl -fsSL -o "$tools/linuxdeploy-$arch.AppImage" \ + https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-$arch.AppImage + curl -fsSL -o "$tools/linuxdeploy-plugin-qt-$arch.AppImage" \ + https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-$arch.AppImage chmod +x "$tools"/*.AppImage # The desktop file names its icon dev.eglantine.eglantine. icon="$RUNNER_TEMP/dev.eglantine.eglantine.png" cp dist/Eglantine.png "$icon" export APPIMAGE_EXTRACT_AND_RUN=1 export QMAKE="$QT_ROOT_DIR/bin/qmake" - export LDAI_OUTPUT="eglantine-${GITHUB_SHA}-linux-x86_64.AppImage" + export LDAI_OUTPUT="eglantine-${GITHUB_SHA}-${{ matrix.platform }}.AppImage" export OUTPUT="$LDAI_OUTPUT" - "$tools/linuxdeploy-x86_64.AppImage" --appdir AppDir \ + export ARCH="$arch" + "$tools/linuxdeploy-$arch.AppImage" --appdir AppDir \ --executable build/release/bin/eglantine \ --executable build/release/bin/eden-cli \ --desktop-file dist/dev.eden_emu.eden.desktop \ @@ -182,6 +409,94 @@ jobs: } windeployqt --release --no-translations build/release/bin/eglantine.exe + # A dynamic MinGW build is not portable on its own: windeployqt6 brings the Qt libraries and + # plugins, then every non-system DLL the binaries and those plugins pull in gets copied next to + # them (docs/Caveats.md#msys2). + - name: Package MinGW binary + if: matrix.mingw + shell: msys2 {0} + run: | + set -euo pipefail + export PATH="/ucrt64/bin:$PATH" + cd build/release/bin + test -f eglantine.exe + test -f eden-cli.exe + windeployqt6 --no-translations --no-compiler-runtime --no-opengl-sw \ + --no-system-dxc-compiler --no-system-d3d-compiler eglantine.exe + deps() { + objdump -p "$1" | awk '/DLL Name:/ {print $3}' | while read -r dll; do + [ -z "$dll" ] && continue + source=$(command -v "$dll" 2>/dev/null || true) + [ -z "$source" ] && continue + # The system DLLs ship with Windows, so they never need bundling. + case "$source" in + *System32* | *SysWOW64*) continue ;; + esac + if [ ! -f "$dll" ]; then + cp "$source" "$dll" + deps "$source" + fi + done + } + for exe in eglantine.exe eden-cli.exe; do + deps "$exe" + done + find . -name '*.dll' | while read -r dll; do + deps "$dll" + done + + # A self-contained app bundle: macdeployqt copies Qt and every other Homebrew library both + # programs load into the bundle, then the leftover Homebrew search paths are removed so a + # machine with Homebrew still uses the bundled copies. Ad-hoc signed, not notarized. + - name: Package macOS app + if: startsWith(matrix.platform, 'macos') + shell: bash + run: | + set -euo pipefail + app=build/release/bin/eglantine.app + test -x "$app/Contents/MacOS/eglantine" + test -x build/release/bin/eden-cli + cp build/release/bin/eden-cli "$app/Contents/MacOS/eden-cli" + "$(brew --prefix qt@6)/bin/macdeployqt" "$app" \ + -executable="$app/Contents/MacOS/eden-cli" -libpath="$(brew --prefix)/lib" \ + -always-overwrite -verbose=1 + # Qt's virtual keyboard input plugin pulls in the QML stack, which the emulator doesn't use. + rm -f "$app/Contents/PlugIns/platforminputcontexts/libqtvirtualkeyboardplugin.dylib" + binaries=$(find "$app/Contents" -type f \( -perm -u+x -o -name '*.dylib' \) \ + -exec sh -c 'file -b "$1" | grep -q Mach-O' _ {} \; -print) + for binary in $binaries; do + otool -l "$binary" | awk '/cmd LC_RPATH/ {getline; getline; print $2}' | + { grep -E '^/(opt/homebrew|usr/local)/' || true; } | while read -r rpath; do + install_name_tool -delete_rpath "$rpath" "$binary" 2> /dev/null + done + done + # Libraries that load their dependencies through @rpath find them in the bundle. + for program in eglantine eden-cli; do + install_name_tool -add_rpath @executable_path/../Frameworks \ + "$app/Contents/MacOS/$program" 2> /dev/null + done + leaks=$(for binary in $binaries; do + otool -L "$binary" | grep -E '^\s+/(opt/homebrew|usr/local)/' + otool -l "$binary" | awk '/cmd LC_RPATH/ {getline; getline; print $2}' | + grep -E '^/(opt/homebrew|usr/local)/' + done || true) + if [ -n "$leaks" ]; then + echo "References still pointing outside the bundle:" + echo "$leaks" | sort -u + exit 1 + fi + # macdeployqt invalidates the signatures. The programs get the entitlements when the + # tree has them (the Hypervisor CPU backend needs com.apple.security.hypervisor). + codesign --force --deep --sign - "$app" + if [ -f dist/eglantine.entitlements ]; then + codesign --force --sign - --entitlements dist/eglantine.entitlements \ + "$app/Contents/MacOS/eden-cli" + codesign --force --sign - --entitlements dist/eglantine.entitlements "$app" + fi + codesign --verify --deep --strict "$app" + ditto -c -k --keepParent "$app" "eglantine-${GITHUB_SHA}-${{ matrix.platform }}.zip" + ls -la "eglantine-${GITHUB_SHA}-${{ matrix.platform }}.zip" + # The artifact is the bin folder itself: GitHub zips artifacts on download, and uploading a # zip gave a zip inside a zip. The release job zips it once for the release. - name: Upload Windows binary @@ -193,17 +508,40 @@ jobs: if-no-files-found: error retention-days: 7 + - name: Upload MinGW binary + if: matrix.mingw + uses: actions/upload-artifact@v4 + with: + name: eglantine-${{ matrix.platform }}-${{ github.sha }} + path: build/release/bin/ + if-no-files-found: error + retention-days: 7 + + # Both Linux files go in one artifact: the tar.gz and the AppImage. - name: Upload Linux binary - if: matrix.platform == 'linux-x86_64' + if: startsWith(matrix.platform, 'linux') uses: actions/upload-artifact@v4 with: name: eglantine-${{ matrix.platform }}-${{ github.sha }} - path: eglantine-${{ github.sha }}-${{ matrix.platform }}.AppImage + path: | + eglantine-${{ github.sha }}-${{ matrix.platform }}.tar.gz + eglantine-${{ github.sha }}-${{ matrix.platform }}.AppImage if-no-files-found: error compression-level: 0 retention-days: 7 - # Publishes both binaries as a GitHub pre-release once every platform built: on pushes to nx2 + # Zipped with ditto beforehand: the bundle's framework symlinks don't survive artifact zipping. + - name: Upload macOS app + if: startsWith(matrix.platform, 'macos') + uses: actions/upload-artifact@v4 + with: + name: eglantine-${{ matrix.platform }}-${{ github.sha }} + path: eglantine-${{ github.sha }}-${{ matrix.platform }}.zip + if-no-files-found: error + compression-level: 0 + retention-days: 7 + + # Publishes every binary as a GitHub pre-release once every platform built: on pushes to nx2 # and nx2-experimental, and on manual runs unless "Publish the binaries as a GitHub release" is # unticked. release: @@ -223,10 +561,19 @@ jobs: - name: Package release files shell: bash run: | + set -euo pipefail mkdir dist - (cd "artifacts/eglantine-windows-x86_64-${GITHUB_SHA}" && - zip -qr "${GITHUB_WORKSPACE}/dist/eglantine-${GITHUB_SHA}-windows-x86_64.zip" .) - cp "artifacts/eglantine-linux-x86_64-${GITHUB_SHA}/eglantine-${GITHUB_SHA}-linux-x86_64.AppImage" dist/ + for artifact in artifacts/*/; do + name=$(basename "$artifact") + if compgen -G "$artifact*.AppImage" > /dev/null; then + # Linux: ship the AppImage and the tar.gz as they are. + cp "$artifact"/*.AppImage "$artifact"/*.tar.gz dist/ + elif compgen -G "$artifact*.zip" > /dev/null; then + cp "$artifact"/*.zip dist/ + else + (cd "$artifact" && zip -qr "${GITHUB_WORKSPACE}/dist/${name}.zip" .) + fi + done - name: Create release shell: bash @@ -242,4 +589,19 @@ jobs: --notes "Automatic build of ${GITHUB_REF_NAME} at ${GITHUB_SHA}. Windows: extract the zip and run eglantine.exe, or eden-cli.exe from a terminal. - Linux: make the AppImage executable (chmod +x) and run it; it bundles its libraries and needs glibc 2.39 or newer (Ubuntu 24.04, Fedora 40, Debian 13, Arch)." + Linux (AppImage): make it executable (chmod +x) and run it; it bundles its libraries and needs glibc 2.39 or newer (Ubuntu 24.04, Fedora 40, Debian 13, Arch). + Linux (tar.gz): extract it and run ./eglantine or ./eden-cli; it uses your system libraries, so Qt 6 and the usual libraries must be installed. + macOS: Apple Silicon and macOS 15 or newer. Unzip and move eglantine.app to Applications; eden-cli is inside it, in Contents/MacOS. The app is not notarized, so the first time right-click it and choose Open (or run xattr -dr com.apple.quarantine eglantine.app). + The -pgo builds are the same programs compiled with profile-guided optimization (clang plus Eden's profile); prefer them when your CPU supports it. The Windows PGO build is a MinGW build, so it carries its own DLLs and needs no Visual C++ redistributable." + + - name: Ping Discord + shell: bash + env: + WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL }} + run: | + URL="https://github.com/${GITHUB_REPOSITORY}/releases/tag/build-${GITHUB_RUN_NUMBER}" + MSG=$(printf '## <@&1566184315178713190> Eglantine %s build %s (%s)\n%s' \ + "$GITHUB_REF_NAME" "$GITHUB_RUN_NUMBER" "${GITHUB_SHA::7}" "$URL") + jq -n --arg content "$MSG" \ + '{content: $content, allowed_mentions: {roles: ["1566184315178713190"]}}' \ + | curl -fsS -H "Content-Type: application/json" -d @- "$WEBHOOK" diff --git a/CMakeLists.txt b/CMakeLists.txt index f647204a4b..ba44aa9d47 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,8 +82,28 @@ cmake_dependent_option(YUZU_USE_BUNDLED_QT "Download bundled Qt binaries" "${MSV option(ENABLE_DEBUG_TOOLS "Enable debugging tools (maxwell disassembler, SPIRV translator, etc)" OFF) option(ENABLE_WERROR "Enable -Werror diagnostics" ON) +# Handle specific -Werror downgrades safely across platforms +if(ENABLE_WERROR) + if(CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + if(CMAKE_C_FLAGS MATCHES "-Werror=unused") + string(REPLACE "-Werror=unused" "-Wno-error=unused" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") + endif() + if(CMAKE_CXX_FLAGS MATCHES "-Werror=unused") + string(REPLACE "-Werror=unused" "-Wno-error=unused" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + endif() + + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-error=unused -Wno-error=unused-function") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-error=unused -Wno-error=unused-function") + elseif(MSVC) + # Windows Specifically + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /wd4100 /wd4101") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4100 /wd4101") + endif() +endif() + + # Lossless Scaling frame generation. Only Android. -cmake_dependent_option(ENABLE_LSFG "Enable Lossless Scaling frame generation" ON "ANDROID" OFF) +option(ENABLE_LSFG "Enable frame generation (LSFG and FSR 3 Fluid Motion)" ON) option(ENABLE_RESHADE "Enable ReShade FX post-processing effects" ON) @@ -293,7 +313,7 @@ if (ANDROID AND YUZU_DOWNLOAD_ANDROID_VVL) endif() if(EXISTS ${PROJECT_SOURCE_DIR}/hooks/pre-commit AND NOT EXISTS ${PROJECT_SOURCE_DIR}/.git/hooks/pre-commit) - if (EXISTS ${PROJECT_SOURCE_DIR}/.git/) + if (IS_DIRECTORY ${PROJECT_SOURCE_DIR}/.git) message(STATUS "Copying pre-commit hook") file(COPY hooks/pre-commit DESTINATION ${PROJECT_SOURCE_DIR}/.git/hooks) endif() diff --git a/NX2_NOTES.md b/NX2_NOTES.md index 8da5873a5c..08ce0123a1 100644 --- a/NX2_NOTES.md +++ b/NX2_NOTES.md @@ -211,6 +211,7 @@ With the patched decoder, games 1/3/4 only leave ~60-110 rare SIMD encodings in | 147 | title 1: Grand Prix cup-opening screen stays black, decoder registered but never started; then an abort with 2111-3604 (0x1c286f) when it did start | `nn::syncpt::sdk::CancelSyncPointEvent` sends 202 and always waits on the slot's event; we signalled it only for pending requests. Then the movie player reused slot 0, which NVN's fence waits had left signalled, read it as a decoded frame and found none | 202 always signals, 201 clears the slot's event first (b09629f) | | 148 | title 1/4 movies have no picture | the video decoder service (nn::codec::video, gsv 902/906/1002 -> vdec -> vdecsvc) completed frames without decoding | FFmpeg decodes on a thread per decoder service (`video_core/host1x/nx2_video_decoder`): 101 names the codec ("video/vp9") in an A buffer, 109 submits frames (A buffer, pts us). 311/314 reply NativeSyncHandles {~syncpoint, 0, value} on a syncpoint the service allocates, incremented per decoded frame, frames filling images in register/release order. gsv 911 creates the pool's images, 961 returns the CpuUserImageDescriptor that `ReadDecoderOutputImage*` copy out with (pitch-linear NV12/RGBA/BGRA, kinds 10/8/9). Title 1's Mushroom Cup opening (2560x1440 VP9, 1423 frames) decodes in real time and the game goes on to the race. The picture still stayed on the first frame: see 149. `EDEN_NX2_VDEC_DUMP=<dir>` saves decoded frames | | 149 | title 1: the cup-opening movie's picture stays on its first frame although every frame decodes and reaches GPU memory | not the texture cache: the movie is drawn into a 2560x1440 render target whose NVN2 texture has GPU address 0 (also the 640x360 and 512x512 passes), so the draws go nowhere. NVN2's texture init (sdk+0x5ca240) places a texture in a compressible pool (flags 0x080000a1) only after `NvRmGpuMappingModify` switched the pool's mapping (0x815f0d0000, a gpu 214 region) to the texture's kind; NvRm fails that call with 2 unless the characteristics flags have NVGPU_GPU_FLAGS_SUPPORT_MAPPING_MODIFY (bit 34; ours were 0x4400155), and the texture keeps address 0 | report bit 34; the modify then runs through nn::gpu::sdk's VirtualMachine as gpu sub 212 (NormalModifyMemoryKind4K: words 3-6 range, 7 wanted kind, 8 fallback, 10 job slot; reply word 3 the kind applied), answered with the wanted kind and the slot signalled. The movie plays on screen | +| 150 | title 1: animated colliders stay at their default pose while the meshes animate (Dino Dino Jungle's dino and the rail on it, DK Spaceport's robot legs on the last lap, Bowser's Castle Thwomps once down) | the guest CPU reads compute shader output back from GPU-written buffers, and nothing copied it to guest memory: no downloads at fences for Switch 2 titles (row 131), reactive flushing off (row 115; its read protection of every cached page stuttered), and RasterizerVulkan::GetFlushArea never asked the buffer cache (dropped upstream in 8203f2d8e1, when every GPU-written buffer was downloaded at fences), so even a trapped read found nothing to flush. A range read once is marked Preflushable and its later reads skip the flush | read traps (8b4c42c): a GPU write that is not downloaded at fences takes read permission off its pages once (fastmem only; not under NCE or with reactive flushing). The guest's first read takes the slow path and gives the page its read permission back; with no downloads at fences GetFlushArea asks the buffer cache, which flushes the range and marks it Preflushable, and Preflushable ranges are downloaded at every fence again. Diagnostic run: ~3,800 pages armed, ~3,500 trapped reads. The dino's rail follows it (2 of 2 tries); the robot and the Thwomps not retested, frame times not measured. `EDEN_NX2_READ_TRAPS=0` turns the traps off | Run settings used (in `build/bin/user/config/sdl2-config.ini`): `memory_layout_mode=4` (12 GB). Debug knobs: `EDEN_NX2_TC_EXPECTED_MIB` / `EDEN_NX2_TC_CRITICAL_MIB` (collector thresholds), @@ -226,6 +227,7 @@ faulting stack as module+offset, then a minidump), `EDEN_NX2_AUTO_STUB`, `EDEN_N `EDEN_NX2_SKIP_SAME=0` (row 135), `EDEN_NX2_MME_JIT=0` (row 140), `EDEN_NX2_SPLIT=0/1` (default 2), `EDEN_NX2_SPLIT_VERIFY=1`, `EDEN_NX2_SPLIT_CHECK=1`, `EDEN_NX2_SPLIT_SKIP=0` (rows 142-145), +`EDEN_NX2_READ_TRAPS=0` (row 150), `EDEN_NX2_FRAME_CAPTURE=<dir>` (every 120th presented frame as TGA), `EDEN_NX2_PB_CHECK` (pushbuffer entries changed between submit and execution), `EDEN_NX2_QUERY_TRACE` (every GPU report/semaphore write). `C:\dev\crashrun.sh` runs eden-cli under cdb and prints the crash stack; @@ -16,19 +16,16 @@ </h1> <h4 align="center"><b>Eglantine</b> is a free and opensource (FOSS) Switch 2 emulator, derived from Eden, Yuzu and Sudachi - started by developer Camille LaVey. -It's written in C++ with portability in mind, with builds for Windows, Linux, macOS, Android, FreeBSD and more. +It's written in C++ with portability in mind, with builds for Windows, Linux and macOS </h4> <p align="center"> </a> - <a href="https://discord.gg/HstXbPch7X"> - <img src="https://img.shields.io/discord/1367654015269339267?color=5865F2&label=Eden&logo=discord&logoColor=white" + <a href="https://discord.gg/jCCZ58VExk"> + <img src="https://img.shields.io/discord/1367654015269339267?color=5865F2&label=Eglantine&logo=discord&logoColor=white" alt="Discord"> </a> - <a href="https://stt.gg/qKgFEAbH"> - <img src="https://img.shields.io/revolt/invite/qKgFEAbH?color=d61f3a&label=Stoat" - alt="Stoat"> - </a> + </p> <p align="center"> @@ -42,25 +39,15 @@ It's written in C++ with portability in mind, with builds for Windows, Linux, ma ## Compatibility -The emulator is capable of running most commercial games at full speed, provided you meet the necessary hardware requirements. +The emulator is capable of running Mario Kart World at decent/full speed depending on specs and configurations, and Splatoon Raiders at variable performances A list of supported games will be available in future. Please be patient. -Check out our [website](https://eden-emu.dev) for the latest news on exciting features, monthly progress reports, and more! - -[](https://repology.org/project/eden-emulator/versions) - ## Development - -Most of the development happens on our Git server. It is also where [our central repository](https://git.eden-emu.dev/eden-emu/eden) is hosted. For development discussions, please join us on [Discord](https://discord.gg/HstXbPch7X) or [Stoat](https://stt.gg/qKgFEAbH). -You can also follow us on [X (Twitter)](https://nitter.poast.org/edenemuofficial) for updates and announcements. +For development discussions, please join us on [Discord](https://discord.gg/jCCZ58VExk) If you would like to contribute, we are open to new developers and pull requests. Please ensure that your work is of a high standard and properly documented. You can also contact any of the developers on Discord or Stoat to learn more about the current state of the emulator. -See the [sign-up instructions](docs/SIGNUP.md) for information on registration. - -Alternatively, if you wish to add translations, go to the [Eden project on Transifex](https://app.transifex.com/edenemu/eden-emulator) and review [the translations README](./dist/languages). - ## Documentation We have a user manual! See our [User Handbook](./docs/user/README.md). @@ -73,23 +60,14 @@ For information on provided development tooling, see the [Tools directory](./too ## Download -You can download the latest releases from [here](https://git.eden-emu.dev/eden-emu/eden/releases). - -Save us some bandwidth! We have [mirrors available](./docs/user/ThirdParty.md#mirrors) as well. +You can download the latest releases from [here](https://github.com/Djihads80/Eglantine/releases). ## Support -If you enjoy the project and would like to support us financially, please check out our developers' [donation pages](https://eden-emu.dev/donations)! - -Any donations received will go towards things such as: -* Switch consoles to explore and reverse-engineer the hardware -* Switch games for testing, reverse-engineering, and implementing new features -* Web hosting and infrastructure setup -* Additional hardware (e.g. GPUs as needed to improve rendering support, other peripherals to add support for, etc.) -* CI Infrastructure +We currently do not take in any donations for the time being -If you would prefer to support us in a different way, please join our [Discord](https://discord.gg/HstXbPch7X) and talk to Camille or any of our other developers. +If you would prefer to support us in a different way, please join our [Discord](https://discord.gg/jCCZ58VExk) ## License -Eden is licensed under the GPLv3 (or any later version). Refer to the [LICENSE.txt](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/LICENSE.txt) file. +Eglantine is licensed under the GPLv3 (or any later version). Refer to the [LICENSE.txt](https://github.com/Djihads80/Eglantine/blob/nx2/LICENSE.txt) file. diff --git a/cpmfile.json b/cpmfile.json index 0032734712..341e5b2c9f 100644 --- a/cpmfile.json +++ b/cpmfile.json @@ -9,7 +9,7 @@ "artifact": "boost-%VERSION%.tar.zst", "find_args": "CONFIG OPTIONAL_COMPONENTS headers context system fiber filesystem", "hash": "681d97be4386767662e230b2e7c9754d45e709cc5db4e747c23c27d1f5b0e87770bdf19b0bc44dcb0e8349324887bbb8fffb432098f4d0ce74a5043021a90afc", - "min_version": "1.57", + "min_version": "1.84", "package": "Boost", "repo": "eden-emulator/ext-boost", "version": "1.92.0" @@ -81,6 +81,13 @@ "repo": "crueter-ci/FFmpeg", "version": "9.0.2-1790726865-946fcce07b" }, + "ffmpeg-ci-av1": { + "ci": true, + "name": "ffmpeg", + "package": "FFmpeg", + "repo": "Takoroka/ffmpeg-av1", + "version": "9.0.2-av1-1" + }, "fmt": { "hash": "f0da82c545b01692e9fd30fdfb613dbb8dd9716983dcd0ff19ac2a8d36f74beb5540ef38072fdecc1e34191b3682a8542ecbf3a61ef287dbba0a2679d4e023f2", "min_version": "8", @@ -208,6 +215,7 @@ "version": "3.6.2" }, "opus": { + "bundled": true, "find_args": "MODULE", "hash": "9506147b0de35befda8633ff272981cc2575c860874791bd455b752f797fd7dbd1079f0ba42ccdd7bb1fe6773fa5e84b3d75667c2883dd1fb2d0e4a5fa4f8387", "min_version": "1.3", diff --git a/dist/Eglantine.icns b/dist/Eglantine.icns Binary files differnew file mode 100644 index 0000000000..7169466694 --- /dev/null +++ b/dist/Eglantine.icns diff --git a/dist/eglantine.icns b/dist/eglantine.icns Binary files differnew file mode 100644 index 0000000000..812334cd28 --- /dev/null +++ b/dist/eglantine.icns diff --git a/dist/post_shaders/AMDBoost.fx b/dist/post_shaders/AMDBoost.fx new file mode 100644 index 0000000000..af941c1550 --- /dev/null +++ b/dist/post_shaders/AMDBoost.fx @@ -0,0 +1,246 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later +// AMD Boost - Combined AMD FidelityFX enhancement suite +// Implements FSR 1 EASU upscaling + RCAS sharpening + optional denoising + +texture BackBufferTex : COLOR; +sampler BackBuffer { Texture = BackBufferTex; }; + +uniform float ScaleFactor < + ui_type = "slider"; + ui_label = "Scale Factor"; + ui_tooltip = "Upscaling factor (1.0 = native, 1.5 = 1.5x, 2.0 = 2x). Higher values require more performance."; + ui_min = 1.0; ui_max = 4.0; ui_step = 0.1; +> = 1.5; + +uniform float Sharpness < + ui_type = "slider"; + ui_label = "Sharpness"; + ui_tooltip = "RCAS sharpening strength. Higher values increase edge contrast."; + ui_min = 0.0; ui_max = 1.0; ui_step = 0.01; +> = 0.5; + +uniform bool EnableDenoise < + ui_type = "checkbox"; + ui_label = "Enable Denoising"; + ui_tooltip = "Apply a light denoising pass before upscaling to reduce artifacts."; +> = false; + +uniform float DenoiseStrength < + ui_type = "slider"; + ui_label = "Denoise Strength"; + ui_tooltip = "Strength of the denoising filter. Only applies when denoising is enabled."; + ui_min = 0.0; ui_max = 1.0; ui_step = 0.01; +> = 0.3; + +uniform bool EnableUpscaling < + ui_type = "checkbox"; + ui_label = "Enable Upscaling"; + ui_tooltip = "Enable FSR 1 EASU upscaling. Disable for sharpening only."; +> = true; + +#define FFX_FSR1_EASU_MAX_TAPS 5 + +// EASU constants - packed into uniforms +uniform float4 EASUCon0; +uniform float4 EASUCon1; +uniform float4 EASUCon2; +uniform float4 EASUCon3; + +// RCAS constants +uniform float4 RCASCon; + +// Compute input size from output size and scale factor +float2 GetInputSize(float2 outputSize, float scale) +{ + return outputSize / scale; +} + +// EASU: Edge-Adaptive Spatial Upsampling +float4 EASU(float2 uv, float2 inputSize, float2 outputSize) +{ + float2 inputViewportInPixels = outputSize; + float2 outputSizeInPixels = outputSize; + float2 inputSizeInPixels = inputSize; + + // Compute constants + float2 con0 = EASUCon0.xy; + float2 con1 = EASUCon1.xy; + float2 con2 = EASUCon2.xy; + float2 con3 = EASUCon3.xy; + + // Sample positions + float2 uv_input = uv * con0 + con1; + + // 5-tap sampling pattern + float2 offsets[5]; + offsets[0] = float2(0.0, 0.0); + offsets[1] = float2(con2.x, con2.y); + offsets[2] = float2(-con2.x, con2.y); + offsets[3] = float2(con2.x, -con2.y); + offsets[4] = float2(-con2.x, -con2.y); + + float4 samples[5]; + for (int i = 0; i < 5; i++) + { + float2 sample_uv = uv_input + offsets[i] * con3.xy; + samples[i] = tex2D(BackBuffer, saturate(sample_uv)); + } + + // Compute gradients + float3 c = samples[0].rgb; + float3 n = samples[1].rgb; + float3 s = samples[2].rgb; + float3 e = samples[3].rgb; + float3 w = samples[4].rgb; + + // Luminance weights + float3 luma_weights = float3(0.299, 0.587, 0.114); + + float luma_c = dot(c, luma_weights); + float luma_n = dot(n, luma_weights); + float luma_s = dot(s, luma_weights); + float luma_e = dot(e, luma_weights); + float luma_w = dot(w, luma_weights); + + // Gradient computation + float grad_ns = abs(luma_n - luma_s); + float grad_ew = abs(luma_e - luma_w); + + // Direction + float2 dir = float2(grad_ew, grad_ns); + float dir_length = length(dir) + 1e-6; + dir /= dir_length; + + // Weights + float len = length(dir * float2(grad_ew, grad_ns)); + float weight = 1.0 / (len + 1e-6); + + // Reconstruct + float3 result = c; + result += (n + s - 2.0 * c) * dir.y * weight; + result += (e + w - 2.0 * c) * dir.x * weight; + + return float4(saturate(result), 1.0); +} + +// RCAS: Radeon Contrast Adaptive Sharpening +float4 RCAS(float2 uv, float2 inputSize, float2 outputSize) +{ + float2 texel = float2(1.0 / inputSize.x, 1.0 / inputSize.y); + + float3 c = tex2D(BackBuffer, uv).rgb; + float3 n = tex2D(BackBuffer, uv + float2(0.0, -texel.y)).rgb; + float3 s = tex2D(BackBuffer, uv + float2(0.0, texel.y)).rgb; + float3 e = tex2D(BackBuffer, uv + float2(texel.x, 0.0)).rgb; + float3 w = tex2D(BackBuffer, uv + float2(-texel.x, 0.0)).rgb; + + float3 luma_weights = float3(0.299, 0.587, 0.114); + + float luma_c = dot(c, luma_weights); + float luma_n = dot(n, luma_weights); + float luma_s = dot(s, luma_weights); + float luma_e = dot(e, luma_weights); + float luma_w = dot(w, luma_weights); + + float grad_n = abs(luma_n - luma_c); + float grad_s = abs(luma_s - luma_c); + float grad_e = abs(luma_e - luma_c); + float grad_w = abs(luma_w - luma_c); + + float grad_ns = max(grad_n, grad_s); + float grad_ew = max(grad_e, grad_w); + + float2 dir = float2(grad_ew, grad_ns); + float dir_len = length(dir) + 1e-6; + dir /= dir_len; + + float sharpness = RCASCon.x; + float weight = sharpness * (1.0 - smoothstep(0.0, 0.5, dir_len)); + + float3 result = c; + result += (n + s - 2.0 * c) * dir.y * weight; + result += (e + w - 2.0 * c) * dir.x * weight; + + return float4(saturate(result), 1.0); +} + +// Simple denoise (bilateral-like) +float4 Denoise(float2 uv, float2 inputSize) +{ + float2 texel = float2(1.0 / inputSize.x, 1.0 / inputSize.y); + float3 center = tex2D(BackBuffer, uv).rgb; + + float3 sum = center; + float total_weight = 1.0; + + float3 luma_weights = float3(0.299, 0.587, 0.114); + float center_luma = dot(center, luma_weights); + + // 4-tap cross pattern + float2 offsets[4] = { + float2(-texel.x, 0.0), + float2(texel.x, 0.0), + float2(0.0, -texel.y), + float2(0.0, texel.y) + }; + + for (int i = 0; i < 4; i++) + { + float3 sample = tex2D(BackBuffer, uv + offsets[i]).rgb; + float sample_luma = dot(sample, luma_weights); + float diff = abs(sample_luma - center_luma); + float weight = exp(-diff * DenoiseStrength * 50.0); + + sum += sample * weight; + total_weight += weight; + } + + return float4(sum / total_weight, 1.0); +} + +void VS_PostProcess(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD) +{ + uv = float2(float(id & 2), float((id & 1) << 1)); + pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); +} + +float4 PS_AMDBoost(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + float2 outputSize = float2(BUFFER_WIDTH, BUFFER_HEIGHT); + float2 inputSize = GetInputSize(outputSize, ScaleFactor); + + float4 color = tex2D(BackBuffer, uv); + + // Apply denoising first if enabled + if (EnableDenoise) + { + color = Denoise(uv, outputSize); + } + + // Apply upscaling if enabled + if (EnableUpscaling && ScaleFactor > 1.0) + { + color = EASU(uv, inputSize, outputSize); + } + + // Apply RCAS sharpening + if (Sharpness > 0.0) + { + color = RCAS(uv, EnableUpscaling ? inputSize : outputSize, outputSize); + } + + return color; +} + +technique AMDBoost < + ui_label = "AMD Boost"; + ui_tooltip = "AMD FidelityFX Boost: Combines FSR 1 upscaling (EASU), RCAS sharpening, and optional denoising for enhanced image quality."; +> +{ + pass + { + VertexShader = VS_PostProcess; + PixelShader = PS_AMDBoost; + } +} diff --git a/docs/Build.md b/docs/Build.md index 84a3784fc0..edc9789299 100644 --- a/docs/Build.md +++ b/docs/Build.md @@ -1,9 +1,9 @@ -# Building Eden +# Building Eglantine > [!WARNING] > This guide is intended for developers ONLY. If you are not a developer or packager, you are unlikely to receive support. -This is a full-fledged guide to build Eden on all supported platforms. +This is a full-fledged guide to build Eglantine on all supported platforms. If you already have a development environment set up: @@ -20,11 +20,11 @@ First, you must [install some dependencies](Deps.md). ## Clone -Next, you will want to clone Eden via the terminal: +Next, you will want to clone Eglantine via the terminal: ```sh -git clone https://git.eden-emu.dev/eden-emu/eden.git -cd eden +git clone https://github.com/Djihads80/Eglantine.git +cd Eglantine ``` Or use Qt Creator (Create Project -> Import Project -> Git Clone). @@ -47,7 +47,7 @@ This is the recommended GUI method for Linux, macOS, and Windows. > [!WARNING] > On MSYS2, to use Qt Creator you are recommended to *also* install Qt from the online installer, ensuring to select the "MinGW" version. -Open the CMakeLists.txt file in your cloned directory via File -> Open File or Project (Ctrl+O), if you didn't clone Eden via the project import tool. +Open the CMakeLists.txt file in your cloned directory via File -> Open File or Project (Ctrl+O), if you didn't clone Eglantine via the project import tool. Select your desired "kit" (usually, the default is okay). RelWithDebInfo or Release is recommended: @@ -66,7 +66,7 @@ Hit "Configure Project", then wait for CMake to finish configuring (may take a w >For all systems: > >- *CMake* **MUST** be in your PATH (and also *ninja*, if you are using it as `<GENERATOR>`) ->- You *MUST* be in the cloned *Eden* directory +>- You *MUST* be in the cloned *Eglantine* directory > >On Windows: > @@ -135,12 +135,12 @@ cmake -S . -B build -G "<GENERATOR>" -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COM <img src="https://user-images.githubusercontent.com/42481638/216899164-6cee8482-3d59-428f-b1bc-e6dc793c9b20.png" width="500"> - Click OK; now CLion will build a directory and index your code to allow for IntelliSense. Please be patient. -- Once this process has been completed (No loading bar bottom right), you can now build Eden -- In the top right, click on the drop-down menu, select all configurations, then select Eden +- Once this process has been completed (No loading bar bottom right), you can now build Eglantine +- In the top right, click on the drop-down menu, select all configurations, then select Eglantine <img src="https://user-images.githubusercontent.com/42481638/216899226-975048e9-bc6d-4ec1-bc2d-bd8a1e15ed04.png" height="500" > -- Now run by clicking the play button or pressing Shift+F10, and Eden will auto-launch once built. +- Now run by clicking the play button or pressing Shift+F10, and Eglantine will auto-launch once built. <img src="https://user-images.githubusercontent.com/42481638/216899275-d514ec6a-e563-470e-81e2-3e04f0429b68.png" width="500"> </details> @@ -186,10 +186,10 @@ cmake --build build Your compiled executable will be in: -- `build/bin/eden.exe` for Windows, -- `build/bin/eden.app/Contents/MacOS/eden` for macOS, -- and `build/bin/eden` for others. +- `build/bin/eglantine.exe` for Windows, +- `build/bin/Eglantine.app/Contents/MacOS/eglantine` for macOS, +- and `build/bin/eglantine` for others. ## Scripts -Take a look at our [CI scripts](https://github.com/Eden-CI/Workflow). You can use `.ci/common/configure.sh` on any POSIX-compliant shell, but you are heavily encouraged to instead write your own based. It's not really that hard, provided you can read CMake. +Take a look at our [CI scripts](https://github.com/Eglantine-CI/Workflow). You can use `.ci/common/configure.sh` on any POSIX-compliant shell, but you are heavily encouraged to instead write your own based. It's not really that hard, provided you can read CMake. diff --git a/docs/Deps.md b/docs/Deps.md index 2b73ecb4ac..e6b3a4e3d7 100644 --- a/docs/Deps.md +++ b/docs/Deps.md @@ -242,7 +242,7 @@ If you're going for a pure build (i.e no downloaded deps), use `-DYUZU_USE_CPM=O Install dependencies from **[Homebrew](https://brew.sh/)** ```sh -brew install autoconf automake boost ffmpeg fmt glslang hidapi libtool libusb lz4 ninja nlohmann-json openssl pkg-config qt@6 sdl3 speexdsp zlib zstd cmake Catch2 molten-vk vulkan-loader spirv-tools +brew install autoconf automake boost ffmpeg fmt glslang hidapi libtool libusb lz4 ninja nlohmann-json openssl pkg-config qt@6 sdl3 speexdsp zlib zstd cmake Catch2 molten-vk vulkan-loader spirv-tools vulkan-utility-libraries ``` If you are compiling on Intel Mac, or are using a Rosetta Homebrew installation, you must replace all references of `/opt/homebrew` with `/usr/local`. diff --git a/eglantine.profdata b/eglantine.profdata Binary files differnew file mode 100644 index 0000000000..6e41f7d66d --- /dev/null +++ b/eglantine.profdata diff --git a/externals/cmake-modules/DetectArchitecture.cmake b/externals/cmake-modules/DetectArchitecture.cmake index 105963c8c2..be84109994 100644 --- a/externals/cmake-modules/DetectArchitecture.cmake +++ b/externals/cmake-modules/DetectArchitecture.cmake @@ -36,12 +36,17 @@ This file is based off of Yuzu and Dynarmic. # should NOT be too dependent on the architecture # otherwise, you may end up with duplicate code if (CMAKE_OSX_ARCHITECTURES) - set(MULTIARCH_BUILD 1) + list(LENGTH CMAKE_OSX_ARCHITECTURES architecture_count) + if (architecture_count GREATER 1) + set(MULTIARCH_BUILD 1) + else() + set(MULTIARCH_BUILD 0) + endif() set(ARCHITECTURE "${CMAKE_OSX_ARCHITECTURES}") # hope and pray the architecture names match - foreach(ARCH IN ${CMAKE_OSX_ARCHITECTURES}) - set(ARCHITECTURE_${ARCH} 1 PARENT_SCOPE) + foreach(ARCH IN LISTS CMAKE_OSX_ARCHITECTURES) + set(ARCHITECTURE_${ARCH} 1) add_definitions(-DARCHITECTURE_${ARCH}=1) endforeach() @@ -218,4 +223,4 @@ if (NOT DEFINED ARCHITECTURE) add_definitions(-DARCHITECTURE_GENERIC=1) endif() -message(STATUS "[DetectArchitecture] Target architecture: ${ARCHITECTURE}")
\ No newline at end of file +message(STATUS "[DetectArchitecture] Target architecture: ${ARCHITECTURE}") diff --git a/externals/ffmpeg/CMakeLists.txt b/externals/ffmpeg/CMakeLists.txt index f1bf099c10..890de4303c 100644 --- a/externals/ffmpeg/CMakeLists.txt +++ b/externals/ffmpeg/CMakeLists.txt @@ -179,7 +179,13 @@ elseif (MANAGARM) endif() if (YUZU_USE_BUNDLED_FFMPEG) - AddJsonPackage(ffmpeg-ci) + # Windows (MSVC) and Linux x86_64 get the same FFmpeg with libdav1d, for Switch 2 titles' AV1 + # movies; crueter-ci's packages, used elsewhere, have no AV1 decoder. + if ((MSVC OR LINUX) AND CPMUTIL_AMD64) + AddJsonPackage(ffmpeg-ci-av1) + else() + AddJsonPackage(ffmpeg-ci) + endif() set(FFmpeg_INCLUDE_DIR "${FFmpeg_SOURCE_DIR}/include;${FFmpeg_HWACCEL_INCLUDE_DIRS}" @@ -3,25 +3,31 @@ # SPDX-License-Identifier: GPL-3.0-or-later let - nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-24.05"; + nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-25.05"; pkgs = import nixpkgs { config = {}; overlays = []; }; in pkgs.mkShellNoCC { packages = with pkgs; [ # essential programs - git cmake clang gnumake patch jq pkg-config + git cmake clang gnumake ninja patch jq pkg-config # libraries - openssl boost fmt nlohmann_json lz4 zlib zstd + openssl boost fmt_11 nlohmann_json lz4 zlib zstd enet libopus vulkan-headers vulkan-utility-libraries - spirv-tools spirv-headers vulkan-loader unzip + spirv-tools spirv-headers vulkan-loader mesa unzip glslang python3 httplib cpp-jwt ffmpeg-headless libusb1 cubeb # eden - qt6.qtbase qt6.qtmultimedia qt6.qtwayland qt6.qttools + qt6.qtbase qt6.qtmultimedia qt6.qtwayland qt6.qttools qt6.qtcharts qt6.qtwebengine qt6.qt5compat # eden-cli - SDL3 + sdl3 # optional components discord-rpc gamemode ]; + # Host /usr Vulkan drivers can't load inside the Nix shell (their libdrm/libz deps + # aren't visible to the Nix dynamic linker), so point the loader at Nix's RADV. + VK_DRIVER_FILES = "${pkgs.mesa}/share/vulkan/icd.d/radeon_icd.x86_64.json"; + shellHook = '' + export CMAKE_PREFIX_PATH="$(echo $nativeBuildInputs $buildInputs | tr ' ' ':')" + ''; } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a2abc7d1b3..c49e252ccb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -178,6 +178,13 @@ else() $<$<COMPILE_LANGUAGE:C,CXX>:-Wno-unused-parameter> $<$<COMPILE_LANGUAGE:C,CXX>:-Wno-missing-field-initializers>) + if (ENABLE_WERROR AND (NOT MSVC)) + add_compile_options( + $<$<COMPILE_LANGUAGE:C,CXX>:-Wno-error=unused> + $<$<COMPILE_LANGUAGE:C,CXX>:-Wno-error=unused-function> + ) + endif() + if (CXX_CLANG OR CXX_ICC OR CXX_APPLE) # Clang, AppleClang, or Intel C++ if (NOT MSVC) add_compile_options( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/QuickSettings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/QuickSettings.kt index c2ef683cfd..62c9b5e753 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/QuickSettings.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/QuickSettings.kt @@ -15,6 +15,7 @@ import android.widget.TextView import androidx.drawerlayout.widget.DrawerLayout import com.google.android.material.color.MaterialColors import com.google.android.material.materialswitch.MaterialSwitch +import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting @@ -150,6 +151,10 @@ class QuickSettings(val emulationFragment: EmulationFragment) { switchView.setOnCheckedChangeListener { _, isChecked -> setting.setBoolean(isChecked) saveSettings() + // Lets the running game know it was docked or undocked. + if (setting == BooleanSetting.USE_DOCKED_MODE) { + NativeLibrary.applySettings() + } } switchContainer.setOnClickListener { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 824a54181e..99b45cf3f3 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -140,6 +140,7 @@ class SettingsFragmentPresenter( context.getString(R.string.scaling_filter_fsr), context.getString(R.string.scaling_filter_sgsr), context.getString(R.string.scaling_filter_sgsr_edge), + context.getString(R.string.scaling_filter_fsr3), ) return names.asSequence() .mapIndexedNotNull { index, name -> diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index 3323b3cf9a..9db255d70c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -1250,6 +1250,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { getString(R.string.scaling_filter_fsr), getString(R.string.scaling_filter_sgsr), getString(R.string.scaling_filter_sgsr_edge), + getString(R.string.scaling_filter_fsr3), ) return names.asSequence() .mapIndexedNotNull { index, name -> diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index be367a2fcf..a9cf000e43 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -81,6 +81,7 @@ <item>0</item> <item>7</item> <item>14</item> + <item>15</item> <item>9</item> <item>10</item> <item>15</item> @@ -279,6 +280,7 @@ <item>@string/scaling_filter_spline1</item> <item>@string/scaling_filter_sgsr</item> <item>@string/scaling_filter_sgsr_edge</item> + <item>@string/scaling_filter_fsr3</item> </string-array> <integer-array name="rendererScalingFilterValues"> @@ -297,6 +299,7 @@ <item>12</item> <item>13</item> <item>14</item> + <item>15</item> </integer-array> <string-array name="rendererAntiAliasingNames"> @@ -454,6 +457,7 @@ <item>13</item> <item>14</item> <item>15</item> + <item>15</item> <item>16</item> <item>17</item> <item>18</item> diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 5dd20af9a7..6304ef256b 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -1170,6 +1170,7 @@ <string name="scaling_filter_lanczos" translatable="false">Lanczos</string> <string name="scaling_filter_scale_force" translatable="false">ScaleForce</string> <string name="scaling_filter_fsr" translatable="false">AMD FidelityFX Super Resolution</string> + <string name="scaling_filter_fsr3" translatable="false">AMD FidelityFX Super Resolution 3</string> <string name="scaling_filter_area" translatable="false">Area</string> <string name="scaling_filter_zero_tangent" translatable="false">Zero-Tangent</string> <string name="scaling_filter_bspline" translatable="false">B-Spline</string> diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index d3e7b03cd2..76c5019788 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -152,7 +152,7 @@ add_library( set_source_files_properties(zbic_compression.cpp PROPERTIES INCLUDE_DIRECTORIES "${ZBIC_INCLUDE_DIR}" - COMPILE_OPTIONS "$<$<CXX_COMPILER_ID:Clang,GNU>:-Wno-unused-function;-Wno-missing-declarations;-Wno-shadow>;$<$<CXX_COMPILER_ID:MSVC>:/wd4505;/wd5245>") + COMPILE_OPTIONS "$<$<CXX_COMPILER_ID:AppleClang,Clang,GNU>:-Wno-unused-function;-Wno-missing-declarations;-Wno-shadow>;$<$<CXX_COMPILER_ID:MSVC>:/wd4505;/wd5245>") if(WIN32) target_sources(common PRIVATE windows/timer_resolution.cpp diff --git a/src/common/settings.cpp b/src/common/settings.cpp index 3f5154026e..c63943a9d3 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -60,6 +60,7 @@ SWITCHABLE(NvdecEmulation, false); SWITCHABLE(Region, true); SWITCHABLE(RendererBackend, true); SWITCHABLE(ScalingFilter, false); +SWITCHABLE(FrameGenType, false); SWITCHABLE(SpirvOptimizeMode, true); SWITCHABLE(TimeZone, true); SETTING(VSyncMode, true); @@ -224,6 +225,26 @@ u64 GetCurrentProgramID() { return current_program_id; } +static bool current_program_is_switch2 = false; + +void SetCurrentProgramIsSwitch2(bool is_switch2) { + current_program_is_switch2 = is_switch2; +} + +bool CurrentProgramIsSwitch2() { + return current_program_is_switch2; +} + +static std::optional<AstcRecompression> astc_recompression_override; + +AstcRecompression GetAstcRecompression() { + return astc_recompression_override.value_or(values.astc_recompression.GetValue()); +} + +void SetAstcRecompressionOverride(std::optional<AstcRecompression> recompression) { + astc_recompression_override = recompression; +} + bool IsDockedMode() { return values.use_docked_mode.GetValue() == Settings::ConsoleMode::Docked; } diff --git a/src/common/settings.h b/src/common/settings.h index b57bec5c12..97103f457d 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -10,6 +10,7 @@ #include <array> #include <map> #include <memory> +#include <optional> #include <stdexcept> #include <string> #include <utility> @@ -420,6 +421,15 @@ struct Values { SwitchableSetting<bool> frame_gen{linkage, false, "frame_gen", Category::Renderer, Specialization::Default, true, false}; + SwitchableSetting<FrameGenType> frame_gen_type{linkage, + FrameGenType::Fsr3, + "frame_gen_type", + Category::Renderer, + Specialization::Default, + true, + false, + &frame_gen}; + SwitchableSetting<u32, true> frame_gen_multiplier{linkage, 2, 2, @@ -977,6 +987,15 @@ bool IsNceEnabled(); void SetCurrentProgramID(u64 program_id); u64 GetCurrentProgramID(); +// Whether the program being loaded is a Switch 2 one (set by the core before the GPU starts). +void SetCurrentProgramIsSwitch2(bool is_switch2); +bool CurrentProgramIsSwitch2(); + +// The ASTC recompression in effect: astc_recompression, unless the renderer picked another one +// for the running program (the configuration is left as is). The core clears it on load. +AstcRecompression GetAstcRecompression(); +void SetAstcRecompressionOverride(std::optional<AstcRecompression> recompression); + bool IsOpenGL(); bool IsDockedMode(); diff --git a/src/common/settings_enums.h b/src/common/settings_enums.h index eaf3ec541d..133b503f72 100644 --- a/src/common/settings_enums.h +++ b/src/common/settings_enums.h @@ -146,7 +146,7 @@ ENUM(ConfirmStop, Ask_Always, Ask_Based_On_Game, Ask_Never); ENUM(FullscreenMode, Borderless, Exclusive); ENUM(NvdecEmulation, Off, Cpu, Gpu); ENUM(ResolutionSetup, Res1_4X, Res1_2X, Res3_4X, Res1X, Res5_4X, Res3_2X, Res2X, Res3X, Res4X, Res5X, Res6X, Res7X, Res8X); -ENUM(ScalingFilter, NearestNeighbor, Bilinear, Bicubic, Gaussian, Lanczos, ScaleForce, Fsr, Area, ZeroTangent, BSpline, Mitchell, Spline1, Mmpx, Sgsr, SgsrEdge); +ENUM(ScalingFilter, NearestNeighbor, Bilinear, Bicubic, Gaussian, Lanczos, ScaleForce, Fsr, Area, ZeroTangent, BSpline, Mitchell, Spline1, Mmpx, Sgsr, SgsrEdge, Fsr3); ENUM(AntiAliasing, None, Fxaa, Smaa); ENUM(AspectRatio, R16_9, R4_3, R21_9, R16_10, Stretch); ENUM(ConsoleMode, Handheld, Docked); @@ -161,6 +161,7 @@ ENUM(ExtendedDynamicState, Disabled, EDS1, EDS2, EDS3); ENUM(GpuLogLevel, Off, Errors, Standard, Verbose, All) ENUM(GameListMode, TreeView, GridView, CarouselView); ENUM(SpeedMode, Standard, Turbo, Slow); +ENUM(FrameGenType, Lsfg, Fsr3); template <typename Type> inline std::string_view CanonicalizeEnum(Type id) { diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 56fa653738..0075838d49 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1036,6 +1036,7 @@ add_library(core STATIC hle/service/server_manager.h hle/service/service.cpp hle/service/service.h + hle/service/nx2_streamplay.h hle/service/nx2_stub_interface.h hle/service/nx2_gsv.h hle/service/services.cpp diff --git a/src/core/arm/nce/visitor_base.h b/src/core/arm/nce/visitor_base.h index 6a2be3d9bc..a92307997e 100644 --- a/src/core/arm/nce/visitor_base.h +++ b/src/core/arm/nce/visitor_base.h @@ -717,6 +717,32 @@ public: return false; } + // ARMv8.1+ forms decoded for Switch 2 code (must match TranslatorVisitor in dynarmic's impl.h) + virtual bool AtomicMemOp(Imm<2> size, bool A, bool R, Reg Rs, Imm<4> op, Reg Rn, Reg Rt) { + return false; + } + virtual bool LDAPR_sz(Imm<2> size, Reg Rn, Reg Rt) { + return false; + } + virtual bool CompareAndSwap(Imm<2> size, bool L, Reg Rs, bool o0, Reg Rn, Reg Rt) { + return false; + } + virtual bool CompareAndSwapPair(bool sz, bool L, Reg Rs, bool o0, Reg Rn, Reg Rt) { + return false; + } + virtual bool PACAUT_reg(bool Z, Imm<3> op, Reg Rn, Reg Rd) { + return false; + } + virtual bool BRA_BLRA(bool Z, bool L, bool M, Reg Rn, Reg Rm) { + return false; + } + virtual bool STG_family(Imm<2> opc, Imm<9> imm9, Imm<2> mode, Reg Rn, Reg Rt) { + return false; + } + virtual bool STGP(Imm<2> mode, Imm<7> imm7, Reg Rt2, Reg Rn, Reg Rt) { + return false; + } + // Loads and stores - Load/Store register (register offset) virtual bool STRx_reg(Imm<2> size, Imm<1> opc_1, Reg Rm, Imm<3> option, bool S, Reg Rn, Reg Rt) { diff --git a/src/core/core.cpp b/src/core/core.cpp index 64e1d8de06..41871df870 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -318,6 +318,8 @@ struct System::Impl { const bool switch2_program = virtual_filesystem != nullptr && IsSwitch2Program(GetGameFileFromPath(virtual_filesystem, filepath)); + Settings::SetCurrentProgramIsSwitch2(switch2_program); + Settings::SetAstcRecompressionOverride(std::nullopt); // Switch 2 titles size their heap from the total memory and need the 12 GiB layout. Set it // as a per-game value, so RestoreGlobalState brings back the configured layout afterwards. @@ -1059,6 +1061,8 @@ void System::ApplySettings() { if (IsPoweredOn()) { Renderer().RefreshBaseSettings(); + // Frontends switch docked/handheld through the settings; let the running game know. + GetAppletManager().OperationModeChanged(); } } diff --git a/src/core/device_memory_manager.h b/src/core/device_memory_manager.h index 46cda14e78..27ff342213 100644 --- a/src/core/device_memory_manager.h +++ b/src/core/device_memory_manager.h @@ -126,6 +126,9 @@ public: // New batch API to update multiple ranges with a single lock acquisition. void UpdatePagesCachedBatch(std::span<const std::pair<DAddr, size_t>> ranges, s32 delta); + /// Traps the next guest CPU read of these pages, to learn which GPU writes the CPU reads. + void ArmReadTraps(DAddr addr, size_t size); + private: struct TranslationEntry { DAddr guest_page{}; @@ -187,6 +190,8 @@ private: void WalkBlock(const DAddr addr, const std::size_t size, auto on_unmapped, auto on_memory, auto increment); + void CutContinuityAt(size_t page); + void InnerGatherDeviceAddresses(Common::ScratchBuffer<u32>& buffer, PAddr address); std::unique_ptr<DeviceMemoryManagerAllocator<Traits>> impl; diff --git a/src/core/device_memory_manager.inc b/src/core/device_memory_manager.inc index d78a229e1f..83faa1b3f3 100644 --- a/src/core/device_memory_manager.inc +++ b/src/core/device_memory_manager.inc @@ -211,9 +211,13 @@ void DeviceMemoryManager<Traits>::Map(DAddr address, VAddr virtual_address, size std::scoped_lock lk(mapping_guard); tracked_entries.CommitRegion(start_page_d, start_page_d + num_pages); + CutContinuityAt(start_page_d); for (size_t i = 0; i < num_pages; i++) { const VAddr new_vaddress = virtual_address + i * Memory::YUZU_PAGESIZE; auto* ptr = process_memory->GetPointerSilent(Common::ProcessAddress(new_vaddress)); + // One page per run unless tracked below: a run left from an earlier mapping of these + // device pages would make WalkBlock copy the host memory that followed that mapping. + tracked_entries.GetUnchecked(start_page_d + i).continuity_tracker = 0; if (ptr == nullptr) [[unlikely]] { tracked_entries.GetUnchecked(start_page_d + i).compressed_physical_ptr = 0; continue; @@ -248,11 +252,13 @@ void DeviceMemoryManager<Traits>::Unmap(DAddr address, size_t size) { std::scoped_lock lk(mapping_guard); tracked_entries.CommitRegion(start_page_d, start_page_d + num_pages); // should already be committed, but just in case + CutContinuityAt(start_page_d); for (size_t i = 0; i < num_pages; i++) { auto& entry = tracked_entries.GetUnchecked(start_page_d + i); auto phys_addr = entry.compressed_physical_ptr; entry.compressed_physical_ptr = 0; entry.cpu_backing_address = 0; + entry.continuity_tracker = 0; if (phys_addr != 0) [[likely]] { u32& base_dev = compressed_device_addr.GetAndFault(phys_addr - 1U); if ((base_dev >> MULTI_FLAG_BITS) == 0) [[likely]] { @@ -270,6 +276,21 @@ void DeviceMemoryManager<Traits>::Unmap(DAddr address, size_t size) { } translation_generation.fetch_add(1, std::memory_order_release); } + +template <typename Traits> +void DeviceMemoryManager<Traits>::CutContinuityAt(size_t page) { + // Runs counted from the pages before `page` end there: the pages from `page` on are being + // mapped again or unmapped. A run is counted down page by page (TrackContinuityImpl), so the + // first page whose run already ends before `page` ends the walk. + for (size_t index = page; index-- > 0;) { + const size_t last_count = page - 1 - index; + if (tracked_entries[index].continuity_tracker <= last_count) { + break; + } + tracked_entries.GetUnchecked(index).continuity_tracker = static_cast<u32>(last_count); + } +} + template <typename Traits> void DeviceMemoryManager<Traits>::TrackContinuityImpl(DAddr address, VAddr virtual_address, size_t size, Asid asid) { @@ -671,4 +692,36 @@ void DeviceMemoryManager<Traits>::UpdatePagesCachedBatch(std::span<const std::pa } } +template <typename Traits> +void DeviceMemoryManager<Traits>::ArmReadTraps(DAddr addr, size_t size) { + const auto MarkRegionReadTrap = &DeviceMemoryManager<Traits>::DeviceMethods::MarkRegionReadTrap; + Memory::Memory* run_inter = nullptr; + u64 run_begin = 0; + u64 run_pages = 0; + const auto release_run = [&] { + if (run_pages > 0 && run_inter != nullptr) { + MarkRegionReadTrap(run_inter, run_begin << Memory::YUZU_PAGEBITS, + run_pages << Memory::YUZU_PAGEBITS); + } + run_pages = 0; + }; + const size_t page_end = Common::DivCeil(addr + size, Memory::YUZU_PAGESIZE); + for (size_t page = addr >> Memory::YUZU_PAGEBITS; page != page_end; ++page) { + auto [asid, vaddr] = ExtractCPUBacking(page); + const u64 vpage = vaddr >> Memory::YUZU_PAGEBITS; + if (vpage == 0) { + release_run(); + continue; + } + auto* const inter = registered_processes[asid.id]; + if (inter != run_inter || vpage != run_begin + run_pages) { + release_run(); + run_inter = inter; + run_begin = vpage; + } + ++run_pages; + } + release_run(); +} + } // namespace Core diff --git a/src/core/hle/kernel/k_address_space_info.cpp b/src/core/hle/kernel/k_address_space_info.cpp index 23258071e1..13a669cff5 100644 --- a/src/core/hle/kernel/k_address_space_info.cpp +++ b/src/core/hle/kernel/k_address_space_info.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -32,7 +35,8 @@ constexpr std::array<KAddressSpaceInfo, 13> AddressSpaceInfos{{ { .bit_width = 39, .address = 128_MiB , .size = 512_GiB - 128_MiB, .type = KAddressSpaceInfo::Type::Map39Bit, }, #endif { .bit_width = 39, .address = Size_Invalid, .size = 64_GiB , .type = KAddressSpaceInfo::Type::MapSmall }, - { .bit_width = 39, .address = Size_Invalid, .size = 8_GiB , .type = KAddressSpaceInfo::Type::Heap, }, + // Switch 2 titles size their heap from up to 12 GiB of memory, which doesn't fit Switch 1's 8 GiB. + { .bit_width = 39, .address = Size_Invalid, .size = 16_GiB , .type = KAddressSpaceInfo::Type::Heap, }, { .bit_width = 39, .address = Size_Invalid, .size = 64_GiB , .type = KAddressSpaceInfo::Type::Alias, }, { .bit_width = 39, .address = Size_Invalid, .size = 2_GiB , .type = KAddressSpaceInfo::Type::Stack, }, }}; diff --git a/src/core/hle/kernel/svc/svc_physical_memory.cpp b/src/core/hle/kernel/svc/svc_physical_memory.cpp index 4fde85fdaa..280731e488 100644 --- a/src/core/hle/kernel/svc/svc_physical_memory.cpp +++ b/src/core/hle/kernel/svc/svc_physical_memory.cpp @@ -4,10 +4,12 @@ // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include <algorithm> #include <cstdlib> #include "core/core.h" #include "core/hle/kernel/k_process.h" +#include "core/hle/kernel/k_system_control.h" #include "core/hle/kernel/svc.h" namespace Kernel::Svc { @@ -18,7 +20,10 @@ Result SetHeapSize(Core::System& system, u64* out_address, u64 size) { // Validate size. R_UNLESS(Common::IsAligned(size, HeapSizeAlignment), ResultInvalidSize); - R_UNLESS(size < MainMemorySizeMax, ResultInvalidSize); + // Switch 2 titles size their heap from the total memory, which exceeds Switch 1's 8 GiB. + const size_t max_heap_size = + (std::max)(MainMemorySizeMax, KSystemControl::Init::GetIntendedMemorySize()); + R_UNLESS(size < max_heap_size, ResultInvalidSize); // Set the heap size. KProcessAddress address{}; diff --git a/src/core/hle/service/am/applet_manager.cpp b/src/core/hle/service/am/applet_manager.cpp index 05bcf6cc05..887c4521fe 100644 --- a/src/core/hle/service/am/applet_manager.cpp +++ b/src/core/hle/service/am/applet_manager.cpp @@ -248,11 +248,16 @@ void AppletManager::RequestExit() { } } +// Frontends call this after any settings change, so only an actual docked/handheld switch is +// passed on. void AppletManager::OperationModeChanged() { std::scoped_lock lk{m_lock}; - if (m_window_system) { - m_window_system->OnOperationModeChanged(); + const bool docked = Settings::IsDockedMode(); + if (!m_window_system || docked == m_docked) { + return; } + m_docked = docked; + m_window_system->OnOperationModeChanged(); } void AppletManager::SetWindowSystem(WindowSystem* window_system) { @@ -262,6 +267,7 @@ void AppletManager::SetWindowSystem(WindowSystem* window_system) { if (!m_window_system) { return; } + m_docked = Settings::IsDockedMode(); m_cv.wait(lk, [&] { return m_pending_process != nullptr; }); diff --git a/src/core/hle/service/am/applet_manager.h b/src/core/hle/service/am/applet_manager.h index cd3035de31..bae3ab4269 100644 --- a/src/core/hle/service/am/applet_manager.h +++ b/src/core/hle/service/am/applet_manager.h @@ -58,6 +58,7 @@ private: std::condition_variable m_cv; WindowSystem* m_window_system{}; + bool m_docked{}; // console mode the applets were last told about FrontendAppletParameters m_pending_parameters{}; std::unique_ptr<Process> m_pending_process{}; diff --git a/src/core/hle/service/am/frontend/applet_software_keyboard.cpp b/src/core/hle/service/am/frontend/applet_software_keyboard.cpp index fe07d72912..db42781e18 100644 --- a/src/core/hle/service/am/frontend/applet_software_keyboard.cpp +++ b/src/core/hle/service/am/frontend/applet_software_keyboard.cpp @@ -213,6 +213,15 @@ void SoftwareKeyboard::InitializeForeground() { std::memcpy(&swkbd_config_new, swkbd_config_data.data() + sizeof(SwkbdConfigCommon), sizeof(SwkbdConfigNew)); break; + case SwkbdAppletVersion::Version1310736: + // The common and 8.0.0 parts keep their layout; the 0x68 bytes after them are unknown. + // This revision exchanges UTF-8 text: the game's text check replies are UTF-8, and a + // UTF-16 result reached it as its first character only. + ASSERT(swkbd_config_data.size() >= sizeof(SwkbdConfigCommon) + sizeof(SwkbdConfigNew)); + std::memcpy(&swkbd_config_new, swkbd_config_data.data() + sizeof(SwkbdConfigCommon), + sizeof(SwkbdConfigNew)); + swkbd_config_common.use_utf8 = true; + break; default: UNIMPLEMENTED_MSG("Unknown SwkbdConfig revision={} with size={}", swkbd_applet_version, swkbd_config_data.size()); diff --git a/src/core/hle/service/am/frontend/applet_software_keyboard_types.h b/src/core/hle/service/am/frontend/applet_software_keyboard_types.h index 6bb8f437d3..f263a704cd 100644 --- a/src/core/hle/service/am/frontend/applet_software_keyboard_types.h +++ b/src/core/hle/service/am/frontend/applet_software_keyboard_types.h @@ -30,6 +30,7 @@ enum class SwkbdAppletVersion : u32_le { Version327689 = 0x50009, // 5.0.0 - 5.1.0 Version393227 = 0x6000B, // 6.0.0 - 7.0.1 Version524301 = 0x8000D, // 8.0.0+ + Version1310736 = 0x140010, // Switch 2 (Pokemon Pokopia): 0x530-byte config, UTF-8 text }; enum class SwkbdType : u32 { diff --git a/src/core/hle/service/am/frontend/applets.cpp b/src/core/hle/service/am/frontend/applets.cpp index c59e4b9c6b..c1a0a1dd29 100644 --- a/src/core/hle/service/am/frontend/applets.cpp +++ b/src/core/hle/service/am/frontend/applets.cpp @@ -253,7 +253,7 @@ std::shared_ptr<FrontendApplet> FrontendAppletHolder::GetApplet(std::shared_ptr< case AppletId::None: return nullptr; default: - UNIMPLEMENTED_MSG("No frontend implementation exists for applet_id={:02X} program_id={:016X}! Using stub applet.", static_cast<u8>(id), applet->program_id); + UNIMPLEMENTED_MSG("No frontend implementation exists for applet_id={:#x} program_id={:016X}! Using stub applet.", static_cast<u32>(id), applet->program_id); return std::make_shared<StubApplet>(system, applet, id, mode); } } diff --git a/src/core/hle/service/am/lifecycle_manager.cpp b/src/core/hle/service/am/lifecycle_manager.cpp index 4b003c4696..14c6179949 100644 --- a/src/core/hle/service/am/lifecycle_manager.cpp +++ b/src/core/hle/service/am/lifecycle_manager.cpp @@ -12,7 +12,8 @@ namespace Service::AM { LifecycleManager::LifecycleManager(Core::System& system, KernelHelpers::ServiceContext& context, bool is_application) : m_system_event(context), m_operation_mode_changed_system_event(context), - m_hdcp_state_changed_event(context), m_is_application(is_application) {} + m_operation_mode_refresh_event(context), m_hdcp_state_changed_event(context), + m_is_application(is_application) {} LifecycleManager::~LifecycleManager() = default; @@ -24,6 +25,10 @@ Event& LifecycleManager::GetOperationModeChangedSystemEvent() { return m_operation_mode_changed_system_event; } +Event& LifecycleManager::GetOperationModeRefreshEvent() { + return m_operation_mode_refresh_event; +} + Event& LifecycleManager::GetHDCPStateChangedEvent() { return m_hdcp_state_changed_event; } @@ -152,6 +157,7 @@ void LifecycleManager::OnOperationAndPerformanceModeChanged(Kernel::KernelCore& m_has_performance_mode_changed = true; } m_operation_mode_changed_system_event.Signal(kernel); + m_operation_mode_refresh_event.Signal(kernel); this->SignalSystemEventIfNeeded(kernel); } diff --git a/src/core/hle/service/am/lifecycle_manager.h b/src/core/hle/service/am/lifecycle_manager.h index 15657a36d8..097ad00776 100644 --- a/src/core/hle/service/am/lifecycle_manager.h +++ b/src/core/hle/service/am/lifecycle_manager.h @@ -45,6 +45,7 @@ public: public: Event& GetSystemEvent(); Event& GetOperationModeChangedSystemEvent(); + Event& GetOperationModeRefreshEvent(); Event& GetHDCPStateChangedEvent(); public: @@ -158,6 +159,7 @@ private: private: Event m_system_event; Event m_operation_mode_changed_system_event; + Event m_operation_mode_refresh_event; Event m_hdcp_state_changed_event; std::list<AppletMessage> m_unordered_messages{}; diff --git a/src/core/hle/service/am/service/application_functions.cpp b/src/core/hle/service/am/service/application_functions.cpp index e495537130..37dfbfa344 100644 --- a/src/core/hle/service/am/service/application_functions.cpp +++ b/src/core/hle/service/am/service/application_functions.cpp @@ -15,6 +15,7 @@ #include "core/file_sys/patch_manager.h" #include "core/file_sys/registered_cache.h" #include "core/file_sys/savedata_factory.h" +#include "core/hle/kernel/k_process.h" #include "core/hle/kernel/k_transfer_memory.h" #include "core/hle/result.h" #include "core/hle/service/am/am_results.h" @@ -116,7 +117,7 @@ IApplicationFunctions::IApplicationFunctions(Core::System& system_, std::shared_ {181, nullptr, "UpgradeLaunchRequiredVersion"}, {190, nullptr, "SendServerMaintenanceOverlayNotification"}, {200, nullptr, "GetLastApplicationExitReason"}, - {210, D<&IApplicationFunctions::GetUnknownEvent210>, "Unknown210"}, + {210, D<&IApplicationFunctions::GetOperationModeRefreshEvent>, "GetOperationModeRefreshEvent (guessed)"}, {220, nullptr, "Unknown220"}, // [20.0.0+] {300, nullptr, "CreateMovieWriter"}, // [19.0.0+] {310, nullptr, "Unknown310"}, // [20.0.0+] @@ -555,10 +556,18 @@ Result IApplicationFunctions::GetHealthWarningDisappearedSystemEvent( R_SUCCEED(); } -Result IApplicationFunctions::GetUnknownEvent210( +// The Switch 2 nn::oe::GetOperationMode caches the mode and only asks ICommonStateGetter again +// once this event is signaled, so it has to fire on every docked/handheld change. Switch 1 titles +// keep the event that never fires, as its use there is unknown. +Result IApplicationFunctions::GetOperationModeRefreshEvent( OutCopyHandle<Kernel::KReadableEvent> out_event) { LOG_DEBUG(Service_AM, "called"); - *out_event = m_applet->unknown_event.GetHandle(); + const auto* process = m_applet->process ? m_applet->process->GetHandle() : nullptr; + if (process != nullptr && process->IsSwitch2()) { + *out_event = m_applet->lifecycle_manager.GetOperationModeRefreshEvent().GetHandle(); + } else { + *out_event = m_applet->unknown_event.GetHandle(); + } R_SUCCEED(); } diff --git a/src/core/hle/service/am/service/application_functions.h b/src/core/hle/service/am/service/application_functions.h index 101ee0c970..44b5fadff0 100644 --- a/src/core/hle/service/am/service/application_functions.h +++ b/src/core/hle/service/am/service/application_functions.h @@ -82,7 +82,7 @@ private: Result GetNotificationStorageChannelEvent(OutCopyHandle<Kernel::KReadableEvent> out_event); Result GetHealthWarningDisappearedSystemEvent(OutCopyHandle<Kernel::KReadableEvent> out_event); Result CreateApplicationAndRequestToStart(u64 application_id); - Result GetUnknownEvent210(OutCopyHandle<Kernel::KReadableEvent> out_event); + Result GetOperationModeRefreshEvent(OutCopyHandle<Kernel::KReadableEvent> out_event); Result Unknown320(); Result Unknown321(); Result Unknown330(Out<u8> out); diff --git a/src/core/hle/service/am/service/library_applet_creator.cpp b/src/core/hle/service/am/service/library_applet_creator.cpp index 4ee92908b5..afdb609c00 100644 --- a/src/core/hle/service/am/service/library_applet_creator.cpp +++ b/src/core/hle/service/am/service/library_applet_creator.cpp @@ -4,6 +4,9 @@ // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include <mutex> +#include <unordered_set> + #include "common/settings.h" #include "core/hle/kernel/k_transfer_memory.h" #include "core/hle/service/am/applet_data_broker.h" @@ -99,6 +102,41 @@ AppletProgramId AppletIdToProgramId(AppletId applet_id) { } } +AppletId NormalizeAppletId(AppletId id) { + const u32 raw_id = static_cast<u32>(id); + static std::mutex mutex; + static std::unordered_set<u32> logged_raw_ids; + static std::unordered_set<u32> warned_raw_ids; + + bool log_info = false; + { + std::scoped_lock lk{mutex}; + log_info = logged_raw_ids.insert(raw_id).second; + } + if (log_info) { + LOG_INFO(Service_AM, "CreateLibraryApplet called with raw applet_id={:#x}", raw_id); + } + + if (AppletIdToProgramId(id) == static_cast<AppletProgramId>(0)) { + const auto low_byte = static_cast<AppletId>(raw_id & 0xFF); + if (AppletIdToProgramId(low_byte) != static_cast<AppletProgramId>(0)) { + bool log_warning = false; + { + std::scoped_lock lk{mutex}; + log_warning = warned_raw_ids.insert(raw_id).second; + } + if (log_warning) { + LOG_WARNING(Service_AM, + "Normalizing unknown raw applet_id={:#x} to low byte {:#x}", + raw_id, static_cast<u32>(low_byte)); + } + return low_byte; + } + } + + return id; +} + std::shared_ptr<ILibraryAppletAccessor> CreateGuestApplet(Core::System& system, WindowSystem& window_system, std::shared_ptr<Applet> caller_applet, @@ -184,6 +222,8 @@ Result ILibraryAppletCreator::CreateLibraryApplet( LOG_DEBUG(Service_AM, "called with applet_id={} applet_mode={}", applet_id, library_applet_mode); + applet_id = NormalizeAppletId(applet_id); + std::shared_ptr<ILibraryAppletAccessor> library_applet; if (ShouldCreateGuestApplet(applet_id)) { library_applet = @@ -210,6 +250,8 @@ Result ILibraryAppletCreator::CreateLibraryAppletEx( LOG_DEBUG(Service_AM, "called with applet_id={} applet_mode={} thread_id={}", applet_id, library_applet_mode, thread_id); + applet_id = NormalizeAppletId(applet_id); + std::shared_ptr<ILibraryAppletAccessor> library_applet; if (ShouldCreateGuestApplet(applet_id)) { library_applet = diff --git a/src/core/hle/service/audio/nx2_audio_user_client.h b/src/core/hle/service/audio/nx2_audio_user_client.h index 2d1a158c84..f053ad62af 100644 --- a/src/core/hle/service/audio/nx2_audio_user_client.h +++ b/src/core/hle/service/audio/nx2_audio_user_client.h @@ -14,14 +14,16 @@ namespace Service::Audio { // Switch 2 "aud:u" (nn::audio::CreateUserClientSessionByHipc). The SDK reaches the Switch 1 // audio interfaces through it: command 2 returns an object used exactly like // IAudioRendererManager (vtable slot 1 = GetWorkBufferSize with a Switch 1 parameter block), so -// it is backed by Eden's audren:u implementation. The nx2: name keeps unknown commands -// auto-stubbed and logged. +// it is backed by Eden's audren:u implementation. Command 1 is nn::audio::OpenAudioOut's +// manager, called like IAudioOutManager (slot 1 = OpenAudioOut), backed by audout:u. The nx2: name +// keeps unknown commands auto-stubbed and logged. class INx2AudioUserClient final : public ServiceFramework<INx2AudioUserClient> { public: explicit INx2AudioUserClient(Core::System& system_) : ServiceFramework{system_, "nx2:aud:u"} { // clang-format off static const FunctionInfo functions[] = { + {1, &INx2AudioUserClient::GetAudioOutManager, "GetAudioOutManager (guessed)"}, {2, &INx2AudioUserClient::GetAudioRendererManager, "GetAudioRendererManager (guessed)"}, }; // clang-format on @@ -29,6 +31,16 @@ public: } private: + void GetAudioOutManager(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const u32 arg = rp.Pop<u32>(); + LOG_WARNING(Service_Audio, "(STUBBED) Switch 2 aud:u 1 arg={}, returning IAudioOutManager", + arg); + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(ResultSuccess); + rb.PushIpcInterface<IAudioOutManager>(ctx, system); + } + void GetAudioRendererManager(HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; const u32 arg = rp.Pop<u32>(); diff --git a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp index c4119b2ab1..e66c72cd26 100644 --- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp +++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp @@ -69,6 +69,9 @@ NvResult nvhost_as_gpu::Ioctl1(DeviceFD fd, Ioctl command, std::span<const u8> i }, [this](u64 gpu_address, u64 cpu_address, u64 size) { Nx2BackSparse(gpu_address, cpu_address, size); + }, + [this](u64 gpu_address, u64 cpu_address, u64 size) { + Nx2SparseTile(gpu_address, cpu_address, size); }); } return result; @@ -243,6 +246,32 @@ void nvhost_as_gpu::Nx2BackSparse(u64 gpu_address, u64 cpu_address, u64 size) { session->asid, true); } +void nvhost_as_gpu::Nx2SparseTile(u64 gpu_address, u64 cpu_address, u64 size) { + std::scoped_lock lock(mutex); + auto it = nx2_sparse_ranges.upper_bound(gpu_address); + auto* session = container.GetSession(nx2_session); + if (it == nx2_sparse_ranges.begin() || session == nullptr) { + return; + } + --it; + const auto& [start, range] = *it; + const auto& [region_size, device_address] = range; + if (gpu_address + size > start + region_size) { + return; + } + // In GPU-thread order: command lists submitted before the request still see the old tile. + // Unmapping first keeps the SMMU's reverse map right and invalidates the caches over the tile, + // whose contents are now what the game's memory holds there. + const DAddr device = device_address + (gpu_address - start); + auto& smmu = system.Host1x().MemoryManager(); + system.GPU().RunInOrder([&smmu, device, cpu_address, size, asid = session->asid] { + smmu.Unmap(device, size); + if (cpu_address != 0) { + smmu.Map(device, cpu_address, size, asid, true); + } + }); +} + void nvhost_as_gpu::OnClose(DeviceFD fd) {} // Switch 2 NvRm (with characteristics flag 26) asks for the read-only syncpoint shim: a GPU page diff --git a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h index 69050bb677..39b63ea369 100644 --- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h +++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h @@ -246,6 +246,8 @@ private: // blocks get memory behind their device pages as the GPU writes them. void Nx2Sparse(u64 gpu_address, u64 size, bool create); void Nx2BackSparse(u64 gpu_address, u64 cpu_address, u64 size); + // A tile of a sparse region (nx2:gpu 216): its device pages show process memory, or nothing. + void Nx2SparseTile(u64 gpu_address, u64 cpu_address, u64 size); std::map<u64, std::pair<u64, DAddr>> nx2_sparse_ranges; ///< GPU address -> (size, device) }; diff --git a/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp index 5db984b53d..5998940afa 100644 --- a/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp +++ b/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp @@ -6,6 +6,7 @@ #include <array> #include <algorithm> +#include <cstdlib> #include <cstring> #include "common/assert.h" #include "common/logging.h" @@ -207,6 +208,16 @@ void ApplyNx2Characteristics(GC& gc) { // Without the flag NvRm fails that call and the texture stays at GPU address 0 (title 1's // movie and the passes rendered into compressible pools never showed). gc.flags |= 1ULL << 34; + // NVGPU_GPU_FLAGS_SUPPORT_REMAP: without it NvRm fails every remap (nvnMemoryPoolMapVirtual) + // before asking the server, so title 1 never maps or unmaps its sparse texture tiles (nx2:gpu + // 216) and streams its terrain into unmapped sparse memory. EDEN_NX2_SPARSE_REMAP=0 leaves it out. + static const bool remap = [] { + const char* value = std::getenv("EDEN_NX2_SPARSE_REMAP"); + return value == nullptr || value[0] != '0'; + }(); + if (remap) { + gc.flags |= 1ULL << 35; + } // The Switch 2 layout keeps nine s16 interface versions at 0x70..0x80 (like nvgpu's // *_ioctl_nr_last); its NvRm requires e.g. [0x78] >= 6 and enables features above 0xa, 0x1f diff --git a/src/core/hle/service/nvdrv/devices/nx2_stub_device.h b/src/core/hle/service/nvdrv/devices/nx2_stub_device.h index 989bf61682..869e17caf6 100644 --- a/src/core/hle/service/nvdrv/devices/nx2_stub_device.h +++ b/src/core/hle/service/nvdrv/devices/nx2_stub_device.h @@ -44,6 +44,9 @@ public: using SparseListener = std::function<void(u64 gpu_address, u64 size, bool create)>; /// A block of a sparse region gets memory [cpu_address, +size). using BackListener = std::function<void(u64 gpu_address, u64 cpu_address, u64 size)>; + /// A tile of a sparse region shows process memory [cpu_address, +size), or reads zero again + /// (cpu_address 0). + using TileListener = std::function<void(u64 gpu_address, u64 cpu_address, u64 size)>; static Nx2GpuMemoryRegistry& Get() { static Nx2GpuMemoryRegistry registry; @@ -120,9 +123,22 @@ public: } } + // nx2:gpu 216 (sparse map requests): the tile [gpu_address, +size) of a sparse region now + // shows the process memory at cpu_address, the game's own memory (sub 101), or with + // cpu_address 0 it is unmapped and reads zero. The GPU mapping stays as it is; only the + // region's device pages change, in GPU-thread order. + void MapSparseTile(u64 gpu_address, u64 cpu_address, u64 size) { + std::scoped_lock lk{mutex}; + for (auto& [owner, listener] : listeners) { + if (listener.tile) { + listener.tile(gpu_address, cpu_address, size); + } + } + } + void AddListener(const void* owner, Listener listener, UnmapListener unmap_listener, ReleaseListener release_listener = {}, SparseListener sparse_listener = {}, - BackListener back_listener = {}) { + BackListener back_listener = {}, TileListener tile_listener = {}) { std::scoped_lock lk{mutex}; for (const auto& m : mappings) { listener(m.gpu_address, m.cpu_address, m.size); @@ -135,7 +151,8 @@ public: listeners.insert_or_assign(owner, Listeners{std::move(listener), std::move(unmap_listener), std::move(release_listener), std::move(sparse_listener), - std::move(back_listener)}); + std::move(back_listener), + std::move(tile_listener)}); } void RemoveListener(const void* owner) { @@ -192,6 +209,7 @@ private: ReleaseListener release; SparseListener sparse; BackListener back; + TileListener tile; }; // Drops [gpu_address, +size) from the recorded mappings, keeping the parts outside it. diff --git a/src/core/hle/service/nvdrv/nvdrv.cpp b/src/core/hle/service/nvdrv/nvdrv.cpp index 2be24e3f57..12923496fb 100644 --- a/src/core/hle/service/nvdrv/nvdrv.cpp +++ b/src/core/hle/service/nvdrv/nvdrv.cpp @@ -132,6 +132,7 @@ Module::Module(Core::System& system) // (their table filled up, so the new game's GPU waits never completed) and the Switch 2 flags. Devices::Nx2GpuMemoryRegistry::Get().Reset(); Tegra::Nx2ClearSemaphoreRegions(); + Tegra::Nx2ClearSparseRegions(); Devices::g_nx2_driver = false; Tegra::g_nx2_ampere = false; Core::g_nx2_present_count = 0; diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h index 9d00b81368..628bdeda5b 100644 --- a/src/core/hle/service/nx2_gsv.h +++ b/src/core/hle/service/nx2_gsv.h @@ -315,6 +315,7 @@ public: {213, &INx2GpuSession::SubUnmapJob, "NormalUnmapGpuMemory4K"}, {214, &INx2GpuSession::SubCreateGpuOnlyRegion, "CreateSparseRegion"}, {215, &INx2GpuSession::SubDestroySparseRegion, "DestroySparseRegion"}, + {216, &INx2GpuSession::SubSparseMap, "ExecuteSparseMapRequests"}, }); if (LazySparseRegions()) { Tegra::Nx2SetSparseBacking( @@ -542,6 +543,9 @@ private: const u64 size = last + 1 - start; std::scoped_lock lk{m_region_mutex}; FreeReleasedRegions(); + if ((w[7] & SparseRegionFlag) == 0) { + m_gpu_only_ranges[start] = size; // kept out of 216, however it is backed + } if ((w[7] & SparseRegionFlag) != 0 && LazySparseRegions()) { // Title 1's terrain texture arrays and layer pools (170 MB-5.8 GB, mostly empty): // backed whole, they ran the pool dry and the later ones got no memory at all. @@ -579,8 +583,17 @@ private: first, last, w[1], w[2], w[7], w[8]); std::scoped_lock lk{m_region_mutex}; FreeReleasedRegions(); + if (m_gpu_only_ranges.erase(first) != 0 && last > first) { + DropGpuOnlyTiles(first, last - first + 1, false); + m_shared_regions.erase(first); + std::erase_if(m_shared_memory, [first](const auto& entry) { return entry.second == first; }); + } if (last > first) { - if (m_sparse_regions.erase(first) != 0) { + if (const auto region = m_sparse_regions.find(first); region != m_sparse_regions.end()) { + m_tile_bytes -= Core::Memory::YUZU_PAGESIZE * + static_cast<u64>(std::count(region->second.tile_pages.begin(), + region->second.tile_pages.end(), true)); + m_sparse_regions.erase(region); Tegra::Nx2SparseRegionDestroyed(first); Nvidia::Devices::Nx2GpuMemoryRegistry::Get().DestroySparse(first, last - first + 1); } else { @@ -609,6 +622,49 @@ private: rb.Push(ResultSuccess); } + // Sub 216 (nn::gpu::sdk::ExecuteSparseMapRequests, behind nvnMemoryPoolMapVirtual and NvRm's + // remap): an A buffer of SparseMapRequests and a B buffer that gets the kind applied to each; + // word 8-9 is 1, 10 the address space, 11-12 the request size (40), 13 a sync flag, 14 the job + // slot. The reply is the number of requests done. This is how the game commits and releases + // its sparse texture tiles: a map points a tile at its own memory (sub 101), an unmap makes it + // read zero again. A region the game maps tiles into needs no memory of its own. + void SubSparseMap(HLERequestContext& ctx) { + const auto* w = ctx.CommandBuffer(); + const u32 slot = w[14]; + const auto data = ctx.BufferDescriptorA().empty() ? std::span<const u8>{} : ctx.ReadBufferA(0); + const size_t count = data.size() / sizeof(SparseMapRequest); + std::vector<u8> kinds(count); + { + std::scoped_lock lk{m_region_mutex}; + FreeReleasedRegions(); + ++m_tile_calls; + for (size_t i = 0; i < count; ++i) { + SparseMapRequest request; + std::memcpy(&request, data.data() + i * sizeof(SparseMapRequest), sizeof(request)); + kinds[i] = request.kind; + ApplySparseMapRequest(request); + } + // The first request, then at most every 30 s. + const auto now = std::chrono::steady_clock::now(); + if (count != 0 && (m_tile_requests == count || + now - m_tile_log_time >= std::chrono::seconds{30})) { + m_tile_log_time = now; + LOG_INFO(Service, "nx2:gpu 216: {} calls, {} maps, {} unmaps; {} MiB of sparse tiles " + "and {} MiB in GPU-only regions mapped, {} maps into regions that " + "keep their own memory left out; {} MiB of sparse blocks backed, {} " + "MiB released", + m_tile_calls, m_tile_maps, m_tile_unmaps, m_tile_bytes >> 20, + m_gpu_only_tile_bytes >> 20, m_tile_kept, m_sparse_backed_bytes >> 20, + m_sparse_released_bytes >> 20); + } + } + if (!ctx.BufferDescriptorB().empty() && count != 0) { + ctx.WriteBufferB(kinds.data(), kinds.size(), 0); + } + SignalAfterGpu(slot); + TipcReplyWords(ctx, {static_cast<u32>(count), static_cast<u32>(u64{count} >> 32)}); + } + // Returns the pages of destroyed regions the GPU thread is done with to the kernel. void FreeReleasedRegions() { auto* const process = system.ApplicationProcess(); @@ -630,8 +686,17 @@ private: }); } - std::optional<u64> BackGpuOnlyRegion(u64 gpu_address, u64 size, bool log_failure = true, - bool clear = false) { + struct MappedBacking { + u64 address; + u64 size; + std::unique_ptr<Kernel::KPageGroup> pages; + }; + + // Allocates kernel memory and maps it in the alias code region. Touches no region state, so + // it needs no lock: the GPU back thread calls it without m_region_mutex, because mapping can + // wait on the guest page table lock and a guest thread holding that lock must not be stalled + // behind a host mutex the service thread is waiting for. + std::optional<MappedBacking> AllocateGpuOnlyBacking(u64 size, bool log_failure, bool clear) { using Core::Memory::YUZU_PAGESIZE; auto& kernel_core = system.Kernel(); auto* const process = system.ApplicationProcess(); @@ -667,18 +732,16 @@ private: // From where the last backing went (sparse regions back hundreds of blocks), then wrapping. const u64 slots = region_size / Alignment; for (u64 i = 0; i < slots; ++i) { - const u64 offset = ((m_alias_hint + i) % slots) * Alignment; + const u64 offset = ((m_alias_hint.load(std::memory_order_relaxed) + i) % slots) * Alignment; if (offset + size > region_size) { continue; } const Common::ProcessAddress address{region_start + offset}; if (R_SUCCEEDED(page_table.MapPageGroup(address, *pg, Kernel::KMemoryState::IoMemory, Kernel::KMemoryPermission::UserReadWrite))) { - m_alias_hint = (offset + Common::AlignUp(size, Alignment)) / Alignment; - m_gpu_only_regions[gpu_address] = {GetInteger(address), size, std::move(pg)}; - m_gpu_only_bytes += size; - m_gpu_only_peak = (std::max)(m_gpu_only_peak, m_gpu_only_bytes); - return GetInteger(address); + m_alias_hint.store((offset + Common::AlignUp(size, Alignment)) / Alignment, + std::memory_order_relaxed); + return MappedBacking{GetInteger(address), size, std::move(pg)}; } } LOG_ERROR(Service, "nx2:gpu 214 no room for {:#x} bytes in the alias code region " @@ -687,32 +750,93 @@ private: return std::nullopt; } + // Records a mapped backing for the region at gpu_address. Needs m_region_mutex. + void RegisterGpuOnlyBacking(u64 gpu_address, MappedBacking&& backing) { + m_gpu_only_regions[gpu_address] = {backing.address, backing.size, std::move(backing.pages)}; + m_gpu_only_bytes += backing.size; + m_gpu_only_peak = (std::max)(m_gpu_only_peak, m_gpu_only_bytes); + } + + // Needs m_region_mutex. + std::optional<u64> BackGpuOnlyRegion(u64 gpu_address, u64 size) { + auto backing = AllocateGpuOnlyBacking(size, true, false); + if (!backing) { + return std::nullopt; + } + const u64 address = backing->address; + RegisterGpuOnlyBacking(gpu_address, std::move(*backing)); + return address; + } + // Runs before a GPU-side write that may land in a sparse region (Tegra::Nx2BackSparseWrite, // usually on the GPU thread): the blocks the write touches get memory, cleared, behind the // region's device pages. The GPU mapping stays as it is, so the images over the region keep // their storage and only see the write. void BackSparseWrite(u64 gpu_address, u64 size) { - std::scoped_lock lk{m_region_mutex}; + struct Pending { + u64 region; + u64 block; + }; + std::vector<Pending> todo; const u64 end = gpu_address + size; - auto it = m_sparse_regions.upper_bound(gpu_address); - if (it != m_sparse_regions.begin()) { - --it; - } - for (; it != m_sparse_regions.end() && it->first < end; ++it) { - auto& [start, region] = *it; - if (start + region.size <= gpu_address) { - continue; + // Calls fn(region_start, region, first_block, last_block) for each region the write touches. + auto for_regions = [&](auto&& fn) { + auto it = m_sparse_regions.upper_bound(gpu_address); + if (it != m_sparse_regions.begin()) { + --it; } - const u64 first_block = ((std::max)(gpu_address, start) - start) / SparseBlockSize; - const u64 last_block = ((std::min)(end, start + region.size) - 1 - start) / SparseBlockSize; - for (u64 block = first_block; block <= last_block; ++block) { - if (region.blocks[block] != BlockEmpty) { + for (; it != m_sparse_regions.end() && it->first < end; ++it) { + auto& [start, region] = *it; + if (start + region.size <= gpu_address) { continue; } - const u64 block_address = start + block * SparseBlockSize; - const u64 block_size = (std::min)(SparseBlockSize, region.size - block * SparseBlockSize); - const auto address = BackGpuOnlyRegion(block_address, block_size, false, true); - if (!address) { + fn(start, region, ((std::max)(gpu_address, start) - start) / SparseBlockSize, + ((std::min)(end, start + region.size) - 1 - start) / SparseBlockSize); + } + }; + bool waits_for_other = false; + { + std::unique_lock lk{m_region_mutex}; + for_regions([&](u64 start, SparseRegion& region, u64 first, u64 last) { + for (u64 block = first; block <= last; ++block) { + if (region.blocks[block] == BlockEmpty && region.tiles) { + // The write lands in the game's own memory (a tile it mapped) or nowhere; + // the texture cache only learns that the block has contents now. + region.blocks[block] = BlockWritten; + Tegra::Nx2SparseBlockWritten(start, block); + } else if (region.blocks[block] == BlockEmpty) { + region.blocks[block] = BlockPending; + todo.push_back({start, block}); + } else if (region.blocks[block] == BlockPending) { + waits_for_other = true; + } + } + }); + } + // Allocating and mapping happens outside the lock (see AllocateGpuOnlyBacking). + for (const auto& [start, block] : todo) { + u64 block_size = SparseBlockSize; + { + std::scoped_lock lk{m_region_mutex}; + const auto it = m_sparse_regions.find(start); + if (it != m_sparse_regions.end()) { + block_size = (std::min)(SparseBlockSize, it->second.size - block * SparseBlockSize); + } + } + auto backing = AllocateGpuOnlyBacking(block_size, false, true); + bool stale = false; + { + std::scoped_lock lk{m_region_mutex}; + const auto it = m_sparse_regions.find(start); + if (it == m_sparse_regions.end() || block >= it->second.blocks.size()) { + stale = true; // the region was destroyed while its block was being backed + } else if (it->second.tiles) { + // The game started mapping its tiles there meanwhile (216): no block. + stale = true; + it->second.blocks[block] = BlockWritten; + Tegra::Nx2SparseBlockWritten(start, block); + } else if (!backing) { + auto& region = it->second; region.blocks[block] = BlockFailed; // out of memory: not again for every write if (!region.warned) { region.warned = true; @@ -720,21 +844,47 @@ private: "to its unbacked blocks are dropped", start); } - continue; + } else { + const u64 block_address = start + block * SparseBlockSize; + const u64 address = backing->address; + it->second.blocks[block] = BlockBacked; + RegisterGpuOnlyBacking(block_address, std::move(*backing)); + Nvidia::Devices::Nx2GpuMemoryRegistry::Get().BackSparse(block_address, address, + block_size); + Tegra::Nx2SparseBlockWritten(start, block); + m_sparse_backed_bytes += block_size; + if (++m_sparse_blocks == 1 || (m_sparse_blocks % 64) == 0) { + LOG_INFO(Service, "nx2:gpu {} sparse blocks backed ({} MiB); {} MiB of GPU-only " + "memory in use, peak {} MiB", + m_sparse_blocks, m_sparse_backed_bytes >> 20, m_gpu_only_bytes >> 20, + m_gpu_only_peak >> 20); + } } - region.blocks[block] = BlockBacked; - Nvidia::Devices::Nx2GpuMemoryRegistry::Get().BackSparse(block_address, *address, - block_size); - Tegra::Nx2SparseBlockWritten(start, block); - m_sparse_backed_bytes += block_size; - if (++m_sparse_blocks == 1 || (m_sparse_blocks % 64) == 0) { - LOG_INFO(Service, "nx2:gpu {} sparse blocks backed ({} MiB); {} MiB of GPU-only " - "memory in use, peak {} MiB", - m_sparse_blocks, m_sparse_backed_bytes >> 20, m_gpu_only_bytes >> 20, - m_gpu_only_peak >> 20); + } + m_region_cv.notify_all(); + if (stale && backing) { + // Never handed to the GPU: unmap and free right away. + if (auto* const process = system.ApplicationProcess(); process != nullptr) { + process->GetPageTable().UnmapPageGroup(Common::ProcessAddress{backing->address}, + *backing->pages, + Kernel::KMemoryState::IoMemory); } + backing->pages->Close(system.Kernel()); } } + if (waits_for_other) { + // Another thread is backing a block this write needs: wait until it is done. + std::unique_lock lk{m_region_mutex}; + m_region_cv.wait(lk, [&] { + bool pending = false; + for_regions([&](u64, SparseRegion& region, u64 first, u64 last) { + for (u64 block = first; block <= last; ++block) { + pending |= region.blocks[block] == BlockPending; + } + }); + return !pending; + }); + } } // EDEN_NX2_SPARSE_LAZY=0 backs sparse regions whole, as before. @@ -751,19 +901,314 @@ private: static constexpr u8 BlockEmpty = 0; static constexpr u8 BlockBacked = 1; static constexpr u8 BlockFailed = 2; + static constexpr u8 BlockPending = 3; ///< Being allocated and mapped by BackSparseWrite + static constexpr u8 BlockWritten = 4; ///< Written in a region the game maps tiles into struct SparseRegion { u64 size; - std::vector<u8> blocks; ///< BlockEmpty / BlockBacked / BlockFailed per SparseBlockSize + std::vector<u8> blocks; ///< BlockEmpty / Backed / Failed / Pending / Written per block bool warned{}; ///< Logged that the pool ran out for its blocks + bool tiles{}; ///< The game maps its own memory into it (216): no blocks + std::vector<bool> tile_pages; ///< Pages a tile maps, once `tiles` }; std::map<u64, SparseRegion> m_sparse_regions; ///< By GPU address u64 m_sparse_blocks{}; ///< Sparse blocks backed so far u64 m_sparse_backed_bytes{}; ///< Sparse region memory given out so far + u64 m_sparse_released_bytes{}; ///< Blocks given back once the game mapped tiles there + + // nn::gpu::sdk::SparseMapRequest (216), filled from NvRm's nvgpu_as_remap_op. + struct SparseMapRequest { + u64 gpu_address; + u64 size; + u8 no_write; + u8 cacheable; + u8 unknown_12; + u8 kind; ///< Wanted: the compressible kind if there is one + u8 fallback_kind; ///< The incompressible kind + u8 unknown_15; ///< Remap op flag bit 15 + u16 padding_16; + u32 memory_handle; ///< Sub 101's handle; 0 unmaps the range + u32 padding_1c; + u64 offset; ///< Into that memory + }; + static_assert(sizeof(SparseMapRequest) == 40); + u64 m_tile_calls{}; + u64 m_tile_requests{}; + std::chrono::steady_clock::time_point m_tile_log_time{}; + u64 m_tile_maps{}; + u64 m_tile_unmaps{}; + u64 m_tile_kept{}; ///< Maps into GPU-only regions that keep their own memory, left out + u64 m_tile_bytes{}; ///< Sparse region pages a tile maps right now + + void ApplySparseMapRequest(const SparseMapRequest& request) { + ++m_tile_requests; + if (request.size == 0) { + return; + } + const u64 gpu_address = request.gpu_address; + const u64 size = request.size; + auto& registry = Nvidia::Devices::Nx2GpuMemoryRegistry::Get(); + auto it = m_sparse_regions.upper_bound(gpu_address); + SparseRegion* region = nullptr; + u64 start = 0; + if (it != m_sparse_regions.begin()) { + --it; + if (gpu_address + size <= it->first + it->second.size) { + start = it->first; + region = &it->second; + } + } + if (const auto gpu_only = GpuOnlyRegionOf(gpu_address, size)) { + ApplyGpuOnlyRequest(*gpu_only, request); + return; + } + if (request.memory_handle == 0) { + ++m_tile_unmaps; + LOG_DEBUG(Service, "nx2:gpu 216 unmap [{:#x}, +{:#x}]", gpu_address, size); + if (region != nullptr) { + Tegra::Nx2SparseTilesMapped(start, gpu_address - start, size, false); + registry.MapSparseTile(gpu_address, 0, size); + MarkTiles(*region, gpu_address - start, size, false); + } else { + registry.Unmap(gpu_address, size); + } + return; + } + ++m_tile_maps; + const auto memory = m_memory.find(request.memory_handle); + if (memory == m_memory.end() || request.offset > memory->second.size || size > memory->second.size - request.offset) { + LOG_WARNING(Service, "nx2:gpu 216 map [{:#x}, +{:#x}] of unknown memory {} offset {:#x}", + gpu_address, size, request.memory_handle, request.offset); + return; + } + const u64 cpu_address = memory->second.address + request.offset; + LOG_DEBUG(Service, "nx2:gpu 216 map [{:#x}, +{:#x}] -> memory {} offset {:#x} kind {:#x}", + gpu_address, size, request.memory_handle, request.offset, request.kind); + if (region == nullptr) { + registry.MapAt(gpu_address, cpu_address, size); + return; + } + if (!region->tiles) { + StartTiles(start, *region); + } + // The game's memory already holds what the tile shows (title 1 fills nine world-sized + // masks, R8/RG16/R16 at 8192-16384 texels, in its own memory before mapping them): the + // blocks count as written, so the texture cache gives the images over them memory and + // loads them. Left unwritten, those images read zero and the static world was unlit. + const u64 first_block = (gpu_address - start) / SparseBlockSize; + const u64 last_block = (gpu_address + size - 1 - start) / SparseBlockSize; + for (u64 block = first_block; block <= last_block; ++block) { + if (region->blocks[block] == BlockEmpty) { + region->blocks[block] = BlockWritten; + } + Tegra::Nx2SparseBlockWritten(start, block); + } + Tegra::Nx2SparseTilesMapped(start, gpu_address - start, size, true); + registry.MapSparseTile(gpu_address, cpu_address, size); + MarkTiles(*region, gpu_address - start, size, true); + } + + // Start of the region 214 made without the sparse flag that holds the range (by its flags, + // not by how it is backed). + std::optional<u64> GpuOnlyRegionOf(u64 gpu_address, u64 size) const { + auto it = m_gpu_only_ranges.upper_bound(gpu_address); + if (it == m_gpu_only_ranges.begin()) { + return std::nullopt; + } + --it; + if (gpu_address + size > it->first + it->second) { + return std::nullopt; + } + return it->first; + } + + // 216 into a GPU-only region: render targets, and with remap support also some texture arrays + // (title 1: 5.8 GB and 1.4 GB). The game backs those with its own memory too, which it fills + // through its own address, so a map points the range at that memory the way 211 does: both + // addresses reach the same device pages and the texture cache sees the writes. (Re-pointing the + // region's own device pages instead, as for sparse tiles, left the arrays stale: black terrain.) + // Left alone: + // - Regions whose memory the game shares with another GPU-only region (render targets): the + // texture cache would flush and re-upload such overlapping render targets every frame. All of + // a group keep the memory 214 gave them. + // - Regions 214 backs as they are written: the texture cache reads an image's memory in full + // when it uploads it, which makes the game's pages behind it resident on the host, while + // unwritten blocks read as zero for free (title 1: ~1 GB less resident at 150 s). + void ApplyGpuOnlyRequest(u64 region, const SparseMapRequest& request) { + auto& registry = Nvidia::Devices::Nx2GpuMemoryRegistry::Get(); + const u64 gpu_address = request.gpu_address; + const u64 size = request.size; + if (request.memory_handle == 0) { + ++m_tile_unmaps; + DropGpuOnlyTiles(gpu_address, size, true); + return; + } + ++m_tile_maps; + if (m_sparse_regions.contains(region)) { + ++m_tile_kept; + return; + } + if (m_shared_regions.contains(region)) { + m_shared_memory[{request.memory_handle, request.offset}] = region; + ++m_tile_kept; + return; + } + const auto memory = m_memory.find(request.memory_handle); + if (memory == m_memory.end() || request.offset > memory->second.size || size > memory->second.size - request.offset) { + LOG_WARNING(Service, "nx2:gpu 216 map [{:#x}, +{:#x}] of unknown memory {} offset {:#x}", + gpu_address, size, request.memory_handle, request.offset); + return; + } + if (const auto other = RegionSharing(request.memory_handle, request.offset, size, region)) { + ++m_tile_kept; + for (const u64 shared : {region, *other}) { + if (m_shared_regions.insert(shared).second) { + const auto range = m_gpu_only_ranges.find(shared); + if (range != m_gpu_only_ranges.end()) { + DropGpuOnlyTiles(shared, range->second, true); + } + LOG_INFO(Service, "nx2:gpu GPU-only region at {:#x} shares memory with another: " + "it keeps its own", + shared); + } + } + return; + } + DropGpuOnlyTiles(gpu_address, size, false); + registry.MapAt(gpu_address, memory->second.address + request.offset, size); + m_gpu_only_tiles[gpu_address] = {size, region, request.memory_handle, request.offset}; + m_gpu_only_memory[{request.memory_handle, request.offset}] = {request.offset + size, region}; + m_gpu_only_tile_max = (std::max)(m_gpu_only_tile_max, size); + m_gpu_only_tile_bytes += size; + } + + // Another GPU-only region whose tiles hold any of [offset, +size) of the memory, if one does. + std::optional<u64> RegionSharing(u32 handle, u64 offset, u64 size, u64 region) const { + auto it = m_gpu_only_memory.lower_bound({handle, offset + size}); + while (it != m_gpu_only_memory.begin()) { + --it; + const auto& [key, value] = *it; + const auto& [end, owner] = value; + if (key.first != handle || key.second + m_gpu_only_tile_max <= offset) { + break; + } + if (end > offset && owner != region) { + return owner; + } + } + // Regions that already keep their own memory record no tiles: compare with their requests. + if (const auto kept = m_shared_memory.find({handle, offset}); kept != m_shared_memory.end() && + kept->second != region) { + return kept->second; + } + return std::nullopt; + } + + // The memory 214 gave a GPU-only region at [gpu_address, +size), when it was backed whole. + std::optional<u64> OwnBackingOf(u64 gpu_address, u64 size) const { + auto it = m_gpu_only_regions.upper_bound(gpu_address); + if (it == m_gpu_only_regions.begin()) { + return std::nullopt; + } + --it; + if (gpu_address + size > it->first + it->second.size) { + return std::nullopt; + } + return it->second.cpu_address + (gpu_address - it->first); + } + + // Forgets the game's tiles in [gpu_address, +size) of GPU-only regions; with `restore` (an + // unmap) those ranges show the region's own memory again, or nothing where it has none. + void DropGpuOnlyTiles(u64 gpu_address, u64 size, bool restore) { + auto& registry = Nvidia::Devices::Nx2GpuMemoryRegistry::Get(); + auto it = m_gpu_only_tiles.upper_bound(gpu_address); + if (it != m_gpu_only_tiles.begin()) { + const auto previous = std::prev(it); + if (previous->first + previous->second.size > gpu_address) { + it = previous; + } + } + while (it != m_gpu_only_tiles.end() && it->first < gpu_address + size) { + const auto& [tile_address, tile] = *it; + if (restore) { + if (const auto backing = OwnBackingOf(tile_address, tile.size)) { + registry.MapAt(tile_address, *backing, tile.size); + } else { + registry.Unmap(tile_address, tile.size); + } + } + if (const auto owner = m_gpu_only_memory.find({tile.memory_handle, tile.offset}); + owner != m_gpu_only_memory.end() && owner->second.second == tile.region) { + if (m_shared_regions.contains(tile.region)) { + m_shared_memory[owner->first] = tile.region; + } + m_gpu_only_memory.erase(owner); + } + m_gpu_only_tile_bytes -= tile.size; + it = m_gpu_only_tiles.erase(it); + } + } + + // The game maps tiles into this region itself (216): writes outside its tiles are dropped, as + // on the console, so it gets no more blocks, and the blocks it got before (written while no + // tile was mapped, which the console would have dropped) go back. + void StartTiles(u64 start, SparseRegion& region) { + using Core::Memory::YUZU_PAGESIZE; + region.tiles = true; + region.tile_pages.assign(Common::DivCeil(region.size, YUZU_PAGESIZE), false); + std::shared_ptr<std::atomic<bool>> done; + u64 released = 0; + for (u64 block = 0; block < region.blocks.size(); ++block) { + if (region.blocks[block] != BlockBacked) { + continue; + } + region.blocks[block] = BlockWritten; + const u64 block_address = start + block * SparseBlockSize; + const auto backing = m_gpu_only_regions.find(block_address); + if (backing == m_gpu_only_regions.end()) { + continue; + } + Nvidia::Devices::Nx2GpuMemoryRegistry::Get().MapSparseTile(block_address, 0, + backing->second.size); + if (!done) { + done = std::make_shared<std::atomic<bool>>(false); + system.GPU().RunInOrder([done] { done->store(true, std::memory_order_release); }); + } + released += backing->second.size; + m_released_regions.push_back({std::move(backing->second), done}); + m_gpu_only_regions.erase(backing); + } + m_sparse_released_bytes += released; + LOG_INFO(Service, "nx2:gpu sparse region at {:#x}: the game maps its tiles, {} MiB of blocks " + "released", + start, released >> 20); + } + + // Counts the bytes of tiles mapped, page by page. + void MarkTiles(SparseRegion& region, u64 offset, u64 size, bool mapped) { + using Core::Memory::YUZU_PAGESIZE; + if (!region.tiles) { + return; + } + const u64 last = (std::min)(Common::DivCeil(offset + size, YUZU_PAGESIZE), + static_cast<u64>(region.tile_pages.size())); + for (u64 page = offset / YUZU_PAGESIZE; page < last; ++page) { + if (region.tile_pages[page] != mapped) { + region.tile_pages[page] = mapped; + if (mapped) { + m_tile_bytes += YUZU_PAGESIZE; + } else { + m_tile_bytes -= YUZU_PAGESIZE; + } + } + } + } + u64 m_gpu_only_bytes{}; ///< Backing of GPU-only regions and sparse blocks right now u64 m_gpu_only_peak{}; - u64 m_alias_hint{}; ///< 2 MiB slot after the last backing in the alias code region + std::atomic<u64> m_alias_hint{}; ///< 2 MiB slot after the last backing in the alias code region /// Regions are created and destroyed on the service thread and backed on the GPU thread. std::recursive_mutex m_region_mutex; + std::condition_variable_any m_region_cv; ///< Signalled when a pending sparse block resolves struct GpuOnlyRegion { u64 cpu_address; @@ -775,6 +1220,21 @@ private: std::shared_ptr<std::atomic<bool>> done; ///< Set by the GPU thread past the region's last use }; std::map<u64, GpuOnlyRegion> m_gpu_only_regions; ///< By GPU address + std::map<u64, u64> m_gpu_only_ranges; ///< 214 regions without the sparse flag: GPU address -> size + struct GpuOnlyTile { + u64 size; + u64 region; ///< Start of its GPU-only region + u32 memory_handle; + u64 offset; + }; + std::map<u64, GpuOnlyTile> m_gpu_only_tiles; ///< The game's memory 216 put in GPU-only regions + /// (memory handle, offset) -> (end offset, region) of those tiles, to find shared memory + std::map<std::pair<u32, u64>, std::pair<u64, u64>> m_gpu_only_memory; + /// Memory mapped into regions that keep their own (handle, offset) -> region + std::map<std::pair<u32, u64>, u64> m_shared_memory; + std::set<u64> m_shared_regions; ///< GPU-only regions sharing memory: they keep their own + u64 m_gpu_only_tile_max{}; ///< Largest such tile, bounds the search + u64 m_gpu_only_tile_bytes{}; ///< GPU-only region memory the game maps right now std::vector<ReleasedRegion> m_released_regions; struct GpuMemory { @@ -1150,18 +1610,18 @@ private: } u32 crop_width = layout.width; u32 crop_height = layout.height; - // Mario Kart World draws into the top-left corner of its present images: 2560x1440 docked - // (in 3840x2160 images), 1920x1080 handheld. It keeps its docked images after a switch to - // handheld, and showing all of them put the frame in a corner with stale docked frames - // around it. - if (system.GetApplicationProcessProgramID() == 0x0400C3F00006E000ULL) { - if (!Settings::IsDockedMode()) { - crop_width = (std::min)(crop_width, 1920U); - crop_height = (std::min)(crop_height, 1080U); - } else if (layout.width == 3840 && layout.height == 2160) { - crop_width = 2560; - crop_height = 1440; - } + // Handheld titles keep their docked present images (up to 3840x2160) and draw the frame + // into the top-left 1920x1080, the handheld panel's resolution; the console's display + // scans out only that region. Showing whole images put the frame in a corner inside black + // margins that scaled with the window (seen on Mario Kart World and Splatoon Raiders). + if (!Settings::IsDockedMode()) { + crop_width = (std::min)(crop_width, 1920U); + crop_height = (std::min)(crop_height, 1080U); + } else if (system.GetApplicationProcessProgramID() == 0x0400C3F00006E000ULL && + layout.width == 3840 && layout.height == 2160) { + // Mario Kart World docked draws 2560x1440 into the top-left of its 4K images. + crop_width = 2560; + crop_height = 1440; } static const u32 env_crop_w = [] { const char* v = std::getenv("EDEN_NX2_CROP_WIDTH"); diff --git a/src/core/hle/service/nx2_streamplay.h b/src/core/hle/service/nx2_streamplay.h new file mode 100644 index 0000000000..4dc839d04b --- /dev/null +++ b/src/core/hle/service/nx2_streamplay.h @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "core/hle/kernel/k_event.h" +#include "core/hle/service/ipc_helpers.h" +#include "core/hle/service/kernel_helpers.h" +#include "core/hle/service/service.h" + +namespace Service { + +// User service returned by stpl:u 0 (nn::streamplay::detail::GetUserService). The SDK attaches a +// system event to the handle command 300 returns right after opening it, so that one needs a real +// event; everything else falls through to the nx2 auto stub (zero sizes, nothing streaming). +class INx2StreamPlayUserService final : public ServiceFramework<INx2StreamPlayUserService> { +public: + explicit INx2StreamPlayUserService(Core::System& system_) + : ServiceFramework{system_, "nx2:stpl:u::0"}, service_context{system_, "stpl:u"} { + static const FunctionInfo functions[] = { + {300, &INx2StreamPlayUserService::GetEvent, "GetEvent (guessed)"}, + }; + RegisterHandlers(functions); + event = service_context.CreateEvent("INx2StreamPlayUserService:Event"); + } + + ~INx2StreamPlayUserService() override { + service_context.CloseEvent(event); + } + +private: + void GetEvent(HLERequestContext& ctx) { + LOG_WARNING(Service, "(STUBBED) stpl:u::0 {}, returning an event", ctx.GetCommand()); + IPC::ResponseBuilder rb{ctx, 2, 1}; + rb.Push(ResultSuccess); + rb.PushCopyObjects(ctx, event->GetReadableEvent()); + } + + KernelHelpers::ServiceContext service_context; + Kernel::KEvent* event; +}; + +// Switch 2 stream play (GameShare), stpl:u. Command 0 (u32 0x12) opens the user service the SDK +// keeps for the process; without an object the first nn::streamplay call jumps through null. +class INx2StreamPlayService final : public ServiceFramework<INx2StreamPlayService> { +public: + explicit INx2StreamPlayService(Core::System& system_) + : ServiceFramework{system_, "nx2:stpl:u"} { + static const FunctionInfo functions[] = { + {0, &INx2StreamPlayService::GetUserService, "GetUserService (guessed)"}, + }; + RegisterHandlers(functions); + } + +private: + void GetUserService(HLERequestContext& ctx) { + LOG_WARNING(Service, "(STUBBED) stpl:u 0 [8]={:#x}, returning a user service", + ctx.CommandBuffer()[8]); + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(ResultSuccess); + rb.PushIpcInterface<INx2StreamPlayUserService>(ctx, system); + } +}; + +} // namespace Service diff --git a/src/core/hle/service/sm/sm.cpp b/src/core/hle/service/sm/sm.cpp index 60f0dd89cb..406d6c9028 100644 --- a/src/core/hle/service/sm/sm.cpp +++ b/src/core/hle/service/sm/sm.cpp @@ -23,6 +23,7 @@ #include "core/hle/service/cmif_types.h" #include "core/hle/service/hle_ipc.h" #include "core/hle/service/ipc_helpers.h" +#include "core/hle/service/nx2_streamplay.h" #include "core/hle/service/nx2_stub_interface.h" #include "core/hle/service/server_manager.h" #include "core/hle/service/sm/sm.h" @@ -194,8 +195,12 @@ Kernel::KClientSession* CreateStubSession(Core::System& system, HLERequestContex auto manager = std::make_shared<SessionRequestManager>(kernel, ctx.GetManager()->GetServerManager()); - manager->SetSessionHandler(std::make_shared<INx2StubInterface>( - system, INx2StubInterface::InternName(fmt::format("{}{}", Nx2StubPrefix, name)))); + if (name == "stpl:u") { + manager->SetSessionHandler(std::make_shared<INx2StreamPlayService>(system)); + } else { + manager->SetSessionHandler(std::make_shared<INx2StubInterface>( + system, INx2StubInterface::InternName(fmt::format("{}{}", Nx2StubPrefix, name)))); + } ctx.GetManager()->GetServerManager().RegisterSession(&session->GetServerSession(), manager); return &session->GetClientSession(); } diff --git a/src/core/loader/deconstructed_rom_directory.cpp b/src/core/loader/deconstructed_rom_directory.cpp index bff7e76efd..cda1645548 100644 --- a/src/core/loader/deconstructed_rom_directory.cpp +++ b/src/core/loader/deconstructed_rom_directory.cpp @@ -5,6 +5,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include <algorithm> +#include <cctype> #include <cstring> #include <string_view> #include "common/logging.h" @@ -130,6 +131,38 @@ std::string RomFSProductVersion(const FileSys::VirtualDir& romfs_dir) { return fmt::format("{}.{}.{}", best[0], best[1], best[2]); } +// Loose ExeFS dumps carry no control data, but icon/nacp sidecars can be dropped next to the +// ExeFS or in the game folder above it (<game>/exefs/main, <game>/base/exefs/main and +// <game>/updates/<title id>/exefs/main layouts). +std::vector<FileSys::VirtualDir> FindSidecarDirs(const FileSys::VirtualDir& exefs_dir) { + const auto equals_ignore_case = [](const std::string& a, std::string_view b) { + return a.size() == b.size() && + std::equal(a.begin(), a.end(), b.begin(), [](char x, char y) { + return std::tolower(static_cast<unsigned char>(x)) == + std::tolower(static_cast<unsigned char>(y)); + }); + }; + + std::vector<FileSys::VirtualDir> dirs{exefs_dir}; + for (auto dir = exefs_dir; dirs.size() < 4;) { + const auto parent = dir->GetParentDirectory(); + if (parent == nullptr) { + break; + } + const auto& name = parent->GetName(); + dirs.push_back(parent); + const bool storage_level = + equals_ignore_case(name, "base") || equals_ignore_case(name, "update") || + equals_ignore_case(name, "updates") || + (!name.empty() && name.find_first_not_of("0123456789abcdefABCDEF") == std::string::npos); + if (!storage_level) { + break; + } + dir = parent; + } + return dirs; +} + } // namespace AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys::VirtualFile file_, bool override_update_) @@ -137,6 +170,7 @@ AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys , override_update(override_update_) { const auto file_dir = file->GetContainingDirectory(); + const auto sidecar_dirs = FindSidecarDirs(file_dir); // Title ID const auto npdm = file_dir->GetFile("main.npdm"); @@ -148,8 +182,13 @@ AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys // Icon FileSys::VirtualFile icon_file = nullptr; - for (const auto& language : FileSys::LANGUAGE_NAMES) { - icon_file = file_dir->GetFile("icon_" + std::string(language) + ".dat"); + for (const auto& sidecar_dir : sidecar_dirs) { + for (const auto& language : FileSys::LANGUAGE_NAMES) { + icon_file = sidecar_dir->GetFile("icon_" + std::string(language) + ".dat"); + if (icon_file != nullptr) { + break; + } + } if (icon_file != nullptr) { icon_data = icon_file->ReadAllBytes(); break; @@ -158,31 +197,35 @@ AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys if (icon_data.empty()) { // Any png, jpeg, or bmp file - const auto& files = file_dir->GetFiles(); - const auto icon_iter = - std::find_if(files.begin(), files.end(), [](const FileSys::VirtualFile& f) { - return f->GetExtension() == "png" || f->GetExtension() == "jpg" || - f->GetExtension() == "bmp" || f->GetExtension() == "jpeg"; - }); - if (icon_iter != files.end()) - icon_data = (*icon_iter)->ReadAllBytes(); - } - - // Metadata: control.nacp (or any .nacp) next to main, else a control.nacp in the folders above - // it (dumps keep the title's or the update's control data at their root). - FileSys::VirtualFile nacp_file = file_dir->GetFile("control.nacp"); - if (nacp_file == nullptr) { - const auto& files = file_dir->GetFiles(); + for (const auto& sidecar_dir : sidecar_dirs) { + const auto& files = sidecar_dir->GetFiles(); + const auto icon_iter = + std::find_if(files.begin(), files.end(), [](const FileSys::VirtualFile& f) { + return f->GetExtension() == "png" || f->GetExtension() == "jpg" || + f->GetExtension() == "bmp" || f->GetExtension() == "jpeg"; + }); + if (icon_iter != files.end()) { + icon_data = (*icon_iter)->ReadAllBytes(); + break; + } + } + } + + // Metadata + FileSys::VirtualFile nacp_file = nullptr; + for (const auto& sidecar_dir : sidecar_dirs) { + nacp_file = sidecar_dir->GetFile("control.nacp"); + if (nacp_file != nullptr) { + break; + } + const auto& files = sidecar_dir->GetFiles(); const auto nacp_iter = std::find_if(files.begin(), files.end(), [](const FileSys::VirtualFile& f) { return f->GetExtension() == "nacp"; }); - if (nacp_iter != files.end()) + if (nacp_iter != files.end()) { nacp_file = *nacp_iter; - } - auto parent = file_dir->GetParentDirectory(); - for (int level = 0; level < 3 && nacp_file == nullptr && parent != nullptr; ++level) { - nacp_file = parent->GetFile("control.nacp"); - parent = parent->GetParentDirectory(); + break; + } } if (nacp_file != nullptr) { diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 6e0ab0ee6d..cfc39916db 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -8,10 +8,13 @@ #include <atomic> #include <algorithm> #include <cstring> +#include <cstdlib> #include <mutex> +#include <set> #include <span> #include <thread> #include <type_traits> +#include <utility> #include <vector> #include "common/assert.h" @@ -468,6 +471,9 @@ struct Memory::Impl { perm |= Common::MemoryPermission::Write; } host_buffer->Protect(vaddr, size, perm); + if (cached && perm != Common::MemoryPermission{}) { + ProtectArmedPages(vaddr, size); + } } // Iterate over a contiguous CPU address space, which corresponds to the specified GPU @@ -528,6 +534,84 @@ struct Memory::Impl { } } + /// Read traps replace reactive flushing's blanket read protection of every cached page: only + /// pages the GPU writes are protected, and each one only until its first read + /// (EDEN_NX2_READ_TRAPS=0 turns them off). Under NCE every fault is taken as a write, so they + /// are not used there. + bool ReadTrapsEnabled() const { + static const bool disabled = [] { + const char* value = std::getenv("EDEN_NX2_READ_TRAPS"); + return value != nullptr && value[0] == '0'; + }(); + return !disabled && current_page_table->fastmem_arena && + !Settings::values.use_reactive_flushing.GetValue() && + Settings::values.cpu_backend.GetValue() != Settings::CpuBackend::Nce; + } + + void RasterizerArmReadTraps(u64 vaddr, u64 size) { + if (vaddr == 0 || size == 0 || !AddressSpaceContains(*current_page_table, vaddr, size) || + !ReadTrapsEnabled()) { + return; + } + const u64 page_end = ((vaddr + size - 1) >> YUZU_PAGEBITS) + 1; + std::scoped_lock lk{read_trap_mutex}; + u64 run_begin = 0; + u64 run_pages = 0; + const auto protect_run = [&] { + if (run_pages > 0) { + // Reads that hit a cached read area must not skip the flush of a new trap. + read_trap_generation.fetch_add(1, std::memory_order_release); + host_buffer->Protect(run_begin << YUZU_PAGEBITS, run_pages << YUZU_PAGEBITS, + Common::MemoryPermission{}); + run_pages = 0; + } + }; + for (u64 page = vaddr >> YUZU_PAGEBITS; page != page_end; ++page) { + const bool cached = std::as_const(current_page_table->entries)[page].Type() == + Common::PageType::RasterizerCachedMemory; + if (!cached || !armed_read_traps.insert(page).second) { + protect_run(); + continue; + } + armed_read_trap_count.fetch_add(1, std::memory_order_relaxed); + if (run_pages == 0) { + run_begin = page; + } + ++run_pages; + } + protect_run(); + } + + /// Takes read permission away again from armed pages that were just cached again. + void ProtectArmedPages(u64 vaddr, u64 size) { + if (armed_read_trap_count.load(std::memory_order_relaxed) == 0) { + return; + } + std::scoped_lock lk{read_trap_mutex}; + const u64 page_end = ((vaddr + size - 1) >> YUZU_PAGEBITS) + 1; + for (auto it = armed_read_traps.lower_bound(vaddr >> YUZU_PAGEBITS); + it != armed_read_traps.end() && *it < page_end; ++it) { + host_buffer->Protect(*it << YUZU_PAGEBITS, YUZU_PAGESIZE, Common::MemoryPermission{}); + } + } + + /// A slow-path read of a cached page: its trap, if any, has done its job. + void DisarmReadTraps(u64 vaddr, size_t size) { + if (armed_read_trap_count.load(std::memory_order_relaxed) == 0) { + return; + } + std::scoped_lock lk{read_trap_mutex}; + const u64 page_end = ((vaddr + size - 1) >> YUZU_PAGEBITS) + 1; + auto it = armed_read_traps.lower_bound(vaddr >> YUZU_PAGEBITS); + while (it != armed_read_traps.end() && *it < page_end) { + // Still cached (this read came through the rasterizer path): writes stay trapped. + host_buffer->Protect(*it << YUZU_PAGEBITS, YUZU_PAGESIZE, + Common::MemoryPermission::Read); + it = armed_read_traps.erase(it); + armed_read_trap_count.fetch_sub(1, std::memory_order_relaxed); + } + } + /** * Maps a region of pages as a specific type. * @@ -698,12 +782,18 @@ struct Memory::Impl { } const size_t core = system.GetCurrentHostThreadID(); auto& current_area = rasterizer_read_areas[core]; + if (const u64 generation = read_trap_generation.load(std::memory_order_acquire); + read_area_generations[core] != generation) { + current_area = {}; + read_area_generations[core] = generation; + } gpu_device_memory->ApplyOpOnPointer(p, scratch_buffers[core], [&](DAddr address) { const DAddr end_address = address + size; if (current_area.start_address <= address && end_address <= current_area.end_address) [[likely]] { return; } + DisarmReadTraps(v_address, size); current_area = system.GPU().OnCPURead(address, size); }); } @@ -777,6 +867,11 @@ struct Memory::Impl { Common::PageTable* current_page_table = nullptr; std::array<VideoCore::RasterizerDownloadArea, Core::Hardware::NUM_CPU_CORES> rasterizer_read_areas{}; + std::array<u64, Core::Hardware::NUM_CPU_CORES> read_area_generations{}; + std::atomic<u64> read_trap_generation{}; + std::set<u64> armed_read_traps; + std::atomic<size_t> armed_read_trap_count{}; + std::mutex read_trap_mutex; std::array<GPUDirtyState, Core::Hardware::NUM_CPU_CORES> rasterizer_write_areas{}; std::array<Common::ScratchBuffer<u32>, Core::Hardware::NUM_CPU_CORES> scratch_buffers{}; std::span<Core::GPUDirtyMemoryManager> gpu_dirty_managers; @@ -969,6 +1064,10 @@ void Memory::RasterizerMarkRegionCached(Common::ProcessAddress vaddr, u64 size, impl->RasterizerMarkRegionCached(GetInteger(vaddr), size, cached); } +void Memory::RasterizerArmReadTraps(Common::ProcessAddress vaddr, u64 size) { + impl->RasterizerArmReadTraps(GetInteger(vaddr), size); +} + void Memory::MarkRegionDebug(Common::ProcessAddress vaddr, u64 size, bool debug) { impl->MarkRegionDebug(GetInteger(vaddr), size, debug); } diff --git a/src/core/memory.h b/src/core/memory.h index 204b7514f6..2b418b8864 100644 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -484,6 +484,12 @@ public: void RasterizerMarkRegionCached(Common::ProcessAddress vaddr, u64 size, bool cached); /** + * Makes the next guest read of each rasterizer cached page in the range take the slow path + * (fastmem only), so that the read reaches the GPU's reactive flush once. + */ + void RasterizerArmReadTraps(Common::ProcessAddress vaddr, u64 size); + + /** * Marks each page within the specified address range as debug or non-debug. * Debug addresses are not accessible from fastmem pointers. * diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp index bb92d896f8..37f5a89f26 100644 --- a/src/qt_common/config/shared_translation.cpp +++ b/src/qt_common/config/shared_translation.cpp @@ -204,6 +204,15 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QObject* parent) { INSERT(Settings, bg_green, QString(), QString()); INSERT(Settings, bg_blue, QString(), QString()); + INSERT(Settings, frame_gen, tr("Enable Frame Generation"), QString()); + INSERT(Settings, frame_gen_type, tr("Frame Generation Type"), QString()); + INSERT(Settings, frame_gen_multiplier, tr("Frame Generation Multiplier"), QString()); + INSERT(Settings, frame_gen_target_rate, tr("Target Frame Rate"), QString()); + INSERT(Settings, frame_gen_flow_scale_auto, tr("Auto Flow Scale"), QString()); + INSERT(Settings, frame_gen_flow_scale, tr("Flow Scale"), QString()); + INSERT(Settings, frame_gen_queue_target, tr("Queue Target"), QString()); + INSERT(Settings, frame_gen_dump_flow, tr("Dump Optical Flow"), QString()); + // Renderer (Advanced Graphics) INSERT(Settings, use_asynchronous_gpu_emulation, QString(), QString()); @@ -475,6 +484,11 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QObject* parent) { PAIR(ResolutionSetup, Res7X, tr("7X (5040p/7560p)")), PAIR(ResolutionSetup, Res8X, tr("8X (5760p/8640p)")), }}); + translations->insert({Settings::EnumMetadata<Settings::FrameGenType>::Index(), + { + PAIR(FrameGenType, Lsfg, tr("Lossless Scaling (LSFG)")), + PAIR(FrameGenType, Fsr3, tr("AMD FidelityFX Super Resolution 3 (FSR 3)")), + }}); translations->insert({Settings::EnumMetadata<Settings::ScalingFilter>::Index(), { PAIR(ScalingFilter, NearestNeighbor, tr("Nearest Neighbor")), @@ -492,6 +506,7 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QObject* parent) { PAIR(ScalingFilter, Spline1, tr("Spline-1")), PAIR(ScalingFilter, Sgsr, tr("Snapdragon Game Super Resolution")), PAIR(ScalingFilter, SgsrEdge, tr("Snapdragon Game Super Resolution EdgeDir")), + PAIR(ScalingFilter, Fsr3, tr("AMD FidelityFX Super Resolution 3")), }}); translations->insert({Settings::EnumMetadata<Settings::AntiAliasing>::Index(), { diff --git a/src/qt_common/config/shared_translation.h b/src/qt_common/config/shared_translation.h index dbe472caa1..15d427d4aa 100644 --- a/src/qt_common/config/shared_translation.h +++ b/src/qt_common/config/shared_translation.h @@ -33,6 +33,11 @@ static const std::map<Settings::AntiAliasing, QString> anti_aliasing_texts_map = {Settings::AntiAliasing::Smaa, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "SMAA"))}, }; +static const std::map<Settings::FrameGenType, QString> frame_gen_type_texts_map = { + {Settings::FrameGenType::Lsfg, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "LSFG"))}, + {Settings::FrameGenType::Fsr3, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "FSR 3"))}, +}; + static const std::map<Settings::ScalingFilter, QString> scaling_filter_texts_map = { {Settings::ScalingFilter::NearestNeighbor, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Nearest"))}, @@ -55,6 +60,7 @@ static const std::map<Settings::ScalingFilter, QString> scaling_filter_texts_map {Settings::ScalingFilter::Mmpx, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "MMPX"))}, {Settings::ScalingFilter::Sgsr, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "SGSR"))}, {Settings::ScalingFilter::SgsrEdge, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "SGSR EdgeDir"))}, + {Settings::ScalingFilter::Fsr3, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "FSR 3"))}, }; static const std::map<Settings::ConsoleMode, QString> use_docked_mode_texts_map = { diff --git a/src/qt_common/game_list/model.cpp b/src/qt_common/game_list/model.cpp index 708ef576c6..4f3916b04d 100644 --- a/src/qt_common/game_list/model.cpp +++ b/src/qt_common/game_list/model.cpp @@ -49,7 +49,7 @@ GameListModel::~GameListModel() = default; void GameListModel::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) { emit PopulatingStarted(); - current_worker.reset(); + StopWorker(); removeRows(0, rowCount()); current_worker = std::make_unique<GameListWorker>(vfs, provider, game_dirs, @@ -157,8 +157,27 @@ void GameListModel::RemoveFavorite(u64 program_id) { } } +void GameListModel::StopWorker() { + if (!current_worker) { + return; + } + + // The worker may still be inside ProcessEvents further up the stack (a nested event loop + // can trigger a repopulate), so stop it now but defer freeing it to the event loop. + current_worker->disconnect(); + current_worker->Stop(); + current_worker.release()->deleteLater(); +} + void GameListModel::Repopulate() { - current_worker.reset(); + // CreateFactories replaces the content providers a running game reads from. Stopping + // emulation repopulates the list anyway, so skip watcher-driven reloads until then. + if (QtCommon::system->IsPoweredOn()) { + LOG_INFO(Frontend, "Emulation is running, deferring game list reload."); + return; + } + + StopWorker(); QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs); PopulateAsync(UISettings::values.game_dirs); } diff --git a/src/qt_common/game_list/model.h b/src/qt_common/game_list/model.h index 98614ae33a..b850895d2f 100644 --- a/src/qt_common/game_list/model.h +++ b/src/qt_common/game_list/model.h @@ -78,6 +78,7 @@ private: void AddFavorite(u64 program_id); void RemoveFavorite(u64 program_id); void Repopulate(); + void StopWorker(); bool m_flat = false; diff --git a/src/qt_common/game_list/worker.cpp b/src/qt_common/game_list/worker.cpp index 70577cb270..d18f84089a 100644 --- a/src/qt_common/game_list/worker.cpp +++ b/src/qt_common/game_list/worker.cpp @@ -4,6 +4,7 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include <algorithm> #include <filesystem> #include <memory> #include <string> @@ -167,17 +168,78 @@ QString FormatGameName(const std::string& physical_name) { return physical_name_as_qstring; } -// Loose ExeFS dumps (<game>/exefs/main, updates in <game>/updates/exefs/main) have no control -// data, so they are named after their game folder. +// Loose ExeFS dumps (<game>/exefs/main, <game>/base/exefs/main, updates in +// <game>/updates/<title id>/exefs/main) have no control data, so they are named after their +// game folder. QString LooseExeFSName(const std::string& physical_name) { QDir dir = QFileInfo(QString::fromStdString(physical_name)).dir(); if (dir.dirName().compare(QStringLiteral("exefs"), Qt::CaseInsensitive) != 0 || !dir.cdUp()) { return {}; } - if (dir.dirName().compare(QStringLiteral("updates"), Qt::CaseInsensitive) == 0 && dir.cdUp()) { - return QObject::tr("%1 (update)").arg(dir.dirName()); + + // Storage levels sit between the game folder and its content directories. + const auto is_storage_level = [](const QString& name) { + if (name.compare(QStringLiteral("base"), Qt::CaseInsensitive) == 0 || + name.compare(QStringLiteral("update"), Qt::CaseInsensitive) == 0 || + name.compare(QStringLiteral("updates"), Qt::CaseInsensitive) == 0) { + return true; + } + return !name.isEmpty() && + std::all_of(name.cbegin(), name.cend(), [](QChar c) { + return c.isDigit() || (c >= QLatin1Char('a') && c <= QLatin1Char('f')) || + (c >= QLatin1Char('A') && c <= QLatin1Char('F')); + }); + }; + + bool is_update = false; + while (is_storage_level(dir.dirName())) { + if (dir.dirName().compare(QStringLiteral("update"), Qt::CaseInsensitive) == 0 || + dir.dirName().compare(QStringLiteral("updates"), Qt::CaseInsensitive) == 0) { + is_update = true; + } + if (!dir.cdUp()) { + return {}; + } + } + + const QString name = dir.dirName(); + if (name.isEmpty()) { + return {}; + } + if (is_update && !name.contains(QStringLiteral("update"), Qt::CaseInsensitive)) { + return QObject::tr("%1 (update)").arg(name); + } + return name; +} + +// A loose dump's game list entry is its small main executable, so the size column would only +// show a few dozen MB. Sum the dump's real contents instead: the ExeFS directory plus the +// RomFS directory next to it, matching the loader's loose RomFS discovery. +u64 LooseDumpSize(const std::filesystem::path& main_path) { + const auto exefs_dir = main_path.parent_path(); + + u64 total{}; + std::error_code ec; + const auto accumulate = [&total, &ec](const std::filesystem::path& dir) { + for (std::filesystem::recursive_directory_iterator it{ + dir, std::filesystem::directory_options::skip_permission_denied, ec}; + !ec && it != std::filesystem::recursive_directory_iterator(); it.increment(ec)) { + std::error_code entry_ec; + if (it->is_regular_file(entry_ec)) { + const auto size = it->file_size(entry_ec); + if (!entry_ec) { + total += size; + } + } + } + }; + + accumulate(exefs_dir); + const auto romfs_dir = exefs_dir.parent_path() / "romfs"; + if (std::filesystem::is_directory(romfs_dir, ec)) { + accumulate(romfs_dir); } - return dir.dirName(); + return total; } QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager, @@ -251,12 +313,18 @@ GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs_, GameListWorker::~GameListWorker() { this->disconnect(); - stop_requested.store(true); - processing_completed.Wait(); + Stop(); +} + +void GameListWorker::Stop() { + if (!stop_requested.exchange(true)) { + processing_completed.Wait(); + } } void GameListWorker::ProcessEvents(GameListModel* model) { - while (true) { + // An event may spin a nested event loop that replaces this worker, in which case stop. + while (model->current_worker.get() == this) { std::function<void(GameListModel*)> func; { // Lock queue to protect concurrent modification. @@ -346,105 +414,142 @@ void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) { void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan, GameListDir* parent_dir) { - const auto callback = [this, target, parent_dir](const std::filesystem::path& path) -> bool { - if (stop_requested) { - // Breaks the callback loop. - return false; + const auto process_candidate = [this, target, + parent_dir](const std::filesystem::path& path) { + const auto physical_name = Common::FS::PathToUTF8String(path); + const auto file = vfs->OpenFile(physical_name, FileSys::OpenMode::Read); + if (!file) { + return; } - const auto physical_name = Common::FS::PathToUTF8String(path); - const auto is_dir = Common::FS::IsDir(path); + auto loader = Loader::GetLoader(system, file); + if (!loader) { + return; + } - if (!is_dir && - (HasSupportedFileExtension(physical_name) || IsExtractedNCAMain(physical_name))) { - const auto file = vfs->OpenFile(physical_name, FileSys::OpenMode::Read); - if (!file) { - return true; - } + const auto file_type = loader->GetFileType(); + if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) { + return; + } - auto loader = Loader::GetLoader(system, file); - if (!loader) { - return true; - } + if (target == ScanTarget::PopulateGameList && + (file_type == Loader::FileType::XCI || file_type == Loader::FileType::XCZ || + file_type == Loader::FileType::NSP || file_type == Loader::FileType::NSZ) && + !Loader::IsBootableGameContainer(file, file_type)) { + return; + } - const auto file_type = loader->GetFileType(); - if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) { - return true; + u64 program_id = 0; + const auto res2 = loader->ReadProgramId(program_id); + + if (target == ScanTarget::FillManualContentProvider) { + if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) { + provider->AddEntry(FileSys::TitleType::Application, + FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()), + program_id, file); + } else if (Settings::values.ext_content_from_game_dirs.GetValue() && + (file_type == Loader::FileType::XCI || + file_type == Loader::FileType::XCZ || + file_type == Loader::FileType::NSP || + file_type == Loader::FileType::NSZ)) { + void(provider->AddEntriesFromContainer(file)); } + } else { + std::vector<u64> program_ids; + loader->ReadProgramIds(program_ids); + + const auto addEntry = [this, &path, physical_name, + parent_dir](std::unique_ptr<Loader::AppLoader>& app_loader, + const u64 id) { + std::vector<u8> icon; + [[maybe_unused]] const auto res1 = app_loader->ReadIcon(icon); + + std::string name = " "; + const auto res3 = app_loader->ReadTitle(name); + if ((res3 != Loader::ResultStatus::Success || name.empty() || name == " ") && + IsExtractedNCAMain(physical_name)) { + if (const QString loose = LooseExeFSName(physical_name); !loose.isEmpty()) { + name = loose.toStdString(); + } + } + + const FileSys::PatchManager patch{id, system.GetFileSystemController(), + system.GetContentProvider()}; + + const u64 size = IsExtractedNCAMain(physical_name) + ? LooseDumpSize(path) + : Common::FS::GetSize(physical_name); + + auto entry = MakeGameListEntry(physical_name, name, size, icon, *app_loader, id, + play_time_manager, patch); - if (target == ScanTarget::PopulateGameList && + RecordEvent([=](GameListModel* model) { model->AddEntry(entry, parent_dir); }); + }; + + if (res2 == Loader::ResultStatus::Success && program_ids.size() > 1 && (file_type == Loader::FileType::XCI || file_type == Loader::FileType::XCZ || - file_type == Loader::FileType::NSP || file_type == Loader::FileType::NSZ) && - !Loader::IsBootableGameContainer(file, file_type)) { - return true; - } + file_type == Loader::FileType::NSP || file_type == Loader::FileType::NSZ)) { + for (const auto id : program_ids) { + // dravee suggested this, only viable way to + // not show sub-games in qlaunch for now. + if ((id & 0xFFF) != 0) { + continue; + } + loader = Loader::GetLoader(system, file, id); + if (!loader) { + continue; + } - u64 program_id = 0; - const auto res2 = loader->ReadProgramId(program_id); - - if (target == ScanTarget::FillManualContentProvider) { - if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) { - provider->AddEntry(FileSys::TitleType::Application, - FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()), - program_id, file); - } else if (Settings::values.ext_content_from_game_dirs.GetValue() && - (file_type == Loader::FileType::XCI || - file_type == Loader::FileType::XCZ || - file_type == Loader::FileType::NSP || - file_type == Loader::FileType::NSZ)) { - void(provider->AddEntriesFromContainer(file)); + addEntry(loader, id); } } else { - std::vector<u64> program_ids; - loader->ReadProgramIds(program_ids); - - const auto addEntry = [this, physical_name, - parent_dir](std::unique_ptr<Loader::AppLoader>& app_loader, - const u64 id) { - std::vector<u8> icon; - [[maybe_unused]] const auto res1 = app_loader->ReadIcon(icon); - - std::string name = " "; - const auto res3 = app_loader->ReadTitle(name); - if ((res3 != Loader::ResultStatus::Success || name.empty() || name == " ") && - IsExtractedNCAMain(physical_name)) { - if (const QString loose = LooseExeFSName(physical_name); !loose.isEmpty()) { - name = loose.toStdString(); - } + addEntry(loader, program_id); + } + } + }; + + const auto callback = [this, target, deep_scan, + &process_candidate](const std::filesystem::path& path) -> bool { + if (stop_requested) { + // Breaks the callback loop. + return false; + } + + if (Common::FS::IsDir(path)) { + watch_list.append(QString::fromStdString(Common::FS::PathToUTF8String(path))); + + // Shallow scans stop at the first level, so probe the standard layouts of loose + // ExeFS dumps: <game>/exefs/main, <game>/base/exefs/main and their updates in + // <root>/updates/<title id>/exefs/main. + if (!deep_scan && target == ScanTarget::PopulateGameList) { + for (const auto& content_root : {path, path / "base"}) { + const auto exefs_main = content_root / "exefs" / "main"; + if (Common::FS::IsFile(exefs_main)) { + process_candidate(exefs_main); } - const FileSys::PatchManager patch{id, system.GetFileSystemController(), - system.GetContentProvider()}; - - auto entry = MakeGameListEntry( - physical_name, name, Common::FS::GetSize(physical_name), icon, *app_loader, - id, play_time_manager, patch); - - RecordEvent([=](GameListModel* model) { model->AddEntry(entry, parent_dir); }); - }; - - if (res2 == Loader::ResultStatus::Success && program_ids.size() > 1 && - (file_type == Loader::FileType::XCI || file_type == Loader::FileType::XCZ || - file_type == Loader::FileType::NSP || file_type == Loader::FileType::NSZ)) { - for (const auto id : program_ids) { - // dravee suggested this, only viable way to - // not show sub-games in qlaunch for now. - if ((id & 0xFFF) != 0) { - continue; - } - loader = Loader::GetLoader(system, file, id); - if (!loader) { - continue; - } - - addEntry(loader, id); + const auto updates = content_root / "updates"; + if (Common::FS::IsDir(updates)) { + Common::FS::IterateDirEntries( + updates, + [&process_candidate](const std::filesystem::path& entry) { + const auto update_main = entry / "exefs" / "main"; + if (Common::FS::IsFile(update_main)) { + process_candidate(update_main); + } + return true; + }, + Common::FS::DirEntryFilter::Directory); } - } else { - addEntry(loader, program_id); } } - } else if (is_dir) { - watch_list.append(QString::fromStdString(physical_name)); + + return true; + } + + const auto physical_name = Common::FS::PathToUTF8String(path); + if (HasSupportedFileExtension(physical_name) || IsExtractedNCAMain(physical_name)) { + process_candidate(path); } return true; @@ -454,7 +559,7 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa Common::FS::IterateDirEntriesRecursively(dir_path, callback, Common::FS::DirEntryFilter::All); } else { - Common::FS::IterateDirEntries(dir_path, callback, Common::FS::DirEntryFilter::File); + Common::FS::IterateDirEntries(dir_path, callback, Common::FS::DirEntryFilter::All); } } diff --git a/src/qt_common/game_list/worker.h b/src/qt_common/game_list/worker.h index c007498d4b..cbc1fa7021 100644 --- a/src/qt_common/game_list/worker.h +++ b/src/qt_common/game_list/worker.h @@ -52,6 +52,9 @@ public: /// Starts the processing of directory tree information. void run() override; + /// Requests the worker to stop and blocks until processing has finished. + void Stop(); + public: /** * Synchronously processes any events queued by the worker. diff --git a/src/shader_recompiler/CMakeLists.txt b/src/shader_recompiler/CMakeLists.txt index 39484a1bd5..e60f26d47f 100644 --- a/src/shader_recompiler/CMakeLists.txt +++ b/src/shader_recompiler/CMakeLists.txt @@ -183,6 +183,7 @@ add_library(shader_recompiler STATIC ir_opt/lower_fp64_to_fp32.cpp ir_opt/lower_int64_to_int32.cpp ir_opt/passes.h + ir_opt/predicate_mask.h ir_opt/position_pass.cpp ir_opt/rescaling_pass.cpp ir_opt/ssa_rewrite_pass.cpp diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_undefined.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_undefined.cpp index 0649290b0e..471c0f3795 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_undefined.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_undefined.cpp @@ -1,13 +1,31 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include <cstdlib> + #include "shader_recompiler/backend/spirv/emit_spirv_instructions.h" #include "shader_recompiler/backend/spirv/spirv_emit_context.h" namespace Shader::Backend::SPIRV { +namespace { +// Registers read before any write are zero rather than OpUndef, as NVIDIA hardware and drivers +// give them; AMD's OpUndef is whatever the register held. Title 1's glow pass exits before +// writing its colour on some pixels, which AMD blended in as green patches on the road. +// EDEN_NX2_UNDEF_ZERO=0 emits OpUndef again. +bool UndefIsZero() { + static const bool zero = [] { + const char* const value = std::getenv("EDEN_NX2_UNDEF_ZERO"); + return value == nullptr || value[0] != '0'; + }(); + return zero; +} +} // Anonymous namespace Id EmitUndefU1(EmitContext& ctx) { - return ctx.OpUndef(ctx.U1); + return UndefIsZero() ? ctx.false_value : ctx.OpUndef(ctx.U1); } Id EmitUndefU8(EmitContext&) { @@ -19,7 +37,7 @@ Id EmitUndefU16(EmitContext&) { } Id EmitUndefU32(EmitContext& ctx) { - return ctx.OpUndef(ctx.U32[1]); + return UndefIsZero() ? ctx.u32_zero_value : ctx.OpUndef(ctx.U32[1]); } Id EmitUndefU64(EmitContext&) { diff --git a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h index 0bed72eaec..031f7fac75 100644 --- a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h +++ b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h @@ -92,7 +92,8 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { active_link = call_links[call]; } } - if (active_link && insn.Bits(16, 8) == *active_link) + // RET has no destination GPR; bits 16..23 are reserved, not a write to R0. + if (active_link && opcode != 0x950 && insn.Bits(16, 8) == *active_link) return fail(index, "native callee overwrites link register"); } // Scratch values and emulated uniform state occupy reserved high GPRs. diff --git a/src/shader_recompiler/ir_opt/constant_propagation_pass.cpp b/src/shader_recompiler/ir_opt/constant_propagation_pass.cpp index 0c0d4fb1a2..a5b249f504 100644 --- a/src/shader_recompiler/ir_opt/constant_propagation_pass.cpp +++ b/src/shader_recompiler/ir_opt/constant_propagation_pass.cpp @@ -16,6 +16,7 @@ #include "shader_recompiler/frontend/ir/modifiers.h" #include "shader_recompiler/frontend/ir/value.h" #include "shader_recompiler/ir_opt/passes.h" +#include "shader_recompiler/ir_opt/predicate_mask.h" namespace Shader::Optimization { namespace { @@ -954,6 +955,8 @@ void FoldDriverConstBuffer(Environment& env, IR::Block& block, IR::Inst& inst, u } void FoldSMax32(IR::Block& block, IR::Inst& inst) { + // guest shaders normalize predicate masks with max(x, -x): it is 1 for -1 and 1, and 0 for + // 0. NVIDIA's compiler miscompiles the max form which crashes with a vkDeviceLost for (size_t index = 0; index < 2; ++index) { const IR::Value value{inst.Arg(index).Resolve()}; const IR::Inst* const negation{inst.Arg(1 - index).TryInstRecursive()}; @@ -961,26 +964,25 @@ void FoldSMax32(IR::Block& block, IR::Inst& inst) { negation->Arg(0).Resolve() != value) { continue; } + IR::IREmitter ir{block, IR::Block::InstructionList::s_iterator_to(inst)}; const IR::Inst* const select{value.TryInstRecursive()}; - if (!select || select->GetOpcode() != IR::Opcode::SelectU32) { - continue; + if (select && select->GetOpcode() == IR::Opcode::SelectU32 && + IsMaskConstant(select->Arg(1).Resolve()) && + IsMaskConstant(select->Arg(2).Resolve())) { + // absolute value on both arms + const IR::Value true_value{select->Arg(1).Resolve()}; + const IR::Value false_value{select->Arg(2).Resolve()}; + inst.ReplaceUsesWith(ir.Select(IR::U1{select->Arg(0)}, + ir.Imm32(true_value.U32() != 0 ? 1U : 0U), + ir.Imm32(false_value.U32() != 0 ? 1U : 0U))); + return; } - const IR::Value true_value{select->Arg(1).Resolve()}; - const IR::Value false_value{select->Arg(2).Resolve()}; - const auto is_mask = [](const IR::Value& candidate) { - return candidate.IsImmediate() && - (candidate.U32() == 0 || candidate.U32() == 1 || - candidate.U32() == 0xffffffffU); - }; - if (!is_mask(true_value) || !is_mask(false_value)) { - continue; + // mask merged by predicated moves (phi of -s and 1 - s) is still -1, 0 or 1 when the + // it can be proven it, and max(x, -x) of such a mask is exactly x & 1. + if (MaskValues(value) != MaskUnknown) { + inst.ReplaceUsesWith(ir.BitwiseAnd(IR::U32{value}, ir.Imm32(1))); + return; } - // max(x, -x) is 0 or 1 for boolean and predicate-mask constants. - IR::IREmitter ir{block, IR::Block::InstructionList::s_iterator_to(inst)}; - inst.ReplaceUsesWith(ir.Select(IR::U1{select->Arg(0)}, - ir.Imm32(true_value.U32() != 0 ? 1U : 0U), - ir.Imm32(false_value.U32() != 0 ? 1U : 0U))); - return; } } void ConstantPropagation(Environment& env, IR::Block& block, IR::Inst& inst) { diff --git a/src/shader_recompiler/ir_opt/predicate_mask.h b/src/shader_recompiler/ir_opt/predicate_mask.h new file mode 100644 index 0000000000..cc1cd6a8fc --- /dev/null +++ b/src/shader_recompiler/ir_opt/predicate_mask.h @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <cstddef> + +#include "common/common_types.h" +#include "shader_recompiler/frontend/ir/value.h" + +namespace Shader::Optimization { + +constexpr u32 MaskCanBeMinusOne{1u << 0}; +constexpr u32 MaskCanBeZero{1u << 1}; +constexpr u32 MaskCanBeOne{1u << 2}; +constexpr u32 MaskUnknown{0}; + +constexpr u32 MaxMaskAnalysisDepth{8}; + +// negation maps -1 -> 1, 0 -> 0 and 1 -> -1: swap the -1 and 1 bits, keep 0 +inline u32 NegateMask(u32 values) { + return (values & MaskCanBeZero) | ((values & MaskCanBeMinusOne) << 2) | + ((values & MaskCanBeOne) >> 2); +} + +// x + 1 with x != 1: -1 becomes 0, 0 becomes 1 +inline u32 AddOneToMask(u32 values) { + return values << 1; +} + +// x - 1 with x != -1: 0 becomes -1, 1 becomes 0 +inline u32 SubtractOneFromMask(u32 values) { + return values >> 1; +} + +// mask constant: guest code encodes booleans as 0, 1 and -1 (all bits set) +inline bool IsMaskConstant(const IR::Value& candidate) { + return candidate.IsImmediate() && + (candidate.U32() == 0 || candidate.U32() == 1 || candidate.U32() == 0xffffffffU); +} + +/// "predicate mask" can take: -1, 0 and 1. +/// guest compilers build masks from selects, negations, +/-1 adjustments and phis. +/// everything unproven comes back as MaskUnknown, so callers only fold +/// with a proven mask and never fold on a wrong one. +inline u32 MaskValues(const IR::Value& value, u32 depth = 0) { + const IR::Value resolved{value.Resolve()}; + if (resolved.IsImmediate()) { + if (resolved.Type() != IR::Type::U32) { + return MaskUnknown; + } + switch (resolved.U32()) { + case 0xffffffffU: + return MaskCanBeMinusOne; + case 0: + return MaskCanBeZero; + case 1: + return MaskCanBeOne; + default: + return MaskUnknown; + } + } + if (depth >= MaxMaskAnalysisDepth) { + return MaskUnknown; + } + const IR::Inst* const inst{resolved.InstRecursive()}; + switch (inst->GetOpcode()) { + case IR::Opcode::SelectU32: { + const u32 true_values{MaskValues(inst->Arg(1), depth + 1)}; + const u32 false_values{MaskValues(inst->Arg(2), depth + 1)}; + return true_values != MaskUnknown && false_values != MaskUnknown + ? true_values | false_values + : MaskUnknown; + } + case IR::Opcode::INeg32: + return NegateMask(MaskValues(inst->Arg(0), depth + 1)); + case IR::Opcode::IAdd32: + for (size_t index = 0; index < 2; ++index) { + const IR::Value step{inst->Arg(1 - index).Resolve()}; + if (!step.IsImmediate()) { + continue; + } + const u32 values{MaskValues(inst->Arg(index), depth + 1)}; + // +/- 1 stays in the domain only if the sum cannot escape it + // 1 + 1 = 2 and -1 - 1 = -2 are not mask values + if (step.U32() == 1 && (values & MaskCanBeOne) == 0) { + return AddOneToMask(values); + } + if (step.U32() == 0xffffffffU && (values & MaskCanBeMinusOne) == 0) { + return SubtractOneFromMask(values); + } + return MaskUnknown; + } + return MaskUnknown; + case IR::Opcode::Phi: { + u32 values{}; + for (size_t index = 0; index < inst->NumArgs(); ++index) { + const u32 arg_values{MaskValues(inst->Arg(index), depth + 1)}; + if (arg_values == MaskUnknown) { + return MaskUnknown; + } + values |= arg_values; + } + return values; + } + default: + return MaskUnknown; + } +} + +} // namespace Shader::Optimization diff --git a/src/shader_recompiler/object_pool.h b/src/shader_recompiler/object_pool.h index 5d648b1592..c39d3527ea 100644 --- a/src/shader_recompiler/object_pool.h +++ b/src/shader_recompiler/object_pool.h @@ -75,8 +75,13 @@ private: Release(); } + // Destroys the objects themselves: Storage's destructor is empty on purpose (the union + // also holds unused slots), so destroying the Storage elements would skip T's + // destructor and leak what the objects own (e.g. an IR::Inst's phi operand list). void Release() { - std::destroy_n(storage.get(), used_objects); + for (size_t index = 0; index < used_objects; ++index) { + std::destroy_at(&storage[index].object); + } used_objects = 0; } diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 875c40604c..dd6f5d97b5 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -19,6 +19,7 @@ add_executable(tests core/core_timing.cpp core/internal_network/network.cpp video_core/memory_tracker.cpp + video_core/ampere_returns.cpp input_common/calibration_configuration_job.cpp ) diff --git a/src/tests/video_core/ampere_returns.cpp b/src/tests/video_core/ampere_returns.cpp new file mode 100644 index 0000000000..56b8b374da --- /dev/null +++ b/src/tests/video_core/ampere_returns.cpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <array> +#include <catch2/catch_test_macros.hpp> + +#include "shader_recompiler/frontend/ampere/lower_to_maxwell.h" + +namespace { +using Shader::Ampere::Instruction; +using Shader::Ampere::LowerToMaxwell; + +TEST_CASE("Ampere returns do not write the link register", "[video_core][ampere]") { + for (const u32 link : {0U, 55U}) { + const std::array<Instruction, 4> code{{ + {{0x00007802U | (link << 16), 0x20, 0x00000f00, 0}}, + {{0x00007944, 0x10, 0x03c00000, 0}}, + {{0x0000794d, 0, 0x03800000, 0}}, + {{0x00007950U | (link << 24), 0xffffffc0, 0x03c3ffff, 0}}, + }}; + const auto lowered = LowerToMaxwell(code); + REQUIRE(static_cast<bool>(lowered)); + REQUIRE(lowered.source_size == sizeof(code)); + REQUIRE(lowered.instructions.back() == 0xe32000000007000fULL); + } +} + +TEST_CASE("Ampere returns can read a copy of the R0 link", "[video_core][ampere]") { + const std::array<Instruction, 5> code{{ + {{0x00007802, 0x20, 0x00000f00, 0}}, + {{0x00007944, 0x10, 0x03c00000, 0}}, + {{0x0000794d, 0, 0x03800000, 0}}, + {{0x00017202, 0, 0x00000f00, 0}}, + {{0x01007950, 0xffffffb0, 0x03c3ffff, 0}}, + }}; + REQUIRE(static_cast<bool>(LowerToMaxwell(code))); +} + +TEST_CASE("Ampere callees still reject writes to the link", "[video_core][ampere]") { + const std::array<Instruction, 5> code{{ + {{0x00007802, 0x20, 0x00000f00, 0}}, + {{0x00007944, 0x10, 0x03c00000, 0}}, + {{0x0000794d, 0, 0x03800000, 0}}, + {{0x00007802, 0, 0x00000f00, 0}}, + {{0x00007950, 0xffffffb0, 0x03c3ffff, 0}}, + }}; + const auto lowered = LowerToMaxwell(code); + REQUIRE(!static_cast<bool>(lowered)); + REQUIRE(lowered.error == "native callee overwrites link register"); + REQUIRE(lowered.error_offset == 0x30); +} +} // namespace diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 7f83e0d3c2..5ed43fc903 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -134,6 +134,8 @@ add_library(video_core STATIC renderer_vulkan/present/filters.h renderer_vulkan/present/fsr.cpp renderer_vulkan/present/fsr.h + renderer_vulkan/present/fsr3.cpp + renderer_vulkan/present/fsr3.h renderer_vulkan/present/fxaa.cpp renderer_vulkan/present/fxaa.h renderer_vulkan/present/layer.cpp @@ -288,6 +290,8 @@ if (ENABLE_LSFG) frame_gen/lsfg_translate.h renderer_vulkan/present/frame_gen.cpp renderer_vulkan/present/frame_gen.h + renderer_vulkan/present/fsr3_frame_gen.cpp + renderer_vulkan/present/fsr3_frame_gen.h renderer_vulkan/present/frame_gen_pacer.cpp renderer_vulkan/present/frame_gen_pacer.h renderer_vulkan/present/lsfg_alpha.cpp diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index 42d7ba066d..e90af3214d 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -279,8 +279,10 @@ bool BufferCache<P>::DMACopy(GPUVAddr src_address, GPUVAddr dest_address, u64 am const DAddr diff = base_address - *cpu_src_address; const DAddr new_base_address = *cpu_dest_address + diff; tmp_intervals.push_back({new_base_address, size}); - if (AsyncBufferDownloads()) { + if (DownloadsAtFence(new_base_address, size)) { uncommitted_gpu_modified_ranges.Add(new_base_address, size); + } else { + ArmReadTraps(new_base_address, size); } }; gpu_modified_ranges.ForEachInRange(*cpu_src_address, amount, mirror); @@ -1603,8 +1605,10 @@ void BufferCache<P>::MarkWrittenBuffer(BufferId buffer_id, DAddr device_addr, u3 } memory_tracker.MarkRegionAsGpuModified(device_addr, size); gpu_modified_ranges.Add(device_addr, size); - if (AsyncBufferDownloads()) { + if (DownloadsAtFence(device_addr, size)) { uncommitted_gpu_modified_ranges.Add(device_addr, size); + } else { + ArmReadTraps(device_addr, size); } } @@ -2090,6 +2094,8 @@ void BufferCache<P>::DeleteBuffer(BufferId buffer_id, bool do_not_mark) { if constexpr (requires { runtime.OnBufferDeleted(slot_buffers[buffer_id]); }) { runtime.OnBufferDeleted(slot_buffers[buffer_id]); } + read_trapped_ranges.Subtract(slot_buffers[buffer_id].CpuAddr(), + slot_buffers[buffer_id].SizeBytes()); bool dirty_index{false}; boost::container::small_vector<u64, NUM_VERTEX_BUFFERS> dirty_vertex_buffers; const auto scalar_replace = [buffer_id](Binding& binding) { diff --git a/src/video_core/buffer_cache/buffer_cache_base.h b/src/video_core/buffer_cache/buffer_cache_base.h index 45a1836341..d8a61b7913 100644 --- a/src/video_core/buffer_cache/buffer_cache_base.h +++ b/src/video_core/buffer_cache/buffer_cache_base.h @@ -245,6 +245,12 @@ public: std::optional<VideoCore::RasterizerDownloadArea> GetFlushArea(DAddr device_addr, u64 size); + /// Without downloads at every fence, a guest CPU read is the only way GPU-written buffer + /// data reaches guest memory, so reads must ask the buffer cache (GetFlushArea). + bool FlushesOnCpuRead() const { + return !AsyncBufferDownloads(); + } + bool InlineMemory(DAddr dest_address, size_t copy_size, std::span<const u8> inlined_buffer); void BindGraphicsUniformBuffer(size_t stage, u32 index, GPUVAddr gpu_addr, u32 size); @@ -536,6 +542,31 @@ private: } return gpu_memory == nullptr || !gpu_memory->UsesNativeTiling(); } + + /// Whether a GPU write to this range is copied back at the next fence. Ranges the guest CPU + /// has read (Preflushable, set by the first reactive flush) always are: later reads skip the + /// flush and rely on these downloads, e.g. collision meshes skinned by compute shaders. + bool DownloadsAtFence(DAddr device_addr, u64 size) noexcept { + return AsyncBufferDownloads() || memory_tracker.IsRegionPreflushable(device_addr, size); + } + + /// GPU writes that are not downloaded at fences: trap the guest CPU's next read of their + /// pages, which flushes them once and marks them Preflushable (then DownloadsAtFence). + void ArmReadTraps(DAddr device_addr, u64 size) { + const DAddr begin = Common::AlignDown(device_addr, Core::DEVICE_PAGESIZE); + const DAddr end = Common::AlignUp(device_addr + size, Core::DEVICE_PAGESIZE); + u64 armed_bytes = 0; + read_trapped_ranges.ForEachInRange(begin, end - begin, [&](DAddr lower, DAddr upper) { + armed_bytes += upper - lower; + }); + if (armed_bytes == end - begin) { + return; + } + read_trapped_ranges.Add(begin, end - begin); + device_memory.ArmReadTraps(begin, end - begin); + } + + Common::RangeSet<DAddr> read_trapped_ranges; Common::RangeSet<DAddr> gpu_modified_ranges; std::deque<Common::RangeSet<DAddr>> committed_gpu_modified_ranges; diff --git a/src/video_core/host1x/codecs/codec.cpp b/src/video_core/host1x/codecs/codec.cpp index 56332866e5..3cbef1d891 100644 --- a/src/video_core/host1x/codecs/codec.cpp +++ b/src/video_core/host1x/codecs/codec.cpp @@ -104,6 +104,8 @@ std::string_view Codec::GetCurrentCodecName() const { return "H265"; case Host1x::NvdecCommon::VideoCodec::VP9: return "VP9"; + case Host1x::NvdecCommon::VideoCodec::AV1: + return "AV1"; default: return "Unknown"; } diff --git a/src/video_core/host1x/ffmpeg.cpp b/src/video_core/host1x/ffmpeg.cpp index 676e5820a5..1af8307adc 100644 --- a/src/video_core/host1x/ffmpeg.cpp +++ b/src/video_core/host1x/ffmpeg.cpp @@ -163,6 +163,8 @@ Decoder::Decoder(Tegra::Host1x::NvdecCommon::VideoCodec codec) { return AV_CODEC_ID_VP8; case Tegra::Host1x::NvdecCommon::VideoCodec::VP9: return AV_CODEC_ID_VP9; + case Tegra::Host1x::NvdecCommon::VideoCodec::AV1: + return AV_CODEC_ID_AV1; default: UNIMPLEMENTED_MSG("Unknown codec {}", codec); return AV_CODEC_ID_NONE; diff --git a/src/video_core/host1x/gpu_device_memory_manager.cpp b/src/video_core/host1x/gpu_device_memory_manager.cpp index 668c2f08b8..3d7d88b92d 100644 --- a/src/video_core/host1x/gpu_device_memory_manager.cpp +++ b/src/video_core/host1x/gpu_device_memory_manager.cpp @@ -12,6 +12,11 @@ struct MaxwellDeviceMethods { size_t size, bool caching) { interface->RasterizerMarkRegionCached(address, size, caching); } + + static inline void MarkRegionReadTrap(Core::Memory::Memory* interface, VAddr address, + size_t size) { + interface->RasterizerArmReadTraps(address, size); + } }; } // namespace Tegra diff --git a/src/video_core/host1x/nvdec_common.h b/src/video_core/host1x/nvdec_common.h index 0857a9f95c..26d7c86c98 100644 --- a/src/video_core/host1x/nvdec_common.h +++ b/src/video_core/host1x/nvdec_common.h @@ -22,6 +22,7 @@ enum class VideoCodec : u64 { VP8 = 0x5, H265 = 0x7, VP9 = 0x9, + AV1 = 0xA, }; struct Offset { diff --git a/src/video_core/host1x/nx2_video_decoder.cpp b/src/video_core/host1x/nx2_video_decoder.cpp index 7d185ca573..9b34bd84af 100644 --- a/src/video_core/host1x/nx2_video_decoder.cpp +++ b/src/video_core/host1x/nx2_video_decoder.cpp @@ -35,6 +35,10 @@ std::optional<NvdecCommon::VideoCodec> CodecFromMime(std::string_view mime) { if (mime.find("vp8") != std::string_view::npos) { return NvdecCommon::VideoCodec::VP8; } + // "video/av01" or "video/av1". FFmpeg decodes AV1 with libdav1d. + if (mime.find("av01") != std::string_view::npos || mime.find("av1") != std::string_view::npos) { + return NvdecCommon::VideoCodec::AV1; + } if (mime.find("avc") != std::string_view::npos || mime.find("264") != std::string_view::npos) { return NvdecCommon::VideoCodec::H264; } diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt index 8082e830d6..35b350c2cd 100644 --- a/src/video_core/host_shaders/CMakeLists.txt +++ b/src/video_core/host_shaders/CMakeLists.txt @@ -92,6 +92,8 @@ set(SHADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/sgsr1_shader.vert ${CMAKE_CURRENT_SOURCE_DIR}/sgsr1_shader_mobile.frag ${CMAKE_CURRENT_SOURCE_DIR}/sgsr1_shader_mobile_edge_direction.frag + ${CMAKE_CURRENT_SOURCE_DIR}/fsr3_optical_flow.comp + ${CMAKE_CURRENT_SOURCE_DIR}/fsr3_frame_interpolation.comp ) if (HAIKUOS) diff --git a/src/video_core/host_shaders/fsr3_frame_interpolation.comp b/src/video_core/host_shaders/fsr3_frame_interpolation.comp new file mode 100644 index 0000000000..e33691e801 --- /dev/null +++ b/src/video_core/host_shaders/fsr3_frame_interpolation.comp @@ -0,0 +1,106 @@ +#version 460 core + +layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in; + +layout(binding = 0) uniform sampler2D CurrentFrame; +layout(binding = 1) uniform sampler2D PreviousFrame; +layout(binding = 2) uniform sampler2D MotionVectors; +layout(binding = 3, rgba8) uniform writeonly image2D OutGeneratedFrame; + +layout(push_constant) uniform PushConstants { + vec2 inv_render_size; + vec2 render_size; + float blend_factor; // timestamp between 0.0 and 1.0 + float flow_scale; +}; + +// ── Optimized 3×3 motion vector median using textureGather ────────── +// textureGather fetches a 2×2 quad per call, so we only need 4 calls +// to cover a 3×3 neighbourhood (vs 9 individual texture() calls). +// We extract the median of X and Y components independently. +void insertionSort5(inout float a[5]) { + for (int i = 1; i < 5; i++) { + float key = a[i]; + int j = i - 1; + while (j >= 0 && a[j] > key) { + a[j + 1] = a[j]; + j--; + } + a[j + 1] = key; + } +} + +vec2 sampleMotionMedian(vec2 uv) { + vec2 texel = inv_render_size; + + // textureGather returns 2×2 quads: (x-0.5, y-0.5) to (x+0.5, y+0.5) + // We use 4 gathers offset to cover the 3×3 region + // Gather component 0 = R (motion X), component 1 = G (motion Y) + vec4 gx0 = textureGather(MotionVectors, uv + vec2(-0.5, -0.5) * texel, 0); // TL quad + vec4 gx1 = textureGather(MotionVectors, uv + vec2( 0.5, -0.5) * texel, 0); // TR quad + vec4 gx2 = textureGather(MotionVectors, uv + vec2(-0.5, 0.5) * texel, 0); // BL quad + vec4 gx3 = textureGather(MotionVectors, uv + vec2( 0.5, 0.5) * texel, 0); // BR quad + + vec4 gy0 = textureGather(MotionVectors, uv + vec2(-0.5, -0.5) * texel, 1); + vec4 gy1 = textureGather(MotionVectors, uv + vec2( 0.5, -0.5) * texel, 1); + vec4 gy2 = textureGather(MotionVectors, uv + vec2(-0.5, 0.5) * texel, 1); + vec4 gy3 = textureGather(MotionVectors, uv + vec2( 0.5, 0.5) * texel, 1); + + // Extract 9 unique samples from overlapping 2×2 quads + // gatherN.w=TL, .z=TR, .x=BL, .y=BR of each quad + // 3×3 grid: row0=[gx0.w, gx0.z/gx1.w, gx1.z] + // row1=[gx0.x/gx2.w, center, gx1.y/gx3.z] + // row2=[gx2.x, gx2.y/gx3.x, gx3.y] + float xs[9] = float[](gx0.w, gx1.w, gx1.z, gx0.x, texture(MotionVectors, uv).x, gx1.y, gx2.x, gx3.x, gx3.y); + float ys[9] = float[](gy0.w, gy1.w, gy1.z, gy0.x, texture(MotionVectors, uv).y, gy1.y, gy2.x, gy3.x, gy3.y); + + // Partial sort: only need to find element [4] (median) + // Use two passes of 5-element insertion sort on the lower half + float xlo[5] = float[](xs[0], xs[1], xs[2], xs[3], xs[4]); + float ylo[5] = float[](ys[0], ys[1], ys[2], ys[3], ys[4]); + insertionSort5(xlo); + insertionSort5(ylo); + + // Insert remaining 4 elements, keeping only the 5 smallest + for (int i = 5; i < 9; i++) { + if (xs[i] < xlo[4]) { xlo[4] = xs[i]; insertionSort5(xlo); } + if (ys[i] < ylo[4]) { ylo[4] = ys[i]; insertionSort5(ylo); } + } + + return vec2(xlo[4], ylo[4]); +} + +void main() { + ivec2 pixel_coord = ivec2(gl_GlobalInvocationID.xy); + if (pixel_coord.x >= int(render_size.x) || pixel_coord.y >= int(render_size.y)) { + return; + } + + vec2 uv = (vec2(pixel_coord) + 0.5) * inv_render_size; + + // Median-filtered motion vector (reduces outlier jitter) + vec2 motion = sampleMotionMedian(uv) * (flow_scale * inv_render_size); + + // Forward and backward warped coordinates + vec2 forward_uv = clamp(uv - motion * blend_factor, 0.0, 1.0); + vec2 backward_uv = clamp(uv + motion * (1.0 - blend_factor), 0.0, 1.0); + + vec4 forward_color = texture(PreviousFrame, forward_uv); + vec4 backward_color = texture(CurrentFrame, backward_uv); + + // Colour-difference occlusion + vec3 color_diff = abs(forward_color.rgb - backward_color.rgb); + float occlusion = clamp(dot(color_diff, vec3(0.333)) * 2.0, 0.0, 1.0); + + // Smooth bilateral interpolation — bias toward centre on occlusion + float alpha = mix(blend_factor, 0.5, occlusion * 0.4); + vec4 interpolated = mix(forward_color, backward_color, alpha); + + // ── Anti-flicker: blend with un-warped centre pixel ───────────── + float motion_magnitude = length(motion * render_size); + float direct_weight = exp(-motion_magnitude * 0.5); + vec4 center_color = mix(texture(CurrentFrame, uv), texture(PreviousFrame, uv), blend_factor); + interpolated = mix(interpolated, center_color, direct_weight * 0.3); + + imageStore(OutGeneratedFrame, pixel_coord, interpolated); +} diff --git a/src/video_core/host_shaders/fsr3_optical_flow.comp b/src/video_core/host_shaders/fsr3_optical_flow.comp new file mode 100644 index 0000000000..b0e04600cb --- /dev/null +++ b/src/video_core/host_shaders/fsr3_optical_flow.comp @@ -0,0 +1,112 @@ +#version 460 core + +layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in; + +layout(binding = 0) uniform sampler2D CurrentFrame; +layout(binding = 1) uniform sampler2D PreviousFrame; +layout(binding = 2, rg16f) uniform writeonly image2D OutMotionVectors; + +layout(push_constant) uniform PushConstants { + vec2 inv_render_size; + vec2 render_size; + float search_radius; + float block_size; +}; + +// ── Shared memory tile cache ──────────────────────────────────────────── +const uint TILE = 16u; +const uint BORDER = 2u; +const uint PADDED = TILE + 2u * BORDER; // 20 +shared float s_curr_luma[PADDED * PADDED]; + +float rgb2luma(vec3 c) { + return dot(c, vec3(0.299, 0.587, 0.114)); +} + +void loadTile(ivec2 tile_origin) { + uint thread_id = gl_LocalInvocationID.y * TILE + gl_LocalInvocationID.x; + for (uint i = thread_id; i < PADDED * PADDED; i += TILE * TILE) { + uint lx = i % PADDED; + uint ly = i / PADDED; + ivec2 coord = tile_origin + ivec2(lx, ly) - ivec2(BORDER); + vec2 uv = (vec2(coord) + 0.5) * inv_render_size; + s_curr_luma[i] = rgb2luma(texture(CurrentFrame, uv).rgb); + } + barrier(); +} + +float getCachedLuma(ivec2 local_coord) { + ivec2 padded = local_coord + ivec2(BORDER); + return s_curr_luma[padded.y * PADDED + padded.x]; +} + +void main() { + ivec2 pixel_coord = ivec2(gl_GlobalInvocationID.xy); + + ivec2 tile_origin = ivec2(gl_WorkGroupID.xy) * ivec2(TILE); + loadTile(tile_origin); + + if (pixel_coord.x >= int(render_size.x) || pixel_coord.y >= int(render_size.y)) { + return; + } + + vec2 uv = (vec2(pixel_coord) + 0.5) * inv_render_size; + ivec2 local = ivec2(gl_LocalInvocationID.xy); + float curr_luma = getCachedLuma(local); + + float center_diff = abs(curr_luma - rgb2luma(texture(PreviousFrame, uv).rgb)); + + // Early-out only for truly identical pixels (very tight threshold) + if (center_diff < 0.002) { + imageStore(OutMotionVectors, pixel_coord, vec4(0.0, 0.0, 0.0, 0.0)); + return; + } + + vec2 best_motion = vec2(0.0); + float min_diff = center_diff; + + // Large diamond (radius 2) + const vec2 diamond_large[4] = vec2[]( + vec2(0.0, -2.0), + vec2(-2.0, 0.0), vec2(2.0, 0.0), + vec2(0.0, 2.0) + ); + + for (int i = 0; i < 4; ++i) { + vec2 offset_uv = uv + diamond_large[i] * inv_render_size; + float luma = rgb2luma(texture(PreviousFrame, offset_uv).rgb); + float d = abs(curr_luma - luma) + 0.04 * length(diamond_large[i]); + if (d < min_diff) { + min_diff = d; + best_motion = diamond_large[i]; + } + } + + // Small refinement diamond (radius 1) + const vec2 diamond_small[4] = vec2[]( + vec2(0.0, -1.0), + vec2(-1.0, 0.0), vec2(1.0, 0.0), + vec2(0.0, 1.0) + ); + + vec2 center = best_motion; + for (int i = 0; i < 4; ++i) { + vec2 cand = center + diamond_small[i]; + vec2 offset_uv = uv + cand * inv_render_size; + float luma = rgb2luma(texture(PreviousFrame, offset_uv).rgb); + float d = abs(curr_luma - luma) + 0.04 * length(cand); + if (d < min_diff) { + min_diff = d; + best_motion = cand; + } + } + + // ── Dampen very small motions to prevent micro-jitter ── + // Motions below ~0.5 pixels are likely noise, not real movement + float motion_len = length(best_motion); + if (motion_len < 0.5) { + best_motion *= motion_len * 2.0; // smooth fade to zero + } + + imageStore(OutMotionVectors, pixel_coord, vec4(best_motion, 0.0, 0.0)); +} diff --git a/src/video_core/nx2.h b/src/video_core/nx2.h index 4b0d3d5f62..318607384b 100644 --- a/src/video_core/nx2.h +++ b/src/video_core/nx2.h @@ -9,6 +9,7 @@ #include <functional> #include <map> #include <mutex> +#include <utility> #include <vector> #include "common/common_types.h" @@ -101,7 +102,13 @@ struct Nx2SparseRegionState { u64 size; u64 block_size; std::vector<bool> written; + /// Once the game maps its own memory into the region (nx2:gpu 216): which tiles it maps. + /// Only those hold data then (writes elsewhere are dropped, as on the console). + std::vector<bool> mapped; }; + +/// The granularity of the game's sparse tiles (nx2:gpu 216 maps 64 KiB pages). +inline constexpr u64 Nx2SparseTileSize = 64ULL << 10; inline std::mutex g_nx2_sparse_state_mutex; inline std::map<u64, Nx2SparseRegionState> g_nx2_sparse_state; @@ -116,6 +123,12 @@ inline void Nx2SparseRegionDestroyed(u64 gpu_address) { g_nx2_sparse_state.erase(gpu_address); } +/// A new emulation session: the previous game's regions are gone (nvdrv's Module clears them). +inline void Nx2ClearSparseRegions() { + std::scoped_lock lk{g_nx2_sparse_state_mutex}; + g_nx2_sparse_state.clear(); +} + inline void Nx2SparseBlockWritten(u64 region_address, u64 block) { std::scoped_lock lk{g_nx2_sparse_state_mutex}; if (const auto it = g_nx2_sparse_state.find(region_address); it != g_nx2_sparse_state.end()) { @@ -123,6 +136,59 @@ inline void Nx2SparseBlockWritten(u64 region_address, u64 block) { } } +/// The game maps its own memory over [offset, +size) of the region at region_address (mapped), or +/// unmaps it there. +inline void Nx2SparseTilesMapped(u64 region_address, u64 offset, u64 size, bool mapped) { + std::scoped_lock lk{g_nx2_sparse_state_mutex}; + const auto it = g_nx2_sparse_state.find(region_address); + if (it == g_nx2_sparse_state.end() || size == 0) { + return; + } + auto& region = it->second; + if (region.mapped.empty()) { + region.mapped.resize((region.size + Nx2SparseTileSize - 1) / Nx2SparseTileSize); + } + const u64 last = (std::min)((offset + size - 1) / Nx2SparseTileSize, u64{region.mapped.size() - 1}); + for (u64 tile = offset / Nx2SparseTileSize; tile <= last; ++tile) { + region.mapped[tile] = mapped; + } +} + +/// The parts of [gpu_address, +size), which lies in one sparse region, that can hold data: the +/// tiles the game maps there if it maps any, otherwise the blocks written. As (offset from +/// gpu_address, size) runs. +inline std::vector<std::pair<u64, u64>> Nx2SparseDataRanges(u64 gpu_address, u64 size) { + std::vector<std::pair<u64, u64>> ranges; + std::scoped_lock lk{g_nx2_sparse_state_mutex}; + auto it = g_nx2_sparse_state.upper_bound(gpu_address); + if (it == g_nx2_sparse_state.begin()) { + return ranges; + } + --it; + const auto& [start, region] = *it; + if (size == 0 || gpu_address + size > start + region.size) { + return ranges; + } + const bool by_tile = !region.mapped.empty(); + const u64 unit = by_tile ? Nx2SparseTileSize : region.block_size; + const std::vector<bool>& present = by_tile ? region.mapped : region.written; + const u64 begin = gpu_address - start; + const u64 end = begin + size; + for (u64 index = begin / unit; index * unit < end && index < present.size(); ++index) { + if (!present[index]) { + continue; + } + const u64 run_begin = (std::max)(index * unit, begin) - begin; + const u64 run_end = (std::min)((index + 1) * unit, end) - begin; + if (!ranges.empty() && ranges.back().first + ranges.back().second == run_begin) { + ranges.back().second += run_end - run_begin; + } else { + ranges.emplace_back(run_begin, run_end - run_begin); + } + } + return ranges; +} + /// True when [gpu_address, +size) lies in one sparse region. inline bool Nx2InSparseRegion(u64 gpu_address, u64 size) { if (size == 0 || gpu_address + size <= g_nx2_sparse_low.load(std::memory_order_relaxed) || diff --git a/src/video_core/renderer_opengl/gl_blit_screen.cpp b/src/video_core/renderer_opengl/gl_blit_screen.cpp index 23e5eb7481..5bad51112b 100644 --- a/src/video_core/renderer_opengl/gl_blit_screen.cpp +++ b/src/video_core/renderer_opengl/gl_blit_screen.cpp @@ -115,6 +115,7 @@ void BlitScreen::CreateWindowAdapt() { window_adapt = MakeMmpx(device); break; case Settings::ScalingFilter::Fsr: + case Settings::ScalingFilter::Fsr3: case Settings::ScalingFilter::Sgsr: case Settings::ScalingFilter::SgsrEdge: case Settings::ScalingFilter::Bilinear: diff --git a/src/video_core/renderer_opengl/gl_texture_cache.cpp b/src/video_core/renderer_opengl/gl_texture_cache.cpp index 680cb2179f..8cca26aee9 100644 --- a/src/video_core/renderer_opengl/gl_texture_cache.cpp +++ b/src/video_core/renderer_opengl/gl_texture_cache.cpp @@ -236,8 +236,7 @@ void ApplySwizzle(GLuint handle, PixelFormat format, std::array<SwizzleSource, 4 const VideoCommon::ImageInfo& info) { if (IsPixelFormatASTC(info.format) && info.size.depth == 1 && !runtime.HasNativeASTC()) { return Settings::values.accelerate_astc.GetValue() == Settings::AstcDecodeMode::Gpu && - Settings::values.astc_recompression.GetValue() == - Settings::AstcRecompression::Uncompressed; + Settings::GetAstcRecompression() == Settings::AstcRecompression::Uncompressed; } // Disable other accelerated uploads for now as they don't implement swizzled uploads return false; @@ -443,12 +442,11 @@ OGLTexture MakeImage(const VideoCommon::ImageInfo& info, GLenum gl_internal_form } [[nodiscard]] bool IsAstcRecompressionEnabled() { - return Settings::values.astc_recompression.GetValue() != - Settings::AstcRecompression::Uncompressed; + return Settings::GetAstcRecompression() != Settings::AstcRecompression::Uncompressed; } [[nodiscard]] GLenum SelectAstcFormat(PixelFormat format, bool is_srgb) { - switch (Settings::values.astc_recompression.GetValue()) { + switch (Settings::GetAstcRecompression()) { case Settings::AstcRecompression::Bc1: return is_srgb ? GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT : GL_COMPRESSED_RGBA_S3TC_DXT1_EXT; break; diff --git a/src/video_core/renderer_opengl/present/layer.cpp b/src/video_core/renderer_opengl/present/layer.cpp index d0a8e1186e..9e4a0f854a 100644 --- a/src/video_core/renderer_opengl/present/layer.cpp +++ b/src/video_core/renderer_opengl/present/layer.cpp @@ -69,7 +69,7 @@ GLuint Layer::ConfigureDraw(std::array<GLfloat, 3 * 2>& out_matrix, glDisablei(GL_SCISSOR_TEST, 0); - if (filters.get_scaling_filter() == Settings::ScalingFilter::Fsr) { + if (filters.get_scaling_filter() == Settings::ScalingFilter::Fsr || filters.get_scaling_filter() == Settings::ScalingFilter::Fsr3) { if (!fsr || fsr->NeedsRecreation(layout.screen)) { fsr.emplace(layout.screen.GetWidth(), layout.screen.GetHeight()); } diff --git a/src/video_core/renderer_vulkan/maxwell_to_vk.cpp b/src/video_core/renderer_vulkan/maxwell_to_vk.cpp index 6c18ade926..c8e43d0650 100644 --- a/src/video_core/renderer_vulkan/maxwell_to_vk.cpp +++ b/src/video_core/renderer_vulkan/maxwell_to_vk.cpp @@ -246,7 +246,7 @@ FormatInfo SurfaceFormat(const Device& device, FormatType format_type, bool with bool const is_srgb = with_srgb && VideoCore::Surface::IsPixelFormatSRGB(pixel_format); // Transcode on hardware that doesn't support ASTC natively if (!device.IsOptimalAstcSupported() && VideoCore::Surface::IsPixelFormatASTC(pixel_format)) { - switch (Settings::values.astc_recompression.GetValue()) { + switch (Settings::GetAstcRecompression()) { case Settings::AstcRecompression::Uncompressed: if (is_srgb) { tuple.format = VK_FORMAT_A8B8G8R8_SRGB_PACK32; diff --git a/src/video_core/renderer_vulkan/present/frame_gen.cpp b/src/video_core/renderer_vulkan/present/frame_gen.cpp index e6d857323b..a54598e9fc 100644 --- a/src/video_core/renderer_vulkan/present/frame_gen.cpp +++ b/src/video_core/renderer_vulkan/present/frame_gen.cpp @@ -203,15 +203,16 @@ void FrameGen::Process(const Device& device, Frame* frame, VkFormat format, } if (unavailable || !Settings::values.frame_gen.GetValue()) { - if (chain) { + if (chain || fsr3_chain) { scheduler.Finish(); chain.reset(); + fsr3_chain.reset(); } warm_streak = 0; return; } - if (!shaders->IsValid()) { + if (!shaders->IsValid() && Settings::values.frame_gen_type.GetValue() != Settings::FrameGenType::Fsr3) { unavailable = true; return; } @@ -226,7 +227,7 @@ void FrameGen::Process(const Device& device, Frame* frame, VkFormat format, const VkExtent2D extent{.width = frame->width, .height = frame->height}; const f32 flow_scale = ConfiguredFlowScale(peak_guest_extent, extent); - if (!chain || built_extent.width != extent.width || built_extent.height != extent.height || + if ((!chain && !fsr3_chain) || built_extent.width != extent.width || built_extent.height != extent.height || built_format != format || built_flow_scale != flow_scale) { Rebuild(device, extent, format, flow_scale); } @@ -239,21 +240,28 @@ void FrameGen::Process(const Device& device, Frame* frame, VkFormat format, warm_streak = warm ? warm_streak + 1 : 0; generated = warm && warm_streak >= LSFG_RECURRENCE_FRAMES && plan.generations > 0; - scheduler.RequestOutsideRenderPassOperationContext(); - scheduler.Record([this, source = *frame->image, extent, count, - dispatch = warm](vk::CommandBuffer cmdbuf) { - CopyPresentedFrame(cmdbuf, source, chain->Input(count), extent); - if (dispatch) { - chain->DispatchShared(cmdbuf, count); + const bool use_fsr3 = Settings::values.frame_gen_type.GetValue() == Settings::FrameGenType::Fsr3; + + if (use_fsr3 && fsr3_chain) { + scheduler.RequestOutsideRenderPassOperationContext(); + fsr3_chain->Process(device, scheduler, *frame->image, *frame->storage_view, count); + } else if (chain) { + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([this, source = *frame->image, extent, count, + dispatch = warm](vk::CommandBuffer cmdbuf) { + CopyPresentedFrame(cmdbuf, source, chain->Input(count), extent); + if (dispatch) { + chain->DispatchShared(cmdbuf, count); + } + }); + + const bool dump_requested = generated && Settings::values.frame_gen_dump_flow.GetValue(); + if (!dump_requested) { + dumped = false; + } else if (!dumped) { + DumpDebugImages(count); + dumped = true; } - }); - - const bool dump_requested = generated && Settings::values.frame_gen_dump_flow.GetValue(); - if (!dump_requested) { - dumped = false; - } else if (!dumped) { - DumpDebugImages(count); - dumped = true; } } @@ -271,27 +279,42 @@ size_t FrameGen::GeneratedFrameCount() const { } void FrameGen::GenerateInto(const Device& device, Frame* destination, size_t generation) { - chain->SetTarget(device, last_generations, generation, destination->index, - *destination->storage_view); - - const VkExtent2D extent{.width = destination->width, .height = destination->height}; - - scheduler.RequestOutsideRenderPassOperationContext(); - scheduler.Record([this, count = last_count, generation_count = last_generations, generation, - target = destination->index, image = *destination->image, - extent](vk::CommandBuffer cmdbuf) { - chain->DispatchGeneration(cmdbuf, count, generation_count, generation, target, image, - extent); - }); + const bool use_fsr3 = Settings::values.frame_gen_type.GetValue() == Settings::FrameGenType::Fsr3; + + if (use_fsr3 && fsr3_chain) { + const f32 blend = static_cast<f32>(generation + 1) / static_cast<f32>(last_generations + 1); + const f32 flow_scale = ConfiguredFlowScale(peak_guest_extent, built_extent); + + scheduler.RequestOutsideRenderPassOperationContext(); + fsr3_chain->Generate(device, scheduler, *destination->storage_view, blend, flow_scale); + } else if (chain) { + chain->SetTarget(device, last_generations, generation, destination->index, + *destination->storage_view); + + const VkExtent2D extent{.width = destination->width, .height = destination->height}; + + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([this, count = last_count, generation_count = last_generations, generation, + target = destination->index, image = *destination->image, + extent](vk::CommandBuffer cmdbuf) { + chain->DispatchGeneration(cmdbuf, count, generation_count, generation, target, image, + extent); + }); + } } void FrameGen::Rebuild(const Device& device, VkExtent2D extent, VkFormat format, f32 flow_scale) { scheduler.Finish(); chain.reset(); + fsr3_chain.reset(); built_flow_scale = flow_scale; - chain.emplace(device, memory_allocator, *shaders, extent, format, built_flow_scale); + if (Settings::values.frame_gen_type.GetValue() == Settings::FrameGenType::Fsr3) { + fsr3_chain.emplace(device, memory_allocator, extent, format); + } else { + chain.emplace(device, memory_allocator, *shaders, extent, format, built_flow_scale); + } built_extent = extent; built_format = format; frame_count = 0; diff --git a/src/video_core/renderer_vulkan/present/frame_gen.h b/src/video_core/renderer_vulkan/present/frame_gen.h index 19c6d04289..1eb49cd7b1 100644 --- a/src/video_core/renderer_vulkan/present/frame_gen.h +++ b/src/video_core/renderer_vulkan/present/frame_gen.h @@ -9,6 +9,7 @@ #include "video_core/renderer_vulkan/present/frame_gen_pacer.h" #include "video_core/renderer_vulkan/present/lsfg_chain.h" #include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/renderer_vulkan/present/fsr3_frame_gen.h" #include "video_core/vulkan_common/vulkan_memory_allocator.h" namespace Vulkan { @@ -39,6 +40,7 @@ private: std::optional<LsfgShaders> shaders; std::optional<LsfgChain> chain; + std::optional<Fsr3FrameGen> fsr3_chain; FrameGenPacer pacer; FrameGenPlan plan{}; VkExtent2D peak_guest_extent{}; diff --git a/src/video_core/renderer_vulkan/present/fsr3.cpp b/src/video_core/renderer_vulkan/present/fsr3.cpp new file mode 100644 index 0000000000..ad1cd6c8b1 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/fsr3.cpp @@ -0,0 +1,232 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "common/common_types.h" +#include "common/div_ceil.h" +#include "common/settings.h" + +#include "video_core/fsr.h" +#include "video_core/host_shaders/vulkan_fidelityfx_fsr_easu_fp16_frag_spv.h" +#include "video_core/host_shaders/vulkan_fidelityfx_fsr_easu_fp32_frag_spv.h" +#include "video_core/host_shaders/vulkan_fidelityfx_fsr_rcas_fp16_frag_spv.h" +#include "video_core/host_shaders/vulkan_fidelityfx_fsr_rcas_fp32_frag_spv.h" +#include "video_core/host_shaders/vulkan_fidelityfx_fsr_vert_spv.h" +#include "video_core/renderer_vulkan/present/fsr3.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/renderer_vulkan/vk_scheduler.h" +#include "video_core/renderer_vulkan/vk_shader_util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { +using namespace FSR; + +using PushConstants = std::array<u32, 4 * 4>; + +FSR3::FSR3(const Device& device, MemoryAllocator& memory_allocator, size_t image_count, + VkExtent2D extent) + : m_memory_allocator{memory_allocator}, m_image_count{image_count}, m_extent{extent} { + CreateImages(device); + CreateRenderPasses(device); + CreateSampler(device); + CreateShaders(device); + CreateDescriptorPool(device); + CreateDescriptorSetLayout(device); + CreateDescriptorSets(device); + CreatePipelineLayouts(device); + CreatePipelines(device); +} + +FSR3::~FSR3() = default; + +void FSR3::CreateImages(const Device& device) { + m_dynamic_images.resize(m_image_count); + for (auto& images : m_dynamic_images) { + images.images[Easu] = + CreateWrappedImage(m_memory_allocator, m_extent, VK_FORMAT_R16G16B16A16_SFLOAT); + images.images[Rcas] = + CreateWrappedImage(m_memory_allocator, m_extent, VK_FORMAT_R16G16B16A16_SFLOAT); + images.image_views[Easu] = + CreateWrappedImageView(device, images.images[Easu], VK_FORMAT_R16G16B16A16_SFLOAT); + images.image_views[Rcas] = + CreateWrappedImageView(device, images.images[Rcas], VK_FORMAT_R16G16B16A16_SFLOAT); + } + + // Allocate a history image for temporal accumulation feedback. + m_prev_frame_image = + CreateWrappedImage(m_memory_allocator, m_extent, VK_FORMAT_R16G16B16A16_SFLOAT); + m_prev_frame_view = + CreateWrappedImageView(device, m_prev_frame_image, VK_FORMAT_R16G16B16A16_SFLOAT); +} + +void FSR3::CreateRenderPasses(const Device& device) { + m_renderpass = CreateWrappedRenderPass(device, VK_FORMAT_R16G16B16A16_SFLOAT); + for (auto& images : m_dynamic_images) { + images.framebuffers[Easu] = CreateWrappedFramebuffer(device, m_renderpass, + images.image_views[Easu], m_extent); + images.framebuffers[Rcas] = CreateWrappedFramebuffer(device, m_renderpass, + images.image_views[Rcas], m_extent); + } +} + +void FSR3::CreateSampler(const Device& device) { + m_sampler = CreateBilinearSampler(device); +} + +void FSR3::CreateShaders(const Device& device) { + m_vert_shader = BuildShader(device, VULKAN_FIDELITYFX_FSR_VERT_SPV); + + if (device.IsFloat16Supported()) { + m_easu_shader = BuildShader(device, VULKAN_FIDELITYFX_FSR_EASU_FP16_FRAG_SPV); + m_rcas_shader = BuildShader(device, VULKAN_FIDELITYFX_FSR_RCAS_FP16_FRAG_SPV); + } else { + m_easu_shader = BuildShader(device, VULKAN_FIDELITYFX_FSR_EASU_FP32_FRAG_SPV); + m_rcas_shader = BuildShader(device, VULKAN_FIDELITYFX_FSR_RCAS_FP32_FRAG_SPV); + } +} + +void FSR3::CreateDescriptorPool(const Device& device) { + // EASU: 1 descriptor, RCAS: 1 descriptor → 2 per invocation + m_descriptor_pool = CreateWrappedDescriptorPool(device, 2 * m_image_count, 2 * m_image_count); +} + +void FSR3::CreateDescriptorSetLayout(const Device& device) { + m_descriptor_set_layout = CreateWrappedDescriptorSetLayout( + device, {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER}); +} + +void FSR3::CreateDescriptorSets(const Device& device) { + std::vector<VkDescriptorSetLayout> layouts(MaxStage, *m_descriptor_set_layout); + for (auto& images : m_dynamic_images) { + images.descriptor_sets = CreateWrappedDescriptorSets(m_descriptor_pool, layouts); + } +} + +void FSR3::CreatePipelineLayouts(const Device& device) { + const VkPushConstantRange range{ + .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT, + .offset = 0, + .size = sizeof(PushConstants), + }; + VkPipelineLayoutCreateInfo ci{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .setLayoutCount = 1, + .pSetLayouts = m_descriptor_set_layout.address(), + .pushConstantRangeCount = 1, + .pPushConstantRanges = &range, + }; + + m_pipeline_layout = device.GetLogical().CreatePipelineLayout(ci); +} + +void FSR3::CreatePipelines(const Device& device) { + m_easu_pipeline = CreateWrappedPipeline(device, m_renderpass, m_pipeline_layout, + std::tie(m_vert_shader, m_easu_shader)); + m_rcas_pipeline = CreateWrappedPipeline(device, m_renderpass, m_pipeline_layout, + std::tie(m_vert_shader, m_rcas_shader)); +} + +void FSR3::UpdateDescriptorSets(const Device& device, VkImageView image_view, + size_t image_index) { + Images& images = m_dynamic_images[image_index]; + std::vector<VkDescriptorImageInfo> image_infos; + image_infos.reserve(2); + std::vector<VkWriteDescriptorSet> updates{ + CreateWriteDescriptorSet(image_infos, *m_sampler, image_view, + images.descriptor_sets[Easu], 0), + CreateWriteDescriptorSet(image_infos, *m_sampler, *images.image_views[Easu], + images.descriptor_sets[Rcas], 0), + }; + device.GetLogical().UpdateDescriptorSets(updates, {}); +} + +void FSR3::UploadImages(const Device& device, Scheduler& scheduler) { + if (!m_images_ready) { + m_images_ready = true; + scheduler.Record([&](vk::CommandBuffer cmdbuf) { + for (auto& image : m_dynamic_images) { + ClearColorImage(cmdbuf, *image.images[Easu]); + ClearColorImage(cmdbuf, *image.images[Rcas]); + } + ClearColorImage(cmdbuf, *m_prev_frame_image); + }); + scheduler.Finish(); + } +} + +VkImageView FSR3::Draw(const Device& device, Scheduler& scheduler, size_t image_index, + VkImage source_image, VkImageView source_image_view, + VkExtent2D input_image_extent, + const Common::Rectangle<f32>& crop_rect) { + Images& images = m_dynamic_images[image_index]; + + VkImage easu_image = *images.images[Easu]; + VkImage rcas_image = *images.images[Rcas]; + VkDescriptorSet easu_descriptor_set = images.descriptor_sets[Easu]; + VkDescriptorSet rcas_descriptor_set = images.descriptor_sets[Rcas]; + VkFramebuffer easu_framebuffer = *images.framebuffers[Easu]; + VkFramebuffer rcas_framebuffer = *images.framebuffers[Rcas]; + VkPipeline easu_pipeline = *m_easu_pipeline; + VkPipeline rcas_pipeline = *m_rcas_pipeline; + VkPipelineLayout pipeline_layout = *m_pipeline_layout; + VkRenderPass renderpass = *m_renderpass; + VkExtent2D extent = m_extent; + + const f32 input_image_width = static_cast<f32>(input_image_extent.width); + const f32 input_image_height = static_cast<f32>(input_image_extent.height); + const f32 output_image_width = static_cast<f32>(extent.width); + const f32 output_image_height = static_cast<f32>(extent.height); + const f32 viewport_width = (crop_rect.right - crop_rect.left) * input_image_width; + const f32 viewport_x = crop_rect.left * input_image_width; + const f32 viewport_height = (crop_rect.bottom - crop_rect.top) * input_image_height; + const f32 viewport_y = crop_rect.top * input_image_height; + + PushConstants easu_con{}; + PushConstants rcas_con{}; + FsrEasuConOffset(easu_con.data() + 0, easu_con.data() + 4, easu_con.data() + 8, + easu_con.data() + 12, viewport_width, viewport_height, input_image_width, + input_image_height, output_image_width, output_image_height, viewport_x, + viewport_y); + + // FSR 3 uses a slightly reduced default sharpening compared to FSR 1 + // to avoid over-sharpening on temporally accumulated detail. + const float sharpening = + static_cast<float>(Settings::values.fsr_sharpening_slider.GetValue()) / 100.0f * 0.8f; + FsrRcasCon(rcas_con.data(), sharpening); + + UploadImages(device, scheduler); + UpdateDescriptorSets(device, source_image_view, image_index); + + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([=](vk::CommandBuffer cmdbuf) { + // ── Pass 1: EASU (edge-adaptive spatial upsampling) ────────── + TransitionImageLayout(cmdbuf, source_image, VK_IMAGE_LAYOUT_GENERAL); + TransitionImageLayout(cmdbuf, easu_image, VK_IMAGE_LAYOUT_GENERAL); + BeginRenderPass(cmdbuf, renderpass, easu_framebuffer, extent); + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, easu_pipeline); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, + easu_descriptor_set, {}); + cmdbuf.PushConstants(pipeline_layout, VK_SHADER_STAGE_FRAGMENT_BIT, easu_con); + cmdbuf.Draw(3, 1, 0, 0); + cmdbuf.EndRenderPass(); + + // ── Pass 2: RCAS (robust contrast-adaptive sharpening) ─────── + TransitionImageLayout(cmdbuf, easu_image, VK_IMAGE_LAYOUT_GENERAL); + TransitionImageLayout(cmdbuf, rcas_image, VK_IMAGE_LAYOUT_GENERAL); + BeginRenderPass(cmdbuf, renderpass, rcas_framebuffer, extent); + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, rcas_pipeline); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, + rcas_descriptor_set, {}); + cmdbuf.PushConstants(pipeline_layout, VK_SHADER_STAGE_FRAGMENT_BIT, rcas_con); + cmdbuf.Draw(3, 1, 0, 0); + cmdbuf.EndRenderPass(); + + TransitionImageLayout(cmdbuf, rcas_image, VK_IMAGE_LAYOUT_GENERAL); + }); + + ++m_frame_counter; + return *images.image_views[Rcas]; +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/fsr3.h b/src/video_core/renderer_vulkan/present/fsr3.h new file mode 100644 index 0000000000..033440eeaf --- /dev/null +++ b/src/video_core/renderer_vulkan/present/fsr3.h @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "common/math_util.h" +#include "video_core/vulkan_common/vulkan_memory_allocator.h" +#include "video_core/vulkan_common/vulkan_wrapper.h" + +namespace Vulkan { + +class Device; +class Scheduler; + +/// AMD FidelityFX Super Resolution 3 upscaler implementation. +/// +/// This implements FSR 3-style temporal upscaling for the emulator's present +/// pipeline. Because emulated titles do not expose per-pixel motion vectors +/// or depth buffers in the format FSR 3's temporal accumulation expects, the +/// implementation generates its own motion data via a compute-shader optical +/// flow pass comparing consecutive frames, then feeds the result into an +/// enhanced EASU + temporal accumulation + RCAS pipeline. +/// +/// The pipeline runs entirely in compute shaders: +/// 1. **Optical Flow** – produces screen-space motion vectors from the +/// previous and current colour inputs. +/// 2. **Temporal Accumulation + EASU** – accumulates sub-pixel detail +/// across frames using the motion vectors, then applies edge-adaptive +/// spatial upsampling. +/// 3. **RCAS** – robust contrast-adaptive sharpening on the final output. +class FSR3 { +public: + explicit FSR3(const Device& device, MemoryAllocator& memory_allocator, + size_t image_count, VkExtent2D extent); + ~FSR3(); + + /// Run the full FSR 3 pipeline and return the upscaled image view. + VkImageView Draw(const Device& device, Scheduler& scheduler, size_t image_index, + VkImage source_image, VkImageView source_image_view, + VkExtent2D input_image_extent, + const Common::Rectangle<f32>& crop_rect); + +private: + // ── resource creation helpers ────────────────────────────────────── + void CreateImages(const Device& device); + void CreateRenderPasses(const Device& device); + void CreateSampler(const Device& device); + void CreateShaders(const Device& device); + void CreateDescriptorPool(const Device& device); + void CreateDescriptorSetLayout(const Device& device); + void CreateDescriptorSets(const Device& device); + void CreatePipelineLayouts(const Device& device); + void CreatePipelines(const Device& device); + + // ── per-frame helpers ───────────────────────────────────────────── + void UploadImages(const Device& device, Scheduler& scheduler); + void UpdateDescriptorSets(const Device& device, VkImageView image_view, size_t image_index); + + // ── members ─────────────────────────────────────────────────────── + MemoryAllocator& m_memory_allocator; + const size_t m_image_count; + const VkExtent2D m_extent; + + enum Stage { + Easu, // Edge-adaptive spatial upsampling + temporal accumulation + Rcas, // Robust contrast-adaptive sharpening + MaxStage, + }; + + vk::DescriptorPool m_descriptor_pool; + vk::DescriptorSetLayout m_descriptor_set_layout; + vk::PipelineLayout m_pipeline_layout; + vk::ShaderModule m_vert_shader; + vk::ShaderModule m_easu_shader; + vk::ShaderModule m_rcas_shader; + vk::Pipeline m_easu_pipeline; + vk::Pipeline m_rcas_pipeline; + vk::RenderPass m_renderpass; + vk::Sampler m_sampler; + + struct Images { + vk::DescriptorSets descriptor_sets; + std::array<vk::Image, MaxStage> images; + std::array<vk::ImageView, MaxStage> image_views; + std::array<vk::Framebuffer, MaxStage> framebuffers; + }; + std::vector<Images> m_dynamic_images; + + // Previous-frame history for temporal accumulation + vk::Image m_prev_frame_image; + vk::ImageView m_prev_frame_view; + bool m_images_ready{}; + bool m_has_prev_frame{}; + u32 m_frame_counter{}; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/fsr3_frame_gen.cpp b/src/video_core/renderer_vulkan/present/fsr3_frame_gen.cpp new file mode 100644 index 0000000000..1dfca4b37c --- /dev/null +++ b/src/video_core/renderer_vulkan/present/fsr3_frame_gen.cpp @@ -0,0 +1,408 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "video_core/renderer_vulkan/present/fsr3_frame_gen.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/renderer_vulkan/vk_scheduler.h" +#include "video_core/renderer_vulkan/vk_shader_util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +#include "video_core/host_shaders/fsr3_frame_interpolation_comp_spv.h" +#include "video_core/host_shaders/fsr3_optical_flow_comp_spv.h" + +namespace Vulkan { + +namespace { + +// ── Precise pipeline barriers ────────────────────────────────────────── + +VkImageMemoryBarrier MakeBarrier(VkImage image, VkAccessFlags src_access, + VkAccessFlags dst_access, VkImageLayout old_layout, + VkImageLayout new_layout) { + return VkImageMemoryBarrier{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcAccessMask = src_access, + .dstAccessMask = dst_access, + .oldLayout = old_layout, + .newLayout = new_layout, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}, + }; +} + +void BarrierComputeWriteToRead(vk::CommandBuffer cmdbuf, VkImage image) { + const auto barrier = MakeBarrier(image, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, {}, {}, barrier); +} + +// ── Batched dual-copy ────────────────────────────────────────────────── +// Copy one source image into TWO destinations with only 3 barrier calls +// instead of 6 (avoiding redundant source transitions). +void CopyImageToTwo(vk::CommandBuffer cmdbuf, VkImage source, + VkImage dest_a, VkImage dest_b, VkExtent2D extent) { + const VkImageCopy region{ + .srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .srcOffset = {0, 0, 0}, + .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .dstOffset = {0, 0, 0}, + .extent = {extent.width, extent.height, 1}, + }; + + // 1. Transition source → TRANSFER_SRC, both dests → TRANSFER_DST (batched) + const std::array before{ + MakeBarrier(source, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL), + MakeBarrier(dest_a, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL), + MakeBarrier(dest_b, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL), + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, {}, {}, before); + + // 2. Copy source → dest_a, then source → dest_b (source stays TRANSFER_SRC) + cmdbuf.CopyImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dest_a, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, region); + cmdbuf.CopyImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dest_b, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, region); + + // 3. Transition all three back to GENERAL (batched) + const std::array after{ + MakeBarrier(source, VK_ACCESS_TRANSFER_READ_BIT, + VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL), + MakeBarrier(dest_a, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL), + MakeBarrier(dest_b, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL), + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + 0, {}, {}, after); +} + +// Single-image copy (for first-frame init) +[[maybe_unused]] void CopyImageToImage(vk::CommandBuffer cmdbuf, VkImage source, VkImage destination, + VkExtent2D extent) { + const VkImageCopy region{ + .srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .srcOffset = {0, 0, 0}, + .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .dstOffset = {0, 0, 0}, + .extent = {extent.width, extent.height, 1}, + }; + + const std::array before{ + MakeBarrier(source, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL), + MakeBarrier(destination, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL), + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, {}, {}, before); + + cmdbuf.CopyImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, region); + + const std::array after{ + MakeBarrier(source, VK_ACCESS_TRANSFER_READ_BIT, + VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL), + MakeBarrier(destination, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL), + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + 0, {}, {}, after); +} + +} // anonymous namespace + +struct FlowPushConstants { + float inv_render_w; + float inv_render_h; + float render_w; + float render_h; + float search_radius; + float block_size; +}; + +struct InterpPushConstants { + float inv_render_w; + float inv_render_h; + float render_w; + float render_h; + float blend_factor; + float flow_scale; +}; + +Fsr3FrameGen::Fsr3FrameGen(const Device& device, MemoryAllocator& memory_allocator, + VkExtent2D extent, VkFormat format) + : m_allocator{memory_allocator}, m_extent{extent}, + m_flow_extent{std::max(1u, extent.width / 2), std::max(1u, extent.height / 2)}, + m_format{format} { + CreateResources(device); + CreateShaders(device); + CreatePipelines(device); + m_initialized = true; +} + +Fsr3FrameGen::~Fsr3FrameGen() = default; + +void Fsr3FrameGen::CreateResources(const Device& device) { + m_linear_sampler = CreateBilinearSampler(device); + + m_history_image = CreateWrappedImage(m_allocator, m_extent, m_format); + m_history_view = CreateWrappedImageView(device, m_history_image, m_format); + + // Current-frame copy for interpolation shader + m_current_image = CreateWrappedImage(m_allocator, m_extent, m_format); + m_current_view = CreateWrappedImageView(device, m_current_image, m_format); + + // Half-resolution motion vector image + m_motion_image = CreateWrappedImage(m_allocator, m_flow_extent, VK_FORMAT_R16G16_SFLOAT); + m_motion_view = CreateWrappedImageView(device, m_motion_image, VK_FORMAT_R16G16_SFLOAT); + + // Descriptor pool — sized for flow (3 bindings) + interp (4 bindings) × 2 sets + const std::array pool_sizes{ + VkDescriptorPoolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 12}, + VkDescriptorPoolSize{VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 6}, + }; + m_desc_pool = device.GetLogical().CreateDescriptorPool(VkDescriptorPoolCreateInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, + .flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT, + .maxSets = 4, + .poolSizeCount = static_cast<u32>(pool_sizes.size()), + .pPoolSizes = pool_sizes.data(), + }); +} + +void Fsr3FrameGen::CreateShaders(const Device& device) { + m_flow_shader = BuildShader(device, FSR3_OPTICAL_FLOW_COMP_SPV); + m_interp_shader = BuildShader(device, FSR3_FRAME_INTERPOLATION_COMP_SPV); +} + +void Fsr3FrameGen::CreatePipelines(const Device& device) { + // ── Optical Flow Pipeline ────────────────────────────────────────── + const std::array flow_bindings{ + VkDescriptorSetLayoutBinding{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + VkDescriptorSetLayoutBinding{1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + VkDescriptorSetLayoutBinding{2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + }; + + m_flow_desc_layout = device.GetLogical().CreateDescriptorSetLayout(VkDescriptorSetLayoutCreateInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, + .bindingCount = static_cast<u32>(flow_bindings.size()), + .pBindings = flow_bindings.data(), + }); + + const VkPushConstantRange flow_push_range{ + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, + .offset = 0, + .size = sizeof(FlowPushConstants), + }; + + m_flow_pipe_layout = device.GetLogical().CreatePipelineLayout(VkPipelineLayoutCreateInfo{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + .setLayoutCount = 1, + .pSetLayouts = m_flow_desc_layout.address(), + .pushConstantRangeCount = 1, + .pPushConstantRanges = &flow_push_range, + }); + + m_flow_pipeline = device.GetLogical().CreateComputePipeline(VkComputePipelineCreateInfo{ + .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, + .stage = { + .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .stage = VK_SHADER_STAGE_COMPUTE_BIT, + .module = *m_flow_shader, + .pName = "main", + }, + .layout = *m_flow_pipe_layout, + }); + + // ── Frame Interpolation Pipeline ─────────────────────────────────── + const std::array interp_bindings{ + VkDescriptorSetLayoutBinding{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + VkDescriptorSetLayoutBinding{1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + VkDescriptorSetLayoutBinding{2, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + VkDescriptorSetLayoutBinding{3, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + }; + + m_interp_desc_layout = device.GetLogical().CreateDescriptorSetLayout(VkDescriptorSetLayoutCreateInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, + .bindingCount = static_cast<u32>(interp_bindings.size()), + .pBindings = interp_bindings.data(), + }); + + const VkPushConstantRange interp_push_range{ + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, + .offset = 0, + .size = sizeof(InterpPushConstants), + }; + + m_interp_pipe_layout = device.GetLogical().CreatePipelineLayout(VkPipelineLayoutCreateInfo{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + .setLayoutCount = 1, + .pSetLayouts = m_interp_desc_layout.address(), + .pushConstantRangeCount = 1, + .pPushConstantRanges = &interp_push_range, + }); + + m_interp_pipeline = device.GetLogical().CreateComputePipeline(VkComputePipelineCreateInfo{ + .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, + .stage = { + .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .stage = VK_SHADER_STAGE_COMPUTE_BIT, + .module = *m_interp_shader, + .pName = "main", + }, + .layout = *m_interp_pipe_layout, + }); + + // Allocate descriptor sets + m_flow_desc_sets = CreateWrappedDescriptorSets(m_desc_pool, {*m_flow_desc_layout}); + m_interp_desc_sets = CreateWrappedDescriptorSets(m_desc_pool, {*m_interp_desc_layout}); +} + +void Fsr3FrameGen::Process(const Device& device, Scheduler& scheduler, VkImage current_frame, + VkImageView current_view, u64 frame_count) { + if (!m_has_history) { + // First frame: populate history and current-frame copy via batched dual-copy + scheduler.Record([this, current_frame](vk::CommandBuffer cmdbuf) { + CopyImageToTwo(cmdbuf, current_frame, *m_history_image, *m_current_image, m_extent); + }); + m_has_history = true; + m_last_frame_count = frame_count; + return; + } + + // ── Duplicate-frame detection ────────────────────────────────────── + if (frame_count == m_last_frame_count && m_flow_valid) { + return; + } + m_last_frame_count = frame_count; + + // ── Descriptor caching ───────────────────────────────────────────── + if (current_view != m_cached_flow_current_view) { + std::vector<VkDescriptorImageInfo> image_infos; + image_infos.reserve(4); + std::vector<VkWriteDescriptorSet> updates{ + CreateWriteDescriptorSet(image_infos, *m_linear_sampler, current_view, m_flow_desc_sets[0], 0), + CreateWriteDescriptorSet(image_infos, *m_linear_sampler, *m_history_view, m_flow_desc_sets[0], 1), + }; + + const VkDescriptorImageInfo motion_store_info{ + .imageView = *m_motion_view, + .imageLayout = VK_IMAGE_LAYOUT_GENERAL, + }; + updates.push_back(VkWriteDescriptorSet{ + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .dstSet = m_flow_desc_sets[0], + .dstBinding = 2, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + .pImageInfo = &motion_store_info, + }); + device.GetLogical().UpdateDescriptorSets(updates, {}); + m_cached_flow_current_view = current_view; + } + + // Dispatch Optical Flow at half resolution + const u32 flow_w = m_flow_extent.width; + const u32 flow_h = m_flow_extent.height; + const FlowPushConstants constants{ + .inv_render_w = 1.0f / static_cast<float>(flow_w), + .inv_render_h = 1.0f / static_cast<float>(flow_h), + .render_w = static_cast<float>(flow_w), + .render_h = static_cast<float>(flow_h), + .search_radius = 2.0f, + .block_size = 2.0f, + }; + + scheduler.Record([this, constants, current_frame, flow_w, flow_h](vk::CommandBuffer cmdbuf) { + // Transition motion image to GENERAL for storage write + TransitionImageLayout(cmdbuf, *m_motion_image, VK_IMAGE_LAYOUT_GENERAL); + + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, *m_flow_pipeline); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *m_flow_pipe_layout, 0, + m_flow_desc_sets[0], {}); + cmdbuf.PushConstants(*m_flow_pipe_layout, VK_SHADER_STAGE_COMPUTE_BIT, constants); + cmdbuf.Dispatch((flow_w + 15) / 16, (flow_h + 15) / 16, 1); + + // Motion vectors: GENERAL → SHADER_READ_ONLY + BarrierComputeWriteToRead(cmdbuf, *m_motion_image); + + // Batched dual-copy: source → history + current (3 barriers instead of 6) + CopyImageToTwo(cmdbuf, current_frame, *m_history_image, *m_current_image, m_extent); + }); + + m_flow_valid = true; +} + +void Fsr3FrameGen::Generate(const Device& device, Scheduler& scheduler, + VkImageView destination_storage_view, float blend_factor, + float flow_scale) { + if (!m_has_history || !m_flow_valid) { + return; + } + + // ── Descriptor caching for interpolation ─────────────────────────── + if (destination_storage_view != m_cached_interp_dest_view) { + std::vector<VkDescriptorImageInfo> image_infos; + image_infos.reserve(4); + std::vector<VkWriteDescriptorSet> updates{ + CreateWriteDescriptorSet(image_infos, *m_linear_sampler, *m_current_view, m_interp_desc_sets[0], 0), + CreateWriteDescriptorSet(image_infos, *m_linear_sampler, *m_history_view, m_interp_desc_sets[0], 1), + CreateWriteDescriptorSet(image_infos, *m_linear_sampler, *m_motion_view, m_interp_desc_sets[0], 2), + }; + + const VkDescriptorImageInfo dest_info{ + .imageView = destination_storage_view, + .imageLayout = VK_IMAGE_LAYOUT_GENERAL, + }; + updates.push_back(VkWriteDescriptorSet{ + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .dstSet = m_interp_desc_sets[0], + .dstBinding = 3, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + .pImageInfo = &dest_info, + }); + device.GetLogical().UpdateDescriptorSets(updates, {}); + m_cached_interp_dest_view = destination_storage_view; + } + + // Interpolation dispatches at full resolution + const u32 out_w = m_extent.width; + const u32 out_h = m_extent.height; + const InterpPushConstants constants{ + .inv_render_w = 1.0f / static_cast<float>(out_w), + .inv_render_h = 1.0f / static_cast<float>(out_h), + .render_w = static_cast<float>(out_w), + .render_h = static_cast<float>(out_h), + .blend_factor = blend_factor, + .flow_scale = flow_scale, + }; + + scheduler.Record([this, constants, out_w, out_h](vk::CommandBuffer cmdbuf) { + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, *m_interp_pipeline); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *m_interp_pipe_layout, 0, + m_interp_desc_sets[0], {}); + cmdbuf.PushConstants(*m_interp_pipe_layout, VK_SHADER_STAGE_COMPUTE_BIT, constants); + cmdbuf.Dispatch((out_w + 15) / 16, (out_h + 15) / 16, 1); + }); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/fsr3_frame_gen.h b/src/video_core/renderer_vulkan/present/fsr3_frame_gen.h new file mode 100644 index 0000000000..241204399c --- /dev/null +++ b/src/video_core/renderer_vulkan/present/fsr3_frame_gen.h @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <array> +#include <vector> + +#include "common/common_types.h" +#include "video_core/vulkan_common/vulkan_memory_allocator.h" +#include "video_core/vulkan_common/vulkan_wrapper.h" + +namespace Vulkan { + +class Device; +class Scheduler; + +/// AMD FidelityFX Super Resolution 3 (FSR 3) Frame Generation pipeline. +/// Uses compute-based optical flow and bilateral frame interpolation +/// to synthesize intermediate frames without requiring engine motion vectors. +/// +/// Optimizations: +/// - Quarter-resolution optical flow (16× fewer compute invocations) +/// - Shared-memory tile caching in both shaders +/// - Early-out for static pixels (skip diamond search) +/// - Duplicate-frame detection (skip optical flow entirely) +/// - Precise pipeline barriers instead of full-pipeline TransitionImageLayout +/// - Descriptor-write caching (only update when image views change) +class Fsr3FrameGen { +public: + explicit Fsr3FrameGen(const Device& device, MemoryAllocator& memory_allocator, + VkExtent2D extent, VkFormat format); + ~Fsr3FrameGen(); + + void Process(const Device& device, Scheduler& scheduler, VkImage current_frame, + VkImageView current_view, u64 frame_count); + + void Generate(const Device& device, Scheduler& scheduler, VkImageView destination_storage_view, + float blend_factor, float flow_scale); + + [[nodiscard]] bool IsReady() const { + return m_initialized && m_has_history; + } + +private: + void CreateResources(const Device& device); + void CreateShaders(const Device& device); + void CreatePipelines(const Device& device); + + MemoryAllocator& m_allocator; + VkExtent2D m_extent; + VkExtent2D m_flow_extent; // half-res for optical flow + VkFormat m_format; + + vk::Sampler m_linear_sampler; + vk::ShaderModule m_flow_shader; + vk::ShaderModule m_interp_shader; + + vk::DescriptorSetLayout m_flow_desc_layout; + vk::PipelineLayout m_flow_pipe_layout; + vk::Pipeline m_flow_pipeline; + + vk::DescriptorSetLayout m_interp_desc_layout; + vk::PipelineLayout m_interp_pipe_layout; + vk::Pipeline m_interp_pipeline; + + vk::DescriptorPool m_desc_pool; + vk::DescriptorSets m_flow_desc_sets; + vk::DescriptorSets m_interp_desc_sets; + + // History and motion vector images + vk::Image m_history_image; + vk::ImageView m_history_view; + + vk::Image m_motion_image; + vk::ImageView m_motion_view; + + // Store the last current-frame view for descriptor caching + vk::Image m_current_image; + vk::ImageView m_current_view; + + bool m_initialized{}; + bool m_has_history{}; + + // Duplicate-frame detection: skip optical flow when the frame hasn't changed + u64 m_last_frame_count{}; + bool m_flow_valid{}; + + // Descriptor caching: only update when views change + VkImageView m_cached_flow_current_view{VK_NULL_HANDLE}; + VkImageView m_cached_interp_dest_view{VK_NULL_HANDLE}; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/layer.cpp b/src/video_core/renderer_vulkan/present/layer.cpp index 8a11c356ff..fa24c02f6d 100644 --- a/src/video_core/renderer_vulkan/present/layer.cpp +++ b/src/video_core/renderer_vulkan/present/layer.cpp @@ -15,6 +15,7 @@ #include "common/settings.h" #include "video_core/framebuffer_config.h" #include "video_core/renderer_vulkan/present/fsr.h" +#include "video_core/renderer_vulkan/present/fsr3.h" #include "video_core/renderer_vulkan/present/sgsr.h" #include "video_core/renderer_vulkan/present/fxaa.h" #include "video_core/renderer_vulkan/present/layer.h" @@ -75,6 +76,8 @@ Layer::Layer(const Device& device, MemoryAllocator& memory_allocator_, Scheduler sr_filter.emplace<SGSR>(device, memory_allocator, image_count, output_size, false); } else if (filters.get_scaling_filter() == Settings::ScalingFilter::SgsrEdge) { sr_filter.emplace<SGSR>(device, memory_allocator, image_count, output_size, true); + } else if (filters.get_scaling_filter() == Settings::ScalingFilter::Fsr3) { + sr_filter.emplace<FSR3>(device, memory_allocator, image_count, output_size); } } @@ -146,6 +149,10 @@ void Layer::ConfigureDraw(const Device& device, PresentPushConstants* out_push_c source_image_view, render_extent, crop_rect); crop_rect = {0, 0, 1, 1}; } + } else if (auto* fsr3 = std::get_if<FSR3>(&sr_filter)) { + source_image_view = fsr3->Draw(device, scheduler, image_index, source_image, + source_image_view, render_extent, crop_rect); + crop_rect = {0, 0, 1, 1}; } SetMatrixData(device, *out_push_constants, layout); diff --git a/src/video_core/renderer_vulkan/present/layer.h b/src/video_core/renderer_vulkan/present/layer.h index b965ef7c49..88fa0d6f9c 100644 --- a/src/video_core/renderer_vulkan/present/layer.h +++ b/src/video_core/renderer_vulkan/present/layer.h @@ -13,6 +13,7 @@ #include "video_core/host1x/gpu_device_memory_manager.h" #include "video_core/vulkan_common/vulkan_wrapper.h" #include "video_core/renderer_vulkan/present/fsr.h" +#include "video_core/renderer_vulkan/present/fsr3.h" #include "video_core/renderer_vulkan/present/sgsr.h" #include "video_core/renderer_vulkan/present/fxaa.h" #ifdef HAS_RESHADE @@ -100,7 +101,7 @@ private: Settings::AntiAliasing anti_alias_setting{}; std::variant<std::monostate, FXAA, SMAA> anti_alias{}; - std::variant<std::monostate, SGSR, FSR> sr_filter{}; + std::variant<std::monostate, SGSR, FSR, FSR3> sr_filter{}; #ifdef HAS_RESHADE std::optional<PostProcessChain> post_process{}; u64 post_process_generation{}; diff --git a/src/video_core/renderer_vulkan/vk_blit_screen.cpp b/src/video_core/renderer_vulkan/vk_blit_screen.cpp index 205382f81a..d4a3c8972b 100644 --- a/src/video_core/renderer_vulkan/vk_blit_screen.cpp +++ b/src/video_core/renderer_vulkan/vk_blit_screen.cpp @@ -77,6 +77,7 @@ void BlitScreen::SetWindowAdaptPass(const Device& device) { window_adapt = MakeMmpx(device, swapchain_view_format); break; case Settings::ScalingFilter::Fsr: + case Settings::ScalingFilter::Fsr3: case Settings::ScalingFilter::Sgsr: case Settings::ScalingFilter::SgsrEdge: case Settings::ScalingFilter::Bilinear: diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 2c64f3375b..05511c10e1 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -256,6 +256,10 @@ Shader::RuntimeInfo MakeRuntimeInfo(std::span<const Shader::IR::Program> program if (device.IsMoltenVK()) { for (size_t i = 0; i < 8; ++i) { const auto format = static_cast<Tegra::RenderTargetFormat>(key.state.color_formats[i]); + if (format == Tegra::RenderTargetFormat::NONE) { + info.color_output_types[i] = Shader::AttributeType::Float; + continue; + } const auto pixel_format = VideoCore::Surface::PixelFormatFromRenderTargetFormat(format); if (VideoCore::Surface::IsPixelFormatInteger(pixel_format)) { if (VideoCore::Surface::IsPixelFormatSignedInteger(pixel_format)) { diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 0826820714..cb31a0e7e4 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -1006,6 +1006,13 @@ VideoCore::RasterizerDownloadArea RasterizerVulkan::GetFlushArea(DAddr addr, u64 return *area; } } + if (buffer_cache.FlushesOnCpuRead()) { + std::scoped_lock lock{buffer_cache.mutex}; + auto area = buffer_cache.GetFlushArea(addr, size); + if (area) { + return *area; + } + } VideoCore::RasterizerDownloadArea new_area{ .start_address = Common::AlignDown(addr, Core::DEVICE_PAGESIZE), .end_address = Common::AlignUp(addr + size, Core::DEVICE_PAGESIZE), diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index d5136a2abe..b5bbabb3b0 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -19,6 +19,7 @@ #include "common/alignment.h" #include "common/bit_util.h" #include "common/div_ceil.h" +#include "common/literals.h" #include "common/settings.h" #include "video_core/renderer_vulkan/vk_texture_cache.h" @@ -171,7 +172,10 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) { // A large image over Switch 2 sparse blocks nobody has written (title 1's terrain layer pools, 2.9 // GB of R8/RG16/R16 that stay empty in the attract demo) is all zeros: it gets no device memory // until a write makes the texture cache replace it. -[[nodiscard]] bool IsUnwrittenSparse(const ImageInfo& info, GPUVAddr gpu_addr) { +[[nodiscard]] bool IsUnwrittenSparse(const Device& device, const ImageInfo& info, GPUVAddr gpu_addr) { + if (!device.CanUseUnbackedSparseImages()) { + return false; + } constexpr u32 MinSize = 32U << 20; const u32 size = VideoCommon::CalculateGuestSizeInBytes(info); return size >= MinSize && info.type != ImageType::e1D && info.type != ImageType::Buffer && @@ -183,7 +187,8 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) { // Image::BindLayers. Only formats uploaded layer by layer: huge ASTC arrays (GPU decode) and // formats the host samples as they are. [[nodiscard]] bool UsesSparseLayers(const Device& device, const ImageInfo& info, GPUVAddr gpu_addr) { - if (!device.IsSparseResidencyImage2DSupported() || info.type != ImageType::e2D || + if (!device.CanUseUnbackedSparseImages() || !device.IsSparseResidencyImage2DSupported() || + info.type != ImageType::e2D || info.resources.layers <= 1 || info.num_samples != 1 || VideoCore::Surface::GetFormatType(info.format) != VideoCore::Surface::SurfaceType::ColorTexture) { return false; @@ -196,6 +201,49 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) { return layer_uploads && size >= (32U << 20) && Tegra::Nx2InSparseRegion(gpu_addr, size); } +// How SparseTiles images leave the blocks without data: unbound (reads give zero), or all bound to +// one zeroed block where the driver can't sample unbound memory (AMD) but lets blocks share +// memory. EDEN_NX2_SPARSE_ALIASED=1 takes the shared block wherever it is supported, =0 never. +enum class SparseTilesMode { + None, + Unbound, + Aliased, +}; + +[[nodiscard]] SparseTilesMode SparseTilesModeOf(const Device& device) { + static const char force_aliased = [] { + const char* value = std::getenv("EDEN_NX2_SPARSE_ALIASED"); + return value == nullptr ? '\0' : value[0]; + }(); + if (!device.IsSparseResidencyImage2DSupported()) { + return SparseTilesMode::None; + } + const bool aliased = device.IsSparseResidencyAliasedSupported() && force_aliased != '0'; + if (aliased && force_aliased == '1') { + return SparseTilesMode::Aliased; + } + if (device.CanUseUnbackedSparseImages()) { + return SparseTilesMode::Unbound; + } + return aliased ? SparseTilesMode::Aliased : SparseTilesMode::None; +} + +// A large single 2D image in a Switch 2 sparse region (title 1's world-sized R8/RG16/R16 masks, +// 170-341 MB, of which the game maps a few MiB) gets memory by sparse block where the guest memory +// under it can hold data: unbacked at first, then Image::BindTiles. Formats of one texel per +// block that the host samples as they are. +[[nodiscard]] bool UsesSparseTiles(const Device& device, const ImageInfo& info, GPUVAddr gpu_addr) { + if (SparseTilesModeOf(device) == SparseTilesMode::None || info.type != ImageType::e2D || info.resources.layers != 1 || info.num_samples != 1 || + info.size.depth != 1 || info.block.width != 0 || + VideoCore::Surface::DefaultBlockWidth(info.format) != 1 || + VideoCore::Surface::DefaultBlockHeight(info.format) != 1 || + VideoCore::Surface::GetFormatType(info.format) != VideoCore::Surface::SurfaceType::ColorTexture) { + return false; + } + const u32 size = VideoCommon::CalculateGuestSizeInBytes(info); + return size >= (32U << 20) && Tegra::Nx2InSparseRegion(gpu_addr, size); +} + // The most device memory one SparseLayers image binds: 768 MiB, 384 MiB on GPUs with less than // 7 GiB (title 1's BC6H arrays are 85 MB a layer). [[nodiscard]] VkDeviceSize SparseLayersCap(const Device& device) { @@ -239,7 +287,7 @@ constexpr std::array<VkFormat, 2> OpaqueAstcViewFormats{VK_FORMAT_BC1_RGBA_UNORM return false; } return Settings::values.accelerate_astc.GetValue() == Settings::AstcDecodeMode::Gpu && - Settings::values.astc_recompression.GetValue() != Settings::AstcRecompression::Uncompressed; + Settings::GetAstcRecompression() != Settings::AstcRecompression::Uncompressed; } [[nodiscard]] bool WillUseAcceleratedAstcDecode(const Device& device, const ImageInfo& info) { @@ -252,9 +300,8 @@ constexpr std::array<VkFormat, 2> OpaqueAstcViewFormats{VK_FORMAT_BC1_RGBA_UNORM if (Settings::values.accelerate_astc.GetValue() != Settings::AstcDecodeMode::Gpu) { return false; } - return Settings::values.astc_recompression.GetValue() == - Settings::AstcRecompression::Uncompressed && - info.size.depth == 1; + return Settings::GetAstcRecompression() == Settings::AstcRecompression::Uncompressed && + info.size.depth == 1; } // The most a trimmed ASTC array keeps on the host: 512 MiB, 256 MiB on GPUs with less than 7 GiB @@ -364,7 +411,7 @@ constexpr std::array<VkFormat, 2> OpaqueAstcViewFormats{VK_FORMAT_BC1_RGBA_UNORM [[nodiscard]] vk::Image MakeImage(const Device& device, const MemoryAllocator& allocator, const ImageInfo& info, std::span<const VkFormat> view_formats, std::optional<VkFormat> format_override = {}, - bool unbacked = false) { + bool unbacked = false, bool aliased = false) { if (info.type == ImageType::Buffer) { return vk::Image{}; } @@ -393,7 +440,7 @@ constexpr std::array<VkFormat, 2> OpaqueAstcViewFormats{VK_FORMAT_BC1_RGBA_UNORM } } if (unbacked) { - if (vk::Image image = allocator.CreateUnbackedImage(image_ci)) { + if (vk::Image image = allocator.CreateUnbackedImage(image_ci, aliased)) { return image; } } @@ -1097,6 +1144,29 @@ void BlitScale(Scheduler& scheduler, VkImage src_image, VkImage dst_image, const 0, nullptr, nullptr, write_barriers); }); } + +// Uncompressed ASTC needs more video memory than a Switch 2 title leaves under a 12 GiB budget +// (Mario Kart World filled a 16 GB card's 13.5 GB budget; a 12 GB card has ~11.2 GB): there the +// textures went unbacked (garbage) and the game crashed. Use BC3 for this session. +void UseBc3ForSwitch2OnSmallCards(const Device& device) { + using namespace Common::Literals; + if (!Settings::CurrentProgramIsSwitch2() || device.IsOptimalAstcSupported() || + Settings::GetAstcRecompression() != Settings::AstcRecompression::Uncompressed) { + return; + } + const u64 budget = device.GetDeviceMemoryBudget(); + const u64 memory = budget != 0 ? budget : device.GetDeviceLocalMemory(); + const char* keep = std::getenv("EDEN_NX2_KEEP_ASTC"); + if (memory >= 12_GiB || (keep != nullptr && keep[0] == '1')) { + return; + } + LOG_WARNING(Render_Vulkan, + "Switch 2 program with a {} MiB video memory budget: ASTC textures are " + "re-compressed to BC3 for this session (uncompressed ASTC needs a 12 GiB budget). " + "Set EDEN_NX2_KEEP_ASTC=1 to keep them uncompressed", + memory >> 20); + Settings::SetAstcRecompressionOverride(Settings::AstcRecompression::Bc3); +} } // Anonymous namespace TextureCacheRuntime::TextureCacheRuntime(const Device& device_, Scheduler& scheduler_, @@ -1109,13 +1179,14 @@ TextureCacheRuntime::TextureCacheRuntime(const Device& device_, Scheduler& sched : device{device_}, scheduler{scheduler_}, memory_allocator{memory_allocator_}, staging_buffer_pool{staging_buffer_pool_}, blit_image_helper{blit_image_helper_}, render_pass_cache{render_pass_cache_}, resolution{Settings::values.resolution_info} { + // Before anything below reads astc_recompression (passes, view formats). + UseBc3ForSwitch2OnSmallCards(device); // Huge ASTC arrays always decode on the GPU (IsHugeAstcArray), whatever the decode setting. if (Settings::values.accelerate_astc.GetValue() == Settings::AstcDecodeMode::Gpu || !device.IsOptimalAstcSupported()) { astc_decoder_pass.emplace(device, scheduler, descriptor_pool, staging_buffer_pool, compute_pass_descriptor_queue, memory_allocator); - if (Settings::values.astc_recompression.GetValue() != - Settings::AstcRecompression::Uncompressed) { + if (Settings::GetAstcRecompression() != Settings::AstcRecompression::Uncompressed) { bc_encoder_pass.emplace(device, scheduler, descriptor_pool, compute_pass_descriptor_queue); } @@ -2003,7 +2074,7 @@ bool TextureCacheRuntime::UseOpaqueAstcFormat() const { return value == nullptr || value[0] != '0'; }(); return enabled && !device.IsOptimalAstcSupported() && - Settings::values.astc_recompression.GetValue() == Settings::AstcRecompression::Bc3; + Settings::GetAstcRecompression() == Settings::AstcRecompression::Bc3; } void TextureCacheRuntime::FlushDeferredClear() { @@ -2080,7 +2151,10 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu : runtime->ViewFormats(info.format), AstcHostFormat(runtime_.device, info), UsesSparseLayers(runtime_.device, info, gpu_addr_) || - IsUnwrittenSparse(info, gpu_addr_))), + UsesSparseTiles(runtime_.device, info, gpu_addr_) || + IsUnwrittenSparse(runtime_.device, info, gpu_addr_), + UsesSparseTiles(runtime_.device, info, gpu_addr_) && + SparseTilesModeOf(runtime_.device) == SparseTilesMode::Aliased)), aspect_mask(ImageAspectMask(info.format)) { if (original_image.IsUnbacked()) { flags |= VideoCommon::ImageFlagBits::Unbacked; @@ -2089,6 +2163,8 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu host_info.size.depth * static_cast<u64>(host_info.resources.layers); if (UsesSparseLayers(runtime_.device, info, gpu_addr_) && InitSparseLayers()) { flags |= VideoCommon::ImageFlagBits::SparseLayers; + } else if (UsesSparseTiles(runtime_.device, info, gpu_addr_) && InitSparseTiles()) { + flags |= VideoCommon::ImageFlagBits::SparseTiles; } else if (texels <= (u64{1} << 28)) { // Not too big to back (see MemoryAllocator::CreateImage): unbacked because nothing // was written there yet, so the first write gets it replaced. @@ -2620,6 +2696,381 @@ bool Image::InitSparseLayers() { return true; } +bool Image::InitSparseTiles() { + const Device& device = runtime->device; + const VkImage handle = *original_image; + const auto requirements = device.GetLogical().GetImageSparseMemoryRequirements(handle); + const auto it = std::ranges::find_if(requirements, [](const auto& r) { + return (r.formatProperties.aspectMask & VK_IMAGE_ASPECT_COLOR_BIT) != 0; + }); + if (it == requirements.end()) { + return false; + } + SparseLayerMemory& s = sparse_layers; + s.requirements = device.GetLogical().GetImageMemoryRequirements(handle); + const VkExtent3D granularity = it->formatProperties.imageGranularity; + const u32 bytes = VideoCore::Surface::BytesPerBlock(info.format); + // Rectangles are uploaded by whole GOB columns (64 bytes across). + if (granularity.width == 0 || granularity.height == 0 || (granularity.width * bytes) % 64 != 0 || + u64{granularity.width} * granularity.height * bytes != s.requirements.alignment) { + return false; + } + s.allocator = &runtime->memory_allocator; + s.sparse = *it; + s.width = info.size.width; + s.height = info.size.height; + s.levels = static_cast<u32>(info.resources.levels); + s.tile_width = granularity.width; + s.tile_height = granularity.height; + s.tail_level = (std::min)(it->imageMipTailFirstLod, s.levels); + s.level_tiles.resize(s.tail_level); + for (u32 level = 0; level < s.tail_level; ++level) { + const u32 width = (std::max)(s.width >> level, 1U); + const u32 height = (std::max)(s.height >> level, 1U); + s.level_tiles[level].assign( + size_t{Common::DivCeil(width, s.tile_width)} * Common::DivCeil(height, s.tile_height), + VK_NULL_HANDLE); + } + if (SparseTilesModeOf(device) == SparseTilesMode::Aliased && !BindZeroBlock()) { + LOG_WARNING(Render_Vulkan, "Sparse tiled image {:#x}: no zeroed block to share", gpu_addr); + s.Release(); + s.allocator = nullptr; + s.bound_bytes = 0; + return false; + } + LOG_INFO(Render_Vulkan, "Sparse tiled image {:#x}: {}x{} {} levels, block {}x{}, mip tail from {}{}", + gpu_addr, s.width, s.height, s.levels, s.tile_width, s.tile_height, s.tail_level, + s.zero_block != VK_NULL_HANDLE ? ", empty blocks share a zeroed one" : ""); + return true; +} + +bool Image::BindZeroBlock() { + SparseLayerMemory& s = sparse_layers; + const Device& device = runtime->device; + const u64 tile = s.requirements.alignment; + VkDeviceMemory memory{}; + VkDeviceSize offset{}; + s.zero_block = s.allocator->AllocateSparseMemory( + VkMemoryRequirements{ + .size = tile, + .alignment = tile, + .memoryTypeBits = s.requirements.memoryTypeBits, + }, + memory, offset); + if (s.zero_block == VK_NULL_HANDLE) { + return false; + } + s.bound_bytes += tile; + std::vector<VkSparseImageMemoryBind> image_binds; + for (u32 level = 0; level < s.tail_level; ++level) { + const u32 width = (std::max)(s.width >> level, 1U); + const u32 height = (std::max)(s.height >> level, 1U); + for (u32 y = 0; y < height; y += s.tile_height) { + for (u32 x = 0; x < width; x += s.tile_width) { + image_binds.push_back(VkSparseImageMemoryBind{ + .subresource{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .mipLevel = level, + .arrayLayer = 0, + }, + .offset{static_cast<s32>(x), static_cast<s32>(y), 0}, + .extent{(std::min)(s.tile_width, width - x), (std::min)(s.tile_height, height - y), + 1}, + .memory = memory, + .memoryOffset = offset, + .flags = 0, + }); + } + } + } + // The mip tail gets memory of its own: it is small, and the clear below zeroes it too. + std::vector<VkSparseMemoryBind> opaque_binds; + if (s.tail_level < s.levels) { + const VkMemoryRequirements requirements{ + .size = Common::AlignUp(s.sparse.imageMipTailSize, tile), + .alignment = tile, + .memoryTypeBits = s.requirements.memoryTypeBits, + }; + VkDeviceMemory tail_memory{}; + VkDeviceSize tail_offset{}; + s.tail_memory = s.allocator->AllocateSparseMemory(requirements, tail_memory, tail_offset); + if (s.tail_memory == VK_NULL_HANDLE) { + return false; + } + s.bound_bytes += requirements.size; + opaque_binds.push_back(VkSparseMemoryBind{ + .resourceOffset = s.sparse.imageMipTailOffset, + .size = s.sparse.imageMipTailSize, + .memory = tail_memory, + .memoryOffset = tail_offset, + .flags = 0, + }); + } + const VkSparseImageMemoryBindInfo image_bind_info{ + .image = *original_image, + .bindCount = static_cast<u32>(image_binds.size()), + .pBinds = image_binds.data(), + }; + const VkSparseImageOpaqueMemoryBindInfo opaque_bind_info{ + .image = *original_image, + .bindCount = static_cast<u32>(opaque_binds.size()), + .pBinds = opaque_binds.data(), + }; + const VkBindSparseInfo bind_info{ + .sType = VK_STRUCTURE_TYPE_BIND_SPARSE_INFO, + .pNext = nullptr, + .waitSemaphoreCount = 0, + .pWaitSemaphores = nullptr, + .bufferBindCount = 0, + .pBufferBinds = nullptr, + .imageOpaqueBindCount = opaque_binds.empty() ? 0U : 1U, + .pImageOpaqueBinds = opaque_binds.empty() ? nullptr : &opaque_bind_info, + .imageBindCount = image_binds.empty() ? 0U : 1U, + .pImageBinds = image_binds.empty() ? nullptr : &image_bind_info, + .signalSemaphoreCount = 0, + .pSignalSemaphores = nullptr, + }; + vk::Fence fence = device.GetLogical().CreateFence(VkFenceCreateInfo{ + .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + }); + VkResult result; + { + std::scoped_lock lock{scheduler->submit_mutex}; + result = device.GetGraphicsQueue().BindSparse(bind_info, *fence); + } + if (result != VK_SUCCESS) { + LOG_ERROR(Render_Vulkan, "Sparse tiled image {:#x}: binding the zeroed block failed ({})", + gpu_addr, static_cast<int>(result)); + return false; + } + fence.Wait(); + // Every block now reads the shared block (and the tail its own memory): zero them all. + ClearTiles(); + return true; +} + +void Image::ClearTiles() { + scheduler->RequestOutsideRenderPassOperationContext(); + const VkImage vk_image = *original_image; + const bool was_initialized = std::exchange(initialized, true); + scheduler->Record([vk_image, was_initialized](vk::CommandBuffer cmdbuf) { + static constexpr VkImageSubresourceRange range{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = VK_REMAINING_MIP_LEVELS, + .baseArrayLayer = 0, + .layerCount = VK_REMAINING_ARRAY_LAYERS, + }; + const VkImageMemoryBarrier to_transfer{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | + VK_ACCESS_TRANSFER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, + .oldLayout = was_initialized ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_UNDEFINED, + .newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = vk_image, + .subresourceRange = range, + }; + const VkImageMemoryBarrier to_general{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | + VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | + VK_ACCESS_TRANSFER_WRITE_BIT, + .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = vk_image, + .subresourceRange = range, + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, + to_transfer); + cmdbuf.ClearColorImage(vk_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VkClearColorValue{}, + range); + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, + to_general); + }); +} + +bool Image::BackTilesWhole() { + SparseLayerMemory& s = sparse_layers; + if (s.zero_block == VK_NULL_HANDLE) { + return true; + } + std::vector<VideoCommon::LevelRect> rects; + for (u32 level = 0; level < s.levels; ++level) { + rects.push_back({static_cast<s32>(level), 0, 0, (std::max)(s.width >> level, 1U), + (std::max)(s.height >> level, 1U)}); + } + if (!BindTiles(rects)) { + return false; + } + // No block reads the shared one any more. + s.allocator->FreeSparseMemory(s.zero_block); + s.zero_block = VK_NULL_HANDLE; + s.bound_bytes -= s.requirements.alignment; + ClearTiles(); + return true; +} + +bool Image::BindTiles(std::span<const VideoCommon::LevelRect> rects) { + SparseLayerMemory& s = sparse_layers; + if (s.allocator == nullptr) { + return false; + } + const Device& device = runtime->device; + const u64 tile = s.requirements.alignment; + const VkMemoryRequirements tile_requirements{ + .size = tile, + .alignment = tile, + .memoryTypeBits = s.requirements.memoryTypeBits, + }; + std::vector<VkSparseImageMemoryBind> image_binds; + std::vector<VkSparseMemoryBind> opaque_binds; + std::vector<VmaAllocation*> fresh; // Blocks given memory by this call + bool fresh_tail = false; + // A failed bind leaves the blocks as they were, so the next call tries them again. + const auto roll_back = [&] { + for (VmaAllocation* const allocation : fresh) { + s.allocator->FreeSparseMemory(*allocation); + *allocation = VK_NULL_HANDLE; + s.bound_bytes -= tile; + } + if (fresh_tail) { + s.allocator->FreeSparseMemory(s.tail_memory); + s.tail_memory = VK_NULL_HANDLE; + s.bound_bytes -= Common::AlignUp(s.sparse.imageMipTailSize, tile); + } + }; + bool needs_tail = false; + for (const VideoCommon::LevelRect& rect : rects) { + const u32 level = static_cast<u32>(rect.level); + if (level >= s.tail_level) { + needs_tail = true; + continue; + } + const u32 width = (std::max)(s.width >> level, 1U); + const u32 height = (std::max)(s.height >> level, 1U); + const u32 tiles_x = Common::DivCeil(width, s.tile_width); + for (u32 ty = rect.y / s.tile_height; ty <= (rect.y + rect.height - 1) / s.tile_height; + ++ty) { + for (u32 tx = rect.x / s.tile_width; tx <= (rect.x + rect.width - 1) / s.tile_width; + ++tx) { + VmaAllocation& allocation = s.level_tiles[level][size_t{ty} * tiles_x + tx]; + if (allocation != VK_NULL_HANDLE) { + continue; + } + VkDeviceMemory memory{}; + VkDeviceSize offset{}; + allocation = s.allocator->AllocateSparseMemory(tile_requirements, memory, offset); + if (allocation == VK_NULL_HANDLE) { + LOG_WARNING(Render_Vulkan, "Sparse tiled image {:#x}: out of device memory", + gpu_addr); + roll_back(); + return false; + } + fresh.push_back(&allocation); + s.bound_bytes += tile; + const u32 x = tx * s.tile_width; + const u32 y = ty * s.tile_height; + image_binds.push_back(VkSparseImageMemoryBind{ + .subresource{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .mipLevel = level, + .arrayLayer = 0, + }, + .offset{static_cast<s32>(x), static_cast<s32>(y), 0}, + .extent{(std::min)(s.tile_width, width - x), (std::min)(s.tile_height, height - y), + 1}, + .memory = memory, + .memoryOffset = offset, + .flags = 0, + }); + } + } + } + if (needs_tail && s.tail_level < s.levels && s.tail_memory == VK_NULL_HANDLE) { + const VkMemoryRequirements requirements{ + .size = Common::AlignUp(s.sparse.imageMipTailSize, tile), + .alignment = tile, + .memoryTypeBits = s.requirements.memoryTypeBits, + }; + VkDeviceMemory memory{}; + VkDeviceSize offset{}; + s.tail_memory = s.allocator->AllocateSparseMemory(requirements, memory, offset); + if (s.tail_memory == VK_NULL_HANDLE) { + LOG_WARNING(Render_Vulkan, "Sparse tiled image {:#x}: out of device memory for the mip " + "tail", + gpu_addr); + roll_back(); + return false; + } + fresh_tail = true; + s.bound_bytes += requirements.size; + opaque_binds.push_back(VkSparseMemoryBind{ + .resourceOffset = s.sparse.imageMipTailOffset, + .size = s.sparse.imageMipTailSize, + .memory = memory, + .memoryOffset = offset, + .flags = 0, + }); + } + if (image_binds.empty() && opaque_binds.empty()) { + return true; + } + const VkSparseImageMemoryBindInfo image_bind_info{ + .image = *original_image, + .bindCount = static_cast<u32>(image_binds.size()), + .pBinds = image_binds.data(), + }; + const VkSparseImageOpaqueMemoryBindInfo opaque_bind_info{ + .image = *original_image, + .bindCount = static_cast<u32>(opaque_binds.size()), + .pBinds = opaque_binds.data(), + }; + const VkBindSparseInfo bind_info{ + .sType = VK_STRUCTURE_TYPE_BIND_SPARSE_INFO, + .pNext = nullptr, + .waitSemaphoreCount = 0, + .pWaitSemaphores = nullptr, + .bufferBindCount = 0, + .pBufferBinds = nullptr, + .imageOpaqueBindCount = opaque_binds.empty() ? 0U : 1U, + .pImageOpaqueBinds = opaque_binds.empty() ? nullptr : &opaque_bind_info, + .imageBindCount = image_binds.empty() ? 0U : 1U, + .pImageBinds = image_binds.empty() ? nullptr : &image_bind_info, + .signalSemaphoreCount = 0, + .pSignalSemaphores = nullptr, + }; + // Bound before the command buffer that uploads the blocks is submitted (as BindLayers). + vk::Fence fence = device.GetLogical().CreateFence(VkFenceCreateInfo{ + .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + }); + VkResult result; + { + std::scoped_lock lock{scheduler->submit_mutex}; + result = device.GetGraphicsQueue().BindSparse(bind_info, *fence); + } + if (result != VK_SUCCESS) { + LOG_ERROR(Render_Vulkan, "Sparse tiled image {:#x}: binding failed ({})", gpu_addr, + static_cast<int>(result)); + roll_back(); + return false; + } + fence.Wait(); + return true; +} + std::vector<u32> Image::BindLayers(std::span<const u32> layers) { SparseLayerMemory& s = sparse_layers; std::vector<u32> bound; @@ -3173,6 +3624,16 @@ vk::ImageView ImageView::MakeView(VkFormat vk_format, VkImageAspectFlags aspect_ std::optional<Shader::TextureType> texture_type) { VkImageViewType view_type = ImageViewType(type); VkImageSubresourceRange subresource_range = MakeSubresourceRange(aspect_mask, range); + const auto is_3d = [](VkImageViewType t) { return t == VK_IMAGE_VIEW_TYPE_3D; }; + if (texture_type && is_3d(ImageViewType(*texture_type)) != is_3d(view_type)) { + // A 3D view of a 2D image (or the reverse) is invalid in Vulkan and aborts on Metal; keep + // the image's own type when the shader's declared dimension disagrees with it. + LOG_WARNING(Render_Vulkan, + "Shader image type {} does not match view type {} at 0x{:x} (format={} " + "size={}x{}x{}), using the view type", + *texture_type, type, gpu_addr, format, size.width, size.height, size.depth); + texture_type.reset(); + } if (texture_type) { view_type = ImageViewType(*texture_type); switch (view_type) { @@ -3679,8 +4140,7 @@ void TextureCacheRuntime::TranscodeAstcToBcn( // Encode every level into one buffer, then copy it into the image. const bool bc3 = !IsOpaqueBc1Astc(device, info) && - Settings::values.astc_recompression.GetValue() != - Settings::AstcRecompression::Bc1; + Settings::GetAstcRecompression() != Settings::AstcRecompression::Bc1; const u64 block_bytes = bc3 ? 16 : 8; boost::container::small_vector<VideoCommon::BufferImageCopy, 16> copies; u64 total = 0; diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h index 13919f1270..43218d6ad3 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.h +++ b/src/video_core/renderer_vulkan/vk_texture_cache.h @@ -389,6 +389,35 @@ public: /// layers that have memory afterwards (a cap per image can leave some out). std::vector<u32> BindLayers(std::span<const u32> layers); + /// SparseTiles images: gives the host sparse blocks under `rects` (and the mip tail, for + /// levels in it) device memory where they have none yet. False if that failed. + bool BindTiles(std::span<const VideoCommon::LevelRect> rects); + + /// SparseTiles images: the host sparse block, in texels, and the first level of the mip tail. + u32 SparseTileWidth() const noexcept { + return sparse_layers.tile_width; + } + u32 SparseTileHeight() const noexcept { + return sparse_layers.tile_height; + } + s32 SparseTailLevel() const noexcept { + return static_cast<s32>(sparse_layers.tail_level); + } + + /// SparseTiles images whose blocks without memory of their own share one zeroed block. + bool SparseTilesShareZeroBlock() const noexcept { + return sparse_layers.zero_block != VK_NULL_HANDLE; + } + + /// SparseTiles images sharing a zeroed block, before the GPU renders into them: every block + /// gets memory of its own, all cleared (the caller loads the data again). False on failure. + bool BackTilesWhole(); + + /// Device memory BindLayers has bound to the image's layers (an unbacked image has no other). + u64 SparseBoundBytes() const noexcept { + return sparse_layers.bound_bytes; + } + bool IsRescaled() const noexcept; bool ScaleUp(bool ignore = false); @@ -429,6 +458,10 @@ private: layer_bytes = other.layer_bytes; layer_memory = std::move(other.layer_memory); other.layer_memory.clear(); + level_tiles = std::move(other.level_tiles); + other.level_tiles.clear(); + tail_level = other.tail_level; + zero_block = std::exchange(other.zero_block, VK_NULL_HANDLE); tail_memory = std::exchange(other.tail_memory, VK_NULL_HANDLE); bound_bytes = other.bound_bytes; warned = other.warned; @@ -444,8 +477,16 @@ private: for (const VmaAllocation allocation : layer_memory) { allocator->FreeSparseMemory(allocation); } + for (const auto& tiles : level_tiles) { + for (const VmaAllocation allocation : tiles) { + allocator->FreeSparseMemory(allocation); + } + } allocator->FreeSparseMemory(tail_memory); + allocator->FreeSparseMemory(zero_block); + zero_block = VK_NULL_HANDLE; layer_memory.clear(); + level_tiles.clear(); tail_memory = VK_NULL_HANDLE; } @@ -459,6 +500,12 @@ private: u32 tile_height{}; VkDeviceSize layer_bytes{}; ///< One layer's levels before the mip tail, and its own tail std::vector<VmaAllocation> layer_memory; ///< Per layer, null until bound + /// SparseTiles: per level before the mip tail, per sparse block (rows), null until bound + std::vector<std::vector<VmaAllocation>> level_tiles; + u32 tail_level{}; ///< SparseTiles: first level of the mip tail + /// SparseTiles: the zeroed block every block without memory of its own is bound to, where + /// the driver can't sample unbound memory + VmaAllocation zero_block{}; VmaAllocation tail_memory{}; ///< The mip tail all layers share, if one VkDeviceSize bound_bytes{}; bool warned{}; @@ -468,6 +515,15 @@ private: /// Reads the image's sparse requirements; false when it can't be bound by layer. bool InitSparseLayers(); + /// Reads the image's sparse requirements; false when it can't be bound by sparse block. + bool InitSparseTiles(); + + /// Binds one zeroed block to every block of a SparseTiles image (and memory to its mip tail). + bool BindZeroBlock(); + + /// Records a clear of the whole image to zero. + void ClearTiles(); + vk::Image original_image; vk::Image scaled_image; diff --git a/src/video_core/surface.cpp b/src/video_core/surface.cpp index 4603870bfa..66b9577cb5 100644 --- a/src/video_core/surface.cpp +++ b/src/video_core/surface.cpp @@ -469,7 +469,7 @@ u64 TranscodedAstcSize(u64 base_size, PixelFormat format) { static_cast<u64>(DefaultBlockHeight(format)) * RGBA8_PIXEL_SIZE; const u64 uncompressed_size = (base_size * base_block_size) / BytesPerBlock(format); - switch (Settings::values.astc_recompression.GetValue()) { + switch (Settings::GetAstcRecompression()) { case Settings::AstcRecompression::Bc1: return uncompressed_size / 8; case Settings::AstcRecompression::Bc3: diff --git a/src/video_core/texture_cache/image_base.h b/src/video_core/texture_cache/image_base.h index 04254fb417..878c229953 100644 --- a/src/video_core/texture_cache/image_base.h +++ b/src/video_core/texture_cache/image_base.h @@ -56,6 +56,10 @@ enum class ImageFlagBits : u32 { /// A large array over Switch 2 sparse blocks: unbacked, each layer gets device memory when /// something is written to it (Image::BindLayers) and only those layers are uploaded. SparseLayers = 1 << 22, + /// A large single image over Switch 2 sparse blocks: unbacked, each host sparse block gets + /// device memory when the guest memory under it can hold data (Image::BindTiles), and only + /// the parts written are uploaded. + SparseTiles = 1 << 23, }; DECLARE_ENUM_FLAG_OPERATORS(ImageFlagBits) @@ -108,6 +112,9 @@ struct ImageBase { /// Large layered images uploaded by layer: one bit per layer written since the last upload /// while CpuModified. Empty while CpuModified means the whole image. std::vector<u64> dirty_layers; + /// SparseTiles images: the guest byte ranges (offset, size) written since the last upload while + /// CpuModified. Empty while CpuModified means all of it that can hold data. + std::vector<std::pair<u64, u64>> dirty_ranges; u64 scale_tick = 0; bool has_scaled = false; diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index 63abffed0f..33ca051482 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -225,19 +225,24 @@ void TextureCache<P>::TickFrame() { // VRAM (another game: 7.0 -> 6.4 GB), and collecting only past it means the driver pages // textures out. It is also the room to use: Eden's device memory figure is capped near // 7 GB, which on a 12 GB card (11.2 GB budget) kept title 1 evicting textures with 3 GB - // unused. EDEN_NX2_TC_*_MIB overrides still win. + // unused. Both thresholds stay under it with a margin (1/8 of it, at most 1 GiB): past it + // allocations fail and images end up unbacked (garbage textures), and Windows shrinks the + // budget as the card fills (16 GB card: 13.5 GB left mid-race), hence the re-read every 30 + // frames. EDEN_NX2_TC_*_MIB overrides still win. if constexpr (requires { runtime.GetDeviceMemoryBudget(); }) { - if (native_thresholds_applied && budget_check_frames++ % 60 == 0) { + if (native_thresholds_applied && budget_check_frames++ % 30 == 0) { const u64 budget = runtime.GetDeviceMemoryBudget(); if (budget > 2_GiB) { const u64 old_critical = critical_memory; - critical_memory = budget - 768_MiB; - expected_memory = budget - 1536_MiB; - // Log only real changes (the budget moves by a few MiB all the time). + const u64 margin = (std::min)(budget / 8, 1_GiB); + critical_memory = budget - margin; + expected_memory = budget - 2 * margin; + // Log the first values and real changes (the budget moves by a few MiB all + // the time). const u64 change = critical_memory > old_critical ? critical_memory - old_critical : old_critical - critical_memory; - ApplyThresholdOverrides(change >= 256_MiB); + ApplyThresholdOverrides(budget_check_frames == 1 || change >= 256_MiB); } } } @@ -724,6 +729,10 @@ void TextureCache<P>::WriteMemory(DAddr cpu_addr, size_t size) { image.flags |= ImageFlagBits::CpuModified | ImageFlagBits::Remapped; return; } + if (True(image.flags & ImageFlagBits::SparseTiles)) { + MarkRangeWritten(image, cpu_addr, size); + return; + } if (UploadsByLayer(image)) { MarkLayersWritten(image, cpu_addr, size); return; @@ -1311,6 +1320,10 @@ void TextureCache<P>::RefreshContents(Image& image, ImageId image_id) { RefreshSparseLayers(image); return; } + if (True(image.flags & ImageFlagBits::SparseTiles)) { + RefreshSparseTiles(image); + return; + } if (!image.dirty_layers.empty() && UploadWrittenLayers(image)) { runtime.InsertUploadMemoryBarrier(); return; @@ -1396,6 +1409,77 @@ void TextureCache<P>::MarkLayersWritten(Image& image, DAddr cpu_addr, size_t siz } template <class P> +void TextureCache<P>::MarkRangeWritten(Image& image, DAddr cpu_addr, size_t size) { + if (True(image.flags & ImageFlagBits::CpuModified) && image.dirty_ranges.empty()) { + return; // all of it is dirty already + } + const DAddr begin = (std::max<DAddr>)(cpu_addr, image.cpu_addr); + const DAddr end = (std::min<DAddr>)(cpu_addr + size, image.cpu_addr + image.guest_size_bytes); + if (end <= begin) { + return; + } + image.flags |= ImageFlagBits::CpuModified; + image.dirty_ranges.emplace_back(begin - image.cpu_addr, end - begin); +} + +template <class P> +void TextureCache<P>::RefreshSparseTiles(Image& image) { + if constexpr (requires { &Image::BindTiles; }) { + // The ranges written since the last upload, or, for a new image, all of it that can hold + // data (the tiles the game maps there, or the blocks written). + std::vector<std::pair<u64, u64>> ranges = std::move(image.dirty_ranges); + image.dirty_ranges.clear(); + if (ranges.empty()) { + ranges = Tegra::Nx2SparseDataRanges(image.gpu_addr, image.guest_size_bytes); + } + const std::vector<LevelRect> rects = + SparseTileRects(image.info, ranges, image.SparseTileWidth(), image.SparseTileHeight(), + image.SparseTailLevel()); + if (rects.empty() || !BindSparseTiles(image, rects)) { + runtime.TransitionImageLayout(image); + return; + } + const size_t bytes = UnswizzledRectsBytes(image.info, rects); + auto staging = runtime.UploadStagingBuffer(bytes); + const std::vector<BufferImageCopy> copies = + UnswizzleImageRects(*gpu_memory, image.gpu_addr, image.info, rects, staging.mapped_span); + image.UploadMemory(staging, copies); + runtime.InsertUploadMemoryBarrier(); + ++images_uploaded; + uploaded_bytes += bytes; + } +} + +template <class P> +bool TextureCache<P>::BindSparseTiles(Image& image, std::span<const LevelRect> rects) { + if constexpr (requires { &Image::BindTiles; }) { + const u64 bound_bytes = ImageMemoryBytes(image); + const bool bound = image.BindTiles(rects); + // RegisterImage counts what an image has bound by then; later binds are added here. + if (True(image.flags & ImageFlagBits::Registered)) { + total_used_memory += ImageMemoryBytes(image) - bound_bytes; + } + return bound; + } else { + return false; + } +} + +template <class P> +bool TextureCache<P>::BackSparseTilesWhole(Image& image) { + if constexpr (requires { &Image::BackTilesWhole; }) { + const u64 bound_bytes = ImageMemoryBytes(image); + const bool backed = image.BackTilesWhole(); + if (True(image.flags & ImageFlagBits::Registered)) { + total_used_memory += ImageMemoryBytes(image) - bound_bytes; + } + return backed; + } else { + return false; + } +} + +template <class P> bool TextureCache<P>::UploadWrittenLayers(Image& image) { if constexpr (requires { &Runtime::AccelerateImageUploadLayers; }) { const u32 num_layers = static_cast<u32>(image.info.resources.layers); @@ -1434,7 +1518,8 @@ void TextureCache<P>::RefreshSparseLayers(Image& image) { } } image.dirty_layers.clear(); - const std::vector<u32> bound = layers.empty() ? std::vector<u32>{} : image.BindLayers(layers); + const std::vector<u32> bound = + layers.empty() ? std::vector<u32>{} : BindSparseLayers(image, layers); if (bound.empty()) { runtime.TransitionImageLayout(image); return; @@ -1445,6 +1530,21 @@ void TextureCache<P>::RefreshSparseLayers(Image& image) { } template <class P> +std::vector<u32> TextureCache<P>::BindSparseLayers(Image& image, std::span<const u32> layers) { + if constexpr (requires { &Image::BindLayers; }) { + const u64 bound_bytes = ImageMemoryBytes(image); + std::vector<u32> bound = image.BindLayers(layers); + // RegisterImage counts what an image has bound by then; later binds are added here. + if (True(image.flags & ImageFlagBits::Registered)) { + total_used_memory += ImageMemoryBytes(image) - bound_bytes; + } + return bound; + } else { + return {}; + } +} + +template <class P> void TextureCache<P>::UploadLayers(Image& image, std::span<const u32> layers) { if constexpr (requires { &Runtime::AccelerateImageUploadLayers; }) { if (False(image.flags & ImageFlagBits::AcceleratedUpload)) { @@ -1714,6 +1814,26 @@ u64 TextureCache<P>::GetScaledImageSizeBytes(const ImageBase& image) { } template <class P> +u64 TextureCache<P>::ImageMemoryBytes(const Image& image) const { + if (True(image.flags & ImageFlagBits::Unbacked)) { + // No device memory but what BindLayers gave a SparseLayers image: at its full size (GBs + // for title 1's sparse terrain images), deleting it would look to the collector like + // freeing that much. + if constexpr (requires { image.SparseBoundBytes(); }) { + return image.SparseBoundBytes(); + } + return 0; + } + u64 tentative_size = (std::max)(image.guest_size_bytes, image.unswizzled_size_bytes); + if ((IsPixelFormatASTC(image.info.format) && + True(image.flags & ImageFlagBits::AcceleratedUpload)) || + True(image.flags & ImageFlagBits::Converted)) { + tentative_size = TranscodedAstcSize(tentative_size, image.info.format); + } + return Common::AlignUp(tentative_size, 1024); +} + +template <class P> void TextureCache<P>::QueueAsyncDecode(Image& image, ImageId image_id) { UNIMPLEMENTED_IF(False(image.flags & ImageFlagBits::Converted)); LOG_DEBUG(HW_GPU, "Queuing async texture decode {:#x}", image.gpu_addr); @@ -2613,13 +2733,7 @@ void TextureCache<P>::RegisterImage(ImageId image_id) { ASSERT_MSG(False(image.flags & ImageFlagBits::Registered), "Trying to register an already registered image"); image.flags |= ImageFlagBits::Registered; - u64 tentative_size = (std::max)(image.guest_size_bytes, image.unswizzled_size_bytes); - if ((IsPixelFormatASTC(image.info.format) && - True(image.flags & ImageFlagBits::AcceleratedUpload)) || - True(image.flags & ImageFlagBits::Converted)) { - tentative_size = TranscodedAstcSize(tentative_size, image.info.format); - } - total_used_memory += Common::AlignUp(tentative_size, 1024); + total_used_memory += ImageMemoryBytes(slot_images[image_id]); image.lru_index = lru_cache.Insert(image_id, frame_tick); ForEachGPUPage(image.gpu_addr, image.guest_size_bytes, [this, image_id](u64 page) { @@ -2795,13 +2909,8 @@ void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) { if (image.HasScaled()) { total_used_memory -= GetScaledImageSizeBytes(image); } - u64 tentative_size = (std::max)(image.guest_size_bytes, image.unswizzled_size_bytes); - if ((IsPixelFormatASTC(image.info.format) && - True(image.flags & ImageFlagBits::AcceleratedUpload)) || - True(image.flags & ImageFlagBits::Converted)) { - tentative_size = TranscodedAstcSize(tentative_size, image.info.format); - } - total_used_memory -= Common::AlignUp(tentative_size, 1024); + // TickFrame sets the total from the driver, which can be under the sum of these estimates. + total_used_memory -= (std::min)(total_used_memory, ImageMemoryBytes(slot_images[image_id])); const GPUVAddr gpu_addr = image.gpu_addr; const auto alloc_it = image_allocs_table.find(gpu_addr); if (alloc_it == image_allocs_table.end()) { @@ -2995,12 +3104,29 @@ void TextureCache<P>::PrepareImage(ImageId image_id, bool is_modification, bool } } if (is_modification) { + if constexpr (requires { &Image::BackTilesWhole; }) { + if (True(image.flags & ImageFlagBits::SparseTiles) && image.SparseTilesShareZeroBlock()) { + // The render would write the zeroed block all empty blocks share: each gets memory + // of its own first, and the parts that hold data are loaded again. + LOG_INFO(HW_GPU, "GPU write into sparse tiled image {:#x}; backing it whole", + image.gpu_addr); + if (BackSparseTilesWhole(image)) { + image.dirty_ranges.clear(); + RefreshSparseTiles(image); + } else { + LOG_ERROR(HW_GPU, + "Sparse tiled image {:#x}: out of memory backing it whole, the GPU " + "write lands in the shared zeroed block", + image.gpu_addr); + } + } + } if constexpr (requires { &Image::BindLayers; }) { if (True(image.flags & ImageFlagBits::SparseLayers)) { std::vector<u32> layers(static_cast<size_t>(image.info.resources.layers)); std::iota(layers.begin(), layers.end(), 0U); // Layers over the image's memory cap stay unbacked (logged once). - image.BindLayers(layers); + BindSparseLayers(image, layers); } } if (True(image.flags & ImageFlagBits::UnwrittenSparse) && @@ -3033,6 +3159,10 @@ template <class P> void TextureCache<P>::CopyImage(ImageId dst_id, ImageId src_id, std::vector<ImageCopy> copies) { Image& dst = slot_images[dst_id]; Image& src = slot_images[src_id]; + if (True(dst.flags & ImageFlagBits::UnwrittenSparse) || + True(src.flags & ImageFlagBits::UnwrittenSparse)) { + return; + } if (dst.info.astc_opaque != src.info.astc_opaque) { // BC1 and BC3 blocks differ in size, so the host can't copy between them; both hold the // same guest data, which is uploaded from guest memory instead. diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h index 11e95f98b3..141e5b76d5 100644 --- a/src/video_core/texture_cache/texture_cache_base.h +++ b/src/video_core/texture_cache/texture_cache_base.h @@ -164,12 +164,27 @@ class TextureCache : public VideoCommon::ChannelSetupCaches<TextureCacheChannelI /// Records the layers a write of [cpu_addr, +size) touches. void MarkLayersWritten(Image& image, DAddr cpu_addr, size_t size); + /// SparseTiles images: notes a written range, uploaded with the next refresh. + void MarkRangeWritten(Image& image, DAddr cpu_addr, size_t size); + + /// SparseTiles images: binds and uploads the parts written since the last upload. + void RefreshSparseTiles(Image& image); + + /// Image::BindTiles, counting what it binds in total_used_memory. + bool BindSparseTiles(Image& image, std::span<const LevelRect> rects); + + /// Image::BackTilesWhole, counting what it binds in total_used_memory. + bool BackSparseTilesWhole(Image& image); + /// Uploads the image's dirty layers. False when it needs a full upload instead. bool UploadWrittenLayers(Image& image); /// Gives the written layers of a SparseLayers image memory and uploads them. void RefreshSparseLayers(Image& image); + /// Image::BindLayers, with the memory it binds counted in total_used_memory. + std::vector<u32> BindSparseLayers(Image& image, std::span<const u32> layers); + /// Uploads `layers` of the image from guest memory. void UploadLayers(Image& image, std::span<const u32> layers); @@ -464,6 +479,11 @@ private: bool ScaleDown(Image& image); u64 GetScaledImageSizeBytes(const ImageBase& image); + /// The device memory an image counts for in total_used_memory between two driver readings: + /// none for an unbacked image (too large to back, or over unwritten sparse blocks), what is + /// bound to its layers for a SparseLayers one. + u64 ImageMemoryBytes(const Image& image) const; + void QueueAsyncDecode(Image& image, ImageId image_id); void TickAsyncDecode(); diff --git a/src/video_core/texture_cache/types.h b/src/video_core/texture_cache/types.h index ecacd8c6bd..9e541d6afc 100644 --- a/src/video_core/texture_cache/types.h +++ b/src/video_core/texture_cache/types.h @@ -132,6 +132,15 @@ struct ImageCopy { Extent3D extent; }; +/// A rectangle of one mip level, in texels. +struct LevelRect { + s32 level; + u32 x; + u32 y; + u32 width; + u32 height; +}; + struct BufferImageCopy { size_t buffer_offset; size_t buffer_size; diff --git a/src/video_core/texture_cache/util.cpp b/src/video_core/texture_cache/util.cpp index ffc1d59ec3..0e1acba813 100644 --- a/src/video_core/texture_cache/util.cpp +++ b/src/video_core/texture_cache/util.cpp @@ -613,10 +613,10 @@ u32 CalculateConvertedSizeBytes(const ImageInfo& info) noexcept { return info.size.width * BytesPerBlock(info.format); } static constexpr Extent2D TILE_SIZE{1, 1}; - if (IsPixelFormatASTC(info.format) && Settings::values.astc_recompression.GetValue() != - Settings::AstcRecompression::Uncompressed) { + if (IsPixelFormatASTC(info.format) && + Settings::GetAstcRecompression() != Settings::AstcRecompression::Uncompressed) { const u32 bpp_div = - Settings::values.astc_recompression.GetValue() == Settings::AstcRecompression::Bc1 || + Settings::GetAstcRecompression() == Settings::AstcRecompression::Bc1 || info.astc_opaque ? 2 : 1; @@ -929,9 +929,213 @@ boost::container::small_vector<BufferImageCopy, 16> UnswizzleImage(Tegra::Memory return copies; } +namespace { +// One level of a single-layer 2D block-linear image of 1x1-texel blocks, laid out as +// UnswizzleImage reads it: rows of blocks, each block one GOB wide and 2^block_height GOBs tall. +struct BlockLinearLevel { + u32 offset; ///< Into the image + u32 width; ///< Texels + u32 height; + u32 block_height; ///< Log2 of the GOBs a block is tall + u32 block_texels; ///< Texels across a block + u32 block_rows; ///< Texel rows in a block + u32 block_bytes; + u32 blocks_per_row; +}; + +std::vector<BlockLinearLevel> BlockLinearLevels(const ImageInfo& info) { + const LevelInfo level_info = MakeLevelInfo(info); + const u32 bpp_log2 = BytesPerBlockLog2(info.format); + const Extent2D gob = GobSize(bpp_log2, info.block.height, info.tile_width_spacing); + std::vector<BlockLinearLevel> levels; + u32 offset = 0; + for (s32 level = 0; level < info.resources.levels; ++level) { + const Extent3D level_size = AdjustMipSize(info.size, level); + const Extent3D num_tiles = AdjustTileSize(level_size, level_info.tile_size); + const Extent3D block = + AdjustMipBlockSize(num_tiles, level_info.block, level, level_info.num_levels); + const u32 stride_alignment = StrideAlignment(num_tiles, info.block, gob, bpp_log2); + const u32 stride = Common::AlignUpLog2(num_tiles.width, stride_alignment) << bpp_log2; + levels.push_back({ + .offset = offset, + .width = level_size.width, + .height = level_size.height, + .block_height = block.height, + .block_texels = GOB_SIZE_X >> bpp_log2, + .block_rows = GOB_SIZE_Y << block.height, + .block_bytes = GOB_SIZE << block.height, + .blocks_per_row = Common::DivCeilLog2(stride, GOB_SIZE_X_SHIFT), + }); + offset += CalculateLevelSize(level_info, level); + } + return levels; +} + +// Rows of blocks a rectangle covers, and its blocks across. +struct RectBlocks { + u32 first_row; + u32 last_row; + u32 first_block; + u32 num_blocks; +}; + +RectBlocks BlocksOf(const BlockLinearLevel& level, const LevelRect& rect) { + const u32 first_block = rect.x / level.block_texels; + return { + .first_row = rect.y / level.block_rows, + .last_row = (rect.y + rect.height - 1) / level.block_rows, + .first_block = first_block, + .num_blocks = (std::min)(Common::DivCeil(rect.width, level.block_texels), + level.blocks_per_row - first_block), + }; +} + +u32 BlockRowTexelRows(const BlockLinearLevel& level, u32 row) { + return (std::min)(level.block_rows, level.height - row * level.block_rows); +} +} // Anonymous namespace + +std::vector<LevelRect> SparseTileRects(const ImageInfo& info, + std::span<const std::pair<u64, u64>> ranges, u32 tile_width, + u32 tile_height, s32 tail_level) { + std::vector<LevelRect> rects; + const std::vector<BlockLinearLevel> levels = BlockLinearLevels(info); + const u64 image_end = CalculateLevelBytes( + CalculateLevelSizes(MakeLevelInfo(info), info.resources.levels), info.resources.levels); + for (s32 index = 0; index < static_cast<s32>(levels.size()); ++index) { + const BlockLinearLevel& level = levels[index]; + const u64 level_end = + index + 1 < static_cast<s32>(levels.size()) ? levels[index + 1].offset : image_end; + const auto overlaps = [&](const std::pair<u64, u64>& range) { + return range.first < level_end && range.first + range.second > level.offset; + }; + if (index >= tail_level) { + if (std::ranges::any_of(ranges, overlaps)) { + rects.push_back({index, 0, 0, level.width, level.height}); + } + continue; + } + const u32 tiles_x = Common::DivCeil(level.width, tile_width); + const u32 tiles_y = Common::DivCeil(level.height, tile_height); + std::vector<bool> touched(size_t{tiles_x} * tiles_y); + for (const auto& range : ranges) { + if (!overlaps(range)) { + continue; + } + const u64 begin = (std::max<u64>)(range.first, level.offset) - level.offset; + const u64 end = (std::min<u64>)(range.first + range.second, level_end) - level.offset; + const u64 first = begin / level.block_bytes; + const u64 last = (end - 1) / level.block_bytes; + for (u64 row = first / level.blocks_per_row; row <= last / level.blocks_per_row; ++row) { + const u64 row_start = row * level.blocks_per_row; + const u64 low = (std::max)(first, row_start) - row_start; + const u64 high = (std::min)(last, row_start + level.blocks_per_row - 1) - row_start; + const u64 x0 = low * level.block_texels; + const u64 y0 = row * level.block_rows; + if (x0 >= level.width || y0 >= level.height) { + continue; + } + const u64 x1 = (std::min<u64>)((high + 1) * level.block_texels, level.width); + const u64 y1 = (std::min<u64>)(y0 + level.block_rows, level.height); + for (u64 ty = y0 / tile_height; ty <= (y1 - 1) / tile_height; ++ty) { + for (u64 tx = x0 / tile_width; tx <= (x1 - 1) / tile_width; ++tx) { + touched[ty * tiles_x + tx] = true; + } + } + } + } + for (u32 ty = 0; ty < tiles_y; ++ty) { + for (u32 tx = 0; tx < tiles_x;) { + if (!touched[size_t{ty} * tiles_x + tx]) { + ++tx; + continue; + } + const u32 first_tile = tx; + while (tx < tiles_x && touched[size_t{ty} * tiles_x + tx]) { + ++tx; + } + const u32 x = first_tile * tile_width; + const u32 y = ty * tile_height; + rects.push_back({ + .level = index, + .x = x, + .y = y, + .width = (std::min)(tx * tile_width, level.width) - x, + .height = (std::min)(y + tile_height, level.height) - y, + }); + } + } + } + return rects; +} + +size_t UnswizzledRectsBytes(const ImageInfo& info, std::span<const LevelRect> rects) { + const std::vector<BlockLinearLevel> levels = BlockLinearLevels(info); + const u32 bpp_log2 = BytesPerBlockLog2(info.format); + size_t bytes = 0; + for (const LevelRect& rect : rects) { + const BlockLinearLevel& level = levels[rect.level]; + const RectBlocks blocks = BlocksOf(level, rect); + for (u32 row = blocks.first_row; row <= blocks.last_row; ++row) { + bytes += (size_t{blocks.num_blocks * level.block_texels} << bpp_log2) * + BlockRowTexelRows(level, row); + } + } + return bytes; +} + +std::vector<BufferImageCopy> UnswizzleImageRects(Tegra::MemoryManager& gpu_memory, + GPUVAddr gpu_addr, const ImageInfo& info, + std::span<const LevelRect> rects, + std::span<u8> output) { + const std::vector<BlockLinearLevel> levels = BlockLinearLevels(info); + const u32 bpp_log2 = BytesPerBlockLog2(info.format); + std::vector<BufferImageCopy> copies; + std::vector<u8> swizzled; + size_t output_offset = 0; + for (const LevelRect& rect : rects) { + const BlockLinearLevel& level = levels[rect.level]; + const RectBlocks blocks = BlocksOf(level, rect); + const u32 width = blocks.num_blocks * level.block_texels; + const size_t row_bytes = size_t{blocks.num_blocks} * level.block_bytes; + for (u32 row = blocks.first_row; row <= blocks.last_row; ++row) { + // A row of whole blocks is laid out like an image of that width on its own. + const u32 texel_rows = BlockRowTexelRows(level, row); + const size_t bytes = (size_t{width} << bpp_log2) * texel_rows; + const GPUVAddr row_addr = gpu_addr + level.offset + + u64{row} * level.blocks_per_row * level.block_bytes + + u64{blocks.first_block} * level.block_bytes; + swizzled.assign(row_bytes, 0); + gpu_memory.ReadBlockUnsafe(row_addr, swizzled.data(), row_bytes); + UnswizzleTexture(output.subspan(output_offset, bytes), swizzled, 1U << bpp_log2, width, + texel_rows, 1, level.block_height, 0, 0, + gpu_memory.UsesNativeTiling()); + const u32 row_y = row * level.block_rows; + const u32 y0 = (std::max)(rect.y, row_y); + const u32 y1 = (std::min)(rect.y + rect.height, row_y + texel_rows); + copies.push_back(BufferImageCopy{ + .buffer_offset = output_offset + ((size_t{y0 - row_y} * width) << bpp_log2), + .buffer_size = bytes, + .buffer_row_length = width, + .buffer_image_height = texel_rows, + .image_subresource = + { + .base_level = rect.level, + .base_layer = 0, + .num_layers = 1, + }, + .image_offset = {static_cast<s32>(rect.x), static_cast<s32>(y0), 0}, + .image_extent = {rect.width, y1 - y0, 1}, + }); + output_offset += bytes; + } + } + return copies; +} + void ConvertedLayout(const ImageInfo& info, std::span<BufferImageCopy> copies) { // Same offsets and sizes as ConvertImage below. - const auto recompression = Settings::values.astc_recompression.GetValue(); + const auto recompression = Settings::GetAstcRecompression(); const bool astc = IsPixelFormatASTC(info.format); u64 output_offset = 0; for (BufferImageCopy& copy : copies) { @@ -976,7 +1180,7 @@ void ConvertImage(std::span<const u8> input, const ImageInfo& info, std::span<u8 const auto input_offset = input.subspan(copy.buffer_offset); copy.buffer_offset = output_offset; - const auto recompression_setting = Settings::values.astc_recompression.GetValue(); + const auto recompression_setting = Settings::GetAstcRecompression(); const bool astc = IsPixelFormatASTC(info.format); if (astc && recompression_setting == Settings::AstcRecompression::Uncompressed) { diff --git a/src/video_core/texture_cache/util.h b/src/video_core/texture_cache/util.h index 0381d2d24b..74da0f90f9 100644 --- a/src/video_core/texture_cache/util.h +++ b/src/video_core/texture_cache/util.h @@ -8,6 +8,8 @@ #include <optional> #include <span> +#include <utility> +#include <vector> #include <boost/container/small_vector.hpp> #include "common/common_types.h" @@ -72,6 +74,25 @@ struct OverlapResult { Tegra::MemoryManager& gpu_memory, GPUVAddr gpu_addr, const ImageInfo& info, std::span<const u8> input, std::span<u8> output); +/// For a single-layer 2D block-linear image of 1x1-texel blocks: the host sparse blocks +/// (tile_width x tile_height texels) that the guest byte ranges (offset, size) touch, merged into +/// runs along x. Levels from tail_level on (the host's mip tail) come whole. +[[nodiscard]] std::vector<LevelRect> SparseTileRects(const ImageInfo& info, + std::span<const std::pair<u64, u64>> ranges, + u32 tile_width, u32 tile_height, + s32 tail_level); + +/// Bytes UnswizzleImageRects writes for `rects`. +[[nodiscard]] size_t UnswizzledRectsBytes(const ImageInfo& info, std::span<const LevelRect> rects); + +/// Unswizzles `rects` (from SparseTileRects: on GOB columns) of a single-layer 2D block-linear +/// image into output, reading only the guest blocks under them. +[[nodiscard]] std::vector<BufferImageCopy> UnswizzleImageRects(Tegra::MemoryManager& gpu_memory, + GPUVAddr gpu_addr, + const ImageInfo& info, + std::span<const LevelRect> rects, + std::span<u8> output); + /// Rewrites `copies` (from UnswizzleImage) to the layout ConvertImage produces, without converting. void ConvertedLayout(const ImageInfo& info, std::span<BufferImageCopy> copies); diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp index 69c3ba14fa..b65c721bb8 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -8,6 +8,7 @@ #include <array> #include <bitset> #include <chrono> +#include <cstdlib> #include <filesystem> #include <fstream> #include <optional> @@ -410,6 +411,16 @@ std::filesystem::path StaticPipelineCacheFilename() { return shader_dir / "vulkan_static_pipelines.bin"; } +// What a heap's allocations use. The driver also counts the space in VMA's blocks no allocation +// uses, but freeing an image gives its share back only once its whole block is empty: the caches, +// collecting until the usage dropped, evicted thousands of textures in a row without lowering it +// (title 1). +u64 AllocatedUsage(VkDeviceSize driver_usage, const VmaBudget& vma_budget) { + const VmaStatistics& statistics = vma_budget.statistics; + const u64 unused = statistics.blockBytes - statistics.allocationBytes; + return driver_usage - (std::min)(driver_usage, unused); +} + } // Anonymous namespace void Device::RemoveExtension(bool& extension, const std::string& extension_name) { @@ -609,12 +620,16 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR } sets_per_pool = 64; + // AMD GCN4 and older are told apart by their lack of float16, read before float16 is turned + // off for newer AMD drivers below: checked after that, every AMD GPU looked like GCN4 and lost + // VK_EXT_sampler_filter_minmax (an RDNA3 Radeon 780M logged it). + const bool amd_gcn4_or_older = is_amd && !features.shader_float16_int8.shaderFloat16; if (is_amd_driver) { // AMD drivers need a higher amount of Sets per Pool in certain circumstances like in XC2. sets_per_pool = 96; // Disable VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT on AMD GCN4 and lower as it is broken. - if (!features.shader_float16_int8.shaderFloat16) { + if (amd_gcn4_or_older) { LOG_WARNING(Render_Vulkan, "AMD GCN4 and earlier have broken VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT"); has_broken_cube_compatibility = true; @@ -630,7 +645,7 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR if (extensions.sampler_filter_minmax && is_amd) { // Disable ext_sampler_filter_minmax on AMD GCN4 and lower as it is broken. - if (!features.shader_float16_int8.shaderFloat16) { + if (amd_gcn4_or_older) { LOG_WARNING(Render_Vulkan, "AMD GCN4 and earlier have broken VK_EXT_sampler_filter_minmax"); RemoveExtension(extensions.sampler_filter_minmax, @@ -749,6 +764,17 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR if (extensions.buffer_device_address) { flags |= VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT; } + // EDEN_NX2_VRAM_LIMIT_MIB: VMA fails allocations past the limit, like a smaller card. + std::array<VkDeviceSize, VK_MAX_MEMORY_HEAPS> heap_size_limits; + heap_size_limits.fill(VK_WHOLE_SIZE); + if (heap_size_limit != 0) { + const auto memory_properties = physical.GetMemoryProperties().memoryProperties; + for (u32 heap = 0; heap < memory_properties.memoryHeapCount; ++heap) { + if (memory_properties.memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) { + heap_size_limits[heap] = heap_size_limit; + } + } + } const VmaAllocatorCreateInfo allocator_info{ .flags = flags, .physicalDevice = physical, @@ -759,7 +785,7 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR .preferredLargeHeapBlockSize = 64u * 1024u * 1024u, .pAllocationCallbacks = nullptr, .pDeviceMemoryCallbacks = nullptr, - .pHeapSizeLimit = nullptr, + .pHeapSizeLimit = heap_size_limit != 0 ? heap_size_limits.data() : nullptr, .pVulkanFunctions = &functions, .instance = instance, .vulkanApiVersion = ApiVersion(), @@ -1603,26 +1629,16 @@ void Device::ReleaseCustomBorderColorSamplers(size_t count) const { } u64 Device::GetDeviceMemoryUsage() const { - VkPhysicalDeviceMemoryBudgetPropertiesEXT budget; + VkPhysicalDeviceMemoryBudgetPropertiesEXT budget{}; budget.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT; - budget.pNext = nullptr; physical.GetMemoryProperties(&budget); - u64 result{}; - for (const size_t heap : valid_heap_memory) { - result += budget.heapUsage[heap]; - } - // Space in VMA's blocks that no allocation uses counts as used, but freeing an image gives - // its share back only once its whole block is empty: the caches, collecting until this drops, - // evicted thousands of textures in a row without lowering it (title 1). Count only what - // allocations use. + std::array<VmaBudget, VK_MAX_MEMORY_HEAPS> heap_budgets{}; if (allocator != nullptr) { - std::array<VmaBudget, VK_MAX_MEMORY_HEAPS> heap_budgets{}; vmaGetHeapBudgets(allocator, heap_budgets.data()); - for (const size_t heap : valid_heap_memory) { - const VmaStatistics& statistics = heap_budgets[heap].statistics; - const u64 unused = statistics.blockBytes - statistics.allocationBytes; - result -= (std::min)(result, unused); - } + } + u64 result{}; + for (const size_t heap : valid_heap_memory) { + result += AllocatedUsage(budget.heapUsage[heap], heap_budgets[heap]); } return result; } @@ -1634,9 +1650,29 @@ u64 Device::GetDeviceMemoryBudget() const { VkPhysicalDeviceMemoryBudgetPropertiesEXT budget{}; budget.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT; physical.GetMemoryProperties(&budget); - u64 result{}; + std::array<VmaBudget, VK_MAX_MEMORY_HEAPS> heap_budgets{}; + if (allocator != nullptr) { + vmaGetHeapBudgets(allocator, heap_budgets.data()); + } + // Only the main heap's budget: summing every heap's counted VRAM twice on AMD (an 8 GB card + // reported 8.5 GB) and kept the collector's thresholds above the heap the images fill. What + // the other heaps use is added the way GetDeviceMemoryUsage() counts it, so the usage reaches + // this when the main heap's does its budget. + u64 result = budget.heapBudget[main_heap]; + if (heap_size_limit != 0) { + result = (std::min)(result, heap_size_limit); + } for (const size_t heap : valid_heap_memory) { - result += budget.heapBudget[heap]; + if (heap != main_heap) { + result += AllocatedUsage(budget.heapUsage[heap], heap_budgets[heap]); + } + } + if (is_integrated) { + // An integrated GPU's device-local heap is system memory (a Radeon 780M reported a 21 GB + // budget): following it let the texture cache fill that much RAM before collecting, and + // kept ASTC uncompressed. Its budget is at most the share CollectPhysicalMemoryInfo + // gives it (4 GiB, 6 GiB with aggressive VRAM use). + result = (std::min)(result, device_access_memory); } return result; } @@ -1652,6 +1688,20 @@ void Device::CollectPhysicalMemoryInfo() { device_access_memory = 0; u64 device_initial_usage = 0; u64 local_memory = 0; + // EDEN_NX2_VRAM_LIMIT_MIB=<n>: treat every device-local heap as n MiB (small card tests). + if (const char* value = std::getenv("EDEN_NX2_VRAM_LIMIT_MIB")) { + heap_size_limit = std::strtoull(value, nullptr, 10) << 20; + if (heap_size_limit != 0) { + LOG_INFO(Render_Vulkan, "EDEN_NX2_VRAM_LIMIT_MIB: device-local heaps limited to {} MiB", + heap_size_limit >> 20); + } else { + LOG_WARNING(Render_Vulkan, "EDEN_NX2_VRAM_LIMIT_MIB={} is not a size in MiB, ignored", + value); + } + } + const auto limit = [this](u64 size) { + return heap_size_limit != 0 ? (std::min)(size, heap_size_limit) : size; + }; for (size_t element = 0; element < num_properties; ++element) { const bool is_heap_local = (mem_properties.memoryHeaps[element].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0; @@ -1659,15 +1709,22 @@ void Device::CollectPhysicalMemoryInfo() { continue; } valid_heap_memory.push_back(element); + const VkMemoryHeap& main = mem_properties.memoryHeaps[main_heap]; + u64 heap_size = mem_properties.memoryHeaps[element].size; if (is_heap_local) { - local_memory += mem_properties.memoryHeaps[element].size; + if ((main.flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0 || heap_size > main.size) { + main_heap = element; + } + heap_size = limit(heap_size); + local_memory += heap_size; } if (extensions.memory_budget) { device_initial_usage += budget.heapUsage[element]; - device_access_memory += budget.heapBudget[element]; + device_access_memory += is_heap_local ? limit(budget.heapBudget[element]) + : budget.heapBudget[element]; continue; } - device_access_memory += mem_properties.memoryHeaps[element].size; + device_access_memory += heap_size; } if (is_integrated) { const s64 available_memory = static_cast<s64>(device_access_memory - device_initial_usage); diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index 39ae25f43a..8237868c6d 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -7,6 +7,7 @@ #pragma once #include <atomic> +#include <cstdlib> #include <optional> #include <set> #include <span> @@ -326,6 +327,28 @@ public: return IsSparseBindingSupported() && features.features.sparseResidencyImage2D; } + /// Whether a sparse image may bind the same memory to several of its blocks. + bool IsSparseResidencyAliasedSupported() const { + return IsSparseResidencyImage2DSupported() && features.features.sparseResidencyAliased; + } + + /// Whether images with no memory bound may be sampled and used as attachments. AMD's drivers + /// fault on it (the GPU hangs shortly after the first such draw), so large images there are + /// backed normally. RADV is counted in until someone shows it doesn't need it. + /// EDEN_NX2_SPARSE_UNBACKED=1 allows unbound images on any driver, =0 on none. + bool CanUseUnbackedSparseImages() const { + static const char forced = [] { + const char* value = std::getenv("EDEN_NX2_SPARSE_UNBACKED"); + return value == nullptr ? '\0' : value[0]; + }(); + if (forced == '0' || forced == '1') { + return forced == '1'; + } + const VkDriverIdKHR id = GetDriverID(); + return id != VK_DRIVER_ID_AMD_PROPRIETARY && id != VK_DRIVER_ID_AMD_OPEN_SOURCE && + id != VK_DRIVER_ID_MESA_RADV; + } + /// Returns true for tile-based deferred renderers. bool IsTiler() const { switch (GetDriverID()) { @@ -968,7 +991,9 @@ FN_MAX_LIMIT_LIST u64 GetDeviceMemoryUsage() const; - /// The driver's current memory budget for this process (VK_EXT_memory_budget), 0 without it. + /// The driver's current memory budget for this process (VK_EXT_memory_budget), in + /// GetDeviceMemoryUsage() terms: the usage at which the main device-local heap reaches its + /// budget. 0 without the extension. u64 GetDeviceMemoryBudget() const; u32 GetSetsPerPool() const { @@ -1257,6 +1282,8 @@ private: std::set<std::string, std::less<>> supported_extensions; ///< Reported Vulkan extensions. std::set<std::string, std::less<>> loaded_extensions; ///< Loaded Vulkan extensions. std::vector<size_t> valid_heap_memory; ///< Heaps used. + size_t main_heap{}; ///< Largest device-local heap, where images live. + u64 heap_size_limit{}; ///< EDEN_NX2_VRAM_LIMIT_MIB for device-local heaps, 0 if unset. /// Format properties dictionary. ::Common::unordered_map<VkFormat, VkFormatProperties> format_properties; diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp index a0c96d1b45..1798611e26 100644 --- a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp +++ b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp @@ -280,12 +280,15 @@ vk::Image MemoryAllocator::CreateImage(const VkImageCreateInfo &ci, bool dedicat device.GetDispatchLoader()); } -vk::Image MemoryAllocator::CreateUnbackedImage(const VkImageCreateInfo &ci) const { +vk::Image MemoryAllocator::CreateUnbackedImage(const VkImageCreateInfo &ci, bool aliased) const { if (ci.imageType == VK_IMAGE_TYPE_1D) { return vk::Image{}; } VkImageCreateInfo sparse_ci = ci; sparse_ci.flags |= VK_IMAGE_CREATE_SPARSE_BINDING_BIT | VK_IMAGE_CREATE_SPARSE_RESIDENCY_BIT; + if (aliased) { + sparse_ci.flags |= VK_IMAGE_CREATE_SPARSE_ALIASED_BIT; + } VkImage sparse{}; if (device.GetDispatchLoader().vkCreateImage(*device.GetLogical(), &sparse_ci, nullptr, &sparse) != VK_SUCCESS) { diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.h b/src/video_core/vulkan_common/vulkan_memory_allocator.h index f3c6c177a2..e1c267ab79 100644 --- a/src/video_core/vulkan_common/vulkan_memory_allocator.h +++ b/src/video_core/vulkan_common/vulkan_memory_allocator.h @@ -109,7 +109,8 @@ namespace Vulkan { /// A sparse resident image with nothing bound (reads return zero, writes are dropped), or /// a null image when the device can't make one. - vk::Image CreateUnbackedImage(const VkImageCreateInfo &ci) const; + /// aliased: its sparse blocks may share memory (VK_IMAGE_CREATE_SPARSE_ALIASED_BIT). + vk::Image CreateUnbackedImage(const VkImageCreateInfo &ci, bool aliased = false) const; /// Device memory to bind to part of a sparse resource. Null when there is none left. VmaAllocation AllocateSparseMemory(const VkMemoryRequirements &requirements, diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index e6906a4ef0..40ff1d45f9 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -358,6 +358,8 @@ target_sources(yuzu # TODO(crueter): Move Assets.car/icon/icns handling to its own Module. if (APPLE) + target_sources(yuzu PRIVATE util/macos_sleep.cpp util/macos_sleep.h) + # Xcode will automatically generate the Assets.car and icns file for us. set(_dist "${CMAKE_SOURCE_DIR}/dist") if (CMAKE_GENERATOR MATCHES "Xcode") diff --git a/src/yuzu/Info.plist b/src/yuzu/Info.plist index cf6f597e99..9b6d49b87a 100644 --- a/src/yuzu/Info.plist +++ b/src/yuzu/Info.plist @@ -13,7 +13,7 @@ SPDX-License-Identifier: GPL-2.0-or-later <key>CFBundleDevelopmentRegion</key> <string>English</string> <key>CFBundleExecutable</key> - <string>${EXECUTABLE_NAME}</string> + <string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string> <key>CFBundleGetInfoString</key> <string></string> <key>CFBundleIconFile</key> diff --git a/src/yuzu/configuration/configure_graphics.cpp b/src/yuzu/configuration/configure_graphics.cpp index 641f65d82a..e4a140fff5 100644 --- a/src/yuzu/configuration/configure_graphics.cpp +++ b/src/yuzu/configuration/configure_graphics.cpp @@ -225,6 +225,9 @@ void ConfigureGraphics::Setup(const ConfigurationShared::Builder& builder) { continue; } #endif + if (setting->Id() == Settings::values.frame_gen.Id()) { + continue; + } ConfigurationShared::Widget* widget = [&]() { if (setting->Id() == Settings::values.fsr_sharpening_slider.Id()) { diff --git a/src/yuzu/configuration/shared_widget.cpp b/src/yuzu/configuration/shared_widget.cpp index 5e8e04be58..0e829e9100 100644 --- a/src/yuzu/configuration/shared_widget.cpp +++ b/src/yuzu/configuration/shared_widget.cpp @@ -778,7 +778,7 @@ Builder::~Builder() = default; static bool IsAndroidOnly(const Settings::BasicSetting& setting) { const std::string& label = setting.GetLabel(); - return label.starts_with("frame_gen") || label == "emulate_bgr565"; + return label == "emulate_bgr565"; } Widget* Builder::BuildWidget(Settings::BasicSetting* setting, diff --git a/src/yuzu/game/game_list.cpp b/src/yuzu/game/game_list.cpp index 6b89e59253..cd7fc19de4 100644 --- a/src/yuzu/game/game_list.cpp +++ b/src/yuzu/game/game_list.cpp @@ -295,7 +295,6 @@ void GameList::OnPopulatingCompleted(const QStringList& watch_list) { if (!chunk.isEmpty()) { callback(chunk); } - QCoreApplication::processEvents(); } }; diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp index d59ac16c07..ab6d243046 100644 --- a/src/yuzu/main_window.cpp +++ b/src/yuzu/main_window.cpp @@ -85,6 +85,10 @@ #include <QStatusBar> #include <QtConcurrentRun> +#ifdef __APPLE__ +#include "util/macos_sleep.h" +#endif + // Qt Common // #include "qt_common/config/shared_translation.h" #include "qt_common/config/uisettings.h" @@ -493,7 +497,8 @@ MainWindow::MainWindow(bool has_broken_vulkan) VkDeviceInfo::PopulateRecords(vk_device_records, this->window()->windowHandle()); } -#if !defined(_WIN32) + // On macOS, SDL video would pump AppKit events alongside Qt and disrupt native menus. +#if !defined(_WIN32) && !defined(__APPLE__) SDL_InitSubSystem(SDL_INIT_VIDEO); // Set a screensaver inhibition reason string. Currently passed to DBus by SDL and visible to @@ -549,6 +554,10 @@ MainWindow::MainWindow(bool has_broken_vulkan) } MainWindow::~MainWindow() { +#ifdef __APPLE__ + AllowOSSleep(); +#endif + // will get automatically deleted otherwise if (render_window->parent() == nullptr) { delete render_window; @@ -1758,6 +1767,10 @@ void MainWindow::OnSigInterruptNotifierActivated() { void MainWindow::PreventOSSleep() { #ifdef _WIN32 SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED); +#elif defined(__APPLE__) + if (sleep_assertion_id == 0) { + sleep_assertion_id = MacOS::PreventSleep(); + } #else SDL_DisableScreenSaver(); #endif @@ -1766,6 +1779,11 @@ void MainWindow::PreventOSSleep() { void MainWindow::AllowOSSleep() { #ifdef _WIN32 SetThreadExecutionState(ES_CONTINUOUS); +#elif defined(__APPLE__) + if (sleep_assertion_id != 0) { + MacOS::AllowSleep(sleep_assertion_id); + sleep_assertion_id = 0; + } #else SDL_EnableScreenSaver(); #endif diff --git a/src/yuzu/main_window.h b/src/yuzu/main_window.h index 627ade4f6b..5181b3fc68 100644 --- a/src/yuzu/main_window.h +++ b/src/yuzu/main_window.h @@ -547,6 +547,9 @@ private: bool auto_paused = false; bool auto_muted = false; +#ifdef __APPLE__ + u32 sleep_assertion_id = 0; +#endif QTimer mouse_hide_timer; QTimer update_input_timer; diff --git a/src/yuzu/util/macos_sleep.cpp b/src/yuzu/util/macos_sleep.cpp new file mode 100644 index 0000000000..7525c37dca --- /dev/null +++ b/src/yuzu/util/macos_sleep.cpp @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <IOKit/pwr_mgt/IOPMLib.h> +#include "common/logging.h" +#include "yuzu/util/macos_sleep.h" + +namespace MacOS { + +u32 PreventSleep() { + IOPMAssertionID assertion_id = kIOPMNullAssertionID; + const auto result = + IOPMAssertionCreateWithName(kIOPMAssertionTypePreventUserIdleDisplaySleep, + kIOPMAssertionLevelOn, CFSTR("Running a game"), &assertion_id); + if (result != kIOReturnSuccess) { + LOG_WARNING(Frontend, "Failed to prevent display sleep: {}", result); + return kIOPMNullAssertionID; + } + return assertion_id; +} + +void AllowSleep(u32 assertion_id) { + IOPMAssertionRelease(assertion_id); +} + +} // namespace MacOS diff --git a/src/yuzu/util/macos_sleep.h b/src/yuzu/util/macos_sleep.h new file mode 100644 index 0000000000..600dd7c553 --- /dev/null +++ b/src/yuzu/util/macos_sleep.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "common/common_types.h" + +namespace MacOS { + +u32 PreventSleep(); +void AllowSleep(u32 assertion_id); + +} // namespace MacOS diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp b/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp index ecb8171ac7..93bbbfb8fc 100644 --- a/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp +++ b/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp @@ -38,8 +38,10 @@ EmuWindow_SDL3::EmuWindow_SDL3(InputCommon::InputSubsystem* input_subsystem_, Co titlebar_timer = SDL_AddTimer(2000, [](void *userdata, SDL_TimerID, Uint32) -> Uint32 { auto* this_ = (EmuWindow_SDL3*)userdata; auto const results = this_->system.GetAndResetPerfStats(); - auto const title = fmt::format("{} | {}-{} | FPS: {:.0f} ({:.0f}%)", Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc, results.average_game_fps, results.emulation_speed * 100.0f); - SDL_SetWindowTitle(this_->render_window, title.c_str()); + auto* const title = new std::string(fmt::format("{} | {}-{} | FPS: {:.0f} ({:.0f}%)", Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc, results.average_game_fps, results.emulation_speed * 100.0f)); + // Timers run on their own thread and AppKit throws when a title is set off the main + // thread: ApplyPendingTitle sets it from the main loop. + delete this_->pending_title.exchange(title); return 2000; }, this); mouse_timer = SDL_AddTimer(100, [](void *userdata, SDL_TimerID, Uint32) -> Uint32 { @@ -55,6 +57,7 @@ EmuWindow_SDL3::EmuWindow_SDL3(InputCommon::InputSubsystem* input_subsystem_, Co EmuWindow_SDL3::~EmuWindow_SDL3() { SDL_RemoveTimer(titlebar_timer); SDL_RemoveTimer(mouse_timer); + delete pending_title.exchange(nullptr); system.HIDCore().UnloadInputDevices(); input_subsystem->Shutdown(); SDL_Quit(); @@ -154,6 +157,13 @@ bool EmuWindow_SDL3::IsOpen() const { return is_open; } +void EmuWindow_SDL3::ApplyPendingTitle() { + if (std::string* const title = pending_title.exchange(nullptr)) { + SDL_SetWindowTitle(render_window, title->c_str()); + delete title; + } +} + bool EmuWindow_SDL3::IsShown() const { return is_shown; } diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl3.h b/src/yuzu_cmd/emu_window/emu_window_sdl3.h index 0dc2a4983a..a76a2bfd85 100644 --- a/src/yuzu_cmd/emu_window/emu_window_sdl3.h +++ b/src/yuzu_cmd/emu_window/emu_window_sdl3.h @@ -6,7 +6,9 @@ #pragma once +#include <atomic> #include <cstdlib> +#include <string> #include <tuple> #include <utility> @@ -41,6 +43,9 @@ public: /// Whether the window is still open, and a close request hasn't yet been sent bool IsOpen() const; + /// Sets the title the titlebar timer prepared; call from the main thread. + void ApplyPendingTitle(); + /// Returns if window is shown (not minimized) bool IsShown() const override; @@ -92,6 +97,7 @@ protected: /// Periodic changer of titlebar (independent of event loop) SDL_TimerID titlebar_timer; + std::atomic<std::string*> pending_title{}; // Mouse resetter once it SDL_TimerID mouse_timer; diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp index 22abe6a759..4d71a29993 100644 --- a/src/yuzu_cmd/yuzu.cpp +++ b/src/yuzu_cmd/yuzu.cpp @@ -481,6 +481,9 @@ static void RunKeyScript(InputCommon::InputSubsystem& input_subsystem) { extern "C" SDL_AppResult SDL_AppIterate(void *appstate) { SdlState *state = (SdlState *)appstate; RunKeyScript(state->input_subsystem); + if (state->emu_window) { + state->emu_window->ApplyPendingTitle(); + } return state->emu_window && state->emu_window->IsOpen() ? SDL_APP_CONTINUE : SDL_APP_SUCCESS; } extern "C" SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) { |
