From c225852fb3828910b5e9c1b3555945ad8ed016b8 Mon Sep 17 00:00:00 2001 From: Sylpheed port agent Date: Sat, 29 Aug 2026 12:50:35 +0000 Subject: [PATCH] port: the ring spins, the plate needs no constant, and rest.t was never the settle Two milestones' known-wrong bits, both now answered by the RE agent, both taken. P5 -- the focus ring. It was drawn at 0 with a comment saying so. The period is now measured (continuous spin, eight evenly spaced autocorrelation peaks over nine revolutions, no angle estimated anywhere) and it needs NO authored constant: the period is the element's own declared t=120, and what the measurement adds is only that the turn repeats rather than stopping -- which "groups hold" could not decide, because 0 and 360 are the same pose. `spin_period_units` is structural and narrow on purpose: two keyframes, differing in nothing but rotation_deg, by a full 360, first timed and second untimed. 16 of 212 elements in this export match and all 16 are focus rings, zero false positives. That check is the point -- the measurement was taken on ONE button of ONE screen, and a rule that caught anything else would be extrapolating it to elements nobody watched. Verified on the port's own render with the RE agent's own control: bit-identical one period apart across the whole frame, 3.6/255 inside the ring's box at quarter-period steps, and box luminance conserved to 0.027 % over eight phases -- which is the observable they used to separate rotation from a pulse. Not claimed: direction (no signed angle was ever measured) and phase across a focus change (their run held focus throughout). P3 -- the plate. Last iteration I refuted their authoring instruction and shipped it anyway rather than pick between two of their numbers. The refutation held and the answer came back better than either option I offered: AUTHOR NOTHING. Both builds run on one clock started together and the plate arrives at its own declared t=238. The 2.13 s constant is deleted. The premise that failed was mine: rest.t IS NOT WHEN A SCREEN SETTLES. It is the last hold keyframe before the exit. ptlogo1 stops MOVING at t=42 and then creeps 5 px and 31 alpha steps to t=251. Reading rest.t put build 4's arrival at 4.350 s instead of 1.967 s, and the "2.51 s, which is not a landmark of anything" I sent them is that error wearing a decimal point. 238 - 118 = 120 units = 2.000 s against a measured 2.135 s at 28.1 fps presentation. Checked against my own export before touching anything. `ScreenView.settle_time()` still uses rest.t, and so the boot sequencer paces every screen off the wrong landmark. NOT changed here: "visible arrival" is a heuristic and getting it wrong re-paces everything. Filed, and asked for a timed boot instead now that their oracle is live. REFUTATION: two of their pages measure the same declared 120 units of wall clock during a static hold and disagree by 2 % -- plate 2.135 s (28.10 fps implied), ring 2.177 s (27.56 fps). That is seven times the plate page's own 6 ms run-to-run agreement, and it lands on the argument that page uses to justify itself: "the build-in is where frames are dropped; the static hold is not". Also the ring page's band, 27.6-28.8 fps, does not contain its own measurement -- the mean needs 27.56 and four of seven spacings are outside. Filed, not worked around: my port uses the declared 120 units either way. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01WM5XL4HfrHuxz8RiMWdCMC --- authored/flow.json | 13 ++-- docs/port/BLOCKED.md | 7 +- docs/port/DECISIONS.md | 151 ++++++++++++++++++++++++++++++++++++ port/scripts/boot.gd | 69 +++++++++------- port/scripts/screen_view.gd | 70 +++++++++++++---- 5 files changed, 261 insertions(+), 49 deletions(-) diff --git a/authored/flow.json b/authored/flow.json index f8e84e6c..752ddfa8 100644 --- a/authored/flow.json +++ b/authored/flow.json @@ -31,12 +31,13 @@ "screen": "title", "overlay": { "screen": "press_start", - "after_settle_seconds": 2.13, - "why": "MEASURED, 2026-08-29, docs/re/title-plate-delay-measured.md on branch auto/no-disc-and-menu-captures at fb536df (NOT yet on main when this was written). The boot title shows build 4 ALONE, and the `PRESS (A) BUTTON` plate -- build 2 -- is composited over it 2.13 s later. Two independent boots of the real game agree to 6 ms. This is the ONE case in the port where two builds are drawn at once.", - "measured_from_why": "The 2.13 s is timed from the frame build 4's own build-in animation FINISHES, not from the frame the title art first appears. That distinction is the finding, not a detail: measured the other way the two runs differ by 0.48 s, because the build-in itself ran 1.64 s and 2.13 s and the emulator's frame pacing during an animation is not the game's clock. A number taken from the wrong instant here looks exactly like a measurement.", - "not_a_fade_in_why": "The alternative -- that the plate was there all along, pulsing too dim to see -- was tested and refuted on two observables. The plate's declared alpha never exceeds 0x50, so a thresholded glyph counter could in principle miss a dim phase; it does not. The count sits at EXACTLY 154 with zero variation for 1.99 s / 2.13 s and then swings 714-1520, and the surface mean is flat to +/-0.03 across the plateau. A pulsing overlay moves the frame mean.", - "scope_why": "Attached to the BOOT STEP, not to the `title` screen, and that is deliberate. What was measured is the boot title. Whether the title shows the plate when it is REACHED AGAIN -- by (B) from the main menu, or after the attract movie -- is not measured, and putting the overlay on the screen would quietly claim it is. See docs/port/BLOCKED.md.", - "no_pulse_why": "The port draws the plate arriving and then HOLDING VISIBLE. It does not pulse it. The plate's own group encodes exactly one cycle -- alpha 0x00 at t=214, 0xff at t=238-244, 0x00 on the final untimed keyframe -- and the port holds at the settle (t=238, alpha 0xff) as it does on every other screen. The running game was measured pulsing continuously at peak intervals averaging 2.24 s, and NO reading of this group produces 2.24 s: looping the whole group gives 268 units = 4.47 s, looping from the first keyframe gives 54 units = 0.90 s. So repeating the group here would be inventing a period, and the port does not. Filed in BLOCKED.md." + "clock": "shared", + "why": "MEASURED, 2026-08-29, docs/re/title-plate-delay-measured.md on branch auto/no-disc-and-menu-captures at 5b0a6e6 (NOT on main when this was written). The boot title shows build 4 ALONE and the `PRESS (A) BUTTON` plate -- build 2 -- arrives later. This is the ONE case in the port where two builds are drawn at once.", + "no_constant_why": "THERE IS NO AUTHORED DELAY HERE, AND THERE WAS ONE FOR ONE ITERATION. The first version of this block carried `after_settle_seconds: 2.13`, taken from the RE agent's instruction. The port refuted that instruction with arithmetic off the disc -- build 2 has a group of its own, and starting it at settle put the plate 3.97 s late -- and the corrected answer needs no constant at all: BOTH BUILDS RUN ON ONE CLOCK, STARTED TOGETHER, and the plate arrives at its own declared t=238. `clock: \"shared\"` is that, spelled out rather than implied by the absence of a delay field.", + "arithmetic_why": "Why one clock reproduces the measurement, checked against this export rather than taken on trust: build 4's visible build-in ends at t=118, where `pteff01`, `pteff02` and `ptlogoall_eff` finish together; `ptbtn00` reaches alpha 255 at t=238; the difference is 120 units = 2.000 s at 60 units/s. The oracle measured 2.138 s and 2.132 s. The gap is presentation rate: 120 units in 2.135 s is 56.2 units/s, i.e. the emulator presenting at 28.1 fps against a nominal 30, and the corpus had independently measured the idle title at 28.5 fps before these runs.", + "the_premise_that_failed_why": "The port's own, and it is worth keeping because it will bite again: `rest.t` IS NOT WHEN A SCREEN SETTLES. It is the last hold keyframe before the exit. `ptlogo1` has rest.t=251 and stops MOVING at t=42, then creeps 5 px and 31 alpha steps over the next 209 units. Reading rest.t as the settle put build 4's arrival at 4.35 s instead of 1.97 s, and every reconciliation computed from it came out wrong by exactly that error. `ScreenView.settle_time()` still uses rest.t -- see docs/port/BLOCKED.md.", + "scope_why": "Attached to the BOOT STEP, not to the `title` screen, and that is deliberate. What was measured is the boot title. Whether the title shows the plate when it is REACHED AGAIN -- by (B) from the main menu, or after the attract movie -- is not measured, and putting the overlay on the screen would quietly claim it is.", + "no_pulse_why": "The port draws the plate arriving and then holding. It does not pulse it. The RE agent identifies the pulse as the plate's FOCUS RECORD `ptbtn00f` -- a glow ramping 0x00 to 0x50 and back, t=6..105 -- not as a loop of `ptbtn00`'s own group, which was the port's earlier reading and was wrong. Looping that record is a candidate the port has NOT taken: its group is 105 timed units plus an AUTHORED 24-unit exit ramp, and hitting the measured 2.24 s mean requires composing that authored constant with a loop assumption, which is tuning rather than measuring. Filed in BLOCKED.md." }, "why": "HANDOFF Q2/Q6: the boot reaches the title after the intro movie. This is the LAST step, and a last step is where the sequence stops rather than fading out -- a boot that ends by fading to black looks like a boot that crashed. P5 gave the title somewhere to go, but that is a HANDOVER and not another boot step: `--boot` still stops here, and `--boot --play` hands the same held title to the menu flow, where (A) opens TITLE_MENU. Kept as a stop rather than folded into `screens` because what the boot does is authored from a measured sequence, and what (A) does is a separate measurement." } diff --git a/docs/port/BLOCKED.md b/docs/port/BLOCKED.md index 0736e5da..b309c6c1 100644 --- a/docs/port/BLOCKED.md +++ b/docs/port/BLOCKED.md @@ -83,11 +83,12 @@ git log -1 --format=%h -- docs/port/HANDOFF.md # newer than 9ca1eb5? re-reconc | P5 โ€” โ’ท on the main menu | **is โ’ท what returns to the title, or the idle timer?** | Q5 | ๐ŸŸก stated in HANDOFF, no capture behind it. The title self-returns after ~8โ€“10 s idle, so one unrecorded observation cannot separate them. `authored/flow.json` implements it and marks it *authored โ€” likely but UNPROVEN*. **Not blocking** โ€” P5 shipped with it โ€” but it is the only navigation rule on that screen with nothing under it. Settled by one run that presses โ’ท well inside the idle window, timestamped. | | P6 BGM โ€” the sub-wave count | **is a music bank's LEADING REGION a stem, or a decoder artefact?** | Q10 | ๐Ÿ”ด **HANDOFF and the decoders disagree, and P6 ships the disagreement.** `media::sound_bank_riffs("BGM_103.slb")` returns **three** sub-waves; HANDOFF Q10's census says a music bank is *"exactly two waves of identical duration (32/32 banks on the disc)"*. The third comes from `slb.rs:380` `to_xma_riffs`, whose hybrid branch emits a leading headerless packet region ahead of the `RIFF` waves โ€” and `docs/re/REFUTED.md` already records that region as what makes `BGM_106`โ€“`BGM_109` *"break the two-wave rule"*. Derived at HANDOFF `9ca1eb5`. **The exporter sums all three and writes a manifest warning**, because choosing which sub-wave to drop is a decoding question and MISSION ยง2 forbids this exporter answering one. So the menu currently plays a sum of three things where the census predicts two. What settles it: whether that leading region carries music. Raised with the Decoder 2026-08-29. | -| P3 โ€” the plate's ONSET | **does the plate become visible 2.13 s after settle, or does its GROUP START then?** | Q2 | ๐Ÿ”ด **new, and it is a disagreement between two of the Decoder's own numbers.** `docs/re/title-plate-delay-measured.md` (`auto/no-disc-and-menu-captures` at `fb536df`, **not on `main`**) measures the plate *becoming visible* at settle + **2.13 s**, and instructs the port to *"wait 2.13 s, composite build 2"*. Those are 3.97 s apart, because build 2 has a group of its own and the port plays it: `press_start`'s `ptbtn00` is alpha `0x00` at t=214, `0x00` at t=236 (it slides y 560โ†’550 invisible), `0xff` at t=238 โ€” **3.967 s** from group start to full alpha at 60 u/s. Neither landmark reconciles them: both groups starting together puts full alpha 0.38 s *before* build 4 settles (`title` settles at t=261 = 4.350 s); build 2 starting at settle puts it at settle + 3.97 s. Landing on settle + 2.13 s needs build 2's group to start 2.51 s after build 4's, which is neither. **The port ships the instruction, not its own arithmetic**, prints the discrepancy on every boot, and is not choosing. First thing to check, and it is about the instrument rather than the game: is *"title settled"* (the glyph counter first reading 154) the same instant as the port's last-element settle? Raised 2026-08-29. | +| ~~P3 โ€” the plate's ONSET~~ | ~~visible 2.13 s after settle, or group starts then?~~ | Q2 | โœ… **resolved 2026-08-29, and the answer is AUTHOR NOTHING.** The port's refutation held and produced a better answer than either option it offered. Correction at `5b0a6e6` on `auto/no-disc-and-menu-captures`: **both builds run on one clock, started together**, and the plate arrives at its own declared `t=238`. Checked against this export rather than taken on trust โ€” build 4's visible build-in ends at `t=118` (`pteff01`, `pteff02`, `ptlogoall_eff` finish together), `ptbtn00` reaches alpha 255 at `t=238`, difference **120 units = 2.000 s**, against a measured 2.138 / 2.132 s at an emulator presenting 28.1 fps rather than 30. The 2.13 s constant is **deleted**. | +| P3/P5 โ€” `settle_time()` | **`rest.t` is not when a screen settles, and the port's sequencer uses it** | โ€” | ๐Ÿ”ด **new, mine, and it is the premise that produced the wrong plate arithmetic.** `5b0a6e6` states it plainly: `rest.t` is the last *hold* keyframe before the exit. `title`'s `ptlogo1` has `rest.t=251` and stops **moving** at `t=42`, then creeps 5 px while its alpha crawls `0xe0`โ†’`0xff` over 209 units. `ScreenView.settle_time()` returns the max `rest.t` โ€” **4.350 s** for the title where the visible build-in is over at `t=118` = **1.967 s**. Everything the boot sequencer paces off that landmark is therefore late: `publisher_logo` is held to t=235, `developer_logos` to t=190, and `_script_settled` waits on it too. โš ๏ธ **Not changed this iteration**, because "visible arrival" is a heuristic and getting it wrong re-paces every screen; it needs its own gate. **What settles it outright:** the oracle is live โ€” a timed boot with screen-to-screen transitions would give the real dwells directly. Asked 2026-08-29. | | ~~P3/P5 โ€” the title plate~~ | ~~does the idle title show `PRESS โ’ถ`~~ | Q2 | โœ… **answered and TAKEN at this iteration.** `auto/no-disc-and-menu-captures` at `fb536df`, `docs/re/title-plate-delay-measured.md`, traces in `docs/re/data/plate-timing-run{1,2}.tsv`. It is the third case: build 4 alone, then the plate composited over it. โš ๏ธ The delay is 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. `ScreenView` now draws two builds at once, as a second `ScreenView` in the same `SubViewport` rather than a subordinate screen inside one. The onset question above is what is left. | -| P3 โ€” the plate's PULSE | **what period does the plate pulse at, and which group boundary repeats?** | Q2 | โ” **open, and the port draws no pulse.** The same finding measures the running plate pulsing continuously โ€” peak/trough intervals 2.12 / 2.19 / 2.34 / 2.31 s, mean **2.24 s**. `press_start`'s group encodes exactly one cycle (`0x00` โ†’ `0xff` at t=238โ€“244 โ†’ `0x00` untimed), and **no reading of it produces 2.24 s**: the whole group is 268 units = 4.47 s, and from its first keyframe 54 units = 0.90 s. So repeating the group would be inventing a period. The port holds the plate at its settle (t=238, alpha `0xff`), which is what it does on every other screen and what the static oracle capture `live-title-press-a.png` shows. What settles it: which instant the group restarts from. | -| P5 focus ring โ€” implementation | ~~the ring's spin period~~ | Q1 | โœ… answered (2.177 s, `4fa3099`) and **still not implemented** โ€” the port draws 0ยฐ, a pose the corpus says the game never shows. Not blocked on anybody; it is queued behind P3's plate this iteration. | +| P3 โ€” the plate's PULSE | **does the plate's focus record loop, and with what period?** | Q2 | โ” **open, and the port's earlier reading of it was wrong.** The port had looked for the pulse in `ptbtn00`'s own group; `5b0a6e6` identifies it as the plate's **focus record** `ptbtn00f` โ€” a glow ramping alpha `0x00`โ†’`0x50` and back, t=6โ€ฆ105. Measured on the running game at 2.12 / 2.19 / 2.34 / 2.31 s, mean **2.24 s**. ๐ŸŸก **The port has not taken it.** Looping that record needs a period, and its group is 105 timed units plus the **authored** 24-unit exit ramp = 129 units = 2.15 s โ€” composing an authored constant with a loop assumption to land on a measured number is tuning, not measuring. Separately: the port draws no focus record on `press_start` at all, because the screen has no `buttons` and nothing is focused, so *whether the game always draws it* is its own question. | +| ~~P5 focus ring โ€” implementation~~ | ~~the ring's spin period~~ | Q1 | โœ… **implemented 2026-08-29.** `ScreenView.spin_period_units` drives it: one turn per the element's own declared `t`, looping, from the screen clock. The period comes off the **disc**; what the RE agent supplied is that the turn repeats rather than stopping. Verified on the port's own render โ€” the ring is bit-identical one period apart across the whole frame, differs by 3.6/255 inside its box at quarter-period steps, and conserves box luminance to **0.027 %** over eight phases, which is the same observable the RE agent used to separate rotation from a pulse. ๐ŸŸก **Direction is not measured** โ€” the port turns 0ยฐโ†’+360ยฐ, which is the sign the disc declares, but the RE agent's angle estimators failed their controls and no signed angle was ever taken. ๐ŸŸก **Phase across a focus change is not measured** either: the port drives the ring off the screen clock, so it does not reset when focus moves. Settled by two frames straddling a focus change. | ## Answered since this file was last written โ€” no longer blocking diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index ca90dfed..85d14a4a 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -1750,3 +1750,154 @@ Two things the run made obvious and that are now fixed: of the title". It applies **no** delay: the delay is a measurement and lives in `authored/flow.json`. The boot-mode narration is suppressed there, because a log line that describes a sequence it is not running is worse than no log line. + +## P5 โ€” the focus ring spins, 2026-08-29 + +The ring was drawn at 0ยฐ and the file said so: *"THIS IS KNOWN TO BE WRONG, and +is drawn anyway because the right answer is a guess."* What was missing was the +**period**, and it is now measured โ€” `docs/re/focus-ring-spin-measured.md` +(`auto/no-disc-and-menu-captures` at `4fa3099`): a continuous spin, from eight +evenly spaced autocorrelation peaks over nine revolutions, **with no angle +estimated anywhere** โ€” both angle estimators failed their own controls and were +not used. + +### The period comes off the disc; the RE agent supplied only that it repeats + +`ptbtneff01` declares two keyframes that differ in **nothing but** +`rotation_deg`, 0 โ†’ 360, the first timed at `t = 120` and the second untimed. +The port turns once per **120 units**. Nothing is authored: the number is on the +disc, and what the measurement adds is that the turn **repeats** rather than +stopping at 360 = 0, which "groups hold" could not distinguish because those are +the same pose. + +`ScreenView.spin_period_units` is the rule, and it is structural and narrow: +exactly two keyframes, differing only in rotation, by a full 360, first timed and +second untimed. **Disc-wide check over this export: 16 of 212 elements match, and +all 16 are focus rings** โ€” `ptbtneff01` on the five main-menu buttons and +`ptbtneff02` on the three `EXTRAS` buttons, in both locales, every one declaring +`t = 120`. Zero false positives. + +That check is the point rather than a formality. The measurement was taken on +**one** button of **one** screen; a rule that also caught something else would be +extrapolating it to elements nobody watched. + +โš ๏ธ It is a rule about **shape**, not a decoded field. Nothing on the disc says +"this loops". The day a loop flag is decoded, this goes. + +### Verified on the port's own render, with the RE agent's own control + +Captures at `--time=` 2.0 โ€ฆ 4.0 s on the settled main menu, `ptbtn01` focused: + +| | | +|---|---| +| t=2.0 vs t=4.0 (one full period apart), **whole frame** | **0.0000 / 255** โ€” bit-identical | +| t=2.5, 3.0, 3.5 against t=2.0, inside the ring's box | 3.60, 3.71, 3.58 / 255 | +| sum of box luminance across **eight** phases | spread **0.027 %** of the mean | + +The last row is deliberately the RE agent's own observable: they separated +rotation from a brightness pulse by showing total annulus brightness is conserved +while per-bin brightness moves. The port's render conserves it to 0.027 % (theirs +was 0.4 % over 16 s, with capture noise in it). A filmstrip of the four +quarter-period phases shows the bright head at top, right, bottom, left. + +### Two things it does not settle + +* **Direction.** The port turns 0ยฐ โ†’ +360ยฐ, the sign the disc declares. No signed + angle was ever measured โ€” the estimator that would have given one failed its + control and was not used. +* **Phase across a focus change.** The port drives the ring off the **screen** + clock, so moving the cursor does not restart the turn. The alternative โ€” the + record's group restarting when the record is instantiated โ€” is the stronger + claim, and the oracle run held focus on one button throughout, so nothing + separates them. Two frames straddling a focus change would. + +## P3, corrected โ€” the plate needs no authored delay at all, 2026-08-29 + +Last iteration the port refuted the RE agent's authoring instruction (*"when +build 4 has settled, wait 2.13 s, composite build 2"*) with arithmetic off the +disc, shipped the instruction anyway rather than pick between two of their +numbers, and printed the discrepancy on every boot. + +**The refutation held, and the answer that came back is better than either option +the port offered: author nothing.** `5b0a6e6`. + +### The premise that failed was the port's, and it will bite again + +> ๐Ÿ”ด **`rest.t` is not when a screen settles.** It is the last *hold* keyframe +> before the exit. + +Checked here rather than taken on trust. `title`'s `ptlogo1`: + +``` +t=26 (-116,-7) 150% a=0x00 the pre-roll +t=42 (179,186) 101% a=0xe0 it has arrived +t=251 (184,193) 100% a=0xff 5 px and 31 alpha steps later, 3.5 s on +``` + +It **stops moving at t=42** and then creeps for 209 units. `rest.t = 251` is the +end of that creep, not the arrival. The title's visible build-in is over at +**`t = 118`**, where `pteff01`, `pteff02` and `ptlogoall_eff` finish together. + +Every reconciliation the port computed last iteration was wrong by exactly that +error: reading `rest.t` put build 4's arrival at 4.350 s instead of 1.967 s, and +the "2.51 s, which is not a landmark of anything" that looked so damning is +`(4.350 โˆ’ 1.967) + 0.13` โ€” the error itself, wearing a decimal point. + +### One clock, and the interval is declared + +| | units | +|---|---| +| build 4's last build-in ramp | `t = 118` | +| `ptbtn00` reaches `a = 255` | `t = 238` | +| **difference** | **120 units = 2.000 s** | + +Measured: **2.138 s** and **2.132 s**. The 6.7 % is presentation rate โ€” 120 units +in 2.135 s is 56.2 units/s, the emulator running 28.1 fps against a nominal 30, +and the corpus had independently measured the idle title at 28.5 fps *before* +these runs. + +So `authored/flow.json` carries `"clock": "shared"` and **no delay**, `boot.gd` +raises the overlay when the step's screen loads rather than at its settle, and +`overlay.time_units = view.time_units` โ€” assigned, not accumulated, because two +independently advanced clocks drift by a frame here and there and the whole +content of the finding is that 120 units is a fixed interval on one timeline. + +โš ๏ธ **The general hazard, stated by the RE agent and worth repeating where the +port will read it: discount a wall-clock number off that oracle by ~6 %.** It is +Canary's presentation rate baked into whatever it measures. A port at a true +30 Hz that authored 2.13 s would be visibly late. + +### Refutation โ€” two of the RE agent's numbers for the same 120 units disagree by 2 % + +Both findings measure the same declared quantity: **120 keyframe units of wall +clock, during a static hold, in Xenia Canary.** + +| | | implied presentation | +|---|---|---| +| plate: settle โ†’ plate, two runs | 2.138, 2.132 s โ†’ mean **2.135 s** | 28.10 fps | +| ring: one revolution, seven spacings | 2.18 2.16 2.18 2.16 2.16 2.20 2.20 โ†’ mean **2.177 s** | 27.56 fps | +| **disagreement** | **0.042 s = 1.97 %** | | + +That is **seven times** the plate finding's own run-to-run agreement of 6 ms, and +it lands on the argument that finding uses to justify trusting itself: *"the +build-in is where frames are dropped; the static hold is not. A model in which +the game's own timing varied would have to move both."* Two static-hold +measurements are exactly what should agree under that model. + +A second, smaller arithmetic slip in the same place: the ring page reconciles +2.177 s against a band of "27.6โ€“28.8 fps", saying the measurement *"sits at the +top of that band"*. It does not sit in it. 60 rendered frames at 27.6 fps is +2.1739 s; the mean needs **27.56 fps** and the two slowest spacings (2.20 s) +need **27.27 fps**. Four of the seven spacings are above the band's top. + +**Verdict: the containment claim is refuted; the spin, the period and the +reconciliation are untouched.** Either the presentation rate genuinely differed +between the two sessions โ€” which the plate page's own corroboration argues +against for static holds โ€” or the ring's revolution is not exactly 120 units. +The corpus should say which, because they are the same claim measured twice. + +๐ŸŸข **Nothing in the port moves either way.** `spin_period_units` uses the +declared 120 units at 60 units/s = **2.000 s of port time**, which is the +true-30 Hz value both readings agree the disc means. This is a corpus +consistency problem, not a port one โ€” which is why it is filed rather than +worked around. diff --git a/port/scripts/boot.gd b/port/scripts/boot.gd index 6593d58f..ea10ca6c 100644 --- a/port/scripts/boot.gd +++ b/port/scripts/boot.gd @@ -52,9 +52,14 @@ var audio: MenuAudio = null ## The second build, drawn OVER `view`. The boot title is the only place in this ## port where two builds are on screen at once (`authored/flow.json`, the boot's -## `title` step): build 4 presents alone, and the `PRESS โ’ถ BUTTON` plate -- -## build 2 -- is composited over it 2.13 s after build 4 settles. MEASURED, two -## boots agreeing to 6 ms; see the step's `why`. +## `title` step): build 4 presents alone and the `PRESS โ’ถ BUTTON` plate -- build +## 2 -- arrives later. +## +## **They share one clock, started together, and there is no authored delay.** +## The plate arrives at its own declared `t = 238`; build 4's visible build-in +## ends at `t = 118`; the 120-unit difference is 2.000 s, against an oracle that +## measured 2.138 s and 2.132 s at an emulator presenting 28.1 fps rather than +## 30. A port running at a true 30 Hz wants the declared 120, not the wall clock. ## ## A second ScreenView rather than a second screen inside one, because that is ## what "two builds at once" actually is: each has its own timeline, its own @@ -198,6 +203,16 @@ func _ready() -> void: # so what it photographs is the real composite and not a mock-up. It applies # NO delay: the delay is a measurement and lives in `authored/flow.json`, # where the boot reads it. + # A boot whose FIRST step declares an overlay: `_advance` raises it for every + # later step, and `_ready` is the one with no `_advance` in front of it. + # Today only the last step has one, so this is a guard rather than a fix -- + # but a silently missing second build is exactly the failure P3 just spent an + # iteration on. + if not _sequence.is_empty() and typeof(_sequence[0].get("overlay", null)) == TYPE_DICTIONARY: + _overlay_spec = _sequence[0]["overlay"] + _overlay_due = 0.0 + _overlay_process(0.0) + if args.has("overlay") and not args.has("boot"): _overlay_spec = {"screen": args["overlay"]} _overlay_due = 0.0 @@ -272,12 +287,11 @@ func _process(delta: float) -> void: # finding, not a detail: measured from first-draw the two oracle # runs disagree by 0.48 s, because the build-in's own duration is # the emulator's frame pacing rather than the game's clock. - var spec: Variant = _sequence[_step].get("overlay", null) - if typeof(spec) == TYPE_DICTIONARY: - _overlay_spec = spec - _overlay_due = _elapsed + float(spec.get("after_settle_seconds", 0.0)) - print(" overlay %s due at %.2f s (+%.2f s after settle)" - % [spec.get("screen", "?"), _overlay_due, _overlay_due - _elapsed]) + if overlay != null: + # It was raised with the screen, 120 units ago. Nothing to do + # here any more -- this hook used to start an authored 2.13 s + # timer, and the timer was the bug. + pass # P5 takes over here: the boot ends on the title and the title has # somewhere to go. Without `--play` the run still stops, because a # boot that ends by waiting for a key it will never get is worse @@ -304,6 +318,14 @@ func _advance() -> void: if not view.load_screen(view.tree, name): push_error(view.tree.error) get_tree().quit(2) + return + # The second build starts WITH the first, not after it. Raised here rather + # than at the screen's settle, which is what the authored-delay version did. + var spec: Variant = next.get("overlay", null) + if typeof(spec) == TYPE_DICTIONARY: + _overlay_spec = spec + _overlay_due = _elapsed + _overlay_process(0.0) ## Play one transcoded movie, full-bleed over the screen. @@ -716,7 +738,11 @@ var _overlay_due: float = 0.0 func _overlay_process(delta: float) -> void: if overlay != null: - overlay.time_units += delta * overlay.units_per_second + # ONE CLOCK. Not `+= delta * ups` on each independently: they would drift + # apart by a frame here and there, and the whole content of the finding + # is that the 120 units between build 4's last ramp and the plate's + # `a=255` is a fixed interval on a shared timeline. + overlay.time_units = view.time_units overlay.queue_redraw() if _overlay_quit_at >= 0.0 and _elapsed >= _overlay_quit_at: print("boot ends on %s + %s at %.2f s" @@ -763,26 +789,17 @@ func _raise_overlay(name: String) -> void: # this. Give the plate its own group time to play before leaving, so the # artifact shows the composited state rather than the frame it began on. var visible_at := overlay.settle_time() / overlay.units_per_second - # The two lines below describe a BOOT. `--screen --overlay=` raises the same - # composite with no delay, as a fast check, and must not narrate a sequence - # it is not running -- a log line that lies is worse than no log line. + # `--screen --overlay=` uses the same code path as a fast check and must not + # narrate a sequence it is not running: a log line that lies is worse than + # no log line. if _sequence.is_empty(): return - # ๐Ÿ”ด SAY THE NUMBER OUT LOUD. `authored/flow.json` implements the RE agent's - # instruction literally -- "when build 4 has settled, wait 2.13 s, composite - # build 2" -- and build 2 then takes its OWN declared 238 units to fade in. - # So the plate is first VISIBLE at settle + 2.13 + 3.97 s, while the thing - # that was measured is the plate becoming visible at settle + 2.13 s. The two - # cannot both be right and the port is not the one to choose. Printed on - # every boot so the disagreement cannot go quiet. - print(" โš  plate raised at settle+%.2f s but its own group reaches full alpha %.2f s later, \ -so it is first VISIBLE at settle+%.2f s -- the measurement is settle+%.2f s. See docs/port/BLOCKED.md." - % [float(spec.get("after_settle_seconds", 0.0)), visible_at, - float(spec.get("after_settle_seconds", 0.0)) + visible_at, - float(spec.get("after_settle_seconds", 0.0))]) + print(" plate reaches full alpha at t=%d (%.2f s on the shared clock), \ +120 units after build 4's last build-in ramp at t=118" + % [int(overlay.settle_time()), visible_at]) if not _play and _film == "": _overlay_quit_at = _elapsed + visible_at - print(" boot ends at %.2f s, once the plate has settled" % _overlay_quit_at) + print(" boot ends at %.2f s, once the plate has arrived" % _overlay_quit_at) # `spec` is read only for the log; the reasoning lives in flow.json where a # reader looking for a decision will find it. if spec.has("why"): diff --git a/port/scripts/screen_view.gd b/port/scripts/screen_view.gd index 36898d07..9b966f60 100644 --- a/port/scripts/screen_view.gd +++ b/port/scripts/screen_view.gd @@ -228,6 +228,41 @@ static func settle_units(element: Dictionary) -> float: return last +## How long one turn takes, in keyframe units, for an element that spins โ€” or 0. +## +## The rule is STRUCTURAL and narrow: exactly two keyframes, differing in +## **nothing but** `rotation_deg`, by a full 360, with the first timed and the +## second untimed. The period is the first keyframe's declared `t`. +## +## Its disc-wide check, over this export: **16 of 212 elements match, and all 16 +## are focus rings** โ€” `ptbtneff01` on the five main-menu buttons and +## `ptbtneff02` on the three `EXTRAS` buttons, in both locales, every one of them +## declaring `t = 120`. Zero false positives. That matters because the rule is +## applied on the strength of a measurement taken on **one** button of one +## screen; a rule that also caught something else would be extrapolating from +## that measurement to elements nobody watched. +## +## โš ๏ธ It is a rule about SHAPE, not a decoded field. Nothing on the disc says +## "this loops". What the disc says is 0ยฐ โ†’ 360ยฐ over `t`; what the RE agent +## measured is that the turn repeats rather than stopping. Those are two +## different sources and the day a loop flag is decoded, this goes. +static func spin_period_units(element: Dictionary) -> float: + var frames: Array = element.get("keyframes", []) + if frames.size() != 2: + return 0.0 + var a: Dictionary = frames[0] + var b: Dictionary = frames[1] + if not a.has("t") or b.has("t"): + return 0.0 + for key in ["pos", "scale", "tint_rgba", "fade_argb"]: + if a.get(key) != b.get(key): + return 0.0 + if absf(float(b.get("rotation_deg", 0)) - float(a.get("rotation_deg", 0))) != 360.0: + return 0.0 + var t := float(a["t"]) + return t if t > 0.0 else 0.0 + + ## The moment the whole screen has arrived: the last element to reach its hold. func settle_time() -> float: var last := 0.0 @@ -326,26 +361,33 @@ func _draw_focus(element: Dictionary) -> void: if tex == null: skipped.append("%s (focus sprite failed to load)" % fe.get("id", "")) continue - # The ring's rest pose, which is rotation_deg 0. + # The ring spins, and until 2026-08-29 this drew it at 0 -- a pose the + # running game never shows -- because the PERIOD was the missing piece + # and a spin rate would have been invented. # - # โš ๏ธ THIS IS KNOWN TO BE WRONG, and is drawn anyway because the right - # answer is a guess. The spin is real -- rotation_deg ramps 0 -> 360 - # with position, scale and alpha all constant -- and measuring the two - # oracle captures says the game never shows 0: the same sprite sits at - # ~76 deg with NEW GAME focused and ~210 deg with OPTIONS focused, - # 134 deg apart at peak correlation 0.97 against a null control of 0.37 - # (docs/port/DECISIONS.md, "the focus ring IS drawn rotated"). + # It is no longer invented. `docs/re/focus-ring-spin-measured.md` + # measures a continuous spin, period 2.177 s wall-clock, from eight + # evenly spaced autocorrelation peaks over nine revolutions, with NO + # angle estimated anywhere -- both angle estimators failed their own + # controls and were not used. It reconciles with the declared `t = 120` + # without a new constant: 120 units is 60 rendered frames, 2.00 s at a + # true 30 Hz and 2.08-2.17 s at the 27.6-28.8 fps that emulator runs. # - # What is missing is the PERIOD, and it has two unknowns, both the - # Decoder's: the ramp's second keyframe is untimed, and "groups hold" - # predicts a stop at 360 = 0, which is not what either capture shows. - # Holding at 0 is the pose that invents nothing; a spin rate would be - # invented. See docs/port/BLOCKED.md. + # So the period comes off the DISC -- the element's own declared `t` -- + # and what the RE agent supplied is that one turn takes exactly that + # long and repeats. See `spin_period_units` for the rule and its check. var pose: Dictionary = fe.get("rest", {}) var pivot := _vec(fe.get("pivot", [0, 0])) var pos := _vec(pose.get("pos", [0, 0])) + var period := spin_period_units(fe) + var rot := _rot_of(pose) + if period > 0.0: + # `time_units` raw, NOT the pose clamped by `holding`: a spinning + # ring is the one thing on the settled main menu that keeps moving, + # and the whole point of the finding is that it does not stop. + rot = 360.0 * fposmod(time_units, period) / period _draw_quad(tex, placement(pose, pivot, tex.get_size()), modulate_of(pose), - pivot, pos, _rot_of(pose)) + pivot, pos, rot) drawn.append(fe.get("id", ""))