port: F6 FOUND -- the gate was declared data the renderer was discarding

The sweep starts early because ScreenView drew a leaf WITHOUT multiplying its
parent's alpha in. ptloop01/ptloop02 declare 0:0 70:0 100:255 238:255 250:0, so
the sweep is invisible until t=70 and full at t=100. The port has had that ramp
in its export the whole time and was throwing it away at the draw call.

Three changes:

1. Parent alpha multiplies into the leaf's. This file asserted the opposite,
   arguing from the sweeps being drawn while their parent had expired -- but that
   could not separate 'the leaf wins' from 'the parent is ignored because it
   draws nothing', and its own comment said so and named the interval that would
   settle it. Measured there: dividing the leaf's declared curve out of the drawn
   alpha pins the implied parent at 255.0 (+/-1.5) over hundreds of frames while
   the drawn alpha swings 242 -> 132 -> 145.

2. rate 0.5, calibrated against a DECLARED interval (ptloop01's own 30-unit ramp)
   rather than captured frames: two runs differing 2x in frames give 0.4795 and
   0.4667, 2.7% apart. This number was 0.514, withdrawn, now reinstated at 0.5 --
   what failed the first time was quoting a rate in presents, and the ratio was
   discarded along with the frames.

3. The 107-unit authored offset is DELETED, with its guard tool. It came from a
   ratio measured against an element identified only by screen position; the
   declared ramp needs no such identification. Deleting an authored value because
   the data already says it is the outcome PORT-MISSION asks for.

Also reverted, same day I made it: removing the transparent-parent skip. I read
'the game submits the sweep ~950 frames past its parent's expiry' as the leaf
outliving the parent. It does not follow -- a draw submitted at alpha 0 is still
a draw, and submitted is not visible. With the multiply, skipping on a
transparent parent IS multiplying by zero.

VERIFIED, pre-registered before running, via --probe-leaf:
  u= 60 -> no draw at all           (predicted: parent alpha 0.00, gated off)
  u=100 -> leaf_t 50.0,  x -439     (predicted 50.0, -439)
  u=236 -> leaf_t 118.0, x -167     (predicted 118.0, -167)
verify-capture: every row unchanged.

⚠️ BUILD-SENSITIVE: builds 5 and 6 of GP_TITLE declare these records as a single
flat a=255 with no ramp. This export reads the ramped build; if that changes the
gate disappears silently.
This commit is contained in:
Sylpheed port agent
2026-09-02 19:47:20 +00:00
parent af10a2ec3c
commit ac1371cda3
4 changed files with 56 additions and 104 deletions

View File

@@ -171,41 +171,40 @@
],
"leaf_clock": {
"title": {
"start_units": 107.0,
"rate": null,
"start_fraction": 0.5,
"fraction_from": "title first visible element (t=0)",
"fraction_to": "press_start ptbtn00 onset (t=214)"
"start_units": null,
"rate": 0.5
}
},
"leaf_clock_why": [
"leaf_t = screen_t - start_units. rate is null = the leaf runs at the screen's",
"rate; nobody has a clock-free measurement of a rate and the one attempt was",
"withdrawn (below).",
"leaf_t = rate * screen_t. A null field means 'not measured'.",
"",
"start_units 107 = 0.500 x (214 - 0), resolved HERE from this export's own",
"declared keyframes. The 0.500 is the measurement; the 107 is arithmetic on it.",
"rate 0.5 -- the leaf's clock runs at HALF the screen's.",
" Measured by the Decoder against a DECLARED calibration rather than captured",
" frame counts: using ptloop01's own declared 30-unit alpha ramp (t=70..100) as",
" an in-capture title clock, two runs whose frame counts differ 2x give",
" leaf/title = 0.4795 and 0.4667, 2.7% apart.",
" docs/re/f6-unit10-parent-alpha-gates-the-sweep.md.",
"",
"MEASURED as a RATIO, which is the point: (onset - title start) / (plate - title",
"start) = 0.489 and 0.507 across two independent captures, 3.7% apart",
"(Decoder, docs/re/f6-unit9-sweep-period-and-onset.md + 15670f4). A ratio needs",
"no clock, which is why it survived when every unit-valued number did not.",
" \ud83d\udd34 This number was adopted as 0.514, withdrawn, and reinstated as 0.5 within",
" one day. What failed the first time was quoting a rate in captured FRAMES --",
" frames are presents and the present rate differs per run (1168 vs 600 for one",
" animation). The ratio itself was sound; it was discarded with the frames.",
" A calibration that is itself declared does not have that failure mode.",
"",
"\ud83d\udd34 EVERY TITLE-UNIT FIGURE FROM THE CAPTURES IS WITHDRAWN, twice over:",
" - rate 0.514 units/frame: captured frames are PRESENTS and the present rate",
" differs per run -- 1168 frames vs 600 for the same animation, 1.947x.",
" - offset '+40 units': converted through the plate's declared 12-unit ramp,",
" a calibration that puts title-start->plate at 75 units where the declared",
" data puts the plate at 238. That is a 3.2x clock conflict, still open, and",
" it is F4's `clock: shared` question.",
"Both were adopted here and both were reverted before shipping.",
"start_units null -- there is NO authored onset any more, and that is the point.",
" F6's gate is DECLARED: ptloop01/ptloop02 carry `0:0 70:0 100:255 238:255",
" 250:0`, so the sweep is invisible until t=70 and full at t=100. The port had",
" that ramp in its export the whole time and was discarding it, because",
" ScreenView drew the leaf without multiplying the parent's alpha in.",
"",
"\ud83d\udccc The human said the animation LOOKS correct while we were carrying a rate",
"that would have made it twice too fast. That was evidence against the number",
"and both agents filed it as a puzzle. An impression is weak about magnitudes",
"and strong about gross wrongness; this one was being read as the former.",
" A 107-unit offset was authored here earlier, from a measured 0.50 ratio. It",
" is retired: the ratio was measured against an element identified only by",
" screen position, and the declared ramp needs no such identification. Deleting",
" an authored value because the data already says it is the better outcome.",
"",
"\u26a0\ufe0f 107 is arithmetic on THIS export's keyframes. tools/port/check-leaf-onset",
"recomputes it and fails if a re-timing moves the anchors underneath it."
"\u26a0\ufe0f BUILD-SENSITIVE. Builds 5 and 6 of GP_TITLE declare these same two records",
"as a single flat a=255 keyframe -- no ramp, hence no gate. This export reads",
"the ramped build (verified: 0:0 70:0 100:255 238:255 250:0). If that ever",
"changes the gate silently disappears."
]
}

View File

@@ -708,7 +708,7 @@ static func _rot_of(pose: Dictionary) -> float:
## "the parent is ignored because it draws nothing" are NOT separated. A capture
## during t=100...238 would separate them.
## Returns whether anything was actually drawn, so the caller can fall back.
func _draw_leaf(element: Dictionary) -> bool:
func _draw_leaf(element: Dictionary, colour: Color) -> bool:
var any_drawn := false
for fe: Dictionary in element.get("leaf", {}).get("elements", []):
var rel: String = fe.get("sprite", "")
@@ -758,7 +758,24 @@ func _draw_leaf(element: Dictionary) -> bool:
skipped.append("%s (leaf scale 0 -- parent drawn instead)" % fe.get("id", ""))
continue
var pivot := _vec(fe.get("pivot", [0, 0]))
_draw_quad(tex, placement(pose, pivot, tex.get_size()), modulate_of(pose),
# 🔴 THE PARENT'S ALPHA MULTIPLIES IN, and this file said the opposite
# until 2026-09-02. The old text argued from the sweeps being drawn at
# t=355 while their parent had expired -- but that reasoning could not
# separate "the leaf wins" from "the parent is ignored because it draws
# nothing", and its own comment said so and named the interval that would.
#
# Measured there: dividing the leaf's declared curve out of the drawn
# alpha pins the implied parent at 255.0 (+/-1.5) across hundreds of
# frames while the drawn alpha swings 242 -> 132 -> 145
# (`docs/re/f6-unit10-parent-alpha-gates-the-sweep.md`).
#
# 📌 And this IS F6's gate, from declared data rather than a measured
# constant: `ptloop01`/`ptloop02` declare `0:0 70:0 100:255`, so the sweep
# is invisible until t=70 and full at t=100. The port had the ramp in its
# export the whole time and was throwing it away at this line.
var leaf_colour := modulate_of(pose)
leaf_colour.a *= colour.a
_draw_quad(tex, placement(pose, pivot, tex.get_size()), leaf_colour,
pivot, _vec(pose.get("pos", [0, 0])), _rot_of(pose))
drawn.append(fe.get("id", ""))
any_drawn = true
@@ -915,19 +932,16 @@ func _draw() -> void:
var pose: Dictionary = element.get("rest", {}) if pose_mode == Pose.REST \
else pose_at(element, time_units)
var colour := modulate_of(pose)
# 🔴 A LEAF OUTLIVES ITS PARENT, AND SKIPPING HERE KILLED IT EARLY.
# 🔴 I REMOVED THIS GUARD ON 2026-09-02 AND PUT IT BACK THE SAME DAY.
#
# The parent's pose gates the whole element, so `ptloop01` expiring at
# t=250 stopped the sweep being drawn at all -- while this file's own
# decode says the leaf runs on its OWN timeline and the parent's alpha is
# not multiplied in. The two were inconsistent and the gate won silently.
#
# The capture settles it: the game submits the sweep across frames
# 746..1913 while the parent expires at frame 956, so it keeps drawing for
# roughly 950 frames after the parent is gone
# (`f6-unit3-sweep-track-fits.md`, `f6-unit8-leaf-clock-in-title-units.md`).
# A transparent parent is not evidence that its leaf is transparent.
if colour.a <= 0.0 and not draw_leaf_for.has(id):
# The argument for removing it was that the game keeps SUBMITTING the
# sweep for ~950 frames after its parent expires, which is true and which
# I read as "the leaf outlives the parent". It does not follow: a draw
# submitted with alpha 0 is still a draw in the command stream, and
# submitted is not visible. Now that the parent's alpha is known to
# multiply into the leaf's (below), skipping on a transparent parent is
# exactly multiplying by zero, and the two are pixel-identical.
if colour.a <= 0.0:
# 🔴 THIS LINE USED TO SAY "at rest" WHATEVER INSTANT IT HAD POSED.
#
# On the timeline path the pose is `pose_at(time_units)`, not
@@ -953,7 +967,7 @@ func _draw() -> void:
# and rotation. See `_draw_leaf`.
if element.get("leaf_carries_geometry", false) \
and draw_leaf_for.has(String(element.get("id", ""))) \
and _draw_leaf(element):
and _draw_leaf(element, colour):
continue
# A FOCUSED button draws its record INSTEAD of its base sprite -- measured,
# the focused sprite covers the base at 100.0 % of base-visible pixels.

View File

@@ -150,8 +150,6 @@ step decisions-index must-pass tools/port/index-decisions --check
# is merely on a peer's unmerged branch is reported, because the fix is a merge
# and nobody in this container can make it.
step trajectory-fit must-pass tools/port/fit-trajectory --selftest
step leaf-onset must-pass tools/port/check-leaf-onset
step leaf-onset-ctl must-pass tools/port/check-leaf-onset --selftest
step doc-citations must-pass tools/port/check-citations
step citations-control must-pass tools/port/check-citations --selftest
# A refuted claim asserted outside its correction is a lie the corpus tells a

View File

@@ -1,59 +0,0 @@
#!/usr/bin/env python3
"""Is `leaf_clock.start_units` still the measured FRACTION of its anchors?
tools/port/check-leaf-onset [--selftest]
The measurement behind F6's sweep onset is a RATIO -- 0.500 of the interval from
the title's first visible element to the plate's onset -- because every
unit-valued figure from the captures was withdrawn (frames are presents; the
unit conversion carried a 3.2x clock conflict). A ratio needs no clock.
`authored/rendering.json` stores the resolved `start_units` so the runtime does
no arithmetic. That resolution is only valid against the keyframes it was
computed from, and a re-export that re-times either anchor moves them silently.
This recomputes it. It is a staleness guard, not a measurement.
"""
import json, sys
TOL = 1.0
def anchors():
t = json.load(open("export/screens/title/title.json"))
start = None
for el in t["elements"]:
for k in el.get("keyframes", []):
if (int(k["fade_argb"], 16) >> 24) > 0:
start = k["t"] if start is None else min(start, k["t"])
break
p = json.load(open("export/screens/title/press_start.json"))
ks = [(k["t"], int(k["fade_argb"], 16) >> 24) for k in p["elements"][0]["keyframes"]]
onset = next(t0 for (t0, a0), (_, a1) in zip(ks, ks[1:]) if a0 == 0 and a1 > 0)
return float(start), float(onset)
def main():
if "--selftest" in sys.argv:
lo, hi = anchors()
good = lo + 0.5 * (hi - lo)
ok_pass = abs(good - (lo + 0.5 * (hi - lo))) <= TOL
ok_fail = abs((good + 25.0) - (lo + 0.5 * (hi - lo))) > TOL
print("selftest: correct value accepted=%s, value off by 25 rejected=%s -> %s"
% (ok_pass, ok_fail, "ok" if (ok_pass and ok_fail) else "🔴 BROKEN"))
return 0 if (ok_pass and ok_fail) else 2
cfg = json.load(open("authored/rendering.json"))["leaf_clock"]["title"]
lo, hi = anchors()
want = lo + float(cfg["start_fraction"]) * (hi - lo)
have = float(cfg["start_units"])
print("anchors: title first visible t=%.0f, plate onset t=%.0f, span %.0f" % (lo, hi, hi - lo))
print("fraction %.3f -> %.1f units; authored start_units %.1f" % (cfg["start_fraction"], want, have))
if abs(want - have) > TOL:
print("🔴 start_units is STALE by %.1f units -- the anchors moved under it." % (have - want))
return 1
print("resolved onset still matches its measured fraction")
return 0
if __name__ == "__main__":
raise SystemExit(main())