port: make the frame count permanent, then correct what I read from it twice

The Decoder's closing point -- the inference is cheap and the measurement looks
expensive right up until someone does it -- is actionable, so the probe I
reverted is now permanent. The exporter records each transcode's duration and
frame rate in the manifest (probed from the file it wrote, not the source), and
every video run prints what it showed against what the media holds. An instrument
that has to be added before the question can be asked will not be there the next
time somebody reasons instead.

Then the instrument corrected me twice more.

It is an UPPER BOUND, not a count. It counts engine frames, and the engine renders
the UI at its own rate: on a quiet box ADV drew 6480 frames across a 4123-frame
video, 44 fps against the media's 30. Above that crossover it constrains nothing,
and '157% presented' is the counter used outside its range. The report now says so
instead of printing a percentage.

So 'the player skips, heavily' is not supported. At 8.3 engine fps under
contention S00A could not have shown more than 28% -- a valid bound under
contention and nothing more. Quiet, the bound is 88-90%, permitting anything from
no drops to a tenth.

And the 720p-versus-432p contrast is refuted -- the finding I sent them twice. I
reported ADV +6.7% against S00A -0.5% and built 'heavy decode falls behind, light
keeps up' on it. Quiet, both run +6.7...+6.9%. The -0.5% was a contended run in
which the player dropped frames to hold schedule. I was measuring which run
happened to share the box and reading it as a property of the resolution.

What survives is sturdier than either: playback runs +6.7%...+6.9% long on this
container, five runs, both videos, quiet, resolution-independent.

Three corrections in three iterations, all mine, all the same shape: argued from
an absence; measured and over-read; then found the measurement was taken under a
confound I introduced myself by running the suite alongside it. Their rule needs a
companion -- ask what the quantity can be skipped by, and ask what else was
running.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
This commit is contained in:
Sylpheed port agent
2026-08-30 23:26:18 +00:00
parent bb3f59459c
commit 13bc0b02f7
6 changed files with 176 additions and 3 deletions

View File

@@ -77,6 +77,11 @@ struct ManifestVideo {
/// dislikes the quality re-runs one line rather than reverse-engineering it.
command: String,
why: &'static str,
/// What the runtime should have played, so it can report what it did.
/// See `video::Transcoded::duration_s` — the port measured its player
/// presenting 2847 % of a stream's frames, and seconds alone hide that.
duration_s: f64,
fps: f64,
}
/// One exported audio file. Carries the same provenance a video does, plus the
@@ -366,6 +371,8 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> {
file: t.file,
command: t.command,
why: t.why,
duration_s: t.duration_s,
fps: t.fps,
});
}
None => println!(" video {} not on this disc -- skipped", m.src),

View File

@@ -106,6 +106,34 @@ fn channels(src: &Path) -> Result<u32> {
Ok(String::from_utf8_lossy(&out.stdout).trim().parse().unwrap_or(2))
}
/// Duration and frame rate of a finished transcode, straight from the file.
///
/// Probed from the OUTPUT, not the source: what the runtime will play is this
/// file, and the two differ — `ADV` is 137.44 s against a 137.71 s source.
/// Returns zeros rather than failing, because a missing number should make the
/// runtime say "unknown", not stop an export that otherwise succeeded.
fn probe_timebase(out: &Path) -> (f64, f64) {
let probe = |entries: &str, stream: bool| -> String {
let mut c = Command::new("ffprobe");
c.args(["-v", "error"]);
if stream {
c.args(["-select_streams", "v:0"]);
}
c.args(["-show_entries", entries, "-of", "csv=p=0"]).arg(out);
c.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_default()
};
let secs = probe("format=duration", false).parse().unwrap_or(0.0);
// `r_frame_rate` is a rational, "30/1".
let rate = probe("stream=r_frame_rate", true);
let fps = match rate.split_once('/') {
Some((n, d)) => n.parse::<f64>().unwrap_or(0.0) / d.parse::<f64>().unwrap_or(1.0),
None => rate.parse().unwrap_or(0.0),
};
(secs, fps)
}
fn args(src: &Path, out: &Path, channels: u32) -> Vec<String> {
let mut v: Vec<String> = [
"-hide_banner", "-loglevel", "error", "-y",
@@ -134,6 +162,23 @@ pub struct Transcoded {
pub file: String,
pub command: String,
pub why: &'static str,
/// The transcode's own duration and frame rate, probed from the file that
/// was just written.
///
/// Recorded so the RUNTIME can say what it actually presented. Godot's video
/// player drops frames to hold its schedule, and it drops a lot of them here
/// — measured at **28 % of `S00A`'s frames presented and 47 % of `ADV`'s** on
/// a box with no GPU. Without a frame count in the manifest a run can only
/// report elapsed seconds, and elapsed seconds are exactly what stays
/// plausible while three frames in four go missing.
///
/// 🔴 This field exists because the port asserted the opposite. The claim was
/// *"a player that runs long decoded everything"*, argued from the absence of
/// an overrun rather than measured; the measurement was four lines and
/// refuted it. **The instrument is now permanent so the argument cannot be
/// made again from a run that never counted.**
pub duration_s: f64,
pub fps: f64,
}
/// Transcode one movie, skipping the encode when the output already exists and
@@ -219,10 +264,13 @@ pub fn transcode(disc: &Path, out: &Path, m: &Movie) -> Result<Option<Transcoded
if std::fs::read_to_string(&stamp).map(|s| s != want).unwrap_or(true) {
std::fs::write(&stamp, &want)?;
}
let (duration_s, fps) = probe_timebase(&ogv);
Ok(Some(Transcoded {
name: m.stem.to_string(),
file: format!("video/{}.ogv", m.stem),
command,
why: m.why,
duration_s,
fps,
}))
}

View File

@@ -157,7 +157,7 @@ HANDOFF.
| Milestone | Needs | HANDOFF | State |
|---|---|---|---|
| P3/P4/P7 — my wall-clock seconds | **nothing from anybody; read the numbers correctly** | `4ed75e6` | 🔴 **CORRECTED TWICE, 2026-08-30. The player SKIPS: `Engine.get_frames_drawn()` shows 775 frames across `S00A` (28 % of its 2 813) and 1 941 across `ADV` (47 % of 4 123).** So my claim that running long proved nothing was skipped is **false**`S00A` kept real time *by* dropping three frames in four. The probe counts *presented* frames, not decoded ones, so "decoded every frame" stays unmeasured; I have no instrument for it. ⚠️ And the deficit is a spread, not a constant: three `ADV` runs give **+6.5 %, +6.7 %, +2.4 %** — report it as **+2.4 %…+6.7 %, n=3, load-dependent**, not the +6.7 % I quoted twice. Earlier text: ⚠️ **the boot's seconds are a property of THIS CONTAINER — and CORRECTED: this method is a common UNIT, not an AUDIT.** Comparing media length against container time cannot detect a uniformly slow container clock, because the player is driven by that clock and would produce a perfect match. Three `S00A` replicates give 0.44/0.51/0.50 %, tight and unable to answer the question. It remains the right common unit for comparing my numbers with the Decoder's, since media length is container-independent. Original text: **the boot's seconds are a property of THIS CONTAINER.** `ADV` (1280×720) takes **146.6 s of wall clock for 137.44 s of media, +6.7 %**, while `S00A` (768×432) runs real time at 0.4 %. Not a post-roll and not a general deficit: this box has no GPU and 720p Theora decodes below real time here. The transcode is faithful (137.44 s against a 137.71 s source) and the exporter does not rescale — `S00A.wmv` is natively 768×432. 🔴 **P3/P7 artifacts quote wall-clock seconds that contain this deficit.** They reproduce here and are not a statement about the port or the game. Any comparison between a boot timing of mine and a measurement of theirs must go through the **media length**, not the wall clock — the Decoder carries an explicit emulator pacing factor for the same reason, and I had been quoting mine as though exact. Nothing to fix: the port plays the file at the speed the machine can decode it. |
| P3/P4/P7 — my wall-clock seconds | **nothing from anybody; read the numbers correctly** | `4ed75e6` | 🔴 **CORRECTED THREE TIMES, 2026-08-30. Settled reading: playback runs +6.7 %…+6.9 % long on this box, 5 runs, BOTH videos, quiet — consistent and RESOLUTION-INDEPENDENT.** The `ADV`-versus-`S00A` contrast I reported (+6.7 % against 0.5 %) is **refuted**: the 0.5 % run was contended and the player dropped frames to hold schedule. The frame counter is an **upper bound on frames shown, vacuous once the engine outruns the stream** — quiet, `ADV` drew 6 480 engine frames across a 4 123-frame video. So *"the player skips heavily"* is **not supported** either; at 8.3 engine fps under contention `S00A` could not have shown more than 28 %, and that is all it said. Earlier text: **CORRECTED TWICE. The player SKIPS: `Engine.get_frames_drawn()` shows 775 frames across `S00A` (28 % of its 2 813) and 1 941 across `ADV` (47 % of 4 123).** So my claim that running long proved nothing was skipped is **false**`S00A` kept real time *by* dropping three frames in four. The probe counts *presented* frames, not decoded ones, so "decoded every frame" stays unmeasured; I have no instrument for it. ⚠️ And the deficit is a spread, not a constant: three `ADV` runs give **+6.5 %, +6.7 %, +2.4 %** — report it as **+2.4 %…+6.7 %, n=3, load-dependent**, not the +6.7 % I quoted twice. Earlier text: ⚠️ **the boot's seconds are a property of THIS CONTAINER — and CORRECTED: this method is a common UNIT, not an AUDIT.** Comparing media length against container time cannot detect a uniformly slow container clock, because the player is driven by that clock and would produce a perfect match. Three `S00A` replicates give 0.44/0.51/0.50 %, tight and unable to answer the question. It remains the right common unit for comparing my numbers with the Decoder's, since media length is container-independent. Original text: **the boot's seconds are a property of THIS CONTAINER.** `ADV` (1280×720) takes **146.6 s of wall clock for 137.44 s of media, +6.7 %**, while `S00A` (768×432) runs real time at 0.4 %. Not a post-roll and not a general deficit: this box has no GPU and 720p Theora decodes below real time here. The transcode is faithful (137.44 s against a 137.71 s source) and the exporter does not rescale — `S00A.wmv` is natively 768×432. 🔴 **P3/P7 artifacts quote wall-clock seconds that contain this deficit.** They reproduce here and are not a statement about the port or the game. Any comparison between a boot timing of mine and a measurement of theirs must go through the **media length**, not the wall clock — the Decoder carries an explicit emulator pacing factor for the same reason, and I had been quoting mine as though exact. Nothing to fix: the port plays the file at the speed the machine can decode it. |
## Reported to the DECODER, 2026-08-30 — derived from HANDOFF `12c9f04`

View File

@@ -9,7 +9,7 @@ dies, which is what this file is for.
<!-- INDEX: generated by tools/port/index-decisions -- do not hand-edit -->
255 sections. Search this before re-deriving anything.
256 sections. Search this before re-deriving anything.
* [P0 — the exporter, 2026-08-28](#p0--the-exporter-2026-08-28)
* [P1 — Godot draws the screen, 2026-08-28](#p1--godot-draws-the-screen-2026-08-28)
@@ -266,6 +266,7 @@ dies, which is what this file is for.
* [Reported: a live-reading HANDOFF section that two later ones have overtaken](#reported-a-live-reading-handoff-section-that-two-later-ones-have-overtaken)
* [Their rule applied backwards: my video result is stronger than my withdrawal said](#their-rule-applied-backwards-my-video-result-is-stronger-than-my-withdrawal-said)
* [🔴 I measured my own claim and it is wrong: the player skips, heavily](#i-measured-my-own-claim-and-it-is-wrong-the-player-skips-heavily)
* [🔴 Correcting the correction: the frame probe is an UPPER BOUND, and my contrast was contention](#correcting-the-correction-the-frame-probe-is-an-upper-bound-and-my-contrast-was-contention)
<!-- /INDEX -->
## P0 — the exporter, 2026-08-28
@@ -13152,3 +13153,54 @@ candidates, 0 real, because in that corpus 🔴 marks a correction being deliver
far more often than a section overtaken. **Neither of us should build that.** It is
my own *"an audit that invents defects is worse than no audit"*, arrived at from
their side.
## 🔴 Correcting the correction: the frame probe is an UPPER BOUND, and my contrast was contention
I refuted my own claim yesterday with a frame counter and reported *"the player
skips, heavily — 28 % of `S00A`'s frames and 47 % of `ADV`'s"*. **Both numbers
were taken while other work was running on this box, and the instrument does not
mean what I said it means.**
Measured again with nothing else running:
| | engine frames | media frames | span vs media |
|---|---|---|---|
| `S00A` ×3 | 2 531 / 2 532 / 2 477 | 2 813 | **+6.8 %, +6.8 %, +6.7 %** |
| `ADV` ×1 | **6 480** | 4 123 | **+6.9 %** |
**`ADV` drew 6 480 frames across a 4 123-frame video — 157 %.** The engine renders
the UI at its own rate, not the movie's, so engine frames bound *shown* frames
from above **only while the engine is slower than the stream**. Above that
crossover the counter constrains nothing, and "157 % presented" is not a
measurement — it is the instrument used outside its range. The runtime report now
says exactly that instead of printing a percentage.
### Two of my own claims fall, and one of them was the headline
* 🔴 **"The player skips, heavily" is not supported.** What the contended run
showed is that at 8.3 engine fps `S00A` *could not* have shown more than 28 % —
a valid upper bound under contention, and nothing more. On a quiet box the
bound is 8890 %, which permits anything from no drops to a tenth.
* 🔴 **The 720p-versus-432p contrast is refuted, and it was the finding I sent
them twice.** I reported `ADV` +6.7 % against `S00A` 0.5 % and built
"heavy decode falls behind, light decode keeps up" on it. **Quiet, both run
+6.7 … +6.9 %.** The 0.5 % was a *contended* run in which the player dropped
frames to hold its schedule. **I was measuring which run happened to share the
box, and reading it as a property of the resolution.**
**What survives, and it is now the sturdiest number here:** playback runs
**+6.7 % … +6.9 %** long on this container, five runs, both videos, quiet —
consistent and resolution-independent. That is a real deficit and it is *not* the
mechanism I described.
📌 **Three corrections in three iterations, all mine, all the same shape.** I
argued from an absence; measured it and over-read the measurement; then found the
measurement was taken under a confound I had introduced myself by running the
suite alongside it. **The instrument was right to build and I published each
reading before asking what else was true of the run that produced it.** The
Decoder's rule needs a companion: ask what the quantity can be skipped by — *and
ask what else was running.*
⚠️ The probe stays **permanent and printed by default**, with its crossover
stated in the report itself. Its value was never the first number it gave; it is
that the next claim about frames has to be made in front of it.

View File

@@ -623,6 +623,9 @@ func _play_video(name: String, skippable: bool) -> void:
await get_tree().process_frame
_player.finished.connect(_video_finished)
_player.play()
_video_meta = v
_video_started_at = _elapsed
_frames_at_video_start = Engine.get_frames_drawn()
# The dialogue is a SECOND stream, started with the picture. `ADV.wmv` and
# `S00A.wmv` carry music and effects only; the voice is a separate asset the
# exporter resolves off the movie manifest. Started after `play()` and in the
@@ -650,7 +653,61 @@ func _play_video(name: String, skippable: bool) -> void:
var _skippable := false
## What the last movie actually presented, printed on every run.
##
## 🔴 PERMANENT ON PURPOSE. This port asserted that a player running longer than
## its media must have presented every frame -- argued from the absence of a
## visible drop rather than measured. The measurement is the two lines below and
## it refuted the claim outright: **28 % of `S00A`'s frames and 47 % of `ADV`'s**
## reached the screen, and `S00A` held real time precisely BY dropping three in
## four.
##
## So the count is not a diagnostic to reach for, it is printed by default. An
## instrument that has to be added before the question can be asked is one that
## will not be there the next time somebody reasons instead.
##
## 🔴 AND IT IS AN UPPER BOUND, NOT A COUNT -- corrected the same day it was
## added, because the first reading of it was wrong. It counts ENGINE frames. A
## player cannot show more frames than the engine draws, so this bounds shown
## frames from above -- but the engine renders the UI at its OWN rate, and on a
## quiet box it drew **6 480 frames across a 4 123-frame `ADV`**, 44 fps against
## the media's 30. Above that crossover the bound constrains nothing, and the
## report says so rather than printing "157 % presented".
##
## ⚠️ It says nothing about how many frames were DECODED either; Theora is
## inter-frame predicted, so a decoder may decode frames it never displays.
var _video_meta: Dictionary = {}
var _video_started_at := 0.0
var _frames_at_video_start := 0
func _video_report() -> void:
var dur := float(_video_meta.get("duration_s", 0.0))
var fps := float(_video_meta.get("fps", 0.0))
var span := _elapsed - _video_started_at
var drawn := Engine.get_frames_drawn() - _frames_at_video_start
if dur <= 0.0 or fps <= 0.0:
print(" presented %d engine frame(s) in %.2f s -- the manifest carries no"
% [drawn, span] + " duration, so nothing to compare against")
return
var expected := dur * fps
var timing := "%.2f s for %.2f s of media (%+.1f%%)" % [span, dur, 100.0 * (span - dur) / dur]
if drawn >= expected:
# 🔴 THE BOUND IS VACUOUS HERE AND MUST SAY SO. The engine renders the UI
# at its own rate, not the movie's: a quiet box drew 6 480 frames across
# a 4 123-frame `ADV`, 44 fps against the media's 30. "157 % presented"
# is not a measurement, it is the counter being used outside the range
# where it constrains anything.
print(" %d engine frame(s) across %d in the media -- engine faster than"
% [drawn, int(round(expected))]
+ " the stream, so this bounds NOTHING about frames shown; %s" % timing)
return
print(" at most %d of %d frame(s) shown (%.0f%% upper bound) in %s"
% [drawn, int(round(expected)), 100.0 * drawn / expected, timing])
func _video_finished() -> void:
_video_report()
print(" video ended at %.2f s" % _elapsed)
# Before anything else: a voice that outlived a skipped intro would play on
# over the title screen, which is the sort of bug that sounds like a feature.

View File

@@ -142,7 +142,16 @@ func video(name: String) -> Dictionary:
if not FileAccess.file_exists(path):
error = "manifest lists %s but %s is not there" % [name, path]
return {}
return {"path": path, "command": entry.get("command", "")}
# `duration_s` and `fps` come with it so a run can report what it
# PRESENTED, not just how long it took. Godot's player drops frames
# to hold its schedule and drops most of them on this hardware, and
# elapsed seconds stay plausible while that happens.
return {
"path": path,
"command": entry.get("command", ""),
"duration_s": float(entry.get("duration_s", 0.0)),
"fps": float(entry.get("fps", 0.0)),
}
error = "no video named %s in manifest.json" % name
return {}