From d9b77cc21ed425a6b7c7d91c806c1c90146b1de7 Mon Sep 17 00:00:00 2001 From: Eric Warmenhoven Date: Thu, 19 Feb 2026 17:30:25 -0500 Subject: [PATCH 01/42] Implement libretro core (#1215) * libretro core * Bringing citra libretro implementation over * libretro: hook up vulkan renderer * libretro: github actions * libretro: gyro * libretro: core options v2 * libretro: on ios turn off shader jit if unavailable * moltenvk 1.3.0 introduces 8-bit indexes but allocates 16-bit for metal; this ends up allocating stream buffer * 2 = 132MiB. Instead, just use 16-bit indexes. (This will be necessary for standalone when bumping moltenvk version.) * libretro core: address review feedback * libretro: microphone support * cmake: Add ENABLE_ROOM_STANDALONE to list of incompatible libretro flags * libretro: proper initial geometry * libretro: fix software renderer * libretro: address review feedback * .github/libretro.yml: Pin macOS runners at macOS 26 * ci: Remove explicit selection of Xcode 16.0 * .github/libretro.yml: remove unnecessary windows builder apt commands * .github/libretro.yml: bump min macos version to 11.0 * ci: Re-enable CI jobs for all libretro cores This is under the condition that we don't introduce build cache for these builds --------- Co-authored-by: OpenSauce04 Co-authored-by: PabloMK7 --- .github/workflows/libretro.yml | 166 +++ .gitlab-ci.yml | 127 +++ .gitmodules | 3 + CMakeLists.txt | 48 +- externals/CMakeLists.txt | 9 + externals/libretro-common/CMakeLists.txt | 16 + externals/libretro-common/libretro-common | 1 + src/CMakeLists.txt | 14 +- src/audio_core/CMakeLists.txt | 1 + src/audio_core/dsp_interface.cpp | 12 +- src/audio_core/input_details.cpp | 17 +- src/audio_core/input_details.h | 3 +- src/audio_core/libretro_input.cpp | 327 ++++++ src/audio_core/libretro_input.h | 36 + src/audio_core/libretro_sink.cpp | 27 + src/audio_core/libretro_sink.h | 33 + src/audio_core/sink.h | 19 +- src/audio_core/sink_details.cpp | 12 +- src/audio_core/sink_details.h | 3 +- src/citra_libretro/CMakeLists.txt | 97 ++ src/citra_libretro/citra_libretro.cpp | 717 ++++++++++++ src/citra_libretro/citra_libretro.h | 10 + src/citra_libretro/core_settings.cpp | 1014 +++++++++++++++++ src/citra_libretro/core_settings.h | 41 + .../emu_window/libretro_window.cpp | 342 ++++++ .../emu_window/libretro_window.h | 79 ++ src/citra_libretro/environment.cpp | 281 +++++ src/citra_libretro/environment.h | 129 +++ src/citra_libretro/input/input_factory.cpp | 216 ++++ src/citra_libretro/input/input_factory.h | 20 + src/citra_libretro/input/mouse_tracker.cpp | 440 +++++++ src/citra_libretro/input/mouse_tracker.h | 111 ++ src/citra_libretro/libretro.osx.def | 27 + src/citra_libretro/libretro_vk.cpp | 860 ++++++++++++++ src/citra_libretro/libretro_vk.h | 175 +++ src/common/CMakeLists.txt | 12 +- src/common/error.cpp | 8 +- src/common/file_util.cpp | 124 +- src/common/file_util.h | 33 +- src/common/logging/backend.cpp | 89 +- src/common/logging/backend.h | 8 +- src/common/zstd_compression.cpp | 1 - src/core/core.h | 4 + src/core/frontend/emu_window.h | 11 + src/core/hle/service/cfg/cfg.cpp | 8 + src/core/hle/service/mic/mic_u.cpp | 6 +- src/core/hle/service/soc/soc_u.cpp | 4 +- src/core/savestate.cpp | 75 +- src/tests/CMakeLists.txt | 4 + src/video_core/CMakeLists.txt | 10 +- src/video_core/gpu.cpp | 48 + src/video_core/gpu.h | 6 + .../renderer_opengl/gl_shader_disk_cache.cpp | 23 + .../renderer_opengl/renderer_opengl.cpp | 12 +- .../renderer_vulkan/renderer_vulkan.cpp | 6 +- .../renderer_vulkan/renderer_vulkan.h | 8 + .../renderer_vulkan/vk_instance.cpp | 19 +- src/video_core/renderer_vulkan/vk_instance.h | 20 +- .../renderer_vulkan/vk_resource_pool.cpp | 9 +- .../renderer_vulkan/vk_scheduler.cpp | 9 +- .../renderer_vulkan/vk_texture_runtime.cpp | 10 +- 61 files changed, 5907 insertions(+), 93 deletions(-) create mode 100644 .github/workflows/libretro.yml create mode 100644 .gitlab-ci.yml create mode 100644 externals/libretro-common/CMakeLists.txt create mode 160000 externals/libretro-common/libretro-common create mode 100644 src/audio_core/libretro_input.cpp create mode 100644 src/audio_core/libretro_input.h create mode 100644 src/audio_core/libretro_sink.cpp create mode 100644 src/audio_core/libretro_sink.h create mode 100644 src/citra_libretro/CMakeLists.txt create mode 100644 src/citra_libretro/citra_libretro.cpp create mode 100644 src/citra_libretro/citra_libretro.h create mode 100644 src/citra_libretro/core_settings.cpp create mode 100644 src/citra_libretro/core_settings.h create mode 100644 src/citra_libretro/emu_window/libretro_window.cpp create mode 100644 src/citra_libretro/emu_window/libretro_window.h create mode 100644 src/citra_libretro/environment.cpp create mode 100644 src/citra_libretro/environment.h create mode 100644 src/citra_libretro/input/input_factory.cpp create mode 100644 src/citra_libretro/input/input_factory.h create mode 100644 src/citra_libretro/input/mouse_tracker.cpp create mode 100644 src/citra_libretro/input/mouse_tracker.h create mode 100644 src/citra_libretro/libretro.osx.def create mode 100644 src/citra_libretro/libretro_vk.cpp create mode 100644 src/citra_libretro/libretro_vk.h diff --git a/.github/workflows/libretro.yml b/.github/workflows/libretro.yml new file mode 100644 index 000000000..5225a43fa --- /dev/null +++ b/.github/workflows/libretro.yml @@ -0,0 +1,166 @@ +name: citra-libretro + +on: + push: + branches: [ "*" ] + tags: [ "*" ] + pull_request: + branches: [ master ] + workflow_dispatch: + +env: + CORE_ARGS: -DENABLE_LIBRETRO=ON + +jobs: + android: + runs-on: ubuntu-22.04 + env: + OS: android + TARGET: arm64-v8a + API_LEVEL: 21 + ANDROID_NDK_VERSION: 26.2.11394342 + ANDROID_ABI: arm64-v8a + BUILD_DIR: build/android-arm64-v8a + EXTRA_PATH: bin/Release + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Set tag name + run: | + if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then + echo "GIT_TAG_NAME=$GITHUB_REF_NAME" >> $GITHUB_ENV + fi + echo $GIT_TAG_NAME + - name: Update Android SDK CMake version + run: | + echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "ndk;$ANDROID_NDK_VERSION" + echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "cmake;3.30.3" + - name: Build + run: | + export NDK_ROOT=${ANDROID_SDK_ROOT}/ndk/$ANDROID_NDK_VERSION + ${ANDROID_SDK_ROOT}/cmake/3.30.3/bin/cmake $CORE_ARGS -DANDROID_PLATFORM=android-$API_LEVEL -DCMAKE_TOOLCHAIN_FILE=$NDK_ROOT/build/cmake/android.toolchain.cmake -DANDROID_STL=c++_static -DANDROID_ABI=$ANDROID_ABI . -B $BUILD_DIR + ${ANDROID_SDK_ROOT}/cmake/3.30.3/bin/cmake --build $BUILD_DIR --target azahar_libretro --config Release -j $(nproc) + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: ${{ env.OS }}-${{ env.TARGET }} + path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro_android.so + linux: + runs-on: ubuntu-22.04 + env: + OS: linux + TARGET: x86_64 + BUILD_DIR: build/linux-x86_64 + EXTRA_PATH: bin/Release + EXTRA_CORE_ARGS: -DCMAKE_C_COMPILER=gcc-12 -DCMAKE_CXX_COMPILER=g++-12 -DENABLE_LTO=OFF + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Build + run: | + cmake $CORE_ARGS $EXTRA_CORE_ARGS . -B $BUILD_DIR + cmake --build $BUILD_DIR --target azahar_libretro --config Release -j $(nproc) + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: ${{ env.OS }}-${{ env.TARGET }} + path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.so + windows: + runs-on: ubuntu-latest + env: + OS: windows + TARGET: x86_64 + BUILD_DIR: build/windows-x86_64 + EXTRA_CORE_ARGS: -DENABLE_LTO=OFF -G Ninja + CMAKE: x86_64-w64-mingw32.static-cmake + IMAGE: git.libretro.com:5050/libretro-infrastructure/libretro-build-mxe-win-cross-cores:mingw12 + EXTRA_PATH: bin/Release + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Build in cross-container + run: | + docker pull $IMAGE + docker run --rm --user root \ + -v "${GITHUB_WORKSPACE}:${GITHUB_WORKSPACE}" \ + -w "${GITHUB_WORKSPACE}" \ + $IMAGE \ + bash -lc "\ + ${CMAKE} $CORE_ARGS $EXTRA_CORE_ARGS . -B $BUILD_DIR && \ + ${CMAKE} --build $BUILD_DIR --target azahar_libretro --config Release -j $(nproc)" + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: ${{ env.OS }}-${{ env.TARGET }} + path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.dll + macos: + runs-on: macos-26 + strategy: + matrix: + target: ["x86_64", "arm64"] + env: + OS: macos + TARGET: ${{ matrix.target }} + MACOSX_DEPLOYMENT_TARGET: 11.0 + BUILD_DIR: build/osx-${{ matrix.target }} + EXTRA_PATH: bin/Release + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install tools + run: brew install spirv-tools + - name: Build + run: | + cmake $CORE_ARGS -DCMAKE_OSX_ARCHITECTURES=$TARGET . -B $BUILD_DIR + cmake --build $BUILD_DIR --target azahar_libretro --config Release + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: ${{ env.OS }}-${{ env.TARGET }} + path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.dylib + ios: + runs-on: macos-26 + env: + OS: ios + TARGET: arm64 + BUILD_DIR: build/ios-arm64 + EXTRA_PATH: bin/Release + EXTRA_CORE_ARGS: -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_C_FLAGS=-DIOS -DCMAKE_CXX_FLAGS=-DIOS -DIOS=ON -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 -DCITRA_USE_PRECOMPILED_HEADERS=OFF -DCMAKE_OSX_ARCHITECTURES=arm64 -DENABLE_OPT=OFF + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Build + run: | + cmake $CORE_ARGS $EXTRA_CORE_ARGS . -B $BUILD_DIR + cmake --build $BUILD_DIR --target azahar_libretro --config Release + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: ${{ env.OS }}-${{ env.TARGET }} + path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.dylib + tvos: + runs-on: macos-26 + env: + OS: tvos + TARGET: arm64 + BUILD_DIR: build/tvos-arm64 + EXTRA_PATH: bin/Release + EXTRA_CORE_ARGS: -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_C_FLAGS=-DIOS -DCMAKE_CXX_FLAGS=-DIOS -DIOS=ON -DCMAKE_SYSTEM_NAME=tvOS -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 -DCITRA_USE_PRECOMPILED_HEADERS=OFF -DCMAKE_OSX_SYSROOT=appletvos -DCMAKE_OSX_ARCHITECTURES=arm64 -DENABLE_OPT=OFF + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Build + run: | + cmake $CORE_ARGS $EXTRA_CORE_ARGS . -B $BUILD_DIR + cmake --build $BUILD_DIR --target azahar_libretro --config Release + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: ${{ env.OS }}-${{ env.TARGET }} + path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.dylib diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..e8f4f141b --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,127 @@ +.core-defs: + variables: + JNI_PATH: . + CORENAME: azahar + API_LEVEL: 21 + BASE_CORE_ARGS: -DENABLE_LIBRETRO=ON -DENABLE_TESTS=OFF + CORE_ARGS: ${BASE_CORE_ARGS} + EXTRA_PATH: bin/Release + +variables: + STATIC_RETROARCH_BRANCH: master + GIT_SUBMODULE_STRATEGY: recursive + +# Inclusion templates, required for the build to work +include: + ################################## DESKTOPS ############################## ## + # Windows 64-bit + - project: 'libretro-infrastructure/ci-templates' + file: '/windows-cmake-mingw.yml' + + # Linux 64-bit + - project: 'libretro-infrastructure/ci-templates' + file: '/linux-cmake.yml' + + # MacOS x86_64 + - project: 'libretro-infrastructure/ci-templates' + file: '/osx-cmake-x86.yml' + + # MacOS ARM64 + - project: 'libretro-infrastructure/ci-templates' + file: '/osx-cmake-arm64.yml' + + ################################## CELLULAR ############################## ## + # Android + - project: 'libretro-infrastructure/ci-templates' + file: '/android-cmake.yml' + + # iOS + - project: 'libretro-infrastructure/ci-templates' + file: '/ios-cmake.yml' + + # tvOS + - project: 'libretro-infrastructure/ci-templates' + file: '/tvos-cmake.yml' + + ################################## CONSOLES ############################## ## + +# Stages for building +stages: + - build-prepare + - build-shared + - build-static + +############################################################################## +#################################### STAGES ################################## +############################################################################## +# +################################### DESKTOPS ################################# +# Windows 64-bit +libretro-build-windows-x64: + extends: + - .core-defs + - .libretro-windows-cmake-x86_64 + image: $CI_SERVER_HOST:5050/libretro-infrastructure/libretro-build-mxe-win-cross-cores:mingw12 + variables: + CORE_ARGS: ${BASE_CORE_ARGS} -DENABLE_LTO=OFF -G Ninja + +# Linux 64-bit +libretro-build-linux-x64: + extends: + - .core-defs + - .libretro-linux-cmake-x86_64 + image: $CI_SERVER_HOST:5050/libretro-infrastructure/libretro-build-amd64-ubuntu:backports + variables: + CORE_ARGS: ${BASE_CORE_ARGS} -DENABLE_LTO=OFF + CC: /usr/bin/gcc-12 + CXX: /usr/bin/g++-12 + +# MacOS x86_64 +libretro-build-osx-x64: + tags: + - mac-apple-silicon + variables: + CORE_ARGS: ${BASE_CORE_ARGS} -DCMAKE_OSX_ARCHITECTURES=x86_64 + MACOSX_DEPLOYMENT_TARGET: "10.15" + extends: + - .core-defs + - .libretro-osx-cmake-x86_64 + +# MacOS ARM64 +libretro-build-osx-arm64: + extends: + - .core-defs + - .libretro-osx-cmake-arm64 + +################################### CELLULAR ################################# +# Android ARMv8a +android-arm64-v8a: + extends: + - .libretro-android-cmake-arm64-v8a + - .core-defs + variables: + ANDROID_NDK_VERSION: 26.2.11394342 + NDK_ROOT: /android-sdk-linux/ndk/$ANDROID_NDK_VERSION + +# iOS arm64 +libretro-build-ios-arm64: + extends: + - .libretro-ios-cmake-arm64 + - .core-defs + variables: + CORE_ARGS: ${BASE_CORE_ARGS} -DCITRA_USE_PRECOMPILED_HEADERS=OFF -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_ARCHITECTURES=arm64 -DENABLE_OPT=OFF + IOS_MINVER: "14.0" + EXTRA_PATH: bin/RelWithDebInfo + +# tvOS arm64 +libretro-build-tvos-arm64: + extends: + - .libretro-tvos-cmake-arm64 + - .core-defs + variables: + CORE_ARGS: ${BASE_CORE_ARGS} -DCITRA_USE_PRECOMPILED_HEADERS=OFF -DIOS=ON -DCMAKE_SYSTEM_NAME=tvOS -DCMAKE_OSX_SYSROOT=appletvos -DCMAKE_OSX_ARCHITECTURES=arm64 -DENABLE_OPT=OFF + MINVER: "14.0" + EXTRA_PATH: bin/RelWithDebInfo + +################################### CONSOLES ################################# + diff --git a/.gitmodules b/.gitmodules index cb600a64c..18c5cc79a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -103,3 +103,6 @@ [submodule "externals/xxHash"] path = externals/xxHash url = https://github.com/Cyan4973/xxHash.git +[submodule "externals/libretro-common"] + path = externals/libretro-common/libretro-common + url = https://github.com/libretro/libretro-common.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a3635988..df86bf28c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,20 +17,23 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMakeModules") list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/externals/cmake-modules") include(DownloadExternals) include(CMakeDependentOption) -include(FindPkgConfig) project(citra LANGUAGES C CXX ASM) +# must be invoked after project() command when using CMAKE_TOOLCHAIN_FILE +include(FindPkgConfig) if (CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR CMAKE_SYSTEM_NAME STREQUAL "iOS") enable_language(OBJC OBJCXX) endif() +option(ENABLE_LIBRETRO "Build as a LibRetro core" OFF) + # Some submodules like to pick their own default build type if not specified. # Make sure we default to Release build type always, unless the generator has custom types. if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE) endif() -if (APPLE) +if (APPLE AND NOT ENABLE_LIBRETRO) # Silence warnings on empty objects, for example when platform-specific code is #ifdef'd out. set(CMAKE_C_ARCHIVE_CREATE " Scr ") set(CMAKE_CXX_ARCHIVE_CREATE " Scr ") @@ -90,6 +93,17 @@ else() set(DEFAULT_ENABLE_OPENGL ON) endif() +# Track which options were explicitly set by the user (for libretro conflict detection) +set(_LIBRETRO_INCOMPATIBLE_OPTIONS + ENABLE_SDL2 ENABLE_QT ENABLE_WEB_SERVICE ENABLE_SCRIPTING + ENABLE_OPENAL ENABLE_ROOM ENABLE_ROOM_STANDALONE ENABLE_CUBEB ENABLE_LIBUSB) +set(_USER_SET_OPTIONS "") +foreach(_opt IN LISTS _LIBRETRO_INCOMPATIBLE_OPTIONS) + if(DEFINED ${_opt}) + list(APPEND _USER_SET_OPTIONS ${_opt}) + endif() +endforeach() + option(ENABLE_SDL2 "Enable using SDL2" ON) CMAKE_DEPENDENT_OPTION(ENABLE_SDL2_FRONTEND "Enable the SDL2 frontend" OFF "ENABLE_SDL2;NOT ANDROID AND NOT IOS" OFF) option(USE_SYSTEM_SDL2 "Use the system SDL2 lib (instead of the bundled one)" OFF) @@ -130,6 +144,31 @@ option(ENABLE_NATIVE_OPTIMIZATION "Enables processor-specific optimizations via option(CITRA_USE_PRECOMPILED_HEADERS "Use precompiled headers" ON) option(CITRA_WARNINGS_AS_ERRORS "Enable warnings as errors" ON) +# Handle incompatible options for libretro builds +if(ENABLE_LIBRETRO) + # Check for explicitly-set conflicting options + set(_CONFLICTS "") + foreach(_opt IN LISTS _LIBRETRO_INCOMPATIBLE_OPTIONS) + list(FIND _USER_SET_OPTIONS ${_opt} _idx) + if(NOT _idx EQUAL -1 AND ${_opt}) + list(APPEND _CONFLICTS ${_opt}) + endif() + endforeach() + + if(_CONFLICTS) + string(REPLACE ";" ", " _CONFLICTS_STR "${_CONFLICTS}") + message(FATAL_ERROR + "ENABLE_LIBRETRO is incompatible with: ${_CONFLICTS_STR}\n" + "These options were explicitly enabled but are not supported for libretro builds.\n" + "Remove these options or set them to OFF.") + endif() + + # Force disable incompatible options (handles defaulted-on options) + foreach(_opt IN LISTS _LIBRETRO_INCOMPATIBLE_OPTIONS) + set(${_opt} OFF CACHE BOOL "Disabled for libretro" FORCE) + endforeach() +endif() + # Pass the following values to C++ land if (ENABLE_QT) add_definitions(-DENABLE_QT) @@ -300,6 +339,9 @@ set(CMAKE_VISIBILITY_INLINES_HIDDEN NO) # set up output paths for executable binaries set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin/$) +if (ENABLE_LIBRETRO) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) +endif() # System imported libraries # ====================== @@ -359,7 +401,7 @@ if (APPLE) find_library(IOSURFACE_LIBRARY IOSurface REQUIRED) set(PLATFORM_LIBRARIES ${COCOA_LIBRARY} ${AVFOUNDATION_LIBRARY} ${IOSURFACE_LIBRARY} ${MOLTENVK_LIBRARY}) - if (ENABLE_VULKAN) + if (ENABLE_VULKAN AND NOT ENABLE_LIBRETRO) if (NOT USE_SYSTEM_MOLTENVK) download_moltenvk() endif() diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 01e76a095..d7cf35f3b 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -292,6 +292,15 @@ if (USE_DISCORD_PRESENCE) target_include_directories(discord-rpc INTERFACE ./discord-rpc/include) endif() +# LibRetro +if (ENABLE_LIBRETRO) + add_library(libretro INTERFACE) + target_include_directories(libretro INTERFACE ./libretro-common/libretro-common/include) + if (ANDROID) + add_subdirectory(libretro-common EXCLUDE_FROM_ALL) + endif() +endif() + # JSON add_library(json-headers INTERFACE) if (USE_SYSTEM_JSON) diff --git a/externals/libretro-common/CMakeLists.txt b/externals/libretro-common/CMakeLists.txt new file mode 100644 index 000000000..0bb4e6b5e --- /dev/null +++ b/externals/libretro-common/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libretro_common STATIC + libretro-common/compat/compat_posix_string.c + libretro-common/compat/fopen_utf8.c + libretro-common/encodings/encoding_utf.c + libretro-common/compat/compat_strl.c + libretro-common/file/file_path.c + libretro-common/streams/file_stream.c + libretro-common/streams/file_stream_transforms.c + libretro-common/string/stdstring.c + libretro-common/time/rtime.c + libretro-common/vfs/vfs_implementation.c +) +target_include_directories(libretro_common PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/libretro-common + ${CMAKE_CURRENT_SOURCE_DIR}/libretro-common/include +) diff --git a/externals/libretro-common/libretro-common b/externals/libretro-common/libretro-common new file mode 160000 index 000000000..7fc7feedd --- /dev/null +++ b/externals/libretro-common/libretro-common @@ -0,0 +1 @@ +Subproject commit 7fc7feeddca391be65c94e6541381467684b814d diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 69d17bac2..b18925867 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -110,10 +110,14 @@ else() # In case a flag isn't supported on e.g. a certain architecture, don't error. -Wno-unused-command-line-argument # Build fortification options - -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong - -fstack-clash-protection ) + if (NOT ENABLE_LIBRETRO) + add_compile_options( + -Wp,-D_GLIBCXX_ASSERTIONS + -fstack-clash-protection + ) + endif() # If we define _FORTIFY_SOURCE when it is already defined, compilation will fail string(FIND "-D_FORTIFY_SOURCE" "${CMAKE_CXX_FLAGS} " FORTIFY_SOURCE_DEFINED) @@ -201,6 +205,10 @@ if (ENABLE_QT OR ENABLE_SDL2_FRONTEND) add_subdirectory(citra_meta) endif() +if (ENABLE_LIBRETRO) + add_subdirectory(citra_libretro) +endif() + if (ENABLE_ROOM) add_subdirectory(citra_room) endif() @@ -209,7 +217,7 @@ if (ENABLE_ROOM_STANDALONE) add_subdirectory(citra_room_standalone) endif() -if (ANDROID) +if (ANDROID AND NOT ENABLE_LIBRETRO) add_subdirectory(android/app/src/main/jni) target_include_directories(citra-android PRIVATE android/app/src/main) endif() diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt index a000e1fc1..6ea16672e 100644 --- a/src/audio_core/CMakeLists.txt +++ b/src/audio_core/CMakeLists.txt @@ -38,6 +38,7 @@ add_library(audio_core STATIC $<$:sdl2_sink.cpp sdl2_sink.h> $<$:cubeb_sink.cpp cubeb_sink.h cubeb_input.cpp cubeb_input.h> + $<$:libretro_sink.cpp libretro_sink.h libretro_input.cpp libretro_input.h> $<$:openal_input.cpp openal_input.h openal_sink.cpp openal_sink.h> ) diff --git a/src/audio_core/dsp_interface.cpp b/src/audio_core/dsp_interface.cpp index 259459a5b..52e781ca4 100644 --- a/src/audio_core/dsp_interface.cpp +++ b/src/audio_core/dsp_interface.cpp @@ -41,7 +41,11 @@ void DspInterface::OutputFrame(StereoFrame16 frame) { return; } - fifo.Push(frame.data(), frame.size()); + if (sink->ImmediateSubmission()) { + sink->PushSamples(frame.data(), frame.size()); + } else { + fifo.Push(frame.data(), frame.size()); + } auto video_dumper = system.GetVideoDumper(); if (video_dumper && video_dumper->IsDumping()) { @@ -54,7 +58,11 @@ void DspInterface::OutputSample(std::array sample) { return; } - fifo.Push(&sample, 1); + if (sink->ImmediateSubmission()) { + sink->PushSamples(&sample, 1); + } else { + fifo.Push(&sample, 1); + } auto video_dumper = system.GetVideoDumper(); if (video_dumper && video_dumper->IsDumping()) { diff --git a/src/audio_core/input_details.cpp b/src/audio_core/input_details.cpp index e98310d31..f0a3b80b4 100644 --- a/src/audio_core/input_details.cpp +++ b/src/audio_core/input_details.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -15,6 +15,9 @@ #ifdef HAVE_OPENAL #include "audio_core/openal_input.h" #endif +#ifdef HAVE_LIBRETRO +#include "audio_core/libretro_input.h" +#endif #include "common/logging/log.h" #include "core/core.h" @@ -22,6 +25,18 @@ namespace AudioCore { namespace { // input_details is ordered in terms of desirability, with the best choice at the top. constexpr std::array input_details = { +#ifdef HAVE_LIBRETRO + InputDetails{InputType::LibRetro, "Real Device (LibRetro)", true, + [](Core::System& system, std::string_view device_id) -> std::unique_ptr { + if (!system.HasMicPermission()) { + LOG_WARNING(Audio, + "Microphone permission denied, falling back to null input."); + return std::make_unique(); + } + return std::make_unique(); + }, + [] { return std::vector{"LibRetro Microphone"}; }}, +#endif #ifdef HAVE_CUBEB InputDetails{InputType::Cubeb, "Real Device (Cubeb)", true, [](Core::System& system, std::string_view device_id) -> std::unique_ptr { diff --git a/src/audio_core/input_details.h b/src/audio_core/input_details.h index a091dfe6c..709453eac 100644 --- a/src/audio_core/input_details.h +++ b/src/audio_core/input_details.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -24,6 +24,7 @@ enum class InputType : u32 { Static = 2, Cubeb = 3, OpenAL = 4, + LibRetro = 5, }; struct InputDetails { diff --git a/src/audio_core/libretro_input.cpp b/src/audio_core/libretro_input.cpp new file mode 100644 index 000000000..49354a65d --- /dev/null +++ b/src/audio_core/libretro_input.cpp @@ -0,0 +1,327 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include "audio_core/libretro_input.h" +#include "citra_libretro/environment.h" +#include "common/logging/log.h" +#include "common/ring_buffer.h" +#include "libretro.h" + +namespace AudioCore { + +namespace { +// Global instance pointer for access from retro_run +LibRetroInput* g_libretro_input = nullptr; +} // namespace + +struct LibRetroInput::Impl { + std::optional mic_interface; + retro_microphone_t* mic_handle = nullptr; + bool is_sampling = false; + u8 sample_size_in_bytes = 2; + int warmup_frames = 0; + + // The rate at which the frontend actually provides samples (may differ from + // what the 3DS mic service requested). We open the mic at this rate to avoid + // RetroArch's internal resampler path, which has a convergence bug when + // downsampling (ratio < 1). We resample ourselves in Read() instead. + u32 native_sample_rate = 0; + + // Ring buffer for thread-safe sample storage + // Capacity: 4096 samples should be plenty for buffering between frames + // The 3DS mic service reads 16 samples at a time at ~32728 Hz + Common::RingBuffer sample_buffer; + + // Temporary buffer for reading from frontend + std::vector read_buffer; + + Impl() { + // Try to get the microphone interface from the frontend + retro_microphone_interface interface{}; + interface.interface_version = RETRO_MICROPHONE_INTERFACE_VERSION; + + if (LibRetro::GetMicrophoneInterface(&interface)) { + if (interface.interface_version == RETRO_MICROPHONE_INTERFACE_VERSION) { + mic_interface = interface; + LOG_INFO(Audio, "LibRetro microphone interface available (version {})", + interface.interface_version); + } else { + LOG_WARNING(Audio, + "LibRetro microphone interface version mismatch: expected {}, got {}", + RETRO_MICROPHONE_INTERFACE_VERSION, interface.interface_version); + } + } else { + LOG_WARNING(Audio, "LibRetro microphone interface not available"); + } + + // Keep this small enough that RetroArch's microphone_driver_read can + // fill its outgoing FIFO in a single flush iteration. The CoreAudio + // driver's internal FIFO is ~480 samples (10ms at 48kHz). If we + // request more than that, the blocking while-loop in + // microphone_driver_read must wait for the next hardware callback, + // and on ARM64 without memory barriers in the FIFO, it may never + // see the new data. 128 samples is conservative enough to succeed + // in one pass. + read_buffer.resize(128); + } + + ~Impl() { + CloseMicrophone(); + } + + bool EnsureMicrophoneOpen() { + if (mic_handle) { + return true; + } + + if (!mic_interface) { + return false; + } + + // Always open at 48000 Hz regardless of what the game requests. + // RetroArch's microphone_driver_read has a resampler whose while-loop + // deadlocks when the ratio is < 1 (core rate < device rate). The + // libretro get_params API only returns the effective (requested) rate, + // not the device's native rate, so we can't detect the mismatch. + // Opening at 48000 Hz (the most common hardware rate) keeps the + // frontend's internal resampling ratio at or near 1.0, avoiding the + // bug. We resample to the game's requested rate ourselves in Read(). + static constexpr u32 kMicOpenRate = 48000; + native_sample_rate = kMicOpenRate; + + retro_microphone_params_t params{}; + params.rate = kMicOpenRate; + + mic_handle = mic_interface->open_mic(¶ms); + if (!mic_handle) { + LOG_ERROR(Audio, "Failed to open LibRetro microphone"); + return false; + } + + // The frontend may start recording immediately in open_mic (e.g. + // CoreAudio calls AudioOutputUnitStart). Pause it right away so the + // mic is available but idle until StartSampling enables it. + mic_interface->set_mic_state(mic_handle, false); + + LOG_INFO(Audio, "LibRetro microphone opened at {} Hz (idle)", native_sample_rate); + return true; + } + + void CloseMicrophone() { + if (mic_interface && mic_handle) { + mic_interface->close_mic(mic_handle); + mic_handle = nullptr; + } + } + + bool SetMicrophoneActive(bool active) { + if (!mic_interface || !mic_handle) { + return false; + } + return mic_interface->set_mic_state(mic_handle, active); + } + + bool IsMicrophoneActive() const { + if (!mic_interface || !mic_handle) { + return false; + } + return mic_interface->get_mic_state(mic_handle); + } +}; + +LibRetroInput::LibRetroInput() : impl(std::make_unique()) { + g_libretro_input = this; +} + +LibRetroInput::~LibRetroInput() { + StopSampling(); + if (g_libretro_input == this) { + g_libretro_input = nullptr; + } +} + +void LibRetroInput::StartSampling(const InputParameters& params) { + if (IsSampling()) { + return; + } + + // LibRetro only provides signed 16-bit PCM samples + // We'll convert to the requested format in Read() + if (params.sign == Signedness::Unsigned) { + LOG_DEBUG(Audio, "Application requested unsigned PCM format; will convert from signed."); + } + + parameters = params; + impl->sample_size_in_bytes = params.sample_size / 8; + + if (!impl->EnsureMicrophoneOpen()) { + LOG_WARNING(Audio, "Cannot start sampling: microphone not available"); + return; + } + + // Enable the microphone (transitions from idle to recording) + if (!impl->SetMicrophoneActive(true)) { + LOG_ERROR(Audio, "Failed to activate microphone"); + return; + } + + impl->is_sampling = true; + // Give the audio hardware a few frames to start delivering data before + // we attempt a (blocking) read_mic call. Without this, the very first + // read can hang because the CoreAudio callback hasn't fired yet. + impl->warmup_frames = 10; + LOG_INFO(Audio, "LibRetro microphone sampling started at {} Hz, {} bit", params.sample_rate, + params.sample_size); +} + +void LibRetroInput::StopSampling() { + if (!impl->is_sampling) { + return; + } + + impl->SetMicrophoneActive(false); + impl->is_sampling = false; + + LOG_INFO(Audio, "LibRetro microphone sampling stopped (mic remains idle)"); +} + +bool LibRetroInput::IsSampling() { + return impl->is_sampling; +} + +void LibRetroInput::AdjustSampleRate(u32 sample_rate) { + if (!IsSampling()) { + return; + } + + // Restart with new sample rate + auto new_parameters = parameters; + new_parameters.sample_rate = sample_rate; + StopSampling(); + StartSampling(new_parameters); +} + +void LibRetroInput::PollMicrophone() { + // This is called from the main thread (retro_run) + // Read samples from the frontend and push to the ring buffer + + if (!impl->is_sampling || !impl->mic_interface || !impl->mic_handle) { + return; + } + + // Wait for the audio hardware to start delivering data before making + // any blocking read_mic calls. + if (impl->warmup_frames > 0) { + impl->warmup_frames--; + return; + } + + // Issue a memory fence before reading. RetroArch's CoreAudio mic driver + // fills its FIFO from a callback thread without memory barriers. On ARM64 + // (weak memory model), the main thread may not see the callback's writes + // without an explicit barrier. + std::atomic_thread_fence(std::memory_order_acquire); + + int samples_read = impl->mic_interface->read_mic(impl->mic_handle, impl->read_buffer.data(), + static_cast(impl->read_buffer.size())); + + if (samples_read > 0) { + impl->sample_buffer.Push( + std::span(impl->read_buffer.data(), static_cast(samples_read))); + } +} + +Samples LibRetroInput::Read() { + // This is called from the CoreTiming scheduler thread + // Pop samples from the ring buffer (thread-safe) + + if (!impl->is_sampling) { + return {}; + } + + // Pop available samples from the buffer (at native device rate) + std::vector raw_samples = impl->sample_buffer.Pop(); + + if (raw_samples.empty()) { + return {}; + } + + // Resample from native device rate to the rate the 3DS mic service expects + if (impl->native_sample_rate != 0 && impl->native_sample_rate != parameters.sample_rate) { + double ratio = static_cast(parameters.sample_rate) / impl->native_sample_rate; + auto output_count = static_cast(raw_samples.size() * ratio); + if (output_count == 0) { + return {}; + } + std::vector resampled(output_count); + for (std::size_t i = 0; i < output_count; i++) { + double src_pos = i / ratio; + auto idx = static_cast(src_pos); + double frac = src_pos - idx; + if (idx + 1 < raw_samples.size()) { + resampled[i] = + static_cast(raw_samples[idx] * (1.0 - frac) + raw_samples[idx + 1] * frac); + } else { + resampled[i] = raw_samples[std::min(idx, raw_samples.size() - 1)]; + } + } + raw_samples = std::move(resampled); + } + + // Convert sample format if needed + constexpr auto convert_s16_to_u16 = [](s16 sample) -> u16 { + return static_cast(sample) ^ 0x8000; + }; + + constexpr auto convert_s16_to_s8 = [](s16 sample) -> s8 { + return static_cast(sample >> 8); + }; + + constexpr auto convert_s16_to_u8 = [](s16 sample) -> u8 { + return static_cast((static_cast(sample) ^ 0x8000) >> 8); + }; + + Samples output; + output.reserve(raw_samples.size() * impl->sample_size_in_bytes); + + if (impl->sample_size_in_bytes == 1) { + // 8-bit output + if (parameters.sign == Signedness::Unsigned) { + for (s16 sample : raw_samples) { + output.push_back(convert_s16_to_u8(sample)); + } + } else { + for (s16 sample : raw_samples) { + output.push_back(static_cast(convert_s16_to_s8(sample))); + } + } + } else { + // 16-bit output + if (parameters.sign == Signedness::Unsigned) { + for (s16 sample : raw_samples) { + u16 converted = convert_s16_to_u16(sample); + output.push_back(static_cast(converted & 0xFF)); + output.push_back(static_cast(converted >> 8)); + } + } else { + // Signed 16-bit - just copy the raw bytes + const u8* data = reinterpret_cast(raw_samples.data()); + output.insert(output.end(), data, data + raw_samples.size() * 2); + } + } + + return output; +} + +LibRetroInput* GetLibRetroInput() { + return g_libretro_input; +} + +} // namespace AudioCore diff --git a/src/audio_core/libretro_input.h b/src/audio_core/libretro_input.h new file mode 100644 index 000000000..2320e4ef2 --- /dev/null +++ b/src/audio_core/libretro_input.h @@ -0,0 +1,36 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "audio_core/input.h" + +namespace AudioCore { + +class LibRetroInput final : public Input { +public: + LibRetroInput(); + ~LibRetroInput() override; + + void StartSampling(const InputParameters& params) override; + void StopSampling() override; + bool IsSampling() override; + void AdjustSampleRate(u32 sample_rate) override; + Samples Read() override; + + /// Called from main thread (retro_run) to read samples from the frontend + /// and store them in the thread-safe buffer for Read() to consume. + void PollMicrophone(); + +private: + struct Impl; + std::unique_ptr impl; +}; + +/// Returns the global LibRetroInput instance, or nullptr if not initialized. +/// This is used by citra_libretro.cpp to poll the microphone from the main thread. +LibRetroInput* GetLibRetroInput(); + +} // namespace AudioCore diff --git a/src/audio_core/libretro_sink.cpp b/src/audio_core/libretro_sink.cpp new file mode 100644 index 000000000..d3bcf0ca5 --- /dev/null +++ b/src/audio_core/libretro_sink.cpp @@ -0,0 +1,27 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "audio_core/libretro_sink.h" +#include "citra_libretro/environment.h" + +namespace AudioCore { + +LibRetroSink::LibRetroSink(std::string) {} + +LibRetroSink::~LibRetroSink() = default; + +unsigned int LibRetroSink::GetNativeSampleRate() const { + return native_sample_rate; +} + +void LibRetroSink::PushSamples(const void* data, std::size_t num_samples) { + // libretro calls stereo pairs "frames", Azahar calls them "samples" + LibRetro::SubmitAudio(static_cast(data), num_samples); +} + +std::vector ListLibretroSinkDevices() { + return std::vector{"LibRetro"}; +} + +} // namespace AudioCore diff --git a/src/audio_core/libretro_sink.h b/src/audio_core/libretro_sink.h new file mode 100644 index 000000000..b9685fb80 --- /dev/null +++ b/src/audio_core/libretro_sink.h @@ -0,0 +1,33 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "audio_core/sink.h" + +namespace AudioCore { + +class LibRetroSink final : public Sink { +public: + explicit LibRetroSink(std::string target_device_name); + ~LibRetroSink() override; + + unsigned int GetNativeSampleRate() const override; + + // Not used for immediate submission sinks + void SetCallback(std::function cb) override {}; + + bool ImmediateSubmission() override { + return true; + } + + void PushSamples(const void* data, std::size_t num_samples) override; +}; + +std::vector ListLibretroSinkDevices(); + +} // namespace AudioCore diff --git a/src/audio_core/sink.h b/src/audio_core/sink.h index 73a10f567..d0d0ad02a 100644 --- a/src/audio_core/sink.h +++ b/src/audio_core/sink.h @@ -5,7 +5,7 @@ #pragma once #include -#include "common/common_types.h" +#include "audio_types.h" namespace AudioCore { @@ -30,6 +30,23 @@ public: * @param sample_count Number of samples. */ virtual void SetCallback(std::function cb) = 0; + + /** + * Override and set this to true if the sink wants audio data submitted + * immediately rather than requesting audio on demand + * @return true if audio data should be pushed to the sink + */ + virtual bool ImmediateSubmission() { + return false; + } + + /** + * Push audio samples directly to the sink, bypassing the FIFO. + * Only called when ImmediateSubmission() returns true. + * @param data Pointer to stereo PCM16 samples (each sample is L+R pair) + * @param num_samples Number of stereo samples + */ + virtual void PushSamples(const void* data, std::size_t num_samples) {} }; } // namespace AudioCore diff --git a/src/audio_core/sink_details.cpp b/src/audio_core/sink_details.cpp index 961e040b3..ca48b1f23 100644 --- a/src/audio_core/sink_details.cpp +++ b/src/audio_core/sink_details.cpp @@ -1,4 +1,4 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -11,6 +11,9 @@ #ifdef HAVE_SDL2 #include "audio_core/sdl2_sink.h" #endif +#ifdef HAVE_LIBRETRO +#include "audio_core/libretro_sink.h" +#endif #ifdef HAVE_CUBEB #include "audio_core/cubeb_sink.h" #endif @@ -23,6 +26,13 @@ namespace AudioCore { namespace { // sink_details is ordered in terms of desirability, with the best choice at the top. constexpr std::array sink_details = { +#ifdef HAVE_LIBRETRO + SinkDetails{SinkType::LibRetro, "libretro", + [](std::string_view device_id) -> std::unique_ptr { + return std::make_unique(std::string(device_id)); + }, + &ListLibretroSinkDevices}, +#endif #ifdef HAVE_CUBEB SinkDetails{SinkType::Cubeb, "Cubeb", [](std::string_view device_id) -> std::unique_ptr { diff --git a/src/audio_core/sink_details.h b/src/audio_core/sink_details.h index 2a0d202a7..a3e7a4edb 100644 --- a/src/audio_core/sink_details.h +++ b/src/audio_core/sink_details.h @@ -1,4 +1,4 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -20,6 +20,7 @@ enum class SinkType : u32 { Cubeb = 2, OpenAL = 3, SDL2 = 4, + LibRetro = 5, }; struct SinkDetails { diff --git a/src/citra_libretro/CMakeLists.txt b/src/citra_libretro/CMakeLists.txt new file mode 100644 index 000000000..8ea9b7b10 --- /dev/null +++ b/src/citra_libretro/CMakeLists.txt @@ -0,0 +1,97 @@ +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/$) +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/CMakeModules) + +# Object library for libretro code (can be linked into both shared lib and tests) +add_library(azahar_libretro_common OBJECT + emu_window/libretro_window.cpp + emu_window/libretro_window.h + input/input_factory.cpp + input/input_factory.h + input/mouse_tracker.cpp + input/mouse_tracker.h + $<$: libretro_vk.cpp libretro_vk.h> + environment.cpp + environment.h + core_settings.cpp + core_settings.h) + +target_compile_definitions(azahar_libretro_common PRIVATE HAVE_LIBRETRO) +target_link_libraries(azahar_libretro_common PRIVATE citra_common citra_core video_core libretro robin_map) +if(ENABLE_OPENGL) + target_link_libraries(azahar_libretro_common PRIVATE glad) +endif() +if(ENABLE_VULKAN) + target_link_libraries(azahar_libretro_common PRIVATE sirit vulkan-headers vma) +endif() + +add_library(azahar_libretro SHARED + citra_libretro.cpp + citra_libretro.h + $) + +create_target_directory_groups(azahar_libretro) + +target_link_libraries(citra_common PRIVATE libretro) +target_link_libraries(citra_core PRIVATE libretro) +target_link_libraries(video_core PRIVATE libretro) +target_link_libraries(audio_core PRIVATE libretro) +target_link_libraries(input_common PRIVATE libretro) +target_compile_definitions(citra_common PRIVATE HAVE_LIBRETRO) +target_compile_definitions(citra_core PRIVATE HAVE_LIBRETRO) +target_compile_definitions(video_core PRIVATE HAVE_LIBRETRO) +target_compile_definitions(audio_core PRIVATE HAVE_LIBRETRO) +target_compile_definitions(input_common PRIVATE HAVE_LIBRETRO) + +target_link_libraries(azahar_libretro PRIVATE citra_common citra_core) +target_link_libraries(azahar_libretro PRIVATE boost dds-ktx libretro robin_map) +if(ENABLE_VULKAN) + target_link_libraries(azahar_libretro PRIVATE sirit vulkan-headers vma) +endif() +if(ENABLE_OPENGL) + target_link_libraries(azahar_libretro PRIVATE glad) +endif() +target_link_libraries(azahar_libretro PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) +if(DEFINED LIBRETRO_STATIC) + target_link_libraries(azahar_libretro PRIVATE -static-libstdc++) +endif() + +set_target_properties(azahar_libretro PROPERTIES PREFIX "") +target_compile_definitions(azahar_libretro PRIVATE HAVE_LIBRETRO) + +if(ANDROID) + target_compile_definitions(citra_common PRIVATE HAVE_LIBRETRO_VFS) + target_compile_definitions(citra_core PRIVATE HAVE_LIBRETRO_VFS) + target_compile_definitions(video_core PRIVATE HAVE_LIBRETRO_VFS) + target_compile_definitions(azahar_libretro_common PRIVATE USING_GLES HAVE_LIBRETRO_VFS) + target_compile_definitions(azahar_libretro PRIVATE USING_GLES HAVE_LIBRETRO_VFS) + target_link_libraries(citra_common PRIVATE libretro_common) + target_link_libraries(citra_core PRIVATE libretro_common) + target_link_libraries(video_core PRIVATE libretro_common) + target_link_libraries(azahar_libretro_common PRIVATE libretro_common) + target_link_libraries(azahar_libretro PRIVATE libretro_common) + # Link Android log library for __android_log_print + target_link_libraries(azahar_libretro PRIVATE log) + set_target_properties(azahar_libretro PROPERTIES SUFFIX "_android.so") +endif() + +if(MINGW) + target_link_libraries(azahar_libretro PRIVATE crypt32) +endif() + +if(IOS) + target_compile_definitions(azahar_libretro_common PRIVATE IOS) + target_compile_definitions(azahar_libretro PRIVATE IOS) + target_link_libraries(azahar_libretro PRIVATE "-framework CoreFoundation" "-framework Foundation") +endif() + +if (SSE42_COMPILE_OPTION) + target_compile_definitions(azahar_libretro PRIVATE CITRA_HAS_SSE42) +endif() + +if (CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR + CMAKE_SYSTEM_NAME STREQUAL "iOS" OR + CMAKE_SYSTEM_NAME STREQUAL "tvOS") + target_link_libraries(azahar_libretro PRIVATE "-Wl,-exported_symbols_list,${CMAKE_CURRENT_SOURCE_DIR}/libretro.osx.def") +else() + target_link_libraries(azahar_libretro PRIVATE "-Wl,-Bsymbolic") +endif() diff --git a/src/citra_libretro/citra_libretro.cpp b/src/citra_libretro/citra_libretro.cpp new file mode 100644 index 000000000..1fb73cb23 --- /dev/null +++ b/src/citra_libretro/citra_libretro.cpp @@ -0,0 +1,717 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include + +#ifdef ENABLE_OPENGL +#include "glad/glad.h" +#include "video_core/renderer_opengl/gl_vars.h" +#endif +#include "libretro.h" + +#include "audio_core/libretro_input.h" +#include "audio_core/libretro_sink.h" +#include "video_core/gpu.h" +#ifdef ENABLE_OPENGL +#include "video_core/renderer_opengl/renderer_opengl.h" +#endif +#ifdef ENABLE_VULKAN +#include "citra_libretro/libretro_vk.h" +#endif +#include "video_core/renderer_software/renderer_software.h" +#include "video_core/video_core.h" + +#include "citra_libretro/citra_libretro.h" +#include "citra_libretro/core_settings.h" +#include "citra_libretro/environment.h" +#include "citra_libretro/input/input_factory.h" + +#include "common/arch.h" +#if CITRA_ARCH(x86_64) +#include "common/x64/cpu_detect.h" +#endif +#include "common/logging/backend.h" +#include "common/logging/filter.h" +#include "common/settings.h" +#include "common/string_util.h" +#include "core/core.h" +#include "core/frontend/applets/default_applets.h" +#include "core/frontend/image_interface.h" +#include "core/hle/kernel/kernel.h" +#include "core/hle/kernel/memory.h" +#include "core/hle/kernel/process.h" +#include "core/loader/loader.h" +#include "core/memory.h" + +#ifdef HAVE_LIBRETRO_VFS +#include +#endif + +class CitraLibRetro { +public: + CitraLibRetro() : log_filter(Common::Log::Level::Debug) {} + + Common::Log::Filter log_filter; + std::unique_ptr emu_window; + bool game_loaded = false; + struct retro_hw_render_callback hw_render{}; +}; + +CitraLibRetro* emu_instance; + +void retro_init() { + emu_instance = new CitraLibRetro(); + Common::Log::LibRetroStart(LibRetro::GetLoggingBackend()); + Common::Log::SetGlobalFilter(emu_instance->log_filter); + + LOG_DEBUG(Frontend, "Initializing core..."); + + // Set up LLE cores + for (const auto& service_module : Service::service_module_map) { + Settings::values.lle_modules.emplace(service_module.name, false); + } + + // Setup default, stub handlers for HLE applets + Frontend::RegisterDefaultApplets(Core::System::GetInstance()); + + // Register generic image interface + Core::System::GetInstance().RegisterImageInterface( + std::make_shared()); + + LibRetro::Input::Init(); +} + +void retro_deinit() { + LOG_DEBUG(Frontend, "Shutting down core..."); + if (Core::System::GetInstance().IsPoweredOn()) { + Core::System::GetInstance().Shutdown(); + } + + LibRetro::Input::Shutdown(); + + delete emu_instance; + + Common::Log::Stop(); +} + +unsigned retro_api_version() { + return RETRO_API_VERSION; +} + +void LibRetro::OnConfigureEnvironment() { + +#ifdef HAVE_LIBRETRO_VFS + struct retro_vfs_interface_info vfs_iface_info{1, nullptr}; + LibRetro::SetVFSCallback(&vfs_iface_info); +#endif + + LibRetro::RegisterCoreOptions(); + + static const struct retro_controller_description controllers[] = { + {"Nintendo 3DS", RETRO_DEVICE_JOYPAD}, + }; + + static const struct retro_controller_info ports[] = { + {controllers, 1}, + {nullptr, 0}, + }; + + LibRetro::SetControllerInfo(ports); +} + +uintptr_t LibRetro::GetFramebuffer() { + return emu_instance->hw_render.get_current_framebuffer(); +} + +/** + * Updates Citra's settings with Libretro's. + */ +static void UpdateSettings() { + LibRetro::ParseCoreOptions(); + + struct retro_input_descriptor desc[] = { + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT, "Left"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP, "Up"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN, "Down"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT, "Right"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_X, "X"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_Y, "Y"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B, "B"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A, "A"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L, "L"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L2, "ZL"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R, "R"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R2, "ZR"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START, "Start"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT, "Select"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L3, "Home/Swap screens"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R3, "Touch Screen Touch"}, + {0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X, + "Circle Pad X"}, + {0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y, + "Circle Pad Y"}, + {0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_X, + "C-Stick / Pointer X"}, + {0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_Y, + "C-Stick / Pointer Y"}, + {0, 0}, + }; + + LibRetro::SetInputDescriptors(desc); + + Settings::values.current_input_profile.touch_device = "engine:emu_window"; + + // Hardcode buttons to bind to libretro - it is entirely redundant to have + // two methods of rebinding controls. + // Citra: A = RETRO_DEVICE_ID_JOYPAD_A (8) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::A] = + "button:8,joystick:0,engine:libretro"; + // Citra: B = RETRO_DEVICE_ID_JOYPAD_B (0) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::B] = + "button:0,joystick:0,engine:libretro"; + // Citra: X = RETRO_DEVICE_ID_JOYPAD_X (9) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::X] = + "button:9,joystick:0,engine:libretro"; + // Citra: Y = RETRO_DEVICE_ID_JOYPAD_Y (1) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Y] = + "button:1,joystick:0,engine:libretro"; + // Citra: UP = RETRO_DEVICE_ID_JOYPAD_UP (4) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Up] = + "button:4,joystick:0,engine:libretro"; + // Citra: DOWN = RETRO_DEVICE_ID_JOYPAD_DOWN (5) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Down] = + "button:5,joystick:0,engine:libretro"; + // Citra: LEFT = RETRO_DEVICE_ID_JOYPAD_LEFT (6) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Left] = + "button:6,joystick:0,engine:libretro"; + // Citra: RIGHT = RETRO_DEVICE_ID_JOYPAD_RIGHT (7) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Right] = + "button:7,joystick:0,engine:libretro"; + // Citra: L = RETRO_DEVICE_ID_JOYPAD_L (10) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::L] = + "button:10,joystick:0,engine:libretro"; + // Citra: R = RETRO_DEVICE_ID_JOYPAD_R (11) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::R] = + "button:11,joystick:0,engine:libretro"; + // Citra: START = RETRO_DEVICE_ID_JOYPAD_START (3) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Start] = + "button:3,joystick:0,engine:libretro"; + // Citra: SELECT = RETRO_DEVICE_ID_JOYPAD_SELECT (2) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Select] = + "button:2,joystick:0,engine:libretro"; + // Citra: ZL = RETRO_DEVICE_ID_JOYPAD_L2 (12) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::ZL] = + "button:12,joystick:0,engine:libretro"; + // Citra: ZR = RETRO_DEVICE_ID_JOYPAD_R2 (13) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::ZR] = + "button:13,joystick:0,engine:libretro"; + // Citra: HOME = RETRO_DEVICE_ID_JOYPAD_L3 (as per above bindings) (14) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Home] = + "button:14,joystick:0,engine:libretro"; + + // Circle Pad + Settings::values.current_input_profile.analogs[0] = "axis:0,joystick:0,engine:libretro"; + // C-Stick + if (LibRetro::settings.analog_function != LibRetro::CStickFunction::Touchscreen) { + Settings::values.current_input_profile.analogs[1] = "axis:1,joystick:0,engine:libretro"; + } else { + Settings::values.current_input_profile.analogs[1] = ""; + } + + if (!emu_instance->emu_window) { + emu_instance->emu_window = std::make_unique(); + } + + // Update the framebuffer sizing. + emu_instance->emu_window->UpdateLayout(); + + Core::System::GetInstance().ApplySettings(); +} + +/** + * libretro callback; Called every game tick. + */ +void retro_run() { + // Check to see if we actually have any config updates to process. + if (LibRetro::HasUpdatedConfig()) { + LibRetro::ParseCoreOptions(); + Core::System::GetInstance().ApplySettings(); + emu_instance->emu_window->UpdateLayout(); + } + + // Poll microphone input from the frontend and buffer it for the emulator + // This must be done from the main thread as LibRetro's mic interface is not thread-safe + if (auto* mic_input = AudioCore::GetLibRetroInput()) { + mic_input->PollMicrophone(); + } + + // Check if the screen swap button is pressed + static bool screen_swap_btn_state = false; + static bool screen_swap_toggled = false; + bool screen_swap_btn = + !!LibRetro::CheckInput(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L3); + if (screen_swap_btn != screen_swap_btn_state) { + if (LibRetro::settings.toggle_swap_screen) { + if (!screen_swap_btn_state) + screen_swap_toggled = !screen_swap_toggled; + + if (screen_swap_toggled) + Settings::values.swap_screen = + LibRetro::FetchVariable("citra_swap_screen", "Top") != "Bottom"; + else + Settings::values.swap_screen = + LibRetro::FetchVariable("citra_swap_screen", "Top") == "Bottom"; + } else { + if (screen_swap_btn) + Settings::values.swap_screen = + LibRetro::FetchVariable("citra_swap_screen", "Top") != "Bottom"; + else + Settings::values.swap_screen = + LibRetro::FetchVariable("citra_swap_screen", "Top") == "Bottom"; + } + + Core::System::GetInstance().ApplySettings(); + + // Update the framebuffer sizing. + emu_instance->emu_window->UpdateLayout(); + + screen_swap_btn_state = screen_swap_btn; + } + +#ifdef ENABLE_OPENGL + if (Settings::values.graphics_api.GetValue() == Settings::GraphicsAPI::OpenGL) { + // We can't assume that the frontend has been nice and preserved all OpenGL settings. Reset. + auto last_state = OpenGL::OpenGLState::GetCurState(); + ResetGLState(); + last_state.Apply(); + } +#endif + + while (!emu_instance->emu_window->HasSubmittedFrame()) { + auto result = Core::System::GetInstance().RunLoop(); + + if (result != Core::System::ResultStatus::Success) { + std::string errorContent = Core::System::GetInstance().GetStatusDetails(); + std::string msg; + + switch (result) { + case Core::System::ResultStatus::ErrorSystemFiles: + msg = "Azahar was unable to locate a 3DS system archive: " + errorContent; + break; + default: + msg = "Fatal Error encountered (" + std::to_string(static_cast(result)) + + "): " + errorContent; + break; + } + + LibRetro::DisplayMessage(msg.c_str()); + } + } +} + +static void setup_memory_maps() { + auto process = Core::System::GetInstance().Kernel().GetCurrentProcess(); + if (!process) + return; + + std::vector descs; + + for (const auto& [addr, vma] : process->vm_manager.vma_map) { + if (vma.type != Kernel::VMAType::BackingMemory) + continue; + if (vma.size == 0 || !vma.backing_memory) + continue; + + // Only expose the well-known user-accessible memory regions + uint64_t flags = 0; + if (vma.base >= Memory::HEAP_VADDR && vma.base < Memory::HEAP_VADDR_END) { + flags = RETRO_MEMDESC_SYSTEM_RAM; + } else if (vma.base >= Memory::LINEAR_HEAP_VADDR && + vma.base < Memory::LINEAR_HEAP_VADDR_END) { + flags = RETRO_MEMDESC_SYSTEM_RAM; + } else if (vma.base >= Memory::NEW_LINEAR_HEAP_VADDR && + vma.base < Memory::NEW_LINEAR_HEAP_VADDR_END) { + flags = RETRO_MEMDESC_SYSTEM_RAM; + } else if (vma.base >= Memory::VRAM_VADDR && vma.base < Memory::VRAM_VADDR_END) { + flags = RETRO_MEMDESC_VIDEO_RAM; + } else { + continue; + } + + retro_memory_descriptor desc = {}; + desc.flags = flags; + desc.ptr = const_cast(vma.backing_memory.GetPtr()); + desc.start = vma.base; + desc.len = vma.size; + + // select=0 requires power-of-2 len AND start aligned to len. + // When that doesn't hold, compute a select mask instead. + bool need_select = (vma.size & (vma.size - 1)) != 0; + if (!need_select && (vma.base & (vma.size - 1)) != 0) + need_select = true; + + if (need_select) { + uint64_t np2 = 1; + while (np2 < vma.size) + np2 <<= 1; + if (vma.base & (np2 - 1)) { + LOG_WARNING(Frontend, "VMA at 0x{:08X} size 0x{:X} not aligned, skipping", vma.base, + vma.size); + continue; + } + desc.select = ~(np2 - 1); + } + + descs.push_back(desc); + } + + if (!descs.empty()) { + retro_memory_map map = {descs.data(), static_cast(descs.size())}; + LibRetro::SetMemoryMaps(&map); + } +} + +static bool do_load_game() { + const Core::System::ResultStatus load_result{ + Core::System::GetInstance().Load(*emu_instance->emu_window, LibRetro::settings.file_path)}; + + switch (load_result) { + case Core::System::ResultStatus::Success: + break; // Expected case + case Core::System::ResultStatus::ErrorGetLoader: + LibRetro::DisplayMessage("Failed to obtain loader for specified ROM!"); + return false; + case Core::System::ResultStatus::ErrorLoader: + LibRetro::DisplayMessage("Failed to load ROM!"); + return false; + case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: + LibRetro::DisplayMessage("The game that you are trying to load must be decrypted before " + "being used with Azahar."); + return false; + case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat: + LibRetro::DisplayMessage("Error while loading ROM: The ROM format is not supported."); + return false; + case Core::System::ResultStatus::ErrorLoader_ErrorGbaTitle: + LibRetro::DisplayMessage( + "Error loading the specified application as it is GBA Virtual Console"); + return false; + case Core::System::ResultStatus::ErrorNotInitialized: + LibRetro::DisplayMessage("CPUCore not initialized"); + return false; + case Core::System::ResultStatus::ErrorSystemMode: + LibRetro::DisplayMessage("Failed to determine system mode!"); + return false; + default: + LibRetro::DisplayMessage( + ("Unknown error: " + std::to_string(static_cast(load_result))).c_str()); + return false; + } + + u64 program_id{}; + Core::System::GetInstance().GetAppLoader().ReadProgramId(program_id); + Core::System::GetInstance().GPU().ApplyPerProgramSettings(program_id); + + if (Settings::values.use_disk_shader_cache) { + Core::System::GetInstance().GPU().Renderer().Rasterizer()->LoadDefaultDiskResources( + false, nullptr); + } + + setup_memory_maps(); + + return true; +} + +#ifdef ENABLE_OPENGL +static void* load_opengl_func(const char* name) { + return (void*)emu_instance->hw_render.get_proc_address(name); +} +#endif + +static void context_reset() { + LOG_DEBUG(Frontend, "context_reset"); + + switch (Settings::values.graphics_api.GetValue()) { +#ifdef ENABLE_OPENGL + case Settings::GraphicsAPI::OpenGL: +#if defined(USING_GLES) + Settings::values.use_gles = true; + // Set the global GLES flag immediately to ensure any shader compilation + // that happens before the Driver is created uses the correct version + OpenGL::GLES = true; +#else + Settings::values.use_gles = false; + OpenGL::GLES = false; +#endif + // Check to see if the frontend provides us with OpenGL symbols + if (emu_instance->hw_render.get_proc_address != nullptr) { + bool loaded = Settings::values.use_gles + ? gladLoadGLES2Loader((GLADloadproc)load_opengl_func) + : gladLoadGLLoader((GLADloadproc)load_opengl_func); + + if (!loaded) { + LOG_CRITICAL(Frontend, "Glad failed to load (frontend-provided symbols)!"); + return; + } + } else { + // Else, try to load them on our own + if (!gladLoadGL()) { + LOG_CRITICAL(Frontend, "Glad failed to load (internal symbols)!"); + return; + } + } + break; +#endif +#ifdef ENABLE_VULKAN + case Settings::GraphicsAPI::Vulkan: + LibRetro::VulkanResetContext(); + break; +#endif + default: + // software renderer never gets here + break; + } + + emu_instance->emu_window->CreateContext(); + + if (!emu_instance->game_loaded) { + emu_instance->game_loaded = do_load_game(); + } else { + // Game is already loaded, just recreate the renderer for the new GL context + if (Settings::values.graphics_api.GetValue() == Settings::GraphicsAPI::OpenGL) { + Core::System::GetInstance().GPU().RecreateRenderer(*emu_instance->emu_window, nullptr); + } + } +} + +static void context_destroy() { + LOG_DEBUG(Frontend, "context_destroy"); + if (emu_instance->game_loaded && + Settings::values.graphics_api.GetValue() == Settings::GraphicsAPI::OpenGL) { + // Release the renderer's OpenGL resources + Core::System::GetInstance().GPU().ReleaseRenderer(); + } + emu_instance->emu_window->DestroyContext(); +} + +void retro_reset() { + LOG_DEBUG(Frontend, "retro_reset"); + Core::System::GetInstance().Shutdown(); + emu_instance->game_loaded = do_load_game(); +} + +/** + * libretro callback; Called when a game is to be loaded. + */ +bool retro_load_game(const struct retro_game_info* info) { + LOG_INFO(Frontend, "Starting Azahar RetroArch game..."); + +#if CITRA_ARCH(x86_64) && CITRA_HAS_SSE42 + if (!Common::GetCPUCaps().sse4_2) { + LOG_CRITICAL(Frontend, "This CPU does not support SSE4.2, which is required by this build"); + LibRetro::DisplayMessage( + "This CPU does not support SSE4.2, which is required by this build"); + return false; + } +#endif + + UpdateSettings(); + + // If using HW rendering, don't actually load the game here. azahar wants + // the graphics context ready and available before calling System::Load. + LibRetro::settings.file_path = info->path; + + if (!LibRetro::SetPixelFormat(RETRO_PIXEL_FORMAT_XRGB8888)) { + LibRetro::DisplayMessage("XRGB8888 is not supported."); + return false; + } + + emu_instance->emu_window->UpdateLayout(); + + switch (Settings::values.graphics_api.GetValue()) { + case Settings::GraphicsAPI::OpenGL: +#ifdef ENABLE_OPENGL + LOG_INFO(Frontend, "Using OpenGL hw renderer"); + LibRetro::SetHWSharedContext(); +#if defined(USING_GLES) + emu_instance->hw_render.context_type = RETRO_HW_CONTEXT_OPENGLES3; + emu_instance->hw_render.version_major = 3; + emu_instance->hw_render.version_minor = 2; +#else + emu_instance->hw_render.context_type = RETRO_HW_CONTEXT_OPENGL_CORE; + emu_instance->hw_render.version_major = 4; + emu_instance->hw_render.version_minor = 3; +#endif + emu_instance->hw_render.context_reset = context_reset; + emu_instance->hw_render.context_destroy = context_destroy; + emu_instance->hw_render.cache_context = false; + emu_instance->hw_render.bottom_left_origin = true; + if (!LibRetro::SetHWRenderer(&emu_instance->hw_render)) { + LibRetro::DisplayMessage("Failed to set HW renderer"); + return false; + } +#endif + break; + case Settings::GraphicsAPI::Vulkan: +#ifdef ENABLE_VULKAN + LOG_INFO(Frontend, "Using Vulkan hw renderer"); + emu_instance->hw_render.context_type = RETRO_HW_CONTEXT_VULKAN; + emu_instance->hw_render.version_major = VK_MAKE_VERSION(1, 1, 0); + emu_instance->hw_render.version_minor = 0; + emu_instance->hw_render.context_reset = context_reset; + emu_instance->hw_render.context_destroy = context_destroy; + emu_instance->hw_render.cache_context = true; + if (!LibRetro::SetHWRenderer(&emu_instance->hw_render)) { + LibRetro::DisplayMessage("Failed to set HW renderer"); + return false; + } + + // Set up Vulkan context negotiation interface + static const struct retro_hw_render_context_negotiation_interface_vulkan vk_negotiation = { + RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN, + RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN_VERSION, + LibRetro::GetVulkanApplicationInfo, + LibRetro::CreateVulkanDevice, + nullptr, // destroy_device - not needed (frontend owns the device) + }; + LibRetro::SetHWRenderContextNegotiationInterface((void**)&vk_negotiation); +#endif + break; + case Settings::GraphicsAPI::Software: + emu_instance->game_loaded = do_load_game(); + if (!emu_instance->game_loaded) + return false; + break; + } + + uint64_t quirks = + RETRO_SERIALIZATION_QUIRK_CORE_VARIABLE_SIZE | RETRO_SERIALIZATION_QUIRK_MUST_INITIALIZE; + LibRetro::SetSerializationQuirks(quirks); + + return true; +} + +void retro_unload_game() { + LOG_DEBUG(Frontend, "Unloading game..."); + Core::System::GetInstance().Shutdown(); +} + +unsigned retro_get_region() { + return RETRO_REGION_NTSC; +} + +bool retro_load_game_special(unsigned game_type, const struct retro_game_info* info, + size_t num_info) { + return retro_load_game(info); +} + +/// Drain any pending async kernel operations by running the emulation loop. +/// +/// Savestates are unsafe to create while RunAsync operations (file I/O, network, etc.) +/// are in flight. The Qt frontend handles this by deferring serialization inside +/// System::RunLoop(): it sets a request flag via SendSignal(Signal::Save), and RunLoop +/// only performs the save when !kernel->AreAsyncOperationsPending() (see core.cpp). +/// +/// The Qt frontend needs that indirection because its UI and emulation run on separate +/// threads. In libretro, the frontend calls API entry points (retro_run, retro_serialize, +/// etc.) sequentially, so we can call RunLoop() directly from here to drain pending ops, +/// then call SaveStateBuffer()/LoadStateBuffer() ourselves. +/// +/// Note: RunLoop() can itself start new async operations (CPU executes HLE service calls), +/// so the pending count may not decrease monotonically. In practice games reach quiescent +/// points between frames; the 5-second timeout (matching RunLoop's existing handler) +/// covers the pathological case. +static bool DrainAsyncOperations(Core::System& system) { + if (!system.KernelRunning() || !system.Kernel().AreAsyncOperationsPending()) { + return true; + } + + emu_instance->emu_window->suppressPresentation = true; + auto start = std::chrono::steady_clock::now(); + + while (system.Kernel().AreAsyncOperationsPending()) { + if (std::chrono::steady_clock::now() - start > std::chrono::seconds(5)) { + LOG_ERROR(Frontend, "Timed out waiting for async operations to complete"); + emu_instance->emu_window->suppressPresentation = false; + return false; + } + auto result = system.RunLoop(); + if (result != Core::System::ResultStatus::Success) { + emu_instance->emu_window->suppressPresentation = false; + return false; + } + } + + emu_instance->emu_window->suppressPresentation = false; + return true; +} + +std::optional> savestate = {}; + +size_t retro_serialize_size() { + auto& system = Core::System::GetInstance(); + if (!system.IsPoweredOn()) + return 0; + + if (!DrainAsyncOperations(system)) { + savestate.reset(); + return 0; + } + + try { + savestate = system.SaveStateBuffer(); + return savestate->size(); + } catch (const std::exception& e) { + LOG_ERROR(Frontend, "Error saving state: {}", e.what()); + savestate.reset(); + return 0; + } +} + +bool retro_serialize(void* data, size_t size) { + if (!savestate.has_value()) + return false; + if (size < savestate->size()) + return false; + memcpy(data, savestate->data(), savestate->size()); + savestate.reset(); + return true; +} + +bool retro_unserialize(const void* data, size_t size) { + auto& system = Core::System::GetInstance(); + if (!system.IsPoweredOn()) + return false; + + if (!DrainAsyncOperations(system)) { + return false; + } + + std::vector buffer(static_cast(data), static_cast(data) + size); + try { + return system.LoadStateBuffer(std::move(buffer)); + } catch (const std::exception& e) { + LOG_ERROR(Frontend, "Error loading state: {}", e.what()); + return false; + } +} + +void* retro_get_memory_data(unsigned id) { + // Memory is exposed via RETRO_ENVIRONMENT_SET_MEMORY_MAPS instead, + // using virtual addresses for stable cheat/achievement support. + return NULL; +} + +size_t retro_get_memory_size(unsigned id) { + return 0; +} + +void retro_cheat_reset() {} + +void retro_cheat_set(unsigned index, bool enabled, const char* code) {} diff --git a/src/citra_libretro/citra_libretro.h b/src/citra_libretro/citra_libretro.h new file mode 100644 index 000000000..0d8a08f89 --- /dev/null +++ b/src/citra_libretro/citra_libretro.h @@ -0,0 +1,10 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "core/core.h" +#include "emu_window/libretro_window.h" + +namespace LibRetro {} // namespace LibRetro diff --git a/src/citra_libretro/core_settings.cpp b/src/citra_libretro/core_settings.cpp new file mode 100644 index 000000000..86e7098f6 --- /dev/null +++ b/src/citra_libretro/core_settings.cpp @@ -0,0 +1,1014 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "citra_libretro/core_settings.h" +#include "citra_libretro/environment.h" + +#include "common/file_util.h" +#include "common/settings.h" +#include "core/hle/service/cfg/cfg.h" + +namespace LibRetro { + +CoreSettings settings = {}; + +namespace config { + +static constexpr const char* enabled = "enabled"; +static constexpr const char* disabled = "disabled"; + +namespace category { +static constexpr const char* cpu = "cpu"; +static constexpr const char* system = "system"; +static constexpr const char* audio = "audio"; +static constexpr const char* graphics = "graphics"; +static constexpr const char* layout = "layout"; +static constexpr const char* storage = "storage"; +static constexpr const char* input = "input"; +} // namespace category + +namespace cpu { +static constexpr const char* use_cpu_jit = "citra_use_cpu_jit"; +static constexpr const char* cpu_clock_percentage = "citra_cpu_scale"; +} // namespace cpu + +namespace system { +static constexpr const char* is_new_3ds = "citra_is_new_3ds"; +static constexpr const char* region = "citra_region_value"; +static constexpr const char* language = "citra_language"; +} // namespace system + +namespace audio { +static constexpr const char* audio_emulation = "citra_audio_emulation"; +static constexpr const char* input_type = "citra_input_type"; +} // namespace audio + +namespace graphics { +static constexpr const char* graphics_api = "citra_graphics_api"; +static constexpr const char* use_hw_shader = "citra_use_hw_shaders"; +static constexpr const char* use_shader_jit = "citra_use_shader_jit"; +static constexpr const char* shaders_accurate_mul = "citra_use_acc_mul"; +static constexpr const char* use_disk_shader_cache = "citra_use_hw_shader_cache"; +static constexpr const char* resolution_factor = "citra_resolution_factor"; +static constexpr const char* texture_filter = "citra_texture_filter"; +static constexpr const char* texture_sampling = "citra_texture_sampling"; +static constexpr const char* custom_textures = "citra_custom_textures"; +static constexpr const char* dump_textures = "citra_dump_textures"; +} // namespace graphics + +namespace layout { +static constexpr const char* layout_option = "citra_layout_option"; +static constexpr const char* swap_screen = "citra_swap_screen"; +static constexpr const char* toggle_swap_screen = "citra_swap_screen_mode"; +} // namespace layout + +namespace storage { +static constexpr const char* use_virtual_sd = "citra_use_virtual_sd"; +static constexpr const char* use_libretro_save_path = "citra_use_libretro_save_path"; +} // namespace storage + +namespace input { +static constexpr const char* analog_function = "citra_analog_function"; +static constexpr const char* deadzone = "citra_deadzone"; +static constexpr const char* mouse_touchscreen = "citra_mouse_touchscreen"; +static constexpr const char* touch_touchscreen = "citra_touch_touchscreen"; +static constexpr const char* render_touchscreen = "citra_render_touchscreen"; +static constexpr const char* motion_enabled = "citra_motion_enabled"; +static constexpr const char* motion_sensitivity = "citra_motion_sensitivity"; +} // namespace input + +} // namespace config + +// clang-format off +static constexpr retro_core_option_v2_category option_categories[] = { + { + config::category::cpu, + "CPU", + "Settings related to CPU emulation performance and accuracy." + }, + { + config::category::system, + "System", + "Nintendo 3DS system configuration and region settings." + }, + { + config::category::audio, + "Audio", + "Audio emulation and microphone settings." + }, + { + config::category::graphics, + "Graphics", + "Graphics API, rendering, and visual enhancement settings." + }, + { + config::category::layout, + "Layout", + "Screen layout and display positioning options." + }, + { + config::category::storage, + "Storage", + "Save data and virtual SD card settings." + }, + { + config::category::input, + "Input", + "Controller and touchscreen input configuration." + }, + { nullptr, nullptr, nullptr } +}; + +// ============================================================================ +// Option Definitions +// ============================================================================ + +static constexpr retro_core_option_v2_definition option_definitions[] = { + // CPU Category + { + config::cpu::use_cpu_jit, + "Enable CPU JIT", + "CPU JIT", + "Enable Just-In-Time compilation for ARM CPU emulation. " + "Significantly improves performance but may reduce accuracy. " + "Restart required.", + nullptr, + config::category::cpu, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::cpu::cpu_clock_percentage, + "CPU Clock Speed", + "CPU Clock Speed", + "Adjust the emulated 3DS CPU clock speed as a percentage of normal speed. " + "Higher values may improve performance in some games but can cause issues. " + "Lower values can help with games that run too fast.", + nullptr, + config::category::cpu, + { + { "25", "25%" }, { "50", "50%" }, { "75", "75%" }, { "100", "100% (Default)" }, + { "125", "125%" }, { "150", "150%" }, { "175", "175%" }, { "200", "200%" }, + { "225", "225%" }, { "250", "250%" }, { "275", "275%" }, { "300", "300%" }, + { "325", "325%" }, { "350", "350%" }, { "375", "375%" }, { "400", "400%" }, + { nullptr, nullptr } + }, + "100" + }, + + // System Category + { + config::system::is_new_3ds, + "3DS System Model", + "System Model", + "Select whether to emulate the original 3DS or New 3DS. " + "New 3DS has additional CPU power and memory, required for some games. " + "Restart required.", + nullptr, + config::category::system, + { + { "Old 3DS", "Original 3DS" }, + { "New 3DS", "New 3DS" }, + { nullptr, nullptr } + }, + "Old 3DS" + }, + { + config::system::region, + "3DS System Region", + "System Region", + "Set the 3DS system region. Auto-select will choose based on the game. " + "Some games are region-locked and require matching regions.", + nullptr, + config::category::system, + { + { "Auto", "Auto" }, + { "Japan", "Japan" }, + { "USA", "USA" }, + { "Europe", "Europe" }, + { "Australia", "Australia" }, + { "China", "China" }, + { "Korea", "Korea" }, + { "Taiwan", "Taiwan" }, + { nullptr, nullptr } + }, + "Auto" + }, + { + config::system::language, + "3DS System Language", + "System Language", + "Set the system language for the emulated 3DS. " + "This affects in-game text language when supported.", + nullptr, + config::category::system, + { + { "English", "English" }, + { "Japanese", "Japanese" }, + { "French", "French" }, + { "Spanish", "Spanish" }, + { "German", "German" }, + { "Italian", "Italian" }, + { "Dutch", "Dutch" }, + { "Portuguese", "Portuguese" }, + { "Russian", "Russian" }, + { "Korean", "Korean" }, + { "Traditional Chinese", "Traditional Chinese" }, + { "Simplified Chinese", "Simplified Chinese" }, + { nullptr, nullptr } + }, + "english" + }, + + // Audio Category + { + config::audio::audio_emulation, + "Audio Emulation", + "Audio Emulation", + "Select audio emulation method. HLE is faster, LLE is more accurate.", + nullptr, + config::category::audio, + { + { "hle", "HLE (Fast)" }, + { "lle", "LLE (Accurate)" }, + { "lle_multithread", "LLE Multithreaded" }, + { nullptr, nullptr } + }, + "hle" + }, + { + config::audio::input_type, + "Microphone Input Type", + "Microphone Input", + "Select how microphone input is handled for games that support it.", + nullptr, + config::category::audio, + { + { "auto", "Auto" }, + { "none", "None" }, + { "static_noise", "Static Noise" }, + { "frontend", "Frontend" }, + { nullptr, nullptr } + }, + "auto" + }, + + // Graphics Category + { + config::graphics::graphics_api, + "Graphics API", + "Graphics API", + "Select the graphics rendering API. Auto will choose the best available option. " + "Restart required.", + nullptr, + config::category::graphics, + { + { "Auto", "Auto" }, +#ifdef ENABLE_VULKAN + { "Vulkan", "Vulkan" }, +#endif +#ifdef ENABLE_OPENGL + { "OpenGL", "OpenGL" }, +#endif + { "Software", "Software" }, + { nullptr, nullptr } + }, + "auto" + }, + { + config::graphics::use_hw_shader, + "Enable Hardware Shaders", + "Hardware Shaders", + "Use GPU hardware to accelerate shader processing. " + "Significantly improves performance but may reduce accuracy.", + nullptr, + config::category::graphics, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::graphics::use_shader_jit, + "Enable Shader JIT", + "Shader JIT", + "Use Just-In-Time compilation for shaders. " + "Improves performance but may cause graphical issues in some games.", + nullptr, + config::category::graphics, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::graphics::shaders_accurate_mul, + "Accurate Shader Multiplication", + "Accurate Multiplication", + "Use accurate multiplication in shaders. " + "More accurate but can reduce performance. Only works with hardware shaders.", + nullptr, + config::category::graphics, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::graphics::use_disk_shader_cache, + "Hardware Shader Cache", + "Shader Cache", + "Save compiled shaders to disk to reduce loading times on subsequent runs.", + nullptr, + config::category::graphics, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::graphics::resolution_factor, + "Internal Resolution", + "Internal Resolution", + "Render the 3DS screens at a higher resolution. " + "Higher values improve visual quality but significantly impact performance.", + nullptr, + config::category::graphics, + { + { "1", "1x (Native 400x240)" }, + { "2", "2x (800x480)" }, + { "3", "3x (1200x720)" }, + { "4", "4x (1600x960)" }, + { "5", "5x (2000x1200)" }, + { "6", "6x (2400x1440)" }, + { "7", "7x (2800x1680)" }, + { "8", "8x (3200x1920)" }, + { "9", "9x (3600x2160)" }, + { "10", "10x (4000x2400)" }, + { nullptr, nullptr } + }, + "1" + }, + { + config::graphics::texture_filter, + "Texture Filter", + "Texture Filter", + "Apply texture filtering to enhance visual quality. " + "Some filters may significantly impact performance.", + nullptr, + config::category::graphics, + { + { "none", "None" }, + { "Anime4K Ultrafast", "Anime4K Ultrafast" }, + { "Bicubic", "Bicubic" }, + { "ScaleForce", "ScaleForce" }, + { "xBRZ", "xBRZ" }, + { "MMPX", "MMPX" }, + { nullptr, nullptr } + }, + "none" + }, + { + config::graphics::texture_sampling, + "Texture Sampling", + "Texture Sampling", + "Control how textures are sampled and filtered.", + nullptr, + config::category::graphics, + { + { "GameControlled", "Game Controlled" }, + { "NearestNeighbor", "Nearest Neighbor" }, + { "Linear", "Linear" }, + { nullptr, nullptr } + }, + "GameControlled" + }, + { + config::graphics::custom_textures, + "Custom Textures", + "Custom Textures", + "Enable loading of custom texture packs to replace original game textures.", + nullptr, + config::category::graphics, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::disabled + }, + { + config::graphics::dump_textures, + "Dump Game Textures", + "Dump Textures", + "Save original game textures to disk for creating custom texture packs. " + "May impact performance.", + nullptr, + config::category::graphics, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::disabled + }, + + // Layout Category + { + config::layout::layout_option, + "Screen Layout", + "Screen Layout", + "Choose how the 3DS screens are arranged in the display.", + nullptr, + config::category::layout, + { + { "default", "Default Top-Bottom" }, + { "single_screen", "Single Screen Only" }, + { "large_screen", "Large Screen, Small Screen" }, + { "side_by_side", "Side by Side" }, + { nullptr, nullptr } + }, + "default" + }, + { + config::layout::swap_screen, + "Prominent 3DS Screen", + "Prominent Screen", + "Choose which screen is displayed prominently in single screen or large screen layouts.", + nullptr, + config::category::layout, + { + { "Top", "Top Screen" }, + { "Bottom", "Bottom Screen" }, + { nullptr, nullptr } + }, + "Top" + }, + { + config::layout::toggle_swap_screen, + "Screen Swap Mode", + "Swap Mode", + "How screen swapping behaves when using the screen swap hotkey.", + nullptr, + config::category::layout, + { + { "Toggle", "Toggle" }, + { "Hold", "Hold" }, + { nullptr, nullptr } + }, + "Toggle" + }, + + // Storage Category + { + config::storage::use_virtual_sd, + "Enable Virtual SD Card", + "Virtual SD Card", + "Enable virtual SD card support for homebrew and some commercial games.", + nullptr, + config::category::storage, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::storage::use_libretro_save_path, + "Save Data Location", + "Save Location", + "Choose where save data and system files are stored.", + nullptr, + config::category::storage, + { + { "LibRetro Default", "LibRetro Default" }, + { "Azahar Default", "Azahar Default" }, + { nullptr, nullptr } + }, + "LibRetro Default" + }, + + // Input Category + { + config::input::analog_function, + "Right Analog Function", + "Right Analog Function", + "Configure what the right analog stick controls.", + nullptr, + config::category::input, + { + { "c_stick_and_touchscreen", "C-Stick and Touchscreen Pointer" }, + { "touchscreen_pointer", "Touchscreen Pointer" }, + { "c_stick", "C-Stick" }, + { nullptr, nullptr } + }, + "c_stick_and_touchscreen" + }, + { + config::input::deadzone, + "Analog Deadzone", + "Analog Deadzone", + "Set the deadzone percentage for analog input to reduce drift.", + nullptr, + config::category::input, + { + { "0", "0%" }, { "5", "5%" }, { "10", "10%" }, { "15", "15%" }, + { "20", "20%" }, { "25", "25%" }, { "30", "30%" }, { "35", "35%" }, + { nullptr, nullptr } + }, + "15" + }, + { + config::input::mouse_touchscreen, + "Mouse Touchscreen Support", + "Mouse Touchscreen", + "Enable mouse input for touchscreen interactions.", + nullptr, + config::category::input, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::input::touch_touchscreen, + "Touch Device Support", + "Touch Support", + "Enable touch device input for touchscreen interactions.", + nullptr, + config::category::input, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::disabled + }, + { + config::input::render_touchscreen, + "Show Touch Interactions", + "Show Touch", + "Visually indicate touchscreen interactions on screen.", + nullptr, + config::category::input, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::disabled + }, + { + config::input::motion_enabled, + "Gyroscope/Accelerometer Support", + "Motion Support", + "Enable gyroscope and accelerometer input for games that support motion controls.", + nullptr, + config::category::input, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::input::motion_sensitivity, + "Motion Sensitivity", + "Motion Sensitivity", + "Adjust sensitivity of motion controls (gyroscope/accelerometer).", + nullptr, + config::category::input, + { + { "0.1", "10%" }, + { "0.25", "25%" }, + { "0.5", "50%" }, + { "0.75", "75%" }, + { "1.0", "100%" }, + { "1.25", "125%" }, + { "1.5", "150%" }, + { "2.0", "200%" }, + { nullptr, nullptr } + }, + "1.0" + }, + + // Terminator + { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, { { nullptr, nullptr } }, nullptr } +}; +// clang-format on + +static const retro_core_options_v2 options_v2 = { + const_cast(option_categories), + const_cast(option_definitions)}; + +void RegisterCoreOptions(void) { + // Try v2 first, then fallback to v1 and v0 if needed + unsigned version = 0; + if (!LibRetro::GetCoreOptionsVersion(&version)) { + version = 0; + } + + LOG_INFO(Frontend, "Frontend reports core options version: {}", version); + + if (version >= 2) { + if (LibRetro::SetCoreOptionsV2(&options_v2)) { + LOG_INFO(Frontend, "V2 core options set successfully"); + return; + } + } + + LOG_WARNING(Frontend, "V2 core options not supported, trying V1"); + + // Count number of options + unsigned num_options = 0; + while (option_definitions[num_options].key != nullptr) { + num_options++; + } + + if (version >= 1) { + // Create V1 options array + std::vector options_v1(num_options + 1); + + // Copy parameters from V2 to V1 + for (unsigned i = 0; i < num_options; i++) { + const auto& v2_option = option_definitions[i]; + auto& v1_option = options_v1[i]; + + v1_option.key = v2_option.key; + v1_option.desc = v2_option.desc; + v1_option.info = v2_option.info; + v1_option.default_value = v2_option.default_value; + std::memcpy(v1_option.values, v2_option.values, sizeof(v1_option.values)); + } + + // Null terminator + std::memset(&options_v1.back(), 0, sizeof(retro_core_option_definition)); + + if (LibRetro::SetCoreOptionsV1(options_v1.data())) { + LOG_INFO(Frontend, "V1 core options set successfully"); + return; + } + } + + LOG_WARNING(Frontend, "V1 core options not supported, trying V0"); + + // Create V0 variables array + std::vector variables(num_options + 1); + std::vector values_buffer(num_options); + + for (unsigned i = 0; i < num_options; i++) { + const auto& option = option_definitions[i]; + std::string desc = option.desc ? option.desc : ""; + std::string default_value = option.default_value ? option.default_value : ""; + + values_buffer[i] = ""; + + if (!desc.empty()) { + // Count number of values + size_t num_values = 0; + size_t default_index = 0; + + while (option.values[num_values].value != nullptr) { + if (!default_value.empty() && + std::string(option.values[num_values].value) == default_value) { + default_index = num_values; + } + num_values++; + } + + // Build values string: "Description; default_value|other_value1|other_value2" + if (num_values > 0) { + values_buffer[i] = desc + "; " + option.values[default_index].value; + + // Add remaining values + for (size_t j = 0; j < num_values; j++) { + if (j != default_index) { + values_buffer[i] += "|" + std::string(option.values[j].value); + } + } + } + } + + variables[i].key = option.key; + variables[i].value = values_buffer[i].c_str(); + } + + // Null terminator + std::memset(&variables.back(), 0, sizeof(retro_variable)); + + // Set V0 variables + if (LibRetro::SetVariables(variables.data())) { + LOG_INFO(Frontend, "V0 core options set successfully"); + } else { + LOG_ERROR(Frontend, "Failed to set core options with any version"); + } +} + +static void ParseCpuOptions(void) { + Settings::values.use_cpu_jit = + LibRetro::FetchVariable(config::cpu::use_cpu_jit, config::enabled) == config::enabled; +#if defined(IOS) + if (!LibRetro::CanUseJIT()) + Settings::values.use_cpu_jit = false; +#endif + + auto cpu_clock = LibRetro::FetchVariable(config::cpu::cpu_clock_percentage, "100"); + Settings::values.cpu_clock_percentage = std::stoi(cpu_clock); +} + +static int GetRegionValue(const std::string& name) { + if (name == "Japan") + return 0; + if (name == "USA") + return 1; + if (name == "Europe") + return 2; + if (name == "Australia") + return 3; + if (name == "China") + return 4; + if (name == "Korea") + return 5; + if (name == "Taiwan") + return 6; + return -1; // Auto +} + +static Service::CFG::SystemLanguage GetLanguageValue(const std::string& name) { + if (name == "Japanese") + return Service::CFG::LANGUAGE_JP; + if (name == "French") + return Service::CFG::LANGUAGE_FR; + if (name == "Spanish") + return Service::CFG::LANGUAGE_ES; + if (name == "German") + return Service::CFG::LANGUAGE_DE; + if (name == "Italian") + return Service::CFG::LANGUAGE_IT; + if (name == "Dutch") + return Service::CFG::LANGUAGE_NL; + if (name == "Portuguese") + return Service::CFG::LANGUAGE_PT; + if (name == "Russian") + return Service::CFG::LANGUAGE_RU; + if (name == "Korean") + return Service::CFG::LANGUAGE_KO; + if (name == "Traditional Chinese") + return Service::CFG::LANGUAGE_TW; + if (name == "Simplified Chinese") + return Service::CFG::LANGUAGE_ZH; + return Service::CFG::LANGUAGE_EN; // English default +} + +static void ParseSystemOptions(void) { + Settings::values.is_new_3ds = + LibRetro::FetchVariable(config::system::is_new_3ds, "Old 3DS") == "New 3DS"; + + Settings::values.region_value = + GetRegionValue(LibRetro::FetchVariable("citra_region_value", "Auto")); + + LibRetro::settings.language_value = + GetLanguageValue(LibRetro::FetchVariable(config::system::language, "English")); +} + +static Settings::AudioEmulation GetAudioEmulation(const std::string& name) { + if (name == "lle") + return Settings::AudioEmulation::LLE; + if (name == "lle_multithread") + return Settings::AudioEmulation::LLEMultithreaded; + return Settings::AudioEmulation::HLE; // Default +} + +static void ParseAudioOptions(void) { + Settings::values.audio_emulation = + GetAudioEmulation(LibRetro::FetchVariable(config::audio::audio_emulation, "hle")); + + auto input_type = LibRetro::FetchVariable(config::audio::input_type, "auto"); + if (input_type == "none") { + Settings::values.input_type = AudioCore::InputType::Null; + } else if (input_type == "static_noise") { + Settings::values.input_type = AudioCore::InputType::Static; + } else if (input_type == "frontend") { + Settings::values.input_type = AudioCore::InputType::LibRetro; + } else { + Settings::values.input_type = AudioCore::InputType::Auto; + } +} + +static Settings::TextureFilter GetTextureFilter(const std::string& name) { + if (name == "Anime4K Ultrafast") + return Settings::TextureFilter::Anime4K; + if (name == "Bicubic") + return Settings::TextureFilter::Bicubic; + if (name == "ScaleForce") + return Settings::TextureFilter::ScaleForce; + if (name == "xBRZ freescale") + return Settings::TextureFilter::xBRZ; + if (name == "MMPX") + return Settings::TextureFilter::MMPX; + + return Settings::TextureFilter::NoFilter; +} + +static Settings::TextureSampling GetTextureSampling(const std::string& name) { + if (name == "NearestNeighbor") + return Settings::TextureSampling::NearestNeighbor; + if (name == "Linear") + return Settings::TextureSampling::Linear; + + return Settings::TextureSampling::GameControlled; +} + +static Settings::GraphicsAPI GetGraphicsAPI(const std::string& name) { + if (name == "Software") + return Settings::GraphicsAPI::Software; +#ifdef ENABLE_VULKAN + if (name == "Vulkan") + return Settings::GraphicsAPI::Vulkan; +#endif +#ifdef ENABLE_OPENGL + if (name == "OpenGL") + return Settings::GraphicsAPI::OpenGL; +#endif + // Auto selection + return LibRetro::GetPreferredRenderer(); +} + +static void ParseGraphicsOptions(void) { + Settings::values.graphics_api = + GetGraphicsAPI(LibRetro::FetchVariable(config::graphics::graphics_api, "auto")); + + Settings::values.use_hw_shader = LibRetro::FetchVariable(config::graphics::use_hw_shader, + config::enabled) == config::enabled; + + Settings::values.use_shader_jit = LibRetro::FetchVariable(config::graphics::use_shader_jit, + config::enabled) == config::enabled; +#if defined(IOS) + if (!LibRetro::CanUseJIT()) + Settings::values.use_shader_jit = false; +#endif + + Settings::values.shaders_accurate_mul = + LibRetro::FetchVariable(config::graphics::shaders_accurate_mul, config::enabled) == + config::enabled; + + Settings::values.use_disk_shader_cache = + LibRetro::FetchVariable(config::graphics::use_disk_shader_cache, config::enabled) == + config::enabled; + + auto resolution = LibRetro::FetchVariable(config::graphics::resolution_factor, "1"); + Settings::values.resolution_factor = std::stoi(resolution); + + Settings::values.texture_filter = + GetTextureFilter(LibRetro::FetchVariable(config::graphics::texture_filter, "none")); + + Settings::values.texture_sampling = GetTextureSampling( + LibRetro::FetchVariable(config::graphics::texture_sampling, "GameControlled")); + + Settings::values.custom_textures = LibRetro::FetchVariable(config::graphics::custom_textures, + config::disabled) == config::enabled; + + Settings::values.dump_textures = LibRetro::FetchVariable(config::graphics::dump_textures, + config::disabled) == config::enabled; +} + +static Settings::LayoutOption GetLayoutOption(const std::string& name) { + if (name == "single_screen" || name == "Single Screen Only") + return Settings::LayoutOption::SingleScreen; + if (name == "large_screen" || name == "Large Screen, Small Screen") + return Settings::LayoutOption::LargeScreen; + if (name == "side_by_side" || name == "Side by Side") + return Settings::LayoutOption::SideScreen; + return Settings::LayoutOption::Default; +} + +static void ParseLayoutOptions(void) { + Settings::values.layout_option = + GetLayoutOption(LibRetro::FetchVariable(config::layout::layout_option, "default")); + + Settings::values.swap_screen = + LibRetro::FetchVariable(config::layout::swap_screen, "Top") == "Bottom"; + + LibRetro::settings.toggle_swap_screen = + LibRetro::FetchVariable(config::layout::toggle_swap_screen, "Toggle") == "Toggle"; +} + +static void ParseStorageOptions(void) { + Settings::values.use_virtual_sd = LibRetro::FetchVariable(config::storage::use_virtual_sd, + config::enabled) == config::enabled; + + // Configure the file storage location + auto use_libretro_saves = LibRetro::FetchVariable(config::storage::use_libretro_save_path, + "LibRetro Default") == "LibRetro Default"; + + if (use_libretro_saves) { + auto target_dir = LibRetro::GetSaveDir(); + if (target_dir.empty()) { + LOG_INFO(Frontend, "No save dir provided; trying system dir..."); + target_dir = LibRetro::GetSystemDir(); + } + + if (!target_dir.empty()) { + if (!target_dir.ends_with("/")) + target_dir += "/"; + + target_dir += "Azahar/"; + + // Ensure that this new dir exists + if (!FileUtil::CreateDir(target_dir)) { + LOG_ERROR(Frontend, "Failed to create \"{}\". Using Azahar's default paths.", + target_dir); + } else { + FileUtil::SetUserPath(target_dir); + const auto& target_dir_result = FileUtil::GetUserPath(FileUtil::UserPath::UserDir); + LOG_INFO(Frontend, "User dir set to \"{}\".", target_dir_result); + } + } + } +} + +static LibRetro::CStickFunction GetAnalogFunction(const std::string& name) { + if (name == "c_stick" || name == "C-Stick") + return LibRetro::CStickFunction::CStick; + if (name == "touchscreen_pointer" || name == "Touchscreen Pointer") + return LibRetro::CStickFunction::Touchscreen; + return LibRetro::CStickFunction::Both; // Default +} + +static void ParseInputOptions(void) { + LibRetro::settings.analog_function = GetAnalogFunction( + LibRetro::FetchVariable(config::input::analog_function, "c_stick_and_touchscreen")); + + if (LibRetro::settings.analog_function != LibRetro::CStickFunction::Touchscreen) { + Settings::values.current_input_profile.analogs[1] = "axis:1,joystick:0,engine:libretro"; + } else { + Settings::values.current_input_profile.analogs[1] = ""; + } + + auto deadzone = LibRetro::FetchVariable(config::input::deadzone, "15"); + LibRetro::settings.deadzone = static_cast(std::stoi(deadzone)) / 100.0f; + + LibRetro::settings.mouse_touchscreen = + LibRetro::FetchVariable(config::input::mouse_touchscreen, config::enabled) == + config::enabled; + + LibRetro::settings.touch_touchscreen = + LibRetro::FetchVariable(config::input::touch_touchscreen, config::enabled) == + config::enabled; + + LibRetro::settings.render_touchscreen = + LibRetro::FetchVariable(config::input::render_touchscreen, config::disabled) == + config::enabled; + LibRetro::settings.motion_enabled = + LibRetro::FetchVariable(config::input::motion_enabled, config::enabled) == config::enabled; + auto motion_sens = LibRetro::FetchVariable(config::input::motion_sensitivity, "1.0"); + LibRetro::settings.motion_sensitivity = std::stof(motion_sens); + + // Configure motion device based on user settings + if (LibRetro::settings.motion_enabled) { + Settings::values.current_input_profile.motion_device = + "port:0,sensitivity:" + std::to_string(LibRetro::settings.motion_sensitivity) + + ",engine:libretro"; + } else { + Settings::values.current_input_profile.motion_device = "engine:motion_emu"; + } +} + +void ParseCoreOptions(void) { + // Override default values that aren't user-selectable and aren't correct for the core + Settings::values.enable_audio_stretching = false; + Settings::values.frame_limit = 0; +#if defined(USING_GLES) + Settings::values.use_gles = true; +#else + Settings::values.use_gles = false; +#endif + Settings::values.filter_mode = false; + + ParseCpuOptions(); + ParseSystemOptions(); + ParseAudioOptions(); + ParseGraphicsOptions(); + ParseLayoutOptions(); + ParseStorageOptions(); + ParseInputOptions(); +} + +} // namespace LibRetro diff --git a/src/citra_libretro/core_settings.h b/src/citra_libretro/core_settings.h new file mode 100644 index 000000000..78bcff236 --- /dev/null +++ b/src/citra_libretro/core_settings.h @@ -0,0 +1,41 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "core/hle/service/cfg/cfg.h" + +namespace LibRetro { + +enum CStickFunction { Both, CStick, Touchscreen }; + +struct CoreSettings { + + std::string file_path; + + float deadzone = 1.f; + + LibRetro::CStickFunction analog_function; + + bool mouse_touchscreen; + + Service::CFG::SystemLanguage language_value; + + bool touch_touchscreen; + + bool render_touchscreen; + + bool toggle_swap_screen; + + bool motion_enabled; + + float motion_sensitivity; + +} extern settings; + +void RegisterCoreOptions(void); +void ParseCoreOptions(void); + +} // namespace LibRetro diff --git a/src/citra_libretro/emu_window/libretro_window.cpp b/src/citra_libretro/emu_window/libretro_window.cpp new file mode 100644 index 000000000..10d49d883 --- /dev/null +++ b/src/citra_libretro/emu_window/libretro_window.cpp @@ -0,0 +1,342 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#ifdef ENABLE_OPENGL +#include +#endif +#include + +#include "audio_core/audio_types.h" +#include "citra_libretro/citra_libretro.h" +#include "citra_libretro/environment.h" +#include "citra_libretro/input/input_factory.h" +#include "common/settings.h" +#include "core/3ds.h" +#ifdef ENABLE_OPENGL +#include "video_core/renderer_opengl/gl_state.h" +#endif +#include "video_core/gpu.h" +#include "video_core/renderer_software/renderer_software.h" + +#ifdef ENABLE_OPENGL +/// LibRetro expects a "default" GL state. +void ResetGLState() { + // Reset internal state. + OpenGL::OpenGLState state{}; + state.Apply(); + + // Clean up global state. + if (!Settings::values.use_gles) { + glLogicOp(GL_COPY); + } + + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); + glDepthMask(GL_TRUE); + + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + + glDisable(GL_STENCIL_TEST); + glStencilFunc(GL_ALWAYS, 0, 0xFFFFFFFF); + + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ZERO); + glBlendEquation(GL_FUNC_ADD); + glBlendFuncSeparate(GL_ONE, GL_ZERO, GL_ONE, GL_ZERO); + glBlendColor(0, 0, 0, 0); + + glDisable(GL_COLOR_LOGIC_OP); + + glDisable(GL_DITHER); + + glDisable(GL_CULL_FACE); + glCullFace(GL_BACK); + + glActiveTexture(GL_TEXTURE0); +} +#endif + +EmuWindow_LibRetro::EmuWindow_LibRetro() { + strict_context_required = true; + window_info.type = Frontend::WindowSystemType::LibRetro; +} + +EmuWindow_LibRetro::~EmuWindow_LibRetro() {} + +void EmuWindow_LibRetro::SwapBuffers() { + if (suppressPresentation) + return; + submittedFrame = true; + + switch (Settings::values.graphics_api.GetValue()) { + case Settings::GraphicsAPI::OpenGL: { +#ifdef ENABLE_OPENGL + auto current_state = OpenGL::OpenGLState::GetCurState(); + ResetGLState(); + if (enableEmulatedPointer && tracker) { + tracker->Render(width, height); + } + LibRetro::UploadVideoFrame(RETRO_HW_FRAME_BUFFER_VALID, static_cast(width), + static_cast(height), 0); + current_state.Apply(); +#endif + break; + } + case Settings::GraphicsAPI::Vulkan: { +#ifdef ENABLE_VULKAN + if (enableEmulatedPointer && tracker) { + tracker->Render(width, height); + } + LibRetro::UploadVideoFrame(RETRO_HW_FRAME_BUFFER_VALID, static_cast(width), + static_cast(height), 0); +#endif + break; + } + case Settings::GraphicsAPI::Software: { + retro_framebuffer fb; + u8* data; + size_t pitch; + bool did_malloc = false; + if (LibRetro::GetSoftwareFramebuffer(&fb, width, height)) { + data = static_cast(fb.data); + pitch = fb.pitch; + } else { + pitch = static_cast(width) * 4; + data = static_cast(calloc(1, pitch * height)); + did_malloc = true; + } + + std::memset(data, 0, pitch * height); + + auto& system = Core::System::GetInstance(); + const auto& renderer = static_cast(system.GPU().Renderer()); + const auto& layout = GetFramebufferLayout(); + + // Blit a single screen from ScreenInfo (column-major RGBA) to the + // output buffer (row-major XRGB8888), rotating and scaling as needed. + // The 3DS framebuffer is portrait-oriented; ScreenInfo stores pixels + // column-major so the transpose gives us the landscape orientation: + // display (dx, dy) -> ScreenInfo (x=dy, y=dx) + auto blit_screen = [&](VideoCore::ScreenId screen_id, const Common::Rectangle& rect) { + const auto& info = renderer.Screen(screen_id); + if (info.pixels.empty()) + return; + + const u32 rect_w = rect.GetWidth(); + const u32 rect_h = rect.GetHeight(); + if (rect_w == 0 || rect_h == 0) + return; + + // Landscape display dimensions (transposed from portrait storage) + const u32 native_w = info.height; + const u32 native_h = info.width; + + for (u32 oy = 0; oy < rect_h; oy++) { + for (u32 ox = 0; ox < rect_w; ox++) { + const u32 dx = ox * native_w / rect_w; + const u32 dy = oy * native_h / rect_h; + + const u32 src_off = (dy * info.height + dx) * 4; + if (src_off + 3 >= info.pixels.size()) + continue; + + const u8* src = info.pixels.data() + src_off; + const size_t dst_off = static_cast(rect.top + oy) * pitch + + static_cast(rect.left + ox) * 4; + + // RGBA -> XRGB8888 (little-endian: B, G, R, 0) + data[dst_off + 0] = src[2]; + data[dst_off + 1] = src[1]; + data[dst_off + 2] = src[0]; + data[dst_off + 3] = 0; + } + } + }; + + if (layout.top_screen_enabled) { + blit_screen(VideoCore::ScreenId::TopLeft, layout.top_screen); + } + if (layout.bottom_screen_enabled) { + blit_screen(VideoCore::ScreenId::Bottom, layout.bottom_screen); + } + + // Software cursor rendering with framebuffer access + if (enableEmulatedPointer && tracker) { + tracker->Render(width, height, data); + } + + LibRetro::UploadVideoFrame(data, static_cast(width), + static_cast(height), pitch); + if (did_malloc) + free(data); + break; + } + } +} + +void EmuWindow_LibRetro::SetupFramebuffer() { + if (Settings::values.graphics_api.GetValue() != Settings::GraphicsAPI::OpenGL) + return; + +#ifdef ENABLE_OPENGL + // TODO: Expose interface in renderer_opengl to configure this in it's internal state + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(LibRetro::GetFramebuffer())); + + // glClear can be a slow path - skip clearing if we don't need to. + if (doCleanFrame) { + glClear(GL_COLOR_BUFFER_BIT); + + doCleanFrame = false; + } +#endif +} + +void EmuWindow_LibRetro::PollEvents() { + // The software renderer doesn't call render_window.SwapBuffers() — standalone + // frontends (Qt/SDL) use separate presentation threads that pull from screen_infos + // instead. In libretro there's no such thread, so we present here: PollEvents is + // called from EndFrame() during each VBlank, right after PrepareRenderTarget has + // filled the screen pixel buffers. + if (Settings::values.graphics_api.GetValue() == Settings::GraphicsAPI::Software) { + SwapBuffers(); + } + + LibRetro::PollInput(); + + // TODO: Poll for right click for motion emu + + if (enableEmulatedPointer && tracker) { + tracker->Update(width, height, GetFramebufferLayout()); + + if (tracker->IsPressed()) { + auto mousePos = tracker->GetPressedPosition(); + + if (hasTouched) { + TouchMoved(mousePos.first, mousePos.second); + } else { + TouchPressed(mousePos.first, mousePos.second); + hasTouched = true; + } + } else if (hasTouched) { + hasTouched = false; + TouchReleased(); + } + } +} + +void EmuWindow_LibRetro::MakeCurrent() { + // They don't get any say in the matter - GL context is always current! +} + +void EmuWindow_LibRetro::DoneCurrent() { + // They don't get any say in the matter - GL context is always current! +} + +void EmuWindow_LibRetro::OnMinimalClientAreaChangeRequest(std::pair _minimal_size) {} + +LayoutGeometry ComputeLayoutGeometry() { + unsigned baseX; + unsigned baseY; + bool emulated_pointer = true; + + float scaling = Settings::values.resolution_factor.GetValue(); + bool swapped = Settings::values.swap_screen.GetValue(); + + switch (Settings::values.layout_option.GetValue()) { + case Settings::LayoutOption::SingleScreen: + if (swapped) { // Bottom screen visible + baseX = Core::kScreenBottomWidth; + baseY = Core::kScreenBottomHeight; + } else { // Top screen visible + baseX = Core::kScreenTopWidth; + baseY = Core::kScreenTopHeight; + emulated_pointer = false; + } + baseX *= scaling; + baseY *= scaling; + break; + case Settings::LayoutOption::LargeScreen: + if (swapped) { // Bottom screen biggest + baseX = Core::kScreenBottomWidth + Core::kScreenTopWidth / 4; + baseY = Core::kScreenBottomHeight; + } else { // Top screen biggest + baseX = Core::kScreenTopWidth + Core::kScreenBottomWidth / 4; + baseY = Core::kScreenTopHeight; + } + + if (scaling < 4) { + // Unfortunately, to get this aspect ratio correct (and have non-blurry 1x scaling), + // we have to have a pretty large buffer for the minimum ratio. + baseX *= 4; + baseY *= 4; + } else { + baseX *= scaling; + baseY *= scaling; + } + break; + case Settings::LayoutOption::SideScreen: + baseX = Core::kScreenBottomWidth + Core::kScreenTopWidth; + baseY = Core::kScreenTopHeight; + baseX *= scaling; + baseY *= scaling; + break; + case Settings::LayoutOption::Default: + default: + baseX = Core::kScreenTopWidth; + baseY = Core::kScreenTopHeight + Core::kScreenBottomHeight; + baseX *= scaling; + baseY *= scaling; + break; + } + + return {baseX, baseY, emulated_pointer}; +} + +void EmuWindow_LibRetro::UpdateLayout() { + auto geom = ComputeLayoutGeometry(); + unsigned baseX = geom.width; + unsigned baseY = geom.height; + enableEmulatedPointer = geom.emulated_pointer; + + // Update Libretro with our status + struct retro_system_av_info info{}; + info.timing.fps = 60.0; + info.timing.sample_rate = AudioCore::native_sample_rate; + info.geometry.aspect_ratio = (float)baseX / (float)baseY; + info.geometry.base_width = baseX; + info.geometry.base_height = baseY; + info.geometry.max_width = baseX; + info.geometry.max_height = baseY; + if (!LibRetro::SetGeometry(&info)) { + LOG_CRITICAL(Frontend, "Failed to update 3DS layout in frontend!"); + } + + width = baseX; + height = baseY; + + UpdateCurrentFramebufferLayout(baseX, baseY); + + doCleanFrame = true; +} + +bool EmuWindow_LibRetro::NeedsClearing() const { + // We manage this ourselves. + return false; +} + +bool EmuWindow_LibRetro::HasSubmittedFrame() { + bool state = submittedFrame; + submittedFrame = false; + return state; +} + +void EmuWindow_LibRetro::CreateContext() { + tracker = std::make_unique(); + + doCleanFrame = true; +} + +void EmuWindow_LibRetro::DestroyContext() { + tracker = nullptr; +} diff --git a/src/citra_libretro/emu_window/libretro_window.h b/src/citra_libretro/emu_window/libretro_window.h new file mode 100644 index 000000000..235d14319 --- /dev/null +++ b/src/citra_libretro/emu_window/libretro_window.h @@ -0,0 +1,79 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include "citra_libretro/input/mouse_tracker.h" +#include "core/frontend/emu_window.h" + +struct LayoutGeometry { + unsigned width; + unsigned height; + bool emulated_pointer; +}; + +/// Compute framebuffer dimensions from current layout/scaling/swap settings. +LayoutGeometry ComputeLayoutGeometry(); + +void ResetGLState(); + +class EmuWindow_LibRetro : public Frontend::EmuWindow { +public: + EmuWindow_LibRetro(); + ~EmuWindow_LibRetro(); + + /// Swap buffers to display the next frame + void SwapBuffers() override; + + /// Polls window events + void PollEvents() override; + + /// Makes the graphics context current for the caller thread + void MakeCurrent() override; + + /// Releases the GL context from the caller thread + void DoneCurrent() override; + + void SetupFramebuffer() override; + + /// Prepares the window for rendering + void UpdateLayout(); + + /// States whether a frame has been submitted. Resets after call. + bool HasSubmittedFrame(); + + /// Flags that the framebuffer should be cleared. + bool NeedsClearing() const override; + + /// Creates state for a currently running OpenGL context. + void CreateContext(); + + /// Destroys a currently running OpenGL context. + void DestroyContext(); + + /// When true, SwapBuffers() is suppressed (used during savestate drain loops) + bool suppressPresentation = false; + +private: + /// Called when a configuration change affects the minimal size of the window + void OnMinimalClientAreaChangeRequest(std::pair minimal_size) override; + + int width; + int height; + + bool submittedFrame = false; + + // Hack to ensure stuff runs on the main thread + bool doCleanFrame = false; + + // For tracking LibRetro state + bool hasTouched = false; + + // For tracking mouse cursor + std::unique_ptr tracker = nullptr; + + bool enableEmulatedPointer = false; +}; diff --git a/src/citra_libretro/environment.cpp b/src/citra_libretro/environment.cpp new file mode 100644 index 000000000..04c78356f --- /dev/null +++ b/src/citra_libretro/environment.cpp @@ -0,0 +1,281 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include + +#include "audio_core/audio_types.h" +#include "audio_core/libretro_sink.h" +#include "common/scm_rev.h" +#include "core/3ds.h" +#include "emu_window/libretro_window.h" +#include "environment.h" + +#ifdef HAVE_LIBRETRO_VFS +#include "streams/file_stream.h" +#endif + +using namespace LibRetro; + +namespace LibRetro { + +namespace { + +static retro_video_refresh_t video_cb; +static retro_audio_sample_batch_t audio_batch_cb; +static retro_environment_t environ_cb; +static retro_input_poll_t input_poll_cb; +static retro_input_state_t input_state_cb; + +} // namespace + +bool GetSoftwareFramebuffer(retro_framebuffer* fb, int width, int height) { + fb->data = nullptr; + fb->width = width; + fb->height = height; + fb->pitch = 0; + fb->format = RETRO_PIXEL_FORMAT_XRGB8888; + fb->access_flags = RETRO_MEMORY_ACCESS_WRITE; + fb->memory_flags = 0; + return environ_cb(RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER, fb); +} + +void UploadVideoFrame(const void* data, unsigned width, unsigned height, size_t pitch) { + return video_cb(data, width, height, pitch); +} + +bool SetHWSharedContext() { + return environ_cb(RETRO_ENVIRONMENT_SET_HW_SHARED_CONTEXT, NULL); +} + +void PollInput() { + return input_poll_cb(); +} + +bool GetSensorInterface(struct retro_sensor_interface* sensor_interface) { + return environ_cb(RETRO_ENVIRONMENT_GET_SENSOR_INTERFACE, sensor_interface); +} + +bool GetMicrophoneInterface(struct retro_microphone_interface* mic_interface) { + return environ_cb(RETRO_ENVIRONMENT_GET_MICROPHONE_INTERFACE, mic_interface); +} + +Settings::GraphicsAPI GetPreferredRenderer() { + // try and maintain the current driver + retro_hw_context_type context_type = RETRO_HW_CONTEXT_OPENGL; + environ_cb(RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER, &context_type); + switch (context_type) { +#ifdef ENABLE_OPENGL + case RETRO_HW_CONTEXT_OPENGL: + case RETRO_HW_CONTEXT_OPENGL_CORE: + case RETRO_HW_CONTEXT_OPENGLES2: + case RETRO_HW_CONTEXT_OPENGLES3: + case RETRO_HW_CONTEXT_OPENGLES_VERSION: + return Settings::GraphicsAPI::OpenGL; +#endif +#ifdef ENABLE_VULKAN + case RETRO_HW_CONTEXT_VULKAN: + return Settings::GraphicsAPI::Vulkan; +#endif + default: + break; + } + // we can't maintain the current driver, need to switch +#if defined(ENABLE_VULKAN) + return Settings::GraphicsAPI::Vulkan; +#elif defined(ENABLE_OPENGL) + return Settings::GraphicsAPI::OpenGL; +#else + return Settings::GraphicsAPI::Software; +#endif +} + +bool SetVariables(const retro_variable vars[]) { + return environ_cb(RETRO_ENVIRONMENT_SET_VARIABLES, (void*)vars); +} + +bool SetCoreOptionsV2(const retro_core_options_v2* options) { + return environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2, (void*)options); +} + +bool SetCoreOptionsV1(const retro_core_option_definition* options) { + return environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS, (void*)options); +} + +bool GetCoreOptionsVersion(unsigned* version) { + return environ_cb(RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION, version); +} + +bool SetMemoryMaps(const retro_memory_map* map) { + return environ_cb(RETRO_ENVIRONMENT_SET_MEMORY_MAPS, (void*)map); +} + +bool SetControllerInfo(const retro_controller_info info[]) { + return environ_cb(RETRO_ENVIRONMENT_SET_CONTROLLER_INFO, (void*)info); +} + +bool SetPixelFormat(const retro_pixel_format fmt) { + return environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, (void*)&fmt); +} + +bool SetHWRenderer(retro_hw_render_callback* cb) { + return environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER, cb); +} + +bool GetHWRenderInterface(void** interface) { + return environ_cb(RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE, interface) && !!*interface; +} + +bool SetHWRenderContextNegotiationInterface(void** interface) { + return environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE, interface) && + !!*interface; +} + +bool SetAudioCallback(retro_audio_callback* cb) { + return environ_cb(RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK, cb); +} + +bool SetFrameTimeCallback(retro_frame_time_callback* cb) { + return environ_cb(RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK, cb); +} + +bool SetGeometry(retro_system_av_info* cb) { + return environ_cb(RETRO_ENVIRONMENT_SET_GEOMETRY, cb); +} + +bool SetInputDescriptors(const retro_input_descriptor desc[]) { + return environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, (void*)desc); +} + +bool HasUpdatedConfig() { + bool updated = false; + return environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated) && updated; +} + +bool Shutdown() { + return environ_cb(RETRO_ENVIRONMENT_SHUTDOWN, NULL); +} + +/// Displays the specified message to the screen. +bool DisplayMessage(const char* sg) { + retro_message msg; + msg.msg = sg; + msg.frames = 60 * 10; + return environ_cb(RETRO_ENVIRONMENT_SET_MESSAGE, &msg); +} + +bool SetSerializationQuirks(uint64_t quirks) { + return environ_cb(RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS, &quirks); +} + +std::string FetchVariable(std::string key, std::string def) { + struct retro_variable var = {nullptr}; + var.key = key.c_str(); + if (!environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) || var.value == nullptr) { + // Fetching variable failed. + LOG_ERROR(Frontend, "Fetching variable {} failed.", key); + return def; + } + return std::string(var.value); +} + +std::string GetSaveDir() { + char* var = nullptr; + if (!environ_cb(RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY, &var) || var == nullptr) { + // Fetching variable failed. + LOG_ERROR(Frontend, "No save directory provided by LibRetro."); + return std::string(); + } + return std::string(var); +} + +std::string GetSystemDir() { + char* var = nullptr; + if (!environ_cb(RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY, &var) || var == nullptr) { + // Fetching variable failed. + LOG_ERROR(Frontend, "No system directory provided by LibRetro."); + return std::string(); + } + return std::string(var); +} + +retro_log_printf_t GetLoggingBackend() { + retro_log_callback callback{}; + if (!environ_cb(RETRO_ENVIRONMENT_GET_LOG_INTERFACE, &callback)) { + return nullptr; + } + return callback.log; +} + +int16_t CheckInput(unsigned port, unsigned device, unsigned index, unsigned id) { + return input_state_cb(port, device, index, id); +} + +#ifdef HAVE_LIBRETRO_VFS +void SetVFSCallback(struct retro_vfs_interface_info* vfs_iface_info) { + if (environ_cb(RETRO_ENVIRONMENT_GET_VFS_INTERFACE, vfs_iface_info)) + filestream_vfs_init(vfs_iface_info); +} +#endif + +#ifdef IOS +bool CanUseJIT() { + bool can_jit = false; + return environ_cb(RETRO_ENVIRONMENT_GET_JIT_CAPABLE, &can_jit) && can_jit; +} +#endif + +}; // namespace LibRetro + +void retro_get_system_info(struct retro_system_info* info) { + memset(info, 0, sizeof(*info)); + info->library_name = "Azahar"; + info->library_version = Common::g_build_fullname; + info->need_fullpath = true; + info->valid_extensions = "3ds|3dsx|z3dsx|elf|axf|cci|zcci|cxi|zcxi|app"; +} + +void LibRetro::SubmitAudio(const int16_t* data, size_t frames) { + audio_batch_cb(data, frames); +} + +void retro_set_audio_sample(retro_audio_sample_t cb) { + // We don't need single audio sample callbacks. +} + +void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb) { + LibRetro::audio_batch_cb = cb; +} + +void retro_set_input_poll(retro_input_poll_t cb) { + LibRetro::input_poll_cb = cb; +} + +void retro_set_video_refresh(retro_video_refresh_t cb) { + LibRetro::video_cb = cb; +} +void retro_set_environment(retro_environment_t cb) { + LibRetro::environ_cb = cb; + LibRetro::OnConfigureEnvironment(); +} + +void retro_set_controller_port_device(unsigned port, unsigned device) {} + +void retro_set_input_state(retro_input_state_t cb) { + input_state_cb = cb; +} + +void retro_get_system_av_info(struct retro_system_av_info* info) { + info->timing.fps = 60.0; + info->timing.sample_rate = AudioCore::native_sample_rate; + + // Compute geometry from current settings so the frontend allocates the + // correct framebuffer on first use. + auto geom = ComputeLayoutGeometry(); + info->geometry.base_width = geom.width; + info->geometry.base_height = geom.height; + // Max must cover the largest possible layout (SideScreen at 10x = 7200). + info->geometry.max_width = (Core::kScreenBottomWidth + Core::kScreenTopWidth) * 10; + info->geometry.max_height = (Core::kScreenTopHeight + Core::kScreenBottomHeight) * 10; + info->geometry.aspect_ratio = (float)geom.width / (float)geom.height; +} diff --git a/src/citra_libretro/environment.h b/src/citra_libretro/environment.h new file mode 100644 index 000000000..2b84d8b85 --- /dev/null +++ b/src/citra_libretro/environment.h @@ -0,0 +1,129 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "common/logging/backend.h" +#include "common/logging/filter.h" +#include "common/logging/log.h" +#include "common/settings.h" +#include "core/core.h" +#include "libretro.h" + +namespace LibRetro { + +/// May fetch a framebuffer that can be rendered into for software rendering +/// @see RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER +/// @see retro_framebuffer +/// @see retro_video_refresh_t +bool GetSoftwareFramebuffer(retro_framebuffer* fb, int width, int height); + +/// Calls back to LibRetro to upload a particular video frame. +/// @see retro_video_refresh_t +void UploadVideoFrame(const void* data, unsigned width, unsigned height, size_t pitch); + +/// Calls back to LibRetro to poll input. +/// @see retro_input_poll_t +void PollInput(); + +/// Gets the sensor interface for motion input +bool GetSensorInterface(struct retro_sensor_interface* sensor_interface); + +/// Gets the microphone interface for audio input +bool GetMicrophoneInterface(struct retro_microphone_interface* mic_interface); + +/// Sets the environmental variables used for settings. +bool SetVariables(const retro_variable vars[]); + +/// Sets the core options using the v2 interface with categories. +bool SetCoreOptionsV2(const retro_core_options_v2* options); + +/// Sets the core options using the v1 interface. +bool SetCoreOptionsV1(const retro_core_option_definition* options); + +/// Gets the core options version supported by the frontend. +bool GetCoreOptionsVersion(unsigned* version); + +bool SetHWSharedContext(void); + +/// Returns the LibRetro save directory, or a empty string if one doesn't exist. +std::string GetSaveDir(); + +/// Returns the LibRetro system directory, or a empty string if one doesn't exist. +std::string GetSystemDir(); + +/// Fetches a variable by key name. +std::string FetchVariable(std::string key, std::string def); + +/// Returns a logging backend, or null if the frontend refuses to provide one. +retro_log_printf_t GetLoggingBackend(); + +/// Returns graphics api based on global frontend setting +Settings::GraphicsAPI GetPreferredRenderer(); + +/// Displays information about the kinds of controllers that this Citra recreates. +bool SetControllerInfo(const retro_controller_info info[]); + +/// Sets the memory maps for the core. +bool SetMemoryMaps(const retro_memory_map* map); + +/// Sets the framebuffer pixel format. +bool SetPixelFormat(const retro_pixel_format fmt); + +/// Sets the H/W rendering context. +bool SetHWRenderer(retro_hw_render_callback* cb); + +/// Gets the H/W rendering interface. +bool GetHWRenderInterface(void** interface); + +/// Sets the H/W rendering context negotiation interface. +bool SetHWRenderContextNegotiationInterface(void** interface); + +/// Sets the async audio callback. +bool SetAudioCallback(retro_audio_callback* cb); + +/// Sets the frame time callback. +bool SetFrameTimeCallback(retro_frame_time_callback* cb); + +/// Set the size of the new screen buffer. +bool SetGeometry(retro_system_av_info* cb); + +/// Tells LibRetro what input buttons are labelled on the 3DS. +bool SetInputDescriptors(const retro_input_descriptor desc[]); + +/// Returns the current status of a input. +int16_t CheckInput(unsigned port, unsigned device, unsigned index, unsigned id); + +/// Called when the emulator environment is ready to be configured. +void OnConfigureEnvironment(); + +/// Submits audio frames to LibRetro. +/// @see retro_audio_sample_batch_t +void SubmitAudio(const int16_t* data, size_t frames); + +/// Checks to see if the frontend configuration has been updated. +bool HasUpdatedConfig(); + +/// Returns the current framebuffer. +uintptr_t GetFramebuffer(); + +/// Tells the frontend that we are done. +bool Shutdown(); + +/// Displays the specified message to the screen. +bool DisplayMessage(const char* sg); + +/// Sets serialization quirks for the core. +bool SetSerializationQuirks(uint64_t quirks); + +#ifdef HAVE_LIBRETRO_VFS +void SetVFSCallback(struct retro_vfs_interface_info* vfs_iface_info); +#endif + +#ifdef IOS +bool CanUseJIT(); +#endif + +} // namespace LibRetro diff --git a/src/citra_libretro/input/input_factory.cpp b/src/citra_libretro/input/input_factory.cpp new file mode 100644 index 000000000..3a5ae9665 --- /dev/null +++ b/src/citra_libretro/input/input_factory.cpp @@ -0,0 +1,216 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include + +#include "common/math_util.h" +#include "common/vector_math.h" +#include "core/frontend/input.h" + +#include "citra_libretro/environment.h" +#include "citra_libretro/input/input_factory.h" + +namespace LibRetro { + +namespace Input { + +class LibRetroButtonFactory; +class LibRetroAxisFactory; +class LibRetroMotionFactory; + +class LibRetroButton final : public ::Input::ButtonDevice { +public: + explicit LibRetroButton(int joystick_, int button_) : joystick(joystick_), button(button_) {} + + bool GetStatus() const override { + return CheckInput((unsigned int)joystick, RETRO_DEVICE_JOYPAD, 0, (unsigned int)button) > 0; + } + +private: + int joystick; + int button; +}; + +/// A button device factory that creates button devices from LibRetro joystick +class LibRetroButtonFactory final : public ::Input::Factory<::Input::ButtonDevice> { +public: + /** + * Creates a button device from a joystick button + * @param params contains parameters for creating the device: + * - "joystick": the index of the joystick to bind + * - "button": the index of the button to bind + */ + std::unique_ptr<::Input::ButtonDevice> Create(const Common::ParamPackage& params) override { + const int joystick_index = params.Get("joystick", 0); + + const int button = params.Get("button", 0); + return std::make_unique(joystick_index, button); + } +}; + +/// A axis device factory that creates axis devices from LibRetro joystick +class LibRetroAxis final : public ::Input::AnalogDevice { +public: + explicit LibRetroAxis(int joystick_, int button_) : joystick(joystick_), button(button_) {} + + std::tuple GetStatus() const override { + auto axis_x = + (float)CheckInput((unsigned int)joystick, RETRO_DEVICE_ANALOG, (unsigned int)button, 0); + auto axis_y = + (float)CheckInput((unsigned int)joystick, RETRO_DEVICE_ANALOG, (unsigned int)button, 1); + return std::make_tuple(axis_x / INT16_MAX, -axis_y / INT16_MAX); + } + +private: + int joystick; + int button; +}; + +/// A axis device factory that creates axis devices from SDL joystick +class LibRetroAxisFactory final : public ::Input::Factory<::Input::AnalogDevice> { +public: + /** + * Creates a button device from a joystick button + * @param params contains parameters for creating the device: + * - "joystick": the index of the joystick to bind + * - "button"(optional): the index of the button to bind + * - "hat"(optional): the index of the hat to bind as direction buttons + * - "axis"(optional): the index of the axis to bind + * - "direction"(only used for hat): the direction name of the hat to bind. Can be "up", + * "down", "left" or "right" + * - "threshould"(only used for axis): a float value in (-1.0, 1.0) which the button is + * triggered if the axis value crosses + * - "direction"(only used for axis): "+" means the button is triggered when the axis value + * is greater than the threshold; "-" means the button is triggered when the axis value + * is smaller than the threshold + */ + std::unique_ptr<::Input::AnalogDevice> Create(const Common::ParamPackage& params) override { + const int joystick_index = params.Get("joystick", 0); + + const int button = params.Get("axis", 0); + return std::make_unique(joystick_index, button); + } +}; + +/// Static sensor interface callbacks for LibRetro motion input +static retro_sensor_get_input_t sensor_get_input_callback = nullptr; +static retro_set_sensor_state_t sensor_set_state_callback = nullptr; +static bool gyro_enabled = false; +static bool accel_enabled = false; + +/// LibRetro motion device that implements 3DS gyroscope and accelerometer input +class LibRetroMotion final : public ::Input::MotionDevice { +public: + explicit LibRetroMotion(int port_, float sensitivity_) + : port(port_), sensitivity(sensitivity_) { + InitSensors(); + } + + std::tuple, Common::Vec3> GetStatus() const override { + Common::Vec3 accel = {0.0f, 0.0f, -1.0f}; // Default gravity pointing down + Common::Vec3 gyro = {0.0f, 0.0f, 0.0f}; // Default no rotation + + if (sensor_get_input_callback) { + if (accel_enabled) { + // Get accelerometer data (in g units) + // LibRetro coordinate system matches 3DS: X=LEFT, Y=OUT, Z=UP + accel.x = + sensor_get_input_callback(port, RETRO_SENSOR_ACCELEROMETER_X) * sensitivity; + accel.y = + sensor_get_input_callback(port, RETRO_SENSOR_ACCELEROMETER_Y) * sensitivity; + accel.z = + sensor_get_input_callback(port, RETRO_SENSOR_ACCELEROMETER_Z) * sensitivity; + } + + if (gyro_enabled) { + // Get gyroscope data (convert to degrees/sec) + // LibRetro gives radians/sec, 3DS expects degrees/sec + constexpr float RAD_TO_DEG = 180.0f / 3.14159265f; + gyro.x = sensor_get_input_callback(port, RETRO_SENSOR_GYROSCOPE_X) * RAD_TO_DEG * + sensitivity; + gyro.y = sensor_get_input_callback(port, RETRO_SENSOR_GYROSCOPE_Y) * RAD_TO_DEG * + sensitivity; + gyro.z = sensor_get_input_callback(port, RETRO_SENSOR_GYROSCOPE_Z) * RAD_TO_DEG * + sensitivity; + } + } + + return std::make_tuple(accel, gyro); + } + +private: + int port; + float sensitivity; + + void InitSensors() const { + // Initialize sensors if not already done + if (!sensor_get_input_callback || !sensor_set_state_callback) { + struct retro_sensor_interface sensor_interface; + if (LibRetro::GetSensorInterface(&sensor_interface)) { + sensor_get_input_callback = sensor_interface.get_sensor_input; + sensor_set_state_callback = sensor_interface.set_sensor_state; + } + } + + // Enable sensors at 60Hz rate (matching 3DS update frequency) + const unsigned int event_rate = 60; + + if (sensor_set_state_callback) { + if (!accel_enabled && + sensor_set_state_callback(port, RETRO_SENSOR_ACCELEROMETER_ENABLE, event_rate)) { + accel_enabled = true; + } + if (!gyro_enabled && + sensor_set_state_callback(port, RETRO_SENSOR_GYROSCOPE_ENABLE, event_rate)) { + gyro_enabled = true; + } + } + } +}; + +/// Motion device factory that creates motion devices from LibRetro sensor interface +class LibRetroMotionFactory final : public ::Input::Factory<::Input::MotionDevice> { +public: + /** + * Creates a motion device from LibRetro sensor interface + * @param params contains parameters for creating the device: + * - "port": the controller port to read motion from (default 0) + * - "sensitivity": motion sensitivity multiplier (default 1.0) + */ + std::unique_ptr<::Input::MotionDevice> Create(const Common::ParamPackage& params) override { + const int port = params.Get("port", 0); + const float sensitivity = params.Get("sensitivity", 1.0f); + return std::make_unique(port, sensitivity); + } +}; + +void Init() { + using namespace ::Input; + RegisterFactory("libretro", std::make_shared()); + RegisterFactory("libretro", std::make_shared()); + RegisterFactory("libretro", std::make_shared()); +} + +void Shutdown() { + using namespace ::Input; + UnregisterFactory("libretro"); + UnregisterFactory("libretro"); + UnregisterFactory("libretro"); + + // Disable sensors on shutdown + if (sensor_set_state_callback) { + sensor_set_state_callback(0, RETRO_SENSOR_ACCELEROMETER_DISABLE, 60); + sensor_set_state_callback(0, RETRO_SENSOR_GYROSCOPE_DISABLE, 60); + sensor_get_input_callback = nullptr; + sensor_set_state_callback = nullptr; + accel_enabled = false; + gyro_enabled = false; + } +} + +} // namespace Input +} // namespace LibRetro diff --git a/src/citra_libretro/input/input_factory.h b/src/citra_libretro/input/input_factory.h new file mode 100644 index 000000000..74ec5fb74 --- /dev/null +++ b/src/citra_libretro/input/input_factory.h @@ -0,0 +1,20 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "core/frontend/input.h" + +namespace LibRetro { + +namespace Input { + +/// Initializes and registers LibRetro device factories +void Init(); + +/// Unresisters LibRetro device factories and shut them down. +void Shutdown(); + +} // namespace Input +} // namespace LibRetro diff --git a/src/citra_libretro/input/mouse_tracker.cpp b/src/citra_libretro/input/mouse_tracker.cpp new file mode 100644 index 000000000..6829aad2d --- /dev/null +++ b/src/citra_libretro/input/mouse_tracker.cpp @@ -0,0 +1,440 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include + +#include "citra_libretro/core_settings.h" +#include "citra_libretro/environment.h" +#include "citra_libretro/input/mouse_tracker.h" +#include "common/settings.h" +#include "core/frontend/framebuffer_layout.h" + +#ifdef ENABLE_OPENGL +#include +#include "video_core/shader/generator/glsl_shader_gen.h" +#endif + +#ifdef ENABLE_VULKAN +#include "core/core.h" +#include "video_core/gpu.h" +#include "video_core/renderer_vulkan/renderer_vulkan.h" +#endif + +namespace LibRetro { + +namespace Input { + +/// Shared cursor coordinate calculation +struct CursorCoordinates { + float centerX, centerY; + float renderWidth, renderHeight; + float boundingLeft, boundingTop, boundingRight, boundingBottom; + float verticalLeft, verticalRight, verticalTop, verticalBottom; + float horizontalLeft, horizontalRight, horizontalTop, horizontalBottom; + + CursorCoordinates(int bufferWidth, int bufferHeight, float projectedX, float projectedY, + float renderRatio, const Layout::FramebufferLayout& layout) { + // Convert to normalized device coordinates + centerX = (projectedX / bufferWidth) * 2 - 1; + centerY = (projectedY / bufferHeight) * 2 - 1; + + renderWidth = renderRatio / bufferWidth; + renderHeight = renderRatio / bufferHeight; + + boundingLeft = (layout.bottom_screen.left / (float)bufferWidth) * 2 - 1; + boundingTop = (layout.bottom_screen.top / (float)bufferHeight) * 2 - 1; + boundingRight = (layout.bottom_screen.right / (float)bufferWidth) * 2 - 1; + boundingBottom = (layout.bottom_screen.bottom / (float)bufferHeight) * 2 - 1; + + // Calculate cursor dimensions + verticalLeft = std::fmax(centerX - renderWidth / 5, boundingLeft); + verticalRight = std::fmin(centerX + renderWidth / 5, boundingRight); + verticalTop = -std::fmax(centerY - renderHeight, boundingTop); + verticalBottom = -std::fmin(centerY + renderHeight, boundingBottom); + + horizontalLeft = std::fmax(centerX - renderWidth, boundingLeft); + horizontalRight = std::fmin(centerX + renderWidth, boundingRight); + horizontalTop = -std::fmax(centerY - renderHeight / 5, boundingTop); + horizontalBottom = -std::fmin(centerY + renderHeight / 5, boundingBottom); + } +}; + +/// Helper function to check if coordinates are within the touchscreen area +/// (uses the same logic as EmuWindow::IsWithinTouchscreen) +static bool IsWithinTouchscreen(const Layout::FramebufferLayout& layout, unsigned framebuffer_x, + unsigned framebuffer_y) { + // Note: LibRetro doesn't support SeparateWindows, so we can skip that check + + Settings::StereoRenderOption render_3d_mode = Settings::values.render_3d.GetValue(); + + if (render_3d_mode == Settings::StereoRenderOption::SideBySide || + render_3d_mode == Settings::StereoRenderOption::SideBySideFull) { + return (framebuffer_y >= layout.bottom_screen.top && + framebuffer_y < layout.bottom_screen.bottom && + ((framebuffer_x >= layout.bottom_screen.left / 2 && + framebuffer_x < layout.bottom_screen.right / 2) || + (framebuffer_x >= (layout.bottom_screen.left / 2) + (layout.width / 2) && + framebuffer_x < (layout.bottom_screen.right / 2) + (layout.width / 2)))); + } else if (render_3d_mode == Settings::StereoRenderOption::CardboardVR) { + return (framebuffer_y >= layout.bottom_screen.top && + framebuffer_y < layout.bottom_screen.bottom && + ((framebuffer_x >= layout.bottom_screen.left && + framebuffer_x < layout.bottom_screen.right) || + (framebuffer_x >= layout.cardboard.bottom_screen_right_eye + (layout.width / 2) && + framebuffer_x < layout.cardboard.bottom_screen_right_eye + + layout.bottom_screen.GetWidth() + (layout.width / 2)))); + } else { + return (framebuffer_y >= layout.bottom_screen.top && + framebuffer_y < layout.bottom_screen.bottom && + framebuffer_x >= layout.bottom_screen.left && + framebuffer_x < layout.bottom_screen.right); + } +} + +MouseTracker::MouseTracker() { + // Create renderer-specific cursor renderer based on current graphics API + cursor_renderer = nullptr; + switch (Settings::values.graphics_api.GetValue()) { + case Settings::GraphicsAPI::OpenGL: +#ifdef ENABLE_OPENGL + cursor_renderer = std::make_unique(); +#endif + break; + case Settings::GraphicsAPI::Vulkan: +#ifdef ENABLE_VULKAN + cursor_renderer = std::make_unique(); +#endif + break; + case Settings::GraphicsAPI::Software: + cursor_renderer = std::make_unique(); + break; + } +} + +MouseTracker::~MouseTracker() = default; + +void MouseTracker::OnMouseMove(int deltaX, int deltaY) { + x += deltaX; + y += deltaY; +} + +void MouseTracker::Restrict(int minX, int minY, int maxX, int maxY) { + x = std::clamp(x, minX, maxX); + y = std::clamp(y, minY, maxY); +} + +void MouseTracker::Update(int bufferWidth, int bufferHeight, + const Layout::FramebufferLayout& layout) { + bool state = false; + + if (LibRetro::settings.mouse_touchscreen) { + // Check mouse input + state |= LibRetro::CheckInput(0, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_LEFT); + + // Read in and convert pointer values to absolute values on the canvas + auto pointerX = LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_X); + auto pointerY = LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_Y); + auto newX = static_cast((pointerX + 0x7fff) / (float)(0x7fff * 2) * bufferWidth); + auto newY = static_cast((pointerY + 0x7fff) / (float)(0x7fff * 2) * bufferHeight); + + // Use mouse pointer movement + if ((pointerX != 0 || pointerY != 0) && (newX != lastMouseX || newY != lastMouseY)) { + lastMouseX = newX; + lastMouseY = newY; + + // Use layout system to validate and map coordinates + if (IsWithinTouchscreen(layout, newX, newY)) { + x = std::clamp(newX, static_cast(layout.bottom_screen.left), + static_cast(layout.bottom_screen.right)) - + layout.bottom_screen.left; + y = std::clamp(newY, static_cast(layout.bottom_screen.top), + static_cast(layout.bottom_screen.bottom)) - + layout.bottom_screen.top; + } + } + } + + if (LibRetro::settings.touch_touchscreen) { + // Check touchscreen input + state |= LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_PRESSED); + + // Read in and convert pointer values to absolute values on the canvas + auto pointerX = LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_X); + auto pointerY = LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_Y); + auto newX = static_cast((pointerX + 0x7fff) / (float)(0x7fff * 2) * bufferWidth); + auto newY = static_cast((pointerY + 0x7fff) / (float)(0x7fff * 2) * bufferHeight); + + // Use mouse pointer movement + if ((pointerX != 0 || pointerY != 0) && (newX != lastMouseX || newY != lastMouseY)) { + lastMouseX = newX; + lastMouseY = newY; + + // Use layout system to validate and map coordinates + if (IsWithinTouchscreen(layout, newX, newY)) { + x = std::clamp(newX, static_cast(layout.bottom_screen.left), + static_cast(layout.bottom_screen.right)) - + layout.bottom_screen.left; + y = std::clamp(newY, static_cast(layout.bottom_screen.top), + static_cast(layout.bottom_screen.bottom)) - + layout.bottom_screen.top; + } + } + } + + if (LibRetro::settings.analog_function != LibRetro::CStickFunction::CStick) { + // Check right analog input + state |= LibRetro::CheckInput(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R3); + + // TODO: Provide config option for ratios here + auto widthSpeed = (layout.bottom_screen.GetWidth() / 20.0); + auto heightSpeed = (layout.bottom_screen.GetHeight() / 20.0); + + // Use controller movement + float controllerX = + ((float)LibRetro::CheckInput(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, + RETRO_DEVICE_ID_ANALOG_X) / + INT16_MAX); + float controllerY = + ((float)LibRetro::CheckInput(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, + RETRO_DEVICE_ID_ANALOG_Y) / + INT16_MAX); + + // Deadzone the controller inputs + float smoothedX = std::abs(controllerX); + float smoothedY = std::abs(controllerY); + + if (smoothedX < LibRetro::settings.deadzone) { + controllerX = 0; + } + if (smoothedY < LibRetro::settings.deadzone) { + controllerY = 0; + } + + OnMouseMove(static_cast(controllerX * widthSpeed), + static_cast(controllerY * heightSpeed)); + } + + Restrict(0, 0, layout.bottom_screen.GetWidth(), layout.bottom_screen.GetHeight()); + + // Make the coordinates 0 -> 1 + projectedX = (float)x / layout.bottom_screen.GetWidth(); + projectedY = (float)y / layout.bottom_screen.GetHeight(); + + // Ensure that the projected position doesn't overlap outside the bottom screen framebuffer. + // TODO: Provide config option + renderRatio = (float)layout.bottom_screen.GetHeight() / 30; + + // Map the mouse coord to the bottom screen's position + projectedX = layout.bottom_screen.left + projectedX * layout.bottom_screen.GetWidth(); + projectedY = layout.bottom_screen.top + projectedY * layout.bottom_screen.GetHeight(); + + isPressed = state; + + this->framebuffer_layout = layout; +} + +void MouseTracker::Render(int bufferWidth, int bufferHeight, void* framebuffer_data) { + if (!LibRetro::settings.render_touchscreen) { + return; + } + + // Delegate to renderer-specific implementation + if (cursor_renderer) { + cursor_renderer->Render(bufferWidth, bufferHeight, projectedX, projectedY, renderRatio, + framebuffer_layout, framebuffer_data); + } +} + +#ifdef ENABLE_OPENGL +// OpenGL-specific cursor renderer implementation +OpenGLCursorRenderer::OpenGLCursorRenderer() { + // Could potentially also use Citra's built-in shaders, if they can be + // wrangled to cooperate. + + std::string vertex; + if (Settings::values.use_gles) { + vertex += fragment_shader_precision_OES; + } + + vertex += R"( + in vec2 position; + + void main() + { + gl_Position = vec4(position, 0.0, 1.0); + } + )"; + + std::string fragment; + if (Settings::values.use_gles) { + fragment += fragment_shader_precision_OES; + } + fragment += R"( + out vec4 color; + + void main() + { + color = vec4(1.0, 1.0, 1.0, 1.0); + } + )"; + + vao.Create(); + vbo.Create(); + + glBindVertexArray(vao.handle); + glBindBuffer(GL_ARRAY_BUFFER, vbo.handle); + + shader.Create(vertex.c_str(), fragment.c_str()); + + auto positionVariable = (GLuint)glGetAttribLocation(shader.handle, "position"); + glEnableVertexAttribArray(positionVariable); + + glVertexAttribPointer(positionVariable, 2, GL_FLOAT, GL_FALSE, 0, 0); +} + +OpenGLCursorRenderer::~OpenGLCursorRenderer() { + shader.Release(); + vao.Release(); + vbo.Release(); +} + +void OpenGLCursorRenderer::Render(int bufferWidth, int bufferHeight, float projectedX, + float projectedY, float renderRatio, + const Layout::FramebufferLayout& layout, void* framebuffer_data) { + // Use shared coordinate calculation + CursorCoordinates coords(bufferWidth, bufferHeight, projectedX, projectedY, renderRatio, + layout); + + glUseProgram(shader.handle); + + glBindVertexArray(vao.handle); + + // clang-format off + GLfloat cursor[] = { + // | in the cursor + coords.verticalLeft, coords.verticalTop, + coords.verticalRight, coords.verticalTop, + coords.verticalRight, coords.verticalBottom, + + coords.verticalLeft, coords.verticalTop, + coords.verticalRight, coords.verticalBottom, + coords.verticalLeft, coords.verticalBottom, + + // - in the cursor + coords.horizontalLeft, coords.horizontalTop, + coords.horizontalRight, coords.horizontalTop, + coords.horizontalRight, coords.horizontalBottom, + + coords.horizontalLeft, coords.horizontalTop, + coords.horizontalRight, coords.horizontalBottom, + coords.horizontalLeft, coords.horizontalBottom + }; + // clang-format on + + glEnable(GL_BLEND); + glBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ONE_MINUS_SRC_COLOR); + + glBindBuffer(GL_ARRAY_BUFFER, vbo.handle); + glBufferData(GL_ARRAY_BUFFER, sizeof(cursor), cursor, GL_STATIC_DRAW); + + glDrawArrays(GL_TRIANGLES, 0, 12); + + glBindVertexArray(0); + glUseProgram(0); + glDisable(GL_BLEND); +} +#endif + +#ifdef ENABLE_VULKAN +// Vulkan-specific cursor renderer implementation +VulkanCursorRenderer::VulkanCursorRenderer() { + // Vulkan cursor rendering will be integrated into the main rendering pipeline +} + +VulkanCursorRenderer::~VulkanCursorRenderer() = default; + +void VulkanCursorRenderer::Render(int bufferWidth, int bufferHeight, float projectedX, + float projectedY, float renderRatio, + const Layout::FramebufferLayout& layout, void* framebuffer_data) { + // Use shared coordinate calculation + CursorCoordinates coords(bufferWidth, bufferHeight, projectedX, projectedY, renderRatio, + layout); + + // TODO: Implement actual Vulkan cursor drawing using the renderer's command buffer + // This would involve: + // 1. Creating a simple vertex buffer with cursor geometry using coords + // 2. Using a basic shader pipeline + // 3. Recording draw commands into the current command buffer + // 4. Using blend mode similar to OpenGL (ONE_MINUS_DST_COLOR, ONE_MINUS_SRC_COLOR) + + // For now, this is a placeholder - the cursor won't be visible in Vulkan mode + // but the touchscreen input will still work +} +#endif + +// Software-specific cursor renderer implementation +SoftwareCursorRenderer::SoftwareCursorRenderer() { + // Software renderer initialization +} + +SoftwareCursorRenderer::~SoftwareCursorRenderer() = default; + +void SoftwareCursorRenderer::Render(int bufferWidth, int bufferHeight, float projectedX, + float projectedY, float renderRatio, + const Layout::FramebufferLayout& layout, + void* framebuffer_data) { + if (!framebuffer_data) { + return; // No framebuffer data available + } + + // Convert coordinates to screen space + int centerX = static_cast(projectedX); + int centerY = static_cast(projectedY); + int radius = static_cast(renderRatio); + + // Calculate cursor dimensions within bounds + int verticalLeft = std::max(centerX - radius / 5, static_cast(layout.bottom_screen.left)); + int verticalRight = + std::min(centerX + radius / 5, static_cast(layout.bottom_screen.right)); + int verticalTop = std::max(centerY - radius, static_cast(layout.bottom_screen.top)); + int verticalBottom = std::min(centerY + radius, static_cast(layout.bottom_screen.bottom)); + + int horizontalLeft = std::max(centerX - radius, static_cast(layout.bottom_screen.left)); + int horizontalRight = std::min(centerX + radius, static_cast(layout.bottom_screen.right)); + int horizontalTop = std::max(centerY - radius / 5, static_cast(layout.bottom_screen.top)); + int horizontalBottom = + std::min(centerY + radius / 5, static_cast(layout.bottom_screen.bottom)); + + // Draw cursor directly to framebuffer (assuming RGBA8888 format) + uint32_t* pixels = static_cast(framebuffer_data); + const uint32_t cursorColor = 0xFFFFFFFF; // White cursor + + // Draw vertical line of cursor + for (int y = verticalTop; y < verticalBottom; ++y) { + for (int x = verticalLeft; x < verticalRight; ++x) { + if (x >= 0 && x < bufferWidth && y >= 0 && y < bufferHeight) { + int pixelIndex = y * bufferWidth + x; + // XOR blend for visibility on any background + pixels[pixelIndex] ^= cursorColor; + } + } + } + + // Draw horizontal line of cursor + for (int y = horizontalTop; y < horizontalBottom; ++y) { + for (int x = horizontalLeft; x < horizontalRight; ++x) { + if (x >= 0 && x < bufferWidth && y >= 0 && y < bufferHeight) { + int pixelIndex = y * bufferWidth + x; + // XOR blend for visibility on any background + pixels[pixelIndex] ^= cursorColor; + } + } + } +} + +} // namespace Input + +} // namespace LibRetro diff --git a/src/citra_libretro/input/mouse_tracker.h b/src/citra_libretro/input/mouse_tracker.h new file mode 100644 index 000000000..3f4435792 --- /dev/null +++ b/src/citra_libretro/input/mouse_tracker.h @@ -0,0 +1,111 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "common/math_util.h" +#include "core/frontend/framebuffer_layout.h" + +#ifdef ENABLE_OPENGL +#include "video_core/renderer_opengl/gl_resource_manager.h" +#endif + +namespace LibRetro { + +namespace Input { + +class CursorRenderer { +public: + virtual ~CursorRenderer() = default; + virtual void Render(int bufferWidth, int bufferHeight, float projectedX, float projectedY, + float renderRatio, const Layout::FramebufferLayout& layout, + void* framebuffer_data = nullptr) = 0; +}; + +/// The mouse tracker provides a mechanism to handle relative mouse/joypad input +/// for a touch-screen device. +class MouseTracker { +public: + MouseTracker(); + ~MouseTracker(); + + /// Called whenever a mouse moves. + void OnMouseMove(int xDelta, int yDelta); + + /// Restricts the mouse cursor to a specified rectangle. + void Restrict(int minX, int minY, int maxX, int maxY); + + /// Updates the tracker. + void Update(int bufferWidth, int bufferHeight, const Layout::FramebufferLayout& layout); + + /// Renders the cursor to the screen (delegates to renderer-specific implementation). + void Render(int bufferWidth, int bufferHeight, void* framebuffer_data = nullptr); + + /// If the touchscreen is being pressed. + bool IsPressed() { + return isPressed; + } + + /// Get the pressed position, relative to the framebuffer. + std::pair GetPressedPosition() { + return {static_cast(projectedX), + static_cast(projectedY)}; + } + +private: + int x; + int y; + + float lastMouseX; + float lastMouseY; + + float projectedX; + float projectedY; + float renderRatio; + + bool isPressed; + + Layout::FramebufferLayout framebuffer_layout; + std::unique_ptr cursor_renderer; +}; + +#ifdef ENABLE_OPENGL +class OpenGLCursorRenderer : public CursorRenderer { +public: + OpenGLCursorRenderer(); + ~OpenGLCursorRenderer(); + void Render(int bufferWidth, int bufferHeight, float projectedX, float projectedY, + float renderRatio, const Layout::FramebufferLayout& layout, + void* framebuffer_data = nullptr) override; + +private: + OpenGL::OGLProgram shader; + OpenGL::OGLVertexArray vao; + OpenGL::OGLBuffer vbo; +}; +#endif + +#ifdef ENABLE_VULKAN +class VulkanCursorRenderer : public CursorRenderer { +public: + VulkanCursorRenderer(); + ~VulkanCursorRenderer(); + void Render(int bufferWidth, int bufferHeight, float projectedX, float projectedY, + float renderRatio, const Layout::FramebufferLayout& layout, + void* framebuffer_data = nullptr) override; +}; +#endif + +class SoftwareCursorRenderer : public CursorRenderer { +public: + SoftwareCursorRenderer(); + ~SoftwareCursorRenderer(); + void Render(int bufferWidth, int bufferHeight, float projectedX, float projectedY, + float renderRatio, const Layout::FramebufferLayout& layout, + void* framebuffer_data = nullptr) override; +}; + +} // namespace Input + +} // namespace LibRetro diff --git a/src/citra_libretro/libretro.osx.def b/src/citra_libretro/libretro.osx.def new file mode 100644 index 000000000..53a556a60 --- /dev/null +++ b/src/citra_libretro/libretro.osx.def @@ -0,0 +1,27 @@ +#LIBRARY "libretro" +#EXPORTS +_retro_set_environment +_retro_set_video_refresh +_retro_set_audio_sample +_retro_set_audio_sample_batch +_retro_set_input_poll +_retro_set_input_state +_retro_init +_retro_deinit +_retro_api_version +_retro_get_system_info +_retro_get_system_av_info +_retro_set_controller_port_device +_retro_reset +_retro_run +_retro_serialize_size +_retro_serialize +_retro_unserialize +_retro_cheat_reset +_retro_cheat_set +_retro_load_game +_retro_load_game_special +_retro_unload_game +_retro_get_region +_retro_get_memory_data +_retro_get_memory_size diff --git a/src/citra_libretro/libretro_vk.cpp b/src/citra_libretro/libretro_vk.cpp new file mode 100644 index 000000000..eed09d5bc --- /dev/null +++ b/src/citra_libretro/libretro_vk.cpp @@ -0,0 +1,860 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include + +#include "citra_libretro/environment.h" +#include "citra_libretro/libretro_vk.h" +#include "common/assert.h" +#include "common/logging/log.h" +#include "common/settings.h" +#include "core/frontend/emu_window.h" +#include "video_core/renderer_vulkan/vk_scheduler.h" + +#include + +static const struct retro_hw_render_interface_vulkan* vulkan_intf; + +namespace LibRetro { + +const VkApplicationInfo* GetVulkanApplicationInfo() { + static VkApplicationInfo app_info{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + app_info.pApplicationName = "Azahar"; + app_info.applicationVersion = VK_MAKE_VERSION(1, 0, 0); + app_info.pEngineName = "Azahar"; + app_info.engineVersion = VK_MAKE_VERSION(1, 0, 0); + // Request Vulkan 1.1 for better compatibility (especially on Android) + // Extensions can be used for features beyond 1.1 + app_info.apiVersion = VK_API_VERSION_1_1; + return &app_info; +} + +void AddExtensionIfAvailable(std::vector& enabled_exts, + const std::vector& available_exts, + const char* ext_name) { + // Check if already in the list + for (const char* ext : enabled_exts) { + if (ext && !strcmp(ext, ext_name)) { + return; // Already enabled + } + } + + // Check if available + for (const auto& ext : available_exts) { + if (!strcmp(ext.extensionName, ext_name)) { + enabled_exts.push_back(ext_name); + LOG_INFO(Render_Vulkan, "Enabling Vulkan extension: {}", ext_name); + return; + } + } + + LOG_DEBUG(Render_Vulkan, "Vulkan extension {} not available", ext_name); +} + +bool CreateVulkanDevice(struct retro_vulkan_context* context, VkInstance instance, + VkPhysicalDevice gpu, VkSurfaceKHR surface, + PFN_vkGetInstanceProcAddr get_instance_proc_addr, + const char** required_device_extensions, + unsigned num_required_device_extensions, + const char** required_device_layers, unsigned num_required_device_layers, + const VkPhysicalDeviceFeatures* required_features) { + + LOG_INFO(Render_Vulkan, "CreateDevice callback invoked - negotiating Vulkan device creation"); + + // Get available extensions for this physical device + uint32_t ext_count = 0; + PFN_vkEnumerateDeviceExtensionProperties vkEnumerateDeviceExtensionProperties = + (PFN_vkEnumerateDeviceExtensionProperties)get_instance_proc_addr( + instance, "vkEnumerateDeviceExtensionProperties"); + + vkEnumerateDeviceExtensionProperties(gpu, nullptr, &ext_count, nullptr); + std::vector available_exts(ext_count); + if (ext_count > 0) { + vkEnumerateDeviceExtensionProperties(gpu, nullptr, &ext_count, available_exts.data()); + } + + // Start with frontend's required extensions + std::vector enabled_exts; + enabled_exts.reserve(num_required_device_extensions + 10); + for (unsigned i = 0; i < num_required_device_extensions; i++) { + if (required_device_extensions[i]) { + enabled_exts.push_back(required_device_extensions[i]); + } + } + + // Add extensions we want (if available) + AddExtensionIfAvailable(enabled_exts, available_exts, VK_KHR_SWAPCHAIN_EXTENSION_NAME); + AddExtensionIfAvailable(enabled_exts, available_exts, VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME); + AddExtensionIfAvailable(enabled_exts, available_exts, + VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME); + AddExtensionIfAvailable(enabled_exts, available_exts, + VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME); + AddExtensionIfAvailable(enabled_exts, available_exts, VK_EXT_TOOLING_INFO_EXTENSION_NAME); + + // These are beneficial but blacklisted on some platforms due to driver bugs + // For now, let the Instance class handle these decisions + // AddExtensionIfAvailable(enabled_exts, available_exts, + // VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME); + // AddExtensionIfAvailable(enabled_exts, available_exts, + // VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME); + + // Merge frontend's required features with our baseline + VkPhysicalDeviceFeatures merged_features{}; + if (required_features) { + // Copy all frontend requirements + for (unsigned i = 0; i < sizeof(VkPhysicalDeviceFeatures) / sizeof(VkBool32); i++) { + if (reinterpret_cast(required_features)[i]) { + reinterpret_cast(&merged_features)[i] = VK_TRUE; + } + } + } + + // Request features we need (these will be OR'd with frontend requirements) + // The Instance class will validate these against actual device capabilities + merged_features.geometryShader = VK_TRUE; // Used for certain rendering effects + merged_features.logicOp = VK_TRUE; // Used for blending modes + merged_features.samplerAnisotropy = VK_TRUE; // Used for texture filtering + + // Find queue family with graphics support + PFN_vkGetPhysicalDeviceQueueFamilyProperties vkGetPhysicalDeviceQueueFamilyProperties = + (PFN_vkGetPhysicalDeviceQueueFamilyProperties)get_instance_proc_addr( + instance, "vkGetPhysicalDeviceQueueFamilyProperties"); + + uint32_t queue_family_count = 0; + vkGetPhysicalDeviceQueueFamilyProperties(gpu, &queue_family_count, nullptr); + std::vector queue_families(queue_family_count); + vkGetPhysicalDeviceQueueFamilyProperties(gpu, &queue_family_count, queue_families.data()); + + uint32_t graphics_queue_family = VK_QUEUE_FAMILY_IGNORED; + for (uint32_t i = 0; i < queue_family_count; i++) { + if (queue_families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { + graphics_queue_family = i; + break; + } + } + + if (graphics_queue_family == VK_QUEUE_FAMILY_IGNORED) { + LOG_CRITICAL(Render_Vulkan, "No graphics queue family found!"); + return false; + } + + // Create device + const float queue_priority = 1.0f; + VkDeviceQueueCreateInfo queue_info{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queue_info.queueFamilyIndex = graphics_queue_family; + queue_info.queueCount = 1; + queue_info.pQueuePriorities = &queue_priority; + + VkDeviceCreateInfo device_info{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; + device_info.queueCreateInfoCount = 1; + device_info.pQueueCreateInfos = &queue_info; + device_info.enabledExtensionCount = static_cast(enabled_exts.size()); + device_info.ppEnabledExtensionNames = enabled_exts.data(); + device_info.enabledLayerCount = num_required_device_layers; + device_info.ppEnabledLayerNames = required_device_layers; + device_info.pEnabledFeatures = &merged_features; + + PFN_vkCreateDevice vkCreateDevice = + (PFN_vkCreateDevice)get_instance_proc_addr(instance, "vkCreateDevice"); + + VkDevice device = VK_NULL_HANDLE; + VkResult result = vkCreateDevice(gpu, &device_info, nullptr, &device); + if (result != VK_SUCCESS) { + LOG_CRITICAL(Render_Vulkan, "vkCreateDevice failed: {}", static_cast(result)); + return false; + } + + // Get the queue + PFN_vkGetDeviceQueue vkGetDeviceQueue = + (PFN_vkGetDeviceQueue)get_instance_proc_addr(instance, "vkGetDeviceQueue"); + + VkQueue queue = VK_NULL_HANDLE; + vkGetDeviceQueue(device, graphics_queue_family, 0, &queue); + + // Fill in the context for the frontend + context->gpu = gpu; + context->device = device; + context->queue = queue; + context->queue_family_index = graphics_queue_family; + context->presentation_queue = queue; // Same queue for LibRetro + context->presentation_queue_family_index = graphics_queue_family; + + LOG_INFO(Render_Vulkan, + "Vulkan device created successfully via negotiation interface (GPU: {}, Queue " + "Family: {})", + static_cast(gpu), graphics_queue_family); + + return true; +} + +void VulkanResetContext() { + LibRetro::GetHWRenderInterface((void**)&vulkan_intf); + + // Initialize dispatcher with LibRetro's function pointers + VULKAN_HPP_DEFAULT_DISPATCHER.init(vulkan_intf->get_instance_proc_addr); + + vk::Instance vk_instance{vulkan_intf->instance}; + VULKAN_HPP_DEFAULT_DISPATCHER.init(vk_instance); +} + +} // namespace LibRetro + +namespace Vulkan { + +std::shared_ptr OpenLibrary( + [[maybe_unused]] Frontend::GraphicsContext* context) { + // the frontend takes care of this, we'll get the instance later + return std::make_shared(); +} + +vk::SurfaceKHR CreateSurface(vk::Instance instance, const Frontend::EmuWindow& emu_window) { + // LibRetro cores don't use surfaces - we render to our own output texture + // This function should not be called in LibRetro mode + LOG_WARNING(Render_Vulkan, "CreateSurface called in LibRetro mode - this should not happen"); + return VK_NULL_HANDLE; +} + +vk::UniqueInstance CreateInstance([[maybe_unused]] const Common::DynamicLibrary& library, + [[maybe_unused]] Frontend::WindowSystemType window_type, + [[maybe_unused]] bool enable_validation, + [[maybe_unused]] bool dump_command_buffers) { + // LibRetro cores don't create instances - frontend handles this + LOG_WARNING(Render_Vulkan, "CreateInstance called in LibRetro mode - this should not happen"); + return vk::UniqueInstance{}; +} + +DebugCallback CreateDebugCallback(vk::Instance instance, bool& debug_utils_supported) { + // LibRetro handles debugging, return empty callback + debug_utils_supported = false; + return {}; +} + +LibRetroVKInstance::LibRetroVKInstance(Frontend::EmuWindow& window, + [[maybe_unused]] u32 physical_device_index) + : Instance(Instance::NoInit{}) { + // Ensure LibRetro interface is available + if (!vulkan_intf) { + LOG_CRITICAL(Render_Vulkan, "LibRetro Vulkan interface not initialized!"); + throw std::runtime_error("LibRetro Vulkan interface not available"); + } + + // Initialize basic Vulkan objects from LibRetro + physical_device = vulkan_intf->gpu; + if (!physical_device) { + LOG_CRITICAL(Render_Vulkan, "LibRetro provided invalid physical device!"); + throw std::runtime_error("Invalid physical device from LibRetro"); + } + + // Get device properties and features + properties = physical_device.getProperties(); + + const std::vector extensions = physical_device.enumerateDeviceExtensionProperties(); + available_extensions.reserve(extensions.size()); + for (const auto& extension : extensions) { + available_extensions.emplace_back(extension.extensionName.data()); + } + + // Get queues from LibRetro + graphics_queue = vulkan_intf->queue; + queue_family_index = vulkan_intf->queue_index; + present_queue = graphics_queue; // Same queue for LibRetro + + if (!graphics_queue) { + LOG_CRITICAL(Render_Vulkan, "LibRetro provided invalid graphics queue!"); + throw std::runtime_error("Invalid graphics queue from LibRetro"); + } + + // Initialize Vulkan HPP dispatcher with LibRetro's device + VULKAN_HPP_DEFAULT_DISPATCHER.init(vk::Device{vulkan_intf->device}); + + // Now run device capability detection with dispatcher initialized + CreateDevice(); + + // LibRetro-specific: Validate function pointers are actually available + // LibRetro's device may not have loaded all extension functions even if extensions are + // available + if (extended_dynamic_state) { + if (!VULKAN_HPP_DEFAULT_DISPATCHER.vkCmdSetCullModeEXT || + !VULKAN_HPP_DEFAULT_DISPATCHER.vkCmdSetDepthTestEnableEXT || + !VULKAN_HPP_DEFAULT_DISPATCHER.vkCmdSetDepthWriteEnableEXT || + !VULKAN_HPP_DEFAULT_DISPATCHER.vkCmdSetFrontFaceEXT) { + LOG_WARNING(Render_Vulkan, "Extended dynamic state function pointers not available in " + "LibRetro context, disabling"); + extended_dynamic_state = false; + } + } + + if (timeline_semaphores) { + if (!VULKAN_HPP_DEFAULT_DISPATCHER.vkGetSemaphoreCounterValueKHR) { + LOG_WARNING(Render_Vulkan, "Timeline semaphore function pointers not available in " + "LibRetro context, disabling"); + timeline_semaphores = false; + } + } + + // Initialize subsystems + CreateAllocator(); + CreateFormatTable(); + CollectToolingInfo(); + CreateCustomFormatTable(); + CreateAttribTable(); + + LOG_INFO(Render_Vulkan, "LibRetro Vulkan Instance initialized successfully"); + LOG_INFO(Render_Vulkan, "Device: {} ({})", properties.deviceName.data(), GetVendorName()); + LOG_INFO(Render_Vulkan, "Driver: {}", GetDriverVersionName()); +} + +vk::Instance LibRetroVKInstance::GetInstance() const { + return vk::Instance{vulkan_intf->instance}; +} + +vk::Device LibRetroVKInstance::GetDevice() const { + return vk::Device{vulkan_intf->device}; +} + +// ============================================================================ +// PresentWindow Implementation (LibRetro version) +// ============================================================================ + +PresentWindow::PresentWindow(Frontend::EmuWindow& emu_window_, const Instance& instance_, + Scheduler& scheduler_, [[maybe_unused]] bool low_refresh_rate) + : emu_window{emu_window_}, instance{instance_}, scheduler{scheduler_}, + graphics_queue{instance.GetGraphicsQueue()} { + const vk::Device device = instance.GetDevice(); + + LOG_INFO(Render_Vulkan, "Initializing LibRetro PresentWindow"); + + // Create command pool for frame operations + const vk::CommandPoolCreateInfo pool_info = { + .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer | + vk::CommandPoolCreateFlagBits::eTransient, + .queueFamilyIndex = instance.GetGraphicsQueueFamilyIndex(), + }; + command_pool = device.createCommandPool(pool_info); + + // Create render pass for LibRetro output + present_renderpass = CreateRenderpass(); + + // Start with initial dimensions from layout + const auto& layout = emu_window.GetFramebufferLayout(); + CreateOutputTexture(layout.width, layout.height); + CreateFrameResources(); + + LOG_INFO(Render_Vulkan, "LibRetro PresentWindow initialized with {}x{}", layout.width, + layout.height); +} + +PresentWindow::~PresentWindow() { + const vk::Device device = instance.GetDevice(); + + LOG_DEBUG(Render_Vulkan, "Destroying LibRetro PresentWindow"); + + // Wait for any pending operations + WaitPresent(); + device.waitIdle(); + + // Destroy frame resources + DestroyFrameResources(); + + // Destroy output texture + DestroyOutputTexture(); + + // Destroy Vulkan objects + if (command_pool) { + device.destroyCommandPool(command_pool); + } + if (present_renderpass) { + device.destroyRenderPass(present_renderpass); + } +} + +void PresentWindow::CreateOutputTexture(u32 width, u32 height) { + if (width == 0 || height == 0) { + LOG_ERROR(Render_Vulkan, "Invalid output texture dimensions: {}x{}", width, height); + return; + } + + // Destroy existing texture if dimensions changed + if (output_image && (output_width != width || output_height != height)) { + DestroyOutputTexture(); + } + + // Skip if already created with correct dimensions + if (output_image && output_width == width && output_height == height) { + return; + } + + const vk::Device device = instance.GetDevice(); + output_width = width; + output_height = height; + + // Create output image with LibRetro requirements + const vk::ImageCreateInfo image_info = { + .imageType = vk::ImageType::e2D, + .format = output_format, + .extent = {width, height, 1}, + .mipLevels = 1, + .arrayLayers = 1, + .samples = vk::SampleCountFlagBits::e1, + .tiling = vk::ImageTiling::eOptimal, + .usage = vk::ImageUsageFlagBits::eColorAttachment | // For rendering + vk::ImageUsageFlagBits::eTransferSrc | // Required by LibRetro + vk::ImageUsageFlagBits::eSampled | // Required by LibRetro + vk::ImageUsageFlagBits::eTransferDst, // For clearing + .sharingMode = vk::SharingMode::eExclusive, + .initialLayout = vk::ImageLayout::eUndefined, + }; + + // Create image with VMA - using budget-aware allocation like standalone version + VmaAllocationCreateInfo alloc_info = {}; + alloc_info.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE; + alloc_info.flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT; + + VkImage vk_image; + const VkResult result = vmaCreateImage(instance.GetAllocator(), + reinterpret_cast(&image_info), + &alloc_info, &vk_image, &output_allocation, nullptr); + + if (result != VK_SUCCESS) { + LOG_CRITICAL(Render_Vulkan, "Failed to create output image: {}", static_cast(result)); + throw std::runtime_error("Failed to create LibRetro output texture"); + } + + output_image = vk::Image{vk_image}; + + // Create image view + output_view_create_info = { + .image = output_image, + .viewType = vk::ImageViewType::e2D, + .format = output_format, + .components = + { + .r = vk::ComponentSwizzle::eIdentity, + .g = vk::ComponentSwizzle::eIdentity, + .b = vk::ComponentSwizzle::eIdentity, + .a = vk::ComponentSwizzle::eIdentity, + }, + .subresourceRange = + { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; + output_image_view = device.createImageView(output_view_create_info); + + LOG_DEBUG(Render_Vulkan, "Created LibRetro output texture: {}x{}", width, height); +} + +void PresentWindow::DestroyOutputTexture() { + if (!output_image) { + return; + } + + const vk::Device device = instance.GetDevice(); + + if (output_image_view) { + device.destroyImageView(output_image_view); + output_image_view = nullptr; + } + + if (output_allocation) { + vmaDestroyImage(instance.GetAllocator(), static_cast(output_image), + output_allocation); + output_allocation = {}; + } + + output_image = nullptr; + output_width = 0; + output_height = 0; +} + +vk::RenderPass PresentWindow::CreateRenderpass() { + const vk::AttachmentDescription color_attachment = { + .format = output_format, + .samples = vk::SampleCountFlagBits::e1, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .stencilLoadOp = vk::AttachmentLoadOp::eDontCare, + .stencilStoreOp = vk::AttachmentStoreOp::eDontCare, + .initialLayout = vk::ImageLayout::eUndefined, + .finalLayout = vk::ImageLayout::eShaderReadOnlyOptimal, // Ready for LibRetro + }; + + const vk::AttachmentReference color_ref = { + .attachment = 0, + .layout = vk::ImageLayout::eColorAttachmentOptimal, + }; + + const vk::SubpassDescription subpass = { + .pipelineBindPoint = vk::PipelineBindPoint::eGraphics, + .colorAttachmentCount = 1, + .pColorAttachments = &color_ref, + }; + + const vk::SubpassDependency dependency = { + .srcSubpass = VK_SUBPASS_EXTERNAL, + .dstSubpass = 0, + .srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput, + .dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput, + .srcAccessMask = {}, + .dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, + }; + + const vk::RenderPassCreateInfo renderpass_info = { + .attachmentCount = 1, + .pAttachments = &color_attachment, + .subpassCount = 1, + .pSubpasses = &subpass, + .dependencyCount = 1, + .pDependencies = &dependency, + }; + + return instance.GetDevice().createRenderPass(renderpass_info); +} + +void PresentWindow::CreateFrameResources() { + const vk::Device device = instance.GetDevice(); + const u32 frame_count = 2; // Double buffering for LibRetro + + // Destroy existing frames + DestroyFrameResources(); + + // Create frame pool + frame_pool.resize(frame_count); + + // Allocate command buffers + const vk::CommandBufferAllocateInfo alloc_info = { + .commandPool = command_pool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = frame_count, + }; + const std::vector command_buffers = device.allocateCommandBuffers(alloc_info); + + // Initialize frames + for (u32 i = 0; i < frame_count; i++) { + Frame& frame = frame_pool[i]; + frame.width = output_width; + frame.height = output_height; + frame.image = output_image; // All frames use the same output texture + frame.image_view = output_image_view; + frame.allocation = {}; // VMA allocation handled separately + frame.cmdbuf = command_buffers[i]; + frame.render_ready = device.createSemaphore({}); + frame.present_done = device.createFence({.flags = vk::FenceCreateFlagBits::eSignaled}); + + // Create framebuffer for this frame + const vk::FramebufferCreateInfo fb_info = { + .renderPass = present_renderpass, + .attachmentCount = 1, + .pAttachments = &output_image_view, + .width = output_width, + .height = output_height, + .layers = 1, + }; + frame.framebuffer = device.createFramebuffer(fb_info); + } + + LOG_DEBUG(Render_Vulkan, "Created {} frame resources for LibRetro", frame_count); +} + +void PresentWindow::DestroyFrameResources() { + if (frame_pool.empty()) { + return; + } + + const vk::Device device = instance.GetDevice(); + + for (auto& frame : frame_pool) { + if (frame.framebuffer) { + device.destroyFramebuffer(frame.framebuffer); + } + if (frame.render_ready) { + device.destroySemaphore(frame.render_ready); + } + if (frame.present_done) { + device.destroyFence(frame.present_done); + } + } + + frame_pool.clear(); + current_frame_index = 0; +} + +Frame* PresentWindow::GetRenderFrame() { + if (frame_pool.empty()) { + LOG_ERROR(Render_Vulkan, "No frames available in LibRetro PresentWindow"); + return nullptr; + } + + // RetroArch may not call context_reset during fullscreen toggle, leaving us + // with a stale interface pointer that can crash + const struct retro_hw_render_interface_vulkan* current_intf = nullptr; + if (!LibRetro::GetHWRenderInterface((void**)¤t_intf) || !current_intf) { + LOG_ERROR(Render_Vulkan, "Failed to get current Vulkan interface"); + return &frame_pool[current_frame_index]; + } + + // Update global interface if it changed + if (current_intf != vulkan_intf) { + LOG_INFO(Render_Vulkan, "Vulkan interface changed during runtime from {} to {}", + static_cast(vulkan_intf), static_cast(current_intf)); + vulkan_intf = current_intf; + } + + // LibRetro synchronization: Use LibRetro's wait mechanism instead of fences + if (vulkan_intf && vulkan_intf->wait_sync_index && vulkan_intf->handle) { + vulkan_intf->wait_sync_index(vulkan_intf->handle); + } + + // Use LibRetro's sync index for frame selection if available + u32 frame_index = current_frame_index; + if (vulkan_intf && vulkan_intf->get_sync_index && vulkan_intf->handle) { + LOG_TRACE(Render_Vulkan, "Calling get_sync_index with handle: {}", + static_cast(vulkan_intf->handle)); + + const u32 sync_index = vulkan_intf->get_sync_index(vulkan_intf->handle); + frame_index = sync_index % frame_pool.size(); + LOG_TRACE(Render_Vulkan, "LibRetro sync index: {}, using frame: {}", sync_index, + frame_index); + } + + return &frame_pool[frame_index]; +} + +void PresentWindow::RecreateFrame(Frame* frame, u32 width, u32 height) { + if (!frame) { + LOG_ERROR(Render_Vulkan, "Invalid frame for recreation"); + return; + } + + if (frame->width == width && frame->height == height) { + return; // No change needed + } + + LOG_DEBUG(Render_Vulkan, "Recreating LibRetro frame: {}x{} -> {}x{}", frame->width, + frame->height, width, height); + + // Wait for frame to be idle + const vk::Device device = instance.GetDevice(); + [[maybe_unused]] const vk::Result wait_result = + device.waitForFences(frame->present_done, VK_TRUE, UINT64_MAX); + + // Recreate output texture with new dimensions + CreateOutputTexture(width, height); + + // Recreate frame resources + CreateFrameResources(); + + LOG_INFO(Render_Vulkan, "LibRetro frame recreated for {}x{}", width, height); +} + +void PresentWindow::Present(Frame* frame) { + if (!frame) { + LOG_ERROR(Render_Vulkan, "Cannot present null frame"); + return; + } + + if (!vulkan_intf) { + LOG_ERROR(Render_Vulkan, "LibRetro Vulkan interface not available for presentation"); + return; + } + + // CRITICAL: Use persistent struct to avoid stack lifetime issues! + // RetroArch may cache this pointer for frame duping during pause + persistent_libretro_image.image_view = static_cast(frame->image_view); + persistent_libretro_image.image_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + persistent_libretro_image.create_info = + static_cast(output_view_create_info); + + vulkan_intf->set_image(vulkan_intf->handle, &persistent_libretro_image, 0, nullptr, + instance.GetGraphicsQueueFamilyIndex()); + + // Call EmuWindow SwapBuffers to trigger LibRetro video frame submission + emu_window.SwapBuffers(); + + // LibRetro manages frame indices via sync_index, so we don't manually increment + // current_frame_index = (current_frame_index + 1) % frame_pool.size(); + + LOG_TRACE(Render_Vulkan, "Frame presented to LibRetro: {}x{}", frame->width, frame->height); +} + +void PresentWindow::WaitPresent() { + if (frame_pool.empty()) { + return; + } + + const vk::Device device = instance.GetDevice(); + + // Wait for all frames to complete + std::vector fences; + fences.reserve(frame_pool.size()); + + for (const auto& frame : frame_pool) { + fences.push_back(frame.present_done); + } + + if (!fences.empty()) { + [[maybe_unused]] const vk::Result wait_result = + device.waitForFences(fences, VK_TRUE, UINT64_MAX); + } +} + +void PresentWindow::NotifySurfaceChanged() { + // LibRetro doesn't use surfaces, so this is a no-op + LOG_DEBUG(Render_Vulkan, "Surface change notification ignored in LibRetro mode"); +} + +// ============================================================================ +// MasterSemaphoreLibRetro Implementation +// ============================================================================ + +constexpr u64 FENCE_RESERVE = 8; + +MasterSemaphoreLibRetro::MasterSemaphoreLibRetro(const Instance& instance_) : instance{instance_} { + const vk::Device device{instance.GetDevice()}; + // Pre-allocate fence pool + for (u64 i = 0; i < FENCE_RESERVE; i++) { + free_queue.push_back(device.createFence({})); + } + // Start background wait thread + wait_thread = std::jthread([this](std::stop_token token) { WaitThread(token); }); +} + +MasterSemaphoreLibRetro::~MasterSemaphoreLibRetro() { + // wait_thread will be automatically stopped by jthread destructor + // Clean up remaining fences + const vk::Device device{instance.GetDevice()}; + for (const auto& fence : free_queue) { + device.destroyFence(fence); + } +} + +void MasterSemaphoreLibRetro::Refresh() {} + +void MasterSemaphoreLibRetro::Wait(u64 tick) { + std::unique_lock lock{free_mutex}; + free_cv.wait(lock, [this, tick] { return gpu_tick.load(std::memory_order_relaxed) >= tick; }); +} + +void MasterSemaphoreLibRetro::SubmitWork(vk::CommandBuffer cmdbuf, vk::Semaphore wait, + vk::Semaphore signal, u64 signal_value) { + if (!vulkan_intf) { + LOG_ERROR(Render_Vulkan, "LibRetro Vulkan interface not available for command submission"); + return; + } + + cmdbuf.end(); + + // Get a fence from the pool + const vk::Fence fence = GetFreeFence(); + + // Strip semaphores - RetroArch handles frame sync, we track resources internally + const vk::SubmitInfo submit_info = { + .waitSemaphoreCount = 0, + .pWaitSemaphores = nullptr, + .pWaitDstStageMask = nullptr, + .commandBufferCount = 1u, + .pCommandBuffers = &cmdbuf, + .signalSemaphoreCount = 0, + .pSignalSemaphores = nullptr, + }; + + // Use LibRetro's queue coordination + if (vulkan_intf->lock_queue) { + vulkan_intf->lock_queue(vulkan_intf->handle); + } + + try { + // Submit with fence for internal resource tracking + vk::Queue queue{vulkan_intf->queue}; + queue.submit(submit_info, fence); + + if (vulkan_intf->unlock_queue) { + vulkan_intf->unlock_queue(vulkan_intf->handle); + } + } catch (vk::DeviceLostError& err) { + if (vulkan_intf->unlock_queue) { + vulkan_intf->unlock_queue(vulkan_intf->handle); + } + UNREACHABLE_MSG("Device lost during submit: {}", err.what()); + } catch (...) { + if (vulkan_intf->unlock_queue) { + vulkan_intf->unlock_queue(vulkan_intf->handle); + } + throw; + } + + // Enqueue fence for wait thread to process + { + std::scoped_lock lock{wait_mutex}; + wait_queue.emplace(fence, signal_value); + wait_cv.notify_one(); + } +} + +void MasterSemaphoreLibRetro::WaitThread(std::stop_token token) { + const vk::Device device{instance.GetDevice()}; + + while (!token.stop_requested()) { + vk::Fence fence; + u64 signal_value; + + // Wait for work + { + std::unique_lock lock{wait_mutex}; + Common::CondvarWait(wait_cv, lock, token, [this] { return !wait_queue.empty(); }); + if (token.stop_requested()) { + return; + } + std::tie(fence, signal_value) = wait_queue.front(); + wait_queue.pop(); + } + + // Wait for fence (blocks only this background thread) + const vk::Result result = device.waitForFences(fence, true, UINT64_MAX); + if (result != vk::Result::eSuccess) { + LOG_ERROR(Render_Vulkan, "Fence wait failed: {}", vk::to_string(result)); + } + + // Reset fence and return to pool + device.resetFences(fence); + + // Update GPU tick - signals main thread's Wait() + gpu_tick.store(signal_value, std::memory_order_release); + + // Return fence to pool + { + std::scoped_lock lock{free_mutex}; + free_queue.push_back(fence); + free_cv.notify_all(); + } + } +} + +vk::Fence MasterSemaphoreLibRetro::GetFreeFence() { + std::scoped_lock lock{free_mutex}; + if (free_queue.empty()) { + // Pool exhausted - create new fence + return instance.GetDevice().createFence({}); + } + + const vk::Fence fence = free_queue.front(); + free_queue.pop_front(); + return fence; +} + +// Factory function for scheduler to create LibRetro MasterSemaphore +std::unique_ptr CreateLibRetroMasterSemaphore(const Instance& instance) { + return std::make_unique(instance); +} + +} // namespace Vulkan diff --git a/src/citra_libretro/libretro_vk.h b/src/citra_libretro/libretro_vk.h new file mode 100644 index 000000000..c51075e21 --- /dev/null +++ b/src/citra_libretro/libretro_vk.h @@ -0,0 +1,175 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include + +#include "common/common_types.h" +#include "common/dynamic_library/dynamic_library.h" +#include "video_core/renderer_vulkan/vk_common.h" +#include "video_core/renderer_vulkan/vk_instance.h" +#include "video_core/renderer_vulkan/vk_master_semaphore.h" +#include "video_core/renderer_vulkan/vk_platform.h" + +#include "libretro_vulkan.h" + +VK_DEFINE_HANDLE(VmaAllocation) + +namespace LibRetro { + +extern void VulkanResetContext(); + +/// Returns VkApplicationInfo for negotiation interface +const VkApplicationInfo* GetVulkanApplicationInfo(); + +/// CreateDevice callback for negotiation interface +bool CreateVulkanDevice(struct retro_vulkan_context* context, VkInstance instance, + VkPhysicalDevice gpu, VkSurfaceKHR surface, + PFN_vkGetInstanceProcAddr get_instance_proc_addr, + const char** required_device_extensions, + unsigned num_required_device_extensions, + const char** required_device_layers, unsigned num_required_device_layers, + const VkPhysicalDeviceFeatures* required_features); + +} // namespace LibRetro + +namespace Vulkan { + +class LibRetroVKInstance : public Instance { +public: + explicit LibRetroVKInstance(Frontend::EmuWindow& window, u32 physical_device_index); + + /// Returns the Vulkan instance + vk::Instance GetInstance() const override; + + /// Returns the Vulkan device + vk::Device GetDevice() const override; +}; + +class Scheduler; +class RenderManager; +class MasterSemaphore; + +/// LibRetro-specific MasterSemaphore implementation +class MasterSemaphoreLibRetro : public MasterSemaphore { + using Waitable = std::pair; + +public: + explicit MasterSemaphoreLibRetro(const Instance& instance); + ~MasterSemaphoreLibRetro() override; + + void Refresh() override; + void Wait(u64 tick) override; + void SubmitWork(vk::CommandBuffer cmdbuf, vk::Semaphore wait, vk::Semaphore signal, + u64 signal_value) override; + +private: + void WaitThread(std::stop_token token); + vk::Fence GetFreeFence(); + + const Instance& instance; + std::deque free_queue; + std::queue wait_queue; + std::mutex free_mutex; + std::mutex wait_mutex; + std::condition_variable free_cv; + std::condition_variable_any wait_cv; + std::jthread wait_thread; +}; + +/// Factory function for scheduler to create LibRetro MasterSemaphore +std::unique_ptr CreateLibRetroMasterSemaphore(const Instance& instance); + +struct Frame { + u32 width; + u32 height; + VmaAllocation allocation; + vk::Framebuffer framebuffer; + vk::Image image; + vk::ImageView image_view; + vk::Semaphore render_ready; + vk::Fence present_done; + vk::CommandBuffer cmdbuf; +}; + +/// LibRetro-specific PresentWindow implementation (same interface as desktop version) +class PresentWindow final { +public: + explicit PresentWindow(Frontend::EmuWindow& emu_window, const Instance& instance, + Scheduler& scheduler, bool low_refresh_rate); + ~PresentWindow(); + + /// Waits for all queued frames to finish presenting. + void WaitPresent(); + + /// Returns the last used render frame. + Frame* GetRenderFrame(); + + /// Recreates the render frame to match provided parameters. + void RecreateFrame(Frame* frame, u32 width, u32 height); + + /// Queues the provided frame for presentation. + void Present(Frame* frame); + + /// This is called to notify the rendering backend of a surface change + void NotifySurfaceChanged(); + + [[nodiscard]] vk::RenderPass Renderpass() const noexcept { + return present_renderpass; + } + + u32 ImageCount() const noexcept { + return static_cast(frame_pool.size()); + } + +private: + /// Creates the render pass for LibRetro output + vk::RenderPass CreateRenderpass(); + + /// Creates output texture for LibRetro submission + void CreateOutputTexture(u32 width, u32 height); + + /// Destroys current output texture + void DestroyOutputTexture(); + + /// Creates frame resources + void CreateFrameResources(); + + /// Destroys frame resources + void DestroyFrameResources(); + +private: + Frontend::EmuWindow& emu_window; + const Instance& instance; + [[maybe_unused]] Scheduler& scheduler; + + // LibRetro output texture (replaces swapchain) + vk::Image output_image{}; + vk::ImageView output_image_view{}; + VmaAllocation output_allocation{}; + vk::Format output_format{vk::Format::eR8G8B8A8Unorm}; + vk::ImageViewCreateInfo output_view_create_info{}; + + // Frame management + vk::RenderPass present_renderpass{}; + vk::CommandPool command_pool{}; + std::vector frame_pool; + u32 current_frame_index{0}; + + // Current output dimensions + u32 output_width{0}; + u32 output_height{0}; + + // Vulkan objects + vk::Queue graphics_queue{}; + + // Persistent LibRetro image descriptor, must persist across frames for RetroArch frame duping + // during pause + retro_vulkan_image persistent_libretro_image{}; +}; + +} // namespace Vulkan diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 6bb14fc9e..4d7981d82 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -63,8 +63,6 @@ add_library(citra_common STATIC aarch64/oaknut_abi.h aarch64/oaknut_util.h alignment.h - android_storage.h - android_storage.cpp announce_multiplayer_room.h arch.h archives.h @@ -172,7 +170,7 @@ if (UNIX AND NOT APPLE) target_link_libraries(citra_common PRIVATE gamemode) endif() -if (APPLE) +if (APPLE AND NOT ENABLE_LIBRETRO) target_sources(citra_common PUBLIC apple_authorization.h apple_authorization.cpp @@ -181,6 +179,14 @@ if (APPLE) ) endif() +# Android storage is only used for non-libretro Android builds +if (ANDROID AND NOT ENABLE_LIBRETRO) + target_sources(citra_common PRIVATE + android_storage.cpp + android_storage.h + ) +endif() + if (MSVC) target_compile_options(citra_common PRIVATE /W4 diff --git a/src/common/error.cpp b/src/common/error.cpp index 1d7467f68..def285115 100644 --- a/src/common/error.cpp +++ b/src/common/error.cpp @@ -1,4 +1,8 @@ -// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +// Copyright 2013 Dolphin Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -31,7 +35,7 @@ std::string NativeErrorToString(int e) { #else char err_str[255]; #if defined(__GLIBC__) && (_GNU_SOURCE || (_POSIX_C_SOURCE < 200112L && _XOPEN_SOURCE < 600)) || \ - defined(ANDROID) + (defined(ANDROID) && !defined(HAVE_LIBRETRO)) // Thread safe (GNU-specific) const char* str = strerror_r(e, err_str, sizeof(err_str)); return std::string(str); diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index 79c0afaca..e42fcd785 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -43,6 +43,12 @@ #define fseeko _fseeki64 #define ftello _ftelli64 #define fileno _fileno +typedef struct _stat64 file_stat_t; +#define fstat _fstat64 +#elif defined(HAVE_LIBRETRO) +typedef struct _stat64 file_stat_t; +#else +typedef struct stat file_stat_t; #endif #else @@ -56,6 +62,7 @@ #include #include #include +typedef struct stat file_stat_t; #endif #if defined(__APPLE__) @@ -75,7 +82,7 @@ #endif -#ifdef ANDROID +#if defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) #include "common/android_storage.h" #include "common/string_util.h" #endif @@ -87,6 +94,36 @@ #define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) #endif +#ifdef HAVE_LIBRETRO_VFS +#define SKIP_STDIO_REDEFINES +#include +#include + +#define FILE RFILE +#define FTELL rftell +#define FOPEN rfopen +#define FCLOSE rfclose +#define FSEEK rfseek +#define FREAD rfread +#define FWRITE rfwrite +#define FEOF rfeof +#define FERROR rferror +#define FFLUSH rfflush + +#else + +#define FTELL ftello +#define FOPEN fopen +#define FCLOSE std::fclose +#define FSEEK fseeko +#define FREAD std::fread +#define FWRITE std::fwrite +#define FEOF feof +#define FERROR ferror +#define FFLUSH std::fflush + +#endif + // This namespace has various generic functions related to files and paths. // The code still needs a ton of cleanup. // REMEMBER: strdup considered harmful! @@ -119,7 +156,7 @@ bool Exists(const std::string& filename) { copy += DIR_SEP_CHR; int result = _wstat64(Common::UTF8ToUTF16W(copy).c_str(), &file_info); -#elif ANDROID +#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) int result = AndroidStorage::FileExists(filename) ? 0 : -1; #else struct stat file_info; @@ -130,7 +167,7 @@ bool Exists(const std::string& filename) { } bool IsDirectory(const std::string& filename) { -#ifdef ANDROID +#if defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) return AndroidStorage::IsDirectory(filename); #endif @@ -178,7 +215,7 @@ bool Delete(const std::string& filename) { LOG_ERROR(Common_Filesystem, "DeleteFile failed on {}: {}", filename, GetLastErrorMsg()); return false; } -#elif ANDROID +#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) if (!AndroidStorage::DeleteDocument(filename)) { LOG_ERROR(Common_Filesystem, "unlink failed on {}", filename); return false; @@ -205,7 +242,7 @@ bool CreateDir(const std::string& path) { } LOG_ERROR(Common_Filesystem, "CreateDirectory failed on {}: {}", path, error); return false; -#elif ANDROID +#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) std::string directory = path; std::string filename = path; if (Common::EndsWith(path, "/")) { @@ -292,7 +329,7 @@ bool DeleteDir(const std::string& filename) { #ifdef _WIN32 if (::RemoveDirectoryW(Common::UTF8ToUTF16W(filename).c_str())) return true; -#elif ANDROID +#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) if (AndroidStorage::DeleteDocument(filename)) return true; #else @@ -310,7 +347,7 @@ bool Rename(const std::string& srcFullPath, const std::string& destFullPath) { if (_wrename(Common::UTF8ToUTF16W(srcFullPath).c_str(), Common::UTF8ToUTF16W(destFullPath).c_str()) == 0) return true; -#elif ANDROID +#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) // srcFullPath and destFullPath are relative to the user directory if (AndroidStorage::GetBuildFlavor() == AndroidStorage::AndroidBuildFlavors::GOOGLEPLAY) { if (AndroidStorage::MoveAndRenameFile(srcFullPath, destFullPath)) @@ -343,36 +380,36 @@ bool Copy(const std::string& srcFilename, const std::string& destFilename) { LOG_ERROR(Common_Filesystem, "failed {} --> {}: {}", srcFilename, destFilename, GetLastErrorMsg()); return false; -#elif ANDROID +#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) return AndroidStorage::CopyFile(srcFilename, std::string(GetParentPath(destFilename)), std::string(GetFilename(destFilename))); #else // Open input file - FILE* input = fopen(srcFilename.c_str(), "rb"); + FILE* input = FOPEN(srcFilename.c_str(), "rb"); if (!input) { LOG_ERROR(Common_Filesystem, "opening input failed {} --> {}: {}", srcFilename, destFilename, GetLastErrorMsg()); return false; } - SCOPE_EXIT({ fclose(input); }); + SCOPE_EXIT({ FCLOSE(input); }); // open output file - FILE* output = fopen(destFilename.c_str(), "wb"); + FILE* output = FOPEN(destFilename.c_str(), "wb"); if (!output) { LOG_ERROR(Common_Filesystem, "opening output failed {} --> {}: {}", srcFilename, destFilename, GetLastErrorMsg()); return false; } - SCOPE_EXIT({ fclose(output); }); + SCOPE_EXIT({ FCLOSE(output); }); // copy loop std::array buffer; - while (!feof(input)) { + while (!FEOF(input)) { // read input - std::size_t rnum = fread(buffer.data(), sizeof(char), buffer.size(), input); + std::size_t rnum = FREAD(buffer.data(), sizeof(char), buffer.size(), input); if (rnum != buffer.size()) { - if (ferror(input) != 0) { + if (FERROR(input) != 0) { LOG_ERROR(Common_Filesystem, "failed reading from source, {} --> {}: {}", srcFilename, destFilename, GetLastErrorMsg()); return false; @@ -380,7 +417,7 @@ bool Copy(const std::string& srcFilename, const std::string& destFilename) { } // write output - std::size_t wnum = fwrite(buffer.data(), sizeof(char), rnum, output); + std::size_t wnum = FWRITE(buffer.data(), sizeof(char), rnum, output); if (wnum != rnum) { LOG_ERROR(Common_Filesystem, "failed writing to output, {} --> {}: {}", srcFilename, destFilename, GetLastErrorMsg()); @@ -408,7 +445,7 @@ u64 GetSize(const std::string& filename) { #ifdef _WIN32 struct _stat64 buf; if (_wstat64(Common::UTF8ToUTF16W(filename).c_str(), &buf) == 0) -#elif ANDROID +#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) u64 result = AndroidStorage::GetSize(filename); LOG_TRACE(Common_Filesystem, "{}: {}", filename, result); return result; @@ -425,7 +462,7 @@ u64 GetSize(const std::string& filename) { } u64 GetSize(const int fd) { - struct stat buf; + file_stat_t buf; if (fstat(fd, &buf) != 0) { LOG_ERROR(Common_Filesystem, "GetSize: stat failed {}: {}", fd, GetLastErrorMsg()); return 0; @@ -435,13 +472,13 @@ u64 GetSize(const int fd) { u64 GetSize(FILE* f) { // can't use off_t here because it can be 32-bit - u64 pos = ftello(f); - if (fseeko(f, 0, SEEK_END) != 0) { + u64 pos = FTELL(f); + if (FSEEK(f, 0, SEEK_END) != 0) { LOG_ERROR(Common_Filesystem, "GetSize: seek failed {}: {}", fmt::ptr(f), GetLastErrorMsg()); return 0; } - u64 size = ftello(f); - if ((size != pos) && (fseeko(f, pos, SEEK_SET) != 0)) { + u64 size = FTELL(f); + if ((size != pos) && (FSEEK(f, pos, SEEK_SET) != 0)) { LOG_ERROR(Common_Filesystem, "GetSize: seek failed {}: {}", fmt::ptr(f), GetLastErrorMsg()); return 0; } @@ -481,7 +518,7 @@ bool ForeachDirectoryEntry(u64* num_entries_out, const std::string& directory, // windows loop do { const std::string virtual_name(Common::UTF16ToUTF8(ffd.cFileName)); -#elif ANDROID +#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) // android loop auto result = AndroidStorage::GetFilesName(directory); for (auto virtual_name : result) { @@ -508,7 +545,7 @@ bool ForeachDirectoryEntry(u64* num_entries_out, const std::string& directory, #ifdef _WIN32 } while (FindNextFileW(handle_find, &ffd) != 0); FindClose(handle_find); -#elif ANDROID +#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) } #else } @@ -605,7 +642,7 @@ void CopyDir([[maybe_unused]] const std::string& source_path, if (!FileUtil::Exists(dest_path)) FileUtil::CreateFullPath(dest_path); -#ifdef ANDROID +#if defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) auto result = AndroidStorage::GetFilesName(source_path); for (auto virtualName : result) { #else @@ -635,7 +672,7 @@ void CopyDir([[maybe_unused]] const std::string& source_path, FileUtil::Copy(source, dest); } -#ifndef ANDROID +#if !(defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)) closedir(dirp); #endif // ANDROID #endif // _WIN32 @@ -811,7 +848,7 @@ void SetUserPath(const std::string& path) { g_paths.emplace(UserPath::ConfigDir, user_path + CONFIG_DIR DIR_SEP); g_paths.emplace(UserPath::CacheDir, user_path + CACHE_DIR DIR_SEP); -#elif ANDROID +#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) user_path = "/"; g_paths.emplace(UserPath::ConfigDir, user_path + CONFIG_DIR DIR_SEP); g_paths.emplace(UserPath::CacheDir, user_path + CACHE_DIR DIR_SEP); @@ -1070,7 +1107,7 @@ std::string_view RemoveTrailingSlash(std::string_view path) { std::string SanitizePath(std::string_view path_, DirectorySeparator directory_separator) { std::string path(path_); -#ifdef ANDROID +#if defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) return std::string(RemoveTrailingSlash(path)); #endif char type1 = directory_separator == DirectorySeparator::BackwardSlash ? '/' : '\\'; @@ -1137,7 +1174,7 @@ bool IOFile::Open() { Common::UTF8ToUTF16W(openmode).c_str(), flags); m_good = m_file != nullptr; -#elif ANDROID +#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) // Check whether filepath is startsWith content AndroidStorage::AndroidOpenMode android_open_mode = AndroidStorage::ParseOpenmode(openmode); if (android_open_mode == AndroidStorage::AndroidOpenMode::WRITE || @@ -1168,7 +1205,7 @@ bool IOFile::Open() { m_good = m_file != nullptr; #else - m_file = std::fopen(filename.c_str(), openmode.c_str()); + m_file = FOPEN(filename.c_str(), openmode.c_str()); m_good = m_file != nullptr; #endif @@ -1176,7 +1213,7 @@ bool IOFile::Open() { } bool IOFile::Close() { - if (!IsOpen() || 0 != std::fclose(m_file)) + if (!IsOpen() || 0 != FCLOSE(m_file)) m_good = false; m_file = nullptr; @@ -1191,7 +1228,7 @@ u64 IOFile::GetSize() const { } bool IOFile::SeekImpl(s64 off, int origin) { - if (!IsOpen() || 0 != fseeko(m_file, off, origin)) + if (!IsOpen() || 0 != FSEEK(m_file, off, origin)) m_good = false; return m_good; @@ -1199,13 +1236,13 @@ bool IOFile::SeekImpl(s64 off, int origin) { u64 IOFile::TellImpl() const { if (IsOpen()) - return ftello(m_file); + return FTELL(m_file); return std::numeric_limits::max(); } bool IOFile::Flush() { - if (!IsOpen() || 0 != std::fflush(m_file)) + if (!IsOpen() || 0 != FFLUSH(m_file)) m_good = false; return m_good; @@ -1223,7 +1260,7 @@ std::size_t IOFile::ReadImpl(void* data, std::size_t length, std::size_t data_si DEBUG_ASSERT(data != nullptr); - return std::fread(data, data_size, length, m_file); + return FREAD(data, data_size, length, m_file); } #ifdef _WIN32 @@ -1266,7 +1303,16 @@ std::size_t IOFile::ReadAtImpl(void* data, std::size_t byte_count, std::size_t o DEBUG_ASSERT(data != nullptr); +#ifdef HAVE_LIBRETRO_VFS + std::scoped_lock lock(m_file_pos_mutex); + int64_t pos = filestream_tell(m_file); + FSEEK(m_file, offset, RETRO_VFS_SEEK_POSITION_START); + int64_t rv = FREAD(data, 1, byte_count, m_file); + FSEEK(m_file, pos, RETRO_VFS_SEEK_POSITION_START); + return rv; +#else return pread(fileno(m_file), data, byte_count, offset); +#endif } std::size_t IOFile::WriteImpl(const void* data, std::size_t length, std::size_t data_size) { @@ -1281,12 +1327,18 @@ std::size_t IOFile::WriteImpl(const void* data, std::size_t length, std::size_t DEBUG_ASSERT(data != nullptr); +#if defined(HAVE_LIBRETRO_VFS) + return rfwrite(data, data_size, length, m_file) / data_size; +#else return std::fwrite(data, data_size, length, m_file); +#endif } bool IOFile::Resize(u64 size) { if (!IsOpen() || 0 != -#ifdef _WIN32 +#if defined(HAVE_LIBRETRO_VFS) + filestream_truncate(m_file, size) +#elif defined(_WIN32) // ector: _chsize sucks, not 64-bit safe // F|RES: changed to _chsize_s. i think it is 64-bit safe _chsize_s(_fileno(m_file), size) diff --git a/src/common/file_util.h b/src/common/file_util.h index 98d232dcf..58cc4f7aa 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -32,6 +33,14 @@ #include "common/string_util.h" #endif +#ifdef HAVE_LIBRETRO_VFS +#define SKIP_STDIO_REDEFINES +#include +#define CORE_FILE RFILE +#else +#define CORE_FILE std::FILE +#endif + namespace FileUtil { // User paths for GetUserPath @@ -120,7 +129,7 @@ private: [[nodiscard]] u64 GetSize(int fd); // Overloaded GetSize, accepts FILE* -[[nodiscard]] u64 GetSize(FILE* f); +[[nodiscard]] u64 GetSize(CORE_FILE* f); // Returns true if successful, or path already exists. bool CreateDir(const std::string& filename); @@ -423,7 +432,11 @@ public: return m_good; } [[nodiscard]] virtual int GetFd() const { -#ifdef ANDROID +#ifdef HAVE_LIBRETRO_VFS + if (m_file == nullptr) + return -1; + return fileno(filestream_get_vfs_handle(m_file)->fp); +#elif defined(ANDROID) return m_fd; #else if (m_file == nullptr) @@ -448,7 +461,12 @@ public: // clear error state virtual void Clear() { m_good = true; + +#ifdef HAVE_LIBRETRO_VFS + filestream_rewind(m_file); +#else std::clearerr(m_file); +#endif } virtual bool IsCrypto() { @@ -476,9 +494,16 @@ protected: virtual u64 TellImpl() const; private: - std::FILE* m_file = nullptr; + CORE_FILE* m_file = nullptr; int m_fd = -1; bool m_good = true; +#ifdef HAVE_LIBRETRO_VFS + // pread() doesn't touch the file position, so it's safe alongside + // concurrent fread/fwrite. Libretro VFS has no pread equivalent, so + // ReadAtImpl emulates it with seek+read+seek, which would corrupt the + // file position for concurrent Read/Write operations. + mutable std::mutex m_file_pos_mutex; +#endif std::string filename; std::string openmode; @@ -547,4 +572,4 @@ void OpenFStream(T& fstream, const std::string& filename, std::ios_base::openmod } BOOST_CLASS_EXPORT_KEY(FileUtil::IOFile) -BOOST_CLASS_EXPORT_KEY(FileUtil::CryptoIOFile) \ No newline at end of file +BOOST_CLASS_EXPORT_KEY(FileUtil::CryptoIOFile) diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp index 36fcfeccf..e1b8621bd 100644 --- a/src/common/logging/backend.cpp +++ b/src/common/logging/backend.cpp @@ -55,6 +55,61 @@ public: virtual void Close() = 0; }; +#ifdef HAVE_LIBRETRO +/** + * LibRetro backend + */ +class LibRetroBackend : public Backend { +public: + explicit LibRetroBackend() {} + explicit LibRetroBackend(retro_log_printf_t callback) : callback(callback) {} + + ~LibRetroBackend() override = default; + + void Write(const Entry& entry) override { + if (callback == nullptr) { + return; + } + retro_log_level log_level; + + switch (entry.log_level) { + case Common::Log::Level::Trace: + log_level = retro_log_level::RETRO_LOG_DEBUG; + break; + case Common::Log::Level::Debug: + log_level = retro_log_level::RETRO_LOG_DEBUG; + break; + case Common::Log::Level::Info: + log_level = retro_log_level::RETRO_LOG_INFO; + break; + case Common::Log::Level::Warning: + log_level = retro_log_level::RETRO_LOG_WARN; + break; + case Common::Log::Level::Error: + log_level = retro_log_level::RETRO_LOG_ERROR; + break; + case Common::Log::Level::Critical: + log_level = retro_log_level::RETRO_LOG_ERROR; + break; + default: + log_level = retro_log_level::RETRO_LOG_DUMMY; + } + + auto str = FormatLogMessage(entry).append(1, '\n'); + callback(log_level, str.c_str()); + } + + void Flush() override {} + + void Close() override {} + + void EnableForStacktrace() override {} + +private: + retro_log_printf_t callback = nullptr; +}; +#endif + /** * Backend that writes to stderr and with color */ @@ -218,7 +273,19 @@ public: } return *instance; } - +#ifdef HAVE_LIBRETRO + static void Initialize(retro_log_printf_t callback) { + if (instance) { + LOG_WARNING(Log, "Reinitializing logging backend"); + return; + } + initialization_in_progress_suppress_logging = true; + Filter filter; + filter.ParseFilterString(Settings::values.log_filter.GetValue()); + instance = std::unique_ptr(new Impl(callback, filter), Deleter); + initialization_in_progress_suppress_logging = false; + } +#endif static void Initialize(std::string_view log_file) { if (instance) { LOG_WARNING(Log, "Reinitializing logging backend"); @@ -310,6 +377,10 @@ public: } private: +#ifdef HAVE_LIBRETRO + Impl(retro_log_printf_t callback, const Filter& filter_) + : filter{filter_}, file_backend{""}, libretro_backend{callback} {} +#endif Impl(const std::string& file_backend_filename, const Filter& filter_) : filter{filter_}, file_backend{file_backend_filename} { #ifdef CITRA_LINUX_GCC_BACKTRACE @@ -412,12 +483,16 @@ private: } void ForEachBackend(auto lambda) { +#ifdef HAVE_LIBRETRO + lambda(static_cast(libretro_backend)); +#else lambda(static_cast(debugger_backend)); lambda(static_cast(color_console_backend)); lambda(static_cast(file_backend)); #ifdef ANDROID lambda(static_cast(lc_backend)); -#endif +#endif // ANDROID +#endif // HAVE_LIBRETRO } static void Deleter(Impl* ptr) { @@ -464,6 +539,9 @@ private: #ifdef ANDROID LogcatBackend lc_backend{}; #endif +#ifdef HAVE_LIBRETRO + LibRetroBackend libretro_backend; +#endif MPSCQueue message_queue{}; std::chrono::steady_clock::time_point time_origin{std::chrono::steady_clock::now()}; @@ -478,6 +556,13 @@ private: }; } // namespace +#ifdef HAVE_LIBRETRO +void LibRetroStart(retro_log_printf_t callback) { + Impl::Initialize(callback); + Impl::Start(); +} +#endif + void Initialize(std::string_view log_file) { Impl::Initialize(log_file.empty() ? LOG_FILE : log_file); } diff --git a/src/common/logging/backend.h b/src/common/logging/backend.h index e16a129b1..c87bff2dc 100644 --- a/src/common/logging/backend.h +++ b/src/common/logging/backend.h @@ -1,4 +1,4 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -6,6 +6,9 @@ #include #include "common/logging/filter.h" +#ifdef HAVE_LIBRETRO +#include "libretro.h" +#endif namespace Common::Log { @@ -13,6 +16,9 @@ class Filter; /// Initializes the logging system. This should be the first thing called in main. void Initialize(std::string_view log_file = ""); +#ifdef HAVE_LIBRETRO +void LibRetroStart(retro_log_printf_t callback); +#endif void Start(); diff --git a/src/common/zstd_compression.cpp b/src/common/zstd_compression.cpp index 3257fa2d8..20840e3ac 100644 --- a/src/common/zstd_compression.cpp +++ b/src/common/zstd_compression.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include diff --git a/src/core/core.h b/src/core/core.h index 085a7b3b0..d493e8491 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -362,6 +362,10 @@ public: void LoadState(u32 slot); + std::vector SaveStateBuffer() const; + + bool LoadStateBuffer(std::vector buffer); + /// Self delete ncch bool SetSelfDelete(const std::string& file) { if (m_filepath == file) { diff --git a/src/core/frontend/emu_window.h b/src/core/frontend/emu_window.h index d5b7803cc..1f0fec7b7 100644 --- a/src/core/frontend/emu_window.h +++ b/src/core/frontend/emu_window.h @@ -27,6 +27,7 @@ enum class WindowSystemType : u8 { MacOS, X11, Wayland, + LibRetro, }; struct Frame; @@ -258,6 +259,16 @@ public: return is_secondary; } + /** + * Requests for a frontend to setup a framebuffer. + */ + virtual void SetupFramebuffer() {} + + /// Flags that the framebuffer should be cleared. + virtual bool NeedsClearing() const { + return true; + } + Settings::StereoRenderOption get3DMode() const; protected: diff --git a/src/core/hle/service/cfg/cfg.cpp b/src/core/hle/service/cfg/cfg.cpp index 070612250..8a34237c6 100644 --- a/src/core/hle/service/cfg/cfg.cpp +++ b/src/core/hle/service/cfg/cfg.cpp @@ -32,6 +32,9 @@ #include "core/hle/service/cfg/cfg_u.h" #include "core/hw/unique_data.h" #include "core/loader/loader.h" +#ifdef HAVE_LIBRETRO +#include "citra_libretro/core_settings.h" +#endif SERVICE_CONSTRUCT_IMPL(Service::CFG::Module) SERIALIZE_EXPORT_IMPL(Service::CFG::Module) @@ -1058,6 +1061,11 @@ void Module::UpdatePreferredRegionCode() { if (preferred_region_chosen || !system.IsPoweredOn()) { return; } +#ifdef HAVE_LIBRETRO + // Apply language set in core options first + SetSystemLanguage(LibRetro::settings.language_value); +#endif + preferred_region_chosen = true; const auto preferred_regions = system.GetAppLoader().GetPreferredRegions(); diff --git a/src/core/hle/service/mic/mic_u.cpp b/src/core/hle/service/mic/mic_u.cpp index d15c077bd..f6fee7784 100644 --- a/src/core/hle/service/mic/mic_u.cpp +++ b/src/core/hle/service/mic/mic_u.cpp @@ -214,7 +214,6 @@ struct MIC_U::Impl { LOG_CRITICAL(Service_MIC, "Application started sampling again before stopping sampling"); mic->StopSampling(); - mic.reset(); } u8 sample_size = encoding == Encoding::PCM8Signed || encoding == Encoding::PCM8 ? 8 : 16; @@ -225,7 +224,9 @@ struct MIC_U::Impl { state.looped_buffer = audio_buffer_loop; state.size = audio_buffer_size; - CreateMic(); + if (!mic) { + CreateMic(); + } StartSampling(); timing.ScheduleEvent(GetBufferUpdatePeriod(state.sample_rate), buffer_write_event); @@ -259,7 +260,6 @@ struct MIC_U::Impl { timing.RemoveEvent(buffer_write_event); if (mic) { mic->StopSampling(); - mic.reset(); } LOG_TRACE(Service_MIC, "called"); } diff --git a/src/core/hle/service/soc/soc_u.cpp b/src/core/hle/service/soc/soc_u.cpp index 23bfb5aec..d4ae261bf 100644 --- a/src/core/hle/service/soc/soc_u.cpp +++ b/src/core/hle/service/soc/soc_u.cpp @@ -2308,7 +2308,9 @@ std::optional SOC_U::GetDefaultInterfaceInfo() { break; } } -#else +#elif !(defined(ANDROID) && defined(HAVE_LIBRETRO)) + // Libretro Android builds target API 21, but getifaddrs() requires API 24+. + // Standalone Android (minSdk 29) and other platforms have getifaddrs(). struct ifaddrs* ifaddr; struct ifaddrs* ifa; if (getifaddrs(&ifaddr) == -1) { diff --git a/src/core/savestate.cpp b/src/core/savestate.cpp index 3390af273..573862d81 100644 --- a/src/core/savestate.cpp +++ b/src/core/savestate.cpp @@ -1,4 +1,4 @@ -// Copyright 2020 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -217,4 +217,77 @@ void System::LoadState(u32 slot) { ia&* this; } +std::vector System::SaveStateBuffer() const { + std::ostringstream sstream{std::ios_base::binary}; + // Serialize + oarchive oa{sstream}; + oa&* this; + + const std::string& str{sstream.str()}; + const auto data = std::span{reinterpret_cast(str.data()), str.size()}; + auto buffer = Common::Compression::CompressDataZSTDDefault(data); + + CSTHeader header{}; + header.filetype = header_magic_bytes; + header.program_id = title_id; + std::string rev_bytes; + CryptoPP::StringSource ss(Common::g_scm_rev, true, + new CryptoPP::HexDecoder(new CryptoPP::StringSink(rev_bytes))); + std::memcpy(header.revision.data(), rev_bytes.data(), sizeof(header.revision)); + header.time = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + const std::string build_fullname = Common::g_build_fullname; + std::memset(header.build_name.data(), 0, sizeof(header.build_name)); + std::memcpy(header.build_name.data(), build_fullname.c_str(), + std::min(build_fullname.length(), sizeof(header.build_name) - 1)); + + std::vector result((u8*)&header, (u8*)&header + sizeof(header)); + std::copy(buffer.begin(), buffer.end(), std::back_inserter(result)); + + return result; +} + +bool System::LoadStateBuffer(std::vector buffer) { + CSTHeader header; + + if (buffer.size() < sizeof(header)) { + LOG_ERROR(Core, "Save state too small"); + return false; + } + + header = *((CSTHeader*)buffer.data()); + + if (header.filetype != header_magic_bytes) { + LOG_ERROR(Core, "Invalid save state"); + return false; + } + + if (header.program_id != title_id) { + LOG_ERROR(Core, "Save state isn't for the current game"); + return false; + } + std::string revision = fmt::format("{:02x}", fmt::join(header.revision, "")); + if (revision != Common::g_scm_rev) { + LOG_ERROR(Core, + "Save state file created from a different revision (core: {}, savestate: {})", + Common::g_scm_rev, revision); + return false; + } + + std::vector state(buffer.begin() + sizeof(CSTHeader), buffer.end()); + auto decompressed = Common::Compression::DecompressDataZSTD(state); + + std::istringstream sstream{ + std::string{reinterpret_cast(decompressed.data()), decompressed.size()}, + std::ios_base::binary}; + decompressed.clear(); + + // Deserialize + iarchive ia{sstream}; + ia&* this; + + return true; +} + } // namespace Core diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 4ac9368b1..f248ece4b 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -26,6 +26,10 @@ create_target_directory_groups(tests) target_link_libraries(tests PRIVATE citra_common citra_core video_core audio_core) target_link_libraries(tests PRIVATE ${PLATFORM_LIBRARIES} catch2 nihstro-headers Threads::Threads) +if (ENABLE_LIBRETRO) + target_link_libraries(tests PRIVATE $) +endif() + add_test(NAME tests COMMAND tests) if (CITRA_USE_PRECOMPILED_HEADERS) diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 3da82159e..a8ade344d 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -183,10 +183,10 @@ if (ENABLE_VULKAN) renderer_vulkan/vk_instance.h renderer_vulkan/vk_pipeline_cache.cpp renderer_vulkan/vk_pipeline_cache.h - renderer_vulkan/vk_platform.cpp + $<$>:renderer_vulkan/vk_platform.cpp> renderer_vulkan/vk_platform.h - renderer_vulkan/vk_present_window.cpp - renderer_vulkan/vk_present_window.h + $<$>:renderer_vulkan/vk_present_window.cpp> + $<$>:renderer_vulkan/vk_present_window.h> renderer_vulkan/vk_render_manager.cpp renderer_vulkan/vk_render_manager.h renderer_vulkan/vk_shader_disk_cache.cpp @@ -195,8 +195,8 @@ if (ENABLE_VULKAN) renderer_vulkan/vk_shader_util.h renderer_vulkan/vk_stream_buffer.cpp renderer_vulkan/vk_stream_buffer.h - renderer_vulkan/vk_swapchain.cpp - renderer_vulkan/vk_swapchain.h + $<$>:renderer_vulkan/vk_swapchain.cpp> + $<$>:renderer_vulkan/vk_swapchain.h> renderer_vulkan/vk_texture_runtime.cpp renderer_vulkan/vk_texture_runtime.h shader/generator/spv_fs_shader_gen.cpp diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp index 37272d4ad..9a76f4859 100644 --- a/src/video_core/gpu.cpp +++ b/src/video_core/gpu.cpp @@ -9,6 +9,7 @@ #include "core/core_timing.h" #include "core/hle/service/gsp/gsp_gpu.h" #include "core/hle/service/plgldr/plgldr.h" +#include "core/loader/loader.h" #include "video_core/debug_utils/debug_utils.h" #include "video_core/gpu.h" #include "video_core/gpu_debugger.h" @@ -421,6 +422,53 @@ void GPU::VBlankCallback(std::uintptr_t user_data, s64 cycles_late) { impl->timing.ScheduleEvent(FRAME_TICKS - cycles_late, impl->vblank_event); } +void GPU::RecreateRenderer(Frontend::EmuWindow& emu_window, Frontend::EmuWindow* secondary_window) { + // Reset the renderer (this will destroy OpenGL resources) + impl->renderer.reset(); + + // Create a new renderer + impl->renderer = + VideoCore::CreateRenderer(emu_window, secondary_window, impl->pica, impl->system); + impl->rasterizer = impl->renderer->Rasterizer(); + + // Rebind the rasterizer to the PICA GPU + impl->pica.BindRasterizer(impl->rasterizer); + + // Update the sw_blitter with the new rasterizer + impl->sw_blitter = std::make_unique(impl->memory, impl->rasterizer); + + // Re-apply per-game configuration and reload disk shader cache + u64 program_id{}; + impl->system.GetAppLoader().ReadProgramId(program_id); + ApplyPerProgramSettings(program_id); + if (Settings::values.use_disk_shader_cache) { + impl->renderer->Rasterizer()->LoadDefaultDiskResources(false, nullptr); + } + + // Mark ALL GPU registers as dirty so current state gets uploaded to new renderer + impl->pica.dirty_regs.SetAllDirty(); + + // Also mark shader setups as dirty so uniforms get re-uploaded and + // stale pointers to the old rasterizer's JIT cache are cleared. + impl->pica.vs_setup.uniforms_dirty = true; + impl->pica.vs_setup.cached_shader = nullptr; + impl->pica.gs_setup.uniforms_dirty = true; + impl->pica.gs_setup.cached_shader = nullptr; + + // Mark all cached LUT/table state in pica as dirty + impl->pica.lighting.lut_dirty = impl->pica.lighting.LutAllDirty; + impl->pica.fog.lut_dirty = true; + impl->pica.proctex.table_dirty = impl->pica.proctex.TableAllDirty; +} + +void GPU::ReleaseRenderer() { + // Just reset the renderer to release OpenGL resources + // Don't null out rasterizer pointer as it will become dangling + impl->renderer.reset(); + impl->sw_blitter.reset(); + LOG_INFO(HW_GPU, "Renderer released for context destroy"); +} + template void GPU::serialize(Archive& ar, const u32 file_version) { ar & impl->pica; diff --git a/src/video_core/gpu.h b/src/video_core/gpu.h index 1b326a067..4e32b6d2e 100644 --- a/src/video_core/gpu.h +++ b/src/video_core/gpu.h @@ -96,6 +96,12 @@ public: void ApplyPerProgramSettings(u64 program_ID); + /// Recreates the renderer (for GL context reset in libretro) + void RecreateRenderer(Frontend::EmuWindow& emu_window, Frontend::EmuWindow* secondary_window); + + /// Releases the renderer (for GL context destroy in libretro) + void ReleaseRenderer(); + private: void SubmitCmdList(u32 index); diff --git a/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp b/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp index 1f9a16793..6969faa88 100644 --- a/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp +++ b/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp @@ -457,6 +457,14 @@ FileUtil::IOFile ShaderDiskCache::AppendTransferableFile() { const auto transferable_path{GetTransferablePath()}; const bool existed = FileUtil::Exists(transferable_path); +#ifdef HAVE_LIBRETRO_VFS + // LibRetro's VFS maps "ab+" to RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING, which + // uses "r+b" internally and fails if the file doesn't exist. Pre-create it. + if (!existed) { + FileUtil::CreateEmptyFile(transferable_path); + } +#endif + FileUtil::IOFile file(transferable_path, "ab+"); if (!file.IsOpen()) { LOG_ERROR(Render_OpenGL, "Failed to open transferable cache in path={}", transferable_path); @@ -480,6 +488,14 @@ FileUtil::IOFile ShaderDiskCache::AppendPrecompiledFile(bool write_header) { const auto precompiled_path{GetPrecompiledPath()}; const bool existed = FileUtil::Exists(precompiled_path); +#ifdef HAVE_LIBRETRO_VFS + // LibRetro's VFS maps "ab+" to RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING, which + // uses "r+b" internally and fails if the file doesn't exist. Pre-create it. + if (!existed) { + FileUtil::CreateEmptyFile(precompiled_path); + } +#endif + FileUtil::IOFile file(precompiled_path, "ab+"); if (!file.IsOpen()) { LOG_ERROR(Render_OpenGL, "Failed to open precompiled cache in path={}", precompiled_path); @@ -514,6 +530,13 @@ void ShaderDiskCache::SaveVirtualPrecompiledFile() { const auto precompiled_path{GetPrecompiledPath()}; +#ifdef HAVE_LIBRETRO_VFS + const bool existed = FileUtil::Exists(precompiled_path); + if (!existed) { + FileUtil::CreateEmptyFile(precompiled_path); + } +#endif + precompiled_file.Close(); if (!FileUtil::Delete(GetPrecompiledPath())) { LOG_ERROR(Render_OpenGL, "Failed to invalidate precompiled file={}", GetPrecompiledPath()); diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp index 967fd21fa..c8cb6c000 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.cpp +++ b/src/video_core/renderer_opengl/renderer_opengl.cpp @@ -94,9 +94,14 @@ void RendererOpenGL::SwapBuffers() { OpenGLState prev_state = OpenGLState::GetCurState(); state.Apply(); + render_window.SetupFramebuffer(); + PrepareRendertarget(); RenderScreenshot(); - +#ifdef HAVE_LIBRETRO + DrawScreens(render_window.GetFramebufferLayout(), false); + render_window.SwapBuffers(); +#else const auto& main_layout = render_window.GetFramebufferLayout(); RenderToMailbox(main_layout, render_window.mailbox, false); @@ -124,6 +129,7 @@ void RendererOpenGL::SwapBuffers() { LOG_DEBUG(Render_OpenGL, "Frame dumper exception caught: {}", exception.what()); } } +#endif system.perf_stats->EndSwap(); EndFrame(); @@ -663,7 +669,9 @@ void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout, bool f const auto& bottom_screen = layout.bottom_screen; glViewport(0, 0, layout.width, layout.height); - glClear(GL_COLOR_BUFFER_BIT); + if (render_window.NeedsClearing()) { + glClear(GL_COLOR_BUFFER_BIT); + } // Set projection matrix std::array ortho_matrix = diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.cpp b/src/video_core/renderer_vulkan/renderer_vulkan.cpp index 59a5a1c94..0b04b0ba7 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.cpp +++ b/src/video_core/renderer_vulkan/renderer_vulkan.cpp @@ -22,7 +22,7 @@ #include -#ifdef __APPLE__ +#if defined(__APPLE__) && !defined(HAVE_LIBRETRO) #include "common/apple_utils.h" #endif @@ -60,11 +60,11 @@ constexpr static std::array PRESENT_BINDINGS namespace { static bool IsLowRefreshRate() { +#if (defined(__APPLE__) || defined(ENABLE_SDL2)) && !defined(HAVE_LIBRETRO) if (!Settings::values.use_display_refresh_rate_detection) { LOG_INFO(Render_Vulkan, "Refresh rate detection is currently disabled via settings"); return false; } -#if defined(__APPLE__) || defined(ENABLE_SDL2) #ifdef __APPLE__ // Apple's low power mode sometimes limits applications to 30fps without changing the refresh // rate, meaning the above code doesn't catch it. @@ -98,7 +98,7 @@ static bool IsLowRefreshRate() { LOG_INFO(Render_Vulkan, "Refresh rate is above emulated 3DS screen: {}hz. Good.", cur_refresh_rate); } -#endif // defined(__APPLE__) || defined(ENABLE_SDL2) +#endif // (defined(__APPLE__) || defined(ENABLE_SDL2)) && !defined(HAVE_LIBRETRO) // We have no available method of checking refresh rate. Just assume that everything is fine :) return false; diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.h b/src/video_core/renderer_vulkan/renderer_vulkan.h index 29ed8a66e..b275f3189 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.h +++ b/src/video_core/renderer_vulkan/renderer_vulkan.h @@ -7,8 +7,12 @@ #include "common/common_types.h" #include "common/math_util.h" #include "video_core/renderer_base.h" +#ifdef HAVE_LIBRETRO +#include "citra_libretro/libretro_vk.h" +#else #include "video_core/renderer_vulkan/vk_instance.h" #include "video_core/renderer_vulkan/vk_present_window.h" +#endif #include "video_core/renderer_vulkan/vk_rasterizer.h" #include "video_core/renderer_vulkan/vk_render_manager.h" #include "video_core/renderer_vulkan/vk_scheduler.h" @@ -116,7 +120,11 @@ private: Memory::MemorySystem& memory; Pica::PicaCore& pica; +#ifdef HAVE_LIBRETRO + LibRetroVKInstance instance; +#else Instance instance; +#endif Scheduler scheduler; RenderManager renderpass_cache; PresentWindow main_present_window; diff --git a/src/video_core/renderer_vulkan/vk_instance.cpp b/src/video_core/renderer_vulkan/vk_instance.cpp index d41cdfb9e..3f968e4c9 100644 --- a/src/video_core/renderer_vulkan/vk_instance.cpp +++ b/src/video_core/renderer_vulkan/vk_instance.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -464,7 +464,9 @@ bool Instance::CreateDevice() { const bool has_custom_border_color = add_extension(VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME, is_qualcomm, "it is broken on most Qualcomm driver versions"); - const bool has_index_type_uint8 = add_extension(VK_EXT_INDEX_TYPE_UINT8_EXTENSION_NAME); + const bool has_index_type_uint8 = + add_extension(VK_EXT_INDEX_TYPE_UINT8_EXTENSION_NAME, is_moltenvk, + "uint8 index conversion causes memory leaks in MoltenVK"); const bool has_fragment_shader_interlock = add_extension(VK_EXT_FRAGMENT_SHADER_INTERLOCK_EXTENSION_NAME, is_nvidia, "it is broken on Nvidia drivers"); @@ -481,6 +483,9 @@ bool Instance::CreateDevice() { return false; } +#ifndef HAVE_LIBRETRO + // Find graphics queue family. LibRetro builds skip this since queue_family_index + // is already set by LibRetroVKInstance from the frontend-provided context. bool graphics_queue_found = false; for (std::size_t i = 0; i < family_properties.size(); i++) { const u32 index = static_cast(i); @@ -494,6 +499,7 @@ bool Instance::CreateDevice() { LOG_CRITICAL(Render_Vulkan, "Unable to find graphics and/or present queues."); return false; } +#endif static constexpr std::array queue_priorities = {1.0f}; @@ -612,6 +618,10 @@ bool Instance::CreateDevice() { #undef PROP_GET #undef FEAT_SET +#ifdef HAVE_LIBRETRO + // LibRetro builds: device already created by frontend, just return after feature detection + return true; +#else try { device = physical_device.createDeviceUnique(device_chain.get()); } catch (vk::ExtensionNotPresentError& err) { @@ -626,6 +636,7 @@ bool Instance::CreateDevice() { CreateAllocator(); return true; +#endif } void Instance::CreateAllocator() { @@ -636,9 +647,9 @@ void Instance::CreateAllocator() { const VmaAllocatorCreateInfo allocator_info = { .physicalDevice = physical_device, - .device = *device, + .device = GetDevice(), .pVulkanFunctions = &functions, - .instance = *instance, + .instance = GetInstance(), .vulkanApiVersion = TargetVulkanApiVersion, }; diff --git a/src/video_core/renderer_vulkan/vk_instance.h b/src/video_core/renderer_vulkan/vk_instance.h index 09984b53f..f6fbc28fb 100644 --- a/src/video_core/renderer_vulkan/vk_instance.h +++ b/src/video_core/renderer_vulkan/vk_instance.h @@ -38,9 +38,11 @@ struct FormatTraits { class Instance { public: + struct NoInit {}; explicit Instance(bool validation = false, bool dump_command_buffers = false); explicit Instance(Frontend::EmuWindow& window, u32 physical_device_index); - ~Instance(); + explicit Instance(NoInit) {} // For LibRetro inheritance - does minimal setup + virtual ~Instance(); /// Returns the FormatTraits struct for the provided pixel format const FormatTraits& GetTraits(VideoCore::PixelFormat pixel_format) const; @@ -58,7 +60,7 @@ public: std::string GetDriverVersionName(); /// Returns the Vulkan instance - vk::Instance GetInstance() const { + virtual vk::Instance GetInstance() const { return *instance; } @@ -68,7 +70,7 @@ public: } /// Returns the Vulkan device - vk::Device GetDevice() const { + virtual vk::Device GetDevice() const { return *device; } @@ -254,6 +256,11 @@ public: return features.shaderSampledImageArrayDynamicIndexing; } + /// Returns true if layered rendering (array attachments) is supported + bool IsLayeredRenderingSupported() const { + return layered_rendering_supported; + } + /// Returns the minimum vertex stride alignment u32 GetMinVertexStrideAlignment() const { return min_vertex_stride_alignment; @@ -270,7 +277,7 @@ public: driver_id == vk::DriverIdKHR::eQualcommProprietary; } -private: +protected: /// Returns the optimal supported usage for the requested format [[nodiscard]] FormatTraits DetermineTraits(VideoCore::PixelFormat pixel_format, vk::Format format); @@ -294,7 +301,7 @@ private: // Collects logging gpu info void CollectToolingInfo(); -private: +protected: std::shared_ptr library; vk::UniqueInstance instance; vk::PhysicalDevice physical_device; @@ -328,10 +335,11 @@ private: bool shader_stencil_export{}; bool external_memory_host{}; u64 min_imported_host_pointer_alignment{}; + bool layered_rendering_supported{true}; bool tooling_info{}; bool debug_utils_supported{}; bool has_nsight_graphics{}; bool has_renderdoc{}; }; -} // namespace Vulkan \ No newline at end of file +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_resource_pool.cpp b/src/video_core/renderer_vulkan/vk_resource_pool.cpp index 0021167e4..03b644ea2 100644 --- a/src/video_core/renderer_vulkan/vk_resource_pool.cpp +++ b/src/video_core/renderer_vulkan/vk_resource_pool.cpp @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + // Copyright 2020 yuzu Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -159,7 +163,10 @@ void DescriptorHeap::Allocate(std::size_t begin, std::size_t end) { if (result == vk::Result::eSuccess) { break; } - if (result == vk::Result::eErrorOutOfPoolMemory) { + // eErrorFragmentedPool: pool has space but is too fragmented to allocate. + // MoltenVK on iOS/tvOS returns this more frequently than native Vulkan drivers. + if (result == vk::Result::eErrorOutOfPoolMemory || + result == vk::Result::eErrorFragmentedPool) { current_pool++; if (current_pool == pools.size()) { LOG_INFO(Render_Vulkan, "Run out of pools, creating new one!"); diff --git a/src/video_core/renderer_vulkan/vk_scheduler.cpp b/src/video_core/renderer_vulkan/vk_scheduler.cpp index 0099b0ca3..5d50b6c97 100644 --- a/src/video_core/renderer_vulkan/vk_scheduler.cpp +++ b/src/video_core/renderer_vulkan/vk_scheduler.cpp @@ -1,4 +1,4 @@ -// Copyright 2019 yuzu Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -8,6 +8,9 @@ #include "common/thread.h" #include "video_core/renderer_vulkan/vk_instance.h" #include "video_core/renderer_vulkan/vk_scheduler.h" +#ifdef HAVE_LIBRETRO +#include "citra_libretro/libretro_vk.h" +#endif MICROPROFILE_DEFINE(Vulkan_WaitForWorker, "Vulkan", "Wait for worker", MP_RGB(255, 192, 192)); MICROPROFILE_DEFINE(Vulkan_Submit, "Vulkan", "Submit Exectution", MP_RGB(255, 192, 255)); @@ -17,11 +20,15 @@ namespace Vulkan { namespace { std::unique_ptr MakeMasterSemaphore(const Instance& instance) { +#ifdef HAVE_LIBRETRO + return CreateLibRetroMasterSemaphore(instance); +#else if (instance.IsTimelineSemaphoreSupported()) { return std::make_unique(instance); } else { return std::make_unique(instance); } +#endif } } // Anonymous namespace diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index fdaa4ef2e..cfcb199a1 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -146,7 +146,10 @@ Handle MakeHandle(const Instance* instance, u32 width, u32 height, u32 levels, T vk::Format format, vk::ImageUsageFlags usage, vk::ImageCreateFlags flags, vk::ImageAspectFlags aspect, bool need_format_list, std::string_view debug_name = {}) { - const u32 layers = type == TextureType::CubeMap ? 6 : 1; + // On tvOS/iOS, fall back to 2D textures when layered rendering isn't supported + const bool is_cube_map = + type == TextureType::CubeMap && instance->IsLayeredRenderingSupported(); + const u32 layers = is_cube_map ? 6 : 1; const std::array format_list = { vk::Format::eR8G8B8A8Unorm, @@ -192,8 +195,7 @@ Handle MakeHandle(const Instance* instance, u32 width, u32 height, u32 levels, T const vk::Image image{unsafe_image}; const vk::ImageViewCreateInfo view_info = { .image = image, - .viewType = - type == TextureType::CubeMap ? vk::ImageViewType::eCube : vk::ImageViewType::e2D, + .viewType = is_cube_map ? vk::ImageViewType::eCube : vk::ImageViewType::e2D, .format = format, .subresourceRange{ .aspectMask = aspect, From 7bcbf8aba4c4d0c82c0f3bc9265cf305a0a39ecf Mon Sep 17 00:00:00 2001 From: Alexandre Bouvier Date: Fri, 20 Feb 2026 05:32:46 +0100 Subject: [PATCH 02/42] cmake: fix import name --- src/citra_libretro/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/citra_libretro/CMakeLists.txt b/src/citra_libretro/CMakeLists.txt index 8ea9b7b10..d90709f36 100644 --- a/src/citra_libretro/CMakeLists.txt +++ b/src/citra_libretro/CMakeLists.txt @@ -16,7 +16,7 @@ add_library(azahar_libretro_common OBJECT core_settings.h) target_compile_definitions(azahar_libretro_common PRIVATE HAVE_LIBRETRO) -target_link_libraries(azahar_libretro_common PRIVATE citra_common citra_core video_core libretro robin_map) +target_link_libraries(azahar_libretro_common PRIVATE citra_common citra_core video_core libretro tsl::robin_map) if(ENABLE_OPENGL) target_link_libraries(azahar_libretro_common PRIVATE glad) endif() @@ -43,7 +43,7 @@ target_compile_definitions(audio_core PRIVATE HAVE_LIBRETRO) target_compile_definitions(input_common PRIVATE HAVE_LIBRETRO) target_link_libraries(azahar_libretro PRIVATE citra_common citra_core) -target_link_libraries(azahar_libretro PRIVATE boost dds-ktx libretro robin_map) +target_link_libraries(azahar_libretro PRIVATE Boost::boost dds-ktx libretro tsl::robin_map) if(ENABLE_VULKAN) target_link_libraries(azahar_libretro PRIVATE sirit vulkan-headers vma) endif() From f3fb0b729e7ca90e9697ca659643e5ef940ce41d Mon Sep 17 00:00:00 2001 From: OpenSauce04 Date: Thu, 19 Feb 2026 22:48:49 +0000 Subject: [PATCH 03/42] Kill SDL2 frontend Good riddance --- CMakeLists.txt | 6 - src/CMakeLists.txt | 6 +- src/citra_meta/CMakeLists.txt | 4 - src/citra_meta/common_strings.h | 7 - src/citra_meta/main.cpp | 28 +- src/citra_sdl/CMakeLists.txt | 48 -- src/citra_sdl/citra_sdl.cpp | 530 ------------------ src/citra_sdl/citra_sdl.h | 7 - src/citra_sdl/config.cpp | 395 ------------- src/citra_sdl/config.h | 35 -- src/citra_sdl/default_ini.h | 405 ------------- src/citra_sdl/emu_window/emu_window_sdl2.cpp | 252 --------- src/citra_sdl/emu_window/emu_window_sdl2.h | 91 --- .../emu_window/emu_window_sdl2_gl.cpp | 167 ------ src/citra_sdl/emu_window/emu_window_sdl2_gl.h | 39 -- .../emu_window/emu_window_sdl2_sw.cpp | 108 ---- src/citra_sdl/emu_window/emu_window_sdl2_sw.h | 43 -- .../emu_window/emu_window_sdl2_vk.cpp | 90 --- src/citra_sdl/emu_window/emu_window_sdl2_vk.h | 24 - src/citra_sdl/precompiled_headers.h | 7 - src/citra_sdl/resource.h | 16 - 21 files changed, 6 insertions(+), 2302 deletions(-) delete mode 100644 src/citra_sdl/CMakeLists.txt delete mode 100644 src/citra_sdl/citra_sdl.cpp delete mode 100644 src/citra_sdl/citra_sdl.h delete mode 100644 src/citra_sdl/config.cpp delete mode 100644 src/citra_sdl/config.h delete mode 100644 src/citra_sdl/default_ini.h delete mode 100644 src/citra_sdl/emu_window/emu_window_sdl2.cpp delete mode 100644 src/citra_sdl/emu_window/emu_window_sdl2.h delete mode 100644 src/citra_sdl/emu_window/emu_window_sdl2_gl.cpp delete mode 100644 src/citra_sdl/emu_window/emu_window_sdl2_gl.h delete mode 100644 src/citra_sdl/emu_window/emu_window_sdl2_sw.cpp delete mode 100644 src/citra_sdl/emu_window/emu_window_sdl2_sw.h delete mode 100644 src/citra_sdl/emu_window/emu_window_sdl2_vk.cpp delete mode 100644 src/citra_sdl/emu_window/emu_window_sdl2_vk.h delete mode 100644 src/citra_sdl/precompiled_headers.h delete mode 100644 src/citra_sdl/resource.h diff --git a/CMakeLists.txt b/CMakeLists.txt index df86bf28c..54eb90e90 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,7 +105,6 @@ foreach(_opt IN LISTS _LIBRETRO_INCOMPATIBLE_OPTIONS) endforeach() option(ENABLE_SDL2 "Enable using SDL2" ON) -CMAKE_DEPENDENT_OPTION(ENABLE_SDL2_FRONTEND "Enable the SDL2 frontend" OFF "ENABLE_SDL2;NOT ANDROID AND NOT IOS" OFF) option(USE_SYSTEM_SDL2 "Use the system SDL2 lib (instead of the bundled one)" OFF) # Set bundled qt as dependent options. @@ -182,9 +181,6 @@ endif() if (ENABLE_SDL2) add_definitions(-DENABLE_SDL2) endif() -if (ENABLE_SDL2_FRONTEND) - add_definitions(-DENABLE_SDL2_FRONTEND) -endif() if(ENABLE_SSE42 AND (CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")) message(STATUS "SSE4.2 enabled for x86_64") @@ -555,8 +551,6 @@ if (NOT ANDROID AND NOT IOS) include(BundleTarget) if (ENABLE_QT) qt_bundle_target(citra_meta) - elseif (ENABLE_SDL2_FRONTEND) - bundle_target(citra_meta) endif() if (ENABLE_ROOM_STANDALONE) bundle_target(citra_room_standalone) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b18925867..bd1810d53 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -193,15 +193,11 @@ if (ENABLE_TESTS) add_subdirectory(tests) endif() -if (ENABLE_SDL2_FRONTEND) - add_subdirectory(citra_sdl) -endif() - if (ENABLE_QT) add_subdirectory(citra_qt) endif() -if (ENABLE_QT OR ENABLE_SDL2_FRONTEND) +if (ENABLE_QT) # Or any other hypothetical future frontends add_subdirectory(citra_meta) endif() diff --git a/src/citra_meta/CMakeLists.txt b/src/citra_meta/CMakeLists.txt index 3b6d2b230..d2c367fb4 100644 --- a/src/citra_meta/CMakeLists.txt +++ b/src/citra_meta/CMakeLists.txt @@ -45,10 +45,6 @@ endif() target_link_libraries(citra_meta PRIVATE fmt) -if (ENABLE_SDL2_FRONTEND) - target_link_libraries(citra_meta PRIVATE citra_sdl) -endif() - if (ENABLE_QT) target_link_libraries(citra_meta PRIVATE citra_qt) target_link_libraries(citra_meta PRIVATE Boost::boost Qt6::Widgets) diff --git a/src/citra_meta/common_strings.h b/src/citra_meta/common_strings.h index 4d78f3a05..0276b697f 100644 --- a/src/citra_meta/common_strings.h +++ b/src/citra_meta/common_strings.h @@ -19,13 +19,6 @@ constexpr char help_string[] = "-r, --movie-record [path] Record a TAS movie to the given file path\n" "-a, --movie-record-author [author] Set the author for the recorded TAS movie (to be used " "alongside --movie-record)\n" -#ifdef ENABLE_SDL2_FRONTEND - "-n, --no-gui Use the lightweight SDL frontend instead of the usual Qt " - "frontend\n" - // TODO: Move -m outside of this check when it is implemented in Qt frontend - "-m, --multiplayer [nick:password@address:port] Nickname, password, address and port for " - "multiplayer (currently only usable with SDL frontend)\n" -#endif #ifdef ENABLE_ROOM " --room Utilize dedicated multiplayer room functionality (equivalent to " "the old citra-room executable)\n" diff --git a/src/citra_meta/main.cpp b/src/citra_meta/main.cpp index 134f2d2dd..f1dba092f 100644 --- a/src/citra_meta/main.cpp +++ b/src/citra_meta/main.cpp @@ -7,15 +7,16 @@ #include "common/detached_tasks.h" #include "common/scope_exit.h" +#if !defined(ENABLE_QT) +#error "citra_meta is somehow building with no frontend. This should be impossible!" +#endif + #ifdef ENABLE_QT #include "citra_qt/citra_qt.h" #endif #ifdef ENABLE_ROOM #include "citra_room/citra_room.h" #endif -#ifdef ENABLE_SDL2_FRONTEND -#include "citra_sdl/citra_sdl.h" -#endif #ifdef _WIN32 extern "C" { @@ -95,25 +96,6 @@ int main(int argc, char* argv[]) { #endif #if ENABLE_QT - bool no_gui = false; - for (int i = 1; i < argc; i++) { - if (strcmp(argv[i], "--no-gui") == 0 || strcmp(argv[i], "-n") == 0) { - no_gui = true; - } - } - - if (!no_gui) { - return LaunchQtFrontend(argc, argv); - } + return LaunchQtFrontend(argc, argv); #endif - -#if ENABLE_SDL2_FRONTEND - return LaunchSdlFrontend(argc, argv); -#else - std::cout << "Cannot use SDL frontend as it was disabled at compile time. Exiting." - << std::endl; - return -1; -#endif - - return 0; } diff --git a/src/citra_sdl/CMakeLists.txt b/src/citra_sdl/CMakeLists.txt deleted file mode 100644 index cf3deda2d..000000000 --- a/src/citra_sdl/CMakeLists.txt +++ /dev/null @@ -1,48 +0,0 @@ -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/CMakeModules) - -add_library(citra_sdl STATIC EXCLUDE_FROM_ALL - config.cpp - config.h - default_ini.h - emu_window/emu_window_sdl2.cpp - emu_window/emu_window_sdl2.h - citra_sdl.cpp - precompiled_headers.h - resource.h -) - -if (ENABLE_SOFTWARE_RENDERER) - target_sources(citra_sdl PRIVATE - emu_window/emu_window_sdl2_sw.cpp - emu_window/emu_window_sdl2_sw.h - ) -endif() -if (ENABLE_OPENGL) - target_sources(citra_sdl PRIVATE - emu_window/emu_window_sdl2_gl.cpp - emu_window/emu_window_sdl2_gl.h - ) -endif() -if (ENABLE_VULKAN) - target_sources(citra_sdl PRIVATE - emu_window/emu_window_sdl2_vk.cpp - emu_window/emu_window_sdl2_vk.h - ) -endif() - -create_target_directory_groups(citra_sdl) - -target_link_libraries(citra_sdl PRIVATE citra_common citra_core input_common network) -target_link_libraries(citra_sdl PRIVATE inih) -if (MSVC) - target_link_libraries(citra_sdl PRIVATE getopt) -endif() -target_link_libraries(citra_sdl PRIVATE ${PLATFORM_LIBRARIES} SDL2::SDL2 Threads::Threads) - -if (ENABLE_OPENGL) - target_link_libraries(citra_sdl PRIVATE glad) -endif() - -if (CITRA_USE_PRECOMPILED_HEADERS) - target_precompile_headers(citra_sdl PRIVATE precompiled_headers.h) -endif() diff --git a/src/citra_sdl/citra_sdl.cpp b/src/citra_sdl/citra_sdl.cpp deleted file mode 100644 index 9c0f4470a..000000000 --- a/src/citra_sdl/citra_sdl.cpp +++ /dev/null @@ -1,530 +0,0 @@ -// Copyright Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include -#include -#include - -// This needs to be included before getopt.h because the latter #defines symbols used by it -#include "common/microprofile.h" - -#include "citra_sdl/config.h" -#include "citra_sdl/emu_window/emu_window_sdl2.h" -#ifdef ENABLE_OPENGL -#include "citra_sdl/emu_window/emu_window_sdl2_gl.h" -#endif -#ifdef ENABLE_SOFTWARE_RENDERER -#include "citra_sdl/emu_window/emu_window_sdl2_sw.h" -#endif -#ifdef ENABLE_VULKAN -#include "citra_sdl/emu_window/emu_window_sdl2_vk.h" -#endif -#include "SDL_messagebox.h" -#include "citra_meta/common_strings.h" -#include "common/common_paths.h" -#include "common/file_util.h" -#include "common/logging/backend.h" -#include "common/logging/log.h" -#include "common/scm_rev.h" -#include "common/scope_exit.h" -#include "common/settings.h" -#include "common/string_util.h" -#include "core/core.h" -#include "core/dumping/backend.h" -#include "core/dumping/ffmpeg_backend.h" -#include "core/frontend/applets/default_applets.h" -#include "core/frontend/framebuffer_layout.h" -#include "core/hle/service/am/am.h" -#include "core/hle/service/cfg/cfg.h" -#include "core/movie.h" -#include "input_common/main.h" -#include "network/network.h" -#include "video_core/gpu.h" -#include "video_core/renderer_base.h" - -#ifdef __unix__ -#include "common/linux/gamemode.h" -#endif - -#undef _UNICODE -#include -#ifndef _MSC_VER -#include -#endif - -#ifdef _WIN32 -// windows.h needs to be included before shellapi.h -#include - -#include -#endif - -static void ShowCommandOutput(std::string title, std::string message) { -#ifdef _WIN32 - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, title.c_str(), message.c_str(), NULL); -#else - std::cout << message << std::endl; -#endif -} - -static void PrintHelp(const char* argv0) { - ShowCommandOutput("Help", fmt::format(Common::help_string, argv0)); -} - -static void OnStateChanged(const Network::RoomMember::State& state) { - switch (state) { - case Network::RoomMember::State::Idle: - LOG_DEBUG(Network, "Network is idle"); - break; - case Network::RoomMember::State::Joining: - LOG_DEBUG(Network, "Connection sequence to room started"); - break; - case Network::RoomMember::State::Joined: - LOG_DEBUG(Network, "Successfully joined to the room"); - break; - case Network::RoomMember::State::Moderator: - LOG_DEBUG(Network, "Successfully joined the room as a moderator"); - break; - default: - break; - } -} - -static void OnNetworkError(const Network::RoomMember::Error& error) { - switch (error) { - case Network::RoomMember::Error::LostConnection: - LOG_DEBUG(Network, "Lost connection to the room"); - break; - case Network::RoomMember::Error::CouldNotConnect: - LOG_ERROR(Network, "Error: Could not connect"); - std::exit(1); - break; - case Network::RoomMember::Error::NameCollision: - LOG_ERROR( - Network, - "You tried to use the same nickname as another user that is connected to the Room"); - std::exit(1); - break; - case Network::RoomMember::Error::MacCollision: - LOG_ERROR(Network, "You tried to use the same MAC-Address as another user that is " - "connected to the Room"); - std::exit(1); - break; - case Network::RoomMember::Error::ConsoleIdCollision: - LOG_ERROR(Network, "Your Console ID conflicted with someone else in the Room"); - std::exit(1); - break; - case Network::RoomMember::Error::WrongPassword: - LOG_ERROR(Network, "Room replied with: Wrong password"); - std::exit(1); - break; - case Network::RoomMember::Error::WrongVersion: - LOG_ERROR(Network, - "You are using a different version than the room you are trying to connect to"); - std::exit(1); - break; - case Network::RoomMember::Error::RoomIsFull: - LOG_ERROR(Network, "The room is full"); - std::exit(1); - break; - case Network::RoomMember::Error::HostKicked: - LOG_ERROR(Network, "You have been kicked by the host"); - break; - case Network::RoomMember::Error::HostBanned: - LOG_ERROR(Network, "You have been banned by the host"); - break; - default: - LOG_ERROR(Network, "Unknown network error: {}", error); - break; - } -} - -static void OnMessageReceived(const Network::ChatEntry& msg) { - std::cout << std::endl << msg.nickname << ": " << msg.message << std::endl << std::endl; -} - -static void OnStatusMessageReceived(const Network::StatusMessageEntry& msg) { - std::string message; - switch (msg.type) { - case Network::IdMemberJoin: - message = fmt::format("{} has joined", msg.nickname); - break; - case Network::IdMemberLeave: - message = fmt::format("{} has left", msg.nickname); - break; - case Network::IdMemberKicked: - message = fmt::format("{} has been kicked", msg.nickname); - break; - case Network::IdMemberBanned: - message = fmt::format("{} has been banned", msg.nickname); - break; - case Network::IdAddressUnbanned: - message = fmt::format("{} has been unbanned", msg.nickname); - break; - } - if (!message.empty()) - std::cout << std::endl << "* " << message << std::endl << std::endl; -} - -/// Application entry point -int LaunchSdlFrontend(int argc, char** argv) { - Common::Log::Initialize(); - Common::Log::SetColorConsoleBackendEnabled(true); - Common::Log::Start(); - SdlConfig config; - int option_index = 0; - bool use_gdbstub = Settings::values.use_gdbstub.GetValue(); - u32 gdb_port = static_cast(Settings::values.gdbstub_port.GetValue()); - std::string movie_record; - std::string movie_record_author; - std::string movie_play; - std::string dump_video; - - char* endarg; -#ifdef _WIN32 - int argc_w; - auto argv_w = CommandLineToArgvW(GetCommandLineW(), &argc_w); - - if (argv_w == nullptr) { - LOG_CRITICAL(Frontend, "Failed to get command line arguments"); - return -1; - } -#endif - std::string filepath; - - bool use_multiplayer = false; - bool fullscreen = false; - std::string nickname{}; - std::string password{}; - std::string address{}; - u16 port = Network::DefaultRoomPort; - - static struct option long_options[] = { - {"dump-video", required_argument, 0, 'd'}, - {"fullscreen", no_argument, 0, 'f'}, - {"gdbport", required_argument, 0, 'g'}, - {"help", no_argument, 0, 'h'}, - {"install", required_argument, 0, 'i'}, - {"movie-play", required_argument, 0, 'p'}, - {"movie-record", required_argument, 0, 'r'}, - {"movie-record-author", required_argument, 0, 'a'}, - {"multiplayer", required_argument, 0, 'm'}, - {"version", no_argument, 0, 'v'}, - {"windowed", no_argument, 0, 'w'}, - {0, 0, 0, 0}, - }; - - while (optind < argc) { - int arg = getopt_long(argc, argv, "d:fg:hi:p:r:a:m:nvw", long_options, &option_index); - if (arg != -1) { - switch (static_cast(arg)) { - case 'd': - dump_video = optarg; - break; - case 'f': - fullscreen = true; - LOG_INFO(Frontend, "Starting in fullscreen mode..."); - break; - case 'g': - errno = 0; - gdb_port = strtoul(optarg, &endarg, 0); - use_gdbstub = true; - if (endarg == optarg) - errno = EINVAL; - if (errno != 0) { - perror("--gdbport"); - return 1; - } - break; - case 'h': - PrintHelp(argv[0]); - return 0; - case 'i': { - const auto cia_progress = [](std::size_t written, std::size_t total) { - LOG_INFO(Frontend, "{:02d}%", (written * 100 / total)); - }; - if (Service::AM::InstallCIA(std::string(optarg), cia_progress) != - Service::AM::InstallStatus::Success) - errno = EINVAL; - if (errno != 0) - return 1; - break; - } - case 'p': - movie_play = optarg; - break; - case 'r': - movie_record = optarg; - break; - case 'a': - movie_record_author = optarg; - break; - case 'm': { - use_multiplayer = true; - const std::string str_arg(optarg); - // regex to check if the format is nickname:password@ip:port - // with optional :password - const std::regex re("^([^:]+)(?::(.+))?@([^:]+)(?::([0-9]+))?$"); - if (!std::regex_match(str_arg, re)) { - std::cout << "Wrong format for option --multiplayer\n"; - PrintHelp(argv[0]); - return 0; - } - - std::smatch match; - std::regex_search(str_arg, match, re); - ASSERT(match.size() == 5); - nickname = match[1]; - password = match[2]; - address = match[3]; - if (!match[4].str().empty()) - port = std::stoi(match[4]); - std::regex nickname_re("^[a-zA-Z0-9._\\- ]+$"); - if (!std::regex_match(nickname, nickname_re)) { - std::cout - << "Nickname is not valid. Must be 4 to 20 alphanumeric characters.\n"; - return 0; - } - if (address.empty()) { - std::cout << "Address to room must not be empty.\n"; - return 0; - } - break; - } - case 'v': - const std::string version_string = - std::string("Azahar ") + Common::g_build_fullname; - ShowCommandOutput("Version", version_string); - return 0; - } - } else { -#ifdef _WIN32 - filepath = Common::UTF16ToUTF8(argv_w[optind]); -#else - filepath = argv[optind]; -#endif - optind++; - } - } - -#ifdef _WIN32 - LocalFree(argv_w); -#endif - - MicroProfileOnThreadCreate("EmuThread"); - SCOPE_EXIT({ MicroProfileShutdown(); }); - - if (filepath.empty()) { - LOG_CRITICAL(Frontend, "Failed to load ROM: No ROM specified"); - return -1; - } - - if (!movie_record.empty() && !movie_play.empty()) { - LOG_CRITICAL(Frontend, "Cannot both play and record a movie"); - return -1; - } - - auto& system = Core::System::GetInstance(); - auto& movie = system.Movie(); - - if (!movie_record.empty()) { - movie.PrepareForRecording(); - } - if (!movie_play.empty()) { - movie.PrepareForPlayback(movie_play); - } - - // Apply the command line arguments - Settings::values.gdbstub_port = gdb_port; - Settings::values.use_gdbstub = use_gdbstub; - system.ApplySettings(); - - // Register frontend applets - Frontend::RegisterDefaultApplets(system); - - EmuWindow_SDL2::InitializeSDL2(); - - const auto create_emu_window = [&](bool fullscreen, - bool is_secondary) -> std::unique_ptr { - const auto graphics_api = Settings::values.graphics_api.GetValue(); - switch (graphics_api) { -#ifdef ENABLE_OPENGL - case Settings::GraphicsAPI::OpenGL: - return std::make_unique(system, fullscreen, is_secondary); -#endif -#ifdef ENABLE_VULKAN - case Settings::GraphicsAPI::Vulkan: - return std::make_unique(system, fullscreen, is_secondary); -#endif -#ifdef ENABLE_SOFTWARE_RENDERER - case Settings::GraphicsAPI::Software: - return std::make_unique(system, fullscreen, is_secondary); -#endif - default: - LOG_CRITICAL( - Frontend, - "Unknown or unsupported graphics API {}, falling back to available default", - graphics_api); -#ifdef ENABLE_OPENGL - return std::make_unique(system, fullscreen, is_secondary); -#elif ENABLE_VULKAN - return std::make_unique(system, fullscreen, is_secondary); -#elif ENABLE_SOFTWARE_RENDERER - return std::make_unique(system, fullscreen, is_secondary); -#else - // TODO: Add a null renderer backend for this, perhaps. -#error "At least one renderer must be enabled." -#endif - } - }; - - const auto emu_window{create_emu_window(fullscreen, false)}; - const bool use_secondary_window{ - Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows && - Settings::values.graphics_api.GetValue() != Settings::GraphicsAPI::Software}; - const auto secondary_window = use_secondary_window ? create_emu_window(false, true) : nullptr; - - const auto scope = emu_window->Acquire(); - - LOG_INFO(Frontend, "Azahar Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch, - Common::g_scm_desc); - Settings::LogSettings(); - - const Core::System::ResultStatus load_result{ - system.Load(*emu_window, filepath, secondary_window.get())}; - - switch (load_result) { - case Core::System::ResultStatus::ErrorGetLoader: - LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filepath); - return -1; - case Core::System::ResultStatus::ErrorLoader: - LOG_CRITICAL(Frontend, "Failed to load ROM!"); - return -1; - case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: - LOG_CRITICAL(Frontend, - "The application that you are trying to load must be decrypted before " - "being used with Azahar. \n\n For more information on dumping and " - "decrypting applications, please refer to: " - "https://web.archive.org/web/20240304210021/https://citra-emu.org/" - "wiki/dumping-game-cartridges/"); - return -1; - case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat: - LOG_CRITICAL(Frontend, "Error while loading ROM: The ROM format is not supported."); - return -1; - case Core::System::ResultStatus::ErrorNotInitialized: - LOG_CRITICAL(Frontend, "CPUCore not initialized"); - return -1; - case Core::System::ResultStatus::ErrorSystemMode: - LOG_CRITICAL(Frontend, "Failed to determine system mode!"); - return -1; - case Core::System::ResultStatus::Success: - break; // Expected case - default: - LOG_ERROR(Frontend, "Error while loading ROM: {}", system.GetStatusDetails()); - break; - } - - if (use_multiplayer) { - if (auto member = Network::GetRoomMember().lock()) { - member->BindOnChatMessageRecieved(OnMessageReceived); - member->BindOnStatusMessageReceived(OnStatusMessageReceived); - member->BindOnStateChanged(OnStateChanged); - member->BindOnError(OnNetworkError); - LOG_DEBUG(Network, "Start connection to {}:{} with nickname {}", address, port, - nickname); - member->Join(nickname, Service::CFG::GetConsoleIdHash(system), address.c_str(), port, 0, - Network::NoPreferredMac, password); - } else { - LOG_ERROR(Network, "Could not access RoomMember"); - return 0; - } - } - - if (!movie_play.empty()) { - auto metadata = movie.GetMovieMetadata(movie_play); - LOG_INFO(Movie, "Author: {}", metadata.author); - LOG_INFO(Movie, "Rerecord count: {}", metadata.rerecord_count); - LOG_INFO(Movie, "Input count: {}", metadata.input_count); - movie.StartPlayback(movie_play); - } - if (!movie_record.empty()) { - movie.StartRecording(movie_record, movie_record_author); - } - if (!dump_video.empty() && DynamicLibrary::FFmpeg::LoadFFmpeg()) { - auto& renderer = system.GPU().Renderer(); - const auto layout{ - Layout::FrameLayoutFromResolutionScale(renderer.GetResolutionScaleFactor())}; - auto dumper = std::make_shared(renderer); - if (dumper->StartDumping(dump_video, layout)) { - system.RegisterVideoDumper(dumper); - } - } - -#ifdef __unix__ - Common::Linux::StartGamemode(); -#endif - - std::thread main_render_thread([&emu_window] { emu_window->Present(); }); - std::thread secondary_render_thread([&secondary_window] { - if (secondary_window) { - secondary_window->Present(); - } - }); - - u64 program_id{}; - system.GetAppLoader().ReadProgramId(program_id); - system.GPU().ApplyPerProgramSettings(program_id); - - std::atomic_bool stop_run; - system.GPU().Renderer().Rasterizer()->LoadDefaultDiskResources( - stop_run, [](VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total) { - LOG_DEBUG(Frontend, "Loading stage {} progress {} {}", static_cast(stage), value, - total); - }); - - const auto secondary_is_open = [&secondary_window] { - // if the secondary window isn't created, it shouldn't affect the main loop - return secondary_window ? secondary_window->IsOpen() : true; - }; - while (emu_window->IsOpen() && secondary_is_open()) { - const auto result = system.RunLoop(); - - switch (result) { - case Core::System::ResultStatus::ShutdownRequested: - emu_window->RequestClose(); - break; - case Core::System::ResultStatus::Success: - break; - default: - LOG_ERROR(Frontend, "Error in main run loop: {}", result, system.GetStatusDetails()); - break; - } - } - emu_window->RequestClose(); - if (secondary_window) { - secondary_window->RequestClose(); - } - main_render_thread.join(); - secondary_render_thread.join(); - - movie.Shutdown(); - - auto video_dumper = system.GetVideoDumper(); - if (video_dumper && video_dumper->IsDumping()) { - video_dumper->StopDumping(); - } - - Network::Shutdown(); - InputCommon::Shutdown(); - - system.Shutdown(); - -#ifdef __unix__ - Common::Linux::StopGamemode(); -#endif - - return 0; -} diff --git a/src/citra_sdl/citra_sdl.h b/src/citra_sdl/citra_sdl.h deleted file mode 100644 index 9a1a2b6c3..000000000 --- a/src/citra_sdl/citra_sdl.h +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -int LaunchSdlFrontend(int argc, char** argv); diff --git a/src/citra_sdl/config.cpp b/src/citra_sdl/config.cpp deleted file mode 100644 index a504dd3aa..000000000 --- a/src/citra_sdl/config.cpp +++ /dev/null @@ -1,395 +0,0 @@ -// Copyright Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include -#include -#include -#include -#include "citra_sdl/config.h" -#include "citra_sdl/default_ini.h" -#include "common/file_util.h" -#include "common/logging/backend.h" -#include "common/logging/log.h" -#include "common/settings.h" -#include "core/hle/service/service.h" -#include "input_common/main.h" -#include "input_common/udp/client.h" -#include "network/network_settings.h" - -SdlConfig::SdlConfig() { - // TODO: Don't hardcode the path; let the frontend decide where to put the config files. - sdl2_config_loc = FileUtil::GetUserPath(FileUtil::UserPath::ConfigDir) + "sdl2-config.ini"; - sdl2_config = std::make_unique(sdl2_config_loc); - - Reload(); -} - -SdlConfig::~SdlConfig() = default; - -bool SdlConfig::LoadINI(const std::string& default_contents, bool retry) { - const std::string& location = this->sdl2_config_loc; - if (sdl2_config->ParseError() < 0) { - if (retry) { - LOG_WARNING(Config, "Failed to load {}. Creating file from defaults...", location); - FileUtil::CreateFullPath(location); - FileUtil::WriteStringToFile(true, location, default_contents); - sdl2_config = std::make_unique(location); // Reopen file - - return LoadINI(default_contents, false); - } - LOG_ERROR(Config, "Failed."); - return false; - } - LOG_INFO(Config, "Successfully loaded {}", location); - return true; -} - -static const std::array default_buttons = { - SDL_SCANCODE_A, SDL_SCANCODE_S, SDL_SCANCODE_Z, SDL_SCANCODE_X, SDL_SCANCODE_T, SDL_SCANCODE_G, - SDL_SCANCODE_F, SDL_SCANCODE_H, SDL_SCANCODE_Q, SDL_SCANCODE_W, SDL_SCANCODE_M, SDL_SCANCODE_N, - SDL_SCANCODE_O, SDL_SCANCODE_P, SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_B, -}; - -static const std::array, Settings::NativeAnalog::NumAnalogs> default_analogs{{ - { - SDL_SCANCODE_UP, - SDL_SCANCODE_DOWN, - SDL_SCANCODE_LEFT, - SDL_SCANCODE_RIGHT, - SDL_SCANCODE_D, - }, - { - SDL_SCANCODE_I, - SDL_SCANCODE_K, - SDL_SCANCODE_J, - SDL_SCANCODE_L, - SDL_SCANCODE_D, - }, -}}; - -template <> -void SdlConfig::ReadSetting(const std::string& group, Settings::Setting& setting) { - std::string setting_value = sdl2_config->Get(group, setting.GetLabel(), setting.GetDefault()); - if (setting_value.empty()) { - setting_value = setting.GetDefault(); - } - setting = std::move(setting_value); -} - -template <> -void SdlConfig::ReadSetting(const std::string& group, Settings::Setting& setting) { - setting = sdl2_config->GetBoolean(group, setting.GetLabel(), setting.GetDefault()); -} - -template -void SdlConfig::ReadSetting(const std::string& group, Settings::Setting& setting) { - if constexpr (std::is_floating_point_v) { - setting = static_cast( - sdl2_config->GetReal(group, setting.GetLabel(), setting.GetDefault())); - } else { - setting = static_cast(sdl2_config->GetInteger( - group, setting.GetLabel(), static_cast(setting.GetDefault()))); - } -} - -void SdlConfig::ReadValues() { - // Controls - // TODO: add multiple input profile support - for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) { - std::string default_param = InputCommon::GenerateKeyboardParam(default_buttons[i]); - Settings::values.current_input_profile.buttons[i] = - sdl2_config->GetString("Controls", Settings::NativeButton::mapping[i], default_param); - if (Settings::values.current_input_profile.buttons[i].empty()) - Settings::values.current_input_profile.buttons[i] = default_param; - } - - for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) { - std::string default_param = InputCommon::GenerateAnalogParamFromKeys( - default_analogs[i][0], default_analogs[i][1], default_analogs[i][2], - default_analogs[i][3], default_analogs[i][4], 0.5f); - Settings::values.current_input_profile.analogs[i] = - sdl2_config->GetString("Controls", Settings::NativeAnalog::mapping[i], default_param); - if (Settings::values.current_input_profile.analogs[i].empty()) - Settings::values.current_input_profile.analogs[i] = default_param; - } - - Settings::values.current_input_profile.motion_device = sdl2_config->GetString( - "Controls", "motion_device", - "engine:motion_emu,update_period:100,sensitivity:0.01,tilt_clamp:90.0"); - Settings::values.current_input_profile.touch_device = - sdl2_config->GetString("Controls", "touch_device", "engine:emu_window"); - Settings::values.current_input_profile.udp_input_address = sdl2_config->GetString( - "Controls", "udp_input_address", InputCommon::CemuhookUDP::DEFAULT_ADDR); - Settings::values.current_input_profile.udp_input_port = - static_cast(sdl2_config->GetInteger("Controls", "udp_input_port", - InputCommon::CemuhookUDP::DEFAULT_PORT)); - ReadSetting("Controls", Settings::values.use_artic_base_controller); - - // Core - ReadSetting("Core", Settings::values.use_cpu_jit); - ReadSetting("Core", Settings::values.cpu_clock_percentage); - - // Renderer - ReadSetting("Renderer", Settings::values.graphics_api); - ReadSetting("Renderer", Settings::values.physical_device); - ReadSetting("Renderer", Settings::values.spirv_shader_gen); - ReadSetting("Renderer", Settings::values.async_shader_compilation); - ReadSetting("Renderer", Settings::values.async_presentation); - ReadSetting("Renderer", Settings::values.use_gles); - ReadSetting("Renderer", Settings::values.use_hw_shader); - ReadSetting("Renderer", Settings::values.shaders_accurate_mul); - ReadSetting("Renderer", Settings::values.use_shader_jit); - ReadSetting("Renderer", Settings::values.resolution_factor); - ReadSetting("Renderer", Settings::values.use_disk_shader_cache); - ReadSetting("Renderer", Settings::values.frame_limit); - ReadSetting("Renderer", Settings::values.use_vsync); - ReadSetting("Renderer", Settings::values.texture_filter); - ReadSetting("Renderer", Settings::values.texture_sampling); - ReadSetting("Renderer", Settings::values.delay_game_render_thread_us); - - ReadSetting("Renderer", Settings::values.mono_render_option); - ReadSetting("Renderer", Settings::values.render_3d); - ReadSetting("Renderer", Settings::values.factor_3d); - ReadSetting("Renderer", Settings::values.pp_shader_name); - ReadSetting("Renderer", Settings::values.anaglyph_shader_name); - ReadSetting("Renderer", Settings::values.filter_mode); - - ReadSetting("Renderer", Settings::values.bg_red); - ReadSetting("Renderer", Settings::values.bg_green); - ReadSetting("Renderer", Settings::values.bg_blue); - ReadSetting("Renderer", Settings::values.disable_right_eye_render); - - // Layout - ReadSetting("Layout", Settings::values.layout_option); - ReadSetting("Layout", Settings::values.swap_screen); - ReadSetting("Layout", Settings::values.upright_screen); - ReadSetting("Layout", Settings::values.large_screen_proportion); - ReadSetting("Layout", Settings::values.custom_top_x); - ReadSetting("Layout", Settings::values.custom_top_y); - ReadSetting("Layout", Settings::values.custom_top_width); - ReadSetting("Layout", Settings::values.custom_top_height); - ReadSetting("Layout", Settings::values.custom_bottom_x); - ReadSetting("Layout", Settings::values.custom_bottom_y); - ReadSetting("Layout", Settings::values.custom_bottom_width); - ReadSetting("Layout", Settings::values.custom_bottom_height); - ReadSetting("Layout", Settings::values.custom_second_layer_opacity); - - ReadSetting("Layout", Settings::values.screen_top_stretch); - ReadSetting("Layout", Settings::values.screen_top_leftright_padding); - ReadSetting("Layout", Settings::values.screen_top_topbottom_padding); - ReadSetting("Layout", Settings::values.screen_bottom_stretch); - ReadSetting("Layout", Settings::values.screen_bottom_leftright_padding); - ReadSetting("Layout", Settings::values.screen_bottom_topbottom_padding); - - ReadSetting("Layout", Settings::values.portrait_layout_option); - ReadSetting("Layout", Settings::values.custom_portrait_top_x); - ReadSetting("Layout", Settings::values.custom_portrait_top_y); - ReadSetting("Layout", Settings::values.custom_portrait_top_width); - ReadSetting("Layout", Settings::values.custom_portrait_top_height); - ReadSetting("Layout", Settings::values.custom_portrait_bottom_x); - ReadSetting("Layout", Settings::values.custom_portrait_bottom_y); - ReadSetting("Layout", Settings::values.custom_portrait_bottom_width); - ReadSetting("Layout", Settings::values.custom_portrait_bottom_height); - - // Utility - ReadSetting("Utility", Settings::values.dump_textures); - ReadSetting("Utility", Settings::values.custom_textures); - ReadSetting("Utility", Settings::values.preload_textures); - ReadSetting("Utility", Settings::values.async_custom_loading); - - // Audio - ReadSetting("Audio", Settings::values.audio_emulation); - ReadSetting("Audio", Settings::values.enable_audio_stretching); - ReadSetting("Audio", Settings::values.enable_realtime_audio); - ReadSetting("Audio", Settings::values.volume); - ReadSetting("Audio", Settings::values.output_type); - ReadSetting("Audio", Settings::values.output_device); - ReadSetting("Audio", Settings::values.input_type); - ReadSetting("Audio", Settings::values.input_device); - - // Data Storage - ReadSetting("Data Storage", Settings::values.use_virtual_sd); - ReadSetting("Data Storage", Settings::values.use_custom_storage); - ReadSetting("Data Storage", Settings::values.compress_cia_installs); - - if (Settings::values.use_custom_storage) { - FileUtil::UpdateUserPath(FileUtil::UserPath::NANDDir, - sdl2_config->GetString("Data Storage", "nand_directory", "")); - FileUtil::UpdateUserPath(FileUtil::UserPath::SDMCDir, - sdl2_config->GetString("Data Storage", "sdmc_directory", "")); - } - - // System - ReadSetting("System", Settings::values.is_new_3ds); - ReadSetting("System", Settings::values.lle_applets); - ReadSetting("System", Settings::values.enable_required_online_lle_modules); - ReadSetting("System", Settings::values.region_value); - ReadSetting("System", Settings::values.init_clock); - { - std::tm t; - t.tm_sec = 1; - t.tm_min = 0; - t.tm_hour = 0; - t.tm_mday = 1; - t.tm_mon = 0; - t.tm_year = 100; - t.tm_isdst = 0; - std::istringstream string_stream( - sdl2_config->GetString("System", "init_time", "2000-01-01 00:00:01")); - string_stream >> std::get_time(&t, "%Y-%m-%d %H:%M:%S"); - if (string_stream.fail()) { - LOG_ERROR(Config, "Failed To parse init_time. Using 2000-01-01 00:00:01"); - } - Settings::values.init_time = - std::chrono::duration_cast( - std::chrono::system_clock::from_time_t(std::mktime(&t)).time_since_epoch()) - .count(); - } - ReadSetting("System", Settings::values.init_ticks_type); - ReadSetting("System", Settings::values.init_ticks_override); - ReadSetting("System", Settings::values.plugin_loader_enabled); - ReadSetting("System", Settings::values.allow_plugin_loader); - ReadSetting("System", Settings::values.steps_per_hour); - ReadSetting("System", Settings::values.apply_region_free_patch); - - { - constexpr const char* default_init_time_offset = "0 00:00:00"; - - std::string offset_string = - sdl2_config->GetString("System", "init_time_offset", default_init_time_offset); - - std::size_t sep_index = offset_string.find(' '); - - if (sep_index == std::string::npos) { - LOG_ERROR(Config, "Failed to parse init_time_offset. Using 0 00:00:00"); - offset_string = default_init_time_offset; - - sep_index = offset_string.find(' '); - } - - std::string day_string = offset_string.substr(0, sep_index); - long long days = 0; - - try { - days = std::stoll(day_string); - } catch (std::logic_error&) { - LOG_ERROR(Config, "Failed to parse days in init_time_offset. Using 0"); - days = 0; - } - - long long days_in_seconds = days * 86400; - - std::tm t; - t.tm_sec = 0; - t.tm_min = 0; - t.tm_hour = 0; - t.tm_mday = 1; - t.tm_mon = 0; - t.tm_year = 100; - t.tm_isdst = 0; - - std::istringstream string_stream(offset_string.substr(sep_index + 1)); - string_stream >> std::get_time(&t, "%H:%M:%S"); - - if (string_stream.fail()) { - LOG_ERROR(Config, - "Failed to parse hours, minutes and seconds in init_time_offset. 00:00:00"); - } - - auto time_offset = - std::chrono::system_clock::from_time_t(std::mktime(&t)).time_since_epoch(); - - auto secs = std::chrono::duration_cast(time_offset).count(); - - Settings::values.init_time_offset = static_cast(secs) + days_in_seconds; - } - - // Camera - using namespace Service::CAM; - Settings::values.camera_name[OuterRightCamera] = - sdl2_config->GetString("Camera", "camera_outer_right_name", "blank"); - Settings::values.camera_config[OuterRightCamera] = - sdl2_config->GetString("Camera", "camera_outer_right_config", ""); - Settings::values.camera_flip[OuterRightCamera] = - sdl2_config->GetInteger("Camera", "camera_outer_right_flip", 0); - Settings::values.camera_name[InnerCamera] = - sdl2_config->GetString("Camera", "camera_inner_name", "blank"); - Settings::values.camera_config[InnerCamera] = - sdl2_config->GetString("Camera", "camera_inner_config", ""); - Settings::values.camera_flip[InnerCamera] = - sdl2_config->GetInteger("Camera", "camera_inner_flip", 0); - Settings::values.camera_name[OuterLeftCamera] = - sdl2_config->GetString("Camera", "camera_outer_left_name", "blank"); - Settings::values.camera_config[OuterLeftCamera] = - sdl2_config->GetString("Camera", "camera_outer_left_config", ""); - Settings::values.camera_flip[OuterLeftCamera] = - sdl2_config->GetInteger("Camera", "camera_outer_left_flip", 0); - - // Miscellaneous - ReadSetting("Miscellaneous", Settings::values.log_filter); - ReadSetting("Miscellaneous", Settings::values.log_regex_filter); - ReadSetting("Miscellaneous", Settings::values.delay_start_for_lle_modules); - ReadSetting("Miscellaneous", Settings::values.deterministic_async_operations); - - // Apply the log_filter setting as the logger has already been initialized - // and doesn't pick up the filter on its own. - Common::Log::Filter filter; - filter.ParseFilterString(Settings::values.log_filter.GetValue()); - Common::Log::SetGlobalFilter(filter); - Common::Log::SetRegexFilter(Settings::values.log_regex_filter.GetValue()); - - // Debugging - Settings::values.record_frame_times = - sdl2_config->GetBoolean("Debugging", "record_frame_times", false); - ReadSetting("Debugging", Settings::values.renderer_debug); - ReadSetting("Debugging", Settings::values.use_gdbstub); - ReadSetting("Debugging", Settings::values.gdbstub_port); - ReadSetting("Debugging", Settings::values.instant_debug_log); - ReadSetting("Debugging", Settings::values.enable_rpc_server); - - for (const auto& service_module : Service::service_module_map) { - bool use_lle = sdl2_config->GetBoolean("Debugging", "LLE\\" + service_module.name, false); - Settings::values.lle_modules.emplace(service_module.name, use_lle); - } - - // Web Service - NetSettings::values.web_api_url = - sdl2_config->GetString("WebService", "web_api_url", "https://api.citra-emu.org"); - NetSettings::values.citra_username = sdl2_config->GetString("WebService", "citra_username", ""); - NetSettings::values.citra_token = sdl2_config->GetString("WebService", "citra_token", ""); - - // Video Dumping - Settings::values.output_format = - sdl2_config->GetString("Video Dumping", "output_format", "webm"); - Settings::values.format_options = sdl2_config->GetString("Video Dumping", "format_options", ""); - - Settings::values.video_encoder = - sdl2_config->GetString("Video Dumping", "video_encoder", "libvpx-vp9"); - - // Options for variable bit rate live streaming taken from here: - // https://developers.google.com/media/vp9/live-encoding - std::string default_video_options; - if (Settings::values.video_encoder == "libvpx-vp9") { - default_video_options = - "quality:realtime,speed:6,tile-columns:4,frame-parallel:1,threads:8,row-mt:1"; - } - Settings::values.video_encoder_options = - sdl2_config->GetString("Video Dumping", "video_encoder_options", default_video_options); - Settings::values.video_bitrate = - sdl2_config->GetInteger("Video Dumping", "video_bitrate", 2500000); - - Settings::values.audio_encoder = - sdl2_config->GetString("Video Dumping", "audio_encoder", "libvorbis"); - Settings::values.audio_encoder_options = - sdl2_config->GetString("Video Dumping", "audio_encoder_options", ""); - Settings::values.audio_bitrate = - sdl2_config->GetInteger("Video Dumping", "audio_bitrate", 64000); -} - -void SdlConfig::Reload() { - LoadINI(DefaultINI::sdl2_config_file); - ReadValues(); -} diff --git a/src/citra_sdl/config.h b/src/citra_sdl/config.h deleted file mode 100644 index d7c3e6c80..000000000 --- a/src/citra_sdl/config.h +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include -#include "common/settings.h" - -class INIReader; - -class SdlConfig { - std::unique_ptr sdl2_config; - std::string sdl2_config_loc; - - bool LoadINI(const std::string& default_contents = "", bool retry = true); - void ReadValues(); - -public: - SdlConfig(); - ~SdlConfig(); - - void Reload(); - -private: - /** - * Applies a value read from the sdl2_config to a Setting. - * - * @param group The name of the INI group - * @param setting The yuzu setting to modify - */ - template - void ReadSetting(const std::string& group, Settings::Setting& setting); -}; diff --git a/src/citra_sdl/default_ini.h b/src/citra_sdl/default_ini.h deleted file mode 100644 index 4c97dbb89..000000000 --- a/src/citra_sdl/default_ini.h +++ /dev/null @@ -1,405 +0,0 @@ -// Copyright Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -namespace DefaultINI { - -const char* sdl2_config_file = R"( -[Controls] -# The input devices and parameters for each 3DS native input -# It should be in the format of "engine:[engine_name],[param1]:[value1],[param2]:[value2]..." -# Escape characters $0 (for ':'), $1 (for ',') and $2 (for '$') can be used in values - -# for button input, the following devices are available: -# - "keyboard" (default) for keyboard input. Required parameters: -# - "code": the code of the key to bind -# - "sdl" for joystick input using SDL. Required parameters: -# - "joystick": the index of the joystick to bind -# - "button"(optional): the index of the button to bind -# - "hat"(optional): the index of the hat to bind as direction buttons -# - "axis"(optional): the index of the axis to bind -# - "direction"(only used for hat): the direction name of the hat to bind. Can be "up", "down", "left" or "right" -# - "threshold"(only used for axis): a float value in (-1.0, 1.0) which the button is -# triggered if the axis value crosses -# - "direction"(only used for axis): "+" means the button is triggered when the axis value -# is greater than the threshold; "-" means the button is triggered when the axis value -# is smaller than the threshold -button_a= -button_b= -button_x= -button_y= -button_up= -button_down= -button_left= -button_right= -button_l= -button_r= -button_start= -button_select= -button_debug= -button_gpio14= -button_zl= -button_zr= -button_home= - -# for analog input, the following devices are available: -# - "analog_from_button" (default) for emulating analog input from direction buttons. Required parameters: -# - "up", "down", "left", "right": sub-devices for each direction. -# Should be in the format as a button input devices using escape characters, for example, "engine$0keyboard$1code$00" -# - "modifier": sub-devices as a modifier. -# - "modifier_scale": a float number representing the applied modifier scale to the analog input. -# Must be in range of 0.0-1.0. Defaults to 0.5 -# - "sdl" for joystick input using SDL. Required parameters: -# - "joystick": the index of the joystick to bind -# - "axis_x": the index of the axis to bind as x-axis (default to 0) -# - "axis_y": the index of the axis to bind as y-axis (default to 1) -circle_pad= -c_stick= - -# for motion input, the following devices are available: -# - "motion_emu" (default) for emulating motion input from mouse input. Required parameters: -# - "update_period": update period in milliseconds (default to 100) -# - "sensitivity": the coefficient converting mouse movement to tilting angle (default to 0.01) -# - "tilt_clamp": the max value of the tilt angle in degrees (default to 90) -# - "cemuhookudp" reads motion input from a udp server that uses cemuhook's udp protocol -motion_device= - -# for touch input, the following devices are available: -# - "emu_window" (default) for emulating touch input from mouse input to the emulation window. No parameters required -# - "cemuhookudp" reads touch input from a udp server that uses cemuhook's udp protocol -# - "min_x", "min_y", "max_x", "max_y": defines the udp device's touch screen coordinate system -touch_device= - -# Most desktop operating systems do not expose a way to poll the motion state of the controllers -# so as a way around it, cemuhook created a udp client/server protocol to broadcast the data directly -# from a controller device to the client program. Citra has a client that can connect and read -# from any cemuhook compatible motion program. - -# IPv4 address of the udp input server (Default "127.0.0.1") -udp_input_address= - -# Port of the udp input server. (Default 26760) -udp_input_port= - -# The pad to request data on. Should be between 0 (Pad 1) and 3 (Pad 4). (Default 0) -udp_pad_index= - -[Core] -# Whether to use the Just-In-Time (JIT) compiler for CPU emulation -# 0: Interpreter (slow), 1 (default): JIT (fast) -use_cpu_jit = - -# Change the Clock Frequency of the emulated 3DS CPU. -# Underclocking can increase the performance of the game at the risk of freezing. -# Overclocking may fix lag that happens on console, but also comes with the risk of freezing. -# Range is any positive integer (but we suspect 25 - 400 is a good idea) Default is 100 -cpu_clock_percentage = - -[Renderer] -# Whether to render using OpenGL or Software -# 0: Software, 1: OpenGL (default), 2: Vulkan -graphics_api = - -# Whether to render using GLES or OpenGL -# 0 (default): OpenGL, 1: GLES -use_gles = - -# Whether to use hardware shaders to emulate 3DS shaders -# 0: Software, 1 (default): Hardware -use_hw_shader = - -# Whether to use accurate multiplication in hardware shaders -# 0: Off (Faster, but causes issues in some games) 1: On (Default. Slower, but correct) -shaders_accurate_mul = - -# Whether to use the Just-In-Time (JIT) compiler for shader emulation -# 0: Interpreter (slow), 1 (default): JIT (fast) -use_shader_jit = - -# Forces VSync on the display thread. Usually doesn't impact performance, but on some drivers it can -# so only turn this off if you notice a speed difference. -# 0: Off, 1 (default): On -use_vsync = - -# Reduce stuttering by storing and loading generated shaders to disk -# 0: Off, 1 (default. On) -use_disk_shader_cache = - -# Resolution scale factor -# 0: Auto (scales resolution to window size), 1: Native 3DS screen resolution, Otherwise a scale -# factor for the 3DS resolution -resolution_factor = - -# Texture filter -# 0: None, 1: Anime4K, 2: Bicubic, 3: Nearest Neighbor, 4: ScaleForce, 5: xBRZ -texture_filter = - -# Limits the speed of the game to run no faster than this value as a percentage of target speed. -# Will not have an effect if unthrottled is enabled. -# 5 - 995: Speed limit as a percentage of target game speed. 0 for unthrottled. 100 (default) -frame_limit = - -# Overrides the frame limiter to use frame_limit_alternate instead of frame_limit. -# 0: Off (default), 1: On -use_frame_limit_alternate = - -# Alternate speed limit to be used instead of frame_limit if use_frame_limit_alternate is enabled -# 5 - 995: Speed limit as a percentage of target game speed. 0 for unthrottled. 200 (default) -frame_limit_alternate = - -# The clear color for the renderer. What shows up on the sides of the bottom screen. -# Must be in range of 0.0-1.0. Defaults to 0.0 for all. -bg_red = -bg_blue = -bg_green = - -# Whether and how Stereoscopic 3D should be rendered -# 0 (default): Off, 1: Side by Side, 2: Reverse Side by Side, 3: Anaglyph, 4: Interlaced, 5: Reverse Interlaced -render_3d = - -# Change 3D Intensity -# 0 - 100: Intensity. 0 (default) -factor_3d = - -# Swap Eyes in 3D -# true or false (default) -swap_eyes_3d = - -# Change Default Eye to Render When in Monoscopic Mode -# 0 (default): Left, 1: Right -mono_render_option = - -# The name of the post processing shader to apply. -# Loaded from shaders if render_3d is off or side by side. -pp_shader_name = - -# The name of the shader to apply when render_3d is anaglyph. -# Loaded from shaders/anaglyph -anaglyph_shader_name = - -# Whether to enable linear filtering or not -# This is required for some shaders to work correctly -# 0: Nearest, 1 (default): Linear -filter_mode = - -[Layout] -# Layout for the screen inside the render window. -# 0 (default): Default Above/Below Screen -# 1: Single Screen Only -# 2: Large Screen Small Screen -# 3: Side by Side -# 4: Separate Windows -# 5: Hybrid Screen -# 6: Custom Layout -layout_option = - -# Screen placement when using Custom layout option -# 0x, 0y is the top left corner of the render window. -custom_top_x = -custom_top_y = -custom_top_width = -custom_top_height = -custom_bottom_x = -custom_bottom_y = -custom_bottom_width = -custom_bottom_height = - -# Opacity of second layer when using custom layout option (bottom screen unless swapped) -custom_second_layer_opacity = - -# Swaps the prominent screen with the other screen. -# For example, if Single Screen is chosen, setting this to 1 will display the bottom screen instead of the top screen. -# 0 (default): Top Screen is prominent, 1: Bottom Screen is prominent -swap_screen = - -# Toggle upright orientation, for book style games. -# 0 (default): Off, 1: On -upright_screen = - -# The proportion between the large and small screens when playing in Large Screen Small Screen layout. -# Must be a real value between 1.0 and 16.0. Default is 4 -large_screen_proportion = - -# Dumps textures as PNG to dump/textures/[Title ID]/. -# 0 (default): Off, 1: On -dump_textures = - -# Reads PNG files from load/textures/[Title ID]/ and replaces textures. -# 0 (default): Off, 1: On -custom_textures = - -# Loads all custom textures into memory before booting. -# 0 (default): Off, 1: On -preload_textures = - -# Loads custom textures asynchronously with background threads. -# 0: Off, 1 (default): On -async_custom_loading = - -[Audio] -# Whether or not to enable DSP LLE -# 0 (default): No, 1: Yes -enable_dsp_lle = - -# Whether or not to run DSP LLE on a different thread -# 0 (default): No, 1: Yes -enable_dsp_lle_thread = - -# Whether or not to enable the audio-stretching post-processing effect. -# This effect adjusts audio speed to match emulation speed and helps prevent audio stutter, -# at the cost of increasing audio latency. -# 0: No, 1 (default): Yes -enable_audio_stretching = - -# Scales audio playback speed to account for drops in emulation framerate -# 0 (default): No, 1: Yes -enable_realtime_audio = - -# Output volume. -# 1.0 (default): 100%, 0.0; mute -volume = - -# Which audio output type to use. -# 0 (default): Auto-select, 1: No audio output, 2: Cubeb (if available), 3: OpenAL (if available), 4: SDL2 (if available) -output_type = - -# Which audio output device to use. -# auto (default): Auto-select -output_device = - -# Which audio input type to use. -# 0 (default): Auto-select, 1: No audio input, 2: Static noise, 3: Cubeb (if available), 4: OpenAL (if available) -input_type = - -# Which audio input device to use. -# auto (default): Auto-select -input_device = - -[Data Storage] -# Whether to create a virtual SD card. -# 1 (default): Yes, 0: No -use_virtual_sd = - -# Whether to use custom storage locations -# 1: Yes, 0 (default): No -use_custom_storage = - -# The path of the virtual SD card directory. -# empty (default) will use the user_path -sdmc_directory = - -# The path of NAND directory. -# empty (default) will use the user_path -nand_directory = - -[System] -# The system model that Citra will try to emulate -# 0: Old 3DS, 1: New 3DS (default) -is_new_3ds = - -# Whether to use LLE system applets, if installed -# 0 (default): No, 1: Yes -lle_applets = - -# The system region that Citra will use during emulation -# -1: Auto-select (default), 0: Japan, 1: USA, 2: Europe, 3: Australia, 4: China, 5: Korea, 6: Taiwan -region_value = - -# The clock to use when citra starts -# 0: System clock (default), 1: fixed time -init_clock = - -# Time used when init_clock is set to fixed_time in the format %Y-%m-%d %H:%M:%S -# set to fixed time. Default 2000-01-01 00:00:01 -# Note: 3DS can only handle times later then Jan 1 2000 -init_time = - -# The system ticks count to use when citra starts -# 0: Random (default), 1: Fixed -init_ticks_type = - -# Tick count to use when init_ticks_type is set to Fixed. -# Defaults to 0. -init_ticks_override = - -# Number of steps per hour reported by the pedometer. Range from 0 to 65,535. -# Defaults to 0. -steps_per_hour = - -[Camera] -# Which camera engine to use for the right outer camera -# blank (default): a dummy camera that always returns black image -camera_outer_right_name = - -# A config string for the right outer camera. Its meaning is defined by the camera engine -camera_outer_right_config = - -# The image flip to apply -# 0: None (default), 1: Horizontal, 2: Vertical, 3: Reverse -camera_outer_right_flip = - -# ... for the left outer camera -camera_outer_left_name = -camera_outer_left_config = -camera_outer_left_flip = - -# ... for the inner camera -camera_inner_name = -camera_inner_config = -camera_inner_flip = - -[Miscellaneous] -# A filter which removes logs below a certain logging level. -# Examples: *:Debug Kernel.SVC:Trace Service.*:Critical -log_filter = *:Info - -[Debugging] -# Record frame time data, can be found in the log directory. Boolean value -record_frame_times = - -# Port for listening to GDB connections. -use_gdbstub=false -gdbstub_port=24689 - -# Whether to enable additional debugging information during emulation -# 0 (default): Off, 1: On -renderer_debug = - -# To LLE a service module add "LLE\=true" - -[WebService] -# URL for Web API -web_api_url = -# Username and token for Citra Web Service -citra_username = -citra_token = - -[Video Dumping] -# Format of the video to output, default: webm -output_format = - -# Options passed to the muxer (optional) -# This is a param package, format: [key1]:[value1],[key2]:[value2],... -format_options = - -# Video encoder used, default: libvpx-vp9 -video_encoder = - -# Options passed to the video codec (optional) -video_encoder_options = - -# Video bitrate, default: 2500000 -video_bitrate = - -# Audio encoder used, default: libvorbis -audio_encoder = - -# Options passed to the audio codec (optional) -audio_encoder_options = - -# Audio bitrate, default: 64000 -audio_bitrate = -)"; -} diff --git a/src/citra_sdl/emu_window/emu_window_sdl2.cpp b/src/citra_sdl/emu_window/emu_window_sdl2.cpp deleted file mode 100644 index a84e38669..000000000 --- a/src/citra_sdl/emu_window/emu_window_sdl2.cpp +++ /dev/null @@ -1,252 +0,0 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include -#define SDL_MAIN_HANDLED -#include -#include "citra_sdl/emu_window/emu_window_sdl2.h" -#include "common/logging/log.h" -#include "common/scm_rev.h" -#include "core/core.h" -#include "input_common/keyboard.h" -#include "input_common/main.h" -#include "input_common/motion_emu.h" -#include "network/network.h" - -void EmuWindow_SDL2::OnMouseMotion(s32 x, s32 y) { - TouchMoved((unsigned)std::max(x, 0), (unsigned)std::max(y, 0)); - InputCommon::GetMotionEmu()->Tilt(x, y); -} - -void EmuWindow_SDL2::OnMouseButton(u32 button, u8 state, s32 x, s32 y) { - if (button == SDL_BUTTON_LEFT) { - if (state == SDL_PRESSED) { - TouchPressed((unsigned)std::max(x, 0), (unsigned)std::max(y, 0)); - } else { - TouchReleased(); - } - } else if (button == SDL_BUTTON_RIGHT) { - if (state == SDL_PRESSED) { - InputCommon::GetMotionEmu()->BeginTilt(x, y); - } else { - InputCommon::GetMotionEmu()->EndTilt(); - } - } -} - -std::pair EmuWindow_SDL2::TouchToPixelPos(float touch_x, float touch_y) const { - int w, h; - SDL_GetWindowSize(render_window, &w, &h); - - touch_x *= w; - touch_y *= h; - - return {static_cast(std::max(std::round(touch_x), 0.0f)), - static_cast(std::max(std::round(touch_y), 0.0f))}; -} - -void EmuWindow_SDL2::OnFingerDown(float x, float y) { - // TODO(NeatNit): keep track of multitouch using the fingerID and a dictionary of some kind - // This isn't critical because the best we can do when we have that is to average them, like the - // 3DS does - - const auto [px, py] = TouchToPixelPos(x, y); - TouchPressed(px, py); -} - -void EmuWindow_SDL2::OnFingerMotion(float x, float y) { - const auto [px, py] = TouchToPixelPos(x, y); - TouchMoved(px, py); -} - -void EmuWindow_SDL2::OnFingerUp() { - TouchReleased(); -} - -void EmuWindow_SDL2::OnKeyEvent(int key, u8 state) { - if (state == SDL_PRESSED) { - InputCommon::GetKeyboard()->PressKey(key); - } else if (state == SDL_RELEASED) { - InputCommon::GetKeyboard()->ReleaseKey(key); - } -} - -bool EmuWindow_SDL2::IsOpen() const { - return is_open; -} - -void EmuWindow_SDL2::RequestClose() { - is_open = false; -} - -void EmuWindow_SDL2::OnResize() { - int width, height; - SDL_GL_GetDrawableSize(render_window, &width, &height); - UpdateCurrentFramebufferLayout(width, height); -} - -void EmuWindow_SDL2::Fullscreen() { - if (SDL_SetWindowFullscreen(render_window, SDL_WINDOW_FULLSCREEN) == 0) { - return; - } - - LOG_ERROR(Frontend, "Fullscreening failed: {}", SDL_GetError()); - - // Try a different fullscreening method - LOG_INFO(Frontend, "Attempting to use borderless fullscreen..."); - if (SDL_SetWindowFullscreen(render_window, SDL_WINDOW_FULLSCREEN_DESKTOP) == 0) { - return; - } - - LOG_ERROR(Frontend, "Borderless fullscreening failed: {}", SDL_GetError()); - - // Fallback algorithm: Maximise window. - // Works on all systems (unless something is seriously wrong), so no fallback for this one. - LOG_INFO(Frontend, "Falling back on a maximised window..."); - SDL_MaximizeWindow(render_window); -} - -EmuWindow_SDL2::EmuWindow_SDL2(Core::System& system_, bool is_secondary) - : EmuWindow(is_secondary), system(system_) {} - -EmuWindow_SDL2::~EmuWindow_SDL2() { - SDL_Quit(); -} - -void EmuWindow_SDL2::InitializeSDL2() { - if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) { - LOG_CRITICAL(Frontend, "Failed to initialize SDL2: {}! Exiting...", SDL_GetError()); - exit(1); - } - - InputCommon::Init(); - Network::Init(); - - SDL_SetMainReady(); -} - -u32 EmuWindow_SDL2::GetEventWindowId(const SDL_Event& event) const { - switch (event.type) { - case SDL_WINDOWEVENT: - return event.window.windowID; - case SDL_KEYDOWN: - case SDL_KEYUP: - return event.key.windowID; - case SDL_MOUSEMOTION: - return event.motion.windowID; - case SDL_MOUSEBUTTONDOWN: - case SDL_MOUSEBUTTONUP: - return event.button.windowID; - case SDL_MOUSEWHEEL: - return event.wheel.windowID; - case SDL_FINGERDOWN: - case SDL_FINGERMOTION: - case SDL_FINGERUP: - return event.tfinger.windowID; - case SDL_TEXTEDITING: - return event.edit.windowID; - case SDL_TEXTEDITING_EXT: - return event.editExt.windowID; - case SDL_TEXTINPUT: - return event.text.windowID; - case SDL_DROPBEGIN: - case SDL_DROPFILE: - case SDL_DROPTEXT: - case SDL_DROPCOMPLETE: - return event.drop.windowID; - case SDL_USEREVENT: - return event.user.windowID; - default: - // Event is not for any particular window, so we can just pretend it's for this one. - return render_window_id; - } -} - -void EmuWindow_SDL2::PollEvents() { - SDL_Event event; - std::vector other_window_events; - - // SDL_PollEvent returns 0 when there are no more events in the event queue - while (SDL_PollEvent(&event)) { - if (GetEventWindowId(event) != render_window_id) { - other_window_events.push_back(event); - continue; - } - - switch (event.type) { - case SDL_WINDOWEVENT: - switch (event.window.event) { - case SDL_WINDOWEVENT_SIZE_CHANGED: - case SDL_WINDOWEVENT_RESIZED: - case SDL_WINDOWEVENT_MAXIMIZED: - case SDL_WINDOWEVENT_RESTORED: - case SDL_WINDOWEVENT_MINIMIZED: - OnResize(); - break; - case SDL_WINDOWEVENT_CLOSE: - RequestClose(); - break; - } - break; - case SDL_KEYDOWN: - case SDL_KEYUP: - OnKeyEvent(static_cast(event.key.keysym.scancode), event.key.state); - break; - case SDL_MOUSEMOTION: - // ignore if it came from touch - if (event.button.which != SDL_TOUCH_MOUSEID) - OnMouseMotion(event.motion.x, event.motion.y); - break; - case SDL_MOUSEBUTTONDOWN: - case SDL_MOUSEBUTTONUP: - // ignore if it came from touch - if (event.button.which != SDL_TOUCH_MOUSEID) { - OnMouseButton(event.button.button, event.button.state, event.button.x, - event.button.y); - } - break; - case SDL_FINGERDOWN: - OnFingerDown(event.tfinger.x, event.tfinger.y); - break; - case SDL_FINGERMOTION: - OnFingerMotion(event.tfinger.x, event.tfinger.y); - break; - case SDL_FINGERUP: - OnFingerUp(); - break; - case SDL_QUIT: - RequestClose(); - break; - default: - break; - } - } - for (auto& e : other_window_events) { - // This is a somewhat hacky workaround to re-emit window events meant for another window - // since SDL_PollEvent() is global but we poll events per window. - SDL_PushEvent(&e); - } - if (!is_secondary) { - UpdateFramerateCounter(); - } -} - -void EmuWindow_SDL2::OnMinimalClientAreaChangeRequest(std::pair minimal_size) { - SDL_SetWindowMinimumSize(render_window, minimal_size.first, minimal_size.second); -} - -void EmuWindow_SDL2::UpdateFramerateCounter() { - const u32 current_time = SDL_GetTicks(); - if (current_time > last_time + 2000) { - const auto results = system.GetAndResetPerfStats(); - const auto title = - fmt::format("Azahar {} | {}-{} | FPS: {:.0f} ({:.0f}%)", Common::g_build_fullname, - Common::g_scm_branch, Common::g_scm_desc, results.game_fps, - results.emulation_speed * 100.0f); - SDL_SetWindowTitle(render_window, title.c_str()); - last_time = current_time; - } -} diff --git a/src/citra_sdl/emu_window/emu_window_sdl2.h b/src/citra_sdl/emu_window/emu_window_sdl2.h deleted file mode 100644 index 28f86f81a..000000000 --- a/src/citra_sdl/emu_window/emu_window_sdl2.h +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include "common/common_types.h" -#include "core/frontend/emu_window.h" - -union SDL_Event; -struct SDL_Window; - -namespace Core { -class System; -} - -class EmuWindow_SDL2 : public Frontend::EmuWindow { -public: - explicit EmuWindow_SDL2(Core::System& system_, bool is_secondary); - ~EmuWindow_SDL2(); - - /// Initializes SDL2 - static void InitializeSDL2(); - - /// Presents the most recent frame from the video backend - virtual void Present() {} - - /// Polls window events - void PollEvents() override; - - /// Whether the window is still open, and a close request hasn't yet been sent - bool IsOpen() const; - - /// Close the window. - void RequestClose(); - -protected: - /// Gets the ID of the window an event originated from. - u32 GetEventWindowId(const SDL_Event& event) const; - - /// Called by PollEvents when a key is pressed or released. - void OnKeyEvent(int key, u8 state); - - /// Called by PollEvents when the mouse moves. - void OnMouseMotion(s32 x, s32 y); - - /// Called by PollEvents when a mouse button is pressed or released - void OnMouseButton(u32 button, u8 state, s32 x, s32 y); - - /// Translates pixel position (0..1) to pixel positions - std::pair TouchToPixelPos(float touch_x, float touch_y) const; - - /// Called by PollEvents when a finger starts touching the touchscreen - void OnFingerDown(float x, float y); - - /// Called by PollEvents when a finger moves while touching the touchscreen - void OnFingerMotion(float x, float y); - - /// Called by PollEvents when a finger stops touching the touchscreen - void OnFingerUp(); - - /// Called by PollEvents when any event that may cause the window to be resized occurs - void OnResize(); - - /// Called when user passes the fullscreen parameter flag - void Fullscreen(); - - /// Called when a configuration change affects the minimal size of the window - void OnMinimalClientAreaChangeRequest(std::pair minimal_size) override; - - /// Called when polling to update framerate - void UpdateFramerateCounter(); - - /// Is the window still open? - bool is_open = true; - - /// Internal SDL2 render window - SDL_Window* render_window; - - /// Internal SDL2 window ID - u32 render_window_id{}; - - /// Fake hidden window for the core context - SDL_Window* dummy_window; - - /// Keeps track of how often to update the title bar during gameplay - u32 last_time = 0; - - Core::System& system; -}; diff --git a/src/citra_sdl/emu_window/emu_window_sdl2_gl.cpp b/src/citra_sdl/emu_window/emu_window_sdl2_gl.cpp deleted file mode 100644 index f2fc28dc3..000000000 --- a/src/citra_sdl/emu_window/emu_window_sdl2_gl.cpp +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include -#define SDL_MAIN_HANDLED -#include -#include -#include "citra_sdl/emu_window/emu_window_sdl2_gl.h" -#include "common/scm_rev.h" -#include "common/settings.h" -#include "core/core.h" -#include "video_core/gpu.h" -#include "video_core/renderer_base.h" - -class SDLGLContext : public Frontend::GraphicsContext { -public: - using SDL_GLContext = void*; - - SDLGLContext() { - window = SDL_CreateWindow(NULL, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 0, 0, - SDL_WINDOW_HIDDEN | SDL_WINDOW_OPENGL); - context = SDL_GL_CreateContext(window); - } - - ~SDLGLContext() override { - SDL_GL_DeleteContext(context); - SDL_DestroyWindow(window); - } - - void MakeCurrent() override { - SDL_GL_MakeCurrent(window, context); - } - - void DoneCurrent() override { - SDL_GL_MakeCurrent(window, nullptr); - } - -private: - SDL_Window* window; - SDL_GLContext context; -}; - -static SDL_Window* CreateGLWindow(const std::string& window_title, bool gles) { - if (gles) { - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); - } else { - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); - } - return SDL_CreateWindow(window_title.c_str(), - SDL_WINDOWPOS_UNDEFINED, // x position - SDL_WINDOWPOS_UNDEFINED, // y position - Core::kScreenTopWidth, - Core::kScreenTopHeight + Core::kScreenBottomHeight, - SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); -} - -EmuWindow_SDL2_GL::EmuWindow_SDL2_GL(Core::System& system_, bool fullscreen, bool is_secondary) - : EmuWindow_SDL2{system_, is_secondary} { - SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); - SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); - SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); - SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); - SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 0); - // Enable context sharing for the shared context - SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1); - // Enable vsync - SDL_GL_SetSwapInterval(1); - // Enable debug context - if (Settings::values.renderer_debug) { - SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG); - } - - std::string window_title = fmt::format("Azahar {} | {}-{}", Common::g_build_fullname, - Common::g_scm_branch, Common::g_scm_desc); - - // First, try to create a context with the requested type. - render_window = CreateGLWindow(window_title, Settings::values.use_gles.GetValue()); - if (render_window == nullptr) { - // On failure, fall back to context with flipped type. - render_window = CreateGLWindow(window_title, !Settings::values.use_gles.GetValue()); - if (render_window == nullptr) { - LOG_CRITICAL(Frontend, "Failed to create SDL2 window: {}", SDL_GetError()); - exit(1); - } - } - - strict_context_required = std::strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0; - - dummy_window = SDL_CreateWindow(NULL, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 0, 0, - SDL_WINDOW_HIDDEN | SDL_WINDOW_OPENGL); - - if (fullscreen) { - Fullscreen(); - } - - window_context = SDL_GL_CreateContext(render_window); - core_context = CreateSharedContext(); - last_saved_context = nullptr; - - if (window_context == nullptr) { - LOG_CRITICAL(Frontend, "Failed to create SDL2 GL context: {}", SDL_GetError()); - exit(1); - } - if (core_context == nullptr) { - LOG_CRITICAL(Frontend, "Failed to create shared SDL2 GL context: {}", SDL_GetError()); - exit(1); - } - - render_window_id = SDL_GetWindowID(render_window); - - int profile_mask = 0; - SDL_GL_GetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, &profile_mask); - auto gl_load_func = - profile_mask == SDL_GL_CONTEXT_PROFILE_ES ? gladLoadGLES2Loader : gladLoadGLLoader; - - if (!gl_load_func(static_cast(SDL_GL_GetProcAddress))) { - LOG_CRITICAL(Frontend, "Failed to initialize GL functions: {}", SDL_GetError()); - exit(1); - } - - OnResize(); - OnMinimalClientAreaChangeRequest(GetActiveConfig().min_client_area_size); - SDL_PumpEvents(); -} - -EmuWindow_SDL2_GL::~EmuWindow_SDL2_GL() { - core_context.reset(); - SDL_DestroyWindow(render_window); - SDL_GL_DeleteContext(window_context); -} - -std::unique_ptr EmuWindow_SDL2_GL::CreateSharedContext() const { - return std::make_unique(); -} - -void EmuWindow_SDL2_GL::MakeCurrent() { - core_context->MakeCurrent(); -} - -void EmuWindow_SDL2_GL::DoneCurrent() { - core_context->DoneCurrent(); -} - -void EmuWindow_SDL2_GL::SaveContext() { - last_saved_context = SDL_GL_GetCurrentContext(); -} - -void EmuWindow_SDL2_GL::RestoreContext() { - SDL_GL_MakeCurrent(render_window, last_saved_context); -} - -void EmuWindow_SDL2_GL::Present() { - SDL_GL_MakeCurrent(render_window, window_context); - SDL_GL_SetSwapInterval(1); - while (IsOpen()) { - system.GPU().Renderer().TryPresent(100, is_secondary); - SDL_GL_SwapWindow(render_window); - } - SDL_GL_MakeCurrent(render_window, nullptr); -} diff --git a/src/citra_sdl/emu_window/emu_window_sdl2_gl.h b/src/citra_sdl/emu_window/emu_window_sdl2_gl.h deleted file mode 100644 index 6e9045cba..000000000 --- a/src/citra_sdl/emu_window/emu_window_sdl2_gl.h +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include "citra_sdl/emu_window/emu_window_sdl2.h" - -struct SDL_Window; - -namespace Core { -class System; -} - -class EmuWindow_SDL2_GL : public EmuWindow_SDL2 { -public: - explicit EmuWindow_SDL2_GL(Core::System& system_, bool fullscreen, bool is_secondary); - ~EmuWindow_SDL2_GL(); - - void Present() override; - std::unique_ptr CreateSharedContext() const override; - void MakeCurrent() override; - void DoneCurrent() override; - void SaveContext() override; - void RestoreContext() override; - -private: - using SDL_GLContext = void*; - - /// The OpenGL context associated with the window - SDL_GLContext window_context; - - /// Used by SaveContext and RestoreContext - SDL_GLContext last_saved_context; - - /// The OpenGL context associated with the core - std::unique_ptr core_context; -}; diff --git a/src/citra_sdl/emu_window/emu_window_sdl2_sw.cpp b/src/citra_sdl/emu_window/emu_window_sdl2_sw.cpp deleted file mode 100644 index 8da894fec..000000000 --- a/src/citra_sdl/emu_window/emu_window_sdl2_sw.cpp +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include -#define SDL_MAIN_HANDLED -#include -#include -#include "citra_sdl/emu_window/emu_window_sdl2_sw.h" -#include "common/scm_rev.h" -#include "common/settings.h" -#include "core/core.h" -#include "core/frontend/emu_window.h" -#include "video_core/gpu.h" -#include "video_core/renderer_software/renderer_software.h" - -class DummyContext : public Frontend::GraphicsContext {}; - -EmuWindow_SDL2_SW::EmuWindow_SDL2_SW(Core::System& system_, bool fullscreen, bool is_secondary) - : EmuWindow_SDL2{system_, is_secondary}, system{system_} { - std::string window_title = fmt::format("Azahar {} | {}-{}", Common::g_build_fullname, - Common::g_scm_branch, Common::g_scm_desc); - render_window = - SDL_CreateWindow(window_title.c_str(), - SDL_WINDOWPOS_UNDEFINED, // x position - SDL_WINDOWPOS_UNDEFINED, // y position - Core::kScreenTopWidth, Core::kScreenTopHeight + Core::kScreenBottomHeight, - SDL_WINDOW_SHOWN); - - if (render_window == nullptr) { - LOG_CRITICAL(Frontend, "Failed to create SDL2 window: {}", SDL_GetError()); - exit(1); - } - - window_surface = SDL_GetWindowSurface(render_window); - renderer = SDL_CreateSoftwareRenderer(window_surface); - - if (renderer == nullptr) { - LOG_CRITICAL(Frontend, "Failed to create SDL2 software renderer: {}", SDL_GetError()); - exit(1); - } - - if (fullscreen) { - Fullscreen(); - } - - render_window_id = SDL_GetWindowID(render_window); - - OnResize(); - OnMinimalClientAreaChangeRequest(GetActiveConfig().min_client_area_size); - SDL_PumpEvents(); -} - -EmuWindow_SDL2_SW::~EmuWindow_SDL2_SW() { - SDL_DestroyRenderer(renderer); - SDL_DestroyWindow(render_window); -} - -std::unique_ptr EmuWindow_SDL2_SW::CreateSharedContext() const { - return std::make_unique(); -} - -void EmuWindow_SDL2_SW::Present() { - const auto layout{Layout::DefaultFrameLayout( - Core::kScreenTopWidth, Core::kScreenTopHeight + Core::kScreenBottomHeight, false, false)}; - - using VideoCore::ScreenId; - - while (IsOpen()) { - SDL_SetRenderDrawColor(renderer, - static_cast(Settings::values.bg_red.GetValue() * 255), - static_cast(Settings::values.bg_green.GetValue() * 255), - static_cast(Settings::values.bg_blue.GetValue() * 255), 0xFF); - SDL_RenderClear(renderer); - - const auto draw_screen = [&](ScreenId screen_id) { - const auto dst_rect = - screen_id == ScreenId::TopLeft ? layout.top_screen : layout.bottom_screen; - SDL_Rect sdl_rect{static_cast(dst_rect.left), static_cast(dst_rect.top), - static_cast(dst_rect.GetWidth()), - static_cast(dst_rect.GetHeight())}; - SDL_Surface* screen = LoadFramebuffer(screen_id); - SDL_BlitSurface(screen, nullptr, window_surface, &sdl_rect); - SDL_FreeSurface(screen); - }; - - draw_screen(ScreenId::TopLeft); - draw_screen(ScreenId::Bottom); - - SDL_RenderPresent(renderer); - SDL_UpdateWindowSurface(render_window); - } -} - -SDL_Surface* EmuWindow_SDL2_SW::LoadFramebuffer(VideoCore::ScreenId screen_id) { - const auto& renderer = static_cast(system.GPU().Renderer()); - const auto& info = renderer.Screen(screen_id); - const int width = static_cast(info.width); - const int height = static_cast(info.height); - SDL_Surface* surface = - SDL_CreateRGBSurfaceWithFormat(0, width, height, 0, SDL_PIXELFORMAT_ABGR8888); - SDL_LockSurface(surface); - std::memcpy(surface->pixels, info.pixels.data(), info.pixels.size()); - SDL_UnlockSurface(surface); - return surface; -} diff --git a/src/citra_sdl/emu_window/emu_window_sdl2_sw.h b/src/citra_sdl/emu_window/emu_window_sdl2_sw.h deleted file mode 100644 index 12f446e33..000000000 --- a/src/citra_sdl/emu_window/emu_window_sdl2_sw.h +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include "citra_sdl/emu_window/emu_window_sdl2.h" - -struct SDL_Renderer; -struct SDL_Surface; - -namespace VideoCore { -enum class ScreenId : u32; -} - -namespace Core { -class System; -} - -class EmuWindow_SDL2_SW : public EmuWindow_SDL2 { -public: - explicit EmuWindow_SDL2_SW(Core::System& system, bool fullscreen, bool is_secondary); - ~EmuWindow_SDL2_SW(); - - void Present() override; - std::unique_ptr CreateSharedContext() const override; - void MakeCurrent() override {} - void DoneCurrent() override {} - -private: - /// Loads a framebuffer to an SDL surface - SDL_Surface* LoadFramebuffer(VideoCore::ScreenId screen_id); - - /// The system class. - Core::System& system; - - /// The SDL software renderer - SDL_Renderer* renderer; - - /// The window surface - SDL_Surface* window_surface; -}; diff --git a/src/citra_sdl/emu_window/emu_window_sdl2_vk.cpp b/src/citra_sdl/emu_window/emu_window_sdl2_vk.cpp deleted file mode 100644 index b002c15bb..000000000 --- a/src/citra_sdl/emu_window/emu_window_sdl2_vk.cpp +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include -#include -#include -#include -#include "citra_sdl/emu_window/emu_window_sdl2_vk.h" -#include "common/logging/log.h" -#include "common/scm_rev.h" -#include "core/frontend/emu_window.h" - -class DummyContext : public Frontend::GraphicsContext {}; - -EmuWindow_SDL2_VK::EmuWindow_SDL2_VK(Core::System& system, bool fullscreen, bool is_secondary) - : EmuWindow_SDL2{system, is_secondary} { - const std::string window_title = fmt::format("Azahar {} | {}-{}", Common::g_build_fullname, - Common::g_scm_branch, Common::g_scm_desc); - render_window = - SDL_CreateWindow(window_title.c_str(), - SDL_WINDOWPOS_UNDEFINED, // x position - SDL_WINDOWPOS_UNDEFINED, // y position - Core::kScreenTopWidth, Core::kScreenTopHeight + Core::kScreenBottomHeight, - SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); - SDL_SysWMinfo wm; - SDL_VERSION(&wm.version); - if (SDL_GetWindowWMInfo(render_window, &wm) == SDL_FALSE) { - LOG_CRITICAL(Frontend, "Failed to get information from the window manager"); - std::exit(EXIT_FAILURE); - } - - if (fullscreen) { - Fullscreen(); - SDL_ShowCursor(false); - } - - switch (wm.subsystem) { -#ifdef SDL_VIDEO_DRIVER_WINDOWS - case SDL_SYSWM_TYPE::SDL_SYSWM_WINDOWS: - window_info.type = Frontend::WindowSystemType::Windows; - window_info.render_surface = reinterpret_cast(wm.info.win.window); - break; -#endif -#ifdef SDL_VIDEO_DRIVER_X11 - case SDL_SYSWM_TYPE::SDL_SYSWM_X11: - window_info.type = Frontend::WindowSystemType::X11; - window_info.display_connection = wm.info.x11.display; - window_info.render_surface = reinterpret_cast(wm.info.x11.window); - break; -#endif -#ifdef SDL_VIDEO_DRIVER_WAYLAND - case SDL_SYSWM_TYPE::SDL_SYSWM_WAYLAND: - window_info.type = Frontend::WindowSystemType::Wayland; - window_info.display_connection = wm.info.wl.display; - window_info.render_surface = wm.info.wl.surface; - break; -#endif -#ifdef SDL_VIDEO_DRIVER_COCOA - case SDL_SYSWM_TYPE::SDL_SYSWM_COCOA: - window_info.type = Frontend::WindowSystemType::MacOS; - window_info.render_surface = SDL_Metal_GetLayer(SDL_Metal_CreateView(render_window)); - break; -#endif -#ifdef SDL_VIDEO_DRIVER_ANDROID - case SDL_SYSWM_TYPE::SDL_SYSWM_ANDROID: - window_info.type = Frontend::WindowSystemType::Android; - window_info.render_surface = reinterpret_cast(wm.info.android.window); - break; -#endif - default: - LOG_CRITICAL(Frontend, "Window manager subsystem {} not implemented", wm.subsystem); - std::exit(EXIT_FAILURE); - break; - } - - render_window_id = SDL_GetWindowID(render_window); - - OnResize(); - OnMinimalClientAreaChangeRequest(GetActiveConfig().min_client_area_size); - SDL_PumpEvents(); -} - -EmuWindow_SDL2_VK::~EmuWindow_SDL2_VK() = default; - -std::unique_ptr EmuWindow_SDL2_VK::CreateSharedContext() const { - return std::make_unique(); -} diff --git a/src/citra_sdl/emu_window/emu_window_sdl2_vk.h b/src/citra_sdl/emu_window/emu_window_sdl2_vk.h deleted file mode 100644 index ce0f1d8ce..000000000 --- a/src/citra_sdl/emu_window/emu_window_sdl2_vk.h +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include "citra_sdl/emu_window/emu_window_sdl2.h" - -namespace Frontend { -class GraphicsContext; -} - -namespace Core { -class System; -} - -class EmuWindow_SDL2_VK final : public EmuWindow_SDL2 { -public: - explicit EmuWindow_SDL2_VK(Core::System& system_, bool fullscreen, bool is_secondary); - ~EmuWindow_SDL2_VK() override; - - std::unique_ptr CreateSharedContext() const override; -}; diff --git a/src/citra_sdl/precompiled_headers.h b/src/citra_sdl/precompiled_headers.h deleted file mode 100644 index ffbb5e177..000000000 --- a/src/citra_sdl/precompiled_headers.h +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2022 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include "common/common_precompiled_headers.h" diff --git a/src/citra_sdl/resource.h b/src/citra_sdl/resource.h deleted file mode 100644 index df8e459e4..000000000 --- a/src/citra_sdl/resource.h +++ /dev/null @@ -1,16 +0,0 @@ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by pcafe.rc -// -#define IDI_ICON3 103 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 105 -#define _APS_NEXT_COMMAND_VALUE 40001 -#define _APS_NEXT_CONTROL_VALUE 1001 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif From 4010f4bc1f558ee8dd22f4e1bbf039e28f46b563 Mon Sep 17 00:00:00 2001 From: RedBlackAka <140876408+RedBlackAka@users.noreply.github.com> Date: Fri, 20 Feb 2026 21:34:03 +0100 Subject: [PATCH 04/42] common/cpu_detect: Remove SSE/SSE2 detection (#1754) --- src/common/x64/cpu_detect.cpp | 4 ---- src/common/x64/cpu_detect.h | 2 -- 2 files changed, 6 deletions(-) diff --git a/src/common/x64/cpu_detect.cpp b/src/common/x64/cpu_detect.cpp index 08290eb7f..b58c4f0c8 100644 --- a/src/common/x64/cpu_detect.cpp +++ b/src/common/x64/cpu_detect.cpp @@ -80,10 +80,6 @@ static CPUCaps Detect() { if (max_std_fn >= 1) { __cpuid(cpu_id, 0x00000001); - if ((cpu_id[3] >> 25) & 1) - caps.sse = true; - if ((cpu_id[3] >> 26) & 1) - caps.sse2 = true; if ((cpu_id[2]) & 1) caps.sse3 = true; if ((cpu_id[2] >> 9) & 1) diff --git a/src/common/x64/cpu_detect.h b/src/common/x64/cpu_detect.h index 7018ec0c1..6d09da549 100644 --- a/src/common/x64/cpu_detect.h +++ b/src/common/x64/cpu_detect.h @@ -16,8 +16,6 @@ namespace Common { struct CPUCaps { char cpu_string[0x21]; char brand_string[0x41]; - bool sse; - bool sse2; bool sse3; bool ssse3; bool sse4_1; From 9628300ff514cd36d035ec2055b6c46b674698a6 Mon Sep 17 00:00:00 2001 From: RedBlackAka <140876408+RedBlackAka@users.noreply.github.com> Date: Fri, 20 Feb 2026 21:34:21 +0100 Subject: [PATCH 05/42] citra_meta: Use integrated SSE4.2 detection method (#1753) --- src/citra_meta/main.cpp | 35 +++++------------------------------ 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/src/citra_meta/main.cpp b/src/citra_meta/main.cpp index f1dba092f..92ea7756f 100644 --- a/src/citra_meta/main.cpp +++ b/src/citra_meta/main.cpp @@ -26,43 +26,18 @@ __declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001; #endif #if CITRA_HAS_SSE42 -#if defined(_WIN32) +#include "common/x64/cpu_detect.h" +#ifdef _WIN32 #include -#if defined(_MSC_VER) -#include -#else -#include -#endif // _MSC_VER -#else -#include -#endif // _WIN32 - -static bool CpuSupportsSSE42() { - uint32_t ecx; - -#if defined(_MSC_VER) - int cpu_info[4]; - __cpuid(cpu_info, 1); - ecx = static_cast(cpu_info[2]); -#elif defined(__GNUC__) || defined(__clang__) - uint32_t eax, ebx, edx; - if (!__get_cpuid(1, &eax, &ebx, &ecx, &edx)) { - return false; - } -#else -#error "Unsupported compiler" #endif - // Bit 20 of ECX indicates SSE4.2 - return (ecx & (1 << 20)) != 0; -} - static bool CheckAndReportSSE42() { - if (!CpuSupportsSSE42()) { + const auto& caps = Common::GetCPUCaps(); + if (!caps.sse4_2) { const std::string error_msg = "This application requires a CPU with SSE4.2 support or higher.\nTo run on unsupported " "systems, recompile the application with the ENABLE_SSE42 option disabled."; -#if defined(_WIN32) +#ifdef _WIN32 MessageBoxA(nullptr, error_msg.c_str(), "Incompatible CPU", MB_OK | MB_ICONERROR); #endif std::cerr << "Error: " << error_msg << std::endl; From 1092295f2aeec32cee1aa6ad9120fe73dcf3d138 Mon Sep 17 00:00:00 2001 From: jbm11208 <81182113+jbm11208@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:39:04 -0500 Subject: [PATCH 06/42] Fix Shadow Rendering / Texture Filtering (#1675) * video_core/renderer_vulkan: Add texture filtering * Fix Shadow Rendering (again...) * Make individual image views per res scale * Refactor texture runtime * Fix some magic numbers * More fixes and filter pipeline cache. * Refactor Surface and Handle move and destructor --------- Co-authored-by: PabloMK7 --- src/common/common_funcs.h | 46 ++ .../rasterizer_cache/pixel_format.h | 9 +- .../renderer_vulkan/vk_blit_helper.cpp | 385 +++++++++++++- .../renderer_vulkan/vk_blit_helper.h | 36 +- .../renderer_vulkan/vk_rasterizer.cpp | 6 +- .../renderer_vulkan/vk_render_manager.cpp | 20 +- .../renderer_vulkan/vk_render_manager.h | 7 +- .../renderer_vulkan/vk_resource_pool.cpp | 5 +- .../renderer_vulkan/vk_texture_runtime.cpp | 486 +++++++++--------- .../renderer_vulkan/vk_texture_runtime.h | 146 ++++-- 10 files changed, 835 insertions(+), 311 deletions(-) diff --git a/src/common/common_funcs.h b/src/common/common_funcs.h index 8f109a8d3..0de82b040 100644 --- a/src/common/common_funcs.h +++ b/src/common/common_funcs.h @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + // Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -102,3 +106,45 @@ __declspec(dllimport) void __stdcall DebugBreak(void); using T = std::underlying_type_t; \ return static_cast(key) == 0; \ } + +#define DECLARE_ENUM_ARITHMETIC_OPERATORS(type) \ + [[nodiscard]] constexpr type operator+(type a, type b) noexcept { \ + using T = std::underlying_type_t; \ + return static_cast(static_cast(a) + static_cast(b)); \ + } \ + [[nodiscard]] constexpr type operator-(type a, type b) noexcept { \ + using T = std::underlying_type_t; \ + return static_cast(static_cast(a) - static_cast(b)); \ + } \ + [[nodiscard]] constexpr type operator*(type a, type b) noexcept { \ + using T = std::underlying_type_t; \ + return static_cast(static_cast(a) * static_cast(b)); \ + } \ + [[nodiscard]] constexpr type operator/(type a, type b) noexcept { \ + using T = std::underlying_type_t; \ + return static_cast(static_cast(a) / static_cast(b)); \ + } \ + [[nodiscard]] constexpr type operator%(type a, type b) noexcept { \ + using T = std::underlying_type_t; \ + return static_cast(static_cast(a) % static_cast(b)); \ + } \ + constexpr type& operator+=(type& a, type b) noexcept { \ + a = a + b; \ + return a; \ + } \ + constexpr type& operator-=(type& a, type b) noexcept { \ + a = a - b; \ + return a; \ + } \ + constexpr type& operator*=(type& a, type b) noexcept { \ + a = a * b; \ + return a; \ + } \ + constexpr type& operator/=(type& a, type b) noexcept { \ + a = a / b; \ + return a; \ + } \ + constexpr type& operator%=(type& a, type b) noexcept { \ + a = a % b; \ + return a; \ + } diff --git a/src/video_core/rasterizer_cache/pixel_format.h b/src/video_core/rasterizer_cache/pixel_format.h index 06e31ca46..51c5d6480 100644 --- a/src/video_core/rasterizer_cache/pixel_format.h +++ b/src/video_core/rasterizer_cache/pixel_format.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -30,13 +30,20 @@ enum class PixelFormat : u32 { A4 = 11, ETC1 = 12, ETC1A4 = 13, + D16 = 14, D24 = 16, D24S8 = 17, + MaxPixelFormat = 18, + + NumColorFormat = (ETC1A4 - RGBA8) + 1, + NumDepthFormat = (D24S8 - D16) + 1, + Invalid = std::numeric_limits::max(), }; constexpr std::size_t PIXEL_FORMAT_COUNT = static_cast(PixelFormat::MaxPixelFormat); +DECLARE_ENUM_ARITHMETIC_OPERATORS(PixelFormat) enum class SurfaceType : u32 { Color = 0, diff --git a/src/video_core/renderer_vulkan/vk_blit_helper.cpp b/src/video_core/renderer_vulkan/vk_blit_helper.cpp index 0a7a3be44..3052082f7 100644 --- a/src/video_core/renderer_vulkan/vk_blit_helper.cpp +++ b/src/video_core/renderer_vulkan/vk_blit_helper.cpp @@ -1,7 +1,9 @@ -// Copyright 2022 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include "common/hash.h" +#include "common/settings.h" #include "common/vector_math.h" #include "video_core/renderer_vulkan/vk_blit_helper.h" #include "video_core/renderer_vulkan/vk_descriptor_update_queue.h" @@ -16,8 +18,19 @@ #include "video_core/host_shaders/vulkan_blit_depth_stencil_frag.h" #include "video_core/host_shaders/vulkan_depth_to_buffer_comp.h" +// Texture filtering shader includes +#include "video_core/host_shaders/texture_filtering/bicubic_frag.h" +#include "video_core/host_shaders/texture_filtering/mmpx_frag.h" +#include "video_core/host_shaders/texture_filtering/refine_frag.h" +#include "video_core/host_shaders/texture_filtering/scale_force_frag.h" +#include "video_core/host_shaders/texture_filtering/x_gradient_frag.h" +#include "video_core/host_shaders/texture_filtering/xbrz_freescale_frag.h" +#include "video_core/host_shaders/texture_filtering/y_gradient_frag.h" +#include "vk_blit_helper.h" + namespace Vulkan { +using Settings::TextureFilter; using VideoCore::PixelFormat; namespace { @@ -55,8 +68,33 @@ constexpr std::array TWO_TEXTURES_BINDINGS = {1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}, }}; +// Texture filtering descriptor set bindings +constexpr std::array SINGLE_TEXTURE_BINDINGS = {{ + {0, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}, +}}; + +constexpr std::array THREE_TEXTURES_BINDINGS = {{ + {0, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}, + {1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}, + {2, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}, +}}; + +// Note: Removed FILTER_UTILITY_BINDINGS as texture filtering doesn't need shadow buffers + +// Push constant structure for texture filtering +struct FilterPushConstants { + std::array tex_scale; + std::array tex_offset; + float res_scale; // For xBRZ filter +}; + +inline constexpr vk::PushConstantRange FILTER_PUSH_CONSTANT_RANGE{ + .stageFlags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, + .offset = 0, + .size = sizeof(FilterPushConstants), +}; inline constexpr vk::PushConstantRange PUSH_CONSTANT_RANGE{ - .stageFlags = vk::ShaderStageFlagBits::eVertex, + .stageFlags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, .offset = 0, .size = sizeof(PushConstants), }; @@ -104,12 +142,17 @@ constexpr vk::PipelineDynamicStateCreateInfo PIPELINE_DYNAMIC_STATE_CREATE_INFO{ .dynamicStateCount = static_cast(DYNAMIC_STATES.size()), .pDynamicStates = DYNAMIC_STATES.data(), }; -constexpr vk::PipelineColorBlendStateCreateInfo PIPELINE_COLOR_BLEND_STATE_EMPTY_CREATE_INFO{ + +constexpr vk::PipelineColorBlendAttachmentState COLOR_BLEND_ATTACHMENT{ + .blendEnable = VK_FALSE, + .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | + vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA, +}; + +constexpr vk::PipelineColorBlendStateCreateInfo PIPELINE_COLOR_BLEND_STATE_CREATE_INFO{ .logicOpEnable = VK_FALSE, - .logicOp = vk::LogicOp::eClear, - .attachmentCount = 0, - .pAttachments = nullptr, - .blendConstants = std::array{0.0f, 0.0f, 0.0f, 0.0f}, + .attachmentCount = 1, + .pAttachments = &COLOR_BLEND_ATTACHMENT, }; constexpr vk::PipelineDepthStencilStateCreateInfo PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO{ .depthTestEnable = VK_TRUE, @@ -128,9 +171,9 @@ inline constexpr vk::SamplerCreateInfo SAMPLER_CREATE_INFO{ .magFilter = filter, .minFilter = filter, .mipmapMode = vk::SamplerMipmapMode::eNearest, - .addressModeU = vk::SamplerAddressMode::eClampToBorder, - .addressModeV = vk::SamplerAddressMode::eClampToBorder, - .addressModeW = vk::SamplerAddressMode::eClampToBorder, + .addressModeU = vk::SamplerAddressMode::eClampToEdge, + .addressModeV = vk::SamplerAddressMode::eClampToEdge, + .addressModeW = vk::SamplerAddressMode::eClampToEdge, .mipLodBias = 0.0f, .anisotropyEnable = VK_FALSE, .maxAnisotropy = 0.0f, @@ -143,12 +186,14 @@ inline constexpr vk::SamplerCreateInfo SAMPLER_CREATE_INFO{ }; constexpr vk::PipelineLayoutCreateInfo PipelineLayoutCreateInfo( - const vk::DescriptorSetLayout* set_layout, bool compute = false) { + const vk::DescriptorSetLayout* set_layout, bool compute = false, bool filter = false) { return vk::PipelineLayoutCreateInfo{ .setLayoutCount = 1, .pSetLayouts = set_layout, .pushConstantRangeCount = 1, - .pPushConstantRanges = (compute ? &COMPUTE_PUSH_CONSTANT_RANGE : &PUSH_CONSTANT_RANGE), + .pPushConstantRanges = + (compute ? &COMPUTE_PUSH_CONSTANT_RANGE + : (filter ? &FILTER_PUSH_CONSTANT_RANGE : &PUSH_CONSTANT_RANGE)), }; } @@ -185,12 +230,20 @@ BlitHelper::BlitHelper(const Instance& instance_, Scheduler& scheduler_, compute_provider{instance, scheduler.GetMasterSemaphore(), COMPUTE_BINDINGS}, compute_buffer_provider{instance, scheduler.GetMasterSemaphore(), COMPUTE_BUFFER_BINDINGS}, two_textures_provider{instance, scheduler.GetMasterSemaphore(), TWO_TEXTURES_BINDINGS, 16}, + single_texture_provider{instance, scheduler.GetMasterSemaphore(), SINGLE_TEXTURE_BINDINGS, + 16}, + three_textures_provider{instance, scheduler.GetMasterSemaphore(), THREE_TEXTURES_BINDINGS, + 16}, compute_pipeline_layout{ device.createPipelineLayout(PipelineLayoutCreateInfo(&compute_provider.Layout(), true))}, compute_buffer_pipeline_layout{device.createPipelineLayout( PipelineLayoutCreateInfo(&compute_buffer_provider.Layout(), true))}, two_textures_pipeline_layout{ device.createPipelineLayout(PipelineLayoutCreateInfo(&two_textures_provider.Layout()))}, + single_texture_pipeline_layout{device.createPipelineLayout( + PipelineLayoutCreateInfo(&single_texture_provider.Layout(), false, true))}, + three_textures_pipeline_layout{device.createPipelineLayout( + PipelineLayoutCreateInfo(&three_textures_provider.Layout(), false, true))}, full_screen_vert{Compile(HostShaders::FULL_SCREEN_TRIANGLE_VERT, vk::ShaderStageFlagBits::eVertex, device)}, d24s8_to_rgba8_comp{Compile(HostShaders::VULKAN_D24S8_TO_RGBA8_COMP, @@ -199,6 +252,14 @@ BlitHelper::BlitHelper(const Instance& instance_, Scheduler& scheduler_, vk::ShaderStageFlagBits::eCompute, device)}, blit_depth_stencil_frag{Compile(HostShaders::VULKAN_BLIT_DEPTH_STENCIL_FRAG, vk::ShaderStageFlagBits::eFragment, device)}, + // Texture filtering shader modules + bicubic_frag{Compile(HostShaders::BICUBIC_FRAG, vk::ShaderStageFlagBits::eFragment, device)}, + scale_force_frag{ + Compile(HostShaders::SCALE_FORCE_FRAG, vk::ShaderStageFlagBits::eFragment, device)}, + xbrz_frag{ + Compile(HostShaders::XBRZ_FREESCALE_FRAG, vk::ShaderStageFlagBits::eFragment, device)}, + mmpx_frag{Compile(HostShaders::MMPX_FRAG, vk::ShaderStageFlagBits::eFragment, device)}, + refine_frag{Compile(HostShaders::REFINE_FRAG, vk::ShaderStageFlagBits::eFragment, device)}, d24s8_to_rgba8_pipeline{MakeComputePipeline(d24s8_to_rgba8_comp, compute_pipeline_layout)}, depth_to_buffer_pipeline{ MakeComputePipeline(depth_to_buffer_comp, compute_buffer_pipeline_layout)}, @@ -212,6 +273,10 @@ BlitHelper::BlitHelper(const Instance& instance_, Scheduler& scheduler_, "BlitHelper: compute_buffer_pipeline_layout"); SetObjectName(device, two_textures_pipeline_layout, "BlitHelper: two_textures_pipeline_layout"); + SetObjectName(device, single_texture_pipeline_layout, + "BlitHelper: single_texture_pipeline_layout"); + SetObjectName(device, three_textures_pipeline_layout, + "BlitHelper: three_textures_pipeline_layout"); SetObjectName(device, full_screen_vert, "BlitHelper: full_screen_vert"); SetObjectName(device, d24s8_to_rgba8_comp, "BlitHelper: d24s8_to_rgba8_comp"); SetObjectName(device, depth_to_buffer_comp, "BlitHelper: depth_to_buffer_comp"); @@ -227,13 +292,25 @@ BlitHelper::BlitHelper(const Instance& instance_, Scheduler& scheduler_, } BlitHelper::~BlitHelper() { + for (const auto& [_, pipeline] : filter_pipeline_cache) { + device.destroyPipeline(pipeline); + } + filter_pipeline_cache.clear(); device.destroyPipelineLayout(compute_pipeline_layout); device.destroyPipelineLayout(compute_buffer_pipeline_layout); device.destroyPipelineLayout(two_textures_pipeline_layout); + device.destroyPipelineLayout(single_texture_pipeline_layout); + device.destroyPipelineLayout(three_textures_pipeline_layout); device.destroyShaderModule(full_screen_vert); device.destroyShaderModule(d24s8_to_rgba8_comp); device.destroyShaderModule(depth_to_buffer_comp); device.destroyShaderModule(blit_depth_stencil_frag); + // Destroy texture filtering shader modules + device.destroyShaderModule(bicubic_frag); + device.destroyShaderModule(scale_force_frag); + device.destroyShaderModule(xbrz_frag); + device.destroyShaderModule(mmpx_frag); + device.destroyShaderModule(refine_frag); device.destroyPipeline(depth_to_buffer_pipeline); device.destroyPipeline(d24s8_to_rgba8_pipeline); device.destroyPipeline(depth_blit_pipeline); @@ -242,7 +319,7 @@ BlitHelper::~BlitHelper() { } void BindBlitState(vk::CommandBuffer cmdbuf, vk::PipelineLayout layout, - const VideoCore::TextureBlit& blit) { + const VideoCore::TextureBlit& blit, const Surface& dest) { const vk::Offset2D offset{ .x = std::min(blit.dst_rect.left, blit.dst_rect.right), .y = std::min(blit.dst_rect.bottom, blit.dst_rect.top), @@ -272,8 +349,9 @@ void BindBlitState(vk::CommandBuffer cmdbuf, vk::PipelineLayout layout, }; cmdbuf.setViewport(0, viewport); cmdbuf.setScissor(0, scissor); - cmdbuf.pushConstants(layout, vk::ShaderStageFlagBits::eVertex, 0, sizeof(push_constants), - &push_constants); + cmdbuf.pushConstants(layout, + vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, 0, + sizeof(push_constants), &push_constants); } bool BlitHelper::BlitDepthStencil(Surface& source, Surface& dest, @@ -300,12 +378,12 @@ bool BlitHelper::BlitDepthStencil(Surface& source, Surface& dest, }; renderpass_cache.BeginRendering(depth_pass); - scheduler.Record([blit, descriptor_set, this](vk::CommandBuffer cmdbuf) { + scheduler.Record([blit, descriptor_set, &dest, this](vk::CommandBuffer cmdbuf) { const vk::PipelineLayout layout = two_textures_pipeline_layout; cmdbuf.bindPipeline(vk::PipelineBindPoint::eGraphics, depth_blit_pipeline); cmdbuf.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, layout, 0, descriptor_set, {}); - BindBlitState(cmdbuf, layout, blit); + BindBlitState(cmdbuf, layout, blit, dest); cmdbuf.draw(3, 1, 0, 0); }); scheduler.MakeDirty(StateFlags::Pipeline); @@ -531,7 +609,7 @@ vk::Pipeline BlitHelper::MakeDepthStencilBlitPipeline() { .pRasterizationState = &PIPELINE_RASTERIZATION_STATE_CREATE_INFO, .pMultisampleState = &PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, .pDepthStencilState = &PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO, - .pColorBlendState = &PIPELINE_COLOR_BLEND_STATE_EMPTY_CREATE_INFO, + .pColorBlendState = &PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, .pDynamicState = &PIPELINE_DYNAMIC_STATE_CREATE_INFO, .layout = two_textures_pipeline_layout, .renderPass = renderpass, @@ -547,4 +625,275 @@ vk::Pipeline BlitHelper::MakeDepthStencilBlitPipeline() { return VK_NULL_HANDLE; } +bool BlitHelper::Filter(Surface& surface, const VideoCore::TextureBlit& blit) { + const auto filter = Settings::values.texture_filter.GetValue(); + if (filter == Settings::TextureFilter::NoFilter) { + return false; + } + if (blit.src_level != 0) { + return true; + } + + switch (filter) { + case TextureFilter::Anime4K: + FilterAnime4K(surface, blit); + break; + case TextureFilter::Bicubic: + FilterBicubic(surface, blit); + break; + case TextureFilter::ScaleForce: + FilterScaleForce(surface, blit); + break; + case TextureFilter::xBRZ: + FilterXbrz(surface, blit); + break; + case TextureFilter::MMPX: + FilterMMPX(surface, blit); + break; + default: + LOG_ERROR(Render_Vulkan, "Unknown texture filter {}", filter); + return false; + } + return true; +} + +void BlitHelper::FilterAnime4K(Surface& surface, const VideoCore::TextureBlit& blit) { + auto pipeline = + MakeFilterPipeline(refine_frag, three_textures_pipeline_layout, surface.pixel_format); + FilterPassThreeTextures(surface, pipeline, three_textures_pipeline_layout, blit); +} + +void BlitHelper::FilterBicubic(Surface& surface, const VideoCore::TextureBlit& blit) { + auto pipeline = + MakeFilterPipeline(bicubic_frag, single_texture_pipeline_layout, surface.pixel_format); + FilterPass(surface, pipeline, single_texture_pipeline_layout, blit); +} + +void BlitHelper::FilterScaleForce(Surface& surface, const VideoCore::TextureBlit& blit) { + auto pipeline = + MakeFilterPipeline(scale_force_frag, single_texture_pipeline_layout, surface.pixel_format); + FilterPass(surface, pipeline, single_texture_pipeline_layout, blit); +} + +void BlitHelper::FilterXbrz(Surface& surface, const VideoCore::TextureBlit& blit) { + auto pipeline = + MakeFilterPipeline(xbrz_frag, single_texture_pipeline_layout, surface.pixel_format); + FilterPass(surface, pipeline, single_texture_pipeline_layout, blit); +} + +void BlitHelper::FilterMMPX(Surface& surface, const VideoCore::TextureBlit& blit) { + auto pipeline = + MakeFilterPipeline(mmpx_frag, single_texture_pipeline_layout, surface.pixel_format); + FilterPass(surface, pipeline, single_texture_pipeline_layout, blit); +} + +vk::Pipeline BlitHelper::MakeFilterPipeline(vk::ShaderModule fragment_shader, + vk::PipelineLayout layout, + VideoCore::PixelFormat color_format) { + + const VkShaderModule c_shader = static_cast(fragment_shader); + const VkPipelineLayout c_layout = static_cast(layout); + const u64 cache_key = Common::HashCombine( + Common::HashCombine(static_cast(reinterpret_cast(c_shader)), + static_cast(reinterpret_cast(c_layout))), + static_cast(color_format)); + + if (const auto it = filter_pipeline_cache.find(cache_key); it != filter_pipeline_cache.end()) { + return it->second; + } + + const std::array stages = MakeStages(full_screen_vert, fragment_shader); + // Use the provided color format for render pass compatibility + const auto renderpass = + renderpass_cache.GetRenderpass(color_format, VideoCore::PixelFormat::Invalid, false); + + vk::GraphicsPipelineCreateInfo pipeline_info = { + .stageCount = static_cast(stages.size()), + .pStages = stages.data(), + .pVertexInputState = &PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, + .pInputAssemblyState = &PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, + .pTessellationState = nullptr, + .pViewportState = &PIPELINE_VIEWPORT_STATE_CREATE_INFO, + .pRasterizationState = &PIPELINE_RASTERIZATION_STATE_CREATE_INFO, + .pMultisampleState = &PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, + .pDepthStencilState = nullptr, + .pColorBlendState = &PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, + .pDynamicState = &PIPELINE_DYNAMIC_STATE_CREATE_INFO, + .layout = layout, + .renderPass = renderpass, + }; + + if (const auto result = device.createGraphicsPipeline({}, pipeline_info); + result.result == vk::Result::eSuccess) { + const vk::Pipeline pipeline = result.value; + filter_pipeline_cache.emplace(cache_key, pipeline); + return pipeline; + } else { + LOG_CRITICAL(Render_Vulkan, "Filter pipeline creation failed!"); + UNREACHABLE(); + } +} + +void BlitHelper::FilterPass(Surface& surface, vk::Pipeline pipeline, vk::PipelineLayout layout, + const VideoCore::TextureBlit& blit) { + const auto texture_descriptor_set = single_texture_provider.Commit(); + update_queue.AddImageSampler(texture_descriptor_set, 0, 0, + surface.ImageView(ViewType::Sample, Type::Base), linear_sampler, + vk::ImageLayout::eGeneral); + + const auto renderpass = renderpass_cache.GetRenderpass(surface.pixel_format, + VideoCore::PixelFormat::Invalid, false); + + const RenderPass render_pass = { + .framebuffer = surface.Framebuffer(), + .render_pass = renderpass, + .render_area = + { + .offset = {0, 0}, + .extent = {surface.GetScaledWidth(), surface.GetScaledHeight()}, + }, + }; + renderpass_cache.BeginRendering(render_pass); + const float src_scale = static_cast(surface.GetResScale()); + // Calculate normalized texture coordinates like OpenGL does + const auto src_extent = surface.RealExtent(false); // Get unscaled texture extent + const float tex_scale_x = + static_cast(blit.src_rect.GetWidth()) / static_cast(src_extent.width); + const float tex_scale_y = + static_cast(blit.src_rect.GetHeight()) / static_cast(src_extent.height); + const float tex_offset_x = + static_cast(blit.src_rect.left) / static_cast(src_extent.width); + const float tex_offset_y = + static_cast(blit.src_rect.bottom) / static_cast(src_extent.height); + + scheduler.Record([pipeline, layout, texture_descriptor_set, blit, tex_scale_x, tex_scale_y, + tex_offset_x, tex_offset_y, src_scale](vk::CommandBuffer cmdbuf) { + const FilterPushConstants push_constants{.tex_scale = {tex_scale_x, tex_scale_y}, + .tex_offset = {tex_offset_x, tex_offset_y}, + .res_scale = src_scale}; + + cmdbuf.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline); + + // Bind single texture descriptor set + cmdbuf.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, layout, 0, + texture_descriptor_set, {}); + + cmdbuf.pushConstants(layout, FILTER_PUSH_CONSTANT_RANGE.stageFlags, + FILTER_PUSH_CONSTANT_RANGE.offset, FILTER_PUSH_CONSTANT_RANGE.size, + &push_constants); + + // Set up viewport and scissor for filtering (don't use BindBlitState as it overwrites push + // constants) + const vk::Offset2D offset{ + .x = std::min(blit.dst_rect.left, blit.dst_rect.right), + .y = std::min(blit.dst_rect.bottom, blit.dst_rect.top), + }; + const vk::Extent2D extent{ + .width = blit.dst_rect.GetWidth(), + .height = blit.dst_rect.GetHeight(), + }; + const vk::Viewport viewport{ + .x = static_cast(offset.x), + .y = static_cast(offset.y), + .width = static_cast(extent.width), + .height = static_cast(extent.height), + .minDepth = 0.0f, + .maxDepth = 1.0f, + }; + const vk::Rect2D scissor{ + .offset = offset, + .extent = extent, + }; + cmdbuf.setViewport(0, viewport); + cmdbuf.setScissor(0, scissor); + cmdbuf.draw(3, 1, 0, 0); + }); + scheduler.MakeDirty(StateFlags::Pipeline); +} + +void BlitHelper::FilterPassThreeTextures(Surface& surface, vk::Pipeline pipeline, + vk::PipelineLayout layout, + const VideoCore::TextureBlit& blit) { + const auto texture_descriptor_set = three_textures_provider.Commit(); + + update_queue.AddImageSampler(texture_descriptor_set, 0, 0, + surface.ImageView(ViewType::Sample, Type::Base), linear_sampler, + vk::ImageLayout::eGeneral); + update_queue.AddImageSampler(texture_descriptor_set, 1, 0, + surface.ImageView(ViewType::Sample, Type::Base), linear_sampler, + vk::ImageLayout::eGeneral); + update_queue.AddImageSampler(texture_descriptor_set, 2, 0, + surface.ImageView(ViewType::Sample, Type::Base), linear_sampler, + vk::ImageLayout::eGeneral); + + const auto renderpass = renderpass_cache.GetRenderpass(surface.pixel_format, + VideoCore::PixelFormat::Invalid, false); + + const RenderPass render_pass = { + .framebuffer = surface.Framebuffer(), + .render_pass = renderpass, + .render_area = + { + .offset = {0, 0}, + .extent = {surface.GetScaledWidth(), surface.GetScaledHeight()}, + }, + }; + renderpass_cache.BeginRendering(render_pass); + + const float src_scale = static_cast(surface.GetResScale()); + // Calculate normalized texture coordinates like OpenGL does + const auto src_extent = surface.RealExtent(false); // Get unscaled texture extent + const float tex_scale_x = + static_cast(blit.src_rect.GetWidth()) / static_cast(src_extent.width); + const float tex_scale_y = + static_cast(blit.src_rect.GetHeight()) / static_cast(src_extent.height); + const float tex_offset_x = + static_cast(blit.src_rect.left) / static_cast(src_extent.width); + const float tex_offset_y = + static_cast(blit.src_rect.bottom) / static_cast(src_extent.height); + + scheduler.Record([pipeline, layout, texture_descriptor_set, blit, tex_scale_x, tex_scale_y, + tex_offset_x, tex_offset_y, src_scale](vk::CommandBuffer cmdbuf) { + const FilterPushConstants push_constants{.tex_scale = {tex_scale_x, tex_scale_y}, + .tex_offset = {tex_offset_x, tex_offset_y}, + .res_scale = src_scale}; + + cmdbuf.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline); + + // Bind single texture descriptor set + cmdbuf.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, layout, 0, + texture_descriptor_set, {}); + + cmdbuf.pushConstants(layout, FILTER_PUSH_CONSTANT_RANGE.stageFlags, + FILTER_PUSH_CONSTANT_RANGE.offset, FILTER_PUSH_CONSTANT_RANGE.size, + &push_constants); + + // Set up viewport and scissor using safe viewport like working filters + const vk::Offset2D offset{ + .x = std::min(blit.dst_rect.left, blit.dst_rect.right), + .y = std::min(blit.dst_rect.bottom, blit.dst_rect.top), + }; + const vk::Extent2D extent{ + .width = blit.dst_rect.GetWidth(), + .height = blit.dst_rect.GetHeight(), + }; + const vk::Viewport viewport{ + .x = static_cast(offset.x), + .y = static_cast(offset.y), + .width = static_cast(extent.width), + .height = static_cast(extent.height), + .minDepth = 0.0f, + .maxDepth = 1.0f, + }; + const vk::Rect2D scissor{ + .offset = offset, + .extent = extent, + }; + cmdbuf.setViewport(0, viewport); + cmdbuf.setScissor(0, scissor); + cmdbuf.draw(3, 1, 0, 0); + }); + scheduler.MakeDirty(StateFlags::Pipeline); +} + } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_blit_helper.h b/src/video_core/renderer_vulkan/vk_blit_helper.h index d9b5c7760..59aee655f 100644 --- a/src/video_core/renderer_vulkan/vk_blit_helper.h +++ b/src/video_core/renderer_vulkan/vk_blit_helper.h @@ -1,9 +1,12 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once +#include + +#include "video_core/rasterizer_cache/pixel_format.h" #include "video_core/renderer_vulkan/vk_resource_pool.h" namespace VideoCore { @@ -27,6 +30,7 @@ public: explicit BlitHelper(const Instance& instance, Scheduler& scheduler, RenderManager& renderpass_cache, DescriptorUpdateQueue& update_queue); ~BlitHelper(); + bool Filter(Surface& surface, const VideoCore::TextureBlit& blit); bool BlitDepthStencil(Surface& source, Surface& dest, const VideoCore::TextureBlit& blit); @@ -38,6 +42,24 @@ public: private: vk::Pipeline MakeComputePipeline(vk::ShaderModule shader, vk::PipelineLayout layout); vk::Pipeline MakeDepthStencilBlitPipeline(); + vk::Pipeline MakeFilterPipeline( + vk::ShaderModule fragment_shader, vk::PipelineLayout layout, + VideoCore::PixelFormat color_format = VideoCore::PixelFormat::RGBA8); + + void FilterAnime4K(Surface& surface, const VideoCore::TextureBlit& blit); + void FilterBicubic(Surface& surface, const VideoCore::TextureBlit& blit); + void FilterScaleForce(Surface& surface, const VideoCore::TextureBlit& blit); + void FilterXbrz(Surface& surface, const VideoCore::TextureBlit& blit); + void FilterMMPX(Surface& surface, const VideoCore::TextureBlit& blit); + + void FilterPass(Surface& surface, vk::Pipeline pipeline, vk::PipelineLayout layout, + const VideoCore::TextureBlit& blit); + + void FilterPassThreeTextures(Surface& surface, vk::Pipeline pipeline, vk::PipelineLayout layout, + const VideoCore::TextureBlit& blit); + + void FilterPassYGradient(Surface& surface, vk::Pipeline pipeline, vk::PipelineLayout layout, + const VideoCore::TextureBlit& blit); private: const Instance& instance; @@ -51,20 +73,32 @@ private: DescriptorHeap compute_provider; DescriptorHeap compute_buffer_provider; DescriptorHeap two_textures_provider; + DescriptorHeap single_texture_provider; + DescriptorHeap three_textures_provider; vk::PipelineLayout compute_pipeline_layout; vk::PipelineLayout compute_buffer_pipeline_layout; vk::PipelineLayout two_textures_pipeline_layout; + vk::PipelineLayout single_texture_pipeline_layout; + vk::PipelineLayout three_textures_pipeline_layout; vk::ShaderModule full_screen_vert; vk::ShaderModule d24s8_to_rgba8_comp; vk::ShaderModule depth_to_buffer_comp; vk::ShaderModule blit_depth_stencil_frag; + vk::ShaderModule bicubic_frag; + vk::ShaderModule scale_force_frag; + vk::ShaderModule xbrz_frag; + vk::ShaderModule mmpx_frag; + vk::ShaderModule refine_frag; vk::Pipeline d24s8_to_rgba8_pipeline; vk::Pipeline depth_to_buffer_pipeline; vk::Pipeline depth_blit_pipeline; vk::Sampler linear_sampler; vk::Sampler nearest_sampler; + + /// Cache of texture filter pipelines (keyed by shader+layout+format hash) + std::unordered_map filter_pipeline_cache; }; } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 7a27032e9..abab77c4e 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -634,7 +634,7 @@ void RasterizerVulkan::SyncTextureUnits(const Framebuffer* framebuffer) { // If the texture unit is disabled bind a null surface to it if (!texture.enabled) { - const Surface& null_surface = res_cache.GetSurface(VideoCore::NULL_SURFACE_ID); + Surface& null_surface = res_cache.GetSurface(VideoCore::NULL_SURFACE_ID); const Sampler& null_sampler = res_cache.GetSampler(VideoCore::NULL_SAMPLER_ID); update_queue.AddImageSampler(texture_set, texture_index, 0, null_surface.ImageView(), null_sampler.Handle()); @@ -669,7 +669,7 @@ void RasterizerVulkan::SyncTextureUnits(const Framebuffer* framebuffer) { Surface& surface = res_cache.GetTextureSurface(texture); Sampler& sampler = res_cache.GetSampler(texture.config); const vk::ImageView color_view = framebuffer->ImageView(SurfaceType::Color); - const bool is_feedback_loop = color_view == surface.ImageView(); + const bool is_feedback_loop = color_view == surface.FramebufferView(); const vk::ImageView texture_view = is_feedback_loop ? surface.CopyImageView() : surface.ImageView(); update_queue.AddImageSampler(texture_set, texture_index, 0, texture_view, sampler.Handle()); @@ -785,7 +785,7 @@ bool RasterizerVulkan::AccelerateDisplay(const Pica::FramebufferConfig& config, return false; } - const Surface& src_surface = res_cache.GetSurface(src_surface_id); + Surface& src_surface = res_cache.GetSurface(src_surface_id); const u32 scaled_width = src_surface.GetScaledWidth(); const u32 scaled_height = src_surface.GetScaledHeight(); diff --git a/src/video_core/renderer_vulkan/vk_render_manager.cpp b/src/video_core/renderer_vulkan/vk_render_manager.cpp index 7b986e6b2..b3615e1ae 100644 --- a/src/video_core/renderer_vulkan/vk_render_manager.cpp +++ b/src/video_core/renderer_vulkan/vk_render_manager.cpp @@ -42,6 +42,7 @@ void RenderManager::BeginRendering(const Framebuffer* framebuffer, }; images = framebuffer->Images(); aspects = framebuffer->Aspects(); + shadow_rendering = framebuffer->shadow_rendering; BeginRendering(new_pass); } @@ -71,9 +72,11 @@ void RenderManager::EndRendering() { return; } - scheduler.Record([images = images, aspects = aspects](vk::CommandBuffer cmdbuf) { + scheduler.Record([images = images, aspects = aspects, + shadow_rendering = shadow_rendering](vk::CommandBuffer cmdbuf) { u32 num_barriers = 0; vk::PipelineStageFlags pipeline_flags{}; + vk::AccessFlags src_access_flags{}; std::array barriers; for (u32 i = 0; i < images.size(); i++) { if (!images[i]) { @@ -81,14 +84,18 @@ void RenderManager::EndRendering() { } const bool is_color = static_cast(aspects[i] & vk::ImageAspectFlagBits::eColor); if (is_color) { - pipeline_flags |= vk::PipelineStageFlagBits::eColorAttachmentOutput; + pipeline_flags |= shadow_rendering + ? vk::PipelineStageFlagBits::eFragmentShader + : vk::PipelineStageFlagBits::eColorAttachmentOutput; + src_access_flags = shadow_rendering ? vk::AccessFlagBits::eShaderWrite + : vk::AccessFlagBits::eColorAttachmentWrite; } else { pipeline_flags |= vk::PipelineStageFlagBits::eEarlyFragmentTests | vk::PipelineStageFlagBits::eLateFragmentTests; + src_access_flags = vk::AccessFlagBits::eDepthStencilAttachmentWrite; } barriers[num_barriers++] = vk::ImageMemoryBarrier{ - .srcAccessMask = is_color ? vk::AccessFlagBits::eColorAttachmentWrite - : vk::AccessFlagBits::eDepthStencilAttachmentWrite, + .srcAccessMask = src_access_flags, .dstAccessMask = vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eTransferRead, .oldLayout = vk::ImageLayout::eGeneral, @@ -120,6 +127,7 @@ void RenderManager::EndRendering() { pass.render_pass = VK_NULL_HANDLE; images = {}; aspects = {}; + shadow_rendering = false; // The Mali guide recommends flushing at the end of each major renderpass // Testing has shown this has a significant effect on rendering performance @@ -136,7 +144,9 @@ vk::RenderPass RenderManager::GetRenderpass(VideoCore::PixelFormat color, const u32 color_index = color == VideoCore::PixelFormat::Invalid ? NumColorFormats : static_cast(color); const u32 depth_index = - depth == VideoCore::PixelFormat::Invalid ? NumDepthFormats : (static_cast(depth) - 14); + depth == VideoCore::PixelFormat::Invalid + ? NumDepthFormats + : (static_cast(depth - VideoCore::PixelFormat::NumColorFormat)); ASSERT_MSG(color_index <= NumColorFormats && depth_index <= NumDepthFormats, "Invalid color index {} and/or depth_index {}", color_index, depth_index); diff --git a/src/video_core/renderer_vulkan/vk_render_manager.h b/src/video_core/renderer_vulkan/vk_render_manager.h index 9d8a8fdff..3ebbd817b 100644 --- a/src/video_core/renderer_vulkan/vk_render_manager.h +++ b/src/video_core/renderer_vulkan/vk_render_manager.h @@ -1,4 +1,4 @@ -// Copyright 2024 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -35,8 +35,8 @@ struct RenderPass { }; class RenderManager { - static constexpr u32 NumColorFormats = 13; - static constexpr u32 NumDepthFormats = 4; + static constexpr u32 NumColorFormats = static_cast(VideoCore::PixelFormat::NumColorFormat); + static constexpr u32 NumDepthFormats = static_cast(VideoCore::PixelFormat::NumDepthFormat); public: explicit RenderManager(const Instance& instance, Scheduler& scheduler); @@ -67,6 +67,7 @@ private: std::mutex cache_mutex; std::array images; std::array aspects; + bool shadow_rendering{}; RenderPass pass{}; u32 num_draws{}; }; diff --git a/src/video_core/renderer_vulkan/vk_resource_pool.cpp b/src/video_core/renderer_vulkan/vk_resource_pool.cpp index 03b644ea2..7dbe90ebd 100644 --- a/src/video_core/renderer_vulkan/vk_resource_pool.cpp +++ b/src/video_core/renderer_vulkan/vk_resource_pool.cpp @@ -107,13 +107,14 @@ vk::CommandBuffer CommandPool::Commit() { return cmd_buffers[index]; } -constexpr u32 DESCRIPTOR_SET_BATCH = 32; +constexpr u32 DESCRIPTOR_SET_BATCH = 64; +constexpr u32 DESCRIPTOR_MULTIPLIER = 4; // Increase capacity of each pool DescriptorHeap::DescriptorHeap(const Instance& instance, MasterSemaphore* master_semaphore, std::span bindings, u32 descriptor_heap_count_) : ResourcePool{master_semaphore, DESCRIPTOR_SET_BATCH}, device{instance.GetDevice()}, - descriptor_heap_count{descriptor_heap_count_} { + descriptor_heap_count{descriptor_heap_count_ * DESCRIPTOR_MULTIPLIER} { // Increase pool size // Create descriptor set layout. const vk::DescriptorSetLayoutCreateInfo layout_ci = { .bindingCount = static_cast(bindings.size()), diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index cfcb199a1..86f46787e 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -2,8 +2,20 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include "video_core/renderer_vulkan/vk_texture_runtime.h" + +#include +#include +#include #include #include +#include +#include "video_core/custom_textures/custom_tex_manager.h" +#include "video_core/rasterizer_cache/pixel_format.h" +#include "video_core/rasterizer_cache/surface_params.h" +#include "video_core/renderer_vulkan/vk_blit_helper.h" +#include "video_core/renderer_vulkan/vk_descriptor_update_queue.h" +#include "video_core/renderer_vulkan/vk_stream_buffer.h" #include "common/literals.h" #include "common/microprofile.h" @@ -20,12 +32,6 @@ #include #include -// Ignore the -Wclass-memaccess warning on memcpy for non-trivially default constructible objects. -#if defined(__GNUC__) && !defined(__clang__) && !defined(__INTEL_COMPILER) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wclass-memaccess" -#endif - MICROPROFILE_DEFINE(Vulkan_ImageAlloc, "Vulkan", "Texture Allocation", MP_RGB(192, 52, 235)); namespace Vulkan { @@ -118,18 +124,18 @@ u32 UnpackDepthStencil(const VideoCore::StagingData& data, vk::Format dest) { return depth_offset; } -boost::container::small_vector MakeInitBarriers( - vk::ImageAspectFlags aspect, std::span images) { - boost::container::small_vector barriers; - for (const vk::Image& image : images) { - barriers.push_back(vk::ImageMemoryBarrier{ +void MakeInitBarriers(vk::ImageAspectFlags aspect, u32 num_images, + std::span images, + std::span out_barriers) { + for (u32 i = 0; i < num_images; i++) { + out_barriers[i] = vk::ImageMemoryBarrier{ .srcAccessMask = vk::AccessFlagBits::eNone, .dstAccessMask = vk::AccessFlagBits::eNone, .oldLayout = vk::ImageLayout::eUndefined, .newLayout = vk::ImageLayout::eGeneral, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = image, + .image = images[i], .subresourceRange{ .aspectMask = aspect, .baseMipLevel = 0, @@ -137,19 +143,38 @@ boost::container::small_vector MakeInitBarriers( .baseArrayLayer = 0, .layerCount = VK_REMAINING_ARRAY_LAYERS, }, - }); + }; } - return barriers; } -Handle MakeHandle(const Instance* instance, u32 width, u32 height, u32 levels, TextureType type, - vk::Format format, vk::ImageUsageFlags usage, vk::ImageCreateFlags flags, - vk::ImageAspectFlags aspect, bool need_format_list, - std::string_view debug_name = {}) { - // On tvOS/iOS, fall back to 2D textures when layered rendering isn't supported +vk::ImageSubresourceRange MakeSubresourceRange(vk::ImageAspectFlags aspect, u32 level = 0, + u32 levels = 1, u32 layer = 0) { + return vk::ImageSubresourceRange{ + .aspectMask = aspect, + .baseMipLevel = level, + .levelCount = levels, + .baseArrayLayer = layer, + .layerCount = VK_REMAINING_ARRAY_LAYERS, + }; +} + +constexpr u64 UPLOAD_BUFFER_SIZE = 512_MiB; +constexpr u64 DOWNLOAD_BUFFER_SIZE = 16_MiB; + +} // Anonymous namespace + +void Handle::Create(const Instance* instance, u32 width, u32 height, u32 levels, TextureType type, + vk::Format format, vk::ImageUsageFlags usage, vk::ImageCreateFlags flags, + vk::ImageAspectFlags aspect, bool need_format_list, + std::string_view debug_name) { const bool is_cube_map = type == TextureType::CubeMap && instance->IsLayeredRenderingSupported(); - const u32 layers = is_cube_map ? 6 : 1; + + this->instance = instance; + this->width = width; + this->height = height; + this->levels = levels; + this->layers = is_cube_map ? 6 : 1; const std::array format_list = { vk::Format::eR8G8B8A8Unorm, @@ -183,7 +208,6 @@ Handle MakeHandle(const Instance* instance, u32 width, u32 height, u32 levels, T VkImage unsafe_image{}; VkImageCreateInfo unsafe_image_info = static_cast(image_info); - VmaAllocation allocation{}; VkResult result = vmaCreateImage(instance->GetAllocator(), &unsafe_image_info, &alloc_info, &unsafe_image, &allocation, nullptr); @@ -192,7 +216,8 @@ Handle MakeHandle(const Instance* instance, u32 width, u32 height, u32 levels, T UNREACHABLE(); } - const vk::Image image{unsafe_image}; + image = vk::Image{unsafe_image}; + const vk::ImageViewCreateInfo view_info = { .image = image, .viewType = is_cube_map ? vk::ImageViewType::eCube : vk::ImageViewType::e2D, @@ -200,55 +225,61 @@ Handle MakeHandle(const Instance* instance, u32 width, u32 height, u32 levels, T .subresourceRange{ .aspectMask = aspect, .baseMipLevel = 0, - .levelCount = levels, + .levelCount = VK_REMAINING_MIP_LEVELS, .baseArrayLayer = 0, - .layerCount = layers, + .layerCount = VK_REMAINING_ARRAY_LAYERS, }, }; - vk::UniqueImageView image_view = instance->GetDevice().createImageViewUnique(view_info); + image_views[ViewType::Sample] = instance->GetDevice().createImageView(view_info); + if (levels == 1) { + image_views[ViewType::Mip0] = image_views[ViewType::Mip0]; + } if (!debug_name.empty() && instance->HasDebuggingToolAttached()) { SetObjectName(instance->GetDevice(), image, debug_name); - SetObjectName(instance->GetDevice(), image_view.get(), "{} View({})", debug_name, - vk::to_string(aspect)); + SetObjectName(instance->GetDevice(), image_views[ViewType::Sample], "{} View({})", + debug_name, vk::to_string(aspect)); + } +} + +void Handle::Destroy() { + if (!allocation || !instance) { + return; } - return Handle{ - .alloc = allocation, - .image = image, - .image_view = std::move(image_view), - }; + const auto device = instance->GetDevice(); + const auto allocator = instance->GetAllocator(); + + // Image views + if (auto view = image_views[ViewType::Sample]) { + device.destroyImageView(view); + } + if (auto view = image_views[ViewType::Mip0]; view && view != image_views[ViewType::Sample]) { + device.destroyImageView(view); + } + if (auto view = image_views[ViewType::Storage]) { + device.destroyImageView(view); + } + if (auto view = image_views[ViewType::Depth]) { + device.destroyImageView(view); + } + if (auto view = image_views[ViewType::Stencil]) { + device.destroyImageView(view); + } + + image_views = {}; + + if (framebuffer) { + device.destroyFramebuffer(framebuffer); + framebuffer = VK_NULL_HANDLE; + } + + vmaDestroyImage(allocator, image, allocation); + + image = VK_NULL_HANDLE; + allocation = VK_NULL_HANDLE; } -vk::UniqueFramebuffer MakeFramebuffer(vk::Device device, vk::RenderPass render_pass, u32 width, - u32 height, std::span attachments) { - const vk::FramebufferCreateInfo framebuffer_info = { - .renderPass = render_pass, - .attachmentCount = static_cast(attachments.size()), - .pAttachments = attachments.data(), - .width = width, - .height = height, - .layers = 1, - }; - return device.createFramebufferUnique(framebuffer_info); -} - -vk::ImageSubresourceRange MakeSubresourceRange(vk::ImageAspectFlags aspect, u32 level = 0, - u32 levels = 1, u32 layer = 0) { - return vk::ImageSubresourceRange{ - .aspectMask = aspect, - .baseMipLevel = level, - .levelCount = levels, - .baseArrayLayer = layer, - .layerCount = VK_REMAINING_ARRAY_LAYERS, - }; -} - -constexpr u64 UPLOAD_BUFFER_SIZE = 512_MiB; -constexpr u64 DOWNLOAD_BUFFER_SIZE = 16_MiB; - -} // Anonymous namespace - TextureRuntime::TextureRuntime(const Instance& instance, Scheduler& scheduler, RenderManager& renderpass_cache, DescriptorUpdateQueue& update_queue, u32 num_swapchain_images_) @@ -702,7 +733,7 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param : SurfaceBase{params}, runtime{&runtime_}, instance{&runtime_.GetInstance()}, scheduler{&runtime_.GetScheduler()}, traits{instance->GetTraits(pixel_format)} { - if (pixel_format == VideoCore::PixelFormat::Invalid) { + if (pixel_format == VideoCore::PixelFormat::Invalid || !traits.transfer_support) { return; } @@ -712,7 +743,8 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param ASSERT_MSG(format != vk::Format::eUndefined && levels >= 1, "Image allocation parameters are invalid"); - boost::container::static_vector raw_images; + u32 num_images{}; + std::array raw_images; vk::ImageCreateFlags flags{}; if (texture_type == VideoCore::TextureType::CubeMap) { @@ -722,24 +754,35 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param flags |= vk::ImageCreateFlagBits::eMutableFormat; } - const bool need_format_list = is_mutable && instance->IsImageFormatListSupported(); - handles[0] = MakeHandle(instance, width, height, levels, texture_type, format, traits.usage, - flags, traits.aspect, need_format_list, DebugName(false)); - raw_images.emplace_back(handles[0].image); - - if (res_scale != 1) { - handles[1] = - MakeHandle(instance, GetScaledWidth(), GetScaledHeight(), levels, texture_type, format, - traits.usage, flags, traits.aspect, need_format_list, DebugName(true)); - raw_images.emplace_back(handles[1].image); + // Ensure color formats have the color attachment bit set for framebuffers + auto usage = traits.usage; + const bool is_color = + (traits.aspect & vk::ImageAspectFlagBits::eColor) != vk::ImageAspectFlags{}; + if (is_color) { + usage |= vk::ImageUsageFlagBits::eColorAttachment; } + const bool need_format_list = is_mutable && instance->IsImageFormatListSupported(); + handles[Type::Base].Create(instance, width, height, levels, texture_type, format, usage, flags, + traits.aspect, need_format_list, DebugName(false)); + raw_images[num_images++] = handles[Type::Base].image; + + if (res_scale != 1) { + handles[Type::Scaled].Create(instance, GetScaledWidth(), GetScaledHeight(), levels, + texture_type, format, usage, flags, traits.aspect, + need_format_list, DebugName(true)); + raw_images[num_images++] = handles[Type::Scaled].image; + } + + current = res_scale != 1 ? Type::Scaled : Type::Base; + runtime->renderpass_cache.EndRendering(); - scheduler->Record([raw_images, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { - const auto barriers = MakeInitBarriers(aspect, raw_images); - cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, - vk::PipelineStageFlagBits::eTopOfPipe, - vk::DependencyFlagBits::eByRegion, {}, {}, barriers); + scheduler->Record([raw_images, num_images, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { + std::array barriers; + MakeInitBarriers(aspect, num_images, raw_images, barriers); + cmdbuf.pipelineBarrier( + vk::PipelineStageFlagBits::eTopOfPipe, vk::PipelineStageFlagBits::eTopOfPipe, + vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, nullptr, num_images, barriers.data()); }); } @@ -754,7 +797,8 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceBase& surface const bool has_normal = mat && mat->Map(MapType::Normal); const vk::Format format = traits.native; - boost::container::static_vector raw_images; + u32 num_images{}; + std::array raw_images; vk::ImageCreateFlags flags{}; if (texture_type == VideoCore::TextureType::CubeMap) { @@ -762,48 +806,37 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceBase& surface } const std::string debug_name = DebugName(false, true); - handles[0] = MakeHandle(instance, mat->width, mat->height, levels, texture_type, format, - traits.usage, flags, traits.aspect, false, debug_name); - raw_images.emplace_back(handles[0].image); + handles[Type::Base].Create(instance, mat->width, mat->height, levels, texture_type, format, + traits.usage, flags, traits.aspect, false, debug_name); + raw_images[num_images++] = handles[Type::Base].image; if (res_scale != 1) { - handles[1] = MakeHandle(instance, mat->width, mat->height, levels, texture_type, - vk::Format::eR8G8B8A8Unorm, traits.usage, flags, traits.aspect, - false, debug_name); - raw_images.emplace_back(handles[1].image); + handles[Type::Scaled].Create(instance, mat->width, mat->height, levels, texture_type, + vk::Format::eR8G8B8A8Unorm, traits.usage, flags, traits.aspect, + false, debug_name); + raw_images[num_images++] = handles[Type::Scaled].image; } if (has_normal) { - handles[2] = MakeHandle(instance, mat->width, mat->height, levels, texture_type, format, - traits.usage, flags, traits.aspect, false, debug_name); - raw_images.emplace_back(handles[2].image); + handles[Type::Custom].Create(instance, mat->width, mat->height, levels, texture_type, + format, traits.usage, flags, traits.aspect, false, debug_name); + raw_images[num_images++] = handles[Type::Custom].image; } + current = res_scale != 1 ? Type::Scaled : Type::Base; + runtime->renderpass_cache.EndRendering(); - scheduler->Record([raw_images, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { - const auto barriers = MakeInitBarriers(aspect, raw_images); - cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, - vk::PipelineStageFlagBits::eTopOfPipe, - vk::DependencyFlagBits::eByRegion, {}, {}, barriers); + scheduler->Record([raw_images, num_images, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { + std::array barriers; + MakeInitBarriers(aspect, num_images, raw_images, barriers); + cmdbuf.pipelineBarrier( + vk::PipelineStageFlagBits::eTopOfPipe, vk::PipelineStageFlagBits::eTopOfPipe, + vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, nullptr, num_images, barriers.data()); }); custom_format = mat->format; material = mat; } -Surface::~Surface() { - if (!handles[0].image_view) { - return; - } - for (const auto& [alloc, image, image_view] : handles) { - if (image) { - vmaDestroyImage(instance->GetAllocator(), image, alloc); - } - } - if (copy_handle.image_view) { - vmaDestroyImage(instance->GetAllocator(), copy_handle.image, copy_handle.alloc); - } -} - void Surface::Upload(const VideoCore::BufferTextureCopy& upload, const VideoCore::StagingData& staging) { runtime->renderpass_cache.EndRendering(); @@ -812,7 +845,7 @@ void Surface::Upload(const VideoCore::BufferTextureCopy& upload, .aspect = Aspect(), .pipeline_flags = PipelineStageFlags(), .src_access = AccessFlags(), - .src_image = Image(0), + .src_image = Image(Type::Base), }; scheduler->Record([buffer = runtime->upload_buffer.Handle(), format = traits.native, params, @@ -878,14 +911,17 @@ void Surface::Upload(const VideoCore::BufferTextureCopy& upload, runtime->upload_buffer.Commit(staging.size); if (res_scale != 1) { + ASSERT_MSG(handles[Type::Scaled], "Scaled allocation missing during upload"); + const VideoCore::TextureBlit blit = { .src_level = upload.texture_level, .dst_level = upload.texture_level, .src_rect = upload.texture_rect, .dst_rect = upload.texture_rect * res_scale, }; - - BlitScale(blit, true); + if (type != SurfaceType::Texture || !runtime->blit_helper.Filter(*this, blit)) { + BlitScale(blit, true); + } } } @@ -895,13 +931,13 @@ void Surface::UploadCustom(const VideoCore::Material* material, u32 level) { const auto color = material->textures[0]; const Common::Rectangle rect{0U, height, width, 0U}; - const auto upload = [&](u32 index, VideoCore::CustomTexture* texture) { + const auto upload = [&](Type type, VideoCore::CustomTexture* texture) { const u32 custom_size = static_cast(texture->data.size()); const RecordParams params = { .aspect = vk::ImageAspectFlagBits::eColor, .pipeline_flags = PipelineStageFlags(), .src_access = AccessFlags(), - .src_image = Image(index), + .src_image = Image(type), }; const auto [data, offset, invalidate] = runtime->upload_buffer.Map(custom_size, 0); @@ -956,14 +992,9 @@ void Surface::UploadCustom(const VideoCore::Material* material, u32 level) { }); }; - upload(0, color); - - for (u32 i = 1; i < VideoCore::MAX_MAPS; i++) { - const auto texture = material->textures[i]; - if (!texture) { - continue; - } - upload(i + 1, texture); + upload(Type::Base, color); + if (auto* texture = material->textures[u32(MapType::Normal)]) { + upload(Type::Custom, texture); } } @@ -996,7 +1027,7 @@ void Surface::Download(const VideoCore::BufferTextureCopy& download, .aspect = Aspect(), .pipeline_flags = PipelineStageFlags(), .src_access = AccessFlags(), - .src_image = Image(0), + .src_image = Image(Type::Base), }; scheduler->Record( @@ -1070,14 +1101,16 @@ void Surface::ScaleUp(u32 new_scale) { flags |= vk::ImageCreateFlagBits::eMutableFormat; } - handles[1] = - MakeHandle(instance, GetScaledWidth(), GetScaledHeight(), levels, texture_type, - traits.native, traits.usage, flags, traits.aspect, false, DebugName(true)); + handles[Type::Scaled].Create(instance, GetScaledWidth(), GetScaledHeight(), levels, + texture_type, traits.native, traits.usage, flags, traits.aspect, + false, DebugName(true)); + current = Type::Scaled; runtime->renderpass_cache.EndRendering(); scheduler->Record( [raw_images = std::array{Image()}, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { - const auto barriers = MakeInitBarriers(aspect, raw_images); + std::array barriers; + MakeInitBarriers(aspect, 1, raw_images, barriers); cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, vk::PipelineStageFlagBits::eTopOfPipe, vk::DependencyFlagBits::eByRegion, {}, {}, barriers); @@ -1131,24 +1164,16 @@ vk::PipelineStageFlags Surface::PipelineStageFlags() const noexcept { : vk::PipelineStageFlagBits::eNone); } -vk::Image Surface::Image(u32 index) const noexcept { - const vk::Image image = handles[index].image; - if (!image) { - return handles[0].image; - } - return image; -} - vk::ImageView Surface::CopyImageView() noexcept { + auto& copy_handle = handles[Type::Copy]; vk::ImageLayout copy_layout = vk::ImageLayout::eGeneral; - if (!copy_handle.image) { + if (!copy_handle) { vk::ImageCreateFlags flags{}; if (texture_type == VideoCore::TextureType::CubeMap) { flags |= vk::ImageCreateFlagBits::eCubeCompatible; } - copy_handle = - MakeHandle(instance, GetScaledWidth(), GetScaledHeight(), levels, texture_type, - traits.native, traits.usage, flags, traits.aspect, false); + copy_handle.Create(instance, GetScaledWidth(), GetScaledHeight(), levels, texture_type, + traits.native, traits.usage, flags, traits.aspect, false); copy_layout = vk::ImageLayout::eUndefined; } @@ -1240,123 +1265,81 @@ vk::ImageView Surface::CopyImageView() noexcept { vk::DependencyFlagBits::eByRegion, {}, {}, post_barriers); }); - return copy_handle.image_view.get(); + return copy_handle.image_views[ViewType::Sample]; } -vk::ImageView Surface::ImageView(u32 index) const noexcept { - const auto& image_view = handles[index].image_view.get(); - if (!image_view) { - return handles[0].image_view.get(); +vk::ImageView Surface::ImageView(ViewType view_type, Type type) noexcept { + auto& handle = handles[type == Type::Current ? current : type]; + if (auto image_view = handle.image_views[view_type]) { + return image_view; } - return image_view; -} -vk::ImageView Surface::FramebufferView() noexcept { - is_framebuffer = true; - return ImageView(); -} + auto aspect = traits.aspect; -vk::ImageView Surface::DepthView() noexcept { - if (depth_view) { - return depth_view.get(); + if (view_type == ViewType::Storage) { + ASSERT(pixel_format == PixelFormat::RGBA8); + is_storage = true; + } + if (view_type == ViewType::Depth || view_type == ViewType::Stencil) { + ASSERT(this->type == SurfaceType::DepthStencil); + aspect = view_type == ViewType::Depth ? vk::ImageAspectFlagBits::eDepth + : vk::ImageAspectFlagBits::eStencil; } const vk::ImageViewCreateInfo view_info = { - .image = Image(), + .image = handle.image, .viewType = vk::ImageViewType::e2D, - .format = instance->GetTraits(pixel_format).native, + .format = view_type == ViewType::Storage ? vk::Format::eR32Uint : traits.native, .subresourceRange{ - .aspectMask = vk::ImageAspectFlagBits::eDepth, + .aspectMask = aspect, .baseMipLevel = 0, - .levelCount = VK_REMAINING_MIP_LEVELS, + .levelCount = (view_type == ViewType::Mip0 || view_type == ViewType::Storage) + ? 1u + : VK_REMAINING_MIP_LEVELS, .baseArrayLayer = 0, .layerCount = VK_REMAINING_ARRAY_LAYERS, }, }; - - depth_view = instance->GetDevice().createImageViewUnique(view_info); - return depth_view.get(); + handle.image_views[view_type] = instance->GetDevice().createImageView(view_info); + return handle.image_views[view_type]; } -vk::ImageView Surface::StencilView() noexcept { - if (stencil_view) { - return stencil_view.get(); +vk::Framebuffer Surface::Framebuffer(Type type) noexcept { + auto& handle = handles[type == Type::Current ? current : type]; + if (handle.framebuffer) { + return handle.framebuffer; } - const vk::ImageViewCreateInfo view_info = { - .image = Image(), - .viewType = vk::ImageViewType::e2D, - .format = instance->GetTraits(pixel_format).native, - .subresourceRange{ - .aspectMask = vk::ImageAspectFlagBits::eStencil, - .baseMipLevel = 0, - .levelCount = VK_REMAINING_MIP_LEVELS, - .baseArrayLayer = 0, - .layerCount = VK_REMAINING_ARRAY_LAYERS, - }, - }; - - stencil_view = instance->GetDevice().createImageViewUnique(view_info); - return stencil_view.get(); -} - -vk::ImageView Surface::StorageView() noexcept { - if (storage_view) { - return storage_view.get(); - } - - if (pixel_format != VideoCore::PixelFormat::RGBA8) { - LOG_WARNING(Render_Vulkan, - "Attempted to retrieve storage view from unsupported surface with format {}", - VideoCore::PixelFormatAsString(pixel_format)); - return ImageView(); - } - - is_storage = true; - - const vk::ImageViewCreateInfo storage_view_info = { - .image = Image(), - .viewType = vk::ImageViewType::e2D, - .format = vk::Format::eR32Uint, - .subresourceRange{ - .aspectMask = vk::ImageAspectFlagBits::eColor, - .baseMipLevel = 0, - .levelCount = VK_REMAINING_MIP_LEVELS, - .baseArrayLayer = 0, - .layerCount = VK_REMAINING_ARRAY_LAYERS, - }, - }; - storage_view = instance->GetDevice().createImageViewUnique(storage_view_info); - return storage_view.get(); -} - -vk::Framebuffer Surface::Framebuffer() noexcept { - const u32 index = res_scale == 1 ? 0u : 1u; - if (framebuffers[index]) { - return framebuffers[index].get(); - } - - const bool is_depth = type == SurfaceType::Depth || type == SurfaceType::DepthStencil; + const bool is_depth = + this->type == SurfaceType::Depth || this->type == SurfaceType::DepthStencil; const auto color_format = is_depth ? PixelFormat::Invalid : pixel_format; const auto depth_format = is_depth ? pixel_format : PixelFormat::Invalid; - const auto render_pass = - runtime->renderpass_cache.GetRenderpass(color_format, depth_format, false); - const auto attachments = std::array{ImageView()}; - framebuffers[index] = MakeFramebuffer(instance->GetDevice(), render_pass, GetScaledWidth(), - GetScaledHeight(), attachments); - return framebuffers[index].get(); + + const auto image_view = ImageView(ViewType::Mip0, type); + const vk::FramebufferCreateInfo framebuffer_info = { + .renderPass = runtime->renderpass_cache.GetRenderpass(color_format, depth_format, false), + .attachmentCount = 1u, + .pAttachments = &image_view, + .width = handle.width, + .height = handle.height, + .layers = handle.layers, + }; + handle.framebuffer = instance->GetDevice().createFramebuffer(framebuffer_info); + return handle.framebuffer; } void Surface::BlitScale(const VideoCore::TextureBlit& blit, bool up_scale) { - const FormatTraits& depth_traits = instance->GetTraits(pixel_format); const bool is_depth_stencil = pixel_format == PixelFormat::D24S8; - if (is_depth_stencil && !depth_traits.blit_support) { + if (is_depth_stencil && !traits.blit_support) { LOG_WARNING(Render_Vulkan, "Depth scale unsupported by hardware"); return; } - scheduler->Record([src_image = Image(!up_scale), aspect = Aspect(), - filter = MakeFilter(pixel_format), dst_image = Image(up_scale), + const auto src_type = up_scale ? Type::Base : Type::Scaled; + const auto dst_type = up_scale ? Type::Scaled : Type::Base; + + scheduler->Record([src_image = Image(src_type), aspect = Aspect(), + filter = MakeFilter(pixel_format), dst_image = Image(dst_type), blit](vk::CommandBuffer render_cmdbuf) { const std::array source_offsets = { vk::Offset3D{static_cast(blit.src_rect.left), @@ -1461,40 +1444,49 @@ Framebuffer::Framebuffer(TextureRuntime& runtime, const VideoCore::FramebufferPa width = height = std::numeric_limits::max(); + u32 num_attachments{}; + std::array attachments; + const auto prepare = [&](u32 index, Surface* surface) { - const VideoCore::Extent extent = surface->RealExtent(); + const auto extent = surface->RealExtent(); width = std::min(width, extent.width); height = std::min(height, extent.height); - if (!shadow_rendering) { - formats[index] = surface->pixel_format; - } + formats[index] = surface->pixel_format; images[index] = surface->Image(); aspects[index] = surface->Aspect(); - image_views[index] = shadow_rendering ? surface->StorageView() : surface->FramebufferView(); + image_views[index] = surface->FramebufferView(); }; - boost::container::static_vector attachments; - - if (color) { - prepare(0, color); - attachments.emplace_back(image_views[0]); - } - - if (depth) { - prepare(1, depth); - attachments.emplace_back(image_views[1]); - } - - const vk::Device device = runtime.GetInstance().GetDevice(); if (shadow_rendering) { + const auto extent = color->RealExtent(); + width = extent.width; + height = extent.height; render_pass = renderpass_cache.GetRenderpass(PixelFormat::Invalid, PixelFormat::Invalid, false); - framebuffer = MakeFramebuffer(device, render_pass, color->GetScaledWidth(), - color->GetScaledHeight(), {}); + images[0] = color->Image(); + image_views[0] = color->StorageView(); + aspects[0] = vk::ImageAspectFlagBits::eColor; } else { + if (color) { + prepare(0, color); + attachments[num_attachments++] = image_views[0]; + } + if (depth) { + prepare(1, depth); + attachments[num_attachments++] = image_views[1]; + } render_pass = renderpass_cache.GetRenderpass(formats[0], formats[1], false); - framebuffer = MakeFramebuffer(device, render_pass, width, height, attachments); } + + const vk::FramebufferCreateInfo framebuffer_info = { + .renderPass = render_pass, + .attachmentCount = num_attachments, + .pAttachments = attachments.data(), + .width = width, + .height = height, + .layers = 1, + }; + framebuffer = runtime.GetInstance().GetDevice().createFramebuffer(framebuffer_info); } Framebuffer::~Framebuffer() = default; diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.h b/src/video_core/renderer_vulkan/vk_texture_runtime.h index e1745b22b..bb5b5ce91 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.h +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.h @@ -1,10 +1,9 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once -#include #include #include "video_core/rasterizer_cache/framebuffer_base.h" #include "video_core/rasterizer_cache/rasterizer_cache_base.h" @@ -26,10 +25,76 @@ class RenderManager; class Surface; class DescriptorUpdateQueue; +enum Type { + Current = -1, + Base = 0, + Scaled, + Custom, + Copy, + Num, +}; + +enum ViewType { + Sample = 0, + Mip0, + Storage, + Depth, + Stencil, + Max, +}; + struct Handle { - VmaAllocation alloc; - vk::Image image; - vk::UniqueImageView image_view; + explicit Handle() = default; + + ~Handle() { + Destroy(); + } + + Handle(Handle&& other) noexcept + : allocation(std::exchange(other.allocation, VK_NULL_HANDLE)), + image(std::exchange(other.image, VK_NULL_HANDLE)), + image_views(std::exchange(other.image_views, {})), + framebuffer(std::exchange(other.framebuffer, VK_NULL_HANDLE)), + width(std::exchange(other.width, 0)), height(std::exchange(other.height, 0)), + levels(std::exchange(other.levels, 0)), layers(std::exchange(other.layers, 0)) {} + + Handle& operator=(Handle&& other) noexcept { + if (this == &other) + return *this; + + allocation = std::exchange(other.allocation, VK_NULL_HANDLE); + image = std::exchange(other.image, VK_NULL_HANDLE); + image_views = std::exchange(other.image_views, {}); + framebuffer = std::exchange(other.framebuffer, VK_NULL_HANDLE); + width = std::exchange(other.width, 0); + height = std::exchange(other.height, 0); + levels = std::exchange(other.levels, 0); + layers = std::exchange(other.layers, 0); + + return *this; + } + + void Create(const Instance* instance, u32 width, u32 height, u32 levels, + VideoCore::TextureType type, vk::Format format, vk::ImageUsageFlags usage, + vk::ImageCreateFlags flags, vk::ImageAspectFlags aspect, bool need_format_list, + std::string_view debug_name = {}); + + void Destroy(); + + operator bool() const { + return allocation; + } + + const Instance* instance{nullptr}; + + VmaAllocation allocation{VK_NULL_HANDLE}; + vk::Image image{VK_NULL_HANDLE}; + std::array image_views{}; + vk::Framebuffer framebuffer{VK_NULL_HANDLE}; + u32 width{}; + u32 height{}; + u32 levels{}; + u32 layers{}; }; /** @@ -110,7 +175,6 @@ public: explicit Surface(TextureRuntime& runtime, const VideoCore::SurfaceParams& params); explicit Surface(TextureRuntime& runtime, const VideoCore::SurfaceBase& surface, const VideoCore::Material* materal); - ~Surface(); Surface(const Surface&) = delete; Surface& operator=(const Surface&) = delete; @@ -123,28 +187,56 @@ public: } /// Returns the image at index, otherwise the base image - vk::Image Image(u32 index = 1) const noexcept; + vk::Image Image(Type type = Type::Current) const noexcept { + return handles[type == Type::Current ? current : type].image; + } /// Returns the image view at index, otherwise the base view - vk::ImageView ImageView(u32 index = 1) const noexcept; + vk::ImageView ImageView(ViewType view_type = ViewType::Sample, + Type type = Type::Current) noexcept; + + /// Returns a framebuffer handle for rendering to this surface + vk::Framebuffer Framebuffer(Type type = Type::Current) noexcept; + + /// Returns width of the surface + u32 GetWidth() const noexcept { + return width; + } + + /// Returns height of the surface + u32 GetHeight() const noexcept { + return height; + } + + /// Returns resolution scale of the surface + u32 GetResScale() const noexcept { + return res_scale; + } /// Returns a copy of the upscaled image handle, used for feedback loops. vk::ImageView CopyImageView() noexcept; /// Returns the framebuffer view of the surface image - vk::ImageView FramebufferView() noexcept; + vk::ImageView FramebufferView() noexcept { + is_framebuffer = true; + return ImageView(ViewType::Mip0); + } /// Returns the depth view of the surface image - vk::ImageView DepthView() noexcept; + vk::ImageView DepthView() noexcept { + return ImageView(ViewType::Depth); + } /// Returns the stencil view of the surface image - vk::ImageView StencilView() noexcept; + vk::ImageView StencilView() noexcept { + return ImageView(ViewType::Stencil); + } - /// Returns the R32 image view used for atomic load/store - vk::ImageView StorageView() noexcept; - - /// Returns a framebuffer handle for rendering to this surface - vk::Framebuffer Framebuffer() noexcept; + /// Returns the R32 image view used for atomic load/store. + vk::ImageView StorageView() noexcept { + is_storage = true; + return ImageView(ViewType::Storage); + } /// Uploads pixel data in staging to a rectangle region of the surface texture void Upload(const VideoCore::BufferTextureCopy& upload, const VideoCore::StagingData& staging); @@ -181,9 +273,8 @@ public: const Instance* instance; Scheduler* scheduler; FormatTraits traits; - std::array handles{}; - std::array framebuffers{}; - Handle copy_handle; + std::array handles; + Type current{}; vk::UniqueImageView depth_view; vk::UniqueImageView stencil_view; vk::UniqueImageView storage_view; @@ -212,7 +303,7 @@ public: } [[nodiscard]] vk::Framebuffer Handle() const noexcept { - return framebuffer.get(); + return framebuffer; } [[nodiscard]] std::array Images() const noexcept { @@ -231,24 +322,17 @@ public: return res_scale; } - u32 Width() const noexcept { - return width; - } - - u32 Height() const noexcept { - return height; - } - private: std::array images{}; std::array image_views{}; - vk::UniqueFramebuffer framebuffer; + vk::Framebuffer framebuffer; vk::RenderPass render_pass; + std::vector framebuffer_views; std::array aspects{}; std::array formats{VideoCore::PixelFormat::Invalid, VideoCore::PixelFormat::Invalid}; - u32 width{}; - u32 height{}; + u32 width; + u32 height; u32 res_scale{1}; }; From c55165e19b8f015bf754bfd32b71b2794cfe9436 Mon Sep 17 00:00:00 2001 From: Chase Harkcom Date: Fri, 20 Feb 2026 15:40:39 -0700 Subject: [PATCH 07/42] Fix segfault when resetting default settings (#1751) --- src/citra_qt/configuration/configure_general.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/citra_qt/configuration/configure_general.cpp b/src/citra_qt/configuration/configure_general.cpp index ab8eb2a3c..ca548b21b 100644 --- a/src/citra_qt/configuration/configure_general.cpp +++ b/src/citra_qt/configuration/configure_general.cpp @@ -163,7 +163,7 @@ void ConfigureGeneral::ResetDefaults() { FileUtil::Delete(FileUtil::GetUserPath(FileUtil::UserPath::ConfigDir) + "qt-config.ini"); FileUtil::DeleteDirRecursively(FileUtil::GetUserPath(FileUtil::UserPath::ConfigDir) + "custom"); - std::exit(0); + qApp->quit(); } void ConfigureGeneral::ApplyConfiguration() { From ac0ec5edea2ff2d8c5343308ed0fccfec8944876 Mon Sep 17 00:00:00 2001 From: OpenSauce04 Date: Sat, 21 Feb 2026 17:17:12 +0000 Subject: [PATCH 08/42] Updated translations via Transifex --- dist/languages/fr.ts | 18 +++++++++--------- dist/languages/it.ts | 4 ++-- dist/languages/pt_BR.ts | 4 ++-- .../src/main/res/values-b+pt+BR/strings.xml | 6 ++++++ .../app/src/main/res/values-fr/strings.xml | 2 ++ .../app/src/main/res/values-it/strings.xml | 4 ++++ 6 files changed, 25 insertions(+), 13 deletions(-) diff --git a/dist/languages/fr.ts b/dist/languages/fr.ts index d8a6179f8..e35d9e806 100644 --- a/dist/languages/fr.ts +++ b/dist/languages/fr.ts @@ -329,7 +329,7 @@ Cela bannira à la fois son nom du forum et son adresse IP. <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> - + <html><head/><body><p>Cet effet de post-traitement ajuste la vitesse audio pour correspondre à la vitesse d'émulation et aide à prévenir les distorsions. Cela augmente cependant la latence du son.</p></body></html> @@ -339,7 +339,7 @@ Cela bannira à la fois son nom du forum et son adresse IP. <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> - + <html><head/><body><p>Adapte la vitesse de lecture de l'audio pour tenir compte des baisses de fréquence d'images de l'émulation. Cela signifie que l'audio sera lu à pleine vitesse même si la fréquence d'images de l'application est faible. Peut entraîner des problèmes de désynchronisation de l'audio.</p></body></html> @@ -479,7 +479,7 @@ Cela bannira à la fois son nom du forum et son adresse IP. <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> - + <html><head/><body><p>Choisissez la provenance de l'image de la caméra émulée. Elle peut être une image ou une vraie caméra.</p></body></html> @@ -1495,7 +1495,7 @@ Souhaitez vous ignorer l'erreur et poursuivre ? <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> - + <html><head/><body><p>VSync empêche les effets de déchirement de l'image, mais elle réduira la performance de certaines cartes graphiques. Laissez-la activée si vous ne constatez pas de différence.</p></body></html> @@ -1505,12 +1505,12 @@ Souhaitez vous ignorer l'erreur et poursuivre ? <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> - + <html><head/><body><p>Si activée, cette option détecte lorsque le taux de rafraichissement de l'écran est inférieur à celui de la 3DS, et si c'est le cas, elle désactive automatiquement la VSync pour éviter que la vitesse d'émulation soit contrainte d'être en dessous de 100%.</p></body></html> Enable display refresh rate detection - + Activer la détection du taux de rafraichissement de l'écran @@ -2532,7 +2532,7 @@ Souhaitez vous ignorer l'erreur et poursuivre ? <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> - + <html><head/><body><p>Compresse le contenu des fichiers CIA lorsqu'ils sont installés sur la carte SD émulée. N'affecte que le contenu CIA installé lorsque le paramètre est activé.</p></body></html> @@ -5196,7 +5196,7 @@ Souhaitez-vous la télécharger ? Delete Vulkan Shader Cache - + Supprimer le cache de shader Vulkan @@ -6082,7 +6082,7 @@ Message de débogage : Loading %3 %1 / %2 - + Chargement %3 %1 / %2 diff --git a/dist/languages/it.ts b/dist/languages/it.ts index 0df8a2c12..f42aabf76 100644 --- a/dist/languages/it.ts +++ b/dist/languages/it.ts @@ -5194,7 +5194,7 @@ Vuoi installarlo? Delete Vulkan Shader Cache - + Elimina la cache degli shader Vulkan @@ -6080,7 +6080,7 @@ Messaggio di debug: Loading %3 %1 / %2 - + Caricamento %3 %1 / %2 diff --git a/dist/languages/pt_BR.ts b/dist/languages/pt_BR.ts index 8c78ab5d9..ff289fb02 100644 --- a/dist/languages/pt_BR.ts +++ b/dist/languages/pt_BR.ts @@ -5195,7 +5195,7 @@ Você gostaria de baixá-la? Delete Vulkan Shader Cache - + Excluir Cache de Shaders Vulkan @@ -6081,7 +6081,7 @@ Mensagem de depuração: Loading %3 %1 / %2 - + Carregando %3 %1 / %2 diff --git a/src/android/app/src/main/res/values-b+pt+BR/strings.xml b/src/android/app/src/main/res/values-b+pt+BR/strings.xml index f963b9ad0..1f5f874af 100644 --- a/src/android/app/src/main/res/values-b+pt+BR/strings.xml +++ b/src/android/app/src/main/res/values-b+pt+BR/strings.xml @@ -118,6 +118,8 @@ Alguns controles podem não ser capazes de mapear os D-pads para um eixo. Se esse for o caso, use a seção de D-Pad (Botões). D-Pad (Botão) Só mapeie o D-pad para isso se você se você estiver encontrando problemas com o mapeamento de botão do D-Pad (Eixo). + Eixo Vertical + Eixo Horizontal Cima Baixo Esquerda @@ -126,6 +128,8 @@ Pressione ou mova uma entrada. Mapeamento de controles Pressione ou mova um botão/alavanca para mapear para %1$s. + Pressione para CIMA no seu joystick. + Pressione para a DIREITA no seu joystick. Menu Principal Trocar telas Turbo @@ -546,6 +550,8 @@ Preparando Shaders + Construindo %s + Jogar Desinstalar Aplicativo diff --git a/src/android/app/src/main/res/values-fr/strings.xml b/src/android/app/src/main/res/values-fr/strings.xml index 95a18358a..154add85d 100644 --- a/src/android/app/src/main/res/values-fr/strings.xml +++ b/src/android/app/src/main/res/values-fr/strings.xml @@ -550,6 +550,8 @@ Préparation des shaders + Construction %s + Jouer Désinstaller l\'application diff --git a/src/android/app/src/main/res/values-it/strings.xml b/src/android/app/src/main/res/values-it/strings.xml index 7c3f37af6..84b7b803a 100644 --- a/src/android/app/src/main/res/values-it/strings.xml +++ b/src/android/app/src/main/res/values-it/strings.xml @@ -133,6 +133,8 @@ Divertiti usando l\'emulatore! Premi o sposta un comando Assegnazione Input Premi o muovi un comando per assegnarlo a %1$s. + Premi UP sul tuo controller. + Premi RIGHT sul tuo controller. Home Inverti schermi Turbo @@ -553,6 +555,8 @@ Divertiti usando l\'emulatore! Preparazione degli shader + Compilazione %s + Riproduci Disinstalla applicazione From 43cecd1692f09aa25d527987f07aabda68ee63d4 Mon Sep 17 00:00:00 2001 From: lannoene <77375172+lannoene@users.noreply.github.com> Date: Sun, 22 Feb 2026 08:07:24 -0800 Subject: [PATCH 09/42] Update File Core and Add HLE DLP Client (#1741) --- src/core/CMakeLists.txt | 5 + src/core/file_sys/ncch_container.cpp | 10 +- src/core/hle/service/am/am.cpp | 5 + src/core/hle/service/am/am.h | 3 + src/core/hle/service/apt/apt.cpp | 4 +- src/core/hle/service/dlp/dlp.cpp | 2 +- src/core/hle/service/dlp/dlp.h | 2 +- src/core/hle/service/dlp/dlp_base.cpp | 251 +++++++ src/core/hle/service/dlp/dlp_base.h | 387 ++++++++++ src/core/hle/service/dlp/dlp_clnt.cpp | 165 ++++- src/core/hle/service/dlp/dlp_clnt.h | 27 +- src/core/hle/service/dlp/dlp_clt_base.cpp | 856 ++++++++++++++++++++++ src/core/hle/service/dlp/dlp_clt_base.h | 144 ++++ src/core/hle/service/dlp/dlp_crypto.cpp | 31 + src/core/hle/service/dlp/dlp_fkcl.cpp | 84 ++- src/core/hle/service/dlp/dlp_fkcl.h | 15 +- src/core/hle/service/dlp/dlp_srvr.cpp | 37 +- src/core/hle/service/dlp/dlp_srvr.h | 10 +- src/core/hle/service/http/http_c.cpp | 3 +- src/core/hle/service/nwm/nwm_uds.cpp | 596 +++++++++------ src/core/hle/service/nwm/nwm_uds.h | 55 +- src/core/hle/service/nwm/uds_data.cpp | 2 +- src/core/hle/service/nwm/uds_data.h | 2 +- src/core/hw/aes/key.cpp | 14 +- src/core/hw/aes/key.h | 4 +- src/core/hw/default_keys.h | 383 +++++----- src/core/loader/ncch.cpp | 17 +- 27 files changed, 2617 insertions(+), 497 deletions(-) create mode 100644 src/core/hle/service/dlp/dlp_base.cpp create mode 100644 src/core/hle/service/dlp/dlp_base.h create mode 100644 src/core/hle/service/dlp/dlp_clt_base.cpp create mode 100644 src/core/hle/service/dlp/dlp_clt_base.h create mode 100644 src/core/hle/service/dlp/dlp_crypto.cpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index f069ff33d..a5abbc585 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -293,6 +293,11 @@ add_library(citra_core STATIC hle/service/dlp/dlp_fkcl.h hle/service/dlp/dlp_srvr.cpp hle/service/dlp/dlp_srvr.h + hle/service/dlp/dlp_clt_base.cpp + hle/service/dlp/dlp_clt_base.h + hle/service/dlp/dlp_base.cpp + hle/service/dlp/dlp_base.h + hle/service/dlp/dlp_crypto.cpp hle/service/dsp/dsp_dsp.cpp hle/service/dsp/dsp_dsp.h hle/service/err/err_f.cpp diff --git a/src/core/file_sys/ncch_container.cpp b/src/core/file_sys/ncch_container.cpp index 1438a766a..382c8943e 100644 --- a/src/core/file_sys/ncch_container.cpp +++ b/src/core/file_sys/ncch_container.cpp @@ -138,7 +138,7 @@ Loader::ResultStatus NCCHContainer::LoadHeader() { return Loader::ResultStatus::Success; } - if (!file->IsOpen()) { + if (!file || !file->IsOpen()) { return Loader::ResultStatus::Error; } @@ -205,6 +205,9 @@ Loader::ResultStatus NCCHContainer::Load() { if (is_loaded) return Loader::ResultStatus::Success; + if (!file) + return Loader::ResultStatus::Error; + int block_size = kBlockSize; if (file->IsOpen()) { @@ -621,7 +624,7 @@ Loader::ResultStatus NCCHContainer::ReadRomFS(std::shared_ptr& romf return Loader::ResultStatus::ErrorNotUsed; } - if (!file->IsOpen()) + if (!file || !file->IsOpen()) return Loader::ResultStatus::Error; u32 romfs_offset = ncch_offset + (ncch_header.romfs_offset * block_size) + 0x1000; @@ -767,6 +770,9 @@ bool NCCHContainer::HasExHeader() { std::unique_ptr NCCHContainer::Reopen( const std::unique_ptr& orig_file, const std::string& new_filename) { + if (!orig_file) + return nullptr; + const bool is_compressed = orig_file->IsCompressed(); const bool is_crypto = orig_file->IsCrypto(); const std::string filename = new_filename.empty() ? orig_file->Filename() : new_filename; diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index 645a015b6..af2739590 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -417,6 +417,10 @@ void AuthorizeCIAFileDecryption(CIAFile* cia_file, Kernel::HLERequestContext& ct } } +void CIAFile::AuthorizeDecryptionFromHLE() { + decryption_authorized = true; +} + CIAFile::CIAFile(Core::System& system_, Service::FS::MediaType media_type, bool from_cdn_) : system(system_), from_cdn(from_cdn_), decryption_authorized(false), media_type(media_type), decryption_state(std::make_unique()) { @@ -873,6 +877,7 @@ bool CIAFile::Close() { // Only delete the content folder as there may be user save data in the title folder. const std::string title_content_path = GetTitlePath(media_type, container.GetTitleMetadata().GetTitleID()) + "content/"; + current_content_file.reset(); FileUtil::DeleteDirRecursively(title_content_path); } return true; diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h index 4dec69e80..96258ecf9 100644 --- a/src/core/hle/service/am/am.h +++ b/src/core/hle/service/am/am.h @@ -233,8 +233,11 @@ public: return install_results; } + void AuthorizeDecryptionFromHLE(); + private: friend void AuthorizeCIAFileDecryption(CIAFile* cia_file, Kernel::HLERequestContext& ctx); + Core::System& system; // Sections (tik, tmd, contents) are being imported individually diff --git a/src/core/hle/service/apt/apt.cpp b/src/core/hle/service/apt/apt.cpp index fcc9b2a89..7b2dfeb19 100644 --- a/src/core/hle/service/apt/apt.cpp +++ b/src/core/hle/service/apt/apt.cpp @@ -39,6 +39,8 @@ SERVICE_CONSTRUCT_IMPL(Service::APT::Module) namespace Service::APT { +constexpr u32 max_wireless_reboot_info_size = 0x10; + template void Module::serialize(Archive& ar, const unsigned int file_version) { DEBUG_SERIALIZATION_POINT; @@ -64,7 +66,7 @@ std::shared_ptr Module::NSInterface::GetModule() const { void Module::NSInterface::SetWirelessRebootInfo(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - const auto size = rp.Pop(); + const auto size = std::min(rp.Pop(), max_wireless_reboot_info_size); const auto buffer = rp.PopStaticBuffer(); apt->wireless_reboot_info = std::move(buffer); diff --git a/src/core/hle/service/dlp/dlp.cpp b/src/core/hle/service/dlp/dlp.cpp index edbd22321..c0565d330 100644 --- a/src/core/hle/service/dlp/dlp.cpp +++ b/src/core/hle/service/dlp/dlp.cpp @@ -1,4 +1,4 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. diff --git a/src/core/hle/service/dlp/dlp.h b/src/core/hle/service/dlp/dlp.h index 28fb73718..ea34e83e2 100644 --- a/src/core/hle/service/dlp/dlp.h +++ b/src/core/hle/service/dlp/dlp.h @@ -1,4 +1,4 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. diff --git a/src/core/hle/service/dlp/dlp_base.cpp b/src/core/hle/service/dlp/dlp_base.cpp new file mode 100644 index 000000000..a2e22212f --- /dev/null +++ b/src/core/hle/service/dlp/dlp_base.cpp @@ -0,0 +1,251 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "dlp_base.h" + +#include +#include +#include "common/alignment.h" +#include "common/swap.h" +#include "common/timer.h" +#include "core/hle/ipc_helpers.h" +#include "core/hle/service/nwm/uds_data.h" +#include "core/hle/service/service.h" +#include "core/hw/aes/key.h" +#include "core/hw/unique_data.h" + +namespace Service::DLP { + +DLP_Base::DLP_Base(Core::System& s) : system(s) {} + +std::shared_ptr DLP_Base::GetCFG() { + return Service::CFG::GetModule(system); +} + +std::shared_ptr DLP_Base::GetUDS() { + return system.ServiceManager().GetService("nwm::UDS"); +} + +std::u16string DLP_Base::DLPUsernameAsString16(DLP_Username uname) { + std::u16string strUsername; + for (auto c : uname) { + strUsername.push_back(c); + } + return strUsername; +} + +DLP_Username DLP_Base::String16AsDLPUsername(std::u16string str) { + DLP_Username out{}; + u32 num_chars_copy = std::min(out.size(), str.size()); + memcpy(out.data(), str.data(), num_chars_copy * sizeof(u16_le)); + return out; +} + +DLPNodeInfo DLP_Base::UDSToDLPNodeInfo(NWM::NodeInfo node_info) { + DLPNodeInfo out{}; + out.username = node_info.username; + out.network_node_id = node_info.network_node_id; + out.friend_code_seed = node_info.friend_code_seed; + return out; +} + +void DLP_Base::GetEventDescription(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + LOG_WARNING(Service_DLP, "(STUBBED) called"); + + DLPEventDescription desc{}; + + IPC::RequestBuilder rb = rp.MakeBuilder(8, 0); + + rb.Push(ResultSuccess); + rb.PushRaw(desc); +} + +void DLP_Base::InitializeDlpBase(u32 shared_mem_size, + std::shared_ptr shared_mem, + std::shared_ptr event, DLP_Username uname) { + dlp_sharedmem_size = shared_mem_size; + dlp_sharedmem = shared_mem; + dlp_status_event = event; + username = uname; + + uds_sharedmem = + system.Kernel() + .CreateSharedMemory(nullptr, uds_sharedmem_size, Kernel::MemoryPermission::ReadWrite, + Kernel::MemoryPermission::ReadWrite, 0, Kernel::MemoryRegion::BASE, + "NWM::UDS:SharedMemory") + .Unwrap(); + + NWM::NodeInfo cnode_info{ + .friend_code_seed = HW::UniqueData::GetLocalFriendCodeSeedB().body.friend_code_seed, + .username = uname, + }; + GetUDS()->Initialize(uds_sharedmem_size, cnode_info, uds_version, uds_sharedmem); +} + +void DLP_Base::FinalizeDlpBase() { + GetUDS()->ShutdownHLE(); + dlp_sharedmem.reset(); + uds_sharedmem.reset(); + dlp_status_event.reset(); + username = DLP_Username{}; +} + +bool DLP_Base::ConnectToNetworkAsync(NWM::NetworkInfo net_info, NWM::ConnectionType conn_type, + std::vector passphrase) { + auto uds = GetUDS(); + + // we need to make this event manually + uds->connection_event = + system.Kernel().CreateEvent(Kernel::ResetType::OneShot, "dlp_connect_to_beacon"); + + uds->ConnectToNetworkHLE(net_info, static_cast(conn_type), passphrase); + + // wait for connection + Common::Timer t_time_out; + t_time_out.Start(); + bool timed_out = false; + while (true) { // busy wait, TODO: change to not busy wait? + if (uds->GetConnectionStatusHLE().status == NWM::NetworkStatus::ConnectedAsSpectator || + uds->GetConnectionStatusHLE().status == NWM::NetworkStatus::ConnectedAsClient) { + // connected + break; + } + constexpr u32 connect_network_timeout_ms = 3000; + if (t_time_out.GetTimeElapsed().count() > connect_network_timeout_ms) { + timed_out = true; + break; + } + } + + if (timed_out) { + // TODO: fix unlikely race cond, timeout happens, we disconnect, then server registers our + // connection + uds->DisconnectNetworkHLE(); + LOG_ERROR(Service_DLP, "Timed out when trying to connect to beacon"); + return false; + } + + if (uds->GetConnectionStatusHLE().status != NWM::NetworkStatus::ConnectedAsSpectator && + uds->GetConnectionStatusHLE().status != NWM::NetworkStatus::ConnectedAsClient) { + // error! + LOG_ERROR(Service_DLP, "Could not connect to network, connected as 0x{:x}", + static_cast(uds->GetConnectionStatusHLE().status)); + return false; + } + + return true; +} + +int DLP_Base::RecvFrom(u16 node_id, std::vector& buffer) { + constexpr u32 max_pullpacket_size = 0x3c00; + std::vector buffer_out; + + NWM::SecureDataHeader secure_data; + auto uds = GetUDS(); + if (!uds) { + LOG_ERROR(Service_DLP, "Could not get get pointer to UDS service!"); + return 0; + } + auto ret = + uds->PullPacketHLE(node_id, max_pullpacket_size, static_cast(max_pullpacket_size) >> 2, + buffer_out, &secure_data); + + if (!ret) { + return 0; + } + + buffer = buffer_out; + return *ret; // size +} + +bool DLP_Base::SendTo(u16 node_id, u8 data_channel, std::vector& buffer, u8 flags) { + constexpr u32 max_sendto_size = 0x3c00; + + if (buffer.size() > max_sendto_size) { + LOG_WARNING(Service_DLP, "Packet size is larger than 0x{:x}", max_sendto_size); + } + + return GetUDS()->SendToHLE(node_id, data_channel, buffer.size(), flags, buffer) == + NWM::ResultStatus::ResultSuccess; +} + +u32 DLP_Base::GeneratePKChecksum(u32 aes_value, void* _input_buffer, u32 packet_size) { + auto input_buffer = reinterpret_cast(_input_buffer); + + u32 working_hash = 0; + // add all word aligned bytes + for (u32 i = 0; i < packet_size / sizeof(u32); i++) { + u32 inp_buf_word = reinterpret_cast(input_buffer)[i]; + working_hash += Common::swap32(inp_buf_word); + } + // add any remaining non word-aligned bytes + if (u32 num_bytes_non_aligned = packet_size & 3; num_bytes_non_aligned != 0) { + u32 non_aligned = 0; + memcpy(&non_aligned, input_buffer + packet_size - num_bytes_non_aligned, + num_bytes_non_aligned); + working_hash += Common::swap32(non_aligned); + } + // hash by the aes value + u8 num_extra_hash = (reinterpret_cast(&aes_value)[3] & 0b0111) + 2; + u8 num_shift_extra_hash = (reinterpret_cast(&aes_value)[2] & 0b1111) + 4; + u32 aes_swap = Common::swap32(aes_value); + for (u8 i = 0; i < num_extra_hash; i++) { + working_hash = + (working_hash >> num_shift_extra_hash | working_hash << num_shift_extra_hash) ^ + aes_swap; + } + return Common::swap32(working_hash); +} + +u32 DLP_Base::GenDLPChecksumKey(Network::MacAddress mac_addr) { + auto dlp_iv_ctr_buf = HW::AES::GetDlpChecksumModIv(); + + std::array ctr_encrypt_buf{}; + for (u32 i = 0; i < 0x10; i++) { + ctr_encrypt_buf[i] = mac_addr[i % 6] ^ dlp_iv_ctr_buf[i]; + } + + u32 val_out = 0; + DLPEncryptCTR(&val_out, sizeof(val_out), ctr_encrypt_buf.data()); + return val_out; +} + +bool DLP_Base::ValidatePacket(u32 aes, void* pk, size_t sz, bool checksum) { + if (sz < sizeof(DLPPacketHeader)) { + LOG_ERROR(Service_DLP, "Packet size is too small"); + return false; + } + + auto ph = reinterpret_cast(pk); + + if (ph->size != sz) { + LOG_ERROR(Service_DLP, "Packet size in header does not match size received"); + return false; + } + + if (checksum) { + std::vector pk_copy; + pk_copy.resize(sz); + memcpy(pk_copy.data(), pk, sz); + + auto ph_cpy = reinterpret_cast(pk_copy.data()); + ph_cpy->checksum = 0; + u32 new_checksum = GeneratePKChecksum(aes, pk_copy.data(), pk_copy.size()); + if (new_checksum != ph->checksum) { + LOG_ERROR(Service_DLP, "Could not verify packet checksum 0x{:x} != 0x{:x}", + new_checksum, ph->checksum); + return false; + } + } + return true; +} + +u32 DLP_Base::GetNumFragmentsFromTitleSize(u32 tsize) { + return Common::AlignUp(tsize - broad_title_size_diff, content_fragment_size) / + content_fragment_size; +} + +} // namespace Service::DLP diff --git a/src/core/hle/service/dlp/dlp_base.h b/src/core/hle/service/dlp/dlp_base.h new file mode 100644 index 000000000..6708937f9 --- /dev/null +++ b/src/core/hle/service/dlp/dlp_base.h @@ -0,0 +1,387 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "core/core.h" +#include "core/hle/service/cfg/cfg.h" +#include "core/hle/service/nwm/nwm_uds.h" +#include "core/hle/service/service.h" + +#include + +// DLP save states are not supported + +namespace Service::DLP { + +using DLP_Username = std::array; +constexpr inline u64 DLP_CHILD_TID_HIGH = 0x0004000100000000; +constexpr inline u32 content_fragment_size = 1440; + +struct DLPTitleInfo { + u32 unique_id; // games look at this to make sure it's their title info + u32 variation; + Network::MacAddress mac_addr; + u16 version; // XX: probably? + std::array age_ratings; + std::array short_description; // UTF-16 + std::array long_description; // UTF-16 + std::array icon; // 48x48, RGB565 + u32 size; + u8 unk2; + u8 unk3; + u16 padding; + std::vector ToBuffer() { + std::vector out; + out.resize(sizeof(DLPTitleInfo)); + memcpy(out.data(), this, sizeof(DLPTitleInfo)); + return out; + } +}; + +static_assert(sizeof(DLPTitleInfo) == 5032, "DLPTitleInfo is the wrong size"); + +struct DLPNodeInfo { + u64 friend_code_seed; + std::array pad; + DLP_Username username; + u32 unk1; + u32 network_node_id; +}; + +static_assert(sizeof(DLPNodeInfo) == 0x28); + +struct DLPEventDescription { + std::array unk; +}; + +static_assert(sizeof(DLPEventDescription) == 0x18); + +// START BIG ENDIAN + +constexpr inline u8 dl_pk_type_broadcast = 0x01; +constexpr inline u8 dl_pk_type_auth = 0x02; +constexpr inline u8 dl_pk_type_start_dist = 0x03; +constexpr inline u8 dl_pk_type_distribute = 0x04; +constexpr inline u8 dl_pk_type_finish_dist = 0x05; +constexpr inline u8 dl_pk_type_start_game = 0x06; + +constexpr inline std::array dl_pk_head_broadcast_header = {dl_pk_type_broadcast, 0x02}; +constexpr inline std::array dl_pk_head_auth_header = {dl_pk_type_auth, 0x02}; +constexpr inline std::array dl_pk_head_start_dist_header = {dl_pk_type_start_dist, 0x02}; +constexpr inline std::array dl_pk_head_distribute_header = {dl_pk_type_distribute, 0x02}; +constexpr inline std::array dl_pk_head_finish_dist_header = {dl_pk_type_finish_dist, 0x02}; +constexpr inline std::array dl_pk_head_start_game_header = {dl_pk_type_start_game, 0x02}; + +struct DLPPacketHeader { + union { + std::array magic; + struct { + u8 type; + u8 mag0x02; + u16 unk; // usually 0x00 0x00 + }; + }; + u16_be size; // size of the whole packet, including the header + std::array unk1; // always 0x02 0x00 + u32 checksum; // always calculate + u8 packet_index; // starts at 0 + std::array resp_id; // copies this from host packet when responding to it +}; + +static_assert(sizeof(DLPPacketHeader) == 0x10); + +// bool with 3ds padding included +struct DLPPacketBool { + union { + u32 raw; + struct { + u8 active_value : 1; + u8 padding : 7; + std::array padding2; + }; + }; + operator bool() { + return active_value; + } + DLPPacketBool& operator=(const bool& o) { + raw = 0x0; + active_value = o; + return *this; + } +}; + +static_assert(sizeof(DLPPacketBool) == sizeof(u32_be)); + +constexpr u32 broad_title_size_diff = 111360; + +#pragma pack(push, 2) +struct DLPBroadcastPacket1 { + DLPPacketHeader head; + u64_be child_title_id; // title id of the child being broadcasted + u64 unk1; + u64 unk2; + u64 unk3; + u64 unk4; // all 0s + u32_be size; // size minus broad_title_size_diff + u32 unk5; + std::array title_short; + std::array title_long; + std::array icon_part; + u64 unk; +}; +#pragma pack(pop) + +static_assert(sizeof(DLPBroadcastPacket1) == 768); + +struct DLPBroadcastPacket2 { + DLPPacketHeader head; + std::array icon_part; +}; + +static_assert(sizeof(DLPBroadcastPacket2) == 1448); + +struct DLPBroadcastPacket3 { + DLPPacketHeader head; + std::array icon_part; +}; + +static_assert(sizeof(DLPBroadcastPacket3) == 1448); + +struct DLPBroadcastPacket4 { + DLPPacketHeader head; + std::array icon_part; +}; + +static_assert(sizeof(DLPBroadcastPacket4) == 1448); + +struct DLPBroadcastPacket5 { + DLPPacketHeader head; + std::array unk1; + std::array unk2; + std::array unk3; +}; + +static_assert(sizeof(DLPBroadcastPacket5) == 1464); + +// auth session +struct DLPSrvr_Auth { + DLPPacketHeader head; + u32 unk1; // 0x0 +}; + +static_assert(sizeof(DLPSrvr_Auth) == 0x14); + +struct DLPClt_AuthAck { + DLPPacketHeader head; + DLPPacketBool initialized; // true + std::array padding; + std::array resp_id; // very important! game specific? +}; + +static_assert(sizeof(DLPClt_AuthAck) == 0x18); + +// start distribution +struct DLPSrvr_StartDistribution { + DLPPacketHeader head; + DLPPacketBool initialized; // 0x1 +}; + +static_assert(sizeof(DLPSrvr_StartDistribution) == 0x14); + +struct DLPClt_StartDistributionAck_NoContentNeeded { + DLPPacketHeader head; + DLPPacketBool initialized; // 0x1 + u32 unk2; // 0x0 +}; + +static_assert(sizeof(DLPClt_StartDistributionAck_NoContentNeeded) == 0x18); + +struct DLPClt_StartDistributionAck_ContentNeeded { + DLPPacketHeader head; + DLPPacketBool initialized; // 0x1 + u16_be unk2; // BE 0x20 unk important! + u16_be unk3; // 0x0 + DLPPacketBool unk4; // 0x1 + u32_be unk5; // 0x0 + std::array unk_body; +}; + +static_assert(sizeof(DLPClt_StartDistributionAck_ContentNeeded) == 0x38); + +// perform distribution of content +// packet_index is 1 +struct DLPSrvr_ContentDistributionFragment { + DLPPacketHeader head; + u32_be content_magic; // extra magic value + u32_be unk1; // 0x1 BE + u16_be frag_index; // BE % dlp_content_block_length + u16_be frag_size; // BE + u8 content_fragment[]; +}; + +static_assert(sizeof(DLPSrvr_ContentDistributionFragment) == 28); + +// finish receiving content +struct DLPSrvr_FinishContentUpload { + DLPPacketHeader head; + DLPPacketBool initialized; // 0x1 + u32_be seq_num; // BE starts at 0x0 and copies whatever number the ack gives it +}; + +static_assert(sizeof(DLPSrvr_FinishContentUpload) == 0x18); + +// it sends this to clients during distribution +#pragma pack(push, 2) +struct DLPClt_FinishContentUploadAck { + DLPPacketHeader head; + DLPPacketBool initialized; // 0x1 + u8 unk2; // 0x1 + u8 needs_content; // 0x1 if downloading conetnt + u32_be seq_ack; // BE client increments this every ack + u16 unk4; // 0x0 +}; +#pragma pack(pop) + +static_assert(sizeof(DLPClt_FinishContentUploadAck) == 0x1C); + +// start game +// these will keep sending until +// the final command is given +struct DLPSrvr_BeginGame { + DLPPacketHeader head; + u32_le unk1; // 0x1 + u32_le unk2; // 0x9 could be DLP_Srvr_State +}; + +static_assert(sizeof(DLPSrvr_BeginGame) == 0x18); + +struct DLPClt_BeginGameAck { + DLPPacketHeader head; + u32_le unk1; // 0x1 + u32_le unk2; // 0x9 could be DLP_Clt_State +}; + +static_assert(sizeof(DLPClt_BeginGameAck) == 0x18); + +// packet_index is 1. this is not acked +struct DLPSrvr_BeginGameFinal { + DLPPacketHeader head; + u32_le unk1; // 0x1 + std::array wireless_reboot_passphrase; + u8 unk2; // 0x09 could be server state + u16 padding; // 0x00 0x00 +}; + +static_assert(sizeof(DLPSrvr_BeginGameFinal) == 0x20); + +// END BIG ENDIAN + +class DLP_Base { +protected: + DLP_Base(Core::System& s); + virtual ~DLP_Base() = default; + + virtual std::shared_ptr GetServiceFrameworkSharedPtr() = 0; + virtual bool IsHost() = 0; + + Core::System& system; + + std::shared_ptr dlp_sharedmem; + std::shared_ptr uds_sharedmem; + + std::shared_ptr dlp_status_event; // out + std::shared_ptr uds_status_event; // in + + bool should_verify_checksum = false; + + const u32 uds_sharedmem_size = 0x4000; + const u32 uds_version = 0x400; + const u32 recv_buffer_size = 0x3c00; + const u32 dlp_channel = 0x10; + const u8 num_broadcast_packets = 5; + u32 dlp_sharedmem_size{}; + + DLP_Username username; + // stubbed as HLE NWM_UDS does not check this. Should be: 0km@tsa$uhmy1a0sa + nul + std::vector dlp_password_buf{}; + std::array wireless_reboot_passphrase; + + const u32 dlp_content_block_length = 182; + + std::shared_ptr GetCFG(); + std::shared_ptr GetUDS(); + + void GetEventDescription(Kernel::HLERequestContext& ctx); + + void InitializeDlpBase(u32 shared_mem_size, std::shared_ptr shared_mem, + std::shared_ptr event, DLP_Username username); + void FinalizeDlpBase(); + + bool ConnectToNetworkAsync(NWM::NetworkInfo net_info, NWM::ConnectionType conn_type, + std::vector passphrase); + int RecvFrom(u16 node_id, std::vector& buffer); + bool SendTo(u16 node_id, u8 data_channel, std::vector& buffer, u8 flags = 0); + + static std::u16string DLPUsernameAsString16(DLP_Username uname); + static DLP_Username String16AsDLPUsername(std::u16string str); + static DLPNodeInfo UDSToDLPNodeInfo(NWM::NodeInfo node_info); + template + static T* GetPacketBody(std::vector& b) { + if (b.size() < sizeof(T)) { + LOG_CRITICAL(Service_DLP, "Packet size is too small to fit content {} < {}", b.size(), + sizeof(T)); + return nullptr; + } + return reinterpret_cast(b.data()); + } + static DLPPacketHeader* GetPacketHead(std::vector& b) { + if (b.size() < sizeof(DLPPacketHeader)) { + LOG_CRITICAL(Service_DLP, "Packet is too small to fit a DLP header"); + return nullptr; + } + return reinterpret_cast(b.data()); + } + + static u32 GeneratePKChecksum(u32 aes_value, void* input_buffer, u32 packet_size); + + template + T* PGen_SetPK(std::array magic, u8 packet_index, std::array resp_id) { + if (!sm_packet_sender_session.try_acquire()) { + LOG_ERROR(Service_DLP, + "Tried to send 2 packets concurrently, causing blocking on this thread"); + sm_packet_sender_session.acquire(); + } + send_packet_ctx.resize(sizeof(T)); + auto ph = GetPacketHead(send_packet_ctx); + ph->magic = magic; + ph->size = sizeof(T); + ph->unk1 = {0x02, 0x00}; + ph->resp_id = resp_id; + ph->packet_index = packet_index; + return GetPacketBody(send_packet_ctx); + } + void PGen_SendPK(u32 aes, u16 node_id, u8 data_channel, u8 flags = 0) { + ASSERT(send_packet_ctx.size() >= sizeof(DLPPacketHeader)); + auto ph = GetPacketHead(send_packet_ctx); + ASSERT(ph->size == send_packet_ctx.size()); + ph->checksum = 0; + ph->checksum = GeneratePKChecksum(aes, ph, ph->size); + SendTo(node_id, data_channel, send_packet_ctx, flags); + send_packet_ctx.clear(); + sm_packet_sender_session.release(); + } + // input the host mac address + u32 GenDLPChecksumKey(Network::MacAddress mac_addr); + static void DLPEncryptCTR(void* out, size_t size, const u8* iv_ctr); + static bool ValidatePacket(u32 aes, void* pk, size_t sz, bool checksum = true); + + static u32 GetNumFragmentsFromTitleSize(u32 tsize); + +private: + std::binary_semaphore sm_packet_sender_session{1}; + std::vector send_packet_ctx; +}; + +} // namespace Service::DLP diff --git a/src/core/hle/service/dlp/dlp_clnt.cpp b/src/core/hle/service/dlp/dlp_clnt.cpp index 8d79e9709..c4121a27c 100644 --- a/src/core/hle/service/dlp/dlp_clnt.cpp +++ b/src/core/hle/service/dlp/dlp_clnt.cpp @@ -1,4 +1,4 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -10,29 +10,150 @@ SERIALIZE_EXPORT_IMPL(Service::DLP::DLP_CLNT) namespace Service::DLP { -DLP_CLNT::DLP_CLNT() : ServiceFramework("dlp:CLNT", 1) { +std::shared_ptr DLP_CLNT::GetServiceFrameworkSharedPtr() { + return shared_from_this(); +} + +u32 DLP_CLNT::ClientNeedsDup() { + [[maybe_unused]] constexpr u32 res_needs_system_update = 0x1; + constexpr u32 res_does_not_need_update = 0x0; + return res_does_not_need_update; +} + +void DLP_CLNT::Initialize(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + u32 shared_mem_size = rp.Pop(); + u32 max_beacons = rp.Pop(); + u32 constant_mem_size = rp.Pop(); + auto [shared_mem, event] = rp.PopObjects(); + + InitializeCltBase(shared_mem_size, max_beacons, constant_mem_size, shared_mem, event, + String16AsDLPUsername(GetCFG()->GetUsername())); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +void DLP_CLNT::Finalize(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + FinalizeCltBase(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +// returns the version of the currently joined server +void DLP_CLNT::GetCupVersion(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + [[maybe_unused]] auto mac_addr = rp.PopRaw(); + [[maybe_unused]] u32 tid_low = rp.PopRaw(); + [[maybe_unused]] u32 tid_high = rp.PopRaw(); + + LOG_WARNING(Service_DLP, "(STUBBED) called"); + + IPC::RequestBuilder rb = rp.MakeBuilder(3, 0); + + // TODO: someone decipher this version code + u64 version_num = 0x0; + + rb.Push(ResultSuccess); + rb.Push(version_num); +} + +// tells us which server to connect to and download an update from +// the dlp app uses this to check whether or not we need the update data +void DLP_CLNT::PrepareForSystemDownload(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + mac_addr_update = rp.PopRaw(); + [[maybe_unused]] u32 tid_low = rp.PopRaw(); + [[maybe_unused]] u32 tid_high = rp.PopRaw(); + + if (ClientNeedsDup()) { + is_preparing_for_update = true; + } + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + + rb.Push(ResultSuccess); + rb.Push(ClientNeedsDup()); +} + +// runs after the user accepts the license agreement to +// download the update +void DLP_CLNT::StartSystemDownload(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + LOG_WARNING(Service_DLP, "(STUBBED) called"); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + if (!is_preparing_for_update) { + // error + LOG_ERROR(Service_DLP, "Called without preparing first. We don't have a mac address!"); + // TODO: verify this on hw + rb.Push(0xD960AC02); + return; + } + + is_preparing_for_update = false; + is_updating = true; + + // TODO: figure out what comes after when + // hw starts downloading update data via dlp. + // it could set some missing client states + // in GetCltState + + rb.Push(ResultSuccess); +} + +// i'm assuming this is a secondary check whether or not we +// can download the update data? +void DLP_CLNT::GetDupAvailability(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + mac_addr_update = rp.PopRaw(); + [[maybe_unused]] u32 tid_low = rp.PopRaw(); + [[maybe_unused]] u32 tid_high = rp.PopRaw(); + + LOG_WARNING(Service_DLP, "(STUBBED) called"); + + [[maybe_unused]] constexpr u32 dup_is_available = 0x1; + constexpr u32 dup_is_not_available = 0x0; + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + + rb.Push(ResultSuccess); + rb.Push(dup_is_not_available); +} + +DLP_CLNT::DLP_CLNT() + : ServiceFramework("dlp:CLNT", 1), DLP_Clt_Base(Core::System::GetInstance(), "CLNT") { static const FunctionInfo functions[] = { // clang-format off - {0x0001, nullptr, "Initialize"}, - {0x0002, nullptr, "Finalize"}, - {0x0003, nullptr, "GetEventDesc"}, - {0x0004, nullptr, "GetChannel"}, - {0x0005, nullptr, "StartScan"}, - {0x0006, nullptr, "StopScan"}, - {0x0007, nullptr, "GetServerInfo"}, - {0x0008, nullptr, "GetTitleInfo"}, - {0x0009, nullptr, "GetTitleInfoInOrder"}, - {0x000A, nullptr, "DeleteScanInfo"}, - {0x000B, nullptr, "PrepareForSystemDownload"}, - {0x000C, nullptr, "StartSystemDownload"}, - {0x000D, nullptr, "StartTitleDownload"}, - {0x000E, nullptr, "GetMyStatus"}, - {0x000F, nullptr, "GetConnectingNodes"}, - {0x0010, nullptr, "GetNodeInfo"}, - {0x0011, nullptr, "GetWirelessRebootPassphrase"}, - {0x0012, nullptr, "StopSession"}, - {0x0013, nullptr, "GetCupVersion"}, - {0x0014, nullptr, "GetDupAvailability"}, + {0x0001, &DLP_CLNT::Initialize, "Initialize"}, + {0x0002, &DLP_CLNT::Finalize, "Finalize"}, + {0x0003, &DLP_CLNT::GetEventDescription, "GetEventDescription"}, + {0x0004, &DLP_CLNT::GetChannels, "GetChannel"}, + {0x0005, &DLP_CLNT::StartScan, "StartScan"}, + {0x0006, &DLP_CLNT::StopScan, "StopScan"}, + {0x0007, &DLP_CLNT::GetServerInfo, "GetServerInfo"}, + {0x0008, &DLP_CLNT::GetTitleInfo, "GetTitleInfo"}, + {0x0009, &DLP_CLNT::GetTitleInfoInOrder, "GetTitleInfoInOrder"}, + {0x000A, &DLP_CLNT::DeleteScanInfo, "DeleteScanInfo"}, + {0x000B, &DLP_CLNT::PrepareForSystemDownload, "PrepareForSystemDownload"}, + {0x000C, &DLP_CLNT::StartSystemDownload, "StartSystemDownload"}, + {0x000D, &DLP_CLNT::StartSession, "StartTitleDownload"}, + {0x000E, &DLP_CLNT::GetMyStatus, "GetMyStatus"}, + {0x000F, &DLP_CLNT::GetConnectingNodes, "GetConnectingNodes"}, + {0x0010, &DLP_CLNT::GetNodeInfo, "GetNodeInfo"}, + {0x0011, &DLP_CLNT::GetWirelessRebootPassphrase, "GetWirelessRebootPassphrase"}, + {0x0012, &DLP_CLNT::StopSession, "StopSession"}, + {0x0013, &DLP_CLNT::GetCupVersion, "GetCupVersion"}, + {0x0014, &DLP_CLNT::GetDupAvailability, "GetDupAvailability"}, // clang-format on }; diff --git a/src/core/hle/service/dlp/dlp_clnt.h b/src/core/hle/service/dlp/dlp_clnt.h index ac6933e7e..8b9a56300 100644 --- a/src/core/hle/service/dlp/dlp_clnt.h +++ b/src/core/hle/service/dlp/dlp_clnt.h @@ -1,20 +1,41 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once #include "core/hle/service/service.h" +#include "dlp_clt_base.h" namespace Service::DLP { -class DLP_CLNT final : public ServiceFramework { +class DLP_CLNT final : public ServiceFramework, public DLP_Clt_Base { public: DLP_CLNT(); - ~DLP_CLNT() = default; + virtual ~DLP_CLNT() = default; + + virtual std::shared_ptr GetServiceFrameworkSharedPtr(); private: SERVICE_SERIALIZATION_SIMPLE + + virtual bool IsFKCL() { + return false; + } + + bool is_preparing_for_update = false; + bool is_updating = false; + Network::MacAddress mac_addr_update; + + u32 ClientNeedsDup(); + + void Initialize(Kernel::HLERequestContext& ctx); + void Finalize(Kernel::HLERequestContext& ctx); + void GetCupVersion(Kernel::HLERequestContext& ctx); + void StartTitleDownload(Kernel::HLERequestContext& ctx); + void PrepareForSystemDownload(Kernel::HLERequestContext& ctx); + void StartSystemDownload(Kernel::HLERequestContext& ctx); + void GetDupAvailability(Kernel::HLERequestContext& ctx); }; } // namespace Service::DLP diff --git a/src/core/hle/service/dlp/dlp_clt_base.cpp b/src/core/hle/service/dlp/dlp_clt_base.cpp new file mode 100644 index 000000000..d127dbd36 --- /dev/null +++ b/src/core/hle/service/dlp/dlp_clt_base.cpp @@ -0,0 +1,856 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "dlp_clt_base.h" + +#include "common/alignment.h" +#include "common/string_util.h" +#include "common/timer.h" +#include "core/hle/ipc_helpers.h" +#include "core/hle/service/am/am.h" +#include "core/hle/service/nwm/uds_beacon.h" + +namespace Service::DLP { + +DLP_Clt_Base::DLP_Clt_Base(Core::System& s, std::string unique_string_id) : DLP_Base(s) { + std::string unique_scan_event_id = fmt::format("DLP::{}::BeaconScanCallback", unique_string_id); + beacon_scan_event = system.CoreTiming().RegisterEvent( + unique_scan_event_id, [this](std::uintptr_t user_data, s64 cycles_late) { + BeaconScanCallback(user_data, cycles_late); + }); +} + +DLP_Clt_Base::~DLP_Clt_Base() { + { + std::scoped_lock lock(beacon_mutex); + is_scanning = false; + system.CoreTiming().UnscheduleEvent(beacon_scan_event, 0); + } + + DisconnectFromServer(); +} + +void DLP_Clt_Base::InitializeCltBase(u32 shared_mem_size, u32 max_beacons, u32 constant_mem_size, + std::shared_ptr shared_mem, + std::shared_ptr event, DLP_Username username) { + InitializeDlpBase(shared_mem_size, shared_mem, event, username); + + clt_state = DLP_Clt_State::Initialized; + max_title_info = max_beacons; + + LOG_INFO(Service_DLP, + "shared mem size: 0x{:x}, max beacons: {}, constant mem size: 0x{:x}, username: {}", + shared_mem_size, max_beacons, constant_mem_size, + Common::UTF16ToUTF8(DLPUsernameAsString16(username)).c_str()); +} + +void DLP_Clt_Base::FinalizeCltBase() { + clt_state = DLP_Clt_State::Initialized; + + if (is_connected) { + DisconnectFromServer(); + } + + FinalizeDlpBase(); + + LOG_INFO(Service_DLP, "called"); +} + +void DLP_Clt_Base::GenerateChannelHandle() { + dlp_channel_handle = 0x0421; // it seems to always be this value on hardware +} + +u32 DLP_Clt_Base::GetCltState() { + std::scoped_lock lock(clt_state_mutex); + u16 node_id = 0x0; + if (is_connected) { + node_id = GetUDS()->GetConnectionStatusHLE().network_node_id; + } + return static_cast(clt_state) << 24 | is_connected << 16 | node_id; +} + +void DLP_Clt_Base::GetChannels(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + GenerateChannelHandle(); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); + rb.Push(dlp_channel_handle); +} + +void DLP_Clt_Base::GetMyStatus(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + IPC::RequestBuilder rb = rp.MakeBuilder(6, 0); + rb.Push(ResultSuccess); + rb.Push(GetCltState()); + rb.Push(dlp_units_total); + rb.Push(dlp_units_downloaded); + // TODO: find out what these are + rb.Push(0x0); + rb.Push(0x0); +} + +int DLP_Clt_Base::GetCachedTitleInfoIdx(Network::MacAddress mac_addr) { + std::scoped_lock lock(title_info_mutex); + + for (int i = 0; auto& t : scanned_title_info) { + if (t.first.mac_addr == mac_addr) { + return i; + } + i++; + } + return -1; +} + +bool DLP_Clt_Base::TitleInfoIsCached(Network::MacAddress mac_addr) { + return GetCachedTitleInfoIdx(mac_addr) != -1; +} + +void DLP_Clt_Base::StartScan(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + u16 scan_handle = rp.Pop(); + scan_title_id_filter = rp.Pop(); + scan_mac_address_filter = rp.PopRaw(); + ASSERT_MSG( + scan_handle == dlp_channel_handle, + "Scan handle and dlp channel handle do not match. Did you input the wrong ipc params?"); + [[maybe_unused]] u32 unk1 = rp.Pop(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + // start beacon worker + if (!IsIdling()) { + rb.Push(Result(0x1, ErrorModule::DLP, ErrorSummary::InvalidState, ErrorLevel::Usage)); + return; + } + + std::scoped_lock lock{beacon_mutex, title_info_mutex, clt_state_mutex}; + + // reset scan dependent variables + scanned_title_info.clear(); + ignore_servers_list.clear(); + title_info_index = 0; + + clt_state = DLP_Clt_State::Scanning; + is_scanning = true; + + // clear out received beacons + GetUDS()->GetReceivedBeacons(Network::BroadcastMac); + + LOG_INFO(Service_DLP, "Starting scan worker"); + + constexpr int first_scan_delay_ms = 0; + + system.CoreTiming().ScheduleEvent(msToCycles(first_scan_delay_ms), beacon_scan_event, 0); + + rb.Push(ResultSuccess); +} + +void DLP_Clt_Base::StopScan(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + // end beacon worker + { + std::scoped_lock lock{beacon_mutex, clt_state_mutex}; + clt_state = DLP_Clt_State::Initialized; + is_scanning = false; + + LOG_INFO(Service_DLP, "Ending scan worker"); + + system.CoreTiming().UnscheduleEvent(beacon_scan_event, 0); + } + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +void DLP_Clt_Base::GetTitleInfo(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + auto mac_addr = rp.PopRaw(); + [[maybe_unused]] u32 tid_low = rp.Pop(); + [[maybe_unused]] u32 tid_high = rp.Pop(); + + std::scoped_lock lock(title_info_mutex); + + if (!TitleInfoIsCached(mac_addr)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NoData, ErrorModule::DLP, ErrorSummary::NotFound, + ErrorLevel::Status)); + return; + } + + auto c_title_idx = GetCachedTitleInfoIdx(mac_addr); + std::vector buffer = scanned_title_info[c_title_idx].first.ToBuffer(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ResultSuccess); + rb.PushStaticBuffer(std::move(buffer), 0); +} + +void DLP_Clt_Base::GetTitleInfoInOrder(Kernel::HLERequestContext& ctx) { + constexpr u8 cmd_reset_iterator = 0x1; + + IPC::RequestParser rp(ctx); + + u8 command = rp.Pop(); + if (command == cmd_reset_iterator) { + title_info_index = 0; + } + + std::scoped_lock lock(title_info_mutex); + + if (title_info_index >= scanned_title_info.size()) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NoData, ErrorModule::DLP, ErrorSummary::NotFound, + ErrorLevel::Status)); + return; + } + + std::vector buffer = scanned_title_info[title_info_index].first.ToBuffer(); + + ++title_info_index; + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ResultSuccess); + rb.PushStaticBuffer(std::move(buffer), 0); +} + +void DLP_Clt_Base::DeleteScanInfo(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + LOG_INFO(Service_DLP, "Called"); + + auto mac_addr = rp.PopRaw(); + + std::scoped_lock lock(title_info_mutex); + + if (!TitleInfoIsCached(mac_addr)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NoData, ErrorModule::DLP, ErrorSummary::NotFound, + ErrorLevel::Status)); + return; + } + + scanned_title_info.erase(scanned_title_info.begin() + GetCachedTitleInfoIdx(mac_addr)); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +void DLP_Clt_Base::GetServerInfo(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + auto mac_addr = rp.PopRaw(); + + std::scoped_lock lock(title_info_mutex); + + if (!TitleInfoIsCached(mac_addr)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NoData, ErrorModule::DLP, ErrorSummary::NotFound, + ErrorLevel::Status)); + return; + } + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + + auto buffer = scanned_title_info[GetCachedTitleInfoIdx(mac_addr)].second.ToBuffer(); + + rb.Push(ResultSuccess); + rb.PushStaticBuffer(std::move(buffer), 0); +} + +class DLP_Clt_Base::ThreadCallback : public Kernel::HLERequestContext::WakeupCallback { +public: + explicit ThreadCallback(std::shared_ptr p) : p_obj(p) {} + + void WakeUp(std::shared_ptr thread, Kernel::HLERequestContext& ctx, + Kernel::ThreadWakeupReason reason) { + IPC::RequestBuilder rb(ctx, 1, 0); + + if (!p_obj->OnConnectCallback()) { + rb.Push(Result(ErrorDescription::Timeout, ErrorModule::DLP, ErrorSummary::Canceled, + ErrorLevel::Status)); + return; + } + rb.Push(ResultSuccess); + } + +private: + ThreadCallback() = default; + std::shared_ptr p_obj; + + template + void serialize(Archive& ar, const unsigned int) { + ar& boost::serialization::base_object(*this); + } + friend class boost::serialization::access; +}; + +bool DLP_Clt_Base::OnConnectCallback() { + auto uds = GetUDS(); + if (uds->GetConnectionStatusHLE().status != NWM::NetworkStatus::ConnectedAsClient) { + LOG_ERROR(Service_DLP, "Could not connect to dlp server (timed out)"); + return false; + } + + is_connected = true; + + client_connection_worker = std::thread([this] { ClientConnectionManager(); }); + + return true; +} + +void DLP_Clt_Base::StartSession(Kernel::HLERequestContext& ctx) { + std::scoped_lock lock(clt_state_mutex); + IPC::RequestParser rp(ctx); + + auto mac_addr = rp.PopRaw(); + + LOG_INFO(Service_DLP, "called"); + + // tells us which child we want to use for this session + // only used for dlp::CLNT + u32 dlp_child_low = rp.Pop(); + u32 dlp_child_high = rp.Pop(); + + if (!IsIdling()) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(0x1, ErrorModule::DLP, ErrorSummary::InvalidState, ErrorLevel::Usage)); + return; + } + if (!TitleInfoIsCached(mac_addr)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NoData, ErrorModule::DLP, ErrorSummary::NotFound, + ErrorLevel::Status)); + return; + } + + dlp_download_child_tid = static_cast(dlp_child_high) << 32 | dlp_child_low; + + // ConnectToNetworkAsync won't work here beacuse this is + // synchronous + + auto shared_this = std::dynamic_pointer_cast(GetServiceFrameworkSharedPtr()); + if (!shared_this) { + LOG_CRITICAL(Service_DLP, + "Could not dynamic_cast service framework shared_ptr to DLP_Clt_Base"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(-1); + return; + } + + host_mac_address = mac_addr; + clt_state = DLP_Clt_State::Joined; + + auto uds = GetUDS(); + NWM::NetworkInfo net_info; + net_info.host_mac_address = mac_addr; + net_info.channel = dlp_net_info_channel; + net_info.initialized = true; + net_info.oui_value = NWM::NintendoOUI; + + uds->ConnectToNetworkHLE(net_info, static_cast(NWM::ConnectionType::Client), + dlp_password_buf); + + // 3 second timeout + constexpr std::chrono::nanoseconds UDSConnectionTimeout{3000000000}; + uds->connection_event = + ctx.SleepClientThread("DLP_Clt_Base::StartSession", UDSConnectionTimeout, + std::make_shared(shared_this)); +} + +void DLP_Clt_Base::StopSession(Kernel::HLERequestContext& ctx) { + LOG_INFO(Service_DLP, "called"); + std::scoped_lock lock(clt_state_mutex); + IPC::RequestParser rp(ctx); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + if (is_connected) { + DisconnectFromServer(); + } + + // this call returns success no matter what + rb.Push(ResultSuccess); +} + +void DLP_Clt_Base::GetConnectingNodes(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + u16 node_array_len = rp.Pop(); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); + + auto conn_status = GetUDS()->GetConnectionStatusHLE(); + + if (!is_connected || conn_status.status != NWM::NetworkStatus::ConnectedAsClient) { + LOG_ERROR(Service_DLP, "called when we are not connected to a server"); + } + + std::vector connected_nodes_buffer; + connected_nodes_buffer.resize(node_array_len * sizeof(u16)); + memcpy(connected_nodes_buffer.data(), conn_status.nodes, + std::min(connected_nodes_buffer.size(), conn_status.total_nodes) * sizeof(u16)); + + rb.Push(ResultSuccess); + rb.Push(conn_status.total_nodes); + rb.PushStaticBuffer(std::move(connected_nodes_buffer), 0); +} + +void DLP_Clt_Base::GetNodeInfo(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + u16 network_node_id = rp.Pop(); + + auto node_info = GetUDS()->GetNodeInformationHLE(network_node_id); + if (!node_info) { + LOG_ERROR(Service_DLP, "Could not get node info for network node id 0x{:x}", + network_node_id); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NoData, ErrorModule::DLP, ErrorSummary::NotFound, + ErrorLevel::Status)); + return; + } + + IPC::RequestBuilder rb = rp.MakeBuilder(11, 0); + + rb.Push(ResultSuccess); + rb.PushRaw(UDSToDLPNodeInfo(*node_info)); +} + +void DLP_Clt_Base::GetWirelessRebootPassphrase(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + LOG_INFO(Service_DLP, "called"); + + std::scoped_lock lock(clt_state_mutex); + if (clt_state != DLP_Clt_State::Complete) { + LOG_WARNING(Service_DLP, "we have not gotten the passphrase yet"); + } + + IPC::RequestBuilder rb = rp.MakeBuilder(4, 0); + rb.Push(ResultSuccess); + rb.PushRaw(wireless_reboot_passphrase); +} + +void DLP_Clt_Base::BeaconScanCallback(std::uintptr_t user_data, s64 cycles_late) { + std::scoped_lock lock{beacon_mutex, title_info_mutex}; + + if (!is_scanning) { + return; + } + + auto uds = GetUDS(); + Common::Timer beacon_parse_timer_total; + + // sadly, we have to impl the scan code ourselves + // because the nwm recvbeaconbroadcastdata function + // has a timeout in it, which won't work here because + // we don't have a uds server/client session + auto beacons = uds->GetReceivedBeacons(Network::BroadcastMac); + + beacon_parse_timer_total.Start(); + + for (auto& beacon : beacons) { + if (auto idx = GetCachedTitleInfoIdx(beacon.transmitter_address); idx != -1) { + // update server info from beacon + auto b = GetDLPServerInfoFromRawBeacon(beacon); + scanned_title_info[idx].second.clients_joined = + b.clients_joined; // we only want to update clients joined + continue; + } + if (scanned_title_info.size() >= max_title_info) { + break; + } + if (ignore_servers_list[beacon.transmitter_address]) { + continue; + } + + CacheBeaconTitleInfo(beacon); + } + + // set our next scan interval + system.CoreTiming().ScheduleEvent( + msToCycles(std::max(0, beacon_scan_interval_ms - + beacon_parse_timer_total.GetTimeElapsed().count())) - + cycles_late, + beacon_scan_event, 0); +} + +void DLP_Clt_Base::CacheBeaconTitleInfo(Network::WifiPacket& beacon) { + // connect to the network as a spectator + // and receive dlp data + + auto uds = GetUDS(); + + NWM::NetworkInfo net_info; + net_info.host_mac_address = beacon.transmitter_address; + net_info.channel = dlp_net_info_channel; + net_info.initialized = true; + net_info.oui_value = NWM::NintendoOUI; + + if (!ConnectToNetworkAsync(net_info, NWM::ConnectionType::Spectator, dlp_password_buf)) { + LOG_ERROR(Service_DLP, "Could not connect to network."); + return; + } + + LOG_INFO(Service_DLP, "Connected to spec to network"); + + auto [ret, data_available_event] = + uds->BindHLE(dlp_bind_node_id, dlp_recv_buffer_size, dlp_broadcast_data_channel, + dlp_host_network_node_id); + if (ret != NWM::ResultStatus::ResultSuccess) { + LOG_ERROR(Service_DLP, "Could not bind on node id 0x{:x}", dlp_bind_node_id); + return; + } + + auto aes = GenDLPChecksumKey(beacon.transmitter_address); + + constexpr u32 max_beacon_recv_time_out_ms = 1000; + + Common::Timer beacon_parse_timer; + beacon_parse_timer.Start(); + + std::unordered_map got_broadcast_packet; + std::unordered_map> broadcast_packet_idx_buf; + DLP_Username server_username; // workaround before I decrypt the beacon data + std::vector recv_buf; + bool got_all_packets = false; + while (beacon_parse_timer.GetTimeElapsed().count() < max_beacon_recv_time_out_ms) { + if (int sz = RecvFrom(dlp_host_network_node_id, recv_buf)) { + auto p_head = reinterpret_cast(recv_buf.data()); + if (!ValidatePacket(aes, p_head, sz, should_verify_checksum) || + p_head->packet_index >= num_broadcast_packets) { + ignore_servers_list[beacon.transmitter_address] = true; + break; // corrupted info + } + got_broadcast_packet[p_head->packet_index] = true; + broadcast_packet_idx_buf[p_head->packet_index] = recv_buf; + if (got_broadcast_packet.size() == num_broadcast_packets) { + got_all_packets = true; + constexpr u16 nwm_host_node_network_id = 0x1; + server_username = uds->GetNodeInformationHLE(nwm_host_node_network_id)->username; + break; // we got all 5! + } + } + } + + uds->UnbindHLE(dlp_bind_node_id); + uds->DisconnectNetworkHLE(); + + if (!got_all_packets) { + if (!got_broadcast_packet.size()) { + // we didn't get ANY packet info from this server + // so we add it to the ignore list + ignore_servers_list[beacon.transmitter_address] = true; + } + LOG_ERROR(Service_DLP, "Connected to beacon, but could not receive all dlp packets"); + return; + } + + // parse packets into cached DLPServerInfo and DLPTitleInfo + auto broad_pk1 = reinterpret_cast(broadcast_packet_idx_buf[0].data()); + auto broad_pk2 = reinterpret_cast(broadcast_packet_idx_buf[1].data()); + auto broad_pk3 = reinterpret_cast(broadcast_packet_idx_buf[2].data()); + auto broad_pk4 = reinterpret_cast(broadcast_packet_idx_buf[3].data()); + [[maybe_unused]] auto broad_pk5 = + reinterpret_cast(broadcast_packet_idx_buf[4].data()); + + // apply title filter + if (scan_title_id_filter && broad_pk1->child_title_id != scan_title_id_filter) { + LOG_WARNING(Service_DLP, "Got title info, but it did not match title id filter"); + return; + } + + DLPServerInfo c_server_info = GetDLPServerInfoFromRawBeacon(beacon); + { + // workaround: load username in host node manually + c_server_info.node_info[0].username = server_username; + } + + DLPTitleInfo c_title_info{}; + c_title_info.mac_addr = beacon.transmitter_address; + + // copy over title string data + std::copy(broad_pk1->title_short.begin(), broad_pk1->title_short.end(), + c_title_info.short_description.begin()); + std::copy(broad_pk1->title_long.begin(), broad_pk1->title_long.end(), + c_title_info.long_description.begin()); + + // unique id should be the title id without the tid high shifted 1 byte right + c_title_info.unique_id = (broad_pk1->child_title_id & 0xFFFFFFFF) >> 8; + + c_title_info.size = broad_pk1->size + broad_title_size_diff; + + // copy over the icon data + auto icon_copy_loc = c_title_info.icon.begin(); + icon_copy_loc = + std::copy(broad_pk1->icon_part.begin(), broad_pk1->icon_part.end(), icon_copy_loc); + icon_copy_loc = + std::copy(broad_pk2->icon_part.begin(), broad_pk2->icon_part.end(), icon_copy_loc); + icon_copy_loc = + std::copy(broad_pk3->icon_part.begin(), broad_pk3->icon_part.end(), icon_copy_loc); + icon_copy_loc = + std::copy(broad_pk4->icon_part.begin(), broad_pk4->icon_part.end(), icon_copy_loc); + + LOG_INFO(Service_DLP, "Got title info"); + + scanned_title_info.emplace_back(c_title_info, c_server_info); + + dlp_status_event->Signal(); +} + +DLPServerInfo DLP_Clt_Base::GetDLPServerInfoFromRawBeacon(Network::WifiPacket& beacon) { + // get networkinfo from beacon + auto p_beacon = beacon.data.data(); + + bool found_net_info = false; + NWM::NetworkInfo net_info; + + // find networkinfo tag + for (auto place = p_beacon + sizeof(NWM::BeaconFrameHeader); place < place + beacon.data.size(); + place += reinterpret_cast(place)->length + sizeof(NWM::TagHeader)) { + auto th = reinterpret_cast(place); + if (th->tag_id == static_cast(NWM::TagId::VendorSpecific) && + th->length <= sizeof(NWM::NetworkInfoTag) - sizeof(NWM::TagHeader)) { + // cast to network info and check if correct + auto ni_tag = reinterpret_cast(place); + memcpy(&net_info.oui_value, ni_tag->network_info.data(), ni_tag->network_info.size()); + // make sure this is really a network info tag + if (net_info.oui_value == NWM::NintendoOUI && + net_info.oui_type == static_cast(NWM::NintendoTagId::NetworkInfo)) { + found_net_info = true; + break; + } + } + } + + if (!found_net_info) { + LOG_ERROR(Service_DLP, "Unable to find network info in beacon payload"); + return DLPServerInfo{}; + } + + DLPServerInfo srv_info{}; + srv_info.mac_addr = beacon.transmitter_address; + srv_info.max_clients = net_info.max_nodes; + srv_info.clients_joined = net_info.total_nodes; + srv_info.signal_strength = DLPSignalStrength::Strong; + srv_info.unk5 = 0x6; + // TODO: decrypt node info and load it in here + return srv_info; +} + +void DLP_Clt_Base::ClientConnectionManager() { + auto uds = GetUDS(); + + auto [ret, data_available_event] = uds->BindHLE( + dlp_bind_node_id, dlp_recv_buffer_size, dlp_client_data_channel, dlp_host_network_node_id); + if (ret != NWM::ResultStatus::ResultSuccess) { + LOG_ERROR(Service_DLP, "Could not bind on node id 0x{:x}", dlp_bind_node_id); + return; + } + + auto aes = GenDLPChecksumKey(host_mac_address); + + auto sleep_poll = [](size_t poll_rate) -> void { + std::this_thread::sleep_for(std::chrono::milliseconds(poll_rate)); + }; + + constexpr u32 dlp_poll_rate_normal = 100; + constexpr u32 dlp_poll_rate_distribute = 0; + + u32 dlp_poll_rate_ms = dlp_poll_rate_normal; + bool got_corrupted_packets = false; + + std::set received_fragments; + + while (sleep_poll(dlp_poll_rate_ms), is_connected) { + std::vector recv_buf; + + if (int sz = RecvFrom(dlp_host_network_node_id, recv_buf)) { + auto p_head = GetPacketHead(recv_buf); + // validate packet header + if (!ValidatePacket(aes, p_head, sz, should_verify_checksum)) { + got_corrupted_packets = true; + LOG_ERROR(Service_DLP, "Could not validate DLP packet header"); + break; + } + + // now we can parse the packet + std::scoped_lock lock{clt_state_mutex, title_info_mutex}; + if (p_head->type == dl_pk_type_auth) { + auto s_body = + PGen_SetPK(dl_pk_head_auth_header, 0, p_head->resp_id); + s_body->initialized = true; + // TODO: find out what this is. this changes each session. + // placeholder + s_body->resp_id = {0x01, 0x02}; + PGen_SendPK(aes, dlp_host_network_node_id, dlp_client_data_channel); + } else if (p_head->type == dl_pk_type_start_dist) { + // poll rate on non-downloading clients still needs to + // be quick enough to eat broadcast content frag packets + dlp_poll_rate_ms = dlp_poll_rate_distribute; + + if (IsFKCL() || !NeedsContentDownload(host_mac_address)) { + auto s_body = PGen_SetPK( + dl_pk_head_start_dist_header, 0, p_head->resp_id); + s_body->initialized = true; + s_body->unk2 = 0x0; + is_downloading_content = false; + clt_state = DLP_Clt_State::WaitingForServerReady; + } else { + // send content needed ack + auto s_body = PGen_SetPK( + dl_pk_head_start_dist_header, 0, p_head->resp_id); + s_body->initialized = true; + // TODO: figure out what these are. seems like magic values + s_body->unk2 = 0x20; + s_body->unk3 = 0x0; + s_body->unk4 = true; + s_body->unk5 = 0x0; + s_body->unk_body = {}; // all zeros + is_downloading_content = true; + clt_state = DLP_Clt_State::Downloading; + + if (!TitleInfoIsCached(host_mac_address)) { + LOG_CRITICAL( + Service_DLP, + "Tried to request content download, but title info was not cached"); + break; + } + + auto tinfo = scanned_title_info[GetCachedTitleInfoIdx(host_mac_address)].first; + + dlp_units_downloaded = 0; + dlp_units_total = GetNumFragmentsFromTitleSize(tinfo.size); + current_content_block = 0; + LOG_INFO(Service_DLP, "Requesting game content"); + } + PGen_SendPK(aes, dlp_host_network_node_id, dlp_client_data_channel); + } else if (p_head->type == dl_pk_type_distribute) { + if (is_downloading_content) { + auto r_pbody = GetPacketBody(recv_buf); + if (r_pbody->frag_size > sz - sizeof(DLPSrvr_ContentDistributionFragment)) { + LOG_CRITICAL(Service_DLP, + "Embedded fragment size is too large. Ignoring fragment."); + continue; + } + std::span cf(r_pbody->content_fragment, + static_cast(r_pbody->frag_size)); + ReceivedFragment frag{ + .index = static_cast(r_pbody->frag_index + + dlp_content_block_length * current_content_block), + .content{cf.begin(), cf.end()}}; + received_fragments.insert(frag); + dlp_units_downloaded++; + if (dlp_units_downloaded == dlp_units_total) { + is_downloading_content = false; + LOG_INFO(Service_DLP, "Finished downloading content. Installing..."); + + if (!InstallEncryptedCIAFromFragments(received_fragments)) { + LOG_ERROR(Service_DLP, "Could not install DLP encrypted content"); + } else { + LOG_INFO(Service_DLP, "Successfully installed DLP encrypted content"); + } + + clt_state = DLP_Clt_State::WaitingForServerReady; + } + } + } else if (p_head->type == dl_pk_type_finish_dist) { + if (p_head->packet_index == 1) { + auto r_pbody = GetPacketBody(recv_buf); + auto s_body = PGen_SetPK( + dl_pk_head_finish_dist_header, 0, p_head->resp_id); + if (is_downloading_content) { + current_content_block++; + } + s_body->initialized = true; + s_body->unk2 = 0x1; + s_body->needs_content = is_downloading_content; + s_body->seq_ack = r_pbody->seq_num + 1; + s_body->unk4 = 0x0; + PGen_SendPK(aes, dlp_host_network_node_id, dlp_client_data_channel); + } else { + LOG_ERROR(Service_DLP, "Received finish dist packet, but packet index was {}", + p_head->packet_index); + } + } else if (p_head->type == dl_pk_type_start_game) { + if (p_head->packet_index == 0) { + dlp_poll_rate_ms = dlp_poll_rate_normal; + auto s_body = PGen_SetPK(dl_pk_head_start_game_header, 0, + p_head->resp_id); + s_body->unk1 = 0x1; + s_body->unk2 = 0x9; + PGen_SendPK(aes, dlp_host_network_node_id, dlp_client_data_channel); + } else if (p_head->packet_index == 1) { + clt_state = DLP_Clt_State::Complete; + auto r_pbody = GetPacketBody(recv_buf); + wireless_reboot_passphrase = r_pbody->wireless_reboot_passphrase; + } else { + LOG_ERROR(Service_DLP, "Unknown packet index {}", p_head->packet_index); + } + } else { + LOG_ERROR(Service_DLP, "Unknown DLP Magic 0x{:x} 0x{:x} 0x{:x} 0x{:x}", + p_head->magic[0], p_head->magic[1], p_head->magic[2], p_head->magic[3]); + } + } + } + + uds->UnbindHLE(dlp_host_network_node_id); + uds->DisconnectNetworkHLE(); +} + +bool DLP_Clt_Base::NeedsContentDownload(Network::MacAddress mac_addr) { + std::scoped_lock lock(title_info_mutex); + if (!TitleInfoIsCached(mac_addr)) { + LOG_ERROR(Service_DLP, "title info was not cached"); + return false; + } + auto tinfo = scanned_title_info[GetCachedTitleInfoIdx(mac_addr)].first; + u64 title_id = DLP_CHILD_TID_HIGH | (tinfo.unique_id << 8); + return !FileUtil::Exists(AM::GetTitleContentPath(FS::MediaType::NAND, title_id)); +} + +// DLP Fragments contain encrypted CIA content by design. +// It is required to decrypt them in order to achieve +// interoperability between HLE & LLE service modules. +bool DLP_Clt_Base::InstallEncryptedCIAFromFragments(std::set& frags) { + auto cia_file = std::make_unique(system, FS::MediaType::NAND); + cia_file->AuthorizeDecryptionFromHLE(); + bool install_errored = false; + for (u64 nb = 0; auto& frag : frags) { + constexpr bool flush_data = true; + constexpr bool update_timestamp = false; + auto res = cia_file->Write(nb, frag.content.size(), flush_data, update_timestamp, + frag.content.data()); + + if (res.Failed()) { + LOG_ERROR(Service_DLP, "Could not install CIA. Error code {:08x}", res.Code().raw); + install_errored = true; + break; + } + + nb += frag.content.size(); + } + cia_file->Close(); + return !install_errored; +} + +void DLP_Clt_Base::DisconnectFromServer() { + is_connected = false; + if (client_connection_worker.joinable()) { + client_connection_worker.join(); + } +} + +bool DLP_Clt_Base::IsIdling() { + std::scoped_lock lock(beacon_mutex); + return !is_scanning && !is_connected; +} + +} // namespace Service::DLP diff --git a/src/core/hle/service/dlp/dlp_clt_base.h b/src/core/hle/service/dlp/dlp_clt_base.h new file mode 100644 index 000000000..10da159b3 --- /dev/null +++ b/src/core/hle/service/dlp/dlp_clt_base.h @@ -0,0 +1,144 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "core/hle/kernel/event.h" +#include "core/hle/kernel/shared_memory.h" +#include "dlp_base.h" + +namespace Service::DLP { + +enum class DLP_Clt_State : u32 { + NotInitialized = 0, // TODO: check on hardware. it probably just errors + Initialized = 1, + Scanning = 2, + Joined = 5, + Downloading = 6, + WaitingForServerReady = 7, + Complete = 9, +}; + +// number of bars +enum class DLPSignalStrength : u8 { + VeryWeak = 0, + Weak = 1, + Medium = 2, + Strong = 3, +}; + +// info from a server that +// can be obtained from its beacon only +struct DLPServerInfo { + Network::MacAddress mac_addr; + u8 unk1; + DLPSignalStrength signal_strength; + u8 max_clients; + u8 clients_joined; + u16 unk3; // node bitmask? + u32 padding; // all zeros + std::array node_info; + u32 unk4; + u32 unk5; + std::vector ToBuffer() { + std::vector out; + out.resize(sizeof(DLPServerInfo)); + memcpy(out.data(), this, sizeof(DLPServerInfo)); + return out; + } +}; + +static_assert(sizeof(DLPServerInfo) == 0x298); + +class DLP_Clt_Base : public DLP_Base { +protected: + DLP_Clt_Base(Core::System& s, std::string unique_string_id); + virtual ~DLP_Clt_Base(); + + virtual bool IsHost() { + return false; + } + + class ThreadCallback; + bool OnConnectCallback(); + void ClientConnectionManager(); + + virtual bool IsFKCL() = 0; + bool IsCLNT() { + return !IsFKCL(); + } + + DLP_Clt_State clt_state = DLP_Clt_State::NotInitialized; + u16 dlp_channel_handle{}; + std::atomic_bool is_connected = false; + u32 dlp_units_downloaded = 0x0, dlp_units_total = 0x0; + u64 dlp_download_child_tid = 0x0; + u32 title_info_index = 0; + u32 max_title_info = 0; ///< once we receive x beacons, we will no longer parse any other + ///< beacons until at least one tinfo buf element is cleared + bool is_scanning = false; + constexpr static inline int beacon_scan_interval_ms = 1000; + std::vector> scanned_title_info; + std::map + ignore_servers_list; // ignore servers which give us bad broadcast data + u64 scan_title_id_filter; + Network::MacAddress scan_mac_address_filter; + Network::MacAddress host_mac_address; + constexpr static inline u16 dlp_net_info_channel = 0x1; + constexpr static inline u16 dlp_bind_node_id = 0x1; + constexpr static inline u32 dlp_recv_buffer_size = 0x3c00; + constexpr static inline u8 dlp_broadcast_data_channel = 0x1; + constexpr static inline u8 dlp_client_data_channel = 0x2; + constexpr static inline u8 dlp_host_network_node_id = 0x1; + + Core::TimingEventType* beacon_scan_event; + + std::mutex beacon_mutex; + std::recursive_mutex title_info_mutex; + std::recursive_mutex clt_state_mutex; + + std::thread client_connection_worker; + + bool is_downloading_content; + struct ReceivedFragment { + u32 index; + std::vector content; + bool operator<(const ReceivedFragment& o) const { + return index < o.index; + } + }; + u16 current_content_block; + + void InitializeCltBase(u32 shared_mem_size, u32 max_beacons, u32 constant_mem_size, + std::shared_ptr shared_mem, + std::shared_ptr event, DLP_Username username); + void FinalizeCltBase(); + void GenerateChannelHandle(); + u32 GetCltState(); + void BeaconScanCallback(std::uintptr_t user_data, s64 cycles_late); + void CacheBeaconTitleInfo(Network::WifiPacket& beacon); + int GetCachedTitleInfoIdx(Network::MacAddress mac_addr); + bool TitleInfoIsCached(Network::MacAddress mac_addr); + DLPServerInfo GetDLPServerInfoFromRawBeacon(Network::WifiPacket& beacon); + bool NeedsContentDownload(Network::MacAddress mac_addr); + bool InstallEncryptedCIAFromFragments(std::set& frags); + void DisconnectFromServer(); + bool IsIdling(); + + void GetMyStatus(Kernel::HLERequestContext& ctx); + void GetChannels(Kernel::HLERequestContext& ctx); + void GetTitleInfo(Kernel::HLERequestContext& ctx); + void GetTitleInfoInOrder(Kernel::HLERequestContext& ctx); + void StartScan(Kernel::HLERequestContext& ctx); + void StopScan(Kernel::HLERequestContext& ctx); + void DeleteScanInfo(Kernel::HLERequestContext& ctx); + void GetServerInfo(Kernel::HLERequestContext& ctx); + void StartSession(Kernel::HLERequestContext& ctx); + void StopSession(Kernel::HLERequestContext& ctx); + void GetConnectingNodes(Kernel::HLERequestContext& ctx); + void GetNodeInfo(Kernel::HLERequestContext& ctx); + void GetWirelessRebootPassphrase(Kernel::HLERequestContext& ctx); +}; + +} // namespace Service::DLP diff --git a/src/core/hle/service/dlp/dlp_crypto.cpp b/src/core/hle/service/dlp/dlp_crypto.cpp new file mode 100644 index 000000000..4cefd3516 --- /dev/null +++ b/src/core/hle/service/dlp/dlp_crypto.cpp @@ -0,0 +1,31 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "common/archives.h" +#include "common/logging/log.h" +#include "core/core.h" +#include "core/hle/ipc_helpers.h" +#include "core/hle/service/dlp/dlp_base.h" +#include "core/hle/service/ssl/ssl_c.h" +#include "core/hw/aes/arithmetic128.h" +#include "core/hw/aes/key.h" + +namespace Service::DLP { + +void DLP_Base::DLPEncryptCTR(void* _out, size_t size, const u8* iv_ctr) { + auto out = reinterpret_cast(_out); + memset(out, 0, size); + + HW::AES::SelectDlpNfcKeyYIndex(HW::AES::DlpNfcKeyY::Dlp); + HW::AES::AESKey key = HW::AES::GetNormalKey(HW::AES::DLPNFCDataKey); + + // AlgorithmType::CTR_Encrypt + CryptoPP::CTR_Mode::Encryption aes; + aes.SetKeyWithIV(key.data(), CryptoPP::AES::BLOCKSIZE, iv_ctr); + aes.ProcessData(out, out, size); +} + +} // namespace Service::DLP diff --git a/src/core/hle/service/dlp/dlp_fkcl.cpp b/src/core/hle/service/dlp/dlp_fkcl.cpp index ed247427d..8576d80a3 100644 --- a/src/core/hle/service/dlp/dlp_fkcl.cpp +++ b/src/core/hle/service/dlp/dlp_fkcl.cpp @@ -1,8 +1,9 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include "common/archives.h" +#include "core/core.h" #include "core/hle/ipc_helpers.h" #include "core/hle/service/dlp/dlp_fkcl.h" @@ -10,26 +11,71 @@ SERIALIZE_EXPORT_IMPL(Service::DLP::DLP_FKCL) namespace Service::DLP { -DLP_FKCL::DLP_FKCL() : ServiceFramework("dlp:FKCL", 1) { +std::shared_ptr DLP_FKCL::GetServiceFrameworkSharedPtr() { + return shared_from_this(); +} + +void DLP_FKCL::Initialize(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + u32 shared_mem_size = rp.Pop(); + u32 max_beacons = rp.Pop(); + constexpr u32 constant_mem_size = 0; + auto [shared_mem, event] = rp.PopObjects(); + + InitializeCltBase(shared_mem_size, max_beacons, constant_mem_size, shared_mem, event, + String16AsDLPUsername(GetCFG()->GetUsername())); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +void DLP_FKCL::InitializeWithName(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + u32 shared_mem_size = rp.Pop(); + u32 max_beacons = rp.Pop(); + constexpr u32 constant_mem_size = 0; + auto username = rp.PopRaw>(); + rp.Skip(1, false); // possible null terminator or unk flags + auto [shared_mem, event] = rp.PopObjects(); + + InitializeCltBase(shared_mem_size, max_beacons, constant_mem_size, shared_mem, event, username); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +void DLP_FKCL::Finalize(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + FinalizeCltBase(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +DLP_FKCL::DLP_FKCL() + : ServiceFramework("dlp:FKCL", 1), DLP_Clt_Base(Core::System::GetInstance(), "FKCL") { static const FunctionInfo functions[] = { // clang-format off - {0x0001, nullptr, "Initialize"}, - {0x0002, nullptr, "Finalize"}, - {0x0003, nullptr, "GetEventDesc"}, - {0x0004, nullptr, "GetChannels"}, - {0x0005, nullptr, "StartScan"}, - {0x0006, nullptr, "StopScan"}, - {0x0007, nullptr, "GetServerInfo"}, - {0x0008, nullptr, "GetTitleInfo"}, - {0x0009, nullptr, "GetTitleInfoInOrder"}, - {0x000A, nullptr, "DeleteScanInfo"}, - {0x000B, nullptr, "StartFakeSession"}, - {0x000C, nullptr, "GetMyStatus"}, - {0x000D, nullptr, "GetConnectingNodes"}, - {0x000E, nullptr, "GetNodeInfo"}, - {0x000F, nullptr, "GetWirelessRebootPassphrase"}, - {0x0010, nullptr, "StopSession"}, - {0x0011, nullptr, "Initialize2"}, + {0x0001, &DLP_FKCL::Initialize, "Initialize"}, + {0x0002, &DLP_FKCL::Finalize, "Finalize"}, + {0x0003, &DLP_FKCL::GetEventDescription, "GetEventDescription"}, + {0x0004, &DLP_FKCL::GetChannels, "GetChannels"}, + {0x0005, &DLP_FKCL::StartScan, "StartScan"}, + {0x0006, &DLP_FKCL::StopScan, "StopScan"}, + {0x0007, &DLP_FKCL::GetServerInfo, "GetServerInfo"}, + {0x0008, &DLP_FKCL::GetTitleInfo, "GetTitleInfo"}, + {0x0009, &DLP_FKCL::GetTitleInfoInOrder, "GetTitleInfoInOrder"}, + {0x000A, &DLP_FKCL::DeleteScanInfo, "DeleteScanInfo"}, + {0x000B, &DLP_FKCL::StartSession, "StartFakeSession"}, + {0x000C, &DLP_FKCL::GetMyStatus, "GetMyStatus"}, + {0x000D, &DLP_FKCL::GetConnectingNodes, "GetConnectingNodes"}, + {0x000E, &DLP_FKCL::GetNodeInfo, "GetNodeInfo"}, + {0x000F, &DLP_FKCL::GetWirelessRebootPassphrase, "GetWirelessRebootPassphrase"}, + {0x0010, &DLP_FKCL::StopSession, "StopSession"}, + {0x0011, &DLP_FKCL::InitializeWithName, "InitializeWithName"}, // clang-format on }; diff --git a/src/core/hle/service/dlp/dlp_fkcl.h b/src/core/hle/service/dlp/dlp_fkcl.h index c05a77b49..74d41f4ac 100644 --- a/src/core/hle/service/dlp/dlp_fkcl.h +++ b/src/core/hle/service/dlp/dlp_fkcl.h @@ -1,20 +1,31 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once #include "core/hle/service/service.h" +#include "dlp_clt_base.h" namespace Service::DLP { -class DLP_FKCL final : public ServiceFramework { +class DLP_FKCL final : public ServiceFramework, public DLP_Clt_Base { public: DLP_FKCL(); ~DLP_FKCL() = default; + virtual std::shared_ptr GetServiceFrameworkSharedPtr(); + private: SERVICE_SERIALIZATION_SIMPLE + + virtual bool IsFKCL() { + return true; + } + + void Initialize(Kernel::HLERequestContext& ctx); + void Finalize(Kernel::HLERequestContext& ctx); + void InitializeWithName(Kernel::HLERequestContext& ctx); }; } // namespace Service::DLP diff --git a/src/core/hle/service/dlp/dlp_srvr.cpp b/src/core/hle/service/dlp/dlp_srvr.cpp index b82d3e286..65632ca53 100644 --- a/src/core/hle/service/dlp/dlp_srvr.cpp +++ b/src/core/hle/service/dlp/dlp_srvr.cpp @@ -1,30 +1,55 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include "common/archives.h" #include "common/common_types.h" #include "common/logging/log.h" +#include "core/core.h" #include "core/hle/ipc_helpers.h" #include "core/hle/result.h" #include "core/hle/service/dlp/dlp_srvr.h" +#include "core/hle/service/fs/fs_user.h" SERIALIZE_EXPORT_IMPL(Service::DLP::DLP_SRVR) namespace Service::DLP { +std::shared_ptr DLP_SRVR::GetServiceFrameworkSharedPtr() { + return shared_from_this(); +} + void DLP_SRVR::IsChild(Kernel::HLERequestContext& ctx) { + auto fs = system.ServiceManager().GetService("fs:USER"); + IPC::RequestParser rp(ctx); - rp.Skip(1, false); + u32 process_id = rp.Pop(); + + bool child; + if (!fs) { + LOG_CRITICAL(Service_DLP, "Could not get direct pointer fs:USER (sm returned null)"); + } + auto title_info = fs->GetProgramLaunchInfo(process_id); + + if (title_info) { + // check if tid corresponds to dlp filter + u32 tid[2]; + memcpy(tid, &title_info->program_id, sizeof(tid)); + LOG_INFO(Service_DLP, "Checked on tid high: {:x} (low {:x})", tid[1], tid[0]); + child = (tid[1] & 0xFFFFC000) == 0x40000 && (tid[1] & 0xFFFF) == 0x1; + } else { // child not found + child = false; + LOG_ERROR(Service_DLP, + "Could not determine program id from process id. (process id not found: {:x})", + process_id); + } IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); rb.Push(ResultSuccess); - rb.Push(false); - - LOG_WARNING(Service_DLP, "(STUBBED) called"); + rb.Push(child); } -DLP_SRVR::DLP_SRVR() : ServiceFramework("dlp:SRVR", 1) { +DLP_SRVR::DLP_SRVR() : ServiceFramework("dlp:SRVR", 1), DLP_Base(Core::System::GetInstance()) { static const FunctionInfo functions[] = { // clang-format off {0x0001, nullptr, "Initialize"}, diff --git a/src/core/hle/service/dlp/dlp_srvr.h b/src/core/hle/service/dlp/dlp_srvr.h index 625740d2f..d9b4fdd42 100644 --- a/src/core/hle/service/dlp/dlp_srvr.h +++ b/src/core/hle/service/dlp/dlp_srvr.h @@ -1,18 +1,24 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once #include "core/hle/service/service.h" +#include "dlp_base.h" namespace Service::DLP { -class DLP_SRVR final : public ServiceFramework { +class DLP_SRVR final : public ServiceFramework, public DLP_Base { public: DLP_SRVR(); ~DLP_SRVR() = default; + virtual std::shared_ptr GetServiceFrameworkSharedPtr(); + virtual bool IsHost() { + return true; + } + private: void IsChild(Kernel::HLERequestContext& ctx); diff --git a/src/core/hle/service/http/http_c.cpp b/src/core/hle/service/http/http_c.cpp index 3a1dc1410..b1f47373b 100644 --- a/src/core/hle/service/http/http_c.cpp +++ b/src/core/hle/service/http/http_c.cpp @@ -1773,9 +1773,8 @@ void HTTP_C::SetClientCertContext(Kernel::HLERequestContext& ctx) { void HTTP_C::GetSSLError(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const u32 context_handle = rp.Pop(); - const u32 unk = rp.Pop(); - LOG_WARNING(Service_HTTP, "(STUBBED) called, context_handle={}, unk={}", context_handle, unk); + LOG_WARNING(Service_HTTP, "(STUBBED) called, context_handle={}", context_handle); [[maybe_unused]] Context& http_context = GetContext(context_handle); diff --git a/src/core/hle/service/nwm/nwm_uds.cpp b/src/core/hle/service/nwm/nwm_uds.cpp index 43547ee42..fadbe3f3e 100644 --- a/src/core/hle/service/nwm/nwm_uds.cpp +++ b/src/core/hle/service/nwm/nwm_uds.cpp @@ -226,7 +226,7 @@ void NWM_UDS::HandleEAPoLPacket(const Network::WifiPacket& packet) { auto node = DeserializeNodeInfo(eapol_start.node); - if (eapol_start.conn_type == ConnectionType::Client) { + if (eapol_start.connection_type == ConnectionType::Client) { // Get an unused network node id u16 node_id = GetNextAvailableNodeId(); node.network_node_id = node_id; @@ -244,13 +244,13 @@ void NWM_UDS::HandleEAPoLPacket(const Network::WifiPacket& packet) { node_map[packet.transmitter_address].spec = false; BroadcastNodeMap(); - } else if (eapol_start.conn_type == ConnectionType::Spectator) { + } else if (eapol_start.connection_type == ConnectionType::Spectator) { node_map[packet.transmitter_address].node_id = NodeIDSpec; node_map[packet.transmitter_address].connected = true; node_map[packet.transmitter_address].spec = true; } else { LOG_ERROR(Service_NWM, "Client tried connecting with unknown connection type: 0x{:x}", - static_cast(eapol_start.conn_type)); + static_cast(eapol_start.connection_type)); } // Send the EAPoL-Logoff packet. @@ -595,9 +595,7 @@ boost::optional NWM_UDS::GetNodeMacAddress(u16 dest_node_id return destination->first; } -void NWM_UDS::Shutdown(Kernel::HLERequestContext& ctx) { - IPC::RequestParser rp(ctx); - +void NWM_UDS::ShutdownHLE() { initialized = false; for (auto& bind_node : channel_data) { @@ -607,12 +605,92 @@ void NWM_UDS::Shutdown(Kernel::HLERequestContext& ctx) { node_map.clear(); recv_buffer_memory.reset(); +} + +void NWM_UDS::Shutdown(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + ShutdownHLE(); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(ResultSuccess); LOG_DEBUG(Service_NWM, "called"); } +void NWM_UDS::RecvBeaconBroadcastData(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + u32 out_buffer_size = rp.Pop(); + + // scan input struct + u32 unk1 = rp.Pop(); + u32 unk2 = rp.Pop(); + + MacAddress mac_address; + rp.PopRaw(mac_address); + + // uninitialized data in scan input struct + rp.Skip(9, false); + + // end scan input struct + + u32 wlan_comm_id = rp.Pop(); + u32 id = rp.Pop(); + // From 3dbrew: + // 'Official user processes create a new event handle which is then passed to this command. + // However, those user processes don't save that handle anywhere afterwards.' + // So we don't save/use that event too. + std::shared_ptr input_event = rp.PopObject(); + + Kernel::MappedBuffer& out_buffer = rp.PopMappedBuffer(); + ASSERT(out_buffer.GetSize() == out_buffer_size); + + std::size_t cur_buffer_size = sizeof(BeaconDataReplyHeader); + + auto beacons = GetReceivedBeacons(mac_address); + + BeaconDataReplyHeader data_reply_header{}; + data_reply_header.total_entries = static_cast(beacons.size()); + data_reply_header.max_output_size = out_buffer_size; + + // Write each of the received beacons into the buffer + for (const auto& beacon : beacons) { + BeaconEntryHeader entry{}; + // TODO(Subv): Figure out what this size is used for. + entry.unk_size = static_cast(sizeof(BeaconEntryHeader) + beacon.data.size()); + entry.total_size = static_cast(sizeof(BeaconEntryHeader) + beacon.data.size()); + entry.wifi_channel = beacon.channel; + entry.header_size = sizeof(BeaconEntryHeader); + entry.mac_address = beacon.transmitter_address; + + ASSERT(cur_buffer_size < out_buffer_size); + + out_buffer.Write(&entry, cur_buffer_size, sizeof(BeaconEntryHeader)); + cur_buffer_size += sizeof(BeaconEntryHeader); + const unsigned char* beacon_data = beacon.data.data(); + out_buffer.Write(beacon_data, cur_buffer_size, beacon.data.size()); + cur_buffer_size += beacon.data.size(); + } + + // Update the total size in the structure and write it to the buffer again. + data_reply_header.total_size = static_cast(cur_buffer_size); + out_buffer.Write(&data_reply_header, 0, sizeof(BeaconDataReplyHeader)); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ResultSuccess); + rb.PushMappedBuffer(out_buffer); + + // on a real 3ds this is about 0.38 seconds + static constexpr std::chrono::nanoseconds UDSBeaconScanInterval{300000000}; + + ctx.SleepClientThread("uds::RecvBeaconBroadcastData", UDSBeaconScanInterval, nullptr); + + LOG_DEBUG(Service_NWM, + "called out_buffer_size=0x{:08X}, wlan_comm_id=0x{:08X}, id=0x{:08X}," + "unk1=0x{:08X}, unk2=0x{:08X}, offset={}", + out_buffer_size, wlan_comm_id, id, unk1, unk2, cur_buffer_size); +} + ResultVal> NWM_UDS::Initialize( u32 sharedmem_size, const NodeInfo& node, u16 version, std::shared_ptr sharedmem) { @@ -671,25 +749,41 @@ void NWM_UDS::InitializeDeprecated(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_NWM, "called sharedmem_size=0x{:08X}", sharedmem_size); } +ConnectionStatus NWM_UDS::GetConnectionStatusHLE() { + std::scoped_lock lock(connection_status_mutex); + ConnectionStatus cs_out = connection_status; + + // Reset the bitmask of changed nodes after each call to this + // function to prevent falsely informing games of outstanding + // changes in subsequent calls. + // TODO(Subv): Find exactly where the NWM module resets this value. + connection_status.changed_nodes = 0; + + return cs_out; +} + void NWM_UDS::GetConnectionStatus(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); IPC::RequestBuilder rb = rp.MakeBuilder(13, 0); rb.Push(ResultSuccess); - { - std::scoped_lock lock(connection_status_mutex); - rb.PushRaw(connection_status); - - // Reset the bitmask of changed nodes after each call to this - // function to prevent falsely informing games of outstanding - // changes in subsequent calls. - // TODO(Subv): Find exactly where the NWM module resets this value. - connection_status.changed_nodes = 0; - } + rb.PushRaw(GetConnectionStatusHLE()); LOG_DEBUG(Service_NWM, "called"); } +std::unique_ptr NWM_UDS::GetNodeInformationHLE(u16 network_node_id) { + std::scoped_lock lock(connection_status_mutex); + auto itr = + std::find_if(node_info.begin(), node_info.end(), [network_node_id](const NodeInfo& node) { + return node.network_node_id == network_node_id; + }); + if (itr == node_info.end()) { + return nullptr; + } + return std::make_unique(*itr); +} + void NWM_UDS::GetNodeInformation(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); u16 network_node_id = rp.Pop(); @@ -702,12 +796,8 @@ void NWM_UDS::GetNodeInformation(Kernel::HLERequestContext& ctx) { } { - std::scoped_lock lock(connection_status_mutex); - auto itr = std::find_if(node_info.begin(), node_info.end(), - [network_node_id](const NodeInfo& node) { - return node.network_node_id == network_node_id; - }); - if (itr == node_info.end()) { + auto node = GetNodeInformationHLE(network_node_id); + if (!node) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(Result(ErrorDescription::NotFound, ErrorModule::UDS, ErrorSummary::WrongArgument, ErrorLevel::Status)); @@ -716,46 +806,31 @@ void NWM_UDS::GetNodeInformation(Kernel::HLERequestContext& ctx) { IPC::RequestBuilder rb = rp.MakeBuilder(11, 0); rb.Push(ResultSuccess); - rb.PushRaw(*itr); + rb.PushRaw(*node); } LOG_DEBUG(Service_NWM, "called"); } -void NWM_UDS::Bind(Kernel::HLERequestContext& ctx) { - IPC::RequestParser rp(ctx); - - u32 bind_node_id = rp.Pop(); - u32 recv_buffer_size = rp.Pop(); - u8 data_channel = rp.Pop(); - u16 network_node_id = rp.Pop(); - - LOG_DEBUG(Service_NWM, "called"); - +std::pair> NWM_UDS::BindHLE(u32 bind_node_id, + u32 recv_buffer_size, + u8 data_channel, + u16 network_node_id) { if (data_channel == 0 || bind_node_id == 0) { - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(Result(ErrorDescription::NotAuthorized, ErrorModule::UDS, - ErrorSummary::WrongArgument, ErrorLevel::Usage)); LOG_WARNING(Service_NWM, "data_channel = {}, bind_node_id = {}", data_channel, bind_node_id); - return; + return std::make_pair(ResultStatus::BindError_ArgsZero, nullptr); } constexpr std::size_t MaxBindNodes = 16; if (channel_data.size() >= MaxBindNodes) { - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(Result(ErrorDescription::OutOfMemory, ErrorModule::UDS, ErrorSummary::OutOfResource, - ErrorLevel::Status)); LOG_WARNING(Service_NWM, "max bind nodes"); - return; + return std::make_pair(ResultStatus::BindError_MaxBinds, nullptr); } constexpr u32 MinRecvBufferSize = 0x5F4; if (recv_buffer_size < MinRecvBufferSize) { - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(Result(ErrorDescription::TooLarge, ErrorModule::UDS, ErrorSummary::WrongArgument, - ErrorLevel::Usage)); LOG_WARNING(Service_NWM, "MinRecvBufferSize"); - return; + return std::make_pair(ResultStatus::BindError_RecvBufferTooLarge, nullptr); } // Create a new event for this bind node. @@ -766,12 +841,61 @@ void NWM_UDS::Bind(Kernel::HLERequestContext& ctx) { ASSERT(channel_data.find(data_channel) == channel_data.end()); // TODO(B3N30): Support more than one bind node per channel. channel_data[data_channel] = {bind_node_id, data_channel, network_node_id, event}; + return std::make_pair(ResultStatus::ResultSuccess, std::move(event)); +} + +void NWM_UDS::Bind(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + u32 bind_node_id = rp.Pop(); + u32 recv_buffer_size = rp.Pop(); + u8 data_channel = rp.Pop(); + u16 network_node_id = rp.Pop(); + + auto [ret, event] = BindHLE(bind_node_id, recv_buffer_size, data_channel, network_node_id); + + switch (ret) { + case ResultStatus::BindError_ArgsZero: { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NotAuthorized, ErrorModule::UDS, + ErrorSummary::WrongArgument, ErrorLevel::Usage)); + return; + } + case ResultStatus::BindError_MaxBinds: { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::OutOfMemory, ErrorModule::UDS, ErrorSummary::OutOfResource, + ErrorLevel::Status)); + return; + } + case ResultStatus::BindError_RecvBufferTooLarge: { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::TooLarge, ErrorModule::UDS, ErrorSummary::WrongArgument, + ErrorLevel::Usage)); + return; + } + default:; + } IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); rb.Push(ResultSuccess); rb.PushCopyObjects(event); } +void NWM_UDS::UnbindHLE(u32 bind_node_id) { + std::scoped_lock lock(connection_status_mutex); + + auto itr = + std::find_if(channel_data.begin(), channel_data.end(), [bind_node_id](const auto& data) { + return data.second.bind_node_id == bind_node_id; + }); + + if (itr != channel_data.end()) { + // TODO(B3N30): Check out what Unbind does if the bind_node_id wasn't in the map + itr->second.event->Signal(); + channel_data.erase(itr); + } +} + void NWM_UDS::Unbind(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); @@ -783,18 +907,7 @@ void NWM_UDS::Unbind(Kernel::HLERequestContext& ctx) { return; } - std::scoped_lock lock(connection_status_mutex); - - auto itr = - std::find_if(channel_data.begin(), channel_data.end(), [bind_node_id](const auto& data) { - return data.second.bind_node_id == bind_node_id; - }); - - if (itr != channel_data.end()) { - // TODO(B3N30): Check out what Unbind does if the bind_node_id wasn't in the map - itr->second.event->Signal(); - channel_data.erase(itr); - } + UnbindHLE(bind_node_id); IPC::RequestBuilder rb = rp.MakeBuilder(5, 0); rb.Push(ResultSuccess); @@ -959,10 +1072,22 @@ void NWM_UDS::EjectClient(Kernel::HLERequestContext& ctx) { } } +Result NWM_UDS::UpdateNetworkAttributeHLE(u16 node_bitmask, u8 flag) { + [[maybe_unused]] constexpr u8 flag_disconnect_and_block_non_bitmasked_nodes = 0x1; + + // stubbed + + return ResultSuccess; +} + void NWM_UDS::UpdateNetworkAttribute(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - rp.Skip(2, false); - LOG_WARNING(Service_NWM, "stubbed"); + + u16 bitmask = rp.Pop(); + u8 flag = rp.Pop(); + + auto res = UpdateNetworkAttributeHLE(bitmask, flag); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(ResultSuccess); } @@ -1005,52 +1130,6 @@ void NWM_UDS::DestroyNetwork(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_NWM, "called"); } -void NWM_UDS::DisconnectNetwork(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_NWM, "disconnecting from network"); - IPC::RequestParser rp(ctx); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - - using Network::WifiPacket; - WifiPacket deauth; - { - std::scoped_lock lock(connection_status_mutex); - if (connection_status.status == NetworkStatus::ConnectedAsHost) { - // A real 3ds makes strange things here. We do the same - u16_le tmp_node_id = connection_status.network_node_id; - connection_status = {}; - connection_status.status = NetworkStatus::ConnectedAsHost; - connection_status.network_node_id = tmp_node_id; - node_map.clear(); - LOG_DEBUG(Service_NWM, "called as a host"); - rb.Push(Result(ErrCodes::WrongStatus, ErrorModule::UDS, ErrorSummary::InvalidState, - ErrorLevel::Status)); - return; - } - u16_le tmp_node_id = connection_status.network_node_id; - connection_status = {}; - connection_status.status = NetworkStatus::NotConnected; - connection_status.network_node_id = tmp_node_id; - node_map.clear(); - connection_status_event->Signal(); - - deauth.channel = network_channel; - // TODO(B3N30): Add disconnect reason - deauth.data = {}; - deauth.destination_address = network_info.host_mac_address; - deauth.type = WifiPacket::PacketType::Deauthentication; - } - - SendPacket(deauth); - - for (auto& bind_node : channel_data) { - bind_node.second.event->Signal(); - } - channel_data.clear(); - - rb.Push(ResultSuccess); - LOG_DEBUG(Service_NWM, "called"); -} - void NWM_UDS::SendTo(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); @@ -1062,32 +1141,54 @@ void NWM_UDS::SendTo(Kernel::HLERequestContext& ctx) { u8 flags = rp.Pop(); std::vector input_buffer = rp.PopStaticBuffer(); - ASSERT(input_buffer.size() >= data_size); - input_buffer.resize(data_size); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + auto res = SendToHLE(dest_node_id, data_channel, data_size, flags, input_buffer); + + switch (res) { + case ResultStatus::SendError_PacketSizeTooLarge: + rb.Push(Result(ErrorDescription::TooLarge, ErrorModule::UDS, ErrorSummary::WrongArgument, + ErrorLevel::Usage)); + return; + case ResultStatus::SendError_NotConnected: + rb.Push(Result(ErrorDescription::NotAuthorized, ErrorModule::UDS, + ErrorSummary::InvalidState, ErrorLevel::Status)); + return; + case ResultStatus::SendError_BadNode: + case ResultStatus::SendError_BadMacAddress: + rb.Push(Result(ErrorDescription::NotFound, ErrorModule::UDS, ErrorSummary::WrongArgument, + ErrorLevel::Status)); + return; + default:; + } + + rb.Push(ResultSuccess); +} + +ResultStatus NWM_UDS::SendToHLE(u32 dest_node_id, u8 data_channel, u32 data_size, u8 flags, + std::vector input_buffer) { + ASSERT(input_buffer.size() >= data_size); + input_buffer.resize(data_size); + std::scoped_lock lock(connection_status_mutex); if (connection_status.status != NetworkStatus::ConnectedAsClient && connection_status.status != NetworkStatus::ConnectedAsHost) { - rb.Push(Result(ErrorDescription::NotAuthorized, ErrorModule::UDS, - ErrorSummary::InvalidState, ErrorLevel::Status)); - return; + LOG_ERROR(Service_NWM, + "You are not connected as a client or a host. (you are connected as type {})", + connection_status.status); + return ResultStatus::SendError_NotConnected; } // There should never be a dest_node_id of 0 if (dest_node_id == 0) { - rb.Push(Result(ErrorDescription::NotFound, ErrorModule::UDS, ErrorSummary::WrongArgument, - ErrorLevel::Status)); LOG_ERROR(Service_NWM, "dest_node_id is 0"); - return; + return ResultStatus::SendError_BadNode; } if (dest_node_id == connection_status.network_node_id) { LOG_ERROR(Service_NWM, "tried to send packet to itself"); - rb.Push(Result(ErrorDescription::NotFound, ErrorModule::UDS, ErrorSummary::WrongArgument, - ErrorLevel::Status)); - return; + return ResultStatus::SendError_BadNode; } if (flags >> 2) { @@ -1096,18 +1197,16 @@ void NWM_UDS::SendTo(Kernel::HLERequestContext& ctx) { auto dest_address = GetNodeMacAddress(dest_node_id, flags); if (!dest_address) { - rb.Push(Result(ErrorDescription::NotFound, ErrorModule::UDS, ErrorSummary::WrongArgument, - ErrorLevel::Status)); - return; + LOG_ERROR(Service_NWM, "Destination address was 0"); + return ResultStatus::SendError_BadMacAddress; } constexpr std::size_t MaxSize = 0x5C6; if (data_size > MaxSize) { - rb.Push(Result(ErrorDescription::TooLarge, ErrorModule::UDS, ErrorSummary::WrongArgument, - ErrorLevel::Usage)); - return; + LOG_ERROR(Service_NWM, "Data size was greater than the max packet size {} > {}", data_size, + MaxSize); + return ResultStatus::SendError_PacketSizeTooLarge; } - // TODO(B3N30): Increment the sequence number after each sent packet. u16 sequence_number = 0; std::vector data_payload = @@ -1126,7 +1225,7 @@ void NWM_UDS::SendTo(Kernel::HLERequestContext& ctx) { SendPacket(packet); - rb.Push(ResultSuccess); + return ResultStatus::ResultSuccess; } void NWM_UDS::PullPacket(Kernel::HLERequestContext& ctx) { @@ -1135,7 +1234,47 @@ void NWM_UDS::PullPacket(Kernel::HLERequestContext& ctx) { u32 bind_node_id = rp.Pop(); u32 max_out_buff_size_aligned = rp.Pop(); u32 max_out_buff_size = rp.Pop(); + std::vector output_buffer; + SecureDataHeader secure_data; + + auto ret = PullPacketHLE(bind_node_id, max_out_buff_size, max_out_buff_size_aligned, + output_buffer, &secure_data); + + switch (ret.error()) { + case ResultStatus::RecvError_NotConnected: { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NotAuthorized, ErrorModule::UDS, + ErrorSummary::InvalidState, ErrorLevel::Status)); + return; + } + case ResultStatus::RecvError_BadNode: { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NotAuthorized, ErrorModule::UDS, + ErrorSummary::InvalidState, ErrorLevel::Status)); + return; + } + case ResultStatus::RecvError_PacketSizeTooLarge: { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::TooLarge, ErrorModule::UDS, ErrorSummary::WrongArgument, + ErrorLevel::Usage)); + return; + } + default:; + } + + IPC::RequestBuilder rb = rp.MakeBuilder(3, 2); + + rb.Push(ResultSuccess); + rb.Push(*ret); // return is data size if gt/eq to 0 + rb.Push(secure_data.src_node_id); + rb.PushStaticBuffer(std::move(output_buffer), 0); +} + +Common::Expected NWM_UDS::PullPacketHLE(u32 bind_node_id, u32 max_out_buff_size, + u32 max_out_buff_size_aligned, + std::vector& output_buffer, + void* secure_data_out) { // This size is hard coded into the uds module. We don't know the meaning yet. u32 buff_size = std::min(max_out_buff_size_aligned, 0x172) << 2; @@ -1143,10 +1282,8 @@ void NWM_UDS::PullPacket(Kernel::HLERequestContext& ctx) { if (connection_status.status != NetworkStatus::ConnectedAsHost && connection_status.status != NetworkStatus::ConnectedAsClient && connection_status.status != NetworkStatus::ConnectedAsSpectator) { - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(Result(ErrorDescription::NotAuthorized, ErrorModule::UDS, - ErrorSummary::InvalidState, ErrorLevel::Status)); - return; + LOG_ERROR(Service_NWM, "Not connected yet."); + return Common::Unexpected(ResultStatus::RecvError_NotConnected); } auto channel = @@ -1155,20 +1292,13 @@ void NWM_UDS::PullPacket(Kernel::HLERequestContext& ctx) { }); if (channel == channel_data.end()) { - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(Result(ErrorDescription::NotAuthorized, ErrorModule::UDS, - ErrorSummary::WrongArgument, ErrorLevel::Usage)); - return; + LOG_ERROR(Service_NWM, "Could not find channel bn 0x{:x}.", bind_node_id); + return Common::Unexpected(ResultStatus::RecvError_BadNode); } if (channel->second.received_packets.empty()) { - std::vector output_buffer(buff_size); - IPC::RequestBuilder rb = rp.MakeBuilder(3, 2); - rb.Push(ResultSuccess); - rb.Push(0); - rb.Push(0); - rb.PushStaticBuffer(std::move(output_buffer), 0); - return; + output_buffer.resize(buff_size); + return int(0); } const auto& next_packet = channel->second.received_packets.front(); @@ -1176,26 +1306,22 @@ void NWM_UDS::PullPacket(Kernel::HLERequestContext& ctx) { auto secure_data = ParseSecureDataHeader(next_packet); auto data_size = secure_data.GetActualDataSize(); - if (data_size > max_out_buff_size) { - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(Result(ErrorDescription::TooLarge, ErrorModule::UDS, ErrorSummary::WrongArgument, - ErrorLevel::Usage)); - return; + if (secure_data_out) { + *reinterpret_cast(secure_data_out) = secure_data; } - IPC::RequestBuilder rb = rp.MakeBuilder(3, 2); + if (data_size > max_out_buff_size) { + LOG_ERROR(Service_NWM, "Data size was too large."); + return Common::Unexpected(ResultStatus::RecvError_PacketSizeTooLarge); + } + output_buffer.resize(buff_size); - std::vector output_buffer(buff_size); // Write the actual data. std::memcpy(output_buffer.data(), next_packet.data() + sizeof(LLCHeader) + sizeof(SecureDataHeader), data_size); - rb.Push(ResultSuccess); - rb.Push(data_size); - rb.Push(secure_data.src_node_id); - rb.PushStaticBuffer(std::move(output_buffer), 0); - channel->second.received_packets.pop_front(); + return int(data_size); } void NWM_UDS::GetChannel(Kernel::HLERequestContext& ctx) { @@ -1219,8 +1345,13 @@ public: void WakeUp(std::shared_ptr thread, Kernel::HLERequestContext& ctx, Kernel::ThreadWakeupReason reason) { - // TODO(B3N30): Add error handling for host full and timeout IPC::RequestBuilder rb(ctx, command_id, 1, 0); + if (reason == Kernel::ThreadWakeupReason::Timeout) { + LOG_ERROR(Service_NWM, "timed out when trying to connect to UDS server"); + rb.Push(Result(ErrorDescription::Timeout, ErrorModule::UDS, ErrorSummary::Canceled, + ErrorLevel::Status)); + return; + } rb.Push(ResultSuccess); LOG_DEBUG(Service_NWM, "connection sequence finished"); } @@ -1237,95 +1368,25 @@ private: friend class boost::serialization::access; }; -void NWM_UDS::RecvBeaconBroadcastData(Kernel::HLERequestContext& ctx) { - IPC::RequestParser rp(ctx); +void NWM_UDS::ConnectToNetworkHLE(NetworkInfo net_info, u8 connection_type, + std::vector passphrase) { + network_info = net_info; - u32 out_buffer_size = rp.Pop(); + conn_type = static_cast(connection_type); - // scan input struct - u32 unk1 = rp.Pop(); - u32 unk2 = rp.Pop(); - - MacAddress mac_address; - rp.PopRaw(mac_address); - - // uninitialized data in scan input struct - rp.Skip(9, false); - - // end scan input struct - - u32 wlan_comm_id = rp.Pop(); - u32 id = rp.Pop(); - // From 3dbrew: - // 'Official user processes create a new event handle which is then passed to this command. - // However, those user processes don't save that handle anywhere afterwards.' - // So we don't save/use that event too. - std::shared_ptr input_event = rp.PopObject(); - - Kernel::MappedBuffer out_buffer = rp.PopMappedBuffer(); - ASSERT(out_buffer.GetSize() == out_buffer_size); - - std::size_t cur_buffer_size = sizeof(BeaconDataReplyHeader); - - // on a real 3ds this is about 0.38 seconds - static constexpr std::chrono::nanoseconds UDSBeaconScanInterval{300000000}; - - ctx.SleepClientThread("uds::RecvBeaconBroadcastData", UDSBeaconScanInterval, - std::make_shared(0xF)); - - // Retrieve all beacon frames that were received from the desired mac address. - auto beacons = GetReceivedBeacons(mac_address); - - BeaconDataReplyHeader data_reply_header{}; - data_reply_header.total_entries = static_cast(beacons.size()); - data_reply_header.max_output_size = out_buffer_size; - - // Write each of the received beacons into the buffer - for (const auto& beacon : beacons) { - BeaconEntryHeader entry{}; - // TODO(Subv): Figure out what this size is used for. - entry.unk_size = static_cast(sizeof(BeaconEntryHeader) + beacon.data.size()); - entry.total_size = static_cast(sizeof(BeaconEntryHeader) + beacon.data.size()); - entry.wifi_channel = beacon.channel; - entry.header_size = sizeof(BeaconEntryHeader); - entry.mac_address = beacon.transmitter_address; - - ASSERT(cur_buffer_size < out_buffer_size); - - out_buffer.Write(&entry, cur_buffer_size, sizeof(BeaconEntryHeader)); - cur_buffer_size += sizeof(BeaconEntryHeader); - const unsigned char* beacon_data = beacon.data.data(); - out_buffer.Write(beacon_data, cur_buffer_size, beacon.data.size()); - cur_buffer_size += beacon.data.size(); - } - - // Update the total size in the structure and write it to the buffer again. - data_reply_header.total_size = static_cast(cur_buffer_size); - out_buffer.Write(&data_reply_header, 0, sizeof(BeaconDataReplyHeader)); - - IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); - rb.Push(ResultSuccess); - rb.PushMappedBuffer(out_buffer); - - LOG_DEBUG(Service_NWM, - "called out_buffer_size=0x{:08X}, wlan_comm_id=0x{:08X}, id=0x{:08X}," - "unk1=0x{:08X}, unk2=0x{:08X}, offset={}", - out_buffer_size, wlan_comm_id, id, unk1, unk2, cur_buffer_size); + // Start the connection sequence + StartConnectionSequence(network_info.host_mac_address); } void NWM_UDS::ConnectToNetwork(Kernel::HLERequestContext& ctx, u16 command_id, std::span network_info_buffer, u8 connection_type, std::vector passphrase) { - network_info = {}; - std::memcpy(&network_info, network_info_buffer.data(), network_info_buffer.size()); - conn_type = static_cast(connection_type); - - // Start the connection sequence - StartConnectionSequence(network_info.host_mac_address); - - // 300 ms + NetworkInfo net_info; + std::memcpy(&net_info, network_info_buffer.data(), network_info_buffer.size()); + ConnectToNetworkHLE(net_info, connection_type, passphrase); + // Originally 300 ms, but was changed to 5s to accommodate high ping // Since this timing is handled by core_timing it could differ from the 'real world' time - static constexpr std::chrono::nanoseconds UDSConnectionTimeout{300000000}; + static constexpr std::chrono::nanoseconds UDSConnectionTimeout{5000000000}; connection_event = ctx.SleepClientThread("uds::ConnectToNetwork", UDSConnectionTimeout, std::make_shared(command_id)); @@ -1364,6 +1425,60 @@ void NWM_UDS::ConnectToNetworkDeprecated(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_NWM, "called"); } +ResultStatus NWM_UDS::DisconnectNetworkHLE() { + using Network::WifiPacket; + WifiPacket deauth; + { + std::scoped_lock lock(connection_status_mutex); + if (connection_status.status == NetworkStatus::ConnectedAsHost) { + // A real 3ds makes strange things here. We do the same + u16_le tmp_node_id = connection_status.network_node_id; + connection_status = {}; + connection_status.status = NetworkStatus::ConnectedAsHost; + connection_status.network_node_id = tmp_node_id; + node_map.clear(); + return ResultStatus::DisconError_CalledAsHost; + } + u16_le tmp_node_id = connection_status.network_node_id; + connection_status = {}; + connection_status.status = NetworkStatus::NotConnected; + connection_status.network_node_id = tmp_node_id; + node_map.clear(); + connection_status_event->Signal(); + + deauth.channel = network_channel; + // TODO(B3N30): Add disconnect reason + deauth.data = {}; + deauth.destination_address = network_info.host_mac_address; + deauth.type = WifiPacket::PacketType::Deauthentication; + } + + SendPacket(deauth); + + for (auto& bind_node : channel_data) { + bind_node.second.event->Signal(); + } + channel_data.clear(); + + return ResultStatus::ResultSuccess; +} + +void NWM_UDS::DisconnectNetwork(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_NWM, "disconnecting from network"); + IPC::RequestParser rp(ctx); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + auto res = DisconnectNetworkHLE(); + if (res == ResultStatus::DisconError_CalledAsHost) { + LOG_DEBUG(Service_NWM, "called as a host"); + rb.Push(Result(ErrCodes::WrongStatus, ErrorModule::UDS, ErrorSummary::InvalidState, + ErrorLevel::Status)); + return; + } + + rb.Push(ResultSuccess); +} + void NWM_UDS::SetApplicationData(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); @@ -1473,6 +1588,16 @@ void NWM_UDS::DecryptBeaconData(Kernel::HLERequestContext& ctx) { rb.PushStaticBuffer(std::move(output_buffer), 0); } +void NWM_UDS::EjectSpectators(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + LOG_WARNING(Service_NWM, "(STUBBED) called"); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + rb.Push(ResultSuccess); +} + // Sends a 802.11 beacon frame with information about the current network. void NWM_UDS::BeaconBroadcastCallback(std::uintptr_t user_data, s64 cycles_late) { // Don't do anything if we're not actually hosting a network @@ -1504,7 +1629,7 @@ NWM_UDS::NWM_UDS(Core::System& system) : ServiceFramework("nwm::UDS"), system(sy {0x0003, &NWM_UDS::Shutdown, "Shutdown"}, {0x0004, &NWM_UDS::BeginHostingNetworkDeprecated, "BeginHostingNetwork (deprecated)"}, {0x0005, &NWM_UDS::EjectClient, "EjectClient"}, - {0x0006, nullptr, "EjectSpectator"}, + {0x0006, &NWM_UDS::EjectSpectators, "EjectSpectators"}, {0x0007, &NWM_UDS::UpdateNetworkAttribute, "UpdateNetworkAttribute"}, {0x0008, &NWM_UDS::DestroyNetwork, "DestroyNetwork"}, {0x0009, &NWM_UDS::ConnectToNetworkDeprecated, "ConnectToNetwork (deprecated)"}, @@ -1542,8 +1667,7 @@ NWM_UDS::NWM_UDS(Core::System& system) : ServiceFramework("nwm::UDS"), system(sy MacAddress mac; - auto cfg = system.ServiceManager().GetService("cfg:u"); - if (cfg.get()) { + if (auto cfg = system.ServiceManager().GetService("cfg:u")) { auto cfg_module = cfg->GetModule(); mac = Service::CFG::MacToArray(cfg_module->GetMacAddress()); } diff --git a/src/core/hle/service/nwm/nwm_uds.h b/src/core/hle/service/nwm/nwm_uds.h index 045d4ba96..feefe8a86 100644 --- a/src/core/hle/service/nwm/nwm_uds.h +++ b/src/core/hle/service/nwm/nwm_uds.h @@ -31,10 +31,31 @@ class Event; class SharedMemory; } // namespace Kernel +namespace Service::DLP { +class DLP_Base; +class DLP_Clt_Base; +class DLP_SRVR; +} // namespace Service::DLP + // Local-WLAN service namespace Service::NWM { +enum class ResultStatus { + ResultSuccess = 0, + BindError_ArgsZero, + BindError_MaxBinds, + BindError_RecvBufferTooLarge, + DisconError_CalledAsHost, + SendError_NotConnected, + SendError_BadNode, + SendError_BadMacAddress, + SendError_PacketSizeTooLarge, + RecvError_NotConnected, + RecvError_BadNode, + RecvError_PacketSizeTooLarge, +}; + using MacAddress = std::array; const std::size_t ApplicationDataSize = 0xC8; @@ -429,6 +450,16 @@ private: */ void EjectClient(Kernel::HLERequestContext& ctx); + /** + * NWM_UDS::EjectSpectators Disconnects all spectators and prevents them from rejoining. + * Inputs: + * 0 : Command header + * Outputs: + * 0 : Return header + * 1 : Result of function, 0 on success, otherwise error code + */ + void EjectSpectators(Kernel::HLERequestContext& ctx); + /** * NWM_UDS::DecryptBeaconData service function. * Decrypts the encrypted data tags contained in the 802.11 beacons. @@ -452,12 +483,31 @@ private: u32 sharedmem_size, const NodeInfo& node, u16 version, std::shared_ptr sharedmem); + void ShutdownHLE(); + Common::Expected PullPacketHLE(u32 bind_node_id, u32 max_out_buff_size, + u32 max_out_buff_size_aligned, + std::vector& output_buffer, + void* secure_data_out); + ConnectionStatus GetConnectionStatusHLE(); + ResultStatus DisconnectNetworkHLE(); + std::pair> BindHLE(u32 bind_node_id, + u32 recv_buffer_size, + u8 data_channel, + u16 network_node_id); + void UnbindHLE(u32 bind_node_id); + std::unique_ptr GetNodeInformationHLE(u16 network_node_id); + ResultStatus SendToHLE(u32 dest_node_id, u8 data_channel, u32 data_size, u8 flags, + std::vector input_buffer); + Result UpdateNetworkAttributeHLE(u16 bitmask, u8 flag); + Result BeginHostingNetwork(std::span network_info_buffer, std::vector passphrase); void ConnectToNetwork(Kernel::HLERequestContext& ctx, u16 command_id, std::span network_info_buffer, u8 connection_type, std::vector passphrase); + void ConnectToNetworkHLE(NetworkInfo net_info, u8 connection_type, std::vector passphrase); + void BeaconBroadcastCallback(std::uintptr_t user_data, s64 cycles_late); /** @@ -576,7 +626,7 @@ private: // Mutex to synchronize access to the connection status between the emulation thread and the // network thread. - std::mutex connection_status_mutex; + std::recursive_mutex connection_status_mutex; std::shared_ptr connection_event; @@ -590,6 +640,9 @@ private: template void serialize(Archive& ar, const unsigned int); friend class boost::serialization::access; + friend class Service::DLP::DLP_Base; + friend class Service::DLP::DLP_Clt_Base; + friend class Service::DLP::DLP_SRVR; }; } // namespace Service::NWM diff --git a/src/core/hle/service/nwm/uds_data.cpp b/src/core/hle/service/nwm/uds_data.cpp index 305aec843..1606d0286 100644 --- a/src/core/hle/service/nwm/uds_data.cpp +++ b/src/core/hle/service/nwm/uds_data.cpp @@ -290,7 +290,7 @@ std::vector GenerateEAPoLStartFrame(u16 association_id, ConnectionType conn_ const NodeInfo& node_info) { EAPoLStartPacket eapol_start{}; eapol_start.association_id = association_id; - eapol_start.conn_type = conn_type; + eapol_start.connection_type = conn_type; eapol_start.node.friend_code_seed = node_info.friend_code_seed; std::copy(node_info.username.begin(), node_info.username.end(), diff --git a/src/core/hle/service/nwm/uds_data.h b/src/core/hle/service/nwm/uds_data.h index faaf53448..50dbda15a 100644 --- a/src/core/hle/service/nwm/uds_data.h +++ b/src/core/hle/service/nwm/uds_data.h @@ -91,7 +91,7 @@ constexpr u16 EAPoLStartMagic = 0x201; struct EAPoLStartPacket { u16_be magic = EAPoLStartMagic; u16_be association_id; - enum_le conn_type; + enum_le connection_type; INSERT_PADDING_BYTES(3); EAPoLNodeInfo node; }; diff --git a/src/core/hw/aes/key.cpp b/src/core/hw/aes/key.cpp index 776eab670..2e46de838 100644 --- a/src/core/hw/aes/key.cpp +++ b/src/core/hw/aes/key.cpp @@ -1,4 +1,4 @@ -// Copyright 2017 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -133,6 +133,9 @@ AESIV nfc_iv; AESKey otp_key; AESIV otp_iv; +// gets xor'd with the mac address to produce the final iv +AESIV dlp_checksum_mod_iv; + KeySlot movable_key; KeySlot movable_cmac; @@ -249,6 +252,11 @@ void LoadPresetKeys() { continue; } + if (name == "dlpChecksumModIv") { + dlp_checksum_mod_iv = key; + continue; + } + const auto key_slot = ParseKeySlotName(name); if (!key_slot) { LOG_ERROR(HW_AES, "Invalid key name '{}'", name); @@ -371,4 +379,8 @@ const AESKey& GetMovableKey(bool cmac_key) { return cmac_key ? movable_cmac.normal.value() : movable_key.normal.value(); } +const AESIV& GetDlpChecksumModIv() { + return dlp_checksum_mod_iv; +} + } // namespace HW::AES diff --git a/src/core/hw/aes/key.h b/src/core/hw/aes/key.h index c9d0d4f09..2920e7b86 100644 --- a/src/core/hw/aes/key.h +++ b/src/core/hw/aes/key.h @@ -1,4 +1,4 @@ -// Copyright 2017 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -102,4 +102,6 @@ std::pair GetOTPKeyIV(); const AESKey& GetMovableKey(bool cmac_key); +const AESIV& GetDlpChecksumModIv(); + } // namespace HW::AES diff --git a/src/core/hw/default_keys.h b/src/core/hw/default_keys.h index 019d5cfd0..554b74ae1 100644 --- a/src/core/hw/default_keys.h +++ b/src/core/hw/default_keys.h @@ -1,8 +1,8 @@ -/* Generated by bin2c, do not edit manually */ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. -/* Contents of file keys.enc */ -const long int default_keys_enc_size = 7360; -const unsigned char default_keys_enc[7360] = { +unsigned char default_keys_enc[] = { 0x4E, 0x81, 0xE9, 0x54, 0xCC, 0xDE, 0xFD, 0x56, 0x7D, 0xD2, 0x72, 0xE6, 0xD9, 0xCD, 0x8E, 0x11, 0xE1, 0x7F, 0x74, 0xF4, 0xFC, 0x54, 0xA6, 0xA4, 0x27, 0xC2, 0xD7, 0x50, 0xEA, 0xE7, 0xBE, 0xC9, 0xA7, 0x5E, 0xE0, 0x2E, 0x4A, 0xBE, 0xF5, 0xD5, 0x0D, 0x22, 0x76, 0x2E, 0xB6, 0x80, 0xD8, 0x54, @@ -279,188 +279,193 @@ const unsigned char default_keys_enc[7360] = { 0x2A, 0xE5, 0x39, 0x2D, 0xA3, 0x4B, 0xD1, 0x82, 0xF5, 0x68, 0x1F, 0x42, 0xE4, 0x0B, 0xB0, 0x2E, 0x37, 0x3C, 0x2A, 0x12, 0x61, 0xEC, 0x54, 0x1D, 0xA2, 0xA3, 0x89, 0x54, 0x25, 0xAD, 0x17, 0xE0, 0x8A, 0xFB, 0xA8, 0xF4, 0x6D, 0xAF, 0xF0, 0x84, 0x12, 0xE1, 0x92, 0x72, 0x9B, 0x41, 0x99, 0xA6, - 0x3E, 0x12, 0x74, 0x28, 0x6F, 0x9C, 0xA3, 0x63, 0xC6, 0x88, 0x76, 0xC6, 0x22, 0x76, 0xEC, 0x48, - 0x2B, 0xB5, 0x41, 0x81, 0x45, 0xBB, 0xCB, 0x4D, 0x9D, 0x77, 0x95, 0x49, 0x5F, 0x43, 0x27, 0x40, - 0xD1, 0x4E, 0xEB, 0x2B, 0xD9, 0x0D, 0x7B, 0xD3, 0x36, 0xAE, 0x18, 0x9E, 0x45, 0x36, 0xA1, 0xA0, - 0xB2, 0x30, 0xF1, 0x82, 0xE5, 0x73, 0x5F, 0xC4, 0x75, 0x43, 0xE9, 0xD7, 0x16, 0xE1, 0x98, 0xB6, - 0x60, 0xEB, 0x43, 0x4D, 0x5C, 0xBE, 0x0C, 0xC1, 0x92, 0x8D, 0x9E, 0x25, 0x3A, 0x55, 0xF6, 0x66, - 0x7C, 0x7F, 0xB4, 0x3F, 0x08, 0xA3, 0x1F, 0xC4, 0xCE, 0x05, 0x0F, 0xBF, 0x99, 0x4D, 0x40, 0x5A, - 0x56, 0xC1, 0x50, 0x87, 0x07, 0xDF, 0xED, 0xA2, 0x43, 0x2A, 0xB1, 0x69, 0x36, 0x44, 0xD3, 0x42, - 0x48, 0x53, 0xF6, 0xD8, 0xA9, 0xD7, 0x61, 0xCB, 0x12, 0x8B, 0xCC, 0x5A, 0xE2, 0x47, 0xCD, 0x8C, - 0xCB, 0xBC, 0x2A, 0x56, 0xE0, 0x00, 0x0F, 0x99, 0x61, 0xE4, 0x4C, 0xAE, 0x83, 0x7D, 0xFB, 0xEF, - 0x61, 0x49, 0x40, 0xF9, 0x37, 0x27, 0x12, 0x34, 0xF4, 0x85, 0xAB, 0x27, 0xB6, 0x96, 0xBF, 0xFE, - 0x00, 0xD1, 0x5A, 0xF6, 0x55, 0x90, 0x64, 0x8C, 0x95, 0xCF, 0x15, 0x02, 0x31, 0x45, 0x4D, 0x70, - 0xC4, 0xF9, 0xFC, 0x57, 0x06, 0x93, 0x46, 0x03, 0xE3, 0x94, 0x3C, 0x94, 0x52, 0x54, 0xED, 0x02, - 0x44, 0x9D, 0x61, 0xB9, 0x74, 0x84, 0xA4, 0x06, 0x9F, 0x1D, 0x38, 0x26, 0xE2, 0x8E, 0x09, 0x11, - 0xA9, 0xAF, 0xA7, 0xFE, 0xE8, 0xFF, 0xFB, 0xF2, 0x07, 0xC7, 0xFF, 0x00, 0xC0, 0x9D, 0xDD, 0x91, - 0x38, 0x4B, 0x65, 0x0F, 0xE5, 0xB2, 0xD1, 0xF2, 0x20, 0x91, 0x19, 0x8C, 0x44, 0xB3, 0x71, 0x6A, - 0x68, 0x3D, 0xE4, 0x4F, 0x56, 0x16, 0xFD, 0x25, 0x61, 0x95, 0x5B, 0xA4, 0xE7, 0xED, 0x8B, 0x07, - 0x25, 0x69, 0xDA, 0xFB, 0xED, 0x6C, 0x60, 0x62, 0x5E, 0x9A, 0x3F, 0x8C, 0xC8, 0xE7, 0xF6, 0xC0, - 0x3F, 0xF1, 0x0E, 0x6A, 0xFE, 0x65, 0xC3, 0x04, 0xCC, 0xBD, 0x6E, 0x01, 0x5A, 0xE5, 0xF1, 0x00, - 0xA6, 0xDC, 0xE7, 0x19, 0x23, 0x6D, 0xB3, 0xB2, 0x72, 0x93, 0x8F, 0x83, 0xFC, 0x1F, 0x3E, 0x8F, - 0x1D, 0x20, 0x68, 0x05, 0x2F, 0x53, 0x7F, 0xE1, 0x71, 0x7F, 0xFC, 0x80, 0x71, 0x8D, 0x51, 0xFF, - 0xE1, 0x57, 0x77, 0x13, 0xDA, 0x99, 0x77, 0x27, 0xF7, 0xDB, 0xAD, 0x43, 0xB7, 0xC1, 0x13, 0x65, - 0x0E, 0xE6, 0x54, 0xCB, 0xC4, 0x3E, 0x3B, 0x49, 0x8E, 0x3F, 0xFE, 0xDC, 0x8D, 0x17, 0x10, 0x72, - 0x12, 0xC2, 0x65, 0xB3, 0x16, 0x05, 0xB2, 0xAC, 0xB5, 0x7D, 0xC8, 0x40, 0x72, 0xC7, 0xCF, 0xC5, - 0x7B, 0xA2, 0x54, 0xA3, 0xC6, 0x7C, 0x2C, 0x24, 0x13, 0x33, 0xEF, 0x8C, 0xC3, 0x5A, 0x07, 0xD1, - 0x18, 0xF8, 0x18, 0xEF, 0x06, 0xC6, 0x5A, 0x78, 0xC6, 0x63, 0x11, 0x09, 0xC2, 0xFC, 0x73, 0x75, - 0xF0, 0x92, 0x7A, 0x90, 0xD9, 0xE4, 0xDB, 0xCF, 0x4F, 0x0A, 0x8D, 0x04, 0x5E, 0xF2, 0x8A, 0x7D, - 0xBD, 0x9E, 0xFD, 0x59, 0x62, 0x0D, 0xE5, 0x53, 0x6B, 0xE0, 0xD5, 0x24, 0xB4, 0x53, 0xD4, 0xAD, - 0xBF, 0xC0, 0x26, 0x86, 0x95, 0x82, 0x80, 0x21, 0x86, 0x18, 0xED, 0xE4, 0xDE, 0x94, 0x9B, 0x5F, - 0x6C, 0x41, 0x90, 0x9E, 0x7B, 0x9C, 0x5F, 0x37, 0x11, 0xD1, 0xFF, 0x17, 0xA3, 0x90, 0xFE, 0x87, - 0xE8, 0x0C, 0x2D, 0x44, 0xD3, 0x7C, 0x2D, 0x63, 0xA4, 0xD0, 0x7D, 0xC3, 0x69, 0x6A, 0x44, 0x65, - 0xD3, 0xB3, 0xEB, 0x77, 0xAA, 0x4E, 0x88, 0xCE, 0xDB, 0x3F, 0x71, 0x9C, 0x67, 0xA6, 0x72, 0xA9, - 0xFD, 0x3E, 0x70, 0x94, 0xB0, 0xA8, 0x82, 0x41, 0xB6, 0x19, 0x87, 0xD8, 0x47, 0x3A, 0x02, 0xCA, - 0x25, 0x17, 0x11, 0x2F, 0xEB, 0xDA, 0x6C, 0x2A, 0x76, 0xA9, 0x19, 0x33, 0xB6, 0x80, 0xBA, 0x88, - 0xDD, 0xAE, 0xCF, 0x31, 0x7F, 0x8C, 0x9B, 0x28, 0x5D, 0x5A, 0xE8, 0xE1, 0x09, 0x3F, 0xF0, 0x25, - 0x88, 0xAF, 0xBC, 0xBB, 0x07, 0x1F, 0x16, 0xCA, 0x74, 0xB3, 0xF0, 0xEE, 0x24, 0x51, 0x80, 0xDD, - 0x21, 0xE0, 0x8A, 0xC7, 0xA4, 0x26, 0x42, 0xFA, 0x0B, 0x4A, 0x7F, 0x8D, 0x41, 0xED, 0x05, 0x1D, - 0x0F, 0xE6, 0xF2, 0x33, 0xF3, 0xA8, 0x27, 0x0E, 0x11, 0x15, 0xED, 0x59, 0x1A, 0x02, 0x8E, 0xCA, - 0x87, 0xCA, 0x09, 0x50, 0x59, 0xC8, 0x1F, 0xA6, 0xC9, 0x60, 0xB3, 0x4D, 0x60, 0x82, 0x12, 0x3F, - 0x83, 0x1B, 0x69, 0x6D, 0xCB, 0x43, 0x39, 0x20, 0x93, 0xDF, 0x53, 0xCD, 0xCA, 0x8F, 0x9F, 0x15, - 0x01, 0xE7, 0xDA, 0x60, 0xAD, 0x2F, 0xCC, 0xBE, 0x09, 0x4E, 0x0F, 0x35, 0x20, 0x6D, 0xCD, 0x32, - 0xB0, 0x51, 0x78, 0x17, 0xEE, 0x06, 0x72, 0x9D, 0x66, 0xB0, 0x0D, 0x09, 0x78, 0xA4, 0x9D, 0x9C, - 0x16, 0x4A, 0x3B, 0xBE, 0x89, 0x6B, 0x1B, 0xD8, 0xBD, 0xAF, 0x00, 0xAE, 0x13, 0x86, 0xE3, 0x38, - 0xB9, 0x15, 0x30, 0xFC, 0x5E, 0x74, 0x5F, 0xFE, 0xB1, 0xEC, 0xCF, 0xB6, 0xE2, 0xBF, 0x63, 0x28, - 0xA7, 0x3E, 0x64, 0xC6, 0xC1, 0x72, 0x86, 0x72, 0xFE, 0x03, 0xB1, 0x60, 0x03, 0x16, 0x6F, 0xDA, - 0xE7, 0x48, 0x33, 0x58, 0x66, 0x07, 0x6C, 0x96, 0x5A, 0x29, 0x7D, 0xBF, 0x1C, 0xA1, 0x1A, 0xC6, - 0x1C, 0x90, 0x6D, 0x5C, 0x94, 0xB4, 0x4A, 0x94, 0x5C, 0x4D, 0xAC, 0x13, 0x07, 0x5A, 0xF8, 0x68, - 0x37, 0x3C, 0x9C, 0x33, 0xF8, 0x7E, 0x85, 0x38, 0x57, 0x28, 0xA9, 0xE3, 0xEE, 0xD8, 0x15, 0x4F, - 0xB7, 0x02, 0x0D, 0x6B, 0xB7, 0xF0, 0x27, 0x1E, 0x23, 0x95, 0x46, 0x50, 0x75, 0x4A, 0x87, 0x0D, - 0x2E, 0xCA, 0xC0, 0x37, 0x6E, 0x20, 0x19, 0x1D, 0xD6, 0x1C, 0xFE, 0x66, 0x67, 0xBB, 0x61, 0x91, - 0xC8, 0x0D, 0x77, 0xB9, 0xE1, 0x03, 0x82, 0x73, 0x40, 0xB5, 0x61, 0x94, 0xC9, 0x71, 0x38, 0xFE, - 0x88, 0x25, 0x3B, 0xD6, 0x67, 0x78, 0x79, 0xBA, 0xE1, 0xBD, 0x12, 0x93, 0x57, 0x91, 0xB8, 0x2C, - 0xEE, 0x4E, 0x48, 0xEC, 0x43, 0x18, 0xFC, 0x33, 0xF3, 0x80, 0x7E, 0xB0, 0x1B, 0xAC, 0xA4, 0x6A, - 0x08, 0xF9, 0x7A, 0x72, 0x7F, 0x0D, 0xAE, 0x5D, 0x80, 0x29, 0xA0, 0x9C, 0x73, 0xF1, 0xD5, 0xB5, - 0x56, 0x5F, 0xF7, 0xDD, 0xDE, 0x51, 0xB2, 0x40, 0xDA, 0x13, 0x8B, 0x57, 0x34, 0xDA, 0x52, 0x6E, - 0xCF, 0x2D, 0x12, 0x60, 0x80, 0x48, 0x84, 0x23, 0xB0, 0x8A, 0x5D, 0x34, 0xA1, 0x29, 0x70, 0x7A, - 0xED, 0x01, 0x44, 0x3B, 0xFC, 0x73, 0x22, 0x37, 0x08, 0x95, 0xD8, 0x7D, 0x07, 0xEF, 0x6B, 0xCD, - 0x46, 0x73, 0x6A, 0xD3, 0xAE, 0x13, 0x7D, 0xAE, 0xAF, 0x30, 0xA6, 0xD6, 0x68, 0x10, 0x1E, 0x1C, - 0x2D, 0x64, 0xF3, 0xA9, 0xE6, 0xE1, 0x10, 0x46, 0xC3, 0x12, 0xFB, 0x29, 0x6A, 0x15, 0xA6, 0x06, - 0x5C, 0x9E, 0xD3, 0x32, 0xE4, 0xCD, 0x96, 0x29, 0xDD, 0xB3, 0x05, 0xEA, 0x27, 0xA5, 0x95, 0x66, - 0xB0, 0x79, 0x64, 0x70, 0x26, 0x7C, 0x46, 0x39, 0x18, 0x79, 0x8D, 0xDC, 0x83, 0x6F, 0x11, 0xF0, - 0x05, 0xC5, 0xDC, 0x54, 0xEB, 0xB7, 0xA1, 0x13, 0x4B, 0x13, 0x28, 0x49, 0x91, 0xCC, 0x2A, 0x26, - 0xB5, 0xF0, 0x25, 0x8E, 0xF4, 0xC0, 0xFF, 0x2F, 0x37, 0xD5, 0x57, 0x7E, 0x25, 0xBF, 0xBE, 0x91, - 0x5C, 0x83, 0x6D, 0xC5, 0x4A, 0xDE, 0x7C, 0xC8, 0x37, 0xEA, 0x0F, 0x51, 0xEE, 0x18, 0xDE, 0xC8, - 0xCC, 0xA6, 0xCE, 0x07, 0xB9, 0x12, 0x1A, 0xB9, 0x7F, 0x08, 0x44, 0x34, 0x15, 0x58, 0x32, 0x69, - 0x8C, 0xEA, 0x44, 0x8B, 0x05, 0x15, 0x7A, 0xD7, 0xBB, 0xD2, 0x0B, 0x57, 0x25, 0x11, 0x75, 0x3A, - 0x47, 0xD4, 0xA7, 0x37, 0x40, 0x0C, 0x0A, 0x25, 0x21, 0x85, 0x2E, 0x95, 0x5F, 0x15, 0x15, 0x8D, - 0x8E, 0xBB, 0x0B, 0x6D, 0xE8, 0x58, 0x3F, 0x0C, 0x1D, 0xAD, 0xE0, 0x61, 0x7F, 0xE5, 0x58, 0xD5, - 0x11, 0x95, 0x90, 0x2B, 0x1D, 0x28, 0x58, 0xE2, 0xB9, 0x9D, 0x52, 0x65, 0x98, 0xE4, 0xF7, 0x4C, - 0x3B, 0x63, 0x3F, 0x9A, 0xBC, 0x17, 0x38, 0xA5, 0xAC, 0x28, 0x37, 0x66, 0x38, 0xC3, 0x79, 0xC1, - 0x43, 0x2E, 0xAB, 0x26, 0x22, 0xA6, 0xCF, 0xB3, 0x9F, 0xBE, 0xAC, 0x7A, 0xF4, 0x31, 0xCA, 0x8D, - 0x03, 0xDD, 0xE6, 0xCC, 0xB3, 0x08, 0x57, 0x9C, 0x1E, 0xC2, 0xC6, 0x73, 0xB5, 0x54, 0x95, 0xD8, - 0xC6, 0xC2, 0xF1, 0x69, 0x4F, 0x1E, 0xE6, 0xB1, 0x77, 0x3E, 0x02, 0x86, 0x26, 0x06, 0x2B, 0x34, - 0xA1, 0x51, 0xC0, 0x3D, 0x8D, 0xC6, 0xD6, 0x4A, 0xD8, 0x29, 0x18, 0x4C, 0x0B, 0xEB, 0x74, 0x1C, - 0xDC, 0x13, 0x62, 0xB4, 0xEE, 0x96, 0xF4, 0x48, 0x09, 0x5D, 0x60, 0xD3, 0xAC, 0x85, 0xDF, 0x9A, - 0x10, 0xAB, 0x1A, 0x94, 0x31, 0x74, 0xF3, 0x1D, 0x76, 0x87, 0xB2, 0x3A, 0xD1, 0xF7, 0x93, 0x79, - 0x67, 0x37, 0x6A, 0x97, 0x1F, 0xB9, 0x9E, 0xE4, 0xA1, 0xD3, 0xEF, 0xB4, 0xE6, 0xB2, 0x76, 0x9B, - 0xAA, 0x68, 0x2E, 0xA3, 0x18, 0x23, 0xE4, 0xA9, 0xAF, 0xFC, 0x8D, 0xBE, 0x19, 0x18, 0xB6, 0xF9, - 0xCE, 0xD2, 0xE2, 0xB5, 0xEC, 0x06, 0x43, 0xDD, 0x5C, 0x19, 0xE9, 0xC0, 0x63, 0x08, 0x87, 0xAE, - 0xDE, 0x34, 0xAB, 0xDA, 0xE8, 0xA3, 0x47, 0x85, 0xD5, 0xB1, 0x7A, 0xC2, 0x0F, 0x6A, 0x82, 0x5A, - 0xA1, 0xA1, 0x1D, 0xCF, 0xC3, 0xB9, 0xA4, 0x87, 0x79, 0x01, 0x4A, 0x59, 0xB0, 0xBB, 0xB0, 0x01, - 0x1E, 0x3C, 0x7A, 0xB7, 0xAF, 0xAF, 0xF7, 0x5A, 0x53, 0x17, 0x4B, 0xEB, 0x83, 0x15, 0xB2, 0x9F, - 0x02, 0x3B, 0x19, 0xDA, 0x8C, 0x91, 0x00, 0xF3, 0x78, 0x52, 0xF6, 0xB5, 0x7A, 0x90, 0x1D, 0x77, - 0x33, 0x35, 0x85, 0x06, 0x7D, 0x5F, 0x0D, 0xF1, 0xC6, 0x2B, 0x9D, 0x6E, 0xEA, 0x9F, 0xC3, 0xC6, - 0x87, 0xCD, 0x65, 0x74, 0xEB, 0x42, 0x5B, 0x21, 0x5F, 0xE3, 0xAA, 0x66, 0x44, 0xE2, 0xBE, 0x91, - 0xC8, 0xF9, 0xDA, 0xEC, 0x04, 0x8C, 0x5B, 0x5B, 0x1F, 0x52, 0x09, 0x57, 0x26, 0x2E, 0x95, 0xFD, - 0xCC, 0xF1, 0xDD, 0xB1, 0xBB, 0x98, 0x33, 0xE6, 0x1A, 0xBE, 0x44, 0x1C, 0xC2, 0xD1, 0x33, 0xB7, - 0xE2, 0x26, 0xDE, 0xC5, 0x0E, 0x28, 0x6A, 0x30, 0xD3, 0x81, 0x42, 0xDA, 0x48, 0x0E, 0x6F, 0xB2, - 0x33, 0xF3, 0xB3, 0x0C, 0xE1, 0x29, 0xB0, 0x77, 0xA0, 0x6C, 0xCB, 0x17, 0xE1, 0x81, 0x66, 0x8F, - 0x77, 0xDD, 0x3E, 0x5D, 0x46, 0x84, 0xF2, 0x9B, 0x7F, 0xAE, 0x2A, 0x8A, 0xCF, 0xB6, 0xB5, 0x92, - 0xF0, 0x8B, 0x1C, 0xF4, 0x38, 0x7F, 0x14, 0x26, 0x00, 0x4B, 0x90, 0x55, 0x11, 0x35, 0xE5, 0x84, - 0x50, 0x5A, 0x79, 0x53, 0x5B, 0xB3, 0xB2, 0xD7, 0xAB, 0x6D, 0x87, 0xC5, 0x94, 0xF9, 0x6F, 0xEA, - 0x74, 0xA0, 0x6F, 0x72, 0x76, 0x66, 0x76, 0xBB, 0x74, 0xF4, 0x4A, 0x69, 0x37, 0x7C, 0xC9, 0x0D, - 0x3F, 0xDE, 0x47, 0xE9, 0x56, 0xC0, 0x97, 0x9A, 0x3A, 0xB0, 0x05, 0xDC, 0x99, 0xA4, 0x9C, 0x1C, - 0x49, 0x57, 0xBE, 0xB0, 0xE1, 0xB0, 0xDC, 0xAD, 0xC1, 0xCC, 0x31, 0xB1, 0x4B, 0xE2, 0x63, 0x7F, - 0x7A, 0xB4, 0x84, 0x55, 0x5E, 0xEF, 0x85, 0x4B, 0xD7, 0x5D, 0x60, 0x9B, 0x82, 0x47, 0x3C, 0x45, - 0x68, 0x5F, 0xCB, 0x59, 0x0E, 0xA3, 0x62, 0x61, 0xE6, 0x1B, 0x7D, 0x29, 0x36, 0x04, 0x57, 0x7E, - 0x73, 0xC6, 0x92, 0x52, 0x1C, 0x07, 0x47, 0xA1, 0x46, 0x9C, 0x55, 0x68, 0xFB, 0xC1, 0x11, 0x5A, - 0x85, 0x08, 0x09, 0xE7, 0xF9, 0x81, 0xC8, 0x0A, 0xEC, 0x5A, 0x46, 0x49, 0x5A, 0x84, 0xB0, 0xEF, - 0x90, 0x79, 0xD8, 0xC5, 0x26, 0xBE, 0x1E, 0xF5, 0x5E, 0xBE, 0x6F, 0x39, 0xD0, 0x96, 0xD1, 0x3B, - 0xAD, 0xD7, 0x2A, 0x91, 0xB5, 0x48, 0x58, 0x10, 0x79, 0x9B, 0x05, 0x98, 0x66, 0xAD, 0xF8, 0x38, - 0xC4, 0xF6, 0x56, 0xCC, 0xF3, 0x7D, 0x4B, 0xB6, 0x97, 0xD6, 0xE8, 0x8B, 0xC5, 0xFB, 0x83, 0x69, - 0x46, 0xE1, 0x4E, 0xF5, 0x67, 0xD5, 0x7B, 0x06, 0x8C, 0x5A, 0x82, 0x5C, 0x60, 0x33, 0xB1, 0xD3, - 0x50, 0x30, 0x52, 0x4E, 0xDA, 0x85, 0xBA, 0x98, 0x30, 0xB4, 0xAB, 0x22, 0x49, 0xC9, 0xD8, 0xB2, - 0xE0, 0x63, 0x1F, 0x12, 0x32, 0x1E, 0xD6, 0x05, 0x21, 0x86, 0x58, 0x53, 0x4F, 0xEA, 0x2A, 0x59, - 0x75, 0x35, 0x2D, 0x1A, 0x82, 0xEF, 0x8C, 0x71, 0x3B, 0xCD, 0x78, 0x32, 0xE8, 0xD2, 0x30, 0x12, - 0x79, 0x22, 0x4F, 0x4D, 0xAE, 0xFF, 0xA5, 0x48, 0x3C, 0xCA, 0x5F, 0x6A, 0x14, 0xB4, 0x96, 0xB7, - 0x6C, 0xD9, 0xC1, 0xD7, 0x24, 0xF7, 0xDE, 0x14, 0x70, 0x70, 0x14, 0xEE, 0x68, 0x4F, 0x39, 0x36, - 0xA9, 0xE0, 0x27, 0xEC, 0xFE, 0x03, 0x5D, 0x2D, 0xBD, 0x66, 0x8E, 0xA7, 0xB4, 0x1F, 0xA0, 0x94, - 0xBE, 0x51, 0xDE, 0x44, 0xEC, 0xE4, 0x6D, 0xFD, 0xAA, 0xAF, 0x9C, 0x3A, 0x41, 0x87, 0xF8, 0x4E, - 0x8A, 0xF4, 0x08, 0xC5, 0xB3, 0xD0, 0xB9, 0x1B, 0x23, 0x77, 0x7F, 0x39, 0x9D, 0xAE, 0x0C, 0xAA, - 0x63, 0xB3, 0x99, 0x84, 0xFB, 0xCE, 0x79, 0x7A, 0x34, 0x78, 0x36, 0xA2, 0x38, 0xF8, 0xF7, 0x2E, - 0x0C, 0x7D, 0xBD, 0xBB, 0xF8, 0x5B, 0x5F, 0x33, 0x92, 0x9E, 0x01, 0xEA, 0x85, 0x77, 0xA7, 0xB0, - 0xA5, 0x06, 0xEB, 0xF3, 0x75, 0x5A, 0x2E, 0xDD, 0xD6, 0x7A, 0x07, 0xE4, 0x24, 0x24, 0xC7, 0x76, - 0x52, 0xAD, 0x7B, 0x3C, 0x45, 0x29, 0xB8, 0x01, 0x32, 0xE5, 0x85, 0x0F, 0x2F, 0x50, 0x19, 0x54, - 0x20, 0x67, 0x58, 0x8B, 0x65, 0xFA, 0x4D, 0x4E, 0xA9, 0x70, 0xC4, 0x9A, 0x3C, 0xF3, 0xB0, 0x35, - 0x02, 0x13, 0x5B, 0xD6, 0xB2, 0x63, 0xBF, 0x2E, 0xB4, 0xB6, 0xC1, 0x7C, 0x8B, 0x75, 0xF6, 0x99, - 0x67, 0xBC, 0xC6, 0xA1, 0xB1, 0x58, 0xF1, 0x72, 0x3A, 0x92, 0x31, 0x5C, 0x2C, 0x2E, 0x1D, 0xF3, - 0x09, 0x9E, 0xD3, 0x18, 0xDB, 0x39, 0x11, 0x3D, 0xE1, 0x9D, 0x7B, 0xE3, 0x4D, 0x16, 0xCF, 0x4F, - 0x2B, 0xF9, 0x97, 0xEF, 0x6A, 0x3A, 0x30, 0xAE, 0x6B, 0x90, 0x85, 0x51, 0x14, 0x78, 0xD9, 0xDF, - 0xDC, 0x65, 0x3C, 0x83, 0xF5, 0xE9, 0xE6, 0xB5, 0x8B, 0x42, 0x8C, 0xBE, 0x05, 0x78, 0xE4, 0x9A, - 0xBA, 0x21, 0xD4, 0x30, 0x48, 0x89, 0x92, 0xE4, 0x7E, 0xF9, 0x43, 0x4D, 0x2C, 0xDF, 0xDE, 0x8E, - 0x63, 0xFB, 0x40, 0xDD, 0x0E, 0x2C, 0x34, 0x4F, 0x44, 0xAE, 0x29, 0xA2, 0x48, 0x58, 0x60, 0xC8, - 0xC7, 0x64, 0x1F, 0x69, 0x99, 0xD1, 0x01, 0x91, 0x81, 0x42, 0x54, 0x10, 0xBE, 0x82, 0x18, 0x39, - 0x78, 0xE4, 0x4E, 0xFA, 0xB6, 0xE6, 0x48, 0xB8, 0x36, 0x65, 0xDF, 0x00, 0xF0, 0x12, 0x60, 0xB3, - 0x74, 0x28, 0x1E, 0x68, 0xF1, 0x40, 0x9A, 0x29, 0xA1, 0xBB, 0x21, 0x9D, 0x96, 0x61, 0x8F, 0x85, - 0x6C, 0x88, 0x58, 0x91, 0x79, 0xF6, 0x88, 0x3A, 0x9A, 0x54, 0xC0, 0xE5, 0x13, 0x88, 0x30, 0x4A, - 0x65, 0xE1, 0x8D, 0x0D, 0x10, 0x61, 0xD8, 0xA5, 0x90, 0x02, 0xED, 0xA6, 0xE9, 0x49, 0xD9, 0xC7, - 0x86, 0x2C, 0xFF, 0xAC, 0xD6, 0x4E, 0xED, 0x5C, 0x4F, 0xA2, 0x8E, 0xF9, 0x18, 0x3B, 0xDE, 0x16, - 0x04, 0xD2, 0x75, 0xEC, 0x15, 0x9F, 0xF0, 0x01, 0xB5, 0xE7, 0x0C, 0x96, 0xBE, 0xC4, 0xBE, 0xEA, - 0xDB, 0xB7, 0x2B, 0xFC, 0x73, 0x6A, 0x1D, 0x0B, 0x74, 0xD8, 0x64, 0x57, 0xD0, 0xB9, 0x4F, 0x9A, - 0x72, 0x74, 0x07, 0xC5, 0x8D, 0xDB, 0x81, 0x4C, 0x13, 0x77, 0xCD, 0xDA, 0x01, 0x8E, 0xDF, 0xFF, - 0xCA, 0x11, 0x62, 0x37, 0xC9, 0xAC, 0xFD, 0x94, 0xF4, 0xCC, 0x42, 0xC7, 0x9B, 0xD1, 0xF9, 0x4D, - 0x85, 0x3B, 0xDC, 0xBF, 0xFC, 0x20, 0x4D, 0xE1, 0x52, 0xCD, 0x29, 0xF1, 0x7D, 0x2A, 0x54, 0xA1, - 0x2E, 0x18, 0x7B, 0xDD, 0x05, 0xE0, 0x36, 0x7D, 0x7C, 0x40, 0x11, 0x9A, 0xC8, 0xE1, 0x63, 0x39, - 0x7D, 0x72, 0x54, 0xB2, 0x1C, 0xE1, 0x40, 0x13, 0x6A, 0x1F, 0x76, 0xB1, 0xAD, 0x75, 0xE3, 0x24, - 0x7B, 0x3F, 0xA9, 0xCA, 0xFD, 0x28, 0x76, 0x6F, 0x65, 0x63, 0xA7, 0xCC, 0x71, 0x84, 0xE3, 0x04, - 0xC5, 0x05, 0x17, 0x5A, 0x1F, 0xD0, 0xEA, 0x69, 0xBB, 0x7A, 0xE1, 0xA1, 0xB0, 0xFB, 0xE0, 0xD2, - 0x70, 0x1F, 0x6B, 0x5C, 0x86, 0xE4, 0xDE, 0x8C, 0x5C, 0xC8, 0x36, 0xA9, 0xDD, 0x5D, 0x13, 0x82, - 0xDB, 0x6E, 0x93, 0x00, 0x77, 0x8C, 0xE1, 0xD3, 0x9A, 0x0C, 0x4D, 0xF4, 0x5A, 0x10, 0xDB, 0xBF, - 0x3D, 0xD0, 0x6C, 0x4E, 0xEC, 0x64, 0xA2, 0xF4, 0x5D, 0x29, 0x80, 0x4B, 0xE7, 0xA1, 0x14, 0xAE, - 0xB4, 0x78, 0x8B, 0x6E, 0xCB, 0xA1, 0xB2, 0x02, 0x35, 0xC7, 0x4E, 0x58, 0x7C, 0x98, 0x46, 0x05, - 0xCE, 0x56, 0x83, 0xB4, 0x5E, 0x82, 0x65, 0xF1, 0xC9, 0x9B, 0x29, 0xAC, 0x42, 0xEB, 0xE5, 0xF1, - 0x1D, 0x1A, 0x11, 0x0C, 0x63, 0xAD, 0xFD, 0xCF, 0x40, 0x68, 0xF1, 0xB3, 0xF4, 0x62, 0xB9, 0x9B, - 0x6C, 0x6C, 0x13, 0x94, 0xAF, 0x82, 0x80, 0xE0, 0xBA, 0x6C, 0xCB, 0x84, 0x0D, 0xA0, 0xE4, 0xAA, - 0x15, 0x8F, 0xAD, 0x29, 0xCE, 0x79, 0x7B, 0xF6, 0x70, 0x3F, 0xCC, 0x8B, 0x92, 0xA9, 0xC5, 0x17, - 0xBA, 0xE0, 0xF0, 0x9B, 0x96, 0x8E, 0x7F, 0xA0, 0x2F, 0x2C, 0x5F, 0x54, 0xF6, 0x5A, 0x9E, 0xBB, - 0xAC, 0x6C, 0xE3, 0xBF, 0xC4, 0x1C, 0xAB, 0x14, 0xEB, 0x12, 0xCE, 0xD8, 0xA5, 0x44, 0xCC, 0x4F, - 0x4B, 0x08, 0x4F, 0x2C, 0x00, 0x81, 0xD5, 0x17, 0x22, 0x07, 0x42, 0xCA, 0xFD, 0x49, 0xAD, 0x06, - 0x95, 0xC8, 0xD1, 0xC7, 0x3E, 0x39, 0x34, 0x1C, 0x41, 0x99, 0xC2, 0xAB, 0x8A, 0xED, 0x50, 0x12, - 0xE8, 0xC7, 0x75, 0x52, 0x19, 0x2B, 0xD4, 0xCC, 0xD3, 0xFA, 0x84, 0xE7, 0x0C, 0xCE, 0xE3, 0x93, - 0xCA, 0x60, 0xE5, 0xB7, 0x06, 0xDB, 0x84, 0xEE, 0x79, 0xA7, 0x54, 0x76, 0xE9, 0x46, 0x85, 0xEA, - 0x4F, 0xF3, 0xA1, 0xEF, 0x10, 0xC1, 0x4C, 0x12, 0xB0, 0xEE, 0x23, 0xDD, 0x81, 0x3A, 0x6E, 0x9F, - 0x01, 0x03, 0x04, 0x7C, 0x6D, 0x47, 0x84, 0xB7, 0xE7, 0x19, 0xE3, 0x4E, 0xCF, 0x23, 0x3A, 0xB2, - 0x23, 0x33, 0x00, 0xCB, 0x07, 0x78, 0x51, 0x8B, 0x0C, 0x30, 0x7A, 0x1F, 0x41, 0x14, 0x75, 0xFF, - 0x9E, 0x43, 0x81, 0xF4, 0x15, 0x89, 0x8B, 0xB7, 0x2B, 0xBC, 0x62, 0x48, 0x64, 0xD9, 0x26, 0xBE, - 0xEA, 0x22, 0xB6, 0x22, 0xB8, 0x6F, 0x2B, 0xB6, 0x9B, 0x8F, 0xC7, 0x63, 0x03, 0x83, 0xA7, 0x22, - 0xF8, 0x5C, 0x08, 0x87, 0x70, 0xA2, 0xCB, 0x6B, 0xD6, 0xC9, 0xF6, 0x59, 0x60, 0x8C, 0x10, 0xFA, - 0x3C, 0xAD, 0x15, 0x1F, 0x8F, 0x18, 0x01, 0x2F, 0xB9, 0x2C, 0x01, 0x59, 0x76, 0x18, 0xE6, 0x55, - 0x98, 0x23, 0x33, 0xA9, 0x05, 0xFC, 0x4C, 0xF3, 0x9A, 0xCB, 0xBA, 0x42, 0x60, 0x0C, 0x50, 0xEB, - 0x69, 0xF1, 0x22, 0x73, 0x03, 0x4B, 0x38, 0x74, 0xBF, 0xBB, 0x7B, 0x4C, 0x7F, 0x30, 0xF0, 0x21, - 0x8C, 0x73, 0x24, 0x69, 0x1F, 0x7F, 0xF4, 0x18, 0x0E, 0x4F, 0xB7, 0x99, 0x8D, 0x5A, 0xE9, 0xF7, - 0x79, 0x8B, 0x25, 0xCB, 0xC7, 0xEA, 0x8C, 0xA7, 0x36, 0x33, 0x72, 0x78, 0x2B, 0x9B, 0x4F, 0xA6, - 0x53, 0x10, 0xFA, 0xF7, 0x1C, 0x66, 0xBB, 0x7C, 0x72, 0x18, 0xBE, 0x91, 0x5C, 0x8C, 0xED, 0x75, - 0x3C, 0x35, 0xF4, 0x49, 0xAD, 0xB0, 0x49, 0x67, 0x05, 0x6B, 0x96, 0x46, 0xF9, 0x2D, 0xAF, 0x8E, - 0x90, 0x3A, 0x3C, 0x35, 0xF5, 0x66, 0x7E, 0xE8, 0x08, 0x04, 0x0D, 0xBF, 0x6E, 0x4E, 0x3F, 0xAD, - 0x0A, 0xB9, 0x06, 0xDF, 0x4B, 0xD1, 0x9E, 0x5D, 0x69, 0x13, 0x4B, 0xCD, 0xB6, 0xE7, 0x4A, 0x12, - 0xF2, 0x94, 0xE7, 0xBF, 0xE3, 0x48, 0x65, 0xF2, 0xD4, 0x2A, 0xCF, 0x17, 0xC2, 0xF8, 0x50, 0xC5, - 0xF9, 0x3B, 0x11, 0x04, 0x5D, 0x84, 0xE6, 0x9D, 0x97, 0x31, 0xDB, 0x72, 0xD0, 0x0F, 0x3E, 0x9D, - 0xE2, 0x5B, 0x81, 0xBF, 0xA3, 0xD1, 0xB3, 0x5D, 0xCF, 0x1D, 0x70, 0xDA, 0x96, 0xA3, 0x67, 0xA5, - 0xF0, 0x5E, 0xA8, 0xD0, 0x15, 0x47, 0x3D, 0xAE, 0xA4, 0x33, 0x80, 0xF4, 0x90, 0x1B, 0x27, 0xD1, - 0x4F, 0x5F, 0x2B, 0x1B, 0xA4, 0xEE, 0x5F, 0x3F, 0x0F, 0xC2, 0x70, 0x13, 0x42, 0x42, 0x98, 0xA1, - 0xC4, 0x0C, 0x2B, 0xB1, 0x4E, 0x69, 0xF2, 0xBC, 0xB5, 0x21, 0xD2, 0x11, 0xA4, 0xC0, 0x3D, 0xB5, - 0xBE, 0x4D, 0x0E, 0x84, 0x9B, 0xF0, 0xB0, 0x0F, 0xE2, 0x23, 0xCB, 0x66, 0x1D, 0x71, 0x4F, 0x20, - 0x2F, 0x3C, 0x9D, 0xC8, 0x3C, 0x11, 0xA7, 0x47, 0xA4, 0x3A, 0x46, 0xB0, 0x83, 0xA1, 0xD5, 0x34, - 0x71, 0xA9, 0x8B, 0xDB, 0xBA, 0x6F, 0x49, 0xDC, 0x69, 0xC9, 0xBF, 0xFE, 0x6C, 0x12, 0x58, 0x3B, - 0xDB, 0x42, 0x39, 0xF7, 0x63, 0xB2, 0xA4, 0xFD, 0x30, 0x48, 0x45, 0x4E, 0x02, 0x15, 0xBD, 0x4B, - 0xC3, 0x59, 0xF9, 0x0B, 0x87, 0x1F, 0xEB, 0x90, 0x0E, 0x69, 0x0C, 0xDE, 0x15, 0xD8, 0x7C, 0xA6, - 0xD2, 0xFC, 0xFA, 0x5F, 0xAE, 0x9B, 0x7D, 0x76, 0x0B, 0xE8, 0x53, 0xE4, 0x10, 0xC6, 0xF0, 0x3D, - 0x33, 0x7A, 0x9E, 0x4E, 0x57, 0x0F, 0x58, 0xB6, 0x13, 0x95, 0x89, 0x6D, 0x84, 0xC6, 0xB2, 0x22, - 0x37, 0xF9, 0x99, 0x27, 0xCC, 0x10, 0xA2, 0x14, 0x84, 0x0F, 0x9A, 0xE7, 0xB9, 0x52, 0xFC, 0xB3, - 0x3F, 0x97, 0x07, 0x67, 0xB5, 0xE5, 0x00, 0x32, 0x4D, 0x90, 0x4D, 0x6A, 0xFE, 0x17, 0xE1, 0xAC, - 0x58, 0x00, 0x69, 0xC8, 0x7A, 0x87, 0x60, 0x10, 0xB5, 0x9C, 0x64, 0xFC, 0xAE, 0xD8, 0x86, 0x88, - 0x5A, 0x76, 0xED, 0x72, 0x97, 0x19, 0x90, 0xD1, 0xE3, 0xAF, 0x6E, 0x07, 0x2F, 0x7A, 0xBB, 0xC6, - 0x0D, 0x63, 0x53, 0x23, 0x00, 0xA3, 0x8A, 0x88, 0x2B, 0x21, 0x9C, 0x3A, 0x9D, 0x86, 0x16, 0xE9, -}; + 0x4F, 0x1C, 0xE7, 0x11, 0xCB, 0x53, 0xD9, 0x1D, 0x46, 0x7F, 0xA5, 0x69, 0xFE, 0x2E, 0x02, 0x2C, + 0x39, 0xFC, 0xEC, 0x2E, 0xB5, 0x3D, 0x92, 0x03, 0x94, 0xDE, 0x72, 0x94, 0x0C, 0x48, 0xA1, 0x85, + 0xDC, 0x84, 0x7E, 0xE0, 0x0C, 0xDD, 0x9B, 0xC4, 0x13, 0x13, 0x01, 0x38, 0x11, 0x46, 0xF7, 0xEB, + 0x2D, 0x6F, 0x30, 0x50, 0x09, 0x99, 0x09, 0x28, 0x6A, 0x76, 0xD9, 0xAC, 0x1A, 0x4E, 0xA2, 0xEE, + 0xD9, 0x26, 0x05, 0xDB, 0x7E, 0x48, 0x9E, 0xC8, 0xEE, 0x6E, 0xE0, 0xE1, 0xED, 0x5C, 0x8A, 0x2A, + 0xC7, 0x4B, 0x78, 0xD8, 0x4B, 0x59, 0x9D, 0x53, 0x48, 0x8E, 0x7F, 0xE8, 0xC0, 0xA9, 0x4C, 0x65, + 0x48, 0x38, 0xDB, 0xE7, 0xBD, 0xBA, 0xC2, 0x45, 0xA3, 0x2E, 0x09, 0x2E, 0xCC, 0xBE, 0x26, 0x92, + 0x31, 0x0C, 0x66, 0xA4, 0xE8, 0xE7, 0x0B, 0x4A, 0x55, 0x37, 0xED, 0xFF, 0x95, 0xE0, 0xE2, 0xBC, + 0x63, 0x74, 0x89, 0x65, 0xCF, 0x9B, 0x5E, 0xCA, 0xF9, 0x4B, 0x52, 0x8A, 0xF9, 0x3F, 0xC0, 0x95, + 0xA9, 0xD6, 0xF5, 0xDD, 0xF9, 0x6E, 0xD8, 0x0C, 0x42, 0x76, 0xFD, 0xE5, 0x7A, 0xA6, 0xD0, 0x8E, + 0x5A, 0x2C, 0x56, 0x52, 0x45, 0x3A, 0x86, 0xAD, 0x94, 0xD2, 0x31, 0x62, 0x62, 0xFF, 0xFF, 0xF8, + 0x72, 0x29, 0x5D, 0x64, 0xC3, 0xEF, 0xC5, 0x98, 0xF6, 0x3D, 0xCC, 0x8A, 0xBF, 0xF8, 0xA4, 0x43, + 0x9F, 0x7C, 0x72, 0xAA, 0x2F, 0xA6, 0xDB, 0x69, 0xAD, 0x32, 0x70, 0xF4, 0xA4, 0x35, 0x84, 0x5D, + 0xFF, 0x3B, 0x4B, 0x83, 0x51, 0x44, 0xE0, 0x97, 0xF3, 0x3C, 0xFE, 0x42, 0x40, 0x0D, 0xBF, 0xAB, + 0x7D, 0x55, 0xC5, 0xCD, 0xA6, 0xDD, 0x07, 0xC7, 0xCD, 0xEC, 0x76, 0x14, 0x65, 0xD5, 0xC2, 0x62, + 0x69, 0xDC, 0x95, 0x26, 0x54, 0x29, 0x1F, 0x68, 0x46, 0x95, 0x40, 0x7D, 0xED, 0x45, 0x12, 0xD2, + 0x5B, 0x9E, 0xF5, 0x32, 0x4C, 0x84, 0xF7, 0xF4, 0x05, 0x93, 0x62, 0xC5, 0x43, 0xED, 0xF6, 0x8E, + 0x17, 0x55, 0x82, 0x26, 0x2C, 0xC8, 0x30, 0x90, 0x10, 0x50, 0x98, 0x14, 0x14, 0x2A, 0xF0, 0xC7, + 0x54, 0xA1, 0x29, 0xF3, 0xDA, 0x41, 0x42, 0x38, 0x91, 0xCB, 0x3D, 0x8F, 0x5C, 0xCA, 0xB7, 0xC6, + 0x56, 0x5C, 0x07, 0x79, 0xFF, 0xA5, 0x8B, 0x90, 0x49, 0x0B, 0xE2, 0x12, 0x9C, 0xAA, 0x7F, 0xF9, + 0x5F, 0x8E, 0x4C, 0xD2, 0xA2, 0xC8, 0x5E, 0x0E, 0xC4, 0xB9, 0x43, 0xE0, 0x57, 0x60, 0x1F, 0x68, + 0xED, 0x76, 0xA2, 0xD9, 0xE7, 0x78, 0x1C, 0x7A, 0xB5, 0x31, 0xED, 0x1D, 0x05, 0x2E, 0xA6, 0x15, + 0x14, 0x22, 0xEF, 0xA6, 0xB7, 0x98, 0xAF, 0x42, 0x47, 0x75, 0xAB, 0xEA, 0x5B, 0x91, 0x43, 0x5C, + 0x2E, 0xE7, 0x62, 0x1A, 0xF1, 0x6E, 0x9D, 0xB3, 0xAC, 0xA4, 0x4A, 0x5F, 0xFE, 0x14, 0x91, 0x02, + 0x3B, 0xEB, 0x3C, 0x5C, 0x9D, 0x08, 0x34, 0xAF, 0x9F, 0x5F, 0x8D, 0xDE, 0xD7, 0x13, 0x15, 0x99, + 0xAD, 0x7C, 0x21, 0x3E, 0x5C, 0x73, 0xED, 0xB1, 0x9F, 0x90, 0x37, 0xDF, 0x6F, 0xD2, 0xCB, 0xEE, + 0x8C, 0x3D, 0x13, 0x64, 0x6B, 0x57, 0xCA, 0xB0, 0xCB, 0x04, 0xAC, 0x05, 0x89, 0x29, 0xCB, 0x41, + 0x8C, 0x86, 0x98, 0x79, 0x89, 0x74, 0x7B, 0xAB, 0x59, 0xC5, 0x77, 0x31, 0xB3, 0x25, 0x7A, 0x72, + 0x44, 0xBF, 0x23, 0xB9, 0x1C, 0xC7, 0xD0, 0x75, 0xD8, 0x20, 0xC3, 0xD1, 0x62, 0x98, 0x6B, 0xE8, + 0x6E, 0xE5, 0x67, 0xEC, 0x08, 0xD8, 0x84, 0x33, 0x0B, 0xB4, 0x9E, 0xAE, 0xA4, 0xD9, 0x28, 0xA0, + 0xDF, 0xEF, 0xA6, 0xFA, 0x5F, 0x5B, 0x2A, 0x27, 0x9B, 0x8F, 0xF4, 0x88, 0x8E, 0x72, 0x16, 0xFF, + 0x8E, 0x00, 0xA9, 0x3D, 0x77, 0x8B, 0x11, 0xCD, 0xF9, 0xA2, 0xC2, 0xF0, 0xE0, 0xB6, 0xFD, 0x3F, + 0x16, 0x89, 0x50, 0xF9, 0x07, 0xCF, 0x13, 0xF8, 0x78, 0xEE, 0x8D, 0x76, 0x3F, 0xDC, 0xE0, 0x1D, + 0x0D, 0xAD, 0x67, 0xE4, 0x5B, 0x90, 0x49, 0x12, 0xDF, 0x70, 0xDF, 0xCA, 0xEC, 0xDA, 0x4A, 0xC8, + 0x5E, 0xA2, 0x6F, 0xAC, 0x0E, 0x11, 0x9A, 0x2C, 0x80, 0x94, 0x3B, 0xD3, 0xDB, 0xC0, 0x26, 0x29, + 0x91, 0x4F, 0x19, 0xB3, 0xE7, 0xCD, 0x76, 0xB5, 0xEE, 0x4E, 0x35, 0xCC, 0x7F, 0x62, 0x66, 0xAB, + 0xB9, 0x50, 0x96, 0x1A, 0xDB, 0x79, 0xB2, 0x1A, 0x4C, 0xE0, 0x5D, 0x32, 0xE0, 0x45, 0x71, 0x8C, + 0xB0, 0xFD, 0x74, 0xA8, 0x5D, 0x99, 0xA9, 0x56, 0x67, 0x3B, 0x29, 0x0C, 0x70, 0xD0, 0x5A, 0xB8, + 0xF7, 0x9C, 0xF7, 0x9F, 0x32, 0x91, 0xB8, 0x29, 0x0F, 0xCF, 0x4A, 0xA1, 0xD9, 0xFE, 0xFF, 0x90, + 0xF9, 0x5C, 0x1D, 0x4C, 0xB4, 0x33, 0xCE, 0xA5, 0xEF, 0x22, 0x97, 0xF4, 0x40, 0xCF, 0xB5, 0x84, + 0x93, 0xB7, 0x8F, 0xDB, 0xE3, 0x70, 0x17, 0xE1, 0x0E, 0x70, 0x0E, 0x6A, 0x5D, 0x41, 0xA5, 0xAD, + 0x78, 0xAF, 0xEC, 0x50, 0xC3, 0xF8, 0x80, 0xBB, 0x07, 0x3F, 0xAA, 0xBB, 0x02, 0xF3, 0x51, 0x64, + 0xF2, 0x3D, 0x36, 0x69, 0xAE, 0x54, 0x8B, 0xFA, 0x13, 0xE8, 0x12, 0x51, 0x5A, 0x2C, 0x89, 0x8A, + 0x43, 0x5F, 0x9B, 0xDE, 0x33, 0xD2, 0x2C, 0xCF, 0xAC, 0x92, 0x65, 0x37, 0x47, 0x96, 0x55, 0x43, + 0xB7, 0x64, 0x52, 0x99, 0xEC, 0x91, 0x77, 0x80, 0x05, 0x8C, 0x59, 0x53, 0x22, 0x79, 0x47, 0xD0, + 0x1B, 0x01, 0xD6, 0x97, 0xF5, 0x4A, 0x02, 0x90, 0xE6, 0xA5, 0xE9, 0x25, 0x8C, 0x94, 0x55, 0x60, + 0x09, 0x0C, 0x2D, 0xF0, 0xA5, 0xFF, 0x97, 0x07, 0x84, 0x24, 0x9A, 0x9A, 0x5D, 0xEC, 0xA3, 0x2C, + 0xF4, 0xB7, 0x91, 0xD2, 0x59, 0x54, 0x39, 0x51, 0xFA, 0x9A, 0x96, 0x1B, 0xD1, 0xC7, 0x30, 0xBF, + 0x0E, 0x00, 0xA6, 0xBC, 0x56, 0x27, 0xC8, 0x66, 0x8E, 0x07, 0xE1, 0x02, 0xB6, 0xEF, 0x68, 0xDF, + 0x52, 0x2C, 0x6B, 0xF1, 0xE7, 0xB6, 0xC4, 0x38, 0x0F, 0x31, 0xBC, 0x02, 0x2B, 0x66, 0x6D, 0x6E, + 0x15, 0xB4, 0x01, 0xC8, 0xD1, 0xEF, 0x3F, 0x73, 0x4B, 0xE3, 0x9A, 0x09, 0x96, 0x8C, 0x65, 0x72, + 0x99, 0x88, 0xC6, 0x1E, 0x67, 0x97, 0x4D, 0xE6, 0x7F, 0x76, 0x8B, 0x9D, 0x1A, 0xAC, 0xBD, 0xB4, + 0xCF, 0x05, 0x32, 0xA4, 0x28, 0x90, 0x34, 0xA4, 0x05, 0x40, 0xF6, 0x58, 0x45, 0xCC, 0x59, 0x50, + 0xAF, 0xFF, 0x27, 0x87, 0x02, 0x26, 0xF6, 0xC6, 0xCF, 0xFF, 0x0D, 0x2E, 0xCC, 0x8D, 0x50, 0x3E, + 0x68, 0xCD, 0x4D, 0xB1, 0xF5, 0xCF, 0x0D, 0xCA, 0x63, 0x10, 0x9F, 0x43, 0xD0, 0x86, 0xA4, 0xA4, + 0x36, 0xD4, 0xC2, 0x7D, 0x06, 0xA3, 0x8F, 0x1A, 0xCE, 0xFD, 0x46, 0x90, 0xFE, 0x95, 0xDD, 0xCE, + 0xBE, 0x6F, 0x6A, 0x9B, 0x40, 0xCC, 0x2D, 0xEF, 0xF2, 0xFE, 0x26, 0x98, 0x23, 0x5A, 0x0D, 0xAD, + 0xF3, 0x46, 0xB0, 0x91, 0x30, 0x6B, 0xCA, 0x2B, 0xB5, 0x1C, 0xE7, 0x61, 0x1C, 0x3F, 0x6F, 0x5B, + 0xB0, 0xEE, 0xF0, 0xF5, 0x42, 0x6A, 0x74, 0xB3, 0x5D, 0x13, 0x32, 0x30, 0x99, 0x51, 0x53, 0x0E, + 0xC3, 0x4E, 0xFA, 0x56, 0x0A, 0x02, 0x38, 0xA8, 0x06, 0x8F, 0xD8, 0x4B, 0x21, 0xF5, 0xFC, 0x0F, + 0x36, 0x09, 0x35, 0x50, 0x37, 0x77, 0xD2, 0x1A, 0x65, 0xED, 0x89, 0xEF, 0x89, 0x9F, 0xB9, 0x5B, + 0x2D, 0x6F, 0xF5, 0x2E, 0xF2, 0x4F, 0xEA, 0x93, 0xA5, 0x7E, 0xB9, 0x16, 0xB1, 0xA2, 0x68, 0x2C, + 0x93, 0xEA, 0x06, 0x28, 0x37, 0x7B, 0xDE, 0x9B, 0x1A, 0x75, 0x5D, 0x02, 0x8A, 0xB8, 0x9F, 0xE0, + 0x65, 0x6B, 0x15, 0x19, 0x89, 0x7F, 0xBD, 0x6D, 0xA3, 0xAB, 0x36, 0x50, 0xBA, 0x99, 0xAA, 0xE6, + 0x7E, 0x29, 0x4C, 0x01, 0x28, 0x4C, 0xBB, 0xD5, 0x15, 0x51, 0x50, 0xDE, 0x17, 0x2C, 0xFF, 0x90, + 0x9D, 0x2F, 0xFA, 0xED, 0x41, 0x23, 0xA2, 0x70, 0x42, 0xE4, 0x51, 0xE9, 0x1E, 0x32, 0x38, 0xA4, + 0x63, 0x05, 0xF5, 0x70, 0x16, 0x56, 0x95, 0x3C, 0xC8, 0x36, 0xDB, 0xCC, 0xA8, 0xF7, 0x70, 0x2F, + 0x5D, 0x15, 0x65, 0x01, 0x96, 0xC3, 0xC9, 0x67, 0x54, 0xEC, 0x40, 0xA4, 0xA1, 0x09, 0x46, 0x22, + 0x7D, 0x8F, 0x35, 0xC9, 0xB6, 0x15, 0xB5, 0x18, 0x4D, 0x3A, 0x43, 0xCF, 0x9F, 0x90, 0x9B, 0x56, + 0x5E, 0x48, 0x54, 0x2D, 0x82, 0x78, 0x0A, 0x0E, 0x29, 0x8B, 0x98, 0x03, 0xAC, 0x73, 0x97, 0x39, + 0xDB, 0xA0, 0x7D, 0x31, 0x12, 0xA8, 0xDC, 0x91, 0x56, 0xA4, 0xC0, 0x92, 0xB0, 0x94, 0xD0, 0xD9, + 0x5F, 0x6E, 0xF7, 0xCE, 0x55, 0xBF, 0xC7, 0x35, 0x47, 0x3B, 0x83, 0x90, 0xFE, 0x83, 0x29, 0xCC, + 0xD9, 0x28, 0xAC, 0x13, 0x86, 0x9D, 0x27, 0x71, 0xE6, 0xFE, 0xFC, 0xC9, 0xE6, 0xF5, 0x1E, 0xB5, + 0xE7, 0x30, 0xA5, 0x58, 0x03, 0xD0, 0xA3, 0x87, 0x60, 0x0A, 0x01, 0x3F, 0xE4, 0x2C, 0x7B, 0x4C, + 0x42, 0xBC, 0xF8, 0x8E, 0x9D, 0x01, 0x09, 0x0F, 0x85, 0x0B, 0x82, 0x0D, 0x7C, 0x76, 0xF2, 0x8D, + 0xB8, 0x8E, 0x27, 0xA9, 0xAD, 0x8A, 0x95, 0x38, 0x44, 0xC9, 0x1B, 0x96, 0x75, 0xD0, 0x6D, 0x51, + 0x93, 0x44, 0x25, 0x5A, 0x37, 0x84, 0x37, 0x16, 0x81, 0xF9, 0xF9, 0x11, 0x15, 0x1D, 0xB8, 0xFE, + 0x23, 0x59, 0xAC, 0xF5, 0xB2, 0x91, 0x30, 0xB3, 0x61, 0x86, 0x1B, 0xAD, 0x9F, 0xA7, 0xEE, 0x92, + 0x37, 0xF1, 0xE3, 0x88, 0x4C, 0x3D, 0x3A, 0x78, 0xC9, 0x16, 0x71, 0x4D, 0x99, 0x46, 0x68, 0x00, + 0xA4, 0x85, 0x0F, 0x3B, 0x0C, 0x2A, 0xDC, 0x93, 0x23, 0x1A, 0x38, 0x60, 0x28, 0x6A, 0x41, 0x59, + 0x57, 0x6E, 0xBA, 0x38, 0x72, 0x1F, 0x65, 0x3D, 0x22, 0xD0, 0x6C, 0xE9, 0x37, 0xE2, 0x51, 0xCA, + 0x2B, 0x43, 0x3D, 0x63, 0x3C, 0x58, 0x2F, 0x5D, 0x98, 0x24, 0x15, 0x79, 0x16, 0x76, 0xBF, 0x3F, + 0x20, 0xC3, 0xF2, 0x4C, 0x53, 0x72, 0xB8, 0x91, 0x6B, 0x01, 0xE1, 0x1D, 0xD2, 0x27, 0xB0, 0xA1, + 0xA1, 0x1D, 0x32, 0x08, 0x7D, 0xC4, 0x99, 0x86, 0x6C, 0x3A, 0x77, 0x0E, 0x3B, 0xCE, 0xC4, 0x6E, + 0x09, 0x44, 0xF7, 0x33, 0xBA, 0xA7, 0x4A, 0xF4, 0xAD, 0x26, 0xC6, 0x00, 0x42, 0x0B, 0xAE, 0x4A, + 0x21, 0x68, 0x93, 0x86, 0x66, 0xA2, 0xCF, 0xB5, 0x85, 0xE3, 0x72, 0xE7, 0x3B, 0xBA, 0xA6, 0x9F, + 0xE6, 0x2D, 0x01, 0xCB, 0x62, 0x7A, 0x5D, 0x3F, 0x97, 0xCA, 0x26, 0xEC, 0x1B, 0xB7, 0x28, 0x9D, + 0x49, 0xEE, 0xE9, 0xA1, 0x2E, 0x2F, 0xE5, 0x1E, 0x8F, 0xCB, 0x38, 0xD6, 0xA7, 0x3B, 0xAD, 0x63, + 0xF1, 0x7A, 0x6D, 0x99, 0x7F, 0x4F, 0x0F, 0xE8, 0x13, 0x21, 0xAA, 0x97, 0x2D, 0x8F, 0x54, 0x89, + 0x31, 0x44, 0xDA, 0x5F, 0x00, 0x2F, 0x1C, 0x25, 0x19, 0xA2, 0x54, 0xA6, 0xF7, 0x2A, 0x0A, 0xAA, + 0xD6, 0xF9, 0xD1, 0x3C, 0x1B, 0x23, 0xAC, 0x92, 0x4B, 0x18, 0xC3, 0x36, 0xB7, 0xDF, 0xFE, 0x10, + 0xEC, 0xF6, 0x96, 0xA4, 0x33, 0x6D, 0xD8, 0xB7, 0x9A, 0xE5, 0xF8, 0x93, 0xA1, 0x88, 0xC4, 0xAD, + 0x61, 0x15, 0xC7, 0xC0, 0x4D, 0xAD, 0x3B, 0x9E, 0xA3, 0x5E, 0x84, 0x4B, 0x26, 0x7F, 0xC6, 0x3A, + 0xDB, 0x90, 0x27, 0x26, 0xFA, 0x42, 0x9D, 0x31, 0xB3, 0x67, 0x94, 0x5A, 0x76, 0x6F, 0xFE, 0x27, + 0x52, 0x64, 0x3F, 0x60, 0xB8, 0xF5, 0xF0, 0x95, 0x0B, 0x9F, 0xA3, 0x0A, 0x96, 0xB8, 0xEE, 0x78, + 0x1F, 0x42, 0x2D, 0xF2, 0x2E, 0x90, 0xCE, 0x62, 0x1F, 0xA2, 0x65, 0xFD, 0x66, 0x21, 0x64, 0xC9, + 0xAD, 0x6A, 0xAC, 0xD9, 0x86, 0x1B, 0x16, 0x06, 0x73, 0x22, 0x56, 0x6C, 0x09, 0x1B, 0xCD, 0x8E, + 0xF8, 0xCE, 0xCA, 0xB2, 0xD5, 0x12, 0xF7, 0x4B, 0x74, 0x50, 0xD1, 0x78, 0xA4, 0x83, 0xE8, 0x38, + 0x43, 0xBE, 0xDE, 0x5A, 0x0B, 0xC3, 0x64, 0x53, 0x03, 0xC1, 0xEB, 0xA9, 0xEB, 0xCD, 0x92, 0x01, + 0xE1, 0x17, 0xB9, 0x7C, 0x2C, 0x10, 0x6D, 0xA1, 0x3F, 0x02, 0x19, 0xEA, 0x9E, 0x13, 0xAF, 0x65, + 0xF7, 0xBA, 0x9E, 0xFB, 0x29, 0xED, 0x95, 0x6F, 0xE5, 0xAA, 0x8B, 0x78, 0xFB, 0xE2, 0xB1, 0x9E, + 0x76, 0xC1, 0xF7, 0x02, 0x70, 0x58, 0x05, 0x6B, 0xB6, 0x8D, 0x29, 0x24, 0xD8, 0x1D, 0x7D, 0x64, + 0x7F, 0x70, 0x29, 0x8D, 0x3A, 0xF1, 0x4B, 0x2C, 0x52, 0xB3, 0x4A, 0x9A, 0x0B, 0xF0, 0x55, 0x8F, + 0xC1, 0xC8, 0x9B, 0x9B, 0x30, 0xB6, 0xCF, 0x08, 0xAA, 0x41, 0x60, 0xDE, 0xC1, 0x00, 0xA8, 0xDB, + 0x2D, 0xA7, 0xDE, 0x84, 0xAC, 0x92, 0xFC, 0x36, 0xE7, 0x17, 0x39, 0x09, 0x03, 0xFC, 0xB0, 0x0A, + 0x68, 0x62, 0xD0, 0xB1, 0xC0, 0xF9, 0x4A, 0x0D, 0xB5, 0x4C, 0xB8, 0xA5, 0xB6, 0xF1, 0xE4, 0x07, + 0x08, 0x2A, 0xDA, 0xDB, 0x5A, 0x1F, 0xD0, 0xA7, 0x01, 0xBB, 0x3C, 0x96, 0x6F, 0xCF, 0xCB, 0x0D, + 0xD7, 0xC3, 0x59, 0x30, 0xA3, 0xF1, 0x41, 0x4C, 0xC5, 0x8A, 0x88, 0xF0, 0xDC, 0xC6, 0x71, 0xEB, + 0x00, 0x79, 0xC2, 0x30, 0x27, 0xDC, 0xE7, 0xDE, 0x38, 0x87, 0xDD, 0x55, 0xB7, 0x9A, 0xF3, 0xB4, + 0x24, 0xBF, 0x26, 0x55, 0xBF, 0xCA, 0xE2, 0xCD, 0x01, 0x92, 0x04, 0x84, 0x41, 0xCE, 0x2A, 0xB2, + 0x44, 0xBE, 0x52, 0x5F, 0x07, 0xC2, 0x0F, 0x22, 0x64, 0x70, 0xC8, 0x5E, 0xFF, 0x9F, 0xCE, 0xCB, + 0x34, 0xC7, 0x4F, 0xD9, 0xA3, 0x7B, 0xBE, 0xAD, 0x8B, 0x10, 0x32, 0xAA, 0x39, 0xBE, 0xB1, 0x7C, + 0x16, 0x2E, 0x32, 0x61, 0x97, 0x3A, 0xE8, 0x41, 0x5F, 0x76, 0xE4, 0xAB, 0x55, 0x6B, 0x1D, 0x55, + 0x1A, 0x06, 0x9F, 0x17, 0x68, 0x23, 0x0D, 0x43, 0x27, 0xBC, 0xDC, 0x9F, 0x8D, 0x0F, 0xC4, 0x11, + 0xD3, 0x1F, 0xF3, 0x63, 0x0A, 0x19, 0x7F, 0x6F, 0xC6, 0xDE, 0x9F, 0xB6, 0x1F, 0x64, 0x21, 0x3A, + 0x76, 0xBD, 0x2F, 0x67, 0x1E, 0x2D, 0xFE, 0x24, 0x96, 0x01, 0x32, 0x7E, 0x07, 0xEA, 0xA8, 0x68, + 0x33, 0x00, 0xA0, 0x98, 0x1E, 0xBA, 0x69, 0xEF, 0xF8, 0x17, 0xF1, 0x9A, 0x09, 0x3E, 0x60, 0xEF, + 0x19, 0x95, 0xB2, 0x0F, 0xF0, 0xB9, 0xC4, 0x3B, 0x61, 0x03, 0x33, 0xE6, 0x3E, 0xDB, 0x65, 0x3E, + 0xAF, 0x09, 0x5C, 0xAB, 0xFB, 0x72, 0x6E, 0x8B, 0xAB, 0xA6, 0x6F, 0x8D, 0x6D, 0xDB, 0x13, 0xC2, + 0xF4, 0xF9, 0x25, 0x70, 0x56, 0x7F, 0xD3, 0xBE, 0x0A, 0xE3, 0x9C, 0x7A, 0x52, 0x9B, 0xC4, 0xDE, + 0xF0, 0x39, 0x70, 0xD5, 0x60, 0x65, 0x28, 0x1E, 0x99, 0x3A, 0x60, 0xA8, 0xD4, 0xED, 0xC5, 0xA7, + 0xC6, 0xF4, 0xE9, 0x1E, 0x9B, 0x6C, 0x79, 0xB0, 0x48, 0x34, 0x0E, 0x54, 0x92, 0x8D, 0x2B, 0x38, + 0x84, 0xDF, 0xCF, 0x86, 0x76, 0x6A, 0x74, 0x4B, 0xF2, 0xE8, 0x34, 0x99, 0x40, 0x31, 0x52, 0xC2, + 0xF5, 0xD8, 0x2B, 0x4E, 0x73, 0x08, 0xE1, 0x62, 0xF9, 0x84, 0xDF, 0x79, 0xC5, 0xB5, 0xB6, 0x9B, + 0xA9, 0x53, 0x16, 0xBE, 0xC6, 0x6E, 0xA6, 0x17, 0xB9, 0x55, 0xD4, 0xC7, 0xE2, 0x68, 0x86, 0x77, + 0x66, 0xBD, 0x5B, 0x98, 0xA8, 0x05, 0x84, 0x92, 0xCA, 0x8B, 0xDA, 0x30, 0x20, 0x5E, 0x93, 0x78, + 0xAD, 0xB0, 0x71, 0x4B, 0x43, 0x46, 0x3E, 0x9F, 0xE0, 0x26, 0x52, 0x6F, 0xA4, 0x28, 0xBC, 0xB0, + 0xBB, 0x6F, 0x44, 0x1F, 0xA6, 0x22, 0x9C, 0x51, 0xC3, 0xB2, 0x8C, 0x6A, 0xF5, 0x02, 0x19, 0x9F, + 0xD2, 0x1F, 0x6C, 0xC6, 0xDB, 0x21, 0xE9, 0x0F, 0xDF, 0xA7, 0xFB, 0x3A, 0xE0, 0xE9, 0x75, 0xD4, + 0x6B, 0x2F, 0x21, 0x96, 0xD2, 0x90, 0x83, 0xD4, 0x78, 0xE2, 0x34, 0x0B, 0x75, 0x9F, 0xBE, 0x93, + 0x97, 0xB6, 0xD7, 0xEF, 0xAD, 0xCF, 0xB8, 0xA6, 0xB8, 0x29, 0xBD, 0xBF, 0xC8, 0xA3, 0xE5, 0x2C, + 0x1C, 0x99, 0x40, 0x83, 0x51, 0xD5, 0x63, 0xCA, 0xC8, 0xCA, 0x67, 0x18, 0x61, 0x4D, 0x3F, 0xB7, + 0x95, 0xB6, 0x5B, 0x38, 0x0A, 0xA8, 0x60, 0x40, 0x7C, 0x06, 0xD4, 0xDE, 0xBD, 0xDE, 0x3E, 0x05, + 0x7D, 0xF5, 0xF8, 0x65, 0xE4, 0x5A, 0x42, 0x91, 0xD8, 0xFC, 0xCB, 0xA0, 0xC9, 0x91, 0x16, 0x9D, + 0x41, 0xB4, 0x16, 0x1C, 0xCE, 0x76, 0x75, 0xAC, 0x2F, 0x54, 0xF1, 0x0A, 0xEE, 0xD4, 0x88, 0xED, + 0x8F, 0xA0, 0x18, 0x53, 0x99, 0x46, 0x6E, 0x66, 0x48, 0xC8, 0x5F, 0xB9, 0xE0, 0xBF, 0xEF, 0x3E, + 0xC1, 0x64, 0xF2, 0xC4, 0xBD, 0x1B, 0x3B, 0xE5, 0x2A, 0x3E, 0x40, 0x2C, 0xE4, 0x8D, 0x32, 0xAB, + 0xC1, 0x0A, 0x28, 0xC0, 0xA5, 0xF6, 0xB5, 0xED, 0x5C, 0x68, 0x45, 0x05, 0xC0, 0x2F, 0x76, 0x95, + 0x5C, 0x16, 0x63, 0x51, 0xD0, 0x8E, 0x22, 0x2B, 0x74, 0x02, 0x10, 0xF6, 0x82, 0x00, 0x9A, 0x31, + 0x2C, 0xD0, 0xF2, 0x20, 0x44, 0x4A, 0x78, 0xE8, 0xEB, 0x04, 0x0F, 0x91, 0xDC, 0x21, 0xA5, 0xC1, + 0x9E, 0xCF, 0x78, 0x89, 0x19, 0xCE, 0xC8, 0x08, 0x91, 0x63, 0x25, 0x8C, 0x1D, 0x98, 0xB3, 0x27, + 0xB8, 0xFE, 0x55, 0x78, 0x35, 0x24, 0x96, 0x5C, 0x92, 0x9F, 0xC9, 0xD2, 0xAC, 0xCC, 0xCE, 0x65, + 0x5E, 0x36, 0xA8, 0xE2, 0x9B, 0x96, 0x69, 0x9B, 0x7F, 0xC3, 0xC4, 0x1B, 0x70, 0x76, 0xAA, 0xC0, + 0xFD, 0xBE, 0xAE, 0xDB, 0x85, 0x7F, 0x72, 0x06, 0xCE, 0x3E, 0x30, 0x5D, 0x3E, 0x24, 0x85, 0x14, + 0x8E, 0x32, 0x71, 0xEA, 0x80, 0x7E, 0x3B, 0x06, 0x65, 0xF1, 0xC8, 0xC8, 0x55, 0x2E, 0x2A, 0x9B, + 0x60, 0xF8, 0xC4, 0x1D, 0x75, 0x9F, 0xFC, 0xE4, 0x2B, 0x3F, 0x64, 0xE4, 0xFB, 0xE0, 0x24, 0x03, + 0xED, 0x8A, 0xD4, 0x7D, 0x31, 0x51, 0x1C, 0xCF, 0x61, 0xDD, 0x80, 0x05, 0x33, 0x59, 0xB0, 0xD1, + 0x83, 0x8F, 0xA8, 0xDF, 0xAC, 0x01, 0xB1, 0x2A, 0xE2, 0x5C, 0x6A, 0xA9, 0xB6, 0xCC, 0xB1, 0x03, + 0xF3, 0x7E, 0x14, 0x93, 0xB4, 0x85, 0x06, 0x33, 0x17, 0x15, 0x14, 0x75, 0xC8, 0x2B, 0xF3, 0xFF, + 0xD6, 0xAC, 0x6B, 0x32, 0xF2, 0xEC, 0x53, 0x8E, 0xEA, 0x04, 0xCB, 0xB0, 0x86, 0x1B, 0xD3, 0x75, + 0xF1, 0xF0, 0x3A, 0x56, 0xF6, 0x3F, 0x31, 0xD0, 0xC4, 0x79, 0x3C, 0x5F, 0x21, 0x73, 0xB5, 0xEA, + 0xED, 0x92, 0x0F, 0xDB, 0x32, 0x6E, 0xEC, 0x11, 0xC1, 0xF3, 0xF5, 0x36, 0x2D, 0x97, 0x5A, 0x50, + 0x13, 0x8F, 0xF7, 0x3A, 0x6E, 0x8A, 0x88, 0xB0, 0x0B, 0x11, 0x73, 0x0C, 0x7C, 0x5B, 0x02, 0xD3, + 0xDF, 0xBA, 0x4C, 0x3F, 0x6A, 0x69, 0x82, 0x68, 0x7A, 0xD1, 0xCD, 0xCA, 0xBA, 0xEB, 0xB1, 0xFD, + 0xCF, 0x45, 0x45, 0x94, 0x23, 0x58, 0xF4, 0xBF, 0x36, 0x5A, 0x6A, 0xBF, 0x0E, 0x7A, 0xF6, 0xE3, + 0x38, 0x42, 0x85, 0x55, 0x7F, 0x31, 0x49, 0x7A, 0x18, 0x21, 0x6C, 0xC9, 0xA5, 0x07, 0xAC, 0x1D, + 0x72, 0x47, 0x4E, 0x7F, 0xB7, 0x17, 0xBC, 0x7B, 0x26, 0xC5, 0x40, 0x2B, 0x95, 0xC6, 0xFF, 0xD1, + 0xCD, 0x46, 0xAB, 0xBB, 0xAB, 0xDC, 0x74, 0x60, 0x30, 0x75, 0x45, 0x49, 0xF7, 0x05, 0x52, 0xB8, + 0x32, 0xB2, 0x14, 0x8B, 0xE9, 0x10, 0x04, 0x28, 0xB4, 0x55, 0x50, 0xE4, 0x36, 0x9F, 0xF8, 0x21, + 0x24, 0xAB, 0x52, 0x19, 0x50, 0xA5, 0x7F, 0x68, 0x39, 0xDC, 0xC5, 0x8C, 0xAF, 0x10, 0xF6, 0x57, + 0x9B, 0x9B, 0x95, 0x33, 0x25, 0xC1, 0x68, 0xA5, 0xFA, 0xE7, 0xBA, 0x9D, 0xA0, 0x5E, 0x03, 0x92, + 0x14, 0x3E, 0xE9, 0xDC, 0x4F, 0x2F, 0xFD, 0x73, 0xE7, 0x49, 0x45, 0xEE, 0x80, 0x69, 0xB3, 0x1F, + 0x51, 0xE0, 0xB0, 0x1F, 0x89, 0xE0, 0xED, 0x1E, 0x89, 0x9C, 0xC3, 0xB7, 0x63, 0x05, 0x82, 0xD2, + 0xA7, 0xC7, 0xBC, 0xA5, 0x53, 0x9E, 0x17, 0x59, 0xFC, 0x8B, 0x48, 0x54, 0x32, 0x0E, 0x8E, 0x9F, + 0xC4, 0xBB, 0x43, 0x24, 0x78, 0x3A, 0xD2, 0xDF, 0xB7, 0xE0, 0xD7, 0x20, 0x9D, 0xBF, 0x49, 0xB8, + 0xD2, 0xB5, 0xA7, 0xD0, 0x1A, 0x32, 0x2B, 0x6C, 0xAD, 0xCA, 0x01, 0x16, 0xC7, 0xBD, 0x32, 0x7B, + 0x5A, 0x82, 0xB1, 0x2B, 0x42, 0x81, 0x4F, 0xB3, 0x91, 0x25, 0xB4, 0x91, 0x91, 0xD0, 0xB8, 0xB1, + 0x51, 0x24, 0x62, 0x92, 0x3D, 0x77, 0x3F, 0x25, 0x68, 0x9A, 0x34, 0x41, 0x33, 0xFF, 0xA0, 0xAF, + 0x1A, 0x14, 0x90, 0xE8, 0xC6, 0x36, 0x4F, 0xC5, 0xAF, 0xDC, 0x66, 0x2C, 0x1A, 0x73, 0x40, 0x5E, + 0xE8, 0x94, 0xD9, 0xC2, 0x27, 0x7F, 0xD7, 0x34, 0x06, 0xC4, 0x89, 0x00, 0xBF, 0x1A, 0x6A, 0x24, + 0x1A, 0x31, 0xEC, 0x91, 0xCD, 0xBB, 0x97, 0xBF, 0x50, 0x74, 0x24, 0xED, 0x0D, 0x72, 0xA5, 0xEA, + 0x43, 0x9E, 0x83, 0x33, 0x19, 0x8B, 0x97, 0x66, 0x9E, 0x43, 0xBA, 0xF2, 0x0A, 0x53, 0x37, 0xEF, + 0xE9, 0x60, 0x76, 0x98, 0x66, 0x0A, 0x21, 0x8D, 0xCA, 0x85, 0xB9, 0x9F, 0x91, 0xCC, 0x0D, 0xA3, + 0xCF, 0xB9, 0x65, 0xEA, 0x50, 0xD9, 0x8A, 0x0C, 0xAF, 0xA1, 0xB3, 0x3C, 0xBA, 0xA1, 0x6B, 0xBA, + 0x53, 0x37, 0xCA, 0x57, 0xF2, 0x55, 0x73, 0x12, 0x63, 0x61, 0x98, 0x01, 0x2C, 0x6E, 0x4A, 0x66, + 0xCC, 0x35, 0xA8, 0x86, 0x52, 0x45, 0xC0, 0xD4, 0x88, 0xDA, 0x00, 0xFF, 0x86, 0x84, 0x97, 0x1C, + 0x5B, 0x16, 0x96, 0x12, 0x77, 0x45, 0x81, 0x8F, 0xA2, 0xCC, 0x55, 0x12, 0x8D, 0x8C, 0xBD, 0xA8, + 0x61, 0xBA, 0x8D, 0x0C, 0xD4, 0x58, 0xC9, 0x03, 0xE4, 0x0C, 0xBE, 0x53, 0x4C, 0x9E, 0xA0, 0x58, + 0x6F, 0x16, 0xEB, 0x9F, 0x89, 0xB8, 0x94, 0x35, 0x58, 0xF8, 0x83, 0x94, 0x3D, 0x49, 0x44, 0x88, + 0x82, 0x18, 0xCB, 0xA9, 0x4B, 0x2D, 0x08, 0xAD, 0x8E, 0xCF, 0x08, 0x8F, 0xF3, 0x08, 0x3B, 0xFF, + 0x10, 0x80, 0x34, 0x31, 0xE7, 0xF9, 0xB9, 0x52, 0xD8, 0x78, 0xD0, 0x0F, 0x5E, 0xB7, 0xE2, 0xF9, + 0x37, 0xCB, 0xD4, 0x49, 0x36, 0x67, 0xF2, 0xE0, 0x78, 0xB9, 0x13, 0x89, 0xEC, 0x85, 0xFA, 0x6D, + 0x74, 0x5E, 0xC5, 0x59, 0xB6, 0xA9, 0xEE, 0x1C, 0x0E, 0xD4, 0xA3, 0x1E, 0x7A, 0x09, 0x0D, 0x4F, + 0xB8, 0x2D, 0xBE, 0x0C, 0xF7, 0x69, 0x04, 0x29, 0x44, 0x27, 0x94, 0x72, 0xC0, 0xAB, 0x86, 0x40, + 0x21, 0x5E, 0xC6, 0xBD, 0x24, 0x4A, 0x5E, 0x06, 0x46, 0x53, 0xA1, 0xD7, 0xD7, 0xBC, 0xD9, 0x97, + 0x13, 0xA9, 0x09, 0x15, 0x33, 0xBC, 0x9B, 0x13, 0x50, 0xCE, 0xA7, 0xDC, 0xFA, 0x69, 0x70, 0x22, + 0x14, 0x79, 0xD0, 0xA8, 0x3C, 0xB3, 0x46, 0xC3, 0xDA, 0x6C, 0x0C, 0xEC, 0x2A, 0xB2, 0x9B, 0x21, + 0xB2, 0xAD, 0x8C, 0x0C, 0x85, 0x9A, 0x8D, 0x7C, 0x10, 0xEA, 0x51, 0x1D, 0x2D, 0xDE, 0x7D, 0x8F}; + +const long int default_keys_enc_size = sizeof(default_keys_enc); diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index 0b2058293..d988dbb83 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -34,6 +34,7 @@ namespace Loader { using namespace Common::Literals; static constexpr u64 UPDATE_TID_HIGH = 0x0004000e00000000; +static constexpr u64 DLP_CHILD_TID_HIGH = 0x0004000100000000; FileType AppLoader_NCCH::IdentifyType(FileUtil::IOFile* file) { u32 magic; @@ -314,12 +315,16 @@ ResultStatus AppLoader_NCCH::Load(std::shared_ptr& process) { LOG_INFO(Loader, "Program ID: {}", program_id); - u64 update_tid = (ncch_program_id & 0xFFFFFFFFULL) | UPDATE_TID_HIGH; - update_ncch.OpenFile( - Service::AM::GetTitleContentPath(Service::FS::MediaType::SDMC, update_tid)); - result = update_ncch.Load(); - if (result == ResultStatus::Success) { - overlay_ncch = &update_ncch; + bool is_dlp_child = (ncch_program_id & 0xFFFFFFFF00000000) == DLP_CHILD_TID_HIGH; + + if (!is_dlp_child) { + u64 update_tid = (ncch_program_id & 0xFFFFFFFFULL) | UPDATE_TID_HIGH; + update_ncch.OpenFile( + Service::AM::GetTitleContentPath(Service::FS::MediaType::SDMC, update_tid)); + result = update_ncch.Load(); + if (result == ResultStatus::Success) { + overlay_ncch = &update_ncch; + } } if (auto room_member = Network::GetRoomMember().lock()) { From 4c054ff2e7e71f8adf46af22f38eb4740ad57198 Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Sun, 22 Feb 2026 22:41:24 +0100 Subject: [PATCH 10/42] video_core: Fix transferability issue in vulkan shader disk cache (#1770) --- .../renderer_vulkan/vk_shader_disk_cache.cpp | 52 +++++-------------- .../renderer_vulkan/vk_shader_disk_cache.h | 21 +++++--- 2 files changed, 26 insertions(+), 47 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp b/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp index 15e45ce81..bc21dc7c1 100644 --- a/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp @@ -713,7 +713,10 @@ bool ShaderDiskCache::InitVSCache(const std::atomic_bool& stop_loading, // New config entry, usually always taken unless there is duplicate entries on the cache // for some reason. - auto shader_it = programmable_vertex_cache.find(entry->spirv_entry_id); + // We cannot trust the SPIRV entry ID anymore if we are regenerating. + auto shader_it = regenerate_file + ? programmable_vertex_cache.end() + : programmable_vertex_cache.find(entry->spirv_entry_id); if (shader_it != programmable_vertex_cache.end()) { // The config entry uses a SPIRV entry that was already compiled (this is the usual // path when the cache doesn't need to be re-generated). @@ -722,40 +725,7 @@ bool ShaderDiskCache::InitVSCache(const std::atomic_bool& stop_loading, entry->spirv_entry_id); iter_config->second = &shader_it->second; - - if (regenerate_file) { - // In case we are re-generating the cache, we could only have gotten here if the - // SPIRV was already compiled and cached, so only cache the config. - AppendVSConfig(*regenerate_file, *entry, curr.Id()); - } - - bool new_program = known_vertex_programs.emplace(entry->program_entry_id).second; - if (new_program && regenerate_file) { - // If the vertex program is not known at this point we need to save it as well. - // This can happen to config entries that compile to the same SPIRV but use - // different program code (maybe because garbage data was in the program - // buffer). - auto program_it = pending_programs.find(entry->program_entry_id); - if (program_it == pending_programs.end()) { - // Program code not in disk cache, should never happen. - LOG_ERROR(Render_Vulkan, "Missing program code for config entry"); - programmable_vertex_map.erase(iter_config); - continue; - } - - // This is very rare so no need to use the LRU. - auto program_cache_entry = vs_cache.ReadAt(program_it->second); - const VSProgramEntry* program_entry; - - if (!program_cache_entry.Valid() || - program_cache_entry.Type() != CacheEntryType::VS_PROGRAM || - !(program_entry = program_cache_entry.Payload()) || - program_entry->version != VSProgramEntry::EXPECTED_VERSION) { - MALFORMED_DISK_CACHE; - } - - AppendVSProgram(*regenerate_file, *program_entry, entry->program_entry_id); - } + known_vertex_programs.emplace(entry->program_entry_id).second; } else { // Cached SPIRV not found, need to recompile. @@ -859,8 +829,7 @@ bool ShaderDiskCache::InitVSCache(const std::atomic_bool& stop_loading, // Asign the SPIRV shader to the config iter_config->second = &iter_prog->second; - LOG_DEBUG(Render_Vulkan, " linked with new SPIRV {:016X}.", - entry->spirv_entry_id); + LOG_DEBUG(Render_Vulkan, " linked with SPIRV {:016X}.", entry->spirv_entry_id); } } } @@ -1017,7 +986,7 @@ bool ShaderDiskCache::InitFSCache(const std::atomic_bool& stop_loading, tot_callback_index, "Fragment Shader"); } - LOG_DEBUG(Render_Vulkan, "Linking {:016X}.", curr.Id()); + LOG_DEBUG(Render_Vulkan, "Linking {:016X}.", offset.first); if (fragment_shaders.find(offset.first) != fragment_shaders.end()) { // SPIRV of config was already compiled, no need to regenerate @@ -1250,7 +1219,7 @@ bool ShaderDiskCache::InitGSCache(const std::atomic_bool& stop_loading, tot_callback_index, "Geometry Shader"); } - LOG_DEBUG(Render_Vulkan, "Linking {:016X}.", curr.Id()); + LOG_DEBUG(Render_Vulkan, "Linking {:016X}.", offset.first); if (fixed_geometry_shaders.find(offset.first) != fixed_geometry_shaders.end()) { // SPIRV of config was already compiled, no need to regenerate @@ -1400,6 +1369,11 @@ bool ShaderDiskCache::InitPLCache(const std::atomic_bool& stop_loading, // if any is missing we cannot build it. std::array shaders; + LOG_DEBUG(Render_Vulkan, " uses VS: {:016X}, FS: {:016X}, GS: {:016X}", + entry->pl_info.shader_ids[ProgramType::VS], + entry->pl_info.shader_ids[ProgramType::FS], + entry->pl_info.shader_ids[ProgramType::GS]); + if (entry->pl_info.shader_ids[ProgramType::VS]) { auto it_vs = programmable_vertex_map.find(entry->pl_info.shader_ids[ProgramType::VS]); diff --git a/src/video_core/renderer_vulkan/vk_shader_disk_cache.h b/src/video_core/renderer_vulkan/vk_shader_disk_cache.h index d6b487f18..c2e44215e 100644 --- a/src/video_core/renderer_vulkan/vk_shader_disk_cache.h +++ b/src/video_core/renderer_vulkan/vk_shader_disk_cache.h @@ -103,7 +103,8 @@ private: struct VSConfigEntry { static constexpr u8 EXPECTED_VERSION = 0; - u8 version; // Surprise tool that can help us later + u64 version; // Surprise tool that can help us later + u64 program_entry_id; u64 spirv_entry_id; Pica::Shader::Generator::PicaVSConfig vs_config; @@ -113,34 +114,38 @@ private: struct VSProgramEntry { static constexpr u8 EXPECTED_VERSION = 0; - u8 version; // Surprise tool that can help us later + u64 version; // Surprise tool that can help us later + u32 program_len; u32 swizzle_len; Pica::ProgramCode program_code; Pica::SwizzleData swizzle_code; }; - static_assert(sizeof(VSProgramEntry) == 32780); + static_assert(sizeof(VSProgramEntry) == 32784); struct FSConfigEntry { static constexpr u8 EXPECTED_VERSION = 0; - u8 version; // Surprise tool that can help us later + u64 version; // Surprise tool that can help us later + Pica::Shader::FSConfig fs_config; }; - static_assert(sizeof(FSConfigEntry) == 276); + static_assert(sizeof(FSConfigEntry) == 280); struct GSConfigEntry { static constexpr u8 EXPECTED_VERSION = 0; - u8 version; // Surprise tool that can help us later + u64 version; // Surprise tool that can help us later + Pica::Shader::Generator::PicaFixedGSConfig gs_config; }; - static_assert(sizeof(GSConfigEntry) == 44); + static_assert(sizeof(GSConfigEntry) == 48); struct PLConfigEntry { static constexpr u8 EXPECTED_VERSION = 0; - u8 version; // Surprise tool that can help us later + u64 version; // Surprise tool that can help us later + StaticPipelineInfo pl_info; }; static_assert(sizeof(PLConfigEntry) == 152); From fcb345e2734cb01de7bc9b052204edda6a1608b1 Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Mon, 23 Feb 2026 14:26:20 +0100 Subject: [PATCH 11/42] logging: Check filter before log format (#1773) --- src/common/logging/backend.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp index e1b8621bd..837911981 100644 --- a/src/common/logging/backend.cpp +++ b/src/common/logging/backend.cpp @@ -333,15 +333,16 @@ public: return true; } + const Filter& GetFilter() const { + return filter; + } + void SetColorConsoleBackendEnabled(bool enabled) { color_console_backend.SetEnabled(enabled); } void PushEntry(Class log_class, Level log_level, const char* filename, unsigned int line_num, const char* function, std::string message) { - if (!filter.CheckMessage(log_class, log_level)) { - return; - } Entry new_entry = CreateEntry(log_class, log_level, filename, line_num, function, std::move(message), time_origin); if (!regex_filter.empty() && @@ -599,6 +600,9 @@ void FmtLogMessageImpl(Class log_class, Level log_level, const char* filename, } if (logging_initialized) [[likely]] { + if (!Impl::Instance().GetFilter().CheckMessage(log_class, log_level)) { + return; + } Impl::Instance().PushEntry(log_class, log_level, filename, line_num, function, fmt::vformat(format, args)); } else { From 8b72dcb235151389f82db9207ba1295fc509c3d0 Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Mon, 23 Feb 2026 15:45:26 +0100 Subject: [PATCH 12/42] video_core: Do not spam file IO when reading vulkan shader disk cache (#1774) --- .../renderer_vulkan/vk_shader_disk_cache.cpp | 50 ++++++++++++++++--- .../renderer_vulkan/vk_shader_disk_cache.h | 5 ++ 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp b/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp index bc21dc7c1..fd8809443 100644 --- a/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp @@ -301,11 +301,11 @@ ShaderDiskCache::CacheEntry::CacheEntryHeader ShaderDiskCache::CacheFile::ReadAt CacheEntry::CacheEntryHeader header; - if (file.ReadAtArray(&header, 1, position) == sizeof(CacheEntry::CacheEntryHeader)) { - return header; + if (!ReadFromFileCached(&header, position, sizeof(header))) { + return CacheEntry::CacheEntryHeader(); } - return CacheEntry::CacheEntryHeader(); + return header; } ShaderDiskCache::CacheEntry ShaderDiskCache::CacheFile::ReadAt(size_t position) { @@ -323,8 +323,8 @@ ShaderDiskCache::CacheEntry ShaderDiskCache::CacheFile::ReadAt(size_t position) u32 payload_size = res.header.entry_size - headers_size; std::vector payload(payload_size); - if (file.ReadAtBytes(payload.data(), payload_size, - position + sizeof(CacheEntry::CacheEntryHeader)) == payload_size) { + if (ReadFromFileCached(payload.data(), (position + sizeof(CacheEntry::CacheEntryHeader)), + payload_size)) { // Decompress data if needed if (res.header.zstd_compressed) { if (Common::Compression::GetDecompressedSize(payload) < @@ -344,6 +344,7 @@ ShaderDiskCache::CacheEntry ShaderDiskCache::CacheFile::ReadAt(size_t position) size_t ShaderDiskCache::CacheFile::GetTotalEntries() { if (!file.IsGood()) { next_entry_id = SIZE_MAX; + file_size = 0; return next_entry_id; } @@ -351,7 +352,7 @@ size_t ShaderDiskCache::CacheFile::GetTotalEntries() { return next_entry_id; } - const size_t file_size = file.GetSize(); + file_size = file.GetSize(); if (file_size == 0) { next_entry_id = 0; return next_entry_id; @@ -363,6 +364,7 @@ size_t ShaderDiskCache::CacheFile::GetTotalEntries() { footer.version == CacheEntry::CacheEntryFooter::ENTRY_VERSION) { next_entry_id = footer.entry_id + 1; } else { + file_size = 0; return SIZE_MAX; } @@ -428,7 +430,9 @@ bool ShaderDiskCache::CacheFile::SwitchMode(CacheOpMode mode) { switch (mode) { case CacheOpMode::READ: { - next_entry_id = SIZE_MAX; // Force reading entries agains + next_entry_id = SIZE_MAX; // Force reading entries again + cached_file_data_start = 0; + cached_file_data.clear(); file = FileUtil::IOFile(filepath, "rb"); bool is_open = file.IsGood(); if (is_open) { @@ -443,6 +447,8 @@ bool ShaderDiskCache::CacheFile::SwitchMode(CacheOpMode mode) { return false; } file.Close(); + cached_file_data_start = 0; + cached_file_data.clear(); curr_mode = mode; if (next_entry_id == SIZE_MAX) { // Cannot append if getting total items fails @@ -454,6 +460,8 @@ bool ShaderDiskCache::CacheFile::SwitchMode(CacheOpMode mode) { } case CacheOpMode::DELETE: { next_entry_id = SIZE_MAX; + cached_file_data_start = 0; + cached_file_data.clear(); file.Close(); curr_mode = mode; return FileUtil::Delete(filepath); @@ -473,6 +481,34 @@ bool ShaderDiskCache::CacheFile::SwitchMode(CacheOpMode mode) { return false; } +bool ShaderDiskCache::CacheFile::ReadFromFileCached(void* dst, size_t position, size_t size) { + if (!dst || position + size > file_size) { + return false; + } + + size_t offset = position - cached_file_data_start; + if (position < cached_file_data_start || offset > cached_file_data.size() || + size > cached_file_data.size() - offset) { + if (size > CacheEntry::MAX_ENTRY_SIZE) { + return false; + } + + size_t to_read = std::min(CacheEntry::MAX_ENTRY_SIZE, file_size - position); + + cached_file_data_start = position; + cached_file_data.resize(to_read); + + if (file.ReadAtBytes(cached_file_data.data(), to_read, position) != to_read) { + return false; + } + + offset = 0; + } + + std::memcpy(dst, cached_file_data.data() + offset, size); + return true; +} + std::string ShaderDiskCache::GetVSFile(u64 title_id, bool is_temp) const { return parent.GetTransferableDir() + DIR_SEP + fmt::format("{:016X}_vs", title_id) + (is_temp ? "_temp" : "") + ".vkch"; diff --git a/src/video_core/renderer_vulkan/vk_shader_disk_cache.h b/src/video_core/renderer_vulkan/vk_shader_disk_cache.h index c2e44215e..e05d43d09 100644 --- a/src/video_core/renderer_vulkan/vk_shader_disk_cache.h +++ b/src/video_core/renderer_vulkan/vk_shader_disk_cache.h @@ -292,9 +292,14 @@ private: bool SwitchMode(CacheOpMode mode); private: + bool ReadFromFileCached(void* dst, size_t absolute_pos, size_t size); + CacheOpMode curr_mode = CacheOpMode::NONE; std::string filepath; FileUtil::IOFile file{}; + size_t file_size; + size_t cached_file_data_start{}; + std::vector cached_file_data; std::atomic next_entry_id = SIZE_MAX; Common::ThreadWorker append_worker{1, "Disk Shader Cache Append Worker"}; }; From 5d583a8a411d3c1c91e70ff64b80b9d7ffe20616 Mon Sep 17 00:00:00 2001 From: OpenSauce04 Date: Mon, 23 Feb 2026 15:55:50 +0000 Subject: [PATCH 13/42] .gitlab-ci.yml: Bump macOS libretro core minimum OS version to 11.0 --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e8f4f141b..75a6bfa5b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -82,7 +82,7 @@ libretro-build-osx-x64: - mac-apple-silicon variables: CORE_ARGS: ${BASE_CORE_ARGS} -DCMAKE_OSX_ARCHITECTURES=x86_64 - MACOSX_DEPLOYMENT_TARGET: "10.15" + MACOSX_DEPLOYMENT_TARGET: "11.0" extends: - .core-defs - .libretro-osx-cmake-x86_64 From 76db4b08f69c7f0c42055bf7454453e4bd7f898f Mon Sep 17 00:00:00 2001 From: OpenSauce04 Date: Mon, 23 Feb 2026 18:25:05 +0000 Subject: [PATCH 14/42] .gitlab-ci.yml: Fixed ARM64 macOS not having minimum OS correctly set --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 75a6bfa5b..64cafa617 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -92,6 +92,8 @@ libretro-build-osx-arm64: extends: - .core-defs - .libretro-osx-cmake-arm64 + variables: + MACOSX_DEPLOYMENT_TARGET: "11.0" ################################### CELLULAR ################################# # Android ARMv8a From 13e0fdeac1d11dcbfe499fa704eb2d4e251c9db5 Mon Sep 17 00:00:00 2001 From: RedBlackAka <140876408+RedBlackAka@users.noreply.github.com> Date: Mon, 23 Feb 2026 21:35:02 +0100 Subject: [PATCH 15/42] libretro core: Add some ifdefs (#1765) --- src/audio_core/input_details.h | 2 ++ src/audio_core/sink_details.h | 2 ++ src/common/file_util.h | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/audio_core/input_details.h b/src/audio_core/input_details.h index 709453eac..fcefb8343 100644 --- a/src/audio_core/input_details.h +++ b/src/audio_core/input_details.h @@ -24,7 +24,9 @@ enum class InputType : u32 { Static = 2, Cubeb = 3, OpenAL = 4, +#ifdef HAVE_LIBRETRO LibRetro = 5, +#endif }; struct InputDetails { diff --git a/src/audio_core/sink_details.h b/src/audio_core/sink_details.h index a3e7a4edb..299aee459 100644 --- a/src/audio_core/sink_details.h +++ b/src/audio_core/sink_details.h @@ -20,7 +20,9 @@ enum class SinkType : u32 { Cubeb = 2, OpenAL = 3, SDL2 = 4, +#ifdef HAVE_LIBRETRO LibRetro = 5, +#endif }; struct SinkDetails { diff --git a/src/common/file_util.h b/src/common/file_util.h index 58cc4f7aa..ea4af8cb3 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -15,7 +15,9 @@ #include #include #include +#ifdef HAVE_LIBRETRO #include +#endif #include #include #include From fe59958b63d3bed4cf085befcb56385d212e1d0f Mon Sep 17 00:00:00 2001 From: Eric Warmenhoven Date: Mon, 23 Feb 2026 15:49:42 -0500 Subject: [PATCH 16/42] older tvos hardware does not support layered rendering --- src/video_core/renderer_vulkan/vk_instance.cpp | 14 ++++++++++++++ .../renderer_vulkan/vk_texture_runtime.cpp | 3 +++ 2 files changed, 17 insertions(+) diff --git a/src/video_core/renderer_vulkan/vk_instance.cpp b/src/video_core/renderer_vulkan/vk_instance.cpp index 3f968e4c9..a2c2cb6be 100644 --- a/src/video_core/renderer_vulkan/vk_instance.cpp +++ b/src/video_core/renderer_vulkan/vk_instance.cpp @@ -618,6 +618,20 @@ bool Instance::CreateDevice() { #undef PROP_GET #undef FEAT_SET + // Check layered rendering support on MoltenVK + // MoltenVK maps Metal's layeredRendering capability to shaderOutputLayer + if (is_moltenvk) { + vk::PhysicalDeviceVulkan12Features vulkan12_features; + vk::PhysicalDeviceFeatures2 features2; + features2.pNext = &vulkan12_features; + physical_device.getFeatures2(&features2); + if (!vulkan12_features.shaderOutputLayer) { + LOG_INFO(Render_Vulkan, + "Disabling layered rendering (shaderOutputLayer not supported by device)"); + layered_rendering_supported = false; + } + } + #ifdef HAVE_LIBRETRO // LibRetro builds: device already created by frontend, just return after feature detection return true; diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index 86f46787e..8bfa0e30f 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -169,6 +169,9 @@ void Handle::Create(const Instance* instance, u32 width, u32 height, u32 levels, std::string_view debug_name) { const bool is_cube_map = type == TextureType::CubeMap && instance->IsLayeredRenderingSupported(); + if (!is_cube_map) { + flags &= ~vk::ImageCreateFlagBits::eCubeCompatible; + } this->instance = instance; this->width = width; From 8fac24d2a414cf362b3d93ab2e5dd310f8caa2b3 Mon Sep 17 00:00:00 2001 From: Eric Warmenhoven Date: Mon, 23 Feb 2026 16:29:08 -0500 Subject: [PATCH 17/42] libretro: better load failure check --- src/citra_libretro/citra_libretro.cpp | 42 +++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/citra_libretro/citra_libretro.cpp b/src/citra_libretro/citra_libretro.cpp index 1fb73cb23..6028349ae 100644 --- a/src/citra_libretro/citra_libretro.cpp +++ b/src/citra_libretro/citra_libretro.cpp @@ -239,6 +239,14 @@ static void UpdateSettings() { * libretro callback; Called every game tick. */ void retro_run() { + if (!emu_instance->game_loaded) { + // Game failed to load (e.g. encrypted ROM, bad path). + // Present an empty frame so RetroArch doesn't hang. + LibRetro::PollInput(); + LibRetro::UploadVideoFrame(nullptr, 0, 0, 0); + return; + } + // Check to see if we actually have any config updates to process. if (LibRetro::HasUpdatedConfig()) { LibRetro::ParseCoreOptions(); @@ -527,6 +535,40 @@ bool retro_load_game(const struct retro_game_info* info) { // the graphics context ready and available before calling System::Load. LibRetro::settings.file_path = info->path; + // Early validation: check that the ROM can be loaded before committing to + // the HW renderer setup. Without this, failures (encrypted ROMs, bad files) + // are only detected in context_reset after retro_load_game already returned + // true, leaving the frontend stuck on a black screen. + // GetLoader + LoadKernelMemoryMode only read ROM headers — no renderer needed. + { + auto loader = Loader::GetLoader(LibRetro::settings.file_path); + if (!loader) { + LibRetro::DisplayMessage("Failed to obtain loader for the specified ROM."); + return false; + } + auto [memory_mode, result] = loader->LoadKernelMemoryMode(); + if (result != Loader::ResultStatus::Success) { + switch (result) { + case Loader::ResultStatus::ErrorEncrypted: + LibRetro::DisplayMessage( + "This ROM is encrypted and must be decrypted before use with Azahar."); + break; + case Loader::ResultStatus::ErrorInvalidFormat: + LibRetro::DisplayMessage("The ROM format is not supported."); + break; + case Loader::ResultStatus::ErrorGbaTitle: + LibRetro::DisplayMessage("GBA Virtual Console titles are not supported."); + break; + default: + LibRetro::DisplayMessage("Failed to load ROM metadata."); + break; + } + return false; + } + // Stash the loader so System::Load can reuse it instead of re-opening + Core::System::GetInstance().RegisterAppLoaderEarly(loader); + } + if (!LibRetro::SetPixelFormat(RETRO_PIXEL_FORMAT_XRGB8888)) { LibRetro::DisplayMessage("XRGB8888 is not supported."); return false; From 27c3e0e5c39b7bb43f23130a0fcb59afde28ac30 Mon Sep 17 00:00:00 2001 From: Eric Warmenhoven Date: Mon, 23 Feb 2026 16:29:56 -0500 Subject: [PATCH 18/42] libretro: better safety on vkDevice feature checks --- src/citra_libretro/libretro_vk.cpp | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/citra_libretro/libretro_vk.cpp b/src/citra_libretro/libretro_vk.cpp index eed09d5bc..2a9c6d9bf 100644 --- a/src/citra_libretro/libretro_vk.cpp +++ b/src/citra_libretro/libretro_vk.cpp @@ -115,11 +115,24 @@ bool CreateVulkanDevice(struct retro_vulkan_context* context, VkInstance instanc } } - // Request features we need (these will be OR'd with frontend requirements) - // The Instance class will validate these against actual device capabilities - merged_features.geometryShader = VK_TRUE; // Used for certain rendering effects - merged_features.logicOp = VK_TRUE; // Used for blending modes - merged_features.samplerAnisotropy = VK_TRUE; // Used for texture filtering + // Query actual device features so we only request what's supported + PFN_vkGetPhysicalDeviceFeatures vkGetPhysicalDeviceFeatures = + (PFN_vkGetPhysicalDeviceFeatures)get_instance_proc_addr(instance, + "vkGetPhysicalDeviceFeatures"); + VkPhysicalDeviceFeatures device_features{}; + vkGetPhysicalDeviceFeatures(gpu, &device_features); + + // Request features we want, gated by actual device support + if (device_features.geometryShader) + merged_features.geometryShader = VK_TRUE; + if (device_features.logicOp) + merged_features.logicOp = VK_TRUE; + if (device_features.samplerAnisotropy) + merged_features.samplerAnisotropy = VK_TRUE; + if (device_features.fragmentStoresAndAtomics) + merged_features.fragmentStoresAndAtomics = VK_TRUE; + if (device_features.shaderClipDistance) + merged_features.shaderClipDistance = VK_TRUE; // Find queue family with graphics support PFN_vkGetPhysicalDeviceQueueFamilyProperties vkGetPhysicalDeviceQueueFamilyProperties = From 17f4c52e56e3a19b84d0ee2e8b9e620f4b0a3aee Mon Sep 17 00:00:00 2001 From: Eric Warmenhoven Date: Mon, 23 Feb 2026 16:31:33 -0500 Subject: [PATCH 19/42] libretro: default system type to New 3DS --- src/citra_libretro/core_settings.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/citra_libretro/core_settings.cpp b/src/citra_libretro/core_settings.cpp index 86e7098f6..b155a9b0f 100644 --- a/src/citra_libretro/core_settings.cpp +++ b/src/citra_libretro/core_settings.cpp @@ -172,11 +172,11 @@ static constexpr retro_core_option_v2_definition option_definitions[] = { nullptr, config::category::system, { - { "Old 3DS", "Original 3DS" }, { "New 3DS", "New 3DS" }, + { "Old 3DS", "Original 3DS" }, { nullptr, nullptr } }, - "Old 3DS" + "New 3DS" }, { config::system::region, @@ -779,7 +779,7 @@ static Service::CFG::SystemLanguage GetLanguageValue(const std::string& name) { static void ParseSystemOptions(void) { Settings::values.is_new_3ds = - LibRetro::FetchVariable(config::system::is_new_3ds, "Old 3DS") == "New 3DS"; + LibRetro::FetchVariable(config::system::is_new_3ds, "New 3DS") == "New 3DS"; Settings::values.region_value = GetRegionValue(LibRetro::FetchVariable("citra_region_value", "Auto")); From d721cbe29bfca9fa38f2a4443234ce617c02d23c Mon Sep 17 00:00:00 2001 From: OpenSauce04 Date: Mon, 23 Feb 2026 22:16:09 +0000 Subject: [PATCH 20/42] cmake: Only add catch2 library if ENABLE_TESTS is enabled --- externals/CMakeLists.txt | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index d7cf35f3b..db8de43a7 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -50,15 +50,17 @@ else() endif() # Catch2 -add_library(catch2 INTERFACE) -if(USE_SYSTEM_CATCH2) - find_package(Catch2 3.0.0 REQUIRED) -else() - set(CATCH_INSTALL_DOCS OFF CACHE BOOL "") - set(CATCH_INSTALL_EXTRAS OFF CACHE BOOL "") - add_subdirectory(catch2) +if (ENABLE_TESTS) + add_library(catch2 INTERFACE) + if(USE_SYSTEM_CATCH2) + find_package(Catch2 3.0.0 REQUIRED) + else() + set(CATCH_INSTALL_DOCS OFF CACHE BOOL "") + set(CATCH_INSTALL_EXTRAS OFF CACHE BOOL "") + add_subdirectory(catch2) + endif() + target_link_libraries(catch2 INTERFACE Catch2::Catch2WithMain) endif() -target_link_libraries(catch2 INTERFACE Catch2::Catch2WithMain) # Crypto++ if(USE_SYSTEM_CRYPTOPP) From 15bdd27b9c0f62a7c07d57a0311f14d652ca3ba1 Mon Sep 17 00:00:00 2001 From: RedBlackAka <140876408+RedBlackAka@users.noreply.github.com> Date: Tue, 24 Feb 2026 18:06:29 +0100 Subject: [PATCH 21/42] citra-meta: Use dedicated GPU by default on AMD (#1783) --- src/citra_meta/main.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/citra_meta/main.cpp b/src/citra_meta/main.cpp index 92ea7756f..b7a7e5584 100644 --- a/src/citra_meta/main.cpp +++ b/src/citra_meta/main.cpp @@ -20,8 +20,10 @@ #ifdef _WIN32 extern "C" { -// tells Nvidia drivers to use the dedicated GPU by default on laptops with switchable graphics +// tells Nvidia and AMD drivers to use the dedicated GPU by default on laptops with switchable +// graphics __declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001; +__declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; } #endif From b3fd0b6c8912214ce863c0ba428fca8861d90e8a Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Wed, 25 Feb 2026 13:08:18 +0100 Subject: [PATCH 22/42] video_core: Apply texture filter to color surfaces (#1784) --- src/video_core/renderer_vulkan/vk_texture_runtime.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index 8bfa0e30f..abe9c400e 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -922,7 +922,8 @@ void Surface::Upload(const VideoCore::BufferTextureCopy& upload, .src_rect = upload.texture_rect, .dst_rect = upload.texture_rect * res_scale, }; - if (type != SurfaceType::Texture || !runtime->blit_helper.Filter(*this, blit)) { + if ((type != SurfaceType::Color && type != SurfaceType::Texture) || + !runtime->blit_helper.Filter(*this, blit)) { BlitScale(blit, true); } } From 7d19679cc536a29c21e5ecd0646ea6e02f8cb7fc Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Wed, 25 Feb 2026 19:53:03 +0100 Subject: [PATCH 23/42] video_core: vk_texture_runtime: Refactor and fix resource leak (#1790) --- .../renderer_vulkan/vk_texture_runtime.cpp | 139 +++++++++--------- .../renderer_vulkan/vk_texture_runtime.h | 62 ++++++-- 2 files changed, 116 insertions(+), 85 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index abe9c400e..52b7c6392 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -163,17 +163,15 @@ constexpr u64 DOWNLOAD_BUFFER_SIZE = 16_MiB; } // Anonymous namespace -void Handle::Create(const Instance* instance, u32 width, u32 height, u32 levels, TextureType type, - vk::Format format, vk::ImageUsageFlags usage, vk::ImageCreateFlags flags, +void Handle::Create(u32 width, u32 height, u32 levels, TextureType type, vk::Format format, + vk::ImageUsageFlags usage, vk::ImageCreateFlags flags, vk::ImageAspectFlags aspect, bool need_format_list, std::string_view debug_name) { - const bool is_cube_map = - type == TextureType::CubeMap && instance->IsLayeredRenderingSupported(); + const bool is_cube_map = type == TextureType::CubeMap && instance.IsLayeredRenderingSupported(); if (!is_cube_map) { flags &= ~vk::ImageCreateFlagBits::eCubeCompatible; } - this->instance = instance; this->width = width; this->height = height; this->levels = levels; @@ -212,7 +210,7 @@ void Handle::Create(const Instance* instance, u32 width, u32 height, u32 levels, VkImage unsafe_image{}; VkImageCreateInfo unsafe_image_info = static_cast(image_info); - VkResult result = vmaCreateImage(instance->GetAllocator(), &unsafe_image_info, &alloc_info, + VkResult result = vmaCreateImage(instance.GetAllocator(), &unsafe_image_info, &alloc_info, &unsafe_image, &allocation, nullptr); if (result != VK_SUCCESS) [[unlikely]] { LOG_CRITICAL(Render_Vulkan, "Failed allocating image with error {}", result); @@ -233,25 +231,20 @@ void Handle::Create(const Instance* instance, u32 width, u32 height, u32 levels, .layerCount = VK_REMAINING_ARRAY_LAYERS, }, }; - image_views[ViewType::Sample] = instance->GetDevice().createImageView(view_info); + image_views[ViewType::Sample] = instance.GetDevice().createImageView(view_info); if (levels == 1) { image_views[ViewType::Mip0] = image_views[ViewType::Mip0]; } - if (!debug_name.empty() && instance->HasDebuggingToolAttached()) { - SetObjectName(instance->GetDevice(), image, debug_name); - SetObjectName(instance->GetDevice(), image_views[ViewType::Sample], "{} View({})", + if (!debug_name.empty() && instance.HasDebuggingToolAttached()) { + SetObjectName(instance.GetDevice(), image, debug_name); + SetObjectName(instance.GetDevice(), image_views[ViewType::Sample], "{} View({})", debug_name, vk::to_string(aspect)); } } void Handle::Destroy() { - if (!allocation || !instance) { - return; - } - - const auto device = instance->GetDevice(); - const auto allocator = instance->GetAllocator(); + const auto device = instance.GetDevice(); // Image views if (auto view = image_views[ViewType::Sample]) { @@ -277,7 +270,9 @@ void Handle::Destroy() { framebuffer = VK_NULL_HANDLE; } - vmaDestroyImage(allocator, image, allocation); + if (allocation) { + vmaDestroyImage(instance.GetAllocator(), image, allocation); + } image = VK_NULL_HANDLE; allocation = VK_NULL_HANDLE; @@ -733,8 +728,9 @@ bool TextureRuntime::NeedsConversion(VideoCore::PixelFormat format) const { } Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& params) - : SurfaceBase{params}, runtime{&runtime_}, instance{&runtime_.GetInstance()}, - scheduler{&runtime_.GetScheduler()}, traits{instance->GetTraits(pixel_format)} { + : SurfaceBase{params}, runtime{runtime_}, instance{runtime_.GetInstance()}, + scheduler{runtime_.GetScheduler()}, traits{instance.GetTraits(pixel_format)}, + handles{Handle(instance), Handle(instance), Handle(instance), Handle(instance)} { if (pixel_format == VideoCore::PixelFormat::Invalid || !traits.transfer_support) { return; @@ -765,22 +761,22 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param usage |= vk::ImageUsageFlagBits::eColorAttachment; } - const bool need_format_list = is_mutable && instance->IsImageFormatListSupported(); - handles[Type::Base].Create(instance, width, height, levels, texture_type, format, usage, flags, + const bool need_format_list = is_mutable && instance.IsImageFormatListSupported(); + handles[Type::Base].Create(width, height, levels, texture_type, format, usage, flags, traits.aspect, need_format_list, DebugName(false)); raw_images[num_images++] = handles[Type::Base].image; if (res_scale != 1) { - handles[Type::Scaled].Create(instance, GetScaledWidth(), GetScaledHeight(), levels, - texture_type, format, usage, flags, traits.aspect, - need_format_list, DebugName(true)); + handles[Type::Scaled].Create(GetScaledWidth(), GetScaledHeight(), levels, texture_type, + format, usage, flags, traits.aspect, need_format_list, + DebugName(true)); raw_images[num_images++] = handles[Type::Scaled].image; } current = res_scale != 1 ? Type::Scaled : Type::Base; - runtime->renderpass_cache.EndRendering(); - scheduler->Record([raw_images, num_images, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { + runtime.renderpass_cache.EndRendering(); + scheduler.Record([raw_images, num_images, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { std::array barriers; MakeInitBarriers(aspect, num_images, raw_images, barriers); cmdbuf.pipelineBarrier( @@ -791,8 +787,9 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceBase& surface, const VideoCore::Material* mat) - : SurfaceBase{surface}, runtime{&runtime_}, instance{&runtime_.GetInstance()}, - scheduler{&runtime_.GetScheduler()}, traits{instance->GetTraits(mat->format)} { + : SurfaceBase{surface}, runtime{runtime_}, instance{runtime_.GetInstance()}, + scheduler{runtime_.GetScheduler()}, traits{instance.GetTraits(mat->format)}, + handles{Handle(instance), Handle(instance), Handle(instance), Handle(instance)} { if (!traits.transfer_support) { return; } @@ -809,26 +806,26 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceBase& surface } const std::string debug_name = DebugName(false, true); - handles[Type::Base].Create(instance, mat->width, mat->height, levels, texture_type, format, - traits.usage, flags, traits.aspect, false, debug_name); + handles[Type::Base].Create(mat->width, mat->height, levels, texture_type, format, traits.usage, + flags, traits.aspect, false, debug_name); raw_images[num_images++] = handles[Type::Base].image; if (res_scale != 1) { - handles[Type::Scaled].Create(instance, mat->width, mat->height, levels, texture_type, + handles[Type::Scaled].Create(mat->width, mat->height, levels, texture_type, vk::Format::eR8G8B8A8Unorm, traits.usage, flags, traits.aspect, false, debug_name); raw_images[num_images++] = handles[Type::Scaled].image; } if (has_normal) { - handles[Type::Custom].Create(instance, mat->width, mat->height, levels, texture_type, - format, traits.usage, flags, traits.aspect, false, debug_name); + handles[Type::Custom].Create(mat->width, mat->height, levels, texture_type, format, + traits.usage, flags, traits.aspect, false, debug_name); raw_images[num_images++] = handles[Type::Custom].image; } current = res_scale != 1 ? Type::Scaled : Type::Base; - runtime->renderpass_cache.EndRendering(); - scheduler->Record([raw_images, num_images, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { + runtime.renderpass_cache.EndRendering(); + scheduler.Record([raw_images, num_images, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { std::array barriers; MakeInitBarriers(aspect, num_images, raw_images, barriers); cmdbuf.pipelineBarrier( @@ -842,7 +839,7 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceBase& surface void Surface::Upload(const VideoCore::BufferTextureCopy& upload, const VideoCore::StagingData& staging) { - runtime->renderpass_cache.EndRendering(); + runtime.renderpass_cache.EndRendering(); const RecordParams params = { .aspect = Aspect(), @@ -851,8 +848,8 @@ void Surface::Upload(const VideoCore::BufferTextureCopy& upload, .src_image = Image(Type::Base), }; - scheduler->Record([buffer = runtime->upload_buffer.Handle(), format = traits.native, params, - staging, upload](vk::CommandBuffer cmdbuf) { + scheduler.Record([buffer = runtime.upload_buffer.Handle(), format = traits.native, params, + staging, upload](vk::CommandBuffer cmdbuf) { boost::container::static_vector buffer_image_copies; const auto rect = upload.texture_rect; @@ -911,7 +908,7 @@ void Surface::Upload(const VideoCore::BufferTextureCopy& upload, vk::DependencyFlagBits::eByRegion, {}, {}, write_barrier); }); - runtime->upload_buffer.Commit(staging.size); + runtime.upload_buffer.Commit(staging.size); if (res_scale != 1) { ASSERT_MSG(handles[Type::Scaled], "Scaled allocation missing during upload"); @@ -923,7 +920,7 @@ void Surface::Upload(const VideoCore::BufferTextureCopy& upload, .dst_rect = upload.texture_rect * res_scale, }; if ((type != SurfaceType::Color && type != SurfaceType::Texture) || - !runtime->blit_helper.Filter(*this, blit)) { + !runtime.blit_helper.Filter(*this, blit)) { BlitScale(blit, true); } } @@ -944,12 +941,12 @@ void Surface::UploadCustom(const VideoCore::Material* material, u32 level) { .src_image = Image(type), }; - const auto [data, offset, invalidate] = runtime->upload_buffer.Map(custom_size, 0); + const auto [data, offset, invalidate] = runtime.upload_buffer.Map(custom_size, 0); std::memcpy(data, texture->data.data(), custom_size); - runtime->upload_buffer.Commit(custom_size); + runtime.upload_buffer.Commit(custom_size); - scheduler->Record([buffer = runtime->upload_buffer.Handle(), level, params, rect, - offset = offset](vk::CommandBuffer cmdbuf) { + scheduler.Record([buffer = runtime.upload_buffer.Handle(), level, params, rect, + offset = offset](vk::CommandBuffer cmdbuf) { const vk::BufferImageCopy buffer_image_copy = { .bufferOffset = offset, .bufferRowLength = 0, @@ -1005,14 +1002,14 @@ void Surface::UploadCustom(const VideoCore::Material* material, u32 level) { void Surface::Download(const VideoCore::BufferTextureCopy& download, const VideoCore::StagingData& staging) { SCOPE_EXIT({ - scheduler->Finish(); - runtime->download_buffer.Commit(staging.size); + scheduler.Finish(); + runtime.download_buffer.Commit(staging.size); }); - runtime->renderpass_cache.EndRendering(); + runtime.renderpass_cache.EndRendering(); if (pixel_format == PixelFormat::D24S8) { - runtime->blit_helper.DepthToBuffer(*this, runtime->download_buffer.Handle(), download); + runtime.blit_helper.DepthToBuffer(*this, runtime.download_buffer.Handle(), download); return; } @@ -1034,8 +1031,8 @@ void Surface::Download(const VideoCore::BufferTextureCopy& download, .src_image = Image(Type::Base), }; - scheduler->Record( - [buffer = runtime->download_buffer.Handle(), params, download](vk::CommandBuffer cmdbuf) { + scheduler.Record( + [buffer = runtime.download_buffer.Handle(), params, download](vk::CommandBuffer cmdbuf) { const auto rect = download.texture_rect; const vk::BufferImageCopy buffer_image_copy = { .bufferOffset = download.buffer_offset, @@ -1105,13 +1102,13 @@ void Surface::ScaleUp(u32 new_scale) { flags |= vk::ImageCreateFlagBits::eMutableFormat; } - handles[Type::Scaled].Create(instance, GetScaledWidth(), GetScaledHeight(), levels, - texture_type, traits.native, traits.usage, flags, traits.aspect, - false, DebugName(true)); + handles[Type::Scaled].Create(GetScaledWidth(), GetScaledHeight(), levels, texture_type, + traits.native, traits.usage, flags, traits.aspect, false, + DebugName(true)); current = Type::Scaled; - runtime->renderpass_cache.EndRendering(); - scheduler->Record( + runtime.renderpass_cache.EndRendering(); + scheduler.Record( [raw_images = std::array{Image()}, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { std::array barriers; MakeInitBarriers(aspect, 1, raw_images, barriers); @@ -1176,12 +1173,12 @@ vk::ImageView Surface::CopyImageView() noexcept { if (texture_type == VideoCore::TextureType::CubeMap) { flags |= vk::ImageCreateFlagBits::eCubeCompatible; } - copy_handle.Create(instance, GetScaledWidth(), GetScaledHeight(), levels, texture_type, - traits.native, traits.usage, flags, traits.aspect, false); + copy_handle.Create(GetScaledWidth(), GetScaledHeight(), levels, texture_type, traits.native, + traits.usage, flags, traits.aspect, false); copy_layout = vk::ImageLayout::eUndefined; } - runtime->renderpass_cache.EndRendering(); + runtime.renderpass_cache.EndRendering(); const RecordParams params = { .aspect = Aspect(), @@ -1191,8 +1188,8 @@ vk::ImageView Surface::CopyImageView() noexcept { .dst_image = copy_handle.image, }; - scheduler->Record([params, copy_layout, levels = this->levels, width = GetScaledWidth(), - height = GetScaledHeight()](vk::CommandBuffer cmdbuf) { + scheduler.Record([params, copy_layout, levels = this->levels, width = GetScaledWidth(), + height = GetScaledHeight()](vk::CommandBuffer cmdbuf) { std::array pre_barriers = { vk::ImageMemoryBarrier{ .srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, @@ -1304,7 +1301,7 @@ vk::ImageView Surface::ImageView(ViewType view_type, Type type) noexcept { .layerCount = VK_REMAINING_ARRAY_LAYERS, }, }; - handle.image_views[view_type] = instance->GetDevice().createImageView(view_info); + handle.image_views[view_type] = instance.GetDevice().createImageView(view_info); return handle.image_views[view_type]; } @@ -1321,14 +1318,14 @@ vk::Framebuffer Surface::Framebuffer(Type type) noexcept { const auto image_view = ImageView(ViewType::Mip0, type); const vk::FramebufferCreateInfo framebuffer_info = { - .renderPass = runtime->renderpass_cache.GetRenderpass(color_format, depth_format, false), + .renderPass = runtime.renderpass_cache.GetRenderpass(color_format, depth_format, false), .attachmentCount = 1u, .pAttachments = &image_view, .width = handle.width, .height = handle.height, .layers = handle.layers, }; - handle.framebuffer = instance->GetDevice().createFramebuffer(framebuffer_info); + handle.framebuffer = instance.GetDevice().createFramebuffer(framebuffer_info); return handle.framebuffer; } @@ -1342,9 +1339,9 @@ void Surface::BlitScale(const VideoCore::TextureBlit& blit, bool up_scale) { const auto src_type = up_scale ? Type::Base : Type::Scaled; const auto dst_type = up_scale ? Type::Scaled : Type::Base; - scheduler->Record([src_image = Image(src_type), aspect = Aspect(), - filter = MakeFilter(pixel_format), dst_image = Image(dst_type), - blit](vk::CommandBuffer render_cmdbuf) { + scheduler.Record([src_image = Image(src_type), aspect = Aspect(), + filter = MakeFilter(pixel_format), dst_image = Image(dst_type), + blit](vk::CommandBuffer render_cmdbuf) { const std::array source_offsets = { vk::Offset3D{static_cast(blit.src_rect.left), static_cast(blit.src_rect.bottom), 0}, @@ -1439,7 +1436,7 @@ void Surface::BlitScale(const VideoCore::TextureBlit& blit, bool up_scale) { Framebuffer::Framebuffer(TextureRuntime& runtime, const VideoCore::FramebufferParams& params, Surface* color, Surface* depth) - : VideoCore::FramebufferParams{params}, + : VideoCore::FramebufferParams{params}, instance{runtime.GetInstance()}, res_scale{color ? color->res_scale : (depth ? depth->res_scale : 1u)} { auto& renderpass_cache = runtime.GetRenderpassCache(); if (shadow_rendering && !color) { @@ -1490,10 +1487,14 @@ Framebuffer::Framebuffer(TextureRuntime& runtime, const VideoCore::FramebufferPa .height = height, .layers = 1, }; - framebuffer = runtime.GetInstance().GetDevice().createFramebuffer(framebuffer_info); + framebuffer = instance.GetDevice().createFramebuffer(framebuffer_info); } -Framebuffer::~Framebuffer() = default; +Framebuffer::~Framebuffer() { + if (framebuffer) { + instance.GetDevice().destroyFramebuffer(framebuffer); + } +} Sampler::Sampler(TextureRuntime& runtime, const VideoCore::SamplerParams& params) { using TextureConfig = VideoCore::SamplerParams::TextureConfig; diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.h b/src/video_core/renderer_vulkan/vk_texture_runtime.h index bb5b5ce91..2149060f2 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.h +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.h @@ -44,20 +44,24 @@ enum ViewType { }; struct Handle { - explicit Handle() = default; + explicit Handle(const Instance& _instance) : instance(_instance) {} ~Handle() { Destroy(); } + Handle(const Handle& other) = delete; + Handle(Handle&& other) noexcept - : allocation(std::exchange(other.allocation, VK_NULL_HANDLE)), + : instance(other.instance), allocation(std::exchange(other.allocation, VK_NULL_HANDLE)), image(std::exchange(other.image, VK_NULL_HANDLE)), image_views(std::exchange(other.image_views, {})), framebuffer(std::exchange(other.framebuffer, VK_NULL_HANDLE)), width(std::exchange(other.width, 0)), height(std::exchange(other.height, 0)), levels(std::exchange(other.levels, 0)), layers(std::exchange(other.layers, 0)) {} + Handle& operator=(const Handle& other) = delete; + Handle& operator=(Handle&& other) noexcept { if (this == &other) return *this; @@ -74,10 +78,9 @@ struct Handle { return *this; } - void Create(const Instance* instance, u32 width, u32 height, u32 levels, - VideoCore::TextureType type, vk::Format format, vk::ImageUsageFlags usage, - vk::ImageCreateFlags flags, vk::ImageAspectFlags aspect, bool need_format_list, - std::string_view debug_name = {}); + void Create(u32 width, u32 height, u32 levels, VideoCore::TextureType type, vk::Format format, + vk::ImageUsageFlags usage, vk::ImageCreateFlags flags, vk::ImageAspectFlags aspect, + bool need_format_list, std::string_view debug_name = {}); void Destroy(); @@ -85,7 +88,7 @@ struct Handle { return allocation; } - const Instance* instance{nullptr}; + const Instance& instance; VmaAllocation allocation{VK_NULL_HANDLE}; vk::Image image{VK_NULL_HANDLE}; @@ -269,9 +272,9 @@ private: const VideoCore::StagingData& staging); public: - TextureRuntime* runtime; - const Instance* instance; - Scheduler* scheduler; + TextureRuntime& runtime; + const Instance& instance; + Scheduler& scheduler; FormatTraits traits; std::array handles; Type current{}; @@ -291,8 +294,34 @@ public: Framebuffer(const Framebuffer&) = delete; Framebuffer& operator=(const Framebuffer&) = delete; - Framebuffer(Framebuffer&& o) noexcept = default; - Framebuffer& operator=(Framebuffer&& o) noexcept = default; + Framebuffer(Framebuffer&& other) noexcept + : instance(other.instance), images(std::exchange(other.images, {})), + image_views(std::exchange(other.image_views, {})), + framebuffer(std::exchange(other.framebuffer, VK_NULL_HANDLE)), + render_pass(std::exchange(other.render_pass, VK_NULL_HANDLE)), + framebuffer_views(std::move(other.framebuffer_views)), + aspects(std::exchange(other.aspects, {})), + formats(std::exchange( + other.formats, {VideoCore::PixelFormat::Invalid, VideoCore::PixelFormat::Invalid})), + width(std::exchange(other.width, 0)), height(std::exchange(other.height, 0)), + res_scale(std::exchange(other.res_scale, 1)) {} + + Framebuffer& operator=(Framebuffer&& other) noexcept { + + images = std::exchange(other.images, {}); + image_views = std::exchange(other.image_views, {}); + framebuffer = std::exchange(other.framebuffer, VK_NULL_HANDLE); + render_pass = std::exchange(other.render_pass, VK_NULL_HANDLE); + framebuffer_views = std::move(other.framebuffer_views); + aspects = std::exchange(other.aspects, {}); + formats = std::exchange(other.formats, + {VideoCore::PixelFormat::Invalid, VideoCore::PixelFormat::Invalid}); + width = std::exchange(other.width, 0); + height = std::exchange(other.height, 0); + res_scale = std::exchange(other.res_scale, 1); + + return *this; + } VideoCore::PixelFormat Format(VideoCore::SurfaceType type) const noexcept { return formats[Index(type)]; @@ -323,16 +352,17 @@ public: } private: + const Instance& instance; std::array images{}; std::array image_views{}; - vk::Framebuffer framebuffer; - vk::RenderPass render_pass; + vk::Framebuffer framebuffer{}; + vk::RenderPass render_pass{}; std::vector framebuffer_views; std::array aspects{}; std::array formats{VideoCore::PixelFormat::Invalid, VideoCore::PixelFormat::Invalid}; - u32 width; - u32 height; + u32 width{}; + u32 height{}; u32 res_scale{1}; }; From 03d62efe130faca6fb69abea2b8717abe84ac982 Mon Sep 17 00:00:00 2001 From: keynote <119899374+keynote@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:34:52 +0100 Subject: [PATCH 24/42] artic_base_client: Fix high cpu usage (#1789) Fixes high CPU usage by adding a small sleep to the Client::Read and Client::Write methods --- src/network/artic_base/artic_base_client.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/network/artic_base/artic_base_client.cpp b/src/network/artic_base/artic_base_client.cpp index b38416cc4..5b3ac3396 100644 --- a/src/network/artic_base/artic_base_client.cpp +++ b/src/network/artic_base/artic_base_client.cpp @@ -606,6 +606,7 @@ bool Client::Read(SocketHolder sockFD, void* buffer, size_t size, if (GET_ERRNO == ERRNO(EWOULDBLOCK) && (timeout == std::chrono::nanoseconds(0) || std::chrono::steady_clock::now() - before < timeout)) { + std::this_thread::sleep_for(100us); continue; } read_bytes = 0; @@ -630,6 +631,7 @@ bool Client::Write(SocketHolder sockFD, const void* buffer, size_t size, if (GET_ERRNO == ERRNO(EWOULDBLOCK) && (timeout == std::chrono::nanoseconds(0) || std::chrono::steady_clock::now() - before < timeout)) { + std::this_thread::sleep_for(100us); continue; } write_bytes = 0; @@ -863,4 +865,4 @@ void Client::OnAllHandlersFinished() { pending_responses.clear(); } -} // namespace Network::ArticBase \ No newline at end of file +} // namespace Network::ArticBase From 325562093487ec1137f51a1025dc0a731366770b Mon Sep 17 00:00:00 2001 From: David Griswold Date: Thu, 26 Feb 2026 04:33:13 -0800 Subject: [PATCH 25/42] Implement integer scaling (#1400) --- .../features/settings/model/BooleanSetting.kt | 1 + .../settings/ui/SettingsFragmentPresenter.kt | 9 + src/android/app/src/main/jni/config.cpp | 1 + src/android/app/src/main/jni/default_ini.h | 4 + .../app/src/main/res/values/strings.xml | 2 + src/citra_qt/configuration/config.cpp | 2 + .../configuration/configure_enhancements.cpp | 6 + .../configuration/configure_enhancements.h | 3 +- .../configuration/configure_enhancements.ui | 12 +- src/common/settings.cpp | 2 + src/common/settings.h | 1 + src/core/frontend/framebuffer_layout.cpp | 197 +++++++----------- 12 files changed, 121 insertions(+), 119 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt index f06324251..f1ed493f1 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt @@ -34,6 +34,7 @@ enum class BooleanSetting( LLE_APPLETS("lle_applets", Settings.SECTION_SYSTEM, false), NEW_3DS("is_new_3ds", Settings.SECTION_SYSTEM, true), LINEAR_FILTERING("filter_mode", Settings.SECTION_RENDERER, true), + USE_INTEGER_SCALING("use_integer_scaling",Settings.SECTION_RENDERER, false), SHADERS_ACCURATE_MUL("shaders_accurate_mul", Settings.SECTION_RENDERER, false), DISK_SHADER_CACHE("use_disk_shader_cache", Settings.SECTION_RENDERER, true), DUMP_TEXTURES("dump_textures", Settings.SECTION_UTILITY, false), diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index 1326401d5..13c1d2d96 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -898,6 +898,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) IntSetting.RESOLUTION_FACTOR.key, IntSetting.RESOLUTION_FACTOR.defaultValue ) + ) + add( + SwitchSetting( + BooleanSetting.USE_INTEGER_SCALING, + R.string.use_integer_scaling, + R.string.use_integer_scaling_description, + BooleanSetting.USE_INTEGER_SCALING.key, + BooleanSetting.USE_INTEGER_SCALING.defaultValue + ) ) add( SwitchSetting( diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp index 6d310fea6..785f9c21a 100644 --- a/src/android/app/src/main/jni/config.cpp +++ b/src/android/app/src/main/jni/config.cpp @@ -168,6 +168,7 @@ void Config::ReadValues() { Settings::values.pp_shader_name = sdl2_config->GetString("Renderer", "pp_shader_name", default_shader); ReadSetting("Renderer", Settings::values.filter_mode); + ReadSetting("Renderer", Settings::values.use_integer_scaling); ReadSetting("Renderer", Settings::values.bg_red); ReadSetting("Renderer", Settings::values.bg_green); diff --git a/src/android/app/src/main/jni/default_ini.h b/src/android/app/src/main/jni/default_ini.h index 08eaf3283..fc86079ad 100644 --- a/src/android/app/src/main/jni/default_ini.h +++ b/src/android/app/src/main/jni/default_ini.h @@ -148,6 +148,10 @@ use_disk_shader_cache = # factor for the 3DS resolution resolution_factor = +# Use Integer Scaling when the layout allows +# 0: Off (default), 1: On +use_integer_scaling = + # Turns on the frame limiter, which will limit frames output to the target game speed # 0: Off, 1: On (default) use_frame_limit = diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 18959b3a8..fa542ea74 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -250,6 +250,8 @@ Compiles shaders in the background to reduce stuttering during gameplay. When enabled expect temporary graphical glitches Linear Filtering Enables linear filtering, which causes game visuals to appear smoother. + Integer Scaling + Scales the screens with an integer multiplier of the original 3DS screen. For layouts with two different screen sizes, the largest screen is integer-scaled. Texture Filter Enhances the visuals of applications by applying a filter to textures. The supported filters are Anime4K Ultrafast, Bicubic, ScaleForce, xBRZ freescale, and MMPX. Delay Game Render Thread diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index ddb4eebd6..35abe81ef 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -700,6 +700,7 @@ void QtConfig::ReadRendererValues() { ReadGlobalSetting(Settings::values.use_vsync); ReadGlobalSetting(Settings::values.use_display_refresh_rate_detection); ReadGlobalSetting(Settings::values.resolution_factor); + ReadGlobalSetting(Settings::values.use_integer_scaling); ReadGlobalSetting(Settings::values.frame_limit); ReadGlobalSetting(Settings::values.turbo_limit); @@ -1241,6 +1242,7 @@ void QtConfig::SaveRendererValues() { WriteGlobalSetting(Settings::values.use_vsync); WriteGlobalSetting(Settings::values.use_display_refresh_rate_detection); WriteGlobalSetting(Settings::values.resolution_factor); + WriteGlobalSetting(Settings::values.use_integer_scaling); WriteGlobalSetting(Settings::values.frame_limit); WriteGlobalSetting(Settings::values.turbo_limit); diff --git a/src/citra_qt/configuration/configure_enhancements.cpp b/src/citra_qt/configuration/configure_enhancements.cpp index aba7a7ac8..62afcbb77 100644 --- a/src/citra_qt/configuration/configure_enhancements.cpp +++ b/src/citra_qt/configuration/configure_enhancements.cpp @@ -63,6 +63,7 @@ void ConfigureEnhancements::SetConfiguration() { static_cast(Settings::values.mono_render_option.GetValue())); updateShaders(Settings::values.render_3d.GetValue()); ui->toggle_linear_filter->setChecked(Settings::values.filter_mode.GetValue()); + ui->use_integer_scaling->setChecked(Settings::values.use_integer_scaling.GetValue()); ui->toggle_dump_textures->setChecked(Settings::values.dump_textures.GetValue()); ui->toggle_custom_textures->setChecked(Settings::values.custom_textures.GetValue()); ui->toggle_preload_textures->setChecked(Settings::values.preload_textures.GetValue()); @@ -127,6 +128,8 @@ void ConfigureEnhancements::ApplyConfiguration() { ConfigurationShared::ApplyPerGameSetting(&Settings::values.filter_mode, ui->toggle_linear_filter, linear_filter); + ConfigurationShared::ApplyPerGameSetting(&Settings::values.use_integer_scaling, + ui->use_integer_scaling, use_integer_scaling); ConfigurationShared::ApplyPerGameSetting(&Settings::values.texture_filter, ui->texture_filter_combobox); ConfigurationShared::ApplyPerGameSetting(&Settings::values.dump_textures, @@ -148,6 +151,7 @@ void ConfigureEnhancements::SetupPerGameUI() { ui->widget_resolution->setEnabled(Settings::values.resolution_factor.UsingGlobal()); ui->widget_texture_filter->setEnabled(Settings::values.texture_filter.UsingGlobal()); ui->toggle_linear_filter->setEnabled(Settings::values.filter_mode.UsingGlobal()); + ui->use_integer_scaling->setEnabled(Settings::values.use_integer_scaling.UsingGlobal()); ui->toggle_dump_textures->setEnabled(Settings::values.dump_textures.UsingGlobal()); ui->toggle_custom_textures->setEnabled(Settings::values.custom_textures.UsingGlobal()); ui->toggle_preload_textures->setEnabled(Settings::values.preload_textures.UsingGlobal()); @@ -166,6 +170,8 @@ void ConfigureEnhancements::SetupPerGameUI() { ConfigurationShared::SetColoredTristate(ui->toggle_linear_filter, Settings::values.filter_mode, linear_filter); + ConfigurationShared::SetColoredTristate( + ui->use_integer_scaling, Settings::values.use_integer_scaling, use_integer_scaling); ConfigurationShared::SetColoredTristate(ui->toggle_dump_textures, Settings::values.dump_textures, dump_textures); ConfigurationShared::SetColoredTristate(ui->toggle_custom_textures, diff --git a/src/citra_qt/configuration/configure_enhancements.h b/src/citra_qt/configuration/configure_enhancements.h index c9f3449b1..70d6a6673 100644 --- a/src/citra_qt/configuration/configure_enhancements.h +++ b/src/citra_qt/configuration/configure_enhancements.h @@ -1,4 +1,4 @@ -// Copyright 2019 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -39,6 +39,7 @@ private: std::unique_ptr ui; ConfigurationShared::CheckState linear_filter; + ConfigurationShared::CheckState use_integer_scaling; ConfigurationShared::CheckState dump_textures; ConfigurationShared::CheckState custom_textures; ConfigurationShared::CheckState preload_textures; diff --git a/src/citra_qt/configuration/configure_enhancements.ui b/src/citra_qt/configuration/configure_enhancements.ui index ebf8520c4..dc3c3deaf 100644 --- a/src/citra_qt/configuration/configure_enhancements.ui +++ b/src/citra_qt/configuration/configure_enhancements.ui @@ -6,7 +6,7 @@ 0 0 - 440 + 639 950 @@ -110,6 +110,16 @@ + + + + Use Integer Scaling + + + <html><head/><body><p>Use Integer Scaling</p><p>Enforces that the larger screen in all layouts is an integer scale of the 240px height of the original 3DS screen.</p></body></html> + + + diff --git a/src/common/settings.cpp b/src/common/settings.cpp index 564090d0b..d0a08b587 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -97,6 +97,7 @@ void LogSettings() { log_setting("Renderer_ShadersAccurateMul", values.shaders_accurate_mul.GetValue()); log_setting("Renderer_UseShaderJit", values.use_shader_jit.GetValue()); log_setting("Renderer_UseResolutionFactor", values.resolution_factor.GetValue()); + log_setting("Renderer_UseIntegerScaling", values.use_integer_scaling.GetValue()); log_setting("Renderer_FrameLimit", values.frame_limit.GetValue()); log_setting("Renderer_VSyncNew", values.use_vsync.GetValue()); log_setting("Renderer_PostProcessingShader", values.pp_shader_name.GetValue()); @@ -206,6 +207,7 @@ void RestoreGlobalState(bool is_powered_on) { values.shaders_accurate_mul.SetGlobal(true); values.use_vsync.SetGlobal(true); values.resolution_factor.SetGlobal(true); + values.use_integer_scaling.SetGlobal(true); values.frame_limit.SetGlobal(true); values.texture_filter.SetGlobal(true); values.texture_sampling.SetGlobal(true); diff --git a/src/common/settings.h b/src/common/settings.h index 709ba0cf8..178db0019 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -524,6 +524,7 @@ struct Values { true, "use_display_refresh_rate_detection"}; Setting use_shader_jit{true, "use_shader_jit"}; SwitchableSetting resolution_factor{1, 0, 10, "resolution_factor"}; + SwitchableSetting use_integer_scaling{false, "use_integer_scaling"}; SwitchableSetting frame_limit{100, 0, 1000, "frame_limit"}; SwitchableSetting turbo_limit{200, 0, 1000, "turbo_limit"}; SwitchableSetting texture_filter{TextureFilter::NoFilter, "texture_filter"}; diff --git a/src/core/frontend/framebuffer_layout.cpp b/src/core/frontend/framebuffer_layout.cpp index 392db8867..918c1454f 100644 --- a/src/core/frontend/framebuffer_layout.cpp +++ b/src/core/frontend/framebuffer_layout.cpp @@ -25,13 +25,33 @@ u32 FramebufferLayout::GetScalingRatio() const { } // Finds the largest size subrectangle contained in window area that is confined to the aspect ratio +// aligned to the upper-left corner of the bounding rectangle template static Common::Rectangle MaxRectangle(Common::Rectangle window_area, float window_aspect_ratio) { float scale = std::min(static_cast(window_area.GetWidth()), window_area.GetHeight() / window_aspect_ratio); - return Common::Rectangle{0, 0, static_cast(std::round(scale)), - static_cast(std::round(scale * window_aspect_ratio))}; + return Common::Rectangle{ + window_area.left, window_area.top, window_area.left + static_cast(std::round(scale)), + window_area.top + static_cast(std::round(scale * window_aspect_ratio))}; +} + +// overload of the above that takes an inner rectangle instead of an aspect ratio, and can be +// limited to integer scaling if desired +template +static Common::Rectangle MaxRectangle(Common::Rectangle bounding_window, + Common::Rectangle inner_window, + bool use_integer = false) { + float scale = + std::min(static_cast(bounding_window.GetWidth()) / inner_window.GetWidth(), + static_cast(bounding_window.GetHeight()) / inner_window.GetHeight()); + if (use_integer && scale >= 1.0) { + scale = std::floor(scale); + } + return Common::Rectangle(bounding_window.left, bounding_window.top, + bounding_window.left + static_cast(inner_window.GetWidth() * scale), + bounding_window.top + + static_cast(inner_window.GetHeight() * scale)); } FramebufferLayout DefaultFrameLayout(u32 width, u32 height, bool swapped, bool upright) { @@ -74,41 +94,31 @@ FramebufferLayout SingleFrameLayout(u32 width, u32 height, bool swapped, bool up FramebufferLayout res{width, height, !swapped, swapped, {}, {}, !upright}; Common::Rectangle screen_window_area{0, 0, width, height}; - Common::Rectangle top_screen; - Common::Rectangle bot_screen; + Common::Rectangle top_screen{0, 0, Core::kScreenTopWidth, Core::kScreenTopHeight}; + Common::Rectangle bot_screen{0, 0, Core::kScreenBottomWidth, Core::kScreenBottomHeight}; - // TODO: This is kind of gross, make it platform agnostic. -OS -#ifdef ANDROID - const float window_aspect_ratio = static_cast(height) / width; + const float window_aspect_ratio = static_cast(height) / static_cast(width); const auto aspect_ratio_setting = Settings::values.aspect_ratio.GetValue(); - float emulation_aspect_ratio = (swapped) ? BOT_SCREEN_ASPECT_RATIO : TOP_SCREEN_ASPECT_RATIO; switch (aspect_ratio_setting) { case Settings::AspectRatio::Default: + // this is the only one where we allow integer scaling to apply + // also the only option on desktop + top_screen = MaxRectangle(screen_window_area, top_screen, + Settings::values.use_integer_scaling.GetValue()); + bot_screen = MaxRectangle(screen_window_area, bot_screen, + Settings::values.use_integer_scaling.GetValue()); break; case Settings::AspectRatio::Stretch: - emulation_aspect_ratio = window_aspect_ratio; + top_screen = MaxRectangle(screen_window_area, window_aspect_ratio); + bot_screen = MaxRectangle(screen_window_area, window_aspect_ratio); break; default: - emulation_aspect_ratio = res.GetAspectRatioValue(aspect_ratio_setting); + float emulation_aspect_ratio = FramebufferLayout::GetAspectRatioValue(aspect_ratio_setting); + top_screen = MaxRectangle(screen_window_area, emulation_aspect_ratio); + bot_screen = MaxRectangle(screen_window_area, emulation_aspect_ratio); } - top_screen = MaxRectangle(screen_window_area, emulation_aspect_ratio); - bot_screen = MaxRectangle(screen_window_area, emulation_aspect_ratio); - - if (window_aspect_ratio < emulation_aspect_ratio) { - top_screen = - top_screen.TranslateX((screen_window_area.GetWidth() - top_screen.GetWidth()) / 2); - bot_screen = - bot_screen.TranslateX((screen_window_area.GetWidth() - bot_screen.GetWidth()) / 2); - } else { - top_screen = top_screen.TranslateY((height - top_screen.GetHeight()) / 2); - bot_screen = bot_screen.TranslateY((height - bot_screen.GetHeight()) / 2); - } -#else - top_screen = MaxRectangle(screen_window_area, TOP_SCREEN_ASPECT_RATIO); - bot_screen = MaxRectangle(screen_window_area, BOT_SCREEN_ASPECT_RATIO); - const bool stretched = (Settings::values.screen_top_stretch.GetValue() && !swapped) || (Settings::values.screen_bottom_stretch.GetValue() && swapped); if (stretched) { @@ -126,7 +136,6 @@ FramebufferLayout SingleFrameLayout(u32 width, u32 height, bool swapped, bool up bot_screen = bot_screen.TranslateX((width - bot_screen.GetWidth()) / 2) .TranslateY((height - bot_screen.GetHeight()) / 2); } -#endif res.top_screen = top_screen; res.bottom_screen = bot_screen; @@ -150,19 +159,17 @@ FramebufferLayout LargeFrameLayout(u32 width, u32 height, bool swapped, bool upr FramebufferLayout res{width, height, true, true, {}, {}, !upright}; // Split the window into two parts. Give proportional width to the smaller screen // To do that, find the total emulation box and maximize that based on window size - u32 gap = (u32)(Settings::values.screen_gap.GetValue() * scale_factor); + u32 gap = (u32)(Settings::values.screen_gap.GetValue()); - float large_height = - swapped ? Core::kScreenBottomHeight * scale_factor : Core::kScreenTopHeight * scale_factor; - float small_height = - static_cast(swapped ? Core::kScreenTopHeight : Core::kScreenBottomHeight); - float large_width = - swapped ? Core::kScreenBottomWidth * scale_factor : Core::kScreenTopWidth * scale_factor; - float small_width = - static_cast(swapped ? Core::kScreenTopWidth : Core::kScreenBottomWidth); + u32 large_height = swapped ? Core::kScreenBottomHeight : Core::kScreenTopHeight; + u32 small_height = static_cast(swapped ? Core::kScreenTopHeight / scale_factor + : Core::kScreenBottomHeight / scale_factor); + u32 large_width = swapped ? Core::kScreenBottomWidth : Core::kScreenTopWidth; + u32 small_width = static_cast(swapped ? Core::kScreenTopWidth / scale_factor + : Core::kScreenBottomWidth / scale_factor); - float emulation_width; - float emulation_height; + u32 emulation_width; + u32 emulation_height; if (vertical) { // width is just the larger size at this point emulation_width = std::max(large_width, small_width); @@ -172,12 +179,13 @@ FramebufferLayout LargeFrameLayout(u32 width, u32 height, bool swapped, bool upr emulation_height = std::max(large_height, small_height); } - const float window_aspect_ratio = static_cast(height) / static_cast(width); - const float emulation_aspect_ratio = emulation_height / emulation_width; - Common::Rectangle screen_window_area{0, 0, width, height}; - Common::Rectangle total_rect = MaxRectangle(screen_window_area, emulation_aspect_ratio); - // TODO: Wtf does this `scale_amount` value represent? -OS + Common::Rectangle total_rect{0, 0, emulation_width, emulation_height}; + total_rect = MaxRectangle(screen_window_area, total_rect, + Settings::values.use_integer_scaling.GetValue()); + total_rect = total_rect.TranslateX((width - total_rect.GetWidth()) / 2) + .TranslateY((height - total_rect.GetHeight()) / 2); + const float scale_amount = static_cast(total_rect.GetHeight()) / emulation_height; gap = static_cast(static_cast(gap) * scale_amount); @@ -190,61 +198,50 @@ FramebufferLayout LargeFrameLayout(u32 width, u32 height, bool swapped, bool upr static_cast(small_width * scale_amount + total_rect.left), static_cast(small_height * scale_amount + total_rect.top)}; - if (window_aspect_ratio < emulation_aspect_ratio) { - // shift the large screen so it is at the left position of the bounding rectangle - large_screen = large_screen.TranslateX((width - total_rect.GetWidth()) / 2); - } else { - // shift the large screen so it is at the top position of the bounding rectangle - large_screen = large_screen.TranslateY((height - total_rect.GetHeight()) / 2); - } - switch (small_screen_position) { case Settings::SmallScreenPosition::TopRight: // Shift the small screen to the top right corner - small_screen = small_screen.TranslateX(large_screen.right + gap); - small_screen = small_screen.TranslateY(large_screen.top); + small_screen = small_screen.TranslateX(large_screen.GetWidth() + gap); + small_screen = small_screen.TranslateY(large_screen.top - small_screen.top); break; case Settings::SmallScreenPosition::MiddleRight: // Shift the small screen to the center right - small_screen = small_screen.TranslateX(large_screen.right + gap); - small_screen = small_screen.TranslateY( - ((large_screen.GetHeight() - small_screen.GetHeight()) / 2) + large_screen.top); + small_screen = small_screen.TranslateX(large_screen.GetWidth() + gap); + small_screen = + small_screen.TranslateY(((large_screen.GetHeight() - small_screen.GetHeight()) / 2) + + large_screen.top - small_screen.top); break; case Settings::SmallScreenPosition::BottomRight: // Shift the small screen to the bottom right corner - small_screen = small_screen.TranslateX(large_screen.right + gap); - small_screen = small_screen.TranslateY(large_screen.bottom - small_screen.GetHeight()); + small_screen = small_screen.TranslateX(large_screen.GetWidth() + gap); + small_screen = small_screen.TranslateY(large_screen.bottom - small_screen.bottom); break; case Settings::SmallScreenPosition::TopLeft: - // shift the small screen to the upper left then shift the large screen to its right - small_screen = small_screen.TranslateX(large_screen.left); + // shift the large screen to the upper right of the small screen large_screen = large_screen.TranslateX(small_screen.GetWidth() + gap); - small_screen = small_screen.TranslateY(large_screen.top); break; case Settings::SmallScreenPosition::MiddleLeft: // shift the small screen to the middle left and shift the large screen to its right - small_screen = small_screen.TranslateX(large_screen.left); large_screen = large_screen.TranslateX(small_screen.GetWidth() + gap); - small_screen = small_screen.TranslateY( - ((large_screen.GetHeight() - small_screen.GetHeight()) / 2) + large_screen.top); + small_screen = + small_screen.TranslateY(((large_screen.GetHeight() - small_screen.GetHeight()) / 2)); break; case Settings::SmallScreenPosition::BottomLeft: // shift the small screen to the bottom left and shift the large screen to its right - small_screen = small_screen.TranslateX(large_screen.left); large_screen = large_screen.TranslateX(small_screen.GetWidth() + gap); - small_screen = small_screen.TranslateY(large_screen.bottom - small_screen.GetHeight()); + small_screen = small_screen.TranslateY(large_screen.bottom - small_screen.bottom); break; case Settings::SmallScreenPosition::AboveLarge: - // shift the large screen down and the bottom screen above it - small_screen = small_screen.TranslateY(large_screen.top); + // shift the large screen down large_screen = large_screen.TranslateY(small_screen.GetHeight() + gap); // If the "large screen" is actually smaller, center it if (large_screen.GetWidth() < total_rect.GetWidth()) { large_screen = large_screen.TranslateX((total_rect.GetWidth() - large_screen.GetWidth()) / 2); } - small_screen = small_screen.TranslateX(large_screen.left + large_screen.GetWidth() / 2 - - small_screen.GetWidth() / 2); + small_screen = + small_screen.TranslateX((large_screen.left - total_rect.left) + + large_screen.GetWidth() / 2 - small_screen.GetWidth() / 2); break; case Settings::SmallScreenPosition::BelowLarge: // shift the bottom_screen down and then over to the center @@ -253,9 +250,10 @@ FramebufferLayout LargeFrameLayout(u32 width, u32 height, bool swapped, bool upr large_screen = large_screen.TranslateX((total_rect.GetWidth() - large_screen.GetWidth()) / 2); } - small_screen = small_screen.TranslateY(large_screen.bottom + gap); - small_screen = small_screen.TranslateX(large_screen.left + large_screen.GetWidth() / 2 - - small_screen.GetWidth() / 2); + small_screen = small_screen.TranslateY(large_screen.GetHeight() + gap); + small_screen = + small_screen.TranslateX((large_screen.left - total_rect.left) + + large_screen.GetWidth() / 2 - small_screen.GetWidth() / 2); break; default: UNREACHABLE(); @@ -276,54 +274,19 @@ FramebufferLayout HybridScreenLayout(u32 width, u32 height, bool swapped, bool u if (upright) { std::swap(width, height); } - FramebufferLayout res{width, height, true, true, {}, {}, !upright, false, true, {}}; // Split the window into two parts. Give 2.25x width to the main screen, // and make a bar on the right side with 1x width top screen and 1.25x width bottom screen // To do that, find the total emulation box and maximize that based on window size - const float window_aspect_ratio = static_cast(height) / static_cast(width); - const float scale_factor = 2.25f; - - float main_screen_aspect_ratio = TOP_SCREEN_ASPECT_RATIO; - float hybrid_area_aspect_ratio = 27.f / 65; - float top_screen_aspect_ratio = TOP_SCREEN_ASPECT_RATIO; - float bot_screen_aspect_ratio = BOT_SCREEN_ASPECT_RATIO; - - if (swapped) { - main_screen_aspect_ratio = BOT_SCREEN_ASPECT_RATIO; - hybrid_area_aspect_ratio = - Core::kScreenBottomHeight * scale_factor / - (Core::kScreenBottomWidth * scale_factor + Core::kScreenTopWidth); - } - - Common::Rectangle screen_window_area{0, 0, width, height}; - Common::Rectangle total_rect = MaxRectangle(screen_window_area, hybrid_area_aspect_ratio); - Common::Rectangle large_main_screen = MaxRectangle(total_rect, main_screen_aspect_ratio); - Common::Rectangle side_rect = total_rect.Scale(1.f / scale_factor); - Common::Rectangle small_top_screen = MaxRectangle(side_rect, top_screen_aspect_ratio); - Common::Rectangle small_bottom_screen = MaxRectangle(side_rect, bot_screen_aspect_ratio); - - if (window_aspect_ratio < hybrid_area_aspect_ratio) { - large_main_screen = large_main_screen.TranslateX((width - total_rect.GetWidth()) / 2); - } else { - large_main_screen = large_main_screen.TranslateY((height - total_rect.GetHeight()) / 2); - } - - // Scale the bottom screen so it's width is the same as top screen - small_bottom_screen = small_bottom_screen.Scale(1.25f); - - // Shift the small bottom screen to the bottom right corner - small_bottom_screen = small_bottom_screen.TranslateX(large_main_screen.right) - .TranslateY(large_main_screen.GetHeight() + large_main_screen.top - - small_bottom_screen.GetHeight()); - - // Shift small top screen to upper right corner - small_top_screen = - small_top_screen.TranslateX(large_main_screen.right).TranslateY(large_main_screen.top); - - res.top_screen = small_top_screen; - res.additional_screen = swapped ? small_bottom_screen : large_main_screen; - res.bottom_screen = swapped ? large_main_screen : small_bottom_screen; + const float scale_factor = swapped ? 2.25 : 1.8; + const Settings::SmallScreenPosition pos = swapped ? Settings::SmallScreenPosition::TopRight + : Settings::SmallScreenPosition::BottomRight; + FramebufferLayout res = LargeFrameLayout(width, height, swapped, upright, scale_factor, pos); + const Common::Rectangle main = swapped ? res.bottom_screen : res.top_screen; + const Common::Rectangle small = swapped ? res.top_screen : res.bottom_screen; + res.additional_screen = Common::Rectangle{small.left, swapped ? small.bottom : main.top, + small.right, swapped ? main.bottom : small.top}; + res.additional_screen_enabled = true; if (upright) { return reverseLayout(res); } else { From 5ac0ef8fdee0fd6832787f22f473014c227d57ad Mon Sep 17 00:00:00 2001 From: David Griswold Date: Thu, 26 Feb 2026 05:27:18 -0800 Subject: [PATCH 26/42] hide portrait layout menu on landscape and vice versa (#1473) --- .../org/citra/citra_emu/fragments/EmulationFragment.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt index 30b22cc64..fba1a167d 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -11,6 +11,7 @@ import android.content.DialogInterface import android.content.Intent import android.content.IntentFilter import android.content.SharedPreferences +import android.content.res.Configuration import android.net.Uri import android.os.BatteryManager import android.os.Build @@ -175,6 +176,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram savedInstanceState: Bundle? ): View { _binding = FragmentEmulationBinding.inflate(inflater) + binding.inGameMenu.menu.findItem(R.id.menu_landscape_screen_layout).isVisible = + CitraApplication.appContext.resources.configuration.orientation != + Configuration.ORIENTATION_PORTRAIT + binding.inGameMenu.menu.findItem(R.id.menu_portrait_screen_layout).isVisible = + CitraApplication.appContext.resources.configuration.orientation == + Configuration.ORIENTATION_PORTRAIT return binding.root } From 6b2ac400ebd66eb41df9ed719d662ebe7567de09 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Thu, 26 Feb 2026 09:40:42 -0800 Subject: [PATCH 27/42] Android: Hotkey Enable Button (#1464) --- .../citra_emu/activities/EmulationActivity.kt | 26 ++- .../citra_emu/features/hotkeys/Hotkey.kt | 3 +- .../features/hotkeys/HotkeyUtility.kt | 150 ++++++++++++++---- .../features/settings/model/Settings.kt | 3 + .../model/view/InputBindingSetting.kt | 55 +++++-- .../settings/ui/SettingsFragmentPresenter.kt | 2 +- .../app/src/main/res/values/strings.xml | 2 + 7 files changed, 177 insertions(+), 64 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt index ca92b308d..1e372720e 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt @@ -267,36 +267,28 @@ class EmulationActivity : AppCompatActivity() { return super.dispatchKeyEvent(event) } - val button = - preferences.getInt(InputBindingSetting.getInputButtonKey(event.keyCode), event.keyCode) - val action: Int = when (event.action) { + when (event.action) { KeyEvent.ACTION_DOWN -> { - hotkeyUtility.handleHotkey(button) - // On some devices, the back gesture / button press is not intercepted by androidx // and fails to open the emulation menu. So we're stuck running deprecated code to // cover for either a fault on androidx's side or in OEM skins (MIUI at least) + if (event.keyCode == KeyEvent.KEYCODE_BACK) { // If the hotkey is pressed, we don't want to open the drawer - if (!hotkeyUtility.HotkeyIsPressed) { + if (!hotkeyUtility.hotkeyIsPressed) { onBackPressed() + return true } } - - // Normal key events. - NativeLibrary.ButtonState.PRESSED + return hotkeyUtility.handleKeyPress(event) } - KeyEvent.ACTION_UP -> { - hotkeyUtility.HotkeyIsPressed = false - NativeLibrary.ButtonState.RELEASED + return hotkeyUtility.handleKeyRelease(event) + } + else -> { + return false; } - else -> return false } - val input = event.device - ?: // Controller was disconnected - return false - return NativeLibrary.onGamePadEvent(input.descriptor, button, action) } private fun onAmiiboSelected(selectedFile: String) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt index 4b22164bb..e2319a7e4 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt @@ -11,5 +11,6 @@ enum class Hotkey(val button: Int) { PAUSE_OR_RESUME(10004), QUICKSAVE(10005), QUICKLOAD(10006), - TURBO_LIMIT(10007); + TURBO_LIMIT(10007), + ENABLE(10008); } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt index 0a4a1ffa3..d01d5f769 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt @@ -5,50 +5,140 @@ package org.citra.citra_emu.features.hotkeys import android.content.Context +import android.view.KeyEvent import android.widget.Toast +import androidx.preference.PreferenceManager +import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.NativeLibrary import org.citra.citra_emu.R import org.citra.citra_emu.utils.EmulationLifecycleUtil import org.citra.citra_emu.utils.TurboHelper import org.citra.citra_emu.display.ScreenAdjustmentUtil +import org.citra.citra_emu.features.settings.model.view.InputBindingSetting +import org.citra.citra_emu.features.settings.model.Settings class HotkeyUtility( private val screenAdjustmentUtil: ScreenAdjustmentUtil, - private val context: Context) { + private val context: Context +) { private val hotkeyButtons = Hotkey.entries.map { it.button } - var HotkeyIsPressed = false + private var hotkeyIsEnabled = false + var hotkeyIsPressed = false + private val currentlyPressedButtons = mutableSetOf() + + fun handleKeyPress(keyEvent: KeyEvent): Boolean { + var handled = false + val buttonSet = InputBindingSetting.getButtonSet(keyEvent) + val enableButton = + PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) + .getString(Settings.HOTKEY_ENABLE, "") + val thisKeyIsEnableButton = buttonSet.contains(Hotkey.ENABLE.button) + val thisKeyIsHotkey = + !thisKeyIsEnableButton && Hotkey.entries.any { buttonSet.contains(it.button) } + hotkeyIsEnabled = hotkeyIsEnabled || enableButton == "" || thisKeyIsEnableButton + + // Now process all internal buttons associated with this keypress + for (button in buttonSet) { + currentlyPressedButtons.add(button) + //option 1 - this is the enable command, which was already handled + if (button == Hotkey.ENABLE.button) { + handled = true + } + // option 2 - this is a different hotkey command + else if (hotkeyButtons.contains(button)) { + if (hotkeyIsEnabled) { + handled = handleHotkey(button) || handled + } + } + // option 3 - this is a normal key + else { + // if this key press is ALSO associated with a hotkey that will process, skip + // the normal key event. + if (!thisKeyIsHotkey || !hotkeyIsEnabled) { + handled = NativeLibrary.onGamePadEvent( + keyEvent.device.descriptor, + button, + NativeLibrary.ButtonState.PRESSED + ) || handled + } + } + } + return handled + } + + fun handleKeyRelease(keyEvent: KeyEvent): Boolean { + var handled = false + val buttonSet = InputBindingSetting.getButtonSet(keyEvent) + val thisKeyIsEnableButton = buttonSet.contains(Hotkey.ENABLE.button) + val thisKeyIsHotkey = + !thisKeyIsEnableButton && Hotkey.entries.any { buttonSet.contains(it.button) } + if (thisKeyIsEnableButton) { + handled = true; hotkeyIsEnabled = false + } + + for (button in buttonSet) { + // this is a hotkey button + if (hotkeyButtons.contains(button)) { + currentlyPressedButtons.remove(button) + if (!currentlyPressedButtons.any { hotkeyButtons.contains(it) }) { + // all hotkeys are no longer pressed + hotkeyIsPressed = false + } + } else { + // if this key ALSO sends a hotkey command that we already/will handle, + // or if we did not register the press of this button, e.g. if this key + // was also a hotkey pressed after enable, but released after enable button release, then + // skip the normal key event + if ((!thisKeyIsHotkey || !hotkeyIsEnabled) && currentlyPressedButtons.contains( + button + ) + ) { + handled = NativeLibrary.onGamePadEvent( + keyEvent.device.descriptor, + button, + NativeLibrary.ButtonState.RELEASED + ) || handled + currentlyPressedButtons.remove(button) + } + } + } + return handled + } fun handleHotkey(bindedButton: Int): Boolean { - if(hotkeyButtons.contains(bindedButton)) { - when (bindedButton) { - Hotkey.SWAP_SCREEN.button -> screenAdjustmentUtil.swapScreen() - Hotkey.CYCLE_LAYOUT.button -> screenAdjustmentUtil.cycleLayouts() - Hotkey.CLOSE_GAME.button -> EmulationLifecycleUtil.closeGame() - Hotkey.PAUSE_OR_RESUME.button -> EmulationLifecycleUtil.pauseOrResume() - Hotkey.TURBO_LIMIT.button -> TurboHelper.toggleTurbo(true) - Hotkey.QUICKSAVE.button -> { - NativeLibrary.saveState(NativeLibrary.QUICKSAVE_SLOT) - Toast.makeText(context, - context.getString(R.string.saving), - Toast.LENGTH_SHORT).show() - } - Hotkey.QUICKLOAD.button -> { - val wasLoaded = NativeLibrary.loadStateIfAvailable(NativeLibrary.QUICKSAVE_SLOT) - val stringRes = if(wasLoaded) { - R.string.loading - } else { - R.string.quickload_not_found - } - Toast.makeText(context, - context.getString(stringRes), - Toast.LENGTH_SHORT).show() - } - else -> {} + when (bindedButton) { + Hotkey.SWAP_SCREEN.button -> screenAdjustmentUtil.swapScreen() + Hotkey.CYCLE_LAYOUT.button -> screenAdjustmentUtil.cycleLayouts() + Hotkey.CLOSE_GAME.button -> EmulationLifecycleUtil.closeGame() + Hotkey.PAUSE_OR_RESUME.button -> EmulationLifecycleUtil.pauseOrResume() + Hotkey.TURBO_LIMIT.button -> TurboHelper.toggleTurbo(true) + Hotkey.QUICKSAVE.button -> { + NativeLibrary.saveState(NativeLibrary.QUICKSAVE_SLOT) + Toast.makeText( + context, + context.getString(R.string.saving), + Toast.LENGTH_SHORT + ).show() } - HotkeyIsPressed = true - return true + + Hotkey.QUICKLOAD.button -> { + val wasLoaded = NativeLibrary.loadStateIfAvailable(NativeLibrary.QUICKSAVE_SLOT) + val stringRes = if (wasLoaded) { + R.string.loading + } else { + R.string.quickload_not_found + } + Toast.makeText( + context, + context.getString(stringRes), + Toast.LENGTH_SHORT + ).show() + } + + else -> {} } - return false + hotkeyIsPressed = true + return true } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt index 02d10cfe9..96100349a 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt @@ -135,6 +135,7 @@ class Settings { const val KEY_CSTICK_AXIS_HORIZONTAL = "cstick_axis_horizontal" const val KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical" const val KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal" + const val HOTKEY_ENABLE = "hotkey_enable" const val HOTKEY_SCREEN_SWAP = "hotkey_screen_swap" const val HOTKEY_CYCLE_LAYOUT = "hotkey_toggle_layout" const val HOTKEY_CLOSE_GAME = "hotkey_close_game" @@ -202,6 +203,7 @@ class Settings { R.string.button_zr ) val hotKeys = listOf( + HOTKEY_ENABLE, HOTKEY_SCREEN_SWAP, HOTKEY_CYCLE_LAYOUT, HOTKEY_CLOSE_GAME, @@ -211,6 +213,7 @@ class Settings { HOTKEY_TURBO_LIMIT ) val hotkeyTitles = listOf( + R.string.controller_hotkey_enable_button, R.string.emulation_swap_screens, R.string.emulation_cycle_landscape_layouts, R.string.emulation_close_game, diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt index 482bc0b08..d78f5c3a3 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt @@ -128,6 +128,7 @@ class InputBindingSetting( Settings.KEY_BUTTON_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN Settings.KEY_BUTTON_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT Settings.KEY_BUTTON_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT + Settings.HOTKEY_ENABLE -> Hotkey.ENABLE.button Settings.HOTKEY_SCREEN_SWAP -> Hotkey.SWAP_SCREEN.button Settings.HOTKEY_CYCLE_LAYOUT -> Hotkey.CYCLE_LAYOUT.button Settings.HOTKEY_CLOSE_GAME -> Hotkey.CLOSE_GAME.button @@ -162,36 +163,40 @@ class InputBindingSetting( fun removeOldMapping() { // Try remove all possible keys we wrote for this setting val oldKey = preferences.getString(reverseKey, "") - (setting as AbstractStringSetting).string = "" if (oldKey != "") { + (setting as AbstractStringSetting).string = "" preferences.edit() .remove(abstractSetting.key) // Used for ui text - .remove(oldKey) // Used for button mapping .remove(oldKey + "_GuestOrientation") // Used for axis orientation .remove(oldKey + "_GuestButton") // Used for axis button .remove(oldKey + "_Inverted") // used for axis inversion - .apply() + .remove(reverseKey) + val buttonCodes = try { + preferences.getStringSet(oldKey, mutableSetOf())!!.toMutableSet() + } catch (e: ClassCastException) { + // if this is an int pref, either old button or an axis, so just remove it + preferences.edit().remove(oldKey).apply() + return; + } + buttonCodes.remove(buttonCode.toString()); + preferences.edit().putStringSet(oldKey,buttonCodes).apply() } } /** * Helper function to write a gamepad button mapping for the setting. */ - private fun writeButtonMapping(key: String) { + private fun writeButtonMapping(keyEvent: KeyEvent) { val editor = preferences.edit() - - // Remove mapping for another setting using this input - val oldButtonCode = preferences.getInt(key, -1) - if (oldButtonCode != -1) { - val oldKey = getButtonKey(oldButtonCode) - editor.remove(oldKey) // Only need to remove UI text setting, others will be overwritten - } - + val key = getInputButtonKey(keyEvent) + // Pull in all codes associated with this key + // Migrate from the old int preference if need be + val buttonCodes = InputBindingSetting.getButtonSet(keyEvent) + buttonCodes.add(buttonCode) // Cleanup old mapping for this setting removeOldMapping() - // Write new mapping - editor.putInt(key, buttonCode) + editor.putStringSet(key, buttonCodes.mapTo(mutableSetOf()) {it.toString()}) // Write next reverse mapping for future cleanup editor.putString(reverseKey, key) @@ -229,7 +234,7 @@ class InputBindingSetting( } val code = translateEventToKeyId(keyEvent) - writeButtonMapping(getInputButtonKey(code)) + writeButtonMapping(keyEvent) val uiString = "${keyEvent.device.name}: Button $code" value = uiString } @@ -289,6 +294,26 @@ class InputBindingSetting( NativeLibrary.ButtonType.DPAD_RIGHT -> Settings.KEY_BUTTON_RIGHT else -> "" } + /** + * Get the mutable set of int button values this key should map to given an event + */ + fun getButtonSet(keyCode: KeyEvent):MutableSet { + val key = getInputButtonKey(keyCode) + val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) + var buttonCodes = try { + preferences.getStringSet(key, mutableSetOf()) + } catch (e: ClassCastException) { + val prefInt = preferences.getInt(key, -1); + val migratedSet = if (prefInt != -1) { + mutableSetOf(prefInt.toString()) + } else { + mutableSetOf() + } + migratedSet + } + if (buttonCodes == null) buttonCodes = mutableSetOf() + return buttonCodes.mapNotNull { it.toIntOrNull() }.toMutableSet() + } /** * Helper function to get the settings key for an gamepad button. diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index 13c1d2d96..9a40ba90b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -811,7 +811,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add(InputBindingSetting(button, Settings.triggerTitles[i])) } - add(HeaderSetting(R.string.controller_hotkeys)) + add(HeaderSetting(R.string.controller_hotkeys,R.string.controller_hotkeys_description)) Settings.hotKeys.forEachIndexed { i: Int, key: String -> val button = getInputObject(key) add(InputBindingSetting(button, Settings.hotkeyTitles[i])) diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index fa542ea74..de2a78ee9 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -122,6 +122,8 @@ Circle Pad C-Stick Hotkeys + If the "Hotkey Enable" key is mapped, that key must be pressed in addition to the mapped hotkey + Hotkey Enable Triggers Trigger D-Pad From b477ba09c1aa72c5e9004307d7c47da72cb144b8 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Fri, 27 Feb 2026 04:21:53 -0800 Subject: [PATCH 28/42] Ability to select which layouts to cycle with the cycle layout hotkey (#1430) --- .../citra_emu/display/ScreenAdjustmentUtil.kt | 11 +- .../settings/model/AbstractListSetting.kt | 9 + .../features/settings/model/IntListSetting.kt | 52 +++++ .../settings/model/view/MultiChoiceSetting.kt | 46 ++++ .../settings/model/view/SettingsItem.kt | 1 + .../features/settings/ui/SettingsAdapter.kt | 94 +++++++- .../settings/ui/SettingsFragmentPresenter.kt | 14 ++ .../ui/viewholder/MultiChoiceViewHolder.kt | 80 +++++++ .../features/settings/utils/SettingsFile.kt | 8 +- .../app/src/main/res/values/strings.xml | 2 + src/citra_qt/CMakeLists.txt | 3 + src/citra_qt/citra_qt.cpp | 35 ++- src/citra_qt/configuration/config.cpp | 126 +++++----- src/citra_qt/configuration/config.h | 7 + .../configuration/configure_layout.cpp | 8 + .../configuration/configure_layout.ui | 88 ++++--- .../configuration/configure_layout_cycle.cpp | 92 ++++++++ .../configuration/configure_layout_cycle.h | 32 +++ .../configuration/configure_layout_cycle.ui | 216 ++++++++++++++++++ src/common/settings.cpp | 2 + src/common/settings.h | 8 + 21 files changed, 815 insertions(+), 119 deletions(-) create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractListSetting.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntListSetting.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/MultiChoiceSetting.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt create mode 100644 src/citra_qt/configuration/configure_layout_cycle.cpp create mode 100644 src/citra_qt/configuration/configure_layout_cycle.h create mode 100644 src/citra_qt/configuration/configure_layout_cycle.ui diff --git a/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt index 105f49ab8..e63960fa8 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt @@ -12,6 +12,7 @@ import org.citra.citra_emu.NativeLibrary import org.citra.citra_emu.R import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.IntSetting +import org.citra.citra_emu.features.settings.model.IntListSetting import org.citra.citra_emu.features.settings.model.Settings import org.citra.citra_emu.features.settings.utils.SettingsFile import org.citra.citra_emu.utils.EmulationMenuSettings @@ -31,8 +32,16 @@ class ScreenAdjustmentUtil( BooleanSetting.SWAP_SCREEN.boolean = isEnabled settings.saveSetting(BooleanSetting.SWAP_SCREEN, SettingsFile.FILE_NAME_CONFIG) } + fun cycleLayouts() { - val landscapeValues = context.resources.getIntArray(R.array.landscapeValues) + + val landscapeLayoutsToCycle = IntListSetting.LAYOUTS_TO_CYCLE.list; + val landscapeValues = + if (landscapeLayoutsToCycle.isNotEmpty()) + landscapeLayoutsToCycle.toIntArray() + else context.resources.getIntArray( + R.array.landscapeValues + ) val portraitValues = context.resources.getIntArray(R.array.portraitValues) if (NativeLibrary.isPortraitMode) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractListSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractListSetting.kt new file mode 100644 index 000000000..d89db48af --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractListSetting.kt @@ -0,0 +1,9 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model + +interface AbstractListSetting : AbstractSetting { + var list: List +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntListSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntListSetting.kt new file mode 100644 index 000000000..0de51acce --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntListSetting.kt @@ -0,0 +1,52 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model + +enum class IntListSetting( + override val key: String, + override val section: String, + override val defaultValue: List, + val canBeEmpty: Boolean = true +) : AbstractListSetting { + + LAYOUTS_TO_CYCLE("layouts_to_cycle", Settings.SECTION_LAYOUT, listOf(0, 1, 2, 3, 4, 5), canBeEmpty = false); + + private var backingList: List = defaultValue + private var lastValidList : List = defaultValue + + override var list: List + get() = backingList + set(value) { + if (!canBeEmpty && value.isEmpty()) { + backingList = lastValidList + } else { + backingList = value + lastValidList = value + } + } + + override val valueAsString: String + get() = list.joinToString() + + + override val isRuntimeEditable: Boolean + get() { + for (setting in NOT_RUNTIME_EDITABLE) { + if (setting == this) { + return false + } + } + return true + } + + companion object { + private val NOT_RUNTIME_EDITABLE: List = emptyList() + + fun from(key: String): IntListSetting? = + values().firstOrNull { it.key == key } + + fun clear() = values().forEach { it.list = it.defaultValue } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/MultiChoiceSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/MultiChoiceSetting.kt new file mode 100644 index 000000000..d097696e0 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/MultiChoiceSetting.kt @@ -0,0 +1,46 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model.view +import org.citra.citra_emu.features.settings.model.AbstractSetting +import org.citra.citra_emu.features.settings.model.IntListSetting +class MultiChoiceSetting( + setting: AbstractSetting?, + titleId: Int, + descriptionId: Int, + val choicesId: Int, + val valuesId: Int, + val key: String? = null, + val defaultValue: List? = null, + override var isEnabled: Boolean = true +) : SettingsItem(setting, titleId, descriptionId) { + override val type = TYPE_MULTI_CHOICE + + val selectedValues: List + get() { + if (setting == null) { + return defaultValue!! + } + try { + val setting = setting as IntListSetting + return setting.list + }catch (_: ClassCastException) { + } + return defaultValue!! + } + + /** + * Write a value to the backing list. If that int was previously null, + * initializes a new one and returns it, so it can be added to the Hashmap. + * + * @param selection New value of the int. + * @return the existing setting with the new value applied. + */ + fun setSelectedValue(selection: List): IntListSetting { + val intSetting = setting as IntListSetting + intSetting.list = selection + return intSetting + } + +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt index c3f11def5..68aa2226c 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt @@ -47,5 +47,6 @@ abstract class SettingsItem( const val TYPE_INPUT_BINDING = 8 const val TYPE_STRING_INPUT = 9 const val TYPE_FLOAT_INPUT = 10 + const val TYPE_MULTI_CHOICE = 11 } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt index 4bd5d3b5f..054ff8d63 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt @@ -41,12 +41,14 @@ import org.citra.citra_emu.features.settings.model.AbstractIntSetting import org.citra.citra_emu.features.settings.model.AbstractSetting import org.citra.citra_emu.features.settings.model.AbstractStringSetting import org.citra.citra_emu.features.settings.model.FloatSetting +import org.citra.citra_emu.features.settings.model.IntListSetting import org.citra.citra_emu.features.settings.model.ScaledFloatSetting import org.citra.citra_emu.features.settings.model.AbstractShortSetting import org.citra.citra_emu.features.settings.model.view.DateTimeSetting import org.citra.citra_emu.features.settings.model.view.InputBindingSetting import org.citra.citra_emu.features.settings.model.view.SettingsItem import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting +import org.citra.citra_emu.features.settings.model.view.MultiChoiceSetting import org.citra.citra_emu.features.settings.model.view.SliderSetting import org.citra.citra_emu.features.settings.model.view.StringInputSetting import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting @@ -55,6 +57,7 @@ import org.citra.citra_emu.features.settings.model.view.SwitchSetting import org.citra.citra_emu.features.settings.ui.viewholder.DateTimeViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.HeaderViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.InputBindingSettingViewHolder +import org.citra.citra_emu.features.settings.ui.viewholder.MultiChoiceViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.RunnableViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.SettingViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.SingleChoiceViewHolder @@ -72,7 +75,8 @@ import kotlin.math.roundToInt class SettingsAdapter( private val fragmentView: SettingsFragmentView, public val context: Context -) : RecyclerView.Adapter(), DialogInterface.OnClickListener { +) : RecyclerView.Adapter(), DialogInterface.OnClickListener, + DialogInterface.OnMultiChoiceClickListener { private var settings: ArrayList? = null private var clickedItem: SettingsItem? = null private var clickedPosition: Int @@ -104,6 +108,10 @@ class SettingsAdapter( SingleChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this) } + SettingsItem.TYPE_MULTI_CHOICE -> { + MultiChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this) + } + SettingsItem.TYPE_SLIDER -> { SliderViewHolder(ListItemSettingBinding.inflate(inflater), this) } @@ -181,21 +189,30 @@ class SettingsAdapter( SettingsItem.TYPE_SLIDER -> { (oldItem as SliderSetting).isEnabled == (newItem as SliderSetting).isEnabled } + SettingsItem.TYPE_SWITCH -> { (oldItem as SwitchSetting).isEnabled == (newItem as SwitchSetting).isEnabled } + SettingsItem.TYPE_SINGLE_CHOICE -> { (oldItem as SingleChoiceSetting).isEnabled == (newItem as SingleChoiceSetting).isEnabled } + SettingsItem.TYPE_MULTI_CHOICE -> { + (oldItem as MultiChoiceSetting).isEnabled == (newItem as MultiChoiceSetting).isEnabled + } + SettingsItem.TYPE_DATETIME_SETTING -> { (oldItem as DateTimeSetting).isEnabled == (newItem as DateTimeSetting).isEnabled } + SettingsItem.TYPE_STRING_SINGLE_CHOICE -> { (oldItem as StringSingleChoiceSetting).isEnabled == (newItem as StringSingleChoiceSetting).isEnabled } + SettingsItem.TYPE_STRING_INPUT -> { (oldItem as StringInputSetting).isEnabled == (newItem as StringInputSetting).isEnabled } + else -> { oldItem == newItem } @@ -214,7 +231,7 @@ class SettingsAdapter( // If statement is required otherwise the app will crash on activity recreate ex. theme settings if (fragmentView.activityView != null) - // Reload the settings list to update the UI + // Reload the settings list to update the UI fragmentView.loadSettingsList() } @@ -232,6 +249,27 @@ class SettingsAdapter( onSingleChoiceClick(item) } + private fun onMultiChoiceClick(item: MultiChoiceSetting) { + clickedItem = item + + val value: BooleanArray = getSelectionForMultiChoiceValue(item); + dialog = MaterialAlertDialogBuilder(context) + .setTitle(item.nameId) + .setMultiChoiceItems(item.choicesId, value, this) + .setOnDismissListener { + if (clickedPosition != -1) { + notifyItemChanged(clickedPosition) + clickedPosition = -1 + } + } + .show() + } + + fun onMultiChoiceClick(item: MultiChoiceSetting, position: Int) { + clickedPosition = position + onMultiChoiceClick(item) + } + private fun onStringSingleChoiceClick(item: StringSingleChoiceSetting) { clickedItem = item dialog = context?.let { @@ -360,14 +398,14 @@ class SettingsAdapter( sliderString = sliderProgress.roundToInt().toString() if (textSliderValue?.text.toString() != sliderString) { textSliderValue?.setText(sliderString) - textSliderValue?.setSelection(textSliderValue?.length() ?: 0 ) + textSliderValue?.setSelection(textSliderValue?.length() ?: 0) } } else { val currentText = textSliderValue?.text.toString() val currentTextValue = currentText.toFloat() if (currentTextValue != sliderProgress) { textSliderValue?.setText(sliderString) - textSliderValue?.setSelection(textSliderValue?.length() ?: 0 ) + textSliderValue?.setSelection(textSliderValue?.length() ?: 0) } } } @@ -447,6 +485,7 @@ class SettingsAdapter( } it.setSelectedValue(value) } + is AbstractShortSetting -> { val value = getValueForSingleChoiceSelection(it, which).toShort() if (it.selectedValue.toShort() != value) { @@ -454,6 +493,7 @@ class SettingsAdapter( } it.setSelectedValue(value) } + else -> throw IllegalStateException("Unrecognized type used for SingleChoiceSetting!") } fragmentView?.putSetting(setting) @@ -499,11 +539,12 @@ class SettingsAdapter( val setting = it.setSelectedValue(value) fragmentView?.putSetting(setting) } + else -> { val setting = it.setSelectedValue(sliderProgress) fragmentView?.putSetting(setting) } - } + } fragmentView.loadSettingsList() closeDialog() } @@ -519,7 +560,7 @@ class SettingsAdapter( fragmentView?.putSetting(setting) fragmentView.loadSettingsList() closeDialog() - } + } } } clickedItem = null @@ -527,6 +568,21 @@ class SettingsAdapter( textInputValue = "" } + //onclick for multichoice + override fun onClick(dialog: DialogInterface?, which: Int, isChecked: Boolean) { + val mcsetting = clickedItem as? MultiChoiceSetting + mcsetting?.let { + val value = getValueForMultiChoiceSelection(it, which) + if (it.selectedValues.contains(value) != isChecked) { + val setting = it.setSelectedValue((if (isChecked) it.selectedValues + value else it.selectedValues - value).sorted()) + fragmentView?.putSetting(setting) + fragmentView?.onSettingChanged() + } + fragmentView.loadSettingsList() + } + } + + fun onLongClick(setting: AbstractSetting, position: Int): Boolean { MaterialAlertDialogBuilder(context) .setMessage(R.string.reset_setting_confirmation) @@ -631,6 +687,16 @@ class SettingsAdapter( } } + private fun getValueForMultiChoiceSelection(item: MultiChoiceSetting, which: Int): Int { + val valuesId = item.valuesId + return if (valuesId > 0) { + val valuesArray = context.resources.getIntArray(valuesId) + valuesArray[which] + } else { + which + } + } + private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int { val value = item.selectedValue val valuesId = item.valuesId @@ -647,4 +713,20 @@ class SettingsAdapter( } return -1 } + + private fun getSelectionForMultiChoiceValue(item: MultiChoiceSetting): BooleanArray { + val value = item.selectedValues; + val valuesId = item.valuesId; + if (valuesId > 0) { + val valuesArray = context.resources.getIntArray(valuesId); + val res = BooleanArray(valuesArray.size){false} + for (index in valuesArray.indices) { + if (value.contains(valuesArray[index])) { + res[index] = true; + } + } + return res; + } + return BooleanArray(1){false}; + } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index 9a40ba90b..be70309ac 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -14,6 +14,7 @@ import android.os.Build import android.text.TextUtils import androidx.preference.PreferenceManager import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.serialization.builtins.IntArraySerializer import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.R import org.citra.citra_emu.display.ScreenLayout @@ -27,12 +28,14 @@ import org.citra.citra_emu.features.settings.model.AbstractStringSetting import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.FloatSetting import org.citra.citra_emu.features.settings.model.IntSetting +import org.citra.citra_emu.features.settings.model.IntListSetting import org.citra.citra_emu.features.settings.model.ScaledFloatSetting import org.citra.citra_emu.features.settings.model.Settings import org.citra.citra_emu.features.settings.model.StringSetting import org.citra.citra_emu.features.settings.model.view.DateTimeSetting import org.citra.citra_emu.features.settings.model.view.HeaderSetting import org.citra.citra_emu.features.settings.model.view.InputBindingSetting +import org.citra.citra_emu.features.settings.model.view.MultiChoiceSetting import org.citra.citra_emu.features.settings.model.view.RunnableSetting import org.citra.citra_emu.features.settings.model.view.SettingsItem import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting @@ -1157,6 +1160,17 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) BooleanSetting.UPRIGHT_SCREEN.defaultValue ) ) + add( + MultiChoiceSetting( + IntListSetting.LAYOUTS_TO_CYCLE, + R.string.layouts_to_cycle, + R.string.layouts_to_cycle_description, + R.array.landscapeLayouts, + R.array.landscapeLayoutValues, + IntListSetting.LAYOUTS_TO_CYCLE.key, + IntListSetting.LAYOUTS_TO_CYCLE.defaultValue + ) + ) add( SingleChoiceSetting( IntSetting.PORTRAIT_SCREEN_LAYOUT, diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt new file mode 100644 index 000000000..8493115a4 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt @@ -0,0 +1,80 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.ui.viewholder + +import android.view.View +import org.citra.citra_emu.databinding.ListItemSettingBinding +import org.citra.citra_emu.features.settings.model.view.SettingsItem +import org.citra.citra_emu.features.settings.model.view.MultiChoiceSetting +import org.citra.citra_emu.features.settings.ui.SettingsAdapter + +class MultiChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : + SettingViewHolder(binding.root, adapter) { + private lateinit var setting: SettingsItem + + override fun bind(item: SettingsItem) { + setting = item + binding.textSettingName.setText(item.nameId) + if (item.descriptionId != 0) { + binding.textSettingDescription.visibility = View.VISIBLE + binding.textSettingDescription.setText(item.descriptionId) + } else { + binding.textSettingDescription.visibility = View.GONE + } + binding.textSettingValue.visibility = View.VISIBLE + binding.textSettingValue.text = getTextSetting() + + if (setting.isActive) { + binding.textSettingName.alpha = 1f + binding.textSettingDescription.alpha = 1f + binding.textSettingValue.alpha = 1f + } else { + binding.textSettingName.alpha = 0.5f + binding.textSettingDescription.alpha = 0.5f + binding.textSettingValue.alpha = 0.5f + } + } + + private fun getTextSetting(): String { + when (val item = setting) { + is MultiChoiceSetting -> { + val resMgr = binding.textSettingDescription.context.resources + val values = resMgr.getIntArray(item.valuesId) + var resList:List = emptyList(); + values.forEachIndexed { i: Int, value: Int -> + if ((setting as MultiChoiceSetting).selectedValues.contains(value)) { + resList = resList + resMgr.getStringArray(item.choicesId)[i]; + } + } + return resList.joinToString(); + } + + else -> return "" + } + } + + override fun onClick(clicked: View) { + if (!setting.isEditable || !setting.isEnabled) { + adapter.onClickDisabledSetting(!setting.isEditable) + return + } + + if (setting is MultiChoiceSetting) { + adapter.onMultiChoiceClick( + (setting as MultiChoiceSetting), + bindingAdapterPosition + ) + } + } + + override fun onLongClick(clicked: View): Boolean { + if (setting.isActive) { + return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) + } else { + adapter.onClickDisabledSetting(!setting.isEditable) + } + return false + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt index dec3e4e0a..a9e1d4743 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -12,6 +12,7 @@ import org.citra.citra_emu.R import org.citra.citra_emu.features.settings.model.AbstractSetting import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.FloatSetting +import org.citra.citra_emu.features.settings.model.IntListSetting import org.citra.citra_emu.features.settings.model.IntSetting import org.citra.citra_emu.features.settings.model.ScaledFloatSetting import org.citra.citra_emu.features.settings.model.SettingSection @@ -255,6 +256,11 @@ object SettingsFile { return stringSetting } + val intListSetting = IntListSetting.from(key) + if (intListSetting != null) { + intListSetting.list = value.split(", ").map { it.toInt() } + } + return null } diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index de2a78ee9..7e8257df3 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -361,6 +361,8 @@ Reverse Landscape Portrait Reverse Portrait + Layouts to Cycle + Which layouts are cycled by the Cycle Layout hotkey Default 16:9 4:3 diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index e6faa88ce..fcb7eabe5 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -49,6 +49,9 @@ add_library(citra_qt STATIC EXCLUDE_FROM_ALL configuration/configure_layout.cpp configuration/configure_layout.h configuration/configure_layout.ui + configuration/configure_layout_cycle.cpp + configuration/configure_layout_cycle.h + configuration/configure_layout_cycle.ui configuration/configure_dialog.cpp configuration/configure_dialog.h configuration/configure_general.cpp diff --git a/src/citra_qt/citra_qt.cpp b/src/citra_qt/citra_qt.cpp index d8e207ec9..c28418eac 100644 --- a/src/citra_qt/citra_qt.cpp +++ b/src/citra_qt/citra_qt.cpp @@ -2741,25 +2741,22 @@ void GMainWindow::AdjustSpeedLimit(bool increase) { void GMainWindow::ToggleScreenLayout() { const Settings::LayoutOption new_layout = []() { - switch (Settings::values.layout_option.GetValue()) { - case Settings::LayoutOption::Default: - return Settings::LayoutOption::SingleScreen; - case Settings::LayoutOption::SingleScreen: - return Settings::LayoutOption::LargeScreen; - case Settings::LayoutOption::LargeScreen: - return Settings::LayoutOption::HybridScreen; - case Settings::LayoutOption::HybridScreen: - return Settings::LayoutOption::SideScreen; - case Settings::LayoutOption::SideScreen: - return Settings::LayoutOption::SeparateWindows; - case Settings::LayoutOption::SeparateWindows: - return Settings::LayoutOption::CustomLayout; - case Settings::LayoutOption::CustomLayout: - return Settings::LayoutOption::Default; - default: - LOG_ERROR(Frontend, "Unknown layout option {}", - Settings::values.layout_option.GetValue()); - return Settings::LayoutOption::Default; + const Settings::LayoutOption current_layout = Settings::values.layout_option.GetValue(); + std::vector layouts_to_cycle = + Settings::values.layouts_to_cycle.GetValue(); + const auto current_pos = + distance(layouts_to_cycle.begin(), + std::find(layouts_to_cycle.begin(), layouts_to_cycle.end(), current_layout)); + // if the layouts_to_cycle setting has somehow been + // cleared out, add just default back in + if (layouts_to_cycle.size() == 0) { + layouts_to_cycle.push_back(Settings::LayoutOption::Default); + } + if (current_pos >= layouts_to_cycle.size() - 1) { + // either this layout wasn't found or it was last so move to the beginning + return layouts_to_cycle[0]; + } else { + return layouts_to_cycle[current_pos + 1]; } }(); diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index 35abe81ef..e909fd4d9 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include "citra_qt/configuration/config.h" #include "common/file_util.h" #include "common/settings.h" @@ -132,6 +134,35 @@ void QtConfig::ReadBasicSetting(Settings::Setting& setting) { setting.SetValue(qt_config->value(name, default_value).toString().toStdString()); } } +// definition for vectors of enums +template +void QtConfig::ReadBasicSetting(Settings::Setting, ranged>& setting) { + const QString name = QString::fromStdString(setting.GetLabel()); + const std::vector default_value = setting.GetDefault(); + QStringList stringList = qt_config->value(name).toStringList(); + + if (qt_config->value(name + QStringLiteral("/default"), false).toBool() || + stringList.size() < 1) { + setting.SetValue(default_value); + } else { + if (stringList.size() < 1) { + setting.SetValue(default_value); + } else { + std::vector newValue; + for (const QString& str : stringList) { + if constexpr (std::is_enum_v) { + using TypeU = std::underlying_type_t; + newValue.push_back(static_cast(str.toInt())); + } else if constexpr (std::is_integral_v) { + newValue.push_back(str.toInt()); + } else { + newValue.push_back(str.toStdString()); + } + } + setting.SetValue(newValue); + } + } +} template void QtConfig::ReadBasicSetting(Settings::Setting& setting) { @@ -158,27 +189,7 @@ void QtConfig::ReadGlobalSetting(Settings::SwitchableSetting& sett const bool use_global = qt_config->value(name + QStringLiteral("/use_global"), true).toBool(); setting.SetGlobal(use_global); if (global || !use_global) { - QVariant default_value{}; - if constexpr (std::is_enum_v) { - using TypeU = std::underlying_type_t; - default_value = QVariant::fromValue(static_cast(setting.GetDefault())); - setting.SetValue(static_cast(ReadSetting(name, default_value).value())); - } else { - default_value = QVariant::fromValue(setting.GetDefault()); - setting.SetValue(ReadSetting(name, default_value).value()); - } - } -} - -template <> -void QtConfig::ReadGlobalSetting(Settings::SwitchableSetting& setting) { - QString name = QString::fromStdString(setting.GetLabel()); - const bool use_global = qt_config->value(name + QStringLiteral("/use_global"), true).toBool(); - setting.SetGlobal(use_global); - if (global || !use_global) { - const QString default_value = QString::fromStdString(setting.GetDefault()); - setting.SetValue( - ReadSetting(name, QVariant::fromValue(default_value)).toString().toStdString()); + ReadBasicSetting(setting); } } @@ -187,16 +198,41 @@ template <> void QtConfig::WriteBasicSetting(const Settings::Setting& setting) { const QString name = QString::fromStdString(setting.GetLabel()); const std::string& value = setting.GetValue(); - qt_config->setValue(name + QStringLiteral("/default"), value == setting.GetDefault()); + if (global) + qt_config->setValue(name + QStringLiteral("/default"), value == setting.GetDefault()); qt_config->setValue(name, QString::fromStdString(value)); } +template +void QtConfig::WriteBasicSetting(const Settings::Setting, ranged>& setting) { + const QString name = QString::fromStdString(setting.GetLabel()); + const std::vector& value = setting.GetValue(); + + if (global) + qt_config->setValue(name + QStringLiteral("/default"), value == setting.GetDefault()); + + QStringList stringList; + if constexpr (std::is_enum_v) { + // For enums, convert to underlying integer type strings + using TypeU = std::underlying_type_t; + for (const Type& item : value) { + stringList.append(QString::number(static_cast(item))); + } + } else { + // For non-enum types (assuming numeric) + for (const Type& item : value) { + stringList.append(QString::number(item)); + } + } + qt_config->setValue(name, stringList); +} // Explicit u16 definition: Qt would store it as QMetaType otherwise, which is not human-readable template <> void QtConfig::WriteBasicSetting(const Settings::Setting& setting) { const QString name = QString::fromStdString(setting.GetLabel()); const u16& value = setting.GetValue(); - qt_config->setValue(name + QStringLiteral("/default"), value == setting.GetDefault()); + if (global) + qt_config->setValue(name + QStringLiteral("/default"), value == setting.GetDefault()); qt_config->setValue(name, static_cast(value)); } @@ -204,7 +240,8 @@ template void QtConfig::WriteBasicSetting(const Settings::Setting& setting) { const QString name = QString::fromStdString(setting.GetLabel()); const Type value = setting.GetValue(); - qt_config->setValue(name + QStringLiteral("/default"), value == setting.GetDefault()); + if (global) + qt_config->setValue(name + QStringLiteral("/default"), value == setting.GetDefault()); if constexpr (std::is_enum_v) { qt_config->setValue(name, static_cast>(value)); } else { @@ -215,44 +252,11 @@ void QtConfig::WriteBasicSetting(const Settings::Setting& setting) template void QtConfig::WriteGlobalSetting(const Settings::SwitchableSetting& setting) { const QString name = QString::fromStdString(setting.GetLabel()); - const Type& value = setting.GetValue(global); if (!global) { qt_config->setValue(name + QStringLiteral("/use_global"), setting.UsingGlobal()); } if (global || !setting.UsingGlobal()) { - qt_config->setValue(name + QStringLiteral("/default"), value == setting.GetDefault()); - if constexpr (std::is_enum_v) { - qt_config->setValue(name, static_cast>(value)); - } else { - qt_config->setValue(name, QVariant::fromValue(value)); - } - } -} - -template <> -void QtConfig::WriteGlobalSetting(const Settings::SwitchableSetting& setting) { - const QString name = QString::fromStdString(setting.GetLabel()); - const std::string& value = setting.GetValue(global); - if (!global) { - qt_config->setValue(name + QStringLiteral("/use_global"), setting.UsingGlobal()); - } - if (global || !setting.UsingGlobal()) { - qt_config->setValue(name + QStringLiteral("/default"), value == setting.GetDefault()); - qt_config->setValue(name, QString::fromStdString(value)); - } -} - -// Explicit u16 definition: Qt would store it as QMetaType otherwise, which is not human-readable -template <> -void QtConfig::WriteGlobalSetting(const Settings::SwitchableSetting& setting) { - const QString name = QString::fromStdString(setting.GetLabel()); - const u16& value = setting.GetValue(global); - if (!global) { - qt_config->setValue(name + QStringLiteral("/use_global"), setting.UsingGlobal()); - } - if (global || !setting.UsingGlobal()) { - qt_config->setValue(name + QStringLiteral("/default"), value == setting.GetDefault()); - qt_config->setValue(name, static_cast(value)); + WriteBasicSetting(setting); } } @@ -528,7 +532,7 @@ void QtConfig::ReadLayoutValues() { ReadGlobalSetting(Settings::values.large_screen_proportion); ReadGlobalSetting(Settings::values.screen_gap); ReadGlobalSetting(Settings::values.small_screen_position); - + ReadGlobalSetting(Settings::values.layouts_to_cycle); if (global) { ReadBasicSetting(Settings::values.mono_render_option); ReadBasicSetting(Settings::values.custom_top_x); @@ -1109,6 +1113,7 @@ void QtConfig::SaveLayoutValues() { WriteGlobalSetting(Settings::values.large_screen_proportion); WriteGlobalSetting(Settings::values.screen_gap); WriteGlobalSetting(Settings::values.small_screen_position); + WriteGlobalSetting(Settings::values.layouts_to_cycle); if (global) { WriteBasicSetting(Settings::values.mono_render_option); WriteBasicSetting(Settings::values.custom_top_x); @@ -1445,7 +1450,8 @@ void QtConfig::WriteSetting(const QString& name, const QVariant& value) { void QtConfig::WriteSetting(const QString& name, const QVariant& value, const QVariant& default_value) { - qt_config->setValue(name + QStringLiteral("/default"), value == default_value); + if (global) + qt_config->setValue(name + QStringLiteral("/default"), value == default_value); qt_config->setValue(name, value); } diff --git a/src/citra_qt/configuration/config.h b/src/citra_qt/configuration/config.h index 2c9039dde..3fba498ed 100644 --- a/src/citra_qt/configuration/config.h +++ b/src/citra_qt/configuration/config.h @@ -120,6 +120,10 @@ private: template void ReadBasicSetting(Settings::Setting& setting); + // Add overload for vectors + template + void ReadBasicSetting(Settings::Setting, ranged>& setting); + /** Sets a value from the setting in the qt_config using the setting's label and default value. * * @param The setting @@ -127,6 +131,9 @@ private: template void WriteBasicSetting(const Settings::Setting& setting); + template + void WriteBasicSetting(const Settings::Setting, ranged>& setting); + ConfigType type; std::unique_ptr qt_config; std::string qt_config_loc; diff --git a/src/citra_qt/configuration/configure_layout.cpp b/src/citra_qt/configuration/configure_layout.cpp index 58a0e66da..1cd06d937 100644 --- a/src/citra_qt/configuration/configure_layout.cpp +++ b/src/citra_qt/configuration/configure_layout.cpp @@ -6,6 +6,7 @@ #include #include "citra_qt/configuration/configuration_shared.h" #include "citra_qt/configuration/configure_layout.h" +#include "citra_qt/configuration/configure_layout_cycle.h" #include "common/settings.h" #include "ui_configure_layout.h" #ifdef ENABLE_OPENGL @@ -111,6 +112,13 @@ ConfigureLayout::ConfigureLayout(QWidget* parent) ui->bg_button->setIcon(color_icon); ui->bg_button->setEnabled(true); }); + + connect(ui->customize_layouts_to_cycle, &QPushButton::clicked, this, [this] { + ui->customize_layouts_to_cycle->setEnabled(false); + QDialog* layout_cycle_dialog = new ConfigureLayoutCycle(this); + layout_cycle_dialog->exec(); + ui->customize_layouts_to_cycle->setEnabled(true); + }); } ConfigureLayout::~ConfigureLayout() = default; diff --git a/src/citra_qt/configuration/configure_layout.ui b/src/citra_qt/configuration/configure_layout.ui index 1125ab2fb..fc4d149dc 100644 --- a/src/citra_qt/configuration/configure_layout.ui +++ b/src/citra_qt/configuration/configure_layout.ui @@ -6,8 +6,8 @@ 0 0 - 705 - 656 + 659 + 662 @@ -51,8 +51,8 @@ 0 0 - 688 - 799 + 646 + 824 @@ -130,18 +130,39 @@ - - - Swap screens - - - - - - - Rotate screens upright - - + + + + + + + Rotate screens upright + + + + + + + Swap screens + + + + + + + + + + 0 + 0 + + + + Customize layout cycling + + + + @@ -356,7 +377,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -376,7 +397,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -396,7 +417,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -416,7 +437,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -440,6 +461,12 @@ Bottom Screen + + false + + + false + @@ -451,7 +478,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -471,7 +498,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -491,7 +518,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -511,7 +538,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -538,7 +565,7 @@ - QAbstractSpinBox::ButtonSymbols::PlusMinus + QAbstractSpinBox::UpDownArrows 10 @@ -583,7 +610,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -610,7 +637,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -659,7 +686,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -672,7 +699,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -717,9 +744,6 @@ - - Qt::Orientation::Vertical - 20 diff --git a/src/citra_qt/configuration/configure_layout_cycle.cpp b/src/citra_qt/configuration/configure_layout_cycle.cpp new file mode 100644 index 000000000..11f1f116f --- /dev/null +++ b/src/citra_qt/configuration/configure_layout_cycle.cpp @@ -0,0 +1,92 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. +#include +#include +#include +#include "citra_qt/configuration/configure_layout_cycle.h" +#include "ui_configure_layout_cycle.h" + +ConfigureLayoutCycle::ConfigureLayoutCycle(QWidget* parent) + : QDialog(parent), ui(std::make_unique()) { + ui->setupUi(this); + SetConfiguration(); + ConnectEvents(); +} + +// You MUST define the destructor in the .cpp file +ConfigureLayoutCycle::~ConfigureLayoutCycle() = default; + +void ConfigureLayoutCycle::ConnectEvents() { + disconnect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, + &ConfigureLayoutCycle::ApplyConfiguration); + connect(ui->globalCheck, &QCheckBox::stateChanged, this, &ConfigureLayoutCycle::UpdateGlobal); +} + +void ConfigureLayoutCycle::SetConfiguration() { + if (Settings::IsConfiguringGlobal()) { + ui->globalCheck->setChecked(true); + ui->globalCheck->setVisible(false); + } else { + ui->globalCheck->setChecked(Settings::values.layouts_to_cycle.UsingGlobal()); + ui->checkGroup->setDisabled(Settings::values.layouts_to_cycle.UsingGlobal()); + } + for (auto option : Settings::values.layouts_to_cycle.GetValue()) { + switch (option) { + case Settings::LayoutOption::Default: + ui->defaultCheck->setChecked(true); + break; + case Settings::LayoutOption::SingleScreen: + ui->singleCheck->setChecked(true); + break; + case Settings::LayoutOption::LargeScreen: + ui->largeCheck->setChecked(true); + break; + case Settings::LayoutOption::SideScreen: + ui->sidebysideCheck->setChecked(true); + break; + case Settings::LayoutOption::SeparateWindows: + ui->separateCheck->setChecked(true); + break; + case Settings::LayoutOption::HybridScreen: + ui->hybridCheck->setChecked(true); + break; + case Settings::LayoutOption::CustomLayout: + ui->customCheck->setChecked(true); + break; + } + } +} + +void ConfigureLayoutCycle::ApplyConfiguration() { + std::vector newSetting{}; + if (ui->defaultCheck->isChecked()) + newSetting.push_back(Settings::LayoutOption::Default); + if (ui->singleCheck->isChecked()) + newSetting.push_back(Settings::LayoutOption::SingleScreen); + if (ui->sidebysideCheck->isChecked()) + newSetting.push_back(Settings::LayoutOption::SideScreen); + if (ui->largeCheck->isChecked()) + newSetting.push_back(Settings::LayoutOption::LargeScreen); + if (ui->separateCheck->isChecked()) + newSetting.push_back(Settings::LayoutOption::SeparateWindows); + if (ui->hybridCheck->isChecked()) + newSetting.push_back(Settings::LayoutOption::HybridScreen); + if (ui->customCheck->isChecked()) + newSetting.push_back(Settings::LayoutOption::CustomLayout); + if (newSetting.empty()) { + QMessageBox::warning(this, tr("No Layout Selected"), + tr("Please select at least one layout option to cycle through.")); + return; + } else { + Settings::values.layouts_to_cycle = newSetting; + accept(); + } +} + +void ConfigureLayoutCycle::UpdateGlobal() { + Settings::values.layouts_to_cycle.SetGlobal(ui->globalCheck->isChecked()); + ui->checkGroup->setDisabled(ui->globalCheck->isChecked()); + ui->checkGroup->repaint(); // Force visual update +} diff --git a/src/citra_qt/configuration/configure_layout_cycle.h b/src/citra_qt/configuration/configure_layout_cycle.h new file mode 100644 index 000000000..bde8f3cb9 --- /dev/null +++ b/src/citra_qt/configuration/configure_layout_cycle.h @@ -0,0 +1,32 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. +#pragma once + +#include +#include +#include "common/settings.h" + +namespace Ui { +class ConfigureLayoutCycle; +} + +class ConfigureLayoutCycle : public QDialog { + Q_OBJECT + +public: + explicit ConfigureLayoutCycle(QWidget* parent = nullptr); + ~ConfigureLayoutCycle() override; + +public slots: + void ApplyConfiguration(); + +private slots: + +private: + void SetConfiguration(); + void ConnectEvents(); + void UpdateGlobal(); + + std::unique_ptr ui; +}; \ No newline at end of file diff --git a/src/citra_qt/configuration/configure_layout_cycle.ui b/src/citra_qt/configuration/configure_layout_cycle.ui new file mode 100644 index 000000000..e1d6aef4f --- /dev/null +++ b/src/citra_qt/configuration/configure_layout_cycle.ui @@ -0,0 +1,216 @@ + + + ConfigureLayoutCycle + + + Qt::ApplicationModal + + + + 0 + 0 + 395 + 334 + + + + + 0 + 0 + + + + Configure Layout Cycling + + + + + 10 + 10 + 381 + 323 + + + + + QLayout::SetDefaultConstraint + + + + + + 0 + 0 + + + + + 14 + + + + Screen Layout Cycling Customization + + + + + + + + 0 + 0 + + + + Select which screen layout options should be cycled with the "Toggle Screen Layout" hotkey + + + true + + + + + + + Use Global Value + + + true + + + + + + + true + + + + + + Qt::Horizontal + + + + + + + Default + + + false + + + + + + + Single Screen + + + false + + + + + + + Large Screen + + + false + + + + + + + Side by Side + + + false + + + + + + + Separate Windows + + + false + + + + + + + Hybrid + + + false + + + + + + + Custom + + + false + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + buttonBox + accepted() + ConfigureLayoutCycle + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + ConfigureLayoutCycle + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/common/settings.cpp b/src/common/settings.cpp index d0a08b587..eb8e9be3b 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -124,6 +124,7 @@ void LogSettings() { log_setting("Layout_ScreenGap", values.screen_gap.GetValue()); log_setting("Layout_LargeScreenProportion", values.large_screen_proportion.GetValue()); log_setting("Layout_SmallScreenPosition", values.small_screen_position.GetValue()); + // log_setting("Layout_LayoutsToCycle",values.layouts_to_cycle.GetValue()); log_setting("Utility_DumpTextures", values.dump_textures.GetValue()); log_setting("Utility_CustomTextures", values.custom_textures.GetValue()); log_setting("Utility_PreloadTextures", values.preload_textures.GetValue()); @@ -215,6 +216,7 @@ void RestoreGlobalState(bool is_powered_on) { values.layout_option.SetGlobal(true); values.portrait_layout_option.SetGlobal(true); values.secondary_display_layout.SetGlobal(true); + values.layouts_to_cycle.SetGlobal(true); values.swap_screen.SetGlobal(true); values.upright_screen.SetGlobal(true); values.large_screen_proportion.SetGlobal(true); diff --git a/src/common/settings.h b/src/common/settings.h index 178db0019..0f0bdd23f 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -538,6 +538,14 @@ struct Values { SwitchableSetting upright_screen{false, "upright_screen"}; SwitchableSetting secondary_display_layout{SecondaryDisplayLayout::None, "secondary_display_layout"}; + SwitchableSetting> layouts_to_cycle{ + {LayoutOption::Default, LayoutOption::SingleScreen, LayoutOption::LargeScreen, + LayoutOption::SideScreen, +#ifndef ANDROID + LayoutOption::SeparateWindows, +#endif + LayoutOption::HybridScreen, LayoutOption::CustomLayout}, + "layouts_to_cycle"}; SwitchableSetting large_screen_proportion{4.f, 1.f, 16.f, "large_screen_proportion"}; SwitchableSetting screen_gap{0, "screen_gap"}; From 526d9d4cea8da1e1d9e704d2c0bfcc74acb70431 Mon Sep 17 00:00:00 2001 From: Richard Date: Fri, 27 Feb 2026 12:57:41 -0600 Subject: [PATCH 29/42] android: Add auto-map controller button with long-press to clear all bindings (#1769) --- .../model/view/InputBindingSetting.kt | 256 +++++++++++++++++- .../settings/model/view/RunnableSetting.kt | 9 +- .../settings/model/view/SettingsItem.kt | 2 +- .../features/settings/ui/SettingsAdapter.kt | 47 +++- .../settings/ui/SettingsFragmentPresenter.kt | 10 + .../ui/viewholder/RunnableViewHolder.kt | 7 +- .../fragments/AutoMapDialogFragment.kt | 152 +++++++++++ .../drawable-xxxhdpi/automap_face_buttons.png | Bin 0 -> 33023 bytes .../src/main/res/layout/dialog_auto_map.xml | 55 ++++ .../app/src/main/res/values/strings.xml | 6 + 10 files changed, 509 insertions(+), 35 deletions(-) create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/fragments/AutoMapDialogFragment.kt create mode 100644 src/android/app/src/main/res/drawable-xxxhdpi/automap_face_buttons.png create mode 100644 src/android/app/src/main/res/layout/dialog_auto_map.xml diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt index d78f5c3a3..6ec851db1 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt @@ -9,6 +9,7 @@ import android.content.SharedPreferences import android.view.InputDevice import android.view.InputDevice.MotionRange import android.view.KeyEvent +import android.view.MotionEvent import android.widget.Toast import androidx.preference.PreferenceManager import org.citra.citra_emu.CitraApplication @@ -235,8 +236,7 @@ class InputBindingSetting( val code = translateEventToKeyId(keyEvent) writeButtonMapping(keyEvent) - val uiString = "${keyEvent.device.name}: Button $code" - value = uiString + value = "${keyEvent.device.name}: ${getButtonName(code)}" } /** @@ -263,8 +263,7 @@ class InputBindingSetting( // use UP (-) to map vertical, but use RIGHT (+) to map horizontal val inverted = if (isHorizontalOrientation()) axisDir == '-' else axisDir == '+' writeAxisMapping(motionRange.axis, button, inverted) - val uiString = "${device.name}: Axis ${motionRange.axis}" + axisDir - value = uiString + value = "Axis ${motionRange.axis}$axisDir" } override val type = TYPE_INPUT_BINDING @@ -272,6 +271,241 @@ class InputBindingSetting( companion object { private const val INPUT_MAPPING_PREFIX = "InputMapping" + private fun toTitleCase(raw: String): String = + raw.replace("_", " ").lowercase() + .split(" ").joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } } + + private const val BUTTON_NAME_L3 = "Button L3" + private const val BUTTON_NAME_R3 = "Button R3" + + private val buttonNameOverrides = mapOf( + KeyEvent.KEYCODE_BUTTON_THUMBL to BUTTON_NAME_L3, + KeyEvent.KEYCODE_BUTTON_THUMBR to BUTTON_NAME_R3, + LINUX_BTN_DPAD_UP to "Dpad Up", + LINUX_BTN_DPAD_DOWN to "Dpad Down", + LINUX_BTN_DPAD_LEFT to "Dpad Left", + LINUX_BTN_DPAD_RIGHT to "Dpad Right" + ) + + fun getButtonName(keyCode: Int): String = + buttonNameOverrides[keyCode] + ?: toTitleCase(KeyEvent.keyCodeToString(keyCode).removePrefix("KEYCODE_")) + + private data class DefaultButtonMapping( + val settingKey: String, + val hostKeyCode: Int, + val guestButtonCode: Int + ) + // Auto-map always sets inverted = false. Users needing inverted axes should remap manually. + private data class DefaultAxisMapping( + val settingKey: String, + val hostAxis: Int, + val guestButton: Int, + val orientation: Int, + val inverted: Boolean + ) + + private val xboxFaceButtonMappings = listOf( + DefaultButtonMapping(Settings.KEY_BUTTON_A, KeyEvent.KEYCODE_BUTTON_B, NativeLibrary.ButtonType.BUTTON_A), + DefaultButtonMapping(Settings.KEY_BUTTON_B, KeyEvent.KEYCODE_BUTTON_A, NativeLibrary.ButtonType.BUTTON_B), + DefaultButtonMapping(Settings.KEY_BUTTON_X, KeyEvent.KEYCODE_BUTTON_Y, NativeLibrary.ButtonType.BUTTON_X), + DefaultButtonMapping(Settings.KEY_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_X, NativeLibrary.ButtonType.BUTTON_Y) + ) + + private val nintendoFaceButtonMappings = listOf( + DefaultButtonMapping(Settings.KEY_BUTTON_A, KeyEvent.KEYCODE_BUTTON_A, NativeLibrary.ButtonType.BUTTON_A), + DefaultButtonMapping(Settings.KEY_BUTTON_B, KeyEvent.KEYCODE_BUTTON_B, NativeLibrary.ButtonType.BUTTON_B), + DefaultButtonMapping(Settings.KEY_BUTTON_X, KeyEvent.KEYCODE_BUTTON_X, NativeLibrary.ButtonType.BUTTON_X), + DefaultButtonMapping(Settings.KEY_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_Y, NativeLibrary.ButtonType.BUTTON_Y) + ) + + private val commonButtonMappings = listOf( + DefaultButtonMapping(Settings.KEY_BUTTON_L, KeyEvent.KEYCODE_BUTTON_L1, NativeLibrary.ButtonType.TRIGGER_L), + DefaultButtonMapping(Settings.KEY_BUTTON_R, KeyEvent.KEYCODE_BUTTON_R1, NativeLibrary.ButtonType.TRIGGER_R), + DefaultButtonMapping(Settings.KEY_BUTTON_ZL, KeyEvent.KEYCODE_BUTTON_L2, NativeLibrary.ButtonType.BUTTON_ZL), + DefaultButtonMapping(Settings.KEY_BUTTON_ZR, KeyEvent.KEYCODE_BUTTON_R2, NativeLibrary.ButtonType.BUTTON_ZR), + DefaultButtonMapping(Settings.KEY_BUTTON_SELECT, KeyEvent.KEYCODE_BUTTON_SELECT, NativeLibrary.ButtonType.BUTTON_SELECT), + DefaultButtonMapping(Settings.KEY_BUTTON_START, KeyEvent.KEYCODE_BUTTON_START, NativeLibrary.ButtonType.BUTTON_START) + ) + + private val dpadButtonMappings = listOf( + DefaultButtonMapping(Settings.KEY_BUTTON_UP, KeyEvent.KEYCODE_DPAD_UP, NativeLibrary.ButtonType.DPAD_UP), + DefaultButtonMapping(Settings.KEY_BUTTON_DOWN, KeyEvent.KEYCODE_DPAD_DOWN, NativeLibrary.ButtonType.DPAD_DOWN), + DefaultButtonMapping(Settings.KEY_BUTTON_LEFT, KeyEvent.KEYCODE_DPAD_LEFT, NativeLibrary.ButtonType.DPAD_LEFT), + DefaultButtonMapping(Settings.KEY_BUTTON_RIGHT, KeyEvent.KEYCODE_DPAD_RIGHT, NativeLibrary.ButtonType.DPAD_RIGHT) + ) + + private val stickAxisMappings = listOf( + DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL, MotionEvent.AXIS_X, NativeLibrary.ButtonType.STICK_LEFT, 0, false), + DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_VERTICAL, MotionEvent.AXIS_Y, NativeLibrary.ButtonType.STICK_LEFT, 1, false), + DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_HORIZONTAL, MotionEvent.AXIS_Z, NativeLibrary.ButtonType.STICK_C, 0, false), + DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_VERTICAL, MotionEvent.AXIS_RZ, NativeLibrary.ButtonType.STICK_C, 1, false) + ) + + private val dpadAxisMappings = listOf( + DefaultAxisMapping(Settings.KEY_DPAD_AXIS_HORIZONTAL, MotionEvent.AXIS_HAT_X, NativeLibrary.ButtonType.DPAD, 0, false), + DefaultAxisMapping(Settings.KEY_DPAD_AXIS_VERTICAL, MotionEvent.AXIS_HAT_Y, NativeLibrary.ButtonType.DPAD, 1, false) + ) + + // Nintendo Switch Joy-Con specific mappings. + // Joy-Cons connected via Bluetooth on Android have several quirks: + // - They register as two separate InputDevices (left and right) + // - Android's evdev translation swaps A<->B (BTN_EAST->BUTTON_B, BTN_SOUTH->BUTTON_A) + // but does NOT swap X<->Y (BTN_NORTH->BUTTON_X, BTN_WEST->BUTTON_Y) + // - D-pad buttons arrive as KEYCODE_UNKNOWN (0) with Linux BTN_DPAD_* scan codes + // - Right stick uses AXIS_RX/AXIS_RY instead of AXIS_Z/AXIS_RZ + private const val NINTENDO_VENDOR_ID = 0x057e + + // Linux BTN_DPAD_* values (0x220-0x223). Joy-Con D-pad buttons arrive as + // KEYCODE_UNKNOWN with these scan codes because Android's input layer doesn't + // translate them to KEYCODE_DPAD_*. translateEventToKeyId() falls back to + // the scan code in that case. + private const val LINUX_BTN_DPAD_UP = 0x220 // 544 + private const val LINUX_BTN_DPAD_DOWN = 0x221 // 545 + private const val LINUX_BTN_DPAD_LEFT = 0x222 // 546 + private const val LINUX_BTN_DPAD_RIGHT = 0x223 // 547 + + // Joy-Con face buttons: A/B are swapped by Android's evdev layer, but X/Y are not. + // This is different from both the standard Xbox table (full swap) and the + // Nintendo table (no swap). + private val joyconFaceButtonMappings = listOf( + DefaultButtonMapping(Settings.KEY_BUTTON_A, KeyEvent.KEYCODE_BUTTON_B, NativeLibrary.ButtonType.BUTTON_A), + DefaultButtonMapping(Settings.KEY_BUTTON_B, KeyEvent.KEYCODE_BUTTON_A, NativeLibrary.ButtonType.BUTTON_B), + DefaultButtonMapping(Settings.KEY_BUTTON_X, KeyEvent.KEYCODE_BUTTON_X, NativeLibrary.ButtonType.BUTTON_X), + DefaultButtonMapping(Settings.KEY_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_Y, NativeLibrary.ButtonType.BUTTON_Y) + ) + + // Joy-Con D-pad: uses Linux scan codes because Android reports BTN_DPAD_* as KEYCODE_UNKNOWN + private val joyconDpadButtonMappings = listOf( + DefaultButtonMapping(Settings.KEY_BUTTON_UP, LINUX_BTN_DPAD_UP, NativeLibrary.ButtonType.DPAD_UP), + DefaultButtonMapping(Settings.KEY_BUTTON_DOWN, LINUX_BTN_DPAD_DOWN, NativeLibrary.ButtonType.DPAD_DOWN), + DefaultButtonMapping(Settings.KEY_BUTTON_LEFT, LINUX_BTN_DPAD_LEFT, NativeLibrary.ButtonType.DPAD_LEFT), + DefaultButtonMapping(Settings.KEY_BUTTON_RIGHT, LINUX_BTN_DPAD_RIGHT, NativeLibrary.ButtonType.DPAD_RIGHT) + ) + + // Joy-Con sticks: left stick is AXIS_X/Y (standard), right stick is AXIS_RX/RY + // (not Z/RZ like most controllers). The horizontal axis is inverted relative to + // the standard orientation - verified empirically on paired Joy-Cons via Bluetooth. + private val joyconStickAxisMappings = listOf( + DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL, MotionEvent.AXIS_X, NativeLibrary.ButtonType.STICK_LEFT, 0, false), + DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_VERTICAL, MotionEvent.AXIS_Y, NativeLibrary.ButtonType.STICK_LEFT, 1, false), + DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_HORIZONTAL, MotionEvent.AXIS_RX, NativeLibrary.ButtonType.STICK_C, 0, true), + DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_VERTICAL, MotionEvent.AXIS_RY, NativeLibrary.ButtonType.STICK_C, 1, false) + ) + + /** + * Detects whether a device is a Nintendo Switch Joy-Con (as opposed to a + * Pro Controller or other Nintendo device) by checking vendor ID + device + * capabilities. Joy-Cons lack AXIS_HAT_X/Y and use AXIS_RX/RY for the + * right stick, while the Pro Controller has standard HAT axes and Z/RZ. + */ + fun isJoyCon(device: InputDevice?): Boolean { + if (device == null) return false + if (device.vendorId != NINTENDO_VENDOR_ID) return false + + // Pro Controllers have HAT_X/HAT_Y (D-pad) and Z/RZ (right stick). + // Joy-Cons lack both: no HAT axes, right stick on RX/RY instead of Z/RZ. + var hasHatAxes = false + var hasStandardRightStick = false + for (range in device.motionRanges) { + when (range.axis) { + MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y -> hasHatAxes = true + MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ -> hasStandardRightStick = true + } + } + return !hasHatAxes && !hasStandardRightStick + } + + private val allBindingKeys: Set by lazy { + (Settings.buttonKeys + Settings.triggerKeys + + Settings.circlePadKeys + Settings.cStickKeys + Settings.dPadAxisKeys + + Settings.dPadButtonKeys).toSet() + } + + fun clearAllBindings() { + val prefs = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) + val editor = prefs.edit() + val allKeys = prefs.all.keys.toList() + for (key in allKeys) { + if (key.startsWith(INPUT_MAPPING_PREFIX) || key in allBindingKeys) { + editor.remove(key) + } + } + editor.apply() + } + + private fun applyBindings( + buttonMappings: List, + axisMappings: List + ) { + val prefs = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) + val editor = prefs.edit() + buttonMappings.forEach { applyDefaultButtonMapping(editor, it) } + axisMappings.forEach { applyDefaultAxisMapping(editor, it) } + editor.apply() + } + + /** + * Applies Joy-Con specific bindings: scan code D-pad, partial face button + * swap, and AXIS_RX/RY right stick. + */ + fun applyJoyConBindings() { + applyBindings( + joyconFaceButtonMappings + commonButtonMappings + joyconDpadButtonMappings, + joyconStickAxisMappings + ) + } + + /** + * Applies auto-mapped bindings based on detected controller layout and d-pad type. + * + * @param isNintendoLayout true if the controller uses Nintendo face button layout + * (A=east, B=south), false for Xbox layout (A=south, B=east) + * @param useAxisDpad true if the d-pad should be mapped as axis (HAT_X/HAT_Y), + * false if it should be mapped as individual button keycodes (DPAD_UP/DOWN/LEFT/RIGHT) + */ + fun applyAutoMapBindings(isNintendoLayout: Boolean, useAxisDpad: Boolean) { + val faceButtons = if (isNintendoLayout) nintendoFaceButtonMappings else xboxFaceButtonMappings + val buttonMappings = if (useAxisDpad) { + faceButtons + commonButtonMappings + } else { + faceButtons + commonButtonMappings + dpadButtonMappings + } + val axisMappings = if (useAxisDpad) { + stickAxisMappings + dpadAxisMappings + } else { + stickAxisMappings + } + applyBindings(buttonMappings, axisMappings) + } + + private fun applyDefaultButtonMapping( + editor: SharedPreferences.Editor, + mapping: DefaultButtonMapping + ) { + val prefKey = getInputButtonKey(mapping.hostKeyCode) + editor.putInt(prefKey, mapping.guestButtonCode) + editor.putString(mapping.settingKey, getButtonName(mapping.hostKeyCode)) + editor.putString( + "${INPUT_MAPPING_PREFIX}_ReverseMapping_${mapping.settingKey}", + prefKey + ) + } + + private fun applyDefaultAxisMapping( + editor: SharedPreferences.Editor, + mapping: DefaultAxisMapping + ) { + val axisKey = getInputAxisKey(mapping.hostAxis) + editor.putInt(getInputAxisOrientationKey(mapping.hostAxis), mapping.orientation) + editor.putInt(getInputAxisButtonKey(mapping.hostAxis), mapping.guestButton) + editor.putBoolean(getInputAxisInvertedKey(mapping.hostAxis), mapping.inverted) + val dir = if (mapping.orientation == 0) '+' else '-' + editor.putString(mapping.settingKey, "Axis ${mapping.hostAxis}$dir") + val reverseKey = "${INPUT_MAPPING_PREFIX}_ReverseMapping_${mapping.settingKey}_${mapping.orientation}" + editor.putString(reverseKey, axisKey) + } + /** * Returns the settings key for the specified Citra button code. */ @@ -315,18 +549,10 @@ class InputBindingSetting( return buttonCodes.mapNotNull { it.toIntOrNull() }.toMutableSet() } - /** - * Helper function to get the settings key for an gamepad button. - * - */ - @Deprecated("Use the new getInputButtonKey(keyEvent) method to handle unknown keys") - fun getInputButtonKey(keyCode: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${keyCode}" + private fun getInputButtonKey(keyId: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${keyId}" - /** - * Helper function to get the settings key for an gamepad button. - * - */ - fun getInputButtonKey(event: KeyEvent): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${translateEventToKeyId(event)}" + /** Falls back to the scan code when keyCode is KEYCODE_UNKNOWN. */ + fun getInputButtonKey(event: KeyEvent): String = getInputButtonKey(translateEventToKeyId(event)) /** * Helper function to get the settings key for an gamepad axis. diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/RunnableSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/RunnableSetting.kt index 99039556b..54e8fd09b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/RunnableSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/RunnableSetting.kt @@ -1,10 +1,11 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. package org.citra.citra_emu.features.settings.model.view import androidx.annotation.DrawableRes +import org.citra.citra_emu.activities.EmulationActivity class RunnableSetting( titleId: Int, @@ -12,7 +13,11 @@ class RunnableSetting( val isRuntimeRunnable: Boolean, @DrawableRes val iconId: Int = 0, val runnable: () -> Unit, - val value: (() -> String)? = null + val value: (() -> String)? = null, + val onLongClick: (() -> Boolean)? = null ) : SettingsItem(null, titleId, descriptionId) { override val type = TYPE_RUNNABLE + + override val isEditable: Boolean + get() = if (EmulationActivity.isRunning()) isRuntimeRunnable else true } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt index 68aa2226c..066912dd9 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt @@ -22,7 +22,7 @@ abstract class SettingsItem( ) { abstract val type: Int - val isEditable: Boolean + open val isEditable: Boolean get() { if (!EmulationActivity.isRunning()) return true return setting?.isRuntimeEditable ?: false diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt index 054ff8d63..43a1dcbbd 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt @@ -65,6 +65,7 @@ import org.citra.citra_emu.features.settings.ui.viewholder.SliderViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.StringInputViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.SwitchSettingViewHolder +import org.citra.citra_emu.fragments.AutoMapDialogFragment import org.citra.citra_emu.fragments.MessageDialogFragment import org.citra.citra_emu.fragments.MotionBottomSheetDialogFragment import org.citra.citra_emu.utils.SystemSaveGame @@ -642,26 +643,42 @@ class SettingsAdapter( ).show((fragmentView as SettingsFragment).childFragmentManager, MessageDialogFragment.TAG) } + fun onClickAutoMap() { + val activity = fragmentView.activityView as FragmentActivity + AutoMapDialogFragment.newInstance { + fragmentView.loadSettingsList() + fragmentView.onSettingChanged() + }.show(activity.supportFragmentManager, AutoMapDialogFragment.TAG) + } + + fun onLongClickAutoMap(): Boolean { + showConfirmationDialog(R.string.controller_clear_all, R.string.controller_clear_all_confirm) { + InputBindingSetting.clearAllBindings() + fragmentView.loadSettingsList() + fragmentView.onSettingChanged() + } + return true + } + fun onClickRegenerateConsoleId() { - MaterialAlertDialogBuilder(context) - .setTitle(R.string.regenerate_console_id) - .setMessage(R.string.regenerate_console_id_description) - .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> - SystemSaveGame.regenerateConsoleId() - notifyDataSetChanged() - } - .setNegativeButton(android.R.string.cancel, null) - .show() + showConfirmationDialog(R.string.regenerate_console_id, R.string.regenerate_console_id_description) { + SystemSaveGame.regenerateConsoleId() + notifyDataSetChanged() + } } fun onClickRegenerateMAC() { + showConfirmationDialog(R.string.regenerate_mac_address, R.string.regenerate_mac_address_description) { + SystemSaveGame.regenerateMac() + notifyDataSetChanged() + } + } + + private fun showConfirmationDialog(titleId: Int, messageId: Int, onConfirm: () -> Unit) { MaterialAlertDialogBuilder(context) - .setTitle(R.string.regenerate_mac_address) - .setMessage(R.string.regenerate_mac_address_description) - .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> - SystemSaveGame.regenerateMac() - notifyDataSetChanged() - } + .setTitle(titleId) + .setMessage(messageId) + .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> onConfirm() } .setNegativeButton(android.R.string.cancel, null) .show() } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index be70309ac..1b7812342 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -779,6 +779,16 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) private fun addControlsSettings(sl: ArrayList) { settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_controls)) sl.apply { + add( + RunnableSetting( + R.string.controller_auto_map, + R.string.controller_auto_map_description, + true, + R.drawable.ic_controller, + { settingsAdapter.onClickAutoMap() }, + onLongClick = { settingsAdapter.onLongClickAutoMap() } + ) + ) add(HeaderSetting(R.string.generic_buttons)) Settings.buttonKeys.forEachIndexed { i: Int, key: String -> val button = getInputObject(key) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt index e3119e60d..d75368598 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt @@ -67,7 +67,10 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA } override fun onLongClick(clicked: View): Boolean { - // no-op - return true + if (!setting.isEditable) { + adapter.onClickDisabledSetting(true) + return true + } + return setting.onLongClick?.invoke() ?: true } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/AutoMapDialogFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/AutoMapDialogFragment.kt new file mode 100644 index 000000000..569a0caca --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/AutoMapDialogFragment.kt @@ -0,0 +1,152 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.fragments + +import android.os.Bundle +import android.view.InputDevice +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.citra.citra_emu.R +import org.citra.citra_emu.databinding.DialogAutoMapBinding +import org.citra.citra_emu.features.settings.model.view.InputBindingSetting +import org.citra.citra_emu.utils.Log + +/** + * Captures a single button press to detect controller layout (Xbox vs Nintendo) + * and d-pad type (axis vs button), then applies the appropriate bindings. + */ +class AutoMapDialogFragment : BottomSheetDialogFragment() { + private var _binding: DialogAutoMapBinding? = null + private val binding get() = _binding!! + + private var onComplete: (() -> Unit)? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = DialogAutoMapBinding.inflate(inflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + BottomSheetBehavior.from(view.parent as View).state = + BottomSheetBehavior.STATE_EXPANDED + + isCancelable = false + view.requestFocus() + view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() } + + binding.textTitle.setText(R.string.controller_auto_map) + binding.textMessage.setText(R.string.auto_map_prompt) + + binding.imageFaceButtons.setImageResource(R.drawable.automap_face_buttons) + + dialog?.setOnKeyListener { _, _, event -> onKeyEvent(event) } + + binding.buttonCancel.setOnClickListener { + dismiss() + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun onKeyEvent(event: KeyEvent): Boolean { + if (event.action != KeyEvent.ACTION_UP) return false + + val keyCode = event.keyCode + val device = event.device + + // Check if this is a Nintendo Switch Joy-Con (not Pro Controller). + // Joy-Cons have unique quirks: split devices, non-standard D-pad scan codes, + // partial A/B swap but no X/Y swap from Android's evdev layer. + val isJoyCon = InputBindingSetting.isJoyCon(device) + + if (isJoyCon) { + Log.info("[AutoMap] Detected Joy-Con - using Joy-Con mappings") + InputBindingSetting.clearAllBindings() + InputBindingSetting.applyJoyConBindings() + onComplete?.invoke() + dismiss() + return true + } + + // For non-Joy-Con controllers, determine layout from which keycode arrives + // for the east/right position. + // The user is pressing the button in the "A" (east/right) position on the 3DS diamond. + // Xbox layout: east position sends KEYCODE_BUTTON_B (97) + // Nintendo layout: east position sends KEYCODE_BUTTON_A (96) + val isNintendoLayout = when (keyCode) { + KeyEvent.KEYCODE_BUTTON_A -> true + KeyEvent.KEYCODE_BUTTON_B -> false + else -> { + // Unrecognized button - ignore and wait for a valid press + Log.warning("[AutoMap] Ignoring unrecognized keycode $keyCode, waiting for A or B") + return true + } + } + + val layoutName = if (isNintendoLayout) "Nintendo" else "Xbox" + Log.info("[AutoMap] Detected $layoutName layout (keyCode=$keyCode)") + + val useAxisDpad = detectDpadType(device) + + val dpadName = if (useAxisDpad) "axis" else "button" + Log.info("[AutoMap] Detected $dpadName d-pad (device=${device?.name})") + + InputBindingSetting.clearAllBindings() + InputBindingSetting.applyAutoMapBindings(isNintendoLayout, useAxisDpad) + + onComplete?.invoke() + dismiss() + return true + } + + companion object { + const val TAG = "AutoMapDialogFragment" + + fun newInstance( + onComplete: () -> Unit + ): AutoMapDialogFragment { + val dialog = AutoMapDialogFragment() + dialog.onComplete = onComplete + return dialog + } + + /** + * Returns true for axis d-pad (HAT_X/HAT_Y), false for button d-pad (DPAD_UP/DOWN/LEFT/RIGHT). + * Prefers axis when both are present. Defaults to axis if detection fails. + */ + private fun detectDpadType(device: InputDevice?): Boolean { + if (device == null) return true + + val hasAxisDpad = device.motionRanges.any { + it.axis == MotionEvent.AXIS_HAT_X || it.axis == MotionEvent.AXIS_HAT_Y + } + + if (hasAxisDpad) return true + + val dpadKeyCodes = intArrayOf( + KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.KEYCODE_DPAD_DOWN, + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.KEYCODE_DPAD_RIGHT + ) + val hasButtonDpad = device.hasKeys(*dpadKeyCodes).any { it } + + return !hasButtonDpad + } + } +} diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/automap_face_buttons.png b/src/android/app/src/main/res/drawable-xxxhdpi/automap_face_buttons.png new file mode 100644 index 0000000000000000000000000000000000000000..60f7c2badd1f74dfac20360111ccb8616d0fd3ec GIT binary patch literal 33023 zcmXtg2RzmP_y60TaW6t;bT1j1A!KB|MTC%|%p_!IlTF?T$yO+{BztF5DkGy@Bb(5* z_x69?&-eH5@#xWm-1qx6&Uv15p67W)+}2h*Lw%kaf}k@u)nPpdBKk}COGysClNT@a z6Z}Kus;8z574@(!f-gv2m2cjm1b?1UK0+YqB6Jg0y5sd`Y0UfCo!`d_C%cD#9Fpw0 z#cvvtk>ax6b1vsqt_YlkOR1TYiNgv$1>P^9EFzPtutG;o_Sg9pa3){w|Hl%L_%A=n z-}wHgvuL67uTn`FUUhYi^&IZ1kw;N~Pv;i*OxoRCpTsCX{+sALUqAoGmznfy%Ifvw zt%37xqO=AE?ww^jB!Ob^9n`ua|J*Rbs@xu!pr-;8LB!C-MSSsk(_Jdc54WlkK zS~g5&q@+Muclq8;$85BzG@ft}zo| zN5Z6f?S_U?I5Gp7Ly%4H_I2(6Mt{e9%rxj~lIHvTRwQVLUiahUfB*hTW^P`&yHz79 zo~es3~lur-V(ccO(= zKr(DZT{J2AVd;1JA@^eAYy;(4Wme;iW2TPn6heOKh_2lbGu_%{NG|Cx^61XJdz>qE zE1rlAHAH)P^6;yX5=;u6LH2AeC;Bu?bSL#?tG)K0e^_|iIixj(0?uiFSUQDx$wP0b zO*+%OYN~wIVIFLeJDc@-a}bFXCJXP$_jQAvN*k!zMSxmd1=W1Tp03^yJC?&kmziQ@~zkcOnwGSFV)5Vv73G zLl*csFj-+PeXC)f9d^?)F&T^ZpSKXi)rgkA+W5|!NlQse?8!BKu9@qGw~(LVSDGM2 zb8S^s)$$2%*3PrSU4@UpJy@dFwISBum3G_$r0^CI)b-)x$HVdoBi2qb@DmG_6%}`+ zV_b=`!sLO-gtuXFg*yNIFC0z*4^~|k9ll|x>$^MV>E7Y;m!$dfWR~%_C!wgOm@f}E z5yaENQZ?GQtI+1l!&0$@%inkp)5Es4e3V)9P7hC+RBJ2p@*d|;)UtMxgB@j6fGuW2 zla5%K@2i4^s~+s1QiXduJ{S4^{M-Fn;JD|Dp`>$c>e=+Xyu3JMPXeMPyPj5Rh(Y_k zK;{Pr2mKd3S-&1I?xq;&+S=R85utHJ%_y**E=u&SQoHr^`d+DjxJ99Y3wTB}IAl`v zr{_f(VghF#Ts`KA-)fF~@?_fd=`~LiS77PvzF95a@oRqOpNe4YS>S=uz4xz+p zXI}jHp^<_5O3e(G*Am$=6V5e#3b9xXhCvt8G;))gj(0|%NmB_J8mAa3MSgi`1xwIC z&-c^A=2&34GVtc2EpqV}XW77oOx7Q+e3pRgTzf(-d zjP+i6Sm`b2*pRa?3pat}LUask6mnvUl&^O+f|* zndE`@o61Xqw{)}ydRRrx6+ccWvEr%dt;&Pn7X14e7$t^p2(ceMDV@NuZU)ncciIu~ zuGDbJ7%MXXZul)4>U#0zVLXD2$|vEx_+HOdf>*ObawJeaPvZ7DY?UT|>_=dX-&O@* zET0K@`pElZY3UVONQxMooDTAy4O-cfrLY{EYVF!!K)*vhPLbqEkM3e{){gdvY3L!u9WJi|L((G#{t5M_qU?kBv!6=E=fXS*B zEO(SqxJz(V;_B6I(m>0jo|ec9*bJuFj;2*ly048dz*YogJ(k`pw`&q99;Anzm4BtD zJ=*B!n+pzL6u5el6rx`0a3~0|E_yQG6{7>VR!?{;`$UtaHy20jstzndq6oHCn8S@) z-?Nc82m+rZndA`fWV|5d_$w#D6`Wq@6)ZJz^P>N}xjhRos`?Vkju=01o;1*czx8?~ z0-=KtG5_}{Vua6WA^;{;rhKJG{Afs0{UoxNExtE3ZmK+|jf>2hT|U&IB>ME}6L=3DCbT-}DwcS(%3`R* zLT{i#kax*!k=`)iMaK*a$FsGTB!C3A`p0z5h7TT$UWeAsG`T*~FfbS?u?X>KJ0nGE z@cZz=cNaG4)1%F9d~wrt+fk44F>NgN-@-!kW{+%fVQHzZ*D{eNzs0wgn3Z^)>%*yY z$Z-r3h&1XxcyNIzki2QLq(8|snpi*x&UujJ&dxOHKUC!~6H3Kc==5yn0v!~XXPe<4 zri}49TJMOq&h)#RZ~7v{nh_n8XX|{pTz#R`@YX`z%A#+-JNpwmySaeV-2hM0ke2#` z46)lhx%JJfs2464j~hoC{SK!f=jRjGd^8OV46E#o<|kW1OfaMv?A8_y)GJC`2ORE9 zhl?U*oK)B?1A~LZiPNI`wN{$CJX?8xkwvp#eSJy;`8rZHm)y8pxF&8i}i*4}{_|D`8s&&3~iiFztAL`zG1PFQc?m6hr>UW7cG9- zt1Y9^N^%@5EN0JKhVYiCIQ!A(`r6mNq{?}hUCI?)Fu8*+y_|aMX>sa4@Z?P`6%`dq zbiN`EvqR?48cMZOXjHDZ7-61alXgR-Ve8e-bd*RzWY(VPo3^PE z&Ww4@L{kk&&{ShjDbRfhpB_|`AJesJu&_M-8?{v$GZ0}@GGTNO;xOv9KbM^UoSsuy zyvFH8i#aMu=mB$yVAK&?KeKZ0^D`HI@n$~LyJv8=L2wR>Rp*J92BGkgkx`2ldPo3! zJ`aCz4Onn)6jhogI&^oZZ9KfYS&2E%g2<4-=`V z#hn=BZFV+^|G_}!bMaIYI>?Q%h#ncdAaBEXE#JkgU_`tI*LBni&NeG6>tRm9)0By8 z2aM>cci|lW#(nUgsBrGz@yCD7Mb%fEP8&D+HAOz8<;^|I^5A27)pRr`LXu}o>}4G5 zFe(XS(ca_U^F|RcanvHo-4VVvQMxr*QUNXFlvI(N1O>JBK=A0-Rz}i}f24gEdf)X| zIp}_j8!SSwCATs^9sMW@i>8kg2xSF_&VW$?T=<;Afwiwa_1XyVUwGEsq%)3k;l+f zjgy39Dcc;!_sj=tP;K&IhLt6mUPoMnz^+5(3 zvo5PvwLEeb&(gG~`f&DJToLXZ4xxN^o6-@)!oVy(qs6OS*wnn60{4m}#A?;-M_?QZ~v;2;F;%M|VDg;rF0T7maOZO3#*uWJsPoCue#`E-Z?Rdx-GYLCf#UkC z#=YsNGz-=Y4zAqLk&*k}zYXqP(*JPh`_X9bhk^przn+wL?`te;eSP-p*RKi@i0N2# zCwaIkn#I)XoqMkmMQst@CnJ}gl5`=%cZJF(E$O@C!!dc@%F4$dwDILe^es7-Z{QihRxOVo{uQ)Z1#e`nJIT4gHEly%tvB|Zh zzXhD_i-Lj);a2u6xj4OPfQq&t$^HIFY;oIPoIFIyxcqT+srTmNzT{{%Ce?RmE=ckf z*Kb5VVSS$8P-241Ew${B8OYNr7wo(|SEYO@OV>Z#JU2UG!d|c=CxPU+XOaab?QmIJ zD*sufbAA8OXqn(o&f7T&&SUO4G85eL@2^}iDG0O8AB*ZPzxhtV1w4gdEqau?uQ>BO zctiKnCe9fyP6C^{LFrOAaqfsGH}th*9Y3gja&g!wWNo+U)WoF0r;-DlgN#rx(kx0_ zYtxsdh>cT>?YO2VmYM@DPVB1NLhtmo^epZ5A~v zb$EuD8I+f7)cKH>ivVN4?%|qFa=lDE$a>x1%p7dvD*<5Z4oGy{-H@J3&|jhvu4#5- z%b)e_wEbiG_*xq{OH!;UdA%+PEvLo$}AqPNATNv#u~{o9IH1?5f0%&d!$3em?$7 zz^J;?#jRs((|C3hnj#UK2}8lVpkBIrM0l*YmVTVx#_^oIvk8kx1_p_E_pk03QE%rulP?4uxTMv`KrP1@iJZMvQmi} zx%JF9XvssIaVDN*ZYIfyx-zAhR(z7M$$9YIjOQin9YOKhGT$>~Jsqhbji(|@v-5LNtygPdWj%GOjUBwwQ5-(gX6SwZ6y(%e* z^-2j73cc7_WmagyYBI|M2)HGFHwvk6!b}d^moY7hyH1lmhzKP zFXoiLvu_VwKq%+t<|@F^wYNtwcGk(*j0L>KU};H-327OH{s*Rlf=GFx24(mu8wJm; z6!T2io@*Y!LgxqaSZOz8Apw8jGJs04XcA|l=D!rfc1_RJdoJ+1T-RD<*HcjKb;@8jJbZ{r0)I-OTYi7M=Sj;d+PuksM;`NEJ_EuY z-p=eWZMLCAyd%EJP#DxeMudoVjv5#<3T^!}BVB`=!PMH;GYT>9O1aYWr`A>9)OUI* zDikFr5^hDP2x$I@ccQSw=qWx*^7Hda?^LYI97xy6+I4sK^*0Q!atTq{i zzN!^+Ni&Jtpwct@eauXApBm@#tyHluuk<$Na~h{OIXYzs4qn5sHD09s)Xq*IEpPZ^ zW_CeAiR}Q-7YVI}Sl{0-N|y+f4u=YCV`ta4zd0X%Q1;2z)m2bPxhGw=R3yyEz~BgPc3J>ZNbTPe(E{R@-k?OX^wyvxid8tN*#Y^oDL(cg zFzm$xd@;Slczx=`?u)rA{vljV#OKN!F7W7NusJ$7obA;S4~n2PF!(|+$>U|GfR6%^t*650l;K5B^8Z1@3`e;q zOq!P0r$R5E6L7GYuZ-K~2=IVOV`jYk&$NkxIG`FNHSsLo^HC?8StmFc(Q@NS)8}fD zS-k+Po`bvtSS-<&Sg25L4p)qHYpGzTU0?HUK2psjK`Peg7;nQ-S$KBL$Z(JV8Xau` z<2m%HfgkkqpXVlIJTD$llkOk$oEW=i^@FL?-Q?$&{cfp=JM&$sjQ><-UvcGbU6*Jv z6d`<64Nd}<6lB4=F%`}^`1q3#tEgcMLf}~if~BBb@+Z!1oWlkuykZtK zL=_ema*N2O)D=aMkkh`660Z9uro}j*`EG4*Cf2h@e%H9ug-u758U3!d*63)KSAnXw zsPl91wj$;Fi~qmG9`~{Nqn?+=2uQ@<0b3=lav0^QUlNExLJ7?x_qj=pB<>y%51=1Ji8In(-!S1d zAnVOrmZtMQx*ghL{*qbfJ`>2&j$$h*7KdHZK@#~TmH;iVy%L;}dVC4k)!BQ-=o66c z$T@9Ur16Y_DkUN^GSacB$Azta_{l|~l zqmG}2EJmwvrs4~AF~HzmCzq=yn4i7aVZmgnz;bELWifI}A-8A$tjLN;hu^-ig9312? z8Iig8I#Gv_ieDDt?0?CW`>}mEI2$3OY8{ zU0Is{LfSQS1nkH^Ctr?68{@+4iW?+Z>B7-+Omij=+{Zme9jJ;K+v|LbRHK+LDZfK` zo7C}y23A!`)-UG0CT=k=$?=UuQYN#yo)T)4+}LBf$#9c~Ijhd3bA$?adOMIB=WaXQ z9x>?IxXI2tfLW4%)C^e1l&f}0spDSlo!5T8w>>Wh2$_SCN;^$I_1D$YkN!(c`+b^7 z_LKmzF&RSNPC_qdPM}S$6ybx5@Di?nZ;2;rYCAe`D|y{IwKt9 zz|RM?WxF}VAJblo87OYJgKd4M65fBbzZsuo0I}KHCB)NXv%GNiPzz_n~U(u?(UUuO zR%ZY*fb1^33>VC;HDL4o)unVRD=Q;`X9y*ruXF+q^u<$eJ>*dt$+L|#Dt{a%Dm)FZNdLTs`O6I5ioN+^*e= zab0pkC@I+8UJ|@O))=>KY++!KtMytkLXE>47N^Z#5D-TX7ZjZIgVeSHBrVq}0fWd5 z*@N0!5`&drYbFVr&kQi#txKhVdf)42&t|DcF__YLn;9jA0aU3T0~K5yaB`0RL3eg= zee8fTl7gvdZBGT_)FlqtgAOFhz|FyJ!LX2!kQe?_HM!WLu1pLG&eYcd z%SLBeEftU8ehWZXeb` zHDWvI(jbOzS?J3i10|S1YDdr{mysw+N?u&&;fe3>`4#8FLbf^a4_lUViJPu94BAZ7L&H53riuAoI6MdwfIX z`!N+-X+Xkj;a#{4aZunc{|Iv1z(BS7&r{8k^74V|qhV}r{R9Yd5+FhFAEtz;npC!u z?_5?*HubR`23eQEh$XF znJQu6D`W4}K{hkK9JLRK_)S$`-$Q0-@7pSyU*#yuWAb=+;+~jvS{ybGd7}{E|6?_a zw)gj8r6A2u*;P3^5IWDDvHe&ODN?@{d;-F~-50$#5exA%q?6W1G!+)!LbvBRkIppmsvYQ{zaP0WnB zfx+4IfRp{ZJ|Yf3@vk4;zu&&KIM9}4F!V7Bv9XglFZ4aW*U%sE1J6Q`n~)ciT#tDV zFHBlj)JZ7h0MV_i%k-08NJJ!gk;=?_*N+QZQ$#}`aywp}n|WMDlA32*V%5n!2k1Ev z+eCh097qE@j_F!7E`U`Ez|T++wd1$jluG~ctlzj|SyO4pMyk#bEb;E0J1=qa6|QH{ zx`0!z_j1uQF!Ni(ueDG-K)gODl)cqnt({AH5DbVP39b0ZtgSswN({xB z^Ron~PSwcWs4PDsgGoW@cq^J+cJQq7%j8vkU8WECI(sLl@JpJgcdW_(Uki}*NFr@g z3e)a^^%+`adodS)Z;s?Z^LKZ5H(|2$F(DTE3V^-}0z2^jPJnSj{`&0o+Hc0rvWtlt z-q$bDyu}|T>L~+wWRBk!!QMBxch9qO;u@>Kd-CA3#s9VWYW~+n)6k=Z8Xr7(&}o;P zx=QF2h1J#7#iG_@l+AEm)bR;Vud!Gqi2&TJe`hh-;(4fZ*-_w{IO<}MsP*LoSY)(Sg%Y^W`EZY%%XiW!P#jEvjV1X1M;~H8a{EwJ< zbKqS}JN0BlgdhcK-L^?wbjJ$x>Bb{&WH*rove4KLpzj*K@016dJze%>{$GPgbF|ti zQyYFjXdW?Q{U46*=ry^DR`G%lnvMjV9?L@JsT0=>*D=@#r25P`v79f2CKuI2ho!d= zbCkR$JRyw83mTb@I#iuTxx2f^`<-bsl&b0I+DMy`PUgby+8btCByD!v2=xS1ZU9v3=wkCNE3PEcB>D4Zo+1E-$FUa zt_lguQYvxqJ%>L20^rQNp~!Yb9ME-LG%GQ`_00`MNxAAi4hto;8@IQ&n^=^Om>^@&5IMR5 zOF~C{=cBLBgToR7G~?MfhJriXQ2ml8T~ZT*grY<$tE!4NWoax^r>7L}zPx|`{$*oa z!v%;3f>u9tdxXPVp{R$#^cLmH0DI0Be*OBjB#Tdnu@5xpE%q^E`s0&zD`eoMAPcuB zlmhm0w$QCj_AJ_r3bL49B1yaUiVupoOPP358i=)O&0z>J_{ zBn!H?hGxzpZP|cwO47)^9Ewsl_}zj?qtFJh1Y&a;{hLHlk@6j595ob-z&-}_#7EYQ zesBVGG2+`<uAC@Ha5fhi4&0mCK1wUh4fY7ai)^{ZWVe^~;S9dz}m-A@qpR z;#!*}|HHsnKLPh;N82Wvlc21dP>gwqAWYcv#E?Gf3MmANJCtrvQI*}j^r7Ez@r$Pn zZddJuA!#IqJBtZ>od|l3?u*mAulEBm9k&K5WMpL0g62cF5E@shD#x+=pYR8j$_O9& z3|XKO^i~Pvppe_A#HF7&7zUj~Y#RIsn?(jiLu*&CU)@cbD$B~A`UW&UF12xVysoEF z%Ld&bYTDN%W9Zw|`|$u|wtE>978|gj=jcaZjyL>rIag%@7)npe1vq8`VNuVgU z0ItB#&)&0aI>gIDy;RUo%tIRVr+WsK?#+Qjr|P@yngme@l`>+7EKrVz=G5e>y*-IV zzHZ7}K@bYK3&m)aUtmFjCGwsc`bwgRf;_hUiH?qYBNuVWkU2wNII?#cI!Z^blLpeK zy?(9f?^eQnp~&r>L6y@42k7RK1P1S5K}A(3&Ll44Z2_+TXvCFj4#T!`sN)PWB@e-$At+XD;fMldz$Im4VRie8Ds| z6qFD%bi@cHVW;gtgcQnFi$&pg zs?XK}aJ3(p?7Tc4V#rL#>83B_*6qo^<3Wqk)!gehH-~^qp zhLj8@BZh*HADeRde*7ewgs_+AfL`HZ5b`?0M%C4IQyjXTP`(zm!dcVM&>)$5D}w@J zftp1Z()OB9xNgFyH`>*56Jmeka2-c<2{b?_;075IL%IN&x-io`P!0e2=#%kP!CDku zI9~Z(FE=*r1)>r(S}Nj7G^x)&XQ_-(%&dE5PXrz4_c8tTk~0;~eM1YCqpwh35^Wz) zqlC1ek~_Ky;ZL4CiS#!t0XLRa@J|I=)0)I*-@XXd#~|~Gt@lsVx%KFvc(e;01P2xm zWL$(Q82XCYI*i6fM(XTr?D*vH{V%4(y9Q^0M=Yhan9XGq z2!$H5Mc?4ul+}?0OzSPhcHpy@-5jF7m#Gmq`|ABT1kuNT#*?*}It%^8-kz2QBTNza z@wi71G$V8!9G1mk!5`ghh`Rk~)u;OgzpWr=v9-|GZ@992K|PmDAY;0L;v&mg;{2RP z7pFRmhs^K0y1G)#?)%EE(zwcTD8wU0n5+=5nhwszsb%_5R8-V)hM6GqYZxAYCPf>y zUYRWFrYc;VJ_TB_lOAvjOc8L8VtXc0b5Jwm=Vx&lpG(M~q%Sk;SN-P~wa1z3fEbD> z$tqn7j;amnx*&}0DR($r1fspO4nOm9b8}l=-mEWr(_&5(Q_iwOB_&l=9+eYHWgh_C z2NvP2Nt&?>GU~x|06He6d*%QNoIaOZlApH6v05c_TZYsM-Bb%v|Qyo9E2fI<`}y^QmJyf{w|0JyNtYso({{Y3nH;z`e@A^=wch zJ!H_V5_fLcL34j6N8$ae-TXIl%OII}SP)01Ae*E+){SLJ+i>I60FLw_QZKZagpM)my6};-OkTQ}8 zZ#kE;%u2|EROOXva0gD!LJ`{2l;g;xG_f8#6c(W-++GY)L=mV0k}#h+c5Gd4QPE1Q zKgIVj!g{fFij-H*)$Lf$UlK7(z-E0?MnLB?_Sf&*5N}&=6-2o&-+Zu0> zzYIL4X7Nj1xUU z7B~-*)6MH@TbiQhqlJkBzc(`yM^S@-c3mj6rvAeR{`uW>SJRXTbvs8#Qx(`+aebFh z<19lTD3(kNA2#IwM8`pp<>Aiq)gpXPaM8_Zu6NE+6%h!hI=|UH19u~AoQ*%=8$N+v z%-JKfZ$pkMHtsj##p5h{x9}x$GArE!+--31O{250*%F{k>FC*{*Oaga!n+Ak7tlAM zy?A_`KKYsRcV5ZQk6o3J2$j&1ZFYI1tBe$5rqPfnPeV{d;AuU}@sn9kCmJ^R7OJlt zu3(48``nM)3JMCExU#nR&Bj?JY;J$6TBd*|y(TlB>T1R^I{=AoBzUvJ+&_2MyD8P6 z>uao=fG091UgPc33sOe%J(h;9*ljKJxpLXUfX(I*2R0tQi8`tA ztUZ*A+1k8`eTwQM50u0ns8jYWNcXq{PEzonYwck(awEHw$QMtWpmkvGt;r;b)&-%d@IfH~9@9R!Yd4 z3#6*V&|4MUP7I|<^jDT~{l)F_&;GUWBg{i;NCv%r4LhO%$DpP`5N#Sz6XMD4vtcZ5 zSWZGm-^jd_bvo@)Sb;*LbT2?}a3EE=QDr8AXSwIIcW@7OV>au`r9o$Un(V59wBEZk zFthYlPIz&EDm+umE}*qYW$_><;a+t`Mg5y@+>d``H{kn%%)Yc}9f)Ix-<=XF_Vn<` z=$5y>YhD4wb{8ONvKn>)cXc?r$|19FaBxSr%9PuVkp-wfltDs31W7aRJ$!7EJ)#!Y zHgy$lfBn5g84Y5{t_ltoaqM6vQUr?7PEa-{RCYfq(@VJkKDS6El)t%TSEYGyU%K+c z2iv~EPY-IP%b&4;Dyd0bBx0KD!Z`@aS$*_utgP@e^j7dfao?({EAaHRJX=px-+ps> z@$kJk)HHFMD9=87Xz2>j4zKrCdIOE(4anJ%o5ARe(NJXd1kEM^8F;f?{^&m*yA7GU zfD}?1$o#5!L3YHOUp+jVAvT)5#of#Z0EYhHxb|@Kdw)KB%nT79@7EXFT#B%H0+hm< zpzgCz2B*P}^&v&?D^mXr(}I%$vh0oyRvZt090O4B5&@!X2)&My+{7XXjb*9f-IW2+ zK^hr!_k31Yy?XH>4&`#w$jGRoDWlqq8gwgP3%Hw{3PH3Kd-YL2Q&>QDr(*mjbt0?J z#z^A4pLB!CWlQ-H2iHuH!91%0UQnPw_@Jwi(J(!Fz2?MA`0lI5C}d(2NTnR$EqQ_z z7?h)aCIc0myB&<%wW1vlO9xQvyv^bzMiYO|$9DYuPpSo=k(apnliX0pK_F!QSmJy- zsbAqIzdCBb8BnC;?aN^~dXwiZXrVylx`)Z`EC_~IffhM22CU$+)<6bHU9RR1yosFa$1NC@oJ53MAQw=m&LvxS2F%i|F*? zTiL)`67;kuBTNew%+I5RY@R$xoAl(kdp@gg!4@i^gkZDS-;Zb~H^2X!h`y1{q9kHx zU(E4qXo!_3%g&1tJvf2VB?`<0l`l}A(FpippuKaD4WadMN&evJ+*_rP0^iNOaRXP3 zFmG0f&!*v5pJ3#0q{@##Be+DtSn;4$fpS6h*hbn)2VYeuj?DF5Eq(ogVWg%&O602{ z50J9GFD^c9Ag~UAy2wZOIqltL>lEY8G6EHE?Bk8M<69{myVF-*Slyy@i2+T!zO;7L z)H#T#x#E@-$M^T5Qdh5P!R-TLkzWi~)5nK~_S~CPN~ReiU^5}DK-+NN z%(QXHHP%SO`!WRkut$}Se_&Bwi!m=WBP9+RK)Hm1mD^&nP>>gg!_F_u@mg2wc8TtL zh|TTd_$z~&a78o{^Rc)%7VsTsi$KPkrg(9U0a}{zOj3UN9TW8I^LYGn5uT&k?{Mdn zyx+TjbHfY~?et+NGW`TJ+GFJp6`asVrUKvA)s6e|=$Hl#`jEj6qZyp#cxydV(~SSV zM?u_t3#w@W%3bGk&t1@1x*2`jhCYdIeczN@JeW9jXO|VD$}B!G2CxsAyYGg{pKya> z;Tm)#fF=;5FZX{vn^M_(GSQ63h;*5qezDR~m~te7p31@az4k^Vduw0U9{Kns$R`+Ant# zmfVEaP7w$Nh}x19(2b_XZ20PnV@Ut_A1=4VUO!M@2XIm(@HK`Vf3g;?aO)=j@d>k- z0axgxUxiUIC{w5)>O9veUO0z{gnSaHKiv5*)fIilnZ?XZev9M!0?745Vtf4~6nP+x z&q0>sM;(2A{dCmy59LuGMzk&^(LFOi_kN%Q+tom!-Ap>dXjsT|6B3GFkI-Ojz!6MG z@MiQj#89Hp+zeDH^1dFCNJ;MwnYLMof}_2CA;Fde0h_3cX}5=NANA-_0`V-re%?Z2 zVq&Z2=%O^8v0I;NU4od%Xy>z690oOl=c5NbciqRBfg(K?p?>Pkgq~`l;1tZ)$#lG* zG`AnD%M=Erq3m-~P5aSt{uxVQOJp!N&fNkiXR-1`Ca{^Y!={lxMqloA4^Fe!FAVpnAWl%k;%g zQf8^7?Tzg~9-yk3mtKQ@2IZ9=0GK7HzTKmq+kD2jTt*!^-H$~@J@3;g$wQ?5kGzE9 zwQo~#!JuSqY4B??rEvwNcs0t!@b1I8m-sq@sxue(Ld3Z(|I$yNwEj4^ox$*1jc=u$ zRec9ziDKnn&4bk|R5LFOp-WuS6o1%+>na(mlxk1n5t zRtP7sl;`-mbKPay+zg^%0qp3_yoN4U6}u--c$Sw%b`4XczyuL69{Rcu3t_Vh*|8qQ zptbmjr!&&cBtJjX|9-%7C2r$yW5SHI^ZFN-@X^sxc^1PC7n_z~vhUe3GC#VX)*Dk1 zHCMvFmhE-FLW*kb!@XCho_jm*O@#}9+R(sP-xvgIZf(%ZiKZ#%jYK^pU&{TuQYuP8 z97Ks`Px)0uyB~W;uhlP*o)(=m}7?o#3%QFLovKJG?^jm3LnKDJn$@B^CD;LMqs8iFYanNm3QV0)3 z$K@ncJe~WYD801Yc3Om4y5LxYv0}YZDs&>ksLa$&p_as6gpc9Ij^7x%a9+}$mBVkV zuV6;H!H1at5}R`~(L>O{3AVP`eSx^~&ke~Rk8#kHCp`5i6=LSa%Kdwv{U;M>gl{g? z?XN7h5_)aC8D7e#pcnkiNbCkYSto4lvzTiIx_Yghesd;aM`?CNH`zUgEF3Wo+|%fr z`~gdzlt~kJ3>wOYMoR6`$^>NvExa@kz6T71pp!q-`Om{rfZQaTRvAh4J@{PA^cqx0 z@sN2_Vg5#tZJO6g?fiWplokv5vi82+8rH zumkjT|KY=R!Mg8@0j zpv-o%n4h_^WWQs@7b?vZS;LMOfl0v%r5#2j+R6&ASF<3LdA9ukJHLeubHQ>pfJ3$V zd^*f>{}7aE;gET(RCd(O%ZaT(ymY^=C>}S(jBN$~69udt2S`Gr38uTcOo01n2QqTI zmbcXgb}$T{@76-b!Za$lB#+G^Ar4a1)zW)(jHQIs4%o9$l$eE@nHYUA7-p9HIZxE@ zGP`Zs-`wx*2M0cL@3Uj{97g~C`%{iV_QEXd!A+Rp8rUM;SbaSaN|4HJ5fmnmo*IS$ zaoXeV^D}eFW`kv{^iUo1l1{ozr66b?36~)1^w;;yt-&l7^mI~Mnx)mdU_m3Y2i>Hd>H_4p(Z5q8N-0?%2ZI8tkl>2fvBUEj~?wR9M)fU z7^}NV3FZHlOVnZOXaVzzo#H*xZDkIl1A-24C=gQI2h$TETz7rFeFhzr#Rptc1JJQV z4TVNG;4+8TJ#w43e1E;yEu*xySUsb`AE5W%H=w*w5z}rE=CKyT39uM!{t6Ib8EDkv zZf1QR$pP3l;&ZY;uZ4kQHMQA0-T`dCcMY`VHy?-OJxe^P<)r!S5W2bl#g6qW5hDE~ z+_j!IKpSKT7Cl@`T$rHlEgii5^+@4$Ru# z<3Wit$$~}<&j3y)oETqMh0Tqa2_4)&!^4a2%#ub)s!=Q=+M#V0e7WOX?m}FYKuiI8 zW=Wi=E0;t0c4gHt(vyKC?du(!#i@D2V6&{`^#}hxYU}GWR#aCHWXE2AvR%=q>+{oI zZsvu!gb6N4ifu8D7|QlM{y86JMCn4=JU_3RM2mBG0k7S7A>IQNBRg9ST-b!f#1?{} zYwHnm-OTMjY2fBhg<_yaH1Znr7$_4=5w!?*2XQ>9A0-L7nS5L<&y58=sLl&&$^GF2 zNG<-wT9QfK7;5MV9`&e(3fglB-AsRKZVx`H=L&PoK8yldDj~Ko02Q2MY4YbWd3m1- zP%6aqw)Mq4w*a?voiO=YEb%-&J>6~8p*mMW3txJS!Ak#4v8-?ci_P%h@ z(6}DhRJ?MUB!YnWtLwFt2 z>~hHlg9i{V6#&$$`)dKe1XEf<`Q_z|JA)Rt4bDp04^8q#PFuC1r5E0YBmh*=Vb5St zg(YMdB4`LWVym1*;m1dUaYnR%6>v3iBGa}f4~-jfXFWg0_s># zJwPuoeREA#{>|7-QpS`sYpm>wba$EHiPQ)UELiOzdodIhjMNLI31I-&Qei(_29v7{ zAc)syw-iM^+fxhH=k=#2hta^RfAXsFM=H|;0jf1%{%H<1Ls4m{tl9aS377eEgOO+b zhEgD>Y@=5&u5`FicX&G%85$Z=x5E`JdoUL!Fid#OoS6r*9M2tzKMncCO@Gtuh(To| z?`O-e?`Fb64AI?yIfxG!fMOCzuj_nUsd$G>6{f%|2_rZ=v++9r#`tn{ssW^ zvsc1t-XbOMW`4RynqNCwZ4I@}-$oIYXe!ynB(;_eDO-duBpCm0!MNx0~)vkWvt5Lr3gdq>LSq_Vjn?HE9$$mlwg}C7urApy0n-Y z(d44O+{W2*Spx$DFOcz*`;4%^MAUVe3_p8(`h@q1Jri9}us5%8I(!q%w#kS9-Pl_~ zlBdbdV4S*3i`E4Wcr`woUQ^4@PoA*y^m9xq_V)I7R_Zr1fRM1j;Nuq1s;ZI2 zicg`@E1)^k11i-ul!p{5&gpvsA12iF$Ll3GH4Unnj#wORTXV(%OIqGk=E)W>FGsAe zqn+LdrvDJP%#9j7)l5QvM=kpNmkTjV=~$GDs9CSC^{6C=_iWq>L5C_GGqZT*xkYc< zRl662mtwF($2f)4eI2Xy=`TTw7bh3N$omCCG0o=EjxNGA)N7uduoMK{9tHn^C6E|z zfNLDAe?txQ@e~|h&0VwmVW>5b8yV^S;GkeeqXjNscJ6s|jR@EMh+?+?=fDA_z%ar%L9HMt~(JiI_PO#u`*%MVJpro-~t%uPS4lxEu)+hz1*A>_~$W zQIlTa6pS0sJ(JTvR!MNLr}J5nq$u{bs9 zy_e{7j||Q+x9Fk(lS0~tpqMX32x4i)z9A`rK_y|&bLv8zYtH~EI6l zOFs9`x=JWq;jw(b!DD$O>#~F7>n}jE7z`$b2 z6YGPJzcFg(>!M#Se8|tB{sU?%3+%&OrU{_8eFq91tmrd={+LE`bm@%rv&GzGI!Q@M zz0zIPL*tK&P;1K16uR3H8!TI)AuZ-DjH-Yib_O^oW0^Vvh68HmGko9V<+VyVii!L9 z`g((Mv}!ADA{P0VFnFK&7Ly{pCKr2sr!>|Zw4OfCOKa<@mK7Dz5wz4^fGPzty?(-| z4qbSgs^8CBJSx7+mC4lrK78-nEef&Z*KiiSzgD(}KTbIZ1c@Kdk7EtgWzzqKbNUbQ zKkma-*^lORaQJqC-)JHvog+P6^!Kfm5t*!ve_NMQZo(@ea#fJABnv%|?EUk{cq_6M zOimJXjn4Dwp5$uc_5*{1dC4MX*F}}?X&exvzhC=)a*5t*)Y&8%U|vmgbF*U0^Kirq zNM5y_oK{ns%mM-wCfso*+Wz=EN&tb&0N2aH=dm1PhKl?2=;NLBiorW(X0f@N$&dRN z-AXfW5T?Z@K-{ums+GQ!u08X{wdXtF@q$&xr#T;Sek}fOr$8*eIr}}{<(IA+8O<3B zdQaAs#zRMIIo~UZdKC}9PzCMH>~6@>^QOD$0mTvX8__C38W4;R z67QMz&7_KPVF5PC)`JPj;uQQv6WkZ!&r4a4yVEWmx=%zgZAjgUm`0%mJ6naS|A_VW z_h&_=a-I|JQJn;o%OXL*Eq~TYfDg~Qh)NO1Kr(B;ijfI6b9l0AhiqWhg`GeV78=Zz>mY z10npL^psuvM0E+2?ip7fp1Jo*AdOSCO zp0^cbs8)h@`}$YE-uHK>U3z`^cY)rTp063U`Gl#3BfOpBD@hyUnXLA^pJENp+l zBH*JS#QU0Q9Uf#E-`c2)qiRLMC!!C>V8jL_^v9be@;V(Jpp#Nr zPON7)*dwir(Q>zelKgVRjxa?%j)qK;oHRxnkG1@MEEAs}_Rri4O3!GoMr1rl@HbS& zrXYz+NqzBxsV>{+E0GaO_WIWCrSAUXS*2-TRm;%7?X#eo%E(_@Mw7@4wKL#9#KWnO}mH76lP%Uqm3UxxC-Rk4oI>*9`T& zIVXB$r=IkI@lfQz)g&;$n(k{8SsRlzZ_Zv$8mF=0V08Ks~;s z1a@5uo;|t%2rY$MJ&>O*)(5N~7&?4kn^fBn%p^y6~Ux=7=V^1saoRa(8(Vx+>R5t zv!;W9FI@8+63CVL!RqBt-yI0_sBa$9C6>L*aho>};5)O1o231AF1qEBJw^hfgQh5? zIe=!_R#sQJKgFmgMuEvdRS(Wj3xm0|qCIr;CXFWSGg>$wx|g=Lj+)gU%W|M&TxNP@Dup@A5h@ zuvt_RFQ@9dZLZ9^&VDQr(UP4L1@?db<=~k}f@;=jW2I_xCHeRq8(0>l8RZu=cDq#j z%#}Z+hrl(REilW^%R3GgrR?W{0pnD5L7^9Z{AVlOm2>t#;?~T`K`BMcbkF(oJ5|mK zzq;$7FnnO#Yw^6g>Ho4`jOzxQeYFwSZ9JU6v0#ZYGUJ6W*RbmdC}%&%dH$D~|qhPp?E-8f}Nx zdo|{i8aMYAahS+lAG2u7B$1Lk9aaFO;WyIbZt;6c{{dVt_}14uwEo0D%O*WNvN2Ku z>@~UC)e{Y6E;YaxCb5Z7^h%7nFKuO-F9JwW^&?43^BskJEiG0Ew+@mtwojWnj953q zH3mc((Fe7JG)bRLq28;JkND*!g$^pLdp}h66K5QeknA436h$BEM^`(v3%&IG0sC8+<(P>GLG!Ev5ws+wXztxlZaY}uwP{O4rRY%4)2u&O`2GG2<;yIP@GJT)4)xnRpudOwd3x+D?vft< zD4g;&K!c>plAe33Hhnq>IJBnA)4l8i-ghk2x7kIR{e-a7u3@*V26FFujMN$EAxPcu zuOk-oJr-Aa+bLI|hPnUvak55j85`y~fas&vdz)tL=}((-l-)ny7XmCZ*JEV=G71I6 z&_^~eZ=I44R5ugJBv}jWYAx_gLFCr*Y7kHiM1h`Q#%{eZtvr=iPE+sZS@i z7Y5$3Lj^UYP*18ndz<$pY-VKvZR3oZni?(JnXHNI7nCP}61@TqepwRxKH4iFnysm= zt^FR74gwLVka`v~YV0(3Z*yK`?8%bX;ohB@JB6#6Zyrk64qVviHXtdK6cBeUMz3h! zWkv0rOznKPUH92En^d4AV25TAYI`~szBT;Rvc_g1la$>3^HJ!=lKW9MTd#QHBfIuE z%ZP{wJUfbV*I(^DRP^<3Gr5aq!NI{PqI>O)h62}iCzHD(bfd?Tr-LGLXZ$~$0sf3r%9^j>!Q+!u*(2{ zc~gt)c-d31Rj)ulgOe#*s#SuWIa=Ev&H)7At?=shDD~!_yea@k(3c8tq?K65Pa_NF zu6wOMn^5KMgijt2(>6vE!bN}u=dKo8rhoF}NiUkRxlfy5-W7j{0ZbgY;%0U@QEMwJ zi(uq5YuYa&jk^uPve6I#A0T@~YtaZp4eImuu*gsV^S?Uj!`QZby%JORt%WA_n>A_8 zg)7OodDVlG;p6- zn;@jQ@IF_y-u2MUpJUlgo(pXrYjW+BUKD=p-);^Ex6O;?7IL(f{`yyGTpC8eP6iH^ z<%VHW90_O2vz(X-G1EaU&{wlO>IFr#o+Jpa$spfC2r#*OQ+n16^Ef3mtsxtkO^DLM%HHn&4tm+)|Qpmy&PjZD<* z;%zX*tDaAf#-n*66Si{!IaRw9ef6ORqDA}Y+x1^`*6lz50Dj(YqmqMmca@<7sg=bh zQlS$VxQ4~8+?wTYw<9f9&Q?z)P)HAa^A$#Bky2;5uB zY`Y;*r^3#{G7e}2tAd}2EUo=iqOsI!4*TYUdT7f>i?Ia0&&(38I^o1WVbZ+OpHaQb zyUZp7i2#-Nfu4$2VoD8X<2*4i_o>paSYnbpo;X7-^Wv;bYSLbTBDj?8Jjd@($-H)hqlqvSL3ivq1=VJk;^LoCTu?12=lb=Pk9w-!S^mRzQ zUECqcPv)B&KrEbMXGiNN;t=dbFmzTIO^Mk2|Yi53aOPB1aia9MAZG(A-dl0yC zjDyizNr3heP4k@@wtf-Zr{)~e*y;W3E>PNQ0su-2MYTq6)O3*LjG=PQPk_cXE?)>c z)?K$)EVLuh-``&xzi}91EFB#kRbGb4tX$WT-a@$CJK};<-~RX;e`jX?N8%(M)RD9P ze9@+7U8|{U#jc5-@hi}sNmMve`wRs!yO z*`E{w0cxt`HA8m$sXdUNTDL1Z*?U!%wY>yaH$gXd_fXnX{@_9)ua}3g0jsDk(EP4B zu#OroNi@U}uVeFmM%^u$zStl_%o)Te~Oc#p%+l(x{7f#P9+@PAm$3%#?fmpB2a z%z)toiTJ;B(6yrVJ{fbIt?V35aZZa|krB?b@i9n8UiYv>9fmG)Fqkz`)}C_oP1cS4 zp<41aM~F(S<;HWzk%$v{I_u0RR@Z8?LtB4!4^07_oq!xCOvQzL;UY(qPL4G0iSi}1 zx~j#O30TZ8U$RLwBp!G`+V}_w<+otl)04$&Z6Lq;cHsU@h1?sJYEKuJjS|@8js5`v zCz(&3vBjBVmgkJ6$nJ^-*tsYZkoOobX3*i)Wx6+Ch|b^UW$#@+uFd_+jmIPj(NOtf z$QNLnc&2!db==jQQC#n6MAkcYVA=n{=IIWU{`vE#iN=!V{u4jBXx@@g^?yH%rIhCS znL7Nr&%>T%#jSj~qG?9dPBZd~c|CDNf0}~~-KSBe`!hjWB4?tAqdkOrExzvQ#BvNo z@Mi_2h}%q~*M%!GIw)p_`5I40Alod{YxHh`t)MnWzK}>HZFE4sw|DJ$Yn*k5X#e=L z94RP)#U=g@=6l%6H)qA*rasK86V1<_nYmTBvCcXW52Oga3fE{Gchg^}^!Zj%P2S~0 zhP%*j6JiAA)rm8022QBmlc*3wk3wwD{bTK@k0gb9n_iu=ZB)+HxhF5eZP zS-kcg#Ntl8Wr*CGX$wzFA5QATr*z@)%Q zEtLbg1;?;J&c4&sIw{jS%m6W(^$*_L-QAtUg?>9QD^CY4(Gg)(80RU|xH<{3J;6@Q zGPhBh?tY(!K{I*3+F9YErJwP2#bnKhqBsT2&FI4jQ2$;d7a1^^WNWhlu~=eHT96~& z1;)3)b}GW25M;v@4WNV5^?(3_8RM_Fc?byj=OxK6|EOy-552do*)|50)_2hoMQGp* zounP_Mtn=_iCt;6K8Do4S#Q35K#l?;k$^O#X^y_<_u6f~@0^odbt@S#LZ@(M;@V;0 z*cIx(dWtv35@)XUF83h!@)qSZ`>egMI5{~*IhwaQOc@sLV@HZMCkrqIc2lV!T5xRE zF^<}V;NXb!S3$b{d2O$?3WR*At2+AkMvK_*E9XNRD3+P$#KP09CnR)6cF$0@ozQB~ zUg^52Sd={ghOyhXh*PJ($hV0ScD2xI;w1fyDgkL1!wllb!b>y$gz(2{V`q(FL3l`| zrxeLl;MI6p@_Q(2@4g(~>uOsu%xspRbIhiLaO2l7xJE}z`>}N=u3kRpeR$pT=O;bX z&K;fq^~?Q|o|0sVOovZ)no&Jb;v9}A@;So)lCp~9a+nfE4deC^!h7kIQ7!~}>@(aX zrd|gZg$U9tYG6b3B#&iwdjKCQY-eYuxwW;`P%xW`3HaZ!q>bV_(E;lv-KUwPq=mV; zO~3Ls>)}x=$mKnnH&2asAXC~gQQNn&HkuNDgyQ3t7~;V?m9wuE`EtjZ9vh47Lk6aM zZAl--`nM;Jv)Ev)Tk1EW*2e6nw1BvpBfhltDa%{z;Kt#|V&UIe)*O)!$%C2HxxHHB zbwyb~K;ZpLn?*C^U%JjY*Q??07WTlQ*as$fHO%lXsl2}4FR;5Vn}&bi{b(W&X4wHb zz5OGw&)8mTX=@sB-@m9{9iS(<%22(U0fNedsEys=H&|w2xN9Y; z6idp{cRIq1o*S~*`_}k-9->MCIG)M-fwDMcBfm)~da06pD4Pwf7n8Md9<5fMt9uyJ z(0hZr?;1U8j z(XBH_l56HizYY#I_W5IU=77fBj$EYUP{`HQzaOf8z1Gp(TJBRunSi`LLc#zM2@@ijdV z!4uk6iy$5+iWL>mFy{22-+I1@=3>KYRgg8GJ$p9tjx>9QSp*Auuk}#KPB2WLj>Gc4XhfMlU2nR$eu$$2V%R-<(C2$>5jN4Ka29CDX_Qdc>UU_~MeIPfMHO^AVx zqzwvW4yMS**Q?OMWp{Aju}BTw2gpUd&$mA%n1K(awdFZ5)SCj$*I8PX7}6ezIA`Eg zfr&x}6_!_Ft?oSoHx~~jfinKmi+Rm$j^@*xeK#Q@^x03TV68EIhm7v(>T(f~ncu(j zGCCbR=^kmhMle1y<%rZm_1y{QZN4a;YLGuID^gU;+2`LMRDsak$RS0192p6j22;Cj z?{qsIpac3MvE39)lOW~P&fKp?Q z7%_t)9>qyfUQ>;l^~&SPgX-P49(AS2QM@?&0$~l%l`f|l zfofw_!>iS3?g(-{W$pX=jWy->7hMXppBbW zs9(uv?QBaPGt#pHYJh20&t<*8H$nwbVQepG&*@kgQGQ94n5rf&3#tEZxEwpHQIaO~ zWmh@U{NB810`H%rA^B_8FwETs)5FPe3wNjvsex*Ve!P)K^qu}nmkcB|x)1x_kUiu` zs4)~z9TO{hhVZjWb{##7Q^v|M?W65ImfE?T^^`D_{=lf5L7>B&SorN&{;Y=PYO%h! zpEHyda{3J5P8xh4)?Ym(R55Zxp~cE#;|S3dyW9m8Koa$l&WTOYJ&7bucWrgu-Cq0` zjBb_!;kdJ!p(N1L&uH{MW6~*BINcxQn%hBfBRV@Okp7x5e*Ql$z&3dn(_T^WN<#+5 zq$+Lm8aZFxa{nfc<-vqA5uR$0pjKA&JAe9i&^}>`^iWGekS7-N%;Z67I7&*{&Fhux z=P3W)L-iT(TJRIpE*jVZ!9pwYLQ1E@C$hcCmM1t@2@P7v8SCXG6#iguUq4D7JYkVR zhDU6%Q`47(z@h_DRbg4M?s{R}UaNCEwC!mjj5F z?UYHmafQ^#;|i~P-~ZmYOKfGaIe;}xFMQY6DMy^4Up#_nfrKNEy0l>Ri4}>G2h9%l ziK!MpU%Gq4piyBtaC2)*sA{$UH6UK15X_xuRSxI~u)1`dv3(+7KehF7b|nDnbdzt1 zpY)@^McN+GWFO~8yT)h)@Mt5=SsMg&^YcMw1*8QmL|>j0^Hq-wf=yk7iaeXzIe8w! zfAt3T^AUJ+$e#O32F#J`3V9EiI3l^QV*rvbl>NJfAqetzuh z!vo_e$#$v|QS1u!UF8cpqmH$HM_Gf#nXy9~66M^LCaKQZCkPryR+~Vy>rJgM@(eF3 z5zher>A&Docb&Fu_Btgn!Pv=Z8jYNlDEUW^IU>rIQua0`1V`xF8{pw!Pil zD&=bIzM7srd)7*VYg02aRpglLr?l-7UTMgHYjC`PU7!!xo+so1e!^%b@w(HLcS6-n zX=u(AvaTMPiD&=~M@TuhvZ|tj(SVC!>eaS$y{xr0JbOCGiAH7L3(vCA{`@*bnWGP@ zS1!cP##Xw!@=#0lSg!XiIYVB%sZ$ChOZssM;|QIjjY&vVC!zk~h6|VT5vEg*AiYRM zhS7v_zi>xabV(i|^dL8)iJlHqd*@4xPV&ylZq*1ZJKBz#wgxIOyzr#796}k;c(cUt zB$}^x^mZDHLfsI8Q+!8ZpKM~N0_5RHsXnPLgV<0Q%5DF@%q{0M_1ac!#TstgT-XGF>N0CGae+~Fn4KUF>vfe6? z`l^Kv-05AR0cA!@bgmBUZhI*UP6kF4{TFXoZhB3--JtwBh)*Pi(6pSv-GNZe3>K!Y zJDMZ%wo^6=q?Q9O?B2;WL9H|f=1Qx%;F;uT5L<2xniXppbWVhTIKRGB*ht#=hJSS@ z8;EEh`F699Bbg*WMD0!)-6@UKPRox&LuUQQo+ZR`O`$<3%ZnZ2Osl1{3M;k9#z%Kqw~6Q3)&82U;6q)j_uy5IC_!=pMK`oWldJ2 zwDu4bcJHfC?%v{GQz?A=gASPrBNcwlY?5INm*d}XmT|6;MnjAMj`;YVV11bycCLyY{QGk@uqgPvEsu#KEF%UGDIJ?^6u{@gVeb$eFodx1_ja9^ z8q4^ouB{&$f5#(=i|;P8-2R-n9rEC`WPl4g`CcD4UWxkg+W$peM~X(TKMy`EYr_Y< zsjbNO`$V2DQJebvTZ5Ytl3jLH#F>7a|ASe6Z7!W)O3`;}8aUbeES)G$>Ooh0F-4pJ zuQq|x>)phP?D*Er)oFUp?LHryZXY$r= zJ05!WrSbV2e;$$RICyuvqX+M7P(syF<`Af%LE;%6u}oK5&-nP{M|&RbJEVCUOJQAe zeMNK~dGL@Ym@ik%%4oYMaS`yzpgB^dXbkzY;d6*HuM>aBDCOx8edSaL9qJNDUNkmP({A4+x)ZLA_V?Gv!$Yv7zLJrm$411E0+p#?3ARZ! zvHpWUn@9{y7q=};mn>zQ6`iv705eeqK7=^^SPojSw8e|abgLOielnG22t}bj&MJ28 ztN|HWPZHG3kByit+%Q;RI9qHCXV9YFXDPaTG=d%2gT7rA%KaAbP4Vj0eq2N9tPE#K zdHMMQ-!4g3Dp#}X`Y9%q2tS)2xa$Ji?4!u{39N?(>YaN30nPb$yPdoMTP$2j!eYL$ zt9hV$^(J6+2cRq4c0S46_|{RRG>1(-InC%Dpq-XDLUqM}+7)k0cvS|3@iL>i zB&4XcUjIR7>als~l(o8m7&6`efdqIm_byBDvn>Nla5fM6>BCWMYVTO-qaKiz^J)*i zIoYf34b8{%@PrYa+DB};P_pzK*u2-r-ZgqKMQhs-f`ORui>k^B2NFU;@5!}XAnELc z#~(tq6K8l~_S#?;&Y=3vBcWC-`XXpBaNQ55!a3;Bd9J@b(TX1$8}m}jz*AUC29luz z8UeF;clOt>uk0^sO8wUPS+CX;3>Gg~ZyLP@35h|5HhNCpK-#lF3FttG;aEI<2(%_)BPQWdh~Gb2YFOsZ7ut- zdxlYYTJY*8vrnZ4Ip-vGp@mUA`VfrZQ#v1P&KgGWZ2}IRyQZ7Px0#wch$N#7B1%x%~H=laby;##PS)R?zu9+~E_tDr|@o zL96AGghbJ*VY+Dv6#01HdSn~>Kq8Y1n>v`CMM^XBhqZd64UBC=oG4j^RztFz^Pd|N zs8oOtLqpS*MJz1uci^NTk0`0viAtqHSTThfmhFWmtbqe4k<4zY`J$Y4Yv}#L!qc4i zKK3(^jaQ&S{yF^XNL5;PKLq}GE?sk&Uq@Y>P%pcE{8BM6=2}-iUg(dCiqefR66Sb8 z9iYV@=3j7JU`f4|-6OKC%TzCMgdpC#+}ciAYQ;tpfvO?^)Q>-a-T(gS`?N^2w@#+Y zx^0xN^=X%3mbqj}>v1!OINua{qcj63wghpC3<_=Tpt5^5w&SVB!N|3v1_>^jo}PaD zN-pnDLMp+WZh3Cy*l!Mwi_gvc*?ESY76m z>CPso-Cm?-Irpp$4x5?JXZy{!?-IvV5;sAk_$y)RunX{98n*7cLD>7^Pd_2FyDiIk zh9kMkvzmi)7u1zJ&jaE}6HQ?h(asRFH0p3FQ)Je@n%)CJjPGOS7cm<*9@!@YA5aUT zZr$fP$8KXJq12BkLb-cM;!V5fO*DF6)M+g_cIUa-S&|(*4Ff!l4D@rOabjm*m@=ZZ z)$#;C<@x~l+85nIzu))i*Am!}5%*rnbloBr-MesEN>3`i=#=oK#UELR^(6Cwyj2iF z*=GYbp0~Bm+439(xQ*{l**AYS`P)>g71;U(nCJsZAYa*o?}VaanuwKd&R=LY89;k< zKodTFQh`JfBa9yKeRNJ&7%fq9<8=({{@MrokfDIb&}a|WHa9G%masO%D zZwMuN7g-G)c>ZMHse9y+QxG(DcpkcKGIMKV0apCbalTmYXQEbOd`i+};PqXOAJj>L zCbT*gIT4A0;w-%tmh8~g43kG&-4tgpuc26eUt%TspGV+fFeW^OQ|N_HfF-1g&hDZ3 z7N^-#D6px6Aw9nQ^T)HUN+58TNbH|gEb#w$J~1iw(b+nZ`Jf&#<-aFuJZ}=?twJ%g z3NTrxy3-C{pU8Z(tU;aQszY+uem$v?PCvYKjpwI`mjd3G!k@DI_`L`knlLYbWO;e5 z(t1aVmxaFkL*v5E0JSB_}Mkh{mTANaD?QmZN zicL=^$G;F1p5jlB-=e;uXpvg~VJXslvp;ZPPjKL0zIrN))&@EzP#a+5lr=SF!aoub zJjyZ5zVcaz8$FhRGNGyxu^W0K=e-_2Tx-}$`2UYP)8@KFM^Tm4v+I;Lu!3YO;i5YQ z^It|?!q@p@2|c;9VS8I+LR2r*jG~sXx@V`-qF_I;Eon;IC}y5_H*DqTCq^8ncd9oB zPiX}?^E$DupK=kZM_A}Z{JMnn*?3Vug;`4_RIABH{V3eg^}{l0Mk*7xIqSwCe<{Fp zzw5o+%}l1F$XwOe?Suey^qB5bU*~GLKZ{bZ^UEm@@UNf$Mnk;3Y-}R=J46T>Llqe6 zdY&{8*Si>eI2{O1#!@9UV3@oJ{IroL4xK4)$M$yDXgYjU+7v8#=ooFYR$wyE%T?7~ z$%xL#;Bl^=9d;@pQ`?qT;0Ay~I6=%p4nFn_`kPMyRnxoHw07ioMUco_?O+A%br!f* z2~(k@?gp$d^+Uv2UX3rufXX9&6i-;}xo+|#A*5^tb)@2+lP zvHNLhAkhP(Bpjvo=gAMgq3Wv-Wx7)iEj*u`7SX#HQwOUonZKch4)-SH*W=g|qqV78 zWalQhdwN+kd0M@6bsJIEf(j}u!_naLqc)>B z+3s6LOwdl8AF6EvO|tzMm>9kUk$IU7=E}+tlrdf{f8Xwg{Tn{(R=qPycmVD#7fW@B zU&Q@j@-%vntYJK=FY7&jCgFzO#bAMJ2lr{9`u;pyr~#AFmS4aV8HxbCt~!t{#$_+R z=I8zr-nRh@Hhpo)%~)k5hYwehZ&%< zs-bZkZY?Lg;E4C7Zf$u#i2Q&5I39v6DFE`MQJ^xP$m;%iW&Rae8Ezc_mMcdnunFlj zwQh^qt3{TD{TC4MNwX~9pEnGyfc5vstF56yZAKO2rqTrQk;Yk|$FLeqTHYWhuhmIV z1O8;1QBzir@Mv#<)fd>}D@!_C8Xm_Iaw(Oe2~6A`ZtM^g zLok5P@OT*^rggycX9iPW%Z+&Te52Y#>TWr`T$3cr{FKsTdb))`!_*|n3 zp;N~u89&E4jwk~wH6-6`Q3j80oc#goHFf`@ppTqKz+{LgtSaQB&;i zBSBhzkA=oYMlOUO>7rj81cEkt3np~q&xs4bdc629qdfaGlq{3=?bZ}Z-v4D)upEvs zbdz27<0g_`QWiDXb^jO=g#Kk%L4Q_WKzluEA^h3x!&KCo9^zFNXN%MBZt4>%hRkKAnEbT_~1*H@B*}{W#rEb2|buwBu_^ zopMT|ZBRxHF8;n8t=SP^^~O&AI)BFrD*^!k^3&_F)anAaB7td$dkY0PPO_as zfm(){yWWI}w|OWi)9QL3aDt|omls3QiGt85{@`&)b+6#FV|WY zXa1p`^1;IX->>0(q5N7RF-z5L>$XGo%c|SSMW<>c|>iMsrdw2;P z39eOD3h1;6Ge9A3li5X=40OXqq*hL9#Q#-N#B)$za-q0e-=0fj!A23+6Hbg?0tLLQ zy_Z+56v6W_Z??52u(0bueN7&-5_|>M<(cD6Vbhqivawz3rTF%zL3wma_ys^1FG)`M z!ykaN;68GEa~aT`b&wcV{V3)hzIZ%yi!{pt1vV5`P~2$_u#eY55zC5w-RNPe_g5hB zE6Ni14VR_TrJ5h=<8t2E#Ho3hCt|tGo=5aLHF0SL?d~H9~h5}03wgSJ! zb78(3Rb%n$-{fHz^P@;#v({*WG~Pc)kF{`{vaF?*UAo+D5mNsU%0~3;$OY zgP)6#?NtArbE6SKK*o^m=39xu-S@{}4zW5WDOrCAKx@QsvGd3LYo*yxBb@A<29)Gi$T zVDSSK;w*`!49{fIuR!Sd81CdC!_`V|P_?-Nv&_}on*)|5T*X77C}u1G6U`gQ+-h43 zZEf?hMH;VSz{f`&YQ;TRiDx+a5)jXoKFgmBOBzWv&2tsae?t?t_2vWjv``{kOmN(0 z-&@1)<=!DOFfcrZli$~p%xl>_KYhc$+`5SKuY~z1cz|IRg$gni`2DfOYy&5G55+){ z%*!CSX$e~PHM8{5Rm<#_6ykQ|z_(2Y0V`5u`@E?m^>6f?;}Ps93J zVV-!=0?2+JQjY{;aJ+=|Tp(7&)~~uRi=>*_{g;M;|6T}4b~h8%<|KxeHI{zxA?P`{ zN-Y8d*hVH7!S>6yd-e4RK30do3zEvv2?ALP?~{p`BlN(1yVw15do!COW;9isS2Vuu zHXg9@bR#EuKLAQJ!3a);O&$vk_(izXZ6Tj}+6SqE4kvKKT5}9X&q(pJjk!Rre*(Tr z*%cWcn9dWJ<)dU%l)iO*8996{!tW`+bP3hoWfVZ)ub_z(4}3wx@C0g*+)f7~snh>j ze-n6;1ZO4p#w21FKPy9VAHZ_scC2W_q3X8nZZXE6S0}@XhSbLePp+ z4%=K2!&N@v@ZmH$mV7Rh{)H^U2kVMk_AJht z$~^I)!lmHC62$eQ_KDr?tjEnVEwJ;@i8Y+^pJ1Zv1&&Fe1+B{U4P>;LE>x;O@!dxahm zqDTb2x8-hCfJjBkiUQ6=w8$T@fN4@t2}O152zC!VTJ`WeCWi2x#NYh*`wgiQFCxH9 zqP!gIhYkP#{2hNR)(?CWZ(FCz48LgvJ+84_>i@6?Zi+b$xpBRY?_gi zVUBY3kpFQJz0-V>`2H+a-lUFc>5_0JVF$G&-b@)(-tfndb#X-ge%sp;+)&XeB@lDL z;BO1yLb_8AXEp372QB97sV#2!xaw5F`o)CN(NPzY{uH?OJ;>EHI+?X$-n)F!)6MMy zV5&IPb4ljwZy%gbZHC)mHL%wEPxatyLU)jw_dk6LGK*xk0iu4D3Dl9t(EBEq<&l1_ z-M@vwN|%fdT3GYNw*&p`EYUZW{`@+K7pxt;y%&Sgn=B41Wk$|u3)b(`{x)!LU^mXU_;>r&XIkwSQny(ci+q*)E^EV1YNCPJWEd*%laxgi&b!a-_qW;TTEQE4fIR zT9EexkeCc7FX%G2kjbmmTBc0VC@Xp$xr4Lgi3F!pS|V@aWoaYCQ>@4YX5A4!r3?qJ zkPsG3>LAW^BN>=wV{{7naPd9s&!2Q~YqU?UnOX^*C$<*xJ_|NSIc#D$ zrDS-W3a|+_;AXWN1jFnb@<|vQcqL<12zX>kBsNhT8G_Usl>jgo-5>Qb0$r4(uslrd zoE{--4HzJngm5cKvcB6+*$(r?&MJKf!4)PfoZ%4THt90{}Fa}hX?9wHgv^Li0O-Ag{bdxpf+8Kkx zp3GS!B~%GbmBW-^^{T~4qqY5%8QA!>y}YZvehWUB1@vE+<2^>qIPvv^!O-BcBOE#P zXyA}5COzVtF@hjL=W(Zvv2@Q(< x5PD==iHm90@_43;i_5H7k%jdmaa{Y>-cucq&DrJ133v$PyzWI@jke9L{|7#S=KBBu literal 0 HcmV?d00001 diff --git a/src/android/app/src/main/res/layout/dialog_auto_map.xml b/src/android/app/src/main/res/layout/dialog_auto_map.xml new file mode 100644 index 000000000..95cb8d138 --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_auto_map.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + +