Answers H1's first half with a negative, and decodes what the timing constants are for. C_PAD_DECODER's +0xB4/+0xB8 and +0xBC/+0xC0 (10 and 90, twice) are not a key-repeat delay/interval pair. They are two identical channels of a DOUBLE-TAP detector, on cfg +0x74 (LT) and +0x70 (LB), firing output bits 0x20 and 0x40. The mechanism, from the image: both timers tick down once per update; a press arms the short timer to 10; a RELEASE adds 1000 to it as a flag stored inside the counter, which is why the tick watches for exactly 1000 and clears both there; a second press while the flag is set arms the long timer to 90; and while the long timer runs the output bit is asserted on EVERY update, with the short timer re-armed to 1010 each time. So it is a latch, not a repeat: a double-tap opens a 90-update window and the bit is held for its whole duration. That is a dash or barrel-roll shape, which fits the buttons it is wired to. H1: the directions have NO timer. The D-pad and left stick reach the output word through bare mask tests -- the four left-stick literals at 0x8220C458, C474, C490, C4AC, and DPAD DOWN through cfg +0xA4 -- with no counter loaded, decremented or tested on any of those paths. On the evidence of this layer a held direction does not repeat. Reach: one layer. A menu could implement repeat on top of a held bit, and this says nothing about that -- but it does establish the repeat is not in the shared decoder, so it would have to be per-screen. "One step per deflection" stays authored for the port; this narrows rather than settles. Refutation of my own earlier note that a decoder "is where a game normally puts its repeat timing, its edge detection and its button remap": remap survives, edge detection survives, repeat timing is REFUTED. That clause was a prior about how games are written, not a reading of this one. Also commits tools/ppc-dis, the minimal PowerPC disassembler this corpus has been rebuilding in scratch and losing to container restarts three times in one session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
4.8 KiB
✅ The decoder's timers are a double-tap detector on LB and LT — and directions have no repeat in this layer
Status: ✅ decoded from the image. Instrument: ⟨image⟩ — /image/sylpheed.pe
via tools/ppc-dis, database not consulted. 2026-09-02.
Answers the first half of H1 — does a held direction repeat in the menus, and at what rate? — with a negative, and decodes what the timers are actually for.
What the timing constants really are
C_PAD_DECODER's constructor sub_8220B610 writes five values that look like a
key-repeat pair — +0xB4 = 10, +0xB8 = 90, +0xBC = 10, +0xC0 = 90. They are
two identical channels of a double-tap detector, and the update
sub_8220B8C0 consumes them like this:
| channel | button | short timer | long timer | output bit |
|---|---|---|---|---|
| A | cfg +0x74 = LT |
this+0 ← +0xB4 = 10 |
this+4 ← +0xB8 = 90 |
0x20 |
| B | cfg +0x70 = LB |
this+8 ← +0xBC = 10 |
this+12 ← +0xC0 = 90 |
0x40 |
Buttons named in the decoder's own bit numbering
(input-button-numbering-is-remapped.md),
not XINPUT's.
The mechanism, in the order the code runs it
1 — both timers tick down once per update (0x8220BAE0):
if this+0 != 0: this+0 -= 1 ; if it lands on exactly 1000: this+0 = 0, this+4 = 0
if this+4 != 0: this+4 -= 1
2 — a press arms the short timer (0x8220BF74): on PRESSED ∧ this+0 == 0,
this+0 = 10.
3 — a release adds 1000 to it (0x8220BFC8, reading 20(r6) = RELEASED):
this+0 += 1000 if it is still running, else 0. The +1000 is a flag stored
inside the counter — "this press has already been released" — which is why the
tick in step 1 watches for the value 1000 and clears both timers there.
4 — a second press while the flag is set arms the long timer
(0x8220BF44): on this+0 > 1000 ∧ this+4 == 0 ∧ PRESSED, this+4 = 90.
5 — while the long timer runs, the output bit fires EVERY frame
(0x8220C0F8):
if this+4 > 0:
this+0 = 10 + 1000 ; re-armed and flagged
this+0x24C |= 0x20 ; the output bit, set on every update
📌 So it is not a repeat, it is a latch. A double-tap opens a 90-update window, and for its whole duration the output bit is asserted continuously. That is the shape of a dash / barrel-roll, not a menu cursor — which fits the buttons it is wired to.
🔴 The H1 answer: no key-repeat in this layer
The directions have no timer at all. The D-pad (ring bits 12–15) and the left
stick (ring bits 4–7) reach the output word through bare mask tests — the four
left-stick literals at 0x8220C458, 0x8220C474, 0x8220C490, 0x8220C4AC, and
DPAD DOWN through cfg +0xA4 — with no counter loaded, decremented or tested
on any of those paths (data/input-decoder-output-map.txt).
On the evidence of
C_PAD_DECODER, a held direction does not repeat. The only two timed inputs in the whole decoder are LB and LT, and what they do is latch, not repeat.
⚠️ Reach, and it matters here. This is one layer. The decoder hands a word to the menus; a menu could implement its own repeat on top of a held bit, and this page says nothing about that. What it does establish is that the repeat is not in the shared input decoder, so it would have to be per-screen — which also means it cannot be answered once for all screens from this function.
For the port: "one step per deflection" remains authored, and this narrows
rather than settles it. The next step is the layer above — the consumer of
this+0x24C — or a capture of a held direction on a real menu, counting cursor
moves against presents.
Refutation attempt, recorded per the adversarial duty
Target: my own earlier note in
input-pad-read-path.md that "a decoder between
the raw wButtons and the menus is where a game normally puts its repeat timing,
its edge detection and its button remap."
Result: two thirds SURVIVE, one third is REFUTED. The remap is there
(24 bits, bijective) and the edge detection is there (+12 held, +16 pressed,
+20 released). The repeat timing is not. The reasoning was "this is where it
normally goes", which is a prior about how games are written, not a reading of
this one — and it is exactly the kind of inference this corpus keeps having to
withdraw.
Reach
⟨image⟩, so it holds for every screen — that is the point of doing it statically. It says nothing about layers above the decoder, and nothing here has been confirmed against a running capture: no row may be labelled measured. The obvious check is to hold a direction on a real menu and count cursor moves per present.