fix(switch): stop pad cursor flicker from SDL mouse drift

Disable mouse-yield on NX where stick/touch moves the system pointer between sparse axis events, clamp pad dt, pixel-snap the overlay, and soften FlexLove GC so the launcher cursor stays steady.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Andrew Quenehen
2026-08-03 14:31:05 -03:00
parent 0e5ea26e36
commit cfe482e1fc
3 changed files with 78 additions and 23 deletions
+14
View File
@@ -109,6 +109,15 @@ function LauncherView.applyNxPerfGuards(imp)
FlexLove._Performance.enabled = false
local mp = FlexLove._Performance._memoryProfiler
if mp then mp.enabled = false end
-- Immediate-mode rebuilds allocate a full tree every frame; the default
-- auto GC steps hitch the pad cursor on Switch. Less frequent steps, higher
-- threshold — desktop keeps FlexLove defaults.
if FlexLove._gcConfig then
FlexLove._gcConfig.strategy = "periodic"
FlexLove._gcConfig.interval = 90
FlexLove._gcConfig.stepSize = 40
FlexLove._gcConfig.memoryThreshold = 180
end
return true
end
@@ -1734,7 +1743,12 @@ end
local function drawPadCursor(imp)
if not imp._padCursorActive then return end
-- Pixel-snap on NX: subpixel polygon edges shimmer on the 720p Switch
-- framebuffer when the stick advances by fractional pixels each frame.
local x, y = imp._padCursor.x, imp._padCursor.y
if imp.isNX then
x, y = math.floor(x + 0.5), math.floor(y + 0.5)
end
love.graphics.push("all")
love.graphics.origin()
love.graphics.setLineWidth(1)
+14 -15
View File
@@ -1984,24 +1984,24 @@ end
function RomImporter:_updatePadCursor(dt)
if self.isNX then
self:_ensureNxPointerBridge()
-- Cap dt so a hitch in the FlexLove immediate-mode frame does not fling
-- the cursor; desktop keeps raw dt (setPosition path already smooth there).
if dt > 1 / 30 then dt = 1 / 30 end
end
-- Real mouse motion yields the pad cursor so desktop users keep a normal
-- pointer after bumping a stick once. On NX the bridged getPosition returns
-- pad coords while active, so yield must sample the *real* mouse or an A
-- press after idle falsely drops the cursor (pad vs last real position).
local mx, my
if self.isNX and self._nxRealGetPosition then
mx, my = self._nxRealGetPosition()
else
mx, my = love.mouse.getPosition()
end
if self._lastMouseX and self._padCursorActive then
if math.abs(mx - self._lastMouseX) > 3 or math.abs(my - self._lastMouseY) > 3 then
self._padCursorActive = false
-- pointer after bumping a stick once. On NX this must stay off: love-nx /
-- SDL often drifts the system mouse with the stick (or touch), and axis
-- events are not every frame, so yield+reactivate flickers the overlay.
if not self.isNX then
local mx, my = love.mouse.getPosition()
if self._lastMouseX and self._padCursorActive then
if math.abs(mx - self._lastMouseX) > 3 or math.abs(my - self._lastMouseY) > 3 then
self._padCursorActive = false
end
end
self._lastMouseX, self._lastMouseY = mx, my
end
self._lastMouseX, self._lastMouseY = mx, my
local ax = self._padAxis.leftx or 0
local ay = self._padAxis.lefty or 0
@@ -2025,8 +2025,7 @@ function RomImporter:_updatePadCursor(dt)
self._padCursor.x = math.max(ox, math.min(ox + w, nx))
self._padCursor.y = math.max(oy, math.min(oy + h, ny))
-- Desktop: FlexLove polls the real mouse, so warp it with the pad pointer.
-- NX: the getPosition bridge already returns pad coords — skip setPosition
-- and leave _lastMouse* on the real pointer baseline (yield above).
-- NX: the getPosition bridge already returns pad coords — skip setPosition.
if not self.isNX and love.mouse.setPosition then
pcall(love.mouse.setPosition, self._padCursor.x, self._padCursor.y)
self._lastMouseX, self._lastMouseY = self._padCursor.x, self._padCursor.y
+50 -8
View File
@@ -86,22 +86,52 @@ do
mouseX, mouseY = 10, 20
local imp = freshImporter(true)
imp._padCursor.x, imp._padCursor.y = 400, 300
-- Idle frames pin _lastMouse* to the real pointer.
-- Idle frames (NX ignores mouse yield entirely).
imp:_updatePadCursor(1 / 60)
eq(imp._lastMouseX, 10, "idle samples real mouse X")
eq(imp._lastMouseY, 20, "idle samples real mouse Y")
-- A / activate without stick motion (clickAt path).
imp:_activatePadCursor()
imp:_updatePadCursor(1 / 60)
check(imp._padCursorActive,
"NX A after idle keeps pad cursor (no false yield via bridged getPosition)")
"NX A after idle keeps pad cursor")
local gx, gy = love.mouse.getPosition()
eq(gx, 400, "bridged getPosition still reports pad X after A")
eq(gy, 300, "bridged getPosition still reports pad Y after A")
-- Real USB-ish motion still yields.
-- System mouse drift must NOT yield on NX (SDL stick→mouse / touch noise).
mouseX, mouseY = 80, 90
imp:_updatePadCursor(1 / 60)
check(not imp._padCursorActive, "NX real mouse motion still yields pad cursor")
check(imp._padCursorActive, "NX ignores real mouse drift for yield")
imp:parkNxPointerForHost()
end
-- ------- NX: sparse stick + SDL mouse drift must not flicker the overlay
do
mouseX, mouseY = 100, 100
local imp = freshImporter(true)
imp._padCursor.x, imp._padCursor.y = 100, 100
local flickers = 0
for i = 1, 60 do
mouseX = mouseX + 8 -- simulated SDL stick→mouse drift
if i % 2 == 1 then
imp._padAxis.leftx = 1
else
imp._padAxis.leftx = 0 -- axis events not every frame
end
local before = imp._padCursorActive
imp:_updatePadCursor(1 / 60)
if before and not imp._padCursorActive then
flickers = flickers + 1
end
end
eq(flickers, 0, "NX sparse stick + mouse drift causes zero pad flickers")
check(imp._padCursorActive, "NX pad stays active after sparse stick run")
-- dt clamp: a 0.2s hitch must not move more than a 1/30 step
local xBefore = imp._padCursor.x
imp._padAxis.leftx = 1
imp:_updatePadCursor(0.2)
local moved = imp._padCursor.x - xBefore
local maxStep = 560 * (1 / 30) + 0.01
check(moved <= maxStep, "NX pad cursor dt is clamped at 1/30")
imp:parkNxPointerForHost()
end
@@ -140,6 +170,11 @@ do
eq(mouseX, imp._padCursor.x, "desktop setPosition tracks pad X")
eq(mouseY, imp._padCursor.y, "desktop setPosition tracks pad Y")
check(not imp._nxPointerBridge, "desktop never installs NX bridge")
-- Desktop yield still drops the pad when the real mouse moves (stick released).
imp._padAxis.leftx = 0
mouseX, mouseY = mouseX + 20, mouseY + 20
imp:_updatePadCursor(1 / 60)
check(not imp._padCursorActive, "desktop real mouse motion still yields pad")
end
-- ------- Metrics: setPosition counts + pad-update cost (NX vs desktop)
@@ -193,14 +228,21 @@ do
"RomImporter owns NX getPosition bridge")
check(impSrc:find("function RomImporter:parkNxPointerForHost", 1, true) ~= nil,
"RomImporter exports parkNxPointerForHost")
check(impSrc:find("_nxRealGetPosition()", 1, true) ~= nil,
"NX yield samples real mouse, not bridged getPosition")
check(impSrc:find("if not self.isNX then", 1, true) ~= nil,
"NX skips desktop mouse-yield path")
check(impSrc:find("if not self.isNX and love.mouse.setPosition", 1, true) ~= nil,
"RomImporter skips setPosition on NX")
check(impSrc:find("dt > 1 / 30", 1, true) ~= nil,
"NX clamps pad cursor dt")
local mainSrc = read("main.lua")
check(mainSrc:find("parkNxPointerForHost", 1, true) ~= nil,
"openEditor parks NX pointer before save editor")
check(view:find("math.floor(x + 0.5)", 1, true) ~= nil,
"NX pad cursor draw is pixel-snapped")
check(view:find('strategy = "periodic"', 1, true) ~= nil,
"NX softens FlexLove GC strategy")
end
T.finish("launcher_nx_pad_cursor")