Skip to content

feat: per-game vibration mode (off/controller/device) + intensity#1564

Open
TideGear wants to merge 1 commit into
utkarshdalal:masterfrom
TideGear:vibration-fixes
Open

feat: per-game vibration mode (off/controller/device) + intensity#1564
TideGear wants to merge 1 commit into
utkarshdalal:masterfrom
TideGear:vibration-fixes

Conversation

@TideGear

@TideGear TideGear commented Jun 10, 2026

Copy link
Copy Markdown

Description

Adds per-game, user-selectable controller vibration (mode + intensity) on top of upstream's evshim, single-controller — and fixes rumble being capped at ~1 second.

A new Vibration dropdown (off / controller / device) and an intensity slider in the controller settings let each game route rumble to the physical pad's motors, to the phone's vibrator, or off, scaled 0–100%.

The ~1s cap is fixed too: SDL auto-expires a rumble after its duration and calls the virtual joystick's OnRumble(0,0) itself, which evshim relayed to the app as a stop. That can't be fixed app-side (the value is zeroed at the source), so the fix lives in evshim.c.

Scope: single controller only — multi-controller is intentionally out of scope (upstream's evshim is single-controller). Phone vibration is pure Java reading the rumble values already present in the futex shared memory, so there's no new shared-memory contract.

Changes

  • WinHandler.javasetVibrationMode / setVibrationIntensity + reconcileActiveRumble (applies a setting change to in-flight rumble); per-motor rumbleViaVibratorManager, vibrateDevice, and blendMotors (floors to ≥1 so a high-freq-only rumble isn't silenced on single-motor targets); VibrationAttributes gated to API 33 with an AudioAttributes fallback (minSdk 26). A keepalive thread refreshes the Android one-shot while the poller is blocked in waitForRumble (controller refreshed frequently at 500 ms; the 60 s device one-shot only near expiry). All apply/cancel is serialized on rumbleLock across the poller, keepalive, and UI threads.
  • evshim.c — SDL rumble keepalive. A thread re-arms SDL with the live shm rumble every 500 ms so its internal expiry timer never fires the false OnRumble(0,0), preserving XInput "set and forget". A thread-local t_keepalive_active guard makes the synchronous OnRumble re-entry from our own re-arm a no-op so it isn't echoed back into shm. libevshim.so is rebuilt from this source (build-evshim.ps1 + sdl2_stub type stubs; 16 KB-aligned, stripped; JNI exports verified, SDL still resolved via dlsym).
  • Settings / UIvibrationMode + vibrationIntensity persisted in ContainerData / PrefManager / ContainerUtils; mode dropdown + intensity slider in ControllerTab; applied on launch via XServerScreen.

Testing

Built :app:installModernDebug and verified on a Galaxy S25 Ultra with a physical XInput controller: controller-mode rumble now sustains for its full in-game duration (a 3 s effect is no longer cut to 1 s) and stops promptly when the game ends it; device mode buzzes the phone; intensity scales; off suppresses. libevshim.so rebuilt with build-evshim.ps1 (NDK 26.1, 16 KB-aligned).

Recording

https://photos.app.goo.gl/uRuX5h6xcWU9iZHf9

Type of Change

  • Bug fix
  • Performance / stability improvement
  • Compatibility improvements
  • Other (requires prior approval)

Checklist

  • If I have access to #code-changes, I have discussed this change there and it has been green-lighted. If I do not have access, I have still provided clear context in this PR. If I skip both, I accept that this change may face delays in review, may not be reviewed at all, or may be closed.
  • This change aligns with the current project scope (core functionality, stability, or performance). If not, it has been explicitly approved beforehand.
  • I have attached a recording of the change.
  • I have read and agree to the contribution guidelines in CONTRIBUTING.md.

Summary by cubic

Adds per‑game vibration mode (off/controller/device) with intensity control and sustains rumble for its full in‑game duration. Fixes the ~1s rumble cutoff caused by SDL auto‑expiry.

  • New Features

    • Controller settings add a Vibration dropdown (off/controller/device) and an intensity slider; values persist per game via ContainerData/PrefManager and apply on launch in XServerScreen.
    • Controller mode drives pad motors via VibratorManager (per‑motor when available); Device mode vibrates the phone; Off disables. Scope: single controller.
    • Live changes take effect immediately via WinHandler.setVibrationMode/setVibrationIntensity and reconcileActiveRumble; a keepalive thread refreshes one‑shots so long effects continue.
  • Bug Fixes

    • Remove ~1s rumble cap by re‑arming SDL rumble in evshim.c using SDL_JoystickRumble on a timer and guarding OnRumble re‑entry, preserving XInput “set and forget” without changing the shared‑memory contract.

Written for commit 36e58fd. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

Release Notes

  • New Features
    • Added vibration settings to control rumble behavior with three modes: Off, Controller, and Device.
    • Added vibration intensity slider to adjust controller rumble strength from 0–100%.
    • Improved rumble keepalive support for sustained controller vibration during gameplay.

Adds user-selectable vibration routing and intensity on top of upstream's
evshim, single-controller. "device" mode vibrates the phone from the rumble
values in the existing futex shared memory; "controller" drives the pad's
motors per-motor via VibratorManager; "off" suppresses rumble.

WinHandler: setVibrationMode/setVibrationIntensity + reconcileActiveRumble;
per-motor rumbleViaVibratorManager + vibrateDevice + blendMotors (floors to >=1
so a high-freq-only rumble isn't silenced); VibrationAttributes gated to API 33
with an AudioAttributes fallback (minSdk 26). A keepalive thread refreshes the
Android one-shot while the poller is blocked in waitForRumble (controller 500ms
frequent; 60s device one-shot only near expiry); apply/cancel serialized on
rumbleLock across the poller, keepalive, and UI threads.

evshim.c: SDL rumble keepalive. Wine drives the virtual joystick via
SDL_JoystickRumble with a duration; SDL auto-expires it and calls OnRumble(0,0)
itself (~1s), capping vibration. A thread re-arms SDL with the live shm rumble
every 500ms so the expiry never fires, preserving XInput set-and-forget; the
t_keepalive_active guard makes the synchronous OnRumble re-entry a no-op so it
isn't echoed back into shm. libevshim.so rebuilt (build-evshim.ps1 + sdl2_stub;
16KB-aligned, stripped).

Settings/UI: vibrationMode + vibrationIntensity in ContainerData / PrefManager /
ContainerUtils, a mode dropdown + intensity slider in ControllerTab, applied on
launch via XServerScreen.
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a comprehensive vibration keepalive and configuration system spanning native SDL rumble re-arming in C, Kotlin configuration and persistence, Compose UI controls, and a refactored Java vibration execution engine with per-motor control and mode-based routing.

Changes

Vibration Keepalive and Configuration System

Layer / File(s) Summary
SDL2 stub header and native keepalive foundation
app/src/main/cpp/evshim/sdl2_stub/SDL2/SDL.h
Defines minimal SDL2 types, macros, and SDL_VirtualJoystickDesc struct layout with no function prototypes, enabling runtime dlsym() resolution.
Native SDL keepalive thread implementation
app/src/main/cpp/evshim/evshim.c
Adds vjoy_handles[] array, TLS guard flag t_keepalive_active, rumble_keepalive thread that periodically reads shm rumble and calls SDL_JoystickRumble with 2000ms duration, and wires symbol loading and thread startup during SDL init.
Build infrastructure for libevshim.so
build-evshim.ps1
PowerShell script locates NDK/SDK, compiles evshim.c with NDK Clang targeting API 29 aarch64 with 16KB page-size settings, and strips the resulting shared library.
Vibration configuration model and persistence
app/src/main/java/com/winlator/container/ContainerData.kt
ContainerData adds vibrationMode (default "controller") and vibrationIntensity (default 100) fields; mapSaver validates mode against allowlist and clamps intensity to 0–100 during serialization/deserialization.
Settings normalization and container flow
app/src/main/java/app/gamenative/PrefManager.kt, app/src/main/java/app/gamenative/utils/ContainerUtils.kt
PrefManager introduces normalizeVibrationModeInput() to validate mode ("off", "controller", "device") and clamp intensity; ContainerUtils integrates vibration fields into default data, reads/writes from container extras, and applies normalization throughout the flow.
Vibration UI configuration and wiring
app/src/main/java/app/gamenative/ui/component/dialog/ControllerTab.kt, app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt, app/src/main/res/values/strings.xml
ControllerTabContent adds mode dropdown and conditional intensity slider; XServerScreen passes normalized mode and parsed intensity from container extras to WinHandler; strings.xml provides UI labels.
Vibration routing engine and keepalive scheduling
app/src/main/java/com/winlator/winhandler/WinHandler.java (core refactor)
Introduces rumbleLock and rumbleKeepaliveThread for sync; adds setVibrationMode()/setVibrationIntensity() that reconcile active rumble; refactors rumble poller with sequence-based change detection; implements startRumbleKeepalive() with per-mode cadence; replaces amplitude scaling with per-motor VibratorManager (API 31+) and device vibration with curved mapping; introduces reconcileActiveRumble() for immediate re-application under new settings.
Vibration lifecycle management
app/src/main/java/com/winlator/winhandler/WinHandler.java (shutdown and API gating)
stop() interrupts and joins keepalive and poller threads; constructor conditionally initializes vibrationAttrs only on TIRAMISU+ with fallback to AudioAttributes on older API levels.

Sequence Diagram(s)

sequenceDiagram
  participant Game
  participant evshim
  participant rumble_keepalive
  participant SDL_JoystickRumble
  Game->>evshim: OnRumble (write to shm)
  rumble_keepalive->>evshim: read shm rumble values (500ms)
  evshim->>SDL_JoystickRumble: call with 2000ms duration
  SDL_JoystickRumble->>rumble_keepalive: re-arm joystick
  Note over evshim: t_keepalive_active blocks OnRumble echo
Loading
sequenceDiagram
  participant Game
  participant RumblePoller
  participant rumbleLock
  participant VibratorManager
  participant Vibrator
  Game->>RumblePoller: writes rumble to shm
  RumblePoller->>rumbleLock: waitForRumble detects change
  rumbleLock->>VibratorManager: startVibration routes by mode
  alt mode == "controller"
    VibratorManager->>VibratorManager: rumbleViaVibratorManager (API 31+)
  else mode == "device"
    VibratorManager->>Vibrator: vibrateDevice with curved amplitude
  else mode == "off"
    VibratorManager->>Vibrator: stopVibration
  end
  VibratorManager->>RumblePoller: return
  RumblePoller->>RumblePoller: keepalive reschedules at cadence
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • utkarshdalal/GameNative#1504: The rumble keepalive logic in evshim.c reads rumble values from gamepad_shm, and this PR changes the shared-memory directory location used by that source.
  • utkarshdalal/GameNative#1047: Both PRs modify rumble/vibration logic in WinHandler.java by restructuring rumble handling and routing—this PR adds keepalive and mode/intensity behavior; the retrieved PR adds multi-slot rumble arrays.
  • utkarshdalal/GameNative#538: Both PRs modify WinHandler's rumble poller logic that reads from shared-memory values and drives haptics/keepalive behavior.

Suggested reviewers

  • utkarshdalal
  • phobos665

Poem

🎮✨ Rumble keepalive threads through the shm,
Vibrations dance to controller's whim,
Mode-based routing finds the perfect feel,
Each motor speaks—a haptic squeal! 🐰💫

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: per-game vibration mode (off/controller/device) + intensity' clearly and concisely summarizes the main feature addition—selectable vibration modes and intensity control per game.
Description check ✅ Passed The PR description comprehensively addresses all required template sections: explains what changed and why, includes a recording link, correctly marks the change type, and all checklist items are checked with clear context about prior approval and testing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Infer (1.2.0)
app/src/main/cpp/evshim/evshim.c

app/src/main/cpp/evshim/evshim.c:16:10: fatal error: 'jni.h' file not found
16 | #include <jni.h>
| ^~~~~~~
1 error generated.
Error: the following clang command did not run successfully:
/opt/infer-linux-x86_64-v1.2.0/lib/infer/facebook-clang-plugins/clang/install/bin/clang-18
@/tmp/coderabbit-infer/36e58fd7056261904817db562f1a73e4bf56f4c2-5ae46e918017bef5/tmp/clang_command_.tmp.d16de9.txt
++Contents of '/tmp/coderabbit-infer/36e58fd7056261904817db562f1a73e4bf56f4c2-5ae46e918017bef5/tmp/clang_command_.tmp.d16de9.txt':
"-cc1" "-load"
"/opt/infer-linux-x86_64-v1.2.0/lib/infer/infer/bin/../../facebook-clang-plugins/libtooling/build/FacebookClangPlugin.dylib"
"-add-plugin" "BiniouASTExporter" "-plugin-arg-BiniouASTExporter" "-"
"-plugin-arg-BiniouASTExporter" "PREPEND_CURRENT_DIR=1"
"-plugin-arg-BiniouASTExporter" "MAX_STRING_SIZE=65535" "-cc1" "-triple"
"x86_64-unknown-linux-gnu" "-emit-obj" "-mrelax-all" "-disable-free"
"-clear-ast

... [truncated 689 characters] ...

ux-x86_64-v1.2.0/lib/infer/facebook-clang-plugins/clang/install/lib/clang/18/include"
"-internal-isystem" "/usr/local/include" "-internal-isystem"
"/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/include"
"-internal-externc-isystem" "/usr/include/x86_64-linux-gnu"
"-internal-externc-isystem" "/include" "-internal-externc-isystem"
"/usr/include" "-Wno-ignored-optimization-argument" "-Wno-everything"
"-ferror-limit" "19" "-fgnuc-version=4.2.1" "-fskip-odr-check-in-gmf"
"-D__GCC_HAVE_DWARF2_CFI_ASM=1" "-o"
"/tmp/coderabbit-infer/5ae46e918017bef5/file.o" "-x" "c"
"app/src/main/cpp/evshim/evshim.c" "-O0" "-fno-builtin" "-include"
"/opt/infer-linux-x86_64-v1.2.0/lib/infer/infer/bin/../lib/clang_wrappers/global_defines.h"
"-Wno-everything"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 11 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt:1921">
P2: Per-game vibration settings are only initialized inside `if (PluviaApp.xEnvironment == null)`, so they are lost when a new WinHandler is created during activity recreation while a game is still running.</violation>
</file>

<file name="app/src/main/java/app/gamenative/ui/component/dialog/ControllerTab.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/component/dialog/ControllerTab.kt:71">
P2: Vibration mode string literals are duplicated in the UI instead of referencing a single source of truth, creating schema-drift risk across PrefManager, WinHandler, ContainerData, and XServerScreen.</violation>
</file>

<file name="app/src/main/res/values/strings.xml">

<violation number="1" location="app/src/main/res/values/strings.xml:768">
P2: New vibration UI strings are missing from locale-specific `values-*/strings.xml` files, causing non-English users to see English fallback text for these settings.</violation>
</file>

<file name="build-evshim.ps1">

<violation number="1" location="build-evshim.ps1:16">
P2: Hardcoded native compile API level (29) may be incompatible with lower-minSdk variants that load this shared library.</violation>

<violation number="2" location="build-evshim.ps1:48">
P2: Strip step lacks exit-code checking, so script can silently report success on failure</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

}
handler.setPreferredInputApi(PreferredInputApi.values()[container.inputType])
handler.setDInputMapperType(container.dinputMapperType)
handler.setVibrationMode(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Per-game vibration settings are only initialized inside if (PluviaApp.xEnvironment == null), so they are lost when a new WinHandler is created during activity recreation while a game is still running.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt, line 1921:

<comment>Per-game vibration settings are only initialized inside `if (PluviaApp.xEnvironment == null)`, so they are lost when a new WinHandler is created during activity recreation while a game is still running.</comment>

<file context>
@@ -1918,6 +1918,12 @@ fun XServerScreen(
                             }
                             handler.setPreferredInputApi(PreferredInputApi.values()[container.inputType])
                             handler.setDInputMapperType(container.dinputMapperType)
+                            handler.setVibrationMode(
+                                PrefManager.normalizeVibrationModeInput(
+                                    container.getExtra("vibrationMode", "controller"),
</file context>

stringResource(R.string.vibration_mode_option_controller),
stringResource(R.string.vibration_mode_option_device),
)
val vibrationModeValues = listOf("off", "controller", "device")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Vibration mode string literals are duplicated in the UI instead of referencing a single source of truth, creating schema-drift risk across PrefManager, WinHandler, ContainerData, and XServerScreen.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/ui/component/dialog/ControllerTab.kt, line 71:

<comment>Vibration mode string literals are duplicated in the UI instead of referencing a single source of truth, creating schema-drift risk across PrefManager, WinHandler, ContainerData, and XServerScreen.</comment>

<file context>
@@ -51,6 +63,40 @@ fun ControllerTabContent(state: ContainerConfigState, default: Boolean) {
+            stringResource(R.string.vibration_mode_option_controller),
+            stringResource(R.string.vibration_mode_option_device),
+        )
+        val vibrationModeValues = listOf("off", "controller", "device")
+        val vibrationModeIndex = vibrationModeValues.indexOf(normalizedVibrationMode).coerceAtLeast(0)
+        SettingsListDropdown(
</file context>
Suggested change
val vibrationModeValues = listOf("off", "controller", "device")
val vibrationModeValues = PrefManager.VIBRATION_MODE_VALUES

@@ -764,6 +764,14 @@
<string name="shooter_mode_toggle_description">Auto-replace stick elements with dynamic joysticks</string>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: New vibration UI strings are missing from locale-specific values-*/strings.xml files, causing non-English users to see English fallback text for these settings.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/res/values/strings.xml, line 768:

<comment>New vibration UI strings are missing from locale-specific `values-*/strings.xml` files, causing non-English users to see English fallback text for these settings.</comment>

<file context>
@@ -764,6 +764,14 @@
     <string name="shooter_mode_on">Enable Mouse</string>
     <string name="shooter_mode_off">Enable Sticks</string>
+
+    <!-- Vibration Settings -->
+    <string name="vibration_mode">Vibration Mode</string>
+    <string name="vibration_intensity">Vibration Intensity</string>
</file context>

Comment thread build-evshim.ps1
# Any installed NDK with 16 KB page-size support works; this is the one used to
# produce the committed .so.
$NDK_VERSION = "26.1.10909125"
$API = 29 # matches the modern flavor's minSdk

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Hardcoded native compile API level (29) may be incompatible with lower-minSdk variants that load this shared library.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At build-evshim.ps1, line 16:

<comment>Hardcoded native compile API level (29) may be incompatible with lower-minSdk variants that load this shared library.</comment>

<file context>
@@ -0,0 +1,50 @@
+# Any installed NDK with 16 KB page-size support works; this is the one used to
+# produce the committed .so.
+$NDK_VERSION = "26.1.10909125"
+$API         = 29   # matches the modern flavor's minSdk
+
+if     ($env:ANDROID_SDK_ROOT) { $SDK_ROOT = $env:ANDROID_SDK_ROOT }
</file context>

Comment thread build-evshim.ps1
& $CLANG @clangArgs
if ($LASTEXITCODE -ne 0) { Write-Error "Compilation failed (exit $LASTEXITCODE)" }

& $STRIP --strip-unneeded $OUT

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Strip step lacks exit-code checking, so script can silently report success on failure

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At build-evshim.ps1, line 48:

<comment>Strip step lacks exit-code checking, so script can silently report success on failure</comment>

<file context>
@@ -0,0 +1,50 @@
+& $CLANG @clangArgs
+if ($LASTEXITCODE -ne 0) { Write-Error "Compilation failed (exit $LASTEXITCODE)" }
+
+& $STRIP --strip-unneeded $OUT
+
+Write-Host "OK  ->  $OUT"
</file context>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (3)
app/src/main/cpp/evshim/evshim.c (1)

291-309: 💤 Low value

Infinite keepalive loop with no shutdown mechanism.

The rumble_keepalive thread runs an unbounded for(;;) loop (line 295) and is detached (line 373), so it cannot be joined or signaled to stop. While the OS will reclaim the thread when the Wine process exits, the lack of a clean shutdown creates a window where the thread may access shm[] after it has been unmapped by other cleanup paths.

♻️ Optional: Add shutdown signal

Introduce a global shutdown flag:

+static volatile int g_shutdown = 0;
+
 static void *rumble_keepalive(void *arg)
 {
     (void)arg;
     for (;;) {
+        if (g_shutdown) break;
         usleep(RUMBLE_KEEPALIVE_US);

And expose a JNI function to set it:

+JNIEXPORT void JNICALL
+Java_com_winlator_winhandler_WinHandler_shutdownKeepalive(JNIEnv *env, jclass cls)
+{
+    g_shutdown = 1;
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/cpp/evshim/evshim.c` around lines 291 - 309, The
rumble_keepalive thread uses an infinite loop and is detached, so add a global
atomic/shutdown flag (e.g., volatile sig_atomic_t or atomic_bool named
rumble_keepalive_shutdown) and check it inside rumble_keepalive's loop to break
out cleanly; update the loop condition from for(;;) to
while(!rumble_keepalive_shutdown) (or check and break after usleep) and ensure
any access to shm[]/vjoy_handles is guarded by that flag to avoid
use-after-unmap. Also add and export a small setter function (e.g.,
evshim_request_rumble_shutdown or JNI-exported method) that sets
rumble_keepalive_shutdown = true during cleanup so the detached thread can exit
before shared memory is released; leave t_keepalive_active semantics unchanged
but ensure cleanup sequences set the shutdown flag before unmapping shm[].
build-evshim.ps1 (2)

13-16: 💤 Low value

Consider documenting the NDK version requirement more prominently.

The script requires a specific NDK version (26.1.10909125) which may not be installed on all developer machines. The error message on line 31 mentions this, but developers discovering the script for the first time might miss the requirement.

📝 Optional: Add version check guidance to header comment
 # build-evshim.ps1
 # Rebuilds app/src/main/cpp/evshim/evshim.c into libevshim.so using the Android
 # NDK clang toolchain, then strips it and copies it into jniLibs.
 #
+# Prerequisites:
+#   - Android NDK 26.1.10909125 (or edit $NDK_VERSION below for an installed version)
+#   - Windows OS (uses windows-x86_64 prebuilt toolchain)
+#
 # SDL functions are resolved at runtime via dlsym(), so only the minimal SDL type
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@build-evshim.ps1` around lines 13 - 16, The script hardcodes NDK_VERSION =
"26.1.10909125" and API = 29 but the requirement is only mentioned later; update
the top-of-file header comment to prominently state the required NDK version
(26.1.10909125), the reason (16 KB page-size support) and how to install or
select it, and optionally add a small runtime validation that reads the
installed NDK/version and compares it to NDK_VERSION (emitting a clear error
advising installation or selection of the required NDK) so developers won’t miss
the requirement when first opening the script.

23-23: ⚖️ Poor tradeoff

Windows-only toolchain path limits cross-platform builds.

The script hardcodes windows-x86_64 in the toolchain path (line 23), preventing use on Linux or macOS. Developers on those platforms would need to manually modify the script or use a different build approach.

♻️ Optional: Detect host OS for cross-platform support
+$OS_ARCH = if ($IsLinux) { "linux-x86_64" } 
+          elseif ($IsMacOS) { "darwin-x86_64" }
+          else { "windows-x86_64" }
+
 $NDK_ROOT = "$SDK_ROOT\ndk\$NDK_VERSION"
-$CLANG    = "$NDK_ROOT\toolchains\llvm\prebuilt\windows-x86_64\bin\aarch64-linux-android$API-clang.cmd"
-$STRIP    = "$NDK_ROOT\toolchains\llvm\prebuilt\windows-x86_64\bin\llvm-strip.exe"
+$CLANG    = "$NDK_ROOT\toolchains\llvm\prebuilt\$OS_ARCH\bin\aarch64-linux-android$API-clang$(if ($IsWindows) {'.cmd'} else {''})"
+$STRIP    = "$NDK_ROOT\toolchains\llvm\prebuilt\$OS_ARCH\bin\llvm-strip$(if ($IsWindows) {'.exe'} else {''})"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@build-evshim.ps1` at line 23, The build script hardcodes the toolchain host
folder in the $CLANG path which breaks non-Windows builds; update
build-evshim.ps1 to detect the host OS (use PowerShell's automatic variables
like $IsWindows, $IsLinux, $IsMacOS or
[System.Runtime.InteropServices.RuntimeInformation] if needed), compute a host
tag variable (e.g. $HOST_TAG = 'windows-x86_64' / 'linux-x86_64' /
'darwin-x86_64') and substitute it into the $CLANG assignment (replace the
hardcoded "windows-x86_64" with $HOST_TAG) so $CLANG is correct across
platforms.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/cpp/evshim/evshim.c`:
- Around line 300-301: The rumble_keepalive function is performing non-atomic
reads of shm[i]->state.low_freq_rumble and high_freq_rumble while OnRumble
writes them and issues a seq_cst fence, which is a data race under C11; fix it
by adding an acquire ordering before reading the rumble fields in
rumble_keepalive (e.g., call atomic_thread_fence(memory_order_acquire) or use
atomic_load_explicit on the shared state) so the reads synchronize with the
seq_cst fence in OnRumble and guarantee visibility of the latest
low_freq_rumble/high_freq_rumble values from shm.
- Around line 291-309: The rumble_keepalive thread reads vjoy_handles[]
concurrently with vjoy_updater writing it, causing a race; fix by preventing the
keepalive loop from accessing vjoy_handles until initialization completes —
e.g., in rumble_keepalive add a short startup wait or loop that sleeps until an
initialization flag set by vjoy_updater is true (use a simple volatile or atomic
bool like vjoy_initialized) before entering the main for(;;) loop, or
alternatively make vjoy_handles stores/loads atomic (change vjoy_handles to
atomic pointers, store in vjoy_updater and load atomically in rumble_keepalive)
so p_SDL_JoystickRumble is only called with a valid js pointer.

In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt`:
- Line 1926: The call to handler.setVibrationIntensity uses
container.getExtra(...).toIntOrNull() ?: 100 without bounding the value,
allowing out-of-range inputs; update the call site to parse the int and clamp it
into 0..100 (e.g., use Kotlin's coerceIn) before passing to
setVibrationIntensity so values like -1 or 500 are constrained to the valid
range; locate this in XServerScreen where container.getExtra(...) and
handler.setVibrationIntensity(...) are used and apply the clamp to the parsed
integer.

In `@app/src/main/java/com/winlator/winhandler/WinHandler.java`:
- Around line 869-873: resolveInputDevice currently uses currentControllerId
which can be stale; change it to derive the InputDevice from the live controller
object (currentController) instead: if currentController is null return null,
otherwise use the controller's device id/method to fetch the InputDevice (e.g.,
InputDevice.getDevice(currentController.getId()) or equivalent) so rumble/cancel
targets the active controller; ensure setCurrentController remains the single
source of truth for currentController and remove reliance on currentControllerId
inside resolveInputDevice.
- Around line 413-421: stop() currently tears down threads but doesn't abort any
in-flight one-shot vibration; ensure you explicitly cancel active rumble before
joining threads by invoking a cancel/stop routine (e.g., extend or use
rumbleTeardown to send an immediate "stop vibration" command or add a
cancelRumble() method) and call that immediately at the start of stop() (before
interrupt/join of rumbleKeepaliveThread and rumblePollerThread) so the device
won't continue vibrating after handler shutdown; update rumbleTeardown or add
cancelRumble() to emit the device stop command and ensure stop() calls it.
- Around line 927-938: The code sets isRumbling true before actually starting
the rumble path, causing false positives if vibrateController/vibrateDevice
fails; change startVibration (synchronized on rumbleLock) to only set isRumbling
after a successful start: call vibrateDevice(lowFreq, highFreq) or
vibrateController(lowFreq, highFreq) based on vibrationMode, check their
return/status (or catch exceptions) to confirm the route started, and then set
isRumbling = true; on failure, call stopVibration() and leave isRumbling false
so the keepalive thread won’t repeatedly retry. Ensure you update logic around
startVibration, vibrateController, vibrateDevice, stopVibration and isRumbling
accordingly.

---

Nitpick comments:
In `@app/src/main/cpp/evshim/evshim.c`:
- Around line 291-309: The rumble_keepalive thread uses an infinite loop and is
detached, so add a global atomic/shutdown flag (e.g., volatile sig_atomic_t or
atomic_bool named rumble_keepalive_shutdown) and check it inside
rumble_keepalive's loop to break out cleanly; update the loop condition from
for(;;) to while(!rumble_keepalive_shutdown) (or check and break after usleep)
and ensure any access to shm[]/vjoy_handles is guarded by that flag to avoid
use-after-unmap. Also add and export a small setter function (e.g.,
evshim_request_rumble_shutdown or JNI-exported method) that sets
rumble_keepalive_shutdown = true during cleanup so the detached thread can exit
before shared memory is released; leave t_keepalive_active semantics unchanged
but ensure cleanup sequences set the shutdown flag before unmapping shm[].

In `@build-evshim.ps1`:
- Around line 13-16: The script hardcodes NDK_VERSION = "26.1.10909125" and API
= 29 but the requirement is only mentioned later; update the top-of-file header
comment to prominently state the required NDK version (26.1.10909125), the
reason (16 KB page-size support) and how to install or select it, and optionally
add a small runtime validation that reads the installed NDK/version and compares
it to NDK_VERSION (emitting a clear error advising installation or selection of
the required NDK) so developers won’t miss the requirement when first opening
the script.
- Line 23: The build script hardcodes the toolchain host folder in the $CLANG
path which breaks non-Windows builds; update build-evshim.ps1 to detect the host
OS (use PowerShell's automatic variables like $IsWindows, $IsLinux, $IsMacOS or
[System.Runtime.InteropServices.RuntimeInformation] if needed), compute a host
tag variable (e.g. $HOST_TAG = 'windows-x86_64' / 'linux-x86_64' /
'darwin-x86_64') and substitute it into the $CLANG assignment (replace the
hardcoded "windows-x86_64" with $HOST_TAG) so $CLANG is correct across
platforms.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 24bef65d-bc3e-49a1-8c39-0e100a90af6f

📥 Commits

Reviewing files that changed from the base of the PR and between a0b49d1 and 36e58fd.

⛔ Files ignored due to path filters (1)
  • app/src/main/jniLibs/arm64-v8a/libevshim.so is excluded by !**/*.so
📒 Files selected for processing (10)
  • app/src/main/cpp/evshim/evshim.c
  • app/src/main/cpp/evshim/sdl2_stub/SDL2/SDL.h
  • app/src/main/java/app/gamenative/PrefManager.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/ControllerTab.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
  • app/src/main/java/app/gamenative/utils/ContainerUtils.kt
  • app/src/main/java/com/winlator/container/ContainerData.kt
  • app/src/main/java/com/winlator/winhandler/WinHandler.java
  • app/src/main/res/values/strings.xml
  • build-evshim.ps1

Comment on lines +291 to +309
// Re-arms SDL's rumble before its internal expiry timer can fire a false OnRumble(0,0).
static void *rumble_keepalive(void *arg)
{
(void)arg;
for (;;) {
usleep(RUMBLE_KEEPALIVE_US);
for (int i = 0; i < MAX_GAMEPADS; i++) {
SDL_Joystick *js = vjoy_handles[i];
if (!js || !shm[i]) continue;
uint16_t lo = shm[i]->state.low_freq_rumble;
uint16_t hi = shm[i]->state.high_freq_rumble;
if (lo == 0 && hi == 0) continue;
t_keepalive_active = 1;
p_SDL_JoystickRumble(js, lo, hi, RUMBLE_KEEPALIVE_DUR_MS);
t_keepalive_active = 0;
}
}
return NULL;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Race on vjoy_handles[] between initialization and keepalive reads.

rumble_keepalive (line 298) reads vjoy_handles[i] concurrently with vjoy_updater writing it (line 250) with no synchronization. If the keepalive thread starts before vjoy_updater completes initialization, it may read NULL or a partially-written pointer, leading to a null-pointer dereference or invalid rumble calls.

🔒 Proposed fix: Add memory barrier or delay keepalive start

Option 1 (preferred): Add a small startup delay to rumble_keepalive

 static void *rumble_keepalive(void *arg)
 {
     (void)arg;
+    // Wait for vjoy_updater threads to complete initialization
+    usleep(RUMBLE_KEEPALIVE_US);
     for (;;) {
         usleep(RUMBLE_KEEPALIVE_US);

Option 2: Use atomic store/load for vjoy_handles[]

Change the declaration:

-static SDL_Joystick *vjoy_handles[MAX_GAMEPADS];
+static _Atomic(SDL_Joystick *) vjoy_handles[MAX_GAMEPADS];

Update the store in vjoy_updater:

-    vjoy_handles[idx] = js;
+    atomic_store_explicit(&vjoy_handles[idx], js, memory_order_release);

Update the load in rumble_keepalive:

-        SDL_Joystick *js = vjoy_handles[i];
+        SDL_Joystick *js = atomic_load_explicit(&vjoy_handles[i], memory_order_acquire);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Re-arms SDL's rumble before its internal expiry timer can fire a false OnRumble(0,0).
static void *rumble_keepalive(void *arg)
{
(void)arg;
for (;;) {
usleep(RUMBLE_KEEPALIVE_US);
for (int i = 0; i < MAX_GAMEPADS; i++) {
SDL_Joystick *js = vjoy_handles[i];
if (!js || !shm[i]) continue;
uint16_t lo = shm[i]->state.low_freq_rumble;
uint16_t hi = shm[i]->state.high_freq_rumble;
if (lo == 0 && hi == 0) continue;
t_keepalive_active = 1;
p_SDL_JoystickRumble(js, lo, hi, RUMBLE_KEEPALIVE_DUR_MS);
t_keepalive_active = 0;
}
}
return NULL;
}
// Re-arms SDL's rumble before its internal expiry timer can fire a false OnRumble(0,0).
static void *rumble_keepalive(void *arg)
{
(void)arg;
// Wait for vjoy_updater threads to complete initialization
usleep(RUMBLE_KEEPALIVE_US);
for (;;) {
usleep(RUMBLE_KEEPALIVE_US);
for (int i = 0; i < MAX_GAMEPADS; i++) {
SDL_Joystick *js = vjoy_handles[i];
if (!js || !shm[i]) continue;
uint16_t lo = shm[i]->state.low_freq_rumble;
uint16_t hi = shm[i]->state.high_freq_rumble;
if (lo == 0 && hi == 0) continue;
t_keepalive_active = 1;
p_SDL_JoystickRumble(js, lo, hi, RUMBLE_KEEPALIVE_DUR_MS);
t_keepalive_active = 0;
}
}
return NULL;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/cpp/evshim/evshim.c` around lines 291 - 309, The
rumble_keepalive thread reads vjoy_handles[] concurrently with vjoy_updater
writing it, causing a race; fix by preventing the keepalive loop from accessing
vjoy_handles until initialization completes — e.g., in rumble_keepalive add a
short startup wait or loop that sleeps until an initialization flag set by
vjoy_updater is true (use a simple volatile or atomic bool like
vjoy_initialized) before entering the main for(;;) loop, or alternatively make
vjoy_handles stores/loads atomic (change vjoy_handles to atomic pointers, store
in vjoy_updater and load atomically in rumble_keepalive) so p_SDL_JoystickRumble
is only called with a valid js pointer.

Comment on lines +300 to +301
uint16_t lo = shm[i]->state.low_freq_rumble;
uint16_t hi = shm[i]->state.high_freq_rumble;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Concurrent reads of shm rumble state without acquire semantics.

rumble_keepalive reads shm[i]->state.low_freq_rumble and high_freq_rumble (lines 300-301) concurrently with OnRumble writing them (lines 205-206). While OnRumble issues a seq_cst fence on line 209, rumble_keepalive does not use an acquire load, so it may see stale rumble values on weakly-ordered architectures.

In practice, the impact is low—stale values cause at most a 500ms delay before the next refresh—but the lack of explicit ordering is a data race under the C11 memory model.

🛡️ Proposed fix: Add acquire fence before reading rumble state
         for (int i = 0; i < MAX_GAMEPADS; i++) {
             SDL_Joystick *js = vjoy_handles[i];
             if (!js || !shm[i]) continue;
+            atomic_thread_fence(memory_order_acquire);
             uint16_t lo = shm[i]->state.low_freq_rumble;
             uint16_t hi = shm[i]->state.high_freq_rumble;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uint16_t lo = shm[i]->state.low_freq_rumble;
uint16_t hi = shm[i]->state.high_freq_rumble;
for (int i = 0; i < MAX_GAMEPADS; i++) {
SDL_Joystick *js = vjoy_handles[i];
if (!js || !shm[i]) continue;
atomic_thread_fence(memory_order_acquire);
uint16_t lo = shm[i]->state.low_freq_rumble;
uint16_t hi = shm[i]->state.high_freq_rumble;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/cpp/evshim/evshim.c` around lines 300 - 301, The
rumble_keepalive function is performing non-atomic reads of
shm[i]->state.low_freq_rumble and high_freq_rumble while OnRumble writes them
and issues a seq_cst fence, which is a data race under C11; fix it by adding an
acquire ordering before reading the rumble fields in rumble_keepalive (e.g.,
call atomic_thread_fence(memory_order_acquire) or use atomic_load_explicit on
the shared state) so the reads synchronize with the seq_cst fence in OnRumble
and guarantee visibility of the latest low_freq_rumble/high_freq_rumble values
from shm.

container.getExtra("vibrationMode", "controller"),
),
)
handler.setVibrationIntensity(container.getExtra("vibrationIntensity", "100").toIntOrNull() ?: 100)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clamp vibration intensity before passing to WinHandler.

Line 1926 parses an integer but does not bound it to 0..100. Invalid legacy/imported extras (for example -1 or 500) can bypass UI limits and create out-of-range runtime behavior. Clamp at this call site to keep launch-time behavior consistent.

Proposed fix
-                            handler.setVibrationIntensity(container.getExtra("vibrationIntensity", "100").toIntOrNull() ?: 100)
+                            val vibrationIntensity = (container.getExtra("vibrationIntensity", "100").toIntOrNull() ?: 100)
+                                .coerceIn(0, 100)
+                            handler.setVibrationIntensity(vibrationIntensity)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
handler.setVibrationIntensity(container.getExtra("vibrationIntensity", "100").toIntOrNull() ?: 100)
val vibrationIntensity = (container.getExtra("vibrationIntensity", "100").toIntOrNull() ?: 100)
.coerceIn(0, 100)
handler.setVibrationIntensity(vibrationIntensity)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt` at line
1926, The call to handler.setVibrationIntensity uses
container.getExtra(...).toIntOrNull() ?: 100 without bounding the value,
allowing out-of-range inputs; update the call site to parse the int and clamp it
into 0..100 (e.g., use Kotlin's coerceIn) before passing to
setVibrationIntensity so values like -1 or 500 are constrained to the valid
range; locate this in XServerScreen where container.getExtra(...) and
handler.setVibrationIntensity(...) are used and apply the clamp to the parsed
integer.

Comment on lines 413 to +421
public void stop() {
this.running = false;
rumbleTeardown(0);
if (rumbleKeepaliveThread != null) rumbleKeepaliveThread.interrupt();
try {
if (rumblePollerThread != null)
this.rumblePollerThread.join();
if (rumbleKeepaliveThread != null)
this.rumbleKeepaliveThread.join();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cancel any active rumble during shutdown.

stop() tears down the threads, but it never cancels the in-flight vibration effect itself. In device mode that leaves the phone vibrating for the remainder of the 60s one-shot after the handler stops.

💡 Suggested fix
 public void stop() {
         this.running = false;
+        stopVibration();
         rumbleTeardown(0);
         if (rumbleKeepaliveThread != null) rumbleKeepaliveThread.interrupt();
         try {
             if (rumblePollerThread != null)
                 this.rumblePollerThread.join();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void stop() {
this.running = false;
rumbleTeardown(0);
if (rumbleKeepaliveThread != null) rumbleKeepaliveThread.interrupt();
try {
if (rumblePollerThread != null)
this.rumblePollerThread.join();
if (rumbleKeepaliveThread != null)
this.rumbleKeepaliveThread.join();
public void stop() {
this.running = false;
stopVibration();
rumbleTeardown(0);
if (rumbleKeepaliveThread != null) rumbleKeepaliveThread.interrupt();
try {
if (rumblePollerThread != null)
this.rumblePollerThread.join();
if (rumbleKeepaliveThread != null)
this.rumbleKeepaliveThread.join();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/winlator/winhandler/WinHandler.java` around lines 413 -
421, stop() currently tears down threads but doesn't abort any in-flight
one-shot vibration; ensure you explicitly cancel active rumble before joining
threads by invoking a cancel/stop routine (e.g., extend or use rumbleTeardown to
send an immediate "stop vibration" command or add a cancelRumble() method) and
call that immediately at the start of stop() (before interrupt/join of
rumbleKeepaliveThread and rumblePollerThread) so the device won't continue
vibrating after handler shutdown; update rumbleTeardown or add cancelRumble() to
emit the device stop command and ensure stop() calls it.

Comment on lines +869 to +873
/** Resolves the physical InputDevice currently driving gamepad input, or null if none. */
private InputDevice resolveInputDevice() {
if (currentControllerId < 0) return null;
return InputDevice.getDevice(currentControllerId);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Resolve the rumble target from currentController, not the side-channel ID.

This helper ignores the controller object the class actually maintains. currentController is reassigned in this file, but currentControllerId is only updated through setCurrentController(...), so controller-mode rumble/cancel can target null or a stale device even while input is flowing through the new controller.

💡 Suggested fix
 private InputDevice resolveInputDevice() {
-        if (currentControllerId < 0) return null;
-        return InputDevice.getDevice(currentControllerId);
+        ExternalController controller = currentController;
+        if (controller != null) {
+            return InputDevice.getDevice(controller.getDeviceId());
+        }
+        if (currentControllerId < 0) return null;
+        return InputDevice.getDevice(currentControllerId);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/winlator/winhandler/WinHandler.java` around lines 869 -
873, resolveInputDevice currently uses currentControllerId which can be stale;
change it to derive the InputDevice from the live controller object
(currentController) instead: if currentController is null return null, otherwise
use the controller's device id/method to fetch the InputDevice (e.g.,
InputDevice.getDevice(currentController.getId()) or equivalent) so rumble/cancel
targets the active controller; ensure setCurrentController remains the single
source of truth for currentController and remove reliance on currentControllerId
inside resolveInputDevice.

Comment on lines +927 to +938
private void startVibration(short lowFreq, short highFreq) {
synchronized (rumbleLock) {
if ("off".equals(vibrationMode) || vibrationIntensity == 0) {
stopVibration();
return;
}
isRumbling = true;
if ("device".equals(vibrationMode)) {
vibrateDevice(lowFreq, highFreq);
} else { // "controller"
vibrateController(lowFreq, highFreq);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Only mark rumble active after the route succeeds.

isRumbling is set before the controller/device path is known to have started. If vibrateController(...) fails, the keepalive thread keeps retrying every cycle and will spam warnings until the game sends 0,0.

💡 Suggested fix
 private void startVibration(short lowFreq, short highFreq) {
         synchronized (rumbleLock) {
             if ("off".equals(vibrationMode) || vibrationIntensity == 0) {
                 stopVibration();
                 return;
             }
-            isRumbling = true;
-            if ("device".equals(vibrationMode)) {
-                vibrateDevice(lowFreq, highFreq);
-            } else { // "controller"
-                vibrateController(lowFreq, highFreq);
-            }
+            boolean started;
+            if ("device".equals(vibrationMode)) {
+                vibrateDevice(lowFreq, highFreq);
+                started = true;
+            } else { // "controller"
+                started = vibrateController(lowFreq, highFreq);
+            }
+            isRumbling = started;
         }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/winlator/winhandler/WinHandler.java` around lines 927 -
938, The code sets isRumbling true before actually starting the rumble path,
causing false positives if vibrateController/vibrateDevice fails; change
startVibration (synchronized on rumbleLock) to only set isRumbling after a
successful start: call vibrateDevice(lowFreq, highFreq) or
vibrateController(lowFreq, highFreq) based on vibrationMode, check their
return/status (or catch exceptions) to confirm the route started, and then set
isRumbling = true; on failure, call stopVibration() and leave isRumbling false
so the keepalive thread won’t repeatedly retry. Ensure you update logic around
startVibration, vibrateController, vibrateDevice, stopVibration and isRumbling
accordingly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant