mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-15 07:41:21 +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
|
each row's decision in `evolution.check`. The hook passes `data` where Gen 1
|
||||||
passes `game`; positions 2-4 (mon, row, trigger) match.
|
passes `game`; positions 2-4 (mon, row, trigger) match.
|
||||||
- *The frame (`src/core/Game2.lua`):* hooks `input.step`, `input.pointer`,
|
- *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
|
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
|
-- 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
|
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
|
`render.hud`'s `gameX` / `gameY` really is where Gold's dialogue boxes and
|
||||||
menus land, because `Chrome.fitScale` / `fitOrigin` and `World:fitScale`
|
menus land, because `Chrome.fitScale` / `fitOrigin` and `World:fitScale`
|
||||||
compute the same number. `render.zones` is handed `nil` in GBC mode (Gold
|
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,
|
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.
|
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
|
`screen.render_visible` receives `(next, state)` while the main screen is being
|
||||||
composed. Return `false` to omit that state from drawing, opacity selection and
|
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
|
palette-zone ownership. The state remains on the stack and keeps its normal
|
||||||
|
|||||||
+20
-7
@@ -73,7 +73,7 @@ end
|
|||||||
--
|
--
|
||||||
-- Gold composites its own frame (Game2:draw / drawScene) and pumps its own pad
|
-- 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
|
-- (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
|
-- 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
|
-- 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
|
-- 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)
|
-- input.pointer uncaptured pointer events (src/core/Game.lua:887)
|
||||||
-- render.zones the palette pass, pre-blit (src/core/Game.lua:505)
|
-- render.zones the palette pass, pre-blit (src/core/Game.lua:505)
|
||||||
-- render.compose the whole-window composite (Renderer.lua:759)
|
-- 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.letterbox the void around the 160x144 blit (Renderer.lua:840)
|
||||||
-- render.hud screen-space UI over the frame (src/core/Game.lua:521)
|
-- 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
|
local zoned = type(zones) == "table" and zones[1] ~= nil
|
||||||
|
|
||||||
-- A present canvas is paid for only when something reads it: the zone pass,
|
-- 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
|
-- GBC FX, a mod post-process, render.compose, or an enabled render.output
|
||||||
-- handed the finished frame. With none of them the frame draws straight to
|
-- subscriber. With none of them the frame draws straight to the screen
|
||||||
-- the screen exactly as it always did.
|
-- exactly as it always did.
|
||||||
local composing = ModRuntime.wantsHook("render.compose")
|
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
|
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)
|
scene = self:presentCanvas(1, w, h)
|
||||||
end
|
end
|
||||||
if not scene then
|
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
|
-- untinted one. On its own the tint rides the final blit and no second
|
||||||
-- canvas is paid for.
|
-- canvas is paid for.
|
||||||
local source = scene
|
local source = scene
|
||||||
local reread = fx or Pipelines.wantsPresent()
|
local reread = fx or Pipelines.wantsPresent() or hasOutputHook
|
||||||
if zoned and reread then
|
if zoned and reread then
|
||||||
local tinted = self:presentCanvas(2, w, h)
|
local tinted = self:presentCanvas(2, w, h)
|
||||||
if tinted then
|
if tinted then
|
||||||
@@ -1439,9 +1442,18 @@ function Game2:draw()
|
|||||||
-- Post-process pipelines run over the finished composite and before GBC
|
-- Post-process pipelines run over the finished composite and before GBC
|
||||||
-- FX. Each hands back a canvas; with none registered this returns `source`
|
-- FX. Each hands back a canvas; with none registered this returns `source`
|
||||||
-- unchanged and the frame is byte-identical (Renderer.lua:1058).
|
-- 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,
|
source = Pipelines.present(source, { width = w, height = h, scale = scale,
|
||||||
dpi = dpi, dpiX = dpi, dpiY = dpi }) or source
|
dpi = dpi, dpiX = dpi, dpiY = dpi }) or source
|
||||||
|
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
|
if fx then
|
||||||
GBCFX.present(source, self:pixelScale(w, h))
|
GBCFX.present(source, self:pixelScale(w, h))
|
||||||
else
|
else
|
||||||
@@ -1450,6 +1462,7 @@ function Game2:draw()
|
|||||||
G.setShader()
|
G.setShader()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
G.pop()
|
G.pop()
|
||||||
G.setColor(1, 1, 1, 1)
|
G.setColor(1, 1, 1, 1)
|
||||||
self:drawHud(w, h)
|
self:drawHud(w, h)
|
||||||
|
|||||||
+17
-5
@@ -768,11 +768,12 @@ function Renderer:endFrame(zones, worldZones)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A post-process pipeline needs the whole composite in a canvas for the
|
-- Post-process pipelines, GBC FX and an enabled final-output owner need the
|
||||||
-- same reason GBC FX does, so either one alone is enough to take the
|
-- whole composite in a canvas. With none of them, the frame draws straight
|
||||||
-- present path; with neither, the frame draws straight to the screen
|
-- to the screen exactly as it always did.
|
||||||
-- exactly as it always did.
|
local hasOutputHook = Runtime.wantsHook("render.output")
|
||||||
local needPresent = GBCFX.active() or Pipelines.wantsPresent()
|
and Runtime.call("render.output_enabled", function() return false end) == true
|
||||||
|
local needPresent = GBCFX.active() or Pipelines.wantsPresent() or hasOutputHook
|
||||||
local present = nil
|
local present = nil
|
||||||
if needPresent then
|
if needPresent then
|
||||||
if not self.presentCanvas or self.presentCanvas:getWidth() ~= ww
|
if not self.presentCanvas or self.presentCanvas:getWidth() ~= ww
|
||||||
@@ -1057,6 +1058,16 @@ function Renderer:endFrame(zones, worldZones)
|
|||||||
-- this returns `present` unchanged and the frame is byte-identical.
|
-- this returns `present` unchanged and the frame is byte-identical.
|
||||||
local composed = Pipelines.present(present,
|
local composed = Pipelines.present(present,
|
||||||
{ width = ww, height = wh, scale = Sp, dpi = dpiY, dpiX = dpiX, dpiY = dpiY }) or present
|
{ width = ww, height = wh, scale = Sp, dpi = dpiY, dpiX = dpiX, dpiY = dpiY }) or present
|
||||||
|
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
|
if GBCFX.active() then
|
||||||
-- shader grid/shadow math is in framebuffer pixels
|
-- shader grid/shadow math is in framebuffer pixels
|
||||||
GBCFX.present(composed, Sp)
|
GBCFX.present(composed, Sp)
|
||||||
@@ -1067,6 +1078,7 @@ function Renderer:endFrame(zones, worldZones)
|
|||||||
love.graphics.draw(composed, 0, 0)
|
love.graphics.draw(composed, 0, 0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
self.worldActive = false
|
self.worldActive = false
|
||||||
self.uprightActive = false
|
self.uprightActive = false
|
||||||
self.worldOverride = nil
|
self.worldOverride = nil
|
||||||
|
|||||||
@@ -434,7 +434,8 @@ local GEN2_HOOKS = {
|
|||||||
-- pointer with the touch overlay given first refusal, the palette zone list
|
-- pointer with the touch overlay given first refusal, the palette zone list
|
||||||
-- handed to the present pass, the letterbox and the HUD rect.
|
-- handed to the present pass, the letterbox and the HUD rect.
|
||||||
"input.step", "input.pointer",
|
"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)
|
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")
|
"a hook naming an unregistered style falls back to the vanilla bits")
|
||||||
Runtime.install(Events.new(), Hooks.new(), {})
|
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
|
-- ------- asset transforms
|
||||||
|
|
||||||
local function seedTransform(id, source)
|
local function seedTransform(id, source)
|
||||||
|
|||||||
Reference in New Issue
Block a user