Compare commits

...

8 Commits

Author SHA1 Message Date
patchzyy 37a0b68d85 keep steam desktop layout off while focused 2026-10-29 18:21:00 +01:00
FireDragon b748155cdb Update README.md to address NTSC-U and NTSC-J support added in v0.9.0 (#375)
* Update README.md

Added NTSC-U and NTSC-J to compatible ISOs. Changed "other regions" to NTSC-K

* Update README.md

Fixed typo and installation.

* Update README.md

Added NTSC-U and NTSC-J to line 130.

* Fixed "and" to "or" and line 130.
2026-10-29 16:08:09 +01:00
patchzyy eb99d8652b Fix wired PDP Afterglow Wave not being detected (#379)
* Fix wired PDP Afterglow Wave not being detected (#377)

Bumps aurora so the vendored SDL treats 0e6f:018b as input-only over USB
(patchzyy/aurora-wiicompiled#8).

* Point aurora at the merged PDP Afterglow Wave fix
2026-10-29 16:06:58 +01:00
patchzyy ee8443423d Add shake to trick (#378)
* add shake to trick

* address review

* add motion trick tests to ci

* reject bad sensor data under fast math

* harden bad data test

* widen motion trick test coverage
2026-10-29 14:41:06 +01:00
patchzyy 3645c4af1f Mask GX matrix indices to six bits (#376) 2026-10-29 11:17:53 +01:00
patchzyy ae54b6e082 skip escape exit prompt with valve controllers (#372) 2026-10-27 20:32:53 +01:00
Gitez af5a45e270 [Feature] Add aspect ratio selector for non-widescreen monitors (#339)
* Added aspect ratio selector

* Dirty 4:3 fix

* Fixed dynamic aspect in 4:3

* Cleaned up aspect ratio

* Fixed force aspect ratio

* More clean up

* use widescreen key, keep force_16_9 compat

* migrate legacy force_16_9 once at startup

---------

Co-authored-by: patchzyy <64382339+patchzyy@users.noreply.github.com>
2026-10-26 19:42:28 +01:00
Begood 896dcef9e3 macOS: pass explicit CMAKE_OSX_SYSROOT to the native configure (#362)
* macOS: pass explicit CMAKE_OSX_SYSROOT to the native configure

Fixes #353

## Problem
On recent macOS versions, the CMake bundled in Setup.app fails to detect the SDK, leaving `CMAKE_OSX_SYSROOT` empty. Regular compile steps still work (clang falls back to the default SDK), but libpng's `genout.cmake` step only passes `-isysroot` when `CMAKE_OSX_SYSROOT` is set, so `pnglibconf.out` generation fails with:

```
fatal error: 'sys/types.h' file not found
```

## Fix
Resolve the SDK path explicitly and pass it to the native configure step, so the build no longer depends on the bundled CMake's SDK detection. Also fails early with a clear message if the SDK cannot be resolved.

## Testing
Confirmed on MacBook Pro M1 (macOS 27.0.1): the workaround (setting `CMAKE_OSX_SYSROOT` manually) completes the build successfully; this PR bakes the same fix into the build script.

* fix sysroot line and check sdk dir

---------

Co-authored-by: patchzyy <64382339+patchzyy@users.noreply.github.com>
2026-10-26 17:14:54 +01:00
21 changed files with 1575 additions and 104 deletions
+1
View File
@@ -102,6 +102,7 @@ jobs:
mkw_nand_settings_tests \
mkw_sc_serial_tests \
mkw_input_expr_tests \
mkw_motion_trick_tests \
mkw_bluetooth_controller_tests \
mkw_macos_native_compile \
mkw_macos_context_abi_tests \
+2 -1
View File
@@ -64,6 +64,7 @@ done
macos_arch=$(uname -m)
case "$macos_arch" in arm64|x86_64) ;; *) fail "unsupported macOS architecture: $macos_arch" ;; esac
macos_deployment_target=12.0
macos_sysroot=$(xcrun --show-sdk-path 2>/dev/null) && [[ -d "$macos_sysroot" ]] || fail 'could not resolve the macOS SDK; install the Xcode Command Line Tools (xcode-select --install)'
workspace=$(cd "$workspace" && pwd)
if [[ -z "$setup_version" && -f "$workspace/Launcher/Directory.Build.props" ]]; then
setup_version=$(sed -nE 's/.*<Version>([^<]+)<\/Version>.*/\1/p' "$workspace/Launcher/Directory.Build.props" | head -n 1)
@@ -186,7 +187,7 @@ fi
step configure-native 'Configuring the native toolchain'
# Use Aurora's pinned SDL3 source on macOS. A system SDL3 can be older than
# Aurora's required API even when find_package() succeeds.
"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER="$cc_bin" -DCMAKE_CXX_COMPILER="$cxx_bin" -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DCMAKE_OSX_ARCHITECTURES="$macos_arch" -DCMAKE_OSX_DEPLOYMENT_TARGET="$macos_deployment_target" -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs" -DMKW_X86_CPU_PROFILE="$x86_cpu_profile" -DAURORA_SDL3_PROVIDER=vendor -DMKW_SETUP_VERSION="$setup_version"
"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER="$cc_bin" -DCMAKE_CXX_COMPILER="$cxx_bin" -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DCMAKE_OSX_ARCHITECTURES="$macos_arch" -DCMAKE_OSX_DEPLOYMENT_TARGET="$macos_deployment_target" -DCMAKE_OSX_SYSROOT="$macos_sysroot" -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs" -DMKW_X86_CPU_PROFILE="$x86_cpu_profile" -DAURORA_SDL3_PROVIDER=vendor -DMKW_SETUP_VERSION="$setup_version"
mkw_write_identity "$toolchain_provenance" ToolchainFingerprint "$toolchain_fingerprint"
targets=(); [[ "$profile" != retro-rewind ]] && targets+=(WiiCompiled); [[ "$profile" != base ]] && targets+=(RetroRewind)
step compile "Compiling ${targets[*]} locally"; "$cmake_bin" --build "$native_build" --target "${targets[@]}" --parallel "$global_jobs"
+6 -7
View File
@@ -22,7 +22,7 @@ anywhere at runtime.
> [!IMPORTANT]
> There is no Nintendo code, no assets and no game data anywhere in this project or its releases.
> You need your own legally dumped copy of the PAL version of the game. Setup only ships the
> You need your own legally dumped copy of the PAL, NTSC-U, or NTSC-J version of the game. Setup only ships the
> toolchain, the translation runs on your machine against your disc image, and nothing ever gets
> uploaded.
@@ -103,14 +103,13 @@ Known limitations of the Wii Remote path:
- About 20 GB of free disk space during installation (Final game size ~5 GB)
- macOS 12 (Monterey) or later on Apple Silicon (`arm64`) or Intel (`x86_64-v3`); pre-Haswell Intel CPUs are unsupported
- On macOS, a Metal-capable GPU and Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing)
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
- A clean, unmodified **PAL `RMCP01`**, **NTSC-U `RMCE01`**, or **NTSC-J `RMCJ01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
GCZ, CISO, WBFS, WIA and RVZ are accepted.
> [!NOTE]
> GPU/CPU minimums are set by driver support and D3D12/Vulkan feature requirements, not by the game's actual demands.
Only the clean PAL revision will work. Anything else (other
regions, patched executables) is rejected outright.
Only the clean PAL, NTSC-U, and NTSC-J revisions will work. Anything else (NTSC-K, patched executables) is rejected outright.
> [!NOTE]
> Nobody here will tell you where to get the game. Dumping your own disc is on you, and links to
@@ -118,7 +117,7 @@ regions, patched executables) is rejected outright.
## Installing
For an easy experience, use [Wheel Wizard](https://github.com/TeamWheelWizard/WheelWizard). Pick your clean PAL `RMCP01`
For an easy experience, use [Wheel Wizard](https://github.com/TeamWheelWizard/WheelWizard). Pick your clean PAL `RMCP01`, NTSC-U `RMCE01`, or NTSC-J `RMCJ01`
image under Settings, turn on **WiiCompiled (beta)**, and hit install from the Home page.
Wheel Wizard downloads the setup tool from this repo and walks you through install, updates and
launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
@@ -128,7 +127,7 @@ launching. The backend itself is deliberately command-line only, Wheel Wizard is
Download `WiiCompiled-Setup.pkg` from this repository's Releases page and open it. The universal
package selects the appropriate bundled tools for the host architecture, supporting both Apple Silicon (`arm64`)
and Intel (`x86_64`) Macs. It installs **WiiCompiled Setup** in Applications; open that app, choose
your clean PAL `RMCP01` disc image, and select either the base game or Retro Rewind. For Retro Rewind, choose the `RetroRewind6` folder
your clean PAL `RMCP01`, NTSC-U `RMCE01`, or NTSC-J `RMCJ01` disc image, and select either the base game or Retro Rewind. For Retro Rewind, choose the `RetroRewind6` folder
or its parent folder.
Setup verifies and extracts the image locally, then translates and compiles the native app on your
@@ -196,7 +195,7 @@ don't want to risk it right now, setup has to run a static recompiler over the w
and then throw a C++ compiler at the result. It's a **one-time cost** on your machine.
**Which game version works?**
Clean PAL `RMCP01`. Other regions and modified executables are **rejected**. Translating
Clean PAL `RMCP01`, NTSC-U `RMCE01`, and NTSC-J `RMCJ01`. NTSC-K `RMCK01` and modified executables are **rejected**. Translating
them against the wrong manifest would give you a subtly broken game that's miserable to debug for us.
**Can I recompile other GameCube/Wii games with it?**
+9
View File
@@ -63,6 +63,15 @@ target_include_directories(mkw_input_expr_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}
target_compile_features(mkw_input_expr_tests PRIVATE cxx_std_17)
add_test(NAME mkw_input_expr_tests COMMAND mkw_input_expr_tests)
add_executable(mkw_motion_trick_tests
"${MKW_TEST_RUNTIME_DIR}/tests/motion_trick_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/motion_trick_detector.cpp")
target_include_directories(mkw_motion_trick_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_motion_trick_tests PRIVATE cxx_std_17)
# Match the runtime's float model so the bad-data guard is tested as shipped.
target_compile_options(mkw_motion_trick_tests PRIVATE $<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-ffast-math>)
add_test(NAME mkw_motion_trick_tests COMMAND mkw_motion_trick_tests)
# The emulated Bluetooth controller is pure host code.
add_executable(mkw_bluetooth_controller_tests
"${MKW_TEST_RUNTIME_DIR}/tests/bluetooth_controller_tests.cpp"
+5 -3
View File
@@ -2,6 +2,7 @@
#include "friend_code_keyboard.h"
#include "settings_overlay.h"
#include "steam_input_layout.h"
#include "runtime_config.h"
#include "gx_worker.h"
#include "perf_stats.h"
@@ -19,9 +20,8 @@
#endif
extern "C" bool g_dynamicAspectRatioEnabled;
void ConfigureMkwDynamicAspect(bool widescreen, bool forceAspect169, uint32_t surfaceWidth, uint32_t surfaceHeight);
void SetMkwForceAspect169(bool enabled);
bool MkwForceAspect169Requested();
void ConfigureMkwDynamicAspect(bool widescreen, bool forceAspect, uint32_t surfaceWidth, uint32_t surfaceHeight);
void SetMkwForceAspect(bool enabled);
void UpdateMkwDynamicAspectSurface(uint32_t surfaceWidth, uint32_t surfaceHeight);
// Arms the "keep EGG::Frustum's projection scale" flag on every screen that
// renders to a fixed-size offscreen target. Cheap and idempotent; called from
@@ -64,6 +64,7 @@ inline void Flush(bool force = false) {
// process directly and do not run the crash/atexit paths.
[[noreturn]] inline void ExitForAuroraWindowClose() noexcept {
settings_overlay::ReleaseControllers();
SteamInputLayout::Release();
WindowPlacementPersistence::Flush(true);
#if defined(_WIN32)
::ExitProcess(0);
@@ -121,6 +122,7 @@ inline void ProcessAuroraEvents(const AuroraEvent* events) {
}
settings_overlay::HandleEvents(events);
FriendCodeKeyboard::HandleEvents(events);
SteamInputLayout::HandleEvents(events);
}
inline void ApplyPendingMkwDynamicAspectSurface() {
+36
View File
@@ -0,0 +1,36 @@
#pragma once
// Shake to trick: a flick of a controller's motion sensor briefly presses the
// matching D-pad direction, which the game treats as a trick (or a wheelie on
// bikes), like shaking a Wii Remote.
#include <cstdint>
#include <dolphin/pad.h>
#include "motion_trick_detector.h"
namespace MotionTrick {
bool Enabled() noexcept;
void SetEnabled(bool enabled) noexcept;
int Sensitivity() noexcept;
void SetSensitivity(int level) noexcept;
// The controller on this port reports a gyro or accelerometer of its own.
bool PortHasMotion(uint32_t port) noexcept;
// Call once per guest PADRead, after every other input source has been merged.
void Apply(PADStatus* statuses) noexcept;
struct PortStatus {
float intensity = 0.0f; // decaying peak, 1 being the trigger threshold
Direction lastTrick = Direction::None;
uint64_t lastTrickUs = 0;
bool sensorsUnavailable = false; // the controller refused to turn its sensors on
};
PortStatus Status(uint32_t port) noexcept;
uint64_t NowUs() noexcept;
} // namespace MotionTrick
+43
View File
@@ -0,0 +1,43 @@
#pragma once
// Shake detection for controllers with motion sensors. Pure host code, so it is
// unit-testable without SDL.
#include <cstdint>
namespace MotionTrick {
enum class Direction : uint8_t { None, Up, Down, Left, Right };
inline constexpr int kMinSensitivity = 1;
inline constexpr int kMaxSensitivity = 5;
inline constexpr int kDefaultSensitivity = 3;
// SDL gamepad sensor axes: x right, y top, z toward the player.
struct MotionSample {
bool hasGyro = false;
bool hasAccel = false;
float gyro[3]{}; // rad/s: pitch, yaw, roll
float accel[3]{}; // m/s^2, gravity included
};
// Fed one sample per game frame; reports at most one trick per flick.
class ShakeDetector {
public:
ShakeDetector() { SetSensitivity(kDefaultSensitivity); }
void SetSensitivity(int level);
Direction Update(const MotionSample& sample, uint64_t nowUs);
// Strength of the last sample, 1 being the trigger threshold.
float Intensity() const { return m_intensity; }
private:
float m_gyroThreshold = 0.0f;
float m_accelThreshold = 0.0f;
float m_intensity = 0.0f;
bool m_armed = true;
uint64_t m_lastFireUs = 0;
};
} // namespace MotionTrick
+82 -8
View File
@@ -33,7 +33,8 @@
struct RuntimeUserConfig {
std::optional<bool> widescreen;
std::optional<bool> forceAspect169;
std::optional<bool> forceAspect;
bool legacyForceAspect = false;
std::optional<int32_t> windowPosX;
std::optional<int32_t> windowPosY;
std::optional<uint32_t> windowWidth;
@@ -69,6 +70,10 @@ struct RuntimeUserConfig {
std::optional<int32_t> wiiIrSensitivity;
std::optional<int32_t> wiiSpeakerVolume;
std::optional<bool> wiiRumble;
// Outside a Steam launch, ask a running Steam client for a game controller layout while
// the window has focus instead of its desktop one (buttons bound to mouse and keyboard).
std::optional<bool> steamInputOverride;
std::optional<uint32_t> steamInputAppId;
std::optional<bool> networkEnabled;
std::optional<bool> discordPresenceEnabled;
// The application ID of the WiiCompiled Discord application. This is only
@@ -86,6 +91,9 @@ struct RuntimeUserConfig {
// "dpad_up,left_shoulder") as values; pressing either bound button counts.
std::array<std::optional<std::string>, 12> controllerButtons;
std::optional<bool> rumbleEnabled;
// Shake a motion controller to press the D-pad, like shaking a Wii Remote.
std::optional<bool> motionTrick;
std::optional<int32_t> motionTrickSensitivity;
std::optional<int32_t> muteHotkey;
std::map<std::string, std::string> controllerExpressions;
};
@@ -271,7 +279,7 @@ inline void EnsureConfigFile() {
"# Set paths.dvd_root to an extracted Mario Kart Wii DATA directory.\n\n"
"[video]\n"
"widescreen = true\n"
"force_16_9 = false\n"
"force_aspect = false\n"
"resolution_multiplier = 1.0\n"
"frame_interpolation_fps = 0\n"
"display_mode = \"windowed\"\n"
@@ -304,6 +312,12 @@ inline void EnsureConfigFile() {
"mix_worker = true\n\n"
"[network]\n"
"enabled = true\n\n"
"[controller]\n"
"# Outside a Steam launch, Steam's desktop layout binds controller buttons\n"
"# to mouse and keyboard. While the game has focus, ask a running Steam\n"
"# client to use the game layout of steam_input_app_id instead.\n"
"steam_input_override = true\n"
"# steam_input_app_id = 480\n\n"
"[discord]\n"
"# Rich Presence talks only to a locally-running Discord client.\n"
"# Retro Rewind supplies its official app ID automatically. Set this\n"
@@ -389,6 +403,10 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
}
config.rumbleEnabled = FindConfigValue<bool>(document, "controller", "rumble");
config.motionTrick = FindConfigValue<bool>(document, "controller", "motion_trick");
if (const auto value = FindConfigInt(document, "controller", "motion_trick_sensitivity"); value && *value >= 1 && *value <= 5) {
config.motionTrickSensitivity = *value;
}
if (auto value = FindConfigInt(document, "audio", "mute_key")) {
config.muteHotkey = *value;
}
@@ -403,7 +421,15 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
}
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
config.forceAspect169 = FindConfigValue<bool>(document, "video", "force_16_9");
config.forceAspect = FindConfigValue<bool>(document, "video", "force_aspect");
if (!config.forceAspect) {
// Legacy key; it always forced a 16:9 game.
config.forceAspect = FindConfigValue<bool>(document, "video", "force_16_9");
config.legacyForceAspect = config.forceAspect.has_value();
if (config.forceAspect.value_or(false)) {
config.widescreen = true;
}
}
config.windowPosX = FindConfigInt(document, "video", "window_x");
config.windowPosY = FindConfigInt(document, "video", "window_y");
if (auto value = FindConfigUint(document, "video", "window_width"); value && *value != 0) {
@@ -473,6 +499,10 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.wiiSpeakerVolume = *value;
}
config.wiiRumble = FindConfigValue<bool>(document, "controller", "wii_rumble");
config.steamInputOverride = FindConfigValue<bool>(document, "controller", "steam_input_override");
if (const auto value = FindConfigUint(document, "controller", "steam_input_app_id"); value && *value != 0) {
config.steamInputAppId = *value;
}
config.networkEnabled = FindConfigValue<bool>(document, "network", "enabled");
config.discordPresenceEnabled = FindConfigValue<bool>(document, "discord", "enabled");
config.discordClientId = FindConfigValue<std::string>(document, "discord", "client_id");
@@ -692,6 +722,24 @@ inline bool SetRumbleEnabled(bool value) {
return WriteSetting("controller", "rumble", value ? "true" : "false");
}
inline bool MotionTrickEnabled(bool fallback = false) {
return Get().motionTrick.value_or(fallback);
}
inline bool SetMotionTrickEnabled(bool value) {
Mutable().motionTrick = value;
return WriteSetting("controller", "motion_trick", value ? "true" : "false");
}
inline int32_t MotionTrickSensitivity(int32_t fallback = 3) {
return Get().motionTrickSensitivity.value_or(fallback);
}
inline bool SetMotionTrickSensitivity(int32_t value) {
Mutable().motionTrickSensitivity = value;
return WriteSetting("controller", "motion_trick_sensitivity", std::to_string(value));
}
inline int32_t MuteHotkey(int32_t fallback) {
return Get().muteHotkey.value_or(fallback);
}
@@ -760,13 +808,30 @@ inline bool WidescreenEnabled(bool fallback = false) {
return Get().widescreen.value_or(fallback);
}
inline bool ForceAspect169Enabled(bool fallback = false) {
return Get().forceAspect169.value_or(fallback);
inline bool ForceAspectEnabled(bool fallback = false) {
return Get().forceAspect.value_or(fallback);
}
inline bool SetForceAspect169(bool value) {
Mutable().forceAspect169 = value;
return WriteSetting("video", "force_16_9", value ? "true" : "false");
inline bool SetForceAspect(bool value) {
Mutable().forceAspect = value;
return WriteSetting("video", "force_aspect", value ? "true" : "false");
}
// The in-memory value stays as booted; SCGetAspectRatio must not change mid-session.
inline bool SetWidescreen(bool value) {
return WriteSetting("video", "widescreen", value ? "true" : "false");
}
// Rewrites a legacy force_16_9 as force_aspect plus the widescreen it implied.
inline void MigrateLegacyForceAspect() {
if (!Get().legacyForceAspect) {
return;
}
const bool force = ForceAspectEnabled();
if ((!force || WriteSetting("video", "widescreen", "true")) &&
WriteSetting("video", "force_aspect", force ? "true" : "false")) {
Mutable().legacyForceAspect = false;
}
}
inline bool WindowPosition(int32_t& x, int32_t& y) {
@@ -844,6 +909,15 @@ inline bool SetWiiRemotesEnabled(bool value) {
return true;
}
inline bool SteamInputOverrideEnabled(bool fallback = true) {
return Get().steamInputOverride.value_or(fallback);
}
// Spacewar, Valve's Steamworks sample app, available to every Steam account.
inline uint32_t SteamInputAppId(uint32_t fallback = 480) {
return Get().steamInputAppId.value_or(fallback);
}
inline bool WiiAutoPairEnabled(bool fallback = true) {
return Get().wiiAutoPair.value_or(fallback);
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <aurora/event.h>
#include <cstdint>
// Outside a Steam launch, Steam applies its desktop controller layout, which binds
// controller buttons to mouse and keyboard actions. While the game window has focus,
// a running Steam client is asked to apply the given app's game layout instead.
namespace SteamInputLayout {
void Initialize(bool enabled, uint32_t appId);
void HandleEvents(const AuroraEvent* events);
// Restores the desktop layout. Idempotent; safe to call from any exit path.
void Release() noexcept;
} // namespace SteamInputLayout
+34 -54
View File
@@ -14,10 +14,9 @@ extern "C" int g_gxFrameCount;
namespace {
bool g_widescreenConfigured = false;
bool g_widescreenSetting = false;
bool g_forceAspect169 = false;
std::atomic_bool g_requestedForceAspect169{false};
bool g_widescreen = true;
bool g_forceAspect = false;
std::atomic_bool g_requestedForceAspect{false};
bool g_policyDirty = false;
uint32_t g_lastEggWidth43 = 0;
uint32_t g_lastEggWidth169 = 0;
@@ -130,19 +129,20 @@ void WriteEggScreenRecord(uint32_t recordAddr, uint32_t width) {
void ApplyEggScreenRecords(uint32_t surfaceWidth, uint32_t surfaceHeight) {
using namespace MkwDynamicAspect;
if (g_forceAspect) {
surfaceWidth = g_widescreen ? 16u : 4u;
surfaceHeight = g_widescreen ? 9u : 3u;
}
const float referenceAspect = g_widescreen ? kPresentationAspect169 : kPresentationAspect43;
const uint32_t width43 =
g_widescreenConfigured ? EggRecordWidth(kEggRecordWidth43, surfaceWidth, surfaceHeight)
: kEggRecordWidth43;
EggRecordWidth(kEggRecordWidth43, surfaceWidth, surfaceHeight, referenceAspect);
const uint32_t width169 =
g_widescreenConfigured ? EggRecordWidth(kEggRecordWidth169, surfaceWidth, surfaceHeight)
: kEggRecordWidth169;
EggRecordWidth(kEggRecordWidth169, surfaceWidth, surfaceHeight);
WriteEggScreenRecord(kEggScreenRecord43, width43);
WriteEggScreenRecord(kEggScreenRecord169, width169);
Memory::WriteFloat32(kEggScreenProjScaleX, 1.0f);
Memory::WriteFloat32(kEggScreenProjScaleY,
g_widescreenConfigured
? VerticalExpansion(surfaceWidth, surfaceHeight)
: 1.0f);
VerticalExpansion(surfaceWidth, surfaceHeight, referenceAspect));
AssertOffscreenScreenBypass();
if (width43 == g_lastEggWidth43 && width169 == g_lastEggWidth169) {
return;
@@ -172,58 +172,38 @@ void AssertMkwOffscreenScreenBypass() {
}
void UpdateMkwDynamicAspectSurface(uint32_t surfaceWidth, uint32_t surfaceHeight) {
const bool requestedForceAspect169 = g_requestedForceAspect169.load(std::memory_order_acquire);
if (g_forceAspect169 != requestedForceAspect169) {
g_forceAspect169 = requestedForceAspect169;
g_widescreenConfigured = g_widescreenSetting || g_forceAspect169;
g_dynamicAspectRatioEnabled = g_widescreenConfigured;
const bool requestedForceAspect = g_requestedForceAspect.load(std::memory_order_acquire);
if (g_forceAspect != requestedForceAspect) {
g_forceAspect = requestedForceAspect;
g_policyDirty = true;
}
if (surfaceWidth == 0 || surfaceHeight == 0) {
return;
}
if (!g_widescreenConfigured) {
if (g_policyDirty) {
if (g_policyDirty) {
if (g_forceAspect && !g_widescreen) {
AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT);
VILockAspectRatio(4, 3);
ApplyEggScreenRecords(surfaceWidth, surfaceHeight);
g_policyDirty = false;
} else {
AuroraSetViewportPolicy(g_forceAspect ? AURORA_VIEWPORT_16_9 : AURORA_VIEWPORT_STRETCH);
VIUnlockAspectRatio();
}
return;
}
if (g_policyDirty) {
VIUnlockAspectRatio();
g_policyDirty = false;
}
AuroraSetViewportPolicy(g_forceAspect169 ? AURORA_VIEWPORT_16_9 : AURORA_VIEWPORT_STRETCH);
ApplyEggScreenRecords(g_forceAspect169 ? 16u : surfaceWidth,
g_forceAspect169 ? 9u : surfaceHeight);
}
void SetMkwForceAspect169(bool enabled) {
g_requestedForceAspect169.store(enabled, std::memory_order_release);
}
bool MkwForceAspect169Requested() {
return g_requestedForceAspect169.load(std::memory_order_acquire);
}
void ConfigureMkwDynamicAspect(bool widescreen, bool forceAspect169, uint32_t surfaceWidth, uint32_t surfaceHeight) {
g_widescreenSetting = widescreen;
g_widescreenConfigured = widescreen || forceAspect169;
g_forceAspect169 = forceAspect169;
g_requestedForceAspect169.store(forceAspect169, std::memory_order_relaxed);
g_dynamicAspectRatioEnabled = g_widescreenConfigured;
g_lastEggWidth43 = 0;
g_lastEggWidth169 = 0;
if (g_widescreenConfigured) {
VIUnlockAspectRatio();
ApplyEggScreenRecords(forceAspect169 ? 16u : surfaceWidth,
forceAspect169 ? 9u : surfaceHeight);
return;
}
AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT);
VILockAspectRatio(4, 3);
ApplyEggScreenRecords(surfaceWidth, surfaceHeight);
}
void SetMkwForceAspect(bool enabled) {
g_requestedForceAspect.store(enabled, std::memory_order_release);
}
void ConfigureMkwDynamicAspect(bool widescreen, bool forceAspect, uint32_t surfaceWidth, uint32_t surfaceHeight) {
g_widescreen = widescreen;
g_forceAspect = forceAspect;
g_requestedForceAspect.store(forceAspect, std::memory_order_relaxed);
g_policyDirty = true;
g_dynamicAspectRatioEnabled = true;
g_lastEggWidth43 = 0;
g_lastEggWidth169 = 0;
ApplyEggScreenRecords(surfaceWidth, surfaceHeight);
}
+20 -16
View File
@@ -13,6 +13,7 @@
namespace MkwDynamicAspect {
inline constexpr float kPresentationAspect169 = 16.0f / 9.0f;
inline constexpr float kPresentationAspect43 = 4.0f / 3.0f;
inline constexpr float kEggFramebufferWidth = 608.0f;
inline constexpr uint32_t kEggScreenHeight = 456u;
inline constexpr uint32_t kEggRecordWidth43 = 608u;
@@ -31,32 +32,35 @@ inline float SurfaceAspect(uint32_t width, uint32_t height) noexcept {
static_cast<float>(width) / static_cast<float>(height));
}
// surfaceAspect / (16:9). Above 1 the surface is wider than 16:9, below 1 it
// is tighter (4:3, portrait).
inline float AspectRatioFactor(uint32_t width, uint32_t height) noexcept {
return SurfaceAspect(width, height) / kPresentationAspect169;
// surfaceAspect / referenceAspect. Above 1 the surface is wider than the
// reference, below 1 it is tighter (4:3, portrait).
inline float AspectRatioFactor(uint32_t width, uint32_t height,
float referenceAspect = kPresentationAspect169) noexcept {
return SurfaceAspect(width, height) / referenceAspect;
}
// Hor+ factor, clamped at 1 so a tighter-than-16:9 surface never narrows the
// canvas (that would crop away scene the 16:9 view showed).
inline float HorizontalExpansion(uint32_t width, uint32_t height) noexcept {
return std::max(1.0f, AspectRatioFactor(width, height));
// Hor+ factor, clamped at 1 so a tighter surface never narrows the canvas
// (that would crop away scene the reference view showed).
inline float HorizontalExpansion(uint32_t width, uint32_t height,
float referenceAspect = kPresentationAspect169) noexcept {
return std::max(1.0f, AspectRatioFactor(width, height, referenceAspect));
}
// Vert+ factor, clamped at 1 so a wider-than-16:9 surface never shortens the
// frustum. Exactly one of the two expansions is ever greater than 1.
inline float VerticalExpansion(uint32_t width, uint32_t height) noexcept {
const float factor = AspectRatioFactor(width, height);
// Vert+ factor, clamped at 1 so a wider surface never shortens the frustum.
// Exactly one of the two expansions is ever greater than 1.
inline float VerticalExpansion(uint32_t width, uint32_t height,
float referenceAspect = kPresentationAspect169) noexcept {
const float factor = AspectRatioFactor(width, height, referenceAspect);
if (!(factor > 0.0f)) {
return 1.0f;
}
return std::clamp(1.0f / factor, 1.0f, kMaxVerticalExpansion);
}
inline uint32_t EggRecordWidth(uint32_t baseWidth, uint32_t surfaceWidth,
uint32_t surfaceHeight) noexcept {
const float scaledWidth =
static_cast<float>(baseWidth) * HorizontalExpansion(surfaceWidth, surfaceHeight);
inline uint32_t EggRecordWidth(uint32_t baseWidth, uint32_t surfaceWidth, uint32_t surfaceHeight,
float referenceAspect = kPresentationAspect169) noexcept {
const float scaledWidth = static_cast<float>(baseWidth) *
HorizontalExpansion(surfaceWidth, surfaceHeight, referenceAspect);
return std::clamp<uint32_t>(
static_cast<uint32_t>(std::lround(scaledWidth / 4.0f)) * 4u, 4u, 0xFFFCu);
}
+2
View File
@@ -2,6 +2,7 @@
#include "memory.h"
#include "hle/controller_status_contract.h"
#include "input_bindings.h"
#include "motion_trick.h"
#include <algorithm>
#include <atomic>
@@ -116,6 +117,7 @@ extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr)
FillTriggersHeldByButtons(statuses);
InputBindings::Apply(statuses);
MotionTrick::Apply(statuses);
try {
for (uint32_t i = 0; i < PAD_CHANMAX; ++i) {
+1 -1
View File
@@ -53,7 +53,7 @@ PPC_NATIVE_OVERRIDE(801B0220, SCCheckStatus_HLE, uint32_t, (), ());
// Returns: 0 = 4:3, 1 = 16:9
extern "C" uint32_t SCGetAspectRatio_HLE()
{
return (RuntimeConfigFile::WidescreenEnabled(true) || MkwForceAspect169Requested()) ? 1u : 0u;
return RuntimeConfigFile::WidescreenEnabled(true) ? 1u : 0u;
}
PPC_NATIVE_OVERRIDE(801B1BE4, SCGetAspectRatio_HLE, uint32_t, (), ());
+25 -7
View File
@@ -41,6 +41,7 @@
#include <mmsystem.h>
#include <dbghelp.h>
#else
#include <fcntl.h>
#include <signal.h>
#if defined(__x86_64__)
// Only the x86 POSIX fault path inspects ucontext_t to recover the page-fault
@@ -68,6 +69,7 @@
#include "aurora_events.h"
#include "wiimote_host.h"
#include "discord_presence.h"
#include "steam_input_layout.h"
#include "fiber_manager.h"
#include "gx_worker.h"
#include "hle_stubs.h"
@@ -337,6 +339,9 @@ bool InstallTranscriptPipe(int& outReadFd, int& outWriteFd, int targetFd) {
if (::pipe(pipeFds) != 0) {
return false;
}
// Spawned helpers must not keep the transcript open; its reader waits for EOF on shutdown.
::fcntl(pipeFds[0], F_SETFD, FD_CLOEXEC);
::fcntl(pipeFds[1], F_SETFD, FD_CLOEXEC);
#endif
outReadFd = pipeFds[0];
@@ -1098,6 +1103,7 @@ LONG ReportFatalSehAndExit(EXCEPTION_POINTERS* info) {
<< " at host address 0x" << record->ExceptionInformation[1];
}
popupDetails << ".\n\nThe process transcript and crash log contain the full CPU and stack diagnostics.";
SteamInputLayout::Release();
ShowRuntimeFatalPopup("a native crash occurred", popupDetails.str());
DumpHostStackTrace();
@@ -1198,6 +1204,7 @@ void PosixMemoryFaultHandler(int sig, siginfo_t* info, void* ucontextVoid) {
}
popupDetails << ".\n\nThe process transcript and crash log contain the full CPU and stack "
"diagnostics.";
SteamInputLayout::Release();
ShowRuntimeFatalPopup("a native crash occurred", popupDetails.str());
DumpHostStackTrace();
WriteFatalLogImpl(sig == SIGBUS ? "sigbus" : "sigsegv");
@@ -1231,6 +1238,7 @@ void AbortSignalHandler(int signum) {
// abort() bypasses atexit, so the guest-memory fault summary has to be
// emitted here too. It is idempotent, so a later AtExitHandler is a no-op.
GuestFlat::LogFaultSummary();
SteamInputLayout::Release();
ShowRuntimeFatalPopup("a fatal internal error occurred",
"The process called abort while running the game or Aurora renderer.\n\n"
@@ -1266,6 +1274,7 @@ void SetRuntimeExitCode(int code) {
// Global handler called via atexit() to flush buffers before any exit
static void AtExitHandler() {
SteamInputLayout::Release();
// End-of-run guest memory report. This runs before the fatal-report check
// below because the counters describe the whole session and are just as
// interesting after a crash as after a clean exit.
@@ -1290,6 +1299,7 @@ static void AtExitHandler() {
// Global terminate handler for uncaught exceptions
static void TerminateHandler() {
SteamInputLayout::Release();
// Skip detailed dump if already reported
if (g_fatalErrorReported.load(std::memory_order_acquire)) {
std::fflush(stderr);
@@ -1391,9 +1401,10 @@ int RuntimeMain(int argc, char** argv) {
auroraConfig.cachePath = auroraCachePath.c_str();
auroraConfig.logCallback = &RuntimeAuroraLogCallback;
auroraConfig.logLevel = LOG_DEBUG;
RuntimeConfigFile::MigrateLegacyForceAspect();
const bool configWidescreen = RuntimeConfigFile::WidescreenEnabled(true);
const bool forceAspect169 = RuntimeConfigFile::ForceAspect169Enabled();
auroraConfig.windowWidth = (configWidescreen || forceAspect169) ? 854 : 640;
const bool forceAspect = RuntimeConfigFile::ForceAspectEnabled();
auroraConfig.windowWidth = configWidescreen ? 854 : 640;
auroraConfig.windowHeight = 480;
auroraConfig.windowWidth = RuntimeConfigFile::WindowWidth(auroraConfig.windowWidth);
auroraConfig.windowHeight = RuntimeConfigFile::WindowHeight(auroraConfig.windowHeight);
@@ -1412,7 +1423,7 @@ int RuntimeMain(int argc, char** argv) {
auroraConfig.vsync = RuntimeConfigFile::Vsync();
auroraConfig.desiredBackend = BACKEND_AUTO;
const float resolutionMultiplier = RuntimeConfigFile::ResolutionMultiplier(1.0f);
ConfigureMkwDynamicAspect(configWidescreen, forceAspect169,
ConfigureMkwDynamicAspect(configWidescreen, forceAspect,
auroraConfig.windowWidth, auroraConfig.windowHeight);
VISetFrameBufferScale(resolutionMultiplier);
// One table for both directions. RuntimeConfigFile::IsSupportedGraphicsApi
@@ -1479,19 +1490,23 @@ int RuntimeMain(int argc, char** argv) {
UpdateMkwDynamicAspectSurface(auroraInfo.windowSize.native_fb_width,
auroraInfo.windowSize.native_fb_height);
settings_overlay::InitializeRuntimeSettings();
if (!g_headless) {
SteamInputLayout::Initialize(RuntimeConfigFile::SteamInputOverrideEnabled(),
RuntimeConfigFile::SteamInputAppId());
}
if (BootRace::Requested()) {
aurora_set_skip_unready_pipelines(false);
}
RT_LOG(RT_TAG_CONFIG) << "video.widescreen=" << (configWidescreen ? "true" : "false")
<< " force_16_9=" << (forceAspect169 ? "true" : "false")
<< " SCGetAspectRatio=" << (configWidescreen || forceAspect169 ? 1 : 0)
<< " force_aspect=" << (forceAspect ? "true" : "false")
<< " SCGetAspectRatio=" << (configWidescreen ? 1 : 0)
<< " resolutionMultiplier=" << resolutionMultiplier
<< " window=" << auroraInfo.windowSize.width << "x" << auroraInfo.windowSize.height
<< " native=" << auroraInfo.windowSize.native_fb_width << "x"
<< auroraInfo.windowSize.native_fb_height
<< " viewportPolicy=" << (forceAspect169 ? "16:9" : (configWidescreen ? "stretch" : "fit"))
<< " viewportPolicy=" << (forceAspect ? (configWidescreen ? "16:9" : "fit") : "stretch")
<< " presentAspect="
<< (forceAspect169 ? "16:9" : (configWidescreen ? "surface (dynamic EGG canvas)" : "4:3"))
<< (forceAspect ? (configWidescreen ? "16:9" : "4:3") : "surface (dynamic EGG canvas)")
<< std::endl;
g_auroraInitialized.store(true, std::memory_order_release);
GxWorker::SetWaitCallback(ServiceGuestTimingDuringAuroraFrameWait);
@@ -1524,6 +1539,7 @@ int RuntimeMain(int argc, char** argv) {
Fiber::GuestFiberManager::Shutdown();
WindowPlacementPersistence::Flush(true);
Wiimote::Shutdown();
SteamInputLayout::Release();
GxWorker::Stop();
aurora_shutdown();
DiscordPresence::Shutdown();
@@ -1538,6 +1554,7 @@ int RuntimeMain(int argc, char** argv) {
<< " len=0x" << ex.length()
<< std::dec << std::nouppercase
<< " reason=" << ex.reason();
SteamInputLayout::Release();
ShowRuntimeFatalPopup("a guest memory access was out of bounds", details.str());
WriteFatalLogImpl("access_violation", details.str());
SetRuntimeExitCodeImpl(1);
@@ -1552,6 +1569,7 @@ int RuntimeMain(int argc, char** argv) {
} catch (const std::exception& ex) {
std::cerr << "Runtime error: " << ex.what() << std::endl;
SystemBridge::DumpCpuState(TryGetCpuContext());
SteamInputLayout::Release();
ShowRuntimeFatalPopup("a runtime exception occurred", ex.what());
WriteFatalLogImpl("exception");
SetRuntimeExitCodeImpl(1);
+227
View File
@@ -0,0 +1,227 @@
#include "motion_trick.h"
#include "input_bindings.h"
#include "runtime_config.h"
#include <algorithm>
#include <array>
#include <atomic>
#include <cstring>
#include <mutex>
#include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_keyboard.h>
#include <SDL3/SDL_timer.h>
namespace MotionTrick {
namespace {
// Long enough for the game to see the press, short enough not to hold a trick.
constexpr int kPulseFrames = 4;
// Sensors report zeros or stale data for a moment after being switched on.
constexpr int kWarmupFrames = 6;
constexpr float kIntensityDecay = 0.85f;
// Real sensors always jitter; identical readings mean the stream was switched
// off behind our back, so ask for it again a few times.
constexpr int kStaleFrames = 30;
constexpr uint64_t kResyncIntervalUs = 2'000'000;
constexpr int kMaxResyncs = 3;
struct PortState {
SDL_JoystickID id = 0;
bool gyro = false;
bool accel = false;
int warmup = 0;
MotionSample previous;
int staleFrames = 0;
int resyncs = 0;
uint64_t lastResyncUs = 0;
ShakeDetector detector;
float displayIntensity = 0.0f;
Direction pulse = Direction::None;
int pulseFrames = 0;
Direction lastTrick = Direction::None;
uint64_t lastTrickUs = 0;
};
std::atomic<bool> g_enabled{RuntimeConfigFile::MotionTrickEnabled(false)};
std::atomic<int> g_sensitivity{RuntimeConfigFile::MotionTrickSensitivity(kDefaultSensitivity)};
std::mutex g_mutex;
std::array<PortState, PAD_CHANMAX> g_ports;
// A keyboard player keeps their controller assigned, but aurora ignores it.
SDL_Gamepad* GamepadForPort(uint32_t port) {
uint32_t keyCount = 0;
if (PADGetKeyButtonBindings(port, &keyCount) != nullptr) {
return nullptr;
}
const s32 index = PADGetIndexForPort(port);
return index < 0 ? nullptr : PADGetSDLGamepadForIndex(static_cast<u32>(index));
}
uint16_t ButtonFor(Direction direction) {
switch (direction) {
case Direction::Up: return PAD_BUTTON_UP;
case Direction::Down: return PAD_BUTTON_DOWN;
case Direction::Left: return PAD_BUTTON_LEFT;
case Direction::Right: return PAD_BUTTON_RIGHT;
default: return 0;
}
}
bool HeldByOtherPort(uint32_t port, SDL_JoystickID id) {
for (uint32_t other = 0; other < g_ports.size(); ++other) {
if (other != port && g_ports[other].id == id) {
return true;
}
}
return false;
}
void SetSensors(SDL_Gamepad* gamepad, const PortState& state, bool enabled) {
if (state.gyro) SDL_SetGamepadSensorEnabled(gamepad, SDL_SENSOR_GYRO, enabled);
if (state.accel) SDL_SetGamepadSensorEnabled(gamepad, SDL_SENSOR_ACCEL, enabled);
}
void Attach(uint32_t port, SDL_Gamepad* gamepad) {
PortState& state = g_ports[port];
const SDL_JoystickID id = gamepad != nullptr ? SDL_GetGamepadID(gamepad) : 0;
if (id == state.id) {
return;
}
if (state.id != 0 && !HeldByOtherPort(port, state.id)) {
if (SDL_Gamepad* previous = SDL_GetGamepadFromID(state.id)) {
SetSensors(previous, state, false);
}
}
state = PortState{};
if (gamepad == nullptr) {
return;
}
state.id = id;
state.gyro = SDL_GamepadHasSensor(gamepad, SDL_SENSOR_GYRO) &&
SDL_SetGamepadSensorEnabled(gamepad, SDL_SENSOR_GYRO, true);
state.accel = SDL_GamepadHasSensor(gamepad, SDL_SENSOR_ACCEL) &&
SDL_SetGamepadSensorEnabled(gamepad, SDL_SENSOR_ACCEL, true);
state.warmup = kWarmupFrames;
}
bool SameReading(const MotionSample& a, const MotionSample& b) {
return a.hasGyro == b.hasGyro && a.hasAccel == b.hasAccel &&
std::memcmp(a.gyro, b.gyro, sizeof(a.gyro)) == 0 && std::memcmp(a.accel, b.accel, sizeof(a.accel)) == 0;
}
// Returns true when the sensors were restarted.
bool ResyncIfStale(SDL_Gamepad* gamepad, PortState& state, const MotionSample& sample, uint64_t now) {
const bool stale = SameReading(sample, state.previous) && SDL_GetKeyboardFocus() != nullptr;
state.previous = sample;
state.staleFrames = stale ? state.staleFrames + 1 : 0;
if (!stale) {
state.resyncs = 0;
}
if (state.staleFrames < kStaleFrames || state.resyncs >= kMaxResyncs ||
now - state.lastResyncUs < kResyncIntervalUs) {
return false;
}
SetSensors(gamepad, state, false);
SetSensors(gamepad, state, true);
state.staleFrames = 0;
++state.resyncs;
state.lastResyncUs = now;
state.warmup = kWarmupFrames;
return true;
}
MotionSample ReadSample(SDL_Gamepad* gamepad, const PortState& state) {
MotionSample sample;
sample.hasGyro = state.gyro && SDL_GetGamepadSensorData(gamepad, SDL_SENSOR_GYRO, sample.gyro, 3);
sample.hasAccel = state.accel && SDL_GetGamepadSensorData(gamepad, SDL_SENSOR_ACCEL, sample.accel, 3) &&
(sample.accel[0] != 0.0f || sample.accel[1] != 0.0f || sample.accel[2] != 0.0f);
return sample;
}
} // namespace
bool Enabled() noexcept {
return g_enabled.load(std::memory_order_relaxed);
}
void SetEnabled(bool enabled) noexcept {
g_enabled.store(enabled, std::memory_order_relaxed);
RuntimeConfigFile::SetMotionTrickEnabled(enabled);
}
int Sensitivity() noexcept {
return g_sensitivity.load(std::memory_order_relaxed);
}
void SetSensitivity(int level) noexcept {
level = std::clamp(level, kMinSensitivity, kMaxSensitivity);
g_sensitivity.store(level, std::memory_order_relaxed);
RuntimeConfigFile::SetMotionTrickSensitivity(level);
}
bool PortHasMotion(uint32_t port) noexcept {
SDL_Gamepad* gamepad = GamepadForPort(port);
return gamepad != nullptr &&
(SDL_GamepadHasSensor(gamepad, SDL_SENSOR_GYRO) || SDL_GamepadHasSensor(gamepad, SDL_SENSOR_ACCEL));
}
uint64_t NowUs() noexcept {
return SDL_GetTicksNS() / 1000;
}
void Apply(PADStatus* statuses) noexcept {
if (statuses == nullptr) {
return;
}
const bool enabled = Enabled();
const int sensitivity = Sensitivity();
const bool blocked = InputBindings::InputBlocked();
const uint64_t now = NowUs();
std::lock_guard<std::mutex> lock(g_mutex);
for (uint32_t port = 0; port < PAD_CHANMAX; ++port) {
SDL_Gamepad* gamepad =
enabled && statuses[port].err == PAD_ERR_NONE ? GamepadForPort(port) : nullptr;
Attach(port, gamepad);
PortState& state = g_ports[port];
if (gamepad == nullptr || (!state.gyro && !state.accel)) {
continue;
}
if (state.warmup > 0) {
--state.warmup;
} else if (const MotionSample sample = ReadSample(gamepad, state);
!ResyncIfStale(gamepad, state, sample, now)) {
state.detector.SetSensitivity(sensitivity);
const Direction fired = state.detector.Update(sample, now);
state.displayIntensity =
std::max(state.detector.Intensity(), state.displayIntensity * kIntensityDecay);
if (fired != Direction::None) {
state.lastTrick = fired;
state.lastTrickUs = now;
state.pulse = fired;
state.pulseFrames = kPulseFrames;
}
}
if (blocked) {
state.pulseFrames = 0;
}
if (state.pulseFrames > 0) {
statuses[port].button |= ButtonFor(state.pulse);
--state.pulseFrames;
}
}
}
PortStatus Status(uint32_t port) noexcept {
if (port >= PAD_CHANMAX) {
return {};
}
std::lock_guard<std::mutex> lock(g_mutex);
const PortState& state = g_ports[port];
return {state.displayIntensity, state.lastTrick, state.lastTrickUs, state.id != 0 && !state.gyro && !state.accel};
}
} // namespace MotionTrick
+80
View File
@@ -0,0 +1,80 @@
#include "motion_trick_detector.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <cstring>
namespace MotionTrick {
namespace {
constexpr float kGravity = 9.80665f;
constexpr float kRearmIntensity = 0.35f;
constexpr uint64_t kCooldownUs = 400'000;
// Indexed by sensitivity level - 1.
constexpr std::array<float, kMaxSensitivity> kGyroThresholds = {11.0f, 9.0f, 7.0f, 5.5f, 4.0f};
constexpr std::array<float, kMaxSensitivity> kAccelThresholds = {20.0f, 16.0f, 13.0f, 10.0f, 8.0f};
// The runtime builds with -ffast-math, which may fold std::isfinite away.
bool Finite(const float (&v)[3]) {
for (const float value : v) {
uint32_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
if ((bits & 0x7f800000u) == 0x7f800000u) {
return false;
}
}
return true;
}
float Length(const float (&v)[3]) {
return std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
}
Direction GyroDirection(const float (&gyro)[3]) {
const float pitch = gyro[0];
const float yaw = gyro[1];
const float roll = gyro[2];
if (std::fabs(pitch) >= std::max(std::fabs(yaw), std::fabs(roll))) {
return pitch >= 0.0f ? Direction::Up : Direction::Down;
}
const float lateral = std::fabs(roll) >= std::fabs(yaw) ? roll : yaw;
return lateral >= 0.0f ? Direction::Left : Direction::Right;
}
} // namespace
void ShakeDetector::SetSensitivity(int level) {
const size_t index = static_cast<size_t>(std::clamp(level, kMinSensitivity, kMaxSensitivity) - 1);
m_gyroThreshold = kGyroThresholds[index];
m_accelThreshold = kAccelThresholds[index];
}
Direction ShakeDetector::Update(const MotionSample& sample, uint64_t nowUs) {
// The accelerometer only stands in for a missing gyro: on a gyro pad it
// mostly picks up bumps, and it carries no direction.
const float(&values)[3] = sample.hasGyro ? sample.gyro : sample.accel;
if ((!sample.hasGyro && !sample.hasAccel) || !Finite(values)) {
m_intensity = 0.0f;
return Direction::None;
}
m_intensity = sample.hasGyro ? Length(sample.gyro) / m_gyroThreshold
: std::fabs(Length(sample.accel) - kGravity) / m_accelThreshold;
if (!m_armed) {
if (m_intensity < kRearmIntensity && nowUs - m_lastFireUs >= kCooldownUs) {
m_armed = true;
}
return Direction::None;
}
if (m_intensity < 1.0f) {
return Direction::None;
}
m_armed = false;
m_lastFireUs = nowUs;
return sample.hasGyro ? GyroDirection(sample.gyro) : Direction::Up;
}
} // namespace MotionTrick
+95 -6
View File
@@ -5,6 +5,7 @@
#include "controller_button_names.h"
#include "controller_mapping_wizard.h"
#include "input_bindings.h"
#include "motion_trick.h"
#include "game_graphics_options.h"
#include "music_attenuation.h"
#include "runtime_config.h"
@@ -113,7 +114,8 @@ bool g_skipUnreadyPipelines = RuntimeConfigFile::SkipUnreadyPipelines(true);
bool g_vsync = RuntimeConfigFile::Vsync();
bool g_disableCopyFilter = RuntimeConfigFile::DisableCopyFilter(true);
bool g_showFps = RuntimeConfigFile::ShowFps(true);
bool g_forceAspect169 = RuntimeConfigFile::ForceAspect169Enabled();
bool g_forceAspect = RuntimeConfigFile::ForceAspectEnabled();
bool g_widescreen = RuntimeConfigFile::WidescreenEnabled(true);
#if defined(__APPLE__)
bool g_metalFxSpatialUpscaling = RuntimeConfigFile::MetalFxSpatialUpscaling(false);
#endif
@@ -744,6 +746,69 @@ void DrawRumbleSettings() {
}
}
const char* TrickDirectionLabel(MotionTrick::Direction direction) {
switch (direction) {
case MotionTrick::Direction::Up: return "up";
case MotionTrick::Direction::Down: return "down";
case MotionTrick::Direction::Left: return "left";
case MotionTrick::Direction::Right: return "right";
default: return "";
}
}
void DrawMotionTrickSettings(uint32_t port) {
const MotionTrick::PortStatus status = MotionTrick::Status(port);
const bool hasMotion = MotionTrick::PortHasMotion(port) && !status.sensorsUnavailable;
bool enabled = MotionTrick::Enabled();
ImGui::BeginDisabled(!hasMotion && !enabled);
if (ImGui::Checkbox("Shake to trick", &enabled)) {
MotionTrick::SetEnabled(enabled);
}
ImGui::EndDisabled();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
ImGui::SetTooltip(hasMotion
? "Flick the controller to trick off ramps, or to wheelie on a bike, like shaking a Wii Remote.\n"
"The flick direction picks the trick. The D-pad keeps working. Applies to every player."
: "Steam Input can hide a controller's motion sensor; turn it off for this game to use it here.");
}
if (!hasMotion) {
ImGui::SameLine();
ImGui::TextDisabled("(no motion sensor)");
return;
}
if (!enabled) {
return;
}
ImGui::Indent();
int sensitivity = MotionTrick::Sensitivity();
ImGui::SetNextItemWidth(160.0f);
if (ImGui::SliderInt("Sensitivity##motion", &sensitivity, MotionTrick::kMinSensitivity,
MotionTrick::kMaxSensitivity, "%d", ImGuiSliderFlags_AlwaysClamp)) {
MotionTrick::SetSensitivity(sensitivity);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Higher reacts to smaller flicks. Lower it if you trick or wheelie by accident while steering.");
}
constexpr float kMeterScale = 1.5f;
const ImVec2 meterPos = ImGui::GetCursorScreenPos();
const ImVec2 meterSize(160.0f, ImGui::GetFrameHeight() * 0.6f);
ImGui::ProgressBar(std::min(status.intensity / kMeterScale, 1.0f), meterSize, "");
const float thresholdX = meterPos.x + meterSize.x / kMeterScale;
ImGui::GetWindowDrawList()->AddLine(ImVec2(thresholdX, meterPos.y), ImVec2(thresholdX, meterPos.y + meterSize.y),
ImGui::GetColorU32(ImGuiCol_Text), 2.0f);
ImGui::SameLine();
const bool recent = status.lastTrick != MotionTrick::Direction::None &&
MotionTrick::NowUs() - status.lastTrickUs < 700'000;
if (recent) {
ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f), "Trick! (%s)", TrickDirectionLabel(status.lastTrick));
} else {
ImGui::TextDisabled("Flick to test");
}
ImGui::Unindent();
}
void DrawGamepadMapping(uint32_t port) {
uint32_t mappingCount = 0;
PADButtonMapping* mappings = PADGetButtonMappings(port, &mappingCount);
@@ -984,6 +1049,7 @@ void DrawControllerSettings() {
}
} else {
ImGui::TextUnformatted("Ready to play.");
DrawMotionTrickSettings(port);
if (ImGui::CollapsingHeader("Customize buttons")) {
ImGui::Indent();
DrawGamepadMapping(port);
@@ -1080,11 +1146,22 @@ void DrawAudioSettings() {
void DrawGraphicsSettings() {
g_displayMode = static_cast<int>(aurora_get_display_mode());
if (ImGui::Checkbox("Force 16:9", &g_forceAspect169)) {
SetMkwForceAspect169(g_forceAspect169);
RuntimeConfigFile::SetForceAspect169(g_forceAspect169);
if (ImGui::BeginCombo("Aspect ratio (restart required)", g_widescreen ? "16:9" : "4:3")) {
if (ImGui::Selectable("16:9", g_widescreen)) {
g_widescreen = true;
RuntimeConfigFile::SetWidescreen(true);
}
if (ImGui::Selectable("4:3", !g_widescreen)) {
g_widescreen = false;
RuntimeConfigFile::SetWidescreen(false);
}
ImGui::EndCombo();
}
ImGui::TextDisabled("Keep a 16:9 image with black bars when the window has another shape.");
if (ImGui::Checkbox("Force aspect ratio", &g_forceAspect)) {
SetMkwForceAspect(g_forceAspect);
RuntimeConfigFile::SetForceAspect(g_forceAspect);
}
ImGui::TextDisabled("Keep the current aspect ratio with black bars when the window has another shape.");
ImGui::Separator();
struct EffectFlag {
const char* label;
@@ -1470,6 +1547,18 @@ void DrawTopBar() {
ImGui::EndMainMenuBar();
}
// Steam's desktop layout maps B to Escape on Valve controllers.
bool ValveControllerConnected() {
int count = 0;
SDL_JoystickID* ids = SDL_GetJoysticks(&count);
bool found = false;
for (int i = 0; ids != nullptr && i < count && !found; ++i) {
found = SDL_GetJoystickVendorForID(ids[i]) == 0x28DE && SDL_GetJoystickProductForID(ids[i]) != 0x11FF;
}
SDL_free(ids);
return found;
}
bool IsToggleKey(const SDL_Event& event, SDL_Scancode code) {
return event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && event.key.scancode == code;
}
@@ -1591,7 +1680,7 @@ void HandleEvents(const AuroraEvent* events) noexcept {
g_exitPromptOpen = false;
} else if (g_topBarVisible) {
SetTopBarVisible(false);
} else {
} else if (!ValveControllerConnected()) {
g_exitPromptOpen = true;
}
ApplyInputBlockState();
+603
View File
@@ -0,0 +1,603 @@
#include "steam_input_layout.h"
#include "runtime_log.h"
#include <aurora/aurora.h>
#include <SDL3/SDL_video.h>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdlib>
#include <memory>
#include <mutex>
#include <string>
#include <system_error>
#include <thread>
#include <utility>
#include <vector>
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <cwchar>
#include <filesystem>
#include <optional>
#else
#include <cerrno>
#include <csignal>
#include <fcntl.h>
#include <spawn.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#if defined(__APPLE__)
#include <libproc.h>
#include <sys/proc.h>
#include <sys/proc_info.h>
#else
#include <dirent.h>
#include <fstream>
#include <sstream>
#endif
extern char** environ;
#endif
namespace SteamInputLayout {
namespace {
using Clock = std::chrono::steady_clock;
enum class Handoff { None, Unconfirmed, Confirmed };
constexpr std::chrono::milliseconds kHandoffTimeout{2000};
#if defined(__linux__)
// xdg-open can return before the Steam forwarder it started has delivered the URL.
constexpr std::chrono::milliseconds kDeliverySpacing{1500};
#else
constexpr std::chrono::milliseconds kDeliverySpacing{0};
#endif
struct State {
std::mutex mutex;
std::condition_variable wake;
// Serializes requests to Steam so they arrive in order.
std::timed_mutex delivery;
bool enabled = false;
bool stopping = false;
bool desired = false;
bool sent = false;
bool maybeForced = false;
uint32_t appId = 0;
SDL_WindowID windowId = 0;
std::atomic<Clock::rep> lastDelivery{0};
std::atomic<bool> released{false};
};
// Never destroyed: the detached worker and late exit paths may still touch it.
std::atomic<State*> g_state{nullptr};
thread_local bool t_isWorker = false;
thread_local bool t_inRelease = false;
// The worker launches and publishes its request helper under this lock, so an exit that
// can't wait for the worker can cancel it: either the helper is published, or it never starts.
std::mutex g_helperMutex;
std::atomic<bool> g_helperCancelled{false};
#if defined(_WIN32)
HANDLE g_helper = nullptr;
#else
pid_t g_helper = 0;
#endif
// These locks are only held briefly; failing to get one means a crash inside it.
bool TryLockBriefly(std::unique_lock<std::mutex>& lock,
std::chrono::milliseconds timeout = std::chrono::milliseconds(250)) {
const auto deadline = Clock::now() + timeout;
while (!lock.try_lock()) {
if (Clock::now() >= deadline) {
return false;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
return true;
}
// Steam applies the launching app's own layout to everything it starts, children included.
bool LaunchedBySteam() {
for (const char* name : {"SteamGameId", "SteamAppId", "SteamClientLaunch"}) {
const char* value = std::getenv(name);
if (value != nullptr && *value != '\0') {
return true;
}
}
#if defined(_WIN32)
if (GetModuleHandleW(L"GameOverlayRenderer64.dll") != nullptr) {
return true;
}
#endif
return false;
}
#if defined(_WIN32)
std::vector<BYTE> TokenUserOf(HANDLE process) {
std::vector<BYTE> user;
HANDLE token = nullptr;
if (!OpenProcessToken(process, TOKEN_QUERY, &token)) {
return user;
}
DWORD size = 0;
GetTokenInformation(token, TokenUser, nullptr, 0, &size);
user.resize(size);
if (size == 0 || !GetTokenInformation(token, TokenUser, user.data(), size, &size)) {
user.clear();
}
CloseHandle(token);
return user;
}
// A stale pid can be reused by another signed-in user's Steam.
bool OwnedByCurrentUser(HANDLE process) {
const auto theirs = TokenUserOf(process);
const auto ours = TokenUserOf(GetCurrentProcess());
return !theirs.empty() && !ours.empty() &&
EqualSid(reinterpret_cast<const TOKEN_USER*>(theirs.data())->User.Sid,
reinterpret_cast<const TOKEN_USER*>(ours.data())->User.Sid);
}
// ActiveProcess\pid outlives a Steam crash, so the process behind it is verified too.
std::optional<std::wstring> RunningSteamExecutable() {
DWORD pid = 0;
DWORD size = sizeof(pid);
if (RegGetValueW(HKEY_CURRENT_USER, L"Software\\Valve\\Steam\\ActiveProcess", L"pid",
RRF_RT_REG_DWORD, nullptr, &pid, &size) != ERROR_SUCCESS ||
pid == 0) {
return std::nullopt;
}
HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if (process == nullptr) {
return std::nullopt;
}
std::wstring path(32768, L'\0');
DWORD length = static_cast<DWORD>(path.size());
DWORD exitCode = 0;
const bool alive = QueryFullProcessImageNameW(process, 0, path.data(), &length) &&
GetExitCodeProcess(process, &exitCode) && exitCode == STILL_ACTIVE &&
OwnedByCurrentUser(process);
CloseHandle(process);
if (!alive) {
return std::nullopt;
}
path.resize(length);
if (_wcsicmp(std::filesystem::path(path).filename().c_str(), L"steam.exe") != 0) {
return std::nullopt;
}
return path;
}
// Same command line as Steam's registered steam:// handler, sent to the verified client.
Handoff OpenSteamUrl(const std::string& url, bool bounded) {
const auto steam = RunningSteamExecutable();
if (!steam) {
return Handoff::None;
}
std::wstring commandLine = L"\"" + *steam + L"\" -- \"" + std::wstring(url.begin(), url.end()) + L"\"";
const std::wstring directory = std::filesystem::path(*steam).parent_path().wstring();
STARTUPINFOW startup{};
startup.cb = sizeof(startup);
PROCESS_INFORMATION info{};
std::unique_lock launch(g_helperMutex, std::defer_lock);
if (!bounded) {
launch.lock();
if (g_helperCancelled) {
return Handoff::None;
}
}
// The worker's helper starts suspended so a cancellation that couldn't take the lock while
// CreateProcessW ran still stops it before it can reach Steam.
if (!CreateProcessW(steam->c_str(), commandLine.data(), nullptr, nullptr, FALSE,
bounded ? 0 : CREATE_SUSPENDED, nullptr, directory.c_str(), &startup, &info)) {
return Handoff::None;
}
if (!bounded) {
g_helper = info.hProcess;
launch.unlock();
if (g_helperCancelled) {
TerminateProcess(info.hProcess, 1);
} else {
ResumeThread(info.hThread);
}
}
CloseHandle(info.hThread);
const DWORD waited = WaitForSingleObject(info.hProcess, bounded ? static_cast<DWORD>(kHandoffTimeout.count()) : INFINITE);
if (!bounded) {
std::lock_guard lock(g_helperMutex);
g_helper = nullptr;
}
DWORD exitCode = 1;
const bool succeeded = waited == WAIT_OBJECT_0 && GetExitCodeProcess(info.hProcess, &exitCode) && exitCode == 0;
CloseHandle(info.hProcess);
return succeeded ? Handoff::Confirmed : Handoff::Unconfirmed;
}
void CancelWorkerRequests() {
g_helperCancelled = true;
std::unique_lock lock(g_helperMutex, std::defer_lock);
if (!TryLockBriefly(lock, kHandoffTimeout)) {
return;
}
if (g_helper != nullptr && TerminateProcess(g_helper, 1)) {
WaitForSingleObject(g_helper, static_cast<DWORD>(kHandoffTimeout.count()));
}
}
#else
// Only a live client of this user; a URL handler with no client running would start Steam.
bool SteamRunning() {
const uid_t uid = getuid();
#if defined(__APPLE__)
const int count = proc_listallpids(nullptr, 0);
if (count <= 0) {
return false;
}
std::vector<pid_t> pids(static_cast<size_t>(count) + 32);
const int filled = proc_listallpids(pids.data(), static_cast<int>(pids.size() * sizeof(pid_t)));
for (int i = 0; i < filled; ++i) {
char name[2 * MAXCOMLEN + 1] = {};
if (proc_name(pids[i], name, sizeof(name)) <= 0 || std::string(name) != "steam_osx") {
continue;
}
proc_bsdinfo info{};
if (proc_pidinfo(pids[i], PROC_PIDTBSDINFO, 0, &info, sizeof(info)) == sizeof(info) &&
info.pbi_uid == uid && info.pbi_status != SZOMB) {
return true;
}
}
return false;
#else
DIR* proc = opendir("/proc");
if (proc == nullptr) {
return false;
}
bool found = false;
while (const dirent* entry = readdir(proc)) {
if (entry->d_name[0] < '1' || entry->d_name[0] > '9') {
continue;
}
std::ifstream status(std::string("/proc/") + entry->d_name + "/status");
std::string line;
bool named = false;
bool live = false;
bool owned = false;
while (std::getline(status, line)) {
if (line.rfind("Name:", 0) == 0) {
named = line == "Name:\tsteam";
} else if (line.rfind("State:", 0) == 0) {
live = line.find_first_of("ZX") == std::string::npos;
} else if (line.rfind("Uid:", 0) == 0) {
std::istringstream fields(line.substr(4));
uid_t real = 0;
owned = static_cast<bool>(fields >> real) && real == uid;
break;
}
}
if (named && live && owned) {
found = true;
break;
}
}
closedir(proc);
return found;
#endif
}
Handoff OpenSteamUrl(const std::string& url, bool bounded) {
if (!SteamRunning()) {
return Handoff::None;
}
// The worker's helper waits for a go-ahead on fd 3 before it runs the URL handler, so a
// cancellation that couldn't take the lock while posix_spawn ran still stops it in time.
static char gateScript[] = "read -r go <&3 || exit 1; exec 3<&-; exec \"$0\" \"$@\"";
#if defined(__APPLE__)
const char* program = "/usr/bin/open";
char* argv[] = {const_cast<char*>("open"), const_cast<char*>("-g"), const_cast<char*>(url.c_str()), nullptr};
char* gated[] = {const_cast<char*>("sh"), const_cast<char*>("-c"), gateScript, const_cast<char*>(program),
const_cast<char*>("-g"), const_cast<char*>(url.c_str()), nullptr};
constexpr int kNoSignal = 0;
#else
const char* program = "xdg-open";
char* argv[] = {const_cast<char*>("xdg-open"), const_cast<char*>(url.c_str()), nullptr};
char* gated[] = {const_cast<char*>("sh"), const_cast<char*>("-c"), gateScript, const_cast<char*>(program),
const_cast<char*>(url.c_str()), nullptr};
constexpr int kNoSignal = MSG_NOSIGNAL;
#endif
int gate[2] = {-1, -1};
if (!bounded) {
if (socketpair(AF_UNIX, SOCK_STREAM, 0, gate) != 0) {
return Handoff::None;
}
// dup2(3, 3) keeps FD_CLOEXEC on macOS, which would close the gate in the child.
if (gate[0] == 3) {
std::swap(gate[0], gate[1]);
}
fcntl(gate[0], F_SETFD, FD_CLOEXEC);
fcntl(gate[1], F_SETFD, FD_CLOEXEC);
#if defined(__APPLE__)
int noSigPipe = 1;
setsockopt(gate[1], SOL_SOCKET, SO_NOSIGPIPE, &noSigPipe, sizeof(noSigPipe));
#endif
}
const auto closeGate = [&gate] {
for (int& end : gate) {
if (end != -1) {
close(end);
end = -1;
}
}
};
posix_spawn_file_actions_t actions;
if (posix_spawn_file_actions_init(&actions) != 0) {
closeGate();
return Handoff::None;
}
if (!bounded) {
posix_spawn_file_actions_adddup2(&actions, gate[0], 3);
}
posix_spawn_file_actions_addopen(&actions, STDIN_FILENO, "/dev/null", O_RDONLY, 0);
posix_spawn_file_actions_addopen(&actions, STDOUT_FILENO, "/dev/null", O_WRONLY, 0);
posix_spawn_file_actions_addopen(&actions, STDERR_FILENO, "/dev/null", O_WRONLY, 0);
// A crash handler can call this with signals blocked; the child should not inherit that.
posix_spawnattr_t attributes;
posix_spawnattr_init(&attributes);
sigset_t noSignals;
sigemptyset(&noSignals);
posix_spawnattr_setsigmask(&attributes, &noSignals);
posix_spawnattr_setflags(&attributes, POSIX_SPAWN_SETSIGMASK);
pid_t child = 0;
std::unique_lock launch(g_helperMutex, std::defer_lock);
if (!bounded) {
launch.lock();
}
int error = ECANCELED;
if (bounded) {
error = posix_spawnp(&child, program, &actions, &attributes, argv, environ);
} else if (!g_helperCancelled) {
error = posix_spawn(&child, "/bin/sh", &actions, &attributes, gated, environ);
}
posix_spawnattr_destroy(&attributes);
posix_spawn_file_actions_destroy(&actions);
if (error != 0) {
closeGate();
return Handoff::None;
}
int status = 0;
if (!bounded) {
g_helper = child;
if (!g_helperCancelled) {
send(gate[1], "1\n", 2, kNoSignal);
}
closeGate();
launch.unlock();
// Wait without reaping so the pid can't be reused while it is still published.
siginfo_t info{};
while (waitid(P_PID, static_cast<id_t>(child), &info, WEXITED | WNOWAIT) == -1 && errno == EINTR) {
}
{
std::lock_guard lock(g_helperMutex);
g_helper = 0;
}
}
const auto deadline = Clock::now() + kHandoffTimeout;
for (;;) {
const pid_t waited = waitpid(child, &status, bounded ? WNOHANG : 0);
if (waited == child) {
return WIFEXITED(status) && WEXITSTATUS(status) == 0 ? Handoff::Confirmed : Handoff::Unconfirmed;
}
if (waited == -1 && errno != EINTR) {
return Handoff::Unconfirmed;
}
if (waited == 0) {
if (Clock::now() >= deadline) {
try {
std::thread([child] { waitpid(child, nullptr, 0); }).detach();
} catch (...) {
}
return Handoff::Unconfirmed;
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
}
void CancelWorkerRequests() {
g_helperCancelled = true;
std::unique_lock lock(g_helperMutex, std::defer_lock);
if (!TryLockBriefly(lock, kHandoffTimeout)) {
return;
}
if (g_helper == 0 || kill(g_helper, SIGKILL) != 0) {
return;
}
// The worker only reaps after clearing g_helper under this lock, so the pid stays ours.
const auto deadline = Clock::now() + kHandoffTimeout;
for (;;) {
siginfo_t info{};
if (waitid(P_PID, static_cast<id_t>(g_helper), &info, WEXITED | WNOWAIT | WNOHANG) != 0 ||
info.si_pid != 0 || Clock::now() >= deadline) {
return;
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
#endif
// Caller holds state.delivery. Only exit paths bound the wait; the worker waits for each
// request to finish so a slow one can never land after the next.
Handoff Deliver(State& state, bool forced, uint32_t appId, bool bounded) {
const auto earliest = Clock::time_point(Clock::duration(state.lastDelivery.load())) + kDeliverySpacing;
if (const auto now = Clock::now(); now < earliest) {
std::this_thread::sleep_for(earliest - now);
}
const Handoff delivered = OpenSteamUrl("steam://forceinputappid/" + std::to_string(forced ? appId : 0), bounded);
if (delivered != Handoff::None) {
state.lastDelivery = Clock::now().time_since_epoch().count();
if (forced) {
RT_LOG(RT_TAG_RUNTIME) << "Steam Input: applying the controller layout of app " << appId << std::endl;
} else {
RT_LOG(RT_TAG_RUNTIME) << "Steam Input: restored the desktop controller layout" << std::endl;
}
}
return delivered;
}
void Worker(State& state) {
t_isWorker = true;
std::unique_lock lock(state.mutex);
for (;;) {
state.wake.wait(lock, [&] { return state.stopping || state.desired != state.sent; });
if (state.stopping) {
return;
}
const bool target = state.desired;
const uint32_t appId = state.appId;
// A failed request is retried on the next focus change rather than in a loop.
state.sent = target;
lock.unlock();
{
std::lock_guard delivery(state.delivery);
{
// Release may have run while this thread waited for the delivery lock.
std::lock_guard guard(state.mutex);
if (state.stopping) {
return;
}
}
const Handoff delivered = Deliver(state, target, appId, false);
std::lock_guard guard(state.mutex);
state.maybeForced = target ? delivered != Handoff::None || state.maybeForced
: state.maybeForced && delivered != Handoff::Confirmed;
}
lock.lock();
}
}
} // namespace
void Initialize(bool enabled, uint32_t appId) {
if (!enabled || appId == 0 || g_state.load() != nullptr) {
return;
}
if (LaunchedBySteam()) {
RT_LOG(RT_TAG_RUNTIME) << "Steam Input: launched by Steam, leaving its controller layout alone" << std::endl;
return;
}
SDL_Window* window = aurora_get_window();
if (window == nullptr) {
return;
}
auto* state = new State();
state->enabled = true;
state->appId = appId;
state->windowId = SDL_GetWindowID(window);
state->desired = (SDL_GetWindowFlags(window) & SDL_WINDOW_INPUT_FOCUS) != 0;
try {
std::thread(Worker, std::ref(*state)).detach();
} catch (const std::exception& error) {
RT_LOG(RT_TAG_RUNTIME) << "Steam Input: " << error.what() << std::endl;
delete state;
return;
}
g_state.store(state);
}
void HandleEvents(const AuroraEvent* events) {
State* state = g_state.load();
if (state == nullptr || events == nullptr) {
return;
}
int focus = -1;
for (const AuroraEvent* event = events; event->type != AURORA_NONE; ++event) {
if (event->type != AURORA_SDL_EVENT) {
continue;
}
const SDL_Event& sdl = event->sdl;
const bool gained = sdl.type == SDL_EVENT_WINDOW_FOCUS_GAINED;
if ((gained || sdl.type == SDL_EVENT_WINDOW_FOCUS_LOST) && sdl.window.windowID == state->windowId) {
focus = gained ? 1 : 0;
}
}
if (focus < 0) {
return;
}
std::lock_guard lock(state->mutex);
if (state->enabled) {
state->desired = focus == 1;
state->wake.notify_one();
}
}
void Release() noexcept {
State* state = g_state.load();
if (state == nullptr) {
return;
}
const auto releaseWait = kHandoffTimeout + kDeliverySpacing + std::chrono::milliseconds(500);
try {
{
std::unique_lock lock(state->mutex, std::defer_lock);
if (!TryLockBriefly(lock)) {
return;
}
if (!state->enabled) {
lock.unlock();
// Another thread is restoring; exiting under it would kill its request.
const auto deadline = Clock::now() + 2 * releaseWait;
while (!t_inRelease && !state->released.load() && Clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
return;
}
t_inRelease = true;
state->enabled = false;
state->stopping = true;
state->wake.notify_one();
}
// Let an in-flight request land first so the restore is the last one Steam sees.
std::unique_lock delivery(state->delivery, std::defer_lock);
const bool ordered = !t_isWorker && delivery.try_lock_for(releaseWait);
bool maybeForced = true;
if (ordered) {
std::unique_lock lock(state->mutex, std::defer_lock);
maybeForced = !TryLockBriefly(lock) || state->maybeForced;
}
// A request still in flight, or about to start, could otherwise land after the restore.
CancelWorkerRequests();
if (maybeForced) {
// Discovery and spawning can block as well; they must not hold up the exit.
auto done = std::make_shared<std::atomic<bool>>(false);
try {
std::thread([state, done] {
Deliver(*state, false, 0, true);
done->store(true);
}).detach();
} catch (const std::system_error&) {
done->store(true);
}
const auto deadline = Clock::now() + releaseWait;
while (!done->load() && Clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
}
} catch (...) {
}
state->released.store(true);
}
} // namespace SteamInputLayout
+247
View File
@@ -0,0 +1,247 @@
#include "motion_trick_detector.h"
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
using MotionTrick::Direction;
using MotionTrick::MotionSample;
using MotionTrick::ShakeDetector;
namespace {
int g_failures = 0;
constexpr uint64_t kFrameUs = 16'667;
void Check(bool ok, const std::string& what) {
if (!ok) {
std::printf(" FAIL: %s\n", what.c_str());
++g_failures;
}
}
// Written as raw bits so no float operation under -ffast-math can drop them.
void SetBits(float& target, uint32_t bits) {
std::memcpy(&target, &bits, sizeof(target));
}
MotionSample Gyro(float pitch, float yaw, float roll) {
MotionSample sample;
sample.hasGyro = true;
sample.gyro[0] = pitch;
sample.gyro[1] = yaw;
sample.gyro[2] = roll;
sample.hasAccel = true;
sample.accel[1] = 9.80665f;
return sample;
}
MotionSample AtRest() {
return Gyro(0.0f, 0.0f, 0.0f);
}
struct Run {
ShakeDetector detector;
uint64_t now = 0;
std::vector<Direction> fired;
void Feed(const MotionSample& sample, int frames = 1) {
for (int i = 0; i < frames; ++i) {
const Direction direction = detector.Update(sample, now);
if (direction != Direction::None) fired.push_back(direction);
now += kFrameUs;
}
}
// Ramp up to the peak and back down over a few frames, like a real flick.
void Flick(const MotionSample& peak) {
for (const float scale : {0.3f, 0.7f, 1.0f, 0.6f, 0.2f}) {
MotionSample sample = peak;
for (float& value : sample.gyro) value *= scale;
Feed(sample);
}
}
};
void TestRestNeverFires() {
Run run;
run.Feed(AtRest(), 600);
Check(run.fired.empty(), "controller at rest fired a trick");
}
void TestSteeringWobbleNeverFires() {
Run run;
for (int i = 0; i < 600; ++i) {
run.Feed(Gyro(1.5f * std::sin(i * 0.2f), 2.0f * std::sin(i * 0.13f), 2.5f * std::cos(i * 0.17f)));
}
Check(run.fired.empty(), "steering wobble fired a trick");
}
void TestOneFlickOneTrick() {
Run run;
run.Flick(Gyro(10.0f, 0.0f, 0.0f));
run.Feed(AtRest(), 60);
Check(run.fired.size() == 1, "one flick did not give exactly one trick");
Check(!run.fired.empty() && run.fired[0] == Direction::Up, "upward flick was not Up");
}
void TestReturnStrokeIsIgnored() {
Run run;
run.Flick(Gyro(10.0f, 0.0f, 0.0f));
run.Flick(Gyro(-9.0f, 0.0f, 0.0f));
run.Feed(AtRest(), 60);
Check(run.fired.size() == 1, "bringing the controller back gave a second trick");
}
void TestSeparateFlicksBothFire() {
Run run;
run.Flick(Gyro(10.0f, 0.0f, 0.0f));
run.Feed(AtRest(), 40);
run.Flick(Gyro(10.0f, 0.0f, 0.0f));
run.Feed(AtRest(), 40);
Check(run.fired.size() == 2, "two separate flicks did not give two tricks");
}
void TestSustainedShakeFiresOnce() {
Run run;
for (int i = 0; i < 120; ++i) {
run.Feed(Gyro(i % 2 == 0 ? 12.0f : -12.0f, 0.0f, 0.0f));
}
Check(run.fired.size() == 1, "continuous shaking fired more than once");
}
void TestDirections() {
const struct {
MotionSample peak;
Direction expected;
const char* name;
} cases[] = {
{Gyro(10.0f, 0.0f, 0.0f), Direction::Up, "pitch up"},
{Gyro(-10.0f, 0.0f, 0.0f), Direction::Down, "pitch down"},
{Gyro(1.0f, 0.0f, 10.0f), Direction::Left, "roll left"},
{Gyro(1.0f, 0.0f, -10.0f), Direction::Right, "roll right"},
{Gyro(0.0f, 10.0f, 2.0f), Direction::Left, "yaw left"},
{Gyro(0.0f, -10.0f, -2.0f), Direction::Right, "yaw right"},
};
for (const auto& c : cases) {
Run run;
run.Flick(c.peak);
Check(run.fired.size() == 1 && run.fired[0] == c.expected, std::string("wrong direction for ") + c.name);
}
}
void TestSensitivity() {
Run low;
low.detector.SetSensitivity(1);
low.Flick(Gyro(6.0f, 0.0f, 0.0f));
Check(low.fired.empty(), "sensitivity 1 fired on a small flick");
Run high;
high.detector.SetSensitivity(5);
high.Flick(Gyro(6.0f, 0.0f, 0.0f));
Check(high.fired.size() == 1, "sensitivity 5 missed a small flick");
Run clamped;
clamped.detector.SetSensitivity(99);
clamped.Flick(Gyro(6.0f, 0.0f, 0.0f));
Check(clamped.fired.size() == 1, "out of range sensitivity was not clamped");
}
void TestAccelOnlyController() {
Run run;
MotionSample rest;
rest.hasAccel = true;
rest.accel[1] = 9.80665f;
run.Feed(rest, 120);
Check(run.fired.empty(), "gravity alone fired a trick");
MotionSample jolt = rest;
jolt.accel[1] = 35.0f;
run.Feed(jolt);
run.Feed(rest, 60);
Check(run.fired.size() == 1 && run.fired[0] == Direction::Up, "accelerometer jolt did not give Up");
}
void TestGyroOnlyController() {
Run run;
MotionSample flick = Gyro(10.0f, 0.0f, 0.0f);
flick.hasAccel = false;
MotionSample rest = AtRest();
rest.hasAccel = false;
run.Flick(flick);
run.Feed(rest, 60);
run.Flick(flick);
run.Feed(rest, 60);
Check(run.fired.size() == 2 && run.fired[0] == Direction::Up && run.fired[1] == Direction::Up,
"repeated gyro-only flicks did not give two Up tricks");
}
void TestGyroPadIgnoresAccel() {
Run bump;
MotionSample jolt = AtRest();
jolt.accel[1] = 40.0f;
bump.Feed(jolt, 3);
bump.Feed(AtRest(), 60);
Check(bump.fired.empty(), "an accelerometer bump on a gyro pad fired a trick");
Run flick;
MotionSample sideways = Gyro(0.0f, 10.0f, 0.0f);
sideways.accel[1] = 40.0f;
flick.Feed(sideways);
Check(flick.fired.size() == 1 && flick.fired[0] == Direction::Left,
"a jolt changed the direction of a sideways flick");
}
void TestNoSensorsNeverFires() {
Run run;
MotionSample none;
none.gyro[0] = 50.0f;
none.accel[1] = 80.0f;
run.Feed(none, 60);
Check(run.fired.empty(), "a sample without sensors fired a trick");
}
void TestBadDataIgnored() {
Run run;
for (const uint32_t bits : {0x7fc00000u, 0x7f800000u}) {
for (int axis = 0; axis < 3; ++axis) {
MotionSample gyro = AtRest();
SetBits(gyro.gyro[axis], bits);
run.Feed(gyro, 10);
MotionSample accel;
accel.hasAccel = true;
SetBits(accel.accel[axis], bits);
run.Feed(accel, 10);
}
}
Check(run.fired.empty(), "NaN or infinite sensor data fired a trick");
run.Flick(Gyro(10.0f, 0.0f, 0.0f));
Check(run.fired.size() == 1, "detector did not recover after NaN data");
}
} // namespace
int main() {
TestRestNeverFires();
TestSteeringWobbleNeverFires();
TestOneFlickOneTrick();
TestReturnStrokeIsIgnored();
TestSeparateFlicksBothFire();
TestSustainedShakeFiresOnce();
TestDirections();
TestSensitivity();
TestAccelOnlyController();
TestGyroOnlyController();
TestGyroPadIgnoresAccel();
TestNoSensorsNeverFires();
TestBadDataIgnored();
if (g_failures != 0) {
std::printf("%d failure(s)\n", g_failures);
return 1;
}
std::printf("motion trick tests passed\n");
return 0;
}
+39
View File
@@ -51,6 +51,45 @@ int main() {
const RuntimeUserConfig config = RuntimeConfigFile::ParseConfig(input, "invalid.toml");
if (!Require(!config.metalFxSpatialUpscaling, "invalid MetalFX setting was accepted")) return 1;
}
{
std::istringstream input("[video]\nwidescreen = false\nforce_16_9 = true\n");
const RuntimeUserConfig config = RuntimeConfigFile::ParseConfig(input, "legacy.toml");
if (!Require(config.forceAspect == std::optional<bool>{true} &&
config.widescreen == std::optional<bool>{true} && config.legacyForceAspect,
"legacy force_16_9 did not force a 16:9 game")) return 1;
}
{
std::istringstream input("[video]\nwidescreen = false\nforce_aspect = true\nforce_16_9 = true\n");
const RuntimeUserConfig config = RuntimeConfigFile::ParseConfig(input, "migrated.toml");
if (!Require(config.forceAspect == std::optional<bool>{true} &&
config.widescreen == std::optional<bool>{false} && !config.legacyForceAspect,
"legacy force_16_9 overrode force_aspect")) return 1;
}
{
std::istringstream input("[controller]\nmotion_trick = true\nmotion_trick_sensitivity = 5\n");
const RuntimeUserConfig config = RuntimeConfigFile::ParseConfig(input, "motion.toml");
if (!Require(config.motionTrick == std::optional<bool>{true} &&
config.motionTrickSensitivity == std::optional<int32_t>{5},
"shake to trick settings were not loaded")) return 1;
}
{
std::istringstream input("[controller]\nmotion_trick_sensitivity = 9\n");
const RuntimeUserConfig config = RuntimeConfigFile::ParseConfig(input, "motion-range.toml");
if (!Require(!config.motionTrick && !config.motionTrickSensitivity,
"out of range shake to trick sensitivity was accepted")) return 1;
}
{
std::istringstream input("[controller]\nsteam_input_override = false\nsteam_input_app_id = 1234\n");
const RuntimeUserConfig config = RuntimeConfigFile::ParseConfig(input, "steam.toml");
if (!Require(config.steamInputOverride == std::optional<bool>{false} &&
config.steamInputAppId == std::optional<uint32_t>{1234},
"Steam Input settings were not loaded")) return 1;
}
{
std::istringstream input("[controller]\nsteam_input_app_id = 0\n");
const RuntimeUserConfig config = RuntimeConfigFile::ParseConfig(input, "steam-zero.toml");
if (!Require(!config.steamInputAppId, "zero Steam Input app id was accepted")) return 1;
}
if (!SetsFps("[video]\nshow_fps = false\n", "bare key was not replaced")) return 1;
if (!SetsFps("[video]\n\"show_fps\" = false\n", "basic-quoted key was not replaced")) return 1;
if (!SetsFps("[video]\n'show_fps'=false\n", "literal-quoted key was not replaced")) return 1;