mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-14 23:31:27 +02:00
feat(mods): expose opt-in final-frame output hooks
This commit is contained in:
@@ -546,11 +546,13 @@ gains a field instead of the name gaining a prefix.
|
||||
each row's decision in `evolution.check`. The hook passes `data` where Gen 1
|
||||
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.letterbox`, `render.hud`. Each sits
|
||||
`render.zones`, `render.compose`, `render.output_enabled`, `render.output`,
|
||||
`render.letterbox`, `render.hud`. 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
|
||||
letterbox, and the finished playfield rect -- and carries the same payload.
|
||||
composed frame before GBCFX, the letterbox, and the finished playfield rect
|
||||
-- and carries the same payload.
|
||||
`render.hud`'s `gameX` / `gameY` really is where Gold's dialogue boxes and
|
||||
menus land, because `Chrome.fitScale` / `fitOrigin` and `World:fitScale`
|
||||
compute the same number. `render.zones` is handed `nil` in GBC mode (Gold
|
||||
|
||||
@@ -440,6 +440,20 @@ oldest queued event as `"action,x,y"` in submitted-frame coordinates, or `nil`.
|
||||
This is what lets a mod lay the two passes out as two stacked Game Boy screens,
|
||||
or push one onto a second screen, without the engine knowing the layout.
|
||||
|
||||
`render.output_enabled` and `render.output` are the later, whole-window seam
|
||||
for mods that need the engine's normal composite rather than its separate
|
||||
layers. It runs after registered present pipelines and before GBCFX,
|
||||
`render.hud`, and touch controls. A mod wraps both hooks: the first returns
|
||||
`true` only while output ownership is needed, and the second receives
|
||||
`(next, ctx)` with `canvas`, `width`, `height`, `gameX`, `gameY`, `gameWidth`,
|
||||
`gameHeight`, `scale`, `dpiX`,
|
||||
`dpiY`, and `generation`. Returning `true` from `render.output` takes over the
|
||||
window; calling `next(ctx)` keeps the normal presentation. Both hooks default
|
||||
to `false`. Enabling the seam requires a full-window canvas for that frame.
|
||||
With no `render.output` subscriber, or while `render.output_enabled` is false,
|
||||
the existing presentation path is unchanged. `render.compose` takes precedence
|
||||
when it owns the frame.
|
||||
|
||||
`screen.render_visible` receives `(next, state)` while the main screen is being
|
||||
composed. Return `false` to omit that state from drawing, opacity selection and
|
||||
palette-zone ownership. The state remains on the stack and keeps its normal
|
||||
|
||||
+26
-13
@@ -73,7 +73,7 @@ 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 six
|
||||
-- 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
|
||||
-- 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
|
||||
@@ -83,6 +83,7 @@ end
|
||||
-- input.pointer uncaptured pointer events (src/core/Game.lua:887)
|
||||
-- render.zones the palette pass, pre-blit (src/core/Game.lua:505)
|
||||
-- render.compose the whole-window composite (Renderer.lua:759)
|
||||
-- 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)
|
||||
--
|
||||
@@ -1382,12 +1383,14 @@ function Game2:draw()
|
||||
local zoned = type(zones) == "table" and zones[1] ~= nil
|
||||
|
||||
-- A present canvas is paid for only when something reads it: the zone pass,
|
||||
-- GBC FX, a mod post-process, or a render.compose subscriber about to be
|
||||
-- handed the finished frame. With none of them the frame draws straight to
|
||||
-- the screen exactly as it always did.
|
||||
-- GBC FX, a mod post-process, render.compose, or an enabled render.output
|
||||
-- subscriber. With none of them the frame draws straight to the screen
|
||||
-- exactly as it always did.
|
||||
local composing = ModRuntime.wantsHook("render.compose")
|
||||
local hasOutputHook = ModRuntime.wantsHook("render.output")
|
||||
and ModRuntime.call("render.output_enabled", function() return false end) == true
|
||||
local scene = nil
|
||||
if zoned or fx or composing or Pipelines.wantsPresent() then
|
||||
if zoned or fx or composing or Pipelines.wantsPresent() or hasOutputHook then
|
||||
scene = self:presentCanvas(1, w, h)
|
||||
end
|
||||
if not scene then
|
||||
@@ -1419,7 +1422,7 @@ function Game2:draw()
|
||||
-- untinted one. On its own the tint rides the final blit and no second
|
||||
-- canvas is paid for.
|
||||
local source = scene
|
||||
local reread = fx or Pipelines.wantsPresent()
|
||||
local reread = fx or Pipelines.wantsPresent() or hasOutputHook
|
||||
if zoned and reread then
|
||||
local tinted = self:presentCanvas(2, w, h)
|
||||
if tinted then
|
||||
@@ -1439,15 +1442,25 @@ function Game2:draw()
|
||||
-- Post-process pipelines run over the finished composite and before GBC
|
||||
-- FX. Each hands back a canvas; with none registered this returns `source`
|
||||
-- unchanged and the frame is byte-identical (Renderer.lua:1058).
|
||||
local scale, _, _, dpi = self:frameFit(w, h)
|
||||
local scale, ox, oy, dpi = self:frameFit(w, h)
|
||||
source = Pipelines.present(source, { width = w, height = h, scale = scale,
|
||||
dpi = dpi, dpiX = dpi, dpiY = dpi }) or source
|
||||
if fx then
|
||||
GBCFX.present(source, self:pixelScale(w, h))
|
||||
else
|
||||
G.setColor(1, 1, 1, 1)
|
||||
G.draw(source, 0, 0)
|
||||
G.setShader()
|
||||
local outputHandled = hasOutputHook
|
||||
and ModRuntime.call("render.output", function() return false end, {
|
||||
canvas = source, width = w, height = h,
|
||||
gameX = ox, gameY = oy,
|
||||
gameWidth = 160 * scale, gameHeight = 144 * scale,
|
||||
scale = scale, dpiX = dpi, dpiY = dpi,
|
||||
generation = 2,
|
||||
}) == true
|
||||
if not outputHandled then
|
||||
if fx then
|
||||
GBCFX.present(source, self:pixelScale(w, h))
|
||||
else
|
||||
G.setColor(1, 1, 1, 1)
|
||||
G.draw(source, 0, 0)
|
||||
G.setShader()
|
||||
end
|
||||
end
|
||||
end
|
||||
G.pop()
|
||||
|
||||
+25
-13
@@ -768,11 +768,12 @@ function Renderer:endFrame(zones, worldZones)
|
||||
end
|
||||
end
|
||||
|
||||
-- A post-process pipeline needs the whole composite in a canvas for the
|
||||
-- same reason GBC FX does, so either one alone is enough to take the
|
||||
-- present path; with neither, the frame draws straight to the screen
|
||||
-- exactly as it always did.
|
||||
local needPresent = GBCFX.active() or Pipelines.wantsPresent()
|
||||
-- Post-process pipelines, GBC FX and an enabled final-output owner need the
|
||||
-- whole composite in a canvas. With none of them, the frame draws straight
|
||||
-- to the screen exactly as it always did.
|
||||
local hasOutputHook = Runtime.wantsHook("render.output")
|
||||
and Runtime.call("render.output_enabled", function() return false end) == true
|
||||
local needPresent = GBCFX.active() or Pipelines.wantsPresent() or hasOutputHook
|
||||
local present = nil
|
||||
if needPresent then
|
||||
if not self.presentCanvas or self.presentCanvas:getWidth() ~= ww
|
||||
@@ -1057,14 +1058,25 @@ function Renderer:endFrame(zones, worldZones)
|
||||
-- this returns `present` unchanged and the frame is byte-identical.
|
||||
local composed = Pipelines.present(present,
|
||||
{ width = ww, height = wh, scale = Sp, dpi = dpiY, dpiX = dpiX, dpiY = dpiY }) or present
|
||||
if GBCFX.active() then
|
||||
-- shader grid/shadow math is in framebuffer pixels
|
||||
GBCFX.present(composed, Sp)
|
||||
else
|
||||
-- the present canvas only existed for the post-process, so put the
|
||||
-- result on the screen at the same 1:1 unit mapping it was built at
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(composed, 0, 0)
|
||||
local outputHandled = hasOutputHook
|
||||
and Runtime.call("render.output", function() return false end, {
|
||||
canvas = composed,
|
||||
width = ww, height = wh,
|
||||
gameX = ox, gameY = oy,
|
||||
gameWidth = vpw, gameHeight = vph,
|
||||
scale = Sp, dpiX = dpiX, dpiY = dpiY,
|
||||
generation = 1,
|
||||
}) == true
|
||||
if not outputHandled then
|
||||
if GBCFX.active() then
|
||||
-- shader grid/shadow math is in framebuffer pixels
|
||||
GBCFX.present(composed, Sp)
|
||||
else
|
||||
-- the present canvas only existed for the post-process, so put the
|
||||
-- result on the screen at the same 1:1 unit mapping it was built at
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(composed, 0, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
self.worldActive = false
|
||||
|
||||
@@ -434,7 +434,8 @@ local GEN2_HOOKS = {
|
||||
-- pointer with the touch overlay given first refusal, the palette zone list
|
||||
-- handed to the present pass, the letterbox and the HUD rect.
|
||||
"input.step", "input.pointer",
|
||||
"render.zones", "render.compose", "render.letterbox", "render.hud",
|
||||
"render.zones", "render.compose", "render.output_enabled", "render.output",
|
||||
"render.letterbox", "render.hud",
|
||||
}
|
||||
|
||||
local function assertShared(name, sites, kind)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local Game2 = require("src.core.Game2")
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
local Hooks = require("src.mods.Hooks")
|
||||
|
||||
local savedHooks = Runtime.hooks
|
||||
local hooks = Hooks.new()
|
||||
Runtime.hooks = hooks
|
||||
|
||||
local received
|
||||
hooks:wrap("render.output", function(_, context)
|
||||
received = context
|
||||
return true
|
||||
end, 0, "test")
|
||||
hooks:wrap("render.output_enabled", function() return false end, 0, "test")
|
||||
|
||||
local canvas = love.graphics.newCanvas(640, 480)
|
||||
local presentCalls = 0
|
||||
local game = setmetatable({
|
||||
frameFit = function() return 3, 80, 24, 1, 480, 432 end,
|
||||
presentCanvas = function() presentCalls = presentCalls + 1 return canvas end,
|
||||
drawScene = function() end,
|
||||
drawHud = function() end,
|
||||
}, Game2)
|
||||
|
||||
Game2.draw(game)
|
||||
T.eq(received, nil, "a disabled Gold output hook does not run")
|
||||
T.eq(presentCalls, 0, "a disabled Gold output hook keeps the direct path")
|
||||
|
||||
hooks:wrap("render.output_enabled", function() return true end, 10, "test")
|
||||
Game2.draw(game)
|
||||
T.check(received ~= nil, "Gold raises render.output for an enabled subscriber")
|
||||
T.eq(received and received.canvas, canvas,
|
||||
"Gold hands render.output the finished present canvas")
|
||||
T.eq(received and received.generation, 2,
|
||||
"Gold identifies the output context without changing Gen 1")
|
||||
T.eq(received and received.gameX, 80, "Gold output carries the fitted game X")
|
||||
T.eq(received and received.gameY, 24, "Gold output carries the fitted game Y")
|
||||
T.eq(received and received.gameWidth, 480,
|
||||
"Gold output carries the fitted game width")
|
||||
T.eq(received and received.gameHeight, 432,
|
||||
"Gold output carries the fitted game height")
|
||||
|
||||
Runtime.hooks = savedHooks
|
||||
T.finish("gen2 render output seam")
|
||||
@@ -987,6 +987,33 @@ check(fallback.style == "doublecircle",
|
||||
"a hook naming an unregistered style falls back to the vanilla bits")
|
||||
Runtime.install(Events.new(), Hooks.new(), {})
|
||||
|
||||
-- ------- gated final-output ownership
|
||||
|
||||
local outputHooks = Hooks.new()
|
||||
Runtime.install(Events.new(), outputHooks, {})
|
||||
local outputCalls, outputContext = 0, nil
|
||||
outputHooks:wrap("render.output", function(nextLink, context)
|
||||
outputCalls, outputContext = outputCalls + 1, context
|
||||
return true
|
||||
end, 0, "test")
|
||||
outputHooks:wrap("render.output_enabled", function() return false end, 0, "test")
|
||||
Renderer:init()
|
||||
Renderer.presentCanvas = nil
|
||||
Renderer:beginFrame(false)
|
||||
Renderer:endFrame(nil, nil)
|
||||
check(outputCalls == 0 and Renderer.presentCanvas == nil,
|
||||
"a disabled output hook leaves the direct render path untouched")
|
||||
|
||||
outputHooks:wrap("render.output_enabled", function() return true end, 10, "test")
|
||||
Renderer:init()
|
||||
Renderer.presentCanvas = nil
|
||||
Renderer:beginFrame(false)
|
||||
Renderer:endFrame(nil, nil)
|
||||
check(outputCalls == 1 and outputContext and outputContext.canvas,
|
||||
"an enabled output hook receives the finished frame")
|
||||
check(outputContext and outputContext.generation == 1,
|
||||
"the output context identifies the active generation")
|
||||
|
||||
-- ------- asset transforms
|
||||
|
||||
local function seedTransform(id, source)
|
||||
|
||||
Reference in New Issue
Block a user