Compare commits

...

39 commits

Author SHA1 Message Date
OpenSauce
7a436ca7e8 renderer_vulkan: Fix incorrect MaxTexelBufferElements return type (#1563) 2026-01-03 00:03:22 +00:00
Chance
a63d6a3834 Fix simple typo that prevented debug builds (MSVC) 2025-12-31 20:56:00 +00:00
OpenSauce
d246f5b436 android: Fix rename + move behaviour for Google Play build (#1545)
* android: Fix rename + move behaviour for Google Play build

* android_storage: Handle rename/move with same source and destination
2025-12-29 17:58:31 +00:00
OpenSauce04
9a926e44c1 ci: Only build Android googleplay target for tags
I couldn't find any method of entirely skipping a single target in a matrix, so skipping each job is the next best thing.
2025-12-29 17:19:54 +00:00
OpenSauce04
f906242a23 ci: Added build job for Android Google Play flavor 2025-12-29 17:19:54 +00:00
OpenSauce04
33e7ed5c5c android: Implemented googlePlay build variants 2025-12-29 17:19:54 +00:00
OpenSauce04
f911af4197 MainActivity.kt: Added missing import 2025-12-24 19:02:06 +00:00
OpenSauce
37dd01fd51 android: Begin migration to raw fs access, starting with Rename reimplementation (#1511)
* Add setup step to grant `MANAGE_EXTERNAL_STORAGE`

* WIP re-implementation of Android `Rename` using native filesystem manipulation

* Applied clang-format and updated license headers

* Support user directories on removable storage devices

* If `MANAGE_EXTERNAL_STORAGE` is lost, re-request it

* Fix missing permission dialog appearing during initial setup

* Added empty code branches to prep for old Android support

* Fixed permission setup completion not accounting for external storage permission

* Implement code for Android <11

* Fixed emulation error if a renamed file is then opened for R/W

* Detect if the current user directory cannot be located, and prompt re-selection

* If an invalid user directory is selected, reject and re-prompt
2025-12-24 12:21:15 +00:00
OpenSauce04
ba5215242f macos: Fixed real camera not activating during emulation
This was due to a plugin which is required for handling camera permissions being missing
2025-12-23 23:28:20 +00:00
OpenSauce04
8e4d4efce4 Updated Linux section of readme with Wayland information 2025-12-22 12:40:50 +00:00
OpenSauce04
14dccb3c47 ci: Split AppImage build into two, with and without Wayland support 2025-12-22 12:40:50 +00:00
OpenSauce04
5431705e79 android: Upgrade AGP to 8.13.1 2025-12-09 23:05:49 +00:00
OpenSauce04
633ae42fbb video_core: Fixed occasional launch crash on certain platforms due to unsafe SDL_Init 2025-12-09 23:02:41 +00:00
OpenSauce04
0ed6b7558e android: Added relWithDebInfoLite build profile 2025-12-09 23:02:27 +00:00
OpenSauce04
bf1a95f438 cmake: Fixed AppImage build failure caused by upstream changes to linuxdeploy
Also improved error message when linuxdeploy run fails
2025-12-09 23:02:19 +00:00
David Griswold
01dc2bb776 android: Add Display Listener methods for smoother secondary display updates 2025-10-03 18:05:22 +01:00
OpenSauce04
4ac3cab012 externals: Updated fmt to 12.0.0
This fixes a build failure with Clang 21
2025-10-03 16:28:27 +01:00
huesos_96
897447e9bd Android: Dual screen fixes for Handhelds that have 2 screens like Ayaneo Pocket DS (#1341)
* Prevent SecondaryDisplay from stealing focus

The SecondaryDisplay Activity was stealing focus from the main
Activity when it was launched.

Set the `FLAG_NOT_FOCUSABLE` and `FLAG_NOT_TOUCH_MODAL` window flags
to prevent the SecondaryDisplay from gaining focus.

* Implement touch controls for secondary display

This commit introduces touch input handling for the secondary display.

The following changes were made:
- Added `onSecondaryTouchEvent` and `onSecondaryTouchMoved` to `NativeLibrary.kt` and `native.cpp` to process touch events on the secondary display.
- Implemented `onTouchListener` in `SecondaryDisplay.kt` to capture touch events and forward them to the native layer.
- Handles `ACTION_DOWN`, `ACTION_POINTER_DOWN`, `ACTION_MOVE`, `ACTION_UP`, `ACTION_POINTER_UP`, and `ACTION_CANCEL` motion events.
- Tracks the active pointer to ensure correct touch event handling.

* Refactor display logic for multi-display support

This commit introduces a `DisplayHelper` class to centralize display-related logic, particularly for handling scenarios where the application might be launched on an external display.

Key changes:
- Added `DisplayHelper.kt` to manage internal and external display identification based on launch conditions.
- `MainActivity` and `EmulationActivity` now use `DisplayHelper.checkLaunchDisplay()` to determine the initial display.
- `SecondaryDisplay` now uses `DisplayHelper.getExternalDisplay()` to correctly identify the target display for the secondary presentation.
- `InputOverlay` now queries `DisplayHelper.isBottomOnPrimary()` to determine if touch input should be processed for the primary display based on the current screen layout.
- `SecondaryDisplay` now queries `DisplayHelper.isBottomOnSecondary()` to conditionally pass touch events to the native layer based on which screen (primary or secondary) is currently displaying the 3DS bottom screen.

These changes ensure that the application behaves correctly when launched on either the internal or an external display, and that touch input is routed appropriately based on the user's chosen screen layout for the dual screens.

* Removed primary-screen checks so the input overlay always forwards touch events, ensuring all touches reach the native handler even when multiple displays are active

* Remove DisplayHelper class and adjust external display logic

* Formatting adjustments

---------

Co-authored-by: DavidRGriswold <novachild@gmail.com>
Co-authored-by: OpenSauce04 <opensauce04@gmail.com>
2025-10-03 14:46:05 +01:00
OpenSauce04
724576cc61 ci: Update all macOS runners to macOS 15 Sequoia 2025-10-03 14:18:27 +01:00
OpenSauce04
3e1b86548a cmake: Remove SYSTEM from target_link_libraries 2025-09-16 16:06:12 +01:00
OpenSauce04
246e06d1a4 vk_pipeline_cache: Fix directory creation failure if shaders/vulkan/ is missing 2025-09-13 01:20:32 +01:00
OpenSauce04
a607e3dd22 tools: Added Github cache purge script 2025-09-13 01:20:32 +01:00
OpenSauce04
a65114eabf Updated compatibility list 2025-09-05 22:22:32 +01:00
OpenSauce04
6ac0733002 tools: Updated guidance regarding translation updates 2025-09-05 21:56:32 +01:00
David Griswold
8519e92eae android: Re-fixed game termination bug (#1357)
* EmulationActivity and EmulationFragment clear only their own hooks

* EmulationLifecycleUtil: Rename `remove()` to `removeHook()`

* EmulationLifecycleUtil: Removed unused function `clear()`

* Corrected somewhat incorrect usage of the word "hook"

* Define `onShutdown` and `onPause` hook functions in constructors

* Formatting nitpicks

* Updated license header

* Re-added log messages for attempting to add duplicate hooks

---------

Co-authored-by: OpenSauce04 <opensauce04@gmail.com>
2025-09-05 21:40:05 +01:00
OpenSauce04
7f2ac35870 Revert "Fix android termination bug (#1354)"
This reverts commit 70f9379eef.
2025-09-05 21:40:05 +01:00
OpenSauce04
1e2dd5ea78 SecondaryDisplay.kt: Remove redundant SurfaceTexture, preventing log spam 2025-09-05 21:40:05 +01:00
David Griswold
beba099fed Fix android termination bug (#1354)
* move hook additions to onCreateView

* Updated license header

* Formatting nitpick

* Added prefix to log messages

---------

Co-authored-by: OpenSauce04 <opensauce04@gmail.com>
2025-09-04 22:59:51 +01:00
OpenSauce04
c888c40b3e macos: Set UIDesignRequiresCompatibility to true 2025-09-03 23:02:34 +01:00
OpenSauce04
57995cd89c android: Bump Vulkan Validation Layers to SDK 1.4.313.0 2025-09-03 22:31:26 +01:00
DavidRGriswold
29a77b342b android: Prevent crash when editing a slider option with an out of bounds value
Co-authored-by: OpenSauce04 <opensauce04@gmail.com>
2025-09-03 13:18:43 +01:00
OpenSauce04
3ef5bc0bfe macos: Patch QMetalLayer.setNeedsDisplayInRect at runtime to avoid freezing on recent Qt 2025-09-03 03:16:00 +01:00
OpenSauce04
d94657a44d cmake: On Windows, download MSVC 2022 Qt versions instead of MSVC 2019 2025-09-03 03:16:00 +01:00
OpenSauce04
ee58988897 cmake: Bump downloaded Qt version to 6.9.2
Also bumps aqtinstall to 3.3.0
2025-09-03 03:16:00 +01:00
OpenSauce04
ec7f00c9a4 cmake: Added check for minimum AppleClang version 2025-09-02 14:06:53 +01:00
OpenSauce04
164b9329c7 cmake: Corrected widespread incorrect usage of the SYSTEM property 2025-09-01 00:43:01 +01:00
OpenSauce04
2292f3ab1b Updated translations via Transifex 2025-08-20 13:57:05 +01:00
PabloMK7
3ab6a304cd am: fix save data being deleted on CIA install failure (#1319) 2025-08-20 13:51:06 +01:00
OpenSauce04
2e3d926dd5 Updated language translations via Transifex 2025-08-15 17:47:35 +01:00
60 changed files with 1143 additions and 240 deletions

View file

@ -9,8 +9,14 @@ fi
cd src/android
chmod +x ./gradlew
./gradlew assembleRelease
./gradlew bundleRelease
if [[ "$TARGET" == "googleplay" ]]; then
./gradlew assembleGooglePlayRelease
./gradlew bundleGooglePlayRelease
else
./gradlew assembleVanillaRelease
./gradlew bundleVanillaRelease
fi
ccache -s -v

View file

@ -1,14 +1,16 @@
#!/bin/bash -ex
if [ "$TARGET" = "appimage" ]; then
if [[ "$TARGET" == "appimage"* ]]; then
# Compile the AppImage we distribute with Clang.
export EXTRA_CMAKE_FLAGS=(-DCMAKE_CXX_COMPILER=clang++
-DCMAKE_C_COMPILER=clang
-DCMAKE_LINKER=/etc/bin/ld.lld
-DENABLE_ROOM_STANDALONE=OFF)
# Bundle required QT wayland libraries
export EXTRA_QT_PLUGINS="waylandcompositor"
export EXTRA_PLATFORM_PLUGINS="libqwayland-egl.so;libqwayland-generic.so"
if [ "$TARGET" = "appimage-wayland" ]; then
# Bundle required QT wayland libraries
export EXTRA_QT_PLUGINS="waylandcompositor"
export EXTRA_PLATFORM_PLUGINS="libqwayland-egl.so;libqwayland-generic.so"
fi
else
# For the linux-fresh verification target, verify compilation without PCH as well.
export EXTRA_CMAKE_FLAGS=(-DCITRA_USE_PRECOMPILED_HEADERS=OFF)
@ -30,7 +32,7 @@ cmake .. -G Ninja \
ninja
strip -s bin/Release/*
if [ "$TARGET" = "appimage" ]; then
if [[ "$TARGET" == "appimage"* ]]; then
ninja bundle
# TODO: Our AppImage environment currently uses an older ccache version without the verbose flag.
ccache -s

View file

@ -27,7 +27,7 @@ jobs:
strategy:
fail-fast: false
matrix:
target: ["appimage", "fresh"]
target: ["appimage", "appimage-wayland", "fresh"]
container:
image: opensauce04/azahar-build-environment:latest
options: -u 1001
@ -51,18 +51,22 @@ jobs:
- name: Build
run: ./.ci/linux.sh
- name: Move AppImage to artifacts directory
if: ${{ matrix.target == 'appimage' }}
if: ${{ contains(matrix.target, 'appimage') }}
run: |
mkdir -p artifacts
mv build/bundle/*.AppImage artifacts/
- name: Rename AppImage
if: ${{ matrix.target == 'appimage-wayland' }}
run: |
mv artifacts/azahar.AppImage artifacts/azahar-wayland.AppImage
- name: Upload
if: ${{ matrix.target == 'appimage' }}
if: ${{ contains(matrix.target, 'appimage') }}
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: artifacts/
macos:
runs-on: ${{ (matrix.target == 'x86_64' && 'macos-13') || 'macos-14' }}
runs-on: ${{ (matrix.target == 'x86_64' && 'macos-15-intel') || 'macos-15' }}
strategy:
fail-fast: false
matrix:
@ -103,7 +107,7 @@ jobs:
name: ${{ env.OS }}-${{ env.TARGET }}
path: artifacts/
macos-universal:
runs-on: macos-14
runs-on: macos-15
needs: macos
env:
OS: macos
@ -212,51 +216,64 @@ jobs:
path: artifacts/
android:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target: ["vanilla", "googleplay"]
env:
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_COMPILERCHECK: content
CCACHE_SLOPPINESS: time_macros
OS: android
TARGET: universal
TARGET: ${{ matrix.target }}
SHOULD_RUN: ${{ (matrix.target == 'vanilla' || github.ref_type == 'tag') }}
steps:
- uses: actions/checkout@v4
if: ${{ env.SHOULD_RUN == 'true' }}
with:
submodules: recursive
- name: Set up cache
if: ${{ env.SHOULD_RUN == 'true' }}
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
${{ env.CCACHE_DIR }}
key: ${{ runner.os }}-android-${{ github.sha }}
key: ${{ runner.os }}-${{ env.OS }}-${{ matrix.target }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-android-
${{ runner.os }}-${{ env.OS }}-${{ matrix.target }}-
- name: Set tag name
if: ${{ env.SHOULD_RUN == 'true' }}
run: |
if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then
echo "GIT_TAG_NAME=$GITHUB_REF_NAME" >> $GITHUB_ENV
fi
echo $GIT_TAG_NAME
- name: Deps
- name: Install tools
if: ${{ env.SHOULD_RUN == 'true' }}
run: |
sudo apt-get update -y
sudo apt-get install ccache apksigner -y
- name: Update Android SDK CMake version
if: ${{ env.SHOULD_RUN == 'true' }}
run: |
echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "cmake;3.30.3"
- name: Build
if: ${{ env.SHOULD_RUN == 'true' }}
run: JAVA_HOME=$JAVA_HOME_17_X64 ./.ci/android.sh
env:
ANDROID_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEYSTORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASS }}
- name: Pack
if: ${{ env.SHOULD_RUN == 'true' }}
run: ../../../.ci/pack.sh
working-directory: src/android/app
env:
UNPACKED: 1
- name: Upload
if: ${{ env.SHOULD_RUN == 'true' }}
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}

View file

@ -48,6 +48,15 @@ if (APPLE)
else()
# Minimum macOS 13
set(CMAKE_OSX_DEPLOYMENT_TARGET "13.4")
# Catch compiler issue on AppleClang versions below 15.0
# TODO: Remove this check when we drop macOS 13 Ventura
if (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang" AND
CMAKE_CXX_COMPILER_VERSION VERSION_LESS 15.0)
message(FATAL_ERROR "AppleClang 15.0 or later is required due to a compiler bug in earlier versions.\n"
"Current version: ${CMAKE_CXX_COMPILER_VERSION}\n"
"After updating, delete 'CMakeCache.txt' in the build directory.")
endif()
endif()
endif()
@ -299,7 +308,7 @@ find_package(Threads REQUIRED)
if (ENABLE_QT)
if (NOT USE_SYSTEM_QT)
download_qt(6.7.2)
download_qt(6.9.2)
endif()
find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia Concurrent)

View file

@ -130,25 +130,10 @@ if (BUNDLE_TARGET_EXECUTE)
--icon-file "${CMAKE_BINARY_DIR}/dist/org.azahar_emu.Azahar.svg"
--desktop-file "${source_path}/dist/${executable_name}.desktop"
--appdir "${appdir_path}"
RESULT_VARIABLE linuxdeploy_appdir_result)
RESULT_VARIABLE linuxdeploy_appdir_result
ERROR_VARIABLE linuxdeploy_appdir_error)
if (NOT linuxdeploy_appdir_result EQUAL "0")
message(FATAL_ERROR "linuxdeploy failed to create AppDir: ${linuxdeploy_appdir_result}")
endif()
if (enable_qt)
set(qt_hook_file "${appdir_path}/apprun-hooks/linuxdeploy-plugin-qt-hook.sh")
file(READ "${qt_hook_file}" qt_hook_contents)
# Add Cinnamon to list of DEs for GTK3 theming.
string(REPLACE
"*XFCE*"
"*X-Cinnamon*|*XFCE*"
qt_hook_contents "${qt_hook_contents}")
# Wayland backend crashes due to changed schemas in Gnome 40.
string(REPLACE
"export QT_QPA_PLATFORMTHEME=gtk3"
"export QT_QPA_PLATFORMTHEME=gtk3; export GDK_BACKEND=x11"
qt_hook_contents "${qt_hook_contents}")
file(WRITE "${qt_hook_file}" "${qt_hook_contents}")
message(FATAL_ERROR "linuxdeploy failed to create AppDir w/ exit code ${linuxdeploy_appdir_result}:\n${linuxdeploy_appdir_error}")
endif()
message(STATUS "Creating AppImage for executable ${executable_path}")

View file

@ -20,9 +20,9 @@ function(determine_qt_parameters target host_out type_out arch_out arch_path_out
set(arch_path "mingw_64")
elseif (MSVC)
if ("arm64" IN_LIST ARCHITECTURE)
set(arch_path "msvc2019_arm64")
set(arch_path "msvc2022_arm64")
elseif ("x86_64" IN_LIST ARCHITECTURE)
set(arch_path "msvc2019_64")
set(arch_path "msvc2022_64")
else()
message(FATAL_ERROR "Unsupported bundled Qt architecture. Enable USE_SYSTEM_QT and provide your own.")
endif()
@ -30,12 +30,13 @@ function(determine_qt_parameters target host_out type_out arch_out arch_path_out
# In case we're cross-compiling, prepare to also fetch the correct host Qt tools.
if (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "AMD64")
set(host_arch_path "msvc2019_64")
set(host_arch_path "msvc2022_64")
elseif (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "ARM64")
# TODO: msvc2019_arm64 doesn't include some of the required tools for some reason,
# TODO: so until it does, just use msvc2019_64 under x86_64 emulation.
# TODO: ^ Is this still true with msvc2022?
# set(host_arch_path "msvc2019_arm64")
set(host_arch_path "msvc2019_64")
set(host_arch_path "msvc2022_64")
endif()
set(host_arch "win64_${host_arch_path}")
else()
@ -105,7 +106,7 @@ function(download_qt_configuration prefix_out target host type arch arch_path ba
if (NOT EXISTS "${prefix}")
message(STATUS "Downloading Qt binaries for ${target}:${host}:${type}:${arch}:${arch_path}")
set(AQT_PREBUILD_BASE_URL "https://github.com/miurahr/aqtinstall/releases/download/v3.1.18")
set(AQT_PREBUILD_BASE_URL "https://github.com/miurahr/aqtinstall/releases/download/v3.3.0")
if (WIN32)
set(aqt_path "${base_path}/aqt.exe")
if (NOT EXISTS "${aqt_path}")

View file

@ -19,7 +19,6 @@ Download the latest release from [Releases](https://github.com/azahar-emu/azahar
If you are unsure of whether you want to use MSYS2 or MSVC, use MSYS2.
---
### MacOS
To download a build that will work on all Macs, you can download the `macos-universal` build on the [Releases](https://github.com/azahar-emu/azahar/releases) page.
@ -55,6 +54,11 @@ The recommended format for using Azahar on Linux is the Flatpak available on Fla
Azahar is also available as an AppImage on the [Releases](https://github.com/azahar-emu/azahar/releases) page.
If you are unsure of which variant to use, we recommend using the default `azahar.AppImage`. This is because of upstream issues in the Wayland ecosystem which may cause problems when running the emulator (e.g. [#1162](https://github.com/azahar-emu/azahar/issues/1162)).
Unless you explicitly require native Wayland support (e.g. you are running a system with no Xwayland), the non-Wayland variant is recommended.
If you are using the Flatpak and run into issues with Wayland, you can disable Wayland support for the Azahar Flatpak using [Flatseal](https://flathub.org/en/apps/com.github.tchx84.Flatseal).
# Build instructions

View file

@ -75,6 +75,9 @@
<true/>
<key>NSHighResolutionCapable</key>
<string>True</string>
<key>UIDesignRequiresCompatibility</key>
<true/> <!-- Remove when Qt Liquid Glass issues are fixed upstream:
https://bugreports.qt.io/browse/QTBUG-138942 -->
<key>UIFileSharingEnabled</key>
<true/>
<key>UILaunchStoryboardName</key>

@ -1 +1 @@
Subproject commit 4f39041699412873d0afcec89a9313148a192647
Subproject commit a36decbe43d0e5a570ac3d3ba9a0b226dc832a17

View file

@ -27,7 +27,7 @@
<message>
<location filename="../../src/citra_qt/aboutdialog.ui" line="30"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/default/256x256/azahar.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"/>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img src=&quot;:/icons/default/256x256/azahar.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<location filename="../../src/citra_qt/aboutdialog.ui" line="60"/>
@ -725,7 +725,7 @@ Desitja ignorar l&apos;error i continuar?</translation>
<message>
<location filename="../../src/citra_qt/configuration/configure_debug.ui" line="107"/>
<source>Show log output in console</source>
<translation type="unfinished"/>
<translation>Mostrar l&apos;eixida del registre en la consola</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_debug.ui" line="114"/>
@ -2275,7 +2275,7 @@ Desitja ignorar l&apos;error i continuar?</translation>
<message>
<location filename="../../src/citra_qt/configuration/configure_motion_touch.cpp" line="102"/>
<source>&lt;a href=&apos;https://web.archive.org/web/20240301211230/https://citra-emu.org/wiki/using-a-controller-or-android-phone-for-motion-or-touch-input&apos;&gt;&lt;span style=&quot;text-decoration: underline; color:#039be5;&quot;&gt;Learn More&lt;/span&gt;&lt;/a&gt;</source>
<translation type="unfinished"/>
<translation>&lt;a href=&apos;https://web.archive.org/web/20240301211230/https://citra-emu.org/wiki/using-a-controller-or-android-phone-for-motion-or-touch-input&apos;&gt;&lt;span style=&quot;text-decoration: underline; color:#039be5;&quot;&gt;Més Informació&lt;/span&gt;&lt;/a&gt;</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_motion_touch.cpp" line="209"/>
@ -2494,12 +2494,12 @@ Desitja ignorar l&apos;error i continuar?</translation>
<message>
<location filename="../../src/citra_qt/configuration/configure_storage.ui" line="187"/>
<source>Compress installed CIA content</source>
<translation type="unfinished"/>
<translation>Comprimir el contingut de CIAs instal·lats</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_storage.ui" line="190"/>
<source>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</source>
<translation type="unfinished"/>
<translation>Comprimix el contingut de fitxers CIA quan són instal·lats a la SD emulada. Només afecta contingut CIA instal·lat amb esta opció activada.</translation>
</message>
<message>
<location filename="../../src/citra_qt/configuration/configure_storage.cpp" line="26"/>
@ -4431,7 +4431,7 @@ Vols reinstal·lar els arxius de totes maneres?</translation>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="2250"/>
<source>3DS Installation File (*.cia *.zcia)</source>
<translation type="unfinished"/>
<translation>Fitxers d&apos;Instalació de 3DS (*.cia *.zcia)</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="2250"/>
@ -4510,24 +4510,24 @@ Vols reinstal·lar els arxius de totes maneres?</translation>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3107"/>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3113"/>
<source>Error compressing file</source>
<translation type="unfinished"/>
<translation>Error al comprimir el fitxer</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="2339"/>
<source>File compress operation failed, check log for details.</source>
<translation type="unfinished"/>
<translation>Operació de compressió fallida, mira el registre per a més detalls.</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="2341"/>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3181"/>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3188"/>
<source>Error decompressing file</source>
<translation type="unfinished"/>
<translation>Error de descompressió del fitxer</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="2342"/>
<source>File decompress operation failed, check log for details.</source>
<translation type="unfinished"/>
<translation>Operació de descompressió fallida, mira el registre per a més detalls.</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="2364"/>
@ -4663,62 +4663,62 @@ Per a veure una guia sobre com instal·lar FFmpeg, polsa Ajuda.</translation>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3063"/>
<source>Load 3DS ROM File</source>
<translation type="unfinished"/>
<translation>Carregar ROM de 3DS</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3064"/>
<source>3DS ROM Files (*.cia *cci *3dsx *cxi)</source>
<translation type="unfinished"/>
<translation>Fitxers ROM 3DS (*.cia *cci *3dsx *cxi)</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3108"/>
<source>The selected file is not a compatible 3DS ROM format. Make sure you have chosen the right file, and that it is not encrypted.</source>
<translation type="unfinished"/>
<translation>El fitxer seleccionat no és un ROM de 3DS compatible. Assegura&apos;t que has triat el fitxer correcte i que no estiga xifrat.</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3114"/>
<source>The selected file is already compressed.</source>
<translation type="unfinished"/>
<translation>El fitxer seleccionat ja està comprimit.</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3119"/>
<source>3DS Compressed ROM File (*.%1)</source>
<translation type="unfinished"/>
<translation>Fitxer ROM 3DS comprimit (*.%1)</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3127"/>
<source>Save 3DS Compressed ROM File</source>
<translation type="unfinished"/>
<translation>Desar fitxer 3DS comprimit</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3152"/>
<source>Load 3DS Compressed ROM File</source>
<translation type="unfinished"/>
<translation>Carregar fitxer 3DS comprimit</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3153"/>
<source>3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi)</source>
<translation type="unfinished"/>
<translation>Fitxer ROM 3DS comprimit (*.zcia *zcci *z3dsx *zcxi)</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3182"/>
<source>The selected file is not a compatible compressed 3DS ROM format. Make sure you have chosen the right file.</source>
<translation type="unfinished"/>
<translation>El fitxer seleccionat no és un format de ROM 3DS comprimit compatible. Assegura&apos;t d&apos;haver triat l&apos;arxiu correcte.</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3189"/>
<source>The selected file is already decompressed.</source>
<translation type="unfinished"/>
<translation>El fitxer seleccionat ja està descomprimit.</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3194"/>
<source>3DS ROM File (*.%1)</source>
<translation type="unfinished"/>
<translation>Fitxer ROM 3DS (*.%1)</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3202"/>
<source>Save 3DS ROM File</source>
<translation type="unfinished"/>
<translation>Desar fitxer ROM 3DS</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3227"/>
@ -4818,7 +4818,7 @@ Per a veure una guia sobre com instal·lar FFmpeg, polsa Ajuda.</translation>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3431"/>
<source>Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms)</source>
<translation type="unfinished"/>
<translation>Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms)</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3440"/>
@ -4839,7 +4839,7 @@ Per a veure una guia sobre com instal·lar FFmpeg, polsa Ajuda.</translation>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3600"/>
<source>%1 is missing. Please &lt;a href=&apos;https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/&apos;&gt;dump your system archives&lt;/a&gt;.&lt;br/&gt;Continuing emulation may result in crashes and bugs.</source>
<translation type="unfinished"/>
<translation>Falta %1 . Per favor,&lt;a href=&apos;https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/&apos;&gt;bolca els teus arxius de sistema&lt;/a&gt;.&lt;br/&gt;Continuar l&apos;emulació pot resultar en penges i errors.</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3608"/>
@ -4869,7 +4869,7 @@ Per a veure una guia sobre com instal·lar FFmpeg, polsa Ajuda.</translation>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3627"/>
<source>A fatal error occurred. &lt;a href=&apos;https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296&apos;&gt;Check the log&lt;/a&gt; for details.&lt;br/&gt;Continuing emulation may result in crashes and bugs.</source>
<translation type="unfinished"/>
<translation>Error fatal.&lt;a href=&apos;https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296&apos;&gt;Mira el log&lt;/a&gt;per a més detalls.&lt;br/&gt;Continuar l&apos;emulació pot resultar en penges i errors.</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3632"/>
@ -6387,12 +6387,12 @@ Missatge de depuració:</translation>
<message>
<location filename="../../src/citra_qt/main.ui" line="466"/>
<source>Compress ROM File...</source>
<translation type="unfinished"/>
<translation>Comprimir fitxer ROM...</translation>
</message>
<message>
<location filename="../../src/citra_qt/main.ui" line="471"/>
<source>Decompress ROM File...</source>
<translation type="unfinished"/>
<translation>Descomprimir fitxer ROM...</translation>
</message>
<message>
<location filename="../../src/citra_qt/main.ui" line="479"/>

View file

@ -4517,7 +4517,7 @@ Reinstall the files anyway?</source>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="2339"/>
<source>File compress operation failed, check log for details.</source>
<translation>Operación de comprensión fallida, mira el registro para más detalles.</translation>
<translation>Operación de compresión fallida, mira el registro para más detalles.</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="2341"/>
@ -4529,7 +4529,7 @@ Reinstall the files anyway?</source>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="2342"/>
<source>File decompress operation failed, check log for details.</source>
<translation>Operación de descomprensión fallida, mira el registro para más detalles.</translation>
<translation>Operación de descompresión fallida, mira el registro para más detalles.</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="2364"/>

View file

@ -3894,7 +3894,7 @@ Drag points to change position, or double-click table cells to edit values.</sou
<message>
<location filename="../../src/citra_qt/configuration/configure_web.ui" line="26"/>
<source>Show current application in your Discord status</source>
<translation type="unfinished"></translation>
<translation>Показывать текущее приложение в статусе Discord</translation>
</message>
</context>
<context>
@ -4027,7 +4027,7 @@ Drag points to change position, or double-click table cells to edit values.</sou
<location filename="../../src/citra_qt/dumping/dumping_dialog.cpp" line="25"/>
<location filename="../../src/citra_qt/dumping/dumping_dialog.cpp" line="85"/>
<source>Azahar</source>
<translation type="unfinished">Azahar</translation>
<translation>Azahar</translation>
</message>
<message>
<location filename="../../src/citra_qt/dumping/dumping_dialog.cpp" line="25"/>
@ -4170,7 +4170,7 @@ Please check your FFmpeg installation used for compilation.</source>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="1299"/>
<source>GBA Virtual Console is not supported by Azahar.</source>
<translation type="unfinished"></translation>
<translation>Azahar не поддерживает GBA Virtual Console.</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="1304"/>
@ -4315,7 +4315,7 @@ Please check your FFmpeg installation used for compilation.</source>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3682"/>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3775"/>
<source>Azahar</source>
<translation type="unfinished">Azahar</translation>
<translation>Azahar</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="2066"/>
@ -4523,7 +4523,7 @@ Reinstall the files anyway?</source>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3181"/>
<location filename="../../src/citra_qt/citra_qt.cpp" line="3188"/>
<source>Error decompressing file</source>
<translation type="unfinished"></translation>
<translation>Ошибра при разжатии файла</translation>
</message>
<message>
<location filename="../../src/citra_qt/citra_qt.cpp" line="2342"/>
@ -5036,7 +5036,7 @@ Would you like to download it?</source>
<message>
<location filename="../../src/citra_qt/game_list.cpp" line="366"/>
<source>Don&apos;t show again</source>
<translation type="unfinished"></translation>
<translation>Не показывать снова</translation>
</message>
<message>
<location filename="../../src/citra_qt/game_list.cpp" line="564"/>
@ -7383,7 +7383,8 @@ They may have left the room.</source>
<source>Azahar has detected user data for Citra.
</source>
<translation type="unfinished"></translation>
<translation>Azahar обнаружил файлы пользователя Citra.
</translation>
</message>
<message>
<location filename="../../src/citra_qt/user_data_migration.cpp" line="77"/>

View file

@ -1,24 +1,37 @@
# Definitions for all external bundled libraries
# Suppress warnings from external libraries
if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
if (MSVC)
add_compile_options(/W0)
else()
add_compile_options(-w)
endif()
function(target_disable_warnings target)
if (MSVC)
target_compile_options(${target} INTERFACE /W0)
else()
target_compile_options(${target} INTERFACE -w)
endif()
endfunction()
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/CMakeModules)
include(DownloadExternals)
include(ExternalProject)
# Boost
if (NOT USE_SYSTEM_BOOST)
if (USE_SYSTEM_BOOST)
unset(BOOST_ROOT CACHE)
unset(Boost_INCLUDE_DIR CACHE)
set(Boost_NO_SYSTEM_PATHS OFF CACHE BOOL "" FORCE)
else()
message(STATUS "Including vendored Boost library")
set(BOOST_ROOT "${CMAKE_SOURCE_DIR}/externals/boost" CACHE STRING "")
set(Boost_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/externals/boost" CACHE STRING "")
set(Boost_NO_SYSTEM_PATHS ON CACHE BOOL "")
add_library(boost INTERFACE)
target_include_directories(boost SYSTEM INTERFACE ${Boost_INCLUDE_DIR})
target_include_directories(boost INTERFACE ${Boost_INCLUDE_DIR})
target_disable_warnings(boost)
# Boost::serialization
file(GLOB boost_serialization_SRC "${CMAKE_SOURCE_DIR}/externals/boost/libs/serialization/src/*.cpp")
@ -33,11 +46,7 @@ if (NOT USE_SYSTEM_BOOST)
${CMAKE_SOURCE_DIR}/externals/boost/libs/iostreams/src/mapped_file.cpp
)
target_link_libraries(boost_iostreams PUBLIC boost)
# Add additional boost libs here; remember to ALIAS them in the root CMakeLists!
else()
unset(BOOST_ROOT CACHE)
unset(Boost_INCLUDE_DIR CACHE)
set(Boost_NO_SYSTEM_PATHS OFF CACHE BOOL "" FORCE)
# Add additional boost libs here; remember to ALIAS them in the root CMakeLists!
endif()
# Catch2
@ -73,6 +82,7 @@ endif()
# dds-ktx
add_library(dds-ktx INTERFACE)
target_include_directories(dds-ktx INTERFACE ./dds-ktx)
target_disable_warnings(dds-ktx)
# fmt and Xbyak need to be added before dynarmic
# libfmt
@ -137,7 +147,8 @@ endif()
# MicroProfile
add_library(microprofile INTERFACE)
target_include_directories(microprofile SYSTEM INTERFACE ./microprofile)
target_include_directories(microprofile INTERFACE ./microprofile)
target_disable_warnings(microprofile)
if (ENABLE_MICROPROFILE)
target_compile_definitions(microprofile INTERFACE MICROPROFILE_ENABLED=1)
else()
@ -146,10 +157,11 @@ endif()
# Nihstro
add_library(nihstro-headers INTERFACE)
target_include_directories(nihstro-headers SYSTEM INTERFACE ./nihstro/include)
if (MSVC)
# TODO: For some reason MSVC still applies this warning even with /W0 for externals.
target_compile_options(nihstro-headers INTERFACE /wd4715)
target_include_directories(nihstro-headers INTERFACE ./nihstro/include)
target_disable_warnings(nihstro-headers)
if (NOT MSVC)
# TODO: For some reason MSYS2 still applied this warnin even with -w
target_compile_options(nihstro-headers INTERFACE -Wno-invalid-specialization)
endif()
# Open Source Archives
@ -173,7 +185,8 @@ if (USE_SYSTEM_FFMPEG_HEADERS)
endif()
if (NOT FOUND_FFMPEG_HEADERS)
message(STATUS "Using bundled ffmpeg headers.")
target_include_directories(library-headers SYSTEM INTERFACE ./library-headers/ffmpeg/include)
target_include_directories(library-headers INTERFACE ./library-headers/ffmpeg/include)
target_disable_warnings(library-headers)
endif()
# SoundTouch
@ -294,7 +307,8 @@ if (USE_SYSTEM_JSON)
# Citra uses "#include <json.hpp>" so we have to add this manually
target_include_directories(json-headers SYSTEM INTERFACE "${NLOHMANN_PREFIX}/nlohmann")
else()
target_include_directories(json-headers SYSTEM INTERFACE ./json)
target_include_directories(json-headers INTERFACE ./json)
target_disable_warnings(json-headers)
endif()
# OpenSSL
@ -310,7 +324,8 @@ if (NOT OPENSSL_FOUND)
set(LIBRESSL_SKIP_INSTALL ON CACHE BOOL "")
set(OPENSSLDIR "/etc/ssl/")
add_subdirectory(libressl EXCLUDE_FROM_ALL)
target_include_directories(ssl SYSTEM INTERFACE ./libressl/include)
target_include_directories(ssl INTERFACE ./libressl/include)
target_disable_warnings(ssl)
target_compile_definitions(ssl PRIVATE -DHAVE_INET_NTOP)
get_directory_property(OPENSSL_LIBRARIES
DIRECTORY libressl
@ -327,17 +342,20 @@ if(USE_SYSTEM_CPP_HTTPLIB)
get_target_property(HTTP_LIBS httplib::httplib INTERFACE_LINK_LIBRARIES)
if(HTTP_LIBS)
message(WARNING "Shared cpp-http (${HTTP_LIBS}) not supported. Falling back to bundled...")
target_include_directories(httplib SYSTEM INTERFACE ./httplib)
target_include_directories(httplib INTERFACE ./httplib)
target_disable_warnings(httplib)
else()
if(CppHttp_FOUND)
target_link_libraries(httplib INTERFACE httplib::httplib)
else()
message(STATUS "Cpp-httplib not found or not suitable version! Falling back to bundled...")
target_include_directories(httplib SYSTEM INTERFACE ./httplib)
endif()
target_include_directories(httplib INTERFACE ./httplib)
target_disable_warnings(httplib)
endif()
endif()
else()
target_include_directories(httplib SYSTEM INTERFACE ./httplib)
target_include_directories(httplib INTERFACE ./httplib)
target_disable_warnings(httplib)
endif()
target_compile_options(httplib INTERFACE -DCPPHTTPLIB_OPENSSL_SUPPORT)
target_link_libraries(httplib INTERFACE ${OPENSSL_LIBRARIES})
@ -354,7 +372,8 @@ if (ENABLE_WEB_SERVICE)
target_link_libraries(cpp-jwt INTERFACE cpp-jwt::cpp-jwt)
else()
add_library(cpp-jwt INTERFACE)
target_include_directories(cpp-jwt SYSTEM INTERFACE ./cpp-jwt/include)
target_include_directories(cpp-jwt INTERFACE ./cpp-jwt/include)
target_disable_warnings(cpp-jwt)
target_compile_definitions(cpp-jwt INTERFACE CPP_JWT_USE_VENDORED_NLOHMANN_JSON)
endif()
endif()
@ -453,7 +472,8 @@ if (ENABLE_VULKAN)
endif()
else()
add_library(vma INTERFACE)
target_include_directories(vma SYSTEM INTERFACE ./vma/include)
target_include_directories(vma INTERFACE ./vma/include)
target_disable_warnings(vma)
endif()
# vulkan-headers
@ -465,7 +485,8 @@ if (ENABLE_VULKAN)
target_link_libraries(vulkan-headers INTERFACE Vulkan::Headers)
endif()
else()
target_include_directories(vulkan-headers SYSTEM INTERFACE ./vulkan-headers/include)
target_include_directories(vulkan-headers INTERFACE ./vulkan-headers/include)
target_disable_warnings(vulkan-headers)
endif()
# adrenotools

2
externals/boost vendored

@ -1 +1 @@
Subproject commit 3c27c785ad0f8a742af02e620dc225673f3a12d8
Subproject commit 2c82bd787302398bcae990e3c9ab2b451284f4ca

2
externals/fmt vendored

@ -1 +1 @@
Subproject commit 123913715afeb8a437e6388b4473fcc4753e1c9a
Subproject commit e424e3f2e607da02742f73db84873b8084fc714c

View file

@ -125,7 +125,7 @@ android {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
signingConfig = signingConfigs.getByName("debug")
isShrinkResources = true
isShrinkResources = true // TODO: Does this actually do anything when isDebuggable is enabled? -OS
isDebuggable = true
isJniDebuggable = true
proguardFiles(
@ -135,6 +135,22 @@ android {
isDefault = true
}
// Same as above, but with isDebuggable disabled.
// Primarily exists to allow development on hardened_malloc systems (e.g. GrapheneOS) without constantly tripping over years-old and seemingly harmless memory bugs.
// We should fix those bugs eventually, but for now this exists as a workaround to allow other work to be done.
register("relWithDebInfoLite") {
initWith(getByName("relWithDebInfo"))
signingConfig = signingConfigs.getByName("debug")
isDebuggable = false
installation {
enableBaselineProfile = false // Disabled by default when isDebuggable is true
}
lint {
checkReleaseBuilds = false // Ditto
// The name of this property is misleading, this doesn't actually disable linting for the `release` build.
}
}
// Signed by debug key disallowing distribution on Play Store.
// Attaches 'debug' suffix to version and package name, allowing installation alongside the release build.
debug {
@ -148,6 +164,18 @@ android {
flavorDimensions.add("version")
productFlavors {
register("vanilla") {
isDefault = true
dimension = "version"
versionNameSuffix = "-vanilla"
}
register("googlePlay") {
dimension = "version"
versionNameSuffix = "-googleplay"
}
}
externalNativeBuild {
cmake {
version = "3.25.0+"
@ -186,7 +214,7 @@ dependencies {
// Download Vulkan Validation Layers from the KhronosGroup GitHub.
val downloadVulkanValidationLayers = tasks.register<Download>("downloadVulkanValidationLayers") {
src("https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases/download/vulkan-sdk-1.4.304.1/android-binaries-1.4.304.1.zip")
src("https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases/download/vulkan-sdk-1.4.313.0/android-binaries-1.4.313.0.zip")
dest(file("${layout.buildDirectory.get().asFile.path}/tmp/Vulkan-ValidationLayers.zip"))
onlyIfModified(true)
}

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<!-- These permissions aren't allowed by Google. We asked, and they declined. -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:node="remove" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="remove" />
</manifest>

View file

@ -29,6 +29,8 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:name="org.citra.citra_emu.CitraApplication"

View file

@ -7,10 +7,12 @@ package org.citra.citra_emu
import android.Manifest.permission
import android.app.Dialog
import android.content.DialogInterface
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.Surface
@ -18,11 +20,14 @@ import android.view.View
import android.widget.TextView
import androidx.annotation.Keep
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.fragment.app.DialogFragment
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.citra.citra_emu.activities.EmulationActivity
import org.citra.citra_emu.utils.FileUtil
import org.citra.citra_emu.utils.Log
import org.citra.citra_emu.utils.RemovableStorageHelper
import java.lang.ref.WeakReference
import java.util.Date
@ -96,6 +101,24 @@ object NativeLibrary {
*/
external fun onTouchMoved(xAxis: Float, yAxis: Float)
/**
* Handles touch events on the secondary display.
*
* @param xAxis The value of the x-axis.
* @param yAxis The value of the y-axis.
* @param pressed To identify if the touch held down or released.
* @return true if the pointer is within the touchscreen
*/
external fun onSecondaryTouchEvent(xAxis: Float, yAxis: Float, pressed: Boolean): Boolean
/**
* Handles touch movement on the secondary display.
*
* @param xAxis The value of the instantaneous x-axis.
* @param yAxis The value of the instantaneous y-axis.
*/
external fun onSecondaryTouchMoved(xAxis: Float, yAxis: Float)
external fun reloadSettings()
external fun getTitleId(filename: String): Long
@ -611,6 +634,36 @@ object NativeLibrary {
FileUtil.getFilesName(path)
}
@Keep
@JvmStatic
fun getUserDirectory(uriOverride: Uri? = null): String {
val preferences: SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
val dirSep = "/"
val udUri = uriOverride ?:
preferences.getString("CITRA_DIRECTORY", "")!!.toUri()
val udPathSegment = udUri.lastPathSegment!!
val udVirtualPath = udPathSegment.substringAfter(":")
if (udPathSegment.startsWith("primary:")) { // User directory is located in primary storage
val primaryStoragePath = Environment.getExternalStorageDirectory().absolutePath
return primaryStoragePath + dirSep + udVirtualPath + dirSep
} else { // User directory probably located on a removable storage device
val storageIdString = udPathSegment.substringBefore(":")
val udRemovablePath = RemovableStorageHelper.getRemovableStoragePath(storageIdString)
if (udRemovablePath == null) {
android.util.Log.e("NativeLibrary",
"Unknown mount location for storage device '$storageIdString' (URI: $udUri)"
)
return ""
}
return udRemovablePath + dirSep + udVirtualPath + dirSep
}
}
@Keep
@JvmStatic
fun getSize(path: String): Long =
@ -620,6 +673,10 @@ object NativeLibrary {
FileUtil.getFileSize(path)
}
@Keep
@JvmStatic
fun getBuildFlavor(): String = BuildConfig.FLAVOR
@Keep
@JvmStatic
fun fileExists(path: String): Boolean =
@ -671,6 +728,24 @@ object NativeLibrary {
FileUtil.renameFile(path, destinationFilename)
}
@Keep
@JvmStatic
fun updateDocumentLocation(sourcePath: String, destinationPath: String): Boolean =
CitraApplication.documentsTree.updateDocumentLocation(sourcePath, destinationPath)
@Keep
@JvmStatic
fun moveFile(filename: String, sourceDirPath: String, destinationDirPath: String): Boolean =
if (FileUtil.isNativePath(sourceDirPath)) {
try {
CitraApplication.documentsTree.moveFile(filename, sourceDirPath, destinationDirPath)
} catch (e: Exception) {
false
}
} else {
FileUtil.moveFile(filename, sourceDirPath, destinationDirPath)
}
@Keep
@JvmStatic
fun deleteDocument(path: String): Boolean =

View file

@ -60,7 +60,15 @@ class EmulationActivity : AppCompatActivity() {
private lateinit var binding: ActivityEmulationBinding
private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil
private lateinit var hotkeyUtility: HotkeyUtility
private lateinit var secondaryDisplay: SecondaryDisplay;
private lateinit var secondaryDisplay: SecondaryDisplay
private val onShutdown = Runnable {
if (intent.getBooleanExtra("launched_from_shortcut", false)) {
finishAffinity()
} else {
this.finish()
}
}
private val emulationFragment: EmulationFragment
get() {
@ -77,8 +85,8 @@ class EmulationActivity : AppCompatActivity() {
ThemeUtil.setTheme(this)
settingsViewModel.settings.loadSettings()
super.onCreate(savedInstanceState)
secondaryDisplay = SecondaryDisplay(this);
secondaryDisplay.updateDisplay();
secondaryDisplay = SecondaryDisplay(this)
secondaryDisplay.updateDisplay()
binding = ActivityEmulationBinding.inflate(layoutInflater)
screenAdjustmentUtil = ScreenAdjustmentUtil(this, windowManager, settingsViewModel.settings)
@ -101,13 +109,7 @@ class EmulationActivity : AppCompatActivity() {
windowManager.defaultDisplay.rotation
)
EmulationLifecycleUtil.addShutdownHook(hook = {
if (intent.getBooleanExtra("launched_from_shortcut", false)) {
finishAffinity()
} else {
this.finish()
}
})
EmulationLifecycleUtil.addShutdownHook(onShutdown)
isEmulationRunning = true
instance = this
@ -165,12 +167,12 @@ class EmulationActivity : AppCompatActivity() {
}
override fun onDestroy() {
EmulationLifecycleUtil.clear()
EmulationLifecycleUtil.removeHook(onShutdown)
NativeLibrary.playTimeManagerStop()
isEmulationRunning = false
instance = null
secondaryDisplay.releasePresentation()
secondaryDisplay.releaseVD();
secondaryDisplay.releaseVD()
super.onDestroy()
}

View file

@ -11,29 +11,30 @@ import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.os.Bundle
import android.view.Display
import android.view.MotionEvent
import android.view.Surface
import android.view.SurfaceHolder
import android.view.SurfaceView
import org.citra.citra_emu.NativeLibrary
import android.view.WindowManager
import org.citra.citra_emu.features.settings.model.IntSetting
import org.citra.citra_emu.display.SecondaryDisplayLayout
import org.citra.citra_emu.NativeLibrary
class SecondaryDisplay(val context: Context) {
class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener {
private var pres: SecondaryDisplayPresentation? = null
private val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
private val vd: VirtualDisplay
init {
val st = SurfaceTexture(0)
st.setDefaultBufferSize(1920, 1080)
val vdSurface = Surface(st)
vd = displayManager.createVirtualDisplay(
"HiddenDisplay",
1920,
1080,
320,
vdSurface,
null,
DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION
)
displayManager.registerDisplayListener(this, null)
}
fun updateSurface() {
@ -44,16 +45,23 @@ class SecondaryDisplay(val context: Context) {
NativeLibrary.secondarySurfaceDestroyed()
}
private fun getExternalDisplay(context: Context): Display? {
val dm = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val internalId = context.display.displayId ?: Display.DEFAULT_DISPLAY
val displays = dm.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION)
return displays.firstOrNull { it.displayId != internalId && it.name != "HiddenDisplay" }
}
fun updateDisplay() {
// decide if we are going to the external display or the internal one
var display = getCustomerDisplay()
var display = getExternalDisplay(context)
if (display == null ||
IntSetting.SECONDARY_DISPLAY_LAYOUT.int == SecondaryDisplayLayout.NONE.int) {
display = vd.display
}
// if our presentation is already on the right display, ignore
if (pres?.display == display) return;
if (pres?.display == display) return
// otherwise, make a new presentation
releasePresentation()
@ -61,29 +69,41 @@ class SecondaryDisplay(val context: Context) {
pres?.show()
}
private fun getCustomerDisplay(): Display? {
val displays = displayManager.displays
// code taken from MelonDS dual screen - should fix odin 2 detection bug
return displayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION)
.firstOrNull { it.displayId != Display.DEFAULT_DISPLAY && it.name != "Built-in Screen" && it.name != "HiddenDisplay"}
}
fun releasePresentation() {
pres?.dismiss()
pres = null
}
fun releaseVD() {
displayManager.unregisterDisplayListener(this)
vd.release()
}
override fun onDisplayAdded(displayId: Int) {
updateDisplay()
}
override fun onDisplayRemoved(displayId: Int) {
updateDisplay()
}
override fun onDisplayChanged(displayId: Int) {
updateDisplay()
}
}
class SecondaryDisplayPresentation(
context: Context, display: Display, val parent: SecondaryDisplay
) : Presentation(context, display) {
private lateinit var surfaceView: SurfaceView
private var touchscreenPointerId = -1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window?.setFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
)
// Initialize SurfaceView
surfaceView = SurfaceView(context)
@ -103,6 +123,42 @@ class SecondaryDisplayPresentation(
}
})
this.surfaceView.setOnTouchListener { _, event ->
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
when (event.actionMasked) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
if (touchscreenPointerId == -1) {
touchscreenPointerId = pointerId
NativeLibrary.onSecondaryTouchEvent(
event.getX(pointerIndex),
event.getY(pointerIndex),
true
)
}
}
MotionEvent.ACTION_MOVE -> {
val index = event.findPointerIndex(touchscreenPointerId)
if (index != -1) {
NativeLibrary.onSecondaryTouchMoved(
event.getX(index),
event.getY(index)
)
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_CANCEL -> {
if (pointerId == touchscreenPointerId) {
NativeLibrary.onSecondaryTouchEvent(0f, 0f, false)
touchscreenPointerId = -1
}
}
}
true
}
setContentView(surfaceView) // Set SurfaceView as content
}

View file

@ -1,4 +1,4 @@
// Copyright Citra Emulator Project / Lime3DS Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -10,7 +10,6 @@ import org.citra.citra_emu.features.settings.model.AbstractSetting
import org.citra.citra_emu.features.settings.model.FloatSetting
import org.citra.citra_emu.features.settings.model.ScaledFloatSetting
import org.citra.citra_emu.utils.Log
import kotlin.math.roundToInt
class SliderSetting(
setting: AbstractSetting?,
@ -27,7 +26,8 @@ class SliderSetting(
val selectedFloat: Float
get() {
val setting = setting ?: return defaultValue!!.toFloat()
return when (setting) {
val ret = when (setting) {
is AbstractIntSetting -> setting.int.toFloat()
is FloatSetting -> setting.float
is ScaledFloatSetting -> setting.float
@ -36,8 +36,8 @@ class SliderSetting(
-1f
}
}
return ret.coerceIn(min.toFloat(), max.toFloat())
}
/**
* Write a value to the backing int. If that int was previously null,
* initializes a new one and returns it, so it can be added to the Hashmap.

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.
@ -60,7 +60,7 @@ class CitraDirectoryDialogFragment : DialogFragment() {
}
.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int ->
if (!PermissionsHandler.hasWriteAccess(requireContext())) {
(requireActivity() as MainActivity)?.openCitraDirectory?.launch(null)
PermissionsHandler.compatibleSelectDirectory((requireActivity() as MainActivity).openCitraDirectory)
}
}
.show()

View file

@ -101,6 +101,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
private val emulationViewModel: EmulationViewModel by activityViewModels()
private val settingsViewModel: SettingsViewModel by viewModels()
private val onPause = Runnable{ togglePause() }
private val onShutdown = Runnable{ emulationState.stop() }
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is EmulationActivity) {
@ -156,8 +159,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
emulationState = EmulationState(game.path)
emulationActivity = requireActivity() as EmulationActivity
screenAdjustmentUtil = ScreenAdjustmentUtil(requireContext(), requireActivity().windowManager, settingsViewModel.settings)
EmulationLifecycleUtil.addShutdownHook(hook = { emulationState.stop() })
EmulationLifecycleUtil.addPauseResumeHook(hook = { togglePause() })
EmulationLifecycleUtil.addPauseResumeHook(onPause)
EmulationLifecycleUtil.addShutdownHook(onShutdown)
}
override fun onCreateView(
@ -507,6 +510,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
super.onDetach()
}
override fun onDestroy() {
EmulationLifecycleUtil.removeHook(onPause)
EmulationLifecycleUtil.removeHook(onShutdown)
super.onDestroy()
}
private fun setupCitraDirectoriesThenStartEmulation() {
val directoryInitializationState = DirectoryInitialization.start()
if (directoryInitializationState ===

View file

@ -0,0 +1,81 @@
// 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.Manifest
import android.app.Dialog
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.citra.citra_emu.R
import org.citra.citra_emu.ui.main.MainActivity
import org.citra.citra_emu.utils.BuildUtil
class GrantMissingFilesystemPermissionFragment : DialogFragment() {
private lateinit var mainActivity: MainActivity
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
BuildUtil.assertNotGooglePlay()
mainActivity = requireActivity() as MainActivity
isCancelable = false
val requestPermissionFunction =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
{
manageExternalStoragePermissionLauncher.launch(
Intent(
Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
Uri.fromParts("package", mainActivity.packageName, null)
)
)
}
} else {
{ permissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) }
}
return MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.filesystem_permission_warning)
.setMessage(R.string.filesystem_permission_lost)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
requestPermissionFunction()
}
.show()
}
@RequiresApi(Build.VERSION_CODES.R)
private val manageExternalStoragePermissionLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (Environment.isExternalStorageManager()) {
return@registerForActivityResult
}
}
private val permissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted) {
return@registerForActivityResult
}
}
companion object {
const val TAG = "GrantMissingFilesystemPermissionFragment"
fun newInstance(): GrantMissingFilesystemPermissionFragment {
BuildUtil.assertNotGooglePlay()
return GrantMissingFilesystemPermissionFragment()
}
}
}

View file

@ -159,7 +159,7 @@ class HomeSettingsFragment : Fragment() {
R.string.select_citra_user_folder,
R.string.select_citra_user_folder_home_description,
R.drawable.ic_home,
{ mainActivity?.openCitraDirectory?.launch(null) },
{ PermissionsHandler.compatibleSelectDirectory(mainActivity.openCitraDirectory) },
details = homeViewModel.userDir
),
HomeSetting(

View file

@ -13,21 +13,25 @@ import androidx.lifecycle.ViewModelProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.citra.citra_emu.R
import org.citra.citra_emu.ui.main.MainActivity
import org.citra.citra_emu.utils.PermissionsHandler
import org.citra.citra_emu.viewmodel.HomeViewModel
class SelectUserDirectoryDialogFragment : DialogFragment() {
class SelectUserDirectoryDialogFragment(titleOverride: Int? = null, descriptionOverride: Int? = null) : DialogFragment() {
private lateinit var mainActivity: MainActivity
private val title = titleOverride ?: R.string.select_citra_user_folder
private val description = descriptionOverride ?: R.string.selecting_user_directory_without_write_permissions
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
mainActivity = requireActivity() as MainActivity
isCancelable = false
return MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.select_citra_user_folder)
.setMessage(R.string.selecting_user_directory_without_write_permissions)
.setTitle(title)
.setMessage(description)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
mainActivity?.openCitraDirectoryLostPermission?.launch(null)
PermissionsHandler.compatibleSelectDirectory(mainActivity.openCitraDirectoryLostPermission)
}
.show()
}
@ -35,9 +39,10 @@ class SelectUserDirectoryDialogFragment : DialogFragment() {
companion object {
const val TAG = "SelectUserDirectoryDialogFragment"
fun newInstance(activity: FragmentActivity): SelectUserDirectoryDialogFragment {
fun newInstance(activity: FragmentActivity, titleOverride: Int? = null, descriptionOverride: Int? = null):
SelectUserDirectoryDialogFragment {
ViewModelProvider(activity)[HomeViewModel::class.java].setPickingUserDir(true)
return SelectUserDirectoryDialogFragment()
return SelectUserDirectoryDialogFragment(titleOverride, descriptionOverride)
}
}
}

View file

@ -11,11 +11,13 @@ import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
@ -30,7 +32,9 @@ import androidx.preference.PreferenceManager
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.transition.MaterialFadeThrough
import org.citra.citra_emu.BuildConfig
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R
import org.citra.citra_emu.adapters.SetupAdapter
import org.citra.citra_emu.databinding.FragmentSetupBinding
@ -41,6 +45,7 @@ import org.citra.citra_emu.model.PageState
import org.citra.citra_emu.model.SetupCallback
import org.citra.citra_emu.model.SetupPage
import org.citra.citra_emu.ui.main.MainActivity
import org.citra.citra_emu.utils.BuildUtil
import org.citra.citra_emu.utils.CitraDirectoryHelper
import org.citra.citra_emu.utils.GameHelper
import org.citra.citra_emu.utils.PermissionsHandler
@ -142,7 +147,57 @@ class SetupFragment : Fragment() {
false,
0,
pageButtons = mutableListOf<PageButton>().apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
if (BuildConfig.FLAVOR != "googlePlay") {
add(
PageButton(
R.drawable.ic_folder,
R.string.filesystem_permission,
R.string.filesystem_permission_description,
buttonAction = {
pageButtonCallback = it
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
manageExternalStoragePermissionLauncher.launch(
Intent(
android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
Uri.fromParts(
"package",
requireActivity().packageName,
null
)
)
)
} else {
permissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
},
buttonState = {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (Environment.isExternalStorageManager()) {
ButtonState.BUTTON_ACTION_COMPLETE
} else {
ButtonState.BUTTON_ACTION_INCOMPLETE
}
} else {
if (ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
) {
ButtonState.BUTTON_ACTION_COMPLETE
} else {
ButtonState.BUTTON_ACTION_INCOMPLETE
}
}
},
isUnskippable = true,
hasWarning = true,
R.string.filesystem_permission_warning,
R.string.filesystem_permission_warning_description,
)
)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
add(
PageButton(
R.drawable.ic_notification,
@ -214,18 +269,36 @@ class SetupFragment : Fragment() {
)
},
) {
if (
var permissionsComplete =
// Microphone
ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.RECORD_AUDIO
) == PackageManager.PERMISSION_GRANTED &&
// Camera
ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED &&
// Notifications
NotificationManagerCompat.from(requireContext())
.areNotificationsEnabled()
) {
// External Storage
@Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
if (BuildConfig.FLAVOR != "googlePlay") {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
permissionsComplete =
(permissionsComplete && Environment.isExternalStorageManager())
} else {
permissionsComplete =
(permissionsComplete && ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED)
}
}
if (permissionsComplete) {
PageState.PAGE_STEPS_COMPLETE
} else {
PageState.PAGE_STEPS_INCOMPLETE
@ -249,7 +322,7 @@ class SetupFragment : Fragment() {
R.string.select_citra_user_folder_description,
buttonAction = {
pageButtonCallback = it
openCitraDirectory.launch(null)
PermissionsHandler.compatibleSelectDirectory(openCitraDirectory)
},
buttonState = {
if (PermissionsHandler.hasWriteAccess(requireContext())) {
@ -452,6 +525,19 @@ class SetupFragment : Fragment() {
}
}
private fun showPermissionDeniedSnackbar() {
Snackbar.make(binding.root, R.string.permission_denied, Snackbar.LENGTH_LONG)
.setAnchorView(binding.buttonNext)
.setAction(R.string.grid_menu_core_settings) {
val intent =
Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", requireActivity().packageName, null)
intent.data = uri
startActivity(intent)
}
.show()
}
private val permissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted) {
@ -459,16 +545,20 @@ class SetupFragment : Fragment() {
return@registerForActivityResult
}
Snackbar.make(binding.root, R.string.permission_denied, Snackbar.LENGTH_LONG)
.setAnchorView(binding.buttonNext)
.setAction(R.string.grid_menu_core_settings) {
val intent =
Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", requireActivity().packageName, null)
intent.data = uri
startActivity(intent)
}
.show()
showPermissionDeniedSnackbar()
}
// We can't use permissionLauncher because MANAGE_EXTERNAL_STORAGE is a special snowflake
@RequiresApi(Build.VERSION_CODES.R)
private val manageExternalStoragePermissionLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
BuildUtil.assertNotGooglePlay()
if (Environment.isExternalStorageManager()) {
checkForButtonState.invoke()
return@registerForActivityResult
}
showPermissionDeniedSnackbar()
}
private val openCitraDirectory = registerForActivityResult<Uri, Uri>(
@ -478,6 +568,15 @@ class SetupFragment : Fragment() {
return@registerForActivityResult
}
if (NativeLibrary.getUserDirectory(result) == "") {
SelectUserDirectoryDialogFragment.newInstance(
mainActivity,
R.string.invalid_selection,
R.string.invalid_user_directory
).show(mainActivity.supportFragmentManager, SelectUserDirectoryDialogFragment.TAG)
return@registerForActivityResult
}
CitraDirectoryHelper(requireActivity(), true).showCitraDirectoryDialog(result, pageButtonCallback, checkForButtonState)
}

View file

@ -153,8 +153,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
if (isActionMove) {
NativeLibrary.onTouchMoved(xPosition.toFloat(), yPosition.toFloat())
continue
}
else if (isActionUp) {
} else if (isActionUp) {
NativeLibrary.onTouchEvent(0f, 0f, false)
break // Up and down actions shouldn't loop
}

View file

@ -4,9 +4,13 @@
package org.citra.citra_emu.ui.main
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import android.view.WindowManager
@ -36,6 +40,8 @@ import androidx.work.WorkManager
import com.google.android.material.color.MaterialColors
import com.google.android.material.navigation.NavigationBarView
import kotlinx.coroutines.launch
import org.citra.citra_emu.BuildConfig
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R
import org.citra.citra_emu.contracts.OpenFileResultContract
import org.citra.citra_emu.databinding.ActivityMainBinding
@ -43,6 +49,7 @@ import org.citra.citra_emu.features.settings.model.Settings
import org.citra.citra_emu.features.settings.model.SettingsViewModel
import org.citra.citra_emu.features.settings.ui.SettingsActivity
import org.citra.citra_emu.features.settings.utils.SettingsFile
import org.citra.citra_emu.fragments.GrantMissingFilesystemPermissionFragment
import org.citra.citra_emu.fragments.SelectUserDirectoryDialogFragment
import org.citra.citra_emu.fragments.UpdateUserDirectoryDialogFragment
import org.citra.citra_emu.utils.CiaInstallWorker
@ -185,14 +192,55 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext)
.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)
if (!firstTimeSetup && !PermissionsHandler.hasWriteAccess(this) &&
!homeViewModel.isPickingUserDir.value
) {
if (firstTimeSetup) {
return
}
@Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
if (BuildConfig.FLAVOR != "googlePlay") {
fun requestMissingFilesystemPermission() =
GrantMissingFilesystemPermissionFragment.newInstance()
.show(supportFragmentManager, GrantMissingFilesystemPermissionFragment.TAG)
if (supportFragmentManager.findFragmentByTag(GrantMissingFilesystemPermissionFragment.TAG) == null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
requestMissingFilesystemPermission()
}
} else {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) {
requestMissingFilesystemPermission()
}
}
}
}
if (homeViewModel.isPickingUserDir.value) {
return
}
if (!PermissionsHandler.hasWriteAccess(this)) {
SelectUserDirectoryDialogFragment.newInstance(this)
.show(supportFragmentManager, SelectUserDirectoryDialogFragment.TAG)
} else if (!firstTimeSetup && !homeViewModel.isPickingUserDir.value && CitraDirectoryUtils.needToUpdateManually()) {
return
} else if (CitraDirectoryUtils.needToUpdateManually()) {
UpdateUserDirectoryDialogFragment.newInstance(this)
.show(supportFragmentManager,UpdateUserDirectoryDialogFragment.TAG)
return
}
@Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
if (BuildConfig.FLAVOR != "googlePlay") {
if (supportFragmentManager.findFragmentByTag(SelectUserDirectoryDialogFragment.TAG) == null) {
if (NativeLibrary.getUserDirectory() == "") {
SelectUserDirectoryDialogFragment.newInstance(this)
.show(supportFragmentManager, SelectUserDirectoryDialogFragment.TAG)
}
}
}
}
@ -316,6 +364,15 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
return@registerForActivityResult
}
if (NativeLibrary.getUserDirectory(result) == "") {
SelectUserDirectoryDialogFragment.newInstance(
this,
R.string.invalid_selection,
R.string.invalid_user_directory
).show(supportFragmentManager, SelectUserDirectoryDialogFragment.TAG)
return@registerForActivityResult
}
CitraDirectoryHelper(this@MainActivity, permissionsLost)
.showCitraDirectoryDialog(result, buttonState = {})
}

View file

@ -0,0 +1,17 @@
// 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.utils
import org.citra.citra_emu.BuildConfig
object BuildUtil {
fun assertNotGooglePlay() {
@Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
if (BuildConfig.FLAVOR == "googlePlay") {
error("Non-GooglePlay code being called in GooglePlay build")
}
}
}

View file

@ -6,10 +6,12 @@ package org.citra.citra_emu.utils
import android.net.Uri
import android.provider.DocumentsContract
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.model.CheapDocument
import java.net.URLDecoder
import java.nio.file.Paths
import java.util.StringTokenizer
import java.util.concurrent.ConcurrentHashMap
@ -191,7 +193,7 @@ class DocumentsTree {
}
@Synchronized
fun renameFile(filepath: String, destinationFilename: String?): Boolean {
fun renameFile(filepath: String, destinationFilename: String): Boolean {
val node = resolvePath(filepath) ?: return false
try {
val filename = URLDecoder.decode(destinationFilename, FileUtil.DECODE_METHOD)
@ -203,6 +205,20 @@ class DocumentsTree {
}
}
@Synchronized
fun moveFile(filename: String, sourceDirPath: String, destDirPath: String): Boolean {
val sourceFileNode = resolvePath(sourceDirPath + "/" + filename) ?: return false
val sourceDirNode = resolvePath(sourceDirPath) ?: return false
val destDirNode = resolvePath(destDirPath) ?: return false
try {
val newUri = DocumentsContract.moveDocument(context.contentResolver, sourceFileNode.uri!!, sourceDirNode.uri!!, destDirNode.uri!!)
updateDocumentLocation("$sourceDirPath/$filename", "$destDirPath/$filename")
return true
} catch (e: Exception) {
error("[DocumentsTree]: Cannot move file, error: " + e.message)
}
}
@Synchronized
fun deleteDocument(filepath: String): Boolean {
val node = resolvePath(filepath) ?: return false
@ -219,6 +235,29 @@ class DocumentsTree {
}
}
@Synchronized
fun updateDocumentLocation(sourcePath: String, destinationPath: String): Boolean {
val sourceNode = resolvePath(sourcePath)
val newName = Paths.get(destinationPath).fileName.toString()
val parentPath = Paths.get(destinationPath).parent.toString()
val newParent = resolvePath(parentPath)
val newUri = (getUri(parentPath).toString() + "%2F$newName").toUri() // <- Is there a better way?
if (sourceNode == null || newParent == null) {
return false
}
sourceNode.parent!!.removeChild(sourceNode)
sourceNode.name = newName
sourceNode.parent = newParent
sourceNode.uri = newUri
newParent.addChild(sourceNode)
return true
}
@Synchronized
private fun resolvePath(filepath: String): DocumentsNode? {
root ?: return null

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.
@ -18,15 +18,27 @@ object EmulationLifecycleUtil {
}
fun addShutdownHook(hook: Runnable) {
shutdownHooks.add(hook)
if (shutdownHooks.contains(hook)) {
Log.warning("[EmulationLifecycleUtil] Tried to add shutdown hook for function that already existed. Skipping.")
} else {
shutdownHooks.add(hook)
}
}
fun addPauseResumeHook(hook: Runnable) {
pauseResumeHooks.add(hook)
if (pauseResumeHooks.contains(hook)) {
Log.warning("[EmulationLifecycleUtil] Tried to add pause resume hook for function that already existed. Skipping.")
} else {
pauseResumeHooks.add(hook)
}
}
fun clear() {
pauseResumeHooks.clear()
shutdownHooks.clear()
fun removeHook(hook: Runnable) {
if (pauseResumeHooks.contains(hook)) {
pauseResumeHooks.remove(hook)
}
if (shutdownHooks.contains(hook)) {
shutdownHooks.remove(hook)
}
}
}

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.
@ -11,6 +11,7 @@ import android.net.Uri
import android.provider.DocumentsContract
import android.system.Os
import android.util.Pair
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.model.CheapDocument
@ -434,6 +435,20 @@ object FileUtil {
return false
}
@JvmStatic
fun moveFile(filename: String, sourceDirUriString: String, destDirUriString: String): Boolean {
try {
val sourceFileUri = ("$sourceDirUriString%2F$filename").toUri()
val sourceDirUri = sourceDirUriString.toUri()
val destDirUri = destDirUriString.toUri()
DocumentsContract.moveDocument(context.contentResolver, sourceFileUri, sourceDirUri, destDirUri)
return true
} catch (e: Exception) {
Log.error("[FileUtil]: Cannot move file, error: " + e.message)
}
return false
}
@JvmStatic
fun deleteDocument(path: String): Boolean {
try {

View file

@ -8,6 +8,9 @@ import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
import android.provider.DocumentsContract
import androidx.activity.result.ActivityResultLauncher
import androidx.preference.PreferenceManager
import androidx.documentfile.provider.DocumentFile
import org.citra.citra_emu.CitraApplication
@ -48,4 +51,17 @@ object PermissionsHandler {
fun setCitraDirectory(uriString: String?) =
preferences.edit().putString(CITRA_DIRECTORY, uriString).apply()
fun compatibleSelectDirectory(activityLauncher: ActivityResultLauncher<Uri?>) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
activityLauncher.launch(null)
} else {
val initialUri = DocumentsContract.buildRootUri(
"com.android.externalstorage.documents",
"primary"
)
activityLauncher.launch(initialUri)
}
}
}

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.
package org.citra.citra_emu.utils
import org.citra.citra_emu.utils.BuildUtil
import java.io.File
object RemovableStorageHelper {
// This really shouldn't be necessary, but the Android API seemingly
// doesn't have a way of doing this?
// Apparently, on certain devices the mount location can vary, so add
// extra cases here if we discover any new ones.
fun getRemovableStoragePath(idString: String): String? {
BuildUtil.assertNotGooglePlay()
var pathFile: File
pathFile = File("/mnt/media_rw/$idString");
if (pathFile.exists()) {
return pathFile.absolutePath
}
return null
}
}

View file

@ -657,6 +657,25 @@ void Java_org_citra_citra_1emu_NativeLibrary_onTouchMoved([[maybe_unused]] JNIEn
window->OnTouchMoved((int)x, (int)y);
}
jboolean Java_org_citra_citra_1emu_NativeLibrary_onSecondaryTouchEvent([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jobject obj,
jfloat x, jfloat y,
jboolean pressed) {
if (!secondary_window) {
return JNI_FALSE;
}
return static_cast<jboolean>(secondary_window->OnTouchEvent(
static_cast<int>(x + 0.5), static_cast<int>(y + 0.5), pressed));
}
void Java_org_citra_citra_1emu_NativeLibrary_onSecondaryTouchMoved([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jobject obj,
jfloat x, jfloat y) {
if (secondary_window) {
secondary_window->OnTouchMoved((int)x, (int)y);
}
}
jlong Java_org_citra_citra_1emu_NativeLibrary_getTitleId(JNIEnv* env, [[maybe_unused]] jobject obj,
jstring j_filename) {
std::string filepath = GetJString(env, j_filename);

View file

@ -193,6 +193,10 @@
<string name="region_mismatch">Advertiment Regió No Vàlida</string>
<string name="region_mismatch_emulated">La configuració del país no és vàlida per a la regió emulada seleccionada.</string>
<string name="region_mismatch_console">La configuració del país no és vàlida per a la consola vinculada actual.</string>
<string name="storage">Emmagatzematge</string>
<string name="compress_cia_installs">Comprimir el contingut de CIAs instal·lats</string>
<string name="compress_cia_installs_description">Comprimix el contingut de fitxers CIA quan són instal·lats a la SD emulada. Només afecta contingut CIA instal·lat amb esta opció activada.</string>
<!-- Camera settings strings -->
<string name="inner_camera">Càmera interior</string>
<string name="outer_left_camera">Càmera esquerra externa</string>
@ -397,6 +401,10 @@ S\'esperen errors gràfics temporals quan estigue activat.</string>
<string name="emulation_configure_controls">Configurar Controls</string>
<string name="emulation_edit_layout">Editar Estil</string>
<string name="emulation_done">Fet</string>
<string name="emulation_button_sliding">Lliscament de botons</string>
<string name="emulation_button_sliding_disabled">Mantindre el botó pressionat originalment</string>
<string name="emulation_button_sliding_enabled">Mantindre el botó pressionat actualment</string>
<string name="emulation_button_sliding_alternative">Mantindre el botó original i actualment pressionat</string>
<string name="emulation_toggle_controls">Activar Controls</string>
<string name="emulation_control_scale">Ajustar Escala</string>
<string name="emulation_control_scale_global">Escala Global</string>
@ -409,6 +417,8 @@ S\'esperen errors gràfics temporals quan estigue activat.</string>
<string name="emulation_aspect_ratio">Relació d\'Aspecte</string>
<string name="emulation_switch_screen_layout">Estil de Pantalla Apaïsada</string>
<string name="emulation_switch_portrait_layout">Estil de Pantalla de Perfil</string>
<string name="emulation_switch_secondary_layout">Estil de Pantalla Secundària</string>
<string name="emulation_switch_secondary_layout_description">La disposició de la pantalla secundària connectada, amb cable o sense fil (Chromecast, Miracast)</string>
<string name="emulation_screen_layout_largescreen">Pantalla amplia</string>
<string name="emulation_screen_layout_portrait">Vertical</string>
<string name="emulation_screen_layout_single">Pantalla Única</string>
@ -416,6 +426,7 @@ S\'esperen errors gràfics temporals quan estigue activat.</string>
<string name="emulation_screen_layout_hybrid">Pantalles híbrides</string>
<string name="emulation_screen_layout_original">Original</string>
<string name="emulation_portrait_layout_top_full">Per omissió</string>
<string name="emulation_secondary_display_default">Per defecte del sistema (espill)</string>
<string name="emulation_screen_layout_custom">Estil Personalitzat</string>
<string name="emulation_small_screen_position">Posició de Pantalla Xicoteta</string>
<string name="small_screen_position_description">On hauria d\'aparéixer la pantalla xicoteta en relació amb la gran en Proporció de Pantalla Gran?</string>
@ -536,6 +547,10 @@ S\'esperen errors gràfics temporals quan estigue activat.</string>
<string name="create_shortcut">Crear drecera</string>
<string name="shortcut_name_empty">El nom de la drecera no pot estar buit</string>
<string name="shortcut_image_stretch_toggle">Allargar per a ajustar la imatge</string>
<string name="game_context_id">ID:</string>
<string name="game_context_file">Fitxer:</string>
<string name="game_context_type">Tipus:</string>
<!-- Performance Overlay settings -->
<string name="performance_overlay_show">Mostrar informació de rendiment</string>
<string name="performance_overlay_options">Informació de rendiment</string>

View file

@ -194,7 +194,7 @@
<string name="region_mismatch_emulated">La configuración del país no es válida para la región emulada seleccionada.</string>
<string name="region_mismatch_console">La configuración del país no es válida para la consola vinculada actual.</string>
<string name="storage">Almacenamiento</string>
<string name="compress_cia_installs">Comprimir el contenido CIA instalado</string>
<string name="compress_cia_installs">Comprimir el contenido de CIAs instalados</string>
<string name="compress_cia_installs_description">Comprime el contenido de archivos CIA cuando son instalados a la SD emulada. Solo afecta contenido CIA instalado con esta opción activada.</string>
<!-- Camera settings strings -->
@ -402,9 +402,9 @@ Se esperan fallos gráficos temporales cuando ésta esté activado.</string>
<string name="emulation_edit_layout">Editar Estilo</string>
<string name="emulation_done">Hecho</string>
<string name="emulation_button_sliding">Deslizamiento de botones</string>
<string name="emulation_button_sliding_disabled">Mantenga el botón presionado originalmente</string>
<string name="emulation_button_sliding_enabled">Mantenga el botón presionado actualmente</string>
<string name="emulation_button_sliding_alternative">Mantenga el botón original y actualmente presionado</string>
<string name="emulation_button_sliding_disabled">Mantener el botón presionado originalmente</string>
<string name="emulation_button_sliding_enabled">Mantener el botón presionado actualmente</string>
<string name="emulation_button_sliding_alternative">Mantener el botón original y actualmente presionado</string>
<string name="emulation_toggle_controls">Activar Controles</string>
<string name="emulation_control_scale">Ajustar Escala</string>
<string name="emulation_control_scale_global">Escala Global</string>
@ -417,6 +417,8 @@ Se esperan fallos gráficos temporales cuando ésta esté activado.</string>
<string name="emulation_aspect_ratio">Relación de Aspecto</string>
<string name="emulation_switch_screen_layout">Estilo de Pantalla Apaisada</string>
<string name="emulation_switch_portrait_layout">Estilo de Pantalla de Perfil</string>
<string name="emulation_switch_secondary_layout">Estilo de Pantalla Secundaria</string>
<string name="emulation_switch_secondary_layout_description">El estilo de la pantalla secundaria conectada, con cable o inalámbrica (Chromecast, Miracast)</string>
<string name="emulation_screen_layout_largescreen">Pantalla amplia</string>
<string name="emulation_screen_layout_portrait">Retrato</string>
<string name="emulation_screen_layout_single">Pantalla Única</string>
@ -424,6 +426,7 @@ Se esperan fallos gráficos temporales cuando ésta esté activado.</string>
<string name="emulation_screen_layout_hybrid">Pantallas híbridas</string>
<string name="emulation_screen_layout_original">Original</string>
<string name="emulation_portrait_layout_top_full">Por defecto</string>
<string name="emulation_secondary_display_default">Por defecto del sistema (espejo)</string>
<string name="emulation_screen_layout_custom">Estilo personalizado</string>
<string name="emulation_small_screen_position">Posición Pantalla Pequeña</string>
<string name="small_screen_position_description">¿Dónde debería aparecer la pantalla pequeña en relación con la grande en Disposicion de Pantalla Grande?</string>

View file

@ -417,6 +417,7 @@
<string name="emulation_switch_screen_layout">Geometryczny Układ Ekranu</string>
<string name="emulation_switch_portrait_layout">Pionowy Układ Ekranu</string>
<string name="emulation_switch_secondary_layout">Układ ekranu wyświetlacza dodatkowego</string>
<string name="emulation_switch_secondary_layout_description">Układ używany przez podłączony dodatkowy ekran, przewodowy lub bezprzewodowy (Chromecast, Miracast)</string>
<string name="emulation_screen_layout_largescreen">Duży Ekran</string>
<string name="emulation_screen_layout_portrait">Ekran</string>
<string name="emulation_screen_layout_single">Pojedynczy ekran</string>
@ -424,6 +425,7 @@
<string name="emulation_screen_layout_hybrid">Hybrydowy Ekran</string>
<string name="emulation_screen_layout_original">Oryginalny</string>
<string name="emulation_portrait_layout_top_full">Domyślny</string>
<string name="emulation_secondary_display_default">Ustawienia domyślne systemu (mirror)</string>
<string name="emulation_screen_layout_custom">Niestandardowy Układ</string>
<string name="emulation_small_screen_position">Pozycja małego ekranu</string>
<string name="small_screen_position_description">Gdzie powinien być wyświetlany mały ekran względem dużego w układzie dużego ekranu?</string>

View file

@ -76,10 +76,14 @@
<string name="give_permission">Grant permission</string>
<string name="notification_warning">Skip granting the notification permission?</string>
<string name="notification_warning_description">Azahar won\'t be able to notify you of important information.</string>
<string name="filesystem_permission_warning">Missing Permissions</string>
<string name="filesystem_permission_warning_description">Azahar requires permission to manage files on this device in order to store and manage its data.\n\nPlease grant the \"Filesystem\" permission before continuing.</string>
<string name="camera_permission">Camera</string>
<string name="camera_permission_description">Grant the camera permission below to emulate the 3DS camera.</string>
<string name="microphone_permission">Microphone</string>
<string name="microphone_permission_description">Grant the microphone permission below to emulate the 3DS microphone.</string>
<string name="filesystem_permission">Filesystem</string>
<string name="filesystem_permission_description">Grant the filesystem permission below to allow Azahar to store files.</string>
<string name="permission_denied">Permission denied</string>
<string name="add_games_warning">Skip selecting applications folder?</string>
<string name="add_games_warning_description">Software won\'t be displayed in the Applications list if a folder isn\'t selected.</string>
@ -100,6 +104,9 @@
<string name="cannot_skip">You can\'t skip setting up the user folder</string>
<string name="cannot_skip_directory_description">This step is required to allow Azahar to work. Please select a directory and then you can continue.</string>
<string name="selecting_user_directory_without_write_permissions">You have lost write permissions on your <a href="https://web.archive.org/web/20240304193549/https://github.com/citra-emu/citra/wiki/Citra-Android-user-data-and-storage">user data</a> directory, where saves and other information are stored. This can happen after some app or Android updates. Please re-select the directory to regain permissions so you can continue.</string>
<string name="invalid_selection">Invalid Selection</string>
<string name="invalid_user_directory">The user directory selection was invalid.\nPlease re-select the user directory, ensuring that you navigate to it from the root of your device\'s storage.</string>
<string name="filesystem_permission_lost">Azahar has lost permission to manage files on this device. This can happen after some app or Android updates. Please re-grant this permission on the next screen to continue using the app.</string>
<string name="cannot_skip_directory_help" translatable="false">https://web.archive.org/web/20240304193549/https://github.com/citra-emu/citra/wiki/Citra-Android-user-data-and-storage</string>
<string name="set_up_theme_settings">Theme Settings</string>
<string name="setup_theme_settings_description">Configure your theme preferences for Azahar.</string>

View file

@ -4,8 +4,8 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.11.1" apply false
id("com.android.library") version "8.11.1" apply false
id("com.android.application") version "8.13.1" apply false
id("com.android.library") version "8.13.1" apply false
id("org.jetbrains.kotlin.android") version "2.0.20" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.20"
}

View file

@ -60,6 +60,10 @@ if (ENABLE_QT AND UNIX AND NOT APPLE)
target_link_libraries(citra_meta PRIVATE Qt6::DBus gamemode)
endif()
if (ENABLE_QT AND APPLE)
target_link_libraries(citra_meta PRIVATE Qt6::GuiPrivate)
endif()
if (ENABLE_QT AND USE_DISCORD_PRESENCE)
target_link_libraries(citra_meta PRIVATE discord-rpc)
endif()

View file

@ -172,12 +172,12 @@ add_library(citra_qt STATIC EXCLUDE_FROM_ALL
multiplayer/state.h
multiplayer/validation.h
precompiled_headers.h
qt_image_interface.cpp
qt_image_interface.h
uisettings.cpp
uisettings.h
user_data_migration.cpp
user_data_migration.h
qt_image_interface.cpp
qt_image_interface.h
util/clickable_label.cpp
util/clickable_label.h
util/graphics_device_info.cpp
@ -190,6 +190,13 @@ add_library(citra_qt STATIC EXCLUDE_FROM_ALL
util/util.h
)
if (APPLE)
target_sources(citra_qt PUBLIC
qt_swizzle.h
qt_swizzle.mm
)
endif()
file(GLOB COMPAT_LIST
${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.qrc
${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.json)
@ -275,7 +282,13 @@ if (NOT WIN32)
target_include_directories(citra_qt PRIVATE ${Qt6Gui_PRIVATE_INCLUDE_DIRS})
endif()
if (UNIX AND NOT APPLE)
if (APPLE)
target_link_libraries(citra_qt PRIVATE Qt6::GuiPrivate)
endif()
if (APPLE)
target_link_libraries(citra_qt PRIVATE Qt6::QDarwinCameraPermissionPlugin)
elseif (UNIX)
target_link_libraries(citra_qt PRIVATE Qt6::DBus gamemode)
endif()

View file

@ -69,6 +69,7 @@
#include "citra_qt/movie/movie_record_dialog.h"
#include "citra_qt/multiplayer/state.h"
#include "citra_qt/qt_image_interface.h"
#include "citra_qt/qt_swizzle.h"
#include "citra_qt/uisettings.h"
#include "common/play_time_manager.h"
#ifdef ENABLE_QT_UPDATE_CHECKER
@ -114,6 +115,7 @@
#ifdef __APPLE__
#include "common/apple_authorization.h"
Q_IMPORT_PLUGIN(QDarwinCameraPermissionPlugin);
#endif
#ifdef USE_DISCORD_PRESENCE
@ -4112,6 +4114,11 @@ static Qt::HighDpiScaleFactorRoundingPolicy GetHighDpiRoundingPolicy() {
}
void LaunchQtFrontend(int argc, char* argv[]) {
#ifdef __APPLE__
// Ensure that the linker doesn't optimize qt_swizzle.mm out of existence.
QtSwizzle::Dummy();
#endif
Common::DetachedTasks detached_tasks;
#if MICROPROFILE_ENABLED

View file

@ -0,0 +1,9 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
namespace QtSwizzle {
void Dummy();
} // namespace QtSwizzle

View file

@ -0,0 +1,48 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#import <QtGui/private/qmetallayer_p.h>
#import <objc/runtime.h>
namespace QtSwizzle {
void Dummy() {
// Call this anywhere to make sure that qt_swizzle.mm is linked.
// noop
}
} // namespace QtSwizzle
@implementation QMetalLayer (AzaharPatch)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class targetClass = [self class];
// Get the original and swizzled methods
Method originalMethod =
class_getInstanceMethod(targetClass, @selector(setNeedsDisplayInRect:));
Method swizzledMethod =
class_getInstanceMethod(targetClass, @selector(swizzled_setNeedsDisplayInRect:));
// Swap the implementations
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
- (void)swizzled_setNeedsDisplayInRect:(CGRect)rect {
constexpr auto tooBig = 1e10; // Arbitrary large number
// Check for problematic huge rectangles
if ((!self.needsDisplay) && (rect.size.width > tooBig || rect.size.height > tooBig ||
rect.origin.x < -tooBig || rect.origin.y < -tooBig)) {
return;
}
// Call the original implementation
[self swizzled_setNeedsDisplayInRect:rect];
}
@end

View file

@ -1,9 +1,13 @@
// 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.
#ifdef ANDROID
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
#include "common/android_storage.h"
#include "common/file_util.h"
#include "common/logging/log.h"
namespace AndroidStorage {
JNIEnv* GetEnvForThread() {
@ -80,8 +84,9 @@ void CleanupJNI() {
}
bool CreateFile(const std::string& directory, const std::string& filename) {
if (create_file == nullptr)
if (create_file == nullptr) {
return false;
}
auto env = GetEnvForThread();
jstring j_directory = env->NewStringUTF(directory.c_str());
jstring j_filename = env->NewStringUTF(filename.c_str());
@ -89,8 +94,9 @@ bool CreateFile(const std::string& directory, const std::string& filename) {
}
bool CreateDir(const std::string& directory, const std::string& filename) {
if (create_dir == nullptr)
if (create_dir == nullptr) {
return false;
}
auto env = GetEnvForThread();
jstring j_directory = env->NewStringUTF(directory.c_str());
jstring j_directory_name = env->NewStringUTF(filename.c_str());
@ -98,8 +104,9 @@ bool CreateDir(const std::string& directory, const std::string& filename) {
}
int OpenContentUri(const std::string& filepath, AndroidOpenMode openmode) {
if (open_content_uri == nullptr)
if (open_content_uri == nullptr) {
return -1;
}
const char* mode = "";
switch (openmode) {
@ -135,8 +142,9 @@ int OpenContentUri(const std::string& filepath, AndroidOpenMode openmode) {
std::vector<std::string> GetFilesName(const std::string& filepath) {
auto vector = std::vector<std::string>();
if (get_files_name == nullptr)
if (get_files_name == nullptr) {
return vector;
}
auto env = GetEnvForThread();
jstring j_filepath = env->NewStringUTF(filepath.c_str());
auto j_object =
@ -150,10 +158,37 @@ std::vector<std::string> GetFilesName(const std::string& filepath) {
return vector;
}
std::optional<std::string> GetUserDirectory() {
if (get_user_directory == nullptr) {
throw std::runtime_error(
"Unable to locate user directory: Function with ID 'get_user_directory' is missing");
}
auto env = GetEnvForThread();
auto j_user_directory =
(jstring)(env->CallStaticObjectMethod(native_library, get_user_directory, nullptr));
auto result = env->GetStringUTFChars(j_user_directory, nullptr);
if (result == "") {
return std::nullopt;
}
return result;
}
std::string GetBuildFlavor() {
if (get_build_flavor == nullptr) {
throw std::runtime_error(
"Unable get build flavor: Function with ID 'get_build_flavor' is missing");
}
auto env = GetEnvForThread();
const auto jflavor =
(jstring)(env->CallStaticObjectMethod(native_library, get_build_flavor, nullptr));
return env->GetStringUTFChars(jflavor, nullptr);
}
bool CopyFile(const std::string& source, const std::string& destination_path,
const std::string& destination_filename) {
if (copy_file == nullptr)
if (copy_file == nullptr) {
return false;
}
auto env = GetEnvForThread();
jstring j_source_path = env->NewStringUTF(source.c_str());
jstring j_destination_path = env->NewStringUTF(destination_path.c_str());
@ -163,8 +198,14 @@ bool CopyFile(const std::string& source, const std::string& destination_path,
}
bool RenameFile(const std::string& source, const std::string& filename) {
if (rename_file == nullptr)
if (rename_file == nullptr) {
return false;
}
if (std::string(FileUtil::GetFilename(source)) ==
std::string(FileUtil::GetFilename(filename))) {
// TODO: Should this be treated as a success or failure?
return false;
}
auto env = GetEnvForThread();
jstring j_source_path = env->NewStringUTF(source.c_str());
jstring j_destination_path = env->NewStringUTF(filename.c_str());
@ -172,6 +213,93 @@ bool RenameFile(const std::string& source, const std::string& filename) {
j_destination_path);
}
bool UpdateDocumentLocation(const std::string& source_path, const std::string& destination_path) {
if (update_document_location == nullptr) {
return false;
}
auto env = GetEnvForThread();
jstring j_source_path = env->NewStringUTF(source_path.c_str());
jstring j_destination_path = env->NewStringUTF(destination_path.c_str());
return env->CallStaticBooleanMethod(native_library, update_document_location, j_source_path,
j_destination_path);
}
bool MoveFile(const std::string& filename, const std::string& source_dir_path,
const std::string& destination_dir_path) {
if (move_file == nullptr) {
return false;
}
if (source_dir_path == destination_dir_path) {
// TODO: Should this be treated as a success or failure?
return false;
}
auto env = GetEnvForThread();
jstring j_filename = env->NewStringUTF(filename.c_str());
jstring j_source_dir_path = env->NewStringUTF(source_dir_path.c_str());
jstring j_destination_dir_path = env->NewStringUTF(destination_dir_path.c_str());
return env->CallStaticBooleanMethod(native_library, move_file, j_filename, j_source_dir_path,
j_destination_dir_path);
}
bool MoveAndRenameFile(const std::string& src_full_path, const std::string& dest_full_path) {
if (src_full_path == dest_full_path) {
// TODO: Should this be treated as a success or failure?
return false;
}
const auto src_filename = std::string(FileUtil::GetFilename(src_full_path));
const auto src_parent_path = std::string(FileUtil::GetParentPath(src_full_path));
const auto dest_filename = std::string(FileUtil::GetFilename(dest_full_path));
const auto dest_parent_path = std::string(FileUtil::GetParentPath(dest_full_path));
bool result;
const std::string tmp_path = "/tmp";
AndroidStorage::CreateDir("/", "tmp");
// If a simultaneous move and rename are not necessary, use individual methods
// TODO: Uncomment this code for 2123.4 RC to allow stress testing of move + rename process in
// beta
/*
if (src_filename == dest_filename || src_parent_path == dest_parent_path) {
if (src_filename != dest_filename) {
return AndroidStorage::RenameFile(src_full_path, dest_filename);
} else if (src_parent_path != dest_parent_path) {
return AndroidStorage::MoveFile(src_filename, src_parent_path, dest_parent_path);
}
}
*/
// Step 1: Create directory named after UUID inside /tmp to house the moved file.
// This prevents clashes if files with the same name are moved simultaneously.
const auto uuid = boost::uuids::to_string(boost::uuids::time_generator_v7()());
const auto allocated_tmp_path = tmp_path + "/" + uuid;
AndroidStorage::CreateDir(tmp_path, uuid);
// Step 2: Attempt to move to allocated temporary directory.
// If this step fails, skip everything except the cleanup.
result = AndroidStorage::MoveFile(src_filename, src_parent_path, allocated_tmp_path);
if (result == true) {
// Step 3: Rename to desired file name.
if (src_filename != dest_filename) { // TODO: Remove this if statement in 2123.4 RC, keeping
// the RenameFile call
AndroidStorage::RenameFile((allocated_tmp_path + "/" + src_filename), dest_filename);
}
// Step 4: If a file with the desired name in the destination exists, remove it.
AndroidStorage::DeleteDocument(dest_full_path);
// Step 5: Attempt to move file to desired location.
// If this step fails, move the file back to where it came from.
result = AndroidStorage::MoveFile(dest_filename, allocated_tmp_path, dest_parent_path);
if (result == false) {
AndroidStorage::MoveAndRenameFile((allocated_tmp_path + "/" + dest_filename),
src_full_path);
}
}
// Step 6: Clean up the allocated temp directory.
AndroidStorage::DeleteDocument(allocated_tmp_path);
return result;
}
#define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature) \
F(FunctionName, ReturnValue, JMethodID, Caller)
#define F(FunctionName, ReturnValue, JMethodID, Caller) \

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.
@ -19,12 +19,23 @@
open_content_uri, "openContentUri", "(Ljava/lang/String;Ljava/lang/String;)I") \
V(GetFilesName, std::vector<std::string>, (const std::string& filepath), get_files_name, \
"getFilesName", "(Ljava/lang/String;)[Ljava/lang/String;") \
V(GetUserDirectory, std::optional<std::string>, (), get_user_directory, "getUserDirectory", \
"(Landroid/net/Uri;)Ljava/lang/String;") \
V(CopyFile, bool, \
(const std::string& source, const std::string& destination_path, \
const std::string& destination_filename), \
copy_file, "copyFile", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z") \
V(RenameFile, bool, (const std::string& source, const std::string& filename), rename_file, \
"renameFile", "(Ljava/lang/String;Ljava/lang/String;)Z")
"renameFile", "(Ljava/lang/String;Ljava/lang/String;)Z") \
V(UpdateDocumentLocation, bool, \
(const std::string& source_path, const std::string& destination_path), \
update_document_location, "updateDocumentLocation", \
"(Ljava/lang/String;Ljava/lang/String;)Z") \
V(GetBuildFlavor, std::string, (), get_build_flavor, "getBuildFlavor", "()Ljava/lang/String;") \
V(MoveFile, bool, \
(const std::string& filename, const std::string& source_dir_path, \
const std::string& destination_dir_path), \
move_file, "moveFile", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z")
#define ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(V) \
V(IsDirectory, bool, is_directory, CallStaticBooleanMethod, "isDirectory", \
"(Ljava/lang/String;)Z") \
@ -44,6 +55,7 @@ ANDROID_STORAGE_FUNCTIONS(FS)
#undef F
#undef FS
#undef FR
bool MoveAndRenameFile(const std::string& src_full_path, const std::string& dest_full_path);
// Reference:
// https://developer.android.com/reference/android/os/ParcelFileDescriptor#parseMode(java.lang.String)
enum class AndroidOpenMode {

View file

@ -304,20 +304,31 @@ bool DeleteDir(const std::string& filename) {
return false;
}
bool Rename(const std::string& srcFilename, const std::string& destFilename) {
LOG_TRACE(Common_Filesystem, "{} --> {}", srcFilename, destFilename);
bool Rename(const std::string& srcFullPath, const std::string& destFullPath) {
LOG_TRACE(Common_Filesystem, "{} --> {}", srcFullPath, destFullPath);
#ifdef _WIN32
if (_wrename(Common::UTF8ToUTF16W(srcFilename).c_str(),
Common::UTF8ToUTF16W(destFilename).c_str()) == 0)
if (_wrename(Common::UTF8ToUTF16W(srcFullPath).c_str(),
Common::UTF8ToUTF16W(destFullPath).c_str()) == 0)
return true;
#elif ANDROID
if (AndroidStorage::RenameFile(srcFilename, std::string(GetFilename(destFilename))))
return true;
// srcFullPath and destFullPath are relative to the user directory
if (AndroidStorage::GetBuildFlavor() == "googlePlay") {
if (AndroidStorage::MoveAndRenameFile(srcFullPath, destFullPath))
return true;
} else {
std::optional<std::string> userDirLocation = AndroidStorage::GetUserDirectory();
if (userDirLocation && rename((*userDirLocation + srcFullPath).c_str(),
(*userDirLocation + destFullPath).c_str()) == 0) {
AndroidStorage::UpdateDocumentLocation(srcFullPath, destFullPath);
// ^ TODO: This shouldn't fail, but what should we do if it somehow does?
return true;
}
}
#else
if (rename(srcFilename.c_str(), destFilename.c_str()) == 0)
if (rename(srcFullPath.c_str(), destFullPath.c_str()) == 0)
return true;
#endif
LOG_ERROR(Common_Filesystem, "failed {} --> {}: {}", srcFilename, destFilename,
LOG_ERROR(Common_Filesystem, "failed {} --> {}: {}", srcFullPath, destFullPath,
GetLastErrorMsg());
return false;
}

View file

@ -136,13 +136,13 @@ bool Delete(const std::string& filename);
// Deletes a directory filename, returns true on success
bool DeleteDir(const std::string& filename);
// renames file srcFilename to destFilename, returns true on success
bool Rename(const std::string& srcFilename, const std::string& destFilename);
// Renames file srcFullPath to destFullPath, returns true on success
bool Rename(const std::string& srcFullPath, const std::string& destFullPath);
// copies file srcFilename to destFilename, returns true on success
// Copies file srcFilename to destFilename, returns true on success
bool Copy(const std::string& srcFilename, const std::string& destFilename);
// creates an empty file filename, returns true on success
// Creates an empty file filename, returns true on success
bool CreateEmptyFile(const std::string& filename);
/**

View file

@ -66,6 +66,10 @@ bool EmuWindow::IsWithinTouchscreen(const Layout::FramebufferLayout& layout, uns
}
#endif
if (!layout.bottom_screen_enabled) {
return false;
}
Settings::StereoRenderOption render_3d_mode = Settings::values.render_3d.GetValue();
if (render_3d_mode == Settings::StereoRenderOption::SideBySide ||

View file

@ -864,8 +864,10 @@ bool CIAFile::Close() {
if (!complete) {
LOG_ERROR(Service_AM, "CIAFile closed prematurely, aborting install...");
if (!is_additional_content) {
FileUtil::DeleteDirRecursively(
GetTitlePath(media_type, container.GetTitleMetadata().GetTitleID()));
// Only delete the content folder as there may be user save data in the title folder.
const std::string title_content_path =
GetTitlePath(media_type, container.GetTitleMetadata().GetTitleID()) + "content/";
FileUtil::DeleteDirRecursively(title_content_path);
}
return true;
}

View file

@ -61,31 +61,6 @@ constexpr static std::array<vk::DescriptorSetLayoutBinding, 1> PRESENT_BINDINGS
namespace {
static bool IsLowRefreshRate() {
#if defined(__APPLE__) || defined(ENABLE_SDL2)
#ifdef __APPLE__ // Need a special implementation because MacOS kills itself in disgust if the
// input thread calls SDL_PumpEvents at the same time as we're in SDL_Init here.
const auto cur_refresh_rate = AppleUtils::GetRefreshRate();
#elif defined(ENABLE_SDL2)
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
LOG_ERROR(Render_Vulkan, "SDL video failed to initialize, unable to check refresh rate");
return false;
}
SDL_DisplayMode cur_display_mode;
SDL_GetCurrentDisplayMode(0, &cur_display_mode); // TODO: Multimonitor handling. -OS
const auto cur_refresh_rate = cur_display_mode.refresh_rate;
SDL_QuitSubSystem(SDL_INIT_VIDEO);
#endif // __APPLE__
if (cur_refresh_rate < SCREEN_REFRESH_RATE) {
LOG_WARNING(Render_Vulkan,
"Detected refresh rate lower than the emulated 3DS screen: {}hz. FIFO will "
"be disabled",
cur_refresh_rate);
return true;
}
#endif // defined(__APPLE__) || defined(ENABLE_SDL2)
#ifdef __APPLE__
// Apple's low power mode sometimes limits applications to 30fps without changing the refresh
// rate, meaning the above code doesn't catch it.
@ -94,8 +69,34 @@ static bool IsLowRefreshRate() {
"framerate. FIFO will be disabled");
return true;
}
#endif
const auto cur_refresh_rate = AppleUtils::GetRefreshRate();
#elif defined(ENABLE_SDL2)
if (SDL_WasInit(SDL_INIT_VIDEO) == 0) {
LOG_ERROR(Render_Vulkan, "Attempted to check refresh rate via SDL, but failed because "
"SDL_INIT_VIDEO wasn't initialized");
return false;
}
SDL_DisplayMode cur_display_mode;
SDL_GetCurrentDisplayMode(0, &cur_display_mode); // TODO: Multimonitor handling. -OS
const auto cur_refresh_rate = cur_display_mode.refresh_rate;
#endif // ENABLE_SDL2
if (cur_refresh_rate < SCREEN_REFRESH_RATE) {
LOG_WARNING(Render_Vulkan,
"Detected refresh rate lower than the emulated 3DS screen: {}hz. FIFO will "
"be disabled",
cur_refresh_rate);
return true;
} else {
LOG_INFO(Render_Vulkan, "Refresh rate is above emulated 3DS screen: {}hz. Good.",
cur_refresh_rate);
}
#endif // defined(__APPLE__) || defined(ENABLE_SDL2)
// We have no available method of checking refresh rate. Just assume that everything is fine :)
return false;
}
} // Anonymous namespace

View file

@ -1,4 +1,4 @@
// Copyright 2022 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -229,7 +229,7 @@ public:
}
/// Returns the maximum supported elements in a texel buffer
u32 MaxTexelBufferElements() const {
u64 MaxTexelBufferElements() const {
return properties.limits.maxTexelBufferElements;
}

View file

@ -556,12 +556,15 @@ bool PipelineCache::EnsureDirectories() const {
};
return create_dir(FileUtil::GetUserPath(FileUtil::UserPath::ShaderDir)) &&
create_dir(GetPipelineCacheDir());
create_dir(GetVulkanDir()) && create_dir(GetPipelineCacheDir());
}
std::string PipelineCache::GetVulkanDir() const {
return FileUtil::GetUserPath(FileUtil::UserPath::ShaderDir) + "vulkan" + DIR_SEP;
}
std::string PipelineCache::GetPipelineCacheDir() const {
return FileUtil::GetUserPath(FileUtil::UserPath::ShaderDir) + "vulkan" + DIR_SEP + "pipeline" +
DIR_SEP;
return GetVulkanDir() + "pipeline" + DIR_SEP;
}
void PipelineCache::SwitchPipelineCache(u64 title_id, const std::atomic_bool& stop_loading,

View file

@ -108,6 +108,9 @@ private:
/// Create pipeline cache directories. Returns true on success.
bool EnsureDirectories() const;
/// Returns the Vulkan shader directory
std::string GetVulkanDir() const;
/// Returns the pipeline cache storage dir
std::string GetPipelineCacheDir() const;

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.
@ -16,6 +16,10 @@
#endif
#include "video_core/video_core.h"
#ifdef ENABLE_SDL2
#include <SDL.h>
#endif
namespace VideoCore {
std::unique_ptr<RendererBase> CreateRenderer(Frontend::EmuWindow& emu_window,
@ -29,6 +33,12 @@ std::unique_ptr<RendererBase> CreateRenderer(Frontend::EmuWindow& emu_window,
#endif
#ifdef ENABLE_VULKAN
case Settings::GraphicsAPI::Vulkan:
#if defined(ENABLE_SDL2) && !defined(__APPLE__)
// TODO: When we migrate to SDL3, refactor so that we don't need to init here.
if (SDL_WasInit(SDL_INIT_VIDEO) == 0) {
SDL_Init(SDL_INIT_VIDEO);
}
#endif // ENABLE_SDL2
return std::make_unique<Vulkan::RendererVulkan>(system, pica, emu_window, secondary_window);
#endif
#ifdef ENABLE_OPENGL

View file

@ -7,7 +7,7 @@ The scripts in this directory assume that your current working directory is the
## Pre-release checklist
- [ ] Update compatibility list
- [ ] Update translations
- [ ] If this is a major release (2123.1 -> major.minor), update translations
### Note:

6
tools/purge-github-cache.sh Executable file
View file

@ -0,0 +1,6 @@
#!/bin/bash -ex
# This script assumes that the Github CLI is installed and that
# the authenticated user has appropriate authorization.
gh cache delete --all