* fix(input): Auto relative mouse stays off for direct touch without a mouse
* fix(input): re-check relative mouse when an input device changes
* fix(input): reapply relative mouse after agent readiness
* fix(input): only the current session clears the agent-ready hook and readiness resets relative mouse retries
* fix(dual-screen): drop the Dashboard notifications page and its notification listener
* test(session): freeze the clock in the memory-pressure policy harness
* fix(gog): owned games the library listing leaves out stay owned
Since the GOG library is read from account/getFilteredProducts, a game
that listing does not return (one hidden in GOG Galaxy, for example)
dropped out of the library, and with it out of the GOG tab, which shows
owned games and the shelves. 0.5.0 read every owned id from
user/data/games and showed them.
The sync now takes the union of the listing and user/data/games. The
listing still names most games at once, and an owned id it lacks is
fetched like any other.
* fix(gog): the GOG tab's search reaches GOG's catalog
GogStoreCatalog.search was never wired to the backend, so searching the
GOG tab only filtered the owned library and the featured shelves; a game
that is neither could not be found. GogBackend now answers search from
GOG's catalog, like the other stores.
* fix(gog): keep cached owned games when the account list is unavailable
fix(desktop): catalog AppImages start from the desktop menu through the shipped runtime
RPCS3 from the desktop menu exited at once: its own AppImage runtime runs
dwarfsextract from a memfd, which PRoot cannot execute (EACCES), and it has no
fallback. Emulator rail launches already avoid this by starting arm64 images
through the shipped uruntime (#1191); the menu entries the catalog writes did not.
The rule now lives in one launcher, droiddeck-appimage-exec, used by the session
and by the catalog's menu entries. Entries written by older versions are moved
to it with the existing menu migration.
* fix(desktop): a GitHub link to one AppImage installs and updates that variant
* fix(desktop): latest-release links, bad escapes and 32/64-bit variants
A /releases/latest/download/<file>.AppImage link now names its asset too, a
malformed %-escape in the field no longer throws while the dialog composes,
and x86-32 and x86_64 assets no longer collapse to the same variant once
version numbers are dropped.
* fix(library): added and Battle.net games sort by when they were played and installed
Recently played and Recently installed put every added game, Battle.net
games included, at the bottom: nothing recorded a launch or an install time
for them, and the Steam play-time lookup was never reached once an entry had
a game. Launching an added game now records its play time, added games carry
their folder's install time (kept in the library cache), Battle.net installs
report theirs, and a zero time falls through to the next source.
* fix(library): an added game's play time is recorded only after it launches and kept when it is rebuilt
* fix(library): an added game's play time is recorded only when its launch is accepted
* fix(library): an added game's play time is recorded when its session actually starts
* fix(library): an added game's play time is recorded when its session service starts
* fix(drivers): leaving a driver bundle's display half also drops its runtime half
Picking either half of an Android + Linux bundle selects both, but picking
another display driver afterwards changed only the display. The session
kept loading the bundle's Linux driver, so switching back to DroidDeck's
drivers could still leave the session failing to start. The runtime driver
now goes back to the default when the display leaves the active bundle; a
runtime driver chosen on its own is kept.
* fix(drivers): picking the Auto display hands both halves to Auto's latest DD-Turnip pair
* fix(drivers): the Auto display pick puts the display on Auto even when no pair fits the GPU
* gamescope: a confine is relative only while it holds the X pointer
Patch 0128 made every hidden-cursor confine relative. Games that hide the
system cursor, draw their own and confine the pointer to their window then
took the host lock, and a direct touch drag became camera motion with no
button held, so cards and items could not be dragged.
A confine now goes relative once the X pointer stays away from the seat
position while input moves, as when a game recentres it or clips it to a
small rectangle, and returns to absolute when the pointer follows again or
the confine ends. Locks and the warp follow rule are unchanged.
* gamescope: a constraint without a hint is relative only while the game holds the X pointer
Xwayland turns a hidden-cursor grab into a lock with no cursor position hint, so the confine-only rule never applied to games that draw their own cursor (Stardew Valley, Balatro). A hinted constraint is relative at once, as upstream. An unhinted lock or confine goes relative once the X pointer stops following the seat's motion, which a recentring or clipping game does and a drawn-cursor game does not.
* gamescope: a pointer constraint is relative only while the game holds the X pointer
Stardew Valley's grab with a hidden cursor becomes an Xwayland lock with a cursor position hint, which upstream made relative at once. Without relative mode that lock still follows gamescope's relative motion and touches still place the pointer, so the hold rule now decides for every constraint, hinted or not.
* gamescope: a hinted lock starts relative and is released once the X pointer follows
Releasing a hinted lock only through the hold rule let a recentring game's first touch swipe press the button. A hinted lock now starts relative as upstream and is released after 30 moving frames on which the X pointer follows the motion gamescope sends, counted from the relative motion while the seat is locked.
* gamescope: count away frames within one half-second window
* gamescope: a hint that arrives with the game's first warp starts the lock relative
* fix(input): end the touch press when a program locks the pointer mid-swipe
A game that only shows its pointer lock after its first warp took the first
swipe as an absolute drag, with the button held until the finger lifted.
The first locked move now ends the forwarded touch before sending deltas.
* gamescope: a new focus window or constraint starts its own pointer hold
A held state from one constrained window carried over to the next one
when focus moved before a reset frame. Track the input focus window and
the constraint, and restart the hold when either changes.
* gamescope: read the pointer constraint under the wlserver lock
GetCursorConstraint asserts the lock is held, so the hold tracker
aborted as soon as a program constrained the pointer.
* fix(input): a mid-swipe pointer lock ends the touch but keeps a held button
The first locked move cancelled the whole direct-touch hold, releasing a
right button held by a long press. It now cancels only the forwarded touch;
the held button is released when the finger lifts, as before.
* fix(input): extra fingers during a locked swipe stay out of the cancelled touch
Once a locked swipe has ended the forwarded touch, a second finger restarted
it with a new touch-down while the swipe carried on as relative motion. Extra
fingers are now ignored until the swipe ends; the final lift still releases a
held button.
* fix(input): a swipe that ended its touch stays closed until the finger lifts
* fix(session): close the bottom panel from the docked menu's Close again
* fix(input): keep imported screen keys on screen when the gamepad is hidden
Screen keys used to be shown on their own, whatever the gamepad did. Since
they were merged into the gamepad layout they disappeared with a controller
connected, in desktop sessions, with on-screen controls set to Never, in
Steam + QAM mode and in Steam's touch mode.
Elements now carry an always-shown flag. The screen key migration sets it on
imported keys (and once on keys merged by earlier builds), and the on-screen
controls keep flagged elements drawn while the rest of the pad is hidden,
in buttons-only mode and with the quick toggle, without duplicating them in
the unfolded strip.
* fix(dual-screen): give the profile controls panel the bottom bar with swap
* fix(dual-screen): keep the layout editor on the bottom screen
The bottom panel was recreated above an open input editor or input
settings window whenever it closed and reopened (session stop/start, or
the bottom screen briefly reported as taken), hiding the editor. The panel
now waits while one of our input windows holds its display and reopens
when the last one closes.
An app arrival recorded before one of our windows covered the bottom
screen no longer marks the screen taken, the editor picks its display only
when a display is added or removed, and it skips a bottom screen another
app holds.
* fix(input): label keyed D-pads and sticks with their keys
Screen key D-pads and sticks used to show each direction's key (W, A, S, D)
when labels were on. The unified renderer drew no direction labels and only
offered Show label for buttons and gamepad sticks, so imported labels did
nothing.
D-pads and sticks bound to directional keys now draw each bound key's name
at its direction through the shared key-label helper, and Show label is
offered for every D-pad and stick.
* fix(input): remember the layout editor's snap choice
* refactor(input): drop controller pref setters and drawer field nothing uses
* fix(input): restore the Deck tabs and right trackpad in the unfolded strip
With Deck controls, the unfolded strip used to offer the Gamepad, Grips,
Left, Right and Both tabs and put a trackpad in the right stick slot. The
unified strip only showed the gamepad with two sticks.
For Deck pads the strip bar now carries the shared Deck panel tabs and
title, the other tabs show the same stock Deck panel layouts as the second
screen, and the docked gamepad takes its stick or trackpad per side from the
Deck gamepad layout. Other pads keep the plain two-stick strip.
* fix(input): carry the Deck gamepad stick or trackpad choice into its layout
The Deck controls Gamepad tab used to let each side switch between a stick
and a trackpad. That choice is now part of the editable Deck gamepad
layout, but the saved choice was never moved over and its setter was left
without callers.
The library upgrade now applies a saved non-default choice to the Deck
gamepad layout once, only while that layout is still the stock one, and the
unused setter is removed.
* chore(i18n): drop the Deck surface switch string nothing shows
* fix(input): offer Show label only where a label can be drawn
* fix(input): Show label follows the unsaved action and diagonal-only sticks
The element properties now get the editor's draft action, so changing a
stick or D-pad to keys offers Show label before saving. A stick bound only
on its diagonals counts as labelled too.
* fix(input): only sticks count their diagonal keys for Show label
Diagonal key names are drawn on sticks only, so a D-pad bound just on its
diagonals no longer offers a label it can't show.
fix(storage): check a raw /mnt/media_rw library against its storage volume
A Second library picked as /mnt/media_rw/<volume>/... was always treated as
unplugged, because Android only reports a volume's state for its /storage
path. Sessions then ran with internal storage only and the library's games
were missing. Map the path to its /storage form before the lookup, reusing
UsbLink.storagePath, and drop the copy in SteamLibraries.
* fix(input): restore on-screen touch behaviour: slide between buttons, mouse pass-through, keyboard on release, overlay haptics, stick reach and hit areas
* fix(dual-screen): restore second-screen bars, swapped Terminal and Deck panel tabs
* fix(input): fit on-screen layouts to the real view size, cutout, Steam + QAM mode and unfolded strip
* fix(input): unfolded strip keeps press haptics
* fix(input): restore layout import/export, element editor rows and layout tools
Bring back .icp layout import and export on the saved layouts page, the
per-element image, opacity and label controls, a uniform size slider and
reset positions in the layout editor, and non-destructive Steam, QAM and
keyboard button toggles stored on the layout style. Remove strings left
unused by the input profile unification.
* fix(input): keep hit slack to gamepad-shaped buttons and fit touch tests to saved layout geometry
* fix(input): button size regenerates the gamepad and scales spacing
A generated gamepad now stores its button size and is regenerated at that
size, so spacing follows the radius and the layout keeps regenerating on
rotation. Edited layouts scale positions from their anchor edge or centre
so gaps grow and shrink with the controls.
* fix(input): button size rescales moved gamepads around the generator
Gamepad layouts that no longer match the generated positions exactly were
scaled element by element from screen thirds, which split the face buttons
and D-pad across anchors. Gamepad controls now take the generator's
positions at the new size plus each control's own offset from the
generated position, so groups keep their shape and spacing.
fix(input): stop L2/R2 keys from overriding analog trigger axes
Pads such as the GameSir X2 Pro send KEYCODE_BUTTON_L2/R2 alongside
their analog trigger axes. Each key-down and every key repeat set the
trigger to full, and the next motion event set it back to the axis
value, so a held trigger jumped to full. Android repeats only the most
recently pressed key, so with both triggers held one side kept being
forced to full while the other tracked its axis.
When a device reports an analog axis for a trigger side (LTRIGGER or
BRAKE, RTRIGGER or GAS, or the wired Xbox Z/RZ layout), its key no
longer drives that trigger. Key repeats never change a trigger value.
Pads with digital-only triggers keep the key behaviour unchanged.
* fix(dual-screen): docked handhelds switch to single-screen mode
With Dual screen on Auto, a dual-screen handheld connected to a wired
external display (dock, HDMI or USB-C monitor) no longer puts the menu,
tabs or session controls on the built-in bottom screen, which nobody can
see while it sits in a dock. The launcher and sessions use the single-screen
layout on the main screen, which the external display mirrors. A bottom
panel opened automatically closes once when the dock is connected and comes
back after undocking.
XR glasses, virtual and overlay displays and switched-off displays do not
count as a dock, and Dual screen set to On keeps the bottom screen.
* fix(dual-screen): only an automatic bottom panel closes on docking, and glasses are told apart from the dock display
* fix(dual-screen): keep the automatic-panel state across activity recreation and reconcile docking before settling the panel
* fix(components): DXVK Auto on Adreno 6xx keeps DroidDeck-Proton on DXVK-gplasync 2.7.1
* fix(components): DXVK Auto says so when the pinned DXVK could not be installed
* fix(components): a DXVK the player picked is shown as their choice, not a failed pinned install
* fix(drivers): a driver repo that fails to answer no longer changes the Auto pick
The release check reads every driver repo and saved the list even when one
of them failed, so that repo's drivers vanished until the next good check.
When GitHub rate-limited or dropped the DroidDeck driver repo, Auto on an
Adreno 6xx saw no DD-Turnip release, switched to Banner's Turnip, and then
switched back after the next clean check.
A repo that fails now keeps its releases from the last good check, while
still being recorded as failed. A repo that answers and no longer lists a
driver still drops it.
* test(drivers): carry-over is checked through refresh with one repo failing
* fix(drivers): drop a repo's partial results when its answer fails and keep the refresh test offline
perf(hud): rasterize Strip graph and logo separately
Replace PR2194's whole-View software layer with a hardware-accelerated
Strip that rasterizes its graph and logo into small reusable bitmaps.
Keep the 66 ms graph invalidation cap, clear stale graph pixels, and
refresh cached logo colours when the theme changes.
Include the comparison method and six sanitized benchmark samples.
With matched Steam navigation, the hybrid averaged 12.2% less UI-thread
CPU and 34.0% lower draw p95 than the PR2194 software path. App-process
CPU averaged 5.9% lower in this run; whole-device and battery savings
are not established. Current-build physical flicker confirmation is
recorded separately from timing validation.
Validation: all 12 DroidDeckHudPanelTest checks passed; comparison APK
built and six alternating device samples completed with matched input
counts, no focus loss, thermal status 0, and no frame metric report drops.
* feat: move UBWC compatibility from Components to Settings
* fix: update driver menu tests for the moved UBWC restart check
* fix: load saved UBWC mode at startup and block Settings restart during runtime operations
Keyboard + trackpad and Deck grips + trackpads are second-screen panels again, rendering the stock layouts without changing the input profile. The last panel is remembered separately for desktop and game sessions; desktop sessions start on the trackpad. A Deck panel without the Deck pad falls back to the default. Stock panels left Steam blue return to the neutral look once.
* fix(fex): show which preset Use Default resolves to and rename FEX defaults to Stock FEX
* test(components): the FEX preset chip names its resolved preset instead of Use Default
* fix(fex): the game settings menu keeps Use Default beside the inherited preset
* fix(fex): inherited preset chip follows the current Default preset
* fix(input): bring back the classic on-screen gamepad look, quick hide toggle and adaptive sticks switch
The unified input surfaces (#1439) drew the built-in gamepad as large
rounded boxes labelled with control names, swapped the compact keyboard
button for a wide pill, dropped the bottom-left show/hide toggle and left
the Adaptive sticks setting with no effect once a layout was migrated.
Gamepad elements are round again with shoulder pills, D-pad arrows, the
old glyph labels and the tint-derived fill and outline. The keyboard is a
small round button beside a restored quick show/hide toggle. Stick touch
areas reach 25% further outward while keeping their inner edge, and the
session menu gets an Adaptive sticks switch that writes the active
layout's sticks. Libraries saved by the first unified build are restyled
once without touching user edits or other layouts.
* fix(input): overlay keyboard opens on the main screen and the D-pad matches the face buttons
A keyboard command now carries the role of the surface that sent it, so the
overlay's keyboard button opens the main screen keyboard as before the
unified surfaces, while lower-screen and controller bindings still use a
second screen. The D-pad buttons take the face buttons' size and spacing,
and an untouched saved D-pad is resized around its own centre. Classic
outlines sit where the old ones did and shoulder labels use their old size.
* fix(input): keep edited D-pads and keyboard buttons, harden the quick and adaptive toggles
Resize a saved D-pad only while its four buttons keep their original geometry, and
replace the old keyboard button only while its name and shape are untouched.
The quick toggle needs its own press before a release toggles it, and a cancelled
touch does nothing. A rejected library write no longer crashes the session or
changes the controller preference, and the Adaptive sticks switch shows whenever
the built-in gamepad is the surface in use.
* fix(input): show the adaptive sticks switch while the unfolded controls are up
* fix(input): run the gamepad restyle as library version 10 and draw the quick toggle in Gray
* fix(input): show the adaptive sticks switch whenever the built-in gamepad is the surface in use
---------
Co-authored-by: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com>
* fix: library scan and download cancel crash on Android 10
The storage scan read StorageVolume.getDirectory() for every volume to
build its key, but that method only exists from Android 11, so on
Android 10 the steam-library-scan thread died with NoSuchMethodError.
The directory is now read once behind the existing Android 11 check and
reused for both the key and the candidate path; older versions fall
back to the uuid, description and /storage scan as before.
Cancelling a download closes the response stream on a background
thread. The platform okhttp can throw IllegalStateException
("Unbalanced enter/exit") from close() while a read is still in
flight, which was uncaught and killed the app. That close now ignores
IllegalStateException as well as IOException; other failures still
surface.
Fixes#2200
* test: wait for the cancel task to finish instead of polling its thread state
The cancel-close tests decided the close task was done from the worker's
thread state, which can read WAITING while the pool is still cleaning up
after a thrown exception. A close failure that escapes ends its worker
thread, and Java runs the uncaught handler before the thread ends, so the
surfacing test now joins that thread. The suppression test hands the
cancel executor more work until it runs on the same thread, which only
happens once the close task has returned without throwing.
* test: watch the close task finish instead of probing which worker runs next
A cached pool can start a second worker, so a probe task may never land
on the thread that ran the close. The suppression test now waits until
the pool has no task running and that thread is back to waiting for
work, a state a worker that is exiting after a thrown exception never
enters.
## Problem and intent
Desktop sessions need configurable mouse, keyboard and application
actions from a controller. On dual-screen devices, a lower-screen
trackpad may already handle the pointer, leaving the physical controls
available for other assignments. This PR gives physical controllers and
local touch controls a shared profile/action model instead of separate
configuration paths.
This follows the centralization direction discussed in
[#1202](https://github.com/Droid-Deck/DroidDeck/pull/1202#issuecomment-6299647180)
and addresses the controller-input portion of #860 and #1356. Those
discussions are context, **not prior maintainer approval** of this
implementation. I would appreciate architectural feedback and guidance
on splitting or coordinating this work if that would make review easier.
## Profiles and configuration
- Controller stays within the existing Setup tabs. Game and Desktop each
have a default-profile selector, followed by one profile list and device
settings.
- Named profiles reference three independently reusable parts: Physical
controller, Main screen and Second screen. Layout geometry and action
bindings are stored separately. Profiles can share physical controls
while assigning different touch actions.
- The session menu selects a complete profile, or expands to mix the
three parts. A mixed selection can be saved as a new profile; Game and
Desktop retain separate selections.
- Both screens use the same full-size editor, with add, move, resize,
snapping, undo and shared properties. The main-screen editor has
movable, collapsible tools and is transparent above a running session.
When editing the secondary display, its entire area is the canvas and
the main display hosts tools/properties.
- The old fixed gamepad, fixed Deck controls and separate Screen keys
editor are replaced with editable presets and the shared renderer.
Existing positions, bindings, shapes, symbols, imported images and
toggle behavior migrate; legacy Screen keys become additional profiles
without replacing the user's selected profile. Migration retains a
backup before removing old preferences.
- Saved layouts have thumbnails and screen targeting. Actions include
compatible native output/remapping, keys/combinations, mouse buttons,
pointer movement, vertical/horizontal scrolling, directional keys,
existing menu/keyboard commands, and disabled. Buttons can use symbols,
shapes and toggle behavior.
## Architecture and latency
```mermaid
flowchart LR
Library[Profiles + reusable layouts and bindings] --> Router[InputRouter]
Physical[Physical controller adapters] --> Router
Touch[Main + secondary InputSurfaceView] --> Sources[Owned pad / Deck sources]
Sources --> Router
BLE[Steam Controller BLE adapter] --> Router
Router --> Native[Existing controller publication]
Native --> Steam[Steam Input / guest controller]
Router --> Held[Shared key and mouse ownership]
Held --> Guest[Existing guest input outputs]
Router --> UI[Existing menu / keyboard hosts]
```
Bindings resolve when the profile changes, outside event dispatch.
Native forwarding has no additional frame/timer queue; identity routes
reuse existing state/reports. Only synthesized continuous stick
pointer/scroll motion uses a frame callback. One source releasing a
key/button does not release another source's hold. Menu, profile,
suspension and removal transitions release old outputs and gate held
controls until neutral.
Steam continues to own game-specific mappings and action sets. Steam
Mobile Touch and VR retain their specialized paths. Raw device adapters
remain responsible for transport/calibration and must join routing
before publishing consumed controls; this does not claim that every
adapter has been unified.
Historical Thor measurements on the native Game path found
kernel-event-to-guest-observer upper-bound medians of **3.061 ms
baseline versus 2.999–3.403 ms candidate**, with a subsequent
installed-APK check at **3.039 ms / 300 matched edges**. These are
relative software-path measurements, not button-to-photon latency. Some
tails increased, simultaneous BLE contention remains unmeasured, and
**this UI revision has not had a new latency measurement**. The previous
cursor fix changed visual update intervals from roughly 33.3 ms to 16.7
ms without changing sensitivity or adding smoothing.
[Full design, migration and
boundaries](https://github.com/Delete228/DroidDeck/blob/codex/unified-controller-input/docs/development/unified-input-proposal.md).
## Validation
Updated through upstream 542bbf89c; current PR head 08526f828. The
latest CodeRabbit review generated no actionable comments.
- **1,017 tests passed in 133 suites after the latest upstream merge**,
with no failures, errors or skips: all input/UI tests, global reset,
component updates, PiP policy, runtime auto-update and Proton menu
auto-selection. The pre-merge review-fix run passed **967
input/UI/global-reset tests**. The subsequent second-screen refresh fix
passed **35 focused surface/presentation tests**, including three new
state-preservation regressions.
- Full local run before the final review follow-ups: **3,598 tests,
3,573 passed, 25 failed**. The 25 are unchanged upstream macOS
limitations: log-export tests requiring SecureDirectoryStream (24) and a
case-sensitive WoW add-on test (1). The global-reset regression also
passes. Earlier disk-exhausted attempts were discarded; the completed
run bounded temporary resources with one worker and JVM recycling,
without excluding tests or changing repository test configuration.
- APK build, runtime/session assets, native dependencies, alignment and
v1/v2/v3 signatures passed. Installed over the separate Input Test app
on **AYN Thor** with its environment retained. Local packaging omits
Rust store engines; the [full Build APK workflow for 08526f828
passed](https://github.com/Droid-Deck/DroidDeck/actions/runs/42295403480),
including Android/JVM, Python/native, Rust store-engine and Xbox checks.
- Review follow-ups keep migration IDs stable, preserve unreadable
supported-version libraries before recovery, retain forward-version
protection, release keyboard owners on focus loss, handle unavailable
presentation displays, and allow controller confirmation/cancellation
after key capture. Deck router cleanup checks ownership; BLE teardown
synchronously blocks late routing/publication, and shared Triton slot
ownership protects a replacement controller from stale instances. Legacy
Select-to-Steam migrates to Desktop without replacing saved bindings;
mouse backend transitions are serialized with publication. Profile
refresh now updates custom controls in place, preserving
terminal/dashboard state, the field keyboard and WoW chat. No physical
BLE-controller test or new latency measurement was performed.
- The latest 08526f828 APK is installed on Thor. Profile preferences
stayed byte-for-byte identical and the retained Steam environment
reached READY at 1280×720 / 60 Hz. The earlier capture-dialog fix was
also exercised on Thor. APK SHA-256:
`ed6ef888648468b4ebeb94e8f1ae74a195bb1f0a3f27e4fb3646d5462a4beb6b`.
- Upstream integration preserves the new WoW chat panel,
platform-pointer and PiP fixes. The fast-double-tap stick-click rule and
its six regressions were adapted to the shared renderer; WoW field
selection also preserves Ctrl held by a profile.
- Latest Thor UI pass: existing Setup navigation, profile/layout
previews, action persistence, centered element creation, main-screen
editing over Steam, secondary canvas with primary tools/properties, and
X returning from the docked menu to controls.
- Across the documented device sessions, the user tested native physical
buttons/sticks/triggers, Deck pads/grips, desktop
pointer/clicks/scrolling/arrows, Android keyboard with physical input,
Ctrl+F and Ctrl+A/X/V/S, simultaneous physical/touch holds, menu release
and Home/resume. These historical checks are distinguished from the
latest UI-only pass.
[Revision-specific results, APK hashes and
limitations](https://github.com/Delete228/DroidDeck/blob/codex/unified-controller-input/docs/development/unified-input-validation.md).
## Coordination and remaining work
- #1202 (jmarti326): shortcut capture/device identity and the
shared-assignment discussion informed this work.
- #1305 (kingokksa): shared PC-key definitions were adapted from that
PR; this does not absorb its entire feature set.
- #1414, #581, #1226 and #1150 remain relevant for external-device
input, calibration, keyboard routing and DualSense integration. The
DualSense coordination discussion in this PR describes adapter
boundaries and duplicate-stream suppression.
Import/export, automatic per-app selection, macros, multi-button
activation chords and automatic text-field keyboard detection remain out
of scope. A single binding emitting a key combination is implemented.
The configuration UI is now implemented and exercised on Thor. I am
happy to continue UX/accessibility/localization refinements here before
merge, or in follow-up PRs, according to maintainer feedback. The main
review questions remain the shared model, source ownership/lifecycle
contract, migration policy and integration with other input work.
## UI walkthrough on AYN Thor
Actual captures from the installed Input Test build. In paired images,
the main display is above the second display. The test application name
and local build badge are not production branding changes.
<details>
<summary>Controller settings: separate defaults for Desktop and Game,
followed by the profile library.</summary>
<img width="960" alt="Controller settings: separate defaults for Desktop
and Game, followed by the profile library."
src="https://github.com/user-attachments/assets/a4c156d6-9ded-43df-b0f8-e5f34028806d"
/>
</details>
<details>
<summary>Physical controller: bindings grouped by control type, with
each current action visible.</summary>
<img width="960" alt="Physical controller: bindings grouped by control
type, with each current action visible."
src="https://github.com/user-attachments/assets/a8a840e3-19ac-473f-9f55-b06ae9c637b5"
/>
</details>
<details>
<summary>Reusable layouts: bounded previews and separate screen
targets.</summary>
<img width="960" alt="Reusable layouts: bounded previews and separate
screen targets."
src="https://github.com/user-attachments/assets/cf0cf7d1-42f3-46cd-b50b-f01e1fa8ed41"
/>
</details>
<details>
<summary>Main-screen editor over the live Steam UI, with the movable
tools expanded. The background is Steam artwork, not a running
game.</summary>
<img width="960" alt="Main-screen editor over the live Steam UI, with
the movable tools expanded. The background is Steam artwork, not a
running game."
src="https://github.com/user-attachments/assets/0781f7c8-fd10-47e5-8a23-817f945a6289"
/>
</details>
<details>
<summary>Dual-screen editor: tools and selection on the main display,
full-size placement canvas on the second display.</summary>
<img width="720" alt="Dual-screen editor: tools and selection on the
main display, full-size placement canvas on the second display."
src="https://github.com/user-attachments/assets/f13d4ef6-be6d-4d95-9d42-e047e0a63b8a"
/>
</details>
<details>
<summary>The selected lower-screen trackpad stays visible while its
properties open on the main display.</summary>
<img width="720" alt="The selected lower-screen trackpad stays visible
while its properties open on the main display."
src="https://github.com/user-attachments/assets/26baf597-f037-419e-9601-e48f086c0d4f"
/>
</details>
<details>
<summary>Session menu: expand Profile to select the physical,
main-screen and second-screen parts independently.</summary>
<img width="720" alt="Session menu: expand Profile to select the
physical, main-screen and second-screen parts independently."
src="https://github.com/user-attachments/assets/27ac4d9c-68b3-4257-8353-23104e2343de"
/>
</details>
* fix(hud): skip relayout when size is unchanged and cap graph redraws
* fix(hud): limit the redraw and relayout changes to the strip
* fix(hud): draw the strip background from a cached bitmap like the panels
* fix(hud): drop the strip-only redraw cap and relayout skip
* fix(hud): the strip graph redraws at most 15 times a second
* fix(hud): isolate strip rendering in a software layer
---------
Co-authored-by: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com>
Track explicit whole-profile choices, including Auto, separately from inherited profiles so physical and main-screen overrides do not suppress legacy panel migration. Persist this distinction in library version 8 and read older selection records compatibly.
Keep saved Deck panels during unavailable-controller fallbacks and retain legacy preferences when migration fails. Add regressions for all three review findings, explicit Auto selection and older saved libraries.
Validation: all 992 input/UI tests passed.
Only migrate stock controls for explicitly saved legacy panel modes. Automatic fallbacks retain the active profile layout and bindings without persisting an override.
Add session regressions for automatic fallbacks, unavailable panels, explicit legacy migration and existing quick selections. Validation: all 986 input/UI tests passed.
* feat(components): Auto updates for FEX, DXVK and VKD3D-Proton
Each component gets an Auto or Manual mode on the Default Components page.
Auto installs the newest DroidDeck-Components nightly into every Proton that
ships the component, matching each Proton's ARM64EC or x64 flavour, keeps the
current and replaced builds, and removes older builds it downloaded.
A game that ran on the previous build and exits within 30 seconds twice on the
new one rolls the line back; the failed build is skipped until a newer one is
published. Existing manual choices keep that component on Manual, and DXVK
stays on Manual while a GPU rule pins its version.
* fix(components): Auto notes per line, Original label and first-update rollback
Boot history on the Original is recorded before the first update so that
update can roll back, rollback notes stay with their own component, the
Original shows by name, and new Protons get the current build even when no
newer one is published.
* fix(components): Auto owns only its own downloads and follows in-place Proton updates
Auto deletes only builds it downloaded, re-swaps a Proton whose version
changed under an existing choice, reports a missing nightly listing as a
failed check, writes its state atomically so the page can read it without
waiting on a download, and words the rollback and download messages to
match what it does.
* fix(components): Auto keeps a rollback and reads updated Protons' own files
After a rollback only a build newer than every rejected one is taken, so an
older build still listed in the nightly cannot replace the restored one.
Flavour detection treats a choice saved for an older Proton version as not
applied, so a Proton Steam updated in place is read from its own files and
re-swapped. A failed state rename falls back to a direct write and is logged.
* fix(components): Keep the last good Auto state when a save fails
* fix(components): Serialise Auto state reads and writes
* Install the Linux runtime by itself on first launch
The runtime auto-update skipped devices with no runtime, so FEX, Proton,
DXVK and VKD3D waited for the first Steam launch to install it. Claim a
first install when the device never had a runtime, on a usable network,
outside battery saver and with a supported GPU, and check Proton as
soon as the runtime is ready. A runtime the user removed stays removed.
* fix(components): Check components as soon as Proton is installed
The Auto check stamped its hourly timer even when no Proton was
installed yet, so a fresh device waited an hour after Proton arrived.
Skip the stamp until a Proton exists and run the check right after a
Proton install finishes.
* fix(components): Apply stored builds on every check and retry unfinished rollbacks
A held or failed download no longer skips applying the stored build or
stamps the hourly timer. Stored builds are applied on every check, so a
new Proton is reconciled inside the hour, and a check requested while
one runs is queued. A rollback that could not restore or swap a Proton
is retried and reported only once it finishes. Removing the runtime
marks it as seen so it is not installed again, and the failed-save test
skips where the process can still write read-only files.
* fix(components): Keep rollback restores pending until applied and save state on failure
* fix(components): Hold newer builds until a pending rollback is applied
* fix(components): Drop pending restores for removed Protons
* fix(components): Never auto-install a non-ARM64X build over a Proton's own ARM64X DXVK or VKD3D
* fix(components): Read probe archives outside the lock and block auto updates while the original build is unknown
* fix(components): Require ARM64X when any Proton on the line needs it, and before each swap
* fix(input): Click a touch stick only on a fast double tap
* fix(input): Count the release position in stick tap travel
* fix(input): Treat cancelled pointer-ups as cancelled stick touches
* Add voice and keyboard chat input for World of Warcraft
Open a chat bar with an editable text field when a WoW client focuses
a text box (reported by Proton's winex11 IME context hook through
agent/wine-ime.json). First use asks for Voice type or Android keyboard
with a remember toggle; the choice is also in the side menu's Keyboard
group while a WoW client runs. Voice type uses the device's speech
recognition service. On dual-screen devices the bar and keyboard open
on the second screen. Send types the text and presses Enter; Cancel
presses Escape.
* Cover the Arm64 WoW client in the chat match test
* Give the second-screen chat bar the session's result and back owners
* Open the WoW chat bar with the game field's current text
When the bar opens, select and copy the game's text field and fill the
bar with it through the session clipboard bridge, then put the guest
clipboard back. Send selects the field first, so the bar's text
replaces what was there instead of being appended.
* Keep WoW chat consistent across screen changes and failed field reads
Close the bar when the game field closes even after the program name
is cleared, move the bar to the top screen when the second screen drops
it, and drop any pending second-screen keyboard request when the bar
opens. A field read that gets no copy of its own leaves the game text
unselected and Send then types without replacing it. Ctrl is only
pressed and released when nothing else holds it, and a pending read
ends when the clipboard bridge stops.
* fix(session): Close WoW chat for other programs and keep the game cursor on cancelled reads
Keep migration binding IDs stable, preserve unreadable libraries before recovery, and retain forward-version protection. Release keyboard owners on focus loss, handle unavailable settings displays, and allow controller confirmation or cancellation after physical key capture. Stop BLE routing and publication synchronously before asynchronous disconnect. Add regression coverage and record local and Thor validation.