From 9f2528b194190f868c46ce5a5d9d53e3b6cb8f94 Mon Sep 17 00:00:00 2001 From: Johnny D Date: Fri, 18 Sep 2026 12:57:53 -0400 Subject: [PATCH 01/12] ci: retain exact host and controller sources for desktop qualification --- .github/workflows/switch2kit-desktop.yml | 41 ++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/switch2kit-desktop.yml diff --git a/.github/workflows/switch2kit-desktop.yml b/.github/workflows/switch2kit-desktop.yml new file mode 100644 index 0000000000..83d21936ec --- /dev/null +++ b/.github/workflows/switch2kit-desktop.yml @@ -0,0 +1,41 @@ +name: Switch2Kit desktop platforms +on: + pull_request: + workflow_dispatch: +permissions: + contents: read +concurrency: + group: switch2kit-desktop-${{ github.ref }} + cancel-in-progress: true +jobs: + source: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - run: git submodule update --init dependencies/Switch2Kit + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + repository: libsdl-org/SDL + ref: f87239e71e42da91ca317a12eefb82cfbf3393eb + path: test-sdl + persist-credentials: false + - name: Archive the exact sources used by the controller integration + run: | + mkdir -p "$RUNNER_TEMP/switch2kit-source" + git archive HEAD -o "$RUNNER_TEMP/switch2kit-source/host.zip" + git -C dependencies/Switch2Kit archive HEAD -o "$RUNNER_TEMP/switch2kit-source/sdk.zip" + git -C test-sdl archive HEAD -o "$RUNNER_TEMP/switch2kit-source/sdl.zip" + { + git rev-parse HEAD + git submodule status dependencies/Switch2Kit + git -C test-sdl rev-parse HEAD + } > "$RUNNER_TEMP/switch2kit-source/revisions.txt" + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: Switch2Kit-desktop-source + path: ${{ runner.temp }}/switch2kit-source + retention-days: 7 + if-no-files-found: error From 9519c665f92ffbeee788fefacf289868189ef5fc Mon Sep 17 00:00:00 2001 From: Johnny D Date: Fri, 18 Sep 2026 14:29:05 -0400 Subject: [PATCH 02/12] Enable native Switch2Kit input in Linux Cemu and validate installed builds Advance the pinned SDK to the existing BlueZ backend. Keep macOS bundle and deployment requirements Apple-specific, add Linux install rules and a platform-aware build helper, and compile/install/relocate the complete Linux application in CI. --- .github/workflows/switch2kit-linux.yml | 68 ++++++++++++++++++++++++++ CMakeLists.txt | 20 ++++++-- dependencies/Switch2Kit | 2 +- scripts/build-switch2kit.sh | 42 +++++++++++----- 4 files changed, 113 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/switch2kit-linux.yml mode change 100644 => 100755 scripts/build-switch2kit.sh diff --git a/.github/workflows/switch2kit-linux.yml b/.github/workflows/switch2kit-linux.yml new file mode 100644 index 0000000000..a877fd4940 --- /dev/null +++ b/.github/workflows/switch2kit-linux.yml @@ -0,0 +1,68 @@ +name: Switch2Kit Linux application +on: + pull_request: + workflow_dispatch: +permissions: + contents: read +concurrency: + group: switch2kit-linux-${{ github.ref }} + cancel-in-progress: true +jobs: + linux: + runs-on: ubuntu-24.04 + container: swift:6.2.1-noble + timeout-minutes: 120 + env: + DEBIAN_FRONTEND: noninteractive + VCPKG_MAX_CONCURRENCY: 3 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + submodules: recursive + - name: Install native dependencies + run: | + apt-get update + apt-get install -y --no-install-recommends build-essential cmake ninja-build python3 pkg-config curl zip unzip tar zstd nasm autoconf automake libtool libtool-bin gettext freeglut3-dev libgcrypt20-dev libglm-dev libgtk-3-dev libpulse-dev libsecret-1-dev libsystemd-dev libudev-dev libbluetooth-dev libgl1-mesa-dev libglu1-mesa-dev libx11-xcb-dev libwayland-dev wayland-protocols extra-cmake-modules dbus xvfb xauth + git config --global --add safe.directory "$PWD" + mkdir -p "$HOME/.cache/vcpkg/archives" + echo "VCPKG_BINARY_SOURCES=clear;files,$HOME/.cache/vcpkg/archives,readwrite" >> "$GITHUB_ENV" + - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 + with: + path: ~/.cache/vcpkg/archives + key: switch2kit-cemu-linux-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json', 'dependencies/vcpkg_overlay_ports*/**') }} + restore-keys: switch2kit-cemu-linux- + - name: Build and install the controller-enabled application + shell: bash + run: | + set -o pipefail + bash scripts/build-switch2kit.sh 2>&1 | tee linux-build.log + - name: Execute controller policies + run: python3 tests/switch2kit/run.py --sanitize + - name: Relocate, resolve native dependencies and launch the installed binary + shell: bash + run: | + set -euo pipefail + mv build-switch2kit/install "$RUNNER_TEMP/Cemu-Switch2Kit-linux-x86_64" + app="$RUNNER_TEMP/Cemu-Switch2Kit-linux-x86_64" + mv build-switch2kit "$RUNNER_TEMP/disabled-build-tree" + test -n "$(find "$app" -name libSwitch2KitC.so -print -quit)" + ldd "$app/bin/Cemu_release" | tee linux-dependencies.log + ! grep -q 'not found' linux-dependencies.log + grep -F "$app" linux-dependencies.log | grep libSwitch2KitC + xvfb-run -a "$app/bin/Cemu_release" --version + tar -C "$RUNNER_TEMP" -czf Cemu-Switch2Kit-linux-x86_64.tar.gz Cemu-Switch2Kit-linux-x86_64 + - name: Controller-enabled development build + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: Cemu-Switch2Kit-linux-x86_64 + path: Cemu-Switch2Kit-linux-x86_64.tar.gz + retention-days: 14 + if-no-files-found: error + - name: Native diagnostics + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: Cemu-Switch2Kit-linux-diagnostics + path: linux-*.log + retention-days: 7 diff --git a/CMakeLists.txt b/CMakeLists.txt index 5de6f7a94b..94fe48067a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.21.1) option(ENABLE_VCPKG "Enable the vcpkg package manager" ON) -option(ENABLE_SWITCH2KIT "Use in-process Switch2Kit controllers on macOS 15+" OFF) +option(ENABLE_SWITCH2KIT "Use in-process Switch2Kit controllers" OFF) set(SWITCH2KIT_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/dependencies/Switch2Kit" CACHE PATH "Switch2Kit source checkout") option(MACOS_BUNDLE "The executable when built on macOS will be created as an application bundle" OFF) option(ALLOW_PORTABLE "Allow Cemu to be run in portable mode" ON) @@ -173,11 +173,14 @@ if(ENABLE_SDL) endif() if(ENABLE_SWITCH2KIT) - if(NOT APPLE OR NOT ENABLE_SDL OR NOT MACOS_BUNDLE) - message(FATAL_ERROR "Switch2Kit requires macOS, ENABLE_SDL and MACOS_BUNDLE") + if(NOT (APPLE OR WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux")) + message(FATAL_ERROR "Switch2Kit requires a macOS, Windows or Linux desktop host") endif() - if(CMAKE_OSX_DEPLOYMENT_TARGET VERSION_LESS 15.0) - message(FATAL_ERROR "Switch2Kit requires CMAKE_OSX_DEPLOYMENT_TARGET=15.0 or newer") + if(NOT ENABLE_SDL) + message(FATAL_ERROR "Switch2Kit requires ENABLE_SDL") + endif() + if(APPLE AND (NOT MACOS_BUNDLE OR CMAKE_OSX_DEPLOYMENT_TARGET VERSION_LESS 15.0)) + message(FATAL_ERROR "Switch2Kit on macOS requires MACOS_BUNDLE and CMAKE_OSX_DEPLOYMENT_TARGET=15.0 or newer") endif() if(NOT EXISTS "${SWITCH2KIT_SOURCE_DIR}/Integrations/SDL3/CMakeLists.txt") message(FATAL_ERROR "Switch2Kit is missing. Run git submodule update --init --recursive") @@ -289,3 +292,10 @@ if (NOT ZArchive_FOUND) endif() add_subdirectory(src) + +if(ENABLE_SWITCH2KIT AND CMAKE_SYSTEM_NAME STREQUAL "Linux") + include(GNUInstallDirs) + switch2kit_install_linux(CemuBin) + install(TARGETS CemuBin RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") + install(DIRECTORY bin/gameProfiles bin/resources DESTINATION "${CMAKE_INSTALL_DATADIR}/Cemu") +endif() diff --git a/dependencies/Switch2Kit b/dependencies/Switch2Kit index 8088ce3ef6..0482227389 160000 --- a/dependencies/Switch2Kit +++ b/dependencies/Switch2Kit @@ -1 +1 @@ -Subproject commit 8088ce3ef6845fe90d8ff7a579e6498668bd65de +Subproject commit 048222738938d0ac2fccfebfd5d0ede4cedb5f9a diff --git a/scripts/build-switch2kit.sh b/scripts/build-switch2kit.sh old mode 100644 new mode 100755 index 250153f74b..cef0813474 --- a/scripts/build-switch2kit.sh +++ b/scripts/build-switch2kit.sh @@ -2,19 +2,35 @@ # Build the checked-out revision and its pinned controller library. set -euo pipefail cd "$(dirname "$0")/.." -if [ "$(uname -s)" != Darwin ]; then echo "This build requires macOS 15 or newer." >&2; exit 1; fi -if [ "$(sysctl -in sysctl.proc_translated 2>/dev/null || true)" = 1 ]; then - echo "Use a native Terminal, not Rosetta, on Apple silicon." >&2; exit 1 -fi -for tool in cmake ninja git xcrun; do command -v "$tool" >/dev/null || { echo "Missing build tool: $tool" >&2; exit 1; }; done -xcrun swift --version +platform=$(uname -s) +options=(-DCMAKE_BUILD_TYPE=Release -DENABLE_SWITCH2KIT=ON -DENABLE_SDL=ON -DENABLE_VULKAN=ON) +case "$platform" in + Darwin) + if [ "$(sysctl -in sysctl.proc_translated 2>/dev/null || true)" = 1 ]; then + echo "Use a native Terminal, not Rosetta, on Apple silicon." >&2; exit 1 + fi + command -v xcrun >/dev/null || { echo 'Install and select Xcode 26+.' >&2; exit 1; } + xcrun swift --version + options+=(-DCMAKE_OSX_DEPLOYMENT_TARGET=15.0 "-DCMAKE_OSX_ARCHITECTURES=$(uname -m)" -DMACOS_BUNDLE=ON) + ;; + Linux) + command -v swift >/dev/null || { echo 'Install the Swift 6.2+ toolchain.' >&2; exit 1; } + swift --version + options+=(-DMACOS_BUNDLE=OFF "-DCMAKE_INSTALL_PREFIX=$PWD/build-switch2kit/install") + ;; + *) echo 'Use macOS or Linux with this helper; use the PowerShell helper on Windows.' >&2; exit 1 ;; +esac +for tool in cmake ninja git; do command -v "$tool" >/dev/null || { echo "Missing build tool: $tool" >&2; exit 1; }; done export VCPKG_MAX_CONCURRENCY="${VCPKG_MAX_CONCURRENCY:-3}" git submodule update --init --recursive if [ ! -x dependencies/vcpkg/vcpkg ]; then bash dependencies/vcpkg/bootstrap-vcpkg.sh; fi -cmake -S . -B build-switch2kit -G Ninja \ - -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_DEPLOYMENT_TARGET=15.0 \ - -DCMAKE_OSX_ARCHITECTURES="$(uname -m)" \ - -DENABLE_SWITCH2KIT=ON -DENABLE_SDL=ON -DMACOS_BUNDLE=ON -DENABLE_VULKAN=ON -cmake --build build-switch2kit --target CemuBin --parallel 3 -echo "Built: $PWD/bin/Cemu_release.app" -if [ "${1:-}" = --run ]; then open "$PWD/bin/Cemu_release.app"; fi +cmake -S . -B build-switch2kit -G Ninja "${options[@]}" +cmake --build build-switch2kit --target CemuBin --parallel "${S2K_BUILD_JOBS:-3}" +if [ "$platform" = Darwin ]; then + echo "Built: $PWD/bin/Cemu_release.app" + if [ "${1:-}" = --run ]; then open "$PWD/bin/Cemu_release.app"; fi +else + cmake --install build-switch2kit + echo "Built: $PWD/build-switch2kit/install/bin/Cemu_release" + if [ "${1:-}" = --run ]; then exec "$PWD/build-switch2kit/install/bin/Cemu_release"; fi +fi From dfb8982e54f0f54b86a4c6e2d989b7d9131afd4b Mon Sep 17 00:00:00 2001 From: Johnny D Date: Fri, 18 Sep 2026 15:18:18 -0400 Subject: [PATCH 03/12] Fix desktop Switch2Kit input-loop ownership and platform policy regressions Run native SDL initialization, pumping and shutdown on the GUI main thread on all enabled platforms, retaining the existing worker in backend-disabled non-macOS builds. This fixes Linux controller discovery never reaching the SDL adapter and keeps session state single-threaded. Update the stale macOS-only source contract while retaining SDL, supported-host, macOS bundle/deployment and opt-in gates. Add production-preprocessor ownership tests for six platform/feature combinations and execute isolated CMake admission cases. Preserve policy diagnostics in the Linux CI artifact. The new lifecycle regression fails on the previous Linux-enabled source; the full policy suite passes locally with Clang and GCC ASan/UBSan. Full application and hardware qualification remain separate. --- .github/workflows/switch2kit-linux.yml | 5 +- src/gui/wxgui/CemuApp.cpp | 15 +-- src/gui/wxgui/CemuApp.h | 2 +- src/input/api/SDL/SDLControllerProvider.cpp | 6 +- src/input/api/SDL/SDLControllerProvider.h | 6 +- src/input/api/SDL/Switch2KitSession.h | 2 +- tests/switch2kit/run.py | 1 + tests/switch2kit/test_desktop_lifecycle.py | 130 ++++++++++++++++++++ tests/switch2kit/test_wiring.py | 6 +- 9 files changed, 155 insertions(+), 18 deletions(-) create mode 100644 tests/switch2kit/test_desktop_lifecycle.py diff --git a/.github/workflows/switch2kit-linux.yml b/.github/workflows/switch2kit-linux.yml index a877fd4940..aa565152ef 100644 --- a/.github/workflows/switch2kit-linux.yml +++ b/.github/workflows/switch2kit-linux.yml @@ -38,7 +38,10 @@ jobs: set -o pipefail bash scripts/build-switch2kit.sh 2>&1 | tee linux-build.log - name: Execute controller policies - run: python3 tests/switch2kit/run.py --sanitize + shell: bash + run: | + set -o pipefail + python3 tests/switch2kit/run.py --sanitize 2>&1 | tee linux-policies.log - name: Relocate, resolve native dependencies and launch the installed binary shell: bash run: | diff --git a/src/gui/wxgui/CemuApp.cpp b/src/gui/wxgui/CemuApp.cpp index 98a3fc4002..3c19d569f1 100644 --- a/src/gui/wxgui/CemuApp.cpp +++ b/src/gui/wxgui/CemuApp.cpp @@ -338,12 +338,12 @@ bool CemuApp::OnInit() UnitTests(); #endif -#if BOOST_OS_MACOS +#if BOOST_OS_MACOS || defined(HAVE_SWITCH2KIT) SDLControllerProvider::InitSDL(); #endif CemuCommonInit(); -#if BOOST_OS_MACOS +#if BOOST_OS_MACOS || defined(HAVE_SWITCH2KIT) m_sdlEventPumpTimer = new wxTimer(this); Bind(wxEVT_TIMER, &CemuApp::OnSDLEventPumpTimer, this); m_sdlEventPumpTimer->Start(5, wxTIMER_CONTINUOUS); @@ -390,7 +390,7 @@ bool CemuApp::OnInit() int CemuApp::OnExit() { -#if BOOST_OS_MACOS +#if BOOST_OS_MACOS || defined(HAVE_SWITCH2KIT) if (m_sdlEventPumpTimer) { m_sdlEventPumpTimer->Stop(); @@ -405,7 +405,7 @@ int CemuApp::OnExit() int retValue = 0; if (auto r = CafeSystem::GetForegroundTitleReturnStatus(); (LaunchSettings::GetLoadFile() || LaunchSettings::GetLoadTitleID()) && r) retValue = *r; -#if BOOST_OS_MACOS +#if BOOST_OS_MACOS || defined(HAVE_SWITCH2KIT) SDLControllerProvider::ShutdownSDL(); #endif // handle restart if requested @@ -434,11 +434,12 @@ int CemuApp::OnExit() #endif } -#if BOOST_OS_MACOS +#if BOOST_OS_MACOS || defined(HAVE_SWITCH2KIT) void CemuApp::OnSDLEventPumpTimer(wxTimerEvent& event) { - // this callback is only used on macOS where SDL event functions need to be called on the main thread - // on other platforms SDLControllerProvider creates a separate thread for SDL event polling + // macOS requires main-thread SDL events. Native Switch2Kit also uses this + // owner on other platforms so Find, Disconnect, Pump and shutdown serialize. + // Backend-disabled non-macOS builds retain SDLControllerProvider's worker. SDLControllerProvider::PumpSDLEvents(); } #endif diff --git a/src/gui/wxgui/CemuApp.h b/src/gui/wxgui/CemuApp.h index 978123dd27..e5a49e503b 100644 --- a/src/gui/wxgui/CemuApp.h +++ b/src/gui/wxgui/CemuApp.h @@ -34,7 +34,7 @@ class CemuApp : public wxApp static std::vector GetAvailableTranslationLanguages(wxTranslations* translationsMgr); MainWindow* m_mainFrame = nullptr; -#if BOOST_OS_MACOS +#if BOOST_OS_MACOS || defined(HAVE_SWITCH2KIT) void OnSDLEventPumpTimer(wxTimerEvent& event); wxTimer* m_sdlEventPumpTimer = nullptr; #endif diff --git a/src/input/api/SDL/SDLControllerProvider.cpp b/src/input/api/SDL/SDLControllerProvider.cpp index 88c97a2d32..773abc59e3 100644 --- a/src/input/api/SDL/SDLControllerProvider.cpp +++ b/src/input/api/SDL/SDLControllerProvider.cpp @@ -87,7 +87,7 @@ struct SDL_JoystickGUIDHash SDLControllerProvider::SDLControllerProvider() { -#if !BOOST_OS_MACOS +#if !BOOST_OS_MACOS && !defined(HAVE_SWITCH2KIT) std::scoped_lock _l(s_mutex); if (s_initCount.fetch_add(1) == 0) { @@ -99,7 +99,7 @@ SDLControllerProvider::SDLControllerProvider() SDLControllerProvider::~SDLControllerProvider() { -#if !BOOST_OS_MACOS +#if !BOOST_OS_MACOS && !defined(HAVE_SWITCH2KIT) bool shutdownSDL = false; { std::scoped_lock _l(s_mutex); @@ -240,7 +240,7 @@ void SDLControllerProvider::ShutdownSDL() SDL_QuitSubSystem(SDL_INIT_GAMEPAD | SDL_INIT_HAPTIC); } -#if BOOST_OS_MACOS +#if BOOST_OS_MACOS || defined(HAVE_SWITCH2KIT) void SDLControllerProvider::PumpSDLEvents() { #ifdef HAVE_SWITCH2KIT diff --git a/src/input/api/SDL/SDLControllerProvider.h b/src/input/api/SDL/SDLControllerProvider.h index 98e3b2007d..d5acd5009f 100644 --- a/src/input/api/SDL/SDLControllerProvider.h +++ b/src/input/api/SDL/SDLControllerProvider.h @@ -34,8 +34,8 @@ class SDLControllerProvider : public ControllerProviderBase static std::optional AvailableSwitch2Motion(SDL_JoystickID id); #endif - // exposed for manual event handling on macOS -#if BOOST_OS_MACOS + // Main-loop ownership on macOS and whenever native Switch2Kit is enabled. +#if BOOST_OS_MACOS || defined(HAVE_SWITCH2KIT) static void InitSDL(); static void ShutdownSDL(); static void PumpSDLEvents(); @@ -44,7 +44,7 @@ class SDLControllerProvider : public ControllerProviderBase private: void event_thread(); static void HandleSDLEvent(union SDL_Event& event); -#if !BOOST_OS_MACOS +#if !BOOST_OS_MACOS && !defined(HAVE_SWITCH2KIT) static void InitSDL(); static void ShutdownSDL(); #endif diff --git a/src/input/api/SDL/Switch2KitSession.h b/src/input/api/SDL/Switch2KitSession.h index a4d762d0fb..84332d5e1f 100644 --- a/src/input/api/SDL/Switch2KitSession.h +++ b/src/input/api/SDL/Switch2KitSession.h @@ -1,6 +1,6 @@ #pragma once -// Start/Stop/Pump are called on Cemu's macOS main thread. The SDK host serializes +// Start/Stop/Pump are called on Cemu's GUI main thread on every native platform. The SDK host serializes // identity, profile and status access from controller/configuration threads. // Keeping the state here also prevents a failed first start from enabling polling. template diff --git a/tests/switch2kit/run.py b/tests/switch2kit/run.py index 565d019d1c..88ed07e29b 100644 --- a/tests/switch2kit/run.py +++ b/tests/switch2kit/run.py @@ -55,6 +55,7 @@ def run(): subprocess.run(command, check=True, timeout=120) subprocess.run([str(binary)], check=True, timeout=30) subprocess.run(['python3', str(ROOT / 'tests/switch2kit/test_wiring.py')], check=True) + subprocess.run(['python3', str(ROOT / 'tests/switch2kit/test_desktop_lifecycle.py')], check=True) if __name__ == '__main__': run() diff --git a/tests/switch2kit/test_desktop_lifecycle.py b/tests/switch2kit/test_desktop_lifecycle.py new file mode 100644 index 0000000000..5773d47dc7 --- /dev/null +++ b/tests/switch2kit/test_desktop_lifecycle.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +"""Exercise production preprocessor ownership and isolated CMake admission gates. + +This does not emulate a controller or claim a native GUI/platform build. Only +includes are removed for preprocessing; the production conditional directives +and method bodies are retained. CMake imports are recorded rather than built. +""" +import os +from pathlib import Path +import re +import shutil +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[2] + + +def text(path): + return (ROOT / path).read_text() + + +def between(source, start, end): + return source.split(start, 1)[1].split(end, 1)[0] + + +class DesktopLifecycle(unittest.TestCase): + def test_single_sdl_owner_for_each_platform_and_feature_mode(self): + compiler = os.environ.get('CXX') or shutil.which('clang++') or shutil.which('g++') + self.assertTrue(compiler, 'A C++ preprocessor is required') + for platform in ('MACOS', 'LINUX', 'WINDOWS'): + for native in (False, True): + with self.subTest(platform=platform, native=native): + flags = [f'-DBOOST_OS_{name}={int(name == platform)}' + for name in ('MACOS', 'LINUX', 'WINDOWS')] + if native: + flags.append('-DHAVE_SWITCH2KIT') + + def preprocess(path): + source = re.sub(r'^\s*#\s*(include|pragma)\b[^\n]*', '', + text(path), flags=re.M) + return subprocess.run( + [compiler, '-E', '-P', '-x', 'c++', *flags, '-'], + input=source, text=True, capture_output=True, + check=True, timeout=30).stdout + + app = preprocess('src/gui/wxgui/CemuApp.cpp') + app_header = preprocess('src/gui/wxgui/CemuApp.h') + provider = preprocess('src/input/api/SDL/SDLControllerProvider.cpp') + header = preprocess('src/input/api/SDL/SDLControllerProvider.h') + main = platform == 'MACOS' or native + constructor = between(provider, 'SDLControllerProvider::SDLControllerProvider()', + 'SDLControllerProvider::~SDLControllerProvider()') + destructor = between(provider, 'SDLControllerProvider::~SDLControllerProvider()', + 'SDLControllerProvider::get_controllers()') + self.assertEqual('s_thread = std::thread' in constructor, not main) + self.assertEqual('s_thread.join()' in destructor, not main) + self.assertEqual('void SDLControllerProvider::PumpSDLEvents()' in provider, main) + self.assertEqual('SDLControllerProvider::PumpSDLEvents();' in app, main) + self.assertEqual('void OnSDLEventPumpTimer(' in app_header, main) + self.assertEqual('static void PumpSDLEvents();' in header, main) + # No duplicated public/private SDL lifecycle declarations. + self.assertEqual(header.count('static void InitSDL();'), 1) + self.assertEqual(header.count('static void ShutdownSDL();'), 1) + self.assertEqual('NativeSession().Pump()' in provider, native) + if main: + startup = between(app, 'bool CemuApp::OnInit()', 'int CemuApp::OnExit()') + shutdown = between(app, 'int CemuApp::OnExit()', + 'void CemuApp::OnSDLEventPumpTimer(') + self.assertLess(startup.index('SDLControllerProvider::InitSDL();'), + startup.index('CemuCommonInit();')) + self.assertIn('m_sdlEventPumpTimer->Start(5, wxTIMER_CONTINUOUS)', startup) + self.assertLess(shutdown.index('m_sdlEventPumpTimer->Stop()'), + shutdown.index('InputManager::instance().Shutdown()')) + self.assertLess(shutdown.index('InputManager::instance().Shutdown()'), + shutdown.index('SDLControllerProvider::ShutdownSDL()')) + else: + self.assertNotIn('SDLControllerProvider::InitSDL();', app) + self.assertNotIn('SDLControllerProvider::ShutdownSDL();', app) + self.assertIn('SDL_WaitEvent(&event)', provider) + + def test_production_cmake_gates(self): + cmake = shutil.which('cmake') + self.assertTrue(cmake, 'CMake is required') + policy = 'if(ENABLE_SWITCH2KIT)' + text('CMakeLists.txt').split( + 'if(ENABLE_SWITCH2KIT)', 1)[1].split('\n# glslang', 1)[0] + # platform, native, SDL, bundle, deployment, SDK exists, expected admission/error + cases = [ + ('Darwin', False, False, False, '13.4', True, False, None), + ('FreeBSD', False, False, False, '', False, False, None), + ('Darwin', True, True, True, '15.0', True, True, None), + ('Darwin', True, True, False, '15.0', True, False, 'MACOS_BUNDLE'), + ('Darwin', True, True, True, '13.4', True, False, '15.0'), + ('Linux', True, True, False, '', True, True, None), + ('Linux', True, False, False, '', True, False, 'ENABLE_SDL'), + ('Linux', True, True, False, '', False, False, 'Switch2Kit is missing'), + ('FreeBSD', True, True, False, '', True, False, 'desktop host'), + ] + with tempfile.TemporaryDirectory(prefix='cemu-cmake-policy-') as directory: + script = Path(directory) / 'policy.cmake' + sdk = ROOT / 'dependencies/Switch2Kit' + for platform, native, sdl, bundle, version, exists, admitted, error in cases: + with self.subTest(platform=platform, native=native, sdl=sdl, + bundle=bundle, version=version, sdk=exists): + values = dict(APPLE=platform == 'Darwin', WIN32=False, + CMAKE_SYSTEM_NAME=platform, ENABLE_SWITCH2KIT=native, + ENABLE_SDL=sdl, MACOS_BUNDLE=bundle, + CMAKE_OSX_DEPLOYMENT_TARGET=version, + SWITCH2KIT_SOURCE_DIR=str(sdk if exists else Path(directory)/'absent')) + setup = ''.join(f'set({key} "{value}")\n' for key, value in values.items()) + script.write_text(setup + ''' +function(add_subdirectory) + message(STATUS "SWITCH2KIT_ADMITTED") +endfunction() +function(add_compile_definitions) +endfunction() +function(include_directories) +endfunction() +''' + policy) + result = subprocess.run([cmake, '-P', str(script)], text=True, + capture_output=True, timeout=30) + output = result.stdout + result.stderr + self.assertEqual(result.returncode == 0, error is None, output) + self.assertEqual('SWITCH2KIT_ADMITTED' in output, admitted, output) + if error: + self.assertIn(error, output) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/tests/switch2kit/test_wiring.py b/tests/switch2kit/test_wiring.py index d4e01633f8..7fd4493bff 100644 --- a/tests/switch2kit/test_wiring.py +++ b/tests/switch2kit/test_wiring.py @@ -9,8 +9,10 @@ def text(path): return (ROOT / path).read_text() class Wiring(unittest.TestCase): def test_opt_in_bundle_and_target(self): cmake = text('CMakeLists.txt') - self.assertIn('option(ENABLE_SWITCH2KIT "Use in-process Switch2Kit controllers on macOS 15+" OFF)', cmake) - self.assertIn('NOT APPLE OR NOT ENABLE_SDL OR NOT MACOS_BUNDLE', cmake) + self.assertIn('option(ENABLE_SWITCH2KIT "Use in-process Switch2Kit controllers" OFF)', cmake) + self.assertIn('if(NOT ENABLE_SDL)', cmake) + self.assertIn('if(NOT (APPLE OR WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux"))', cmake) + self.assertIn('if(APPLE AND (NOT MACOS_BUNDLE OR CMAKE_OSX_DEPLOYMENT_TARGET VERSION_LESS 15.0))', cmake) self.assertIn('CMAKE_OSX_DEPLOYMENT_TARGET VERSION_LESS 15.0', cmake) self.assertIn('${CMAKE_CURRENT_SOURCE_DIR}/dependencies/Switch2Kit', cmake) self.assertIn('SWITCH2KIT_BLUETOOTH_USAGE', text('src/resource/MacOSXBundleInfo.plist.in')) From ab4ac09627f460baf6c1b660c62731096e8244f3 Mon Sep 17 00:00:00 2001 From: Johnny D Date: Fri, 18 Sep 2026 15:22:35 -0400 Subject: [PATCH 04/12] Preserve external SDK selection in desktop lifecycle tests Forward the existing policy runner's --sdk option to the new CMake admission tests. Verified the complete sanitizer suite with the in-tree SDK moved away and only the explicitly supplied external checkout available. --- tests/switch2kit/run.py | 3 ++- tests/switch2kit/test_desktop_lifecycle.py | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/switch2kit/run.py b/tests/switch2kit/run.py index 88ed07e29b..6d1da9f721 100644 --- a/tests/switch2kit/run.py +++ b/tests/switch2kit/run.py @@ -55,7 +55,8 @@ def run(): subprocess.run(command, check=True, timeout=120) subprocess.run([str(binary)], check=True, timeout=30) subprocess.run(['python3', str(ROOT / 'tests/switch2kit/test_wiring.py')], check=True) - subprocess.run(['python3', str(ROOT / 'tests/switch2kit/test_desktop_lifecycle.py')], check=True) + subprocess.run(['python3', str(ROOT / 'tests/switch2kit/test_desktop_lifecycle.py'), + '--sdk', str(sdk)], check=True) if __name__ == '__main__': run() diff --git a/tests/switch2kit/test_desktop_lifecycle.py b/tests/switch2kit/test_desktop_lifecycle.py index 5773d47dc7..495f87e244 100644 --- a/tests/switch2kit/test_desktop_lifecycle.py +++ b/tests/switch2kit/test_desktop_lifecycle.py @@ -5,6 +5,7 @@ includes are removed for preprocessing; the production conditional directives and method bodies are retained. CMake imports are recorded rather than built. """ +import argparse import os from pathlib import Path import re @@ -14,6 +15,7 @@ import unittest ROOT = Path(__file__).resolve().parents[2] +SDK = ROOT / 'dependencies/Switch2Kit' def text(path): @@ -98,7 +100,7 @@ def test_production_cmake_gates(self): ] with tempfile.TemporaryDirectory(prefix='cemu-cmake-policy-') as directory: script = Path(directory) / 'policy.cmake' - sdk = ROOT / 'dependencies/Switch2Kit' + sdk = SDK for platform, native, sdl, bundle, version, exists, admitted, error in cases: with self.subTest(platform=platform, native=native, sdl=sdl, bundle=bundle, version=version, sdk=exists): @@ -127,4 +129,8 @@ def test_production_cmake_gates(self): if __name__ == '__main__': - unittest.main(verbosity=2) + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--sdk', type=Path, default=SDK) + args = parser.parse_args() + SDK = args.sdk.resolve() + unittest.main(argv=[__file__], verbosity=2) From 477ee337d10ec1e1a0cf737f5740cdda9faebaa9 Mon Sep 17 00:00:00 2001 From: Johnny D Date: Fri, 18 Sep 2026 16:00:52 -0400 Subject: [PATCH 05/12] Build and package native Switch2Kit on Windows x64 Pin the Windows-capable SDK, embed its DLL beside Cemu, add a PowerShell build-and-run helper and full Windows application CI with relocation and normal launch/quit/relaunch checks. Preserve the real opt-in test without prescribing CMake help text. Let an already-running Linux dependency build finish instead of discarding its cache on each commit. --- .github/workflows/switch2kit-linux.yml | 2 +- .github/workflows/switch2kit-windows.yml | 60 ++++++++++++++++++++++++ dependencies/Switch2Kit | 2 +- scripts/build-switch2kit.ps1 | 42 +++++++++++++++++ src/CMakeLists.txt | 4 ++ tests/switch2kit/test_wiring.py | 3 +- tests/switch2kit/windows-launch.ps1 | 32 +++++++++++++ 7 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/switch2kit-windows.yml create mode 100644 scripts/build-switch2kit.ps1 create mode 100644 tests/switch2kit/windows-launch.ps1 diff --git a/.github/workflows/switch2kit-linux.yml b/.github/workflows/switch2kit-linux.yml index aa565152ef..f76cd7b3ca 100644 --- a/.github/workflows/switch2kit-linux.yml +++ b/.github/workflows/switch2kit-linux.yml @@ -6,7 +6,7 @@ permissions: contents: read concurrency: group: switch2kit-linux-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: false jobs: linux: runs-on: ubuntu-24.04 diff --git a/.github/workflows/switch2kit-windows.yml b/.github/workflows/switch2kit-windows.yml new file mode 100644 index 0000000000..0c9090063b --- /dev/null +++ b/.github/workflows/switch2kit-windows.yml @@ -0,0 +1,60 @@ +name: Switch2Kit Windows application +on: + pull_request: + workflow_dispatch: +permissions: + contents: read +concurrency: + group: switch2kit-windows-${{ github.ref }} + cancel-in-progress: true +jobs: + windows: + runs-on: windows-2022 + timeout-minutes: 120 + env: + VCPKG_MAX_CONCURRENCY: 3 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + submodules: recursive + - uses: compnerd/gha-setup-swift@397094e75494a93fa8d81db0268dbc8f5d6cf7c6 + with: + swift-version: swift-6.2.1-release + swift-build: 6.2.1-RELEASE + - name: Build the complete controller-enabled application + shell: pwsh + run: | + & ./scripts/build-switch2kit.ps1 2>&1 | Tee-Object windows-build.log + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: Stage and launch the application away from the build tree + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $stage = Join-Path $env:RUNNER_TEMP 'Cemu-Switch2Kit-windows-x86_64' + New-Item -ItemType Directory -Path $stage | Out-Null + # Robocopy follows directory junctions and copies linked resources. + robocopy 'bin' $stage /E /NFL /NDL /NJH /NJS + if ($LASTEXITCODE -ge 8) { throw 'Application staging failed.' } + $global:LASTEXITCODE = 0 + New-Item -ItemType Directory -Path "$stage/user" -Force | Out-Null + Move-Item build-switch2kit-windows "$env:RUNNER_TEMP/disabled-build-tree" + & ./tests/switch2kit/windows-launch.ps1 -Executable "$stage/Cemu_release.exe" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Compress-Archive -Path $stage -DestinationPath 'Cemu-Switch2Kit-windows-x86_64.zip' + - name: Controller-enabled development application + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: Cemu-Switch2Kit-windows-x86_64 + path: Cemu-Switch2Kit-windows-x86_64.zip + retention-days: 14 + if-no-files-found: error + - name: Native diagnostics + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: Cemu-Switch2Kit-windows-diagnostics + path: | + windows-*.log + windows-launch.json + retention-days: 7 diff --git a/dependencies/Switch2Kit b/dependencies/Switch2Kit index 0482227389..3a66976255 160000 --- a/dependencies/Switch2Kit +++ b/dependencies/Switch2Kit @@ -1 +1 @@ -Subproject commit 048222738938d0ac2fccfebfd5d0ede4cedb5f9a +Subproject commit 3a669762553ebb6bd7724f733260f306a76563c8 diff --git a/scripts/build-switch2kit.ps1 b/scripts/build-switch2kit.ps1 new file mode 100644 index 0000000000..232edae5d9 --- /dev/null +++ b/scripts/build-switch2kit.ps1 @@ -0,0 +1,42 @@ +param([switch]$Run) +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest +Set-Location (Join-Path $PSScriptRoot '..') +if (-not [Environment]::Is64BitOperatingSystem -or -not [Environment]::Is64BitProcess) { + throw 'Use 64-bit PowerShell on x64 Windows.' +} +foreach ($tool in @('git', 'cmake', 'ninja', 'swift', 'swiftc')) { + if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) { throw "Missing build tool: $tool" } +} +$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" +if (-not (Test-Path $vswhere)) { throw 'Install Visual Studio 2022 Desktop development with C++ and a Windows SDK.' } +$vs = & $vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath +if (-not $vs) { throw 'Visual C++ x64 tools were not found.' } +cmd /c "`"$vs\Common7\Tools\VsDevCmd.bat`" -arch=x64 -host_arch=x64 >nul && set" | ForEach-Object { + if ($_ -match '^([^=]+)=(.*)$') { [Environment]::SetEnvironmentVariable($Matches[1], $Matches[2], 'Process') } +} +if ($LASTEXITCODE -ne 0) { throw 'Could not initialize the Visual C++ environment.' } +$target = swiftc -print-target-info | ConvertFrom-Json +if ($LASTEXITCODE -ne 0 -or $target.target.triple -notmatch '^x86_64-.*windows-msvc$') { + throw 'Install the native x64 Swift toolchain; ARM64 and cross-compilation are not supported here.' +} +# Keep runtime lookup local to this process and its launched application. +$env:PATH = (($target.paths.runtimeLibraryPaths | Where-Object { Test-Path $_ }) -join ';') + ';' + $env:PATH +git submodule update --init --recursive +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +if (-not (Test-Path 'dependencies/vcpkg/vcpkg.exe')) { + & .\dependencies\vcpkg\bootstrap-vcpkg.bat -disableMetrics + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +} +if (-not $env:VCPKG_MAX_CONCURRENCY) { $env:VCPKG_MAX_CONCURRENCY = '3' } +cmake -S . -B build-switch2kit-windows -G Ninja -DCMAKE_BUILD_TYPE=Release ` + -DENABLE_SWITCH2KIT=ON -DENABLE_SDL=ON -DENABLE_VULKAN=ON -DMACOS_BUNDLE=OFF +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +cmake --build build-switch2kit-windows --target CemuBin --parallel 3 +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +$app = Join-Path $PWD 'bin/Cemu_release.exe' +if (-not (Test-Path $app) -or -not (Test-Path (Join-Path (Split-Path $app) 'Switch2KitC.dll'))) { + throw 'The controller-enabled application or its native DLL is missing.' +} +Write-Host "Built: $app" +if ($Run) { Start-Process -FilePath $app -WorkingDirectory (Split-Path $app) } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 10902ea213..d42cbdca3a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -240,3 +240,7 @@ endif() if (BSD) target_link_libraries(CemuBin PRIVATE execinfo SPIRV-Tools SPIRV-Tools-opt) endif() + +if(WIN32 AND ENABLE_SWITCH2KIT) + switch2kit_embed_windows(CemuBin) +endif() diff --git a/tests/switch2kit/test_wiring.py b/tests/switch2kit/test_wiring.py index 7fd4493bff..45f4daca34 100644 --- a/tests/switch2kit/test_wiring.py +++ b/tests/switch2kit/test_wiring.py @@ -9,7 +9,8 @@ def text(path): return (ROOT / path).read_text() class Wiring(unittest.TestCase): def test_opt_in_bundle_and_target(self): cmake = text('CMakeLists.txt') - self.assertIn('option(ENABLE_SWITCH2KIT "Use in-process Switch2Kit controllers" OFF)', cmake) + # Preserve the opt-in contract without prescribing the help text. + self.assertRegex(cmake, r'option\s*\(\s*ENABLE_SWITCH2KIT\s+"(?:[^"\\]|\\.)*"\s+OFF\s*\)') self.assertIn('if(NOT ENABLE_SDL)', cmake) self.assertIn('if(NOT (APPLE OR WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux"))', cmake) self.assertIn('if(APPLE AND (NOT MACOS_BUNDLE OR CMAKE_OSX_DEPLOYMENT_TARGET VERSION_LESS 15.0))', cmake) diff --git a/tests/switch2kit/windows-launch.ps1 b/tests/switch2kit/windows-launch.ps1 new file mode 100644 index 0000000000..e7f25b3549 --- /dev/null +++ b/tests/switch2kit/windows-launch.ps1 @@ -0,0 +1,32 @@ +param([Parameter(Mandatory=$true)][string]$Executable) +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest +$exe = (Resolve-Path $Executable).Path +$directory = Split-Path $exe +$expected = (Resolve-Path (Join-Path $directory 'Switch2KitC.dll')).Path +$report = @() +for ($attempt = 1; $attempt -le 2; $attempt++) { + $process = Start-Process -FilePath $exe -WorkingDirectory $directory -PassThru + try { + $deadline = [DateTime]::UtcNow.AddSeconds(60) + do { + Start-Sleep -Milliseconds 250 + $process.Refresh() + if ($process.HasExited) { throw "Application exited before opening a window: $($process.ExitCode)" } + } until ($process.MainWindowHandle -ne 0 -or [DateTime]::UtcNow -gt $deadline) + if ($process.MainWindowHandle -eq 0) { throw 'No application window appeared within 60 seconds.' } + $loaded = @($process.Modules | Where-Object { $_.ModuleName -eq 'Switch2KitC.dll' }) + if ($loaded.Count -ne 1 -or $loaded[0].FileName -ne $expected) { + throw 'The running application did not load its own Switch2Kit DLL.' + } + if (-not $process.CloseMainWindow()) { throw 'The application rejected a normal close request.' } + if (-not $process.WaitForExit(20000)) { throw 'The application did not shut down normally.' } + if ($process.ExitCode -ne 0) { throw "Application failed during shutdown: $($process.ExitCode)" } + $report += @{ attempt=$attempt; visibleWindow=$true; localControllerDLL=$true; exitCode=$process.ExitCode } + } finally { + if (-not $process.HasExited) { $process.Kill(); $process.WaitForExit() } + $process.Dispose() + } +} +$report | ConvertTo-Json | Set-Content windows-launch.json +Write-Host 'PASS relocated application launch, local DLL loading, normal quit and relaunch (no physical controller).' From 6799bccece295bf9a744abe0462a1c740927acc9 Mon Sep 17 00:00:00 2001 From: Johnny D Date: Fri, 18 Sep 2026 21:17:58 -0400 Subject: [PATCH 06/12] Fix Switch2Kit Windows host-file build failure and add regressions Pin the SDK's Windows-compatible bounded file reader instead of including POSIX-only unistd.h on MSVC. The SDK fix preserves POSIX behavior, UTF-8 paths, regular-file validation, byte/retry limits, and failure atomicity. Add standalone tests against the production SDK header for binary data, Unicode paths, missing/invalid/special files and size boundaries. Run them with the Linux sanitizer suite and with MSVC in the Windows application workflow. Keep all existing build, relocation and launch checks enabled. Validated locally: Clang and GCC host-file tests with ASan/UBSan; existing controller policy tests; 9 wiring tests; 2 desktop lifecycle tests. Full native Windows application validation remains a CI check. --- .github/workflows/switch2kit-windows.yml | 3 + dependencies/Switch2Kit | 2 +- tests/switch2kit/HostFileTests.cpp | 80 ++++++++++++++++++++++++ tests/switch2kit/run.py | 5 ++ tests/switch2kit/test_host_file.py | 42 +++++++++++++ 5 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 tests/switch2kit/HostFileTests.cpp mode change 100644 => 100755 tests/switch2kit/run.py create mode 100644 tests/switch2kit/test_host_file.py diff --git a/.github/workflows/switch2kit-windows.yml b/.github/workflows/switch2kit-windows.yml index 0c9090063b..e254c1e225 100644 --- a/.github/workflows/switch2kit-windows.yml +++ b/.github/workflows/switch2kit-windows.yml @@ -27,6 +27,9 @@ jobs: run: | & ./scripts/build-switch2kit.ps1 2>&1 | Tee-Object windows-build.log if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + # The build helper initialized MSVC in this PowerShell process. + python tests/switch2kit/test_host_file.py 2>&1 | Tee-Object windows-host-file.log + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Stage and launch the application away from the build tree shell: pwsh run: | diff --git a/dependencies/Switch2Kit b/dependencies/Switch2Kit index 3a66976255..0d76136e40 160000 --- a/dependencies/Switch2Kit +++ b/dependencies/Switch2Kit @@ -1 +1 @@ -Subproject commit 3a669762553ebb6bd7724f733260f306a76563c8 +Subproject commit 0d76136e40398f6a25aae1195a849fa1b66b071c diff --git a/tests/switch2kit/HostFileTests.cpp b/tests/switch2kit/HostFileTests.cpp new file mode 100644 index 0000000000..c7cd7ac992 --- /dev/null +++ b/tests/switch2kit/HostFileTests.cpp @@ -0,0 +1,80 @@ +// Exercise the production SDK header without Cemu's precompiled header. +#include "HostFile.hpp" +#include +#include +#include +#include + +namespace fs = std::filesystem; +using Switch2Kit::HostFileResult; +using Switch2Kit::readHostFile; + +static std::string utf8(const fs::path& path) { + const auto bytes = path.u8string(); + return std::string(bytes.begin(), bytes.end()); +} + +static void require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +static void reject(const std::string& path, size_t limit, + HostFileResult expected = HostFileResult::Invalid) { + std::string output = "unchanged"; + require(readHostFile(path, limit, output) == expected, "unexpected failure result"); + require(output == "unchanged", "failure changed the caller's output"); +} + +static void checkFile(const fs::path& path, const std::string& bytes, size_t limit) { + { + std::ofstream file(path, std::ios::binary); + file.write(bytes.data(), static_cast(bytes.size())); + require(file.good(), "could not create fixture"); + } + std::string output = "old contents"; + require(readHostFile(utf8(path), limit, output) == HostFileResult::OK, "regular file rejected"); + require(output == bytes, "file bytes changed or were truncated"); + if (bytes.size() > 1) reject(utf8(path), bytes.size() - 1); +} + +int main() { + try { + const auto root = fs::current_path() / "host-file-fixtures"; + require(fs::create_directory(root), "fixture directory already exists"); + const auto path = root / "profile.bin"; + checkFile(path, "", 1); + checkFile(path, std::string("a\r\nb\x1a\0\xff", 7), 7); // No CRT text translation. + checkFile(path, std::string(524288, 'x'), 524288); + checkFile(root / fs::path(u8"profile-\u03c0-\U0001f3ae.json"), "unicode path", 12); + reject(utf8(path), 0); + reject(utf8(path), 524289); + reject("", 1024); + reject(std::string(4097, 'x'), 1024); + reject("bad\npath", 1024); + reject("bad\x7fpath", 1024); + reject(std::string("bad\0path", 8), 1024); + reject(utf8(root), 1024); + reject(utf8(root / "missing"), 1024, HostFileResult::Missing); + reject(utf8(root / "absent" / "missing"), 1024, HostFileResult::Missing); +#if defined(_WIN32) + reject("NUL", 1024); + reject(std::string("\xc0\xaf", 2), 1024); // Invalid UTF-8, not an ANSI path. + const auto name = L"\\\\.\\pipe\\switch2kit-host-file-" + std::to_wstring(::GetCurrentProcessId()); + const HANDLE pipe = ::CreateNamedPipeW(name.c_str(), PIPE_ACCESS_DUPLEX, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, 1, 4096, 4096, 0, nullptr); + require(pipe != INVALID_HANDLE_VALUE, "could not create named-pipe fixture"); + struct Close { HANDLE value; ~Close() { ::CloseHandle(value); } } close{pipe}; + reject("\\\\.\\pipe\\switch2kit-host-file-" + std::to_string(::GetCurrentProcessId()), 1024); +#else + reject("/dev/null", 1024); + const auto fifo = root / "fifo"; + require(::mkfifo(fifo.c_str(), 0600) == 0, "could not create FIFO fixture"); + reject(utf8(fifo), 1024); // Must not block waiting for a writer. +#endif + fs::remove_all(root); + std::cout << "Host-file regressions passed\n"; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/tests/switch2kit/run.py b/tests/switch2kit/run.py old mode 100644 new mode 100755 index 6d1da9f721..547bb6371e --- a/tests/switch2kit/run.py +++ b/tests/switch2kit/run.py @@ -11,6 +11,7 @@ import re import shutil import subprocess +import sys import tempfile ROOT = Path(__file__).resolve().parents[2] @@ -54,6 +55,10 @@ def run(): command += ['-fsanitize=address,undefined', '-fno-omit-frame-pointer'] subprocess.run(command, check=True, timeout=120) subprocess.run([str(binary)], check=True, timeout=30) + host_file = [sys.executable, str(ROOT / 'tests/switch2kit/test_host_file.py'), '--sdk', str(sdk)] + if args.sanitize: + host_file.append('--sanitize') + subprocess.run(host_file, check=True) subprocess.run(['python3', str(ROOT / 'tests/switch2kit/test_wiring.py')], check=True) subprocess.run(['python3', str(ROOT / 'tests/switch2kit/test_desktop_lifecycle.py'), '--sdk', str(sdk)], check=True) diff --git a/tests/switch2kit/test_host_file.py b/tests/switch2kit/test_host_file.py new file mode 100644 index 0000000000..6db5167ce8 --- /dev/null +++ b/tests/switch2kit/test_host_file.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Compile and run the SDK's actual host-file reader on the current platform.""" +import argparse +import os +from pathlib import Path +import shutil +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[2] + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--sdk', type=Path, default=ROOT / 'dependencies/Switch2Kit') + parser.add_argument('--sanitize', action='store_true') + args = parser.parse_args() + include = args.sdk.resolve() / 'Integrations/Emulators' + if not (include / 'HostFile.hpp').is_file(): + parser.error('Initialize the Switch2Kit submodule first') + compiler = (shutil.which('cl') if os.name == 'nt' else + os.environ.get('CXX') or shutil.which('clang++') or shutil.which('g++')) + if not compiler: + parser.error('A C++20 compiler is required; on Windows initialize the MSVC environment') + source = ROOT / 'tests/switch2kit/HostFileTests.cpp' + with tempfile.TemporaryDirectory(prefix='cemu-host-file-') as directory: + work = Path(directory) + binary = work / ('host-file.exe' if os.name == 'nt' else 'host-file') + if os.name == 'nt': + command = [compiler, '/nologo', '/std:c++20', '/EHsc', '/W4', '/WX', '/utf-8', + '/I' + str(include), str(source), '/Fe:' + str(binary)] + else: + command = [compiler, '-std=c++20', '-Wall', '-Wextra', '-Werror', + '-I' + str(include), str(source), '-o', str(binary)] + if args.sanitize: + command += ['-fsanitize=address,undefined', '-fno-omit-frame-pointer'] + subprocess.run(command, cwd=work, check=True, timeout=120) + subprocess.run([str(binary)], cwd=work, check=True, timeout=30) + + +if __name__ == '__main__': + main() From 26d6f6b2b5f9d3fd66b4d33f005d28928471a868 Mon Sep 17 00:00:00 2001 From: Johnny D Date: Fri, 18 Sep 2026 21:37:50 -0400 Subject: [PATCH 07/12] Align Cemu with the native runtime repair validated in Switch2Kit PR 75 Advance only dependencies/Switch2Kit to immutable commit 06bc206047e9b5c9d3f07940e9a7ce29a7de366d, the same reviewed SDK selected for the maintained Dolphin integration. Both native Windows toolchains passed real C/SDL and extracted-package qualification at this revision, with packaged Swift runtime, isolated profiles, OS-only PATH and missing-DLL controls. Review the preceding divergent 0d76136 host-file fix before replacing its pin. The selected SDK retains a Win32 UTF-8, regular-file, bounded reader with non-inheritable handles and unchanged outputs on error; it additionally rejects device namespaces and concurrent in-place writes. Cemu's production-header host-file regressions pass against this SDK under Linux Clang ASan/UBSan. Preserve the newly added Cemu MSVC and sanitizer regressions and all other host work. The SDK also includes the complete runtime dependency closure, stale-copy protection and short deployment-copy locking. Fresh native Cemu builds and application/package checks remain required; SDK or local fixture passes are not physical-controller evidence. Merge the SDK with this commit retained/reachable first, or repin and revalidate after a squash/rebase. --- dependencies/Switch2Kit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/Switch2Kit b/dependencies/Switch2Kit index 0d76136e40..06bc206047 160000 --- a/dependencies/Switch2Kit +++ b/dependencies/Switch2Kit @@ -1 +1 @@ -Subproject commit 0d76136e40398f6a25aae1195a849fa1b66b071c +Subproject commit 06bc206047e9b5c9d3f07940e9a7ce29a7de366d From ef6fdfe6390fed7561f845a619b8a80fb8295b33 Mon Sep 17 00:00:00 2001 From: Johnny D Date: Fri, 18 Sep 2026 22:09:07 -0400 Subject: [PATCH 08/12] Qualify extracted Cemu desktop applications and document native controller setup Archive the actual installed/staged application before testing it, then delegate to the pinned SDK's extracted GUI supervisors on Linux and Windows. Check normal GUI startup, local controller/Swift runtime modules, normal quit and relaunch with isolated profiles and no compiler/developer PATH. Preserve JSON diagnostics on failure and upload the exact qualified archive only after success. Linux --version and a pre-archive Windows staging directory are no longer the launch gate. Reject unexpected developer settings from the CI staging directory; seed only private extracted test settings, never erase a real user's configuration. Keep full native builds, Windows host-file regressions, sanitizer policy tests, SDK notices and other checks enabled. Rewrite the initial README/controller guide for macOS, Ubuntu 24.04 x86-64 and experimental Windows x64: correct maintained-fork application artifacts, expiration/authentication, extraction/executable paths, runtime prerequisites, copyable source fallbacks, Find/Sync, physical/player selection, emulated type, mappings, rumble and subsequent launches. Preserve Joy-Con 2 complementary-source instructions, measured-motion calibration, saved identities and custom-mapping backups. Do not claim Cemu implements Dolphin's automatic reconnection, or that automated checks are physical hardware evidence. Validation: all five uploaded blobs match the locally reviewed files. Existing Cemu mapping/identity/session/rollback policy tests and production host-file checks pass with Clang ASan/UBSan against SDK 06bc206; nine wiring and two executable desktop lifecycle/guard tests pass. YAML parses and local documentation links resolve. Fresh native application/GUI CI remains required for this final host revision. SDK pin and unrelated host code are unchanged. --- .github/workflows/switch2kit-linux.yml | 12 ++- .github/workflows/switch2kit-windows.yml | 11 ++- README.md | 70 +++++++------ docs/Switch2Kit.md | 119 +++++++++++++++-------- tests/switch2kit/windows-launch.ps1 | 40 ++------ 5 files changed, 141 insertions(+), 111 deletions(-) diff --git a/.github/workflows/switch2kit-linux.yml b/.github/workflows/switch2kit-linux.yml index f76cd7b3ca..32dfabac67 100644 --- a/.github/workflows/switch2kit-linux.yml +++ b/.github/workflows/switch2kit-linux.yml @@ -23,7 +23,7 @@ jobs: - name: Install native dependencies run: | apt-get update - apt-get install -y --no-install-recommends build-essential cmake ninja-build python3 pkg-config curl zip unzip tar zstd nasm autoconf automake libtool libtool-bin gettext freeglut3-dev libgcrypt20-dev libglm-dev libgtk-3-dev libpulse-dev libsecret-1-dev libsystemd-dev libudev-dev libbluetooth-dev libgl1-mesa-dev libglu1-mesa-dev libx11-xcb-dev libwayland-dev wayland-protocols extra-cmake-modules dbus xvfb xauth + apt-get install -y --no-install-recommends build-essential cmake ninja-build python3 pkg-config curl zip unzip tar zstd nasm autoconf automake libtool libtool-bin gettext freeglut3-dev libgcrypt20-dev libglm-dev libgtk-3-dev libpulse-dev libsecret-1-dev libsystemd-dev libudev-dev libbluetooth-dev libgl1-mesa-dev libglu1-mesa-dev libx11-xcb-dev libwayland-dev wayland-protocols extra-cmake-modules dbus xvfb xauth openbox wmctrl x11-utils git config --global --add safe.directory "$PWD" mkdir -p "$HOME/.cache/vcpkg/archives" echo "VCPKG_BINARY_SOURCES=clear;files,$HOME/.cache/vcpkg/archives,readwrite" >> "$GITHUB_ENV" @@ -53,8 +53,12 @@ jobs: ldd "$app/bin/Cemu_release" | tee linux-dependencies.log ! grep -q 'not found' linux-dependencies.log grep -F "$app" linux-dependencies.log | grep libSwitch2KitC - xvfb-run -a "$app/bin/Cemu_release" --version tar -C "$RUNNER_TEMP" -czf Cemu-Switch2Kit-linux-x86_64.tar.gz Cemu-Switch2Kit-linux-x86_64 + mv "$app" "$RUNNER_TEMP/unavailable-staged-application" + dbus-run-session -- xvfb-run -a python3 dependencies/Switch2Kit/tests/emulator-launch/linux.py cemu \ + "$PWD/Cemu-Switch2Kit-linux-x86_64.tar.gz" --report "$PWD/linux-launch.json" \ + --forbidden-root "$PWD" --forbidden-root "$RUNNER_TEMP/disabled-build-tree" \ + --forbidden-root "$RUNNER_TEMP/unavailable-staged-application" - name: Controller-enabled development build uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: @@ -67,5 +71,7 @@ jobs: uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: name: Cemu-Switch2Kit-linux-diagnostics - path: linux-*.log + path: | + linux-*.log + linux-launch.json retention-days: 7 diff --git a/.github/workflows/switch2kit-windows.yml b/.github/workflows/switch2kit-windows.yml index e254c1e225..39f55bda03 100644 --- a/.github/workflows/switch2kit-windows.yml +++ b/.github/workflows/switch2kit-windows.yml @@ -40,11 +40,16 @@ jobs: robocopy 'bin' $stage /E /NFL /NDL /NJH /NJS if ($LASTEXITCODE -ge 8) { throw 'Application staging failed.' } $global:LASTEXITCODE = 0 - New-Item -ItemType Directory -Path "$stage/user" -Force | Out-Null + # Never distribute a developer/test profile. The supervisor seeds only + # its private extracted copy, after the distributable archive is made. + foreach ($profile in @('User', 'user', 'portable', 'portable.txt', 'settings.xml')) { + if (Test-Path (Join-Path $stage $profile)) { throw "Unexpected profile in application staging: $profile" } + } Move-Item build-switch2kit-windows "$env:RUNNER_TEMP/disabled-build-tree" - & ./tests/switch2kit/windows-launch.ps1 -Executable "$stage/Cemu_release.exe" - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } Compress-Archive -Path $stage -DestinationPath 'Cemu-Switch2Kit-windows-x86_64.zip' + Move-Item $stage "$env:RUNNER_TEMP/unavailable-staged-application" + & ./tests/switch2kit/windows-launch.ps1 -Archive 'Cemu-Switch2Kit-windows-x86_64.zip' -ForbiddenRoot "$PWD" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Controller-enabled development application uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: diff --git a/README.md b/README.md index ee4b87facb..2d0898ce00 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,60 @@ -# **Cemu - Wii U emulator** +# Cemu - Wii U emulator -**This fork supports the Nintendo Switch Online GameCube controller and Nintendo Switch 2 Pro Controller on macOS through [Switch2Kit](https://github.com/jmonster/Switch2Kit).** +**This fork embeds [Switch2Kit](https://github.com/jmonster/Switch2Kit) for NSO GameCube and Nintendo Switch 2 Pro controllers on macOS 15+, with experimental Linux x86-64 and Windows x64 builds.** -[![Native Switch2Kit builds](https://github.com/jmonster/Cemu/actions/workflows/native-switch2kit.yml/badge.svg)](https://github.com/jmonster/Cemu/actions/workflows/native-switch2kit.yml) +Use this controller-enabled Cemu directly: connect the controller, choose a player slot and emulated controller, and play. No separate dashboard, network bridge, SDL override or virtual-controller driver is needed. Joy-Con 2 halves remain available as separate, complementary input sources. -Controller support is built into Cemu. There is no separate Switch2Kit app or controller driver to install, and GameCube/Pro controller setup includes recommended mappings and rumble. +## Quick start -## Quick start (macOS 15+) +Sign in to GitHub and select a successful application run for `feature/switch2kit-desktop-platforms` while this PR is unmerged. Choose the named **application** artifact below and check its native build/launch jobs, not a source or diagnostics archive. These are expiring development builds, not published production releases. Use [Build from source](#build-from-source-alternative) when a matching application artifact is unavailable. Ordinary upstream downloads do not include this integration. -### Get a controller-enabled build +### macOS -1. Sign in to GitHub, open this fork's [Native Switch2Kit builds](https://github.com/jmonster/Cemu/actions/workflows/native-switch2kit.yml), and select a successful run with a green check. Use **Native Switch2Kit**, not the ordinary **Build check** workflow. -2. Under **Artifacts**, download **Cemu-Switch2Kit-arm64** for an Apple Silicon Mac or **Cemu-Switch2Kit-x86_64** for an Intel Mac. Choose the application artifact, not a diagnostics artifact. -3. Extract the downloaded ZIP, open the **build-switch2kit** folder, and extract **integration-app.zip**. Move **Cemu_release.app** to Applications and open it. Reopen this same app for later sessions. +Use macOS 15 or newer and [Native Switch2Kit builds](https://github.com/jmonster/Cemu/actions/workflows/native-switch2kit.yml). Download **Cemu-Switch2Kit-arm64** for Apple Silicon or **Cemu-Switch2Kit-x86_64** for Intel. Extract the outer ZIP, then `build-switch2kit/integration-app.zip` inside it. Move `Cemu_release.app` to Applications and open it. Enable Bluetooth and allow Cemu's Bluetooth request; denied access is managed under **System Settings > Privacy & Security > Bluetooth**. -Downloads currently come from GitHub Actions, not a published release. Application artifacts expire after 14 days; when no application download is available, use [Build from source](#build-from-source-alternative) below. Ordinary upstream Cemu downloads do not include this Switch2Kit integration. +These apps are ad-hoc signed, not notarized. For a source you trust, use Apple's [per-app Open Anyway procedure](https://support.apple.com/en-us/102445); do not disable Gatekeeper globally. -These are development builds, not notarized releases. For an unverified-developer warning, use Apple's [per-app Open Anyway instructions](https://support.apple.com/en-us/102445) only when you trust the download's source. Do not disable Gatekeeper globally. +### Linux -### Connect and play +Use Ubuntu 24.04 x86-64 with a desktop session, graphics drivers, normal system-bus permissions, a powered Bluetooth LE adapter and the BlueZ service. See the [Linux prerequisites](docs/Switch2Kit.md#linux). From [Switch2Kit Linux application builds](https://github.com/jmonster/Cemu/actions/workflows/switch2kit-linux.yml), download **Cemu-Switch2Kit-linux-x86_64**. Extract the outer artifact ZIP and then: -1. Turn on your Mac's Bluetooth and close other apps managing the controller, including the Switch2Kit dashboard or Dolphin. In Cemu, open **Options > Input settings**, click **Find Switch 2 Controllers**, allow Bluetooth access, and hold the controller's **Sync** button until its player lights sweep. The search lasts 60 seconds; click Find again to retry. -2. On the desired controller tab, select your **GameCube** or **Pro Controller 2** in the dropdown beside **Emulated controller**. Cemu applies the button and stick mappings automatically. An empty first slot becomes a **Wii U GamePad**; other empty slots become **Wii U Pro Controllers**. Use the emulated controller type your game supports. -3. Check that the input display responds to button presses, releases, and stick movement. Open the physical controller's **Settings** to adjust **Rumble** and click **Test rumble**. Close Input settings and open your Wii U game. +```sh +tar -xzf Cemu-Switch2Kit-linux-x86_64.tar.gz +./Cemu-Switch2Kit-linux-x86_64/bin/Cemu_release +``` -Cemu saves assignments and mappings. On later launches, use **Find Switch 2 Controllers** again to connect; **Disconnect Switch 2 Controllers** stops the current session without erasing profiles. Replacing a populated slot asks for confirmation and saves a backup; reconnecting does not reset custom mappings. +Keep `bin`, `lib` and `share` together. The Swift runtime is packaged; no Swift installation or loader-path override is needed for a correctly staged application. Compatible system libraries and drivers remain required. This is not an AppImage or universal Linux binary. -The NSO GameCube controller has no stick-click buttons, so bind those actions to spare buttons or a keyboard when a game needs them. A controller does not replace the Wii U touchscreen. Motion requires a measured, device-matching `.s2kmotion` profile and **Use motion** in the physical controller's Settings; it is not automatically calibrated and is not required for ordinary button/stick input. See the [full controller guide](docs/Switch2Kit.md) for these details and for adding both Joy-Con 2 halves through **+ > SDLController**. +### Windows -**No Find button?** Open the controller-enabled app above, not an upstream or backend-disabled build. **No controller?** Check Bluetooth access for Cemu in **System Settings > Privacy & Security > Bluetooth**, close competing controller apps, and retry Find while holding Sync. +Use Windows 11 x64 for these experimental instructions, with a Bluetooth LE driver, graphics drivers and the Microsoft Visual C++ x64 runtime. Native CI uses Windows Server runners, not physical Windows 11 controllers. From [Switch2Kit Windows application builds](https://github.com/jmonster/Cemu/actions/workflows/switch2kit-windows.yml), download **Cemu-Switch2Kit-windows-x86_64**. Extract the outer artifact ZIP and then `Cemu-Switch2Kit-windows-x86_64.zip`. Open `Cemu-Switch2Kit-windows-x86_64/Cemu_release.exe`. -### Build from source (alternative) +Keep its DLLs, `resources`, `gameProfiles` and `Switch2KitNotices` beside it. The selected Swift runtime is included; launching does not require the Swift compiler or its PATH. Run normally, not as administrator, and do not disable operating-system security to bypass errors. See the [Windows source fallback](docs/Switch2Kit.md#windows). -
-Build and launch the controller-enabled app on your Mac +### Connect and play -Use macOS 15+, [Xcode](https://developer.apple.com/xcode/) 26+ with Swift 6.2+, and [Homebrew](https://brew.sh/). Open Xcode once to finish setup and select it under **Xcode > Settings > Locations > Command Line Tools**. On Apple Silicon, use a native Terminal and native Homebrew, not Rosetta. +1. Close competing controller apps/consoles. In Cemu, open **Options > Input settings**, click **Find Switch 2 Controllers**, and hold the controller's **Sync** button until its player lights sweep. Allow legitimate Bluetooth access prompts. The search lasts 60 seconds; repeat Find to retry. +2. Select the desired controller tab (for example, **Controller 1**). In the physical-controller dropdown beside **Emulated controller**, select the connected **GameCube** or **Pro Controller 2**. Cemu applies recommended mappings. An empty first slot becomes a Wii U GamePad, other empty slots become Wii U Pro Controllers; existing GamePad/Pro/Classic types are retained. Choose a type the game accepts. +3. Verify buttons, sticks and triggers in Input settings. Open the physical controller's **Settings** to adjust **Rumble** and use **Test rumble**, then open the game. NSO GameCube trigger travel and full clicks remain independent inputs; Pro triggers are digital. GameCube sticks have no click buttons, so games needing those actions require additional bindings. -Run these commands in Terminal: +Saved assignments follow the physical controller rather than its discovery order. Reopen the same application and use **Find** again on subsequent launches; Cemu does not implement Dolphin's automatic-reconnect option. **Disconnect Switch 2 Controllers** stops the backend without erasing saved profiles. Replacing a populated slot asks first and creates a **Before Switch2Kit-…** backup; reconnecting does not overwrite custom mappings. -```sh -brew install cmake ninja nasm automake libtool molten-vk -git clone --recurse-submodules https://github.com/jmonster/Cemu.git cemu-switch2kit -cd cemu-switch2kit -bash scripts/build-switch2kit.sh --run -``` +For Joy-Con 2, discover each half with Find/Sync, choose the emulated controller type, and add both via **+ > SDLController** to the same player slot. These are separate complementary sources, not a system-wide paired virtual controller. Motion requires a measured, device-matching `.s2kmotion` profile selected in physical-controller Settings and **Use motion** enabled. Never use synthetic test profiles for gameplay. See [controller differences and motion](docs/Switch2Kit.md#controller-differences-and-motion). -The helper builds this fork and its pinned dependencies, enables Switch2Kit, and opens **bin/Cemu_release.app**. No separate SDK checkout or patching is needed. Once it opens, follow [Connect and play](#connect-and-play). +### Build from source (alternative) -For later launches, reopen that app. To update the source build, quit Cemu, run `git pull --ff-only` from this checkout, and rerun `bash scripts/build-switch2kit.sh --run`. The helper updates the pinned submodules without replacing your settings. The ordinary upstream build instructions below do not enable Switch2Kit by default. +Follow the [platform build guide](docs/Switch2Kit.md#build-from-source). Start from this implementation branch while the PR is unmerged: -
+```sh +git clone --branch feature/switch2kit-desktop-platforms --recurse-submodules https://github.com/jmonster/Cemu.git cemu-switch2kit +cd cemu-switch2kit +``` -See the [full Switch2Kit controller guide](docs/Switch2Kit.md) for custom mappings, profile backups, multiplayer, motion calibration, and testing limits. Automated build/launch checks do not establish physical-controller or gameplay acceptance. Switch2Kit support in this fork is macOS-only; the SDK's experimental Linux work is separate. +The helpers enable Switch2Kit and use the pinned submodule; do not apply the SDK's separate upstream patches. The ordinary upstream instructions below do not enable it by default. Linux/Windows support remains experimental, and native build/launch tests do not establish physical-controller or gameplay acceptance. ## Upstream Cemu information -The information below describes Cemu generally, including builds without this fork's Switch2Kit feature. Controller-enabled builds have the macOS 15+ requirements above. +The information below describes Cemu generally, including builds without this fork's Switch2Kit feature. Controller-enabled builds use the platform requirements above. [![Upstream Build Process](https://github.com/cemu-project/Cemu/actions/workflows/build.yml/badge.svg)](https://github.com/cemu-project/Cemu/actions/workflows/build.yml) [![Discord](https://img.shields.io/discord/286429969104764928?label=Cemu&logo=discord&logoColor=FFFFFF)](https://discord.gg/5psYsup) @@ -82,7 +80,7 @@ Cemu is currently only available for 64-bit Windows, Linux & macOS devices. ## Upstream downloads (without this integration) -For this fork's Switch2Kit support, use [Get a controller-enabled build](#get-a-controller-enabled-build) above instead. +For this fork's Switch2Kit support, use [Quick start](#quick-start) above instead. You can download the latest upstream Cemu releases for Windows, Linux and Mac from the [upstream GitHub Releases](https://github.com/cemu-project/Cemu/releases/). For Linux you can also find upstream Cemu on [Flathub](https://flathub.org/apps/info.cemu.Cemu). @@ -94,7 +92,7 @@ Pre-2.0 releases can be found on Cemu's [changelog page](https://cemu.info/chang ## Build Instructions -For a controller-enabled macOS build, use [Build from source](#build-from-source-alternative) above. For ordinary upstream-style builds on Windows, Linux or macOS, view [BUILD.md](/BUILD.md). +For a controller-enabled desktop build, use [Build from source](#build-from-source-alternative) above. For ordinary upstream-style builds on Windows, Linux or macOS, view [BUILD.md](/BUILD.md). ## Issues diff --git a/docs/Switch2Kit.md b/docs/Switch2Kit.md index 9c2b536cab..fb4cee3c4f 100644 --- a/docs/Switch2Kit.md +++ b/docs/Switch2Kit.md @@ -1,30 +1,46 @@ -# Switch 2 controllers on macOS +# Native Switch 2 controllers in Cemu -This build connects Switch 2 controllers directly inside Cemu. No standalone -Switch2Kit app, SDL override, virtual-HID driver or Accessibility permission is -needed. Bluetooth input, motor rumble and calibrated motion use the pinned -Switch2Kit library included as a submodule. +**This maintained Cemu fork embeds [Switch2Kit](https://github.com/jmonster/Switch2Kit) for NSO GameCube and Nintendo Switch 2 Pro controllers on macOS 15+, with experimental Linux x86-64 and Windows x64 support.** -## Build and open +Use the [controller-enabled downloads](../README.md#quick-start), launch that Cemu and [select your controller/player slot](#connect-and-play). Ordinary upstream downloads do not embed this backend. No separate dashboard, SDL override, network bridge, virtual-controller driver or Accessibility permission is required. Joy-Con 2 halves remain separate complementary sources; motion requires explicit measured calibration. -Use macOS 15 or newer and Xcode 26 or newer with Swift 6.2. Open Xcode once to -finish setup, and select it under Settings > Locations > Command Line Tools. -Install the build tools once: +## Applications and prerequisites + +Sign in to GitHub and select a successful run for `feature/switch2kit-desktop-platforms` while this PR is unmerged. Download the application artifact, not a source or diagnostics archive. The outer GitHub artifact ZIP contains the application ZIP/tarball. Desktop application artifacts expire after 14 days; use the source fallback when no matching successful artifact remains. These are development builds, not published production releases. A workflow configuration or a different revision's result is not qualification of the selected download. + +### macOS + +Use macOS 15 or newer on Apple Silicon (`arm64`) or Intel (`x86_64`). From [Native Switch2Kit](https://github.com/jmonster/Cemu/actions/workflows/native-switch2kit.yml), download **Cemu-Switch2Kit-arm64** or **Cemu-Switch2Kit-x86_64**, extract the outer and inner ZIPs, move `Cemu_release.app` to Applications and open it. The controller library and runtime are embedded. Enable Bluetooth and permit Cemu under **System Settings > Privacy & Security > Bluetooth** when requested. + +The application is ad-hoc signed, not notarized. Use Apple's [per-app approval procedure](https://support.apple.com/en-us/102445) only for an application you trust; do not disable Gatekeeper globally. macOS emulator performance/support limitations are separate from controller input support. + +### Linux + +The application target is Ubuntu 24.04 x86-64 with a graphical desktop and working OpenGL/Vulkan/audio drivers. This is an installed prefix, not a universal Linux package. Install the distribution's normal runtime libraries; on Ubuntu 24.04: ```sh -brew install cmake ninja nasm automake libtool molten-vk +sudo apt-get update +sudo apt-get install bluez libsystemd0 libgtk-3-0t64 libpulse0 libsecret-1-0 libgcrypt20 libudev1 libgl1 libegl1 libvulkan1 libx11-xcb1 ``` -In your Cemu checkout: +Get **Cemu-Switch2Kit-linux-x86_64** from [the Linux application workflow](https://github.com/jmonster/Cemu/actions/workflows/switch2kit-linux.yml). Extract the outer ZIP, then: ```sh -bash scripts/build-switch2kit.sh --run +tar -xzf Cemu-Switch2Kit-linux-x86_64.tar.gz +./Cemu-Switch2Kit-linux-x86_64/bin/Cemu_release ``` -The script builds `bin/Cemu_release.app` and opens it. Open that app again normally -for later sessions. To update, quit Cemu, run `git pull --ff-only`, and run the -script again. The script updates submodules to the versions selected by Cemu. -It does not replace existing settings or install a system driver. +Keep the whole prefix, including `lib`, `share/Cemu` and `share/Switch2KitNotices`. The selected Swift runtime is packaged; no Swift installation or `LD_LIBRARY_PATH` override is needed to launch it. System desktop libraries and drivers remain prerequisites. + +Turn Bluetooth on in your desktop settings. BlueZ must be running, the adapter powered and LE-capable, and your normal account allowed system-bus/GATT access. `bluetoothctl show` can inspect the adapter. The integration does not power it on, change D-Bus permissions, erase bonds or call BlueZ Pair/RemoveDevice. Use Cemu's Find/Sync procedure; the controller handshake is distinct from operating-system pairing. Resolve denied access through normal distribution Bluetooth policy instead of running Cemu as root. + +### Windows + +The experimental desktop target is Windows 11 x64 with a working Bluetooth LE adapter/driver and graphics drivers. Native CI uses Windows Server runners, not physical Windows 11 controllers. ARM64 and 32-bit Windows are not covered. Install the [Microsoft Visual C++ x64 runtime](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist) if required. + +Get **Cemu-Switch2Kit-windows-x86_64** from [the Windows application workflow](https://github.com/jmonster/Cemu/actions/workflows/switch2kit-windows.yml). Extract the outer ZIP and its inner `Cemu-Switch2Kit-windows-x86_64.zip`, then open `Cemu-Switch2Kit-windows-x86_64/Cemu_release.exe`. Keep DLLs, `resources`, `gameProfiles` and `Switch2KitNotices` together. The package includes the Swift runtime; installing Swift or adding compiler directories to PATH is not a launch requirement. + +Enable **Settings > Bluetooth & devices > Bluetooth** and use Cemu's Find/Sync procedure. Honor legitimate system pairing/access prompts; the backend does not erase bonds or bypass pairing policy. Run as a normal user, not administrator, and do not disable SmartScreen/antivirus. A missing DLL before the GUI opens is a packaging/runtime prerequisite failure: re-extract the complete controller-enabled artifact and check the native launch result for its revision. ## Connect and play @@ -52,7 +68,7 @@ Assignments persist by physical identity, including when identical controllers reconnect in a different order. Use **Find** again after restarting Cemu. Use **Disconnect Switch 2 Controllers** to stop this backend without erasing profiles. -## Controller differences +## Controller differences and motion GameCube and Pro mappings preserve printed Nintendo A/B/X/Y labels. On GameCube, C supplies Minus and Capture supplies the GamePad microphone button. Its physical @@ -83,27 +99,52 @@ is an explicit action and the path is saved with the controller configuration. Do not use test-fixture profiles for gameplay. Calibration must match the real device and its sensor setup; the SDK documents the measurement procedure in -`dependencies/Switch2Kit/docs/switch2kit/motion-profiles.md`. Without valid calibration the +[the pinned SDK motion-profile guide](../dependencies/Switch2Kit/docs/switch2kit/motion-profiles.md). Without valid calibration the backend does not invent sensor values or integrate across a disconnect or gap. -## Build scope and checks - -`ENABLE_SWITCH2KIT` remains off by default for ordinary cross-platform builds. -Enabled builds require SDL, a macOS application bundle and deployment target 15.0 -or newer. The normal disabled build keeps the upstream deployment requirements -and does not compile or link Switch2Kit. The build helper enables these options, -embeds the SDK and its distribution notices, then ad-hoc signs the completed app. -These are development builds, not notarized releases. - -`python3 tests/switch2kit/run.py --sanitize` runs mapping, identity, session and -backup/rollback policies after the native build. Use `--sdl /path/to/SDL-source` -and `--sdk /path/to/Switch2Kit` for a separate source checkout. Policy tests use -real enum definitions with controlled host/storage boundaries; they do not claim -Bluetooth or GUI interaction. Native CI compiles the full Cemu app on Apple -silicon and Intel, checks its bundle/signature, and launches, normally quits and -relaunches the exact ZIP with build dependencies denied. The SDK job also tests -real SDL, motor packets and the Cemu motion consumer. - -Physical pairing, input, reconnect, multiplayer, rumble start/stop, measured -motion and gameplay still require a real Mac and controller. There is no claim -that CI tests exercise physical hardware or downloaded-app Gatekeeper approval. +## Build from source + +`ENABLE_SWITCH2KIT` is OFF by default. Disabled builds do not require Swift and retain upstream platform/deployment requirements. Enabled Linux/Windows builds use native SDL3 and the desktop backend; only enabled macOS builds require a macOS 15+ app bundle. Initialize the SDK revision selected by this maintained fork, not a moving SDK branch or the SDK's separate upstream patches. While the PR is unmerged: + +```sh +git clone --branch feature/switch2kit-desktop-platforms --recurse-submodules https://github.com/jmonster/Cemu.git cemu-switch2kit +cd cemu-switch2kit +``` + +**macOS:** install Xcode 26+ with Swift 6.2+, finish its first-launch setup and select its Command Line Tools. Then: + +```sh +brew install cmake ninja nasm automake libtool molten-vk +bash scripts/build-switch2kit.sh --run +``` + +The helper builds and opens `bin/Cemu_release.app` with the native architecture. The finished app has its controller/runtime dependencies and notices embedded before ad-hoc signing. + +**Linux:** install Swift **6.2.1** from [the official Linux instructions](https://www.swift.org/install/linux/), then the native dependencies used by the Ubuntu workflow: + +```sh +sudo apt-get install build-essential cmake ninja-build python3 pkg-config curl zip unzip tar zstd nasm autoconf automake libtool libtool-bin gettext freeglut3-dev libgcrypt20-dev libglm-dev libgtk-3-dev libpulse-dev libsecret-1-dev libsystemd-dev libudev-dev libbluetooth-dev libgl1-mesa-dev libglu1-mesa-dev libx11-xcb-dev libwayland-dev wayland-protocols extra-cmake-modules dbus +bash scripts/build-switch2kit.sh --run +``` + +The helper builds through the recorded vcpkg dependencies, installs to `build-switch2kit/install` and opens `build-switch2kit/install/bin/Cemu_release`. Keep the entire install prefix. CI additionally installs Xvfb/Openbox/wmctrl for isolated graphical testing; an ordinary desktop does not need those test tools. + +**Windows:** use native x64 Swift **6.2.1**, Visual Studio **2022 Desktop development with C++** and its Windows SDK, CMake, Ninja, Git, Python 3 and 64-bit PowerShell 7. Follow [Swift's Windows installation guide](https://www.swift.org/install/windows/) for its toolchain. This combination matches Cemu's Windows job; do not combine Swift 6.2's bundled compiler with VS 2026 STL headers. The helper currently selects the latest installed Visual C++ instance, so use a build machine where that instance is the compatible VS 2022 toolchain. + +```powershell +./scripts/build-switch2kit.ps1 -Run +``` + +It bootstraps the pinned vcpkg checkout, builds `CemuBin` with Switch2Kit enabled, stages runtime dependencies and opens `bin/Cemu_release.exe`. Build-time PATH changes stay process-local; extracted packages are tested without the compiler PATH. + +For updates, quit Cemu, run `git pull --ff-only`, update the recorded submodules and rerun the same helper. Do not delete your settings, profiles or game data to make a new build launch. + +## Qualification and troubleshooting + +A missing **Find Switch 2 Controllers** button means a backend-disabled binary was launched. For absent input, verify Bluetooth power/access, Sync mode, competing connections, physical selection and the emulated type accepted by the game, then retry Find. A controller already assigned to another slot must be removed there before the quick setup shortcut can move it. Cemu requires Find again after restarting; it does not implement Dolphin's automatic-reconnection option. A changed adapter or rotating device address can change physical identity, so verify player assignments after such a change. + +Desktop CI builds the complete application, archives it, extracts that exact archive into a new location, checks that the GUI loads its packaged controller/Swift libraries, requests normal quit and relaunches with a private profile. It deliberately seeds noninteractive test settings; pristine first-use dialogs, downloaded-app security approval and physical hardware are not tested. No existing user configuration is erased. The artifact is qualified only after these jobs pass for its exact revision. + +`python3 tests/switch2kit/run.py --sanitize` retains executable mapping, identity, lifecycle and rollback regressions against controlled host/storage boundaries. `python3 tests/switch2kit/test_host_file.py --sdk dependencies/Switch2Kit --sanitize` exercises the real bounded file reader; native MSVC coverage is retained. SDK tests cover protocol values, real C/SDL consumers, calibrated sample handling, rumble bounds, cancellation, runtime relocation and required notices. Source-contract checks supplement those tests, not prose length or English wording restrictions. + +Record separate hardware acceptance for each model/firmware/OS/adapter and tested commit: first pairing and denied-access retry; all controls and releases; independent GameCube trigger travel/clicks; rumble start/stop; two identical controllers reconnecting in reversed order; persisted player assignments after restart; Bluetooth/adapter loss; explicit disconnect; normal shutdown; measured motion where used; and an actual gameplay session. Joy-Con 2 acceptance must include both complementary sources in one slot. No fixture profile or automated pass substitutes for those physical results. diff --git a/tests/switch2kit/windows-launch.ps1 b/tests/switch2kit/windows-launch.ps1 index e7f25b3549..31692418ea 100644 --- a/tests/switch2kit/windows-launch.ps1 +++ b/tests/switch2kit/windows-launch.ps1 @@ -1,32 +1,12 @@ -param([Parameter(Mandatory=$true)][string]$Executable) +param([Parameter(Mandatory=$true)][string]$Archive, + [string]$Report = 'windows-launch.json', + [string]$ForbiddenRoot = '') $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest -$exe = (Resolve-Path $Executable).Path -$directory = Split-Path $exe -$expected = (Resolve-Path (Join-Path $directory 'Switch2KitC.dll')).Path -$report = @() -for ($attempt = 1; $attempt -le 2; $attempt++) { - $process = Start-Process -FilePath $exe -WorkingDirectory $directory -PassThru - try { - $deadline = [DateTime]::UtcNow.AddSeconds(60) - do { - Start-Sleep -Milliseconds 250 - $process.Refresh() - if ($process.HasExited) { throw "Application exited before opening a window: $($process.ExitCode)" } - } until ($process.MainWindowHandle -ne 0 -or [DateTime]::UtcNow -gt $deadline) - if ($process.MainWindowHandle -eq 0) { throw 'No application window appeared within 60 seconds.' } - $loaded = @($process.Modules | Where-Object { $_.ModuleName -eq 'Switch2KitC.dll' }) - if ($loaded.Count -ne 1 -or $loaded[0].FileName -ne $expected) { - throw 'The running application did not load its own Switch2Kit DLL.' - } - if (-not $process.CloseMainWindow()) { throw 'The application rejected a normal close request.' } - if (-not $process.WaitForExit(20000)) { throw 'The application did not shut down normally.' } - if ($process.ExitCode -ne 0) { throw "Application failed during shutdown: $($process.ExitCode)" } - $report += @{ attempt=$attempt; visibleWindow=$true; localControllerDLL=$true; exitCode=$process.ExitCode } - } finally { - if (-not $process.HasExited) { $process.Kill(); $process.WaitForExit() } - $process.Dispose() - } -} -$report | ConvertTo-Json | Set-Content windows-launch.json -Write-Host 'PASS relocated application launch, local DLL loading, normal quit and relaunch (no physical controller).' +$root = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path +$qualifier = Join-Path $root 'dependencies/Switch2Kit/tests/emulator-launch/windows.ps1' +if (-not (Test-Path $qualifier)) { throw 'Initialize the pinned Switch2Kit submodule before qualification.' } +# The SDK supervisor extracts the exact archive into a new path with spaces, +# launches with an isolated profile and OS-only PATH, inspects loaded DLLs, +# then normally closes and relaunches the real GUI. A failure is not a pass. +& $qualifier -Emulator cemu -Archive $Archive -Report $Report -ForbiddenRoot $ForbiddenRoot From 3b87d27753467d90c6621a86c24f6633ea734f05 Mon Sep 17 00:00:00 2001 From: Johnny D Date: Fri, 18 Sep 2026 22:46:52 -0400 Subject: [PATCH 09/12] Pin the bounded X11 readiness repair for extracted Cemu qualification The ef6fdfe Linux job built the complete controller-enabled application and passed controller policies, but the exact extracted GUI supervisor raced Openbox: wmctrl -m had succeeded before the client-list property existed, so the first wmctrl -lp failed before a window could be observed. Select immutable Switch2Kit 5198ca5a5fb9e39657832951d8751ad1e9472a4c. Its supervisor waits for both identity and the possibly empty client list within the existing five-second bound, without swallowing later observer/runtime/GUI/shutdown errors. Seven portable readiness tests and a real isolated Xvfb/Openbox/Xlib reproduction pass. The native observer fixture is separate from this complete application and cannot satisfy its launch gate. The SDK's product sources, Windows transport/runtime repair and controller policies are unchanged from the previously verified 06bc206 revision. The selected SDK adds only qualification code, tests and their Linux workflow. Preserve all host code, current documentation, extracted-archive gates and unrelated dependencies. Fresh final-pin native application validation remains required; do not rerun the old failing code or relabel --version as GUI acceptance. --- dependencies/Switch2Kit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/Switch2Kit b/dependencies/Switch2Kit index 06bc206047..5198ca5a5f 160000 --- a/dependencies/Switch2Kit +++ b/dependencies/Switch2Kit @@ -1 +1 @@ -Subproject commit 06bc206047e9b5c9d3f07940e9a7ce29a7de366d +Subproject commit 5198ca5a5fb9e39657832951d8751ad1e9472a4c From 8069a9266f2a51b6d8ab6c2b2ec0ad80d7b34d21 Mon Sep 17 00:00:00 2001 From: Johnny D Date: Sat, 19 Sep 2026 11:16:43 -0400 Subject: [PATCH 10/12] Match the static SDL adapter to Cemu's CRT and pin the Windows profile repair The hash-verified Windows diagnostic archive for 3b87d277 shows LNK2038: Switch2KitSDL3 was compiled with MD_DynamicRelease while Cemu uses MT_StaticRelease, followed by unresolved __imp_llround. Copy CemuInput's MSVC_RUNTIME_LIBRARY target property to the statically linked adapter for all configurations. Do not alter the Swift DLL runtime or disable the linker's ABI checks. Pin immutable Switch2Kit 8a6ff6f7ed0763849e53a6bcf47c38cdd6e22ad7, which repairs the shared Windows package supervisor's read-only HOME collision and adds native preparation/failure regressions. The full application must still pass extracted relocation, runtime-origin, normal shutdown and relaunch checks. Local CMake configuration probes reproduce the differing Debug/Release CRT properties before the change and matching static properties afterward. The disabled configuration adds no adapter or Swift invocation. Real native Windows linking and final-pin Linux/macOS validation remain required in CI. --- dependencies/Switch2Kit | 2 +- src/input/CMakeLists.txt | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/dependencies/Switch2Kit b/dependencies/Switch2Kit index 5198ca5a5f..8a6ff6f7ed 160000 --- a/dependencies/Switch2Kit +++ b/dependencies/Switch2Kit @@ -1 +1 @@ -Subproject commit 5198ca5a5fb9e39657832951d8751ad1e9472a4c +Subproject commit 8a6ff6f7ed0763849e53a6bcf47c38cdd6e22ad7 diff --git a/src/input/CMakeLists.txt b/src/input/CMakeLists.txt index a5f16b561a..ccb2f484e8 100644 --- a/src/input/CMakeLists.txt +++ b/src/input/CMakeLists.txt @@ -110,5 +110,11 @@ if (ENABLE_BLUEZ) endif () if(ENABLE_SWITCH2KIT) + # This static adapter is linked into Cemu, not into the Swift DLL. Match + # CemuInput's CRT in every configuration; leave the DLL's runtime untouched. + if(MSVC) + get_target_property(_cemu_input_crt CemuInput MSVC_RUNTIME_LIBRARY) + set_property(TARGET Switch2KitSDL3 PROPERTY MSVC_RUNTIME_LIBRARY "${_cemu_input_crt}") + endif() target_link_libraries(CemuInput PRIVATE Switch2Kit::SDL3) endif() From 9a8a563c93791634bc2a5288fde885a25fbcddd4 Mon Sep 17 00:00:00 2001 From: Johnny D Date: Sat, 19 Sep 2026 11:39:27 -0400 Subject: [PATCH 11/12] Align the SDK pin with the reviewed Windows rediscovery repair Advance only dependencies/Switch2Kit to immutable 3d3ce3a605733c47db061af687168ad5914cbf0c, matching maintained Dolphin. The SDK removes a retired connection's scan-admission entry so fresh controller advertisements are not permanently suppressed during an existing discovery session. Stable identity, stale-token fences, bounded scanning, retry/consent and explicit stop remain intact. The added adaptation regressions fail on the old implementation and pass with the repair. They neither stand in for native Windows/WinRT execution nor claim physical-controller evidence. Revalidate the complete native applications and exact extracted GUI/quit/relaunch packages on this pin. Preserve 8069a926's static SDL adapter CRT alignment, all lifecycle/mapping/calibration/rollback tests and existing platform user guides. Cemu still does not adopt Dolphin's opt-in automatic startup/reconnection policy. No merge or release is performed. --- dependencies/Switch2Kit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/Switch2Kit b/dependencies/Switch2Kit index 8a6ff6f7ed..3d3ce3a605 160000 --- a/dependencies/Switch2Kit +++ b/dependencies/Switch2Kit @@ -1 +1 @@ -Subproject commit 8a6ff6f7ed0763849e53a6bcf47c38cdd6e22ad7 +Subproject commit 3d3ce3a605733c47db061af687168ad5914cbf0c From e3b579269caa3122cdab3b0dbf8e0c79da882049 Mon Sep 17 00:00:00 2001 From: Johnny D Date: Sat, 19 Sep 2026 11:53:26 -0400 Subject: [PATCH 12/12] Test CMake outcomes rather than diagnostic prose and retain the CRT regression Remove assertions that freeze English error-message fragments in the platform admission tests. Keep the actual CMake exit status and SDK-evaluation checks, with explicit expected success/admission booleans. Add Windows enabled, missing-SDL and disabled-without-SDK cases; macOS deployment, Linux dependency and unsupported-platform guards remain. Add an executable configuration regression that includes the complete production src/input/CMakeLists.txt and evaluates the host and static adapter CRT properties in Debug and Release. Enabled targets must agree on the static CRT; disabled input configuration must not acquire the adapter. The real Windows workflow remains responsible for full native compile/link and extracted GUI qualification. Local evidence: the new CRT regression fails twice against the original MD/MT-mismatched input target and passes after the production fix. Merely rewording the missing-SDK diagnostic causes the old test to fail but the revised outcome-based test to pass, with identical guard behavior. All three updated test methods pass. Temporarily varied source files were restored byte-for-byte before publication. Local new test blob: 76d6fbbb7c869d4a00f9fdbefea2ed80ceac7bfe. The SDK remains the deliberately selected immutable 3d3ce3a605733c47db061af687168ad5914cbf0c, matching Dolphin. Subsequent SDK commit 13abafeb changes documentation links only. No production behavior, permission, runtime-dependency check, required notice or complete application CI gate is relaxed. --- tests/switch2kit/test_desktop_lifecycle.py | 79 ++++++++++++++++++---- 1 file changed, 64 insertions(+), 15 deletions(-) diff --git a/tests/switch2kit/test_desktop_lifecycle.py b/tests/switch2kit/test_desktop_lifecycle.py index 495f87e244..76d6fbbb7c 100644 --- a/tests/switch2kit/test_desktop_lifecycle.py +++ b/tests/switch2kit/test_desktop_lifecycle.py @@ -81,30 +81,81 @@ def preprocess(path): self.assertNotIn('SDLControllerProvider::ShutdownSDL();', app) self.assertIn('SDL_WaitEvent(&event)', provider) + def test_static_adapter_uses_the_hosts_crt_in_both_build_configurations(self): + # Include the complete production input CMake file, not a copied setter. + # These configure-only targets record CRT choices; the native Windows + # application workflow separately compiles and links the real binaries. + for enabled in (False, True): + for configuration in ('Debug', 'Release'): + with self.subTest(enabled=enabled, configuration=configuration): + with tempfile.TemporaryDirectory(prefix='cemu-crt-policy-') as directory: + root = Path(directory) + (root / 'fixture.cpp').write_text('int adapter_fixture;\n') + source = '''cmake_minimum_required(VERSION 3.24) +project(CemuCRTPolicy LANGUAGES CXX) +set(MSVC TRUE) +set(ENABLE_SDL ON) +function(cemu_use_precompiled_header) +endfunction() +add_library(CemuCommon INTERFACE) +add_library(CemuGui INTERFACE) +add_library(SDL3::SDL3 INTERFACE IMPORTED) +''' + source += f'set(ENABLE_SWITCH2KIT {"ON" if enabled else "OFF"})\n' + if enabled: + source += '''add_library(Switch2KitSDL3 STATIC fixture.cpp) +add_library(Switch2Kit::SDL3 ALIAS Switch2KitSDL3) +set_property(TARGET Switch2KitSDL3 PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") +''' + source += f'add_subdirectory("{(ROOT / "src/input").as_posix()}" input)\n' + source += 'file(GENERATE OUTPUT "${CMAKE_BINARY_DIR}/host-crt.txt" CONTENT "$>")\n' + if enabled: + source += 'file(GENERATE OUTPUT "${CMAKE_BINARY_DIR}/adapter-crt.txt" CONTENT "$>")\n' + else: + source += '''if(TARGET Switch2KitSDL3) + message(FATAL_ERROR "The disabled input target must not acquire the adapter") +endif() +''' + (root / 'CMakeLists.txt').write_text(source) + result = subprocess.run( + ['cmake', '-S', str(root), '-B', str(root / 'build'), + '-G', 'Ninja', f'-DCMAKE_BUILD_TYPE={configuration}'], + text=True, capture_output=True, timeout=30) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + expected = 'MultiThreadedDebug' if configuration == 'Debug' else 'MultiThreaded' + self.assertEqual((root / 'build/host-crt.txt').read_text(), expected) + adapter = root / 'build/adapter-crt.txt' + self.assertEqual(adapter.exists(), enabled) + if enabled: + self.assertEqual(adapter.read_text(), expected) + def test_production_cmake_gates(self): cmake = shutil.which('cmake') self.assertTrue(cmake, 'CMake is required') policy = 'if(ENABLE_SWITCH2KIT)' + text('CMakeLists.txt').split( 'if(ENABLE_SWITCH2KIT)', 1)[1].split('\n# glslang', 1)[0] - # platform, native, SDL, bundle, deployment, SDK exists, expected admission/error + # platform, native, SDL, bundle, deployment, SDK exists, SDK reached, configure succeeds cases = [ - ('Darwin', False, False, False, '13.4', True, False, None), - ('FreeBSD', False, False, False, '', False, False, None), - ('Darwin', True, True, True, '15.0', True, True, None), - ('Darwin', True, True, False, '15.0', True, False, 'MACOS_BUNDLE'), - ('Darwin', True, True, True, '13.4', True, False, '15.0'), - ('Linux', True, True, False, '', True, True, None), - ('Linux', True, False, False, '', True, False, 'ENABLE_SDL'), - ('Linux', True, True, False, '', False, False, 'Switch2Kit is missing'), - ('FreeBSD', True, True, False, '', True, False, 'desktop host'), + ('Darwin', False, False, False, '13.4', True, False, True), + ('FreeBSD', False, False, False, '', False, False, True), + ('Darwin', True, True, True, '15.0', True, True, True), + ('Darwin', True, True, False, '15.0', True, False, False), + ('Darwin', True, True, True, '13.4', True, False, False), + ('Linux', True, True, False, '', True, True, True), + ('Windows', True, True, False, '', True, True, True), + ('Windows', True, False, False, '', True, False, False), + ('Windows', False, False, False, '', False, False, True), + ('Linux', True, False, False, '', True, False, False), + ('Linux', True, True, False, '', False, False, False), + ('FreeBSD', True, True, False, '', True, False, False), ] with tempfile.TemporaryDirectory(prefix='cemu-cmake-policy-') as directory: script = Path(directory) / 'policy.cmake' sdk = SDK - for platform, native, sdl, bundle, version, exists, admitted, error in cases: + for platform, native, sdl, bundle, version, exists, admitted, valid in cases: with self.subTest(platform=platform, native=native, sdl=sdl, bundle=bundle, version=version, sdk=exists): - values = dict(APPLE=platform == 'Darwin', WIN32=False, + values = dict(APPLE=platform == 'Darwin', WIN32=platform == 'Windows', CMAKE_SYSTEM_NAME=platform, ENABLE_SWITCH2KIT=native, ENABLE_SDL=sdl, MACOS_BUNDLE=bundle, CMAKE_OSX_DEPLOYMENT_TARGET=version, @@ -122,10 +173,8 @@ def test_production_cmake_gates(self): result = subprocess.run([cmake, '-P', str(script)], text=True, capture_output=True, timeout=30) output = result.stdout + result.stderr - self.assertEqual(result.returncode == 0, error is None, output) + self.assertEqual(result.returncode == 0, valid, output) self.assertEqual('SWITCH2KIT_ADMITTED' in output, admitted, output) - if error: - self.assertIn(error, output) if __name__ == '__main__':