mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
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:
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user