mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
feat(switch): switch handheld 720p / docked 1080p at runtime
Unlock love-nx SDL dock/undock resizing and sync via NxDisplay so booting docked is not stuck on the conf 720p hint. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -60,11 +60,13 @@ function love.conf(t)
|
||||
local mobile = osName == "Android" or osName == "iOS"
|
||||
local nx = osName == "NX"
|
||||
if nx then
|
||||
-- Switch (love-nx): docked/handheld 720p surface; no desktop resize hints.
|
||||
-- Switch (love-nx): hint handheld 720p. SDL auto-switches portable↔dock
|
||||
-- (720p↔1080p) only when the window is resizable and not exclusive
|
||||
-- fullscreen; NxDisplay.sync also applies the size on boot and dock change.
|
||||
t.window.width = 1280
|
||||
t.window.height = 720
|
||||
t.window.fullscreen = true
|
||||
t.window.resizable = false
|
||||
t.window.fullscreen = false
|
||||
t.window.resizable = true
|
||||
t.window.highdpi = false
|
||||
elseif mobile then
|
||||
-- resizable is what unlocks orientation. SDL's Android backend, given no
|
||||
|
||||
@@ -49,10 +49,11 @@ the transfer runbook).
|
||||
- Hardware evidence for Phase 0 probe, ROM import, naming A/B, save/suspend, fused NRO — see `docs/switch-hardware-evidence.md`
|
||||
- Path-gated CI selftest + canonical fused PR artifact; release Switch hard-fail
|
||||
- Save editor pad/touch input (virtual cursor, A click, B close) — see `tools/save-editor/README.md`
|
||||
- Dynamic display size on NX only: handheld **1280×720**, docked/TV **1920×1080** (`src/core/NxDisplay.lua` + resizable conf so love-nx SDL can follow dock/undock at runtime)
|
||||
|
||||
### Known gaps / welcome contributions
|
||||
|
||||
- Docked vs handheld soak, long-play soak (≥30 min)
|
||||
- Docked vs handheld soak (≥30 min) and Lite coverage — resolution switch is implemented; long soak still welcome
|
||||
- Switch Lite and fuller Pro Controller / third-party pad matrices
|
||||
- Applet Mode remains unsupported by design (title override required)
|
||||
- `nxlink` / netloader contrib fast-loop (deferred — see [switch-transfer.md](switch-transfer.md))
|
||||
@@ -476,7 +477,7 @@ Operator evidence lives in `docs/switch-hardware-evidence.md`. **Do not invent p
|
||||
| P0-12 | Fused NRO boots without adjacent `game.love` | **pass** | T24 — `docs/switch-hardware-evidence.md` |
|
||||
| P0-14 | Fused NRO MTP round-trip SHA-256 | **pass** | T24 — first artifact `b019e2e8…` @ `6fb5602` (redeploy after Blue fix) |
|
||||
| P0-15 | Replace NRO only; saves persist | **pass** | T24 — operator NRO-only update keeps saves |
|
||||
| P1-01 | Docked vs handheld spot-check | **deferred** | Not exercised on OLED dock yet |
|
||||
| P1-01 | Docked vs handheld spot-check | **deferred** | Code: `NxDisplay` 720p↔1080p; OLED dock soak not recorded yet |
|
||||
| P1-02 | Applet Mode documented unsupported | **pass** | Title override required; Album path not validated |
|
||||
| P1-03 | Long-play soak (≥30 min) | **deferred** | No soak session recorded |
|
||||
| P1-04 | Reboot persistence | **pass** | T19 |
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
local editorMode = os.getenv("POKEPORT_EDITOR") == "1" or POKEPORT_EDITOR_MODE == true
|
||||
|
||||
local SwitchDiagnostics = require("src.debug.SwitchDiagnostics")
|
||||
local NxDisplay = require("src.core.NxDisplay")
|
||||
|
||||
-- Lua errors: persist a redacted trace in the save dir and surface a hint.
|
||||
do
|
||||
@@ -233,6 +234,9 @@ function love.load(args)
|
||||
end
|
||||
end
|
||||
love.graphics.setDefaultFilter("nearest", "nearest")
|
||||
-- NX: handheld 720p / docked 1080p. Runs for every boot path (launcher,
|
||||
-- editor, scripted); no-op on desktop/mobile.
|
||||
NxDisplay.sync()
|
||||
|
||||
-- Standalone editor. A bare `--editor` run has no launcher behind it, so
|
||||
-- Close quits; --save points it at a specific file, otherwise it opens the
|
||||
@@ -299,6 +303,8 @@ end
|
||||
|
||||
function love.update(dt)
|
||||
SwitchDiagnostics.maybeFlush(false)
|
||||
-- NX only (no-op elsewhere): follow dock/undock without waiting for SDL.
|
||||
NxDisplay.sync()
|
||||
if editorMode then return EditorApp.update(dt) end
|
||||
if TouchEditor then return TouchEditor.update(dt) end
|
||||
if Importer then return Importer:update(dt) end
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
-- Switch-only display size: handheld 1280x720, docked (TV) 1920x1080.
|
||||
-- love-nx's SDL backend can auto-resize on dock/undock when the window is
|
||||
-- resizable; this module also syncs on boot and every frame so a docked
|
||||
-- launch is not stuck at the conf.lua 720p hint until the next mode change.
|
||||
|
||||
local Platform = require("src.core.Platform")
|
||||
|
||||
local NxDisplay = {}
|
||||
|
||||
NxDisplay.HANDHELD_W, NxDisplay.HANDHELD_H = 1280, 720
|
||||
NxDisplay.DOCKED_W, NxDisplay.DOCKED_H = 1920, 1080
|
||||
|
||||
-- AppletOperationMode from libnx: Handheld = 0, Console (docked) = 1.
|
||||
local MODE_HANDHELD = 0
|
||||
local MODE_CONSOLE = 1
|
||||
|
||||
-- Test hooks (nil = use live Platform / FFI / love.window).
|
||||
NxDisplay._forceNXForTests = nil
|
||||
NxDisplay._operationModeForTests = nil
|
||||
|
||||
local ffiOk, ffiC
|
||||
|
||||
local function ensureFfi()
|
||||
if ffiOk ~= nil then return ffiOk end
|
||||
ffiOk = false
|
||||
local ok, ffi = pcall(require, "ffi")
|
||||
if not ok or not ffi then return false end
|
||||
-- Redefinition is fine across hot reload / tests; we only need the symbol.
|
||||
pcall(ffi.cdef, [[
|
||||
unsigned char appletGetOperationMode(void);
|
||||
]])
|
||||
local probeOk = pcall(function()
|
||||
return ffi.C.appletGetOperationMode
|
||||
end)
|
||||
if not probeOk then return false end
|
||||
ffiC = ffi.C
|
||||
ffiOk = true
|
||||
return true
|
||||
end
|
||||
|
||||
local function isNX()
|
||||
if NxDisplay._forceNXForTests ~= nil then
|
||||
return not not NxDisplay._forceNXForTests
|
||||
end
|
||||
return Platform.isNX()
|
||||
end
|
||||
|
||||
-- Returns AppletOperationMode or nil when unavailable.
|
||||
function NxDisplay.operationMode()
|
||||
if NxDisplay._operationModeForTests ~= nil then
|
||||
return NxDisplay._operationModeForTests
|
||||
end
|
||||
if not ensureFfi() or not ffiC then return nil end
|
||||
local ok, mode = pcall(function()
|
||||
return tonumber(ffiC.appletGetOperationMode())
|
||||
end)
|
||||
if not ok then return nil end
|
||||
return mode
|
||||
end
|
||||
|
||||
-- Map operation mode → framebuffer size. Unknown / nil → handheld 720p.
|
||||
function NxDisplay.desiredSize(mode)
|
||||
if mode == nil then mode = NxDisplay.operationMode() end
|
||||
if mode == MODE_CONSOLE then
|
||||
return NxDisplay.DOCKED_W, NxDisplay.DOCKED_H
|
||||
end
|
||||
return NxDisplay.HANDHELD_W, NxDisplay.HANDHELD_H
|
||||
end
|
||||
|
||||
-- Apply handheld/dock size when on NX and the window differs. No-op elsewhere.
|
||||
-- Returns true when setMode ran.
|
||||
function NxDisplay.sync()
|
||||
if not isNX() then return false end
|
||||
if not (love and love.window and love.window.getMode and love.window.setMode) then
|
||||
return false
|
||||
end
|
||||
local wantW, wantH = NxDisplay.desiredSize()
|
||||
local curW, curH, flags = love.window.getMode()
|
||||
flags = flags or {}
|
||||
if curW == wantW and curH == wantH
|
||||
and flags.fullscreen == false and flags.resizable == true then
|
||||
return false
|
||||
end
|
||||
flags.fullscreen = false
|
||||
flags.resizable = true
|
||||
love.window.setMode(wantW, wantH, flags)
|
||||
return true
|
||||
end
|
||||
|
||||
function NxDisplay._resetForTests()
|
||||
NxDisplay._forceNXForTests = nil
|
||||
NxDisplay._operationModeForTests = nil
|
||||
ffiOk, ffiC = nil, nil
|
||||
end
|
||||
|
||||
return NxDisplay
|
||||
@@ -0,0 +1,123 @@
|
||||
-- NX handheld/dock display sync (portable 720p / docked 1080p).
|
||||
-- Self-contained: luajit tests/engine/nx_display_test.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check = T.check
|
||||
local eq = T.eq
|
||||
|
||||
local savedLove = _G.love
|
||||
|
||||
package.loaded["src.core.Platform"] = nil
|
||||
package.loaded["src.core.NxDisplay"] = nil
|
||||
|
||||
local NxDisplay = require("src.core.NxDisplay")
|
||||
|
||||
local function withWindow(state, fn)
|
||||
local setCalls = {}
|
||||
_G.love = {
|
||||
system = {
|
||||
getOS = function() return state.os or "NX" end,
|
||||
},
|
||||
window = {
|
||||
getMode = function()
|
||||
return state.w, state.h, {
|
||||
fullscreen = state.fullscreen,
|
||||
resizable = state.resizable,
|
||||
vsync = 1,
|
||||
}
|
||||
end,
|
||||
setMode = function(w, h, flags)
|
||||
setCalls[#setCalls + 1] = { w = w, h = h, flags = flags }
|
||||
state.w, state.h = w, h
|
||||
state.fullscreen = flags and flags.fullscreen
|
||||
state.resizable = flags and flags.resizable
|
||||
end,
|
||||
},
|
||||
}
|
||||
package.loaded["src.core.Platform"] = nil
|
||||
require("src.core.Platform")._resetForTests()
|
||||
NxDisplay._resetForTests()
|
||||
package.loaded["src.core.NxDisplay"] = nil
|
||||
NxDisplay = require("src.core.NxDisplay")
|
||||
local ok, err = pcall(fn, setCalls)
|
||||
_G.love = savedLove
|
||||
package.loaded["src.core.Platform"] = nil
|
||||
package.loaded["src.core.NxDisplay"] = nil
|
||||
NxDisplay = require("src.core.NxDisplay")
|
||||
NxDisplay._resetForTests()
|
||||
if not ok then error(err) end
|
||||
end
|
||||
|
||||
-- Size mapping
|
||||
do
|
||||
local w, h = NxDisplay.desiredSize(0)
|
||||
eq(w, 1280, "handheld mode → 1280 wide")
|
||||
eq(h, 720, "handheld mode → 720 tall")
|
||||
w, h = NxDisplay.desiredSize(1)
|
||||
eq(w, 1920, "console/docked mode → 1920 wide")
|
||||
eq(h, 1080, "console/docked mode → 1080 tall")
|
||||
w, h = NxDisplay.desiredSize(nil)
|
||||
eq(w, 1280, "nil mode falls back to handheld width")
|
||||
eq(h, 720, "nil mode falls back to handheld height")
|
||||
w, h = NxDisplay.desiredSize(99)
|
||||
eq(w, 1280, "unknown mode falls back to handheld width")
|
||||
end
|
||||
|
||||
-- Non-NX: sync is a no-op
|
||||
withWindow({ os = "Linux", w = 1024, h = 768, fullscreen = false, resizable = true }, function(setCalls)
|
||||
NxDisplay._forceNXForTests = false
|
||||
NxDisplay._operationModeForTests = 1
|
||||
eq(NxDisplay.sync(), false, "sync returns false off NX")
|
||||
eq(#setCalls, 0, "sync never calls setMode off NX")
|
||||
end)
|
||||
|
||||
-- NX handheld already correct: no setMode
|
||||
withWindow({
|
||||
os = "NX", w = 1280, h = 720, fullscreen = false, resizable = true,
|
||||
}, function(setCalls)
|
||||
NxDisplay._forceNXForTests = true
|
||||
NxDisplay._operationModeForTests = 0
|
||||
eq(NxDisplay.sync(), false, "sync skips setMode when already handheld")
|
||||
eq(#setCalls, 0, "no setMode calls when size+flags match handheld")
|
||||
end)
|
||||
|
||||
-- NX docked boot from 720p hint → 1080p
|
||||
withWindow({
|
||||
os = "NX", w = 1280, h = 720, fullscreen = true, resizable = false,
|
||||
}, function(setCalls)
|
||||
NxDisplay._forceNXForTests = true
|
||||
NxDisplay._operationModeForTests = 1
|
||||
eq(NxDisplay.sync(), true, "sync upgrades docked boot to 1080p")
|
||||
eq(#setCalls, 1, "one setMode on docked boot")
|
||||
eq(setCalls[1].w, 1920, "docked setMode width")
|
||||
eq(setCalls[1].h, 1080, "docked setMode height")
|
||||
eq(setCalls[1].flags.fullscreen, false, "docked setMode clears exclusive fullscreen")
|
||||
eq(setCalls[1].flags.resizable, true, "docked setMode enables resizable for SDL backup")
|
||||
end)
|
||||
|
||||
-- NX undock: 1080p → 720p
|
||||
withWindow({
|
||||
os = "NX", w = 1920, h = 1080, fullscreen = false, resizable = true,
|
||||
}, function(setCalls)
|
||||
NxDisplay._forceNXForTests = true
|
||||
NxDisplay._operationModeForTests = 0
|
||||
eq(NxDisplay.sync(), true, "sync shrinks to handheld after undock")
|
||||
eq(setCalls[1].w, 1280, "undock setMode width")
|
||||
eq(setCalls[1].h, 720, "undock setMode height")
|
||||
end)
|
||||
|
||||
-- Flags-only fix when size already matches
|
||||
withWindow({
|
||||
os = "NX", w = 1280, h = 720, fullscreen = true, resizable = false,
|
||||
}, function(setCalls)
|
||||
NxDisplay._forceNXForTests = true
|
||||
NxDisplay._operationModeForTests = 0
|
||||
eq(NxDisplay.sync(), true, "sync fixes fullscreen/resizable even if size matches")
|
||||
eq(setCalls[1].w, 1280, "flags-only setMode keeps handheld width")
|
||||
eq(setCalls[1].flags.fullscreen, false, "flags-only clears fullscreen")
|
||||
eq(setCalls[1].flags.resizable, true, "flags-only sets resizable")
|
||||
end)
|
||||
|
||||
T.finish("nx_display")
|
||||
Reference in New Issue
Block a user