Merge pull request #574 from andrewqsantos/feat/switch-nx

Nintendo Switch / love-nx support (#531)
This commit is contained in:
bryanthaboi
2026-08-03 17:08:54 -04:00
committed by GitHub
87 changed files with 8915 additions and 276 deletions
+53 -17
View File
@@ -143,6 +143,30 @@ LauncherView._refreshAutoHeight = refreshAutoHeight
-- ------- 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
-- 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
local function ensureFlex(imp)
if not FlexLove.isReady() then
FlexLove.init({
@@ -151,6 +175,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 {}
@@ -169,7 +196,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)
@@ -674,8 +708,10 @@ end
-- ------- game panel
local function buildRomCard(imp, parent, m, version, info, ready, locked)
local dropHint = imp.android and Strings("Copy the .gb/.gbc via USB.")
or Strings("Or drop the .gb/.gbc file here.")
local dropHint = imp.isNX and Strings("Copy the .gb/.gbc via MTP into imports/.")
or (imp.android and Strings("Copy the .gb/.gbc via USB.")
or Strings("Or drop the .gb/.gbc file here."))
local importLabel = imp.isNX and Strings("Scan again") or Strings("Import ROM")
local romState, romDetail, romBtnLabel, romBtnEnabled, romProgress
if locked then
romState, romDetail = Strings("Not supported yet"),
@@ -696,12 +732,12 @@ local function buildRomCard(imp, parent, m, version, info, ready, locked)
elseif erroring then
romState = Strings("Import failed")
romDetail = imp.detail or Strings("That ROM could not be imported.")
romBtnLabel, romBtnEnabled = Strings("Import ROM"), true
romBtnLabel, romBtnEnabled = importLabel, true
elseif notice then
romState = Strings("No ROM imported")
romDetail = ((notice.status or "") .. " " .. (notice.detail or ""))
:gsub("^%s+", ""):gsub("%s+$", "")
romBtnLabel, romBtnEnabled = Strings("Import ROM"), true
romBtnLabel, romBtnEnabled = importLabel, true
elseif imp.returning[version] then
romState = Strings("Update required")
romDetail = Strings("This build needs a few more things from your ")
@@ -711,7 +747,7 @@ local function buildRomCard(imp, parent, m, version, info, ready, locked)
romState = Strings("No ROM imported")
romDetail = Strings("The ROM is verified before any files are created. ")
.. dropHint
romBtnLabel, romBtnEnabled = Strings("Import ROM"), true
romBtnLabel, romBtnEnabled = importLabel, true
end
end
@@ -750,13 +786,11 @@ local function buildSaveFilesCard(imp, parent, m, version, ready, locked)
hintText, hintCol = sfNotice.text, (sfNotice.ok and "green" or "danger")
elseif locked then
hintText, hintCol = Strings("Not available yet."), "warn"
elseif imp.android then
hintText = Strings("Import or export a .sav with the system file picker.")
hintCol = "warn"
else
hintText = Strings("Import a .sav to a new slot, or export the active slot.")
hintText = imp:_savesDefaultHint(version)
hintCol = "warn"
end
local savImportLabel = imp.isNX and Strings("Scan again") or Strings("Import save")
local c = card(parent, { padding = m.cardPad, gap = 8 * m.s })
label(c, "SAVE FILES", 12 * m.s + 1, C("gray"))
@@ -765,7 +799,7 @@ local function buildSaveFilesCard(imp, parent, m, version, ready, locked)
-- explicit halves rather than flex growth, which mis-distributed inside
-- an auto-height card
local halfW = math.floor((m.colW - 32 - 10 * m.s) / 2)
button(imp, row, "sav-import-" .. version, Strings("Import save"), {
button(imp, row, "sav-import-" .. version, savImportLabel, {
w = halfW, h = m.btnH, size = 13 * m.s + 1,
kind = sfImportEnabled and "neutral" or "disabled",
action = sfImportEnabled and function()
@@ -1001,7 +1035,7 @@ local function buildModsPanel(imp, parent, m)
action = function() imp:_setAllMods(false) end,
})
end
button(imp, head, "mods-import", Strings("Import mod .zip"), {
button(imp, head, "mods-import", imp:_modsImportButtonLabel(), {
h = m.btnH, size = 13 * m.s + 1, kind = "neutral",
action = function() imp:chooseMod() end,
})
@@ -1010,8 +1044,7 @@ local function buildModsPanel(imp, parent, m)
label(parent, imp.modNotice.text, 12 * m.s + 2,
C(imp.modNotice.ok and "green" or "danger"))
else
label(parent, imp.android and Strings("Or copy a mod .zip via USB.")
or Strings("Or drop a mod .zip onto the window."), 12 * m.s + 2, C("warn"))
label(parent, imp:_modsDefaultHint(), 12 * m.s + 2, C("warn"))
end
if #mods == 0 then
@@ -1022,9 +1055,7 @@ local function buildModsPanel(imp, parent, m)
positioning = "flex", justifyContent = "center", alignItems = "center",
padding = { horizontal = 16 },
})
label(box, imp.android
and Strings("No mods installed - tap Import mod .zip to add one.")
or Strings("No mods installed - drop a mod .zip here to add one."),
label(box, imp:_modsEmptyHint(),
math.floor(12 * m.s + 2.5), C("detail"), { textAlign = "center" })
return
end
@@ -1814,7 +1845,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)