Compare commits

..

10 Commits

Author SHA1 Message Date
patchzyy 88703702df merge main 2026-10-18 15:07:41 +02:00
patchzyy c255df5136 Synchronize surface release with GPU idle (#308) 2026-10-18 15:05:53 +02:00
patchzyy 2ee48fc880 Implement controller sumble on wiimote (#304) 2026-10-18 15:05:43 +02:00
patchzyy d7b64361e7 Fix soft-drift (#302) 2026-10-18 15:05:31 +02:00
patchzyy 5ee158918f Show sha (#301) 2026-10-18 15:05:19 +02:00
patchzyy bd12087cc3 Fix native build path quoting (#300) 2026-10-18 15:05:08 +02:00
patchzyy 93353ee335 read kernel remote buttons and pair classic node 2026-10-18 14:59:59 +02:00
patchzyy d5296779ec Store Wii calibration per remote (#299)
* Store Wii calibration per remote

* show calibration state per port

* tie calibration message to remote
2026-10-18 14:16:38 +02:00
patchzyy a93eaf582f Sky Station crash (#303) 2026-10-18 14:14:50 +02:00
patchzyy 5688b8b5b6 Support Linux Wii controller mappings 2026-10-18 12:00:08 +02:00
18 changed files with 540 additions and 161 deletions
+2 -1
View File
@@ -429,7 +429,8 @@ try {
}
$redist = Join-Path $Toolkit 'Redist'
if (Test-Path -LiteralPath $redist -PathType Container) {
Copy-Item -Path (Join-Path $redist '*.dll') -Destination $Destination -Force
Get-ChildItem -LiteralPath $redist -Filter '*.dll' -File |
Copy-Item -Destination $Destination -Force
}
foreach ($name in @('config','wii_bootstrap')) {
$directory = Join-Path $build $name
+2 -1
View File
@@ -145,7 +145,8 @@ function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Descri
else { Write-Host "${LogPrefix}: $Description" }
$quotedArguments = @($Arguments | ForEach-Object {
if ($_.Contains('"')) { throw "A native build argument contains an unsupported quote: $_" }
'"' + $_ + '"'
# Backslashes directly before the closing quote would otherwise escape it.
'"' + ($_ -replace '(\\+)$', '$1$1') + '"'
})
if ($WaitForProcessTree) {
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments `
@@ -501,17 +501,31 @@ internal sealed class InstallerEngine
var dol = Path.Combine(dataRoot, "sys", "main.dol");
var rel = Path.Combine(dataRoot, "files", "rel", "StaticR.rel");
var fst = Path.Combine(dataRoot, "sys", "fst.bin");
if (!File.Exists(dol) || !File.Exists(rel) || !File.Exists(fst))
throw new InvalidDataException("The disc extraction is missing required Mario Kart Wii files.");
var dolHash = InputValidation.Sha256File(dol);
var relHash = InputValidation.Sha256File(rel);
if (!dolHash.Equals(manifest.ExpectedDolSha256, StringComparison.OrdinalIgnoreCase) ||
!relHash.Equals(manifest.ExpectedRelSha256, StringComparison.OrdinalIgnoreCase))
{
var missing = new[] { dol, rel, fst }.Where(path => !File.Exists(path))
.Select(path => Path.GetRelativePath(dataRoot, path)).ToList();
if (missing.Count > 0)
throw new InvalidDataException(
"The disc is RMCP01 but does not match the supported clean PAL revision. " +
"Patched or otherwise modified game code cannot be installed safely.");
"The disc extraction is missing required Mario Kart Wii files: " +
string.Join(", ", missing) + ".");
var mismatches = new List<string>();
foreach (var (name, path, expected) in new[]
{
("main.dol", dol, manifest.ExpectedDolSha256),
("StaticR.rel", rel, manifest.ExpectedRelSha256)
})
{
var actual = InputValidation.Sha256File(path);
if (!actual.Equals(expected, StringComparison.OrdinalIgnoreCase))
mismatches.Add($"{name} is {new FileInfo(path).Length:N0} bytes with SHA-256 {actual} " +
$"(expected {expected})");
}
if (mismatches.Count > 0)
throw new InvalidDataException(
"The disc is RMCP01 but its game code does not match the supported clean PAL revision: " +
string.Join("; ", mismatches) + ". " +
"Patched or otherwise modified game code cannot be installed safely.");
}
private static void ValidateInstallDirectory(string path) =>
+3 -1
View File
@@ -447,7 +447,9 @@ if [[ "$cc_bin" == */* ]]; then
configure_args+=(-DCMAKE_ASM_COMPILER="$cc_bin")
fi
if [[ -n "$fuse_ld_override" ]]; then
configure_args+=(-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=$fuse_ld_override")
# CMAKE_EXE_LINKER_FLAGS is one string that CMake pastes into the shell link command.
printf -v fuse_ld_quoted '%q' "$fuse_ld_override"
configure_args+=(-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=$fuse_ld_quoted")
fi
if [[ -n "$native_prebuilt_dir" ]]; then
configure_args+=(-DMKW_NATIVE_PREBUILT_DIR="$native_prebuilt_dir")
+4 -2
View File
@@ -51,8 +51,10 @@ dol=$(find "$scratch/extracted" -type f -path '*/sys/main.dol' -print -quit)
rel=$(find "$scratch/extracted" -type f -path '*/files/rel/StaticR.rel' -print -quit)
[[ -n "$dol" ]] || fail 'nodtool extraction did not contain sys/main.dol'
[[ -n "$rel" ]] || fail 'nodtool extraction did not contain files/rel/StaticR.rel'
[[ $(sha256 "$dol") == "$EXPECTED_DOL_SHA256" ]] || fail 'disc is not the supported clean PAL RMCP01 Mario Kart Wii image'
[[ $(sha256 "$rel") == "$EXPECTED_REL_SHA256" ]] || fail 'disc has an unexpected StaticR.rel; use a clean PAL RMCP01 image'
dol_sha=$(sha256 "$dol")
rel_sha=$(sha256 "$rel")
[[ $dol_sha == "$EXPECTED_DOL_SHA256" ]] || fail "main.dol does not match the clean PAL RMCP01 image (expected $EXPECTED_DOL_SHA256, got $dol_sha)"
[[ $rel_sha == "$EXPECTED_REL_SHA256" ]] || fail "StaticR.rel does not match the clean PAL RMCP01 image (expected $EXPECTED_REL_SHA256, got $rel_sha)"
data_root=$(dirname "$(dirname "$dol")")
[[ -d "$data_root/files" ]] || fail 'nodtool extraction did not contain the Wii files directory'
+15 -4
View File
@@ -11,7 +11,13 @@ workspace="$support_root/BuildWorkspace"
products="$support_root/Products"
fail() { printf 'WiiCompiled Setup: %s\n' "$*" >&2; exit 1; }
notice() { /usr/bin/osascript -e "display dialog \"${1//\"/\\\"}\" buttons {\"OK\"} default button \"OK\" with icon caution" >/dev/null; }
notice() {
/usr/bin/osascript - "$1" >/dev/null <<'APPLESCRIPT'
on run argv
display dialog (item 1 of argv) buttons {"OK"} default button "OK" with icon caution
end run
APPLESCRIPT
}
usage() {
cat <<'EOF'
Usage: setup.command --game IMAGE [--retro-dir DIR] [--install-location {user|applications}]
@@ -146,7 +152,7 @@ fi
"$workspace/Launcher/local-build-macos.command" "${build_args[@]}" --profile "$profile" --cmake "$cmake_bin" --ninja "$ninja_bin" --translator-bin "$translator"
config="$support_root/Config.toml"
toml_string() { printf '%s' "$1" | sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g'; }
toml_string() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'; }
set_path() {
local key=$1 value=$2 encoded line temporary
encoded=$(toml_string "$value"); line="$key = \"$encoded\""; temporary="$config.tmp"
@@ -172,8 +178,13 @@ install_app() {
local app=$1
[[ -d "$products/$app" ]] || return 0
if [[ "$destination" == /Applications ]]; then
command="mkdir -p /Applications && rm -rf '/Applications/$app' && ditto '$products/$app' '/Applications/$app'"
/usr/bin/osascript -e "do shell script \"$command\" with administrator privileges"
/usr/bin/osascript - "$products/$app" "/Applications/$app" <<'APPLESCRIPT'
on run argv
set sourcePath to quoted form of (item 1 of argv)
set installedPath to quoted form of (item 2 of argv)
do shell script "mkdir -p /Applications && rm -rf " & installedPath & " && /usr/bin/ditto " & sourcePath & " " & installedPath with administrator privileges
end run
APPLESCRIPT
else
mkdir -p "$destination"; rm -rf "$destination/$app"; /usr/bin/ditto "$products/$app" "$destination/$app"
fi
@@ -15,6 +15,7 @@ function(aurora_copy_runtime_dlls target)
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_RUNTIME_DLLS:${_t}> $<TARGET_FILE_DIR:${target}>
COMMAND_EXPAND_LISTS
VERBATIM
)
endforeach ()
@@ -30,6 +31,7 @@ function(aurora_copy_runtime_dlls target)
if (EXISTS "${_dawn_bin_dir}/${_dll}")
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_dawn_bin_dir}/${_dll}" $<TARGET_FILE_DIR:${target}>
VERBATIM
)
endif ()
endforeach ()
+6 -3
View File
@@ -195,7 +195,7 @@ std::array<PADButtonMapping, PAD_BUTTON_COUNT> g_defaultButtonsJoyPair{{
}};
// Wii U Pro Controllers through SDL's HIDAPI Wii driver. No SDL_GamepadType
// singles them out, so they are picked by the name the driver gives them (see
// singles them out, so they are picked by name or USB id (see
// __PADSetDefaultMapping). Wii Remotes, with or without a Nunchuk or Classic
// Controller, are read by the game through KPAD instead and never get a
// GameCube mapping (the runtime hides those ports from PADRead).
@@ -451,11 +451,14 @@ static void reset_alt_button_mapping(aurora::input::GameController* controller)
// SDL's hidapi Wii driver names the pad "Nintendo Wii U Pro Controller"; the
// other names it produces are Wii Remotes, which the game reads through KPAD
// and which therefore never take a GameCube mapping.
// and which therefore never take a GameCube mapping. The Linux kernel driver's
// "Nintendo Wii Remote Pro Controller" is only recognised by its USB id.
static bool wii_default_mapping(const aurora::input::GameController* controller,
std::array<PADButtonMapping, PAD_BUTTON_COUNT>& out) {
const char* name = SDL_GetGamepadName(controller->m_controller);
if (name == nullptr || SDL_strstr(name, "Wii U Pro Controller") == nullptr) {
const bool wiiUProId = SDL_GetGamepadVendor(controller->m_controller) == 0x057e &&
SDL_GetGamepadProduct(controller->m_controller) == 0x0330;
if (!wiiUProId && (name == nullptr || SDL_strstr(name, "Wii U Pro Controller") == nullptr)) {
return false;
}
out = g_defaultButtonsWiiUPro;
+16
View File
@@ -174,6 +174,12 @@ static std::string color_arg_reg(GXTevColorArg arg, size_t stageIdx, const Shade
case GX_CC_KONST: {
switch (stage.kcSel) {
DEFAULT_FATAL("invalid kcSel {}", underlying(stage.kcSel));
// Unsourced encodings evaluate to zero in hardware.
case static_cast<GXTevKColorSel>(0x08):
case static_cast<GXTevKColorSel>(0x09):
case static_cast<GXTevKColorSel>(0x0A):
case static_cast<GXTevKColorSel>(0x0B):
return "vec3f(0.0)";
case GX_TEV_KCSEL_8_8:
return "vec3f(1.0)";
case GX_TEV_KCSEL_7_8:
@@ -275,6 +281,16 @@ static std::string alpha_arg_reg(GXTevAlphaArg arg, size_t stageIdx, const Shade
case GX_CA_KONST: {
switch (stage.kaSel) {
DEFAULT_FATAL("invalid kaSel {}", underlying(stage.kaSel));
// Unsourced encodings evaluate to zero in hardware.
case static_cast<GXTevKAlphaSel>(0x08):
case static_cast<GXTevKAlphaSel>(0x09):
case static_cast<GXTevKAlphaSel>(0x0A):
case static_cast<GXTevKAlphaSel>(0x0B):
case static_cast<GXTevKAlphaSel>(0x0C):
case static_cast<GXTevKAlphaSel>(0x0D):
case static_cast<GXTevKAlphaSel>(0x0E):
case static_cast<GXTevKAlphaSel>(0x0F):
return "0.0";
case GX_TEV_KASEL_8_8:
return "1.0";
case GX_TEV_KASEL_7_8:
+6 -1
View File
@@ -893,9 +893,14 @@ void release_surface() noexcept {
}
g_surface = {};
if (hadSurface && g_instance && g_device && g_queue) {
// Retired swapchain is freed after a later submission completes and the device ticks.
submit_staging_commands(g_device.CreateCommandEncoder().Finish());
const auto future = g_queue.OnSubmittedWorkDone(
wgpu::CallbackMode::WaitAnyOnly, [](wgpu::QueueWorkDoneStatus, wgpu::StringView) {});
g_instance.WaitAny(future, 1000000000);
if (const auto status = g_instance.WaitAny(future, 1000000000); status != wgpu::WaitStatus::Success) {
Log.warn("Timed out waiting for queue idle on surface release: {}", magic_enum::enum_name(status));
}
g_device.Tick();
}
}
+6
View File
@@ -42,6 +42,12 @@ else()
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS x86_64/arm64")
endif()
if(MKW_PLATFORM_WINDOWS)
# CMake and Ninja write response files with Windows quoting, but clang tokenizes them with
# POSIX rules by default, where a bare apostrophe in a path opens a quoted span.
add_link_options(--rsp-quoting=windows)
endif()
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
message(FATAL_ERROR "WiiCompiled only supports Release builds")
endif()
+6 -6
View File
@@ -229,7 +229,7 @@ function(mkw_configure_product target)
(MKW_SQLITE_TARGET_TYPE STREQUAL "SHARED_LIBRARY" OR
MKW_SQLITE_TARGET_TYPE STREQUAL "MODULE_LIBRARY"))
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>)
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}> VERBATIM)
endif()
if(MKW_PLATFORM_WINDOWS)
@@ -263,7 +263,7 @@ function(mkw_configure_product target)
endif()
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${runtime_dll_path}" $<TARGET_FILE_DIR:${target}>)
"${runtime_dll_path}" $<TARGET_FILE_DIR:${target}> VERBATIM)
endforeach()
endif()
@@ -272,7 +272,7 @@ function(mkw_configure_product target)
message(FATAL_ERROR "Missing Wii first-run bootstrap payload: ${MKW_WII_BOOTSTRAP_SOURCE_DIR}")
endif()
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory
"${MKW_WII_BOOTSTRAP_SOURCE_DIR}" "$<TARGET_FILE_DIR:${target}>/wii_bootstrap")
"${MKW_WII_BOOTSTRAP_SOURCE_DIR}" "$<TARGET_FILE_DIR:${target}>/wii_bootstrap" VERBATIM)
set(MKW_DSP_COEFFICIENT_ROM "${MKW_RUNTIME_SOURCE_DIR}/assets/dsp/dsp_coef.bin")
if(NOT EXISTS "${MKW_DSP_COEFFICIENT_ROM}")
@@ -284,7 +284,7 @@ function(mkw_configure_product target)
message(FATAL_ERROR "Wii DSP coefficient ROM hash mismatch: ${MKW_DSP_COEFFICIENT_ROM_SHA256}")
endif()
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${MKW_DSP_COEFFICIENT_ROM}" "$<TARGET_FILE_DIR:${target}>/dsp_coef.bin")
"${MKW_DSP_COEFFICIENT_ROM}" "$<TARGET_FILE_DIR:${target}>/dsp_coef.bin" VERBATIM)
# Aurora imports this portable recipe database into each user's writable
# pipeline cache. Keep the upstream filename so its default resourcesPath
@@ -296,7 +296,7 @@ function(mkw_configure_product target)
endif()
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${MKW_INITIAL_PIPELINE_CACHE}"
"$<TARGET_FILE_DIR:${target}>/initial_pipeline_cache.db")
"$<TARGET_FILE_DIR:${target}>/initial_pipeline_cache.db" VERBATIM)
# Non-Windows TLS (runtime/src/hle/net/network_ssl.cpp's mbed TLS path) needs a trusted root
# CA bundle to verify server certificates against - Windows gets this for free from the OS via
@@ -310,7 +310,7 @@ function(mkw_configure_product target)
message(FATAL_ERROR "Missing TLS root CA bundle: ${MKW_CA_CERTIFICATE_BUNDLE}")
endif()
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${MKW_CA_CERTIFICATE_BUNDLE}" "$<TARGET_FILE_DIR:${target}>/cacert.pem")
"${MKW_CA_CERTIFICATE_BUNDLE}" "$<TARGET_FILE_DIR:${target}>/cacert.pem" VERBATIM)
endif()
endfunction()
+52 -33
View File
@@ -69,10 +69,11 @@ struct RuntimeUserConfig {
// SDL's sensor frame (x right, y out of the button face, z towards the user).
// SDL's Wii driver falls back to a nominal zero point when its read of the
// remote's calibration block times out (common over Bluetooth), so this is
// measured in the overlay with the remote at rest.
std::optional<double> wiiAccelOffsetX;
std::optional<double> wiiAccelOffsetY;
std::optional<double> wiiAccelOffsetZ;
// measured in the overlay with the remote at rest. Stored per remote under
// [controller.wii_accel_offsets]; the old shared
// wii_accel_offset_* keys are only detected, to report that they are ignored.
std::map<std::string, std::array<double, 3>> wiiAccelOffsets;
bool hasLegacyWiiAccelOffset = false;
// Debugging aid: append every KPAD sample of the Bluetooth remote (raw and
// corrected accelerometer, buttons) to wii_accel_trace.csv next to Config.toml.
std::optional<bool> wiiAccelTrace;
@@ -381,6 +382,28 @@ inline void AppendOverlayRoots(RuntimeUserConfig& config, const std::string& roo
}
// Reads every supported setting out of a parsed Config.toml document.
// [x, y, z] of finite numbers (integers allowed), else nullopt.
inline std::optional<std::array<double, 3>> ParseAccelOffset(const toml::value& value) {
if (!value.is_array() || value.as_array().size() != 3) {
return std::nullopt;
}
std::array<double, 3> offset{};
for (size_t i = 0; i < 3; ++i) {
const toml::value& element = value.as_array()[i];
if (element.is_floating()) {
offset[i] = element.as_floating();
} else if (element.is_integer()) {
offset[i] = static_cast<double>(element.as_integer());
} else {
return std::nullopt;
}
if (!std::isfinite(offset[i])) {
return std::nullopt;
}
}
return offset;
}
inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
RuntimeUserConfig config;
@@ -404,6 +427,13 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.controllerExpressions[key] = value.as_string();
}
}
if (section->contains("wii_accel_offsets") && section->at("wii_accel_offsets").is_table()) {
for (const auto& [key, value] : section->at("wii_accel_offsets").as_table()) {
if (const auto offset = ParseAccelOffset(value)) {
config.wiiAccelOffsets[key] = *offset;
}
}
}
}
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
@@ -465,9 +495,9 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
FindConfigValue<bool>(document, "audio", "attenuate_music_when_media_plays");
config.wiiRemotes = FindConfigValue<bool>(document, "controller", "wii_remotes");
config.wiiContinuousScan = FindConfigValue<bool>(document, "controller", "wii_continuous_scan");
config.wiiAccelOffsetX = FindConfigValue<double>(document, "controller", "wii_accel_offset_x");
config.wiiAccelOffsetY = FindConfigValue<double>(document, "controller", "wii_accel_offset_y");
config.wiiAccelOffsetZ = FindConfigValue<double>(document, "controller", "wii_accel_offset_z");
for (const char* key : {"wii_accel_offset_x", "wii_accel_offset_y", "wii_accel_offset_z"}) {
config.hasLegacyWiiAccelOffset |= FindConfigValue<double>(document, "controller", key).value_or(0.0) != 0.0;
}
config.wiiAccelTrace = FindConfigValue<bool>(document, "controller", "wii_accel_trace");
config.networkEnabled = FindConfigValue<bool>(document, "network", "enabled");
config.discordPresenceEnabled = FindConfigValue<bool>(document, "discord", "enabled");
@@ -842,12 +872,13 @@ inline bool SetWiiContinuousScanEnabled(bool value) {
return WriteSetting("controller", "wii_continuous_scan", value ? "true" : "false");
}
// Wii Remote accelerometer zero-point correction (g, SDL sensor frame); all zero
// when the remote has not been calibrated.
inline std::array<double, 3> WiiAccelOffset() {
const RuntimeUserConfig& config = Get();
return {config.wiiAccelOffsetX.value_or(0.0), config.wiiAccelOffsetY.value_or(0.0),
config.wiiAccelOffsetZ.value_or(0.0)};
// Stored correction of the remote identified by `remoteKey`, if it has one.
inline std::optional<std::array<double, 3>> WiiAccelOffsetFor(const std::string& remoteKey) {
const auto it = Get().wiiAccelOffsets.find(remoteKey);
if (it == Get().wiiAccelOffsets.end()) {
return std::nullopt;
}
return it->second;
}
// Whether to write the per-frame accelerometer trace (off unless asked for).
@@ -855,26 +886,14 @@ inline bool WiiAccelTraceEnabled(bool fallback = false) {
return Get().wiiAccelTrace.value_or(fallback);
}
// True while a non-zero correction is stored ("Clear calibration" writes zeros).
inline bool HasWiiAccelOffset() {
const std::array<double, 3> offset = WiiAccelOffset();
return offset[0] != 0.0 || offset[1] != 0.0 || offset[2] != 0.0;
}
// Persists the accelerometer correction measured by the overlay's calibration.
inline bool SetWiiAccelOffset(const std::array<double, 3>& offset) {
Mutable().wiiAccelOffsetX = offset[0];
Mutable().wiiAccelOffsetY = offset[1];
Mutable().wiiAccelOffsetZ = offset[2];
bool ok = true;
const char* keys[3] = {"wii_accel_offset_x", "wii_accel_offset_y", "wii_accel_offset_z"};
for (size_t i = 0; i < 3; ++i) {
// Always a float literal, so a whole-number offset does not come back as a TOML integer.
std::ostringstream formatted;
formatted << std::fixed << std::setprecision(4) << offset[i];
ok = WriteSetting("controller", keys[i], formatted.str()) && ok;
}
return ok;
// Persists the accelerometer correction of one remote. `remoteKey` must be a bare TOML key.
inline bool SetWiiAccelOffsetFor(const std::string& remoteKey, const std::array<double, 3>& offset) {
Mutable().wiiAccelOffsets[remoteKey] = offset;
// Always float literals, so a whole-number offset does not come back as a TOML integer.
std::ostringstream formatted;
formatted << std::fixed << std::setprecision(4) << '[' << offset[0] << ", " << offset[1] << ", " << offset[2]
<< ']';
return WriteSetting("controller.wii_accel_offsets", remoteKey, formatted.str());
}
// Target frame rate for frame interpolation, or 0 to disable it.
+13 -7
View File
@@ -1,6 +1,8 @@
#pragma once
#include <array>
#include <cstdint>
#include <optional>
struct PADStatus;
@@ -73,16 +75,20 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample);
// no remote or SDL has not delivered a sample yet.
bool ReadAccelDebug(uint32_t chan, float sdlG[3], float kpadAcc[3]);
// Residual zero-point trim on top of SDL's factory-calibrated decode. Expects the
// remote lying still, buttons up; Poll() averages about a second and stores it.
// Residual zero-point trim on top of SDL's factory-calibrated decode, stored per
// physical remote under [controller.wii_accel_offsets]. Expects the remote lying
// still, buttons up; Poll() averages about a second and stores it for that remote.
void StartAccelCalibration(uint32_t chan);
// Forgets the stored correction.
void ClearAccelCalibration();
bool IsAccelCalibrating();
// Forgets the stored correction of the remote on `chan`.
void ClearAccelCalibration(uint32_t chan);
bool IsAccelCalibrating(uint32_t chan);
// 0..1 while a calibration is collecting samples.
float AccelCalibrationProgress();
// Outcome of the last calibration attempt for the overlay, or nullptr.
const char* AccelCalibrationMessage();
// Correction in effect for the remote on `chan` (zeros when uncalibrated), or
// nullopt when the port has no remote.
std::optional<std::array<double, 3>> ReadAccelOffset(uint32_t chan);
// Outcome of the last calibration attempt on `chan` for the overlay, or nullptr.
const char* AccelCalibrationMessage(uint32_t chan);
// Reports "no controller" on the GameCube side for every port served through
// KPAD, so the game never sees the same remote twice. Runs after aurora's PADRead.
+5
View File
@@ -89,6 +89,11 @@ void WritePadStatus(uint32_t base, const PADStatus& status) {
} // namespace
extern "C" bool PAD_HLE_RumbleEnabled()
{
return g_rumbleEnabled.load(std::memory_order_relaxed);
}
extern "C" void PAD_HLE_SetRumbleEnabled(bool enabled)
{
g_rumbleEnabled.store(enabled, std::memory_order_relaxed);
+5 -2
View File
@@ -5,6 +5,9 @@
#include <cstdint>
#include <dolphin/pad.h>
extern "C" bool PAD_HLE_RumbleEnabled();
void NandQueueIosCallback(uint32_t callbackPtr, int32_t result, uint32_t callbackArg);
namespace {
@@ -143,8 +146,8 @@ PPC_NATIVE_OVERRIDE(801C0990, WPADProbe_HLE, int32_t, (uint32_t chan, uint32_t t
extern "C" void WPADControlMotor_HLE(uint32_t chan, uint32_t command)
{
(void)chan;
(void)command;
const bool on = command != 0 && PAD_HLE_RumbleEnabled();
PADControlMotor(chan, on ? PAD_MOTOR_RUMBLE : PAD_MOTOR_STOP_HARD);
}
PPC_NATIVE_OVERRIDE(801C0EC4, WPADControlMotor_HLE, void, (uint32_t chan, uint32_t command), (chan, command));
+14 -6
View File
@@ -311,7 +311,7 @@ void DrawWiiRemoteAccelerometer(uint32_t port) {
}
// Residual per-axis bias left after SDL's factory-calibrated decode, measured
// here with the remote at rest.
if (WiiRemoteInput::IsAccelCalibrating()) {
if (WiiRemoteInput::IsAccelCalibrating(port)) {
ImGui::ProgressBar(WiiRemoteInput::AccelCalibrationProgress(), ImVec2(220.0f, 0.0f), "Hold still...");
} else if (ImGui::Button("Calibrate (remote lying flat, buttons up)")) {
WiiRemoteInput::StartAccelCalibration(port);
@@ -321,15 +321,17 @@ void DrawWiiRemoteAccelerometer(uint32_t port) {
"for about two seconds. Corrects the steering offset of a remote held sideways.");
}
ImGui::SameLine();
ImGui::BeginDisabled(!RuntimeConfigFile::HasWiiAccelOffset() || WiiRemoteInput::IsAccelCalibrating());
const auto stored = WiiRemoteInput::ReadAccelOffset(port);
const std::array<double, 3> offset = stored.value_or(std::array<double, 3>{});
const bool calibrated = offset[0] != 0.0 || offset[1] != 0.0 || offset[2] != 0.0;
ImGui::BeginDisabled(!calibrated || WiiRemoteInput::IsAccelCalibrating(port));
if (ImGui::Button("Clear calibration")) {
WiiRemoteInput::ClearAccelCalibration();
WiiRemoteInput::ClearAccelCalibration(port);
}
ImGui::EndDisabled();
if (const char* message = WiiRemoteInput::AccelCalibrationMessage()) {
if (const char* message = WiiRemoteInput::AccelCalibrationMessage(port)) {
ImGui::TextWrapped("%s", message);
} else if (RuntimeConfigFile::HasWiiAccelOffset()) {
const std::array<double, 3> offset = RuntimeConfigFile::WiiAccelOffset();
} else if (calibrated) {
ImGui::TextDisabled("Stored offset: x %+.3f y %+.3f z %+.3f g", offset[0], offset[1], offset[2]);
} else {
ImGui::TextDisabled("No trim stored.");
@@ -789,6 +791,12 @@ void DrawControllerSettings() {
ImGui::EndMenu();
}
if (WiiRemoteInput::IsRemoteChannel(selectedGamePort)) {
ImGui::TextDisabled("The game reads this controller as a Wii Remote, so the button mapping does not apply.");
DrawRumbleSettings();
return;
}
uint32_t mappingCount = 0;
PADButtonMapping* mappings = PADGetButtonMappings(static_cast<uint32_t>(g_controllerPort), &mappingCount);
if (mappings == nullptr || mappingCount != PAD_BUTTON_COUNT) {
+360 -85
View File
@@ -5,6 +5,7 @@
#include <dolphin/pad.h>
#include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_hidapi.h>
#include <SDL3/SDL_hints.h>
#include <SDL3/SDL_log.h>
#include <SDL3/SDL_sensor.h>
@@ -12,11 +13,16 @@
#include <algorithm>
#include <array>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <optional>
#include <string>
#include <system_error>
#include <unordered_map>
#include <unordered_set>
namespace WiiRemoteInput {
@@ -63,6 +69,8 @@ constexpr uint32_t kClUp = 0x0001, kClLeft = 0x0002, kClZR = 0x0004, kClX = 0x00
kClB = 0x0040, kClZL = 0x0080, kClR = 0x0200, kClPlus = 0x0400, kClHome = 0x0800,
kClMinus = 0x1000, kClL = 0x2000, kClDown = 0x4000, kClRight = 0x8000;
constexpr Uint16 kNintendoVendor = 0x057e;
// How long a vanished remote keeps its channel alive with neutral input. SDL's
// in-place reconnect after an extension change takes well under a second; a
// remote that is really gone shows up as disconnected after this.
@@ -86,17 +94,24 @@ bool g_scanning = false;
uint64_t g_driverOffSinceMs = 0;
// When the current scan started (last Wii controller seen).
uint64_t g_lostAtMs = 0;
bool g_hidrawWarned = false;
// Instance ids whose accelerometers have been switched on. SDL keeps sensors
// off until asked and forgets that when the gamepad is closed, so a re-paired
// remote gets a fresh id and is enabled again.
std::array<SDL_JoystickID, PAD_MAX_CONTROLLERS> g_sensorsEnabledFor{};
// Zero-point correction subtracted from the remote's accelerometer, in g and in
// SDL's sensor frame; loaded from Config.toml on first use, replaced by a
// calibration run. The Nunchuk accelerometer is left uncorrected.
std::array<float, 3> g_accelOffset{};
bool g_accelOffsetLoaded = false;
// Zero-point correction subtracted from a remote's accelerometer, in g and in
// SDL's sensor frame, with the config key it is stored under. Resolved once per
// gamepad instance (a re-created joystick of the same remote resolves to the same
// key) and replaced by a calibration run. The Nunchuk accelerometer is left
// uncorrected.
struct RemoteCalibration {
SDL_JoystickID instance = 0;
std::string key;
std::array<float, 3> offset{};
};
std::array<RemoteCalibration, PAD_MAX_CONTROLLERS> g_remoteCalibration{};
// Frames sampled by a calibration run (about 1.5 s at 60 Hz) and how far a
// sample may stray from the first one before the run is declared "moved".
@@ -110,12 +125,15 @@ constexpr double kCalibrationMinTiltCosine = 0.9396926;
struct AccelCalibration {
bool active = false;
uint32_t chan = 0;
SDL_JoystickID instance = 0;
int count = 0;
double sum[3] = {};
float first[3] = {};
};
AccelCalibration g_calibration;
char g_calibrationMessage[160] = {};
uint32_t g_calibrationMessageChan = 0;
SDL_JoystickID g_calibrationMessageRemote = 0;
// Last accepted KPAD acc per port, for the remote and for the Nunchuk. Reused on
// frames that bring no sample or a glitched one, so the game never sees a jump.
@@ -155,14 +173,73 @@ void EnsureSensors(SDL_Gamepad* gamepad, uint32_t port) {
}
}
// Loads the stored zero-point correction once.
const std::array<float, 3>& AccelOffset() {
if (!g_accelOffsetLoaded) {
const std::array<double, 3> stored = RuntimeConfigFile::WiiAccelOffset();
for (size_t i = 0; i < 3; ++i) g_accelOffset[i] = static_cast<float>(stored[i]);
g_accelOffsetLoaded = true;
// USB interface number of the HID device at `path`, or -1 when it is not a USB
// interface or cannot be found. A DolphinBar shows each remote slot as one.
int UsbInterfaceForPath(const char* path, Uint16 vendor, Uint16 product) {
if (path == nullptr) {
return -1;
}
return g_accelOffset;
int interfaceNumber = -1;
SDL_hid_device_info* list = SDL_hid_enumerate(vendor, product);
for (const SDL_hid_device_info* info = list; info != nullptr; info = info->next) {
if (info->path != nullptr && std::strcmp(info->path, path) == 0) {
interfaceNumber = info->interface_number;
break;
}
}
SDL_hid_free_enumeration(list);
return interfaceNumber;
}
// Config key identifying the physical remote behind a gamepad: its HID serial
// (the Bluetooth address when paired directly), else its USB interface number
// (a DolphinBar has no serial and one interface per remote slot) and the game
// port, else the game port. Only characters valid in a bare TOML key.
std::string RemoteKey(SDL_Gamepad* gamepad, uint32_t port) {
std::string serial;
if (const char* raw = SDL_GetGamepadSerial(gamepad)) {
for (const char* c = raw; *c != '\0'; ++c) {
if (std::isalnum(static_cast<unsigned char>(*c))) {
serial += static_cast<char>(std::tolower(static_cast<unsigned char>(*c)));
}
}
}
if (serial.find_first_not_of('0') != std::string::npos) {
return "serial_" + serial;
}
const Uint16 vendor = SDL_GetGamepadVendor(gamepad);
const Uint16 product = SDL_GetGamepadProduct(gamepad);
const int interfaceNumber = UsbInterfaceForPath(SDL_GetGamepadPath(gamepad), vendor, product);
char key[48];
if (interfaceNumber >= 0) {
std::snprintf(key, sizeof(key), "usb_%04x_%04x_if%d_port%u", vendor, product, interfaceNumber,
port + 1);
} else {
std::snprintf(key, sizeof(key), "port_%u", port + 1);
}
return key;
}
// Calibration of the remote currently on `port`, resolved on first use per gamepad
// instance. A remote without a stored entry has a zero offset.
RemoteCalibration& CalibrationFor(SDL_Gamepad* gamepad, uint32_t port) {
RemoteCalibration& calibration = g_remoteCalibration[port];
const SDL_JoystickID id = SDL_GetGamepadID(gamepad);
if (calibration.instance != id) {
static bool s_legacyReported = false;
if (!s_legacyReported && RuntimeConfigFile::Get().hasLegacyWiiAccelOffset) {
s_legacyReported = true;
RT_LOG(RT_TAG_CONFIG) << "Old shared Wii Remote calibration ignored; recalibrate each remote in F10"
<< std::endl;
}
calibration.instance = id;
calibration.key = RemoteKey(gamepad, port);
const std::optional<std::array<double, 3>> stored = RuntimeConfigFile::WiiAccelOffsetFor(calibration.key);
for (size_t i = 0; i < 3; ++i) calibration.offset[i] = stored ? static_cast<float>((*stored)[i]) : 0.0f;
RT_LOG(RT_TAG_CONFIG) << "Wii Remote on port " << (port + 1) << ": calibration key " << calibration.key
<< (stored ? "" : " (no stored offset)") << std::endl;
}
return calibration;
}
// Raw SDL sample in m/s^2, rejecting anything SDL has not delivered yet.
@@ -200,9 +277,10 @@ bool IsGlitchedSample(SDL_SensorType sensor, const float* g) {
// +x left, +y towards the user, +z out of the button face (wiibrew, Dolphin);
// SDL_hidapi_wii.c posts (-wiiX, wiiZ, wiiY): x right across the face, y out of
// the face (+1 at rest, buttons up), z towards the user, i.e. away from the tip.
// The remote's sample gets the zero-point correction; the Nunchuk's does not.
// False when there is no sample yet or the sample is a glitch (see above).
bool ReadAccelG(SDL_Gamepad* gamepad, SDL_SensorType sensor, float* g) {
// The remote's sample gets the zero-point correction of the remote on `port`; the
// Nunchuk's does not. False when there is no sample yet or the sample is a glitch
// (see above).
bool ReadAccelG(SDL_Gamepad* gamepad, uint32_t port, SDL_SensorType sensor, float* g) {
float sdl[3] = {};
if (!ReadSdlAccel(gamepad, sensor, sdl)) {
return false;
@@ -212,7 +290,7 @@ bool ReadAccelG(SDL_Gamepad* gamepad, SDL_SensorType sensor, float* g) {
return false;
}
if (sensor == SDL_SENSOR_ACCEL) {
const std::array<float, 3>& offset = AccelOffset();
const std::array<float, 3>& offset = CalibrationFor(gamepad, port).offset;
for (int i = 0; i < 3; ++i) g[i] -= offset[i];
}
return true;
@@ -231,9 +309,9 @@ void AccelGToKpad(const float* g, float* kpad) {
}
// Sensor -> KPAD acc for ReadKpadSample.
bool ReadAccelAsKpad(SDL_Gamepad* gamepad, SDL_SensorType sensor, float* kpad) {
bool ReadAccelAsKpad(SDL_Gamepad* gamepad, uint32_t port, SDL_SensorType sensor, float* kpad) {
float g[3] = {};
if (!ReadAccelG(gamepad, sensor, g)) {
if (!ReadAccelG(gamepad, port, sensor, g)) {
return false;
}
AccelGToKpad(g, kpad);
@@ -275,10 +353,25 @@ void TraceSample(uint32_t chan, const KpadSample& sample, const float* rawG, boo
trace.flush();
}
// Ends a calibration run with a message for the overlay.
void FinishAccelCalibration(const char* message) {
g_calibration.active = false;
// Gamepad of the Wii Remote on `chan`, or nullptr.
SDL_Gamepad* RemoteGamepad(uint32_t chan) {
if (chan >= PAD_MAX_CONTROLLERS || !IsRemoteChannel(chan)) {
return nullptr;
}
return SDL_GetGamepadFromPlayerIndex(static_cast<int>(chan));
}
void SetCalibrationMessage(uint32_t chan, const char* message) {
SDL_Gamepad* gamepad = RemoteGamepad(chan);
g_calibrationMessageChan = chan;
g_calibrationMessageRemote = gamepad != nullptr ? SDL_GetGamepadID(gamepad) : 0;
std::snprintf(g_calibrationMessage, sizeof(g_calibrationMessage), "%s", message);
}
// Ends a calibration run with a message for the overlay.
void FinishAccelCalibration(uint32_t chan, const char* message) {
g_calibration.active = false;
SetCalibrationMessage(chan, message);
RT_LOG(RT_TAG_CONFIG) << "Wii Remote accelerometer calibration: " << message << std::endl;
}
@@ -288,9 +381,10 @@ void StepAccelCalibration() {
if (!g_calibration.active) {
return;
}
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(g_calibration.chan));
if (gamepad == nullptr || !IsRemoteChannel(g_calibration.chan)) {
FinishAccelCalibration("Cancelled: the Wii Remote went away.");
SDL_Gamepad* gamepad = RemoteGamepad(g_calibration.chan);
if (gamepad == nullptr ||
SDL_GetGamepadID(gamepad) != g_calibration.instance) {
FinishAccelCalibration(g_calibration.chan, "Cancelled: the Wii Remote went away.");
return;
}
EnsureSensors(gamepad, g_calibration.chan);
@@ -308,7 +402,7 @@ void StepAccelCalibration() {
} else {
for (int i = 0; i < 3; ++i) {
if (std::fabs(g[i] - g_calibration.first[i]) > kCalibrationMaxDeviationG) {
FinishAccelCalibration("Failed: the remote moved. Put it down, buttons up, and try again.");
FinishAccelCalibration(g_calibration.chan, "Failed: the remote moved. Put it down, buttons up, and try again.");
return;
}
}
@@ -322,18 +416,18 @@ void StepAccelCalibration() {
const double length = std::sqrt(mean[0] * mean[0] + mean[1] * mean[1] + mean[2] * mean[2]);
if (length < kCalibrationMinGravityG || length > kCalibrationMaxGravityG ||
mean[1] / length < kCalibrationMinTiltCosine) {
FinishAccelCalibration("Failed: the remote was not resting flat with the buttons up.");
FinishAccelCalibration(g_calibration.chan, "Failed: the remote was not resting flat with the buttons up.");
return;
}
const std::array<double, 3> offset = {mean[0], mean[1] - 1.0, mean[2]};
for (int i = 0; i < 3; ++i) g_accelOffset[i] = static_cast<float>(offset[i]);
g_accelOffsetLoaded = true;
const bool saved = RuntimeConfigFile::SetWiiAccelOffset(offset);
RemoteCalibration& calibration = CalibrationFor(gamepad, g_calibration.chan);
for (int i = 0; i < 3; ++i) calibration.offset[i] = static_cast<float>(offset[i]);
const bool saved = RuntimeConfigFile::SetWiiAccelOffsetFor(calibration.key, offset);
char message[160];
std::snprintf(message, sizeof(message), "%s offset x %+.3f y %+.3f z %+.3f g",
saved ? "Calibrated:" : "Calibrated (could not write Config.toml):", offset[0], offset[1],
offset[2]);
FinishAccelCalibration(message);
FinishAccelCalibration(g_calibration.chan, message);
}
// True when any controller aurora knows about is a Wii device.
@@ -347,6 +441,110 @@ bool AnyWiiControllerConnected() {
return false;
}
// True for an evdev node served by the kernel hid-wiimote driver.
bool IsKernelWiiNode(SDL_JoystickID id) {
const char* path = SDL_GetGamepadPathForID(id);
return SDL_GetGamepadVendorForID(id) == kNintendoVendor && path != nullptr &&
std::strncmp(path, "/dev/input/", 11) == 0;
}
bool IsClassicNodeName(const char* name) {
return NameContains(name, "Classic Controller");
}
// HID device behind a kernel evdev node; the remote and its extension nodes share it.
std::string KernelParent(SDL_JoystickID id) {
static std::unordered_map<SDL_JoystickID, std::string> s_parents;
if (const auto it = s_parents.find(id); it != s_parents.end()) {
return it->second;
}
const char* path = SDL_GetGamepadPathForID(id);
if (path == nullptr) {
return {};
}
std::error_code ec;
const auto parent = std::filesystem::canonical(
std::filesystem::path("/sys/class/input") / std::filesystem::path(path).filename() / "device" / "device", ec);
if (ec) {
return {};
}
return s_parents.emplace(id, parent.string()).first->second;
}
// The kernel node on the same remote as `gamepad`: its Classic Controller node
// when `wantClassic`, else its remote node.
SDL_Gamepad* KernelSibling(SDL_Gamepad* gamepad, bool wantClassic) {
const SDL_JoystickID self = SDL_GetGamepadID(gamepad);
const std::string parent = KernelParent(self);
if (parent.empty()) {
return nullptr;
}
int count = 0;
SDL_JoystickID* ids = SDL_GetGamepads(&count);
if (ids == nullptr) {
return nullptr;
}
SDL_Gamepad* sibling = nullptr;
for (int i = 0; i < count && sibling == nullptr; ++i) {
if (ids[i] != self && IsKernelWiiNode(ids[i]) &&
IsClassicNodeName(SDL_GetGamepadNameForID(ids[i])) == wantClassic && KernelParent(ids[i]) == parent) {
sibling = SDL_GetGamepadFromID(ids[i]);
}
}
SDL_free(ids);
return sibling;
}
// The kernel exposes a remote and its Classic Controller as separate gamepads.
// The remote's port reads both, as on a Wii; the Classic node's own port stays empty.
Kind KindForGamepad(SDL_Gamepad* gamepad) {
if (gamepad == nullptr) {
return Kind::NotWii;
}
const char* name = SDL_GetGamepadName(gamepad);
const Kind kind = KindForName(name);
if ((kind != Kind::Remote && kind != Kind::RemoteWithClassic) || !IsKernelWiiNode(SDL_GetGamepadID(gamepad))) {
return kind;
}
if (IsClassicNodeName(name)) {
return KernelSibling(gamepad, false) != nullptr ? Kind::NotWii : kind;
}
return KernelSibling(gamepad, true) != nullptr ? Kind::RemoteWithClassic : kind;
}
// A kernel Classic Controller node whose remote node carries it.
bool IsCarriedKernelNode(SDL_Gamepad* gamepad) {
return gamepad != nullptr && IsKernelWiiNode(SDL_GetGamepadID(gamepad)) &&
IsClassicNodeName(SDL_GetGamepadName(gamepad)) && KernelSibling(gamepad, false) != nullptr;
}
// Logs once when the kernel driver serves a Wii controller SDL's driver could have had.
void WarnIfKernelDriverServing() {
if (g_hidrawWarned || !g_wiiDriverEnabled) {
return;
}
const uint64_t now = SDL_GetTicks();
static uint64_t s_nextCheckMs = 0;
if (now < s_nextCheckMs) {
return;
}
s_nextCheckMs = now + 2000;
int count = 0;
SDL_JoystickID* ids = SDL_GetGamepads(&count);
if (ids == nullptr) {
return;
}
for (int i = 0; i < count && !g_hidrawWarned; ++i) {
const Uint16 product = SDL_GetGamepadProductForID(ids[i]);
if ((product == 0x0306 || product == 0x0330) && IsKernelWiiNode(ids[i])) {
g_hidrawWarned = true;
RT_LOG(RT_TAG_CONFIG) << "A Wii controller is served by the kernel hid-wiimote driver. For full Wii "
<< "Remote support see the Linux udev rule in the README." << std::endl;
}
}
SDL_free(ids);
}
// Route SDL's input diagnostics (HIDAPI open failures, the Wii driver's
// extension/status messages) into console.log, minus the periodic chatter.
// A sub-warning message is written once: SDL repeats the same line on every
@@ -465,6 +663,7 @@ void Poll() {
for (uint32_t port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
(void)EffectiveKind(port);
}
WarnIfKernelDriverServing();
if (!g_wiiDriverEnabled) {
return;
}
@@ -519,11 +718,13 @@ uint32_t ScanCount() {
return g_scanCount;
}
// Maps the gamepad name SDL's Wii driver reports to a Kind.
// Maps the gamepad name SDL reports to a Kind: "Nintendo Wii Remote",
// "... with Nunchuk", "... with Classic Controller" and "Nintendo Wii U Pro
// Controller" from SDL's Wii driver; the Linux kernel's "Nintendo Wii Remote
// Classic Controller" is read like the Classic Controller.
Kind KindForName(const char* name) {
// Names come from SDL's hidapi Wii driver: "Nintendo Wii Remote",
// "Nintendo Wii Remote with Nunchuk", "Nintendo Wii Remote with Classic
// Controller" and "Nintendo Wii U Pro Controller".
// The kernel's Pro Controller is a plain gamepad with a built-in SDL mapping.
if (NameContains(name, "Wii Remote Pro Controller")) return Kind::NotWii;
if (NameContains(name, "Wii U Pro Controller")) return Kind::WiiUPro;
if (!NameContains(name, "Wii Remote")) return Kind::NotWii;
if (NameContains(name, "Nunchuk")) return Kind::RemoteWithNunchuk;
@@ -535,8 +736,7 @@ Kind KindForName(const char* name) {
Kind KindForPort(uint32_t port) {
if (port >= PAD_MAX_CONTROLLERS) return Kind::NotWii;
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(port));
if (gamepad == nullptr) return Kind::NotWii;
return KindForName(SDL_GetGamepadName(gamepad));
return KindForGamepad(gamepad);
}
// Human-readable name of a Kind for the settings overlay.
@@ -562,7 +762,7 @@ Kind EffectiveKind(uint32_t chan) {
if (chan >= PAD_MAX_CONTROLLERS) return Kind::NotWii;
PortMemory& memory = g_ports[chan];
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(chan));
const Kind live = gamepad != nullptr ? KindForName(SDL_GetGamepadName(gamepad)) : Kind::NotWii;
const Kind live = KindForGamepad(gamepad);
const uint64_t now = SDL_GetTicks();
if (live != Kind::NotWii) {
memory.lastKind = live;
@@ -588,7 +788,8 @@ bool IsRemoteChannel(uint32_t chan) {
// Marks KPAD-served ports as "no controller" in the GameCube pad statuses.
void HideRemotesFromPad(PADStatus* statuses, uint32_t count) {
for (uint32_t port = 0; port < count && port < PAD_MAX_CONTROLLERS; ++port) {
if (IsRemoteChannel(port)) {
if (IsRemoteChannel(port) ||
IsCarriedKernelNode(SDL_GetGamepadFromPlayerIndex(static_cast<int>(port)))) {
statuses[port] = {};
statuses[port].err = PAD_ERR_NO_CONTROLLER;
}
@@ -611,6 +812,14 @@ int16_t ClassicStickRaw(Sint16 axis, bool invert) {
return static_cast<int16_t>(std::clamp(std::lround(value / 64.0), -512L, 511L));
}
// The kernel driver's evdev axes span SDL's full range; scaled to WPAD's.
int16_t KernelClassicStickRaw(Sint16 axis, bool invert) {
float value = static_cast<float>(axis) / 32767.0f;
if (invert) value = -value;
value = std::clamp(value, -1.0f, 1.0f);
return static_cast<int16_t>(std::clamp(std::lround(value * 512.0f), -512L, 511L));
}
// KPAD's default Classic Controller stick limits; the game uses the cross clamp.
constexpr float kKpadStickMin = 60.0f;
constexpr float kKpadStickMax = 308.0f;
@@ -635,7 +844,7 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
return false;
}
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(chan));
const Kind kind = gamepad != nullptr ? KindForName(SDL_GetGamepadName(gamepad)) : Kind::NotWii;
const Kind kind = KindForGamepad(gamepad);
if (!IsKpadKind(kind)) {
const Kind remembered = EffectiveKind(chan);
if (!IsKpadKind(remembered)) {
@@ -651,26 +860,48 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
EnsureSensors(gamepad, chan);
sample = {};
const auto raw = [&](int index, uint32_t bit) {
if (SDL_GetJoystickButton(joystick, index)) sample.hold |= bit;
};
raw(kRawA, kWpadA);
raw(kRawB, kWpadB);
raw(kRawOne, kWpadOne);
raw(kRawTwo, kWpadTwo);
raw(kRawPlus, kWpadPlus);
raw(kRawMinus, kWpadMinus);
raw(kRawHome, kWpadHome);
raw(kRawDpadUp, kWpadUp);
raw(kRawDpadDown, kWpadDown);
raw(kRawDpadLeft, kWpadLeft);
raw(kRawDpadRight, kWpadRight);
// The kernel's evdev nodes have regular SDL mappings, so they are read as gamepads.
const bool kernel = IsKernelWiiNode(SDL_GetGamepadID(gamepad));
const bool classicNode = kernel && IsClassicNodeName(SDL_GetGamepadName(gamepad));
if (kernel) {
const auto mapped = [&](SDL_GamepadButton button, uint32_t bit) {
if (SDL_GetGamepadButton(gamepad, button)) sample.hold |= bit;
};
if (!classicNode) {
mapped(SDL_GAMEPAD_BUTTON_EAST, kWpadA);
mapped(SDL_GAMEPAD_BUTTON_SOUTH, kWpadB);
mapped(SDL_GAMEPAD_BUTTON_WEST, kWpadOne);
mapped(SDL_GAMEPAD_BUTTON_NORTH, kWpadTwo);
mapped(SDL_GAMEPAD_BUTTON_START, kWpadPlus);
mapped(SDL_GAMEPAD_BUTTON_BACK, kWpadMinus);
mapped(SDL_GAMEPAD_BUTTON_GUIDE, kWpadHome);
mapped(SDL_GAMEPAD_BUTTON_DPAD_UP, kWpadUp);
mapped(SDL_GAMEPAD_BUTTON_DPAD_DOWN, kWpadDown);
mapped(SDL_GAMEPAD_BUTTON_DPAD_LEFT, kWpadLeft);
mapped(SDL_GAMEPAD_BUTTON_DPAD_RIGHT, kWpadRight);
}
} else {
const auto raw = [&](int index, uint32_t bit) {
if (SDL_GetJoystickButton(joystick, index)) sample.hold |= bit;
};
raw(kRawA, kWpadA);
raw(kRawB, kWpadB);
raw(kRawOne, kWpadOne);
raw(kRawTwo, kWpadTwo);
raw(kRawPlus, kWpadPlus);
raw(kRawMinus, kWpadMinus);
raw(kRawHome, kWpadHome);
raw(kRawDpadUp, kWpadUp);
raw(kRawDpadDown, kWpadDown);
raw(kRawDpadLeft, kWpadLeft);
raw(kRawDpadRight, kWpadRight);
}
float rawG[3] = {};
const bool haveRaw = ReadSdlAccel(gamepad, SDL_SENSOR_ACCEL, rawG);
for (float& v : rawG) v /= kStandardGravity;
LastAcc& last = g_lastAcc[chan];
const bool accepted = ReadAccelAsKpad(gamepad, SDL_SENSOR_ACCEL, sample.acc);
const bool accepted = ReadAccelAsKpad(gamepad, chan, SDL_SENSOR_ACCEL, sample.acc);
if (accepted) {
last.valid = true;
for (int i = 0; i < 3; ++i) last.acc[i] = sample.acc[i];
@@ -690,8 +921,17 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
// Home, the L/R clicks as shoulders, the D-pad as buttons 11-14 (never
// through SDL's gamepad mapping, which expects a hat), ZL/ZR as the
// trigger axes.
const auto cl = [&](int index, uint32_t bit) {
if (SDL_GetJoystickButton(joystick, index)) sample.clHold |= bit;
SDL_Gamepad* classic = gamepad;
if (kernel && !classicNode) {
classic = KernelSibling(gamepad, true);
if (classic == nullptr) {
classic = gamepad;
}
}
const auto cl = [&](SDL_GamepadButton button, uint32_t bit) {
if (kernel ? SDL_GetGamepadButton(classic, button) : SDL_GetJoystickButton(joystick, button)) {
sample.clHold |= bit;
}
};
cl(SDL_GAMEPAD_BUTTON_EAST, kClA);
cl(SDL_GAMEPAD_BUTTON_SOUTH, kClB);
@@ -706,18 +946,29 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
cl(SDL_GAMEPAD_BUTTON_DPAD_DOWN, kClDown);
cl(SDL_GAMEPAD_BUTTON_DPAD_LEFT, kClLeft);
cl(SDL_GAMEPAD_BUTTON_DPAD_RIGHT, kClRight);
if (SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 0) sample.clHold |= kClZL;
if (SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > 0) sample.clHold |= kClZR;
const Sint16 lx = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
const Sint16 ly = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
const Sint16 rx = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTX);
const Sint16 ry = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTY);
sample.clLStickRaw[0] = ClassicStickRaw(lx, false);
sample.clLStickRaw[1] = ClassicStickRaw(ly, true);
sample.clRStickRaw[0] = ClassicStickRaw(rx, false);
sample.clRStickRaw[1] = ClassicStickRaw(ry, true);
ClassicStickFloat(sample.clLStickRaw, sample.clLStick);
ClassicStickFloat(sample.clRStickRaw, sample.clRStick);
if (SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 0) sample.clHold |= kClZL;
if (SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > 0) sample.clHold |= kClZR;
const Sint16 lx = SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_LEFTX);
const Sint16 ly = SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_LEFTY);
const Sint16 rx = SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_RIGHTX);
const Sint16 ry = SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_RIGHTY);
if (kernel) {
sample.clLStickRaw[0] = KernelClassicStickRaw(lx, false);
sample.clLStickRaw[1] = KernelClassicStickRaw(ly, true);
sample.clRStickRaw[0] = KernelClassicStickRaw(rx, false);
sample.clRStickRaw[1] = KernelClassicStickRaw(ry, true);
sample.clLStick[0] = std::clamp(static_cast<float>(lx) / 32767.0f, -1.0f, 1.0f);
sample.clLStick[1] = std::clamp(-static_cast<float>(ly) / 32767.0f, -1.0f, 1.0f);
sample.clRStick[0] = std::clamp(static_cast<float>(rx) / 32767.0f, -1.0f, 1.0f);
sample.clRStick[1] = std::clamp(-static_cast<float>(ry) / 32767.0f, -1.0f, 1.0f);
} else {
sample.clLStickRaw[0] = ClassicStickRaw(lx, false);
sample.clLStickRaw[1] = ClassicStickRaw(ly, true);
sample.clRStickRaw[0] = ClassicStickRaw(rx, false);
sample.clRStickRaw[1] = ClassicStickRaw(ry, true);
ClassicStickFloat(sample.clLStickRaw, sample.clLStick);
ClassicStickFloat(sample.clRStickRaw, sample.clRStick);
}
// Only the full-press click of L/R reaches SDL; report it as a full pull.
sample.clTriggerL = (sample.clHold & kClL) ? 255 : 0;
sample.clTriggerR = (sample.clHold & kClR) ? 255 : 0;
@@ -732,7 +983,7 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
sample.stick[1] = -static_cast<float>(SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY)) / 32767.0f;
for (float& v : sample.stick) v = std::clamp(v, -1.0f, 1.0f);
LastAcc& lastNunchuk = g_lastNunchukAcc[chan];
if (ReadAccelAsKpad(gamepad, SDL_SENSOR_ACCEL_L, sample.nunchukAcc)) {
if (ReadAccelAsKpad(gamepad, chan, SDL_SENSOR_ACCEL_L, sample.nunchukAcc)) {
lastNunchuk.valid = true;
for (int i = 0; i < 3; ++i) lastNunchuk.acc[i] = sample.nunchukAcc[i];
} else {
@@ -752,7 +1003,7 @@ bool ReadAccelDebug(uint32_t chan, float sdlG[3], float kpadAcc[3]) {
return false;
}
EnsureSensors(gamepad, chan);
if (!ReadAccelG(gamepad, SDL_SENSOR_ACCEL, sdlG)) {
if (!ReadAccelG(gamepad, chan, SDL_SENSOR_ACCEL, sdlG)) {
return false;
}
AccelGToKpad(sdlG, kpadAcc);
@@ -761,28 +1012,47 @@ bool ReadAccelDebug(uint32_t chan, float sdlG[3], float kpadAcc[3]) {
// Begins collecting rest samples from the remote on `chan`.
void StartAccelCalibration(uint32_t chan) {
if (!IsRemoteChannel(chan)) {
FinishAccelCalibration("No Wii Remote on this port.");
SDL_Gamepad* gamepad = RemoteGamepad(chan);
if (gamepad == nullptr) {
FinishAccelCalibration(chan, "No Wii Remote on this port.");
return;
}
g_calibration = {};
g_calibration.active = true;
g_calibration.chan = chan;
g_calibration.instance = SDL_GetGamepadID(gamepad);
g_calibrationMessage[0] = '\0';
}
// Drops the stored correction and goes back to SDL's raw reading.
void ClearAccelCalibration() {
g_calibration.active = false;
g_accelOffset = {};
g_accelOffsetLoaded = true;
RuntimeConfigFile::SetWiiAccelOffset({0.0, 0.0, 0.0});
std::snprintf(g_calibrationMessage, sizeof(g_calibrationMessage), "Calibration cleared.");
// Zeroes the stored correction of the remote on `chan`.
void ClearAccelCalibration(uint32_t chan) {
if (g_calibration.chan == chan) {
g_calibration.active = false;
}
SDL_Gamepad* gamepad = RemoteGamepad(chan);
if (gamepad == nullptr) {
SetCalibrationMessage(chan, "No Wii Remote on this port.");
return;
}
RemoteCalibration& calibration = CalibrationFor(gamepad, chan);
calibration.offset = {};
RuntimeConfigFile::SetWiiAccelOffsetFor(calibration.key, {0.0, 0.0, 0.0});
SetCalibrationMessage(chan, "Calibration cleared.");
}
// True while a calibration run is collecting samples.
bool IsAccelCalibrating() {
return g_calibration.active;
// Correction in effect for the remote on `chan`, for the overlay.
std::optional<std::array<double, 3>> ReadAccelOffset(uint32_t chan) {
SDL_Gamepad* gamepad = RemoteGamepad(chan);
if (gamepad == nullptr) {
return std::nullopt;
}
const RemoteCalibration& calibration = CalibrationFor(gamepad, chan);
return std::array<double, 3>{calibration.offset[0], calibration.offset[1], calibration.offset[2]};
}
// True while a calibration run is collecting samples from `chan`.
bool IsAccelCalibrating(uint32_t chan) {
return g_calibration.active && g_calibration.chan == chan;
}
// Share of the calibration samples collected so far, 0..1; 0 when idle.
@@ -790,9 +1060,14 @@ float AccelCalibrationProgress() {
return g_calibration.active ? static_cast<float>(g_calibration.count) / kCalibrationSamples : 0.0f;
}
// Outcome of the last calibration run for the overlay, or nullptr before any.
const char* AccelCalibrationMessage() {
return g_calibrationMessage[0] != '\0' ? g_calibrationMessage : nullptr;
// Outcome of the last calibration run on `chan` for the overlay, or nullptr.
const char* AccelCalibrationMessage(uint32_t chan) {
if (g_calibrationMessage[0] == '\0' || g_calibrationMessageChan != chan) {
return nullptr;
}
SDL_Gamepad* gamepad = RemoteGamepad(chan);
const SDL_JoystickID remote = gamepad != nullptr ? SDL_GetGamepadID(gamepad) : 0;
return remote == g_calibrationMessageRemote ? g_calibrationMessage : nullptr;
}
} // namespace WiiRemoteInput