From c63b542ef32b92b89a8d7a8e7399d0ea2e4ac402 Mon Sep 17 00:00:00 2001 From: hernan0078 Date: Fri, 31 Jul 2026 15:47:32 -0400 Subject: [PATCH] Pin dpiscale on the mod's own render targets sceneSize() asks for love.graphics.getPixelDimensions(), which is right -- the engine composites a pipeline's canvas with draw(canvas, 0, 0, 0, 1/dpiX, 1/dpiY), so the canvas has to REPORT the pixel size for that blit to cover the window. But love.graphics.newCanvas defaults `dpiscale` to getDPIScale(), so the texture it allocates is that size times the DPI scale again. On an iPad reporting 2, asking for the window's 2752x2064 pixels allocated 5504x4128 -- past the driver's maximum texture size: Cannot create texture: pixel width of 5504 is too large for this system newCanvas threw, beginScene returned false, drawWorld returned nil -- and returning nil IS the 2D fallback, so the engine quietly kept drawing the flat world. Every switch said the mode was on (the VOXEL row, the mod manager, the hotkey) and nothing ever appeared, with no error anywhere the player could see. Desktop never sees it: dpiscale is 1 there. Every render target in this mod is already counted in framebuffer pixels -- the scene canvas is getPixelDimensions(), the shadow map is a resolution rung whose texel count sunTexel is derived from, the blur pair is the size of the image it was handed -- so all three go through a small lib/PixelCanvas.lua that pins dpiscale = 1. getWidth()/getHeight() always reported the REQUESTED size, so nothing downstream moves: no geometry, no UV, no compositing scale. It also squares the FX overlay, whose projected coordinates are already in canvas pixels and were being scaled by the DPI factor a second time. Verified on an iPad Pro 13-inch (M5) simulator running iPadOS 27 at DPI scale 2, where the diorama had been silently falling back to the flat world: terrain, cast shadows, depth occlusion and the tilt-shift pass all render, across the 15/35/50/75 rungs and FULL. The mod's own suite is unchanged -- same single pre-existing failure before and after. --- lib/PixelCanvas.lua | 40 ++++++++++++++++++++++++++++++++++++++++ lib/ShadowMap.lua | 4 +++- lib/TiltShift.lua | 10 ++++++++-- lib/Voxel3D.lua | 4 +++- 4 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 lib/PixelCanvas.lua diff --git a/lib/PixelCanvas.lua b/lib/PixelCanvas.lua new file mode 100644 index 0000000..d3f960d --- /dev/null +++ b/lib/PixelCanvas.lua @@ -0,0 +1,40 @@ +-- Render targets measured in real framebuffer PIXELS. +-- +-- love.graphics.newCanvas defaults its `dpiscale` to +-- love.graphics.getDPIScale(), so on a highdpi surface the texture it +-- allocates is NOT the size it was asked for -- newCanvas(2752, 2064) on a +-- panel reporting a DPI scale of 2 asks the driver for 5504x4128. +-- +-- Every target in this mod is already counted in framebuffer pixels: the +-- free-roam scene canvas is love.graphics.getPixelDimensions() (see +-- sceneSize in main.lua, and the 1/dpi blit the engine composites it with), +-- the shadow map is a resolution rung, the blur pairs are the size of the +-- image they were handed. Letting LOVE scale them a second time is the DPI +-- factor applied twice. +-- +-- On an iPad at DPI scale 2 that took the scene canvas past the driver's +-- maximum texture size. newCanvas threw, beginScene returned false, +-- drawWorld returned nil -- and returning nil IS the 2D fallback, so the +-- engine quietly kept drawing the flat world. Every switch said the mode was +-- on (the OPTIONS row, the mod manager, the hotkey) and nothing ever +-- appeared. Desktop never saw it: dpiscale is 1 there. +-- +-- getWidth()/getHeight() always report the REQUESTED size, so pinning the +-- scale changes only the resolution of the target -- no geometry, no UV, no +-- compositing scale anywhere moves, and the FX overlay's projected pixel +-- coordinates finally land on the pixels they name. The engine's own render +-- targets do this for the same reason (src/render/PixelCanvas.lua). + +-- the mod namespace (see main.lua) +local V = ... + +local PixelCanvas = {} + +-- One framebuffer pixel per w/h, always. Returns ok, canvas -- the shape the +-- pcall-guarded call sites already branch on, so a driver that refuses the +-- allocation still degrades instead of erroring. +function PixelCanvas.new(w, h) + return pcall(love.graphics.newCanvas, w, h, { dpiscale = 1 }) +end + +return PixelCanvas diff --git a/lib/ShadowMap.lua b/lib/ShadowMap.lua index 9798462..e3dd332 100644 --- a/lib/ShadowMap.lua +++ b/lib/ShadowMap.lua @@ -180,7 +180,9 @@ 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) + -- pixel-exact: `res` IS the texel count the filter's sunTexel is derived + -- from, so the texture must be exactly that big (see lib/PixelCanvas.lua) + local ok, c = V.require("PixelCanvas").new(res, res) if not (ok and c) then canvas = false return nil diff --git a/lib/TiltShift.lua b/lib/TiltShift.lua index d95c10e..a9cc66b 100644 --- a/lib/TiltShift.lua +++ b/lib/TiltShift.lua @@ -25,6 +25,9 @@ -- failure -- headless, no shader support) apply() hands the canvas back -- untouched, so every other path is byte-for-byte what it always was. +-- the mod namespace (see main.lua): V.require loads a sibling module +local V = ... + local TiltShift = {} TiltShift.level = 0 @@ -85,9 +88,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) + -- pixel-exact: w and h come from the scene canvas this pass was handed, + -- which is already counted in framebuffer pixels (see lib/PixelCanvas.lua) + local PixelCanvas = V.require("PixelCanvas") + local ok, a = PixelCanvas.new(w, h) if not ok then return nil end - local okB, b = pcall(love.graphics.newCanvas, w, h) + local okB, b = PixelCanvas.new(w, h) 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 62e98be..31e732f 100644 --- a/lib/Voxel3D.lua +++ b/lib/Voxel3D.lua @@ -33,6 +33,7 @@ local WorldCurve = V.require("WorldCurve") local Sky = V.require("Sky") local DayNight = V.require("DayNight") local GlassMask = V.require("GlassMask") +local PixelCanvas = V.require("PixelCanvas") local Voxel3D = {} @@ -540,7 +541,8 @@ function Voxel3D.beginScene(w, h, cx, cy, vw, vh, sky, slot) local name = slot or "world" local held = slots[name] if not (held and held.w == w and held.h == h) then - local ok, c = pcall(love.graphics.newCanvas, w, h) + -- pixel-exact: w and h are framebuffer pixels already (see PixelCanvas) + local ok, c = PixelCanvas.new(w, h) if not ok then return false end c:setFilter("nearest", "nearest") if held and held.canvas and held.canvas.release then