mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 19:24:01 +02:00
Merge pull request #1405 from AverageConsumer/codex/mod-render-viewport
This commit is contained in:
@@ -554,7 +554,7 @@ gains a field instead of the name gaining a prefix.
|
||||
passes `game`; positions 2-4 (mon, row, trigger) match.
|
||||
- *The frame (`src/core/Game2.lua`):* hooks `input.step`, `input.pointer`,
|
||||
`render.zones`, `render.compose`, `render.output_enabled`, `render.output`,
|
||||
`render.letterbox`, `render.hud`. Each sits
|
||||
`render.letterbox`, `render.hud`, `render.viewport`, `render.window`. Each sits
|
||||
at the same moment `src/core/Game.lua` and `src/render/Renderer.lua` raise it
|
||||
-- the logic tick before the pad is read, a pointer the touch overlay gets
|
||||
first refusal on, the palette zone list handed to the present pass, the
|
||||
|
||||
+22
-3
@@ -654,11 +654,14 @@ the wrapper is visible during that same fixed step. The callback receives
|
||||
|
||||
`input.pointer` delivers uncaptured gameplay pointer events -- touches and
|
||||
real mouse input alike. The callback receives `(next, game, ev)` where `ev`
|
||||
is `{ phase, source, id, x, y, dx, dy, pressure, button }`: `phase` is
|
||||
is `{ phase, source, id, x, y, gameX, gameY, insideGame, dx, dy, pressure,
|
||||
button }`: `phase` is
|
||||
`"pressed"`, `"moved"`, `"released"` or `"cancelled"`; `source` is `"touch"`
|
||||
or `"mouse"`; `id` is the LÖVE touch id or `"mouse"`; and the coordinates
|
||||
are LOVE window units, the same space `render.hud`'s viewport and the touch
|
||||
overlay lay out in. The on-screen touch controls keep first refusal: a
|
||||
`x` / `y` are LOVE window units, while `gameX` / `gameY` are local to the
|
||||
active game viewport and `insideGame` says whether the pointer is inside it.
|
||||
Without a custom viewport both coordinate pairs are identical. The on-screen
|
||||
touch controls keep first refusal: a
|
||||
pointer that begins on a virtual control belongs to the pad for its whole
|
||||
lifecycle and never reaches the hook, while one that begins outside stays
|
||||
visible even if it later crosses a control. A real mouse reaches the hook
|
||||
@@ -691,6 +694,22 @@ composited and before touch controls draw. The window-space viewport contains
|
||||
and `dpiY`, so a tool can use the letterbox margins without drawing over the
|
||||
playfield or pushing an updating game state.
|
||||
|
||||
`render.viewport` lets a layout mod reserve the window-space rectangle in which
|
||||
the game renders. It receives `(next, ctx)` with the full window's `width`,
|
||||
`height`, `pixelWidth`, `pixelHeight`, `dpiX`, `dpiY`, and `generation`, and
|
||||
returns `{ x, y, width, height }`. The engine clamps that rectangle to the
|
||||
window and makes game layout, safe-area calculations, and rendering use it as
|
||||
their display. Set `capture = true` to request a composition canvas even when
|
||||
the rectangle fills the window. With no subscriber, no canvas is allocated and
|
||||
the normal presentation path is unchanged.
|
||||
|
||||
When a viewport is active, `render.window` receives `(next, game, ctx)` after
|
||||
the game frame has been captured. `ctx` contains its `canvas`, `x`, `y`,
|
||||
`width`, `height`, the full `windowWidth` / `windowHeight`, `dpiX`, `dpiY`, and
|
||||
`generation`. Calling `next(game, ctx)` draws the game at the requested origin;
|
||||
a wrapper may instead compose that canvas with its own UI. Touch controls remain
|
||||
full-size OS-window chrome and draw after this hook.
|
||||
|
||||
`render.compose` wraps the whole-window composite in `Renderer:endFrame`. It
|
||||
receives `(next, renderer, ctx)`; returning `true` without calling `next` hands
|
||||
the mod full control of the window, while calling `next` runs the engine's
|
||||
|
||||
@@ -15,6 +15,7 @@ local LaunchOptions = require("src.core.LaunchOptions")
|
||||
local NxDisplay = require("src.core.NxDisplay")
|
||||
local PlatformHooks = require("src.core.PlatformHooks")
|
||||
local HostDisplay = require("src.core.HostDisplay")
|
||||
local GameViewport = require("src.render.GameViewport")
|
||||
|
||||
-- Lua errors: persist a redacted trace in the save dir and surface a hint.
|
||||
do
|
||||
@@ -503,24 +504,30 @@ end
|
||||
|
||||
function love.draw()
|
||||
if editorMode then
|
||||
GameViewport.reset()
|
||||
HostDisplay.beginFrame("editor", EditorApp)
|
||||
local result = EditorApp.draw()
|
||||
HostDisplay.endFrame("editor", EditorApp)
|
||||
return result
|
||||
end
|
||||
if TouchEditor then
|
||||
GameViewport.reset()
|
||||
HostDisplay.beginFrame("touch_editor", TouchEditor)
|
||||
local result = TouchEditor.draw()
|
||||
HostDisplay.endFrame("touch_editor", TouchEditor)
|
||||
return result
|
||||
end
|
||||
if Importer then
|
||||
GameViewport.reset()
|
||||
HostDisplay.beginFrame("launcher", Importer)
|
||||
local result = Importer:draw()
|
||||
HostDisplay.endFrame("launcher", Importer)
|
||||
return result
|
||||
end
|
||||
if not Game then return end
|
||||
if not Game then
|
||||
GameViewport.reset()
|
||||
return
|
||||
end
|
||||
|
||||
HostDisplay.beginFrame("game", Game)
|
||||
Game:draw()
|
||||
|
||||
+7
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
-- Optional game viewport inside the OS window. A layout mod may reserve any
|
||||
-- window-space rectangle through render.viewport; the game then renders as if
|
||||
-- that rectangle were its whole display. With no subscriber this module is a
|
||||
-- pass-through and allocates no canvas.
|
||||
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
|
||||
local Viewport = {
|
||||
rect = nil,
|
||||
full = nil,
|
||||
canvas = nil,
|
||||
generation = nil,
|
||||
frameActive = false,
|
||||
}
|
||||
|
||||
local function finite(value)
|
||||
return type(value) == "number" and value == value
|
||||
and value > -math.huge and value < math.huge
|
||||
end
|
||||
|
||||
local function realMetrics()
|
||||
local G = love.graphics
|
||||
local w, h = G.getDimensions()
|
||||
local pw, ph = w, h
|
||||
if G.getPixelDimensions then pw, ph = G.getPixelDimensions() end
|
||||
local dpiX = w > 0 and pw / w or 1
|
||||
local dpiY = h > 0 and ph / h or 1
|
||||
if dpiX < 1e-6 then dpiX = 1 end
|
||||
if dpiY < 1e-6 then dpiY = 1 end
|
||||
return math.max(1, w), math.max(1, h),
|
||||
math.max(1, pw), math.max(1, ph), dpiX, dpiY
|
||||
end
|
||||
|
||||
local function clampRect(value, w, h)
|
||||
if type(value) ~= "table" then
|
||||
return { x = 0, y = 0, width = w, height = h }
|
||||
end
|
||||
local x = finite(value.x) and math.floor(value.x) or 0
|
||||
local y = finite(value.y) and math.floor(value.y) or 0
|
||||
local rw = finite(value.width) and math.floor(value.width) or w
|
||||
local rh = finite(value.height) and math.floor(value.height) or h
|
||||
x = math.max(0, math.min(x, w - 1))
|
||||
y = math.max(0, math.min(y, h - 1))
|
||||
rw = math.max(1, math.min(rw, w - x))
|
||||
rh = math.max(1, math.min(rh, h - y))
|
||||
return { x = x, y = y, width = rw, height = rh }
|
||||
end
|
||||
|
||||
local function sameSize(canvas, w, h)
|
||||
return canvas and canvas:getWidth() == w and canvas:getHeight() == h
|
||||
end
|
||||
|
||||
function Viewport.begin(generation)
|
||||
local w, h, pw, ph, dpiX, dpiY = realMetrics()
|
||||
local context = {
|
||||
width = w, height = h, pixelWidth = pw, pixelHeight = ph,
|
||||
dpiX = dpiX, dpiY = dpiY, generation = generation,
|
||||
}
|
||||
local requested
|
||||
if Runtime.wantsHook("render.viewport") then
|
||||
requested = Runtime.call("render.viewport", function(ctx)
|
||||
return { x = 0, y = 0, width = ctx.width, height = ctx.height }
|
||||
end, context)
|
||||
end
|
||||
local rect = clampRect(requested, w, h)
|
||||
Viewport.full = context
|
||||
Viewport.rect = rect
|
||||
Viewport.generation = generation
|
||||
local active = type(requested) == "table" and requested.capture == true
|
||||
or rect.x ~= 0 or rect.y ~= 0
|
||||
or rect.width ~= w or rect.height ~= h
|
||||
Viewport.frameActive = active
|
||||
if active then
|
||||
if not sameSize(Viewport.canvas, rect.width, rect.height) then
|
||||
if Viewport.canvas and Viewport.canvas.release then
|
||||
Viewport.canvas:release()
|
||||
end
|
||||
Viewport.canvas = love.graphics.newCanvas(rect.width, rect.height)
|
||||
Viewport.canvas:setFilter("nearest", "nearest")
|
||||
end
|
||||
else
|
||||
if Viewport.canvas and Viewport.canvas.release then
|
||||
Viewport.canvas:release()
|
||||
end
|
||||
Viewport.canvas = nil
|
||||
end
|
||||
return rect
|
||||
end
|
||||
|
||||
function Viewport.active()
|
||||
return Viewport.frameActive == true and Viewport.canvas ~= nil
|
||||
end
|
||||
|
||||
function Viewport.dimensions()
|
||||
if Viewport.active() then
|
||||
return Viewport.rect.width, Viewport.rect.height
|
||||
end
|
||||
return love.graphics.getDimensions()
|
||||
end
|
||||
|
||||
function Viewport.pixelDimensions()
|
||||
if Viewport.active() then
|
||||
if Viewport.canvas.getPixelDimensions then
|
||||
local w, h = Viewport.canvas:getPixelDimensions()
|
||||
return math.max(1, w), math.max(1, h)
|
||||
end
|
||||
return math.max(1, math.floor(Viewport.rect.width * Viewport.full.dpiX)),
|
||||
math.max(1, math.floor(Viewport.rect.height * Viewport.full.dpiY))
|
||||
end
|
||||
if love.graphics.getPixelDimensions then
|
||||
return love.graphics.getPixelDimensions()
|
||||
end
|
||||
return love.graphics.getDimensions()
|
||||
end
|
||||
|
||||
function Viewport.fullDimensions()
|
||||
if Viewport.full then return Viewport.full.width, Viewport.full.height end
|
||||
return love.graphics.getDimensions()
|
||||
end
|
||||
|
||||
function Viewport.target()
|
||||
return Viewport.canvas
|
||||
end
|
||||
|
||||
function Viewport.setTarget()
|
||||
love.graphics.setCanvas(Viewport.canvas)
|
||||
end
|
||||
|
||||
function Viewport.toLocal(x, y)
|
||||
local rect = Viewport.rect
|
||||
if not rect then return x, y, true end
|
||||
local lx, ly = x - rect.x, y - rect.y
|
||||
return lx, ly,
|
||||
lx >= 0 and ly >= 0 and lx < rect.width and ly < rect.height
|
||||
end
|
||||
|
||||
function Viewport.localSafeRect(x, y, w, h)
|
||||
local rect = Viewport.rect
|
||||
if not Viewport.active() or not rect then return x, y, w, h end
|
||||
local x1, y1 = math.max(x, rect.x), math.max(y, rect.y)
|
||||
local x2 = math.min(x + w, rect.x + rect.width)
|
||||
local y2 = math.min(y + h, rect.y + rect.height)
|
||||
if x2 <= x1 or y2 <= y1 then
|
||||
return 0, 0, rect.width, rect.height
|
||||
end
|
||||
return x1 - rect.x, y1 - rect.y, x2 - x1, y2 - y1
|
||||
end
|
||||
|
||||
function Viewport.finish(game)
|
||||
if not Viewport.active() then return end
|
||||
local G = love.graphics
|
||||
local rect, full = Viewport.rect, Viewport.full
|
||||
G.setCanvas()
|
||||
G.push("all")
|
||||
G.origin()
|
||||
G.setScissor()
|
||||
G.setShader()
|
||||
G.setBlendMode("alpha")
|
||||
G.clear(0, 0, 0, 1)
|
||||
local context = {
|
||||
canvas = Viewport.canvas,
|
||||
x = rect.x, y = rect.y, width = rect.width, height = rect.height,
|
||||
windowWidth = full.width, windowHeight = full.height,
|
||||
dpiX = full.dpiX, dpiY = full.dpiY,
|
||||
generation = Viewport.generation,
|
||||
}
|
||||
Runtime.call("render.window", function(_, ctx)
|
||||
G.setColor(1, 1, 1, 1)
|
||||
G.draw(ctx.canvas, ctx.x, ctx.y)
|
||||
end, game, context)
|
||||
G.pop()
|
||||
end
|
||||
|
||||
function Viewport.reset()
|
||||
Viewport.frameActive = false
|
||||
Viewport.rect = nil
|
||||
Viewport.full = nil
|
||||
Viewport.generation = nil
|
||||
if Viewport.canvas and Viewport.canvas.release then
|
||||
Viewport.canvas:release()
|
||||
end
|
||||
Viewport.canvas = nil
|
||||
end
|
||||
|
||||
return Viewport
|
||||
@@ -12,6 +12,7 @@ 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 GameViewport = require("src.render.GameViewport")
|
||||
-- leaf module (no renderer dependency), so requiring it here cannot cycle
|
||||
local FaithfulRes = require("src.core.FaithfulRes")
|
||||
|
||||
@@ -69,11 +70,9 @@ Renderer.UPRIGHT_MARGIN = 160
|
||||
-- Keep separate dpiX/dpiY so each GB pixel covers fitScale() physical pixels
|
||||
-- on BOTH axes (square).
|
||||
local function displayMetrics()
|
||||
local ww, wh = love.graphics.getDimensions()
|
||||
local ww, wh = GameViewport.dimensions()
|
||||
local pw, ph = ww, wh
|
||||
if love.graphics.getPixelDimensions then
|
||||
pw, ph = love.graphics.getPixelDimensions()
|
||||
end
|
||||
pw, ph = GameViewport.pixelDimensions()
|
||||
local dpiX, dpiY = 1, 1
|
||||
if ww > 0 and pw > 0 then dpiX = pw / ww end
|
||||
if wh > 0 and ph > 0 then dpiY = ph / wh end
|
||||
@@ -698,7 +697,7 @@ end
|
||||
-- When GBC FX is active the composite is drawn into presentCanvas and
|
||||
-- presented through the GBC FX shader as a final pass.
|
||||
function Renderer:endFrame(zones, worldZones)
|
||||
love.graphics.setCanvas()
|
||||
GameViewport.setTarget()
|
||||
local ww, wh, pw, ph, dpiX, dpiY = displayMetrics()
|
||||
-- Sp = integer framebuffer pixels per GB pixel;
|
||||
-- Sx/Sy = LOVE-unit draw scales (may differ when dpiX ≠ dpiY).
|
||||
@@ -1064,7 +1063,7 @@ function Renderer:endFrame(zones, worldZones)
|
||||
end
|
||||
|
||||
if present then
|
||||
love.graphics.setCanvas()
|
||||
GameViewport.setTarget()
|
||||
-- Post-process pipelines run over the finished composite -- world, UI
|
||||
-- and all -- and before GBC FX, so a blur or colour grade is what the
|
||||
-- LCD grid is then drawn over rather than something that smears the
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
-- covered by tests; the state at the bottom is the only part that draws.
|
||||
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
local GameViewport = require("src.render.GameViewport")
|
||||
local Palettes = require("src.world.gen2.Palettes")
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
local SpriteAnims = require("src.ui.gen2.SpriteAnims")
|
||||
@@ -509,7 +510,7 @@ function BattleTransition:blackAt(col, row)
|
||||
end
|
||||
|
||||
function BattleTransition:draw()
|
||||
local w, h = love.graphics.getDimensions()
|
||||
local w, h = GameViewport.dimensions()
|
||||
self:drawWidescreen(w, h)
|
||||
end
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
local Kit = require("src.ui.kit.Kit")
|
||||
local Theme = require("src.ui.kit.Theme")
|
||||
local SafeArea = require("src.core.SafeArea")
|
||||
local GameViewport = require("src.render.GameViewport")
|
||||
|
||||
local Layout = {}
|
||||
|
||||
@@ -41,7 +42,7 @@ local lastW, lastH, lastOx, lastOy, lastSw, lastSh, lastMax
|
||||
function Layout.metrics(maxAppW)
|
||||
local W, H = 0, 0
|
||||
if love and love.graphics and love.graphics.getDimensions then
|
||||
W, H = love.graphics.getDimensions()
|
||||
W, H = GameViewport.dimensions()
|
||||
end
|
||||
local ox, oy, sw, sh = SafeArea.rect()
|
||||
local s = Kit.layout(sw, sh)
|
||||
|
||||
@@ -9,6 +9,7 @@ local Collision = require("src.world.Collision")
|
||||
local Encounter = require("src.world.Encounter")
|
||||
local FieldDefaults = require("src.world.FieldDefaults")
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local GameViewport = require("src.render.GameViewport")
|
||||
local Logger = require("src.core.Logger")
|
||||
local Map = require("src.world.Map")
|
||||
local MapLoader = require("src.world.MapLoader")
|
||||
@@ -5131,7 +5132,7 @@ function OverworldState:drawWorld()
|
||||
-- point projects under the pipeline's own camera. That is the direct
|
||||
-- analogue of what :billboard does for tilt, and it keeps exactly one
|
||||
-- copy of every effect: the closures above are the ones that run.
|
||||
local pw, ph = love.graphics.getDimensions()
|
||||
local pw, ph = GameViewport.dimensions()
|
||||
local pscale = Zoom.scale(Game.renderer:fitScale())
|
||||
local ctx = {
|
||||
state = self, cam = cam, vw = vw, vh = vh, bgY = bgY,
|
||||
|
||||
@@ -34,6 +34,7 @@ local Font = require("src.render.Font")
|
||||
-- a mod has taken a facade (src/mods/Gen2Compat.lua).
|
||||
local Gen1Facade = require("src.mods.Gen2Compat")
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
local GameViewport = require("src.render.GameViewport")
|
||||
local Gen2Save = require("src.core.gen2.Save")
|
||||
local HallOfFame = require("src.core.gen2.HallOfFame")
|
||||
local HiddenItems = require("src.world.gen2.HiddenItems")
|
||||
@@ -7548,7 +7549,7 @@ function World:interactBody()
|
||||
end
|
||||
|
||||
function World:fitScale()
|
||||
local w, h = love.graphics.getDimensions()
|
||||
local w, h = GameViewport.dimensions()
|
||||
return math.max(1, math.floor(math.min(w / 160, h / 144)))
|
||||
end
|
||||
|
||||
@@ -8375,7 +8376,7 @@ function World:rebuildNeighbors()
|
||||
self.neighbors = {}
|
||||
if not self.map then return end
|
||||
local s = self:zoomScale()
|
||||
local ww, wh = love.graphics.getDimensions()
|
||||
local ww, wh = GameViewport.dimensions()
|
||||
local vw = math.ceil(ww / s)
|
||||
local vh = math.ceil(wh / s)
|
||||
if vw % 2 ~= 0 then vw = vw + 1 end
|
||||
@@ -9742,7 +9743,7 @@ function World:drawGround(s)
|
||||
if canvas then
|
||||
bw, bh = canvas:getDimensions()
|
||||
else
|
||||
bw, bh = G.getDimensions()
|
||||
bw, bh = GameViewport.dimensions()
|
||||
end
|
||||
BorderFill.draw(self, self:borderImageFor(self.map.id),
|
||||
cam.x, cam.y, bw, bh, s, self.map.id)
|
||||
@@ -10002,7 +10003,7 @@ end
|
||||
|
||||
function World:draw()
|
||||
local G = love.graphics
|
||||
local w, h = G.getDimensions()
|
||||
local w, h = GameViewport.dimensions()
|
||||
self:refreshColorMode()
|
||||
G.clear(0.07, 0.05, 0.02, 1)
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
love = require("tests.love_stub")
|
||||
|
||||
local Hooks = require("src.mods.Hooks")
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
local savedHooks = Runtime.hooks
|
||||
local hooks = Hooks.new()
|
||||
Runtime.hooks = hooks
|
||||
|
||||
local Viewport = require("src.render.GameViewport")
|
||||
local SafeArea = require("src.core.SafeArea")
|
||||
local TouchControls = require("src.core.TouchControls")
|
||||
|
||||
Viewport.begin(1)
|
||||
assert(not Viewport.active(), "vanilla frame must not allocate a viewport")
|
||||
local w, h = Viewport.dimensions()
|
||||
assert(w == 640 and h == 576, "vanilla dimensions must stay unchanged")
|
||||
|
||||
hooks:wrap("render.viewport", function(next, ctx)
|
||||
local full = next(ctx)
|
||||
assert(full.width == 640 and full.height == 576,
|
||||
"viewport hook receives OS-independent window geometry")
|
||||
return { x = 320, y = 12, width = 320, height = 288 }
|
||||
end, 0, "fixture")
|
||||
|
||||
local presented
|
||||
hooks:wrap("render.window", function(next, game, ctx)
|
||||
presented = ctx
|
||||
return next(game, ctx)
|
||||
end, 0, "fixture")
|
||||
|
||||
Viewport.begin(2)
|
||||
assert(Viewport.active(), "a reserved rectangle creates a game target")
|
||||
w, h = Viewport.dimensions()
|
||||
assert(w == 320 and h == 288, "game renders against reserved dimensions")
|
||||
Viewport.target().getPixelDimensions = function() return 737, 664 end
|
||||
local pw, ph = Viewport.pixelDimensions()
|
||||
assert(pw == 737 and ph == 664,
|
||||
"captured rendering uses the target's real high-DPI pixel dimensions")
|
||||
local x, y, inside = Viewport.toLocal(400, 100)
|
||||
assert(x == 80 and y == 88 and inside,
|
||||
"window pointers expose viewport-local coordinates")
|
||||
local _, _, outside = Viewport.toLocal(20, 20)
|
||||
assert(not outside, "reserved companion space is outside the game viewport")
|
||||
local _, _, localW, localH = SafeArea.rect()
|
||||
local _, _, windowW, windowH = SafeArea.windowRect()
|
||||
assert(localW == 320 and localH == 288,
|
||||
"game chrome may still use viewport-local safe geometry")
|
||||
assert(windowW == 640 and windowH == 576,
|
||||
"OS chrome can retain the full-window safe geometry")
|
||||
TouchControls:init()
|
||||
local controls = TouchControls:layout()
|
||||
assert(controls.dpad.cx < 160 and controls.a.cx > 480,
|
||||
"touch controls stay laid out across the full OS window")
|
||||
local function source(path)
|
||||
local file = assert(io.open(path, "r"))
|
||||
local text = file:read("*a")
|
||||
file:close()
|
||||
return text
|
||||
end
|
||||
for _, path in ipairs({ "src/core/Game.lua", "src/core/Game2.lua" }) do
|
||||
local text = source(path)
|
||||
local finish = assert(text:find("GameViewport.finish(self)", 1, true))
|
||||
local controlsDraw = assert(text:find("TouchControls:draw()", finish, true))
|
||||
assert(controlsDraw > finish,
|
||||
path .. " draws touch controls after final window composition")
|
||||
end
|
||||
Viewport.setTarget()
|
||||
assert(love.graphics.getCanvas() == Viewport.target(),
|
||||
"game rendering is redirected into the viewport canvas")
|
||||
Viewport.finish({})
|
||||
assert(presented and presented.x == 320 and presented.y == 12
|
||||
and presented.width == 320 and presented.height == 288
|
||||
and presented.windowWidth == 640 and presented.windowHeight == 576
|
||||
and presented.generation == 2,
|
||||
"window composition receives game and host geometry")
|
||||
assert(love.graphics.getCanvas() == nil,
|
||||
"window composition restores the OS render target")
|
||||
Viewport.reset()
|
||||
assert(not Viewport.active(),
|
||||
"viewport geometry cannot leak into the launcher after presentation")
|
||||
|
||||
hooks.chains["render.viewport"] = nil
|
||||
hooks:wrap("render.viewport", function(next, ctx)
|
||||
local full = next(ctx)
|
||||
full.capture = true
|
||||
return full
|
||||
end, 0, "capture-fixture")
|
||||
Viewport.begin(1)
|
||||
assert(Viewport.active() and Viewport.dimensions() == 640,
|
||||
"a full-window capture allocates a final composition target")
|
||||
presented = nil
|
||||
Viewport.setTarget()
|
||||
Viewport.finish({})
|
||||
assert(presented and presented.width == 640 and presented.height == 576,
|
||||
"a full-window capture reaches final window composition")
|
||||
Viewport.reset()
|
||||
|
||||
Runtime.hooks = savedHooks
|
||||
print("render viewport: ok")
|
||||
Reference in New Issue
Block a user