Merge branch 'master' into COMBO_BUTTON

This commit is contained in:
ADAS2024 2026-03-04 16:17:13 -05:00 committed by GitHub
commit 3ef66be317
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
223 changed files with 21336 additions and 11169 deletions

View file

@ -8,7 +8,7 @@ REV_NAME="azahar-$OS-$TARGET-$GITDATE-$GITREV"
# Determine the name of the release being built.
if [ "$GITHUB_REF_TYPE" = "tag" ]; then
RELEASE_NAME=azahar-$GITHUB_REF_NAME
REV_NAME="azahar-$GITHUB_REF_NAME-$OS-$TARGET"
REV_NAME="azahar-$OS-$TARGET-$GITHUB_REF_NAME"
else
RELEASE_NAME=azahar-head
fi

View file

@ -72,7 +72,7 @@ jobs:
path: artifacts/
macos:
runs-on: ${{ (matrix.target == 'x86_64' && 'macos-15-intel') || 'macos-26' }}
runs-on: ${{ (matrix.target == 'x86_64' && 'macos-26-intel') || 'macos-26' }}
strategy:
fail-fast: false
matrix:

172
.github/workflows/libretro.yml vendored Normal file
View file

@ -0,0 +1,172 @@
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)
zip -j -9 azahar-libretro-android-arm64.zip ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.*
- name: Upload
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: ./*.zip
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)
zip -j -9 azahar-libretro-linux-x86_64.zip ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.*
- name: Upload
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: ./*.zip
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)"
zip -j -9 azahar-libretro-windows-x86_64.zip ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.*
- name: Upload
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: ./*.zip
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
zip -j -9 azahar-libretro-macos-$TARGET.zip ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.*
- name: Upload
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: ./*.zip
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
zip -j -9 azahar-libretro-ios.zip ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.*
- name: Upload
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: ./*.zip
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
zip -j -9 azahar-libretro-tvos.zip ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.*
- name: Upload
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: ./*.zip

4
.gitignore vendored
View file

@ -56,3 +56,7 @@ repo/
.ccache/
node_modules/
VULKAN_SDK/
# Version info files
GIT-COMMIT
GIT-TAG

133
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,133 @@
.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: "11.0"
extends:
- .core-defs
- .libretro-osx-cmake-x86_64
# MacOS ARM64
libretro-build-osx-arm64:
extends:
- .core-defs
- .libretro-osx-cmake-arm64
variables:
MACOSX_DEPLOYMENT_TARGET: "11.0"
################################### 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
LIBNAME: ${CORENAME}_libretro.so
artifacts:
paths:
- $LIBNAME
# 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 #################################

3
.gitmodules vendored
View file

@ -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

View file

@ -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 "<CMAKE_AR> Scr <TARGET> <LINK_FLAGS> <OBJECTS>")
set(CMAKE_CXX_ARCHIVE_CREATE "<CMAKE_AR> Scr <TARGET> <LINK_FLAGS> <OBJECTS>")
@ -90,8 +93,18 @@ 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)
# Set bundled qt as dependent options.
@ -130,6 +143,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)
@ -143,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")
@ -300,6 +335,9 @@ set(CMAKE_VISIBILITY_INLINES_HIDDEN NO)
# set up output paths for executable binaries
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin/$<CONFIG>)
if (ENABLE_LIBRETRO)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
endif()
# System imported libraries
# ======================
@ -359,7 +397,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()
@ -513,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)

View file

@ -1,4 +1,6 @@
macro(generate_build_info)
find_package(Git QUIET)
# Gets a UTC timstamp and sets the provided variable to it
function(get_timestamp _var)
string(TIMESTAMP timestamp UTC)
@ -6,9 +8,14 @@ macro(generate_build_info)
endfunction()
get_timestamp(BUILD_DATE)
list(APPEND CMAKE_MODULE_PATH "${SRC_DIR}/externals/cmake-modules")
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/externals/cmake-modules")
if (EXISTS "${SRC_DIR}/.git/objects")
if (EXISTS "${CMAKE_SOURCE_DIR}/GIT-COMMIT" AND EXISTS "${CMAKE_SOURCE_DIR}/GIT-TAG")
file(READ "${CMAKE_SOURCE_DIR}/GIT-COMMIT" GIT_REV_RAW LIMIT 64)
string(STRIP "${GIT_REV_RAW}" GIT_REV)
string(SUBSTRING "${GIT_REV_RAW}" 0 9 GIT_DESC)
set(GIT_BRANCH "HEAD")
elseif (EXISTS "${CMAKE_SOURCE_DIR}/.git/objects")
# Find the package here with the known path so that the GetGit commands can find it as well
find_package(Git QUIET PATHS "${GIT_EXECUTABLE}")
@ -17,12 +24,6 @@ macro(generate_build_info)
get_git_head_revision(GIT_REF_SPEC GIT_REV)
git_describe(GIT_DESC --always --long --dirty)
git_branch_name(GIT_BRANCH)
elseif (EXISTS "${SRC_DIR}/GIT-COMMIT" AND EXISTS "${SRC_DIR}/GIT-TAG")
# unified source archive
file(READ "${SRC_DIR}/GIT-COMMIT" GIT_REV_RAW LIMIT 64)
string(STRIP "${GIT_REV_RAW}" GIT_REV)
string(SUBSTRING "${GIT_REV_RAW}" 0 9 GIT_DESC)
set(GIT_BRANCH "HEAD")
else()
# self-packed archive?
set(GIT_REV "UNKNOWN")
@ -39,8 +40,8 @@ macro(generate_build_info)
if ($ENV{GITHUB_REF_TYPE} STREQUAL "tag")
set(GIT_TAG $ENV{GITHUB_REF_NAME})
endif()
elseif (EXISTS "${SRC_DIR}/GIT-COMMIT" AND EXISTS "${SRC_DIR}/GIT-TAG")
file(READ "${SRC_DIR}/GIT-TAG" GIT_TAG)
elseif (EXISTS "${CMAKE_SOURCE_DIR}/GIT-COMMIT" AND EXISTS "${CMAKE_SOURCE_DIR}/GIT-TAG")
file(READ "${CMAKE_SOURCE_DIR}/GIT-TAG" GIT_TAG)
string(STRIP ${GIT_TAG} GIT_TAG)
endif()

View file

@ -1,9 +1,10 @@
list(APPEND CMAKE_MODULE_PATH "${SRC_DIR}/CMakeModules")
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/CMakeModules")
include(GenerateBuildInfo)
generate_build_info()
# The variable SRC_DIR must be passed into the script (since it uses the current build directory for all values of CMAKE_*_DIR)
set(VIDEO_CORE "${SRC_DIR}/src/video_core")
set(VIDEO_CORE "${CMAKE_SOURCE_DIR}/src/video_core")
set(HASH_FILES
"${VIDEO_CORE}/renderer_opengl/gl_shader_disk_cache.cpp"
"${VIDEO_CORE}/renderer_opengl/gl_shader_disk_cache.h"
@ -47,4 +48,4 @@ foreach (F IN LISTS HASH_FILES)
set(COMBINED "${COMBINED}${TMP}")
endforeach()
string(MD5 SHADER_CACHE_VERSION "${COMBINED}")
configure_file("${SRC_DIR}/src/common/scm_rev.cpp.in" "scm_rev.cpp" @ONLY)
configure_file("${CMAKE_SOURCE_DIR}/src/common/scm_rev.cpp.in" "scm_rev.cpp" @ONLY)

View file

@ -0,0 +1,275 @@
## This file should be the *only place* where setting keys exist as strings.
# All references to setting strings should be derived from the
# `setting_keys.h` and `jni_setting_keys.cpp` files generated here.
# !!! Changes made here should be mirrored to SettingKeys.kt if used on Android
# Shared setting keys (multi-platform)
foreach(KEY IN ITEMS
"use_artic_base_controller"
"enable_gamemode"
"use_cpu_jit"
"cpu_clock_percentage"
"is_new_3ds"
"lle_applets"
"deterministic_async_operations"
"enable_required_online_lle_modules"
"use_virtual_sd"
"use_custom_storage"
"compress_cia_installs"
"region_value"
"init_clock"
"init_time"
"init_time_offset"
"init_ticks_type"
"init_ticks_override"
"plugin_loader"
"allow_plugin_loader"
"steps_per_hour"
"apply_region_free_patch"
"graphics_api"
"physical_device"
"use_gles"
"renderer_debug"
"dump_command_buffers"
"spirv_shader_gen"
"disable_spirv_optimizer"
"async_shader_compilation"
"async_presentation"
"use_hw_shader"
"use_disk_shader_cache"
"shaders_accurate_mul"
"use_vsync"
"use_display_refresh_rate_detection"
"use_shader_jit"
"resolution_factor"
"frame_limit"
"turbo_limit"
"texture_filter"
"texture_sampling"
"delay_game_render_thread_us"
"layout_option"
"swap_screen"
"upright_screen"
"secondary_display_layout"
"large_screen_proportion"
"screen_gap"
"small_screen_position"
"custom_top_x"
"custom_top_y"
"custom_top_width"
"custom_top_height"
"custom_bottom_x"
"custom_bottom_y"
"custom_bottom_width"
"custom_bottom_height"
"custom_second_layer_opacity"
"aspect_ratio"
"screen_top_stretch"
"screen_top_leftright_padding"
"screen_top_topbottom_padding"
"screen_bottom_stretch"
"screen_bottom_leftright_padding"
"screen_bottom_topbottom_padding"
"portrait_layout_option"
"custom_portrait_top_x"
"custom_portrait_top_y"
"custom_portrait_top_width"
"custom_portrait_top_height"
"custom_portrait_bottom_x"
"custom_portrait_bottom_y"
"custom_portrait_bottom_width"
"custom_portrait_bottom_height"
"bg_red"
"bg_green"
"bg_blue"
"render_3d"
"factor_3d"
"swap_eyes_3d"
"render_3d_which_display"
"mono_render_option"
"cardboard_screen_size"
"cardboard_x_shift"
"cardboard_y_shift"
"filter_mode"
"pp_shader_name"
"anaglyph_shader_name"
"dump_textures"
"custom_textures"
"preload_textures"
"async_custom_loading"
"disable_right_eye_render"
"audio_emulation"
"enable_audio_stretching"
"enable_realtime_audio"
"volume"
"output_type"
"output_device"
"input_type"
"input_device"
"delay_start_for_lle_modules"
"use_gdbstub"
"gdbstub_port"
"instant_debug_log"
"enable_rpc_server"
"log_filter"
"log_regex_filter"
"use_integer_scaling"
"layouts_to_cycle"
"camera_inner_flip"
"camera_outer_left_flip"
"camera_outer_right_flip"
"camera_inner_name"
"camera_inner_config"
"camera_outer_left_name"
"camera_outer_left_config"
"camera_outer_right_name"
"camera_outer_right_config"
"video_encoder"
"video_encoder_options"
"video_bitrate"
"audio_encoder"
"audio_encoder_options"
"audio_bitrate"
"last_artic_base_addr"
"motion_device"
"touch_device"
"udp_input_address"
"udp_input_port"
"udp_pad_index"
"record_frame_times"
"language" # FIXME: DUPLICATE KEY (libretro equivalent: language_value)
"web_api_url"
"citra_username"
"citra_token"
)
set(SETTING_KEY_LIST "${SETTING_KEY_LIST}\n\"${KEY}\",")
set(SETTING_KEY_DEFINITIONS "${SETTING_KEY_DEFINITIONS}\nDEFINE_KEY(${KEY})")
if (ANDROID)
string(REPLACE "_" "_1" KEY_JNI_ESCAPED ${KEY})
set(JNI_SETTING_KEY_DEFINITIONS "${JNI_SETTING_KEY_DEFINITIONS}
JNI_DEFINE_KEY(${KEY}, ${KEY_JNI_ESCAPED})")
endif()
endforeach()
# Qt exclusive setting keys
# Note: A lot of these are very generic because our Qt settings are currently put under groups:
# E.g. UILayout\geometry
# TODO: We should probably get rid of these groups and use complete keys at some point. -OS
# FIXME: Some of these settings don't use the standard snake_case. When we can migrate, address that. -OS
if (ENABLE_QT)
foreach(KEY IN ITEMS
"nickname"
"ip"
"port"
"room_nickname"
"room_name"
"room_port"
"host_type"
"max_player"
"room_description"
"multiplayer_filter_text"
"multiplayer_filter_games_owned"
"multiplayer_filter_hide_empty"
"multiplayer_filter_hide_full"
"username_ban_list"
"username"
"ip_ban_list"
"romsPath"
"symbolsPath"
"movieRecordPath"
"moviePlaybackPath"
"videoDumpingPath"
"gameListRootDir"
"gameListDeepScan"
"path"
"deep_scan"
"expanded"
"recentFiles"
"output_format"
"format_options"
"theme"
"program_id"
"geometry"
"state"
"geometryRenderWindow"
"gameListHeaderState"
"microProfileDialogGeometry"
"name"
"bind"
"profile"
"use_touch_from_button"
"touch_from_button_map"
"touch_from_button_maps" # Why are these two so similar? Basically typo bait
"nand_directory"
"sdmc_directory"
"game_id"
"KeySeq"
"gamedirs"
"libvorbis"
"Context"
"favorites"
)
set(SETTING_KEY_LIST "${SETTING_KEY_LIST}\n\"${KEY}\",")
set(SETTING_KEY_DEFINITIONS "${SETTING_KEY_DEFINITIONS}\nDEFINE_KEY(${KEY})")
endforeach()
endif()
# Android exclusive setting keys (standalone app only, not Android libretro)
if (ANDROID)
foreach(KEY IN ITEMS
"expand_to_cutout_area"
"performance_overlay_enable"
"performance_overlay_show_fps"
"performance_overlay_show_frame_time"
"performance_overlay_show_speed"
"performance_overlay_show_app_ram_usage"
"performance_overlay_show_available_ram"
"performance_overlay_show_battery_temp"
"performance_overlay_background"
"use_frame_limit" # FIXME: DUPLICATE KEY (shared equivalent: frame_limit)
"android_hide_images"
"screen_orientation"
"performance_overlay_position"
)
string(REPLACE "_" "_1" KEY_JNI_ESCAPED ${KEY})
set(SETTING_KEY_LIST "${SETTING_KEY_LIST}\n\"${KEY}\",")
set(SETTING_KEY_DEFINITIONS "${SETTING_KEY_DEFINITIONS}\nDEFINE_KEY(${KEY})")
set(JNI_SETTING_KEY_DEFINITIONS "${JNI_SETTING_KEY_DEFINITIONS}
JNI_DEFINE_KEY(${KEY}, ${KEY_JNI_ESCAPED})")
endforeach()
endif()
# Libretro exclusive setting keys
if (ENABLE_LIBRETRO)
foreach(KEY IN ITEMS
"language_value"
"swap_screen_mode"
"use_libretro_save_path"
"analog_function"
"analog_deadzone"
"enable_mouse_touchscreen"
"enable_touch_touchscreen"
"render_touchscreen"
"enable_motion"
"motion_sensitivity"
)
string(REPLACE "_" "_1" KEY_JNI_ESCAPED ${KEY})
set(SETTING_KEY_LIST "${SETTING_KEY_LIST}\n\"${KEY}\",")
set(SETTING_KEY_DEFINITIONS "${SETTING_KEY_DEFINITIONS}\nDEFINE_KEY(${KEY})")
endforeach()
endif()
# Trim trailing comma and newline from SETTING_KEY_LIST
string(LENGTH "${SETTING_KEY_LIST}" SETTING_KEY_LIST_LENGTH)
math(EXPR SETTING_KEY_LIST_NEW_LENGTH "${SETTING_KEY_LIST_LENGTH} - 1")
string(SUBSTRING "${SETTING_KEY_LIST}" 0 ${SETTING_KEY_LIST_NEW_LENGTH} SETTING_KEY_LIST)
# Configure files
configure_file("common/setting_keys.h.in" "common/setting_keys.h" @ONLY)
if (ENABLE_QT)
configure_file("citra_qt/setting_qkeys.h.in" "citra_qt/setting_qkeys.h" @ONLY)
endif()
if (ANDROID AND NOT ENABLE_LIBRETRO)
configure_file("android/app/src/main/jni/jni_setting_keys.cpp.in" "android/app/src/main/jni/jni_setting_keys.cpp" @ONLY)
endif()

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

637
dist/languages/de.ts vendored

File diff suppressed because it is too large Load diff

637
dist/languages/el.ts vendored

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

637
dist/languages/fi.ts vendored

File diff suppressed because it is too large Load diff

655
dist/languages/fr.ts vendored

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

637
dist/languages/id.ts vendored

File diff suppressed because it is too large Load diff

689
dist/languages/it.ts vendored

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

637
dist/languages/nb.ts vendored

File diff suppressed because it is too large Load diff

637
dist/languages/nl.ts vendored

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

641
dist/languages/sv.ts vendored

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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)
@ -292,6 +294,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)

2
externals/boost vendored

@ -1 +1 @@
Subproject commit 2c82bd787302398bcae990e3c9ab2b451284f4ca
Subproject commit f9b15f673a688982f78a5f63a49a27275b318e5f

View file

@ -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
)

@ -0,0 +1 @@
Subproject commit 7fc7feeddca391be65c94e6541381467684b814d

View file

@ -1,6 +1,8 @@
# Enable modules to include each other's files
include_directories(.)
include(GenerateSettingKeys)
# CMake seems to only define _DEBUG on Windows
set_property(DIRECTORY APPEND PROPERTY
COMPILE_DEFINITIONS $<$<CONFIG:Debug>:_DEBUG> $<$<NOT:$<CONFIG:Debug>>:NDEBUG>)
@ -110,10 +112,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)
@ -189,18 +195,18 @@ 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()
if (ENABLE_LIBRETRO)
add_subdirectory(citra_libretro)
endif()
if (ENABLE_ROOM)
add_subdirectory(citra_room)
endif()
@ -209,7 +215,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()

View file

@ -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) {

View file

@ -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) {

View file

@ -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);
}

View file

@ -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<Int>()
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
}
}

View file

@ -0,0 +1,140 @@
// 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
// This list should mirror the list in GenerateSettingKeys.cmake,
// specifically the Shared and Android setting keys.
@Suppress("KotlinJniMissingFunction", "FunctionName")
object SettingKeys {
// Shared
external fun use_artic_base_controller(): String
external fun use_cpu_jit(): String
external fun cpu_clock_percentage(): String
external fun is_new_3ds(): String
external fun lle_applets(): String
external fun deterministic_async_operations(): String
external fun enable_required_online_lle_modules(): String
external fun use_virtual_sd(): String
external fun compress_cia_installs(): String
external fun region_value(): String
external fun init_clock(): String
external fun init_time(): String
external fun init_ticks_type(): String
external fun init_ticks_override(): String
external fun plugin_loader(): String
external fun allow_plugin_loader(): String
external fun steps_per_hour(): String
external fun apply_region_free_patch(): String
external fun graphics_api(): String
external fun use_gles(): String
external fun renderer_debug(): String
external fun spirv_shader_gen(): String
external fun disable_spirv_optimizer(): String
external fun async_shader_compilation(): String
external fun async_presentation(): String
external fun use_hw_shader(): String
external fun use_disk_shader_cache(): String
external fun shaders_accurate_mul(): String
external fun use_vsync(): String
external fun use_shader_jit(): String
external fun resolution_factor(): String
external fun frame_limit(): String
external fun turbo_limit(): String
external fun texture_filter(): String
external fun texture_sampling(): String
external fun delay_game_render_thread_us(): String
external fun layout_option(): String
external fun swap_screen(): String
external fun upright_screen(): String
external fun secondary_display_layout(): String
external fun large_screen_proportion(): String
external fun screen_gap(): String
external fun small_screen_position(): String
external fun custom_top_x(): String
external fun custom_top_y(): String
external fun custom_top_width(): String
external fun custom_top_height(): String
external fun custom_bottom_x(): String
external fun custom_bottom_y(): String
external fun custom_bottom_width(): String
external fun custom_bottom_height(): String
external fun custom_second_layer_opacity(): String
external fun aspect_ratio(): String
external fun portrait_layout_option(): String
external fun custom_portrait_top_x(): String
external fun custom_portrait_top_y(): String
external fun custom_portrait_top_width(): String
external fun custom_portrait_top_height(): String
external fun custom_portrait_bottom_x(): String
external fun custom_portrait_bottom_y(): String
external fun custom_portrait_bottom_width(): String
external fun custom_portrait_bottom_height(): String
external fun bg_red(): String
external fun bg_green(): String
external fun bg_blue(): String
external fun render_3d(): String
external fun factor_3d(): String
external fun swap_eyes_3d(): String
external fun render_3d_which_display(): String
external fun cardboard_screen_size(): String
external fun cardboard_x_shift(): String
external fun cardboard_y_shift(): String
external fun filter_mode(): String
external fun pp_shader_name(): String
external fun anaglyph_shader_name(): String
external fun dump_textures(): String
external fun custom_textures(): String
external fun preload_textures(): String
external fun async_custom_loading(): String
external fun disable_right_eye_render(): String
external fun audio_emulation(): String
external fun enable_audio_stretching(): String
external fun enable_realtime_audio(): String
external fun volume(): String
external fun output_type(): String
external fun output_device(): String
external fun input_type(): String
external fun input_device(): String
external fun delay_start_for_lle_modules(): String
external fun use_gdbstub(): String
external fun gdbstub_port(): String
external fun instant_debug_log(): String
external fun enable_rpc_server(): String
external fun log_filter(): String
external fun log_regex_filter(): String
external fun use_integer_scaling(): String
external fun layouts_to_cycle(): String
external fun camera_inner_flip(): String
external fun camera_outer_left_flip(): String
external fun camera_outer_right_flip(): String
external fun camera_inner_name(): String
external fun camera_inner_config(): String
external fun camera_outer_left_name(): String
external fun camera_outer_left_config(): String
external fun camera_outer_right_name(): String
external fun camera_outer_right_config(): String
external fun last_artic_base_addr(): String
external fun motion_device(): String
external fun touch_device(): String
external fun udp_input_address(): String
external fun udp_input_port(): String
external fun udp_pad_index(): String
external fun record_frame_times(): String
// Android
external fun expand_to_cutout_area(): String
external fun performance_overlay_enable(): String
external fun performance_overlay_show_fps(): String
external fun performance_overlay_show_frame_time(): String
external fun performance_overlay_show_speed(): String
external fun performance_overlay_show_app_ram_usage(): String
external fun performance_overlay_show_available_ram(): String
external fun performance_overlay_show_battery_temp(): String
external fun performance_overlay_background(): String
external fun use_frame_limit(): String
external fun android_hide_images(): String
external fun screen_orientation(): String
external fun performance_overlay_position(): String
}

View file

@ -6,4 +6,4 @@ package org.citra.citra_emu.features.settings.model
interface AbstractListSetting<E> : AbstractSetting {
var list: List<E>
}
}

View file

@ -4,56 +4,58 @@
package org.citra.citra_emu.features.settings.model
import org.citra.citra_emu.features.settings.SettingKeys
enum class BooleanSetting(
override val key: String,
override val section: String,
override val defaultValue: Boolean
) : AbstractBooleanSetting {
EXPAND_TO_CUTOUT_AREA("expand_to_cutout_area", Settings.SECTION_LAYOUT, false),
SPIRV_SHADER_GEN("spirv_shader_gen", Settings.SECTION_RENDERER, true),
ASYNC_SHADERS("async_shader_compilation", Settings.SECTION_RENDERER, false),
DISABLE_SPIRV_OPTIMIZER("disable_spirv_optimizer", Settings.SECTION_RENDERER, true),
PLUGIN_LOADER("plugin_loader", Settings.SECTION_SYSTEM, false),
ALLOW_PLUGIN_LOADER("allow_plugin_loader", Settings.SECTION_SYSTEM, true),
SWAP_SCREEN("swap_screen", Settings.SECTION_LAYOUT, false),
INSTANT_DEBUG_LOG("instant_debug_log", Settings.SECTION_DEBUG, false),
ENABLE_RPC_SERVER("enable_rpc_server", Settings.SECTION_DEBUG, false),
CUSTOM_LAYOUT("custom_layout",Settings.SECTION_LAYOUT,false),
SWAP_EYES_3D("swap_eyes_3d",Settings.SECTION_RENDERER,false),
PERF_OVERLAY_ENABLE("performance_overlay_enable", Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_SHOW_FPS("performance_overlay_show_fps", Settings.SECTION_LAYOUT, true),
PERF_OVERLAY_SHOW_FRAMETIME("performance_overlay_show_frame_time", Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_SHOW_SPEED("performance_overlay_show_speed", Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_SHOW_APP_RAM_USAGE("performance_overlay_show_app_ram_usage", Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_SHOW_AVAILABLE_RAM("performance_overlay_show_available_ram", Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_SHOW_BATTERY_TEMP("performance_overlay_show_battery_temp", Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_BACKGROUND("performance_overlay_background", Settings.SECTION_LAYOUT, false),
DELAY_START_LLE_MODULES("delay_start_for_lle_modules", Settings.SECTION_DEBUG, true),
DETERMINISTIC_ASYNC_OPERATIONS("deterministic_async_operations", Settings.SECTION_DEBUG, false),
REQUIRED_ONLINE_LLE_MODULES("enable_required_online_lle_modules", Settings.SECTION_SYSTEM, false),
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),
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),
CUSTOM_TEXTURES("custom_textures", Settings.SECTION_UTILITY, false),
ASYNC_CUSTOM_LOADING("async_custom_loading", Settings.SECTION_UTILITY, true),
PRELOAD_TEXTURES("preload_textures", Settings.SECTION_UTILITY, false),
ENABLE_AUDIO_STRETCHING("enable_audio_stretching", Settings.SECTION_AUDIO, true),
ENABLE_REALTIME_AUDIO("enable_realtime_audio", Settings.SECTION_AUDIO, false),
CPU_JIT("use_cpu_jit", Settings.SECTION_CORE, true),
HW_SHADER("use_hw_shader", Settings.SECTION_RENDERER, true),
SHADER_JIT("use_shader_jit", Settings.SECTION_RENDERER, true),
VSYNC("use_vsync", Settings.SECTION_RENDERER, false),
USE_FRAME_LIMIT("use_frame_limit", Settings.SECTION_RENDERER, true),
DEBUG_RENDERER("renderer_debug", Settings.SECTION_DEBUG, false),
DISABLE_RIGHT_EYE_RENDER("disable_right_eye_render", Settings.SECTION_RENDERER, false),
USE_ARTIC_BASE_CONTROLLER("use_artic_base_controller", Settings.SECTION_CONTROLS, false),
UPRIGHT_SCREEN("upright_screen", Settings.SECTION_LAYOUT, false),
COMPRESS_INSTALLED_CIA_CONTENT("compress_cia_installs", Settings.SECTION_STORAGE, false),
ANDROID_HIDE_IMAGES("android_hide_images", Settings.SECTION_CORE, false),
APPLY_REGION_FREE_PATCH("apply_region_free_patch", Settings.SECTION_SYSTEM, true),
EXPAND_TO_CUTOUT_AREA(SettingKeys.expand_to_cutout_area(), Settings.SECTION_LAYOUT, false),
SPIRV_SHADER_GEN(SettingKeys.spirv_shader_gen(), Settings.SECTION_RENDERER, true),
ASYNC_SHADERS(SettingKeys.async_shader_compilation(), Settings.SECTION_RENDERER, false),
DISABLE_SPIRV_OPTIMIZER(SettingKeys.disable_spirv_optimizer(), Settings.SECTION_RENDERER, true),
PLUGIN_LOADER(SettingKeys.plugin_loader(), Settings.SECTION_SYSTEM, false),
ALLOW_PLUGIN_LOADER(SettingKeys.allow_plugin_loader(), Settings.SECTION_SYSTEM, true),
SWAP_SCREEN(SettingKeys.swap_screen(), Settings.SECTION_LAYOUT, false),
INSTANT_DEBUG_LOG(SettingKeys.instant_debug_log(), Settings.SECTION_DEBUG, false),
ENABLE_RPC_SERVER(SettingKeys.enable_rpc_server(), Settings.SECTION_DEBUG, false),
SWAP_EYES_3D(SettingKeys.swap_eyes_3d(),Settings.SECTION_RENDERER, false),
PERF_OVERLAY_ENABLE(SettingKeys.performance_overlay_enable(), Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_SHOW_FPS(SettingKeys.performance_overlay_show_fps(), Settings.SECTION_LAYOUT, true),
PERF_OVERLAY_SHOW_FRAMETIME(SettingKeys.performance_overlay_show_frame_time(), Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_SHOW_SPEED(SettingKeys.performance_overlay_show_speed(), Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_SHOW_APP_RAM_USAGE(SettingKeys.performance_overlay_show_app_ram_usage(), Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_SHOW_AVAILABLE_RAM(SettingKeys.performance_overlay_show_available_ram(), Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_SHOW_BATTERY_TEMP(SettingKeys.performance_overlay_show_battery_temp(), Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_BACKGROUND(SettingKeys.performance_overlay_background(), Settings.SECTION_LAYOUT, false),
DELAY_START_LLE_MODULES(SettingKeys.delay_start_for_lle_modules(), Settings.SECTION_DEBUG, true),
DETERMINISTIC_ASYNC_OPERATIONS(SettingKeys.deterministic_async_operations(), Settings.SECTION_DEBUG, false),
REQUIRED_ONLINE_LLE_MODULES(SettingKeys.enable_required_online_lle_modules(), Settings.SECTION_SYSTEM, false),
LLE_APPLETS(SettingKeys.lle_applets(), Settings.SECTION_SYSTEM, false),
NEW_3DS(SettingKeys.is_new_3ds(), Settings.SECTION_SYSTEM, true),
LINEAR_FILTERING(SettingKeys.filter_mode(), Settings.SECTION_RENDERER, true),
SHADERS_ACCURATE_MUL(SettingKeys.shaders_accurate_mul(), Settings.SECTION_RENDERER, false),
DISK_SHADER_CACHE(SettingKeys.use_disk_shader_cache(), Settings.SECTION_RENDERER, true),
DUMP_TEXTURES(SettingKeys.dump_textures(), Settings.SECTION_UTILITY, false),
CUSTOM_TEXTURES(SettingKeys.custom_textures(), Settings.SECTION_UTILITY, false),
ASYNC_CUSTOM_LOADING(SettingKeys.async_custom_loading(), Settings.SECTION_UTILITY, true),
PRELOAD_TEXTURES(SettingKeys.preload_textures(), Settings.SECTION_UTILITY, false),
ENABLE_AUDIO_STRETCHING(SettingKeys.enable_audio_stretching(), Settings.SECTION_AUDIO, true),
ENABLE_REALTIME_AUDIO(SettingKeys.enable_realtime_audio(), Settings.SECTION_AUDIO, false),
CPU_JIT(SettingKeys.use_cpu_jit(), Settings.SECTION_CORE, true),
HW_SHADER(SettingKeys.use_hw_shader(), Settings.SECTION_RENDERER, true),
SHADER_JIT(SettingKeys.use_shader_jit(), Settings.SECTION_RENDERER, true),
VSYNC(SettingKeys.use_vsync(), Settings.SECTION_RENDERER, false),
USE_FRAME_LIMIT(SettingKeys.use_frame_limit(), Settings.SECTION_RENDERER, true),
DEBUG_RENDERER(SettingKeys.renderer_debug(), Settings.SECTION_DEBUG, false),
DISABLE_RIGHT_EYE_RENDER(SettingKeys.disable_right_eye_render(), Settings.SECTION_RENDERER, false),
USE_ARTIC_BASE_CONTROLLER(SettingKeys.use_artic_base_controller(), Settings.SECTION_CONTROLS, false),
UPRIGHT_SCREEN(SettingKeys.upright_screen(), Settings.SECTION_LAYOUT, false),
COMPRESS_INSTALLED_CIA_CONTENT(SettingKeys.compress_cia_installs(), Settings.SECTION_STORAGE, false),
ANDROID_HIDE_IMAGES(SettingKeys.android_hide_images(), Settings.SECTION_MISC, false),
APPLY_REGION_FREE_PATCH(SettingKeys.apply_region_free_patch(), Settings.SECTION_SYSTEM, true),
USE_INTEGER_SCALING(SettingKeys.use_integer_scaling(), Settings.SECTION_RENDERER, false),
ENABLE_COMBO_KEY("enable_combo_key", Settings.SECTION_CONTROLS, true);
override var boolean: Boolean = defaultValue

View file

@ -4,17 +4,18 @@
package org.citra.citra_emu.features.settings.model
import org.citra.citra_emu.features.settings.SettingKeys
enum class FloatSetting(
override val key: String,
override val section: String,
override val defaultValue: Float
) : AbstractFloatSetting {
LARGE_SCREEN_PROPORTION("large_screen_proportion",Settings.SECTION_LAYOUT,2.25f),
SECOND_SCREEN_OPACITY("custom_second_layer_opacity", Settings.SECTION_RENDERER, 100f),
BACKGROUND_RED("bg_red", Settings.SECTION_RENDERER, 0f),
BACKGROUND_BLUE("bg_blue", Settings.SECTION_RENDERER, 0f),
BACKGROUND_GREEN("bg_green", Settings.SECTION_RENDERER, 0f),
EMPTY_SETTING("", "", 0.0f);
LARGE_SCREEN_PROPORTION(SettingKeys.large_screen_proportion(),Settings.SECTION_LAYOUT,2.25f),
SECOND_SCREEN_OPACITY(SettingKeys.custom_second_layer_opacity(), Settings.SECTION_RENDERER, 100f),
BACKGROUND_RED(SettingKeys.bg_red(), Settings.SECTION_RENDERER, 0f),
BACKGROUND_BLUE(SettingKeys.bg_blue(), Settings.SECTION_RENDERER, 0f),
BACKGROUND_GREEN(SettingKeys.bg_green(), Settings.SECTION_RENDERER, 0f);
override var float: Float = defaultValue

View file

@ -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<Int>,
val canBeEmpty: Boolean = true
) : AbstractListSetting<Int> {
LAYOUTS_TO_CYCLE("layouts_to_cycle", Settings.SECTION_LAYOUT, listOf(0, 1, 2, 3, 4, 5), canBeEmpty = false);
private var backingList: List<Int> = defaultValue
private var lastValidList : List<Int> = defaultValue
override var list: List<Int>
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<IntListSetting> = emptyList()
fun from(key: String): IntListSetting? =
values().firstOrNull { it.key == key }
fun clear() = values().forEach { it.list = it.defaultValue }
}
}

View file

@ -4,57 +4,59 @@
package org.citra.citra_emu.features.settings.model
import org.citra.citra_emu.features.settings.SettingKeys
enum class IntSetting(
override val key: String,
override val section: String,
override val defaultValue: Int
) : AbstractIntSetting {
FRAME_LIMIT("frame_limit", Settings.SECTION_RENDERER, 100),
EMULATED_REGION("region_value", Settings.SECTION_SYSTEM, -1),
INIT_CLOCK("init_clock", Settings.SECTION_SYSTEM, 0),
CAMERA_INNER_FLIP("camera_inner_flip", Settings.SECTION_CAMERA, 0),
CAMERA_OUTER_LEFT_FLIP("camera_outer_left_flip", Settings.SECTION_CAMERA, 0),
CAMERA_OUTER_RIGHT_FLIP("camera_outer_right_flip", Settings.SECTION_CAMERA, 0),
GRAPHICS_API("graphics_api", Settings.SECTION_RENDERER, 1),
RESOLUTION_FACTOR("resolution_factor", Settings.SECTION_RENDERER, 1),
STEREOSCOPIC_3D_MODE("render_3d", Settings.SECTION_RENDERER, 2),
STEREOSCOPIC_3D_DEPTH("factor_3d", Settings.SECTION_RENDERER, 0),
STEPS_PER_HOUR("steps_per_hour", Settings.SECTION_SYSTEM, 0),
CARDBOARD_SCREEN_SIZE("cardboard_screen_size", Settings.SECTION_LAYOUT, 85),
CARDBOARD_X_SHIFT("cardboard_x_shift", Settings.SECTION_LAYOUT, 0),
CARDBOARD_Y_SHIFT("cardboard_y_shift", Settings.SECTION_LAYOUT, 0),
SCREEN_LAYOUT("layout_option", Settings.SECTION_LAYOUT, 0),
SMALL_SCREEN_POSITION("small_screen_position",Settings.SECTION_LAYOUT,0),
LANDSCAPE_TOP_X("custom_top_x",Settings.SECTION_LAYOUT,0),
LANDSCAPE_TOP_Y("custom_top_y",Settings.SECTION_LAYOUT,0),
LANDSCAPE_TOP_WIDTH("custom_top_width",Settings.SECTION_LAYOUT,800),
LANDSCAPE_TOP_HEIGHT("custom_top_height",Settings.SECTION_LAYOUT,480),
LANDSCAPE_BOTTOM_X("custom_bottom_x",Settings.SECTION_LAYOUT,80),
LANDSCAPE_BOTTOM_Y("custom_bottom_y",Settings.SECTION_LAYOUT,480),
LANDSCAPE_BOTTOM_WIDTH("custom_bottom_width",Settings.SECTION_LAYOUT,640),
LANDSCAPE_BOTTOM_HEIGHT("custom_bottom_height",Settings.SECTION_LAYOUT,480),
SCREEN_GAP("screen_gap",Settings.SECTION_LAYOUT,0),
PORTRAIT_SCREEN_LAYOUT("portrait_layout_option",Settings.SECTION_LAYOUT,0),
SECONDARY_DISPLAY_LAYOUT("secondary_display_layout",Settings.SECTION_LAYOUT,0),
PORTRAIT_TOP_X("custom_portrait_top_x",Settings.SECTION_LAYOUT,0),
PORTRAIT_TOP_Y("custom_portrait_top_y",Settings.SECTION_LAYOUT,0),
PORTRAIT_TOP_WIDTH("custom_portrait_top_width",Settings.SECTION_LAYOUT,800),
PORTRAIT_TOP_HEIGHT("custom_portrait_top_height",Settings.SECTION_LAYOUT,480),
PORTRAIT_BOTTOM_X("custom_portrait_bottom_x",Settings.SECTION_LAYOUT,80),
PORTRAIT_BOTTOM_Y("custom_portrait_bottom_y",Settings.SECTION_LAYOUT,480),
PORTRAIT_BOTTOM_WIDTH("custom_portrait_bottom_width",Settings.SECTION_LAYOUT,640),
PORTRAIT_BOTTOM_HEIGHT("custom_portrait_bottom_height",Settings.SECTION_LAYOUT,480),
AUDIO_INPUT_TYPE("input_type", Settings.SECTION_AUDIO, 0),
CPU_CLOCK_SPEED("cpu_clock_percentage", Settings.SECTION_CORE, 100),
TEXTURE_FILTER("texture_filter", Settings.SECTION_RENDERER, 0),
TEXTURE_SAMPLING("texture_sampling", Settings.SECTION_RENDERER, 0),
USE_FRAME_LIMIT("use_frame_limit", Settings.SECTION_RENDERER, 1),
DELAY_RENDER_THREAD_US("delay_game_render_thread_us", Settings.SECTION_RENDERER, 0),
ORIENTATION_OPTION("screen_orientation", Settings.SECTION_LAYOUT, 2),
TURBO_LIMIT("turbo_limit", Settings.SECTION_CORE, 200),
PERFORMANCE_OVERLAY_POSITION("performance_overlay_position", Settings.SECTION_LAYOUT, 0),
RENDER_3D_WHICH_DISPLAY("render_3d_which_display",Settings.SECTION_RENDERER,0),
ASPECT_RATIO("aspect_ratio", Settings.SECTION_LAYOUT, 0);
FRAME_LIMIT(SettingKeys.frame_limit(), Settings.SECTION_RENDERER, 100),
EMULATED_REGION(SettingKeys.region_value(), Settings.SECTION_SYSTEM, -1),
INIT_CLOCK(SettingKeys.init_clock(), Settings.SECTION_SYSTEM, 0),
CAMERA_INNER_FLIP(SettingKeys.camera_inner_flip(), Settings.SECTION_CAMERA, 0),
CAMERA_OUTER_LEFT_FLIP(SettingKeys.camera_outer_left_flip(), Settings.SECTION_CAMERA, 0),
CAMERA_OUTER_RIGHT_FLIP(SettingKeys.camera_outer_right_flip(), Settings.SECTION_CAMERA, 0),
GRAPHICS_API(SettingKeys.graphics_api(), Settings.SECTION_RENDERER, 1),
RESOLUTION_FACTOR(SettingKeys.resolution_factor(), Settings.SECTION_RENDERER, 1),
STEREOSCOPIC_3D_MODE(SettingKeys.render_3d(), Settings.SECTION_RENDERER, 2),
STEREOSCOPIC_3D_DEPTH(SettingKeys.factor_3d(), Settings.SECTION_RENDERER, 0),
STEPS_PER_HOUR(SettingKeys.steps_per_hour(), Settings.SECTION_SYSTEM, 0),
CARDBOARD_SCREEN_SIZE(SettingKeys.cardboard_screen_size(), Settings.SECTION_LAYOUT, 85),
CARDBOARD_X_SHIFT(SettingKeys.cardboard_x_shift(), Settings.SECTION_LAYOUT, 0),
CARDBOARD_Y_SHIFT(SettingKeys.cardboard_y_shift(), Settings.SECTION_LAYOUT, 0),
SCREEN_LAYOUT(SettingKeys.layout_option(), Settings.SECTION_LAYOUT, 0),
SMALL_SCREEN_POSITION(SettingKeys.small_screen_position(),Settings.SECTION_LAYOUT,0),
LANDSCAPE_TOP_X(SettingKeys.custom_top_x(),Settings.SECTION_LAYOUT,0),
LANDSCAPE_TOP_Y(SettingKeys.custom_top_y(),Settings.SECTION_LAYOUT,0),
LANDSCAPE_TOP_WIDTH(SettingKeys.custom_top_width(),Settings.SECTION_LAYOUT,800),
LANDSCAPE_TOP_HEIGHT(SettingKeys.custom_top_height(),Settings.SECTION_LAYOUT,480),
LANDSCAPE_BOTTOM_X(SettingKeys.custom_bottom_x(),Settings.SECTION_LAYOUT,80),
LANDSCAPE_BOTTOM_Y(SettingKeys.custom_bottom_y(),Settings.SECTION_LAYOUT,480),
LANDSCAPE_BOTTOM_WIDTH(SettingKeys.custom_bottom_width(),Settings.SECTION_LAYOUT,640),
LANDSCAPE_BOTTOM_HEIGHT(SettingKeys.custom_bottom_height(),Settings.SECTION_LAYOUT,480),
SCREEN_GAP(SettingKeys.screen_gap(),Settings.SECTION_LAYOUT,0),
PORTRAIT_SCREEN_LAYOUT(SettingKeys.portrait_layout_option(),Settings.SECTION_LAYOUT,0),
SECONDARY_DISPLAY_LAYOUT(SettingKeys.secondary_display_layout(),Settings.SECTION_LAYOUT,0),
PORTRAIT_TOP_X(SettingKeys.custom_portrait_top_x(),Settings.SECTION_LAYOUT,0),
PORTRAIT_TOP_Y(SettingKeys.custom_portrait_top_y(),Settings.SECTION_LAYOUT,0),
PORTRAIT_TOP_WIDTH(SettingKeys.custom_portrait_top_width(),Settings.SECTION_LAYOUT,800),
PORTRAIT_TOP_HEIGHT(SettingKeys.custom_portrait_top_height(),Settings.SECTION_LAYOUT,480),
PORTRAIT_BOTTOM_X(SettingKeys.custom_portrait_bottom_x(),Settings.SECTION_LAYOUT,80),
PORTRAIT_BOTTOM_Y(SettingKeys.custom_portrait_bottom_y(),Settings.SECTION_LAYOUT,480),
PORTRAIT_BOTTOM_WIDTH(SettingKeys.custom_portrait_bottom_width(),Settings.SECTION_LAYOUT,640),
PORTRAIT_BOTTOM_HEIGHT(SettingKeys.custom_portrait_bottom_height(),Settings.SECTION_LAYOUT,480),
AUDIO_INPUT_TYPE(SettingKeys.input_type(), Settings.SECTION_AUDIO, 0),
CPU_CLOCK_SPEED(SettingKeys.cpu_clock_percentage(), Settings.SECTION_CORE, 100),
TEXTURE_FILTER(SettingKeys.texture_filter(), Settings.SECTION_RENDERER, 0),
TEXTURE_SAMPLING(SettingKeys.texture_sampling(), Settings.SECTION_RENDERER, 0),
USE_FRAME_LIMIT(SettingKeys.use_frame_limit(), Settings.SECTION_RENDERER, 1),
DELAY_RENDER_THREAD_US(SettingKeys.delay_game_render_thread_us(), Settings.SECTION_RENDERER, 0),
ORIENTATION_OPTION(SettingKeys.screen_orientation(), Settings.SECTION_LAYOUT, 2),
TURBO_LIMIT(SettingKeys.turbo_limit(), Settings.SECTION_CORE, 200),
PERFORMANCE_OVERLAY_POSITION(SettingKeys.performance_overlay_position(), Settings.SECTION_LAYOUT, 0),
RENDER_3D_WHICH_DISPLAY(SettingKeys.render_3d_which_display(),Settings.SECTION_RENDERER,0),
ASPECT_RATIO(SettingKeys.aspect_ratio(), Settings.SECTION_LAYOUT, 0);
override var int: Int = defaultValue

View file

@ -1,16 +1,18 @@
// 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
import org.citra.citra_emu.features.settings.SettingKeys
enum class ScaledFloatSetting(
override val key: String,
override val section: String,
override val defaultValue: Float,
val scale: Int
) : AbstractFloatSetting {
AUDIO_VOLUME("volume", Settings.SECTION_AUDIO, 1.0f, 100);
AUDIO_VOLUME(SettingKeys.volume(), Settings.SECTION_AUDIO, 1.0f, 100);
override var float: Float = defaultValue
get() = field * scale

View file

@ -113,6 +113,7 @@ class Settings {
const val SECTION_CUSTOM_PORTRAIT = "Custom Portrait Layout"
const val SECTION_PERFORMANCE_OVERLAY = "Performance Overlay"
const val SECTION_STORAGE = "Storage"
const val SECTION_MISC = "Miscellaneous"
const val KEY_BUTTON_A = "button_a"
@ -137,6 +138,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"
@ -206,6 +208,7 @@ class Settings {
R.string.button_zr
)
val hotKeys = listOf(
HOTKEY_ENABLE,
HOTKEY_SCREEN_SWAP,
HOTKEY_CYCLE_LAYOUT,
HOTKEY_CLOSE_GAME,
@ -215,6 +218,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,
@ -226,6 +230,7 @@ class Settings {
val comboSelection = mutableSetOf<String>()
// TODO: Move these in with the other setting keys in GenerateSettingKeys.cmake
const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
const val PREF_MATERIAL_YOU = "MaterialYouTheme"
const val PREF_THEME_MODE = "ThemeMode"

View file

@ -1,21 +1,23 @@
// 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
import org.citra.citra_emu.features.settings.SettingKeys
enum class StringSetting(
override val key: String,
override val section: String,
override val defaultValue: String
) : AbstractStringSetting {
INIT_TIME("init_time", Settings.SECTION_SYSTEM, "946731601"),
CAMERA_INNER_NAME("camera_inner_name", Settings.SECTION_CAMERA, "ndk"),
CAMERA_INNER_CONFIG("camera_inner_config", Settings.SECTION_CAMERA, "_front"),
CAMERA_OUTER_LEFT_NAME("camera_outer_left_name", Settings.SECTION_CAMERA, "ndk"),
CAMERA_OUTER_LEFT_CONFIG("camera_outer_left_config", Settings.SECTION_CAMERA, "_back"),
CAMERA_OUTER_RIGHT_NAME("camera_outer_right_name", Settings.SECTION_CAMERA, "ndk"),
CAMERA_OUTER_RIGHT_CONFIG("camera_outer_right_config", Settings.SECTION_CAMERA, "_back");
INIT_TIME(SettingKeys.init_time(), Settings.SECTION_SYSTEM, "946731601"),
CAMERA_INNER_NAME(SettingKeys.camera_inner_name(), Settings.SECTION_CAMERA, "ndk"),
CAMERA_INNER_CONFIG(SettingKeys.camera_inner_config(), Settings.SECTION_CAMERA, "_front"),
CAMERA_OUTER_LEFT_NAME(SettingKeys.camera_outer_left_name(), Settings.SECTION_CAMERA, "ndk"),
CAMERA_OUTER_LEFT_CONFIG(SettingKeys.camera_outer_left_config(), Settings.SECTION_CAMERA, "_back"),
CAMERA_OUTER_RIGHT_NAME(SettingKeys.camera_outer_right_name(), Settings.SECTION_CAMERA, "ndk"),
CAMERA_OUTER_RIGHT_CONFIG(SettingKeys.camera_outer_right_config(), Settings.SECTION_CAMERA, "_back");
override var string: String = defaultValue

View file

@ -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
@ -129,6 +130,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
@ -163,36 +165,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<String>())!!.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)
@ -230,9 +236,8 @@ class InputBindingSetting(
}
val code = translateEventToKeyId(keyEvent)
writeButtonMapping(getInputButtonKey(code))
val uiString = "${keyEvent.device.name}: Button $code"
value = uiString
writeButtonMapping(keyEvent)
value = "${keyEvent.device.name}: ${getButtonName(code)}"
}
/**
@ -259,8 +264,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
@ -268,6 +272,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<String> 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<DefaultButtonMapping>,
axisMappings: List<DefaultAxisMapping>
) {
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.
*/
@ -291,19 +530,31 @@ class InputBindingSetting(
NativeLibrary.ButtonType.DPAD_RIGHT -> Settings.KEY_BUTTON_RIGHT
else -> ""
}
/**
* Helper function to get the settings key for an gamepad button.
*
* Get the mutable set of int button values this key should map to given an event
*/
@Deprecated("Use the new getInputButtonKey(keyEvent) method to handle unknown keys")
fun getInputButtonKey(keyCode: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${keyCode}"
fun getButtonSet(keyCode: KeyEvent):MutableSet<Int> {
val key = getInputButtonKey(keyCode)
val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
var buttonCodes = try {
preferences.getStringSet(key, mutableSetOf<String>())
} catch (e: ClassCastException) {
val prefInt = preferences.getInt(key, -1);
val migratedSet = if (prefInt != -1) {
mutableSetOf(prefInt.toString())
} else {
mutableSetOf<String>()
}
migratedSet
}
if (buttonCodes == null) buttonCodes = mutableSetOf<String>()
return buttonCodes.mapNotNull { it.toIntOrNull() }.toMutableSet()
}
/**
* Helper function to get the settings key for an gamepad button.
*
*/
fun getInputButtonKey(event: KeyEvent): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${translateEventToKeyId(event)}"
private fun getInputButtonKey(keyId: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${keyId}"
/** 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.

View file

@ -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<Int>? = null,
override var isEnabled: Boolean = true
) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_MULTI_CHOICE
val selectedValues: List<Int>
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<Int>): IntListSetting {
val intSetting = setting as IntListSetting
intSetting.list = selection
return intSetting
}
}

View file

@ -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
}

View file

@ -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

View file

@ -43,6 +43,7 @@ import org.citra.citra_emu.features.settings.model.AbstractMultiStringSetting
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.Settings
@ -50,6 +51,7 @@ 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.StringMultiChoiceSetting
@ -59,7 +61,8 @@ 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.StringMultiChoiceViewHolder
import org.citra.citra_emu.features.settings.ui.viewholder.StringMultiChoiceViewHolder // TODO: Remove and integrate within MultiChoiceViewHolder
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
@ -67,6 +70,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
@ -77,7 +81,8 @@ import kotlin.math.roundToInt
class SettingsAdapter(
private val fragmentView: SettingsFragmentView,
public val context: Context
) : RecyclerView.Adapter<SettingViewHolder?>(), DialogInterface.OnClickListener, DialogInterface.OnMultiChoiceClickListener {
) : RecyclerView.Adapter<SettingViewHolder?>(), DialogInterface.OnClickListener,
DialogInterface.OnMultiChoiceClickListener {
private var settings: ArrayList<SettingsItem>? = null
private var clickedItem: SettingsItem? = null
private var clickedPosition: Int
@ -109,6 +114,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)
}
@ -190,24 +199,35 @@ 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_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
SettingsItem.TYPE_MULTI_CHOICE -> {
(oldItem as MultiChoiceSetting).isEnabled == (newItem as MultiChoiceSetting).isEnabled
}
SettingsItem.TYPE_STRING_MULTI_CHOICE -> {
(oldItem as StringMultiChoiceSetting).isEnabled == (newItem as StringMultiChoiceSetting).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
}
@ -226,7 +246,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()
}
@ -244,6 +264,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 {
@ -372,14 +413,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)
}
}
}
@ -459,6 +500,7 @@ class SettingsAdapter(
}
it.setSelectedValue(value)
}
is AbstractShortSetting -> {
val value = getValueForSingleChoiceSelection(it, which).toShort()
if (it.selectedValue.toShort() != value) {
@ -466,6 +508,7 @@ class SettingsAdapter(
}
it.setSelectedValue(value)
}
else -> throw IllegalStateException("Unrecognized type used for SingleChoiceSetting!")
}
fragmentView?.putSetting(setting)
@ -511,11 +554,12 @@ class SettingsAdapter(
val setting = it.setSelectedValue(value)
fragmentView?.putSetting(setting)
}
else -> {
val setting = it.setSelectedValue(sliderProgress)
fragmentView?.putSetting(setting)
}
}
}
fragmentView.loadSettingsList()
closeDialog()
}
@ -531,7 +575,7 @@ class SettingsAdapter(
fragmentView?.putSetting(setting)
fragmentView.loadSettingsList()
closeDialog()
}
}
}
}
clickedItem = null
@ -539,6 +583,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)
@ -598,26 +657,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()
}
@ -643,6 +718,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
@ -660,23 +745,22 @@ class SettingsAdapter(
return -1
}
private fun onStringMultiChoiceClick(item: StringMultiChoiceSetting) {
clickedItem = item
dialog = context?.let {
MaterialAlertDialogBuilder(it)
.setTitle(item.nameId)
.setMultiChoiceItems(item.choices, item.selectValueIndices, this)
.show()
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};
}
fun onStringMultiChoiceClick(item: StringMultiChoiceSetting, position: Int) {
clickedPosition = position
onStringMultiChoiceClick(item)
}
//TODO: I only added MultiChoice for fleshing out backend, debating whether to remove MultiChoiceSetting and related code for cleaning up
override fun onClick(dialog: DialogInterface?, which: Int, is_checked: Boolean) {
when (clickedItem) {
@ -752,5 +836,4 @@ class SettingsAdapter(
}
}
}
}
}

View file

@ -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
@ -28,12 +29,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
@ -798,6 +801,16 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
val buttons = settingsActivity.resources.getStringArray(R.array.n3dsButtons).take(10).toTypedArray()
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)
@ -833,7 +846,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]))
@ -931,6 +944,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(
@ -1181,6 +1203,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,

View file

@ -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<String> = 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
}
}

View file

@ -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
}
}

View file

@ -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
@ -260,6 +261,11 @@ object SettingsFile {
if (stringListSetting != null) {
stringListSetting.list = value.split(", ").map { it }
}
val intListSetting = IntListSetting.from(key)
if (intListSetting != null) {
intListSetting.list = value.split(", ").map { it.toInt() }
}
return null
}

View file

@ -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>(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
}
}
}

View file

@ -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
}

View file

@ -33,7 +33,7 @@ import org.citra.citra_emu.adapters.HomeSettingAdapter
import org.citra.citra_emu.databinding.DialogSoftwareKeyboardBinding
import org.citra.citra_emu.databinding.FragmentHomeSettingsBinding
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.SettingKeys
import org.citra.citra_emu.features.settings.ui.SettingsActivity
import org.citra.citra_emu.features.settings.utils.SettingsFile
import org.citra.citra_emu.model.Game
@ -89,7 +89,7 @@ class HomeSettingsFragment : Fragment() {
{
val inflater = LayoutInflater.from(context)
val inputBinding = DialogSoftwareKeyboardBinding.inflate(inflater)
var textInputValue: String = preferences.getString("last_artic_base_addr", "")!!
var textInputValue: String = preferences.getString(SettingKeys.last_artic_base_addr(), "")!!
inputBinding.editTextInput.setText(textInputValue)
inputBinding.editTextInput.doOnTextChanged { text, _, _, _ ->
@ -103,7 +103,7 @@ class HomeSettingsFragment : Fragment() {
.setPositiveButton(android.R.string.ok) { _, _ ->
if (textInputValue.isNotEmpty()) {
preferences.edit()
.putString("last_artic_base_addr", textInputValue)
.putString(SettingKeys.last_artic_base_addr(), textInputValue)
.apply()
val menu = Game(
title = getString(R.string.artic_base),

View file

@ -39,6 +39,7 @@ import org.citra.citra_emu.R
import org.citra.citra_emu.databinding.DialogSoftwareKeyboardBinding
import org.citra.citra_emu.databinding.FragmentSystemFilesBinding
import org.citra.citra_emu.features.settings.model.Settings
import org.citra.citra_emu.features.settings.SettingKeys
import org.citra.citra_emu.model.Game
import org.citra.citra_emu.utils.SystemSaveGame
import org.citra.citra_emu.viewmodel.GamesViewModel
@ -177,7 +178,7 @@ class SystemFilesFragment : Fragment() {
binding.buttonSetUpSystemFiles.setOnClickListener {
val inflater = LayoutInflater.from(context)
val inputBinding = DialogSoftwareKeyboardBinding.inflate(inflater)
var textInputValue: String = preferences.getString("last_artic_base_addr", "")!!
var textInputValue: String = preferences.getString(SettingKeys.last_artic_base_addr(), "")!!
val progressDialog = showProgressDialog(
getText(R.string.setup_system_files),
@ -274,7 +275,7 @@ class SystemFilesFragment : Fragment() {
.setPositiveButton(android.R.string.ok) { diag, _ ->
if (textInputValue.isNotEmpty() && !(!buttonO3ds.isChecked && !buttonN3ds.isChecked)) {
preferences.edit()
.putString("last_artic_base_addr", textInputValue)
.putString(SettingKeys.last_artic_base_addr(), textInputValue)
.apply()
val menu = Game(
title = getString(R.string.artic_base),

View file

@ -22,6 +22,7 @@ add_library(citra-android SHARED
game_info.cpp
id_cache.cpp
id_cache.h
jni_setting_keys.cpp
native.cpp
ndk_motion.cpp
ndk_motion.h

View file

@ -4,13 +4,16 @@
#include <iomanip>
#include <memory>
#include <ranges>
#include <sstream>
#include <unordered_map>
#include <INIReader.h>
#include <boost/hana/string.hpp>
#include "common/file_util.h"
#include "common/logging/backend.h"
#include "common/logging/log.h"
#include "common/param_package.h"
#include "common/setting_keys.h"
#include "common/settings.h"
#include "core/core.h"
#include "core/hle/service/cfg/cfg.h"
@ -25,11 +28,11 @@
Config::Config() {
// TODO: Don't hardcode the path; let the frontend decide where to put the config files.
sdl2_config_loc = FileUtil::GetUserPath(FileUtil::UserPath::ConfigDir) + "config.ini";
android_config_loc = FileUtil::GetUserPath(FileUtil::UserPath::ConfigDir) + "config.ini";
std::string ini_buffer;
FileUtil::ReadFileToString(true, sdl2_config_loc, ini_buffer);
FileUtil::ReadFileToString(true, android_config_loc, ini_buffer);
if (!ini_buffer.empty()) {
sdl2_config = std::make_unique<INIReader>(ini_buffer.c_str(), ini_buffer.size());
android_config = std::make_unique<INIReader>(ini_buffer.c_str(), ini_buffer.size());
}
Reload();
@ -38,15 +41,15 @@ Config::Config() {
Config::~Config() = default;
bool Config::LoadINI(const std::string& default_contents, bool retry) {
const std::string& location = this->sdl2_config_loc;
if (sdl2_config == nullptr || sdl2_config->ParseError() < 0) {
const std::string& location = this->android_config_loc;
if (android_config == nullptr || android_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);
std::string ini_buffer;
FileUtil::ReadFileToString(true, location, ini_buffer);
sdl2_config =
android_config =
std::make_unique<INIReader>(ini_buffer.c_str(), ini_buffer.size()); // Reopen file
return LoadINI(default_contents, false);
@ -77,7 +80,8 @@ static const std::array<int, Settings::NativeAnalog::NumAnalogs> default_analogs
template <>
void Config::ReadSetting(const std::string& group, Settings::Setting<std::string>& setting) {
std::string setting_value = sdl2_config->Get(group, setting.GetLabel(), setting.GetDefault());
std::string setting_value =
android_config->Get(group, setting.GetLabel(), setting.GetDefault());
if (setting_value.empty()) {
setting_value = setting.GetDefault();
}
@ -86,15 +90,15 @@ void Config::ReadSetting(const std::string& group, Settings::Setting<std::string
template <>
void Config::ReadSetting(const std::string& group, Settings::Setting<bool>& setting) {
setting = sdl2_config->GetBoolean(group, setting.GetLabel(), setting.GetDefault());
setting = android_config->GetBoolean(group, setting.GetLabel(), setting.GetDefault());
}
template <typename Type, bool ranged>
void Config::ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting) {
if constexpr (std::is_floating_point_v<Type>) {
setting = sdl2_config->GetReal(group, setting.GetLabel(), setting.GetDefault());
setting = android_config->GetReal(group, setting.GetLabel(), setting.GetDefault());
} else {
setting = static_cast<Type>(sdl2_config->GetInteger(
setting = static_cast<Type>(android_config->GetInteger(
group, setting.GetLabel(), static_cast<long>(setting.GetDefault())));
}
}
@ -103,30 +107,30 @@ void Config::ReadValues() {
// Controls
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
std::string default_param = InputManager::GenerateButtonParamPackage(default_buttons[i]);
Settings::values.current_input_profile.buttons[i] =
sdl2_config->GetString("Controls", Settings::NativeButton::mapping[i], default_param);
Settings::values.current_input_profile.buttons[i] = android_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 = InputManager::GenerateAnalogParamPackage(default_analogs[i]);
Settings::values.current_input_profile.analogs[i] =
sdl2_config->GetString("Controls", Settings::NativeAnalog::mapping[i], default_param);
Settings::values.current_input_profile.analogs[i] = android_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(
Settings::values.current_input_profile.motion_device = android_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(
android_config->GetString("Controls", "touch_device", "engine:emu_window");
Settings::values.current_input_profile.udp_input_address = android_config->GetString(
"Controls", "udp_input_address", InputCommon::CemuhookUDP::DEFAULT_ADDR);
Settings::values.current_input_profile.udp_input_port =
static_cast<u16>(sdl2_config->GetInteger("Controls", "udp_input_port",
InputCommon::CemuhookUDP::DEFAULT_PORT));
static_cast<u16>(android_config->GetInteger("Controls", "udp_input_port",
InputCommon::CemuhookUDP::DEFAULT_PORT));
ReadSetting("Controls", Settings::values.use_artic_base_controller);
@ -135,9 +139,9 @@ void Config::ReadValues() {
ReadSetting("Core", Settings::values.cpu_clock_percentage);
// Renderer
Settings::values.use_gles = sdl2_config->GetBoolean("Renderer", "use_gles", true);
Settings::values.use_gles = android_config->GetBoolean("Renderer", "use_gles", true);
Settings::values.shaders_accurate_mul =
sdl2_config->GetBoolean("Renderer", "shaders_accurate_mul", false);
android_config->GetBoolean("Renderer", "shaders_accurate_mul", false);
ReadSetting("Renderer", Settings::values.graphics_api);
ReadSetting("Renderer", Settings::values.async_presentation);
ReadSetting("Renderer", Settings::values.async_shader_compilation);
@ -152,7 +156,7 @@ void Config::ReadValues() {
ReadSetting("Renderer", Settings::values.texture_sampling);
ReadSetting("Renderer", Settings::values.turbo_limit);
// Workaround to map Android setting for enabling the frame limiter to the format Citra expects
if (sdl2_config->GetBoolean("Renderer", "use_frame_limit", true)) {
if (android_config->GetBoolean("Renderer", "use_frame_limit", true)) {
ReadSetting("Renderer", Settings::values.frame_limit);
} else {
Settings::values.frame_limit = 0;
@ -166,8 +170,9 @@ void Config::ReadValues() {
else if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::Interlaced)
default_shader = "Horizontal (builtin)";
Settings::values.pp_shader_name =
sdl2_config->GetString("Renderer", "pp_shader_name", default_shader);
android_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);
@ -180,18 +185,19 @@ void Config::ReadValues() {
// Layout
// Somewhat inelegant solution to ensure layout value is between 0 and 5 on read
// since older config files may have other values
int layoutInt = (int)sdl2_config->GetInteger(
int layoutInt = (int)android_config->GetInteger(
"Layout", "layout_option", static_cast<int>(Settings::LayoutOption::LargeScreen));
if (layoutInt < 0 || layoutInt > 5) {
layoutInt = static_cast<int>(Settings::LayoutOption::LargeScreen);
}
Settings::values.layout_option = static_cast<Settings::LayoutOption>(layoutInt);
Settings::values.screen_gap = static_cast<int>(sdl2_config->GetReal("Layout", "screen_gap", 0));
Settings::values.screen_gap =
static_cast<int>(android_config->GetReal("Layout", "screen_gap", 0));
Settings::values.large_screen_proportion =
static_cast<float>(sdl2_config->GetReal("Layout", "large_screen_proportion", 2.25));
static_cast<float>(android_config->GetReal("Layout", "large_screen_proportion", 2.25));
Settings::values.small_screen_position = static_cast<Settings::SmallScreenPosition>(
sdl2_config->GetInteger("Layout", "small_screen_position",
static_cast<int>(Settings::SmallScreenPosition::TopRight)));
android_config->GetInteger("Layout", "small_screen_position",
static_cast<int>(Settings::SmallScreenPosition::TopRight)));
ReadSetting("Layout", Settings::values.screen_gap);
ReadSetting("Layout", Settings::values.custom_top_x);
ReadSetting("Layout", Settings::values.custom_top_y);
@ -208,12 +214,12 @@ void Config::ReadValues() {
ReadSetting("Layout", Settings::values.upright_screen);
Settings::values.portrait_layout_option =
static_cast<Settings::PortraitLayoutOption>(sdl2_config->GetInteger(
static_cast<Settings::PortraitLayoutOption>(android_config->GetInteger(
"Layout", "portrait_layout_option",
static_cast<int>(Settings::PortraitLayoutOption::PortraitTopFullWidth)));
Settings::values.secondary_display_layout = static_cast<Settings::SecondaryDisplayLayout>(
sdl2_config->GetInteger("Layout", "secondary_display_layout",
static_cast<int>(Settings::SecondaryDisplayLayout::None)));
android_config->GetInteger("Layout", Settings::HKeys::secondary_display_layout.c_str(),
static_cast<int>(Settings::SecondaryDisplayLayout::None)));
ReadSetting("Layout", Settings::values.custom_portrait_top_x);
ReadSetting("Layout", Settings::values.custom_portrait_top_y);
ReadSetting("Layout", Settings::values.custom_portrait_top_width);
@ -252,7 +258,8 @@ void Config::ReadValues() {
ReadSetting("System", Settings::values.region_value);
ReadSetting("System", Settings::values.init_clock);
{
std::string time = sdl2_config->GetString("System", "init_time", "946681277");
std::string time =
android_config->GetString("System", Settings::HKeys::init_time.c_str(), "946681277");
try {
Settings::values.init_time = std::stoll(time);
} catch (...) {
@ -267,24 +274,27 @@ void Config::ReadValues() {
// Camera
using namespace Service::CAM;
Settings::values.camera_name[OuterRightCamera] =
sdl2_config->GetString("Camera", "camera_outer_right_name", "ndk");
Settings::values.camera_config[OuterRightCamera] = sdl2_config->GetString(
"Camera", "camera_outer_right_config", std::string{Camera::NDK::BackCameraPlaceholder});
Settings::values.camera_name[OuterRightCamera] = android_config->GetString(
"Camera", Settings::HKeys::camera_outer_right_name.c_str(), "ndk");
Settings::values.camera_config[OuterRightCamera] =
android_config->GetString("Camera", Settings::HKeys::camera_outer_right_config.c_str(),
std::string{Camera::NDK::BackCameraPlaceholder});
Settings::values.camera_flip[OuterRightCamera] =
sdl2_config->GetInteger("Camera", "camera_outer_right_flip", 0);
android_config->GetInteger("Camera", Settings::HKeys::camera_outer_right_flip.c_str(), 0);
Settings::values.camera_name[InnerCamera] =
sdl2_config->GetString("Camera", "camera_inner_name", "ndk");
Settings::values.camera_config[InnerCamera] = sdl2_config->GetString(
"Camera", "camera_inner_config", std::string{Camera::NDK::FrontCameraPlaceholder});
android_config->GetString("Camera", Settings::HKeys::camera_inner_name.c_str(), "ndk");
Settings::values.camera_config[InnerCamera] =
android_config->GetString("Camera", Settings::HKeys::camera_inner_config.c_str(),
std::string{Camera::NDK::FrontCameraPlaceholder});
Settings::values.camera_flip[InnerCamera] =
sdl2_config->GetInteger("Camera", "camera_inner_flip", 0);
android_config->GetInteger("Camera", Settings::HKeys::camera_inner_flip.c_str(), 0);
Settings::values.camera_name[OuterLeftCamera] =
sdl2_config->GetString("Camera", "camera_outer_left_name", "ndk");
Settings::values.camera_config[OuterLeftCamera] = sdl2_config->GetString(
"Camera", "camera_outer_left_config", std::string{Camera::NDK::BackCameraPlaceholder});
android_config->GetString("Camera", Settings::HKeys::camera_outer_left_name.c_str(), "ndk");
Settings::values.camera_config[OuterLeftCamera] =
android_config->GetString("Camera", Settings::HKeys::camera_outer_left_config.c_str(),
std::string{Camera::NDK::BackCameraPlaceholder});
Settings::values.camera_flip[OuterLeftCamera] =
sdl2_config->GetInteger("Camera", "camera_outer_left_flip", 0);
android_config->GetInteger("Camera", Settings::HKeys::camera_outer_left_flip.c_str(), 0);
// Miscellaneous
ReadSetting("Miscellaneous", Settings::values.log_filter);
@ -299,7 +309,7 @@ void Config::ReadValues() {
// Debugging
Settings::values.record_frame_times =
sdl2_config->GetBoolean("Debugging", "record_frame_times", false);
android_config->GetBoolean("Debugging", Settings::HKeys::record_frame_times.c_str(), false);
ReadSetting("Debugging", Settings::values.renderer_debug);
ReadSetting("Debugging", Settings::values.use_gdbstub);
ReadSetting("Debugging", Settings::values.gdbstub_port);
@ -307,18 +317,34 @@ void Config::ReadValues() {
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);
bool use_lle =
android_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", "");
android_config->GetString("WebService", "web_api_url", "https://api.citra-emu.org");
NetSettings::values.citra_username =
android_config->GetString("WebService", "citra_username", "");
NetSettings::values.citra_token = android_config->GetString("WebService", "citra_token", "");
}
void Config::Reload() {
LoadINI(DefaultINI::sdl2_config_file);
for (auto key = Settings::Keys::keys_array.begin(); key != Settings::Keys::keys_array.end();
++key) {
const auto key_declaration_string = std::string(*key) + " =";
// FIXME: This code looks so ass when formatted by clang-format -OS
if (std::ranges::find(DefaultINI::android_config_omitted_keys, *key) ==
std::end(DefaultINI::android_config_omitted_keys) &&
std::string(DefaultINI::android_config_default_file_content)
.find(key_declaration_string) == std::string::npos) {
ASSERT_MSG(false,
"Validation of default content config failed: Missing or malformed key "
"declaration {}",
*key);
}
}
LoadINI(DefaultINI::android_config_default_file_content);
ReadValues();
}

View file

@ -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.
@ -12,8 +12,8 @@ class INIReader;
class Config {
private:
std::unique_ptr<INIReader> sdl2_config;
std::string sdl2_config_loc;
std::unique_ptr<INIReader> android_config;
std::string android_config_loc;
bool LoadINI(const std::string& default_contents = "", bool retry = true);
void ReadValues();
@ -26,7 +26,7 @@ public:
private:
/**
* Applies a value read from the sdl2_config to a Setting.
* Applies a value read from the android_config to a Setting.
*
* @param group The name of the INI group
* @param setting The yuzu setting to modify

View file

@ -4,73 +4,61 @@
#pragma once
#include <boost/hana/string.hpp>
#include "common/setting_keys.h"
namespace Keys = Settings::Keys;
namespace DefaultINI {
const char* sdl2_config_file = R"(
// All of these setting keys are either not currently used by Android or are too niche to bother
// documenting (people can contribute documentation if they care), or some other explained reason
constexpr std::array android_config_omitted_keys = {
Keys::enable_gamemode,
Keys::use_custom_storage,
Keys::init_time_offset,
Keys::physical_device,
Keys::use_gles, // Niche
Keys::dump_command_buffers,
Keys::use_display_refresh_rate_detection,
Keys::screen_top_stretch,
Keys::screen_top_leftright_padding,
Keys::screen_top_topbottom_padding,
Keys::screen_bottom_stretch,
Keys::screen_bottom_leftright_padding,
Keys::screen_bottom_topbottom_padding,
Keys::mono_render_option,
Keys::log_regex_filter, // Niche
Keys::video_encoder,
Keys::video_encoder_options,
Keys::video_bitrate,
Keys::audio_encoder,
Keys::audio_encoder_options,
Keys::audio_bitrate,
Keys::last_artic_base_addr, // On Android, this value is stored as a "preference"
};
// clang-format off
// Is this macro nasty? Yes.
// Does it make the code way more readable? Also yes.
#define DECLARE_KEY(KEY) +Settings::HKeys::KEY+BOOST_HANA_STRING(" =")+
static const char* android_config_default_file_content = (BOOST_HANA_STRING(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=
)") DECLARE_KEY(motion_device) BOOST_HANA_STRING(R"(
# 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= engine:emu_window
)") DECLARE_KEY(touch_device) BOOST_HANA_STRING(R"(
# 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
@ -78,136 +66,155 @@ touch_device= engine:emu_window
# from any cemuhook compatible motion program.
# IPv4 address of the udp input server (Default "127.0.0.1")
udp_input_address=
)") DECLARE_KEY(udp_input_address) BOOST_HANA_STRING(R"(
# Port of the udp input server. (Default 26760)
udp_input_port=
)") DECLARE_KEY(udp_input_port) BOOST_HANA_STRING(R"(
# The pad to request data on. Should be between 0 (Pad 1) and 3 (Pad 4). (Default 0)
udp_pad_index=
)") DECLARE_KEY(udp_pad_index) BOOST_HANA_STRING(R"(
# Use Artic Controller when connected to Artic Base Server. (Default 0)
use_artic_base_controller=
)") DECLARE_KEY(use_artic_base_controller) BOOST_HANA_STRING(R"(
[Core]
# Whether to use the Just-In-Time (JIT) compiler for CPU emulation
# 0: Interpreter (slow), 1 (default): JIT (fast)
use_cpu_jit =
)") DECLARE_KEY(use_cpu_jit) BOOST_HANA_STRING(R"(
# 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 =
)") DECLARE_KEY(cpu_clock_percentage) BOOST_HANA_STRING(R"(
[Renderer]
# Whether to render using OpenGL
# 1: OpenGL ES (default), 2: Vulkan
graphics_api =
)") DECLARE_KEY(graphics_api) BOOST_HANA_STRING(R"(
# Whether to compile shaders on multiple worker threads (Vulkan only)
# 0: Off, 1: On (default)
async_shader_compilation =
)") DECLARE_KEY(async_shader_compilation) BOOST_HANA_STRING(R"(
# Whether to emit PICA fragment shader using SPIRV or GLSL (Vulkan only)
# 0: GLSL, 1: SPIR-V (default)
spirv_shader_gen =
)") DECLARE_KEY(spirv_shader_gen) BOOST_HANA_STRING(R"(
# Whether to disable the SPIRV optimizer. Disabling it reduces stutter, but may slightly worsen performance
# 0: Enabled, 1: Disabled (default)
disable_spirv_optimizer =
)") DECLARE_KEY(disable_spirv_optimizer) BOOST_HANA_STRING(R"(
# Whether to use hardware shaders to emulate 3DS shaders
# 0: Software, 1 (default): Hardware
use_hw_shader =
)") DECLARE_KEY(use_hw_shader) BOOST_HANA_STRING(R"(
# Whether to use accurate multiplication in hardware shaders
# 0: Off (Default. Faster, but causes issues in some games) 1: On (Slower, but correct)
shaders_accurate_mul =
)") DECLARE_KEY(shaders_accurate_mul) BOOST_HANA_STRING(R"(
# Whether to use the Just-In-Time (JIT) compiler for shader emulation
# 0: Interpreter (slow), 1 (default): JIT (fast)
use_shader_jit =
)") DECLARE_KEY(use_shader_jit) BOOST_HANA_STRING(R"(
# Overrides the sampling filter used by games. This can be useful in certain
# cases with poorly behaved games when upscaling.
# 0 (default): Game Controlled, 1: Nearest Neighbor, 2: Linear
texture_sampling =
)") DECLARE_KEY(texture_sampling) BOOST_HANA_STRING(R"(
# Forces VSync on the display thread. Can cause input delay, so only turn this on
# if you have screen tearing, which is unusual on Android
# 0 (default): Off, 1: On
use_vsync =
)") DECLARE_KEY(use_vsync) BOOST_HANA_STRING(R"(
# Reduce stuttering by storing and loading generated shaders to disk
# 0: Off, 1 (default. On)
use_disk_shader_cache =
)") DECLARE_KEY(use_disk_shader_cache) BOOST_HANA_STRING(R"(
# 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 =
)") DECLARE_KEY(resolution_factor) BOOST_HANA_STRING(R"(
# Use Integer Scaling when the layout allows
# 0: Off (default), 1: On
)") DECLARE_KEY(use_integer_scaling) BOOST_HANA_STRING(R"(
# Turns on the frame limiter, which will limit frames output to the target game speed
# 0: Off, 1: On (default)
use_frame_limit =
)") DECLARE_KEY(use_frame_limit) BOOST_HANA_STRING(R"(
# Limits the speed of the game to run no faster than this value as a percentage of target speed
# 1 - 9999: Speed limit as a percentage of target game speed. 100 (default)
frame_limit =
)") DECLARE_KEY(frame_limit) BOOST_HANA_STRING(R"(
# Alternative frame limit which can be triggered by the user
# 1 - 9999: Speed limit as a percentage of target game speed. 100 (default)
turbo_limit =
)") DECLARE_KEY(turbo_limit) BOOST_HANA_STRING(R"(
# 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 =
)") DECLARE_KEY(bg_red) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(bg_blue) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(bg_green) BOOST_HANA_STRING(R"(
# Opacity of second layer when using custom layout option (bottom screen unless swapped). Useful if positioning on top of the first layer.
custom_second_layer_opacity =
)") DECLARE_KEY(custom_second_layer_opacity) BOOST_HANA_STRING(R"(
# Whether and how Stereoscopic 3D should be rendered
# 0: Off, 1: Half Width Side by Side, 2 (default): Full Width Side by Side, 3: Anaglyph, 4: Interlaced, 5: Reverse Interlaced, 6: Cardboard VR
# 0 is no longer supported in the interface, as using render_3d_which_display = 0 has the same effect, but supported here for backwards compatibility
render_3d =
)") DECLARE_KEY(render_3d) BOOST_HANA_STRING(R"(
# Change 3D Intensity
# 0 - 255: Intensity. 0 (default)
factor_3d =
)") DECLARE_KEY(factor_3d) BOOST_HANA_STRING(R"(
# Swap Eyes in 3d
# true: Swap eyes, false (default): Do not swap eyes
swap_eyes_3d =
)") DECLARE_KEY(swap_eyes_3d) BOOST_HANA_STRING(R"(
# Which Display to render 3d mode to
# 0 (default) - None. Equivalent to render_3d=0
# 1: Both, 2: Primary Only, 3: Secondary Only
render_3d_which_display =
)") DECLARE_KEY(render_3d_which_display) BOOST_HANA_STRING(R"(
# The name of the post processing shader to apply.
# Loaded from shaders if render_3d is off or side by side.
pp_shader_name =
)") DECLARE_KEY(pp_shader_name) BOOST_HANA_STRING(R"(
# The name of the shader to apply when render_3d is anaglyph.
# Loaded from shaders/anaglyph
anaglyph_shader_name =
)") DECLARE_KEY(anaglyph_shader_name) BOOST_HANA_STRING(R"(
# Whether to enable linear filtering or not
# This is required for some shaders to work correctly
# 0: Nearest, 1 (default): Linear
filter_mode =
)") DECLARE_KEY(filter_mode) BOOST_HANA_STRING(R"(
# Delays the game render thread by the specified amount of microseconds
# Set to 0 for no delay, only useful in dynamic-fps games to simulate GPU delay.
delay_game_render_thread_us =
)") DECLARE_KEY(delay_game_render_thread_us) BOOST_HANA_STRING(R"(
# Disables rendering the right eye image.
# Disables rendering the right eye image
# Greatly improves performance in some games, but can cause flickering in others.
# 0 (default): Enable right eye rendering, 1: Disable right eye rendering
disable_right_eye_render =
# 0 : Enable right eye rendering, 1: Disable right eye rendering
)") DECLARE_KEY(disable_right_eye_render) BOOST_HANA_STRING(R"(
# Perform presentation on separate threads
# Improves performance when using Vulkan in most applications.
# Adds ~1 frame of input lag.
# 0: Enable async presentation, 1 (default): Disable async presentation
)") DECLARE_KEY(async_presentation) BOOST_HANA_STRING(R"(
# Which texture filter should be used
# 0 (default): NoFilter
# 1: Anime4K
# 2: Bicubic
# 3: ScaleForce
# 4: xBRZ
# 5: MMPX
)") DECLARE_KEY(texture_filter) BOOST_HANA_STRING(R"(
[Layout]
# Layout for the screen inside the render window, landscape mode
@ -217,12 +224,52 @@ disable_right_eye_render =
# 3: Side by Side
# 4: Hybrid
# 5: Custom Layout
layout_option =
)") DECLARE_KEY(layout_option) BOOST_HANA_STRING(R"(
# Whether or not the screens should be rotated upright
# 0 (default): Not rotated, 1: Rotated upright
)") DECLARE_KEY(upright_screen) BOOST_HANA_STRING(R"(
# Which aspect ratio should be used by the emulated 3DS screens
# 0: (default): Default
# 1: 16:9
# 2: 4:3
# 3: 21:9
# 4: 16:10
# 5: Stretch
)") DECLARE_KEY(aspect_ratio) BOOST_HANA_STRING(R"(
# Whether or not the performance overlay should be displayed
# 0 (default): Hidden, 1: Shown
)") DECLARE_KEY(performance_overlay_enable) BOOST_HANA_STRING(R"(
# Whether or not the emulation framerate should be displayed in the performance overlay
# 0: Hidden, 1 (default): Shown
)") DECLARE_KEY(performance_overlay_show_fps) BOOST_HANA_STRING(R"(
# Whether or not the emulation frame time should be displayed in the performance overlay
# 0 (default): Hidden, 1: Shown
)") DECLARE_KEY(performance_overlay_show_frame_time) BOOST_HANA_STRING(R"(
# Whether or not the emulation speed should be displayed as a percentage in the performance overlay
)") DECLARE_KEY(performance_overlay_show_speed) BOOST_HANA_STRING(R"(
# Whether or not Azahar's RAM usage should be displayed in the performance overlay
)") DECLARE_KEY(performance_overlay_show_app_ram_usage) BOOST_HANA_STRING(R"(
# Whether or not the amount of available RAM should be displayed in the performance overlay
)") DECLARE_KEY(performance_overlay_show_available_ram) BOOST_HANA_STRING(R"(
# Whether or not the battery's current temperature should be displayed in the performance overlay
)") DECLARE_KEY(performance_overlay_show_battery_temp) BOOST_HANA_STRING(R"(
# Whether or not the performance overlay should have a transparent background to aid visibility
)") DECLARE_KEY(performance_overlay_background) BOOST_HANA_STRING(R"(
[Storage]
# Whether to compress the installed CIA contents
# 0 (default): Do not compress, 1: Compress
compress_cia_installs =
)") DECLARE_KEY(compress_cia_installs) BOOST_HANA_STRING(R"(
# Position of the performance overlay
# 0: Top Left
@ -231,38 +278,37 @@ compress_cia_installs =
# 3: Bottom Left
# 4: Center Bottom
# 5: Bottom Right
performance_overlay_position =
)") DECLARE_KEY(performance_overlay_position) BOOST_HANA_STRING(R"(
# Screen Gap - adds a gap between screens in all two-screen modes
# Measured in pixels relative to the 240px default height of the screens
# Scales with the larger screen (so 24 is 10% of the larger screen height)
# Default value is 0.0
screen_gap =
)") DECLARE_KEY(screen_gap) BOOST_HANA_STRING(R"(
# Large Screen Proportion - Relative size of large:small in large screen mode
# Default value is 2.25
large_screen_proportion =
)") DECLARE_KEY(large_screen_proportion) BOOST_HANA_STRING(R"(
# Small Screen Position - where is the small screen relative to the large
# Default value is 0
# 0: Top Right 1: Middle Right 2: Bottom Right
# 3: Top Left 4: Middle left 5: Bottom Left
# 6: Above the large screen 7: Below the large screen
small_screen_position =
)") DECLARE_KEY(small_screen_position) BOOST_HANA_STRING(R"(
# Screen placement when using Custom layout option
# 0x, 0y is the top left corner of the render window.
# suggested aspect ratio for top screen is 5:3
# suggested aspect ratio for bottom screen is 4:3
custom_top_x =
custom_top_y =
custom_top_width =
custom_top_height =
custom_bottom_x =
custom_bottom_y =
custom_bottom_width =
custom_bottom_height =
)") DECLARE_KEY(custom_top_x) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(custom_top_y) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(custom_top_width) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(custom_top_height) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(custom_bottom_x) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(custom_bottom_y) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(custom_bottom_width) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(custom_bottom_height) BOOST_HANA_STRING(R"(
# Orientation option for the emulator
# 2 (default): Automatic
@ -270,32 +316,32 @@ custom_bottom_height =
# 8: Landscape (Flipped)
# 1: Portrait
# 9: Portrait (Flipped)
screen_orientation =
)") DECLARE_KEY(screen_orientation) BOOST_HANA_STRING(R"(
# Layout for the portrait mode
# 0 (default): Top and bottom screens at top, full width
# 1: Custom Layout
portrait_layout_option =
)") DECLARE_KEY(portrait_layout_option) BOOST_HANA_STRING(R"(
# Screen placement when using Portrait Custom layout option
# 0x, 0y is the top left corner of the render window.
custom_portrait_top_x =
custom_portrait_top_y =
custom_portrait_top_width =
custom_portrait_top_height =
custom_portrait_bottom_x =
custom_portrait_bottom_y =
custom_portrait_bottom_width =
custom_portrait_bottom_height =
)") DECLARE_KEY(custom_portrait_top_x) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(custom_portrait_top_y) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(custom_portrait_top_width) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(custom_portrait_top_height) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(custom_portrait_bottom_x) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(custom_portrait_bottom_y) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(custom_portrait_bottom_width) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(custom_portrait_bottom_height) BOOST_HANA_STRING(R"(
# 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 =
)") DECLARE_KEY(swap_screen) BOOST_HANA_STRING(R"(
# Expands the display area to include the cutout (or notch) area
# 0 (default): Off, 1: On
expand_to_cutout_area =
)") DECLARE_KEY(expand_to_cutout_area) BOOST_HANA_STRING(R"(
# Secondary Display Layout
# What the game should do if a secondary display is connected physically or using
@ -304,129 +350,137 @@ expand_to_cutout_area =
# 1 - Show Top Screen Only
# 2 - Show Bottom Screen Only
# 3 - Show both screens side by side
secondary_display_layout =
)") DECLARE_KEY(secondary_display_layout) BOOST_HANA_STRING(R"(
# Screen placement settings when using Cardboard VR (render3d = 4)
# 30 - 100: Screen size as a percentage of the viewport. 85 (default)
cardboard_screen_size =
)") DECLARE_KEY(cardboard_screen_size) BOOST_HANA_STRING(R"(
# -100 - 100: Screen X-Coordinate shift as a percentage of empty space. 0 (default)
cardboard_x_shift =
)") DECLARE_KEY(cardboard_x_shift) BOOST_HANA_STRING(R"(
# -100 - 100: Screen Y-Coordinate shift as a percentage of empty space. 0 (default)
cardboard_y_shift =
)") DECLARE_KEY(cardboard_y_shift) BOOST_HANA_STRING(R"(
# Which of the available layouts should be cycled by Cycle Layouts
# A list of any values from 0 to 5 inclusive (e.g. 0, 1, 2, 5)
# Default: 0, 1, 2, 3, 4, 5
# 0: Default,
# 1: Single Screen,
# 2: Large Screen,
# 3: Side by Side,
# 4: Hybrid Screens,
# 5: Custom Layout,
)") DECLARE_KEY(layouts_to_cycle) BOOST_HANA_STRING(R"(
[Utility]
# Dumps textures as PNG to dump/textures/[Title ID]/.
# 0 (default): Off, 1: On
dump_textures =
)") DECLARE_KEY(dump_textures) BOOST_HANA_STRING(R"(
# Reads PNG files from load/textures/[Title ID]/ and replaces textures.
# 0 (default): Off, 1: On
custom_textures =
)") DECLARE_KEY(custom_textures) BOOST_HANA_STRING(R"(
# Loads all custom textures into memory before booting.
# 0 (default): Off, 1: On
preload_textures =
)") DECLARE_KEY(preload_textures) BOOST_HANA_STRING(R"(
# Loads custom textures asynchronously with background threads.
# 0: Off, 1 (default): On
async_custom_loading =
)") DECLARE_KEY(async_custom_loading) BOOST_HANA_STRING(R"(
[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 audio emulation should be enabled
# 0: Disabled, 1 (default): Enabled
)") DECLARE_KEY(audio_emulation) BOOST_HANA_STRING(R"(
# Whether or not to enable the audio-stretching post-processing effect.
# 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 =
)") DECLARE_KEY(enable_audio_stretching) BOOST_HANA_STRING(R"(
# Scales audio playback speed to account for drops in emulation framerate
# 0 (default): No, 1: Yes
enable_realtime_audio =
)") DECLARE_KEY(enable_realtime_audio) BOOST_HANA_STRING(R"(
# Output volume.
# 1.0 (default): 100%, 0.0; mute
volume =
)") DECLARE_KEY(volume) BOOST_HANA_STRING(R"(
# 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 =
)") DECLARE_KEY(output_type) BOOST_HANA_STRING(R"(
# Which audio output device to use.
# auto (default): Auto-select
output_device =
)") DECLARE_KEY(output_device) BOOST_HANA_STRING(R"(
# 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 =
)") DECLARE_KEY(input_type) BOOST_HANA_STRING(R"(
# Which audio input device to use.
# auto (default): Auto-select
input_device =
)") DECLARE_KEY(input_device) BOOST_HANA_STRING(R"(
[Data Storage]
# Whether to create a virtual SD card.
# 1 (default): Yes, 0: No
use_virtual_sd =
)") DECLARE_KEY(use_virtual_sd) BOOST_HANA_STRING(R"(
[System]
# The system model that Citra will try to emulate
# 0: Old 3DS (default), 1: New 3DS
is_new_3ds =
)") DECLARE_KEY(is_new_3ds) BOOST_HANA_STRING(R"(
# Whether to use LLE system applets, if installed
# 0: No, 1 (default): Yes
lle_applets =
)") DECLARE_KEY(lle_applets) BOOST_HANA_STRING(R"(
# Whether to enable LLE modules for online play
# 0 (default): No, 1: Yes
enable_required_online_lle_modules =
)") DECLARE_KEY(enable_required_online_lle_modules) BOOST_HANA_STRING(R"(
# 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 =
)") DECLARE_KEY(region_value) BOOST_HANA_STRING(R"(
# The system language that Citra will use during emulation
# 0: Japanese, 1: English (default), 2: French, 3: German, 4: Italian, 5: Spanish,
# 6: Simplified Chinese, 7: Korean, 8: Dutch, 9: Portuguese, 10: Russian, 11: Traditional Chinese
language =
)") DECLARE_KEY(language) BOOST_HANA_STRING(R"(
# The clock to use when citra starts
# 0: System clock (default), 1: fixed time
init_clock =
)") DECLARE_KEY(init_clock) BOOST_HANA_STRING(R"(
# 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 =
)") DECLARE_KEY(init_time) BOOST_HANA_STRING(R"(
# The system ticks count to use when citra starts
# 0: Random (default), 1: Fixed
init_ticks_type =
)") DECLARE_KEY(init_ticks_type) BOOST_HANA_STRING(R"(
# Tick count to use when init_ticks_type is set to Fixed.
# Defaults to 0.
init_ticks_override =
)") DECLARE_KEY(init_ticks_override) BOOST_HANA_STRING(R"(
# Number of steps per hour reported by the pedometer. Range from 0 to 65,535.
# Defaults to 0.
steps_per_hour =
)") DECLARE_KEY(steps_per_hour) BOOST_HANA_STRING(R"(
# Plugin loader state, if enabled plugins will be loaded from the SD card.
# You can also set if homebrew apps are allowed to enable the plugin loader
plugin_loader =
allow_plugin_loader =
)") DECLARE_KEY(plugin_loader) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(allow_plugin_loader) BOOST_HANA_STRING(R"(
# Apply region free patch to installed applications
# Patches the region of installed applications to be region free, so that they always appear on the home menu.
# 0: Disabled, 1 (default): Enabled
apply_region_free_patch =
)") DECLARE_KEY(apply_region_free_patch) BOOST_HANA_STRING(R"(
[Camera]
# Which camera engine to use for the right outer camera
@ -437,66 +491,72 @@ apply_region_free_patch =
# If you don't specify an ID, the default setting will be used. For outer cameras,
# the back-facing camera will be used. For the inner camera, the front-facing
# camera will be used. Please note that 'Legacy' cameras are not supported.
camera_outer_right_name =
)") DECLARE_KEY(camera_outer_right_name) BOOST_HANA_STRING(R"(
# A config string for the right outer camera. Its meaning is defined by the camera engine
camera_outer_right_config =
)") DECLARE_KEY(camera_outer_right_config) BOOST_HANA_STRING(R"(
# The image flip to apply
# 0: None (default), 1: Horizontal, 2: Vertical, 3: Reverse
camera_outer_right_flip =
)") DECLARE_KEY(camera_outer_right_flip) BOOST_HANA_STRING(R"(
# ... for the left outer camera
camera_outer_left_name =
camera_outer_left_config =
camera_outer_left_flip =
)") DECLARE_KEY(camera_outer_left_name) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(camera_outer_left_config) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(camera_outer_left_flip) BOOST_HANA_STRING(R"(
# ... for the inner camera
camera_inner_name =
camera_inner_config =
camera_inner_flip =
)") DECLARE_KEY(camera_inner_name) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(camera_inner_config) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(camera_inner_flip) BOOST_HANA_STRING(R"(
[Miscellaneous]
# A filter which removes logs below a certain logging level.
# Examples: *:Debug Kernel.SVC:Trace Service.*:Critical
log_filter = *:Info
)") DECLARE_KEY(log_filter) BOOST_HANA_STRING(R"(
# Whether or not Azahar-related images should be hidden from the Android gallery
# 0 (default): No, 1: Yes
)") DECLARE_KEY(android_hide_images) BOOST_HANA_STRING(R"(
[Debugging]
# Record frame time data, can be found in the log directory. Boolean value
record_frame_times =
)") DECLARE_KEY(record_frame_times) BOOST_HANA_STRING(R"(
# Whether to enable additional debugging information during emulation
# 0 (default): Off, 1: On
renderer_debug =
)") DECLARE_KEY(renderer_debug) BOOST_HANA_STRING(R"(
# Port for listening to GDB connections.
use_gdbstub=false
gdbstub_port=24689
)") DECLARE_KEY(use_gdbstub) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(gdbstub_port) BOOST_HANA_STRING(R"(
# Flush log output on every message
# Immediately commits the debug log to file. Use this if Azahar crashes and the log output is being cut.
instant_debug_log =
)") DECLARE_KEY(instant_debug_log) BOOST_HANA_STRING(R"(
# Enable RPC server for scripting purposes. Allows accessing guest memory remotely.
# 0 (default): Off, 1: On
enable_rpc_server =
)") DECLARE_KEY(enable_rpc_server) BOOST_HANA_STRING(R"(
# Delay the start of apps when LLE modules are enabled
# 0: Off, 1 (default): On
delay_start_for_lle_modules =
)") DECLARE_KEY(delay_start_for_lle_modules) BOOST_HANA_STRING(R"(
# Force deterministic async operations
# Only needed for debugging, makes performance worse if enabled
# 0: Off (default), 1: On
deterministic_async_operations =
)") DECLARE_KEY(deterministic_async_operations) BOOST_HANA_STRING(R"(
# To LLE a service module add "LLE\<module name>=true"
[WebService]
# URL for Web API
web_api_url =
)") DECLARE_KEY(web_api_url) BOOST_HANA_STRING(R"(
# Username and token for Citra Web Service
citra_username =
citra_token =
)";
}
)") DECLARE_KEY(citra_username) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(citra_token) BOOST_HANA_STRING("\n")).c_str();
// clang-format on
} // namespace DefaultINI

View file

@ -0,0 +1,19 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <jni.h>
#define JNI_DEFINE_KEY(KEY, KEY_JNI_ESCAPED) \
JNIEXPORT jstring JNICALL \
Java_org_citra_citra_1emu_features_settings_SettingKeys_##KEY_JNI_ESCAPED( \
JNIEnv* env, jobject obj \
) { \
return env->NewStringUTF(#KEY); \
}
extern "C" {
@JNI_SETTING_KEY_DEFINITIONS@
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="24dp"
android:paddingBottom="24dp"
android:defaultFocusHighlightEnabled="false"
android:focusable="true"
android:focusableInTouchMode="true"
android:focusedByDefault="true"
android:orientation="vertical">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_title"
style="@style/TextAppearance.Material3.HeadlineSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
tools:text="Auto-Map Controller" />
<ImageView
android:id="@+id/image_face_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:contentDescription="@string/auto_map_image_description"
tools:ignore="ImageContrastCheck" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_message"
style="@style/TextAppearance.Material3.BodyLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
tools:text="Press this button!" />
<Button
android:id="@+id/button_cancel"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:focusable="false"
android:text="@android:string/cancel" />
</LinearLayout>

View file

@ -100,7 +100,6 @@
<string name="search_recently_added">Acabats d\'afegir</string>
<string name="search_installed">Instal·lats</string>
<!-- Input related strings -->
<string name="controller_circlepad">Pad circular</string>
<string name="controller_c">Palanca C</string>
<string name="controller_hotkeys">Tecles de drecera</string>

View file

@ -107,7 +107,6 @@
<string name="search_recently_added">For nyligt tilføjet</string>
<string name="search_installed">Installeret</string>
<!-- Input related strings -->
<string name="controller_circlepad">Circle Pad</string>
<string name="controller_c">C-Stick</string>
<string name="controller_hotkeys">Genvejstaster</string>

View file

@ -104,7 +104,6 @@
<string name="search_recently_added">Recién Añadidos</string>
<string name="search_installed">Instalados</string>
<!-- Input related strings -->
<string name="controller_circlepad">Pad Circular</string>
<string name="controller_c">Palanca C</string>
<string name="controller_hotkeys">Teclas de atajo</string>

View file

@ -107,7 +107,6 @@
<string name="search_recently_added">Ostatnio dodane</string>
<string name="search_installed">Zainstalowane</string>
<!-- Input related strings -->
<string name="controller_circlepad">Analog</string>
<string name="controller_c">C-Stick</string>
<string name="controller_hotkeys">Skróty klawiszowe</string>
@ -229,6 +228,8 @@
<string name="async_shaders_description">Kompiluje shadery w tle, aby zmniejszyć zacinanie się podczas rozgrywki. Po włączeniu należy spodziewać się tymczasowych błędów graficznych</string>
<string name="linear_filtering">Filtrowanie Linear</string>
<string name="linear_filtering_description">Włącza filtrowanie liniowe, które sprawia, że grafika w grach jest płynniejsza.</string>
<string name="use_integer_scaling">Skalowanie całkowitoliczbowe</string>
<string name="use_integer_scaling_description">Skaluje ekrany za pomocą całkowitego mnożnika oryginalnego ekranu 3DS. W przypadku układów z dwoma różnymi rozmiarami ekranu największy ekran jest skalowany całkowicie.</string>
<string name="texture_filter_name">Filtr tekstur</string>
<string name="texture_filter_description">Ulepsza oprawę wizualną aplikacji poprzez zastosowanie filtrów do tekstur. Obsługiwane filtry to Anime4K Ultrafast, Bicubic, ScaleForce, xBRZ freescale i MMPX.</string>
<string name="delay_render_thread">Opóźnienie Renderowania Wątku Gry</string>

View file

@ -107,7 +107,6 @@
<string name="search_recently_added">Recentemente adicionado</string>
<string name="search_installed">Instalado</string>
<!-- Input related strings -->
<string name="controller_circlepad">Analógico</string>
<string name="controller_c">Direcional C-Stick</string>
<string name="controller_hotkeys">Teclas de atalho</string>
@ -118,6 +117,8 @@
<string name="controller_dpad_axis_description">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).</string>
<string name="controller_dpad_button">D-Pad (Botão)</string>
<string name="controller_dpad_button_description">Só mapeie o D-pad para isso se você se você estiver encontrando problemas com o mapeamento de botão do D-Pad (Eixo).</string>
<string name="controller_axis_vertical">Eixo Vertical</string>
<string name="controller_axis_horizontal">Eixo Horizontal</string>
<string name="direction_up">Cima</string>
<string name="direction_down">Baixo</string>
<string name="direction_left">Esquerda</string>
@ -126,6 +127,8 @@
<string name="input_dialog_description">Pressione ou mova uma entrada.</string>
<string name="input_binding">Mapeamento de controles</string>
<string name="input_binding_description">Pressione ou mova um botão/alavanca para mapear para %1$s.</string>
<string name="input_binding_description_vertical_axis">Pressione para CIMA no seu joystick.</string>
<string name="input_binding_description_horizontal_axis">Pressione para a DIREITA no seu joystick.</string>
<string name="button_home">Menu Principal</string>
<string name="button_swap">Trocar telas</string>
<string name="button_turbo">Turbo</string>
@ -546,6 +549,8 @@
<!-- Disk Shader Cache -->
<string name="preparing_shaders">Preparando Shaders</string>
<string name="building_shaders">Construindo %s</string>
<!-- About Game Dialog -->
<string name="play">Jogar</string>
<string name="uninstall_cia">Desinstalar Aplicativo</string>

View file

@ -58,7 +58,6 @@
<string name="search_recently_added">Добавленные недавно</string>
<string name="search_installed">Установленные</string>
<!-- Input related strings -->
<string name="controller_circlepad">Джойстик</string>
<string name="controller_c">C-стик</string>
<string name="controller_hotkeys">Горячие клавиши</string>

View file

@ -99,7 +99,6 @@
<string name="search_recently_added">Son eklenen</string>
<string name="search_installed">Yüklü</string>
<!-- Input related strings -->
<string name="controller_circlepad">Circle Pad</string>
<string name="controller_c">C-Stick</string>
<string name="controller_hotkeys">Kısayol tuşları</string>

View file

@ -100,7 +100,6 @@
<string name="search_recently_added">最近添加</string>
<string name="search_installed">已安装</string>
<!-- Input related strings -->
<string name="controller_circlepad">方向摇杆</string>
<string name="controller_c">C 摇杆</string>
<string name="controller_hotkeys">热键</string>

View file

@ -100,7 +100,6 @@
<string name="search_recently_added">Kürzlich hinzugefügt</string>
<string name="search_installed">Installiert</string>
<!-- Input related strings -->
<string name="controller_circlepad">Schiebepad</string>
<string name="controller_c">C-Stick</string>
<string name="controller_hotkeys">Tastenkürzel</string>

View file

@ -108,9 +108,17 @@
<string name="search_installed">Installé</string>
<!-- Input related strings -->
<string name="controller_auto_map">Assigner les boutons de la manette automatiquement</string>
<string name="controller_auto_map_description">Applique un assignement standard de manette pour tous les boutons et axes</string>
<string name="auto_map_prompt">Appuyez sur ce bouton de votre manette !</string>
<string name="auto_map_image_description">Les quatre boutons situés à droite de la 3DS avec le bouton A surligné</string>
<string name="controller_clear_all">Réinitialiser toutes les assignations de touche</string>
<string name="controller_clear_all_confirm">Cela effacera toutes les assignations de touche de manette actuelles.</string>
<string name="controller_circlepad">Pad circulaire</string>
<string name="controller_c">Stick C</string>
<string name="controller_hotkeys">Raccourcis</string>
<string name="controller_hotkeys_description">Si la touche \"Activer les raccourcis\" est assignée, celle-ci doit être pressée en plus de tout autre touche de raccourci.</string>
<string name="controller_hotkey_enable_button">Activer les raccourcis</string>
<string name="controller_triggers">Gachettes</string>
<string name="controller_trigger">Déclencheur</string>
<string name="controller_dpad">Manette +</string>
@ -229,6 +237,8 @@
<string name="async_shaders_description">Compile les shaders en arrière-plan pour réduire les saccades pendant le jeu. Lorsqu\'il est activé, prévoyez des problèmes graphiques temporaires.</string>
<string name="linear_filtering">Filtrage linéaire</string>
<string name="linear_filtering_description">Active le filtrage linéaire, qui améliorera le lissage graphique du jeu.</string>
<string name="use_integer_scaling">Redimensionnement par nombre entier</string>
<string name="use_integer_scaling_description">Redimensionne les écrans avec un multiple entier de la taille de l\'écran original de la 3DS. Pour les dispositions avec deux tailles d\'écrans différentes, c\'est l\'écran le plus large qui est redimensionné par un multiple entier.</string>
<string name="texture_filter_name">Filtrage des textures</string>
<string name="texture_filter_description">Améliore l\'aspect visuel des applications en appliquant un filtre aux textures. Les filtres pris en charge sont Anime4K Ultrafast, Bicubique, ScaleForce, xBRZ freescale et MMPX.</string>
<string name="delay_render_thread">Retarder le thread de rendu du jeu</string>
@ -336,6 +346,8 @@
<string name="layout_screen_orientation_landscape_reverse">Paysage inversé</string>
<string name="layout_screen_orientation_portrait">Portait</string>
<string name="layout_screen_orientation_portrait_reverse">Portait inversé</string>
<string name="layouts_to_cycle">Dispositions à alterner</string>
<string name="layouts_to_cycle_description">Quelles dispositions sont alternées par le raccourci Alterner les dispositions</string>
<string name="aspect_ratio_default">Par défaut</string>
<string name="aspect_ratio_16_9">16:9</string>
<string name="aspect_ratio_4_3">4:3</string>
@ -550,6 +562,8 @@
<!-- Disk Shader Cache -->
<string name="preparing_shaders">Préparation des shaders</string>
<string name="building_shaders">Construction %s</string>
<!-- About Game Dialog -->
<string name="play">Jouer</string>
<string name="uninstall_cia">Désinstaller l\'application</string>

View file

@ -1,10 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_disclaimer">Questo software eseguirà applicazioni per la console portatile Nintendo 3DS. Nessun titolo è incluso.
Prima di iniziare con l\'emulazione, seleziona una cartella che conterrà i dati utente.
Cos\'è questo:
<a href='https://web.archive.org/web/20240304193549/https://github.com/citra-emu/citra/wiki/Citra-Android-user-data-and-storage'> Wiki - Citra Android user data and storage</a></string>
<string name="app_disclaimer">Questo software eseguirà applicazioni per la console portatile Nintendo 3DS. Nessun titolo è incluso.\n\nPrima di iniziare con l\'emulazione, seleziona una cartella che conterrà i dati utente.\n\nCos\'è questo:\n<a href='https://web.archive.org/web/20240304193549/https://github.com/citra-emu/citra/wiki/Citra-Android-user-data-and-storage'> Wiki - Citra Android user data and storage</a></string>
<string name="app_notification_channel_description">Notifiche emulatore Azahar 3DS</string>
<string name="app_notification_running">Azahar è in esecuzione</string>
<string name="app_game_install_description">Ora è necessario selezionare una cartella Applicazioni. Azahar mostrerà tutte le ROM 3DS all\'interno della cartella selezionata nell\'app. Le ROM CIA, gli aggiornamenti e DLC dovranno essere installati separatamente cliccando sulla cartella e selezionando installa CIA.</string>
@ -24,8 +21,7 @@ Cos\'è questo:
<string name="install_gpu_driver_description">Installa driver alternativi per possibili miglioramenti delle prestazioni o dell\'accuratezza</string>
<string name="driver_already_installed">Driver già installato</string>
<string name="custom_driver_not_supported">Driver personalizzato non supportato</string>
<string name="custom_driver_not_supported_description">Il caricamento di driver personalizzati non è disponibile per questo dispositivo.
Controlla ancora questa opzione in futuro per controllare se il supporto è stato aggiunto!</string>
<string name="custom_driver_not_supported_description">Il caricamento di driver personalizzati non è supportato su questo dispositivo.\nControlla di nuovo in futuro per verificare eventuali aggiornamenti!</string>
<string name="share_log_not_found">Nessun file di log trovato</string>
<string name="select_games_folder">Seleziona cartella applicazioni</string>
<string name="select_games_folder_description">Permette ad Azahar di popolare la lista di applicazioni</string>
@ -57,26 +53,25 @@ Controlla ancora questa opzione in futuro per controllare se il supporto è stat
<string name="licenses">Licenze</string>
<!-- Setup strings -->
<string name="welcome">Benvenuto!</string>
<string name="welcome_description">Scopri come impostare Azahar e immergiti nell\'emulazione.</string>
<string name="welcome_description">Scopri come impostare &lt;b>Azahar&lt;/b> e immergiti nell\'emulazione.</string>
<string name="get_started">Inizia</string>
<string name="step_complete">Completato!</string>
<string name="games">Applicazioni</string>
<string name="games_description">Seleziona la tua cartella &lt;b>Applicazioni&lt;/b> usando il pulsante qui sotto.</string>
<string name="done">Fatto</string>
<string name="done_description">Adesso sei pronto.
Divertiti usando l\'emulatore!</string>
<string name="done_description">Adesso sei pronto.\nDivertiti usando l\'emulatore!</string>
<string name="text_continue">Continua</string>
<string name="notifications">Notifiche</string>
<string name="notifications_description">Concedi il permesso di notifica usando il pulsante qui sotto.</string>
<string name="give_permission">Concedi il permesso</string>
<string name="notification_warning">Vuoi saltare la concessione del permesso di notifica?</string>
<string name="notification_warning">Vuoi saltare l\'autorizzazione per le notifiche?</string>
<string name="notification_warning_description">Azahar non avrà il permesso di notificarti con informazioni importanti.</string>
<string name="filesystem_permission_warning">Permessi mancanti</string>
<string name="filesystem_permission_warning_description">Azahar richiede l\'autorizzazione per gestire i file su questo dispositivo per archiviare e gestire i propri dati.\n\nDare il permesso \"Filesystem\" prima di continuare.</string>
<string name="camera_permission">Fotocamera</string>
<string name="camera_permission_description">Concedi il permesso alla fotocamera qui sotto per emulare la fotocamera del 3DS</string>
<string name="camera_permission_description">Concedi il permesso alla fotocamera qui sotto per emulare la fotocamera del 3DS.</string>
<string name="microphone_permission">Microfono</string>
<string name="microphone_permission_description">Concedi il permesso all\'utilizzo del microfono per emulare il microfono del 3DS</string>
<string name="microphone_permission_description">Concedi il permesso all\'utilizzo del microfono per emulare il microfono del 3DS.</string>
<string name="filesystem_permission">Filesystem</string>
<string name="filesystem_permission_description">Dare il permesso del file system qui sotto per consentire ad Azahar di archiviare file.</string>
<string name="permission_denied">Permesso negato</string>
@ -113,9 +108,17 @@ Divertiti usando l\'emulatore!</string>
<string name="search_installed">Installati</string>
<!-- Input related strings -->
<string name="controller_auto_map">Mappatura automatica controller</string>
<string name="controller_auto_map_description">Applica la mappatura standard per tutti i tasti e gli assi</string>
<string name="auto_map_prompt">Premi questo tasto sul controller!</string>
<string name="auto_map_image_description">Schema tasti 3DS con il tasto A evidenziato</string>
<string name="controller_clear_all">Rimuovi tutte le assegnazioni</string>
<string name="controller_clear_all_confirm">Tutte le assegnazioni attuali dei controller verranno rimosse.</string>
<string name="controller_circlepad">Pad scorrevole</string>
<string name="controller_c">Levetta C</string>
<string name="controller_hotkeys">Scorciatoie</string>
<string name="controller_hotkeys">Tasti rapidi</string>
<string name="controller_hotkeys_description">Se il tasto \"Abilita tasti rapidi\" è assegnato, va premuto insieme al tasto rapido configurato.</string>
<string name="controller_hotkey_enable_button">Abilita tasti rapidi</string>
<string name="controller_triggers">Grilletti</string>
<string name="controller_trigger">Grilletto</string>
<string name="controller_dpad">Tasti direzionali</string>
@ -133,6 +136,8 @@ Divertiti usando l\'emulatore!</string>
<string name="input_dialog_description">Premi o sposta un comando</string>
<string name="input_binding">Assegnazione Input</string>
<string name="input_binding_description">Premi o muovi un comando per assegnarlo a %1$s.</string>
<string name="input_binding_description_vertical_axis">Premi UP sul tuo controller.</string>
<string name="input_binding_description_horizontal_axis">Premi RIGHT sul tuo controller.</string>
<string name="button_home">Home</string>
<string name="button_swap">Inverti schermi</string>
<string name="button_turbo">Turbo</string>
@ -232,6 +237,8 @@ Divertiti usando l\'emulatore!</string>
<string name="async_shaders_description">Compila gli shader in background per ridurre gli scatti durante il gioco. Se l\'opzione è abilitata, potrebbero verificarsi dei glitch grafici temporanei.</string>
<string name="linear_filtering">Filtraggio lineare</string>
<string name="linear_filtering_description">Abilita il filtraggio lineare, che fa sembrare più smussata la grafica dei giochi.</string>
<string name="use_integer_scaling">Scalabilità intera</string>
<string name="use_integer_scaling_description">Scala gli schermi usando un multiplo intero della risoluzione originale del 3DS. Nei layout con schermi di dimensioni diverse, la scalabilità intera viene applicata allo schermo più grande.</string>
<string name="texture_filter_name">Filtro texture</string>
<string name="texture_filter_description">Migliora la grafica delle applicazioni applicando un filtro alle texture. I filtri supportati sono Anime4k Ultrafast, Bicubic, ScaleForce, xBRZ freescale e MMPX.</string>
<string name="delay_render_thread">Ritarda il thread di rendering del gioco</string>
@ -244,13 +251,13 @@ Divertiti usando l\'emulatore!</string>
<string name="asynchronous_gpu">Abilita l\'emulazione GPU asincrona</string>
<string name="asynchronous_gpu_description">Usa un thread separato per l\'emulazione asincrona della GPU. Quando è abilitato le prestazioni saranno migliori.</string>
<string name="frame_limit_enable">Limita velocità</string>
<string name="frame_limit_enable_description">Quando l\'opzione è abilitata, la velocità di emulazione sarà limitata a una percentuale specificata della velocità normale. Se è disabilitata, la velocità di emulazione non verrà limitata e la scorciatoia della velocità Turbo non funzionerà.</string>
<string name="frame_limit_enable_description">Se attiva, la velocità di emulazione viene limitata a una percentuale specifica. Se disattivata, la velocità non avrà limiti e il tasto rapido per il turbo non funzionerà.</string>
<string name="frame_limit_slider">Limite della velocità in percentuale</string>
<string name="frame_limit_slider_description">Specifica a quale percentuale limitare la velocità d\'emulazione. Utilizzando l\'impostazione predefinita di 100% l\'emulazione sarà limitata alla velocità normale. Valori superiori o inferiori aumenteranno o diminuiranno il limite di velocità.</string>
<string name="android_hide_images">Nascondi le immagini 3DS da Android</string>
<string name="android_hide_images_description">Impedisci che le immagini della fotocamera 3DS, degli screenshot e delle texture personalizzate vengano indicizzate da Android e visualizzate nella galleria. Potrebbe essere necessario riavviare il dispositivo dopo aver modificato questa impostazione per avere effetto.</string>
<string name="turbo_limit">Limite della velocità Turbo</string>
<string name="turbo_limit_description">Limite di velocità dell\'emulazione usato quando la scorciatoia della velocità Turbo è attiva.</string>
<string name="turbo_limit_description">Limite di velocità dell\'emulazione usato quando il tasto rapido della velocità Turbo è attiva.</string>
<string name="expand_to_cutout_area">Espandi all\'area di ritaglio</string>
<string name="expand_to_cutout_area_description">Espande l\'area di visualizzazione per includere l\'area di ritaglio (o notch).</string>
<string name="internal_resolution">Risoluzione interna</string>
@ -339,6 +346,8 @@ Divertiti usando l\'emulatore!</string>
<string name="layout_screen_orientation_landscape_reverse">Orizzontale rovesciato</string>
<string name="layout_screen_orientation_portrait">Verticale</string>
<string name="layout_screen_orientation_portrait_reverse">Verticale rovesciato</string>
<string name="layouts_to_cycle">Layout da ciclare</string>
<string name="layouts_to_cycle_description">Quali layout vengono alternati dal tasto rapido \"Cicla layout\"</string>
<string name="aspect_ratio_default">Default</string>
<string name="aspect_ratio_16_9">16:9</string>
<string name="aspect_ratio_4_3">4:3</string>
@ -367,14 +376,14 @@ Divertiti usando l\'emulatore!</string>
<string name="install">Installa</string>
<string name="delete">Cancella</string>
<string name="reset_all_settings">Reimpostare tutte le impostazioni?</string>
<string name="reset_all_settings_description">Tutte le impostazioni avanzate verranno reimpostate alla loro configurazione predefinita. Questa operazione non può essere annullata.</string>
<string name="reset_all_settings_description">Tutte le impostazioni avanzate verranno ripristinate ai valori predefiniti. L\'operazione non è annullabile.</string>
<string name="settings_reset">Reimpostazione impostazioni</string>
<string name="select_rtc_date">Seleziona data RTC</string>
<string name="select_rtc_time">Seleziona orario RTC</string>
<string name="reset_setting_confirmation">Vuoi reimpostare questa opzione al suo valore predefinito?</string>
<string name="setting_not_editable">Non puoi modificare questo ora</string>
<string name="setting_disabled">Impostazione disabilitata</string>
<string name="setting_disabled_description">Questa impostazione è attualmente disattivata a causa di un\'altra impostazione che non è il valore appropriato.</string>
<string name="setting_disabled_description">Questa impostazione è disattivata perché un\'altra opzione non ha il valore richiesto.</string>
<string name="setting_not_editable_description">Questa opzione non può essere modificata mentre un gioco è in esecuzione.</string>
<string name="auto_select">Seleziona automaticamente</string>
<string name="start">Avvia</string>
@ -383,7 +392,7 @@ Divertiti usando l\'emulatore!</string>
<string name="information">Informazione</string>
<!-- Add Directory Screen-->
<string name="select_game_folder">Seleziona cartella dei giochi</string>
<string name="select_game_folder">Seleziona cartella del gioco</string>
<!-- Game Properties -->
<string name="properties">Proprietà</string>
@ -553,6 +562,8 @@ Divertiti usando l\'emulatore!</string>
<!-- Disk Shader Cache -->
<string name="preparing_shaders">Preparazione degli shader</string>
<string name="building_shaders">Compilazione %s</string>
<!-- About Game Dialog -->
<string name="play">Riproduci</string>
<string name="uninstall_cia">Disinstalla applicazione</string>

View file

@ -3,7 +3,6 @@
<!-- Home Strings -->
<string name="grid_menu_core_settings">Innstillinger</string>
<!-- Input related strings -->
<string name="controller_circlepad">Gliplate</string>
<string name="controller_c">C-Spak</string>
<string name="controller_triggers">Utløser</string>

View file

@ -73,7 +73,6 @@ Vink deze optie in de toekomst nogmaals aan om te zien of er ondersteuning is to
<string name="search_recently_added">Recent Toegevoegd</string>
<string name="search_installed">Geïnstalleerd</string>
<!-- Input related strings -->
<string name="controller_circlepad">Circle Pad</string>
<string name="controller_c">C-Stick</string>
<string name="controller_hotkeys">Sneltoetsen</string>

View file

@ -107,7 +107,6 @@
<string name="search_recently_added">Nyligen tillagd</string>
<string name="search_installed">Installerad</string>
<!-- Input related strings -->
<string name="controller_circlepad">Circle Pad</string>
<string name="controller_c">C-spak</string>
<string name="controller_hotkeys">Snabbtangenter</string>
@ -550,6 +549,8 @@
<!-- Disk Shader Cache -->
<string name="preparing_shaders">Förbereder shaders</string>
<string name="building_shaders">Bygger %s</string>
<!-- About Game Dialog -->
<string name="play">Spela</string>
<string name="uninstall_cia">Avinstallera applikation</string>

View file

@ -119,9 +119,17 @@
<string name="search_installed">Installed</string>
<!-- Input related strings -->
<string name="controller_auto_map">Auto-Map Controller</string>
<string name="controller_auto_map_description">Apply standard gamepad mapping for all buttons and axes</string>
<string name="auto_map_prompt">Press this button on your controller!</string>
<string name="auto_map_image_description">3DS face button diamond with A button highlighted</string>
<string name="controller_clear_all">Clear All Bindings</string>
<string name="controller_clear_all_confirm">This will remove all current controller bindings.</string>
<string name="controller_circlepad">Circle Pad</string>
<string name="controller_c">C-Stick</string>
<string name="controller_hotkeys">Hotkeys</string>
<string name="controller_hotkeys_description">If the "Hotkey Enable" key is mapped, that key must be pressed in addition to the mapped hotkey</string>
<string name="controller_hotkey_enable_button">Hotkey Enable</string>
<string name="controller_triggers">Triggers</string>
<string name="controller_trigger">Trigger</string>
<string name="controller_dpad">D-Pad</string>
@ -255,6 +263,8 @@
<string name="async_shaders_description">Compiles shaders in the background to reduce stuttering during gameplay. When enabled expect temporary graphical glitches</string>
<string name="linear_filtering">Linear Filtering</string>
<string name="linear_filtering_description">Enables linear filtering, which causes game visuals to appear smoother.</string>
<string name="use_integer_scaling">Integer Scaling</string>
<string name="use_integer_scaling_description">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.</string>
<string name="texture_filter_name">Texture Filter</string>
<string name="texture_filter_description">Enhances the visuals of applications by applying a filter to textures. The supported filters are Anime4K Ultrafast, Bicubic, ScaleForce, xBRZ freescale, and MMPX.</string>
<string name="delay_render_thread">Delay Game Render Thread</string>
@ -362,6 +372,8 @@
<string name="layout_screen_orientation_landscape_reverse">Reverse Landscape</string>
<string name="layout_screen_orientation_portrait">Portrait</string>
<string name="layout_screen_orientation_portrait_reverse">Reverse Portrait</string>
<string name="layouts_to_cycle">Layouts to Cycle</string>
<string name="layouts_to_cycle_description">Which layouts are cycled by the Cycle Layout hotkey</string>
<string name="aspect_ratio_default">Default</string>
<string name="aspect_ratio_16_9">16:9</string>
<string name="aspect_ratio_4_3">4:3</string>

View file

@ -38,6 +38,7 @@ add_library(audio_core STATIC
$<$<BOOL:${ENABLE_SDL2}>:sdl2_sink.cpp sdl2_sink.h>
$<$<BOOL:${ENABLE_CUBEB}>:cubeb_sink.cpp cubeb_sink.h cubeb_input.cpp cubeb_input.h>
$<$<BOOL:${ENABLE_LIBRETRO}>:libretro_sink.cpp libretro_sink.h libretro_input.cpp libretro_input.h>
$<$<BOOL:${ENABLE_OPENAL}>:openal_input.cpp openal_input.h openal_sink.cpp openal_sink.h>
)

View file

@ -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<s16, 2> 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()) {

View file

@ -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<Input> {
if (!system.HasMicPermission()) {
LOG_WARNING(Audio,
"Microphone permission denied, falling back to null input.");
return std::make_unique<NullInput>();
}
return std::make_unique<LibRetroInput>();
},
[] { return std::vector<std::string>{"LibRetro Microphone"}; }},
#endif
#ifdef HAVE_CUBEB
InputDetails{InputType::Cubeb, "Real Device (Cubeb)", true,
[](Core::System& system, std::string_view device_id) -> std::unique_ptr<Input> {

View file

@ -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,9 @@ enum class InputType : u32 {
Static = 2,
Cubeb = 3,
OpenAL = 4,
#ifdef HAVE_LIBRETRO
LibRetro = 5,
#endif
};
struct InputDetails {

View file

@ -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 <algorithm>
#include <atomic>
#include <cstring>
#include <mutex>
#include <optional>
#include <vector>
#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<retro_microphone_interface> 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<s16, 4096> sample_buffer;
// Temporary buffer for reading from frontend
std::vector<s16> 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(&params);
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<Impl>()) {
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<size_t>(impl->read_buffer.size()));
if (samples_read > 0) {
impl->sample_buffer.Push(
std::span<const s16>(impl->read_buffer.data(), static_cast<size_t>(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<s16> 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<double>(parameters.sample_rate) / impl->native_sample_rate;
auto output_count = static_cast<std::size_t>(raw_samples.size() * ratio);
if (output_count == 0) {
return {};
}
std::vector<s16> resampled(output_count);
for (std::size_t i = 0; i < output_count; i++) {
double src_pos = i / ratio;
auto idx = static_cast<std::size_t>(src_pos);
double frac = src_pos - idx;
if (idx + 1 < raw_samples.size()) {
resampled[i] =
static_cast<s16>(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<u16>(sample) ^ 0x8000;
};
constexpr auto convert_s16_to_s8 = [](s16 sample) -> s8 {
return static_cast<s8>(sample >> 8);
};
constexpr auto convert_s16_to_u8 = [](s16 sample) -> u8 {
return static_cast<u8>((static_cast<u16>(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<u8>(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<u8>(converted & 0xFF));
output.push_back(static_cast<u8>(converted >> 8));
}
} else {
// Signed 16-bit - just copy the raw bytes
const u8* data = reinterpret_cast<const u8*>(raw_samples.data());
output.insert(output.end(), data, data + raw_samples.size() * 2);
}
}
return output;
}
LibRetroInput* GetLibRetroInput() {
return g_libretro_input;
}
} // namespace AudioCore

View file

@ -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 <memory>
#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> 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

View file

@ -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<const s16*>(data), num_samples);
}
std::vector<std::string> ListLibretroSinkDevices() {
return std::vector<std::string>{"LibRetro"};
}
} // namespace AudioCore

View file

@ -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 <cstddef>
#include <string>
#include <vector>
#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<void(s16*, std::size_t)> cb) override {};
bool ImmediateSubmission() override {
return true;
}
void PushSamples(const void* data, std::size_t num_samples) override;
};
std::vector<std::string> ListLibretroSinkDevices();
} // namespace AudioCore

View file

@ -5,7 +5,7 @@
#pragma once
#include <functional>
#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<void(s16*, std::size_t)> 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

View file

@ -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<Sink> {
return std::make_unique<LibRetroSink>(std::string(device_id));
},
&ListLibretroSinkDevices},
#endif
#ifdef HAVE_CUBEB
SinkDetails{SinkType::Cubeb, "Cubeb",
[](std::string_view device_id) -> std::unique_ptr<Sink> {

View file

@ -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,9 @@ enum class SinkType : u32 {
Cubeb = 2,
OpenAL = 3,
SDL2 = 4,
#ifdef HAVE_LIBRETRO
LibRetro = 5,
#endif
};
struct SinkDetails {

View file

@ -0,0 +1,96 @@
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/$<CONFIG>)
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
$<$<BOOL:${ENABLE_VULKAN}>: 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 tsl::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
$<TARGET_OBJECTS:azahar_libretro_common>)
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::boost dds-ktx libretro tsl::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)
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()

View file

@ -0,0 +1,759 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <list>
#include <numeric>
#include <vector>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#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 <streams/file_stream_transforms.h>
#endif
class CitraLibRetro {
public:
CitraLibRetro() : log_filter(Common::Log::Level::Debug) {}
Common::Log::Filter log_filter;
std::unique_ptr<EmuWindow_LibRetro> 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<Frontend::ImageInterface>());
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<EmuWindow_LibRetro>();
}
// Update the framebuffer sizing.
emu_instance->emu_window->UpdateLayout();
Core::System::GetInstance().ApplySettings();
}
/**
* 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();
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_button_state = false;
static bool screens_swapped = false;
bool screen_swap_btn =
!!LibRetro::CheckInput(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L3);
if (screen_swap_btn != screen_swap_button_state) {
if (LibRetro::settings.swap_screen_mode == "Toggle") {
if (!screen_swap_button_state)
screens_swapped = !screens_swapped;
if (screens_swapped)
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_button_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<int>(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<retro_memory_descriptor> 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<u8*>(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<unsigned>(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<int>(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;
// 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;
}
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<std::vector<u8>> 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<u8> buffer(static_cast<const u8*>(data), static_cast<const u8*>(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) {}

Some files were not shown because too many files have changed in this diff Show more