diff --git a/.ci/scripts/linux/upload.sh b/.ci/scripts/linux/upload.sh
index 8173c5728a..e0f336427b 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 790ba82183..6f522feed0 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 d463281ded..21abcd752e 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 2a1bf93bc0..ea405e5dc6 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 16 2019" -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-mainline-step2.yml b/.ci/yuzu-mainline-step2.yml
index 0e99f43fa8..b294827f49 100644
--- a/.ci/yuzu-mainline-step2.yml
+++ b/.ci/yuzu-mainline-step2.yml
@@ -50,7 +50,7 @@ stages:
     timeoutInMinutes: 120
     displayName: 'msvc'
     pool:
-      vmImage: windows-2019
+      vmImage: windows-2022
     steps:
     - template: ./templates/sync-source.yml
       parameters:
diff --git a/.ci/yuzu-patreon-step2.yml b/.ci/yuzu-patreon-step2.yml
index 33c081c532..71a23ebe62 100644
--- a/.ci/yuzu-patreon-step2.yml
+++ b/.ci/yuzu-patreon-step2.yml
@@ -11,11 +11,32 @@ stages:
 - stage: build
   displayName: 'build'
   jobs:
-  - job: build
+  - job: linux
     timeoutInMinutes: 120
-    displayName: 'windows-msvc'
+    displayName: 'linux'
     pool:
-      vmImage: windows-2019
+      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:
     - template: ./templates/sync-source.yml
       parameters:
diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
index 9a973ee0c1..7cde8380bb 100644
--- a/.github/workflows/verify.yml
+++ b/.github/workflows/verify.yml
@@ -71,7 +71,7 @@ jobs:
   build-msvc:
     name: 'test build (windows, msvc)'
     needs: format
-    runs-on: windows-2019
+    runs-on: windows-2022
     steps:
       - name: Set up cache
         uses: actions/cache@v3
@@ -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 cdf37962ae..a5f7248c70 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 fe4fa2f07f..9a90f9eb69 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 2ab0ea589d..20dd1383f5 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
similarity index 100%
rename from dist/qt_themes/default/icons/16x16/checked.png
rename to 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
similarity index 100%
rename from dist/qt_themes/default/icons/16x16/failed.png
rename to 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
new file mode 100644
index 0000000000..8b9330f4c8
Binary files /dev/null and b/dist/qt_themes/colorful/icons/16x16/info.png differ
diff --git a/dist/qt_themes/colorful/icons/16x16/sync.png b/dist/qt_themes/colorful/icons/16x16/sync.png
new file mode 100644
index 0000000000..0d57789c3a
Binary files /dev/null and b/dist/qt_themes/colorful/icons/16x16/sync.png differ
diff --git a/dist/qt_themes/default/icons/16x16/view-refresh.png b/dist/qt_themes/colorful/icons/16x16/view-refresh.png
similarity index 100%
rename from dist/qt_themes/default/icons/16x16/view-refresh.png
rename to 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
similarity index 100%
rename from dist/qt_themes/default/icons/48x48/no_avatar.png
rename to 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
index 47e491d32c..652d61bc32 100644
Binary files a/dist/qt_themes/colorful/icons/48x48/sd_card.png and b/dist/qt_themes/colorful/icons/48x48/sd_card.png differ
diff --git a/dist/qt_themes/colorful/icons/index.theme b/dist/qt_themes/colorful/icons/index.theme
index b452aca16b..6eb3c69495 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 507e0e58b4..82cd367be9 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 9853fd438f..72451ef023 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 a07f2a9c15..2e01a34342 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
index 60dfba2693..6bcb7f6b1d 100644
Binary files a/dist/qt_themes/default/icons/48x48/sd_card.png and b/dist/qt_themes/default/icons/48x48/sd_card.png differ
diff --git a/dist/qt_themes/default/icons/index.theme b/dist/qt_themes/default/icons/index.theme
index 1edbe64084..21b35e3e3c 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
index 87ae5186d9..15e5e40245 100644
Binary files a/dist/qt_themes/qdarkstyle/icons/48x48/sd_card.png and b/dist/qt_themes/qdarkstyle/icons/48x48/sd_card.png differ
diff --git a/dist/qt_themes/qdarkstyle/icons/index.theme b/dist/qt_themes/qdarkstyle/icons/index.theme
index d1e12f3ef0..502717617c 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 447a6c8be9..20f9f6d633 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/externals/CMakeLists.txt b/externals/CMakeLists.txt
index eea70fc27d..e80fd124eb 100644
--- a/externals/CMakeLists.txt
+++ b/externals/CMakeLists.txt
@@ -16,7 +16,6 @@ endif()
 
 # Dynarmic
 if (ARCHITECTURE_x86_64)
-    set(DYNARMIC_TESTS OFF)
     set(DYNARMIC_NO_BUNDLED_FMT ON)
     set(DYNARMIC_IGNORE_ASSERTS ON CACHE BOOL "" FORCE)
     add_subdirectory(dynarmic)
diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt
index 5fe1d5fa5f..144f1bab29 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 78e615a10e..c845330cd4 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 0f7d61ee4b..e33e00a3e9 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.
+     * Get NVDEC state.
      */
-    u32 GetStreamQueue() const;
-
-    /**
-     * Get the size of the current stream queue.
-     *
-     * @param size - New stream size.
-     */
-    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 82dd32dca5..012d2ed70e 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 4aadb7fd61..f39fb40021 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 75b73a0b6f..8a519df999 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 70316e9cbd..8cbd95e22e 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 71d67de647..1766efde15 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 7a846835bb..7aba2b4230 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 6a508ec560..bf48371903 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 cae7fa9704..7128ef72a7 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 5d1979ea0c..3dae1a3b71 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 095fc96ce1..9950604147 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 4a031b7651..74f4dc0850 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 c946895d64..91ccd5ad77 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 6253891d5e..092ab72369 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 ec5d37ed44..e7f918a47b 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 165e35d83b..b9dc0e60ff 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 9a8d8a7421..d3ee4f0ebc 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 f6b9216451..946f345c61 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 35afddf06d..8b907590a9 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 4ca2f3417f..0817b2f371 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 e05a22d863..a283956631 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 4dfcef4a51..f7a2f25e4a 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 3967ccfe69..bafe4822a8 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 b6ced9d2b0..02e923c842 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 3f99173e3e..d78269e1d0 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 d5886e55e4..0d9d8f6ce5 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 1f449f2612..dd6be70eeb 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 c5d4d66d8f..3d2a913122 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 7333c297f3..15c9483442 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 06a37e1a66..c0a307b895 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 f0b445d9c0..c817d8d8df 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 496b0e50a3..1621708469 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 d80d9b0d8b..b3cd7b408f 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 2ebc140f13..7229618e87 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 ffdafa1c8d..d67123cd82 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 770f57e802..52f74a2738 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 027276e5a4..3b0ce67ef0 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 47e0c67226..e88372a75e 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 85955bd9c9..8f6d6e7d87 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 8c95838789..8525fde05c 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 7a088a6270..1ebbc5c4cd 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 b4df9f6eff..a72475c3cb 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 4cfefea8ed..bb5c930e1f 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 a1e0958a29..94b1d1254e 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 b82176bef1..b65caa9b6e 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 b326819eda..9c1331e193 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 1291e9e0ef..81457a3a18 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 70cd42b08d..83c697c0c1 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 896723e0c7..930180895b 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 90d049e8e4..36b115ad69 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,206 +143,19 @@ 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 f0f43dfa12..4b0cb160d4 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,23 +45,13 @@ public:
      *
      * @param stream - The stream to close.
      */
-    void CloseStream(const SinkStream* stream) override;
+    void CloseStream(SinkStream* stream) override;
 
     /**
      * Close all streams.
      */
     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.
      *
@@ -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 47a3421719..1215d3cd25 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 d6c9ec90dd..1bd001b941 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,216 +107,28 @@ 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 186bc2fa3a..f01eddc1b4 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,23 +43,13 @@ public:
      *
      * @param stream - The stream to close.
      */
-    void CloseStream(const SinkStream* stream) override;
+    void CloseStream(SinkStream* stream) override;
 
     /**
      * Close all streams.
      */
     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.
      *
@@ -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 91fe455e48..f28c6d1268 100644
--- a/src/audio_core/sink/sink.h
+++ b/src/audio_core/sink/sink.h
@@ -32,23 +32,13 @@ public:
      *
      * @param stream - The stream to close.
      */
-    virtual void CloseStream(const SinkStream* stream) = 0;
+    virtual void CloseStream(SinkStream* stream) = 0;
 
     /**
      * Close all streams.
      */
     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 253c0fd1e5..67bdab7799 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 0000000000..37fe725e41
--- /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 17ed6593fb..38a4b2f517 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;
     }
 
@@ -127,34 +110,6 @@ public:
         return device_channels;
     }
 
-    /**
-     * 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.
      *
@@ -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 635fb85c89..68436a4bcf 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/announce_multiplayer_room.h b/src/common/announce_multiplayer_room.h
index cb004e0eb7..4a3100fa4a 100644
--- a/src/common/announce_multiplayer_room.h
+++ b/src/common/announce_multiplayer_room.h
@@ -16,6 +16,7 @@ namespace AnnounceMultiplayerRoom {
 struct GameInfo {
     std::string name{""};
     u64 id{0};
+    std::string version{""};
 };
 
 struct Member {
diff --git a/src/common/input.h b/src/common/input.h
index 213aa2384d..825b0d650d 100644
--- a/src/common/input.h
+++ b/src/common/input.h
@@ -102,6 +102,8 @@ struct AnalogProperties {
     float offset{};
     // Invert direction of the sensor data
     bool inverted{};
+    // Press once to activate, press again to release
+    bool toggle{};
 };
 
 // Single analog sensor data
@@ -115,8 +117,11 @@ struct AnalogStatus {
 struct ButtonStatus {
     Common::UUID uuid{};
     bool value{};
+    // Invert value of the button
     bool inverted{};
+    // Press once to activate, press again to release
     bool toggle{};
+    // Internal lock for the toggle status
     bool locked{};
 };
 
diff --git a/src/common/parent_of_member.h b/src/common/parent_of_member.h
index 70b1c56248..8e03f17d8b 100644
--- a/src/common/parent_of_member.h
+++ b/src/common/parent_of_member.h
@@ -11,7 +11,7 @@ namespace Common {
 namespace detail {
 template <typename T, size_t Size, size_t Align>
 struct TypedStorageImpl {
-    std::aligned_storage_t<Size, Align> storage_;
+    alignas(Align) u8 storage_[Size];
 };
 } // namespace detail
 
diff --git a/src/common/settings.cpp b/src/common/settings.cpp
index 7282a45d32..0a560ebb72 100644
--- a/src/common/settings.cpp
+++ b/src/common/settings.cpp
@@ -195,6 +195,7 @@ void RestoreGlobalState(bool is_powered_on) {
     values.shader_backend.SetGlobal(true);
     values.use_asynchronous_shaders.SetGlobal(true);
     values.use_fast_gpu_time.SetGlobal(true);
+    values.use_pessimistic_flushes.SetGlobal(true);
     values.bg_red.SetGlobal(true);
     values.bg_green.SetGlobal(true);
     values.bg_blue.SetGlobal(true);
diff --git a/src/common/settings.h b/src/common/settings.h
index 14ed9b2377..851812f28b 100644
--- a/src/common/settings.h
+++ b/src/common/settings.h
@@ -446,6 +446,7 @@ struct Values {
                                                           ShaderBackend::SPIRV, "shader_backend"};
     SwitchableSetting<bool> use_asynchronous_shaders{false, "use_asynchronous_shaders"};
     SwitchableSetting<bool> use_fast_gpu_time{true, "use_fast_gpu_time"};
+    SwitchableSetting<bool> use_pessimistic_flushes{false, "use_pessimistic_flushes"};
 
     SwitchableSetting<u8> bg_red{0, "bg_red"};
     SwitchableSetting<u8> bg_green{0, "bg_green"};
@@ -529,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 1552f58e0f..e17a7850fe 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 8db9a3c654..33cf470d5c 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -2,16 +2,8 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 
 add_library(core STATIC
-    announce_multiplayer_session.cpp
-    announce_multiplayer_session.h
     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
@@ -527,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
@@ -540,14 +535,14 @@ add_library(core STATIC
     hle/service/npns/npns.cpp
     hle/service/npns/npns.h
     hle/service/ns/errors.h
+    hle/service/ns/iplatform_service_manager.cpp
+    hle/service/ns/iplatform_service_manager.h
     hle/service/ns/language.cpp
     hle/service/ns/language.h
     hle/service/ns/ns.cpp
     hle/service/ns/ns.h
     hle/service/ns/pdm_qry.cpp
     hle/service/ns/pdm_qry.h
-    hle/service/ns/pl_u.cpp
-    hle/service/ns/pl_u.h
     hle/service/nvdrv/devices/nvdevice.h
     hle/service/nvdrv/devices/nvdisp_disp0.cpp
     hle/service/nvdrv/devices/nvdisp_disp0.h
diff --git a/src/core/core.cpp b/src/core/core.cpp
index ea32a4a8da..1210928682 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;
@@ -319,10 +315,19 @@ struct System::Impl {
         if (app_loader->ReadTitle(name) != Loader::ResultStatus::Success) {
             LOG_ERROR(Core, "Failed to read title for ROM (Error {})", load_result);
         }
+
+        std::string title_version;
+        const FileSys::PatchManager pm(program_id, system.GetFileSystemController(),
+                                       system.GetContentProvider());
+        const auto metadata = pm.GetControlMetadata();
+        if (metadata.first != nullptr) {
+            title_version = metadata.first->GetVersionString();
+        }
         if (auto room_member = room_network.GetRoomMember().lock()) {
             Network::GameInfo game_info;
             game_info.name = name;
             game_info.id = program_id;
+            game_info.version = title_version;
             room_member->SendGameInfo(game_info);
         }
 
diff --git a/src/core/core_timing.cpp b/src/core/core_timing.cpp
index 2dbb99c8b0..f6c4567baf 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) {
-                // 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};
-                if (evt.time < pause_end_time) {
-                    next_time = pause_end_time + evt.reschedule_time;
-                }
-
                 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 + next_schedule_time};
+                if (evt.time < pause_end_time) {
+                    next_time = pause_end_time + next_schedule_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 6aa3ae923d..3259397b28 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/file_sys/system_archive/shared_font.cpp b/src/core/file_sys/system_archive/shared_font.cpp
index f841988ffc..3210583f09 100644
--- a/src/core/file_sys/system_archive/shared_font.cpp
+++ b/src/core/file_sys/system_archive/shared_font.cpp
@@ -9,7 +9,7 @@
 #include "core/file_sys/system_archive/data/font_standard.h"
 #include "core/file_sys/system_archive/shared_font.h"
 #include "core/file_sys/vfs_vector.h"
-#include "core/hle/service/ns/pl_u.h"
+#include "core/hle/service/ns/iplatform_service_manager.h"
 
 namespace FileSys::SystemArchive {
 
diff --git a/src/core/hid/emulated_controller.cpp b/src/core/hid/emulated_controller.cpp
index f9f902c2dd..01c43be934 100644
--- a/src/core/hid/emulated_controller.cpp
+++ b/src/core/hid/emulated_controller.cpp
@@ -562,6 +562,16 @@ void EmulatedController::SetButton(const Common::Input::CallbackStatus& callback
         return;
     }
 
+    // GC controllers have triggers not buttons
+    if (npad_type == NpadStyleIndex::GameCube) {
+        if (index == Settings::NativeButton::ZR) {
+            return;
+        }
+        if (index == Settings::NativeButton::ZL) {
+            return;
+        }
+    }
+
     switch (index) {
     case Settings::NativeButton::A:
         controller.npad_button_state.a.Assign(current_status.value);
@@ -738,6 +748,11 @@ void EmulatedController::SetTrigger(const Common::Input::CallbackStatus& callbac
         return;
     }
 
+    // Only GC controllers have analog triggers
+    if (npad_type != NpadStyleIndex::GameCube) {
+        return;
+    }
+
     const auto& trigger = controller.trigger_values[index];
 
     switch (index) {
diff --git a/src/core/hid/input_converter.cpp b/src/core/hid/input_converter.cpp
index 68d143a013..52fb69e9c8 100644
--- a/src/core/hid/input_converter.cpp
+++ b/src/core/hid/input_converter.cpp
@@ -52,6 +52,9 @@ Common::Input::ButtonStatus TransformToButton(const Common::Input::CallbackStatu
     Common::Input::ButtonStatus status{};
     switch (callback.type) {
     case Common::Input::InputType::Analog:
+        status.value = TransformToTrigger(callback).pressed.value;
+        status.toggle = callback.analog_status.properties.toggle;
+        break;
     case Common::Input::InputType::Trigger:
         status.value = TransformToTrigger(callback).pressed.value;
         break;
diff --git a/src/core/hle/result.h b/src/core/hle/result.h
index 4de44cd061..47a1b829b2 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/acc/acc.cpp b/src/core/hle/service/acc/acc.cpp
index def1058321..bb838e2850 100644
--- a/src/core/hle/service/acc/acc.cpp
+++ b/src/core/hle/service/acc/acc.cpp
@@ -534,7 +534,7 @@ public:
 
 private:
     void CheckAvailability(Kernel::HLERequestContext& ctx) {
-        LOG_WARNING(Service_ACC, "(STUBBED) called");
+        LOG_DEBUG(Service_ACC, "(STUBBED) called");
         IPC::ResponseBuilder rb{ctx, 3};
         rb.Push(ResultSuccess);
         rb.Push(false); // TODO: Check when this is supposed to return true and when not
diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp
index 118f226e41..6fb7e198ea 100644
--- a/src/core/hle/service/am/am.cpp
+++ b/src/core/hle/service/am/am.cpp
@@ -754,7 +754,7 @@ void ICommonStateGetter::ReceiveMessage(Kernel::HLERequestContext& ctx) {
 }
 
 void ICommonStateGetter::GetCurrentFocusState(Kernel::HLERequestContext& ctx) {
-    LOG_WARNING(Service_AM, "(STUBBED) called");
+    LOG_DEBUG(Service_AM, "(STUBBED) called");
 
     IPC::ResponseBuilder rb{ctx, 3};
     rb.Push(ResultSuccess);
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 1b145b6963..4705d019fa 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/am/applets/applet_web_browser.cpp b/src/core/hle/service/am/applets/applet_web_browser.cpp
index 4b804b78cf..14aa6f69ee 100644
--- a/src/core/hle/service/am/applets/applet_web_browser.cpp
+++ b/src/core/hle/service/am/applets/applet_web_browser.cpp
@@ -21,7 +21,7 @@
 #include "core/hle/service/am/am.h"
 #include "core/hle/service/am/applets/applet_web_browser.h"
 #include "core/hle/service/filesystem/filesystem.h"
-#include "core/hle/service/ns/pl_u.h"
+#include "core/hle/service/ns/iplatform_service_manager.h"
 #include "core/loader/loader.h"
 
 namespace Service::AM::Applets {
diff --git a/src/core/hle/service/apm/apm_controller.cpp b/src/core/hle/service/apm/apm_controller.cpp
index 4e710491b6..d6de84066c 100644
--- a/src/core/hle/service/apm/apm_controller.cpp
+++ b/src/core/hle/service/apm/apm_controller.cpp
@@ -80,7 +80,7 @@ PerformanceConfiguration Controller::GetCurrentPerformanceConfiguration(Performa
 }
 
 void Controller::SetClockSpeed(u32 mhz) {
-    LOG_INFO(Service_APM, "called, mhz={:08X}", mhz);
+    LOG_DEBUG(Service_APM, "called, mhz={:08X}", mhz);
     // TODO(DarkLordZach): Actually signal core_timing to change clock speed.
     // TODO(Rodrigo): Remove [[maybe_unused]] when core_timing is used.
 }
diff --git a/src/core/hle/service/audio/audout_u.cpp b/src/core/hle/service/audio/audout_u.cpp
index a44dd842a0..49c0923016 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 bc69117c62..6fb07c37dc 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/audio/hwopus.cpp b/src/core/hle/service/audio/hwopus.cpp
index 4f2ed2d52e..8bafc3a983 100644
--- a/src/core/hle/service/audio/hwopus.cpp
+++ b/src/core/hle/service/audio/hwopus.cpp
@@ -255,6 +255,32 @@ void HwOpus::GetWorkBufferSizeEx(Kernel::HLERequestContext& ctx) {
     GetWorkBufferSize(ctx);
 }
 
+void HwOpus::GetWorkBufferSizeForMultiStreamEx(Kernel::HLERequestContext& ctx) {
+    OpusMultiStreamParametersEx param;
+    std::memcpy(&param, ctx.ReadBuffer().data(), ctx.GetReadBufferSize());
+
+    const auto sample_rate = param.sample_rate;
+    const auto channel_count = param.channel_count;
+    const auto number_streams = param.number_streams;
+    const auto number_stereo_streams = param.number_stereo_streams;
+
+    LOG_DEBUG(
+        Audio,
+        "called with sample_rate={}, channel_count={}, number_streams={}, number_stereo_streams={}",
+        sample_rate, channel_count, number_streams, number_stereo_streams);
+
+    ASSERT_MSG(sample_rate == 48000 || sample_rate == 24000 || sample_rate == 16000 ||
+                   sample_rate == 12000 || sample_rate == 8000,
+               "Invalid sample rate");
+
+    const u32 worker_buffer_sz =
+        static_cast<u32>(opus_multistream_decoder_get_size(number_streams, number_stereo_streams));
+
+    IPC::ResponseBuilder rb{ctx, 3};
+    rb.Push(ResultSuccess);
+    rb.Push<u32>(worker_buffer_sz);
+}
+
 void HwOpus::OpenHardwareOpusDecoder(Kernel::HLERequestContext& ctx) {
     IPC::RequestParser rp{ctx};
     const auto sample_rate = rp.Pop<u32>();
@@ -335,7 +361,7 @@ HwOpus::HwOpus(Core::System& system_) : ServiceFramework{system_, "hwopus"} {
         {4, &HwOpus::OpenHardwareOpusDecoderEx, "OpenHardwareOpusDecoderEx"},
         {5, &HwOpus::GetWorkBufferSizeEx, "GetWorkBufferSizeEx"},
         {6, nullptr, "OpenHardwareOpusDecoderForMultiStreamEx"},
-        {7, nullptr, "GetWorkBufferSizeForMultiStreamEx"},
+        {7, &HwOpus::GetWorkBufferSizeForMultiStreamEx, "GetWorkBufferSizeForMultiStreamEx"},
     };
     RegisterHandlers(functions);
 }
diff --git a/src/core/hle/service/audio/hwopus.h b/src/core/hle/service/audio/hwopus.h
index 265dd0cc63..e6092e2903 100644
--- a/src/core/hle/service/audio/hwopus.h
+++ b/src/core/hle/service/audio/hwopus.h
@@ -11,6 +11,16 @@ class System;
 
 namespace Service::Audio {
 
+struct OpusMultiStreamParametersEx {
+    u32 sample_rate;
+    u32 channel_count;
+    u32 number_streams;
+    u32 number_stereo_streams;
+    u32 use_large_frame_size;
+    u32 padding;
+    std::array<u32, 64> channel_mappings;
+};
+
 class HwOpus final : public ServiceFramework<HwOpus> {
 public:
     explicit HwOpus(Core::System& system_);
@@ -21,6 +31,7 @@ private:
     void OpenHardwareOpusDecoderEx(Kernel::HLERequestContext& ctx);
     void GetWorkBufferSize(Kernel::HLERequestContext& ctx);
     void GetWorkBufferSizeEx(Kernel::HLERequestContext& ctx);
+    void GetWorkBufferSizeForMultiStreamEx(Kernel::HLERequestContext& ctx);
 };
 
 } // namespace Service::Audio
diff --git a/src/core/hle/service/hid/hid.cpp b/src/core/hle/service/hid/hid.cpp
index 7909141c01..3d34571600 100644
--- a/src/core/hle/service/hid/hid.cpp
+++ b/src/core/hle/service/hid/hid.cpp
@@ -819,12 +819,12 @@ void Hid::EnableSixAxisSensorUnalteredPassthrough(Kernel::HLERequestContext& ctx
     const auto result = controller.EnableSixAxisSensorUnalteredPassthrough(
         parameters.sixaxis_handle, parameters.enabled);
 
-    LOG_WARNING(Service_HID,
-                "(STUBBED) called, enabled={}, npad_type={}, npad_id={}, device_index={}, "
-                "applet_resource_user_id={}",
-                parameters.enabled, parameters.sixaxis_handle.npad_type,
-                parameters.sixaxis_handle.npad_id, parameters.sixaxis_handle.device_index,
-                parameters.applet_resource_user_id);
+    LOG_DEBUG(Service_HID,
+              "(STUBBED) called, enabled={}, npad_type={}, npad_id={}, device_index={}, "
+              "applet_resource_user_id={}",
+              parameters.enabled, parameters.sixaxis_handle.npad_type,
+              parameters.sixaxis_handle.npad_id, parameters.sixaxis_handle.device_index,
+              parameters.applet_resource_user_id);
 
     IPC::ResponseBuilder rb{ctx, 2};
     rb.Push(result);
@@ -846,7 +846,7 @@ void Hid::IsSixAxisSensorUnalteredPassthroughEnabled(Kernel::HLERequestContext&
     const auto result = controller.IsSixAxisSensorUnalteredPassthroughEnabled(
         parameters.sixaxis_handle, is_unaltered_sisxaxis_enabled);
 
-    LOG_WARNING(
+    LOG_DEBUG(
         Service_HID,
         "(STUBBED) called, npad_type={}, npad_id={}, device_index={}, applet_resource_user_id={}",
         parameters.sixaxis_handle.npad_type, parameters.sixaxis_handle.npad_id,
diff --git a/src/core/hle/service/ldn/ldn_types.h b/src/core/hle/service/ldn/ldn_types.h
index 0c07a7397a..6231e936da 100644
--- a/src/core/hle/service/ldn/ldn_types.h
+++ b/src/core/hle/service/ldn/ldn_types.h
@@ -113,7 +113,7 @@ enum class LinkLevel : s8 {
     Bad,
     Low,
     Good,
-    Excelent,
+    Excellent,
 };
 
 struct NodeLatestUpdate {
@@ -145,11 +145,19 @@ struct NetworkId {
 static_assert(sizeof(NetworkId) == 0x20, "NetworkId is an invalid size");
 
 struct Ssid {
-    u8 length;
-    std::array<char, SsidLengthMax + 1> raw;
+    u8 length{};
+    std::array<char, SsidLengthMax + 1> raw{};
+
+    Ssid() = default;
+
+    explicit Ssid(std::string_view data) {
+        length = static_cast<u8>(std::min(data.size(), SsidLengthMax));
+        data.copy(raw.data(), length);
+        raw[length] = 0;
+    }
 
     std::string GetStringValue() const {
-        return std::string(raw.data(), length);
+        return std::string(raw.data());
     }
 };
 static_assert(sizeof(Ssid) == 0x22, "Ssid is an invalid size");
diff --git a/src/core/hle/service/mii/mii.cpp b/src/core/hle/service/mii/mii.cpp
index efb5699932..390514fdcc 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 544c92a001..c484a9c8de 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 6a286bd966..d847de0bda 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 45edbfeae3..9e3247397f 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/mm/mm_u.cpp b/src/core/hle/service/mm/mm_u.cpp
index 5ebb124a77..ba8c0e2305 100644
--- a/src/core/hle/service/mm/mm_u.cpp
+++ b/src/core/hle/service/mm/mm_u.cpp
@@ -46,7 +46,7 @@ private:
         IPC::RequestParser rp{ctx};
         min = rp.Pop<u32>();
         max = rp.Pop<u32>();
-        LOG_WARNING(Service_MM, "(STUBBED) called, min=0x{:X}, max=0x{:X}", min, max);
+        LOG_DEBUG(Service_MM, "(STUBBED) called, min=0x{:X}, max=0x{:X}", min, max);
 
         current = min;
         IPC::ResponseBuilder rb{ctx, 2};
@@ -54,7 +54,7 @@ private:
     }
 
     void GetOld(Kernel::HLERequestContext& ctx) {
-        LOG_WARNING(Service_MM, "(STUBBED) called");
+        LOG_DEBUG(Service_MM, "(STUBBED) called");
 
         IPC::ResponseBuilder rb{ctx, 3};
         rb.Push(ResultSuccess);
@@ -81,8 +81,8 @@ private:
         u32 input_id = rp.Pop<u32>();
         min = rp.Pop<u32>();
         max = rp.Pop<u32>();
-        LOG_WARNING(Service_MM, "(STUBBED) called, input_id=0x{:X}, min=0x{:X}, max=0x{:X}",
-                    input_id, min, max);
+        LOG_DEBUG(Service_MM, "(STUBBED) called, input_id=0x{:X}, min=0x{:X}, max=0x{:X}", input_id,
+                  min, max);
 
         current = min;
         IPC::ResponseBuilder rb{ctx, 2};
@@ -90,7 +90,7 @@ private:
     }
 
     void Get(Kernel::HLERequestContext& ctx) {
-        LOG_WARNING(Service_MM, "(STUBBED) called");
+        LOG_DEBUG(Service_MM, "(STUBBED) called");
 
         IPC::ResponseBuilder rb{ctx, 3};
         rb.Push(ResultSuccess);
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 0000000000..31dd3a307f
--- /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 0000000000..af7335912f
--- /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 0000000000..bf2de811ad
--- /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 6c5b41dd14..e0ed3f771d 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,37 +645,43 @@ void Module::Interface::CreateUserInterface(Kernel::HLERequestContext& ctx) {
     rb.PushIpcInterface<IUser>(*this, system);
 }
 
-bool Module::Interface::LoadAmiibo(const std::vector<u8>& buffer) {
+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};
+
+    if (!amiibo_file.IsOpen()) {
+        LOG_ERROR(Service_NFP, "Amiibo is already on use");
+        return false;
+    }
+
+    // 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));
+
+    if (!AmiiboCrypto::IsAmiiboValid(encrypted_tag_data)) {
+        LOG_INFO(Service_NFP, "Invalid amiibo");
+        return false;
+    }
+
+    file_path = filename;
+    return true;
+}
+
+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;
     }
 
-    constexpr auto tag_size = sizeof(NTAG215File);
-    constexpr auto tag_size_without_password = sizeof(NTAG215File) - sizeof(NTAG215Password);
-
-    std::vector<u8> amiibo_buffer = buffer;
-
-    if (amiibo_buffer.size() < tag_size_without_password) {
-        LOG_ERROR(Service_NFP, "Wrong file size {}", buffer.size());
+    if (!LoadAmiiboFile(filename)) {
         return false;
     }
 
-    // Ensure it has the correct size
-    if (amiibo_buffer.size() != tag_size) {
-        amiibo_buffer.resize(tag_size, 0);
-    }
-
-    LOG_INFO(Service_NFP, "Amiibo detected");
-    std::memcpy(&tag_data, buffer.data(), tag_size);
-
-    if (!IsAmiiboValid()) {
-        return false;
-    }
-
-    // This value can't be dumped from a tag. Generate it
-    tag_data.password.PWD = GetTagPassword(tag_data.uuid);
-
     device_state = DeviceState::TagFound;
     activate_event->GetWritableEvent().Signal();
     return true;
@@ -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;
-    }
-    if (!is_application_area_initialized) {
+
+    // Fallback for lack of amiibo keys
+    if (!is_data_decoded) {
         LOG_WARNING(Service_NFP, "Application area is not initialized");
         return ErrCodes::ApplicationAreaIsNotInitialized;
     }
+
+    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;
+    }
 
-void Module::Interface::SaveAmiiboApplicationData(u32 access_id,
-                                                  const std::vector<u8>& data) const {
-    // TODO(german77): Save file
+    std::memcpy(&tag_data.application_area, data.data(), sizeof(ApplicationArea));
+    tag_data.application_area_id = access_id;
+
+    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 0fc8087813..0de0b48e71 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/ns/pl_u.cpp b/src/core/hle/service/ns/iplatform_service_manager.cpp
similarity index 89%
rename from src/core/hle/service/ns/pl_u.cpp
rename to src/core/hle/service/ns/iplatform_service_manager.cpp
index cc11f3e082..fd047ff26d 100644
--- a/src/core/hle/service/ns/pl_u.cpp
+++ b/src/core/hle/service/ns/iplatform_service_manager.cpp
@@ -20,7 +20,7 @@
 #include "core/hle/kernel/kernel.h"
 #include "core/hle/kernel/physical_memory.h"
 #include "core/hle/service/filesystem/filesystem.h"
-#include "core/hle/service/ns/pl_u.h"
+#include "core/hle/service/ns/iplatform_service_manager.h"
 
 namespace Service::NS {
 
@@ -99,7 +99,7 @@ static u32 GetU32Swapped(const u8* data) {
     return Common::swap32(value);
 }
 
-struct PL_U::Impl {
+struct IPlatformServiceManager::Impl {
     const FontRegion& GetSharedFontRegion(std::size_t index) const {
         if (index >= shared_font_regions.size() || shared_font_regions.empty()) {
             // No font fallback
@@ -134,16 +134,16 @@ struct PL_U::Impl {
     std::vector<FontRegion> shared_font_regions;
 };
 
-PL_U::PL_U(Core::System& system_)
-    : ServiceFramework{system_, "pl:u"}, impl{std::make_unique<Impl>()} {
+IPlatformServiceManager::IPlatformServiceManager(Core::System& system_, const char* service_name_)
+    : ServiceFramework{system_, service_name_}, impl{std::make_unique<Impl>()} {
     // clang-format off
     static const FunctionInfo functions[] = {
-        {0, &PL_U::RequestLoad, "RequestLoad"},
-        {1, &PL_U::GetLoadState, "GetLoadState"},
-        {2, &PL_U::GetSize, "GetSize"},
-        {3, &PL_U::GetSharedMemoryAddressOffset, "GetSharedMemoryAddressOffset"},
-        {4, &PL_U::GetSharedMemoryNativeHandle, "GetSharedMemoryNativeHandle"},
-        {5, &PL_U::GetSharedFontInOrderOfPriority, "GetSharedFontInOrderOfPriority"},
+        {0, &IPlatformServiceManager::RequestLoad, "RequestLoad"},
+        {1, &IPlatformServiceManager::GetLoadState, "GetLoadState"},
+        {2, &IPlatformServiceManager::GetSize, "GetSize"},
+        {3, &IPlatformServiceManager::GetSharedMemoryAddressOffset, "GetSharedMemoryAddressOffset"},
+        {4, &IPlatformServiceManager::GetSharedMemoryNativeHandle, "GetSharedMemoryNativeHandle"},
+        {5, &IPlatformServiceManager::GetSharedFontInOrderOfPriority, "GetSharedFontInOrderOfPriority"},
         {6, nullptr, "GetSharedFontInOrderOfPriorityForSystem"},
         {100, nullptr, "RequestApplicationFunctionAuthorization"},
         {101, nullptr, "RequestApplicationFunctionAuthorizationByProcessId"},
@@ -206,9 +206,9 @@ PL_U::PL_U(Core::System& system_)
     }
 }
 
-PL_U::~PL_U() = default;
+IPlatformServiceManager::~IPlatformServiceManager() = default;
 
-void PL_U::RequestLoad(Kernel::HLERequestContext& ctx) {
+void IPlatformServiceManager::RequestLoad(Kernel::HLERequestContext& ctx) {
     IPC::RequestParser rp{ctx};
     const u32 shared_font_type{rp.Pop<u32>()};
     // Games don't call this so all fonts should be loaded
@@ -218,7 +218,7 @@ void PL_U::RequestLoad(Kernel::HLERequestContext& ctx) {
     rb.Push(ResultSuccess);
 }
 
-void PL_U::GetLoadState(Kernel::HLERequestContext& ctx) {
+void IPlatformServiceManager::GetLoadState(Kernel::HLERequestContext& ctx) {
     IPC::RequestParser rp{ctx};
     const u32 font_id{rp.Pop<u32>()};
     LOG_DEBUG(Service_NS, "called, font_id={}", font_id);
@@ -228,7 +228,7 @@ void PL_U::GetLoadState(Kernel::HLERequestContext& ctx) {
     rb.Push<u32>(static_cast<u32>(LoadState::Done));
 }
 
-void PL_U::GetSize(Kernel::HLERequestContext& ctx) {
+void IPlatformServiceManager::GetSize(Kernel::HLERequestContext& ctx) {
     IPC::RequestParser rp{ctx};
     const u32 font_id{rp.Pop<u32>()};
     LOG_DEBUG(Service_NS, "called, font_id={}", font_id);
@@ -238,7 +238,7 @@ void PL_U::GetSize(Kernel::HLERequestContext& ctx) {
     rb.Push<u32>(impl->GetSharedFontRegion(font_id).size);
 }
 
-void PL_U::GetSharedMemoryAddressOffset(Kernel::HLERequestContext& ctx) {
+void IPlatformServiceManager::GetSharedMemoryAddressOffset(Kernel::HLERequestContext& ctx) {
     IPC::RequestParser rp{ctx};
     const u32 font_id{rp.Pop<u32>()};
     LOG_DEBUG(Service_NS, "called, font_id={}", font_id);
@@ -248,7 +248,7 @@ void PL_U::GetSharedMemoryAddressOffset(Kernel::HLERequestContext& ctx) {
     rb.Push<u32>(impl->GetSharedFontRegion(font_id).offset);
 }
 
-void PL_U::GetSharedMemoryNativeHandle(Kernel::HLERequestContext& ctx) {
+void IPlatformServiceManager::GetSharedMemoryNativeHandle(Kernel::HLERequestContext& ctx) {
     // Map backing memory for the font data
     LOG_DEBUG(Service_NS, "called");
 
@@ -261,7 +261,7 @@ void PL_U::GetSharedMemoryNativeHandle(Kernel::HLERequestContext& ctx) {
     rb.PushCopyObjects(&kernel.GetFontSharedMem());
 }
 
-void PL_U::GetSharedFontInOrderOfPriority(Kernel::HLERequestContext& ctx) {
+void IPlatformServiceManager::GetSharedFontInOrderOfPriority(Kernel::HLERequestContext& ctx) {
     IPC::RequestParser rp{ctx};
     const u64 language_code{rp.Pop<u64>()}; // TODO(ogniK): Find out what this is used for
     LOG_DEBUG(Service_NS, "called, language_code={:X}", language_code);
diff --git a/src/core/hle/service/ns/pl_u.h b/src/core/hle/service/ns/iplatform_service_manager.h
similarity index 89%
rename from src/core/hle/service/ns/pl_u.h
rename to src/core/hle/service/ns/iplatform_service_manager.h
index 07d0ac9343..ed6eda89f2 100644
--- a/src/core/hle/service/ns/pl_u.h
+++ b/src/core/hle/service/ns/iplatform_service_manager.h
@@ -36,10 +36,10 @@ constexpr std::array<std::pair<FontArchives, const char*>, 7> SHARED_FONTS{
 void DecryptSharedFontToTTF(const std::vector<u32>& input, std::vector<u8>& output);
 void EncryptSharedFont(const std::vector<u32>& input, std::vector<u8>& output, std::size_t& offset);
 
-class PL_U final : public ServiceFramework<PL_U> {
+class IPlatformServiceManager final : public ServiceFramework<IPlatformServiceManager> {
 public:
-    explicit PL_U(Core::System& system_);
-    ~PL_U() override;
+    explicit IPlatformServiceManager(Core::System& system_, const char* service_name_);
+    ~IPlatformServiceManager() override;
 
 private:
     void RequestLoad(Kernel::HLERequestContext& ctx);
diff --git a/src/core/hle/service/ns/ns.cpp b/src/core/hle/service/ns/ns.cpp
index aafc8fe03f..f7318c3cb8 100644
--- a/src/core/hle/service/ns/ns.cpp
+++ b/src/core/hle/service/ns/ns.cpp
@@ -9,10 +9,10 @@
 #include "core/file_sys/vfs.h"
 #include "core/hle/ipc_helpers.h"
 #include "core/hle/service/ns/errors.h"
+#include "core/hle/service/ns/iplatform_service_manager.h"
 #include "core/hle/service/ns/language.h"
 #include "core/hle/service/ns/ns.h"
 #include "core/hle/service/ns/pdm_qry.h"
-#include "core/hle/service/ns/pl_u.h"
 #include "core/hle/service/set/set.h"
 
 namespace Service::NS {
@@ -764,7 +764,8 @@ void InstallInterfaces(SM::ServiceManager& service_manager, Core::System& system
 
     std::make_shared<PDM_QRY>(system)->InstallAsService(service_manager);
 
-    std::make_shared<PL_U>(system)->InstallAsService(service_manager);
+    std::make_shared<IPlatformServiceManager>(system, "pl:s")->InstallAsService(service_manager);
+    std::make_shared<IPlatformServiceManager>(system, "pl:u")->InstallAsService(service_manager);
 }
 
 } // namespace Service::NS
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_nvdec.cpp b/src/core/hle/service/nvdrv/devices/nvhost_nvdec.cpp
index 2a5128c602..a7385fce81 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/nvdrv/devices/nvmap.cpp b/src/core/hle/service/nvdrv/devices/nvmap.cpp
index 728bfa00b9..d8518149d7 100644
--- a/src/core/hle/service/nvdrv/devices/nvmap.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvmap.cpp
@@ -198,7 +198,7 @@ NvResult nvmap::IocParam(const std::vector<u8>& input, std::vector<u8>& output)
     IocParamParams params;
     std::memcpy(&params, input.data(), sizeof(params));
 
-    LOG_WARNING(Service_NVDRV, "(STUBBED) called type={}", params.param);
+    LOG_DEBUG(Service_NVDRV, "(STUBBED) called type={}", params.param);
 
     auto object = GetObject(params.handle);
     if (!object) {
@@ -243,7 +243,7 @@ NvResult nvmap::IocFree(const std::vector<u8>& input, std::vector<u8>& output) {
     IocFreeParams params;
     std::memcpy(&params, input.data(), sizeof(params));
 
-    LOG_WARNING(Service_NVDRV, "(STUBBED) called");
+    LOG_DEBUG(Service_NVDRV, "(STUBBED) called");
 
     auto itr = handles.find(params.handle);
     if (itr == handles.end()) {
diff --git a/src/core/hle/service/nvflinger/nvflinger.cpp b/src/core/hle/service/nvflinger/nvflinger.cpp
index 5574269eb2..9b382bf566 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 4775597cce..044ac6ac8d 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/core/hle/service/sockets/bsd.cpp b/src/core/hle/service/sockets/bsd.cpp
index e08c3cb67a..cc679cc81d 100644
--- a/src/core/hle/service/sockets/bsd.cpp
+++ b/src/core/hle/service/sockets/bsd.cpp
@@ -933,7 +933,11 @@ BSD::BSD(Core::System& system_, const char* name)
     }
 }
 
-BSD::~BSD() = default;
+BSD::~BSD() {
+    if (auto room_member = room_network.GetRoomMember().lock()) {
+        room_member->Unbind(proxy_packet_received);
+    }
+}
 
 BSDCFG::BSDCFG(Core::System& system_) : ServiceFramework{system_, "bsdcfg"} {
     // clang-format off
diff --git a/src/core/internal_network/socket_proxy.cpp b/src/core/internal_network/socket_proxy.cpp
index 49d067f4c2..0c746bd824 100644
--- a/src/core/internal_network/socket_proxy.cpp
+++ b/src/core/internal_network/socket_proxy.cpp
@@ -26,6 +26,12 @@ void ProxySocket::HandleProxyPacket(const ProxyPacket& packet) {
         closed) {
         return;
     }
+
+    if (!broadcast && packet.broadcast) {
+        LOG_INFO(Network, "Received broadcast packet, but not configured for broadcast mode");
+        return;
+    }
+
     std::lock_guard guard(packets_mutex);
     received_packets.push(packet);
 }
@@ -203,7 +209,7 @@ std::pair<s32, Errno> ProxySocket::SendTo(u32 flags, const std::vector<u8>& mess
     packet.local_endpoint = local_endpoint;
     packet.remote_endpoint = *addr;
     packet.protocol = protocol;
-    packet.broadcast = broadcast;
+    packet.broadcast = broadcast && packet.remote_endpoint.ip[3] == 255;
 
     auto& ip = local_endpoint.ip;
     auto ipv4 = Network::GetHostIPv4Address();
diff --git a/src/dedicated_room/CMakeLists.txt b/src/dedicated_room/CMakeLists.txt
index b674b915b7..1efdbc1f76 100644
--- a/src/dedicated_room/CMakeLists.txt
+++ b/src/dedicated_room/CMakeLists.txt
@@ -10,13 +10,13 @@ add_executable(yuzu-room
 
 create_target_directory_groups(yuzu-room)
 
-target_link_libraries(yuzu-room PRIVATE common core network)
+target_link_libraries(yuzu-room PRIVATE common network)
 if (ENABLE_WEB_SERVICE)
     target_compile_definitions(yuzu-room PRIVATE -DENABLE_WEB_SERVICE)
     target_link_libraries(yuzu-room PRIVATE web_service)
 endif()
 
-target_link_libraries(yuzu-room PRIVATE mbedtls)
+target_link_libraries(yuzu-room PRIVATE mbedtls mbedcrypto)
 if (MSVC)
     target_link_libraries(yuzu-room PRIVATE getopt)
 endif()
diff --git a/src/dedicated_room/yuzu_room.cpp b/src/dedicated_room/yuzu_room.cpp
index 482e772fb3..7b6deba417 100644
--- a/src/dedicated_room/yuzu_room.cpp
+++ b/src/dedicated_room/yuzu_room.cpp
@@ -27,8 +27,8 @@
 #include "common/scm_rev.h"
 #include "common/settings.h"
 #include "common/string_util.h"
-#include "core/announce_multiplayer_session.h"
 #include "core/core.h"
+#include "network/announce_multiplayer_session.h"
 #include "network/network.h"
 #include "network/room.h"
 #include "network/verify_user.h"
@@ -75,6 +75,12 @@ static constexpr char BanListMagic[] = "YuzuRoom-BanList-1";
 
 static constexpr char token_delimiter{':'};
 
+static void PadToken(std::string& token) {
+    while (token.size() % 4 != 0) {
+        token.push_back('=');
+    }
+}
+
 static std::string UsernameFromDisplayToken(const std::string& display_token) {
     std::size_t outlen;
 
@@ -300,6 +306,7 @@ int main(int argc, char** argv) {
         if (username.empty()) {
             LOG_INFO(Network, "Hosting a public room");
             Settings::values.web_api_url = web_api_url;
+            PadToken(token);
             Settings::values.yuzu_username = UsernameFromDisplayToken(token);
             username = Settings::values.yuzu_username.GetValue();
             Settings::values.yuzu_token = TokenFromDisplayToken(token);
diff --git a/src/input_common/drivers/sdl_driver.cpp b/src/input_common/drivers/sdl_driver.cpp
index de388ec4cf..5cc1ccbd97 100644
--- a/src/input_common/drivers/sdl_driver.cpp
+++ b/src/input_common/drivers/sdl_driver.cpp
@@ -40,13 +40,13 @@ public:
     void EnableMotion() {
         if (sdl_controller) {
             SDL_GameController* controller = sdl_controller.get();
-            if (SDL_GameControllerHasSensor(controller, SDL_SENSOR_ACCEL) && !has_accel) {
+            has_accel = SDL_GameControllerHasSensor(controller, SDL_SENSOR_ACCEL);
+            has_gyro = SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO);
+            if (has_accel) {
                 SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE);
-                has_accel = true;
             }
-            if (SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO) && !has_gyro) {
+            if (has_gyro) {
                 SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE);
-                has_gyro = true;
             }
         }
     }
@@ -305,6 +305,7 @@ void SDLDriver::InitJoystick(int joystick_index) {
         auto joystick = std::make_shared<SDLJoystick>(guid, 0, sdl_joystick, sdl_gamecontroller);
         PreSetController(joystick->GetPadIdentifier());
         SetBattery(joystick->GetPadIdentifier(), joystick->GetBatteryLevel());
+        joystick->EnableMotion();
         joystick_map[guid].emplace_back(std::move(joystick));
         return;
     }
@@ -316,6 +317,7 @@ void SDLDriver::InitJoystick(int joystick_index) {
 
     if (joystick_it != joystick_guid_list.end()) {
         (*joystick_it)->SetSDLJoystick(sdl_joystick, sdl_gamecontroller);
+        (*joystick_it)->EnableMotion();
         return;
     }
 
@@ -323,6 +325,7 @@ void SDLDriver::InitJoystick(int joystick_index) {
     auto joystick = std::make_shared<SDLJoystick>(guid, port, sdl_joystick, sdl_gamecontroller);
     PreSetController(joystick->GetPadIdentifier());
     SetBattery(joystick->GetPadIdentifier(), joystick->GetBatteryLevel());
+    joystick->EnableMotion();
     joystick_guid_list.emplace_back(std::move(joystick));
 }
 
diff --git a/src/input_common/input_poller.cpp b/src/input_common/input_poller.cpp
index 133422d5cd..ffb9b945e5 100644
--- a/src/input_common/input_poller.cpp
+++ b/src/input_common/input_poller.cpp
@@ -824,6 +824,7 @@ std::unique_ptr<Common::Input::InputDevice> InputFactory::CreateAnalogDevice(
         .threshold = std::clamp(params.Get("threshold", 0.5f), 0.0f, 1.0f),
         .offset = std::clamp(params.Get("offset", 0.0f), -1.0f, 1.0f),
         .inverted = params.Get("invert", "+") == "-",
+        .toggle = static_cast<bool>(params.Get("toggle", false)),
     };
     input_engine->PreSetController(identifier);
     input_engine->PreSetAxis(identifier, axis);
diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt
index 312f79b686..6f8ca4b90c 100644
--- a/src/network/CMakeLists.txt
+++ b/src/network/CMakeLists.txt
@@ -2,6 +2,8 @@
 # SPDX-License-Identifier: GPL-3.0-or-later
 
 add_library(network STATIC
+    announce_multiplayer_session.cpp
+    announce_multiplayer_session.h
     network.cpp
     network.h
     packet.cpp
@@ -17,3 +19,7 @@ add_library(network STATIC
 create_target_directory_groups(network)
 
 target_link_libraries(network PRIVATE common enet Boost::boost)
+if (ENABLE_WEB_SERVICE)
+    target_compile_definitions(network PRIVATE -DENABLE_WEB_SERVICE)
+    target_link_libraries(network PRIVATE web_service)
+endif()
diff --git a/src/core/announce_multiplayer_session.cpp b/src/network/announce_multiplayer_session.cpp
similarity index 100%
rename from src/core/announce_multiplayer_session.cpp
rename to src/network/announce_multiplayer_session.cpp
diff --git a/src/core/announce_multiplayer_session.h b/src/network/announce_multiplayer_session.h
similarity index 100%
rename from src/core/announce_multiplayer_session.h
rename to src/network/announce_multiplayer_session.h
diff --git a/src/network/room.cpp b/src/network/room.cpp
index b06797bf11..8c63b255bc 100644
--- a/src/network/room.cpp
+++ b/src/network/room.cpp
@@ -221,7 +221,7 @@ public:
      * Extracts the game name from a received ENet packet and broadcasts it.
      * @param event The ENet event that was received.
      */
-    void HandleGameNamePacket(const ENetEvent* event);
+    void HandleGameInfoPacket(const ENetEvent* event);
 
     /**
      * Removes the client from the members list if it was in it and announces the change
@@ -234,7 +234,7 @@ public:
 void Room::RoomImpl::ServerLoop() {
     while (state != State::Closed) {
         ENetEvent event;
-        if (enet_host_service(server, &event, 50) > 0) {
+        if (enet_host_service(server, &event, 5) > 0) {
             switch (event.type) {
             case ENET_EVENT_TYPE_RECEIVE:
                 switch (event.packet->data[0]) {
@@ -242,7 +242,7 @@ void Room::RoomImpl::ServerLoop() {
                     HandleJoinRequest(&event);
                     break;
                 case IdSetGameInfo:
-                    HandleGameNamePacket(&event);
+                    HandleGameInfoPacket(&event);
                     break;
                 case IdProxyPacket:
                     HandleProxyPacket(&event);
@@ -778,6 +778,7 @@ void Room::RoomImpl::BroadcastRoomInformation() {
             packet.Write(member.fake_ip);
             packet.Write(member.game_info.name);
             packet.Write(member.game_info.id);
+            packet.Write(member.game_info.version);
             packet.Write(member.user_data.username);
             packet.Write(member.user_data.display_name);
             packet.Write(member.user_data.avatar_url);
@@ -817,6 +818,7 @@ void Room::RoomImpl::HandleProxyPacket(const ENetEvent* event) {
     in_packet.IgnoreBytes(sizeof(u16)); // Port
 
     in_packet.IgnoreBytes(sizeof(u8)); // Protocol
+
     bool broadcast;
     in_packet.Read(broadcast); // Broadcast
 
@@ -909,7 +911,7 @@ void Room::RoomImpl::HandleChatPacket(const ENetEvent* event) {
     }
 }
 
-void Room::RoomImpl::HandleGameNamePacket(const ENetEvent* event) {
+void Room::RoomImpl::HandleGameInfoPacket(const ENetEvent* event) {
     Packet in_packet;
     in_packet.Append(event->packet->data, event->packet->dataLength);
 
@@ -917,6 +919,7 @@ void Room::RoomImpl::HandleGameNamePacket(const ENetEvent* event) {
     GameInfo game_info;
     in_packet.Read(game_info.name);
     in_packet.Read(game_info.id);
+    in_packet.Read(game_info.version);
 
     {
         std::lock_guard lock(member_mutex);
@@ -935,7 +938,8 @@ void Room::RoomImpl::HandleGameNamePacket(const ENetEvent* event) {
             if (game_info.name.empty()) {
                 LOG_INFO(Network, "{} is not playing", display_name);
             } else {
-                LOG_INFO(Network, "{} is playing {}", display_name, game_info.name);
+                LOG_INFO(Network, "{} is playing {} ({})", display_name, game_info.name,
+                         game_info.version);
             }
         }
     }
diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp
index 9f08bf611a..06818af783 100644
--- a/src/network/room_member.cpp
+++ b/src/network/room_member.cpp
@@ -103,7 +103,7 @@ public:
 
     /**
      * Extracts a ProxyPacket from a received ENet packet.
-     * @param event The  ENet event that was received.
+     * @param event The ENet event that was received.
      */
     void HandleProxyPackets(const ENetEvent* event);
 
@@ -159,7 +159,7 @@ void RoomMember::RoomMemberImpl::MemberLoop() {
     while (IsConnected()) {
         std::lock_guard lock(network_mutex);
         ENetEvent event;
-        if (enet_host_service(client, &event, 100) > 0) {
+        if (enet_host_service(client, &event, 5) > 0) {
             switch (event.type) {
             case ENET_EVENT_TYPE_RECEIVE:
                 switch (event.packet->data[0]) {
@@ -315,6 +315,7 @@ void RoomMember::RoomMemberImpl::HandleRoomInformationPacket(const ENetEvent* ev
         packet.Read(member.fake_ip);
         packet.Read(member.game_info.name);
         packet.Read(member.game_info.id);
+        packet.Read(member.game_info.version);
         packet.Read(member.username);
         packet.Read(member.display_name);
         packet.Read(member.avatar_url);
@@ -622,6 +623,7 @@ void RoomMember::SendGameInfo(const GameInfo& game_info) {
     packet.Write(static_cast<u8>(IdSetGameInfo));
     packet.Write(game_info.name);
     packet.Write(game_info.id);
+    packet.Write(game_info.version);
     room_member_impl->Send(std::move(packet));
 }
 
diff --git a/src/network/room_member.h b/src/network/room_member.h
index 4252b7146d..f578f7f6a3 100644
--- a/src/network/room_member.h
+++ b/src/network/room_member.h
@@ -146,7 +146,7 @@ public:
               const std::string& password = "", const std::string& token = "");
 
     /**
-     * Sends a WiFi packet to the room.
+     * Sends a Proxy packet to the room.
      * @param packet The WiFi packet to send.
      */
     void SendProxyPacket(const ProxyPacket& packet);
diff --git a/src/shader_recompiler/ir_opt/texture_pass.cpp b/src/shader_recompiler/ir_opt/texture_pass.cpp
index 5cead5135e..597112ba47 100644
--- a/src/shader_recompiler/ir_opt/texture_pass.cpp
+++ b/src/shader_recompiler/ir_opt/texture_pass.cpp
@@ -415,11 +415,11 @@ void TexturePass(Environment& env, IR::Program& program) {
             inst->SetFlags(flags);
             break;
         case IR::Opcode::ImageSampleImplicitLod:
-            if (flags.type == TextureType::Color2D) {
-                auto texture_type = ReadTextureType(env, cbuf);
-                if (texture_type == TextureType::Color2DRect) {
-                    PatchImageSampleImplicitLod(*texture_inst.block, *texture_inst.inst);
-                }
+            if (flags.type != TextureType::Color2D) {
+                break;
+            }
+            if (ReadTextureType(env, cbuf) == TextureType::Color2DRect) {
+                PatchImageSampleImplicitLod(*texture_inst.block, *texture_inst.inst);
             }
             break;
         case IR::Opcode::ImageFetch:
diff --git a/src/video_core/buffer_cache/buffer_base.h b/src/video_core/buffer_cache/buffer_base.h
index 0b2bc67b1f..f9a6472cfd 100644
--- a/src/video_core/buffer_cache/buffer_base.h
+++ b/src/video_core/buffer_cache/buffer_base.h
@@ -12,6 +12,7 @@
 #include "common/common_funcs.h"
 #include "common/common_types.h"
 #include "common/div_ceil.h"
+#include "common/settings.h"
 #include "core/memory.h"
 
 namespace VideoCommon {
@@ -219,7 +220,9 @@ public:
             NotifyRasterizer<false>(word_index, untracked_words[word_index], cached_bits);
             untracked_words[word_index] |= cached_bits;
             cpu_words[word_index] |= cached_bits;
-            cached_words[word_index] = 0;
+            if (!Settings::values.use_pessimistic_flushes) {
+                cached_words[word_index] = 0;
+            }
         }
     }
 
diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
index 3adad5af4c..9708dc45e4 100644
--- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
@@ -53,7 +53,7 @@ using VideoCommon::FileEnvironment;
 using VideoCommon::GenericEnvironment;
 using VideoCommon::GraphicsEnvironment;
 
-constexpr u32 CACHE_VERSION = 5;
+constexpr u32 CACHE_VERSION = 6;
 
 template <typename Container>
 auto MakeSpan(Container& container) {
diff --git a/src/video_core/shader_environment.cpp b/src/video_core/shader_environment.cpp
index 3ead06dd65..c903975fc2 100644
--- a/src/video_core/shader_environment.cpp
+++ b/src/video_core/shader_environment.cpp
@@ -39,11 +39,8 @@ static Shader::TextureType ConvertType(const Tegra::Texture::TICEntry& entry) {
         return Shader::TextureType::Color1D;
     case Tegra::Texture::TextureType::Texture2D:
     case Tegra::Texture::TextureType::Texture2DNoMipmap:
-        if (entry.normalized_coords) {
-            return Shader::TextureType::Color2D;
-        } else {
-            return Shader::TextureType::Color2DRect;
-        }
+        return entry.normalized_coords ? Shader::TextureType::Color2D
+                                       : Shader::TextureType::Color2DRect;
     case Tegra::Texture::TextureType::Texture3D:
         return Shader::TextureType::Color3D;
     case Tegra::Texture::TextureType::TextureCubemap:
diff --git a/src/video_core/textures/astc.cpp b/src/video_core/textures/astc.cpp
index e3f3d3c5d0..b159494c54 100644
--- a/src/video_core/textures/astc.cpp
+++ b/src/video_core/textures/astc.cpp
@@ -13,7 +13,9 @@
 
 #include <boost/container/static_vector.hpp>
 
+#include "common/alignment.h"
 #include "common/common_types.h"
+#include "common/thread_worker.h"
 #include "video_core/textures/astc.h"
 
 class InputBitStream {
@@ -1650,29 +1652,41 @@ static void DecompressBlock(std::span<const u8, 16> inBuf, const u32 blockWidth,
 
 void Decompress(std::span<const uint8_t> data, uint32_t width, uint32_t height, uint32_t depth,
                 uint32_t block_width, uint32_t block_height, std::span<uint8_t> output) {
-    u32 block_index = 0;
-    std::size_t depth_offset = 0;
-    for (u32 z = 0; z < depth; z++) {
-        for (u32 y = 0; y < height; y += block_height) {
-            for (u32 x = 0; x < width; x += block_width) {
-                const std::span<const u8, 16> blockPtr{data.subspan(block_index * 16, 16)};
+    const u32 rows = Common::DivideUp(height, block_height);
+    const u32 cols = Common::DivideUp(width, block_width);
 
-                // Blocks can be at most 12x12
-                std::array<u32, 12 * 12> uncompData;
-                DecompressBlock(blockPtr, block_width, block_height, uncompData);
+    Common::ThreadWorker workers{std::max(std::thread::hardware_concurrency(), 2U) / 2,
+                                 "yuzu:ASTCDecompress"};
 
-                u32 decompWidth = std::min(block_width, width - x);
-                u32 decompHeight = std::min(block_height, height - y);
+    for (u32 z = 0; z < depth; ++z) {
+        const u32 depth_offset = z * height * width * 4;
+        for (u32 y_index = 0; y_index < rows; ++y_index) {
+            auto decompress_stride = [data, width, height, depth, block_width, block_height, output,
+                                      rows, cols, z, depth_offset, y_index] {
+                const u32 y = y_index * block_height;
+                for (u32 x_index = 0; x_index < cols; ++x_index) {
+                    const u32 block_index = (z * rows * cols) + (y_index * cols) + x_index;
+                    const u32 x = x_index * block_width;
 
-                const std::span<u8> outRow = output.subspan(depth_offset + (y * width + x) * 4);
-                for (u32 jj = 0; jj < decompHeight; jj++) {
-                    std::memcpy(outRow.data() + jj * width * 4,
-                                uncompData.data() + jj * block_width, decompWidth * 4);
+                    const std::span<const u8, 16> blockPtr{data.subspan(block_index * 16, 16)};
+
+                    // Blocks can be at most 12x12
+                    std::array<u32, 12 * 12> uncompData;
+                    DecompressBlock(blockPtr, block_width, block_height, uncompData);
+
+                    u32 decompWidth = std::min(block_width, width - x);
+                    u32 decompHeight = std::min(block_height, height - y);
+
+                    const std::span<u8> outRow = output.subspan(depth_offset + (y * width + x) * 4);
+                    for (u32 h = 0; h < decompHeight; ++h) {
+                        std::memcpy(outRow.data() + h * width * 4,
+                                    uncompData.data() + h * block_width, decompWidth * 4);
+                    }
                 }
-                ++block_index;
-            }
+            };
+            workers.QueueWork(std::move(decompress_stride));
         }
-        depth_offset += height * width * 4;
+        workers.WaitForRequests();
     }
 }
 
diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt
index 50007338fe..29d506c472 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 8be311fcbe..1d8072243b 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 da6e5aa885..a4ed684229 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();
 }
@@ -684,6 +684,7 @@ void Config::ReadRendererValues() {
     ReadGlobalSetting(Settings::values.shader_backend);
     ReadGlobalSetting(Settings::values.use_asynchronous_shaders);
     ReadGlobalSetting(Settings::values.use_fast_gpu_time);
+    ReadGlobalSetting(Settings::values.use_pessimistic_flushes);
     ReadGlobalSetting(Settings::values.bg_red);
     ReadGlobalSetting(Settings::values.bg_green);
     ReadGlobalSetting(Settings::values.bg_blue);
@@ -1160,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();
 }
@@ -1300,6 +1302,7 @@ void Config::SaveRendererValues() {
                  Settings::values.shader_backend.UsingGlobal());
     WriteGlobalSetting(Settings::values.use_asynchronous_shaders);
     WriteGlobalSetting(Settings::values.use_fast_gpu_time);
+    WriteGlobalSetting(Settings::values.use_pessimistic_flushes);
     WriteGlobalSetting(Settings::values.bg_red);
     WriteGlobalSetting(Settings::values.bg_green);
     WriteGlobalSetting(Settings::values.bg_blue);
@@ -1545,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 486ceea949..06fa7d2d07 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 e16d127a8e..622808e944 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"
@@ -14,7 +15,7 @@
 #include "yuzu/uisettings.h"
 
 ConfigureDebug::ConfigureDebug(const Core::System& system_, QWidget* parent)
-    : QWidget(parent), ui{std::make_unique<Ui::ConfigureDebug>()}, system{system_} {
+    : QScrollArea(parent), ui{std::make_unique<Ui::ConfigureDebug>()}, system{system_} {
     ui->setupUi(this);
     SetConfiguration();
 
@@ -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 64d68ab8fa..030a0b7f75 100644
--- a/src/yuzu/configuration/configure_debug.h
+++ b/src/yuzu/configuration/configure_debug.h
@@ -4,7 +4,7 @@
 #pragma once
 
 #include <memory>
-#include <QWidget>
+#include <QScrollArea>
 
 namespace Core {
 class System;
@@ -14,7 +14,7 @@ namespace Ui {
 class ConfigureDebug;
 }
 
-class ConfigureDebug : public QWidget {
+class ConfigureDebug : public QScrollArea {
     Q_OBJECT
 
 public:
@@ -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 4c16274fc6..314d47af5c 100644
--- a/src/yuzu/configuration/configure_debug.ui
+++ b/src/yuzu/configuration/configure_debug.ui
@@ -1,62 +1,66 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <ui version="4.0">
  <class>ConfigureDebug</class>
- <widget class="QWidget" name="ConfigureDebug">
+ <widget class="QScrollArea" name="ConfigureDebug">
+  <property name="widgetResizable">
+   <bool>true</bool>
+  </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>
-           <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="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>
-       </item>
-      </layout>
+        </item>
+       </layout>
+      </widget>
      </item>
+    </layout>
+   </item>
    <item>
     <widget class="QGroupBox" name="groupBox_2">
      <property name="title">
@@ -227,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">
@@ -234,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>
@@ -322,6 +333,7 @@
    </item>
   </layout>
  </widget>
+ </widget>
  <tabstops>
   <tabstop>log_filter_edit</tabstop>
   <tabstop>toggle_console</tabstop>
@@ -335,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_graphics_advanced.cpp b/src/yuzu/configuration/configure_graphics_advanced.cpp
index 7c3196c83a..01f074699c 100644
--- a/src/yuzu/configuration/configure_graphics_advanced.cpp
+++ b/src/yuzu/configuration/configure_graphics_advanced.cpp
@@ -28,6 +28,7 @@ void ConfigureGraphicsAdvanced::SetConfiguration() {
     ui->use_vsync->setChecked(Settings::values.use_vsync.GetValue());
     ui->use_asynchronous_shaders->setChecked(Settings::values.use_asynchronous_shaders.GetValue());
     ui->use_fast_gpu_time->setChecked(Settings::values.use_fast_gpu_time.GetValue());
+    ui->use_pessimistic_flushes->setChecked(Settings::values.use_pessimistic_flushes.GetValue());
 
     if (Settings::IsConfiguringGlobal()) {
         ui->gpu_accuracy->setCurrentIndex(
@@ -55,6 +56,8 @@ void ConfigureGraphicsAdvanced::ApplyConfiguration() {
                                              use_asynchronous_shaders);
     ConfigurationShared::ApplyPerGameSetting(&Settings::values.use_fast_gpu_time,
                                              ui->use_fast_gpu_time, use_fast_gpu_time);
+    ConfigurationShared::ApplyPerGameSetting(&Settings::values.use_pessimistic_flushes,
+                                             ui->use_pessimistic_flushes, use_pessimistic_flushes);
 }
 
 void ConfigureGraphicsAdvanced::changeEvent(QEvent* event) {
@@ -77,6 +80,8 @@ void ConfigureGraphicsAdvanced::SetupPerGameUI() {
         ui->use_asynchronous_shaders->setEnabled(
             Settings::values.use_asynchronous_shaders.UsingGlobal());
         ui->use_fast_gpu_time->setEnabled(Settings::values.use_fast_gpu_time.UsingGlobal());
+        ui->use_pessimistic_flushes->setEnabled(
+            Settings::values.use_pessimistic_flushes.UsingGlobal());
         ui->anisotropic_filtering_combobox->setEnabled(
             Settings::values.max_anisotropy.UsingGlobal());
 
@@ -89,6 +94,9 @@ void ConfigureGraphicsAdvanced::SetupPerGameUI() {
                                             use_asynchronous_shaders);
     ConfigurationShared::SetColoredTristate(ui->use_fast_gpu_time,
                                             Settings::values.use_fast_gpu_time, use_fast_gpu_time);
+    ConfigurationShared::SetColoredTristate(ui->use_pessimistic_flushes,
+                                            Settings::values.use_pessimistic_flushes,
+                                            use_pessimistic_flushes);
     ConfigurationShared::SetColoredComboBox(
         ui->gpu_accuracy, ui->label_gpu_accuracy,
         static_cast<int>(Settings::values.gpu_accuracy.GetValue(true)));
diff --git a/src/yuzu/configuration/configure_graphics_advanced.h b/src/yuzu/configuration/configure_graphics_advanced.h
index 1ef7bd9161..12e816905a 100644
--- a/src/yuzu/configuration/configure_graphics_advanced.h
+++ b/src/yuzu/configuration/configure_graphics_advanced.h
@@ -39,6 +39,7 @@ private:
     ConfigurationShared::CheckState use_vsync;
     ConfigurationShared::CheckState use_asynchronous_shaders;
     ConfigurationShared::CheckState use_fast_gpu_time;
+    ConfigurationShared::CheckState use_pessimistic_flushes;
 
     const Core::System& system;
 };
diff --git a/src/yuzu/configuration/configure_graphics_advanced.ui b/src/yuzu/configuration/configure_graphics_advanced.ui
index d6d8193647..87a1214710 100644
--- a/src/yuzu/configuration/configure_graphics_advanced.ui
+++ b/src/yuzu/configuration/configure_graphics_advanced.ui
@@ -99,6 +99,16 @@
           </property>
          </widget>
         </item>
+        <item>
+         <widget class="QCheckBox" name="use_pessimistic_flushes">
+          <property name="toolTip">
+            <string>Enables pessimistic buffer flushes. This option will force unmodified buffers to be flushed, which can cost performance.</string>
+          </property>
+          <property name="text">
+           <string>Use pessimistic buffer flushes (Hack)</string>
+          </property>
+         </widget>
+        </item>
         <item>
          <widget class="QWidget" name="af_layout" native="true">
           <layout class="QHBoxLayout" name="horizontalLayout_1">
diff --git a/src/yuzu/configuration/configure_input.cpp b/src/yuzu/configuration/configure_input.cpp
index 16fba3deb2..cb55472c9e 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 109689c88d..9e5a40fe74 100644
--- a/src/yuzu/configuration/configure_input_player.cpp
+++ b/src/yuzu/configuration/configure_input_player.cpp
@@ -161,6 +161,7 @@ QString ConfigureInputPlayer::ButtonToText(const Common::ParamPackage& param) {
 
     const QString toggle = QString::fromStdString(param.Get("toggle", false) ? "~" : "");
     const QString inverted = QString::fromStdString(param.Get("inverted", false) ? "!" : "");
+    const QString invert = QString::fromStdString(param.Get("invert", "+") == "-" ? "-" : "");
     const auto common_button_name = input_subsystem->GetButtonName(param);
 
     // Retrieve the names from Qt
@@ -184,7 +185,7 @@ QString ConfigureInputPlayer::ButtonToText(const Common::ParamPackage& param) {
         }
         if (param.Has("axis")) {
             const QString axis = QString::fromStdString(param.Get("axis", ""));
-            return QObject::tr("%1%2Axis %3").arg(toggle, inverted, axis);
+            return QObject::tr("%1%2Axis %3").arg(toggle, invert, axis);
         }
         if (param.Has("axis_x") && param.Has("axis_y") && param.Has("axis_z")) {
             const QString axis_x = QString::fromStdString(param.Get("axis_x", ""));
@@ -362,18 +363,18 @@ ConfigureInputPlayer::ConfigureInputPlayer(QWidget* parent, std::size_t player_i
                         button_map[button_id]->setText(tr("[not set]"));
                     });
                     if (param.Has("code") || param.Has("button") || param.Has("hat")) {
-                        context_menu.addAction(tr("Toggle button"), [&] {
-                            const bool toggle_value = !param.Get("toggle", false);
-                            param.Set("toggle", toggle_value);
-                            button_map[button_id]->setText(ButtonToText(param));
-                            emulated_controller->SetButtonParam(button_id, param);
-                        });
                         context_menu.addAction(tr("Invert button"), [&] {
                             const bool invert_value = !param.Get("inverted", false);
                             param.Set("inverted", invert_value);
                             button_map[button_id]->setText(ButtonToText(param));
                             emulated_controller->SetButtonParam(button_id, param);
                         });
+                        context_menu.addAction(tr("Toggle button"), [&] {
+                            const bool toggle_value = !param.Get("toggle", false);
+                            param.Set("toggle", toggle_value);
+                            button_map[button_id]->setText(ButtonToText(param));
+                            emulated_controller->SetButtonParam(button_id, param);
+                        });
                     }
                     if (param.Has("axis")) {
                         context_menu.addAction(tr("Invert axis"), [&] {
@@ -398,6 +399,12 @@ ConfigureInputPlayer::ConfigureInputPlayer(QWidget* parent, std::size_t player_i
                             }
                             emulated_controller->SetButtonParam(button_id, param);
                         });
+                        context_menu.addAction(tr("Toggle axis"), [&] {
+                            const bool toggle_value = !param.Get("toggle", false);
+                            param.Set("toggle", toggle_value);
+                            button_map[button_id]->setText(ButtonToText(param));
+                            emulated_controller->SetButtonParam(button_id, param);
+                        });
                     }
                     context_menu.exec(button_map[button_id]->mapToGlobal(menu_location));
                 });
@@ -1410,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 af8343b2e3..c3cb8f61d1 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 cf88a5bf08..625af0c891 100644
--- a/src/yuzu/configuration/configure_tas.ui
+++ b/src/yuzu/configuration/configure_tas.ui
@@ -16,6 +16,9 @@
           <property name="text">
            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Reads controller input from scripts in the same format as TAS-nx scripts.&lt;br/&gt;For a more detailed explanation, please consult the &lt;a href=&quot;https://yuzu-emu.org/help/feature/tas/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#039be5;&quot;&gt;help page&lt;/span&gt;&lt;/a&gt; on the yuzu website.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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 d668c992b2..ab526e4ca5 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 20b22e7de7..807afbeb25 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 65fc9e62c5..2bf3e42508 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 e103df977a..3c1bd19db6 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__
@@ -860,7 +863,7 @@ void GMainWindow::InitializeWidgets() {
     });
 
     multiplayer_state = new MultiplayerState(this, game_list->GetModel(), ui->action_Leave_Room,
-                                             ui->action_Show_Room, system->GetRoomNetwork());
+                                             ui->action_Show_Room, *system);
     multiplayer_state->setVisible(false);
 
     // Create status bar
@@ -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 1ae2b93d9b..716aef063c 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 0000000000..a34dc6a9c4
--- /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 0000000000..d6b6cca845
--- /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/multiplayer/chat_room.cpp b/src/yuzu/multiplayer/chat_room.cpp
index 1968a3c754..9e672f82e9 100644
--- a/src/yuzu/multiplayer/chat_room.cpp
+++ b/src/yuzu/multiplayer/chat_room.cpp
@@ -16,7 +16,7 @@
 #include <QUrl>
 #include <QtConcurrent/QtConcurrentRun>
 #include "common/logging/log.h"
-#include "core/announce_multiplayer_session.h"
+#include "network/announce_multiplayer_session.h"
 #include "ui_chat_room.h"
 #include "yuzu/game_list_p.h"
 #include "yuzu/multiplayer/chat_room.h"
@@ -122,19 +122,22 @@ public:
     static const int UsernameRole = Qt::UserRole + 2;
     static const int AvatarUrlRole = Qt::UserRole + 3;
     static const int GameNameRole = Qt::UserRole + 4;
+    static const int GameVersionRole = Qt::UserRole + 5;
 
     PlayerListItem() = default;
     explicit PlayerListItem(const std::string& nickname, const std::string& username,
-                            const std::string& avatar_url, const std::string& game_name) {
+                            const std::string& avatar_url,
+                            const AnnounceMultiplayerRoom::GameInfo& game_info) {
         setEditable(false);
         setData(QString::fromStdString(nickname), NicknameRole);
         setData(QString::fromStdString(username), UsernameRole);
         setData(QString::fromStdString(avatar_url), AvatarUrlRole);
-        if (game_name.empty()) {
+        if (game_info.name.empty()) {
             setData(QObject::tr("Not playing a game"), GameNameRole);
         } else {
-            setData(QString::fromStdString(game_name), GameNameRole);
+            setData(QString::fromStdString(game_info.name), GameNameRole);
         }
+        setData(QString::fromStdString(game_info.version), GameVersionRole);
     }
 
     QVariant data(int role) const override {
@@ -149,7 +152,13 @@ public:
         } else {
             name = QStringLiteral("%1 (%2)").arg(nickname, username);
         }
-        return QStringLiteral("%1\n      %2").arg(name, data(GameNameRole).toString());
+        const QString version = data(GameVersionRole).toString();
+        QString version_string;
+        if (!version.isEmpty()) {
+            version_string = QStringLiteral("(%1)").arg(version);
+        }
+        return QStringLiteral("%1\n      %2 %3")
+            .arg(name, data(GameNameRole).toString(), version_string);
     }
 };
 
@@ -167,6 +176,10 @@ ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_unique<Ui::C
 
     ui->chat_history->document()->setMaximumBlockCount(max_chat_lines);
 
+    auto font = ui->chat_history->font();
+    font.setPointSizeF(10);
+    ui->chat_history->setFont(font);
+
     // register the network structs to use in slots and signals
     qRegisterMetaType<Network::ChatEntry>();
     qRegisterMetaType<Network::StatusMessageEntry>();
@@ -366,7 +379,7 @@ void ChatRoom::SetPlayerList(const Network::RoomMember::MemberList& member_list)
         if (member.nickname.empty())
             continue;
         QStandardItem* name_item = new PlayerListItem(member.nickname, member.username,
-                                                      member.avatar_url, member.game_info.name);
+                                                      member.avatar_url, member.game_info);
 
 #ifdef ENABLE_WEB_SERVICE
         if (!icon_cache.count(member.avatar_url) && !member.avatar_url.empty()) {
diff --git a/src/yuzu/multiplayer/client_room.cpp b/src/yuzu/multiplayer/client_room.cpp
index 86baafbf03..b34a8d004a 100644
--- a/src/yuzu/multiplayer/client_room.cpp
+++ b/src/yuzu/multiplayer/client_room.cpp
@@ -10,7 +10,7 @@
 #include <QTime>
 #include <QtConcurrent/QtConcurrentRun>
 #include "common/logging/log.h"
-#include "core/announce_multiplayer_session.h"
+#include "network/announce_multiplayer_session.h"
 #include "ui_client_room.h"
 #include "yuzu/game_list_p.h"
 #include "yuzu/multiplayer/client_room.h"
diff --git a/src/yuzu/multiplayer/direct_connect.cpp b/src/yuzu/multiplayer/direct_connect.cpp
index 4c0ea0a6b7..0170630744 100644
--- a/src/yuzu/multiplayer/direct_connect.cpp
+++ b/src/yuzu/multiplayer/direct_connect.cpp
@@ -8,6 +8,8 @@
 #include <QString>
 #include <QtConcurrent/QtConcurrentRun>
 #include "common/settings.h"
+#include "core/core.h"
+#include "core/internal_network/network_interface.h"
 #include "network/network.h"
 #include "ui_direct_connect.h"
 #include "yuzu/main.h"
@@ -20,9 +22,10 @@
 
 enum class ConnectionType : u8 { TraversalServer, IP };
 
-DirectConnectWindow::DirectConnectWindow(Network::RoomNetwork& room_network_, QWidget* parent)
+DirectConnectWindow::DirectConnectWindow(Core::System& system_, QWidget* parent)
     : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
-      ui(std::make_unique<Ui::DirectConnect>()), room_network{room_network_} {
+      ui(std::make_unique<Ui::DirectConnect>()), system{system_}, room_network{
+                                                                      system.GetRoomNetwork()} {
 
     ui->setupUi(this);
 
@@ -53,10 +56,20 @@ void DirectConnectWindow::RetranslateUi() {
 }
 
 void DirectConnectWindow::Connect() {
+    if (!Network::GetSelectedNetworkInterface()) {
+        NetworkMessage::ErrorManager::ShowError(
+            NetworkMessage::ErrorManager::NO_INTERFACE_SELECTED);
+        return;
+    }
     if (!ui->nickname->hasAcceptableInput()) {
         NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::USERNAME_NOT_VALID);
         return;
     }
+    if (system.IsPoweredOn()) {
+        if (!NetworkMessage::WarnGameRunning()) {
+            return;
+        }
+    }
     if (const auto member = room_network.GetRoomMember().lock()) {
         // Prevent the user from trying to join a room while they are already joining.
         if (member->GetState() == Network::RoomMember::State::Joining) {
diff --git a/src/yuzu/multiplayer/direct_connect.h b/src/yuzu/multiplayer/direct_connect.h
index 4e10430532..e39dd1e0d6 100644
--- a/src/yuzu/multiplayer/direct_connect.h
+++ b/src/yuzu/multiplayer/direct_connect.h
@@ -12,11 +12,15 @@ namespace Ui {
 class DirectConnect;
 }
 
+namespace Core {
+class System;
+}
+
 class DirectConnectWindow : public QDialog {
     Q_OBJECT
 
 public:
-    explicit DirectConnectWindow(Network::RoomNetwork& room_network_, QWidget* parent = nullptr);
+    explicit DirectConnectWindow(Core::System& system_, QWidget* parent = nullptr);
     ~DirectConnectWindow();
 
     void RetranslateUi();
@@ -39,5 +43,6 @@ private:
     QFutureWatcher<void>* watcher;
     std::unique_ptr<Ui::DirectConnect> ui;
     Validation validation;
+    Core::System& system;
     Network::RoomNetwork& room_network;
 };
diff --git a/src/yuzu/multiplayer/host_room.cpp b/src/yuzu/multiplayer/host_room.cpp
index d70a9a3c86..0c6adfd040 100644
--- a/src/yuzu/multiplayer/host_room.cpp
+++ b/src/yuzu/multiplayer/host_room.cpp
@@ -12,7 +12,9 @@
 #include <QtConcurrent/QtConcurrentRun>
 #include "common/logging/log.h"
 #include "common/settings.h"
-#include "core/announce_multiplayer_session.h"
+#include "core/core.h"
+#include "core/internal_network/network_interface.h"
+#include "network/announce_multiplayer_session.h"
 #include "ui_host_room.h"
 #include "yuzu/game_list_p.h"
 #include "yuzu/main.h"
@@ -27,10 +29,11 @@
 
 HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list,
                                std::shared_ptr<Core::AnnounceMultiplayerSession> session,
-                               Network::RoomNetwork& room_network_)
+                               Core::System& system_)
     : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
       ui(std::make_unique<Ui::HostRoom>()),
-      announce_multiplayer_session(session), room_network{room_network_} {
+      announce_multiplayer_session(session), system{system_}, room_network{
+                                                                  system.GetRoomNetwork()} {
     ui->setupUi(this);
 
     // set up validation for all of the fields
@@ -105,6 +108,11 @@ std::unique_ptr<Network::VerifyUser::Backend> HostRoomWindow::CreateVerifyBacken
 }
 
 void HostRoomWindow::Host() {
+    if (!Network::GetSelectedNetworkInterface()) {
+        NetworkMessage::ErrorManager::ShowError(
+            NetworkMessage::ErrorManager::NO_INTERFACE_SELECTED);
+        return;
+    }
     if (!ui->username->hasAcceptableInput()) {
         NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::USERNAME_NOT_VALID);
         return;
@@ -121,6 +129,11 @@ void HostRoomWindow::Host() {
         NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::GAME_NOT_SELECTED);
         return;
     }
+    if (system.IsPoweredOn()) {
+        if (!NetworkMessage::WarnGameRunning()) {
+            return;
+        }
+    }
     if (auto member = room_network.GetRoomMember().lock()) {
         if (member->GetState() == Network::RoomMember::State::Joining) {
             return;
diff --git a/src/yuzu/multiplayer/host_room.h b/src/yuzu/multiplayer/host_room.h
index a968042d03..034cb2eefd 100644
--- a/src/yuzu/multiplayer/host_room.h
+++ b/src/yuzu/multiplayer/host_room.h
@@ -17,8 +17,9 @@ class HostRoom;
 }
 
 namespace Core {
+class System;
 class AnnounceMultiplayerSession;
-}
+} // namespace Core
 
 class ConnectionError;
 class ComboBoxProxyModel;
@@ -35,7 +36,7 @@ class HostRoomWindow : public QDialog {
 public:
     explicit HostRoomWindow(QWidget* parent, QStandardItemModel* list,
                             std::shared_ptr<Core::AnnounceMultiplayerSession> session,
-                            Network::RoomNetwork& room_network_);
+                            Core::System& system_);
     ~HostRoomWindow();
 
     /**
@@ -54,6 +55,7 @@ private:
     QStandardItemModel* game_list;
     ComboBoxProxyModel* proxy;
     Validation validation;
+    Core::System& system;
     Network::RoomNetwork& room_network;
 };
 
diff --git a/src/yuzu/multiplayer/lobby.cpp b/src/yuzu/multiplayer/lobby.cpp
index 1cc518279c..107d405476 100644
--- a/src/yuzu/multiplayer/lobby.cpp
+++ b/src/yuzu/multiplayer/lobby.cpp
@@ -6,6 +6,8 @@
 #include <QtConcurrent/QtConcurrentRun>
 #include "common/logging/log.h"
 #include "common/settings.h"
+#include "core/core.h"
+#include "core/internal_network/network_interface.h"
 #include "network/network.h"
 #include "ui_lobby.h"
 #include "yuzu/game_list_p.h"
@@ -22,11 +24,11 @@
 #endif
 
 Lobby::Lobby(QWidget* parent, QStandardItemModel* list,
-             std::shared_ptr<Core::AnnounceMultiplayerSession> session,
-             Network::RoomNetwork& room_network_)
+             std::shared_ptr<Core::AnnounceMultiplayerSession> session, Core::System& system_)
     : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
       ui(std::make_unique<Ui::Lobby>()),
-      announce_multiplayer_session(session), room_network{room_network_} {
+      announce_multiplayer_session(session), system{system_}, room_network{
+                                                                  system.GetRoomNetwork()} {
     ui->setupUi(this);
 
     // setup the watcher for background connections
@@ -114,6 +116,18 @@ void Lobby::OnExpandRoom(const QModelIndex& index) {
 }
 
 void Lobby::OnJoinRoom(const QModelIndex& source) {
+    if (!Network::GetSelectedNetworkInterface()) {
+        NetworkMessage::ErrorManager::ShowError(
+            NetworkMessage::ErrorManager::NO_INTERFACE_SELECTED);
+        return;
+    }
+
+    if (system.IsPoweredOn()) {
+        if (!NetworkMessage::WarnGameRunning()) {
+            return;
+        }
+    }
+
     if (const auto member = room_network.GetRoomMember().lock()) {
         // Prevent the user from trying to join a room while they are already joining.
         if (member->GetState() == Network::RoomMember::State::Joining) {
diff --git a/src/yuzu/multiplayer/lobby.h b/src/yuzu/multiplayer/lobby.h
index 82744ca945..2696aec21a 100644
--- a/src/yuzu/multiplayer/lobby.h
+++ b/src/yuzu/multiplayer/lobby.h
@@ -9,7 +9,7 @@
 #include <QSortFilterProxyModel>
 #include <QStandardItemModel>
 #include "common/announce_multiplayer_room.h"
-#include "core/announce_multiplayer_session.h"
+#include "network/announce_multiplayer_session.h"
 #include "network/network.h"
 #include "yuzu/multiplayer/validation.h"
 
@@ -20,6 +20,10 @@ class Lobby;
 class LobbyModel;
 class LobbyFilterProxyModel;
 
+namespace Core {
+class System;
+}
+
 /**
  * Listing of all public games pulled from services. The lobby should be simple enough for users to
  * find the game they want to play, and join it.
@@ -30,7 +34,7 @@ class Lobby : public QDialog {
 public:
     explicit Lobby(QWidget* parent, QStandardItemModel* list,
                    std::shared_ptr<Core::AnnounceMultiplayerSession> session,
-                   Network::RoomNetwork& room_network_);
+                   Core::System& system_);
     ~Lobby() override;
 
     /**
@@ -94,6 +98,7 @@ private:
     std::weak_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session;
     QFutureWatcher<void>* watcher;
     Validation validation;
+    Core::System& system;
     Network::RoomNetwork& room_network;
 };
 
diff --git a/src/yuzu/multiplayer/message.cpp b/src/yuzu/multiplayer/message.cpp
index 94d7a38b83..758b5b731d 100644
--- a/src/yuzu/multiplayer/message.cpp
+++ b/src/yuzu/multiplayer/message.cpp
@@ -49,6 +49,9 @@ const ConnectionError ErrorManager::PERMISSION_DENIED(
     QT_TR_NOOP("You do not have enough permission to perform this action."));
 const ConnectionError ErrorManager::NO_SUCH_USER(QT_TR_NOOP(
     "The user you are trying to kick/ban could not be found.\nThey may have left the room."));
+const ConnectionError ErrorManager::NO_INTERFACE_SELECTED(
+    QT_TR_NOOP("No network interface is selected.\nPlease go to Configure -> System -> Network and "
+               "make a selection."));
 
 static bool WarnMessage(const std::string& title, const std::string& text) {
     return QMessageBox::Ok == QMessageBox::warning(nullptr, QObject::tr(title.c_str()),
@@ -60,6 +63,13 @@ void ErrorManager::ShowError(const ConnectionError& e) {
     QMessageBox::critical(nullptr, tr("Error"), tr(e.GetString().c_str()));
 }
 
+bool WarnGameRunning() {
+    return WarnMessage(
+        QT_TR_NOOP("Game already running"),
+        QT_TR_NOOP("Joining a room when the game is already running is discouraged "
+                   "and can cause the room feature not to work correctly.\nProceed anyway?"));
+}
+
 bool WarnCloseRoom() {
     return WarnMessage(
         QT_TR_NOOP("Leave Room"),
diff --git a/src/yuzu/multiplayer/message.h b/src/yuzu/multiplayer/message.h
index 812495c72d..f038b9a1f6 100644
--- a/src/yuzu/multiplayer/message.h
+++ b/src/yuzu/multiplayer/message.h
@@ -43,11 +43,20 @@ public:
     static const ConnectionError IP_COLLISION;
     static const ConnectionError PERMISSION_DENIED;
     static const ConnectionError NO_SUCH_USER;
+    static const ConnectionError NO_INTERFACE_SELECTED;
     /**
      *  Shows a standard QMessageBox with a error message
      */
     static void ShowError(const ConnectionError& e);
 };
+
+/**
+ * Show a standard QMessageBox with a warning message about joining a room when
+ * the game is already running
+ * return true if the user wants to close the network connection
+ */
+bool WarnGameRunning();
+
 /**
  * Show a standard QMessageBox with a warning message about leaving the room
  * return true if the user wants to close the network connection
diff --git a/src/yuzu/multiplayer/state.cpp b/src/yuzu/multiplayer/state.cpp
index dba76b22b0..66e098296d 100644
--- a/src/yuzu/multiplayer/state.cpp
+++ b/src/yuzu/multiplayer/state.cpp
@@ -8,6 +8,7 @@
 #include <QStandardItemModel>
 #include "common/announce_multiplayer_room.h"
 #include "common/logging/log.h"
+#include "core/core.h"
 #include "yuzu/game_list.h"
 #include "yuzu/multiplayer/client_room.h"
 #include "yuzu/multiplayer/direct_connect.h"
@@ -19,10 +20,9 @@
 #include "yuzu/util/clickable_label.h"
 
 MultiplayerState::MultiplayerState(QWidget* parent, QStandardItemModel* game_list_model_,
-                                   QAction* leave_room_, QAction* show_room_,
-                                   Network::RoomNetwork& room_network_)
+                                   QAction* leave_room_, QAction* show_room_, Core::System& system_)
     : QWidget(parent), game_list_model(game_list_model_), leave_room(leave_room_),
-      show_room(show_room_), room_network{room_network_} {
+      show_room(show_room_), system{system_}, room_network{system.GetRoomNetwork()} {
     if (auto member = room_network.GetRoomMember().lock()) {
         // register the network structs to use in slots and signals
         state_callback_handle = member->BindOnStateChanged(
@@ -208,15 +208,14 @@ static void BringWidgetToFront(QWidget* widget) {
 
 void MultiplayerState::OnViewLobby() {
     if (lobby == nullptr) {
-        lobby = new Lobby(this, game_list_model, announce_multiplayer_session, room_network);
+        lobby = new Lobby(this, game_list_model, announce_multiplayer_session, system);
     }
     BringWidgetToFront(lobby);
 }
 
 void MultiplayerState::OnCreateRoom() {
     if (host_room == nullptr) {
-        host_room =
-            new HostRoomWindow(this, game_list_model, announce_multiplayer_session, room_network);
+        host_room = new HostRoomWindow(this, game_list_model, announce_multiplayer_session, system);
     }
     BringWidgetToFront(host_room);
 }
@@ -279,7 +278,7 @@ void MultiplayerState::OnOpenNetworkRoom() {
 
 void MultiplayerState::OnDirectConnectToRoom() {
     if (direct_connect == nullptr) {
-        direct_connect = new DirectConnectWindow(room_network, this);
+        direct_connect = new DirectConnectWindow(system, this);
     }
     BringWidgetToFront(direct_connect);
 }
diff --git a/src/yuzu/multiplayer/state.h b/src/yuzu/multiplayer/state.h
index 9c60712d51..c92496413c 100644
--- a/src/yuzu/multiplayer/state.h
+++ b/src/yuzu/multiplayer/state.h
@@ -4,7 +4,7 @@
 #pragma once
 
 #include <QWidget>
-#include "core/announce_multiplayer_session.h"
+#include "network/announce_multiplayer_session.h"
 #include "network/network.h"
 
 class QStandardItemModel;
@@ -14,12 +14,16 @@ class ClientRoomWindow;
 class DirectConnectWindow;
 class ClickableLabel;
 
+namespace Core {
+class System;
+}
+
 class MultiplayerState : public QWidget {
     Q_OBJECT;
 
 public:
     explicit MultiplayerState(QWidget* parent, QStandardItemModel* game_list, QAction* leave_room,
-                              QAction* show_room, Network::RoomNetwork& room_network_);
+                              QAction* show_room, Core::System& system_);
     ~MultiplayerState();
 
     /**
@@ -86,6 +90,7 @@ private:
     Network::RoomMember::CallbackHandle<Network::RoomMember::Error> error_callback_handle;
 
     bool show_notification = false;
+    Core::System& system;
     Network::RoomNetwork& room_network;
 };
 
diff --git a/src/yuzu/startup_checks.cpp b/src/yuzu/startup_checks.cpp
index 8421280bf2..29b87da051 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 096dd54a89..f2fc2d9d45 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/src/yuzu_cmd/config.cpp b/src/yuzu_cmd/config.cpp
index bd0fb75f8d..66dd0dc154 100644
--- a/src/yuzu_cmd/config.cpp
+++ b/src/yuzu_cmd/config.cpp
@@ -314,6 +314,7 @@ void Config::ReadValues() {
     ReadSetting("Renderer", Settings::values.nvdec_emulation);
     ReadSetting("Renderer", Settings::values.accelerate_astc);
     ReadSetting("Renderer", Settings::values.use_fast_gpu_time);
+    ReadSetting("Renderer", Settings::values.use_pessimistic_flushes);
 
     ReadSetting("Renderer", Settings::values.bg_red);
     ReadSetting("Renderer", Settings::values.bg_green);
diff --git a/src/yuzu_cmd/default_ini.h b/src/yuzu_cmd/default_ini.h
index 1168cf1362..d214771b0e 100644
--- a/src/yuzu_cmd/default_ini.h
+++ b/src/yuzu_cmd/default_ini.h
@@ -319,6 +319,10 @@ use_asynchronous_gpu_emulation =
 # 0: Off, 1 (default): On
 use_fast_gpu_time =
 
+# Force unmodified buffers to be flushed, which can cost performance.
+# 0: Off (default), 1: On
+use_pessimistic_flushes =
+
 # Whether to use garbage collection or not for GPU caches.
 # 0 (default): Off, 1: On
 use_caches_gc =
diff --git a/vcpkg.json b/vcpkg.json
index c4413e22ad..3c92510d60 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": [