Fix LÖVE 12 scissor seam

This commit is contained in:
luisgonzaleznf
2026-08-04 14:19:43 +02:00
committed by Adrian Castro
parent e61f231316
commit 84d09d0894
2 changed files with 49 additions and 20 deletions
+21 -12
View File
@@ -564,16 +564,24 @@ function Renderer:drawTiltedWorld(zoneList, sx, sy, wox, woy, target)
return true return true
end end
-- LÖVE 11 truncates scissor arguments to framebuffer pixels; the half-pixel
-- bias keeps values divided back through a fractional DPI scale from landing
-- one short. LÖVE 12 passes fractional arguments through and rounds in the
-- graphics backend instead, where that bias shifts each origin by one pixel
-- and extends its far edge by two (#673).
local SCISSOR_PIXEL_BIAS = 0.5
if love and love.getVersion and select(1, love.getVersion()) >= 12 then
SCISSOR_PIXEL_BIAS = 0
end
-- Clamp a scissor rect to the viewport box, then round it outward to whole -- Clamp a scissor rect to the viewport box, then round it outward to whole
-- framebuffer pixels. love.graphics.setScissor truncates x, y, w and h to -- framebuffer pixels. On LÖVE 11, x, y, w and h are truncated independently,
-- pixels independently, so a rect with fractional unit edges (Android's -- so a rect with fractional unit edges (Android's non-integer DPI puts
-- non-integer DPI puts fitScale/dpi in Sx/Sy) loses up to a pixel per side -- fitScale/dpi in Sx/Sy) loses up to a pixel per side and two adjacent SGB
-- and two adjacent SGB zones stop sharing an edge: the letterbox clear shows -- zones stop sharing an edge: the letterbox clear shows through as a seam at
-- through as a horizontal seam at every zone boundary (#373). Rounding -- every zone boundary (#373). Rounding outward makes neighbours overlap by
-- outward makes neighbours overlap by at most one row instead -- the overlap -- at most one row instead; SCISSOR_PIXEL_BIAS preserves that result across the
-- redraws the same canvas pixels one palette later, and past the canvas edge -- LÖVE 11 and 12 conversion rules.
-- there is nothing to draw. The half pixel keeps LOVE's truncation on the
-- snapped edge rather than one short of it.
local function scissorClamped(x, y, w, h, ox, oy, vpw, vph, dpiX, dpiY) local function scissorClamped(x, y, w, h, ox, oy, vpw, vph, dpiX, dpiY)
local x2, y2 = math.min(x + w, ox + vpw), math.min(y + h, oy + vph) local x2, y2 = math.min(x + w, ox + vpw), math.min(y + h, oy + vph)
x, y = math.max(x, ox), math.max(y, oy) x, y = math.max(x, ox), math.max(y, oy)
@@ -581,9 +589,10 @@ local function scissorClamped(x, y, w, h, ox, oy, vpw, vph, dpiX, dpiY)
dpiX, dpiY = dpiX or 1, dpiY or 1 dpiX, dpiY = dpiX or 1, dpiY or 1
local px1, py1 = math.floor(x * dpiX), math.floor(y * dpiY) local px1, py1 = math.floor(x * dpiX), math.floor(y * dpiY)
local px2, py2 = math.ceil(x2 * dpiX), math.ceil(y2 * dpiY) local px2, py2 = math.ceil(x2 * dpiX), math.ceil(y2 * dpiY)
love.graphics.setScissor((px1 + 0.5) / dpiX, (py1 + 0.5) / dpiY, local b = SCISSOR_PIXEL_BIAS
(px2 - px1 + 0.5) / dpiX, love.graphics.setScissor((px1 + b) / dpiX, (py1 + b) / dpiY,
(py2 - py1 + 0.5) / dpiY) (px2 - px1 + b) / dpiX,
(py2 - py1 + b) / dpiY)
return true return true
end end
+28 -8
View File
@@ -15,21 +15,41 @@ local check, eq, same = T.check, T.eq, T.same
-- endFrame's blit closure, which needs canvases and a compiled shader. The -- endFrame's blit closure, which needs canvases and a compiled shader. The
-- source is loaded directly so the rect arithmetic can be exercised with no -- source is loaded directly so the rect arithmetic can be exercised with no
-- GPU, the same way parity_picker_pointer_grab reads RomImporter (#254). -- GPU, the same way parity_picker_pointer_grab reads RomImporter (#254).
local scissorClamped, captured local captured
do local function loadScissor(loveMajor)
local f = io.open("src/render/Renderer.lua", "rb") local f = io.open("src/render/Renderer.lua", "rb")
check(f ~= nil, "Renderer source is readable") check(f ~= nil, "Renderer source is readable")
local src = f and f:read("*a") or "" local src = f and f:read("*a") or ""
if f then f:close() end if f then f:close() end
local bias = src:match("\nlocal SCISSOR_PIXEL_BIAS = 0%.5.-\nend\n")
check(bias ~= nil, "scissor bias is still version-gated")
local body = src:match("\nlocal function scissorClamped.-\nend\n") local body = src:match("\nlocal function scissorClamped.-\nend\n")
check(body ~= nil, "scissorClamped is still a single local function") check(body ~= nil, "scissorClamped is still a single local function")
local fakeLove = { graphics = { setScissor = function(x, y, w, h) local fakeLove = {
captured = { x = x, y = y, w = w, h = h } getVersion = function() return loveMajor, 0, 0 end,
end } } graphics = { setScissor = function(x, y, w, h)
local chunk = assert(loadstring("local love = ...\n" .. (body or "") captured = { x = x, y = y, w = w, h = h }
end },
}
local chunk = assert(loadstring("local love = ...\n" .. (bias or "")
.. (body or "")
.. "\nreturn scissorClamped")) .. "\nreturn scissorClamped"))
scissorClamped = chunk(fakeLove) local scissor = chunk(fakeLove)
check(type(scissorClamped) == "function", "scissorClamped loads standalone") check(type(scissor) == "function", "scissorClamped loads standalone")
return scissor
end
local scissorClamped = loadScissor(11)
-- LÖVE 12 changed setScissor from truncating Lua integers to accepting floats
-- and rounding in the backend. Its arguments must therefore describe the
-- already-snapped rectangle exactly, without LÖVE 11's half-pixel nudge.
do
local scissor12 = loadScissor(12)
captured = nil
check(scissor12(50, 60, 10, 20, 0, 0, 100, 100, 2, 2),
"LÖVE 12 integer test rect draws")
same(captured, { x = 50, y = 60, w = 10, h = 20 },
"LÖVE 12 receives an unbiased snapped scissor")
end end
-- The title's three SGB zones in canvas pixels (PaletteFX.zone turns the -- The title's three SGB zones in canvas pixels (PaletteFX.zone turns the