From 1de57e629f156b3f0b9dea535c091fc5e54ae363 Mon Sep 17 00:00:00 2001 From: Myles Resnick Date: Sat, 1 Aug 2026 17:02:19 -0400 Subject: [PATCH 1/2] Pixel-sized canvases: dpiscale 1 on every framebuffer allocation newCanvas defaults a canvas's dpiscale to the display's, and every width/height handed to these eight allocations is already in PIXELS -- so on a highdpi phone (iOS and Android both run with highdpi on, see the engine's conf.lua) each framebuffer came out density times larger per axis than asked for. At a 3x display the window-sized scene canvas is NINE times the screen's pixels, rendered in full -- geometry, sun shadows, water, the lot -- and then downsampled to a ninth of that at the composite. The mirror the water reads, both tilt-shift buffers, both DOF buffers, the HUD frost and the shadow map (1024 asked, 3072x3072 stored) all paid the same multiplier. TerrainAtlas and BattlePics already pass dpiscale = 1 for exactly this reason; this brings the remaining allocations under the same rule. Nothing about coordinates changes -- every caller works in the canvas's nominal units, which are what getDimensions() reports either way -- so desktops (dpiscale 1) are bit-identical, and phones render the pixels their screens can actually show. On an iPhone 16 Pro this is the difference between the mod visibly heating the device and not. Co-Authored-By: Claude Fable 5 --- lib/BattleDOF.lua | 5 +++-- lib/BattleHud.lua | 3 ++- lib/ShadowMap.lua | 6 +++++- lib/TiltShift.lua | 7 +++++-- lib/Voxel3D.lua | 16 +++++++++++++--- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/lib/BattleDOF.lua b/lib/BattleDOF.lua index 1b58ca4..3373774 100644 --- a/lib/BattleDOF.lua +++ b/lib/BattleDOF.lua @@ -98,9 +98,10 @@ end -- would reallocate both every time a battle started or ended. local function getCanvases(w, h) if not ping or cw ~= w or ch ~= h then - local ok, a = pcall(love.graphics.newCanvas, w, h) + -- dpiscale 1: pixel-sized buffers, like TiltShift's (see Voxel3D.newDepth) + local ok, a = pcall(love.graphics.newCanvas, w, h, { dpiscale = 1 }) if not ok then return nil end - local okB, b = pcall(love.graphics.newCanvas, w, h) + local okB, b = pcall(love.graphics.newCanvas, w, h, { dpiscale = 1 }) if not okB then return nil end -- the gaussian's fractional tap offsets need linear filtering a:setFilter("linear", "linear") diff --git a/lib/BattleHud.lua b/lib/BattleHud.lua index 34432e7..e2f3dfd 100644 --- a/lib/BattleHud.lua +++ b/lib/BattleHud.lua @@ -79,7 +79,8 @@ local function getShader() end local function canvasOf(w, h, filter) - local ok, c = pcall(love.graphics.newCanvas, w, h) + -- dpiscale 1: pixel-sized buffers (see Voxel3D.newDepth for the trap) + local ok, c = pcall(love.graphics.newCanvas, w, h, { dpiscale = 1 }) if not ok then return nil end c:setFilter(filter or "linear", filter or "linear") return c diff --git a/lib/ShadowMap.lua b/lib/ShadowMap.lua index 18ceffd..7824471 100644 --- a/lib/ShadowMap.lua +++ b/lib/ShadowMap.lua @@ -186,7 +186,11 @@ end local function getCanvas(res) if canvas == false then return nil end if canvas and canvasRes == res then return canvas end - local ok, c = pcall(love.graphics.newCanvas, res, res) + -- dpiscale 1: `res` is the map's edge in TEXELS, and a highdpi default + -- would cube the memory for texels the filter never asks for (see + -- Voxel3D.newDepth) -- sunTexel = 1/res in the main pass assumes the + -- stored map is exactly res texels across + local ok, c = pcall(love.graphics.newCanvas, res, res, { dpiscale = 1 }) if not (ok and c) then canvas = false return nil diff --git a/lib/TiltShift.lua b/lib/TiltShift.lua index d95c10e..a9852aa 100644 --- a/lib/TiltShift.lua +++ b/lib/TiltShift.lua @@ -85,9 +85,12 @@ end local function getCanvases(w, h) if not ping or cw ~= w or ch ~= h then - local ok, a = pcall(love.graphics.newCanvas, w, h) + -- dpiscale 1: `w`/`h` came off the input canvas in pixels, and a + -- highdpi default would multiply by the display density again (see + -- Voxel3D.newDepth) -- two gaussian passes at 9x the pixels + local ok, a = pcall(love.graphics.newCanvas, w, h, { dpiscale = 1 }) if not ok then return nil end - local okB, b = pcall(love.graphics.newCanvas, w, h) + local okB, b = pcall(love.graphics.newCanvas, w, h, { dpiscale = 1 }) if not okB then return nil end -- the gaussian's fractional tap offsets need linear filtering a:setFilter("linear", "linear") diff --git a/lib/Voxel3D.lua b/lib/Voxel3D.lua index 54ef847..e345b34 100644 --- a/lib/Voxel3D.lua +++ b/lib/Voxel3D.lua @@ -308,8 +308,15 @@ local function newDepth(w, h) if not (love.graphics and love.graphics.newCanvas) then return nil end local c = nil for _, format in ipairs(DEPTH_FORMATS) do + -- dpiscale 1: `w`/`h` are already PIXELS. Left to default, a highdpi + -- surface (iOS, Android -- see conf.lua) multiplies the canvas by the + -- display density AGAIN -- the same trap TerrainAtlas and BattlePics + -- already step around -- and a nominally window-sized buffer comes out + -- density^2 the screen's area (9x at 3x). Everywhere a canvas here is + -- sized from pixel dimensions gets the same option, so no pair of them + -- can disagree about what a coordinate means. local ok, made = pcall(love.graphics.newCanvas, w, h, - { format = format, readable = true }) + { format = format, readable = true, dpiscale = 1 }) if ok and made then c = made break end end if not c then return nil end @@ -640,7 +647,8 @@ function Voxel3D.beginScene(w, h, cx, cy, vw, vh, sky, slot) local name = slot or "world" local slotHeld = slots[name] if not (slotHeld and slotHeld.w == w and slotHeld.h == h) then - local ok, c = pcall(love.graphics.newCanvas, w, h) + -- dpiscale 1, like the depth canvas (see newDepth for the why) + local ok, c = pcall(love.graphics.newCanvas, w, h, { dpiscale = 1 }) if not ok then return false end c:setFilter("nearest", "nearest") if slotHeld then releaseSlot(slotHeld) end @@ -876,7 +884,9 @@ end function Voxel3D.beginWater(paint) if not (active and canvas and held and held.depth) then return nil end if not held.mirror then - local ok, c = pcall(love.graphics.newCanvas, held.w, held.h) + -- dpiscale 1, like the scene canvas it mirrors (see newDepth) + local ok, c = pcall(love.graphics.newCanvas, held.w, held.h, + { dpiscale = 1 }) if not (ok and c) then return nil end pcall(c.setFilter, c, "nearest", "nearest") pcall(c.setWrap, c, "clamp", "clamp") From 07b35b2bf51042710ecf9fa3a8d456fd448c8e68 Mon Sep 17 00:00:00 2001 From: Myles Resnick Date: Sat, 1 Aug 2026 17:37:42 -0400 Subject: [PATCH 2/2] AntiAlias fold target too: the ninth allocation The 1.4.1 AA pass folds its supersampled canvas down into a target sized in display pixels, allocated on the same highdpi default as the rest -- so on a phone the fold's destination was itself density x too big per axis, and the supersample multiplied on top of that. Same one-line rule as the other eight sites. Co-Authored-By: Claude Fable 5 --- lib/AntiAlias.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/AntiAlias.lua b/lib/AntiAlias.lua index 30f8761..f10c376 100644 --- a/lib/AntiAlias.lua +++ b/lib/AntiAlias.lua @@ -133,7 +133,10 @@ local targets = {} local function targetFor(slot, w, h) local t = targets[slot] if not (t and t.w == w and t.h == h) then - local ok, c = pcall(love.graphics.newCanvas, w, h) + -- dpiscale 1: `w`/`h` are the pass's DISPLAY size in pixels, and the + -- highdpi default would fold the supersample down into a target that + -- is itself density x too big (see Voxel3D.newDepth for the trap) + local ok, c = pcall(love.graphics.newCanvas, w, h, { dpiscale = 1 }) if not (ok and c) then return nil end -- nearest, like the canvas it stands in for: this one is composited a -- canvas pixel to a display pixel, and the smoothing has already happened