A pad whose Select is KEYCODE_BACK quit the session on one press #235
Closed
enricobuehler
wants to merge 1 commits from
worktree-shield-select-back-quit into main
pull from: worktree-shield-select-back-quit
merge into: :main
:main
:worktree-ds-inhibit-storm
:worktree-security-review-2026-08-15-fixes
:worktree-pairing-friendly-device-name
:worktree-shield-select-back-quit
:worktree-android-canary-open-testing
:fix-host-cer-alias-null-key
:worktree-azure-trusted-signing
:worktree-audio-soft-limit-gain
:worktree-gamescope-exclusive-dpms
:fix/playnite-launcher-resolve
:worktree-macos-mic-rebuild-loop
:worktree-hyprland-cursor-mode-negotiation
:worktree-hd2-idle-recording-default
:worktree-steam-art-root-windows
:worktree-android-aaudio-shield-silence
:worktree-unpair-all-button
:worktree-appletv-present-depth
:worktree-av1-level-sentinel
:worktree-nix-plugin-runner-resolve
:worktree-release-0280
:worktree-console-plugin-update-all
:worktree-flatpak-skia-099-pin
:worktree-debian-ubuntu-support
:worktree-wowlan-support
:worktree-decky-shortcut-dup
:worktree-vk-level-gate-clamp
:worktree-edition-2024
:worktree-win-sleep-blockers
:worktree-rt-gpu-priority-defaults
:worktree-black-stream-fixes
:worktree-edid-lock-toggle
:worktree-gate-double-arm-fix
:chore/rust-safety-programme
:worktree-console-pinned-profile-library
:worktree-blackscreen-fixes
:build/web-silence-rollup-directive-warnings
:worktree-android-console-visual-refresh
:worktree-nixos-capped-compositor-exe-denied
:worktree-release-0270
:worktree-dm-helper-diagnosis
:worktree-gamescope-pin-bump-nobara
:worktree-notes-capsysnice-correction
:worktree-bazzite-docs-warning
:worktree-kwin-capability-identification
:worktree-gamescope-rpm-deb-builddeps
:worktree-pyrowave-wire-dead-code
:worktree-release-0260
:worktree-cursor-composite-hardening
:worktree-library-settings-origin-split
:worktree-native-decode-m0
:worktree-july-rpm-and-cursor
:worktree-presenter-cadence-wp1
:worktree-decky-brand-name
:worktree-win-amd-decoder-order
:worktree-stall-ride-through
:fix/pad-audio-wasapi-module-path
:feat/android-pad-audio
:worktree-adl-emul-probe
:worktree-haptics-m12-dry
:worktree-haptics-m9-richfb
:worktree-haptics-m11-settings
:worktree-apple-store-screenshots
:worktree-decky-slim-rework
:worktree-appstore-copy
:worktree-haptics-m10-wire
:worktree-haptics-m8-proto
:worktree-haptics-m7-windows
:worktree-haptics-m6-presenter
:worktree-wire-mtu-resilience
:worktree-haptics-m5-android
:worktree-haptics-m4-apple
:worktree-haptics-m3-rumble-engine
:worktree-android-presenter-023-triage
:worktree-vdisplay-resume-recovery
:worktree-kwin-vout-demirror
:fix/web-console-sweep
:fix/windows-web-console-lifecycle
:chore/windows-rerender-semantics
:fix/pf-encode-phase8
:fix/pf-encode-phase7
:feat/cursor-channel
:feat/desktop-mouse-mode
:feat/plugin-kit
:fix/network-split-standing-latency
:fix/windows-msix-arm64-and-manifest
:fix/encode-medium-tier
:fix/encode-rfi-taint-and-oob
:fix/encode-init-leak-and-twin-drift
:fix/encode-teardown-uaf
:feat/pyrowave-windows
:fix/ubuntu-2404-host-deb
:fix/ios-archive-provisioning
:merge/first-frame-latency
:feat/shared-clipboard-v2
:feat/apple-live-activities-widgets
:debug/touch-finger-logging
:feat/shared-clipboard
:midstream-resize
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Field report: pressing Select on the pad disconnected the stream, reported as "the session crashes".
It was neither a crash nor a drop. The host log said so itself —
client datagram stream endedplusvirtual display torn down (deliberate quit — keep-alive skipped)is a client that announced it was leaving.Why this identifies the cause without knowing the controller
Only two client paths raise a deliberate quit (
grep nativeDisconnectQuit):StreamScreen'sBackHandler, androuter.onExitChord.The chord is excluded by construction —
armExitposts a 1 s timer and releasing any member callsdisarmExit, so a tap always cancels. That leaves the back stack, and from aSOURCE_GAMEPADdeviceKEYCODE_BACKis the only keycode that reaches it: a mapped button is consumed in the gamepad branch, anything with a VK is consumed on the keycode path, volume/power go to the system, and aFLAG_FALLBACKBACK is swallowed.So a one-press quit proves the button's keycode on its own. Which pad was on the couch is not knowable from the logs and does not matter.
Root cause
Plenty of controllers deliver Select as the plain
KEYCODE_BACKa remote's Back uses, with noBUTTON_SELECTscancode behind it — the Android-TV shape, where every input device is expected to offer Back, reached whether the vendor prints "Back" on the button (NVIDIA's SHIELD controller) or "Select"/"View" (pads in an Android mode).Gamepad.buttonBithad no row forKEYCODE_BACK, so the press fell out ofdispatchKeyEvent's streaming gamepad branch unconsumed and landed onStreamScreen'sBackHandler— the deliberate-quit exit. One press, session over.The same gap meant those pads could not produce
BTN_BACKat all, so every shortcut built on Select was unreachable on exactly the devices whose users have no keyboard: the emergency exit chord this client's own start banner advertises ("Hold Select + Start + L1 + R1 to leave"), the mic mute, and the stats tier. The banner was promising a chord the hardware could not type.The change
New
Gamepad.padButtonBit(keyCode, flags)—buttonBitplus that one row — resolves a gamepad-sourced BACK toBTN_BACK, and MainActivity's streaming branch asks it instead.buttonBithad exactly one call site, which is what made this containable.BUTTON_SELECTis unaffected in both directions — it never had the bug.FLAG_FALLBACKstays excluded. Those are the synthetic BACK the framework raises after an unconsumedBUTTON_*press; forwarding one would put a phantom Select on the wire, and one landing while Start + L1 + R1 were held would complete the exit chord out of nowhere.mouseSideButtonmoves above the gamepad branch so a device that can be a mouse keeps its X1/X2 semantics. It answers null for everything that cannot be a mouse, so nothing else changes route.Behaviour change worth flagging
With a controller attached, Back no longer exits the stream — the 1 s chord does. That is the designed cross-client contract (SDL/Apple
DISCONNECT_HOLD) and the reason the hold exists at all, but it will read as a regression to anyone who had been using Back to leave.Verification
:kit:testDebugUnitTest+:app:testDebugUnitTestgreen,:app:compileDebugKotlinclean. NewPadButtonBitTest(4/4) pins the mapping, the fallback exclusion, that all three Select chords are reachable from a BACK-only pad, and that no other keycode moved.Not verified on glass — that needs a real pad: Select reaches the game, Back no longer quits, the chord still leaves.
Anyone can confirm the diagnosis on their own hardware with the shipped build: Controllers screen → input test → press Select. If every other button lights and the "Select" cell stays dark, that pad emits
KEYCODE_BACK(the grid keys that cell strictly onKEYCODE_BUTTON_SELECT). Safe to try — the probe consumes all pad keys while the test runs, so it will not back you out of the screen.Field report: pressing Select disconnected the stream. The host log was unambiguous about what it was NOT — "client datagram stream ended" plus "virtual display torn down (deliberate quit — keep-alive skipped)" is a client that said it was leaving, not a drop and not a compositor crash. Only two client paths raise that: StreamScreen's BackHandler, and the exit chord (router.onExitChord). The chord is excluded by construction — `armExit` posts a 1 s timer and releasing any member calls `disarmExit`, so a tap always cancels. That leaves the back stack, and from a SOURCE_GAMEPAD device KEYCODE_BACK is the ONLY keycode that reaches it: a mapped button is consumed in the gamepad branch, anything with a VK is consumed on the keycode path, volume/power go to the system, and a FLAG_FALLBACK BACK is swallowed. So a one-press quit identifies the button's keycode without knowing which controller was on the couch. Plenty of pads deliver Select as the plain KEYCODE_BACK a remote's Back uses, with no BUTTON_SELECT scancode behind it — the Android-TV shape, where every input device is expected to offer Back, reached whether the vendor prints "Back" on the button or "Select"/"View". `buttonBit` had no row for KEYCODE_BACK, so the press fell through unconsumed into StreamScreen's BackHandler, which is the deliberate-quit exit. One press, session over. The same gap meant those pads could not produce BTN_BACK at all, so every shortcut built on Select was unreachable on exactly the devices whose users have no keyboard: the emergency exit chord StreamScreen's own start banner advertises ("Hold Select + Start + L1 + R1 to leave"), the mic mute, the stats tier. New `Gamepad.padButtonBit(keyCode, flags)` — buttonBit plus that one row — resolves a gamepad-sourced BACK to BTN_BACK, and MainActivity's streaming branch asks it instead. It keys off the keycode, not the vendor, so it covers every pad with this behaviour; a pad that does carry BUTTON_SELECT is unaffected in both directions, having never had the bug. FLAG_FALLBACK events stay excluded: those are the synthetic BACK the framework raises after an unconsumed BUTTON_* press, and forwarding one would put a phantom Select on the wire (one landing while Start + L1 + R1 were held would complete the exit chord out of nowhere). A remote's or keyboard's BACK is neither mouse- nor gamepad-sourced, so it still leaves the stream — for a device with no pad on it that is the documented way out, and the banner says so. The mouse-side-button hook moves above the gamepad branch so a device that can be a mouse keeps its X1/X2 semantics; it answers null for everything that cannot be a mouse, so nothing else changes route. PadButtonBitTest pins the mapping, the fallback exclusion, that the three Select chords are now reachable from a BACK-only pad, and that no other keycode moved. Verified: :kit:testDebugUnitTest + :app:testDebugUnitTest green (PadButtonBitTest 4/4), :app:compileDebugKotlin clean. NOT yet verified on-glass — the behaviour needs a real pad: Select reaches the game, and Back no longer quits.Pull request closed