Merge branch 'master' into COMBO_BUTTON

This commit is contained in:
ADAS2024 2026-05-09 18:36:12 -04:00 committed by GitHub
commit 4332dfe809
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
92 changed files with 2891 additions and 735 deletions

26
.ci/mxe.sh Executable file
View file

@ -0,0 +1,26 @@
#!/bin/bash -ex
# TODO: Why doesn't the CI environment use the PATH set in the Dockerimage?
# It works fine when using the image locally.
export PATH="/mxe/usr/bin:${PATH}"
mkdir build && cd build
if [ "$GITHUB_REF_TYPE" == "tag" ]; then
export EXTRA_CMAKE_FLAGS=(-DENABLE_QT_UPDATE_CHECKER=ON)
fi
x86_64-w64-mingw32.shared-cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DENABLE_QT_TRANSLATION=ON \
-DUSE_DISCORD_PRESENCE=ON \
-DUSE_SYSTEM_BOOST=ON \
-DUSE_SYSTEM_CRYPTOPP=ON \
"${EXTRA_CMAKE_FLAGS[@]}"
x86_64-w64-mingw32.shared-cmake --build . -- -j$(nproc)
x86_64-w64-mingw32.shared-strip -s bin/Release/*.exe
make bundle
ccache -s -v

View file

@ -36,10 +36,10 @@ function pack_artifacts() {
fi
# Create .zip/.tar.gz
if [ "$OS" = "windows" ]; then
if [ "$OS" = "windows" ] && [ "$TARGET" != "mxe" ]; then
ARCHIVE_FULL_NAME="$ARCHIVE_NAME.zip"
powershell Compress-Archive "$REV_NAME" "$ARCHIVE_FULL_NAME"
elif [ "$OS" = "android" ] || [ "$OS" = "macos" ]; then
elif [ "$OS" = "android" ] || [ "$OS" = "macos" ] || [ "$TARGET" = "mxe" ]; then
ARCHIVE_FULL_NAME="$ARCHIVE_NAME.zip"
zip -r "$ARCHIVE_FULL_NAME" "$REV_NAME"
else

View file

@ -143,11 +143,21 @@ jobs:
path: artifacts/
windows:
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
target: ["msvc", "msys2"]
include:
- target: msvc
os: windows-latest
- target: msys2
os: windows-latest
- target: mxe
os: ubuntu-latest
container:
image: opensauce04/azahar-build-environment:latest
options: -u 1001
runs-on: ${{ matrix.os }}
container: ${{ matrix.container }}
defaults:
run:
shell: ${{ (matrix.target == 'msys2' && 'msys2') || 'bash' }} {0}
@ -195,23 +205,35 @@ jobs:
with:
args: install ptime wget
- name: Install NSIS
if: ${{ github.ref_type == 'tag' }}
if: ${{ github.ref_type == 'tag' && matrix.target != 'mxe' }}
run: |
wget https://download.sourceforge.net/project/nsis/NSIS%203/3.11/nsis-3.11-setup.exe -O D:/a/_temp/nsis-setup.exe
ptime D:/a/_temp/nsis-setup.exe /S
shell: pwsh
- name: Disable line ending translation
run: git config --global core.autocrlf input
- name: Build
- name: Build (Native)
if: ${{ matrix.target != 'mxe' }}
run: ./.ci/windows.sh
- name: Generate installer
if: ${{ github.ref_type == 'tag' }}
- name: Build (MXE)
if: ${{ matrix.target == 'mxe' }}
run: ./.ci/mxe.sh
- name: Generate installer (Native)
if: ${{ github.ref_type == 'tag' && matrix.target != 'mxe' }}
run: |
cd src\installer
"C:\Program Files (x86)\NSIS\makensis.exe" /DPRODUCT_VARIANT=${{ matrix.target }} /DPRODUCT_VERSION=${{ github.ref_name }} citra.nsi
mkdir ..\..\artifacts 2> NUL
move /y *.exe ..\..\artifacts\
shell: cmd
- name: Generate installer (MXE)
if: ${{ github.ref_type == 'tag' && matrix.target == 'mxe' }}
run: |
export PATH="/mxe/usr/bin:${PATH}" # TODO: Why do we have to do this if it's in the image?
cd src/installer
x86_64-w64-mingw32.shared-makensis -DPRODUCT_VARIANT=${{ matrix.target }} -DPRODUCT_VERSION=${{ github.ref_name }} citra.nsi
mkdir -p ../../artifacts
mv ./*.exe ../../artifacts/
- name: Pack
run: ./.ci/pack.sh
- name: Upload
@ -256,18 +278,15 @@ jobs:
echo "GIT_TAG_NAME=$GITHUB_REF_NAME" >> $GITHUB_ENV
fi
echo $GIT_TAG_NAME
- name: Update Android SDK CMake version
if: ${{ env.SHOULD_RUN == 'true' }}
run: |
yes | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "cmake;3.30.3"
- name: Install tools
if: ${{ env.SHOULD_RUN == 'true' }}
run: |
sudo apt-get update -y
sudo apt-get install -y apksigner ccache
echo "Checking ninja version..."
/usr/local/bin/ninja --version
ln -sf /usr/local/bin/ninja /usr/local/lib/android/sdk/cmake/3.30.3/bin/ninja
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

5
.gitmodules vendored
View file

@ -66,7 +66,7 @@
url = https://github.com/septag/dds-ktx
[submodule "openal-soft"]
path = externals/openal-soft
url = https://github.com/kcat/openal-soft
url = https://github.com/azahar-emu/openal-soft
[submodule "glslang"]
path = externals/glslang
url = https://github.com/KhronosGroup/glslang
@ -106,3 +106,6 @@
[submodule "externals/libretro-common"]
path = externals/libretro-common/libretro-common
url = https://github.com/libretro/libretro-common.git
[submodule "dllwalker"]
path = externals/dllwalker
url = https://github.com/azahar-emu/dllwalker

View file

@ -13,6 +13,9 @@ cmake_policy(SET CMP0063 NEW)
cmake_policy(SET CMP0127 NEW)
set(CMAKE_POLICY_DEFAULT_CMP0063 NEW)
# Prefer building bundled dependencies as static instead of shared
set(BUILD_SHARED_LIBS OFF)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMakeModules")
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/externals/cmake-modules")
include(DownloadExternals)
@ -25,6 +28,11 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR CMAKE_SYSTEM_NAME STREQUAL "iOS")
enable_language(OBJC OBJCXX)
endif()
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux" AND MINGW)
string(TOLOWER ${LIBTYPE} LIBTYPE_LOWER)
set(CMAKE_AR x86_64-w64-mingw32.${LIBTYPE_LOWER}-gcc-ar)
endif()
if (BSD STREQUAL "OpenBSD")
add_link_options(-z wxneeded)
endif()
@ -99,7 +107,7 @@ endif()
# Track which options were explicitly set by the user (for libretro conflict detection)
set(_LIBRETRO_INCOMPATIBLE_OPTIONS
ENABLE_SDL2 ENABLE_QT ENABLE_WEB_SERVICE ENABLE_SCRIPTING
ENABLE_SDL2 ENABLE_QT ENABLE_WEB_SERVICE ENABLE_SCRIPTING ENABLE_GDBSTUB
ENABLE_OPENAL ENABLE_ROOM ENABLE_ROOM_STANDALONE ENABLE_CUBEB ENABLE_LIBUSB)
set(_USER_SET_OPTIONS "")
foreach(_opt IN LISTS _LIBRETRO_INCOMPATIBLE_OPTIONS)
@ -122,6 +130,7 @@ CMAKE_DEPENDENT_OPTION(ENABLE_ROOM_STANDALONE "Enable generating a standalone de
option(ENABLE_WEB_SERVICE "Enable web services (telemetry, etc.)" ON)
option(ENABLE_SCRIPTING "Enable RPC server for scripting" ON)
option(ENABLE_GDBSTUB "Enable GDB stub for emulated applications" ON)
CMAKE_DEPENDENT_OPTION(ENABLE_CUBEB "Enables the cubeb audio backend" ON "NOT IOS" OFF)
option(ENABLE_OPENAL "Enables the OpenAL audio backend" ON)

View file

@ -198,6 +198,10 @@ if (BUNDLE_TARGET_EXECUTE)
# On Linux, always bundle an AppImage.
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")
if (IS_MINGW)
return()
endif()
if (IN_PLACE)
message(FATAL_ERROR "Cannot bundle for Linux in-place.")
endif()
@ -273,15 +277,23 @@ else()
# On Linux, add a command to prepare linuxdeploy and any required plugins before any bundling occurs.
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")
add_custom_command(
TARGET bundle
COMMAND ${CMAKE_COMMAND}
"-DBUNDLE_TARGET_DOWNLOAD_LINUXDEPLOY=1"
"-DLINUXDEPLOY_PATH=${CMAKE_BINARY_DIR}/externals/linuxdeploy"
"-DLINUXDEPLOY_ARCH=${CMAKE_HOST_SYSTEM_PROCESSOR}"
-P "${CMAKE_SOURCE_DIR}/CMakeModules/BundleTarget.cmake"
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
POST_BUILD)
if (MINGW)
add_custom_command(
TARGET bundle
# The target here is arbitrary
COMMAND cp -r "$<TARGET_FILE_DIR:citra_meta>/*" "${CMAKE_BINARY_DIR}/bundle/"
POST_BUILD)
else()
add_custom_command(
TARGET bundle
COMMAND ${CMAKE_COMMAND}
"-DBUNDLE_TARGET_DOWNLOAD_LINUXDEPLOY=1"
"-DLINUXDEPLOY_PATH=${CMAKE_BINARY_DIR}/externals/linuxdeploy"
"-DLINUXDEPLOY_ARCH=${CMAKE_HOST_SYSTEM_PROCESSOR}"
-P "${CMAKE_SOURCE_DIR}/CMakeModules/BundleTarget.cmake"
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
POST_BUILD)
endif()
endif()
endfunction()
@ -293,6 +305,11 @@ else()
create_base_bundle_target()
endif()
if (CMAKE_HOST_SYSTEM STREQUAL "Linux" AND MINGW)
# We don't really need to "bundle" MXE builds, so don't do anything
return()
endif()
set(bundle_executable_path "$<TARGET_FILE:${target_name}>")
if (bundle_qt AND APPLE)
# For Qt targets on Apple, expect an app bundle.
@ -331,6 +348,7 @@ else()
"-DBUNDLE_LIBRARY_PATHS=\"${bundle_library_paths}\""
"-DBUNDLE_QT=${bundle_qt}"
"-DIN_PLACE=${in_place}"
"-DIS_MINGW=${MINGW}"
"-DLINUXDEPLOY=${CMAKE_BINARY_DIR}/externals/linuxdeploy/squashfs-root/AppRun"
-P "${CMAKE_SOURCE_DIR}/CMakeModules/BundleTarget.cmake"
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}")

View file

@ -17,6 +17,7 @@ foreach(KEY IN ITEMS
"use_virtual_sd"
"use_custom_storage"
"compress_cia_installs"
"async_fs_operations"
"region_value"
"init_clock"
"init_time"
@ -48,6 +49,7 @@ foreach(KEY IN ITEMS
"texture_filter"
"texture_sampling"
"delay_game_render_thread_us"
"simulate_3ds_gpu_timings"
"layout_option"
"swap_screen"
"upright_screen"
@ -107,6 +109,7 @@ foreach(KEY IN ITEMS
"output_device"
"input_type"
"input_device"
"simulate_headphones_plugged"
"delay_start_for_lle_modules"
"use_gdbstub"
"gdbstub_port"
@ -115,6 +118,7 @@ foreach(KEY IN ITEMS
"log_filter"
"log_regex_filter"
"toggle_unique_data_console_type"
"break_on_unmapped_memory_access"
"use_integer_scaling"
"layouts_to_cycle"
"camera_inner_flip"

View file

@ -9,6 +9,7 @@ COPY . /var/azahar-src
RUN mkdir builddir && cd builddir && \
cmake /var/azahar-src -G Ninja \
-DENABLE_QT=OFF \
-DENABLE_GDBSTUB=OFF \
-DENABLE_TESTS=OFF \
-DENABLE_ROOM=ON \
-DENABLE_ROOM_STANDALONE=ON \

View file

@ -57,7 +57,7 @@ if (ENABLE_TESTS)
else()
set(CATCH_INSTALL_DOCS OFF CACHE BOOL "")
set(CATCH_INSTALL_EXTRAS OFF CACHE BOOL "")
add_subdirectory(catch2)
add_subdirectory(catch2 EXCLUDE_FROM_ALL)
endif()
target_link_libraries(catch2 INTERFACE Catch2::Catch2WithMain)
include(Catch)
@ -79,7 +79,7 @@ else()
set(CRYPTOPP_BUILD_TESTING OFF CACHE BOOL "")
set(CRYPTOPP_INSTALL OFF CACHE BOOL "")
set(CRYPTOPP_SOURCES "${CMAKE_SOURCE_DIR}/externals/cryptopp" CACHE STRING "")
add_subdirectory(cryptopp-cmake)
add_subdirectory(cryptopp-cmake EXCLUDE_FROM_ALL)
endif()
# dds-ktx
@ -142,7 +142,7 @@ endif()
# getopt
if (MSVC)
add_subdirectory(getopt)
add_subdirectory(getopt EXCLUDE_FROM_ALL)
endif()
# inih
@ -151,7 +151,7 @@ if(USE_SYSTEM_INIH)
add_library(inih INTERFACE)
target_link_libraries(inih INTERFACE inih::inih inih::inir)
else()
add_subdirectory(inih)
add_subdirectory(inih EXCLUDE_FROM_ALL)
endif()
# MicroProfile
@ -174,7 +174,7 @@ if (NOT MSVC)
endif()
# Open Source Archives
add_subdirectory(open_source_archives)
add_subdirectory(open_source_archives EXCLUDE_FROM_ALL)
# faad2
add_subdirectory(faad2 EXCLUDE_FROM_ALL)
@ -213,12 +213,12 @@ add_subdirectory(teakra EXCLUDE_FROM_ALL)
# SDL2
if (ENABLE_SDL2 AND NOT USE_SYSTEM_SDL2)
add_subdirectory(sdl2)
add_subdirectory(sdl2 EXCLUDE_FROM_ALL)
endif()
# libusb
if (ENABLE_LIBUSB AND NOT USE_SYSTEM_LIBUSB)
add_subdirectory(libusb)
add_subdirectory(libusb EXCLUDE_FROM_ALL)
set(LIBUSB_INCLUDE_DIR "" PARENT_SCOPE)
set(LIBUSB_LIBRARIES usb PARENT_SCOPE)
endif()
@ -262,7 +262,7 @@ if(USE_SYSTEM_ENET)
add_library(enet INTERFACE)
target_link_libraries(enet INTERFACE libenet::libenet)
else()
add_subdirectory(enet)
add_subdirectory(enet EXCLUDE_FROM_ALL)
target_include_directories(enet INTERFACE ./enet/include)
endif()
@ -327,12 +327,8 @@ endif()
# OpenSSL
if (USE_SYSTEM_OPENSSL)
find_package(OpenSSL 1.1)
if (OPENSSL_FOUND)
set(OPENSSL_LIBRARIES OpenSSL::SSL OpenSSL::Crypto)
endif()
endif()
if (NOT OPENSSL_FOUND)
set(OPENSSL_LIBRARIES OpenSSL::SSL OpenSSL::Crypto)
else()
# LibreSSL
set(LIBRESSL_SKIP_INSTALL ON CACHE BOOL "")
set(OPENSSLDIR "/etc/ssl/")
@ -374,7 +370,7 @@ target_compile_options(httplib INTERFACE -DCPPHTTPLIB_OPENSSL_SUPPORT)
target_link_libraries(httplib INTERFACE ${OPENSSL_LIBRARIES})
if (UNIX AND NOT APPLE)
add_subdirectory(gamemode)
add_subdirectory(gamemode EXCLUDE_FROM_ALL)
endif()
# cpp-jwt
@ -397,13 +393,13 @@ if(USE_SYSTEM_LODEPNG)
find_package(lodepng REQUIRED)
target_link_libraries(lodepng INTERFACE lodepng::lodepng)
else()
add_subdirectory(lodepng)
add_subdirectory(lodepng EXCLUDE_FROM_ALL)
endif()
# (xperia64): Only use libyuv on Android b/c of build issues on Windows and mandatory JPEG
if(ANDROID)
# libyuv
add_subdirectory(libyuv)
add_subdirectory(libyuv EXCLUDE_FROM_ALL)
target_include_directories(yuv INTERFACE ./libyuv/include)
endif()
@ -432,7 +428,7 @@ endif()
# OpenGL dependencies
if (ENABLE_OPENGL)
# Glad
add_subdirectory(glad)
add_subdirectory(glad EXCLUDE_FROM_ALL)
endif()
# Vulkan dependencies
@ -472,7 +468,7 @@ if (ENABLE_VULKAN)
set(ENABLE_CTEST OFF CACHE BOOL "")
set(ENABLE_HLSL OFF CACHE BOOL "")
set(BUILD_EXTERNAL OFF CACHE BOOL "")
add_subdirectory(glslang)
add_subdirectory(glslang EXCLUDE_FROM_ALL)
endif()
# sirit
@ -520,7 +516,7 @@ if (ENABLE_VULKAN)
# adrenotools
if (ANDROID AND "arm64" IN_LIST ARCHITECTURE)
add_subdirectory(libadrenotools)
add_subdirectory(libadrenotools EXCLUDE_FROM_ALL)
endif()
endif()

1
externals/dllwalker vendored Submodule

@ -0,0 +1 @@
Subproject commit 2f8b349c26832cae612aa7082154c0697a9cbc8e

@ -1 +1 @@
Subproject commit c41d64c6a35f6174bf4a27010aeac52a8d3bb2c6
Subproject commit e399840fc6aba5f7bc3f0633e8ff10bba0640906

View file

@ -79,7 +79,8 @@ android {
"-DENABLE_QT=0", // Don't use QT
"-DENABLE_SDL2=0", // Don't use SDL
"-DANDROID_ARM_NEON=true", // cryptopp requires Neon to work
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" // Support Android 15 16KiB page sizes
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON", // Support Android 15 16KiB page sizes
"-DENABLE_GDBSTUB=OFF", // Disable GDB stub
)
}
}

View file

@ -19,6 +19,7 @@ import android.view.Surface
import android.view.View
import android.widget.TextView
import androidx.annotation.Keep
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.fragment.app.DialogFragment
@ -317,6 +318,12 @@ object NativeLibrary {
canContinue = false
}
CoreError.ErrorCoreExceptionRaised -> {
title = emulationActivity.getString(R.string.fatal_error)
message = emulationActivity.getString(R.string.fatal_error_message)
canContinue = false
}
CoreError.ErrorUnknown -> {
title = emulationActivity.getString(R.string.fatal_error)
message = emulationActivity.getString(R.string.fatal_error_message)
@ -439,7 +446,7 @@ object NativeLibrary {
return
}
if (resultCode == EmulationErrorDialogFragment.ShutdownRequested) {
if (resultCode == CoreError.ShutdownRequested.value) {
emulationActivity.finish()
return
}
@ -458,23 +465,51 @@ object NativeLibrary {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
emulationActivity = requireActivity() as EmulationActivity
var captionId = R.string.loader_error_invalid_format
val result = requireArguments().getInt(RESULT_CODE)
if (result == ErrorLoader_ErrorEncrypted) {
captionId = R.string.loader_error_encrypted
}
if (result == ErrorArticDisconnected) {
captionId = R.string.artic_base
var coreError = CoreError.fromInt(requireArguments().getInt(RESULT_CODE))
val title: String
val message: String
when (coreError) {
CoreError.ErrorGetLoader, CoreError.ErrorLoader_ErrorInvalidFormat, CoreError.ErrorSystemMode -> {
title = getString(R.string.loader_error_invalid_format)
message = getString(R.string.loader_error_invalid_format_description)
}
CoreError.ErrorLoader_ErrorEncrypted -> {
title = getString(R.string.loader_error_encrypted)
message = getString(R.string.loader_error_encrypted_description)
}
CoreError.ErrorArticDisconnected -> {
title = getString(R.string.artic_base)
message = getString(R.string.artic_server_comm_error)
}
CoreError.ErrorN3DSApplication -> {
title = getString(R.string.loader_error_invalid_system_mode)
message = getString(R.string.loader_error_invalid_system_mode_description)
}
CoreError.ErrorLoader_ErrorPatches -> {
title = getString(R.string.loader_error_applying_patches)
message = getString(R.string.loader_error_applying_patches_description)
}
CoreError.ErrorLoader_ErrorPatchesInvalidTitle -> {
title = getString(R.string.loader_error_applying_patches)
message = getString(R.string.loader_error_patch_wrong_application)
}
else -> {
title = getString(R.string.loader_error_generic_title)
message = getString(R.string.loader_error_generic,
getString(coreError.stringRes), coreError.value)
}
}
val alert = MaterialAlertDialogBuilder(requireContext())
.setTitle(captionId)
.setTitle(title)
.setMessage(
Html.fromHtml(
if (result == ErrorArticDisconnected)
CitraApplication.appContext.resources.getString(R.string.artic_server_comm_error)
else
CitraApplication.appContext.resources.getString(R.string.redump_games),
Html.fromHtml(message,
Html.FROM_HTML_MODE_LEGACY
)
)
@ -496,21 +531,6 @@ object NativeLibrary {
const val RESULT_CODE = "resultcode"
const val Success = 0
const val ErrorNotInitialized = 1
const val ErrorGetLoader = 2
const val ErrorSystemMode = 3
const val ErrorLoader = 4
const val ErrorLoader_ErrorEncrypted = 5
const val ErrorLoader_ErrorInvalidFormat = 6
const val ErrorLoader_ErrorGBATitle = 7
const val ErrorSystemFiles = 8
const val ErrorSavestate = 9
const val ErrorArticDisconnected = 10
const val ErrorN3DSApplication = 11
const val ShutdownRequested = 12
const val ErrorUnknown = 13
fun newInstance(resultCode: Int): EmulationErrorDialogFragment {
val args = Bundle()
args.putInt(RESULT_CODE, resultCode)
@ -857,12 +877,31 @@ object NativeLibrary {
FileUtil.deleteDocument(path)
}
enum class CoreError {
ErrorSystemFiles,
ErrorSavestate,
ErrorArticDisconnected,
ErrorN3DSApplication,
ErrorUnknown
enum class CoreError(val value: Int, @StringRes val stringRes: Int) {
Success(0, R.string.core_error_success),
ErrorNotInitialized(1, R.string.core_error_not_initialized),
ErrorGetLoader(2, R.string.core_error_get_loader),
ErrorSystemMode(3, R.string.core_error_system_mode),
ErrorLoader(4, R.string.core_error_loader),
ErrorLoader_ErrorEncrypted(5, R.string.core_error_loader_encrypted),
ErrorLoader_ErrorInvalidFormat(6, R.string.core_error_loader_invalid_format),
ErrorLoader_ErrorGBATitle(7, R.string.core_error_loader_gba_title),
ErrorLoader_ErrorPatches(8, R.string.core_error_loader_error_patches),
ErrorLoader_ErrorPatchesInvalidTitle(9, R.string.core_error_loader_patches_invalid_title),
ErrorSystemFiles(10, R.string.core_error_system_files),
ErrorSavestate(11, R.string.core_error_savestate),
ErrorArticDisconnected(12, R.string.core_error_artic_disconnected),
ErrorN3DSApplication(13, R.string.core_error_n3ds_application),
ErrorCoreExceptionRaised(14, R.string.core_error_core_exception_raised),
ErrorMemoryExceptionRaised(15, R.string.core_error_memory_exception_raised),
ShutdownRequested(16, R.string.core_error_shutdown_requested),
ErrorUnknown(17, R.string.core_error_unknown);
companion object {
fun fromInt(value: Int): CoreError {
return entries.find { it.value == value } ?: ErrorUnknown
}
}
}
enum class InstallStatus {

View file

@ -18,6 +18,7 @@ object SettingKeys {
external fun enable_required_online_lle_modules(): String
external fun use_virtual_sd(): String
external fun compress_cia_installs(): String
external fun async_fs_operations(): String
external fun region_value(): String
external fun init_clock(): String
external fun init_time(): String
@ -45,6 +46,7 @@ object SettingKeys {
external fun texture_filter(): String
external fun texture_sampling(): String
external fun delay_game_render_thread_us(): String
external fun simulate_3ds_gpu_timings(): String
external fun layout_option(): String
external fun swap_screen(): String
external fun upright_screen(): String
@ -92,6 +94,7 @@ object SettingKeys {
external fun audio_emulation(): String
external fun enable_audio_stretching(): String
external fun enable_realtime_audio(): String
external fun simulate_headphones_plugged(): String
external fun volume(): String
external fun output_type(): String
external fun output_device(): String

View file

@ -44,6 +44,7 @@ enum class BooleanSetting(
PRELOAD_TEXTURES(SettingKeys.preload_textures(), Settings.SECTION_UTILITY, false),
ENABLE_AUDIO_STRETCHING(SettingKeys.enable_audio_stretching(), Settings.SECTION_AUDIO, true),
ENABLE_REALTIME_AUDIO(SettingKeys.enable_realtime_audio(), Settings.SECTION_AUDIO, false),
SIMULATE_HEADPHONES_PLUGGED(SettingKeys.simulate_headphones_plugged(), Settings.SECTION_AUDIO, false),
CPU_JIT(SettingKeys.use_cpu_jit(), Settings.SECTION_CORE, true),
HW_SHADER(SettingKeys.use_hw_shader(), Settings.SECTION_RENDERER, true),
SHADER_JIT(SettingKeys.use_shader_jit(), Settings.SECTION_RENDERER, true),
@ -54,9 +55,11 @@ enum class BooleanSetting(
USE_ARTIC_BASE_CONTROLLER(SettingKeys.use_artic_base_controller(), Settings.SECTION_CONTROLS, false),
UPRIGHT_SCREEN(SettingKeys.upright_screen(), Settings.SECTION_LAYOUT, false),
COMPRESS_INSTALLED_CIA_CONTENT(SettingKeys.compress_cia_installs(), Settings.SECTION_STORAGE, false),
ASYNC_FS_OPERATIONS(SettingKeys.async_fs_operations(), Settings.SECTION_STORAGE, true),
ANDROID_HIDE_IMAGES(SettingKeys.android_hide_images(), Settings.SECTION_MISC, false),
APPLY_REGION_FREE_PATCH(SettingKeys.apply_region_free_patch(), Settings.SECTION_SYSTEM, true),
USE_INTEGER_SCALING(SettingKeys.use_integer_scaling(), Settings.SECTION_RENDERER, false);
USE_INTEGER_SCALING(SettingKeys.use_integer_scaling(), Settings.SECTION_RENDERER, false),
SIMULATE_3DS_GPU_TIMINGS(SettingKeys.simulate_3ds_gpu_timings(), Settings.SECTION_RENDERER, true);
override var boolean: Boolean = defaultValue
@ -91,6 +94,7 @@ enum class BooleanSetting(
SHADERS_ACCURATE_MUL,
USE_ARTIC_BASE_CONTROLLER,
COMPRESS_INSTALLED_CIA_CONTENT,
ASYNC_FS_OPERATIONS,
ANDROID_HIDE_IMAGES,
PERF_OVERLAY_ENABLE, // Works in overlay options, but not from the settings menu
APPLY_REGION_FREE_PATCH

View file

@ -599,6 +599,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
BooleanSetting.COMPRESS_INSTALLED_CIA_CONTENT.defaultValue
)
)
add(
SwitchSetting(
BooleanSetting.ASYNC_FS_OPERATIONS,
R.string.async_fs_operations,
R.string.async_fs_operations_description,
BooleanSetting.ASYNC_FS_OPERATIONS.key,
BooleanSetting.ASYNC_FS_OPERATIONS.defaultValue
)
)
}
}
@ -1719,6 +1728,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
BooleanSetting.ENABLE_REALTIME_AUDIO.defaultValue
)
)
add(
SwitchSetting(
BooleanSetting.SIMULATE_HEADPHONES_PLUGGED,
R.string.simulate_headphones_plugged,
R.string.simulate_headphones_plugged_description,
BooleanSetting.SIMULATE_HEADPHONES_PLUGGED.key,
BooleanSetting.SIMULATE_HEADPHONES_PLUGGED.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.AUDIO_INPUT_TYPE,
@ -1805,6 +1823,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
BooleanSetting.VSYNC.defaultValue
)
)
add(
SwitchSetting(
BooleanSetting.SIMULATE_3DS_GPU_TIMINGS,
R.string.simulate_3ds_gpu_timings,
R.string.simulate_3ds_gpu_timings_description,
BooleanSetting.SIMULATE_3DS_GPU_TIMINGS.key,
BooleanSetting.SIMULATE_3DS_GPU_TIMINGS.defaultValue
)
)
add(
SwitchSetting(
BooleanSetting.DEBUG_RENDERER,

View file

@ -179,6 +179,7 @@ void Config::ReadValues() {
ReadSetting("Renderer", Settings::values.bg_blue);
ReadSetting("Renderer", Settings::values.custom_second_layer_opacity);
ReadSetting("Renderer", Settings::values.delay_game_render_thread_us);
ReadSetting("Renderer", Settings::values.simulate_3ds_gpu_timings);
ReadSetting("Renderer", Settings::values.disable_right_eye_render);
ReadSetting("Renderer", Settings::values.swap_eyes_3d);
ReadSetting("Renderer", Settings::values.render_3d_which_display);
@ -231,6 +232,7 @@ void Config::ReadValues() {
// Storage
ReadSetting("Storage", Settings::values.compress_cia_installs);
ReadSetting("Storage", Settings::values.async_fs_operations);
// Utility
ReadSetting("Utility", Settings::values.dump_textures);
@ -242,6 +244,7 @@ void Config::ReadValues() {
ReadSetting("Audio", Settings::values.audio_emulation);
ReadSetting("Audio", Settings::values.enable_audio_stretching);
ReadSetting("Audio", Settings::values.enable_realtime_audio);
ReadSetting("Audio", Settings::values.simulate_headphones_plugged);
ReadSetting("Audio", Settings::values.volume);
ReadSetting("Audio", Settings::values.output_type);
ReadSetting("Audio", Settings::values.output_device);
@ -316,6 +319,7 @@ void Config::ReadValues() {
ReadSetting("Debugging", Settings::values.instant_debug_log);
ReadSetting("Debugging", Settings::values.enable_rpc_server);
ReadSetting("Debugging", Settings::values.toggle_unique_data_console_type);
ReadSetting("Debugging", Settings::values.break_on_unmapped_memory_access);
for (const auto& service_module : Service::service_module_map) {
bool use_lle =

View file

@ -35,7 +35,10 @@ constexpr std::array android_config_omitted_keys = {
Keys::audio_encoder,
Keys::audio_encoder_options,
Keys::audio_bitrate,
Keys::last_artic_base_addr, // On Android, this value is stored as a "preference"
Keys::last_artic_base_addr, // On Android, this value is stored as a "preference"
Keys::break_on_unmapped_memory_access, // Does nothing as the error is ignored
Keys::use_gdbstub, // GDB functionality disabled by deafult on Android
Keys::gdbstub_port,
};
// clang-format off
@ -196,6 +199,10 @@ static const char* android_config_default_file_content = (BOOST_HANA_STRING(R"(
# Set to 0 for no delay, only useful in dynamic-fps games to simulate GPU delay.
)") DECLARE_KEY(delay_game_render_thread_us) BOOST_HANA_STRING(R"(
# Delays GPU completion events based on measurements taken from real hardware
# 0: No delay, 1 (default): Enable delay
)") DECLARE_KEY(simulate_3ds_gpu_timings) BOOST_HANA_STRING(R"(
# Disables rendering the right eye image
# Greatly improves performance in some games, but can cause flickering in others.
# 0 : Enable right eye rendering, 1: Disable right eye rendering
@ -271,6 +278,10 @@ static const char* android_config_default_file_content = (BOOST_HANA_STRING(R"(
# 0 (default): Do not compress, 1: Compress
)") DECLARE_KEY(compress_cia_installs) BOOST_HANA_STRING(R"(
# Whether to enable async filesystem operations
# 0: Disabled, 1 (default): Enabled
)") DECLARE_KEY(async_fs_operations) BOOST_HANA_STRING(R"(
# Position of the performance overlay
# 0: Top Left
# 1: Center Top
@ -404,6 +415,10 @@ static const char* android_config_default_file_content = (BOOST_HANA_STRING(R"(
# 0 (default): No, 1: Yes
)") DECLARE_KEY(enable_realtime_audio) BOOST_HANA_STRING(R"(
# Simulates whether headphones are plugged in to the emulated 3DS system
# 0 (default): No, 1: Yes
)") DECLARE_KEY(simulate_headphones_plugged) BOOST_HANA_STRING(R"(
# Output volume.
# 1.0 (default): 100%, 0.0; mute
)") DECLARE_KEY(volume) BOOST_HANA_STRING(R"(
@ -527,10 +542,6 @@ static const char* android_config_default_file_content = (BOOST_HANA_STRING(R"(
# 0 (default): Off, 1: On
)") DECLARE_KEY(renderer_debug) BOOST_HANA_STRING(R"(
# Port for listening to GDB connections.
)") DECLARE_KEY(use_gdbstub) BOOST_HANA_STRING(R"(
)") DECLARE_KEY(gdbstub_port) BOOST_HANA_STRING(R"(
# Flush log output on every message
# Immediately commits the debug log to file. Use this if Azahar crashes and the log output is being cut.
)") DECLARE_KEY(instant_debug_log) BOOST_HANA_STRING(R"(

View file

@ -238,6 +238,8 @@
<string name="storage">Storage</string>
<string name="compress_cia_installs">Compress installed CIA content</string>
<string name="compress_cia_installs_description">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.</string>
<string name="async_fs_operations">Asynchronous filesystem operations</string>
<string name="async_fs_operations_description">Makes emulated filesystem accesses asynchronous. Greatly reduces filesystem related stutter, but may slightly increase load times.</string>
<!-- Camera settings strings -->
<string name="inner_camera">Inner Camera</string>
@ -269,6 +271,8 @@
<string name="texture_filter_description">Enhances the visuals of applications by applying a filter to textures. The supported filters are Anime4K Ultrafast, Bicubic, ScaleForce, xBRZ freescale, and MMPX.</string>
<string name="delay_render_thread">Delay Game Render Thread</string>
<string name="delay_render_thread_description">Delay the game render thread when it submits data to the GPU. Helps with performance issues in the (very few) applications with dynamic framerates.</string>
<string name="simulate_3ds_gpu_timings">Simulate 3DS GPU Timings</string>
<string name="simulate_3ds_gpu_timings_description">Delays GPU completion events based on measurements taken from real hardware, so that games have more realistic GPU time measurements. Helps stabilize dynamic FPS games. Disabling this feature may improve performance in some rare cases at the cost of stability.</string>
<string name="advanced">Advanced</string>
<string name="texture_sampling_name">Texture Sampling</string>
<string name="texture_sampling_description">Overrides the sampling filter used by games. This can be useful in certain cases with poorly behaved games when upscaling. If unsure, set this to Game Controlled.</string>
@ -341,6 +345,8 @@
<string name="audio_stretch_description">Stretches audio to reduce stuttering. When enabled, increases audio latency and slightly reduces performance.</string>
<string name="realtime_audio">Enable Realtime Audio</string>
<string name="realtime_audio_description">Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the game framerate is low. May cause audio desync issues.</string>
<string name="simulate_headphones_plugged">Simulate Headphones Plugged In</string>
<string name="simulate_headphones_plugged_description">Simulates whether headphones are plugged in to the emulated 3DS system.</string>
<string name="audio_input_type">Audio Input Device</string>
<string name="sound_output_mode">Sound Output Mode</string>
@ -396,7 +402,6 @@
<string name="learn_more">Learn More</string>
<string name="close">Close</string>
<string name="reset_to_default">Reset to Default</string>
<string name="redump_games"><![CDATA[Please follow the guides to redump your <a href="https://web.archive.org/web/20240304210021/https://citra-emu.org/wiki/dumping-game-cartridges/">game cartridges</a> or <a href="https://web.archive.org/web/20240304210011/https://citra-emu.org/wiki/dumping-installed-titles/">installed titles</a>.]]></string>
<string name="option_default">Default</string>
<string name="none">None</string>
<string name="auto">Auto</string>
@ -439,11 +444,39 @@
<string name="preferences_layout">Layout</string>
<!-- ROM loading errors -->
<string name="loader_error_generic_title">Error loading application</string>
<string name="loader_error_invalid_format">Invalid application format</string>
<string name="loader_error_invalid_format_description"><![CDATA[The application file format not supported.<br>Please make sure you are using one of the compatible file formats:<ul><li>Cartridge images: <b>.cci/.zcci/.3ds</b></li><li>Installable archives: <b>.cia/.zcia</b></li><li>Homebrew titles: <b>.3dsx/.z3dsx</b></li><li>NCCH containers: <b>.cxi/.zcxi/.app</b></li><li>ELF files: <b>.elf/.axf</b></li></ul>]]></string>
<string name="loader_error_invalid_system_mode">Invalid system mode</string>
<string name="loader_error_invalid_system_mode_description">New 3DS exclusive applications cannot be loaded without enabling the New 3DS mode.</string>
<string name="loader_error_applying_patches">Error applying patches</string>
<string name="loader_error_applying_patches_description">A generic error occurred while applying a patch to the application. Please check the log for more details.</string>
<string name="loader_error_patch_wrong_application">Failed to apply a patch because it is designed for a different application. Please make sure you are using the patches for the right application, region and version.</string>
<string name="loader_error_encrypted">Your ROM is Encrypted</string>
<string name="loader_error_invalid_format">Invalid ROM format</string>
<string name="loader_error_encrypted_description"><![CDATA[Azahar does not support encrypted applications. Read our <a href="https://azahar-emu.org/blog/game-loading-changes/">blog post</a> for more information.]]></string>
<string name="loader_error_file_not_found">ROM file does not exist</string>
<string name="no_game_present">No bootable game present!</string>
<string name="loader_error_generic">An error occurred while loading ROM: \"%s (%d)\"</string>
<string name="core_error_success">Success</string>
<string name="core_error_not_initialized">Not initialized</string>
<string name="core_error_get_loader">Loader for file not found, incompatible file type</string>
<string name="core_error_system_mode">Failed to parse file</string>
<string name="core_error_loader">Generic loader error</string>
<string name="core_error_loader_encrypted">Encrypted file</string>
<string name="core_error_loader_invalid_format">Corrupted file</string>
<string name="core_error_loader_gba_title">File is GBA title</string>
<string name="core_error_loader_error_patches">Error applying patches</string>
<string name="core_error_loader_patches_invalid_title">Patches are for a different application</string>
<string name="core_error_system_files">Missing system files</string>
<string name="core_error_savestate">Savestate failed</string>
<string name="core_error_artic_disconnected">Artic Base disconnected</string>
<string name="core_error_n3ds_application">File is New 3DS application</string>
<string name="core_error_core_exception_raised">Core exception raised</string>
<string name="core_error_memory_exception_raised">Memory exception raised</string>
<string name="core_error_shutdown_requested">Shutdown requested</string>
<string name="core_error_unknown">Unknown error</string>
<!-- Emulation Menu -->
<string name="emulation_menu_help">Press Back to access the menu.</string>
<string name="emulation_save_state">Save State</string>

View file

@ -1,4 +1,4 @@
add_library(audio_core STATIC
add_library(audio_core STATIC EXCLUDE_FROM_ALL
audio_types.h
codec.cpp
codec.h

View file

@ -48,6 +48,26 @@ if (APPLE)
endif()
endif()
if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux" AND MINGW)
# TODO: Do this for all executables, not just citra_meta
# TODO: Don't hardcode MXE directory to root?
set(dllwalker_command "${CMAKE_SOURCE_DIR}/externals/dllwalker/dllwalker.rb"
$<TARGET_FILE:citra_meta>
/mxe/usr/x86_64-w64-mingw32.shared/bin/
/mxe/usr/x86_64-w64-mingw32.shared/qt6/bin/)
set(dll_list_filename "${CMAKE_CURRENT_BINARY_DIR}/required_dlls_list.txt")
add_custom_command(TARGET citra_meta POST_BUILD
COMMAND ${dllwalker_command} > ${dll_list_filename}
)
add_custom_command(TARGET citra_meta POST_BUILD
COMMAND cat ${dll_list_filename} | xargs -I {} ${CMAKE_COMMAND} -E copy {} $<TARGET_FILE_DIR:citra_meta>
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:citra_meta>/plugins/"
COMMAND ${CMAKE_COMMAND} -E copy_directory /mxe/usr/x86_64-w64-mingw32.shared/qt6/plugins/ "$<TARGET_FILE_DIR:citra_meta>/plugins/"
COMMAND_EXPAND_LISTS
DEPENDS ${dll_list_filename}
)
endif()
target_link_libraries(citra_meta PRIVATE citra_common fmt)
if (ENABLE_QT)

View file

@ -855,10 +855,11 @@ void GMainWindow::InitializeHotkeys() {
// QAction Hotkeys
const auto link_action_shortcut = [&](QAction* action, const QString& action_name,
const bool primary_only = false) {
const bool primary_only = false,
const bool auto_repeat = false) {
static const QString main_window = QStringLiteral("Main Window");
action->setShortcut(hotkey_registry.GetKeySequence(main_window, action_name));
action->setAutoRepeat(false);
action->setAutoRepeat(auto_repeat);
this->addAction(action);
if (!primary_only)
secondary_window->addAction(action);
@ -875,6 +876,9 @@ void GMainWindow::InitializeHotkeys() {
link_action_shortcut(ui->action_Show_Status_Bar, QStringLiteral("Toggle Status Bar"));
link_action_shortcut(ui->action_Fullscreen, fullscreen, true);
link_action_shortcut(ui->action_Capture_Screenshot, QStringLiteral("Capture Screenshot"));
link_action_shortcut(ui->action_Debug_Pause, QStringLiteral("Debug Pause"));
link_action_shortcut(ui->action_Debug_Resume, QStringLiteral("Debug Resume"));
link_action_shortcut(ui->action_Debug_Step, QStringLiteral("Debug Step"), false, true);
link_action_shortcut(ui->action_Screen_Layout_Swap_Screens, QStringLiteral("Swap Screens"));
link_action_shortcut(ui->action_Screen_Layout_Upright_Screens,
QStringLiteral("Rotate Screens Upright"));
@ -1189,6 +1193,23 @@ void GMainWindow::ConnectMenuEvents() {
connect_menu(ui->action_Capture_Screenshot, &GMainWindow::OnCaptureScreenshot);
connect_menu(ui->action_Dump_Video, &GMainWindow::OnDumpVideo);
// Tools debug
connect_menu(ui->action_Debug_Pause, [this] {
if (emu_thread) {
emu_thread->SetRunning(false);
}
});
connect_menu(ui->action_Debug_Resume, [this] {
if (emu_thread) {
emu_thread->SetRunning(true);
}
});
connect_menu(ui->action_Debug_Step, [this] {
if (emu_thread) {
emu_thread->ExecStep();
}
});
// Tools
connect_menu(ui->action_Compress_ROM_File, &GMainWindow::OnCompressFile);
connect_menu(ui->action_Decompress_ROM_File, &GMainWindow::OnDecompressFile);
@ -1345,61 +1366,39 @@ bool GMainWindow::LoadROM(const QString& filename) {
system.Load(*render_window, filename.toStdString(), secondary_window)};
if (result != Core::System::ResultStatus::Success) {
QString invalid_format = tr("Invalid application format");
QString invalid_format_description =
tr("The application file format not supported.<br>Please make sure you are using one "
"of the compatible file formats:<ul><li>Cartridge images: "
"<b>.cci/.zcci/.3ds</b></li><li>Installable archives: "
"<b>.cia/.zcia</b></li><li>Homebrew titles: <b>.3dsx/.z3dsx</b></li><li>NCCH "
"containers: <b>.cxi/.zcxi/.app</b></li><li>ELF files: <b>.elf/.axf</b></li></ul>");
switch (result) {
case Core::System::ResultStatus::ErrorGetLoader:
LOG_CRITICAL(Frontend, "Failed to obtain loader for {}", filename.toStdString());
QMessageBox::critical(
this, tr("Invalid App Format"),
tr("Your app format is not supported.<br/>Please follow the guides to redump your "
"<a "
"href='https://web.archive.org/web/20240304210021/https://citra-emu.org/wiki/"
"dumping-game-cartridges/'>game "
"cartridges</a> or "
"<a "
"href='https://web.archive.org/web/20240304210011/https://citra-emu.org/wiki/"
"dumping-installed-titles/'>installed "
"titles</a>."));
QMessageBox::critical(this, invalid_format, invalid_format_description);
break;
case Core::System::ResultStatus::ErrorSystemMode:
LOG_CRITICAL(Frontend, "Failed to load App!");
QMessageBox::critical(
this, tr("App Corrupted"),
tr("Your app is corrupted. <br/>Please follow the guides to redump your "
"<a "
"href='https://web.archive.org/web/20240304210021/https://citra-emu.org/wiki/"
"dumping-game-cartridges/'>game "
"cartridges</a> or "
"<a "
"href='https://web.archive.org/web/20240304210011/https://citra-emu.org/wiki/"
"dumping-installed-titles/'>installed "
"titles</a>."));
LOG_CRITICAL(Frontend, "Failed to load application!");
QMessageBox::critical(this, invalid_format, invalid_format_description);
break;
case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: {
QMessageBox::critical(this, tr("App Encrypted"),
tr("Your app is encrypted. <br/>"
QMessageBox::critical(this, tr("Encrypted application"),
tr("Encrypted applications are not supported.<br/>"
"<a "
"href='https://azahar-emu.org/blog/game-loading-changes/'>"
"Please check our blog for more info.</a>"));
break;
}
case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat:
QMessageBox::critical(
this, tr("Invalid App Format"),
tr("Your app format is not supported.<br/>Please follow the guides to redump your "
"<a "
"href='https://web.archive.org/web/20240304210021/https://citra-emu.org/wiki/"
"dumping-game-cartridges/'>game "
"cartridges</a> or "
"<a "
"href='https://web.archive.org/web/20240304210011/https://citra-emu.org/wiki/"
"dumping-installed-titles/'>installed "
"titles</a>."));
QMessageBox::critical(this, invalid_format, invalid_format_description);
break;
case Core::System::ResultStatus::ErrorLoader_ErrorGbaTitle:
QMessageBox::critical(this, tr("Unsupported App"),
QMessageBox::critical(this, tr("Unsupported application"),
tr("GBA Virtual Console is not supported by Azahar."));
break;
@ -1416,10 +1415,27 @@ bool GMainWindow::LoadROM(const QString& filename) {
tr("New 3DS exclusive applications cannot be loaded without "
"enabling the New 3DS mode."));
break;
case Core::System::ResultStatus::ErrorLoader:
QMessageBox::critical(this, tr("Generic load error"),
tr("An generic load error occurred while loading the "
"application.<br/>Please check the log for more details."));
break;
case Core::System::ResultStatus::ErrorLoader_ErrorPatches:
QMessageBox::critical(this, tr("Error applying patches"),
tr("A generic error occurred while applying a patch to the "
"application.<br/>Please check the log for more details."));
break;
case Core::System::ResultStatus::ErrorLoader_ErrorPatchesInvalidTitle:
QMessageBox::critical(
this, tr("Error applying patches"),
tr("Failed to apply a patch because it is designed for a different "
"application.<br/>Please make sure you are using the patches for "
"the right application, region and version."));
break;
default:
QMessageBox::critical(
this, tr("Error while loading App!"),
tr("An unknown error occurred. Please see the log for more details."));
this, tr("Error while loading application"),
tr("An unknown error occurred.<br/>Please see the log for more details."));
break;
}
return false;
@ -1478,7 +1494,8 @@ void GMainWindow::BootGame(const QString& filename) {
auto loader = Loader::GetLoader(path);
u64 title_id{0};
Loader::ResultStatus res = loader->ReadProgramId(title_id);
Loader::ResultStatus res =
loader ? loader->ReadProgramId(title_id) : Loader::ResultStatus::Error;
if (Loader::ResultStatus::Success == res) {
// Load per game settings
@ -1491,7 +1508,7 @@ void GMainWindow::BootGame(const QString& filename) {
// Artic Server cannot accept a client multiple times, so multiple loaders are not
// possible. Instead register the app loader early and do not create it again on system load.
if (!loader->SupportsMultipleInstancesForSameFile()) {
if (loader && !loader->SupportsMultipleInstancesForSameFile()) {
system.RegisterAppLoaderEarly(loader);
}
@ -3880,6 +3897,18 @@ void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string det
.c_str());
error_severity_icon = QMessageBox::Icon::Critical;
can_continue = false;
} else if (result == Core::System::ResultStatus::ErrorCoreExceptionRaised) {
title = tr("An exception occurred");
message = tr("An exception occurred while executing the emulated application.\n\n");
message += QString::fromStdString(details);
error_severity_icon = QMessageBox::Icon::Critical;
can_continue = false;
} else if (result == Core::System::ResultStatus::ErrorMemoryExceptionRaised) {
title = tr("An invalid memory access occurred");
message =
tr("An invalid memory access occurred while executing the emulated application.\n\n");
message += QString::fromStdString(details);
error_severity_icon = QMessageBox::Icon::Critical;
} else {
title = tr("Fatal Error");
message = tr("A fatal error occurred. "

View file

@ -57,12 +57,15 @@ const std::array<std::array<int, 5>, Settings::NativeAnalog::NumAnalogs> QtConfi
// This must be in alphabetical order according to action name as it must have the same order as
// UISetting::values.shortcuts, which is alphabetically ordered.
// clang-format off
const std::array<UISettings::Shortcut, 38> QtConfig::default_hotkeys {{
const std::array<UISettings::Shortcut, 41> QtConfig::default_hotkeys {{
{QStringLiteral("Advance Frame"), QStringLiteral("Main Window"), {QStringLiteral(""), Qt::ApplicationShortcut}},
{QStringLiteral("Audio Mute/Unmute"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+M"), Qt::WindowShortcut}},
{QStringLiteral("Audio Volume Down"), QStringLiteral("Main Window"), {QStringLiteral(""), Qt::WindowShortcut}},
{QStringLiteral("Audio Volume Up"), QStringLiteral("Main Window"), {QStringLiteral(""), Qt::WindowShortcut}},
{QStringLiteral("Capture Screenshot"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+P"), Qt::WidgetWithChildrenShortcut}},
{QStringLiteral("Debug Pause"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F4"),Qt::WidgetWithChildrenShortcut}},
{QStringLiteral("Debug Resume"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F5"),Qt::WidgetWithChildrenShortcut}},
{QStringLiteral("Debug Step"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F6"),Qt::WidgetWithChildrenShortcut}},
{QStringLiteral("Continue/Pause Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F4"), Qt::WindowShortcut}},
{QStringLiteral("Decrease 3D Factor"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+-"), Qt::ApplicationShortcut}},
{QStringLiteral("Decrease Speed Limit"), QStringLiteral("Main Window"), {QStringLiteral("-"), Qt::ApplicationShortcut}},
@ -287,6 +290,7 @@ void QtConfig::ReadAudioValues() {
ReadGlobalSetting(Settings::values.audio_emulation);
ReadGlobalSetting(Settings::values.enable_audio_stretching);
ReadGlobalSetting(Settings::values.enable_realtime_audio);
ReadGlobalSetting(Settings::values.simulate_headphones_plugged);
ReadGlobalSetting(Settings::values.volume);
if (global) {
@ -483,6 +487,7 @@ void QtConfig::ReadDataStorageValues() {
ReadBasicSetting(Settings::values.use_virtual_sd);
ReadBasicSetting(Settings::values.use_custom_storage);
ReadBasicSetting(Settings::values.compress_cia_installs);
ReadBasicSetting(Settings::values.async_fs_operations);
const std::string nand_dir =
ReadSetting(Settings::QKeys::nand_directory, QStringLiteral("")).toString().toStdString();
@ -510,6 +515,7 @@ void QtConfig::ReadDebuggingValues() {
ReadBasicSetting(Settings::values.instant_debug_log);
ReadBasicSetting(Settings::values.enable_rpc_server);
ReadBasicSetting(Settings::values.toggle_unique_data_console_type);
ReadBasicSetting(Settings::values.break_on_unmapped_memory_access);
qt_config->beginGroup(QStringLiteral("LLE"));
for (const auto& service_module : Service::service_module_map) {
@ -722,6 +728,8 @@ void QtConfig::ReadRendererValues() {
ReadGlobalSetting(Settings::values.delay_game_render_thread_us);
ReadGlobalSetting(Settings::values.disable_right_eye_render);
ReadGlobalSetting(Settings::values.simulate_3ds_gpu_timings);
if (global) {
ReadBasicSetting(Settings::values.use_shader_jit);
}
@ -937,6 +945,7 @@ void QtConfig::SaveAudioValues() {
WriteGlobalSetting(Settings::values.audio_emulation);
WriteGlobalSetting(Settings::values.enable_audio_stretching);
WriteGlobalSetting(Settings::values.enable_realtime_audio);
WriteGlobalSetting(Settings::values.simulate_headphones_plugged);
WriteGlobalSetting(Settings::values.volume);
if (global) {
@ -1073,6 +1082,7 @@ void QtConfig::SaveDataStorageValues() {
WriteBasicSetting(Settings::values.use_virtual_sd);
WriteBasicSetting(Settings::values.use_custom_storage);
WriteBasicSetting(Settings::values.compress_cia_installs);
WriteBasicSetting(Settings::values.async_fs_operations);
WriteSetting(Settings::QKeys::nand_directory,
QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir)),
QStringLiteral(""));
@ -1094,6 +1104,7 @@ void QtConfig::SaveDebuggingValues() {
WriteBasicSetting(Settings::values.instant_debug_log);
WriteBasicSetting(Settings::values.enable_rpc_server);
WriteBasicSetting(Settings::values.toggle_unique_data_console_type);
WriteBasicSetting(Settings::values.break_on_unmapped_memory_access);
qt_config->beginGroup(QStringLiteral("LLE"));
for (const auto& service_module : Settings::values.lle_modules) {
@ -1266,6 +1277,8 @@ void QtConfig::SaveRendererValues() {
WriteGlobalSetting(Settings::values.delay_game_render_thread_us);
WriteGlobalSetting(Settings::values.disable_right_eye_render);
WriteGlobalSetting(Settings::values.simulate_3ds_gpu_timings);
if (global) {
WriteSetting(Settings::QKeys::use_shader_jit, Settings::values.use_shader_jit.GetValue(),
true);

View file

@ -26,7 +26,7 @@ public:
static const std::array<int, Settings::NativeButton::NumButtons> default_buttons;
static const std::array<std::array<int, 5>, Settings::NativeAnalog::NumAnalogs> default_analogs;
static const std::array<UISettings::Shortcut, 38> default_hotkeys;
static const std::array<UISettings::Shortcut, 41> default_hotkeys;
private:
void Initialize(const std::string& config_name);

View file

@ -1,4 +1,4 @@
// Copyright 2016 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -67,6 +67,8 @@ void ConfigureAudio::SetConfiguration() {
ui->toggle_audio_stretching->setChecked(Settings::values.enable_audio_stretching.GetValue());
ui->toggle_realtime_audio->setChecked(Settings::values.enable_realtime_audio.GetValue());
ui->simulate_headphones_plugged->setChecked(
Settings::values.simulate_headphones_plugged.GetValue());
SetHleFeaturesEnabled();
const s32 volume =
@ -175,6 +177,9 @@ void ConfigureAudio::ApplyConfiguration() {
&Settings::values.volume, ui->volume_combo_box, [this](s32) {
return static_cast<float>(ui->volume_slider->value()) / ui->volume_slider->maximum();
});
ConfigurationShared::ApplyPerGameSetting(&Settings::values.simulate_headphones_plugged,
ui->simulate_headphones_plugged,
simulate_headphones_plugged);
if (Settings::IsConfiguringGlobal()) {
Settings::values.output_type =
@ -252,4 +257,7 @@ void ConfigureAudio::SetupPerGameUI() {
ConfigurationShared::SetColoredTristate(ui->toggle_realtime_audio,
Settings::values.enable_realtime_audio, realtime_audio);
ConfigurationShared::SetColoredTristate(ui->simulate_headphones_plugged,
Settings::values.simulate_headphones_plugged,
simulate_headphones_plugged);
}

View file

@ -1,4 +1,4 @@
// Copyright 2016 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -41,5 +41,6 @@ private:
ConfigurationShared::CheckState audio_stretching;
ConfigurationShared::CheckState realtime_audio;
ConfigurationShared::CheckState simulate_headphones_plugged;
std::unique_ptr<Ui::ConfigureAudio> ui;
};

View file

@ -85,26 +85,6 @@
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="toggle_audio_stretching">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Enable audio stretching</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="toggle_realtime_audio">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Enable realtime audio</string>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="volume_layout" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_2">
@ -192,6 +172,36 @@
</layout>
</widget>
</item>
<item>
<widget class="QCheckBox" name="toggle_audio_stretching">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Enable audio stretching</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="toggle_realtime_audio">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Enable realtime audio</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="simulate_headphones_plugged">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Simulates whether headphones are plugged in to the emulated 3DS system.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Simulate headphones plugged in</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View file

@ -12,6 +12,7 @@
#include "common/file_util.h"
#include "common/logging/backend.h"
#include "common/settings.h"
#include "core/core.h"
#include "ui_configure_debug.h"
#ifdef ENABLE_VULKAN
#include "video_core/renderer_vulkan/vk_instance.h"
@ -33,6 +34,17 @@ ConfigureDebug::ConfigureDebug(bool is_powered_on_, QWidget* parent)
ui->setupUi(this);
SetConfiguration();
connect(ui->toggle_gdbstub, &QCheckBox::clicked,
[this](bool checked) { ui->debug_next_process->setEnabled(checked); });
connect(ui->debug_next_process, &QCheckBox::clicked, [](bool checked) {
if (checked) {
Core::System::GetInstance().SetDebugNextProcessFlag();
} else {
Core::System::GetInstance().ClearDebugNextProcessFlag();
}
});
connect(ui->open_log_button, &QPushButton::clicked, []() {
QString path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::LogDir));
QDesktopServices::openUrl(QUrl::fromLocalFile(path));
@ -87,6 +99,10 @@ ConfigureDebug::ConfigureDebug(bool is_powered_on_, QWidget* parent)
ui->clock_speed_label->setVisible(Settings::IsConfiguringGlobal());
ui->clock_speed_combo->setVisible(!Settings::IsConfiguringGlobal());
#ifndef ENABLE_GDBSTUB
ui->gdb_groupbox->setVisible(false);
#endif
SetupPerGameUI();
}
@ -94,6 +110,9 @@ ConfigureDebug::~ConfigureDebug() = default;
void ConfigureDebug::SetConfiguration() {
ui->toggle_gdbstub->setChecked(Settings::values.use_gdbstub.GetValue());
if (!ui->toggle_gdbstub->isChecked()) {
ui->debug_next_process->setEnabled(false);
}
ui->gdbport_spinbox->setEnabled(Settings::values.use_gdbstub.GetValue());
ui->gdbport_spinbox->setValue(Settings::values.gdbstub_port.GetValue());
ui->toggle_console->setEnabled(!is_powered_on);
@ -112,6 +131,8 @@ void ConfigureDebug::SetConfiguration() {
#endif // !ENABLE_SCRIPTING
ui->toggle_unique_data_console_type->setChecked(
Settings::values.toggle_unique_data_console_type.GetValue());
ui->break_on_unmapped_memory_access->setChecked(
Settings::values.break_on_unmapped_memory_access.GetValue());
ui->toggle_renderer_debug->setChecked(Settings::values.renderer_debug.GetValue());
ui->toggle_dump_command_buffers->setChecked(Settings::values.dump_command_buffers.GetValue());
@ -133,6 +154,10 @@ void ConfigureDebug::SetConfiguration() {
ui->clock_display_label->setText(
QStringLiteral("%1%").arg(Settings::values.cpu_clock_percentage.GetValue()));
ui->instant_debug_log->setChecked(Settings::values.instant_debug_log.GetValue());
if (Core::System::GetInstance().GetDebugNextProcessFlag()) {
ui->debug_next_process->setChecked(true);
}
}
void ConfigureDebug::ApplyConfiguration() {
@ -153,6 +178,8 @@ void ConfigureDebug::ApplyConfiguration() {
Settings::values.enable_rpc_server = ui->enable_rpc_server->isChecked();
Settings::values.toggle_unique_data_console_type =
ui->toggle_unique_data_console_type->isChecked();
Settings::values.break_on_unmapped_memory_access =
ui->break_on_unmapped_memory_access->isChecked();
Settings::values.renderer_debug = ui->toggle_renderer_debug->isChecked();
Settings::values.dump_command_buffers = ui->toggle_dump_command_buffers->isChecked();
Settings::values.instant_debug_log = ui->instant_debug_log->isChecked();
@ -174,10 +201,11 @@ void ConfigureDebug::SetupPerGameUI() {
ConfigurationShared::SetHighlight(ui->clock_speed_widget, index == 1);
});
ui->groupBox->setVisible(false);
ui->gdb_groupbox->setVisible(false);
ui->groupBox_2->setVisible(false);
ui->enable_rpc_server->setVisible(false);
ui->toggle_unique_data_console_type->setVisible(false);
ui->break_on_unmapped_memory_access->setVisible(false);
ui->toggle_cpu_jit->setVisible(false);
}

View file

@ -17,7 +17,7 @@
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox">
<widget class="QGroupBox" name="gdb_groupbox">
<property name="title">
<string>GDB</string>
</property>
@ -60,6 +60,13 @@
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="debug_next_process">
<property name="text">
<string>Pause next non-sysmodule process at start</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -299,6 +306,16 @@
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="break_on_unmapped_memory_access">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Pauses emulation and shows an error message if an unmapped memory access is detected.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Break on unmapped memory access</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View file

@ -154,6 +154,7 @@ void ConfigureGraphics::SetConfiguration() {
ui->toggle_display_refresh_rate_detection->setChecked(
Settings::values.use_display_refresh_rate_detection.GetValue());
}
ui->simulate_3ds_gpu_timings->setChecked(Settings::values.simulate_3ds_gpu_timings.GetValue());
}
void ConfigureGraphics::ApplyConfiguration() {
@ -182,6 +183,9 @@ void ConfigureGraphics::ApplyConfiguration() {
ConfigurationShared::ApplyPerGameSetting(
&Settings::values.delay_game_render_thread_us, ui->delay_render_combo,
[this](s32) { return ui->delay_render_slider->value(); });
ConfigurationShared::ApplyPerGameSetting(&Settings::values.simulate_3ds_gpu_timings,
ui->simulate_3ds_gpu_timings,
simulate_3ds_gpu_timings);
if (Settings::IsConfiguringGlobal()) {
Settings::values.use_shader_jit = ui->toggle_shader_jit->isChecked();
@ -212,6 +216,8 @@ void ConfigureGraphics::SetupPerGameUI() {
ui->physical_device_combo->setEnabled(Settings::values.physical_device.UsingGlobal());
ui->delay_render_combo->setEnabled(
Settings::values.delay_game_render_thread_us.UsingGlobal());
ui->simulate_3ds_gpu_timings->setEnabled(
Settings::values.simulate_3ds_gpu_timings.UsingGlobal());
return;
}
@ -254,6 +260,9 @@ void ConfigureGraphics::SetupPerGameUI() {
ConfigurationShared::SetColoredTristate(ui->disable_spirv_optimizer,
Settings::values.disable_spirv_optimizer,
disable_spirv_optimizer);
ConfigurationShared::SetColoredTristate(ui->simulate_3ds_gpu_timings,
Settings::values.simulate_3ds_gpu_timings,
simulate_3ds_gpu_timings);
}
void ConfigureGraphics::SetPhysicalDeviceComboVisibility(int index) {

View file

@ -44,6 +44,7 @@ private:
ConfigurationShared::CheckState async_presentation;
ConfigurationShared::CheckState spirv_shader_gen;
ConfigurationShared::CheckState disable_spirv_optimizer;
ConfigurationShared::CheckState simulate_3ds_gpu_timings;
std::unique_ptr<Ui::ConfigureGraphics> ui;
QColor bg_color;
};

View file

@ -372,7 +372,7 @@
<number>0</number>
</property>
<property name="maximum">
<number>16000</number>
<number>65000</number>
</property>
<property name="singleStep">
<number>100</number>
@ -404,6 +404,16 @@
</layout>
</widget>
</item>
<item>
<widget class="QCheckBox" name="simulate_3ds_gpu_timings">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Delays GPU completion events based on measurements taken from real hardware, so that games have more realistic GPU time measurements. Helps stabilize dynamic FPS games. Disabling this feature may improve performance in some rare cases at the cost of stability.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Simulate 3DS GPU timings</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View file

@ -78,6 +78,7 @@ void ConfigureStorage::SetConfiguration() {
ui->toggle_virtual_sd->setChecked(Settings::values.use_virtual_sd.GetValue());
ui->toggle_custom_storage->setChecked(Settings::values.use_custom_storage.GetValue());
ui->toggle_compress_cia->setChecked(Settings::values.compress_cia_installs.GetValue());
ui->async_fs_operations->setChecked(Settings::values.async_fs_operations.GetValue());
ui->storage_group->setEnabled(!is_powered_on);
}
@ -86,6 +87,7 @@ void ConfigureStorage::ApplyConfiguration() {
Settings::values.use_virtual_sd = ui->toggle_virtual_sd->isChecked();
Settings::values.use_custom_storage = ui->toggle_custom_storage->isChecked();
Settings::values.compress_cia_installs = ui->toggle_compress_cia->isChecked();
Settings::values.async_fs_operations = ui->async_fs_operations->isChecked();
if (!Settings::values.use_custom_storage) {
FileUtil::UpdateUserPath(FileUtil::UserPath::NANDDir,

View file

@ -180,7 +180,7 @@
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<layout class="QVBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QCheckBox" name="toggle_compress_cia">
<property name="text">
@ -191,6 +191,16 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="async_fs_operations">
<property name="text">
<string>Asynchronous filesystem operations</string>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Makes emulated filesystem accesses asynchronous. Greatly reduces filesystem related stutter, but may slightly increase load times.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>

View file

@ -206,6 +206,16 @@
<addaction name="action_Advance_Frame"/>
<addaction name="menu_Movie"/>
<addaction name="separator"/>
<widget class="QMenu" name="menu_Debug">
<property name="title">
<string>Debug</string>
</property>
<addaction name="action_Debug_Pause"/>
<addaction name="action_Debug_Resume"/>
<addaction name="action_Debug_Step"/>
</widget>
<addaction name="menu_Debug"/>
<addaction name="separator"/>
<addaction name="action_Capture_Screenshot"/>
<addaction name="action_Dump_Video"/>
<addaction name="separator"/>
@ -453,6 +463,30 @@
<string>Capture Screenshot</string>
</property>
</action>
<action name="action_Debug_Pause">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Debug Pause</string>
</property>
</action>
<action name="action_Debug_Resume">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Debug Resume</string>
</property>
</action>
<action name="action_Debug_Step">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Debug Step</string>
</property>
</action>
<action name="action_Dump_Video">
<property name="checkable">
<bool>true</bool>

View file

@ -1,6 +1,6 @@
include(GenerateSCMRev)
add_library(citra_common STATIC
add_library(citra_common STATIC EXCLUDE_FROM_ALL
aarch64/cpu_detect.cpp
aarch64/cpu_detect.h
aarch64/oaknut_abi.h
@ -59,6 +59,7 @@ add_library(citra_common STATIC
microprofile.cpp
microprofile.h
microprofileui.h
optional_helper.h
param_package.cpp
param_package.h
play_time_manager.cpp

View file

@ -198,5 +198,18 @@ HackManager hack_manager = {
0x00040000001D1A00, // EUR
},
}},
{HackType::DELAY_TEXTURE_COPY_COMPLETION,
HackEntry{
.mode = HackAllowMode::FORCE,
.affected_title_ids =
{
// Super Mario 3D Land
0x0004000000054100, // JPN
0x0004000000054000, // USA
0x0004000000053F00, // EUR
0x0004000000089E00, // CHN
0x0004000000089D00, // KOR
},
}},
}};
}

View file

@ -16,6 +16,7 @@ enum class HackType : int {
REGION_FROM_SECURE,
REQUIRES_SHADER_FIXUP,
SPOOF_FRIEND_CODE_SEED,
DELAY_TEXTURE_COPY_COMPLETION,
};
class UserHackData {};

View file

@ -1,4 +1,4 @@
// Copyright 2020 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -153,7 +153,9 @@ private:
void serialize(Archive& ar, const unsigned int) {
ar & backing_mem;
ar & offset;
Init();
if (Archive::is_loading::value) {
Init();
}
}
friend class boost::serialization::access;
};

View file

@ -0,0 +1,42 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <optional>
#include <type_traits>
namespace detail {
template <typename T>
struct is_optional_trait : std::false_type {
using value_type = T;
};
template <typename T>
struct is_optional_trait<std::optional<T>> : std::true_type {
using value_type = T;
};
} // namespace detail
/**
* Returns true if T is a std::optional, false otherwise.
* For example:
* using Test1 = u32;
* using Test2 = std::optional<u32>;
* is_optional_type<Test1> -> false
* is_optional_type<Test2> -> true
*/
template <typename T>
inline constexpr bool is_optional_type = detail::is_optional_trait<T>::value;
/**
* Provides the inner type of T if it is a std::optional, or T itself if it is not.
* For example:
* using Test1 = u32;
* using Test2 = std::optional<u32>;
* optional_value_type<Test1> -> u32
* optional_value_type<Test2> -> u32
*/
template <typename T>
using optional_inner_or_type = typename detail::is_optional_trait<T>::value_type;

View file

@ -106,6 +106,7 @@ void LogSettings() {
log_setting("Renderer_TextureSampling",
GetTextureSamplingName(values.texture_sampling.GetValue()));
log_setting("Renderer_DelayGameRenderThreasUs", values.delay_game_render_thread_us.GetValue());
log_setting("Renderer_Simulate3DSGPUTimings", values.simulate_3ds_gpu_timings.GetValue());
log_setting("Renderer_DisableRightEyeRender", values.disable_right_eye_render.GetValue());
log_setting("Stereoscopy_Render3d", values.render_3d.GetValue());
log_setting("Stereoscopy_Factor3d", values.factor_3d.GetValue());
@ -165,6 +166,8 @@ void LogSettings() {
log_setting("Debugging_InstantDebugLog", values.instant_debug_log.GetValue());
log_setting("Debugging_ToggleUniqueDataConsoleType",
values.toggle_unique_data_console_type.GetValue());
log_setting("Debugging_BreakOnUnmappedMemoryAccess",
values.break_on_unmapped_memory_access.GetValue());
}
bool IsConfiguringGlobal() {
@ -215,6 +218,7 @@ void RestoreGlobalState(bool is_powered_on) {
values.texture_filter.SetGlobal(true);
values.texture_sampling.SetGlobal(true);
values.delay_game_render_thread_us.SetGlobal(true);
values.simulate_3ds_gpu_timings.SetGlobal(true);
values.layout_option.SetGlobal(true);
values.portrait_layout_option.SetGlobal(true);
values.secondary_display_layout.SetGlobal(true);

View file

@ -484,6 +484,7 @@ struct Values {
Setting<bool> use_virtual_sd{true, Keys::use_virtual_sd};
Setting<bool> use_custom_storage{false, Keys::use_custom_storage};
Setting<bool> compress_cia_installs{false, Keys::compress_cia_installs};
Setting<bool> async_fs_operations{true, Keys::async_fs_operations};
// System
SwitchableSetting<s32> region_value{REGION_VALUE_AUTO_SELECT, Keys::region_value};
@ -540,8 +541,9 @@ struct Values {
SwitchableSetting<TextureFilter> texture_filter{TextureFilter::NoFilter, Keys::texture_filter};
SwitchableSetting<TextureSampling> texture_sampling{TextureSampling::GameControlled,
Keys::texture_sampling};
SwitchableSetting<u16, true> delay_game_render_thread_us{0, 0, 16000,
SwitchableSetting<u16, true> delay_game_render_thread_us{0, 0, 65000,
Keys::delay_game_render_thread_us};
SwitchableSetting<bool> simulate_3ds_gpu_timings{true, Keys::simulate_3ds_gpu_timings};
SwitchableSetting<LayoutOption> layout_option{LayoutOption::Default, Keys::layout_option};
SwitchableSetting<bool> swap_screen{false, Keys::swap_screen};
@ -627,6 +629,7 @@ struct Values {
Setting<std::string> output_device{"Auto", Keys::output_device};
Setting<AudioCore::InputType> input_type{AudioCore::InputType::Auto, Keys::input_type};
Setting<std::string> input_device{"Auto", Keys::input_device};
SwitchableSetting<bool> simulate_headphones_plugged{false, Keys::simulate_headphones_plugged};
// Camera
std::array<std::string, Service::CAM::NumCameras> camera_name;
@ -642,6 +645,7 @@ struct Values {
Setting<bool> instant_debug_log{false, Keys::instant_debug_log};
Setting<bool> enable_rpc_server{false, Keys::enable_rpc_server};
Setting<bool> toggle_unique_data_console_type{false, Keys::toggle_unique_data_console_type};
Setting<bool> break_on_unmapped_memory_access{false, Keys::break_on_unmapped_memory_access};
// Miscellaneous
Setting<std::string> log_filter{"*:Info", Keys::log_filter};

View file

@ -1,3 +1,7 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -20,6 +24,10 @@
#include <windows.h>
#endif
#if defined(__APPLE__)
#include <CoreFoundation/CFString.h>
#endif
namespace Common {
/// Make a char lowercase
@ -114,8 +122,7 @@ void BuildCompleteFilename(std::string& _CompleteFilename, const std::string& _P
_CompleteFilename += _Filename;
}
std::vector<std::string> SplitString(const std::string& str, const char delim) {
std::istringstream iss(str);
static std::vector<std::string> SplitString(std::istringstream& iss, const char delim) {
std::vector<std::string> output(1);
while (std::getline(iss, *output.rbegin(), delim)) {
@ -126,6 +133,16 @@ std::vector<std::string> SplitString(const std::string& str, const char delim) {
return output;
}
std::vector<std::string> SplitString(std::string_view str, const char delim) {
std::istringstream iss{std::string(str)};
return SplitString(iss, delim);
}
std::vector<std::string> SplitString(const std::string& str, const char delim) {
std::istringstream iss(str);
return SplitString(iss, delim);
}
std::string TabsToSpaces(int tab_size, std::string in) {
std::size_t i = 0;
@ -164,6 +181,52 @@ std::u16string UTF8ToUTF16(std::string_view input) {
return boost::locale::conv::utf_to_utf<char16_t>(input.data(), input.data() + input.size());
}
#if defined(__APPLE__)
// macOS filesystems may expose decomposed Unicode names through directory listings.
// Normalize to NFC before passing names to guest APIs that expect stable text.
std::string NormalizeNFDToNFC(std::string_view input) {
const std::string fallback(input);
// Core Foundation string
CFStringRef source =
CFStringCreateWithBytes(kCFAllocatorDefault, reinterpret_cast<const UInt8*>(input.data()),
static_cast<CFIndex>(input.size()), kCFStringEncodingUTF8, false);
if (source == nullptr) {
return fallback;
}
// Mutable copy of the source string
CFMutableStringRef normalized = CFStringCreateMutableCopy(kCFAllocatorDefault, 0, source);
CFRelease(source);
if (normalized == nullptr) {
return fallback;
}
// Normalize the string to NFC form
CFStringNormalize(normalized, kCFStringNormalizationFormC);
const CFIndex max_size = CFStringGetMaximumSizeForEncoding(CFStringGetLength(normalized),
kCFStringEncodingUTF8) +
1; // +1 for null terminator
std::string output(static_cast<std::size_t>(max_size), '\0');
// Convert the normalized string back to UTF-8
const bool converted =
CFStringGetCString(normalized, &output[0], max_size, kCFStringEncodingUTF8);
CFRelease(normalized);
if (!converted) {
return fallback;
}
output.resize(std::strlen(output.c_str()));
return output;
}
#endif
#ifdef _WIN32
static std::wstring CPToUTF16(u32 code_page, const std::string& input) {
const auto size =

View file

@ -36,6 +36,7 @@ namespace Common {
[[nodiscard]] bool EndsWith(const std::string& value, const std::string& ending);
[[nodiscard]] std::vector<std::string> SplitString(std::string_view str, const char delim);
[[nodiscard]] std::vector<std::string> SplitString(const std::string& str, const char delim);
// "C:/Windows/winhelp.exe" to "C:/Windows/", "winhelp", ".exe"
@ -49,6 +50,10 @@ void BuildCompleteFilename(std::string& _CompleteFilename, const std::string& _P
[[nodiscard]] std::string UTF16ToUTF8(std::u16string_view input);
[[nodiscard]] std::u16string UTF8ToUTF16(std::string_view input);
// Returns UTF-8 normalized to NFC on platforms that need explicit Unicode normalization.
#if defined(__APPLE__)
[[nodiscard]] std::string NormalizeNFDToNFC(std::string_view input);
#endif
#ifdef _WIN32
[[nodiscard]] std::string UTF16ToUTF8(const std::wstring& input);

View file

@ -1,4 +1,4 @@
add_library(citra_core STATIC
add_library(citra_core STATIC EXCLUDE_FROM_ALL
3ds.h
arm/arm_interface.h
arm/dyncom/arm_dyncom.cpp
@ -126,10 +126,6 @@ add_library(citra_core STATIC
frontend/image_interface.cpp
frontend/image_interface.h
frontend/input.h
gdbstub/gdbstub.cpp
gdbstub/gdbstub.h
gdbstub/hio.cpp
gdbstub/hio.h
hle/applets/applet.cpp
hle/applets/applet.h
hle/applets/erreula.cpp
@ -529,6 +525,16 @@ if (ENABLE_SCRIPTING)
)
endif()
if (ENABLE_GDBSTUB)
target_compile_definitions(citra_core PUBLIC -DENABLE_GDBSTUB)
target_sources(citra_core PRIVATE
gdbstub/gdbstub.cpp
gdbstub/gdbstub.h
gdbstub/hio.cpp
gdbstub/hio.h
)
endif()
if ("x86_64" IN_LIST ARCHITECTURE OR "arm64" IN_LIST ARCHITECTURE)
target_sources(citra_core PRIVATE
arm/dynarmic/arm_dynarmic.cpp

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.
@ -186,6 +186,13 @@ public:
/// Prepare core for thread reschedule (if needed to correctly handle state)
virtual void PrepareReschedule() = 0;
/**
* Whether the backend allows to break with single instruction accuracy
* when Run() is used. If false is returned, the user should expect
* innaccuracies with memory watchpoints access exceptions.
*/
virtual bool HasSingleInstructionBreakAccuracy() = 0;
Core::Timing::Timer& GetTimer() {
return *timer;
}
@ -198,12 +205,28 @@ public:
return id;
}
/**
* Sets the core to not being runnable until its break condition is handled.
*/
void SetBreakFlag() {
break_flag = true;
}
/*
* Sets the core being runnable after its break condition was handled.
*/
void ClearBreakFlag() {
break_flag = false;
}
protected:
// This us used for serialization. Returning nullptr is valid if page tables are not used.
virtual std::shared_ptr<Memory::PageTable> GetPageTable() const = 0;
std::shared_ptr<Core::Timing::Timer> timer;
bool break_flag{};
private:
u32 id;
@ -213,6 +236,7 @@ private:
void save(Archive& ar, const unsigned int file_version) const {
ar << timer;
ar << id;
ar << break_flag;
const auto page_table = GetPageTable();
ar << page_table;
for (int i = 0; i < 15; i++) {
@ -258,6 +282,7 @@ private:
ClearInstructionCache();
ar >> timer;
ar >> id;
ar >> break_flag;
std::shared_ptr<Memory::PageTable> page_table{};
ar >> page_table;
SetPageTable(page_table);

View file

@ -1,7 +1,8 @@
// Copyright 2016 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <csignal>
#include <cstring>
#include <dynarmic/interface/A32/a32.h>
#include <dynarmic/interface/optimization_flags.h>
@ -13,10 +14,20 @@
#include "core/arm/dynarmic/arm_tick_counts.h"
#include "core/core.h"
#include "core/core_timing.h"
#ifdef ENABLE_GDBSTUB
#include "core/gdbstub/gdbstub.h"
#endif
#include "core/hle/kernel/svc.h"
#include "core/memory.h"
#ifndef SIGILL
constexpr u32 SIGILL = 4;
#endif
#ifndef SIGTRAP
constexpr u32 SIGTRAP = 5;
#endif
namespace Core {
class DynarmicUserCallbacks final : public Dynarmic::A32::UserCallbacks {
@ -25,6 +36,10 @@ public:
: parent(parent), svc_context(parent.system), memory(parent.memory) {}
~DynarmicUserCallbacks() = default;
std::optional<std::uint32_t> MemoryReadCode(VAddr vaddr) override {
return memory.Read32OrNullopt(vaddr);
}
std::uint8_t MemoryRead8(VAddr vaddr) override {
return memory.Read8(vaddr);
}
@ -82,12 +97,13 @@ public:
case Dynarmic::A32::Exception::NoExecuteFault:
break;
case Dynarmic::A32::Exception::Breakpoint:
#ifdef ENABLE_GDBSTUB
if (GDBStub::IsConnected()) {
parent.jit->HaltExecution();
parent.SetPC(pc);
parent.ServeBreak();
parent.ServeBreak(SIGTRAP);
return;
}
#endif
break;
case Dynarmic::A32::Exception::SendEvent:
case Dynarmic::A32::Exception::SendEventLocal:
@ -99,11 +115,40 @@ public:
case Dynarmic::A32::Exception::PreloadInstruction:
return;
}
for (int i = 0; i < 16; i++) {
LOG_CRITICAL(Debug, "r{:02d} = {:08X}", i, parent.GetReg(i));
static constexpr auto ExceptionToString = [](Dynarmic::A32::Exception e) -> std::string {
switch (e) {
case Dynarmic::A32::Exception::UndefinedInstruction:
return "UndefinedInstruction";
case Dynarmic::A32::Exception::UnpredictableInstruction:
return "UnpredictableInstruction";
case Dynarmic::A32::Exception::DecodeError:
return "DecodeError";
case Dynarmic::A32::Exception::NoExecuteFault:
return "NoExecuteFault";
case Dynarmic::A32::Exception::Breakpoint:
return "Breakpoint";
default:
return fmt::format("Unknown({})", e);
}
};
parent.SetPC(pc);
#ifdef ENABLE_GDBSTUB
if (GDBStub::IsConnected()) {
parent.ServeBreak(SIGILL);
} else
#endif
{
std::string error;
for (int i = 0; i < 16; i++) {
error += fmt::format("r{:02d} = {:08X}\n", i, parent.GetReg(i));
}
error += fmt::format("ExceptionRaised(exception = {}, pc = {:08X})",
ExceptionToString(exception), pc);
parent.system.SetStatus(Core::System::ResultStatus::ErrorCoreExceptionRaised,
error.c_str());
}
ASSERT_MSG(false, "ExceptionRaised(exception = {}, pc = {:08X}, code = {:08X})", exception,
pc, MemoryReadCode(pc).value());
}
void AddTicks(std::uint64_t ticks) override {
@ -138,16 +183,19 @@ MICROPROFILE_DEFINE(ARM_Jit, "ARM JIT", "ARM JIT", MP_RGB(255, 64, 64));
void ARM_Dynarmic::Run() {
ASSERT(memory.GetCurrentPageTable() == current_page_table);
MICROPROFILE_SCOPE(ARM_Jit);
if (break_flag) [[unlikely]] {
return;
}
jit->Run();
}
void ARM_Dynarmic::Step() {
jit->Step();
if (GDBStub::IsConnected()) {
ServeBreak();
if (break_flag) [[unlikely]] {
return;
}
jit->Step();
}
void ARM_Dynarmic::SetPC(u32 pc) {
@ -294,11 +342,10 @@ void ARM_Dynarmic::SetPageTable(const std::shared_ptr<Memory::PageTable>& page_t
jits.emplace(current_page_table, std::move(new_jit));
}
void ARM_Dynarmic::ServeBreak() {
Kernel::Thread* thread = system.Kernel().GetCurrentThreadManager().GetCurrentThread();
SaveContext(thread->context);
GDBStub::Break();
GDBStub::SendTrap(thread, 5);
void ARM_Dynarmic::ServeBreak([[maybe_unused]] int signal) {
#ifdef ENABLE_GDBSTUB
GDBStub::Break(signal);
#endif
}
std::unique_ptr<Dynarmic::A32::Jit> ARM_Dynarmic::MakeJit() {

View file

@ -1,4 +1,4 @@
// Copyright 2016 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -56,11 +56,15 @@ public:
void ClearExclusiveState() override;
void SetPageTable(const std::shared_ptr<Memory::PageTable>& page_table) override;
bool HasSingleInstructionBreakAccuracy() override {
return false;
}
protected:
std::shared_ptr<Memory::PageTable> GetPageTable() const override;
private:
void ServeBreak();
void ServeBreak(int signal);
friend class DynarmicUserCallbacks;
Core::System& system;

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.
@ -24,10 +24,16 @@ ARM_DynCom::ARM_DynCom(Core::System& system_, Memory::MemorySystem& memory,
ARM_DynCom::~ARM_DynCom() {}
void ARM_DynCom::Run() {
if (break_flag) [[unlikely]] {
return;
}
ExecuteInstructions(std::max<s64>(timer->GetDowncount(), 0));
}
void ARM_DynCom::Step() {
if (break_flag) [[unlikely]] {
return;
}
ExecuteInstructions(1);
}

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.
@ -51,6 +51,10 @@ public:
void SetPageTable(const std::shared_ptr<Memory::PageTable>& page_table) override;
void PrepareReschedule() override;
bool HasSingleInstructionBreakAccuracy() override {
return true;
}
protected:
std::shared_ptr<Memory::PageTable> GetPageTable() const override;

View file

@ -1,3 +1,7 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
// Copyright 2012 Michael Kang, 2014 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -19,7 +23,9 @@
#include "core/arm/skyeye_common/vfp/vfp.h"
#include "core/core.h"
#include "core/core_timing.h"
#ifdef ENABLE_GDBSTUB
#include "core/gdbstub/gdbstub.h"
#endif
#include "core/hle/kernel/svc.h"
#include "core/memory.h"
@ -918,9 +924,11 @@ MICROPROFILE_DEFINE(DynCom_Execute, "DynCom", "Execute", MP_RGB(255, 0, 0));
unsigned InterpreterMainLoop(ARMul_State* cpu) {
MICROPROFILE_SCOPE(DynCom_Execute);
#ifdef ENABLE_GDBSTUB
/// Nearest upcoming GDB code execution breakpoint, relative to the last dispatch's address.
GDBStub::BreakpointAddress breakpoint_data;
breakpoint_data.type = GDBStub::BreakpointType::None;
#endif
#undef RM
#undef RS
@ -948,17 +956,15 @@ unsigned InterpreterMainLoop(ARMul_State* cpu) {
#define INC_PC(l) ptr += sizeof(arm_inst) + l
#define INC_PC_STUB ptr += sizeof(arm_inst)
#ifdef ANDROID
#ifndef ENABLE_GDBSTUB
#define GDB_BP_CHECK
#else
#define GDB_BP_CHECK \
cpu->Cpsr &= ~(1 << 5); \
cpu->Cpsr |= cpu->TFlag << 5; \
if (GDBStub::IsServerEnabled()) { \
if (GDBStub::IsMemoryBreak()) { \
goto END; \
} else if (breakpoint_data.type != GDBStub::BreakpointType::None && \
PC == breakpoint_data.address) { \
if (GDBStub::IsServerEnabled()) [[unlikely]] { \
if (breakpoint_data.type != GDBStub::BreakpointType::None && \
PC == breakpoint_data.address) { \
cpu->RecordBreak(breakpoint_data); \
goto END; \
} \
@ -1651,7 +1657,7 @@ DISPATCH: {
goto END;
}
#ifndef ANDROID
#ifdef ENABLE_GDBSTUB
// Find breakpoint if one exists within the block
if (GDBStub::IsConnected()) {
breakpoint_data =

View file

@ -1,15 +1,23 @@
// Copyright 2015 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <algorithm>
#include <csignal>
#include "common/logging/log.h"
#include "common/swap.h"
#include "core/arm/skyeye_common/armstate.h"
#include "core/arm/skyeye_common/vfp/vfp.h"
#include "core/core.h"
#ifdef ENABLE_GDBSTUB
#include "core/gdbstub/gdbstub.h"
#endif
#include "core/memory.h"
#ifndef SIGTRAP
constexpr u32 SIGTRAP = 5;
#endif
ARMul_State::ARMul_State(Core::System& system_, Memory::MemorySystem& memory_,
PrivilegeMode initial_mode)
: system{system_}, memory{memory_} {
@ -182,26 +190,12 @@ void ARMul_State::ResetMPCoreCP15Registers() {
CP15[CP15_MAIN_TLB_LOCKDOWN_ATTRIBUTE] = 0x00000000;
CP15[CP15_TLB_DEBUG_CONTROL] = 0x00000000;
}
#ifdef ANDROID
static void CheckMemoryBreakpoint(u32 address, GDBStub::BreakpointType type) {}
#else
static void CheckMemoryBreakpoint(u32 address, GDBStub::BreakpointType type) {
if (GDBStub::IsServerEnabled() && GDBStub::CheckBreakpoint(address, type)) {
LOG_DEBUG(Debug, "Found memory breakpoint @ {:08x}", address);
GDBStub::Break(true);
}
}
#endif
u8 ARMul_State::ReadMemory8(u32 address) const {
CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Read);
return memory.Read8(address);
}
u16 ARMul_State::ReadMemory16(u32 address) const {
CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Read);
u16 data = memory.Read16(address);
if (InBigEndianMode())
@ -211,8 +205,6 @@ u16 ARMul_State::ReadMemory16(u32 address) const {
}
u32 ARMul_State::ReadMemory32(u32 address) const {
CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Read);
u32 data = memory.Read32(address);
if (InBigEndianMode())
@ -222,8 +214,6 @@ u32 ARMul_State::ReadMemory32(u32 address) const {
}
u64 ARMul_State::ReadMemory64(u32 address) const {
CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Read);
u64 data = memory.Read64(address);
if (InBigEndianMode())
@ -233,14 +223,10 @@ u64 ARMul_State::ReadMemory64(u32 address) const {
}
void ARMul_State::WriteMemory8(u32 address, u8 data) {
CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Write);
memory.Write8(address, data);
}
void ARMul_State::WriteMemory16(u32 address, u16 data) {
CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Write);
if (InBigEndianMode())
data = Common::swap16(data);
@ -248,8 +234,6 @@ void ARMul_State::WriteMemory16(u32 address, u16 data) {
}
void ARMul_State::WriteMemory32(u32 address, u32 data) {
CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Write);
if (InBigEndianMode())
data = Common::swap32(data);
@ -257,8 +241,6 @@ void ARMul_State::WriteMemory32(u32 address, u32 data) {
}
void ARMul_State::WriteMemory64(u32 address, u64 data) {
CheckMemoryBreakpoint(address, GDBStub::BreakpointType::Write);
if (InBigEndianMode())
data = Common::swap64(data);
@ -601,6 +583,7 @@ void ARMul_State::WriteCP15Register(u32 value, u32 crn, u32 opcode_1, u32 crm, u
}
void ARMul_State::ServeBreak() {
#ifdef ENABLE_GDBSTUB
if (!GDBStub::IsServerEnabled()) {
return;
}
@ -609,12 +592,9 @@ void ARMul_State::ServeBreak() {
DEBUG_ASSERT(Reg[15] == last_bkpt.address);
}
Kernel::Thread* thread = system.Kernel().GetCurrentThreadManager().GetCurrentThread();
system.GetRunningCore().SaveContext(thread->context);
if (last_bkpt_hit || GDBStub::IsMemoryBreak() || GDBStub::GetCpuStepFlag()) {
if (last_bkpt_hit) {
last_bkpt_hit = false;
GDBStub::Break();
GDBStub::SendTrap(thread, 5);
GDBStub::Break(SIGTRAP);
}
#endif
}

View file

@ -1,3 +1,7 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
/* armdefs.h -- ARMulator common definitions: ARM6 Instruction Emulator.
Copyright (C) 1994 Advanced RISC Machines Ltd.
@ -21,7 +25,9 @@
#include <unordered_map>
#include "common/common_types.h"
#include "core/arm/skyeye_common/arm_regformat.h"
#ifdef ENABLE_GDBSTUB
#include "core/gdbstub/gdbstub.h"
#endif
namespace Core {
class System;
@ -199,10 +205,12 @@ public:
return TFlag ? 2 : 4;
}
#ifdef ENABLE_GDBSTUB
void RecordBreak(GDBStub::BreakpointAddress bkpt) {
last_bkpt = bkpt;
last_bkpt_hit = true;
}
#endif
void ServeBreak();
@ -267,6 +275,8 @@ private:
u32 exclusive_tag; // The address for which the local monitor is in exclusive access mode
bool exclusive_state;
#ifdef ENABLE_GDBSTUB
GDBStub::BreakpointAddress last_bkpt{};
bool last_bkpt_hit = false;
#endif
};

View file

@ -26,7 +26,9 @@
#include "core/dumping/backend.h"
#include "core/file_sys/ncch_container.h"
#include "core/frontend/image_interface.h"
#ifdef ENABLE_GDBSTUB
#include "core/gdbstub/gdbstub.h"
#endif
#include "core/global.h"
#include "core/hle/kernel/ipc_debugger/recorder.h"
#include "core/hle/kernel/kernel.h"
@ -83,23 +85,17 @@ System::ResultStatus System::RunLoop(bool tight_loop) {
return ResultStatus::ErrorNotInitialized;
}
#ifdef ENABLE_GDBSTUB
if (GDBStub::IsServerEnabled()) {
Kernel::Thread* thread = kernel->GetCurrentThreadManager().GetCurrentThread();
if (thread && running_core) {
running_core->SaveContext(thread->context);
// The break flag is only set if GDB is connected,
// we can do clearing here safely. If it is ever
// used outside, move the clearing outside the if.
for (auto& cpu_core : cpu_cores) {
cpu_core->ClearBreakFlag();
}
GDBStub::HandlePacket(*this);
// If the loop is halted and we want to step, use a tiny (1) number of instructions to
// execute. Otherwise, get out of the loop function.
if (GDBStub::GetCpuHaltFlag()) {
if (GDBStub::GetCpuStepFlag()) {
tight_loop = false;
} else {
return ResultStatus::Success;
}
}
}
#endif
Signal signal{Signal::None};
u32 param{};
@ -259,6 +255,8 @@ System::ResultStatus System::RunLoop(bool tight_loop) {
cpu_core->GetTimer().Idle();
PrepareReschedule();
} else {
// In the rare case the break flag is set (due to exception thrown)
// there is probably no need to adjust the timer accordingly.
if (tight_loop) {
cpu_core->Run();
} else {
@ -269,10 +267,6 @@ System::ResultStatus System::RunLoop(bool tight_loop) {
}
}
if (GDBStub::IsServerEnabled()) {
GDBStub::SetCpuStepFlag(false);
}
Reschedule();
return status;
@ -440,6 +434,10 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st
return ResultStatus::ErrorLoader_ErrorInvalidFormat;
case Loader::ResultStatus::ErrorGbaTitle:
return ResultStatus::ErrorLoader_ErrorGbaTitle;
case Loader::ResultStatus::ErrorPatches:
return ResultStatus::ErrorLoader_ErrorPatches;
case Loader::ResultStatus::ErrorPatchesInvalidTitle:
return ResultStatus::ErrorLoader_ErrorPatchesInvalidTitle;
case Loader::ResultStatus::ErrorArtic:
return ResultStatus::ErrorArticDisconnected;
default:
@ -571,7 +569,9 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window,
app_loader->ReadProgramId(loading_title_id);
HW::AES::InitKeys();
Service::Init(*this, loading_title_id, lle_modules, !app_loader->DoingInitialSetup());
#ifdef ENABLE_GDBSTUB
GDBStub::DeferStart();
#endif
if (!registered_image_interface) {
registered_image_interface = std::make_shared<Frontend::ImageInterface>();
@ -581,8 +581,9 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window,
auto gsp = service_manager->GetService<Service::GSP::GSP_GPU>("gsp::Gpu");
gpu = std::make_unique<VideoCore::GPU>(*this, emu_window, secondary_window);
gpu->SetInterruptHandler(
[gsp](Service::GSP::InterruptId interrupt_id) { gsp->SignalInterrupt(interrupt_id); });
gpu->SetInterruptHandler([gsp](Service::GSP::InterruptId interrupt_id, u64 wait_delay_ns) {
gsp->SignalInterrupt(interrupt_id, wait_delay_ns);
});
auto plg_ldr = Service::PLGLDR::GetService(*this);
if (plg_ldr) {
@ -695,7 +696,9 @@ void System::Shutdown(bool is_deserializing) {
gpu.reset();
if (!is_deserializing) {
lle_modules.clear();
#ifdef ENABLE_GDBSTUB
GDBStub::Shutdown();
#endif
perf_stats.reset();
app_loader.reset();
}
@ -758,8 +761,10 @@ void System::Reset() {
}
void System::ApplySettings() {
#ifdef ENABLE_GDBSTUB
GDBStub::SetServerPort(Settings::values.gdbstub_port.GetValue());
GDBStub::ToggleServer(Settings::values.use_gdbstub.GetValue());
#endif
if (gpu) {
#ifndef ANDROID
@ -902,8 +907,9 @@ void System::serialize(Archive& ar, const unsigned int file_version) {
// Re-register gpu callback, because gsp service changed after service_manager got
// serialized
auto gsp = service_manager->GetService<Service::GSP::GSP_GPU>("gsp::Gpu");
gpu->SetInterruptHandler(
[gsp](Service::GSP::InterruptId interrupt_id) { gsp->SignalInterrupt(interrupt_id); });
gpu->SetInterruptHandler([gsp](Service::GSP::InterruptId interrupt_id, u64 wait_delay_ns) {
gsp->SignalInterrupt(interrupt_id, wait_delay_ns);
});
// Apply per program settings and switch the shader cache to the title running when the
// savestate was created.

View file

@ -99,12 +99,16 @@ public:
/// invalid format
ErrorLoader_ErrorGbaTitle, ///< Error loading the specified application as it is GBA Virtual
///< Console
ErrorSystemFiles, ///< Error in finding system files
ErrorSavestate, ///< Error saving or loading
ErrorArticDisconnected, ///< Error when artic base disconnects
ErrorN3DSApplication, ///< Error launching New 3DS application in Old 3DS mode
ShutdownRequested, ///< Emulated program requested a system shutdown
ErrorUnknown ///< Any other error
ErrorLoader_ErrorPatches, ///< Generic error while loading patches for an application
ErrorLoader_ErrorPatchesInvalidTitle, ///< A patch was loaded for the incorrect application
ErrorSystemFiles, ///< Error in finding system files
ErrorSavestate, ///< Error saving or loading
ErrorArticDisconnected, ///< Error when artic base disconnects
ErrorN3DSApplication, ///< Error launching New 3DS application in Old 3DS mode
ErrorCoreExceptionRaised, ///< The CPU emulation raised an exception
ErrorMemoryExceptionRaised, ///< Unmmaped memory was accessed
ShutdownRequested, ///< Emulated program requested a system shutdown
ErrorUnknown ///< Any other error
};
explicit System();
@ -403,6 +407,18 @@ public:
info_led_color_changed = func;
}
void SetDebugNextProcessFlag() {
debug_next_process = true;
}
bool GetDebugNextProcessFlag() {
return debug_next_process;
}
void ClearDebugNextProcessFlag() {
debug_next_process = false;
}
private:
/**
* Initialize the emulated system.
@ -512,6 +528,8 @@ private:
Common::Vec3<u8> info_led_color;
std::function<void()> info_led_color_changed;
bool debug_next_process;
friend class boost::serialization::access;
template <typename Archive>
void serialize(Archive& ar, const unsigned int file_version);

View file

@ -1,13 +1,15 @@
// 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.
#include <algorithm>
#include <iterator>
#include <memory>
#include "common/archives.h"
#include "common/common_types.h"
#include "common/file_util.h"
#include "common/logging/log.h"
#include "common/string_util.h"
#include "core/file_sys/disk_archive.h"
#include "core/file_sys/errors.h"
@ -62,22 +64,29 @@ u32 DiskDirectory::Read(const u32 count, Entry* entries) {
while (entries_read < count && children_iterator != directory.children.cend()) {
const FileUtil::FSTEntry& file = *children_iterator;
// Directory entries are exposed to the guest as UTF-16. Normalize host UTF-8 names first
// so host Unicode normalization differences do not leak into guest-visible SDMC paths.
#if defined(__APPLE__)
const std::string filename = Common::NormalizeNFDToNFC(file.virtualName);
#else
const std::string& filename = file.virtualName;
#endif
const std::u16string filename_utf16 = Common::UTF8ToUTF16(filename);
Entry& entry = entries[entries_read];
LOG_TRACE(Service_FS, "File {}: size={} dir={}", filename, file.size, file.isDirectory);
// TODO(Link Mauve): use a proper conversion to UTF-16.
for (std::size_t j = 0; j < FILENAME_LENGTH; ++j) {
entry.filename[j] = filename[j];
if (!filename[j])
break;
std::fill(std::begin(entry.filename), std::end(entry.filename), u'\0');
const std::size_t copy_length = std::min(filename_utf16.size(), FILENAME_LENGTH - 1);
for (std::size_t j = 0; j < copy_length; ++j) {
entry.filename[j] = filename_utf16[j];
}
FileUtil::SplitFilename83(filename, entry.short_name, entry.extension);
entry.is_directory = file.isDirectory;
entry.is_hidden = (filename[0] == '.');
entry.is_hidden = (!filename.empty() && filename[0] == '.');
entry.is_read_only = 0;
entry.file_size = file.size;
@ -92,5 +101,4 @@ u32 DiskDirectory::Read(const u32 count, Entry* entries) {
}
return entries_read;
}
} // namespace FileSys

View file

@ -1,4 +1,4 @@
// Copyright 2020 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -247,14 +247,14 @@ void LayeredFS::LoadExtRelocations() {
std::vector<u8> buffer(file.relocation.size); // Original size
romfs->ReadFile(file.relocation.original_offset, buffer.size(), buffer.data());
bool ret = false;
Loader::ResultStatus ret{};
if (extension == ".ips") {
ret = Patch::ApplyIpsPatch(patch, buffer);
} else {
ret = Patch::ApplyBpsPatch(patch, buffer);
}
if (ret) {
if (ret == Loader::ResultStatus::Success) {
LOG_INFO(Service_FS, "LayeredFS patched file {}", file_path);
file.relocation.type = 2;

View file

@ -1,4 +1,4 @@
// Copyright 2020 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.

View file

@ -518,7 +518,7 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect
Loader::ResultStatus NCCHContainer::ApplyCodePatch(std::vector<u8>& code) const {
struct PatchLocation {
std::string path;
bool (*patch_fn)(const std::vector<u8>& patch, std::vector<u8>& code);
Loader::ResultStatus (*patch_fn)(const std::vector<u8>& patch, std::vector<u8>& code);
};
const auto mods_path =
@ -555,11 +555,12 @@ Loader::ResultStatus NCCHContainer::ApplyCodePatch(std::vector<u8>& code) const
std::vector<u8> patch(patch_file.GetSize());
if (patch_file.ReadBytes(patch.data(), patch.size()) != patch.size())
return Loader::ResultStatus::Error;
return Loader::ResultStatus::ErrorPatches;
LOG_INFO(Service_FS, "File {} patching code.bin", info.path);
if (!info.patch_fn(patch, code))
return Loader::ResultStatus::Error;
auto patch_result = info.patch_fn(patch, code);
if (patch_result != Loader::ResultStatus::Success)
return patch_result;
return Loader::ResultStatus::Success;
}

View file

@ -1,4 +1,4 @@
// Copyright 2019 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -14,21 +14,21 @@
namespace FileSys::Patch {
bool ApplyIpsPatch(const std::vector<u8>& ips, std::vector<u8>& buffer) {
Loader::ResultStatus ApplyIpsPatch(const std::vector<u8>& ips, std::vector<u8>& buffer) {
std::size_t cursor = 5;
std::size_t patch_length = ips.size() - 3;
std::string ips_header(ips.begin(), ips.begin() + 5);
if (ips_header != "PATCH") {
LOG_INFO(Service_FS, "Attempted to load invalid IPS");
return false;
return Loader::ResultStatus::ErrorPatches;
}
while (cursor < patch_length) {
std::string eof_check(ips.begin() + cursor, ips.begin() + cursor + 3);
if (eof_check == "EOF")
return false;
break;
std::size_t offset = ips[cursor] << 16 | ips[cursor + 1] << 8 | ips[cursor + 2];
std::size_t length = ips[cursor + 3] << 8 | ips[cursor + 4];
@ -38,7 +38,7 @@ bool ApplyIpsPatch(const std::vector<u8>& ips, std::vector<u8>& buffer) {
length = ips[cursor + 5] << 8 | ips[cursor + 6];
if (buffer.size() < offset + length)
return false;
return Loader::ResultStatus::ErrorPatches;
for (u32 i = 0; i < length; ++i)
buffer[offset + i] = ips[cursor + 7];
@ -49,12 +49,12 @@ bool ApplyIpsPatch(const std::vector<u8>& ips, std::vector<u8>& buffer) {
}
if (buffer.size() < offset + length)
return false;
return Loader::ResultStatus::ErrorPatches;
std::memcpy(&buffer[offset], &ips[cursor + 5], length);
cursor += length + 5;
}
return true;
return Loader::ResultStatus::Success;
}
namespace Bps {
@ -149,11 +149,11 @@ public:
PatchApplier(Stream<const u8> source, Stream<u8> target, Stream<const u8> patch)
: m_source{source}, m_target{target}, m_patch{patch} {}
bool Apply() {
Loader::ResultStatus Apply() {
const auto magic = *m_patch.Read<std::array<char, MagicSize>>();
if (std::string_view(magic.data(), magic.size()) != "BPS1") {
LOG_ERROR(Service_FS, "Invalid BPS magic");
return false;
return Loader::ResultStatus::ErrorPatches;
}
const Bps::Number source_size = m_patch.ReadNumber();
@ -161,7 +161,7 @@ public:
const Bps::Number metadata_size = m_patch.ReadNumber();
if (source_size > m_source.size() || target_size > m_target.size() || metadata_size != 0) {
LOG_ERROR(Service_FS, "Invalid sizes");
return false;
return Loader::ResultStatus::ErrorPatches;
}
const std::size_t command_start_offset = m_patch.Tell();
@ -173,22 +173,22 @@ public:
if (crc32(m_source.data(), source_size) != source_crc32) {
LOG_ERROR(Service_FS, "Unexpected source hash");
return false;
return Loader::ResultStatus::ErrorPatchesInvalidTitle;
}
// Process all patch commands.
std::memset(m_target.data(), 0, m_target.size());
while (m_patch.Tell() < command_end_offset) {
if (!HandleCommand())
return false;
return Loader::ResultStatus::ErrorPatches;
}
if (crc32(m_target.data(), target_size) != target_crc32) {
LOG_ERROR(Service_FS, "Unexpected target hash");
return false;
return Loader::ResultStatus::ErrorPatches;
}
return true;
return Loader::ResultStatus::Success;
}
private:
@ -257,7 +257,7 @@ private:
} // namespace Bps
bool ApplyBpsPatch(const std::vector<u8>& patch, std::vector<u8>& buffer) {
Loader::ResultStatus ApplyBpsPatch(const std::vector<u8>& patch, std::vector<u8>& buffer) {
Bps::Stream patch_stream{patch.data(), patch.size()};
// Move the offset past the file format marker (i.e. "BPS1")

View file

@ -1,4 +1,4 @@
// Copyright 2019 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -7,11 +7,12 @@
#include <vector>
#include "common/common_types.h"
#include "core/loader/loader.h"
namespace FileSys::Patch {
bool ApplyIpsPatch(const std::vector<u8>& patch, std::vector<u8>& buffer);
Loader::ResultStatus ApplyIpsPatch(const std::vector<u8>& patch, std::vector<u8>& buffer);
bool ApplyBpsPatch(const std::vector<u8>& patch, std::vector<u8>& buffer);
Loader::ResultStatus ApplyBpsPatch(const std::vector<u8>& patch, std::vector<u8>& buffer);
} // namespace FileSys::Patch

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,7 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
// Copyright 2013 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
@ -10,6 +14,10 @@
#include "common/common_types.h"
#include "core/hle/kernel/thread.h"
#ifndef ENABLE_GDBSTUB
#error "File was included with GDB stub support disabled"
#endif
namespace Core {
class System;
}
@ -60,18 +68,28 @@ void Shutdown();
/// Checks if the gdbstub server is enabled.
bool IsServerEnabled();
/// Returns true if the GDB server is initialized
bool IsInitialized();
/// Returns true if there is an active socket connection.
bool IsConnected();
/**
* Signal to the gdbstub server that it should halt CPU execution.
*
* @param is_memory_break If true, the break resulted from a memory breakpoint.
* @param signal Signal that produced the break (SIGTRAP by default)
*/
void Break(bool is_memory_break = false);
void Break(int signal);
/// Determine if there was a memory breakpoint.
bool IsMemoryBreak();
/**
* Signal to the GDB stub that the specified process ID is exiting
*/
void OnProcessExit(u32 process_id);
/**
* Signal to the GDB stub that the specified thread ID is exiting
*/
void OnThreadExit(u32 thread_id);
/// Read and handle packet from gdb client.
void HandlePacket(Core::System& system);
@ -88,37 +106,10 @@ BreakpointAddress GetNextBreakpointFromAddress(VAddr addr, GDBStub::BreakpointTy
* Check if a breakpoint of the specified type exists at the given address.
*
* @param addr Address of breakpoint.
* @param access_len Access size in bytes.
* @param type Type of breakpoint.
*/
bool CheckBreakpoint(VAddr addr, GDBStub::BreakpointType type);
// If set to true, the CPU will halt at the beginning of the next CPU loop.
bool GetCpuHaltFlag();
/**
* If set to true, the CPU will halt at the beginning of the next CPU loop.
*
* @param halt whether to halt on the next loop
*/
void SetCpuHaltFlag(bool halt);
// If set to true and the CPU is halted, the CPU will step one instruction.
bool GetCpuStepFlag();
/**
* When set to true, the CPU will step one instruction when the CPU is halted next.
*
* @param is_step
*/
void SetCpuStepFlag(bool is_step);
/**
* Send trap signal from thread back to the gdbstub server.
*
* @param thread Sending thread.
* @param trap Trap no.
*/
void SendTrap(Kernel::Thread* thread, int trap);
bool CheckBreakpoint(VAddr addr, u32 access_len, BreakpointType type);
/**
* Send reply to gdb client.

View file

@ -1,13 +1,22 @@
// 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.
#include <csignal>
#include <fmt/ranges.h>
#include "common/string_util.h"
#include "core/core.h"
#include "core/gdbstub/gdbstub.h"
#include "core/gdbstub/hio.h"
#ifndef ENABLE_GDBSTUB
#error "File was compiled with GDB stub support disabled"
#endif
#ifndef SIGTRAP
constexpr u32 SIGTRAP = 5;
#endif
namespace GDBStub {
namespace {
@ -23,9 +32,6 @@ enum class Status {
static std::atomic<Status> request_status{Status::NoRequest};
static std::atomic<bool> was_halted = false;
static std::atomic<bool> was_stepping = false;
} // namespace
/**
@ -54,7 +60,7 @@ static void SendErrorReply(int error_code, int retval = -1) {
SendReply(packet.data());
}
void SetHioRequest(Core::System& system, const VAddr addr) {
void SetHioRequest(Core::System& system, Kernel::Process* process, const VAddr addr) {
if (!IsServerEnabled()) {
LOG_WARNING(Debug_GDBStub, "HIO requested but GDB stub is not running");
return;
@ -70,14 +76,13 @@ void SetHioRequest(Core::System& system, const VAddr addr) {
}
auto& memory = system.Memory();
const auto process = system.Kernel().GetCurrentProcess();
if (!memory.IsValidVirtualAddress(*process, addr)) {
LOG_WARNING(Debug_GDBStub, "Invalid address for HIO request");
return;
}
memory.ReadBlock(addr, &current_hio_request, sizeof(PackedGdbHioRequest));
memory.ReadBlock(*process, addr, &current_hio_request, sizeof(PackedGdbHioRequest));
if (current_hio_request.magic != std::array{'G', 'D', 'B', '\0'}) {
std::string_view bad_magic{
@ -97,18 +102,13 @@ void SetHioRequest(Core::System& system, const VAddr addr) {
current_hio_request_addr = addr;
request_status = Status::NotSent;
was_halted = GetCpuHaltFlag();
was_stepping = GetCpuStepFlag();
// Now halt, so that no further instructions are executed until the request
// is processed by the client. We will continue after the reply comes back
Break();
SetCpuHaltFlag(true);
SetCpuStepFlag(false);
Break(SIGTRAP);
system.GetRunningCore().ClearInstructionCache();
}
void HandleHioReply(Core::System& system, const u8* const command_buffer,
void HandleHioReply(Core::System& system, Kernel::Process* process, const u8* const command_buffer,
const u32 command_length) {
if (!IsWaitingForHioReply()) {
LOG_WARNING(Debug_GDBStub, "Got HIO reply but never sent a request");
@ -177,7 +177,6 @@ void HandleHioReply(Core::System& system, const u8* const command_buffer,
current_hio_request.retval, current_hio_request.gdb_errno,
current_hio_request.ctrl_c);
const auto process = system.Kernel().GetCurrentProcess();
auto& memory = system.Memory();
// should have been checked when we first initialized the request,
@ -188,15 +187,14 @@ void HandleHioReply(Core::System& system, const u8* const command_buffer,
return;
}
memory.WriteBlock(current_hio_request_addr, &current_hio_request, sizeof(PackedGdbHioRequest));
memory.WriteBlock(*process, current_hio_request_addr, &current_hio_request,
sizeof(PackedGdbHioRequest));
current_hio_request = {};
current_hio_request_addr = 0;
request_status = Status::NoRequest;
// Restore state from before the request came in
SetCpuStepFlag(was_stepping);
SetCpuHaltFlag(was_halted);
system.GetRunningCore().ClearInstructionCache();
}

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.
@ -6,10 +6,18 @@
#include "common/common_types.h"
#ifndef ENABLE_GDBSTUB
#error "File was included with GDB stub support disabled"
#endif
namespace Core {
class System;
}
namespace Kernel {
class Process;
}
namespace GDBStub {
/**
@ -51,7 +59,7 @@ static_assert(sizeof(PackedGdbHioRequest) == 152,
*
* @param address The memory address of the \ref PackedGdbHioRequest.
*/
void SetHioRequest(Core::System& system, const VAddr address);
void SetHioRequest(Core::System& system, Kernel::Process* process, const VAddr address);
/**
* If there is a pending HIO request, send it to the client.
@ -63,6 +71,7 @@ bool HandlePendingHioRequestPacket();
/**
* Process an HIO reply from the client.
*/
void HandleHioReply(Core::System& system, const u8* const command_buffer, const u32 command_length);
void HandleHioReply(Core::System& system, Kernel::Process* process, const u8* const command_buffer,
const u32 command_length);
} // namespace GDBStub

View file

@ -17,6 +17,7 @@
#include "common/serialization/boost_small_vector.hpp"
#include "common/settings.h"
#include "common/swap.h"
#include "common/thread_worker.h"
#include "core/hle/ipc.h"
#include "core/hle/kernel/object.h"
#include "core/hle/kernel/server_session.h"
@ -327,6 +328,55 @@ public:
}
}
/**
* Same as RunAsync, but runs the async operation on a specific thread worker provided by the
* caller.
* @param worker The thread worker where the operation will be run.
* @param async_section Callable that takes Kernel::HLERequestContext& as argument
* and returns the amount of nanoseconds to wait before calling result_function.
* This callable is ran asynchronously.
* @param result_function Callable that takes Kernel::HLERequestContext& as argument
* and doesn't return anything. This callable is ran from the emulator thread
* and can be used to set the IPC result.
* @param really_async If set to false, it will call both async_section and result_function
* from the emulator thread.
*/
template <typename AsyncFunctor, typename ResultFunctor>
void RunOnThreadWorker(Common::ThreadWorker& worker, AsyncFunctor async_section,
ResultFunctor result_function, bool really_async = true) {
if (!Settings::values.deterministic_async_operations && really_async) {
kernel.ReportAsyncState(true);
// We use packaged_task so we can retrieve a std::future to pass to AsyncWakeUpCallback
auto task = std::make_shared<std::packaged_task<void()>>([this, async_section] {
s64 sleep_for = async_section(*this);
this->thread->WakeAfterDelay(sleep_for, true);
});
auto future = task->get_future();
worker.QueueWork([task]() { (*task)(); });
this->SleepClientThread("RunOnThread", std::chrono::nanoseconds(-1),
std::make_shared<AsyncWakeUpCallback<ResultFunctor>>(
kernel, result_function, std::move(future)));
} else {
// Synchronous fallback (same logic as original)
s64 sleep_for = async_section(*this);
if (sleep_for > 0) {
kernel.ReportAsyncState(true);
auto parallel_wakeup = std::make_shared<AsyncWakeUpCallback<ResultFunctor>>(
kernel, result_function, std::move(std::future<void>()));
this->SleepClientThread("RunOnThread", std::chrono::nanoseconds(sleep_for),
parallel_wakeup);
} else {
result_function(*this);
}
}
}
/**
* Resolves a object id from the request command buffer into a pointer to an object. See the
* "HLE handle protocol" section in the class documentation for more details.

View file

@ -16,6 +16,9 @@
#include "common/logging/log.h"
#include "common/serialization/boost_vector.hpp"
#include "core/core.h"
#ifdef ENABLE_GDBSTUB
#include "core/gdbstub/gdbstub.h"
#endif
#include "core/hle/kernel/errors.h"
#include "core/hle/kernel/memory.h"
#include "core/hle/kernel/process.h"
@ -260,9 +263,25 @@ void Process::Run(s32 main_thread_priority, u32 stack_size) {
vm_manager.LogLayout(Common::Log::Level::Debug);
Kernel::SetupMainThread(kernel, codeset->entrypoint, main_thread_priority, SharedFrom(this));
// Pause process at start if flag enabled and we are not a sysmodule
if (Core::System::GetInstance().GetDebugNextProcessFlag() &&
resource_limit->GetCategory() != Kernel::ResourceLimitCategory::Other) {
#ifdef ENABLE_GDBSTUB
if (GDBStub::IsServerEnabled()) {
LOG_INFO(Loader, "Pausing process {} at start", process_id);
SetDebugBreak(true);
}
#endif
Core::System::GetInstance().ClearDebugNextProcessFlag();
}
}
void Process::Exit() {
#ifdef ENABLE_GDBSTUB
GDBStub::OnProcessExit(process_id);
#endif
auto plgldr = Service::PLGLDR::GetService(Core::System::GetInstance());
if (plgldr) {
plgldr->OnProcessExit(*this, kernel);
@ -592,6 +611,42 @@ Result Process::Unmap(VAddr target, VAddr source, u32 size, VMAPermission perms,
return ResultSuccess;
}
std::vector<std::shared_ptr<Kernel::Thread>> Kernel::Process::GetThreadList() {
std::vector<std::shared_ptr<Kernel::Thread>> ret;
for (u32 core = 0; core < Core::GetNumCores(); core++) {
auto thread_list = kernel.GetThreadManager(core).GetThreadList();
for (auto& thread : thread_list) {
if (thread->owner_process.lock().get() == this) {
ret.push_back(thread);
}
}
}
return ret;
}
void Kernel::Process::SetDebugBreak(bool debug_break, std::vector<u32> thread_ids) {
auto thread_list = GetThreadList();
bool needs_reschedule = false;
for (auto& t : thread_list) {
if (!thread_ids.empty()) {
u32 thread_id = t->thread_id;
if (std::find(thread_ids.begin(), thread_ids.end(), thread_id) == thread_ids.end()) {
continue;
}
}
needs_reschedule |= t->SetDebugBreak(debug_break);
}
if (needs_reschedule) {
for (u32 i = 0; i < Core::GetNumCores(); i++) {
Core::GetCore(i).PrepareReschedule();
kernel.GetThreadManager(i).Reschedule();
}
}
}
void Process::FreeAllMemory() {
if (memory_region == nullptr || resource_limit == nullptr) {
return;

View file

@ -1,4 +1,4 @@
// Copyright 2015 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -226,6 +226,10 @@ public:
Result Unmap(VAddr target, VAddr source, u32 size, VMAPermission perms,
bool privileged = false);
std::vector<std::shared_ptr<Kernel::Thread>> GetThreadList();
void SetDebugBreak(bool debug_break, std::vector<u32> thread_ids = {});
private:
void FreeAllMemory();

View file

@ -14,7 +14,9 @@
#include "core/arm/arm_interface.h"
#include "core/core.h"
#include "core/core_timing.h"
#ifdef ENABLE_GDBSTUB
#include "core/gdbstub/hio.h"
#endif
#include "core/hle/kernel/address_arbiter.h"
#include "core/hle/kernel/client_port.h"
#include "core/hle/kernel/client_session.h"
@ -1171,7 +1173,9 @@ void SVC::OutputDebugString(VAddr address, s32 len) {
}
if (len == 0) {
GDBStub::SetHioRequest(system, address);
#ifdef ENABLE_GDBSTUB
GDBStub::SetHioRequest(system, kernel.GetCurrentProcess().get(), address);
#endif
return;
}
@ -2051,12 +2055,16 @@ Result SVC::GetProcessList(s32* process_count, VAddr out_process_array,
}
Result SVC::InvalidateInstructionCacheRange(u32 addr, u32 size) {
system.GetRunningCore().InvalidateCacheRange(addr, size);
for (size_t i = 0; i < system.GetNumCores(); i++) {
system.GetCore(i).InvalidateCacheRange(addr, size);
}
return ResultSuccess;
}
Result SVC::InvalidateEntireInstructionCache() {
system.GetRunningCore().ClearInstructionCache();
for (size_t i = 0; i < system.GetNumCores(); i++) {
system.GetCore(i).ClearInstructionCache();
}
return ResultSuccess;
}

View file

@ -17,6 +17,9 @@
#include "core/arm/arm_interface.h"
#include "core/arm/skyeye_common/armstate.h"
#include "core/core.h"
#ifdef ENABLE_GDBSTUB
#include "core/gdbstub/gdbstub.h"
#endif
#include "core/hle/kernel/errors.h"
#include "core/hle/kernel/kernel.h"
#include "core/hle/kernel/mutex.h"
@ -77,6 +80,7 @@ void Thread::serialize(Archive& ar, const unsigned int file_version) {
}
}
ar & wakeup_callback;
ar & debug_break;
}
SERIALIZE_IMPL(Thread)
@ -133,6 +137,10 @@ void Thread::Stop() {
process->tls_slots[tls_page].reset(tls_slot);
process->resource_limit->Release(ResourceLimitType::Thread, 1);
}
#ifdef ENABLE_GDBSTUB
GDBStub::OnThreadExit(thread_id);
#endif
}
void ThreadManager::SwitchContext(Thread* new_thread) {
@ -191,7 +199,7 @@ Thread* ThreadManager::PopNextReadyThread() {
u32 next_priority{};
next = nullptr;
if (thread && thread->status == ThreadStatus::Running) {
if (thread && thread->status == ThreadStatus::Running && thread->CanSchedule()) {
do {
// We have to do better than the current thread.
// This call returns null when that's not possible.
@ -201,18 +209,18 @@ Thread* ThreadManager::PopNextReadyThread() {
// Otherwise just keep going with the current thread
next = thread;
break;
} else if (!next->can_schedule) {
} else if (!next->CanSchedule()) {
skipped.push_back({next_priority, next});
}
} while (!next->can_schedule);
} while (!next->CanSchedule());
} else {
do {
std::tie(next_priority, next) = ready_queue.pop_first();
if (next && !next->can_schedule) {
if (next && !next->CanSchedule()) {
skipped.push_back({next_priority, next});
}
} while (next && !next->can_schedule);
} while (next && !next->CanSchedule());
}
for (auto it = skipped.rbegin(); it != skipped.rend(); it++) {
@ -537,6 +545,14 @@ VAddr Thread::GetCommandBufferAddress() const {
return GetTLSAddress() + command_header_offset;
}
bool Thread::SetDebugBreak(bool _debug_break) {
if (debug_break == _debug_break) {
return false;
}
debug_break = _debug_break;
return true;
}
CpuLimiter::~CpuLimiter() {}
CpuLimiterMulti::CpuLimiterMulti(Kernel::KernelSystem& _kernel) : kernel(_kernel) {}

View file

@ -365,6 +365,15 @@ public:
return status == ThreadStatus::WaitSynchAll;
}
bool CanSchedule() {
// TODO(PabloMK7): This may not be the proper way
// threads are marked as non-schedulable when they
// are in debug break. Figure out and fix.
return can_schedule && !debug_break;
}
bool SetDebugBreak(bool debug_break);
Core::ARM_Interface::ThreadContext context{};
u32 thread_id;
@ -410,6 +419,7 @@ public:
private:
ThreadManager& thread_manager;
bool debug_break{};
friend class boost::serialization::access;
template <class Archive>

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.
@ -290,9 +290,8 @@ void DSP_DSP::GetHeadphoneStatus(Kernel::HLERequestContext& ctx) {
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
rb.Push(ResultSuccess);
rb.Push(false); /// u8, 0 = not inserted, 1 = inserted
LOG_DEBUG(Service_DSP, "called");
rb.Push(Settings::values.simulate_headphones_plugged
.GetValue()); /// u8, 0 = not inserted, 1 = inserted
}
void DSP_DSP::ForceHeadphoneOut(Kernel::HLERequestContext& ctx) {

View file

@ -1,4 +1,4 @@
// Copyright 2018 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -42,27 +42,54 @@ Directory::~Directory() {}
void Directory::Read(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
u32 count = rp.Pop<u32>();
auto& buffer = rp.PopMappedBuffer();
std::vector<FileSys::Entry> entries(count);
LOG_TRACE(Service_FS, "Read {}: count={}", GetName(), count);
// Number of entries actually read
u32 read = backend->Read(static_cast<u32>(entries.size()), entries.data());
buffer.Write(entries.data(), 0, read * sizeof(FileSys::Entry));
IPC::RequestBuilder rb = rp.MakeBuilder(2, 2);
rb.Push(ResultSuccess);
rb.Push(read);
rb.PushMappedBuffer(buffer);
struct AsyncData {
// Input
u32 count;
// Output
Result ret{0};
u32 read;
Kernel::MappedBuffer* buffer;
};
auto async_data = std::make_shared<AsyncData>();
async_data->count = rp.Pop<u32>();
async_data->buffer = &rp.PopMappedBuffer();
ctx.RunAsync(
[this, async_data](Kernel::HLERequestContext& ctx) {
std::vector<FileSys::Entry> entries(async_data->count);
LOG_TRACE(Service_FS, "Read {}: count={}", GetName(), count);
// Number of entries actually read
async_data->read = backend->Read(static_cast<u32>(entries.size()), entries.data());
async_data->buffer->Write(entries.data(), 0, async_data->read * sizeof(FileSys::Entry));
return 0;
},
[async_data](Kernel::HLERequestContext& ctx) {
IPC::RequestBuilder rb(ctx, 2, 2);
rb.Push(ResultSuccess);
rb.Push(async_data->read);
rb.PushMappedBuffer(*async_data->buffer);
},
Settings::values.async_fs_operations.GetValue());
}
void Directory::Close(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
LOG_TRACE(Service_FS, "Close {}", GetName());
backend->Close();
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(ResultSuccess);
ctx.RunAsync(
[this](Kernel::HLERequestContext& ctx) {
backend->Close();
return 0;
},
[](Kernel::HLERequestContext& ctx) {
IPC::RequestBuilder rb(ctx, 1, 0);
rb.Push(ResultSuccess);
},
Settings::values.async_fs_operations.GetValue());
}
} // namespace Service::FS

View file

@ -75,8 +75,13 @@ void File::Read(Kernel::HLERequestContext& ctx) {
offset, length, backend->GetSize());
}
const bool allows_cache_reads = backend->AllowsCachedReads();
// Conventional reading if the backend does not support cache.
if (!backend->AllowsCachedReads()) {
// Do not use asynchronous operations on file reads, as in most cases
// there are many of them with small sizes. This causes a lot of delay
// due to thread communication overhead.
if (!allows_cache_reads) {
auto& buffer = rp.PopMappedBuffer();
IPC::RequestBuilder rb = rp.MakeBuilder(2, 2);
std::unique_ptr<u8[]> data = std::make_unique_for_overwrite<u8[]>(length);
@ -115,7 +120,8 @@ void File::Read(Kernel::HLERequestContext& ctx) {
async_data->length = length;
async_data->offset = offset;
async_data->cache_ready = backend->CacheReady(offset, length);
if (!async_data->cache_ready) {
const bool really_async = !async_data->cache_ready;
if (really_async) {
async_data->pre_timer = std::chrono::steady_clock::now();
}
@ -161,7 +167,7 @@ void File::Read(Kernel::HLERequestContext& ctx) {
}
rb.PushMappedBuffer(*async_data->buffer);
},
!async_data->cache_ready);
really_async);
}
void File::Write(Kernel::HLERequestContext& ctx) {
@ -186,6 +192,7 @@ void File::Write(Kernel::HLERequestContext& ctx) {
}
bool flush = (flags & 0xFF) != 0, update_timestamp = (flags & 0xFF00) != 0;
// Do not use asynchronous fs operations here for the same reason as File::Read.
if (!backend->AllowsCachedReads()) {
std::vector<u8> data(length);
buffer.Read(data.data(), 0, data.size());
@ -274,7 +281,7 @@ void File::SetSize(Kernel::HLERequestContext& ctx) {
return;
}
if (!backend->AllowsCachedReads()) {
if (!backend->AllowsCachedReads() && !Settings::values.async_fs_operations) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
file->size = size;
backend->SetSize(size);
@ -303,7 +310,7 @@ void File::Close(Kernel::HLERequestContext& ctx) {
LOG_WARNING(Service_FS, "Closing File backend but {} clients still connected",
connected_sessions.size());
if (!backend->AllowsCachedReads()) {
if (!backend->AllowsCachedReads() && !Settings::values.async_fs_operations) {
backend->Close();
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(ResultSuccess);
@ -334,7 +341,7 @@ void File::Flush(Kernel::HLERequestContext& ctx) {
return;
}
if (!backend->AllowsCachedReads()) {
if (!backend->AllowsCachedReads() && !Settings::values.async_fs_operations) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
backend->Flush();
rb.Push(ResultSuccess);

View file

@ -67,7 +67,7 @@ void FS_USER::OpenFile(Kernel::HLERequestContext& ctx) {
LOG_DEBUG(Service_FS, "path={}, mode={} attrs={}", file_path.DebugStr(), mode.hex, attributes);
if (!archives.ArchiveIsSlow(archive_handle)) {
if (!archives.ArchiveIsSlow(archive_handle) && !Settings::values.async_fs_operations) {
const auto [file_res, open_timeout_ns] =
archives.OpenFileFromArchive(archive_handle, file_path, mode, attributes);
IPC::RequestBuilder rb = rp.MakeBuilder(1, 2);
@ -100,7 +100,8 @@ void FS_USER::OpenFile(Kernel::HLERequestContext& ctx) {
async_data->attributes = attributes;
async_data->pre_timer = std::chrono::steady_clock::now();
ctx.RunAsync(
ctx.RunOnThreadWorker(
fs_async_worker,
[this, async_data](Kernel::HLERequestContext& ctx) {
async_data->file =
archives.OpenFileFromArchive(async_data->archive_handle, async_data->file_path,
@ -151,7 +152,7 @@ void FS_USER::OpenFileDirectly(Kernel::HLERequestContext& ctx) {
u64 program_id = GetSessionData(ctx.Session())->program_id;
if (!archives.ArchiveIsSlow(archive_id)) {
if (!archives.ArchiveIsSlow(archive_id) && !Settings::values.async_fs_operations) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 2);
ResultVal<ArchiveHandle> archive_handle =
@ -203,7 +204,8 @@ void FS_USER::OpenFileDirectly(Kernel::HLERequestContext& ctx) {
async_data->attributes = attributes;
async_data->pre_timer = std::chrono::steady_clock::now();
ctx.RunAsync(
ctx.RunOnThreadWorker(
fs_async_worker,
[this, async_data](Kernel::HLERequestContext& ctx) {
async_data->archive_handle = archives.OpenArchive(
async_data->archive_id, async_data->archive_path, async_data->program_id);
@ -259,7 +261,7 @@ void FS_USER::DeleteFile(Kernel::HLERequestContext& ctx) {
LOG_DEBUG(Service_FS, "type={} size={} data={}", filename_type, filename_size,
file_path.DebugStr());
if (!archives.ArchiveIsSlow(archive_handle)) {
if (!archives.ArchiveIsSlow(archive_handle) && !Settings::values.async_fs_operations) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(archives.DeleteFileFromArchive(archive_handle, file_path));
return;
@ -275,7 +277,8 @@ void FS_USER::DeleteFile(Kernel::HLERequestContext& ctx) {
async_data->archive_handle = archive_handle;
async_data->file_path = file_path;
ctx.RunAsync(
ctx.RunOnThreadWorker(
fs_async_worker,
[this, async_data](Kernel::HLERequestContext& ctx) {
async_data->res =
archives.DeleteFileFromArchive(async_data->archive_handle, async_data->file_path);
@ -312,7 +315,7 @@ void FS_USER::RenameFile(Kernel::HLERequestContext& ctx) {
dest_filename_size, dest_file_path.DebugStr());
if (!archives.ArchiveIsSlow(src_archive_handle) &&
!archives.ArchiveIsSlow(dest_archive_handle)) {
!archives.ArchiveIsSlow(dest_archive_handle) && !Settings::values.async_fs_operations) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(archives.RenameFileBetweenArchives(src_archive_handle, src_file_path,
dest_archive_handle, dest_file_path));
@ -333,7 +336,8 @@ void FS_USER::RenameFile(Kernel::HLERequestContext& ctx) {
async_data->dest_archive_handle = dest_archive_handle;
async_data->dest_file_path = dest_file_path;
ctx.RunAsync(
ctx.RunOnThreadWorker(
fs_async_worker,
[this, async_data](Kernel::HLERequestContext& ctx) {
async_data->res = archives.RenameFileBetweenArchives(
async_data->src_archive_handle, async_data->src_file_path,
@ -362,7 +366,7 @@ void FS_USER::DeleteDirectory(Kernel::HLERequestContext& ctx) {
LOG_DEBUG(Service_FS, "type={} size={} data={}", dirname_type, dirname_size,
dir_path.DebugStr());
if (!archives.ArchiveIsSlow(archive_handle)) {
if (!archives.ArchiveIsSlow(archive_handle) && !Settings::values.async_fs_operations) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(archives.DeleteDirectoryFromArchive(archive_handle, dir_path));
return;
@ -378,7 +382,8 @@ void FS_USER::DeleteDirectory(Kernel::HLERequestContext& ctx) {
async_data->archive_handle = archive_handle;
async_data->dir_path = dir_path;
ctx.RunAsync(
ctx.RunOnThreadWorker(
fs_async_worker,
[this, async_data](Kernel::HLERequestContext& ctx) {
async_data->res = archives.DeleteDirectoryFromArchive(async_data->archive_handle,
async_data->dir_path);
@ -406,7 +411,7 @@ void FS_USER::DeleteDirectoryRecursively(Kernel::HLERequestContext& ctx) {
LOG_DEBUG(Service_FS, "type={} size={} data={}", dirname_type, dirname_size,
dir_path.DebugStr());
if (!archives.ArchiveIsSlow(archive_handle)) {
if (!archives.ArchiveIsSlow(archive_handle) && !Settings::values.async_fs_operations) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(archives.DeleteDirectoryRecursivelyFromArchive(archive_handle, dir_path));
return;
@ -422,7 +427,8 @@ void FS_USER::DeleteDirectoryRecursively(Kernel::HLERequestContext& ctx) {
async_data->archive_handle = archive_handle;
async_data->dir_path = dir_path;
ctx.RunAsync(
ctx.RunOnThreadWorker(
fs_async_worker,
[this, async_data](Kernel::HLERequestContext& ctx) {
async_data->res = archives.DeleteDirectoryRecursivelyFromArchive(
async_data->archive_handle, async_data->dir_path);
@ -452,7 +458,7 @@ void FS_USER::CreateFile(Kernel::HLERequestContext& ctx) {
LOG_DEBUG(Service_FS, "type={} attributes={} size={:x} data={}", filename_type, attributes,
file_size, file_path.DebugStr());
if (!archives.ArchiveIsSlow(archive_handle)) {
if (!archives.ArchiveIsSlow(archive_handle) && !Settings::values.async_fs_operations) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(archives.CreateFileInArchive(archive_handle, file_path, file_size, attributes));
return;
@ -472,7 +478,8 @@ void FS_USER::CreateFile(Kernel::HLERequestContext& ctx) {
async_data->file_size = file_size;
async_data->attributes = attributes;
ctx.RunAsync(
ctx.RunOnThreadWorker(
fs_async_worker,
[this, async_data](Kernel::HLERequestContext& ctx) {
async_data->res =
archives.CreateFileInArchive(async_data->archive_handle, async_data->file_path,
@ -500,7 +507,7 @@ void FS_USER::CreateDirectory(Kernel::HLERequestContext& ctx) {
LOG_DEBUG(Service_FS, "type={} size={} data={}", dirname_type, dirname_size,
dir_path.DebugStr());
if (!archives.ArchiveIsSlow(archive_handle)) {
if (!archives.ArchiveIsSlow(archive_handle) && !Settings::values.async_fs_operations) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(archives.CreateDirectoryFromArchive(archive_handle, dir_path, attributes));
return;
@ -518,7 +525,8 @@ void FS_USER::CreateDirectory(Kernel::HLERequestContext& ctx) {
async_data->dir_path = dir_path;
async_data->attributes = attributes;
ctx.RunAsync(
ctx.RunOnThreadWorker(
fs_async_worker,
[this, async_data](Kernel::HLERequestContext& ctx) {
async_data->res = archives.CreateDirectoryFromArchive(
async_data->archive_handle, async_data->dir_path, async_data->attributes);
@ -554,7 +562,7 @@ void FS_USER::RenameDirectory(Kernel::HLERequestContext& ctx) {
dest_dirname_size, dest_dir_path.DebugStr());
if (!archives.ArchiveIsSlow(src_archive_handle) &&
!archives.ArchiveIsSlow(dest_archive_handle)) {
!archives.ArchiveIsSlow(dest_archive_handle) && !Settings::values.async_fs_operations) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(archives.RenameDirectoryBetweenArchives(src_archive_handle, src_dir_path,
dest_archive_handle, dest_dir_path));
@ -575,7 +583,8 @@ void FS_USER::RenameDirectory(Kernel::HLERequestContext& ctx) {
async_data->dest_archive_handle = dest_archive_handle;
async_data->dest_dir_path = dest_dir_path;
ctx.RunAsync(
ctx.RunOnThreadWorker(
fs_async_worker,
[this, async_data](Kernel::HLERequestContext& ctx) {
async_data->res = archives.RenameDirectoryBetweenArchives(
async_data->src_archive_handle, async_data->src_dir_path,
@ -602,7 +611,7 @@ void FS_USER::OpenDirectory(Kernel::HLERequestContext& ctx) {
LOG_DEBUG(Service_FS, "type={} size={} data={}", dirname_type, dirname_size,
dir_path.DebugStr());
if (!archives.ArchiveIsSlow(archive_handle)) {
if (!archives.ArchiveIsSlow(archive_handle) && Settings::values.async_fs_operations) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 2);
ResultVal<std::shared_ptr<Directory>> dir_res =
archives.OpenDirectoryFromArchive(archive_handle, dir_path);
@ -630,7 +639,8 @@ void FS_USER::OpenDirectory(Kernel::HLERequestContext& ctx) {
async_data->archive_handle = archive_handle;
async_data->dir_path = dir_path;
ctx.RunAsync(
ctx.RunOnThreadWorker(
fs_async_worker,
[this, async_data](Kernel::HLERequestContext& ctx) {
async_data->dir_res =
archives.OpenDirectoryFromArchive(async_data->archive_handle, async_data->dir_path);
@ -669,7 +679,7 @@ void FS_USER::OpenArchive(Kernel::HLERequestContext& ctx) {
u64 program_id = slot->program_id;
// Conventional opening
if (!archives.ArchiveIsSlow(archive_id)) {
if (!archives.ArchiveIsSlow(archive_id) && !Settings::values.async_fs_operations) {
IPC::RequestBuilder rb = rp.MakeBuilder(3, 0);
const ResultVal<ArchiveHandle> handle =
archives.OpenArchive(archive_id, archive_path, program_id);
@ -699,7 +709,8 @@ void FS_USER::OpenArchive(Kernel::HLERequestContext& ctx) {
async_data->archive_path = archive_path;
async_data->program_id = program_id;
ctx.RunAsync(
ctx.RunOnThreadWorker(
fs_async_worker,
[this, async_data](Kernel::HLERequestContext& ctx) {
async_data->handle = archives.OpenArchive(
async_data->archive_id, async_data->archive_path, async_data->program_id);
@ -727,7 +738,7 @@ void FS_USER::ControlArchive(Kernel::HLERequestContext& ctx) {
const auto input_size = rp.Pop<u32>();
const auto output_size = rp.Pop<u32>();
if (!archives.ArchiveIsSlow(archive_handle)) {
if (!archives.ArchiveIsSlow(archive_handle) && !Settings::values.async_fs_operations) {
auto input = rp.PopMappedBuffer();
auto output = rp.PopMappedBuffer();
std::vector<u8> in_data(input_size);
@ -766,7 +777,8 @@ void FS_USER::ControlArchive(Kernel::HLERequestContext& ctx) {
async_data->in_buffer = &rp.PopMappedBuffer();
async_data->out_buffer = &rp.PopMappedBuffer();
ctx.RunAsync(
ctx.RunOnThreadWorker(
fs_async_worker,
[this, async_data](Kernel::HLERequestContext& ctx) {
std::vector<u8> in_data(async_data->in_size);
async_data->in_buffer->Read(in_data.data(), 0, in_data.size());
@ -792,7 +804,7 @@ void FS_USER::CloseArchive(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
const auto archive_handle = rp.PopRaw<ArchiveHandle>();
if (!archives.ArchiveIsSlow(archive_handle)) {
if (!archives.ArchiveIsSlow(archive_handle) && !Settings::values.async_fs_operations) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(archives.CloseArchive(archive_handle));
return;
@ -806,7 +818,8 @@ void FS_USER::CloseArchive(Kernel::HLERequestContext& ctx) {
auto async_data = std::make_shared<AsyncData>();
async_data->handle = archive_handle;
ctx.RunAsync(
ctx.RunOnThreadWorker(
fs_async_worker,
[this, async_data](Kernel::HLERequestContext& ctx) {
async_data->res = archives.CloseArchive(async_data->handle);
return 0;
@ -1395,7 +1408,7 @@ void FS_USER::ObsoletedSetSaveDataSecureValue(Kernel::HLERequestContext& ctx) {
const u32 unique_id = rp.Pop<u32>();
const u8 title_variation = rp.Pop<u8>();
if (!secure_value_backend->BackendIsSlow()) {
if (!secure_value_backend->BackendIsSlow() && !Settings::values.async_fs_operations) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(secure_value_backend->ObsoletedSetSaveDataSecureValue(unique_id, title_variation,
secure_value_slot, value));
@ -1416,7 +1429,8 @@ void FS_USER::ObsoletedSetSaveDataSecureValue(Kernel::HLERequestContext& ctx) {
async_data->unique_id = unique_id;
async_data->title_variation = title_variation;
ctx.RunAsync(
ctx.RunOnThreadWorker(
fs_async_worker,
[this, async_data](Kernel::HLERequestContext& ctx) {
async_data->res = secure_value_backend->ObsoletedSetSaveDataSecureValue(
async_data->unique_id, async_data->title_variation, async_data->secure_value_slot,
@ -1436,7 +1450,7 @@ void FS_USER::ObsoletedGetSaveDataSecureValue(Kernel::HLERequestContext& ctx) {
const u32 unique_id = rp.Pop<u32>();
const u8 title_variation = rp.Pop<u8>();
if (!secure_value_backend->BackendIsSlow()) {
if (!secure_value_backend->BackendIsSlow() && !Settings::values.async_fs_operations) {
auto res = secure_value_backend->ObsoletedGetSaveDataSecureValue(unique_id, title_variation,
secure_value_slot);
if (res.Failed()) {
@ -1463,7 +1477,8 @@ void FS_USER::ObsoletedGetSaveDataSecureValue(Kernel::HLERequestContext& ctx) {
async_data->unique_id = unique_id;
async_data->title_variation = title_variation;
ctx.RunAsync(
ctx.RunOnThreadWorker(
fs_async_worker,
[this, async_data](Kernel::HLERequestContext& ctx) {
async_data->res = secure_value_backend->ObsoletedGetSaveDataSecureValue(
async_data->unique_id, async_data->title_variation, async_data->secure_value_slot);
@ -1511,7 +1526,7 @@ void FS_USER::SetThisSaveDataSecureValue(Kernel::HLERequestContext& ctx) {
const u32 secure_value_slot = rp.Pop<u32>();
const u64 value = rp.Pop<u64>();
if (!secure_value_backend->BackendIsSlow()) {
if (!secure_value_backend->BackendIsSlow() && !Settings::values.async_fs_operations) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(secure_value_backend->SetThisSaveDataSecureValue(secure_value_slot, value));
return;
@ -1527,7 +1542,8 @@ void FS_USER::SetThisSaveDataSecureValue(Kernel::HLERequestContext& ctx) {
async_data->value = value;
async_data->secure_value_slot = secure_value_slot;
ctx.RunAsync(
ctx.RunOnThreadWorker(
fs_async_worker,
[this, async_data](Kernel::HLERequestContext& ctx) {
async_data->res = secure_value_backend->SetThisSaveDataSecureValue(
async_data->secure_value_slot, async_data->value);
@ -1544,7 +1560,7 @@ void FS_USER::GetThisSaveDataSecureValue(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
const u32 secure_value_slot = rp.Pop<u32>();
if (!secure_value_backend->BackendIsSlow()) {
if (!secure_value_backend->BackendIsSlow() && !Settings::values.async_fs_operations) {
auto res = secure_value_backend->GetThisSaveDataSecureValue(secure_value_slot);
if (res.Failed()) {
@ -1569,7 +1585,8 @@ void FS_USER::GetThisSaveDataSecureValue(Kernel::HLERequestContext& ctx) {
auto async_data = std::make_shared<AsyncData>();
async_data->secure_value_slot = secure_value_slot;
ctx.RunAsync(
ctx.RunOnThreadWorker(
fs_async_worker,
[this, async_data](Kernel::HLERequestContext& ctx) {
async_data->res =
secure_value_backend->GetThisSaveDataSecureValue(async_data->secure_value_slot);
@ -1599,7 +1616,7 @@ void FS_USER::SetSaveDataSecureValue(Kernel::HLERequestContext& ctx) {
const u64 value = rp.Pop<u64>();
const bool flush = rp.Pop<bool>();
if (!archives.ArchiveIsSlow(archive_handle)) {
if (!archives.ArchiveIsSlow(archive_handle) && !Settings::values.async_fs_operations) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(archives.SetSaveDataSecureValue(archive_handle, secure_value_slot, value, flush));
@ -1620,7 +1637,8 @@ void FS_USER::SetSaveDataSecureValue(Kernel::HLERequestContext& ctx) {
async_data->secure_value_slot = secure_value_slot;
async_data->flush = flush;
ctx.RunAsync(
ctx.RunOnThreadWorker(
fs_async_worker,
[this, async_data](Kernel::HLERequestContext& ctx) {
async_data->res = archives.SetSaveDataSecureValue(async_data->archive_handle,
async_data->secure_value_slot,
@ -1639,7 +1657,7 @@ void FS_USER::GetSaveDataSecureValue(Kernel::HLERequestContext& ctx) {
const auto archive_handle = rp.PopRaw<ArchiveHandle>();
const u32 secure_value_slot = rp.Pop<u32>();
if (!archives.ArchiveIsSlow(archive_handle)) {
if (!archives.ArchiveIsSlow(archive_handle) && !Settings::values.async_fs_operations) {
auto res = archives.GetSaveDataSecureValue(archive_handle, secure_value_slot);
if (res.Failed()) {
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
@ -1665,7 +1683,8 @@ void FS_USER::GetSaveDataSecureValue(Kernel::HLERequestContext& ctx) {
async_data->archive_handle = archive_handle;
async_data->secure_value_slot = secure_value_slot;
ctx.RunAsync(
ctx.RunOnThreadWorker(
fs_async_worker,
[this, async_data](Kernel::HLERequestContext& ctx) {
async_data->res = archives.GetSaveDataSecureValue(async_data->archive_handle,
async_data->secure_value_slot);
@ -1900,6 +1919,9 @@ FS_USER::FS_USER(Core::System& system)
template <class Archive>
void Service::FS::FS_USER::serialize(Archive& ar, const unsigned int) {
DEBUG_SERIALIZATION_POINT;
if (!Archive::is_loading::value) {
fs_async_worker.WaitForRequests();
}
ar& boost::serialization::base_object<Kernel::SessionRequestHandler>(*this);
ar & priority;
ar & secure_value_backend;

View file

@ -49,6 +49,9 @@ private:
class FS_USER final : public ServiceFramework<FS_USER, ClientSlot> {
public:
explicit FS_USER(Core::System& system);
~FS_USER() {
fs_async_worker.WaitForRequests();
}
// On real HW this is part of FSReg (FSReg:Register). But since that module is only used by
// loader and pm, which we HLEed, we can just directly use it here
@ -756,6 +759,8 @@ private:
std::shared_ptr<FileSys::SecureValueBackend> secure_value_backend;
Common::ThreadWorker fs_async_worker{1, "FSUSER_Worker"};
template <class Archive>
void serialize(Archive& ar, const unsigned int);
friend class boost::serialization::access;

View file

@ -12,6 +12,7 @@
#include "common/hacks/hack_manager.h"
#include "common/settings.h"
#include "core/core.h"
#include "core/core_timing.h"
#include "core/hle/ipc_helpers.h"
#include "core/hle/kernel/shared_memory.h"
#include "core/hle/kernel/shared_page.h"
@ -295,6 +296,23 @@ void GSP_GPU::SetAxiConfigQoSMode(Kernel::HLERequestContext& ctx) {
LOG_DEBUG(Service_GSP, "(STUBBED) called mode=0x{:08X}", mode);
}
void GSP_GPU::SetPerfLogMode(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
bool enabled = rp.Pop<u32>() != 0;
perf_recorder.SetEnabled(enabled);
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
rb.Push(ResultSuccess);
}
void GSP_GPU::GetPerfLog(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
IPC::RequestBuilder rb = rp.MakeBuilder(15, 0);
rb.PushRaw(perf_recorder.GetResults());
}
void GSP_GPU::RegisterInterruptRelayQueue(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
u32 flags = rp.Pop<u32>();
@ -337,7 +355,124 @@ void GSP_GPU::UnregisterInterruptRelayQueue(Kernel::HLERequestContext& ctx) {
LOG_DEBUG(Service_GSP, "called");
}
void GSP_GPU::SignalInterruptForThread(InterruptId interrupt_id, u32 thread_id) {
// Uncomment the following line to display the average delay calculated for every frame.
// #define SHOW_AVERAGE_TIME_PER_FRAME
void GSP_GPU::SignalInterruptForThread(InterruptId interrupt_id, u32 thread_id, u64 wait_delay_ns) {
// Every gsp request takes a constant amount of time to be
// processed and control returned to the application. This
// time is estimated below.
static constexpr u64 sync_delay_nanoseconds = 300 * 1000;
// For a reason not yet understood, Super Mario 3D Land hangs on a white screen after the title
// screen when any of the save slots have the completion star icons. This is in some way related
// to the timings of texture copy commands, and gets fixed if we increase the amount of time
// those take. This issue may be resolved as timings become more accurate in the future.
static constexpr u64 sync_delay_nanoseconds_delayed_texcopy = 1000 * 1000;
#ifdef SHOW_AVERAGE_TIME_PER_FRAME
auto track_average = [&](bool is_vsync) {
using clock = std::chrono::steady_clock;
static uint64_t total_ns = 0;
static uint64_t sample_count = 0;
static auto last_print = clock::now();
if (!is_vsync) {
total_ns += wait_delay_ns;
++sample_count;
}
auto now = clock::now();
if (now - last_print >= std::chrono::milliseconds(250)) {
double average_ns =
(sample_count > 0)
? (static_cast<double>(total_ns) / static_cast<double>(sample_count))
: 0;
LOG_INFO(Service_GSP, "Average delay milliseconds per frame: {}",
average_ns / 1000000.f);
total_ns = 0;
sample_count = 0;
last_print = now;
}
};
#endif
// Signal VBlank interrupt immediately, this interrupt is signaled from
// an scheduler event so it already has the proper timing.
if (interrupt_id == InterruptId::PDC0 || interrupt_id == InterruptId::PDC1) {
#ifdef SHOW_AVERAGE_TIME_PER_FRAME
track_average(true);
#endif
if (perf_recorder.IsEnabled()) {
constexpr u64 nanoseconds_per_frame = static_cast<u64>(
((static_cast<double>(VideoCore::FRAME_TICKS) / BASE_CLOCK_RATE_ARM11) * 1e9));
perf_recorder.UpdateTime(interrupt_id, nanoseconds_per_frame);
}
ProcessPendingInterruptImpl(interrupt_id, thread_id);
return;
}
if (perf_recorder.IsEnabled()) {
perf_recorder.UpdateTime(interrupt_id, wait_delay_ns);
}
if (Settings::values.simulate_3ds_gpu_timings.GetValue()) {
if (delay_texture_copy_completion) {
wait_delay_ns += (interrupt_id == InterruptId::PPF)
? sync_delay_nanoseconds_delayed_texcopy
: sync_delay_nanoseconds;
} else {
wait_delay_ns += sync_delay_nanoseconds;
}
} else {
if (delay_texture_copy_completion && interrupt_id == InterruptId::PPF) {
wait_delay_ns += sync_delay_nanoseconds_delayed_texcopy;
} else {
wait_delay_ns = 0;
}
}
#ifdef SHOW_AVERAGE_TIME_PER_FRAME
track_average(false);
#endif
if (wait_delay_ns) {
size_t pending_interrupt_id =
pending_interrupts.Push(std::make_pair(interrupt_id, thread_id));
if (pending_interrupt_id == std::numeric_limits<size_t>::max()) {
LOG_ERROR(Service_GSP, "Pending interrupts queue is full");
ProcessPendingInterruptImpl(interrupt_id, thread_id);
} else {
system.Kernel().timing.ScheduleEvent(nsToCycles(wait_delay_ns),
SignalInterruptEventType,
static_cast<uintptr_t>(pending_interrupt_id));
}
} else {
ProcessPendingInterruptImpl(interrupt_id, thread_id);
}
}
void Service::GSP::GSP_GPU::ProcessPendingInterrupt(size_t pending_interrupt_id) {
auto pending_interrupt = pending_interrupts.Pop(pending_interrupt_id);
if (!pending_interrupt.has_value()) {
return;
}
const auto& [interrupt_id, thread_id] = *pending_interrupt;
ProcessPendingInterruptImpl(interrupt_id, thread_id);
}
void Service::GSP::GSP_GPU::ProcessPendingInterruptImpl(InterruptId interrupt_id, u32 thread_id) {
SessionData* session_data = FindRegisteredThreadData(thread_id);
if (!session_data) {
return;
@ -349,32 +484,55 @@ void GSP_GPU::SignalInterruptForThread(InterruptId interrupt_id, u32 thread_id)
return;
}
const bool is_pdc = interrupt_id == InterruptId::PDC0 || interrupt_id == InterruptId::PDC1;
auto* interrupt_relay_queue = GetInterruptRelayQueue(thread_id);
u8 next = interrupt_relay_queue->index;
next += interrupt_relay_queue->number_interrupts;
next = next % 0x34; // 0x34 is the number of interrupt slots
interrupt_relay_queue->number_interrupts += 1;
auto queue_interrupt = [&]() {
if (interrupt_relay_queue->number_interrupts >= InterruptRelayQueue::max_slots) {
interrupt_relay_queue->error_code = InterruptRelayQueue::queue_full_error;
} else {
u8 next = interrupt_relay_queue->index;
next += interrupt_relay_queue->number_interrupts;
next %= InterruptRelayQueue::max_slots;
interrupt_relay_queue->slot[next] = interrupt_id;
interrupt_relay_queue->error_code = 0x0; // No error
interrupt_relay_queue->number_interrupts += 1;
interrupt_relay_queue->slot[next] = interrupt_id;
interrupt_event->Signal();
}
};
if (is_pdc) {
if (!interrupt_relay_queue->ignore_pdc.Value()) {
if (interrupt_relay_queue->number_interrupts >=
InterruptRelayQueue::stop_queuing_pdc_threeshold) {
if (interrupt_id == InterruptId::PDC0) {
interrupt_relay_queue->missed_PDC0++;
} else {
interrupt_relay_queue->missed_PDC1++;
}
} else {
queue_interrupt();
}
}
// Update framebuffer information if requested
const s32 screen_id = (interrupt_id == InterruptId::PDC0) ? 0 : 1;
// Update framebuffer information if requested
const s32 screen_id = (interrupt_id == InterruptId::PDC0) ? 0
: (interrupt_id == InterruptId::PDC1) ? 1
: -1;
if (screen_id != -1) {
auto* info = GetFrameBufferInfo(thread_id, screen_id);
if (info->is_dirty) {
system.GPU().SetBufferSwap(screen_id, info->framebuffer_info[info->index]);
info->is_dirty.Assign(false);
}
}
interrupt_event->Signal();
} else {
queue_interrupt();
}
}
void GSP_GPU::SignalInterrupt(InterruptId interrupt_id) {
void GSP_GPU::SignalInterrupt(InterruptId interrupt_id, u64 wait_delay_ns) {
if (nullptr == shared_memory) {
LOG_WARNING(Service_GSP, "cannot synchronize until GSP shared memory has been created!");
return;
@ -385,7 +543,7 @@ void GSP_GPU::SignalInterrupt(InterruptId interrupt_id) {
// right), but the PDC0/1 interrupts are signaled for every registered thread.
if (interrupt_id == InterruptId::PDC0 || interrupt_id == InterruptId::PDC1) {
for (u32 thread_id = 0; thread_id < MaxGSPThreads; ++thread_id) {
SignalInterruptForThread(interrupt_id, thread_id);
SignalInterruptForThread(interrupt_id, thread_id, wait_delay_ns);
}
return;
}
@ -395,7 +553,7 @@ void GSP_GPU::SignalInterrupt(InterruptId interrupt_id) {
return;
}
SignalInterruptForThread(interrupt_id, active_thread_id);
SignalInterruptForThread(interrupt_id, active_thread_id, wait_delay_ns);
}
void GSP_GPU::SetLcdForceBlack(Kernel::HLERequestContext& ctx) {
@ -692,6 +850,11 @@ Result GSP_GPU::AcquireGpuRight(const Kernel::HLERequestContext& ctx,
Common::Hacks::HackType::REQUIRES_SHADER_FIXUP, process->codeset->program_id,
Common::Hacks::HackAllowMode::DISALLOW) != Common::Hacks::HackAllowMode::DISALLOW;
delay_texture_copy_completion =
Common::Hacks::hack_manager.GetHackAllowMode(
Common::Hacks::HackType::DELAY_TEXTURE_COPY_COMPLETION, process->codeset->program_id,
Common::Hacks::HackAllowMode::DISALLOW) != Common::Hacks::HackAllowMode::DISALLOW;
auto& gpu = system.GPU();
gpu.ApplyPerProgramSettings(process->codeset->program_id);
gpu.GetRightEyeDisabler().SetEnabled(right_eye_disable_allow);
@ -818,6 +981,9 @@ void GSP_GPU::serialize(Archive& ar, const unsigned int) {
ar & first_initialization;
ar & used_thread_ids;
ar & saved_vram;
ar & delay_texture_copy_completion;
ar & pending_interrupts;
ar & perf_recorder;
}
SERIALIZE_IMPL(GSP_GPU)
@ -840,8 +1006,8 @@ GSP_GPU::GSP_GPU(Core::System& system) : ServiceFramework("gsp::Gpu", 4), system
{0x000E, nullptr, "SetTextureCopy"},
{0x000F, nullptr, "SetMemoryFill"},
{0x0010, &GSP_GPU::SetAxiConfigQoSMode, "SetAxiConfigQoSMode"},
{0x0011, nullptr, "SetPerfLogMode"},
{0x0012, nullptr, "GetPerfLog"},
{0x0011, &GSP_GPU::SetPerfLogMode, "SetPerfLogMode"},
{0x0012, &GSP_GPU::GetPerfLog, "GetPerfLog"},
{0x0013, &GSP_GPU::RegisterInterruptRelayQueue, "RegisterInterruptRelayQueue"},
{0x0014, &GSP_GPU::UnregisterInterruptRelayQueue, "UnregisterInterruptRelayQueue"},
{0x0015, &GSP_GPU::TryAcquireRight, "TryAcquireRight"},
@ -866,6 +1032,11 @@ GSP_GPU::GSP_GPU(Core::System& system) : ServiceFramework("gsp::Gpu", 4), system
Kernel::MemoryRegion::BASE, "GSP:SharedMemory")
.Unwrap();
SignalInterruptEventType = system.Kernel().timing.RegisterEvent(
"GSPPendingInterrupt", [this](uintptr_t arg, s64 cycle_late) {
ProcessPendingInterrupt(static_cast<size_t>(arg));
});
first_initialization = true;
};

View file

@ -4,8 +4,10 @@
#pragma once
#include <array>
#include <cstddef>
#include <memory>
#include <optional>
#include <string>
#include <boost/optional/optional.hpp>
#include <boost/serialization/export.hpp>
@ -104,7 +106,7 @@ public:
* Signals that the specified interrupt type has occurred to userland code
* @param interrupt_id ID of interrupt that is being signalled
*/
void SignalInterrupt(InterruptId interrupt_id);
void SignalInterrupt(InterruptId interrupt_id, u64 wait_delay_ns);
/**
* Retrieves the framebuffer info stored in the GSP shared memory for the
@ -143,7 +145,11 @@ private:
* @param interrupt_id ID of interrupt that is being signalled.
* @param thread_id GSP thread that will receive the interrupt.
*/
void SignalInterruptForThread(InterruptId interrupt_id, u32 thread_id);
void SignalInterruptForThread(InterruptId interrupt_id, u32 thread_id, u64 wait_delay_ns);
void ProcessPendingInterrupt(size_t pending_interrupt_id);
void ProcessPendingInterruptImpl(InterruptId interrupt_id, u32 thread_id);
/**
* GSP_GPU::WriteHWRegs service function
@ -240,6 +246,10 @@ private:
*/
void SetAxiConfigQoSMode(Kernel::HLERequestContext& ctx);
void SetPerfLogMode(Kernel::HLERequestContext& ctx);
void GetPerfLog(Kernel::HLERequestContext& ctx);
/**
* GSP_GPU::RegisterInterruptRelayQueue service function
* Inputs:
@ -405,6 +415,118 @@ private:
/// Thread ids currently in use by the sessions connected to the GSPGPU service.
std::array<bool, MaxGSPThreads> used_thread_ids{};
/// The current thread needs a longer emulated texture copy completion
bool delay_texture_copy_completion{};
class PendingInterruptArray {
public:
PendingInterruptArray() {
for (size_t i = 0; i < array_size; i++) {
elements[i].first = InterruptId::COUNT;
}
}
size_t Push(const std::pair<InterruptId, u32> elem) {
if (elements[head].first != InterruptId::COUNT) {
// If the head position is occupied, the queue is full
return std::numeric_limits<size_t>::max();
}
elements[head] = elem;
size_t index = head;
head = (head + 1) % array_size;
return index;
}
std::optional<std::pair<InterruptId, u32>> Pop(size_t at) {
if (at >= array_size || elements[at].first == InterruptId::COUNT) {
// Invalid index or already free
return std::nullopt;
}
std::pair<InterruptId, u32> value = elements[at];
elements[at].first = InterruptId::COUNT;
return value;
}
private:
static constexpr size_t array_size = 512;
size_t head = 0;
std::array<std::pair<InterruptId, u32>, array_size> elements;
template <class Archive>
void serialize(Archive& ar, const unsigned int) {
ar & elements;
ar & head;
}
friend class boost::serialization::access;
};
class PerformanceRecorder {
public:
struct PerformanceEntry {
u32 delta_time{};
u32 sum_time{};
template <class Archive>
void serialize(Archive& ar, const unsigned int) {
ar & delta_time;
ar & sum_time;
}
friend class boost::serialization::access;
};
using PerfArray = std::array<PerformanceEntry, static_cast<u8>(InterruptId::COUNT)>;
PerformanceRecorder() = default;
void Reset() {
entries.fill({});
}
bool IsEnabled() {
return enabled;
}
void SetEnabled(bool _enabled) {
enabled = _enabled;
if (enabled) {
Reset();
}
}
void UpdateTime(InterruptId id, u64 nanoseconds) {
// These counters may overflow, which is normal.
entries[static_cast<u8>(id)].delta_time = static_cast<u32>(nanoseconds);
entries[static_cast<u8>(id)].sum_time += static_cast<u32>(nanoseconds);
}
const PerfArray& GetResults() {
return entries;
}
private:
PerfArray entries{};
bool enabled{};
template <class Archive>
void serialize(Archive& ar, const unsigned int) {
ar & entries;
ar & enabled;
}
friend class boost::serialization::access;
};
// This array is only needed to keep track of delayed notifications and simulate the GPU
// taking some time to finish the work, it doesn't exist on real hardware.
PendingInterruptArray pending_interrupts;
PerformanceRecorder perf_recorder;
Core::TimingEventType* SignalInterruptEventType = nullptr;
friend class SessionData;
template <class Archive>

View file

@ -1,10 +1,11 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <functional>
#include "common/bit_field.h"
#include "common/common_types.h"
namespace Service::GSP {
@ -18,25 +19,35 @@ enum class InterruptId : u8 {
PPF = 0x04,
P3D = 0x05,
DMA = 0x06,
COUNT,
};
/// GSP thread interrupt relay queue
struct InterruptRelayQueue {
static constexpr size_t max_slots = 0x34;
static constexpr size_t stop_queuing_pdc_threeshold = 0x20;
static constexpr u8 queue_full_error = 0x1;
// Index of last interrupt in the queue
u8 index;
// Number of interrupts remaining to be processed by the userland code
u8 number_interrupts;
// Error code - zero on success, otherwise an error has occurred
u8 error_code;
u8 padding1;
union {
u8 config;
BitField<0, 1, u8> ignore_pdc;
};
u32 missed_PDC0;
u32 missed_PDC1;
InterruptId slot[0x34]; ///< Interrupt ID slots
InterruptId slot[max_slots]; ///< Interrupt ID slots
};
static_assert(sizeof(InterruptRelayQueue) == 0x40, "InterruptRelayQueue struct has incorrect size");
using InterruptHandler = std::function<void(InterruptId)>;
using InterruptHandler = std::function<void(InterruptId, u64)>;
} // namespace Service::GSP

View file

@ -76,6 +76,8 @@ enum class ResultStatus {
ErrorGbaTitle,
ErrorArtic,
ErrorNotFound,
ErrorPatches,
ErrorPatchesInvalidTitle,
};
constexpr u32 MakeMagic(char a, char b, char c, char d) {

View file

@ -3,6 +3,7 @@
// Refer to the license.txt file included.
#include <array>
#include <csignal>
#include <cstring>
#include <boost/serialization/array.hpp>
#include <boost/serialization/binary_object.hpp>
@ -12,10 +13,14 @@
#include "common/atomic_ops.h"
#include "common/common_types.h"
#include "common/logging/log.h"
#include "common/optional_helper.h"
#include "common/settings.h"
#include "common/swap.h"
#include "core/arm/arm_interface.h"
#include "core/core.h"
#ifdef ENABLE_GDBSTUB
#include "core/gdbstub/gdbstub.h"
#endif
#include "core/global.h"
#include "core/hle/kernel/process.h"
#include "core/hle/service/plgldr/plgldr.h"
@ -28,6 +33,14 @@ SERIALIZE_EXPORT_IMPL(Memory::MemorySystem::BackingMemImpl<Memory::Region::VRAM>
SERIALIZE_EXPORT_IMPL(Memory::MemorySystem::BackingMemImpl<Memory::Region::DSP>)
SERIALIZE_EXPORT_IMPL(Memory::MemorySystem::BackingMemImpl<Memory::Region::N3DS>)
#ifndef SIGTRAP
constexpr u32 SIGTRAP = 5;
#endif
#ifndef SIGSEGV
constexpr u32 SIGSEGV = 11;
#endif
namespace Memory {
void PageTable::Clear() {
@ -187,7 +200,17 @@ public:
std::memcpy(dest_buffer, src_ptr, copy_amount);
break;
}
case PageType::RasterizerCachedMemory: {
case PageType::MemoryWatchpoint: {
auto it = page_table.watchpoint_pages_map.find(page_index);
ASSERT_MSG(it != page_table.watchpoint_pages_map.end(),
"Missing memory for watchpoint page");
const u8* src_ptr = it->second.memory.GetPtr() + page_offset;
std::memcpy(dest_buffer, src_ptr, copy_amount);
break;
}
case PageType::RasterizerCachedMemory:
case PageType::RasterizerCachedMemoryWatchpoint: {
if constexpr (!UNSAFE) {
RasterizerFlushVirtualRegion(current_vaddr, static_cast<u32>(copy_amount),
FlushMode::Flush);
@ -235,7 +258,17 @@ public:
std::memcpy(dest_ptr, src_buffer, copy_amount);
break;
}
case PageType::RasterizerCachedMemory: {
case PageType::MemoryWatchpoint: {
auto it = page_table.watchpoint_pages_map.find(page_index);
ASSERT_MSG(it != page_table.watchpoint_pages_map.end(),
"Missing memory for watchpoint page");
u8* dest_ptr = it->second.memory.GetPtr() + page_offset;
std::memcpy(dest_ptr, src_buffer, copy_amount);
break;
}
case PageType::RasterizerCachedMemory:
case PageType::RasterizerCachedMemoryWatchpoint: {
if constexpr (!UNSAFE) {
RasterizerFlushVirtualRegion(current_vaddr, static_cast<u32>(copy_amount),
FlushMode::Invalidate);
@ -392,6 +425,88 @@ PAddr& Memory::MemorySystem::Plugin3GXFramebufferAddress() {
return impl->plugin_fb_address;
}
void MemorySystem::RegisterWatchpoint(const Kernel::Process& process, VAddr addr, u32 size) {
auto& page_table = *process.vm_manager.page_table;
VAddr current = addr;
VAddr end = addr + size;
while (current < end) {
const VAddr page_base = (current & ~CITRA_PAGE_MASK);
const VAddr page_index = page_base >> CITRA_PAGE_BITS;
auto it = page_table.watchpoint_pages_map.find(page_index);
if (it != page_table.watchpoint_pages_map.end()) {
// Nothing to do, only increment count.
it->second.watchpoint_count++;
} else {
MemoryRef mem;
PageType& type = page_table.attributes[page_index];
switch (type) {
case PageType::Memory:
mem = page_table.pointers.Ref(page_index);
type = PageType::MemoryWatchpoint;
page_table.pointers[page_index] = nullptr;
break;
case PageType::RasterizerCachedMemory:
mem = GetPointerForRasterizerCache(page_base);
type = PageType::RasterizerCachedMemoryWatchpoint;
break;
default:
LOG_ERROR(HW_Memory, "Cannot get pointer to register watchpoint for page 0x{:08X}",
page_base);
continue;
}
page_table.watchpoint_pages_map.insert(
{page_index,
PageTable::WatchpointPageInfo{.watchpoint_count = 1, .memory = std::move(mem)}});
}
current = page_base + CITRA_PAGE_SIZE;
}
}
void MemorySystem::UnregisterWatchpoint(const Kernel::Process& process, VAddr addr, u32 size) {
auto& page_table = *process.vm_manager.page_table;
VAddr current = addr;
VAddr end = addr + size;
while (current < end) {
const VAddr page_base = (current & ~CITRA_PAGE_MASK);
const VAddr page_index = page_base >> CITRA_PAGE_BITS;
auto it = page_table.watchpoint_pages_map.find(page_index);
if (it != page_table.watchpoint_pages_map.end()) {
if (--it->second.watchpoint_count == 0) {
PageType& type = page_table.attributes[page_index];
switch (type) {
case PageType::MemoryWatchpoint:
type = PageType::Memory;
page_table.pointers[page_index] = it->second.memory;
break;
case PageType::RasterizerCachedMemoryWatchpoint:
type = PageType::RasterizerCachedMemory;
break;
default:
LOG_ERROR(HW_Memory, "Invalid watchpoint page type for page 0x{:08X}: {}",
page_base, static_cast<u8>(type));
}
page_table.watchpoint_pages_map.erase(page_index);
}
} else {
LOG_ERROR(HW_Memory, "No watchpoint found on page 0x{:08X}", page_base);
}
current = page_base + CITRA_PAGE_SIZE;
}
}
void MemorySystem::MapPages(PageTable& page_table, u32 base, u32 size, MemoryRef memory,
PageType type) {
LOG_DEBUG(HW_Memory, "Mapping {} onto {:08X}-{:08X}", (void*)memory.GetPtr(),
@ -449,13 +564,37 @@ void MemorySystem::UnregisterPageTable(std::shared_ptr<PageTable> page_table) {
}
}
template <typename T>
void MemorySystem::UnmappedAccess(const VAddr vaddr, const T value, bool read) {
const std::string mode = (read ? "Read" : "Write");
const std::string value_str = read ? std::string("") : fmt::format(" 0x{:08X}", value);
const std::string message = fmt::format("unmapped {}{}{} @ 0x{:08X} at PC 0x{:08X}", mode,
sizeof(T) * 8, value_str, vaddr, impl->GetPC());
#ifdef ENABLE_GDBSTUB
if (GDBStub::IsConnected()) {
GDBStub::Break(SIGSEGV);
} else
#endif
if (Settings::values.break_on_unmapped_memory_access) {
impl->system.SetStatus(Core::System::ResultStatus::ErrorMemoryExceptionRaised,
message.c_str());
}
LOG_ERROR(HW_Memory, "{}", message);
}
template <typename T>
T MemorySystem::Read(const std::shared_ptr<PageTable>& page_table, const VAddr vaddr) {
constexpr bool is_optional = is_optional_type<T>;
using ReadType = optional_inner_or_type<T>;
constexpr size_t read_size = sizeof(ReadType);
const u8* page_pointer = page_table->pointers[vaddr >> CITRA_PAGE_BITS];
if (page_pointer) {
// NOTE: Avoid adding any extra logic to this fast-path block
T value;
std::memcpy(&value, &page_pointer[vaddr & CITRA_PAGE_MASK], sizeof(T));
ReadType value;
std::memcpy(&value, &page_pointer[vaddr & CITRA_PAGE_MASK], read_size);
return value;
}
@ -464,37 +603,77 @@ T MemorySystem::Read(const std::shared_ptr<PageTable>& page_table, const VAddr v
if (vaddr & (1 << 31)) {
PAddr paddr = (vaddr & ~(1 << 31));
if ((paddr & 0xF0000000) == Memory::FCRAM_PADDR) { // Check FCRAM region
T value;
std::memcpy(&value, GetFCRAMPointer(paddr - Memory::FCRAM_PADDR), sizeof(T));
ReadType value;
std::memcpy(&value, GetFCRAMPointer(paddr - Memory::FCRAM_PADDR), read_size);
return value;
} else if ((paddr & 0xF0000000) == 0x10000000 &&
paddr >= Memory::IO_AREA_PADDR) { // Check MMIO region
return impl->system.GPU().ReadReg(static_cast<VAddr>(paddr) - Memory::IO_AREA_PADDR +
0x1EC00000);
return static_cast<ReadType>(impl->system.GPU().ReadReg(
static_cast<VAddr>(paddr) - Memory::IO_AREA_PADDR + 0x1EC00000));
}
}
PageType type = page_table->attributes[vaddr >> CITRA_PAGE_BITS];
switch (type) {
case PageType::Unmapped:
LOG_ERROR(HW_Memory, "unmapped Read{} @ 0x{:08X} at PC 0x{:08X}", sizeof(T) * 8, vaddr,
impl->GetPC());
return 0;
case PageType::Unmapped: {
UnmappedAccess<ReadType>(vaddr, 0, true);
if constexpr (is_optional) {
return std::nullopt;
} else {
return T{};
}
}
case PageType::Memory:
ASSERT_MSG(false, "Mapped memory page without a pointer @ {:08X}", vaddr);
break;
case PageType::RasterizerCachedMemory: {
RasterizerFlushVirtualRegion(vaddr, sizeof(T), FlushMode::Flush);
case PageType::MemoryWatchpoint: {
auto it = page_table->watchpoint_pages_map.find(vaddr >> CITRA_PAGE_BITS);
ASSERT_MSG(it != page_table->watchpoint_pages_map.end(),
"Missing memory for watchpoint page");
ReadType value;
std::memcpy(&value, it->second.memory.GetPtr() + (vaddr & CITRA_PAGE_MASK), read_size);
#ifdef ENABLE_GDBSTUB
if (GDBStub::CheckBreakpoint(vaddr, read_size, GDBStub::BreakpointType::Read)) {
GDBStub::Break(SIGTRAP);
}
#endif
return value;
}
[[likely]] case PageType::RasterizerCachedMemory: {
RasterizerFlushVirtualRegion(vaddr, read_size, FlushMode::Flush);
ReadType value;
std::memcpy(&value, GetPointerForRasterizerCache(vaddr), read_size);
return value;
}
case PageType::RasterizerCachedMemoryWatchpoint: {
RasterizerFlushVirtualRegion(vaddr, read_size, FlushMode::Flush);
ReadType value;
std::memcpy(&value, GetPointerForRasterizerCache(vaddr), read_size);
#ifdef ENABLE_GDBSTUB
if (GDBStub::CheckBreakpoint(vaddr, read_size, GDBStub::BreakpointType::Read)) {
GDBStub::Break(SIGTRAP);
}
#endif
T value;
std::memcpy(&value, GetPointerForRasterizerCache(vaddr), sizeof(T));
return value;
}
default:
UNREACHABLE();
}
return T{};
if constexpr (is_optional) {
return std::nullopt;
} else {
return T{};
}
}
template <typename T>
@ -527,17 +706,43 @@ void MemorySystem::Write(const std::shared_ptr<PageTable>& page_table, const VAd
PageType type = page_table->attributes[vaddr >> CITRA_PAGE_BITS];
switch (type) {
case PageType::Unmapped:
LOG_ERROR(HW_Memory, "unmapped Write{} 0x{:08X} @ 0x{:08X} at PC 0x{:08X}",
sizeof(data) * 8, (u32)data, vaddr, impl->GetPC());
(void)UnmappedAccess<T>(vaddr, data, false);
return;
case PageType::Memory:
ASSERT_MSG(false, "Mapped memory page without a pointer @ {:08X}", vaddr);
break;
case PageType::RasterizerCachedMemory: {
case PageType::MemoryWatchpoint: {
auto it = page_table->watchpoint_pages_map.find(vaddr >> CITRA_PAGE_BITS);
ASSERT_MSG(it != page_table->watchpoint_pages_map.end(),
"Missing memory for watchpoint page");
std::memcpy(it->second.memory.GetPtr() + (vaddr & CITRA_PAGE_MASK), &data, sizeof(T));
#ifdef ENABLE_GDBSTUB
if (GDBStub::CheckBreakpoint(vaddr, sizeof(T), GDBStub::BreakpointType::Write)) {
GDBStub::Break(SIGTRAP);
}
#endif
break;
}
[[likely]] case PageType::RasterizerCachedMemory: {
RasterizerFlushVirtualRegion(vaddr, sizeof(T), FlushMode::Invalidate);
std::memcpy(GetPointerForRasterizerCache(vaddr), &data, sizeof(T));
break;
}
case PageType::RasterizerCachedMemoryWatchpoint: {
RasterizerFlushVirtualRegion(vaddr, sizeof(T), FlushMode::Invalidate);
std::memcpy(GetPointerForRasterizerCache(vaddr), &data, sizeof(T));
#ifdef ENABLE_GDBSTUB
if (GDBStub::CheckBreakpoint(vaddr, sizeof(T), GDBStub::BreakpointType::Write)) {
GDBStub::Break(SIGTRAP);
}
#endif
break;
}
default:
UNREACHABLE();
}
@ -556,18 +761,48 @@ bool MemorySystem::WriteExclusive(const VAddr vaddr, const T data, const T expec
PageType type = impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS];
switch (type) {
case PageType::Unmapped:
LOG_ERROR(HW_Memory, "unmapped Write{} 0x{:08X} @ 0x{:08X} at PC 0x{:08X}",
sizeof(data) * 8, static_cast<u32>(data), vaddr, impl->GetPC());
(void)UnmappedAccess<T>(vaddr, data, false);
return true;
case PageType::Memory:
ASSERT_MSG(false, "Mapped memory page without a pointer @ {:08X}", vaddr);
return true;
case PageType::RasterizerCachedMemory: {
case PageType::MemoryWatchpoint: {
auto it = impl->current_page_table->watchpoint_pages_map.find(vaddr >> CITRA_PAGE_BITS);
ASSERT_MSG(it != impl->current_page_table->watchpoint_pages_map.end(),
"Missing memory for watchpoint page");
const auto volatile_pointer =
reinterpret_cast<volatile T*>(it->second.memory.GetPtr() + (vaddr & CITRA_PAGE_MASK));
bool ret = Common::AtomicCompareAndSwap(volatile_pointer, data, expected);
#ifdef ENABLE_GDBSTUB
if (GDBStub::CheckBreakpoint(vaddr, sizeof(T), GDBStub::BreakpointType::Write)) {
GDBStub::Break(SIGTRAP);
}
#endif
return ret;
}
[[likely]] case PageType::RasterizerCachedMemory: {
RasterizerFlushVirtualRegion(vaddr, sizeof(T), FlushMode::Invalidate);
const auto volatile_pointer =
reinterpret_cast<volatile T*>(GetPointerForRasterizerCache(vaddr).GetPtr());
return Common::AtomicCompareAndSwap(volatile_pointer, data, expected);
}
case PageType::RasterizerCachedMemoryWatchpoint: {
RasterizerFlushVirtualRegion(vaddr, sizeof(T), FlushMode::Invalidate);
const auto volatile_pointer =
reinterpret_cast<volatile T*>(GetPointerForRasterizerCache(vaddr).GetPtr());
#ifdef ENABLE_GDBSTUB
if (GDBStub::CheckBreakpoint(vaddr, sizeof(T), GDBStub::BreakpointType::Write)) {
GDBStub::Break(SIGTRAP);
}
#endif
return Common::AtomicCompareAndSwap(volatile_pointer, data, expected);
}
default:
UNREACHABLE();
}
@ -582,7 +817,7 @@ bool MemorySystem::IsValidVirtualAddress(const Kernel::Process& process, const V
return true;
}
if (page_table.attributes[vaddr >> CITRA_PAGE_BITS] == PageType::RasterizerCachedMemory) {
if (page_table.attributes[vaddr >> CITRA_PAGE_BITS] != PageType::Unmapped) {
return true;
}
@ -600,7 +835,9 @@ u8* MemorySystem::GetPointer(const VAddr vaddr) {
}
if (impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS] ==
PageType::RasterizerCachedMemory) {
PageType::RasterizerCachedMemory ||
impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS] ==
PageType::RasterizerCachedMemoryWatchpoint) {
return GetPointerForRasterizerCache(vaddr);
}
@ -615,7 +852,9 @@ const u8* MemorySystem::GetPointer(const VAddr vaddr) const {
}
if (impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS] ==
PageType::RasterizerCachedMemory) {
PageType::RasterizerCachedMemory ||
impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS] ==
PageType::RasterizerCachedMemoryWatchpoint) {
return GetPointerForRasterizerCache(vaddr);
}
@ -755,7 +994,10 @@ void MemorySystem::RasterizerMarkRegionCached(PAddr start, u32 size, bool cached
// address space, for example, a system module need not have a VRAM mapping.
break;
case PageType::Memory:
page_type = PageType::RasterizerCachedMemory;
case PageType::MemoryWatchpoint:
page_type = (page_type == PageType::Memory)
? PageType::RasterizerCachedMemory
: PageType::RasterizerCachedMemoryWatchpoint;
page_table->pointers[vaddr >> CITRA_PAGE_BITS] = nullptr;
break;
default:
@ -768,10 +1010,16 @@ void MemorySystem::RasterizerMarkRegionCached(PAddr start, u32 size, bool cached
// It is not necessary for a process to have this region mapped into its
// address space, for example, a system module need not have a VRAM mapping.
break;
case PageType::RasterizerCachedMemory: {
page_type = PageType::Memory;
page_table->pointers[vaddr >> CITRA_PAGE_BITS] =
GetPointerForRasterizerCache(vaddr & ~CITRA_PAGE_MASK);
case PageType::RasterizerCachedMemory:
case PageType::RasterizerCachedMemoryWatchpoint: {
page_type = (page_type == PageType::RasterizerCachedMemory)
? PageType::Memory
: PageType::MemoryWatchpoint;
if (page_type == PageType::Memory) {
page_table->pointers[vaddr >> CITRA_PAGE_BITS] =
GetPointerForRasterizerCache(vaddr & ~CITRA_PAGE_MASK);
}
break;
}
default:
@ -815,6 +1063,14 @@ u64 MemorySystem::Read64(const Kernel::Process& process, VAddr addr) {
return Read<u64_le>(process.vm_manager.page_table, addr);
}
std::optional<u32> MemorySystem::Read32OrNullopt(VAddr addr) {
return Read<std::optional<u32_le>>(impl->current_page_table, addr);
}
std::optional<u32> MemorySystem::Read32OrNullopt(const Kernel::Process& process, VAddr addr) {
return Read<std::optional<u32_le>>(process.vm_manager.page_table, addr);
}
void MemorySystem::ReadBlock(const Kernel::Process& process, const VAddr src_addr,
void* dest_buffer, const std::size_t size) {
return impl->ReadBlockImpl<false>(process, src_addr, dest_buffer, size);
@ -911,7 +1167,17 @@ void MemorySystem::ZeroBlock(const Kernel::Process& process, const VAddr dest_ad
std::memset(dest_ptr, 0, copy_amount);
break;
}
case PageType::RasterizerCachedMemory: {
case PageType::MemoryWatchpoint: {
auto it = page_table.watchpoint_pages_map.find(page_index);
ASSERT_MSG(it != page_table.watchpoint_pages_map.end(),
"Missing memory for watchpoint page");
u8* dest_ptr = it->second.memory.GetPtr() + page_offset;
std::memset(dest_ptr, 0, copy_amount);
break;
}
case PageType::RasterizerCachedMemory:
case PageType::RasterizerCachedMemoryWatchpoint: {
RasterizerFlushVirtualRegion(current_vaddr, static_cast<u32>(copy_amount),
FlushMode::Invalidate);
std::memset(GetPointerForRasterizerCache(current_vaddr), 0, copy_amount);
@ -960,7 +1226,17 @@ void MemorySystem::CopyBlock(const Kernel::Process& dest_process,
WriteBlock(dest_process, dest_addr, src_ptr, copy_amount);
break;
}
case PageType::RasterizerCachedMemory: {
case PageType::MemoryWatchpoint: {
auto it = page_table.watchpoint_pages_map.find(page_index);
ASSERT_MSG(it != page_table.watchpoint_pages_map.end(),
"Missing memory for watchpoint page");
const u8* src_ptr = it->second.memory.GetPtr() + page_offset;
WriteBlock(dest_process, dest_addr, src_ptr, copy_amount);
break;
}
case PageType::RasterizerCachedMemory:
case PageType::RasterizerCachedMemoryWatchpoint: {
RasterizerFlushVirtualRegion(current_vaddr, static_cast<u32>(copy_amount),
FlushMode::Flush);
WriteBlock(dest_process, dest_addr, GetPointerForRasterizerCache(current_vaddr),

View file

@ -5,11 +5,13 @@
#pragma once
#include <array>
#include <cstddef>
#include <optional>
#include <string>
#include <boost/serialization/array.hpp>
#include <boost/serialization/vector.hpp>
#include "common/common_types.h"
#include "common/memory_ref.h"
#include "common/swap.h"
namespace Kernel {
class Process;
@ -34,7 +36,7 @@ constexpr u32 CITRA_PAGE_MASK = CITRA_PAGE_SIZE - 1;
constexpr int CITRA_PAGE_BITS = 12;
constexpr std::size_t PAGE_TABLE_NUM_ENTRIES = 1 << (32 - CITRA_PAGE_BITS);
enum class PageType {
enum class PageType : u8 {
/// Page is unmapped and should cause an access error.
Unmapped,
/// Page is mapped to regular memory. This is the only type you can get pointers to.
@ -42,6 +44,12 @@ enum class PageType {
/// Page is mapped to regular memory, but also needs to check for rasterizer cache flushing and
/// invalidation
RasterizerCachedMemory,
/// Page is mapped to regular memory. Furthermore a debug watchpoint is set to an address within
/// the page.
MemoryWatchpoint,
/// Page is mapped to regular memory, but also needs to check for rasterizer cache flushing and
/// invalidation. Furthermore a debug watchpoint is set to an address within the page.
RasterizerCachedMemoryWatchpoint,
};
/**
@ -82,6 +90,10 @@ struct PageTable {
return Entry(*this, static_cast<VAddr>(idx));
}
const MemoryRef& Ref(std::size_t idx) {
return refs[idx];
}
private:
std::array<u8*, PAGE_TABLE_NUM_ENTRIES> raw;
std::array<MemoryRef, PAGE_TABLE_NUM_ENTRIES> refs;
@ -100,6 +112,22 @@ struct PageTable {
return pointers.raw;
}
struct WatchpointPageInfo {
u32 watchpoint_count{};
MemoryRef memory;
template <class Archive>
void serialize(Archive& ar, const unsigned int) {
ar & watchpoint_count;
ar & memory;
}
};
// Map holding pages that are marked to contain watchpoints. We don't need
// any fancy performance tricks here, as watchpoints are only used rarely
// while debugging and performance is not a priority in such cases.
std::unordered_map<VAddr, WatchpointPageInfo> watchpoint_pages_map{};
void Clear();
private:
@ -107,6 +135,7 @@ private:
void serialize(Archive& ar, const unsigned int) {
ar & pointers.refs;
ar & attributes;
ar & watchpoint_pages_map;
for (std::size_t i = 0; i < PAGE_TABLE_NUM_ENTRIES; i++) {
pointers.raw[i] = pointers.refs[i].GetPtr();
}
@ -360,6 +389,29 @@ public:
*/
u64 Read64(const Kernel::Process& process, VAddr addr);
/**
* Reads a 32-bit unsigned value from the current process' address space
* at the given virtual address. If the address is invalid std::nullopt
* is returned instead.
*
* @param addr The virtual address to read the 32-bit value from.
*
* @returns the read 32-bit unsigned value or std::nullopt.
*/
std::optional<u32> Read32OrNullopt(VAddr addr);
/**
* Reads a 32-bit unsigned value from the process' address space
* at the given virtual address. If the address is invalid std::nullopt
* is returned instead.
*
* @param process The process to read from.
* @param addr The virtual address to read the 32-bit value from.
*
* @returns the read 32-bit unsigned value or std::nullopt.
*/
std::optional<u32> Read32OrNullopt(const Kernel::Process& process, VAddr addr);
/**
* Writes an 8-bit unsigned integer to the given virtual address in
* the current process' address space.
@ -649,7 +701,14 @@ public:
/// Returns a reference to the framebuffer address of the currently loaded 3GX plugin.
PAddr& Plugin3GXFramebufferAddress();
void RegisterWatchpoint(const Kernel::Process& process, VAddr addr, u32 size);
void UnregisterWatchpoint(const Kernel::Process& process, VAddr addr, u32 size);
private:
template <typename T>
void UnmappedAccess(const VAddr vaddr, const T value, bool read);
template <typename T>
T Read(const std::shared_ptr<PageTable>& page_table, const VAddr vaddr);

View file

@ -1,4 +1,4 @@
add_library(input_common STATIC
add_library(input_common STATIC EXCLUDE_FROM_ALL
analog_from_button.cpp
analog_from_button.h
keyboard.cpp

View file

@ -1,4 +1,4 @@
add_library(network STATIC
add_library(network STATIC EXCLUDE_FROM_ALL
announce_multiplayer_session.cpp
announce_multiplayer_session.h
artic_base/artic_base_client.cpp

View file

@ -36,7 +36,7 @@ if (ENABLE_LIBRETRO)
endif()
add_test(NAME tests COMMAND tests)
if(NOT ANDROID)
if(NOT ANDROID AND (CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME))
catch_discover_tests(tests)
endif()

View file

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -24,3 +24,13 @@ TEST_CASE("SplitFilename83 Sanity", "[common]") {
REQUIRE(std::memcmp(short_name.data(), expected_short_name.data(), short_name.size()) == 0);
REQUIRE(std::memcmp(extension.data(), expected_extension.data(), extension.size()) == 0);
}
#if defined(__APPLE__)
TEST_CASE("NormalizeNFDToNFC Sanity", "[common]") {
const std::string decomposed = "i\xCC\x81";
const std::string composed = "\xC3\xAD";
REQUIRE(Common::NormalizeNFDToNFC(decomposed) == composed);
}
#endif

View file

@ -1,6 +1,6 @@
add_subdirectory(host_shaders)
add_library(video_core STATIC
add_library(video_core STATIC EXCLUDE_FROM_ALL
custom_textures/custom_format.cpp
custom_textures/custom_format.h
custom_textures/custom_tex_manager.cpp

View file

@ -26,6 +26,63 @@ namespace VideoCore {
constexpr VAddr VADDR_LCD = 0x1ED02000;
constexpr VAddr VADDR_GPU = 0x1EF00000;
class DelayGenerator {
private:
DelayGenerator() = default;
// Average transfer speed based on measurements taken from real
// hardware. 4 different modes have been taken into consideration:
// RAM -> RAM, RAM -> VRAM, VRAM -> RAM and VRAM -> VRAM.
// Furthermore, measurements are split into DMA transfers and tex
// copies. For simplicity, we will assume fills are as fast as
// texture copies.
static constexpr double mibps_to_ns_per_byte(double mib_per_sec) {
return 1'000'000'000.0 / (mib_per_sec * 1024.0 * 1024.0);
}
static constexpr std::array<std::array<double, 4>, 2> speed_mibps = {
{{
190.0, // DMA RAMTORAM
310.0, // DMA RAMTOVRAM
380.0, // DMA VRAMTORAM
380.0, // DMA VRAMTOVRAM
},
{
450.0, // TEX RAMTORAM
3100.0, // TEX RAMTOVRAM
5400.0, // TEX VRAMTORAM
5400.0, // TEX VRAMTOVRAM
}}};
public:
enum class CopyMode {
RAMTORAM,
RAMTOVRAM,
VRAMTORAM,
VRAMTOVRAM,
};
static CopyMode GetCopyMode(bool input_vram, bool output_vram) {
if (!input_vram && !output_vram) {
return CopyMode::RAMTORAM;
} else if (!input_vram && output_vram) {
return CopyMode::RAMTOVRAM;
} else if (input_vram && !output_vram) {
return CopyMode::VRAMTORAM;
} else {
return CopyMode::VRAMTOVRAM;
}
}
static u64 CalculateDelayNanoseconds(CopyMode mode, bool is_textre, size_t size) {
double base_ns_per_byte =
mibps_to_ns_per_byte(speed_mibps[is_textre][static_cast<u32>(mode)]);
return static_cast<u64>(size * base_ns_per_byte);
}
};
MICROPROFILE_DEFINE(GPU_DisplayTransfer, "GPU", "DisplayTransfer", MP_RGB(100, 100, 255));
MICROPROFILE_DEFINE(GPU_CmdlistProcessing, "GPU", "Cmdlist Processing", MP_RGB(100, 255, 100));
@ -102,7 +159,17 @@ void GPU::Execute(const Service::GSP::Command& command) {
const auto process = impl->system.Kernel().GetCurrentProcess();
impl->memory.CopyBlock(*process, command.dma_request.dest_address,
command.dma_request.source_address, command.dma_request.size);
impl->signal_interrupt(Service::GSP::InterruptId::DMA);
auto is_vram = [&](u32 addr) {
return addr >= Memory::VRAM_VADDR && addr <= Memory::VRAM_VADDR_END;
};
u64 delay = DelayGenerator::CalculateDelayNanoseconds(
DelayGenerator::GetCopyMode(is_vram(command.dma_request.source_address),
is_vram(command.dma_request.dest_address)),
false, command.dma_request.size);
impl->signal_interrupt(Service::GSP::InterruptId::DMA, delay);
break;
}
case CommandId::SubmitCmdList: {
@ -361,13 +428,18 @@ void GPU::MemoryFill(u32 index, u32 intr_index) {
impl->sw_blitter->MemoryFill(config);
}
// Treat fill as texture transfer from VRAM
u64 delay = DelayGenerator::CalculateDelayNanoseconds(
DelayGenerator::GetCopyMode(true, config.IsVRAM()), true,
config.GetEndAddress() - config.GetStartAddress());
// It seems that it won't signal interrupt if "address_start" is zero.
// TODO: hwtest this
if (config.GetStartAddress() != 0) {
if (intr_index == 0) {
impl->signal_interrupt(Service::GSP::InterruptId::PSC0);
impl->signal_interrupt(Service::GSP::InterruptId::PSC0, delay);
} else if (intr_index == 1) {
impl->signal_interrupt(Service::GSP::InterruptId::PSC1);
impl->signal_interrupt(Service::GSP::InterruptId::PSC1, delay);
}
}
@ -391,11 +463,15 @@ void GPU::MemoryTransfer() {
impl->debug_context->OnEvent(Pica::DebugContext::Event::IncomingDisplayTransfer, nullptr);
}
u64 delay{};
// Perform memory transfer
if (config.is_texture_copy) {
if (!impl->rasterizer->AccelerateTextureCopy(config)) {
impl->sw_blitter->TextureCopy(config);
}
delay = DelayGenerator::CalculateDelayNanoseconds(
DelayGenerator::GetCopyMode(config.IsInputVRAM(), config.IsOutputVRAM()), true,
config.texture_copy.size);
} else {
if (right_eye_disabler->ShouldAllowDisplayTransfer(config.GetPhysicalInputAddress(),
config.input_height)) {
@ -403,11 +479,14 @@ void GPU::MemoryTransfer() {
impl->sw_blitter->DisplayTransfer(config);
}
}
delay = DelayGenerator::CalculateDelayNanoseconds(
DelayGenerator::GetCopyMode(config.IsInputVRAM(), config.IsOutputVRAM()), true,
config.input_width * config.input_height * BytesPerPixel(config.input_format));
}
// Complete transfer.
config.trigger.Assign(0);
impl->signal_interrupt(Service::GSP::InterruptId::PPF);
impl->signal_interrupt(Service::GSP::InterruptId::PPF, delay);
}
void GPU::VBlankCallback(std::uintptr_t user_data, s64 cycles_late) {
@ -415,8 +494,8 @@ void GPU::VBlankCallback(std::uintptr_t user_data, s64 cycles_late) {
impl->renderer->SwapBuffers();
// Signal to GSP that GPU interrupt has occurred
impl->signal_interrupt(Service::GSP::InterruptId::PDC0);
impl->signal_interrupt(Service::GSP::InterruptId::PDC1);
impl->signal_interrupt(Service::GSP::InterruptId::PDC0, 0);
impl->signal_interrupt(Service::GSP::InterruptId::PDC1, 0);
// Reschedule recurrent event
impl->timing.ScheduleEvent(FRAME_TICKS - cycles_late, impl->vblank_event);

View file

@ -98,7 +98,7 @@ void PicaCore::SetInterruptHandler(Service::GSP::InterruptHandler& signal_interr
void PicaCore::ProcessCmdList(PAddr list, u32 size, bool ignore_list) {
if (ignore_list) {
signal_interrupt(Service::GSP::InterruptId::P3D);
signal_interrupt(Service::GSP::InterruptId::P3D, delay_generator.CalculateAndResetDelay());
return;
}
// Initialize command list tracking.
@ -148,6 +148,8 @@ void PicaCore::WriteInternalReg(u32 id, u32 value, u32 mask, bool& stop_requeste
return;
}
delay_generator.AddCommands(1);
// Expand a 4-bit mask to 4-byte mask, e.g. 0b0101 -> 0x00FF00FF
constexpr std::array<u32, 16> ExpandBitsToBytes = {
0x00000000, 0x000000ff, 0x0000ff00, 0x0000ffff, 0x00ff0000, 0x00ff00ff,
@ -174,7 +176,8 @@ void PicaCore::WriteInternalReg(u32 id, u32 value, u32 mask, bool& stop_requeste
// TODO(PabloMK7): This logic is not fully accurate, but close enough:
// https://problemkaputt.de/gbatek-3ds-gpu-internal-registers-finalize-interrupt-registers.htm
if (any_byte_match(regs.internal.reg_array[id], regs.internal.irq_compare)) [[likely]] {
signal_interrupt(Service::GSP::InterruptId::P3D);
signal_interrupt(Service::GSP::InterruptId::P3D,
delay_generator.CalculateAndResetDelay());
if (regs.internal.irq_autostop) [[likely]] {
stop_requested = true;
}
@ -551,6 +554,10 @@ void PicaCore::DrawArrays(bool is_indexed) {
return accelerate_draw;
}();
// Add vertices to the delay generator.
delay_generator.AddVertices(regs.internal.pipeline.num_vertices,
regs.internal.pipeline.triangle_topology);
// Attempt to use hardware vertex shaders if possible.
if (accelerate_draw && rasterizer->AccelerateDrawBatch(is_indexed)) {
return;

View file

@ -29,6 +29,86 @@ namespace Pica {
class DebugContext;
class ShaderEngine;
class DelayGenerator {
private:
// A GPU is a very complex system, the timings resulting from
// a 3D draw depend on many factors, including triangle counts,
// texture sizes and format, shader complexity, cache
// and memory layout, etc. At this point in time, we don't
// have enough information nor implemented hw emulation
// capabilities to achieve a proper timing estimate.
//
// Instead, we will try to measure how complex a scene is based
// on the amount of geometry that is drawn, the amount of GPU
// commands and the shader complexity. We will ignore all
// the other factors for now.
// Using Mario Kart 7 as the reference, it is understood that on
// average the console can handle around 20k triangles per frame.
// This game uses standard GPU features, with no fancy stuff,
// so we can consider it an average. To prevent hurting performance,
// we will also assume the GPU is twice as powerful. Afterall we only
// want timing accuracy to fix bugs at this point.
// This average already takes into account shader complexity averages.
static constexpr float nanoseconds_per_triangle = 800.f / 2;
// Of the total amount of submitted triangles, many of them will be culled.
// This heavily depends on the specific scene, so we will assume 35% of the
// triangles being culled. Furthermore, the culled triangles will take way less
// processing time as they will skip most of the pipeline processing, so we
// can assume that a culled triangle will only take about 20% of the time.
static constexpr float culled_triangle_threshold = 0.35f; // 35%
static constexpr float culled_triangle_time_cost = 0.20f; // 20%
// We will assume that each command will take around 6 cycles @ 268MHz
// There are no real measurements to support this claim, but it sounds
// reasonable. TODO: Measure on real HW.
static constexpr float nanoseconds_per_command = 22.4f;
public:
inline void AddCommands(size_t commands) {
command_count += commands;
}
inline void AddVertices(size_t vertices, PipelineRegs::TriangleTopology topology) {
size_t triangles{};
if (topology == PipelineRegs::TriangleTopology::Fan ||
topology == PipelineRegs::TriangleTopology::Strip) {
triangles = (vertices >= 3) ? (vertices - 2) : 1;
} else {
// Geometry shaders produce more vertices per given vertex,
// but they are not that relevant for timing emulation.
triangles = vertices / 3;
}
triangle_count += triangles;
}
u64 CalculateAndResetDelay() {
float result = command_count * nanoseconds_per_command;
result += (1.f - culled_triangle_threshold) * triangle_count * nanoseconds_per_triangle;
result += culled_triangle_threshold * triangle_count *
(nanoseconds_per_triangle * culled_triangle_time_cost);
triangle_count = 0;
command_count = 0;
return static_cast<u64>(result);
}
private:
size_t triangle_count{};
size_t command_count{};
friend class boost::serialization::access;
template <class Archive>
void serialize(Archive& ar, const u32 file_version) {
ar & triangle_count;
ar & command_count;
}
};
class PicaCore {
public:
explicit PicaCore(Memory::MemorySystem& memory, std::shared_ptr<DebugContext> debug_context_);
@ -277,6 +357,8 @@ public:
AttributeBuffer input_default_attributes{};
ImmediateModeState immediate{};
DelayGenerator delay_generator{};
private:
friend class boost::serialization::access;
template <class Archive>
@ -291,6 +373,7 @@ private:
ar & fog;
ar & input_default_attributes;
ar & immediate;
ar & delay_generator;
ar & geometry_pipeline;
ar & primitive_assembler;
ar & cmd_list;

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.
@ -10,6 +10,7 @@
#include "common/assert.h"
#include "common/bit_field.h"
#include "common/common_funcs.h"
#include "core/memory.h"
namespace Pica {
@ -85,6 +86,11 @@ struct MemoryFillConfig {
return DecodeAddressRegister(address_end);
}
bool IsVRAM() const {
u32 addr = GetStartAddress();
return !(addr >= Memory::FCRAM_PADDR && addr < Memory::FCRAM_PADDR_END);
}
inline std::string DebugName() const {
return fmt::format("from {:#X} to {:#X} with {}-bit value {:#X}", GetStartAddress(),
GetEndAddress(), fill_32bit ? "32" : (fill_24bit ? "24" : "16"),
@ -155,6 +161,16 @@ struct DisplayTransferConfig {
input_width.Value(), output_width.Value());
}
bool IsInputVRAM() {
u32 addr = GetPhysicalInputAddress();
return !(addr >= Memory::FCRAM_PADDR && addr < Memory::FCRAM_PADDR_END);
}
bool IsOutputVRAM() {
u32 addr = GetPhysicalOutputAddress();
return !(addr >= Memory::FCRAM_PADDR && addr < Memory::FCRAM_PADDR_END);
}
union {
u32 output_size;

View file

@ -1,4 +1,4 @@
add_library(web_service STATIC
add_library(web_service STATIC EXCLUDE_FROM_ALL
announce_room_json.cpp
announce_room_json.h
precompiled_headers.h