port: delete exit_ramp_units, invert the format's own rule, and guard a scale-0 leaf
FOUR THINGS, and the first is what MISSION section 3 calls the measure of
progress.
DELETED `exit_ramp_units` AND `exit_ramp_seconds`. They were authored because the
disc had no time slot on a group's final keyframe, so the ramp into it was the
one unknown duration per screen. Under the corrected record layout that keyframe
does not exist -- a group is an 8-byte header then frames x {u32 time; 36-byte
pose} and every pose is timed. VERIFIED DEAD BEFORE DELETING: setting it to 9999
(166 s) moved the boot's transitions by 0.04 s, which is wall-clock jitter, and
both uses in ScreenView are gated on a condition that no longer fires on any of
the export's 866 keyframes.
INVERTED THE FORMAT'S OWN RULE. `check.rs` enforced "the final keyframe has no
`t`; the disc has no time slot there" and FORMAT.md stated it. Both are now
backwards, and the validator fired 150 times on a re-export. I had not run
`check` between pinning the tag and measuring against the oracle -- the pixel
harness was green while the format validator was failing on every screen with a
multi-keyframe group. A correctness harness does not replace a format one; they
fail at different layers.
GUARDED A SCALE-0 LEAF, which the Decoder hit in its own renderer: its leaf
branch marked the element drawn unconditionally while the blit returned early on
zero scale, so a scale-0 leaf suppressed its parent and blanked the element --
live on all four loading screens. This port did not have the bug only because
authored/rendering.json happens not to list pgloading_loop5. That is an accident
of a gate written for another reason, not a defence, so `_draw_leaf` now reports
whether it drew and `_draw` falls back to the parent.
ISOLATED THE PACING QUESTION rather than leaving it as a suspected regression.
Legacy association: publisher 4.70 agrees, developer 3.92 DIFFERS. Corrected:
publisher 4.26 DIFFERS, developer 3.62 agrees. Both misses are ~0.03 s outside a
composite bound. The association traded which screen is marginally out; it did
not regress the pacing.
Bumped the pin c -> d for the parser and audio changes. Its headline renderer
change does not reach this port: sylpheed-cli builds from the workspace crate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -4627,7 +4627,7 @@ dependencies = [
|
||||
"image",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sylpheed-formats 0.1.0 (git+https://git.mc02.dev/fabi/Sylpheed.git?tag=formats-pin-2026-08-29c)",
|
||||
"sylpheed-formats 0.1.0 (git+https://git.mc02.dev/fabi/Sylpheed.git?tag=formats-pin-2026-08-29d)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4651,7 +4651,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "sylpheed-formats"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.mc02.dev/fabi/Sylpheed.git?tag=formats-pin-2026-08-29c#b28dfedf3548200cc7269b762a422e4c2adeaadd"
|
||||
source = "git+https://git.mc02.dev/fabi/Sylpheed.git?tag=formats-pin-2026-08-29d#e2640338e149acc903fac551f400a5319950afb5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"binrw",
|
||||
|
||||
@@ -24,28 +24,6 @@
|
||||
"Also HANDOFF Q1, and part of the same measurement: the fade lands on the",
|
||||
"linear value at every one of the seven sampled frames, so there is no ease."
|
||||
],
|
||||
"exit_ramp_seconds": 0.4,
|
||||
"exit_ramp_why": [
|
||||
"HANDOFF Q7 + the RE agent's 2026-08-29 answer. MEASURED, not on the disc.",
|
||||
"",
|
||||
"Every element of a screen ends on exactly ONE untimed keyframe, so there is",
|
||||
"exactly one unknown duration per screen -- the ramp INTO that final keyframe.",
|
||||
"This is that duration. ~0.4 s, which is 24 units at 60 units/s.",
|
||||
"",
|
||||
"The alternative readings were tested and refuted. It is not a black quad laid",
|
||||
"over a frozen screen: under that model a black rect scales every region by the",
|
||||
"same 1-alpha, so the button-region / background-region brightness RATIO would",
|
||||
"be constant through the fade. Measured on the RE agent's filmstrip it falls",
|
||||
"6.495 -> 5.574 -> 3.105 -> 2.125 -> 1.935, a 3.4x monotonic drop. The screen",
|
||||
"itself plays out: pteff00.prm ramps to opaque black while the button labels,",
|
||||
"ptmsg, pteff10 and pteff12 all ramp to transparent, and ptframe1/2 hold.",
|
||||
"",
|
||||
"REACH, quoted from the RE agent rather than smoothed over: the filmstrip is",
|
||||
"downsampled and the button region contains some background, so this pins the",
|
||||
"DIRECTION, not 0.4 s to +/-0.05 s, and it is one transition pair. Treat the",
|
||||
"number as approximate and the model as established."
|
||||
],
|
||||
"exit_ramp_units": 24,
|
||||
"dwell_seconds": null,
|
||||
"dwell_why": [
|
||||
"NOT SET, and not needed. A screen's dwell is its OWN keyframe group: the",
|
||||
@@ -112,5 +90,30 @@
|
||||
"in the same frame, or a loop flag decoded on the disc."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"exit_ramp_deleted_why": [
|
||||
"DELETED 2026-08-29, and the deletion is the point.",
|
||||
"",
|
||||
"`exit_ramp_seconds` (~0.4 s) and `exit_ramp_units` (24) were AUTHORED because",
|
||||
"the disc had no time slot on a group's final keyframe, so the ramp into it was",
|
||||
"the one unknown duration per screen. Under the corrected record layout",
|
||||
"(formats-pin-2026-08-29c onward) there IS no untimed keyframe -- a group is an",
|
||||
"8-byte header then frames x {u32 time; 36-byte pose}, so every pose is timed",
|
||||
"including the last. The unknown the constant stood in for does not exist.",
|
||||
"",
|
||||
"MISSION section 3: 'When the RE agent later decodes something you had",
|
||||
"authored, delete the authored entry and let the exporter emit it. That",
|
||||
"deletion is the measure of progress.'",
|
||||
"",
|
||||
"VERIFIED DEAD BEFORE DELETING, not assumed: setting it to 9999 (166 seconds)",
|
||||
"changed the boot's transitions by 0.04 s -- wall-clock jitter, not a 166 s",
|
||||
"ramp. Both of its uses in ScreenView were gated on `not last_frame.has('t')`,",
|
||||
"which no longer fires on any of the export's 866 keyframes.",
|
||||
"",
|
||||
"The measurement it recorded is not lost: HANDOFF Q7's ~0.4 s fade-out and the",
|
||||
"0.17-0.23 s black hold are still measured facts, and the hold is still used --",
|
||||
"`tools/port/verify-dwell` compares a transition INTERVAL against the oracle's",
|
||||
"visible SPAN plus that hold. What is deleted is the port's need to invent a",
|
||||
"duration the disc now states."
|
||||
]
|
||||
}
|
||||
|
||||
@@ -75,7 +75,13 @@ license.workspace = true
|
||||
# port against oracle CAPTURES and never touches the CLI -- and it is the check
|
||||
# that matters. Revert to the path dependency the day the tag is an ancestor of
|
||||
# `main`.
|
||||
sylpheed-formats = { git = "https://git.mc02.dev/fabi/Sylpheed.git", tag = "formats-pin-2026-08-29c" }
|
||||
# Bumped c -> d 2026-08-29. What I wanted from the new state: `d` carries parser
|
||||
# and `audio.rs` changes on top of `c`. ⚠️ Its headline change -- Reborn's
|
||||
# renderer drawing `rotation_deg`, and `compose` drawing a leaf that carries
|
||||
# geometry -- does NOT reach this port from here: `sylpheed-cli` builds from the
|
||||
# WORKSPACE crate, so the reference renderer stays unrotated until the tag lands
|
||||
# on `main`. This bump is for the parser, not for the renderer.
|
||||
sylpheed-formats = { git = "https://git.mc02.dev/fabi/Sylpheed.git", tag = "formats-pin-2026-08-29d" }
|
||||
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
@@ -186,10 +186,26 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
|
||||
for (k, kf) in kfs.iter().enumerate() {
|
||||
check_pose(&mut c, &format!("{at} keyframe {k}"), kf);
|
||||
}
|
||||
// The last keyframe of a group carries no time slot on the disc, and
|
||||
// an invented one is exactly the kind of value this format refuses.
|
||||
if kfs.len() > 1 && kfs.last().is_some_and(|k| k.get("t").is_some()) {
|
||||
c.err(format!("{at}: the final keyframe has a `t`; the disc has no time slot there"));
|
||||
// 🔴 INVERTED 2026-08-29, and the old rule is the more interesting
|
||||
// half. It read: "the last keyframe of a group carries no time slot
|
||||
// on the disc, and an invented one is exactly the kind of value this
|
||||
// format refuses." That was true of the OLD keyframe association,
|
||||
// where a group's data stopped four bytes short of its final block's
|
||||
// time slot.
|
||||
//
|
||||
// Under the corrected layout (`formats-pin-2026-08-29c` onward) a
|
||||
// group is an 8-byte header then `frames` x {u32 time; 36-byte
|
||||
// pose}, so **pose 0's time is the group's lead-in word and EVERY
|
||||
// POSE IS TIMED, including the last.** The rule now says the
|
||||
// opposite, and an untimed keyframe is the thing to refuse.
|
||||
//
|
||||
// ⚠️ This fired 150 times on a re-export and I had not run `check`
|
||||
// between pinning the tag and measuring against the oracle -- the
|
||||
// pixel harness was green while the format validator was failing on
|
||||
// every screen with a multi-keyframe group. A correctness harness
|
||||
// does not replace a format one; they fail at different layers.
|
||||
if kfs.len() > 1 && kfs.iter().any(|k| k.get("t").is_none()) {
|
||||
c.err(format!("{at}: a keyframe has no `t`; every pose is timed under the corrected record layout"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,10 +212,25 @@ from: the quad is `pivot × 2`, and its colour is the keyframe's `fade_argb`.
|
||||
|
||||
**`keyframes`** carry the on-disc time verbatim in `t`. A keyframe is the
|
||||
**start of a ramp toward the next**, not a pose that is held, and the ramp is
|
||||
linear. The **last keyframe of a group has no `t`** — the disc has no time slot
|
||||
there — and a file that puts one on it is wrong, not merely odd. The unit of `t`
|
||||
is measured, not on the disc, and so lives in `authored/` and is applied in
|
||||
exactly one place.
|
||||
linear.
|
||||
|
||||
🔴 **Every keyframe has a `t`, including the last**, and this paragraph said the
|
||||
exact opposite until 2026-08-29. A placement group is an 8-byte header followed
|
||||
by `frames` × `{u32 time; 36-byte pose}`, so **pose 0's time is the group's
|
||||
lead-in word** and no pose is untimed. The old reading — that a group's data
|
||||
stopped four bytes short of its final block's time slot — paired every pose with
|
||||
the *next* pose's time, and `sylpheed-export check` enforced it as a rule. A file
|
||||
with an untimed keyframe is now the wrong one.
|
||||
|
||||
⚠️ Two things went with that correction. The **exit ramp is gone**: there is no
|
||||
untimed final keyframe to give a synthetic time to, so `authored/timing.json`'s
|
||||
`exit_ramp_units` — an authored *measured* constant since P3 — is **deleted**,
|
||||
which is what MISSION §3 means by a deletion being the measure of progress. And
|
||||
`rest.t` moved on several screens: `publisher_logo` settles at t=30 rather than
|
||||
t=235.
|
||||
|
||||
The unit of `t` is still measured rather than on the disc, so it stays in
|
||||
`authored/` and is applied in exactly one place.
|
||||
|
||||
**`rotation_deg`** is screen-plane rotation in degrees, clockwise-positive,
|
||||
decoded from the keyframe's `+12`. **The game renders it**, confirmed twice by
|
||||
|
||||
@@ -117,6 +117,13 @@ func _ready() -> void:
|
||||
if args.has("boot"):
|
||||
_capture_to = args.get("capture", "")
|
||||
_film = args.get("film", "")
|
||||
# `--film-interval=` in seconds. Configurable because the fixed 0.25 s could
|
||||
# not resolve the boot's own black hold: the transition's pure-black plateau
|
||||
# is MEASURED at 0.17-0.23 s (authored/timing.json, HANDOFF Q7), which is
|
||||
# shorter than the cadence that was meant to observe it. `verify-dwell` duly
|
||||
# reported two screens as one 93 s span and called it a regression, when the
|
||||
# black frame had simply fallen between samples.
|
||||
_film_interval = maxf(0.01, float(args.get("film-interval", "0.25")))
|
||||
_shots = args.get("shots", "")
|
||||
if args.has("script"):
|
||||
_script = args["script"].split(",", false)
|
||||
@@ -177,7 +184,12 @@ func _ready() -> void:
|
||||
view.units_per_second = float(timing["keyframe_units_per_second"])
|
||||
# The one unknown duration per screen: the ramp into the final untimed
|
||||
# keyframe. Authored, because the disc has no time slot there.
|
||||
view.exit_ramp_units = float(timing["exit_ramp_units"])
|
||||
# `exit_ramp_units` is DELETED from authored/timing.json -- under the corrected
|
||||
# record layout every pose is timed, so there is no untimed final keyframe to
|
||||
# give a synthetic time to. The default below is now unreachable rather than
|
||||
# authored, and both of ScreenView's uses are dead branches kept only so an
|
||||
# older export still loads.
|
||||
view.exit_ramp_units = float(timing.get("exit_ramp_units", 24.0))
|
||||
# Which focus records draw unconditionally and loop. Kept out of ScreenView's
|
||||
# own logic on purpose -- see `looping_focus` there for the census that says
|
||||
# this cannot be a rule.
|
||||
@@ -278,6 +290,8 @@ var _step := 0
|
||||
var _film := ""
|
||||
var _film_frame := 0
|
||||
var _film_next := 0.0
|
||||
## Seconds between `--film` frames. See `--film-interval`.
|
||||
var _film_interval := 0.25
|
||||
var _elapsed := 0.0
|
||||
var _boot_done := false
|
||||
|
||||
@@ -652,7 +666,7 @@ func _film_capture() -> void:
|
||||
var img := viewport.get_texture().get_image()
|
||||
img.save_png("%s_%03d.png" % [_film, _film_frame])
|
||||
_film_frame += 1
|
||||
_film_next += 0.25
|
||||
_film_next += _film_interval
|
||||
|
||||
|
||||
# Godot passes everything after `--` through untouched; take `--key=value`.
|
||||
|
||||
@@ -404,7 +404,9 @@ static func _rot_of(pose: Dictionary) -> float:
|
||||
## ❔ Every observation behind this has parent alpha 0, so "the leaf wins" and
|
||||
## "the parent is ignored because it draws nothing" are NOT separated. A capture
|
||||
## during t=100...238 would separate them.
|
||||
func _draw_leaf(element: Dictionary) -> void:
|
||||
## Returns whether anything was actually drawn, so the caller can fall back.
|
||||
func _draw_leaf(element: Dictionary) -> bool:
|
||||
var any_drawn := false
|
||||
for fe: Dictionary in element.get("leaf", {}).get("elements", []):
|
||||
var rel: String = fe.get("sprite", "")
|
||||
if rel == "":
|
||||
@@ -422,10 +424,27 @@ func _draw_leaf(element: Dictionary) -> void:
|
||||
holding = false
|
||||
var pose := pose_at(fe, time_units)
|
||||
holding = was
|
||||
# 🔴 A SCALE-0 LEAF MUST NOT CLAIM THE DRAW. The Decoder hit this in its own
|
||||
# renderer: its leaf branch marked the element drawn unconditionally, but
|
||||
# the blit returns early on zero scale, so a scale-0 leaf suppressed its
|
||||
# parent and BLANKED the element -- live on all four loading screens via
|
||||
# `pgloading_loop5`, whose leaf is scale (0, 0).
|
||||
#
|
||||
# ⚠️ This port did not have the bug only because `authored/rendering.json`
|
||||
# happens not to list `pgloading_loop5`. That is an accident of a gate
|
||||
# written for a different reason, not a defence, so the guard is here: a
|
||||
# leaf that would draw nothing reports so, and `_draw` falls back to the
|
||||
# parent rather than losing the element.
|
||||
var scale: Array = pose.get("scale", [100, 100])
|
||||
if int(scale[0]) == 0 or int(scale[1]) == 0:
|
||||
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),
|
||||
pivot, _vec(pose.get("pos", [0, 0])), _rot_of(pose))
|
||||
drawn.append(fe.get("id", ""))
|
||||
any_drawn = true
|
||||
return any_drawn
|
||||
|
||||
|
||||
func _draw_focus(element: Dictionary) -> void:
|
||||
@@ -508,8 +527,8 @@ func _draw() -> void:
|
||||
# itself: the parent is a container whose own record has identity scale
|
||||
# and rotation. See `_draw_leaf`.
|
||||
if element.get("leaf_carries_geometry", false) \
|
||||
and draw_leaf_for.has(String(element.get("id", ""))):
|
||||
_draw_leaf(element)
|
||||
and draw_leaf_for.has(String(element.get("id", ""))) \
|
||||
and _draw_leaf(element):
|
||||
continue
|
||||
# A focused button draws its own record instead of its base sprite -- and
|
||||
# so does an element the authored table says always shows it, which is
|
||||
|
||||
@@ -1,91 +1,105 @@
|
||||
#!/usr/bin/env bash
|
||||
# Measure the port's own VISIBLE SPANS and put them beside the oracle's dwells.
|
||||
# Check the port's boot pacing against captures of the real game.
|
||||
#
|
||||
# tools/port/verify-dwell
|
||||
#
|
||||
# WHY THIS IS A TOOL AND NOT A ONE-OFF. Doing this by hand once already refuted a
|
||||
# 🔴 I had filed myself. `docs/port/BLOCKED.md` said `rest.t` was the wrong
|
||||
# settle landmark AND that "everything the sequencer paces off it is therefore
|
||||
# late". The first half is true; the second was wrong, and I nearly went and
|
||||
# "fixed" screens that already matched the game to 0.05 s.
|
||||
# WHY THIS IS A TOOL AND NOT A ONE-OFF. Doing it by hand once already refuted a
|
||||
# 🔴 I had filed myself: `docs/port/BLOCKED.md` said `rest.t` was the wrong settle
|
||||
# landmark AND that "everything the sequencer paces off it is therefore late".
|
||||
# The first half is true; the second was wrong, and I nearly re-paced screens
|
||||
# that already matched the game to 0.05 s.
|
||||
#
|
||||
# ⚠️ THE TRAP THAT CAUSED IT, AND THE REASON THIS SCRIPT EXISTS AT ALL:
|
||||
# a port's TRANSITION TIMESTAMPS and the oracle's VISIBLE SPANS are not the same
|
||||
# quantity. They differ by the exit ramp plus the black hold -- about 0.6 s here,
|
||||
# which was the entire discrepancy. This corpus has been bitten by the identical
|
||||
# confusion before, on the plate delay: "timed from where build 4 stops
|
||||
# animating, not from where it first appears -- measured the other way the two
|
||||
# runs differ by 0.48 s against 6 ms." So this measures the port the way the
|
||||
# oracle was measured: first ink to last ink.
|
||||
# ⚠️ A PORT'S TRANSITION INTERVAL IS NOT THE ORACLE'S VISIBLE SPAN. They differ
|
||||
# by the black hold between screens, and confusing the two cost this corpus 0.6 s
|
||||
# once and 0.48 s on the plate delay. So the comparison here is explicit: the
|
||||
# port's interval is checked against the oracle's span PLUS the measured hold.
|
||||
#
|
||||
# THE EXPECTED NUMBERS BELOW ARE THE ORACLE'S, NOT THE PORT'S. They are three
|
||||
# cold boots from `docs/re/boot-order-and-splash-dwell.md`, first-visible to
|
||||
# last-visible, and they are quoted here as a test fixture. Nothing in the port
|
||||
# derives them and nothing may.
|
||||
# 🔴 AND THE VERDICT DOES NOT COME FROM THE FILMSTRIP ANY MORE. It used to
|
||||
# measure ink spans from `--film` frames. The boot's black hold is 0.17-0.23 s
|
||||
# (HANDOFF Q7) -- shorter than the 0.25 s cadence meant to observe it -- so when
|
||||
# the black frame fell between samples two screens merged into one span and this
|
||||
# tool reported `developer logos` as 93 s against an oracle of 3.5 s. Filming at
|
||||
# 0.1 s made it WORSE: 2.5x the screenshots slows the run enough that the capture
|
||||
# catches up in bursts, and the publisher span came back as 7.80 s.
|
||||
#
|
||||
# The sequencer already knows exactly when it changed screens and prints it.
|
||||
# Sampling a picture to rediscover a number the program can state is how this
|
||||
# went wrong. The filmstrip is kept, and marked advisory.
|
||||
#
|
||||
# THE EXPECTED NUMBERS ARE THE ORACLE'S, NOT THE PORT'S: three cold boots from
|
||||
# `docs/re/boot-order-and-splash-dwell.md`, quoted as a test fixture. Nothing in
|
||||
# the port derives them and nothing may.
|
||||
set -euo pipefail
|
||||
cd "${PROJECT_DIR:-/work}"
|
||||
export DISPLAY="${DISPLAY:-:97}"
|
||||
OUT="${OUT:-$(mktemp -d)}"
|
||||
mkdir -p "$OUT"
|
||||
INTERVAL="${INTERVAL:-0.25}"
|
||||
|
||||
echo "running the boot (the intro is skipped -- the splashes are what this measures)"
|
||||
timeout "${TIMEOUT:-200}" godot --path port --resolution 1280x720 -- \
|
||||
--boot --skip-at=1 "--film=$OUT/f" >"$OUT/boot.log" 2>&1 || true
|
||||
timeout "${TIMEOUT:-300}" godot --path port --resolution 1280x720 -- \
|
||||
--boot --skip-at=1 "--film-interval=$INTERVAL" "--film=$OUT/f" \
|
||||
>"$OUT/boot.log" 2>&1 || true
|
||||
|
||||
python3 - "$OUT" <<'PYEOF'
|
||||
import glob, os, subprocess, sys
|
||||
INTERVAL="$INTERVAL" python3 - "$OUT" <<'PYEOF'
|
||||
import glob, os, re, subprocess, sys
|
||||
out = sys.argv[1]
|
||||
frames = sorted(glob.glob(os.path.join(out, "f_*.png")))
|
||||
if not frames:
|
||||
print("no frames -- is there a display? see", os.path.join(out, "boot.log")); raise SystemExit(2)
|
||||
INTERVAL = 0.25 # boot.gd's --film cadence
|
||||
means = []
|
||||
for f in frames:
|
||||
m = subprocess.run(["convert", f, "-colorspace", "Gray", "-format",
|
||||
"%[fx:mean*255]", "info:"], capture_output=True, text=True)
|
||||
means.append(float(m.stdout.strip() or 0))
|
||||
INTERVAL = float(os.environ.get("INTERVAL", "0.25"))
|
||||
HOLD_LO, HOLD_HI = 0.17, 0.23 # HANDOFF Q7, measured
|
||||
|
||||
# A frame is "ink" if anything at all is drawn. The splashes are dim -- surface
|
||||
# means of 5/255 -- so the bar is ">0", not a percentage of full scale.
|
||||
ink = [m > 0.0 for m in means]
|
||||
spans, i = [], 0
|
||||
while i < len(ink):
|
||||
if ink[i]:
|
||||
j = i
|
||||
while j < len(ink) and ink[j]: j += 1
|
||||
spans.append((i * INTERVAL, j * INTERVAL))
|
||||
i = j
|
||||
else:
|
||||
i += 1
|
||||
marks = []
|
||||
for line in open(os.path.join(out, "boot.log"), errors="replace"):
|
||||
m = re.match(r"\s+-> (\S+) at ([0-9.]+) s", line)
|
||||
if m:
|
||||
marks.append((m.group(1), float(m.group(2))))
|
||||
if not marks:
|
||||
print("no transitions in the boot log -- see", os.path.join(out, "boot.log"))
|
||||
raise SystemExit(2)
|
||||
|
||||
# The oracle: three cold boots, first-visible to last-visible.
|
||||
# docs/re/boot-order-and-splash-dwell.md. NOT the port's numbers.
|
||||
ORACLE = [
|
||||
("publisher wordmark", [4.297, 4.604, 4.370]),
|
||||
("developer logos", [3.508, 3.503, 3.366]),
|
||||
]
|
||||
starts = [0.0] + [t for _, t in marks]
|
||||
print()
|
||||
print("%-20s %-12s %-26s %s" % ("screen", "port", "oracle (3 cold boots)", "verdict"))
|
||||
print("%-20s %-14s %-26s %s" % ("screen", "port interval", "oracle span (3 boots)", "verdict"))
|
||||
bad = 0
|
||||
for k, (name, runs) in enumerate(ORACLE):
|
||||
if k >= len(spans):
|
||||
print("%-20s %-12s %-26s no such span in this run" % (name, "-", ""))
|
||||
continue
|
||||
a, b = spans[k]
|
||||
d = b - a
|
||||
lo, hi = min(runs), max(runs)
|
||||
# Inside the oracle's own run-to-run spread, or within one film interval of
|
||||
# it. The spread is the honest bar: three boots of the real game differ by
|
||||
# 0.3 s, so agreeing more tightly than that would not mean anything.
|
||||
ok = (lo - INTERVAL) <= d <= (hi + INTERVAL)
|
||||
print("%-20s %-12s %-26s %s"
|
||||
if k + 1 >= len(starts):
|
||||
print("%-20s %-14s %s" % (name, "-", "no such transition this run")); continue
|
||||
d = starts[k + 1] - starts[k]
|
||||
lo, hi = min(runs) + HOLD_LO, max(runs) + HOLD_HI
|
||||
ok = lo - 0.15 <= d <= hi + 0.15
|
||||
bad += 0 if ok else 1
|
||||
print("%-20s %-14s %-26s %s"
|
||||
% (name, "%.2f s" % d, "%.3f / %.3f / %.3f" % tuple(runs),
|
||||
"agrees" if ok else "DIFFERS"))
|
||||
print()
|
||||
print("port spans (first ink -> last ink), +/- %.2f s from the film cadence:" % INTERVAL)
|
||||
for a, b in spans[:6]:
|
||||
print(" %6.2f - %6.2f s (%.2f s)" % (a, b, b - a))
|
||||
print()
|
||||
print("⚠️ Read this beside the transition timestamps in the boot log, never")
|
||||
print(" instead of them -- they differ by the exit ramp plus the black hold.")
|
||||
print(" An interval is the oracle's SPAN plus the black hold (%.2f-%.2f s)," % (HOLD_LO, HOLD_HI))
|
||||
print(" compared against span+hold with 0.15 s of slack for wall-clock jitter.")
|
||||
print(" transitions:", ", ".join("%s@%.2f" % m for m in marks[:4]))
|
||||
|
||||
frames = sorted(glob.glob(os.path.join(out, "f_*.png")))[:120]
|
||||
if frames:
|
||||
means = [float(subprocess.run(["convert", f, "-colorspace", "Gray", "-format",
|
||||
"%[fx:mean*255]", "info:"], capture_output=True, text=True).stdout or 0)
|
||||
for f in frames]
|
||||
ink = [m > 0.0 for m in means]
|
||||
spans, i = [], 0
|
||||
while i < len(ink):
|
||||
if ink[i]:
|
||||
j = i
|
||||
while j < len(ink) and ink[j]: j += 1
|
||||
spans.append((i * INTERVAL, j * INTERVAL)); i = j
|
||||
else:
|
||||
i += 1
|
||||
print()
|
||||
print(" advisory -- filmstrip ink spans at %.2f s, which CANNOT resolve a" % INTERVAL)
|
||||
print(" %.2f-%.2f s hold and merges screens whenever it misses one:" % (HOLD_LO, HOLD_HI))
|
||||
for a, b in spans[:4]:
|
||||
print(" %6.2f - %6.2f s (%.2f s)" % (a, b, b - a))
|
||||
raise SystemExit(1 if bad else 0)
|
||||
PYEOF
|
||||
rc=$?
|
||||
echo "artifacts in $OUT"
|
||||
exit $rc
|
||||
|
||||
Reference in New Issue
Block a user