Android SCALING fixes (#298)

* android fixes for mods and saves

* perhaps this is the true scaling android issue fix
This commit is contained in:
bryanthaboi
2026-07-27 09:27:24 -04:00
committed by GitHub
parent 25df838b45
commit 3448fae3f5
6 changed files with 118 additions and 11 deletions
+5 -2
View File
@@ -3876,8 +3876,11 @@ function BattleState:colorMode()
if g and g.newCanvas and g.setScissor and g.setShader and g.getCanvas
and love.image and PaletteFX.pack(self.data)
and PaletteFX.shader() then
local ok1, bg = pcall(g.newCanvas, 160, 144)
local ok2, wv = pcall(g.newCanvas, 160, 144)
-- 160x144 real pixels, not DPI units, or the colored battle background
-- resamples against the UI canvas on mobile (#208; PixelCanvas.lua)
local PixelCanvas = require("src.render.PixelCanvas")
local ok1, bg = pcall(PixelCanvas.new, 160, 144)
local ok2, wv = pcall(PixelCanvas.new, 160, 144)
if ok1 and ok2 and bg and wv then
self.bgCanvas, self.waveCanvas = bg, wv
ready = true
+14
View File
@@ -96,6 +96,20 @@ function Game:load()
end
Logger.info("game loaded")
-- Scaling bug reports (#87, #208) are unanswerable without these three
-- numbers: LOVE units, drawable pixels, and the integer physical pixels
-- per GB pixel the renderer settled on. Cheap, once, and it turns "it
-- looks stretched" into something reproducible.
if love.graphics and love.graphics.getDimensions then
local ww, wh = love.graphics.getDimensions()
local pw, ph = ww, wh
if love.graphics.getPixelDimensions then
pw, ph = love.graphics.getPixelDimensions()
end
Logger.info(string.format(
"display: %dx%d units, %dx%d px, fit scale %d px/GB px",
ww, wh, pw, ph, Renderer:fitScale()))
end
end
-- the merged field.boot: spawn, names, money and the naming presets a
+45
View File
@@ -0,0 +1,45 @@
-- Render targets measured in real framebuffer pixels.
--
-- love.graphics.newCanvas defaults its `dpiscale` to
-- love.graphics.getDPIScale(), so on a highdpi surface the canvas texture is
-- NOT the size it was asked for: newCanvas(160, 144) on a device reporting a
-- DPI scale of 2.755 allocates a 441x397 texture, and the 160x144 GB scene
-- then renders into it at 2.755 texels per GB pixel. conf.lua sets
-- t.window.highdpi on Android/iOS (required for Retina), and Android's
-- DisplayMetrics.density is routinely non-integer (1.5, 2.75, ...), so this
-- is the normal mobile case, not an edge case.
--
-- That fractional render breaks the whole premise of Renderer's integer
-- scale pipeline. Renderer:fitScale() picks a whole number of physical
-- pixels per GB pixel (7 on 1080p) and the composite blit lands on it
-- exactly -- but if the *source* already holds 2.755 texels per GB pixel,
-- some GB pixels are 2 texels wide and some are 3, and the nearest-neighbour
-- upscale turns them into 5 / 7 / 8 physical pixels instead of a uniform 7.
-- Measured on a 1080p density-2.755 fixture, only 43 of 160 columns came out
-- the right width. Fonts show it worst: their strokes are single pixels.
-- That is issue #208 ("some are noticeably stretched, especially fonts").
--
-- Forcing dpiscale = 1 makes canvas texels the pixels the renderer already
-- believes it is drawing. getWidth()/getHeight() are unaffected (they always
-- reported the requested size), so no geometry anywhere changes -- only the
-- resolution of the target. Desktop is unchanged; dpiscale is already 1
-- there because conf.lua sets highdpi on mobile only.
--
-- Exception: a canvas that is deliberately sized in LOVE *units* and blitted
-- back at unit scale 1 (Renderer's presentCanvas) must keep the screen's DPI
-- scale so its texture still covers the framebuffer; it is not built with
-- this helper.
local PixelCanvas = {}
-- One framebuffer pixel per w/h unit, always. `filter` is applied only when
-- given, so callers that relied on LOVE's default ("linear") keep it.
function PixelCanvas.new(w, h, filter)
local canvas = love.graphics.newCanvas(w, h, { dpiscale = 1 })
if filter and canvas and canvas.setFilter then
canvas:setFilter(filter, filter)
end
return canvas
end
return PixelCanvas
+16 -8
View File
@@ -10,6 +10,7 @@ local Zoom = require("src.render.Zoom")
local Tilt = require("src.render.Tilt")
local PaletteFX = require("src.render.PaletteFX")
local Pipelines = require("src.render.Pipelines")
local PixelCanvas = require("src.render.PixelCanvas")
local Runtime = require("src.mods.Runtime")
local Renderer = {}
@@ -79,8 +80,10 @@ local function displayMetrics()
end
function Renderer:init()
self.canvas = love.graphics.newCanvas(self.WIDTH, self.HEIGHT)
self.canvas:setFilter("nearest", "nearest")
-- 160x144 real pixels, never DPI-scaled: see src/render/PixelCanvas.lua
-- (#208). Every canvas below is sized in framebuffer pixels for the same
-- reason -- worldViewSize() already works in drawable pixels.
self.canvas = PixelCanvas.new(self.WIDTH, self.HEIGHT, "nearest")
self.worldCanvas = nil
self.worldActive = false
-- tilt mode only: a transparent overlay canvas the size of the world
@@ -231,8 +234,7 @@ function Renderer:beginWorldPass()
-- the view size every frame, so without this the superseded canvases
-- pile up in VRAM until a GC finalizer happens to run
if self.worldCanvas and self.worldCanvas.release then self.worldCanvas:release() end
self.worldCanvas = love.graphics.newCanvas(vw, vh)
self.worldCanvas:setFilter("nearest", "nearest")
self.worldCanvas = PixelCanvas.new(vw, vh, "nearest")
end
self.worldActive = true
PaletteFX.setPass("world")
@@ -260,8 +262,7 @@ function Renderer:beginUprightPass()
if not self.uprightCanvas or self.uprightCanvas:getWidth() ~= cw
or self.uprightCanvas:getHeight() ~= ch then
if self.uprightCanvas and self.uprightCanvas.release then self.uprightCanvas:release() end
self.uprightCanvas = love.graphics.newCanvas(cw, ch)
self.uprightCanvas:setFilter("nearest", "nearest")
self.uprightCanvas = PixelCanvas.new(cw, ch, "nearest")
end
self.uprightActive = true
PaletteFX.setPass(nil)
@@ -355,8 +356,7 @@ function Renderer:drawTiltedWorld(zoneList, sx, sy, wox, woy, target)
if not self.tiltCanvas or self.tiltCanvas:getWidth() ~= wvw
or self.tiltCanvas:getHeight() ~= wvh then
if self.tiltCanvas and self.tiltCanvas.release then self.tiltCanvas:release() end
self.tiltCanvas = love.graphics.newCanvas(wvw, wvh)
self.tiltCanvas:setFilter("linear", "linear")
self.tiltCanvas = PixelCanvas.new(wvw, wvh, "linear")
end
love.graphics.setCanvas(self.tiltCanvas)
@@ -465,6 +465,14 @@ function Renderer:endFrame(zones, worldZones)
if needPresent then
if not self.presentCanvas or self.presentCanvas:getWidth() ~= ww
or self.presentCanvas:getHeight() ~= wh then
-- The one canvas NOT built through PixelCanvas: it is sized in LOVE
-- units and blitted back at unit scale 1 (and handed to mod present
-- passes as ww x wh), so it has to keep the screen's DPI scale for its
-- texture to cover the framebuffer. Everything composited into it is
-- already native-resolution now, so #208's fractional source is gone;
-- what remains here is the dpiX vs dpiY truncation gap (well under 1%,
-- one seam across the window) that a single scalar dpiscale cannot
-- express.
self.presentCanvas = love.graphics.newCanvas(ww, wh)
self.presentCanvas:setFilter("linear", "linear")
end
+4 -1
View File
@@ -542,7 +542,10 @@ end
local function bakeBorderFill(self, block)
local border = self.map.tileset.blocks[block + 1]
if not border then return end
local canvas = love.graphics.newCanvas(32, 32)
-- 32x32 real pixels: a DPI-scaled canvas would bake the border block at a
-- fractional texel size and the repeat-wrapped image would then tile at
-- non-square pixels (#208, see src/render/PixelCanvas.lua)
local canvas = require("src.render.PixelCanvas").new(32, 32)
love.graphics.push("all")
love.graphics.setCanvas(canvas)
love.graphics.clear(1, 1, 1, 1)
+34
View File
@@ -1917,6 +1917,40 @@ do
check(math.abs(physX - 7) < 1e-9 and math.abs(physY - 7) < 1e-9,
"#208 swapped-aspect still yields square 7x7 physical GB pixels")
-- #208 part two: getting the draw scale right is useless if the SOURCE is
-- fractional. love.graphics.newCanvas defaults dpiscale to
-- love.graphics.getDPIScale(), so on mobile (conf.lua sets highdpi) a
-- newCanvas(160, 144) is really a 441x397 texture holding 2.755 texels per
-- GB pixel; the integer blit then lands those on 5 / 7 / 8 physical pixels
-- instead of a uniform 7. Every render target must be pixel-exact.
Zoom.reset()
g.getDimensions = function() return 698, 392 end
g.getPixelDimensions = function() return 1920, 1080 end
g.getDPIScale = function() return 1080 / 392 end
-- one table, not a fistful of locals: this chunk is close to Lua's
-- 200-local ceiling and a few more here overflow it
local probe = { made = {}, newCanvas = g.newCanvas,
ui = Renderer.canvas, world = Renderer.worldCanvas }
g.newCanvas = function(w, h, settings)
probe.made[#probe.made + 1] =
{ w = w, h = h, dpiscale = settings and settings.dpiscale }
return probe.newCanvas(w, h)
end
Renderer:init()
Renderer:beginWorldPass()
love.graphics.setCanvas()
g.newCanvas = probe.newCanvas
Renderer.canvas, Renderer.worldCanvas = probe.ui, probe.world
Renderer.worldActive = false
check(#probe.made >= 2, "#208 init + world pass allocated their canvases")
for _, c in ipairs(probe.made) do
eq(c.dpiscale, 1,
("#208 canvas %dx%d is pixel-exact (dpiscale 1, not the screen's)")
:format(c.w, c.h))
if c.w == 160 and c.h == 144 then probe.sawUi = true end
end
check(probe.sawUi, "#208 the UI canvas is requested at exactly 160x144")
-- missing pixel API falls back to getDimensions (headless / old stub)
g.getPixelDimensions = nil
g.getDPIScale = nil