Merge remote-tracking branch 'origin/main' into auto/port-p6-audio

This commit is contained in:
Sylpheed port agent
2026-09-01 16:01:19 +00:00
4 changed files with 421 additions and 0 deletions

View File

@@ -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.

View File

@@ -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.110.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 ~2025 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 7486."* 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.

View File

@@ -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
@@ -19,6 +73,19 @@ an export schema, stop and go back to the question you were answering.
git -C /work fetch origin && git -C /work merge --no-edit origin/main
```
🔴 **On your FIRST iteration after 2026-09-01, also merge the human's branch:**
```bash
git -C /work merge --no-edit origin/human/r1-register-reclassification
```
It carries the **R1 reclassification of `REFUTED.md`** (every entry now names its
`⟨instrument⟩`; ten moved ❌ → 🟡), R1 as standing text in `PROTOCOL.md`, and
`tools/stale-instrument`. It branches from `auto/frame-blend-draw-path`, so if
you are on that line it is a fast-forward. **Two of the ten re-opened entries
land on this iteration's focus** — do not start the splashes without reading
them.
You work on a topic branch, and you read the protocol, the mission and the
shared tooling **from your own checkout** — so without this you are following
whichever version of the rules existed when your branch started. That is not
@@ -40,6 +107,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 <that 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 +217,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

View File

@@ -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
@@ -15,6 +68,19 @@ guess of yours is indistinguishable from a fact and will be believed later.
git -C /work fetch origin && git -C /work merge --no-edit origin/main
```
🔴 **On your FIRST iteration after 2026-09-01, also merge the human's branch:**
```bash
git -C /work merge --no-edit origin/human/r1-retro-tick
```
It carries **the two input fixes made for you** (`port/scripts/gamepad.gd`,
`tools/port/verify-input` + its control, wired into `check-all`), the new
`BLOCKED.md` rows **H1H3**, and the retro tick. It branches from
`auto/port-p6-audio`, so on that line it is a fast-forward. **Merge it before
touching input**, or you will re-derive a fix that is already written and
asserted.
You work on a topic branch, and you read the protocol, the mission and the
shared tooling **from your own checkout** — so without this you are following
whichever version of the rules existed when your branch started. That is not
@@ -79,6 +145,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.