diff --git a/docs/agents/PLAYTEST-2026-09-01.md b/docs/agents/PLAYTEST-2026-09-01.md new file mode 100644 index 00000000..e60a8a84 --- /dev/null +++ b/docs/agents/PLAYTEST-2026-09-01.md @@ -0,0 +1,106 @@ +# Play-test, 2026-09-01 — a human, a real controller, the port + +**The first time a person played this port on real hardware.** It found four +things. Two were fixed on the spot by the human; two are open and are the +**current focus of both agents**. + +⚠️ This page is a record of observations, not a mission change. `PORT-MISSION.md` +and the loop briefs carry the objective. + +## What was found + +| # | finding | status | +|---|---|---| +| 1 | **Ⓐ and Ⓑ did nothing on the pad.** Could not skip the intro, could not open a submenu. | ✅ fixed by the human — `port/scripts/gamepad.gd` | +| 2 | **The left stick moved the cursor far too fast.** | ✅ fixed by the human — latched to one step per deflection | +| 3 | **The `PRESS Ⓐ` plate appears too late.** | 🔴 **OPEN** | +| 4 | **The splash fade/blur is wrong** — the game's is *more pronounced*. | 🔴 **OPEN** | + +## 1 & 2 — why no check caught them, which matters more than the fixes + +> **`--script` sends `InputEventAction`, which BYPASSES the input map.** + +Every check the port had asserted the code *below* the input map and nothing +about the map itself. The map turned out to have **no joypad binding for +`ui_accept` or `ui_cancel` at all** — measured on Godot 4.7.2, not remembered, +because the remembered answer was wrong: + +``` +ui_accept key:Enter, key:Kp Enter, key:Space <- no joypad button at all +ui_cancel key:Escape <- no joypad button at all +ui_up key:Up, JOYBTN:11, JOYAXIS:1- <- d-pad AND left stick +ui_down key:Down, JOYBTN:12, JOYAXIS:1+ +``` + +Four actions reached the pad and two did not. Ⓐ was dead for the whole of P5 +while the unattended walk passed on every iteration. + +The **same blind spot** hid finding 2: an `InputEventAction` is not an analog +axis, so nothing could observe that a stick held at deflection emits an event +per *jitter*, each reporting the action as pressed — one cursor step per jitter. + +Now asserted by `tools/port/verify-input`, with a control that removes each +check's own subject. (Its first version inverted all nine assertions when only +two depended on the fix, and reported seven correct checks as broken. Three rows +now say plainly they are **not controllable** — they assert Godot's own bindings +— and one is a **negative carrying a positive control** rather than a faked +inversion.) + +### The standing rule that follows + +**Synthetic input is not a test of input.** Anything injected below the input map +is evidence about the code above it and nothing else. A test of input must go in +at the device level — `InputEventJoypadButton`, `InputEventJoypadMotion`, +`InputEventKey` — or must assert the map directly. + +## 3 — the plate is late + +The port raises the plate at `t=236`, **3.93 s** after the shared clock starts, +which it derives as `238 − 118 = 120 units = 2.000 s` after the title's build-in +ends. A human watching both says it is **late**. + +This lands in a spot the corpus already knows is soft. All of the following are +live: + +* `REFUTED.md`: *"a screen has SETTLED at its `rest.t`"* → ❌ — believing `rest.t` + had already put a port's plate **3.97 s late** once. +* `REFUTED.md`: the 2.13 s figure was *"a wall-clock reading stretched by Canary + presenting at ~28.1 fps"*, corrected to 120 units. **So the conversion between + units and seconds is load-bearing here and is exactly what + [`TEMPORAL-VERIFICATION.md`](TEMPORAL-VERIFICATION.md) says not to trust from a + wall clock.** +* The keyframe **time-unit shift** is unresolved (`ui-keyframe-time-unit.md`). +* 🔴 After the 2026-09-01 R1 pass, *"the declared keyframe timeline reproduces the + captured splash"* is **🟡 `⟨our-reader⟩`**, not ❌ — the record-layout fix + re-times a group's final pose and the entry was never re-derived under it. + +**Candidate causes, none established:** the unit→seconds constant; the clock +origin (do both builds really start together?); `rest.t` again; the record layout. +Settle it by **ordering and counts**, not by a stopwatch. + +## 4 — the splash fade/blur + +The port applies **no blur at all**. It draws declared keyframe alphas. So +*"more pronounced in the game"* is consistent with a post-process the export does +not describe, a different ramp shape, or both — and nothing in the export can +distinguish those. + +🔴 **And the two splashes are the ONLY screens that reach `rest()`'s +plateau-less fallback** — title, main menu and `EXTRAS` reach it zero times. So +finding 4 lands precisely where our resting-pose heuristic is least trustworthy, +and the R1 pass just re-opened that question **in both directions** (see the +`rest()` pair in `REFUTED.md`). That is not a coincidence to step around. + +## The human's verdict on method + +> *"It seems the agents were essentially guessing and trying to copy what one +> would see, but while they did get close it still is not quite right."* + +Close-but-not-right is the signature of reproducing **appearance** instead of +deriving **mechanism**. A ramp tuned until it looks right is wrong in a way +nobody can name and has no reach to the next screen. + +The instruction that follows: for the splashes, **find out what the game is +doing** — is there a post-process pass, how many, what shader, what blend, what +render targets, and where do its parameters come from — before proposing any +curve. See the Decoder's brief. diff --git a/docs/agents/TEMPORAL-VERIFICATION.md b/docs/agents/TEMPORAL-VERIFICATION.md new file mode 100644 index 00000000..1e086b65 --- /dev/null +++ b/docs/agents/TEMPORAL-VERIFICATION.md @@ -0,0 +1,139 @@ +# Verifying things that MOVE + +**Both agents read this.** Set by the human on 2026-09-01, after a play-test +found the splash fade and the `PRESS Ⓐ` plate visibly wrong while every check +either agent had was green. + +## The diagnosis, in one sentence + +> **We have been trying to photograph the game at time *t*, and *t* is never the +> same twice.** + +Every temporal claim in this corpus rests on grabbing a frame at a wall-clock +instant and comparing it to something. That instant drifts — emulator speed +varies with host load, Canary presents at ~28.1 fps rather than 30, the capture +path costs a variable 0.1–10.8 s, and a long-lived `x11grab` stream degrades and +then freezes. So the comparison is between *our render at the time we meant* and +*the game at some other time*, and the difference between those two things is +being read as a difference in the **content**. + +The register already carries four separate refutations of this exact shape: + +* *"a latency read off a classified `x11grab` stream is a duration"* — ❌. At + 1503 ms per classification against an 8 fps stream the consumer ran at + 0.64 fps; four "durations" died. A screen transition, a button press and a + plate fade all came out at ~20–25 s, which is the tell. +* *"2 391 frames over 600 s, max glyph 0, therefore the title never appears"* — + withdrawn: **the instrument stalls**, repeating one stale frame, reading + surface mean 5.21 where `import` read 125.65 at the same moment. +* *"the boot harness fails because its polling loop samples every ~41 s"* — + the defect was real and fixing it (13.7×) **did not change the answer**. +* *"the in-box capture noise of 0.32 between sessions"* — it was not noise, it + was the **trigger**: gating on the plate pulse phase-locks the shutter, so + 0.32 is a lower bound produced by the instrument. At an arbitrary phase the + honest figure is 11.9. + +That last one is the important one, and it cuts both ways: **gating hides +variance, and not gating produces it.** Neither is a measurement of the game. + +## What to do instead + +The rule is simple and it removes the whole class: + +> **Never compare at an absolute time. Record a SEQUENCE, and align it by +> CONTENT.** + +### 1. Capture a film, not a photograph + +Record a continuous run of frames with an index and a timestamp each, spanning +the whole animation with margin at both ends. One frame is a sample of a +distribution you have not characterised; a film *is* the distribution. + +**State the achieved rate against the requested rate, every time.** A capture +that asked for 4 fps and delivered 1.6 is not a slow capture, it is a +**different capture**, and it has already produced two withdrawn findings here. +An instrument that cannot report its own completeness may not be trusted (R3). + +### 2. Align by content, then measure + +Find the offset that best matches, rather than assuming offset zero: + +* reduce each frame to a scalar or a small vector — mean of a region, an + element's alpha, a per-tile amplitude; +* do the same for the prediction; +* **search the lag** that maximises agreement, and report *both* the lag and the + agreement at it. + +The lag is not an error to be minimised away — **it is a measurement**. A +consistent lag across runs is a real offset in our model. A lag that varies +run to run is the harness, and says so. + +### 3. Prefer quantities that have no phase + +Ranked by how much they survive a drifting clock: + +| quantity | survives drift? | +|---|---| +| **ordering** — A finishes before B starts | ✅ completely | +| **counts** — 83 frames at full alpha | ✅ (given a known, reported rate) | +| **durations and ratios** — ramp is 2× the hold | ✅ | +| **shape** — monotone, eased, stepped, its inflections | ✅ | +| a value **at a named event** — alpha when the plate first appears | 🟡 needs the event found, not the time | +| a value **at wall-clock t** | ❌ this is the thing that has been failing | + +The two strongest existing results in the corpus are both of this kind: the +**hold duration** (83 frames of full alpha) is called *calibration-free* in +`ui-keyframe-time-unit.md` and decided the question; and the `_eff` glows' +**exact steps of 34** are a shape, not a sample. + +### 4. Anchor on an event + +Quote everything relative to a frame you can *find* rather than a time you +requested: the first frame an element is non-black, the frame the plate first +appears, the last frame of the previous screen. Then a drifting start costs +nothing, because every number is a difference. + +### 5. Say what you expected before you look (R2) + +*"This ramp declares 80 units, so at 30 fps I expect ~80 frames and I will +accept 74–86."* Written first, it makes a near-miss legible as a near-miss +instead of something to rationalise. Written after, any number can be explained. + +### 6. Convert units deliberately + +⚠️ **Canary presents at ~28.1 fps, so a wall-clock duration off this emulator is +~6 % long.** A measured interval landing near a round number of keyframe units +probably *is* that number of units — that is how `2.13 s` turned out to be +`120 units = 2.000 s`. Quote the unit count, then the seconds, then the fps you +divided by. Never the seconds alone. + +## The other half: stop reproducing by eye + +The play-test's verdict on the splashes was *"close, but not quite right"*, and +that is the signature of **matching appearance instead of deriving mechanism**. +A ramp tuned until it looks right will be wrong in a way nobody can name, and +"looks right" has no reach — it does not tell you what the next screen will do. + +So for anything visual that is still not exact, the question is not *"what +curve fits?"* but **"what is the game actually doing?"**: + +* Is there a **post-process pass at all** — a blur, a bloom, a fade quad, a tone + curve? That is a GPU-state question with a yes/no answer. +* If yes: how many passes, what render targets, what blend, what shader, and + **where do its parameters come from** — immediate constants, a table in a pak, + a computed ramp? +* Only then, what curve. + +A mechanism found this way is *decoded*, generalises to every screen, and cannot +be "close". A curve fitted by eye is none of those things. + +## What this does not license + +Doing more of this is not a reason to stop shipping. A measurement that would +take an hour is not blocked on building the perfect harness first — take the +cheap phase-invariant version (an ordering, a count) and say what its reach is. + +And **an instrument that cannot pass a control is not a starting point.** A +filter that fails its own known-positive is dead, not tuneable; a lag search +that cannot recover a synthetic 30-frame offset cannot measure an unknown one. +Run the control first, and record it. diff --git a/docs/agents/decoder-loop.md b/docs/agents/decoder-loop.md index a71515d3..3cb53595 100644 --- a/docs/agents/decoder-loop.md +++ b/docs/agents/decoder-loop.md @@ -1,6 +1,60 @@ You are the **Decoder**. Answer the open questions the Godot menu port is blocked on, one at a time. +## 🔴 CURRENT FOCUS, set by the human 2026-09-01: THE LOGO SPLASHES, EXACTLY + +A human played the port on real hardware and reported that the splashes are +**close but not right** — the fade/blur is more pronounced in the game — and that +the `PRESS Ⓐ` plate arrives late. Read +[`PLAYTEST-2026-09-01.md`](PLAYTEST-2026-09-01.md) first; it has the findings and +why none of our checks caught them. + +Their verdict on how we have been working is the part that matters: + +> *"It seems the agents were essentially guessing and trying to copy what one +> would see, but while they did get close it still is not quite right."* + +**So do not fit a curve to a screenshot. Find the mechanism.** For the splashes, +in this order, and answer each with evidence rather than by inference: + +1. **Is there a post-process pass at all?** A blur, a bloom, a fade quad, a tone + curve, a resolve-and-resample. Yes/no, from GPU state. +2. **If yes: what is it?** How many passes, which render targets, what blend + state, which shaders (you have their hashes in the draw log already). +3. **Where do its parameters come from?** Immediate constants in the command + stream, PS/VS constant banks, a table in a pak, a computed ramp in code. +4. **Only then, what curve** — and it should fall out of 3, not be fitted. + +Use **both** routes and say which produced each fact: + +* **Dynamic** — Canary. Per-draw capture, shader constants, render-target + bindings, blend state, and where those are not logged, **add the logging**: + `/canary` is yours read-write and the draw logger already exists. Guest memory + and CPU state are available too; the splash's driver is a `GamePart` and its + parameters are somewhere in it. +* **Static** — the `.pe` image, `sylpheed.db`, the paks. The code that *sets up* + the pass is in the image, its constants may be immediates, and shader blobs + ship on the disc. A mechanism confirmed statically **generalises to every + screen**; one observed in a capture holds for that capture. + +A mechanism found this way is *decoded* and cannot be "close". A curve fitted by +eye is neither. + +⚠️ Anything you conclude about *timing* here must obey +[`TEMPORAL-VERIFICATION.md`](TEMPORAL-VERIFICATION.md). The plate-late finding is +a timing question and the corpus has already lost four claims to the wall clock. + +### Second, and not optional: the complete input set + +The port had **no joypad binding for Ⓐ or Ⓑ** and nobody noticed for a whole +milestone. The port has fixed its side. Yours is the other half: + +**Decode what the game actually reads.** Every button, both sticks, the triggers, +START and BACK — per screen if it differs. The pad read path is in the image and +`sub_821CC860`'s decoded arguments already include `PAD`. Deliver the *set*, and +say for each entry whether it is decoded from the image, measured in a capture, +or neither. Guessing which buttons exist by pressing them is how we got here. + ## Your objective `docs/port/MISSION.md` — read it every iteration. It lists the open questions and @@ -40,6 +94,24 @@ If the merge conflicts, resolve it, say so in your reply, and carry on. side**. Fill it in as you go: you are the one who sees the real screens. 8. `docs/agents/CONTAINER-NOTES.md` — the container's tooling, and the reference assets described below. +9. `docs/agents/TEMPORAL-VERIFICATION.md` — **how to verify anything that + moves.** Set by the human. Every temporal claim must obey it. +10. `docs/agents/PLAYTEST-2026-09-01.md` — what a human found playing the port. + +⚠️ **`REFUTED.md` was reclassified by the human on 2026-09-01 under rule R1.** +Every entry now ends with its `⟨instrument⟩`, and **ten entries moved ❌ → 🟡** +because the instrument that killed them was one of ours. A 🟡 is *not* dead — it +is re-openable, and each says what would settle it. Read the file's own "How to +read this file" section once. When you improve a renderer, a reader or the +capture harness, run `tools/stale-instrument `: it lists exactly +what that instrument killed, so those claims re-open instead of staying dead +because nobody remembered which ones rested on it. + +🔴 Two of the ten bear directly on the current focus. *"The declared keyframe +timeline reproduces the captured splash"* is now 🟡 `⟨our-reader⟩`, never +re-derived under the record-layout fix. And the **`rest()` pair** is open in +**both** directions — both legs run through our renderer — and the two splashes +are the only screens that reach that fallback. ## Reference assets you may not know you have @@ -132,6 +204,23 @@ renderer is a claim about our renderer. * Verify with an **artifact**, not "it compiles". * Commit reference data beside the finding, so the port can work without a disc. +### Anything that moves + +**Read `docs/agents/TEMPORAL-VERIFICATION.md` and follow it.** The short form: + +* **Record a film, not a photograph.** One frame is a sample of a distribution + you have not characterised. +* **Align by CONTENT, not by clock.** Search the lag that best matches and report + the lag *and* the agreement at it. The lag is a measurement, not an error. +* **Prefer quantities that have no phase** — ordering, counts, durations, ratios, + shape. The two strongest timing results in this corpus are both of that kind. +* **Anchor on an event**, then quote differences from it. +* **State the expected number before reading the actual one.** +* **Report achieved fps against requested fps.** A capture that asked 4 and got + 1.6 is a different capture; that has already produced two withdrawn findings. +* ⚠️ Canary presents at **~28.1 fps**, so a wall-clock duration off this emulator + is **~6 % long**. Quote unit counts first, then seconds, then the fps used. + ## Talking to the other agent `ListAgents` shows who is reachable; `SendMessage(to: "sylpheed-port", ...)` reaches diff --git a/docs/agents/port-loop.md b/docs/agents/port-loop.md index 419de7fb..e5e0fcdb 100644 --- a/docs/agents/port-loop.md +++ b/docs/agents/port-loop.md @@ -1,5 +1,58 @@ You are the **Port**. Build the Godot menu shell, one milestone at a time. +## 🔴 CURRENT FOCUS, set by the human 2026-09-01: THE LOGO SPLASHES, EXACTLY + +A human played this port on a real controller for the first time. Read +[`../agents/PLAYTEST-2026-09-01.md`](../agents/PLAYTEST-2026-09-01.md) **before +anything else** — it has all four findings and, more importantly, why none of +your checks caught two of them. + +**Two were fixed for you by the human. Do not re-do them; do read them.** + +1. **Ⓐ and Ⓑ were never bound to the pad.** Godot 4.7.2 binds no joypad button to + `ui_accept` or `ui_cancel`, while it binds the d-pad *and* the left stick to + `ui_up`/`ui_down`. Ⓐ was dead on real hardware for the whole of P5 while your + unattended walk passed every iteration. Fixed in `port/scripts/gamepad.gd`; + asserted by `tools/port/verify-input`, now in `check-all`. +2. **The left stick fired once per jitter.** An axis is not an edge. Latched to + one step per deflection, with hysteresis. + +> ### The rule that follows, and it is the reason this happened +> +> **`--script` sends `InputEventAction`, which BYPASSES the input map.** Every +> check you had asserted the code *below* the map and nothing about the map. +> Synthetic input is not a test of input. +> +> **From now on: a test of input goes in at the DEVICE level** — +> `InputEventJoypadButton`, `InputEventJoypadMotion`, `InputEventKey`, +> through `Input.parse_input_event` — or it asserts the input map directly. +> `InputEventAction` remains fine for driving a walk; it is not evidence that +> input works. + +**Two are open and are your focus:** + +3. **The `PRESS Ⓐ` plate arrives late.** You raise it at `t=236`, derived as + `238 − 118 = 120 units = 2.000 s`. A human watching both says late. The + unit→seconds conversion is load-bearing and is exactly what the wall clock + cannot be trusted for. **This is an RE question if the cause is the unit; it + is yours if the cause is the clock origin or `rest.t`.** Establish which + half it is before asking, and say how you established it. +4. **The splash fade/blur is not the game's** — the game's is more pronounced. + You apply **no blur at all**. Whether the game runs a post-process pass is an + oracle question and it is with the Decoder. **Do not fit a curve to a + screenshot while waiting** — that is exactly what produced "close but not + right". + +⚠️ Anything you conclude about timing must obey +[`../agents/TEMPORAL-VERIFICATION.md`](../agents/TEMPORAL-VERIFICATION.md). +Record a film and align by content; never compare at an absolute time. + +⚠️ **`REFUTED.md` was reclassified by the human on 2026-09-01 (rule R1).** Ten +entries moved ❌ → 🟡 because our own renderer or reader killed them. Two bear on +your focus: *"the declared keyframe timeline reproduces the captured splash"* is +now 🟡 `⟨our-reader⟩`, and the **`rest()` pair is open in both directions** — and +the two splashes are the **only** screens reaching that fallback. + ## Your objective `docs/port/PORT-MISSION.md` — read it every iteration. Milestones P0…P7, each @@ -79,6 +132,14 @@ That is the easiest thing here to get subtly wrong. disagree, say which is wrong rather than tuning until they match. * Godot runs headless (`godot-headless`), or windowed under Xvfb with `screenshot`. +* **Input is verified at the device level or not at all** — see the focus block + at the top. `tools/port/verify-input` is the pattern: it asserts the input map + itself, and feeds real `InputEventJoypadMotion` values through the latch. Run + it and its `--control` in `check-all`. +* **Anything that moves** follows `../agents/TEMPORAL-VERIFICATION.md`: a film + rather than a frame, aligned by content; prefer ordering, counts, durations and + shape over a value at a wall-clock instant; report achieved fps against + requested fps; state the expected number first. * Audio: `docs/port/AUDIO-VERIFICATION.md` — no sound card is needed to answer any of it. Write to a temp name and rename on completion; another agent probing a file you are still writing gets a confident wrong number.