Merge pull request 'feat(port): adopt the measured held-direction repeat rate (F1)' (#27) from feat/f1-held-repeat into main
Reviewed-on: #27
This commit was merged in pull request #27.
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
# F1 — the menu repeats on a held direction: mechanism shipped, **rate deliberately not**
|
||||
# F1 — the menu repeats on a held direction: mechanism shipped, **and the rate adopted**
|
||||
|
||||
**Status:** ✅ mechanism implemented and wired. 🔴 **inert on purpose** — it does
|
||||
nothing until a measured repeat rate exists. Written 2026-09-02 by the Port.
|
||||
**Status:** ✅ mechanism implemented and wired, 2026-09-02. ✅ **rate adopted
|
||||
2026-09-13** from `docs/re/f1-repeat-measured-via-driver-patch.md` —
|
||||
`REPEAT_DELAY = 0.402`, `REPEAT_INTERVAL = 0.134`. It ran inert for eleven days
|
||||
and that was the right state; this page keeps the inert-era reasoning because it
|
||||
is why the two numbers can be trusted now.
|
||||
|
||||
## What was reported
|
||||
|
||||
@@ -57,42 +60,136 @@ A repeat that could fire during a movie or mid-transition would be a **second,
|
||||
subtly different input path**, and the first thing this port learned about input
|
||||
is that a second path is where the defect hides.
|
||||
|
||||
## 🔴 And the rate is not shipped
|
||||
## ✅ The rate, and how far each half of it reaches
|
||||
|
||||
An earlier draft of this change had `REPEAT_DELAY = 0.40` and
|
||||
`REPEAT_INTERVAL = 0.20`, with a paragraph explaining that they were authored.
|
||||
**They were removed rather than commented out**, on an explicit instruction:
|
||||
An earlier draft had `REPEAT_DELAY = 0.40` and `REPEAT_INTERVAL = 0.20` with a
|
||||
paragraph explaining that they were authored. They were removed rather than
|
||||
commented out, on an explicit instruction:
|
||||
|
||||
> *"Take the RATE from the Decoder — do NOT ship a placeholder interval. An
|
||||
> invented rate here is indistinguishable from a measured one later, and this is
|
||||
> the exact field where that already cost us."*
|
||||
|
||||
The instruction is right and the draft was the named failure mode: the
|
||||
explanation would have merged, the numbers would have felt roughly right, and
|
||||
nothing downstream could have separated them from a measurement. `REPEAT_DELAY`
|
||||
is `-1.0`; `repeat_due()` returns 0 while `repeat_rate_known()` is false.
|
||||
📌 **The measurement has now landed, and it vindicates the instruction in the
|
||||
most awkward possible way: the guessed delay was nearly right and the guessed
|
||||
interval was off by 50 %.** 0.40 against a measured 0.402; 0.20 against a
|
||||
measured 0.134. Had both shipped, the half that was wrong would have been
|
||||
protected by the half that was right.
|
||||
|
||||
**One thing about the rate IS measured, and it narrows the question.** The game
|
||||
digitises the left stick to four direction bits at 61 % deflection, so it cannot
|
||||
see deflection magnitude at all — the repeat it drives *cannot* be
|
||||
faster-the-harder-you-push. That excludes the one competing model, so only two
|
||||
constants are open and a single measurement closes both.
|
||||
The Decoder measured, in Canary at an achieved 29.87 fps guest rate, **12 frames**
|
||||
from the press-triggered step to the first repeat and **4 frames** per step after
|
||||
it. Converted to seconds here because this port does not run at the guest's rate
|
||||
and it is the cadence that was measured: 12 / 29.87 = **0.402**, 4 / 29.87 =
|
||||
**0.134**.
|
||||
|
||||
## ⚠️ Adopting the rate breaks a green check, for the right reason
|
||||
### ⚠️ The two numbers are not equally well evidenced
|
||||
|
||||
`tools/port/verify-input` asserts *"a held stick is ONE step, not six"*. That row
|
||||
passes today **because the feature is inert**, i.e. it asserts the absence of the
|
||||
repeat. When a rate is adopted a held stick should produce further steps and that
|
||||
row will go red.
|
||||
No physical controller exists in the Decoder's container. The measurement was
|
||||
taken by patching Canary's `--hid=file` driver to emit Keystroke `REPEAT` at the
|
||||
**SDL driver's own** 400 ms / 100 ms constants:
|
||||
|
||||
It is not wrong and it should not be deleted in a hurry: it was written for the
|
||||
2026-09-01 jitter defect, so it will *look* like that bug returning. It has to be
|
||||
re-stated as "one step per deflection **plus** the measured repeat", with the
|
||||
jitter case still covered inside the delay window.
|
||||
* the **delay** came back as 402 ms — to within the frame quantum, *the constant
|
||||
that was fed in*. It confirms the instrument, not the game;
|
||||
* the **interval** came back as 133 ms against a fed-in 100 ms. That gap is the
|
||||
genuinely new fact: the game paces repeats to its own frame consumption rather
|
||||
than to the event stream.
|
||||
|
||||
So the interval is what the game does; the delay is what Xenia's SDL driver does,
|
||||
and the game was not observed to disagree. **One run** — the corpus's own two-run
|
||||
minimum is not met, and the source page says so itself.
|
||||
|
||||
**One thing about the rate was already measured, and it narrowed the question.**
|
||||
The game digitises the left stick to four direction bits at 61 % deflection, so
|
||||
it cannot see deflection magnitude at all — the repeat it drives *cannot* be
|
||||
faster-the-harder-you-push. That excluded the one competing model, so only two
|
||||
constants were ever open and a single measurement closed both.
|
||||
|
||||
## 🔴 Adopting the rate was predicted to break a green check. It did not, and that was worse
|
||||
|
||||
This page and `gamepad.gd` both said that `verify-input`'s row *"a held stick is
|
||||
ONE step, not six"* asserts the **absence** of the repeat, and would go red on
|
||||
adoption — looking like the 2026-09-01 jitter defect returning.
|
||||
|
||||
**Run on adoption day: the row stayed green.** `steps()` feeds axis values through
|
||||
the latch and never advances a clock, so it had never called `repeat_due()` at
|
||||
all. The row tests the *latch*, which the repeat does not touch. The prediction
|
||||
was reasoned rather than run.
|
||||
|
||||
What it hid is the real problem: the rate was about to ship into a harness with
|
||||
**no coverage of this feature whatsoever**, and that green line would have been
|
||||
read as coverage of it.
|
||||
|
||||
The fix was not to change that row. `verify-input` gains a `repeat` subject that
|
||||
holds a direction through the same latch and ticks `repeat_due()`, asserting
|
||||
**these two numbers** — a shape-only check ("it repeats eventually") would have
|
||||
passed on the 0.40 / 0.20 guess this port refused to ship. Five rows: nothing
|
||||
before the delay, the first repeat on the delay, the steady interval, cadence
|
||||
independent of frame rate (the code claims this in a comment, so it is asserted),
|
||||
and a direction change restarting the delay.
|
||||
|
||||
⚠️ **The origin is one frame, and it is not a tolerance.** `repeat_due()`'s first
|
||||
call only latches the direction; the clock accumulates from the call after it. In
|
||||
the port that first call is the frame the press is handled — the frame that
|
||||
produced the press-triggered step — which is the origin the finding measures its
|
||||
12 frames from. Measured from `t = 0` the harness read 0.433 against 0.402 and
|
||||
the tolerance would have had to be widened to hide a units mismatch.
|
||||
|
||||
## 🔴 It did not work on a real controller, and every instrument here said it did
|
||||
|
||||
**Reported 2026-09-13, by a human holding a real stick: one step, then nothing.**
|
||||
|
||||
`held_direction()`'s own comment said *"Polled at the DEVICE, never through
|
||||
`Input.is_action_pressed`"*. It was not. For the stick it read `_latched` — a
|
||||
reconstruction of the stick's position from the **event history** — and only the
|
||||
d-pad and keyboard branches actually polled anything.
|
||||
|
||||
`_latched` changes only when an event arrives. A stick held perfectly still
|
||||
sends nothing, so the reconstruction is only as good as the last event seen, and
|
||||
any single event reading below `RELEASE` — a spring settling, a deadzone-shaped
|
||||
value, a driver emitting a zero on focus change — clears it with nothing
|
||||
afterwards to set it back. From then on the port believes the stick is centred
|
||||
while the player is holding it. That is exactly "it moves one item and stops".
|
||||
|
||||
### Why nothing here caught it
|
||||
|
||||
Every instrument in this repo **supplies the input it then measures**:
|
||||
|
||||
| instrument | what it feeds |
|
||||
|---|---|
|
||||
| `verify-input`'s `repeat` rows | calls `repeat_due()` directly, after setting the latch through `accepts()` |
|
||||
| `--script=up,down,accept` | `InputEventAction` — bypasses the input map entirely |
|
||||
| `--script=hold:down:2.0` | injects one `InputEventJoypadMotion` |
|
||||
|
||||
All three agreed with each other and none of them agreed with the controller,
|
||||
because none of them read a device. This is the same shape as the 2026-09-01
|
||||
report that opened `gamepad.gd`: *a synthetic-input test asserts the code after
|
||||
the input map, never the input map itself* — one level deeper, and it caught us
|
||||
again with the lesson already written down.
|
||||
|
||||
### What changed
|
||||
|
||||
`held_direction()` now polls `Input.get_joy_axis()` against the game's own 0.61,
|
||||
which is what its comment always meant. The latch survives as a **fallback for
|
||||
injected events**, because `Input.parse_input_event()` does move `get_joy_axis()`
|
||||
but the harness must keep working if that ever changes.
|
||||
|
||||
And `--input-probe` prints what the devices report, on change:
|
||||
|
||||
```
|
||||
probe: [0] Generic X-Box pad Y=+1.000 X=+0.000 dpad=-- latched=1 held_direction=1 (ENTER=0.61 RELEASE=0.40)
|
||||
```
|
||||
|
||||
⚠️ **This fix is not verified.** It is a defect that matches the symptom exactly,
|
||||
found by reading, and the only instrument that can confirm it is a human holding
|
||||
a stick. The probe exists so that the answer is a measurement either way.
|
||||
|
||||
## What this does not claim
|
||||
|
||||
* That the repeat feels right. It cannot — it does not run.
|
||||
* That the repeat feels right. It runs now, and **only a human can answer that**:
|
||||
0.134 s is ~7.5 steps a second, and the play-test that opened this asked for
|
||||
*"slow enough to see which item is selected"*. If it reads as too fast, the
|
||||
100 ms constant is Xenia's and not the game's, and no capture in that
|
||||
container would have revealed it.
|
||||
* Any rate, or any bound on one. "Medium pace" is a direction, not a number, and
|
||||
it is not recorded anywhere as data.
|
||||
* That the d-pad and the stick repeat at the *same* rate. Both repeat; nobody
|
||||
|
||||
Reference in New Issue
Block a user