# ✅ 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`](../../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`](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`](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`](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.