diff --git a/.ci/mxe.sh b/.ci/mxe.sh new file mode 100755 index 000000000..2aa2ccfa9 --- /dev/null +++ b/.ci/mxe.sh @@ -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 diff --git a/.ci/pack.sh b/.ci/pack.sh index 1a6356e11..71c2339b4 100755 --- a/.ci/pack.sh +++ b/.ci/pack.sh @@ -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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c804ad5b4..70a8417f0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/.gitmodules b/.gitmodules index 18c5cc79a..d09df6f0d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 3d58cc70d..42664571b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/CMakeModules/BundleTarget.cmake b/CMakeModules/BundleTarget.cmake index fead41acf..45e5ee296 100644 --- a/CMakeModules/BundleTarget.cmake +++ b/CMakeModules/BundleTarget.cmake @@ -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 "$/*" "${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 "$") 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}") diff --git a/CMakeModules/GenerateSettingKeys.cmake b/CMakeModules/GenerateSettingKeys.cmake index 90110cd33..7aff65db5 100644 --- a/CMakeModules/GenerateSettingKeys.cmake +++ b/CMakeModules/GenerateSettingKeys.cmake @@ -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" diff --git a/docker/azahar-room/Dockerfile b/docker/azahar-room/Dockerfile index c47896dd9..9a5f07820 100644 --- a/docker/azahar-room/Dockerfile +++ b/docker/azahar-room/Dockerfile @@ -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 \ diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index a0e75c2ac..c5d424ff4 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -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() diff --git a/externals/dllwalker b/externals/dllwalker new file mode 160000 index 000000000..2f8b349c2 --- /dev/null +++ b/externals/dllwalker @@ -0,0 +1 @@ +Subproject commit 2f8b349c26832cae612aa7082154c0697a9cbc8e diff --git a/externals/openal-soft b/externals/openal-soft index c41d64c6a..e399840fc 160000 --- a/externals/openal-soft +++ b/externals/openal-soft @@ -1 +1 @@ -Subproject commit c41d64c6a35f6174bf4a27010aeac52a8d3bb2c6 +Subproject commit e399840fc6aba5f7bc3f0633e8ff10bba0640906 diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index 733656c0e..2ee844985 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -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 ) } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt index 018fea670..72035cae4 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt @@ -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 { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/SettingKeys.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/SettingKeys.kt index 1d6e0dcee..56ffb6789 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/SettingKeys.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/SettingKeys.kt @@ -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 diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt index e12d87544..0e88dacf3 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt @@ -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 diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index c692e56c8..67d70b825 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -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, diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp index e0fa260c5..967166e12 100644 --- a/src/android/app/src/main/jni/config.cpp +++ b/src/android/app/src/main/jni/config.cpp @@ -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 = diff --git a/src/android/app/src/main/jni/default_ini.h b/src/android/app/src/main/jni/default_ini.h index ce93d0e6f..6864d04b0 100644 --- a/src/android/app/src/main/jni/default_ini.h +++ b/src/android/app/src/main/jni/default_ini.h @@ -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"( diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 34f2b5881..224c6f431 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -238,6 +238,8 @@ Storage Compress installed CIA content 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. + Asynchronous filesystem operations + Makes emulated filesystem accesses asynchronous. Greatly reduces filesystem related stutter, but may slightly increase load times. Inner Camera @@ -269,6 +271,8 @@ Enhances the visuals of applications by applying a filter to textures. The supported filters are Anime4K Ultrafast, Bicubic, ScaleForce, xBRZ freescale, and MMPX. Delay Game Render Thread Delay the game render thread when it submits data to the GPU. Helps with performance issues in the (very few) applications with dynamic framerates. + Simulate 3DS GPU Timings + 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. Advanced Texture Sampling 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. @@ -341,6 +345,8 @@ Stretches audio to reduce stuttering. When enabled, increases audio latency and slightly reduces performance. Enable Realtime Audio 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. + Simulate Headphones Plugged In + Simulates whether headphones are plugged in to the emulated 3DS system. Audio Input Device Sound Output Mode @@ -396,7 +402,6 @@ Learn More Close Reset to Default - game cartridges or installed titles.]]> Default None Auto @@ -439,11 +444,39 @@ Layout + Error loading application + Invalid application format + Please make sure you are using one of the compatible file formats:
  • Cartridge images: .cci/.zcci/.3ds
  • Installable archives: .cia/.zcia
  • Homebrew titles: .3dsx/.z3dsx
  • NCCH containers: .cxi/.zcxi/.app
  • ELF files: .elf/.axf
]]>
+ Invalid system mode + New 3DS exclusive applications cannot be loaded without enabling the New 3DS mode. + Error applying patches + A generic error occurred while applying a patch to the application. Please check the log for more details. + 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. Your ROM is Encrypted - Invalid ROM format + blog post for more information.]]> ROM file does not exist No bootable game present! + An error occurred while loading ROM: \"%s (%d)\" + Success + Not initialized + Loader for file not found, incompatible file type + Failed to parse file + Generic loader error + Encrypted file + Corrupted file + File is GBA title + Error applying patches + Patches are for a different application + Missing system files + Savestate failed + Artic Base disconnected + File is New 3DS application + Core exception raised + Memory exception raised + Shutdown requested + Unknown error + Press Back to access the menu. Save State diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt index 6ea16672e..f6f8bf9ed 100644 --- a/src/audio_core/CMakeLists.txt +++ b/src/audio_core/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(audio_core STATIC +add_library(audio_core STATIC EXCLUDE_FROM_ALL audio_types.h codec.cpp codec.h diff --git a/src/citra_meta/CMakeLists.txt b/src/citra_meta/CMakeLists.txt index b19238a9c..a2398b5d6 100644 --- a/src/citra_meta/CMakeLists.txt +++ b/src/citra_meta/CMakeLists.txt @@ -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" + $ + /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 {} $ + COMMAND ${CMAKE_COMMAND} -E make_directory "$/plugins/" + COMMAND ${CMAKE_COMMAND} -E copy_directory /mxe/usr/x86_64-w64-mingw32.shared/qt6/plugins/ "$/plugins/" + COMMAND_EXPAND_LISTS + DEPENDS ${dll_list_filename} + ) +endif() + target_link_libraries(citra_meta PRIVATE citra_common fmt) if (ENABLE_QT) diff --git a/src/citra_qt/citra_qt.cpp b/src/citra_qt/citra_qt.cpp index 62fb24b9b..bda6d0816 100644 --- a/src/citra_qt/citra_qt.cpp +++ b/src/citra_qt/citra_qt.cpp @@ -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.
Please make sure you are using one " + "of the compatible file formats:
  • Cartridge images: " + ".cci/.zcci/.3ds
  • Installable archives: " + ".cia/.zcia
  • Homebrew titles: .3dsx/.z3dsx
  • NCCH " + "containers: .cxi/.zcxi/.app
  • ELF files: .elf/.axf
"); + 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.
Please follow the guides to redump your " - "game " - "cartridges or " - "installed " - "titles.")); + 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.
Please follow the guides to redump your " - "game " - "cartridges or " - "installed " - "titles.")); + 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.
" + QMessageBox::critical(this, tr("Encrypted application"), + tr("Encrypted applications are not supported.
" "" "Please check our blog for more info.")); break; } case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat: - QMessageBox::critical( - this, tr("Invalid App Format"), - tr("Your app format is not supported.
Please follow the guides to redump your " - "game " - "cartridges or " - "installed " - "titles.")); + 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.
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.
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.
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.
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. " diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index a5aa9896b..81d1956af 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -57,12 +57,15 @@ const std::array, 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 QtConfig::default_hotkeys {{ +const std::array 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); diff --git a/src/citra_qt/configuration/config.h b/src/citra_qt/configuration/config.h index 3fba498ed..6cc87f7f0 100644 --- a/src/citra_qt/configuration/config.h +++ b/src/citra_qt/configuration/config.h @@ -26,7 +26,7 @@ public: static const std::array default_buttons; static const std::array, Settings::NativeAnalog::NumAnalogs> default_analogs; - static const std::array default_hotkeys; + static const std::array default_hotkeys; private: void Initialize(const std::string& config_name); diff --git a/src/citra_qt/configuration/configure_audio.cpp b/src/citra_qt/configuration/configure_audio.cpp index 6dd775926..1a36da86d 100644 --- a/src/citra_qt/configuration/configure_audio.cpp +++ b/src/citra_qt/configuration/configure_audio.cpp @@ -1,4 +1,4 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -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(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); } diff --git a/src/citra_qt/configuration/configure_audio.h b/src/citra_qt/configuration/configure_audio.h index 2da6e3eea..fe6087e07 100644 --- a/src/citra_qt/configuration/configure_audio.h +++ b/src/citra_qt/configuration/configure_audio.h @@ -1,4 +1,4 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -41,5 +41,6 @@ private: ConfigurationShared::CheckState audio_stretching; ConfigurationShared::CheckState realtime_audio; + ConfigurationShared::CheckState simulate_headphones_plugged; std::unique_ptr ui; }; diff --git a/src/citra_qt/configuration/configure_audio.ui b/src/citra_qt/configuration/configure_audio.ui index 706e335c0..946f5c0e7 100644 --- a/src/citra_qt/configuration/configure_audio.ui +++ b/src/citra_qt/configuration/configure_audio.ui @@ -85,26 +85,6 @@ - - - - <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> - - - Enable audio stretching - - - - - - - <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> - - - Enable realtime audio - - - @@ -192,6 +172,36 @@ + + + + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + + + Enable audio stretching + + + + + + + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> + + + Enable realtime audio + + + + + + + <html><head/><body><p>Simulates whether headphones are plugged in to the emulated 3DS system.</p></body></html> + + + Simulate headphones plugged in + + + diff --git a/src/citra_qt/configuration/configure_debug.cpp b/src/citra_qt/configuration/configure_debug.cpp index 0fe4f49cf..bdb945eb9 100644 --- a/src/citra_qt/configuration/configure_debug.cpp +++ b/src/citra_qt/configuration/configure_debug.cpp @@ -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); } diff --git a/src/citra_qt/configuration/configure_debug.ui b/src/citra_qt/configuration/configure_debug.ui index 990835a80..d9c833949 100644 --- a/src/citra_qt/configuration/configure_debug.ui +++ b/src/citra_qt/configuration/configure_debug.ui @@ -17,7 +17,7 @@ - + GDB @@ -60,6 +60,13 @@ + + + + Pause next non-sysmodule process at start + + + @@ -299,6 +306,16 @@ + + + + <html><head/><body><p>Pauses emulation and shows an error message if an unmapped memory access is detected.</p></body></html> + + + Break on unmapped memory access + + + diff --git a/src/citra_qt/configuration/configure_graphics.cpp b/src/citra_qt/configuration/configure_graphics.cpp index aac27ab16..2a2ea3f6c 100644 --- a/src/citra_qt/configuration/configure_graphics.cpp +++ b/src/citra_qt/configuration/configure_graphics.cpp @@ -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) { diff --git a/src/citra_qt/configuration/configure_graphics.h b/src/citra_qt/configuration/configure_graphics.h index 61a462a67..f4ec4f104 100644 --- a/src/citra_qt/configuration/configure_graphics.h +++ b/src/citra_qt/configuration/configure_graphics.h @@ -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; QColor bg_color; }; diff --git a/src/citra_qt/configuration/configure_graphics.ui b/src/citra_qt/configuration/configure_graphics.ui index ff5ec613f..7f9a01956 100644 --- a/src/citra_qt/configuration/configure_graphics.ui +++ b/src/citra_qt/configuration/configure_graphics.ui @@ -372,7 +372,7 @@ 0 - 16000 + 65000 100 @@ -404,6 +404,16 @@ + + + + <html><head/><body><p>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.</p></body></html> + + + Simulate 3DS GPU timings + + + diff --git a/src/citra_qt/configuration/configure_storage.cpp b/src/citra_qt/configuration/configure_storage.cpp index a8cabd987..aa468d4c1 100644 --- a/src/citra_qt/configuration/configure_storage.cpp +++ b/src/citra_qt/configuration/configure_storage.cpp @@ -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, diff --git a/src/citra_qt/configuration/configure_storage.ui b/src/citra_qt/configuration/configure_storage.ui index 5cfd0c449..6e8909a3a 100644 --- a/src/citra_qt/configuration/configure_storage.ui +++ b/src/citra_qt/configuration/configure_storage.ui @@ -180,7 +180,7 @@ - + @@ -191,6 +191,16 @@ + + + + Asynchronous filesystem operations + + + <html><head/><body><p>Makes emulated filesystem accesses asynchronous. Greatly reduces filesystem related stutter, but may slightly increase load times.</p></body></html> + + + diff --git a/src/citra_qt/main.ui b/src/citra_qt/main.ui index acca5361d..1e5dbb4e5 100644 --- a/src/citra_qt/main.ui +++ b/src/citra_qt/main.ui @@ -206,6 +206,16 @@ + + + Debug + + + + + + + @@ -453,6 +463,30 @@ Capture Screenshot + + + true + + + Debug Pause + + + + + true + + + Debug Resume + + + + + true + + + Debug Step + + true diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 4b6e74c49..ae812c89d 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -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 diff --git a/src/common/hacks/hack_list.cpp b/src/common/hacks/hack_list.cpp index 5f320c673..73662c028 100644 --- a/src/common/hacks/hack_list.cpp +++ b/src/common/hacks/hack_list.cpp @@ -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 + }, + }}, }}; } \ No newline at end of file diff --git a/src/common/hacks/hack_list.h b/src/common/hacks/hack_list.h index 61a8cccfa..1c34d22e0 100644 --- a/src/common/hacks/hack_list.h +++ b/src/common/hacks/hack_list.h @@ -16,6 +16,7 @@ enum class HackType : int { REGION_FROM_SECURE, REQUIRES_SHADER_FIXUP, SPOOF_FRIEND_CODE_SEED, + DELAY_TEXTURE_COPY_COMPLETION, }; class UserHackData {}; diff --git a/src/common/memory_ref.h b/src/common/memory_ref.h index f5935bf4c..3e4ddc67f 100644 --- a/src/common/memory_ref.h +++ b/src/common/memory_ref.h @@ -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; }; diff --git a/src/common/optional_helper.h b/src/common/optional_helper.h new file mode 100644 index 000000000..f057973d9 --- /dev/null +++ b/src/common/optional_helper.h @@ -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 +#include + +namespace detail { +template +struct is_optional_trait : std::false_type { + using value_type = T; +}; + +template +struct is_optional_trait> : 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; + * is_optional_type -> false + * is_optional_type -> true + */ +template +inline constexpr bool is_optional_type = detail::is_optional_trait::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; + * optional_value_type -> u32 + * optional_value_type -> u32 + */ +template +using optional_inner_or_type = typename detail::is_optional_trait::value_type; diff --git a/src/common/settings.cpp b/src/common/settings.cpp index 2ac231f50..df8451e28 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -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); diff --git a/src/common/settings.h b/src/common/settings.h index 5eca53421..4196557d1 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -484,6 +484,7 @@ struct Values { Setting use_virtual_sd{true, Keys::use_virtual_sd}; Setting use_custom_storage{false, Keys::use_custom_storage}; Setting compress_cia_installs{false, Keys::compress_cia_installs}; + Setting async_fs_operations{true, Keys::async_fs_operations}; // System SwitchableSetting region_value{REGION_VALUE_AUTO_SELECT, Keys::region_value}; @@ -540,8 +541,9 @@ struct Values { SwitchableSetting texture_filter{TextureFilter::NoFilter, Keys::texture_filter}; SwitchableSetting texture_sampling{TextureSampling::GameControlled, Keys::texture_sampling}; - SwitchableSetting delay_game_render_thread_us{0, 0, 16000, + SwitchableSetting delay_game_render_thread_us{0, 0, 65000, Keys::delay_game_render_thread_us}; + SwitchableSetting simulate_3ds_gpu_timings{true, Keys::simulate_3ds_gpu_timings}; SwitchableSetting layout_option{LayoutOption::Default, Keys::layout_option}; SwitchableSetting swap_screen{false, Keys::swap_screen}; @@ -627,6 +629,7 @@ struct Values { Setting output_device{"Auto", Keys::output_device}; Setting input_type{AudioCore::InputType::Auto, Keys::input_type}; Setting input_device{"Auto", Keys::input_device}; + SwitchableSetting simulate_headphones_plugged{false, Keys::simulate_headphones_plugged}; // Camera std::array camera_name; @@ -642,6 +645,7 @@ struct Values { Setting instant_debug_log{false, Keys::instant_debug_log}; Setting enable_rpc_server{false, Keys::enable_rpc_server}; Setting toggle_unique_data_console_type{false, Keys::toggle_unique_data_console_type}; + Setting break_on_unmapped_memory_access{false, Keys::break_on_unmapped_memory_access}; // Miscellaneous Setting log_filter{"*:Info", Keys::log_filter}; diff --git a/src/common/string_util.cpp b/src/common/string_util.cpp index 1637c01f1..64effc9e2 100644 --- a/src/common/string_util.cpp +++ b/src/common/string_util.cpp @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + // Copyright 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 #endif +#if defined(__APPLE__) +#include +#endif + namespace Common { /// Make a char lowercase @@ -114,8 +122,7 @@ void BuildCompleteFilename(std::string& _CompleteFilename, const std::string& _P _CompleteFilename += _Filename; } -std::vector SplitString(const std::string& str, const char delim) { - std::istringstream iss(str); +static std::vector SplitString(std::istringstream& iss, const char delim) { std::vector output(1); while (std::getline(iss, *output.rbegin(), delim)) { @@ -126,6 +133,16 @@ std::vector SplitString(const std::string& str, const char delim) { return output; } +std::vector SplitString(std::string_view str, const char delim) { + std::istringstream iss{std::string(str)}; + return SplitString(iss, delim); +} + +std::vector 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(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(input.data()), + static_cast(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(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 = diff --git a/src/common/string_util.h b/src/common/string_util.h index 342434928..5c6d0bdac 100644 --- a/src/common/string_util.h +++ b/src/common/string_util.h @@ -36,6 +36,7 @@ namespace Common { [[nodiscard]] bool EndsWith(const std::string& value, const std::string& ending); +[[nodiscard]] std::vector SplitString(std::string_view str, const char delim); [[nodiscard]] std::vector 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); diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index ec7a45bfe..dfcb20a27 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -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 diff --git a/src/core/arm/arm_interface.h b/src/core/arm/arm_interface.h index 1f6ddc9e9..ceb945148 100644 --- a/src/core/arm/arm_interface.h +++ b/src/core/arm/arm_interface.h @@ -1,4 +1,4 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -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 GetPageTable() const = 0; std::shared_ptr 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 page_table{}; ar >> page_table; SetPageTable(page_table); diff --git a/src/core/arm/dynarmic/arm_dynarmic.cpp b/src/core/arm/dynarmic/arm_dynarmic.cpp index 2cdc9c119..64b740569 100644 --- a/src/core/arm/dynarmic/arm_dynarmic.cpp +++ b/src/core/arm/dynarmic/arm_dynarmic.cpp @@ -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 #include #include #include @@ -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 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& 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 ARM_Dynarmic::MakeJit() { diff --git a/src/core/arm/dynarmic/arm_dynarmic.h b/src/core/arm/dynarmic/arm_dynarmic.h index ec0c13390..713bd99db 100644 --- a/src/core/arm/dynarmic/arm_dynarmic.h +++ b/src/core/arm/dynarmic/arm_dynarmic.h @@ -1,4 +1,4 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -56,11 +56,15 @@ public: void ClearExclusiveState() override; void SetPageTable(const std::shared_ptr& page_table) override; + bool HasSingleInstructionBreakAccuracy() override { + return false; + } + protected: std::shared_ptr GetPageTable() const override; private: - void ServeBreak(); + void ServeBreak(int signal); friend class DynarmicUserCallbacks; Core::System& system; diff --git a/src/core/arm/dyncom/arm_dyncom.cpp b/src/core/arm/dyncom/arm_dyncom.cpp index c9bb66e38..63db2f9c4 100644 --- a/src/core/arm/dyncom/arm_dyncom.cpp +++ b/src/core/arm/dyncom/arm_dyncom.cpp @@ -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(timer->GetDowncount(), 0)); } void ARM_DynCom::Step() { + if (break_flag) [[unlikely]] { + return; + } ExecuteInstructions(1); } diff --git a/src/core/arm/dyncom/arm_dyncom.h b/src/core/arm/dyncom/arm_dyncom.h index 3fd37989a..cfe1d3e92 100644 --- a/src/core/arm/dyncom/arm_dyncom.h +++ b/src/core/arm/dyncom/arm_dyncom.h @@ -1,4 +1,4 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -51,6 +51,10 @@ public: void SetPageTable(const std::shared_ptr& page_table) override; void PrepareReschedule() override; + bool HasSingleInstructionBreakAccuracy() override { + return true; + } + protected: std::shared_ptr GetPageTable() const override; diff --git a/src/core/arm/dyncom/arm_dyncom_interpreter.cpp b/src/core/arm/dyncom/arm_dyncom_interpreter.cpp index 862c422bc..af30fdad3 100644 --- a/src/core/arm/dyncom/arm_dyncom_interpreter.cpp +++ b/src/core/arm/dyncom/arm_dyncom_interpreter.cpp @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + // Copyright 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 = diff --git a/src/core/arm/skyeye_common/armstate.cpp b/src/core/arm/skyeye_common/armstate.cpp index 9de68dc11..9ff89c5a7 100644 --- a/src/core/arm/skyeye_common/armstate.cpp +++ b/src/core/arm/skyeye_common/armstate.cpp @@ -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 +#include #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 } diff --git a/src/core/arm/skyeye_common/armstate.h b/src/core/arm/skyeye_common/armstate.h index f36c46c07..925881304 100644 --- a/src/core/arm/skyeye_common/armstate.h +++ b/src/core/arm/skyeye_common/armstate.h @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + /* armdefs.h -- ARMulator common definitions: ARM6 Instruction Emulator. Copyright (C) 1994 Advanced RISC Machines Ltd. @@ -21,7 +25,9 @@ #include #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 }; diff --git a/src/core/core.cpp b/src/core/core.cpp index 0132e24bd..f9dfe1534 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -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(); @@ -581,8 +581,9 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, auto gsp = service_manager->GetService("gsp::Gpu"); gpu = std::make_unique(*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("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. diff --git a/src/core/core.h b/src/core/core.h index 05620da32..c46a196b2 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -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 info_led_color; std::function info_led_color_changed; + bool debug_next_process; + friend class boost::serialization::access; template void serialize(Archive& ar, const unsigned int file_version); diff --git a/src/core/file_sys/disk_archive.cpp b/src/core/file_sys/disk_archive.cpp index a7ae5e92e..db0290df6 100644 --- a/src/core/file_sys/disk_archive.cpp +++ b/src/core/file_sys/disk_archive.cpp @@ -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 +#include #include #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 diff --git a/src/core/file_sys/layered_fs.cpp b/src/core/file_sys/layered_fs.cpp index 1cac97be4..93ce3e854 100644 --- a/src/core/file_sys/layered_fs.cpp +++ b/src/core/file_sys/layered_fs.cpp @@ -1,4 +1,4 @@ -// Copyright 2020 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -247,14 +247,14 @@ void LayeredFS::LoadExtRelocations() { std::vector 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; diff --git a/src/core/file_sys/layered_fs.h b/src/core/file_sys/layered_fs.h index 69f6e5939..e51bd8f1c 100644 --- a/src/core/file_sys/layered_fs.h +++ b/src/core/file_sys/layered_fs.h @@ -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. diff --git a/src/core/file_sys/ncch_container.cpp b/src/core/file_sys/ncch_container.cpp index 382c8943e..c51de40a9 100644 --- a/src/core/file_sys/ncch_container.cpp +++ b/src/core/file_sys/ncch_container.cpp @@ -518,7 +518,7 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect Loader::ResultStatus NCCHContainer::ApplyCodePatch(std::vector& code) const { struct PatchLocation { std::string path; - bool (*patch_fn)(const std::vector& patch, std::vector& code); + Loader::ResultStatus (*patch_fn)(const std::vector& patch, std::vector& code); }; const auto mods_path = @@ -555,11 +555,12 @@ Loader::ResultStatus NCCHContainer::ApplyCodePatch(std::vector& code) const std::vector 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; } diff --git a/src/core/file_sys/patch.cpp b/src/core/file_sys/patch.cpp index fecc7fe05..83d3989eb 100644 --- a/src/core/file_sys/patch.cpp +++ b/src/core/file_sys/patch.cpp @@ -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& ips, std::vector& buffer) { +Loader::ResultStatus ApplyIpsPatch(const std::vector& ips, std::vector& 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& ips, std::vector& 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& ips, std::vector& 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 source, Stream target, Stream patch) : m_source{source}, m_target{target}, m_patch{patch} {} - bool Apply() { + Loader::ResultStatus Apply() { const auto magic = *m_patch.Read>(); 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& patch, std::vector& buffer) { +Loader::ResultStatus ApplyBpsPatch(const std::vector& patch, std::vector& buffer) { Bps::Stream patch_stream{patch.data(), patch.size()}; // Move the offset past the file format marker (i.e. "BPS1") diff --git a/src/core/file_sys/patch.h b/src/core/file_sys/patch.h index 9a8118475..6f8993384 100644 --- a/src/core/file_sys/patch.h +++ b/src/core/file_sys/patch.h @@ -1,4 +1,4 @@ -// Copyright 2019 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -7,11 +7,12 @@ #include #include "common/common_types.h" +#include "core/loader/loader.h" namespace FileSys::Patch { -bool ApplyIpsPatch(const std::vector& patch, std::vector& buffer); +Loader::ResultStatus ApplyIpsPatch(const std::vector& patch, std::vector& buffer); -bool ApplyBpsPatch(const std::vector& patch, std::vector& buffer); +Loader::ResultStatus ApplyBpsPatch(const std::vector& patch, std::vector& buffer); } // namespace FileSys::Patch diff --git a/src/core/gdbstub/gdbstub.cpp b/src/core/gdbstub/gdbstub.cpp index 660168bc1..017677606 100644 --- a/src/core/gdbstub/gdbstub.cpp +++ b/src/core/gdbstub/gdbstub.cpp @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + // Copyright 2013 Dolphin Emulator Project // Licensed under GPLv2+ // Refer to the license.txt file included. @@ -36,8 +40,16 @@ #include "core/gdbstub/gdbstub.h" #include "core/gdbstub/hio.h" #include "core/hle/kernel/process.h" +#include "core/loader/loader.h" #include "core/memory.h" +#ifndef ENABLE_GDBSTUB +#error "File was compiled with GDB stub support disabled" +#endif + +// Uncomment to log all GDB traffic +// #define PRINT_GDB_TRAFFIC + namespace GDBStub { namespace { constexpr int GDB_BUFFER_SIZE = 10000; @@ -59,20 +71,38 @@ constexpr u32 SIGTERM = 15; constexpr u32 MSG_WAITALL = 8; #endif +#ifndef SIGSEGV +constexpr u32 SIGSEGV = 11; +#endif + +#ifdef _WIN32 +using SOCKET = UINT_PTR; +#else +using SOCKET = int; +#define closesocket(x) close(x) +#endif // _WIN32 + +#define INVALID_SOCKET ((SOCKET)(~0)) + constexpr u32 SP_REGISTER = 13; constexpr u32 LR_REGISTER = 14; constexpr u32 PC_REGISTER = 15; constexpr u32 CPSR_REGISTER = 25; constexpr u32 D0_REGISTER = 26; constexpr u32 FPSCR_REGISTER = 42; +constexpr u32 FPEXC_REGISTER = 43; // For sample XML files see the GDB source /gdb/features // GDB also wants the l character at the start // This XML defines what the registers are for this specific ARM device +// The CPSR is register 25, rather than register 16, because the FPA registers historically were +// placed between the PC and the CPSR in the "g" packet. constexpr char target_xml[] = R"(l + arm + 3DS @@ -90,11 +120,6 @@ constexpr char target_xml[] = - - - @@ -115,32 +140,41 @@ constexpr char target_xml[] = + )"; -int gdbserver_socket = -1; +SOCKET gdbserver_socket = INVALID_SOCKET; bool defer_start = false; u8 command_buffer[GDB_BUFFER_SIZE]; -u32 command_length; +u32 recv_command_length; +u32 send_command_length; u32 latest_signal = 0; -bool memory_break = false; static Kernel::Thread* current_thread = nullptr; +static Kernel::Process* current_process = nullptr; // Binding to a port within the reserved ports range (0-1023) requires root permissions, // so default to a port outside of that range. u16 gdbstub_port = 24689; -bool halt_loop = true; -bool step_loop = false; -bool send_trap = false; +constexpr bool supports_extended_mode = true; +bool is_extended_mode = false; + +bool is_running = false; +bool current_process_finished = false; // If set to false, the server will never be started and no // gdbstub-related functions will be executed. std::atomic server_enabled(false); +SOCKET accept_socket = INVALID_SOCKET; +int continue_thread = -1; + +static Kernel::Thread* break_thread = nullptr; +static int break_signal = 0; #ifdef _WIN32 WSADATA InitData; @@ -159,17 +193,47 @@ BreakpointMap breakpoints_read; BreakpointMap breakpoints_write; } // Anonymous namespace +static void ResetState() { + gdbserver_socket = INVALID_SOCKET; + defer_start = false; + + memset(command_buffer, 0, GDB_BUFFER_SIZE); + recv_command_length = 0; + send_command_length = 0; + + latest_signal = 0; + + current_thread = nullptr; + current_process = nullptr; + + is_extended_mode = false; + + is_running = false; + current_process_finished = false; + + accept_socket = INVALID_SOCKET; + continue_thread = -1; + + break_thread = nullptr; + break_signal = 0; + + breakpoints_execute.clear(); + breakpoints_read.clear(); + breakpoints_write.clear(); +} + static Kernel::Thread* FindThreadById(int id) { - u32 num_cores = Core::GetNumCores(); - for (u32 i = 0; i < num_cores; ++i) { - const auto& threads = - Core::System::GetInstance().Kernel().GetThreadManager(i).GetThreadList(); - for (auto& thread : threads) { - if (thread->GetThreadId() == static_cast(id)) { - return thread.get(); - } + if (!current_process) { + return nullptr; + } + + auto thread_list = current_process->GetThreadList(); + for (auto& thread : thread_list) { + if (thread->GetThreadId() == static_cast(id)) { + return thread.get(); } } + return nullptr; } @@ -210,6 +274,8 @@ static u64 FpuRead(std::size_t id, Kernel::Thread* thread = nullptr) { return ret; } else if (id == FPSCR_REGISTER) { return thread->context.fpscr; + } else if (id == FPEXC_REGISTER) { + return thread->context.fpexc; } else { return 0; } @@ -225,6 +291,15 @@ static void FpuWrite(std::size_t id, u64 val, Kernel::Thread* thread = nullptr) thread->context.fpu_registers[2 * (id - D0_REGISTER) + 1] = static_cast(val >> 32); } else if (id == FPSCR_REGISTER) { thread->context.fpscr = static_cast(val); + } else if (id == FPEXC_REGISTER) { + thread->context.fpexc = static_cast(val); + } +} + +// Clear instruction cache for all cores. +static void ClearAllInstructionCache() { + for (int i = 0; i < Core::GetNumCores(); i++) { + Core::GetCore(i).ClearInstructionCache(); } } @@ -356,13 +431,42 @@ static u64 GdbHexToLong(const u8* src) { return output; } +static int GetErrno() { +#ifdef _WIN32 + return WSAGetLastError(); +#else + return errno; +#endif +} + +static bool SetNonBlock(SOCKET socket, bool nonblock) { +#ifdef _WIN32 + unsigned long nonblocking = nonblock ? 1 : 0; + int ret = ioctlsocket(socket, FIONBIO, &nonblocking); + if (ret < 0) { + LOG_ERROR(Debug_GDBStub, "Failed to set non-blocking gdb socket"); + return false; + } +#else + int flags = nonblock ? O_NONBLOCK : 0; + + const int ret = ::fcntl(socket, F_SETFL, flags); + if (ret < 0) { + LOG_ERROR(Debug_GDBStub, "Failed to set non-blocking gdb socket"); + return false; + } +#endif + return true; +} + /// Read a byte from the gdb client. static u8 ReadByte() { - u8 c; + u8 c{}; std::size_t received_size = recv(gdbserver_socket, reinterpret_cast(&c), 1, MSG_WAITALL); if (received_size != 1) { - LOG_ERROR(Debug_GDBStub, "recv failed : {}", received_size); - Shutdown(); + LOG_ERROR(Debug_GDBStub, "recv failed : {}", GetErrno()); + ToggleServer(false); + ToggleServer(true); } return c; @@ -409,17 +513,34 @@ static void RemoveBreakpoint(BreakpointType type, VAddr addr) { bp->second.len, bp->second.addr, type); if (type == BreakpointType::Execute) { - Core::System::GetInstance().Memory().WriteBlock( - *Core::System::GetInstance().Kernel().GetCurrentProcess(), bp->second.addr, - bp->second.inst.data(), bp->second.inst.size()); + Core::System::GetInstance().Memory().WriteBlock(*current_process, bp->second.addr, + bp->second.inst.data(), bp->second.len); u32 num_cores = Core::GetNumCores(); for (u32 i = 0; i < num_cores; ++i) { Core::GetCore(i).ClearInstructionCache(); } + } else { + Core::System::GetInstance().Memory().UnregisterWatchpoint(*current_process, bp->second.addr, + bp->second.len); } p.erase(addr); } +void RemoveAllBreakpoints() { + std::vector> trash_bin; + + for (auto type : {BreakpointType::Execute, BreakpointType::Read, BreakpointType::Write}) { + auto& map = GetBreakpointMap(type); + for (auto b : map) { + trash_bin.push_back({type, b.first}); + } + } + + for (auto& p : trash_bin) { + RemoveBreakpoint(p.first, p.second); + } +} + BreakpointAddress GetNextBreakpointFromAddress(VAddr addr, BreakpointType type) { const BreakpointMap& p = GetBreakpointMap(type); const auto next_breakpoint = p.lower_bound(addr); @@ -436,38 +557,65 @@ BreakpointAddress GetNextBreakpointFromAddress(VAddr addr, BreakpointType type) return breakpoint; } -bool CheckBreakpoint(VAddr addr, BreakpointType type) { +bool CheckBreakpoint(VAddr addr, u32 access_len, BreakpointType type) { if (!IsConnected()) { return false; } const BreakpointMap& p = GetBreakpointMap(type); - const auto bp = p.find(addr); - if (bp == p.end()) { - return false; - } + // Access range: [addr, access_end) + const VAddr access_end = addr + access_len; - u32 len = bp->second.len; + for (const auto& [base_addr, bp] : p) { + if (!bp.active) { + continue; + } - // IDA Pro defaults to 4-byte breakpoints for all non-hardware breakpoints - // no matter if it's a 4-byte or 2-byte instruction. When you execute a - // Thumb instruction with a 4-byte breakpoint set, it will set a breakpoint on - // two instructions instead of the single instruction you placed the breakpoint - // on. So, as a way to make sure that execution breakpoints are only breaking - // on the instruction that was specified, set the length of an execution - // breakpoint to 1. This should be fine since the CPU should never begin executing - // an instruction anywhere except the beginning of the instruction. - if (type == BreakpointType::Execute) { - len = 1; - } + u32 bp_len = bp.len; - if (bp->second.active && (addr >= bp->second.addr && addr < bp->second.addr + len)) { - LOG_DEBUG(Debug_GDBStub, - "Found breakpoint type {} @ {:08x}, range: {:08x}" - " - {:08x} ({:x} bytes)", - type, addr, bp->second.addr, bp->second.addr + len, len); - return true; + // IDA Pro defaults to 4-byte breakpoints for all non-hardware breakpoints + // no matter if it's a 4-byte or 2-byte instruction. When you execute a + // Thumb instruction with a 4-byte breakpoint set, it will set a breakpoint on + // two instructions instead of the single instruction you placed the breakpoint + // on. So, as a way to make sure that execution breakpoints are only breaking + // on the instruction that was specified, set the length of an execution + // breakpoint to 1. This should be fine since the CPU should never begin executing + // an instruction anywhere except the beginning of the instruction. + if (type == BreakpointType::Execute) { + bp_len = 1; + } + + // Breakpoint/watchpoint range: [bp.addr, bp_end) + const VAddr bp_end = bp.addr + bp_len; + + bool hit = false; + + if (type == BreakpointType::Execute) { + // Execute breakpoints should only trigger on exact PC match. + hit = (addr == bp.addr); + } else { + // Range overlap test: + // [addr, access_end) overlaps [bp.addr, bp_end) + hit = (addr < bp_end) && (bp.addr < access_end); + } + + if (hit) { + LOG_DEBUG(Debug_GDBStub, + "Found breakpoint type {}, " + "access range: {:08x} - {:08x}, " + "breakpoint range: {:08x} - {:08x}", + type, addr, addr, access_end, bp.addr, bp_end); + + if (type != BreakpointType::Execute && + !Core::GetCore(0).HasSingleInstructionBreakAccuracy()) { + LOG_WARNING(Debug_GDBStub, + "The current CPU backend does not support accurate watchpoints and " + "memory exceptions. Disable CPU JIT for more accuracy."); + } + + return true; + } } return false; @@ -492,28 +640,35 @@ void SendReply(const char* reply) { std::memset(command_buffer, 0, sizeof(command_buffer)); - command_length = static_cast(strlen(reply)); - if (command_length + 4 > sizeof(command_buffer)) { + send_command_length = static_cast(strlen(reply)); + +#ifdef PRINT_GDB_TRAFFIC + LOG_INFO(Debug_GDBStub, "Res: {}", reply); +#endif + + if (send_command_length + 4 > sizeof(command_buffer)) { LOG_ERROR(Debug_GDBStub, "command_buffer overflow in SendReply"); return; } - std::memcpy(command_buffer + 1, reply, command_length); + std::memcpy(command_buffer + 1, reply, send_command_length); - u8 checksum = CalculateChecksum(command_buffer, command_length + 1); + u8 checksum = CalculateChecksum(command_buffer, send_command_length + 1); command_buffer[0] = GDB_STUB_START; - command_buffer[command_length + 1] = GDB_STUB_END; - command_buffer[command_length + 2] = NibbleToHex(checksum >> 4); - command_buffer[command_length + 3] = NibbleToHex(checksum); + command_buffer[send_command_length + 1] = GDB_STUB_END; + command_buffer[send_command_length + 2] = NibbleToHex(checksum >> 4); + command_buffer[send_command_length + 3] = NibbleToHex(checksum); u8* ptr = command_buffer; - u32 left = command_length + 4; + u32 left = send_command_length + 4; while (left > 0) { s32 sent_size = static_cast(send(gdbserver_socket, reinterpret_cast(ptr), left, 0)); if (sent_size < 0) { LOG_ERROR(Debug_GDBStub, "gdb: send failed"); - return Shutdown(); + ToggleServer(false); + ToggleServer(true); + return; } left -= sent_size; @@ -528,60 +683,103 @@ static void HandleQuery() { if (strcmp(query, "TStatus") == 0) { SendReply("T0"); + } else if (strncmp(query, "Attached", strlen("Attached")) == 0) { + SendReply("1"); } else if (strncmp(query, "Supported", strlen("Supported")) == 0) { // PacketSize needs to be large enough for target xml - SendReply("PacketSize=2000;qXfer:features:read+;qXfer:threads:read+"); + SendReply("PacketSize=9800;qXfer:features:read+;qXfer:osdata:read+;qXfer:threads:read+;" + "vContSupported+"); } else if (strncmp(query, "Xfer:features:read:target.xml:", strlen("Xfer:features:read:target.xml:")) == 0) { SendReply(target_xml); } else if (strncmp(query, "fThreadInfo", strlen("fThreadInfo")) == 0) { - std::string val = "m"; - u32 num_cores = Core::GetNumCores(); - for (u32 i = 0; i < num_cores; ++i) { - const auto& threads = - Core::System::GetInstance().Kernel().GetThreadManager(i).GetThreadList(); - for (const auto& thread : threads) { - val += fmt::format("{:x},", thread->GetThreadId()); - } + if (!current_process) { + SendReply("E01"); + return; } + + auto thread_list = current_process->GetThreadList(); + std::string val = "m"; + for (const auto& thread : thread_list) { + val += fmt::format("{:x},", thread->GetThreadId()); + } + val.pop_back(); SendReply(val.c_str()); } else if (strncmp(query, "sThreadInfo", strlen("sThreadInfo")) == 0) { SendReply("l"); + } else if (strncmp(query, "Xfer:osdata:read:processes", strlen("Xfer:osdata:read:processes")) == + 0) { + std::string buffer; + buffer += "l"; + auto process_list = Core::System::GetInstance().Kernel().GetProcessList(); + for (const auto& p : process_list) { + buffer += fmt::format("" + "{}" + "{}" + "", + p->process_id, p->codeset->name); + } + buffer += ""; + SendReply(buffer.c_str()); } else if (strncmp(query, "Xfer:threads:read", strlen("Xfer:threads:read")) == 0) { + if (!current_process) { + SendReply("E01"); + return; + } + std::string buffer; buffer += "l"; buffer += ""; - u32 num_cores = Core::GetNumCores(); - for (u32 i = 0; i < num_cores; ++i) { - const auto& threads = - Core::System::GetInstance().Kernel().GetThreadManager(i).GetThreadList(); - for (const auto& thread : threads) { - buffer += fmt::format(R"*()*", - thread->GetThreadId(), thread->GetThreadId()); - } + auto thread_list = current_process->GetThreadList(); + for (const auto& thread : thread_list) { + buffer += fmt::format(R"*()*", + thread->GetThreadId(), thread->GetThreadId()); } buffer += ""; + SendReply(buffer.c_str()); } else { SendReply(""); } } - -/// Handle set thread command from gdb client. -static void HandleSetThread() { - int thread_id = -1; - if (command_buffer[2] != '-') { - thread_id = static_cast(HexToInt(command_buffer + 2, command_length - 2)); +static bool SetThread(int thread_id) { + if (!current_process) { + // The process has not been selected yet + return false; } + if (thread_id >= 1) { current_thread = FindThreadById(thread_id); } if (!current_thread) { - thread_id = 1; - current_thread = FindThreadById(thread_id); + auto thread_list = current_process->GetThreadList(); + if (thread_list.size() > 0) { + // Select the lowest thread ID, which is the main thread + std::sort(thread_list.begin(), thread_list.end(), + [](const std::shared_ptr& a, + const std::shared_ptr& b) { + return a->thread_id < b->thread_id; + }); + current_thread = thread_list[0].get(); + } } - if (current_thread) { + return current_thread != nullptr; +} +/// Handle set thread command from gdb client. +static void HandleSetThread() { + int thread_id = -1; + if (command_buffer[2] != '-') { + thread_id = static_cast(HexToInt(command_buffer + 2, recv_command_length - 2)); + } + + if (command_buffer[1] == 'c') { + continue_thread = thread_id; + SendReply("OK"); + return; + } + + if (SetThread(thread_id)) { SendReply("OK"); return; } @@ -590,7 +788,12 @@ static void HandleSetThread() { /// Handle thread alive command from gdb client. static void HandleThreadAlive() { - int thread_id = static_cast(HexToInt(command_buffer + 1, command_length - 1)); + if (!current_process) { + SendReply("E01"); + return; + } + + int thread_id = static_cast(HexToInt(command_buffer + 1, recv_command_length - 1)); if (thread_id == 0) { thread_id = 1; } @@ -601,13 +804,27 @@ static void HandleThreadAlive() { SendReply("E01"); } +static void HandleExtendedMode() { + if (supports_extended_mode) { + is_extended_mode = true; + SendReply("OK"); + } else { + SendReply(""); + } +} + /** * Send signal packet to client. * * @param signal Signal to be sent to client. */ -static void SendSignal(Kernel::Thread* thread, u32 signal, bool full = true) { - if (gdbserver_socket == -1) { +static void SendStopReply(Kernel::Thread* thread, u32 signal, bool full = true) { + if (gdbserver_socket == INVALID_SOCKET) { + return; + } + + if (current_process_finished) { + SendReply("W00"); return; } @@ -619,11 +836,16 @@ static void SendSignal(Kernel::Thread* thread, u32 signal, bool full = true) { std::string buffer; if (full) { - + Core::ARM_Interface::ThreadContext ctx{}; + if (thread) { + ctx = thread->context; + } else { + Core::GetRunningCore().SaveContext(ctx); + } buffer = fmt::format("T{:02x}{:02x}:{:08x};{:02x}:{:08x};{:02x}:{:08x}", latest_signal, - PC_REGISTER, htonl(Core::GetRunningCore().GetPC()), SP_REGISTER, - htonl(Core::GetRunningCore().GetReg(SP_REGISTER)), LR_REGISTER, - htonl(Core::GetRunningCore().GetReg(LR_REGISTER))); + PC_REGISTER, htonl(ctx.cpu_registers[PC_REGISTER]), SP_REGISTER, + htonl(ctx.cpu_registers[SP_REGISTER]), LR_REGISTER, + htonl(ctx.cpu_registers[LR_REGISTER])); } else { buffer = fmt::format("T{:02x}", latest_signal); } @@ -636,9 +858,59 @@ static void SendSignal(Kernel::Thread* thread, u32 signal, bool full = true) { SendReply(buffer.c_str()); } +static void HandleGetStopReason() { + if (is_extended_mode) { + // In extended mode, tell the debugger that there is no selected process yet. + // That way the debugger can ask for the process list and attach to the right one. + if (!current_process) { + // The process has not been selected yet. + SendReply("W00"); + } else { + SendStopReply(current_thread, latest_signal); + } + } else { + // In non extended mode, select the process corresponding to the "main" + // launched application. + u64 program_id = 0; + Core::System::GetInstance().GetAppLoader().ReadProgramId(program_id); + auto process_list = Core::System::GetInstance().Kernel().GetProcessList(); + for (const auto& process : process_list) { + if (process->codeset->program_id == program_id) { + current_process = process.get(); + current_process->SetDebugBreak(true); + is_running = false; + if (SetThread(0)) { + SendStopReply(current_thread, 0); + } else { + // Should never happen + SendReply("W00"); + } + return; + } + } + + // No process, should never happen + SendReply("W00"); + } +} + +static void BreakImpl(int signal) { + if (signal == SIGSEGV && !Core::GetCore(0).HasSingleInstructionBreakAccuracy()) { + LOG_WARNING(Debug_GDBStub, "The current CPU backend does not support accurate watchpoints " + "and memory exceptions. Disable CPU JIT for more accuracy."); + } + + current_process->SetDebugBreak(true); + is_running = false; + + latest_signal = signal; + + SendStopReply(current_thread, signal); +} + /// Read command from gdb client. static void ReadCommand() { - command_length = 0; + recv_command_length = 0; std::memset(command_buffer, 0, sizeof(command_buffer)); u8 c = ReadByte(); @@ -647,8 +919,7 @@ static void ReadCommand() { return; } else if (c == 0x03) { LOG_INFO(Debug_GDBStub, "gdb: found break command\n"); - halt_loop = true; - SendSignal(current_thread, SIGTRAP); + BreakImpl(SIGTRAP); return; } else if (c != GDB_STUB_START) { LOG_DEBUG(Debug_GDBStub, "gdb: read invalid byte {:02x}\n", c); @@ -656,27 +927,27 @@ static void ReadCommand() { } while ((c = ReadByte()) != GDB_STUB_END) { - if (command_length >= sizeof(command_buffer)) { + if (recv_command_length >= sizeof(command_buffer)) { LOG_ERROR(Debug_GDBStub, "gdb: command_buffer overflow\n"); SendPacket(GDB_STUB_NACK); return; } - command_buffer[command_length++] = c; + command_buffer[recv_command_length++] = c; } u8 checksum_received = HexCharToValue(ReadByte()) << 4; checksum_received |= HexCharToValue(ReadByte()); - u8 checksum_calculated = CalculateChecksum(command_buffer, command_length); + u8 checksum_calculated = CalculateChecksum(command_buffer, recv_command_length); if (checksum_received != checksum_calculated) { LOG_ERROR( Debug_GDBStub, "gdb: invalid checksum: calculated {:02x} and read {:02x} for ${}# (length: {})\n", checksum_calculated, checksum_received, reinterpret_cast(command_buffer), - command_length); + recv_command_length); - command_length = 0; + recv_command_length = 0; SendPacket(GDB_STUB_NACK); return; @@ -694,7 +965,7 @@ static bool IsDataAvailable() { fd_set fd_socket; FD_ZERO(&fd_socket); - FD_SET(static_cast(gdbserver_socket), &fd_socket); + FD_SET(gdbserver_socket, &fd_socket); struct timeval t; t.tv_sec = 0; @@ -710,6 +981,11 @@ static bool IsDataAvailable() { /// Send requested register to gdb client. static void ReadRegister() { + if (!current_process || !current_thread) { + SendReply("E01"); + return; + } + static u8 reply[64]; std::memset(reply, 0, sizeof(reply)); @@ -727,6 +1003,8 @@ static void ReadRegister() { LongToGdbHex(reply, FpuRead(id, current_thread)); } else if (id == FPSCR_REGISTER) { IntToGdbHex(reply, static_cast(FpuRead(id, current_thread))); + } else if (id == FPEXC_REGISTER) { + IntToGdbHex(reply, static_cast(FpuRead(id, current_thread))); } else { return SendReply("E01"); } @@ -736,6 +1014,11 @@ static void ReadRegister() { /// Send all registers to the gdb client. static void ReadRegisters() { + if (!current_process || !current_thread) { + SendReply("E01"); + return; + } + static u8 buffer[GDB_BUFFER_SIZE - 4]; std::memset(buffer, 0, sizeof(buffer)); @@ -759,11 +1042,31 @@ static void ReadRegisters() { IntToGdbHex(bufptr, static_cast(FpuRead(FPSCR_REGISTER, current_thread))); + bufptr += 8; + + IntToGdbHex(bufptr, static_cast(FpuRead(FPEXC_REGISTER, current_thread))); + SendReply(reinterpret_cast(buffer)); } +static void UpdateCPUThreadContext() { + u32 core_id = current_thread->core_id; + auto& system = Core::System::GetInstance(); + auto& thread_manager = system.Kernel().GetThreadManager(core_id); + if (thread_manager.GetCurrentThread() == current_thread) { + // Only update CPU context if current thread is active, + // otherwise it will be updated when the thread is selected + Core::GetCore(current_thread->core_id).LoadContext(current_thread->context); + } +} + /// Modify data of register specified by gdb client. static void WriteRegister() { + if (!current_process || !current_thread) { + SendReply("E01"); + return; + } + const u8* buffer_ptr = command_buffer + 3; u32 id = HexCharToValue(command_buffer[1]); @@ -781,71 +1084,84 @@ static void WriteRegister() { FpuWrite(id, GdbHexToLong(buffer_ptr), current_thread); } else if (id == FPSCR_REGISTER) { FpuWrite(id, GdbHexToInt(buffer_ptr), current_thread); + } else if (id == FPEXC_REGISTER) { + FpuWrite(id, GdbHexToInt(buffer_ptr), current_thread); } else { return SendReply("E01"); } - Core::GetRunningCore().LoadContext(current_thread->context); + UpdateCPUThreadContext(); SendReply("OK"); } /// Modify all registers with data received from the client. static void WriteRegisters() { + if (!current_process || !current_thread) { + SendReply("E01"); + return; + } + const u8* buffer_ptr = command_buffer + 1; if (command_buffer[0] != 'G') return SendReply("E01"); - for (u32 i = 0, reg = 0; reg <= FPSCR_REGISTER; i++, reg++) { + for (u32 i = 0, reg = 0; reg <= FPEXC_REGISTER; i++, reg++) { if (reg <= PC_REGISTER) { - RegWrite(reg, GdbHexToInt(buffer_ptr + i * 8)); + RegWrite(reg, GdbHexToInt(buffer_ptr + i * 8), current_thread); } else if (reg == CPSR_REGISTER) { - RegWrite(reg, GdbHexToInt(buffer_ptr + i * 8)); + RegWrite(reg, GdbHexToInt(buffer_ptr + i * 8), current_thread); } else if (reg == CPSR_REGISTER - 1) { // Dummy FPA register, ignore } else if (reg < CPSR_REGISTER) { // Dummy FPA registers, ignore i += 2; } else if (reg >= D0_REGISTER && reg < FPSCR_REGISTER) { - FpuWrite(reg, GdbHexToLong(buffer_ptr + i * 16)); + FpuWrite(reg, GdbHexToLong(buffer_ptr + i * 16), current_thread); i++; // Skip padding } else if (reg == FPSCR_REGISTER) { - FpuWrite(reg, GdbHexToInt(buffer_ptr + i * 8)); + FpuWrite(reg, GdbHexToInt(buffer_ptr + i * 8), current_thread); + } else if (reg == FPEXC_REGISTER) { + FpuWrite(reg, GdbHexToInt(buffer_ptr + i * 8), current_thread); } } - Core::GetRunningCore().LoadContext(current_thread->context); + UpdateCPUThreadContext(); SendReply("OK"); } /// Read location in memory specified by gdb client. static void ReadMemory() { + if (!current_process) { + SendReply(""); + return; + } + static u8 reply[GDB_BUFFER_SIZE - 4]; auto start_offset = command_buffer + 1; - auto addr_pos = std::find(start_offset, command_buffer + command_length, ','); + auto addr_pos = std::find(start_offset, command_buffer + recv_command_length, ','); VAddr addr = HexToInt(start_offset, static_cast(addr_pos - start_offset)); start_offset = addr_pos + 1; - u32 len = - HexToInt(start_offset, static_cast((command_buffer + command_length) - start_offset)); + u32 len = HexToInt(start_offset, + static_cast((command_buffer + recv_command_length) - start_offset)); LOG_DEBUG(Debug_GDBStub, "ReadMemory addr: {:08x} len: {:08x}", addr, len); if (len * 2 > sizeof(reply)) { - SendReply("E01"); + SendReply(""); } auto& memory = Core::System::GetInstance().Memory(); - if (!memory.IsValidVirtualAddress(*Core::System::GetInstance().Kernel().GetCurrentProcess(), - addr)) { - return SendReply("E00"); + if (!memory.IsValidVirtualAddress(*current_process, addr)) { + return SendReply(""); } std::vector data(len); - memory.ReadBlock(addr, data.data(), len); + memory.ReadBlock(*current_process, addr, data.data(), len); MemToGdbHex(reply, data.data(), len); reply[len * 2] = '\0'; @@ -858,60 +1174,84 @@ static void ReadMemory() { /// Modify location in memory with data received from the gdb client. static void WriteMemory() { + if (!current_process) { + SendReply("E01"); + return; + } + auto start_offset = command_buffer + 1; - auto addr_pos = std::find(start_offset, command_buffer + command_length, ','); + auto addr_pos = std::find(start_offset, command_buffer + recv_command_length, ','); VAddr addr = HexToInt(start_offset, static_cast(addr_pos - start_offset)); start_offset = addr_pos + 1; - auto len_pos = std::find(start_offset, command_buffer + command_length, ':'); + auto len_pos = std::find(start_offset, command_buffer + recv_command_length, ':'); u32 len = HexToInt(start_offset, static_cast(len_pos - start_offset)); auto& memory = Core::System::GetInstance().Memory(); - if (!memory.IsValidVirtualAddress(*Core::System::GetInstance().Kernel().GetCurrentProcess(), - addr)) { - return SendReply("E00"); + if (!memory.IsValidVirtualAddress(*current_process, addr)) { + return SendReply("E0E"); } std::vector data(len); GdbHexToMem(data.data(), len_pos + 1, len); - memory.WriteBlock(addr, data.data(), len); - Core::GetRunningCore().ClearInstructionCache(); + memory.WriteBlock(*current_process, addr, data.data(), len); + ClearAllInstructionCache(); SendReply("OK"); } -void Break(bool is_memory_break) { - send_trap = true; - - memory_break = is_memory_break; -} - -/// Tell the CPU that it should perform a single step. -static void Step() { - if (command_length > 1) { - RegWrite(PC_REGISTER, GdbHexToInt(command_buffer + 1), current_thread); - Core::GetRunningCore().LoadContext(current_thread->context); +void Break(int signal) { + if (!IsConnected() || !current_process || + Core::System::GetInstance().Kernel().GetCurrentProcess().get() != current_process) { + LOG_ERROR(Debug_GDBStub, "Got signal for un-attached process, ignoring..."); + return; } - step_loop = true; - halt_loop = true; - send_trap = true; + + if (break_thread) { + LOG_ERROR(Debug_GDBStub, + "Got multiple break signals in quick succession, latest may be lost"); + return; + } + + break_thread = + Core::System::GetInstance().Kernel().GetCurrentThreadManager().GetCurrentThread(); + if (break_thread) { + break_signal = signal; + } + + // Try to break CPU asap + Core::GetRunningCore().SetBreakFlag(); Core::GetRunningCore().ClearInstructionCache(); -} - -bool IsMemoryBreak() { - if (!IsConnected()) { - return false; - } - - return memory_break; + Core::GetRunningCore().PrepareReschedule(); } /// Tell the CPU to continue executing. static void Continue() { - memory_break = false; - step_loop = false; - halt_loop = false; - Core::GetRunningCore().ClearInstructionCache(); + if (!current_process) { + return; + } + + u32 thread_id = -1; + if (continue_thread == 0) { + thread_id = current_process->GetThreadList()[0]->thread_id; + } else { + thread_id = continue_thread; + } + + // There is no documentation anywhere if continue should + // reset the continue thread value. Luma3DS implementation + // does this, and looks like IDA Pro expects it that way too. + continue_thread = -1; + + std::vector continue_list{}; + if (thread_id != -1) { + continue_list.push_back(thread_id); + } + + current_process->SetDebugBreak(false, continue_list); + is_running = true; + + ClearAllInstructionCache(); } /** @@ -924,20 +1264,26 @@ static void Continue() { static bool CommitBreakpoint(BreakpointType type, VAddr addr, u32 len) { BreakpointMap& p = GetBreakpointMap(type); + if (type == BreakpointType::Execute && len != 2 && len != 4) { + return false; + } + Breakpoint breakpoint; breakpoint.active = true; breakpoint.addr = addr; breakpoint.len = len; - Core::System::GetInstance().Memory().ReadBlock( - *Core::System::GetInstance().Kernel().GetCurrentProcess(), addr, breakpoint.inst.data(), - breakpoint.inst.size()); + Core::System::GetInstance().Memory().ReadBlock(*current_process, addr, breakpoint.inst.data(), + len); static constexpr std::array btrap{0x70, 0x00, 0x20, 0xe1}; + static constexpr std::array btrap_thumb{0x00, 0xBE}; + if (type == BreakpointType::Execute) { Core::System::GetInstance().Memory().WriteBlock( - *Core::System::GetInstance().Kernel().GetCurrentProcess(), addr, btrap.data(), - btrap.size()); - Core::GetRunningCore().ClearInstructionCache(); + *current_process, addr, (len == 2) ? btrap_thumb.data() : btrap.data(), len); + ClearAllInstructionCache(); + } else { + Core::System::GetInstance().Memory().RegisterWatchpoint(*current_process, addr, len); } p.insert({addr, breakpoint}); @@ -949,6 +1295,11 @@ static bool CommitBreakpoint(BreakpointType type, VAddr addr, u32 len) { /// Handle add breakpoint command from gdb client. static void AddBreakpoint() { + if (!current_process) { + SendReply("E01"); + return; + } + BreakpointType type; u8 type_id = HexCharToValue(command_buffer[1]); @@ -971,12 +1322,12 @@ static void AddBreakpoint() { } auto start_offset = command_buffer + 3; - auto addr_pos = std::find(start_offset, command_buffer + command_length, ','); + auto addr_pos = std::find(start_offset, command_buffer + recv_command_length, ','); VAddr addr = HexToInt(start_offset, static_cast(addr_pos - start_offset)); start_offset = addr_pos + 1; - u32 len = - HexToInt(start_offset, static_cast((command_buffer + command_length) - start_offset)); + u32 len = HexToInt(start_offset, + static_cast((command_buffer + recv_command_length) - start_offset)); if (type == BreakpointType::Access) { // Access is made up of Read and Write types, so add both breakpoints @@ -998,6 +1349,11 @@ static void AddBreakpoint() { /// Handle remove breakpoint command from gdb client. static void RemoveBreakpoint() { + if (!current_process) { + SendReply("E01"); + return; + } + BreakpointType type; u8 type_id = HexCharToValue(command_buffer[1]); @@ -1020,7 +1376,7 @@ static void RemoveBreakpoint() { } auto start_offset = command_buffer + 3; - auto addr_pos = std::find(start_offset, command_buffer + command_length, ','); + auto addr_pos = std::find(start_offset, command_buffer + recv_command_length, ','); VAddr addr = HexToInt(start_offset, static_cast(addr_pos - start_offset)); if (type == BreakpointType::Access) { @@ -1035,11 +1391,146 @@ static void RemoveBreakpoint() { SendReply("OK"); } +void HandleVCommand() { + std::string_view cmd_view((const char*)command_buffer, recv_command_length); + + if (cmd_view.size() <= 1) { + SendReply("E01"); + return; + } + size_t delimiter_pos = cmd_view.find(';'); + std::string_view command = + cmd_view.substr(1, delimiter_pos == cmd_view.npos ? cmd_view.npos : delimiter_pos); + if (command == "Attach;") { + if (!is_extended_mode) { + SendReply("E01"); + return; + } + + std::string_view arg = cmd_view.substr(delimiter_pos + 1); + u32 pid = HexToInt(reinterpret_cast(arg.data()), arg.size()); + auto process = Core::System::GetInstance().Kernel().GetProcessById(pid); + if (!process) { + SendReply("E02"); + } else { + current_process = process.get(); + current_process->SetDebugBreak(true); + is_running = false; + if (SetThread(0)) { + SendStopReply(current_thread, 0); + } else { + // Should never happen + SendReply("W00"); + } + } + return; + } else if (command == "Cont?") { + SendReply("vCont;c;C"); + } else if (command == "Cont;") { + if (!current_process) { + SendReply("E01"); + return; + } + + std::string_view arg = cmd_view.substr(delimiter_pos + 1); + auto actions = Common::SplitString(arg, ';'); + + if (actions.empty()) { + SendReply("E01"); + return; + } + for (auto& action : actions) { + auto threads = Common::SplitString(action, ':'); + if (threads.empty()) { + SendReply("E01"); + return; + } + char action_type = threads[0][0]; + if (action_type != 'c' && action_type != 'C') { + SendReply("E01"); + return; + } + std::vector thread_ids; + for (size_t i = 1; i < threads.size(); i++) { + thread_ids.push_back( + HexToInt(reinterpret_cast(threads[i].c_str()), threads[i].size())); + } + + current_process->SetDebugBreak(false, thread_ids); + is_running = true; + } + } else { + SendReply(""); + } +} + +void OnProcessExit(u32 process_id) { + if (!GDBStub::IsConnected || !current_process || current_process->process_id != process_id) { + return; + } + + current_process_finished = true; + if (is_running) { + SendStopReply(nullptr, 0); + } + current_process = nullptr; + current_thread = nullptr; +} + +void OnThreadExit(u32 thread_id) { + if (!GDBStub::IsConnected || !current_thread || current_thread->thread_id != thread_id) { + return; + } + + current_thread = nullptr; +} + void HandlePacket(Core::System& system) { + if (!IsConnected()) { if (defer_start) { ToggleServer(true); + defer_start = false; } + + // Handle accept new GDB connection + if (accept_socket != INVALID_SOCKET) { + sockaddr_in saddr_client; + sockaddr* client_addr = reinterpret_cast(&saddr_client); + socklen_t client_addrlen = sizeof(saddr_client); + gdbserver_socket = + static_cast(accept(accept_socket, client_addr, &client_addrlen)); + if (gdbserver_socket == INVALID_SOCKET) { +#ifdef _WIN32 + if (GetErrno() == WSAEWOULDBLOCK) { + // Nothing connected yet + return; + } +#else + if (GetErrno() == EAGAIN || GetErrno() == EWOULDBLOCK) { + // Nothing connected yet + return; + } +#endif + LOG_ERROR(Debug_GDBStub, "Failed to accept gdb client"); + } else { + LOG_INFO(Debug_GDBStub, "Client connected.\n"); + SetNonBlock(gdbserver_socket, false); + } + + shutdown(accept_socket, SHUT_RDWR); + closesocket(accept_socket); + accept_socket = INVALID_SOCKET; + } + return; + } + + if (break_thread) { + current_thread = break_thread; + break_thread = nullptr; + int signal = break_signal; + break_signal = 0; + BreakImpl(signal); return; } @@ -1053,10 +1544,15 @@ void HandlePacket(Core::System& system) { } ReadCommand(); - if (command_length == 0) { + if (recv_command_length == 0) { return; } +#ifdef PRINT_GDB_TRAFFIC + std::string cmd_str(command_buffer + 1, command_buffer + recv_command_length); + LOG_INFO(Debug_GDBStub, "Req: {:c} {}", command_buffer[0], cmd_str); +#endif + LOG_DEBUG(Debug_GDBStub, "Packet: {0:d} ('{0:c}')", command_buffer[0]); switch (command_buffer[0]) { @@ -1067,16 +1563,29 @@ void HandlePacket(Core::System& system) { HandleSetThread(); break; case '?': - SendSignal(current_thread, latest_signal); + HandleGetStopReason(); + break; + case '!': + HandleExtendedMode(); + break; + case 'D': + SendReply("OK"); + ToggleServer(false); + ToggleServer(true); + // Continue execution + continue_thread = -1; + Continue(); break; case 'k': LOG_INFO(Debug_GDBStub, "killed by gdb"); ToggleServer(false); - // Continue execution so we don't hang forever after shutting down the server + // Continue execution and stop emulation + continue_thread = -1; Continue(); + system.RequestShutdown(); return; case 'F': - HandleHioReply(system, command_buffer, command_length); + HandleHioReply(system, current_process, command_buffer, recv_command_length); break; case 'g': ReadRegisters(); @@ -1097,7 +1606,8 @@ void HandlePacket(Core::System& system) { WriteMemory(); break; case 's': - Step(); + // Single step not supported, return ENOTSUP + SendReply("E5F"); return; case 'C': case 'c': @@ -1112,6 +1622,9 @@ void HandlePacket(Core::System& system) { case 'T': HandleThreadAlive(); break; + case 'v': + HandleVCommand(); + break; default: SendReply(""); break; @@ -1127,14 +1640,12 @@ void ToggleServer(bool status) { server_enabled = status; // Start server - if (!IsConnected() && Core::System::GetInstance().IsPoweredOn()) { + if (!IsInitialized() && Core::System::GetInstance().IsPoweredOn()) { Init(); } } else { // Stop server - if (IsConnected()) { - Shutdown(); - } + Shutdown(); server_enabled = status; } @@ -1146,21 +1657,9 @@ void DeferStart() { static void Init(u16 port) { if (!server_enabled) { - // Set the halt loop to false in case the user enabled the gdbstub mid-execution. - // This way the CPU can still execute normally. - halt_loop = false; - step_loop = false; return; } - // Setup initial gdbstub status - halt_loop = true; - step_loop = false; - - breakpoints_execute.clear(); - breakpoints_read.clear(); - breakpoints_write.clear(); - // Start gdb server LOG_INFO(Debug_GDBStub, "Starting GDB server on port {}...", port); @@ -1173,50 +1672,36 @@ static void Init(u16 port) { WSAStartup(MAKEWORD(2, 2), &InitData); #endif - int tmpsock = static_cast(socket(PF_INET, SOCK_STREAM, 0)); - if (tmpsock == -1) { + accept_socket = static_cast(socket(PF_INET, SOCK_STREAM, 0)); + if (accept_socket == INVALID_SOCKET) { LOG_ERROR(Debug_GDBStub, "Failed to create gdb socket"); + return; } // Set socket to SO_REUSEADDR so it can always bind on the same port int reuse_enabled = 1; - if (setsockopt(tmpsock, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse_enabled, + if (setsockopt(accept_socket, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse_enabled, sizeof(reuse_enabled)) < 0) { LOG_ERROR(Debug_GDBStub, "Failed to set gdb socket option"); + Shutdown(); + return; } const sockaddr* server_addr = reinterpret_cast(&saddr_server); socklen_t server_addrlen = sizeof(saddr_server); - if (bind(tmpsock, server_addr, server_addrlen) < 0) { + if (bind(accept_socket, server_addr, server_addrlen) < 0) { LOG_ERROR(Debug_GDBStub, "Failed to bind gdb socket"); + Shutdown(); + return; } - if (listen(tmpsock, 1) < 0) { + if (listen(accept_socket, 1) < 0) { LOG_ERROR(Debug_GDBStub, "Failed to listen to gdb socket"); + Shutdown(); + return; } - // Wait for gdb to connect - LOG_INFO(Debug_GDBStub, "Waiting for gdb to connect...\n"); - sockaddr_in saddr_client; - sockaddr* client_addr = reinterpret_cast(&saddr_client); - socklen_t client_addrlen = sizeof(saddr_client); - gdbserver_socket = static_cast(accept(tmpsock, client_addr, &client_addrlen)); - if (gdbserver_socket < 0) { - // In the case that we couldn't start the server for whatever reason, just start CPU - // execution like normal. - halt_loop = false; - step_loop = false; - - LOG_ERROR(Debug_GDBStub, "Failed to accept gdb client"); - } else { - LOG_INFO(Debug_GDBStub, "Client connected.\n"); - saddr_client.sin_addr.s_addr = ntohl(saddr_client.sin_addr.s_addr); - } - - // Clean up temporary socket if it's still alive at this point. - if (tmpsock != -1) { - shutdown(tmpsock, SHUT_RDWR); - } + SetNonBlock(accept_socket, true); } void Init() { @@ -1227,18 +1712,28 @@ void Shutdown() { if (!server_enabled) { return; } - defer_start = false; + + RemoveAllBreakpoints(); LOG_INFO(Debug_GDBStub, "Stopping GDB ..."); - if (gdbserver_socket != -1) { + if (gdbserver_socket != INVALID_SOCKET) { shutdown(gdbserver_socket, SHUT_RDWR); - gdbserver_socket = -1; + closesocket(gdbserver_socket); + gdbserver_socket = INVALID_SOCKET; + } + + if (accept_socket != INVALID_SOCKET) { + shutdown(accept_socket, SHUT_RDWR); + closesocket(accept_socket); + accept_socket = INVALID_SOCKET; } #ifdef _WIN32 WSACleanup(); #endif + ResetState(); + LOG_INFO(Debug_GDBStub, "GDB stopped."); } @@ -1246,36 +1741,12 @@ bool IsServerEnabled() { return server_enabled; } +bool IsInitialized() { + return IsServerEnabled() && + (accept_socket != INVALID_SOCKET || gdbserver_socket != INVALID_SOCKET); +} + bool IsConnected() { - return IsServerEnabled() && gdbserver_socket != -1; -} - -bool GetCpuHaltFlag() { - return halt_loop; -} - -void SetCpuHaltFlag(bool halt) { - halt_loop = halt; -} - -bool GetCpuStepFlag() { - return step_loop; -} - -void SetCpuStepFlag(bool is_step) { - step_loop = is_step; -} - -void SendTrap(Kernel::Thread* thread, int trap) { - if (!send_trap) { - return; - } - - current_thread = thread; - - SendSignal(thread, trap); - - halt_loop = true; - send_trap = false; + return IsServerEnabled() && gdbserver_socket != INVALID_SOCKET; } }; // namespace GDBStub diff --git a/src/core/gdbstub/gdbstub.h b/src/core/gdbstub/gdbstub.h index fc0c7df23..0fd3c7c00 100644 --- a/src/core/gdbstub/gdbstub.h +++ b/src/core/gdbstub/gdbstub.h @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + // Copyright 2013 Dolphin Emulator Project // 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. diff --git a/src/core/gdbstub/hio.cpp b/src/core/gdbstub/hio.cpp index 00eab9061..8a8a7adc6 100644 --- a/src/core/gdbstub/hio.cpp +++ b/src/core/gdbstub/hio.cpp @@ -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 #include #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 request_status{Status::NoRequest}; -static std::atomic was_halted = false; -static std::atomic 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, ¤t_hio_request, sizeof(PackedGdbHioRequest)); + memory.ReadBlock(*process, addr, ¤t_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, ¤t_hio_request, sizeof(PackedGdbHioRequest)); + memory.WriteBlock(*process, current_hio_request_addr, ¤t_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(); } diff --git a/src/core/gdbstub/hio.h b/src/core/gdbstub/hio.h index 827521841..066bef55a 100644 --- a/src/core/gdbstub/hio.h +++ b/src/core/gdbstub/hio.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -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 diff --git a/src/core/hle/kernel/hle_ipc.h b/src/core/hle/kernel/hle_ipc.h index 1b3b8c93e..bdd7877fc 100644 --- a/src/core/hle/kernel/hle_ipc.h +++ b/src/core/hle/kernel/hle_ipc.h @@ -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 + 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>([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>( + 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>( + kernel, result_function, std::move(std::future())); + 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. diff --git a/src/core/hle/kernel/process.cpp b/src/core/hle/kernel/process.cpp index 5e0d3810f..b895a1f13 100644 --- a/src/core/hle/kernel/process.cpp +++ b/src/core/hle/kernel/process.cpp @@ -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> Kernel::Process::GetThreadList() { + std::vector> 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 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; diff --git a/src/core/hle/kernel/process.h b/src/core/hle/kernel/process.h index 7ec1adfbb..4067839cd 100644 --- a/src/core/hle/kernel/process.h +++ b/src/core/hle/kernel/process.h @@ -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> GetThreadList(); + + void SetDebugBreak(bool debug_break, std::vector thread_ids = {}); + private: void FreeAllMemory(); diff --git a/src/core/hle/kernel/svc.cpp b/src/core/hle/kernel/svc.cpp index 833543d9a..715aedd6a 100644 --- a/src/core/hle/kernel/svc.cpp +++ b/src/core/hle/kernel/svc.cpp @@ -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; } diff --git a/src/core/hle/kernel/thread.cpp b/src/core/hle/kernel/thread.cpp index 41a706fdd..5f982c9e6 100644 --- a/src/core/hle/kernel/thread.cpp +++ b/src/core/hle/kernel/thread.cpp @@ -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) {} diff --git a/src/core/hle/kernel/thread.h b/src/core/hle/kernel/thread.h index be515319b..49891b31a 100644 --- a/src/core/hle/kernel/thread.h +++ b/src/core/hle/kernel/thread.h @@ -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 diff --git a/src/core/hle/service/dsp/dsp_dsp.cpp b/src/core/hle/service/dsp/dsp_dsp.cpp index 9b4a9c27b..ae936ba94 100644 --- a/src/core/hle/service/dsp/dsp_dsp.cpp +++ b/src/core/hle/service/dsp/dsp_dsp.cpp @@ -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) { diff --git a/src/core/hle/service/fs/directory.cpp b/src/core/hle/service/fs/directory.cpp index 0a862f097..13f011b5c 100644 --- a/src/core/hle/service/fs/directory.cpp +++ b/src/core/hle/service/fs/directory.cpp @@ -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(); - auto& buffer = rp.PopMappedBuffer(); - std::vector entries(count); - LOG_TRACE(Service_FS, "Read {}: count={}", GetName(), count); - // Number of entries actually read - u32 read = backend->Read(static_cast(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(); + async_data->count = rp.Pop(); + async_data->buffer = &rp.PopMappedBuffer(); + + ctx.RunAsync( + [this, async_data](Kernel::HLERequestContext& ctx) { + std::vector entries(async_data->count); + LOG_TRACE(Service_FS, "Read {}: count={}", GetName(), count); + // Number of entries actually read + async_data->read = backend->Read(static_cast(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 diff --git a/src/core/hle/service/fs/file.cpp b/src/core/hle/service/fs/file.cpp index 7ba13f935..6ba8f2b77 100644 --- a/src/core/hle/service/fs/file.cpp +++ b/src/core/hle/service/fs/file.cpp @@ -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 data = std::make_unique_for_overwrite(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 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); diff --git a/src/core/hle/service/fs/fs_user.cpp b/src/core/hle/service/fs/fs_user.cpp index c9641a518..fdbdfbc58 100644 --- a/src/core/hle/service/fs/fs_user.cpp +++ b/src/core/hle/service/fs/fs_user.cpp @@ -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 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> 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 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(); const auto output_size = rp.Pop(); - 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 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 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(); - 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(); 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(); const u8 title_variation = rp.Pop(); - 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(); const u8 title_variation = rp.Pop(); - 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(); const u64 value = rp.Pop(); - 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(); - 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(); 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(); const bool flush = rp.Pop(); - 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(); const u32 secure_value_slot = rp.Pop(); - 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 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(*this); ar & priority; ar & secure_value_backend; diff --git a/src/core/hle/service/fs/fs_user.h b/src/core/hle/service/fs/fs_user.h index f94f318f2..9dd021bdc 100644 --- a/src/core/hle/service/fs/fs_user.h +++ b/src/core/hle/service/fs/fs_user.h @@ -49,6 +49,9 @@ private: class FS_USER final : public ServiceFramework { 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 secure_value_backend; + Common::ThreadWorker fs_async_worker{1, "FSUSER_Worker"}; + template void serialize(Archive& ar, const unsigned int); friend class boost::serialization::access; diff --git a/src/core/hle/service/gsp/gsp_gpu.cpp b/src/core/hle/service/gsp/gsp_gpu.cpp index 39d10f2d3..853dd8d3c 100644 --- a/src/core/hle/service/gsp/gsp_gpu.cpp +++ b/src/core/hle/service/gsp/gsp_gpu.cpp @@ -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() != 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(); @@ -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(total_ns) / static_cast(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( + ((static_cast(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::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(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(arg)); + }); + first_initialization = true; }; diff --git a/src/core/hle/service/gsp/gsp_gpu.h b/src/core/hle/service/gsp/gsp_gpu.h index 0864e37de..3afbb0916 100644 --- a/src/core/hle/service/gsp/gsp_gpu.h +++ b/src/core/hle/service/gsp/gsp_gpu.h @@ -4,8 +4,10 @@ #pragma once +#include #include #include +#include #include #include #include @@ -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 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 elem) { + if (elements[head].first != InterruptId::COUNT) { + // If the head position is occupied, the queue is full + return std::numeric_limits::max(); + } + + elements[head] = elem; + size_t index = head; + head = (head + 1) % array_size; + return index; + } + + std::optional> Pop(size_t at) { + if (at >= array_size || elements[at].first == InterruptId::COUNT) { + // Invalid index or already free + return std::nullopt; + } + + std::pair value = elements[at]; + elements[at].first = InterruptId::COUNT; + + return value; + } + + private: + static constexpr size_t array_size = 512; + size_t head = 0; + + std::array, array_size> elements; + + template + 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 + void serialize(Archive& ar, const unsigned int) { + ar & delta_time; + ar & sum_time; + } + friend class boost::serialization::access; + }; + + using PerfArray = std::array(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(id)].delta_time = static_cast(nanoseconds); + entries[static_cast(id)].sum_time += static_cast(nanoseconds); + } + + const PerfArray& GetResults() { + return entries; + } + + private: + PerfArray entries{}; + bool enabled{}; + + template + 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 diff --git a/src/core/hle/service/gsp/gsp_interrupt.h b/src/core/hle/service/gsp/gsp_interrupt.h index db2b584a6..efd46b39e 100644 --- a/src/core/hle/service/gsp/gsp_interrupt.h +++ b/src/core/hle/service/gsp/gsp_interrupt.h @@ -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 +#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; +using InterruptHandler = std::function; } // namespace Service::GSP diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h index 9e4c76122..678b48bf7 100644 --- a/src/core/loader/loader.h +++ b/src/core/loader/loader.h @@ -76,6 +76,8 @@ enum class ResultStatus { ErrorGbaTitle, ErrorArtic, ErrorNotFound, + ErrorPatches, + ErrorPatchesInvalidTitle, }; constexpr u32 MakeMagic(char a, char b, char c, char d) { diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 41ad049e5..ee446eb6d 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -3,6 +3,7 @@ // Refer to the license.txt file included. #include +#include #include #include #include @@ -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 SERIALIZE_EXPORT_IMPL(Memory::MemorySystem::BackingMemImpl) SERIALIZE_EXPORT_IMPL(Memory::MemorySystem::BackingMemImpl) +#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(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(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(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 page_table) { } } +template +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 T MemorySystem::Read(const std::shared_ptr& page_table, const VAddr vaddr) { + constexpr bool is_optional = is_optional_type; + using ReadType = optional_inner_or_type; + + 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& 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(paddr) - Memory::IO_AREA_PADDR + - 0x1EC00000); + return static_cast(impl->system.GPU().ReadReg( + static_cast(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(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 @@ -527,17 +706,43 @@ void MemorySystem::Write(const std::shared_ptr& 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(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(data), vaddr, impl->GetPC()); + (void)UnmappedAccess(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(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(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(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(process.vm_manager.page_table, addr); } +std::optional MemorySystem::Read32OrNullopt(VAddr addr) { + return Read>(impl->current_page_table, addr); +} + +std::optional MemorySystem::Read32OrNullopt(const Kernel::Process& process, VAddr addr) { + return Read>(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(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(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(copy_amount), FlushMode::Flush); WriteBlock(dest_process, dest_addr, GetPointerForRasterizerCache(current_vaddr), diff --git a/src/core/memory.h b/src/core/memory.h index 5c215b3f2..d4f09fa9e 100644 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -5,11 +5,13 @@ #pragma once #include #include +#include #include #include #include #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(idx)); } + const MemoryRef& Ref(std::size_t idx) { + return refs[idx]; + } + private: std::array raw; std::array refs; @@ -100,6 +112,22 @@ struct PageTable { return pointers.raw; } + struct WatchpointPageInfo { + u32 watchpoint_count{}; + MemoryRef memory; + + template + 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 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 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 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 + void UnmappedAccess(const VAddr vaddr, const T value, bool read); + template T Read(const std::shared_ptr& page_table, const VAddr vaddr); diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt index 70d36cbfd..2226f7931 100644 --- a/src/input_common/CMakeLists.txt +++ b/src/input_common/CMakeLists.txt @@ -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 diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt index 3feb6f479..365bd6877 100644 --- a/src/network/CMakeLists.txt +++ b/src/network/CMakeLists.txt @@ -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 diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 027609c26..cf99e004d 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -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() diff --git a/src/tests/common/file_util.cpp b/src/tests/common/file_util.cpp index bd7fcbdd9..72734e0a9 100644 --- a/src/tests/common/file_util.cpp +++ b/src/tests/common/file_util.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -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 \ No newline at end of file diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index a8ade344d..528072c9a 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -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 diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp index 9a76f4859..735144f42 100644 --- a/src/video_core/gpu.cpp +++ b/src/video_core/gpu.cpp @@ -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, 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(mode)]); + + return static_cast(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); diff --git a/src/video_core/pica/pica_core.cpp b/src/video_core/pica/pica_core.cpp index 9f19e9572..d264c35b5 100644 --- a/src/video_core/pica/pica_core.cpp +++ b/src/video_core/pica/pica_core.cpp @@ -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 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; diff --git a/src/video_core/pica/pica_core.h b/src/video_core/pica/pica_core.h index 61d8e75c2..426a3ca8e 100644 --- a/src/video_core/pica/pica_core.h +++ b/src/video_core/pica/pica_core.h @@ -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(result); + } + +private: + size_t triangle_count{}; + size_t command_count{}; + + friend class boost::serialization::access; + template + 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 debug_context_); @@ -277,6 +357,8 @@ public: AttributeBuffer input_default_attributes{}; ImmediateModeState immediate{}; + DelayGenerator delay_generator{}; + private: friend class boost::serialization::access; template @@ -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; diff --git a/src/video_core/pica/regs_external.h b/src/video_core/pica/regs_external.h index a629a11a7..4961ee23f 100644 --- a/src/video_core/pica/regs_external.h +++ b/src/video_core/pica/regs_external.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -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; diff --git a/src/web_service/CMakeLists.txt b/src/web_service/CMakeLists.txt index 7dabb7b45..d21c0eace 100644 --- a/src/web_service/CMakeLists.txt +++ b/src/web_service/CMakeLists.txt @@ -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