diff --git a/CMakeLists.txt b/CMakeLists.txt
index f5d7126f92..3599105020 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -139,65 +139,50 @@ endif()
# Set bundled sdl2/qt as dependent options.
# On Linux system SDL2 is likely to be lacking HIDAPI support which have drawbacks but is needed for SDL motion
-CMAKE_DEPENDENT_OPTION(ENABLE_SDL2 "Enable the SDL2 frontend" ON "NOT ANDROID" OFF)
-
-set(EXT_DEFAULT OFF)
-
-if (MSVC OR ANDROID)
- set(EXT_DEFAULT ON)
-endif()
+cmake_dependent_option(ENABLE_SDL2 "Enable the SDL2 frontend" ON "NOT ANDROID" OFF)
if (ENABLE_SDL2)
# TODO(crueter): Cleanup, each dep that has a bundled option should allow to choose between bundled, external, system
- CMAKE_DEPENDENT_OPTION(YUZU_USE_EXTERNAL_SDL2 "Compile external SDL2" OFF "NOT MSVC" OFF)
+ cmake_dependent_option(YUZU_USE_EXTERNAL_SDL2 "Compile external SDL2" OFF "NOT MSVC" OFF)
option(YUZU_USE_BUNDLED_SDL2 "Download bundled SDL2 build" "${MSVC}")
endif()
+option(ENABLE_QT "Enable the Qt frontend" ON)
+option(ENABLE_QT_TRANSLATION "Enable translations for the Qt frontend" OFF)
+option(ENABLE_QT_UPDATE_CHECKER "Enable update checker for the Qt frontend" OFF)
+cmake_dependent_option(YUZU_USE_BUNDLED_QT "Download bundled Qt binaries" "${MSVC}" "ENABLE_QT" OFF)
+option(YUZU_USE_QT_MULTIMEDIA "Use QtMultimedia for Camera" OFF)
+option(YUZU_USE_QT_WEB_ENGINE "Use QtWebEngine for web applet implementation" OFF)
+set(YUZU_QT_MIRROR "" CACHE STRING "What mirror to use for downloading the bundled Qt libraries")
+
+option(ENABLE_CUBEB "Enables the cubeb audio backend" ON)
+
+set(EXT_DEFAULT OFF)
+if (MSVC OR ANDROID)
+ set(EXT_DEFAULT ON)
+endif()
+option(YUZU_USE_CPM "Use CPM to fetch system dependencies (fmt, boost, etc) if needed. Externals will still be fetched." ${EXT_DEFAULT})
+
+option(YUZU_USE_BUNDLED_FFMPEG "Download bundled FFmpeg" ${EXT_DEFAULT})
+cmake_dependent_option(YUZU_USE_EXTERNAL_FFMPEG "Build FFmpeg from source" OFF "NOT WIN32 AND NOT ANDROID" OFF)
+
cmake_dependent_option(ENABLE_LIBUSB "Enable the use of LibUSB" ON "NOT ANDROID" OFF)
cmake_dependent_option(ENABLE_OPENGL "Enable OpenGL" ON "NOT WIN32 OR NOT ARCHITECTURE_arm64" OFF)
mark_as_advanced(FORCE ENABLE_OPENGL)
-option(ENABLE_QT "Enable the Qt frontend" ON)
-option(ENABLE_QT_TRANSLATION "Enable translations for the Qt frontend" OFF)
-option(ENABLE_QT_UPDATE_CHECKER "Enable update checker for the Qt frontend" OFF)
-
-CMAKE_DEPENDENT_OPTION(YUZU_USE_BUNDLED_QT "Download bundled Qt binaries" "${MSVC}" "ENABLE_QT" OFF)
-
-option(YUZU_USE_CPM "Use CPM to fetch system dependencies (fmt, boost, etc) if needed. Externals will still be fetched." ${EXT_DEFAULT})
-
option(ENABLE_WEB_SERVICE "Enable web services (telemetry, etc.)" ON)
option(ENABLE_WIFI_SCAN "Enable WiFi scanning" OFF)
-option(YUZU_USE_BUNDLED_FFMPEG "Download bundled FFmpeg" ${EXT_DEFAULT})
-cmake_dependent_option(YUZU_USE_EXTERNAL_FFMPEG "Build FFmpeg from source" OFF "NOT WIN32 AND NOT ANDROID" OFF)
-
-option(YUZU_USE_QT_MULTIMEDIA "Use QtMultimedia for Camera" OFF)
-
-option(YUZU_USE_QT_WEB_ENGINE "Use QtWebEngine for web applet implementation" OFF)
-
-set(YUZU_QT_MIRROR "" CACHE STRING "What mirror to use for downloading the bundled Qt libraries")
-
-option(ENABLE_CUBEB "Enables the cubeb audio backend" ON)
-
-CMAKE_DEPENDENT_OPTION(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF "ENABLE_QT" OFF)
+cmake_dependent_option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF "ENABLE_QT" OFF)
option(YUZU_TESTS "Compile tests" "${BUILD_TESTING}")
-option(YUZU_USE_PRECOMPILED_HEADERS "Use precompiled headers" ${EXT_DEFAULT})
-
-# TODO(crueter): CI this?
-option(YUZU_DOWNLOAD_ANDROID_VVL "Download validation layer binary for android" ON)
-
-
-CMAKE_DEPENDENT_OPTION(YUZU_ROOM "Enable dedicated room functionality" ON "NOT ANDROID" OFF)
-
-CMAKE_DEPENDENT_OPTION(YUZU_ROOM_STANDALONE "Enable standalone room executable" ON "YUZU_ROOM" OFF)
-
-CMAKE_DEPENDENT_OPTION(YUZU_CMD "Compile the eden-cli executable" ON "ENABLE_SDL2;NOT ANDROID" OFF)
-
-CMAKE_DEPENDENT_OPTION(YUZU_CRASH_DUMPS "Compile crash dump (Minidump) support" OFF "WIN32 OR LINUX" OFF)
-
+option(YUZU_USE_PRECOMPILED_HEADERS "Use precompiled headers" OFF)
+if (YUZU_USE_PRECOMPILED_HEADERS)
+ message(STATUS "Using Precompiled Headers.")
+ set(CMAKE_PCH_INSTANTIATE_TEMPLATES ON)
+endif()
option(YUZU_ENABLE_LTO "Enable link-time optimization" OFF)
if(YUZU_ENABLE_LTO)
include(CheckIPOSupported)
@@ -205,17 +190,42 @@ if(YUZU_ENABLE_LTO)
if(NOT COMPILER_SUPPORTS_LTO)
message(FATAL_ERROR "Your compiler does not support interprocedural optimization (IPO). Re-run CMake with -DYUZU_ENABLE_LTO=OFF.")
endif()
+ set(CMAKE_POLICY_DEFAULT_CMP0069 NEW)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ${COMPILER_SUPPORTS_LTO})
endif()
+option(USE_CCACHE "Use ccache for compilation" OFF)
+set(CCACHE_PATH "ccache" CACHE STRING "Path to ccache binary")
+if(USE_CCACHE)
+ find_program(CCACHE_BINARY ${CCACHE_PATH})
+ if(CCACHE_BINARY)
+ message(STATUS "Found ccache at: ${CCACHE_BINARY}")
+ set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE_BINARY})
+ set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE_BINARY})
+ if (YUZU_USE_PRECOMPILED_HEADERS)
+ message(FATAL_ERROR "Precompiled headers are incompatible with ccache. Re-run CMake with -DYUZU_USE_PRECOMPILED_HEADERS=OFF.")
+ endif()
+ else()
+ message(WARNING "USE_CCACHE enabled, but no executable found at: ${CCACHE_PATH}")
+ endif()
+endif()
+
+# TODO(crueter): CI this?
+option(YUZU_DOWNLOAD_ANDROID_VVL "Download validation layer binary for android" ON)
+
+cmake_dependent_option(YUZU_ROOM "Enable dedicated room functionality" ON "NOT ANDROID" OFF)
+cmake_dependent_option(YUZU_ROOM_STANDALONE "Enable standalone room executable" ON "YUZU_ROOM" OFF)
+
+cmake_dependent_option(YUZU_CMD "Compile the eden-cli executable" ON "ENABLE_SDL2;NOT ANDROID" OFF)
+
+cmake_dependent_option(YUZU_CRASH_DUMPS "Compile crash dump (Minidump) support" OFF "WIN32 OR LINUX" OFF)
option(YUZU_DOWNLOAD_TIME_ZONE_DATA "Always download time zone binaries" ON)
-
-CMAKE_DEPENDENT_OPTION(YUZU_USE_FASTER_LD "Check if a faster linker is available" ON "NOT WIN32" OFF)
-
-CMAKE_DEPENDENT_OPTION(USE_SYSTEM_MOLTENVK "Use the system MoltenVK lib (instead of the bundled one)" OFF "APPLE" OFF)
-
set(YUZU_TZDB_PATH "" CACHE STRING "Path to a pre-downloaded timezone database")
+cmake_dependent_option(YUZU_USE_FASTER_LD "Check if a faster linker is available" ON "LINUX" OFF)
+
+cmake_dependent_option(YUZU_APPLE_USE_BUNDLED_MONTENVK "Download bundled MoltenVK lib" ON "APPLE" OFF)
+
option(YUZU_DISABLE_LLVM "Disable LLVM (useful for CI)" OFF)
set(DEFAULT_ENABLE_OPENSSL ON)
@@ -228,15 +238,12 @@ if (ANDROID OR WIN32 OR APPLE OR PLATFORM_SUN)
# your own copy of it.
set(DEFAULT_ENABLE_OPENSSL OFF)
endif()
-
if (ENABLE_WEB_SERVICE)
set(DEFAULT_ENABLE_OPENSSL ON)
endif()
-
option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENABLE_OPENSSL})
-
if (ENABLE_OPENSSL)
- CMAKE_DEPENDENT_OPTION(YUZU_USE_BUNDLED_OPENSSL "Download bundled OpenSSL build" "${MSVC}" "NOT ANDROID" ON)
+ cmake_dependent_option(YUZU_USE_BUNDLED_OPENSSL "Download bundled OpenSSL build" "${MSVC}" "NOT ANDROID" ON)
endif()
if (ANDROID AND YUZU_DOWNLOAD_ANDROID_VVL)
@@ -263,21 +270,6 @@ if (ANDROID)
set(CMAKE_POLICY_VERSION_MINIMUM 3.5) # Workaround for Oboe
endif()
-if (YUZU_USE_PRECOMPILED_HEADERS)
- if (MSVC AND CCACHE)
- # buildcache does not properly cache PCH files, leading to compilation errors.
- # See https://github.com/mbitsnbites/buildcache/discussions/230
- message(WARNING "buildcache does not properly support Precompiled Headers. Disabling PCH")
- set(DYNARMIC_USE_PRECOMPILED_HEADERS OFF CACHE BOOL "" FORCE)
- set(YUZU_USE_PRECOMPILED_HEADERS OFF CACHE BOOL "" FORCE)
- endif()
-endif()
-
-if (YUZU_USE_PRECOMPILED_HEADERS)
- message(STATUS "Using Precompiled Headers.")
- set(CMAKE_PCH_INSTANTIATE_TEMPLATES ON)
-endif()
-
# Default to a Release build
get_property(IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if (NOT IS_MULTI_CONFIG AND NOT CMAKE_BUILD_TYPE)
@@ -894,24 +886,47 @@ if (MSVC AND CXX_CLANG)
link_libraries(llvm-mingw-runtime)
endif()
+#[[
+ search order:
+ - gold (GCC only) - the best, generally, but unfortunately not packaged anymore
+ - mold (GCC only) - generally does well on GCC
+ - ldd - preferred on clang
+ - bfd - the final fallback
+ - If none are found (macOS uses ld.prime, etc) just use the default linker
+]]
if (YUZU_USE_FASTER_LD)
- # fallback if everything fails (bfd)
- set(LINKER bfd)
- # clang should always use lld
- find_program(LLD lld)
- if (LLD)
+ find_program(LINKER_BFD bfd)
+ if (LINKER_BFD)
+ set(LINKER bfd)
+ endif()
+
+ find_program(LINKER_LLD lld)
+ if (LINKER_LLD)
set(LINKER lld)
endif()
- # GNU appears to work better with mold
- # TODO: mold has been slow lately, see if better options exist (search for gold?)
+
if (CXX_GCC)
- find_program(MOLD mold)
- if (MOLD AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "12.1")
+ find_program(LINKER_MOLD mold)
+ if (LINKER_MOLD AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "12.1")
set(LINKER mold)
endif()
+
+ find_program(LINKER_GOLD gold)
+ if (LINKER_GOLD)
+ set(LINKER gold)
+ endif()
+ endif()
+
+ if (LINKER)
+ message(NOTICE "Selecting ${LINKER} as linker")
+ add_link_options("-fuse-ld=${LINKER}")
+ else()
+ message(WARNING "No faster linker found--using default")
+ endif()
+
+ if (LINKER STREQUAL "lld" AND CXX_GCC)
+ message(WARNING "Using lld on GCC may cause issues with certain LTO settings. If the program fails to compile, disable YUZU_USE_FASTER_LD, or install mold or GNU gold.")
endif()
- message(NOTICE "Selecting ${LINKER} as linker")
- add_link_options("-fuse-ld=${LINKER}")
endif()
# Set runtime library to MD/MDd for all configurations
diff --git a/CMakeModules/DownloadExternals.cmake b/CMakeModules/DownloadExternals.cmake
index 6c4afc03be..f6e3aaa4ad 100644
--- a/CMakeModules/DownloadExternals.cmake
+++ b/CMakeModules/DownloadExternals.cmake
@@ -10,6 +10,7 @@ function(download_bundled_external remote_path lib_name cpm_key prefix_var versi
set(package_base_url "https://github.com/eden-emulator/")
set(package_repo "no_platform")
set(package_extension "no_platform")
+ set(CACHE_KEY "")
# TODO(crueter): Need to convert ffmpeg to a CI.
if (WIN32 OR FORCE_WIN_ARCHIVES)
@@ -33,8 +34,9 @@ function(download_bundled_external remote_path lib_name cpm_key prefix_var versi
else()
message(FATAL_ERROR "No package available for this platform")
endif()
- set(package_url "${package_base_url}${package_repo}")
- set(full_url ${package_url}${remote_path}${lib_name}${package_extension})
+ string(CONCAT package_url "${package_base_url}" "${package_repo}")
+ string(CONCAT full_url "${package_url}" "${remote_path}" "${lib_name}" "${package_extension}")
+ message(STATUS "Resolved bundled URL: ${full_url}")
# TODO(crueter): DELETE THIS ENTIRELY, GLORY BE TO THE CI!
AddPackage(
@@ -47,26 +49,12 @@ function(download_bundled_external remote_path lib_name cpm_key prefix_var versi
# TODO(crueter): hash
)
- set(${prefix_var} "${${cpm_key}_SOURCE_DIR}" PARENT_SCOPE)
- message(STATUS "Using bundled binaries at ${${cpm_key}_SOURCE_DIR}")
-endfunction()
-
-function(download_moltenvk_external platform version)
- set(MOLTENVK_DIR "${CMAKE_BINARY_DIR}/externals/MoltenVK")
- set(MOLTENVK_TAR "${CMAKE_BINARY_DIR}/externals/MoltenVK.tar")
- if (NOT EXISTS ${MOLTENVK_DIR})
- if (NOT EXISTS ${MOLTENVK_TAR})
- file(DOWNLOAD https://github.com/KhronosGroup/MoltenVK/releases/download/${version}/MoltenVK-${platform}.tar
- ${MOLTENVK_TAR} SHOW_PROGRESS)
- endif()
-
- execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf "${MOLTENVK_TAR}"
- WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/externals")
+ if (DEFINED ${cpm_key}_SOURCE_DIR)
+ set(${prefix_var} "${${cpm_key}_SOURCE_DIR}" PARENT_SCOPE)
+ message(STATUS "Using bundled binaries at ${${cpm_key}_SOURCE_DIR}")
+ else()
+ message(FATAL_ERROR "AddPackage did not set ${cpm_key}_SOURCE_DIR")
endif()
-
- # Add the MoltenVK library path to the prefix so find_library can locate it.
- list(APPEND CMAKE_PREFIX_PATH "${MOLTENVK_DIR}/MoltenVK/dylib/${platform}")
- set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} PARENT_SCOPE)
endfunction()
# Determine installation parameters for OS, architecture, and compiler
@@ -108,7 +96,7 @@ function(determine_qt_parameters target host_out type_out arch_out arch_path_out
set(host "linux")
set(type "desktop")
set(arch "linux_gcc_64")
- set(arch_path "linux")
+ set(arch_path "gcc_64")
endif()
set(${host_out} "${host}" PARENT_SCOPE)
@@ -143,56 +131,79 @@ function(download_qt_configuration prefix_out target host type arch arch_path ba
set(install_args -c "${CURRENT_MODULE_DIR}/aqt_config.ini")
if (tool)
set(prefix "${base_path}/Tools")
- set(install_args ${install_args} install-tool --outputdir ${base_path} ${host} desktop ${target})
+ list(APPEND install_args install-tool --outputdir "${base_path}" "${host}" desktop "${target}")
else()
set(prefix "${base_path}/${target}/${arch_path}")
- set(install_args ${install_args} install-qt --outputdir ${base_path} ${host} ${type} ${target} ${arch} -m qt_base)
+ list(APPEND install_args install-qt --outputdir "${base_path}" "${host}" "${type}" "${target}" "${arch}" -m qt_base)
if (YUZU_USE_QT_MULTIMEDIA)
- set(install_args ${install_args} qtmultimedia)
+ list(APPEND install_args qtmultimedia)
endif()
if (YUZU_USE_QT_WEB_ENGINE)
- set(install_args ${install_args} qtpositioning qtwebchannel qtwebengine)
+ list(APPEND install_args qtpositioning qtwebchannel qtwebengine)
endif()
- if (NOT ${YUZU_QT_MIRROR} STREQUAL "")
+ if (NOT "${YUZU_QT_MIRROR}" STREQUAL "")
message(STATUS "Using Qt mirror ${YUZU_QT_MIRROR}")
- set(install_args ${install_args} -b ${YUZU_QT_MIRROR})
+ list(APPEND install_args -b "${YUZU_QT_MIRROR}")
endif()
endif()
- message(STATUS "Install Args ${install_args}")
+ message(STATUS "Install Args: ${install_args}")
+
if (NOT EXISTS "${prefix}")
message(STATUS "Downloading Qt binaries for ${target}:${host}:${type}:${arch}:${arch_path}")
set(AQT_PREBUILD_BASE_URL "https://github.com/miurahr/aqtinstall/releases/download/v3.3.0")
if (WIN32)
set(aqt_path "${base_path}/aqt.exe")
if (NOT EXISTS "${aqt_path}")
- file(DOWNLOAD
- ${AQT_PREBUILD_BASE_URL}/aqt.exe
- ${aqt_path} SHOW_PROGRESS)
+ file(DOWNLOAD "${AQT_PREBUILD_BASE_URL}/aqt.exe" "${aqt_path}" SHOW_PROGRESS)
+ endif()
+ execute_process(COMMAND "${aqt_path}" ${install_args}
+ WORKING_DIRECTORY "${base_path}"
+ RESULT_VARIABLE aqt_res
+ OUTPUT_VARIABLE aqt_out
+ ERROR_VARIABLE aqt_err)
+ if (NOT aqt_res EQUAL 0)
+ message(FATAL_ERROR "aqt.exe failed: ${aqt_err}")
endif()
- execute_process(COMMAND ${aqt_path} ${install_args}
- WORKING_DIRECTORY ${base_path})
elseif (APPLE)
set(aqt_path "${base_path}/aqt-macos")
if (NOT EXISTS "${aqt_path}")
- file(DOWNLOAD
- ${AQT_PREBUILD_BASE_URL}/aqt-macos
- ${aqt_path} SHOW_PROGRESS)
+ file(DOWNLOAD "${AQT_PREBUILD_BASE_URL}/aqt-macos" "${aqt_path}" SHOW_PROGRESS)
+ endif()
+ execute_process(COMMAND chmod +x "${aqt_path}")
+ execute_process(COMMAND "${aqt_path}" ${install_args}
+ WORKING_DIRECTORY "${base_path}"
+ RESULT_VARIABLE aqt_res
+ ERROR_VARIABLE aqt_err)
+ if (NOT aqt_res EQUAL 0)
+ message(FATAL_ERROR "aqt-macos failed: ${aqt_err}")
endif()
- execute_process(COMMAND chmod +x ${aqt_path})
- execute_process(COMMAND ${aqt_path} ${install_args}
- WORKING_DIRECTORY ${base_path})
else()
+ find_program(PYTHON3_EXECUTABLE python3)
+ if (NOT PYTHON3_EXECUTABLE)
+ message(FATAL_ERROR "python3 is required to install Qt using aqt (pip mode).")
+ endif()
set(aqt_install_path "${base_path}/aqt")
file(MAKE_DIRECTORY "${aqt_install_path}")
- execute_process(COMMAND python3 -m pip install --target=${aqt_install_path} aqtinstall
- WORKING_DIRECTORY ${base_path})
- execute_process(COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${aqt_install_path} python3 -m aqt ${install_args}
- WORKING_DIRECTORY ${base_path})
+ execute_process(COMMAND "${PYTHON3_EXECUTABLE}" -m pip install --target="${aqt_install_path}" aqtinstall
+ WORKING_DIRECTORY "${base_path}"
+ RESULT_VARIABLE pip_res
+ ERROR_VARIABLE pip_err)
+ if (NOT pip_res EQUAL 0)
+ message(FATAL_ERROR "pip install aqtinstall failed: ${pip_err}")
+ endif()
+
+ execute_process(COMMAND "${CMAKE_COMMAND}" -E env PYTHONPATH="${aqt_install_path}" "${PYTHON3_EXECUTABLE}" -m aqt ${install_args}
+ WORKING_DIRECTORY "${base_path}"
+ RESULT_VARIABLE aqt_res
+ ERROR_VARIABLE aqt_err)
+ if (NOT aqt_res EQUAL 0)
+ message(FATAL_ERROR "aqt (python) failed: ${aqt_err}")
+ endif()
endif()
message(STATUS "Downloaded Qt binaries for ${target}:${host}:${type}:${arch}:${arch_path} to ${prefix}")
@@ -210,7 +221,7 @@ endfunction()
function(download_qt target)
determine_qt_parameters("${target}" host type arch arch_path host_type host_arch host_arch_path)
- get_external_prefix(qt base_path)
+ set(base_path "${CMAKE_BINARY_DIR}/externals/qt")
file(MAKE_DIRECTORY "${base_path}")
download_qt_configuration(prefix "${target}" "${host}" "${type}" "${arch}" "${arch_path}" "${base_path}")
@@ -227,26 +238,34 @@ function(download_qt target)
set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} PARENT_SCOPE)
endfunction()
-function(download_moltenvk)
-set(MOLTENVK_PLATFORM "macOS")
+function(download_moltenvk version platform)
+ if(NOT version)
+ message(FATAL_ERROR "download_moltenvk: version argument is required")
+ endif()
+ if(NOT platform)
+ message(FATAL_ERROR "download_moltenvk: platform argument is required")
+ endif()
-set(MOLTENVK_DIR "${CMAKE_BINARY_DIR}/externals/MoltenVK")
-set(MOLTENVK_TAR "${CMAKE_BINARY_DIR}/externals/MoltenVK.tar")
-if (NOT EXISTS ${MOLTENVK_DIR})
-if (NOT EXISTS ${MOLTENVK_TAR})
- file(DOWNLOAD https://github.com/KhronosGroup/MoltenVK/releases/download/v1.2.10-rc2/MoltenVK-all.tar
- ${MOLTENVK_TAR} SHOW_PROGRESS)
-endif()
+ set(MOLTENVK_DIR "${CMAKE_BINARY_DIR}/externals/MoltenVK")
+ set(MOLTENVK_TAR "${CMAKE_BINARY_DIR}/externals/MoltenVK.tar")
-execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf "${MOLTENVK_TAR}"
- WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/externals")
-endif()
+ if(NOT EXISTS "${MOLTENVK_DIR}")
+ if(NOT EXISTS "${MOLTENVK_TAR}")
+ file(DOWNLOAD "https://github.com/KhronosGroup/MoltenVK/releases/download/${version}/MoltenVK-${platform}.tar"
+ "${MOLTENVK_TAR}" SHOW_PROGRESS)
+ endif()
-# Add the MoltenVK library path to the prefix so find_library can locate it.
-list(APPEND CMAKE_PREFIX_PATH "${MOLTENVK_DIR}/MoltenVK/dylib/${MOLTENVK_PLATFORM}")
-set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} PARENT_SCOPE)
+ execute_process(
+ COMMAND ${CMAKE_COMMAND} -E tar xf "${MOLTENVK_TAR}"
+ WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/externals"
+ RESULT_VARIABLE tar_res
+ ERROR_VARIABLE tar_err
+ )
+ if(NOT tar_res EQUAL 0)
+ message(FATAL_ERROR "Extracting MoltenVK failed: ${tar_err}")
+ endif()
+ endif()
+ list(APPEND CMAKE_PREFIX_PATH "${MOLTENVK_DIR}/MoltenVK/dylib/${platform}")
+ set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} PARENT_SCOPE)
endfunction()
-function(get_external_prefix lib_name prefix_var)
- set(${prefix_var} "${CMAKE_BINARY_DIR}/externals/${lib_name}" PARENT_SCOPE)
-endfunction()
diff --git a/docs/Deps.md b/docs/Deps.md
index 0e7b7cff62..573d1fe14a 100644
--- a/docs/Deps.md
+++ b/docs/Deps.md
@@ -101,7 +101,7 @@ sudo pacman -Syu --needed base-devel boost catch2 cmake enet ffmpeg fmt git glsl
Ubuntu, Debian, Mint Linux
```sh
-sudo apt-get install autoconf cmake g++ gcc git glslang-tools libasound2 libboost-context-dev libglu1-mesa-dev libhidapi-dev libpulse-dev libtool libudev-dev libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-xinerama0 libxcb-xkb1 libxext-dev libxkbcommon-x11-0 mesa-common-dev nasm ninja-build qt6-base-private-dev libmbedtls-dev catch2 libfmt-dev liblz4-dev nlohmann-json3-dev libzstd-dev libssl-dev libavfilter-dev libavcodec-dev libswscale-dev pkg-config zlib1g-dev libva-dev libvdpau-dev qt6-tools-dev libzydis-dev zydis-tools libzycore-dev
+sudo apt-get install autoconf cmake g++ gcc git glslang-tools libasound2t64 libboost-context-dev libglu1-mesa-dev libhidapi-dev libpulse-dev libtool libudev-dev libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-xinerama0 libxcb-xkb1 libxext-dev libxkbcommon-x11-0 mesa-common-dev nasm ninja-build qt6-base-private-dev libmbedtls-dev catch2 libfmt-dev liblz4-dev nlohmann-json3-dev libzstd-dev libssl-dev libavfilter-dev libavcodec-dev libswscale-dev pkg-config zlib1g-dev libva-dev libvdpau-dev qt6-tools-dev libzydis-dev zydis-tools libzycore-dev vulkan-utility-libraries-dev libvulkan-dev spirv-tools spirv-headers libusb-1.0-0-dev libxbyak-dev
```
* Ubuntu 22.04, Linux Mint 20, or Debian 12 or later is required.
diff --git a/docs/Options.md b/docs/Options.md
index d19aab63f6..6af91e4918 100644
--- a/docs/Options.md
+++ b/docs/Options.md
@@ -31,7 +31,7 @@ Notes:
* Currently, build fails without this
- `YUZU_USE_FASTER_LD` (ON) Check if a faster linker is available
* Only available on UNIX
-- `USE_SYSTEM_MOLTENVK` (OFF, macOS only) Use the system MoltenVK lib (instead of the bundled one)
+- `YUZU_APPLE_USE_BUNDLED_MONTENVK` (ON, macOS only) Download bundled MoltenVK lib)
- `YUZU_TZDB_PATH` (string) Path to a pre-downloaded timezone database (useful for nixOS)
- `ENABLE_OPENSSL` (ON for Linux and *BSD) Enable OpenSSL backend for the ssl service
* Always enabled if the web service is enabled
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
index 2046af42c8..da40453497 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
@@ -68,6 +68,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
var isActivityRecreated = false
private lateinit var nfcReader: NfcReader
+ private var touchDownTime: Long = 0
+ private val maxTapDuration = 500L
+
private val gyro = FloatArray(3)
private val accel = FloatArray(3)
private var motionTimestamp: Long = 0
@@ -489,6 +492,38 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
}
}
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean {
+ val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragment_container) as? NavHostFragment
+ val emulationFragment = navHostFragment?.childFragmentManager?.fragments?.firstOrNull() as? org.yuzu.yuzu_emu.fragments.EmulationFragment
+
+ emulationFragment?.let { fragment ->
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ touchDownTime = System.currentTimeMillis()
+ // show overlay immediately on touch and cancel timer
+ if (!emulationViewModel.drawerOpen.value) {
+ fragment.handler.removeCallbacksAndMessages(null)
+ fragment.showOverlay()
+ }
+ }
+ MotionEvent.ACTION_UP -> {
+ if (!emulationViewModel.drawerOpen.value) {
+ val touchDuration = System.currentTimeMillis() - touchDownTime
+
+ if (touchDuration <= maxTapDuration) {
+ fragment.handleScreenTap(false)
+ } else {
+ // just start the auto-hide timer without toggling visibility
+ fragment.handleScreenTap(true)
+ }
+ }
+ }
+ }
+ }
+
+ return super.dispatchTouchEvent(event)
+ }
+
fun onEmulationStarted() {
emulationViewModel.setEmulationStarted(true)
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
index 3c5b9003de..638e1101db 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
@@ -55,6 +55,8 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
FRAME_INTERPOLATION("frame_interpolation"),
// FRAME_SKIPPING("frame_skipping"),
+ ENABLE_INPUT_OVERLAY_AUTO_HIDE("enable_input_overlay_auto_hide"),
+
PERF_OVERLAY_BACKGROUND("perf_overlay_background"),
SHOW_PERFORMANCE_OVERLAY("show_performance_overlay"),
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
index 21aad8b5d1..d5556a337b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
@@ -59,7 +59,8 @@ enum class IntSetting(override val key: String) : AbstractIntSetting {
OFFLINE_WEB_APPLET("offline_web_applet_mode"),
LOGIN_SHARE_APPLET("login_share_applet_mode"),
WIFI_WEB_AUTH_APPLET("wifi_web_auth_applet_mode"),
- MY_PAGE_APPLET("my_page_applet_mode")
+ MY_PAGE_APPLET("my_page_applet_mode"),
+ INPUT_OVERLAY_AUTO_HIDE("input_overlay_auto_hide")
;
override fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
index a52f582031..454d0e4807 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
@@ -12,6 +12,7 @@ object Settings {
SECTION_SYSTEM(R.string.preferences_system),
SECTION_RENDERER(R.string.preferences_graphics),
SECTION_PERFORMANCE_STATS(R.string.stats_overlay_options),
+ SECTION_INPUT_OVERLAY(R.string.input_overlay_options),
SECTION_SOC_OVERLAY(R.string.soc_overlay_options),
SECTION_AUDIO(R.string.preferences_audio),
SECTION_INPUT(R.string.preferences_controls),
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
index 1f2ba81a73..5f7f7a43f9 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
@@ -96,6 +96,7 @@ abstract class SettingsItem(
const val TYPE_INT_SINGLE_CHOICE = 9
const val TYPE_INPUT_PROFILE = 10
const val TYPE_STRING_INPUT = 11
+ const val TYPE_SPINBOX = 12
const val FASTMEM_COMBINED = "fastmem_combined"
@@ -385,6 +386,22 @@ abstract class SettingsItem(
warningMessage = R.string.warning_resolution
)
)
+ put(
+ SwitchSetting(
+ BooleanSetting.ENABLE_INPUT_OVERLAY_AUTO_HIDE,
+ titleId = R.string.enable_input_overlay_auto_hide,
+ )
+ )
+ put(
+ SpinBoxSetting(
+ IntSetting.INPUT_OVERLAY_AUTO_HIDE,
+ titleId = R.string.overlay_auto_hide,
+ descriptionId = R.string.overlay_auto_hide_description,
+ min = 1,
+ max = 999,
+ valueHint = R.string.seconds
+ )
+ )
put(
SwitchSetting(
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SpinBoxSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SpinBoxSetting.kt
new file mode 100644
index 0000000000..0b0d01dfe0
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SpinBoxSetting.kt
@@ -0,0 +1,41 @@
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model.view
+
+import androidx.annotation.StringRes
+import org.yuzu.yuzu_emu.features.settings.model.AbstractByteSetting
+import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting
+import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
+import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
+import org.yuzu.yuzu_emu.features.settings.model.AbstractShortSetting
+
+class SpinBoxSetting(
+ setting: AbstractSetting,
+ @StringRes titleId: Int = 0,
+ titleString: String = "",
+ @StringRes descriptionId: Int = 0,
+ descriptionString: String = "",
+ val valueHint: Int,
+ val min: Int,
+ val max: Int
+) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
+ override val type = TYPE_SPINBOX
+
+ fun getSelectedValue(needsGlobal: Boolean = false) =
+ when (setting) {
+ is AbstractByteSetting -> setting.getByte(needsGlobal).toInt()
+ is AbstractShortSetting -> setting.getShort(needsGlobal).toInt()
+ is AbstractIntSetting -> setting.getInt(needsGlobal)
+ is AbstractFloatSetting -> setting.getFloat(needsGlobal).toInt()
+ else -> 0
+ }
+
+ fun setSelectedValue(value: Int) =
+ when (setting) {
+ is AbstractByteSetting -> setting.setByte(value.toByte())
+ is AbstractShortSetting -> setting.setShort(value.toShort())
+ is AbstractFloatSetting -> setting.setFloat(value.toFloat())
+ else -> (setting as AbstractIntSetting).setInt(value)
+ }
+}
\ No newline at end of file
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
index 500ac6e66e..bdc51b7070 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui
@@ -61,6 +61,10 @@ class SettingsAdapter(
SliderViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
+ SettingsItem.TYPE_SPINBOX -> {
+ SpinBoxViewHolder(ListItemSettingBinding.inflate(inflater), this)
+ }
+
SettingsItem.TYPE_SUBMENU -> {
SubmenuViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
@@ -191,6 +195,14 @@ class SettingsAdapter(
position
).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
}
+ fun onSpinBoxClick(item: SpinBoxSetting, position: Int) {
+ SettingsDialogFragment.newInstance(
+ settingsViewModel,
+ item,
+ SettingsItem.TYPE_SPINBOX,
+ position
+ ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
+ }
fun onSubmenuClick(item: SubmenuSetting) {
val action = SettingsNavigationDirections.actionGlobalSettingsFragment(item.menuKey, null)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt
index dc9f561eca..2a97f15892 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt
@@ -14,6 +14,7 @@ import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
@@ -22,6 +23,7 @@ import com.google.android.material.slider.Slider
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogEditTextBinding
import org.yuzu.yuzu_emu.databinding.DialogSliderBinding
+import org.yuzu.yuzu_emu.databinding.DialogSpinboxBinding
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.input.model.AnalogDirection
import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting
@@ -30,6 +32,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.IntSingleChoiceSetting
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting
import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting
+import org.yuzu.yuzu_emu.features.settings.model.view.SpinBoxSetting
import org.yuzu.yuzu_emu.features.settings.model.view.StringInputSetting
import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting
import org.yuzu.yuzu_emu.utils.ParamPackage
@@ -46,6 +49,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
private lateinit var sliderBinding: DialogSliderBinding
private lateinit var stringInputBinding: DialogEditTextBinding
+ private lateinit var spinboxBinding: DialogSpinboxBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -142,6 +146,76 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
.create()
}
+ SettingsItem.TYPE_SPINBOX -> {
+ spinboxBinding = DialogSpinboxBinding.inflate(layoutInflater)
+ val item = settingsViewModel.clickedItem as SpinBoxSetting
+
+ val currentValue = item.getSelectedValue()
+ spinboxBinding.editValue.setText(currentValue.toString())
+ spinboxBinding.textInputLayout.hint = getString(item.valueHint)
+
+ val dialog = MaterialAlertDialogBuilder(requireContext())
+ .setTitle(item.title)
+ .setView(spinboxBinding.root)
+ .setPositiveButton(android.R.string.ok, this)
+ .setNegativeButton(android.R.string.cancel, defaultCancelListener)
+ .create()
+
+ val updateButtonState = { enabled: Boolean ->
+ dialog.setOnShowListener { dialogInterface ->
+ (dialogInterface as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)?.isEnabled = enabled
+ }
+ if (dialog.isShowing) {
+ dialog.getButton(DialogInterface.BUTTON_POSITIVE)?.isEnabled = enabled
+ }
+ }
+
+ val updateValidity = { value: Int ->
+ val isValid = value in item.min..item.max
+ if (isValid) {
+ spinboxBinding.textInputLayout.error = null
+ } else {
+ spinboxBinding.textInputLayout.error = getString(
+ if (value < item.min) R.string.value_too_low else R.string.value_too_high,
+ if (value < item.min) item.min else item.max
+ )
+ }
+ updateButtonState(isValid)
+ }
+
+ spinboxBinding.buttonDecrement.setOnClickListener {
+ val current = spinboxBinding.editValue.text.toString().toIntOrNull() ?: currentValue
+ val newValue = current - 1
+ spinboxBinding.editValue.setText(newValue.toString())
+ updateValidity(newValue)
+ }
+
+ spinboxBinding.buttonIncrement.setOnClickListener {
+ val current = spinboxBinding.editValue.text.toString().toIntOrNull() ?: currentValue
+ val newValue = current + 1
+ spinboxBinding.editValue.setText(newValue.toString())
+ updateValidity(newValue)
+ }
+
+ spinboxBinding.editValue.addTextChangedListener(object : TextWatcher {
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
+ override fun afterTextChanged(s: Editable?) {
+ val value = s.toString().toIntOrNull()
+ if (value != null) {
+ updateValidity(value)
+ } else {
+ spinboxBinding.textInputLayout.error = getString(R.string.invalid_value)
+ updateButtonState(false)
+ }
+ }
+ })
+
+ updateValidity(currentValue)
+
+ dialog
+ }
+
SettingsItem.TYPE_STRING_INPUT -> {
stringInputBinding = DialogEditTextBinding.inflate(layoutInflater)
val item = settingsViewModel.clickedItem as StringInputSetting
@@ -281,6 +355,14 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
sliderSetting.setSelectedValue(settingsViewModel.sliderProgress.value)
}
+ is SpinBoxSetting -> {
+ val spinBoxSetting = settingsViewModel.clickedItem as SpinBoxSetting
+ val value = spinboxBinding.editValue.text.toString().toIntOrNull()
+ if (value != null && value in spinBoxSetting.min..spinBoxSetting.max) {
+ spinBoxSetting.setSelectedValue(value)
+ }
+ }
+
is StringInputSetting -> {
val stringInputSetting = settingsViewModel.clickedItem as StringInputSetting
stringInputSetting.setSelectedValue(
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
index 14d62ceec3..715baec72f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
@@ -97,6 +97,7 @@ class SettingsFragmentPresenter(
MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl)
MenuTag.SECTION_PERFORMANCE_STATS -> addPerformanceOverlaySettings(sl)
MenuTag.SECTION_SOC_OVERLAY -> addSocOverlaySettings(sl)
+ MenuTag.SECTION_INPUT_OVERLAY -> addInputOverlaySettings(sl)
MenuTag.SECTION_AUDIO -> addAudioSettings(sl)
MenuTag.SECTION_INPUT -> addInputSettings(sl)
MenuTag.SECTION_INPUT_PLAYER_ONE -> addInputPlayer(sl, 0)
@@ -156,6 +157,14 @@ class SettingsFragmentPresenter(
menuKey = MenuTag.SECTION_SOC_OVERLAY
)
)
+ add(
+ SubmenuSetting(
+ titleId = R.string.input_overlay_options,
+ iconId = R.drawable.ic_controller,
+ descriptionId = R.string.input_overlay_options_description,
+ menuKey = MenuTag.SECTION_INPUT_OVERLAY
+ )
+ )
}
add(
SubmenuSetting(
@@ -264,6 +273,13 @@ class SettingsFragmentPresenter(
}
}
+ private fun addInputOverlaySettings(sl: ArrayList) {
+ sl.apply {
+ add(BooleanSetting.ENABLE_INPUT_OVERLAY_AUTO_HIDE.key)
+ add(IntSetting.INPUT_OVERLAY_AUTO_HIDE.key)
+ }
+ }
+
private fun addSocOverlaySettings(sl: ArrayList) {
sl.apply {
add(HeaderSetting(R.string.stats_overlay_customization))
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SpinBoxViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SpinBoxViewHolder.kt
new file mode 100644
index 0000000000..1f9a16b798
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SpinBoxViewHolder.kt
@@ -0,0 +1,44 @@
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.ui.viewholder
+
+import android.view.View
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
+import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
+import org.yuzu.yuzu_emu.features.settings.model.view.SpinBoxSetting
+import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
+import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
+
+class SpinBoxViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
+ SettingViewHolder(binding.root, adapter) {
+ private lateinit var setting: SpinBoxSetting
+
+ override fun bind(item: SettingsItem) {
+ setting = item as SpinBoxSetting
+ binding.textSettingName.text = setting.title
+ binding.textSettingDescription.setVisible(item.description.isNotEmpty())
+ binding.textSettingDescription.text = setting.description
+ binding.textSettingValue.setVisible(true)
+ binding.textSettingValue.text = setting.getSelectedValue().toString()
+
+ binding.buttonClear.setVisible(setting.clearable)
+ binding.buttonClear.setOnClickListener {
+ adapter.onClearClick(setting, bindingAdapterPosition)
+ }
+
+ setStyle(setting.isEditable, binding)
+ }
+ override fun onClick(clicked: View) {
+ if (setting.isEditable) {
+ adapter.onSpinBoxClick(setting, bindingAdapterPosition)
+ }
+ }
+ override fun onLongClick(clicked: View): Boolean {
+ if (setting.isEditable) {
+ return adapter.onLongClick(setting, bindingAdapterPosition)
+ }
+ return false
+ }
+}
\ No newline at end of file
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
index 8162098caa..b2d6135372 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
@@ -96,6 +96,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private var perfStatsUpdater: (() -> Unit)? = null
private var socUpdater: (() -> Unit)? = null
+ val handler = Handler(Looper.getMainLooper())
+ private var isOverlayVisible = true
+
private var _binding: FragmentEmulationBinding? = null
private val binding get() = _binding!!
@@ -452,7 +455,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
/**
* Ask user if they want to launch with default settings when custom settings fail
*/
- private suspend fun askUserToLaunchWithDefaultSettings(gameTitle: String, errorMessage: String): Boolean {
+ private suspend fun askUserToLaunchWithDefaultSettings(
+ gameTitle: String,
+ errorMessage: String
+ ): Boolean {
return suspendCoroutine { continuation ->
requireActivity().runOnUiThread {
MaterialAlertDialogBuilder(requireContext())
@@ -728,6 +734,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
updateShowStatsOverlay()
updateSocOverlay()
+ initializeOverlayAutoHide()
+
// Re update binding when the specs values get initialized properly
binding.inGameMenu.getHeaderView(0).apply {
val titleView = findViewById(R.id.text_game_title)
@@ -917,6 +925,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
updatePauseMenuEntry(emulationState.isPaused)
}
}
+
+ // if the overlay auto-hide setting is changed while paused,
+ // we need to reinitialize the auto-hide timer
+ initializeOverlayAutoHide()
+
}
private fun resetInputOverlay() {
@@ -924,6 +937,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
IntSetting.OVERLAY_OPACITY.reset()
binding.surfaceInputOverlay.post {
binding.surfaceInputOverlay.resetLayoutVisibilityAndPlacement()
+ binding.surfaceInputOverlay.resetIndividualControlScale()
}
}
@@ -1034,7 +1048,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
val status = batteryIntent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
- status == BatteryManager.BATTERY_STATUS_FULL
+ status == BatteryManager.BATTERY_STATUS_FULL
if (isCharging) {
sb.append(" ${getString(R.string.charging)}")
@@ -1546,6 +1560,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
.setNeutralButton(R.string.slider_default) { _: DialogInterface?, _: Int ->
setControlScale(50)
setControlOpacity(100)
+ binding.surfaceInputOverlay.resetIndividualControlScale()
}
.show()
}
@@ -1726,4 +1741,61 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private val perfStatsUpdateHandler = Handler(Looper.myLooper()!!)
private val socUpdateHandler = Handler(Looper.myLooper()!!)
}
-}
\ No newline at end of file
+
+ private fun startOverlayAutoHideTimer(seconds: Int) {
+ handler.removeCallbacksAndMessages(null)
+
+ handler.postDelayed({
+ if (isOverlayVisible) {
+ hideOverlay()
+ }
+ }, seconds * 1000L)
+ }
+
+ fun handleScreenTap(isLongTap: Boolean) {
+ val autoHideSeconds = IntSetting.INPUT_OVERLAY_AUTO_HIDE.getInt()
+ val shouldProceed = BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean() && BooleanSetting.ENABLE_INPUT_OVERLAY_AUTO_HIDE.getBoolean()
+
+ if (!shouldProceed) {
+ return
+ }
+
+ // failsafe
+ if (autoHideSeconds == 0) {
+ showOverlay()
+ return
+ }
+
+ if (!isOverlayVisible && !isLongTap) {
+ showOverlay()
+ }
+
+ startOverlayAutoHideTimer(autoHideSeconds)
+ }
+
+ private fun initializeOverlayAutoHide() {
+ val autoHideSeconds = IntSetting.INPUT_OVERLAY_AUTO_HIDE.getInt()
+ val autoHideEnabled = BooleanSetting.ENABLE_INPUT_OVERLAY_AUTO_HIDE.getBoolean()
+ val showOverlay = BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean()
+
+ if (autoHideEnabled && showOverlay) {
+ showOverlay()
+ startOverlayAutoHideTimer(autoHideSeconds)
+ }
+ }
+
+
+ fun showOverlay() {
+ if (!isOverlayVisible) {
+ isOverlayVisible = true
+ ViewUtils.showView(binding.surfaceInputOverlay, 500)
+ }
+ }
+
+ private fun hideOverlay() {
+ if (isOverlayVisible) {
+ isOverlayVisible = false
+ ViewUtils.hideView(binding.surfaceInputOverlay, 500)
+ }
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
index 737e035840..9f050a5053 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.overlay
@@ -13,6 +13,8 @@ import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.graphics.drawable.VectorDrawable
import android.os.Build
+import android.os.Handler
+import android.os.Looper
import android.util.AttributeSet
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
@@ -52,6 +54,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
private var dpadBeingConfigured: InputOverlayDrawableDpad? = null
private var joystickBeingConfigured: InputOverlayDrawableJoystick? = null
+ private var scaleDialog: OverlayScaleDialog? = null
+ private var touchStartX = 0f
+ private var touchStartY = 0f
+ private var hasMoved = false
+ private val moveThreshold = 20f
+
private lateinit var windowInsets: WindowInsets
var layout = OverlayLayout.Landscape
@@ -254,23 +262,44 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
) {
buttonBeingConfigured = button
buttonBeingConfigured!!.onConfigureTouch(event)
+ touchStartX = event.getX(pointerIndex)
+ touchStartY = event.getY(pointerIndex)
+ hasMoved = false
}
MotionEvent.ACTION_MOVE -> if (buttonBeingConfigured != null) {
- buttonBeingConfigured!!.onConfigureTouch(event)
- invalidate()
- return true
+ val moveDistance = kotlin.math.sqrt(
+ (event.getX(pointerIndex) - touchStartX).let { it * it } +
+ (event.getY(pointerIndex) - touchStartY).let { it * it }
+ )
+
+ if (moveDistance > moveThreshold) {
+ hasMoved = true
+ buttonBeingConfigured!!.onConfigureTouch(event)
+ invalidate()
+ return true
+ }
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> if (buttonBeingConfigured === button) {
- // Persist button position by saving new place.
- saveControlPosition(
- buttonBeingConfigured!!.overlayControlData.id,
- buttonBeingConfigured!!.bounds.centerX(),
- buttonBeingConfigured!!.bounds.centerY(),
- layout
- )
+ if (!hasMoved) {
+ showScaleDialog(
+ buttonBeingConfigured,
+ null,
+ null,
+ fingerPositionX,
+ fingerPositionY
+ )
+ } else {
+ saveControlPosition(
+ buttonBeingConfigured!!.overlayControlData.id,
+ buttonBeingConfigured!!.bounds.centerX(),
+ buttonBeingConfigured!!.bounds.centerY(),
+ individuaScale = buttonBeingConfigured!!.overlayControlData.individualScale,
+ layout
+ )
+ }
buttonBeingConfigured = null
}
}
@@ -287,23 +316,46 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
) {
dpadBeingConfigured = dpad
dpadBeingConfigured!!.onConfigureTouch(event)
+ touchStartX = event.getX(pointerIndex)
+ touchStartY = event.getY(pointerIndex)
+ hasMoved = false
}
MotionEvent.ACTION_MOVE -> if (dpadBeingConfigured != null) {
- dpadBeingConfigured!!.onConfigureTouch(event)
- invalidate()
- return true
+ val moveDistance = kotlin.math.sqrt(
+ (event.getX(pointerIndex) - touchStartX).let { it * it } +
+ (event.getY(pointerIndex) - touchStartY).let { it * it }
+ )
+
+ if (moveDistance > moveThreshold) {
+ hasMoved = true
+ dpadBeingConfigured!!.onConfigureTouch(event)
+ invalidate()
+ return true
+ }
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> if (dpadBeingConfigured === dpad) {
- // Persist button position by saving new place.
- saveControlPosition(
- OverlayControl.COMBINED_DPAD.id,
- dpadBeingConfigured!!.bounds.centerX(),
- dpadBeingConfigured!!.bounds.centerY(),
- layout
- )
+ if (!hasMoved) {
+ // This was a click, show scale dialog for dpad
+ showScaleDialog(
+ null,
+ dpadBeingConfigured,
+ null,
+ fingerPositionX,
+ fingerPositionY
+ )
+ } else {
+ // This was a move, save position
+ saveControlPosition(
+ OverlayControl.COMBINED_DPAD.id,
+ dpadBeingConfigured!!.bounds.centerX(),
+ dpadBeingConfigured!!.bounds.centerY(),
+ individuaScale = dpadBeingConfigured!!.individualScale,
+ layout
+ )
+ }
dpadBeingConfigured = null
}
}
@@ -317,21 +369,43 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
) {
joystickBeingConfigured = joystick
joystickBeingConfigured!!.onConfigureTouch(event)
+ touchStartX = event.getX(pointerIndex)
+ touchStartY = event.getY(pointerIndex)
+ hasMoved = false
}
MotionEvent.ACTION_MOVE -> if (joystickBeingConfigured != null) {
- joystickBeingConfigured!!.onConfigureTouch(event)
- invalidate()
+ val moveDistance = kotlin.math.sqrt(
+ (event.getX(pointerIndex) - touchStartX).let { it * it } +
+ (event.getY(pointerIndex) - touchStartY).let { it * it }
+ )
+
+ if (moveDistance > moveThreshold) {
+ hasMoved = true
+ joystickBeingConfigured!!.onConfigureTouch(event)
+ invalidate()
+ }
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> if (joystickBeingConfigured != null) {
- saveControlPosition(
- joystickBeingConfigured!!.prefId,
- joystickBeingConfigured!!.bounds.centerX(),
- joystickBeingConfigured!!.bounds.centerY(),
- layout
- )
+ if (!hasMoved) {
+ showScaleDialog(
+ null,
+ null,
+ joystickBeingConfigured,
+ fingerPositionX,
+ fingerPositionY
+ )
+ } else {
+ saveControlPosition(
+ joystickBeingConfigured!!.prefId,
+ joystickBeingConfigured!!.bounds.centerX(),
+ joystickBeingConfigured!!.bounds.centerY(),
+ individuaScale = joystickBeingConfigured!!.individualScale,
+ layout
+ )
+ }
joystickBeingConfigured = null
}
}
@@ -607,25 +681,117 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
invalidate()
}
- private fun saveControlPosition(id: String, x: Int, y: Int, layout: OverlayLayout) {
+ private fun saveControlPosition(
+ id: String,
+ x: Int,
+ y: Int,
+ individuaScale: Float,
+ layout: OverlayLayout
+ ) {
val windowSize = getSafeScreenSize(context, Pair(measuredWidth, measuredHeight))
val min = windowSize.first
val max = windowSize.second
val overlayControlData = NativeConfig.getOverlayControlData()
val data = overlayControlData.firstOrNull { it.id == id }
val newPosition = Pair((x - min.x).toDouble() / max.x, (y - min.y).toDouble() / max.y)
+
when (layout) {
OverlayLayout.Landscape -> data?.landscapePosition = newPosition
OverlayLayout.Portrait -> data?.portraitPosition = newPosition
OverlayLayout.Foldable -> data?.foldablePosition = newPosition
+
}
+
+ data?.individualScale = individuaScale
+
NativeConfig.setOverlayControlData(overlayControlData)
}
fun setIsInEditMode(editMode: Boolean) {
inEditMode = editMode
+ if (!editMode) {
+ scaleDialog?.dismiss()
+ scaleDialog = null
+ }
}
+ private fun showScaleDialog(
+ button: InputOverlayDrawableButton?,
+ dpad: InputOverlayDrawableDpad?,
+ joystick: InputOverlayDrawableJoystick?,
+ x: Int, y: Int
+ ) {
+ val overlayControlData = NativeConfig.getOverlayControlData()
+ // prevent dialog from being spam opened
+ scaleDialog?.dismiss()
+
+
+ when {
+ button != null -> {
+ val buttonData =
+ overlayControlData.firstOrNull { it.id == button.overlayControlData.id }
+ if (buttonData != null) {
+ scaleDialog =
+ OverlayScaleDialog(context, button.overlayControlData) { newScale ->
+ saveControlPosition(
+ button.overlayControlData.id,
+ button.bounds.centerX(),
+ button.bounds.centerY(),
+ individuaScale = newScale,
+ layout
+ )
+ refreshControls()
+ }
+
+ scaleDialog?.showDialog(x,y, button.bounds.width(), button.bounds.height())
+
+ }
+ }
+
+ dpad != null -> {
+ val dpadData =
+ overlayControlData.firstOrNull { it.id == OverlayControl.COMBINED_DPAD.id }
+ if (dpadData != null) {
+ scaleDialog = OverlayScaleDialog(context, dpadData) { newScale ->
+ saveControlPosition(
+ OverlayControl.COMBINED_DPAD.id,
+ dpad.bounds.centerX(),
+ dpad.bounds.centerY(),
+ newScale,
+ layout
+ )
+
+ refreshControls()
+ }
+
+ scaleDialog?.showDialog(x,y, dpad.bounds.width(), dpad.bounds.height())
+
+ }
+ }
+
+ joystick != null -> {
+ val joystickData = overlayControlData.firstOrNull { it.id == joystick.prefId }
+ if (joystickData != null) {
+ scaleDialog = OverlayScaleDialog(context, joystickData) { newScale ->
+ saveControlPosition(
+ joystick.prefId,
+ joystick.bounds.centerX(),
+ joystick.bounds.centerY(),
+ individuaScale = newScale,
+ layout
+ )
+
+ refreshControls()
+ }
+
+ scaleDialog?.showDialog(x,y, joystick.bounds.width(), joystick.bounds.height())
+
+ }
+ }
+ }
+ }
+
+
/**
* Applies and saves all default values for the overlay
*/
@@ -664,12 +830,24 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
val overlayControlData = NativeConfig.getOverlayControlData()
overlayControlData.forEach {
it.enabled = OverlayControl.from(it.id)?.defaultVisibility == true
+ it.individualScale = OverlayControl.from(it.id)?.defaultIndividualScaleResource!!
}
NativeConfig.setOverlayControlData(overlayControlData)
refreshControls()
}
+ fun resetIndividualControlScale() {
+ val overlayControlData = NativeConfig.getOverlayControlData()
+ overlayControlData.forEach { data ->
+ val defaultControlData = OverlayControl.from(data.id) ?: return@forEach
+ data.individualScale = defaultControlData.defaultIndividualScaleResource
+ }
+ NativeConfig.setOverlayControlData(overlayControlData)
+ NativeConfig.saveGlobalConfig()
+ refreshControls()
+ }
+
private fun defaultOverlayPositionByLayout(layout: OverlayLayout) {
val overlayControlData = NativeConfig.getOverlayControlData()
for (data in overlayControlData) {
@@ -860,6 +1038,9 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
scale *= (IntSetting.OVERLAY_SCALE.getInt() + 50).toFloat()
scale /= 100f
+ // Apply individual scale
+ scale *= overlayControlData.individualScale
+
// Initialize the InputOverlayDrawableButton.
val defaultStateBitmap = getBitmap(context, defaultResId, scale)
val pressedStateBitmap = getBitmap(context, pressedResId, scale)
@@ -922,11 +1103,20 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
// Resources handle for fetching the initial Drawable resource.
val res = context.resources
+ // Get the dpad control data for individual scale
+ val overlayControlData = NativeConfig.getOverlayControlData()
+ val dpadData = overlayControlData.firstOrNull { it.id == OverlayControl.COMBINED_DPAD.id }
+
// Decide scale based on button ID and user preference
var scale = 0.25f
scale *= (IntSetting.OVERLAY_SCALE.getInt() + 50).toFloat()
scale /= 100f
+ // Apply individual scale
+ if (dpadData != null) {
+ scale *= dpadData.individualScale
+ }
+
// Initialize the InputOverlayDrawableDpad.
val defaultStateBitmap =
getBitmap(context, defaultResId, scale)
@@ -1000,6 +1190,9 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
scale *= (IntSetting.OVERLAY_SCALE.getInt() + 50).toFloat()
scale /= 100f
+ // Apply individual scale
+ scale *= overlayControlData.individualScale
+
// Initialize the InputOverlayDrawableJoystick.
val bitmapOuter = getBitmap(context, resOuter, scale)
val bitmapInnerDefault = getBitmap(context, defaultResInner, 1.0f)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt
index 0cb6ff2440..01f07e4f36 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableDpad.kt
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.overlay
@@ -42,6 +42,8 @@ class InputOverlayDrawableDpad(
val width: Int
val height: Int
+ var individualScale: Float = 1.0f
+
private val defaultStateBitmap: BitmapDrawable
private val pressedOneDirectionStateBitmap: BitmapDrawable
private val pressedTwoDirectionsStateBitmap: BitmapDrawable
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt
index 4b07107fca..bc3ff15b21 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlayDrawableJoystick.kt
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.overlay
@@ -51,6 +51,8 @@ class InputOverlayDrawableJoystick(
val width: Int
val height: Int
+ var individualScale: Float = 1.0f
+
private var opacity: Int = 0
private var virtBounds: Rect
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/OverlayScaleDialog.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/OverlayScaleDialog.kt
new file mode 100644
index 0000000000..f489ef3b7c
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/OverlayScaleDialog.kt
@@ -0,0 +1,124 @@
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package org.yuzu.yuzu_emu.overlay
+
+import android.app.Dialog
+import android.content.Context
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.WindowManager
+import android.widget.TextView
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.slider.Slider
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.overlay.model.OverlayControlData
+
+class OverlayScaleDialog(
+ context: Context,
+ private val overlayControlData: OverlayControlData,
+ private val onScaleChanged: (Float) -> Unit
+) : Dialog(context) {
+
+ private var currentScale = overlayControlData.individualScale
+ private val originalScale = overlayControlData.individualScale
+ private lateinit var scaleValueText: TextView
+ private lateinit var scaleSlider: Slider
+
+ init {
+ setupDialog()
+ }
+
+ private fun setupDialog() {
+ val view = LayoutInflater.from(context).inflate(R.layout.dialog_overlay_scale, null)
+ setContentView(view)
+
+ window?.setBackgroundDrawable(null)
+
+ window?.apply {
+ attributes = attributes.apply {
+ flags = flags and WindowManager.LayoutParams.FLAG_DIM_BEHIND.inv()
+ flags = flags or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+ }
+ }
+
+ scaleValueText = view.findViewById(R.id.scaleValueText)
+ scaleSlider = view.findViewById(R.id.scaleSlider)
+ val resetButton = view.findViewById(R.id.resetButton)
+ val confirmButton = view.findViewById(R.id.confirmButton)
+ val cancelButton = view.findViewById(R.id.cancelButton)
+
+ scaleValueText.text = String.format("%.1fx", currentScale)
+ scaleSlider.value = currentScale
+
+ scaleSlider.addOnChangeListener { _, value, input ->
+ if (input) {
+ currentScale = value
+ scaleValueText.text = String.format("%.1fx", currentScale)
+ }
+ }
+
+ scaleSlider.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
+ override fun onStartTrackingTouch(slider: Slider) {
+ // pass
+ }
+
+ override fun onStopTrackingTouch(slider: Slider) {
+ onScaleChanged(currentScale)
+ }
+ })
+
+ resetButton.setOnClickListener {
+ currentScale = 1.0f
+ scaleSlider.value = 1.0f
+ scaleValueText.text = String.format("%.1fx", currentScale)
+ onScaleChanged(currentScale)
+ }
+
+ confirmButton.setOnClickListener {
+ overlayControlData.individualScale = currentScale
+ //slider value is already saved on touch dispatch but just to be sure
+ onScaleChanged(currentScale)
+ dismiss()
+ }
+
+ // both cancel button and back gesture should revert the scale change
+ cancelButton.setOnClickListener {
+ onScaleChanged(originalScale)
+ dismiss()
+ }
+
+ setOnCancelListener {
+ onScaleChanged(originalScale)
+ dismiss()
+ }
+ }
+
+ fun showDialog(anchorX: Int, anchorY: Int, anchorHeight: Int, anchorWidth: Int) {
+ show()
+
+ show()
+
+ // TODO: this calculation is a bit rough, improve it later on
+ window?.let { window ->
+ val layoutParams = window.attributes
+ layoutParams.gravity = Gravity.TOP or Gravity.START
+
+ val density = context.resources.displayMetrics.density
+ val dialogWidthPx = (320 * density).toInt()
+ val dialogHeightPx = (400 * density).toInt() // set your estimated dialog height
+
+ val screenHeight = context.resources.displayMetrics.heightPixels
+
+
+ layoutParams.x = anchorX + anchorWidth / 2 - dialogWidthPx / 2
+ layoutParams.y = anchorY + anchorHeight / 2 - dialogHeightPx / 2
+ layoutParams.width = dialogWidthPx
+
+
+ window.attributes = layoutParams
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/model/OverlayControl.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/model/OverlayControl.kt
index a0eeadf4bc..10cc547d0b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/model/OverlayControl.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/model/OverlayControl.kt
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.overlay.model
@@ -12,126 +12,144 @@ enum class OverlayControl(
val defaultVisibility: Boolean,
@IntegerRes val defaultLandscapePositionResources: Pair,
@IntegerRes val defaultPortraitPositionResources: Pair,
- @IntegerRes val defaultFoldablePositionResources: Pair
+ @IntegerRes val defaultFoldablePositionResources: Pair,
+ val defaultIndividualScaleResource: Float,
) {
BUTTON_A(
"button_a",
true,
Pair(R.integer.BUTTON_A_X, R.integer.BUTTON_A_Y),
Pair(R.integer.BUTTON_A_X_PORTRAIT, R.integer.BUTTON_A_Y_PORTRAIT),
- Pair(R.integer.BUTTON_A_X_FOLDABLE, R.integer.BUTTON_A_Y_FOLDABLE)
+ Pair(R.integer.BUTTON_A_X_FOLDABLE, R.integer.BUTTON_A_Y_FOLDABLE),
+ 1.0f
),
BUTTON_B(
"button_b",
true,
Pair(R.integer.BUTTON_B_X, R.integer.BUTTON_B_Y),
Pair(R.integer.BUTTON_B_X_PORTRAIT, R.integer.BUTTON_B_Y_PORTRAIT),
- Pair(R.integer.BUTTON_B_X_FOLDABLE, R.integer.BUTTON_B_Y_FOLDABLE)
+ Pair(R.integer.BUTTON_B_X_FOLDABLE, R.integer.BUTTON_B_Y_FOLDABLE),
+ 1.0f
),
BUTTON_X(
"button_x",
true,
Pair(R.integer.BUTTON_X_X, R.integer.BUTTON_X_Y),
Pair(R.integer.BUTTON_X_X_PORTRAIT, R.integer.BUTTON_X_Y_PORTRAIT),
- Pair(R.integer.BUTTON_X_X_FOLDABLE, R.integer.BUTTON_X_Y_FOLDABLE)
+ Pair(R.integer.BUTTON_X_X_FOLDABLE, R.integer.BUTTON_X_Y_FOLDABLE),
+ 1.0f
),
BUTTON_Y(
"button_y",
true,
Pair(R.integer.BUTTON_Y_X, R.integer.BUTTON_Y_Y),
Pair(R.integer.BUTTON_Y_X_PORTRAIT, R.integer.BUTTON_Y_Y_PORTRAIT),
- Pair(R.integer.BUTTON_Y_X_FOLDABLE, R.integer.BUTTON_Y_Y_FOLDABLE)
+ Pair(R.integer.BUTTON_Y_X_FOLDABLE, R.integer.BUTTON_Y_Y_FOLDABLE),
+ 1.0f
),
BUTTON_PLUS(
"button_plus",
true,
Pair(R.integer.BUTTON_PLUS_X, R.integer.BUTTON_PLUS_Y),
Pair(R.integer.BUTTON_PLUS_X_PORTRAIT, R.integer.BUTTON_PLUS_Y_PORTRAIT),
- Pair(R.integer.BUTTON_PLUS_X_FOLDABLE, R.integer.BUTTON_PLUS_Y_FOLDABLE)
+ Pair(R.integer.BUTTON_PLUS_X_FOLDABLE, R.integer.BUTTON_PLUS_Y_FOLDABLE),
+ 1.0f
),
BUTTON_MINUS(
"button_minus",
true,
Pair(R.integer.BUTTON_MINUS_X, R.integer.BUTTON_MINUS_Y),
Pair(R.integer.BUTTON_MINUS_X_PORTRAIT, R.integer.BUTTON_MINUS_Y_PORTRAIT),
- Pair(R.integer.BUTTON_MINUS_X_FOLDABLE, R.integer.BUTTON_MINUS_Y_FOLDABLE)
+ Pair(R.integer.BUTTON_MINUS_X_FOLDABLE, R.integer.BUTTON_MINUS_Y_FOLDABLE),
+ 1.0f
),
BUTTON_HOME(
"button_home",
false,
Pair(R.integer.BUTTON_HOME_X, R.integer.BUTTON_HOME_Y),
Pair(R.integer.BUTTON_HOME_X_PORTRAIT, R.integer.BUTTON_HOME_Y_PORTRAIT),
- Pair(R.integer.BUTTON_HOME_X_FOLDABLE, R.integer.BUTTON_HOME_Y_FOLDABLE)
+ Pair(R.integer.BUTTON_HOME_X_FOLDABLE, R.integer.BUTTON_HOME_Y_FOLDABLE),
+ 1.0f
),
BUTTON_CAPTURE(
"button_capture",
false,
Pair(R.integer.BUTTON_CAPTURE_X, R.integer.BUTTON_CAPTURE_Y),
Pair(R.integer.BUTTON_CAPTURE_X_PORTRAIT, R.integer.BUTTON_CAPTURE_Y_PORTRAIT),
- Pair(R.integer.BUTTON_CAPTURE_X_FOLDABLE, R.integer.BUTTON_CAPTURE_Y_FOLDABLE)
+ Pair(R.integer.BUTTON_CAPTURE_X_FOLDABLE, R.integer.BUTTON_CAPTURE_Y_FOLDABLE),
+ 1.0f
),
BUTTON_L(
"button_l",
true,
Pair(R.integer.BUTTON_L_X, R.integer.BUTTON_L_Y),
Pair(R.integer.BUTTON_L_X_PORTRAIT, R.integer.BUTTON_L_Y_PORTRAIT),
- Pair(R.integer.BUTTON_L_X_FOLDABLE, R.integer.BUTTON_L_Y_FOLDABLE)
+ Pair(R.integer.BUTTON_L_X_FOLDABLE, R.integer.BUTTON_L_Y_FOLDABLE),
+ 1.0f
),
BUTTON_R(
"button_r",
true,
Pair(R.integer.BUTTON_R_X, R.integer.BUTTON_R_Y),
Pair(R.integer.BUTTON_R_X_PORTRAIT, R.integer.BUTTON_R_Y_PORTRAIT),
- Pair(R.integer.BUTTON_R_X_FOLDABLE, R.integer.BUTTON_R_Y_FOLDABLE)
+ Pair(R.integer.BUTTON_R_X_FOLDABLE, R.integer.BUTTON_R_Y_FOLDABLE),
+ 1.0f
),
BUTTON_ZL(
"button_zl",
true,
Pair(R.integer.BUTTON_ZL_X, R.integer.BUTTON_ZL_Y),
Pair(R.integer.BUTTON_ZL_X_PORTRAIT, R.integer.BUTTON_ZL_Y_PORTRAIT),
- Pair(R.integer.BUTTON_ZL_X_FOLDABLE, R.integer.BUTTON_ZL_Y_FOLDABLE)
+ Pair(R.integer.BUTTON_ZL_X_FOLDABLE, R.integer.BUTTON_ZL_Y_FOLDABLE),
+ 1.0f
),
BUTTON_ZR(
"button_zr",
true,
Pair(R.integer.BUTTON_ZR_X, R.integer.BUTTON_ZR_Y),
Pair(R.integer.BUTTON_ZR_X_PORTRAIT, R.integer.BUTTON_ZR_Y_PORTRAIT),
- Pair(R.integer.BUTTON_ZR_X_FOLDABLE, R.integer.BUTTON_ZR_Y_FOLDABLE)
+ Pair(R.integer.BUTTON_ZR_X_FOLDABLE, R.integer.BUTTON_ZR_Y_FOLDABLE),
+ 1.0f
),
BUTTON_STICK_L(
"button_stick_l",
true,
Pair(R.integer.BUTTON_STICK_L_X, R.integer.BUTTON_STICK_L_Y),
Pair(R.integer.BUTTON_STICK_L_X_PORTRAIT, R.integer.BUTTON_STICK_L_Y_PORTRAIT),
- Pair(R.integer.BUTTON_STICK_L_X_FOLDABLE, R.integer.BUTTON_STICK_L_Y_FOLDABLE)
+ Pair(R.integer.BUTTON_STICK_L_X_FOLDABLE, R.integer.BUTTON_STICK_L_Y_FOLDABLE),
+ 1.0f
),
BUTTON_STICK_R(
"button_stick_r",
true,
Pair(R.integer.BUTTON_STICK_R_X, R.integer.BUTTON_STICK_R_Y),
Pair(R.integer.BUTTON_STICK_R_X_PORTRAIT, R.integer.BUTTON_STICK_R_Y_PORTRAIT),
- Pair(R.integer.BUTTON_STICK_R_X_FOLDABLE, R.integer.BUTTON_STICK_R_Y_FOLDABLE)
+ Pair(R.integer.BUTTON_STICK_R_X_FOLDABLE, R.integer.BUTTON_STICK_R_Y_FOLDABLE),
+ 1.0f
),
STICK_L(
"stick_l",
true,
Pair(R.integer.STICK_L_X, R.integer.STICK_L_Y),
Pair(R.integer.STICK_L_X_PORTRAIT, R.integer.STICK_L_Y_PORTRAIT),
- Pair(R.integer.STICK_L_X_FOLDABLE, R.integer.STICK_L_Y_FOLDABLE)
+ Pair(R.integer.STICK_L_X_FOLDABLE, R.integer.STICK_L_Y_FOLDABLE),
+ 1.0f
),
STICK_R(
"stick_r",
true,
Pair(R.integer.STICK_R_X, R.integer.STICK_R_Y),
Pair(R.integer.STICK_R_X_PORTRAIT, R.integer.STICK_R_Y_PORTRAIT),
- Pair(R.integer.STICK_R_X_FOLDABLE, R.integer.STICK_R_Y_FOLDABLE)
+ Pair(R.integer.STICK_R_X_FOLDABLE, R.integer.STICK_R_Y_FOLDABLE),
+ 1.0f
),
COMBINED_DPAD(
"combined_dpad",
true,
Pair(R.integer.COMBINED_DPAD_X, R.integer.COMBINED_DPAD_Y),
Pair(R.integer.COMBINED_DPAD_X_PORTRAIT, R.integer.COMBINED_DPAD_Y_PORTRAIT),
- Pair(R.integer.COMBINED_DPAD_X_FOLDABLE, R.integer.COMBINED_DPAD_Y_FOLDABLE)
+ Pair(R.integer.COMBINED_DPAD_X_FOLDABLE, R.integer.COMBINED_DPAD_Y_FOLDABLE),
+ 1.0f
);
fun getDefaultPositionForLayout(layout: OverlayLayout): Pair {
@@ -173,7 +191,8 @@ enum class OverlayControl(
defaultVisibility,
getDefaultPositionForLayout(OverlayLayout.Landscape),
getDefaultPositionForLayout(OverlayLayout.Portrait),
- getDefaultPositionForLayout(OverlayLayout.Foldable)
+ getDefaultPositionForLayout(OverlayLayout.Foldable),
+ defaultIndividualScaleResource
)
companion object {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/model/OverlayControlData.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/model/OverlayControlData.kt
index 26cfeb1db5..6cc5a59c98 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/model/OverlayControlData.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/model/OverlayControlData.kt
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.overlay.model
@@ -8,7 +8,8 @@ data class OverlayControlData(
var enabled: Boolean,
var landscapePosition: Pair,
var portraitPosition: Pair,
- var foldablePosition: Pair
+ var foldablePosition: Pair,
+ var individualScale: Float
) {
fun positionFromLayout(layout: OverlayLayout): Pair =
when (layout) {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt
index de0794a17f..5325f688b6 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.utils
@@ -170,7 +170,8 @@ object DirectoryInitialization {
buttonEnabled,
Pair(landscapeXPosition, landscapeYPosition),
Pair(portraitXPosition, portraitYPosition),
- Pair(foldableXPosition, foldableYPosition)
+ Pair(foldableXPosition, foldableYPosition),
+ OverlayControl.map[buttonId]?.defaultIndividualScaleResource ?: 1.0f
)
overlayControlDataMap[buttonId] = controlData
setOverlayData = true
diff --git a/src/android/app/src/main/jni/android_config.cpp b/src/android/app/src/main/jni/android_config.cpp
index a79a64afbb..41ac680d6b 100644
--- a/src/android/app/src/main/jni/android_config.cpp
+++ b/src/android/app/src/main/jni/android_config.cpp
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
#include
#include
@@ -103,6 +103,7 @@ void AndroidConfig::ReadOverlayValues() {
ReadDoubleSetting(std::string("foldable\\x_position"));
control_data.foldable_position.second =
ReadDoubleSetting(std::string("foldable\\y_position"));
+ control_data.individual_scale = static_cast(ReadDoubleSetting(std::string("individual_scale")));
AndroidSettings::values.overlay_control_data.push_back(control_data);
}
EndArray();
@@ -255,6 +256,7 @@ void AndroidConfig::SaveOverlayValues() {
control_data.foldable_position.first);
WriteDoubleSetting(std::string("foldable\\y_position"),
control_data.foldable_position.second);
+ WriteDoubleSetting(std::string("individual_scale"), static_cast(control_data.individual_scale));
}
EndArray();
diff --git a/src/android/app/src/main/jni/android_settings.h b/src/android/app/src/main/jni/android_settings.h
index cd18f1e5b3..c9e59ce105 100644
--- a/src/android/app/src/main/jni/android_settings.h
+++ b/src/android/app/src/main/jni/android_settings.h
@@ -24,6 +24,7 @@ namespace AndroidSettings {
std::pair landscape_position;
std::pair portrait_position;
std::pair foldable_position;
+ float individual_scale;
};
struct Values {
@@ -79,6 +80,15 @@ namespace AndroidSettings {
Settings::Category::Overlay,
Settings::Specialization::Paired, true,
true};
+ Settings::Setting enable_input_overlay_auto_hide{linkage, false,
+ "enable_input_overlay_auto_hide",
+ Settings::Category::Overlay,
+ Settings::Specialization::Default, true,
+ true,};
+
+ Settings::Setting input_overlay_auto_hide{linkage, 5, "input_overlay_auto_hide",
+ Settings::Category::Overlay,
+ Settings::Specialization::Default, true, true, &enable_input_overlay_auto_hide};
Settings::Setting perf_overlay_background{linkage, false, "perf_overlay_background",
Settings::Category::Overlay,
Settings::Specialization::Default, true,
diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp
index 0b26280c6c..e6021ed217 100644
--- a/src/android/app/src/main/jni/native_config.cpp
+++ b/src/android/app/src/main/jni/native_config.cpp
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
#include
@@ -369,7 +369,9 @@ jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getOverlayControlData(JN
env->NewObject(Common::Android::GetOverlayControlDataClass(),
Common::Android::GetOverlayControlDataConstructor(),
Common::Android::ToJString(env, control_data.id), control_data.enabled,
- jlandscapePosition, jportraitPosition, jfoldablePosition);
+ jlandscapePosition, jportraitPosition, jfoldablePosition,
+ control_data.individual_scale);
+
env->SetObjectArrayElement(joverlayControlDataArray, i, jcontrolData);
}
return joverlayControlDataArray;
@@ -418,9 +420,12 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setOverlayControlData(
env,
env->GetObjectField(jfoldablePosition, Common::Android::GetPairSecondField())));
+ float individual_scale = static_cast(env->GetFloatField(
+ joverlayControlData, Common::Android::GetOverlayControlDataIndividualScaleField()));
+
AndroidSettings::values.overlay_control_data.push_back(AndroidSettings::OverlayControlData{
Common::Android::GetJString(env, jidString), enabled, landscape_position,
- portrait_position, foldable_position});
+ portrait_position, foldable_position, individual_scale});
}
}
diff --git a/src/android/app/src/main/res/layout/dialog_overlay_scale.xml b/src/android/app/src/main/res/layout/dialog_overlay_scale.xml
new file mode 100644
index 0000000000..9e047f211e
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_overlay_scale.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/android/app/src/main/res/layout/dialog_spinbox.xml b/src/android/app/src/main/res/layout/dialog_spinbox.xml
new file mode 100644
index 0000000000..2f18ab750d
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_spinbox.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index ab8375b325..a9707a15c7 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -12,6 +12,24 @@
Eden
Eden Switch emulator notifications
Eden is Running
+ Seconds
+
+
+ Increment
+ Decrement
+ Value
+ Value must be at least %1$d
+ Value must be at most %1$d
+ Invalid value
+
+
+
+ Overlay Auto Hide
+ Automatically hide the touch controls overlay after the specified time of inactivity.
+ Enable Overlay Auto Hide
+
+ Input Overlay
+ Configure on-screen controls
@@ -855,6 +873,7 @@
Touchscreen
Lock drawer
Unlock drawer
+ Reset
Loading settingsā¦
@@ -870,7 +889,7 @@
Save/Load Error
Fatal Error
A fatal error occurred. Check the log for details.\nContinuing emulation may result in crashes.
- Turning off this setting will significantly degrade performance. It's recommended that you leave this setting enabled.
+ Turning off this setting will significantly degrade performance. It"s recommended that you leave this setting enabled.
Device RAM: %1$s\nRecommended: %2$s
%1$s %2$s
No bootable game present!
diff --git a/src/common/android/id_cache.cpp b/src/common/android/id_cache.cpp
index e0edd006a5..1198833996 100644
--- a/src/common/android/id_cache.cpp
+++ b/src/common/android/id_cache.cpp
@@ -1,7 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-// SPDX-FileCopyrightText: 2025 Eden Emulator Project
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include
@@ -49,6 +46,7 @@ static jclass s_overlay_control_data_class;
static jmethodID s_overlay_control_data_constructor;
static jfieldID s_overlay_control_data_id_field;
static jfieldID s_overlay_control_data_enabled_field;
+static jfieldID s_overlay_control_data_individual_scale_field;
static jfieldID s_overlay_control_data_landscape_position_field;
static jfieldID s_overlay_control_data_portrait_position_field;
static jfieldID s_overlay_control_data_foldable_position_field;
@@ -244,6 +242,10 @@ namespace Common::Android {
return s_overlay_control_data_enabled_field;
}
+ jfieldID GetOverlayControlDataIndividualScaleField() {
+ return s_overlay_control_data_individual_scale_field;
+ }
+
jfieldID GetOverlayControlDataLandscapePositionField() {
return s_overlay_control_data_landscape_position_field;
}
@@ -494,7 +496,7 @@ namespace Common::Android {
reinterpret_cast(env->NewGlobalRef(overlay_control_data_class));
s_overlay_control_data_constructor =
env->GetMethodID(overlay_control_data_class, "",
- "(Ljava/lang/String;ZLkotlin/Pair;Lkotlin/Pair;Lkotlin/Pair;)V");
+ "(Ljava/lang/String;ZLkotlin/Pair;Lkotlin/Pair;Lkotlin/Pair;F)V");
s_overlay_control_data_id_field =
env->GetFieldID(overlay_control_data_class, "id", "Ljava/lang/String;");
s_overlay_control_data_enabled_field =
@@ -505,6 +507,8 @@ namespace Common::Android {
env->GetFieldID(overlay_control_data_class, "portraitPosition", "Lkotlin/Pair;");
s_overlay_control_data_foldable_position_field =
env->GetFieldID(overlay_control_data_class, "foldablePosition", "Lkotlin/Pair;");
+ s_overlay_control_data_individual_scale_field =
+ env->GetFieldID(overlay_control_data_class, "individualScale", "F");
env->DeleteLocalRef(overlay_control_data_class);
const jclass patch_class = env->FindClass("org/yuzu/yuzu_emu/model/Patch");
diff --git a/src/common/android/id_cache.h b/src/common/android/id_cache.h
index c56ffcf5c6..6b48f471b7 100644
--- a/src/common/android/id_cache.h
+++ b/src/common/android/id_cache.h
@@ -1,7 +1,4 @@
-// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-// SPDX-FileCopyrightText: 2025 Eden Emulator Project
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
@@ -67,6 +64,7 @@ jclass GetOverlayControlDataClass();
jmethodID GetOverlayControlDataConstructor();
jfieldID GetOverlayControlDataIdField();
jfieldID GetOverlayControlDataEnabledField();
+jfieldID GetOverlayControlDataIndividualScaleField();
jfieldID GetOverlayControlDataLandscapePositionField();
jfieldID GetOverlayControlDataPortraitPositionField();
jfieldID GetOverlayControlDataFoldablePositionField();
diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt
index b16c1d99ce..c3d8f5387a 100644
--- a/src/yuzu/CMakeLists.txt
+++ b/src/yuzu/CMakeLists.txt
@@ -366,10 +366,10 @@ if (APPLE)
set_target_properties(yuzu PROPERTIES MACOSX_BUNDLE TRUE)
set_target_properties(yuzu PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist)
- if (NOT USE_SYSTEM_MOLTENVK)
+ if (YUZU_APPLE_USE_BUNDLED_MONTENVK)
set(MOLTENVK_PLATFORM "macOS")
set(MOLTENVK_VERSION "v1.3.0")
- download_moltenvk_external(${MOLTENVK_PLATFORM} ${MOLTENVK_VERSION})
+ download_moltenvk(${MOLTENVK_PLATFORM} ${MOLTENVK_VERSION})
endif()
find_library(MOLTENVK_LIBRARY MoltenVK REQUIRED)
message(STATUS "Using MoltenVK at ${MOLTENVK_LIBRARY}.")