port: leaf clock mechanism -- origin and rate, both UNSET

F6 is a clock question, not an effect question: the human confirms the light
animation itself looks like the game's and only starts earlier, and separately
that the port may be running it faster (flagged by them as an impression).

The port passed time_units straight to the leaf, which is an assumption -- offset
0, rate 1 -- that nobody measured and that the Decoder's capture contradicts:
the game's leaf t=0 is its first drawn frame, 40 frames after the title's first
element, at a rate measuring well below the title's.

ScreenView.leaf_clock(screen_units) applies an origin and a rate; both default to
-1.0 meaning unmeasured, in which case it is the identity and behaviour is
unchanged. Fed from authored/rendering.json leaf_clock per screen, currently null
with the provenance recorded.

No placeholder values, per F1: an invented constant here is indistinguishable
from a measured one later.

Verified inert: title rendered at t=2/3/4 s, 0 differing pixels against captures
taken before the change.

Also corrected in the process: my earlier 'the sweep enters the viewport at t=61'
used the UNROTATED sprite width. The leaf carries a 30 deg rest rotation, so its
AABB is 886 px against a 399 px sprite -- matching the Decoder's measured ~890 to
within 4 px. Port and game both put the quad on screen at leaf t~0, so the whole
discrepancy is the leaf clock's origin and rate.
This commit is contained in:
Sylpheed port agent
2026-09-02 19:06:17 +00:00
parent 8c282e4249
commit 4d533185e7
3 changed files with 248 additions and 171 deletions

View File

@@ -305,6 +305,10 @@ func _ready() -> void:
var rendering: Variant = export_tree.authored("rendering.json")
_draw_leaf_for = [] if rendering == null else rendering.get("draw_leaf_for", [])
_loop_leaf_screens = [] if rendering == null else rendering.get("loop_leaf_on_screens", [])
# F6: the leaf's clock relative to its screen's. Absent or null -> the leaf
# runs on the screen clock, which is what this port has always done and is
# itself an unmeasured assumption (offset 0, rate 1). See ScreenView.leaf_clock.
_leaf_clock = {} if rendering == null else rendering.get("leaf_clock", {})
# `additive_elements` is DELETED from authored/rendering.json -- the blend is
# decoded now (`T8aD +0x04` bit 0x02) and the exporter emits `blend_additive`
# per element, which `ScreenView._draw` reads directly. Nothing to assign,
@@ -337,6 +341,9 @@ func _ready() -> void:
view.loop_phase_units = _loop_phase
view.draw_leaf_for = _draw_leaf_for
view.loop_leaf = _loop_leaf_screens.has(name)
var lc: Dictionary = _leaf_clock.get(name, {})
view.leaf_start_units = float(lc.get("start_units", -1.0)) if lc.get("start_units") != null else -1.0
view.leaf_rate = float(lc.get("rate", -1.0)) if lc.get("rate") != null else -1.0
view.focused_id = _force_focus
if not view.load_screen(export_tree, name):
push_error(export_tree.error)
@@ -463,6 +470,9 @@ var _looping: Dictionary = {}
## `authored/rendering.json` `draw_leaf_for`.
var _draw_leaf_for: Array = []
## `authored/rendering.json` `additive_elements` -- measured off the running game.
## `authored/rendering.json` `leaf_clock`: screen -> {start_units, rate}.
var _leaf_clock: Dictionary = {}
## `authored/rendering.json` `loop_leaf_on_screens`.
var _loop_leaf_screens: Array = []
var _script: PackedStringArray = PackedStringArray()
@@ -701,6 +711,9 @@ func _advance() -> void:
view.loop_phase_units = _loop_phase
view.draw_leaf_for = _draw_leaf_for
view.loop_leaf = _loop_leaf_screens.has(name)
var lc: Dictionary = _leaf_clock.get(name, {})
view.leaf_start_units = float(lc.get("start_units", -1.0)) if lc.get("start_units") != null else -1.0
view.leaf_rate = float(lc.get("rate", -1.0)) if lc.get("rate") != null else -1.0
if not view.load_screen(view.tree, name):
push_error(view.tree.error)
get_tree().quit(2)
@@ -1100,6 +1113,9 @@ func _menu_arrive() -> void:
view.loop_phase_units = _loop_phase
view.draw_leaf_for = _draw_leaf_for
view.loop_leaf = _loop_leaf_screens.has(name)
var lc: Dictionary = _leaf_clock.get(name, {})
view.leaf_start_units = float(lc.get("start_units", -1.0)) if lc.get("start_units") != null else -1.0
view.leaf_rate = float(lc.get("rate", -1.0)) if lc.get("rate") != null else -1.0
if not view.load_screen(view.tree, name):
push_error(view.tree.error)
get_tree().quit(2)

View File

@@ -184,6 +184,39 @@ var loop_leaf := false
## pose, the leaf is placed at whatever phase is being tested.
var leaf_time_units: float = -1.0
## The leaf's clock relative to the SCREEN's, as an origin and a rate.
##
## 🔴 **BOTH UNSET (-1.0) AND THE LEAF THEREFORE RUNS ON THE SCREEN CLOCK,
## EXACTLY AS BEFORE.** F6: the human reports the title's light sweep starts
## earlier here than in the game, and the Decoder's capture puts the game's leaf
## `t=0` at its first drawn frame, 40 frames after the title's first element, at a
## rate that is not the title's. The port has neither an origin nor a rate for the
## leaf -- it simply passes `time_units` through, which is an assumption (offset 0,
## rate 1) that nobody measured and that the capture now contradicts.
##
## ⚠️ **No placeholder.** F1 established the rule the hard way: an invented
## constant here is indistinguishable from a measured one later. These stay unset
## until the Decoder's figures land, and `authored/rendering.json` carries them as
## data with their provenance when they do.
##
## `leaf_time_units` above is a different thing -- an ABSOLUTE override for the
## `--leaf-time` diagnostic, which pins a pose and ignores both of these.
var leaf_start_units: float = -1.0
var leaf_rate: float = -1.0
## Screen units -> leaf units. Identity while unmeasured.
##
## The clamp at 0 matters and is not arbitrary: at leaf `t=0` the quad's rotated
## AABB only just touches the viewport -- 886 px wide against a 399 px sprite at
## 30 deg, its right edge about 6 px inside the frame -- so parking at 0 before the
## start is visually indistinguishable from not drawing, without disturbing the
## parent-fallback path that `_draw_leaf` returns into.
func leaf_clock(screen_units: float) -> float:
if leaf_start_units < 0.0 or leaf_rate <= 0.0:
return screen_units
return maxf(0.0, (screen_units - leaf_start_units) * leaf_rate)
## While true the screen holds at `rest` and never plays its exit. The
## sequencer clears it to send the screen away.
var holding: bool = true
@@ -681,7 +714,7 @@ func _draw_leaf(element: Dictionary) -> bool:
# cannot tell "loops at 600" from "runs once for 600 and stops".
var was := holding
holding = false
var t := leaf_time_units if leaf_time_units >= 0.0 else time_units
var t := leaf_time_units if leaf_time_units >= 0.0 else leaf_clock(time_units)
if loop_leaf:
var span := 0.0
for k: Dictionary in fe.get("keyframes", []):