mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
Add graphics performance tier for low-end devices
Introduce an OPTIONS -> PERFORMANCE setting that scales the port's optional presentation extras down for weaker hardware, so older/lower-end devices can run the game smoothly. The tier governs the three heaviest non-faithful extras -- the 3D TILT, the GBC FX post-process shader, and survey ZOOM (which renders connected neighbor maps) -- plus a hard FPS ceiling. It never touches game logic, which is fixed-step off dt, so every tier plays identically. - src/core/Performance.lua: tiers (auto/high/balanced/low), a conservative device auto-detect (ARM handhelds -> low, phones -> balanced, normal desktops -> high), per-tier caps, and the option-row cycle. Zero requires, like GameVersion. - Game:applyOptions clamps the *live* presentation state against the tier without rewriting stored options, so a lower tier hides the player's TILT/GBC FX/ZOOM/FPS choices and a higher tier restores them exactly. - Zoom.offsetRange floors the range at FIT when survey is disallowed, so the option row, hotkey, and mouse wheel all stop at close-up on LOW. - New save.options.performance default "auto"; OPTIONS row heads the display group and re-applies live. - Tests: tests/engine/performance_tiers.lua (ROM-free); mod_ui_tests row golden updated for the spliced row. AUTO resolves to HIGH on a normal desktop and on every options.lua that predates the option, so the common case is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q6bFAiQyZ5jDmewsbB4LG9
This commit is contained in:
@@ -108,6 +108,17 @@ supported out of the box.
|
||||
COLORS, TILT, ZOOM, GBC FX, and VOID FILL are also in the Options menu
|
||||
and persist in `options.lua`.
|
||||
|
||||
### Low-end devices
|
||||
|
||||
**OPTIONS → PERFORMANCE** scales the port's optional extras for weaker
|
||||
hardware: **HIGH** (everything on), **BALANCED** (no 3D tilt or GBC FX),
|
||||
**LOW** (also no survey zoom, FPS capped), or **AUTO** — the default, which
|
||||
picks a tier from your device (ARM handhelds → LOW, phones → BALANCED,
|
||||
normal desktops → HIGH, unchanged). It only scales presentation; the
|
||||
fixed-step game logic is identical on every tier, and a lower tier hides
|
||||
your tilt/zoom/GBC-FX preferences without forgetting them. Details in
|
||||
[docs/new-features.md](docs/new-features.md#performance-tier-low-end-devices).
|
||||
|
||||
### Rulesets
|
||||
|
||||
**OPTIONS → RULESET** picks which set of Gen 1 battle behaviors to run.
|
||||
|
||||
@@ -65,6 +65,13 @@ display option — COLORS, TILT, ZOOM, VOID FILL, MAX FPS — works normally. If
|
||||
your device turns out to handle the pass, launch with `POKEPORT_GBCFX=1` to
|
||||
put the row back.
|
||||
|
||||
**PERFORMANCE defaults to LOW here.** The OPTIONS → PERFORMANCE tier defaults
|
||||
to AUTO, which reads this device as an ARM Linux handheld and resolves to
|
||||
**LOW**: the 3D tilt and survey zoom stay off and the frame rate is capped,
|
||||
so the overworld runs smoothly on the H700 out of the box. Bump it to
|
||||
BALANCED or HIGH from OPTIONS if you want the extras and your device keeps
|
||||
up; see [Performance tier](new-features.md#performance-tier-low-end-devices).
|
||||
|
||||
The pack bundles the LÖVE 11.5 aarch64 runtime from
|
||||
[PortMaster](https://portmaster.games/), so the device does not need a
|
||||
separate `love_11.5` runtime download on first launch. The launcher resolves
|
||||
|
||||
@@ -137,6 +137,41 @@ effect, `=1` forces it available. The Anbernic handheld pack exports `0` from
|
||||
its launcher because the device reports `"Linux"` while its GPU is in the
|
||||
phone class (see [Anbernic RG34XXSP](anbernic-rg34xxsp.md)).
|
||||
|
||||
## Performance tier (low-end devices)
|
||||
|
||||
The Options **PERFORMANCE** row scales the port's optional presentation
|
||||
extras down for weaker hardware. The extras it governs are the three
|
||||
heaviest things the port adds on top of the original -- the whole-screen 3D
|
||||
**TILT** (transforms the entire map as a ground plane), the **GBC FX**
|
||||
post-process shader (a fullscreen pass), and survey **ZOOM** (zooming out
|
||||
renders the connected neighbor maps, a lot of extra overdraw) -- plus a hard
|
||||
FPS ceiling. None of this touches game logic, which is fixed-step off `dt`
|
||||
(`src/core/FixedStep.lua`), so every tier plays identically; they differ
|
||||
only in how much eye-candy the renderer is allowed to do.
|
||||
|
||||
| Tier | TILT | GBC FX | Survey ZOOM | Extra FPS ceiling |
|
||||
| ------------ | ---- | ------ | ----------- | ----------------- |
|
||||
| **HIGH** | on | on | on | none |
|
||||
| **BALANCED** | off | off | on | none |
|
||||
| **LOW** | off | off | off | 60 |
|
||||
| **AUTO** | picks a default from the device (below) |||
|
||||
|
||||
- **AUTO** (the default) reads the device once at boot: ARM Linux handhelds
|
||||
(e.g. the RG34XXSP) resolve to **LOW**, phones/tablets and very-low-core
|
||||
desktops to **BALANCED**, and everything else -- a normal desktop, and
|
||||
every existing `options.lua` that predates this option -- to **HIGH**,
|
||||
so the common case is unchanged. See `src/core/Performance.detect`.
|
||||
- AUTO only chooses the *default*; all four tiers are selectable, so a
|
||||
wrong guess is one row away from being overridden.
|
||||
- The clamps are applied **live** against your stored options and never
|
||||
rewrite them (`Game:applyOptions`), so a lower tier hides your TILT / GBC
|
||||
FX / ZOOM without forgetting them -- raising the tier restores exactly
|
||||
what you had. (This is why the TILT / GBC FX / ZOOM rows still show your
|
||||
saved choice on a clamped tier: it's your preference, waiting for a tier
|
||||
that can afford it.)
|
||||
- Persisted as `save.options.performance` (`auto` | `high` | `balanced` |
|
||||
`low`); unit-tested in `tests/engine/performance_tiers.lua`.
|
||||
|
||||
## Peer-to-peer link play (lua-enet)
|
||||
|
||||
Trades and link battles connect two copies of the game directly over
|
||||
|
||||
@@ -618,6 +618,23 @@ function Game:applyOptions(opts)
|
||||
-- normalizes a nil/garbage cap to the 60 default, so old saves with no
|
||||
-- fpsCap key pace at the standard rate (issue #88)
|
||||
require("src.core.FrameCap").applyOptions(opts)
|
||||
-- Scale the optional presentation extras to the device's performance
|
||||
-- tier. Every heavy feature was just applied from the stored options
|
||||
-- above; here we clamp the *live* state down for a weaker device without
|
||||
-- rewriting what the player saved, so raising the tier later restores
|
||||
-- their exact TILT / GBC FX / ZOOM / MAX FPS choices. A HIGH tier (the
|
||||
-- default on a normal desktop, and every options.lua predating this
|
||||
-- option) clamps nothing, so it is a no-op for the common case.
|
||||
local caps = require("src.core.Performance").applyOptions(opts)
|
||||
if not caps.tilt then require("src.render.Tilt").setLevel(0) end
|
||||
if not caps.gbcfx then require("src.render.GBCFX").setLevel(0) end
|
||||
local Zoom = require("src.render.Zoom")
|
||||
Zoom.allowSurvey = caps.survey
|
||||
if not caps.survey and Zoom.offset < 0 then Zoom.offset = 0 end
|
||||
if caps.fpsMax then
|
||||
local FrameCap = require("src.core.FrameCap")
|
||||
if FrameCap.current > caps.fpsMax then FrameCap.apply(caps.fpsMax) end
|
||||
end
|
||||
Input:applyBindings(opts.bindings)
|
||||
-- heal soft-bricked APK installs that already saved gbcfx > 0 (#136)
|
||||
if gbcCleared then self:writeOptions() end
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
-- Graphics performance tier: one knob that scales the port's optional,
|
||||
-- non-faithful presentation extras down for weaker hardware.
|
||||
--
|
||||
-- The heavy extras are all things the Game Boy never had and this port
|
||||
-- adds on top: the whole-screen 3D TILT (transforms the entire map as a
|
||||
-- ground plane), the GBC FX post-process shader (a fullscreen pass), and
|
||||
-- survey ZOOM (zooming out renders the connected neighbor maps -- a lot of
|
||||
-- extra overdraw). A hard FPS ceiling caps present cost on top of that.
|
||||
-- None of this touches game logic, which is fixed-step off dt
|
||||
-- (src/core/FixedStep.lua), so every tier plays identically; they differ
|
||||
-- only in how much optional eye-candy the renderer is allowed to do.
|
||||
--
|
||||
-- The tier is persisted as save.options.performance:
|
||||
-- auto pick a default from the device (see detect())
|
||||
-- high everything on -- the historical behavior
|
||||
-- balanced no TILT, no GBC FX (kept: survey zoom, colors, uncapped FPS)
|
||||
-- low no TILT, no GBC FX, no survey zoom, FPS capped
|
||||
--
|
||||
-- AUTO only chooses the *default* for the current device; every tier is
|
||||
-- selectable in OPTIONS, so a heuristic that guesses wrong is one row away
|
||||
-- from being overridden. The clamps are applied live in Game:applyOptions
|
||||
-- against the stored options without rewriting them, so raising the tier
|
||||
-- restores exactly the TILT / GBC FX / ZOOM / FPS the player had.
|
||||
--
|
||||
-- Zero requires: loads during love.conf and under plain Lua for tools and
|
||||
-- tests, the same way src/core/GameVersion.lua does.
|
||||
|
||||
local Performance = {}
|
||||
|
||||
-- Option-row order (auto first, then most to least capable).
|
||||
Performance.TIERS = { "auto", "high", "balanced", "low" }
|
||||
|
||||
Performance.LABELS = {
|
||||
auto = "AUTO",
|
||||
high = "HIGH",
|
||||
balanced = "BALANCED",
|
||||
low = "LOW",
|
||||
}
|
||||
|
||||
-- What each concrete tier permits. `auto` is resolved to one of these
|
||||
-- before caps are read, so it has no row here. fpsMax = false means no
|
||||
-- extra ceiling (the player's own MAX FPS still applies).
|
||||
Performance.CAPS = {
|
||||
high = { tilt = true, gbcfx = true, survey = true, fpsMax = false },
|
||||
balanced = { tilt = false, gbcfx = false, survey = true, fpsMax = false },
|
||||
low = { tilt = false, gbcfx = false, survey = false, fpsMax = 60 },
|
||||
}
|
||||
|
||||
-- Live resolved tier (never "auto"); Game:applyOptions sets it and the
|
||||
-- renderer / options row read it back. Defaults to the unclamped tier so
|
||||
-- a pre-boot launcher behaves exactly as it always did.
|
||||
Performance.tier = "high"
|
||||
|
||||
local function loveOS()
|
||||
if love and love.system and love.system.getOS then
|
||||
return love.system.getOS()
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function processorCount()
|
||||
if love and love.system and love.system.getProcessorCount then
|
||||
return love.system.getProcessorCount()
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- CPU architecture via LuaJIT's jit.arch when present: "arm"/"arm64" on
|
||||
-- phones and PortMaster handhelds, "x86"/"x64" on desktops. nil under a
|
||||
-- plain-Lua tool or test with no jit table, which resolves to HIGH (no
|
||||
-- clamping) so tooling is never surprised.
|
||||
local function cpuArch()
|
||||
return (jit and jit.arch) or nil
|
||||
end
|
||||
|
||||
-- Default tier for the current device. Deliberately conservative: only
|
||||
-- the platforms that are reliably weak drop below HIGH, and AUTO is always
|
||||
-- overridable, so a wrong guess costs one OPTIONS row. A normal
|
||||
-- multi-core desktop -- the common case, and every existing install whose
|
||||
-- options.lua predates this option -- resolves to HIGH and behaves exactly
|
||||
-- as before.
|
||||
function Performance.detect()
|
||||
local os = loveOS()
|
||||
local arch = cpuArch()
|
||||
local isArm = arch == "arm" or arch == "arm64"
|
||||
local cores = processorCount()
|
||||
|
||||
-- PortMaster-style ARM Linux handhelds (e.g. the RG34XXSP the project
|
||||
-- already ships a build for): the weakest target here.
|
||||
if isArm and os ~= "Android" and os ~= "iOS" then
|
||||
return "low"
|
||||
end
|
||||
-- Phones and tablets: GBC FX is already force-disabled here (issue #136);
|
||||
-- balanced additionally drops the 3D tilt, the heaviest remaining extra.
|
||||
if os == "Android" or os == "iOS" then
|
||||
return "balanced"
|
||||
end
|
||||
-- Desktop: a dual-core (or single-core) box is the low-end line.
|
||||
if cores and cores <= 2 then
|
||||
return "balanced"
|
||||
end
|
||||
return "high"
|
||||
end
|
||||
|
||||
-- Fold a stored option value to a concrete tier, resolving "auto" (and any
|
||||
-- hand-edited garbage) through detect().
|
||||
function Performance.resolve(value)
|
||||
if value == "high" or value == "balanced" or value == "low" then
|
||||
return value
|
||||
end
|
||||
return Performance.detect()
|
||||
end
|
||||
|
||||
-- Normalize a stored value to a valid option id (keeps "auto"); a bad value
|
||||
-- degrades to auto so a corrupt options.lua still lands on something sane.
|
||||
function Performance.normalize(value)
|
||||
for _, id in ipairs(Performance.TIERS) do
|
||||
if value == id then return id end
|
||||
end
|
||||
return "auto"
|
||||
end
|
||||
|
||||
-- Row label for a stored value: the tier's own name (AUTO / HIGH / ...).
|
||||
function Performance.label(value)
|
||||
return Performance.LABELS[Performance.normalize(value)]
|
||||
end
|
||||
|
||||
-- Concrete caps for a stored option value (resolving auto).
|
||||
function Performance.caps(value)
|
||||
return Performance.CAPS[Performance.resolve(value)]
|
||||
end
|
||||
|
||||
-- Resolve the tier for these options, record it live, and return its caps.
|
||||
-- Called from Game:applyOptions, which clamps the live presentation modules
|
||||
-- against the returned caps.
|
||||
function Performance.applyOptions(opts)
|
||||
local tier = Performance.resolve(opts and opts.performance)
|
||||
Performance.tier = tier
|
||||
return Performance.CAPS[tier]
|
||||
end
|
||||
|
||||
-- Cycle the OPTIONS row: auto -> high -> balanced -> low -> auto (dir -1
|
||||
-- reverses). Lua's `%` is non-negative for a positive modulus, so a
|
||||
-- negative dir wraps correctly.
|
||||
function Performance.cycle(value, dir)
|
||||
value = Performance.normalize(value)
|
||||
local i = 1
|
||||
for idx, id in ipairs(Performance.TIERS) do
|
||||
if id == value then i = idx break end
|
||||
end
|
||||
local n = #Performance.TIERS
|
||||
local nextIdx = (i - 1 + (dir or 1)) % n + 1
|
||||
return Performance.TIERS[nextIdx]
|
||||
end
|
||||
|
||||
return Performance
|
||||
@@ -241,6 +241,11 @@ function SaveData.defaultOptions()
|
||||
videoMode = "windowed",
|
||||
-- hard render frame-rate cap; render-only pacing (issue #88, FrameCap.lua)
|
||||
fpsCap = 60,
|
||||
-- graphics performance tier: auto | high | balanced | low. "auto"
|
||||
-- picks a default from the device (ARM handhelds/phones drop the heavy
|
||||
-- extras); scales TILT / GBC FX / survey ZOOM / FPS but never game
|
||||
-- logic. See src/core/Performance.lua.
|
||||
performance = "auto",
|
||||
-- Per-pipeline display levels, keyed by render_pipelines id (see
|
||||
-- src/render/Pipelines.lua). A level for a mod that is not installed
|
||||
-- is kept rather than pruned, so re-enabling the mod restores the mode
|
||||
|
||||
@@ -10,6 +10,13 @@ local Zoom = {}
|
||||
|
||||
Zoom.offset = 0
|
||||
|
||||
-- Survey zoom (zooming out past FIT, which renders connected neighbor maps)
|
||||
-- is the port's most expensive optional extra. The performance tier sets
|
||||
-- this false on LOW hardware (Game:applyOptions); offsetRange then floors
|
||||
-- the range at FIT so the option row, hotkey, and mouse wheel all stop at
|
||||
-- close-up. Nil/true keeps the historical full range.
|
||||
Zoom.allowSurvey = true
|
||||
|
||||
-- legal offset range for a given fit scale (vanilla: survey at 1 px/world
|
||||
-- through 2× fit). zoom.range may widen or shrink the window.
|
||||
function Zoom.offsetRange(S)
|
||||
@@ -21,6 +28,9 @@ function Zoom.offsetRange(S)
|
||||
hi = math.floor(tonumber(hi) or S)
|
||||
if lo > hi then lo, hi = hi, lo end
|
||||
end
|
||||
-- LOW performance tier: no survey (negative offsets), even if a mod's
|
||||
-- zoom.range widened it. == false so nil/true stays permissive.
|
||||
if Zoom.allowSurvey == false and lo < 0 then lo = 0 end
|
||||
return lo, hi
|
||||
end
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ local GameSpeed = require("src.core.GameSpeed")
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local VideoMode = require("src.core.VideoMode")
|
||||
local FrameCap = require("src.core.FrameCap")
|
||||
local Performance = require("src.core.Performance")
|
||||
local Logger = require("src.core.Logger")
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
local OptionRows = require("src.ui.OptionRows")
|
||||
@@ -202,6 +203,21 @@ local function buildRows(game)
|
||||
require("src.core.Music").setFilterLevel(o.musicFilter)
|
||||
return true
|
||||
end },
|
||||
-- Heads the port's display group: one tier that scales the heavy extras
|
||||
-- (TILT / GBC FX / survey ZOOM) and the FPS ceiling for weaker devices.
|
||||
-- AUTO picks a default from the hardware; every tier is overridable.
|
||||
-- Re-applies live so the extras clamp (or, on a higher tier, restore to
|
||||
-- the player's stored TILT / GBC FX / ZOOM) the moment the row changes.
|
||||
{ id = "performance", label = Strings("PERFORMANCE"),
|
||||
value = function(g)
|
||||
return Strings(Performance.label(g.save.options.performance))
|
||||
end,
|
||||
step = function(g, dir)
|
||||
local o = g.save.options
|
||||
o.performance = Performance.cycle(o.performance, dir)
|
||||
g:applyOptions(o)
|
||||
return true
|
||||
end },
|
||||
{ id = "colors", label = Strings("COLORS"),
|
||||
value = function(g)
|
||||
return PaletteFX.modeLabel(g.save.options.colors or "gbc")
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
-- Graphics performance tier (src/core/Performance.lua): the AUTO device
|
||||
-- heuristic, tier normalization/labels, per-tier caps, the option-row
|
||||
-- cycle, and applyOptions recording the live tier. Pure logic over
|
||||
-- stubbed love/jit globals, so it needs no ROM and runs in the T2 tier.
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local Performance = require("src.core.Performance")
|
||||
|
||||
-- detect() reads the live `love` and `jit` globals; swap them per scenario
|
||||
-- and restore afterward so nothing leaks between checks (or into luajit).
|
||||
local realLove, realJit = love, jit
|
||||
|
||||
local function device(os, arch, cores)
|
||||
love = {
|
||||
system = {
|
||||
getOS = function() return os end,
|
||||
getProcessorCount = function() return cores end,
|
||||
},
|
||||
}
|
||||
jit = arch and { arch = arch } or nil
|
||||
end
|
||||
|
||||
local function restore()
|
||||
love, jit = realLove, realJit
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------- detect
|
||||
device("Linux", "arm64", 4)
|
||||
T.eq(Performance.detect(), "low", "ARM64 Linux handheld -> low")
|
||||
|
||||
device("Linux", "arm", 2)
|
||||
T.eq(Performance.detect(), "low", "32-bit ARM Linux handheld -> low")
|
||||
|
||||
device("Android", "arm64", 8)
|
||||
T.eq(Performance.detect(), "balanced", "Android phone -> balanced (not low)")
|
||||
|
||||
device("iOS", "arm64", 6)
|
||||
T.eq(Performance.detect(), "balanced", "iOS -> balanced")
|
||||
|
||||
device("OS X", "x64", 8)
|
||||
T.eq(Performance.detect(), "high", "8-core desktop -> high")
|
||||
|
||||
device("Windows", "x64", 2)
|
||||
T.eq(Performance.detect(), "balanced", "dual-core desktop -> balanced")
|
||||
|
||||
device("Windows", "x86", 1)
|
||||
T.eq(Performance.detect(), "balanced", "single-core desktop -> balanced")
|
||||
|
||||
-- No love, no jit (a plain-Lua tool/test): nothing looks weak -> high, so
|
||||
-- tooling that runs applyOptions is never surprised by a clamp.
|
||||
love, jit = nil, nil
|
||||
T.eq(Performance.detect(), "high", "no love/jit -> high (no clamping)")
|
||||
restore()
|
||||
|
||||
-- ---------------------------------------------------------------- resolve
|
||||
T.eq(Performance.resolve("high"), "high", "resolve passes concrete high")
|
||||
T.eq(Performance.resolve("balanced"), "balanced", "resolve passes balanced")
|
||||
T.eq(Performance.resolve("low"), "low", "resolve passes low")
|
||||
|
||||
device("Linux", "arm64", 4)
|
||||
T.eq(Performance.resolve("auto"), "low", "resolve auto -> detect (handheld)")
|
||||
T.eq(Performance.resolve(nil), "low", "resolve nil -> detect")
|
||||
T.eq(Performance.resolve("bogus"), "low", "resolve garbage -> detect")
|
||||
restore()
|
||||
|
||||
-- --------------------------------------------------------------- normalize
|
||||
T.eq(Performance.normalize("auto"), "auto", "normalize keeps auto")
|
||||
T.eq(Performance.normalize("low"), "low", "normalize keeps low")
|
||||
T.eq(Performance.normalize("xyz"), "auto", "normalize garbage -> auto")
|
||||
T.eq(Performance.normalize(nil), "auto", "normalize nil -> auto")
|
||||
|
||||
-- ------------------------------------------------------------------- label
|
||||
T.eq(Performance.label("low"), "LOW", "label low")
|
||||
T.eq(Performance.label("balanced"), "BALANCED", "label balanced")
|
||||
T.eq(Performance.label(nil), "AUTO", "label nil -> AUTO")
|
||||
|
||||
-- -------------------------------------------------------------------- caps
|
||||
local hi, lo = Performance.CAPS.high, Performance.CAPS.low
|
||||
T.check(hi.tilt and hi.gbcfx and hi.survey and not hi.fpsMax, "high caps: all on, no fps ceiling")
|
||||
T.check((not lo.tilt) and (not lo.gbcfx) and (not lo.survey), "low caps: heavy extras off")
|
||||
T.eq(lo.fpsMax, 60, "low caps: FPS ceiling of 60")
|
||||
local bal = Performance.CAPS.balanced
|
||||
T.check((not bal.tilt) and (not bal.gbcfx) and bal.survey and not bal.fpsMax,
|
||||
"balanced caps: no tilt/gbcfx, survey kept, no fps ceiling")
|
||||
|
||||
device("Linux", "arm64", 4)
|
||||
T.eq(Performance.caps("auto"), Performance.CAPS.low, "caps(auto) resolves through detect")
|
||||
restore()
|
||||
|
||||
-- ------------------------------------------------------------- applyOptions
|
||||
local caps = Performance.applyOptions({ performance = "low" })
|
||||
T.eq(caps, Performance.CAPS.low, "applyOptions returns the resolved caps")
|
||||
T.eq(Performance.tier, "low", "applyOptions records the live tier")
|
||||
|
||||
Performance.applyOptions({ performance = "high" })
|
||||
T.eq(Performance.tier, "high", "applyOptions high tier")
|
||||
|
||||
device("Linux", "arm64", 4)
|
||||
Performance.applyOptions(nil)
|
||||
T.eq(Performance.tier, "low", "applyOptions(nil) resolves auto to the device tier")
|
||||
restore()
|
||||
|
||||
-- ------------------------------------------------------------------- cycle
|
||||
T.eq(Performance.cycle("auto", 1), "high", "cycle auto -> high")
|
||||
T.eq(Performance.cycle("high", 1), "balanced", "cycle high -> balanced")
|
||||
T.eq(Performance.cycle("balanced", 1), "low", "cycle balanced -> low")
|
||||
T.eq(Performance.cycle("low", 1), "auto", "cycle low wraps to auto")
|
||||
T.eq(Performance.cycle("auto", -1), "low", "cycle back auto -> low")
|
||||
T.eq(Performance.cycle("high", -1), "auto", "cycle back high -> auto")
|
||||
|
||||
T.finish("performance_tiers")
|
||||
+16
-14
@@ -282,7 +282,8 @@ local function optGame()
|
||||
end
|
||||
local om = OptionsMenu.new(optGame())
|
||||
local WANT_IDS = { "textSpeed", "animations", "battleStyle", "battleLayout",
|
||||
"ruleset", "musicVol", "sfxVol", "musicFilter", "colors",
|
||||
"ruleset", "musicVol", "sfxVol", "musicFilter",
|
||||
"performance", "colors",
|
||||
"tilt", "gbcfx", "zoom", "voidFill", "videoMode", "fpsCap",
|
||||
"speed", "mods", "controls" }
|
||||
check(#om.rows == #WANT_IDS, "vanilla options row count (plus MODS/CONTROLS)")
|
||||
@@ -320,35 +321,36 @@ check(om.game.save.options.musicVol == 6, "music volume steps down")
|
||||
for _ = 1, 10 do om.rows[6].step(om.game, -1) end
|
||||
check(om.game.save.options.musicVol == 0, "music volume clamps at 0")
|
||||
|
||||
-- ZOOM / VOID FILL rows
|
||||
-- ZOOM / VOID FILL rows (indices shifted +1 by the PERFORMANCE row spliced
|
||||
-- in ahead of COLORS)
|
||||
local Zoom = require("src.render.Zoom")
|
||||
local TileRenderer = require("src.render.TileRenderer")
|
||||
om.game.save.options.zoom = 0
|
||||
Zoom.offset = 0
|
||||
check(om.rows[12].value(om.game) == "FIT", "ZOOM row shows FIT at offset 0")
|
||||
om.rows[12].step(om.game, 1)
|
||||
check(om.rows[13].value(om.game) == "FIT", "ZOOM row shows FIT at offset 0")
|
||||
om.rows[13].step(om.game, 1)
|
||||
check(om.game.save.options.zoom == 1 and Zoom.offset == 1,
|
||||
"ZOOM row steps to IN1")
|
||||
om.rows[13].step(om.game, 1)
|
||||
om.rows[14].step(om.game, 1)
|
||||
check(om.game.save.options.voidFill == "water"
|
||||
and TileRenderer.voidFill == "water",
|
||||
"VOID FILL row cycles TREES → WATER")
|
||||
om.rows[13].step(om.game, 1)
|
||||
om.rows[14].step(om.game, 1)
|
||||
check(om.game.save.options.voidFill == "black", "VOID FILL steps to BLACK")
|
||||
om.rows[13].step(om.game, 1)
|
||||
om.rows[14].step(om.game, 1)
|
||||
check(om.game.save.options.voidFill == "trees", "VOID FILL wraps to TREES")
|
||||
|
||||
-- the MAX FPS row cycles the render-cap steps and shows the value plain
|
||||
om.game.save.options.fpsCap = nil
|
||||
check(om.rows[15].value(om.game) == "60",
|
||||
check(om.rows[16].value(om.game) == "60",
|
||||
"MAX FPS row defaults to 60 with no saved cap")
|
||||
om.rows[15].step(om.game, 1)
|
||||
om.rows[16].step(om.game, 1)
|
||||
check(om.game.save.options.fpsCap == 75, "MAX FPS steps up from 60 to 75")
|
||||
check(om.rows[15].value(om.game) == "75", "the MAX FPS row renders the cap")
|
||||
check(om.rows[16].value(om.game) == "75", "the MAX FPS row renders the cap")
|
||||
om.game.save.options.fpsCap = 160
|
||||
om.rows[15].step(om.game, 1)
|
||||
om.rows[16].step(om.game, 1)
|
||||
check(om.game.save.options.fpsCap == 30, "MAX FPS wraps past the ceiling to 30")
|
||||
om.rows[15].step(om.game, -1)
|
||||
om.rows[16].step(om.game, -1)
|
||||
check(om.game.save.options.fpsCap == 160, "MAX FPS wraps back down to the ceiling")
|
||||
|
||||
-- ------- FrameCap normalize / cycle (issue #88)
|
||||
@@ -378,7 +380,7 @@ check(FrameCap.current == 60, "FrameCap.applyOptions defaults a missing key to 6
|
||||
-- the MODS row is the manager's discoverable home
|
||||
local mgGame = optGame()
|
||||
om = OptionsMenu.new(mgGame)
|
||||
om.rows[17].activate(mgGame)
|
||||
om.rows[18].activate(mgGame)
|
||||
check(getmetatable(mgGame.stack:top()) == ManagerState,
|
||||
"the MODS row opens the manager")
|
||||
check(mgGame.stack:top().screenId == "ManagerState",
|
||||
@@ -388,7 +390,7 @@ check(mgGame.stack:top().screenId == "ManagerState",
|
||||
local BindingsMenu = require("src.ui.BindingsMenu")
|
||||
local cbGame = optGame()
|
||||
om = OptionsMenu.new(cbGame)
|
||||
om.rows[18].activate(cbGame)
|
||||
om.rows[19].activate(cbGame)
|
||||
local bm = cbGame.stack:top()
|
||||
check(getmetatable(bm) == BindingsMenu,
|
||||
"the CONTROLS row opens the rebind list")
|
||||
|
||||
Reference in New Issue
Block a user