Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions .github/workflows/native-switch2kit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
name: Native Switch2Kit
on:
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: native-switch2kit-${{ github.ref }}
cancel-in-progress: true
jobs:
policies:
runs-on: ubuntu-latest
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: Execute mapping, identity, setup rollback and session regressions
run: python3 tests/switch2kit/run.py --sdl test-sdl --sanitize
macos:
if: github.repository == 'jmonster/Cemu'
strategy:
fail-fast: false
matrix:
include:
- os: macos-15
arch: arm64
- os: macos-15-intel
arch: x86_64
runs-on: ${{ matrix.os }}
timeout-minutes: 90
env:
DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer
VCPKG_MAX_CONCURRENCY: 3
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
submodules: recursive
persist-credentials: false
- name: Install native tools
run: brew install cmake ninja nasm automake libtool molten-vk
- name: Build the complete application
run: |
set -o pipefail
test "$(uname -m)" = '${{ matrix.arch }}'
bash scripts/build-switch2kit.sh 2>&1 | tee native-build.log
- name: Check production policies on the native architecture
run: python3 tests/switch2kit/run.py --sanitize
- name: Inspect embedded SDK, architecture and application signature
run: |
codesign --verify --deep --strict bin/Cemu_release.app
python3 dependencies/Switch2Kit/Integrations/Emulators/verify-bundle.py \
cemu . build-switch2kit --architecture '${{ matrix.arch }}'
- name: Launch, quit and relaunch the exact ZIP with build dependencies denied
run: |
xcrun swiftc -swift-version 6 -warnings-as-errors \
dependencies/Switch2Kit/tests/emulator-launch/Observe.swift \
-o "$RUNNER_TEMP/cemu-window-observer"
mkdir "$RUNNER_TEMP/cemu-app"
ditto -x -k build-switch2kit/integration-app.zip "$RUNNER_TEMP/cemu-app"
python3 dependencies/Switch2Kit/tests/emulator-launch/verify.py cemu \
"$RUNNER_TEMP/cemu-app/Cemu_release.app" "$RUNNER_TEMP/cemu-window-observer" \
"$PWD/launch-results"
- name: Check the disabled option retains the upstream deployment target
run: |
cmake -S . -B build-switch2kit -DENABLE_SWITCH2KIT=OFF \
-DMACOS_BUNDLE=OFF -DCMAKE_OSX_DEPLOYMENT_TARGET=13.4
python3 - <<'PY'
import json
from pathlib import Path
commands = json.loads(Path('build-switch2kit/compile_commands.json').read_text())
assert not any('HAVE_SWITCH2KIT' in row['command'] for row in commands)
assert not any('Switch2KitSetup.cpp' in row['file'] for row in commands)
cache = Path('build-switch2kit/CMakeCache.txt').read_text()
# Command-line cache entries may be STRING or UNINITIALIZED. Check
# the deployment value, not CMake's incidental cache type spelling.
values = {line.split(':', 1)[0]: line.split('=', 1)[1]
for line in cache.splitlines()
if not line.startswith(('#', '//')) and ':' in line and '=' in line}
assert values.get('CMAKE_OSX_DEPLOYMENT_TARGET') == '13.4', values.get('CMAKE_OSX_DEPLOYMENT_TARGET')
print('PASS disabled option: no native backend sources or defines; macOS 13.4 retained')
PY
- name: Qualified development application
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: Cemu-Switch2Kit-${{ matrix.arch }}
path: |
build-switch2kit/integration-app.zip
build-switch2kit/integration-inspection.json
launch-results/launch.json
if-no-files-found: error
retention-days: 14
- name: Native diagnostics
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: Cemu-Switch2Kit-${{ matrix.arch }}-diagnostics
path: |
native-build.log
build-switch2kit/integration-native-diagnostics.txt
build-switch2kit/CMakeCache.txt
build-switch2kit/CMakeFiles/CMakeConfigureLog.yaml
launch-results
sdk:
if: github.repository == 'jmonster/Cemu'
runs-on: macos-15
timeout-minutes: 30
env:
DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer
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
- run: brew install cmake ninja boost
- name: Exercise the pinned SDK, motor packets, real SDL and Cemu motion consumer
run: |
set -o pipefail
swift test --package-path dependencies/Switch2Kit -Xswiftc -warnings-as-errors 2>&1 | tee sdk-tests.log
bash dependencies/Switch2Kit/tests/rumble/run.sh 2>&1 | tee motor-tests.log
bash dependencies/Switch2Kit/tests/session/run.sh 2>&1 | tee session-tests.log
S2K_SDL_SOURCE="$PWD/test-sdl" \
bash dependencies/Switch2Kit/tests/sdl-inprocess/verify.sh 2>&1 | tee sdl-tests.log
S2K_SDL_SOURCE="$PWD/test-sdl" S2K_CEMU_SOURCE="$PWD" \
bash dependencies/Switch2Kit/tests/emulator-host/verify.sh 2>&1 | tee host-tests.log
- name: Test diagnostics
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: Cemu-Switch2Kit-sdk-diagnostics
path: '*-tests.log'
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@
[submodule "dependencies/xbyak_aarch64"]
path = dependencies/xbyak_aarch64
url = https://github.com/fujitsu/xbyak_aarch64

[submodule "dependencies/Switch2Kit"]
path = dependencies/Switch2Kit
url = https://github.com/jmonster/Switch2Kit.git
22 changes: 21 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
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)
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)

Expand Down Expand Up @@ -99,7 +101,9 @@ endif()

if (APPLE)
enable_language(OBJC OBJCXX)
set(CMAKE_OSX_DEPLOYMENT_TARGET "13.4")
if(NOT CMAKE_OSX_DEPLOYMENT_TARGET)
set(CMAKE_OSX_DEPLOYMENT_TARGET "13.4")
endif()
endif()

if (UNIX AND NOT APPLE)
Expand Down Expand Up @@ -168,6 +172,22 @@ if(ENABLE_SDL)
add_compile_definitions(HAS_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")
endif()
if(CMAKE_OSX_DEPLOYMENT_TARGET VERSION_LESS 15.0)
message(FATAL_ERROR "Switch2Kit requires 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")
endif()
add_subdirectory("${SWITCH2KIT_SOURCE_DIR}/Integrations/SDL3" "${CMAKE_BINARY_DIR}/switch2kit")
add_compile_definitions(HAVE_SWITCH2KIT)
include_directories("${SWITCH2KIT_SOURCE_DIR}/Integrations/Emulators")
set(SWITCH2KIT_BLUETOOTH_USAGE "<key>NSBluetoothAlwaysUsageDescription</key><string>Cemu uses Bluetooth to connect to the controllers you select.</string>")
endif()

# glslang versions older than 11.11.0 define targets without a namespace
if (NOT TARGET glslang::SPIRV AND TARGET SPIRV)
add_library(glslang::SPIRV ALIAS SPIRV)
Expand Down
13 changes: 13 additions & 0 deletions cmake/SignSwitch2KitBundle.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Development signing only, after all bundle relocation. Not notarization.
if(NOT DEFINED APP OR NOT EXISTS "${APP}/Contents/Info.plist")
message(FATAL_ERROR "A complete Cemu application bundle is required")
endif()
file(GLOB libraries "${APP}/Contents/Frameworks/*.dylib")
foreach(image IN LISTS libraries)
execute_process(COMMAND /usr/bin/codesign --force --sign - --timestamp=none "${image}"
COMMAND_ERROR_IS_FATAL ANY)
endforeach()
execute_process(COMMAND /usr/bin/codesign --force --sign - --timestamp=none "${APP}"
COMMAND_ERROR_IS_FATAL ANY)
execute_process(COMMAND /usr/bin/codesign --verify --deep --strict "${APP}"
COMMAND_ERROR_IS_FATAL ANY)
1 change: 1 addition & 0 deletions dependencies/Switch2Kit
Submodule Switch2Kit added at 8088ce
109 changes: 109 additions & 0 deletions docs/Switch2Kit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Switch 2 controllers on macOS

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.

## Build and open

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:

```sh
brew install cmake ninja nasm automake libtool molten-vk
```

In your Cemu checkout:

```sh
bash scripts/build-switch2kit.sh --run
```

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.

## Connect and play

Close any other app managing the same controllers. Open **Options > Input
settings**, click **Find Switch 2 Controllers**, allow Bluetooth, and hold the
controller's **Sync** button. Scanning lasts 60 seconds.

Choose the connected **GameCube** or **Pro Controller 2** in the new dropdown
beside **Emulated controller** on the desired controller tab. 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. Existing GamePad, Pro
or Classic controller types are retained. Choose a type that the game supports.

The shortcut enables rumble for a new attachment. Open the physical controller's
**Settings**, adjust **Rumble**, and click **Test rumble** for a half-second test.
GameCube rumble is on/off; Pro rumble uses its existing HD-rumble path.

Replacing a populated slot asks first and saves a **Before Switch2Kit-…** backup
in Profile > Load. Cancelling, a failed backup/save, or changing the slot during
confirmation keeps the old configuration. Refreshing or reconnecting does not
reset custom mappings. A physical controller already assigned to another slot
must be removed there before using the shortcut.

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

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
sticks do not have click buttons; games requiring L-stick/R-stick clicks need
those actions mapped to spare buttons or an additional keyboard source. Stick
travel and trigger axes remain available in manual mappings.

The Pro controller maps +/−, both stick clicks, shoulders, triggers and D-pad.
Capture supplies the GamePad microphone button and C supplies Show Screen. GL/GR
and other spare inputs remain available for custom bindings.

For Joy-Con 2, pair both halves, choose the emulated controller type, and use
**+ > SDLController** to add each half to the same slot. Each receives its own
complementary mappings; adding one does not overwrite the other's assignments.
This uses Cemu's multiple-source model, not a system-wide virtual paired device.
The quick dropdown is for GameCube and Pro controllers. Wiimote mappings remain
manual. A controller does not replace the Wii U touchscreen; use Cemu's normal
mouse/touchscreen facilities when a game requires them.

## Motion

The in-process sensor path includes GamePad/KPAD motion integration, but **motion
is not automatically calibrated**. In the physical controller's Settings, choose
a measured, device-matching `.s2kmotion` profile and enable **Use motion**, then
click OK. The status explains missing, disabled, disconnected or stale input.
An invalid profile leaves the previous selection intact. Profile import/removal
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
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.
20 changes: 20 additions & 0 deletions scripts/build-switch2kit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash
# 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
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
13 changes: 12 additions & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,18 @@ if (MACOS_BUNDLE)
set(MACOSX_BUNDLE_COPYRIGHT "Copyright © ${CURRENT_YEAR} Cemu Project")

set(MACOSX_BUNDLE_CATEGORY "public.app-category.games")
set(MACOSX_MINIMUM_SYSTEM_VERSION "13.4")
set(MACOSX_MINIMUM_SYSTEM_VERSION "${CMAKE_OSX_DEPLOYMENT_TARGET}")
set(MACOSX_BUNDLE_TYPE_EXTENSION "wua")

set_target_properties(CemuBin PROPERTIES
MACOSX_BUNDLE true
RESOURCE "${RESOURCE_FILES}"
)

if(ENABLE_SWITCH2KIT)
switch2kit_embed(CemuBin)
endif()

set(FOLDERS gameProfiles resources)
foreach(folder ${FOLDERS})
add_custom_command (TARGET CemuBin POST_BUILD
Expand Down Expand Up @@ -174,6 +178,13 @@ if (MACOS_BUNDLE)
"$<TARGET_FILE:CemuBin>"
)
endif()
if(ENABLE_SWITCH2KIT)
add_custom_command(TARGET CemuBin POST_BUILD
COMMAND ${CMAKE_COMMAND} "-DAPP=$<TARGET_BUNDLE_DIR:CemuBin>"
-P "${CMAKE_SOURCE_DIR}/cmake/SignSwitch2KitBundle.cmake"
VERBATIM)
endif()

else()
if(APPLE)
if (ENABLE_VULKAN)
Expand Down
5 changes: 5 additions & 0 deletions src/gui/wxgui/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,8 @@ endif()
if(ALLOW_PORTABLE)
target_compile_definitions(CemuWxGui PRIVATE CEMU_ALLOW_PORTABLE)
endif ()

if(ENABLE_SWITCH2KIT)
target_sources(CemuWxGui PRIVATE input/Switch2KitSetup.cpp input/Switch2KitSetup.h)
target_link_libraries(CemuWxGui PRIVATE Switch2Kit::SDL3)
endif()
Loading
Loading