From e021aae60d086069c7c5911fa490c9301e2c8a72 Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Sat, 11 Jul 2026 04:22:13 +0000 Subject: [PATCH 1/3] Fix slow, choppy mouse at 60Hz refresh rate Captured pointer moves read only the last sample of each batched MotionEvent, dropping the historical samples that carry the rest of the motion at lower refresh rates. Sum history plus current sample for the relative-axis deltas and the getX/getY fallback. --- .../display/XServerDisplayActivity.java | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/app/src/main/runtime/display/XServerDisplayActivity.java b/app/src/main/runtime/display/XServerDisplayActivity.java index cc095b365..8a7406fc1 100644 --- a/app/src/main/runtime/display/XServerDisplayActivity.java +++ b/app/src/main/runtime/display/XServerDisplayActivity.java @@ -2334,11 +2334,24 @@ private void handleCapturedPointer(MotionEvent event) { } private int[] getCapturedPointerDelta(MotionEvent event) { - float dx = event.getAxisValue(MotionEvent.AXIS_RELATIVE_X); - float dy = event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y); + // Motion events are batched per vsync; at low refresh rates each event + // carries multiple samples and skipping the history drops movement. + final int historySize = event.getHistorySize(); + float dx = 0.0f; + float dy = 0.0f; + for (int i = 0; i < historySize; i++) { + dx += event.getHistoricalAxisValue(MotionEvent.AXIS_RELATIVE_X, i); + dy += event.getHistoricalAxisValue(MotionEvent.AXIS_RELATIVE_Y, i); + } + dx += event.getAxisValue(MotionEvent.AXIS_RELATIVE_X); + dy += event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y); if (dx == 0.0f && dy == 0.0f) { - dx = event.getX(); - dy = event.getY(); + for (int i = 0; i < historySize; i++) { + dx += event.getHistoricalX(i); + dy += event.getHistoricalY(i); + } + dx += event.getX(); + dy += event.getY(); } dx *= globalCursorSpeed; dy *= globalCursorSpeed; From 0d4d2d472c06dcc5dcb9a0cdaaec85ac32ab943f Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Sat, 11 Jul 2026 06:21:39 +0000 Subject: [PATCH 2/3] Fix mouse judder when app refresh rate is set to 60Hz The refresh-rate override injected a wall-clock frame limiter into the guest with vsync disabled, which drifts against the panel vsync grid and causes judder felt most in the mouse. Cap frames through the vsync-aligned pacer instead and drop the guest-side limiter; an explicit FPS-limit slider value still takes priority. --- .../display/XServerDisplayActivity.java | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/app/src/main/runtime/display/XServerDisplayActivity.java b/app/src/main/runtime/display/XServerDisplayActivity.java index 8a7406fc1..6e46c4c87 100644 --- a/app/src/main/runtime/display/XServerDisplayActivity.java +++ b/app/src/main/runtime/display/XServerDisplayActivity.java @@ -624,17 +624,9 @@ private int getGlobalRefreshRateOverride() { return Math.max(0, preferences.getInt("refresh_rate_override", 0)); } - private int getDxvkFrameRateOverride() { - int perGameRate = getPerGameRefreshRateOverride(); - if (perGameRate > 0) { - return perGameRate; - } - - int globalRate = getGlobalRefreshRateOverride(); - if (globalRate > 0) { - return globalRate; - } - return 0; + // Refresh-rate override caps frames via the vsync-aligned pacer; an explicit FPS-limit slider value wins. + private int getEffectiveFpsLimit() { + return runtimeFpsLimit > 0 ? runtimeFpsLimit : getRefreshRateOverride(); } private int parsePositiveInt(String value) { @@ -680,10 +672,11 @@ private void applyPreferredRefreshRate() { Runnable applyRefresh = () -> { if (isFinishing() || isDestroyed()) return; - float hz = RefreshRateUtils.applyPreferredRefreshRate(this, getRefreshRateOverride(), runtimeFpsLimit); + int effectiveFpsLimit = getEffectiveFpsLimit(); + float hz = RefreshRateUtils.applyPreferredRefreshRate(this, getRefreshRateOverride(), effectiveFpsLimit); if (xServer != null) { xServer.getFramePaceClock().setDisplayRefreshHz(hz); - xServer.getFramePaceClock().setCapActive(runtimeFpsLimit > 0); + xServer.getFramePaceClock().setCapActive(effectiveFpsLimit > 0); } }; @@ -4287,7 +4280,7 @@ public void onGyroscopeCardExpandedChanged(boolean expanded) { public void onFPSLimitChanged(int limit) { runtimeFpsLimit = Math.max(0, limit); if (xServerView != null) { - xServerView.getRenderer().setFpsLimit(runtimeFpsLimit); + xServerView.getRenderer().setFpsLimit(getEffectiveFpsLimit()); } applyPreferredRefreshRate(); if (shortcut != null) { @@ -6976,11 +6969,11 @@ private void setupUI() { String savedFpsLimit = shortcut.getExtra("fpsLimit", "0"); try { runtimeFpsLimit = Integer.parseInt(savedFpsLimit); - renderer.setFpsLimit(runtimeFpsLimit); } catch (NumberFormatException e) { runtimeFpsLimit = 0; } } + renderer.setFpsLimit(getEffectiveFpsLimit()); applyScreenEffects(); xServer.setRenderer(renderer); @@ -7573,8 +7566,7 @@ private void extractGraphicsDriverFiles() { File rootDir = imageFs.getRootDir(); if (dxwrapper.contains("dxvk")) { - int refreshRateOverride = getDxvkFrameRateOverride(); - DXVKConfigUtils.setEnvVars(this, dxwrapperConfig, envVars, refreshRateOverride); + DXVKConfigUtils.setEnvVars(this, dxwrapperConfig, envVars); String version = dxwrapperConfig.get("version"); if (version.equals("1.11.1-sarek")) { Log.d("GraphicsDriverExtraction", "Disabling Wrapper PATCH_OPCONSTCOMP SPIR-V pass"); From 12de545c180be5f5ca8637108c696bd13608868c Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Sat, 11 Jul 2026 14:19:59 +0000 Subject: [PATCH 3/3] Fix half-rate cursor rendering at 60Hz refresh Coalesced render requests took a main-handler hop before arming the frame callback, so vsync-phased requests like captured mouse moves only rendered every other frame - a 30fps cursor on a 60Hz panel. Post the callback directly on the cached main Choreographer instead. Also clear preferredRefreshRate when a display mode id is set so the refresh-rate vote applies consistently across devices. --- .../display/XServerDisplayActivity.java | 5 ++--- .../display/renderer/VulkanRenderer.java | 22 ++++++++++++++----- .../main/shared/android/RefreshRateUtils.java | 3 ++- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/app/src/main/runtime/display/XServerDisplayActivity.java b/app/src/main/runtime/display/XServerDisplayActivity.java index 6e46c4c87..987d2fbff 100644 --- a/app/src/main/runtime/display/XServerDisplayActivity.java +++ b/app/src/main/runtime/display/XServerDisplayActivity.java @@ -624,7 +624,7 @@ private int getGlobalRefreshRateOverride() { return Math.max(0, preferences.getInt("refresh_rate_override", 0)); } - // Refresh-rate override caps frames via the vsync-aligned pacer; an explicit FPS-limit slider value wins. + // FPS-limit slider takes priority over the refresh-rate override. private int getEffectiveFpsLimit() { return runtimeFpsLimit > 0 ? runtimeFpsLimit : getRefreshRateOverride(); } @@ -2327,8 +2327,7 @@ private void handleCapturedPointer(MotionEvent event) { } private int[] getCapturedPointerDelta(MotionEvent event) { - // Motion events are batched per vsync; at low refresh rates each event - // carries multiple samples and skipping the history drops movement. + // Sum batched samples; skipping history drops movement at low refresh rates. final int historySize = event.getHistorySize(); float dx = 0.0f; float dy = 0.0f; diff --git a/app/src/main/runtime/display/renderer/VulkanRenderer.java b/app/src/main/runtime/display/renderer/VulkanRenderer.java index 058986a45..a804fe339 100644 --- a/app/src/main/runtime/display/renderer/VulkanRenderer.java +++ b/app/src/main/runtime/display/renderer/VulkanRenderer.java @@ -130,6 +130,10 @@ public VulkanRenderer(XServerSurfaceView view, XServer xServer) { this.xServer = xServer; this.effectComposer = new EffectComposer(this); this.rootCursorDrawable = createRootCursorDrawable(); + this.coalescedRenderCallback = frameTimeNanos -> { + renderRequested.set(false); + xServerView.requestRender(); + }; } public void destroy() { @@ -163,13 +167,21 @@ public void destroy() { } } + private volatile Choreographer mainChoreographer; + private final Choreographer.FrameCallback coalescedRenderCallback; + public void requestRenderCoalesced() { if (renderRequested.compareAndSet(false, true)) { - mainHandler.post(() -> - Choreographer.getInstance().postFrameCallback(frameTimeNanos -> { - renderRequested.set(false); - xServerView.requestRender(); - })); + // Post directly (thread-safe): a handler hop arms past the next doFrame and halves the visible cursor rate. + Choreographer choreographer = mainChoreographer; + if (choreographer != null) { + choreographer.postFrameCallback(coalescedRenderCallback); + } else { + mainHandler.post(() -> { + mainChoreographer = Choreographer.getInstance(); + mainChoreographer.postFrameCallback(coalescedRenderCallback); + }); + } } } diff --git a/app/src/main/shared/android/RefreshRateUtils.java b/app/src/main/shared/android/RefreshRateUtils.java index 505e86756..2960a3110 100644 --- a/app/src/main/shared/android/RefreshRateUtils.java +++ b/app/src/main/shared/android/RefreshRateUtils.java @@ -302,7 +302,8 @@ public static float applyPreferredRefreshRate(Activity activity, int requestedHz int modeId = resolvePreferredDisplayModeId(activity, effectiveRequestedHz); float refreshRate = resolvePreferredRefreshRate(activity, effectiveRequestedHz); params.preferredDisplayModeId = modeId; - params.preferredRefreshRate = refreshRate; + // Must be 0 when a modeId is set. + params.preferredRefreshRate = modeId != 0 ? 0f : refreshRate; activity.getWindow().setAttributes(params); Log.d( TAG,