mods: add OS-independent game viewport composition

This commit is contained in:
AverageConsumer
2026-08-16 15:17:35 +02:00
parent 1151c188a7
commit 1f3d13adaf
14 changed files with 379 additions and 46 deletions
+7 -1
View File
@@ -6,6 +6,7 @@ local FixedStep = require("src.core.FixedStep")
local Input = require("src.core.Input")
local Logger = require("src.core.Logger")
local Renderer = require("src.render.Renderer")
local GameViewport = require("src.render.GameViewport")
local SaveData = require("src.core.SaveData")
local StateStack = require("src.core.StateStack")
local TouchControls = require("src.core.TouchControls")
@@ -452,6 +453,7 @@ local function centerClassicZones(zones, offset)
end
function Game:draw()
GameViewport.begin(1)
-- the UI canvas clears transparent when the overworld's world pass
-- shows through beneath it; opaque full-screen states get the classic
-- white clear
@@ -563,7 +565,9 @@ function Game:draw()
if ModRuntime.wantsHook("render.hud") then
ModRuntime.call("render.hud", function() end, self, viewport)
end
-- on-screen mobile controls: pure screen-space, over the finished frame
GameViewport.finish(self)
-- OS-window chrome: keep the pad full-size and above any composed companion
-- view instead of capturing and shrinking it with the game viewport.
TouchControls:draw()
end
@@ -939,8 +943,10 @@ local function pointerUnclaimed() return false end
-- coordinates are LOVE window units, the same space render.hud's viewport
-- and the touch overlay lay out in
function Game:pointerEvent(phase, source, id, x, y, dx, dy, pressure, button)
local gameX, gameY, insideGame = GameViewport.toLocal(x, y)
return ModRuntime.call("input.pointer", pointerUnclaimed, self, {
phase = phase, source = source, id = id, x = x, y = y,
gameX = gameX, gameY = gameY, insideGame = insideGame,
dx = dx or 0, dy = dy or 0, pressure = pressure, button = button,
})
end
+24 -18
View File
@@ -35,6 +35,7 @@ local World = require("src.world.gen2.World")
-- The mod event/hook buses. Gold reaches them through Runtime like every
-- other engine file, so a call site here is the same call site Gen 1 has.
local ModRuntime = require("src.mods.Runtime")
local GameViewport = require("src.render.GameViewport")
-- Only for the mod-supplied save migrations and the mods-changed report, which
-- are keyed off save.meta and know nothing about a generation; Gold's own save
-- IO is src/core/gen2/Save.lua.
@@ -73,8 +74,8 @@ end
--
-- Gold composites its own frame (Game2:draw / drawScene) and pumps its own pad
-- (the FixedStep callback in Game2:load), so none of it goes through
-- src/render/Renderer.lua or src/core/Game.lua. That explains why the eight
-- hooks below never used to fire here; it is not a reason they should not. A
-- src/render/Renderer.lua or src/core/Game.lua. That explains why the hooks
-- below never used to fire here; it is not a reason they should not. A
-- hook is a contract about a MOMENT in the frame, and Gold has every one of
-- these moments -- so each is raised under the Gen 1 NAME with the Gen 1
-- PAYLOAD, at the Gen 1 point in the order:
@@ -86,6 +87,8 @@ end
-- render.output* the normal composed frame (Renderer.lua:1063)
-- render.letterbox the void around the 160x144 blit (Renderer.lua:840)
-- render.hud screen-space UI over the frame (src/core/Game.lua:521)
-- render.viewport the game's OS-window rectangle (GameViewport.lua:52)
-- render.window final OS-window composition (GameViewport.lua:145)
--
-- Where Gold genuinely cannot tell two Gen 1 things apart -- it composites the
-- world pass and the UI into ONE canvas, not two -- the call site says so and
@@ -1197,9 +1200,7 @@ function Game2:frameFit(w, h)
dpi = tonumber(love.window.getDPIScale()) or 1
end
local pw, ph = w * dpi, h * dpi
if love.graphics.getPixelDimensions then
pw, ph = love.graphics.getPixelDimensions()
end
pw, ph = GameViewport.pixelDimensions()
return scale, ox, oy, dpi, pw, ph
end
@@ -1217,18 +1218,15 @@ function Game2:viewport(w, h)
}
end
-- The screen-space layer, in the Gen 1 order: render.hud and then the
-- on-screen pad (src/core/Game.lua:521 and :524, either side of
-- Renderer:endFrame). Both are window-space, both sit over the finished
-- frame -- post passes, letterbox and all -- and neither ever enters the game
-- canvas. Every exit path of Game2:draw ends here, which is what makes that
-- true of the composed frame a mod owns as well as of the plain one.
-- The render.hud layer, in Gen 1's order over the finished game frame. The
-- on-screen pad is drawn separately after GameViewport.finish, because it is
-- OS-window chrome and must not be captured or scaled with this canvas.
--
-- render.hud: persistent tool status. The call is fenced with
-- push("all")/pop for the reason src/render/Pipelines.lua:guardRender fences a
-- mod render callback: a subscriber that returns cleanly but leaves a shader
-- bound, the canvas redirected or the colour changed must not corrupt the next
-- frame -- or, now, the pad drawn immediately after it.
-- frame.
function Game2:drawHud(w, h)
if ModRuntime.wantsHook("render.hud") then
local G = love.graphics
@@ -1236,10 +1234,6 @@ function Game2:drawHud(w, h)
ModRuntime.call("render.hud", noop, self, self:viewport(w, h))
G.pop()
end
-- The pad LAST, so a HUD mod cannot draw over the controls the player is
-- pressing. It draws nothing at all off Android/iOS unless POKEPORT_TOUCH=1
-- forces it, and nothing ever while a controller is in use.
TouchControls:draw()
end
-- render.letterbox: SGB borders and custom void art in the bars around the
@@ -1363,9 +1357,9 @@ end
-- is being shown on. Mod post-processes fold in between the two, where
-- Renderer.lua:1058 folds them -- a blur or a colour grade is what the LCD grid
-- is then drawn over, rather than something that smears the grid itself.
function Game2:draw()
function Game2:drawViewportFrame()
local G = love.graphics
local w, h = G.getDimensions()
local w, h = GameViewport.dimensions()
local GBCFX = require("src.render.GBCFX")
local GbcPalette = require("src.render.GbcPalette")
local Pipelines = require("src.render.Pipelines")
@@ -1477,6 +1471,16 @@ function Game2:draw()
self:drawHud(w, h)
end
function Game2:draw()
GameViewport.begin(2)
GameViewport.setTarget()
self:drawViewportFrame()
GameViewport.finish(self)
-- OS-window chrome: draw after companion composition so viewport layouts
-- neither shrink nor cover the touch pad.
TouchControls:draw()
end
-- The paper a pushed TextBox has to sit on. A textbox is built entirely from
-- font-page tiles ($79-$7e frame, ' ' $7f interior), so it takes BG palette 0
-- colour 0 from the screen UNDER it (pokegold engine/pokegear/pokegear.asm
@@ -1803,8 +1807,10 @@ end
-- coordinates are LOVE window units, the same space render.hud's viewport is in
function Game2:pointerEvent(phase, source, id, x, y, dx, dy, pressure, button)
local gameX, gameY, insideGame = GameViewport.toLocal(x, y)
return ModRuntime.call("input.pointer", pointerUnclaimed, self, {
phase = phase, source = source, id = id, x = x, y = y,
gameX = gameX, gameY = gameY, insideGame = insideGame,
dx = dx or 0, dy = dy or 0, pressure = pressure, button = button,
})
end
+8 -2
View File
@@ -7,12 +7,14 @@
-- (touch overlay, launcher) should prefer this over getDimensions; the game
-- canvas may still letterbox into the full framebuffer for immersion.
local GameViewport = require("src.render.GameViewport")
local SafeArea = {}
function SafeArea.rect()
function SafeArea.windowRect()
local ww, wh = 0, 0
if love and love.graphics and love.graphics.getDimensions then
ww, wh = love.graphics.getDimensions()
ww, wh = GameViewport.fullDimensions()
end
if ww <= 0 then ww = 1 end
if wh <= 0 then wh = 1 end
@@ -55,4 +57,8 @@ function SafeArea.rect()
return x, y, w, h
end
function SafeArea.rect()
return GameViewport.localSafeRect(SafeArea.windowRect())
end
return SafeArea
+7 -7
View File
@@ -339,7 +339,7 @@ end
-- demand. Mirrors it into self.orientation / self.positions / self.scale,
-- which layout(), the editor chrome and the tests read.
function TouchControls:currentBucket()
local _, _, sw, sh = SafeArea.rect()
local _, _, sw, sh = SafeArea.windowRect()
local o = orientationFor(sw, sh)
self.layouts = self.layouts or { portrait = {}, landscape = {} }
local b = self.layouts[o]
@@ -363,7 +363,7 @@ end
-- while sizes stay derived from the short edge, times the orientation's
-- size setting (#633).
function TouchControls:layout()
local ox, oy, sw, sh = SafeArea.rect()
local ox, oy, sw, sh = SafeArea.windowRect()
if self.layoutW == sw and self.layoutH == sh
and self.layoutOx == ox and self.layoutOy == oy and self.L then
return self.L
@@ -397,7 +397,7 @@ end
-- Move one control to a screen-space point and persist its normalized
-- position within the safe rect. Used by the layout editor while dragging.
function TouchControls:setControlCenter(name, cx, cy)
local ox, oy, sw, sh = SafeArea.rect()
local ox, oy, sw, sh = SafeArea.windowRect()
local L = self:layout()
local zone = L[name]
if not zone then return end
@@ -608,10 +608,10 @@ local function drawIcon(img, zone, pressed, alphaMul)
zone.cy - img:getHeight() * scale / 2, 0, scale, scale)
end
-- Screen-space, called by Game:draw after Renderer:endFrame -- and by
-- Game2:drawHud after Gold's own present pass -- so the overlay rides on top
-- of everything (world, UI, CRT/GBC FX included). Also used by the launcher
-- layout editor under preview mode.
-- OS-window space, called after GameViewport.finish so the overlay rides on
-- top of the game, companion composition and post-processing without being
-- captured or scaled with any game viewport. Also used by the launcher layout
-- editor under preview mode.
function TouchControls:draw()
if not self:visible() then return end
local L = self:layout()