Skip to content

fix(pad): give edge detection its own read-miss carry (#271) - #289

Merged
NathanNeurotic merged 1 commit into
masterfrom
claude/271-edge-view
Jul 28, 2026
Merged

fix(pad): give edge detection its own read-miss carry (#271)#289
NathanNeurotic merged 1 commit into
masterfrom
claude/271-edge-view

Conversation

@NathanNeurotic

@NathanNeurotic NathanNeurotic commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Follow-up to #288. Addresses the remaining half of #271.

What Beta-3442 told us

zackcage6, with video:

  • Hold is fixed"the list now repeats smoothly with no lag or sluggish feel"
  • "Scrolling left/right still does jump 2-3 games and hangs on 2-3 games"
  • 🔑 "i do not hang or pause during scrolling, my scroll pattern is continuous, one button press at a time, this issue does occur also in settings menu"

That last line rules out most of what was being chased. It is not key-repeat (he's tapping, and the repeat path is confirmed working) and not coverflow (it happens in the settings dialog too).

It also rules it out on code grounds, not just statistics: getKey() tests getKeyOn() first, and that branch re-arms delaycnt (pad.c:1019-1024). A long poll gap therefore can never reach the repeat leg on a fresh press. Every extra move is a separate getKeyOn edge.

Root cause — introduced by #288's own carry

paddata serves two detectors whose failure modes are opposite, and they were sharing one bounded carry:

View Consumer Over-carrying Under-carrying
Level getKeyPresseddelaycnt invents auto-repeats (the #272 bug)
Edge getKeyOn / getKeyOff harmless invents presses

The 48 ms bound is load-bearing for the level view — it is what makes the hold case correct. But for a ~150 ms tap at 33 ms frames it expires mid-press:

F0  read OK, LEFT down        -> getKeyOn -> move #1
F1  miss, carry (33ms)        -> no edge
F2  miss, carry (66ms)        -> no edge
F3  miss, 66 >= 48 -> DROPPED -> global paddata falls to 0, finger still down
F4  read OK, LEFT STILL DOWN  -> 0 -> 1 -> getKeyOn -> move #2 from ONE press

Fix

Split the views. edgedata/oldedgedata carry a missed poll's bits with no time bound and feed only getKeyOn/getKeyOff; paddata keeps its bounded carry and feeds everything else.

Safe by construction in the direction that matters: an unbounded carry on the edge view can only ever suppress a second edge — it cannot invent one, because an edge requires a 0 → 1 transition and carrying a 1 forward never produces that. It self-corrects on the first successful read showing the button clear, and it reduces dropped presses too, since a miss no longer advances the edge state.

Why not just raise PAD_READ_CARRY_MS

Because it looks obvious and is wrong. To bridge a whole tap the window must exceed the user's release gap — at which point the carry is still asserting the previous tap when the next one arrives, the 0 → 1 never happens, and presses get eaten. That trades jumps for hangs.

Residual — stated, not hidden

If a genuine release is never sampled at all (the whole release gap falls inside a GUI-thread stall), the following press is suppressed: an extra move becomes a dropped one. Strictly the better failure, but real.

This also does not repair the separate "tap falls entirely between two polls" path. That needs the frame stalls themselves addressed — prime suspect is initializePad() running inline on the GUI thread from readPad() (pad.c:485), ~250-300 ms on the healthy path. Already instrumented: it bumps SH on the debug HUD, so it can be tested with no new build.

Verification

  • make clean && make opl.elf green on ps2dev:latest; clang-format clean
  • Boot behaviour unchanged — edgedata is BSS-zero exactly like paddata
  • Not hardware-tested.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved button press and release detection when controller reads are briefly missed.
    • Prevented held buttons from being incorrectly reported as released and pressed again after an interrupted poll.
    • Preserved consistent key-on and key-off behavior across transient controller read failures.

Beta-3442 hardware result (zackcage6, video): the HOLD half of #272 is fixed --
"the list now repeats smoothly with no lag or sluggish feel". Discrete taps
still misbehave: "scrolling left/right still does jump 2-3 games and hangs on
2-3 games", and crucially "i do not hang or pause during scrolling, my scroll
pattern is continuous, ONE BUTTON PRESS AT A TIME, this issue does occur also in
settings menu".

That rules out most of what was being chased. It is not key-repeat (he is
tapping, and the repeat path is confirmed working), and it is not coverflow (it
happens in the settings dialog too). Since getKey() tests getKeyOn() FIRST and
that branch re-arms delaycnt, a long poll gap can never reach the repeat leg on
a fresh press -- so every extra move is a separate getKeyOn EDGE, not a repeat.

ROOT CAUSE -- introduced by the #272 carry itself. paddata serves two detectors
with opposite failure modes, and they were sharing one bounded carry:

  * LEVEL (getKeyPressed -> delaycnt): over-carrying INVENTS auto-repeats, so it
    must stay bounded. PAD_READ_CARRY_MS is load-bearing and is what fixed the
    hold case.
  * EDGE (getKeyOn/getKeyOff): under-carrying INVENTS presses.

With a ~150ms tap and 33ms frames, the 48ms budget expires mid-press:

    F0  read OK, LEFT down        -> getKeyOn -> move #1
    F1  miss, carry (33ms)        -> no edge
    F2  miss, carry (66ms)        -> no edge
    F3  miss, 66 >= 48 -> DROPPED -> global paddata falls to 0, finger still down
    F4  read OK, LEFT STILL DOWN  -> 0 -> 1 -> getKeyOn -> move #2 from ONE press

FIX. Split the two views. edgedata/oldedgedata carry a missed poll's bits with
NO time bound and feed only getKeyOn/getKeyOff; paddata keeps its bounded carry
and feeds everything else. Safe by construction in the direction that matters:
an unbounded carry on the EDGE view can only SUPPRESS a second edge, never
invent one, because an edge needs a 0 -> 1 transition and carrying a 1 forward
cannot produce that. It self-corrects on the first successful read showing the
button clear, and it also reduces dropped presses, since a miss no longer
advances the edge state.

NOT raising PAD_READ_CARRY_MS, which looks like the obvious fix and is wrong: to
bridge a whole tap the window must exceed the user's RELEASE gap, at which point
the carry still asserts the previous tap when the next arrives, the 0 -> 1 never
happens, and presses get eaten. That trades jumps for hangs.

Residual, stated rather than hidden: if a genuine release is never sampled at
all (the whole release gap falls inside a GUI-thread stall) the next press is
suppressed -- an extra move becomes a dropped one. Strictly the better failure,
but real, and it does not repair the separate "tap falls entirely between two
polls" path. That one needs the frame stalls themselves addressed; the prime
suspect is initializePad() running inline on the GUI thread from readPad()
(~250-300ms), which is already instrumented as SH on the debug HUD.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1c44adaa-cfd3-4acd-ac22-61fcff76872a

📥 Commits

Reviewing files that changed from the base of the PR and between 7877cb3 and 75f1df1.

📒 Files selected for processing (1)
  • src/pad.c
📜 Recent review details
⏰ Context from checks skipped due to timeout. (17)
  • GitHub Check: github-advanced-security
  • GitHub Check: build-variants-ps2dev-latest (EXTRA_FEATURES=0, PADEMU=0)
  • GitHub Check: build-ps2dev-latest
  • GitHub Check: build-variants (EXTRA_FEATURES=0, PADEMU=0)
  • GitHub Check: build-debug (iopcore_debug, @sha256:c64ae69c9817865ed98ff054e4ae5360b9e280ed952c97946bca95d9d35be...
  • GitHub Check: build-debug (DTL_T10000=1, @sha256:c64ae69c9817865ed98ff054e4ae5360b9e280ed952c97946bca95d9d35be995)
  • GitHub Check: check-format
  • GitHub Check: build
  • GitHub Check: Analyze (python)
  • GitHub Check: Analyze (c-cpp)
  • GitHub Check: build-debug (DTL_T10000=1, @sha256:c64ae69c9817865ed98ff054e4ae5360b9e280ed952c97946bca95d9d35be995)
  • GitHub Check: build-debug-ps2dev-latest (eesio_debug)
  • GitHub Check: build-debug-ps2dev-latest (iopcore_debug)
  • GitHub Check: build-variants (EXTRA_FEATURES=1, PADEMU=0)
  • GitHub Check: build-variants (EXTRA_FEATURES=0, PADEMU=0)
  • GitHub Check: build-variants (EXTRA_FEATURES=1, PADEMU=1)
  • GitHub Check: build-variants-ps2dev-latest (EXTRA_FEATURES=1, PADEMU=1)
🔇 Additional comments (5)
src/pad.c (5)

156-188: LGTM!


569-587: 🎯 Functional Correctness

Run the missed-read sequence on real hardware.

Build and formatting checks cannot exercise the SIO2/freepad miss path. Verify a held button across misses both below and beyond PAD_READ_CARRY_MS, followed by a sampled release and repress, produces one press edge, no phantom repeats, and one release edge.


966-969: LGTM!


1105-1107: LGTM!

Also applies to: 1122-1123


569-587: 🎯 Functional Correctness

No extra reset here. updatePadState() ignores intermediate non-ready states, and pad->paddata is already cleared on disconnect; the carry on ready-state misses is intentional and resetting it here would reintroduce duplicate edge handling.

			> Likely an incorrect or invalid review comment.

📝 Walkthrough

Walkthrough

readPad() now tracks separate level and edge button states, carries missed-read edge state without a timeout, and uses the edge view for key-on/off detection.

Changes

Pad edge-state handling

Layer / File(s) Summary
Separate level and edge views
src/pad.c
Adds edge-state storage and updates successful or missed pad reads so level data remains time-limited while edge data is carried through ready-state misses.
Frame transitions and key detection
src/pad.c
Rolls and clears edge data once per poll cycle, then detects key-on and key-off transitions from edge-state changes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: separating edge detection from bounded read-miss carry handling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/271-edge-view

Comment @coderabbitai help to get the list of available commands.

@NathanNeurotic
NathanNeurotic merged commit 4994a33 into master Jul 28, 2026
66 checks passed
@NathanNeurotic
NathanNeurotic deleted the claude/271-edge-view branch July 28, 2026 23:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant