Skip to content

[DO NOT MERGE - TEST ONLY] fix(pad/sound): composite #340 fix -- async pad re-init + off-thread SFX + miss-pause - #358

Draft
NathanNeurotic wants to merge 9 commits into
masterfrom
fix/340-composite
Draft

[DO NOT MERGE - TEST ONLY] fix(pad/sound): composite #340 fix -- async pad re-init + off-thread SFX + miss-pause#358
NathanNeurotic wants to merge 9 commits into
masterfrom
fix/340-composite

Conversation

@NathanNeurotic

Copy link
Copy Markdown
Owner

TEST BUILD D — the composite fix the HUD photos dictated. Do not merge; a clean PR replaces it if it wins.

What the photos showed (the payoff of the counters)

init:9(306/306ms) and init:5(~308-908ms) — the inline initializePad re-init ran 5–9 times per session at 0.3–0.9 s each, every run a GUI-thread blackout: that is the felt hang. Plus brst:~10-25 (0.2–0.4 s continuous blind windows swallowing whole taps) and SFX:0/352ms (one cursor-sound RPC blocking the GUI for a third of a second). The symptom was a composite — which is why every single-layer fix 'didn't do it'.

What this build changes (stacked on #355's per-pad miss-pause + counters)

  1. Pad re-init never blocks the GUI again: the blocking DelayThread init sequence is now a per-frame state machine (same stage semantics, wall-clock budgets, 2 s hard cap), and failed analog self-heals back off exponentially (1 s → 16 s) instead of storming a flapping freepad every second.
  2. Sounds never block the GUI: the audsrv RPC moved to a dedicated dispatch thread (two-producer-safe, quiesced across theme reloads and audio shutdown); a congested IOP now drops a tick instead of freezing the menu.
  3. poll: HUD field fixed: it inherited the whole boot (8–13 s readings); now it starts counting when the main loop starts polling, so in-session GUI stalls are finally measurable.

For Zack

Same torture test, Debug Colors ON. Expected: hold-scroll smooth, no 0.3–0.9 s freezes, no dead zones after pausing; init: may still count runs but their (…ms) wall costs no longer stall input; poll: should now read ~16–33 ms (a big number = real GUI stall, finally visible). Misses/bursts will still occur (transport-level; the pause absorbs them). Falsified if hangs persist with init climbing — then the machine's SIO2 traffic itself is the problem, not its blocking.

Adversarially reviewed before push: two-producer race and lazy-create double-thread corruption fixed, frame-budget inflation replaced with wall-clock budgets, mid-init unplug housekeeping, quiesce across audsrv shutdown/theme reload.

🤖 Generated with Claude Code

NathanNeurotic and others added 5 commits August 3, 2026 16:11
…ss counters (#340)

A pad-read MISS (connected pad, no fresh sample this poll) leaves the held
buttons out of global paddata for the frame while edgedata still carries the
last valid sample. The baseline repeat loop treated that as a release and
re-armed the full 3x initial delay (300-1500 ms depending on scroll speed),
so recurring misses on real hardware starved auto-repeat entirely -- the
'hangs on the 3rd highlighted item' pattern in #340/#272. Pause the countdown
for keys whose edgedata bit survives a miss poll instead; a key genuinely
released during a blind window still resets on the first good read, and an
emulator (which never misses) sees byte-for-byte baseline behavior. This
restores the hold-repeat feel HW-validated at Beta-3442 and lost in the
PR #328 revert.

Also: readMisses/missBurst/missBurstMax had no writers since that revert
(the Debug HUD always rendered miss:0) -- count them again, and treat a
miss poll with a carried held sample as input activity so the 250-600 ms
inline initializePad self-heal cannot fire mid-navigation off misses that
masked the user's own presses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fafc14d shipped the toggle with no reader anywhere -- readLeftJoy never
checked gEnableAnalogNav and guiShowControllerConfig never synced the
dialog row, so the checkbox always rendered off and its edits went
nowhere. Stick sensitivity already carries the real control: Off maps to
a 128 deadzone which fully gates an axis out of d-pad navigation, per
axis. Remove the dead row, enum id, global, and config key rather than
wiring a redundant switch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first cut decided 'miss' per poll: any pad producing a fresh sample
masked another pad's miss, so with two pads -- or a PADEMU ds34 alongside
a native pad, exactly the USB-heavy setup that starves SIO2 the most --
the pause never engaged and the HUD counted nothing. Track the held bits
of each ready pad that produced no sample (pollMissedHeld) and drive the
repeat pause, the activity stamp, and the miss counters from that. Also
drop a carried sample once nothing supplies its port any more (a ds34
pad that stopped reporting), so a stale press cannot pin the repeat
pause or the self-heal idle gate open forever.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rame state machine (#340)

The HW Debug-HUD photos finally measured the felt hangs: initializePad
ran 5-9 times PER SESSION (init:N) at 306-908 ms each -- every run a
GUI-thread blackout from DelayThread polling loops, triggered ~1 s apart
by a chronically flapping freepad. Unroll the same init sequence into a
state machine advanced one status sample per rendered frame: identical
stage semantics (retryable half-built mode tables, best-effort pressure/
actuator setup, PR #151's >=100 ms request budgets), zero GUI stall.
Stage budgets are wall-clock milliseconds (frame counts would inflate
under exactly the stretched frames being fixed) with a 2 s total cap so
a wedged freepad can never hold a port unread for long. The pad is not
read while its machine runs (matching the old semantics); its carried
held sample feeds the repeat pause, not the miss counters, and a mid-
init unplug performs the full unplug housekeeping the machine's own
state sampling would otherwise swallow.

Failed self-heals back off exponentially (60 -> 960 good reads): each
attempt adds mode traffic to the contended bus that caused it, and the
photos showed heal storms against a pad that never recovers analog.
Reconnect-edge runs do not consume backoff steps, and a fresh physical
connection resets it. Boot-time init keeps the proven blocking sequence
(no GUI exists to stall yet).

Also stop the worst-poll HUD tracker from inheriting the whole boot
(photos read poll:8-13 s, hiding real in-session stalls): reseed the
poll clock when the main loop starts polling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hread (#340)

The HW photos measured a single cursor-tick audsrv_ch_play_adpcm RPC
blocking the GUI thread for 352 ms under IOP contention (SFX:0/352ms).
Queue sounds to a small dedicated thread (prio 45, below GUI/IO) via a
ring instead; a full ring drops the sound -- a skipped tick beats a
stalled menu. Rumble hooks, gates, the 45 ms cursor rate limit, and
channel rotation stay on the producer side; the RPC wall time is still
measured into the same HUD fields, now reporting IOP congestion without
stalling anyone.

Concurrency contract (per adversarial review): sfxPlay has TWO
producers -- the GUI thread and the prio-30 ioman worker (BD connect/
disconnect sounds), which preempts it -- so the slot claim is DIntr/
EIntr-bracketed and the thread+semaphore are created once from
audioInit, which happens-before any producer (both gate on
audio_initialized). sfxInit (theme reload rewrites sfx[]) and audioEnd
(audsrv_quit / NBD IOP reset) park the consumer via a bounded quiesce
so no RPC can cross either. Stale queued cursor ticks (>100 ms) are
dropped unplayed so a cleared IOP stall does not chirp the backlog.
Synchronous fallback if the thread cannot be created.

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

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4e8efaf5-10af-4b64-a7ff-49d198e4554e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

NathanNeurotic and others added 2 commits August 4, 2026 05:05
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Zack's first composite-build HUD line found the last hang:
'init:3 (2002/2002ms)' -- his freepad never completes the re-init
sequence, so every heal run burned to the 2 s total cap, and because
readPad suspended reads for the machine's lifetime each run was a 2 s
hold-scroll freeze, while everything else finally measured clean
(miss:2, poll:34ms, SFX:0/1ms; taps 'tight, chances to miss near
none').

The no-reads-during-init rule was inherited from the blocking design,
where it was an accident of the thread being busy -- the protocol has
no such rule. freepad keeps serving reads across a mode change, and
the miss/digital frames it may return during the transition are
exactly what the per-pad miss machinery and repeat pause already
absorb. So: tick the machine, then read normally. A wedged re-init
now costs literally nothing felt; the machine idles to its cap in the
background while input flows.

Drops the pollPadsInit special-casing (reads during init now count
honestly in the miss/burst diagnostics).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@NathanNeurotic

Copy link
Copy Markdown
Owner Author

v2 pushed (2924b3e8) — targets the last hang Zack's HUD line isolated.

His line init:3 (2002/2002ms) meant: the re-init machine never completes on his freepad and burns to the 2 s cap — and v1 suspended pad reads for the machine's lifetime, so each run was a 2 s hold-scroll freeze (while taps, misses, poll, and SFX all finally measured clean). The no-reads-during-init rule was an accident of the old blocking design, not a protocol requirement — v2 ticks the machine and keeps reading the pad normally; miss/digital frames during a mode transition are absorbed by the existing per-pad miss machinery. A wedged re-init now costs nothing felt.

Expected on v2: hold-scroll smooth end-to-end, including the moments init: runs fire. init:(…ms) may still read 2002 on this pad — that's now cosmetic. Misses counted during init windows are honest (reads are live there).

🤖 Generated with Claude Code

…ge (#340)

Zack's v2 HUD line closed the case on the last hang: with reads kept
live through the re-init machine, the reads MISS for the window anyway
(miss:2 -> 47, burst 28) -- freepad's vblank task stops serving
READ_DATA while a mode-change request is in flight, and his pad never
completes one (three runs per session, all at the 2002 ms total cap).
So every heal attempt is a ~2 s input outage that no EE-side reading,
pausing, or scheduling can bridge: the heal itself is the hang.

Stop healing after PAD_HEAL_GIVE_UP (2) consecutive failed heals. The
d-pad is fully functional in digital mode, so on a pad that cannot
negotiate DualShock mode this trades a dead analog re-arm nobody was
winning for uninterrupted input. A reconnect edge (real replug) resets
healFailures and re-arms healing; healthy pads that heal on the first
or second try are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@NathanNeurotic

Copy link
Copy Markdown
Owner Author

v3 pushed (7ab3bd16) — the heal itself was the hang; now it gives up.

v2's HUD line was decisive: with reads kept live through the re-init machine, they miss anyway (miss:47 brst:0/28 vs v1's miss:2) — freepad stops serving READ_DATA entirely while a mode-change request is in flight, and this pad never completes one (init:3 (2002/2002ms), every run capped). Every self-heal attempt is therefore a guaranteed ~2 s input outage that nothing EE-side can bridge.

v3: after 2 consecutive failed heals, healing stops for the session (a real replug re-arms it). The D-pad is fully functional in digital mode, so this trades an analog re-arm that was never succeeding for uninterrupted input.

Expected on v3: at most ~2 brief hitches early in a session while the give-up threshold is reached, then hold-scroll continuous for the rest of the session. init: should stop climbing after 2-3. Falsified if hangs keep recurring all session with init: still climbing (would mean heals are firing past the give-up — a logic bug) or with init: frozen (would mean a different mechanism entirely; poll:/miss: decide which).

🤖 Generated with Claude Code

#340 v4)

The v3 give-up was dead code on a flappy pad: every DISCONN->ready
reconnect edge reset healFailures and launched a full, un-counted init
run, so each flap re-armed up to three ~2 s mode-window read outages
(the hangs), and the DISCONN transition dropped the held sample at
once, forging a release+re-press on 1-2-poll flaps (the skips and
phantom confirms).

v4 discriminates by dwell time -- the one observable at the edge.
freepad's error-driven flaps reconnect within 1-2 polls (~16-33 ms); a
human replug dwells hundreds of ms. Short-dwell DISCONN polls are now
treated exactly like read misses (held sample carried: repeat pauses,
no forged edge) and a short-dwell reconnect runs NO init and does NOT
reset the give-up budget. Only a dwell past PAD_FLAP_DWELL_MS (300)
does the clean-slate housekeeping, and only its reconnect edge grants
a fresh init + give-up reset. Every failed init run now counts toward
the give-up regardless of trigger, and the machine aborts un-counted
if the user resumed input before the mode request was actually issued.

Net effect on a chronically flapping pad: at most 2 failed init runs
per session (plus any real replugs), flaps degrade to ordinary
miss-pauses, and no pad mode request is ever issued during or within
a second of user input.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@NathanNeurotic

Copy link
Copy Markdown
Owner Author

v4 pushed (b212b237) — and the investigation flipped with Zack's official-main data point.

The reframe

Zack's direct tests: current official OPL main is smooth on his console (and sOPL — identified as mystyq/Stable-Open-PS2-Loader, an official fork with byte-identical pad.c and no MMCE code — is too). Official master hasn't changed since June, builds on the same modern SDK, and shares the same miss-amplifying pad semantics we started from. Conclusion: the misses/flaps aren't the SDK's doing and aren't amplitude-of-symptom differences — they're manufactured by something only RiptOPL runs in its menu.

The prime suspect: mmceman.irx

  • RiptOPL is the only build in Zack's comparison set that loads a sio2man interposer under freepad at menu time. Official/sOPL/uOPL contain zero MMCE code.
  • It loads right after GUI_INIT_DONE whenever mmce_gameid=1 — which was the default from 2024-08 to 2026-07-28, is written unconditionally on every settings save, and overrides today's default from any config saved before the flip. A July-era settings_riptopl.cfg on a USB-only rig with no MMCE hardware still loads it today.
  • The hook permanently replaces sio2man's sio2_transfer export and relinks freepad's import table; every MMCE command takes sio2man's exclusive lock and swaps the global SIO2 interrupt handler; with no MMCE hardware each probe burns its full 200 ms timeout while freepad's vblank reads block (~12-13 missed polls) — matching the measured 28-poll burst signature. Precedents: RiptOPL [ISSUE]: OPL get stuck after "config loaded from mass0:" notification #254 (this exact module, total pad death on three console models) and ps2sdk PR [ISSUE][SLES_520.55]: Harry Potter and the Philosopher’s Stone on mx4sio with vmc ps2homebrew/Open-PS2-Loader#898's own admission.

The 60-second decisive test for Zack (before anything else)

  1. Settings → MMCE Settings → Send GameID on Launch: OFF; MMCE device: OFF; Save Changes.
  2. FULL POWER CYCLE (the resident module survives an in-session toggle — reboot is mandatory).
  3. Scroll. Also worth one in-session A/B first: Audio Settings → SFX OFF (official's run was silent — different config file — so per-scroll audsrv RPCs are a second fork-only delta this discriminates instantly).
  4. Send us settings_riptopl.cfg from the USB stick so we can read mmce_gameid/enable_sfx/enable_bgm/enable_rumble instead of inferring.

What v4 fixes regardless

The v3 audit found its give-up was unreachable on a flappy pad (every flap reset it and ran an un-counted init = up to ~6 s outage per flap) and the flap-forged release edges (the skipping) were still live. v4 discriminates flap-vs-replug by DISCONN dwell (300 ms): flaps now degrade to ordinary miss-pauses with no forged edges, no init runs, no give-up reset; every failed init counts toward give-up; no mode request is ever issued during input. Even with mmceman still hooked, v4 should reduce the symptom to brief hitches — but killing mmceman via the config test addresses the disease.

🤖 Generated with Claude Code

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