From aad3fb382eaf13aa36c0751bfbc54de177b5dc38 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Tue, 1 Sep 2026 17:59:17 +0200 Subject: [PATCH] agents: the splashes exactly, and stop photographing a moving thing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A human played the port on real hardware for the first time (2026-09-01) and found four things. Two were port defects, fixed. Two are open and are now both agents' focus: the PRESS (A) plate arrives late, and the splash fade/blur is weaker than the game's. Their verdict on method is the reason this is a brief change and not a ticket: "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. So the Decoder's focus block asks, in order: is there a post-process pass at all, what is it, where do its parameters come from -- and only then what curve. Both routes, dynamic (GPU state, shader constants, render targets; add logging to Canary, it is theirs read-write) and static (.pe, the DB, the paks), with each fact labelled by which produced it. TEMPORAL-VERIFICATION.md is the other half, and it generalises past the splashes. We have been photographing the game at time t, and t is never the same twice: emulator speed varies with host load, Canary presents at ~28.1 fps, the capture path costs a variable 0.1-10.8 s, and a long-lived x11grab stream degrades and then freezes. The register already carries FOUR refutations of exactly this shape. The replacement rule: record a film, not a photograph; align by CONTENT, not by clock, and report the lag as a measurement rather than minimising it away; prefer ordering, counts, durations and shape over any value at a wall-clock instant; anchor on an event; report achieved fps against requested fps. Also into both briefs: the input set. The port had no joypad binding for (A) or (B) and nobody noticed for a whole milestone, because --script sends InputEventAction, which BYPASSES the input map -- so every check asserted the code below the map and nothing about the map. The Decoder is asked to DECODE the full set the game reads rather than discover it by pressing buttons; the Port is told input is verified at the device level or not at all. And both briefs now point at the R1 register reclassification, because two of the ten re-opened entries land on this focus: "the declared keyframe timeline reproduces the captured splash" is 🟑 our-reader, and the rest() pair is open in BOTH directions -- while the two splashes are the only screens that reach that fallback. --- docs/agents/PLAYTEST-2026-09-01.md | 106 ++++++++++++++++++++ docs/agents/TEMPORAL-VERIFICATION.md | 139 +++++++++++++++++++++++++++ docs/agents/decoder-loop.md | 89 +++++++++++++++++ docs/agents/port-loop.md | 61 ++++++++++++ 4 files changed, 395 insertions(+) create mode 100644 docs/agents/PLAYTEST-2026-09-01.md create mode 100644 docs/agents/TEMPORAL-VERIFICATION.md 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.