re: the decoder's timers are a double-tap latch on LB/LT, not key repeat

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
This commit is contained in:
sylph-decoder
2026-09-02 16:20:50 +00:00
parent cac31deccd
commit dcaa322146
2 changed files with 219 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
# ✅ 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 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`](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.