Files
Sylpheed/docs/re/pad-decoder-double-tap-not-key-repeat.md
sylph-decoder ad3d14ab26 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

153 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ✅ 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.
## 🔴 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.