Skip to content

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

Open
TideGear wants to merge 5 commits into
utkarshdalal:masterfrom
TideGear:vibration-fixes
Open

feat: per-game vibration mode (off/controller/device) + intensity#1564
TideGear wants to merge 5 commits 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/KqJqdVYEvqiQhr389

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 routing (off/controller/device) with intensity, and sustains rumble for its full in‑game duration on both controller and phone. evshim.c uses an event‑driven, lazily‑spawned keepalive with C11‑safe atomics to avoid SDL’s ~1s cutoff.

  • New Features

    • Controller settings add a Vibration dropdown (off/controller/device) and an intensity slider; values persist per game via ContainerData/PrefManager/ContainerUtils and apply on launch in XServerScreen.
    • WinHandler routes rumble: per‑motor via VibratorManager (with a blending fallback) or vibrates the device with a haptic curve. Live changes apply via setVibrationMode/setVibrationIntensity and reconcileActiveRumble. Keepalive refreshes at 240 ms (controller) and only near expiry for the 60 s device one‑shot. Scope: single controller.
  • Bug Fixes

    • Android: keepalive is event‑driven (no idle polling) and thread‑safe; startVibration marks rumbling only on success; stop() cancels in‑flight vibration.
    • evshim.c: SDL rumble keepalive re‑arms every 500 ms, ignores self‑reentry via t_keepalive_active, and is created lazily on first non‑zero rumble; it idles on a condvar when inactive. C11 races are fixed with a packed atomic rumble snapshot and atomic vjoy_handles. Enabled only if SDL_JoystickRumble is available. libevshim.so rebuilt against real SDL2 headers (resolved via dlsym).

Written for commit 2149a1d. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Vibration settings: choose Off, Controller, or Device.
    • Intensity slider (0–100%) with validation and clamping.
    • Per-container vibration preferences are saved and restored.
    • Improved rumble handling with a background keepalive to better sustain controller/device feedback.

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

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds vibrationMode/intensity persistence and UI controls; wires settings into WinHandler; implements SDL rumble keepalive in native evshim; refactors WinHandler to route and sustain rumble with mode-based scheduling and per-motor/device routing.

Changes

Vibration Keepalive and Configuration System

Layer / File(s) Summary
Vibration configuration data model
app/src/main/java/com/winlator/container/ContainerData.kt
ContainerData adds vibrationMode (default "controller") and vibrationIntensity (default 100) with mapSaver save/restore that validates mode and clamps intensity to 0..100.
Settings normalization and container integration
app/src/main/java/app/gamenative/PrefManager.kt, app/src/main/java/app/gamenative/utils/ContainerUtils.kt
PrefManager normalizes/validates vibrationMode and clamps vibrationIntensity on read/write; ContainerUtils integrates these fields into default container data, extras parsing, applyToContainer, and new-container initialization.
Vibration UI configuration and string resources
app/src/main/java/app/gamenative/ui/component/dialog/ControllerTab.kt, app/src/main/res/values/strings.xml
Adds a vibration mode dropdown and conditional intensity slider (0–100) in ControllerTab; adds strings for labels and options.
Configuration wiring to native handler
app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
During container activation, passes normalized vibrationMode and parsed/clamped vibrationIntensity to WinHandler.setVibrationMode() and setVibrationIntensity().
Native SDL rumble keepalive thread
app/src/main/cpp/evshim/evshim.c
Adds per-player atomic joystick handles and packed rumble snapshot atomics, resolves SDL_JoystickRumble optionally, suppresses OnRumble during keepalive calls, records joystick handles, and starts a rumble_keepalive thread that re-arms SDL rumble every 500ms when needed.
WinHandler vibration routing engine and keepalive
app/src/main/java/com/winlator/winhandler/WinHandler.java
Introduces rumbleLock, rumbleKeepaliveThread, volatile rumble state, setVibrationMode/setVibrationIntensity, poller refactor to waitForRumble, reconcileActiveRumble, amplitude scaling and blending, per-motor VibratorManager routing (API 31+), device vibrator mapping, and orderly shutdown handling.

Sequence Diagrams

sequenceDiagram
  participant Game
  participant evshim_OnRumble
  participant evshim_keepalive
  participant SDL_JoystickRumble
  Game->>evshim_OnRumble: OnRumble(low, high)
  evshim_OnRumble->>evshim_OnRumble: write shm & atomic snapshot (unless t_keepalive_active)
  loop every 500ms
    evshim_keepalive->>evshim_keepalive: load atomic snapshot
    alt snapshot non-zero and joystick handle present
      evshim_keepalive->>evshim_keepalive: set t_keepalive_active
      evshim_keepalive->>SDL_JoystickRumble: p_SDL_JoystickRumble(player, low, high, duration)
      SDL_JoystickRumble-->>evshim_keepalive: re-arm ack
      evshim_keepalive->>evshim_keepalive: clear t_keepalive_active
    end
  end
Loading
sequenceDiagram
  participant Game
  participant RumblePoller
  participant rumbleLock
  participant WinHandler
  participant VibratorManager
  participant DeviceVibrator
  Game->>RumblePoller: write rumble to shm
  RumblePoller->>rumbleLock: waitForRumble detects change
  rumbleLock->>WinHandler: reconcileActiveRumble/startVibration
  alt mode == "controller"
    WinHandler->>VibratorManager: rumbleViaVibratorManager (per-motor)
  else mode == "device"
    WinHandler->>DeviceVibrator: vibrateDevice (mapped amplitude)
  else mode == "off"
    WinHandler->>VibratorManager: stopVibration
    WinHandler->>DeviceVibrator: cancel
  end
  WinHandler->>RumblePoller: update isRumbling and notify keepalive
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • utkarshdalal
  • phobos665

Poem

🐰 A tiny rabbit hops with glee,
Threads hum steady, joysticks free,
Modes are trimmed and sliders pranced,
Keepalive drums keep motors danced,
Hooray — the rabbit taps its knee!

🚥 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 accurately summarizes the main change: adding per-game vibration mode selection and intensity control.
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.
Description check ✅ Passed The pull request provides a comprehensive description with clear objectives, implementation details, testing confirmation, and proper checklist completion.

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

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

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/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/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/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>

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 Outdated
Comment thread build-evshim.ps1 Outdated

@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 thread app/src/main/cpp/evshim/evshim.c
Comment thread app/src/main/cpp/evshim/evshim.c Outdated
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 thread app/src/main/java/com/winlator/winhandler/WinHandler.java
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 thread app/src/main/java/com/winlator/winhandler/WinHandler.java
Comment thread build-evshim.ps1 Outdated

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

please remove this

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

remove this as well. evshim needs to be built with a real SDL library.

@utkarshdalal

Copy link
Copy Markdown
Owner

Please build evshim.so and push it as well

evshim.c: fix C11 data races in the rumble keepalive — vjoy_handles is now
atomic (release store / acquire load); an acquire fence pairs with OnRumble's
seq_cst fence before reading the shm rumble fields.

WinHandler.java:
- rumble keepalive is now event-driven: it blocks on rumbleLock while idle and
  is woken by startVibration, instead of waking every 240ms when nothing is
  rumbling (also matches the original branch's wait/notify design).
- startVibration sets isRumbling only when a route actually starts
  (vibrateController/vibrateDevice now both report success); on failure it
  clears state so the keepalive won't repeatedly retry.
- stop() cancels any in-flight vibration after joining threads, so the device
  won't keep vibrating after handler shutdown.

Build evshim against REAL SDL2 (2.30.9) headers; remove build-evshim.ps1 and the
sdl2_stub per review. libevshim.so rebuilt: 16KB-aligned, JNI exports intact,
SDL still resolved via dlsym (no libSDL2 link).

@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.

1 issue found across 5 files (changes from recent commits).

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/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/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/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>

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

Re-trigger cubic

Comment thread app/src/main/cpp/evshim/evshim.c Outdated
The previous acquire fence was inert: a standalone atomic_thread_fence(acquire)
only synchronizes-with OnRumble if it pairs with an atomic READ of the released
object (rumble_seq), but the keepalive then read the plain shm rumble fields
directly — no valid release/acquire edge, so a C11 data race remained.

OnRumble now release-stores the rumble into an atomic snapshot
(last_rumble_low/high), and the keepalive acquire-loads it instead of reading
the plain shm fields. Every shared access the keepalive makes is now a proper
atomic acquire load, forming a valid release/acquire pair (the plain shm fields
remain for the cross-process Java reader). libevshim.so rebuilt against real
SDL2 (2.30.9).

@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.

1 issue found across 2 files (changes from recent commits).

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/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/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/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>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread app/src/main/cpp/evshim/evshim.c Outdated
TideGear added 2 commits June 10, 2026 22:50
The keepalive read last_rumble_low/high as two independent atomics, so a stop
(0,0) could be observed half-applied (new low=0 with stale high!=0), failing the
zero check and re-arming a stale vibration after the game stopped. Pack low+high
into a single _Atomic uint32_t so the pair is stored and loaded indivisibly.
libevshim.so rebuilt against real SDL2 2.30.9.
evshim is LD_PRELOAD'd into every wine process, so creating the keepalive at
init meant a thread in every process even when no rumble ever happens. Now it is
created via pthread_once on the first non-zero OnRumble, so processes that never
rumble never spawn it. While active it re-arms SDL every ~500ms via
pthread_cond_timedwait; when no pad is rumbling it blocks on the condvar instead
of polling, and OnRumble signals it when a rumble arrives. libevshim.so rebuilt
against real SDL2 2.30.9.
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.

2 participants