diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 64ffa983..5bc0f6a7 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -500,7 +500,46 @@ constructed in `sub_8220B610` and released in `sub_821A6470`. A *decoder* betwee live. That is the next read. --- -## ✅✅ 2026-09-01 (eighth) — **KEEP YOUR 60.** The guest presents at 30 fps, measured against a ruler that is not a clock +## ❌❌ 2026-09-01 (ninth) — **I WITHDRAW the section below. Do NOT act on "keep your 60" — but do NOT change it either** + +[`../re/guest-frame-rate-WITHDRAWN.md`](../re/guest-frame-rate-WITHDRAWN.md) · +[interval data](../re/data/present-interval-vs-vblank.txt) + +I published "the guest presents at 30 fps, so 60 units/s" and told you to change +nothing. **The measurement was real; the inference was not established**, and the +flaw is one I wrote into my own pre-registration and then failed to apply. + +The unguarded assumption was *"the guest may be frame-locked to its own +presentation rather than to the movie clock."* **A perfect 1.0000 is exactly what +that produces** — a buffer rotating once per present gives run-length 1 at any +frame rate. So the cleanness I read as strength is equally the signature of the +failure mode. **A clean result on an instrument whose key assumption is unguarded +is not confirmation.** + +What surfaced it: the draw log carries a per-frame `gtick` marker. Xenia locks +vblank to 60 Hz, and the interval between guest presents is **one** vblank 71.7 % +of the time and two 24.6 % — a guest hard-locked to 30 fps would put the mass at +two. The tail at 2+ is dropped frames, the only direction a slow emulator can push. + +🔴 **But I am NOT telling you 120.** Three routes now disagree: + +| route | says | plate at `t=236` | +|---|---|---| +| movie cadence (withdrawn) | 60 units/s | 3.93 s | +| present interval vs vblank | ~120 units/s | 1.97 s | +| `title-plate-delay` — 120 units in 2.13 s, twice, to 6 ms | ~56 units/s | ~4.2 s | + +Two of the three must be wrong and I do not know which. **Keep your 60 for now** — +not because I have shown it right, but because changing it on my second guess in +one day is worse. It is now an authored value, not a measured one, and you should +know which. + +The settling experiment is named in the finding: hash the movie luma plane's +*contents* per present rather than its base address, which separates "the buffer +rotated" from "a frame was decoded". The logger needs a small extension for it. + +--- +## ❌ 2026-09-01 (eighth) — WITHDRAWN, see above. ~~KEEP YOUR 60. The guest presents at 30 fps~~ [`../re/guest-frame-rate-measured.md`](../re/guest-frame-rate-measured.md) · [pre-registration](../re/guest-frame-rate-preregistration.md) · diff --git a/docs/re/data/present-interval-vs-vblank.txt b/docs/re/data/present-interval-vs-vblank.txt new file mode 100644 index 00000000..8ad5b085 --- /dev/null +++ b/docs/re/data/present-interval-vs-vblank.txt @@ -0,0 +1,40 @@ +# Interval between guest PRESENTS, from the draw log's own per-frame +# gtick marker. Same capture as guest-frame-rate-cadence.txt. +# +# ⚠️ gtick is NOT guest-intended time. Canary's Clock::QueryGuestTickCount() +# is `host_tick_count * guest_tick_ratio` with the scalar at 1.0, i.e. HOST +# time rescaled. So these are wall-clock intervals and TEMPORAL-VERIFICATION +# applies to them in full. +# +# What they are good for is the SHAPE. Xenia locks vblank to 60 Hz when +# vsync is on and framerate_limit is 0 (graphics_system.cc: 'If VSYNC is +# enabled, but frames are not limited, lock framerate at default value of +# 60'). So the question with no phase in it is: does the guest present once +# per vblank (16.7 ms) or once per two (33.3 ms)? + +frames 1..599 intervals n=594 +span 671,751,499 ticks = 13.435 host-seconds +overall 44.21 presents per host-second + +min 14.94 ms +Q1 16.47 ms +median 17.22 ms <-- the mode of the distribution +Q3 30.43 ms +max 157.20 ms + +histogram, in units of one 60 Hz vblank (16.667 ms): + 1 vblank(s) ( 16.67 ms) : 426 ############################################################ + 2 vblank(s) ( 33.33 ms) : 146 ############################################################ + 3 vblank(s) ( 50.00 ms) : 12 ############ + 4 vblank(s) ( 66.67 ms) : 4 #### + 5 vblank(s) ( 83.34 ms) : 3 ### + 6 vblank(s) (100.00 ms) : 2 ## + 9 vblank(s) (150.00 ms) : 1 # + +ONE vblank : 426/594 = 71.7% +TWO vblanks: 146/594 = 24.6% + +# A guest hard-locked to 30 fps presents every SECOND vblank and would put +# the mass at 2. It is at 1. The tail at 2+ is dropped frames, which is the +# expected direction for a slow emulator -- a slow emulator cannot make +# intervals SHORTER than the guest asked for. diff --git a/docs/re/guest-frame-rate-WITHDRAWN.md b/docs/re/guest-frame-rate-WITHDRAWN.md new file mode 100644 index 00000000..79a8ba19 --- /dev/null +++ b/docs/re/guest-frame-rate-WITHDRAWN.md @@ -0,0 +1,107 @@ +# 🔴 WITHDRAWN — "the guest presents at 30 fps, so 60 units/s" + +**Status: ❌ withdrawn by its own author, 2026-09-01, the same day it was +published.** The measurement was real; the **inference from it was not +established**, and the flaw is one I named in my own pre-registration and then +failed to apply when the data came back clean. + +Supersedes the verdict in +[`guest-frame-rate-measured.md`](guest-frame-rate-measured.md). The data in that +page stands; its conclusion does not. + +--- + +## What I claimed + +That counting presented frames per decoded `ADV.wmv` movie frame gives +`guest_fps / 30`, that it measured **1.0000**, and therefore that the guest +presents at 30 fps and the UI clock runs at **60 units/s**. I told the port +"keep your 60, change nothing." + +## Why it does not hold + +My own pre-registration listed three ways the instrument could lie and guarded +two. The third was: + +> *"**The movie is not playing at its declared rate** because the guest is +> frame-locked to its own presentation instead of to the movie clock. This is the +> assumption the whole method rests on and it is **not** guarded."* + +**That is the one that occurred, and a perfect 1.0000 is exactly what it +produces.** A guest that advances the movie's buffer once per present — whether +or not a new frame was decoded — yields run-length 1 at *any* presentation rate. +So the cleanliness I read as confirmation is equally the signature of the failure +mode, and the measurement cannot separate: + +* a 30 fps guest decoding one movie frame per present, from +* a 60 fps guest rotating a triple buffer once per present. + +🔴 **The methodological error is the part worth keeping.** I wrote the guard down, +saw a result so clean it had no tail at all, and treated the cleanness as +strength. **A clean result on an instrument whose key assumption is unguarded is +not confirmation — the cleanness may be the failure mode's own signature.** The +guards I *did* build (buffer-cycle shape, run-length distribution) both tested the +reading of the buffer, and neither tested whether a buffer change means a decode. + +## The evidence that surfaced it + +The draw log carries a per-frame `gtick`/`gfreq` marker I had not noticed. It is +**not** guest-intended time — Canary's `Clock::QueryGuestTickCount()` is +`host_tick_count * guest_tick_ratio` with the scalar at 1.0, i.e. host time +rescaled — so it is a wall clock and cannot be read as a rate. + +But its **shape** has no phase in it. Xenia locks vblank to 60 Hz when vsync is on +and `framerate_limit` is 0 (`graphics_system.cc`: *"If VSYNC is enabled, but +frames are not limited, lock framerate at default value of 60"*). So: + +``` +interval between presents, in units of one 60 Hz vblank + 1 vblank ( 16.67 ms) : 426 71.7% + 2 vblanks( 33.33 ms) : 146 24.6% + 3+ : 22 3.7% +``` + +[`data/present-interval-vs-vblank.txt`](data/present-interval-vs-vblank.txt) + +**A guest hard-locked to 30 fps presents every SECOND vblank and would put the +mass at 2. It is at 1.** The tail at 2+ is dropped frames, which is the only +direction a slow emulator can push: it cannot make an interval *shorter* than the +guest asked for. + +## 🔴 So what is the answer? OPEN — and I am not replacing one over-claim with another + +The vblank cadence favours **60 fps ⇒ 120 units/s**, which would put the plate at +**1.97 s** and make the port ~2 s late — matching the play-test. But it does not +settle it, because a **third** measurement disagrees with both: + +| route | says | +|---|---| +| movie cadence (withdrawn above) | 60 units/s | +| present interval vs vblank (this page) | ~120 units/s | +| `title-plate-delay-measured.md` — 120 declared units in **2.13 s**, twice, agreeing to 6 ms | **~56 units/s** | + +The third is a wall-clock reading, but two runs agreeing to 6 ms is not nothing, +and 56 is a factor of **2.13** from 120. **Two of these three must be wrong** and I +do not know which. Publishing 120 now would repeat exactly the mistake this page +withdraws. + +## The experiment that would settle it + +Distinguish "the buffer rotated" from "a new frame was decoded": **hash the movie +luma plane's contents per present**, not its base address. Identical content on +consecutive presents ⇒ rotation without decode ⇒ 60 fps guest. Changing content +every present ⇒ genuine 30 fps decode ⇒ the movie ruler was valid after all. + +The draw logger dumps texture *bases*, not contents, so this needs the logger +extended — `/canary` is read-write and the change is small. + +Second, independent, and cheaper: the **UI clock's own advance per present** is +already measured at 2 units. What is unmeasured is whether the *animation* ticks +once per present or once per two. The splash alpha data needed for that is in the +`vb=` vertex dump this same log carries and which I have not yet parsed. + +## What the port should do + +**Nothing yet, and I have told them so.** Their 60 is no longer *supported* by me, +but it is not *refuted* either — one route favours 120 and another favours 56. A +port that changes on this page would be acting on my second guess in one day.