fix(switch): cut launcher pad-cursor lag on NX

Skip per-frame mouse warps and FlexLove perf sampling on Switch, feed pad coords through a getPosition bridge, and park that shim before the save editor so desktop paths stay unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Andrew Quenehen
2026-08-03 14:00:06 -03:00
parent 30cd9afc92
commit 0886573d34
4 changed files with 289 additions and 8 deletions
+5
View File
@@ -119,6 +119,11 @@ local function openEditor(version, slotId)
require("src.import.CacheFs").mountVersion(version)
editorVersion = version
editorHost = Importer
-- NX: drop the launcher getPosition shim so the save editor sees the real
-- pointer / its own pad cursor. Desktop has no shim — no-op.
if Importer and Importer.parkNxPointerForHost then
Importer:parkNxPointerForHost()
end
Importer = nil
editorMode = true
resizeForEditor()
+26 -1
View File
@@ -97,6 +97,21 @@ local function clamp(v, lo, hi) return math.max(lo, math.min(hi, v)) end
-- ------- lifecycle
-- NX-only: FlexLove's init maps `performanceMonitoring = false` to true
-- (`false or true`), which leaves layout/render timers + memory sampling on
-- every immediate-mode frame and makes the pad cursor feel lagged. Force
-- them off after init. Desktop keeps the library default. Exported so the
-- engine tier can assert the Switch guards without drawing the full tree.
function LauncherView.applyNxPerfGuards(imp)
if not (imp and imp.isNX and FlexLove.isReady() and FlexLove._Performance) then
return false
end
FlexLove._Performance.enabled = false
local mp = FlexLove._Performance._memoryProfiler
if mp then mp.enabled = false end
return true
end
local function ensureFlex(imp)
if not FlexLove.isReady() then
FlexLove.init({
@@ -105,6 +120,9 @@ local function ensureFlex(imp)
keyboardNavigation = false,
})
end
-- Re-apply on every ensure: FlexLove may already be ready from a prior
-- init (hot reload / editor round-trip). No-op when not NX.
LauncherView.applyNxPerfGuards(imp)
if not imp._flex then
imp._flex = true
imp._hot = imp._hot or {}
@@ -123,7 +141,14 @@ end
-- engine draws with raw love.graphics and must not share canvases or input
-- polling with a live UI toolkit.
function LauncherView.detach(imp)
if not imp._flex then return end
-- Restore the NX mouse shim even if _flex was never set (bridge can
-- install on the first update before the first draw).
if imp and imp.parkNxPointerForHost then
pcall(imp.parkNxPointerForHost, imp)
elseif imp and imp._restoreNxPointerBridge then
pcall(imp._restoreNxPointerBridge, imp)
end
if not imp or not imp._flex then return end
imp._flex = nil
if love.keyboard and love.keyboard.setKeyRepeat then
pcall(love.keyboard.setKeyRepeat, false)
+52 -7
View File
@@ -1937,6 +1937,41 @@ function RomImporter:_activatePadCursor()
self._padCursorActive = true
end
-- NX: FlexLove hover/hit-test polls love.mouse.getPosition every interactive
-- element. Warping via setPosition every stick frame is expensive on love-nx
-- and makes the virtual cursor lag. Expose the pad pointer through a getPosition
-- shim instead; desktop keeps the setPosition path unchanged.
function RomImporter:_ensureNxPointerBridge()
if not self.isNX or self._nxPointerBridge then return end
if not (love and love.mouse and love.mouse.getPosition) then return end
self._nxRealGetPosition = love.mouse.getPosition
local importer = self
love.mouse.getPosition = function()
if importer._padCursorActive then
return importer._padCursor.x, importer._padCursor.y
end
return importer._nxRealGetPosition()
end
self._nxPointerBridge = true
end
function RomImporter:_restoreNxPointerBridge()
if not self._nxPointerBridge then return end
if love and love.mouse and self._nxRealGetPosition then
love.mouse.getPosition = self._nxRealGetPosition
end
self._nxPointerBridge = false
self._nxRealGetPosition = nil
end
-- NX only: drop the getPosition shim + hide the virtual cursor before a host
-- takes over input (embedded save editor). Desktop is a no-op.
function RomImporter:parkNxPointerForHost()
if not self.isNX then return end
self._padCursorActive = false
self:_restoreNxPointerBridge()
end
function RomImporter:_cycleTab(delta)
local order = { "red", "blue", "yellow", "mods", "find" }
local idx = 1
@@ -1947,9 +1982,20 @@ function RomImporter:_cycleTab(delta)
end
function RomImporter:_updatePadCursor(dt)
if self.isNX then
self:_ensureNxPointerBridge()
end
-- Real mouse motion yields the pad cursor so desktop users keep a normal
-- pointer after bumping a stick once.
local mx, my = love.mouse.getPosition()
-- 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
@@ -1978,11 +2024,10 @@ function RomImporter:_updatePadCursor(dt)
local ny = self._padCursor.y + dy * speed * dt
self._padCursor.x = math.max(ox, math.min(ox + w, nx))
self._padCursor.y = math.max(oy, math.min(oy + h, ny))
-- The FlexLove view polls the real mouse for hover and wheel targeting,
-- so the pad pointer warps it along. The self-caused motion is recorded
-- as the last seen position, or the yield check above would read the warp
-- as real mouse movement and drop the pad cursor immediately.
if love.mouse.setPosition then
-- 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).
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
end
@@ -0,0 +1,206 @@
-- NX launcher pad-cursor lag guards (Switch-only).
-- Proves the virtual mouse no longer warps love.mouse via setPosition on NX,
-- that FlexLove still sees pad coords through the getPosition bridge, that
-- desktop keeps setPosition, and that LauncherView wires NX perf guards.
-- Also prints a small metric block (setPosition counts + update cost).
--
-- FlexLove itself is not loaded here: the engine tier runs under plain luajit
-- without luautf8, which FlexLove requires. RomImporter owns the pointer
-- bridge; LauncherView wiring is asserted via source seams.
-- luajit tests/engine/launcher_nx_pad_cursor_test.lua
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local check, eq = T.check, T.eq
love = love or require("tests.love_stub")
-- Instrument setPosition so we can count warps (love_stub has none).
local setPositionCalls = 0
local mouseX, mouseY = 0, 0
love.mouse.getPosition = function() return mouseX, mouseY end
love.mouse.setPosition = function(x, y)
setPositionCalls = setPositionCalls + 1
mouseX, mouseY = x, y
end
local RomImporter = require("src.import.RomImporter")
local function freshImporter(isNX)
return setmetatable({
isNX = isNX and true or false,
_padCursor = { x = 100, y = 100 },
_padCursorActive = false,
_padAxis = { leftx = 0, lefty = 0, righty = 0 },
_padDir = {},
_rawHatDirs = {},
_padInited = true,
_flex = true,
tab = "red",
}, RomImporter)
end
local function stickRight(imp, frames, dt)
dt = dt or (1 / 60)
imp._padAxis.leftx = 1
for _ = 1, frames do
imp:_updatePadCursor(dt)
end
end
local function read(path)
local f = assert(io.open(path, "r"))
local src = f:read("*a")
f:close()
return src
end
-- ------- NX: no setPosition warps; getPosition bridge tracks the pad
do
setPositionCalls = 0
mouseX, mouseY = 0, 0
local imp = freshImporter(true)
local x0 = imp._padCursor.x
stickRight(imp, 30)
check(imp._padCursorActive, "NX stick activates pad cursor")
check(imp._padCursor.x > x0, "NX stick moves pad cursor right")
eq(setPositionCalls, 0, "NX pad move never calls love.mouse.setPosition")
check(imp._nxPointerBridge, "NX installs getPosition bridge")
local gx, gy = love.mouse.getPosition()
eq(gx, imp._padCursor.x, "NX getPosition X matches pad cursor")
eq(gy, imp._padCursor.y, "NX getPosition Y matches pad cursor")
-- Real (stored) mouse must stay where the stub left it — bridge only.
eq(mouseX, 0, "NX does not warp the underlying mouse X")
eq(mouseY, 0, "NX does not warp the underlying mouse Y")
imp:_restoreNxPointerBridge()
check(not imp._nxPointerBridge, "restore clears NX mouse bridge")
local rx, ry = love.mouse.getPosition()
eq(rx, 0, "after restore getPosition is the real stub again")
eq(ry, 0, "after restore getPosition Y is the real stub again")
end
-- ------- NX: A after idle must not false-yield the pad cursor
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.
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)")
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.
mouseX, mouseY = 80, 90
imp:_updatePadCursor(1 / 60)
check(not imp._padCursorActive, "NX real mouse motion still yields pad cursor")
imp:parkNxPointerForHost()
end
-- ------- NX: parkNxPointerForHost restores mouse for embedded editor
do
mouseX, mouseY = 5, 6
local imp = freshImporter(true)
stickRight(imp, 5)
check(imp._nxPointerBridge, "bridge on before park")
check(imp._padCursorActive, "pad active before park")
imp:parkNxPointerForHost()
check(not imp._nxPointerBridge, "park clears bridge")
check(not imp._padCursorActive, "park clears pad active")
local gx, gy = love.mouse.getPosition()
eq(gx, 5, "after park getPosition is real mouse X")
eq(gy, 6, "after park getPosition is real mouse Y")
-- Desktop no-op.
local desk = freshImporter(false)
desk._padCursorActive = true
desk:parkNxPointerForHost()
check(desk._padCursorActive, "desktop parkNxPointerForHost is a no-op")
end
-- ------- Desktop: setPosition still warps (unchanged path)
do
setPositionCalls = 0
mouseX, mouseY = 0, 0
local imp = freshImporter(false)
local x0 = imp._padCursor.x
stickRight(imp, 30)
check(imp._padCursorActive, "desktop stick activates pad cursor")
check(imp._padCursor.x > x0, "desktop stick moves pad cursor right")
eq(setPositionCalls, 30, "desktop pad move warps mouse every frame")
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")
end
-- ------- Metrics: setPosition counts + pad-update cost (NX vs desktop)
do
local frames = 120
local dt = 1 / 60
setPositionCalls = 0
local nx = freshImporter(true)
local t0 = os.clock()
stickRight(nx, frames, dt)
local nxMs = (os.clock() - t0) * 1000
local nxSet = setPositionCalls
nx:_restoreNxPointerBridge()
setPositionCalls = 0
local desk = freshImporter(false)
t0 = os.clock()
stickRight(desk, frames, dt)
local deskMs = (os.clock() - t0) * 1000
local deskSet = setPositionCalls
eq(nxSet, 0, "metric: NX setPosition count is 0 over 120 frames")
eq(deskSet, frames, "metric: desktop setPosition count equals frame count")
print(string.format(
"METRICS nx_pad_cursor: frames=%d nx_setPosition=%d desk_setPosition=%d nx_update_ms=%.3f desk_update_ms=%.3f",
frames, nxSet, deskSet, nxMs, deskMs))
end
-- ------- Source seams: LauncherView NX perf + detach restore
do
local view = read("src/import/LauncherView.lua")
check(view:find("function LauncherView.applyNxPerfGuards", 1, true) ~= nil,
"LauncherView exports applyNxPerfGuards")
check(view:find("LauncherView.applyNxPerfGuards(imp)", 1, true) ~= nil,
"ensureFlex calls applyNxPerfGuards")
check(view:find("FlexLove._Performance.enabled = false", 1, true) ~= nil,
"NX guard disables Performance.enabled")
check(view:find("mp.enabled = false", 1, true) ~= nil,
"NX guard disables memory profiling")
check(view:find("if not (imp and imp.isNX", 1, true) ~= nil,
"perf guard is gated on imp.isNX")
check(view:find("parkNxPointerForHost", 1, true) ~= nil,
"detach parks NX pointer before tearing down")
local impSrc = read("src/import/RomImporter.lua")
check(impSrc:find("function RomImporter:_ensureNxPointerBridge", 1, true) ~= nil,
"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 and love.mouse.setPosition", 1, true) ~= nil,
"RomImporter skips setPosition on NX")
local mainSrc = read("main.lua")
check(mainSrc:find("parkNxPointerForHost", 1, true) ~= nil,
"openEditor parks NX pointer before save editor")
end
T.finish("launcher_nx_pad_cursor")