diff options
129 files changed, 2755 insertions, 1671 deletions
diff --git a/.ci/scripts/linux/upload.sh b/.ci/scripts/linux/upload.sh index 8173c5728..e0f336427 100755 --- a/.ci/scripts/linux/upload.sh +++ b/.ci/scripts/linux/upload.sh @@ -5,21 +5,24 @@ . .ci/scripts/common/pre-upload.sh -APPIMAGE_NAME="yuzu-${GITDATE}-${GITREV}.AppImage" -REV_NAME="yuzu-linux-${GITDATE}-${GITREV}" +APPIMAGE_NAME="yuzu-${RELEASE_NAME}-${GITDATE}-${GITREV}.AppImage" +BASE_NAME="yuzu-linux" +REV_NAME="${BASE_NAME}-${GITDATE}-${GITREV}" ARCHIVE_NAME="${REV_NAME}.tar.xz" COMPRESSION_FLAGS="-cJvf" -if [ "${RELEASE_NAME}" = "mainline" ]; then - DIR_NAME="${REV_NAME}" +if [ "${RELEASE_NAME}" = "mainline" ] || [ "${RELEASE_NAME}" = "early-access" ]; then + DIR_NAME="${BASE_NAME}-${RELEASE_NAME}" else - DIR_NAME="${REV_NAME}_${RELEASE_NAME}" + DIR_NAME="${REV_NAME}-${RELEASE_NAME}" fi mkdir "$DIR_NAME" cp build/bin/yuzu-cmd "$DIR_NAME" -cp build/bin/yuzu "$DIR_NAME" +if [ "${RELEASE_NAME}" != "early-access" ] && [ "${RELEASE_NAME}" != "mainline" ]; then + cp build/bin/yuzu "$DIR_NAME" +fi # Build an AppImage cd build @@ -32,6 +35,11 @@ if ! ./appimagetool-x86_64.AppImage --version; then export APPIMAGE_EXTRACT_AND_RUN=1 fi +# Don't let AppImageLauncher ask to integrate EA +if [ "${RELEASE_NAME}" = "mainline" ] || [ "${RELEASE_NAME}" = "early-access" ]; then + echo "X-AppImage-Integrate=false" >> AppDir/org.yuzu_emu.yuzu.desktop +fi + if [ "${RELEASE_NAME}" = "mainline" ]; then # Generate update information if releasing to mainline ./appimagetool-x86_64.AppImage -u "gh-releases-zsync|yuzu-emu|yuzu-${RELEASE_NAME}|latest|yuzu-*.AppImage.zsync" AppDir "${APPIMAGE_NAME}" @@ -46,4 +54,9 @@ if [ -f "build/${APPIMAGE_NAME}.zsync" ]; then cp "build/${APPIMAGE_NAME}.zsync" "${ARTIFACTS_DIR}/" fi +# Copy the AppImage to the general release directory and remove git revision info +if [ "${RELEASE_NAME}" = "mainline" ] || [ "${RELEASE_NAME}" = "early-access" ]; then + cp "build/${APPIMAGE_NAME}" "${DIR_NAME}/yuzu-${RELEASE_NAME}.AppImage" +fi + . .ci/scripts/common/post-upload.sh diff --git a/.ci/scripts/windows/docker.sh b/.ci/scripts/windows/docker.sh index 790ba8218..6f522feed 100755 --- a/.ci/scripts/windows/docker.sh +++ b/.ci/scripts/windows/docker.sh @@ -21,6 +21,7 @@ cmake .. \ -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON \ -DENABLE_QT_TRANSLATION=ON \ -DUSE_CCACHE=ON \ + -DYUZU_CRASH_DUMPS=ON \ -DYUZU_USE_BUNDLED_SDL2=OFF \ -DYUZU_USE_EXTERNAL_SDL2=OFF \ -DYUZU_TESTS=OFF \ diff --git a/.ci/scripts/windows/upload.ps1 b/.ci/scripts/windows/upload.ps1 index d463281de..21abcd752 100644 --- a/.ci/scripts/windows/upload.ps1 +++ b/.ci/scripts/windows/upload.ps1 @@ -65,8 +65,8 @@ if ("$env:GITHUB_ACTIONS" -eq "true") { # None of the other GHA builds are including source, so commenting out today #Copy-Item $MSVC_SOURCE_TARXZ -Destination "artifacts" - # Are debug symbols important? - # cp .\build\bin\yuzu*.pdb .\pdb\ + # Debugging symbols + cp .\build\bin\yuzu*.pdb .\artifacts\ # Write out a tag BUILD_TAG to environment for the Upload step # We're getting ${{ github.event.number }} as $env:PR_NUMBER" diff --git a/.ci/templates/build-msvc.yml b/.ci/templates/build-msvc.yml index a2ee71bd8..ea405e5dc 100644 --- a/.ci/templates/build-msvc.yml +++ b/.ci/templates/build-msvc.yml @@ -9,7 +9,7 @@ parameters: steps: - script: choco install vulkan-sdk displayName: 'Install vulkan-sdk' -- script: refreshenv && mkdir build && cd build && cmake -G "Visual Studio 17 2022" -A x64 -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_QT_WEB_ENGINE=ON -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DYUZU_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} -DYUZU_TESTS=OFF -DUSE_DISCORD_PRESENCE=ON -DENABLE_QT_TRANSLATION=ON -DDISPLAY_VERSION=${{ parameters['version'] }} -DCMAKE_BUILD_TYPE=Release .. && cd .. +- script: refreshenv && mkdir build && cd build && cmake -G "Visual Studio 17 2022" -A x64 -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_QT_WEB_ENGINE=ON -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DYUZU_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} -DYUZU_TESTS=OFF -DUSE_DISCORD_PRESENCE=ON -DENABLE_QT_TRANSLATION=ON -DDISPLAY_VERSION=${{ parameters['version'] }} -DCMAKE_BUILD_TYPE=Release -DYUZU_CRASH_DUMPS=ON .. && cd .. displayName: 'Configure CMake' - task: MSBuild@1 displayName: 'Build' diff --git a/.ci/yuzu-patreon-step2.yml b/.ci/yuzu-patreon-step2.yml index 5d5b140fd..71a23ebe6 100644 --- a/.ci/yuzu-patreon-step2.yml +++ b/.ci/yuzu-patreon-step2.yml @@ -11,9 +11,30 @@ stages: - stage: build displayName: 'build' jobs: - - job: build + - job: linux timeoutInMinutes: 120 - displayName: 'windows-msvc' + displayName: 'linux' + pool: + vmImage: ubuntu-latest + strategy: + maxParallel: 10 + matrix: + linux: + BuildSuffix: 'linux' + ScriptFolder: 'linux' + steps: + - template: ./templates/sync-source.yml + parameters: + artifactSource: $(parameters.artifactSource) + needSubmodules: 'true' + - template: ./templates/build-single.yml + parameters: + artifactSource: 'false' + cache: $(parameters.cache) + version: $(DisplayVersion) + - job: msvc + timeoutInMinutes: 120 + displayName: 'windows' pool: vmImage: windows-2022 steps: diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 733a52764..7cde8380b 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -104,7 +104,7 @@ jobs: run: | glslangValidator --version mkdir build - cmake . -B build -GNinja -DCMAKE_TOOLCHAIN_FILE="CMakeModules/MSVCCache.cmake" -DUSE_CCACHE=ON -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_QT_WEB_ENGINE=ON -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DYUZU_ENABLE_COMPATIBILITY_REPORTING=ON -DUSE_DISCORD_PRESENCE=ON -DENABLE_QT_TRANSLATION=ON -DCMAKE_BUILD_TYPE=Release -DGIT_BRANCH=pr-verify + cmake . -B build -GNinja -DCMAKE_TOOLCHAIN_FILE="CMakeModules/MSVCCache.cmake" -DUSE_CCACHE=ON -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_QT_WEB_ENGINE=ON -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DYUZU_ENABLE_COMPATIBILITY_REPORTING=ON -DUSE_DISCORD_PRESENCE=ON -DENABLE_QT_TRANSLATION=ON -DCMAKE_BUILD_TYPE=Release -DGIT_BRANCH=pr-verify -DYUZU_CRASH_DUMPS=ON - name: Build run: cmake --build build - name: Cache Summary diff --git a/.gitignore b/.gitignore index cdf37962a..a5f7248c7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-2.0-or-later # Build directory -[Bb]uild/ +[Bb]uild*/ doc-build/ # Generated source files diff --git a/.reuse/dep5 b/.reuse/dep5 index fe4fa2f07..9a90f9eb6 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -6,6 +6,7 @@ Files: dist/english_plurals/* dist/icons/controller/*.png dist/icons/overlay/*.png dist/languages/* + dist/qt_themes/*/icons/48x48/sd_card.png dist/qt_themes/*/icons/index.theme dist/qt_themes/default/style.qss Copyright: yuzu Emulator Project @@ -51,6 +52,8 @@ Files: dist/qt_themes/colorful/icons/16x16/lock.png dist/qt_themes/colorful/icons/48x48/chip.png dist/qt_themes/colorful/icons/48x48/folder.png dist/qt_themes/colorful_dark/icons/16x16/lock.png + dist/qt_themes/colorful/icons/16x16/info.png + dist/qt_themes/colorful/icons/16x16/sync.png Copyright: Icons8 License: MIT Comment: https://github.com/icons8/flat-color-icons @@ -66,11 +69,9 @@ Files: dist/qt_themes/*/icons/48x48/no_avatar.png Copyright: Ionic (http://ionic.io/) License: MIT - -Files: dist/qt_themes/*/icons/48x48/sd_card.png - dist/qt_themes/colorful/icons/48x48/star.png - dist/qt_themes/default/icons/16x16/checked.png - dist/qt_themes/default/icons/16x16/failed.png +Files: dist/qt_themes/colorful/icons/48x48/star.png + dist/qt_themes/colorful/icons/16x16/checked.png + dist/qt_themes/colorful/icons/16x16/failed.png Copyright: SVG Repo License: CC0-1.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index 2ab0ea589..20dd1383f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,8 @@ option(YUZU_USE_BUNDLED_OPUS "Compile bundled opus" ON) option(YUZU_TESTS "Compile tests" ON) +CMAKE_DEPENDENT_OPTION(YUZU_CRASH_DUMPS "Compile Windows crash dump (Minidump) support" OFF "WIN32" OFF) + option(YUZU_USE_BUNDLED_VCPKG "Use vcpkg for yuzu dependencies" "${MSVC}") option(YUZU_CHECK_SUBMODULES "Check if submodules are present" ON) @@ -46,6 +48,9 @@ if (YUZU_USE_BUNDLED_VCPKG) if (YUZU_TESTS) list(APPEND VCPKG_MANIFEST_FEATURES "yuzu-tests") endif() + if (YUZU_CRASH_DUMPS) + list(APPEND VCPKG_MANIFEST_FEATURES "dbghelp") + endif() include(${CMAKE_SOURCE_DIR}/externals/vcpkg/scripts/buildsystems/vcpkg.cmake) elseif(NOT "$ENV{VCPKG_TOOLCHAIN_FILE}" STREQUAL "") @@ -447,6 +452,13 @@ elseif (WIN32) # PSAPI is the Process Status API set(PLATFORM_LIBRARIES ${PLATFORM_LIBRARIES} psapi imm32 version) endif() + + if (YUZU_CRASH_DUMPS) + find_library(DBGHELP_LIBRARY dbghelp) + if ("${DBGHELP_LIBRARY}" STREQUAL "DBGHELP_LIBRARY-NOTFOUND") + message(FATAL_ERROR "YUZU_CRASH_DUMPS enabled but dbghelp library not found") + endif() + endif() elseif (CMAKE_SYSTEM_NAME MATCHES "^(Linux|kFreeBSD|GNU|SunOS)$") set(PLATFORM_LIBRARIES rt) endif() diff --git a/dist/qt_themes/default/icons/16x16/checked.png b/dist/qt_themes/colorful/icons/16x16/checked.png Binary files differindex b9e64e9e0..b9e64e9e0 100644 --- a/dist/qt_themes/default/icons/16x16/checked.png +++ b/dist/qt_themes/colorful/icons/16x16/checked.png diff --git a/dist/qt_themes/default/icons/16x16/failed.png b/dist/qt_themes/colorful/icons/16x16/failed.png Binary files differindex a1872835d..a1872835d 100644 --- a/dist/qt_themes/default/icons/16x16/failed.png +++ b/dist/qt_themes/colorful/icons/16x16/failed.png diff --git a/dist/qt_themes/colorful/icons/16x16/info.png b/dist/qt_themes/colorful/icons/16x16/info.png Binary files differnew file mode 100644 index 000000000..8b9330f4c --- /dev/null +++ b/dist/qt_themes/colorful/icons/16x16/info.png diff --git a/dist/qt_themes/colorful/icons/16x16/sync.png b/dist/qt_themes/colorful/icons/16x16/sync.png Binary files differnew file mode 100644 index 000000000..0d57789c3 --- /dev/null +++ b/dist/qt_themes/colorful/icons/16x16/sync.png diff --git a/dist/qt_themes/default/icons/16x16/view-refresh.png b/dist/qt_themes/colorful/icons/16x16/view-refresh.png Binary files differindex 69f9474ac..69f9474ac 100644 --- a/dist/qt_themes/default/icons/16x16/view-refresh.png +++ b/dist/qt_themes/colorful/icons/16x16/view-refresh.png diff --git a/dist/qt_themes/default/icons/48x48/no_avatar.png b/dist/qt_themes/colorful/icons/48x48/no_avatar.png Binary files differindex 76f812349..76f812349 100644 --- a/dist/qt_themes/default/icons/48x48/no_avatar.png +++ b/dist/qt_themes/colorful/icons/48x48/no_avatar.png diff --git a/dist/qt_themes/colorful/icons/48x48/sd_card.png b/dist/qt_themes/colorful/icons/48x48/sd_card.png Binary files differindex 47e491d32..652d61bc3 100644 --- a/dist/qt_themes/colorful/icons/48x48/sd_card.png +++ b/dist/qt_themes/colorful/icons/48x48/sd_card.png diff --git a/dist/qt_themes/colorful/icons/index.theme b/dist/qt_themes/colorful/icons/index.theme index b452aca16..6eb3c6949 100644 --- a/dist/qt_themes/colorful/icons/index.theme +++ b/dist/qt_themes/colorful/icons/index.theme @@ -1,7 +1,6 @@ [Icon Theme] Name=colorful Comment=Colorful theme -Inherits=default Directories=16x16,48x48,256x256 [16x16] diff --git a/dist/qt_themes/colorful/style.qrc b/dist/qt_themes/colorful/style.qrc index 507e0e58b..82cd367be 100644 --- a/dist/qt_themes/colorful/style.qrc +++ b/dist/qt_themes/colorful/style.qrc @@ -6,14 +6,20 @@ SPDX-License-Identifier: GPL-2.0-or-later <RCC> <qresource prefix="icons/colorful"> <file alias="index.theme">icons/index.theme</file> + <file alias="16x16/checked.png">icons/16x16/checked.png</file> <file alias="16x16/connected.png">icons/16x16/connected.png</file> <file alias="16x16/connected_notification.png">icons/16x16/connected_notification.png</file> <file alias="16x16/disconnected.png">icons/16x16/disconnected.png</file> + <file alias="16x16/failed.png">icons/16x16/failed.png</file> + <file alias="16x16/info.png">icons/16x16/info.png</file> <file alias="16x16/lock.png">icons/16x16/lock.png</file> + <file alias="16x16/sync.png">icons/16x16/sync.png</file> + <file alias="16x16/view-refresh.png">icons/16x16/view-refresh.png</file> <file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file> <file alias="48x48/chip.png">icons/48x48/chip.png</file> <file alias="48x48/folder.png">icons/48x48/folder.png</file> <file alias="48x48/list-add.png">icons/48x48/list-add.png</file> + <file alias="48x48/no_avatar.png">icons/48x48/no_avatar.png</file> <file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file> <file alias="48x48/star.png">icons/48x48/star.png</file> <file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file> diff --git a/dist/qt_themes/colorful_dark/style.qrc b/dist/qt_themes/colorful_dark/style.qrc index 9853fd438..72451ef02 100644 --- a/dist/qt_themes/colorful_dark/style.qrc +++ b/dist/qt_themes/colorful_dark/style.qrc @@ -5,19 +5,9 @@ SPDX-License-Identifier: GPL-2.0-or-later <RCC> <qresource prefix="icons/colorful_dark"> - <file alias="16x16/connected.png">../colorful/icons/16x16/connected.png</file> - <file alias="16x16/connected_notification.png">../colorful/icons/16x16/connected_notification.png</file> - <file alias="16x16/disconnected.png">../colorful/icons/16x16/disconnected.png</file> <file alias="index.theme">icons/index.theme</file> <file alias="16x16/lock.png">icons/16x16/lock.png</file> <file alias="16x16/view-refresh.png">icons/16x16/view-refresh.png</file> - <file alias="48x48/bad_folder.png">../colorful/icons/48x48/bad_folder.png</file> - <file alias="48x48/chip.png">../colorful/icons/48x48/chip.png</file> - <file alias="48x48/folder.png">../colorful/icons/48x48/folder.png</file> - <file alias="48x48/no_avatar.png">../qdarkstyle/icons/48x48/no_avatar.png</file> - <file alias="48x48/list-add.png">../colorful/icons/48x48/list-add.png</file> - <file alias="48x48/sd_card.png">../colorful/icons/48x48/sd_card.png</file> - <file alias="256x256/plus_folder.png">../colorful/icons/256x256/plus_folder.png</file> </qresource> <qresource prefix="qss_icons"> diff --git a/dist/qt_themes/default/default.qrc b/dist/qt_themes/default/default.qrc index a07f2a9c1..2e01a3434 100644 --- a/dist/qt_themes/default/default.qrc +++ b/dist/qt_themes/default/default.qrc @@ -5,23 +5,20 @@ SPDX-License-Identifier: GPL-2.0-or-later <RCC> <qresource prefix="icons/default"> + <!-- "colorful" is now the default theme, add new icons there --> <file alias="index.theme">icons/index.theme</file> - <file alias="16x16/checked.png">icons/16x16/checked.png</file> - <file alias="16x16/failed.png">icons/16x16/failed.png</file> - <file alias="16x16/lock.png">icons/16x16/lock.png</file> <file alias="16x16/connected.png">icons/16x16/connected.png</file> - <file alias="16x16/disconnected.png">icons/16x16/disconnected.png</file> <file alias="16x16/connected_notification.png">icons/16x16/connected_notification.png</file> - <file alias="16x16/view-refresh.png">icons/16x16/view-refresh.png</file> + <file alias="16x16/disconnected.png">icons/16x16/disconnected.png</file> + <file alias="16x16/lock.png">icons/16x16/lock.png</file> <file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file> <file alias="48x48/chip.png">icons/48x48/chip.png</file> <file alias="48x48/folder.png">icons/48x48/folder.png</file> - <file alias="48x48/no_avatar.png">icons/48x48/no_avatar.png</file> <file alias="48x48/list-add.png">icons/48x48/list-add.png</file> <file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file> <file alias="48x48/star.png">icons/48x48/star.png</file> - <file alias="256x256/yuzu.png">icons/256x256/yuzu.png</file> <file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file> + <file alias="256x256/yuzu.png">icons/256x256/yuzu.png</file> </qresource> <qresource prefix="default"> <file>style.qss</file> diff --git a/dist/qt_themes/default/icons/48x48/sd_card.png b/dist/qt_themes/default/icons/48x48/sd_card.png Binary files differindex 60dfba269..6bcb7f6b1 100644 --- a/dist/qt_themes/default/icons/48x48/sd_card.png +++ b/dist/qt_themes/default/icons/48x48/sd_card.png diff --git a/dist/qt_themes/default/icons/index.theme b/dist/qt_themes/default/icons/index.theme index 1edbe6408..21b35e3e3 100644 --- a/dist/qt_themes/default/icons/index.theme +++ b/dist/qt_themes/default/icons/index.theme @@ -1,6 +1,7 @@ [Icon Theme] Name=default Comment=default theme +Inherits=colorful Directories=16x16,48x48,256x256 [16x16] @@ -10,4 +11,4 @@ Size=16 Size=48 [256x256] -Size=256
\ No newline at end of file +Size=256 diff --git a/dist/qt_themes/qdarkstyle/icons/48x48/sd_card.png b/dist/qt_themes/qdarkstyle/icons/48x48/sd_card.png Binary files differindex 87ae5186d..15e5e4024 100644 --- a/dist/qt_themes/qdarkstyle/icons/48x48/sd_card.png +++ b/dist/qt_themes/qdarkstyle/icons/48x48/sd_card.png diff --git a/dist/qt_themes/qdarkstyle/icons/index.theme b/dist/qt_themes/qdarkstyle/icons/index.theme index d1e12f3ef..502717617 100644 --- a/dist/qt_themes/qdarkstyle/icons/index.theme +++ b/dist/qt_themes/qdarkstyle/icons/index.theme @@ -1,7 +1,7 @@ [Icon Theme] Name=qdarkstyle Comment=dark theme -Inherits=default +Inherits=colorful Directories=16x16,48x48,256x256 [16x16] @@ -11,4 +11,4 @@ Size=16 Size=48 [256x256] -Size=256
\ No newline at end of file +Size=256 diff --git a/dist/qt_themes/qdarkstyle_midnight_blue/icons/index.theme b/dist/qt_themes/qdarkstyle_midnight_blue/icons/index.theme index 447a6c8be..20f9f6d63 100644 --- a/dist/qt_themes/qdarkstyle_midnight_blue/icons/index.theme +++ b/dist/qt_themes/qdarkstyle_midnight_blue/icons/index.theme @@ -1,7 +1,7 @@ [Icon Theme] Name=qdarkstyle_midnight_blue Comment=dark theme -Inherits=default +Inherits=colorful Directories=16x16,48x48,256x256 [16x16] diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt index 5fe1d5fa5..144f1bab2 100644 --- a/src/audio_core/CMakeLists.txt +++ b/src/audio_core/CMakeLists.txt @@ -194,6 +194,7 @@ add_library(audio_core STATIC sink/sink.h sink/sink_details.cpp sink/sink_details.h + sink/sink_stream.cpp sink/sink_stream.h ) diff --git a/src/audio_core/audio_core.cpp b/src/audio_core/audio_core.cpp index 78e615a10..c845330cd 100644 --- a/src/audio_core/audio_core.cpp +++ b/src/audio_core/audio_core.cpp @@ -47,22 +47,12 @@ AudioRenderer::ADSP::ADSP& AudioCore::GetADSP() { return *adsp; } -void AudioCore::PauseSinks(const bool pausing) const { - if (pausing) { - output_sink->PauseStreams(); - input_sink->PauseStreams(); - } else { - output_sink->UnpauseStreams(); - input_sink->UnpauseStreams(); - } +void AudioCore::SetNVDECActive(bool active) { + nvdec_active = active; } -u32 AudioCore::GetStreamQueue() const { - return estimated_queue.load(); -} - -void AudioCore::SetStreamQueue(u32 size) { - estimated_queue.store(size); +bool AudioCore::IsNVDECActive() const { + return nvdec_active; } } // namespace AudioCore diff --git a/src/audio_core/audio_core.h b/src/audio_core/audio_core.h index 0f7d61ee4..e33e00a3e 100644 --- a/src/audio_core/audio_core.h +++ b/src/audio_core/audio_core.h @@ -17,7 +17,7 @@ namespace AudioCore { class AudioManager; /** - * Main audio class, sotred inside the core, and holding the audio manager, all sinks, and the ADSP. + * Main audio class, stored inside the core, and holding the audio manager, all sinks, and the ADSP. */ class AudioCore { public: @@ -58,26 +58,16 @@ public: AudioRenderer::ADSP::ADSP& GetADSP(); /** - * Pause the sink. Called from the core. + * Toggle NVDEC state, used to avoid stall in playback. * - * @param pausing - Is this pause due to an actual pause, or shutdown? - * Unfortunately, shutdown also pauses streams, which can cause issues. + * @param active - Set true if nvdec is active, otherwise false. */ - void PauseSinks(bool pausing) const; + void SetNVDECActive(bool active); /** - * Get the size of the current stream queue. - * - * @return Current stream queue size. - */ - u32 GetStreamQueue() const; - - /** - * Get the size of the current stream queue. - * - * @param size - New stream size. + * Get NVDEC state. */ - void SetStreamQueue(u32 size); + bool IsNVDECActive() const; private: /** @@ -93,8 +83,8 @@ private: std::unique_ptr<Sink::Sink> input_sink; /// The ADSP in the sysmodule std::unique_ptr<AudioRenderer::ADSP::ADSP> adsp; - /// Current size of the stream queue - std::atomic<u32> estimated_queue{0}; + /// Is NVDec currently active? + bool nvdec_active{false}; }; } // namespace AudioCore diff --git a/src/audio_core/audio_event.h b/src/audio_core/audio_event.h index 82dd32dca..012d2ed70 100644 --- a/src/audio_core/audio_event.h +++ b/src/audio_core/audio_event.h @@ -14,7 +14,7 @@ namespace AudioCore { * Responsible for the input/output events, set by the stream backend when buffers are consumed, and * waited on by the audio manager. These callbacks signal the game's events to keep the audio buffer * recycling going. - * In a real Switch this is not a seprate class, and exists entirely within the audio manager. + * In a real Switch this is not a separate class, and exists entirely within the audio manager. * On the Switch it's implemented more simply through a MultiWaitEventHolder, where it can * wait on multiple events at once, and the events are not needed by the backend. */ @@ -81,7 +81,7 @@ public: void ClearEvents(); private: - /// Lock, used bythe audio manager + /// Lock, used by the audio manager std::mutex event_lock; /// Array of events, one per system type (see Type), last event is used to terminate std::array<std::atomic<bool>, 4> events_signalled; diff --git a/src/audio_core/audio_in_manager.cpp b/src/audio_core/audio_in_manager.cpp index 4aadb7fd6..f39fb4002 100644 --- a/src/audio_core/audio_in_manager.cpp +++ b/src/audio_core/audio_in_manager.cpp @@ -82,7 +82,7 @@ u32 Manager::GetDeviceNames(std::vector<AudioRenderer::AudioDevice::AudioDeviceN auto input_devices{Sink::GetDeviceListForSink(Settings::values.sink_id.GetValue(), true)}; if (input_devices.size() > 1) { - names.push_back(AudioRenderer::AudioDevice::AudioDeviceName("Uac")); + names.emplace_back("Uac"); return 1; } return 0; diff --git a/src/audio_core/audio_in_manager.h b/src/audio_core/audio_in_manager.h index 75b73a0b6..8a519df99 100644 --- a/src/audio_core/audio_in_manager.h +++ b/src/audio_core/audio_in_manager.h @@ -59,9 +59,10 @@ public: /** * Get a list of audio in device names. * - * @oaram names - Output container to write names to. - * @param max_count - Maximum numebr of deivce names to write. Unused + * @param names - Output container to write names to. + * @param max_count - Maximum number of device names to write. Unused * @param filter - Should the list be filtered? Unused. + * * @return Number of names written. */ u32 GetDeviceNames(std::vector<AudioRenderer::AudioDevice::AudioDeviceName>& names, diff --git a/src/audio_core/audio_manager.h b/src/audio_core/audio_manager.h index 70316e9cb..8cbd95e22 100644 --- a/src/audio_core/audio_manager.h +++ b/src/audio_core/audio_manager.h @@ -76,7 +76,7 @@ public: private: /** - * Main thread, waiting on a manager signal and calling the registered fucntion. + * Main thread, waiting on a manager signal and calling the registered function. */ void ThreadFunc(); diff --git a/src/audio_core/audio_out_manager.cpp b/src/audio_core/audio_out_manager.cpp index 71d67de64..1766efde1 100644 --- a/src/audio_core/audio_out_manager.cpp +++ b/src/audio_core/audio_out_manager.cpp @@ -74,7 +74,7 @@ void Manager::BufferReleaseAndRegister() { u32 Manager::GetAudioOutDeviceNames( std::vector<AudioRenderer::AudioDevice::AudioDeviceName>& names) const { - names.push_back({"DeviceOut"}); + names.emplace_back("DeviceOut"); return 1; } diff --git a/src/audio_core/audio_render_manager.cpp b/src/audio_core/audio_render_manager.cpp index 7a846835b..7aba2b423 100644 --- a/src/audio_core/audio_render_manager.cpp +++ b/src/audio_core/audio_render_manager.cpp @@ -25,8 +25,8 @@ SystemManager& Manager::GetSystemManager() { return *system_manager; } -auto Manager::GetWorkBufferSize(const AudioRendererParameterInternal& params, u64& out_count) - -> Result { +Result Manager::GetWorkBufferSize(const AudioRendererParameterInternal& params, + u64& out_count) const { if (!CheckValidRevision(params.revision)) { return Service::Audio::ERR_INVALID_REVISION; } @@ -54,7 +54,7 @@ void Manager::ReleaseSessionId(const s32 session_id) { session_ids[--session_count] = session_id; } -u32 Manager::GetSessionCount() { +u32 Manager::GetSessionCount() const { std::scoped_lock l{session_lock}; return session_count; } diff --git a/src/audio_core/audio_render_manager.h b/src/audio_core/audio_render_manager.h index 6a508ec56..bf4837190 100644 --- a/src/audio_core/audio_render_manager.h +++ b/src/audio_core/audio_render_manager.h @@ -46,7 +46,7 @@ public: * @param out_count - Output size of the required workbuffer. * @return Result code. */ - Result GetWorkBufferSize(const AudioRendererParameterInternal& params, u64& out_count); + Result GetWorkBufferSize(const AudioRendererParameterInternal& params, u64& out_count) const; /** * Get a new session id. @@ -60,14 +60,14 @@ public: * * @return The number of active sessions. */ - u32 GetSessionCount(); + u32 GetSessionCount() const; /** * Add a renderer system to the manager. - * The system will be reguarly called to generate commands for the AudioRenderer. + * The system will be regularly called to generate commands for the AudioRenderer. * * @param system - The system to add. - * @return True if the system was sucessfully added, otherwise false. + * @return True if the system was successfully added, otherwise false. */ bool AddSystem(System& system); @@ -75,7 +75,7 @@ public: * Remove a renderer system from the manager. * * @param system - The system to remove. - * @return True if the system was sucessfully removed, otherwise false. + * @return True if the system was successfully removed, otherwise false. */ bool RemoveSystem(System& system); @@ -94,7 +94,7 @@ private: /// Number of active renderers u32 session_count{}; /// Lock for interacting with the sessions - std::mutex session_lock{}; + mutable std::mutex session_lock{}; /// Regularly generates commands from the registered systems for the AudioRenderer std::unique_ptr<SystemManager> system_manager{}; }; diff --git a/src/audio_core/device/audio_buffer.h b/src/audio_core/device/audio_buffer.h index cae7fa970..7128ef72a 100644 --- a/src/audio_core/device/audio_buffer.h +++ b/src/audio_core/device/audio_buffer.h @@ -8,6 +8,10 @@ namespace AudioCore { struct AudioBuffer { + /// Timestamp this buffer started playing. + u64 start_timestamp; + /// Timestamp this buffer should finish playing. + u64 end_timestamp; /// Timestamp this buffer completed playing. s64 played_timestamp; /// Game memory address for these samples. diff --git a/src/audio_core/device/audio_buffers.h b/src/audio_core/device/audio_buffers.h index 5d1979ea0..3dae1a3b7 100644 --- a/src/audio_core/device/audio_buffers.h +++ b/src/audio_core/device/audio_buffers.h @@ -36,7 +36,7 @@ public: * * @param buffer - The new buffer. */ - void AppendBuffer(AudioBuffer& buffer) { + void AppendBuffer(const AudioBuffer& buffer) { std::scoped_lock l{lock}; buffers[appended_index] = buffer; appended_count++; @@ -58,6 +58,7 @@ public: if (index < 0) { index += N; } + out_buffers.push_back(buffers[index]); registered_count++; registered_index = (registered_index + 1) % append_limit; @@ -87,10 +88,12 @@ public: /** * Release all registered buffers. * - * @param timestamp - The released timestamp for this buffer. + * @param core_timing - The CoreTiming instance + * @param session - The device session + * * @return Is the buffer was released. */ - bool ReleaseBuffers(Core::Timing::CoreTiming& core_timing, DeviceSession& session) { + bool ReleaseBuffers(const Core::Timing::CoreTiming& core_timing, const DeviceSession& session) { std::scoped_lock l{lock}; bool buffer_released{false}; while (registered_count > 0) { @@ -100,7 +103,7 @@ public: } // Check with the backend if this buffer can be released yet. - if (!session.IsBufferConsumed(buffers[index].tag)) { + if (!session.IsBufferConsumed(buffers[index])) { break; } @@ -280,6 +283,16 @@ public: return true; } + u64 GetNextTimestamp() const { + // Iterate backwards through the buffer queue, and take the most recent buffer's end + std::scoped_lock l{lock}; + auto index{appended_index - 1}; + if (index < 0) { + index += append_limit; + } + return buffers[index].end_timestamp; + } + private: /// Buffer lock mutable std::recursive_mutex lock{}; diff --git a/src/audio_core/device/device_session.cpp b/src/audio_core/device/device_session.cpp index 095fc96ce..995060414 100644 --- a/src/audio_core/device/device_session.cpp +++ b/src/audio_core/device/device_session.cpp @@ -7,11 +7,20 @@ #include "audio_core/device/device_session.h" #include "audio_core/sink/sink_stream.h" #include "core/core.h" +#include "core/core_timing.h" #include "core/memory.h" namespace AudioCore { -DeviceSession::DeviceSession(Core::System& system_) : system{system_} {} +using namespace std::literals; +constexpr auto INCREMENT_TIME{5ms}; + +DeviceSession::DeviceSession(Core::System& system_) + : system{system_}, thread_event{Core::Timing::CreateEvent( + "AudioOutSampleTick", + [this](std::uintptr_t, s64 time, std::chrono::nanoseconds) { + return ThreadFunc(); + })} {} DeviceSession::~DeviceSession() { Finalize(); @@ -50,25 +59,26 @@ void DeviceSession::Finalize() { } void DeviceSession::Start() { - stream->SetPlayedSampleCount(played_sample_count); - stream->Start(); + if (stream) { + stream->Start(); + system.CoreTiming().ScheduleLoopingEvent(std::chrono::nanoseconds::zero(), INCREMENT_TIME, + thread_event); + } } void DeviceSession::Stop() { if (stream) { - played_sample_count = stream->GetPlayedSampleCount(); stream->Stop(); + system.CoreTiming().UnscheduleEvent(thread_event, {}); } } -void DeviceSession::AppendBuffers(std::span<AudioBuffer> buffers) const { - auto& memory{system.Memory()}; - - for (size_t i = 0; i < buffers.size(); i++) { +void DeviceSession::AppendBuffers(std::span<const AudioBuffer> buffers) const { + for (const auto& buffer : buffers) { Sink::SinkBuffer new_buffer{ - .frames = buffers[i].size / (channel_count * sizeof(s16)), + .frames = buffer.size / (channel_count * sizeof(s16)), .frames_played = 0, - .tag = buffers[i].tag, + .tag = buffer.tag, .consumed = false, }; @@ -76,26 +86,22 @@ void DeviceSession::AppendBuffers(std::span<AudioBuffer> buffers) const { std::vector<s16> samples{}; stream->AppendBuffer(new_buffer, samples); } else { - std::vector<s16> samples(buffers[i].size / sizeof(s16)); - memory.ReadBlockUnsafe(buffers[i].samples, samples.data(), buffers[i].size); + std::vector<s16> samples(buffer.size / sizeof(s16)); + system.Memory().ReadBlockUnsafe(buffer.samples, samples.data(), buffer.size); stream->AppendBuffer(new_buffer, samples); } } } -void DeviceSession::ReleaseBuffer(AudioBuffer& buffer) const { +void DeviceSession::ReleaseBuffer(const AudioBuffer& buffer) const { if (type == Sink::StreamType::In) { - auto& memory{system.Memory()}; auto samples{stream->ReleaseBuffer(buffer.size / sizeof(s16))}; - memory.WriteBlockUnsafe(buffer.samples, samples.data(), buffer.size); + system.Memory().WriteBlockUnsafe(buffer.samples, samples.data(), buffer.size); } } -bool DeviceSession::IsBufferConsumed(u64 tag) const { - if (stream) { - return stream->IsBufferConsumed(tag); - } - return true; +bool DeviceSession::IsBufferConsumed(const AudioBuffer& buffer) const { + return played_sample_count >= buffer.end_timestamp; } void DeviceSession::SetVolume(f32 volume) const { @@ -105,10 +111,22 @@ void DeviceSession::SetVolume(f32 volume) const { } u64 DeviceSession::GetPlayedSampleCount() const { - if (stream) { - return stream->GetPlayedSampleCount(); + return played_sample_count; +} + +std::optional<std::chrono::nanoseconds> DeviceSession::ThreadFunc() { + // Add 5ms of samples at a 48K sample rate. + played_sample_count += 48'000 * INCREMENT_TIME / 1s; + if (type == Sink::StreamType::Out) { + system.AudioCore().GetAudioManager().SetEvent(Event::Type::AudioOutManager, true); + } else { + system.AudioCore().GetAudioManager().SetEvent(Event::Type::AudioInManager, true); } - return 0; + return std::nullopt; +} + +void DeviceSession::SetRingSize(u32 ring_size) { + stream->SetRingSize(ring_size); } } // namespace AudioCore diff --git a/src/audio_core/device/device_session.h b/src/audio_core/device/device_session.h index 4a031b765..74f4dc085 100644 --- a/src/audio_core/device/device_session.h +++ b/src/audio_core/device/device_session.h @@ -3,6 +3,9 @@ #pragma once +#include <chrono> +#include <memory> +#include <optional> #include <span> #include "audio_core/common/common.h" @@ -11,9 +14,13 @@ namespace Core { class System; -} +namespace Timing { +struct EventType; +} // namespace Timing +} // namespace Core namespace AudioCore { + namespace Sink { class SinkStream; struct SinkBuffer; @@ -55,22 +62,23 @@ public: * * @param buffers - The buffers to play. */ - void AppendBuffers(std::span<AudioBuffer> buffers) const; + void AppendBuffers(std::span<const AudioBuffer> buffers) const; /** * (Audio In only) Pop samples from the backend, and write them back to this buffer's address. * * @param buffer - The buffer to write to. */ - void ReleaseBuffer(AudioBuffer& buffer) const; + void ReleaseBuffer(const AudioBuffer& buffer) const; /** * Check if the buffer for the given tag has been consumed by the backend. * - * @param tag - Unqiue tag of the buffer to check. + * @param buffer - the buffer to check. + * * @return true if the buffer has been consumed, otherwise false. */ - bool IsBufferConsumed(u64 tag) const; + bool IsBufferConsumed(const AudioBuffer& buffer) const; /** * Start this device session, starting the backend stream. @@ -96,6 +104,16 @@ public: */ u64 GetPlayedSampleCount() const; + /* + * CoreTiming callback to increment played_sample_count over time. + */ + std::optional<std::chrono::nanoseconds> ThreadFunc(); + + /* + * Set the size of the ring buffer. + */ + void SetRingSize(u32 ring_size); + private: /// System Core::System& system; @@ -118,9 +136,13 @@ private: /// Applet resource user id of this device session u64 applet_resource_user_id{}; /// Total number of samples played by this device session - u64 played_sample_count{}; + std::atomic<u64> played_sample_count{}; + /// Event increasing the played sample count every 5ms + std::shared_ptr<Core::Timing::EventType> thread_event; /// Is this session initialised? bool initialized{}; + /// Buffer queue + std::vector<AudioBuffer> buffer_queue{}; }; } // namespace AudioCore diff --git a/src/audio_core/in/audio_in.cpp b/src/audio_core/in/audio_in.cpp index c946895d6..91ccd5ad7 100644 --- a/src/audio_core/in/audio_in.cpp +++ b/src/audio_core/in/audio_in.cpp @@ -72,7 +72,7 @@ Kernel::KReadableEvent& In::GetBufferEvent() { return event->GetReadableEvent(); } -f32 In::GetVolume() { +f32 In::GetVolume() const { std::scoped_lock l{parent_mutex}; return system.GetVolume(); } @@ -82,17 +82,17 @@ void In::SetVolume(f32 volume) { system.SetVolume(volume); } -bool In::ContainsAudioBuffer(u64 tag) { +bool In::ContainsAudioBuffer(u64 tag) const { std::scoped_lock l{parent_mutex}; return system.ContainsAudioBuffer(tag); } -u32 In::GetBufferCount() { +u32 In::GetBufferCount() const { std::scoped_lock l{parent_mutex}; return system.GetBufferCount(); } -u64 In::GetPlayedSampleCount() { +u64 In::GetPlayedSampleCount() const { std::scoped_lock l{parent_mutex}; return system.GetPlayedSampleCount(); } diff --git a/src/audio_core/in/audio_in.h b/src/audio_core/in/audio_in.h index 6253891d5..092ab7236 100644 --- a/src/audio_core/in/audio_in.h +++ b/src/audio_core/in/audio_in.h @@ -102,7 +102,7 @@ public: * * @return The current volume. */ - f32 GetVolume(); + f32 GetVolume() const; /** * Set the system volume. @@ -117,21 +117,21 @@ public: * @param tag - The tag to search for. * @return True if the buffer is in the system, otherwise false. */ - bool ContainsAudioBuffer(u64 tag); + bool ContainsAudioBuffer(u64 tag) const; /** * Get the maximum number of buffers. * * @return The maximum number of buffers. */ - u32 GetBufferCount(); + u32 GetBufferCount() const; /** * Get the total played sample count for this audio in. * * @return The played sample count. */ - u64 GetPlayedSampleCount(); + u64 GetPlayedSampleCount() const; private: /// The AudioIn::Manager this audio in is registered with diff --git a/src/audio_core/in/audio_in_system.cpp b/src/audio_core/in/audio_in_system.cpp index ec5d37ed4..e7f918a47 100644 --- a/src/audio_core/in/audio_in_system.cpp +++ b/src/audio_core/in/audio_in_system.cpp @@ -34,16 +34,16 @@ size_t System::GetSessionId() const { return session_id; } -std::string_view System::GetDefaultDeviceName() { +std::string_view System::GetDefaultDeviceName() const { return "BuiltInHeadset"; } -std::string_view System::GetDefaultUacDeviceName() { +std::string_view System::GetDefaultUacDeviceName() const { return "Uac"; } Result System::IsConfigValid(const std::string_view device_name, - const AudioInParameter& in_params) { + const AudioInParameter& in_params) const { if ((device_name.size() > 0) && (device_name != GetDefaultDeviceName() && device_name != GetDefaultUacDeviceName())) { return Service::Audio::ERR_INVALID_DEVICE_NAME; @@ -93,6 +93,7 @@ Result System::Start() { std::vector<AudioBuffer> buffers_to_flush{}; buffers.RegisterBuffers(buffers_to_flush); session->AppendBuffers(buffers_to_flush); + session->SetRingSize(static_cast<u32>(buffers_to_flush.size())); return ResultSuccess; } @@ -112,8 +113,15 @@ bool System::AppendBuffer(const AudioInBuffer& buffer, const u64 tag) { return false; } - AudioBuffer new_buffer{ - .played_timestamp = 0, .samples = buffer.samples, .tag = tag, .size = buffer.size}; + const auto timestamp{buffers.GetNextTimestamp()}; + const AudioBuffer new_buffer{ + .start_timestamp = timestamp, + .end_timestamp = timestamp + buffer.size / (channel_count * sizeof(s16)), + .played_timestamp = 0, + .samples = buffer.samples, + .tag = tag, + .size = buffer.size, + }; buffers.AppendBuffer(new_buffer); RegisterBuffers(); @@ -194,11 +202,11 @@ void System::SetVolume(const f32 volume_) { session->SetVolume(volume_); } -bool System::ContainsAudioBuffer(const u64 tag) { +bool System::ContainsAudioBuffer(const u64 tag) const { return buffers.ContainsBuffer(tag); } -u32 System::GetBufferCount() { +u32 System::GetBufferCount() const { return buffers.GetAppendedRegisteredCount(); } diff --git a/src/audio_core/in/audio_in_system.h b/src/audio_core/in/audio_in_system.h index 165e35d83..b9dc0e60f 100644 --- a/src/audio_core/in/audio_in_system.h +++ b/src/audio_core/in/audio_in_system.h @@ -68,7 +68,7 @@ public: * * @return The default audio input device name. */ - std::string_view GetDefaultDeviceName(); + std::string_view GetDefaultDeviceName() const; /** * Get the default USB audio input device name. @@ -77,7 +77,7 @@ public: * * @return The default USB audio input device name. */ - std::string_view GetDefaultUacDeviceName(); + std::string_view GetDefaultUacDeviceName() const; /** * Is the given initialize config valid? @@ -86,7 +86,7 @@ public: * @param in_params - Input parameters, see AudioInParameter. * @return Result code. */ - Result IsConfigValid(std::string_view device_name, const AudioInParameter& in_params); + Result IsConfigValid(std::string_view device_name, const AudioInParameter& in_params) const; /** * Initialize this system. @@ -208,7 +208,7 @@ public: /** * Set this system's current volume. * - * @param The new volume. + * @param volume The new volume. */ void SetVolume(f32 volume); @@ -218,14 +218,14 @@ public: * @param tag - Unique tag to search for. * @return True if the buffer is in the system, otherwise false. */ - bool ContainsAudioBuffer(u64 tag); + bool ContainsAudioBuffer(u64 tag) const; /** * Get the maximum number of usable buffers (default 32). * * @return The number of buffers. */ - u32 GetBufferCount(); + u32 GetBufferCount() const; /** * Get the total number of samples played by this system. diff --git a/src/audio_core/out/audio_out.cpp b/src/audio_core/out/audio_out.cpp index 9a8d8a742..d3ee4f0eb 100644 --- a/src/audio_core/out/audio_out.cpp +++ b/src/audio_core/out/audio_out.cpp @@ -72,7 +72,7 @@ Kernel::KReadableEvent& Out::GetBufferEvent() { return event->GetReadableEvent(); } -f32 Out::GetVolume() { +f32 Out::GetVolume() const { std::scoped_lock l{parent_mutex}; return system.GetVolume(); } @@ -82,17 +82,17 @@ void Out::SetVolume(const f32 volume) { system.SetVolume(volume); } -bool Out::ContainsAudioBuffer(const u64 tag) { +bool Out::ContainsAudioBuffer(const u64 tag) const { std::scoped_lock l{parent_mutex}; return system.ContainsAudioBuffer(tag); } -u32 Out::GetBufferCount() { +u32 Out::GetBufferCount() const { std::scoped_lock l{parent_mutex}; return system.GetBufferCount(); } -u64 Out::GetPlayedSampleCount() { +u64 Out::GetPlayedSampleCount() const { std::scoped_lock l{parent_mutex}; return system.GetPlayedSampleCount(); } diff --git a/src/audio_core/out/audio_out.h b/src/audio_core/out/audio_out.h index f6b921645..946f345c6 100644 --- a/src/audio_core/out/audio_out.h +++ b/src/audio_core/out/audio_out.h @@ -102,7 +102,7 @@ public: * * @return The current volume. */ - f32 GetVolume(); + f32 GetVolume() const; /** * Set the system volume. @@ -117,21 +117,21 @@ public: * @param tag - The tag to search for. * @return True if the buffer is in the system, otherwise false. */ - bool ContainsAudioBuffer(u64 tag); + bool ContainsAudioBuffer(u64 tag) const; /** * Get the maximum number of buffers. * * @return The maximum number of buffers. */ - u32 GetBufferCount(); + u32 GetBufferCount() const; /** * Get the total played sample count for this audio out. * * @return The played sample count. */ - u64 GetPlayedSampleCount(); + u64 GetPlayedSampleCount() const; private: /// The AudioOut::Manager this audio out is registered with diff --git a/src/audio_core/out/audio_out_system.cpp b/src/audio_core/out/audio_out_system.cpp index 35afddf06..8b907590a 100644 --- a/src/audio_core/out/audio_out_system.cpp +++ b/src/audio_core/out/audio_out_system.cpp @@ -27,11 +27,12 @@ void System::Finalize() { buffer_event->GetWritableEvent().Signal(); } -std::string_view System::GetDefaultOutputDeviceName() { +std::string_view System::GetDefaultOutputDeviceName() const { return "DeviceOut"; } -Result System::IsConfigValid(std::string_view device_name, const AudioOutParameter& in_params) { +Result System::IsConfigValid(std::string_view device_name, + const AudioOutParameter& in_params) const { if ((device_name.size() > 0) && (device_name != GetDefaultOutputDeviceName())) { return Service::Audio::ERR_INVALID_DEVICE_NAME; } @@ -92,6 +93,7 @@ Result System::Start() { std::vector<AudioBuffer> buffers_to_flush{}; buffers.RegisterBuffers(buffers_to_flush); session->AppendBuffers(buffers_to_flush); + session->SetRingSize(static_cast<u32>(buffers_to_flush.size())); return ResultSuccess; } @@ -111,8 +113,15 @@ bool System::AppendBuffer(const AudioOutBuffer& buffer, u64 tag) { return false; } - AudioBuffer new_buffer{ - .played_timestamp = 0, .samples = buffer.samples, .tag = tag, .size = buffer.size}; + const auto timestamp{buffers.GetNextTimestamp()}; + const AudioBuffer new_buffer{ + .start_timestamp = timestamp, + .end_timestamp = timestamp + buffer.size / (channel_count * sizeof(s16)), + .played_timestamp = 0, + .samples = buffer.samples, + .tag = tag, + .size = buffer.size, + }; buffers.AppendBuffer(new_buffer); RegisterBuffers(); @@ -192,11 +201,11 @@ void System::SetVolume(const f32 volume_) { session->SetVolume(volume_); } -bool System::ContainsAudioBuffer(const u64 tag) { +bool System::ContainsAudioBuffer(const u64 tag) const { return buffers.ContainsBuffer(tag); } -u32 System::GetBufferCount() { +u32 System::GetBufferCount() const { return buffers.GetAppendedRegisteredCount(); } diff --git a/src/audio_core/out/audio_out_system.h b/src/audio_core/out/audio_out_system.h index 4ca2f3417..0817b2f37 100644 --- a/src/audio_core/out/audio_out_system.h +++ b/src/audio_core/out/audio_out_system.h @@ -68,7 +68,7 @@ public: * * @return The default audio output device name. */ - std::string_view GetDefaultOutputDeviceName(); + std::string_view GetDefaultOutputDeviceName() const; /** * Is the given initialize config valid? @@ -77,7 +77,7 @@ public: * @param in_params - Input parameters, see AudioOutParameter. * @return Result code. */ - Result IsConfigValid(std::string_view device_name, const AudioOutParameter& in_params); + Result IsConfigValid(std::string_view device_name, const AudioOutParameter& in_params) const; /** * Initialize this system. @@ -199,7 +199,7 @@ public: /** * Set this system's current volume. * - * @param The new volume. + * @param volume The new volume. */ void SetVolume(f32 volume); @@ -209,14 +209,14 @@ public: * @param tag - Unique tag to search for. * @return True if the buffer is in the system, otherwise false. */ - bool ContainsAudioBuffer(u64 tag); + bool ContainsAudioBuffer(u64 tag) const; /** * Get the maximum number of usable buffers (default 32). * * @return The number of buffers. */ - u32 GetBufferCount(); + u32 GetBufferCount() const; /** * Get the total number of samples played by this system. diff --git a/src/audio_core/renderer/adsp/adsp.cpp b/src/audio_core/renderer/adsp/adsp.cpp index e05a22d86..a28395663 100644 --- a/src/audio_core/renderer/adsp/adsp.cpp +++ b/src/audio_core/renderer/adsp/adsp.cpp @@ -50,7 +50,7 @@ u32 ADSP::GetRemainCommandCount(const u32 session_id) const { return render_mailbox.GetRemainCommandCount(session_id); } -void ADSP::SendCommandBuffer(const u32 session_id, CommandBuffer& command_buffer) { +void ADSP::SendCommandBuffer(const u32 session_id, const CommandBuffer& command_buffer) { render_mailbox.SetCommandBuffer(session_id, command_buffer); } diff --git a/src/audio_core/renderer/adsp/adsp.h b/src/audio_core/renderer/adsp/adsp.h index 4dfcef4a5..f7a2f25e4 100644 --- a/src/audio_core/renderer/adsp/adsp.h +++ b/src/audio_core/renderer/adsp/adsp.h @@ -63,8 +63,6 @@ public: /** * Stop the ADSP. - * - * @return True if started or already running, otherwise false. */ void Stop(); @@ -133,7 +131,7 @@ public: * @param session_id - The session id to check (0 or 1). * @param command_buffer - The command buffer to process. */ - void SendCommandBuffer(u32 session_id, CommandBuffer& command_buffer); + void SendCommandBuffer(u32 session_id, const CommandBuffer& command_buffer); /** * Clear the command buffers (does not clear the time taken or the remaining command count) diff --git a/src/audio_core/renderer/adsp/audio_renderer.cpp b/src/audio_core/renderer/adsp/audio_renderer.cpp index 3967ccfe6..bafe4822a 100644 --- a/src/audio_core/renderer/adsp/audio_renderer.cpp +++ b/src/audio_core/renderer/adsp/audio_renderer.cpp @@ -51,7 +51,7 @@ CommandBuffer& AudioRenderer_Mailbox::GetCommandBuffer(const s32 session_id) { return command_buffers[session_id]; } -void AudioRenderer_Mailbox::SetCommandBuffer(const u32 session_id, CommandBuffer& buffer) { +void AudioRenderer_Mailbox::SetCommandBuffer(const u32 session_id, const CommandBuffer& buffer) { command_buffers[session_id] = buffer; } @@ -106,9 +106,6 @@ void AudioRenderer::Start(AudioRenderer_Mailbox* mailbox_) { mailbox = mailbox_; thread = std::thread(&AudioRenderer::ThreadFunc, this); - for (auto& stream : streams) { - stream->Start(); - } running = true; } @@ -130,6 +127,7 @@ void AudioRenderer::CreateSinkStreams() { std::string name{fmt::format("ADSP_RenderStream-{}", i)}; streams[i] = sink.AcquireSinkStream(system, channels, name, ::AudioCore::Sink::StreamType::Render); + streams[i]->SetRingSize(4); } } @@ -198,11 +196,6 @@ void AudioRenderer::ThreadFunc() { command_list_processor.Process(index) - start_time; } - if (index == 0) { - auto stream{command_list_processor.GetOutputSinkStream()}; - system.AudioCore().SetStreamQueue(stream->GetQueueSize()); - } - const auto end_time{system.CoreTiming().GetClockTicks()}; command_buffer.remaining_command_count = diff --git a/src/audio_core/renderer/adsp/audio_renderer.h b/src/audio_core/renderer/adsp/audio_renderer.h index b6ced9d2b..02e923c84 100644 --- a/src/audio_core/renderer/adsp/audio_renderer.h +++ b/src/audio_core/renderer/adsp/audio_renderer.h @@ -52,7 +52,7 @@ public: /** * Send a message from the host to the AudioRenderer. * - * @param message_ - The message to send to the AudioRenderer. + * @param message - The message to send to the AudioRenderer. */ void HostSendMessage(RenderMessage message); @@ -66,7 +66,7 @@ public: /** * Send a message from the AudioRenderer to the host. * - * @param message_ - The message to send to the host. + * @param message - The message to send to the host. */ void ADSPSendMessage(RenderMessage message); @@ -91,7 +91,7 @@ public: * @param session_id - The session id to get (0 or 1). * @param buffer - The command buffer to set. */ - void SetCommandBuffer(u32 session_id, CommandBuffer& buffer); + void SetCommandBuffer(u32 session_id, const CommandBuffer& buffer); /** * Get the total render time taken for the last command lists sent. @@ -163,7 +163,7 @@ public: /** * Start the AudioRenderer. * - * @param The mailbox to use for this session. + * @param mailbox The mailbox to use for this session. */ void Start(AudioRenderer_Mailbox* mailbox); diff --git a/src/audio_core/renderer/adsp/command_list_processor.h b/src/audio_core/renderer/adsp/command_list_processor.h index 3f99173e3..d78269e1d 100644 --- a/src/audio_core/renderer/adsp/command_list_processor.h +++ b/src/audio_core/renderer/adsp/command_list_processor.h @@ -33,10 +33,10 @@ public: /** * Initialize the processor. * - * @param system_ - The core system. - * @param buffer - The command buffer to process. - * @param size - The size of the buffer. - * @param stream_ - The stream to be used for sending the samples. + * @param system - The core system. + * @param buffer - The command buffer to process. + * @param size - The size of the buffer. + * @param stream - The stream to be used for sending the samples. */ void Initialize(Core::System& system, CpuAddr buffer, u64 size, Sink::SinkStream* stream); @@ -72,7 +72,8 @@ public: /** * Process the command list. * - * @param index - Index of the current command list. + * @param session_id - Session ID for the commands being processed. + * * @return The time taken to process. */ u64 Process(u32 session_id); @@ -89,7 +90,7 @@ public: u8* commands{}; /// The command buffer size u64 commands_buffer_size{}; - /// The maximum processing time alloted + /// The maximum processing time allotted u64 max_process_time{}; /// The number of commands in the buffer u32 command_count{}; diff --git a/src/audio_core/renderer/audio_device.cpp b/src/audio_core/renderer/audio_device.cpp index d5886e55e..0d9d8f6ce 100644 --- a/src/audio_core/renderer/audio_device.cpp +++ b/src/audio_core/renderer/audio_device.cpp @@ -1,6 +1,9 @@ // SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include <array> +#include <span> + #include "audio_core/audio_core.h" #include "audio_core/common/feature_support.h" #include "audio_core/renderer/audio_device.h" @@ -9,14 +12,33 @@ namespace AudioCore::AudioRenderer { +constexpr std::array usb_device_names{ + AudioDevice::AudioDeviceName{"AudioStereoJackOutput"}, + AudioDevice::AudioDeviceName{"AudioBuiltInSpeakerOutput"}, + AudioDevice::AudioDeviceName{"AudioTvOutput"}, + AudioDevice::AudioDeviceName{"AudioUsbDeviceOutput"}, +}; + +constexpr std::array device_names{ + AudioDevice::AudioDeviceName{"AudioStereoJackOutput"}, + AudioDevice::AudioDeviceName{"AudioBuiltInSpeakerOutput"}, + AudioDevice::AudioDeviceName{"AudioTvOutput"}, +}; + +constexpr std::array output_device_names{ + AudioDevice::AudioDeviceName{"AudioBuiltInSpeakerOutput"}, + AudioDevice::AudioDeviceName{"AudioTvOutput"}, + AudioDevice::AudioDeviceName{"AudioExternalOutput"}, +}; + AudioDevice::AudioDevice(Core::System& system, const u64 applet_resource_user_id_, const u32 revision) : output_sink{system.AudioCore().GetOutputSink()}, applet_resource_user_id{applet_resource_user_id_}, user_revision{revision} {} u32 AudioDevice::ListAudioDeviceName(std::vector<AudioDeviceName>& out_buffer, - const size_t max_count) { - std::span<AudioDeviceName> names{}; + const size_t max_count) const { + std::span<const AudioDeviceName> names{}; if (CheckFeatureSupported(SupportTags::AudioUsbDeviceOutput, user_revision)) { names = usb_device_names; @@ -24,7 +46,7 @@ u32 AudioDevice::ListAudioDeviceName(std::vector<AudioDeviceName>& out_buffer, names = device_names; } - u32 out_count{static_cast<u32>(std::min(max_count, names.size()))}; + const u32 out_count{static_cast<u32>(std::min(max_count, names.size()))}; for (u32 i = 0; i < out_count; i++) { out_buffer.push_back(names[i]); } @@ -32,8 +54,8 @@ u32 AudioDevice::ListAudioDeviceName(std::vector<AudioDeviceName>& out_buffer, } u32 AudioDevice::ListAudioOutputDeviceName(std::vector<AudioDeviceName>& out_buffer, - const size_t max_count) { - u32 out_count{static_cast<u32>(std::min(max_count, output_device_names.size()))}; + const size_t max_count) const { + const u32 out_count{static_cast<u32>(std::min(max_count, output_device_names.size()))}; for (u32 i = 0; i < out_count; i++) { out_buffer.push_back(output_device_names[i]); @@ -45,7 +67,7 @@ void AudioDevice::SetDeviceVolumes(const f32 volume) { output_sink.SetDeviceVolume(volume); } -f32 AudioDevice::GetDeviceVolume([[maybe_unused]] std::string_view name) { +f32 AudioDevice::GetDeviceVolume([[maybe_unused]] std::string_view name) const { return output_sink.GetDeviceVolume(); } diff --git a/src/audio_core/renderer/audio_device.h b/src/audio_core/renderer/audio_device.h index 1f449f261..dd6be70ee 100644 --- a/src/audio_core/renderer/audio_device.h +++ b/src/audio_core/renderer/audio_device.h @@ -3,7 +3,7 @@ #pragma once -#include <span> +#include <string_view> #include "audio_core/audio_render_manager.h" @@ -23,21 +23,13 @@ namespace AudioRenderer { class AudioDevice { public: struct AudioDeviceName { - std::array<char, 0x100> name; + std::array<char, 0x100> name{}; - AudioDeviceName(const char* name_) { - std::strncpy(name.data(), name_, name.size()); + constexpr AudioDeviceName(std::string_view name_) { + name_.copy(name.data(), name.size() - 1); } }; - std::array<AudioDeviceName, 4> usb_device_names{"AudioStereoJackOutput", - "AudioBuiltInSpeakerOutput", "AudioTvOutput", - "AudioUsbDeviceOutput"}; - std::array<AudioDeviceName, 3> device_names{"AudioStereoJackOutput", - "AudioBuiltInSpeakerOutput", "AudioTvOutput"}; - std::array<AudioDeviceName, 3> output_device_names{"AudioBuiltInSpeakerOutput", "AudioTvOutput", - "AudioExternalOutput"}; - explicit AudioDevice(Core::System& system, u64 applet_resource_user_id, u32 revision); /** @@ -47,7 +39,7 @@ public: * @param max_count - Maximum number of devices to write (count of out_buffer). * @return Number of device names written. */ - u32 ListAudioDeviceName(std::vector<AudioDeviceName>& out_buffer, size_t max_count); + u32 ListAudioDeviceName(std::vector<AudioDeviceName>& out_buffer, size_t max_count) const; /** * Get a list of the available output devices. @@ -57,7 +49,7 @@ public: * @param max_count - Maximum number of devices to write (count of out_buffer). * @return Number of device names written. */ - u32 ListAudioOutputDeviceName(std::vector<AudioDeviceName>& out_buffer, size_t max_count); + u32 ListAudioOutputDeviceName(std::vector<AudioDeviceName>& out_buffer, size_t max_count) const; /** * Set the volume of all streams in the backend sink. @@ -73,7 +65,7 @@ public: * @param name - Name of the device to check. Unused. * @return Volume of the device. */ - f32 GetDeviceVolume(std::string_view name); + f32 GetDeviceVolume(std::string_view name) const; private: /// Backend output sink for the device diff --git a/src/audio_core/renderer/behavior/behavior_info.cpp b/src/audio_core/renderer/behavior/behavior_info.cpp index c5d4d66d8..3d2a91312 100644 --- a/src/audio_core/renderer/behavior/behavior_info.cpp +++ b/src/audio_core/renderer/behavior/behavior_info.cpp @@ -34,7 +34,7 @@ void BehaviorInfo::ClearError() { error_count = 0; } -void BehaviorInfo::AppendError(ErrorInfo& error) { +void BehaviorInfo::AppendError(const ErrorInfo& error) { LOG_ERROR(Service_Audio, "Error during RequestUpdate, reporting code {:04X} address {:08X}", error.error_code.raw, error.address); if (error_count < MaxErrors) { @@ -42,14 +42,16 @@ void BehaviorInfo::AppendError(ErrorInfo& error) { } } -void BehaviorInfo::CopyErrorInfo(std::span<ErrorInfo> out_errors, u32& out_count) { - auto error_count_{std::min(error_count, MaxErrors)}; - std::memset(out_errors.data(), 0, MaxErrors * sizeof(ErrorInfo)); +void BehaviorInfo::CopyErrorInfo(std::span<ErrorInfo> out_errors, u32& out_count) const { + out_count = std::min(error_count, MaxErrors); - for (size_t i = 0; i < error_count_; i++) { - out_errors[i] = errors[i]; + for (size_t i = 0; i < MaxErrors; i++) { + if (i < out_count) { + out_errors[i] = errors[i]; + } else { + out_errors[i] = {}; + } } - out_count = error_count_; } void BehaviorInfo::UpdateFlags(const Flags flags_) { diff --git a/src/audio_core/renderer/behavior/behavior_info.h b/src/audio_core/renderer/behavior/behavior_info.h index 7333c297f..15c948344 100644 --- a/src/audio_core/renderer/behavior/behavior_info.h +++ b/src/audio_core/renderer/behavior/behavior_info.h @@ -94,7 +94,7 @@ public: * * @param error - The new error. */ - void AppendError(ErrorInfo& error); + void AppendError(const ErrorInfo& error); /** * Copy errors to the given output container. @@ -102,7 +102,7 @@ public: * @param out_errors - Output container to receive the errors. * @param out_count - The number of errors written. */ - void CopyErrorInfo(std::span<ErrorInfo> out_errors, u32& out_count); + void CopyErrorInfo(std::span<ErrorInfo> out_errors, u32& out_count) const; /** * Update the behaviour flags. diff --git a/src/audio_core/renderer/behavior/info_updater.cpp b/src/audio_core/renderer/behavior/info_updater.cpp index 06a37e1a6..c0a307b89 100644 --- a/src/audio_core/renderer/behavior/info_updater.cpp +++ b/src/audio_core/renderer/behavior/info_updater.cpp @@ -485,7 +485,7 @@ Result InfoUpdater::UpdateBehaviorInfo(BehaviorInfo& behaviour_) { return ResultSuccess; } -Result InfoUpdater::UpdateErrorInfo(BehaviorInfo& behaviour_) { +Result InfoUpdater::UpdateErrorInfo(const BehaviorInfo& behaviour_) { auto out_params{reinterpret_cast<BehaviorInfo::OutStatus*>(output)}; behaviour_.CopyErrorInfo(out_params->errors, out_params->error_count); diff --git a/src/audio_core/renderer/behavior/info_updater.h b/src/audio_core/renderer/behavior/info_updater.h index f0b445d9c..c817d8d8d 100644 --- a/src/audio_core/renderer/behavior/info_updater.h +++ b/src/audio_core/renderer/behavior/info_updater.h @@ -130,7 +130,7 @@ public: * @param behaviour - Behaviour to update. * @return Result code. */ - Result UpdateErrorInfo(BehaviorInfo& behaviour); + Result UpdateErrorInfo(const BehaviorInfo& behaviour); /** * Update splitter. diff --git a/src/audio_core/renderer/command/command_buffer.h b/src/audio_core/renderer/command/command_buffer.h index 496b0e50a..162170846 100644 --- a/src/audio_core/renderer/command/command_buffer.h +++ b/src/audio_core/renderer/command/command_buffer.h @@ -191,6 +191,7 @@ public: * @param volume - Current mix volume used for calculating the ramp. * @param prev_volume - Previous mix volume, used for calculating the ramp, * also applied to the input. + * @param prev_samples - Previous sample buffer. Used for depopping. * @param precision - Number of decimal bits for fixed point operations. */ void GenerateMixRampCommand(s32 node_id, s16 buffer_count, s16 input_index, s16 output_index, @@ -208,6 +209,7 @@ public: * @param volumes - Current mix volumes used for calculating the ramp. * @param prev_volumes - Previous mix volumes, used for calculating the ramp, * also applied to the input. + * @param prev_samples - Previous sample buffer. Used for depopping. * @param precision - Number of decimal bits for fixed point operations. */ void GenerateMixRampGroupedCommand(s32 node_id, s16 buffer_count, s16 input_index, @@ -297,11 +299,11 @@ public: /** * Generate a device sink command, adding it to the command list. * - * @param node_id - Node id of the voice this command is generated for. - * @param buffer_offset - Base mix buffer offset to use. - * @param sink_info - The sink_info to generate this command from. - * @session_id - System session id this command is generated from. - * @samples_buffer - The buffer to be sent to the sink if upsampling is not used. + * @param node_id - Node id of the voice this command is generated for. + * @param buffer_offset - Base mix buffer offset to use. + * @param sink_info - The sink_info to generate this command from. + * @param session_id - System session id this command is generated from. + * @param samples_buffer - The buffer to be sent to the sink if upsampling is not used. */ void GenerateDeviceSinkCommand(s32 node_id, s16 buffer_offset, SinkInfoBase& sink_info, u32 session_id, std::span<s32> samples_buffer); diff --git a/src/audio_core/renderer/command/command_generator.h b/src/audio_core/renderer/command/command_generator.h index d80d9b0d8..b3cd7b408 100644 --- a/src/audio_core/renderer/command/command_generator.h +++ b/src/audio_core/renderer/command/command_generator.h @@ -197,9 +197,9 @@ public: /** * Generate an I3DL2 reverb effect command. * - * @param buffer_offset - Base mix buffer offset to use. - * @param effect_info_base - I3DL2Reverb effect info. - * @param node_id - Node id of the mix this command is generated for. + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info - I3DL2Reverb effect info. + * @param node_id - Node id of the mix this command is generated for. */ void GenerateI3dl2ReverbEffectCommand(s16 buffer_offset, EffectInfoBase& effect_info, s32 node_id); @@ -207,18 +207,18 @@ public: /** * Generate an aux effect command. * - * @param buffer_offset - Base mix buffer offset to use. - * @param effect_info_base - Aux effect info. - * @param node_id - Node id of the mix this command is generated for. + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info - Aux effect info. + * @param node_id - Node id of the mix this command is generated for. */ void GenerateAuxCommand(s16 buffer_offset, EffectInfoBase& effect_info, s32 node_id); /** * Generate a biquad filter effect command. * - * @param buffer_offset - Base mix buffer offset to use. - * @param effect_info_base - Aux effect info. - * @param node_id - Node id of the mix this command is generated for. + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info - Aux effect info. + * @param node_id - Node id of the mix this command is generated for. */ void GenerateBiquadFilterEffectCommand(s16 buffer_offset, EffectInfoBase& effect_info, s32 node_id); @@ -226,10 +226,10 @@ public: /** * Generate a light limiter effect command. * - * @param buffer_offset - Base mix buffer offset to use. - * @param effect_info_base - Limiter effect info. - * @param node_id - Node id of the mix this command is generated for. - * @param effect_index - Index for the statistics state. + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info - Limiter effect info. + * @param node_id - Node id of the mix this command is generated for. + * @param effect_index - Index for the statistics state. */ void GenerateLightLimiterEffectCommand(s16 buffer_offset, EffectInfoBase& effect_info, s32 node_id, u32 effect_index); @@ -238,21 +238,20 @@ public: * Generate a capture effect command. * Writes a mix buffer back to game memory. * - * @param buffer_offset - Base mix buffer offset to use. - * @param effect_info_base - Capture effect info. - * @param node_id - Node id of the mix this command is generated for. + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info - Capture effect info. + * @param node_id - Node id of the mix this command is generated for. */ void GenerateCaptureCommand(s16 buffer_offset, EffectInfoBase& effect_info, s32 node_id); /** * Generate a compressor effect command. * - * @param buffer_offset - Base mix buffer offset to use. - * @param effect_info_base - Compressor effect info. - * @param node_id - Node id of the mix this command is generated for. + * @param buffer_offset - Base mix buffer offset to use. + * @param effect_info - Compressor effect info. + * @param node_id - Node id of the mix this command is generated for. */ - void GenerateCompressorCommand(const s16 buffer_offset, EffectInfoBase& effect_info, - const s32 node_id); + void GenerateCompressorCommand(s16 buffer_offset, EffectInfoBase& effect_info, s32 node_id); /** * Generate all effect commands for a mix. @@ -318,8 +317,9 @@ public: * Generate a performance command. * Used to report performance metrics of the AudioRenderer back to the game. * - * @param buffer_offset - Base mix buffer offset to use. - * @param sink_info - Sink info to generate the commands from. + * @param node_id - Node ID of the mix this command is generated for + * @param state - Output state of the generated performance command + * @param entry_addresses - Addresses to be written */ void GeneratePerformanceCommand(s32 node_id, PerformanceState state, const PerformanceEntryAddresses& entry_addresses); diff --git a/src/audio_core/renderer/command/effect/compressor.cpp b/src/audio_core/renderer/command/effect/compressor.cpp index 2ebc140f1..7229618e8 100644 --- a/src/audio_core/renderer/command/effect/compressor.cpp +++ b/src/audio_core/renderer/command/effect/compressor.cpp @@ -11,7 +11,7 @@ namespace AudioCore::AudioRenderer { -static void SetCompressorEffectParameter(CompressorInfo::ParameterVersion2& params, +static void SetCompressorEffectParameter(const CompressorInfo::ParameterVersion2& params, CompressorInfo::State& state) { const auto ratio{1.0f / params.compressor_ratio}; auto makeup_gain{0.0f}; @@ -31,9 +31,9 @@ static void SetCompressorEffectParameter(CompressorInfo::ParameterVersion2& para state.unk_20 = c; } -static void InitializeCompressorEffect(CompressorInfo::ParameterVersion2& params, +static void InitializeCompressorEffect(const CompressorInfo::ParameterVersion2& params, CompressorInfo::State& state) { - std::memset(&state, 0, sizeof(CompressorInfo::State)); + state = {}; state.unk_00 = 0; state.unk_04 = 1.0f; @@ -42,7 +42,7 @@ static void InitializeCompressorEffect(CompressorInfo::ParameterVersion2& params SetCompressorEffectParameter(params, state); } -static void ApplyCompressorEffect(CompressorInfo::ParameterVersion2& params, +static void ApplyCompressorEffect(const CompressorInfo::ParameterVersion2& params, CompressorInfo::State& state, bool enabled, std::vector<std::span<const s32>> input_buffers, std::vector<std::span<s32>> output_buffers, u32 sample_count) { @@ -103,8 +103,7 @@ static void ApplyCompressorEffect(CompressorInfo::ParameterVersion2& params, } else { for (s16 channel = 0; channel < params.channel_count; channel++) { if (params.inputs[channel] != params.outputs[channel]) { - std::memcpy((char*)output_buffers[channel].data(), - (char*)input_buffers[channel].data(), + std::memcpy(output_buffers[channel].data(), input_buffers[channel].data(), output_buffers[channel].size_bytes()); } } diff --git a/src/audio_core/renderer/command/mix/mix_ramp.cpp b/src/audio_core/renderer/command/mix/mix_ramp.cpp index ffdafa1c8..d67123cd8 100644 --- a/src/audio_core/renderer/command/mix/mix_ramp.cpp +++ b/src/audio_core/renderer/command/mix/mix_ramp.cpp @@ -7,17 +7,7 @@ #include "common/logging/log.h" namespace AudioCore::AudioRenderer { -/** - * Mix input mix buffer into output mix buffer, with volume applied to the input. - * - * @tparam Q - Number of bits for fixed point operations. - * @param output - Output mix buffer. - * @param input - Input mix buffer. - * @param volume - Volume applied to the input. - * @param ramp - Ramp applied to volume every sample. - * @param sample_count - Number of samples to process. - * @return The final gained input sample, used for depopping. - */ + template <size_t Q> s32 ApplyMixRamp(std::span<s32> output, std::span<const s32> input, const f32 volume_, const f32 ramp_, const u32 sample_count) { @@ -40,10 +30,8 @@ s32 ApplyMixRamp(std::span<s32> output, std::span<const s32> input, const f32 vo return sample.to_int(); } -template s32 ApplyMixRamp<15>(std::span<s32>, std::span<const s32>, const f32, const f32, - const u32); -template s32 ApplyMixRamp<23>(std::span<s32>, std::span<const s32>, const f32, const f32, - const u32); +template s32 ApplyMixRamp<15>(std::span<s32>, std::span<const s32>, f32, f32, u32); +template s32 ApplyMixRamp<23>(std::span<s32>, std::span<const s32>, f32, f32, u32); void MixRampCommand::Dump(const ADSP::CommandListProcessor& processor, std::string& string) { const auto ramp{(volume - prev_volume) / static_cast<f32>(processor.sample_count)}; diff --git a/src/audio_core/renderer/command/mix/mix_ramp.h b/src/audio_core/renderer/command/mix/mix_ramp.h index 770f57e80..52f74a273 100644 --- a/src/audio_core/renderer/command/mix/mix_ramp.h +++ b/src/audio_core/renderer/command/mix/mix_ramp.h @@ -61,13 +61,13 @@ struct MixRampCommand : ICommand { * @tparam Q - Number of bits for fixed point operations. * @param output - Output mix buffer. * @param input - Input mix buffer. - * @param volume - Volume applied to the input. - * @param ramp - Ramp applied to volume every sample. + * @param volume_ - Volume applied to the input. + * @param ramp_ - Ramp applied to volume every sample. * @param sample_count - Number of samples to process. * @return The final gained input sample, used for depopping. */ template <size_t Q> -s32 ApplyMixRamp(std::span<s32> output, std::span<const s32> input, const f32 volume_, - const f32 ramp_, const u32 sample_count); +s32 ApplyMixRamp(std::span<s32> output, std::span<const s32> input, f32 volume_, f32 ramp_, + u32 sample_count); } // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/command/mix/mix_ramp_grouped.h b/src/audio_core/renderer/command/mix/mix_ramp_grouped.h index 027276e5a..3b0ce67ef 100644 --- a/src/audio_core/renderer/command/mix/mix_ramp_grouped.h +++ b/src/audio_core/renderer/command/mix/mix_ramp_grouped.h @@ -50,9 +50,9 @@ struct MixRampGroupedCommand : ICommand { std::array<s16, MaxMixBuffers> inputs; /// Output mix buffer indexes for each mix buffer std::array<s16, MaxMixBuffers> outputs; - /// Previous mix vloumes for each mix buffer + /// Previous mix volumes for each mix buffer std::array<f32, MaxMixBuffers> prev_volumes; - /// Current mix vloumes for each mix buffer + /// Current mix volumes for each mix buffer std::array<f32, MaxMixBuffers> volumes; /// Pointer to the previous sample buffer, used for depop CpuAddr previous_samples; diff --git a/src/audio_core/renderer/command/sink/device.cpp b/src/audio_core/renderer/command/sink/device.cpp index 47e0c6722..e88372a75 100644 --- a/src/audio_core/renderer/command/sink/device.cpp +++ b/src/audio_core/renderer/command/sink/device.cpp @@ -46,6 +46,10 @@ void DeviceSinkCommand::Process(const ADSP::CommandListProcessor& processor) { out_buffer.tag = reinterpret_cast<u64>(samples.data()); stream->AppendBuffer(out_buffer, samples); + + if (stream->IsPaused()) { + stream->Start(); + } } bool DeviceSinkCommand::Verify(const ADSP::CommandListProcessor& processor) { diff --git a/src/audio_core/renderer/effect/effect_context.h b/src/audio_core/renderer/effect/effect_context.h index 85955bd9c..8f6d6e7d8 100644 --- a/src/audio_core/renderer/effect/effect_context.h +++ b/src/audio_core/renderer/effect/effect_context.h @@ -15,15 +15,15 @@ class EffectContext { public: /** * Initialize the effect context - * @param effect_infos List of effect infos for this context - * @param effect_count The number of effects in the list - * @param result_states_cpu The workbuffer of result states for the CPU for this context - * @param result_states_dsp The workbuffer of result states for the DSP for this context - * @param state_count The number of result states + * @param effect_infos_ - List of effect infos for this context + * @param effect_count_ - The number of effects in the list + * @param result_states_cpu_ - The workbuffer of result states for the CPU for this context + * @param result_states_dsp_ - The workbuffer of result states for the DSP for this context + * @param dsp_state_count - The number of result states */ - void Initialize(std::span<EffectInfoBase> effect_infos_, const u32 effect_count_, + void Initialize(std::span<EffectInfoBase> effect_infos_, u32 effect_count_, std::span<EffectResultState> result_states_cpu_, - std::span<EffectResultState> result_states_dsp_, const size_t dsp_state_count); + std::span<EffectResultState> result_states_dsp_, size_t dsp_state_count); /** * Get the EffectInfo for a given index diff --git a/src/audio_core/renderer/effect/effect_info_base.h b/src/audio_core/renderer/effect/effect_info_base.h index 8c9583878..8525fde05 100644 --- a/src/audio_core/renderer/effect/effect_info_base.h +++ b/src/audio_core/renderer/effect/effect_info_base.h @@ -291,7 +291,7 @@ public: * Update the info with new parameters, version 1. * * @param error_info - Used to write call result code. - * @param in_params - New parameters to update the info with. + * @param params - New parameters to update the info with. * @param pool_mapper - Pool for mapping buffers. */ virtual void Update(BehaviorInfo::ErrorInfo& error_info, @@ -305,7 +305,7 @@ public: * Update the info with new parameters, version 2. * * @param error_info - Used to write call result code. - * @param in_params - New parameters to update the info with. + * @param params - New parameters to update the info with. * @param pool_mapper - Pool for mapping buffers. */ virtual void Update(BehaviorInfo::ErrorInfo& error_info, diff --git a/src/audio_core/renderer/effect/i3dl2.h b/src/audio_core/renderer/effect/i3dl2.h index 7a088a627..1ebbc5c4c 100644 --- a/src/audio_core/renderer/effect/i3dl2.h +++ b/src/audio_core/renderer/effect/i3dl2.h @@ -99,7 +99,7 @@ public: return out_sample; } - Common::FixedPoint<50, 14> Read() { + Common::FixedPoint<50, 14> Read() const { return *output; } @@ -110,7 +110,7 @@ public: } } - Common::FixedPoint<50, 14> TapOut(const s32 index) { + Common::FixedPoint<50, 14> TapOut(const s32 index) const { auto out{input - (index + 1)}; if (out < buffer.data()) { out += max_delay + 1; diff --git a/src/audio_core/renderer/effect/reverb.h b/src/audio_core/renderer/effect/reverb.h index b4df9f6ef..a72475c3c 100644 --- a/src/audio_core/renderer/effect/reverb.h +++ b/src/audio_core/renderer/effect/reverb.h @@ -95,7 +95,7 @@ public: return out_sample; } - Common::FixedPoint<50, 14> Read() { + Common::FixedPoint<50, 14> Read() const { return *output; } @@ -106,7 +106,7 @@ public: } } - Common::FixedPoint<50, 14> TapOut(const s32 index) { + Common::FixedPoint<50, 14> TapOut(const s32 index) const { auto out{input - (index + 1)}; if (out < buffer.data()) { out += sample_count; diff --git a/src/audio_core/renderer/memory/address_info.h b/src/audio_core/renderer/memory/address_info.h index 4cfefea8e..bb5c930e1 100644 --- a/src/audio_core/renderer/memory/address_info.h +++ b/src/audio_core/renderer/memory/address_info.h @@ -19,8 +19,8 @@ public: /** * Setup a new AddressInfo. * - * @param cpu_address - The CPU address of this region. - * @param size - The size of this region. + * @param cpu_address_ - The CPU address of this region. + * @param size_ - The size of this region. */ void Setup(CpuAddr cpu_address_, u64 size_) { cpu_address = cpu_address_; @@ -42,7 +42,6 @@ public: * Assign this region to a memory pool. * * @param memory_pool_ - Memory pool to assign. - * @return The CpuAddr address of this region. */ void SetPool(MemoryPoolInfo* memory_pool_) { memory_pool = memory_pool_; diff --git a/src/audio_core/renderer/nodes/node_states.h b/src/audio_core/renderer/nodes/node_states.h index a1e0958a2..94b1d1254 100644 --- a/src/audio_core/renderer/nodes/node_states.h +++ b/src/audio_core/renderer/nodes/node_states.h @@ -56,7 +56,7 @@ class NodeStates { * * @return The current stack position. */ - u32 Count() { + u32 Count() const { return pos; } @@ -83,7 +83,7 @@ class NodeStates { * * @return The node on the top of the stack. */ - u32 top() { + u32 top() const { return stack[pos - 1]; } @@ -112,11 +112,11 @@ public: /** * Initialize the node states. * - * @param buffer - The workbuffer to use. Unused. + * @param buffer_ - The workbuffer to use. Unused. * @param node_buffer_size - The size of the workbuffer. Unused. * @param count - The number of nodes in the graph. */ - void Initialize(std::span<u8> nodes, u64 node_buffer_size, u32 count); + void Initialize(std::span<u8> buffer_, u64 node_buffer_size, u32 count); /** * Sort the graph. Only calls DepthFirstSearch. diff --git a/src/audio_core/renderer/performance/performance_manager.h b/src/audio_core/renderer/performance/performance_manager.h index b82176bef..b65caa9b6 100644 --- a/src/audio_core/renderer/performance/performance_manager.h +++ b/src/audio_core/renderer/performance/performance_manager.h @@ -73,7 +73,8 @@ public: * Calculate the required size for the performance workbuffer. * * @param behavior - Check which version is supported. - * @param params - Input parameters. + * @param params - Input parameters. + * * @return Required workbuffer size. */ static u64 GetRequiredBufferSizeForPerformanceMetricsPerFrame( @@ -104,7 +105,7 @@ public: * @param workbuffer - Workbuffer to use for performance frames. * @param workbuffer_size - Size of the workbuffer. * @param params - Input parameters. - * @param behavior - Behaviour to check version and data format. + * @param behavior - Behaviour to check version and data format. * @param memory_pool - Used to translate the workbuffer address for the DSP. */ virtual void Initialize(std::span<u8> workbuffer, u64 workbuffer_size, @@ -160,7 +161,8 @@ public: * workbuffer, to be written by the AudioRenderer. * * @param addresses - Filled with pointers to the new detail, which should be passed - * to the AudioRenderer with Performance commands to be written. + * to the AudioRenderer with Performance commands to be written. + * @param detail_type - Performance detail type. * @param entry_type - The type of this detail. See PerformanceEntryType * @param node_id - Node id for this detail. * @return True if a new detail was created and the offsets are valid, otherwise false. diff --git a/src/audio_core/renderer/system_manager.cpp b/src/audio_core/renderer/system_manager.cpp index b326819ed..9c1331e19 100644 --- a/src/audio_core/renderer/system_manager.cpp +++ b/src/audio_core/renderer/system_manager.cpp @@ -15,17 +15,14 @@ MICROPROFILE_DEFINE(Audio_RenderSystemManager, "Audio", "Render System Manager", MP_RGB(60, 19, 97)); namespace AudioCore::AudioRenderer { -constexpr std::chrono::nanoseconds BaseRenderTime{5'000'000UL}; -constexpr std::chrono::nanoseconds RenderTimeOffset{400'000UL}; +constexpr std::chrono::nanoseconds RENDER_TIME{5'000'000UL}; SystemManager::SystemManager(Core::System& core_) : core{core_}, adsp{core.AudioCore().GetADSP()}, mailbox{adsp.GetRenderMailbox()}, thread_event{Core::Timing::CreateEvent( "AudioRendererSystemManager", [this](std::uintptr_t, s64 time, std::chrono::nanoseconds) { return ThreadFunc2(time); - })} { - core.CoreTiming().RegisterPauseCallback([this](bool paused) { PauseCallback(paused); }); -} + })} {} SystemManager::~SystemManager() { Stop(); @@ -36,8 +33,8 @@ bool SystemManager::InitializeUnsafe() { if (adsp.Start()) { active = true; thread = std::jthread([this](std::stop_token stop_token) { ThreadFunc(); }); - core.CoreTiming().ScheduleLoopingEvent(std::chrono::nanoseconds(0), - BaseRenderTime - RenderTimeOffset, thread_event); + core.CoreTiming().ScheduleLoopingEvent(std::chrono::nanoseconds(0), RENDER_TIME, + thread_event); } } @@ -121,42 +118,9 @@ void SystemManager::ThreadFunc() { } std::optional<std::chrono::nanoseconds> SystemManager::ThreadFunc2(s64 time) { - std::optional<std::chrono::nanoseconds> new_schedule_time{std::nullopt}; - const auto queue_size{core.AudioCore().GetStreamQueue()}; - switch (state) { - case StreamState::Filling: - if (queue_size >= 5) { - new_schedule_time = BaseRenderTime; - state = StreamState::Steady; - } - break; - case StreamState::Steady: - if (queue_size <= 2) { - new_schedule_time = BaseRenderTime - RenderTimeOffset; - state = StreamState::Filling; - } else if (queue_size > 5) { - new_schedule_time = BaseRenderTime + RenderTimeOffset; - state = StreamState::Draining; - } - break; - case StreamState::Draining: - if (queue_size <= 5) { - new_schedule_time = BaseRenderTime; - state = StreamState::Steady; - } - break; - } - update.store(true); update.notify_all(); - return new_schedule_time; -} - -void SystemManager::PauseCallback(bool paused) { - if (paused && core.IsPoweredOn() && core.IsShuttingDown()) { - update.store(true); - update.notify_all(); - } + return std::nullopt; } } // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/system_manager.h b/src/audio_core/renderer/system_manager.h index 1291e9e0e..81457a3a1 100644 --- a/src/audio_core/renderer/system_manager.h +++ b/src/audio_core/renderer/system_manager.h @@ -73,13 +73,6 @@ private: */ std::optional<std::chrono::nanoseconds> ThreadFunc2(s64 time); - /** - * Callback from core timing when pausing, used to detect shutdowns and stop ThreadFunc. - * - * @param paused - Are we pausing or resuming? - */ - void PauseCallback(bool paused); - enum class StreamState { Filling, Steady, @@ -106,8 +99,6 @@ private: std::shared_ptr<Core::Timing::EventType> thread_event; /// Atomic for main thread to wait on std::atomic<bool> update{}; - /// Current state of the streams - StreamState state{StreamState::Filling}; }; } // namespace AudioCore::AudioRenderer diff --git a/src/audio_core/renderer/upsampler/upsampler_manager.h b/src/audio_core/renderer/upsampler/upsampler_manager.h index 70cd42b08..83c697c0c 100644 --- a/src/audio_core/renderer/upsampler/upsampler_manager.h +++ b/src/audio_core/renderer/upsampler/upsampler_manager.h @@ -27,7 +27,7 @@ public: /** * Free the given upsampler. * - * @param The upsampler to be freed. + * @param info The upsampler to be freed. */ void Free(UpsamplerInfo* info); diff --git a/src/audio_core/renderer/voice/voice_info.h b/src/audio_core/renderer/voice/voice_info.h index 896723e0c..930180895 100644 --- a/src/audio_core/renderer/voice/voice_info.h +++ b/src/audio_core/renderer/voice/voice_info.h @@ -185,7 +185,8 @@ public: /** * Does this voice ned an update? * - * @param params - Input parametetrs to check matching. + * @param params - Input parameters to check matching. + * * @return True if this voice needs an update, otherwise false. */ bool ShouldUpdateParameters(const InParameter& params) const; @@ -194,9 +195,9 @@ public: * Update the parameters of this voice. * * @param error_info - Output error code. - * @param params - Input parametters to udpate from. + * @param params - Input parameters to update from. * @param pool_mapper - Used to map buffers. - * @param behavior - behavior to check supported features. + * @param behavior - behavior to check supported features. */ void UpdateParameters(BehaviorInfo::ErrorInfo& error_info, const InParameter& params, const PoolMapper& pool_mapper, const BehaviorInfo& behavior); @@ -218,12 +219,12 @@ public: /** * Update all wavebuffers. * - * @param error_infos - Output 2D array of errors, 2 per wavebuffer. - * @param error_count - Number of errors provided. Unused. - * @param params - Input parametters to be used for the update. + * @param error_infos - Output 2D array of errors, 2 per wavebuffer. + * @param error_count - Number of errors provided. Unused. + * @param params - Input parameters to be used for the update. * @param voice_states - The voice states for each channel in this voice to be updated. - * @param pool_mapper - Used to map the wavebuffers. - * @param behavior - Used to check for supported features. + * @param pool_mapper - Used to map the wavebuffers. + * @param behavior - Used to check for supported features. */ void UpdateWaveBuffers(std::span<std::array<BehaviorInfo::ErrorInfo, 2>> error_infos, u32 error_count, const InParameter& params, @@ -233,13 +234,13 @@ public: /** * Update a wavebuffer. * - * @param error_infos - Output array of errors. + * @param error_info - Output array of errors. * @param wave_buffer - The wavebuffer to be updated. * @param wave_buffer_internal - Input parametters to be used for the update. * @param sample_format - Sample format of the wavebuffer. * @param valid - Is this wavebuffer valid? * @param pool_mapper - Used to map the wavebuffers. - * @param behavior - Used to check for supported features. + * @param behavior - Used to check for supported features. */ void UpdateWaveBuffer(std::span<BehaviorInfo::ErrorInfo> error_info, WaveBuffer& wave_buffer, const WaveBufferInternal& wave_buffer_internal, @@ -276,7 +277,7 @@ public: /** * Check if this voice has any mixing connections. * - * @return True if this voice participes in mixing, otherwise false. + * @return True if this voice participates in mixing, otherwise false. */ bool HasAnyConnection() const; @@ -301,7 +302,8 @@ public: /** * Update this voice on command generation. * - * @param voice_states - Voice states for these wavebuffers. + * @param voice_context - Voice context for these wavebuffers. + * * @return True if this voice should be generated, otherwise false. */ bool UpdateForCommandGeneration(VoiceContext& voice_context); diff --git a/src/audio_core/sink/cubeb_sink.cpp b/src/audio_core/sink/cubeb_sink.cpp index 90d049e8e..36b115ad6 100644 --- a/src/audio_core/sink/cubeb_sink.cpp +++ b/src/audio_core/sink/cubeb_sink.cpp @@ -1,21 +1,13 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include <algorithm> -#include <atomic> #include <span> +#include <vector> -#include "audio_core/audio_core.h" -#include "audio_core/audio_event.h" -#include "audio_core/audio_manager.h" +#include "audio_core/common/common.h" #include "audio_core/sink/cubeb_sink.h" #include "audio_core/sink/sink_stream.h" -#include "common/assert.h" -#include "common/fixed_point.h" #include "common/logging/log.h" -#include "common/reader_writer_queue.h" -#include "common/ring_buffer.h" -#include "common/settings.h" #include "core/core.h" #ifdef _WIN32 @@ -42,10 +34,10 @@ public: * @param system_ - Core system. * @param event - Event used only for audio renderer, signalled on buffer consume. */ - CubebSinkStream(cubeb* ctx_, const u32 device_channels_, const u32 system_channels_, + CubebSinkStream(cubeb* ctx_, u32 device_channels_, u32 system_channels_, cubeb_devid output_device, cubeb_devid input_device, const std::string& name_, - const StreamType type_, Core::System& system_) - : ctx{ctx_}, type{type_}, system{system_} { + StreamType type_, Core::System& system_) + : SinkStream(system_, type_), ctx{ctx_} { #ifdef _WIN32 CoInitializeEx(nullptr, COINIT_MULTITHREADED); #endif @@ -79,12 +71,10 @@ public: minimum_latency = std::max(minimum_latency, 256u); - playing_buffer.consumed = true; - - LOG_DEBUG(Service_Audio, - "Opening cubeb stream {} type {} with: rate {} channels {} (system channels {}) " - "latency {}", - name, type, params.rate, params.channels, system_channels, minimum_latency); + LOG_INFO(Service_Audio, + "Opening cubeb stream {} type {} with: rate {} channels {} (system channels {}) " + "latency {}", + name, type, params.rate, params.channels, system_channels, minimum_latency); auto init_error{0}; if (type == StreamType::In) { @@ -111,6 +101,8 @@ public: ~CubebSinkStream() override { LOG_DEBUG(Service_Audio, "Destructing cubeb stream {}", name); + Unstall(); + if (!ctx) { return; } @@ -136,21 +128,14 @@ public: * @param resume - Set to true if this is resuming the stream a previously-active stream. * Default false. */ - void Start(const bool resume = false) override { - if (!ctx) { + void Start(bool resume = false) override { + if (!ctx || !paused) { return; } - if (resume && was_playing) { - if (cubeb_stream_start(stream_backend) != CUBEB_OK) { - LOG_CRITICAL(Audio_Sink, "Error starting cubeb stream"); - } - paused = false; - } else if (!resume) { - if (cubeb_stream_start(stream_backend) != CUBEB_OK) { - LOG_CRITICAL(Audio_Sink, "Error starting cubeb stream"); - } - paused = false; + paused = false; + if (cubeb_stream_start(stream_backend) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "Error starting cubeb stream"); } } @@ -158,207 +143,20 @@ public: * Stop the sink stream. */ void Stop() override { - if (!ctx) { + Unstall(); + + if (!ctx || paused) { return; } + paused = true; if (cubeb_stream_stop(stream_backend) != CUBEB_OK) { LOG_CRITICAL(Audio_Sink, "Error stopping cubeb stream"); } - - was_playing.store(!paused); - paused = true; - } - - /** - * Append a new buffer and its samples to a waiting queue to play. - * - * @param buffer - Audio buffer information to be queued. - * @param samples - The s16 samples to be queue for playback. - */ - void AppendBuffer(::AudioCore::Sink::SinkBuffer& buffer, std::vector<s16>& samples) override { - if (type == StreamType::In) { - queue.enqueue(buffer); - queued_buffers++; - } else { - constexpr s32 min{std::numeric_limits<s16>::min()}; - constexpr s32 max{std::numeric_limits<s16>::max()}; - - auto yuzu_volume{Settings::Volume()}; - if (yuzu_volume > 1.0f) { - yuzu_volume = 0.6f + 20 * std::log10(yuzu_volume); - } - auto volume{system_volume * device_volume * yuzu_volume}; - - if (system_channels == 6 && device_channels == 2) { - // We're given 6 channels, but our device only outputs 2, so downmix. - constexpr std::array<f32, 4> down_mix_coeff{1.0f, 0.707f, 0.251f, 0.707f}; - - for (u32 read_index = 0, write_index = 0; read_index < samples.size(); - read_index += system_channels, write_index += device_channels) { - const auto left_sample{ - ((Common::FixedPoint<49, 15>( - samples[read_index + static_cast<u32>(Channels::FrontLeft)]) * - down_mix_coeff[0] + - samples[read_index + static_cast<u32>(Channels::Center)] * - down_mix_coeff[1] + - samples[read_index + static_cast<u32>(Channels::LFE)] * - down_mix_coeff[2] + - samples[read_index + static_cast<u32>(Channels::BackLeft)] * - down_mix_coeff[3]) * - volume) - .to_int()}; - - const auto right_sample{ - ((Common::FixedPoint<49, 15>( - samples[read_index + static_cast<u32>(Channels::FrontRight)]) * - down_mix_coeff[0] + - samples[read_index + static_cast<u32>(Channels::Center)] * - down_mix_coeff[1] + - samples[read_index + static_cast<u32>(Channels::LFE)] * - down_mix_coeff[2] + - samples[read_index + static_cast<u32>(Channels::BackRight)] * - down_mix_coeff[3]) * - volume) - .to_int()}; - - samples[write_index + static_cast<u32>(Channels::FrontLeft)] = - static_cast<s16>(std::clamp(left_sample, min, max)); - samples[write_index + static_cast<u32>(Channels::FrontRight)] = - static_cast<s16>(std::clamp(right_sample, min, max)); - } - - samples.resize(samples.size() / system_channels * device_channels); - - } else if (system_channels == 2 && device_channels == 6) { - // We need moar samples! Not all games will provide 6 channel audio. - // TODO: Implement some upmixing here. Currently just passthrough, with other - // channels left as silence. - std::vector<s16> new_samples(samples.size() / system_channels * device_channels, 0); - - for (u32 read_index = 0, write_index = 0; read_index < samples.size(); - read_index += system_channels, write_index += device_channels) { - const auto left_sample{static_cast<s16>(std::clamp( - static_cast<s32>( - static_cast<f32>( - samples[read_index + static_cast<u32>(Channels::FrontLeft)]) * - volume), - min, max))}; - - new_samples[write_index + static_cast<u32>(Channels::FrontLeft)] = left_sample; - - const auto right_sample{static_cast<s16>(std::clamp( - static_cast<s32>( - static_cast<f32>( - samples[read_index + static_cast<u32>(Channels::FrontRight)]) * - volume), - min, max))}; - - new_samples[write_index + static_cast<u32>(Channels::FrontRight)] = - right_sample; - } - samples = std::move(new_samples); - - } else if (volume != 1.0f) { - for (u32 i = 0; i < samples.size(); i++) { - samples[i] = static_cast<s16>(std::clamp( - static_cast<s32>(static_cast<f32>(samples[i]) * volume), min, max)); - } - } - - samples_buffer.Push(samples); - queue.enqueue(buffer); - queued_buffers++; - } - } - - /** - * Release a buffer. Audio In only, will fill a buffer with recorded samples. - * - * @param num_samples - Maximum number of samples to receive. - * @return Vector of recorded samples. May have fewer than num_samples. - */ - std::vector<s16> ReleaseBuffer(const u64 num_samples) override { - static constexpr s32 min = std::numeric_limits<s16>::min(); - static constexpr s32 max = std::numeric_limits<s16>::max(); - - auto samples{samples_buffer.Pop(num_samples)}; - - // TODO: Up-mix to 6 channels if the game expects it. - // For audio input this is unlikely to ever be the case though. - - // Incoming mic volume seems to always be very quiet, so multiply by an additional 8 here. - // TODO: Play with this and find something that works better. - auto volume{system_volume * device_volume * 8}; - for (u32 i = 0; i < samples.size(); i++) { - samples[i] = static_cast<s16>( - std::clamp(static_cast<s32>(static_cast<f32>(samples[i]) * volume), min, max)); - } - - if (samples.size() < num_samples) { - samples.resize(num_samples, 0); - } - return samples; - } - - /** - * Check if a certain buffer has been consumed (fully played). - * - * @param tag - Unique tag of a buffer to check for. - * @return True if the buffer has been played, otherwise false. - */ - bool IsBufferConsumed(const u64 tag) override { - if (released_buffer.tag == 0) { - if (!released_buffers.try_dequeue(released_buffer)) { - return false; - } - } - - if (released_buffer.tag == tag) { - released_buffer.tag = 0; - return true; - } - return false; - } - - /** - * Empty out the buffer queue. - */ - void ClearQueue() override { - samples_buffer.Pop(); - while (queue.pop()) { - } - while (released_buffers.pop()) { - } - queued_buffers = 0; - released_buffer = {}; - playing_buffer = {}; - playing_buffer.consumed = true; } private: /** - * Signal events back to the audio system that a buffer was played/can be filled. - * - * @param buffer - Consumed audio buffer to be released. - */ - void SignalEvent(const ::AudioCore::Sink::SinkBuffer& buffer) { - auto& manager{system.AudioCore().GetAudioManager()}; - switch (type) { - case StreamType::Out: - released_buffers.enqueue(buffer); - manager.SetEvent(Event::Type::AudioOutManager, true); - break; - case StreamType::In: - released_buffers.enqueue(buffer); - manager.SetEvent(Event::Type::AudioInManager, true); - break; - case StreamType::Render: - break; - } - } - - /** * Main callback from Cubeb. Either expects samples from us (audio render/audio out), or will * provide samples to be copied (audio in). * @@ -378,106 +176,15 @@ private: const std::size_t num_channels = impl->GetDeviceChannels(); const std::size_t frame_size = num_channels; - const std::size_t frame_size_bytes = frame_size * sizeof(s16); const std::size_t num_frames{static_cast<size_t>(num_frames_)}; - size_t frames_written{0}; - [[maybe_unused]] bool underrun{false}; if (impl->type == StreamType::In) { - // INPUT std::span<const s16> input_buffer{reinterpret_cast<const s16*>(in_buff), num_frames * frame_size}; - - while (frames_written < num_frames) { - auto& playing_buffer{impl->playing_buffer}; - - // If the playing buffer has been consumed or has no frames, we need a new one - if (playing_buffer.consumed || playing_buffer.frames == 0) { - if (!impl->queue.try_dequeue(impl->playing_buffer)) { - // If no buffer was available we've underrun, just push the samples and - // continue. - underrun = true; - impl->samples_buffer.Push(&input_buffer[frames_written * frame_size], - (num_frames - frames_written) * frame_size); - frames_written = num_frames; - continue; - } else { - // Successfully got a new buffer, mark the old one as consumed and signal. - impl->queued_buffers--; - impl->SignalEvent(impl->playing_buffer); - } - } - - // Get the minimum frames available between the currently playing buffer, and the - // amount we have left to fill - size_t frames_available{ - std::min(playing_buffer.frames - playing_buffer.frames_played, - num_frames - frames_written)}; - - impl->samples_buffer.Push(&input_buffer[frames_written * frame_size], - frames_available * frame_size); - - frames_written += frames_available; - playing_buffer.frames_played += frames_available; - - // If that's all the frames in the current buffer, add its samples and mark it as - // consumed - if (playing_buffer.frames_played >= playing_buffer.frames) { - impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels); - impl->playing_buffer.consumed = true; - } - } - - std::memcpy(&impl->last_frame[0], &input_buffer[(frames_written - 1) * frame_size], - frame_size_bytes); + impl->ProcessAudioIn(input_buffer, num_frames); } else { - // OUTPUT std::span<s16> output_buffer{reinterpret_cast<s16*>(out_buff), num_frames * frame_size}; - - while (frames_written < num_frames) { - auto& playing_buffer{impl->playing_buffer}; - - // If the playing buffer has been consumed or has no frames, we need a new one - if (playing_buffer.consumed || playing_buffer.frames == 0) { - if (!impl->queue.try_dequeue(impl->playing_buffer)) { - // If no buffer was available we've underrun, fill the remaining buffer with - // the last written frame and continue. - underrun = true; - for (size_t i = frames_written; i < num_frames; i++) { - std::memcpy(&output_buffer[i * frame_size], &impl->last_frame[0], - frame_size_bytes); - } - frames_written = num_frames; - continue; - } else { - // Successfully got a new buffer, mark the old one as consumed and signal. - impl->queued_buffers--; - impl->SignalEvent(impl->playing_buffer); - } - } - - // Get the minimum frames available between the currently playing buffer, and the - // amount we have left to fill - size_t frames_available{ - std::min(playing_buffer.frames - playing_buffer.frames_played, - num_frames - frames_written)}; - - impl->samples_buffer.Pop(&output_buffer[frames_written * frame_size], - frames_available * frame_size); - - frames_written += frames_available; - playing_buffer.frames_played += frames_available; - - // If that's all the frames in the current buffer, add its samples and mark it as - // consumed - if (playing_buffer.frames_played >= playing_buffer.frames) { - impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels); - impl->playing_buffer.consumed = true; - } - } - - std::memcpy(&impl->last_frame[0], &output_buffer[(frames_written - 1) * frame_size], - frame_size_bytes); + impl->ProcessAudioOutAndRender(output_buffer, num_frames); } return num_frames_; @@ -490,32 +197,12 @@ private: * @param user_data - Custom data pointer passed along, points to a CubebSinkStream. * @param state - New state of the device. */ - static void StateCallback([[maybe_unused]] cubeb_stream* stream, - [[maybe_unused]] void* user_data, - [[maybe_unused]] cubeb_state state) {} + static void StateCallback(cubeb_stream*, void*, cubeb_state) {} /// Main Cubeb context cubeb* ctx{}; /// Cubeb stream backend cubeb_stream* stream_backend{}; - /// Name of this stream - std::string name{}; - /// Type of this stream - StreamType type; - /// Core system - Core::System& system; - /// Ring buffer of the samples waiting to be played or consumed - Common::RingBuffer<s16, 0x10000> samples_buffer; - /// Audio buffers queued and waiting to play - Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> queue; - /// The currently-playing audio buffer - ::AudioCore::Sink::SinkBuffer playing_buffer{}; - /// Audio buffers which have been played and are in queue to be released by the audio system - Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> released_buffers{}; - /// Currently released buffer waiting to be taken by the audio system - ::AudioCore::Sink::SinkBuffer released_buffer{}; - /// The last played (or received) frame of audio, used when the callback underruns - std::array<s16, MaxChannels> last_frame{}; }; CubebSink::CubebSink(std::string_view target_device_name) { @@ -569,15 +256,15 @@ CubebSink::~CubebSink() { #endif } -SinkStream* CubebSink::AcquireSinkStream(Core::System& system, const u32 system_channels, - const std::string& name, const StreamType type) { +SinkStream* CubebSink::AcquireSinkStream(Core::System& system, u32 system_channels, + const std::string& name, StreamType type) { SinkStreamPtr& stream = sink_streams.emplace_back(std::make_unique<CubebSinkStream>( ctx, device_channels, system_channels, output_device, input_device, name, type, system)); return stream.get(); } -void CubebSink::CloseStream(const SinkStream* stream) { +void CubebSink::CloseStream(SinkStream* stream) { for (size_t i = 0; i < sink_streams.size(); i++) { if (sink_streams[i].get() == stream) { sink_streams[i].reset(); @@ -591,18 +278,6 @@ void CubebSink::CloseStreams() { sink_streams.clear(); } -void CubebSink::PauseStreams() { - for (auto& stream : sink_streams) { - stream->Stop(); - } -} - -void CubebSink::UnpauseStreams() { - for (auto& stream : sink_streams) { - stream->Start(true); - } -} - f32 CubebSink::GetDeviceVolume() const { if (sink_streams.empty()) { return 1.0f; @@ -611,19 +286,19 @@ f32 CubebSink::GetDeviceVolume() const { return sink_streams[0]->GetDeviceVolume(); } -void CubebSink::SetDeviceVolume(const f32 volume) { +void CubebSink::SetDeviceVolume(f32 volume) { for (auto& stream : sink_streams) { stream->SetDeviceVolume(volume); } } -void CubebSink::SetSystemVolume(const f32 volume) { +void CubebSink::SetSystemVolume(f32 volume) { for (auto& stream : sink_streams) { stream->SetSystemVolume(volume); } } -std::vector<std::string> ListCubebSinkDevices(const bool capture) { +std::vector<std::string> ListCubebSinkDevices(bool capture) { std::vector<std::string> device_list; cubeb* ctx; diff --git a/src/audio_core/sink/cubeb_sink.h b/src/audio_core/sink/cubeb_sink.h index f0f43dfa1..4b0cb160d 100644 --- a/src/audio_core/sink/cubeb_sink.h +++ b/src/audio_core/sink/cubeb_sink.h @@ -34,8 +34,7 @@ public: * May differ from the device's channel count. * @param name - Name of this stream. * @param type - Type of this stream, render/in/out. - * @param event - Audio render only, a signal used to prevent the renderer running too - * fast. + * * @return A pointer to the created SinkStream */ SinkStream* AcquireSinkStream(Core::System& system, u32 system_channels, @@ -46,7 +45,7 @@ public: * * @param stream - The stream to close. */ - void CloseStream(const SinkStream* stream) override; + void CloseStream(SinkStream* stream) override; /** * Close all streams. @@ -54,16 +53,6 @@ public: void CloseStreams() override; /** - * Pause all streams. - */ - void PauseStreams() override; - - /** - * Unpause all streams. - */ - void UnpauseStreams() override; - - /** * Get the device volume. Set from calls to the IAudioDevice service. * * @return Volume of the device. @@ -101,7 +90,7 @@ private: }; /** - * Get a list of conencted devices from Cubeb. + * Get a list of connected devices from Cubeb. * * @param capture - Return input (capture) devices if true, otherwise output devices. */ diff --git a/src/audio_core/sink/null_sink.h b/src/audio_core/sink/null_sink.h index 47a342171..1215d3cd2 100644 --- a/src/audio_core/sink/null_sink.h +++ b/src/audio_core/sink/null_sink.h @@ -3,10 +3,29 @@ #pragma once +#include <string> +#include <string_view> +#include <vector> + #include "audio_core/sink/sink.h" #include "audio_core/sink/sink_stream.h" +namespace Core { +class System; +} // namespace Core + namespace AudioCore::Sink { +class NullSinkStreamImpl final : public SinkStream { +public: + explicit NullSinkStreamImpl(Core::System& system_, StreamType type_) + : SinkStream{system_, type_} {} + ~NullSinkStreamImpl() override {} + void AppendBuffer(SinkBuffer&, std::vector<s16>&) override {} + std::vector<s16> ReleaseBuffer(u64) override { + return {}; + } +}; + /** * A no-op sink for when no audio out is wanted. */ @@ -15,17 +34,16 @@ public: explicit NullSink(std::string_view) {} ~NullSink() override = default; - SinkStream* AcquireSinkStream([[maybe_unused]] Core::System& system, - [[maybe_unused]] u32 system_channels, - [[maybe_unused]] const std::string& name, - [[maybe_unused]] StreamType type) override { - return &null_sink_stream; + SinkStream* AcquireSinkStream(Core::System& system, u32, const std::string&, + StreamType type) override { + if (null_sink == nullptr) { + null_sink = std::make_unique<NullSinkStreamImpl>(system, type); + } + return null_sink.get(); } - void CloseStream([[maybe_unused]] const SinkStream* stream) override {} + void CloseStream(SinkStream*) override {} void CloseStreams() override {} - void PauseStreams() override {} - void UnpauseStreams() override {} f32 GetDeviceVolume() const override { return 1.0f; } @@ -33,20 +51,7 @@ public: void SetSystemVolume(f32 volume) override {} private: - struct NullSinkStreamImpl final : SinkStream { - void Finalize() override {} - void Start(bool resume = false) override {} - void Stop() override {} - void AppendBuffer([[maybe_unused]] ::AudioCore::Sink::SinkBuffer& buffer, - [[maybe_unused]] std::vector<s16>& samples) override {} - std::vector<s16> ReleaseBuffer([[maybe_unused]] u64 num_samples) override { - return {}; - } - bool IsBufferConsumed([[maybe_unused]] const u64 tag) { - return true; - } - void ClearQueue() override {} - } null_sink_stream; + SinkStreamPtr null_sink{}; }; } // namespace AudioCore::Sink diff --git a/src/audio_core/sink/sdl2_sink.cpp b/src/audio_core/sink/sdl2_sink.cpp index d6c9ec90d..1bd001b94 100644 --- a/src/audio_core/sink/sdl2_sink.cpp +++ b/src/audio_core/sink/sdl2_sink.cpp @@ -1,20 +1,13 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include <algorithm> -#include <atomic> +#include <span> +#include <vector> -#include "audio_core/audio_core.h" -#include "audio_core/audio_event.h" -#include "audio_core/audio_manager.h" +#include "audio_core/common/common.h" #include "audio_core/sink/sdl2_sink.h" #include "audio_core/sink/sink_stream.h" -#include "common/assert.h" -#include "common/fixed_point.h" #include "common/logging/log.h" -#include "common/reader_writer_queue.h" -#include "common/ring_buffer.h" -#include "common/settings.h" #include "core/core.h" // Ignore -Wimplicit-fallthrough due to https://github.com/libsdl-org/SDL/issues/4307 @@ -44,10 +37,9 @@ public: * @param system_ - Core system. * @param event - Event used only for audio renderer, signalled on buffer consume. */ - SDLSinkStream(u32 device_channels_, const u32 system_channels_, - const std::string& output_device, const std::string& input_device, - const StreamType type_, Core::System& system_) - : type{type_}, system{system_} { + SDLSinkStream(u32 device_channels_, u32 system_channels_, const std::string& output_device, + const std::string& input_device, StreamType type_, Core::System& system_) + : SinkStream{system_, type_} { system_channels = system_channels_; device_channels = device_channels_; @@ -63,8 +55,6 @@ public: spec.callback = &SDLSinkStream::DataCallback; spec.userdata = this; - playing_buffer.consumed = true; - std::string device_name{output_device}; bool capture{false}; if (type == StreamType::In) { @@ -84,31 +74,30 @@ public: return; } - LOG_DEBUG(Service_Audio, - "Opening sdl stream {} with: rate {} channels {} (system channels {}) " - " samples {}", - device, obtained.freq, obtained.channels, system_channels, obtained.samples); + LOG_INFO(Service_Audio, + "Opening SDL stream {} with: rate {} channels {} (system channels {}) " + " samples {}", + device, obtained.freq, obtained.channels, system_channels, obtained.samples); } /** * Destroy the sink stream. */ ~SDLSinkStream() override { - if (device == 0) { - return; - } - - SDL_CloseAudioDevice(device); + LOG_DEBUG(Service_Audio, "Destructing SDL stream {}", name); + Finalize(); } /** * Finalize the sink stream. */ void Finalize() override { + Unstall(); if (device == 0) { return; } + Stop(); SDL_CloseAudioDevice(device); } @@ -118,217 +107,29 @@ public: * @param resume - Set to true if this is resuming the stream a previously-active stream. * Default false. */ - void Start(const bool resume = false) override { - if (device == 0) { + void Start(bool resume = false) override { + if (device == 0 || !paused) { return; } - if (resume && was_playing) { - SDL_PauseAudioDevice(device, 0); - paused = false; - } else if (!resume) { - SDL_PauseAudioDevice(device, 0); - paused = false; - } + paused = false; + SDL_PauseAudioDevice(device, 0); } /** * Stop the sink stream. */ - void Stop() { - if (device == 0) { + void Stop() override { + Unstall(); + if (device == 0 || paused) { return; } - SDL_PauseAudioDevice(device, 1); paused = true; - } - - /** - * Append a new buffer and its samples to a waiting queue to play. - * - * @param buffer - Audio buffer information to be queued. - * @param samples - The s16 samples to be queue for playback. - */ - void AppendBuffer(::AudioCore::Sink::SinkBuffer& buffer, std::vector<s16>& samples) override { - if (type == StreamType::In) { - queue.enqueue(buffer); - queued_buffers++; - } else { - constexpr s32 min = std::numeric_limits<s16>::min(); - constexpr s32 max = std::numeric_limits<s16>::max(); - - auto yuzu_volume{Settings::Volume()}; - auto volume{system_volume * device_volume * yuzu_volume}; - - if (system_channels == 6 && device_channels == 2) { - // We're given 6 channels, but our device only outputs 2, so downmix. - constexpr std::array<f32, 4> down_mix_coeff{1.0f, 0.707f, 0.251f, 0.707f}; - - for (u32 read_index = 0, write_index = 0; read_index < samples.size(); - read_index += system_channels, write_index += device_channels) { - const auto left_sample{ - ((Common::FixedPoint<49, 15>( - samples[read_index + static_cast<u32>(Channels::FrontLeft)]) * - down_mix_coeff[0] + - samples[read_index + static_cast<u32>(Channels::Center)] * - down_mix_coeff[1] + - samples[read_index + static_cast<u32>(Channels::LFE)] * - down_mix_coeff[2] + - samples[read_index + static_cast<u32>(Channels::BackLeft)] * - down_mix_coeff[3]) * - volume) - .to_int()}; - - const auto right_sample{ - ((Common::FixedPoint<49, 15>( - samples[read_index + static_cast<u32>(Channels::FrontRight)]) * - down_mix_coeff[0] + - samples[read_index + static_cast<u32>(Channels::Center)] * - down_mix_coeff[1] + - samples[read_index + static_cast<u32>(Channels::LFE)] * - down_mix_coeff[2] + - samples[read_index + static_cast<u32>(Channels::BackRight)] * - down_mix_coeff[3]) * - volume) - .to_int()}; - - samples[write_index + static_cast<u32>(Channels::FrontLeft)] = - static_cast<s16>(std::clamp(left_sample, min, max)); - samples[write_index + static_cast<u32>(Channels::FrontRight)] = - static_cast<s16>(std::clamp(right_sample, min, max)); - } - - samples.resize(samples.size() / system_channels * device_channels); - - } else if (system_channels == 2 && device_channels == 6) { - // We need moar samples! Not all games will provide 6 channel audio. - // TODO: Implement some upmixing here. Currently just passthrough, with other - // channels left as silence. - std::vector<s16> new_samples(samples.size() / system_channels * device_channels, 0); - - for (u32 read_index = 0, write_index = 0; read_index < samples.size(); - read_index += system_channels, write_index += device_channels) { - const auto left_sample{static_cast<s16>(std::clamp( - static_cast<s32>( - static_cast<f32>( - samples[read_index + static_cast<u32>(Channels::FrontLeft)]) * - volume), - min, max))}; - - new_samples[write_index + static_cast<u32>(Channels::FrontLeft)] = left_sample; - - const auto right_sample{static_cast<s16>(std::clamp( - static_cast<s32>( - static_cast<f32>( - samples[read_index + static_cast<u32>(Channels::FrontRight)]) * - volume), - min, max))}; - - new_samples[write_index + static_cast<u32>(Channels::FrontRight)] = - right_sample; - } - samples = std::move(new_samples); - - } else if (volume != 1.0f) { - for (u32 i = 0; i < samples.size(); i++) { - samples[i] = static_cast<s16>(std::clamp( - static_cast<s32>(static_cast<f32>(samples[i]) * volume), min, max)); - } - } - - samples_buffer.Push(samples); - queue.enqueue(buffer); - queued_buffers++; - } - } - - /** - * Release a buffer. Audio In only, will fill a buffer with recorded samples. - * - * @param num_samples - Maximum number of samples to receive. - * @return Vector of recorded samples. May have fewer than num_samples. - */ - std::vector<s16> ReleaseBuffer(const u64 num_samples) override { - static constexpr s32 min = std::numeric_limits<s16>::min(); - static constexpr s32 max = std::numeric_limits<s16>::max(); - - auto samples{samples_buffer.Pop(num_samples)}; - - // TODO: Up-mix to 6 channels if the game expects it. - // For audio input this is unlikely to ever be the case though. - - // Incoming mic volume seems to always be very quiet, so multiply by an additional 8 here. - // TODO: Play with this and find something that works better. - auto volume{system_volume * device_volume * 8}; - for (u32 i = 0; i < samples.size(); i++) { - samples[i] = static_cast<s16>( - std::clamp(static_cast<s32>(static_cast<f32>(samples[i]) * volume), min, max)); - } - - if (samples.size() < num_samples) { - samples.resize(num_samples, 0); - } - return samples; - } - - /** - * Check if a certain buffer has been consumed (fully played). - * - * @param tag - Unique tag of a buffer to check for. - * @return True if the buffer has been played, otherwise false. - */ - bool IsBufferConsumed(const u64 tag) override { - if (released_buffer.tag == 0) { - if (!released_buffers.try_dequeue(released_buffer)) { - return false; - } - } - - if (released_buffer.tag == tag) { - released_buffer.tag = 0; - return true; - } - return false; - } - - /** - * Empty out the buffer queue. - */ - void ClearQueue() override { - samples_buffer.Pop(); - while (queue.pop()) { - } - while (released_buffers.pop()) { - } - released_buffer = {}; - playing_buffer = {}; - playing_buffer.consumed = true; - queued_buffers = 0; + SDL_PauseAudioDevice(device, 1); } private: /** - * Signal events back to the audio system that a buffer was played/can be filled. - * - * @param buffer - Consumed audio buffer to be released. - */ - void SignalEvent(const ::AudioCore::Sink::SinkBuffer& buffer) { - auto& manager{system.AudioCore().GetAudioManager()}; - switch (type) { - case StreamType::Out: - released_buffers.enqueue(buffer); - manager.SetEvent(Event::Type::AudioOutManager, true); - break; - case StreamType::In: - released_buffers.enqueue(buffer); - manager.SetEvent(Event::Type::AudioInManager, true); - break; - case StreamType::Render: - break; - } - } - - /** * Main callback from SDL. Either expects samples from us (audio render/audio out), or will * provide samples to be copied (audio in). * @@ -345,122 +146,20 @@ private: const std::size_t num_channels = impl->GetDeviceChannels(); const std::size_t frame_size = num_channels; - const std::size_t frame_size_bytes = frame_size * sizeof(s16); const std::size_t num_frames{len / num_channels / sizeof(s16)}; - size_t frames_written{0}; - [[maybe_unused]] bool underrun{false}; if (impl->type == StreamType::In) { - std::span<s16> input_buffer{reinterpret_cast<s16*>(stream), num_frames * frame_size}; - - while (frames_written < num_frames) { - auto& playing_buffer{impl->playing_buffer}; - - // If the playing buffer has been consumed or has no frames, we need a new one - if (playing_buffer.consumed || playing_buffer.frames == 0) { - if (!impl->queue.try_dequeue(impl->playing_buffer)) { - // If no buffer was available we've underrun, just push the samples and - // continue. - underrun = true; - impl->samples_buffer.Push(&input_buffer[frames_written * frame_size], - (num_frames - frames_written) * frame_size); - frames_written = num_frames; - continue; - } else { - impl->queued_buffers--; - impl->SignalEvent(impl->playing_buffer); - } - } - - // Get the minimum frames available between the currently playing buffer, and the - // amount we have left to fill - size_t frames_available{ - std::min(playing_buffer.frames - playing_buffer.frames_played, - num_frames - frames_written)}; - - impl->samples_buffer.Push(&input_buffer[frames_written * frame_size], - frames_available * frame_size); - - frames_written += frames_available; - playing_buffer.frames_played += frames_available; - - // If that's all the frames in the current buffer, add its samples and mark it as - // consumed - if (playing_buffer.frames_played >= playing_buffer.frames) { - impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels); - impl->playing_buffer.consumed = true; - } - } - - std::memcpy(&impl->last_frame[0], &input_buffer[(frames_written - 1) * frame_size], - frame_size_bytes); + std::span<const s16> input_buffer{reinterpret_cast<const s16*>(stream), + num_frames * frame_size}; + impl->ProcessAudioIn(input_buffer, num_frames); } else { std::span<s16> output_buffer{reinterpret_cast<s16*>(stream), num_frames * frame_size}; - - while (frames_written < num_frames) { - auto& playing_buffer{impl->playing_buffer}; - - // If the playing buffer has been consumed or has no frames, we need a new one - if (playing_buffer.consumed || playing_buffer.frames == 0) { - if (!impl->queue.try_dequeue(impl->playing_buffer)) { - // If no buffer was available we've underrun, fill the remaining buffer with - // the last written frame and continue. - underrun = true; - for (size_t i = frames_written; i < num_frames; i++) { - std::memcpy(&output_buffer[i * frame_size], &impl->last_frame[0], - frame_size_bytes); - } - frames_written = num_frames; - continue; - } else { - impl->queued_buffers--; - impl->SignalEvent(impl->playing_buffer); - } - } - - // Get the minimum frames available between the currently playing buffer, and the - // amount we have left to fill - size_t frames_available{ - std::min(playing_buffer.frames - playing_buffer.frames_played, - num_frames - frames_written)}; - - impl->samples_buffer.Pop(&output_buffer[frames_written * frame_size], - frames_available * frame_size); - - frames_written += frames_available; - playing_buffer.frames_played += frames_available; - - // If that's all the frames in the current buffer, add its samples and mark it as - // consumed - if (playing_buffer.frames_played >= playing_buffer.frames) { - impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels); - impl->playing_buffer.consumed = true; - } - } - - std::memcpy(&impl->last_frame[0], &output_buffer[(frames_written - 1) * frame_size], - frame_size_bytes); + impl->ProcessAudioOutAndRender(output_buffer, num_frames); } } /// SDL device id of the opened input/output device SDL_AudioDeviceID device{}; - /// Type of this stream - StreamType type; - /// Core system - Core::System& system; - /// Ring buffer of the samples waiting to be played or consumed - Common::RingBuffer<s16, 0x10000> samples_buffer; - /// Audio buffers queued and waiting to play - Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> queue; - /// The currently-playing audio buffer - ::AudioCore::Sink::SinkBuffer playing_buffer{}; - /// Audio buffers which have been played and are in queue to be released by the audio system - Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> released_buffers{}; - /// Currently released buffer waiting to be taken by the audio system - ::AudioCore::Sink::SinkBuffer released_buffer{}; - /// The last played (or received) frame of audio, used when the callback underruns - std::array<s16, MaxChannels> last_frame{}; }; SDLSink::SDLSink(std::string_view target_device_name) { @@ -482,14 +181,14 @@ SDLSink::SDLSink(std::string_view target_device_name) { SDLSink::~SDLSink() = default; -SinkStream* SDLSink::AcquireSinkStream(Core::System& system, const u32 system_channels, - const std::string&, const StreamType type) { +SinkStream* SDLSink::AcquireSinkStream(Core::System& system, u32 system_channels, + const std::string&, StreamType type) { SinkStreamPtr& stream = sink_streams.emplace_back(std::make_unique<SDLSinkStream>( device_channels, system_channels, output_device, input_device, type, system)); return stream.get(); } -void SDLSink::CloseStream(const SinkStream* stream) { +void SDLSink::CloseStream(SinkStream* stream) { for (size_t i = 0; i < sink_streams.size(); i++) { if (sink_streams[i].get() == stream) { sink_streams[i].reset(); @@ -503,18 +202,6 @@ void SDLSink::CloseStreams() { sink_streams.clear(); } -void SDLSink::PauseStreams() { - for (auto& stream : sink_streams) { - stream->Stop(); - } -} - -void SDLSink::UnpauseStreams() { - for (auto& stream : sink_streams) { - stream->Start(); - } -} - f32 SDLSink::GetDeviceVolume() const { if (sink_streams.empty()) { return 1.0f; @@ -523,19 +210,19 @@ f32 SDLSink::GetDeviceVolume() const { return sink_streams[0]->GetDeviceVolume(); } -void SDLSink::SetDeviceVolume(const f32 volume) { +void SDLSink::SetDeviceVolume(f32 volume) { for (auto& stream : sink_streams) { stream->SetDeviceVolume(volume); } } -void SDLSink::SetSystemVolume(const f32 volume) { +void SDLSink::SetSystemVolume(f32 volume) { for (auto& stream : sink_streams) { stream->SetSystemVolume(volume); } } -std::vector<std::string> ListSDLSinkDevices(const bool capture) { +std::vector<std::string> ListSDLSinkDevices(bool capture) { std::vector<std::string> device_list; if (!SDL_WasInit(SDL_INIT_AUDIO)) { diff --git a/src/audio_core/sink/sdl2_sink.h b/src/audio_core/sink/sdl2_sink.h index 186bc2fa3..f01eddc1b 100644 --- a/src/audio_core/sink/sdl2_sink.h +++ b/src/audio_core/sink/sdl2_sink.h @@ -32,8 +32,7 @@ public: * May differ from the device's channel count. * @param name - Name of this stream. * @param type - Type of this stream, render/in/out. - * @param event - Audio render only, a signal used to prevent the renderer running too - * fast. + * * @return A pointer to the created SinkStream */ SinkStream* AcquireSinkStream(Core::System& system, u32 system_channels, @@ -44,7 +43,7 @@ public: * * @param stream - The stream to close. */ - void CloseStream(const SinkStream* stream) override; + void CloseStream(SinkStream* stream) override; /** * Close all streams. @@ -52,16 +51,6 @@ public: void CloseStreams() override; /** - * Pause all streams. - */ - void PauseStreams() override; - - /** - * Unpause all streams. - */ - void UnpauseStreams() override; - - /** * Get the device volume. Set from calls to the IAudioDevice service. * * @return Volume of the device. @@ -92,7 +81,7 @@ private: }; /** - * Get a list of conencted devices from Cubeb. + * Get a list of connected devices from SDL. * * @param capture - Return input (capture) devices if true, otherwise output devices. */ diff --git a/src/audio_core/sink/sink.h b/src/audio_core/sink/sink.h index 91fe455e4..f28c6d126 100644 --- a/src/audio_core/sink/sink.h +++ b/src/audio_core/sink/sink.h @@ -32,7 +32,7 @@ public: * * @param stream - The stream to close. */ - virtual void CloseStream(const SinkStream* stream) = 0; + virtual void CloseStream(SinkStream* stream) = 0; /** * Close all streams. @@ -40,16 +40,6 @@ public: virtual void CloseStreams() = 0; /** - * Pause all streams. - */ - virtual void PauseStreams() = 0; - - /** - * Unpause all streams. - */ - virtual void UnpauseStreams() = 0; - - /** * Create a new sink stream, kept within this sink, with a pointer returned for use. * Do not free the returned pointer. When done with the stream, call CloseStream on the sink. * @@ -58,8 +48,7 @@ public: * May differ from the device's channel count. * @param name - Name of this stream. * @param type - Type of this stream, render/in/out. - * @param event - Audio render only, a signal used to prevent the renderer running too - * fast. + * * @return A pointer to the created SinkStream */ virtual SinkStream* AcquireSinkStream(Core::System& system, u32 system_channels, diff --git a/src/audio_core/sink/sink_details.cpp b/src/audio_core/sink/sink_details.cpp index 253c0fd1e..67bdab779 100644 --- a/src/audio_core/sink/sink_details.cpp +++ b/src/audio_core/sink/sink_details.cpp @@ -5,7 +5,7 @@ #include <memory> #include <string> #include <vector> -#include "audio_core/sink/null_sink.h" + #include "audio_core/sink/sink_details.h" #ifdef HAVE_CUBEB #include "audio_core/sink/cubeb_sink.h" @@ -13,6 +13,7 @@ #ifdef HAVE_SDL2 #include "audio_core/sink/sdl2_sink.h" #endif +#include "audio_core/sink/null_sink.h" #include "common/logging/log.h" namespace AudioCore::Sink { @@ -59,8 +60,7 @@ const SinkDetails& GetOutputSinkDetails(std::string_view sink_id) { if (sink_id == "auto" || iter == std::end(sink_details)) { if (sink_id != "auto") { - LOG_ERROR(Audio, "AudioCore::Sink::GetOutputSinkDetails given invalid sink_id {}", - sink_id); + LOG_ERROR(Audio, "Invalid sink_id {}", sink_id); } // Auto-select. // sink_details is ordered in terms of desirability, with the best choice at the front. diff --git a/src/audio_core/sink/sink_stream.cpp b/src/audio_core/sink/sink_stream.cpp new file mode 100644 index 000000000..37fe725e4 --- /dev/null +++ b/src/audio_core/sink/sink_stream.cpp @@ -0,0 +1,279 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <array> +#include <atomic> +#include <memory> +#include <span> +#include <vector> + +#include "audio_core/audio_core.h" +#include "audio_core/common/common.h" +#include "audio_core/sink/sink_stream.h" +#include "common/common_types.h" +#include "common/fixed_point.h" +#include "common/settings.h" +#include "core/core.h" + +namespace AudioCore::Sink { + +void SinkStream::AppendBuffer(SinkBuffer& buffer, std::vector<s16>& samples) { + if (type == StreamType::In) { + queue.enqueue(buffer); + queued_buffers++; + return; + } + + constexpr s32 min{std::numeric_limits<s16>::min()}; + constexpr s32 max{std::numeric_limits<s16>::max()}; + + auto yuzu_volume{Settings::Volume()}; + if (yuzu_volume > 1.0f) { + yuzu_volume = 0.6f + 20 * std::log10(yuzu_volume); + } + auto volume{system_volume * device_volume * yuzu_volume}; + + if (system_channels == 6 && device_channels == 2) { + // We're given 6 channels, but our device only outputs 2, so downmix. + constexpr std::array<f32, 4> down_mix_coeff{1.0f, 0.707f, 0.251f, 0.707f}; + + for (u32 read_index = 0, write_index = 0; read_index < samples.size(); + read_index += system_channels, write_index += device_channels) { + const auto left_sample{ + ((Common::FixedPoint<49, 15>( + samples[read_index + static_cast<u32>(Channels::FrontLeft)]) * + down_mix_coeff[0] + + samples[read_index + static_cast<u32>(Channels::Center)] * down_mix_coeff[1] + + samples[read_index + static_cast<u32>(Channels::LFE)] * down_mix_coeff[2] + + samples[read_index + static_cast<u32>(Channels::BackLeft)] * down_mix_coeff[3]) * + volume) + .to_int()}; + + const auto right_sample{ + ((Common::FixedPoint<49, 15>( + samples[read_index + static_cast<u32>(Channels::FrontRight)]) * + down_mix_coeff[0] + + samples[read_index + static_cast<u32>(Channels::Center)] * down_mix_coeff[1] + + samples[read_index + static_cast<u32>(Channels::LFE)] * down_mix_coeff[2] + + samples[read_index + static_cast<u32>(Channels::BackRight)] * down_mix_coeff[3]) * + volume) + .to_int()}; + + samples[write_index + static_cast<u32>(Channels::FrontLeft)] = + static_cast<s16>(std::clamp(left_sample, min, max)); + samples[write_index + static_cast<u32>(Channels::FrontRight)] = + static_cast<s16>(std::clamp(right_sample, min, max)); + } + + samples.resize(samples.size() / system_channels * device_channels); + + } else if (system_channels == 2 && device_channels == 6) { + // We need moar samples! Not all games will provide 6 channel audio. + // TODO: Implement some upmixing here. Currently just passthrough, with other + // channels left as silence. + std::vector<s16> new_samples(samples.size() / system_channels * device_channels, 0); + + for (u32 read_index = 0, write_index = 0; read_index < samples.size(); + read_index += system_channels, write_index += device_channels) { + const auto left_sample{static_cast<s16>(std::clamp( + static_cast<s32>( + static_cast<f32>(samples[read_index + static_cast<u32>(Channels::FrontLeft)]) * + volume), + min, max))}; + + new_samples[write_index + static_cast<u32>(Channels::FrontLeft)] = left_sample; + + const auto right_sample{static_cast<s16>(std::clamp( + static_cast<s32>( + static_cast<f32>(samples[read_index + static_cast<u32>(Channels::FrontRight)]) * + volume), + min, max))}; + + new_samples[write_index + static_cast<u32>(Channels::FrontRight)] = right_sample; + } + samples = std::move(new_samples); + + } else if (volume != 1.0f) { + for (u32 i = 0; i < samples.size(); i++) { + samples[i] = static_cast<s16>( + std::clamp(static_cast<s32>(static_cast<f32>(samples[i]) * volume), min, max)); + } + } + + samples_buffer.Push(samples); + queue.enqueue(buffer); + queued_buffers++; +} + +std::vector<s16> SinkStream::ReleaseBuffer(u64 num_samples) { + constexpr s32 min = std::numeric_limits<s16>::min(); + constexpr s32 max = std::numeric_limits<s16>::max(); + + auto samples{samples_buffer.Pop(num_samples)}; + + // TODO: Up-mix to 6 channels if the game expects it. + // For audio input this is unlikely to ever be the case though. + + // Incoming mic volume seems to always be very quiet, so multiply by an additional 8 here. + // TODO: Play with this and find something that works better. + auto volume{system_volume * device_volume * 8}; + for (u32 i = 0; i < samples.size(); i++) { + samples[i] = static_cast<s16>( + std::clamp(static_cast<s32>(static_cast<f32>(samples[i]) * volume), min, max)); + } + + if (samples.size() < num_samples) { + samples.resize(num_samples, 0); + } + return samples; +} + +void SinkStream::ClearQueue() { + samples_buffer.Pop(); + while (queue.pop()) { + } + queued_buffers = 0; + playing_buffer = {}; + playing_buffer.consumed = true; +} + +void SinkStream::ProcessAudioIn(std::span<const s16> input_buffer, std::size_t num_frames) { + const std::size_t num_channels = GetDeviceChannels(); + const std::size_t frame_size = num_channels; + const std::size_t frame_size_bytes = frame_size * sizeof(s16); + size_t frames_written{0}; + + // If we're paused or going to shut down, we don't want to consume buffers as coretiming is + // paused and we'll desync, so just return. + if (system.IsPaused() || system.IsShuttingDown()) { + return; + } + + if (queued_buffers > max_queue_size) { + Stall(); + } + + while (frames_written < num_frames) { + // If the playing buffer has been consumed or has no frames, we need a new one + if (playing_buffer.consumed || playing_buffer.frames == 0) { + if (!queue.try_dequeue(playing_buffer)) { + // If no buffer was available we've underrun, just push the samples and + // continue. + samples_buffer.Push(&input_buffer[frames_written * frame_size], + (num_frames - frames_written) * frame_size); + frames_written = num_frames; + continue; + } + // Successfully dequeued a new buffer. + queued_buffers--; + } + + // Get the minimum frames available between the currently playing buffer, and the + // amount we have left to fill + size_t frames_available{std::min(playing_buffer.frames - playing_buffer.frames_played, + num_frames - frames_written)}; + + samples_buffer.Push(&input_buffer[frames_written * frame_size], + frames_available * frame_size); + + frames_written += frames_available; + playing_buffer.frames_played += frames_available; + + // If that's all the frames in the current buffer, add its samples and mark it as + // consumed + if (playing_buffer.frames_played >= playing_buffer.frames) { + playing_buffer.consumed = true; + } + } + + std::memcpy(&last_frame[0], &input_buffer[(frames_written - 1) * frame_size], frame_size_bytes); + + if (queued_buffers <= max_queue_size) { + Unstall(); + } +} + +void SinkStream::ProcessAudioOutAndRender(std::span<s16> output_buffer, std::size_t num_frames) { + const std::size_t num_channels = GetDeviceChannels(); + const std::size_t frame_size = num_channels; + const std::size_t frame_size_bytes = frame_size * sizeof(s16); + size_t frames_written{0}; + + // If we're paused or going to shut down, we don't want to consume buffers as coretiming is + // paused and we'll desync, so just play silence. + if (system.IsPaused() || system.IsShuttingDown()) { + constexpr std::array<s16, 6> silence{}; + for (size_t i = frames_written; i < num_frames; i++) { + std::memcpy(&output_buffer[i * frame_size], &silence[0], frame_size_bytes); + } + return; + } + + // Due to many frames being queued up with nvdec (5 frames or so?), a lot of buffers also get + // queued up (30+) but not all at once, which causes constant stalling here, so just let the + // video play out without attempting to stall. + // Can hopefully remove this later with a more complete NVDEC implementation. + const auto nvdec_active{system.AudioCore().IsNVDECActive()}; + if (!nvdec_active && queued_buffers > max_queue_size) { + Stall(); + } + + while (frames_written < num_frames) { + // If the playing buffer has been consumed or has no frames, we need a new one + if (playing_buffer.consumed || playing_buffer.frames == 0) { + if (!queue.try_dequeue(playing_buffer)) { + // If no buffer was available we've underrun, fill the remaining buffer with + // the last written frame and continue. + for (size_t i = frames_written; i < num_frames; i++) { + std::memcpy(&output_buffer[i * frame_size], &last_frame[0], frame_size_bytes); + } + frames_written = num_frames; + continue; + } + // Successfully dequeued a new buffer. + queued_buffers--; + } + + // Get the minimum frames available between the currently playing buffer, and the + // amount we have left to fill + size_t frames_available{std::min(playing_buffer.frames - playing_buffer.frames_played, + num_frames - frames_written)}; + + samples_buffer.Pop(&output_buffer[frames_written * frame_size], + frames_available * frame_size); + + frames_written += frames_available; + playing_buffer.frames_played += frames_available; + + // If that's all the frames in the current buffer, add its samples and mark it as + // consumed + if (playing_buffer.frames_played >= playing_buffer.frames) { + playing_buffer.consumed = true; + } + } + + std::memcpy(&last_frame[0], &output_buffer[(frames_written - 1) * frame_size], + frame_size_bytes); + + if (stalled && queued_buffers <= max_queue_size) { + Unstall(); + } +} + +void SinkStream::Stall() { + if (stalled) { + return; + } + stalled = true; + system.StallProcesses(); +} + +void SinkStream::Unstall() { + if (!stalled) { + return; + } + system.UnstallProcesses(); + stalled = false; +} + +} // namespace AudioCore::Sink diff --git a/src/audio_core/sink/sink_stream.h b/src/audio_core/sink/sink_stream.h index 17ed6593f..38a4b2f51 100644 --- a/src/audio_core/sink/sink_stream.h +++ b/src/audio_core/sink/sink_stream.h @@ -3,12 +3,20 @@ #pragma once +#include <array> #include <atomic> #include <memory> +#include <span> #include <vector> #include "audio_core/common/common.h" #include "common/common_types.h" +#include "common/reader_writer_queue.h" +#include "common/ring_buffer.h" + +namespace Core { +class System; +} // namespace Core namespace AudioCore::Sink { @@ -34,20 +42,24 @@ struct SinkBuffer { * You should regularly call IsBufferConsumed with the unique SinkBuffer tag to check if the buffer * has been consumed. * - * Since these are a FIFO queue, always check IsBufferConsumed in the same order you appended the - * buffers, skipping a buffer will result in all following buffers to never release. + * Since these are a FIFO queue, IsBufferConsumed must be checked in the same order buffers were + * appended, skipping a buffer will result in the queue getting stuck, and all following buffers to + * never release. * * If the buffers appear to be stuck, you can stop and re-open an IAudioIn/IAudioOut service (this * is what games do), or call ClearQueue to flush all of the buffers without a full restart. */ class SinkStream { public: - virtual ~SinkStream() = default; + explicit SinkStream(Core::System& system_, StreamType type_) : system{system_}, type{type_} {} + virtual ~SinkStream() { + Unstall(); + } /** * Finalize the sink stream. */ - virtual void Finalize() = 0; + virtual void Finalize() {} /** * Start the sink stream. @@ -55,48 +67,19 @@ public: * @param resume - Set to true if this is resuming the stream a previously-active stream. * Default false. */ - virtual void Start(bool resume = false) = 0; + virtual void Start(bool resume = false) {} /** * Stop the sink stream. */ - virtual void Stop() = 0; - - /** - * Append a new buffer and its samples to a waiting queue to play. - * - * @param buffer - Audio buffer information to be queued. - * @param samples - The s16 samples to be queue for playback. - */ - virtual void AppendBuffer(SinkBuffer& buffer, std::vector<s16>& samples) = 0; - - /** - * Release a buffer. Audio In only, will fill a buffer with recorded samples. - * - * @param num_samples - Maximum number of samples to receive. - * @return Vector of recorded samples. May have fewer than num_samples. - */ - virtual std::vector<s16> ReleaseBuffer(u64 num_samples) = 0; - - /** - * Check if a certain buffer has been consumed (fully played). - * - * @param tag - Unique tag of a buffer to check for. - * @return True if the buffer has been played, otherwise false. - */ - virtual bool IsBufferConsumed(u64 tag) = 0; - - /** - * Empty out the buffer queue. - */ - virtual void ClearQueue() = 0; + virtual void Stop() {} /** * Check if the stream is paused. * * @return True if paused, otherwise false. */ - bool IsPaused() { + bool IsPaused() const { return paused; } @@ -128,34 +111,6 @@ public: } /** - * Get the total number of samples played by this stream. - * - * @return Number of samples played. - */ - u64 GetPlayedSampleCount() const { - return played_sample_count; - } - - /** - * Set the number of samples played. - * This is started and stopped on system start/stop. - * - * @param played_sample_count_ - Number of samples to set. - */ - void SetPlayedSampleCount(u64 played_sample_count_) { - played_sample_count = played_sample_count_; - } - - /** - * Add to the played sample count. - * - * @param num_samples - Number of samples to add. - */ - void AddPlayedSampleCount(u64 num_samples) { - played_sample_count += num_samples; - } - - /** * Get the system volume. * * @return The current system volume. @@ -196,27 +151,97 @@ public: * * @return The number of queued buffers. */ - u32 GetQueueSize() { + u32 GetQueueSize() const { return queued_buffers.load(); } + /** + * Set the maximum buffer queue size. + */ + void SetRingSize(u32 ring_size) { + max_queue_size = ring_size; + } + + /** + * Append a new buffer and its samples to a waiting queue to play. + * + * @param buffer - Audio buffer information to be queued. + * @param samples - The s16 samples to be queue for playback. + */ + virtual void AppendBuffer(SinkBuffer& buffer, std::vector<s16>& samples); + + /** + * Release a buffer. Audio In only, will fill a buffer with recorded samples. + * + * @param num_samples - Maximum number of samples to receive. + * @return Vector of recorded samples. May have fewer than num_samples. + */ + virtual std::vector<s16> ReleaseBuffer(u64 num_samples); + + /** + * Empty out the buffer queue. + */ + void ClearQueue(); + + /** + * Callback for AudioIn. + * + * @param input_buffer - Input buffer to be filled with samples. + * @param num_frames - Number of frames to be filled. + */ + void ProcessAudioIn(std::span<const s16> input_buffer, std::size_t num_frames); + + /** + * Callback for AudioOut and AudioRenderer. + * + * @param output_buffer - Output buffer to be filled with samples. + * @param num_frames - Number of frames to be filled. + */ + void ProcessAudioOutAndRender(std::span<s16> output_buffer, std::size_t num_frames); + + /** + * Stall core processes if the audio thread falls too far behind. + */ + void Stall(); + + /** + * Unstall core processes. + */ + void Unstall(); + protected: - /// Number of buffers waiting to be played - std::atomic<u32> queued_buffers{}; - /// Total samples played by this stream - std::atomic<u64> played_sample_count{}; + /// Core system + Core::System& system; + /// Type of this stream + StreamType type; /// Set by the audio render/in/out system which uses this stream - f32 system_volume{1.0f}; - /// Set via IAudioDevice service calls - f32 device_volume{1.0f}; - /// Set by the audio render/in/out systen which uses this stream u32 system_channels{2}; /// Channels supported by hardware u32 device_channels{2}; /// Is this stream currently paused? std::atomic<bool> paused{true}; - /// Was this stream previously playing? - std::atomic<bool> was_playing{false}; + /// Name of this stream + std::string name{}; + +private: + /// Ring buffer of the samples waiting to be played or consumed + Common::RingBuffer<s16, 0x10000> samples_buffer; + /// Audio buffers queued and waiting to play + Common::ReaderWriterQueue<SinkBuffer> queue; + /// The currently-playing audio buffer + SinkBuffer playing_buffer{}; + /// The last played (or received) frame of audio, used when the callback underruns + std::array<s16, MaxChannels> last_frame{}; + /// Number of buffers waiting to be played + std::atomic<u32> queued_buffers{}; + /// The ring size for audio out buffers (usually 4, rarely 2 or 8) + u32 max_queue_size{}; + /// Set by the audio render/in/out system which uses this stream + f32 system_volume{1.0f}; + /// Set via IAudioDevice service calls + f32 device_volume{1.0f}; + /// True if coretiming has been stalled + bool stalled{false}; }; using SinkStreamPtr = std::unique_ptr<SinkStream>; diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 635fb85c8..68436a4bc 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -19,7 +19,7 @@ find_package(Git QUIET) add_custom_command(OUTPUT scm_rev.cpp COMMAND ${CMAKE_COMMAND} - -DSRC_DIR=${CMAKE_SOURCE_DIR} + -DSRC_DIR=${PROJECT_SOURCE_DIR} -DBUILD_REPOSITORY=${BUILD_REPOSITORY} -DTITLE_BAR_FORMAT_IDLE=${TITLE_BAR_FORMAT_IDLE} -DTITLE_BAR_FORMAT_RUNNING=${TITLE_BAR_FORMAT_RUNNING} @@ -31,13 +31,13 @@ add_custom_command(OUTPUT scm_rev.cpp -DGIT_BRANCH=${GIT_BRANCH} -DBUILD_FULLNAME=${BUILD_FULLNAME} -DGIT_EXECUTABLE=${GIT_EXECUTABLE} - -P ${CMAKE_SOURCE_DIR}/CMakeModules/GenerateSCMRev.cmake + -P ${PROJECT_SOURCE_DIR}/CMakeModules/GenerateSCMRev.cmake DEPENDS # Check that the scm_rev files haven't changed "${CMAKE_CURRENT_SOURCE_DIR}/scm_rev.cpp.in" "${CMAKE_CURRENT_SOURCE_DIR}/scm_rev.h" # technically we should regenerate if the git version changed, but its not worth the effort imo - "${CMAKE_SOURCE_DIR}/CMakeModules/GenerateSCMRev.cmake" + "${PROJECT_SOURCE_DIR}/CMakeModules/GenerateSCMRev.cmake" VERBATIM ) @@ -166,6 +166,7 @@ if(ARCHITECTURE_x86_64) x64/xbyak_abi.h x64/xbyak_util.h ) + target_link_libraries(common PRIVATE xbyak) endif() if (MSVC) @@ -189,7 +190,7 @@ endif() create_target_directory_groups(common) target_link_libraries(common PUBLIC ${Boost_LIBRARIES} fmt::fmt microprofile Threads::Threads) -target_link_libraries(common PRIVATE lz4::lz4 xbyak) +target_link_libraries(common PRIVATE lz4::lz4) if (TARGET zstd::zstd) target_link_libraries(common PRIVATE zstd::zstd) else() diff --git a/src/common/settings.h b/src/common/settings.h index 13651de57..851812f28 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -530,6 +530,7 @@ struct Values { Setting<bool> use_debug_asserts{false, "use_debug_asserts"}; Setting<bool> use_auto_stub{false, "use_auto_stub"}; Setting<bool> enable_all_controllers{false, "enable_all_controllers"}; + Setting<bool> create_crash_dumps{false, "create_crash_dumps"}; // Miscellaneous Setting<std::string> log_filter{"*:Info", "log_filter"}; diff --git a/src/common/thread.h b/src/common/thread.h index 1552f58e0..e17a7850f 100644 --- a/src/common/thread.h +++ b/src/common/thread.h @@ -54,6 +54,10 @@ public: is_set = false; } + [[nodiscard]] bool IsSet() { + return is_set; + } + private: std::condition_variable condvar; std::mutex mutex; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 806e7ff6c..33cf470d5 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -4,12 +4,6 @@ add_library(core STATIC arm/arm_interface.h arm/arm_interface.cpp - arm/dynarmic/arm_dynarmic_32.cpp - arm/dynarmic/arm_dynarmic_32.h - arm/dynarmic/arm_dynarmic_64.cpp - arm/dynarmic/arm_dynarmic_64.h - arm/dynarmic/arm_dynarmic_cp15.cpp - arm/dynarmic/arm_dynarmic_cp15.h arm/dynarmic/arm_exclusive_monitor.cpp arm/dynarmic/arm_exclusive_monitor.h arm/exclusive_monitor.cpp @@ -525,6 +519,9 @@ add_library(core STATIC hle/service/ncm/ncm.h hle/service/nfc/nfc.cpp hle/service/nfc/nfc.h + hle/service/nfp/amiibo_crypto.cpp + hle/service/nfp/amiibo_crypto.h + hle/service/nfp/amiibo_types.h hle/service/nfp/nfp.cpp hle/service/nfp/nfp.h hle/service/nfp/nfp_user.cpp diff --git a/src/core/core.cpp b/src/core/core.cpp index e651ce100..121092868 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -141,8 +141,6 @@ struct System::Impl { core_timing.SyncPause(false); is_paused = false; - audio_core->PauseSinks(false); - return status; } @@ -150,8 +148,6 @@ struct System::Impl { std::unique_lock<std::mutex> lk(suspend_guard); status = SystemResultStatus::Success; - audio_core->PauseSinks(true); - core_timing.SyncPause(true); kernel.Suspend(true); is_paused = true; diff --git a/src/core/core_timing.cpp b/src/core/core_timing.cpp index 2dbb99c8b..f6c4567ba 100644 --- a/src/core/core_timing.cpp +++ b/src/core/core_timing.cpp @@ -73,7 +73,6 @@ void CoreTiming::Shutdown() { if (timer_thread) { timer_thread->join(); } - pause_callbacks.clear(); ClearPendingEvents(); timer_thread.reset(); has_started = false; @@ -86,10 +85,6 @@ void CoreTiming::Pause(bool is_paused) { if (!is_paused) { pause_end_time = GetGlobalTimeNs().count(); } - - for (auto& cb : pause_callbacks) { - cb(is_paused); - } } void CoreTiming::SyncPause(bool is_paused) { @@ -110,10 +105,6 @@ void CoreTiming::SyncPause(bool is_paused) { if (!is_paused) { pause_end_time = GetGlobalTimeNs().count(); } - - for (auto& cb : pause_callbacks) { - cb(is_paused); - } } bool CoreTiming::IsRunning() const { @@ -143,13 +134,17 @@ void CoreTiming::ScheduleLoopingEvent(std::chrono::nanoseconds start_time, std::chrono::nanoseconds resched_time, const std::shared_ptr<EventType>& event_type, std::uintptr_t user_data, bool absolute_time) { - std::scoped_lock scope{basic_lock}; - const auto next_time{absolute_time ? start_time : GetGlobalTimeNs() + start_time}; + { + std::scoped_lock scope{basic_lock}; + const auto next_time{absolute_time ? start_time : GetGlobalTimeNs() + start_time}; + + event_queue.emplace_back( + Event{next_time.count(), event_fifo_id++, user_data, event_type, resched_time.count()}); - event_queue.emplace_back( - Event{next_time.count(), event_fifo_id++, user_data, event_type, resched_time.count()}); + std::push_heap(event_queue.begin(), event_queue.end(), std::greater<>()); + } - std::push_heap(event_queue.begin(), event_queue.end(), std::greater<>()); + event.Set(); } void CoreTiming::UnscheduleEvent(const std::shared_ptr<EventType>& event_type, @@ -219,11 +214,6 @@ void CoreTiming::RemoveEvent(const std::shared_ptr<EventType>& event_type) { } } -void CoreTiming::RegisterPauseCallback(PauseCallback&& callback) { - std::scoped_lock lock{basic_lock}; - pause_callbacks.emplace_back(std::move(callback)); -} - std::optional<s64> CoreTiming::Advance() { std::scoped_lock lock{advance_lock, basic_lock}; global_timer = GetGlobalTimeNs().count(); @@ -243,17 +233,17 @@ std::optional<s64> CoreTiming::Advance() { basic_lock.lock(); if (evt.reschedule_time != 0) { + const auto next_schedule_time{new_schedule_time.has_value() + ? new_schedule_time.value().count() + : evt.reschedule_time}; + // If this event was scheduled into a pause, its time now is going to be way behind. // Re-set this event to continue from the end of the pause. - auto next_time{evt.time + evt.reschedule_time}; + auto next_time{evt.time + next_schedule_time}; if (evt.time < pause_end_time) { - next_time = pause_end_time + evt.reschedule_time; + next_time = pause_end_time + next_schedule_time; } - const auto next_schedule_time{new_schedule_time.has_value() - ? new_schedule_time.value().count() - : evt.reschedule_time}; - event_queue.emplace_back( Event{next_time, event_fifo_id++, evt.user_data, evt.type, next_schedule_time}); std::push_heap(event_queue.begin(), event_queue.end(), std::greater<>()); @@ -264,8 +254,7 @@ std::optional<s64> CoreTiming::Advance() { } if (!event_queue.empty()) { - const s64 next_time = event_queue.front().time - global_timer; - return next_time; + return event_queue.front().time; } else { return std::nullopt; } @@ -278,11 +267,29 @@ void CoreTiming::ThreadLoop() { paused_set = false; const auto next_time = Advance(); if (next_time) { - if (*next_time > 0) { - std::chrono::nanoseconds next_time_ns = std::chrono::nanoseconds(*next_time); - event.WaitFor(next_time_ns); + // There are more events left in the queue, wait until the next event. + const auto wait_time = *next_time - GetGlobalTimeNs().count(); + if (wait_time > 0) { + // Assume a timer resolution of 1ms. + static constexpr s64 TimerResolutionNS = 1000000; + + // Sleep in discrete intervals of the timer resolution, and spin the rest. + const auto sleep_time = wait_time - (wait_time % TimerResolutionNS); + if (sleep_time > 0) { + event.WaitFor(std::chrono::nanoseconds(sleep_time)); + } + + while (!paused && !event.IsSet() && GetGlobalTimeNs().count() < *next_time) { + // Yield to reduce thread starvation. + std::this_thread::yield(); + } + + if (event.IsSet()) { + event.Reset(); + } } } else { + // Queue is empty, wait until another event is scheduled and signals us to continue. wait_set = true; event.Wait(); } diff --git a/src/core/core_timing.h b/src/core/core_timing.h index 6aa3ae923..3259397b2 100644 --- a/src/core/core_timing.h +++ b/src/core/core_timing.h @@ -22,7 +22,6 @@ namespace Core::Timing { /// A callback that may be scheduled for a particular core timing event. using TimedCallback = std::function<std::optional<std::chrono::nanoseconds>( std::uintptr_t user_data, s64 time, std::chrono::nanoseconds ns_late)>; -using PauseCallback = std::function<void(bool paused)>; /// Contains the characteristics of a particular event. struct EventType { @@ -134,9 +133,6 @@ public: /// Checks for events manually and returns time in nanoseconds for next event, threadsafe. std::optional<s64> Advance(); - /// Register a callback function to be called when coretiming pauses. - void RegisterPauseCallback(PauseCallback&& callback); - private: struct Event; @@ -176,8 +172,6 @@ private: /// Cycle timing u64 ticks{}; s64 downcount{}; - - std::vector<PauseCallback> pause_callbacks{}; }; /// Creates a core timing event with the given name and callback. diff --git a/src/core/hle/result.h b/src/core/hle/result.h index 4de44cd06..47a1b829b 100644 --- a/src/core/hle/result.h +++ b/src/core/hle/result.h @@ -117,6 +117,7 @@ union Result { BitField<0, 9, ErrorModule> module; BitField<9, 13, u32> description; + Result() = default; constexpr explicit Result(u32 raw_) : raw(raw_) {} constexpr Result(ErrorModule module_, u32 description_) @@ -130,6 +131,7 @@ union Result { return !IsSuccess(); } }; +static_assert(std::is_trivial_v<Result>); [[nodiscard]] constexpr bool operator==(const Result& a, const Result& b) { return a.raw == b.raw; diff --git a/src/core/hle/service/am/applets/applet_mii_edit_types.h b/src/core/hle/service/am/applets/applet_mii_edit_types.h index 1b145b696..4705d019f 100644 --- a/src/core/hle/service/am/applets/applet_mii_edit_types.h +++ b/src/core/hle/service/am/applets/applet_mii_edit_types.h @@ -32,7 +32,7 @@ enum class MiiEditResult : u32 { }; struct MiiEditCharInfo { - Service::Mii::MiiInfo mii_info{}; + Service::Mii::CharInfo mii_info{}; }; static_assert(sizeof(MiiEditCharInfo) == 0x58, "MiiEditCharInfo has incorrect size."); diff --git a/src/core/hle/service/audio/audout_u.cpp b/src/core/hle/service/audio/audout_u.cpp index a44dd842a..49c092301 100644 --- a/src/core/hle/service/audio/audout_u.cpp +++ b/src/core/hle/service/audio/audout_u.cpp @@ -246,9 +246,8 @@ void AudOutU::ListAudioOuts(Kernel::HLERequestContext& ctx) { const auto write_count = static_cast<u32>(ctx.GetWriteBufferSize() / sizeof(AudioDevice::AudioDeviceName)); std::vector<AudioDevice::AudioDeviceName> device_names{}; - std::string print_names{}; if (write_count > 0) { - device_names.push_back(AudioDevice::AudioDeviceName("DeviceOut")); + device_names.emplace_back("DeviceOut"); LOG_DEBUG(Service_Audio, "called. \nName=DeviceOut"); } else { LOG_DEBUG(Service_Audio, "called. Empty buffer passed in."); diff --git a/src/core/hle/service/audio/audren_u.cpp b/src/core/hle/service/audio/audren_u.cpp index bc69117c6..6fb07c37d 100644 --- a/src/core/hle/service/audio/audren_u.cpp +++ b/src/core/hle/service/audio/audren_u.cpp @@ -252,7 +252,7 @@ private: std::vector<AudioDevice::AudioDeviceName> out_names{}; - u32 out_count = impl->ListAudioDeviceName(out_names, in_count); + const u32 out_count = impl->ListAudioDeviceName(out_names, in_count); std::string out{}; for (u32 i = 0; i < out_count; i++) { @@ -365,7 +365,7 @@ private: std::vector<AudioDevice::AudioDeviceName> out_names{}; - u32 out_count = impl->ListAudioOutputDeviceName(out_names, in_count); + const u32 out_count = impl->ListAudioOutputDeviceName(out_names, in_count); std::string out{}; for (u32 i = 0; i < out_count; i++) { diff --git a/src/core/hle/service/mii/mii.cpp b/src/core/hle/service/mii/mii.cpp index efb569993..390514fdc 100644 --- a/src/core/hle/service/mii/mii.cpp +++ b/src/core/hle/service/mii/mii.cpp @@ -43,7 +43,7 @@ public: {20, nullptr, "IsBrokenDatabaseWithClearFlag"}, {21, &IDatabaseService::GetIndex, "GetIndex"}, {22, &IDatabaseService::SetInterfaceVersion, "SetInterfaceVersion"}, - {23, nullptr, "Convert"}, + {23, &IDatabaseService::Convert, "Convert"}, {24, nullptr, "ConvertCoreDataToCharInfo"}, {25, nullptr, "ConvertCharInfoToCoreData"}, {26, nullptr, "Append"}, @@ -130,7 +130,7 @@ private: return; } - std::vector<MiiInfo> values; + std::vector<CharInfo> values; for (const auto& element : *result) { values.emplace_back(element.info); } @@ -144,7 +144,7 @@ private: void UpdateLatest(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; - const auto info{rp.PopRaw<MiiInfo>()}; + const auto info{rp.PopRaw<CharInfo>()}; const auto source_flag{rp.PopRaw<SourceFlag>()}; LOG_DEBUG(Service_Mii, "called with source_flag={}", source_flag); @@ -156,9 +156,9 @@ private: return; } - IPC::ResponseBuilder rb{ctx, 2 + sizeof(MiiInfo) / sizeof(u32)}; + IPC::ResponseBuilder rb{ctx, 2 + sizeof(CharInfo) / sizeof(u32)}; rb.Push(ResultSuccess); - rb.PushRaw<MiiInfo>(*result); + rb.PushRaw<CharInfo>(*result); } void BuildRandom(Kernel::HLERequestContext& ctx) { @@ -191,9 +191,9 @@ private: return; } - IPC::ResponseBuilder rb{ctx, 2 + sizeof(MiiInfo) / sizeof(u32)}; + IPC::ResponseBuilder rb{ctx, 2 + sizeof(CharInfo) / sizeof(u32)}; rb.Push(ResultSuccess); - rb.PushRaw<MiiInfo>(manager.BuildRandom(age, gender, race)); + rb.PushRaw<CharInfo>(manager.BuildRandom(age, gender, race)); } void BuildDefault(Kernel::HLERequestContext& ctx) { @@ -210,14 +210,14 @@ private: return; } - IPC::ResponseBuilder rb{ctx, 2 + sizeof(MiiInfo) / sizeof(u32)}; + IPC::ResponseBuilder rb{ctx, 2 + sizeof(CharInfo) / sizeof(u32)}; rb.Push(ResultSuccess); - rb.PushRaw<MiiInfo>(manager.BuildDefault(index)); + rb.PushRaw<CharInfo>(manager.BuildDefault(index)); } void GetIndex(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; - const auto info{rp.PopRaw<MiiInfo>()}; + const auto info{rp.PopRaw<CharInfo>()}; LOG_DEBUG(Service_Mii, "called"); @@ -239,6 +239,18 @@ private: rb.Push(ResultSuccess); } + void Convert(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + + const auto mii_v3{rp.PopRaw<Ver3StoreData>()}; + + LOG_INFO(Service_Mii, "called"); + + IPC::ResponseBuilder rb{ctx, 2 + sizeof(CharInfo) / sizeof(u32)}; + rb.Push(ResultSuccess); + rb.PushRaw<CharInfo>(manager.ConvertV3ToCharInfo(mii_v3)); + } + constexpr bool IsInterfaceVersionSupported(u32 interface_version) const { return current_interface_version >= interface_version; } diff --git a/src/core/hle/service/mii/mii_manager.cpp b/src/core/hle/service/mii/mii_manager.cpp index 544c92a00..c484a9c8d 100644 --- a/src/core/hle/service/mii/mii_manager.cpp +++ b/src/core/hle/service/mii/mii_manager.cpp @@ -42,7 +42,7 @@ std::array<T, DestArraySize> ResizeArray(const std::array<T, SourceArraySize>& i return out; } -MiiInfo ConvertStoreDataToInfo(const MiiStoreData& data) { +CharInfo ConvertStoreDataToInfo(const MiiStoreData& data) { MiiStoreBitFields bf; std::memcpy(&bf, data.data.data.data(), sizeof(MiiStoreBitFields)); @@ -409,8 +409,8 @@ u32 MiiManager::GetCount(SourceFlag source_flag) const { return static_cast<u32>(count); } -ResultVal<MiiInfo> MiiManager::UpdateLatest([[maybe_unused]] const MiiInfo& info, - SourceFlag source_flag) { +ResultVal<CharInfo> MiiManager::UpdateLatest([[maybe_unused]] const CharInfo& info, + SourceFlag source_flag) { if ((source_flag & SourceFlag::Database) == SourceFlag::None) { return ERROR_CANNOT_FIND_ENTRY; } @@ -419,14 +419,91 @@ ResultVal<MiiInfo> MiiManager::UpdateLatest([[maybe_unused]] const MiiInfo& info return ERROR_CANNOT_FIND_ENTRY; } -MiiInfo MiiManager::BuildRandom(Age age, Gender gender, Race race) { +CharInfo MiiManager::BuildRandom(Age age, Gender gender, Race race) { return ConvertStoreDataToInfo(BuildRandomStoreData(age, gender, race, user_id)); } -MiiInfo MiiManager::BuildDefault(std::size_t index) { +CharInfo MiiManager::BuildDefault(std::size_t index) { return ConvertStoreDataToInfo(BuildDefaultStoreData(RawData::DefaultMii.at(index), user_id)); } +CharInfo MiiManager::ConvertV3ToCharInfo(Ver3StoreData mii_v3) const { + Service::Mii::MiiManager manager; + auto mii = manager.BuildDefault(0); + + // Check if mii data exist + if (mii_v3.mii_name[0] == 0) { + return mii; + } + + // TODO: We are ignoring a bunch of data from the mii_v3 + + mii.gender = static_cast<u8>(mii_v3.mii_information.gender); + mii.favorite_color = static_cast<u8>(mii_v3.mii_information.favorite_color); + mii.height = mii_v3.height; + mii.build = mii_v3.build; + + memset(mii.name.data(), 0, sizeof(mii.name)); + memcpy(mii.name.data(), mii_v3.mii_name.data(), sizeof(mii_v3.mii_name)); + mii.font_region = mii_v3.region_information.character_set; + + mii.faceline_type = mii_v3.appearance_bits1.face_shape; + mii.faceline_color = mii_v3.appearance_bits1.skin_color; + mii.faceline_wrinkle = mii_v3.appearance_bits2.wrinkles; + mii.faceline_make = mii_v3.appearance_bits2.makeup; + + mii.hair_type = mii_v3.hair_style; + mii.hair_color = mii_v3.appearance_bits3.hair_color; + mii.hair_flip = mii_v3.appearance_bits3.flip_hair; + + mii.eye_type = static_cast<u8>(mii_v3.appearance_bits4.eye_type); + mii.eye_color = static_cast<u8>(mii_v3.appearance_bits4.eye_color); + mii.eye_scale = static_cast<u8>(mii_v3.appearance_bits4.eye_scale); + mii.eye_aspect = static_cast<u8>(mii_v3.appearance_bits4.eye_vertical_stretch); + mii.eye_rotate = static_cast<u8>(mii_v3.appearance_bits4.eye_rotation); + mii.eye_x = static_cast<u8>(mii_v3.appearance_bits4.eye_spacing); + mii.eye_y = static_cast<u8>(mii_v3.appearance_bits4.eye_y_position); + + mii.eyebrow_type = static_cast<u8>(mii_v3.appearance_bits5.eyebrow_style); + mii.eyebrow_color = static_cast<u8>(mii_v3.appearance_bits5.eyebrow_color); + mii.eyebrow_scale = static_cast<u8>(mii_v3.appearance_bits5.eyebrow_scale); + mii.eyebrow_aspect = static_cast<u8>(mii_v3.appearance_bits5.eyebrow_yscale); + mii.eyebrow_rotate = static_cast<u8>(mii_v3.appearance_bits5.eyebrow_rotation); + mii.eyebrow_x = static_cast<u8>(mii_v3.appearance_bits5.eyebrow_spacing); + mii.eyebrow_y = static_cast<u8>(mii_v3.appearance_bits5.eyebrow_y_position); + + mii.nose_type = static_cast<u8>(mii_v3.appearance_bits6.nose_type); + mii.nose_scale = static_cast<u8>(mii_v3.appearance_bits6.nose_scale); + mii.nose_y = static_cast<u8>(mii_v3.appearance_bits6.nose_y_position); + + mii.mouth_type = static_cast<u8>(mii_v3.appearance_bits7.mouth_type); + mii.mouth_color = static_cast<u8>(mii_v3.appearance_bits7.mouth_color); + mii.mouth_scale = static_cast<u8>(mii_v3.appearance_bits7.mouth_scale); + mii.mouth_aspect = static_cast<u8>(mii_v3.appearance_bits7.mouth_horizontal_stretch); + mii.mouth_y = static_cast<u8>(mii_v3.appearance_bits8.mouth_y_position); + + mii.mustache_type = static_cast<u8>(mii_v3.appearance_bits8.mustache_type); + mii.mustache_scale = static_cast<u8>(mii_v3.appearance_bits9.mustache_scale); + mii.mustache_y = static_cast<u8>(mii_v3.appearance_bits9.mustache_y_position); + + mii.beard_type = static_cast<u8>(mii_v3.appearance_bits9.bear_type); + mii.beard_color = static_cast<u8>(mii_v3.appearance_bits9.facial_hair_color); + + mii.glasses_type = static_cast<u8>(mii_v3.appearance_bits10.glasses_type); + mii.glasses_color = static_cast<u8>(mii_v3.appearance_bits10.glasses_color); + mii.glasses_scale = static_cast<u8>(mii_v3.appearance_bits10.glasses_scale); + mii.glasses_y = static_cast<u8>(mii_v3.appearance_bits10.glasses_y_position); + + mii.mole_type = static_cast<u8>(mii_v3.appearance_bits11.mole_enabled); + mii.mole_scale = static_cast<u8>(mii_v3.appearance_bits11.mole_scale); + mii.mole_x = static_cast<u8>(mii_v3.appearance_bits11.mole_x_position); + mii.mole_y = static_cast<u8>(mii_v3.appearance_bits11.mole_y_position); + + // TODO: Validate mii data + + return mii; +} + ResultVal<std::vector<MiiInfoElement>> MiiManager::GetDefault(SourceFlag source_flag) { std::vector<MiiInfoElement> result; @@ -441,7 +518,7 @@ ResultVal<std::vector<MiiInfoElement>> MiiManager::GetDefault(SourceFlag source_ return result; } -Result MiiManager::GetIndex([[maybe_unused]] const MiiInfo& info, u32& index) { +Result MiiManager::GetIndex([[maybe_unused]] const CharInfo& info, u32& index) { constexpr u32 INVALID_INDEX{0xFFFFFFFF}; index = INVALID_INDEX; diff --git a/src/core/hle/service/mii/mii_manager.h b/src/core/hle/service/mii/mii_manager.h index 6a286bd96..d847de0bd 100644 --- a/src/core/hle/service/mii/mii_manager.h +++ b/src/core/hle/service/mii/mii_manager.h @@ -19,11 +19,12 @@ public: bool CheckAndResetUpdateCounter(SourceFlag source_flag, u64& current_update_counter); bool IsFullDatabase() const; u32 GetCount(SourceFlag source_flag) const; - ResultVal<MiiInfo> UpdateLatest(const MiiInfo& info, SourceFlag source_flag); - MiiInfo BuildRandom(Age age, Gender gender, Race race); - MiiInfo BuildDefault(std::size_t index); + ResultVal<CharInfo> UpdateLatest(const CharInfo& info, SourceFlag source_flag); + CharInfo BuildRandom(Age age, Gender gender, Race race); + CharInfo BuildDefault(std::size_t index); + CharInfo ConvertV3ToCharInfo(Ver3StoreData mii_v3) const; ResultVal<std::vector<MiiInfoElement>> GetDefault(SourceFlag source_flag); - Result GetIndex(const MiiInfo& info, u32& index); + Result GetIndex(const CharInfo& info, u32& index); private: const Common::UUID user_id{}; diff --git a/src/core/hle/service/mii/types.h b/src/core/hle/service/mii/types.h index 45edbfeae..9e3247397 100644 --- a/src/core/hle/service/mii/types.h +++ b/src/core/hle/service/mii/types.h @@ -86,7 +86,8 @@ enum class SourceFlag : u32 { }; DECLARE_ENUM_FLAG_OPERATORS(SourceFlag); -struct MiiInfo { +// nn::mii::CharInfo +struct CharInfo { Common::UUID uuid; std::array<char16_t, 11> name; u8 font_region; @@ -140,16 +141,16 @@ struct MiiInfo { u8 mole_y; u8 padding; }; -static_assert(sizeof(MiiInfo) == 0x58, "MiiInfo has incorrect size."); -static_assert(std::has_unique_object_representations_v<MiiInfo>, - "All bits of MiiInfo must contribute to its value."); +static_assert(sizeof(CharInfo) == 0x58, "CharInfo has incorrect size."); +static_assert(std::has_unique_object_representations_v<CharInfo>, + "All bits of CharInfo must contribute to its value."); #pragma pack(push, 4) struct MiiInfoElement { - MiiInfoElement(const MiiInfo& info_, Source source_) : info{info_}, source{source_} {} + MiiInfoElement(const CharInfo& info_, Source source_) : info{info_}, source{source_} {} - MiiInfo info{}; + CharInfo info{}; Source source{}; }; static_assert(sizeof(MiiInfoElement) == 0x5c, "MiiInfoElement has incorrect size."); @@ -243,6 +244,131 @@ static_assert(sizeof(MiiStoreBitFields) == 0x1c, "MiiStoreBitFields has incorrec static_assert(std::is_trivially_copyable_v<MiiStoreBitFields>, "MiiStoreBitFields is not trivially copyable."); +// This is nn::mii::Ver3StoreData +// Based on citra HLE::Applets::MiiData and PretendoNetwork. +// https://github.com/citra-emu/citra/blob/master/src/core/hle/applets/mii_selector.h#L48 +// https://github.com/PretendoNetwork/mii-js/blob/master/mii.js#L299 +struct Ver3StoreData { + u8 version; + union { + u8 raw; + + BitField<0, 1, u8> allow_copying; + BitField<1, 1, u8> profanity_flag; + BitField<2, 2, u8> region_lock; + BitField<4, 2, u8> character_set; + } region_information; + u16_be mii_id; + u64_be system_id; + u32_be specialness_and_creation_date; + std::array<u8, 0x6> creator_mac; + u16_be padding; + union { + u16 raw; + + BitField<0, 1, u16> gender; + BitField<1, 4, u16> birth_month; + BitField<5, 5, u16> birth_day; + BitField<10, 4, u16> favorite_color; + BitField<14, 1, u16> favorite; + } mii_information; + std::array<char16_t, 0xA> mii_name; + u8 height; + u8 build; + union { + u8 raw; + + BitField<0, 1, u8> disable_sharing; + BitField<1, 4, u8> face_shape; + BitField<5, 3, u8> skin_color; + } appearance_bits1; + union { + u8 raw; + + BitField<0, 4, u8> wrinkles; + BitField<4, 4, u8> makeup; + } appearance_bits2; + u8 hair_style; + union { + u8 raw; + + BitField<0, 3, u8> hair_color; + BitField<3, 1, u8> flip_hair; + } appearance_bits3; + union { + u32 raw; + + BitField<0, 6, u32> eye_type; + BitField<6, 3, u32> eye_color; + BitField<9, 4, u32> eye_scale; + BitField<13, 3, u32> eye_vertical_stretch; + BitField<16, 5, u32> eye_rotation; + BitField<21, 4, u32> eye_spacing; + BitField<25, 5, u32> eye_y_position; + } appearance_bits4; + union { + u32 raw; + + BitField<0, 5, u32> eyebrow_style; + BitField<5, 3, u32> eyebrow_color; + BitField<8, 4, u32> eyebrow_scale; + BitField<12, 3, u32> eyebrow_yscale; + BitField<16, 4, u32> eyebrow_rotation; + BitField<21, 4, u32> eyebrow_spacing; + BitField<25, 5, u32> eyebrow_y_position; + } appearance_bits5; + union { + u16 raw; + + BitField<0, 5, u16> nose_type; + BitField<5, 4, u16> nose_scale; + BitField<9, 5, u16> nose_y_position; + } appearance_bits6; + union { + u16 raw; + + BitField<0, 6, u16> mouth_type; + BitField<6, 3, u16> mouth_color; + BitField<9, 4, u16> mouth_scale; + BitField<13, 3, u16> mouth_horizontal_stretch; + } appearance_bits7; + union { + u8 raw; + + BitField<0, 5, u8> mouth_y_position; + BitField<5, 3, u8> mustache_type; + } appearance_bits8; + u8 allow_copying; + union { + u16 raw; + + BitField<0, 3, u16> bear_type; + BitField<3, 3, u16> facial_hair_color; + BitField<6, 4, u16> mustache_scale; + BitField<10, 5, u16> mustache_y_position; + } appearance_bits9; + union { + u16 raw; + + BitField<0, 4, u16> glasses_type; + BitField<4, 3, u16> glasses_color; + BitField<7, 4, u16> glasses_scale; + BitField<11, 5, u16> glasses_y_position; + } appearance_bits10; + union { + u16 raw; + + BitField<0, 1, u16> mole_enabled; + BitField<1, 4, u16> mole_scale; + BitField<5, 5, u16> mole_x_position; + BitField<10, 5, u16> mole_y_position; + } appearance_bits11; + + std::array<u16_le, 0xA> author_name; + INSERT_PADDING_BYTES(0x4); +}; +static_assert(sizeof(Ver3StoreData) == 0x60, "Ver3StoreData is an invalid size"); + struct MiiStoreData { using Name = std::array<char16_t, 10>; diff --git a/src/core/hle/service/nfp/amiibo_crypto.cpp b/src/core/hle/service/nfp/amiibo_crypto.cpp new file mode 100644 index 000000000..31dd3a307 --- /dev/null +++ b/src/core/hle/service/nfp/amiibo_crypto.cpp @@ -0,0 +1,383 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2017 socram8888/amiitool +// SPDX-License-Identifier: MIT + +#include <array> +#include <mbedtls/aes.h> +#include <mbedtls/hmac_drbg.h> + +#include "common/fs/file.h" +#include "common/fs/path_util.h" +#include "common/logging/log.h" +#include "core/hle/service/mii/mii_manager.h" +#include "core/hle/service/nfp/amiibo_crypto.h" + +namespace Service::NFP::AmiiboCrypto { + +bool IsAmiiboValid(const EncryptedNTAG215File& ntag_file) { + const auto& amiibo_data = ntag_file.user_memory; + LOG_DEBUG(Service_NFP, "uuid_lock=0x{0:x}", ntag_file.static_lock); + LOG_DEBUG(Service_NFP, "compability_container=0x{0:x}", ntag_file.compability_container); + LOG_INFO(Service_NFP, "write_count={}", amiibo_data.write_counter); + + LOG_INFO(Service_NFP, "character_id=0x{0:x}", amiibo_data.model_info.character_id); + LOG_INFO(Service_NFP, "character_variant={}", amiibo_data.model_info.character_variant); + LOG_INFO(Service_NFP, "amiibo_type={}", amiibo_data.model_info.amiibo_type); + LOG_INFO(Service_NFP, "model_number=0x{0:x}", amiibo_data.model_info.model_number); + LOG_INFO(Service_NFP, "series={}", amiibo_data.model_info.series); + LOG_DEBUG(Service_NFP, "fixed_value=0x{0:x}", amiibo_data.model_info.constant_value); + + LOG_DEBUG(Service_NFP, "tag_dynamic_lock=0x{0:x}", ntag_file.dynamic_lock); + LOG_DEBUG(Service_NFP, "tag_CFG0=0x{0:x}", ntag_file.CFG0); + LOG_DEBUG(Service_NFP, "tag_CFG1=0x{0:x}", ntag_file.CFG1); + + // Validate UUID + constexpr u8 CT = 0x88; // As defined in `ISO / IEC 14443 - 3` + if ((CT ^ ntag_file.uuid[0] ^ ntag_file.uuid[1] ^ ntag_file.uuid[2]) != ntag_file.uuid[3]) { + return false; + } + if ((ntag_file.uuid[4] ^ ntag_file.uuid[5] ^ ntag_file.uuid[6] ^ ntag_file.uuid[7]) != + ntag_file.uuid[8]) { + return false; + } + + // Check against all know constants on an amiibo binary + if (ntag_file.static_lock != 0xE00F) { + return false; + } + if (ntag_file.compability_container != 0xEEFF10F1U) { + return false; + } + if (amiibo_data.constant_value != 0xA5) { + return false; + } + if (amiibo_data.model_info.constant_value != 0x02) { + return false; + } + // dynamic_lock value apparently is not constant + // ntag_file.dynamic_lock == 0x0F0001 + if (ntag_file.CFG0 != 0x04000000U) { + return false; + } + if (ntag_file.CFG1 != 0x5F) { + return false; + } + return true; +} + +NTAG215File NfcDataToEncodedData(const EncryptedNTAG215File& nfc_data) { + NTAG215File encoded_data{}; + + memcpy(encoded_data.uuid2.data(), nfc_data.uuid.data() + 0x8, sizeof(encoded_data.uuid2)); + encoded_data.static_lock = nfc_data.static_lock; + encoded_data.compability_container = nfc_data.compability_container; + encoded_data.hmac_data = nfc_data.user_memory.hmac_data; + encoded_data.constant_value = nfc_data.user_memory.constant_value; + encoded_data.write_counter = nfc_data.user_memory.write_counter; + encoded_data.settings = nfc_data.user_memory.settings; + encoded_data.owner_mii = nfc_data.user_memory.owner_mii; + encoded_data.title_id = nfc_data.user_memory.title_id; + encoded_data.applicaton_write_counter = nfc_data.user_memory.applicaton_write_counter; + encoded_data.application_area_id = nfc_data.user_memory.application_area_id; + encoded_data.unknown = nfc_data.user_memory.unknown; + encoded_data.hash = nfc_data.user_memory.hash; + encoded_data.application_area = nfc_data.user_memory.application_area; + encoded_data.hmac_tag = nfc_data.user_memory.hmac_tag; + memcpy(encoded_data.uuid.data(), nfc_data.uuid.data(), sizeof(encoded_data.uuid)); + encoded_data.model_info = nfc_data.user_memory.model_info; + encoded_data.keygen_salt = nfc_data.user_memory.keygen_salt; + encoded_data.dynamic_lock = nfc_data.dynamic_lock; + encoded_data.CFG0 = nfc_data.CFG0; + encoded_data.CFG1 = nfc_data.CFG1; + encoded_data.password = nfc_data.password; + + return encoded_data; +} + +EncryptedNTAG215File EncodedDataToNfcData(const NTAG215File& encoded_data) { + EncryptedNTAG215File nfc_data{}; + + memcpy(nfc_data.uuid.data() + 0x8, encoded_data.uuid2.data(), sizeof(encoded_data.uuid2)); + memcpy(nfc_data.uuid.data(), encoded_data.uuid.data(), sizeof(encoded_data.uuid)); + nfc_data.static_lock = encoded_data.static_lock; + nfc_data.compability_container = encoded_data.compability_container; + nfc_data.user_memory.hmac_data = encoded_data.hmac_data; + nfc_data.user_memory.constant_value = encoded_data.constant_value; + nfc_data.user_memory.write_counter = encoded_data.write_counter; + nfc_data.user_memory.settings = encoded_data.settings; + nfc_data.user_memory.owner_mii = encoded_data.owner_mii; + nfc_data.user_memory.title_id = encoded_data.title_id; + nfc_data.user_memory.applicaton_write_counter = encoded_data.applicaton_write_counter; + nfc_data.user_memory.application_area_id = encoded_data.application_area_id; + nfc_data.user_memory.unknown = encoded_data.unknown; + nfc_data.user_memory.hash = encoded_data.hash; + nfc_data.user_memory.application_area = encoded_data.application_area; + nfc_data.user_memory.hmac_tag = encoded_data.hmac_tag; + nfc_data.user_memory.model_info = encoded_data.model_info; + nfc_data.user_memory.keygen_salt = encoded_data.keygen_salt; + nfc_data.dynamic_lock = encoded_data.dynamic_lock; + nfc_data.CFG0 = encoded_data.CFG0; + nfc_data.CFG1 = encoded_data.CFG1; + nfc_data.password = encoded_data.password; + + return nfc_data; +} + +u32 GetTagPassword(const TagUuid& uuid) { + // Verifiy that the generated password is correct + u32 password = 0xAA ^ (uuid[1] ^ uuid[3]); + password &= (0x55 ^ (uuid[2] ^ uuid[4])) << 8; + password &= (0xAA ^ (uuid[3] ^ uuid[5])) << 16; + password &= (0x55 ^ (uuid[4] ^ uuid[6])) << 24; + return password; +} + +HashSeed GetSeed(const NTAG215File& data) { + HashSeed seed{ + .magic = data.write_counter, + .padding = {}, + .uuid1 = {}, + .uuid2 = {}, + .keygen_salt = data.keygen_salt, + }; + + // Copy the first 8 bytes of uuid + memcpy(seed.uuid1.data(), data.uuid.data(), sizeof(seed.uuid1)); + memcpy(seed.uuid2.data(), data.uuid.data(), sizeof(seed.uuid2)); + + return seed; +} + +std::vector<u8> GenerateInternalKey(const InternalKey& key, const HashSeed& seed) { + const std::size_t seedPart1Len = sizeof(key.magic_bytes) - key.magic_length; + const std::size_t string_size = key.type_string.size(); + std::vector<u8> output(string_size + seedPart1Len); + + // Copy whole type string + memccpy(output.data(), key.type_string.data(), '\0', string_size); + + // Append (16 - magic_length) from the input seed + memcpy(output.data() + string_size, &seed, seedPart1Len); + + // Append all bytes from magicBytes + output.insert(output.end(), key.magic_bytes.begin(), + key.magic_bytes.begin() + key.magic_length); + + output.insert(output.end(), seed.uuid1.begin(), seed.uuid1.end()); + output.insert(output.end(), seed.uuid2.begin(), seed.uuid2.end()); + + for (std::size_t i = 0; i < sizeof(seed.keygen_salt); i++) { + output.emplace_back(static_cast<u8>(seed.keygen_salt[i] ^ key.xor_pad[i])); + } + + return output; +} + +void CryptoInit(CryptoCtx& ctx, mbedtls_md_context_t& hmac_ctx, const HmacKey& hmac_key, + const std::vector<u8>& seed) { + + // Initialize context + ctx.used = false; + ctx.counter = 0; + ctx.buffer_size = sizeof(ctx.counter) + seed.size(); + memcpy(ctx.buffer.data() + sizeof(u16), seed.data(), seed.size()); + + // Initialize HMAC context + mbedtls_md_init(&hmac_ctx); + mbedtls_md_setup(&hmac_ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 1); + mbedtls_md_hmac_starts(&hmac_ctx, hmac_key.data(), hmac_key.size()); +} + +void CryptoStep(CryptoCtx& ctx, mbedtls_md_context_t& hmac_ctx, DrgbOutput& output) { + // If used at least once, reinitialize the HMAC + if (ctx.used) { + mbedtls_md_hmac_reset(&hmac_ctx); + } + + ctx.used = true; + + // Store counter in big endian, and increment it + ctx.buffer[0] = static_cast<u8>(ctx.counter >> 8); + ctx.buffer[1] = static_cast<u8>(ctx.counter >> 0); + ctx.counter++; + + // Do HMAC magic + mbedtls_md_hmac_update(&hmac_ctx, reinterpret_cast<const unsigned char*>(ctx.buffer.data()), + ctx.buffer_size); + mbedtls_md_hmac_finish(&hmac_ctx, output.data()); +} + +DerivedKeys GenerateKey(const InternalKey& key, const NTAG215File& data) { + const auto seed = GetSeed(data); + + // Generate internal seed + const std::vector<u8> internal_key = GenerateInternalKey(key, seed); + + // Initialize context + CryptoCtx ctx{}; + mbedtls_md_context_t hmac_ctx; + CryptoInit(ctx, hmac_ctx, key.hmac_key, internal_key); + + // Generate derived keys + DerivedKeys derived_keys{}; + std::array<DrgbOutput, 2> temp{}; + CryptoStep(ctx, hmac_ctx, temp[0]); + CryptoStep(ctx, hmac_ctx, temp[1]); + memcpy(&derived_keys, temp.data(), sizeof(DerivedKeys)); + + // Cleanup context + mbedtls_md_free(&hmac_ctx); + + return derived_keys; +} + +void Cipher(const DerivedKeys& keys, const NTAG215File& in_data, NTAG215File& out_data) { + mbedtls_aes_context aes; + std::size_t nc_off = 0; + std::array<u8, sizeof(keys.aes_iv)> nonce_counter{}; + std::array<u8, sizeof(keys.aes_iv)> stream_block{}; + + const auto aes_key_size = static_cast<u32>(keys.aes_key.size() * 8); + mbedtls_aes_setkey_enc(&aes, keys.aes_key.data(), aes_key_size); + memcpy(nonce_counter.data(), keys.aes_iv.data(), sizeof(keys.aes_iv)); + + constexpr std::size_t encrypted_data_size = HMAC_TAG_START - SETTINGS_START; + mbedtls_aes_crypt_ctr(&aes, encrypted_data_size, &nc_off, nonce_counter.data(), + stream_block.data(), + reinterpret_cast<const unsigned char*>(&in_data.settings), + reinterpret_cast<unsigned char*>(&out_data.settings)); + + // Copy the rest of the data directly + out_data.uuid2 = in_data.uuid2; + out_data.static_lock = in_data.static_lock; + out_data.compability_container = in_data.compability_container; + + out_data.constant_value = in_data.constant_value; + out_data.write_counter = in_data.write_counter; + + out_data.uuid = in_data.uuid; + out_data.model_info = in_data.model_info; + out_data.keygen_salt = in_data.keygen_salt; + out_data.dynamic_lock = in_data.dynamic_lock; + out_data.CFG0 = in_data.CFG0; + out_data.CFG1 = in_data.CFG1; + out_data.password = in_data.password; +} + +bool LoadKeys(InternalKey& locked_secret, InternalKey& unfixed_info) { + const auto yuzu_keys_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::KeysDir); + + const Common::FS::IOFile keys_file{yuzu_keys_dir / "key_retail.bin", + Common::FS::FileAccessMode::Read, + Common::FS::FileType::BinaryFile}; + + if (!keys_file.IsOpen()) { + LOG_ERROR(Service_NFP, "No keys detected"); + return false; + } + + if (keys_file.Read(unfixed_info) != 1) { + LOG_ERROR(Service_NFP, "Failed to read unfixed_info"); + return false; + } + if (keys_file.Read(locked_secret) != 1) { + LOG_ERROR(Service_NFP, "Failed to read locked-secret"); + return false; + } + + return true; +} + +bool DecodeAmiibo(const EncryptedNTAG215File& encrypted_tag_data, NTAG215File& tag_data) { + InternalKey locked_secret{}; + InternalKey unfixed_info{}; + + if (!LoadKeys(locked_secret, unfixed_info)) { + return false; + } + + // Generate keys + NTAG215File encoded_data = NfcDataToEncodedData(encrypted_tag_data); + const auto data_keys = GenerateKey(unfixed_info, encoded_data); + const auto tag_keys = GenerateKey(locked_secret, encoded_data); + + // Decrypt + Cipher(data_keys, encoded_data, tag_data); + + // Regenerate tag HMAC. Note: order matters, data HMAC depends on tag HMAC! + constexpr std::size_t input_length = DYNAMIC_LOCK_START - UUID_START; + mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), tag_keys.hmac_key.data(), + sizeof(HmacKey), reinterpret_cast<const unsigned char*>(&tag_data.uuid), + input_length, reinterpret_cast<unsigned char*>(&tag_data.hmac_tag)); + + // Regenerate data HMAC + constexpr std::size_t input_length2 = DYNAMIC_LOCK_START - WRITE_COUNTER_START; + mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), data_keys.hmac_key.data(), + sizeof(HmacKey), + reinterpret_cast<const unsigned char*>(&tag_data.write_counter), input_length2, + reinterpret_cast<unsigned char*>(&tag_data.hmac_data)); + + if (tag_data.hmac_data != encrypted_tag_data.user_memory.hmac_data) { + LOG_ERROR(Service_NFP, "hmac_data doesn't match"); + return false; + } + + if (tag_data.hmac_tag != encrypted_tag_data.user_memory.hmac_tag) { + LOG_ERROR(Service_NFP, "hmac_tag doesn't match"); + return false; + } + + return true; +} + +bool EncodeAmiibo(const NTAG215File& tag_data, EncryptedNTAG215File& encrypted_tag_data) { + InternalKey locked_secret{}; + InternalKey unfixed_info{}; + + if (!LoadKeys(locked_secret, unfixed_info)) { + return false; + } + + // Generate keys + const auto data_keys = GenerateKey(unfixed_info, tag_data); + const auto tag_keys = GenerateKey(locked_secret, tag_data); + + NTAG215File encoded_tag_data{}; + + // Generate tag HMAC + constexpr std::size_t input_length = DYNAMIC_LOCK_START - UUID_START; + constexpr std::size_t input_length2 = HMAC_TAG_START - WRITE_COUNTER_START; + mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), tag_keys.hmac_key.data(), + sizeof(HmacKey), reinterpret_cast<const unsigned char*>(&tag_data.uuid), + input_length, reinterpret_cast<unsigned char*>(&encoded_tag_data.hmac_tag)); + + // Init mbedtls HMAC context + mbedtls_md_context_t ctx; + mbedtls_md_init(&ctx); + mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 1); + + // Generate data HMAC + mbedtls_md_hmac_starts(&ctx, data_keys.hmac_key.data(), sizeof(HmacKey)); + mbedtls_md_hmac_update(&ctx, reinterpret_cast<const unsigned char*>(&tag_data.write_counter), + input_length2); // Data + mbedtls_md_hmac_update(&ctx, reinterpret_cast<unsigned char*>(&encoded_tag_data.hmac_tag), + sizeof(HashData)); // Tag HMAC + mbedtls_md_hmac_update(&ctx, reinterpret_cast<const unsigned char*>(&tag_data.uuid), + input_length); + mbedtls_md_hmac_finish(&ctx, reinterpret_cast<unsigned char*>(&encoded_tag_data.hmac_data)); + + // HMAC cleanup + mbedtls_md_free(&ctx); + + // Encrypt + Cipher(data_keys, tag_data, encoded_tag_data); + + // Convert back to hardware + encrypted_tag_data = EncodedDataToNfcData(encoded_tag_data); + + return true; +} + +} // namespace Service::NFP::AmiiboCrypto diff --git a/src/core/hle/service/nfp/amiibo_crypto.h b/src/core/hle/service/nfp/amiibo_crypto.h new file mode 100644 index 000000000..af7335912 --- /dev/null +++ b/src/core/hle/service/nfp/amiibo_crypto.h @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <array> + +#include "core/hle/service/nfp/amiibo_types.h" + +struct mbedtls_md_context_t; + +namespace Service::NFP::AmiiboCrypto { +// Byte locations in Service::NFP::NTAG215File +constexpr std::size_t HMAC_DATA_START = 0x8; +constexpr std::size_t SETTINGS_START = 0x2c; +constexpr std::size_t WRITE_COUNTER_START = 0x29; +constexpr std::size_t HMAC_TAG_START = 0x1B4; +constexpr std::size_t UUID_START = 0x1D4; +constexpr std::size_t DYNAMIC_LOCK_START = 0x208; + +using HmacKey = std::array<u8, 0x10>; +using DrgbOutput = std::array<u8, 0x20>; + +struct HashSeed { + u16 magic; + std::array<u8, 0xE> padding; + std::array<u8, 0x8> uuid1; + std::array<u8, 0x8> uuid2; + std::array<u8, 0x20> keygen_salt; +}; +static_assert(sizeof(HashSeed) == 0x40, "HashSeed is an invalid size"); + +struct InternalKey { + HmacKey hmac_key; + std::array<char, 0xE> type_string; + u8 reserved; + u8 magic_length; + std::array<u8, 0x10> magic_bytes; + std::array<u8, 0x20> xor_pad; +}; +static_assert(sizeof(InternalKey) == 0x50, "InternalKey is an invalid size"); +static_assert(std::is_trivially_copyable_v<InternalKey>, "InternalKey must be trivially copyable."); + +struct CryptoCtx { + std::array<char, 480> buffer; + bool used; + std::size_t buffer_size; + s16 counter; +}; + +struct DerivedKeys { + std::array<u8, 0x10> aes_key; + std::array<u8, 0x10> aes_iv; + std::array<u8, 0x10> hmac_key; +}; +static_assert(sizeof(DerivedKeys) == 0x30, "DerivedKeys is an invalid size"); + +/// Validates that the amiibo file is not corrupted +bool IsAmiiboValid(const EncryptedNTAG215File& ntag_file); + +/// Converts from encrypted file format to encoded file format +NTAG215File NfcDataToEncodedData(const EncryptedNTAG215File& nfc_data); + +/// Converts from encoded file format to encrypted file format +EncryptedNTAG215File EncodedDataToNfcData(const NTAG215File& encoded_data); + +/// Returns password needed to allow write access to protected memory +u32 GetTagPassword(const TagUuid& uuid); + +// Generates Seed needed for key derivation +HashSeed GetSeed(const NTAG215File& data); + +// Middle step on the generation of derived keys +std::vector<u8> GenerateInternalKey(const InternalKey& key, const HashSeed& seed); + +// Initializes mbedtls context +void CryptoInit(CryptoCtx& ctx, mbedtls_md_context_t& hmac_ctx, const HmacKey& hmac_key, + const std::vector<u8>& seed); + +// Feeds data to mbedtls context to generate the derived key +void CryptoStep(CryptoCtx& ctx, mbedtls_md_context_t& hmac_ctx, DrgbOutput& output); + +// Generates the derived key from amiibo data +DerivedKeys GenerateKey(const InternalKey& key, const NTAG215File& data); + +// Encodes or decodes amiibo data +void Cipher(const DerivedKeys& keys, const NTAG215File& in_data, NTAG215File& out_data); + +/// Loads both amiibo keys from key_retail.bin +bool LoadKeys(InternalKey& locked_secret, InternalKey& unfixed_info); + +/// Decodes encripted amiibo data returns true if output is valid +bool DecodeAmiibo(const EncryptedNTAG215File& encrypted_tag_data, NTAG215File& tag_data); + +/// Encodes plain amiibo data returns true if output is valid +bool EncodeAmiibo(const NTAG215File& tag_data, EncryptedNTAG215File& encrypted_tag_data); + +} // namespace Service::NFP::AmiiboCrypto diff --git a/src/core/hle/service/nfp/amiibo_types.h b/src/core/hle/service/nfp/amiibo_types.h new file mode 100644 index 000000000..bf2de811a --- /dev/null +++ b/src/core/hle/service/nfp/amiibo_types.h @@ -0,0 +1,197 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <array> + +#include "core/hle/service/mii/types.h" + +namespace Service::NFP { +static constexpr std::size_t amiibo_name_length = 0xA; + +enum class ServiceType : u32 { + User, + Debug, + System, +}; + +enum class State : u32 { + NonInitialized, + Initialized, +}; + +enum class DeviceState : u32 { + Initialized, + SearchingForTag, + TagFound, + TagRemoved, + TagMounted, + Unaviable, + Finalized, +}; + +enum class ModelType : u32 { + Amiibo, +}; + +enum class MountTarget : u32 { + Rom, + Ram, + All, +}; + +enum class AmiiboType : u8 { + Figure, + Card, + Yarn, +}; + +enum class AmiiboSeries : u8 { + SuperSmashBros, + SuperMario, + ChibiRobo, + YoshiWoollyWorld, + Splatoon, + AnimalCrossing, + EightBitMario, + Skylanders, + Unknown8, + TheLegendOfZelda, + ShovelKnight, + Unknown11, + Kiby, + Pokemon, + MarioSportsSuperstars, + MonsterHunter, + BoxBoy, + Pikmin, + FireEmblem, + Metroid, + Others, + MegaMan, + Diablo, +}; + +using TagUuid = std::array<u8, 10>; +using HashData = std::array<u8, 0x20>; +using ApplicationArea = std::array<u8, 0xD8>; + +struct AmiiboDate { + u16 raw_date{}; + + u16 GetYear() const { + return static_cast<u16>(((raw_date & 0xFE00) >> 9) + 2000); + } + u8 GetMonth() const { + return static_cast<u8>(((raw_date & 0x01E0) >> 5) - 1); + } + u8 GetDay() const { + return static_cast<u8>(raw_date & 0x001F); + } +}; +static_assert(sizeof(AmiiboDate) == 2, "AmiiboDate is an invalid size"); + +struct Settings { + union { + u8 raw{}; + + BitField<4, 1, u8> amiibo_initialized; + BitField<5, 1, u8> appdata_initialized; + }; +}; +static_assert(sizeof(Settings) == 1, "AmiiboDate is an invalid size"); + +struct AmiiboSettings { + Settings settings; + u8 country_code_id; + u16_be crc_counter; // Incremented each time crc is changed + AmiiboDate init_date; + AmiiboDate write_date; + u32_be crc; + std::array<u16_be, amiibo_name_length> amiibo_name; // UTF-16 text +}; +static_assert(sizeof(AmiiboSettings) == 0x20, "AmiiboSettings is an invalid size"); + +struct AmiiboModelInfo { + u16 character_id; + u8 character_variant; + AmiiboType amiibo_type; + u16 model_number; + AmiiboSeries series; + u8 constant_value; // Must be 02 + INSERT_PADDING_BYTES(0x4); // Unknown +}; +static_assert(sizeof(AmiiboModelInfo) == 0xC, "AmiiboModelInfo is an invalid size"); + +struct NTAG215Password { + u32 PWD; // Password to allow write access + u16 PACK; // Password acknowledge reply + u16 RFUI; // Reserved for future use +}; +static_assert(sizeof(NTAG215Password) == 0x8, "NTAG215Password is an invalid size"); + +#pragma pack(1) +struct EncryptedAmiiboFile { + u8 constant_value; // Must be A5 + u16 write_counter; // Number of times the amiibo has been written? + INSERT_PADDING_BYTES(0x1); // Unknown 1 + AmiiboSettings settings; // Encrypted amiibo settings + HashData hmac_tag; // Hash + AmiiboModelInfo model_info; // Encrypted amiibo model info + HashData keygen_salt; // Salt + HashData hmac_data; // Hash + Service::Mii::Ver3StoreData owner_mii; // Encrypted Mii data + u64_be title_id; // Encrypted Game id + u16_be applicaton_write_counter; // Encrypted Counter + u32_be application_area_id; // Encrypted Game id + std::array<u8, 0x2> unknown; + HashData hash; // Probably a SHA256-HMAC hash? + ApplicationArea application_area; // Encrypted Game data +}; +static_assert(sizeof(EncryptedAmiiboFile) == 0x1F8, "AmiiboFile is an invalid size"); + +struct NTAG215File { + std::array<u8, 0x2> uuid2; + u16 static_lock; // Set defined pages as read only + u32 compability_container; // Defines available memory + HashData hmac_data; // Hash + u8 constant_value; // Must be A5 + u16 write_counter; // Number of times the amiibo has been written? + INSERT_PADDING_BYTES(0x1); // Unknown 1 + AmiiboSettings settings; + Service::Mii::Ver3StoreData owner_mii; // Encrypted Mii data + u64_be title_id; + u16_be applicaton_write_counter; // Encrypted Counter + u32_be application_area_id; + std::array<u8, 0x2> unknown; + HashData hash; // Probably a SHA256-HMAC hash? + ApplicationArea application_area; // Encrypted Game data + HashData hmac_tag; // Hash + std::array<u8, 0x8> uuid; + AmiiboModelInfo model_info; + HashData keygen_salt; // Salt + u32 dynamic_lock; // Dynamic lock + u32 CFG0; // Defines memory protected by password + u32 CFG1; // Defines number of verification attempts + NTAG215Password password; // Password data +}; +static_assert(sizeof(NTAG215File) == 0x21C, "NTAG215File is an invalid size"); +static_assert(std::is_trivially_copyable_v<NTAG215File>, "NTAG215File must be trivially copyable."); +#pragma pack() + +struct EncryptedNTAG215File { + TagUuid uuid; // Unique serial number + u16 static_lock; // Set defined pages as read only + u32 compability_container; // Defines available memory + EncryptedAmiiboFile user_memory; // Writable data + u32 dynamic_lock; // Dynamic lock + u32 CFG0; // Defines memory protected by password + u32 CFG1; // Defines number of verification attempts + NTAG215Password password; // Password data +}; +static_assert(sizeof(EncryptedNTAG215File) == 0x21C, "EncryptedNTAG215File is an invalid size"); +static_assert(std::is_trivially_copyable_v<EncryptedNTAG215File>, + "EncryptedNTAG215File must be trivially copyable."); + +} // namespace Service::NFP diff --git a/src/core/hle/service/nfp/nfp.cpp b/src/core/hle/service/nfp/nfp.cpp index 6c5b41dd1..e0ed3f771 100644 --- a/src/core/hle/service/nfp/nfp.cpp +++ b/src/core/hle/service/nfp/nfp.cpp @@ -4,7 +4,10 @@ #include <array> #include <atomic> +#include "common/fs/file.h" +#include "common/fs/path_util.h" #include "common/logging/log.h" +#include "common/string_util.h" #include "core/core.h" #include "core/hid/emulated_controller.h" #include "core/hid/hid_core.h" @@ -12,6 +15,7 @@ #include "core/hle/ipc_helpers.h" #include "core/hle/kernel/k_event.h" #include "core/hle/service/mii/mii_manager.h" +#include "core/hle/service/nfp/amiibo_crypto.h" #include "core/hle/service/nfp/nfp.h" #include "core/hle/service/nfp/nfp_user.h" @@ -19,12 +23,14 @@ namespace Service::NFP { namespace ErrCodes { constexpr Result DeviceNotFound(ErrorModule::NFP, 64); constexpr Result WrongDeviceState(ErrorModule::NFP, 73); +constexpr Result NfcDisabled(ErrorModule::NFP, 80); +constexpr Result WriteAmiiboFailed(ErrorModule::NFP, 88); +constexpr Result TagRemoved(ErrorModule::NFP, 97); constexpr Result ApplicationAreaIsNotInitialized(ErrorModule::NFP, 128); +constexpr Result WrongApplicationAreaId(ErrorModule::NFP, 152); constexpr Result ApplicationAreaExist(ErrorModule::NFP, 168); } // namespace ErrCodes -constexpr u32 ApplicationAreaSize = 0xD8; - IUser::IUser(Module::Interface& nfp_interface_, Core::System& system_) : ServiceFramework{system_, "NFP::IUser"}, service_context{system_, service_name}, nfp_interface{nfp_interface_} { @@ -39,7 +45,7 @@ IUser::IUser(Module::Interface& nfp_interface_, Core::System& system_) {7, &IUser::OpenApplicationArea, "OpenApplicationArea"}, {8, &IUser::GetApplicationArea, "GetApplicationArea"}, {9, &IUser::SetApplicationArea, "SetApplicationArea"}, - {10, nullptr, "Flush"}, + {10, &IUser::Flush, "Flush"}, {11, nullptr, "Restore"}, {12, &IUser::CreateApplicationArea, "CreateApplicationArea"}, {13, &IUser::GetTagInfo, "GetTagInfo"}, @@ -53,7 +59,7 @@ IUser::IUser(Module::Interface& nfp_interface_, Core::System& system_) {21, &IUser::GetNpadId, "GetNpadId"}, {22, &IUser::GetApplicationAreaSize, "GetApplicationAreaSize"}, {23, &IUser::AttachAvailabilityChangeEvent, "AttachAvailabilityChangeEvent"}, - {24, nullptr, "RecreateApplicationArea"}, + {24, &IUser::RecreateApplicationArea, "RecreateApplicationArea"}, }; RegisterHandlers(functions); @@ -87,11 +93,23 @@ void IUser::Finalize(Kernel::HLERequestContext& ctx) { void IUser::ListDevices(Kernel::HLERequestContext& ctx) { LOG_INFO(Service_NFP, "called"); + if (state == State::NonInitialized) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ErrCodes::NfcDisabled); + return; + } + std::vector<u64> devices; // TODO(german77): Loop through all interfaces devices.push_back(nfp_interface.GetHandle()); + if (devices.size() == 0) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ErrCodes::DeviceNotFound); + return; + } + ctx.WriteBuffer(devices); IPC::ResponseBuilder rb{ctx, 3}; @@ -105,6 +123,12 @@ void IUser::StartDetection(Kernel::HLERequestContext& ctx) { const auto nfp_protocol{rp.Pop<s32>()}; LOG_INFO(Service_NFP, "called, device_handle={}, nfp_protocol={}", device_handle, nfp_protocol); + if (state == State::NonInitialized) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ErrCodes::NfcDisabled); + return; + } + // TODO(german77): Loop through all interfaces if (device_handle == nfp_interface.GetHandle()) { const auto result = nfp_interface.StartDetection(nfp_protocol); @@ -124,6 +148,12 @@ void IUser::StopDetection(Kernel::HLERequestContext& ctx) { const auto device_handle{rp.Pop<u64>()}; LOG_INFO(Service_NFP, "called, device_handle={}", device_handle); + if (state == State::NonInitialized) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ErrCodes::NfcDisabled); + return; + } + // TODO(german77): Loop through all interfaces if (device_handle == nfp_interface.GetHandle()) { const auto result = nfp_interface.StopDetection(); @@ -146,6 +176,12 @@ void IUser::Mount(Kernel::HLERequestContext& ctx) { LOG_INFO(Service_NFP, "called, device_handle={}, model_type={}, mount_target={}", device_handle, model_type, mount_target); + if (state == State::NonInitialized) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ErrCodes::NfcDisabled); + return; + } + // TODO(german77): Loop through all interfaces if (device_handle == nfp_interface.GetHandle()) { const auto result = nfp_interface.Mount(); @@ -165,6 +201,12 @@ void IUser::Unmount(Kernel::HLERequestContext& ctx) { const auto device_handle{rp.Pop<u64>()}; LOG_INFO(Service_NFP, "called, device_handle={}", device_handle); + if (state == State::NonInitialized) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ErrCodes::NfcDisabled); + return; + } + // TODO(german77): Loop through all interfaces if (device_handle == nfp_interface.GetHandle()) { const auto result = nfp_interface.Unmount(); @@ -186,6 +228,12 @@ void IUser::OpenApplicationArea(Kernel::HLERequestContext& ctx) { LOG_WARNING(Service_NFP, "(STUBBED) called, device_handle={}, access_id={}", device_handle, access_id); + if (state == State::NonInitialized) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ErrCodes::NfcDisabled); + return; + } + // TODO(german77): Loop through all interfaces if (device_handle == nfp_interface.GetHandle()) { const auto result = nfp_interface.OpenApplicationArea(access_id); @@ -205,9 +253,15 @@ void IUser::GetApplicationArea(Kernel::HLERequestContext& ctx) { const auto device_handle{rp.Pop<u64>()}; LOG_INFO(Service_NFP, "called, device_handle={}", device_handle); + if (state == State::NonInitialized) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ErrCodes::NfcDisabled); + return; + } + // TODO(german77): Loop through all interfaces if (device_handle == nfp_interface.GetHandle()) { - std::vector<u8> data{}; + ApplicationArea data{}; const auto result = nfp_interface.GetApplicationArea(data); ctx.WriteBuffer(data); IPC::ResponseBuilder rb{ctx, 3}; @@ -229,6 +283,12 @@ void IUser::SetApplicationArea(Kernel::HLERequestContext& ctx) { LOG_WARNING(Service_NFP, "(STUBBED) called, device_handle={}, data_size={}", device_handle, data.size()); + if (state == State::NonInitialized) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ErrCodes::NfcDisabled); + return; + } + // TODO(german77): Loop through all interfaces if (device_handle == nfp_interface.GetHandle()) { const auto result = nfp_interface.SetApplicationArea(data); @@ -243,6 +303,31 @@ void IUser::SetApplicationArea(Kernel::HLERequestContext& ctx) { rb.Push(ErrCodes::DeviceNotFound); } +void IUser::Flush(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto device_handle{rp.Pop<u64>()}; + LOG_WARNING(Service_NFP, "(STUBBED) called, device_handle={}", device_handle); + + if (state == State::NonInitialized) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ErrCodes::NfcDisabled); + return; + } + + // TODO(german77): Loop through all interfaces + if (device_handle == nfp_interface.GetHandle()) { + const auto result = nfp_interface.Flush(); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + return; + } + + LOG_ERROR(Service_NFP, "Handle not found, device_handle={}", device_handle); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ErrCodes::DeviceNotFound); +} + void IUser::CreateApplicationArea(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; const auto device_handle{rp.Pop<u64>()}; @@ -251,6 +336,12 @@ void IUser::CreateApplicationArea(Kernel::HLERequestContext& ctx) { LOG_WARNING(Service_NFP, "(STUBBED) called, device_handle={}, data_size={}, access_id={}", device_handle, access_id, data.size()); + if (state == State::NonInitialized) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ErrCodes::NfcDisabled); + return; + } + // TODO(german77): Loop through all interfaces if (device_handle == nfp_interface.GetHandle()) { const auto result = nfp_interface.CreateApplicationArea(access_id, data); @@ -270,6 +361,12 @@ void IUser::GetTagInfo(Kernel::HLERequestContext& ctx) { const auto device_handle{rp.Pop<u64>()}; LOG_INFO(Service_NFP, "called, device_handle={}", device_handle); + if (state == State::NonInitialized) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ErrCodes::NfcDisabled); + return; + } + // TODO(german77): Loop through all interfaces if (device_handle == nfp_interface.GetHandle()) { TagInfo tag_info{}; @@ -291,6 +388,12 @@ void IUser::GetRegisterInfo(Kernel::HLERequestContext& ctx) { const auto device_handle{rp.Pop<u64>()}; LOG_INFO(Service_NFP, "called, device_handle={}", device_handle); + if (state == State::NonInitialized) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ErrCodes::NfcDisabled); + return; + } + // TODO(german77): Loop through all interfaces if (device_handle == nfp_interface.GetHandle()) { RegisterInfo register_info{}; @@ -312,6 +415,12 @@ void IUser::GetCommonInfo(Kernel::HLERequestContext& ctx) { const auto device_handle{rp.Pop<u64>()}; LOG_INFO(Service_NFP, "called, device_handle={}", device_handle); + if (state == State::NonInitialized) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ErrCodes::NfcDisabled); + return; + } + // TODO(german77): Loop through all interfaces if (device_handle == nfp_interface.GetHandle()) { CommonInfo common_info{}; @@ -333,6 +442,12 @@ void IUser::GetModelInfo(Kernel::HLERequestContext& ctx) { const auto device_handle{rp.Pop<u64>()}; LOG_INFO(Service_NFP, "called, device_handle={}", device_handle); + if (state == State::NonInitialized) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ErrCodes::NfcDisabled); + return; + } + // TODO(german77): Loop through all interfaces if (device_handle == nfp_interface.GetHandle()) { ModelInfo model_info{}; @@ -354,6 +469,12 @@ void IUser::AttachActivateEvent(Kernel::HLERequestContext& ctx) { const auto device_handle{rp.Pop<u64>()}; LOG_DEBUG(Service_NFP, "called, device_handle={}", device_handle); + if (state == State::NonInitialized) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ErrCodes::NfcDisabled); + return; + } + // TODO(german77): Loop through all interfaces if (device_handle == nfp_interface.GetHandle()) { IPC::ResponseBuilder rb{ctx, 2, 1}; @@ -373,6 +494,12 @@ void IUser::AttachDeactivateEvent(Kernel::HLERequestContext& ctx) { const auto device_handle{rp.Pop<u64>()}; LOG_DEBUG(Service_NFP, "called, device_handle={}", device_handle); + if (state == State::NonInitialized) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ErrCodes::NfcDisabled); + return; + } + // TODO(german77): Loop through all interfaces if (device_handle == nfp_interface.GetHandle()) { IPC::ResponseBuilder rb{ctx, 2, 1}; @@ -419,6 +546,12 @@ void IUser::GetNpadId(Kernel::HLERequestContext& ctx) { const auto device_handle{rp.Pop<u64>()}; LOG_DEBUG(Service_NFP, "called, device_handle={}", device_handle); + if (state == State::NonInitialized) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ErrCodes::NfcDisabled); + return; + } + // TODO(german77): Loop through all interfaces if (device_handle == nfp_interface.GetHandle()) { IPC::ResponseBuilder rb{ctx, 3}; @@ -442,7 +575,7 @@ void IUser::GetApplicationAreaSize(Kernel::HLERequestContext& ctx) { if (device_handle == nfp_interface.GetHandle()) { IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(ApplicationAreaSize); + rb.Push(sizeof(ApplicationArea)); return; } @@ -455,11 +588,45 @@ void IUser::GetApplicationAreaSize(Kernel::HLERequestContext& ctx) { void IUser::AttachAvailabilityChangeEvent(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_NFP, "(STUBBED) called"); + if (state == State::NonInitialized) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ErrCodes::NfcDisabled); + return; + } + IPC::ResponseBuilder rb{ctx, 2, 1}; rb.Push(ResultSuccess); rb.PushCopyObjects(availability_change_event->GetReadableEvent()); } +void IUser::RecreateApplicationArea(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto device_handle{rp.Pop<u64>()}; + const auto access_id{rp.Pop<u32>()}; + const auto data{ctx.ReadBuffer()}; + LOG_WARNING(Service_NFP, "(STUBBED) called, device_handle={}, data_size={}, access_id={}", + device_handle, access_id, data.size()); + + if (state == State::NonInitialized) { + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ErrCodes::NfcDisabled); + return; + } + + // TODO(german77): Loop through all interfaces + if (device_handle == nfp_interface.GetHandle()) { + const auto result = nfp_interface.RecreateApplicationArea(access_id, data); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(result); + return; + } + + LOG_ERROR(Service_NFP, "Handle not found, device_handle={}", device_handle); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ErrCodes::DeviceNotFound); +} + Module::Interface::Interface(std::shared_ptr<Module> module_, Core::System& system_, const char* name) : ServiceFramework{system_, name}, module{std::move(module_)}, @@ -478,36 +645,42 @@ void Module::Interface::CreateUserInterface(Kernel::HLERequestContext& ctx) { rb.PushIpcInterface<IUser>(*this, system); } -bool Module::Interface::LoadAmiibo(const std::vector<u8>& buffer) { - if (device_state != DeviceState::SearchingForTag) { - LOG_ERROR(Service_NFP, "Game is not looking for amiibos, current state {}", device_state); - return false; - } - - constexpr auto tag_size = sizeof(NTAG215File); +bool Module::Interface::LoadAmiiboFile(const std::string& filename) { constexpr auto tag_size_without_password = sizeof(NTAG215File) - sizeof(NTAG215Password); + const Common::FS::IOFile amiibo_file{filename, Common::FS::FileAccessMode::Read, + Common::FS::FileType::BinaryFile}; - std::vector<u8> amiibo_buffer = buffer; + if (!amiibo_file.IsOpen()) { + LOG_ERROR(Service_NFP, "Amiibo is already on use"); + return false; + } - if (amiibo_buffer.size() < tag_size_without_password) { - LOG_ERROR(Service_NFP, "Wrong file size {}", buffer.size()); + // Workaround for files with missing password data + std::array<u8, sizeof(EncryptedNTAG215File)> buffer{}; + if (amiibo_file.Read(buffer) < tag_size_without_password) { + LOG_ERROR(Service_NFP, "Failed to read amiibo file"); return false; } + memcpy(&encrypted_tag_data, buffer.data(), sizeof(EncryptedNTAG215File)); - // Ensure it has the correct size - if (amiibo_buffer.size() != tag_size) { - amiibo_buffer.resize(tag_size, 0); + if (!AmiiboCrypto::IsAmiiboValid(encrypted_tag_data)) { + LOG_INFO(Service_NFP, "Invalid amiibo"); + return false; } - LOG_INFO(Service_NFP, "Amiibo detected"); - std::memcpy(&tag_data, buffer.data(), tag_size); + file_path = filename; + return true; +} - if (!IsAmiiboValid()) { +bool Module::Interface::LoadAmiibo(const std::string& filename) { + if (device_state != DeviceState::SearchingForTag) { + LOG_ERROR(Service_NFP, "Game is not looking for amiibos, current state {}", device_state); return false; } - // This value can't be dumped from a tag. Generate it - tag_data.password.PWD = GetTagPassword(tag_data.uuid); + if (!LoadAmiiboFile(filename)) { + return false; + } device_state = DeviceState::TagFound; activate_event->GetWritableEvent().Signal(); @@ -517,55 +690,13 @@ bool Module::Interface::LoadAmiibo(const std::vector<u8>& buffer) { void Module::Interface::CloseAmiibo() { LOG_INFO(Service_NFP, "Remove amiibo"); device_state = DeviceState::TagRemoved; + is_data_decoded = false; is_application_area_initialized = false; - application_area_id = 0; - application_area_data.clear(); + encrypted_tag_data = {}; + tag_data = {}; deactivate_event->GetWritableEvent().Signal(); } -bool Module::Interface::IsAmiiboValid() const { - const auto& amiibo_data = tag_data.user_memory; - LOG_DEBUG(Service_NFP, "uuid_lock=0x{0:x}", tag_data.lock_bytes); - LOG_DEBUG(Service_NFP, "compability_container=0x{0:x}", tag_data.compability_container); - LOG_DEBUG(Service_NFP, "crypto_init=0x{0:x}", amiibo_data.crypto_init); - LOG_DEBUG(Service_NFP, "write_count={}", amiibo_data.write_count); - - LOG_DEBUG(Service_NFP, "character_id=0x{0:x}", amiibo_data.model_info.character_id); - LOG_DEBUG(Service_NFP, "character_variant={}", amiibo_data.model_info.character_variant); - LOG_DEBUG(Service_NFP, "amiibo_type={}", amiibo_data.model_info.amiibo_type); - LOG_DEBUG(Service_NFP, "model_number=0x{0:x}", amiibo_data.model_info.model_number); - LOG_DEBUG(Service_NFP, "series={}", amiibo_data.model_info.series); - LOG_DEBUG(Service_NFP, "fixed_value=0x{0:x}", amiibo_data.model_info.fixed); - - LOG_DEBUG(Service_NFP, "tag_dynamic_lock=0x{0:x}", tag_data.dynamic_lock); - LOG_DEBUG(Service_NFP, "tag_CFG0=0x{0:x}", tag_data.CFG0); - LOG_DEBUG(Service_NFP, "tag_CFG1=0x{0:x}", tag_data.CFG1); - - // Check against all know constants on an amiibo binary - if (tag_data.lock_bytes != 0xE00F) { - return false; - } - if (tag_data.compability_container != 0xEEFF10F1U) { - return false; - } - if ((amiibo_data.crypto_init & 0xFF) != 0xA5) { - return false; - } - if (amiibo_data.model_info.fixed != 0x02) { - return false; - } - if ((tag_data.dynamic_lock & 0xFFFFFF) != 0x0F0001) { - return false; - } - if (tag_data.CFG0 != 0x04000000U) { - return false; - } - if (tag_data.CFG1 != 0x5F) { - return false; - } - return true; -} - Kernel::KReadableEvent& Module::Interface::GetActivateEvent() const { return activate_event->GetReadableEvent(); } @@ -576,13 +707,20 @@ Kernel::KReadableEvent& Module::Interface::GetDeactivateEvent() const { void Module::Interface::Initialize() { device_state = DeviceState::Initialized; + is_data_decoded = false; + is_application_area_initialized = false; + encrypted_tag_data = {}; + tag_data = {}; } void Module::Interface::Finalize() { + if (device_state == DeviceState::TagMounted) { + Unmount(); + } + if (device_state == DeviceState::SearchingForTag || device_state == DeviceState::TagRemoved) { + StopDetection(); + } device_state = DeviceState::Unaviable; - is_application_area_initialized = false; - application_area_id = 0; - application_area_data.clear(); } Result Module::Interface::StartDetection(s32 protocol_) { @@ -618,42 +756,102 @@ Result Module::Interface::StopDetection() { return ErrCodes::WrongDeviceState; } -Result Module::Interface::Mount() { - if (device_state == DeviceState::TagFound) { - device_state = DeviceState::TagMounted; +Result Module::Interface::Flush() { + // Ignore write command if we can't encrypt the data + if (!is_data_decoded) { return ResultSuccess; } - LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); - return ErrCodes::WrongDeviceState; + constexpr auto tag_size_without_password = sizeof(NTAG215File) - sizeof(NTAG215Password); + EncryptedNTAG215File tmp_encrypted_tag_data{}; + const Common::FS::IOFile amiibo_file{file_path, Common::FS::FileAccessMode::ReadWrite, + Common::FS::FileType::BinaryFile}; + + if (!amiibo_file.IsOpen()) { + LOG_ERROR(Core, "Amiibo is already on use"); + return ErrCodes::WriteAmiiboFailed; + } + + // Workaround for files with missing password data + std::array<u8, sizeof(EncryptedNTAG215File)> buffer{}; + if (amiibo_file.Read(buffer) < tag_size_without_password) { + LOG_ERROR(Core, "Failed to read amiibo file"); + return ErrCodes::WriteAmiiboFailed; + } + memcpy(&tmp_encrypted_tag_data, buffer.data(), sizeof(EncryptedNTAG215File)); + + if (!AmiiboCrypto::IsAmiiboValid(tmp_encrypted_tag_data)) { + LOG_INFO(Service_NFP, "Invalid amiibo"); + return ErrCodes::WriteAmiiboFailed; + } + + bool is_uuid_equal = memcmp(tmp_encrypted_tag_data.uuid.data(), tag_data.uuid.data(), 8) == 0; + bool is_character_equal = tmp_encrypted_tag_data.user_memory.model_info.character_id == + tag_data.model_info.character_id; + if (!is_uuid_equal || !is_character_equal) { + LOG_ERROR(Service_NFP, "Not the same amiibo"); + return ErrCodes::WriteAmiiboFailed; + } + + if (!AmiiboCrypto::EncodeAmiibo(tag_data, encrypted_tag_data)) { + LOG_ERROR(Service_NFP, "Failed to encode data"); + return ErrCodes::WriteAmiiboFailed; + } + + // Return to the start of the file + if (!amiibo_file.Seek(0)) { + LOG_ERROR(Service_NFP, "Error writting to file"); + return ErrCodes::WriteAmiiboFailed; + } + + if (!amiibo_file.Write(encrypted_tag_data)) { + LOG_ERROR(Service_NFP, "Error writting to file"); + return ErrCodes::WriteAmiiboFailed; + } + + return ResultSuccess; +} + +Result Module::Interface::Mount() { + if (device_state != DeviceState::TagFound) { + LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); + return ErrCodes::WrongDeviceState; + } + + is_data_decoded = AmiiboCrypto::DecodeAmiibo(encrypted_tag_data, tag_data); + LOG_INFO(Service_NFP, "Is amiibo decoded {}", is_data_decoded); + + is_application_area_initialized = false; + device_state = DeviceState::TagMounted; + return ResultSuccess; } Result Module::Interface::Unmount() { - if (device_state == DeviceState::TagMounted) { - is_application_area_initialized = false; - application_area_id = 0; - application_area_data.clear(); - device_state = DeviceState::TagFound; - return ResultSuccess; + if (device_state != DeviceState::TagMounted) { + LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); + return ErrCodes::WrongDeviceState; } - LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); - return ErrCodes::WrongDeviceState; + is_data_decoded = false; + is_application_area_initialized = false; + device_state = DeviceState::TagFound; + return ResultSuccess; } Result Module::Interface::GetTagInfo(TagInfo& tag_info) const { - if (device_state == DeviceState::TagFound || device_state == DeviceState::TagMounted) { - tag_info = { - .uuid = tag_data.uuid, - .uuid_length = static_cast<u8>(tag_data.uuid.size()), - .protocol = protocol, - .tag_type = static_cast<u32>(tag_data.user_memory.model_info.amiibo_type), - }; - return ResultSuccess; + if (device_state != DeviceState::TagFound && device_state != DeviceState::TagMounted) { + LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); + return ErrCodes::WrongDeviceState; } - LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); - return ErrCodes::WrongDeviceState; + tag_info = { + .uuid = encrypted_tag_data.uuid, + .uuid_length = static_cast<u8>(encrypted_tag_data.uuid.size()), + .protocol = protocol, + .tag_type = static_cast<u32>(encrypted_tag_data.user_memory.model_info.amiibo_type), + }; + + return ResultSuccess; } Result Module::Interface::GetCommonInfo(CommonInfo& common_info) const { @@ -662,14 +860,28 @@ Result Module::Interface::GetCommonInfo(CommonInfo& common_info) const { return ErrCodes::WrongDeviceState; } - // Read this data from the amiibo save file + if (is_data_decoded && tag_data.settings.settings.amiibo_initialized != 0) { + const auto& settings = tag_data.settings; + // TODO: Validate this data + common_info = { + .last_write_year = settings.write_date.GetYear(), + .last_write_month = settings.write_date.GetMonth(), + .last_write_day = settings.write_date.GetDay(), + .write_counter = settings.crc_counter, + .version = 1, + .application_area_size = sizeof(ApplicationArea), + }; + return ResultSuccess; + } + + // Generate a generic answer common_info = { .last_write_year = 2022, .last_write_month = 2, .last_write_day = 7, - .write_counter = tag_data.user_memory.write_count, + .write_counter = 0, .version = 1, - .application_area_size = ApplicationAreaSize, + .application_area_size = sizeof(ApplicationArea), }; return ResultSuccess; } @@ -680,26 +892,53 @@ Result Module::Interface::GetModelInfo(ModelInfo& model_info) const { return ErrCodes::WrongDeviceState; } - model_info = tag_data.user_memory.model_info; + const auto& model_info_data = encrypted_tag_data.user_memory.model_info; + model_info = { + .character_id = model_info_data.character_id, + .character_variant = model_info_data.character_variant, + .amiibo_type = model_info_data.amiibo_type, + .model_number = model_info_data.model_number, + .series = model_info_data.series, + .constant_value = model_info_data.constant_value, + }; return ResultSuccess; } Result Module::Interface::GetRegisterInfo(RegisterInfo& register_info) const { if (device_state != DeviceState::TagMounted) { LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); + if (device_state == DeviceState::TagRemoved) { + return ErrCodes::TagRemoved; + } return ErrCodes::WrongDeviceState; } Service::Mii::MiiManager manager; - // Read this data from the amiibo save file + if (is_data_decoded && tag_data.settings.settings.amiibo_initialized != 0) { + const auto& settings = tag_data.settings; + + // TODO: Validate this data + register_info = { + .mii_char_info = manager.ConvertV3ToCharInfo(tag_data.owner_mii), + .first_write_year = settings.init_date.GetYear(), + .first_write_month = settings.init_date.GetMonth(), + .first_write_day = settings.init_date.GetDay(), + .amiibo_name = GetAmiiboName(settings), + .font_region = {}, + }; + + return ResultSuccess; + } + + // Generate a generic answer register_info = { .mii_char_info = manager.BuildDefault(0), .first_write_year = 2022, .first_write_month = 2, .first_write_day = 7, .amiibo_name = {'Y', 'u', 'z', 'u', 'A', 'm', 'i', 'i', 'b', 'o', 0}, - .unknown = {}, + .font_region = {}, }; return ResultSuccess; } @@ -707,31 +946,47 @@ Result Module::Interface::GetRegisterInfo(RegisterInfo& register_info) const { Result Module::Interface::OpenApplicationArea(u32 access_id) { if (device_state != DeviceState::TagMounted) { LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); + if (device_state == DeviceState::TagRemoved) { + return ErrCodes::TagRemoved; + } return ErrCodes::WrongDeviceState; } - if (AmiiboApplicationDataExist(access_id)) { - application_area_data = LoadAmiiboApplicationData(access_id); - application_area_id = access_id; - is_application_area_initialized = true; + + // Fallback for lack of amiibo keys + if (!is_data_decoded) { + LOG_WARNING(Service_NFP, "Application area is not initialized"); + return ErrCodes::ApplicationAreaIsNotInitialized; } - if (!is_application_area_initialized) { + + if (tag_data.settings.settings.appdata_initialized == 0) { LOG_WARNING(Service_NFP, "Application area is not initialized"); return ErrCodes::ApplicationAreaIsNotInitialized; } + + if (tag_data.application_area_id != access_id) { + LOG_WARNING(Service_NFP, "Wrong application area id"); + return ErrCodes::WrongApplicationAreaId; + } + + is_application_area_initialized = true; return ResultSuccess; } -Result Module::Interface::GetApplicationArea(std::vector<u8>& data) const { +Result Module::Interface::GetApplicationArea(ApplicationArea& data) const { if (device_state != DeviceState::TagMounted) { LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); + if (device_state == DeviceState::TagRemoved) { + return ErrCodes::TagRemoved; + } return ErrCodes::WrongDeviceState; } + if (!is_application_area_initialized) { LOG_ERROR(Service_NFP, "Application area is not initialized"); return ErrCodes::ApplicationAreaIsNotInitialized; } - data = application_area_data; + data = tag_data.application_area; return ResultSuccess; } @@ -739,46 +994,69 @@ Result Module::Interface::GetApplicationArea(std::vector<u8>& data) const { Result Module::Interface::SetApplicationArea(const std::vector<u8>& data) { if (device_state != DeviceState::TagMounted) { LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); + if (device_state == DeviceState::TagRemoved) { + return ErrCodes::TagRemoved; + } return ErrCodes::WrongDeviceState; } + if (!is_application_area_initialized) { LOG_ERROR(Service_NFP, "Application area is not initialized"); return ErrCodes::ApplicationAreaIsNotInitialized; } - application_area_data = data; - SaveAmiiboApplicationData(application_area_id, application_area_data); + + if (data.size() != sizeof(ApplicationArea)) { + LOG_ERROR(Service_NFP, "Wrong data size {}", data.size()); + return ResultUnknown; + } + + std::memcpy(&tag_data.application_area, data.data(), sizeof(ApplicationArea)); return ResultSuccess; } Result Module::Interface::CreateApplicationArea(u32 access_id, const std::vector<u8>& data) { if (device_state != DeviceState::TagMounted) { LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); + if (device_state == DeviceState::TagRemoved) { + return ErrCodes::TagRemoved; + } return ErrCodes::WrongDeviceState; } - if (AmiiboApplicationDataExist(access_id)) { + + if (tag_data.settings.settings.appdata_initialized != 0) { LOG_ERROR(Service_NFP, "Application area already exist"); return ErrCodes::ApplicationAreaExist; } - application_area_data = data; - application_area_id = access_id; - SaveAmiiboApplicationData(application_area_id, application_area_data); + + if (data.size() != sizeof(ApplicationArea)) { + LOG_ERROR(Service_NFP, "Wrong data size {}", data.size()); + return ResultUnknown; + } + + std::memcpy(&tag_data.application_area, data.data(), sizeof(ApplicationArea)); + tag_data.application_area_id = access_id; + return ResultSuccess; } -bool Module::Interface::AmiiboApplicationDataExist(u32 access_id) const { - // TODO(german77): Check if file exist - return false; -} +Result Module::Interface::RecreateApplicationArea(u32 access_id, const std::vector<u8>& data) { + if (device_state != DeviceState::TagMounted) { + LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); + if (device_state == DeviceState::TagRemoved) { + return ErrCodes::TagRemoved; + } + return ErrCodes::WrongDeviceState; + } -std::vector<u8> Module::Interface::LoadAmiiboApplicationData(u32 access_id) const { - // TODO(german77): Read file - std::vector<u8> data(ApplicationAreaSize); - return data; -} + if (data.size() != sizeof(ApplicationArea)) { + LOG_ERROR(Service_NFP, "Wrong data size {}", data.size()); + return ResultUnknown; + } + + std::memcpy(&tag_data.application_area, data.data(), sizeof(ApplicationArea)); + tag_data.application_area_id = access_id; -void Module::Interface::SaveAmiiboApplicationData(u32 access_id, - const std::vector<u8>& data) const { - // TODO(german77): Save file + return ResultSuccess; } u64 Module::Interface::GetHandle() const { @@ -791,16 +1069,25 @@ DeviceState Module::Interface::GetCurrentState() const { } Core::HID::NpadIdType Module::Interface::GetNpadId() const { - return npad_id; + // Return first connected npad id as a workaround for lack of a single nfc interface per + // controller + return system.HIDCore().GetFirstNpadId(); } -u32 Module::Interface::GetTagPassword(const TagUuid& uuid) const { - // Verifiy that the generated password is correct - u32 password = 0xAA ^ (uuid[1] ^ uuid[3]); - password &= (0x55 ^ (uuid[2] ^ uuid[4])) << 8; - password &= (0xAA ^ (uuid[3] ^ uuid[5])) << 16; - password &= (0x55 ^ (uuid[4] ^ uuid[6])) << 24; - return password; +AmiiboName Module::Interface::GetAmiiboName(const AmiiboSettings& settings) const { + std::array<char16_t, amiibo_name_length> settings_amiibo_name{}; + AmiiboName amiibo_name{}; + + // Convert from big endian to little endian + for (std::size_t i = 0; i < amiibo_name_length; i++) { + settings_amiibo_name[i] = static_cast<u16>(settings.amiibo_name[i]); + } + + // Convert from utf16 to utf8 + const auto amiibo_name_utf8 = Common::UTF16ToUTF8(settings_amiibo_name.data()); + memcpy(amiibo_name.data(), amiibo_name_utf8.data(), amiibo_name_utf8.size()); + + return amiibo_name; } void InstallInterfaces(SM::ServiceManager& service_manager, Core::System& system) { diff --git a/src/core/hle/service/nfp/nfp.h b/src/core/hle/service/nfp/nfp.h index 0fc808781..0de0b48e7 100644 --- a/src/core/hle/service/nfp/nfp.h +++ b/src/core/hle/service/nfp/nfp.h @@ -9,6 +9,7 @@ #include "common/common_funcs.h" #include "core/hle/service/kernel_helpers.h" #include "core/hle/service/mii/types.h" +#include "core/hle/service/nfp/amiibo_types.h" #include "core/hle/service/service.h" namespace Kernel { @@ -21,71 +22,7 @@ enum class NpadIdType : u32; } // namespace Core::HID namespace Service::NFP { - -enum class ServiceType : u32 { - User, - Debug, - System, -}; - -enum class State : u32 { - NonInitialized, - Initialized, -}; - -enum class DeviceState : u32 { - Initialized, - SearchingForTag, - TagFound, - TagRemoved, - TagMounted, - Unaviable, - Finalized, -}; - -enum class ModelType : u32 { - Amiibo, -}; - -enum class MountTarget : u32 { - Rom, - Ram, - All, -}; - -enum class AmiiboType : u8 { - Figure, - Card, - Yarn, -}; - -enum class AmiiboSeries : u8 { - SuperSmashBros, - SuperMario, - ChibiRobo, - YoshiWoollyWorld, - Splatoon, - AnimalCrossing, - EightBitMario, - Skylanders, - Unknown8, - TheLegendOfZelda, - ShovelKnight, - Unknown11, - Kiby, - Pokemon, - MarioSportsSuperstars, - MonsterHunter, - BoxBoy, - Pikmin, - FireEmblem, - Metroid, - Others, - MegaMan, - Diablo -}; - -using TagUuid = std::array<u8, 10>; +using AmiiboName = std::array<char, (amiibo_name_length * 4) + 1>; struct TagInfo { TagUuid uuid; @@ -114,21 +51,19 @@ struct ModelInfo { AmiiboType amiibo_type; u16 model_number; AmiiboSeries series; - u8 fixed; // Must be 02 - INSERT_PADDING_BYTES(0x4); // Unknown - INSERT_PADDING_BYTES(0x20); // Probably a SHA256-(HMAC?) hash - INSERT_PADDING_BYTES(0x14); // SHA256-HMAC + u8 constant_value; // Must be 02 + INSERT_PADDING_BYTES(0x38); // Unknown }; static_assert(sizeof(ModelInfo) == 0x40, "ModelInfo is an invalid size"); struct RegisterInfo { - Service::Mii::MiiInfo mii_char_info; + Service::Mii::CharInfo mii_char_info; u16 first_write_year; u8 first_write_month; u8 first_write_day; - std::array<u8, 11> amiibo_name; - u8 unknown; - INSERT_PADDING_BYTES(0x98); + AmiiboName amiibo_name; + u8 font_region; + INSERT_PADDING_BYTES(0x7A); }; static_assert(sizeof(RegisterInfo) == 0x100, "RegisterInfo is an invalid size"); @@ -140,39 +75,9 @@ public: const char* name); ~Interface() override; - struct EncryptedAmiiboFile { - u16 crypto_init; // Must be A5 XX - u16 write_count; // Number of times the amiibo has been written? - INSERT_PADDING_BYTES(0x20); // System crypts - INSERT_PADDING_BYTES(0x20); // SHA256-(HMAC?) hash - ModelInfo model_info; // This struct is bigger than documentation - INSERT_PADDING_BYTES(0xC); // SHA256-HMAC - INSERT_PADDING_BYTES(0x114); // section 1 encrypted buffer - INSERT_PADDING_BYTES(0x54); // section 2 encrypted buffer - }; - static_assert(sizeof(EncryptedAmiiboFile) == 0x1F8, "AmiiboFile is an invalid size"); - - struct NTAG215Password { - u32 PWD; // Password to allow write access - u16 PACK; // Password acknowledge reply - u16 RFUI; // Reserved for future use - }; - static_assert(sizeof(NTAG215Password) == 0x8, "NTAG215Password is an invalid size"); - - struct NTAG215File { - TagUuid uuid; // Unique serial number - u16 lock_bytes; // Set defined pages as read only - u32 compability_container; // Defines available memory - EncryptedAmiiboFile user_memory; // Writable data - u32 dynamic_lock; // Dynamic lock - u32 CFG0; // Defines memory protected by password - u32 CFG1; // Defines number of verification attempts - NTAG215Password password; // Password data - }; - static_assert(sizeof(NTAG215File) == 0x21C, "NTAG215File is an invalid size"); - void CreateUserInterface(Kernel::HLERequestContext& ctx); - bool LoadAmiibo(const std::vector<u8>& buffer); + bool LoadAmiibo(const std::string& filename); + bool LoadAmiiboFile(const std::string& filename); void CloseAmiibo(); void Initialize(); @@ -182,6 +87,7 @@ public: Result StopDetection(); Result Mount(); Result Unmount(); + Result Flush(); Result GetTagInfo(TagInfo& tag_info) const; Result GetCommonInfo(CommonInfo& common_info) const; @@ -189,9 +95,10 @@ public: Result GetRegisterInfo(RegisterInfo& register_info) const; Result OpenApplicationArea(u32 access_id); - Result GetApplicationArea(std::vector<u8>& data) const; + Result GetApplicationArea(ApplicationArea& data) const; Result SetApplicationArea(const std::vector<u8>& data); Result CreateApplicationArea(u32 access_id, const std::vector<u8>& data); + Result RecreateApplicationArea(u32 access_id, const std::vector<u8>& data); u64 GetHandle() const; DeviceState GetCurrentState() const; @@ -204,27 +111,21 @@ public: std::shared_ptr<Module> module; private: - /// Validates that the amiibo file is not corrupted - bool IsAmiiboValid() const; - - bool AmiiboApplicationDataExist(u32 access_id) const; - std::vector<u8> LoadAmiiboApplicationData(u32 access_id) const; - void SaveAmiiboApplicationData(u32 access_id, const std::vector<u8>& data) const; - - /// return password needed to allow write access to protected memory - u32 GetTagPassword(const TagUuid& uuid) const; + AmiiboName GetAmiiboName(const AmiiboSettings& settings) const; const Core::HID::NpadIdType npad_id; - DeviceState device_state{DeviceState::Unaviable}; - KernelHelpers::ServiceContext service_context; + bool is_data_decoded{}; + bool is_application_area_initialized{}; + s32 protocol; + std::string file_path{}; Kernel::KEvent* activate_event; Kernel::KEvent* deactivate_event; + DeviceState device_state{DeviceState::Unaviable}; + KernelHelpers::ServiceContext service_context; + NTAG215File tag_data{}; - s32 protocol; - bool is_application_area_initialized{}; - u32 application_area_id; - std::vector<u8> application_area_data; + EncryptedNTAG215File encrypted_tag_data{}; }; }; @@ -243,6 +144,7 @@ private: void OpenApplicationArea(Kernel::HLERequestContext& ctx); void GetApplicationArea(Kernel::HLERequestContext& ctx); void SetApplicationArea(Kernel::HLERequestContext& ctx); + void Flush(Kernel::HLERequestContext& ctx); void CreateApplicationArea(Kernel::HLERequestContext& ctx); void GetTagInfo(Kernel::HLERequestContext& ctx); void GetRegisterInfo(Kernel::HLERequestContext& ctx); @@ -255,6 +157,7 @@ private: void GetNpadId(Kernel::HLERequestContext& ctx); void GetApplicationAreaSize(Kernel::HLERequestContext& ctx); void AttachAvailabilityChangeEvent(Kernel::HLERequestContext& ctx); + void RecreateApplicationArea(Kernel::HLERequestContext& ctx); KernelHelpers::ServiceContext service_context; diff --git a/src/core/hle/service/nvdrv/devices/nvhost_nvdec.cpp b/src/core/hle/service/nvdrv/devices/nvhost_nvdec.cpp index 2a5128c60..a7385fce8 100644 --- a/src/core/hle/service/nvdrv/devices/nvhost_nvdec.cpp +++ b/src/core/hle/service/nvdrv/devices/nvhost_nvdec.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include "audio_core/audio_core.h" #include "common/assert.h" #include "common/logging/log.h" #include "core/core.h" @@ -65,7 +66,10 @@ NvResult nvhost_nvdec::Ioctl3(DeviceFD fd, Ioctl command, const std::vector<u8>& return NvResult::NotImplemented; } -void nvhost_nvdec::OnOpen(DeviceFD fd) {} +void nvhost_nvdec::OnOpen(DeviceFD fd) { + LOG_INFO(Service_NVDRV, "NVDEC video stream started"); + system.AudioCore().SetNVDECActive(true); +} void nvhost_nvdec::OnClose(DeviceFD fd) { LOG_INFO(Service_NVDRV, "NVDEC video stream ended"); @@ -73,6 +77,7 @@ void nvhost_nvdec::OnClose(DeviceFD fd) { if (iter != fd_to_id.end()) { system.GPU().ClearCdmaInstance(iter->second); } + system.AudioCore().SetNVDECActive(false); } } // namespace Service::Nvidia::Devices diff --git a/src/core/hle/service/nvflinger/nvflinger.cpp b/src/core/hle/service/nvflinger/nvflinger.cpp index 5574269eb..9b382bf56 100644 --- a/src/core/hle/service/nvflinger/nvflinger.cpp +++ b/src/core/hle/service/nvflinger/nvflinger.cpp @@ -38,20 +38,16 @@ void NVFlinger::SplitVSync(std::stop_token stop_token) { Common::SetCurrentThreadName(name.c_str()); Common::SetCurrentThreadPriority(Common::ThreadPriority::High); - s64 delay = 0; + while (!stop_token.stop_requested()) { + vsync_signal.wait(false); + vsync_signal.store(false); + guard->lock(); - const s64 time_start = system.CoreTiming().GetGlobalTimeNs().count(); + Compose(); - const auto ticks = GetNextTicks(); - const s64 time_end = system.CoreTiming().GetGlobalTimeNs().count(); - const s64 time_passed = time_end - time_start; - const s64 next_time = std::max<s64>(0, ticks - time_passed - delay); + guard->unlock(); - if (next_time > 0) { - std::this_thread::sleep_for(std::chrono::nanoseconds{next_time}); - } - delay = (system.CoreTiming().GetGlobalTimeNs().count() - time_end) - next_time; } } @@ -66,27 +62,41 @@ NVFlinger::NVFlinger(Core::System& system_, HosBinderDriverServer& hos_binder_dr guard = std::make_shared<std::mutex>(); // Schedule the screen composition events - composition_event = Core::Timing::CreateEvent( + multi_composition_event = Core::Timing::CreateEvent( + "ScreenComposition", + [this](std::uintptr_t, s64 time, + std::chrono::nanoseconds ns_late) -> std::optional<std::chrono::nanoseconds> { + vsync_signal.store(true); + vsync_signal.notify_all(); + return std::chrono::nanoseconds(GetNextTicks()); + }); + + single_composition_event = Core::Timing::CreateEvent( "ScreenComposition", [this](std::uintptr_t, s64 time, std::chrono::nanoseconds ns_late) -> std::optional<std::chrono::nanoseconds> { const auto lock_guard = Lock(); Compose(); - return std::max(std::chrono::nanoseconds::zero(), - std::chrono::nanoseconds(GetNextTicks()) - ns_late); + return std::chrono::nanoseconds(GetNextTicks()); }); if (system.IsMulticore()) { + system.CoreTiming().ScheduleLoopingEvent(frame_ns, frame_ns, multi_composition_event); vsync_thread = std::jthread([this](std::stop_token token) { SplitVSync(token); }); } else { - system.CoreTiming().ScheduleLoopingEvent(frame_ns, frame_ns, composition_event); + system.CoreTiming().ScheduleLoopingEvent(frame_ns, frame_ns, single_composition_event); } } NVFlinger::~NVFlinger() { - if (!system.IsMulticore()) { - system.CoreTiming().UnscheduleEvent(composition_event, 0); + if (system.IsMulticore()) { + system.CoreTiming().UnscheduleEvent(multi_composition_event, {}); + vsync_thread.request_stop(); + vsync_signal.store(true); + vsync_signal.notify_all(); + } else { + system.CoreTiming().UnscheduleEvent(single_composition_event, {}); } for (auto& display : displays) { diff --git a/src/core/hle/service/nvflinger/nvflinger.h b/src/core/hle/service/nvflinger/nvflinger.h index 4775597cc..044ac6ac8 100644 --- a/src/core/hle/service/nvflinger/nvflinger.h +++ b/src/core/hle/service/nvflinger/nvflinger.h @@ -126,12 +126,15 @@ private: u32 swap_interval = 1; /// Event that handles screen composition. - std::shared_ptr<Core::Timing::EventType> composition_event; + std::shared_ptr<Core::Timing::EventType> multi_composition_event; + std::shared_ptr<Core::Timing::EventType> single_composition_event; std::shared_ptr<std::mutex> guard; Core::System& system; + std::atomic<bool> vsync_signal; + std::jthread vsync_thread; KernelHelpers::ServiceContext service_context; diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 50007338f..29d506c47 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -208,6 +208,16 @@ add_executable(yuzu yuzu.rc ) +if (WIN32 AND YUZU_CRASH_DUMPS) + target_sources(yuzu PRIVATE + mini_dump.cpp + mini_dump.h + ) + + target_link_libraries(yuzu PRIVATE ${DBGHELP_LIBRARY}) + target_compile_definitions(yuzu PRIVATE -DYUZU_DBGHELP) +endif() + file(GLOB COMPAT_LIST ${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.qrc ${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.json) diff --git a/src/yuzu/applets/qt_controller.cpp b/src/yuzu/applets/qt_controller.cpp index 8be311fcb..1d8072243 100644 --- a/src/yuzu/applets/qt_controller.cpp +++ b/src/yuzu/applets/qt_controller.cpp @@ -63,7 +63,7 @@ QtControllerSelectorDialog::QtControllerSelectorDialog( InputCommon::InputSubsystem* input_subsystem_, Core::System& system_) : QDialog(parent), ui(std::make_unique<Ui::QtControllerSelectorDialog>()), parameters(std::move(parameters_)), input_subsystem{input_subsystem_}, - input_profiles(std::make_unique<InputProfiles>(system_)), system{system_} { + input_profiles(std::make_unique<InputProfiles>()), system{system_} { ui->setupUi(this); player_widgets = { diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index 8ecd87150..a4ed68422 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -15,8 +15,7 @@ namespace FS = Common::FS; -Config::Config(Core::System& system_, const std::string& config_name, ConfigType config_type) - : type(config_type), system{system_} { +Config::Config(const std::string& config_name, ConfigType config_type) : type(config_type) { global = config_type == ConfigType::GlobalConfig; Initialize(config_name); @@ -546,6 +545,7 @@ void Config::ReadDebuggingValues() { ReadBasicSetting(Settings::values.use_debug_asserts); ReadBasicSetting(Settings::values.use_auto_stub); ReadBasicSetting(Settings::values.enable_all_controllers); + ReadBasicSetting(Settings::values.create_crash_dumps); qt_config->endGroup(); } @@ -1161,6 +1161,7 @@ void Config::SaveDebuggingValues() { WriteBasicSetting(Settings::values.use_debug_asserts); WriteBasicSetting(Settings::values.disable_macro_jit); WriteBasicSetting(Settings::values.enable_all_controllers); + WriteBasicSetting(Settings::values.create_crash_dumps); qt_config->endGroup(); } @@ -1547,7 +1548,6 @@ void Config::Reload() { ReadValues(); // To apply default value changes SaveValues(); - system.ApplySettings(); } void Config::Save() { diff --git a/src/yuzu/configuration/config.h b/src/yuzu/configuration/config.h index 486ceea94..06fa7d2d0 100644 --- a/src/yuzu/configuration/config.h +++ b/src/yuzu/configuration/config.h @@ -25,7 +25,7 @@ public: InputProfile, }; - explicit Config(Core::System& system_, const std::string& config_name = "qt-config", + explicit Config(const std::string& config_name = "qt-config", ConfigType config_type = ConfigType::GlobalConfig); ~Config(); @@ -194,8 +194,6 @@ private: std::unique_ptr<QSettings> qt_config; std::string qt_config_loc; bool global; - - Core::System& system; }; // These metatype declarations cannot be in common/settings.h because core is devoid of QT diff --git a/src/yuzu/configuration/configure_debug.cpp b/src/yuzu/configuration/configure_debug.cpp index 04d397750..622808e94 100644 --- a/src/yuzu/configuration/configure_debug.cpp +++ b/src/yuzu/configuration/configure_debug.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include <QDesktopServices> +#include <QMessageBox> #include <QUrl> #include "common/fs/path_util.h" #include "common/logging/backend.h" @@ -26,6 +27,16 @@ ConfigureDebug::ConfigureDebug(const Core::System& system_, QWidget* parent) connect(ui->toggle_gdbstub, &QCheckBox::toggled, [&]() { ui->gdbport_spinbox->setEnabled(ui->toggle_gdbstub->isChecked()); }); + + connect(ui->create_crash_dumps, &QCheckBox::stateChanged, [&](int) { + if (crash_dump_warning_shown) { + return; + } + QMessageBox::warning(this, tr("Restart Required"), + tr("yuzu is required to restart in order to apply this setting."), + QMessageBox::Ok, QMessageBox::Ok); + crash_dump_warning_shown = true; + }); } ConfigureDebug::~ConfigureDebug() = default; @@ -71,7 +82,14 @@ void ConfigureDebug::SetConfiguration() { ui->disable_web_applet->setChecked(UISettings::values.disable_web_applet.GetValue()); #else ui->disable_web_applet->setEnabled(false); - ui->disable_web_applet->setText(QString::fromUtf8("Web applet not compiled")); + ui->disable_web_applet->setText(tr("Web applet not compiled")); +#endif + +#ifdef YUZU_DBGHELP + ui->create_crash_dumps->setChecked(Settings::values.create_crash_dumps.GetValue()); +#else + ui->create_crash_dumps->setEnabled(false); + ui->create_crash_dumps->setText(tr("MiniDump creation not compiled")); #endif } @@ -84,6 +102,7 @@ void ConfigureDebug::ApplyConfiguration() { Settings::values.enable_fs_access_log = ui->fs_access_log->isChecked(); Settings::values.reporting_services = ui->reporting_services->isChecked(); Settings::values.dump_audio_commands = ui->dump_audio_commands->isChecked(); + Settings::values.create_crash_dumps = ui->create_crash_dumps->isChecked(); Settings::values.quest_flag = ui->quest_flag->isChecked(); Settings::values.use_debug_asserts = ui->use_debug_asserts->isChecked(); Settings::values.use_auto_stub = ui->use_auto_stub->isChecked(); diff --git a/src/yuzu/configuration/configure_debug.h b/src/yuzu/configuration/configure_debug.h index 42d30f170..030a0b7f7 100644 --- a/src/yuzu/configuration/configure_debug.h +++ b/src/yuzu/configuration/configure_debug.h @@ -32,4 +32,6 @@ private: std::unique_ptr<Ui::ConfigureDebug> ui; const Core::System& system; + + bool crash_dump_warning_shown{false}; }; diff --git a/src/yuzu/configuration/configure_debug.ui b/src/yuzu/configuration/configure_debug.ui index 47b8b80f1..314d47af5 100644 --- a/src/yuzu/configuration/configure_debug.ui +++ b/src/yuzu/configuration/configure_debug.ui @@ -7,60 +7,60 @@ </property> <widget class="QWidget"> <layout class="QVBoxLayout" name="verticalLayout_1"> - <item> - <layout class="QVBoxLayout" name="verticalLayout_2"> - <item> - <widget class="QGroupBox" name="groupBox"> - <property name="title"> - <string>Debugger</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Debugger</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_11"> + <item> + <widget class="QCheckBox" name="toggle_gdbstub"> + <property name="text"> + <string>Enable GDB Stub</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="label_11"> + <property name="text"> + <string>Port:</string> + </property> + </widget> + </item> <item> - <layout class="QHBoxLayout" name="horizontalLayout_11"> - <item> - <widget class="QCheckBox" name="toggle_gdbstub"> - <property name="text"> - <string>Enable GDB Stub</string> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QLabel" name="label_11"> - <property name="text"> - <string>Port:</string> - </property> - </widget> - </item> - <item> - <widget class="QSpinBox" name="gdbport_spinbox"> - <property name="minimum"> - <number>1024</number> - </property> - <property name="maximum"> - <number>65535</number> - </property> - </widget> - </item> - </layout> + <widget class="QSpinBox" name="gdbport_spinbox"> + <property name="minimum"> + <number>1024</number> + </property> + <property name="maximum"> + <number>65535</number> + </property> + </widget> </item> </layout> - </widget> - </item> - </layout> + </item> + </layout> + </widget> </item> + </layout> + </item> <item> <widget class="QGroupBox" name="groupBox_2"> <property name="title"> @@ -231,6 +231,13 @@ <string>Debugging</string> </property> <layout class="QGridLayout" name="gridLayout_3"> + <item row="2" column="0"> + <widget class="QCheckBox" name="reporting_services"> + <property name="text"> + <string>Enable Verbose Reporting Services**</string> + </property> + </widget> + </item> <item row="0" column="0"> <widget class="QCheckBox" name="fs_access_log"> <property name="text"> @@ -238,20 +245,20 @@ </property> </widget> </item> - <item row="1" column="0"> + <item row="0" column="1"> <widget class="QCheckBox" name="dump_audio_commands"> - <property name="text"> - <string>Dump Audio Commands To Console**</string> - </property> <property name="toolTip"> <string>Enable this to output the latest generated audio command list to the console. Only affects games using the audio renderer.</string> </property> + <property name="text"> + <string>Dump Audio Commands To Console**</string> + </property> </widget> </item> - <item row="2" column="0"> - <widget class="QCheckBox" name="reporting_services"> + <item row="2" column="1"> + <widget class="QCheckBox" name="create_crash_dumps"> <property name="text"> - <string>Enable Verbose Reporting Services**</string> + <string>Create Minidump After Crash</string> </property> </widget> </item> @@ -340,7 +347,6 @@ <tabstop>disable_loop_safety_checks</tabstop> <tabstop>fs_access_log</tabstop> <tabstop>reporting_services</tabstop> - <tabstop>dump_audio_commands</tabstop> <tabstop>quest_flag</tabstop> <tabstop>enable_cpu_debugging</tabstop> <tabstop>use_debug_asserts</tabstop> diff --git a/src/yuzu/configuration/configure_input.cpp b/src/yuzu/configuration/configure_input.cpp index 16fba3deb..cb55472c9 100644 --- a/src/yuzu/configuration/configure_input.cpp +++ b/src/yuzu/configuration/configure_input.cpp @@ -65,7 +65,7 @@ void OnDockedModeChanged(bool last_state, bool new_state, Core::System& system) ConfigureInput::ConfigureInput(Core::System& system_, QWidget* parent) : QWidget(parent), ui(std::make_unique<Ui::ConfigureInput>()), - profiles(std::make_unique<InputProfiles>(system_)), system{system_} { + profiles(std::make_unique<InputProfiles>()), system{system_} { ui->setupUi(this); } diff --git a/src/yuzu/configuration/configure_input_player.cpp b/src/yuzu/configuration/configure_input_player.cpp index 9b4f765ce..9e5a40fe7 100644 --- a/src/yuzu/configuration/configure_input_player.cpp +++ b/src/yuzu/configuration/configure_input_player.cpp @@ -1417,7 +1417,7 @@ void ConfigureInputPlayer::HandleClick( ui->controllerFrame->BeginMappingAnalog(button_id); } - timeout_timer->start(2500); // Cancel after 2.5 seconds + timeout_timer->start(4000); // Cancel after 4 seconds poll_timer->start(25); // Check for new inputs every 25ms } diff --git a/src/yuzu/configuration/configure_per_game.cpp b/src/yuzu/configuration/configure_per_game.cpp index af8343b2e..c3cb8f61d 100644 --- a/src/yuzu/configuration/configure_per_game.cpp +++ b/src/yuzu/configuration/configure_per_game.cpp @@ -42,8 +42,7 @@ ConfigurePerGame::ConfigurePerGame(QWidget* parent, u64 title_id_, const std::st const auto file_path = std::filesystem::path(Common::FS::ToU8String(file_name)); const auto config_file_name = title_id == 0 ? Common::FS::PathToUTF8String(file_path.filename()) : fmt::format("{:016X}", title_id); - game_config = - std::make_unique<Config>(system, config_file_name, Config::ConfigType::PerGameConfig); + game_config = std::make_unique<Config>(config_file_name, Config::ConfigType::PerGameConfig); addons_tab = std::make_unique<ConfigurePerGameAddons>(system_, this); audio_tab = std::make_unique<ConfigureAudio>(system_, this); diff --git a/src/yuzu/configuration/configure_tas.ui b/src/yuzu/configuration/configure_tas.ui index cf88a5bf0..625af0c89 100644 --- a/src/yuzu/configuration/configure_tas.ui +++ b/src/yuzu/configuration/configure_tas.ui @@ -16,6 +16,9 @@ <property name="text"> <string><html><head/><body><p>Reads controller input from scripts in the same format as TAS-nx scripts.<br/>For a more detailed explanation, please consult the <a href="https://yuzu-emu.org/help/feature/tas/"><span style=" text-decoration: underline; color:#039be5;">help page</span></a> on the yuzu website.</p></body></html></string> </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> </widget> </item> <item row="1" column="0" colspan="4"> diff --git a/src/yuzu/configuration/configure_web.cpp b/src/yuzu/configuration/configure_web.cpp index d668c992b..ab526e4ca 100644 --- a/src/yuzu/configuration/configure_web.cpp +++ b/src/yuzu/configuration/configure_web.cpp @@ -128,20 +128,25 @@ void ConfigureWeb::RefreshTelemetryID() { void ConfigureWeb::OnLoginChanged() { if (ui->edit_token->text().isEmpty()) { user_verified = true; - - const QPixmap pixmap = QIcon::fromTheme(QStringLiteral("checked")).pixmap(16); - ui->label_token_verified->setPixmap(pixmap); + // Empty = no icon + ui->label_token_verified->setPixmap(QPixmap()); + ui->label_token_verified->setToolTip(QString()); } else { user_verified = false; - const QPixmap pixmap = QIcon::fromTheme(QStringLiteral("failed")).pixmap(16); + // Show an info icon if it's been changed, clearer than showing failure + const QPixmap pixmap = QIcon::fromTheme(QStringLiteral("info")).pixmap(16); ui->label_token_verified->setPixmap(pixmap); + ui->label_token_verified->setToolTip( + tr("Unverified, please click Verify before saving configuration", "Tooltip")); } } void ConfigureWeb::VerifyLogin() { ui->button_verify_login->setDisabled(true); ui->button_verify_login->setText(tr("Verifying...")); + ui->label_token_verified->setPixmap(QIcon::fromTheme(QStringLiteral("sync")).pixmap(16)); + ui->label_token_verified->setToolTip(tr("Verifying...")); verify_watcher.setFuture(QtConcurrent::run( [username = UsernameFromDisplayToken(ui->edit_token->text().toStdString()), token = TokenFromDisplayToken(ui->edit_token->text().toStdString())] { @@ -155,13 +160,13 @@ void ConfigureWeb::OnLoginVerified() { if (verify_watcher.result()) { user_verified = true; - const QPixmap pixmap = QIcon::fromTheme(QStringLiteral("checked")).pixmap(16); - ui->label_token_verified->setPixmap(pixmap); + ui->label_token_verified->setPixmap(QIcon::fromTheme(QStringLiteral("checked")).pixmap(16)); + ui->label_token_verified->setToolTip(tr("Verified", "Tooltip")); ui->username->setText( QString::fromStdString(UsernameFromDisplayToken(ui->edit_token->text().toStdString()))); } else { - const QPixmap pixmap = QIcon::fromTheme(QStringLiteral("failed")).pixmap(16); - ui->label_token_verified->setPixmap(pixmap); + ui->label_token_verified->setPixmap(QIcon::fromTheme(QStringLiteral("failed")).pixmap(16)); + ui->label_token_verified->setToolTip(tr("Verification failed", "Tooltip")); ui->username->setText(tr("Unspecified")); QMessageBox::critical(this, tr("Verification failed"), tr("Verification failed. Check that you have entered your token " diff --git a/src/yuzu/configuration/input_profiles.cpp b/src/yuzu/configuration/input_profiles.cpp index 20b22e7de..807afbeb2 100644 --- a/src/yuzu/configuration/input_profiles.cpp +++ b/src/yuzu/configuration/input_profiles.cpp @@ -27,7 +27,7 @@ std::filesystem::path GetNameWithoutExtension(std::filesystem::path filename) { } // namespace -InputProfiles::InputProfiles(Core::System& system_) : system{system_} { +InputProfiles::InputProfiles() { const auto input_profile_loc = FS::GetYuzuPath(FS::YuzuPath::ConfigDir) / "input"; if (!FS::IsDir(input_profile_loc)) { @@ -43,8 +43,8 @@ InputProfiles::InputProfiles(Core::System& system_) : system{system_} { if (IsINI(filename) && IsProfileNameValid(name_without_ext)) { map_profiles.insert_or_assign( - name_without_ext, std::make_unique<Config>(system, name_without_ext, - Config::ConfigType::InputProfile)); + name_without_ext, + std::make_unique<Config>(name_without_ext, Config::ConfigType::InputProfile)); } return true; @@ -80,8 +80,7 @@ bool InputProfiles::CreateProfile(const std::string& profile_name, std::size_t p } map_profiles.insert_or_assign( - profile_name, - std::make_unique<Config>(system, profile_name, Config::ConfigType::InputProfile)); + profile_name, std::make_unique<Config>(profile_name, Config::ConfigType::InputProfile)); return SaveProfile(profile_name, player_index); } diff --git a/src/yuzu/configuration/input_profiles.h b/src/yuzu/configuration/input_profiles.h index 65fc9e62c..2bf3e4250 100644 --- a/src/yuzu/configuration/input_profiles.h +++ b/src/yuzu/configuration/input_profiles.h @@ -15,7 +15,7 @@ class Config; class InputProfiles { public: - explicit InputProfiles(Core::System& system_); + explicit InputProfiles(); virtual ~InputProfiles(); std::vector<std::string> GetInputProfileNames(); @@ -31,6 +31,4 @@ private: bool ProfileExistsInMap(const std::string& profile_name) const; std::unordered_map<std::string, std::unique_ptr<Config>> map_profiles; - - Core::System& system; }; diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index a85adc072..3c1bd19db 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -138,6 +138,10 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "yuzu/uisettings.h" #include "yuzu/util/clickable_label.h" +#ifdef YUZU_DBGHELP +#include "yuzu/mini_dump.h" +#endif + using namespace Common::Literals; #ifdef USE_DISCORD_PRESENCE @@ -269,10 +273,9 @@ bool GMainWindow::CheckDarkMode() { #endif // __linux__ } -GMainWindow::GMainWindow(bool has_broken_vulkan) +GMainWindow::GMainWindow(std::unique_ptr<Config> config_, bool has_broken_vulkan) : ui{std::make_unique<Ui::MainWindow>()}, system{std::make_unique<Core::System>()}, - input_subsystem{std::make_shared<InputCommon::InputSubsystem>()}, - config{std::make_unique<Config>(*system)}, + input_subsystem{std::make_shared<InputCommon::InputSubsystem>()}, config{std::move(config_)}, vfs{std::make_shared<FileSys::RealVfsFilesystem>()}, provider{std::make_unique<FileSys::ManualContentProvider>()} { #ifdef __linux__ @@ -1637,7 +1640,8 @@ void GMainWindow::BootGame(const QString& filename, u64 program_id, std::size_t const auto config_file_name = title_id == 0 ? Common::FS::PathToUTF8String(file_path.filename()) : fmt::format("{:016X}", title_id); - Config per_game_config(*system, config_file_name, Config::ConfigType::PerGameConfig); + Config per_game_config(config_file_name, Config::ConfigType::PerGameConfig); + system->ApplySettings(); } // Save configurations @@ -2981,7 +2985,7 @@ void GMainWindow::OnConfigure() { Settings::values.disabled_addons.clear(); - config = std::make_unique<Config>(*system); + config = std::make_unique<Config>(); UISettings::values.reset_to_defaults = false; UISettings::values.game_dirs = std::move(old_game_dirs); @@ -3042,6 +3046,7 @@ void GMainWindow::OnConfigure() { UpdateStatusButtons(); controller_dialog->refreshConfiguration(); + system->ApplySettings(); } void GMainWindow::OnConfigureTas() { @@ -3254,26 +3259,7 @@ void GMainWindow::LoadAmiibo(const QString& filename) { return; } - QFile nfc_file{filename}; - if (!nfc_file.open(QIODevice::ReadOnly)) { - QMessageBox::warning(this, tr("Error opening Amiibo data file"), - tr("Unable to open Amiibo file \"%1\" for reading.").arg(filename)); - return; - } - - const u64 nfc_file_size = nfc_file.size(); - std::vector<u8> buffer(nfc_file_size); - const u64 read_size = nfc_file.read(reinterpret_cast<char*>(buffer.data()), nfc_file_size); - if (nfc_file_size != read_size) { - QMessageBox::warning(this, tr("Error reading Amiibo data file"), - tr("Unable to fully read Amiibo data. Expected to read %1 bytes, but " - "was only able to read %2 bytes.") - .arg(nfc_file_size) - .arg(read_size)); - return; - } - - if (!nfc->LoadAmiibo(buffer)) { + if (!nfc->LoadAmiibo(filename.toStdString())) { QMessageBox::warning(this, tr("Error loading Amiibo data"), tr("Unable to load Amiibo data.")); } @@ -4082,7 +4068,24 @@ void GMainWindow::changeEvent(QEvent* event) { #endif int main(int argc, char* argv[]) { + std::unique_ptr<Config> config = std::make_unique<Config>(); bool has_broken_vulkan = false; + bool is_child = false; + if (CheckEnvVars(&is_child)) { + return 0; + } + +#ifdef YUZU_DBGHELP + PROCESS_INFORMATION pi; + if (!is_child && Settings::values.create_crash_dumps.GetValue() && + MiniDump::SpawnDebuggee(argv[0], pi)) { + // Delete the config object so that it doesn't save when the program exits + config.reset(nullptr); + MiniDump::DebugDebuggee(pi); + return 0; + } +#endif + if (StartupChecks(argv[0], &has_broken_vulkan)) { return 0; } @@ -4135,7 +4138,7 @@ int main(int argc, char* argv[]) { // generating shaders setlocale(LC_ALL, "C"); - GMainWindow main_window{has_broken_vulkan}; + GMainWindow main_window{std::move(config), has_broken_vulkan}; // After settings have been loaded by GMainWindow, apply the filter main_window.show(); diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 1ae2b93d9..716aef063 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -120,7 +120,7 @@ class GMainWindow : public QMainWindow { public: void filterBarSetChecked(bool state); void UpdateUITheme(); - explicit GMainWindow(bool has_broken_vulkan); + explicit GMainWindow(std::unique_ptr<Config> config_, bool has_broken_vulkan); ~GMainWindow() override; bool DropAction(QDropEvent* event); diff --git a/src/yuzu/mini_dump.cpp b/src/yuzu/mini_dump.cpp new file mode 100644 index 000000000..a34dc6a9c --- /dev/null +++ b/src/yuzu/mini_dump.cpp @@ -0,0 +1,202 @@ +// SPDX-FileCopyrightText: 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <cstdio> +#include <cstring> +#include <ctime> +#include <filesystem> +#include <fmt/format.h> +#include <windows.h> +#include "yuzu/mini_dump.h" +#include "yuzu/startup_checks.h" + +// dbghelp.h must be included after windows.h +#include <dbghelp.h> + +namespace MiniDump { + +void CreateMiniDump(HANDLE process_handle, DWORD process_id, MINIDUMP_EXCEPTION_INFORMATION* info, + EXCEPTION_POINTERS* pep) { + char file_name[255]; + const std::time_t the_time = std::time(nullptr); + std::strftime(file_name, 255, "yuzu-crash-%Y%m%d%H%M%S.dmp", std::localtime(&the_time)); + + // Open the file + HANDLE file_handle = CreateFileA(file_name, GENERIC_READ | GENERIC_WRITE, 0, nullptr, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + + if (file_handle == nullptr || file_handle == INVALID_HANDLE_VALUE) { + fmt::print(stderr, "CreateFileA failed. Error: {}", GetLastError()); + return; + } + + // Create the minidump + const MINIDUMP_TYPE dump_type = MiniDumpNormal; + + const bool write_dump_status = MiniDumpWriteDump(process_handle, process_id, file_handle, + dump_type, (pep != 0) ? info : 0, 0, 0); + + if (write_dump_status) { + fmt::print(stderr, "MiniDump created: {}", file_name); + } else { + fmt::print(stderr, "MiniDumpWriteDump failed. Error: {}", GetLastError()); + } + + // Close the file + CloseHandle(file_handle); +} + +void DumpFromDebugEvent(DEBUG_EVENT& deb_ev, PROCESS_INFORMATION& pi) { + EXCEPTION_RECORD& record = deb_ev.u.Exception.ExceptionRecord; + + HANDLE thread_handle = OpenThread(THREAD_GET_CONTEXT, false, deb_ev.dwThreadId); + if (thread_handle == nullptr) { + fmt::print(stderr, "OpenThread failed ({})", GetLastError()); + return; + } + + // Get child process context + CONTEXT context = {}; + context.ContextFlags = CONTEXT_ALL; + if (!GetThreadContext(thread_handle, &context)) { + fmt::print(stderr, "GetThreadContext failed ({})", GetLastError()); + return; + } + + // Create exception pointers for minidump + EXCEPTION_POINTERS ep; + ep.ExceptionRecord = &record; + ep.ContextRecord = &context; + + MINIDUMP_EXCEPTION_INFORMATION info; + info.ThreadId = deb_ev.dwThreadId; + info.ExceptionPointers = &ep; + info.ClientPointers = false; + + CreateMiniDump(pi.hProcess, pi.dwProcessId, &info, &ep); + + if (CloseHandle(thread_handle) == 0) { + fmt::print(stderr, "error: CloseHandle(thread_handle) failed ({})", GetLastError()); + } +} + +bool SpawnDebuggee(const char* arg0, PROCESS_INFORMATION& pi) { + std::memset(&pi, 0, sizeof(pi)); + + // Don't debug if we are already being debugged + if (IsDebuggerPresent()) { + return false; + } + + if (!SpawnChild(arg0, &pi, 0)) { + fmt::print(stderr, "warning: continuing without crash dumps"); + return false; + } + + const bool can_debug = DebugActiveProcess(pi.dwProcessId); + if (!can_debug) { + fmt::print(stderr, + "warning: DebugActiveProcess failed ({}), continuing without crash dumps", + GetLastError()); + return false; + } + + return true; +} + +static const char* ExceptionName(DWORD exception) { + switch (exception) { + case EXCEPTION_ACCESS_VIOLATION: + return "EXCEPTION_ACCESS_VIOLATION"; + case EXCEPTION_DATATYPE_MISALIGNMENT: + return "EXCEPTION_DATATYPE_MISALIGNMENT"; + case EXCEPTION_BREAKPOINT: + return "EXCEPTION_BREAKPOINT"; + case EXCEPTION_SINGLE_STEP: + return "EXCEPTION_SINGLE_STEP"; + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: + return "EXCEPTION_ARRAY_BOUNDS_EXCEEDED"; + case EXCEPTION_FLT_DENORMAL_OPERAND: + return "EXCEPTION_FLT_DENORMAL_OPERAND"; + case EXCEPTION_FLT_DIVIDE_BY_ZERO: + return "EXCEPTION_FLT_DIVIDE_BY_ZERO"; + case EXCEPTION_FLT_INEXACT_RESULT: + return "EXCEPTION_FLT_INEXACT_RESULT"; + case EXCEPTION_FLT_INVALID_OPERATION: + return "EXCEPTION_FLT_INVALID_OPERATION"; + case EXCEPTION_FLT_OVERFLOW: + return "EXCEPTION_FLT_OVERFLOW"; + case EXCEPTION_FLT_STACK_CHECK: + return "EXCEPTION_FLT_STACK_CHECK"; + case EXCEPTION_FLT_UNDERFLOW: + return "EXCEPTION_FLT_UNDERFLOW"; + case EXCEPTION_INT_DIVIDE_BY_ZERO: + return "EXCEPTION_INT_DIVIDE_BY_ZERO"; + case EXCEPTION_INT_OVERFLOW: + return "EXCEPTION_INT_OVERFLOW"; + case EXCEPTION_PRIV_INSTRUCTION: + return "EXCEPTION_PRIV_INSTRUCTION"; + case EXCEPTION_IN_PAGE_ERROR: + return "EXCEPTION_IN_PAGE_ERROR"; + case EXCEPTION_ILLEGAL_INSTRUCTION: + return "EXCEPTION_ILLEGAL_INSTRUCTION"; + case EXCEPTION_NONCONTINUABLE_EXCEPTION: + return "EXCEPTION_NONCONTINUABLE_EXCEPTION"; + case EXCEPTION_STACK_OVERFLOW: + return "EXCEPTION_STACK_OVERFLOW"; + case EXCEPTION_INVALID_DISPOSITION: + return "EXCEPTION_INVALID_DISPOSITION"; + case EXCEPTION_GUARD_PAGE: + return "EXCEPTION_GUARD_PAGE"; + case EXCEPTION_INVALID_HANDLE: + return "EXCEPTION_INVALID_HANDLE"; + default: + return "unknown exception type"; + } +} + +void DebugDebuggee(PROCESS_INFORMATION& pi) { + DEBUG_EVENT deb_ev = {}; + + while (deb_ev.dwDebugEventCode != EXIT_PROCESS_DEBUG_EVENT) { + const bool wait_success = WaitForDebugEvent(&deb_ev, INFINITE); + if (!wait_success) { + fmt::print(stderr, "error: WaitForDebugEvent failed ({})", GetLastError()); + return; + } + + switch (deb_ev.dwDebugEventCode) { + case OUTPUT_DEBUG_STRING_EVENT: + case CREATE_PROCESS_DEBUG_EVENT: + case CREATE_THREAD_DEBUG_EVENT: + case EXIT_PROCESS_DEBUG_EVENT: + case EXIT_THREAD_DEBUG_EVENT: + case LOAD_DLL_DEBUG_EVENT: + case RIP_EVENT: + case UNLOAD_DLL_DEBUG_EVENT: + // Continue on all other debug events + ContinueDebugEvent(deb_ev.dwProcessId, deb_ev.dwThreadId, DBG_CONTINUE); + break; + case EXCEPTION_DEBUG_EVENT: + EXCEPTION_RECORD& record = deb_ev.u.Exception.ExceptionRecord; + + // We want to generate a crash dump if we are seeing the same exception again. + if (!deb_ev.u.Exception.dwFirstChance) { + fmt::print(stderr, "Creating MiniDump on ExceptionCode: 0x{:08x} {}\n", + record.ExceptionCode, ExceptionName(record.ExceptionCode)); + DumpFromDebugEvent(deb_ev, pi); + } + + // Continue without handling the exception. + // Lets the debuggee use its own exception handler. + // - If one does not exist, we will see the exception once more where we make a minidump + // for. Then when it reaches here again, yuzu will probably crash. + // - DBG_CONTINUE on an exception that the debuggee does not handle can set us up for an + // infinite loop of exceptions. + ContinueDebugEvent(deb_ev.dwProcessId, deb_ev.dwThreadId, DBG_EXCEPTION_NOT_HANDLED); + break; + } + } +} + +} // namespace MiniDump diff --git a/src/yuzu/mini_dump.h b/src/yuzu/mini_dump.h new file mode 100644 index 000000000..d6b6cca84 --- /dev/null +++ b/src/yuzu/mini_dump.h @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <windows.h> + +#include <dbghelp.h> + +namespace MiniDump { + +void CreateMiniDump(HANDLE process_handle, DWORD process_id, MINIDUMP_EXCEPTION_INFORMATION* info, + EXCEPTION_POINTERS* pep); + +void DumpFromDebugEvent(DEBUG_EVENT& deb_ev, PROCESS_INFORMATION& pi); +bool SpawnDebuggee(const char* arg0, PROCESS_INFORMATION& pi); +void DebugDebuggee(PROCESS_INFORMATION& pi); + +} // namespace MiniDump diff --git a/src/yuzu/startup_checks.cpp b/src/yuzu/startup_checks.cpp index 8421280bf..29b87da05 100644 --- a/src/yuzu/startup_checks.cpp +++ b/src/yuzu/startup_checks.cpp @@ -31,19 +31,36 @@ void CheckVulkan() { } } -bool StartupChecks(const char* arg0, bool* has_broken_vulkan) { +bool CheckEnvVars(bool* is_child) { #ifdef _WIN32 // Check environment variable to see if we are the child char variable_contents[8]; const DWORD startup_check_var = GetEnvironmentVariableA(STARTUP_CHECK_ENV_VAR, variable_contents, 8); - if (startup_check_var > 0 && std::strncmp(variable_contents, "ON", 8) == 0) { + if (startup_check_var > 0 && std::strncmp(variable_contents, ENV_VAR_ENABLED_TEXT, 8) == 0) { CheckVulkan(); return true; } + // Don't perform startup checks if we are a child process + char is_child_s[8]; + const DWORD is_child_len = GetEnvironmentVariableA(IS_CHILD_ENV_VAR, is_child_s, 8); + if (is_child_len > 0 && std::strncmp(is_child_s, ENV_VAR_ENABLED_TEXT, 8) == 0) { + *is_child = true; + return false; + } else if (!SetEnvironmentVariableA(IS_CHILD_ENV_VAR, ENV_VAR_ENABLED_TEXT)) { + std::fprintf(stderr, "SetEnvironmentVariableA failed to set %s with error %d\n", + IS_CHILD_ENV_VAR, GetLastError()); + return true; + } +#endif + return false; +} + +bool StartupChecks(const char* arg0, bool* has_broken_vulkan) { +#ifdef _WIN32 // Set the startup variable for child processes - const bool env_var_set = SetEnvironmentVariableA(STARTUP_CHECK_ENV_VAR, "ON"); + const bool env_var_set = SetEnvironmentVariableA(STARTUP_CHECK_ENV_VAR, ENV_VAR_ENABLED_TEXT); if (!env_var_set) { std::fprintf(stderr, "SetEnvironmentVariableA failed to set %s with error %d\n", STARTUP_CHECK_ENV_VAR, GetLastError()); @@ -53,7 +70,7 @@ bool StartupChecks(const char* arg0, bool* has_broken_vulkan) { PROCESS_INFORMATION process_info; std::memset(&process_info, '\0', sizeof(process_info)); - if (!SpawnChild(arg0, &process_info)) { + if (!SpawnChild(arg0, &process_info, 0)) { return false; } @@ -106,7 +123,7 @@ bool StartupChecks(const char* arg0, bool* has_broken_vulkan) { } #ifdef _WIN32 -bool SpawnChild(const char* arg0, PROCESS_INFORMATION* pi) { +bool SpawnChild(const char* arg0, PROCESS_INFORMATION* pi, int flags) { STARTUPINFOA startup_info; std::memset(&startup_info, '\0', sizeof(startup_info)); @@ -120,7 +137,7 @@ bool SpawnChild(const char* arg0, PROCESS_INFORMATION* pi) { nullptr, // lpProcessAttributes nullptr, // lpThreadAttributes false, // bInheritHandles - 0, // dwCreationFlags + flags, // dwCreationFlags nullptr, // lpEnvironment nullptr, // lpCurrentDirectory &startup_info, // lpStartupInfo diff --git a/src/yuzu/startup_checks.h b/src/yuzu/startup_checks.h index 096dd54a8..f2fc2d9d4 100644 --- a/src/yuzu/startup_checks.h +++ b/src/yuzu/startup_checks.h @@ -7,11 +7,14 @@ #include <windows.h> #endif +constexpr char IS_CHILD_ENV_VAR[] = "YUZU_IS_CHILD"; constexpr char STARTUP_CHECK_ENV_VAR[] = "YUZU_DO_STARTUP_CHECKS"; +constexpr char ENV_VAR_ENABLED_TEXT[] = "ON"; void CheckVulkan(); +bool CheckEnvVars(bool* is_child); bool StartupChecks(const char* arg0, bool* has_broken_vulkan); #ifdef _WIN32 -bool SpawnChild(const char* arg0, PROCESS_INFORMATION* pi); +bool SpawnChild(const char* arg0, PROCESS_INFORMATION* pi, int flags); #endif diff --git a/vcpkg.json b/vcpkg.json index c4413e22a..3c92510d6 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -31,6 +31,10 @@ "yuzu-tests": { "description": "Compile tests", "dependencies": [ "catch2" ] + }, + "dbghelp": { + "description": "Compile Windows crash dump (Minidump) support", + "dependencies": [ "dbghelp" ] } }, "overrides": [ |