Files
Sylpheed/docs/re/pad-decoder-double-tap-not-key-repeat.md
sylph-decoder 6da9dac745 re: a THIRD timed channel in the decoder -- my "only two" was wrong, and +0xC4/+0xC8 are timings not masks
Chasing F1 I examined a counter block I had skipped. this+0x10 decrements
every update and fires output bit 0x4 at zero, guarded against the HELD
word by cfg +0xA0; it is re-armed from +0xC4 = 10 and +0xC8 = 8.

So my statement that "the only two timed inputs in the whole decoder are LB
and LT" is false, and two constants I tabulated as ring-word button masks
(B|Y and Y) are counter reload values. A number that is a valid mask AND a
valid duration cannot be told apart by its value, only by its use, and I
classified those two by value.

What it does not change: F1 is still not answered. This channel is guarded
by cfg +0xA0 = ring bit 1 = B, not a direction. The D-pad and left-stick
paths still carry no counter, so "a held DIRECTION has no repeat timer in
C_PAD_DECODER" stands, and the human's observation that it repeats in the
real game is consistent with the caveat this page already carried: the
repeat is in the layer above.

Explicitly NOT offering 10 and 8 as F1's two numbers. They belong to a
channel on a button that is not a direction, and at 60 units/s they are
0.167 s and 0.133 s -- about 7 moves a second, which does not match "a
medium pace, slow enough to see which item is selected". Reporting them as
the menu repeat would be the same read-a-config-infer-a-behaviour error
this correction is about.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-02 16:35:55 +00:00

7.1 KiB
Raw Blame History

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 H1does 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 1215) and the left stick (ring bits 47) 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.

🔴 Correction 2026-09-02 — there is a THIRD timed channel, and my "only two" was wrong

I wrote above: "the only two timed inputs in the whole decoder are LB and LT." That is false. Chasing F1 I examined a counter block I had skipped, and found a third timer on a different shape:

8220BB50  lwz  r11,16(r31)     ; this+0x10, a counter
8220BB5C  addic. r11,r11,-1    ; decrements EVERY update
8220BB64  bne -> skip          ; only when it reaches ZERO:
8220BB6C  lwz  r11,160(r31)    ; cfg +0xA0
8220BB70  lwz  r10,12(r10)     ;   ...against the HELD word
8220BBAC  ori  r11,r11,0x4     ; fires output bit 0x4

and it is re-armed from two more constants I had mis-read as button masks:

8220CB50  lwz r11,196(r31)     ; +0xC4 = 10   -> this+0x10
8220CB58  lwz r11,200(r31)     ; +0xC8 =  8   -> this+0x14

⚠️ So +0xC4 and +0xC8 are TIMINGS, not masks. My constructor table read them as ring-word button sets (B|Y and Y) because 10 and 8 are legal masks. They are counter reload values, exactly like +0xB4/+0xBC = 10. A number that is a valid mask and a valid duration cannot be told apart by its value — only by its use, and I classified these by value.

What it does NOT change

F1 is still not answered here. This third channel is guarded by cfg +0xA0, which is ring bit 1 — B, not a direction. The D-pad and left-stick paths still carry no counter, so the finding stands where it matters:

A held DIRECTION has no repeat timer in C_PAD_DECODER.

The human watched the real game and reports that a held direction does repeat. Both can be true, and the caveat this page already carried is the reason: the repeat is in the layer above. That is where F1 must be answered — the consumer of this+0x24C, or a capture holding a direction on a real menu.

⚠️ And I am not offering 10 and 8 as F1's two numbers. They are this channel's constants, on a button that is not a direction. At 60 units/s they would be 0.167 s and 0.133 s — about 7 moves a second, which does not match "a medium pace, slow enough to see which item is selected". Reporting them as the menu repeat would be the same error as reading a config value and inferring a behaviour, which is the error this correction is about.