Compare commits

...
Author SHA1 Message Date
MaxsTechReview 914b3588e1 fix(drivers): leaving a driver bundle no longer keeps its runtime half, and the Auto display picks the latest pair (#2255)
* 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
2026-11-07 21:47:04 -05:00
MaxsTechReview 2ade269be3 fix(gamescope): direct-touch drags work again in games that confine a hidden cursor (#2220)
* 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
2026-11-07 21:30:21 -05:00
MaxsTechReview 0d034b1996 fix(input): restore the rest of what #1439 removed, and keep the editor on the bottom screen (#2242)
* 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.
2026-11-07 21:04:34 -05:00
Kurt Himebauch ef46c011db fix: keep the bottom-screen menu and layout editor open when the screen turns off (#2247) 2026-11-07 20:11:20 -05:00
MaxsTechReview 5b2f7a9a6d fix(storage): a Second library on a raw /mnt/media_rw path is used again (#2245)
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.
2026-11-07 19:32:13 -05:00
Kurt Himebauch 10cfc6c6b2 feat: let users turn off the low-storage warning (#2244) 2026-11-07 19:09:51 -05:00
MaxsTechReview 4948f73912 test: skip the unscrubbable session log test when file permissions are not enforced (#2241) 2026-11-07 18:37:44 -05:00
Kurt Himebauch d29a1a2dc2 ci: build and test on root self-hosted runners (#2237) 2026-11-07 17:02:19 -05:00
MaxsTechReview 8fa30b99f9 fix(input): restore the controls, panels and settings #1439 removed (#2238)
* 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.
2026-11-07 16:36:23 -05:00
MaxsTechReview a33954b8bd perf: answer Steam's missing-file checks on SD and USB libraries from the directory cache (#2212)
* perf: answer Steam's missing-file checks on FUSE libraries from the directory cache

Before creating and committing each file, the Steam client checks whether it
already exists under its exact, lower-cased and folded spellings. On an SD or
USB library reached through Android's FUSE layer every one of those misses is a
round trip to MediaProvider (Brawlhalla: about 65,000 misses, 92 of the 161 s
of preallocation and 60 of the 93 s of commit).

The client already caches the listings of its steamapps directories on FUSE and
keeps them exact through its own changes, inotify and a short TTL. access() now
answers a name that is absent from a live listing of its directory without
asking the filesystem. Directory paths match case-insensitively only when the
library's filesystem folds case, and names compare case-insensitively, so an
existing file is never reported missing.

* fix: reuse a listing for a differently cased folder only when the whole path folds case

Checking that steamapps alone folds case let a listing of one folder answer
for a different folder below it whose name differs only in case. The fill
now compares the listed folder with its fully case-swapped path below
steamapps, so a listing is reused across case only when the filesystem
folds every level.

* fix(dircache): match a folded listing only below the same prefix and skip it during a tracked change

* fix(linuxfs): a folder renamed or removed above a cached listing drops that listing

* fix(linuxfs): watch the folders above a cached listing so a rename or removal by any process drops it

* fix(linuxfs): also watch the folders above steamapps, and stop a stuck probe from hanging the tests

* fix(linuxfs): a listing under a symlink or an unwatchable folder no longer answers absent lookups

* fix(linuxfs): a symlink check that cannot be watched also stops absent answers

* fix(linuxfs): a differently cased lookup reuses a listing only for the same folder
2026-11-07 16:36:15 -05:00
Kurt Himebauch 3526309f88 feat(session): setting to choose the Android keyboard for game text fields (#2236) 2026-11-07 16:19:40 -05:00
MaxsTechReview be0fe35271 fix(proot): let the fast path fall back when Android 10 refuses statx (#2224) 2026-11-07 15:48:44 -05:00
MaxsTechReview 7f0427b830 fix(input): L2/R2 keys stop overriding analog trigger axes (#2205)
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.
2026-11-07 15:48:41 -05:00
MaxsTechReview 2476c824f6 fix(dual-screen): docked handhelds switch to single-screen mode (#2232)
* 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
2026-11-07 15:48:17 -05:00
MaxsTechReview d74575ed7f fix(components): DXVK Auto on Adreno 6xx keeps DroidDeck-Proton on DXVK-gplasync 2.7.1 (#2226)
* 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
2026-11-07 15:48:14 -05:00
MaxsTechReview e4d4096342 fix(drivers): a driver repo that fails to answer no longer changes the Auto pick (#2233)
* 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
2026-11-07 15:48:09 -05:00
KeegantheVeganat0r 11218f0b24 perf(hud): reduce Strip rendering cost with bitmap graph and logo (#2235)
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.
2026-11-07 15:41:46 -05:00
Kurt Himebauch d9bf6d8a17 feat: move UBWC compatibility from Components to Settings (#2008)
* 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
2026-11-07 15:39:55 -05:00
MaxsTechReview 041c3b1b55 ci: run compile jobs on the org workstation runner when it is idle (#2234)
* ci: run compile jobs on the org workstation runner when it is idle

* ci: pick the compile runner right before each heavy job

* ci: cover the runner picks in the job dependency test
2026-11-07 15:00:39 -05:00
Kurt Himebauch e5434117f2 fix(input): controller navigation works across the Controller tab and profile menus (#2231)
* fix(input): controller navigation works across the Controller tab and profile menus

* fix(input): dialog choice rows expose their checked state
2026-11-07 14:37:00 -05:00
MaxsTechReview b474cba79e fix(dual-screen): bring back the trackpad and Deck panels on the second screen (#2229)
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.
2026-11-07 14:36:30 -05:00
Kurt Himebauch ffb76240fd fix(fex): show which preset Use Default resolves to (#2225)
* 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
2026-11-07 14:22:00 -05:00
MaxsTechReview 875f8e3ce2 feat(wow): download addons from WoWInterface and open CurseForge in the browser (#2219) 2026-11-07 13:09:21 -05:00
MaxsTechReview 5446497301 fix: uninstalling a Flatpak app that Flatpak no longer has removes its folder (#2216) 2026-11-07 12:46:52 -05:00
MaxsTechReviewandKurt Himebauch 98a670fb81 fix(input): classic on-screen gamepad look, quick hide toggle and adaptive sticks switch (#2215)
* 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 <[email protected]>
2026-11-07 12:42:45 -05:00
MaxsTechReview a123af7c13 fix(script-run): run x86 programs a native script starts through FEX (#2217) 2026-11-07 12:27:24 -05:00
MaxsTechReview 6026d72435 fix(input): restore the neutral look of the bottom-screen controls (#2221) 2026-11-07 12:06:03 -05:00
MaxsTechReview d5f3d1a0d5 fix(proton-run): follow a removed DroidDeck-Proton build to the newest one (#2223)
Battle.net, store games and desktop programs start through droiddeck-proton-run
without the Steam client. When the Proton auto-update replaced the DroidDeck-Proton
build that Steam's default or a title's mapping named, the name no longer matched
an installed tool and the run fell back to Valve's Proton Experimental ARM64, which
lacks DroidDeck-Proton's patches. World of Warcraft then hung at launch.

A removed DroidDeck-Proton build now resolves to the newest installed one, the same
successor steam-compatibility moves Steam's mappings to.
2026-11-07 11:37:17 -05:00
MaxsTechReview 4cb3a89483 fix: library scan and download cancel crash on Android 10 (#2204)
* 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.
2026-11-07 11:16:08 -05:00
Kurt Himebauch 22a675a664 Unify controller and screen input profiles for game and desktop sessions (#1439)
## 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>
2026-11-07 08:32:50 -05:00
MaxsTechReview ca6c409436 fix(drivers): built-in Auto picks match the published table on Adreno 6xx and 8xx (#2151) 2026-11-07 08:25:08 -05:00
Jordan Y 7c429f3a26 fix: droiddeckctl pacing reads timestats dumps with Windows line ends (#2186) 2026-11-07 08:23:06 -05:00
Kurt Himebauch aba55031c3 perf: answer chown and bound thermal paths through the proot fast path (#2203) 2026-11-07 01:08:06 -05:00
MaxsTechReview 18c7d56409 fix: Auto components update only DroidDeck-Proton and delete old builds (#2193) 2026-11-06 20:43:44 -05:00
MaxsTechReview 0b4bc827e7 fix(battlenet): stop Battle.net's pages playing videos on the CPU (#2195) 2026-11-06 20:39:37 -05:00
KeegantheVeganat0randKurt Himebauch a552f1e293 fix(hud): isolate strip rendering in a software layer (#2194)
* 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 <[email protected]>
2026-11-06 20:23:10 -05:00
MaxsTechReview 066834d17d feat: compress idle game memory before low-RAM devices run out (#2187) 2026-11-06 19:49:06 -05:00
Delete228 aff9738748 Preserve second-screen choices across partial selections and migration retries
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.
2026-11-07 02:44:47 +03:00
Kurt Himebauch f33d5b9e09 fix(components): auto updates follow DroidDeck-Proton only (#2190) 2026-11-06 18:41:37 -05:00
Delete228 281c2aea1b Preserve custom profiles when opening second-screen fallbacks
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.
2026-11-07 02:29:31 +03:00
Delete228 c2d7765619 Fix desktop profile selection and simplify default touch layouts 2026-11-07 02:10:26 +03:00
MaxsTechReview 250f80fbb5 fix: stop switching between display modes with the same refresh rate (S26 lag and heat since 0.6.7) (#2192)
fix: stop switching between display modes with the same refresh rate
2026-11-06 17:18:24 -05:00
Delete228 08526f8283 Preserve second-screen state when refreshing input profiles 2026-11-07 00:16:45 +03:00
Delete228 f44826ae3a Merge main and retain unified input behavior 2026-11-07 00:01:20 +03:00
Delete228 31b8adbb55 Guard shared input slots and mouse backend transitions 2026-11-06 23:56:30 +03:00
MaxsTechReview 542bbf89c3 fix: launch Battle.net and store games from any game page tab (#2185)
* fix(library): Keep one Play request per game and show Starting while it prepares

* Track launch preparation per game

* Keep each game preparing until its own worker finishes

* Drop store lookups that finish after the activity ends
2026-11-06 15:31:09 -05:00
Kurt Himebauch e1987ca785 chore: pin gamescope to the DroidDeck-Components p22 release (#2183) 2026-11-06 14:35:10 -05:00
MaxsTechReview a81da1e476 feat(components): Auto updates for FEX, DXVK and VKD3D-Proton (#2156)
* 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
2026-11-06 13:56:34 -05:00
MaxsTechReview 116728917f fix(input): Keep the Android pointer hidden over the game after hovering session menus (#2176)
* fix(input): Keep the Android pointer hidden over the game after hovering session menus

* fix(input): Defer the overlay pointer icon from a container, since ComposeView is final
2026-11-06 13:56:30 -05:00
MaxsTechReview 5f180d909b fix(input): Click a touch stick only on a fast double tap (#2173)
* 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
2026-11-06 13:56:25 -05:00
Kurt Himebauch 88acdbc3ab ci: publish gamescope builds to DroidDeck-Components and never replace a tag (#2181) 2026-11-06 13:36:56 -05:00
Kurt Himebauch 42b2810822 ci: retry PR test build cleanup and sweep leftovers every six hours (#2180) 2026-11-06 13:36:52 -05:00
Kurt Himebauch 1001bc00af fix: PiP scales the full-size output instead of shrinking it (#2174)
* fix: PiP scales the full-size output instead of shrinking it

* fix: hide the performance HUD as PiP starts so it does not flash at full size
2026-11-06 13:24:37 -05:00
MaxsTechReview 69c6c14f3f fix(wow): Clear the game field selection after reading and sending chat (#2175)
* fix(wow): Clear the game field selection after reading and sending chat

* fix(wow): Skip the field cleanup when the chat request has closed
2026-11-06 13:17:13 -05:00
MaxsTechReview 6060f2288f feat(wow): voice type and keyboard chat bar for World of Warcraft (#2171)
* 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
2026-11-06 12:17:37 -05:00
Delete228 915f373d82 Harden input migration and session lifecycle
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.
2026-11-06 19:53:40 +03:00
Delete228 7b7722bd25 Preserve replacement session Deck input routing 2026-11-06 19:25:41 +03:00
Delete228 63004567fd Unify configurable control surfaces and profile management 2026-11-06 19:11:58 +03:00
Delete228 aa3757eff1 Merge remote-tracking branch 'origin/main' into codex/unified-controller-input 2026-11-06 18:50:33 +03:00
William Allenandmaxjivi05 a1bc8467fd feat: show Steam launch options on the Launch tab (#2163)
* Show Steam launch options on the Launch tab

* fix: Launch options is one row on the Launch tab, like the shortcut fields

* fix(launch): reread Steam's launch options when the game page resumes

* fix(launch): keep launch options disabled while they reload on resume

* fix(launch): a saved launch option is not replaced by a read that started before it

* fix(launch): an unreadable Steam config keeps the shown launch options instead of clearing them

* fix(launch): an unparsable Steam config and a pending reload keep the shown launch options

* fix(launch): the resume reload follows the game the panel shows

---------

Co-authored-by: maxjivi05 <[email protected]>
2026-11-06 10:43:44 -05:00
Kurt Himebauch d6566f0dbe feat: GOG achievements on by default, drop stores engine row (#2168) 2026-11-06 10:22:42 -05:00
Delete228 ba5876bf6d Merge latest upstream main before Controller UI integration
# Conflicts:
#	app/src/main/java/com/droiddeck/launcher/SessionActivity.kt
#	app/src/main/java/com/droiddeck/launcher/input/OnScreenControls.kt
#	app/src/main/java/com/droiddeck/launcher/input/PadBridge.java
#	app/src/main/java/com/droiddeck/launcher/ui/DeckControlsPanel.kt
#	app/src/test/java/com/droiddeck/launcher/input/PadBridgeQamTest.kt
2026-11-06 14:37:50 +03:00
Kurt Himebauch bf693d7f71 fix: Auto picks the upstream RADV pair on Xclipse 950 and 960 (#2158)
* fix: Auto picks the upstream RADV pair on Xclipse 950 and 960

* fix: Auto reads the Xclipse model after the Vulkan query

* fix: Auto treats an unnamed Xclipse like the 950 and 960

* fix: Auto reads any three digit Xclipse model number
2026-11-05 23:06:06 -05:00
Kurt Himebauch 4cbd7722f8 feat: the PowerVR runtime is a downloaded add-on pack, not bundled in the APK (#2157)
* feat: the PowerVR runtime is a downloaded add-on pack, not bundled in the APK

* fix: a stopped session cancels the PowerVR pack download

* fix: a session stopped during runtime unpacking ends before staging

* fix: a failed PowerVR pack download ends the session with a message
2026-11-05 22:58:10 -05:00
+1 3733c50902 feat: non-Adreno graphics stack (Venus, open driver pairs, gamescope patches) (#2029)
* Retain compositor resources when GPU completion is unconfirmed

* Stop quarantined GPU callers before retries and mapped writes

* Support Android hardware-buffer presentation over Wayland

* Support PowerVR swapchains and linear image imports in Gamescope

* Bundle verified PowerVR graphics components through existing build preparation

* Detect and select the system Vulkan backend on PowerVR

* Apply PowerVR Steam and game compatibility during normal launches

* Show PowerVR support through existing driver and component controls

* Update existing hardware guidance for system Vulkan support

* Import the shared system Vulkan readiness check

* Discover and propagate the actual PowerVR render node

* Prune obsolete PowerVR runtimes after guest processes exit

* Retain external DMA-BUF ownership after GPU quarantine

* Name the PowerVR HAL selection explicitly

* Isolate component UI tests from application startup

* Cover the inherited FEX preset in PowerVR profile tests

* Log test worker failures for CI diagnosis

* Remove temporary test logging after diagnostic CI

* Prefer system dependencies for the Android Vulkan loader

* Capture raw stderr before test worker termination

* Remove the temporary test worker crash probe

* Import pinned DXVK recipe prerequisite from PowerVR PR570

Copy only the standalone build recipe, source pin, BC-format prerequisite
and presentation-sync patch/check from wallentx/DroidDeck at d0f7264ea8.
Source: https://github.com/Droid-Deck/DroidDeck/pull/570
Preserve that proposal attribution; no Android app, compositor, managed
pack, private data or fork development history is imported.

* Add opt-in low32 GPU mapping adapter and integrity probes

* Add opt-in D3D9 single-sample workaround and fix HUD texel format

* Document verified production game workaround and publication scope

* Restrict reset diagnostics and create private output atomically

* Target official runtime review and document PR570 prerequisite provenance

* Fix runtime tool output isolation, mmap64 chaining and DST ambiguity

* Normalize swapchain backbuffers for the opt-in D3D9 single-sample path

* Experimental Samsung Xclipse support on the system Vulkan path

Detect Samsung's Vulkan HAL (vulkan.samsung.so) and its sgpu render node
alongside PowerVR, and run Xclipse through the same libhybris bridge:

- VK_LAYER_DROIDDECK_xclipse (tools/linuxfs/xclipse-layer), built with the
  glibc preloads and enabled only on Xclipse: sets bionic's environ for the
  driver's shader compiler, avoids immutable YCbCr samplers in compute
  layouts, forwards the KHR calibrated-timestamp entry points to the EXT
  ones, and reports textureCompressionBC and BC4/BC5 filtering over the
  compat layer's BC emulation.
- Upstream DXVK 2.6.2, pinned as a tar.gz component and selected on Xclipse:
  the driver has 128 bytes of push constants, DXVK 3 needs 256.
- android_wlegl accepts a zero stride and passes it through unchanged:
  Samsung's gralloc records none for GPU-only buffers.

Tested on a Galaxy A56 (Xclipse 540, driver 24.0.560): Steam Big Picture
and PICO PARK (D3D9).

* Xclipse layer: copy the device chain and track samplers per device

- Clear textureCompressionBC in a copy of the device-create chain (every
  structure up to the VkPhysicalDeviceFeatures2 that enables it) instead of
  writing the caller's structure; a chain led by a structure of unknown size
  is passed on unchanged.
- Track YCbCr samplers per device in a growable list, drop them on
  vkDestroySampler and device teardown, and fail sampler creation rather
  than leave one untracked when the list cannot grow.
- Fail descriptor set layout creation with VK_ERROR_OUT_OF_HOST_MEMORY when
  a replacement sampler array cannot be allocated, instead of passing the
  original YCbCr samplers to the driver.

* Xclipse layer: copy any device-chain structure the headers define

The chain copy that clears the emulated textureCompressionBC only knew a
handful of structure sizes, so a valid chain such as
VkPhysicalDeviceTimelineSemaphoreFeatures -> VkPhysicalDeviceFeatures2 was
passed on unchanged and the compat layer refused the bit. Size every
structure that may extend VkDeviceCreateInfo from a table generated from the
Vulkan registry (gen_device_chain.py, Vulkan-Headers 1.3.275, the headers
the build compiles with), and copy onto the heap instead of a fixed buffer.
Only a structure newer than those headers still passes the chain unchanged.

Add a regression test that runs the layer between a simulated loader and a
mock driver: the timeline-semaphore chain, pEnabledFeatures, an unknown
structure, the caller's structures left untouched, per-device YCbCr sampler
tracking with handle reuse, and the KHR calibrated-timestamp forwarding. CI
installs libvulkan-dev for it.

* Mali bridge (#517, by ASEbrahim) on the non-Adreno test branch

Squash of Droid-Deck/DroidDeck#517 (ASEbrahim/DroidDeck mali-bridge @ 6b823341): the Vulkan
bridge (tools/vkbridge) that runs the guest's Vulkan on the phone's own driver, with BC1-BC7,
clip/cull distance, vertex-divisor and depth-clip emulation and dma-heap memory sharing for Mali.
All of the bridge is ASEbrahim's work; squashed only to carry it on this test branch.

Combined with #570 on this branch, a device draws through exactly one path:
- Adreno: Turnip, unchanged (checked first; the bridge setting is ignored there).
- PowerVR / Xclipse with the system HAL and a render node: the system-Vulkan path (#570).
- Anything else with the Setup toggle on (Mali): the bridge.

* Test branch: keep #570's DXVK build recipe so its pinned PowerVR components verify

#602 adds output checks to tools/dxvk/build.sh; tools/powervr/fetch.py pins the exact
recipe the published PowerVR DXVK was built from, so the branch carries #570's copy.

* Test branch: make #570, #602 and #560/#561 pass each other's tests

- tools/dxvk/build.sh: #602's copy (its output checks) and the PowerVR pin in
  tools/powervr/release.json moved to that file's hash; the recipe's outputs are unchanged.
- droiddeck-session: the presentation block sets _bl_cef for #570's PowerVR path, and the UI
  block keeps main's order, defaulting to -cef-use-angle=vulkan when the block has not run.
- Compositor test harnesses: AHardwareBuffer stubs for #570's AHB import, and a stub for
  vkp_effects_forget_image (main #597) that #561's caller harness predates.

* Auto drivers for Mali and Xclipse on the non-Adreno test branch

Auto now picks an open driver pair for the GPU, the way it does for Adreno:
- Mali (no KGSL, /dev/mali0): the GPU's name ("Mali-G720-Immortalis MC12", from the vendor
  Vulkan driver or kbase's gpuinfo) picks the PanVK build: G52-G78 (Job Manager), G610/G710,
  G615 or G720. A Mali none of them covers (G925...), or one whose pair is not published yet,
  draws through the Mali bridge, switched on by Auto.
- Xclipse keeps the system-driver path as Auto; the RADV pair is offered as a manual choice.
- The pairs come from The412Banner/droiddeck-nonadreno-drivers (one pre-release per driver,
  each a DriverBundle zip). Only a device without an Adreno reads that repo.
- The importers accept PanVK and RADV libraries (libvulkan_panfrost / libvulkan_radeon).
- An open driver pair and the Mali bridge are alternatives: a selected pair turns the bridge off.
- TU_DEBUG is only set for Turnip, on an Adreno.

Every new branch is reached only when the device has no Adreno; an Adreno's detection, release
sources, Auto choice and session environment are unchanged.

* Auto: the one-binary PanVK kbase build for Mali v9-v13 on Android 15+

The412Banner/droiddeck-nonadreno-drivers adds panvk-mali-kbase: one PanVK build for
G57/G68/G77/G78, G610/G710, G615/G715, G720/G725 and G925, whose display half needs Android 15.
On Android 15+ Auto prefers it for every Mali it covers; below 15, and whenever its release is
not listed, Auto falls back to the GPU generation's own pair. G52/G76 keep the Job Manager
build; a G715/G725/G925 below Android 15, or any Mali no pair covers, draws through the bridge.
Every pair stays selectable by hand.

* Test: the G925 is in the kbase build's family now

* Offer faux123's device PanVK builds (RP4 Pro, Ace, RG556) as manual pairs for their Mali

* Detailed graphics logs for non-Adreno test reports

- graphics-report.txt in every session folder: the graphics path the session draws with and why
  (Turnip, system Vulkan #570, Mali bridge #517, or an open driver pair with its release), the
  driver mode and choices, what Auto would pick, the GPU (family, Vulkan device name, kbase
  gpuinfo, Android, SoC, kernel), the GPU nodes the app can see (access, device numbers, SELinux
  label; nothing is opened) and the guest's graphics environment. At the end it adds the
  compositor's driver lines from app.log and the game launches that were logged.
- vulkan-summary.txt: the session's own vulkaninfo --summary with the driver as exported, in the
  background with a 10 s timeout.
- Per-game logs under games/<time>-<appid>/: Proton's log, DXVK's and vkd3d-proton's, Mesa's and
  PanVK's startup messages and the launch environment, from droiddeck-game-env. Each file is cut
  to 20 MB and the folder to 120 MB at the session's end.
- The per-game logs and the Vulkan summary are on for a GPU that is not an Adreno, and on an
  Adreno only with Setup's new "Detailed graphics logs" switch. Share logs already takes the
  whole session folder, so all of it is in the zip, scrubbed as before.

* Mali pairs: a stand-in render node, the driver's real library name, and a Mali known by its chip

- Auto on Jim's Dimensity 9300+ chose the bridge: the GPU had no name (the app had not asked
  Vulkan yet and kbase's gpuinfo is unreadable under SELinux), so a Mali-G720 counted as a Mali
  no build covers. The name now falls back to the GPU the chip is known to carry (Dimensity,
  Helio, Kompanio, Tensor); and when nothing names it, Auto asks the vendor Vulkan driver off the
  main thread before it settles for the bridge. Auto logs what it decided and why.
- An open Mali driver pair gets /dev/mali0 as the guest's render node, as the bridge does
  (bindGpuNode, BL_DRM_STANDIN): gamescope needs the node the driver reports. The session shim
  only answers libdrm's PRIME and GEM-close calls on it, which PanVK on kbase never makes.
- A PanVK or RADV runtime half is installed as libvulkan_panfrost.so / libvulkan_radeon.so rather
  than under Turnip's name; Turnip keeps libvulkan_freedreno.so.
- graphics-report.txt names the render node the guest got, adds the session script's driver lines
  and flags a failed Mali-bridge self-test at the top.

* Auto takes the RADV pair on a Samsung Xclipse, with the system route as its fallback

A tester's Galaxy S24 FE (Xclipse 940) boots to Steam with the RADV-Xclipse pair, where the
system-driver route fails at Steam's OpenGL (glamor and GLX cannot load a driver). Auto now
downloads and selects RADV on an Xclipse like the Mali pairs, and falls back to the system route
only when RADV is not published. #570's reset of both drivers at each start no longer undoes an
open driver pair. PowerVR (vulkan.powervr.so) keeps the system driver as before.

* An Exynos with a Mali is a Mali, not an Xclipse

The system route took vulkan.samsung.so as Samsung's Xclipse, but an Exynos 1280/1380 (Galaxy Tab
S9 FE: Mali-G68) may name its HAL the same; with /dev/mali0 present that HAL is not the system
route's, and the device is detected as the Mali it is. The chip table adds the Exynos 1380 and
1280 (s5e8835, s5e8825: Mali-G68).

* Auto fetches the open drivers itself; Xclipse named as such; sound and frame-gen in the logs

- Auto looks online again when its saved release list predates this device's open drivers, so a
  Samsung or Mali phone gets its pair without pressing refresh first.
- A Samsung Xclipse is labelled "Samsung Xclipse" (not PowerVR), with its own support line.
- Per-game logs add audio-streams.txt: PulseAudio's sink inputs 20 s and 60 s into the launch,
  so a silent game shows whether its stream reached Pulse.
- graphics-report.txt states the frame-generation setting and keeps the LSFG gate, FrameGen and
  audio lines from app.log.

* Client sound retries a briefly disconnected AAudio; kbase ioctl refusals explained in the report

A Galaxy S24 FE's session had no sound at all: the DirectAudio sink's AAudio stream opened with
AAUDIO_ERROR_DISCONNECTED while the output route was still changing at session start, and the
plain AAudio fallback failed the same moment. The sink guard now retries DirectAudio, then the
plain sink, five times over ten seconds and says which one it got. graphics-report.txt keeps the
client-sink and kbase lines, and explains a KBASE_IOCTL_MEM_EXEC_INIT EPERM as the kbase driver's
own answer rather than the session's sandbox.

* Mali pairs draw the display on the phone's own Mali driver; Xwayland falls back without glamor

- Testers' Mali pairs got the guest drawing (gamescope on the stand-in node, Zink on PanVK) but
  the compositor's PanVK Android half failed on Mali gralloc: it probes AHB formats 0x38/0x3b that
  mali_gralloc does not have, and "vkCreateImage w/ native buffer failed". Under Auto a Mali pair
  now leaves the display on the phone's own Vulkan driver (dma-buf, modifiers and AHB all pass the
  testers' checks) and runs the pair's PanVK in the guest; the Android half stays installed for a
  manual pick. "Pair in use" now follows the runtime half.
- A session after one whose Xwayland crashed (segfault, then gamescope's X11 I/O error) starts
  Xwayland with XWAYLAND_NO_GLAMOR=1 on a non-Adreno device, and says so in app.log.
- graphics-report.txt names the display driver the compositor used, and keeps gralloc, dma-buf,
  modifier and import lines from app.log and Xwayland's own lines from the session log.

* Non-Adreno sound: the daemon falls back on its own; the build's commit in device.txt and the report

The S24 FE's session still had no sound: its AAudio refuses DirectAudio's low-latency stream with
AAUDIO_ERROR_DISCONNECTED, and the sink guard never got to retry because pactl did not reach the
daemon in time ("the daemon did not answer in 15 s"). On a GPU that is not an Adreno the daemon's
own start-up script now asks DirectAudio again without low latency and loads the plain AAudio sink
underneath it, so a default sink exists whatever pactl does. device.txt and graphics-report.txt
name the build label and source commit, so a log says which build it came from.

* Non-Adreno sound: no second DirectAudio load (it hung the daemon); the plain AAudio sink underneath

On the S24 FE the daemon stopped after DirectAudio's failed low-latency load: the second
DirectAudio load in the same daemon never returned, so nothing after it ran and pactl never
connected. The daemon now loads only the plain AAudio sink (no low latency) after DirectAudio,
inside .nofail, and sets whichever exists as the default.

* gamescope: explicit sync off where the kernel or the stand-in node cannot signal its timelines

Adds tools/gamescope/patches/0126 (GAMESCOPE_DISABLE_EXPLICIT_SYNC=1 for the nested backends) and
sets it on a GPU that is not an Adreno when the kernel is older than 6.6 (no
DRM_IOCTL_SYNCOBJ_EVENTFD) or the guest's render node is Mali's stand-in. The graphics report
lists it with the rest of the guest's graphics environment.

* Test branch: gamescope with the explicit-sync switch (0126), pinned by sha

Built by build-gamescope.yml run 38335744351 from 0136fb61 and published as a component release
of The412Banner/droiddeck-nonadreno-drivers; the apk build fetches it by tag and checks the sha.

* Sound: nothing that can block in the daemon's start-up; Steam's web compositing in software on open pairs

- The S24 FE (US) daemon hung loading the plain AAudio sink from default.pa (its AAudio open
  blocks rather than fails), so it never finished starting. default.pa is back to DirectAudio
  alone inside .nofail; the fallbacks are loaded from the app once the daemon answers, each pactl
  bounded by its timeout (DirectAudio, then the plain sink with and without low latency). The
  guard waits 30 s and logs, with timestamps, how long the daemon took, whether its socket and
  pactl exist, and what it finally got.
- With an open driver pair on a GPU that is not an Adreno, Steam's CEF composites in software
  (-cef-disable-gpu-compositing) with GPU raster kept: RADV on Xclipse showed stale, half-written
  tiles, as #570 saw on PowerVR. The session log says so.

* Mali under Auto: the display always on Android's own Vulkan loader; a reboot hint for refused audio

Axel's Tab S9 FE still drew its display with the PanVK Android half, set by an earlier build, and
stalled at "Waiting for Steam's first frame" on Mali gralloc's format probes. Under Auto a Mali's
display now goes to Android's own loader whatever was set before (DriverMenus resets it, and the
compositor's driver choice ignores a pair's Android half), with no HAL path to find first. A pair
counts as in use when its runtime half is. A manual pick of the PanVK display is kept.
When audio.log shows AAUDIO_ERROR_DISCONNECTED, the report and the client-sink line say Android
refused the output and that rebooting the phone usually fixes it.

* Fix: call SessionPrefs.gpuDriverMode through its instance from Java

* Compositor: log each new dma-buf format/modifier/size it imports (for colour and swizzle reports)

Steam's software web compositing already covers every open driver pair, Mali included.

* Open driver pairs: vkd3d-proton reports the driver's caps; buffer attach/release tracing; Mesa log

- With a PanVK or RADV pair (BL_OPEN_DRIVER=1), droiddeck-game-env drops the app-wide
  VKD3D_FEATURE_LEVEL=12_2 and VKD3D_SHADER_MODEL=6_9 so vkd3d-proton reports what the driver has
  (an Unreal DX12 game crashed at start on PanVK with the overrides); a game's own setting still
  wins, and launch.txt records DROIDDECK_VKD3D_CAPS=driver.
- The compositor traces, on a GPU that is not an Adreno, the first 400 dma-buf attaches and buffer
  releases by wl_buffer id and sending line, and logs any release of a buffer already released
  and not attached again (gamescope's "Compositor released us but we were not acquired" on G925).
- Per-game logs no longer set MESA_DEBUG=silent, which kept mesa.log empty.

* Tests: the dma-buf quarantine harness follows send_buffer_release's tracing macro

* Mali (Venus): David Cimo's Venus route sources and recipes

From DavidCimo/droiddeck-mali, branch mali-venus @ 4f838891 (prototype 13; his 12 commits on top
of DroidDeck 0.3.1, GPL-3.0), by David Cimo:
- tools/venus: Mesa Venus vtest patch (incl. the per-queue submit_mutex fix and the cached-memory
  frame readback), virglrenderer Android patch, the Mali compatibility layer (the six features
  DXVK needs: fill modes, multi-viewport, robustness2, clip/cull distance, BC1-BC7) and his tools.
- tools/linuxfs/preload/venus.c: keeps Zink's kopper under Venus.
- tools/gamescope/patches/0127 (his 0114): shm buffers uploaded on every commit.
- docs/development/mali-venus.md: his design notes.
- tools/venus/ci-build.sh and build-venus.yml: his build recipes as one CI job that packs the
  component (vtest server, ICD, layer) as venus.tzst for the apk.

Co-authored-by: Dersim Abbas <[email protected]>

* Mali (Venus) as a selectable route, by David Cimo

Ports David Cimo's Venus route (DavidCimo/droiddeck-mali, mali-venus @ 4f838891) onto this branch
as "Mali (Venus)", one tap in Setup next to the Mali bridge:
- VenusComponent (his): stages the Venus ICD and runs virglrenderer's vtest server over the
  phone's own Vulkan driver; the guest gets VK_ICD_FILENAMES, VN_DEBUG=vtest, VTEST_SOCKET_NAME and
  MESA_VK_WSI_DEBUG=sw.
- Selection rule: Venus when Setup's "Mali (Venus)" is on, on a GPU that is neither an Adreno nor
  a system-driver one (PowerVR, Xclipse), and the apk carries it. It then replaces the Mali bridge
  and any open driver pair for the session, and the display stays on the phone's own driver.
  Auto is unchanged (the PanVK pair). Adreno never takes any of it.
- The compositor enables only the dma-buf extensions the driver has, logs a vendor driver's
  extension list, and gives a zero row pitch a packed row (his vk_present.c changes).
- The Mali compatibility layer is staged into the runtime on non-Adreno devices only.
- The component (vtest server, ICD, layer) comes from build-venus.yml as venus.tzst, published on
  The412Banner/droiddeck-nonadreno-drivers and pinned by sha in tools/venus/release.env.
- gamescope 3.16.29-p15 adds his shm upload patch (0127) next to 0126; pinned by sha.
- device.txt and graphics-report.txt name the route.

Co-authored-by: Dersim Abbas <[email protected]>

* Mali: Venus gamescope without robustness2, the bridge switch honoured and kept, clean driver switches

- gamescope 0128: VK_EXT_robustness2 is optional. A Mali driver through Venus has none, and
  gamescope stopped at "Missing required extension: VK_EXT_robustness2" (the wayland_surface error
  after it was only the knock-on). Without it, unused descriptor slots point at a 1x1 texture
  instead of VK_NULL_HANDLE. (David Cimo's Pixel 9 driver has robustness2, so his build never
  needed this.) gamescope component rebuilt and pinned by sha.
- Open-files limit: on a GPU that is not an Adreno the session script raises the soft limit to the
  hard limit for the whole session tree (a PanVK-kbase session froze on "Too many open files"
  after ten minutes) and graphics-report.txt shows before and after. A stopgap; the driver leak is
  fixed separately.
- The Mali bridge switch is honoured: when on and usable it wins over a PanVK pair for the
  session (Venus still wins over it). Selecting a pair no longer switches the bridge off, which is
  why the switch did not survive an app restart (Auto re-selected the pair at start). Setup says
  when this phone cannot use the bridge at all.
- Driver switching: picking a Mali pair by hand keeps the display on the phone's own driver, as
  Auto does (a PanVK Android display stalls on Mali gralloc at "Waiting for Steam's first frame",
  and stayed set after switching back); installing a bundle again replaces it instead of adding a
  "-2" copy beside the old one.
- graphics-report.txt names the runtime driver's folder and libraries, and the previous session's
  drivers, flagged when they changed.

* gamescope p16 (0128 robustness2 optional), pinned by sha; graphics-report says who chose the display driver

* Bundle replace only for open non-Adreno pairs; Adreno bundles keep their numbered copies

* Rebuilt open drivers reach phones: re-downloaded when their digest changes, release list hourly on non-Adreno

The non-Adreno driver releases replace their assets in place (the PanVK kbase sync-file fd leak
fix). A download is now remembered with its GitHub sha256, and an asset whose digest changed
counts as not installed, so Auto fetches it again and the bundle replaces the old one; the release
list is re-read after an hour instead of a day on a GPU that is not an Adreno.

* Mali compatibility layer: multiDrawIndirect, depthClipEnable and robustness2 for DXVK on Venus

A Galaxy A15 (Mali-G57) on Venus got gamescope and Steam up, but DXVK refused the GPU:
"Device does not support required feature 'multiDrawIndirect'". DXVK 3.1 stops at the first
missing feature, so the rest of its required list (dxvk_device_info.cpp) was checked against what
the testers' Mali drivers cannot have, and covered in the same pass, on top of David Cimo's six:
- multiDrawIndirect: reported, taken off the device, and an indirect draw with drawCount > 1 is
  recorded as single draws at offset + i * stride; maxDrawIndirectCount reported as 2^30.
  (drawIndirectCount is optional for DXVK and stays as the driver says.)
- VK_EXT_depth_clip_enable (required): offered when the driver lacks it; depthClipEnable off in a
  pipeline becomes depthClampEnable, and the extension's structures never reach the driver.
- VK_EXT_robustness2 missing entirely (gamescope saw the same on these phones): offered with
  robustBufferAccess2 (as robustBufferAccess, as before) and nullDescriptor, emulated by small
  stand-ins (a zeroed buffer, a 1x1 image) for VK_NULL_HANDLE buffers, image views and vertex
  buffers in plain, template and push descriptor writes and vertex-buffer binds. A null
  texel-buffer view stays null (its format is not known).
The layer logs every feature it emulates per device.

* Venus component 22694a38 (layer: multiDrawIndirect, depthClipEnable, robustness2), pinned by sha

* Non-Adreno round 3: bridge images, Venus GL, shared buffers, Steam GPU drawing

- vkbridge: the memory model skips lazily allocated types. Mali-G615 lists
  DEVICE_LOCAL|LAZILY_ALLOCATED first; taken as the device-local type it left
  images with no memory type (self-test "image clear/draw: setup" FAIL,
  gamescope "No compatible image memory type").
- Session: a bridge self-test that fails images or draws falls back to the
  runtime's driver pair for that session (BL_VKBRIDGE_FALLBACK_ICD); Setup
  and the graphics report say so.
- Venus: the Mali compatibility layer runs in every process but gamescope
  (DROIDDECK_MALI_COMPAT=1), so Zink in Steam and Xwayland gets
  nullDescriptor; null texel-buffer views get stand-ins too. Without
  nullDescriptor the session's GL goes to llvmpipe.
- gamescope 0129: a refused compute pipeline is named with its SPIR-V
  capabilities.
- Compositor: a buffer on two surfaces at once (gamescope's planes while a
  notification is up) is released only when the last one lets go.
- Open driver pairs: processes.txt timeline for the first three minutes;
  Setup "Steam interface: GPU drawing" turns off software web compositing.

* Pin gamescope p17 (0129) and Venus 55b30fbf

* Non-Adreno round 4: Mali GPU-drawing block, gamescope YCbCr retry, GL probe, bridge draw sync

- Setup's "Steam interface: GPU drawing" is hidden on Mali and a saved "on" is
  cleared: with PanVK it rebooted an Edge 60 (G615) as steamwebhelper started.
  Kept for Xclipse.
- gamescope 0130: a composite pipeline refused at start-up is retried with a
  layout without immutable YCbCr samplers (G615 through Venus refused every
  composite shader; their SPIR-V has no Float16, so fp16 was not the cause).
- Venus: droiddeck-glprobe opens GL through GLX the way the Steam client does,
  with the loader's reasons logged; when it fails the client's GL goes to
  llvmpipe.
- vkbridge self-test: the draw check waits for the render pass's stores before
  copying out (a tiler read zeros without the dependency).

* Pin gamescope p18 (0130)

* Non-Adreno round 5: gamescope memory fallback, Venus destroy guard, OpenAL depot

- gamescope 0131: DEVICE_LOCAL|HOST_VISIBLE requests fall back to host-visible
  memory (the bridge's unified-memory model lists none with both), with the
  request, type bits and memory types logged on failure; 0130's probe pipeline
  is kept and skipped when forced.
- Venus: GAMESCOPE_NO_YCBCR_SAMPLERS=1 from the start; virglrenderer ignores
  the destroy of a pipeline it never created instead of marking the context
  fatal (vtest server exit 137 on a Mali-G615).
- Steam's OpenAL depot (229020) maps to oalinst_dll, so games that list it
  (Hotline Miami) get OpenAL in their prefix.

* Pin gamescope p19 (0131) and Venus 6199a93f

* Non-Adreno round 6: PowerVR PRIME on old kernels, thermal, hang-up names, bridge flippable images, Venus Zink variants

- drm.c: PR #570's current PRIME ownership check (F_DUPFD_QUERY, then kcmp,
  then the F_SETOWN_EX owner marker): on a kernel before 6.10 with kcmp
  blocked, drmPrimeFDToHandle answered EOPNOTSUPP for every PowerVR import
  (Pixel 10, Android 17: Zink "failed drmPrimeFDToHandle", Xwayland error 7,
  gamescope X11 I/O error). The Mali stand-in node is kept.
- A /sys/class/thermal the kernel will not list gets an empty directory, so
  mangoapp no longer aborts on it.
- gamescope 0132: a hung-up waitable is named before the abort; 0133: the
  bridge's flippable images use supported-but-unexportable modifiers instead
  of asserting.
- Venus: when Zink does not start, the loader's view is logged and Zink is
  retried without ZINK_DEBUG=compact, with ZINK_DESCRIPTORS=auto and without
  kopper before falling back to llvmpipe.

* Pin gamescope p20 (0132, 0133)

* Non-Adreno round 7: bridge command-buffer guard, Venus Zink trace, PowerVR mangoapp, Xclipse label

- vkbridge server: every command buffer a device hands out is tracked; Begin,
  End (stream replay) and QueueSubmit refuse one it never allocated or has
  freed (VK_ERROR_DEVICE_LOST, logged) instead of passing it to the driver,
  where Mali's vkBeginCommandBuffer crashed the app (SIGSEGV 0x10, Steam's
  Zink on an Edge 60 and a Tab S11).
- vkbridge server: a fault inside the driver while serving a request cuts
  that client off (SIGSEGV/SIGBUS guard per request) and leaves its objects
  alone; the app and the other clients carry on. Other faults go to the
  previous handler.
- vkbridge server: the pipeline cache directory is copied; the JNI string it
  pointed at was freed (garbage in the cache path).
- Venus: DROIDDECK_MALI_COMPAT_TRACE=1 makes the Mali compatibility layer log
  what an application queries before vkCreateDevice; a failed glprobe adds a
  traced run and vulkaninfo --summary under the same environment.
- PowerVR: mangoapp draws with llvmpipe (Zink could not make it a swapchain
  and it crashed at every start).
- Setup: an Xclipse GPU reads "Experimental: RADV or the system Xclipse
  driver", not the PowerVR line.

* Pin Venus e40cd361 (layer trace)

* Non-Adreno round 8: bridge sharing fallback and crash detail, Venus diagnostics and 1.2 gate, compositor disconnect reasons, RADV upstream

- Mali bridge: the self-test fails "memory sharing" when the server offers
  no host-visible memory type (no sharing method passed, RP4 Pro G77); the
  session then falls back to the PanVK pair like a failed draw, and Setup
  says the bridge cannot share memory with that driver. Without a pair to
  fall back on, the log says so.
- Mali bridge: a guarded driver crash now logs the sub-command it was in,
  the fault address and pc (library + offset via dladdr), the command
  buffer's level, begin flags, pool flags and state, and the last 32
  replayed commands. Begin on an executable buffer from a pool without
  RESET_COMMAND_BUFFER_BIT is logged once (implicit reset).
- Venus: vulkan-summary.txt gets the device extension list and key
  features (full vulkaninfo in vulkan-full.txt); a failed GL probe runs
  once with VN_DEBUG=init,result, ZINK_DEBUG=mem, MESA_LOG_LEVEL=debug and
  the layer trace into glprobe-trace.txt. The Mali compatibility layer's
  trace covers every pre-device query (devices, memory, queue families,
  formats, image formats, external handles) and names the extensions.
- Venus: off when the phone's driver reports Vulkan below 1.2 (gamescope
  skips such a device); Setup and the graphics report say why and the
  session uses the PanVK pair.
- Compositor: libwayland's own disconnect reasons go to wayland.log, and a
  client's buffer may grow to 1 MB before a slow reader is dropped. A
  session that ended without teardown records Android's exit reasons.
- Drivers: RADV-Xclipse upstream (radv-xclipse-upstream) is a manual
  choice for Xclipse; Auto keeps radv-xclipse.

* Tests: the terminal-callers harness stubs the Quest layer and Cardboard modules and Android's system properties

* Pin gamescope p21 (main's patches + 0120-0133) and Venus a4d47cf3 (layer trace)

* Display driver Auto: Quest's patched Turnip first; the system driver only for a Mali, other GPUs keep the generic fallback

* Open driver pairs: the installed build's version, its upload date, and an Update when the release is rebuilt in place

- The release check records each asset's GitHub id and updated_at; a
  download records them (with the digest) for an open non-Adreno pair.
- Components > GPU drivers: an installed open pair shows its manifest
  version ("android <tag> + linux <commit>"), one not yet installed shows
  "built YYYY-MM-DD". A different digest, asset id or upload time on the
  same tag marks the installed pair "Update available" with an Update
  action: the pair is downloaded again and replaced in place, and stays
  selected only if it was. Adreno and DD-Turnip pairs are unchanged.
- graphics-report.txt: the pair release line gives the installed and the
  listed upload dates and flags a newer build; an exact bundle id match is
  preferred (RADV-Xclipse-upstream is not RADV-Xclipse).

* Session preload: drop oom_score_adj raises so lmkd does not treat games as cached apps

steam-launch-wrapper starts every game with --oom-score-adjust 900; on
Android 900 is CACHED_APP_MIN_ADJ, so lmkd kills the running game first
when memory runs short (and on Linux 6.1 the raise becomes the process's
permanent minimum). A process in the app cannot go below the score it
inherits, so the writes that succeed from inside are raises: every
write-open of /proc/<pid|self|thread-self>[/task/<tid>]/oom_score_adj (or
oom_adj), through open/openat or fopen, goes to /dev/null, logged once.

Based on the oom_score_adj redirect in the preload's opens.c by jdmg28 (https://github.com/jdmg28/DroidDeck-Mali/commit/6be00e00)

* Display: why presenting failed on the session-ended screen, and the display driver that really loaded

- The compositor records why it cannot present (no Vulkan driver loaded,
  vkCreateInstance/vkCreateDevice failed, no GPU or graphics queue, no
  swapchain on the screen with the driver's reason, device lost, GPU work
  unconfirmed). Each is a DD_PRESENT_FAULT line in wayland.log, read back
  over JNI or from wayland.log; the session-ended screen shows it instead
  of a black screen, and graphics-report.txt gets "!! DISPLAY FAILED". A
  first frame or a working swapchain clears it.
- After the display device is created, the driver that answered is logged
  from VkPhysicalDeviceDriverProperties (device, driverName, driverInfo,
  driver id, Vulkan version) and reported in graphics-report.txt. When a
  driver pair's display half was requested but a vendor driver answered
  (adrenotools falls back to the system driver when the requested one does
  not load), it says so.

Based on DD_PRESENT_FAULT in vk_present.c / waylandcomp_jni.c / SessionActivity by jdmg28 (https://github.com/jdmg28/DroidDeck-Mali/blob/arena/cd8ab18a-droiddeck-mali/app/src/main/cpp/waylandcomp/src/vk_present.c)
Based on the adrenotools silent-fallback finding by ryanistr (https://github.com/ryanistr/DroidDeck-Mali/blob/main/docs/development/panvk-mali-g77-bringup.md)

* tests: steam presentation test sets 0.5.0's steam_client

* Non-Adreno: find every driver pair, Venus before the bridge, fix the bridge's begin info

From the logs testers sent since c9ebb247:
- The app read the drivers repo's newest 15 releases only; the component releases (gamescope,
  Venus) pushed every PanVK pair and RADV-Xclipse past them, so Auto found no pair on any Mali
  and switched the Mali bridge on. The non-Adreno repo is now read in one page of 100.
- Auto's stand-in for a missing pair: Mali (Venus) where the apk carries it (Steam runs on it
  from the G57 to the G1-Ultra), the bridge otherwise, nothing on a driver below Vulkan 1.2. It
  no longer clears the Linux driver, does nothing when the list could not be read, records that
  it chose the route, and turns it off again once a pair is installed. A bridge that earlier
  builds left on under Auto is taken as Auto's.
- Mali bridge: vkBeginCommandBuffer sent its begin info without the count the server decodes, so
  Mali got a zeroed begin info with stale flags and crashed at 0x10 (Pixel 8 Pro G715, Lenovo
  G615, Xiaomi G925, Tab S11).
- Mali-G1 series (MT6993, arch v14) known by its chip; its label says Venus, not the bridge.
- A session Android killed for memory is said so on the next start (the S24 FE "crashes" were
  low-memory kills, not driver faults).
- The in-app updater never installs an official (Adreno-only) build over this test build.

* Venus: destroy the device's object mutex after the objects that lock it

Bionic aborts on locking a destroyed mutex ("pthread_mutex_lock called on a destroyed mutex",
Tab S9 FE G68, Pixel 9a), so a client that exited holding objects took the render worker down.
From BattleTempest's DroidDeck-Pixel (kevinbryant551-ux/DroidDeck-Pixel, GPL-3.0).

* release check: a session helper may be extracted to another runtime path

droiddeck-steam-taskset (#570) goes to usr/local/libexec/droiddeck-steam/taskset, where PATH puts
it in front of Steam's own taskset for the client only; 0.5.0's check wanted every droiddeck-*
helper at its own path.

* Venus component 93038b7d (destroyed-mutex fix)

* Update MaliBridge to v0.5.1 (#1135)

* Mali Auto: never leave a session on the runtime's Turnip

Logs from a Retroid Pocket 4 Pro (G77), Galaxy S21 Ultra (G78), Tab S11 Ultra
(G925) and Xiaomi G610 showed Auto ending on the runtime's Turnip, where
gamescope finds no GPU ("failed to find physical device"): after a pair's
trial rolled back, when the pair is on the bad list, and when the driver
list could not be read. In each case Auto now switches on its stand-in
(Venus, or the bridge) when no open driver, Venus or bridge is set.
Under Auto the Mali display also leaves any imported driver, not only a
pair's PanVK half (a lone PanVK_G720 import gave a black screen).

* Mali bridge: MaliBridge 224b6e36 (v0.5.3 source, by ASEbrahim)

From c65062b6 (v0.5.1). v0.5.2/v0.5.3: Steam mode on older Mali drivers
(G77/G710/G610/G57) gets past "starting display and audio", OpenGL 4.6 on
most Malis through dynamic-rendering and vertex-format emulation, Dimensity
9500 texture fixes, async pipeline compile, RE2 flicker on G925, DirectX on
the Pixel 8 family. JNI entry and session env names are unchanged.

* gamescope 0134: linear when the backend offers no modifier; Venus on Mesa 26.2.5

0134: CVulkanTexture::BInit asserted !modifiers.empty() when the nested Wayland
backend offered no modifier for a flippable image's format (NieR Replicant on a
Pixel 9a under Venus, session exit 134). It now tries linear and logs it.
Venus ICD: mesa-26.2.5 (venus TLS teardown from an unloaded driver, virtio-gpu
devices without 3D skipped); virglrenderer stays on 1.3.0.

* gamescope p22-nonadreno (0134 + main's 0116 cursor fix; build run 40159674290)

* CI: build the non-Adreno additions before the preload check

* build.gradle: drop verifyHandModel (main moved the hand models into the tracking pack)

* Venus component venus-proto13-e24ed8ab (Mesa 26.2.5; build run 40161447324)

* CI: fetch the Mali (Venus) component again (lost when build.yml took main's jobs)

Build a7a1ebf1 (run 40161734101) had no assets/venus or libvirgl_test_server.so, so
VenusComponent.available() was false and Auto's stand-in would have been the bridge.

* Auto: the Mali bridge stands in on a Vulkan 1.1 Mali driver

Auto refused both Venus and the bridge when the phone's driver reports Vulkan
below 1.2, so on the G77, G78 and G610 (r32p1/r38, Vulkan 1.1) a rolled-back or
missing pair left the runtime's Turnip: "failed to find physical device"
(Retroid Pocket 4 Pro after the JM pair's trial, Xiaomi 13T). Only Venus needs
1.2; the bridge raises the driver for gamescope and runs on these phones. A
Venus switch that cannot start on such a driver no longer counts as a route.

* Tests: stub main's g_release_unpaced in the DMA-BUF quarantine probe

The probe copies release_buffer out of compositor.c; since the merge it reads
g_release_unpaced, which the harness did not declare (setUpClass failed to compile).

* Update MaliBridge to v0.5.4 (ASEbrahim)

48477fe6 (tag v0.5.4): Dimensity 9500 green squares and red-tinted textures,
Balatro and other OpenGL text, fewer crashes when memory runs low, one screen
update per game frame, picture kept after a long background pause (SIGHUP no
longer stops the server), wrong colours after changing graphics settings on
the G720, more in the session report. The app-side changes of his DroidDeck
Mali app (Steam menus setting, per-game setup, Send logs, updates) are not
part of the MaliBridge repository and are not in this build.

* gamescope 0135: linear when the driver rejects every offered modifier

An r32p1 Mali-G610 (Vulkan 1.1) behind the Mali bridge answered both of the
backend's modifiers for AR24 with VK_ERROR_FORMAT_NOT_SUPPORTED, so the first
flippable image failed and gamescope asserted in vulkan_create_flat_texture
(Xiaomi 13T). Linear, when offered, is tried anyway; the bridge makes such
images without the handle type and exports them through a dma-heap. If the
image cannot be made, BInit fails as before.

* gamescope: pin the non-Adreno p23 build (0135)

* Tests: the terminal-callers harness stubs take_shm's game-frame detection

Main's take_shm now reads surface damage, parent/role/mapped and the HUD state
to spot game frames; the harness declares them so the copied function compiles
(setUpClass failed in Build APK run 40505286127). No damage, so it stays off.

* gamescope: number the test branch's patches 0920-0935

Main now has its own 0121-0125 (window override walk, host-event wake, commit fences, done at
import, HUD frame times), which sorted in between this branch's 0120-0135. The branch's sixteen
patches move to 0920-0935 with the same last two digits, so they apply after main's set and stay
clear of its next numbers. Applying all 38 to gamescope 3.16.29 in the old order and the new one
gives the same tree (checked with patch -p1, as build-in-arch.sh does). PATCHES.md and the one
code comment that named 0130 follow the new numbers.

* check_session_assets: helpers count in usr/local/bin or the Steam taskset folder only

The merge kept this branch's pattern (any usr/ target), which main's new test rejects: a helper
extracted into a subfolder of usr/local/bin must not count. The pattern now takes usr/local/bin
under any name, as on main, plus usr/local/libexec/droiddeck-steam for droiddeck-steam-taskset.
Every helper in the overlay is still found extracted in SessionFiles.kt.

* gamescope: pin 3.16.29 p24 (non-Adreno)

Built from this branch @d13b9d88 (run 41119284267): main a5b035b2's patches plus this branch's,
now 0920-0935. All 38 applied, gamescope's tests 2/2. sha256 1d1088d4.

* Update MaliBridge to v0.5.4.2 (ASEbrahim)

95a1e600 (tag v0.5.4.2), was v0.5.4 (48477fe6). For the bridge: memory sharing without the handle
type where that is all the self-test passed (unflagged buffers, client/memory.c, server_memory.c),
the older-Mali (G52-G77) game-mode start fixes, the r38p1 beta geometry-shader erratum (gs_hide),
present timeouts and an API cap per process. Client, server and the in-app library all build from
the submodule with MaliBridge's own scripts, which did not change; the protocol only gained
fields an older server sends as 0.

* Session end: the compositor fault query no longer fails the hint when the native library is absent

presentationFault() (6778a224) called WaylandCompositor.compositorFault() directly. Main's new
SessionEndHintTest runs sessionEndHint() in Robolectric, where libdroiddeckwayland cannot load, so
all six cases ended in UnsatisfiedLinkError (Build APK run 41125749537). The call is now wrapped
like runtimeDamageHint()'s, and without an answer the hint reads wayland.log as before.

* test: stub the GPU busy reader in the caller harness

* fix: release build is not debuggable, Mali uses the system display driver, staging failure ends its own session

* fix: address non-Adreno review findings

* fix: preserve current gamescope and GPU warning behavior

* fix: retain graphics failure hints and launch diagnostics

* fix: identify non-Adreno GPUs in driver failure hints

* test: harnesses follow the external client barrier and effects upscaling changes

* fix: client sources return to foreign ownership after the overlay draws

---------

Co-authored-by: Leask Wong <[email protected]>
Co-authored-by: wallentx <[email protected]>
Co-authored-by: Stefan Zijlstra <[email protected]>
Co-authored-by: William Allen <[email protected]>
Co-authored-by: The412Banner <[email protected]>
Co-authored-by: Dersim Abbas <[email protected]>
Co-authored-by: root <[email protected]>
Co-authored-by: ASEbrahim <[email protected]>
Co-authored-by: The412Banner <[email protected]>
Co-authored-by: The412Banner <[email protected]>
2026-11-05 22:06:16 -05:00
MaxsTechReview f4e5032b7d fix(fex): skip local patches already in the FEX release (#2152) 2026-11-05 21:33:57 -05:00
Kurt Himebauch c8a76e7cce perf(agent): run diagnostic cleanup off startup thread (#2147)
* perf(agent): run stage cleanup off startup thread

* fix(agent): preserve active diagnostic stages

* test(agent): verify active stage locks cross-process
2026-11-05 20:04:16 -05:00
Kurt Himebauch 6a681bcbc2 docs: clarify DX12 budget consumers and physical limits (#2148)
* docs: clarify DX12 budget consumers and physical limits

* docs: record controlled Thor gameplay limits
2026-11-05 19:04:11 -05:00
Moroni Granja cd452e2b60 fix: upload GOG cloud saves in Galaxy's gzip + metadata format (#1952)
fix: upload GOG cloud saves in Galaxy's gzip format
2026-11-05 18:56:21 -05:00
MaxsTechReviewandKurt Himebauch 5971b91ed4 fix: an unplugged game drive falls back to internal storage (#1913)
* fix: an unplugged game drive falls back to internal storage

When the drive chosen for game storage is not connected, installs, sessions and the install picker use internal storage instead of failing with "Game storage is unavailable". The saved drive is kept and used again once it is plugged back in.

* fix: store installs set to external use internal storage while the drive is unplugged

With default storage set to external, a missing drive or folder no longer stops installs at the location dialog or leaves the Epic launcher pointing at it. Installs go to internal storage and switch back to the drive once it is connected again.

* fix: external installs do not move to another card while the saved drive is unplugged

* fix(storage): verify saved volume is mounted

* fix(storage): reject unknown external volumes

---------

Co-authored-by: Kurt Himebauch <[email protected]>
2026-11-05 18:33:38 -05:00
Kurt HimebauchandK11MCH1 906e60a042 feat(desktop): show programs installed by Windows installers (#2146)
* Desktop: programs a Windows installer adds get a menu entry and desktop icon

Opening a Windows installer on the desktop runs it with Proton in a prefix
of its own (droiddeck-proton-run's "own" prefix). Proton's menu builder
turns the installer's Start Menu links into .desktop files, but keeps them
in the prefix's drive_c/proton_shortcuts with a Windows .lnk path as Exec,
and nothing outside the prefix pointed at the installed program: Nexon
Launcher installed fine on an AYANEO Pocket FIT Elite and could only be
found by browsing the hidden compatdata folder.

droiddeck-desktop-programs reads proton_shortcuts in every prefix that is
neither a Steam game's nor a current Steam shortcut's, resolves each link to
its program inside the prefix, and writes a menu entry that opens it with
droiddeck-open, so it runs in that same prefix, with the icon Proton
extracted. A program whose installer also put a link on the Windows desktop
gets a desktop icon once; one the user deletes is not put back. Entries go
away with their program; uninstallers get none. It runs when the desktop
starts and after droiddeck-open finishes a Windows program.

* Desktop programs: only programs that run in their prefix, Windows argument rules, one refresh at a time

A link resolved through Proton's Z: mapping could name a program outside
the scanned prefix, and droiddeck-open would then run it in another one.
A program is now listed only when droiddeck-proton-run's prefix_of puts it
in the prefix it was found in.

Shortcut arguments were split with shlex, so a Windows argument with an
apostrophe raised and stopped the whole refresh; they are now split by
the Windows command-line rules.

The desktop start and the end of a Windows program can refresh at the
same time and race on the same temporary files; refreshes now take turns
on a lock beside the placement record.

* Desktop programs: extract the script at session start

SessionFiles lists the overlay scripts the app copies into the runtime;
droiddeck-desktop-programs was missing, so a real build would never have
installed it and check_session_assets failed the build.

* Desktop programs: a compatdata folder it cannot read changes nothing

Any error listing steamapps/compatdata made the refresh see no prefixes,
so it removed every program entry and forgot which desktop icons it had
placed; the next good run then put back icons the user had deleted. Only a
missing compatdata now means there is nothing installed; any other read
error leaves the menu, the desktop and the placement record as they were.

* Desktop programs: an unreadable shortcut or desktop folder changes nothing

os.path.isdir and glob read an unreadable proton_shortcuts or Windows
desktop folder as empty, so the refresh dropped that prefix's entries and
placement records and a later run put back icons the user had deleted.
Those folders are now listed directly: a missing one is still empty, and
any other error leaves the menu, the desktop and the record as they were.

* Desktop programs: only a missing file reads as absent

read(), link_target() and host_path()'s letter-case lookup turned any
error into "absent", so an unreadable placement record read as empty and
brought back icons the user had deleted, and an unreadable shortcut or
link dropped its program's entry. A missing file, a wrong file type and a
malformed link still read as absent; any other error now reaches main(),
which leaves the menu, the desktop and the record as they were.

* Desktop programs: keep the placement record true when a run fails part way

The placement record was read after the menu was already rewritten, so an
unreadable record changed the menu although the run reported it had not.
It is now read first.

When writing desktop icons failed part way, the icons already written were
not recorded, and the next run recreated any of them the user had deleted.
The record is now written from the icons that exist after the attempt,
icons are written in a fixed order, and a failed write leaves no temporary
file on the desktop.

os.path.exists and isfile also read a program whose path cannot be reached
as missing; a stat that fails only because the file is absent now counts
as missing, and any other error leaves everything as it was.

* Desktop programs: leave the menu alone when Steam's shortcuts cannot be read

The list of Steam shortcuts comes from a reader that treats an unreadable
or damaged shortcuts.vdf as having no shortcuts, so a Steam shortcut's
prefix could get a menu entry and a desktop icon of its own. The file is
now checked first: a missing one still means no shortcuts, and any other
read or parse failure leaves the menu and the desktop as they were.

* Desktop programs: an unreadable Steam account or empty shortcuts file also stops the refresh

account_dir takes an unreadable loginusers.vdf or userdata folder as
missing, so the shortcuts check could be skipped and a Steam shortcut's
prefix still get its own entry. Both are now read first, and an access
error leaves the menu as it was.

An empty shortcuts.vdf is now parsed like any other, so it stops the
refresh too, the same way droiddeck-steam-shortcuts refuses to rewrite it.

---------

Co-authored-by: K11MCH1 <[email protected]>
2026-11-05 18:33:15 -05:00
Kurt Himebauch 9d2580d502 perf: reclaim launcher memory earlier under pressure (#2138)
* perf: reclaim launcher memory under system pressure

* docs: record Caravan save-load OOM with spare swap

* fix: wait for completed reclaim before increasing backoff
2026-11-05 18:27:35 -05:00
Kurt Himebauchanduubooster-team 6a5722c6d1 fix(preload): answer NETLINK_ROUTE queries inside a session (#2141)
* fix(preload): answer NETLINK_ROUTE queries inside a session

ip(8) could not run in a session: the app's SELinux domain has no
netlink_route_socket bind permission, so iproute2 failed at bind and never saw
the link that getifaddrs() and /proc/net already expose.

Serve NETLINK_ROUTE from the same etc/droiddeck-net snapshot: an AF_UNIX
socketpair backs the netlink fd, link/addr/route dumps and route get are
synthesised, writes return EPERM, and multicast subscriptions keep failing
with EACCES so consumers that wait for link events keep their fallback.

Replies carry the port id reported by getsockname() and recvmsg() rewrites
the source address to AF_NETLINK/pid 0. iproute2's dump loop requires both
before it accepts a message or NLMSG_DONE; when either is missing every
message is skipped silently and the process blocks in recvmsg().

* fix(preload): keep socket() and send() semantics for emulated netlink

* fix(preload): deliver netlink replies with the caller's blocking mode

* fix(preload): preserve NETLINK_ROUTE protocol and fd semantics

- Send ACKs for successful queries and honor NETLINK_CAP_ACK
- Reject unsupported subscriptions and apply socket timeouts
- Track socket identity and shared references across dup and close
- Support kernel connect and rewrite recvfrom source addresses
- Resolve local and connected routes before default gateways
- Add netlink and iproute2 regression tests

* fix(preload): handle netlink request limits and socket lifecycle

- Collect large sendmsg requests without falling back to UNIX sockets
- Reject oversized replies explicitly instead of returning partial results
- Allocate unique port IDs and reject duplicate bindings
- Honor socket flags and restore inherited sockets across exec
- Validate socket types and report netlink socket identity
- Replace the fixed socket table with a dynamic registry
- Add regression tests for batching, port conflicts and exec inheritance

* fix(preload): optimize netlink tracking and support link name queries

- Add lock-free fd tracking to bypass netlink locks on ordinary sockets
- Move descriptor restoration to initialization and explicit discovery paths
- Support RTM_GETLINK lookups via IFLA_IFNAME with correct error codes
- Add regression coverage for hot-path overhead and name-based queries

* fix(preload): accept unterminated netlink interface names

- Copy IFLA_IFNAME into a bounded, NUL-terminated buffer before lookup
- Match kernel string length validation and ERANGE errors
- Add regression coverage for unterminated names and boundary cases

* fix(preload): pin peers for netlink replies

* fix(preload): make autobind idempotent

* fix(preload): isolate forked netlink cleanup

* fix(preload): avoid interposed fd scans

* perf(preload): skip PID checks for untracked fds

* fix(preload): verify peer before replay

---------

Co-authored-by: uubooster-team <[email protected]>
2026-11-05 18:27:08 -05:00
MaxsTechReviewandKurt Himebauch b875b39905 fix(input): release held mouse buttons and keys when the session loses focus (#2139)
* fix(input): release held mouse buttons and keys when the session loses focus

A mouse button or key held in the guest stayed down when another window took focus or pointer capture ended before the release, because the release went to that window. A game that hides the cursor while a button is held, such as mouselook in World of Warcraft, then kept turning the camera with the cursor gone until the button was pressed again.

Buttons sent to the guest are now tracked like keys, and every held button and key is released when the session window loses focus or pointer capture, and when picture-in-picture opens.

* fix(input): release a held button where it was pressed

A button pressed through the compositor was released through the raw mouse channel if that channel turned on while it was held, and the reverse, so the guest kept it down. Each held button now remembers where it was pressed. A scroll-touch drag is also cancelled when the session loses focus.

* fix(input): release trackpad buttons on focus loss

---------

Co-authored-by: Kurt Himebauch <[email protected]>
2026-11-05 18:26:38 -05:00
Kurt Himebauchandxor ecedf131e0 fix(game-env): allow Supergiant mscoree fallback (#2143)
* fix(game-env): allow the builtin mscoree for Supergiant .NET crash reporter

* test(game-env): cover Supergiant mscoree fallback

---------

Co-authored-by: xor <[email protected]>
2026-11-05 18:04:16 -05:00
MaxsTechReview 8ac5652131 fix(components): a native-only override falls back to builtin when the prefix has no native DLL (#2142)
* fix(components): a native-only override falls back to builtin when the prefix has no native DLL

A component that asks for a DLL native-only (for example .NET 4.8's mscoree=n) left the DLL unloadable when its native copy never reached the prefix. On ARM64 prefixes the .NET installer leaves Wine's builtin mscoree in system32, so every program that needs .NET exited at launch with 'mscoree.dll not found'.

At launch, an override of n for a DLL whose system32 or syswow64 copy is missing or is Wine's own (a link into the Proton build, or a builtin or placeholder stub) becomes n,b, and the session log says so. A real native copy is still loaded first, and the saved component state is unchanged, so the override returns to n once the native DLL is there.

* Treat any prefix symlink as builtin when checking native overrides

* Treat an unreadable prefix DLL as missing for native overrides
2026-11-05 18:00:38 -05:00
Kurt HimebauchandMoroni Granja 5624b483fb fix(input): reach full guest screen with glasses pointer (#2145)
fix: the glasses pointer reaches the whole guest screen

Absolute input was clamped to the letterboxed picture rect computed for the phone view, while the compositor reads the input space against the glasses output. With different shapes that limited the guest pointer to a middle band of the scene, so the bottom and top edges were unreachable.

Send absolute pointer positions as the full-view fraction (the compositor already clamps to the scene) and use the whole view for the touchpad bounds and relative scale while the glasses own the output.

Fixes #2055

Co-authored-by: Moroni Granja <[email protected]>
2026-11-05 17:53:15 -05:00
Kurt Himebauch d64205ba5b fix: report Vulkan memory budgets without rejecting allocations (#2014)
Extend the VRAM setting to Vulkan memory-budget reports for DXVK and D3D12 while preserving real heap capacity and allocation results, removing the hard rejection that caused game crashes in FIT and Discord tests.
2026-11-05 17:25:54 -05:00
Kurt Himebauch df76f66e8f gamescope: ship p20 so the EA sign-in stays above the launcher splash (#2045)
#2037 added gamescope patch 0126, but the app does not build gamescope.
It stages the prebuilt binary pinned in `tools/gamescope/release.env`,
which is still `gamescope-3.16.29-p19`, built on Oct 29 before the patch
existed. A fresh install of a main build therefore still draws the EA
Steam Launcher's "Authenticating…" splash over the EA "Verify your
identity" sign-in. The APK of main 951270b carries gamescope
`1a9c836e…`, the same executable as p19.

This points `release.env` at `gamescope-3.16.29-p20`.

**Needs a release first:** please publish the `gamescope-patched`
artifact of the main build of 951270bc, [run
41637838020](https://github.com/Droid-Deck/DroidDeck/actions/runs/41637838020),
as `gamescope-3.16.29-p20` in Droid-Deck/DroidDeck-Components. Until
that tag exists, the runtime component build here cannot download it.

- That run applied patches 0002–0126 and passed gamescope's tests (Ok 2,
Fail 0).
- Archive SHA256:
`4537fa58b042f84e605123c4778ee7089442591cd3bfe0648ad96e113ef6117d`
- Gamescope executable SHA256:
`b22078c80c7262a24339105b2b02c90a8cf1a7929f6e10c212f465eb772dbb6c`
- WSI layer SHA256:
`eabc9dcab72dc52dcbd7def2df9ab99e2a26e662eba2c387059ed655801825c5`

The executable is byte-identical to a local build of the same patch
series.

## Testing

AYANEO Pocket FIT Elite, DroidDeck 0.6.9 (main 951270b), Burnout
Paradise Remastered installed fresh:

- With p19 as shipped, the EA app installed and opened its sign-in, and
the Burnout splash covered most of it.
- With this run's executable staged over `/usr/local/bin/gamescope`,
`EASteamLauncher.exe` (the splash) was running. `gamescopectl
focus_info` showed focus, input and keyboard on the EA sign-in with no
override window, and the sign-in was fully visible.
- `tools.tests.test_prepare_windows_runtime`,
`test_gamescope_override_stacking` and `test_hud_frames` pass.
2026-11-05 16:58:52 -05:00
MaxsTechReview da851dae22 fix(input): taps land where touched while a game holds the pointer (#2133)
* fix(input): taps land where touched while a game holds the pointer

Games that hide the system cursor and draw their own, such as Stardew Valley, hold the pointer the same way first-person games do. Every touch on the default touch mode then went through the touchpad, so a tap clicked wherever the cursor already was: tapping New opened Load, and a tap after Back could press Exit.

A touch now stays a direct touch while the pointer is held. A tap clicks where the finger is, and once a swipe moves past the touch slop it turns the camera with relative motion as before. Touchpad mode is unchanged.

* fix(input): keep the swipe anchor when the first finger lifts

When the first finger of a locked-pointer swipe lifts and another stays down, the next move is reported for the remaining finger. The anchor now moves to that finger, so the camera does not jump.
2026-11-05 16:29:19 -05:00
Kurt Himebauch 7a78d2b66b fix(agent): secure and refresh unrooted log exports (#2131)
## Summary
- Keep raw diagnostic staging in private cache and give each exported
archive a unique request directory.
- Remove that request directory after the CLI pull, including failed
transfers.
- Replace files from the previous extracted snapshot while preserving
unrelated destination files.
- Document that the old rooted-device fallback is not scrubbed.

Follow-up to #2061. The refreshed export tests now cover independent
archive outputs and stale snapshot replacement.

## Validation
- `wsl python3 -m unittest tools.tests.test_droiddeckctl` — 49 passed.
- `:app:testDebugUnitTest -x buildVkBridgeServer --tests
com.droiddeck.launcher.session.SessionLogShareZipTest.anArchiveCanBeWrittenOutsideTheShareFolder`
— passed.
- `git diff --check` — passed.
2026-11-05 15:57:27 -05:00
Kurt Himebauch 604a01d353 fix: map log destination errors 2026-11-05 15:41:47 -05:00
Kurt Himebauch e8ffe1164f fix: detect D3D12 capabilities and remove redundant game workarounds (#2134) 2026-11-05 15:37:46 -05:00
Kurt Himebauch 1ae37055c9 fix: preserve diagnostic staging failures 2026-11-05 15:30:21 -05:00
Kurt Himebauch a431c40b1b fix: preserve per-file diagnostic failures 2026-11-05 15:14:23 -05:00
Kurt Himebauch 285ff7956b test: cover independent agent log exports 2026-11-05 15:09:04 -05:00
Kurt Himebauch 64f114c96f fix: detect graphics capabilities and drop redundant title overrides 2026-11-05 15:03:08 -05:00
Kurt Himebauch 9f22f63222 test: verify independent log export paths 2026-11-05 14:59:37 -05:00
Kurt Himebauch 0bb8384c76 fix: avoid unsupported ray tracing on Stellar Blade startup 2026-11-05 14:53:15 -05:00
Kurt Himebauch 671c4a623f test: assert cleanup after failed log pull 2026-11-05 14:51:17 -05:00
Kurt Himebauch dfc0fd4340 fix: harden diagnostic staging lifecycle 2026-11-05 14:49:11 -05:00
Kurt Himebauch 11c0763fba fix: preserve log cleanup failures 2026-11-05 14:42:22 -05:00
Kurt Himebauch ff686d7c3b fix: expire stale log exports safely 2026-11-05 14:42:22 -05:00
Kurt Himebauch 4765644451 fix: harden log export transactions 2026-11-05 14:42:22 -05:00
Kurt Himebauch 718712eb01 fix: secure and refresh unrooted log exports 2026-11-05 14:42:22 -05:00
Kurt Himebauch ca0689d254 fix(steam): default listed titles with a bundled arm64 Linux build to FEX (#2126)
Refs #1774.

Maldhalla (4385780) ships its x64 and arm64 Linux builds side by side in
one depot. Its launcher starts the arm64 build when it runs on an Arm
host, natively or as an x86 shell under FEX. Steamworks offers no arm64
value for a launch option's CPU architecture, so the appinfo still reads
`oslist` linux+windows and `osarch` 64. `native_targets()` therefore
leaves the title on the Proton ARM64 default, which runs the Windows
build.

This PR adds `FEX_DEFAULT_APPS`, an explicit allowlist (the opt-in
suggested in #1774). A listed title defaults to `droiddeck-fex` instead
of `NATIVE_CHOICE`. Nothing else changes:
- A player's own pick is kept, as before.
- Unlisted titles keep today's behaviour, including the Factorio
native-choice case.

On the Snapdragon 8+ Gen 1 tablet from #1774, our benchmark scores 8–11
with the Windows build under Proton ARM64 and above 100 with the native
arm64 build.

Test:
`ProtonDefaultTest.test_a_listed_title_with_a_bundled_arm64_linux_build_defaults_to_fex`
in `tools/tests/test_game_environment.py`. It fails without the change.
`ProtonDefaultTest`, `test_droiddeck_proton_default`,
`test_steam_compat_mapping`, `test_appinfo_lookup` and
`test_steam_compat_live` pass.
2026-11-05 14:41:54 -05:00
phj1081 7e0859872b fix(steam): keep a cleared FEX default for a listed title as a native choice
A listed title whose automatic FEX mapping the player clears now records
the empty mapping as a native choice, as titles with NATIVE_CHOICE do,
instead of getting FEX again on the next run.
2026-11-05 14:22:15 -05:00
phj1081 28d4f58f33 fix(steam): default listed titles with a bundled arm64 Linux build to FEX
Steamworks has no arm64 launch-option architecture, so a title that ships
x64 and arm64 Linux builds side by side still reads as osarch 64 and gets
the Proton ARM64 default. List such titles in FEX_DEFAULT_APPS so they
default to Linux x86 (FEX), whose launch runs the bundled arm64 build. A
player's own pick is kept, and unlisted titles are unchanged.

Refs #1774
2026-11-05 14:22:15 -05:00
Kurt Himebauch ee0d8d9997 fix: guard session clock logger lifecycle 2026-11-05 14:00:30 -05:00
angelrenefm 5805631be5 Session logs: record clocks, thermal status and battery temperature in every session
ClockLogComponent was only created inside startVkBridge(), so it never ran
on Adreno. Create it for every session (clocks.log, was vkbridge-clocks.log)
and append the battery temperature from ACTION_BATTERY_CHANGED to each
sample.

Part 1 of #1837.
2026-11-05 14:00:30 -05:00
K baf7d90df9 build: drop the unused androidTest variant so the VS Code Kotlin import works
The project has no instrumented tests. The Kotlin extension's Gradle import
reads the debugAndroidTest outputs without running their tasks and fails.
2026-11-05 14:00:11 -05:00
K11MCH1 cb3c3cb2c9 Added games: a reused ranking is not remembered as a fresh walk
A ranking a scan took from added-games-scan.json was also put in the
60-second in-memory cache with the current time, so for the next minute
single(), addFolder() and the exe picker, which ask for a fresh walk, got
a ranking that could be a week old. Kept rankings are now returned without
entering that cache, so it only ever holds rankings from real walks.
2026-11-05 13:59:59 -05:00
K11MCH1 df60fdb194 Added games cache: skip bad records, expire names on read, retry failed saves
A record whose fields could not be decoded threw while the cache loaded,
and since every scan loads it first, one bad record blocked every scan;
each record is now decoded on its own and skipped if it is malformed.

name() returned names older than a week that were still in memory; it now
applies the same age limit as rank().

save() cleared its dirty flag before writing and called startWrite outside
its try, so a write that failed was never retried and could replace the
scan's result with an exception; a failed write now leaves the cache dirty
and is tried again on the next save.
2026-11-05 13:59:59 -05:00
K11MCH1 93923e125d Added games: one scan at a time, and keep what it found across launches
Every Steam session scans the added games before the Linux side starts,
walking each game folder for its exe and reading the exe's version info.
On an AYANEO Pocket FIT Elite with 40 games on an exFAT SD card that walk
took 10 s with a warm cache and up to 155 s after Android had dropped it,
and the front end's art pass and library load ran the same scan at the
same time, so the session waited on all of them.

Scans now run one at a time, so a waiting scan reuses what the one before
it just found. Each game folder's exe ranking and identified name are kept
in added-games-scan.json; a scan reuses a ranking while the folder's
modification time is unchanged, its first exe still exists and it is under
a week old, and walks the folder again otherwise. The exe picker, single
game refreshes and adding a folder still walk the folder fresh.
2026-11-05 13:59:59 -05:00
K11MCH1 fcbd45453f Steam imports: read only the linked manifest, and leave staged ones alone
seed_install read the app's manifest from every library before checking
that it was the linked DroidDeck install, so a manifest in another library
that is not UTF-8 raised past the caller and stopped routing for later
games. It now returns before reading anything but the linked manifest.

An untouched manifest from an older version was migrated even while
steamapps/downloading/<appid> existed; that migration now waits until
Steam is not staging the app, like the rewrite of a queued import.
2026-11-05 13:59:44 -05:00
K11MCH1 1ec1c20be1 Steam: imported games with no known build update only when launched
An added game the account owns is linked into Steam as DroidDeck-<appid>.
When the folder carries no Steam build, Steam marks it Update Required and
checks every file before updating. That check reads the whole game, and an
interrupted one starts over, so on an SD card it ran again in every session
and kept the card busy: Lords of the Fallen (33 GB) restarted 51 times in a
day on an AYANEO Pocket FIT Elite.

The manifest for such an import now asks Steam to update the game when it
is launched. Imports that Steam already rewrote, but whose update has not
started, switch from "always keep updated" to "update when launched"; a
priority of 2, an update in progress, or an import with a recorded build is
left alone.
2026-11-05 13:59:44 -05:00
Kurt Himebauch ccab14384d test: isolate unrooted log archive output 2026-11-05 13:52:43 -05:00
angelrenefm 8cc652e812 fix(droiddeckctl): address review on logs latest
Stream each archive entry to disk instead of reading it into memory,
refuse an archive from a session that started after the state call, and
delete the exported zip from shared storage once it has been pulled.
2026-11-05 13:52:43 -05:00
angelrenefm 2fb066507a fix(droiddeckctl): logs latest works without root
`logs latest` pulled the session folder under getFilesDir(), which adb
cannot read on an unrooted device, so it always failed with "Permission
denied" and droiddeck-scenario kept no logs for failed runs.

The agent bridge gets a `logs` method that writes the Share logs archive
to the app's external files folder (agent-logs/), and droiddeckctl pulls
that zip and unpacks it into the destination with the session's files at
the top, as before. Builds without the method still get the old pull.
2026-11-05 13:52:43 -05:00
Kurt Himebauch 4b6cba0a6d fix(l10n): mark Russian percent string unformatted 2026-11-05 13:49:20 -05:00
flexeykinDev 4f831fbd34 l10n(ru): translate strings added since the last translation pass
Adds the 83 Russian strings that showed in English: extra Steam libraries, session logs, dual screen launcher and sessions, PC keyboard options, the bottom screen desktop, screen swap, the stats HUD and the Mali bridge GPU info.
2026-11-05 13:49:20 -05:00
Kurt Himebauch a28ff05981 fix: handle enhanced log chooser launch failures 2026-11-05 13:32:46 -05:00
Kurt Himebauch 2ab8a1bac3 test: validate enhanced log share descriptor reads 2026-11-05 13:32:46 -05:00
wallentx 7053058db6 Harden enhanced diagnostics collection and sharing 2026-11-05 13:32:46 -05:00
wallentx 44f8d5de70 Check the matching diagnostic manifest entry 2026-11-05 13:32:46 -05:00
wallentx 3c6447528a Make secure log export testable under Robolectric 2026-11-05 13:32:46 -05:00
wallentx 5614bd5d5a Add enhanced one-session diagnostic bundles 2026-11-05 13:32:46 -05:00
KeegantheVeganat0r b563b8d1eb fix(hud): translate low FPS toggles in all supported locales 2026-11-05 12:38:55 -05:00
KeegantheVeganat0r 6b887c42af feat(hud): add independent 1% and 0.1% low toggles 2026-11-05 12:38:55 -05:00
Omar Al Taki 3d1f0f63b9 fix(input): release a held mouse Back only when its own device disconnects 2026-11-05 12:13:51 -05:00
Omar Al Taki aad60fef5b fix(input): keep the right button down until every overlapping mouse Back is released 2026-11-05 12:13:51 -05:00
Omar Al Taki db8c66b107 fix(input): keep mouse Back routed to screen-key text fields while editing them 2026-11-05 12:13:51 -05:00
Omar Al Taki abb28792e8 fix(input): release a mouse Back right click on focus loss and pass nav Back through
Address review: a navigation-bar Back that arrives while an injected mouse
Back is held now stays Back instead of releasing the right button early.
A held right button is released on pause, focus loss and input-device
removal, like the other held Back handling. While the session is
suspended or still loading, Back is no longer sent to the guest.
2026-11-05 12:13:51 -05:00
Omar Al Taki 57b3623a9a fix(input): mouse right click no longer acts as Back in REDMAGIC cast mode
REDMAGIC's cast mode replaces a mouse's right button with a Back key that
names no device and lacks the virtual-hard-key flag that the navigation
bar and back gesture set. During a session, a Back from a mouse, or that
injected form while a mouse is connected, now presses the guest's right
button. The navigation bar, back gesture and controller Back still act
as Back, and so does a right click while a DroidDeck menu is open.

A mouse's Back and Forward buttons no longer also send a left click;
Android delivers them as key events.
2026-11-05 12:13:51 -05:00
Anton bae4ecd867 fix: leave run-mode apps unpinned when all cores are selected 2026-11-05 12:13:45 -05:00
Boran Miao 17fb80ad51 test: quote emulator installer paths with spaces 2026-11-05 12:09:56 -05:00
Boran Miao 5e0f5cb9cd fix: install bundled PPSSPP through the Flatpak helper 2026-11-05 12:09:56 -05:00
HCOOKIELYT 0544933abd fix(steam): show progress while applying client updates
Forward extraction and installation messages from the x64 bootstrap log and replace stale download percentages with the applying-update state. Add regression coverage for phase ordering and log forwarding.

Fixes #812
2026-11-05 11:56:42 -05:00
Kurt Himebauch fe1dd8ca72 test(battlenet): close the folder scan case 2026-11-05 11:56:37 -05:00
visail 969b7e625f fix: list Battle.net games installed under Blizzard's China uids
Blizzard's China client records some products under a CN uid, so the installs
it makes never reach the Library and their Stores card keeps offering Install:

  Diablo II: Resurrected  osic      (catalogue had osi)
  StarCraft II            s2_cn     (catalogue had s2)
  Diablo Immortal         anbs_cn   (catalogue had anbs)

title() now resolves those uids to the international title, so the Library, the
Stores "installed" state, the cover folder and the launch code all agree, while
Game.installUid keeps the uid the Agent recorded for updates and uninstalls.
TITLES and STORE are untouched.
2026-11-05 11:56:37 -05:00
sanogueralorenzo 1c24c600cd refactor: clarify adaptive stick hit testing and test setup 2026-11-05 11:43:46 -05:00
sanogueralorenzo d8c1bc6059 fix: extend left adaptive stick activation to the screen edge 2026-11-05 11:43:46 -05:00
uubooster-team e1fe6f99cf fix(network): handle gatewayless IPv4 VPN links
Provide compatible gateway metadata for IPv4 VPN links without changing
VPN routing, and keep text and JSON network snapshots consistent.

Add regression tests for gateway selection and network snapshots.
2026-11-05 11:43:30 -05:00
coffeemeist3r ef305c93cd fix(mesa): Steam's UI no longer crashes when scrolling the Home shelf fast on PowerVR
Zink's rebind_image() and zink_rebind_all_images() refreshed only a sampler
view's main image view after the resource got a new backing object. The
view's zs_view and cube_array surfaces stayed on the old object and were
freed with it, so the next descriptor update wrote a garbage VkImageView.

On PowerVR (needs_zs_shader_swizzle) every RGBX/BGRX view has a zs_view, and
Steam's web helper segfaulted in vkUpdateDescriptorSetWithTemplate within
seconds of fast Right presses on the Home shelf. The new Mesa patch rebuilds
all of the view's surfaces with init_sampler_view().
2026-11-05 11:28:51 -05:00
Systina12 f6f26d03f9 fix: quote GPU environment values before writing shell exports 2026-11-05 11:18:29 -05:00
dddms e72cddccd9 perf(steam): decode requested appinfo records only
Avoid decoding every app when Start without Steam resolves one game launch. Use wanted-app selection in the single-game helpers while keeping the existing field decoder and full-reader default for bulk callers.

Track partial coverage separately from complete results, share one immutable byte snapshot for incremental queries, and keep timestamp invalidation and warm hits. Preserve the last valid duplicate record and release the raw snapshot after a full decode.

Add format/cache/structural-budget and launch-resolution regressions, reuse the existing binary fixture encoder, and document the host-only performance scope.
2026-11-05 11:17:03 -05:00
vitamin663-sys 92a588ea16 fix(input): add radial stick dead zone setting 2026-11-05 10:53:15 -05:00
cphillamandmaxjivi05 e879375ca4 fix: upscale game buffers before native-resolution composition (#1367)
* fix: upscale game buffers before native-resolution composition

* fix: drop an added code comment from the effects source draw

* fix: effects source keeps pillarboxed games and near-full source rectangles

* fix: a larger opaque draw above keeps the full composition path

---------

Co-authored-by: maxjivi05 <[email protected]>
2026-11-05 10:34:19 -05:00
Kurt Himebauch 7c5bf77f17 fix: fit output after pip resize 2026-11-05 10:18:37 -05:00
visail a0a7a66470 fix: use Blizzard's China hosts for Battle.net installs and update checks
The Agent's install request pointed at http://cn.patch.battle.net:1119/<product>.
That host only 301-redirects to the country's version service today; Blizzard's
China client asks cn.patch.battlenet.com.cn:1119 directly.

The update check built <branch>.version.battle.net, so a China install (branch
cn) asked cn.version.battle.net, which does not answer; the China service is
cn.version.battlenet.com.cn.

Only region cn changes.
2026-11-05 09:56:40 -05:00
kmnkv1 26c562c174 fix(stores): moving through the side menu no longer freezes the launcher (#2105) 2026-11-05 09:44:45 -05:00
Kurt Himebauch e1c2b0e340 ci: trust ASEbrahim for signed test builds (#2124) 2026-11-05 09:38:38 -05:00
MaxsTechReview d4fe440b49 docs: README install step links the current APK (#2122) 2026-11-05 08:43:24 -05:00
MaxsTechReview 4c23092c55 fix(components): repaired components reach prefixes that already have them (#2121)
fix(components): the library sweep refreshes the catalog when it is safe to

A game job reads the cached catalog, and the installed check compares against
the cached revision, so a repaired component never reached a prefix that
already had it. The sweep now fetches the catalog first when online and not on
a metered or constrained connection.
2026-11-05 08:43:21 -05:00
MaxsTechReview a309428a2a feat(battlenet): import, list and delete World of Warcraft addons (#2111)
World of Warcraft, Classic and Forever get an Addons tab in the Library. It imports addon zips into the right Interface/AddOns folder, lists the installed addons, and deletes one after a confirmation.
2026-11-05 02:04:03 -05:00
MaxsTechReview b9dbd006be fix(storage): Windows installs on a natively mounted card no longer ask for an administrator (#2100)
* fix(storage): Windows installs on a natively mounted card no longer ask for an administrator

Cards mounted directly as FAT, exFAT or NTFS were not treated as Android
storage, so wineserver reported an Anonymous owner and refused permission
changes. Its chmod on these volumes also no longer fails the request.

* fix(storage): keep NTFS owners and the chmod error
2026-11-05 00:47:38 -05:00
Kurt Himebauch a9d2d09e64 fix: restore single-screen session menu controls (#2110) 2026-11-04 22:15:03 -05:00
Kurt Himebauch 516e3862fa fix: refresh repaired components and render Ubisoft Connect (#2090)
* fix: refresh repaired components and render Ubisoft Connect

* fix: address component repair and Wine registry review

* fix: repair manually enabled components and check registry writes

* fix: serialize component repairs and use Proton runinprefix

* fix: report queued component repair failures

* fix: preserve newer component repair waits

* Make component queue transitions atomic

* Isolate component apply failures by game

* Preserve pending repair when catalog is stale

* Restore persisted component repair revisions

* Update pending requirements atomically

* Respect disabled pending components per game

* Keep queue key state atomic while worker waits

* Pass selected game to component repairs

* Preserve pending repair when component is absent

* Preserve pending revision on catalog failure

* Test legacy pending repair revision recovery

* Isolate fake worker Steam app info lookup

* Merge queued Steam app info lookups

* Persist launch component revisions before repair

* Gate launch on component repair discovery

* Expand and refresh launch repair discovery

* Include new automatic picks in launch requirements

* Log repair persistence failures and align repair keys

* Use runtime key for Battle.net component repairs

* Retain failed component reapplications for retry

* Harden pending repairs and launch preparation

* Isolate wait-cool sleep mocking

* Allow launches without catalog when no repair is pending

* Queue launch repairs even if persistence fails

* Block launch when repair state cannot persist

* Allow launch when no component repair is needed

* Persist selected repairs when catalog is offline

* Drain offline component repair in test
2026-11-04 21:54:00 -05:00
K11MCH1 1ee5e924b6 Merge remote-tracking branch 'upstream/main' into fix/gamescope-p20
# Conflicts:
#	tools/gamescope/release.env
2026-11-05 08:23:28 +09:00
MaxsTechReview ab92033b90 fix(desktop): Steam's own header moves and resizes its windows (#2097)
* fix(desktop): Steam's own header moves and resizes its windows

Steam never asks KWin to move or resize its windows, and its maximize
button can only maximize. Each normal Steam window now reports presses
on its header and edges, and KWin handles them natively. No extra title
bar is added and other apps are unchanged.

* fix(desktop): drag regions cover the elements inside them

* fix(desktop): find Steam's drag regions by position

The Friends window's drag region ignores pointer events, so a hit test
never finds it. The check now runs once a press moves, so plain clicks
cost nothing.
2026-11-04 17:47:00 -05:00
Kurt Himebauch d62f017bce Add session log management to setup diagnostics (#2099)
* feat: manage session logs in setup

* fix: address session log review feedback

* fix: honor device date and time settings
2026-11-04 16:05:56 -05:00
Delete228 68b0396dfe Merge upstream PC input and dual-screen updates 2026-11-05 00:00:34 +03:00
MaxsTechReview f005241549 fix(desktop): download language packs from the release that has them (#2096)
* fix(desktop): download language packs from the release that has them

The Ubuntu runtime's release has no language packs or index, so the
desktop stayed in English for every language.

* fix(desktop): reinstall language packs when their release changes
2026-11-04 15:09:30 -05:00
MaxsTechReview bba40cd69c fix(desktop): a game's window follows its own window mode (#2094)
* fix(desktop): a game's window follows its own window mode

Desktop games ran in a titled gamescope window whatever mode they asked for. gamescope now fills the desktop for a fullscreen or borderless game and matches the window size of a windowed one.

* Use the p30 gamescope test build

* fix(desktop): a windowed game too big for the desktop is maximised

* fix(desktop): a decorated game window stays a window

* Use the published gamescope p21
2026-11-04 14:59:39 -05:00
Kurt Himebauch f3d6efbcfa docs: update download badge to 0.6.9 2026-11-04 14:14:07 -05:00
MaxsTechReview f1c1ca59fe fix(battlenet): use the right artwork for each game (#2092)
* fix(battlenet): use the right artwork for each game

World of Warcraft: Forever took retail art and its logo from a loose name lookup, and only fetched its own art when the Store tab was opened. Shop pages also supplied trailer stills and seasonal promos.

Each game now has chosen Blizzard key art with the shop page as a fallback, installed games fetch their art without opening the store, and lookups by name must match exactly. Art found by the old lookup is fetched again once.

* fix(battlenet): keep cached art if the new art cannot be fetched

Classic's variants fetch through the shared title, and a failing chosen image falls back to the shop page.
2026-11-04 14:12:16 -05:00
5cf69732ef fix(input): make a hardware mouse and keyboard work like a PC (#2053)
* feat(gamescope): a three way switch for relative mouse motion

Games that confine the cursor and warp it back to read movement never
reach the nested hints rule, so their aim stays bounded by the screen.
The convar defaults to 0 and leaves that rule alone; 1 forces relative
motion for the focused game, 2 keeps the pointer absolute even when the
rule would apply.

(cherry picked from commit db3d7555e0)

* feat(input): the session carries a relative mouse mode per game

Reads the mode from the game's gesture config, hands it to gamescope as
gamescope_relative_mouse, and pushes a change to the running session
through gamescopectl.

(cherry picked from commit e350d88464)

* feat(drawer): the relative mouse switch is a three way choice

Auto leaves it to the game, on forces relative motion, off keeps the
pointer absolute. Named in all 32 locales so none of the three falls
back to English.

(cherry picked from commit e96a187e02)

* fix(input): follow the focused game's relative mouse mode

Switching to a game whose saved mode differs left the session on the
previous game's mode, since only the drawer state was refreshed. The
requests also ran on a thread per selection, so two quick picks could
reach gamescope out of order.

(cherry picked from commit 7ff8c5d568)

* fix(input): retry the relative mouse mode gamescope did not take

The drawer state moved before the request ran, so a failed request left
the two out of step and the next focus change skipped the resend.

(cherry picked from commit cf0f82d8cd)

* fix(input): the relative mouse mode tracks the latest choice

The guest agent answers ok when it handled the exec, so a failing
gamescopectl was recorded as applied. Reconcile the newest desired mode
on the worker thread after each request, and only mark applied when the
command really exited zero.

(cherry picked from commit 5f5e3bd52e)

* gamescope: a hidden cursor under any pointer constraint is relative

Games that hide the cursor and confine the pointer had no lock hint, so the
nested pointer stayed absolute and the camera froze.

* Treat a hardware mouse as a real mouse in sessions

A mouse now moves the guest pointer by relative motion for the whole session
through pointer capture, so it no longer stops at screen edges or reaches
system gestures. Its cursor stays visible in every touch mode, follows the
game showing or hiding it, and clicks no longer press on-screen controls.

* gamescope: use the mouse test release

* Direct touch turns the camera while a game holds the pointer

A swipe in direct touch now sends relative motion when a game has the pointer
locked, including when the lock starts during the swipe, as when a game
hides the cursor while a button is held.

* Address review findings on mouse and touch input

Keep touchpad taps on on-screen controls, cancel a pending long press during a
locked swipe, let the three-finger gesture cancel a locked swipe, refresh
capture when the output changes, and use the menu's key for the relative
mouse setting.

* Keep locked touches inside the game picture and free the mouse in touch editors

* End touch editor ownership when the editor goes away

* Bring the mouse cursor back when a touch editor closes

* Release a pointer lock at the game's pointer

* gamescope: use the pointer position test release

* Fit the X screen to a fullscreen window larger than it

* gamescope: use the X screen fit test release

* Return the X screen when no game window has focus

* gamescope: use the X screen return test release

* ci: avoid redundant gamescope builds

* fix: hardware mouse in Desktop and pointer warping games

KWin nested on the host only reads relative motion while its own pointer lock is on, so a program that locked the pointer in Desktop (Blender orbit and pan) stopped receiving motion. A captured hardware mouse now turns that lock on, and a real Right Ctrl no longer toggles it.

Gamescope follows a game's pointer warps instead of overriding them, and absolute motion also sends raw motion. Also retries relative mouse requests, tidies the unplug path, passes side buttons and fits a second oversized window correctly.

* chore: use the gamescope build with patch 0131

* fix: Desktop mouse lock keeps retrying after early attempts fail

* fix: hold the key that turns on the Desktop mouse lock

KWin ignores a press and release delivered together.

* fix: gamescope enters warp following sooner and keeps steady motion

* chore: use gamescope p26

* Capture system keys in a focused session and keep the first Desktop mouse motion in place

* Use the p27 gamescope build

* Update the system keys row test

* Recognise a captured mouse so the Desktop grab can start

* Keep touch warps out of relative motion

* Send Esc from keyboards that report it as Back

* Use the p28 gamescope build

* Release captured keys on pause, forward motion during a grab and wait for a pending screen fit

* Use the p29 gamescope build

* fix(input): keep held system keys consumed until they are released

Copy each captured key event, since the service owns the original only for the callback, and consume the physical release of a key already released to the guest.

Use the published gamescope p20.

* fix(input): capture a system key only from its first press

---------

Co-authored-by: kingokksa <[email protected]>
Co-authored-by: maxjivi05 <[email protected]>
2026-11-04 13:27:08 -05:00
Kurt Himebauch eed9edaa1c feat: consistent bottom screen bar and menu-first navigation in sessions (#2084) 2026-11-04 13:26:35 -05:00
MaxsTechReviewandmaxjivi05 940efdab3a Reduce DXVK render-path pacing stalls on Adreno 722 (#2066)
Co-authored-by: maxjivi05 <[email protected]>
2026-11-04 12:44:26 -05:00
MaxsTechReviewandAlbert b7878d2841 fix(battlenet): keep games installed inside Battle.net's folder running (#2087)
fix(battlenet): keep games installed inside Battle.net's folder running

A game installed under C:\Program Files (x86)\Battle.net\ (for example
World of Warcraft: Forever in "Battle.net\World of Warcraft\_forever_")
was counted as part of Battle.net itself. Launched from DroidDeck, the
launch never saw a game, said "Battle.net closed before a game started",
stopped everything left in the prefix, game included, and exited 1.

Battle.net's own files are now only those directly in its folder, in its
Battle.net.<build> folders and under ProgramData\Battle.net.

Co-authored-by: Albert <[email protected]>
2026-11-04 12:13:34 -05:00
Kurt Himebauch 430af5e22d feat: desktop and scrolling wall live wallpapers (#2012)
* feat: desktop and scrolling wall live wallpapers selectable from Android settings

* fix: render the wall wallpaper off the main thread and give it its own thumbnail

* fix: serialize wall wallpaper visibility and wait out in-flight draws on surface destroy

* fix: restart the wall loop on surface recreation and keep queued visibility changes

* fix: keep the wall loop alive across a failed canvas lock and finish redraws before returning

* fix: post the wall redraw asynchronously on surface change

* fix: redraw the desktop wallpaper logo from the real mark and show game covers on the wall

* chore: mount the git dir in build containers and seed new worktrees from the main checkout

* fix: decode wall covers as RGB_565 and seed worktree assets only into empty dirs

* feat: drop the desktop wallpaper and add frame rate and speed settings to the wall

* feat: wall settings over the live wallpaper with a match-screen rate, and drop the center vignette

* perf: pace the wall on vsync and draw it on a hardware canvas

* fix: stage worktree seed copies and publish them only after they succeed

* fix: point the wall settings link at the activity class so the picker shows the gear

* fix: dedupe wall covers by app and space repeats like the in-app wall, keep the frame loop alive across surface gaps

* fix: show the picker preview behind wall settings and drop the display-rate lookup

* fix: stop wall frame callbacks when the surface goes and only replace an empty seed destination

* fix: skip a seed when the destination reappears before the staged copy is moved in
2026-11-04 11:38:54 -05:00
Moroni Granja 0110dc3c0f fix: a GOG install launches the game, not its config tool (#1644)
Blood Omen ships dxconfig.exe beside kain.exe and the shortcut pointed at
the config tool: pickExe kept the first .exe the folder listing returned
when no title match was found. Read the game's play task from GOG's own
goggame-<id>.info manifest first - a primary game task, or a primary
launcher task for launcher games, and the product's manifest ahead of any
other goggame-*.info - then the product's temp_executable; without either,
rank the folder's exes so a word of the title wins, config and tool names
lose, and depth then size break the remaining ties.

GOG's galaxy_*.exe support installer is not the game either. The Witcher:
Enhanced Edition ships galaxy_the_witcher_enhanced_edition_2.1.0.15.exe
beside launcher.exe, and its build metadata names that file as the
product's temp executable, so launching it starts a 32-bit installer that
stops at "Path not found" under ARM64 FEX - the same dialog as the setup
stub #1174 removed. Play tasks, the hint and the name search skip it.

Fixes #680
Fixes #694

Only a primary launcher task counts as a launch task: GOG files the
configuration tool beside a game as a non-primary launcher, so it now falls
through to the executable search instead of becoming the game.

The temp_executable hint is matched by basename with either separator and
must resolve inside the install folder, so a nested galaxy_*.exe is skipped
too and a hint cannot point outside the game.

GOG runs a task from the working directory it declares, and from the install
root when it declares none. That directory now reaches the sidecar and the
shortcut's Start In: a game in a subfolder (System/witcher.exe) starts where
GOG starts it instead of in System/, where it cannot find its Data folder.
2026-11-04 11:38:02 -05:00
MaxsTechReview d849e6ae0a fix: Home screen setting keeps DroidDeck in RedMagic Game Space (#2083) 2026-11-04 09:06:37 -05:00
MaxsTechReviewandmaxjivi05 26274114a3 ci: avoid redundant gamescope builds (#2070)
Co-authored-by: maxjivi05 <[email protected]>
2026-11-04 00:38:17 -05:00
Kurt Himebauch 45fb017496 perf(steam): batch compatibility mapping updates (#1755) 2026-11-03 22:01:41 -05:00
Kurt Himebauch a2a05ebc5b fix: preserve the runtime PATH when overriding guest binaries (#1826) 2026-11-03 22:00:41 -05:00
Kurt Himebauch ad0d39f457 fix(quest): gamescope no longer aborts at start with vk.WaitSemaphores -13 (#1938) 2026-11-03 21:58:49 -05:00
Kurt Himebauch 1915e16fe1 fix: controller focus moves into an opened folder (#1962) 2026-11-03 21:56:57 -05:00
Kurt Himebauch c1b8f4c706 fix(hud): show the GPU clock in the Steam overlay on Adreno without root (#2059) 2026-11-03 21:54:55 -05:00
Kurt Himebauch 306cd789df feat(droiddeckctl): thermal and wait cool (#2062) 2026-11-03 21:54:29 -05:00
angelrenefm 8e480d0e84 feat(droiddeckctl): thermal and wait cool
Nothing in droiddeckctl reported temperature, so a before/after
comparison could start one run hot and the next cold.

`thermal` reads the battery temperature from dumpsys battery, the
thermal status from dumpsys thermalservice, and each CPU policy's
scaling_max_freq against cpuinfo_max_freq (capped when lower), all
readable over plain adb. `wait cool --below <C>` polls it every 10 s
until the battery is under that temperature, or exits 5 at --timeout.
2026-11-04 00:13:31 +01:00
angelrenefm 3ad13b1b66 fix(hud): bind the GPU clock and load nodes even when the HUD starts hidden
With the overlay hidden at session start the blocked feeds have no value,
so cur_freq and perf_now were never written before the binds were taken
and stayed unbound for the whole session.
2026-11-04 00:07:13 +01:00
angelrenefm 7901e76561 fix(hud): see the Steam session's mangoapp config when deciding whether the HUD is shown
Steam sessions point mangoapp at /tmp/droiddeck-mangohud.conf, so
OverlayShown never saw the overlay and the GPU clock stayed at 0 MHz.
2026-11-04 00:04:30 +01:00
MaxsTechReview f3650d3d37 Reduce on-screen stick load and honour Low latency off (#2048)
* Reduce on-screen stick load and honour Low latency off

On-screen controls requested unbuffered touch on every press, so turning
Low latency off had no effect on them. They now follow the setting.

Every stick move also redrew the whole overlay. Redraws during movement are
capped at 30 per second in both overlays, and stick moves under a pixel
are dropped in the standard overlay.

* Keep full stick deflection reachable

Moves under a pixel were dropped even at the edge of the stick, so the pad
could stop just short of full range.
2026-11-03 16:12:08 -05:00
Kurt Himebauch ad90d9e200 fix: find Steam libraries whose installs have no appmanifest (#2054) 2026-11-03 15:56:12 -05:00
Kurt Himebauch a9f365851e fix: fall back to completed manifests when depot.config lists none 2026-11-03 15:14:58 -05:00
Kurt Himebauch 4cc117b45b fix: read DepotDownloader's compressed protobuf depot.config 2026-11-03 15:05:04 -05:00
Kurt Himebauch 0cc187025f fix: read installed depots from completed manifest names too 2026-11-03 14:54:33 -05:00
Kurt Himebauch be5f1560b2 fix: find Steam libraries whose installs have no appmanifest 2026-11-03 14:39:45 -05:00
Kurt Himebauch 9f6c73442b fix(quest): answer denied KGSL timestamp reads so Steam starts on Quest 3 (#2050) 2026-11-03 13:06:57 -05:00
Daniel Noam 627aea0e8f Merge main 2026-11-03 19:32:54 +02:00
Kurt Himebauch edbe0c7179 fix(quest): drop a KGSL context's tracking before its destroy ioctl and restore it on failure 2026-11-03 10:55:54 -05:00
Kurt Himebauch ccd317384d fix(quest): ignore a finished wait when its KGSL context was recreated 2026-11-03 10:30:50 -05:00
Kurt Himebauch 9388ef07d8 fix(quest): grow the timestamp table instead of evicting an active context 2026-11-03 10:26:24 -05:00
Kurt Himebauch b7e550ae89 fix(quest): keep active contexts tracked and answer only queued and retired reads 2026-11-03 10:20:49 -05:00
Kurt Himebauch 4e571f1f99 fix(quest): answer denied KGSL timestamp reads so Steam starts on Quest 3 2026-11-03 10:09:44 -05:00
K11MCH1 5766696555 gamescope: ship p20 so the EA sign-in stays above the launcher splash
Patch 0126 (#2037) is in the source tree, but the app still stages the
p19 binary pinned here, built on Oct 29 before the patch existed. A fresh
install keeps drawing the EA Steam Launcher splash over the EA sign-in.

p20 is the gamescope-patched archive from the main build of 951270bc
(Droid-Deck/DroidDeck actions run 41637838020): patches 0002-0126
applied, gamescope tests Ok 2 / Fail 0.
2026-11-03 22:41:58 +09:00
K11MCH1 951270bc8f gamescope: EA sign-in stays above the EA launcher's splash (#2037)
* gamescope: EA sign-in stays above the EA launcher's splash

The EA app's Steam launcher shows the game's splash as a layered tool
window, and the EA app, another process, opens its sign-in window
topmost. gamescope took the splash as the sign-in's override window, so
the splash was painted over the sign-in and became gamescope's keyboard
focus window (Burnout Paradise Remastered). With patch 0126 a window from
another process that is neither topmost nor transient for a topmost
focus window is no longer an override candidate for it, as on Windows.
map_win now also reads _WINE_HWND_EXSTYLE: gamescope only took it from a
PropertyNotify, which it listens for from map on, so a style Wine set
before mapping was never known.

* gamescope: the topmost rule needs both windows' pids

gamescope keeps a window's pid at -1 when XRes cannot name its client,
so an unknown pid next to a known one is not evidence of two processes.
Patch 0126 now applies the rule only when both pids are known. The patch
carries function names in its hunk headers, and the test also checks
that its map_win hunk reads _WINE_HWND_EXSTYLE into hasHwndStyleEx (a
check of the patch text, not of a running gamescope).
2026-11-03 07:41:24 -05:00
K11MCH1 b7b08a40e8 Steam: EA games install the EA app (#2036)
The redistributable seeder marks every process in a game's install scripts
as already run, except Vulkan runtime and Ubisoft Connect installers. EA
games on Steam list the EA app installer (EADesktopSetup) there and start
through steam2ea://, which only the EA app handles. With the installer
skipped, Proton could not open that link (ret 31) and the game closed
without a window (Burnout Paradise Remastered). The EA app installer is
now left to run like Ubisoft Connect's, and a marker an earlier seeding
wrote is taken back out, so an existing prefix installs it on its next
launch.
2026-11-03 07:41:01 -05:00
Kurt Himebauch 8eeea5ef5a feat: add size, opacity and hidden-key settings for the PC keyboard (#1897) 2026-11-03 07:01:33 -05:00
Kurt Himebauch 61cbf91150 fix: the navigation bar stays dark when the launcher is not fullscreen (#1975) 2026-11-03 06:58:51 -05:00
Kurt Himebauch 3dff7d3962 fix: Project Zomboid runs on G1 and the C1 JIT instead of ZGC (#2016) 2026-11-03 06:56:20 -05:00
MaxsTechReview 2b790a8a71 fix(runtime): keep the Linux desktop installed across a runtime update (#2021)
* fix(runtime): keep the Linux desktop installed across a runtime update

A runtime update replaces the whole runtime tree, and the desktop pack
is built against one runtime, so every update removed the installed
desktop. Since the runtime started updating by itself, this happened
without the user doing anything: the next Open showed "Install & open"
and downloaded the whole desktop again.

When the desktop was installed before the update, the same update job
now installs the new runtime's desktop pack right after the runtime,
with the update's progress. If that install fails, the runtime update
still counts as done and Open offers the install as before. A repair
from the session no longer installs the desktop a second time when the
runtime install already did.

* fix(runtime): roll back a failed desktop restore after a runtime update

If putting the desktop back fails after its archive started unpacking,
the install journal stayed behind, and every later launch, Steam
included, tried to resume it and stopped when that failed. The update
now rolls back the partial desktop with the desktop removal, which
restores the runtime's own files and clears the journal. Open then
offers the desktop install as before.

Whether the desktop was installed is now checked on its own, so a
repair of a damaged runtime also restores it.

* fix(session): do not install the desktop again after resuming it

Opening the desktop with an interrupted desktop install resumed that
install and then installed the desktop a second time. The second
install is now skipped when the resume already completed the desktop.
2026-11-02 21:20:18 -05:00
Kurt Himebauch 4b8f63d84d feat: Mali Vulkan support through MaliBridge (#2027) 2026-11-02 20:23:21 -05:00
Kurt Himebauch 3dcb3eac42 chore: revert the Mali Vulkan squash to re-land it with history (#2026) 2026-11-02 20:22:29 -05:00
张先生 1f91283f40 feat: Mali Vulkan support through MaliBridge (#1950) 2026-11-02 20:16:48 -05:00
LingWindT c43a493ad8 preload: set PR_SET_CHILD_SUBREAPER so double-forked Xwayland is not orphaned to init (#1716) 2026-11-02 20:00:31 -05:00
Kurt Himebauch 80da86a5d3 fix: resolver order is active DNS, discovered DNS, public, then gateways 2026-11-02 19:58:58 -05:00
Kurt Himebauch ece3fec8ba fix: the active network's first three DNS servers stay ahead of public fallbacks 2026-11-02 19:43:57 -05:00
Kurt Himebauch 3a4c6a4a7a fix: DNS gateway pick and public fallback slot, drop reference to an unshipped script 2026-11-02 18:56:45 -05:00
Kurt Himebauch 0a0bbb68d1 fix: a stale session's startup failure no longer stops the newer session (#1984)
* fix: a stale session's startup failure no longer stops the newer session

* fix: stale session failures and queued-launch cleanup are checked against the current session under the stop lock
2026-11-02 18:44:12 -05:00
Kurt Himebauch 34a1cbbead fix: keep PRoot out of the APK, VPN DNS first, bridge failure exits, HUD polling stays on 2026-11-02 18:17:00 -05:00
Kurt Himebauch f05e0df39e Merge remote-tracking branch 'origin/main' into review/malibridge-1950 2026-11-02 18:14:00 -05:00
Moroni Granja cdde9d3390 fix: a GOG game whose installer cannot run gets its install registry (#2022) 2026-11-02 18:06:08 -05:00
Kurt Himebauch cae095ce30 fix: download monitor polls off the main thread so slow storage can't ANR the session (#2020)
* fix: download monitor polls off the main thread so slow storage can't ANR the session

* fix: cancel the queued download poll before the service shuts its executor down

* fix: ignore download monitor submissions that arrive after the service shut it down
2026-11-02 17:53:02 -05:00
Kurt Himebauch 5fe3021e5e feat: suggest Steam libraries found on internal storage and cards (#2023)
* feat: suggest Steam libraries found on internal storage and cards

* fix: add found card libraries by their readable path and drop superseded scan results
2026-11-02 17:40:55 -05:00
MaxsTechReview bc5edb81db fix(battlenet): list World of Warcraft: Forever in the Library (#2018)
Forever was missing for two reasons. Its install was not a known title,
so the scan skipped it as "not listed". And every WoW version installs
into the same World of Warcraft folder, so once one version was listed
the Library dropped the others as the same game.

- Forever is a title: launch code WoWF, product wowf, folder _forever_.
- An install whose uid is not known is matched by the product code
  product.db records for it.
- When versions share the folder, the one listed first keeps it and the
  others use their own version folder (_forever_, _classic_, ...). Each
  title's folder is remembered, so a shortcut keeps its app id when
  another version is installed or removed.

Games installed before this update are listed on the next Library scan.
They do not need to be installed again.
2026-11-02 17:05:16 -05:00
The412Banner 8d3616eccd feat(dual-screen): swap screens - game on the bottom screen, panel on top (#2006)
Swap screens for dual-screen handhelds: the game moves to the built-in
bottom screen while the top screen shows the Dashboard or the KDE desktop.
Swap button in the Dashboard and Desktop bars and a Show the game on
row in the session menu. The desktop resizes to the screen it is on,
Follow screen and Match screen sessions take the bottom screen's shape,
the keyboard opens on the top screen, and the game swaps back on session
end, picture-in-picture, glasses, or when the bottom screen is taken.
Review follow-ups cover desktop restart, refresh pacing, key and pointer
ownership, touch cancellation and bottom surface recovery.
2026-11-02 21:43:22 +00:00
Kurt Himebauch d2d5540ff1 fix: Project Zomboid JVM rewrite drops other collectors and covers Windows vmArgs 2026-11-02 14:32:36 -05:00
Kurt Himebauch 3fbd86c89d fix: bottom screen frame graph updates smoothly and no longer looks jagged (#2013) 2026-11-02 14:29:39 -05:00
Kurt Himebauch ce3f31a48c fix: Project Zomboid runs on G1 and the C1 JIT instead of ZGC 2026-11-02 14:20:41 -05:00
Kurt Himebauch 89a2ae87d7 fix: drop the HUD watermark and use the wordmark in headers (#2011) 2026-11-02 13:39:53 -05:00
Daniel Noam 730acbeeb7 Merge main 2026-11-02 18:48:59 +02:00
FX1922 3583643a11 feat: Add the Game Jolt store (#1986) 2026-11-02 10:05:30 -05:00
Kurt Himebauch 0050b5eb6b fix: docked HUD tab FPS number sits between the old and halved sizes (#2003) 2026-11-02 09:23:22 -05:00
Kurt Himebauch b16b67e6c4 feat: desktop session follows the app language (#1971) 2026-11-02 09:08:48 -05:00
Kurt Himebauch 55362d68ef fix: wall scrims overhang the window so no edge pixels stay unshaded (#1994) 2026-11-02 07:03:20 -05:00
Kurt Himebauch 2bc6985ae3 feat: choose Steam or Desktop to open when DroidDeck starts (#1993) 2026-11-02 07:03:02 -05:00
The412Banner 55da0afd06 fix(hud): GPU clock and load without root on Adreno 8xx and hardware-scheduled kernels (#1981)
On phones where Android blocks the GPU's sysfs files for apps, the HUD
reads the GPU without root (#1969). On Adreno 8xx that showed no GPU
clock and only the partial load from Vulkan timing (OnePlus 13, Adreno
830, reported by iamTooShaar).

- GPU clock: the clock job used the Adreno 6xx/7xx registers. The
  register layout is now picked from the KGSL chip id: on 8xx
  CP_ALWAYS_ON_COUNTER is 0x8e7 and the RBBM cycle countable is 1.
  Unknown later generations read nothing.
- GPU load: hardware-scheduled kgsl kernels (every 8xx, and some 7xx
  such as the AYANEO Pocket FIT's) never write the memstore ringbuffer
  timestamps. When those stay 0, the reader samples the contexts' own
  memstore entries (every context that has submitted work, up to 64,
  contexts with new or running work first).
- HUD diagnostics report: a "GPU readers without root" line with the
  reason each reader works or fails, "none" instead of "null", and the
  per-core CPU % source named honestly.

Tested: iamTooShaar's OnePlus 13 shows GPU clock and load with the PR
test build (06be2c5). Pocket FIT standalone build of the final readers
against the kernel's sysfs numbers: idle 1-2 % vs 3-4 %, GPU job loop
25-29 % vs 28-30 %, clock 1050 MHz vs 1050 MHz. The review fixes after
the test build were not tested on the OnePlus 13.
2026-11-02 10:07:26 +00:00
siyuan.qsy 2fdc8b0841 docs: preserve unrelated settings when recovering guest PATH 2026-11-01 23:35:27 -08:00
MaxsTechReviewandKurt Himebauch 313b6a59d2 feat(dual-screen): single-screen mode on the Retroid Pocket Duo and AYN Thor while another window holds the bottom screen (#1979)
* feat(dual-screen): step aside on the Retroid Pocket Duo's bottom screen while another window holds it

When the Duo's double-Home performance panel or another app (Chrome, YouTube) is on the bottom screen, DroidDeck switches to single-screen mode: the launcher's bottom panel and the in-game bottom controls close and the top screen takes its single-screen layout. When the bottom screen is back on its home screen, dual-screen mode returns, with the in-game controls reopening in the mode they had.

The DroidDeck shortcuts accessibility service watches the bottom screen's windows on the Duo only. An app opened under DroidDeck's panel is caught by its window opening on that display; the Retroid panel and any app on top are read from the window list.

* fix(dual-screen): log the bottom screen's holders only when they change

* fix(dual-screen): the topmost window other than DroidDeck's decides who holds the bottom screen

Apps opened under DroidDeck's bottom panel are still listed beneath it, so DroidDeck's own windows are skipped and the next full-size window decides. Fallback home screens such as Settings' boot-time one no longer count as a home screen.

* fix(dual-screen): catch apps opened under the in-game bottom controls

The in-game bottom controls hide the windows beneath them from the window list, so an app opening a window on the bottom screen while only DroidDeck is listed now holds the screen until the list can see it.

* fix(dual-screen): keyboards never hold the bottom screen; watch the built-in panel only

Keyboard windows opening on the bottom screen no longer count as an app arriving, so typing with the in-game bottom keyboard keeps the controls up. Only the built-in bottom panel is watched, so an external display can't close the controls. An arrival recorded while a check runs is kept for the next check, and stopping the watcher clears the held state after any running check.

* feat(dual-screen): step aside on the AYN Thor's bottom screen too

The bottom-screen watch now runs on every handheld in DualScreen's known dual-screen models, the Retroid Pocket Duo and the AYN Thor, including model names that extend them such as AYN Thor Max.

* fix(dual-screen): only the current bottom-screen watch publishes; Thor variants match when the display type is hidden

* fix: session drawer top row scrolls and Stop stays an icon-only button

* fix: docked drawer drops the second-screen page; tapping the active mode returns to none

* fix: docked drawer drops the bottom-screen mode icons; the Mode page keeps them

* fix: restore the second-screen drawer page when docked

* feat: dual screen launcher and dual screen sessions are separate Automatic/On/Off settings

* fix: session drawer top row keeps its right alignment

---------

Co-authored-by: Kurt Himebauch <[email protected]>
2026-11-01 23:14:18 -05:00
Kurt Himebauch 92ec0929ae fix: remove the Bottom screen HUD descriptions from the HUD settings (#1988)
* fix: remove the Bottom screen HUD descriptions from the performance HUD settings

* fix: remove the telemetry blurb, speed units row and tap hint from the bottom stats page

* fix: keep steamClientLabel
2026-11-01 22:17:24 -05:00
FX1922 57b7c99b9a fix: keep games being updated in the installed list for every store (#1989) 2026-11-01 22:05:39 -05:00
DroidDeck Merge 8171bf20fd Merge latest origin/main and resolve conflicts 2026-11-02 10:44:24 +08:00
DroidDeck Merge 99dce74b09 fix: address maintainer review (restore README, revert version bump) 2026-11-02 10:43:42 +08:00
The412Banner 40dfcd94f9 fix(game-env): DOOM (2016) keeps the swapchain image count it asks for (#1987)
DOOM (2016) asks for exactly two swapchain images and stops with "FATAL
ERROR: getSwapchainImagesKHR failed with error (VK_SUCCESS)" when it gets
more. Every game launch gets GAMESCOPE_WSI_MIN_IMAGE_COUNT=8 and
GAMESCOPE_WSI_ENSURE_MIN_IMAGE_COUNT=1, so gamescope's WSI layer gave it 8
images (3 with only the count removed) and the game crashed on start.

When DOOMx64vk.exe sits beside the launched program (DOOMx64.exe for a GOG
install, or DOOMx64vk.exe), both variables are removed and
vk_x11_strict_image_count=true is set, the option Mesa's driconf gives the
game. Other games keep the defaults. A per-game value in the environment
editor still wins.

Tested on an AYANEO Pocket FIT with DroidDeck 0.6.9 and the same values set
per game: the swapchain gets 2 images and the game reaches its main menu at
60 fps; before, it crashed on every launch.
2026-11-02 02:02:18 +00:00
Kurt Himebauch 84a04d9ead fix: Steam shows cached library art when it starts without reaching Steam (#1985) 2026-11-01 19:55:18 -05:00
The412Banner e1d241d723 feat(dual-screen): bottom-screen HUD, Line strip, strip under every bottom tab, Bottom screen HUD settings (#1982)
- In-game stats shows a bottom-screen HUD while no game sends dashboard data: 11 styles, tap to change; it reads the game's frames and the device itself, so it works with the top-screen HUD off.
- Line strip: one line like the top HUD's Strip (API + FPS, frame-time graph, 1 / 0.1 / 0.01 % lows, GPU, CPU with temperature, VRAM, RAM, clock); default strip above a game's dashboard; numbers that do not fit are left out by importance.
- The same strip under the tabs on every other bottom page during a session.
- Bottom screen HUD settings, shown even with the top HUD off: one switch for the whole bottom HUD, strip on every tab, and which numbers the Line strip shows.
- The bottom HUD redraws about 10 times a second (was about 3) and reads device stats twice a second.
- GPU load, clock and VRAM readers also run while the bottom HUD shows them (BottomHudPresence).
- Night style stays dim instead of flickering with the FPS.
- Review fixes: slow-frame ring 2048, tab strip redraw without gamescope frames, Proton name per game, Graphs' duration from the kept readings, Fahrenheit in Tiles, Rings and Cores.
2026-11-01 19:23:31 -05:00
Kurt Himebauch 6a30a9bb4b fix: docked HUD tab FPS number is half its previous size (#1983) 2026-11-01 18:38:36 -05:00
MaxsTechReview da665bb14f feat: Auto picks DD-Turnip on Adreno 7xx (#1978)
Auto now prefers the newest DD-Turnip release on the Adreno 7xx, 710/720/722 included, and falls back to the previous Banners-Turnip pick when no DD-Turnip pair is published or the build fails to start a session on the device. The 8 Gen 2 on One UI keeps its patched Banners build and the 6xx keep Banners-Turnip.
2026-11-01 17:57:57 -05:00
Kurt Himebauch e30e722f8b feat: manage several Steam libraries beside internal and the card (#1967)
* feat: manage several Steam libraries beside internal and the card

* fix: serialize Steam library registry edits, refuse folders inside a Games root, refresh games after changes

* fix: rename refreshes games, added x86-64 libraries join redist seeding and direct mapping
2026-11-01 17:35:05 -05:00
MaxsTechReview c5577478ca chore: remove source comments and unused code, translate every missing string (#1965)
* chore(i18n): translate every missing string in all locales

Adds the 87 strings the bottom-screen Dashboard and in-game stats pages
introduced to all 32 locales, plus the 51 older settings strings Japanese
and the 50 Chinese locales were still missing. Every locale now has all
3456 translatable strings with the same placeholders as English.

Also translates 36 entries that were still English where the language
does not use the English word, and makes es-419 touch_app_id use the same
%1$s placeholder as English.

* chore(patches): drop comments from the lines downstream patches add

Removes comment lines and trailing comments from lines our patches add,
with hunk headers recounted. Lines that a later patch uses as context
and the upstream backport 0123 are left as they are.

The gamescope, FEX, proot and xodus series still apply to their pinned
sources, and the patched trees differ only in those comments.
test_hud_frames now finds the HUD writer by its first declaration.

* docs: credit Greenlight, Better xCloud, viture-v2, GameNative and DiRTTelemetryErrorFix

These were credited only in source headers and NOTICE files.

* chore: remove source comments and unused functions

Removes explanatory comments from first-party Kotlin, Java, C, C++,
Rust, TypeScript, Python, shell, Gradle, CMake and workflow files.
License and attribution headers stay, and vendored third-party trees,
generated files and license files are untouched. A shellcheck directive
is kept, and udevmon's fallthrough comments became fallthrough attributes.

Syntax trees and token streams match the previous sources apart from the
comments and Python docstrings. Module docstrings that argparse prints
are kept, and device-smoke.sh prints the same --help text from a heredoc.

Also removes 37 functions and 4 fields that nothing referenced.

* chore: remove comments from the Syncthing integration and the latest main sources

* docs: credit Syncthing and the syncthing-android fork

* chore(i18n): translate the Syncthing strings in all locales

The 107 strings the Syncthing integration added are now in all 32 locales.
sync_web_path_locked names the Syncthing tab by its label.

* docs: credit @xXJSONDeruloXx for the HUD metric readers

The CPU, GPU load, thermal and battery readers come from their
PerformanceHudView pull requests #777, #877, #991, #1406 and #1573.
The FusionHUD credit line is unchanged.

* docs: credit the HUD metric readers to @xXJSONDeruloXx's work
2026-11-01 16:27:27 -05:00
Kurt Himebauch d730f0fa8b fix: docked HUD tab follows the HUD outline setting (#1976) 2026-11-01 16:20:35 -05:00
Kurt Himebauch 4798fadc12 docs: update README tagline (#1977) 2026-11-01 16:18:04 -05:00
Kurt Himebauch bf9435cd36 feat: KDE desktop on the bottom screen beside the session (#1966)
* feat: KDE desktop as a bottom-screen mode beside the top-screen session

* test: bottom-desktop launch test sets DISPLAY instead of inheriting it

* fix: drop the bottom desktop's launch when the session stops while preparing it

* fix: bottom desktop restarts on a fresh surface, trackpad/touch toggle, loading throbber, DroidDeck's display driver

* fix: bottom desktop pauses with the session and opens the keyboard for a tapped text field

* fix: no bottom desktop guest starts or keeps running unfrozen while the session is paused

* fix: bottom desktop treats a freeze in progress as paused

* fix: only the session that began a freeze clears its pausing flag

* fix: bottom desktop launch is stored and cleared only for its own session generation

* fix: a stopped session tears down the bottom desktop it started even after a newer launch is prepared

* fix: a new session's bottom desktop replaces one an earlier session left running

* fix: release the bottom desktop's keyboard watcher when its compositor dies

* fix: bottom desktop starts its guest only for the launch its compositor was attached for

* fix: bottom desktop ignores compositor replies to an earlier session's attach
2026-11-01 16:13:52 -05:00
Kirk fdea9f0435 fix: the navigation bar stays dark when the launcher is not fullscreen
From Android 14 the Material theme leaves the navigation bar
transparent, so with launcher fullscreen off a white system bar showed
under the dark launcher. AppTheme now sets the navigation bar to black,
matching the status bar.
2026-11-02 04:51:54 +08:00
The412Banner 8cb5c2168e feat(hud): GPU load and clock without root where Android blocks kgsl (#1969)
On some Adreno devices (the Retroid Pocket Duo) SELinux hides kgsl's sysfs
nodes from apps, so the HUD showed no GPU % and no GPU clock. Devices whose
nodes are readable keep using them; nothing changes there or on GPUs
without kgsl.

Where the nodes are blocked:

- GPU %: a native reader maps kgsl's memstore read-only (any kgsl client
  may) and samples the ringbuffer start/end timestamps once a millisecond
  while a HUD is shown. That covers all GPU work, Android's own
  composition included.
- Backup for GPU %: an implicit Vulkan layer in the guest
  (tools/gpuload) and the compositor time their own GPU work with
  timestamp queries. It runs until the memstore stamps are seen moving,
  and for the whole session if they never move.
- GPU clock: a small GPU job of our own reads the always-on and RBBM
  cycle counters (libgpuclock).

The HUD diagnostics export lists which source each GPU value came from.
tools/gpuload/build.sh takes the Vulkan headers from build_local.sh's NDK
first and keeps NDK paths with spaces intact.
2026-11-01 20:22:51 +00:00
Kurt Himebauch 6cd844ea51 feat: Follow screen resolution can be scaled down by a factor (#1964) 2026-11-01 14:51:28 -05:00
The412Banner 7eb9175def fix(dual-screen): In-game stats hint says "a game's data output", not only racing games (#1968)
War Thunder and Elite Dangerous show on this page too, so the hint no longer says racing game.
2026-11-01 19:26:03 +00:00
Daniel Noam af821184d5 fix: folder focus survives renaming, scrolling and filters 2026-11-01 20:07:55 +02:00
superboo07 8c09b2c6a8 Intergrated syncthing (#1442) 2026-11-01 12:58:09 -05:00
Kurt Himebauch 5b7e401b11 fix(ci): fork PR builds no longer fail pinning the source tree (#1963) 2026-11-01 12:56:33 -05:00
Daniel Noam d22ef554bb fix: controller focus moves into an opened folder 2026-11-01 19:26:31 +02:00
Kurt Himebauch 3566779895 feat: setting to turn off held-Back Steam shortcuts (#1955) 2026-11-01 11:15:59 -05:00
Kurt Himebauch 9c5f1e4df9 feat: HUD shrinks into its tab as it is dragged off an edge, and snaps to the sides (#1937) 2026-11-01 10:15:36 -05:00
DroidDeck Merge a2a5f84711 Merge origin/main into feat/malibridge-v25 2026-11-01 19:59:31 +08:00
DroidDeck Merge 55897a47a9 docs: show openMali worklog on repository home 2026-11-01 19:51:53 +08:00
DroidDeck Merge 67197aafec docs: add openMali MaliBridge v25 worklog 2026-11-01 19:47:33 +08:00
DroidDeck Merge ddfc94d7ce local: version 25 with mscoree fallback 2026-11-01 18:29:08 +08:00
DroidDeck Merge 22a806d05a local: CPU temperature fallback from adb feeder 2026-11-01 18:22:54 +08:00
DroidDeck Merge 65e2c55e9b fix: fall back to builtin mscoree when dotnet40 has no native 2026-11-01 18:20:40 +08:00
openMali e97e6b29df local: CPU clock before temperature in HUD 2026-11-01 17:54:04 +08:00
openMali 6d4961f7b6 local: session resolution and desktop HUD visibility 2026-11-01 17:32:51 +08:00
openMali e31a6eef18 local: hide unreadable Mali HUD GPU metrics 2026-11-01 17:24:06 +08:00
openMali a3d76b9a94 local: keep GPU bridge row visible after data clear 2026-11-01 16:03:31 +08:00
openMali 234b137328 local: show GPU via Mali bridge in Setup 2026-11-01 15:57:32 +08:00
openMali 9419a19c66 local: MaliBridge rc4, auto local DNS, KWin render node 2026-11-01 15:17:41 +08:00
Anuar Lukpanov 3193a7a706 fix(quest): keep KGSL submitted timestamps monotonic
An older submission that returns late must not hide a newer one. Contexts
with user-generated timestamps can have gaps, so they keep the failed read
instead of waiting once per integer.
2026-11-01 11:46:01 +05:00
Anuar Lukpanov 1259abed5f fix(quest): check a caught-up KGSL context at timestamp zero too
A context with no submissions skipped the ownership check. Check whenever
nothing is pending, since a pending timestamp's wait already checks it.
2026-11-01 11:33:03 +05:00
Anuar Lukpanov 6d7732710c fix(quest): check a tracked KGSL context before answering a read
A descriptor can close without destroying its contexts and its number can
be reused. KGSL checks the owner on every wait, so one wait on the retired
timestamp keeps a read of a dead context an error.
2026-11-01 11:27:57 +05:00
The412Banner ec9573643a fix(game-env): give Codemasters EGO games a thread plan for the core count Windows sees (#1936)
Codemasters EGO games (DiRT 3, DiRT Showdown, GRID, F1 ...) pick their thread plan by core count from system/hardware_settings_restrictions.xml, which only has plans for 2, 3, 4, 6 and 8+ cores. When DroidDeck leaves a phone's slow cores out Windows can see 5 (Retroid Pocket Duo), and DiRT 3 then puts every worker on one core and stops on a loading screen; Showdown closes at start.

When a game starts with WINE_CPU_TOPOLOGY set and its restrictions have no cpu rule for that count, droiddeck-game-env adds one copied from the nearest smaller plan (5 -> the game's 4-core plan), only if that plan's worker map file exists. The file as it was is kept as .droiddeck-original; later launches change nothing. Only ASCII digits count as a core count, and the edited file is staged under a unique name.

Tested on a Retroid Pocket Duo: with DiRT 3's shipped file, the plan is added at launch and the game loads to its menus and races.
2026-11-01 06:22:36 +00:00
Anuar Lukpanov bf0ee99b6d fix(quest): keep timestamp read failures for unknown contexts
Answer a failed read only for a context created on the same file
descriptor, so an invalid context or a closed descriptor stays an error
instead of reading as timestamp zero.
2026-11-01 11:01:58 +05:00
Anuar Lukpanov 9fce12730c fix(quest): answer a refused KGSL timestamp read from waits
Mesa 27 polls timeline points with CMDSTREAM_READTIMESTAMP_CTXTID and
reports any failure as VK_ERROR_UNKNOWN, so gamescope aborted at start on
Quest with vk.WaitSemaphores result -13. Track each context's submitted
timestamps in the preload and answer a failed read from one millisecond
WAITTIMESTAMP calls. Log refused KGSL ioctls to session.log.
2026-11-01 10:52:06 +05:00
Kurt Himebauch 09d170eb22 fix: performance HUD cycles colors with the Inner Gamer theme (#1932) 2026-11-01 01:41:42 -04:00
Kurt Himebauch 65b414df93 feat: Strip HUD size, a one-line bar across the top or bottom edge (#1935) 2026-11-01 01:26:30 -04:00
The412Banner 730b3affae fix(dual-screen): open the bottom-screen Dashboard in every session, not only the first (#1933)
On dual-screen handhelds the bottom-screen Dashboard only opened by itself in the first Steam session after DroidDeck started. SessionActivity.onCreate checks for a new session twice; the first block calls SessionEvents.begin(), which moves the phase to PREPARING, so the second block (which held the per-session reset of secondScreenAutoOpened / secondScreenProvisional) never ran. The reset now happens in the first block, right after begin().

Tested on a Retroid Pocket Duo: two Steam sessions in one app process, Dashboard on the bottom screen both times.
2026-11-01 04:44:44 +00:00
Moroni Granja dbf114a0f2 fix: draw the guest cursor into the glasses picture (#1925) 2026-11-01 00:26:14 -04:00
l4nr3 3fc80a03d9 fix: read the paged-out session back from zram after a resume (#1839) 2026-11-01 00:24:04 -04:00
K11MCH1 901c453c16 Session: the drawer's frame limit works in desktop mode, up to 165 (#1885) 2026-11-01 00:16:11 -04:00
Kurt Himebauch 9b024ffcdc chore: quick_deploy reuses runtime assets prepared on another branch (#1934) 2026-11-01 00:09:11 -04:00
MaxsTechReview 6e57af02af perf: Little Nightmares keeps its render thread (+12.7% FPS on the Retroid Pocket Duo) (#1930)
perf: Little Nightmares keeps its render thread (up to 13% more FPS on the Retroid Pocket Duo)
2026-10-31 23:49:56 -04:00
MaxsTechReview 86983092af fix: Flathub installs work again outside a session (#1923) 2026-10-31 23:36:24 -04:00
0x142857 85c41f9032 fix: opening DroidDeck from iisu on the AYN Thor no longer bounces back (#1901) 2026-10-31 23:33:40 -04:00
Kurt Himebauch 2ff3ab447d fix: local builds bundle the addon packs they pin (#1908) 2026-10-31 23:32:58 -04:00
The412Banner 8a7022fc14 feat(dual-screen): bottom-screen Dashboard with downloads, notifications, screenshots, Steam chat and In-game stats (#1920)
A Dashboard mode for the second screen of dual-screen handhelds (developed on a Retroid Pocket Duo), the default on dual-screen devices. It opens on Chat at each Steam start and switches to In-game stats when a game starts.

- Downloads: Steam downloads and updates (SD-card libraries too), shader caches and DroidDeck store downloads.
- Notifications: other apps' notifications once access is allowed, kept in memory only.
- Screenshots: a game screenshot without the overlay, and a gallery of DroidDeck's and Steam's screenshots.
- Chat & voice: mic and game volume buttons, and Steam's web chat signed in with the Steam client's own session.
- In-game stats: FPS, CPU, GPU, RAM and battery plus the running game or Steam client type, and a racing dash from Codemasters, Forza and OutGauge games' data output, with War Thunder and Elite Dangerous layouts; speed in km/h or mph.
- Send dashboard data: turns a Codemasters game's data output on for the dash (per game or Automatic) and keeps the original setting to put back.
- DiRT 3 / DiRT Showdown: with their data output on they stop with error 10038 at race end; a ws2_32.dll stand-in (tools/dirt-ws2) is swapped into the prefix while the game runs, skipped on DroidDeck-Proton 11-11.10 which has the fix built in. Same behaviour as DiRTTelemetryErrorFix by RoccoC, based on Carsten Gottschling's fix.

About "telemetry": here it means a racing game's own live driving data (speed, gear, rpm, pedals), not app analytics. For In-game stats no information leaves the device and nothing is shared: the game sends its data to 127.0.0.1 on the same device, DroidDeck only listens there while the page is open and keeps the numbers in memory just long enough to draw them. DroidDeck sends no data about the user or their use of the app; logs leave the device only when the user taps Share logs.
2026-11-01 02:36:57 +00:00
MaxsTechReview bfa61b5139 fix: "DroidDeck isn't responding" on dual-screen handhelds after the bottom screen sits idle (#1917)
The bottom-screen panel handed focus back to the main screen only when it opened or was touched within the last second. When the panel became the active screen later, the paused launcher kept no focused window, so controller input waited until Android reported the app as not responding. The panel now hands focus back whenever it becomes the active screen or gains window focus, unless bottom-screen typing is open or an app the launcher opened is in front.
2026-10-31 20:40:57 -04:00
Dantalian 113ba7b77c fix: keep each key cap's gesture keyed and make the default size selectable 2026-11-01 05:38:54 +08:00
MaxsTechReview 0a07926ef3 docs: DroidDeck 0.6.9 release notes (#1904)
* docs: DroidDeck 0.6.9 release notes

* docs: explain the banking app fix in the 0.6.9 notes

* docs: banking app line in the 0.6.9 summary

* docs: dual-screen line in the 0.6.9 summary

* docs: move the dual-screen line into the 0.6.9 intro

* Update release notes for version 0.6.9

Reformatted the release notes for clarity and improved readability.

* docs: add #1888 and #1896 to the 0.6.9 notes

* docs: add #1903 to the 0.6.9 notes
2026-10-31 13:58:57 -04:00
Kurt Himebauch b63feb5eaa fix: three-finger tap opens the keyboard in touchpad mode (#1903) 2026-10-31 13:55:03 -04:00
MaxsTechReview d691bcf832 chore: bump version to 0.6.9 (17) (#1902) 2026-10-31 13:54:30 -04:00
K11MCH1 583f34fc55 fix: stop importing games as Spacewar (480) and follow a changed Steam app id (#1896) 2026-10-31 13:36:43 -04:00
K11MCH1 88719f25d2 Games: launch options for Steam games in the Edit screen, shared with Steam's Properties (#1888) 2026-10-31 13:36:39 -04:00
MaxsTechReview ac132a23e2 fix: dual-screen update prompt survives a bottom-screen tap, home launcher stays out of Recents, Quest panel opens landscape (#1893)
* fix: dual-screen launcher keeps the update prompt on screen and stays out of Recents as home; Quest panel opens landscape

Outside screens the launcher opens (the update prompt, Settings, pickers) now hold the top screen when the bottom screen is touched, so a bottom-screen tap no longer buries the Android install prompt and leaves updates stuck waiting for confirmation (DD-368).

When DroidDeck is the home app its launcher task is excluded from Recents, so it can no longer be swiped closed; a running game session is still listed (DD-377).

The launcher declares a 1024x640 dp default panel size so Meta Quest opens it landscape again after the orientation manifest change in #1606 (DD-360).

* fix: Recents exclusion follows the top DroidDeck activity and only undoes its own change

The launcher task is hidden from Recents only while FrontEndActivity is on top, DroidDeck is home and no session runs; the file manager and a running session show it again, the check repeats when the launcher pauses, and tasks hidden by someone else are left alone.

* fix: Recents ownership survives process death and never claims a task someone else excluded
2026-10-31 09:21:49 -04:00
MaxsTechReview 85dd8beae1 zRAM: compress Battle.net with the Steam client while one of its games runs (#1882) 2026-10-31 08:10:57 -04:00
DroidDeck Merge 69ffc0966b chore(mali): sync MaliBridge to v0.5.4.2 and document Mali support 2026-10-31 19:02:12 +08:00
DroidDeck Merge d4a6d95127 Merge MaliBridge integration (feat/malibridge) into 0.6.7 2026-10-31 19:01:40 +08:00
Dantalian 3df48d5625 feat: make the PC keyboard's size, opacity, and key groups configurable 2026-10-31 17:37:05 +08:00
MaxsTechReview b07eca3c06 perf: .NET games keep the FEX preset's multiblock setting (Stardew Valley starts in 45 s instead of 85 s) (#1880)
perf: .NET games keep the FEX preset's multiblock setting
2026-10-31 00:17:09 -04:00
Kurt Himebauch 6eef0721c9 feat: the HUD is DroidDeck HUD, pinch resizes it, and pushing it off an edge docks it as an FPS tab (#1868) 2026-10-30 22:44:23 -04:00
Kurt Himebauch e3c3471d2f Update README (#1874) 2026-10-30 21:57:32 -04:00
MaxsTechReview a81d1a4d4e Ask for All files access when a USB drive is mounted but unreadable (#1872) 2026-10-30 21:21:26 -04:00
MaxsTechReview 28067b3bbe Run x86_64 Flatpak apps through FEX (#1875)
* Run x86_64 Flatpak apps through FEX

* proot: run an execveat with an absolute path like execve

* Find x86_64 Flatpak installs and resolve sandbox paths like the sandbox does
2026-10-30 21:21:22 -04:00
MaxsTechReview 8088e47d14 fix: "DroidDeck isn't responding" on dual-screen handhelds after the bottom screen takes focus (#1871) 2026-10-30 19:52:17 -04:00
MaxsTechReview 6673a892e2 fix: x86 Flatpak bundles are refused with a clear message instead of installing and failing to start (#1873) 2026-10-30 19:03:27 -04:00
MaxsTechReview 8cf01ebb77 fix: Linux games on an NTFS USB drive start the first time instead of failing to load a library (#1850)
* fix: a read-only file on a USB drive no longer fails to close the first time it is read

On the RedMagic Astra 2, Android's NTFS USB drive returns EIO from close() for a file opened read-only when that first read updates its access time; the update itself succeeds and the descriptor is released. The x86 loader under FEX treats a failed close as fatal, so every Linux game newly installed or updated on such a drive failed to start once (libfreetype.so.6: cannot close file descriptor: Input/output error). The session library's close() now reports success for EIO on read-only descriptors; errors on descriptors opened for writing are still returned.

* Check the read-write close still reports EIO
2026-10-30 18:36:08 -04:00
MaxsTechReview 7e47db2e2a fix: a game with a launch-option picker is no longer closed while the picker is open (MK11) (#1869)
fix: a game with a launch-option picker is no longer closed while the picker is open
2026-10-30 18:16:11 -04:00
MaxsTechReview 484afffffd Translate the strings added since the last translation pass (#1855)
* Translate the strings added since the last translation pass

* Fix translation wording from review

Use the Android settings labels for the pairing guide in bg, cs, da, de, fi, ru and zh-CN, correct the frame limit divisibility wording in de and nl, describe owned games without implying purchase in hi and ru, fix the VRAM label in es-419, describe GOG replacement files correctly in tr and name the Guide button in zh-CN.
2026-10-30 18:08:05 -04:00
MaxsTechReview 0b1af34a5f fix: library filter, sort and display menus aren't cut off in portrait (#1827)
* fix: library menus open toward the side with room, so in portrait they are no longer cut off

In portrait the Filter, Sort, Display and Collections buttons sit at the left of the screen, but their menus still grew to the left and were squeezed into the space before the button, cutting the labels. A menu now opens the other way when its own side is narrower than the menu and the other side has more room.

* fix: step-out menus keep their outline with the content when a resize flips the side

* fix: step-out menus still take keyboard focus when the layout shifts while opening
2026-10-30 17:04:59 -04:00
MaxsTechReview 2ff682a4f5 fix: a folder of games with an extra non-game folder still lists each game (Rance 01) (#1822)
* fix: a folder of games still splits into its games when it also holds a folder with no game, as with Rance 01 in a VNs folder

A visual-novel folder on game storage held a steamapps folder with no exe in it. The group check asked every subfolder to hold a game, so the whole folder stayed one game and a pick in Rance 01 switched that game instead of adding its own. Folders without a game and Steam library folders no longer count, and old games folders being moved to entries split such a folder the same way.

* Check a folder's own entry, removal and store ownership before splitting it, and keep folders with redist or extras as one game

An old Games folder that was removed keeps its games removed after the split, a folder already added stays as it is, and a store game's folder is never split.

* fix: a split games folder keeps the game it launched and that game's settings

When an old games folder entry is split into one entry per game, the
folder holding the selected exe is kept even if ranking left it out (or
the parent stays whole when the exe sits directly in it), and the
parent's name, app ID, launch options and other settings carry over to
that game without overwriting settings it already has.

* fix: a split games folder never migrates steamapps and keeps the launched exe

A saved exe under a steamapps or hidden folder keeps the old entry whole
instead of adding that folder as a game, and the game that inherits the
old entry's settings keeps the exe the old entry launched when nothing
was picked.
2026-10-30 16:58:23 -04:00
MaxsTechReview 18f6305977 fix: banking apps no longer refuse to open while DroidDeck is installed (#1824)
* fix: banking apps open again with DroidDeck installed; the shortcuts accessibility service is listed only once the user sets it up

0.6.7 added HomeButtonService, a key-filtering accessibility service, enabled in the manifest. Android lists it as an installed accessibility service on every phone, and banking apps such as UBS refuse to start while one from outside the Play Store is installed. The service is now disabled by default, turned on when the user opens the Shortcuts setup, turned back off if they leave it unset, and kept on for anyone who already uses it.

* Keep the shortcuts service declared and turn it off at start unless the user chose it, so an update never drops their choice

A failed read of the enabled-services setting now changes nothing.

* Hide the unchosen shortcuts service right after an update, before DroidDeck is opened

A package-replaced receiver in its own light process runs the same listing sync as startup. The manifest default stays enabled because Android drops an unavailable service from the enabled-services setting, which would erase the choice of users updating from 0.6.7.

* Hide the shortcuts service as soon as the user turns it off in Accessibility settings
2026-10-30 16:55:09 -04:00
MaxsTechReview 7bc3529722 fix(audio): DirectAudio no longer crackles where Android refuses low-latency output (#1862)
* fix(audio): DirectAudio no longer crackles where Android refuses low-latency output

On devices where Android won't give the game a low-latency output (AYN Thor,
Android 13), DirectAudio kept Wine's 10 ms period and 12 ms buffer. An
emulated game audio thread can't keep that pace there, so audio crackled.
The session now checks for low-latency output and, only when it is refused,
asks DirectAudio for a 20 ms period and a 60 ms buffer.

DirectAudio's settings are now AUDIO_DIRECT_* everywhere in DroidDeck and are
handed to the driver under the names it reads when DirectAudio is set up.
AUDIO_DIRECT_MS and AUDIO_DIRECT_PERIOD_MS can be set per game.

* fix(audio): game profile DirectAudio settings and the microphone-off setting reach the driver
2026-10-30 16:34:45 -04:00
Kurt Himebauch 9cc56631cd feat: test and local builds show a faint PR, commit and title watermark (#1863) 2026-10-30 15:29:41 -04:00
Kurt Himebauch a5b035b247 test: the required-status gate expects the skip-on-superseded condition (#1866) 2026-10-30 15:25:28 -04:00
MaxsTechReview a8cebd024b fix(ci): required-check test matches the gate's !cancelled() condition (#1864)
fix(ci): the required-check test expects the gate's !cancelled() condition
2026-10-30 15:20:30 -04:00
MaxsTechReview 7c49eef79f fix(dual-screen): keep the bottom-screen controls up while a game loads (#1840)
* fix(dual-screen): keep the bottom-screen controls up while a game loads

The session opened its bottom-screen trackpad only at the game's first frame. The launcher's bottom menu closes when a game starts, so on dual-screen handhelds the bottom screen showed whatever Android app was behind it for the whole load, which is long for Xbox games while Xodus prepares them. The controls now open when the session starts. Devices without a built-in second screen, and players who turned the in-game controls off, are unchanged.

* fix(dual-screen): pick the bottom-screen panel again once the session's controller is known

Opening the bottom-screen controls at session start can run before SessionService has prepared the Steam Deck pad, while SessionState.deckPad is still false, so the trackpad could be chosen where the Deck controls belong. That early choice is now provisional: at the game's first frame it switches to the panel the remembered choice and the real deckPad give, unless the player has already picked one.

* fix(dual-screen): keep the loading-time panel choice through PiP, background and new sessions

* fix(dual-screen): retry a terminal panel opened before it was ready and keep a manual close made during loading
2026-10-30 14:32:34 -04:00
Kurt Himebauch c8c5e22a34 fix: new Steam installs start with shader pre-caching off (#1861) 2026-10-30 14:17:45 -04:00
Kurt Himebauch fa4a54f64a Default game library to vertical covers (#1857) 2026-10-30 14:09:44 -04:00
MaxsTechReview 83980035c6 fix(desktop): video wallpapers no longer race ahead after DroidDeck returns from the background (#1833)
* fix(desktop): video wallpapers no longer race ahead after DroidDeck returns from the background

* test: give the vrclient loader harness the clock syscall hook
2026-10-30 13:58:49 -04:00
MaxsTechReview 8f7cdb7106 fix: Battle.net scales its windows to large desktops instead of opening tiny on high-resolution screens (#1832) 2026-10-30 13:58:44 -04:00
MaxsTechReview 9a1334b73b fix: on dual-screen handhelds DroidDeck no longer jumps back over apps opened from outside it (#1828)
fix: the bottom-screen panel no longer pulls DroidDeck back over an app opened on the main screen from outside DroidDeck

On dual-screen handhelds the panel took focus and moved DroidDeck's task forward whenever another app opened over it on the main screen, unless DroidDeck had opened that app itself. A frontend such as iisu, or Settings opened from a shortcut, was covered again within a fraction of a second. The panel now stops reclaiming once the launcher is paused, and starts again when it resumes.
2026-10-30 13:58:40 -04:00
MaxsTechReview ab949a4a5a fix(runtime): the x86-64 Steam client starts again on the Ubuntu runtime (liblzo2-2 for FEX) (#1825)
* fix(runtime): ship liblzo2-2 so the x86-64 Steam client can install FEX

FEX's unsquashfs links liblzo2. The Ubuntu runtime did not ship it, so the FEX check failed on every build, the x86-64 client was never set up, and the session went straight back to the home screen. Only phones with the desktop installed had it.

* fix: the x86-64 Steam client starts again after it updates itself under FEX

On its first start the x86-64 client downloads a 500 MB update and, under FEX, ends on an illegal instruction (rc=132) instead of asking for a restart with rc=42, so the session ended there. The client is started again once.
2026-10-30 13:58:35 -04:00
MaxsTechReview b91e8fdeab fix: a session in picture-in-picture draws at 30 fps instead of the full refresh (#1842)
* fix: a session in picture-in-picture draws at 30 fps instead of the full refresh

Since auto picture-in-picture became the default (#1727), pressing Home leaves the session running in a small window that takes no input. A game kept drawing at its full rate there: on a REDMAGIC Astra 2 Among Us in picture-in-picture drew 3.31 W, the same as playing it and about 1.05 W more than a suspended session. While in picture-in-picture the compositor now paces the session at 30 fps (or a lower limit already set). Mirroring to glasses keeps the glasses rate, and the display mode request ignores the picture-in-picture limit so the app in front is not slowed.

* fix: keep the no-argument output limit the glasses pacing tests read

* fix: hold scene renders to the output limit and keep frame generation off in picture-in-picture

A frame limit only paced dmabuf buffer releases, so a shared-memory client was still drawn on every vsync with its frame callbacks sent at the panel's rate. The vsync tick now waits until the limit's interval, less half a refresh, has passed since the last scene. Frame generation is disarmed while picture-in-picture is shown and armed again when it ends; frame generation requests are applied in order.

* Give the commit render harness the frame limit hold

* Hold scene renders to a frame limit deadline, also on the fallback timer, and keep frame generation for glasses mirroring in picture in picture
2026-10-30 13:29:10 -04:00
MaxsTechReview 021e7153fa fix: Automatic dual screen needs a real second touch panel (Odin keeps its side rail) (#1801)
* fix: AYN Odin handhelds are single-screen in automatic dual-screen mode

* Use a second screen only when more than one physical screen is lit, instead of excluding AYN models

* fix: open the session panel when the second screen turns on, and keep the On mode's display choice

* fix: let the session panel open again after its screen goes away and returns

* fix: Automatic dual screen needs a touchscreen for the second panel instead of counting lit displays

* fix: check the second screen again when a touchscreen comes back after the display

* fix: check the second screen again when a touchscreen is added or removed, and on resume

* Keep the second screen panel closed once the session has ended
2026-10-30 13:19:21 -04:00
Kurt Himebauch 524292f457 fix: drop the runtime installs on first Play chip from the Steam home (#1854) 2026-10-30 13:04:16 -04:00
Kurt Himebauch 92c1560f63 feat: Steam's sleep-after timers start every session as Disabled (#1848) 2026-10-30 12:13:12 -04:00
maxjivi05 0a5955d6fa Merge remote-tracking branch 'droiddeck/main' into feat/malibridge
# Conflicts:
#	README.md
#	app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java
#	app/src/main/java/com/droiddeck/launcher/runtime/BwrapSpawner.kt
#	app/src/main/res/values-ar/strings.xml
#	app/src/main/res/values-b+es+419/strings.xml
#	app/src/main/res/values-bg/strings.xml
#	app/src/main/res/values-cs/strings.xml
#	app/src/main/res/values-da/strings.xml
#	app/src/main/res/values-de/strings.xml
#	app/src/main/res/values-el/strings.xml
#	app/src/main/res/values-es/strings.xml
#	app/src/main/res/values-fi/strings.xml
#	app/src/main/res/values-fr/strings.xml
#	app/src/main/res/values-hi/strings.xml
#	app/src/main/res/values-hu/strings.xml
#	app/src/main/res/values-in/strings.xml
#	app/src/main/res/values-it/strings.xml
#	app/src/main/res/values-ja/strings.xml
#	app/src/main/res/values-ko/strings.xml
#	app/src/main/res/values-ms/strings.xml
#	app/src/main/res/values-nb/strings.xml
#	app/src/main/res/values-nl/strings.xml
#	app/src/main/res/values-pl/strings.xml
#	app/src/main/res/values-pt-rPT/strings.xml
#	app/src/main/res/values-pt/strings.xml
#	app/src/main/res/values-ro/strings.xml
#	app/src/main/res/values-ru/strings.xml
#	app/src/main/res/values-sv/strings.xml
#	app/src/main/res/values-th/strings.xml
#	app/src/main/res/values-tr/strings.xml
#	app/src/main/res/values-uk/strings.xml
#	app/src/main/res/values-vi/strings.xml
#	app/src/main/res/values-zh-rCN/strings.xml
#	app/src/main/res/values-zh-rHK/strings.xml
#	app/src/main/res/values-zh-rTW/strings.xml
#	app/src/main/res/values/strings.xml
#	tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env
2026-10-30 11:58:55 -04:00
MaxsTechReview 41615d2460 fix: Steam's UI runs at the panel's full refresh rate again (#1841)
#1809 held Steam's own UI to an even share of the refresh near 60 fps whenever it had focus. On 120 Hz and faster screens Big Picture then felt locked at 60 while games ran at the full rate, and players reported it as a regression. The compositor now releases Steam UI buffers unpaced again, like a game's. The larger swapchain stays limited to games, so the client UI still uses gamescope's default image count.
2026-10-30 11:10:03 -04:00
Kurt Himebauch bc1a010411 fix: launching a game from a frontend starts it instead of sitting on the home screen (#1844) 2026-10-30 10:34:30 -04:00
MaxsTechReview d31f234f74 fix(xbox): games run from their own folder on the short drive, not the drive root (#1804)
* fix(xbox): keep the game in its own folder on the short drive instead of at the drive root

* fix(xbox): SD card Xbox games can read their own package files

Xodus mounts a store library on the selected SD card at /mnt/droiddeck-stores/<card> and links the launch view to it, but game sessions only bound that library as /mnt/droiddeck-sd. The links were dangling inside the game, so MicrosoftGame.Config, appxmanifest.xml and XCurl.dll could not be read and GDK games stopped with "Failed to initialize GDK". Game sessions now bind the same store roots Xodus uses.

* fix(xbox): give the SD Steam library its own store path when another store root shares the card

externalRootsForXodus added the selected Steam library's Games folder with the card's guest path even when another store root on the same card already used it, so both bound to one destination. It now takes a path-derived suffix in that case, the same way externalRoots separates its own roots.
2026-10-30 10:10:23 -04:00
Delete228 d6b4b0589b Merge upstream shortcuts and dual-screen session updates
Preserve profile bindings, shared held-input ownership and custom panels alongside upstream physical shortcuts, mouse routing and docked menus. Add integration regressions and record the full 2,918-test JVM run.
2026-10-30 17:00:29 +03:00
FX1922 6ffe675a57 fix: match the game page's check for updates, uninstall and edit art buttons to the frosted design (#1831) 2026-10-30 09:31:05 -04:00
MaxsTechReview fb5af9d35e fix(runtime): cutscenes and in-game videos play again with DroidDeck-Proton (libva-drm2, libva-x11-2) (#1823)
fix(runtime): ship libva-drm2 and libva-x11-2 so DroidDeck-Proton's video demuxer loads

DroidDeck-Proton's ffmpeg links libva-drm and libva-x11. The runtime only had libva2, so winedmo.so failed to load and Media Foundation video stopped: Resident Evil Revelations hung on its loading screen with "Failed to create demuxer, status 0xc000007a", and RE engine and Kingdom Hearts cutscenes broke. Installing the desktop hid it because Plasma pulls both in.
2026-10-30 09:18:09 -04:00
Kurt Himebauch b75060c280 feat: cap the frame rate in session for games launched without Steam (#1813) 2026-10-30 07:39:31 -04:00
FX1922 67d391958f feat: apply itch updates as butler patches (#1829) 2026-10-30 07:38:06 -04:00
4770d11e39 feat: Add physical controller button shortcuts (#1202)
* feat(controller): add physical button shortcuts

* fix(controller): improve shortcut capture and assignment feedback

* refactor(controller): simplify menu button assignment UI

* feat(controller): save menu assignments on button release

* refactor(controller): remove obsolete shortcut UI resources

* fix(controller): place drawer remaps below Back

* refactor(controller): simplify shortcut capture wording

* fix(controller): refresh active shortcut bindings on changes

* fix(controller): preserve drawer confirmation for mapped A

* fix: preserve controller shortcut navigation and cancel

* test: align shortcut dialog assertions

---------

Co-authored-by: xXJsonDeruloXx <[email protected]>
Co-authored-by: Kurt Himebauch <[email protected]>
2026-10-30 07:08:59 -04:00
qinsiyuan ab43524bbc fix: preserve the runtime PATH when overriding guest binaries 2026-10-30 18:35:06 +08:00
MaxsTechReview f69e2d0171 fix: Steam's UI draws at an even share of the refresh near 60 fps instead of the panel's full rate (lag and heat since 0.6.7) (#1809) 2026-10-30 02:16:25 -04:00
MaxsTechReview b0a8c2065d fix: a folder of games on game storage lists each game instead of one combined game (#1808) 2026-10-30 02:01:56 -04:00
MaxsTechReview d4fd7ac5c3 fix: "DroidDeck isn't responding" on dual-screen handhelds when input reaches the bottom screen (#1806) 2026-10-30 01:56:44 -04:00
MaxsTechReview ed661d7007 fix: the performance HUD is off by default and keeps the user's own choice (#1807) 2026-10-30 00:10:06 -04:00
MaxsTechReview 265335d550 fix: the Stop button no longer wraps letter by letter on narrow bottom screens (#1805) 2026-10-30 00:09:53 -04:00
MaxsTechReview 2b0d633535 fix: the Duo's bottom screen keeps the panel you picked across games (#1803) 2026-10-29 23:45:06 -04:00
MaxsTechReview 3b917c0db3 fix(runtime): ship GTK 3 and the libraries Proton's arm64 builds need (#1802) 2026-10-29 23:44:50 -04:00
MaxsTechReview 58968c9286 fix: itch.io installs unpack a game archive nested inside the download (#1800) 2026-10-29 23:21:38 -04:00
Kurt Himebauch 5736297987 fix: keep the launcher from reclaiming focus when an app opens on the primary screen (#1797) 2026-10-29 21:58:23 -04:00
MaxsTechReview 26e2f26ec9 fix: Steam's Browse buttons work when another app holds port 8080 with Decky enabled (#1798) 2026-10-29 21:22:14 -04:00
MaxsTechReview 23bb361006 fix: Steam no longer restarts every launch to import added games, and asks first (#1795) 2026-10-29 21:14:11 -04:00
MaxsTechReview ee3876b965 Windows components: restore bundle parts, wait for restores and keep older picks (#1788) 2026-10-29 20:31:18 -04:00
Kurt Himebauch 5d6f9c7d74 feat: show the DroidDeck D mark as the fastfetch logo (#1778) 2026-10-29 20:21:49 -04:00
MaxsTechReview d5d3aa2ae4 fix: ARMSX2, melonDS and Cemu start without the desktop installed (#1793) 2026-10-29 20:16:55 -04:00
MaxsTechReview 31c943c82a fix: Windows installs to Android storage no longer ask for an administrator (#1791) 2026-10-29 19:12:22 -04:00
MaxsTechReview 3b9abf6302 Update the Linux runtime by itself when a new one is out (#1787) 2026-10-29 18:50:26 -04:00
MaxsTechReview 161a93a4b3 Session follows the Android time zone, and Teardown keeps its own vsync (#1790) 2026-10-29 18:49:11 -04:00
Kurt Himebauch 7d3d338fb9 Keep the manual second screen option when Dual screen is Off (#1777) 2026-10-29 18:39:17 -04:00
MaxsTechReview a3da6f0bda Launch frontend game links without a tap on dual-screen devices (#1789) 2026-10-29 18:37:52 -04:00
MaxsTechReview b726e19fa4 Keep Windows components across runtime updates and reinstall lost ones (#1786) 2026-10-29 17:48:04 -04:00
Kurt Himebauch 001ec02a17 chore: bump version to 0.6.7 (16) (#1783) 2026-10-29 15:20:25 -04:00
Kurt Himebauch a69ae77fc5 docs: DroidDeck 0.6.7 release notes (#1765) 2026-10-29 15:18:55 -04:00
MaxsTechReview 22d39998f0 fix(desktop): let desktop apps save read-only game files, and make game libraries writable (#1781) 2026-10-29 15:16:17 -04:00
MaxsTechReview 5786e89ff4 feat(desktop): one bottom panel with the system tray instead of a top bar (#1772) 2026-10-29 12:32:38 -04:00
Kurt Himebauch e892805c2f fix: ship libxcb-keysyms1 in the Ubuntu runtime so DD-Turnip loads (#1773) 2026-10-29 12:22:25 -04:00
MaxsTechReview 0258f10df0 fix(desktop): start Electron and Chromium apps from apt without the sandbox, on X11 (#1775) 2026-10-29 12:02:40 -04:00
Kurt Himebauch 30923635af fix: remove performance HUD detail text (#1768) 2026-10-29 10:37:25 -04:00
The412Banner 15b00fc23c fix(release): rename the dual-screen and typing task affinities for variant APKs (#1767) 2026-10-29 10:12:02 -04:00
MaxsTechReview 9f7f6d821d perf(epic): keep the hidden launcher from loading its store and Fab pages, and compress it while games run (#1764)
* perf(epic): keep the hidden launcher from loading its store and Fab pages, and compress it while games run

The hidden Epic Games Launcher that stays up for sign-in while a game runs pre-warmed its store and Fab pages on a display nobody sees. Under FEX each page used about one core and grew past a gigabyte, crashing and reloading in a loop. The hidden instance now skips the store pre-warm, gets a blank page in place of Fab, and is paged out to zRAM with the Steam client once a game has focus. The visible launcher is unchanged.

* test(zram): keep the scan fixture PIDs above the kernel PID range
2026-10-29 10:01:25 -04:00
MaxsTechReview 29c83976d0 Move the Linux runtime to Ubuntu 26.04 LTS with a separate Kubuntu desktop (#1733) 2026-10-29 09:05:20 -04:00
The412Banner 5de690ef39 fix: FusionHUD Pill battery row, and a locked HUD lets on-screen controls work (#1763)
Pill: battery level, temperature and watts share one "BAT" row, as on the
other sizes, and each part follows its own switch. Turning Memory off no
longer hides the battery temperature (reported by Keegan), and watts show
without the battery level.

Touch (part of #1752, reported by ZZCSK): a locked HUD lets a touch through
where an on-screen control under it takes it: controller buttons and sticks,
Steam touch controls, screen keys and the controls panel below the game.
Each layer answers takesTouchAt with its own touch checks. Everywhere else
the HUD keeps the touch, so a long press still locks or unlocks it. A new
HUD starts locked. The "Lock in place" hint says so in every language.

Tests: Pill battery row per switch combination; takesTouchAt agrees with a
real touch over a grid for the controller buttons, screen keys and both
controls panel layouts (Deck and gamepad, landscape and portrait); a locked
HUD passes touches to a control under it, keeps the rest, and unlocks on a
long press.
2026-10-29 12:51:28 +00:00
MaxsTechReview 283e3b4549 Use the second screen on dual-screen handhelds (#1734)
* Use the second screen on dual-screen handhelds

Detect built-in second screens such as the Retroid Pocket Duo and AYN Thor.
The sections move to a menu on the bottom screen, L2 and R2 switch sections,
and text fields type on the bottom screen so the top keeps the full view.
Sessions open the bottom-screen trackpad and keyboard when they start.
Setup gains a Dual screen choice (Automatic, On, Off) with toggles for the
bottom menu and the in-game controls. Closing the bottom menu no longer ends
a running session.

* Type into Steam from the bottom screen

While the bottom-screen trackpad and keyboard controls are open in a Steam
session, Steam's own on-screen keyboard no longer covers the top screen.
The agent keeps Steam's text target and asks the app instead, which opens
the Android keyboard on the bottom screen and hands focus back to the game
when it closes. Modal and VR keyboards, and sessions without the bottom
controls, keep Steam's keyboard. The Show keyboard button now follows the
keyboard when it is closed another way.

* Open the bottom controls after the first frame and tidy dual-screen translations

* Move the in-game menu to the bottom screen on dual-screen handhelds

Fix controller input and focus on the second screen: keys and sticks reach the game after touching the bottom controls, closing the bottom controls returns focus to the game, B closes bottom typing, and the typing panel keeps a focusable window until focus has left it.

* Let B close the open layer of the session menu first and focus the selected bottom-screen mode

* Let the Retroid Pocket Duo Home double press open its quick menu during sessions

* Open the keyboard on the bottom screen for Steam text fields and hide it on Enter or when the field loses focus

* Keep controller focus on the top screen when a section is picked on the bottom screen

* Keep LB and RB on the store tabs and let Back return from a store to its tab

* Show the Adreno 722 as supported on the Retroid Pocket Duo

* Label the Steam integration suggestion's close button Close and cover that closing it is permanent

* Close the docked menu with its presentation, keep a focus picked during the stop delay, and ignore panel picks on process-settings pages
2026-10-29 07:52:18 -04:00
MaxsTechReview e9e0186e66 perf(fex): answer raw stat, readlink and statx syscalls through the proot fast path (#1757)
* perf(fex): answer raw stat, readlink and statx syscalls through the proot fast path

FEX issues newfstatat, readlinkat and statx as raw syscall() calls, which skipped the libc wrappers the fast path hooks and fell through to ptrace. Route them through libblfastpath the same way raw openat already is, and keep the fast path's own /proc/self/exe lookup off the hooked syscall().

* test(fex): keep the raw lookup test passing on kernels without statx
2026-10-29 07:25:39 -04:00
Kurt Himebauch 8329033121 feat: setting to hide sidebar labels (#1749) 2026-10-29 07:19:40 -04:00
Kurt Himebauch 81a6186f9e fix: smooth grid scrolling when focus moves past the visible rows (#1750) 2026-10-29 07:01:32 -04:00
dddms 2ea78cb238 perf(steam): batch compatibility mapping updates
Apply planned compatibility changes in one traversal instead of copying and searching the whole token list for each game. Wire retired-Proton replacement, default/tool changes and x86-64 repointing through the batch writer.

Share entry and name updates to preserve metadata, first-match semantics and reverse insertion order. Retain the single-item path and sequential alias/non-string behavior without changing the planner, tool policy, file locks or atomic writes.

Add reference, work-budget and integration regressions, and document the host-only bulk configuration benefit.
2026-10-29 14:22:42 +08:00
MaxsTechReview d1bd23df16 feat: warn when a game library drive is linked at USB 2.0 speed (#1746)
* Warn when a drive used as a game library is linked at USB 2.0 speed

Reads the drive's USB link speed from sysfs through its mount, and shows a
dismissible warning with the USB generation, link speed and a tip for a
faster cable or drive. The warning returns after the drive is plugged in
again unless Don't show this again is switched on.

* Keep slow USB dismissals per plug-in across restarts and recheck after a dismiss

Dismissals are stored with the boot count and the USB bus and device number,
so they survive a process restart, end when that drive is plugged in again
even while the launcher is stopped, and never clear other drives. A dismiss
looks for the next slow drive, stale scans are dropped, and an unknown port
speed is no longer treated as a USB 2.0 port.

* Drop slow USB scans that a newer library or mount check has superseded

* Invalidate pending slow USB scans as soon as a media broadcast arrives

* Revalidate an open slow USB warning on every check and match raw mount library paths

Every check now rescans, so a warning that is already showing is replaced or
cleared when its drive is removed or the library changes. Library paths saved
under /mnt/media_rw are treated as their /storage volume.

* Keep the Don't show this again choice when a rescan closes the slow USB warning

* Save the slow USB opt-out before the launcher activity is torn down
2026-10-29 00:18:07 -04:00
Kurt Himebauch 2410b8c67c feat: art-first store cards (#1747)
* feat: art-first store cards that share the library display settings

* fix: keep price and installed state on store cards when size is hidden

* fix: darken the compact store badge for contrast on light artwork
2026-10-28 23:47:00 -04:00
Kurt Himebauch 64950099a3 fix(storage): stack the summary card in narrow panes (#1736) 2026-10-28 23:08:59 -04:00
Kurt Himebauch 0516d023fa feat: art-first library cards with a display menu (#1744)
* feat: art-first library cards with a display menu for size, title and storage

* build: use native arm64 docker on Apple Silicon and add a fast deploy loop

* fix: address review feedback on library cards and local build scripts
2026-10-28 22:40:53 -04:00
The412Banner 110cfc28f8 feat: FusionHUD replaces the session HUD, with MangoHud's readings (#1743)
The session HUD is FusionHUD (Bannerlator's HUD) with every reading taken from
the same source, and worked out the same way, as the MangoHud build that runs
Steam's performance overlay in a DroidDeck session.

- gamescope patch 0125 (gamescope-3.16.29-p19) shares the frame times that
  mangoapp reads (one per signalled commit of connector 0) in a ring named by
  DROIDDECK_HUD_FRAMES; the app reads it once per display frame and counts the
  compositor's frames when gamescope sends none.
- MangoHud's rules: FPS over periods of at least 500 ms, frame time = 1000/FPS,
  200-frame graph on a fixed 0-50 ms scale; CPU, GPU, temperatures, RAM, swap,
  VRAM and battery from MangoHud's sources. Zones 22/26/28 are typed as GPU and
  DDR sensors, so Steam's overlay CPU temperature no longer includes them.
- Five sizes in a panel shaped like the DroidDeck mark, with a centred header,
  one left edge, neon outline and FPS; Minimal is the default size. Settings on
  a new "Performance HUD" drawer page, with Bannerlator's options.
- The HUD reads sensors and polls frames only while it is drawn (not while
  hidden, in picture-in-picture or behind another app), keeps its place after
  picture-in-picture, and the diagnostics export has its own reader.

Device-tested on the AYANEO Pocket FIT: same readings as Steam's overlay side
by side in Dead Cells; the HUD updates in a game, hides in picture-in-picture
and comes back in place, no crashes.
2026-10-29 01:53:45 +00:00
devSharaku e871557bf2 Update Xbox backend to 0.2.12 and accept compact JWE tickets (#1729)
Some Microsoft accounts get a delegation-compact (d=) ticket, which the
pinned Xbox backend (Nexus 0.2.8) rejected: the library stayed empty
(store-ticket-format) and downloads failed. This moves the backend to
Nexus 0.2.12, which has the upstream store-ticket fix, rebases the
DroidDeck credentials, launch-view and cloud-token patches, and strips
the d= prefix from compact JWE tickets so downloads work again.

Tested by seven users in the Discord Xbox Store Issues thread on the
pr-1729 build at d55e05a0.

* Update Xbox backend to 0.2.12
* Fix cloud offering fallback and add regression tests
* Fix Xbox authentication with compact JWE tickets
2026-10-29 00:07:53 +00:00
The412Banner a8b3c2d5dc docs(license): name DroidDeck in the GPL notice instead of Pluvia (#1741)
The notice at the end of LICENSE still carried Pluvia's name and
copyright line from GameNative's copy of the file.
2026-10-28 23:56:12 +00:00
Kurt Himebauch 87572a7f91 feat: game page with art backdrop and frosted controls (#1738) 2026-10-28 19:33:33 -04:00
Kurt Himebauch 1e92d10fa5 fix(ui): keep the ring veil count per test (#1739)
* ci: run unit tests in four forks

* fix(ui): keep the ring veil count per test so tests do not share snapshot state

* ci: keep two unit test forks, four were slower
2026-10-28 18:19:15 -04:00
Kurt Himebauch 92787dd0f0 ci: run unit tests in two parallel forks (#1737)
* ci: run unit tests in two forks

* test: start spatial controller mode tests with glasses output selected
2026-10-28 17:35:11 -04:00
Kurt Himebauch aef1f85aa9 ci: run store engine host tests in a parallel job (#1735)
* ci: run store engine host tests in a parallel non-gating job

* ci: harden the store tests job permissions, timeout and exit status
2026-10-28 16:47:42 -04:00
Kurt Himebauch 198f667bf0 feat: add GOG maintenance 2026-10-28 16:10:17 -04:00
Kurt Himebauch 52fa41767c fix: stop stale Android readbacks snapping Steam's brightness slider back (#1731)
* fix: stop stale Android readbacks snapping Steam's brightness slider back during a held stick

* fix: let deploy_fast sign with the release env on macOS and Linux

* fix: honor DROIDDECK_SIGNING_ENV first and cover the env signer in tests

* fix: honor DROIDDECK_SIGNING_ENV first and cover the env signer in tests

* fix: retry held brightness readbacks, use a monotonic hold clock and resolve the signing env path

* fix: resolve the build-tools path before signing changes directory

* fix: pick the newest build-tools numerically and keep the env file from replacing them

* fix: only pick build-tools that have aapt, zipalign and apksigner
2026-10-28 15:25:48 -04:00
Kurt Himebauch 6469a7693c ci: check out without history blobs or the release-note GIFs (#1725) 2026-10-28 13:11:14 -04:00
Kurt Himebauch cd1caf523d Default to Native background behavior and automatic PiP (#1727) 2026-10-28 13:10:08 -04:00
Kurt Himebauch 37e0f30cc6 Reset a game's Wine prefix from Game data (#1724) 2026-10-28 13:09:47 -04:00
Kurt Himebauch 23811147aa Guide the child-process toggle when developer options are off (#1726)
* feat(setup): guide build number and the child-process toggle when developer options are off

* fix(setup): only guide the child-process toggle on devices that have it
2026-10-28 13:01:44 -04:00
Kurt Himebauch 27bdc68867 ci: faster pipeline and fewer network flakes (#1719) 2026-10-28 11:31:20 -04:00
Kurt Himebauch 6ecf1ede2c tool: deploy session helper changes by grafting them into the latest main APK (#1721) 2026-10-28 11:07:12 -04:00
Kurt Himebauch 9294a60172 perf(proot): accelerate and cache glibc syscall relays (#1687)
Libc-internal file operations bypass the existing preload wrappers and
still stop in PRoot. Patch recognized ARM64 SVC sites in libc and the
runtime loader, reuse the current translator for 20 path syscall
numbers, and return declined calls to PRoot. Enabled by default after
the tracer-key check; `PROOT_FP_PATCH_SVC=0` remains the troubleshooting
opt-out.

The hardened implementation patches 47 sites on Thor's glibc 2.43.
Relays preserve general/SIMD registers, flags, FP state and errno, and
provide unwind metadata back to the original libc frame. Text is patched
through `/proc/self/mem` after relay pages become executable.
Initialization skips multiple live threads and SVE/SME. Alternate signal
stacks and insufficient remaining stack use PRoot; main-stack growth
respects the current limit. No proroot code or binaries are imported.

Robust-list set/get sites deliberately remain with PRoot/the kernel.
Device tests reproduced stale state when the earlier private-TLS
implementation mixed patched and raw callers. Removing that shortcut
fixes coherence and preserves the host's actual return policy; the
earlier thread-creation gain is retired. SDK forwarding still enables
faccessat2 ENOSYS fallback and clone3 ENOSYS only before Android 15.
Successful clone3 is never executed from the C relay. Existing fsync
handling is unchanged. No in-process RET_TRAP filter is added, so
SIGSYS-mask/handler machinery is not applicable.

Version-2 build-ID/API tables cache relative patch sites in `/tmp`.
Every hit revalidates executable bounds, alignment, instructions and
syscall numbers. Invalid/unsafe files rescan; publication uses a private
temporary file and atomic rename. The PRoot dependency fetch also gains
a mirror matching the existing pinned checksum, resolving upstream
availability failures without changing versions.

Five alternating pairs using the final bundled library on Thor/Android
13:

| operation | wrappers only | hardened relays |
|---|---:|---:|
| libc-internal stat | 49.6 us | 6.0 us |
| libc-internal open/close | 50.4 us | 7.8 us |
| libc faccessat | 49.7 us | 22.0 us |
| libc getcwd | 54.9 us | 5.0 us |
| thread create/join | 136 us | 144 us |
| complete tiny exec/wait | 4.44 ms | 5.60 ms |

An earlier five-pair hardened-library run measured stat 50.1 to 5.9 us
and open/close 51.9 to 7.1 us. The metadata gains survive the safety
checks. Neither run shows a thread-creation gain, and tiny execs retain
roughly 0.9-1.2 ms overhead.

Three full Steam startup pairs yielded Home-ready medians of 47.02 s off
/ 45.25 s on, with overlapping 43-50 s ranges. Steam/CEF-only PSS
medians were 1,223.1 / 1,233.9 MiB; the roughly 11 MiB difference is
smaller than observed variation, but does not establish zero memory
cost. Startup/memory used one unchanged signed bundled-library APK
without overrides; the final stack-gap guard only increases distance
reserved above other mappings. No reliable whole-client startup
improvement is claimed.

Earlier fresh Half-Life SD installs took 350/341 s off and 326/327 s on.
These are historical results from the preceding robust-list revision,
not a post-hardening download retest. Network/shader scheduling varied,
allocation did not reliably improve, and the repeat baseline committed
faster. This PR demonstrates metadata acceleration; it does not
demonstrate a reliable fix for Steam's SD allocation/commit bottleneck.

Validation on `2c3c135b` (production code unchanged since `4998b046`):

- Local release build, runtime/native-library checks, release signing
and four focused Android tests pass. [Final-head
CI](https://github.com/Droid-Deck/DroidDeck/actions/runs/40704837477)
passed: 1,431 Python/native tests (49 skips), 2,648 Android tests (zero
failures/skips), and 162 Rust tests. CodeRabbit is green on the same
head. The installed production APK forwards API 33 and patches 47 sites
with an unset enable flag and no override; Steam Home renders.
- Embedded and shared-PIC matrices pass on glibc 2.43; shared-PIC also
passes on separately loaded glibc 2.39. Coverage includes raw
results/errno/registers/FP state, path/flag/error and protected-pointer
cases, exact readlink boundaries, mixed raw/relay robust state,
concurrent threads, fork, thread churn, alternate-stack signals,
deferred/FIFO cancellation, signal siglongjmp recovery, forced unwind
with caller cleanup, and limited remaining stack.
- Four device test-process filters deny the memory patch file, relay
mapping, executable permission, or patch writes. Every case leaves
original syscall execution working. Host injection also verifies partial
installation after an individual site fails.
- Concurrent atomic symlink replacement passes 3,000 internal
stat/readlink iterations at default/zero TTL. Eight focused host tests
pass, covering instruction/branch limits, stack/limit fallback,
untrusted cache files and NDEBUG checks. API selection gives 45/47/46
sites for unknown/33/simulated-35; unset and explicit 1 agree, and 0
leaves sites untouched. This does not replace Android 15 hardware
testing.
- All bench tools compile with -DNDEBUG. Relative/absolute fixture
workloads agree, and thread/ABI/failure checks pass on Thor with those
builds.
- Earlier internal fts/realpath/NSS/dlopen and normalized read-path
equivalence pass. The write suite retains the existing wrapper SD
Unix-socket mismatch against PRoot.

CodeRabbit disposition: fixed relative fixture paths, prevented
benchmark checks disappearing under NDEBUG, corrected the
limited-fallback description, and narrowed memory-file denial so the
thread check still succeeds. The default-off recommendation was
addressed by fixing/testing the underlying guard and unwind issue while
retaining the requested default-on behavior. The readlink count
suggestion is intentionally not adopted: device boundary comparisons
reproduce current PRoot behavior, so changing that inherited policy
belongs in a separate correctness change.

This is not consequence-free instruction patching: constructor time,
private text copies and stack checks have costs. Android 15+ ARM64 and
16 KiB-page hardware, arbitrary asynchronous cancellation/non-local
exits, user-managed stacks, and attributable tracer-counter measurements
remain gaps. Loader calls before the constructor and arbitrary guest ELF
syscall sites remain outside scope.

Thor was restored to its original APK and bundled library, with app data
preserved and test settings, overrides, temporary files and version-2
test caches cleared.

Detailed evidence and revision boundaries: [proroot
analysis](https://github.com/Droid-Deck/DroidDeck/blob/2c3c135b52c3dd949ef5a99a67d8c12eba24406c/docs/development/proroot-analysis.md).
2026-10-28 10:53:26 -04:00
Kurt Himebauch 2c3c135b52 test(proot): address benchmark review findings 2026-10-28 10:19:29 -04:00
Kurt Himebauch ac8d97e098 test(proot): target memory patch access denial 2026-10-28 10:11:05 -04:00
Kurt Himebauch 4998b04636 fix(proot): harden relay state and unwinding 2026-10-28 10:06:44 -04:00
Kurt Himebauch 4aa19b6a39 fix: cancelling the first-run desktop or Proton install stops its download (#1042)
Pressing Cancel (or B) while a session is installing the desktop or the
Steam-mode Proton seed now stops the download. Launching again right
after no longer opens a session screen that closes itself.

Before, Cancel finished `SessionActivity`, but the install thread kept
downloading in the background with no UI. Two things went wrong:
- `Downloader` and `DesktopCatalog.install` had no way to be cancelled.
- "an install is running" lived in a per-activity field
(`installingRuntime`). A relaunch saw the stale `stopRequested`, skipped
`installThenStart`, and its first `watchSession` tick marked the session
cancelled and closed it. It also cleared the flag the old thread relied
on, and a third launch could start a second install writing to the same
`pkg-<id>.download`.

Changes:
- `Downloader.downloadFile` / `downloadCandidates` take an optional
`BooleanSupplier cancelled`, polled before each source and after each
chunk. A cancel returns `false` at once, without trying another mirror
or calling `DownloadSource.failed()`. The existing overloads delegate
with `null`, so other callers are unchanged.
- `DesktopCatalog.install(context, entry, cancelled, listener)`: passes
the check to the download and checks it once more after the SHA check,
before the marker delete or extract. Extraction itself still runs to the
end. The old 3-argument overload stays, so `MainActivity.installPackage`
is unchanged.
- `SessionActivity`: `installDesktop` / `installProton` pass `{
SessionState.stopRequested }`. `installingRuntime` now reads the
process-wide `SessionState.installing`.
- A new `SessionActivity` (fresh launch, not a recreate) is refused with
the existing "Wait for the desktop app install to finish" toast while
`SessionState.installing != null && !running`.
`MainActivity.startActivity` applies the same check before the launch
animation.
- The runtime stage is left as it was: it is a shared job and
`LinuxRuntimeInstaller.isBusy()` already blocks relaunch.

Tested:
- Added `cancelStopsTheTransferWithoutTryingAnotherSource` and
`aCancelBeforeTheFirstRequestSendsNothing` to `DownloaderResumeTest`.
That class passes (9/9), as does `DesktopCatalogTest`, and the app
compiles.
- Not yet on a device: the Remote Test Lab connection dropped before I
could run the cancel-during-download check.

## Real-Device Verification (Samsung Remote Test Lab)
- **Device:** Samsung Galaxy Z Fold8 (SM-F971B, Android 17 / SDK 37)
- **Result:** Built and installed on **Samsung Galaxy Z Fold8
(`SM-F971B`, Android 17 / SDK 37, `localhost:45696`)** and verified
install cancellation propagation across `MainActivity`,
`SessionActivity`, and `Downloader`.
2026-10-28 10:04:06 -04:00
Kurt Himebauch 8f6b2a4123 feat(itch): update detection and safe updates for installed games (#1665)
## Summary

Adds update detection for installed itch.io games. This is PR4 of the
itch.io store rework (browse, filters, game page and collections are
merged; Linux support is #1589). Butler patching comes later as an
add-on pack. Updates here download the full build.

## What changed

- `ItchUpdates` (new): compares each installed itch game with the
uploads itch lists for it (`GET /games/{id}/uploads`), using what the
install already saves: `installVersion` (the upload's `updated_at`) plus
`gameId`, `downloadKeyId` and `uploadId` in `extra`.
- The installed upload is still listed and has a later `updated_at`: an
update.
- The installed upload is gone: the newest Windows upload is offered as
a replaced build.
- An install without an `uploadId` compares the newest Windows upload's
date.
- An unknown installed version, or no answer from itch, never reports an
update.
- The check walks installed games one at a time, one second apart, and
logs `itch.io: checked <n> installed games, <m> with updates`. It runs
when the itch tab opens (at most every 6 hours) and from the new button.
A game itch did not answer for keeps its last result. Results are cached
in the itch preferences until the update is installed or the account
signs out.
- Library: games with an update show an "Update" label on their tile and
row. A Check for updates button in the Library header (itch tab only)
runs the check on demand, shows progress, and ends with a toast:
"Updates available: N" or "Up to date".
- Game page: an Update button appears for installed itch games with an
update. A replaced build asks first and names the new file. A plain
newer build updates directly.
- Safe updates: an update never extracts into the live game. The archive
is unpacked into `.droiddeck-update` inside the game folder, and the new
build must contain an executable before any game file is touched. A
failed or cancelled download or extraction removes the staging folder
and leaves the game as it was. Then `ItchStaging.merge` moves the staged
files into place with renames inside one folder. A staged file replaces
the file with the same path, directories are merged, and anything the
new build doesn't contain (saves, settings, install record, launcher,
art) stays. The install record is marked updating just before the move,
so an interruption there is resumable.
- New strings `stores_itch_updates_found` and
`stores_itch_update_replaced` in all 33 locales. The button label, toast
and dialog buttons reuse existing strings.
- `docs/development/itch-updates.md` describes the feature.

## Notes

- One extra copy of the build's size must be free while an update runs.
- Files the developer removed in the new version stay in the game
folder, because everything the new build doesn't contain is kept.
- The final rename step is not an all-or-nothing swap. A crash or power
loss inside it leaves the game marked updating and resumable.
- The update reruns the normal install over the game's own folder and
storage location.

## Tests

- `ItchUpdatesTest` (Robolectric, like the other itch tests): newer,
same and older stamps, replaced upload falling back to the newest
Windows build, unknown installed version, no answer, an install without
an upload id, and the cache round trip.
- `ItchStagingTest`: new files replace old ones, other files stay, and a
file replacing a directory and the other way round.
- Not run on a device or compiled locally; relying on CI. The controller
focus behaviour of the new buttons is untested.
2026-10-28 09:36:19 -04:00
The412Banner 9d82df6981 fix(game-env): OpenGL games stop waiting for each screen refresh (#1715)
With no frame limit, AIO's Direct3D 7 (DirectDraw) test ran at exactly
144 fps on a 144 Hz panel: every frame waited one refresh (6.94 ms).
Wine asks for a swap interval of 1 on every OpenGL window, and WineD3D's
front-buffer drawing used by DirectDraw and Direct3D 1-7 never lowers
it; it only sets the interval in its present path. Mesa's Zink then
presents in FIFO.

droiddeck-game-env sets vblank_mode=0 for games launched through Proton
before the game's environment edits, so a value set in the launch
options or the editor wins and an editor Remove leaves Mesa's default.
Steam's own runs in compatdata/0 are unchanged. Steam's Frame Limit
still holds a game: gamescope's WSI layer presents every swapchain in
FIFO while its limiter is on.

On the AYANEO Pocket FIT, AIO Direct3D 7 went from 144 to 2,345 and
2,265 fps (1 % low 99 -> 620) with no limit; with the Quick Access
limit at 144 and 72 it held at 143.76 and 71.92.
2026-10-28 13:34:45 +00:00
Kurt Himebauch ddb8ed32cb test(proot): record expanded relay validation 2026-10-28 08:05:50 -04:00
Kurt Himebauch 40b7c480a3 fix(build): mirror pinned talloc downloads 2026-10-28 07:44:59 -04:00
Kurt Himebauch 9a8f54dee7 fix(proot): preserve cleared robust-list state 2026-10-28 07:31:52 -04:00
Kurt Himebauch c62b9b5d1b perf(proot): expand and cache syscall relays 2026-10-28 07:17:53 -04:00
Delete228 b245017593 Merge upstream main and preserve routed Home input
Integrate upstream 213c2da5, keeping Home/Guide player-slot handling compatible with remapped controls and disabled routing. Preserve the input-settings overlay gate for Home and Steam volume keys.

Validation: 2,707 JVM tests across 389 suites pass. Device validation and latency measurements were not repeated.
2026-10-28 12:45:41 +03:00
The412Banner 213c2da57e fix(steam): answer Steam's D3D driver query without starting Wine (#1708)
At every start, Steam's shader pre-caching runs d3ddriverquery64.exe
through each Proton tool (DroidDeck-Proton and the ARM64 Proton) in
compatdata/0. Here the query has never succeeded (948 of 948 runs in
the client's shader log failed, mostly exit code 1), and each run
started Wine in that prefix, where the two Protons upgraded it back and
forth (11.0-100 <-> GE-Proton11-11) with the 32-bit wine.inf install.
When that overlapped a game's start, AIO ran 25-30 % slower for its
first 5-7 seconds.

Both Proton wrappers now exit 1 for Steam's own query, "run
.../d3ddriverquery64.exe" (or the 32-bit name) with STEAM_COMPAT_DATA_PATH
in compatdata/0, before doing anything else, so Steam gets the same
failure without Wine. Every other verb and prefix goes on to Proton.
2026-10-28 09:21:49 +00:00
The412Banner 8549ac9ce8 perf(compositor): draw a new frame when it arrives (#1707)
A new game frame waited for the next screen tick before the compositor
drew it: 1.9-3.5 ms on average at 144 Hz, measured with the pacing
counters. With frames going straight back to the game (no compositor
limit), the compositor now draws as soon as the clients' requests are
handled, at most once per refresh, and presentation times stay on the
tick.

gamescope delivers about one frame per refresh, so the tick leaves a
frame younger than one refresh for the next arrival to draw, unless the
tick itself changed the window, effects or headset pose; a frame no
other frame follows is drawn at the tick after. Once a held frame is
still waiting at the next tick (a client paced by frame callbacks), the
tick draws every frame for a second before holding one again. The
queued draw checks again that drawing on arrival is still on.

Paced output (glasses), streaming and frame generation keep drawing on
the tick. The hidden pref renderOnCommit (default on) turns it off. The
pacing log line counts the scenes drawn on arrival. On the device new
frames wait 0.12 ms instead of ~2 ms.
2026-10-28 09:21:07 +00:00
The412Banner 8c4f97a170 perf(gamescope): p18 – handle a finished frame in one loop pass (#1706)
0123 backports upstream 0efe47c and 666c52b by Matthew Schwartz (commit
completion list lifetime, no Rc to a dying commit). 0124 polls a new
commit's fence at import; a finished frame is marked done at once and
handled in the same steamcompmgr pass instead of a second wake through
the image waiter. commit_ready_check (default on) turns it off.

Pins gamescope 3.16.29-p18 (0123 + 0124). On the AYANEO Pocket FIT about
81 % of commit fences are ready at import; gamescope-xwm 22-26 % -> 16 %
of a core, gamescope_img 10-11 % -> under 2.5 %.
2026-10-28 09:20:51 +00:00
FX192 52e9810fbc Pick the newest Windows upload across types for replaced builds and let an update past its commit point finish 2026-10-28 09:00:36 +03:00
FX192 9b44bf798d Offer only Windows builds as a replaced itch upload and stop a cancelled update from completing 2026-10-28 08:47:50 +03:00
Kurt Himebauch f4656ad83a fix: show every held volume step in Steam's volume slider (#1704) 2026-10-28 00:01:00 -04:00
Kurt Himebauch 3fd9662161 fix: show every held volume step in Steam's volume slider 2026-10-27 23:48:05 -04:00
Kurt Himebauch 8164f9fae1 feat(session): manual pause button in the session menu (#1701) 2026-10-27 22:34:00 -04:00
Kurt Himebauch 374d4783fc fix: avoid incompatible Turnip selection and explain Steam startup failures (#1380) 2026-10-27 22:29:40 -04:00
Kurt Himebauch 3716c8acdf fix: an unproven update-channel guess no longer moves stable users to Nightly (#1067) 2026-10-27 22:27:20 -04:00
Kurt Himebauch 3cb7022cc2 merge main into feat/manual-pause 2026-10-27 22:26:02 -04:00
Kurt Himebauch eceda6e840 Add opt-in game debug logs and bounded log sharing (#1061) 2026-10-27 22:22:33 -04:00
Kurt Himebauch bc094d1920 fix(session): keep wake locks released under a manual hold and retry the paused frame 2026-10-27 22:06:05 -04:00
Kurt Himebauch 82c07c4be7 fix(library): keep installed/all owned filter when stores tab is off (#1700) 2026-10-27 21:43:36 -04:00
Kurt Himebauch c5cd52b5c9 feat(session): manual pause button in the session menu for every suspend policy 2026-10-27 21:40:53 -04:00
FX192 0e173ed05a Block itch updates while the game is being uninstalled 2026-10-28 02:13:48 +03:00
The412Banner 4ef6b53eb0 fix(proton): Windows CPU 0 is the fastest core (#1697)
Wine numbers the Windows CPUs like the host, so Windows CPU 0 was host
core 0, a small core on most Android chips. Games that pin their main
thread to CPU 0 ran it there: GTA IV 32 fps on the AYANEO Pocket FIT,
61 fps once CPU 0 is the fastest core.

droiddeck-game-env hands Wine the game's cores fastest first, from each
core's cpu_capacity (or its top clock). Equal cores keep their order.
Cores picked globally or in a game profile stay the only cores the game
sees, now fastest first; a pick only counts when it narrows the cores.
Games on Proton's own default_cpu_limit list keep their count on the
fastest cores (quoted and unquoted counts); other values such as "P"
are left to Proton. A typed WINE_CPU_TOPOLOGY / PROTON_CPU_TOPOLOGY
still wins, an empty one turns it off for a game, and
DROIDDECK_FASTEST_CORES=0 turns it off. Steam's own Proton runs in
compatdata/0 are left as before.

Builds on maxjivi05's "Windows games see only the cores they are pinned
to" (9d85af75).
2026-10-27 19:07:59 -04:00
FX192 ab130c7754 Forget a pending itch update when its game is uninstalled 2026-10-28 01:55:02 +03:00
FX192 f371149e1f Keep launch options across itch updates and stop cancellation from racing the file merge 2026-10-28 01:29:46 +03:00
FX192 bcec13ab63 Keep an updating itch game's folder out of every failed-download cleanup 2026-10-28 01:14:18 +03:00
FX192 29e8b2f679 Never delete the install record while replacing it and clean the staging folder if marking fails 2026-10-28 01:10:47 +03:00
FX192 e7907d0c74 Check itch updates per game from the game page instead of from the Library header 2026-10-28 01:08:33 +03:00
ovurrsl 42b799c194 runtime: finalize package markers once extraction completes 2026-10-28 00:05:04 +03:00
ovurrsl 1b5cd6add9 fix(install): recheck cancellation after sha256 and wire package resume cancel 2026-10-27 23:51:46 +03:00
ovurrsl 36c8ea2110 fix(update): keep preview-only channel guess transient 2026-10-27 23:41:40 +03:00
ovurrsl 97480e5e83 Merge remote-tracking branch 'origin/main' into fix/update-channel-guess 2026-10-27 23:38:04 +03:00
kingokksa 4f58230a20 fix(session): look up the launched game without rescanning the library (#1595) 2026-10-27 15:53:31 -04:00
maxjivi05 2d19c31872 fix(release): rename the pin task affinity for variant APKs 2026-10-27 15:50:51 -04:00
Kurt Himebauch c9044a27e3 fix(release): rename pairing guide affinity for variant APKs 2026-10-27 15:16:58 -04:00
maxjivi05 10174c1bd5 fix(manifest): name the pairing guide's task from the application id 2026-10-27 15:16:58 -04:00
Kurt Himebauch da3b8017cb fix(steam): hide duplicate Android volume indicator in Steam sessions (#1689) 2026-10-27 15:00:13 -04:00
Kurt Himebauch e906d2f077 feat(launcher): full launcher Apps page (#1686)
* feat(launcher): apps page with search, sort, filter, folders, widgets and page swipes

* feat(launcher): app shortcuts, pinned shortcuts, hidden apps and LauncherApps listing

* fix(ui): let page content scroll to the bottom edge

* feat(launcher): accept widgets other apps ask to pin

* i18n(launcher): translate apps page strings

* fix(launcher): keep disabled pins, serialize widget adds, add many plurals

* fix(launcher): drop in-flight pin refreshes after unpinning

* fix(launcher): skip unpin when pinned shortcuts can't be read

* fix(launcher): drop in-flight app lists when home role is lost

* fix(launcher): only migrate folder entries with one launcher activity
2026-10-27 14:50:11 -04:00
Kurt Himebauch 785d1bf1da perf(proot): enable syscall patching by default 2026-10-27 14:29:58 -04:00
Kurt Himebauch 4b230cbd0f perf(proot): add opt-in glibc syscall patching 2026-10-27 14:06:52 -04:00
Kurt Himebauch ef9866e423 fix(input): stop controller rumble when the device sleeps (#1668) 2026-10-27 13:14:23 -04:00
Kurt Himebauch f07dd5eb97 feat(setup): guided Wireless debugging onboarding for the child-process limit (#1682)
* feat(setup): guide wireless pairing from a picture-in-picture window

* feat(setup): share one child-process fix flow between Setup and Play Steam

* fix(setup): read the child-process limit from the settings provider

* fix(setup): re-check the child-process limit while Setup is open

* fix(setup): skip a dead saved pairing and quiet the pairing guide

* fix(setup): handle repeat guide launches and start pairing after the display choice
2026-10-27 12:33:10 -04:00
The412Banner 067dabe81c perf(compositor): screen tick on its own thread, pacing counters (#1678)
The Choreographer frame callback that drives the compositor ran on the
app's main thread, so main-thread work delayed the tick before it
reached the compositor thread. It now runs on a display-priority
HandlerThread with its own Choreographer; the session pref vsyncThread
(default true) switches back to the main thread. Each resume posts a new
callback with its own generation, so one left queued from an earlier
resume, on either thread, stops itself instead of ticking twice.

A new 10 s "pacing" log line reports how late the tick reaches the
compositor, how long a new frame waits to be drawn, game frames against
buffers re-attached after an empty attach, empty attaches and distinct
buffers, and presentation feedback shown, replaced before drawing and
not drawn.
2026-10-27 12:32:15 -04:00
Kurt Himebauch 378beacfac fix(ui): portrait nav highlight drift and blurry Android app icons (#1677)
* fix(ui): keep nav highlight on its tab while the bar scrolls

* fix(ui): render Android app icons at display density
2026-10-27 10:15:05 -04:00
Kurt Himebauch e8bfe0c4e4 fix: hold Home as Steam on Retroid Nova pads (#1674)
* fix: hold Home as Steam on Retroid Nova pads

* fix(input): track Home Guide apart from controller Guide

* fix(input): keep held controls on Home cancel and resume on Home
2026-10-27 10:14:43 -04:00
The412Banner b5cc6dd185 perf(gamescope): skip the repeated window lookup and wake on host events (#1672)
The WSI layer sends override_window_content on every present, and
gamescope walked the X tree to the top-level window (a blocking
XQueryTree round trip per level, under the wlserver lock) before
noticing the same swapchain already overrides it. A swapchain repeating
the same window now skips the walk for 100 ms (0121).

Backport of upstream gamescope 2962992 by Matthew Schwartz: the nested
Wayland backend's input thread reads the host display, but steamcompmgr
does not watch that fd, so buffer releases and presentation feedback
could wait up to one refresh for its next poll; the input thread now
wakes it after each read (0122).

Pins gamescope-3.16.29-p17, the archive that was A/B-tested on the
device. PATCHES.md also describes the p16 change to 0120.
2026-10-27 09:10:53 -04:00
FX192 2cce33d6f8 Keep the install record out of update merges and drop update results that went stale mid-check 2026-10-27 14:53:06 +03:00
FX192 efaf5bf9b7 Make staging cleanup symlink-safe and drop update checks that outlive a sign-out 2026-10-27 14:43:13 +03:00
FX192 e34db5adcb Count an itch install without a game id as unanswered in the update pass 2026-10-27 14:41:38 +03:00
FX192 0a7397cd0e Fix the itch update cache reader for Kotlin 2.1 and keep symlinked staged entries out of the merge recursion 2026-10-27 14:25:42 +03:00
Kurt Himebauch 0f839bef5d fix(ui): bloom into the compatibility tools loading screen (#1666) 2026-10-27 07:23:47 -04:00
Delete228 ed121f247c Fix routed QAM publication and redundant input state writes 2026-10-27 14:23:37 +03:00
FX192 c90ad997b1 Unpack itch updates into a staging folder before touching the game 2026-10-27 14:00:29 +03:00
FX192 8a16bd3b3b Detect itch.io updates and offer to install them
Installed itch games are compared with the uploads itch lists for them. A game with a newer upload shows an Update label in the Library and an Update button on its page, a replaced build asks first, and a Check for updates button in the itch Library header runs the check on demand.
2026-10-27 13:53:35 +03:00
Delete228 92b4361100 Merge upstream main into unified input profiles
Integrate screen keys, portrait controls, and Home/Back shortcuts with routed input. Preserve remapped axes, neutral gating, and explicit Back bindings; discard stale queued chord reports when routing changes.

Validation: 2,632 debug JVM tests across 385 suites pass with Java 17 and skipRust. Device testing and latency measurements were not repeated for this merge.
2026-10-27 13:14:29 +03:00
The412Banner 43a0ce93f9 fix: Frame Limit sync and refresh follow on a still screen (review follow-ups) (#1663)
The Steam helper checked the Frame Limit only when the offered frame
rates changed. With the limit off during a refresh change, turning it
back on left a value the new rate does not offer, and a failed write was
not tried again. It now checks on every state change and every 5 s, and
waits for Steam's settings write so a rejected write is caught and tried
again on the next check.

gamescope 3.16.29-p16 (0120 changed): gamescope took a new host refresh
only from the presentation feedback of a frame it drew, so with nothing
moving on screen it kept the old rate. It now presents once when the
host's wl_output mode changes and keeps presenting while a new rate is
pending, so the 300 ms check runs on a still screen too.

Found by CodeRabbit in the reviews of #1650 and #1663.
2026-10-27 05:08:21 -04:00
Kurt Himebauch 07a0a7e344 fix: show gesture settings only in gesture mode (#1654) 2026-10-27 00:23:44 -04:00
maxjivi05 c25d54aced fix: leave published component choices alone when state.json cannot be read 2026-10-27 00:07:45 -04:00
maxjivi05 c781cfd525 fix: publish component choices again when a new Proton build appears
Game and global component choices were written to desired.tsv only when a
selection changed, so a Proton build installed later (DroidDeck-Proton
11-11.8) had no rows and kept its bundled FEX. Battle.net's d2rfix FEX never
reached it and StarCraft died in ClientSdk.dll's DllMain. The choices are now
republished at session start and on Battle.net's FEX request whenever the
installed tools no longer match what was published.
2026-10-27 00:07:45 -04:00
The412Banner 4c7e59ed8b feat: Steam's Frame Limit decides; gamescope and the limit follow the refresh rate (#1650)
Remove the Performance mode switch. The session always runs the way
Performance mode did: replaced buffers go back to the game at once
(except while mirroring to glasses), gamescope's WSI layer and winex11
reuse their per-present window checks, and games get eight swapchain
images while uncapped. Steam's Frame Limit in the Quick Access menu is
the only limit.

gamescope 3.16.29-p15 (p14 plus two patches):
- 0119: with the Frame Limit on, gamescope forces FIFO while the WSI
  layer hands the driver MAILBOX, so a game with eight images could
  queue up to seven frames ahead of the screen. While the limiter forces
  FIFO the layer uses its default of 3 images; DXVK and VKD3D-Proton
  recreate their swapchain when the limiter changes, other apps are asked
  to with VK_SUBOPTIMAL_KHR.
- 0120: -r fixed gamescope's pacing and limiter rate for the whole
  session. With GAMESCOPE_NESTED_REFRESH_FOLLOWS_HOST=1 a rate the host
  has reported for 300 ms replaces it (a switch from 144 to 120 Hz passes
  through 72 Hz for a few frames), and Steam gets the display information
  again. The session sets it whenever it passes -r.

Steam keeps its Frame Limit as the client setting
gamescope_app_target_framerate and offers the values gamescope's display
state lists. After a refresh change, a limit the new rate does not offer
(72 at 120 Hz) left the slider on its first value while the game kept the
old limit. The Steam helper watches the offered values and, once they
settle, keeps the limit when it is still offered and moves it to the
highest value otherwise, through Steam's own setting so the slider shows
it. Steam's webpack does not expose its module cache, so the gamescope
service is found by the calls its module makes, and the helper retries
the install every 10 s until Steam is ready.

The session window asks for the fastest mode the peak refresh setting
allows and chose again only when the display changed, so after it had
asked for 60 Hz, raising Android's refresh setting left the display,
gamescope and the limit at 60. It now also chooses again when
peak_refresh_rate or min_refresh_rate changes.
2026-10-26 23:17:35 -04:00
K11MCH1 e450765eb6 Mali bridge with under 4 GB of RAM: Steam's browser UI in software
On a 2.9 GB Mali-G76 MC4 phone the GPU-drawn Steam UI took about 400 MB,
and Android's low-memory killer ended DroidDeck itself while The Binding
of Isaac loaded; with the UI in software Isaac reached its title screen.
Below 4 GB of RAM, bridge sessions now default BL_VKBRIDGE_CEF_GPU to 0;
a value already set is kept. The software UI takes about a core while it
animates and idles while a game has focus.
2026-10-26 23:15:05 -04:00
K11MCH1 018d0e1db9 Steam UI on the Mali bridge: gamescope composites every frame
Through the Mali bridge the Steam UI flickered: frames alternated with
and without its footer while gamescope handed its layers to the phone's
compositor as separate planes. Turning gamescope's overlay planes off
nearly stopped it (2 flips), and a composited image has none (Mali-G76
MC4 r26p0 at rest: 145 footer flips in 6 s, 0 composited). Where in the
plane path it goes wrong is not known. Steam sessions on the bridge set
gamescope_composite_force=1 unless it is already set.
2026-10-26 23:15:05 -04:00
Kurt Himebauch 0e9a31ccfe feat: trust contributors for test builds (#1646)
* feat: trust contributors for test builds

* fix: restrict trusted list changes

* fix: retain same-repository test signing

* fix: publish completed fork builds

* feat: trust K11MCH1 test builds

* fix: recognize maintain collaborator role
2026-10-26 22:54:57 -04:00
Kurt Himebauch 8749154cbf fix(input): keep AYN system key out of Home shortcuts (#1643) 2026-10-26 21:11:46 -04:00
maxjivi05 bb9a64fa5f Reconfigure the app decoder when the capture layout changes
A capture format or buffer change now closes the app's decoder so the
next packet reopens it with the new stride and height, and a capture
slot whose remap fails no longer keeps its old mapping.
2026-10-26 20:36:24 -04:00
maxjivi05 916a39585f Decode Remote Play streams on the arm64 Steam client
Steam's arm64 streaming_client only decodes through a V4L2 stateful
decoder and libdrm dumb buffers, neither of which exist on Android, so
streams from a PC never showed a picture. The preload now offers it a
/dev/video-dec0 and a libdrm stand-in backed by dma-heap buffers, and
each frame is decoded by the app's MediaCodec (Qualcomm low-latency
mode) over the encoder socket, falling back to the FFmpeg in Steam's
runtime.
2026-10-26 20:36:24 -04:00
maxjivi05 9555cb9f65 fix(skate.): run ReSkate at D3D12 feature level 12_1
skate. under ReSkate crashed with an access violation in Skate.exe 12 to
60 seconds after it started at the default feature level 12_2; at 12_1
it loads San Vansterdam and plays. The rule matches Skate.exe with
ReSkate.dll beside it, so it covers ReSkate started from skate.'s own
Steam entry and as a non-Steam game, and a game profile can still set
another level.
2026-10-26 20:35:59 -04:00
maxjivi05 70d8fc6559 Hand a profile's WINE_CPU_TOPOLOGY to Proton as well 2026-10-26 18:31:11 -04:00
maxjivi05 70c512693a Set PROTON_CPU_TOPOLOGY too so Proton's per-game CPU list doesn't replace it
Proton sets WINE_CPU_TOPOLOGY from its own default_cpu_limit for games
like Prototype (8 cores) unless PROTON_CPU_TOPOLOGY is set, so the
pinned topology never reached the game the issue was about.
2026-10-26 18:31:11 -04:00
maxjivi05 43266b2923 Keep an empty WINE_CPU_TOPOLOGY from a game profile 2026-10-26 18:31:11 -04:00
maxjivi05 9d85af7558 fix(proton): Windows games see only the cores they are pinned to
Game cores pinned the game with taskset, but Wine counts the online
CPUs, so a game held to two cores still saw eight processors and sized
its thread pools for them. Prototype freezes at its splash screen that
way. When the session or a game profile limits the game cores, Wine now
gets WINE_CPU_TOPOLOGY for exactly those cores unless the profile sets
its own.
2026-10-26 18:31:11 -04:00
FX1922andmaxjivi05 d905f03bf7 feat(itch): collections filter in the Library (#1597)
* Add itch.io collections to the Library

A Collections button on the Library's itch tab filters the library to one collection of the signed-in account. Its games that are not installed show in the Not installed section.

* Keep itch collections complete across failures, sign-out and empty libraries

A failed later page no longer publishes a partial collection, selecting a collection refreshes it, pagination runs until an empty page, signing out clears collection state, and the itch chip shows for a signed-in account with no owned games.

* Tie itch collection requests to the account that started them

Requests capture the account generation before reading the key and dedupe per generation, sign-out clears collections after the token is removed, and the collections load follows the itch sign-in state.

* Keep paging itch collections past a page of already seen games

* Run the itch collections tests under Robolectric like the other itch tests

* Let the Installed filter apply inside a collection and keep the grid for an empty itch account

* fix(itch): stop paging a collection when a page adds no new games

* Keep the collections icon on the itch collections tab when it opens

---------

Co-authored-by: maxjivi05 <[email protected]>
2026-10-26 18:15:24 -04:00
Kurt Himebauch f13916ee25 feat(steam): optional Steam integration setup (#1637)
* feat(steam): add optional device integration setup

* feat(steam): show integration setup as inline settings rows

* fix(steam): offer App info when shortcuts stay blocked
2026-10-26 17:51:44 -04:00
Kurt Himebauch 73daaf7adc fix(gamescope): keep Fill scaling and magnification visible (#1634)
* fix(gamescope): preserve signed plane positions

* fix(gamescope): pin validated scaling component
2026-10-26 16:06:40 -04:00
maxjivi05 aa17b0553d fix(input): end the forwarded touchpad finger when the Steam menu opens 2026-10-26 15:52:02 -04:00
maxjivi05 46076e165b fix(input): let on-screen controls work while the touchpad finger is down
The game surface only took touches in gestures mode, so in touchpad and
direct mode a finger on the game left the window with no touch target and
Android sent every later finger to the touchpad, even over a stick or
button. The surface now owns those fingers in every mode, and the
on-screen and Steam touch controls hand fingers that miss a control back
to the touchpad instead of dropping them.
2026-10-26 15:52:02 -04:00
Kurt Himebauch 2d8957a401 feat(launch): show compatibility install progress (#1629) 2026-10-26 15:41:46 -04:00
Kurt Himebauch b3561bf658 fix(input): discard queued chords on disconnect (#1632) 2026-10-26 15:31:51 -04:00
Kurt Himebauch 13eb291ba8 fix(input): handle immediate Back chords and tap timing (#1628)
* feat(input): support instant Back chords and tap timing

* fix(input): send QAM before shortcut inputs

* fix(input): stop delayed stick input on Back release
2026-10-26 15:04:55 -04:00
Kurt Himebauch c8f650e167 feat(ui): add game resource links (#1630) 2026-10-26 14:53:21 -04:00
maxjivi05 c300342a29 Update MaliBridge to v0.5.4
v0.5.4 backs the exported memory with AHardwareBuffers on phones without a
dma-buf heap (older Mali such as the G68, G77 and G78) and fixes green and
red textures on the Dimensity 9000 series.
2026-10-26 14:08:24 -04:00
The412Banner 830c753312 fix: turn off D3DGear's capture hook when it sits beside a game (#1627)
The Crew 2 loads D3DGear64.dll from its folder. Its DXGI Present hook
faults over a thousand times within seconds under Wine and the game
never presents again: black screen while the sound keeps playing.
droiddeck-game-env now disables the DLL when it finds it beside the
game, and DLL overrides from several engine fixes are kept together
instead of the last one replacing the others.
2026-10-26 18:07:15 +00:00
maxjivi05 fdc4ff607d Credit MaliBridge by ASEbrahim 2026-10-26 13:55:43 -04:00
maxjivi05 3639196c83 Translate the Mali bridge settings 2026-10-26 13:55:31 -04:00
maxjivi05 9c8d2acacc Turn the Mali bridge on by default on a Mali GPU
A device with /dev/mali0 and no KGSL starts with the bridge on, so it draws
without a trip to Setup. An Adreno keeps Turnip and its automatic driver
route whatever the setting says, other GPUs keep the switch off until it is
turned on, and a choice made in Setup always wins. The GPU checks are read
once per process.
2026-10-26 13:55:31 -04:00
ASEbrahim 7aeb194d6e Mali old DXVK: a setting to keep Proton's own, and Proton's own back when the bridge is off
Setup has a "DXVK" row under the bridge's safe mode: automatic (the default, as before) or
Proton's own (SessionPrefs.bridgeProtonDxvk). With Proton's own, nothing is chosen and an earlier
automatic choice goes back to the Proton's copy.

Automatic choices also go back when the session is not a bridge session (the bridge turned off),
even when nothing is known of the Vulkan; a queued automatic swap is dropped with them, the
player's own queued swaps stay. Turning the bridge off, or choosing Proton's own, does it right
away in the background instead of at the next session. A failed detection in a bridge session
still keeps the last choice.
2026-10-26 13:55:31 -04:00
ASEbrahim 678e16af4d Mali old DXVK: a failed detection keeps the last automatic choice, one Proton failing does not stop the others, the bridge file's driver_api counts unless promotion=on
When nothing could be read (no apps-vulkan file and VulkanInfo failing), every automatic choice was
undone: a 1.1 phone went back to a DXVK that cannot start, and the next session flipped it again.
Now an automatic choice is undone only when the Vulkan is known and wants none.

Each Proton's swap or undo is on its own: a failure (a missing saved copy, an I/O error) is said in
the result and the remaining Protons still get theirs. When the Proton is the same but its saved copy
is gone, the choice stays and is said, instead of being forgotten with DXVK 1.10.3 still installed.

apps-vulkan: the version counts only with promotion=on; otherwise, and in files from servers before
the field (which wrote a promoted version even where it was not trusted), driver_api does. A file
whose driver_api is not the driver's own version (a driver update, a server rolled back) is left out.
2026-10-26 13:55:31 -04:00
ASEbrahim 9040f828e9 Mali bridge: DXVK 1.10.3 on phones whose Vulkan stops below 1.3
Proton's DXVK 3 refuses a device below Vulkan 1.3, without shaderInt64 or
with less than 256 bytes of push constants, so on a Mali with a 1.1 driver
(OnePlus Ace 2V, Mali-G710, r32p1) every D3D9-11 game exits. On a bridge
session the forced-package step now picks the newest DXVK that starts on
what games see: 1.10.3 below Vulkan 1.3, 2.6.2 on 1.3 without shaderInt64
or 256 bytes of push constants, Proton's own otherwise.

What games see comes from <bridge cache dir>/apps-vulkan (a bridge with
Vulkan promotion writes it after its self-test) or else from the phone's
driver (vkinfo now reports shader_int64 and max_push_constants). These
choices are marked automatic: a later session with another answer swaps or
restores them, and the player's own choice in Components is never touched.
2026-10-26 13:55:31 -04:00
Ahmad Sayed Alsayed Ebrahim e63fad1daa fix: droiddeck-session: ANGLE on Vulkan brings Chromium's Vulkan compositing
With BL_VKBRIDGE_CEF_ANGLE=vulkan the CEF GPU process stopped crashing but
Chromium still composited in software (gpu_compositing: disabled_software,
Steam "Disabling GPU acceleration: Disabled/RuntimeDetect", VizCompositorThread
at a full core). The GPU report shows why: "eglCreateWindowSurface failed with
error EGL_BAD_CONFIG". ANGLE's Vulkan backend on X11 gives every EGL config the
root window's visual; Chromium's X11 surface asks for a config whose
EGL_NATIVE_VISUAL_ID is the ARGB visual whenever a compositing manager owns
_NET_WM_CM_S0 (gamescope does), finds none and passes a null config.

bl_webhelper_layout() now adds +Vulkan +VulkanFromANGLE +DefaultANGLEVulkan
for ANGLE on Vulkan, so Skia composites through an xcb swapchain on ANGLE's
Vulkan device and no EGL window surface is needed. BL_VKBRIDGE_CEF_VULKAN=0
leaves it off; unset BL_VKBRIDGE_CEF_ANGLE still means Steam's stock layout.
2026-10-26 13:55:31 -04:00
Ahmad Sayed Alsayed Ebrahim e527d4a9f3 fix: droiddeck-session: BL_VKBRIDGE_CEF_ANGLE reaches Chromium through the web helper preload
Steam drops -cef-use-angle: on the bridge the CEF GPU process reports
ANGLE_OPENGL (ANGLE on Zink) with -cef-use-angle=vulkan on the command line.
An explicit BL_VKBRIDGE_CEF_ANGLE now also becomes --use-gl=angle
--use-angle=<backend> on steamwebhelper's own argv (stock process layout kept);
unset, nothing changes.
2026-10-26 13:55:31 -04:00
Ahmad Sayed Alsayed Ebrahim 30a70a0f7c Setup: show the stored Mali bridge switch in a new process; log the server's start time
FrontEndActivity read vkBridge and vkBridgeSafe only when the Performance page
opened, so after an app restart (every APK update) Setup showed
"Mali bridge: off" while sessions used the bridge. refresh() now reads them.
VkBridgeComponent logs how long the server took to listen.
2026-10-26 13:55:31 -04:00
Ahmad Sayed Alsayed Ebrahim 9c28b41d49 droiddeck-session: wait up to 10 s for the Mali bridge socket before falling back
The bridge check tested the server's socket once. It now polls every 50 ms
(BL_VKBRIDGE_WAIT_MS, default 10000) on the first pass, logs how long the
socket took, stops waiting when the session is told to stop, and names what
is missing (icd, library, socket) when it falls back.
2026-10-26 13:55:31 -04:00
Ahmad Sayed Alsayed Ebrahim 5b35ace098 Mali bridge: Setup's "Safe mode" switch, on by default
A second row under the GPU check, shown only when the bridge is on: "Safe mode" keeps the
bridge's proven workarounds wherever its startup probe has not verified this GPU and driver;
off is performance mode, native paths wherever the probe passed. It reaches the server as
<cache dir>/mode ("safe" or "performance", written atomically before the server starts or is
reused, so the in-app server's JNI start stays as it is) and the guest as VKBRIDGE_MODE, which
/root/.vkbridge-env can still override. A server without modes ignores the file.
2026-10-26 13:55:31 -04:00
Ahmad Sayed Alsayed Ebrahim e04eb84090 Mali bridge: keep the server's cache directory in the app's files, not its cache
Android empties an app's cacheDir when storage runs low; it took the bridge's pipeline
caches, the probe profile, stuck-pipelines and the env file from under a running server.
The directory is now files/vkbridge-pipelines (--cache-dir and the in-app start alike), and
an existing cache/vkbridge-pipelines is moved there once. Nothing trims filesDir, so the
server's own limits bound it (64 MB per pipeline cache file, a total limit in newer servers).
2026-10-26 13:55:31 -04:00
Ahmad Sayed Alsayed Ebrahim b3c5570bdd Mali bridge sessions: keep Steam's web helper GPU process out of process
Steam's web helper now runs its GPU work inside the browser process
(--in-process-gpu --no-zygote, #566). On the Mali bridge that browser stops
answering after its first window, Steam restarts it every 10 s and then quits
before the login page. Bridge sessions keep the stock process layout
(BL_WEBHELPER_ARGS=stock); BL_VKBRIDGE_WEBHELPER_ARGS overrides it for tests.
Adreno sessions are unchanged.
2026-10-26 13:55:31 -04:00
Ahmad Sayed Alsayed Ebrahim dae36b0e67 Mali bridge sessions: Setup switch, server, session wiring
On a GPU without KGSL, Setup offers an experimental Mali bridge switch
(off by default; ignored on Adreno). With it on, the session starts the
bridge server, points the Vulkan loader at the bridge ICD, names Mali's
device as the stand-in render node, runs the bridge self-test into the
session log, and caps vkd3d-proton's feature level at 12_0. Flatpak
sandboxes get the bridge ICD and socket. The DRM preload falls back to
flock in a bridge session when kcmp is refused, and a clock/thermal log
is kept beside the bridge logs.
2026-10-26 13:55:31 -04:00
Ahmad Sayed Alsayed Ebrahim 3943794b0e waylandcomp: import layer pool buffers as AHardwareBuffers off Adreno
A non-QTI gralloc handle (Arm/MediaTek gralloc on Mali) has no layout
the compositor can read, so the pool import guessed a linear dma-buf and
failed. On drivers other than Qualcomm's the compositor device enables
VK_ANDROID_external_memory_android_hardware_buffer and
VK_EXT_queue_family_foreign and imports such buffers as AHardwareBuffers;
the linear dma-buf stays as the last resort. Adreno/Turnip keeps the
same device and the dma-buf path.
2026-10-26 13:55:18 -04:00
maxjivi05 c883ce4439 CI builds the MaliBridge client as a component
The components job checks out external/MaliBridge and builds the guest
ICD, its manifest and the self-test as the vkbridge recipe; a submodule
counts in a recipe's identity by its pinned commit.
2026-10-26 13:55:18 -04:00
Ahmad Sayed Alsayed Ebrahim a69ce00b6a Build and package the Mali bridge from external/MaliBridge
The server is built with the NDK into libvkbridge_server.so (a program
the app execs from its native library directory); the Linux ICD, its
manifest and the self-test go into the runtime assets with the other
glibc preloads, locally and in CI.
2026-10-26 13:55:13 -04:00
Ahmad Sayed Alsayed Ebrahim 061ae17cc2 Add MaliBridge as a submodule at external/MaliBridge
MaliBridge (https://github.com/ASEbrahim/MaliBridge) is a Vulkan
command bridge that lets the Linux side draw on a non-Adreno GPU (Mali)
through the phone's own Vulkan driver. Pinned at v0.5.3.1.
2026-10-26 13:55:13 -04:00
Kurt Himebauch a0a68f7280 feat(steam): sync QAM controls and pass held Back shortcuts (#1621)
Steam's native QAM sliders now control Android brightness and media
volume, with device changes reflected back in Steam. Holding Android
Back for 700 ms sends a real held QAM button for Steam shortcuts. Single
and double taps keep the existing menu/QAM order and swap setting; the
double-tap window remains 500 ms.

- Discover native brightness through a guest backlight descriptor and
use Android's Modify system settings permission. Retain the latest
pending request until permission is granted or the session stops, with a
notification action for hidden sessions.
- Give DirectAudio and fallback AAudio sinks Android media volume/mute
callbacks, keeping software gain at unity. Recover when initial volume
state is absent or invalid, brightness acknowledgements differ or never
arrive, or the Display bridge is not ready yet.
- Forward other buttons while QAM is held in Steam Deck and Steam
Controller modes. Consume release after a hold and clear pending input
on overlays, focus loss, pause, disconnect, and teardown. The session
menu retains its compact Back choice without an explanatory paragraph.

Validation:
- Local ARM64 release build passed. The latest timing change passed 20
targeted input tests, including release at 600 ms remaining a tap. The
preceding full feature check passed 61 Android input/device-control
tests, with permission cases on API 28 and 33.
- 66 Linux Python/Node/C tests passed for device controls, audio
recovery, Steam compatibility, and file dialogs. Both audio sink modules
rebuilt with Android NDK 27.2.
- Release-signed integration build 14d12cf0 is installed on the unrooted
AYN Thor, carrying the Android/input changes through 749ade4a plus the
existing portrait changes. Device checks confirmed the configured
QAM/menu tap order, Back dismissal, removal of the inline menu
description, and held QAM overlapping X with clean release. Hands-on
testing confirmed this installed build works.
- Earlier Thor testing covered brightness permission and pending
requests, brightness synchronization in both directions, native volume
changes, hardware volume key readback, and both sink backends. Failure
recovery is covered by the automated tests above.

The subsequent queued-notification fix in 4d7ceb23 is covered by the
Linux regression suite and has not replaced the Thor install.

Bluetooth/wired output switching and other device models remain
untested. CI and CodeRabbit monitoring continue against the pushed head.
2026-10-26 13:46:14 -04:00
maxjivi05 455bf0fba9 fix(fex): install DroidDeck's FEX build again when it no longer loads
A tag that matches no longer vouches for the build: FEX and FEXServer must
also load (ld.so --list), otherwise the session reinstalls it and x86
programs use the Steam or system FEX meanwhile.
2026-10-26 13:40:36 -04:00
maxjivi05 8df90c30dd fix(fex): read the fallback FEX version from droiddeck-fex-update
The pin moved there from droiddeck-steam-x64, so the client settings sheet
and its test read it from the new script. The x86-64 client notes now name
the updater and Components' FEX watcher.
2026-10-26 13:40:36 -04:00
maxjivi05 4de4c65c2a fix: wait before retrying a FEX build that did not install
A failed catalog install is retried after an hour or when the session names
another build, a missing updater falls back to the FEX already here, and the
archive download gives up on a stalled connection.
2026-10-26 13:40:36 -04:00
maxjivi05 bb1cbf95c0 docs: credit sharun and userland-execve 2026-10-26 13:40:36 -04:00
maxjivi05 7e2d13b641 fix(fex): x86 programs use DroidDeck's own FEX build, kept to the catalog's version 2026-10-26 13:40:36 -04:00
maxjivi05 5bdf0d6b04 fix(fex): report the x86 platform under AT_PLATFORM 2026-10-26 13:40:36 -04:00
maxjivi05 13493d44c3 fix: keep Sentry for Godot games that are not .NET
The Sentry GDExtension was turned off for every Godot game that ships it,
because its crash handler takes the .NET runtime down under FEX. GDScript
games such as Pathogenic call SentrySDK from their scripts, so without the
extension their autoloads fail to parse and the game quits. Only Godot .NET
builds have it turned off now.
2026-10-26 13:27:59 -04:00
Kurt Himebauch 4d7ceb2325 fix(steam): drain queued control notifications 2026-10-26 13:27:03 -04:00
maxjivi05 a984d11742 fix: run Xbox games from a short drive letter
The launch view lives under the app's files folder, so games saw an
install path like Z:\data\user\0\com.droiddeck.launcher\files\xbox-runtime
\views\launch-<n>\droiddeck-xbox-<id>. Forza Horizon checks that length and
quits with "Installation path is too long". The view is now mapped to a
free drive letter in the prefix (X: first) for the length of the launch.
2026-10-26 13:21:31 -04:00
Kurt Himebauch 749ade4a45 fix(input): delay held QAM to 700 ms 2026-10-26 13:20:51 -04:00
Kurt Himebauch fa5506df35 fix(steam): preserve Back taps and recover QAM controls 2026-10-26 13:13:08 -04:00
Kurt Himebauch dde1a55d00 feat(input): pass held Back through as Steam QAM 2026-10-26 12:49:51 -04:00
maxjivi05 c446566f42 fix: hold a setup lock instead of guessing from running processes 2026-10-26 12:47:00 -04:00
maxjivi05 2e2b950964 fix: Flatpak setup clears a package lock left by an interrupted run 2026-10-26 12:47:00 -04:00
Kurt Himebauch 53d643dd00 fix(audio): clear volume bridge during sink teardown 2026-10-26 12:34:31 -04:00
Kurt Himebauch 6a5dfe1486 feat(steam): sync QAM brightness and volume with Android 2026-10-26 12:32:36 -04:00
Kurt Himebauch da127d1b4c fix: delay client compression until game focus (#1406) 2026-10-26 12:12:04 -04:00
maxjivi05 9b68b33e73 fix: pick the HUD surface by the scene's draw order 2026-10-26 12:02:17 -04:00
maxjivi05 1f60e57749 fix: time the HUD graph from the game's frames as shown on screen 2026-10-26 12:02:17 -04:00
Kurt Himebauch eaaecf0661 feat(session): add portrait sessions and controls below the game (#1612) 2026-10-26 12:01:44 -04:00
Kurt Himebauch d08afbd878 fix: restart compression delay for new games 2026-10-26 11:55:01 -04:00
Kurt Himebauch 36135327f5 fix: resolve zram focus conflicts 2026-10-26 11:45:17 -04:00
0x1Nawaf d65912f0c5 fix: stop starting a session on the slow proot (#1395) 2026-10-26 11:41:18 -04:00
Kurt Himebauch 5d945d7784 refactor: remove duplicate session frame limiter (#1616) 2026-10-26 11:25:31 -04:00
maxjivi05 ee1b3b26e1 fix: clip damage against the buffer committed with it 2026-10-26 10:50:19 -04:00
maxjivi05 9c02233b06 fix: clip damage to the surface and let the toplevel's size count for coverage 2026-10-26 10:50:19 -04:00
maxjivi05 f98c272300 fix: compare the window's shown size with the scene 2026-10-26 10:50:19 -04:00
maxjivi05 744a42d324 fix: count one shm window, only after new GPU frames stop, with damage in buffer pixels 2026-10-26 10:50:19 -04:00
maxjivi05 30da4d2bcd fix: count a desktop window game's frames for the performance HUD 2026-10-26 10:50:19 -04:00
maxjivi05 e492a78f55 fix: treat a product.db with a damaged install record as unreadable 2026-10-26 10:30:16 -04:00
maxjivi05 538b5c5270 fix: hide registry-only games when product.db lists none, keep the Agent off without the game storage card 2026-10-26 10:30:16 -04:00
maxjivi05 be5bec2d44 fix: keep every game library mounted for Battle.net's Agent and drop games it no longer tracks 2026-10-26 10:30:16 -04:00
maxjivi05 fd9de4a21e fix: list Battle.net's games in a grid that scrolls down 2026-10-26 10:10:31 -04:00
The412Banner 1b3814a587 Hold games to the frame limit in Performance mode (#1609)
A frame limit only paced what reached the screen: a game without vsync
kept drawing hundreds of frames a second that were thrown away (AIO at
a 60 fps limit drew 155 to 2,446 fps), so the limit saved no power.

With Performance mode on and a frame limit set, the session now sets
gamescope's per-game limit over gamescope_control (gamescopectl
debug_set_fps_limit). gamescope's limiter then has its WSI layer hold
Vulkan games, DXVK and VKD3D included, to the limit. gamescope's own
--framerate-limit option is reset on its first frame in 3.16.29, and
Steam resets the per-game limit while it starts, so the session watches
gamescope's limiter state file and sets the limit again whenever it is
off (a once-a-second check with shell builtins, no process started).
With Performance mode off, a limit paces the screen only, as before.

The session also gives gamescope a fixed limiter state file path, so
gamescope stops leaving a new one behind on every start, and removes
the old ones.

Device-proven on the AYANEO Pocket FIT (Turnip, AIO D3D11, 45 s runs on
battery, three runs each): at a 60 fps limit the game held exactly
60.0 fps and the device drew 7.08 W instead of 9.45 W, with 1.3 instead
of 2.6 busy cores and 5 instead of 11 % GPU load. No limit: unchanged.
2026-10-26 09:16:05 -04:00
Kurt Himebauch 78f95c5a0c feat(ui): support portrait launcher layouts (#1606) 2026-10-26 09:14:35 -04:00
maxjivi05 db56cff880 fix: check a Battle.net game for updates again when the last check failed 2026-10-26 09:01:02 -04:00
maxjivi05 24f0b97c08 fix: report a gamescope that cannot start and ignore Agent answers that are not objects 2026-10-26 09:01:02 -04:00
maxjivi05 3b6b304f55 fix: take the Agent's empty answer for an uninstalled game as gone
Once Blizzard's Agent has removed a game it answers GET /game/<uid> with
an empty object, not installed false, so a finished uninstall read as a
failure and opened Battle.net.
2026-10-26 09:01:02 -04:00
maxjivi05 c48297e0d1 fix: wait for a cancelled Battle.net job before removing it, and bound uninstall waits
A cancelled install now waits for its Agent job to end before the
uninstall runs, and removes the folder only once the Agent no longer
lists the game or the folder is empty. Uninstall gives up when Battle.net
or a game keeps the prefix busy for a minute, so the page falls back to
opening Battle.net. An uninstall only counts when the Agent reports the
game as not installed.
2026-10-26 09:01:02 -04:00
maxjivi05 b887e8653d feat: install Battle.net games from Stores with downloads, updates and uninstall
Battle.net games now install like Epic, Xbox and itch games: Install,
pick a location, and the download runs in the Downloads tab while the
Stores page stays open. droiddeck-battlenet starts Blizzard's own Agent
on its own under a headless gamescope and drives its local API, so no
window opens and nothing waits on the Battle.net client.

- Progress, speed and time left come from the Agent, with pause, resume
  (the Agent keeps what it downloaded) and cancel, which uninstalls the
  partial game and removes its folder.
- A job only finishes once the Agent has really run it and the game is
  playable; the first status after a request can still describe the
  previous operation.
- Installed games check Blizzard's version service and show Update when
  the build on disk is behind; the update repairs missing files too.
- Battle.net games can be uninstalled from their page through the Agent.
- Games land in the chosen location: internal storage, other storage or
  a Games folder.
- The hero keeps its action row clear of the playtime column.
2026-10-26 09:01:02 -04:00
Kurt Himebauch 9fc48515c1 feat(input): unify low latency mode (#1604) 2026-10-26 08:12:47 -04:00
Kurt Himebauch 63d1e4d61d feat(library): remember visibility and store order (#1591) 2026-10-26 07:44:07 -04:00
Kurt Himebauch fbb334263c fix(ui): restore continuous ambient motion (#1592) 2026-10-26 02:05:19 -04:00
Danbro007 b15a360cf2 test: 覆盖会话启动失败提示的调用路径 2026-10-26 13:59:11 +08:00
Danbro007 35b85952cf merge: 同步官方 main 并解决多语言资源冲突 2026-10-26 13:11:19 +08:00
Kurt Himebauch 3fd6b1f00d fix(ui): restore consistent focus glide transitions (#1588) 2026-10-25 23:20:57 -04:00
wallentx 91cb966f93 Provide captured compositor log in artifact collection test 2026-10-25 21:59:59 -05:00
wallentx d3faca1919 Register stopped-session collection before allowing restart 2026-10-25 21:41:30 -05:00
wallentx d3d12d7585 Import assertion for failed Steam copy regression 2026-10-25 21:35:26 -05:00
wallentx f845b5c44f Keep failed session artifact collections incomplete 2026-10-25 21:23:17 -05:00
Kurt Himebauch 5978a13f09 fix(stores): restore store capabilities in Library (#1586) 2026-10-25 22:07:42 -04:00
MaxsTechReview c71ba6da96 Run desktop games on the GPU in a window (#1584)
Steam games and Battle.net started from the KDE desktop run on the GPU in gamescope, opened as a KDE window, and Plasma no longer leaves programs with a zero realtime CPU limit that got games killed.
2026-10-25 21:09:24 -04:00
wallentx 5eaa6a4902 Preserve scrub cancellation callbacks and initialize test snapshots 2026-10-25 18:39:19 -05:00
DrummingBird1 cce742766d fix: zero-copy launch log no longer points at a drawer switch that do… (#1354)
fix: describe the actual HDR zero-copy launch policy
2026-10-25 19:25:31 -04:00
Kurt Himebauch 0ec1fe1bdc fix: keep a Nightly guess backed by published preview history 2026-10-25 19:24:43 -04:00
Kurt Himebauch 1ee597e07f Merge remote-tracking branch 'origin/main' into pr1067 2026-10-25 19:22:34 -04:00
Thomas Lee 4922d309c8 perf: allocate only solved DD-FG flow levels (#1556)
The flow solver starts at flowFinest_, but its images, uniform buffers and descriptor sets were allocated for every pyramid level. Keep the seven-level indices and allocate flow resources only where the solver reads and writes them.

The default preset avoids three unused RGBA16F images, saving 38,707,200 bytes of pixel payload at 2560x1440 without changing the shaders or recorded GPU work. Add native allocation/dispatch regressions and document the resource budgets.
2026-10-25 19:21:40 -04:00
wallentx 3bdf303bbc Sync latest upstream touch handling 2026-10-25 18:16:10 -05:00
wallentx f4adbeb602 Merge upstream main and close diagnostic publication races 2026-10-25 18:15:44 -05:00
MaxsTechReview 71fee6b3cc Keep a still direct touch held unless Long press is on (#1578) 2026-10-25 18:32:28 -04:00
Kurt Himebauch 8cf2ffe2ad fix: say why Flatpak setup failed instead of blaming the mirrors (#1580) 2026-10-25 18:05:41 -04:00
Kurt Himebauch 4e00b40333 fix: start the session even when the agent environment can't be prepared (#1579) 2026-10-25 17:57:52 -04:00
Kurt Himebauch 65bacc067b fix: extract GOG installer fallback natively on ARM (#1174) 2026-10-25 17:56:35 -04:00
Kurt Himebauch 53ac21b4d0 fix: keep pending agent entries until the session's environment is ready 2026-10-25 17:50:51 -04:00
e76489ccea fix: refuse overwriting in-use component packages and clear unpacked store on replace (#1144)
* fix: refuse overwriting in-use component packages and clear unpacked store on replace

* fix: preserve component store until replacement succeeds

* fix: invalidate completed component cache before replace

* fix: constrain unpacked component cache path

* Test rejected package cache dot segments

---------

Co-authored-by: ovurrsl <[email protected]>
Co-authored-by: xXJsonDeruloXx <[email protected]>
Co-authored-by: Kurt Himebauch <[email protected]>
2026-10-25 17:50:03 -04:00
b4b7883648 fix: Reuse the prefix the client made for a shortcut id (#1107)
* fix: Reuse the prefix the client made for a shortcut id

droiddeck-proton-run builds the prefix path as steamapps/compatdata/<appid>,
but the client names that folder with the signed 32-bit form of the shortcut
id. A shortcut whose id has the top bit set therefore got a second, empty
prefix, and the first launch after that ran wineboot from scratch: the
session sat on a black screen for the length of a full prefix init, and the
old prefix was never reused.

Match the folder by its unsigned value instead, the way the client does:
int(name) % 2 ** 32 == appid % 2 ** 32. prefix_of() also accepts a negative
folder name now, and reports its unsigned id so the Steam environment it sets
up stays valid.

* fix: Reject folder names int() cannot read, and skip files

lstrip("-").isdigit() accepts "--1" and superscript digits, which int() then
raises on, and one such entry in compatdata would stop shortcut resolution.
Match r"-?[0-9]+" instead. A matching name is also only used when it is a
directory, so a stray file cannot take STEAM_COMPAT_DATA_PATH.

* fix: reuse signed shortcut compatdata prefix

* fix: prefer initialized Proton shortcut prefixes

---------

Co-authored-by: xXJsonDeruloXx <[email protected]>
Co-authored-by: Kurt Himebauch <[email protected]>
2026-10-25 17:49:58 -04:00
ovurrsl 007166753c fix: start the session even when the agent environment can't be prepared 2026-10-25 17:35:36 -04:00
Kurt Himebauch 02767b7e6c fix: report a full battery at 100% while charging, so Steam stops showing 99% (#1577)
* fix: report a full battery at 100% while charging, so Steam stops showing 99%

* test: charging at 100% is reported full
2026-10-25 17:29:43 -04:00
cf13762939 feat: Show the drawer's game settings when a game starts without Steam (#1113)
* feat: Show the drawer's game settings when a game starts without Steam

A session started without the client has no Steam overview to read the
current title from, so the drawer's game profile group stayed empty and the
per-game settings, FEX preset, environment and unfolded controls were hidden.
The launch already knows which game it starts, so carry its id into the
session and fall back to it when Steam has nothing to report.

* fix: Pass the game id into the service start, and keep the unfolded toggle to Steam

SessionActivity.surfaceCreated() hands the mode, program and args to
SessionService.start(), which builds the service intent. EXTRA_GAME_ID was
only put on the activity intent, so a new no-Steam session never received it
and the drawer fallback stayed empty. Pass it through.

The unfolded-controls toggle also went out for direct sessions, but
applyUnfoldedControls() still requires Steam mode and a Deck pad, neither of
which a direct session sets up. Keep the toggle to Steam.

* fix: hide Steam controls in direct session drawer

---------

Co-authored-by: xXJsonDeruloXx <[email protected]>
Co-authored-by: Kurt Himebauch <[email protected]>
2026-10-25 17:29:39 -04:00
kingokksa 9f4794769c feat: on-screen keys for any key or key combo (#1305) 2026-10-25 17:27:07 -04:00
6e67dbb5be fix: chosen game art survives a Steam account switch or runtime reset (#1026)
Every grid-<slot> record held only a file signature, not which grid
folder it came from. After switching accounts or reinstalling the
runtime, the new, empty grid folder looked like the user removed every
image in Steam, and all chosen art was deleted. The grid folder now
carries a random id and records store it; a record from another folder
counts as a first look, so the choice is placed again.

fix: never accept a record made with the fallback grid id

CodeRabbit: when the grid id could not be read or written, "?" was
stored in records and a later "?" matched it, so an empty grid could
still delete local art. Records now never match the fallback id, and a
blank .droiddeck-grid file is replaced instead of blocking the rename.

Co-authored-by: ovurrsl <[email protected]>
Co-authored-by: Kurt Himebauch <[email protected]>
2026-10-25 17:18:31 -04:00
19c655bd7e fix: update status receiver only acts on DroidDeck's own install session (#1015)
* fix: the update status receiver only acts on this app's own install session

Before Android 13 the receiver was exported, so any app could send a
fake PENDING_USER_ACTION broadcast and have DroidDeck launch an arbitrary
intent. The receiver now ignores broadcasts whose session id does not
match the running install, launches only a system installer activity,
and strips URI grant flags.

* fix: record the install session id before committing it

CodeRabbit: the status broadcast could arrive between commit() and
install() returning, while session was still -1, and be dropped. The id
is now reported through a callback before commit, and reset to -1 at
the start of each attempt so a late broadcast from an earlier session
is ignored.

* fix: declare package installer confirmation query

* Query current PackageInstaller confirmation action

---------

Co-authored-by: ovurrsl <[email protected]>
Co-authored-by: xXJsonDeruloXx <[email protected]>
Co-authored-by: Kurt Himebauch <[email protected]>
2026-10-25 17:03:25 -04:00
Kurt Himebauch 815b93db5f feat: profile PRoot costs and test fast-path libraries without an APK rebuild (#1528) 2026-10-25 16:49:35 -04:00
MaxsTechReview aca4b5dd3e Start World of Warcraft on DirectX 11 by default (#1576) 2026-10-25 16:41:41 -04:00
Kurt Himebauch 8d858b4334 feat: pick a game's CPU cores in its game profile (#1566)
* feat: pick a game's CPU cores in its game profile

* fix: match per-game cores against the online CPU list
2026-10-25 16:38:11 -04:00
ianandKurt Himebauch ee3f32d31e feat: default storage for new installs, with USB/SDCARD/SSD detection (#1520)
* feat: choose default storage for new store installs

Store installs and the Epic Games Launcher can use internal storage, external storage, a folder picked through Choose folder..., or ask every time. The choice is stored in SessionPrefs and used by StoreInstallRoot; Storage > Default storage exposes it.

* fix: allow choosing any folder for default storage

The default storage choice can now hold an arbitrary folder path, not just internal or app-specific external storage. StoreInstallRoot resolves and validates picked folders, keeps a disconnected choice instead of silently falling back, and the folder picker stays available from Storage.

* fix: keep the Epic install folder when external storage is away

* test: use a unique missing folder for the Epic storage check

* fix: detect USB SSDs that the storage manager hides

List storage volumes that have no UUID instead of dropping them, keep volumes that are present but not yet mounted, and fall back to parsing /proc/mounts so USB OTG drives appear even when Android does not expose them as storage volumes or app directories. Prefer the system's volume description for removable drives. (Raw removable drives as move targets, the folder-picker fallback, and the unreadable-drive guidance follow with the move stack.)

* fix: compare mount paths as strings in StorageRootsTest

* fix: never drop a present storage volume from the drive list

Key volumes by description or index when they report no UUID and no directory, keep nofs/unmountable states so a plugged-in drive stays visible with guidance, only attach app directories on strict UUID matches, and accept any fstype under vold-managed locations plus usb-ish mounts directly under /mnt.

* fix: probe folder writes with a fresh temp file and keep Epic's folder unset while external storage is away

---------

Co-authored-by: Kurt Himebauch <[email protected]>
2026-10-25 16:38:06 -04:00
The412Banner 38926b3157 feat: Performance mode puts frame pacing and per-frame window-check fixes behind one switch (#1563)
Adds a Performance mode switch under Mode settings > Frame rate, off by default and stored per mode like the frame limit. Off keeps today's behaviour. On:

- with no frame limit, the compositor hands buffers back to the game as soon as they are replaced instead of once per refresh, and gamescope's WSI layer is asked for at least 8 swapchain images; a chosen frame limit still paces
- DROIDDECK_WSI_BYPASS_CHECK_MS=100 lets gamescope's WSI layer reuse its present-time Xwayland bypass check (patch 0117, gamescope-3.16.29-p13); WINE_OFFSCREEN_CHECK_MS=100 does the same for winex11 once a DroidDeck-Proton release carries that fix. Off sets both to 0.

The gamescope pack now ships the patched WSI layer and SessionFiles stages it over the runtime's stock copy. Two per-game options, off by default: PROTON_DXVK_D3D8 and WINE_D3D_CONFIG renderer=vulkan|gl.

AIO Graphics Test on an AYANEO Pocket FIT (Adreno 750), off -> on: Vulkan 204 -> 1787, OpenGL 296 -> 2169, D3D12 308 -> 3563, D3D11 1133 -> 2874, D3D9 1253 -> 3098 fps; DiRT Showdown 92.2 -> 93.4 (GPU-bound).
2026-10-25 20:29:26 +00:00
Kurt Himebauch 12eac3e9ee Fix Nova layouts and session drawer shortcuts (#1574)
Improve Nova’s compact layouts with scrolling store tabs and build details, remove the redundant Updates heading, preserve full portrait artwork, and add drawer PiP and Select/Start shortcuts for Guide/QAM.
2026-10-25 16:06:19 -04:00
MaxsTechReview 679e9476c3 Add Battle.net logs to Share logs (#1573) 2026-10-25 15:21:27 -04:00
Kurt Himebauch 8addf7d7f0 fix: correct Nova M1 and M2 mapping (#1571)
* fix: correct Nova M1 and M2 mapping

* fix: recognize Nova Xbox controller ID
2026-10-25 14:40:14 -04:00
MaxsTechReview 1f3f1ebc6a fix: Arabic text reads right to left when it mixes in English words (#1568) 2026-10-25 14:25:43 -04:00
MaxsTechReview 645dc3e781 fix: close the Stores and Library search with its X or Back (#1567) 2026-10-25 14:25:39 -04:00
Kurt Himebauch 9593529197 feat: a setting to keep Steam uncompressed while games run (#1565)
* feat: a setting to keep Steam uncompressed while games run

* fix: say that the RAM cap still compresses Steam when this is off
2026-10-25 13:20:33 -04:00
ae193e9b8d fix: find GOG Galaxy SDK cloud saves when remote-config lists no locations (#970)
* fix: find GOG Galaxy SDK cloud saves when remote-config lists no locations

cloudStorage.enabled with an empty locations list is Galaxy SDK IStorage
under __default. DroidDeck treated that as no cloud.

Use the SDK folder, strip/prefix the __default namespace, and inflate gzip
bodies GOG stores on download. An empty cached template is fetched again
so devices that already recorded "no cloud" recover.

* fix: keep an empty GOG namespace, expire cached none, close gzip

An explicit empty cloud_namespace is Auto Cloud and must not become
__default just because the path template looks like Galaxy storage.
A cached none is retried after a day so a later enablement is picked up.
GZIPInputStream is closed after a compressed download.

* fix: peel GOG cloud gzip only when the service or PUT compressor says so

GET asks for identity encoding so the body is what GOG stored. A
Content-Encoding of gzip is inflated once. Otherwise a gzip body is
inflated only when it round-trips as GOG's PUT gzip (deflate 6, mtime 0),
so a gzip-format save is not rewritten.

* Reject corrupt GOG cloud downloads

---------

Co-authored-by: xXJsonDeruloXx <[email protected]>
Co-authored-by: Kurt Himebauch <[email protected]>
2026-10-25 13:20:29 -04:00
MaxsTechReview 27e390a846 fix: StarCraft starts its 32-bit client from Battle.net (#1564) 2026-10-25 13:10:28 -04:00
MaxsTechReviewandKurt Himebauch 067f82e5e2 fix(stores): let a game's description fill the free height of the page (#1493)
Co-authored-by: Kurt Himebauch <[email protected]>
2026-10-25 12:59:58 -04:00
7ef52865d2 fix: place GOG cloud saves at the location root (#1178)
GOG lists every cloud object as <location name>/<path>. The location name
is not part of the local folder, so Silksong's saves/user1.dat belongs at
default/user1.dat, not default/saves/user1.dat. Strip the segment for local
paths and add it back for HEAD/GET/PUT, cache the location name, and migrate
files written by the old layout without touching files the game wrote.

Co-authored-by: xXJsonDeruloXx <[email protected]>
Co-authored-by: Kurt Himebauch <[email protected]>
2026-10-25 12:58:54 -04:00
0de672f94e fix: a Linux runtime update keeps imported apps and catalog AppImages (#1062)
* fix: a Linux runtime update keeps imported apps and catalog AppImages

The update carried only /root into the new tree and then discarded the
old one, so every imported AppImage, script app, Flatpak override and
catalog emulator AppImage was deleted. User-owned paths are now moved
across as a list, moved back if the carry or swap fails partway, and
recovered after an interrupted update.

* fix(runtime): retain user data when carry and recovery fail

* fix(runtime): mark staging carry start so interrupted extraction is discardable

* test: mark interrupted runtime data for recovery

* fix: make runtime data carry recoverable

* fix: the runtime update with carried user data builds again

carryUserData passed the de-duplicated Set of user paths where a List is expected after the two carry changes met; copy it into a List.

* fix: a runtime update stops instead of dropping app markers it cannot inspect, and keeps app names with line breaks

userPaths now uses the throwing exists() helper for the AppImage, menu entry and marker checks. The carry marker stores each path URL-encoded on its own line, so a name containing a newline cannot split into two paths. The symlink test now links a parent directory, which is what safePath rejects.

* fix: retain conflicting runtime recovery data on every retry

Stop on unreadable carry markers and keep rollback tests independent of add-on downloads.

---------

Co-authored-by: ovurrsl <[email protected]>
Co-authored-by: xXJsonDeruloXx <[email protected]>
Co-authored-by: Kurt Himebauch <[email protected]>
2026-10-25 12:57:04 -04:00
Kurt Himebauch 0ad4b7cc14 feat: Back in the file manager returns to the folder you came from (#1562)
* feat: Back in the file manager returns to the folder you came from

* fix: the file manager's up arrow closes favorites first, as before
2026-10-25 12:55:11 -04:00
Charles DuBose c4df7e9b3d feat: Battle.net on the desktop with its games and store, plus memory options (#979) 2026-10-25 12:49:27 -04:00
Kurt Himebauch 6918fd9308 feat: install the redists a Steam game's install script runs on Windows (#1561)
* feat: install the redists a Steam game's install script runs on Windows

* fix: rescan a game's components when its install script changes
2026-10-25 12:44:35 -04:00
597402107d [FIX] statx call on old kernels < 4.11 (#947)
* [FIX]
Fix git statx call on old kernels < 4.11 for correct using of "Fast proot path"

Issue
https://github.com/Droid-Deck/DroidDeck/issues/770

* [FIX]
fix: don't call major() from the proot fast path

* fix: preserve statx fallback request semantics

* fix statx fallback argument handling

* fix: silence statx null check warning

---------

Co-authored-by: xXJsonDeruloXx <[email protected]>
Co-authored-by: Kurt Himebauch <[email protected]>
2026-10-25 12:42:51 -04:00
Kurt Himebauch 10fe5c5f77 feat: stretch 16:9 games to screens wider than 16:9 too (#1558)
* feat: stretch 16:9 games to screens wider than 16:9 too

* fix: don't offer 16:9 game fit while the session follows the screen
2026-10-25 12:37:47 -04:00
FX1922 411fda343a itch.io: version picker and installing free games (#1503)
* itch.io: temporary Test itch.io button that logs what the API answers (not for merge)

* itch.io: show Test itch.io only while Debugging tools is on

* itch.io: version picker and installing free games

* itch.io: build the platform names outside joinToString so the composable call compiles

* itch.io: ask for the version before resuming an unfinished install

* itch.io: keep the chosen upload, save free games in the queue, don't auto-install a closed picker

* itch.io: install straight away when exactly one build is installable

* itch.io: show the picker again unless the game has exactly one build

* itch.io: ignore a second build tap while the picker is closing

* itch.io: empty a partial install before extracting so builds never mix

* stores: deleteTree removes a symlink without following it

* stores: skip symlinked folders in the uninstall walk, and only empty a partial install that is this game's own
2026-10-25 12:37:04 -04:00
Kurt Himebauch 7d99a24ad5 fix: find the arm64 reaper under XDG_DATA_HOME and serve queued file dialog requests at once (#1560) 2026-10-25 12:27:42 -04:00
Kurt Himebauch 8658ffff90 fix: Steam's Browse buttons open the file chooser with the x86-64 client too (#1557) 2026-10-25 12:08:12 -04:00
angelrenefm 86ecb716eb fix(build): build the local image when Podman reports it missing (#1527) 2026-10-25 12:08:08 -04:00
informalTechCode b1c70f5f81 Add Rokid Max 2 support using its verified USB identity (#1535)
Add Rokid Max 2 USB recognition and validated Max support
2026-10-25 12:07:47 -04:00
VIIII4andVIIII4 c6bc8aef04 Add an optional HTTP proxy for downloads (#1474)
* Add an optional HTTP proxy for downloads

Downloads only reached GitHub directly or through URL-rewrite
mirrors, which does not help where a local proxy is the workable
route. Setup now takes a host:port HTTP proxy that every Downloader
connection uses, independently of the download source mode.

* Reference the Kotlin object instance from Java

Java cannot call a Kotlin object member on the class name without
@JvmStatic; the surrounding code already uses INSTANCE.

* Assert the proxy host literal without a reverse lookup

InetSocketAddress.getHostName reverse-resolves on some JVMs
(127.0.0.1 became localhost on CI); hostString is the literal.

* Pass the app proxy into the guest environment

The Steam client installer and the guest package tools fetch with
plain curl against their own CDNs, outside Downloader. Export the
usual proxy variables for them when the app proxy is set; proot
shares the host network, so a localhost proxy is reachable as-is.

---------

Co-authored-by: VIIII4 <[email protected]>
2026-10-25 12:07:26 -04:00
Jamison Prineandjazzyjames 2be08bc67d feat: adjustable RAM and VRAM caps for the container (#1537)
* feat: adjustable RAM and VRAM caps for the container

Performance > Memory gets two sliders:

- RAM cap (soft, Steam sessions): when the session is resident above the
  cap, the session watcher compresses the Steam client into zRAM, waits for
  its page-out acknowledgment, then compresses the rest of the session
  (processes that loaded libblsession) if still over. Nothing is closed.
  Measured at most every 30 s; the client is read back once usage drops
  below the cap. Invalid values leave the cap off. Needs zRAM swap.
- VRAM cap: sets dxgi.maxDeviceMemory ahead of the game's own DXVK_CONFIG,
  so Direct3D games budget for less video memory. A game profile still
  wins. Applies from the next session.

* fix: back off the RAM cap when paging out cannot get under it

A cap below the game's working set was never met, so the watcher paged out
the client and the whole session every 30 s for the rest of the session.
Usage is still measured every 30 s, but after a page-out that leaves the
session over the cap the next one waits twice as long (60 s up to 10 min),
resetting once usage is under the cap. The client step is skipped while the
client is already under 512 MB resident.

* fix: RAM cap covers desktop Steam, signals each group once, no idle churn

- Steam opened from the desktop runs the same session watcher, so the cap
  is exported whenever the Steam client runs, not only in Steam mode.
- The second page-out goes to the rest of the session only; Steam was
  signalled again even when its own step had been skipped as already small.
- A cap page-out while no game runs stays paged out. Reading the client back
  right away pushed it over the cap again and repeated every minute; only a
  game closing while over the cap still defers a read-back.

* fix: RAM cap marks the client packed only when it compressed it

When the cap skipped the client as already small during a game, it still
recorded the client as packed, so the regular in-game client page-out
waited up to 300 s for a page-out that never happened.

---------

Co-authored-by: jazzyjames <[email protected]>
2026-10-25 12:06:45 -04:00
Kurt Himebauch dfe8227f49 fix: run RoboCop: Rogue City at D3D12 feature level 12_1 (#1553) 2026-10-25 11:58:38 -04:00
Kurt Himebauch 3995fe3829 fix: install XACT audio with Skyrim so its voice lines play (#1554)
* fix: install XACT audio with Skyrim so its voice lines play

* fix: rescan cached component findings when the detectors change
2026-10-25 11:50:47 -04:00
Kurt Himebauch 9cecd528e8 fix: Start without Steam uses the launch options set in Steam's Properties (#1552)
* fix: Start without Steam uses the launch options set in Steam's Properties

* fix: treat a malformed shortcuts.vdf as having no launch options
2026-10-25 11:50:44 -04:00
Kurt Himebauch 709c041b24 perf: pause the home page's ambient motion after 20 s without input (#1550)
* perf: pause the home page's ambient motion after 20 s without input

* fix: restart the idle timer when a touch gesture ends
2026-10-25 11:41:39 -04:00
Kurt Himebauch e62698e395 fix: show when non-Steam games were last played, from Steam's shortcuts (#1551) 2026-10-25 11:33:24 -04:00
MaxsTechReview c9565bc0b9 Skip umu-protonfixes for Black Ops III and Dark Souls Remastered (#1508)
GE-based Protons, the default with the x86-64 Steam client, run umu-protonfixes. Its Black Ops III fix switches the prefix to Wine's ALSA driver, which has no device here and replaces DirectAudio, and its Dark Souls Remastered fix runs the VC++ 2017 installer, which fails under ARM64 Proton.
2026-10-25 11:28:28 -04:00
MaxsTechReview a7753a667c fix: Flatpak apps see the device's network, so WebRTC streams connect (#1548) 2026-10-25 11:28:25 -04:00
maxjivi05 6ef2bcc9c5 docs: Star History rank badge above the awards 2026-10-25 10:24:48 -04:00
MaxsTechReview 3fdc74051c feat: DroidDeck-Proton is downloaded, kept up to date and used by default (#1540) 2026-10-25 10:09:22 -04:00
ovurrsl 287eafed0c Merge current main into PR #1042 2026-10-25 16:29:58 +03:00
ovurrsl 9119ae96a5 fix: the stalled-download cancel test gives a loaded CI runner time to tear the transfer down
The cancel and the download's return each had 2 s, which a busy release-test runner missed while the test passes locally every time. The held server body still guarantees the download only returns because of the cancel.
2026-10-25 16:29:57 +03:00
Kurt Himebauch 2a69505199 feat: merge Games and store libraries into one Library and one game page (#1460) 2026-10-25 08:39:56 -04:00
Kurt Himebauch 0cf5b42788 Add an in-session microphone toggle (#1506) 2026-10-25 08:37:49 -04:00
0x1Nawaf aa1a8da39a merge: with main branch 2026-10-25 10:38:42 +00:00
0x1Nawaf 73e158ab3b fix: compress only while the running game is focused 2026-10-25 10:38:37 +00:00
diegodru 1f42f512e2 fix: GOG sign-in pages use the browser's own user agent (#1500)
The GOG sign-in WebView sent GOG Galaxy's Windows user agent while the page's scripts and client hints report an Android WebView, so Cloudflare's check on gog.com never passed (#1486). The sign-in flow now uses the WebView's own Chrome user agent, like the Epic, Google, Facebook and Apple sign-ins. API calls outside the WebView keep the Galaxy user agent.

Fixes #1486
2026-10-25 00:06:54 -04:00
The412Banner c5d7cdc058 Xbox Cloud Gaming: stream from the store and Games tab, native player with DroidDeck frame generation (#1531)
Xbox Cloud Gaming: stream from the store and Games tab, native player with DroidDeck frame generation

Stream Xbox games with Xbox Cloud Gaming (Game Pass) from DroidDeck, outside
the Linux session.

- Backend: `xodus-cli cloud-token` (tools/xbox/droiddeck-cloud-token.patch)
  gets the streaming tokens; they stay in memory.
- Web player (default): the Greenlight player page (tools/xcloud-player, MIT)
  in a WebView, plus Microsoft's own xbox.com/play player. In-stream menu with
  screen effects (sharpen, colour, deband, FXAA, CRT, NTSC, Toon, fps cap),
  stream quality, performance overlay (level, layout, top right / top left)
  and rumble.
- Native player (opt-in, Android 10+, Player engine: Native): Google's WebRTC
  (io.github.webrtc-sdk 125.6422.07, BSD) with the Greenlight protocol ported
  to Kotlin, in its own process (:xcloudnative) with its own instance of
  DroidDeck's compositor. Decoded frames reach the compositor as
  AHardwareBuffers (new stream mode, VK_ANDROID_external_memory_android_
  hardware_buffer import), so DDFG 2x and LSFG 2x/3x/4x run on them. Live
  frame generation, frame-rate target (presets and a 10-165 fps slider, even
  pacing), zero-copy picture, small network buffer, fast decoder, instant
  controller input, overlay with engine and on-screen fps, background end,
  restart and web fallback. It takes the main process's tokens through
  XboxCloudTokenService; they are fetched ahead while a cloud game is shown.
- The compositor requests the dma-buf import extensions only where the driver
  has them (the system driver has none).
- Catalog: cloud availability, Game Pass plan CFQ7TTC0K6L8 counted, library
  filter All / Owned / Game Pass / Cloud, Stream and Add to Games on the store
  page, stream-only Games-tab entries (Stream + Download / Play + Stream).
- Xbox games get VC++ 2015-2022 and the ARM64 runtime registry entries
  (vcredist2022_arm64, built in until the catalog has it) before each launch.
- Controller focus reaches pickers opened from a game page (install location,
  uninstall); Add / Remove from Games keep the focus.

Credits: Greenlight by UnknownSKL (Jim Kroon), MIT, player and protocol;
Better xCloud for the resolution / OS mapping; MGamer404's Bannerlator #627
for the ARM64 runtime approach. Ported from the412banner's Bannerlator.
2026-10-24 23:17:57 -04:00
The412BannerandKeegantheVeganat0r 5ca1f610ab Xbox: fix sign-in on phones with a slow StrongBox Keystore (#1521)
On some phones (tester: Honor BKQ-N49, Android 17) Xbox sign-in stopped with
"the Xbox helper stopped with exit code 1" before Microsoft's page opened.
The backend asks the app to read and save credentials over a socket and waits
10 s for each reply. The app sealed credentials with a StrongBox-backed
Keystore key, and each StrongBox operation took 7-15 s on that phone, so the
backend gave up. The app still finished those saves afterwards, leaving a
record without a username that every later attempt had to read back slowly.

- Seal new store credentials with a TEE Keystore key (v2 envelopes); v1
  envelopes still open with the old key and move to v2 on their next write.
- A sign-in discards an Xbox record without a username (an unfinished
  sign-in) before starting the backend.
- The backend waits 30 s for the app's credential reply.
- The backend names the failing step instead of only credential-io (socket
  connect/exchange, session lock, missing socket name/port/key), and falls
  back to a keyed 127.0.0.1 port when the abstract socket is refused.
- The app's credential listener logs and retries accept errors, serves each
  connection on its own thread, and logs slow requests with per-stage times.

Refs #1337, #1300


Co-authored-by: KeegantheVeganat0r <[email protected]>
2026-10-25 01:39:09 +00:00
maxjivi05 82d8eb3b79 Keep a Proton choice unless the shared build needs the other platform 2026-10-24 21:13:58 -04:00
maxjivi05 5f0e6a8b7e Match Proton tools across clients by their arm64 and a64 names 2026-10-24 21:13:58 -04:00
maxjivi05 c866bb90db Share installed games between the arm64 and x86-64 Steam clients 2026-10-24 21:13:58 -04:00
MaxsTechReview 8d407aa1e8 Focus flatpak and other sandboxed shortcuts in gaming mode (#1524) 2026-10-24 20:04:54 -04:00
Kurt Himebauch aca7fca19f fix: keep cancellation polling from ending session teardown early 2026-10-24 19:50:10 -04:00
Kurt Himebauch 96ba915378 fix: publish session stopping before clearing the running flag 2026-10-24 19:50:10 -04:00
Kurt Himebauch 71f84f0593 fix: defer automatic Proton updates until session teardown ends 2026-10-24 19:50:10 -04:00
Kurt Himebauch b56ea6122a fix: exclude pending launches and retain Proton update cooldowns 2026-10-24 19:50:10 -04:00
Kurt Himebauch 8a948fbe45 feat: automatically update installed Proton builds on app open 2026-10-24 19:50:10 -04:00
maxjivi05 74a6a88df8 fix(desktop): keep the Desktop actions and Settings handle on small phones 2026-10-24 18:58:45 -04:00
maxjivi05 c9493993af fix(desktop): keep the Desktop page from overlapping on phones 2026-10-24 18:58:45 -04:00
MaxsTechReview dc08385411 Stop the library helper when the KDE Steam client exits (#1519) 2026-10-24 17:57:59 -04:00
MaxsTechReview 49b6a0739d Present games from the KDE Steam client through software WSI (#1518) 2026-10-24 17:56:40 -04:00
MaxsTechReview 8471682b1c Collect only the Steam log lines a session wrote (#1505) 2026-10-24 14:47:21 -04:00
MaxsTechReview 4dda1c1e52 fix(epic): a paused install keeps what it downloaded (#1504) 2026-10-24 14:47:18 -04:00
Thomas Lee 8156a9649d perf: reuse Steam artwork directory scans (#1466) 2026-10-24 14:35:14 -04:00
FX1922 7b518d7e6a itch.io: platform and tag filters (#1497) 2026-10-24 13:39:57 -04:00
maxjivi05 8d66570d33 Steam file dialog: keep spaces in the chosen path, cancel when GTK 3 is missing or the bridge reconnects 2026-10-24 13:18:04 -04:00
maxjivi05 5a56d9f785 fix(steam): Browse buttons open a file chooser on the ARM64 client
Steam's ARM64 client has no desktop-portal file chooser, so
SteamClient.System.OpenFileDialog failed at once and every Browse button
(Add a Non-Steam Game, shortcut target, library folder) did nothing.
The Steam UI bridge now answers it with a GTK file chooser run under
Steam's reaper, so gamescope shows it over Steam.
2026-10-24 13:18:04 -04:00
MaxsTechReview 77e23cb397 itch.io: browse the Store tab by sort and price, with paging and a disk cache (#1473)
## What this does

The itch.io Store tab used to show only four fixed shelves. This is the
first of a series of PRs that rework it. It adds real browsing: pick an
order and a price filter, and the tab loads itch's own listing for that
choice and keeps loading as you scroll. Login, the library, downloads
and installs are unchanged.

Platform and tag filters, the game page with the install picker, updates
and collections are not in this PR. They come in the following ones.

## How it works

itch serves every listing page as an RSS feed if you add `.xml` to the
address. The app already reads those feeds for the shelves, so browsing
reuses the same parser (`ItchShelves.parse`). Nothing here needs the API
key.

The address is built the way itch builds its own links: sort, then
price, then `platform-<name>`, then `tag-<slug>`.

| Choice | Address |
|---|---|
| Popular (default) | `https://itch.io/games` |
| New & popular | `/games/new-and-popular` |
| Top sellers | `/games/top-sellers` |
| Top rated | `/games/top-rated` |
| Most recent | `/games/newest` |
| Free | `/games/free` |
| On sale | `/games/on-sale` |
| Paid | `/games/store` |
| $5 or less | `/games/5-dollars-or-less` |
| $15 or less | `/games/15-dollars-or-less` |

Choices combine, for example `/games/top-rated/free`, then `.xml` for
the feed. Page 2 and later add `?page=N`.

## Changes

**`stores/itch/ItchBrowse.kt` (new)**
- `BrowseQuery` holds sort, price, platforms and tags. The default query
is the front page, which keeps the existing shelves.
- `ItchBrowse.path` and `url` build the addresses above. Only the
spellings listed above are used. Platforms and tags are already accepted
by the builder, ready for the next PR.
- `ItchBrowse.page` loads one page of results:
- Disk cache under `cacheDir/itch-browse`, keyed by the SHA-1 of the
address, fresh for 3 hours.
  - Requests are made one at a time, at least 1.2 s apart.
  - A 429 answer waits 8 s and tries once more.
- Each request is written to the store log (address, status, number of
games).
- `ItchBrowseState` is the Compose state for the tab: the query, the
games loaded so far, and the loading, ended and failed flags. A
generation counter discards answers that arrive after the user has
changed the query. Paging skips games it has already shown.

**`ui/ItchBrowseBar.kt` (new)**
- A sort chip row and a price chip row, plus a load-more trigger at the
end of the grid.
- Chips are selectable, work with the controller (`controllerConfirm`)
and use the same glide border as the rest of the app.

**`ui/StoresPage.kt`**
- The storefront shows the chip bar for itch.io and, when a non-default
choice is active, the browse results in the existing grid instead of the
shelves.
- Games you already own are marked as owned in the results.
- Empty and failed states have their own messages.

**`stores/itch/ItchShelves.kt`**
- `open(url)` is now `internal` so browsing can reuse it.

**`stores/StoresState.kt`**
- `detailItem` also looks in the browse results for itch.io, so tapping
a result opens its detail page.

**Strings**
- 13 new strings (sort names, price names, "Load more", "Couldn't load
games"), translated in all 33 locales.

**Docs and tests**
- `docs/development/itch-browse.md` explains the address scheme, the
cache and the paging.
- `ItchBrowseTest` checks the addresses the code builds against the ones
listed above.

## Testing

I checked every address with `.xml` added, and all of them load without
errors:
- Every sort, every price filter and every platform on its own.
- Combinations: `top-rated/free`, `top-rated/on-sale`,
`top-rated/store`, `top-rated/5-dollars-or-less`,
`top-rated/free/platform-windows` and
`top-rated/free/platform-windows/tag-pixel-art`.
- Paging: `?page=2` and `top-rated.xml?page=2`.

The unit test locks those addresses in.

## Not in this PR

- Platform and tag filters in the UI, and search.
- The game page, the version picker and install for free games.
- Updates for installed games and itch collections.
- Android builds will link out to the itch.io page. Butler patching
comes last, as an add-on pack.
2026-10-24 11:33:47 -04:00
maxjivi05 1883a8c7cf itch.io browse: drop code comments 2026-10-24 11:15:35 -04:00
maxjivi05 aefd8a3b60 fix(xbox): report a failed class registration in the launch log 2026-10-24 11:14:20 -04:00
maxjivi05 fee8ce91f3 fix(xbox): register the runtime's WinRT classes and load its wintypes so Minecraft starts 2026-10-24 11:14:20 -04:00
maxjivi05 bcb18e3fb7 fix(xbox): match only the 401 rejection Xodus prints 2026-10-24 09:07:13 -04:00
maxjivi05 6855dd4467 fix(xbox): say to sign in again when Microsoft rejects the download's licenses 2026-10-24 09:07:13 -04:00
maxjivi05 a66d7d74a4 fix: Steam Input mouse clicks, scrolling and pointer moves reach games again 2026-10-24 09:07:04 -04:00
Kurt Himebauch 2bd279a0f3 fix: read Steam shader settings off the main thread 2026-10-24 08:54:41 -04:00
maxjivi05 0790cb4390 fix: keep a Proton's originals when its folder could not be fully removed 2026-10-24 08:22:00 -04:00
maxjivi05 ad6d8d824a fix: removing a Proton also deletes its stored component originals 2026-10-24 08:22:00 -04:00
Delete228 5ee3ed07be fix: release panel touch source after tab teardown
Release the panel touch source after child detach callbacks finish publishing button releases. Cover tab switching and whole-panel removal with ownership regressions.

Validation: 22 targeted JVM tests, debug APK build, session-asset check and runtime-input verification passed. Device validation remains pending.
2026-10-24 13:43:09 +03:00
maxjivi05 0b671b2144 fix: match an annotated overlay header and keep the settings file's mode 2026-10-24 06:27:46 -04:00
maxjivi05 31cead9b32 fix: keep blank lines inside the overlay section and stage settings under a unique name 2026-10-24 06:27:46 -04:00
maxjivi05 26c3dcc885 fix: turn off the Ubisoft Connect overlay so Ubisoft games stop crashing at start 2026-10-24 06:27:46 -04:00
FX192 dc57f367e1 itch.io browse: don't re-fire the load-more trigger when a page is appended 2026-10-24 13:07:16 +03:00
FX192 5838587639 itch.io browse: let a failed page be retried instead of ending the list 2026-10-24 13:02:17 +03:00
FX192 7e87cdacb5 Merge remote-tracking branch 'upstream/main' into feat/itch-browse 2026-10-24 12:46:26 +03:00
FX192 dbcbf0b6fa itch.io browse: use itch's own sort, price and platform paths
The sort and price chips now cover everything itch's browse page offers, with the
paths copied from its links: new-and-popular, top-sellers, top-rated and newest
for sorts; free, on-sale, store, 5-dollars-or-less and 15-dollars-or-less for
prices; platform-windows, -osx, -linux, -android, -ios, -web and -mobile-web.
The guessed alternative spelling of the free filter is gone, so a query is one
address. The tests assert the addresses itch itself uses.

New and reworded strings in all 33 locales.
2026-10-24 09:27:49 +03:00
maxjivi05 476642be71 chore: drop the added comments 2026-10-24 01:42:20 -04:00
Nikolay Nadorichev 8c86b718a0 fix: find the signed-in Steam account when loginusers.vdf has no MostRecent
droiddeck-steam-shortcuts and AddedGames.accountConfig took the account
marked "MostRecent" in loginusers.vdf, else the only userdata folder.
The arm64 client here writes no MostRecent, so with two accounts in one
install nothing was picked: no added or store game reached
shortcuts.vdf ("no signed-in account yet; nothing written") and Steam
answered their launch with "App configuration unavailable".

Without MostRecent, take the account marked AutoLogin, then the one with
the latest timestamp. Both sides use the same order so the app reads the
config the guest script writes.
2026-10-24 01:42:20 -04:00
maxjivi05 473ac666b3 Take over Steam's Default startup movie pick 2026-10-24 01:41:08 -04:00
FX192 cc0a4f5aa9 itch.io: browse the Store tab by sort and price, with paging and a disk cache
The Store tab only had four fixed shelves and a title search. A row of sort
(Popular, New, Top rated, Top sellers) and price (Any, Free, On sale) chips now
sets a BrowseQuery; anything but the defaults swaps the shelves for that query's
games, loaded page by page as the grid scrolls.

ItchBrowse builds itch's listing-feed URL (platform and tag segments are in the
builder for the next change), tries each spelling of a price filter until one
answers, spaces requests 1.2 s apart, backs off once on a 429, and keeps pages
in the app cache for three hours. Every request is logged with its status and
game count so a wrong URL guess can be fixed from a device. Owned games replace
their browse copy so Install and the owned markers are right, and the detail
page finds browse results.

Strings for all 33 locales; URL-builder tests; docs/development/itch-browse.md.
2026-10-24 07:49:05 +03:00
maxjivi05 f89dc04072 fix: Proton's EasyAntiCheat and BattlEye runtimes are not listed as games
Steam installs Proton EasyAntiCheat Runtime (1826330) and Proton BattlEye Runtime (1161040) as tool depots. The launcher's Games tab and the desktop's game entries now skip them like the other runtimes.
2026-10-24 00:47:45 -04:00
maxjivi05 dd79f44b3a fix: the Steam download monitor no longer stalls the main thread on a long content log
The content log was read with RandomAccessFile.readLine, one byte per read, on the main thread. A 4 MB log took about 2 s on the first poll, enough for an ANR on slower devices while a download runs. It is now read through a buffered reader with the same Latin-1 lines and offset, and the per-line regexes only run on lines that can match.
2026-10-24 00:27:22 -04:00
maxjivi05 b0e405ebf3 fix: Epic client imports get the launch command, versioned installs need a matching manifest build
Games the Epic client installed now carry their LaunchCommand arguments too, split by a new launch.split op. A doubled quote inside a quoted argument is one literal quote, and an install with a version ignores a manifest without the same build.
2026-10-23 23:59:37 -04:00
maxjivi05 42026b382f fix: keep manifest launch arguments to the installed build and their own additions
The manifest's arguments apply only when its build matches the installed version, only arguments the merge inserted are tracked as its own, escaped quotes split like a Windows command line, and the REDprelauncher rule needs Cyberpunk2077.exe beside it.
2026-10-23 23:59:37 -04:00
maxjivi05 8209a707f4 fix: Caravan SandWitch and Cyberpunk 2077 run at D3D12 feature level 12_1
Feature level 12_2 makes vkd3d-proton report mesh shaders and ray tracing that Turnip doesn't provide. Caravan SandWitch (Unreal Engine 5) then builds mesh shader pipelines without a pipeline layout and the driver crashes; Cyberpunk 2077 stops at ray tracing initialization. Only these two games get 12_1, and a value set in a game's own settings still wins.
2026-10-23 23:59:37 -04:00
maxjivi05 46cd8e1777 fix: Epic games get their manifest launch command
Epic manifests carry a launch command next to the launch executable, which the Epic launcher passes to the game. It was read and dropped, so Caravan SandWitch never got -PCStore=EGS and quit with "started outside of any known PC store". The manifest's launch command now joins the game's arguments, including for games already installed.
2026-10-23 23:59:37 -04:00
maxjivi05 6e1621fe53 fix: Steam's Ubisoft games install Ubisoft Connect again
The redistributable seeder marked every process in a game's install scripts as already run, so the Ubisoft Connect installer that Ubisoft games list was skipped and they stopped with "Unable to locate Uplay PC". It is now left to run like the Vulkan runtime installer, and a marker an earlier seeding wrote is taken back out.
2026-10-23 23:58:39 -04:00
XxJSONDeruloxX 546574e982 fix(xbox): enable proot fast path in Xodus 2026-10-23 23:56:24 -04:00
XxJSONDeruloxX b1ab55856e fix(ui): clear search focus and use store accents 2026-10-23 23:56:24 -04:00
XxJSONDeruloxX 4a0eb14d1f fix(xbox): mount selected SD library in Xodus 2026-10-23 23:56:24 -04:00
XxJSONDeruloxX 2c8ae3753c fix(xbox): mount external storage for downloads 2026-10-23 23:56:24 -04:00
Kurt Himebauch fcfbb74068 fix: one refused thermal or power query no longer drops the others from the session report (#991) 2026-10-23 23:43:01 -04:00
MaxsTechReview 0340be5a7d Steam client download: resumes, retries, shows megabytes, and isn't paused out of sight (#587)
A user's first Steam setup never finished on an Honor Pad (Snapdragon 6
Gen 1, metered Wi-Fi with a 14 Mbit/s link). Their log stops at
`downloading Steam: bins_linuxarm64_linuxarm64 (2/17)`. The log was
shared 31 s into the session, so nothing had failed yet. The way the
download works explains why it never got further.

## Why

- **The session pauses the download.** With the default background
behavior (Manual), a session pauses as soon as DroidDeck is hidden or
the screen turns off. In the user's log that happened one second after
start, before the download had begun.
- "Keep downloads running" doesn't help. It's off by default, and it
only recognizes Steam's own game downloads, not the runtime fetching the
client.
- A first run shows nothing for minutes, so leaving the app or letting
the screen time out is exactly what people do.
- **The download is bigger than the screen says.** The client is about
360 MB in 17 parts, and part 2 alone is 110 MB. The loading screen said
"~1-2 min" and counted parts, not bytes, so on a slow link it looks
frozen at about 5%.
- **Nothing handles a bad connection.** Each part was a plain `curl
-fsSL` with no timeout, no retry and no progress output.
  - A stalled connection could wait forever.
- A failed one ended the session and threw away everything fetched, so
the next try started from part 1.

## What changes

- **`droiddeck-steam-install`**:
- Parts go to `package/droiddeck-download` and stay there until the
client is installed.
  - Each part resumes from the bytes on disk (`curl -C -`).
  - A transfer under 4 KB/s for a minute is dropped and resumed.
- Attempts that make no progress back off 5–30 s, then give up with a
clear error. Whatever arrived is kept.
  - A finished part is marked unpacked, so a later session skips it.
- Bytes that fail their checksum are fetched whole again. A server that
won't resume starts the part over. Parts left over from another client
version are dropped.
- A repair redoes every part once, and keeps what an interrupted repair
already redid.
- Every 5 s the step line also carries megabytes overall: `… (2/17), 45
of 360 MB`.
- **Loading screen** (`LoadingState`): shows `Downloading the Steam
client · 45 / 360 MB`, with the bar following the bytes. Older runtimes
still show the part name.
- **`SessionService`**: until the session's first frame, the download
watch also checks whether the client is still to be fetched (no install
stamp or client binary, or a repair requested). While it is:
- the session stays awake with its wake and Wi-Fi locks, whatever the
downloads setting says;
  - the notification shows "Steam download active" with **Suspend now**;
  - a `steam.client_download` event marks start and end.
- The watch is set up before the first suspend decision, so a session
started with the screen off isn't paused first. Once the client is
there, the chosen behavior applies again.
- **`droiddeck-session`**: the step no longer promises 1–2 minutes.

## Testing

- `python3 -m unittest discover -s tools/tests -p 'test_*.py'`: 497 OK,
1 skipped.
- `test_steam_install.py` gets a fake curl that drops connections,
refuses to resume or fails on cue.
- New cases cover: megabyte progress; resuming after a dropped
connection; keeping finished and partial parts across a failed session;
restarting a part the server won't resume; re-fetching bad bytes;
dropping another version's parts; a repair resumed once.
- Kotlin: `:app:testDebugUnitTest --tests
'com.droiddeck.launcher.session.*'` passes. That includes the new
`LoadingStateTest` and the `clientInstallPending` cases in
`SteamDownloadMonitorTest`.
- RedMagic NP06J, with Background behavior set to Manual and "Keep
downloads running" off (the user's settings), and a client repair
requested:
- Started Steam and sent DroidDeck to the background straight away. The
client download stayed active for 30 s with no pause, and all 360 MB
arrived. Then the session paused as Manual should.
- Stopped a second repair during part 2. Part 1 was marked unpacked, the
110 MB part 2 was kept, and the repair marker stayed.
- Started again: it resumed at 110 of 360 MB, unpacked part 2 without
fetching it again, finished, and Steam started normally.
- The loading screen showed `Downloading the Steam client · 248 / 360
MB`.

Retested 2026-10-23 on the REDMAGIC tablet after merging main, which
brought in the stable-channel fallback. Background behavior was set to
Manual with "Keep downloads running" off. A client repair was forced,
then Home was pressed right after Start:

| Build | Session while hidden | Client download |
|---|---|---|
| main | suspended 1 s after Home | stuck at part 3/17 after 90 s |
| this PR | kept awake until the client was in | all 17 parts, 362 MB in
46 s with MB progress; suspended once done |

`tools.tests.test_steam_install` (22), `SteamDownloadMonitorTest` and
`LoadingStateTest` pass.

## Not changed

- **The download is the plain zips.** Valve's manifest also lists `.vz`
(LZMA) versions, 228 MB instead of 360 MB. Using them needs a VZ decoder
in the guest; that could be a follow-up.
- **The stall and retry path** is covered by the tests but wasn't forced
on the device.
2026-10-23 23:28:05 -04:00
maxjivi05 132f40ac97 fix: install step progress cannot overflow 2026-10-23 23:11:34 -04:00
maxjivi05 5981bbb1e2 fix: an oversized install step count falls back to the step text 2026-10-23 22:51:28 -04:00
maxjivi05 7419ba114f fix: the client install stays awake until Steam's first frame and its end is recorded 2026-10-23 22:22:14 -04:00
maxjivi05 96e2bf992d fix: an HTTP 4xx part hands over to the next CDN at once 2026-10-23 21:20:50 -04:00
Kurt Himebauch ad9eb8eb29 xbox backend x86 to arm cross compilation (#1440) 2026-10-23 21:02:20 -04:00
MaxsTechReview 116d0e4f1a fix: a game started from another launcher returns to that launcher (#1438)
A game started from another launcher (ES-DE, a `droiddeck://game/` link,
a pinned shortcut) should send the player back to that launcher when it
closes. Instead DroidDeck always opened the Android home screen, which
is a different place whenever that launcher isn't the default Home app.

## Cause
The link is handled by `FrontEndActivity`, which starts the session in
its own task and stays behind it in a second DroidDeck task. When the
session ended, DroidDeck started `CATEGORY_HOME` to cover that leftover
task, so the player always landed on the system Home.

## Fix
When a link-launched session ends, `SessionActivity` removes the task
that handled the link (a `FrontEndActivity` task whose base intent is a
game link), then removes its own task. Android then shows whatever was
underneath: ES-DE, any other app that launched the game, or Home when
Home launched it.

Sessions started from DroidDeck's own Games tab are unchanged.

## Testing
On a REDMAGIC NP06J, with the `droiddeck://game/945360` link started
using ES-DE's flags (`NEW_TASK | CLEAR_TASK`). Among Us was closed and
"Return to launcher after a game" was on.

| Launched from | main | this branch |
|---|---|---|
| Calculator (stands in for a non-Home frontend) | Android home |
Calculator |
| Home | Android home | Android home |
| DroidDeck's Games tab | DroidDeck | DroidDeck |

## Credits
Reported on Discord by whatsupbros.
2026-10-23 20:45:32 -04:00
maxjivi05 4a57c6f6f1 chore: drop the comments this change added 2026-10-23 20:45:14 -04:00
maxjivi05 2d9d4dbeb0 Merge branch 'main' into fix/steam-client-download
# Conflicts:
#	tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-install
#	tools/tests/test_steam_install.py
2026-10-23 20:23:44 -04:00
MaxsTechReview 866551e797 fix: Snapdragon 8 Gen 2 no longer pins Steam to one core by default (#1441)
## Summary

Since 0.5.0, Snapdragon 8 Gen 2 devices (Retroid Pocket 6, Galaxy S23,
AYN Thor) pin the Steam client, its UI helper and gamescope to the
single prime core whenever the core settings were never touched. The
Steam menu and overlay lag on those devices, and games launched from
Steam get CPU-starved.

This drops that default, so 8 Gen 2 behaves like 0.4.0 and every other
chip: Android schedules Steam on all cores. Core choices users saved in
Performance are kept, and the all-core restore for games launched from a
pinned Steam client stays.

## Evidence

The device reports attached to both bug reports show the default in
effect:

| Device | SoC | Client core override | Client cores |
|---|---|---|---|
| Retroid Pocket 6 | QCS8550 | true (never set) | 7 |
| Galaxy S23 | SM8550 | true (never set) | 7 |

Both reporters say 0.4.0, which left Steam on all 8 cores, was smooth.
The PR that added the default did not benchmark it.

Tablet bench (REDMAGIC, SM8850): the 8 Gen 2 default was reproduced by
pinning Steam to one prime core (7) and games to cores 0-3. The test
scrolls the Big Picture library with the d-pad for 20 s and records
frame pacing. Two runs each:

| Steam client | UI fps | 1% low | p99 frame | off-cadence |
|---|---|---|---|---|
| all cores (this PR) | 114.4 / 115.2 | **62.5 / 62.5** | 16 ms | 4.7 /
4.0 % |
| one core (old default) | 109.7 / 110.9 | 41.7 / 41.7 | 24 ms | 8.0 /
7.0 % |

Among Us ran the same either way (58.9 vs 59.0 fps).

## Tests

`CoreAllocationPrefsTest`: absent preferences leave every core to the
scheduler, and saved choices are kept.

## Credits

- shanume (RP6) and Kacper (S23): reports, and the 8-cores-vs-1 finding.
- Kurt Himebauch (#654): the core allocation work this keeps, apart from
the default.
2026-10-23 20:22:23 -04:00
maxjivi05 a0c6545e77 fix: a link that joins DroidDeck's open task still returns to the caller 2026-10-23 20:22:05 -04:00
Delete228 884671a065 fix: keep select mapping and panel input ownership consistent
Keep Select-as-Steam in the existing one-time profile migration and remove the obsolete bridge flag. Raw SELECT state stays stable across router attachment changes, while explicit native, disabled and local profile bindings remain authoritative.

Let the panel close its shared DeckControls source. Detached sticks, D-pads and face buttons release only their own controls, preserving other trackpad contacts and clicks.

Add seven regressions. All 2,246 JVM tests pass; the debug APK build, session assets and runtime inputs validate. Thor validation remains pending.
2026-10-24 02:52:09 +03:00
maxjivi05 c7bcd82782 fix: Snapdragon 8 Gen 2 no longer pins Steam to one core by default 2026-10-23 19:47:41 -04:00
maxjivi05 caaf6b3a01 fix: keep the link task id when an in-app resume reaches the session 2026-10-23 19:47:32 -04:00
superboo07 8123d5b361 xbox backend x86 to arm cross compilation 2026-10-23 23:40:49 +00:00
Delete228 1dae4bf8ee fix: preserve input lifecycle and system actions
Keep unused pad rings closed during profile and enable changes. Route Steam menu and VR stick-switch pulses independently of primary bindings, preserving physical holds and cancelling stale releases.

Seed replacement routes with retained source state and require held controls to return to neutral. Add nine regressions and document validation: 2,239 JVM tests and the debug APK build pass; device validation remains pending.
2026-10-24 02:30:11 +03:00
Kurt Himebauch 2b4bba63c5 docs: add the Trendshift #1 daily and weekly badges to the README (#1435) 2026-10-23 19:18:00 -04:00
maxjivi05 69b2b8fcdf fix: only close the game-link task that started the session 2026-10-23 19:17:08 -04:00
MaxsTechReview 0d8b5a7cf2 fix: stored components can be deleted after a Proton update (#1434)
After a Proton update, stored DXVK / VKD3D / FEX packages can't be
deleted. Components says they're "in use" by a Proton that's gone
(`DroidDeck-Proton_11-9.1` after updating to 11-11.1) or by a bare
number (`4005446849`).

## Cause
`deletePackage` and the Storage page's in-use check count every recorded
selection:
- **Removed Protons:** swaps on Protons that are no longer installed.
- **Out-of-date swaps:** swaps from before the Proton's version changed.
The launcher already ignores these.
- **Game profiles:** a per-game pick blocked the delete with the "swap
that Proton" message, naming the game's id.

## Fix
- **Out-of-date selections:** a selection only counts while its Proton
is installed and at the version the swap was made on. That's the rule
launches already use. Deleting a package removes its stale entries.
- **Profile picks:** deleting a package also clears it from any game's
component profile, so those games go back to the Proton's own copy.
Storage cleanup still keeps profile picks.
- **Live swaps:** a package swapped into an installed Proton is still
blocked. The message now shows the Proton's name rather than its id.

No new strings.

## Testing
On a REDMAGIC NP06J, with the state from the reports recreated:

| Case | main | this branch |
|---|---|---|
| DXVK swapped into a removed `DroidDeck-Proton_11-9.1` | Deleting
failed: in use in DroidDeck-Proton_11-9.1 | Deleted, stale entry removed
|
| FEX picked in game profile `4005446849` | Deleting failed: in use in
4005446849 | Deleted, profile cleared |
| DXVK swapped into installed Proton Experimental | blocked | blocked:
in use in Proton Experimental (ARM64) |

<img
src="https://raw.githubusercontent.com/maxjivi05/DroidDeck/pr-media/1434/main-stale-proton.jpg"
width="420"> <img
src="https://raw.githubusercontent.com/maxjivi05/DroidDeck/pr-media/1434/main-game-profile.jpg"
width="420">
<img
src="https://raw.githubusercontent.com/maxjivi05/DroidDeck/pr-media/1434/fix-game-profile.jpg"
width="420"> <img
src="https://raw.githubusercontent.com/maxjivi05/DroidDeck/pr-media/1434/fix-live-blocked.jpg"
width="420">

`ComponentsSelectionTest` covers removed, out-of-date and live
selections.

## Credits
Reported on Discord by Siigonis, Bado and woodrow1712.
2026-10-23 19:13:29 -04:00
maxjivi05 7261b26264 fix: a game started from another launcher returns to that launcher 2026-10-23 18:56:44 -04:00
maxjivi05 db9aae7b87 fix: delete the package before saving, and match Protons the same way 2026-10-23 18:56:24 -04:00
delete d202233beb docs: describe input architecture and Thor validation limits 2026-10-24 01:53:51 +03:00
delete 6530faaf78 fix: preserve shared input ownership and reset profile settings 2026-10-24 01:53:51 +03:00
delete 3dd6aeef46 fix: keep mapped desktop pointer visible and update it each frame 2026-10-24 01:43:34 +03:00
delete 45a496b802 fix: preserve temporary-file modes in input preload open wrappers 2026-10-24 01:43:34 +03:00
delete ab8f41b80e Unify input profiles and configurable controller surfaces 2026-10-24 01:43:34 +03:00
maxjivi05 70b8ec5de4 docs: add the Trendshift #1 daily and weekly badges to the README 2026-10-23 18:36:24 -04:00
maxjivi05 94b530ecb2 fix: stored components can be deleted after a Proton update 2026-10-23 18:28:35 -04:00
MaxsTechReview 906e0d1f7d fix: other apps can record game audio (#1430)
Fixes #1314.

Screen recorders and apps that use `AudioPlaybackCapture`
(MediaProjection) get silence from DroidDeck. The app targets SDK 28,
and Android blocks playback capture for apps below SDK 29 unless they
opt in. That was a side effect of the target SDK, not a choice.
`android:allowAudioPlaybackCapture="true"` opts in. Game audio already
plays with `AAUDIO_USAGE_GAME`, a usage that capture allows.

REDMAGIC NP06J with Among Us running, `dumpsys media.audio_policy`
("Allow playback capture log"); the game's streams are AAudio
`USAGE_GAME` with no capture-blocking flags:

| Build | |
| --- | --- |
| main | `uid=10494, allowPlaybackCapture=false,
packageName=com.droiddeck.launcher` |
| this PR | `uid=10494, allowPlaybackCapture=true,
packageName=com.droiddeck.launcher` |

## Credits
Requested and diagnosed by @WilsonNet in #1314.
2026-10-23 18:01:24 -04:00
maxjivi05 992912fdf8 fix: other apps can record game audio 2026-10-23 17:43:44 -04:00
MaxsTechReview 4ff4be2371 fix: keep a slightly early vsync tick for frame pacing (#1405)
Gamescope paces from the presentation timestamp. The Choreographer tick
is often still slightly ahead of the render thread, and that tick was
thrown away, so the draw time was reported instead.

A tick within one refresh of the draw is now kept, whether it is
slightly ahead or slightly behind. A missing tick, or one more than a
refresh away, still falls back to the draw time.
2026-10-23 17:00:32 -04:00
maxjivi05 06946fe8ae chore: drop comments 2026-10-23 16:01:41 -04:00
0x1Nawaf 0a8358d11f fix: keep the gamescope cursor out of the zram change
The session script had also picked up the cursor switch from the other branch.
2026-10-23 15:52:35 +00:00
0x1Nawaf 1f1873a18e fix: compress the client only after the game window is focused 2026-10-23 15:46:00 +00:00
0x1Nawaf 53c63b6dc2 fix: keep a slightly early vsync tick for frame pacing 2026-10-23 15:43:52 +00:00
XxJSONDeruloxX b2185be039 fix: close stalled download streams on cancel 2026-10-23 11:26:41 -04:00
Kurt Himebauch 04dd421699 Merge main and harden diagnostic publication and streaming cleanup 2026-10-23 10:31:46 -04:00
Kurt Himebauch 4babf24433 Merge branch 'main' into fix/install-cancel 2026-10-23 10:16:23 -04:00
Kurt Himebauch a25081df7c Merge branch 'main' into fix/steam-client-download 2026-10-23 10:16:19 -04:00
Danbro007 abd90ce238 test: 隔离凭据测试的应用启动任务 2026-10-23 17:35:24 +08:00
Danbro007 42de5f3dd1 fix: avoid incompatible Turnip selection and explain Steam startup failures 2026-10-23 16:37:04 +08:00
ovurrsl ac032be525 Merge current main into PR #1042 2026-10-23 08:35:44 +03:00
ovurrsl e1e2017590 Merge current main into PR #1042 2026-10-22 21:24:19 +03:00
Kurt Himebauch 1da489aebb Merge current main into PR #1174 2026-10-22 13:11:08 -04:00
Kurt Himebauch e550b3354f Merge current main into PR #1067 2026-10-22 13:11:07 -04:00
Kurt Himebauch 60b9c6602e Merge current main into PR #1061 2026-10-22 13:11:06 -04:00
Kurt Himebauch 18ffc6ea70 Merge current main into PR #1042 2026-10-22 13:11:06 -04:00
Kurt Himebauch 958446ab7c Merge current main into PR #991 2026-10-22 13:11:05 -04:00
Kurt Himebauch bde9ca733f Merge current main into PR #587 2026-10-22 13:11:02 -04:00
ovurrsl 62d218fe37 fix: cancelling the first-run Proton install also stops its catalog download
installProton fetched the catalog before its cancellation existed, so a stop had to wait for the catalog request. The cancellation is now registered first and passed through DesktopCatalog.fetch to a cancellable downloadString, which disconnects the request and does not count a cancelled fetch as a source failure.
2026-10-22 19:36:10 +03:00
Kurt Himebauch 0b616146ff Merge current main into PR #1174 2026-10-22 12:13:52 -04:00
Kurt Himebauch 96a8501343 Merge current main into PR #1067 2026-10-22 12:13:50 -04:00
Kurt Himebauch b4a9dec170 Merge current main into PR #1061 2026-10-22 12:13:50 -04:00
Kurt Himebauch 65a5f69873 Merge current main into PR #1042 2026-10-22 12:13:49 -04:00
Kurt Himebauch 495ff952d2 Merge current main into PR #991 2026-10-22 12:13:48 -04:00
Kurt Himebauch 0819aa725d Merge current main into PR #587 2026-10-22 12:13:46 -04:00
ovurrsl 430e77178e fix: one refused thermal or power query no longer drops the others from the session report
Thermal status, thermal headroom and power saver are now read one by
one, so a vendor that throws on one of them still gets the other two
lines, and the refused one shows as unknown.
2026-10-22 18:48:39 +03:00
Resul Övür 9b176753cc fix: keep ambiguous update-channel guesses temporary 2026-10-22 18:46:40 +03:00
ovurrsl ac8957ae56 fix: an unproven update-channel guess no longer moves stable users to Nightly for good
Any release-signed build not running the catalog's current stable commit
was saved as NIGHTLY, so an older stable APK, a release-day install or a
stale cached catalog put stable users on pre-release updates. The guess
is now saved only when the catalog proves it (PR build, the catalog
stable, or a version older than it); otherwise it applies to this run
and is guessed again with a newer catalog.
2026-10-22 18:46:40 +03:00
xXJsonDeruloXx 88644dcad5 Merge origin/main into cancellable package installs 2026-10-22 10:29:50 -04:00
xXJsonDeruloXx f67ff86470 Merge origin/main into game debug logging 2026-10-22 10:24:09 -04:00
xXJsonDeruloXx 6cbf8f4d73 Merge remote-tracking branch 'origin/main' into fix/pr-1174-gog-installer
# Conflicts:
#	app/src/main/java/com/droiddeck/launcher/stores/gog/GogBackend.kt
#	app/src/main/java/com/droiddeck/launcher/stores/gog/GogDownloadManager.java
2026-10-22 10:16:32 -04:00
xXJsonDeruloXx c8cc17bf3a Merge main into Steam client download PR 2026-10-22 10:01:34 -04:00
xXJsonDeruloXx 0ab2172857 fix: cancel Proton seed downloads promptly 2026-10-22 09:57:45 -04:00
xXJsonDeruloXx c02832ac59 fix: disconnect desktop download on session cancel 2026-10-22 09:56:05 -04:00
xXJsonDeruloXx 4e4a731221 test: cancel downloads after the request starts 2026-10-22 09:52:32 -04:00
Moroni Granja d006538f13 Merge branch 'main' into fix/gog-native-installer-extract 2026-10-21 07:56:53 -03:00
Moroni Granja 38c72c6304 Merge branch 'main' into fix/gog-native-installer-extract 2026-10-20 09:54:16 -03:00
Moroni Granja b9d703db34 fix: extract GOG installers natively on ARM
The GOG installer fallback downloaded only the setup stub and then launched it through Proton/FEX, where the 32-bit Inno Setup stub corrupts its own path strings before CreateFileW. Download every installer part and extract them with the bundled ARM64 innoextract in the Linux guest instead, registering the real game executable. Also classify truncated response bodies as server faults so the native engine retries on the other CDN mirror.
2026-10-20 08:09:20 -03:00
ovurrsl b3483349e3 fix(install): check cancellation around retries/completion and keep install worker ownership 2026-10-19 16:18:04 +03:00
ovurrsl f6d44376ed fix: cancelling the first-run desktop or Proton install stops its download
Cancel closed the session screen, but the desktop or Proton download kept
running with no UI, and a relaunch saw the stale stop flag and closed
itself. Downloads now take a cancel check, DesktopCatalog.install stops
before changing anything on disk, the install state is process-wide, and
a new session is refused while a cancelled install is still winding down.
2026-10-19 15:12:16 +03:00
xXJsonDeruloXx 6d105c1649 chore: merge main to catch up with 0.5.0 2026-10-18 17:05:49 -04:00
xXJsonDeruloXx 5de7af9b41 chore: merge main to catch up with 0.5.0 2026-10-18 16:52:02 -04:00
wallentx cbd3d8c313 Merge remote-tracking branch 'upstream/main' into wallentx/game-debug-logging 2026-10-18 12:37:08 -05:00
wallentx 93c358c541 Assert empty crash snapshot retention 2026-10-18 04:26:34 -05:00
wallentx 5c55b84d94 Preserve recovered crash snapshots 2026-10-18 04:15:04 -05:00
wallentx 3e90875be8 Restrict library redaction exceptions to loader diagnostics 2026-10-18 04:05:36 -05:00
wallentx 4ab4a86507 Defer abandoned log scrubbing past launch 2026-10-18 03:56:09 -05:00
wallentx 66f5d39bab Fix game diagnostics recovery races 2026-10-18 03:36:30 -05:00
wallentx 536bb11df8 Preserve Android shared-library names in logs 2026-10-18 03:22:32 -05:00
wallentx 6521c3c3ad Add opt-in game debug logs and bounded log sharing 2026-10-18 02:22:15 -05:00
MaxsTechReview 11bd604dfd Merge branch 'main' into fix/steam-client-download 2026-10-11 15:53:38 -04:00
MaxsTechReview b834a92244 Merge branch 'main' into fix/steam-client-download 2026-10-11 15:51:21 -04:00
maxjivi05 0cd7f07725 fix: a first run's Steam client download is not paused out of sight
With the default background behavior (Manual), a session pauses as soon
as DroidDeck is hidden or the screen turns off - one second after start in
the report this came from, before the download had begun. "Keep downloads
running" did not help: it is off by default, and it only recognizes the
client's own game downloads, not the runtime fetching the client itself.

Until the session shows its first frame, the download watch now also
asks whether the client is still to be fetched (no install stamp or no
client binary, or a repair asked for). While it is, the session is kept
awake with its wake and Wi-Fi locks whatever the downloads setting says,
the notification reads "Steam download active" with Suspend now, and a
steam.client_download event marks start and end. The watch is set up
before the first suspend decision, so a session started with the screen
off is not paused first. Once the client is there, the chosen behavior
applies again.
2026-10-11 14:45:57 -04:00
maxjivi05 342d2e7f20 fix: the Steam client download resumes, retries and shows megabytes
A first run downloads the native client, about 360 MB in 17 parts, before
anything is on screen. The script ran plain `curl -fsSL` per part: no
timeout, no retry, no progress. A stalled connection could wait for ever
on "downloading Steam: bins_linuxarm64_linuxarm64 (2/17)" (a 110 MB part),
a failed one ended the session and threw away everything fetched, and
the loading screen said "~1-2 min" while counting parts, not bytes.

Parts now go to package/droiddeck-download and stay there until the client
is installed. Each one resumes from the bytes on disk (curl -C -), a
transfer under 4 KB/s for a minute is dropped and resumed, attempts
without progress back off 5-30 s before giving up, and a finished part is
marked unpacked so a later session skips it. Bytes that fail their
checksum are fetched whole again; a server that will not resume starts the
part over; parts left for another client version are dropped. A repair
redoes every part once and keeps what an interrupted repair redid.

Every 5 s the step line carries megabytes overall ("(2/17), 45 of 360
MB"), which the loading screen shows as "45 / 360 MB" with the bar
following the bytes. The session's step no longer promises 1-2 minutes.
2026-10-11 14:45:57 -04:00
1774 changed files with 182035 additions and 12716 deletions

No files matched your search

+1 -1
View File
@@ -2,8 +2,8 @@ tools/linuxfs/** text=auto eol=lf
tools/mangoapp/** text=auto eol=lf
tools/comet/LICENSE whitespace=-blank-at-eof
tools/comet/droiddeck.patch whitespace=-blank-at-eol,-blank-at-eof
tools/gamescope/patches/0900-enhanced-frame-diagnostics.patch whitespace=-blank-at-eol,-blank-at-eof,-space-before-tab
# These helpers are packaged verbatim and executed inside the Linux runtime.
tools/linuxfs/overlay/** text eol=lf
tools/linuxfs/desktop/** text eol=lf
tools/mangoapp/mangoapp text eol=lf
+5
View File
@@ -0,0 +1,5 @@
ASEbrahim
FX192
K11MCH1
kingokksa
ovurrsl
+9 -5
View File
@@ -60,11 +60,12 @@ jobs:
- name: Build
run: |
set -euo pipefail
sudo apt-get update -qq && sudo apt-get install -y -qq zstd xz-utils gpgv >/dev/null
KDE_KWIN_OVERRIDE="$PWD/out/kwin" tools/desktop-kde/build.sh out
sudo apt-get update -qq && sudo apt-get install -y -qq zstd xz-utils gpgv gcc-aarch64-linux-gnu >/dev/null
KDE_KWIN_OVERRIDE="$PWD/out/kwin" DESKTOP_KDE_RELEASE_TAG='${{ inputs.tag }}' tools/desktop-kde/build.sh out
cp out/kwin/desktop-kde.kwin.txt out/
echo "size $(stat -c %s out/desktop-kde.tar.zst) sha256 $(cat out/desktop-kde.sha256)"
echo "language packs $(ls out/desktop-kde-i18n-*.tar.zst | wc -l)"
echo "pacman repository $(ls out/repo | wc -l) files, $(du -sm out/repo | cut -f1) MiB"
- uses: actions/upload-artifact@v4
with:
name: desktop-kde
@@ -77,6 +78,7 @@ jobs:
out/desktop-kde.sha256
out/desktop-kde.i18n.txt
out/desktop-kde-i18n-*.tar.zst
out/repo/
- name: Publish
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
env:
@@ -89,8 +91,10 @@ jobs:
echo "::error::$TAG is already published in DroidDeck-Components, and published tags are never replaced: use a new tag"
exit 1
fi
gh release create "$TAG" --latest=false --prerelease=false --title "$TAG" \
prerelease=false
case $TAG in *-rc*) prerelease=true ;; esac
gh release create "$TAG" --latest=false --prerelease="$prerelease" --title "$TAG" \
--notes "KDE Plasma desktop package for DroidDeck's Linux runtime ($GITHUB_REPOSITORY tools/desktop-kde, $GITHUB_SHA). size $(stat -c %s out/desktop-kde.tar.zst), sha256 $(cat out/desktop-kde.sha256). desktop-kde.packages.txt lists the signature-checked Arch Linux ARM packages it was built from." \
out/desktop-kde.tar.zst out/desktop-kde.sha256 out/desktop-kde.packages.txt out/desktop-kde.kwin.txt \
out/desktop-kde.i18n.txt out/desktop-kde-i18n-*.tar.zst
gh release edit "$TAG" --latest=false --prerelease=false
out/desktop-kde.i18n.txt out/desktop-kde-i18n-*.tar.zst out/repo/*
gh release edit "$TAG" --latest=false --prerelease="$prerelease"
-4
View File
@@ -1,8 +1,5 @@
name: Build FEX (x86-64 Steam client)
# Checks tools/fex: the same build DroidDeck-Components' build-fex.yml runs from this repository to publish FEX
# (its single `fex` release and fex.json, which the app follows). Nothing is published from here.
on:
push:
paths:
@@ -31,7 +28,6 @@ jobs:
with:
persist-credentials: false
- name: Build in Arch Linux ARM
# Inputs reach the shell as environment variables, never pasted into the script.
env:
FEX_EXTRA_CXXFLAGS: ${{ inputs.cxxflags }}
FEX_THUNKS: ${{ inputs.thunks == 'true' && '1' || '0' }}
+25 -4
View File
@@ -2,8 +2,11 @@ name: Build gamescope (patched)
on:
push:
branches: [main]
paths:
- 'tools/gamescope/**'
- 'tools/gamescope/build-in-arch.sh'
- 'tools/gamescope/patches/**'
- 'tools/gamescope/runtime-sonames.txt'
- '.github/workflows/build-gamescope.yml'
workflow_dispatch:
inputs:
@@ -15,6 +18,10 @@ on:
permissions:
contents: write
concurrency:
group: ${{ inputs.tag && format('gamescope-tag-{0}', inputs.tag) || format('gamescope-{0}-{1}', github.event_name, github.ref) }}
cancel-in-progress: ${{ github.event_name == 'push' }}
jobs:
build:
runs-on: ubuntu-24.04-arm
@@ -32,11 +39,25 @@ jobs:
path: |
gamescope.tzst
gamescope.tzst.sha256
- uses: actions/create-github-app-token@v2
id: components-token
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
with:
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
private-key: ${{ secrets.DROIDDECK_CI_APP_KEY }}
owner: Droid-Deck
repositories: DroidDeck-Components
- name: Publish
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
env:
GH_TOKEN: ${{ github.token }}
GH_TOKEN: ${{ steps.components-token.outputs.token }}
GH_REPO: Droid-Deck/DroidDeck-Components
TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
gh release create "${{ inputs.tag }}" --latest=false --prerelease=false --title "${{ inputs.tag }}" --notes "gamescope 3.16.29 with tools/gamescope/patches; staged by the apk over /usr/local/bin/gamescope. sha256 $(cut -d' ' -f1 gamescope.tzst.sha256)" gamescope.tzst gamescope.tzst.sha256 || gh release upload "${{ inputs.tag }}" gamescope.tzst gamescope.tzst.sha256 --clobber
gh release edit "${{ inputs.tag }}" --latest=false --prerelease=false
if gh release view "$TAG" >/dev/null 2>&1; then
echo "::error::$TAG is already published in DroidDeck-Components, and published tags are never replaced: use a new tag"
exit 1
fi
gh release create "$TAG" --latest=false --prerelease=false --title "$TAG" --notes "gamescope 3.16.29 with tools/gamescope/patches ($GITHUB_REPOSITORY $GITHUB_SHA, run $GITHUB_RUN_ID); staged by the apk over /usr/local/bin/gamescope. sha256 $(cut -d' ' -f1 gamescope.tzst.sha256)" gamescope.tzst gamescope.tzst.sha256
gh release edit "$TAG" --latest=false --prerelease=false
@@ -0,0 +1,95 @@
name: Build Linux runtime (Ubuntu)
on:
push:
paths:
- 'tools/ubuntu/**'
- '.github/workflows/build-ubuntu-runtime.yml'
workflow_dispatch:
inputs:
tag:
description: 'Release tag to publish as (e.g. ubuntu-r1); empty = artifact only'
required: false
default: ''
permissions:
contents: read
jobs:
debs:
runs-on: ubuntu-24.04-arm
timeout-minutes: 240
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Build Mesa with KGSL and the patched KWin
run: |
set -euo pipefail
docker run --rm -v "$PWD:/work" -w /work ubuntu:26.04 bash tools/ubuntu/build-debs.sh /work/out/debs
sudo chown -R "$(id -u):$(id -g)" out
- uses: actions/upload-artifact@v4
with:
name: ubuntu-runtime-debs
retention-days: 7
compression-level: 0
path: out/debs/
rootfs:
needs: debs
runs-on: ubuntu-24.04-arm
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
with:
name: ubuntu-runtime-debs
path: out/debs
- uses: actions/create-github-app-token@v2
id: components-token
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
with:
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
private-key: ${{ secrets.DROIDDECK_CI_APP_KEY }}
owner: Droid-Deck
repositories: DroidDeck-Components
- name: Refuse a published tag
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
env:
GH_TOKEN: ${{ steps.components-token.outputs.token }}
run: |
if gh release view '${{ inputs.tag }}' -R Droid-Deck/DroidDeck-Components >/dev/null 2>&1; then
echo "::error::${{ inputs.tag }} is already published, and published tags are never replaced: use a new tag"
exit 1
fi
- name: Build
run: |
set -euo pipefail
sudo apt-get update -qq && sudo apt-get install -y -qq zstd gpgv ubuntu-keyring >/dev/null
tag='${{ inputs.tag }}'
tools/ubuntu/build.sh out/debs out "${tag:-ubuntu-dev}"
- uses: actions/upload-artifact@v4
with:
name: ubuntu-runtime
retention-days: 7
compression-level: 0
path: |
out/*.tar.zst
out/*.sha256
out/*.packages.txt
- name: Publish
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
env:
GH_TOKEN: ${{ steps.components-token.outputs.token }}
GH_REPO: Droid-Deck/DroidDeck-Components
run: |
set -euo pipefail
TAG='${{ inputs.tag }}'
prerelease=false
case $TAG in *-rc*) prerelease=true ;; esac
gh release create "$TAG" --latest=false --prerelease="$prerelease" --title "$TAG" \
--notes "Ubuntu 26.04 LTS arm64 runtime for DroidDeck ($GITHUB_REPOSITORY tools/ubuntu, $GITHUB_SHA): size $(stat -c %s "out/$TAG.tar.zst"), sha256 $(cat "out/$TAG.sha256"). $TAG-desktop.tar.zst is the Kubuntu Plasma desktop built on top of it, holding only what the runtime does not already have: size $(stat -c %s "out/$TAG-desktop.tar.zst"), sha256 $(cat "out/$TAG-desktop.sha256"). The .packages.txt files list every installed package and version." \
"out/$TAG.tar.zst" "out/$TAG.sha256" "out/$TAG.packages.txt" \
"out/$TAG-desktop.tar.zst" "out/$TAG-desktop.sha256" "out/$TAG-desktop.packages.txt" out/debs/*.deb
gh release edit "$TAG" --latest=false --prerelease="$prerelease"
+31
View File
@@ -0,0 +1,31 @@
name: Build Venus (Mali)
# The Mali (Venus) route's binaries from tools/venus (David Cimo's recipes): Mesa's Venus ICD and the
# Mali compatibility layer (glibc, for the session) and virglrenderer's vtest server (bionic, run by
# the app over the phone's own Vulkan driver), packed as venus.tzst rooted at app/src/main. The apk
# build fetches it by tools/venus/release.env (tag + sha256); nothing compiled is kept in this repo.
on:
workflow_dispatch:
push:
branches: [test/non-adreno-all]
paths: ['tools/venus/**', '.github/workflows/build-venus.yml']
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-24.04
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- name: Tools
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq ninja-build pkg-config zstd python3-pip
pip3 install --break-system-packages 'meson>=1.5'
- name: Build
run: ANDROID_NDK_HOME="$ANDROID_NDK_LATEST_HOME" WORK=$PWD/venus-work tools/venus/ci-build.sh
- uses: actions/upload-artifact@v4
with:
name: venus
path: |
venus-work/venus.tzst
venus-work/venus.tzst.sha256
+196 -19
View File
@@ -8,6 +8,8 @@ on:
- '**/*.md'
- 'LICENSE'
- '.gitignore'
- 'docs/**'
- '.github/ISSUE_TEMPLATE/**'
pull_request:
branches: [ main ]
types: [ opened, synchronize, reopened, ready_for_review ]
@@ -15,6 +17,8 @@ on:
- '**/*.md'
- 'LICENSE'
- '.gitignore'
- 'docs/**'
- '.github/ISSUE_TEMPLATE/**'
workflow_dispatch:
inputs:
rebuild_preloads:
@@ -28,8 +32,8 @@ permissions:
pull-requests: write
concurrency:
group: build-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
group: build-${{ github.event.pull_request.number || (github.event_name == 'push' && 'main') || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}
jobs:
source:
@@ -45,6 +49,10 @@ jobs:
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
sparse-checkout: |
/*
!/docs/releases/media/
sparse-checkout-cone-mode: false
persist-credentials: false
- name: Pin and merge the APK source
id: merge
@@ -55,6 +63,8 @@ jobs:
echo "base-sha=$(git rev-parse origin/main)" >> "$GITHUB_OUTPUT"
git config user.name "ci"
git config user.email "[email protected]"
# Partial clone: a merged blob that only exists in a fork's history can't be lazily fetched (GitHub: "not our ref"); merge-tree writes it locally first.
git merge-tree --write-tree HEAD origin/main >/dev/null || true
git merge --no-ff --no-edit origin/main
- name: Resolve trusted manifest base
id: trusted
@@ -78,6 +88,10 @@ jobs:
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
sparse-checkout: |
/*
!/docs/releases/media/
sparse-checkout-cone-mode: false
persist-credentials: false
- name: Merge the pinned APK base
if: github.event_name == 'pull_request'
@@ -88,6 +102,7 @@ jobs:
git fetch --no-tags origin "$BUILD_BASE_SHA"
git config user.name "ci"
git config user.email "[email protected]"
git merge-tree --write-tree HEAD FETCH_HEAD >/dev/null || true
git merge --no-ff --no-edit FETCH_HEAD
- name: Verify pinned source tree
env:
@@ -121,17 +136,28 @@ jobs:
if-no-files-found: error
retention-days: 14
components:
name: Prepare verified components
components-runner:
needs: source
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
runs-on: ubuntu-24.04
uses: ./.github/workflows/pick-runner.yml
secrets:
RUNNER_STATUS_TOKEN: ${{ secrets.RUNNER_STATUS_TOKEN }}
components:
name: Prepare verified components
needs: [source, components-runner]
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
runs-on: ${{ fromJSON(needs.components-runner.outputs.labels) }}
timeout-minutes: 40
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
sparse-checkout: |
/*
!/docs/releases/media/
sparse-checkout-cone-mode: false
persist-credentials: false
- name: Merge the pinned APK base
if: github.event_name == 'pull_request'
@@ -142,6 +168,7 @@ jobs:
git fetch --no-tags origin "$BUILD_BASE_SHA"
git config user.name "ci"
git config user.email "[email protected]"
git merge-tree --write-tree HEAD FETCH_HEAD >/dev/null || true
git merge --no-ff --no-edit FETCH_HEAD
- name: Verify pinned source tree
env:
@@ -149,6 +176,8 @@ jobs:
run: test "$(git rev-parse 'HEAD^{tree}')" = "$SOURCE_TREE"
- name: Resolve the pinned Android NDK
run: bash tools/ci/resolve-ndk.sh
- name: Check out MaliBridge
run: git submodule update --init external/MaliBridge
- name: Plan component identities
id: plan
@@ -212,6 +241,7 @@ jobs:
${{ steps.preloads.outputs.cache-hit != 'true' && 'runtime' || '' }}
${{ steps.component-preloads.outputs.arm64 != 'true' && 'arm64-preloads' || '' }}
${{ steps.comet.outputs.cache-hit != 'true' && 'comet' || '' }}
vkbridge
run: python3 tools/components/prepare.py tools
- name: Build changed ARM64 preloads
@@ -222,6 +252,9 @@ jobs:
if: steps.preloads.outputs.cache-hit != 'true'
run: python3 tools/components/prepare.py runtime
- name: Build the Mali bridge's Linux side
run: python3 tools/components/prepare.py vkbridge
- name: Fetch the Debian image the preloads build in
if: steps.component-preloads.outputs.x86 != 'true'
env:
@@ -249,15 +282,37 @@ jobs:
if: steps.proot.outputs.cache-hit != 'true'
run: python3 tools/components/prepare.py proot
- name: Build the non-Adreno test additions (Mali bridge client, Venus GL probe, Xclipse layer)
run: |
set -euo pipefail
bash tools/ci/install-ubuntu-packages.sh gcc-aarch64-linux-gnu libvulkan-dev
D=app/src/main/assets/linuxfs
mkdir -p "$D/usr/local/bin" "$D/usr/local/lib/droiddeck-xclipse"
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -Wextra -Wno-unused-parameter -fvisibility=hidden -pthread \
-o "$D/usr/local/lib/droiddeck-xclipse/libVkLayer_droiddeck_xclipse.so" tools/linuxfs/xclipse-layer/xclipse_layer.c -ldl
aarch64-linux-gnu-strip --strip-unneeded "$D/usr/local/lib/droiddeck-xclipse/libVkLayer_droiddeck_xclipse.so"
install -m644 tools/linuxfs/xclipse-layer/VkLayer_droiddeck_xclipse.json "$D/usr/local/lib/droiddeck-xclipse/"
aarch64-linux-gnu-gcc -O2 -Wall -o "$D/usr/local/bin/droiddeck-glprobe" tools/venus/glprobe.c -ldl
aarch64-linux-gnu-strip --strip-unneeded "$D/usr/local/bin/droiddeck-glprobe"
- name: Check the preloads
run: |
set -euo pipefail
bash tools/linuxfs/check-preloads.sh app/src/main/assets/linuxfs
D=app/src/main/assets/linuxfs
VKB="$D/usr/local/lib/vkbridge/libvulkan_droidbridge.so"
test -f "$VKB" && test -f "$D/usr/local/share/vkbridge/droidbridge_icd.json" && test -f "$D/usr/local/bin/droiddeck-vkbridge-smoke"
VKB_NEED=$(readelf -d "$VKB" | sed -n 's/.*NEEDED.*\[\(.*\)\]/\1/p' | grep -vx 'libc.so.6' | grep -vx 'ld-linux-aarch64.so.1' || true)
[ -z "$VKB_NEED" ] || { echo "::error::the bridge ICD needs $VKB_NEED"; exit 1; }
[ "$(readelf -Ws "$VKB" | awk '$4 == "FUNC" && $5 == "GLOBAL" && $7 != "UND" {sub(/@.*/, "", $8); print $8}' | grep -c '^vk_icd')" = 3 ] || { echo "::error::the bridge ICD does not export the loader interface"; exit 1; }
- name: Build GOG Galaxy companion
if: steps.comet.outputs.cache-hit != 'true'
run: python3 tools/components/prepare.py comet
- name: Build the DiRT 3 / DiRT Showdown ws2_32 stand-in
run: python3 tools/components/prepare.py dirt-ws2
- name: Prepare graphics driver variants
run: python3 tools/quest/build-driver.py "$ANDROID_HOME/ndk/$NDK_VERSION"
- name: Record and pack prepared components
@@ -270,11 +325,18 @@ jobs:
compression-level: 0
retention-days: 14
build-runner:
needs: [components]
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
uses: ./.github/workflows/pick-runner.yml
secrets:
RUNNER_STATUS_TOKEN: ${{ secrets.RUNNER_STATUS_TOKEN }}
build:
needs: [source, components, xbox]
needs: [source, components, xbox, build-runner]
name: Compile APK and Android tests
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
runs-on: ubuntu-24.04
runs-on: ${{ fromJSON(needs.build-runner.outputs.labels) }}
timeout-minutes: 45
outputs:
variants: ${{ steps.variants.outputs.list }}
@@ -283,7 +345,12 @@ jobs:
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
sparse-checkout: |
/*
!/docs/releases/media/
sparse-checkout-cone-mode: false
persist-credentials: false
submodules: recursive
- name: Check human authorship
env:
@@ -344,6 +411,7 @@ jobs:
git fetch --no-tags origin "$BUILD_BASE_SHA"
BASE=$(git rev-parse --short FETCH_HEAD)
HEAD=$(git rev-parse --short HEAD)
git merge-tree --write-tree HEAD FETCH_HEAD >/dev/null || true
if git merge --no-ff --no-edit FETCH_HEAD >/tmp/merge.out 2>&1; then
{
echo "### Mergeable"
@@ -404,7 +472,7 @@ jobs:
- uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}
cache-cleanup: on-success
cache-cleanup: ${{ github.event_name == 'pull_request' && 'never' || 'on-success' }}
- name: Resolve the pinned Android NDK
run: bash tools/ci/resolve-ndk.sh
@@ -413,6 +481,7 @@ jobs:
id: build-label
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BUILD_REF: ${{ github.ref_name }}
BUILD_SHA: ${{ github.sha }}
@@ -431,6 +500,7 @@ jobs:
echo "label=$label" >> "$GITHUB_OUTPUT"
echo "DROIDDECK_SOURCE_COMMIT=${PR_HEAD_SHA:-$BUILD_SHA}" >> "$GITHUB_ENV"
echo "DROIDDECK_PR_NUMBER=${PR_NUMBER:-0}" >> "$GITHUB_ENV"
echo "DROIDDECK_PR_TITLE=${PR_TITLE//$'\n'/ }" >> "$GITHUB_ENV"
echo "Build: \`$label\`" >> "$GITHUB_STEP_SUMMARY"
- name: Rust toolchain for the store engines (aarch64-linux-android)
@@ -492,7 +562,58 @@ jobs:
key: pack-inputs-v1-${{ hashFiles('tools/packs/build.sh', 'tools/packs/fetch-verified.sh', 'tools/packs/tracking/release.env') }}
restore-keys: pack-inputs-v1-
- name: Build the add-on packs
run: bash tools/packs/build.sh app/build/packs
run: |
python3 tools/powervr/fetch.py
bash tools/packs/build.sh app/build/packs
- name: Pin the Syncthing source
id: syncthing-src
run: |
set -euo pipefail
git submodule update --init --recursive tools/syncthing/syncthing-android
echo "commit=$(git -C tools/syncthing/syncthing-android/syncthing/src/github.com/syncthing/syncthing rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Cache Syncthing
id: syncthing
uses: actions/cache@v4
with:
path: app/src/main/jniLibs/arm64-v8a/libsyncthingnative.so
key: syncthing-${{ env.NDK_VERSION }}-${{ hashFiles('tools/syncthing/build.sh', 'tools/syncthing/syncthing-android/gradle/libs.versions.toml') }}-${{ steps.syncthing-src.outputs.commit }}
- uses: actions/setup-go@v5
if: steps.syncthing.outputs.cache-hit != 'true'
with:
go-version: stable
cache: false
- name: Build Syncthing
if: steps.syncthing.outputs.cache-hit != 'true'
run: |
set -euo pipefail
export NDK="$ANDROID_HOME/ndk/$NDK_VERSION"
tools/syncthing/build.sh app/src/main/jniLibs/arm64-v8a
- name: Note when the proot pack pin is behind this build
run: |
set -euo pipefail
# shellcheck disable=SC1091
source tools/packs/proot/release.env
sha=$(sha256sum app/build/packs/proot.tar.zst | cut -d' ' -f1)
if [ "$sha" != "$PROOT_PACK_SHA256" ]; then
echo "::warning::built proot pack $sha does not match tools/packs/proot/release.env ($PROOT_PACK_SHA256). This build publishes the pack it just built. Source builds keep downloading the pinned pack until release.env is updated."
fi
- name: Check out MaliBridge
run: git submodule update --init external/MaliBridge
- name: Fetch the Mali (Venus) component (tools/venus/release.env)
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
. tools/venus/release.env
gh release download "${VENUS_TAG}" -R "${VENUS_REPO}" -p venus.tzst -O venus.tzst
echo "${VENUS_SHA256} venus.tzst" | sha256sum -c -
tar --use-compress-program=unzstd -xf venus.tzst -C app/src/main
test -f app/src/main/jniLibs/arm64-v8a/libvirgl_test_server.so
test -f app/src/main/assets/venus/libvulkan_virtio.so
rm venus.tzst
- name: Build
id: build
@@ -501,6 +622,7 @@ jobs:
SGDB_API_KEY: ${{ secrets.SGDB_API_KEY }}
run: |
set -o pipefail
python3 tools/powervr/fetch.py
python3 tools/release/runtime_inputs.py record
./gradlew assembleRelease --console=plain -PndkVersion="$NDK_VERSION" 2>&1 | tee /tmp/build.log
python3 tools/release/check_session_assets.py app/build/outputs/apk/release/app-release.apk
@@ -516,13 +638,6 @@ jobs:
git diff --exit-code -- app/src/main/rust/stores/Cargo.lock \
|| { echo "::error::Cargo.lock changed during the build; commit the regenerated lockfile (uploaded as stores-cargo-lock)"; exit 1; }
- name: Store engines host unit tests
timeout-minutes: 10
continue-on-error: true
run: |
cd app/src/main/rust/stores
cargo test --locked 2>&1 | tail -60
- name: Upload the regenerated Cargo.lock
if: failure()
uses: actions/upload-artifact@v4
@@ -610,9 +725,66 @@ jobs:
UNIT_TEST_OUTCOME: ${{ steps.unit-tests.outcome }}
TEST_DIAGNOSTICS_URL: ${{ steps.unit-test-diagnostics.outputs.artifact-url }}
APK_OUTCOME: ${{ steps.apk-upload.outcome }}
SAME_REPO: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
run: bash tools/release/report-build.sh
store-tests:
name: Store engines host tests
needs: source
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
runs-on: ubuntu-24.04
timeout-minutes: 15
continue-on-error: true
permissions:
contents: read
actions: read
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
sparse-checkout: |
/*
!/docs/releases/media/
sparse-checkout-cone-mode: false
persist-credentials: false
- name: Merge the pinned APK base
if: github.event_name == 'pull_request'
env:
BUILD_BASE_SHA: ${{ needs.source.outputs.base-sha }}
run: |
set -euo pipefail
git fetch --no-tags origin "$BUILD_BASE_SHA"
git config user.name "ci"
git config user.email "[email protected]"
git merge-tree --write-tree HEAD FETCH_HEAD >/dev/null || true
git merge --no-ff --no-edit FETCH_HEAD
- name: Verify backend and test source match
env:
XBOX_SOURCE_TREE: ${{ needs.source.outputs.tree-sha }}
run: test "$(git rev-parse 'HEAD^{tree}')" = "$XBOX_SOURCE_TREE"
- name: Rust toolchain
id: rust
run: echo "rustc=$(rustc -V | awk '{print $2}')" >> "$GITHUB_OUTPUT"
- name: Cache cargo registry and the host test target
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
app/src/main/rust/stores/target
key: stores-host-v1-${{ runner.os }}-${{ runner.arch }}-${{ steps.rust.outputs.rustc }}-${{ hashFiles('app/src/main/rust/stores/Cargo.lock') }}-${{ hashFiles('app/src/main/rust/stores/src/**', 'app/src/main/rust/stores/Cargo.toml', 'app/src/main/rust/stores/build.rs') }}
restore-keys: |
stores-host-v1-${{ runner.os }}-${{ runner.arch }}-${{ steps.rust.outputs.rustc }}-${{ hashFiles('app/src/main/rust/stores/Cargo.lock') }}-
stores-host-v1-${{ runner.os }}-${{ runner.arch }}-${{ steps.rust.outputs.rustc }}-
- name: Store engines host unit tests
timeout-minutes: 10
continue-on-error: true
run: |
set -o pipefail
cd app/src/main/rust/stores
cargo test --locked 2>&1 | tail -60
python-tests:
name: Python and native regression tests
needs: source
@@ -624,6 +796,10 @@ jobs:
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
sparse-checkout: |
/*
!/docs/releases/media/
sparse-checkout-cone-mode: false
persist-credentials: false
- name: Merge the pinned APK base
if: github.event_name == 'pull_request'
@@ -634,6 +810,7 @@ jobs:
git fetch --no-tags origin "$BUILD_BASE_SHA"
git config user.name "ci"
git config user.email "[email protected]"
git merge-tree --write-tree HEAD FETCH_HEAD >/dev/null || true
git merge --no-ff --no-edit FETCH_HEAD
- name: Verify backend and test source match
env:
@@ -672,12 +849,12 @@ jobs:
timeout 10 /tmp/test-presenter-shutdown
- name: Steam import and game environment tests
run: python3 -m unittest discover -s tools/tests -p 'test_*.py'
run: bash tools/ci/run-python-tests.sh
checks:
name: Compile and test APK
needs: [build, python-tests]
if: ${{ always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
if: ${{ !cancelled() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
+38
View File
@@ -0,0 +1,38 @@
name: Pick runner
on:
workflow_call:
outputs:
labels:
value: ${{ jobs.pick.outputs.labels }}
secrets:
RUNNER_STATUS_TOKEN:
required: false
permissions: {}
jobs:
pick:
name: Pick runner
runs-on: ubuntu-24.04
timeout-minutes: 2
outputs:
labels: ${{ steps.runner.outputs.labels || '"ubuntu-24.04"' }}
steps:
- name: Pick the compile runner
id: runner
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.RUNNER_STATUS_TOKEN }}
run: |
set -euo pipefail
labels='"ubuntu-24.04"'
if [ -n "$GH_TOKEN" ]; then
idle=$(gh api "orgs/${GITHUB_REPOSITORY_OWNER}/actions/runners" --jq '[.runners[] | select(.status == "online" and .busy == false and any(.labels[]; .name == "droiddeck-ws"))] | length' 2>/dev/null || echo 0)
if [ "${idle:-0}" -gt 0 ]; then
labels='["self-hosted","droiddeck-ws"]'
fi
fi
echo "labels=$labels" >> "$GITHUB_OUTPUT"
echo "Compile runner: \`$labels\`" >> "$GITHUB_STEP_SUMMARY"
+10 -2
View File
@@ -47,10 +47,18 @@ jobs:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
retry() {
for attempt in 1 2 3 4; do
"$@" && return 0
[ "$attempt" = 4 ] || sleep $((attempt * 20))
done
return 1
}
existing=$(retry gh api --paginate "repos/Droid-Deck/DroidDeck-CI/releases?per_page=100" --jq '.[].tag_name')
found=0
for tag in "pr-$PR_NUMBER" "pr-$PR_NUMBER.0"; do
if gh release view "$tag" -R Droid-Deck/DroidDeck-CI >/dev/null 2>&1; then
gh release delete "$tag" -R Droid-Deck/DroidDeck-CI --yes --cleanup-tag
if grep -qxF "$tag" <<<"$existing"; then
retry gh release delete "$tag" -R Droid-Deck/DroidDeck-CI --yes --cleanup-tag
echo "Deleted $tag"
found=1
fi
+103 -31
View File
@@ -8,6 +8,10 @@ on:
description: Open pull request number
required: true
type: string
automatic_build_run_id:
description: Completed build run for automatic publishing
required: false
type: string
pull_request_target:
types: [labeled]
branches: [main]
@@ -27,16 +31,47 @@ concurrency:
queue: max
jobs:
dispatch-fork-builds:
if: >-
github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_repository.full_name != github.repository
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
actions: write
pull-requests: read
steps:
- name: Find fork PRs for the completed build
env:
GH_TOKEN: ${{ github.token }}
RUN_ID: ${{ github.event.workflow_run.id }}
RUN_SHA: ${{ github.event.workflow_run.head_sha }}
RUN_REPO: ${{ github.event.workflow_run.head_repository.full_name }}
RUN_BRANCH: ${{ github.event.workflow_run.head_branch }}
run: |
set -euo pipefail
owner=${RUN_REPO%%/*}
gh api --paginate --slurp -X GET "repos/$GITHUB_REPOSITORY/pulls" \
-f state=open -f base=main -f "head=$owner:$RUN_BRANCH" -f per_page=100 |
jq -r --arg sha "$RUN_SHA" --arg repo "$RUN_REPO" --arg branch "$RUN_BRANCH" '
.[][] | select(.head.sha == $sha and .head.repo.full_name == $repo and .head.ref == $branch) | .number' |
while IFS= read -r number; do
gh workflow run pr-release-publish.yml -R "$GITHUB_REPOSITORY" --ref main \
-f "pr_number=$number" -f "automatic_build_run_id=$RUN_ID"
done
authorize:
if: >-
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') ||
(github.event_name == 'pull_request_target' && github.event.label.name == 'publish-test') ||
(github.event_name == 'pull_request_target' &&
(github.event.action != 'labeled' || github.event.label.name == 'publish-test')) ||
(github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_repository.full_name == github.repository &&
github.event.workflow_run.pull_requests[0] != null)
runs-on: ubuntu-24.04
timeout-minutes: 45
timeout-minutes: 150
outputs:
pr_number: ${{ steps.gate.outputs.pr_number }}
run_id: ${{ steps.gate.outputs.run_id }}
@@ -50,6 +85,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: The packages a signed build ships as
id: variants
@@ -61,22 +97,14 @@ jobs:
EVENT_NAME: ${{ github.event_name }}
REQUESTER: ${{ github.actor }}
PR_NUMBER: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number || inputs.pr_number }}
ACTION: ${{ github.event.action }}
BUILT_SHA: ${{ github.event.workflow_run.head_sha }}
AUTO_RUN: ${{ inputs.automatic_build_run_id }}
run: |
set -euo pipefail
[[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || { echo "::error::Invalid PR number '$PR_NUMBER'"; exit 1; }
skip() { echo "::notice::Not publishing PR #$PR_NUMBER: $1"; echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0; }
if [ "$EVENT_NAME" = workflow_run ]; then
echo "automatic=true" >> "$GITHUB_OUTPUT"
pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
[ "$(jq -r .state <<<"$pr")" = open ] || skip "it is not open"
[ "$(jq -r .draft <<<"$pr")" = false ] || skip "it is a draft"
[ "$(jq -r .base.ref <<<"$pr")" = main ] || skip "it does not target main"
[ "$(jq -r .head.repo.full_name <<<"$pr")" = "$GITHUB_REPOSITORY" ] || skip "it comes from a fork"
jq -e 'any(.labels[]; .name == "no-test-build")' <<<"$pr" >/dev/null && skip "it is labelled no-test-build"
[ "$(jq -r .head.sha <<<"$pr")" = "$BUILT_SHA" ] || skip "a newer push is building"
fi
if [ "$EVENT_NAME" = pull_request_target ]; then
if [ "$EVENT_NAME" = pull_request_target ] && [ "$ACTION" = labeled ]; then
permission=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$REQUESTER/permission" --jq .permission)
case "$permission" in
admin|maintain|write) ;;
@@ -85,29 +113,59 @@ jobs:
fi
pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
jq -e '.state == "open" and .draft == false and .base.ref == "main"' <<<"$pr" >/dev/null ||
{ echo "::error::PR #$PR_NUMBER must be open, ready for review, and target main"; exit 1; }
skip "it must be open, ready for review, and target main"
if [ "$EVENT_NAME" = workflow_run ]; then
[ "$(jq -r .head.repo.full_name <<<"$pr")" = "$GITHUB_REPOSITORY" ] || skip "it comes from a fork"
[ "$(jq -r .head.sha <<<"$pr")" = "$BUILT_SHA" ] || skip "a newer push is building"
jq -e 'any(.labels[]; .name == "no-test-build")' <<<"$pr" >/dev/null && skip "it is labelled no-test-build"
echo "automatic=true" >> "$GITHUB_OUTPUT"
elif [ "$EVENT_NAME" = workflow_dispatch ] && [ -n "$AUTO_RUN" ]; then
[[ "$AUTO_RUN" =~ ^[1-9][0-9]*$ ]] || { echo '::error::Invalid automatic build run ID'; exit 1; }
jq -e 'any(.labels[]; .name == "no-test-build")' <<<"$pr" >/dev/null && skip "it is labelled no-test-build"
git fetch --no-tags origin main
opener=$(jq -r .user.login <<<"$pr")
git show origin/main:.github/trusted-test-builds.txt | grep -Fixq -- "$opener" || skip "PR opener @$opener is not trusted for automatic signing"
echo "automatic=true" >> "$GITHUB_OUTPUT"
fi
head=$(jq -r '.head.sha' <<<"$pr")
base=$(jq -r '.base.sha' <<<"$pr")
run_id=
for attempt in $(seq 1 60); do
latest=$(gh run list -R "$GITHUB_REPOSITORY" --workflow build.yml \
--event pull_request --commit "$head" \
--json databaseId,headSha,status,conclusion,createdAt,attempt -L 100 |
jq -c --arg head "$head" '[.[] | select(.headSha == $head)] |
sort_by([.createdAt, .attempt]) | last // empty')
for attempt in $(seq 1 240); do
latest=$(gh api "repos/$GITHUB_REPOSITORY/actions/workflows/build.yml/runs?event=pull_request&head_sha=$head&per_page=100" |
jq -c --arg head "$head" --arg repo "$(jq -r .head.repo.full_name <<<"$pr")" --arg ref "$(jq -r .head.ref <<<"$pr")" '
[.workflow_runs[] | select(.head_sha == $head and .head_repository.full_name == $repo and .head_branch == $ref)] |
sort_by([.created_at, .run_attempt]) | last // empty')
if [[ -z "$latest" && "$attempt" == 1 ]]; then
git fetch --no-tags origin main
git fetch --no-tags origin "refs/pull/$PR_NUMBER/head"
git diff --no-renames --name-only -z "$base...FETCH_HEAD" > "$RUNNER_TEMP/pr-changed-paths"
excluded=true
while IFS= read -r -d '' path; do
case "$path" in
*.md|LICENSE|.gitignore) ;;
*) excluded=false; break ;;
esac
done < "$RUNNER_TEMP/pr-changed-paths"
[ "$excluded" = false ] || skip "its changes do not trigger the APK build"
fi
if [[ -n "$latest" && $(jq -r .status <<<"$latest") == completed ]]; then
conclusion=$(jq -r .conclusion <<<"$latest")
[[ "$conclusion" == success ]] || { echo "::error::The latest PR build is not green ($conclusion)"; exit 1; }
run_id=$(jq -r .databaseId <<<"$latest")
break
if [[ "$conclusion" == success ]]; then
run_id=$(jq -r .id <<<"$latest")
if [ -n "$AUTO_RUN" ] && [ "$run_id" != "$AUTO_RUN" ]; then
skip "a newer build exists"
fi
break
fi
[[ "$conclusion" == action_required ]] || { echo "::error::The latest PR build is not green ($conclusion)"; exit 1; }
fi
status=not-started
[[ -z "$latest" ]] || status=$(jq -r .status <<<"$latest")
echo "Waiting for the PR build of ${head:0:7} to finish ($status)"
sleep 30
done
[[ -n "$run_id" ]] || { echo '::error::The PR build did not finish within 30 minutes'; exit 1; }
[[ -n "$run_id" ]] || { echo '::error::The PR build did not finish within 120 minutes'; exit 1; }
run=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id")
jq -e --arg head "$head" --arg repo "$(jq -r .head.repo.full_name <<<"$pr")" --arg ref "$(jq -r .head.ref <<<"$pr")" '
.event == "pull_request" and .head_sha == $head and .conclusion == "success" and
@@ -137,6 +195,13 @@ jobs:
.head.sha == $head and .base.sha == $base
' <<<"$current" >/dev/null ||
{ echo "::error::PR #$PR_NUMBER moved on while its build was checked; publish again"; exit 1; }
if [ "$EVENT_NAME" = workflow_run ] || { [ "$EVENT_NAME" = workflow_dispatch ] && [ -n "$AUTO_RUN" ]; }; then
jq -e 'any(.labels[]; .name == "no-test-build")' <<<"$current" >/dev/null && skip "it is labelled no-test-build"
if [ "$EVENT_NAME" = workflow_dispatch ]; then
git fetch --no-tags origin main
git show origin/main:.github/trusted-test-builds.txt | grep -Fixq -- "$(jq -r .user.login <<<"$current")" || skip "the PR opener is no longer trusted"
fi
fi
{
echo "pr_number=$PR_NUMBER"
echo "run_id=$run_id"
@@ -271,17 +336,24 @@ jobs:
echo "release=$release" >> "$GITHUB_OUTPUT"
echo "apk=$GITHUB_SERVER_URL/$repo/releases/download/$tag/$apk" >> "$GITHUB_OUTPUT"
- name: Whether the PR's branch is in this repo
id: same-repo
- name: Whether the PR opener is trusted
id: trusted-opener
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
run: |
head_repo=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq .head.repo.full_name)
echo "same=$([ "$head_repo" = "$GITHUB_REPOSITORY" ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
opener=$(jq -r .user.login <<<"$pr")
git fetch --no-tags origin main
if [ "$(jq -r .head.repo.full_name <<<"$pr")" = "$GITHUB_REPOSITORY" ] ||
git show origin/main:.github/trusted-test-builds.txt | grep -Fixq -- "$opener"; then
echo "trusted=true" >> "$GITHUB_OUTPUT"
else
echo "trusted=false" >> "$GITHUB_OUTPUT"
fi
- name: Clear the publish request label
if: ${{ github.event_name == 'pull_request_target' && steps.publish.outcome == 'success' }}
if: ${{ github.event_name == 'pull_request_target' && github.event.action == 'labeled' && steps.publish.outcome == 'success' }}
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
@@ -295,12 +367,12 @@ jobs:
HEAD_SHA: ${{ needs.authorize.outputs.head_sha }}
RELEASE: ${{ steps.publish.outputs.release }}
APK: ${{ steps.publish.outputs.apk }}
SAME_REPO: ${{ steps.same-repo.outputs.same }}
TRUSTED_OPENER: ${{ steps.trusted-opener.outputs.trusted }}
run: |
set -euo pipefail
short=${HEAD_SHA:0:7}
mark='<!-- droiddeck-pr-release -->'
if [ "$SAME_REPO" = true ]; then
if [ "$TRUSTED_OPENER" = true ]; then
NEXT='Each green build of a new push is signed and published here on its own. Label the PR `no-test-build` to stop that.'
else
NEXT='Pushes to this PR do not re-sign automatically. After a new green build, a maintainer can add the `publish-test` label again.'
+57
View File
@@ -0,0 +1,57 @@
name: PR Release Sweep
run-name: sweep test builds of closed PRs
on:
schedule:
- cron: '17 */6 * * *'
workflow_dispatch:
permissions:
contents: read
pull-requests: read
concurrency:
group: pr-release-sweep
cancel-in-progress: false
jobs:
sweep:
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/create-github-app-token@v2
id: ci-token
with:
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
private-key: ${{ secrets.DROIDDECK_CI_APP_KEY }}
owner: Droid-Deck
repositories: DroidDeck-CI
- name: Delete test builds whose PR is no longer open
env:
CI_TOKEN: ${{ steps.ci-token.outputs.token }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
retry() {
for attempt in 1 2 3 4; do
"$@" && return 0
[ "$attempt" = 4 ] || sleep $((attempt * 20))
done
return 1
}
tags=$(GH_TOKEN="$CI_TOKEN" retry gh api --paginate "repos/Droid-Deck/DroidDeck-CI/releases?per_page=100" --jq '.[].tag_name' |
grep -E '^pr-[0-9]+(\.0)?$' || true)
failed=0
for tag in $tags; do
number=${tag#pr-}
number=${number%.0}
state=$(retry gh api "repos/$GITHUB_REPOSITORY/pulls/$number" --jq .state) || { failed=1; continue; }
if [ "$state" != open ]; then
if GH_TOKEN="$CI_TOKEN" retry gh release delete "$tag" -R Droid-Deck/DroidDeck-CI --yes --cleanup-tag; then
echo "Deleted $tag (PR #$number is $state)"
else
failed=1
fi
fi
done
exit "$failed"
+1 -7
View File
@@ -1,11 +1,5 @@
name: Publish add-on packs
# The add-on packs (tools/packs/build.sh) a Build APK run made, published to DroidDeck-Components under
# content-addressed tags (pack-<name>-<first 16 hex of sha256>). The APK from that run pins each pack's
# SHA-256 and downloads it from that tag. A tag is created once and never replaced.
# Only main and release builds publish here; a PR test build attaches the packs main has not published
# to its pr-N release on DroidDeck-CI (pr-release-publish.yml), which is deleted when the PR closes.
on:
workflow_call:
inputs:
@@ -61,7 +55,7 @@ jobs:
jq -r --arg n "$2" '.assets // [] | .[] | select(.name == $n) | .digest // empty'
}
commit=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$BUILD_RUN" --jq .head_sha 2>/dev/null || echo "$GITHUB_SHA")
for name in proot directaudio tracking xbox comet; do
for name in proot directaudio tracking xbox comet butler powervr; do
file="packs/$name.tar.zst"
[ -f "$file" ] || { echo "::error::$name.tar.zst is missing from Build APK run $BUILD_RUN"; exit 1; }
sha=$(sha256sum "$file" | cut -d' ' -f1)
+3
View File
@@ -13,6 +13,8 @@ app/src/main/jniLibs/arm64-v8a/libproot.so
app/src/main/jniLibs/arm64-v8a/.proot-inputs
app/src/main/jniLibs/arm64-v8a/.proot-outputs
app/src/main/jniLibs/arm64-v8a/libproot-loader.so
app/src/main/jniLibs/arm64-v8a/libsyncthingnative.so
app/src/main/jniLibs/arm64-v8a/.syncthing-inputs
app/src/main/jniLibs/arm64-v8a/libdirectaudiorelay.so
app/src/main/rust/stores/target/
app/src/main/assets/droiddeck-esync/
@@ -32,4 +34,5 @@ __pycache__/
.signing.env
app/src/main/assets/directaudio/
app/src/main/assets/hybris/
/tools/vr/out/
+6
View File
@@ -0,0 +1,6 @@
[submodule "external/MaliBridge"]
path = external/MaliBridge
url = https://github.com/ASEbrahim/MaliBridge.git
[submodule "tools/syncthing/syncthing-android"]
path = tools/syncthing/syncthing-android
url = https://github.com/researchxxl/syncthing-android
+4
View File
@@ -0,0 +1,4 @@
# Copy to .signing.env, fill in, chmod 600 .signing.env
RELEASE_KEYSTORE=/path/to/release.p12
RELEASE_STORE_PASSWORD=change-me
RELEASE_KEY_ALIAS=droiddeck
+3 -3
View File
@@ -631,8 +631,8 @@ to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Pluvia is an unofficial Steam client app for Android
Copyright (C) 2024 Oxters Wyzgowski
DroidDeck brings the SteamOS experience to Android
Copyright (C) 2026 DroidDeck contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Pluvia Copyright (C) 2024 Oxters Wyzgowski
DroidDeck Copyright (C) 2026 DroidDeck contributors
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
+128 -25
View File
@@ -5,54 +5,157 @@
</picture>
</p>
DroidDeck brings the SteamOS experience to Android: Valve's Steam client in Big Picture on your Adreno handheld, with Windows games through Valve's ARM64 Proton.
DroidDeck brings the SteamOS experience to Android: Steam Gaming Mode, full Linux desktop, integration with GOG, Epic Games, Xbox, and other storefronts, Steam VR, and more!
<p align="center"><a href="https://discord.gg/JRGAvawjsm"><img src="https://img.shields.io/badge/Discord-Join%20the%20community-5865F2?logo=discord&logoColor=white" alt="Join the DroidDeck Discord"></a></p>
<p align="center"><b>100% open source. No ads. No telemetry. No source withheld.</b></p>
<p align="center"><img src="docs/releases/media/0.2.0/launch-into-steam.gif" width="80%" alt="Tapping DroidDeck on the Android home screen and landing in Steam Big Picture"></p>
<p align="center">
<a href="https://github.com/Droid-Deck/DroidDeck/releases/download/0.6.9/DroidDeck-0.6.9.apk"><img src="https://img.shields.io/badge/Download-DroidDeck%200.6.9%20APK-3DDC84?style=for-the-badge&logo=android&logoColor=white" alt="Download DroidDeck 0.6.9"></a>
<a href="https://discord.gg/JRGAvawjsm"><img src="https://img.shields.io/badge/Discord-Join%20us-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Join the DroidDeck Discord"></a>
</p>
> Note: DroidDeck does not have a stand-alone website. Do not click on any download links from websites claiming to be the DroidDeck team.
<p align="center">
<a href="https://www.star-history.com/droid-deck/droiddeck"><img src="https://api.star-history.com/badge?repo=Droid-Deck/DroidDeck&type=rank&theme=dark" alt="Star History Rank"></a>
</p>
## Requirements and install
<p align="center">
<a href="https://trendshift.io/repositories/267852"><img src="https://trendshift.io/api/badge/trendshift/repositories/267852/daily?language=C" alt="Droid-Deck%2FDroidDeck | Trendshift" width="250" height="55"></a>
<a href="https://trendshift.io/repositories/267852"><img src="https://trendshift.io/api/badge/trendshift/repositories/267852/daily?language=Kotlin" alt="Droid-Deck%2FDroidDeck | Trendshift" width="250" height="55"></a>
</p>
Use Android 9 or newer on a supported Adreno device (730 or newer, or 8xx). Adreno 6xx is experimental: DirectX 11 uses DXVK 2 and may run, and DirectX 12 games can still crash. Mali, Xclipse, PowerVR, and Adreno 710 are unsupported. No root is required. Allow about 3 GB for the runtime and 1.1 GB more for the desktop and emulators. Install the APK from [Releases](https://github.com/Droid-Deck/DroidDeck/releases), install the Linux runtime, then press **Play** and sign in. Steam downloads on first launch. Install **Desktop & apps** to use the desktop and emulators. The **Store** installs Linux apps and games from Flathub (ARM64 builds) with Flatpak; with additional options to install Appimages and set up scripts. **Stores** (Setup › Stores › "Show Stores in the rail") signs in to GOG, Epic Games and Amazon Games and downloads games into the Games storage. Epic keeps its direct API install path. Opening Epic Stores prepares Epic’s official Linux launcher preview. When installed, Epic games launched from DroidDeck use that client, and games installed in the client are imported into DroidDeck’s library and shortcuts. The x86-64 client runs through FEX on ARM and DroidDeck retains its Proton hooks; this preview and ARM setup are outside Epic’s listed Linux support. The cog on a store’s chip row sets its opening tab and download speed tier.
<p align="center"><img src="docs/media/steam-to-desktop.gif" width="100%" alt="On a Retroid Pocket Duo: Play Steam from the DroidDeck launcher, open Pac-Man in Steam Big Picture and play it, exit the game, then Switch to Desktop from the Power menu"></p>
On Android 14 and newer, before Steam launches, you must turn on **Disable child process restrictions** in Developer options. If this option is not available in developer settings (Android 12 and 13 devices), first launch of Steam will present a "Fix it for me" button, which will help automate the setup process.
> DroidDeck has no website. Only download it from this repository's [Releases](https://github.com/Droid-Deck/DroidDeck/releases).
## Smart glasses
<p align="center">
<img src="docs/releases/media/0.6.7/library.gif" width="49%" alt="The Library switching source chips, changing the card size and opening the Filter menu">
<img src="docs/releases/media/0.3.0/in-game.gif" width="49%" alt="A Windows game running through Proton with the on-screen controls">
<img src="docs/releases/media/0.4.0/multi-controller.gif" width="49%" alt="Two controllers playing split screen on one handheld">
<img src="docs/releases/media/0.6.7/ubuntu-desktop-discover.jpg" width="49%" alt="The Kubuntu desktop with Discover open">
</p>
Use **Setup → Smart glasses** for app-wide virtual-screen settings across DroidDeck menus, launchers, desktop sessions, and games. Supported VITURE, RayNeo, XREAL, and Rokid connections provide 3DoF; Luma Ultra adds optional experimental 6DoF and stereo-assisted hand/Joy-Con tracking. Model support and SDK requirements differ: see the [smart-glasses support matrix](docs/smart-glasses-support.md) for supported devices, controls, setup, and limitations.
## Install
## Community
1. Android 9 or newer on Adreno 730 or newer, or any 8xx. Adreno 6xx is experimental. Mali, Xclipse, PowerVR and Adreno 710 are unsupported. No root needed.
2. About 3 GB free for the runtime, plus 1.1 GB for the desktop and emulators.
3. Install the [APK](https://github.com/Droid-Deck/DroidDeck/releases/download/0.6.9/DroidDeck-0.6.9.apk), install the Linux runtime, press **Play** and sign in to Steam.
4. On Android 14 and newer, turn on **Disable child process restrictions** in Developer options. On Android 12 and 13, Steam's first launch shows a **Fix it for me** button that walks you through it.
Join the [DroidDeck Discord](https://discord.gg/JRGAvawjsm) for help, Preview builds, and device reports. Bug reports go in its **#bug-reports** forum; attach the zip from **Share logs** (Setup, or the session drawer) so the logs come with it.
Smart glasses setup is in the [support matrix](docs/smart-glasses-support.md). Release notes are in [docs/releases](docs/releases).
Help, Preview builds and device reports are on [Discord](https://discord.gg/JRGAvawjsm). For bugs, post in **#bug-reports** with the zip from **Share logs**.
## Build
Run `tools/build_local.sh` with Docker, Java 17, the Android SDK/NDK, Rust through rustup, GitHub CLI, Python 3, and `zstd` installed. It prepares the same native libraries, pinned components and audio sinks as CI, then checks the APK's assets, JNI exports and dependencies. The APK is at `app/build/outputs/apk/release/app-release.apk`. Set `DROIDDECK_PA13_SOURCE_DIR` to an existing PulseAudio 13.0 source directory to skip downloading it. Set `DROIDDECK_SIGNING_ENV` to a signing environment file to install over a release build; relative keystore paths resolve beside that file. To install on an attached device, run `tools/deploy_local.sh`.
Needs Docker, Java 17, the Android SDK and NDK, Rust via rustup, GitHub CLI, Python 3 and `zstd`.
After preparation, ordinary `./gradlew :app:assembleRelease` builds can reuse the verified inputs. If native sources, component pins or staged files change, packaging stops with instructions to rerun the helper. Generated audio stays under `app/build/`; the tracked base bundle is never modified. `./gradlew -PskipRust=true :app:testDebugUnitTest` needs no native preparation. For an APK without store engines, use `DROIDDECK_SKIP_RUST=1 tools/build_local.sh`; it omits cached engine libraries too.
```bash
tools/build_local.sh
```
For the prepared Windows checkout, run `./tools/run_windows.ps1` (optionally with `-Serial <adb-serial>`), or select **DroidDeck XR** in Android Studio and run the **app** module on the connected phone. Machine-specific options live in the ignored `local.properties`: `droiddeck.python` selects a Python executable, `droiddeck.skipRust=true` omits the optional store download engines, and `droiddeck.debugApplicationIdSuffix=.xr` installs **DroidDeck XR Dev** alongside the signed release. The development app has its own runtime and Steam login. The local Java selection is in `.gradle/config.properties`.
The APK lands in `app/build/outputs/apk/release/`. Install it with `tools/deploy_local.sh`. After the first run, `./gradlew :app:assembleRelease` reuses the prepared inputs. Unit tests run with `./gradlew -PskipRust=true :app:testDebugUnitTest`, and the helper and preload suite with `bash tools/test_local.sh`.
`python tools/prepare_windows_runtime.py` restores this checkout's audited runtime inputs from the pinned CI APK, three locally rebuilt preload libraries, and the checksum-pinned Gamescope archive in `build/windows-setup/`. Start Docker Desktop's Linux engine first: preparation now rebuilds DroidDeck's ARM64 VR driver and Vulkan layer from current source and runs their native regression tests. The compiler image is built automatically on first use. Vulkan headers come from the NDK beside `--llvm-strip`, or from an explicit `--vk-include <NDK-sysroot-include>` directory.
To reuse that build, pass `--rebuilt-vr build/windows-setup/vr`. Preparation verifies its `vr-build.json` against the current Git revision, source files, Vulkan headers, output checksums, and ARM64 architecture; missing or stale builds fail instead of falling back to the donor's VR libraries. The script checks the actual donor CI merge tree, still rejects unrelated unaudited runtime changes, and records component provenance in `app/build/windows-runtime-provenance.json`. Preserve these ignored preparation files when rebuilding locally; `clean` removes the prepared assets and their verification manifest.
Run the helper and preload regression suite with `bash tools/test_local.sh`. On macOS it runs in Linux through Docker, matching the helpers' runtime and CI instead of compiling Linux preloads against macOS headers.
On Windows, see `tools/run_windows.ps1` and `tools/prepare_windows_runtime.py`.
## Limits
Compatibility and performance vary by device; hardware validation is limited. Desktop compositing uses software rendering. Firefox sandboxing is reduced under proot. Logs are kept app-private (`files/logs/`, the last 30 sessions); **Share logs** packs the newest session, with the stores and tool logs, into one scrubbed zip.
Compatibility and performance vary by device. Desktop compositing is software rendered, and Firefox sandboxing is reduced under proot.
## Credits and licence
GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). Epic Games and GOG support build on [Phobos’s](https://github.com/phobos665) adaptation of [Legendary](https://github.com/legendary-gl/legendary) and [gogdl](https://github.com/Heroic-Games-Launcher/heroic-gogdl), respectively. The registry entries a GOG game's install script (`goggame-<id>.script`) sets are read the way [Heroic Games Launcher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher)'s GOG setup (GPL-3.0) reads them. LSFG frame generation is from the work of Camille LaVey and the [Eden](https://eden-emu.dev) emulator project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported to WinNative and DroidDeck by [@maxjivi05](https://github.com/maxjivi05); it needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. x86 AppImages, and any whose own runtime cannot unpack them, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence, which also starts the arm64 emulator AppImages whose own runtime cannot run its extractor on Android. The Steam client's audio goes through [@The412Banner](https://github.com/The412Banner)'s [DirectAudio](https://github.com/The412Banner/directaudio) PulseAudio sink (LGPL-2.1-or-later), built from `tools/directaudio/sink` with its request size kept to its ring so clients are not asked to queue 100 ms ahead. mmap ring-buffer controller input was developed by ribbit384 ([WinNative #345](https://github.com/WinNative-Emu/WinNative/pull/345)). Why the x86-64 Steam client refused the arm64 client's sign-in was checked by reading Steam's saved token with the `ConnectCache` format from [deadlock-api-ingest](https://github.com/deadlock-api/deadlock-api-ingest). x86 programs that link OpenSSL 3 under FEX use the copy in the [Steam Linux Runtime 4.0](https://gitlab.steamos.cloud/steamrt/steamrt) platform. Hooks written with `WriteProcessMemory` into code [FEX](https://github.com/FEX-Emu/FEX) has translated go through `/proc/<pid>/mem` again when the kernel refuses `process_vm_writev`, as [Wine](https://gitlab.winehq.org/wine/wine)'s `server/ptrace.c` wrote them before Wine 11. x86 Linux games under FEX get the same `/etc/asound.conf` that [pressure-vessel](https://gitlab.steamos.cloud/steamrt/steam-runtime-tools) writes into the Steam Linux Runtime, so ALSA plays through PulseAudio. Games render with [Mesa](https://gitlab.freedesktop.org/mesa/mesa) Turnip's own per-render-pass choice between tile memory and system memory, while the Steam client keeps `TU_DEBUG=sysmem`; [FEX](https://github.com/FEX-Emu/FEX)'s code disk cache was checked in its source and is already on through Proton. The proot fast path picks the guest name of a host file bound under several names in the order [PRoot](https://github.com/proot-me/proot)'s `path/binding.c` (GPL-2.0-or-later) sorts its bindings, so links and `/proc/self/fd` read back as they do through proot. The session's [PipeWire](https://gitlab.freedesktop.org/pipewire/pipewire) (MIT) opens its sockets with the per-socket access settings documented in its `module-access.c`, and [WirePlumber](https://gitlab.freedesktop.org/pipewire/wireplumber) (MIT) keeps those clients at full permissions through its `access.rules`. Wine write watches use the userfaultfd support in [Proton](https://github.com/ValveSoftware/wine)'s `dlls/ntdll/unix/virtual.c` (LGPL-2.1-or-later): libblsession gives it the arm64 syscall number and answers its `PAGEMAP_SCAN` from `/proc/self/pagemap` where Android refuses the ioctl. Wine on arm64 sets a thread's full context by raising `SIGUSR2` from `syscall_dispatcher_return_slowpath` in [Wine](https://gitlab.winehq.org/wine/wine)'s `dlls/ntdll/unix/signal_arm64.c` (LGPL-2.1-or-later); libblsession runs that handler in place on a frame laid out as the [Linux](https://github.com/torvalds/linux) kernel's `arch/arm64/kernel/signal.c` builds it and returns through `rt_sigreturn`, so proot does not stop the thread for it. Wine's process list reads each process's memory counters from `/proc/<pid>/status` in `fill_vm_counters` ([Wine](https://gitlab.winehq.org/wine/wine) `dlls/ntdll/unix/process.c`, LGPL-2.1-or-later), so libblsession gives those reads the kernel's file instead of a copy with `TracerPid` cleared. droiddeck-ntsync is checked against the object, wait and error semantics documented for the [Linux](https://github.com/torvalds/linux) ntsync driver in `Documentation/userspace-api/ntsync.rst` (GPL-2.0). See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve.
DroidDeck is [GPL-3.0](LICENSE). Steam and Proton belong to Valve Corporation. VITURE, RayNeo, XREAL, Rokid, REDMAGIC, Nintendo Switch and Joy-Con are trademarks of their owners. This project is not affiliated with any of them.
XR glasses support builds on WinNative's VITURE glasses work. VITURE protocol details were checked against [XRLinuxDriver](https://github.com/wheaney/XRLinuxDriver) by [@wheaney](https://github.com/wheaney), including the VITURE protocol header it ships, [ar-drivers-rs](https://github.com/badicsalex/ar-drivers-rs) by [@badicsalex](https://github.com/badicsalex) and the [go-macos/iokit VITURE package](https://pkg.go.dev/github.com/go-macos/iokit/viture); the optional VITURE SDK belongs to VITURE and is not included. RayNeo support adapts [@informalTechCode](https://github.com/informalTechCode)'s [RayNeo-Air-Series-3DoF-Scaffold](https://github.com/informalTechCode/RayNeo-Air-Series-3DoF-Scaffold) (Apache-2.0) and references [@verncat](https://github.com/verncat)'s [RayNeo-Air-3S-Pro-OpenVR](https://github.com/verncat/RayNeo-Air-3S-Pro-OpenVR) (MIT) and [Monado](https://gitlab.freedesktop.org/monado/monado) (BSL-1.0); see [docs/third-party/rayneo](docs/third-party/rayneo/README.md). XREAL 1S support references [XRLinuxDriver](https://github.com/wheaney/XRLinuxDriver) and [xrealOneDeviceKit](https://github.com/wheaney/xrealOneDeviceKit) by @wheaney, [xreal_one_driver](https://github.com/rohitsangwan01/xreal_one_driver) by [@rohitsangwan01](https://github.com/rohitsangwan01) (MIT) and [xr-tools](https://github.com/0xcaff/xr-tools) by [@0xcaff](https://github.com/0xcaff). Rokid support follows the Rokid protocol notes in [ar-drivers-rs](https://github.com/badicsalex/ar-drivers-rs) and @badicsalex's [Void Computing write-ups](https://voidcomputing.hu/blog/worse-better-prettier/), and [Monado](https://gitlab.freedesktop.org/monado/monado)'s Rokid driver. The REDMAGIC glasses-display findings draw on [@mekhontsev](https://github.com/mekhontsev)'s [MagicDesk investigation](https://github.com/mekhontsev/magicdesk/blob/main/docs/xr-resolution-fix.md) and [@Coding-BR](https://github.com/Coding-BR)'s [decompiled Nubia kernel](https://github.com/Coding-BR/android_kernel_nubia_sm8850_qwjujube). VITURE, RayNeo, XREAL, Rokid and REDMAGIC are trademarks of their owners; this project is not affiliated with them.
<details>
<summary><b>Runtime, Wine and Proton</b></summary>
Joy-Con VR controllers port the arm model of Google's [Google VR SDK for Unity](https://github.com/googlevr/gvr-unity-sdk) (Apache-2.0), adapt the hand skeleton of Valve's [OpenVR](https://github.com/ValveSoftware/openvr) `handskeletonsimulation` sample (BSD-3-Clause), which follows Moshi Turner's hand simulation in [Monado](https://gitlab.freedesktop.org/monado/monado), and use the Index controller offsets from Monado's `vive_poses.c` (BSL-1.0). Joy-Con behaviour was researched in Linux's [hid-nintendo](https://github.com/torvalds/linux/blob/master/drivers/hid/hid-nintendo.c) driver by Daniel J. Ogorchock, [@dekuNukem](https://github.com/dekuNukem)'s [Nintendo_Switch_Reverse_Engineering](https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering), [SDL](https://github.com/libsdl-org/SDL) and the Android Open Source Project, and checked against [ALVR](https://github.com/alvr-org/ALVR), [OpenComposite](https://gitlab.com/znixian/OpenOVR), [PSMoveSteamVRBridge](https://github.com/HipsterSloth/PSMoveSteamVRBridge), [@F8LEFT](https://github.com/F8LEFT)'s [JoyConVr](https://github.com/F8LEFT/JoyConVr), [@krnelpan1c](https://github.com/krnelpan1c)'s [Daydream-Controller-SteamVR](https://github.com/krnelpan1c/Daydream-Controller-SteamVR), [@yoyobuae](https://github.com/yoyobuae)'s [joyconvr](https://github.com/yoyobuae/joyconvr), [Moonlight](https://github.com/moonlight-stream/moonlight-android) and [Dolphin](https://github.com/dolphin-emu/dolphin); see [docs/third-party/joycon](docs/third-party/joycon/README.md). Nintendo Switch and Joy-Con are trademarks of Nintendo; this project is not affiliated with Nintendo.
- Runtime, shim, input and controller work build on WinNative and Bannerlator by maxjivi05.
- mmap ring buffer controller input by ribbit384 ([WinNative #345](https://github.com/WinNative-Emu/WinNative/pull/345)).
- [FEX](https://github.com/FEX-Emu/FEX) (MIT) runs x86 code. DroidDeck's build puts the platform name under `AT_PLATFORM` so [sharun](https://github.com/VHSgunzo/sharun) AppImages by VHSgunzo (MIT) start through its [userland-execve](https://crates.io/crates/userland-execve) loader (MIT). FEX's code disk cache was checked in its source.
- x86 AppImages, and arm64 ones whose runtime can't unpack on Android, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence.
- x86 programs linking OpenSSL 3 use the copy in the [Steam Linux Runtime 4.0](https://gitlab.steamos.cloud/steamrt/steamrt). x86 Linux games get the `/etc/asound.conf` that [pressure-vessel](https://gitlab.steamos.cloud/steamrt/steam-runtime-tools) writes.
- `WriteProcessMemory` hooks fall back to `/proc/<pid>/mem` the way [Wine](https://gitlab.winehq.org/wine/wine)'s `server/ptrace.c` did before Wine 11.
- Wine write watches use userfaultfd support in [Proton](https://github.com/ValveSoftware/wine)'s `dlls/ntdll/unix/virtual.c` (LGPL-2.1-or-later). libblsession supplies the arm64 syscall number and answers `PAGEMAP_SCAN` from `/proc/self/pagemap`.
- libblsession runs Wine's arm64 `SIGUSR2` context handler (`dlls/ntdll/unix/signal_arm64.c`, LGPL-2.1-or-later) in place on a frame laid out like [Linux](https://github.com/torvalds/linux)'s `arch/arm64/kernel/signal.c`, and serves `fill_vm_counters` in `dlls/ntdll/unix/process.c` the kernel's own `/proc/<pid>/status`.
- droiddeck-ntsync follows the [Linux](https://github.com/torvalds/linux) ntsync semantics in `Documentation/userspace-api/ntsync.rst` (GPL-2.0).
- The proot fast path orders bindings like [PRoot](https://github.com/proot-me/proot)'s `path/binding.c` (GPL-2.0-or-later).
- DiRT 3 and DiRT Showdown keep their telemetry socket the way RoccoC's DiRTTelemetryErrorFix (MIT) does, based on the fix by Carsten Gottschling.
- Games use [Mesa](https://gitlab.freedesktop.org/mesa/mesa) Turnip's per render pass memory choice. The Steam client keeps `TU_DEBUG=sysmem`.
- Black Ops III and Dark Souls Remastered start without [umu-protonfixes](https://github.com/Open-Wine-Components/umu-protonfixes) (BSD-2-Clause), whose `311210.py` and `570940.py` fail under ARM64 Proton.
- [ReSkate](https://github.com/Dingo-Shenanigans/ReSkate) (GPL-3.0) by Dingo Shenanigans uses the launch option from its README and D3D12 feature level 12_1.
- The arm64 client's sign in was checked by reading Steam's saved token with the `ConnectCache` format from [deadlock-api-ingest](https://github.com/deadlock-api/deadlock-api-ingest).
Camera hand tracking runs Google's [MediaPipe](https://github.com/google-ai-edge/mediapipe) Tasks Vision library with its Pose Landmarker (Lite) model, BlazePose GHUM 3D, and its Hand Landmarker model (all Apache-2.0), on frames from AndroidX [CameraX](https://developer.android.com/jetpack/androidx/releases/camera) (Apache-2.0); MediaPipe's usage reports are switched off. The design was checked against [@F8LEFT](https://github.com/F8LEFT)'s [JoyConVr](https://github.com/F8LEFT/JoyConVr) camera app (MIT), and the short Joy-Con movements follow the accelerometer mode of the [Google VR SDK for Unity](https://github.com/googlevr/gvr-unity-sdk)'s arm model, Eric Foxlin's zero-velocity updates and Frosio, Pedersini and Borghese's accelerometer autocalibration, and the camera and Joy-Con are joined by a Kalman filter (R. E. Kalman, 1960). Bare camera hands take their gestures after [ALVR](https://github.com/alvr-org/ALVR)'s `hand_gestures.rs` (MIT) and Meta Quest's menu pinch, with distances checked on photos from the [HaGRID](https://github.com/hukenovs/hagrid) gesture set (CC BY-SA 4.0, not included), fit the palm with Horn's quaternion method (1987) and smooth it with the 1€ filter (Casiez, Roussel and Vogel, 2012); the white glove model follows Valve's OpenVR render model reference and Index controller poses, is meshed with surface nets (Gibson, 1998; Mikola Lysenko) and blends and shades it with Inigo Quilez's smooth minimum and distance-field ambient occlusion, porting his polygon distance (MIT); see [docs/third-party/handtracking](docs/third-party/handtracking/README.md).
</details>
The Store installs and runs apps with [Flatpak](https://github.com/flatpak/flatpak) (LGPL-2.1). Its sandbox goes through DroidDeck's own bubblewrap shim, which follows how Flatpak hands bwrap its environment. Chromium and Electron apps from Flathub start their browser processes through [zypak](https://github.com/refi64/zypak) by [@refi64](https://github.com/refi64) (BSD-3-Clause); its source was read to see that it always spawns them through Flatpak's spawn portal, which is why DroidDeck starts them directly without Chromium's sandbox instead.
<details>
<summary><b>Stores</b></summary>
- Epic Games and GOG build on [Phobos](https://github.com/phobos665)'s adaptation of [Legendary](https://github.com/legendary-gl/legendary) and [gogdl](https://github.com/Heroic-Games-Launcher/heroic-gogdl). GOG install script registry entries are read the way [Heroic Games Launcher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher) (GPL-3.0) reads them.
- Epic's official Linux launcher preview runs through FEX. That setup is outside Epic's listed Linux support.
- Battle.net is by [@kayakyakr](https://github.com/kayakyakr). It runs Blizzard's own client in Proton (downloaded from Blizzard, not included) and shows key art from the [Battle.net Shop](https://shop.battle.net). Without Steam, its games see the pad as an Xbox 360 controller via `PROTON_SPOOF_STEAMINPUT_VIDPID` in [Proton](https://github.com/ValveSoftware/wine)'s `dlls/winebus.sys`.
- Battle.net installs drive Blizzard's Agent the way [@barncastle](https://github.com/barncastle)'s [Battle.Net-Installer](https://github.com/barncastle/Battle.Net-Installer) does, with requests from [wowdev.wiki](https://wowdev.wiki/Agent). Read without taking code: [@mubaraknumann](https://github.com/mubaraknumann)'s [unifideck](https://github.com/mubaraknumann/unifideck), [@djmittens](https://github.com/djmittens)'s [neo-mittens](https://github.com/djmittens/neo-mittens) `bnetswitch`, [@BCD1210](https://github.com/BCD1210)'s [soju](https://github.com/BCD1210/soju) and [Lutris](https://github.com/lutris/lutris)'s `product.db` reader.
- World of Warcraft addon imports replace an addon's folder from a temporary copy, as [WowUp](https://github.com/WowUp/WowUp) does, and read flavour folders the way [WoWDeck](https://github.com/sigboe/wowdeck) does. No code from either is used.
- The World of Warcraft addon Download window searches [WoWInterface](https://www.wowinterface.com) through its MMOUI API. [CurseForge](https://www.curseforge.com/wow) opens in the user's browser, with no API and no key.
- Xbox games load [@domsvoboda](https://github.com/domsvoboda)'s [xodus-nexus](https://github.com/domsvoboda/xodus-nexus) modules (LGPL-2.1-or-later), including `wintypes.dll`. Minecraft's GameInput setup was checked against [@Wyze3306](https://github.com/Wyze3306)'s [BedrockOnLinux](https://github.com/Wyze3306/BedrockOnLinux) (MIT).
- The Xbox Cloud Gaming player uses the WebRTC client of [@unknownskl](https://github.com/unknownskl)'s [Greenlight](https://github.com/unknownskl/greenlight) (MIT), and its offer tweaks and sharpening follow [@redphx](https://github.com/redphx)'s [Better xCloud](https://github.com/redphx/better-xcloud) (MIT). See `tools/xcloud-player/NOTICE.md`.
- The itch.io butler pack ships butler's `7z.so`, itch.io's build of [7-Zip](https://www.7-zip.org) by Igor Pavlov (LGPL-2.1, unRAR restriction on RAR code), unmodified. Other archives use Apache Commons Compress and the runtime's bsdtar (libarchive).
- The Store uses [Flatpak](https://github.com/flatpak/flatpak) (LGPL-2.1) through DroidDeck's own bubblewrap shim. Chromium and Electron apps start directly, since [zypak](https://github.com/refi64/zypak) by [@refi64](https://github.com/refi64) (BSD-3-Clause) always uses Flatpak's spawn portal.
- Folder sync runs [Syncthing](https://github.com/syncthing/syncthing) (MPL-2.0), built from [@researchxxl](https://github.com/researchxxl)'s [syncthing-android](https://github.com/researchxxl/syncthing-android) fork (MPL-2.0).
</details>
<details>
<summary><b>Audio, video and HUD</b></summary>
- Steam audio goes through [@The412Banner](https://github.com/The412Banner)'s [DirectAudio](https://github.com/The412Banner/directaudio) PulseAudio sink (LGPL-2.1-or-later), built from `tools/directaudio/sink`.
- The DroidDeck HUD is [@The412Banner](https://github.com/The412Banner)'s [FusionHUD](https://github.com/The412Banner/FusionHUD) (GPL-3.0 with an attribution term). Its readings match Valve's [MangoHud](https://github.com/flightlessmango/MangoHud) build (MIT), with frame times from gamescope patch 0125.
- The HUD's CPU, GPU load, thermal and battery readers come from [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx)'s work.
- LSFG frame generation comes from Camille LaVey and the [Eden](https://eden-emu.dev) project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported by [@maxjivi05](https://github.com/maxjivi05). It needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders.
- Remote Play decodes through Android's MediaCodec or the [FFmpeg](https://ffmpeg.org) (LGPL-2.1-or-later) in Steam's runtime. Qualcomm low latency settings follow [Moonlight](https://github.com/moonlight-stream/moonlight-android) (GPL-3.0).
- [PipeWire](https://gitlab.freedesktop.org/pipewire/pipewire) (MIT) sockets use the settings in its `module-access.c`, and [WirePlumber](https://gitlab.freedesktop.org/pipewire/wireplumber) (MIT) keeps those clients at full permissions.
</details>
<details>
<summary><b>Desktop</b></summary>
- The KDE pacman repository snapshots [Arch Linux ARM](https://archlinuxarm.org) and installs with [pacman](https://gitlab.archlinux.org/pacman/pacman) (GPL-2.0-or-later).
- [KWin](https://invent.kde.org/plasma/kwin) (GPL-2.0-or-later) is patched to pass on the scroll data its `wayland_backend.cpp` leaves as a TODO.
- Read without taking code: [Qt](https://code.qt.io/cgit/qt/qtbase.git)'s `qwaylandinputdevice.cpp` and `qguiapplication.cpp`, [Konsole](https://invent.kde.org/utilities/konsole)'s `TerminalDisplay.cpp`, and [Mesa](https://gitlab.freedesktop.org/mesa/mesa)'s `u_threaded_context.c` and `zink_resource.c`.
- The Steam Keyboard window rule follows Valve's `steamdeck-kde-presets` with [Bazzite](https://github.com/ublue-os/bazzite)'s `0001-steam-virtual-keyboard.patch`.
- Steam's Browse buttons open a GTK 3 file chooser via PyGObject (LGPL-2.1-or-later), shown over Steam by [gamescope](https://github.com/ValveSoftware/gamescope) (BSD-2-Clause) through `GetAppIdFromReaper`.
- The AYN Odin 2 phantom second display was confirmed by [@SapphireRhodonite](https://github.com/SapphireRhodonite)'s [WatermelonDS 0.0.5](https://github.com/SapphireRhodonite/WatermelonDS/releases/tag/0.0.5) notes and [iiSU #363](https://github.com/iisu-network/iiSU/issues/363), without taking their code.
</details>
<details>
<summary><b>Smart glasses</b></summary>
- Builds on WinNative's VITURE work. Protocol checked against [@wheaney](https://github.com/wheaney)'s [XRLinuxDriver](https://github.com/wheaney/XRLinuxDriver), [@badicsalex](https://github.com/badicsalex)'s [ar-drivers-rs](https://github.com/badicsalex/ar-drivers-rs) and [go-macos/iokit](https://pkg.go.dev/github.com/go-macos/iokit/viture). The optional VITURE SDK is not included.
- The VITURE protocol V2 frames adapt [@elasticjava](https://github.com/elasticjava)'s [viture-v2](https://github.com/elasticjava/viture-v2) (MIT) by Holger Bartnick. See `docs/third-party/viture-v2`.
- RayNeo adapts [@informalTechCode](https://github.com/informalTechCode)'s [RayNeo-Air-Series-3DoF-Scaffold](https://github.com/informalTechCode/RayNeo-Air-Series-3DoF-Scaffold) (Apache-2.0) and references [@verncat](https://github.com/verncat)'s [RayNeo-Air-3S-Pro-OpenVR](https://github.com/verncat/RayNeo-Air-3S-Pro-OpenVR) (MIT) and [Monado](https://gitlab.freedesktop.org/monado/monado) (BSL-1.0). See [docs/third-party/rayneo](docs/third-party/rayneo/README.md).
- XREAL 1S references XRLinuxDriver and [xrealOneDeviceKit](https://github.com/wheaney/xrealOneDeviceKit) by @wheaney, [@rohitsangwan01](https://github.com/rohitsangwan01)'s [xreal_one_driver](https://github.com/rohitsangwan01/xreal_one_driver) (MIT) and [@0xcaff](https://github.com/0xcaff)'s [xr-tools](https://github.com/0xcaff/xr-tools).
- Rokid follows ar-drivers-rs, @badicsalex's [Void Computing write-ups](https://voidcomputing.hu/blog/worse-better-prettier/) and Monado's Rokid driver.
- REDMAGIC display findings draw on [@mekhontsev](https://github.com/mekhontsev)'s [MagicDesk investigation](https://github.com/mekhontsev/magicdesk/blob/main/docs/xr-resolution-fix.md) and [@Coding-BR](https://github.com/Coding-BR)'s [decompiled Nubia kernel](https://github.com/Coding-BR/android_kernel_nubia_sm8850_qwjujube).
</details>
<details>
<summary><b>VR, Joy-Con and hand tracking</b></summary>
- Joy-Con VR ports the arm model from Google's [GVR SDK for Unity](https://github.com/googlevr/gvr-unity-sdk) (Apache-2.0) and adapts the hand skeleton from Valve's [OpenVR](https://github.com/ValveSoftware/openvr) `handskeletonsimulation` sample (BSD-3-Clause), which follows Moshi Turner's work in Monado. Index offsets come from Monado's `vive_poses.c` (BSL-1.0).
- Joy-Con research: Linux's [hid-nintendo](https://github.com/torvalds/linux/blob/master/drivers/hid/hid-nintendo.c) by Daniel J. Ogorchock, [@dekuNukem](https://github.com/dekuNukem)'s [Nintendo_Switch_Reverse_Engineering](https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering), [SDL](https://github.com/libsdl-org/SDL) and AOSP. Checked against [ALVR](https://github.com/alvr-org/ALVR), [OpenComposite](https://gitlab.com/znixian/OpenOVR), [PSMoveSteamVRBridge](https://github.com/HipsterSloth/PSMoveSteamVRBridge), [@F8LEFT](https://github.com/F8LEFT)'s [JoyConVr](https://github.com/F8LEFT/JoyConVr), [@krnelpan1c](https://github.com/krnelpan1c)'s [Daydream-Controller-SteamVR](https://github.com/krnelpan1c/Daydream-Controller-SteamVR), [@yoyobuae](https://github.com/yoyobuae)'s [joyconvr](https://github.com/yoyobuae/joyconvr), [Moonlight](https://github.com/moonlight-stream/moonlight-android) and [Dolphin](https://github.com/dolphin-emu/dolphin). See [docs/third-party/joycon](docs/third-party/joycon/README.md).
- Camera hand tracking runs Google's [MediaPipe](https://github.com/google-ai-edge/mediapipe) Pose Landmarker (Lite), BlazePose GHUM 3D and Hand Landmarker (Apache-2.0) on [CameraX](https://developer.android.com/jetpack/androidx/releases/camera) frames (Apache-2.0), with usage reports off. Checked against JoyConVr's camera app (MIT).
- Tracking math: the GVR arm model's accelerometer mode, Eric Foxlin's zero velocity updates, Frosio, Pedersini and Borghese's accelerometer autocalibration, a Kalman filter (Kalman, 1960), Horn's quaternion method (1987) and the 1€ filter (Casiez, Roussel and Vogel, 2012).
- Gestures follow [ALVR](https://github.com/alvr-org/ALVR)'s `hand_gestures.rs` (MIT) and Meta Quest's menu pinch, tuned on [HaGRID](https://github.com/hukenovs/hagrid) photos (CC BY-SA 4.0, not included).
- The glove model follows OpenVR's render model reference, is meshed with surface nets (Gibson, 1998; Mikola Lysenko), and uses Inigo Quilez's smooth minimum, distance field ambient occlusion and polygon distance (MIT). See [docs/third-party/handtracking](docs/third-party/handtracking/README.md).
</details>
## Star history
<a href="https://www.star-history.com/#Droid-Deck/DroidDeck&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=Droid-Deck/DroidDeck&type=Date&theme=dark">
<img alt="Star history chart" src="https://api.star-history.com/svg?repos=Droid-Deck/DroidDeck&type=Date" width="100%">
</picture>
</a>
+61 -9
View File
@@ -4,7 +4,6 @@ plugins {
apply plugin: 'kotlin-android'
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
// Machine-specific tooling and optional CI-prepared inputs stay out of version control.
def localBuildOptions = new Properties()
def localBuildOptionsFile = rootProject.layout.projectDirectory.file('local.properties')
if (localBuildOptionsFile.asFile.exists()) {
@@ -78,6 +77,10 @@ def escapedBuildLabel = buildLabel.replace('\\', '\\\\').replace('"', '\\"')
def sourceCommit = providers.environmentVariable('DROIDDECK_SOURCE_COMMIT').getOrNull()
?: providers.environmentVariable('GITHUB_SHA').getOrNull() ?: gitValue(['rev-parse', 'HEAD']) ?: ''
def prNumber = (providers.environmentVariable('DROIDDECK_PR_NUMBER').getOrNull() ?: '0').with { it.isInteger() ? it : '0' }
def prTitle = providers.environmentVariable('DROIDDECK_PR_TITLE').getOrNull() ?: ''
def buildBranch = providers.environmentVariable('GITHUB_HEAD_REF').getOrNull()?.trim()
?: gitValue(['branch', '--show-current']) ?: ''
def escapeJava = { String value -> value.replace('\\', '\\\\').replace('"', '\\"').replace('\n', ' ').replace('\r', ' ') }
def ciBuild = providers.environmentVariable('GITHUB_ACTIONS').getOrNull() == 'true'
def releaseSigner = rootProject.file('keystore/release-signer.sha256').text.trim()
def commitTime = (gitValue(['show', '-s', '--format=%ct', '--end-of-options', sourceCommit]) ?: '0').with { it.isLong() ? it : '0' }
@@ -87,10 +90,16 @@ def desktopKde = rootProject.file('tools/desktop-kde/release.env').readLines()
def trackingPack = rootProject.file('tools/packs/tracking/release.env').readLines()
.findAll { it.contains('=') && !it.startsWith('#') }
.collectEntries { def kv = it.split('=', 2); [(kv[0].trim()): kv[1].trim()] }
def addonPacks = ['proot', 'directaudio', 'tracking', 'xbox', 'comet'].collectEntries { name ->
def prootPackPin = rootProject.file('tools/packs/proot/release.env').readLines()
.findAll { it.contains('=') && !it.startsWith('#') }
.collectEntries { def kv = it.split('=', 2); [(kv[0].trim()): kv[1].trim()] }
def addonPacks = ['proot', 'directaudio', 'tracking', 'xbox', 'comet', 'butler', 'powervr'].collectEntries { name ->
def pack = file("build/packs/${name}.tar.zst")
if (!pack.isFile()) {
if (ciBuild) throw new GradleException("Add-on pack ${pack} is missing: run tools/packs/build.sh app/build/packs")
if (name == 'proot' && prootPackPin.PROOT_PACK_SHA256) {
return [(name): [sha256: prootPackPin.PROOT_PACK_SHA256, size: (prootPackPin.PROOT_PACK_SIZE ?: '0') as long]]
}
return [(name): [sha256: '', size: 0L]]
}
def digest = java.security.MessageDigest.getInstance('SHA-256')
@@ -122,11 +131,13 @@ android {
applicationId "com.droiddeck.launcher"
minSdkVersion 26
targetSdkVersion 28
versionCode 15
versionName "0.6.0"
versionCode 17
versionName "0.6.9"
buildConfigField "String", "BUILD_LABEL", "\"${escapedBuildLabel}\""
buildConfigField "String", "SOURCE_COMMIT", "\"${sourceCommit}\""
buildConfigField "int", "PR_NUMBER", "${prNumber}"
buildConfigField "String", "PR_TITLE", "\"${escapeJava(prTitle)}\""
buildConfigField "String", "BUILD_BRANCH", "\"${escapeJava(buildBranch)}\""
buildConfigField "boolean", "CI_BUILD", "${ciBuild}"
buildConfigField "String", "RELEASE_SIGNER", "\"${releaseSigner}\""
buildConfigField "long", "COMMIT_TIME", "${commitTime}L * 1000L"
@@ -194,8 +205,7 @@ android {
}
androidResources {
// aapt's default ignore list, plus the trees shipped as add-on packs (tools/packs).
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~:<dir>droiddeck-xbox:<dir>droiddeck-comet:<dir>directaudio'
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~:<dir>droiddeck-xbox:<dir>droiddeck-comet:<dir>directaudio:<dir>hybris'
}
packagingOptions {
@@ -205,7 +215,6 @@ android {
jniLibs {
useLegacyPackaging = true
pickFirsts += ['**/libtermux.so']
// Shipped in the proot and tracking packs (tools/packs) and run or loaded from there.
excludes += ['**/libproot.so', '**/libproot-loader.so', '**/libopencv_java4.so', '**/libmediapipe_tasks_jni.so']
}
}
@@ -224,8 +233,8 @@ android {
outputs.upToDateWhen { false }
outputs.doNotCacheIf('CI executes the test suite on every run') { true }
}
// Robolectric keeps a sandbox per SDK level the tests ask for; seven no longer fit in Gradle's default 512 MB.
maxHeapSize = '2g'
maxParallelForks = 2
def logTarget = providers.systemProperty('robolectric.logging').orNull
if (logTarget != null) systemProperty 'robolectric.logging', logTarget
}
@@ -240,6 +249,11 @@ android {
}
}
// No instrumented tests; the unused androidTest variant breaks the VS Code Kotlin import.
androidComponents {
beforeVariants(selector().all()) { it.androidTest.enable = false }
}
// Vendor binaries remain in the user's SDK directory; never vendor them into Git.
if (vitureSdkDir) {
android.sourceSets.main.jniLibs.srcDir(new File(vitureSdkDir, 'android'))
@@ -262,6 +276,7 @@ def runtimeAssetsDir = layout.buildDirectory.dir('runtimeAssets')
def stageRuntimeAssets = tasks.register('stageRuntimeAssets', Sync) {
dependsOn(stageSessionScripts)
from(layout.projectDirectory.dir('src/main/assets')) { exclude 'pulseaudio.tzst' }
from(rootProject.file('tools/linuxfs/innoextract'))
from(layout.buildDirectory.dir('prepared-assets'))
into(runtimeAssetsDir)
}
@@ -277,10 +292,28 @@ if (uvcDir) {
}
tasks.named('preBuild').configure { dependsOn(stageRuntimeAssets) }
def stageGogNotices = tasks.register('stageGogNotices', Copy) {
from(layout.projectDirectory.dir('src/main/rust/stores/vendor/xdelta3')) { include 'LICENSE', 'README.droiddeck.md' }
into(layout.buildDirectory.dir('gogNotices/licenses/xdelta3'))
}
android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('gogNotices'))
tasks.named('preBuild').configure { dependsOn(stageGogNotices) }
if (!ciBuild) {
def stageLocalPacks = tasks.register('stageLocalPacks', Sync) {
from(layout.buildDirectory.dir('packs')) { include '*.tar.zst' }
into(layout.buildDirectory.dir('localPackAssets/packs'))
}
android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('localPackAssets'))
tasks.named('preBuild').configure { dependsOn(stageLocalPacks) }
}
def verifyRuntimeInputs = tasks.register('verifyRuntimeInputs', Exec) {
mustRunAfter(stageSessionScripts, stageRuntimeAssets)
workingDir rootProject.projectDir
commandLine pythonExecutable, 'tools/release/runtime_inputs.py', 'verify'
def allowStale = (project.findProperty('allowStaleRuntime')
?: localBuildOptions.getProperty('droiddeck.allowStaleRuntime', 'false')).toString().toBoolean()
commandLine([pythonExecutable, 'tools/release/runtime_inputs.py', 'verify'] + (allowStale ? ['--allow-stale-sources'] : []))
}
tasks.configureEach { task ->
if (task.name in ['packageDebug', 'packageRelease']) task.dependsOn(verifyRuntimeInputs)
@@ -294,6 +327,8 @@ def buildRustStores = tasks.register('buildRustStores', Exec) {
description = 'Cross-compiles the store download engines (libdroiddeckstores.so) with cargo-ndk.'
onlyIf { !skipRust }
inputs.dir(rustCrateDir.dir('src'))
inputs.dir(rustCrateDir.dir('vendor'))
inputs.file(rustCrateDir.file('build.rs'))
inputs.files(rustCrateDir.file('Cargo.toml'), rustCrateDir.file('Cargo.lock'))
inputs.property('ndkVersion', android.ndkVersion)
if (!skipRust) inputs.property('rustToolchain', providers.exec {
@@ -316,6 +351,21 @@ def buildRustStores = tasks.register('buildRustStores', Exec) {
if (!skipRust) android.sourceSets.main.jniLibs.srcDir(rustJniLibsDir)
tasks.named('preBuild').configure { dependsOn(buildRustStores) }
def maliBridgeDir = rootProject.file('external/MaliBridge')
def vkbridgeJniLibsDir = layout.buildDirectory.dir('vkbridgeJniLibs')
def buildVkBridgeServer = tasks.register('buildVkBridgeServer', Exec) {
description = 'Builds the Vulkan bridge server (libvkbridge_server.so) with the NDK.'
inputs.dir(new File(maliBridgeDir, 'common'))
inputs.dir(new File(maliBridgeDir, 'server'))
inputs.dir(new File(maliBridgeDir, 'third_party'))
inputs.file(new File(maliBridgeDir, 'scripts/build-server-android.sh'))
outputs.dir(vkbridgeJniLibsDir)
commandLine 'bash', new File(maliBridgeDir, 'scripts/build-server-android.sh').absolutePath,
android.ndkDirectory.absolutePath, vkbridgeJniLibsDir.get().asFile.absolutePath
}
android.sourceSets.main.jniLibs.srcDir(vkbridgeJniLibsDir)
tasks.named('preBuild').configure { dependsOn(buildVkBridgeServer) }
dependencies {
implementation "org.opencv:opencv:${trackingPack.OPENCV_VERSION}"
implementation 'org.khronos.openxr:openxr_loader_for_android:1.1.49'
@@ -337,9 +387,11 @@ dependencies {
implementation 'androidx.camera:camera-camera2:1.4.2'
implementation 'androidx.camera:camera-lifecycle:1.4.2'
implementation 'androidx.webkit:webkit:1.12.1'
implementation 'io.github.webrtc-sdk:android:125.6422.07'
implementation("com.google.mediapipe:tasks-vision:${trackingPack.MEDIAPIPE_VERSION}") {
exclude group: 'com.google.auto.value', module: 'auto-value'
}
implementation 'com.google.zxing:core:3.5.3'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.16.1'
+145 -13
View File
@@ -2,6 +2,25 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<queries>
<package android:name="cn.nubia.gamelauncher" />
<intent>
<action android:name="android.content.pm.action.CONFIRM_INSTALL" />
</intent>
<intent>
<action android:name="android.content.pm.action.CONFIRM_PERMISSIONS" />
</intent>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
</intent>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.SECONDARY_HOME" />
</intent>
</queries>
<uses-permission android:name="android.permission.REORDER_TASKS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -18,7 +37,10 @@
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.CAMERA" />
@@ -34,6 +56,7 @@
android:localeConfig="@xml/locales_config"
android:networkSecurityConfig="@xml/network_security_config"
android:allowBackup="false"
android:allowAudioPlaybackCapture="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
@@ -46,6 +69,42 @@
<meta-data android:name="android.game_mode_config" android:resource="@xml/game_mode_config" />
<service android:name=".glasses.LumaRgbCaptureService" android:exported="false"
android:process=":rgb_diagnostic" />
<service android:name=".wallpaper.SteamWallService"
android:label="@string/steam_wall_label"
android:permission="android.permission.BIND_WALLPAPER"
android:exported="true">
<intent-filter>
<action android:name="android.service.wallpaper.WallpaperService" />
</intent-filter>
<meta-data android:name="android.service.wallpaper" android:resource="@xml/steam_wall_wallpaper" />
</service>
<activity android:name=".wallpaper.WallpaperSettingsActivity"
android:label="@string/steam_wall_label"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:exported="true" />
<service android:name=".input.HomeButtonService"
android:label="@string/integration_shortcuts_service"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data android:name="android.accessibilityservice" android:resource="@xml/home_button_service" />
</service>
<service android:name=".session.BottomNotificationListener"
android:label="@string/bottom_notifications_listener"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
<receiver android:name=".core.ShortcutsListingReceiver" android:exported="false"
android:process=":shortcuts_sync">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<meta-data android:name="com.oculus.supportedDevices" android:value="quest2|quest3|quest3s" />
<activity android:name=".QuestXrActivity" android:exported="true" android:launchMode="singleTask"
android:permission="android.permission.DUMP"
@@ -60,11 +119,23 @@
</intent-filter>
</activity>
<activity
android:name=".PairingGuideActivity"
android:exported="false"
android:launchMode="singleTask"
android:taskAffinity="${applicationId}.pairingguide"
android:excludeFromRecents="true"
android:supportsPictureInPicture="true"
android:resizeableActivity="true"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density|keyboard|keyboardHidden|navigation"
android:theme="@style/AppTheme" />
<activity
android:name=".FrontEndActivity"
android:exported="true"
android:screenOrientation="sensorLandscape"
android:screenOrientation="user"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density|keyboard|keyboardHidden|navigation">
<layout android:defaultWidth="1024dp" android:defaultHeight="640dp" />
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
@@ -115,7 +186,7 @@
android:excludeFromRecents="true"
android:noHistory="true"
android:taskAffinity="${applicationId}.home"
android:screenOrientation="sensorLandscape"
android:screenOrientation="user"
android:theme="@android:style/Theme.Translucent.NoTitleBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -124,26 +195,37 @@
</intent-filter>
</activity>
<activity
android:name=".PinRequestActivity"
android:exported="true"
android:excludeFromRecents="true"
android:taskAffinity="${applicationId}.pin"
android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
<intent-filter>
<action android:name="android.content.pm.action.CONFIRM_PIN_SHORTCUT" />
<action android:name="android.content.pm.action.CONFIRM_PIN_APPWIDGET" />
</intent-filter>
</activity>
<activity
android:name=".input.ControllerEditorActivity"
android:exported="false"
android:theme="@style/SessionTheme"
android:screenOrientation="sensorLandscape"
android:resizeableActivity="true"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
<activity
android:name=".files.FileManagerActivity"
android:name=".sync.SyncthingWebActivity"
android:exported="false"
android:screenOrientation="sensorLandscape"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density"
android:theme="@style/AppTheme" />
<activity
android:name=".files.FileManagerActivity"
android:exported="false"
android:screenOrientation="user"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density"
android:theme="@style/AppTheme" />
<activity
android:name=".files.FilePickerActivity"
android:exported="false"
android:screenOrientation="sensorLandscape"
android:screenOrientation="user"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density"
android:theme="@style/AppTheme" />
@@ -151,10 +233,31 @@
android:process=":xboxlogin" android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density"
android:theme="@style/AppTheme" />
<activity android:name=".stores.xbox.XboxCloudPlayerActivity" android:exported="false"
android:screenOrientation="sensorLandscape" android:hardwareAccelerated="true"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|density|navigation|uiMode"
android:theme="@style/AppTheme" />
<service android:name=".stores.xbox.XboxCloudTokenService" android:exported="false" />
<activity android:name=".stores.xbox.XboxNativePlayerActivity" android:exported="false"
android:process=":xcloudnative" android:screenOrientation="sensorLandscape" android:hardwareAccelerated="true"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|density|navigation|uiMode"
android:theme="@style/AppTheme" />
<activity android:name=".stores.xbox.XboxCloudActivity" android:exported="false"
android:process=":xboxcloud" android:launchMode="singleTop" android:screenOrientation="sensorLandscape"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|density|navigation|uiMode"
android:theme="@style/AppTheme" />
<activity android:name=".stores.xbox.XboxCloudSettingsActivity" android:exported="false"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density"
android:theme="@style/AppTheme" />
<activity
android:name=".stores.StoreLoginActivity"
android:exported="false"
android:screenOrientation="sensorLandscape"
android:screenOrientation="user"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density"
android:theme="@style/AppTheme" />
@@ -177,6 +280,26 @@
android:resizeableActivity="true"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
<activity
android:name=".ui.DualScreenActivity"
android:exported="false"
android:excludeFromRecents="true"
android:taskAffinity="${applicationId}.dualscreen"
android:theme="@style/SessionTheme"
android:resizeableActivity="true"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
<activity
android:name=".ui.SessionTypingActivity"
android:exported="false"
android:excludeFromRecents="true"
android:noHistory="true"
android:taskAffinity="${applicationId}.typing"
android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen"
android:resizeableActivity="true"
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
<activity
android:name=".gpu.SteamVrRestartActivity"
android:exported="false"
@@ -191,6 +314,11 @@
android:name=".session.SessionService"
android:exported="false"
android:stopWithTask="false" />
<!-- The compositor's native state is one per process, so the bottom-screen desktop's compositor runs in its own. -->
<service
android:name=".wayland.BottomDesktopCompositorService"
android:process=":bottom_desktop"
android:exported="false" />
<service
android:name=".runtime.RuntimeInstallService"
android:exported="false" />
@@ -201,6 +329,10 @@
android:name=".stores.download.StoreDownloadService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name=".sync.SyncthingService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.logs"
@@ -3,31 +3,52 @@
"lxqt-r1": "c2bb860b3b1688287451e2495be5db313ec07f8c8af501dfac2768a9f417bc86",
"desktop-kde-r1": "55747e75d0ec055ab7ea1e92a757f3be227cc3205bb9101f5b53cadef6318579",
"desktop-kde-r2": "9ba832677b498654136c69d8c8330434d17e4d53cd099acac9bde3ba429b35bf",
"emulators-r1": "a1f4806a9663d28f424b4cab1277e0ede80a92e69de5bc063cdc8b45af220164"
"emulators-r1": "a1f4806a9663d28f424b4cab1277e0ede80a92e69de5bc063cdc8b45af220164",
"desktop-kde-r3": "8b7142fcec8d86143ae39fc14ffc86907d30d9a5b5a45f49cb1b55109d6fc4eb"
},
"desktopApplications": {
"ark-26.08.1-1-aarch64.pkg.tar.xz": "93e2c2b75ff20cd29b4f312ed1979ef6f27255b93232b93bfcdb31239903c1d4",
"breeze-6.7.5-1-aarch64.pkg.tar.xz": "1d7f4eb9d8d234caaea23621909f5233d19462c2f058afeeb54bed7b81bd03bd",
"breeze-gtk-6.7.5-1-any.pkg.tar.xz": "a0f59bc4fc426879c8f8a97f6dcc707fb7d1b52913da5eb7e9db83777b6957b7",
"breeze-icons-6.30.0-1-aarch64.pkg.tar.xz": "4bae4689032501156687a227024e56554820747644953c67fc8d2e2b6b0b045a",
"dolphin-26.08.1-1-aarch64.pkg.tar.xz": "fef738ab8132d3eba1156b98c116699ce0a5efcb4cb462efbacdcf4c47f79a23",
"firefox-157.0.1-1-aarch64.pkg.tar.xz": "d142fdb8a339a65eca7cea6a2bba5b29202386166abd9d59851d6757c9eb2163",
"gwenview-26.08.1-1-aarch64.pkg.tar.xz": "a9dc7d5ab73fa677905d5a57248b1b9b404b6b16e862958cc7443d387f684a47",
"kate-26.08.1-1-aarch64.pkg.tar.xz": "acf50710042746f243a3b219a1ae0f788e338f3dd9d75093e8bb671a618b83b6",
"kde-cli-tools-6.7.5-1-aarch64.pkg.tar.xz": "af466398a912a994a97fadfe46aff401d88a90d7fdce7e843dc4218098fa38ac",
"kde-gtk-config-6.7.5-1-aarch64.pkg.tar.xz": "ba6cb4cb01dbb3f2c247f2f6973ede3b64c0849f89340217b51dfbf11cb8cb5c",
"kinfocenter-6.7.5-1-aarch64.pkg.tar.xz": "f3d5416db96b50f9a5b8a284d82ae21d834a8cce646010abdd7087491c399b0b",
"konsole-26.08.1-1-aarch64.pkg.tar.xz": "1ecf87c0d11d620ebe4aeafb02788e0d5f4fe9019ad9c5d9f1011c43237dd5ec",
"kscreen-6.7.5-1-aarch64.pkg.tar.xz": "8fbaa6fedf93edeba021a5416b551663654919b27c2ee237d696ec0b0ce7d5b7",
"kwin-6.7.5-1-aarch64.pkg.tar.xz": "a7535710c74b90359f80306134b2b7581132cbac508dc84b7e07ef275cc8c3fe",
"7zip-26.05-1-aarch64.pkg.tar.xz": "17569bafbc5bd1a044ca1db164b96cb15857a22a3575f36679de46e693ae285e",
"ark-26.08.2-1-aarch64.pkg.tar.xz": "b93cc90c4fc8a6cb0247fe8fb0f9de1c2074f5173fa2735d3d1959c9162c08f2",
"breeze-6.8.1-1-aarch64.pkg.tar.xz": "70a2ff3afd41602f184814af5c6e298ff84ef7cb7e5880ca7cb4f6fb28521511",
"breeze-gtk-6.8.1-1-any.pkg.tar.xz": "e92b9acf3155db22d4a288a43e8d95c20c72b8d8cffd028dd6800eb01a304bf0",
"breeze-icons-6.31.0-1-aarch64.pkg.tar.xz": "b252ef02395621b5891d72c5f7fccd1a1ead1ce770886e9fced4bf21e1130096",
"discover-6.8.1-1-aarch64.pkg.tar.xz": "64cfca615e7c18530c684325eb36a5c45e18c63b31f651335474f53b0178958c",
"dolphin-26.08.2-1-aarch64.pkg.tar.xz": "4b5f7a9212c88d47146b374742f67f5620ab128ad893d945721bb43501066b70",
"elisa-26.08.2-1-aarch64.pkg.tar.xz": "fd2df84a2905b59fba0ab1b7fadafe567c5a1dfd44d53ae4cd4996f683b83ba2",
"filelight-26.08.2-1-aarch64.pkg.tar.xz": "17f39435104baf0743ab508bdde6e8bd1b3417b025b67ca8f343032d2fdef3e6",
"firefox-158.0.1-1-aarch64.pkg.tar.xz": "4940e14c05522bfee8df901e1cfe81f56036c82e8f3d95f9f7a2a12e6b38d232",
"flatpak-kcm-6.8.1-1-aarch64.pkg.tar.xz": "3145601002fb87d61307949a3011a8f27a7b1e9851fc59835460f934f8e00098",
"git-2.56.0-1-aarch64.pkg.tar.xz": "b152a20edb215e98cada4b2dfbd02a9e925bda0b2de6ac1441e9e60b6698d3b5",
"gwenview-26.08.2-1-aarch64.pkg.tar.xz": "02aae3973ec0563022c89e312ce7696d05d33514101ae2a01df3275717929de2",
"htop-3.5.3-1-aarch64.pkg.tar.xz": "a58899bd2596d55575493509b3a5aa6f53075cb3d88bb6c712088c57068d9eb7",
"kate-26.08.2-1-aarch64.pkg.tar.xz": "89c21ad8e62d20961b13f8fac8d9f4d358b1a3feebed243f6412e4ad996d5e9e",
"kcalc-26.08.2-1-aarch64.pkg.tar.xz": "79b3303ccc5ae956e97313439456243e59f0b79a8166ca91606a25d1c9e41029",
"kde-cli-tools-6.8.1-1-aarch64.pkg.tar.xz": "b052e9c25e7d6896be67846696f14c9e00430c0445f358ee64cf2a8ea16aa2eb",
"kde-gtk-config-6.8.1-1-aarch64.pkg.tar.xz": "3095da034ac5fe8594165e3396f61f083b0c011cd96807146ddfb53690118608",
"kfind-26.08.2-1-aarch64.pkg.tar.xz": "b4108cc3bd9efd62970379aadb3cb22e99dd1f54873ff432392edd0a833bb393",
"kinfocenter-6.8.1-1-aarch64.pkg.tar.xz": "758882ef13a166205acab0e6bb96ce756d638d91e501db400cb579b9e1331d31",
"konsole-26.08.2-1-aarch64.pkg.tar.xz": "0cf27f69d1792a094d4a935f578c82f8151b2d359198d9cd32bfdbd8dec544e4",
"kscreen-6.8.1-1-aarch64.pkg.tar.xz": "72c948dfdebfef305623527de27274baf1a2328c87e71033fd852e386d9fdf5c",
"kwin-6.8.1-2-aarch64.pkg.tar.xz": "726c98a3f8682b487c2b313633edbd80aa8b20ff657a6b63bbb1b73454f6e0c4",
"less-1:710-1-aarch64.pkg.tar.xz": "8e1d4cbecf6d2ae734cfc1c5495367c0a9245f92dfd2bc03d4d0ab14f8b26ff2",
"man-db-2.13.1-2-aarch64.pkg.tar.xz": "67515751f848717e1f6d3470f57ad2ea6900ee07618fd2726b6fcaa695687e80",
"man-pages-6.19-1-any.pkg.tar.xz": "69df38f1a1aa35240d239ff82a2e067950ac394f23b8a6c7307580ccd7e95f07",
"mpv-1:0.41.0-6-aarch64.pkg.tar.xz": "6c89d00736820210a3c7f32af4cf95d155191d463f9df8bd22137c45f30a554a",
"plasma-desktop-6.7.5-1-aarch64.pkg.tar.xz": "b266dd42cea965b5ab1cc1ae600877e9d3bc83ec642c61fe1669c4457e641a55",
"plasma-integration-6.7.5-1-aarch64.pkg.tar.xz": "daa96da14d9a4d12c0d9c0341e1be6836a9a437c469d61da5e9844d51e2606cf",
"plasma-pa-6.7.5-1-aarch64.pkg.tar.xz": "02dbefe9d3e0069b2bb37cac2a6be18b8f0a1887c87704a0716e7a9268eadcd8",
"plasma-workspace-6.7.5-1-aarch64.pkg.tar.xz": "3a1455857afaa9ccf342ab6167f0f15794ee9d248b0925398ea6f62f12730a86",
"spectacle-1:6.7.5-1-aarch64.pkg.tar.xz": "0c9837412ea6119edef18bcb0a8fceb74634cb35be0ca78b3e1dd1ff2443866e",
"systemsettings-6.7.5-1-aarch64.pkg.tar.xz": "b02b6a726857d5125c92d1ec95379216c7a3bc1f187a3b46e88c0d056b29ce70",
"xdg-desktop-portal-kde-6.7.5-1-aarch64.pkg.tar.xz": "2c57d17d34d577023eb9cd7b0fca1ba9163549afe60450f6271aa148af099ef2"
"okular-26.08.2-1-aarch64.pkg.tar.xz": "aef87cf74730d2345092184d7e9b14a0068ba4030de58564aa770506175cbf06",
"plasma-desktop-6.8.1-1-aarch64.pkg.tar.xz": "4b24662a2059b21ca98b5b35b72216f22c9e316057247ea018bf6318b07cfdb6",
"plasma-integration-6.8.1-1-aarch64.pkg.tar.xz": "514f7d2a96370814b9374d7c52e1873257af6e18190d130e5a8d3d6d1fffdc54",
"plasma-pa-6.8.1-1-aarch64.pkg.tar.xz": "13dff66d30072744c7870bd1a294ae96f0fee086a1533b8052d7dfc038d57c45",
"plasma-systemmonitor-6.8.1-1-aarch64.pkg.tar.xz": "551b7c1f24cd5df64de4f5ba199bc71c00e2924c16a8241b21c020584794e10d",
"plasma-workspace-6.8.1-1-aarch64.pkg.tar.xz": "4c713990aeab118d50865c818f9689ae7637dc6556e4a0e2fec5e0b348cdcd29",
"spectacle-1:6.8.1-1-aarch64.pkg.tar.xz": "0d697803bd0135701688be3869d4d99eec6b8b43ca761610da73f44cb77bfa5c",
"systemsettings-6.8.1-1-aarch64.pkg.tar.xz": "db528089fa328d67dadb50be3b822858238785d8c659a9ef95389b993efcb234",
"unrar-1:7.3.1-1-aarch64.pkg.tar.xz": "9883e19b1f1adfa430285191b1f96db4cc208608baebd955f3fe9fee92cd3bdd",
"unzip-6.0-23-aarch64.pkg.tar.xz": "edc91c7e4e400f20345439744262d1ceded104c78d940659a6b9876076ef33fc",
"wget-1.25.0-6-aarch64.pkg.tar.xz": "e2bf0067cca93877c737c7d48bc36fff32e184c7874829429b9473c10a12dd39",
"wl-clipboard-1:2.3.0-1-aarch64.pkg.tar.xz": "723a63b140e00085a533ebb1ec36fa9305b5e6cbd8fb80c0521fbeff77a72982",
"xclip-0.13-6-aarch64.pkg.tar.xz": "772ec9336b679b0ef277d1ef0b69b74777467239e16ec845af00c07fa849d166",
"xdg-desktop-portal-kde-6.8.1-1-aarch64.pkg.tar.xz": "8098c0ef0454335bb7c806049d66977c512657346222606737d72a7ae1de3146",
"zip-3.0-14-aarch64.pkg.tar.xz": "9887f177544021809d065d0298514e98f76b55f9120119f748f5a28c8bb51994"
},
"legacyApplications": {
"featherpad-1.6.3-1-aarch64.pkg.tar.xz": "fcac55215815c0b712453f9bcc09abfe4b518e36025a111a7a7598ef04913f3d",
@@ -1,2 +1,3 @@
desktop-kde-r1
desktop-kde-r2
desktop-kde-r3
+18
View File
@@ -0,0 +1,18 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<style>
html, body { margin: 0; height: 100%; background: #000; overflow: hidden; }
#player { position: absolute; inset: 0; }
#status { position: absolute; left: 0; right: 0; bottom: 12%; text-align: center; color: #ddd;
font: 16px sans-serif; text-shadow: 0 0 4px #000; pointer-events: none; }
</style>
</head>
<body>
<div id="player"></div>
<div id="status"></div>
<script src="player.js"></script>
</body>
</html>
+187
View File
@@ -0,0 +1,187 @@
/* DroidDeck Xbox Cloud Gaming player. Includes Greenlight's player (c) UnknownSKL (Jim Kroon), MIT. */
(()=>{var _=class{_player;_dataChannel;constructor(e){this._player=e,this._dataChannel=this._player._peerConnection.createDataChannel(this.getChannelName(),this.getChannelConfig()),this._dataChannel.onopen=this.onOpen.bind(this),this._dataChannel.onmessage=this.onMessage.bind(this),this._dataChannel.onclosing=this.onClosing.bind(this),this._dataChannel.onclose=this.onClose.bind(this),this._dataChannel.onerror=this.onError.bind(this)}getChannelName(){return"channel"}getChannelConfig(){return{}}onOpen(e){console.log(this.getChannelName(),"Opening channel:",e)}onMessage(e){console.log(this.getChannelName(),"Message channel:",e)}onClosing(e){console.log(this.getChannelName(),"Closing channel:",e)}onClose(e){console.log(this.getChannelName(),"Closed channel:",e)}onError(e){console.log(this.getChannelName(),"Error channel:",e)}send(e){if(this._dataChannel.readyState!=="open"){console.log("["+this.getChannelName()+"] Tried to send data while channel is not open. readyState is:",this._dataChannel.readyState);return}return typeof e=="string"&&(e=new TextEncoder().encode(e)),this._dataChannel.send(e)}getPlayer(){return this._player}destroy(){}};var S=class extends _{_micPermissions="prompt";_micStream;constructor(e){super(e),navigator.permissions.query({name:"microphone"}).then(t=>{this._micPermissions=t.state,t.onchange=()=>{this._micPermissions=t.state}})}getChannelName(){return"chat"}getChannelConfig(){return{ordered:!0,protocol:"chatV1"}}destroy(){}};var v=class r{_player;_index;_physicalGamepadId=-1;_rumbleInterval;_isFocused=!0;_gamepadOverride={buttons:{}};_options={enable_keyboard:!1,enable_gamepad:!0,enable_vibration:!0,keyboard_mapping:{A:"Enter",B:"Backspace",X:"x",Y:"y",DPadUp:"ArrowUp",DPadDown:"ArrowDown",DPadLeft:"ArrowLeft",DPadRight:"ArrowRight",LeftShoulder:"[",RightShoulder:"]",LeftThumb:"l",RightThumb:"r",LeftTrigger:"-",RightTrigger:"=",Menu:"m",View:"v",Nexus:"n"},gamepad_mapping:{A:"0",B:"1",X:"2",Y:"3",DPadUp:"12",DPadDown:"13",DPadLeft:"14",DPadRight:"15",LeftShoulder:"4",RightShoulder:"5",LeftThumb:"10",RightThumb:"11",LeftTrigger:"6",RightTrigger:"7",Menu:"9",View:"8",Nexus:"16"},gamepad_axes_mapping:{LeftThumbXAxis:"0",LeftThumbYAxis:"1",RightThumbXAxis:"2",RightThumbYAxis:"3"},gamepad_deadzone:.08,gamepad_force_capture:!1};_listener={keyDown:this.onKeyDown.bind(this),keyUp:this.onKeyUp.bind(this),gamepadConnected:this.onGamepadConnected.bind(this),gamepadDisconnected:this.onGamepadDisconnected.bind(this),windowFocus:this.onWindowFocus.bind(this),windowBlur:this.onWindowBlur.bind(this)};constructor(e,t={}){this._index=e,this._options={...this._options,...t}}attach(e){this._player=e,this._player._channels.control.sendGamepadState(this._index,!0,this),this._options.enable_keyboard===!0&&(window.addEventListener("keydown",this._listener.keyDown),window.addEventListener("keyup",this._listener.keyUp)),this._options.enable_gamepad===!0&&(window.addEventListener("gamepadconnected",this._listener.gamepadConnected),window.addEventListener("gamepaddisconnected",this._listener.gamepadDisconnected)),this._options.enable_gamepad===!0&&this._options.gamepad_force_capture===!1&&(window.addEventListener("blur",this._listener.windowBlur),window.addEventListener("focus",this._listener.windowFocus)),this.detectActiveGamepad()}detach(){if(this._player===void 0){console.log("[Gamepad] Player is not attached. this._player is:",this._player);return}this._player._channels.control.sendGamepadState(this._index,!1),this._physicalGamepadId=-1,this._rumbleInterval!==void 0&&clearInterval(this._rumbleInterval),this._options.enable_keyboard===!0&&(window.removeEventListener("keydown",this._listener.keyDown),window.removeEventListener("keyup",this._listener.keyUp)),this._options.enable_gamepad===!0&&(window.removeEventListener("gamepadconnected",this._listener.gamepadConnected),window.removeEventListener("gamepaddisconnected",this._listener.gamepadDisconnected)),this._options.enable_gamepad===!0&&this._options.gamepad_force_capture===!1&&(window.removeEventListener("blur",this._listener.windowBlur),window.removeEventListener("focus",this._listener.windowFocus))}sendButtonState(e,t){if(this._player===void 0){console.log("[Gamepad] Player is not attached. this._player is:",this._player);return}if(!(e in this.getDefaultFamepadFrame())){console.log("[Gamepad] Invalid button:",e);return}let n=this.getDefaultFamepadFrame();n.GamepadIndex=this._index,this._gamepadOverride.buttons[e]=t}onKeyDown(e){if(this._isFocused!==!1)for(let t in this._options.keyboard_mapping)this._options.keyboard_mapping[t]===e.key&&(this._gamepadOverride.buttons[t]=1,e.preventDefault())}onKeyUp(e){if(this._isFocused!==!1)for(let t in this._options.keyboard_mapping)this._options.keyboard_mapping[t]===e.key&&(this._gamepadOverride.buttons[t]=0,e.preventDefault())}onWindowFocus(){this._isFocused=!0}onWindowBlur(){}getDefaultFamepadFrame(){return{GamepadIndex:0,Nexus:0,Menu:0,View:0,A:0,B:0,X:0,Y:0,DPadUp:0,DPadDown:0,DPadLeft:0,DPadRight:0,LeftShoulder:0,Line truncated
`+t.ContentText)?this.completeTransaction(e.id,{Result:0}):this.completeTransaction(e.id,{Result:1})}};var M=class extends _{_handshakeCompleted=!1;getChannelName(){return"message"}getChannelConfig(){return{protocol:"messageV1",ordered:!0}}onOpen(){console.log("[MessageChannel] Sending handshake...");let e=JSON.stringify({type:"Handshake",version:"messageV1",id:"be0bfc6d-1e83-4c8a-90ed-fa8601c5a179",cv:"0"});this.send(e)}onMessage(e){let t=JSON.parse(e.data);switch(t.type){case"HandshakeAck":console.log("[MessageChannel] Handshake completed"),this._handshakeCompleted=!0,this.getPlayer()._channels.control.sendAuthorization(),this.getPlayer()._channels.input.start(),this.sendConfig();break;case"TransactionStart":case"Message":new D(this,t);break;default:console.log("[MessageChannel] Unhandled message type:",t.type,t)}}sendConfig(){let e=[],t=[0,2,0],n=JSON.stringify(this.generateMessage("/streaming/systemUi/configuration",{version:t,systemUis:e}));this.send(n);let i=JSON.stringify(this.generateMessage("/streaming/properties/clientappinstallidchanged",{clientAppInstallId:"c97d7ee0-73b2-4239-bf1d-9d805a338429"}));this.send(i);let a=JSON.stringify(this.generateMessage("/streaming/characteristics/orientationchanged",{orientation:0}));this.send(a);let s=JSON.stringify(this.generateMessage("/streaming/characteristics/touchinputenabledchanged",{touchInputEnabled:!1}));this.send(s);let o=JSON.stringify(this.generateMessage("/streaming/characteristics/clientdevicecapabilities",{}));this.send(o);let d=JSON.stringify(this.generateMessage("/streaming/characteristics/dimensionschanged",{horizontal:1920,vertical:1080,preferredWidth:1920,preferredHeight:1080,safeAreaLeft:0,safeAreaTop:0,safeAreaRight:1920,safeAreaBottom:1080,supportsCustomResolution:!0}));this.send(d)}generateMessage(e,t){return{type:"Message",content:JSON.stringify(t),id:Q(),target:e,cv:""}}destroy(){}};var E=class{_address;_ipv4Address;_ipv4Port;constructor(e){this._address=e;let t=this._address.split(":"),n=t[6]+t[7],i=t[5],a=~parseInt(n.substring(0,2),16)&255,s=~parseInt(n.substring(2,4),16)&255,o=~parseInt(n.substring(4,6),16)&255,d=~parseInt(n.substring(6,8),16)&255,u=~parseInt(i.substring(0,4),16)&65535;this._ipv4Address=a+"."+s+"."+o+"."+d,this._ipv4Port=u}getIpv4Address(){return this._ipv4Address}getIpv4Port(){return this._ipv4Port}};var R=class{_peerConnection;_iceCandidates=[];constructor(e){this._peerConnection=e._peerConnection,this._peerConnection.addEventListener("icecandidate",t=>{t.candidate&&this._iceCandidates.push(t.candidate)})}getCandidates(){return this._iceCandidates}setRemoteCandidates(e){e.length===0&&console.warn("Error: No candidates received!");for(let t in e){if(e[t].candidate==="a=end-of-candidates")continue;let n=e[t].candidate.split(" ");if(n.length>4&&n[4].substr(0,4)==="2001"){let i=new E(n[4]);this._peerConnection.addIceCandidate({candidate:"a=candidate:10 1 UDP 1 "+i.getIpv4Address()+" 9002 typ host ",sdpMLineIndex:e[t].sdpMLineIndex,sdpMid:e[t].sdpMid}),this._peerConnection.addIceCandidate({candidate:"a=candidate:11 1 UDP 1 "+i.getIpv4Address()+" "+i.getIpv4Port()+" typ host ",sdpMLineIndex:e[t].sdpMLineIndex,sdpMid:e[t].sdpMid})}this._peerConnection.addIceCandidate({candidate:e[t].candidate,sdpMid:e[t].sdpMid,sdpMLineIndex:e[t].sdpMLineIndex})}}};var I=class{_player;_peerConnection;constructor(e){this._player=e,this._peerConnection=e._peerConnection}getAvailableCodecs(){let e=RTCRtpReceiver.getCapabilities("video")?.codecs;if(e===void 0)return[];let t=[];for(let n in e)t.includes(e[n].mimeType)||t.push(e[n].mimeType);return t}setLocalSDP(e){return console.log("[SDP] setLocalSDP() Setting local SDP:",e),e.sdp=e.sdp?.replace("useinbandfec=1","useinbandfec=1; stereo=1"),e}setRemoteSDP(e){return console.log("[SDP] setRemoteSDP() Setting remote SDP:",e),e}getDefaultCodecPreferences(){let e=RTCRtpReceiver.getCapabilities("video")?.codecs;if(e===void 0)return[];let t=[],n=[],i=[],a=[];for(let o in e)e[o].mimeType.includes("H264")?e[o].sdpFmtpLine?.includes("profile-level-id=4d")?t.push(e[o]):e[o].sdpFmtpLine?.includes("profile-level-id=42e")?n.push(e[o]):e[o].sdpFmtpLine?.includes("profile-level-id=420")&&i.push(e[o]):(e[o].mimeType.includes("ulpfec")||e[o].mimeType.includes("flexfec")||e[o].mimeType.includes("VP9")||e[o].mimeType.includes("VP8"))&&a.push(e[o]);return[...t,...n,...i,...a]}_setBitrate(e,t,n){let i=e.split(`
`),a=-1;for(let o=0;o<i.length;o++)if(i[o].indexOf("m="+t)===0){a=o;break}if(a===-1)return console.debug("Could not find the m line for",t),e;for(a++;i[a].indexOf("i=")===0||i[a].indexOf("c=")===0;)a++;if(i[a].indexOf("b")===0)return i[a]="b=AS:"+n,i.join(`
`);let s=i.slice(0,a);return s.push("b=AS:"+n),s=s.concat(i.slice(a,i.length)),s.join(`
`)}};var P=class{_player;_peerConnection;_videoCodec="";_audioCodec="";_videoWidth=0;_videoHeight=0;_videoFps=0;_rtt=0;_remoteHost="";_remotePort=0;_remoteIsLocal=!1;_remoteIsIpv6=!1;_activeRemoteCandidate="";constructor(e){this._player=e,this._peerConnection=e._peerConnection,setInterval(this.loop.bind(this),1e3)}loop(){this._peerConnection.getStats().then(e=>{e.forEach(t=>{t.type==="inbound-rtp"?this.readInboundRtp(t):t.type==="candidate-pair"&&t.packetsReceived!==void 0&&t.packetsReceived>0?this.readCandidatePair(t):t.type==="remote-candidate"&&this.readRemoteCandidate(t)})})}readInboundRtp(e){e.kind==="video"&&e.codecId&&e.packetsReceived!==void 0&&e.packetsReceived>0?(e.codecId.includes("profile-level-id=4d")?this._videoCodec="H264 (High)":e.codecId.includes("profile-level-id=42e")?this._videoCodec="H264 (Normal)":e.codecId.includes("profile-level-id=420")?this._videoCodec="H264 (Low)":this._videoCodec="Other ("+e.codecId+")",e.frameWidth&&(this._videoWidth=e.frameWidth),e.frameHeight&&(this._videoHeight=e.frameHeight),e.framesPerSecond&&(this._videoFps=e.framesPerSecond)):e.kind==="audio"&&e.codecId&&e.packetsReceived!==void 0&&e.packetsReceived>0&&(this._audioCodec=e.codecId)}readCandidatePair(e){this._activeRemoteCandidate=e.remoteCandidateId,this._rtt=e.currentRoundTripTime}readRemoteCandidate(e){e.id===this._activeRemoteCandidate&&(this.isPrivateIP(e.address)?this._remoteIsLocal=!0:this._remoteIsLocal=!1,this.isIpv6(e.address)?this._remoteIsIpv6=!0:this._remoteIsIpv6=!1,this._remoteHost=e.address,this._remotePort=e.port)}isPrivateIP(e){let t=e.split(".");return t[0]==="10"||t[0]==="172"&&parseInt(t[1],10)>=16&&parseInt(t[1],10)<=31||t[0]==="192"&&t[1]==="168"}isIpv6(e){return e.split(":").length>3}};var G=class{_player;_videoComponent;_overlays={debug:void 0};constructor(e,t){this._videoComponent=e,this._player=t}toggleDebug(){this._overlays.debug===void 0?this.createDebugOverlay():this.destroy()}createDebugOverlay(){this._overlays.debug=document.createElement("div"),this._overlays.debug.id="playerDebugOverlay",this._overlays.debug.style.position="absolute",this._overlays.debug.style.top="0",this._overlays.debug.style.left="0",this._overlays.debug.style.width=this._videoComponent.getElement()?.style.width||"100%",this._overlays.debug.style.height=this._videoComponent.getElement()?.style.height||"auto",this._overlays.debug.style.background="linear-gradient(0deg, rgba(0,0,0, 0) 0%, rgba(0,0,0, 0) 75%, rgba(0,0,0, 0.5) 100%)",this._overlays.debug.style.padding="10px",this._overlays.debug.style.pointerEvents="none",document.getElementById(this._player.getElementId())?.appendChild(this._overlays.debug),setTimeout(()=>{this.refreshDebugOverlay()},500),this.refreshDebugOverlay()}destroy(){this._overlays.debug?.remove(),this._overlays.debug=void 0}refreshDebugOverlay(){if(this._overlays.debug!==void 0){this._overlays.debug.innerHTML="",this._overlays.debug.appendChild(this.createLabel("Local Play",this._player.getStats()._remoteIsLocal?"Local":"Remote","ok")),this._overlays.debug.appendChild(this.createLabel("Resolution",this._player.getStats()._videoWidth+"x"+this._player.getStats()._videoHeight,"ok")),this._overlays.debug.appendChild(this.createLabel("Stream FPS",this._player.getStats()._videoFps.toString(),this._player.getStats()._videoFps>=58?"ok":"warning"));let e=this._videoComponent.getFrameStats?.();e&&(this._overlays.debug.appendChild(this.createLabel("Render FPS",e.renderingFps.toString(),e.renderingFps>=58?"ok":"warning")),this._overlays.debug.appendChild(this.createLabel("Render Delay",e.renderDelayMs.toString()+" ms",e.renderDelayMs<16?"ok":e.renderDelayMs<33?"warning":"error")),this._overlays.debug.appendChild(this.createLabel("Dropped Frames",e.droppedFrames.toString(),e.droppedFrames===0?"ok":e.droppedFrames<30?"warning":"error"))),this._overlays.debug.appendChild(this.createLabel("Connection",this._player.getStats()._remoteIsIpv6?"IPv6":"IPv4","ok")),this._overlays.debug.appendChild(this.createLabel("Video",this._player.getStats()._videoCodec,"ok"));let t=this._player.getStats()._rtt*1e3>75?"error":this._player.getStats()._rtt*1e3>40?"warning":"ok";this._overlays.debug.appendChild(this.createLabel("RTT",(this._player.getStats()._rtt*1e3).toString()+" ms",t)),setTimeout(()=>{this.refreshDebugOverlay()},500)}}createLabel(e,t="",n=""){let i={borderRadius:"5px",background:"linear-gradient(0deg, rgba(28,26,26,1) 0%, rgba(47,45,45,1) 100%)",padding:"10px",fontSize:"12px",marginTop:"3px",marginRight:"10px",marginBottom:"2px",textTransform:"uppercase",color:"white",display:"inline-block"},a=document.createElement("span"),s={error:"rgb(220, 53, 69)",warning:"rgb(255, 193, 7)",ok:"rgb(25, 135, 84)"};t!==""?a.innerHTML=`${e}: <span style="background: ${s[n]}; border-radius: 5px; padding: 4px; padding-left: 8px; padding-right: 8px;">${t}</span>`:a.innerText=e;for(let o in i)a.style[o]=i[o];return a}};var L=class{_player;_element;_overlay;constructor(e){this._player=e,this._overlay=new G(this,this._pLine truncated
in vec2 position;
out vec2 vUV;
void main() { vUV = position * 0.5 + 0.5; gl_Position = vec4(position, 0.0, 1.0); }`,j=`attribute vec2 position;
varying vec2 vUV;
void main() { vUV = position * 0.5 + 0.5; gl_Position = vec4(position, 0.0, 1.0); }`,Z=`#version 300 es
precision mediump float;
uniform sampler2D screenTexture;
uniform vec2 resolution;
uniform int filterId;
uniform float sharpenFactor;
uniform float brightness;
uniform float contrast;
uniform float gamma;
uniform float saturation;
uniform float deband;
out vec4 fragColor;
const float CAS_CONTRAST_PEAK = 0.8 * -3.0 + 8.0;
vec3 sharpen(vec2 coord, vec3 e) {
vec2 px = 1.0 / resolution;
vec3 b = texture(screenTexture, coord + px * vec2(0, 1)).rgb;
vec3 d = texture(screenTexture, coord + px * vec2(-1, 0)).rgb;
vec3 f = texture(screenTexture, coord + px * vec2(1, 0)).rgb;
vec3 h = texture(screenTexture, coord + px * vec2(0, -1)).rgb;
vec3 a = texture(screenTexture, coord + px * vec2(-1, 1)).rgb;
vec3 c = texture(screenTexture, coord + px * vec2(1, 1)).rgb;
vec3 g = texture(screenTexture, coord + px * vec2(-1, -1)).rgb;
vec3 i = texture(screenTexture, coord + px * vec2(1, -1)).rgb;
if (filterId == 1) {
vec3 blur = ((a + c + g + i) + (b + d + f + h) * 2.0 + e * 4.0) / 16.0;
return e + (e - blur) * sharpenFactor / 3.0;
}
vec3 minRgb = min(min(min(d, e), min(f, b)), h) + min(min(a, c), min(g, i));
vec3 maxRgb = max(max(max(d, e), max(f, b)), h) + max(max(a, c), max(g, i));
vec3 amplify = inversesqrt(clamp(min(minRgb, 2.0 - maxRgb) / maxRgb, 0.0, 1.0));
vec3 weight = -(1.0 / (amplify * CAS_CONTRAST_PEAK));
vec3 outColor = clamp(((b + d + f + h) * weight + e) / (4.0 * weight + 1.0), 0.0, 1.0);
return mix(e, outColor, sharpenFactor / 2.0);
}
float ign(vec2 p) { return fract(52.9829189 * fract(dot(p, vec2(0.06711056, 0.00583715)))); }
void main() {
vec2 uv = gl_FragCoord.xy / resolution;
vec3 color = texture(screenTexture, uv).rgb;
if (sharpenFactor > 0.0) color = sharpen(uv, color);
color = clamp(color + brightness, 0.0, 1.0);
color = (color - 0.5) * clamp(contrast + 1.0, 0.5, 2.0) + 0.5;
color = mix(vec3(dot(color, vec3(0.2126, 0.7152, 0.0722))), color, saturation);
color = pow(max(color, 0.0), vec3(1.0 / gamma));
if (deband > 0.0) color += vec3(((ign(gl_FragCoord.xy) + ign(gl_FragCoord.xy + vec2(11.0, 17.0))) - 1.0) * (deband / 255.0));
fragColor = vec4(color, 1.0);
}`,ee=`precision highp float;
#define FXAA_MIN_REDUCE (1.0 / 128.0)
#define FXAA_MUL_REDUCE (1.0 / 8.0)
#define MAX_SPAN 8.0
uniform sampler2D screenTexture;
uniform vec2 resolution;
const vec3 luma = vec3(0.299, 0.587, 0.114);
void main() {
vec2 invResolution = 1.0 / resolution;
vec3 rgbNW = texture2D(screenTexture, (gl_FragCoord.xy + vec2(-1.0, -1.0)) * invResolution).rgb;
vec3 rgbNE = texture2D(screenTexture, (gl_FragCoord.xy + vec2( 1.0, -1.0)) * invResolution).rgb;
vec3 rgbSW = texture2D(screenTexture, (gl_FragCoord.xy + vec2(-1.0, 1.0)) * invResolution).rgb;
vec3 rgbSE = texture2D(screenTexture, (gl_FragCoord.xy + vec2( 1.0, 1.0)) * invResolution).rgb;
vec3 rgbM = texture2D(screenTexture, gl_FragCoord.xy * invResolution).rgb;
float lumaNW = dot(rgbNW, luma);
float lumaNE = dot(rgbNE, luma);
float lumaSW = dot(rgbSW, luma);
float lumaSE = dot(rgbSE, luma);
float lumaM = dot(rgbM, luma);
float lumaMin = min(lumaM, min(min(lumaNW, lumaNE), min(lumaSW, lumaSE)));
float lumaMax = max(lumaM, max(max(lumaNW, lumaNE), max(lumaSW, lumaSE)));
vec2 dir;
dir.x = -((lumaNW + lumaNE) - (lumaSW + lumaSE));
dir.y = ((lumaNW + lumaSW) - (lumaNE + lumaSE));
float dirReduce = max((lumaNW + lumaNE + lumaSW + lumaSE) * 0.25 * FXAA_MUL_REDUCE, FXAA_MIN_REDUCE);
float minDirFactor = 1.0 / (min(abs(dir.x), abs(dir.y)) + dirReduce);
dir = clamp(dir * minDirFactor, vec2(-MAX_SPAN), vec2(MAX_SPAN)) * invResolution;
vec4 rgbA = 0.5 * (
texture2D(screenTexture, gl_FragCoord.xy * invResolution + dir * (1.0 / 3.0 - 0.5)) +
texture2D(screenTexture, gl_FragCoord.xy * invResolution + dir * (2.0 / 3.0 - 0.5)));
vec4 rgbB = rgbA * 0.5 + 0.25 * (
texture2D(screenTexture, gl_FragCoord.xy * invResolution + dir * -0.5) +
texture2D(screenTexture, gl_FragCoord.xy * invResolution + dir * 0.5));
float lumaB = dot(rgbB, vec4(luma, 0.0));
gl_FragColor = lumaB < lumaMin || lumaB > lumaMax ? rgbA : rgbB;
}`,te=`precision highp float;
uniform sampler2D screenTexture;
uniform vec2 resolution;
void main() {
vec2 uv = gl_FragCoord.xy / resolution;
float edgeThreshold = 0.2;
vec2 offset = vec2(1.0) / resolution;
vec3 colorCenter = texture2D(screenTexture, uv).rgb;
vec3 colorLeft = texture2D(screenTexture, uv - vec2(offset.x, 0.0)).rgb;
vec3 colorRight = texture2D(screenTexture, uv + vec2(offset.x, 0.0)).rgb;
vec3 colorUp = texture2D(screenTexture, uv - vec2(0.0, offset.y)).rgb;
vec3 colorDown = texture2D(screenTexture, uv + vec2(0.0, offset.y)).rgb;
float diffHorizontal = length(colorRight - colorLeft);
float diffVertical = length(colorUp - colorDown);
float edgeFactor = step(edgeThreshold, diffHorizontal + diffVertical);
vec3 outlineColor = mix(colorCenter, vec3(0.0), edgeFactor);
gl_FragColor = vec4(outlineColor, 1.0);
}`,ne=`precision highp float;
#define CA_AMOUNT 1.0025
#define SCANLINE_INTENSITY_X 0.125
#define SCANLINE_INTENSITY_Y 0.375
#define SCANLINE_SIZE 1024.0
uniform sampler2D screenTexture;
varying vec2 vUV;
void main() {
vec4 finalColor = texture2D(screenTexture, vUV);
finalColor.rgb = vec3(
texture2D(screenTexture, (vUV - 0.5) * CA_AMOUNT + 0.5).r,
finalColor.g,
texture2D(screenTexture, (vUV - 0.5) / CA_AMOUNT + 0.5).b
);
float scanlineX = abs(sin(vUV.x * SCANLINE_SIZE) * 0.5 * SCANLINE_INTENSITY_X);
float scanlineY = abs(sin(vUV.y * SCANLINE_SIZE) * 0.5 * SCANLINE_INTENSITY_Y);
gl_FragColor = vec4(mix(finalColor.rgb, vec3(0.0), scanlineX + scanlineY), finalColor.a);
}`,ie=`precision highp float;
#define PI 3.14159265
#define SCANLINE_INTENSITY 0.35
#define CHROMA_OFFSET 0.005
#define BLUR_RADIUS 0.002
#define WARP_AMOUNT 0.01
#define SCANLINE_DARKEN 0.5
uniform sampler2D screenTexture;
uniform int FrameCount;
uniform vec2 TextureSize;
uniform vec2 resolution;
varying vec2 vUV;
const mat3 yiq_mat = mat3(
0.299, 0.587, 0.114,
0.596, -0.275, -0.321,
0.212, -0.523, 0.311
);
const mat3 yiq2rgb_mat = mat3(
1.0, 0.956, 0.621,
1.0, -0.272, -0.647,
1.0, -1.106, 1.705
);
vec3 applyNTSC(vec2 uv) {
vec3 col = texture2D(screenTexture, uv).rgb;
vec3 yiq = col * yiq_mat;
float chromaPhase = PI * (mod(uv.y * TextureSize.y, 2.0) + float(FrameCount));
yiq.y *= cos(chromaPhase * 0.5);
yiq.z *= sin(chromaPhase * 0.5);
vec3 rgb = yiq * yiq2rgb_mat;
vec3 finalColor;
finalColor.r = texture2D(screenTexture, uv + vec2(CHROMA_OFFSET, 0.0)).r;
finalColor.g = texture2D(screenTexture, uv + vec2(0.0, BLUR_RADIUS)).g;
finalColor.b = texture2D(screenTexture, uv - vec2(CHROMA_OFFSET, 0.0)).b;
return finalColor;
}
vec3 applyScanlines(vec2 uv) {
vec3 col = texture2D(screenTexture, uv).rgb;
float scanline = abs(sin(uv.y * resolution.y * 2.0)) * SCANLINE_INTENSITY;
col *= 1.0 - (scanline * SCANLINE_DARKEN);
return col;
}
vec2 applyWarp(vec2 uv) {
uv = uv * 2.0 - 1.0;
float r = sqrt(uv.x * uv.x + uv.y * uv.y);
uv += uv * (r * r) * WARP_AMOUNT;
return uv * 0.5 + 0.5;
}
void main() {
vec2 warpedUV = applyWarp(vUV);
vec3 ntscColor = applyNTSC(warpedUV);
vec3 scanlineColor = applyScanlines(warpedUV);
vec3 finalColor = mix(ntscColor, scanlineColor, 0.7);
gl_FragColor = vec4(finalColor, 1.0);
}`,H=class{constructor(e,t){this.video=e;this.effects=t;this.canvas=document.createElement("canvas"),Object.assign(this.canvas.style,{position:"absolute",inset:"0",width:"100%",height:"100%",objectFit:"contain",zIndex:"1"}),e.parentElement?.appendChild(this.canvas);let n=this.canvas.getContext("webgl2",{alpha:!1,antialias:!1,depth:!1,desynchronized:!0,powerPreference:"high-performance"});if(n===null)throw new Error("WebGL2 unavailable");this.gl=n;let i=n.createBuffer();n.bindBuffer(n.ARRAY_BUFFER,i),n.bufferData(n.ARRAY_BUFFER,new Float32Array([-1,-1,3,-1,-1,3]),n.STATIC_DRAW),this.passes.set("base",this.compile(J,Z,["screenTexture","resolution","filterId","sharpenFactor","brightness","contrast","gamma","saturation","deband"])),this.videoTexture=this.texture(),this.schedule()}canvas;gl;passes=new Map;videoTexture;targets=[];width=0;height=0;frame=0;lastDraw=0;stopped=!1;idle=!1;pending=!1;update(e){this.effects=e}pause(){this.idle||this.stopped||(this.idle=!0,this.canvas.style.display="none")}resume(e){this.effects=e,!(!this.idle||this.stopped)&&(this.idle=!1,this.canvas.style.display="",this.schedule())}schedule(){this.pending||(this.pending=!0,this.video.requestVideoFrameCallback(this.onFrame))}compile(e,t,n){let i=this.gl,a=(o,d)=>{let u=i.createShader(o);if(i.shaderSource(u,d),i.compileShader(u),!i.getShaderParameter(u,i.COMPILE_STATUS))throw new Error(`shader: ${i.getShaderInfoLog(u)}`);return u},s=i.createProgram();if(i.attachShader(s,a(i.VERTEX_SHADER,e)),i.attachShader(s,a(i.FRAGMENT_SHADER,t)),i.bindAttribLocation(s,0,"position"),i.linkProgram(s),!i.getProgramParameter(s,i.LINK_STATUS))throw new Error(`program: ${i.getProgramInfoLog(s)}`);return{program:s,uniforms:new Map(n.map(o=>[o,i.getUniformLocation(s,o)]))}}extra(e){let t=this.passes.get(e);if(t===void 0){let n={fxaa:ee,toon:te,crt:ne,ntsc:ie}[e];t=this.compile(j,n,["screenTexture","resolution","TextureSize","FrameCount"]),this.passes.set(e,t)}return t}texture(){let e=this.gl,t=e.createTexture();return e.bindTexture(e.TEXTURE_2D,t),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE),t}resize(e,t){let n=this.gl;this.width=e,this.height=t,this.canvas.width=e,this.canvas.height=t;for(let i of this.targets)n.deleteFramebuffer(i.framebuffer),n.deleteTexture(i.texture);this.targets=[0,1].map(()=>{let i=this.texture();n.texImage2D(n.TEXTURE_2D,0,n.RGBA,e,t,0,n.RGBA,n.UNSIGNED_BYTE,null);let a=n.createFramebuffer();return n.bindFramebuffer(n.FRAMEBUFFER,a),n.framebufferTexture2D(n.FRAMEBUFFER,n.COLOR_ATTACHMENT0,n.TEXTURE_2D,i,0),{framebuffer:a,texture:i}}),n.bindFramebuffer(n.FRAMEBUFFER,null)}onFrame=e=>{if(this.pending=!1,this.stopped||this.idle)return;this.schedule();let t=this.effects.maxFps;if(!(t>0&&e-this.lastDraw<1e3/t-2)){this.lastDraw=e;try{this.draw()}catch(n){window.DDCloud?.log(`effects: ${String(n).slice(0,200)}`),this.destroy()}}};draw(){let e=this.gl,t=this.effects,n=this.video.videoWidth,i=this.video.videoHeight;if(n===0||i===0)return;(n!==this.width||i!==this.height)&&this.resize(n,i),this.frame++,e.activeTexture(e.TEXTURE0),e.bindTexture(e.TEXTURE_2D,this.videoTexture),e.pixelStorei(e.UNPACK_FLIP_Y_WEBGL,!0),e.texImage2D(e.TEXTURE_2D,0,e.RGBA,e.RGBA,e.UNSIGNED_BYTE,this.video);let a=["fxaa","toon","crt","ntsc"].filter(d=>t[d]);e.viewport(0,0,n,i),e.enableVertexAttribArray(0),e.vertexAttribPointer(0,2,e.FLOAT,!1,0,0);let s=this.passes.get("base");e.bindFramebuffer(e.FRAMEBUFFER,a.length>0?this.targets[0].framebuffer:null),e.useProgram(s.program);let o=s.uniforms;e.uniform1i(o.get("screenTexture"),0),e.uniform2f(o.get("resolution"),n,i),e.uniform1i(o.get("filterId"),t.sharpenMode==="usm"?1:2),e.uniform1f(o.get("sharpenFactor"),t.sharpen),e.uniform1f(o.get("brightness"),t.brightness/100),e.uniform1f(o.get("contrast"),t.contrast/100),e.uniform1f(o.get("gamma"),Math.min(5,Math.max(.1,t.gamma))),e.uniform1f(o.get("saturation"),t.saturation/100),e.uniform1f(o.get("deband"),t.deband),e.drawArrays(e.TRIANGLES,0,3),a.forEach((d,u)=>{let h=this.extra(d),g=this.targets[u%2],p=u===a.length-1;e.bindFramebuffer(e.FRAMEBUFFER,p?null:this.targets[(u+1)%2].framebuffer),e.useProgram(h.program),e.bindTexture(e.TEXTURE_2D,g.texture);let c=h.uniforms;e.uniform1i(c.get("screenTexture")??null,0),e.uniform2f(c.get("resolution")??null,n,i),e.uniform2f(c.get("TextureSize")??null,n,i),e.uniform1i(c.get("FrameCount")??null,this.frame),e.drawArrays(e.TRIANGLES,0,3)})}destroy(){this.stopped=!0,this.canvas.remove(),this.gl.getExtension("WEBGL_lose_context")?.loseContext()}};var V=class{constructor(e){this.pc=e;this.element=document.createElement("div"),Object.assign(this.element.style,{position:"absolute",top:"8px",right:"8px",zIndex:"3",padding:"4px 8px",borderRadius:"6px",background:"rgba(0,0,0,0.55)",color:"#e8e8e8",font:"12px monospace",lineHeight:"16px",pointerEvent Line truncated
`:" \xB7 ")}destroy(){this.show(0,this.vertical,this.server,this.left),this.element.remove()}};function z(r,e){let t=e==="high"?"4d":e==="low"?"420":"42e",n=[];for(let d of r.matchAll(/a=fmtp:(\d+).*profile-level-id=([0-9a-f]{6})/g))d[2].startsWith(t)&&n.push(d[1]);if(n.length===0)return r;let i=r.split(`\r
`),a=i.findIndex(d=>d.startsWith("m=video"));if(a<0)return r;let s=i[a].trim().split(" "),o=s.slice(3).filter(d=>!n.includes(d));return i[a]=s.slice(0,3).concat(n,o).join(" "),i.join(`\r
`)}function K(r,e){let t=r.split(`\r
`),n=t.findIndex(a=>a.startsWith("m=video"));if(n<0)return r;let i=`b=AS:${e}`;for(let a=n+1;a<=t.length;a++){let s=t[a];if(s===void 0||s.startsWith("m=")){t.splice(a,0,i);break}if(!(s.startsWith("i=")||s.startsWith("c="))){if(s.startsWith("b=AS:")){t[a]=i;break}t.splice(a,0,i);break}}return t.join(`\r
`)}var m=JSON.parse(DDCloud.settings()),ae=RTCPeerConnection.prototype.setLocalDescription;RTCPeerConnection.prototype.setLocalDescription=function(r){return r?.sdp&&((m.codec==="high"||m.codec==="normal"||m.codec==="low")&&(r.sdp=z(r.sdp,m.codec)),m.bitrateMbps>0&&(r.sdp=K(r.sdp,m.bitrateMbps*1e3))),ae.call(this,r)};window.__ddRumble=m.rumble/100;window.addEventListener("error",r=>DDCloud.log(`error: ${r.message} @${r.lineno}`));window.addEventListener("unhandledrejection",r=>DDCloud.log(`rejection: ${String(r.reason).slice(0,200)}`));window.addEventListener("gamepadconnected",r=>DDCloud.log(`controller: ${r.gamepad.id.slice(0,60)} mapping=${r.gamepad.mapping||"none"} buttons=${r.gamepad.buttons.length} axes=${r.gamepad.axes.length}`));var W=new Map,re=1;window.__ddResolve=(r,e,t)=>{let n=W.get(r);W.delete(r),n&&n({status:e,body:t})};function C(r,e=""){return new Promise(t=>{let n=re++;W.set(n,t),DDCloud.call(n,r,typeof e=="string"?e:JSON.stringify(e))})}var B=r=>new Promise(e=>setTimeout(e,r)),se=r=>r.status>=200&&r.status<300;function x(r){let e=document.getElementById("status");e&&(e.textContent=r),DDCloud.status(r)}var T=class extends Error{constructor(t,n){super(t);this.stage=t;this.detail=n}};function k(r,e){if(!se(e))throw new T(r,`${e.status} ${e.body.slice(0,300)}`);return e}function oe(r,e){return new Promise(t=>{if(r.iceGatheringState==="complete")return t();let n=setTimeout(t,e);r.addEventListener("icegatheringstatechange",()=>{r.iceGatheringState==="complete"&&(clearTimeout(n),t())})})}async function de(){x("Starting the game on Xbox Cloud Gaming\u2026"),k("start",await C("start"));let r=!1,e=0;for(;;){let l=k("state",await C("state")),f=JSON.parse(l.body||"{}");if(f.state==="Provisioned")break;if(f.state==="Failed")throw new T("provision",JSON.stringify(f.errorDetails??{}).slice(0,300));f.state==="ReadyToConnect"&&!r?(r=!0,x("Signing in to the cloud console\u2026"),k("connect",await C("connect"))):f.state==="WaitingForResources"?x(`In the queue for a cloud console\u2026 ${Math.round(e/1e3)} s`):x(`Getting a cloud console ready\u2026 ${Math.round(e/1e3)} s`),await B(1e3),e+=1e3}x("Connecting\u2026");let t=new U("player");t.init();let n=t._peerConnection,i=t._channels.input,a=i.queueMetadataFrame.bind(i);i.queueMetadataFrame=l=>{if(m.preventDrops){let f=l.frameRenderedTimeMs-l.frameDecodedTimeMs;l.frameDecodedTimeMs=l.frameSubmittedTimeMs+10,l.frameRenderedTimeMs=l.frameDecodedTimeMs+f}a(l)};let s,o,d=()=>{if(!(o===void 0||s===void 0&&m.volume===100)){if(s===void 0){let l=new AudioContext;s=l.createGain(),l.createMediaStreamSource(o).connect(s).connect(l.destination);let f=document.querySelector("#player audio");f&&(f.muted=!0)}s.gain.value=m.volume/100}};n.addEventListener("track",l=>{l.track.kind==="audio"&&(o=l.streams[0],d())});let u=new V(n),h,g=()=>{let l=t.getVideoElement();if(l!==void 0){if(!X(m)){h?.pause();return}if(h===void 0)try{h=new H(l,m)}catch(f){DDCloud.log(`effects unavailable: ${String(f).slice(0,200)}`)}else h.resume(m)}};window.__ddApply=l=>{m=JSON.parse(l),window.__ddRumble=m.rumble/100,u.show(m.statsLevel,m.statsLayout==="vertical",m.server,m.statsCorner==="left"),d(),g()},n.addEventListener("connectionstatechange",()=>{n.connectionState==="connected"?(x(""),DDCloud.playing(),u.show(m.statsLevel,m.statsLayout==="vertical",m.server,m.statsCorner==="left"),setTimeout(g,500)):n.connectionState==="failed"?DDCloud.failed("webrtc","connection failed"):n.connectionState==="disconnected"&&x("Connection lost\u2026")});let p=await t.createOffer(),c=k("sdp",await C("sdp",p.sdp??"")),O=JSON.parse(JSON.parse(c.body).exchangeResponse);if(O.status&&O.status!=="success")throw new T("sdp-answer",JSON.stringify(O).slice(0,300));await t.setRemoteSDP(O.sdp),await oe(n,3e3);let Y=(await t.getICECandidates()).map(l=>JSON.stringify({candidate:l.candidate,sdpMid:l.sdpMid,sdpMLineIndex:l.sdpMLineIndex,usernameFragment:l.usernameFragment})),$=k("ice",await C("ice",Y));t.setRemoteIceCandidates(JSON.parse(JSON.parse($.body).exchangeResponse));let q=Date.now();for(;!t._channels.message._handshakeCompleted&&Date.now()-q<2e4;)await B(100);DDCloud.log(`handshake ${t._channels.message._handshakeCompleted?"done":"missing"} after ${Date.now()-q} ms`),await B(300);let b=new v(0,{enable_keyboard:!1});b.attach(t),DDCloud.log(`controllers at attach: ${Array.from(navigator.getGamepads()).filter(Boolean).length}`),window.__ddNexus=()=>{b.sendButtonState("Nexus",1),setTimeout(()=>b.sendButtonState("Nexus",0),200)},window.__ddButton=(l,f)=>b.sendButtonState(l,f),window.__ddInput=l=>{l?b.onWindowFocus():b.onWindowBlur()},setInterval(()=>{C("keepalive")},3e4),window.__ddStop=()=>{try{h?.destroy(),u.destroy(),b.detach(),t.destroy()}catch{}}}de().catch(r=>{r instanceof T?DDCloud.failed(r.stage,r.detail):DDCloud.failed("page",String(r).slice(0,300))});})();
+2 -2
View File
@@ -8,7 +8,6 @@ set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-z,max-page-size=16384
add_subdirectory(adrenotools)
# DXVK's DXBC->SPIR-V translator is zlib licensed (vendored under thirdparty/dxbc).
add_subdirectory(thirdparty/dxbc)
set(DDFG_UPSTREAM "v0.3.0")
@@ -37,13 +36,14 @@ target_link_libraries(sdnative log)
add_library(deviceinfo SHARED deviceinfo/vkinfo.c)
target_link_libraries(deviceinfo dl)
add_library(gpuclock SHARED gpuclock/kgsl_clock.c gpuclock/kgsl_busy.c)
add_library(termux SHARED termux/termux.c)
add_library(ddvenc SHARED venc/venc_server.cpp)
target_compile_features(ddvenc PRIVATE cxx_std_17)
target_link_libraries(ddvenc mediandk log)
# The optional proprietary SDK is supplied locally; its headers/libraries are not vendored.
if(DROIDDECK_VITURE_SDK_DIR)
add_library(droiddeck_viture SHARED viture/sdk_bridge.cpp)
target_compile_features(droiddeck_viture PRIVATE cxx_std_17)
+2
View File
@@ -62,6 +62,7 @@ Java_com_droiddeck_launcher_gpu_VulkanInfo_nativeQuery(JNIEnv *env, jclass clazz
LINE("driver_version=%u.%u.%u\n", p.driverVersion >> 22, (p.driverVersion >> 12) & 0x3ff, p.driverVersion & 0xfff);
LINE("vendor_id=0x%04x\ndevice_id=0x%08x\n", p.vendorID, p.deviceID);
LINE("max_image_2d=%u\n", p.limits.maxImageDimension2D);
LINE("max_push_constants=%u\n", p.limits.maxPushConstantsSize);
uint32_t extCount = 0;
if (vkEnumerateDeviceExtensionProperties) vkEnumerateDeviceExtensionProperties(device, NULL, &extCount, NULL);
@@ -91,6 +92,7 @@ Java_com_droiddeck_launcher_gpu_VulkanInfo_nativeQuery(JNIEnv *env, jclass clazz
vkGetPhysicalDeviceFeatures(device, &f);
LINE("geometry_shader=%s\ntessellation_shader=%s\ntexture_bc=%s\n",
f.geometryShader ? "yes" : "no", f.tessellationShader ? "yes" : "no", f.textureCompressionBC ? "yes" : "no");
LINE("shader_int64=%s\n", f.shaderInt64 ? "yes" : "no");
}
if (vkGetPhysicalDeviceMemoryProperties) {
VkPhysicalDeviceMemoryProperties m;
+133 -21
View File
@@ -78,7 +78,6 @@ static constexpr uint8_t GAMEPAD_AXIS_COUNT = 8;
static constexpr uint8_t GAMEPAD_BUTTON_COUNT = 11;
static constexpr uint32_t FAKE_INPUT_RING_MAGIC = 0x46494252;
static constexpr uint32_t FAKE_INPUT_RING_VERSION = 2;
// Rings hold the 64-bit input_event layout that the arm64 writers use; 32-bit guests convert on read.
struct RingEvent {
int64_t sec;
int64_t usec;
@@ -93,6 +92,7 @@ static constexpr unsigned int FAKE_INPUT_MAJOR = 13;
static constexpr unsigned int FAKE_INPUT_EVENT_MINOR_BASE = 64;
static constexpr unsigned int FAKE_INPUT_JS_MINOR_BASE = 0;
static constexpr int UINPUT_EVENT_BASE = 16;
static constexpr int MOUSE_EVENT_SLOT = 4;
static constexpr int UINPUT_EVENT_LIMIT = 64;
static constexpr uint32_t UINPUT_DESCRIPTION_MAGIC = 0x46495544;
@@ -121,6 +121,12 @@ static constexpr size_t FAKE_INPUT_RING_HEADER_SIZE =
static constexpr size_t FAKE_INPUT_RING_SIZE =
FAKE_INPUT_RING_HEADER_SIZE +
(FAKE_INPUT_RING_CAPACITY * FAKE_INPUT_EVENT_SIZE);
static constexpr size_t DECK_IMU_BLOCK_SIZE = 64;
static constexpr size_t FAKE_INPUT_RING_PAD_TYPE_OFFSET = 44;
static constexpr uint32_t PAD_TYPE_DECK = 0, PAD_TYPE_STEAM = 1, PAD_TYPE_XBOX = 2, PAD_TYPE_DS4 = 3,
PAD_TYPE_DUALSENSE = 4, PAD_TYPE_LAST = PAD_TYPE_DUALSENSE,
PAD_TYPE_MOUSE = 5;
static constexpr uint32_t PAD_TYPE_SET = 0x80000000u;
struct UinputDescription {
uint32_t magic;
@@ -149,6 +155,7 @@ struct FakeController {
size_t mapping_size = 0;
bool closed = false;
bool needs_keyframe = true;
bool mouse = false;
~FakeController() {
if (ring) munmap(ring, mapping_size);
@@ -383,7 +390,6 @@ send_vibration(int strong, int weak, uint16_t duration_ms, uint16_t slot) {
syscall(SYS_close, sock);
}
// Must match RumbleComponent.HOLD_MS: play until the slot's next effect or stop.
static constexpr uint16_t RUMBLE_HOLD_MS = 0xffff;
__attribute__((visibility("hidden"))) static void send_ff_effect(const struct ff_effect &effect, uint16_t slot) {
@@ -607,7 +613,7 @@ open_fake_input_ring(const char *event, int flags) {
int slot = get_event_number(event);
bool virtual_pad = slot >= UINPUT_EVENT_BASE;
std::string ring_path = virtual_pad ? uinput_ring_path(slot) : get_ring_path_for_slot(slot);
size_t mapping_size = virtual_pad ? UINPUT_FILE_SIZE : FAKE_INPUT_RING_SIZE;
size_t mapping_size = virtual_pad ? UINPUT_FILE_SIZE : FAKE_INPUT_RING_SIZE + DECK_IMU_BLOCK_SIZE;
if (ring_path.empty()) {
Logger::log("No input ring is configured for %s (slot %d)\n", event, slot);
errno = ENODEV;
@@ -651,9 +657,22 @@ open_fake_input_ring(const char *event, int flags) {
controller->uinput = description;
controller->mapping_size = mapping_size;
controller->generation = ring_generation(ring);
if (!virtual_pad) {
uint32_t pad_type = __atomic_load_n(
reinterpret_cast<volatile uint32_t *>(static_cast<uint8_t *>(mapping) + FAKE_INPUT_RING_SIZE +
FAKE_INPUT_RING_PAD_TYPE_OFFSET),
__ATOMIC_ACQUIRE);
controller->mouse = (pad_type & PAD_TYPE_SET) && (pad_type & ~PAD_TYPE_SET) == PAD_TYPE_MOUSE;
}
SnapshotState snap;
if (read_snapshot(ring, snap) && snap.generation == controller->generation)
capture_keyframe(*controller, snap);
if (read_snapshot(ring, snap) && snap.generation == controller->generation) {
if (controller->mouse) {
controller->read_seq = snap.write_seq;
controller->needs_keyframe = false;
} else {
capture_keyframe(*controller, snap);
}
}
{
std::lock_guard<std::recursive_mutex> guard(controller_mutex());
controller_map()[fd] = controller;
@@ -1156,7 +1175,8 @@ struct DeckImu {
uint32_t pad_type;
uint32_t latency;
};
static_assert(offsetof(DeckImu, pad_type) == 44, "DeckImu::pad_type matches FakeInputWriter");
static_assert(offsetof(DeckImu, pad_type) == FAKE_INPUT_RING_PAD_TYPE_OFFSET,
"DeckImu::pad_type matches FakeInputWriter and the ring reader");
static_assert(offsetof(DeckImu, latency) == 48, "DeckImu::latency matches FakeInputWriter");
static constexpr uint32_t LATENCY_CONTROLLER = 1, LATENCY_TOUCH = 2;
static constexpr int LOW_LATENCY_REPORT_INTERVAL_US = 1000;
@@ -1173,7 +1193,6 @@ struct DeckExtras {
uint32_t controls = 0;
};
static constexpr uint32_t DECK_IMU_MAGIC = 0x31554D49;
static constexpr size_t DECK_IMU_BLOCK_SIZE = 64;
static_assert(sizeof(DeckImu) <= DECK_IMU_BLOCK_SIZE, "the IMU block is 64 bytes in the ring file");
struct TritonSharedData {
@@ -1222,7 +1241,6 @@ static constexpr int TRITON_REPORT_ID_BYTES = 1;
static constexpr int TRITON_STATE_BYTES = 45;
static constexpr int TRITON_REPORT_INTERVAL_US = 4000;
static constexpr int TRITON_FEATURE_TIMEOUT_MS = 500;
// The app clears output_length once it has taken a report; a writer waits this long for that before overwriting.
static constexpr int TRITON_OUTPUT_TIMEOUT_MS = 50;
static constexpr const char *TRITON_NAME = "Steam Controller";
static constexpr const char *TRITON_SERIAL = "DROIDDECKSC01";
@@ -1270,10 +1288,6 @@ static bool fake_deck_enabled() {
return enabled == 1;
}
static constexpr uint32_t PAD_TYPE_DECK = 0, PAD_TYPE_STEAM = 1, PAD_TYPE_XBOX = 2, PAD_TYPE_DS4 = 3,
PAD_TYPE_DUALSENSE = 4, PAD_TYPE_LAST = PAD_TYPE_DUALSENSE;
static constexpr uint32_t PAD_TYPE_SET = 0x80000000u;
__attribute__((visibility("hidden"))) static uint32_t session_pad_type() {
static int type = -1;
if (type < 0) {
@@ -1408,6 +1422,12 @@ __attribute__((visibility("hidden"))) static bool is_withdrawn_pad_path(const ch
if (strncmp(event, "event", 5) && strncmp(event, "js", 2)) return false;
int number = get_event_number(event);
if (number < 0 || number >= UINPUT_EVENT_BASE) return false;
if (number == MOUSE_EVENT_SLOT && !strncmp(event, "event", 5)) {
char *fake = from_real_to_fake_path(pathname);
bool exposed = fake && path_exists(fake);
free(fake);
return !exposed;
}
return number == 0 || !process_is_steam_client();
}
@@ -2650,9 +2670,6 @@ __attribute__((visibility("hidden"))) static int triton_get_feature(DeckHidraw &
static constexpr uint32_t TRITON_HWID = 72;
static constexpr uint32_t TRITON_FW_TIMESTAMP = 0x6AC686B3;
// Steam compares the controller's firmware with TRITON_FW_TS in its bundled hardwareupdater.cfg (and blocks
// with a required update below MUST_UPDATE_TRITON_FW_TS), so the emulated pad reports at least the build
// Steam ships.
static uint32_t triton_fw_timestamp() {
static const uint32_t timestamp = [] {
std::vector<std::string> roots;
@@ -2936,15 +2953,19 @@ EXPORT DIR *opendir(const char *name) {
return dir;
}
static bool open_has_mode(int flags) {
return (flags & O_CREAT) != 0 || (flags & O_TMPFILE) == O_TMPFILE;
}
EXPORT int open(const char *pathname, int flags, ...) {
va_list va;
mode_t mode;
mode_t mode = 0;
int fd;
bool hasMode;
va_start(va, flags);
hasMode = flags & O_CREAT;
hasMode = open_has_mode(flags);
if (hasMode) {
mode = va_arg(va, mode_t);
@@ -3016,13 +3037,13 @@ EXPORT int open(const char *pathname, int flags, ...) {
EXPORT int openat(int dirfd, const char *pathname, int flags, ...) {
va_list va;
mode_t mode;
mode_t mode = 0;
int fd;
bool hasMode;
va_start(va, flags);
hasMode = flags & O_CREAT;
hasMode = open_has_mode(flags);
if (hasMode) {
mode = va_arg(va, mode_t);
@@ -3351,6 +3372,85 @@ static int copy_ioctl_bits(int op, void *destination, const unsigned char (&bits
return static_cast<int>(size);
}
static constexpr uint16_t MOUSE_VENDOR_ID = 0x1dd1;
static constexpr uint16_t MOUSE_PRODUCT_ID = 0x0004;
static int ioctl_mouse(int op, void *argp) {
int type = (op >> 8 & 0xFF);
int number = (op >> 0 & 0xFF);
if (type != 0x45) {
errno = EINVAL;
return -1;
}
switch (number) {
case 0x01: {
int version = 65536;
memcpy(argp, &version, sizeof(int));
return 0;
}
case 0x02: {
struct input_id id;
memset(&id, 0, sizeof(id));
id.bustype = 0x03;
id.vendor = MOUSE_VENDOR_ID;
id.product = MOUSE_PRODUCT_ID;
id.version = 1;
memcpy(argp, &id, sizeof(id));
return 0;
}
case 0x06:
if (argp && _IOC_SIZE(op)) snprintf(static_cast<char *>(argp), _IOC_SIZE(op), "DroidDeck Mouse");
return 0;
case 0x07:
if (argp && _IOC_SIZE(op)) snprintf(static_cast<char *>(argp), _IOC_SIZE(op), "usb-droiddeck/mouse");
return 0;
case 0x08:
if (argp && _IOC_SIZE(op)) static_cast<char *>(argp)[0] = '\0';
return 0;
case 0x09: {
unsigned char bitmask[(INPUT_PROP_MAX + 8) / 8] = {};
bitmask[INPUT_PROP_POINTER / 8] |= 1 << (INPUT_PROP_POINTER % 8);
return copy_ioctl_bits(op, argp, bitmask);
}
case 0x18: {
unsigned char bitmask[(KEY_MAX + 8) / 8] = {};
return copy_ioctl_bits(op, argp, bitmask);
}
case 0x20: {
unsigned char bitmask[(EV_MAX + 8) / 8] = {};
bitmask[EV_SYN / 8] |= 1 << (EV_SYN % 8);
bitmask[EV_KEY / 8] |= 1 << (EV_KEY % 8);
bitmask[EV_REL / 8] |= 1 << (EV_REL % 8);
bitmask[EV_MSC / 8] |= 1 << (EV_MSC % 8);
return copy_ioctl_bits(op, argp, bitmask);
}
case 0x21: {
unsigned char bitmask[(KEY_MAX + 8) / 8] = {};
for (int button = BTN_LEFT; button <= BTN_TASK; button++)
if (button <= BTN_MIDDLE || button == BTN_SIDE || button == BTN_EXTRA)
bitmask[button / 8] |= 1 << (button % 8);
return copy_ioctl_bits(op, argp, bitmask);
}
case 0x22: {
unsigned char bitmask[(REL_MAX + 8) / 8] = {};
for (uint16_t code : {REL_X, REL_Y, REL_WHEEL, REL_HWHEEL, REL_WHEEL_HI_RES, REL_HWHEEL_HI_RES})
bitmask[code / 8] |= 1 << (code % 8);
return copy_ioctl_bits(op, argp, bitmask);
}
case 0x23: {
unsigned char bitmask[(ABS_MAX + 8) / 8] = {};
return copy_ioctl_bits(op, argp, bitmask);
}
case 0x35: {
unsigned char bitmask[(FF_MAX + 8) / 8] = {};
return copy_ioctl_bits(op, argp, bitmask);
}
default:
errno = EINVAL;
return -1;
}
}
static bool wait_snapshot(std::shared_ptr<FakeController> fake,
std::unique_lock<std::recursive_mutex> &guard,
SnapshotState &snap) {
@@ -3417,6 +3517,13 @@ EXPORT int ioctl(int fd, ioctl_request_t op, ...) {
int event_number = controller->second->slot;
const UinputDescription *made = controller->second->uinput;
if (controller->second->mouse) {
int result = ioctl_mouse(op, argp);
if (result >= 0) return result;
guard.unlock();
return forward_ioctl(fd, op, argp);
}
if (type == 0x45 && number == 0x1) {
Logger::log("Hooking ioctl EVIOCGVERSION for event %s\n", event);
int version = 65536;
@@ -3570,7 +3677,6 @@ EXPORT int ioctl(int fd, ioctl_request_t op, ...) {
Logger::log("Hooking ioctl EVIOCGRAB for event %s\n", event);
return 0;
} else if (type == 0x6A && !strncmp(event, "event", 5)) {
// Older SDL2 builds treat any JSIOCGNAME answer as a joydev node and skip EVIOCGID, then drop the pad as 0000:0000.
Logger::log("Rejecting joydev ioctl %d for event %s\n", number, event);
guard.unlock();
errno = EINVAL;
@@ -3680,6 +3786,12 @@ static ssize_t read_input(int fd, void *buf, size_t count) {
if (fake.needs_keyframe || snap.resync_seq != fake.resync_seq ||
snap.write_seq < fake.read_seq ||
snap.write_seq - fake.read_seq > FAKE_INPUT_RING_CAPACITY) {
if (fake.mouse) {
fake.read_seq = snap.write_seq;
fake.resync_seq = snap.resync_seq;
fake.needs_keyframe = false;
continue;
}
capture_keyframe(fake, snap);
} else {
size_t events = std::min<uint64_t>(requested, snap.write_seq - fake.read_seq);
@@ -4143,7 +4255,7 @@ EXPORT int select(int nfds, fd_set *readfds, fd_set *writefds,
#ifdef __GLIBC__
static mode_t open_mode(int flags, va_list va) {
return (flags & (O_CREAT | O_TMPFILE)) ? va_arg(va, mode_t) : 0;
return open_has_mode(flags) ? va_arg(va, mode_t) : 0;
}
EXPORT int open64(const char *pathname, int flags, ...) {
+2 -1
View File
@@ -184,8 +184,9 @@ bool FrameGen::onResize(VkExtent2D extent, VkFormat , bool force) {
VkExtent2D e = levelExtent(extent, l);
if (!makeImage(pyrA_[l], e, VK_FORMAT_R32_SFLOAT, lumaUsage)) return false;
if (!makeImage(pyrB_[l], e, VK_FORMAT_R32_SFLOAT, lumaUsage)) return false;
if (!makeImage(flowLvl_[l], e, VK_FORMAT_R16G16B16A16_SFLOAT, flowUsage)) return false;
}
for (int l = flowFinest_; l < kLevels; ++l)
if (!makeImage(flowLvl_[l], levelExtent(extent, l), VK_FORMAT_R16G16B16A16_SFLOAT, flowUsage)) return false;
pyrAs_.resize(kLevels);
for (int l = flowFinest_; l < kLevels; ++l)
if (!makeImage(pyrAs_[l], levelExtent(extent, l), VK_FORMAT_R32_SFLOAT, lumaUsage)) return false;
@@ -267,8 +267,6 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur
pFlowReg_, flowRegA_, flowRegB_, gmSsbo_, kLevels, cfg_);
}
// Every generation of one real frame is recorded into the same command buffer, so the flow
// between prev and curr is computed once and each generation gets its own synth UBO and set.
if (reuseFlow && !g_scratch.flowReady) return;
if (!reuseFlow) {
bool gmEngaged = updateGlobalMotion(gmSlot, extent_);
@@ -439,7 +437,7 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
makeUbo(dd, dev, mp, sizeof(FlowRegUBO), S.flowRegUbo, S.flowRegUboMem, S.flowRegUboPtr);
if (S.flowRegUboPtr) { FlowRegUBO fu{cfg.frDt, cfg.frLambda, cfg.frEdge, cfg.frEps}; std::memcpy(S.flowRegUboPtr, &fu, sizeof(fu)); }
S.flowUbo.resize(kLevels); S.flowUboMem.resize(kLevels); S.flowUboPtr.resize(kLevels);
for (int l = 0; l < kLevels; ++l) {
for (int l = fg->flowFinest(); l < kLevels; ++l) {
makeUbo(dd, dev, mp, sizeof(FlowUBO), S.flowUbo[l], S.flowUboMem[l], S.flowUboPtr[l]);
if (S.flowUboPtr[l]) { FlowUBO u{cfg.flowScale, (uint32_t)l, 0.6f, 1.6f}; std::memcpy(S.flowUboPtr[l], &u, sizeof(u)); }
}
@@ -464,13 +462,12 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
}
S.sFlow.resize(kLevels);
for (int l = 0; l < kLevels; ++l) {
for (int l = fg->flowFinest(); l < kLevels; ++l) {
S.sFlow[l] = allocSet(dd, dev, pool, pFlow.setLayout);
int pred = (l + 1 < kLevels) ? l + 1 : l;
VkImageView currLvl = (l >= fg->flowFinest()) ? pyrAs[l].view : pyrA[l].view;
wUbo(dd, dev, S.sFlow[l], 0, S.flowUbo[l], sizeof(FlowUBO));
wImg(dd, dev, S.sFlow[l], 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrB[l].view, samp, GEN);
wImg(dd, dev, S.sFlow[l], 33, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, currLvl, samp, GEN);
wImg(dd, dev, S.sFlow[l], 33, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrAs[l].view, samp, GEN);
wImg(dd, dev, S.sFlow[l], 34, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, flowLvl[pred].view, samp, GEN);
wImg(dd, dev, S.sFlow[l], 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, flowLvl[l].view, VK_NULL_HANDLE, GEN);
}
+1 -2
View File
@@ -1,5 +1,4 @@
// LSFG from Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk (PancakeTAS).
// Ported to WinNative and DroidDeck by @maxjivi05. GPL-3.0-or-later.
// LSFG from Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk (PancakeTAS). Ported to WinNative and DroidDeck by @maxjivi05. GPL-3.0-or-later.
#include "lsfg_dll.h"
#include "lsfg_dxbc.h"
+1 -2
View File
@@ -1,5 +1,4 @@
// LSFG from Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk (PancakeTAS).
// Ported to WinNative and DroidDeck by @maxjivi05. GPL-3.0-or-later.
// LSFG from Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk (PancakeTAS). Ported to WinNative and DroidDeck by @maxjivi05. GPL-3.0-or-later.
#pragma once
@@ -1,5 +1,4 @@
#pragma once
// DXVK's dxbc subset is zlib licensed; vendored under cpp/thirdparty/dxbc.
#include <cstdint>
#include <vector>
+154
View File
@@ -0,0 +1,154 @@
// Whole-GPU busy share without root on Adreno (KGSL) devices whose kgsl sysfs is blocked for apps: a thread
// samples the memstore timestamps (kgsl_busy.h) once a millisecond while running. Ringbuffer entries are used where
// the kernel writes them; on hardware-scheduled kernels, which leave them at 0, the contexts' own entries are used.
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
#include "../adrenotools/src/hook/kgsl.h"
#include "kgsl_busy.h"
#define SAMPLE_NS 1000000L
// Context mode: how many samples between scans of the context entries.
#define SCAN_EVERY 200
enum { MODE_RINGBUFFER = 1, MODE_CONTEXTS = 2 };
struct reader {
int fd;
void *map;
size_t bytes;
int first;
int mode;
uint32_t last[KGSL_BUSY_SLOTS];
uint32_t *seen, *ctx_last;
int live[KGSL_BUSY_MAX_LIVE];
int live_count;
pthread_mutex_t lock;
pthread_t thread;
int thread_on;
atomic_int run;
atomic_uint_fast64_t busy, samples, moves;
};
static void *sample_loop(void *arg) {
struct reader *r = arg;
const struct timespec gap = {0, SAMPLE_NS};
int until_scan = 0;
while (atomic_load(&r->run)) {
int moved = 0, busy;
if (r->mode == MODE_CONTEXTS) {
if (until_scan-- <= 0) {
r->live_count = kgsl_busy_scan_contexts(r->map, r->first, r->seen, r->live, KGSL_BUSY_MAX_LIVE);
until_scan = SCAN_EVERY;
}
busy = kgsl_busy_sample_contexts(r->map, r->live, r->live_count, r->ctx_last, &moved);
} else {
busy = kgsl_busy_sample(r->map, r->first, r->last, &moved);
}
if (busy) atomic_fetch_add(&r->busy, 1);
if (moved) atomic_fetch_add(&r->moves, 1);
atomic_fetch_add(&r->samples, 1);
nanosleep(&gap, NULL);
}
return NULL;
}
static void reader_run(struct reader *r, int on) {
pthread_mutex_lock(&r->lock);
if (on && !r->thread_on) {
atomic_store(&r->run, 1);
if (pthread_create(&r->thread, NULL, sample_loop, r) == 0) r->thread_on = 1;
else atomic_store(&r->run, 0);
} else if (!on && r->thread_on) {
atomic_store(&r->run, 0);
pthread_join(r->thread, NULL);
r->thread_on = 0;
}
pthread_mutex_unlock(&r->lock);
}
static void reader_close(struct reader *r) {
if (!r) return;
reader_run(r, 0);
if (r->map && r->map != MAP_FAILED) munmap(r->map, r->bytes);
if (r->fd >= 0) close(r->fd);
free(r->seen);
free(r->ctx_last);
pthread_mutex_destroy(&r->lock);
free(r);
}
// Returns 0 or a negative errno; *out is set on success.
static int reader_open(struct reader **out) {
struct reader *r = calloc(1, sizeof(*r));
if (!r) return -ENOMEM;
pthread_mutex_init(&r->lock, NULL);
r->fd = open("/dev/kgsl-3d0", O_RDWR | O_CLOEXEC);
if (r->fd < 0) { int e = -errno; reader_close(r); return e; }
struct kgsl_shadowprop shadow;
memset(&shadow, 0, sizeof(shadow));
struct kgsl_device_getproperty prop = {.type = KGSL_PROP_DEVICE_SHADOW, .value = &shadow, .sizebytes = sizeof(shadow)};
if (ioctl(r->fd, IOCTL_KGSL_DEVICE_GETPROPERTY, &prop)) { int e = -errno; reader_close(r); return e; }
r->bytes = shadow.size;
r->first = kgsl_busy_first_slot(shadow.size);
if (r->first < 0) { reader_close(r); return -ERANGE; }
r->map = mmap(NULL, r->bytes, PROT_READ, MAP_SHARED, r->fd, (off_t)shadow.gpuaddr);
if (r->map == MAP_FAILED) { int e = -errno; reader_close(r); return e; }
int moved = 0, started = 0;
kgsl_busy_sample(r->map, r->first, r->last, &moved);
for (int i = 0; i < KGSL_BUSY_SLOTS; i++) started |= r->last[i] != 0;
if (started) {
r->mode = MODE_RINGBUFFER;
} else {
r->seen = calloc((size_t)r->first, sizeof(uint32_t));
r->ctx_last = calloc((size_t)r->first, sizeof(uint32_t));
if (!r->seen || !r->ctx_last) { reader_close(r); return -ENOMEM; }
r->live_count = kgsl_busy_scan_contexts(r->map, r->first, r->seen, r->live, KGSL_BUSY_MAX_LIVE);
kgsl_busy_sample_contexts(r->map, r->live, r->live_count, r->ctx_last, &moved);
r->mode = MODE_CONTEXTS;
}
*out = r;
return 0;
}
#ifndef KGSL_BUSY_NO_JNI
#include <jni.h>
JNIEXPORT jlong JNICALL Java_com_droiddeck_launcher_session_KgslBusy_nativeOpen(JNIEnv *env, jclass cls, jintArray error) {
struct reader *r = NULL;
int e = reader_open(&r);
if (e && error && (*env)->GetArrayLength(env, error) > 0) {
jint v = e;
(*env)->SetIntArrayRegion(env, error, 0, 1, &v);
}
return e ? 0 : (jlong)(intptr_t)r;
}
JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_session_KgslBusy_nativeMode(JNIEnv *env, jclass cls, jlong handle) {
return handle ? ((struct reader *)(intptr_t)handle)->mode : 0;
}
JNIEXPORT void JNICALL Java_com_droiddeck_launcher_session_KgslBusy_nativeRun(JNIEnv *env, jclass cls, jlong handle, jboolean on) {
if (handle) reader_run((struct reader *)(intptr_t)handle, on ? 1 : 0);
}
JNIEXPORT jboolean JNICALL Java_com_droiddeck_launcher_session_KgslBusy_nativeRead(JNIEnv *env, jclass cls, jlong handle, jlongArray out) {
if (!handle || !out || (*env)->GetArrayLength(env, out) < 3) return JNI_FALSE;
struct reader *r = (struct reader *)(intptr_t)handle;
jlong v[3] = {(jlong)atomic_load(&r->busy), (jlong)atomic_load(&r->samples), (jlong)atomic_load(&r->moves)};
(*env)->SetLongArrayRegion(env, out, 0, 3, v);
return JNI_TRUE;
}
JNIEXPORT void JNICALL Java_com_droiddeck_launcher_session_KgslBusy_nativeClose(JNIEnv *env, jclass cls, jlong handle) {
reader_close((struct reader *)(intptr_t)handle);
}
#endif
+77
View File
@@ -0,0 +1,77 @@
#ifndef DROIDDECK_KGSL_BUSY_H
#define DROIDDECK_KGSL_BUSY_H
// kgsl lets any client map its memstore read-only (KGSL_PROP_DEVICE_SHADOW). The CP writes a ringbuffer's
// start-of-pipe timestamp there before each submission's command buffers and its end-of-pipe timestamp after them
// (adreno_gen7_ringbuffer.c gen7_ringbuffer_addcmds), so start != end means that ringbuffer has work on the GPU.
// Ringbuffer entries start at KGSL_MEMSTORE_MAX: entries - 2 - 4 levels, or entries - 1 - 4 on kernels without the
// LPAC slot. Entries - 6 .. entries - 2 cover both; a context entry in that range follows the same start/end rule.
#include <stddef.h>
#include <stdint.h>
#define KGSL_BUSY_ENTRY_WORDS 10
#define KGSL_BUSY_SOP 0
#define KGSL_BUSY_EOP 2
#define KGSL_BUSY_SLOTS 5
static inline int kgsl_busy_first_slot(size_t bytes) {
size_t entries = bytes / (KGSL_BUSY_ENTRY_WORDS * sizeof(uint32_t));
return entries < 8 ? -1 : (int)(entries - 6);
}
// 1 when a watched slot has a submission between its start and end timestamps. *moved is set when a start
// timestamp differs from last[], which then holds the new ones.
static inline int kgsl_busy_sample(const volatile uint32_t *memstore, int first, uint32_t last[KGSL_BUSY_SLOTS], int *moved) {
int busy = 0;
for (int i = 0; i < KGSL_BUSY_SLOTS; i++) {
const volatile uint32_t *entry = memstore + (size_t)(first + i) * KGSL_BUSY_ENTRY_WORDS;
uint32_t sop = entry[KGSL_BUSY_SOP], eop = entry[KGSL_BUSY_EOP];
if (sop != eop) busy = 1;
if (sop != last[i]) {
*moved = 1;
last[i] = sop;
}
}
return busy;
}
// Hardware-scheduled kernels (every gen8 GPU and some gen7 ones: .gpudev = *_hwsched_gpudev in adreno-gpulist.h)
// leave the ringbuffer entries at 0. The GMU firmware writes each context's own entry instead
// (gen8_hwsched_hfi.c send_context_pointers). Context ids run from 1 below the ringbuffer entries, lowest free first,
// and kgsl zeroes an entry when its context is created or destroyed.
#define KGSL_BUSY_MAX_LIVE 64
// Collects the contexts to sample: every entry below first with a non-zero start stamp (kgsl zeroes an entry when its
// context is created or destroyed). Entries whose start stamp changed since the previous scan (seen[]) or that are
// busy now come first, so the max_live cap only drops idle ones. Returns how many ids went into live[].
static inline int kgsl_busy_scan_contexts(const volatile uint32_t *memstore, int first, uint32_t *seen, int *live, int max_live) {
int n = 0, idle = 0;
int idle_ids[KGSL_BUSY_MAX_LIVE];
for (int id = 1; id < first; id++) {
const volatile uint32_t *entry = memstore + (size_t)id * KGSL_BUSY_ENTRY_WORDS;
uint32_t sop = entry[KGSL_BUSY_SOP], eop = entry[KGSL_BUSY_EOP];
int changed = sop != seen[id];
seen[id] = sop;
if ((changed || sop != eop) && n < max_live) live[n++] = id;
else if (sop != 0 && idle < KGSL_BUSY_MAX_LIVE) idle_ids[idle++] = id;
}
for (int i = 0; i < idle && n < max_live; i++) live[n++] = idle_ids[i];
return n;
}
// 1 when a live context has a submission between its start and end timestamps. *moved is set when a start
// timestamp differs from last[] (indexed by context id), which then holds the new ones.
static inline int kgsl_busy_sample_contexts(const volatile uint32_t *memstore, const int *live, int count, uint32_t *last, int *moved) {
int busy = 0;
for (int i = 0; i < count; i++) {
const volatile uint32_t *entry = memstore + (size_t)live[i] * KGSL_BUSY_ENTRY_WORDS;
uint32_t sop = entry[KGSL_BUSY_SOP], eop = entry[KGSL_BUSY_EOP];
if (sop != eop) busy = 1;
if (sop != last[live[i]]) {
*moved = 1;
last[live[i]] = sop;
}
}
return busy;
}
#endif
+159
View File
@@ -0,0 +1,159 @@
// GPU clock without root on Adreno (KGSL) devices whose kgsl sysfs is blocked for apps.
//
// kgsl lets any app reserve a performance counter (IOCTL_KGSL_PERFCOUNTER_GET) but refuses to read it back while
// /sys/class/kgsl/kgsl-3d0/perfcounter is 0, and clears the counters whenever the GPU switches to another process.
// So the counter is read by the GPU itself: a small command buffer copies CP_ALWAYS_ON_COUNTER (19.2 MHz) and the
// RBBM ALWAYS_COUNT counter (GPU core cycles) to memory, runs a block of NOP packets, and copies both again.
// clock = cycles / always-on ticks * 19.2 MHz. Both reads sit in one command buffer, so a counter clear between
// them only happens if the buffer is preempted; such samples fail the range checks and are dropped.
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#include "../adrenotools/src/hook/kgsl.h"
#include "kgsl_layout.h"
#define CP_TYPE7_PKT 0x70000000u
#define CP_NOP 0x10u
#define CP_REG_TO_MEM 0x3eu
#define XO_HZ 19200000.0
#define NOP_DWORDS 0x3fffu
#define NOP_BLOCKS 4
struct sampler {
int fd;
const struct kgsl_layout *layout;
unsigned context;
unsigned counter_reg;
struct kgsl_gpumem_alloc_id ib, out;
uint32_t *ib_map;
volatile uint64_t *out_map;
size_t ib_bytes;
};
static unsigned odd_parity(unsigned v) {
v ^= v >> 16;
v ^= v >> 8;
v ^= v >> 4;
v &= 0xf;
return (~0x6996u >> v) & 1u;
}
static uint32_t pkt7(unsigned op, unsigned count) {
return CP_TYPE7_PKT | count | odd_parity(count) << 15 | (op & 0x7f) << 16 | odd_parity(op) << 23;
}
static void sampler_close(struct sampler *s) {
if (!s) return;
if (s->fd >= 0) {
if (s->ib_map && s->ib_map != MAP_FAILED) munmap(s->ib_map, s->ib.mmapsize);
if (s->out_map && s->out_map != MAP_FAILED) munmap((void *)s->out_map, s->out.mmapsize);
if (s->ib.id) { struct kgsl_gpumem_free_id f = {.id = s->ib.id}; ioctl(s->fd, IOCTL_KGSL_GPUMEM_FREE_ID, &f); }
if (s->out.id) { struct kgsl_gpumem_free_id f = {.id = s->out.id}; ioctl(s->fd, IOCTL_KGSL_GPUMEM_FREE_ID, &f); }
if (s->context) { struct kgsl_drawctxt_destroy d = {.drawctxt_id = s->context}; ioctl(s->fd, IOCTL_KGSL_DRAWCTXT_DESTROY, &d); }
if (s->counter_reg) {
struct kgsl_perfcounter_put p = {.groupid = KGSL_PERFCOUNTER_GROUP_RBBM, .countable = s->layout->cycles};
ioctl(s->fd, IOCTL_KGSL_PERFCOUNTER_PUT, &p);
}
close(s->fd);
}
free(s);
}
static void emit_reads(struct sampler *s, size_t *n, unsigned pass) {
const unsigned regs[2] = {s->layout->always_on, s->counter_reg};
for (unsigned k = 0; k < 2; k++) {
uint64_t dst = s->out.gpuaddr + (uint64_t)(pass * 2 + k) * sizeof(uint64_t);
s->ib_map[(*n)++] = pkt7(CP_REG_TO_MEM, 3);
s->ib_map[(*n)++] = regs[k] | (2u << 18) | (1u << 30); // two dwords, 64-bit read
s->ib_map[(*n)++] = (uint32_t)dst;
s->ib_map[(*n)++] = (uint32_t)(dst >> 32);
}
}
// Returns 0 or a negative errno; *out is set on success.
static int sampler_open(struct sampler **out) {
struct sampler *s = calloc(1, sizeof(*s));
if (!s) return -ENOMEM;
s->fd = open("/dev/kgsl-3d0", O_RDWR | O_CLOEXEC);
if (s->fd < 0) { int e = -errno; s->fd = -1; sampler_close(s); return e; }
struct kgsl_devinfo info;
memset(&info, 0, sizeof(info));
struct kgsl_device_getproperty prop = {.type = KGSL_PROP_DEVICE_INFO, .value = &info, .sizebytes = sizeof(info)};
if (ioctl(s->fd, IOCTL_KGSL_DEVICE_GETPROPERTY, &prop)) { int e = -errno; sampler_close(s); return e; }
s->layout = kgsl_layout_for(info.chip_id);
if (!s->layout) { sampler_close(s); return -ENOTSUP; }
struct kgsl_perfcounter_get get = {.groupid = KGSL_PERFCOUNTER_GROUP_RBBM, .countable = s->layout->cycles};
if (ioctl(s->fd, IOCTL_KGSL_PERFCOUNTER_GET, &get)) { int e = -errno; sampler_close(s); return e; }
s->counter_reg = get.offset;
if (!get.offset || get.offset > s->layout->readable_last) { sampler_close(s); return -ERANGE; }
struct kgsl_drawctxt_create ctx = {.flags = KGSL_CONTEXT_NO_GMEM_ALLOC | KGSL_CONTEXT_PREAMBLE};
if (ioctl(s->fd, IOCTL_KGSL_DRAWCTXT_CREATE, &ctx)) { int e = -errno; sampler_close(s); return e; }
s->context = ctx.drawctxt_id;
s->ib_bytes = (16 + NOP_BLOCKS * (NOP_DWORDS + 1)) * sizeof(uint32_t);
s->ib.size = (s->ib_bytes + 4095) & ~(size_t)4095;
s->out.size = 4096;
if (ioctl(s->fd, IOCTL_KGSL_GPUMEM_ALLOC_ID, &s->ib)) { int e = -errno; s->ib.id = 0; sampler_close(s); return e; }
if (ioctl(s->fd, IOCTL_KGSL_GPUMEM_ALLOC_ID, &s->out)) { int e = -errno; s->out.id = 0; sampler_close(s); return e; }
s->ib_map = mmap(NULL, s->ib.mmapsize, PROT_READ | PROT_WRITE, MAP_SHARED, s->fd, (off_t)s->ib.id << 12);
s->out_map = mmap(NULL, s->out.mmapsize, PROT_READ | PROT_WRITE, MAP_SHARED, s->fd, (off_t)s->out.id << 12);
if (s->ib_map == MAP_FAILED || s->out_map == MAP_FAILED) { int e = -errno; sampler_close(s); return e; }
size_t n = 0;
emit_reads(s, &n, 0);
for (unsigned b = 0; b < NOP_BLOCKS; b++) {
s->ib_map[n++] = pkt7(CP_NOP, NOP_DWORDS);
memset(&s->ib_map[n], 0, NOP_DWORDS * sizeof(uint32_t));
n += NOP_DWORDS;
}
emit_reads(s, &n, 1);
s->ib_bytes = n * sizeof(uint32_t);
*out = s;
return 0;
}
// MHz, or a negative value: -errno from kgsl, or -1000 for a sample that failed the range checks.
static int sampler_mhz(struct sampler *s) {
for (int i = 0; i < 4; i++) s->out_map[i] = 0;
struct kgsl_command_object cmd = {.gpuaddr = s->ib.gpuaddr, .size = s->ib_bytes, .flags = KGSL_CMDLIST_IB};
struct kgsl_gpu_command submit = {
.cmdlist = (uint64_t)(uintptr_t)&cmd, .cmdsize = sizeof(cmd), .numcmds = 1, .context_id = s->context,
};
if (ioctl(s->fd, IOCTL_KGSL_GPU_COMMAND, &submit)) return -errno;
struct kgsl_device_waittimestamp_ctxtid wait = {.context_id = s->context, .timestamp = submit.timestamp, .timeout = 250};
if (ioctl(s->fd, IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID, &wait)) return -errno;
uint64_t xo0 = s->out_map[0], cyc0 = s->out_map[1], xo1 = s->out_map[2], cyc1 = s->out_map[3];
if (xo1 <= xo0 || cyc1 <= cyc0) return -1000;
uint64_t xo = xo1 - xo0, cycles = cyc1 - cyc0;
// 5 us .. 50 ms of always-on time, and a clock between 50 MHz and 2 GHz.
if (xo < 96 || xo > 960000) return -1000;
double mhz = (double)cycles / (double)xo * XO_HZ / 1e6;
if (mhz < 50 || mhz > 2000) return -1000;
return (int)(mhz + 0.5);
}
#ifndef KGSL_CLOCK_NO_JNI
#include <jni.h>
JNIEXPORT jlong JNICALL Java_com_droiddeck_launcher_session_KgslClock_nativeOpen(JNIEnv *env, jclass cls, jintArray error) {
struct sampler *s = NULL;
int r = sampler_open(&s);
if (r && error && (*env)->GetArrayLength(env, error) > 0) {
jint e = r;
(*env)->SetIntArrayRegion(env, error, 0, 1, &e);
}
return r ? 0 : (jlong)(intptr_t)s;
}
JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_session_KgslClock_nativeSample(JNIEnv *env, jclass cls, jlong handle) {
return handle ? sampler_mhz((struct sampler *)(intptr_t)handle) : -EBADF;
}
JNIEXPORT void JNICALL Java_com_droiddeck_launcher_session_KgslClock_nativeClose(JNIEnv *env, jclass cls, jlong handle) {
sampler_close((struct sampler *)(intptr_t)handle);
}
#endif
+27
View File
@@ -0,0 +1,27 @@
#ifndef DROIDDECK_KGSL_LAYOUT_H
#define DROIDDECK_KGSL_LAYOUT_H
#include <stddef.h>
// Register layout per GPU generation. A counter kgsl places outside the range the kernel's CP protection lets
// command buffers read (adreno-gpulist.h *_protected_regs) is not read.
struct kgsl_layout {
unsigned always_on; // CP_ALWAYS_ON_COUNTER
unsigned cycles; // RBBM countable that counts GPU cycles
unsigned readable_last; // last register of the readable range starting at 0
};
// A6xx/A7xx: CP_ALWAYS_ON_COUNTER 0x980, RBBM countable 0 = ALWAYS_COUNT, 0x000-0x4ff readable (gen7 list).
static const struct kgsl_layout KGSL_LAYOUT_A7XX = {0x980u, 0u, 0x4ffu};
// A8xx: CP_ALWAYS_ON_COUNTER 0x8e7 (gen8_reg.h), RBBM countable 1 = US_ALWAYS_COUNT (0 is NEVER_COUNT, Mesa
// a8xx_perfcntrs.xml), 0x000-0x3a3 readable (gen8_0_0_protected_regs).
static const struct kgsl_layout KGSL_LAYOUT_A8XX = {0x8e7u, 1u, 0x3a3u};
// The KGSL chip id's top byte: 0x44 on every A8xx (Adreno 810/829/830/840, Mesa freedreno_devices.py),
// 0x43 or the generation number on A6xx/A7xx. Later generations are not known, so nothing is read there.
static inline const struct kgsl_layout *kgsl_layout_for(unsigned chip_id) {
unsigned core = chip_id >> 24;
if (core == 0x44u) return &KGSL_LAYOUT_A8XX;
if (core > 0x44u) return NULL;
return &KGSL_LAYOUT_A7XX;
}
#endif
-2
View File
@@ -76,7 +76,6 @@ public:
}
lastFrame_ = frame.header.frame_id;
if (state == XrGpuFrameState::Stale) {
// No GPU read was submitted, so the producer can safely reclaim a dropped frame.
dropped_++;
if (!xr_gpu_send(peer_, &frame.header.frame_id, sizeof(frame.header.frame_id))) Disconnect();
return false;
@@ -88,7 +87,6 @@ public:
glBindImageTexture(0, raw_, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8);
glDispatchCompute((width_ + 7) / 8, (height_ + 7) / 8, 1);
glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
// The producer may overwrite this slot only after the native GPU has finished reading it.
glFinish();
if (glGetError() != GL_NO_ERROR) { rejected_++; Disconnect(); return false; }
if (!xr_gpu_send(peer_, &frame.header.frame_id, sizeof(frame.header.frame_id))) Disconnect();
+27 -6
View File
@@ -3,13 +3,9 @@
#include <stdint.h>
/* SOCK_SEQPACKET between the guest's Remote Play hooks and the app: the emulated /dev/video-enc0 and
* MediaCodec encoder, and remote touches. A config carries the buffer memfds (inputs first) as SCM_RIGHTS;
* every frame gets one reply, touches get none. */
#define VENC_MAX_BUFFERS 16
enum venc_msg { VENC_CONFIG = 1, VENC_FRAME = 2, VENC_DONE = 3, VENC_BITRATE = 4, VENC_TOUCH = 5 };
enum venc_msg { VENC_CONFIG = 1, VENC_FRAME = 2, VENC_DONE = 3, VENC_BITRATE = 4, VENC_TOUCH = 5, VDEC_CONFIG = 6, VDEC_CAPTURE = 7, VDEC_PACKET = 8, VDEC_RESULT = 9 };
enum venc_touch_action { VENC_TOUCH_DOWN = 0, VENC_TOUCH_MOVE = 1, VENC_TOUCH_UP = 2 };
enum venc_codec { VENC_H264 = 0, VENC_HEVC = 1 };
@@ -45,11 +41,36 @@ struct venc_bitrate {
uint32_t bitrate;
};
/* Remote Play's touch-as-mouse turned back into a touch; x and y are fractions of the screen in 1/65535. */
struct venc_touch {
uint32_t type;
uint32_t action;
uint32_t x, y;
};
struct vdec_config {
uint32_t type;
uint32_t codec;
uint32_t width, height;
uint32_t stride, rows;
uint32_t bitstreams, bitstream_size;
};
struct vdec_capture {
uint32_t type;
uint32_t index;
uint32_t size;
};
struct vdec_packet {
uint32_t type;
uint32_t input, bytes, capture;
int64_t pts_us;
};
struct vdec_result {
uint32_t type;
int32_t status;
uint32_t frame;
};
#endif
+242 -3
View File
@@ -3,6 +3,7 @@
#include <jni.h>
#include <media/NdkMediaCodec.h>
#include <media/NdkMediaFormat.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/stat.h>
@@ -11,6 +12,7 @@
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cerrno>
#include <cstring>
#include <string>
@@ -31,8 +33,8 @@ struct Mapping {
constexpr int32_t kColorFormatYuv420SemiPlanar = 21;
constexpr uint32_t kBufferFlagKeyFrame = 1;
constexpr uint32_t kBufferFlagCodecConfig = 2;
constexpr int kTouchWidth = 1920, kTouchHeight = 1080; // InputSpace.kt
constexpr int kRemoteTouchId = 31; // the compositor's last touch slot (MAX_TOUCH_POINTS 32)
constexpr int kTouchWidth = 1920, kTouchHeight = 1080;
constexpr int kRemoteTouchId = 31;
int32_t avc_profile(int32_t v4l2) {
switch (v4l2) {
@@ -45,6 +47,218 @@ int32_t avc_profile(int32_t v4l2) {
}
}
struct DmaBufSync {
uint64_t flags;
};
constexpr unsigned long kDmaBufIoctlSync = _IOW('b', 0, DmaBufSync);
constexpr uint64_t kDmaBufSyncWrite = 2, kDmaBufSyncStart = 0, kDmaBufSyncEnd = 4;
constexpr int32_t kColorFormatYuv420Planar = 19;
class Decoder {
public:
~Decoder() { release(); }
void release() {
if (codec_) {
AMediaCodec_stop(codec_);
AMediaCodec_delete(codec_);
codec_ = nullptr;
}
if (bitstream_) munmap((void *)bitstream_, bitstream_size_);
bitstream_ = nullptr;
for (auto &c : captures_) drop(c);
}
bool configure(const vdec_config &c, int fd) {
release();
if (c.width < 16 || c.height < 16 || c.stride < c.width || c.rows < c.height || c.bitstreams == 0 ||
c.bitstreams > VENC_MAX_BUFFERS || c.bitstream_size == 0) {
LOG("bad decoder config: %ux%u stride %u rows %u, %u bitstreams", c.width, c.height, c.stride, c.rows,
c.bitstreams);
return false;
}
bitstream_size_ = (size_t)c.bitstreams * c.bitstream_size;
void *p = mmap(nullptr, bitstream_size_, PROT_READ, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) {
LOG("could not map the bitstream buffers: %s", strerror(errno));
return false;
}
bitstream_ = (const uint8_t *)p;
config_ = c;
codec_ = create(c, true);
if (!codec_) codec_ = create(c, false);
if (!codec_) {
LOG("no %s decoder for %ux%u", c.codec == VENC_HEVC ? "HEVC" : "H.264", c.width, c.height);
release();
return false;
}
src_stride_ = (int32_t)c.width;
src_rows_ = (int32_t)c.height;
src_width_ = (int32_t)c.width;
src_height_ = (int32_t)c.height;
src_planar_ = false;
shown_ = false;
using GetName = media_status_t (*)(AMediaCodec *, char **);
using ReleaseName = void (*)(AMediaCodec *, char *);
static auto get_name = (GetName)dlsym(RTLD_DEFAULT, "AMediaCodec_getName");
static auto release_name = (ReleaseName)dlsym(RTLD_DEFAULT, "AMediaCodec_releaseName");
char *name = nullptr;
if (get_name && get_name(codec_, &name) != AMEDIA_OK) name = nullptr;
LOG("decoding %s %ux%u with %s", c.codec == VENC_HEVC ? "HEVC" : "H.264", c.width, c.height,
name ? name : "MediaCodec");
if (name && release_name) release_name(codec_, name);
return true;
}
void capture(const vdec_capture &c, int fd) {
if (c.index >= VENC_MAX_BUFFERS) {
close(fd);
return;
}
Capture &cap = captures_[c.index];
drop(cap);
void *p = mmap(nullptr, c.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) {
LOG("could not map capture buffer %u: %s", c.index, strerror(errno));
close(fd);
return;
}
cap = {fd, (uint8_t *)p, c.size};
}
bool decode(const vdec_packet &p, bool *frame) {
*frame = false;
if (!codec_ || p.capture >= VENC_MAX_BUFFERS || !captures_[p.capture].data || p.input >= config_.bitstreams ||
p.bytes == 0 || p.bytes > config_.bitstream_size)
return false;
auto start = std::chrono::steady_clock::now();
ssize_t in = AMediaCodec_dequeueInputBuffer(codec_, 20000);
if (in < 0) return false;
size_t capacity = 0;
uint8_t *dst = AMediaCodec_getInputBuffer(codec_, (size_t)in, &capacity);
if (!dst || capacity < p.bytes) {
AMediaCodec_queueInputBuffer(codec_, (size_t)in, 0, 0, 0, 0);
return false;
}
memcpy(dst, bitstream_ + (size_t)p.input * config_.bitstream_size, p.bytes);
if (AMediaCodec_queueInputBuffer(codec_, (size_t)in, 0, p.bytes, (uint64_t)std::max<int64_t>(p.pts_us, 0), 0) !=
AMEDIA_OK)
return false;
auto wait = std::chrono::milliseconds(shown_ ? 25 : 250);
while (std::chrono::steady_clock::now() - start < wait) {
AMediaCodecBufferInfo info;
ssize_t o = AMediaCodec_dequeueOutputBuffer(codec_, &info, 2000);
if (o == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) {
read_format();
continue;
}
if (o < 0) continue;
size_t size = 0;
uint8_t *data = AMediaCodec_getOutputBuffer(codec_, (size_t)o, &size);
if (data && info.size > 0 && (size_t)info.offset + (size_t)info.size <= size)
*frame = copy(data + info.offset, (size_t)info.size, captures_[p.capture]) || *frame;
AMediaCodec_releaseOutputBuffer(codec_, (size_t)o, false);
if (info.presentationTimeUs >= p.pts_us) break;
}
shown_ = shown_ || *frame;
return true;
}
private:
struct Capture {
int fd = -1;
uint8_t *data = nullptr;
size_t size = 0;
};
static void drop(Capture &c) {
if (c.data) munmap(c.data, c.size);
if (c.fd >= 0) close(c.fd);
c = Capture();
}
AMediaCodec *create(const vdec_config &c, bool low_latency) {
const char *mime = c.codec == VENC_HEVC ? "video/hevc" : "video/avc";
AMediaCodec *codec = AMediaCodec_createDecoderByType(mime);
if (!codec) return nullptr;
AMediaFormat *f = AMediaFormat_new();
AMediaFormat_setString(f, AMEDIAFORMAT_KEY_MIME, mime);
AMediaFormat_setInt32(f, AMEDIAFORMAT_KEY_WIDTH, (int32_t)c.width);
AMediaFormat_setInt32(f, AMEDIAFORMAT_KEY_HEIGHT, (int32_t)c.height);
AMediaFormat_setInt32(f, AMEDIAFORMAT_KEY_COLOR_FORMAT, kColorFormatYuv420SemiPlanar);
AMediaFormat_setInt32(f, AMEDIAFORMAT_KEY_MAX_INPUT_SIZE, (int32_t)c.bitstream_size);
if (low_latency) {
AMediaFormat_setInt32(f, "low-latency", 1);
AMediaFormat_setInt32(f, "vendor.qti-ext-dec-low-latency.enable", 1);
AMediaFormat_setInt32(f, "vendor.qti-ext-dec-picture-order.enable", 1);
AMediaFormat_setInt32(f, "priority", 0);
AMediaFormat_setFloat(f, "operating-rate", 120.0f);
}
media_status_t s = AMediaCodec_configure(codec, f, nullptr, nullptr, 0);
AMediaFormat_delete(f);
if (s == AMEDIA_OK) s = AMediaCodec_start(codec);
if (s != AMEDIA_OK) {
AMediaCodec_delete(codec);
return nullptr;
}
return codec;
}
void read_format() {
AMediaFormat *f = AMediaCodec_getOutputFormat(codec_);
if (!f) return;
int32_t v;
if (AMediaFormat_getInt32(f, AMEDIAFORMAT_KEY_WIDTH, &v) && v > 0) src_width_ = v;
if (AMediaFormat_getInt32(f, AMEDIAFORMAT_KEY_HEIGHT, &v) && v > 0) src_height_ = v;
src_stride_ = AMediaFormat_getInt32(f, "stride", &v) && v >= src_width_ ? v : src_width_;
src_rows_ = AMediaFormat_getInt32(f, "slice-height", &v) && v >= src_height_ ? v : src_height_;
src_planar_ = AMediaFormat_getInt32(f, AMEDIAFORMAT_KEY_COLOR_FORMAT, &v) && v == kColorFormatYuv420Planar;
LOG("decoder output %dx%d stride %d rows %d %s", src_width_, src_height_, src_stride_, src_rows_,
src_planar_ ? "planar" : "semi-planar");
AMediaFormat_delete(f);
}
bool copy(const uint8_t *src, size_t size, const Capture &cap) {
size_t luma = (size_t)src_stride_ * src_rows_;
size_t chroma = src_planar_ ? 2 * (size_t)(src_stride_ / 2) * (src_rows_ / 2) : (size_t)src_stride_ * (src_rows_ / 2);
if (size < luma + chroma) return false;
uint32_t w = std::min<uint32_t>((uint32_t)src_width_, config_.stride);
uint32_t h = std::min<uint32_t>((uint32_t)src_height_, config_.rows);
if ((size_t)config_.stride * config_.rows * 3 / 2 > cap.size) return false;
DmaBufSync sync = {kDmaBufSyncStart | kDmaBufSyncWrite};
ioctl(cap.fd, kDmaBufIoctlSync, &sync);
uint8_t *y = cap.data, *uv = cap.data + (size_t)config_.stride * config_.rows;
for (uint32_t row = 0; row < h; row++)
memcpy(y + (size_t)row * config_.stride, src + (size_t)row * src_stride_, w);
const uint8_t *su = src + luma;
for (uint32_t row = 0; row < h / 2; row++) {
uint8_t *d = uv + (size_t)row * config_.stride;
if (!src_planar_) {
memcpy(d, su + (size_t)row * src_stride_, w & ~1u);
continue;
}
const uint8_t *u = su + (size_t)row * (src_stride_ / 2);
const uint8_t *vv = su + (size_t)(src_stride_ / 2) * (src_rows_ / 2) + (size_t)row * (src_stride_ / 2);
for (uint32_t x = 0; x < w / 2; x++) {
d[2 * x] = u[x];
d[2 * x + 1] = vv[x];
}
}
sync.flags = kDmaBufSyncEnd | kDmaBufSyncWrite;
ioctl(cap.fd, kDmaBufIoctlSync, &sync);
return true;
}
AMediaCodec *codec_ = nullptr;
vdec_config config_ = {};
const uint8_t *bitstream_ = nullptr;
size_t bitstream_size_ = 0;
Capture captures_[VENC_MAX_BUFFERS];
int32_t src_width_ = 0, src_height_ = 0, src_stride_ = 0, src_rows_ = 0;
bool src_planar_ = false;
bool shown_ = false;
};
class Session {
public:
explicit Session(int fd) : fd_(fd) {}
@@ -85,6 +299,24 @@ public:
memcpy(&b, buf, sizeof b);
set_bitrate(b.bitrate);
for (int i = 0; i < nfds; i++) close(fds[i]);
} else if (type == VDEC_CONFIG && n == (ssize_t)sizeof(vdec_config)) {
vdec_config c;
memcpy(&c, buf, sizeof c);
bool ok = nfds == 1 && decoder_.configure(c, fds[0]);
for (int i = 0; i < nfds; i++) close(fds[i]);
decoded(ok ? 0 : -1, false);
} else if (type == VDEC_CAPTURE && n == (ssize_t)sizeof(vdec_capture)) {
vdec_capture c;
memcpy(&c, buf, sizeof c);
if (nfds == 1) decoder_.capture(c, fds[0]);
else for (int i = 0; i < nfds; i++) close(fds[i]);
} else if (type == VDEC_PACKET && n == (ssize_t)sizeof(vdec_packet)) {
vdec_packet p;
memcpy(&p, buf, sizeof p);
bool frame = false;
bool ok = decoder_.decode(p, &frame);
for (int i = 0; i < nfds; i++) close(fds[i]);
decoded(ok ? 0 : -1, frame);
} else {
LOG("unexpected message %u of %zd bytes; closing", type, n);
for (int i = 0; i < nfds; i++) close(fds[i]);
@@ -117,7 +349,13 @@ private:
send(fd_, &d, sizeof d, MSG_NOSIGNAL);
}
void decoded(int32_t status, bool frame) {
vdec_result r = {VDEC_RESULT, status, frame ? 1u : 0u};
send(fd_, &r, sizeof r, MSG_NOSIGNAL);
}
void release() {
decoder_.release();
if (codec_) {
AMediaCodec_stop(codec_);
AMediaCodec_delete(codec_);
@@ -292,6 +530,7 @@ private:
int32_t dst_stride_ = 0, dst_rows_ = 0;
std::vector<Mapping> inputs_, outputs_;
std::vector<uint8_t> header_;
Decoder decoder_;
};
std::atomic<int> g_listener{-1};
@@ -310,7 +549,7 @@ void serve(int listener) {
}
}
} // namespace
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_droiddeck_launcher_session_VideoEncodeBridge_nativeStart(JNIEnv *env, jobject, jstring jpath) {
+11 -13
View File
@@ -1,20 +1,18 @@
#pragma once
// Our controller uses the standard display-mode vocabulary, even for Beast.
// Native mode reuses some numbers with different meanings; never pass them through.
namespace droiddeck::viture {
constexpr int to_native_mode(int standard) {
switch (standard) {
case 0x31: return 0x31; // 1080p 60
case 0x33: return 0x32; // 1080p 90
case 0x34: return 0x33; // 1080p 120
case 0x32: return 0x37; // 1080p SBS 60
case 0x35: return 0x38; // 1080p SBS 90
case 0x41: return 0x34; // 1200p 60
case 0x43: return 0x35; // 1200p 90
case 0x44: return 0x36; // 1200p 120
case 0x42: return 0x3a; // 1200p SBS 60
case 0x45: return 0x3b; // 1200p SBS 90
case 0x31: return 0x31;
case 0x33: return 0x32;
case 0x34: return 0x33;
case 0x32: return 0x37;
case 0x35: return 0x38;
case 0x41: return 0x34;
case 0x43: return 0x35;
case 0x44: return 0x36;
case 0x42: return 0x3a;
case 0x45: return 0x3b;
default: return -4;
}
}
@@ -30,7 +28,7 @@ constexpr int from_native_mode(int native) {
case 0x36: return 0x44;
case 0x3a: return 0x42;
case 0x3b: return 0x45;
default: return -4; // Ultrawide is not stereo.
default: return -4;
}
}
}
-1
View File
@@ -5,7 +5,6 @@
inline std::size_t mjpeg_payload_size(const uint8_t* bytes, std::size_t size) {
if (!bytes || size < 4 || size > 4*1024*1024 || bytes[0] != 0xff || bytes[1] != 0xd8) return 0;
std::size_t end = size;
// Luma Ultra UVC frames can pad the JPEG to an eight-byte boundary with zeros.
while (end > 4 && size-end < 7 && bytes[end-1] == 0) --end;
return bytes[end-2] == 0xff && bytes[end-1] == 0xd9 ? end : 0;
}
-4
View File
@@ -30,7 +30,6 @@
#include "stereo_frame_gate.h"
namespace {
// SDK callbacks have no user-data parameter. Only one provider may exist at a time.
std::mutex api_mutex;
std::mutex pose_mutex;
std::array<float, 4> latest_pose{};
@@ -64,7 +63,6 @@ void on_pose(float* data, uint64_t) {
std::lock_guard<std::mutex> lock(pose_mutex);
if (!accepting_pose) return;
std::copy_n(data + 3, 4, latest_pose.begin());
// SDK device ticks are not Android's elapsed-realtime clock.
latest_time = now_ns();
}
@@ -234,8 +232,6 @@ int set_display(Session& s, int standard) {
if (mode == 0) {
int rc = xr_device_provider_set_display_mode(s.device, standard);
if (rc != VITURE_GLASSES_ERROR_NOT_SUPPORTED) return rc;
// Older Beast firmware exposes fewer bypass modes. Fall back to its
// separate native table only when the SDK explicitly rejects the mode.
}
const int native = droiddeck::viture::to_native_mode(standard);
if (native < 0) return native;
@@ -11,6 +11,7 @@ add_library(droiddeckwayland SHARED
src/gpu_clock_pin.c
src/droiddeck_ext.c
src/wl_dmabuf.c
src/wl_android_buffer.c
src/wl_clipboard.c
src/wl_text_input.c
src/wl_toplevel_icon.c
@@ -18,9 +19,11 @@ add_library(droiddeckwayland SHARED
src/wl_color_mgmt.c
src/hdr_compose.c
src/blend_pass.c
src/gpu_busy.c
generated/color-management-v1-protocol.c
generated/xdg-shell-protocol.c
generated/linux-dmabuf-v1-protocol.c
generated/wayland-android-protocol.c
generated/viewporter-protocol.c
generated/banner-desktop-v1-protocol.c
generated/presentation-time-protocol.c
@@ -33,6 +36,8 @@ add_library(droiddeckwayland SHARED
generated/banner-ahb-v1-protocol.c
src/framegen_bridge.c
src/framegen_jni.c
src/hudframes.c
src/hudframes_jni.c
)
target_compile_options(droiddeckwayland PRIVATE
$<$<COMPILE_LANGUAGE:C>:-Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers>)
@@ -41,6 +46,7 @@ target_include_directories(droiddeckwayland PRIVATE
prebuilt/include
src
${CMAKE_CURRENT_SOURCE_DIR}/../shaders
${CMAKE_CURRENT_SOURCE_DIR}/../../../../../tools/gpuload/src
)
set(DROIDDECK_FRAMEGEN_LSFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../framegen/lsfg)
@@ -95,4 +101,5 @@ target_link_libraries(droiddeckwayland
dl
log
android
nativewindow
)
@@ -0,0 +1,104 @@
/* Generated by wayland-scanner 1.26.0 */
/*
* Copyright © 2012 Collabora, Ltd.
*
* Permission to use, copy, modify, distribute, and sell this
* software and its documentation for any purpose is hereby granted
* without fee, provided that the above copyright notice appear in
* all copies and that both that copyright notice and this permission
* notice appear in supporting documentation, and that the name of
* the copyright holders not be used in advertising or publicity
* pertaining to distribution of the software without specific,
* written prior permission. The copyright holders make no
* representations about the suitability of this software for any
* purpose. It is provided "as is" without express or implied
* warranty.
*
* THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
* SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
* SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
* ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
* THIS SOFTWARE.
*/
#include <stdbool.h>
#include <stdlib.h>
#include <stdint.h>
#include "wayland-util.h"
#ifndef __has_attribute
# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */
#endif
#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4)
#define WL_PRIVATE __attribute__ ((visibility("hidden")))
#else
#define WL_PRIVATE
#endif
extern const struct wl_interface android_wlegl_handle_interface;
extern const struct wl_interface android_wlegl_server_buffer_handle_interface;
extern const struct wl_interface wl_buffer_interface;
static const struct wl_interface *android_types[] = {
NULL,
NULL,
&android_wlegl_handle_interface,
NULL,
NULL,
&wl_buffer_interface,
NULL,
NULL,
NULL,
NULL,
NULL,
&android_wlegl_handle_interface,
&android_wlegl_server_buffer_handle_interface,
NULL,
NULL,
NULL,
NULL,
&wl_buffer_interface,
NULL,
NULL,
};
static const struct wl_message android_wlegl_requests[] = {
{ "create_handle", "nia", android_types + 2 },
{ "create_buffer", "niiiiio", android_types + 5 },
{ "get_server_buffer_handle", "2niiii", android_types + 12 },
};
WL_PRIVATE const struct wl_interface android_wlegl_interface = {
"android_wlegl", 3,
3, android_wlegl_requests,
0, NULL,
};
static const struct wl_message android_wlegl_handle_requests[] = {
{ "add_fd", "h", android_types + 0 },
{ "destroy", "", android_types + 0 },
};
WL_PRIVATE const struct wl_interface android_wlegl_handle_interface = {
"android_wlegl_handle", 1,
2, android_wlegl_handle_requests,
0, NULL,
};
static const struct wl_message android_wlegl_server_buffer_handle_events[] = {
{ "buffer_fd", "h", android_types + 0 },
{ "buffer_ints", "a", android_types + 0 },
{ "buffer", "nii", android_types + 17 },
{ "linear_layout", "3uu", android_types + 0 },
};
WL_PRIVATE const struct wl_interface android_wlegl_server_buffer_handle_interface = {
"android_wlegl_server_buffer_handle", 3,
0, NULL,
4, android_wlegl_server_buffer_handle_events,
};
@@ -0,0 +1,341 @@
/* Generated by wayland-scanner 1.26.0 */
#ifndef ANDROID_SERVER_PROTOCOL_H
#define ANDROID_SERVER_PROTOCOL_H
#include <stdint.h>
#include <stddef.h>
#include "wayland-server.h"
#ifdef __cplusplus
extern "C" {
#endif
struct wl_client;
struct wl_resource;
/**
* @page page_android The android protocol
* @section page_ifaces_android Interfaces
* - @subpage page_iface_android_wlegl - Android EGL graphics buffer support
* - @subpage page_iface_android_wlegl_handle - An Android native_handle_t object
* - @subpage page_iface_android_wlegl_server_buffer_handle - a server allocated buffer
* @section page_copyright_android Copyright
* <pre>
*
* Copyright © 2012 Collabora, Ltd.
*
* Permission to use, copy, modify, distribute, and sell this
* software and its documentation for any purpose is hereby granted
* without fee, provided that the above copyright notice appear in
* all copies and that both that copyright notice and this permission
* notice appear in supporting documentation, and that the name of
* the copyright holders not be used in advertising or publicity
* pertaining to distribution of the software without specific,
* written prior permission. The copyright holders make no
* representations about the suitability of this software for any
* purpose. It is provided "as is" without express or implied
* warranty.
*
* THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
* SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
* SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
* ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
* THIS SOFTWARE.
* </pre>
*/
struct android_wlegl;
struct android_wlegl_handle;
struct android_wlegl_server_buffer_handle;
struct wl_buffer;
#ifndef ANDROID_WLEGL_INTERFACE
#define ANDROID_WLEGL_INTERFACE
/**
* @page page_iface_android_wlegl android_wlegl
* @section page_iface_android_wlegl_desc Description
*
* Interface used in the Android wrapper libEGL to share
* graphics buffers between the server and the client.
* @section page_iface_android_wlegl_api API
* See @ref iface_android_wlegl.
*/
/**
* @defgroup iface_android_wlegl The android_wlegl interface
*
* Interface used in the Android wrapper libEGL to share
* graphics buffers between the server and the client.
*/
extern const struct wl_interface android_wlegl_interface;
#endif
#ifndef ANDROID_WLEGL_HANDLE_INTERFACE
#define ANDROID_WLEGL_HANDLE_INTERFACE
/**
* @page page_iface_android_wlegl_handle android_wlegl_handle
* @section page_iface_android_wlegl_handle_desc Description
*
* The Android native_handle_t is a semi-opaque object, that
* contains an EGL implementation specific number of int32
* values and file descriptors.
*
* We cannot send a variable size array of file descriptors
* over the Wayland protocol, so we send them one by one.
* @section page_iface_android_wlegl_handle_api API
* See @ref iface_android_wlegl_handle.
*/
/**
* @defgroup iface_android_wlegl_handle The android_wlegl_handle interface
*
* The Android native_handle_t is a semi-opaque object, that
* contains an EGL implementation specific number of int32
* values and file descriptors.
*
* We cannot send a variable size array of file descriptors
* over the Wayland protocol, so we send them one by one.
*/
extern const struct wl_interface android_wlegl_handle_interface;
#endif
#ifndef ANDROID_WLEGL_SERVER_BUFFER_HANDLE_INTERFACE
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_INTERFACE
/**
* @page page_iface_android_wlegl_server_buffer_handle android_wlegl_server_buffer_handle
* @section page_iface_android_wlegl_server_buffer_handle_desc Description
*
* On creation a server_side_buffer object will immediately send
* the "buffer_fd" and "buffer_ints" events needed by the client to
* reference the gralloc buffer, followed by the "buffer" event carrying
* the wl_buffer object.
* @section page_iface_android_wlegl_server_buffer_handle_api API
* See @ref iface_android_wlegl_server_buffer_handle.
*/
/**
* @defgroup iface_android_wlegl_server_buffer_handle The android_wlegl_server_buffer_handle interface
*
* On creation a server_side_buffer object will immediately send
* the "buffer_fd" and "buffer_ints" events needed by the client to
* reference the gralloc buffer, followed by the "buffer" event carrying
* the wl_buffer object.
*/
extern const struct wl_interface android_wlegl_server_buffer_handle_interface;
#endif
#ifndef ANDROID_WLEGL_ERROR_ENUM
#define ANDROID_WLEGL_ERROR_ENUM
enum android_wlegl_error {
ANDROID_WLEGL_ERROR_BAD_HANDLE = 0,
ANDROID_WLEGL_ERROR_BAD_VALUE = 1,
};
#endif /* ANDROID_WLEGL_ERROR_ENUM */
#ifndef ANDROID_WLEGL_ERROR_ENUM_IS_VALID
#define ANDROID_WLEGL_ERROR_ENUM_IS_VALID
/**
* @ingroup iface_android_wlegl
* Validate a android_wlegl error value.
*
* @return true on success, false on error.
* @ref android_wlegl_error
*/
static inline bool
android_wlegl_error_is_valid(uint32_t value, uint32_t version) {
switch (value) {
case ANDROID_WLEGL_ERROR_BAD_HANDLE:
return version >= 1;
case ANDROID_WLEGL_ERROR_BAD_VALUE:
return version >= 1;
default:
return false;
}
}
#endif /* ANDROID_WLEGL_ERROR_ENUM_IS_VALID */
/**
* @ingroup iface_android_wlegl
* @struct android_wlegl_interface
*/
struct android_wlegl_interface {
/**
* Create an Android native_handle_t object
*
* This creator method initialises the native_handle_t object
* with everything except the file descriptors, which have to be
* submitted separately.
* @param ints an array of int32_t
*/
void (*create_handle)(struct wl_client *client,
struct wl_resource *resource,
uint32_t id,
int32_t num_fds,
struct wl_array *ints);
/**
* Create a wl_buffer from the native handle
*
* Pass the Android native_handle_t to the server and attach it
* to the new wl_buffer object.
*
* The android_wlegl_handle object must be destroyed immediately
* after this request.
*/
void (*create_buffer)(struct wl_client *client,
struct wl_resource *resource,
uint32_t id,
int32_t width,
int32_t height,
int32_t stride,
int32_t format,
int32_t usage,
struct wl_resource *native_handle);
/**
* @since 2
*/
void (*get_server_buffer_handle)(struct wl_client *client,
struct wl_resource *resource,
uint32_t id,
int32_t width,
int32_t height,
int32_t format,
int32_t usage);
};
/**
* @ingroup iface_android_wlegl
*/
#define ANDROID_WLEGL_CREATE_HANDLE_SINCE_VERSION 1
/**
* @ingroup iface_android_wlegl
*/
#define ANDROID_WLEGL_CREATE_BUFFER_SINCE_VERSION 1
/**
* @ingroup iface_android_wlegl
*/
#define ANDROID_WLEGL_GET_SERVER_BUFFER_HANDLE_SINCE_VERSION 2
#ifndef ANDROID_WLEGL_HANDLE_ERROR_ENUM
#define ANDROID_WLEGL_HANDLE_ERROR_ENUM
enum android_wlegl_handle_error {
ANDROID_WLEGL_HANDLE_ERROR_TOO_MANY_FDS = 0,
};
#endif /* ANDROID_WLEGL_HANDLE_ERROR_ENUM */
#ifndef ANDROID_WLEGL_HANDLE_ERROR_ENUM_IS_VALID
#define ANDROID_WLEGL_HANDLE_ERROR_ENUM_IS_VALID
/**
* @ingroup iface_android_wlegl_handle
* Validate a android_wlegl_handle error value.
*
* @return true on success, false on error.
* @ref android_wlegl_handle_error
*/
static inline bool
android_wlegl_handle_error_is_valid(uint32_t value, uint32_t version) {
switch (value) {
case ANDROID_WLEGL_HANDLE_ERROR_TOO_MANY_FDS:
return version >= 1;
default:
return false;
}
}
#endif /* ANDROID_WLEGL_HANDLE_ERROR_ENUM_IS_VALID */
/**
* @ingroup iface_android_wlegl_handle
* @struct android_wlegl_handle_interface
*/
struct android_wlegl_handle_interface {
/**
*/
void (*add_fd)(struct wl_client *client,
struct wl_resource *resource,
int32_t fd);
/**
*/
void (*destroy)(struct wl_client *client,
struct wl_resource *resource);
};
/**
* @ingroup iface_android_wlegl_handle
*/
#define ANDROID_WLEGL_HANDLE_ADD_FD_SINCE_VERSION 1
/**
* @ingroup iface_android_wlegl_handle
*/
#define ANDROID_WLEGL_HANDLE_DESTROY_SINCE_VERSION 1
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER_FD 0
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER_INTS 1
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER 2
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_LINEAR_LAYOUT 3
/**
* @ingroup iface_android_wlegl_server_buffer_handle
*/
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER_FD_SINCE_VERSION 1
/**
* @ingroup iface_android_wlegl_server_buffer_handle
*/
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER_INTS_SINCE_VERSION 1
/**
* @ingroup iface_android_wlegl_server_buffer_handle
*/
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER_SINCE_VERSION 1
/**
* @ingroup iface_android_wlegl_server_buffer_handle
*/
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_LINEAR_LAYOUT_SINCE_VERSION 3
/**
* @ingroup iface_android_wlegl_server_buffer_handle
* Sends an buffer_fd event to the client owning the resource.
* @param resource_ The client's resource
*/
static inline void
android_wlegl_server_buffer_handle_send_buffer_fd(struct wl_resource *resource_, int32_t fd)
{
wl_resource_post_event(resource_, ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER_FD, fd);
}
/**
* @ingroup iface_android_wlegl_server_buffer_handle
* Sends an buffer_ints event to the client owning the resource.
* @param resource_ The client's resource
*/
static inline void
android_wlegl_server_buffer_handle_send_buffer_ints(struct wl_resource *resource_, struct wl_array *ints)
{
wl_resource_post_event(resource_, ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER_INTS, ints);
}
/**
* @ingroup iface_android_wlegl_server_buffer_handle
* Sends an buffer event to the client owning the resource.
* @param resource_ The client's resource
*/
static inline void
android_wlegl_server_buffer_handle_send_buffer(struct wl_resource *resource_, struct wl_resource *buffer, int32_t format, int32_t stride)
{
wl_resource_post_event(resource_, ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER, buffer, format, stride);
}
/**
* @ingroup iface_android_wlegl_server_buffer_handle
* Sends an linear_layout event to the client owning the resource.
* @param resource_ The client's resource
*/
static inline void
android_wlegl_server_buffer_handle_send_linear_layout(struct wl_resource *resource_, uint32_t drm_format, uint32_t stride)
{
wl_resource_post_event(resource_, ANDROID_WLEGL_SERVER_BUFFER_HANDLE_LINEAR_LAYOUT, drm_format, stride);
}
#ifdef __cplusplus
}
#endif
#endif
@@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="android">
<copyright>
Copyright © 2012 Collabora, Ltd.
Permission to use, copy, modify, distribute, and sell this
software and its documentation for any purpose is hereby granted
without fee, provided that the above copyright notice appear in
all copies and that both that copyright notice and this permission
notice appear in supporting documentation, and that the name of
the copyright holders not be used in advertising or publicity
pertaining to distribution of the software without specific,
written prior permission. The copyright holders make no
representations about the suitability of this software for any
purpose. It is provided "as is" without express or implied
warranty.
THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
</copyright>
<interface name="android_wlegl" version="3">
<description summary="Android EGL graphics buffer support">
Interface used in the Android wrapper libEGL to share
graphics buffers between the server and the client.
</description>
<enum name="error">
<entry name="bad_handle" value="0" />
<entry name="bad_value" value="1" />
</enum>
<request name="create_handle">
<description summary="Create an Android native_handle_t object">
This creator method initialises the native_handle_t object
with everything except the file descriptors, which have to be
submitted separately.
</description>
<arg name="id" type="new_id" interface="android_wlegl_handle" />
<arg name="num_fds" type="int" />
<arg name="ints" type="array" summary="an array of int32_t" />
</request>
<request name="create_buffer">
<description summary="Create a wl_buffer from the native handle">
Pass the Android native_handle_t to the server and attach it
to the new wl_buffer object.
The android_wlegl_handle object must be destroyed immediately
after this request.
</description>
<arg name="id" type="new_id" interface="wl_buffer" />
<arg name="width" type="int" />
<arg name="height" type="int" />
<arg name="stride" type="int" />
<arg name="format" type="int" />
<arg name="usage" type="int" />
<arg name="native_handle" type="object" interface="android_wlegl_handle" />
</request>
<request name="get_server_buffer_handle" since="2">
<arg name="id" type="new_id" interface="android_wlegl_server_buffer_handle"/>
<arg name="width" type="int"/>
<arg name="height" type="int"/>
<arg name="format" type="int"/>
<arg name="usage" type="int"/>
</request>
</interface>
<interface name="android_wlegl_handle" version="1">
<description summary="An Android native_handle_t object">
The Android native_handle_t is a semi-opaque object, that
contains an EGL implementation specific number of int32
values and file descriptors.
We cannot send a variable size array of file descriptors
over the Wayland protocol, so we send them one by one.
</description>
<enum name="error">
<entry name="too_many_fds" value="0" />
</enum>
<request name="add_fd">
<arg name="fd" type="fd" />
</request>
<request name="destroy" type="destructor" />
</interface>
<interface name="android_wlegl_server_buffer_handle" version="3">
<description summary="a server allocated buffer">
On creation a server_side_buffer object will immediately send
the "buffer_fd" and "buffer_ints" events needed by the client to
reference the gralloc buffer, followed by the "buffer" event carrying
the wl_buffer object.
</description>
<event name="buffer_fd">
<arg name="fd" type="fd"/>
</event>
<event name="buffer_ints">
<arg name="ints" type="array"/>
</event>
<event name="buffer">
<description summary="the wl_buffer">
This event will be sent after the ints and all the fds have been sent
</description>
<arg name="buffer" type="new_id" interface="wl_buffer"/>
<arg name="format" type="int"/>
<arg name="stride" type="int"/>
</event>
<event name="linear_layout" since="3">
<description summary="verified single-plane linear allocation">
Optional metadata sent before the buffer event. The first buffer_fd
contains one uncompressed 32-bit RGB plane at byte offset zero.
drm_format is its DRM FourCC and stride is its row pitch in bytes.
The server has verified the layout and allocation bounds using
Android mapper metadata, not inferred them from opaque handle ints.
Absence of this event means the layout is unspecified.
</description>
<arg name="drm_format" type="uint"/>
<arg name="stride" type="uint"/>
</event>
</interface>
</protocol>
@@ -79,6 +79,9 @@ static struct ahb_buf *find_buf(uint64_t id) {
}
static void buf_free(struct ahb_buf *ab) {
/* A display release does not prove the compositor's Vulkan reads finished.
* Keep the AHB reference and its DMA-BUF record even if the client is gone. */
if (vkp_completion_unknown()) return;
if (ab->resource) { wl_list_remove(&ab->resource_destroy.link); ab->resource = NULL; }
if (ab->fence_src) { wl_event_source_remove(ab->fence_src); ab->fence_src = NULL; }
if (ab->fence_fd >= 0) { close(ab->fence_fd); ab->fence_fd = -1; }
@@ -103,6 +106,7 @@ static void on_buffer_resource_destroyed(struct wl_listener *l, void *data) {
static void send_deferred_release(struct ahb_buf *ab) {
if (vkp_completion_unknown()) return;
if (ab->release_pending && ab->resource)
droiddeck_release_buffer(ab->surface, ab->resource, ab->surface != NULL, ab->deferred_ns);
ab->release_pending = 0;
@@ -371,8 +375,8 @@ void ahb_swapchain_set_mode(int on, int live) {
: "; gralloc frames still in flight stay on the layer (or take the copy path) until then");
else
droiddeck_log("layer", "zero-copy: %s at launch (%s)", on ? "on" : "off",
on ? "DROIDDECK_WAYLAND_ZERO_COPY=1: games present their own gralloc buffers on the display layer"
: "the drawer's Zero-copy presentation switch turns it on live");
on ? "HDR session: games present their own gralloc buffers on the display layer"
: "only HDR sessions turn it on");
if (live) wl_display_flush_clients(droiddeck_get_display());
droiddeck_request_redraw();
}
+7 -1
View File
@@ -34,7 +34,6 @@ void cardboard_clamp(struct cardboard_params *p) {
p->screen_width_m = clampf(p->screen_width_m, 0.2f, 60.0f);
p->k1 = clampf(p->k1, -1.0f, 2.0f);
p->k2 = clampf(p->k2, -1.0f, 2.0f);
// Optical vertical FOV on supported glasses can be as low as ~22 degrees.
p->fov_deg = clampf(p->fov_deg, 20.0f, 140.0f);
p->lens_inset = clampf(p->lens_inset, -0.25f, 0.25f);
p->chroma = clampf(p->chroma, 0.0f, 0.05f);
@@ -186,6 +185,13 @@ int vkp_cardboard_active(void) {
return on;
}
int vkp_cardboard_source(void) {
pthread_mutex_lock(&g_lock);
const int source = g_set.enabled ? g_set.source : CARDBOARD_SOURCE_SCREEN;
pthread_mutex_unlock(&g_lock);
return source;
}
int vkp_cardboard_pose_changed(void) {
if (!g_changed) return 0;
g_changed = 0;
@@ -27,6 +27,7 @@ void vkp_cardboard_set_render_height(int height);
void vkp_cardboard_bind_device(VkDevice dev, const VkPhysicalDeviceMemoryProperties *memprops);
int vkp_cardboard_active(void);
int vkp_cardboard_source(void);
int vkp_cardboard_pose_changed(void);
void vkp_cardboard_begin_frame(void);
VkImage vkp_cardboard_run(VkCommandBuffer cmd, VkImage src, int src_w, int src_h, int out_w, int out_h);
@@ -1,4 +1,3 @@
// 1115.1.0
#pragma once
const uint32_t cardboard_frag_code[] = {
0x07230203,0x00010000,0x0008000b,0x000001ba,0x00000000,0x00020011,0x00000001,0x0006000b,
File diff suppressed because it is too large. Load diff
@@ -61,3 +61,6 @@ extern volatile int g_no_render_node;
COMPOSITOR_INTERNAL extern dev_t g_main_device;
COMPOSITOR_INTERNAL void bind_dmabuf(struct wl_client *c, void *data, uint32_t ver, uint32_t id);
/* ---- wl_android_buffer.c */
COMPOSITOR_INTERNAL void droiddeck_android_wlegl_init(struct wl_display *display);
@@ -1,5 +1,6 @@
#define _POSIX_C_SOURCE 200809L
#include "effects_chain.h"
#include "blend_pass.h"
#include "vk_loader.h"
#include "vk_present.h"
#include <pthread.h>
@@ -146,6 +147,16 @@ int vkp_effects_active(void) {
return spatial_mode(g_cur.scaling) || effect_count(&g_cur) > 0;
}
int vkp_effects_upscaling(void) {
vkp_effects_sync();
return spatial_mode(g_cur.scaling) && g_cur.scaling != 6;
}
int vkp_effects_postprocessing(void) {
vkp_effects_sync();
return effect_count(&g_cur) > 0;
}
VkFilter vkp_effects_blit_filter(void) {
vkp_effects_sync();
return g_cur.scaling == 2 ? VK_FILTER_NEAREST : VK_FILTER_LINEAR;
@@ -233,6 +244,7 @@ static VkImageView make_view(VkImage img, VkFormat fmt) {
}
static void target_destroy(struct fx_target *t) {
if (t->img) blendp_forget_image(t->img);
if (t->ds) g_vk.FreeDescriptorSets(g_dev, g_dpool, 1, &t->ds);
if (t->fb) g_vk.DestroyFramebuffer(g_dev, t->fb, NULL);
if (t->view) g_vk.DestroyImageView(g_dev, t->view, NULL);
@@ -10,6 +10,9 @@ int vkp_effects_sync(void);
int vkp_effects_active(void);
int vkp_effects_upscaling(void);
int vkp_effects_postprocessing(void);
VkFilter vkp_effects_blit_filter(void);
VkImage vkp_effects_run(VkCommandBuffer cmd, VkImage scene, int scene_w, int scene_h,
@@ -374,16 +374,11 @@ int vkp_framegen_run(VkCommandBuffer cmd, VkImage scene, VkImageView scene_view,
mark_failed(kind);
return 0;
}
/* The ring and the engine's images are new on this frame. Dispatching into them now
* null-dereferenced Turnip (vkp_framegen_run, first Steam frame, 2560x1440 to the panel).
* Show this frame as it is; generation starts once the size has settled. */
if (size_changed || ring == 2) {
FGLOG("frame generation catches up at %dx%d; this frame is shown as it is", w, h);
return 0;
}
/* The pacer's source rate must be the game's, not this loop's: a loop slowed by its own
* generations would otherwise read as a slower game and justify even more generations. */
++g_source_frames;
const uint64_t game_frames = droiddeck_game_frames();
uint32_t n_gen = fge_plan(g_ring_n, game_frames ? game_frames : g_source_frames, g_presented_rate);
@@ -0,0 +1,89 @@
#include "gpu_busy.h"
#include <jni.h>
#include <pthread.h>
#include <stdatomic.h>
#include "gpuload_ring.h"
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
static struct gpuload_vk g_v;
static struct gpuload_ring g_ring;
static uint32_t g_family, g_bits;
static PFN_vkQueueSubmit g_submit;
static int g_was_on;
static _Atomic int g_on, g_lost, g_timing;
static _Atomic uint64_t g_busy_ns;
static VkResult VKAPI_CALL timed_submit(VkQueue queue, uint32_t count, const VkSubmitInfo *infos, VkFence fence) {
if (!count || !atomic_load(&g_on) || atomic_load(&g_lost)) return g_submit(queue, count, infos, fence);
pthread_mutex_lock(&g_lock);
if (!atomic_load(&g_on) || atomic_load(&g_lost)) {
pthread_mutex_unlock(&g_lock);
return g_submit(queue, count, infos, fence);
}
if (g_ring.state == 0 && gpuload_ring_setup(&g_ring, &g_v, g_family, g_bits)) atomic_store(&g_timing, 1);
VkSubmitInfo *mine = NULL;
uint32_t used = 0;
if (g_ring.state == 1) {
uint64_t ns = gpuload_ring_harvest(&g_ring, &g_v);
if (g_was_on) atomic_fetch_add(&g_busy_ns, ns); // the first harvest after a pause holds work timed before it
g_was_on = 1;
mine = gpuload_ring_wrap(&g_ring, count, infos, &used);
}
VkResult r = g_submit(queue, count, mine ? mine : infos, fence);
if (mine && r != VK_SUCCESS) gpuload_ring_unwind(&g_ring, used);
pthread_mutex_unlock(&g_lock);
free(mine);
return r;
}
void vkp_gpu_busy_device(VkPhysicalDevice pd, VkDevice dev, uint32_t family) {
pthread_mutex_lock(&g_lock);
memset(&g_ring, 0, sizeof(g_ring));
g_was_on = 0;
atomic_store(&g_timing, 0);
atomic_store(&g_lost, 0);
PFN_vkGetDeviceProcAddr gdpa = g_vk.GetDeviceProcAddr;
g_v = (struct gpuload_vk){
.device = dev,
.create_pool = g_vk.CreateCommandPool,
.destroy_pool = (PFN_vkDestroyCommandPool)gdpa(dev, "vkDestroyCommandPool"),
.alloc_cmds = g_vk.AllocateCommandBuffers,
.begin = g_vk.BeginCommandBuffer,
.end = g_vk.EndCommandBuffer,
.cmd_reset = (PFN_vkCmdResetQueryPool)gdpa(dev, "vkCmdResetQueryPool"),
.cmd_timestamp = (PFN_vkCmdWriteTimestamp)gdpa(dev, "vkCmdWriteTimestamp"),
.create_queries = (PFN_vkCreateQueryPool)gdpa(dev, "vkCreateQueryPool"),
.destroy_queries = (PFN_vkDestroyQueryPool)gdpa(dev, "vkDestroyQueryPool"),
.results = (PFN_vkGetQueryPoolResults)gdpa(dev, "vkGetQueryPoolResults"),
};
VkPhysicalDeviceProperties props;
g_vk.GetPhysicalDeviceProperties(pd, &props);
gpuload_vk_period(&g_v, props.limits.timestampPeriod);
VkQueueFamilyProperties families[16];
uint32_t n = 16;
g_vk.GetPhysicalDeviceQueueFamilyProperties(pd, &n, families);
g_family = family;
g_bits = family < n ? families[family].timestampValidBits : 0;
// vk_loader_load_device has just put the driver's function back.
if (g_vk.QueueSubmit != timed_submit) g_submit = g_vk.QueueSubmit;
if (g_submit) g_vk.QueueSubmit = timed_submit;
pthread_mutex_unlock(&g_lock);
}
void vkp_gpu_busy_device_lost(void) {
atomic_store(&g_lost, 1);
atomic_store(&g_timing, 0);
}
JNIEXPORT jlong JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeGpuBusyNs(JNIEnv *env, jclass cls) {
return atomic_load(&g_timing) ? (jlong)atomic_load(&g_busy_ns) : -1;
}
JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetGpuTiming(JNIEnv *env, jclass cls, jboolean on) {
pthread_mutex_lock(&g_lock);
if (!on) g_was_on = 0;
atomic_store(&g_on, on ? 1 : 0);
pthread_mutex_unlock(&g_lock);
}
@@ -0,0 +1,9 @@
#ifndef GPU_BUSY_H
#define GPU_BUSY_H
#include "vk_loader.h"
// Times the compositor's own GPU work for the HUD (tools/gpuload/src/gpuload_ring.h); call after the device and its
// functions are loaded. Off until WaylandCompositor.nativeSetGpuTiming(true).
void vkp_gpu_busy_device(VkPhysicalDevice pd, VkDevice dev, uint32_t family);
void vkp_gpu_busy_device_lost(void);
#endif
@@ -103,7 +103,6 @@ static bool prepare_pin(void) {
(off_t)pin.memory << 12);
if (commands == MAP_FAILED) return false;
pin.commands = commands;
// A3xx/A4xx use type-3 CP_NOP; newer cores use type-7 with zero odd-parity bits.
uint32_t *words = commands;
words[0] = pin.nop;
words[1] = 0;
@@ -124,7 +123,6 @@ static void *hold_pin(void *unused) {
if (errno == EINTR || errno == ETIMEDOUT) continue;
break;
}
// Constraints expire after idle_timer (normally 80 ms); renew with a NOP, keeping devfreq alive.
usleep(25000);
if (atomic_load(&running) && !submit_pin()) break;
}
@@ -24,7 +24,6 @@ struct droiddeck_headpose {
float eye_aspect;
uint32_t eye_w, eye_h;
float display_hz;
// Uses a formerly reserved word; older publishers leave this zero.
uint32_t render_height;
uint32_t reserved[10];
};
@@ -0,0 +1,97 @@
#include <fcntl.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include "hudframes.h"
#define HUD_FRAMES_MAGIC 0x46484444u
#define HUD_FRAMES_HEADER 128u
#define HUD_FRAMES_ENTRY 16u
struct hud_frames {
const uint8_t *map;
size_t size;
uint64_t seen;
};
static uint32_t load32(const uint8_t *p) { return __atomic_load_n((const uint32_t *)p, __ATOMIC_ACQUIRE); }
static uint64_t load64(const uint8_t *p) { return __atomic_load_n((const uint64_t *)p, __ATOMIC_ACQUIRE); }
struct hud_frames *hud_frames_open(const char *path) {
int fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0) return NULL;
struct stat st;
void *map = MAP_FAILED;
if (fstat(fd, &st) == 0 && st.st_size >= (off_t)HUD_FRAMES_HEADER)
map = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_SHARED, fd, 0);
close(fd);
if (map == MAP_FAILED) return NULL;
struct hud_frames *h = calloc(1, sizeof(*h));
if (!h) {
munmap(map, (size_t)st.st_size);
return NULL;
}
h->map = map;
h->size = (size_t)st.st_size;
return h;
}
void hud_frames_close(struct hud_frames *h) {
if (!h) return;
munmap((void *)h->map, h->size);
free(h);
}
static uint32_t capacity(const struct hud_frames *h) {
if (load32(h->map) != HUD_FRAMES_MAGIC) return 0;
uint32_t cap;
memcpy(&cap, h->map + 8, 4);
if (cap == 0 || HUD_FRAMES_HEADER + (size_t)cap * HUD_FRAMES_ENTRY > h->size) return 0;
return cap;
}
int hud_frames_poll(struct hud_frames *h, int64_t *frametimes, int64_t *times, int max) {
uint32_t cap = capacity(h);
if (!cap || max <= 0) return -1;
uint64_t count = load64(h->map + 16);
if (count < h->seen) h->seen = 0;
uint64_t from = h->seen;
if (count - from > cap) from = count - cap;
if (count - from > (uint64_t)max) from = count - (uint64_t)max;
int n = 0;
for (uint64_t i = from; i < count; i++, n++) {
const uint8_t *e = h->map + HUD_FRAMES_HEADER + (i % cap) * HUD_FRAMES_ENTRY;
memcpy(&frametimes[n], e, 8);
memcpy(&times[n], e + 8, 8);
}
uint64_t after = load64(h->map + 16);
uint64_t valid = after > cap ? after - cap : 0;
if (from < valid) {
int skip = (int)(valid - from < (uint64_t)n ? valid - from : (uint64_t)n);
memmove(frametimes, frametimes + skip, (size_t)(n - skip) * sizeof(int64_t));
memmove(times, times + skip, (size_t)(n - skip) * sizeof(int64_t));
n -= skip;
}
h->seen = count;
return n;
}
int hud_frames_info(const struct hud_frames *h, int32_t *info, char *engine, size_t engine_len) {
if (!capacity(h) || engine_len == 0) return 0;
for (int tries = 0; tries < 8; tries++) {
uint32_t seq = load32(h->map + 24);
if (seq & 1u) continue;
memcpy(&info[0], h->map + 12, 4);
memcpy(&info[1], h->map + 28, 16);
size_t n = engine_len - 1 < 40 ? engine_len - 1 : 40;
memcpy(engine, h->map + 44, n);
engine[n] = '\0';
__atomic_thread_fence(__ATOMIC_ACQUIRE);
if (__atomic_load_n((const uint32_t *)(h->map + 24), __ATOMIC_RELAXED) == seq) return 1;
}
return 0;
}
@@ -0,0 +1,14 @@
#ifndef DROIDDECK_HUDFRAMES_H
#define DROIDDECK_HUDFRAMES_H
#include <stddef.h>
#include <stdint.h>
struct hud_frames;
struct hud_frames *hud_frames_open(const char *path);
void hud_frames_close(struct hud_frames *h);
int hud_frames_poll(struct hud_frames *h, int64_t *frametimes, int64_t *times, int max);
int hud_frames_info(const struct hud_frames *h, int32_t *info, char *engine, size_t engine_len);
#endif
@@ -0,0 +1,55 @@
#include <jni.h>
#include <stdint.h>
#include <stdlib.h>
#include "hudframes.h"
JNIEXPORT jlong JNICALL
Java_com_droiddeck_launcher_hud_HudFrames_nativeOpen(JNIEnv *env, jclass clazz, jstring path) {
(void)clazz;
const char *p = path ? (*env)->GetStringUTFChars(env, path, NULL) : NULL;
if (!p) return 0;
struct hud_frames *h = hud_frames_open(p);
(*env)->ReleaseStringUTFChars(env, path, p);
return (jlong)(intptr_t)h;
}
JNIEXPORT void JNICALL
Java_com_droiddeck_launcher_hud_HudFrames_nativeClose(JNIEnv *env, jclass clazz, jlong handle) {
(void)env; (void)clazz;
hud_frames_close((struct hud_frames *)(intptr_t)handle);
}
JNIEXPORT jint JNICALL
Java_com_droiddeck_launcher_hud_HudFrames_nativePoll(JNIEnv *env, jclass clazz, jlong handle,
jlongArray frametimes, jlongArray times) {
(void)clazz;
struct hud_frames *h = (struct hud_frames *)(intptr_t)handle;
if (!h || !frametimes || !times) return -1;
jsize max = (*env)->GetArrayLength(env, frametimes);
jsize max2 = (*env)->GetArrayLength(env, times);
if (max2 < max) max = max2;
if (max <= 0) return -1;
int64_t *ft = malloc(sizeof(int64_t) * (size_t)max);
int64_t *tm = malloc(sizeof(int64_t) * (size_t)max);
int n = ft && tm ? hud_frames_poll(h, ft, tm, max) : -1;
if (n > 0) {
(*env)->SetLongArrayRegion(env, frametimes, 0, n, (const jlong *)ft);
(*env)->SetLongArrayRegion(env, times, 0, n, (const jlong *)tm);
}
free(ft);
free(tm);
return n;
}
JNIEXPORT jstring JNICALL
Java_com_droiddeck_launcher_hud_HudFrames_nativeInfo(JNIEnv *env, jclass clazz, jlong handle, jintArray info) {
(void)clazz;
const struct hud_frames *h = (const struct hud_frames *)(intptr_t)handle;
if (!h || !info || (*env)->GetArrayLength(env, info) < 5) return NULL;
int32_t values[5];
char engine[41];
if (!hud_frames_info(h, values, engine, sizeof(engine))) return NULL;
(*env)->SetIntArrayRegion(env, info, 0, 5, (const jint *)values);
return (*env)->NewStringUTF(env, engine);
}
@@ -3,7 +3,6 @@
#include <string.h>
#include <strings.h>
/* Presentation only; this does not select a GPU driver or expand driver support. */
static inline int quest_device_matches(const char *model, const char *device, const char *maker) {
return !strcasecmp(model, "Quest 2") || !strcasecmp(model, "Quest 3") ||
!strcasecmp(model, "Quest 3S") ||
+74 -21
View File
@@ -291,7 +291,7 @@ int sc_layer_base_black(int width, int height) {
ARect src = {0, 0, 16, 16}, dst = {0, 0, width, height};
api.setBuffer(tx, g_base_sc, black, -1);
api.setBufferTransparency(tx, g_base_sc, ASC_TRANSPARENCY_OPAQUE);
api.setZOrder(tx, g_base_sc, 0); /* GAME = 1, OVERLAY = 2 */
api.setZOrder(tx, g_base_sc, 0);
api.setGeometry(tx, g_base_sc, &src, &dst, 0);
api.setVisibility(tx, g_base_sc, ASC_VISIBILITY_SHOW);
api.txApply(tx); api.txDelete(tx);
@@ -381,6 +381,26 @@ static int sniff_modifier(const struct droiddeck_native_handle *h, uint64_t *mod
return 1;
}
#define SC_MOD_GRALLOC (~0ULL - 1)
static const char *pool_kind(uint64_t mod) {
return mod == MOD_QCOM_COMPRESSED ? "UBWC" : mod == SC_MOD_GRALLOC ? "gralloc-laid-out" : "linear";
}
static void describe_handle(const struct droiddeck_native_handle *h, char *out, size_t n) {
size_t o = 0;
out[0] = 0;
if (!h) { snprintf(out, n, "no handle"); return; }
for (int i = 0; i < h->numFds && i < 4 && o < n; i++) {
off_t sz = lseek(h->data[i], 0, SEEK_END);
if (sz >= 0) lseek(h->data[i], 0, SEEK_SET);
o += (size_t)snprintf(out + o, n - o, "%sfd%d %lld bytes", i ? ", " : "", i, (long long)sz);
}
if (o < n) o += (size_t)snprintf(out + o, n - o, "; ints");
for (int i = 0; i < h->numInts && i < 8 && o < n; i++)
o += (size_t)snprintf(out + o, n - o, " %#x", (unsigned)h->data[h->numFds + i]);
}
static int g_alloc_failed;
enum { TIER_UBWC, TIER_PLAIN, TIER_LINEAR };
@@ -416,22 +436,54 @@ static int alloc_slot(struct layer *l, struct slot *s, int w, int h, uint32_t fm
AHardwareBuffer_Desc got; AHardwareBuffer_describe(ahb, &got);
const struct droiddeck_native_handle *nh = api.getNativeHandle(ahb);
uint64_t mod = MOD_LINEAR;
if (!sniff_modifier(nh, &mod)) {
if (tier < TIER_LINEAR) {
struct vkp_image *img = NULL;
const char *path;
char info[160] = "";
if (sniff_modifier(nh, &mod)) {
img = vkp_image_import_dmabuf(nh->data[0], fmt == AHB_RGB10A2 ? DRM_ABGR2101010 : DRM_ABGR8888, mod, w, h,
got.stride * 4, 0, 1);
path = "dma-buf (QTI handle)";
} else {
if (tier == TIER_UBWC) {
char hd[256];
describe_handle(nh, hd, sizeof(hd));
droiddeck_log("layer", "%s: gralloc handle is not a QTI one (%d fds, %d ints: %s): no UBWC request; "
"pool buffers are imported %s", l->name, nh ? nh->numFds : -1, nh ? nh->numInts : -1, hd,
vkp_can_import_ahb() ? "as AHardwareBuffers (the driver takes gralloc's own layout)"
: "as linear dma-bufs (this driver has no AHardwareBuffer import)");
AHardwareBuffer_release(ahb);
l->alloc_tier = TIER_LINEAR;
droiddeck_log("layer", "%s: gralloc handle layout unknown (%d fds, %d ints): using linear pool buffers",
l->name, nh ? nh->numFds : -1, nh ? nh->numInts : -1);
l->alloc_tier = vkp_can_import_ahb() ? TIER_PLAIN : TIER_LINEAR;
continue;
}
mod = MOD_LINEAR;
mod = SC_MOD_GRALLOC;
path = "AHardwareBuffer";
img = vkp_image_import_ahb(ahb, info, sizeof(info));
if (!img && tier == TIER_LINEAR) {
const off_t need = (off_t)got.stride * 4 * h;
int fd = -1, which = -1;
for (int i = 0; nh && i < nh->numFds; i++) {
off_t sz = lseek(nh->data[i], 0, SEEK_END);
if (sz >= 0) lseek(nh->data[i], 0, SEEK_SET);
if (sz >= need) { fd = nh->data[i]; which = i; break; }
}
if (fd < 0 && nh && nh->numFds > 0) { fd = nh->data[0]; which = 0; }
mod = MOD_LINEAR;
path = "linear dma-buf";
img = fd >= 0 ? vkp_image_import_dmabuf(fd, fmt == AHB_RGB10A2 ? DRM_ABGR2101010 : DRM_ABGR8888, mod,
w, h, got.stride * 4, 0, 1) : NULL;
snprintf(info, sizeof(info), "handle fd %d of %d", which, nh ? nh->numFds : 0);
if (vkp_can_import_ahb())
droiddeck_log("layer", "%s: AHardwareBuffer import failed: the linear dma-buf in the handle's fd %d "
"(of %d) -> %s", l->name, which, nh ? nh->numFds : 0, img ? "imported" : "failed too");
}
}
int fd = (nh && nh->numFds > 0) ? nh->data[0] : -1;
struct vkp_image *img = fd >= 0 ? vkp_image_import_dmabuf(fd, fmt == AHB_RGB10A2 ? DRM_ABGR2101010 : DRM_ABGR8888,
mod, w, h, got.stride * 4, 0, 1) : NULL;
if (!img) {
droiddeck_log("layer", "%s: import of a %s %dx%d pool buffer (stride %u px) into the compositor's Turnip failed",
l->name, mod == MOD_QCOM_COMPRESSED ? "UBWC" : "linear", w, h, got.stride);
char hd[256];
describe_handle(nh, hd, sizeof(hd));
droiddeck_log("layer", "%s: import of a %s %dx%d pool buffer (stride %u px, %s, usage %#llx) into the "
"compositor's Turnip failed via %s; gralloc handle %d fds / %d ints: %s", l->name,
pool_kind(mod), w, h, got.stride, tier_name(tier), (unsigned long long)got.usage, path,
nh ? nh->numFds : -1, nh ? nh->numInts : -1, hd);
AHardwareBuffer_release(ahb);
if (tier < TIER_LINEAR) { l->alloc_tier = tier + 1; continue; }
g_alloc_failed = 1;
@@ -442,10 +494,11 @@ static int alloc_slot(struct layer *l, struct slot *s, int w, int h, uint32_t fm
if (l->logged_w != w || l->logged_h != h || l->logged_fmt != fmt || l->logged_mod != mod ||
l->logged_tier != tier) {
l->logged_w = w; l->logged_h = h; l->logged_fmt = fmt; l->logged_mod = mod; l->logged_tier = tier;
droiddeck_log("layer", "%s: pool buffers %dx%d%s: %s (%s), stride %u px, up to %d buffers (gralloc handle %d fds / %d ints)",
l->name, w, h, fmt == AHB_RGB10A2 ? " 10-bit RGBA1010102" : "",
mod == MOD_QCOM_COMPRESSED ? "UBWC (QCOM_COMPRESSED)" : "linear", tier_name(tier),
got.stride, l->pool_n, nh ? nh->numFds : -1, nh ? nh->numInts : -1);
droiddeck_log("layer", "%s: pool buffers %dx%d%s: %s (%s), stride %u px, up to %d buffers (gralloc handle %d fds / "
"%d ints), imported as %s%s%s%s", l->name, w, h, fmt == AHB_RGB10A2 ? " 10-bit RGBA1010102" : "",
mod == MOD_QCOM_COMPRESSED ? "UBWC (QCOM_COMPRESSED)" : pool_kind(mod), tier_name(tier),
got.stride, l->pool_n, nh ? nh->numFds : -1, nh ? nh->numInts : -1, path,
info[0] ? " (" : "", info, info[0] ? ")" : "");
}
return 0;
}
@@ -814,7 +867,7 @@ int sc_layer_present(struct vkp_image *src, int scene_w, int scene_h, const stru
l->first_logged = 1;
droiddeck_log("layer", "presenting %dx%d game frames on their own SurfaceControl layer (%s pool, %d buffers); "
"HUD and pointer stay Android views above it", sw, sh,
l->pool_modifier == MOD_QCOM_COMPRESSED ? "UBWC" : "linear", l->pool_n);
pool_kind(l->pool_modifier), l->pool_n);
}
vkp_signal_first_frame();
return 0;
@@ -836,7 +889,7 @@ int sc_layer_present_pass(const struct vkp_draw *draws, int n, int scene_w, int
l->first_logged = 1;
droiddeck_log("layer", "presenting %dx%d frames on their own SurfaceControl layer (%s pool, %d buffers); "
"HUD and pointer stay Android views above it", rw, rh,
l->pool_modifier == MOD_QCOM_COMPRESSED ? "UBWC" : "linear", l->pool_n);
pool_kind(l->pool_modifier), l->pool_n);
}
vkp_signal_first_frame();
return 0;
@@ -874,12 +927,12 @@ int sc_layer_present_hdr_scene(const struct vkp_draw *draws, int n, const struct
if (tm)
droiddeck_log("color", "tone-mapped picture on the game's display layer: %dx%d, %s 8-bit buffers, untagged "
"(sRGB) - HDR output is switched off", rw, rh,
l->pool_modifier == MOD_QCOM_COMPRESSED ? "UBWC" : "linear");
pool_kind(l->pool_modifier));
else {
char cov[96];
coverage_text(l, cov, sizeof(cov));
droiddeck_log("color", "HDR picture on its own display layer: %dx%d, %s %s buffers, tagged %s%s%s", rw, rh,
l->pool_modifier == MOD_QCOM_COMPRESSED ? "UBWC" : "linear",
pool_kind(l->pool_modifier),
g_hdr_pool_fmt == AHB_RGB10A2 ? "10-bit" : "8-bit",
color && color->dataspace == DROIDDECK_ADATASPACE_BT2020_PQ ? "BT2020_PQ" : "HDR",
cov[0] ? ", " : "", cov);
@@ -973,7 +1026,7 @@ void sc_layer_hide_overlay(void) {
}
void sc_layer_window_gone(void) {
base_hide(1); /* the backdrop belongs to the old window, just like GAME and OVERLAY */
base_hide(1);
if (api.state != 1) return;
layers_init();
for (int i = SC_LAYER_COUNT - 1; i >= 0; i--) retire_sc(&g_layers[i]);
@@ -0,0 +1,47 @@
#ifndef VK_EXTERNAL_IMAGE_SYNC_H
#define VK_EXTERNAL_IMAGE_SYNC_H
#include <vulkan/vulkan.h>
struct vkp_external_client_barrier_pair {
VkImageMemoryBarrier acquire;
VkImageMemoryBarrier release;
};
static inline int vkp_is_external_client_image(int dmabuf, int blit_dst) {
return dmabuf && !blit_dst;
}
/* Client-owned dma-bufs, including AHardwareBuffers, live in GENERAL while
* FOREIGN owns them. Layer pool dma-bufs are destinations with a separate
* acquire/release lifecycle and must never take this source-image path. */
static inline int vkp_external_client_barrier_pair(
VkImage image, uint32_t queue_family, int dmabuf, int blit_dst,
struct vkp_external_client_barrier_pair *pair) {
if (!vkp_is_external_client_image(dmabuf, blit_dst) || !pair) return 0;
const VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
pair->acquire = (VkImageMemoryBarrier){
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.oldLayout = VK_IMAGE_LAYOUT_GENERAL,
.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT,
.dstQueueFamilyIndex = queue_family,
.image = image,
.subresourceRange = range,
.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT,
};
pair->release = (VkImageMemoryBarrier){
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
.srcQueueFamilyIndex = queue_family,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT,
.image = image,
.subresourceRange = range,
.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT,
};
return 1;
}
#endif
+2 -2
View File
@@ -12,7 +12,7 @@
X(CreateAndroidSurfaceKHR) X(DestroySurfaceKHR) X(CreateDevice) \
X(GetDeviceProcAddr) X(EnumerateDeviceExtensionProperties) \
X(GetPhysicalDeviceMemoryProperties) X(GetPhysicalDeviceFormatProperties2) \
X(GetPhysicalDeviceImageFormatProperties2)
X(GetPhysicalDeviceImageFormatProperties2) X(GetPhysicalDeviceProperties2)
#define VK_DEVICE_FUNCS(X) \
X(GetDeviceQueue) X(CreateSwapchainKHR) X(DestroySwapchainKHR) X(GetSwapchainImagesKHR) \
@@ -21,7 +21,7 @@
X(BindImageMemory) X(DestroyImage) X(FreeMemory) X(AcquireNextImageKHR) \
X(ResetCommandBuffer) X(BeginCommandBuffer) X(CmdPipelineBarrier) X(CmdBlitImage) \
X(EndCommandBuffer) X(ResetFences) X(QueueSubmit) X(QueuePresentKHR) X(WaitForFences) \
X(QueueWaitIdle) X(GetMemoryFdPropertiesKHR) \
X(QueueWaitIdle) X(GetMemoryFdPropertiesKHR) X(GetAndroidHardwareBufferPropertiesANDROID) \
X(GetImageSubresourceLayout) X(MapMemory) X(UnmapMemory) X(CmdClearColorImage) \
\
X(CreateShaderModule) X(DestroyShaderModule) X(CreatePipelineLayout) X(DestroyPipelineLayout) \
File diff suppressed because it is too large. Load diff
@@ -1,7 +1,10 @@
#ifndef VK_PRESENT_H
#define VK_PRESENT_H
#include <stdint.h>
#include <stddef.h>
#include <android/native_window.h>
#include <android/hardware_buffer.h>
void vk_present_set_driver(const char *driver_path, const char *library_name,
const char *native_lib_dir);
@@ -16,10 +19,14 @@ enum vkp_align { VKP_ALIGN_CENTER = 0, VKP_ALIGN_TOP = 1, VKP_ALIGN_BOTTOM = 2,
void vk_present_set_scale_mode(int mode, int alignment);
int vkp_game_picture_rect(float rect[4]);
int vkp_output_to_scene(double ox, double oy, double *sx, double *sy);
int vkp_scene_to_output(double sx, double sy, double *ox, double *oy);
void vkp_output_size(int *w, int *h);
struct vkp_image;
int vkp_android_buffer_supported(void);
struct vkp_image *vkp_image_from_android_buffer(AHardwareBuffer *buffer);
#define VKP_MOD_LINEAR 0x0000000000000000ULL
#define VKP_MOD_QCOM_COMPRESSED 0x0500000000000001ULL
#define VKP_MOD_INVALID 0x00ffffffffffffffULL
@@ -32,8 +39,14 @@ struct vkp_image *vkp_image_import_dmabuf(int fd, uint32_t drm_format, uint64_t
int w, int h, uint32_t stride, uint32_t offset,
int as_blit_dst);
int vkp_image_is_dmabuf(const struct vkp_image *img);
struct AHardwareBuffer;
int vkp_can_import_ahb(void);
struct vkp_image *vkp_image_import_ahb(struct AHardwareBuffer *ahb, char *info, size_t info_n);
struct vkp_image *vkp_image_create_shm(int w, int h);
struct AHardwareBuffer;
struct vkp_image *vkp_image_from_ahb(struct AHardwareBuffer *ahb);
void vkp_wait_idle(void);
void vkp_image_upload_shm(struct vkp_image *img, const void *data, int stride);
int vkp_image_width(const struct vkp_image *img);
@@ -53,6 +66,10 @@ int vkp_ready(void);
int vkp_has_window(void);
int vkp_wait_window_request(int timeout_ms);
int vkp_device_lost(void);
/* Compositor thread only. Submitted reads may still be pending: retain imported
* buffer references and withhold DMA-BUF releases until the app process exits.
* This terminal state is distinct from genuine device loss. */
int vkp_completion_unknown(void);
int vkp_render(int scene_w, int scene_h, const struct vkp_draw *draws, int n);
int vkp_render_plain(int scene_w, int scene_h);
@@ -9,6 +9,7 @@
#include <sys/resource.h>
#include <android/log.h>
#include <android/native_window_jni.h>
#include <android/hardware_buffer_jni.h>
#include "vk_present.h"
#include "droiddeck_ext.h"
#include "ahb_swapchain.h"
@@ -24,12 +25,21 @@ extern void droiddeck_wayland_send_pointer(int action, int x, int y);
extern void droiddeck_wayland_send_touch(int action, int pointer_id, int x, int y);
extern int droiddeck_cursor_snapshot(int *out, int cap);
extern int droiddeck_cursor_serial(void);
extern int droiddeck_hud_frame_times(float *out, int len, int64_t *total);
extern void droiddeck_wayland_send_key(int evdev, int state);
extern int droiddeck_focus_takes_touch(void);
extern int droiddeck_cursor_input_pos(int *x, int *y);
extern void droiddeck_wayland_send_scene_input(int type, int a, int b);
extern void droiddeck_wayland_set_cursor_composite(int on);
extern void droiddeck_wayland_vsync(int64_t frame_time_ns);
extern void droiddeck_stream_set(int on);
extern void droiddeck_stream_frame(AHardwareBuffer *ahb);
extern unsigned droiddeck_stream_frames(void);
extern void droiddeck_wayland_output_resized(void);
extern void droiddeck_wayland_set_output_refresh_rate(int mhz);
extern volatile int g_fps_limit;
extern volatile int g_release_unpaced;
extern volatile int g_render_on_commit;
extern volatile int g_hide_shell;
extern volatile int g_zero_copy;
extern volatile unsigned g_zero_copy_last;
@@ -206,6 +216,24 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCompositorTid(JNIEnv
return (jint)g_comp_tid;
}
/* Why presenting failed ("" while it works), for the session-ended screen and the report. */
JNIEXPORT jstring JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCompositorFault(JNIEnv *env, jclass clazz) {
(void)clazz;
extern const char *vkp_fault(void);
const char *f = vkp_fault();
return (*env)->NewStringUTF(env, f ? f : "");
}
/* The display driver as the driver itself reports it ("" before the compositor's device exists). */
JNIEXPORT jstring JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeDisplayDriver(JNIEnv *env, jclass clazz) {
(void)clazz;
extern const char *vkp_display_driver(void);
const char *d = vkp_display_driver();
return (*env)->NewStringUTF(env, d ? d : "");
}
JNIEXPORT void JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeStart(JNIEnv *env, jclass clazz,
jstring xdgRuntimeDir) {
@@ -263,18 +291,58 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCursorSnapshot(
return n;
}
JNIEXPORT jlong JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHudFrameTimes(JNIEnv *env, jclass clazz, jfloatArray out) {
(void)clazz;
if (!out) return 0;
jint cap = (*env)->GetArrayLength(env, out);
float *buf = cap > 0 ? malloc(sizeof(float) * (size_t)cap) : NULL;
if (!buf) return 0;
int64_t total = 0;
int n = droiddeck_hud_frame_times(buf, cap, &total);
if (n > 0) (*env)->SetFloatArrayRegion(env, out, 0, n, buf);
free(buf);
return (jlong)total;
}
JNIEXPORT jint JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCursorSerial(JNIEnv *env, jclass clazz) {
(void)env; (void)clazz;
return droiddeck_cursor_serial();
}
JNIEXPORT void JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCursorComposite(
JNIEnv *env, jclass clazz, jboolean on) {
(void)env; (void)clazz;
droiddeck_wayland_set_cursor_composite(on ? 1 : 0);
}
JNIEXPORT void JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendKey(
JNIEnv *env, jclass clazz, jint evdev, jint state) {
droiddeck_wayland_send_key(evdev, state);
}
JNIEXPORT jboolean JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeFocusTakesTouch(
JNIEnv *env, jclass clazz) {
(void)env; (void)clazz;
return droiddeck_focus_takes_touch() ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT jboolean JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCursorInputPos(
JNIEnv *env, jclass clazz, jintArray out) {
(void)clazz;
if (!out || (*env)->GetArrayLength(env, out) < 2) return JNI_FALSE;
int pos[2];
if (!droiddeck_cursor_input_pos(&pos[0], &pos[1])) return JNI_FALSE;
jint jpos[2] = {pos[0], pos[1]};
(*env)->SetIntArrayRegion(env, out, 0, 2, jpos);
return JNI_TRUE;
}
JNIEXPORT void JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendSceneInput(
JNIEnv *env, jclass clazz, jint type, jint a, jint b) {
@@ -567,9 +635,16 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetLookName(JNIEnv *
free(s);
}
JNIEXPORT void JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetRenderOnCommit(JNIEnv *env, jclass clazz, jboolean on) {
g_render_on_commit = on ? 1 : 0;
__android_log_print(ANDROID_LOG_INFO, TAG, "render on commit %s", on ? "on" : "off");
}
JNIEXPORT void JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetFpsLimit(JNIEnv *env, jclass clazz, jint fps) {
g_fps_limit = fps > 0 ? fps : 0;
g_release_unpaced = fps < 0;
__android_log_print(ANDROID_LOG_INFO, TAG, "fps limit %d", fps);
}
@@ -641,7 +716,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetGpuTurbo(JNIEnv *
jclass error = (*env)->FindClass(env, "java/lang/IllegalStateException");
if (error) (*env)->ThrowNew(env, error, strerror(saved_errno));
}
// Clear a legacy global pin when recovering its marker after an app upgrade or crash.
if (on != JNI_TRUE) adrenotools_set_turbo(false);
}
@@ -667,3 +741,20 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCardboardSpatialP
(*env)->GetFloatArrayRegion(env, pos, 0, 3, p);
vkp_cardboard_set_spatial_pose(m, p, live ? 1 : 0);
}
JNIEXPORT void JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeStreamMode(JNIEnv *env, jclass clazz, jboolean on) {
droiddeck_stream_set(on ? 1 : 0);
}
JNIEXPORT void JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeStreamFrame(JNIEnv *env, jclass clazz, jobject hardwareBuffer) {
if (!hardwareBuffer) return;
AHardwareBuffer *ahb = AHardwareBuffer_fromHardwareBuffer(env, hardwareBuffer);
if (ahb) droiddeck_stream_frame(ahb);
}
JNIEXPORT jint JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeStreamFrames(JNIEnv *env, jclass clazz) {
return (jint)droiddeck_stream_frames();
}
@@ -0,0 +1,216 @@
/* android_wlegl buffer import for the existing libhybris Wayland WSI.
* Protocol: Collabora/libhybris, vendored with its license in protocols/wayland-android.xml.
* Native handles are cloned through Android; opaque vendor integers are never interpreted. */
#include "compositor_internal.h"
#include "droiddeck_ext.h"
#include "wayland-android-server-protocol.h"
#include <android/hardware_buffer.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* Stable native_handle_t ABI, unavailable in the public NDK. */
struct android_native_handle {
int version, num_fds, num_ints;
int data[];
};
struct wire_handle {
struct android_native_handle *handle;
int received;
};
enum { MAX_HANDLE_FDS = 64, MAX_HANDLE_INTS = 256, MAX_BUFFER_SIDE = 8192 };
/* VNDK ABI (not exposed by the NDK): frameworks/native/libs/nativewindow/include/
* vndk/hardware_buffer.h, AHARDWAREBUFFER_CREATE_FROM_HANDLE_METHOD_CLONE = 3.
* Method 1 is not accepted by AHardwareBuffer_createFromHandle. */
enum { CREATE_FROM_HANDLE_CLONE = 3 };
static int (*import_handle)(const AHardwareBuffer_Desc *, const struct android_native_handle *,
int32_t, AHardwareBuffer **);
static const struct android_native_handle *(*native_handle)(const AHardwareBuffer *);
static int valid_description(int32_t width, int32_t height, int32_t stride, int32_t format) {
return width > 0 && height > 0 && width <= MAX_BUFFER_SIDE && height <= MAX_BUFFER_SIDE &&
stride >= width && stride <= MAX_BUFFER_SIDE * 4 &&
(format == 1 || format == 2 || format == 5);
}
static void handle_resource_destroy(struct wl_resource *resource) {
struct wire_handle *wire = wl_resource_get_user_data(resource);
if (!wire) return;
for (int i = 0; i < wire->received; i++) close(wire->handle->data[i]);
free(wire->handle);
free(wire);
}
static void handle_add_fd(struct wl_client *client, struct wl_resource *resource, int32_t fd) {
struct wire_handle *wire = wl_resource_get_user_data(resource);
if (wire->received >= wire->handle->num_fds) {
close(fd);
wl_resource_post_error(resource, ANDROID_WLEGL_HANDLE_ERROR_TOO_MANY_FDS,
"more native handle descriptors than declared");
return;
}
wire->handle->data[wire->received++] = fd;
}
static void handle_destroy(struct wl_client *client, struct wl_resource *resource) {
wl_resource_destroy(resource);
}
static const struct android_wlegl_handle_interface handle_impl = {
.add_fd = handle_add_fd, .destroy = handle_destroy,
};
static void create_handle(struct wl_client *client, struct wl_resource *resource,
uint32_t id, int32_t fds, struct wl_array *ints) {
if (fds < 0 || fds > MAX_HANDLE_FDS || ints->size % sizeof(int32_t) ||
ints->size > MAX_HANDLE_INTS * sizeof(int32_t)) {
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_VALUE, "invalid native handle size");
return;
}
struct wire_handle *wire = calloc(1, sizeof(*wire));
if (!wire) { wl_client_post_no_memory(client); return; }
wire->handle = calloc(1, sizeof(*wire->handle) + (size_t)fds * sizeof(int32_t) + ints->size);
if (!wire->handle) { free(wire); wl_client_post_no_memory(client); return; }
wire->handle->version = sizeof(*wire->handle);
wire->handle->num_fds = fds;
wire->handle->num_ints = (int)(ints->size / sizeof(int32_t));
if (ints->size) memcpy(wire->handle->data + fds, ints->data, ints->size);
struct wl_resource *handle = wl_resource_create(client, &android_wlegl_handle_interface, 1, id);
if (!handle) {
free(wire->handle); free(wire); wl_client_post_no_memory(client); return;
}
wl_resource_set_implementation(handle, &handle_impl, wire, handle_resource_destroy);
}
static void buffer_resource_destroy(struct wl_resource *resource) {
dmabuf_buffer_unref(wl_resource_get_user_data(resource));
}
static struct wl_resource *create_android_buffer(struct wl_client *client, uint32_t id,
AHardwareBuffer *hardware) {
AHardwareBuffer_Desc desc;
AHardwareBuffer_describe(hardware, &desc);
struct vkp_image *image = vkp_image_from_android_buffer(hardware);
if (!image) return NULL;
struct dmabuf_buffer *buffer = calloc(1, sizeof(*buffer));
if (!buffer) { vkp_image_destroy(image); wl_client_post_no_memory(client); return NULL; }
buffer->refs = 1;
buffer->width = (int32_t)desc.width; buffer->height = (int32_t)desc.height;
buffer->format = desc.format == 5 ? DRM_ARGB8888 : desc.format == 2 ? DRM_XBGR8888 : DRM_ABGR8888;
buffer->modifier = MOD_INVALID; /* No DMA-BUF layout was inferred from the native handle. */
buffer->img = image;
for (int i = 0; i < MAX_PLANES; i++) buffer->fd[i] = -1;
struct wl_resource *result = wl_resource_create(client, &wl_buffer_interface, 1, id);
if (!result) { dmabuf_buffer_unref(buffer); wl_client_post_no_memory(client); return NULL; }
wl_resource_set_implementation(result, &dbuf_buffer_impl, buffer, buffer_resource_destroy);
struct client_info *info = client_info_of(client);
if (info) info->dmabuf_buffers++;
return result;
}
static void create_buffer(struct wl_client *client, struct wl_resource *resource, uint32_t id,
int32_t width, int32_t height, int32_t stride, int32_t format,
int32_t usage, struct wl_resource *handle_resource) {
/* Some vendor allocators (Samsung Xclipse) record no stride for GPU-only buffers. The import
* must pass that 0 back unchanged: gralloc validates it against the handle's own value. */
if (!valid_description(width, height, stride ? stride : width, format) ||
!wl_resource_instance_of(handle_resource, &android_wlegl_handle_interface, &handle_impl)) {
droiddeck_log("display", "android_wlegl rejected: %dx%d stride=%d format=%d usage=0x%x",
width, height, stride, format, (uint32_t)usage);
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_VALUE, "invalid Android buffer description");
return;
}
struct wire_handle *wire = wl_resource_get_user_data(handle_resource);
if (wire->received != wire->handle->num_fds || !wire->received) {
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_HANDLE, "incomplete native handle");
return;
}
AHardwareBuffer_Desc desc = {
.width = (uint32_t)width, .height = (uint32_t)height, .layers = 1,
.format = (uint32_t)format, .usage = (uint32_t)usage, .stride = (uint32_t)stride};
AHardwareBuffer *hardware = NULL;
int status = import_handle(&desc, wire->handle, CREATE_FROM_HANDLE_CLONE, &hardware);
if (status != 0 || !hardware) {
droiddeck_log("display", "android_wlegl import failed: status=%d %dx%d stride=%d format=%d usage=0x%x fds=%d ints=%d",
status, width, height, stride, format, (uint32_t)usage,
wire->handle->num_fds, wire->handle->num_ints);
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_HANDLE,
"Android rejected the native handle (status %d)", status);
return;
}
struct wl_resource *buffer = create_android_buffer(client, id, hardware);
AHardwareBuffer_release(hardware);
if (!buffer) wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_HANDLE, "Vulkan cannot import this buffer");
}
static void get_server_buffer(struct wl_client *client, struct wl_resource *resource, uint32_t id,
int32_t width, int32_t height, int32_t format, int32_t usage) {
if (!format) format = 1;
if (!valid_description(width, height, width, format)) {
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_VALUE, "invalid buffer allocation request");
return;
}
AHardwareBuffer_Desc desc = {
.width = (uint32_t)width, .height = (uint32_t)height, .layers = 1,
.format = (uint32_t)format,
.usage = (uint32_t)usage | AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT};
AHardwareBuffer *hardware = NULL;
if (AHardwareBuffer_allocate(&desc, &hardware) != 0 || !hardware) {
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_VALUE, "Android buffer allocation failed");
return;
}
AHardwareBuffer_describe(hardware, &desc);
const struct android_native_handle *handle = native_handle(hardware);
if (!handle || handle->num_fds < 1 || handle->num_fds > MAX_HANDLE_FDS ||
handle->num_ints < 0 || handle->num_ints > MAX_HANDLE_INTS) {
AHardwareBuffer_release(hardware);
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_HANDLE, "invalid allocated native handle");
return;
}
struct wl_resource *buffer = create_android_buffer(client, 0, hardware);
if (!buffer) {
AHardwareBuffer_release(hardware);
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_HANDLE, "Vulkan cannot import allocated buffer");
return;
}
struct wl_resource *reply = wl_resource_create(client, &android_wlegl_server_buffer_handle_interface,
wl_resource_get_version(resource), id);
if (!reply) {
wl_resource_destroy(buffer); AHardwareBuffer_release(hardware);
wl_client_post_no_memory(client); return;
}
struct wl_array ints = {.size = (size_t)handle->num_ints * sizeof(int32_t),
.alloc = 0, .data = (void *)(handle->data + handle->num_fds)};
android_wlegl_server_buffer_handle_send_buffer_ints(reply, &ints);
for (int i = 0; i < handle->num_fds; i++)
android_wlegl_server_buffer_handle_send_buffer_fd(reply, handle->data[i]);
/* No linear_layout event: Android's opaque allocation need not be linear. */
android_wlegl_server_buffer_handle_send_buffer(reply, buffer, (int32_t)desc.format, (int32_t)desc.stride);
wl_resource_destroy(reply);
AHardwareBuffer_release(hardware);
}
static const struct android_wlegl_interface wlegl_impl = {
.create_handle = create_handle, .create_buffer = create_buffer,
.get_server_buffer_handle = get_server_buffer,
};
static void bind_android_wlegl(struct wl_client *client, void *data, uint32_t version, uint32_t id) {
struct wl_resource *resource = wl_resource_create(client, &android_wlegl_interface,
version < 3 ? version : 3, id);
if (!resource) { wl_client_post_no_memory(client); return; }
wl_resource_set_implementation(resource, &wlegl_impl, NULL, NULL);
}
void droiddeck_android_wlegl_init(struct wl_display *display) {
if (!vkp_android_buffer_supported()) return;
void *library = dlopen("libnativewindow.so", RTLD_NOW | RTLD_LOCAL);
if (!library) return;
import_handle = dlsym(library, "AHardwareBuffer_createFromHandle");
native_handle = dlsym(library, "AHardwareBuffer_getNativeHandle");
if (!import_handle || !native_handle) { dlclose(library); return; }
/* Keep the platform entry points valid for the compositor's lifetime. */
if (wl_global_create(display, &android_wlegl_interface, 3, NULL, bind_android_wlegl))
droiddeck_log("display", "android_wlegl v3: Android buffer import available");
}
@@ -3,6 +3,7 @@ package com.droiddeck.launcher
import android.app.Application
import com.droiddeck.launcher.runtime.BwrapSpawner
import com.droiddeck.launcher.session.CrashHandler
import com.droiddeck.launcher.session.EnhancedSessionLogShare
import com.droiddeck.launcher.session.GpuClockPin
class App : Application() {
@@ -16,8 +17,10 @@ class App : Application() {
override fun onCreate() {
super.onCreate()
Thread({ runCatching { EnhancedSessionLogShare.cleanupAbandonedStages(this) } }, "diagnostic-stage-cleanup").start()
com.droiddeck.launcher.ui.Themes.loadGradient(this)
// Helper processes must not initialize the main USB provider or guest graphics context.
com.droiddeck.launcher.ui.BuildWatermark.install(this)
HomeApp.trackRecents(this)
val process = if (android.os.Build.VERSION.SDK_INT >= 28) Application.getProcessName()
else (getSystemService(ACTIVITY_SERVICE) as android.app.ActivityManager)
.runningAppProcesses?.firstOrNull { it.pid == android.os.Process.myPid() }?.processName
@@ -25,8 +28,18 @@ class App : Application() {
if (android.os.Build.VERSION.SDK_INT >= 28) android.webkit.WebView.setDataDirectorySuffix("xbox")
return
}
if (process == "$packageName:vr_recovery" || process == "$packageName:rgb_diagnostic") return
if (process == "$packageName:xboxcloud") {
if (android.os.Build.VERSION.SDK_INT >= 28) android.webkit.WebView.setDataDirectorySuffix("xboxcloud")
return
}
if (process == "$packageName:xcloudnative") {
com.droiddeck.launcher.stores.StoresState.init(this)
return
}
if (process == "$packageName:vr_recovery" || process == "$packageName:rgb_diagnostic" ||
process == "$packageName:shortcuts_sync" || process == "$packageName:bottom_desktop") return
CrashHandler.install(this)
com.droiddeck.launcher.core.SteamIntegration.syncShortcutsListing(this)
com.droiddeck.launcher.session.QuestExperimental.initialize(this)
com.droiddeck.launcher.session.SessionPrefs.applyNtsyncDefault(this)
com.droiddeck.launcher.session.SessionPrefs.settleGpuDriverMode(this)
@@ -37,16 +50,19 @@ class App : Application() {
com.droiddeck.launcher.glasses.Glasses.init(this)
com.droiddeck.launcher.glasses.GlassesMenuMirror.initialize(this)
BwrapSpawner.start(this)
com.droiddeck.launcher.runtime.RootSpawner.start(this)
com.droiddeck.launcher.frontend.GameFileSync.start(this)
com.droiddeck.launcher.stores.StoresState.init(this)
com.droiddeck.launcher.stores.StoresState.register(com.droiddeck.launcher.stores.gog.GogBackend)
com.droiddeck.launcher.stores.StoresState.register(com.droiddeck.launcher.stores.epic.EpicBackend)
com.droiddeck.launcher.stores.StoresState.register(com.droiddeck.launcher.stores.amazon.AmazonBackend)
com.droiddeck.launcher.stores.StoresState.register(com.droiddeck.launcher.stores.itch.ItchBackend)
com.droiddeck.launcher.stores.StoresState.register(com.droiddeck.launcher.stores.gamejolt.GameJoltBackend)
com.droiddeck.launcher.stores.StoresState.register(com.droiddeck.launcher.stores.xbox.XboxBackend)
com.droiddeck.launcher.sync.Syncthing.init(this)
com.droiddeck.launcher.stores.StoresState.register(com.droiddeck.launcher.stores.battlenet.BattleNetBackend)
}
/** Fetches the packs an existing setup already relies on, so the first use after an update does not wait. */
private fun prefetchAddonPacks() {
val app = this
Thread({
@@ -5,8 +5,11 @@ import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.droiddeck.launcher.session.ComponentUpdater
import com.droiddeck.launcher.session.ComponentUpdates
import com.droiddeck.launcher.session.ComponentsManager
import com.droiddeck.launcher.session.ProtonDefault
import com.droiddeck.launcher.session.SessionPrefs
internal class ComponentsMenu(private val activity: android.app.Activity, private val ui: Handler) {
var compSnapshot by mutableStateOf<ComponentsManager.Snapshot?>(null)
@@ -20,6 +23,10 @@ internal class ComponentsMenu(private val activity: android.app.Activity, privat
get() = downloads
private set(value) { downloads = value }
val compDownloadRevision: Long get() = downloadRevision
var compModes by mutableStateOf<Map<String, String>>(emptyMap())
var compAuto by mutableStateOf<Map<String, List<ComponentUpdater.Status>>>(emptyMap())
var compAutoHeld by mutableStateOf<Set<String>>(emptySet())
var compAutoChecking by mutableStateOf(false)
private var shownDefault: String? = null
fun refreshComponents(snapshotFirst: Boolean = false) {
@@ -29,7 +36,14 @@ internal class ComponentsMenu(private val activity: android.app.Activity, privat
val snap = runCatching { ComponentsManager.snapshot(activity) }.onFailure { Log.w(TAG, "components", it) }.getOrNull()
val cat = runCatching { ComponentsManager.catalog(activity, false) }.getOrNull()
val chosen = snap?.let { s -> runCatching { ProtonDefault.selectedId(activity, s.protons.map { it.proton }) }.getOrNull() }
runCatching { ComponentUpdater.settle(activity) }
val modes = ComponentsManager.COMPONENTS.associateWith { SessionPrefs.componentMode(activity, it) }
val auto = ComponentsManager.COMPONENTS.associateWith { runCatching { ComponentUpdater.status(activity, it) }.getOrDefault(emptyList()) }
val held = if (runCatching { ComponentsManager.autoDxvkHeld(activity) }.getOrDefault(false)) setOf("dxvk") else emptySet()
ui.post {
compModes = modes
compAuto = auto
compAutoHeld = held
compSnapshot = snap ?: ComponentsManager.Snapshot(emptyList(), emptyList())
if (cat != null && cat.fetchedAt > 0) { compCatalog = cat.items; compCatalogAt = cat.fetchedAt }
if (chosen != null && chosen != shownDefault) {
@@ -42,6 +56,35 @@ internal class ComponentsMenu(private val activity: android.app.Activity, privat
}, "components").start()
}
fun chooseComponentMode(comp: String, mode: String) {
SessionPrefs.setComponentMode(activity, comp, mode)
compModes = compModes + (comp to mode)
if (mode != ComponentUpdates.AUTO) return
checkComponentUpdates(force = true)
}
private var checkAgain: Boolean? = null
fun checkComponentUpdates(force: Boolean) {
if (compAutoChecking) {
checkAgain = force || checkAgain == true
return
}
compAutoChecking = true
Thread({
val notes = runCatching { ComponentUpdater.ensure(activity, force) }.onFailure { Log.w(TAG, "component updates", it) }.getOrDefault(emptyList())
ui.post {
compAutoChecking = false
checkAgain?.let { again ->
checkAgain = null
checkComponentUpdates(again)
}
if (notes.isNotEmpty()) android.widget.Toast.makeText(activity, notes.joinToString("\n"), android.widget.Toast.LENGTH_LONG).show()
refreshComponents()
}
}, "component-updates").start()
}
fun chooseProton(id: String) {
val proton = compSnapshot?.protons?.firstOrNull { it.proton.id == id }?.proton ?: return
compProton = id
@@ -14,10 +14,12 @@ import androidx.compose.runtime.mutableIntStateOf
import com.droiddeck.launcher.gpu.AutoDriverTrial
import com.droiddeck.launcher.gpu.AutoPicks
import com.droiddeck.launcher.gpu.DriverBundle
import com.droiddeck.launcher.core.DeviceSupport
import com.droiddeck.launcher.gpu.DriverPairs
import com.droiddeck.launcher.gpu.GpuInfo
import com.droiddeck.launcher.gpu.LinuxVulkanDriver
import com.droiddeck.launcher.gpu.LinuxVulkanDriverManager
import com.droiddeck.launcher.gpu.SystemVulkanDriver
import com.droiddeck.launcher.gpu.TurnipDriver
import com.droiddeck.launcher.gpu.TurnipReleases
import com.droiddeck.launcher.session.SessionService
@@ -42,7 +44,8 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
var canRestoreBundled by mutableStateOf(false)
private val releaseProgress = HashMap<String, Int>()
var linuxSelected by mutableStateOf("")
val gpu: GpuInfo = GpuInfo.detect()
var gpu: GpuInfo = GpuInfo.detect()
private set
var mode by mutableStateOf(SessionPrefs.GPU_DRIVERS_AUTO)
var pairRows by mutableStateOf<List<PairRow>>(emptyList())
var pairBusy by mutableStateOf<String?>(null)
@@ -65,20 +68,23 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
}
fun state() = GpuDriversState(
gpuName = gpu.displayName(activity), gpuFamily = gpu.family.label(activity), soc = gpu.soc, supportText = gpu.supportText(activity),
gpuName = gpu.displayName(activity), gpuFamily = if (gpu.name.contains("Xclipse", ignoreCase = true)) gpu.family.label(gpu.name) else gpu.family.label(activity), soc = gpu.soc, supportText = gpu.supportText(activity, com.droiddeck.launcher.core.DeviceSupport.vkBridge(activity)),
supported = gpu.support == GpuInfo.Support.TESTED, unsupported = gpu.support == GpuInfo.Support.UNSUPPORTED,
auto = mode == SessionPrefs.GPU_DRIVERS_AUTO, pairs = pairRows, busy = pairBusy, percent = pairPercent,
autoStatus = autoStatus, releaseStatus = releaseStatus.ifEmpty { activity.getString(R.string.drivers_not_checked) }, checking = releaseChecking,
linuxRows = linuxRows, linuxSelected = linuxSelected, androidRows = androidRows, androidSelected = androidSelected,
linuxDownloads = linuxDownloads, androidDownloads = androidDownloads, canRestoreBundled = canRestoreBundled,
activeBundle = activeBundle,
activeBundle = activeBundle, powerVr = gpu.family == GpuInfo.Family.POWERVR,
systemPowerVr = SystemVulkanDriver.usesDefault(linuxSelected),
mode = uiMode, display = displaySlot(), runtime = runtimeSlot(),
canRestart = !sessionRunning,
checkedAt = TurnipReleases.cached(activity)?.checkedAt ?: 0L,
tpUbwcMode = tpUbwcMode.takeIf { gpu.model == 740 },
tpUbwcRestartRequired = com.droiddeck.launcher.gpu.TpUbwcHint.restartRequired(activity),
)
fun tpUbwcModeForDevice(): String? = tpUbwcMode.takeIf { gpu.model == 740 }
fun tpUbwcRestartRequired(): Boolean = com.droiddeck.launcher.gpu.TpUbwcHint.restartRequired(activity)
fun chooseTpUbwcMode(mode: String) {
if (gpu.model != 740) return
SessionPrefs.setTpUbwcMode(activity, mode)
@@ -123,8 +129,14 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
)
}
var restartBlocked: () -> Boolean = { SessionState.running }
fun loadTpUbwcMode() {
tpUbwcMode = SessionPrefs.tpUbwcMode(activity)
}
fun restartApp() {
if (SessionState.running) return
if (restartBlocked()) return
val launch = activity.packageManager.getLaunchIntentForPackage(activity.packageName) ?: return
activity.startActivity(android.content.Intent.makeRestartActivityTask(launch.component)
.putExtra(com.droiddeck.launcher.stores.download.StoreDownloadService.EXTRA_NAV, "components"))
@@ -142,7 +154,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
DriverRow(id, it.label, activity.getString(if (linux) R.string.drivers_bundle_sets_display else R.string.drivers_bundle_sets_runtime), true, DriverRow.BUNDLE)
}
linuxRows = LinuxVulkanDriver.optionValues(activity).map { id ->
if (id.isEmpty()) DriverRow("", activity.getString(R.string.gpu_runtime_default), activity.getString(R.string.drivers_runtime_builtin), false)
if (id.isEmpty()) DriverRow("", activity.getString(if (gpu.family == GpuInfo.Family.POWERVR) R.string.gpu_system_powervr else R.string.gpu_runtime_default), activity.getString(R.string.drivers_runtime_builtin), false)
else bundleRow(id, linux = true) ?: DriverRow(
id, lm.getDriverName(id),
listOfNotNull(
@@ -177,6 +189,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
fun summary(): String {
val active = pairRows.firstOrNull { it.active }?.name ?: activeBundle
?: if (SystemVulkanDriver.usesDefault(linuxSelected) && androidSelected == TurnipDriver.AUTO) activity.getString(R.string.gpu_system_powervr) else null
val display = displaySlot().name
val base = when {
uiMode == GPU_MODE_AUTO -> activity.getString(R.string.drivers_summary, activity.getString(R.string.common_auto), active ?: display)
@@ -193,17 +206,28 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
val pairs = DriverPairs.from(TurnipReleases.cached(activity))
val recommended = if (QuestExperimental.isSupported()) null else recommendedKey(pairs)
pairRows = pairs.map { p ->
val bundle = p.bundle?.let { installedBundle(it) }
val nad = DriverPairs.isNonAdreno(p.key)
val bundle = p.bundle?.let { if (nad) installedBundleAnyBuild(it) else installedBundle(it) }
val displayId = if (p.bundle != null) bundle?.androidId else p.display?.let { TurnipReleases.installedId(activity, it, td::isInstalled) }
val linuxId = if (p.bundle != null) bundle?.linuxId else p.linux?.let { TurnipReleases.installedId(activity, it, lm::isInstalled) }
val mb = p.assets.sumOf { it.size } / 1_048_576.0
val update = nad && bundle != null && p.bundle?.let { TurnipReleases.updateAvailable(activity, it) } == true
val builtOn = if (nad) p.bundle?.builtOn.orEmpty() else ""
val version = when {
!nad -> p.version
bundle != null -> bundle.version.ifEmpty { p.version }
builtOn.isNotEmpty() -> activity.getString(R.string.drivers_built_on, builtOn)
else -> p.version
}
PairRow(
key = p.key, name = p.name, version = p.version,
key = p.key, name = p.name, version = version, update = if (update) builtOn.takeIf { it.isNotEmpty() }?.let { activity.getString(R.string.drivers_built_on, it) } ?: p.version else null,
detail = if (!p.complete) activity.getString(R.string.drivers_half_published) else activity.getString(R.string.drivers_pair_size, mb),
recommended = p.key == recommended, suits = p.suits(gpu), complete = p.complete,
installed = displayId != null && linuxId != null,
active = displayId != null && linuxId != null &&
SessionPrefs.androidDriver(activity) == displayId && SessionPrefs.linuxDriver(activity) == linuxId,
active = displayId != null && linuxId != null && SessionPrefs.linuxDriver(activity) == linuxId &&
(SessionPrefs.androidDriver(activity) == displayId ||
// A Mali pair under Auto: the display is the phone's own driver.
(gpu.family.mali && DriverPairs.isNonAdreno(p.key) && SessionPrefs.androidDriver(activity) == TurnipDriver.AUTO)),
)
}.sortedByDescending { it.recommended }
}
@@ -237,7 +261,9 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
private fun releasesStale(): Boolean {
val cached = TurnipReleases.cached(activity)
return cached == null || System.currentTimeMillis() - cached.checkedAt > RELEASE_CHECK_INTERVAL_MS ||
cached.assets.none { it.pair.isNotEmpty() }
cached.assets.none { it.pair.isNotEmpty() } ||
// A list checked before this device's open non-Adreno drivers were offered has none of them.
(!DeviceSupport.adreno() && cached.assets.none { DriverPairs.isNonAdreno(it.pair) })
}
fun ensureAuto(force: Boolean) {
@@ -247,9 +273,33 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
if (force) autoStatus = activity.getString(R.string.drivers_auto_waits_for_session)
return
}
if (gpu.family == GpuInfo.Family.POWERVR && !SystemVulkanDriver.isXclipse()) {
// Auto owns both choices. Manual imports remain available when Manual is selected.
SessionPrefs.ensurePowerVrAutoDrivers(activity)
autoStatus = activity.getString(R.string.drivers_powervr_builtin)
refreshDrivers()
return
}
// A Mali pair set by an earlier build put its PanVK Android half on the display, which fails
// on Mali gralloc. Under Auto the display is the phone's own driver (Android's loader).
if (gpu.family.mali) {
// Builds before 0.5.0-nad1 turned the bridge on under Auto without recording it, and
// left it on for good (it wins over the pair). Under Auto that switch is taken as Auto's.
if (!SessionPrefs.maliFallbackKnown(activity))
SessionPrefs.setMaliFallbackByAuto(activity, if (SessionPrefs.vkBridge(activity)) "bridge" else "")
// A lone imported PanVK Android driver set by hand stalls the display the same way
// ("black screen with the start-up logo"), so under Auto any imported display goes.
val display = SessionPrefs.androidDriver(activity)
if (display != TurnipDriver.AUTO) {
SessionPrefs.setAndroidDriver(activity, TurnipDriver.AUTO)
Log.i(TAG, "auto: Mali display back on the phone's own driver (was $display)")
}
}
AutoDriverTrial.rollbackDue(activity)?.let { trial ->
AutoDriverTrial.rollBack(activity, trial)
autoStatus = activity.getString(R.string.drivers_rolled_back, trial.name)
// The drivers before the trial on a Mali are the runtime's Turnip, which finds no GPU there.
if (gpu.family.mali && maliHasNoRoute()) useMaliFallback(trial.name)
refreshDrivers()
return
}
@@ -263,8 +313,31 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
refreshDrivers()
return
}
if (gpu.family.mali && DriverPairs.recommendedKey(gpu, emptyList()) == null &&
com.droiddeck.launcher.gpu.VulkanInfo.cachedOrNull() == null) {
releaseChecking = true
autoStatus = activity.getString(R.string.drivers_checking_latest)
Thread({
com.droiddeck.launcher.gpu.VulkanInfo.query()
ui.post {
releaseChecking = false
gpu = GpuInfo.detect()
Log.i(TAG, "auto: Mali named by its driver: ${gpu.name} -> ${gpu.family}")
lastAutoCheck = 0L
ensureAuto(force)
}
}, "gpu-name").start()
return
}
Log.i(TAG, "auto: ${gpu.name} (${gpu.family}) -> " + (DriverPairs.recommendedKey(gpu, emptyList()) ?: "no pair"))
if (gpu.family.mali && DriverPairs.recommendedKey(gpu, emptyList()) == null) {
// No PanVK build covers this Mali (e.g. the G1 series, arch v14): the phone's own driver.
autoStatus = useMaliFallback(null)
refreshDrivers()
return
}
if (DriverPairs.recommendedKey(gpu, emptyList()) == null) {
autoStatus = activity.getString(R.string.drivers_none_to_set, gpu.supportText(activity).lowercase())
autoStatus = activity.getString(R.string.drivers_none_to_set, gpu.supportText(activity, com.droiddeck.launcher.core.DeviceSupport.vkBridge(activity)).lowercase())
return
}
releaseChecking = true
@@ -275,6 +348,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
runCatching { com.droiddeck.launcher.gpu.VulkanInfo.query() }
ui.post {
releaseChecking = false
if (gpu.family == GpuInfo.Family.POWERVR) gpu = GpuInfo.detect()
refreshReleaseRows()
refreshPairs()
val pairs = DriverPairs.from(TurnipReleases.cached(activity))
@@ -284,10 +358,37 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
when {
row?.active == true -> autoStatus = problem?.let { activity.getString(R.string.drivers_up_to_date_check_failed, it) }
?: activity.getString(R.string.upd_current_status)
gpu.family == GpuInfo.Family.POWERVR && (pair == null || !pair.complete) -> {
// RADV is not published right now: the Xclipse falls back to the system route.
SessionPrefs.ensurePowerVrAutoDrivers(activity)
autoStatus = activity.getString(R.string.drivers_powervr_builtin)
refreshDrivers()
}
gpu.family.mali && (pair == null || !pair.complete) &&
(problem != null || TurnipReleases.cached(activity)?.failed?.contains(TurnipReleases.NON_ADRENO_LABEL) == true) -> {
// The list could not be read: nothing is known to be missing, so a working route stays.
// With no open driver, Venus or bridge the session would run the runtime's Turnip,
// which finds no GPU on a Mali, so the stand-in goes on anyway.
autoStatus = activity.getString(R.string.upd_couldnt_check, problem ?: TurnipReleases.NON_ADRENO_LABEL)
if (maliHasNoRoute()) {
useMaliFallback(null)
refreshDrivers()
}
}
gpu.family.mali && (pair == null || !pair.complete) -> {
// No open driver for this Mali published yet: the phone's own driver until it is.
autoStatus = useMaliFallback(key?.let { DriverPairs.nonAdrenoName(it) } ?: gpu.name)
refreshDrivers()
}
pair == null || !pair.complete ->
autoStatus = problem?.let { activity.getString(R.string.upd_couldnt_check, it) } ?: activity.getString(R.string.drivers_recommended_unpublished)
AutoDriverTrial.release(pair) in AutoDriverTrial.bad(activity) ->
AutoDriverTrial.release(pair) in AutoDriverTrial.bad(activity) -> {
autoStatus = activity.getString(R.string.drivers_skipping_bad, pair.name, pair.version)
if (gpu.family.mali && maliHasNoRoute()) {
useMaliFallback(pair.name)
refreshDrivers()
}
}
SessionState.running -> autoStatus = activity.getString(R.string.drivers_auto_waits_for_session)
holdBack && row?.installed != true -> autoStatus = activity.getString(R.string.drivers_auto_waits_for_network, pair.name, pair.version)
else -> installPair(pair, auto = true)
@@ -296,6 +397,53 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
}, "gpu-driver-auto").start()
}
/**
* A Mali with no open driver pair to install ([missing] names the unpublished pair, or null when
* no PanVK build covers this GPU) runs through the phone's own driver: Mali (Venus) where this
* apk carries it (Steam runs on it from the G57 to the G1 series), the Mali bridge otherwise.
* Venus needs Vulkan 1.2 from the phone's driver; the bridge raises a Vulkan 1.1 driver
* (r32p1 on the G77, G78 and G610) for gamescope itself, so there the bridge stands in.
* The Linux driver choice is left alone (it is the bridge's fallback), and Auto
* switches only its own choice off again once a pair is installed ([endMaliFallback]).
*/
private fun useMaliFallback(missing: String?): String {
val oldVulkan = com.droiddeck.launcher.session.VenusComponent.hostVulkanTooOld(activity)
val venus = oldVulkan == null && com.droiddeck.launcher.session.VenusComponent.available(activity)
if (oldVulkan != null) Log.i(TAG, "auto: Mali driver reports Vulkan $oldVulkan; Venus needs 1.2, the bridge stands in")
if (venus && !SessionPrefs.venus(activity)) {
SessionPrefs.setVenus(activity, true)
SessionPrefs.setMaliFallbackByAuto(activity, "venus")
} else if (!venus && !SessionPrefs.vkBridge(activity)) {
SessionPrefs.setVkBridge(activity, true)
SessionPrefs.setMaliFallbackByAuto(activity, "bridge")
}
val route = activity.getString(if (venus) R.string.drivers_route_venus else R.string.drivers_route_bridge)
Log.i(TAG, "auto: Mali stand-in route $route (missing pair: ${missing ?: "none covers ${gpu.name}"})")
if (oldVulkan != null) return activity.getString(R.string.drivers_mali_old_vulkan_bridge, missing ?: gpu.name, oldVulkan)
return if (missing == null) activity.getString(R.string.drivers_mali_no_pair_auto, route)
else activity.getString(R.string.drivers_mali_unpublished_auto, missing, route)
}
/** Neither an installed open Linux driver nor Venus or the bridge: the runtime's Turnip would run. */
private fun maliHasNoRoute(): Boolean {
if (SessionPrefs.vkBridge(activity)) return false
// Venus on a Vulkan 1.1 driver does not start (VenusComponent.wanted), so it is no route.
if (SessionPrefs.venus(activity) && com.droiddeck.launcher.session.VenusComponent.hostVulkanTooOld(activity) == null) return false
val linux = SessionPrefs.linuxDriver(activity)
return linux.isEmpty() || !LinuxVulkanDriverManager(activity).isInstalled(linux)
}
/** A pair is installed for this Mali: the route Auto switched on in its place goes off again. */
private fun endMaliFallback() {
when (SessionPrefs.maliFallbackByAuto(activity)) {
"venus" -> SessionPrefs.setVenus(activity, false)
"bridge" -> SessionPrefs.setVkBridge(activity, false)
else -> return
}
Log.i(TAG, "auto: pair installed; ${SessionPrefs.maliFallbackByAuto(activity)} (Auto's stand-in) off")
SessionPrefs.setMaliFallbackByAuto(activity, "")
}
fun selectPair(key: String) {
if (pairBusy != null) return
val pair = DriverPairs.from(TurnipReleases.cached(activity)).firstOrNull { it.key == key && it.complete } ?: return
@@ -304,7 +452,15 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
installPair(pair, auto = false)
}
private fun installPair(pair: DriverPairs.DriverPair, auto: Boolean) {
/** A newer build of an installed open non-Adreno pair: replaced in place, still selected if it was. */
fun updatePair(key: String) {
if (pairBusy != null) return
val pair = DriverPairs.from(TurnipReleases.cached(activity)).firstOrNull { it.key == key && it.complete } ?: return
val wasActive = pairRows.firstOrNull { it.key == key }?.active == true
installPair(pair, auto = wasActive && mode == SessionPrefs.GPU_DRIVERS_AUTO, force = true, select = wasActive)
}
private fun installPair(pair: DriverPairs.DriverPair, auto: Boolean, force: Boolean = false, select: Boolean = true) {
if (!pair.complete) return
pairBusy = pair.key
pairPercent = 0
@@ -314,7 +470,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
val result = runCatching {
val zips = pair.assets
val ids = zips.mapIndexed { i, asset ->
installedIdOf(asset)
(if (force) null else installedIdOf(asset))
?: installAsset(asset) { pct -> ui.post { pairPercent = (i * 100 + pct) / zips.size } }.also { downloaded = true }
}
if (pair.bundle != null) DriverBundle.get(activity, ids[0])!!.let { listOf(it.androidId, it.linuxId) } else ids
@@ -322,23 +478,39 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
ui.post {
pairBusy = null
pairPercent = -1
result.onSuccess { (displayId, linuxId) ->
result.onSuccess { (pairDisplayId, linuxId) ->
if (!select) {
android.widget.Toast.makeText(activity, activity.getString(R.string.drivers_pair_updated, pair.name), android.widget.Toast.LENGTH_LONG).show()
return@onSuccess
}
if (auto && SessionState.running) {
lastAutoCheck = 0L
autoStatus = activity.getString(R.string.drivers_auto_waits_for_session)
return@onSuccess
}
// A Mali pair under Auto: the compositor draws on the phone's own Mali driver
// (dma-buf, modifiers and AHB all work there) and the guest runs the pair's
// PanVK. Its Android half stays installed for a manual pick.
// The same when the pair is picked by hand: a PanVK Android half on the display
// stalls Mali gralloc ("Waiting for Steam's first frame"); it stays a manual pick
// under "Runtime and display drivers separately".
val displayId = if (gpu.family.mali && DriverPairs.isNonAdreno(pair.key)) TurnipDriver.AUTO else pairDisplayId
val previousDisplay = SessionPrefs.androidDriver(activity)
val previousLinux = SessionPrefs.linuxDriver(activity)
val displayChanged = previousDisplay != displayId
SessionPrefs.setAndroidDriver(activity, displayId)
SessionPrefs.setLinuxDriver(activity, linuxId)
// A route Auto switched on in place of a missing pair goes off now that one is
// here. A Mali bridge or Venus switch the user set stays: it wins for the session
// (DeviceSupport.vkBridge, VenusComponent.wanted) and the pair waits.
if (gpu.family.mali && DriverPairs.isNonAdreno(pair.key)) endMaliFallback()
val restart = displayChanged && CompositorHost.isStarted
if (auto) {
val trial = AutoDriverTrial.current(activity)
val lastGood = trial?.let { setOf(it.previousDisplay, it.previousLinux) } ?: setOf(previousDisplay, previousLinux)
val changed = previousDisplay != displayId || previousLinux != linuxId
SessionPrefs.setGpuAutoInstalled(activity, SessionPrefs.gpuAutoInstalled(activity) + setOf(displayId, linuxId))
// A Mali pair's display is the phone's own driver (AUTO): not a download to own or prune.
SessionPrefs.setGpuAutoInstalled(activity, SessionPrefs.gpuAutoInstalled(activity) + setOf(displayId, linuxId).filter { it.isNotEmpty() && it != TurnipDriver.AUTO })
if (changed) AutoDriverTrial.begin(
activity, AutoDriverTrial.release(pair), "${pair.name} ${pair.version}", displayId, linuxId,
trial?.previousDisplay ?: previousDisplay, trial?.previousLinux ?: previousLinux,
@@ -366,6 +538,9 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
private fun installedBundle(asset: TurnipReleases.Asset): DriverBundle.Bundle? =
TurnipReleases.installedId(activity, asset) { DriverBundle.get(activity, it) != null }?.let { DriverBundle.get(activity, it) }
private fun installedBundleAnyBuild(asset: TurnipReleases.Asset): DriverBundle.Bundle? =
TurnipReleases.installedIdAnyBuild(activity, asset) { DriverBundle.get(activity, it) != null }?.let { DriverBundle.get(activity, it) }
private fun installedIdOf(asset: TurnipReleases.Asset): String? = when {
asset.bundle -> installedBundle(asset)?.id
asset.linux -> TurnipReleases.installedId(activity, asset, LinuxVulkanDriverManager(activity)::isInstalled)
@@ -428,11 +603,19 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
}
fun selectDriver(id: String, linux: Boolean) {
if (!linux && id == TurnipDriver.AUTO) {
DriverBundle.selectDisplay(activity, id)
pairModeOpen = false
lastAutoCheck = 0L
setMode(auto = true)
refreshDrivers()
return
}
val bundle = DriverBundle.containing(activity, id, linux)
when {
bundle != null -> useBundle(bundle)
linux -> SessionPrefs.setLinuxDriver(activity, id)
else -> SessionPrefs.setAndroidDriver(activity, id)
else -> DriverBundle.selectDisplay(activity, id)
}
pairModeOpen = false
setMode(auto = false, mix = true)
File diff suppressed because it is too large. Load diff
@@ -13,7 +13,7 @@ class HomeActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestedOrientation = com.droiddeck.launcher.session.SessionPrefs.orientation(this)
requestedOrientation = com.droiddeck.launcher.core.AppUiPrefs.orientation(this)
routeHome()
}
@@ -6,20 +6,44 @@ import android.graphics.Bitmap
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.LauncherApps
import android.content.pm.ShortcutInfo
import android.graphics.Rect
import android.os.Process
import android.os.UserHandle
import android.os.UserManager
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.provider.Settings
import androidx.core.graphics.drawable.toBitmap
import com.droiddeck.launcher.session.SessionService
import com.droiddeck.launcher.session.SessionState
import kotlin.math.roundToInt
object HomeApp {
private const val ICON_DP = 72
private const val GAME_SPACE = "cn.nubia.gamelauncher"
private const val GAME_SPACE_ADD = "cn.nubia.gamelauncher.action.GAME_MANAGER"
private val GAME_SPACE_GAMES = Uri.parse("content://cn.nubia.gamelauncher.db.AppAddProvider/appadd")
data class LaunchableApp(
val label: String,
val packageName: String,
val className: String,
val icon: Bitmap?,
)
val system: Boolean = false,
val game: Boolean = false,
val installedAt: Long = 0L,
val updatedAt: Long = 0L,
val user: UserHandle? = null,
val userSerial: Long = 0L,
) {
val component get() = ComponentName(packageName, className)
val key get() = "$packageName/$className" + if (userSerial != 0L) "#$userSerial" else ""
val work get() = userSerial != 0L
}
fun isHomeScreenEnabled(context: Context): Boolean =
context.packageManager.getComponentEnabledSetting(ComponentName(context, HomeActivity::class.java)) ==
@@ -33,6 +57,24 @@ object HomeApp {
)
}
fun hasGameSpace(context: Context): Boolean =
runCatching { context.packageManager.getPackageInfo(GAME_SPACE, 0) }.isSuccess
fun inGameSpace(context: Context): Boolean =
runCatching {
context.contentResolver.query(GAME_SPACE_GAMES, arrayOf("component"), null, null, null)?.use { rows ->
val prefix = context.packageName + ","
generateSequence { if (rows.moveToNext()) rows.getString(0) else null }.any { it.startsWith(prefix) }
}
}.getOrNull() ?: true
fun gameSpaceAddIntent(): Intent = Intent(GAME_SPACE_ADD).setPackage(GAME_SPACE)
fun releaseUnusedHomeEntry(context: Context) {
if (isHomeScreenEnabled(context) && !isDefault(context) && hasGameSpace(context))
setHomeScreenEnabled(context, false)
}
fun isDefault(context: Context): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val roles = context.getSystemService(RoleManager::class.java)
@@ -69,33 +111,133 @@ object HomeApp {
fun settingsIntent(): Intent = Intent(Settings.ACTION_HOME_SETTINGS)
fun launchableApps(context: Context): List<LaunchableApp> {
val launcher = context.getSystemService(LauncherApps::class.java)
val users = context.getSystemService(UserManager::class.java)
val pm = context.packageManager
val launch = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)
val apps = pm.queryIntentActivities(launch, 0)
.asSequence()
.mapNotNull { result ->
val activity = result.activityInfo ?: return@mapNotNull null
if (!activity.exported || activity.packageName == context.packageName) return@mapNotNull null
val label = result.loadLabel(pm)?.toString()?.trim().orEmpty().ifEmpty { activity.packageName }
val icon = runCatching { result.loadIcon(pm).toBitmap(48, 48) }.getOrNull()
LaunchableApp(label, activity.packageName, activity.name, icon)
val own = Process.myUserHandle()
val iconPx = (ICON_DP * context.resources.displayMetrics.density).roundToInt()
val apps = users.userProfiles.flatMap { user ->
val serial = if (user == own) 0L else users.getSerialNumberForUser(user)
runCatching { launcher.getActivityList(null, user) }.getOrDefault(emptyList()).mapNotNull { info ->
val component = info.componentName
if (component.packageName == context.packageName && user == own) return@mapNotNull null
val label = info.label?.toString()?.trim().orEmpty().ifEmpty { component.packageName }
val icon = runCatching { info.getBadgedIcon(0).toBitmap(iconPx, iconPx) }.getOrNull()
val app = info.applicationInfo
val updated = if (user == own) runCatching { pm.getPackageInfo(component.packageName, 0).lastUpdateTime }.getOrNull() else null
@Suppress("DEPRECATION")
val game = app.category == ApplicationInfo.CATEGORY_GAME || app.flags and ApplicationInfo.FLAG_IS_GAME != 0
LaunchableApp(
label, component.packageName, component.className, icon,
system = app.flags and (ApplicationInfo.FLAG_SYSTEM or ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0,
game = game,
installedAt = info.firstInstallTime,
updatedAt = updated ?: info.firstInstallTime,
user = user,
userSerial = serial,
)
}
.distinctBy { it.packageName }
.toList()
return apps.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.label })
}
return apps.distinctBy { it.key }.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.label })
}
fun launch(context: Context, app: LaunchableApp, displayId: Int? = null) {
val intent = Intent(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_LAUNCHER)
.setComponent(ComponentName(app.packageName, app.className))
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
if (displayId == null) {
context.startActivity(intent)
return
fun launch(context: Context, app: LaunchableApp, displayId: Int? = null, bounds: Rect? = null) {
val options = displayId?.let { ActivityOptions.makeBasic().setLaunchDisplayId(it).toBundle() }
val launcher = context.getSystemService(LauncherApps::class.java)
launcher.startMainActivity(app.component, app.user ?: Process.myUserHandle(), bounds, options)
}
fun canUseShortcuts(context: Context): Boolean =
runCatching { context.getSystemService(LauncherApps::class.java).hasShortcutHostPermission() }.getOrDefault(false)
fun shortcuts(context: Context, app: LaunchableApp): List<ShortcutInfo> {
if (!canUseShortcuts(context)) return emptyList()
val query = LauncherApps.ShortcutQuery()
.setPackage(app.packageName)
.setActivity(app.component)
.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST)
return runCatching { context.getSystemService(LauncherApps::class.java).getShortcuts(query, app.user ?: Process.myUserHandle()) }
.getOrNull().orEmpty()
.filter { it.isEnabled }
.sortedWith(compareBy<ShortcutInfo> { if (it.isDeclaredInManifest) 0 else 1 }.thenBy { it.rank })
.take(5)
}
fun shortcutIcon(context: Context, info: ShortcutInfo, px: Int = (ICON_DP * context.resources.displayMetrics.density).roundToInt()): Bitmap? = runCatching {
context.getSystemService(LauncherApps::class.java)
.getShortcutIconDrawable(info, context.resources.displayMetrics.densityDpi)?.toBitmap(px, px)
}.getOrNull()
fun trackRecents(app: android.app.Application) {
app.registerActivityLifecycleCallbacks(object : android.app.Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: android.app.Activity, savedInstanceState: android.os.Bundle?) = Unit
override fun onActivityStarted(activity: android.app.Activity) = Unit
override fun onActivityResumed(activity: android.app.Activity) = syncRecents(activity)
override fun onActivityPaused(activity: android.app.Activity) { if (activity is FrontEndActivity) syncRecents(activity) }
override fun onActivityStopped(activity: android.app.Activity) = Unit
override fun onActivitySaveInstanceState(activity: android.app.Activity, outState: android.os.Bundle) = Unit
override fun onActivityDestroyed(activity: android.app.Activity) = Unit
})
}
private fun syncRecents(activity: android.app.Activity) =
keepOutOfRecents(activity, activity is FrontEndActivity && !SessionState.running && isDefault(activity))
internal enum class RecentsStep { HIDE, SHOW, FORGET, NONE }
internal fun recentsStep(hidden: Boolean, owned: Boolean, excluded: Boolean): RecentsStep = when {
hidden && !excluded -> RecentsStep.HIDE
!hidden && owned && excluded -> RecentsStep.SHOW
!hidden && owned -> RecentsStep.FORGET
else -> RecentsStep.NONE
}
fun keepOutOfRecents(activity: android.app.Activity, hidden: Boolean) {
val prefs = activity.getSharedPreferences("home-recents", Context.MODE_PRIVATE)
val owned = prefs.getStringSet("tasks", null).orEmpty()
val id = activity.taskId.toString()
if (!hidden && id !in owned) return
runCatching {
@Suppress("DEPRECATION")
fun idOf(task: android.app.ActivityManager.AppTask) =
if (Build.VERSION.SDK_INT >= 29) task.taskInfo.taskId else task.taskInfo.persistentId
val tasks = activity.getSystemService(android.app.ActivityManager::class.java).appTasks
val task = tasks.firstOrNull { idOf(it) == activity.taskId } ?: return
val excluded = task.taskInfo.baseIntent.flags and Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS != 0
val step = recentsStep(hidden, id in owned, excluded)
if (step == RecentsStep.NONE) return
if (step != RecentsStep.FORGET) task.setExcludeFromRecents(step == RecentsStep.HIDE)
val live = tasks.map { idOf(it).toString() }.toSet()
val next = owned.filter { it in live && it != id }.toMutableSet()
if (step == RecentsStep.HIDE) next += id
prefs.edit().putStringSet("tasks", next).apply()
}
val options = ActivityOptions.makeBasic().setLaunchDisplayId(displayId)
context.startActivity(intent, options.toBundle())
}
fun startShortcut(context: Context, info: ShortcutInfo) {
val task = generateSequence(context) { (it as? android.content.ContextWrapper)?.baseContext }
.filterIsInstance<android.app.Activity>().firstOrNull()?.taskId
task?.let { com.droiddeck.launcher.ui.DualScreenPanel.yieldTo(it, true) }
try {
context.getSystemService(LauncherApps::class.java).startShortcut(info, null, null)
} catch (e: Exception) {
task?.let { com.droiddeck.launcher.ui.DualScreenPanel.yieldTo(it, false) }
throw e
}
}
fun openAppInfo(context: Context, app: LaunchableApp) {
context.getSystemService(LauncherApps::class.java)
.startAppDetailsActivity(app.component, app.user ?: Process.myUserHandle(), null, null)
}
fun canUninstall(app: LaunchableApp): Boolean = !app.system && !app.work
fun uninstall(context: Context, app: LaunchableApp) {
context.startActivity(
Intent(Intent.ACTION_DELETE, Uri.fromParts("package", app.packageName, null))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
)
}
fun isActiveSteamSession(): Boolean =
@@ -0,0 +1,65 @@
package com.droiddeck.launcher
import android.content.Context
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import org.json.JSONArray
import org.json.JSONObject
import java.util.UUID
object HomeFolders {
data class Folder(val id: String, val name: String, val apps: List<String>)
private const val PREFS = "home_launcher"
private const val KEY = "folders"
var folders by mutableStateOf<List<Folder>>(emptyList())
private set
private var loaded = false
fun load(context: Context) {
if (loaded) return
loaded = true
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY, null) ?: return
folders = runCatching {
val array = JSONArray(raw)
(0 until array.length()).map { i ->
val o = array.getJSONObject(i)
val apps = o.getJSONArray("apps")
Folder(o.getString("id"), o.getString("name"), (0 until apps.length()).map { apps.getString(it) })
}
}.getOrDefault(emptyList())
}
fun create(context: Context, name: String, appKey: String): Folder {
val folder = Folder(UUID.randomUUID().toString(), name, listOf(appKey))
update(context, without(appKey) + folder)
return folder
}
fun add(context: Context, folderId: String, appKey: String) =
update(context, without(appKey).map { if (it.id == folderId) it.copy(apps = it.apps + appKey) else it })
fun remove(context: Context, appKey: String) = update(context, without(appKey))
fun rename(context: Context, folderId: String, name: String) =
update(context, folders.map { if (it.id == folderId) it.copy(name = name) else it })
fun delete(context: Context, folderId: String) = update(context, folders.filterNot { it.id == folderId })
fun migrateMembers(context: Context, keyFor: (String) -> String?) {
if (folders.none { f -> f.apps.any { '/' !in it } }) return
update(context, folders.map { f -> f.copy(apps = f.apps.map { if ('/' in it) it else keyFor(it) ?: it }.distinct()) })
}
private fun without(appKey: String): List<Folder> =
folders.map { it.copy(apps = it.apps - appKey) }.filter { it.apps.isNotEmpty() }
private fun update(context: Context, next: List<Folder>) {
folders = next
val array = JSONArray()
for (f in next) array.put(JSONObject().put("id", f.id).put("name", f.name).put("apps", JSONArray(f.apps)))
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putString(KEY, array.toString()).apply()
}
}
@@ -0,0 +1,26 @@
package com.droiddeck.launcher
import android.content.Context
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
object HomeHidden {
private const val PREFS = "home_launcher"
private const val KEY = "hidden"
var keys by mutableStateOf<Set<String>>(emptySet())
private set
private var loaded = false
fun load(context: Context) {
if (loaded) return
loaded = true
keys = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getStringSet(KEY, emptySet()).orEmpty().toSet()
}
fun set(context: Context, key: String, hidden: Boolean) {
keys = if (hidden) keys + key else keys - key
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putStringSet(KEY, keys).apply()
}
}
@@ -0,0 +1,74 @@
package com.droiddeck.launcher
import android.content.Context
import android.content.pm.LauncherApps
import android.content.pm.ShortcutInfo
import android.graphics.Bitmap
import android.os.Handler
import android.os.Looper
import android.os.Process
import android.os.UserManager
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
object HomePins {
class Pinned(val info: ShortcutInfo, val icon: Bitmap?, val appLabel: String, val userSerial: Long) {
val key get() = "pin:${info.`package`}/${info.id}" + if (userSerial != 0L) "#$userSerial" else ""
val label get() = info.shortLabel?.toString()?.trim().orEmpty().ifEmpty { appLabel }
val work get() = userSerial != 0L
}
var pinned by mutableStateOf<List<Pinned>>(emptyList())
private set
private val main = Handler(Looper.getMainLooper())
private var listing = 0
fun refresh(context: Context) {
val app = context.applicationContext
val seq = ++listing
Thread({
val list = runCatching { load(app) }.getOrDefault(emptyList())
main.post { if (seq == listing) pinned = list }
}, "home-pins").start()
}
private fun load(context: Context): List<Pinned> {
if (!HomeApp.canUseShortcuts(context)) return emptyList()
val launcher = context.getSystemService(LauncherApps::class.java)
val users = context.getSystemService(UserManager::class.java)
val pm = context.packageManager
val own = Process.myUserHandle()
val query = LauncherApps.ShortcutQuery().setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED)
return users.userProfiles.flatMap { user ->
val serial = if (user == own) 0L else users.getSerialNumberForUser(user)
runCatching { launcher.getShortcuts(query, user) }.getOrNull().orEmpty()
.filter { it.isEnabled && it.isPinned }
.map { info ->
val appLabel = runCatching { pm.getApplicationLabel(pm.getApplicationInfo(info.`package`, 0)).toString() }
.getOrDefault(info.`package`)
Pinned(info, HomeApp.shortcutIcon(context, info), appLabel, serial)
}
}
}
fun accept(context: Context, request: LauncherApps.PinItemRequest): Boolean {
val ok = runCatching { request.isValid && request.accept() }.getOrDefault(false)
if (ok) refresh(context)
return ok
}
fun unpin(context: Context, pinned: Pinned) {
val info = pinned.info
val launcher = context.getSystemService(LauncherApps::class.java)
val query = LauncherApps.ShortcutQuery().setPackage(info.`package`).setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED)
val unpinned = runCatching {
val current = checkNotNull(launcher.getShortcuts(query, info.userHandle))
val keep = current.map { it.id } - info.id
launcher.pinShortcuts(info.`package`, keep, info.userHandle)
}.isSuccess
if (!unpinned) return
listing++
this.pinned = this.pinned.filterNot { it.key == pinned.key }
}
}
@@ -0,0 +1,316 @@
package com.droiddeck.launcher
import android.app.Activity
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetHostView
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.Context
import android.content.Intent
import android.content.pm.LauncherApps
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.SizeF
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.ViewConfiguration
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.graphics.drawable.toBitmap
import org.json.JSONArray
import org.json.JSONObject
import kotlin.math.hypot
import kotlin.math.roundToInt
object HomeWidgets {
data class Placed(val id: Int, val widthDp: Int, val heightDp: Int)
data class Provider(
val info: AppWidgetProviderInfo,
val label: String,
val appLabel: String,
val preview: Bitmap?,
val widthDp: Int,
val heightDp: Int,
)
private const val HOST_ID = 0x0DDE
private const val PREFS = "home_launcher"
private const val KEY = "widgets"
private const val REQUEST_BIND = 0x0DD1
private const val REQUEST_CONFIGURE = 0x0DD2
private const val REQUEST_RECONFIGURE = 0x0DD3
const val MIN_WIDTH_DP = 96
const val MIN_HEIGHT_DP = 56
var widgets by mutableStateOf<List<Placed>>(emptyList())
private set
var providers by mutableStateOf<List<Provider>?>(null)
private set
private var loaded = false
private var host: Host? = null
private var pending: AppWidgetProviderInfo? = null
private var pendingId = AppWidgetManager.INVALID_APPWIDGET_ID
private var pinRequest: LauncherApps.PinItemRequest? = null
private val main = Handler(Looper.getMainLooper())
private class Host(context: Context) : AppWidgetHost(context, HOST_ID) {
override fun onCreateView(context: Context, appWidgetId: Int, appWidget: AppWidgetProviderInfo?): AppWidgetHostView =
HomeWidgetView(context)
}
class HomeWidgetView(context: Context) : AppWidgetHostView(context) {
var onLongPress: (() -> Unit)? = null
private var fired = false
private var downX = 0f
private var downY = 0f
private val slop = ViewConfiguration.get(context).scaledTouchSlop
private val longPress = Runnable {
fired = true
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
onLongPress?.invoke()
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
when (ev.actionMasked) {
MotionEvent.ACTION_DOWN -> {
fired = false
downX = ev.x
downY = ev.y
postDelayed(longPress, ViewConfiguration.getLongPressTimeout().toLong())
}
MotionEvent.ACTION_MOVE -> if (hypot(ev.x - downX, ev.y - downY) > slop) removeCallbacks(longPress)
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_POINTER_DOWN -> removeCallbacks(longPress)
}
return fired
}
override fun onTouchEvent(event: MotionEvent): Boolean {
if (fired) {
if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) fired = false
return true
}
return super.onTouchEvent(event)
}
}
private fun host(context: Context): Host = host ?: Host(context.applicationContext).also { host = it }
fun load(context: Context) {
if (loaded) return
loaded = true
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY, null) ?: return
val manager = AppWidgetManager.getInstance(context)
val parsed = runCatching {
val array = JSONArray(raw)
(0 until array.length()).map { i ->
val o = array.getJSONObject(i)
Placed(o.getInt("id"), o.getInt("w"), o.getInt("h"))
}
}.getOrDefault(emptyList())
val live = parsed.filter { manager.getAppWidgetInfo(it.id) != null }
parsed.filterNot { it in live }.forEach { host(context).deleteAppWidgetId(it.id) }
widgets = live
if (live.size != parsed.size) save(context)
}
fun startListening(context: Context) {
load(context)
runCatching { host(context).startListening() }
}
fun stopListening(context: Context) {
runCatching { host(context).stopListening() }
}
fun info(context: Context, id: Int): AppWidgetProviderInfo? = AppWidgetManager.getInstance(context).getAppWidgetInfo(id)
fun createView(context: Context, id: Int): HomeWidgetView? {
val info = info(context, id) ?: return null
return host(context).createView(context, id, info) as? HomeWidgetView
}
fun applySize(view: AppWidgetHostView, widthDp: Int, heightDp: Int) {
if (Build.VERSION.SDK_INT >= 31) {
view.updateAppWidgetSize(Bundle(), listOf(SizeF(widthDp.toFloat(), heightDp.toFloat())))
} else {
@Suppress("DEPRECATION")
view.updateAppWidgetSize(null, widthDp, heightDp, widthDp, heightDp)
}
}
fun loadProviders(context: Context) {
if (providers != null) return
val app = context.applicationContext
Thread({
val pm = app.packageManager
val manager = AppWidgetManager.getInstance(app)
val dpi = app.resources.displayMetrics.densityDpi
val list = manager.installedProviders.mapNotNull { info ->
runCatching {
val appLabel = runCatching { pm.getApplicationLabel(pm.getApplicationInfo(info.provider.packageName, 0)).toString() }
.getOrDefault(info.provider.packageName)
val drawable = info.loadPreviewImage(app, dpi) ?: info.loadIcon(app, dpi)
val preview = drawable?.let { d ->
val w = d.intrinsicWidth.takeIf { it > 0 } ?: 192
val h = d.intrinsicHeight.takeIf { it > 0 } ?: 192
val scale = minOf(1f, 360f / maxOf(w, h))
d.toBitmap((w * scale).roundToInt().coerceAtLeast(1), (h * scale).roundToInt().coerceAtLeast(1))
}
val (w, h) = defaultSize(app, info)
Provider(info, info.loadLabel(pm)?.toString()?.trim().orEmpty().ifEmpty { appLabel }, appLabel, preview, w, h)
}.getOrNull()
}.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, Provider::appLabel).thenBy(String.CASE_INSENSITIVE_ORDER, Provider::label))
main.post { providers = list }
}, "widget-providers").start()
}
fun refreshProviders(context: Context) {
providers = null
loadProviders(context)
}
private fun defaultSize(context: Context, info: AppWidgetProviderInfo): Pair<Int, Int> {
val density = context.resources.displayMetrics.density
var w = (info.minWidth / density).roundToInt()
var h = (info.minHeight / density).roundToInt()
if (Build.VERSION.SDK_INT >= 31) {
if (info.targetCellWidth > 0) w = maxOf(w, info.targetCellWidth * 80)
if (info.targetCellHeight > 0) h = maxOf(h, info.targetCellHeight * 80)
}
return w.coerceIn(160, 560) to h.coerceIn(MIN_HEIGHT_DP, 360)
}
fun minSize(context: Context, id: Int): Pair<Int, Int> {
val info = info(context, id) ?: return MIN_WIDTH_DP to MIN_HEIGHT_DP
val density = context.resources.displayMetrics.density
val w = (if (info.minResizeWidth > 0) info.minResizeWidth else info.minWidth) / density
val h = (if (info.minResizeHeight > 0) info.minResizeHeight else info.minHeight) / density
return w.roundToInt().coerceAtLeast(MIN_WIDTH_DP) to h.roundToInt().coerceAtLeast(MIN_HEIGHT_DP)
}
fun add(activity: Activity, provider: Provider) {
if (pendingId == AppWidgetManager.INVALID_APPWIDGET_ID) add(activity, provider.info, null)
}
fun addPinned(activity: Activity, request: LauncherApps.PinItemRequest): Boolean {
if (pendingId != AppWidgetManager.INVALID_APPWIDGET_ID) return false
val info = request.getAppWidgetProviderInfo(activity) ?: return false
load(activity)
add(activity, info, request)
return pending != null
}
private fun add(activity: Activity, info: AppWidgetProviderInfo, request: LauncherApps.PinItemRequest?) {
val h = host(activity)
val id = h.allocateAppWidgetId()
pending = info
pendingId = id
pinRequest = request
val manager = AppWidgetManager.getInstance(activity)
val bound = runCatching { manager.bindAppWidgetIdIfAllowed(id, info.profile, info.provider, null) }.getOrDefault(false)
if (bound) {
bound(activity, id, info)
return
}
val bind = Intent(AppWidgetManager.ACTION_APPWIDGET_BIND)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.provider)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER_PROFILE, info.profile)
@Suppress("DEPRECATION")
runCatching { activity.startActivityForResult(bind, REQUEST_BIND) }.onFailure { abandon(activity) }
}
private fun bound(activity: Activity, id: Int, info: AppWidgetProviderInfo) {
val request = pinRequest
if (request == null) {
configure(activity, id, info)
return
}
val accepted = runCatching { request.isValid && request.accept(Bundle().apply { putInt(AppWidgetManager.EXTRA_APPWIDGET_ID, id) }) }
.getOrDefault(false)
if (accepted) place(activity, id, info) else abandon(activity)
}
private fun configure(activity: Activity, id: Int, info: AppWidgetProviderInfo) {
val optional = Build.VERSION.SDK_INT >= 31 &&
info.widgetFeatures and AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL != 0
if (info.configure == null || optional) {
place(activity, id, info)
return
}
runCatching { host(activity).startAppWidgetConfigureActivityForResult(activity, id, 0, REQUEST_CONFIGURE, null) }
.onFailure { place(activity, id, info) }
}
fun canReconfigure(context: Context, id: Int): Boolean {
val info = info(context, id) ?: return false
if (info.configure == null) return false
return Build.VERSION.SDK_INT < 28 || info.widgetFeatures and AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE != 0
}
fun reconfigure(activity: Activity, id: Int) {
runCatching { host(activity).startAppWidgetConfigureActivityForResult(activity, id, 0, REQUEST_RECONFIGURE, null) }
}
fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int): Boolean {
when (requestCode) {
REQUEST_BIND -> {
val info = pending
if (resultCode == Activity.RESULT_OK && info != null) bound(activity, pendingId, info) else abandon(activity)
}
REQUEST_CONFIGURE -> {
val info = pending
if (resultCode == Activity.RESULT_OK && info != null) place(activity, pendingId, info) else abandon(activity)
}
REQUEST_RECONFIGURE -> Unit
else -> return false
}
return true
}
private fun abandon(context: Context) {
if (pendingId != AppWidgetManager.INVALID_APPWIDGET_ID) host(context).deleteAppWidgetId(pendingId)
pending = null
pinRequest = null
pendingId = AppWidgetManager.INVALID_APPWIDGET_ID
}
private fun place(context: Context, id: Int, info: AppWidgetProviderInfo) {
val (w, h) = defaultSize(context, info)
pending = null
pinRequest = null
pendingId = AppWidgetManager.INVALID_APPWIDGET_ID
widgets = widgets + Placed(id, w, h)
save(context)
}
fun resize(context: Context, id: Int, widthDp: Int, heightDp: Int) {
widgets = widgets.map { if (it.id == id) it.copy(widthDp = widthDp, heightDp = heightDp) else it }
save(context)
}
fun move(context: Context, id: Int, by: Int) {
val i = widgets.indexOfFirst { it.id == id }
val j = i + by
if (i < 0 || j !in widgets.indices) return
widgets = widgets.toMutableList().apply { add(j, removeAt(i)) }
save(context)
}
fun remove(context: Context, id: Int) {
host(context).deleteAppWidgetId(id)
widgets = widgets.filterNot { it.id == id }
save(context)
}
private fun save(context: Context) {
val array = JSONArray()
for (w in widgets) array.put(JSONObject().put("id", w.id).put("w", w.widthDp).put("h", w.heightDp))
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putString(KEY, array.toString()).apply()
}
}
@@ -0,0 +1,346 @@
package com.droiddeck.launcher
import android.app.ActivityOptions
import android.app.AppOpsManager
import android.app.PictureInPictureParams
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.os.Process
import android.provider.Settings
import android.util.Rational
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.droiddeck.launcher.core.AppLanguage
import com.droiddeck.launcher.core.GlobalSettings
import com.droiddeck.launcher.core.PhantomProcessLimit
import com.droiddeck.launcher.core.WirelessAdbPairingService
import com.droiddeck.launcher.core.WirelessAdbPairingService.Stage
import com.droiddeck.launcher.ui.DroidDeckTheme
import com.droiddeck.launcher.ui.LocalPalette
import com.droiddeck.launcher.ui.PrimaryButton
import com.droiddeck.launcher.ui.SecondaryButton
import kotlinx.coroutines.delay
class PairingGuideActivity : ComponentActivity() {
enum class Step { BUILD_NUMBER, CHILD_PROCESS, WIRELESS_DEBUGGING, PAIR, CODE, WORKING, DONE, FAILED }
private var settingsDisplay: Int? = null
private var toggleOnly = false
private var inPip by mutableStateOf(false)
private var launchPending = false
override fun attachBaseContext(newBase: Context) = super.attachBaseContext(AppLanguage.wrap(newBase))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
settingsDisplay = intent.getIntExtra(EXTRA_DISPLAY, -1).takeIf { it >= 0 }
toggleOnly = intent.getBooleanExtra(EXTRA_TOGGLE_ONLY, false)
setContent { DroidDeckTheme { Guide() } }
launchPending = savedInstanceState == null
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
settingsDisplay = intent.getIntExtra(EXTRA_DISPLAY, -1).takeIf { it >= 0 }
toggleOnly = intent.getBooleanExtra(EXTRA_TOGGLE_ONLY, false)
launchPending = true
}
override fun onResume() {
super.onResume()
if (!launchPending) return
if (inPip || settingsDisplay != null) {
launchSettings()
return
}
enterPip()
if (!inPip) launchSettings()
}
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
inPip = isInPictureInPictureMode
if (isInPictureInPictureMode && launchPending) launchSettings()
}
private fun launchSettings() {
launchPending = false
openSettings(stepFor(SettingsState.read(this), WirelessAdbPairingService.stage.value, toggleOnly))
}
private fun enterPip() {
inPip = runCatching {
enterPictureInPictureMode(PictureInPictureParams.Builder().setAspectRatio(Rational(4, 3)).build())
}.getOrDefault(false)
}
private fun openSettings(step: Step) {
val intent = if (step == Step.BUILD_NUMBER) {
Intent(Settings.ACTION_DEVICE_INFO_SETTINGS).putExtra(HIGHLIGHT_KEY, "build_number")
} else {
Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS).apply {
if (step != Step.CHILD_PROCESS) putExtra(HIGHLIGHT_KEY, "toggle_adb_wireless")
}
}.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val display = settingsDisplay
runCatching {
if (display == null) startActivity(intent)
else startActivity(intent, ActivityOptions.makeBasic().setLaunchDisplayId(display).toBundle())
}.onFailure {
runCatching { startActivity(Intent(Settings.ACTION_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) }
}
}
private fun returnToApp() {
startActivity(Intent(this, FrontEndActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT))
finish()
}
@Composable
private fun Guide() {
val stage by WirelessAdbPairingService.stage.collectAsState()
val settingsState by produceState(SettingsState.read(this)) {
while (true) {
value = SettingsState.read(this@PairingGuideActivity)
delay(500)
}
}
val step = stepFor(settingsState, stage, toggleOnly)
var previous by remember { mutableStateOf(step) }
LaunchedEffect(step) {
if (previous == Step.BUILD_NUMBER && (step == Step.WIRELESS_DEBUGGING || step == Step.CHILD_PROCESS)) openSettings(step)
previous = step
when (step) {
Step.DONE -> { delay(1_500); returnToApp() }
Step.FAILED -> { delay(3_000); returnToApp() }
else -> Unit
}
}
LaunchedEffect(stage) { if (stage == Stage.Idle && !toggleOnly) finish() }
BackHandler(enabled = !inPip && settingsDisplay == null) { enterPip() }
Box(Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), contentAlignment = Alignment.Center) {
if (inPip) CompactStep(step, stage)
else FullGuide(step, stage)
}
}
@Composable
private fun CompactStep(step: Step, stage: Stage) {
Column(Modifier.fillMaxSize().padding(10.dp), verticalArrangement = Arrangement.SpaceBetween) {
Text(
instruction(step, stage), style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onBackground, maxLines = 3, overflow = TextOverflow.Ellipsis,
)
StepVisual(step, compact = true)
StepDots(step)
}
}
@Composable
private fun FullGuide(step: Step, stage: Stage) {
Column(
Modifier.widthIn(max = 520.dp).fillMaxWidth().padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(stringResource(R.string.gate_title), style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onBackground)
Text(instruction(step, stage), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onBackground)
StepVisual(step, compact = false)
StepDots(step)
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
if (step == Step.DONE || step == Step.FAILED) {
PrimaryButton(stringResource(R.string.common_back), onClick = ::returnToApp)
} else {
PrimaryButton(stringResource(R.string.gate_open_dev), onClick = {
if (settingsDisplay == null) enterPip()
openSettings(step)
})
SecondaryButton(stringResource(R.string.common_cancel), onClick = {
if (!toggleOnly) WirelessAdbPairingService.cancel(this@PairingGuideActivity)
returnToApp()
})
}
}
}
}
@Composable
private fun instruction(step: Step, stage: Stage): String = when (step) {
Step.BUILD_NUMBER -> stringResource(R.string.pairguide_build_number)
Step.CHILD_PROCESS -> stringResource(R.string.pairguide_child_process)
Step.WIRELESS_DEBUGGING -> stringResource(R.string.pairguide_wireless)
Step.PAIR -> stringResource(R.string.pairguide_pair)
Step.CODE -> (stage as? Stage.CodeNeeded)?.error ?: stringResource(R.string.gate_step3)
Step.WORKING -> (stage as? Stage.Working)?.step ?: stringResource(R.string.adbpair_title_working)
Step.DONE -> stringResource(R.string.adbpair_title_done)
Step.FAILED -> (stage as? Stage.Failed)?.error ?: stringResource(R.string.adbpair_title_failed)
}
@Composable
private fun StepVisual(step: Step, compact: Boolean) {
val colors = MaterialTheme.colorScheme
val accent = LocalPalette.current.signal
val pulse = rememberInfiniteTransition(label = "guidePulse")
val beat by pulse.animateFloat(0f, 1f, infiniteRepeatable(tween(900, easing = LinearEasing), RepeatMode.Restart), label = "beat")
val label = when (step) {
Step.BUILD_NUMBER -> stringResource(R.string.pairguide_row_build_number)
Step.CHILD_PROCESS -> stringResource(R.string.pairguide_row_child_process)
Step.WIRELESS_DEBUGGING -> stringResource(R.string.pairguide_row_wireless)
Step.PAIR -> stringResource(R.string.pairguide_row_pair)
Step.CODE -> stringResource(R.string.adbpair_enter_code)
Step.WORKING, Step.DONE, Step.FAILED -> return
}
val rowHeight = if (compact) 34.dp else 56.dp
Row(
Modifier.fillMaxWidth().height(rowHeight).background(colors.surfaceVariant, RoundedCornerShape(10.dp))
.border(1.dp, accent.copy(alpha = 0.35f), RoundedCornerShape(10.dp)).padding(horizontal = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(label, style = if (compact) MaterialTheme.typography.labelMedium else MaterialTheme.typography.bodyLarge,
color = colors.onSurface, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f))
when (step) {
Step.WIRELESS_DEBUGGING, Step.CHILD_PROCESS -> Toggle(on = beat > 0.5f, accent = accent, compact = compact)
else -> Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
if (step == Step.BUILD_NUMBER) Text("×7", style = MaterialTheme.typography.labelLarge, color = accent)
Box(contentAlignment = Alignment.Center) {
Box(Modifier.size(if (compact) 18.dp else 26.dp).scale(0.6f + beat * 0.8f).alpha(1f - beat)
.background(accent, CircleShape))
Box(Modifier.size(if (compact) 8.dp else 12.dp).background(accent, CircleShape))
}
}
}
}
}
@Composable
private fun Toggle(on: Boolean, accent: Color, compact: Boolean) {
val width = if (compact) 26.dp else 40.dp
val knob = if (compact) 12.dp else 18.dp
Box(
Modifier.width(width).height(knob + 4.dp)
.background(if (on) accent.copy(alpha = 0.6f) else MaterialTheme.colorScheme.outline, CircleShape).padding(2.dp),
contentAlignment = if (on) Alignment.CenterEnd else Alignment.CenterStart,
) { Box(Modifier.size(knob).background(Color.White, CircleShape)) }
}
@Composable
private fun StepDots(step: Step) {
val accent = LocalPalette.current.signal
val shown = if (toggleOnly) listOf(Step.BUILD_NUMBER, Step.CHILD_PROCESS)
else listOf(Step.BUILD_NUMBER, Step.WIRELESS_DEBUGGING, Step.PAIR, Step.CODE)
val at = shown.indexOf(step).takeIf { it >= 0 } ?: shown.size
Row(horizontalArrangement = Arrangement.spacedBy(5.dp), verticalAlignment = Alignment.CenterVertically) {
shown.forEachIndexed { index, _ ->
Box(Modifier.size(if (index == at) 8.dp else 6.dp)
.background(if (index <= at) accent else MaterialTheme.colorScheme.outline, CircleShape))
}
Spacer(Modifier.width(2.dp))
}
}
private data class SettingsState(
val developerOptions: Boolean,
val wirelessDebugging: Boolean,
val childProcessBlocked: Boolean,
) {
companion object {
fun read(context: Context) = SettingsState(
GlobalSettings.developerOptionsOn(context),
GlobalSettings.wirelessDebuggingOn(context),
PhantomProcessLimit.blocksSteam(PhantomProcessLimit.read(context)),
)
}
}
companion object {
private const val EXTRA_DISPLAY = "settingsDisplay"
private const val EXTRA_TOGGLE_ONLY = "toggleOnly"
private const val HIGHLIGHT_KEY = ":settings:fragment_args_key"
private fun stepFor(settings: SettingsState, stage: Stage, toggleOnly: Boolean): Step = if (toggleOnly) when {
!settings.developerOptions -> Step.BUILD_NUMBER
settings.childProcessBlocked -> Step.CHILD_PROCESS
else -> Step.DONE
} else when (stage) {
is Stage.Working -> Step.WORKING
Stage.Done -> Step.DONE
is Stage.Failed -> Step.FAILED
is Stage.CodeNeeded -> Step.CODE
else -> when {
!settings.developerOptions -> Step.BUILD_NUMBER
!settings.wirelessDebugging -> Step.WIRELESS_DEBUGGING
else -> Step.PAIR
}
}
fun canFloat(context: Context): Boolean {
if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) return false
val ops = context.getSystemService(AppOpsManager::class.java) ?: return true
val mode = if (Build.VERSION.SDK_INT >= 29) {
ops.unsafeCheckOpNoThrow(AppOpsManager.OPSTR_PICTURE_IN_PICTURE, Process.myUid(), context.packageName)
} else {
@Suppress("DEPRECATION")
ops.checkOpNoThrow(AppOpsManager.OPSTR_PICTURE_IN_PICTURE, Process.myUid(), context.packageName)
}
return mode == AppOpsManager.MODE_ALLOWED
}
fun start(context: Context, settingsDisplay: Int?, toggleOnly: Boolean = false): Boolean {
if (settingsDisplay == null && !canFloat(context)) return false
context.startActivity(Intent(context, PairingGuideActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(EXTRA_TOGGLE_ONLY, toggleOnly)
.apply { if (settingsDisplay != null) putExtra(EXTRA_DISPLAY, settingsDisplay) })
return true
}
}
}
@@ -0,0 +1,82 @@
package com.droiddeck.launcher
import android.content.Intent
import android.content.pm.LauncherApps
import android.graphics.Bitmap
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.core.graphics.drawable.toBitmap
import com.droiddeck.launcher.core.AppUiPrefs
import com.droiddeck.launcher.ui.DroidDeckTheme
import com.droiddeck.launcher.ui.PinRequestPrompt
class PinRequestActivity : ComponentActivity() {
override fun attachBaseContext(newBase: android.content.Context) {
super.attachBaseContext(newBase)
com.droiddeck.launcher.core.AppLanguage.applyTo(this, newBase)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val request = runCatching { getSystemService(LauncherApps::class.java).getPinItemRequest(intent) }.getOrNull()
if (request == null || !request.isValid) {
finish()
return
}
val prompt = when (request.requestType) {
LauncherApps.PinItemRequest.REQUEST_TYPE_SHORTCUT -> request.shortcutInfo?.let { info ->
val appLabel = appLabel(info.`package`)
Triple(info.shortLabel?.toString().orEmpty().ifEmpty { appLabel }, appLabel, HomeApp.shortcutIcon(this, info))
}
LauncherApps.PinItemRequest.REQUEST_TYPE_APPWIDGET -> request.getAppWidgetProviderInfo(this)?.let { info ->
val appLabel = appLabel(info.provider.packageName)
val dpi = resources.displayMetrics.densityDpi
val icon = runCatching { (info.loadPreviewImage(this, dpi) ?: info.loadIcon(this, dpi))?.let(::scaled) }.getOrNull()
Triple(info.loadLabel(packageManager)?.toString().orEmpty().ifEmpty { appLabel }, appLabel, icon)
}
else -> null
}
if (prompt == null) {
finish()
return
}
val (label, appLabel, icon) = prompt
val widget = request.requestType == LauncherApps.PinItemRequest.REQUEST_TYPE_APPWIDGET
setContent {
DroidDeckTheme(appScale = AppUiPrefs.scale(this)) {
PinRequestPrompt(
label = label, appLabel = appLabel, icon = icon, wide = widget,
onAdd = {
if (!widget) {
HomePins.accept(this, request)
finish()
} else if (!HomeWidgets.addPinned(this, request)) finish()
},
onCancel = { finish() },
)
}
}
}
@Suppress("OVERRIDE_DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (HomeWidgets.onActivityResult(this, requestCode, resultCode)) {
finish()
return
}
@Suppress("DEPRECATION")
super.onActivityResult(requestCode, resultCode, data)
}
private fun appLabel(packageName: String): String =
runCatching { packageManager.getApplicationLabel(packageManager.getApplicationInfo(packageName, 0)).toString() }
.getOrDefault(packageName)
private fun scaled(d: android.graphics.drawable.Drawable): Bitmap {
val w = d.intrinsicWidth.takeIf { it > 0 } ?: 192
val h = d.intrinsicHeight.takeIf { it > 0 } ?: 192
val k = minOf(1f, 480f / maxOf(w, h))
return d.toBitmap((w * k).toInt().coerceAtLeast(1), (h * k).toInt().coerceAtLeast(1))
}
}
@@ -1,6 +1,8 @@
package com.droiddeck.launcher
import android.os.Handler
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
@@ -8,31 +10,60 @@ import androidx.compose.runtime.setValue
import com.droiddeck.launcher.session.ProtonExtras
import com.droiddeck.launcher.ui.ProtonRow
import com.droiddeck.launcher.session.SessionState
import com.droiddeck.launcher.session.SessionPhase
import com.droiddeck.launcher.frontend.AutoComponents
import com.droiddeck.launcher.runtime.LinuxRuntime
import com.droiddeck.launcher.runtime.LinuxRuntimeInstaller
internal class ProtonMenu(private val activity: android.app.Activity, private val ui: android.os.Handler) {
var protonRows by mutableStateOf<List<ProtonRow>>(emptyList())
var protonBusyId by mutableStateOf<String?>(null)
var protonStage by mutableStateOf<String?>(null)
var protonPercent by mutableIntStateOf(-1)
internal class ProtonMenu(
private val activity: android.app.Activity,
private val ui: Handler,
private val onInstalled: () -> Unit = {},
private val idle: () -> Boolean = { true },
) {
private object Operation {
val rows = mutableStateOf<List<ProtonRow>>(emptyList())
val busyId = mutableStateOf<String?>(null)
val stage = mutableStateOf<String?>(null)
val percent = mutableIntStateOf(-1)
val problem = mutableStateOf<String?>(null)
}
var protonRows by Operation.rows
var protonBusyId by Operation.busyId
var protonStage by Operation.stage
var protonPercent by Operation.percent
var protonProblem by Operation.problem
private val checking = java.util.concurrent.atomic.AtomicBoolean(false)
@VisibleForTesting internal var installer: (ProtonExtras.Tool, (String, Int) -> Unit) -> String? =
{ tool, progress -> ProtonExtras.install(activity, tool, progress) }
fun installProton(id: String) {
fun installProton(id: String, automatic: Boolean = false) {
val tool = ProtonExtras.tools.firstOrNull { it.id == id } ?: return
if (protonBusyId != null || SessionState.running) return
if (protonBusyId != null || ProtonExtras.installInProgress || SessionState.running) return
ProtonExtras.unqueue(activity, tool)
protonBusyId = id
protonProblem = null
protonStage = activity.getString(R.string.store_starting)
protonPercent = -1
Thread({
val problem = ProtonExtras.install(activity, tool) { label, value ->
ui.post { protonStage = label; protonPercent = value }
}
val problem = runCatching {
installer(tool) { label, value ->
ui.post { protonStage = label; protonPercent = value }
}
}.getOrElse { it.message ?: it.javaClass.simpleName }
if (problem == null && !automatic && tool.id == ProtonExtras.OWN) ProtonExtras.setDeclined(activity, false)
ui.post {
protonProblem = problem
protonBusyId = null
protonStage = null
protonPercent = -1
refreshProtons()
if (problem != null) android.widget.Toast.makeText(activity, problem, android.widget.Toast.LENGTH_LONG).show()
if (problem != null) {
if (automatic) Log.w("ProtonMenu", "auto-update ${tool.id}: $problem")
else android.widget.Toast.makeText(activity, problem, android.widget.Toast.LENGTH_LONG).show()
}
if (problem == null) onInstalled()
if (automatic) ensureAutoUpdates()
}
}, "install-proton-$id").start()
}
@@ -41,10 +72,12 @@ internal class ProtonMenu(private val activity: android.app.Activity, private va
val tool = ProtonExtras.tools.firstOrNull { it.id == id } ?: return
if (protonBusyId != null || SessionState.running) return
protonBusyId = id
protonProblem = null
protonStage = activity.getString(R.string.main_removing_named, dir ?: tool.name)
protonPercent = -1
Thread({
if (older) ProtonExtras.removeOlder(activity, tool) else ProtonExtras.remove(activity, tool, dir)
if (tool.id == ProtonExtras.OWN && ProtonExtras.installedAll(activity, tool).isEmpty()) ProtonExtras.setDeclined(activity, true)
ui.post {
protonBusyId = null
protonStage = null
@@ -64,7 +97,39 @@ internal class ProtonMenu(private val activity: android.app.Activity, private va
if (!checking.compareAndSet(false, true)) return
Thread({
val asked = try { ProtonExtras.checkLatest(activity, force) } catch (_: Throwable) { false } finally { checking.set(false) }
if (asked) ui.post { refreshProtons() }
ui.post {
if (asked) refreshProtons()
ensureAutoUpdates()
}
}, "proton-updates").start()
}
internal fun ensureAutoUpdates(now: Long = System.currentTimeMillis()) {
if (activity.isFinishing || activity.isDestroyed || !idle() || protonBusyId != null ||
ProtonExtras.installInProgress || SessionState.running || LinuxRuntimeInstaller.isBusy() ||
SessionState.phase !in setOf(SessionPhase.IDLE, SessionPhase.FAILED) ||
!LinuxRuntime.isInstalled(activity) || !AutoComponents.online(activity) || AutoComponents.unfavourable(activity)) return
if (ProtonExtras.installedAll(activity, ProtonExtras.tools.first { it.id == ProtonExtras.OWN }).size > 1) {
protonBusyId = ProtonExtras.OWN
protonProblem = null
Thread({
ProtonExtras.tidy(activity)
val tidied = ProtonExtras.installedAll(activity, ProtonExtras.tools.first { it.id == ProtonExtras.OWN }).size <= 1
ui.post {
protonBusyId = null
refreshProtons()
if (tidied) ensureAutoUpdates()
}
}, "proton-tidy").start()
return
}
val attempts = activity.getSharedPreferences("proton-auto-updates", android.content.Context.MODE_PRIVATE)
val (tool, update) = ProtonExtras.tools.firstNotNullOfOrNull { tool ->
val update = ProtonExtras.update(activity, tool) ?: ProtonExtras.missing(activity, tool) ?: return@firstNotNullOfOrNull null
val previous = attempts.getLong("${tool.id}.at", 0L)
if (attempts.getString("${tool.id}.tag", null) != update || now - previous !in 0 until 15L * 60 * 1000) tool to update else null
} ?: return
attempts.edit().putString("${tool.id}.tag", update).putLong("${tool.id}.at", now).apply()
installProton(tool.id, automatic = true)
}
}
@@ -71,7 +71,6 @@ class QuestXrActivity : NativeActivity() {
}
} finally {
synchronized(steamVrLock) {
// A retiring activity must not stop a replacement's SteamVR session.
if (steamVrOwner === this) {
if (sessionAlive()) SteamVr.run(this, "stop")
steamVrOwner = null
File diff suppressed because it is too large. Load diff
@@ -29,6 +29,7 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl
private var pending: AppUpdates.Release? = null
private var lastCheck = 0L
private var request = 0
@Volatile private var session = -1
fun state() = UpdatesState(catalog, follow, checking, error, stage, percent, askPermission)
@@ -87,13 +88,14 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl
if (SessionState.running) { error = activity.getString(R.string.upd_stop_first); return }
if (!SelfInstaller.canInstall(activity)) { pending = r; askPermission = true; return }
error = null
session = -1
stage = activity.getString(R.string.upd_stage_downloading)
percent = 0
Thread({
val result = runCatching {
val file = SelfInstaller.download(activity, apk) { pct -> ui.post { percent = pct } }
ui.post { stage = activity.getString(R.string.upd_stage_installing); percent = -1 }
SelfInstaller.install(activity, file)
SelfInstaller.install(activity, file) { id -> session = id }
}
result.onFailure { e -> ui.post { stage = null; error = e.message ?: activity.getString(R.string.upd_failed) } }
}, "app-update-install").start()
@@ -101,13 +103,15 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1).let { it < 0 || it != session }) return
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val confirm = if (Build.VERSION.SDK_INT >= 33) intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
else @Suppress("DEPRECATION") intent.getParcelableExtra(Intent.EXTRA_INTENT)
if (confirm == null) { stage = null; error = activity.getString(R.string.upd_not_offered); return }
val safe = confirm?.let { SelfInstaller.safeConfirmIntent(activity, it) }
if (safe == null) { stage = null; error = activity.getString(R.string.upd_not_offered); return }
stage = activity.getString(R.string.upd_stage_confirm)
activity.startActivity(confirm.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
if (runCatching { activity.startActivity(safe) }.isFailure) { stage = null; error = activity.getString(R.string.upd_not_offered) }
}
PackageInstaller.STATUS_SUCCESS -> stage = null
else -> {
@@ -18,12 +18,15 @@ import com.droiddeck.launcher.runtime.LinuxRuntime
import com.droiddeck.launcher.runtime.LinuxRuntimeInstaller
import com.droiddeck.launcher.session.SessionArtifacts
import com.droiddeck.launcher.session.SessionEvents
import com.droiddeck.launcher.session.SessionLogShare
import com.droiddeck.launcher.session.SessionPhase
import com.droiddeck.launcher.session.SessionService
import com.droiddeck.launcher.session.SessionState
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.nio.file.Files
import java.util.UUID
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@@ -93,6 +96,7 @@ class AgentBridgeProvider : ContentProvider() {
}
"prefs" -> prefs(context, request)
"glasses" -> glasses(context, request)
"logs" -> logs(context)
"access" -> JSONObject().put("ok", true).put("commands", AgentAccess.commandsAllowed(context))
.put("debugBuild", BuildConfig.DEBUG).put("inbox", AgentEnv.inbox(context).absolutePath)
else -> error("UNKNOWN_COMMAND", "Unknown agent command '$method'")
@@ -197,9 +201,46 @@ class AgentBridgeProvider : ContentProvider() {
private fun hex(text: String, max: Int): Int? =
text.removePrefix("0x").removePrefix("0X").takeIf { it.isNotEmpty() }?.toIntOrNull(16)?.takeIf { it in 0..max }
private fun logDir(context: Context): File? =
SessionState.logDirectory ?: SessionState.logFile?.parentFile ?: SessionPaths.sessionFolders(context).lastOrNull()
private fun cleanupExpiredLogExports(exports: File) {
val now = System.currentTimeMillis()
val canonicalRoot = exports.canonicalFile
exports.listFiles()?.forEach { candidate ->
val isUuid = runCatching { UUID.fromString(candidate.name).toString() == candidate.name }.getOrDefault(false)
if (!isUuid || !candidate.isDirectory || Files.isSymbolicLink(candidate.toPath())) return@forEach
if (candidate.canonicalFile.parentFile != canonicalRoot) return@forEach
if (now - candidate.lastModified() >= LOG_EXPORT_RETENTION_MS) candidate.deleteRecursively()
}
}
// The session folder is app-private, so adb pulls this copy from external files instead.
private fun logs(context: Context): JSONObject {
val dir = logDir(context)?.takeIf { it.isDirectory }
?: return error("NO_SESSION_LOGS", "No session artifact directory is available")
val externalFiles = context.getExternalFilesDir(null)
?: return error("NO_SESSION_LOGS", "External app storage is unavailable")
val exports = File(externalFiles, "agent-logs")
if ((!exports.exists() && !exports.mkdirs()) || !exports.isDirectory) {
return error("NO_SESSION_LOGS", "Could not create the session export directory")
}
cleanupExpiredLogExports(exports)
val output = File(exports, UUID.randomUUID().toString())
val zip = try {
SessionLogShare.zipFolder(context, dir, output)
} catch (error: Exception) {
output.deleteRecursively()
throw error
}
?: return error("NO_SESSION_LOGS", "The session artifact directory is empty")
return JSONObject().put("ok", true).put("command", "logs").put("sessionId", dir.name)
.put("zip", zip.absolutePath).put("cleanupDir", output.absolutePath)
}
private fun state(context: Context): JSONObject {
val runtimeVersion = LinuxRuntimeInstaller.installedVersion(context)
val dir = SessionState.logDirectory ?: SessionState.logFile?.parentFile ?: SessionPaths.sessionFolders(context).lastOrNull()
val dir = logDir(context)
val session = JSONObject()
.put("id", SessionState.sessionId ?: dir?.name ?: JSONObject.NULL)
.put("phase", SessionState.phase.name)
@@ -351,6 +392,7 @@ class AgentBridgeProvider : ContentProvider() {
const val RESULT_JSON = "json"
const val EXTRA_REQUEST = "request"
const val SCHEMA = 2
private const val LOG_EXPORT_RETENTION_MS = 24 * 60 * 60 * 1000L
private val recoveryComplete = CountDownLatch(1)
fun awaitRecovery() {
@@ -3,9 +3,13 @@ package com.droiddeck.launcher.agent
import android.content.Context
import android.util.Log
import com.droiddeck.launcher.runtime.LinuxRuntime
import com.droiddeck.launcher.runtime.ProotFastPath
import com.droiddeck.launcher.session.SessionPhase
import com.droiddeck.launcher.session.SessionState
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.security.MessageDigest
import java.nio.file.Files
import java.nio.file.LinkOption.NOFOLLOW_LINKS
import java.nio.file.StandardOpenOption.READ
@@ -14,7 +18,6 @@ object AgentEnv {
private const val TAG = "AgentEnv"
private const val FILE = "agent-env.json"
private const val GUEST_BIN = "/opt/droiddeck-agent/bin"
private const val BASE_PATH = "/usr/local/bin:/usr/bin:/bin"
private val NAME = Regex("[A-Za-z0-9._-]{1,64}")
private val KEY = Regex("[A-Za-z_][A-Za-z0-9_]*")
private val lock = Any()
@@ -55,6 +58,9 @@ object AgentEnv {
fun override(context: Context, name: String): JSONObject = synchronized(lock) {
AgentAccess.requireCommands(context)
if (NAME.matchEntire(name) == null || name in setOf(".", "..")) throw AgentException("INVALID_NAME", "Override names are letters, digits, '.', '_' and '-'")
if (name == "libblfastpath.so" && (SessionState.running || SessionState.guestPid > 0 || SessionState.phase == SessionPhase.STOPPING)) {
throw AgentException("SESSION_ACTIVE", "Stop the session before installing a fast-path library")
}
val source = File(inbox(context), name)
if (Files.isSymbolicLink(source.toPath())) throw AgentException("INVALID_NAME", "The override must be a regular file")
if (!source.isFile) throw AgentException("NO_SUCH_FILE", "Push the binary to ${source.path} first")
@@ -67,6 +73,10 @@ object AgentEnv {
staged.delete()
throw AgentException("OVERRIDE_FAILED", "Could not stage the override")
}
if (name == "libblfastpath.so" && !arm64SharedLibrary(staged)) {
staged.delete()
throw AgentException("INVALID_LIBRARY", "The fast path must be a 64-bit little-endian ARM64 ELF shared library")
}
if (!staged.setExecutable(true, false) || !staged.renameTo(target)) {
staged.delete()
throw AgentException("OVERRIDE_FAILED", "Could not install $name in the rootfs")
@@ -76,6 +86,40 @@ object AgentEnv {
state.put("pendingOverrides", JSONArray(merge(strings(state.optJSONArray("pendingOverrides")), listOf(name))))
save(context, state)
state.put("installed", "$GUEST_BIN/$name")
if (name == "libblfastpath.so") {
val digest = MessageDigest.getInstance("SHA-256")
target.inputStream().use { input ->
val buffer = ByteArray(65536)
while (true) {
val count = input.read(buffer)
if (count < 0) break
digest.update(buffer, 0, count)
}
}
state.put("sha256", digest.digest().joinToString("") { "%02x".format(it) })
}
state
}
fun fastPathLibrary(context: Context): String = synchronized(lock) {
if (!AgentAccess.commandsAllowed(context)) return@synchronized ProotFastPath.LIBRARY
if ("libblfastpath.so" !in strings(load(context).optJSONArray("activeOverrides"))) return@synchronized ProotFastPath.LIBRARY
val file = File(bin(context), "libblfastpath.so")
if (!file.isFile || Files.isSymbolicLink(file.toPath())) return@synchronized ProotFastPath.LIBRARY
"$GUEST_BIN/libblfastpath.so"
}
private fun arm64SharedLibrary(file: File): Boolean = file.inputStream().use { input ->
val header = ByteArray(20)
var count = 0
while (count < header.size) {
val read = input.read(header, count, header.size - count)
if (read < 0) return@use false
count += read
}
header.take(7) == listOf<Byte>(0x7f, 0x45, 0x4c, 0x46, 2, 1, 1) &&
header[16] == 3.toByte() && header[17] == 0.toByte() &&
header[18] == 183.toByte() && header[19] == 0.toByte()
}
fun beginSession(context: Context): List<String> = synchronized(lock) {
@@ -86,13 +130,13 @@ object AgentEnv {
val state = load(context)
val active = strings(state.optJSONArray("pending"))
val overrides = strings(state.optJSONArray("pendingOverrides"))
state.put("active", JSONArray(active)).put("pending", JSONArray())
state.put("activeOverrides", JSONArray(overrides)).put("pendingOverrides", JSONArray())
save(context, state)
bin(context).listFiles()
?.filter { it.name !in overrides }?.forEach { it.delete() }
val lines = strings(state.optJSONArray("persistent")) + active
val withPath = if (overrides.isEmpty()) lines else lines + "PATH=$GUEST_BIN:${pathOf(lines)}"
val withPath = if (overrides.none { it != "libblfastpath.so" }) lines else lines + "PATH=$GUEST_BIN:${pathOf(lines)}"
state.put("active", JSONArray(active)).put("pending", JSONArray())
state.put("activeOverrides", JSONArray(overrides)).put("pendingOverrides", JSONArray())
save(context, state)
if (withPath.isNotEmpty()) Log.i(TAG, "agent environment keys for this session: ${withPath.map { it.substringBefore('=') }}")
withPath
}
@@ -115,7 +159,7 @@ object AgentEnv {
}
private fun pathOf(lines: List<String>) =
lines.lastOrNull { it.startsWith("PATH=") }?.substringAfter('=')?.takeIf { it.isNotBlank() } ?: BASE_PATH
lines.lastOrNull { it.startsWith("PATH=") }?.substringAfter('=')?.takeIf { it.isNotBlank() } ?: LinuxRuntime.GUEST_PATH
private fun bin(context: Context): File {
val root = LinuxRuntime.rootDir(context).canonicalFile.toPath()
@@ -22,6 +22,18 @@ object AgentGuest {
private set
var steamKeyboard by mutableStateOf(false)
private set
var desktopTextField by mutableStateOf(false)
private set
var keyboardRequests by mutableIntStateOf(0)
private set
var gameTextField by mutableStateOf(false)
private set
var gameProgram by mutableStateOf<String?>(null)
private set
var keyboardDismissals by mutableIntStateOf(0)
private set
@Volatile private var keyboardRoute = false
@Volatile private var routeDir: File? = null
private const val TAG = "AgentGuest"
private const val POLL_MS = 50L
private val lock = Any()
@@ -35,10 +47,12 @@ object AgentGuest {
observer?.stopWatching()
observer = null
lastFocusKey = null
main.post { steamKeyboard = false }
main.post { steamKeyboard = false; desktopTextField = false; gameTextField = false; gameProgram = null }
val dir = dir(context)
dir.deleteRecursively()
dir.mkdirs()
routeDir = dir
writeKeyboardRoute(dir, keyboardRoute)
@Suppress("DEPRECATION")
observer = object : FileObserver(dir.path, MOVED_TO or CLOSE_WRITE) {
override fun onEvent(event: Int, path: String?) {
@@ -48,6 +62,10 @@ object AgentGuest {
}
"focus.json" -> readJson(File(dir, path))?.let(::recordFocus)
"keyboard.json" -> readJson(File(dir, path))?.let { recordKeyboard(this, it) }
"ime.json" -> readJson(File(dir, path))?.let { recordTextField(this, it) }
"wine-ime.json" -> readJson(File(dir, path))?.let { recordGameTextField(this, it) }
"keyboard-request.json" -> readJson(File(dir, path))?.let { recordKeyboardRequest(this) }
"keyboard-dismiss.json" -> readJson(File(dir, path))?.let { recordKeyboardDismiss(this) }
}
}
}.also { it.startWatching() }
@@ -58,10 +76,46 @@ object AgentGuest {
synchronized(lock) {
observer?.stopWatching()
observer = null
main.post { steamKeyboard = false }
main.post { steamKeyboard = false; desktopTextField = false; gameTextField = false; gameProgram = null }
}
}
fun routeKeyboard(context: Context, bottom: Boolean) {
synchronized(lock) {
if (keyboardRoute == bottom) return
keyboardRoute = bottom
writeKeyboardRoute(routeDir ?: dir(context), bottom)
}
}
private fun writeKeyboardRoute(dir: File, bottom: Boolean) {
val file = File(dir, "keyboard-route")
runCatching {
if (!bottom) file.delete()
else if (dir.isDirectory) {
val temp = File(dir, "keyboard-route.tmp")
temp.writeText("bottom")
if (!temp.renameTo(file)) temp.delete()
}
}.onFailure { Log.w(TAG, "keyboard route not written", it) }
}
private fun recordKeyboardRequest(source: FileObserver) {
synchronized(lock) {
if (source !== observer) return
main.post { keyboardRequests++ }
}
SessionEvents.record("keyboard.requested", emptyMap())
}
private fun recordKeyboardDismiss(source: FileObserver) {
synchronized(lock) {
if (source !== observer) return
main.post { keyboardDismissals++ }
}
SessionEvents.record("keyboard.dismissed", emptyMap())
}
private fun recordKeyboard(source: FileObserver, snapshot: JSONObject) {
val showing = snapshot.optBoolean("steamKeyboard")
synchronized(lock) {
@@ -71,6 +125,25 @@ object AgentGuest {
SessionEvents.record("keyboard.changed", mapOf("steamKeyboard" to showing, "seq" to snapshot.optInt("seq")))
}
private fun recordTextField(source: FileObserver, snapshot: JSONObject) {
val focused = snapshot.optBoolean("textField")
synchronized(lock) {
if (source !== observer) return
main.post { desktopTextField = focused }
}
SessionEvents.record("ime.changed", mapOf("textField" to focused, "seq" to snapshot.optInt("seq")))
}
private fun recordGameTextField(source: FileObserver, snapshot: JSONObject) {
val focused = snapshot.optBoolean("textField")
val program = snapshot.optString("program").takeIf { it.isNotEmpty() }
synchronized(lock) {
if (source !== observer) return
main.post { gameProgram = program; gameTextField = focused }
}
SessionEvents.record("game.ime.changed", mapOf("textField" to focused, "program" to program, "seq" to snapshot.optInt("seq")))
}
private fun recordFocus(snapshot: JSONObject) {
val fields = linkedMapOf<String, Any?>(
"focusedApp" to snapshot.opt("focusedApp"),
@@ -1,5 +1,6 @@
package com.droiddeck.launcher.agent
import com.droiddeck.launcher.input.PadBridge
import com.droiddeck.launcher.input.PadState
import com.droiddeck.launcher.session.SessionState
import com.droiddeck.launcher.wayland.WaylandCompositor
@@ -143,9 +144,10 @@ object AgentInput {
val right = request.optString("stick", "left") == "right"
val x = request.optDouble("x", 0.0).toFloat().coerceIn(-1f, 1f)
val y = request.optDouble("y", 0.0).toFloat().coerceIn(-1f, 1f)
bridge.applyTouch { if (right) { it.rightX = x; it.rightY = y } else { it.leftX = x; it.leftY = y } }
val stickWrite = if (right) PadBridge.RIGHT_STICK else PadBridge.LEFT_STICK
bridge.applyTouch(stickWrite) { if (right) { it.rightX = x; it.rightY = y } else { it.leftX = x; it.leftY = y } }
hold(request, 300)
bridge.applyTouch { if (right) { it.rightX = 0f; it.rightY = 0f } else { it.leftX = 0f; it.leftY = 0f } }
bridge.applyTouch(stickWrite) { if (right) { it.rightX = 0f; it.rightY = 0f } else { it.leftX = 0f; it.leftY = 0f } }
return JSONObject().put("stick", if (right) "right" else "left")
}
@@ -23,6 +23,9 @@ class AgentStartActivity : Activity() {
Thread({
try {
AgentBridgeProvider.awaitRecovery()
val until = android.os.SystemClock.elapsedRealtime() + RUNTIME_WAIT_MS
while (com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.isBusy() && !isFinishing &&
android.os.SystemClock.elapsedRealtime() < until) Thread.sleep(RUNTIME_POLL_MS)
runOnUiThread {
if (!isFinishing) startSession(request)
}
@@ -51,6 +54,11 @@ class AgentStartActivity : Activity() {
}
private fun startSession(request: JSONObject) {
if (com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.isBusy()) {
Log.w(TAG, "the Linux runtime is being installed or changed")
finish()
return
}
try {
AgentAccess.requireStart(this, request)
} catch (e: AgentException) {
@@ -107,5 +115,7 @@ class AgentStartActivity : Activity() {
companion object {
const val EXTRA_REQUEST = "request"
private const val TAG = "AgentStartActivity"
private const val RUNTIME_WAIT_MS = 15 * 60_000L
private const val RUNTIME_POLL_MS = 500L
}
}
@@ -3,6 +3,7 @@ package com.droiddeck.launcher.audio;
import android.os.Process;
import android.util.Log;
import com.droiddeck.launcher.core.FileUtils;
import com.droiddeck.launcher.core.SessionPart;
import com.droiddeck.launcher.core.HostProcess;
@@ -19,10 +20,13 @@ public class DirectAudioRelayComponent extends SessionPart {
private volatile int pid = -1;
private final Object micLock = new Object();
private boolean micHot;
private final File micControl;
public java.util.function.Consumer<Boolean> onMicEnabled;
public DirectAudioRelayComponent(File socketPath, File micFifoPath) {
this.socketPath = socketPath;
this.micFifoPath = micFifoPath;
this.micControl = micFifoPath == null ? null : new File(micFifoPath.getParentFile(), "mic-enabled");
}
public void setLogFile(File file) {
@@ -63,8 +67,13 @@ public class DirectAudioRelayComponent extends SessionPart {
StringBuilder command = new StringBuilder(binary.getAbsolutePath());
command.append(" --socket ").append(socketPath.getAbsolutePath());
command.append(" --log");
if (micFifoPath != null) {
final boolean withMic = micFifoPath != null && setMicEnabled(true);
if (withMic) {
command.append(" --mic-fifo ").append(micFifoPath.getAbsolutePath());
command.append(" --mic-control ").append(micControl.getAbsolutePath());
} else if (micFifoPath != null) {
Log.w(TAG, "microphone control unavailable; starting with capture disabled");
command.append(" --mic-control /dev/null");
}
ArrayList<String> env = new ArrayList<>();
env.add("HOME=" + app().getFilesDir());
@@ -73,7 +82,7 @@ public class DirectAudioRelayComponent extends SessionPart {
try {
out = new java.io.PrintWriter(new java.io.FileWriter(logFile, true));
out.println("== DirectAudio relay helper starting"
+ (micFifoPath != null ? " with the microphone" : " (no microphone)"));
+ (withMic ? " with the microphone" : " (no microphone)"));
out.flush();
} catch (Exception e) {
Log.w(TAG, "could not open " + logFile, e);
@@ -84,14 +93,23 @@ public class DirectAudioRelayComponent extends SessionPart {
app().getFilesDir(), null,
line -> {
Log.i(TAG, line);
trackMic(line);
if (withMic) trackMic(line);
if (log != null) synchronized (log) { log.println(line); log.flush(); }
});
Log.i(TAG, "started pid=" + pid + " socket=" + socketPath
+ (micFifoPath != null ? " mic=" + micFifoPath : " (no microphone)"));
+ (withMic ? " mic=" + micFifoPath : " (no microphone)"));
}
public synchronized boolean setMicEnabled(boolean enabled) {
if (micControl == null) return false;
File next = new File(micControl.getPath() + ".new");
return FileUtils.writeString(next, enabled ? "1" : "0") && next.renameTo(micControl);
}
private void trackMic(String line) {
if (line.startsWith("DA-Relay: mic enabled=") && onMicEnabled != null) {
onMicEnabled.accept(line.equals("DA-Relay: mic enabled=1"));
}
boolean start = line.startsWith("DA-Relay: mic start ");
if (!start && !line.startsWith("DA-Relay: mic stop ")) return;
synchronized (micLock) {
@@ -100,7 +118,6 @@ public class DirectAudioRelayComponent extends SessionPart {
}
}
// Android keeps the input stream's wake lock while a frozen helper still has it started.
public boolean awaitMicReleased(long timeoutMs) {
long deadline = System.currentTimeMillis() + timeoutMs;
synchronized (micLock) {
@@ -0,0 +1,42 @@
package com.droiddeck.launcher.audio
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioTrack
import android.util.Log
object DirectAudioTiming {
private const val TAG = "DirectAudioTiming"
const val PERIOD_ENV = "AUDIO_DIRECT_PERIOD_MS"
const val BUFFER_ENV = "AUDIO_DIRECT_MS"
const val SLOW_OUTPUT_PERIOD_MS = 20
const val SLOW_OUTPUT_BUFFER_MS = 60
fun environment(lowLatencyOutput: Boolean): List<String> =
if (lowLatencyOutput) emptyList()
else listOf("$PERIOD_ENV=$SLOW_OUTPUT_PERIOD_MS", "$BUFFER_ENV=$SLOW_OUTPUT_BUFFER_MS")
fun lowLatencyOutput(): Boolean = try {
val track = AudioTrack.Builder()
.setAudioAttributes(AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build())
.setAudioFormat(AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_FLOAT)
.setSampleRate(48000)
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
.build())
.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY)
.setTransferMode(AudioTrack.MODE_STREAM)
.build()
try {
track.performanceMode == AudioTrack.PERFORMANCE_MODE_LOW_LATENCY
} finally {
track.release()
}
} catch (e: Exception) {
Log.w(TAG, "audio: could not check for low-latency output; DirectAudio keeps its defaults", e)
true
}
}
@@ -26,6 +26,7 @@ public class PulseAudioComponent extends SessionPart {
private final File workingDir;
private final File deviceControls;
private File logFile;
private final String micFifoPath;
private volatile int pid = -1;
@@ -36,6 +37,8 @@ public class PulseAudioComponent extends SessionPart {
public PulseAudioComponent(Context context, String micFifoPath) {
this.workingDir = new File(context.getFilesDir(), "pulseaudio");
this.deviceControls = com.droiddeck.launcher.runtime.DeviceControlsComponent.directory(
com.droiddeck.launcher.runtime.LinuxRuntime.rootDir(context));
this.micFifoPath = micFifoPath;
}
@@ -94,7 +97,7 @@ public class PulseAudioComponent extends SessionPart {
config.add("load-module module-native-protocol-unix auth-anonymous=1 auth-cookie-enabled=0 socket=\""
+ socket().getAbsolutePath() + "\"");
config.add(".nofail");
config.add("load-module module-directaudio-native-sink sink_name=DirectAudio performance_mode=1 adaptive=1 decay=0 limiter=1 watchdog=1 volume=1.0");
config.add("load-module module-directaudio-native-sink sink_name=DirectAudio performance_mode=1 adaptive=1 decay=0 limiter=1 watchdog=1 volume=1.0 control_dir=" + deviceControls);
config.add("set-default-sink DirectAudio");
config.add(".fail");
if (micFifoPath != null && !micFifoPath.isEmpty()) {
@@ -127,22 +130,46 @@ public class PulseAudioComponent extends SessionPart {
private void ensureClientSink(java.io.PrintWriter out) {
try {
long deadline = System.currentTimeMillis() + 15000L;
// Nothing that may block (a sink whose AAudio open hangs: a Galaxy S24 FE (US) hung the
// daemon's start-up loading one from default.pa) is loaded by the daemon itself; the
// fallbacks are loaded here, after it is up, each pactl call bounded by its timeout.
long started = System.currentTimeMillis();
long deadline = started + 30000L;
String sinks = null;
boolean socketSeen = false;
while (System.currentTimeMillis() < deadline && pid > 1) {
if (socket().exists() && (sinks = pactl("list", "short", "sinks")) != null) break;
socketSeen |= socket().exists();
if (socketSeen && (sinks = pactl("list", "short", "sinks")) != null) break;
try { Thread.sleep(250L); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; }
}
note(out, "client sink: daemon " + (sinks != null ? "answered" : "did not answer") + " after "
+ (System.currentTimeMillis() - started) + " ms (socket " + (socketSeen ? "present" : "never appeared") + ", pactl "
+ (new File(workingDir, "pactl").isFile() ? "present" : "MISSING") + ", daemon pid " + pid + ")");
String line;
if (sinks == null) {
line = "== client sink: the daemon did not answer in 15 s; no fallback attempted";
line = "== client sink: the daemon did not answer in 30 s; no fallback attempted";
} else if (sinks.contains("\tDirectAudio\t") || sinks.startsWith("DirectAudio\t") || sinks.contains("\tDirectAudio\n") || sinks.matches("(?s).*\\bDirectAudio\\b.*")) {
line = "== client sink: DirectAudio (module-directaudio-native-sink)";
} else {
boolean loaded = pactl("load-module", "module-aaudio-sink", "sink_name=AAudioSink", "performance_mode=1", "adaptive=1", "volume=1.0") != null
&& pactl("set-default-sink", "AAudioSink") != null;
line = loaded ? "== client sink: DirectAudio did not come up; FALLBACK to the plain AAudio sink (AAudioSink)"
: "== client sink: DirectAudio did not come up and the AAudio fallback failed too - no client sound";
// AAudio can refuse the first open with AAUDIO_ERROR_DISCONNECTED while Android is
// still switching the output route as the session starts (seen on a Galaxy S24 FE):
// try DirectAudio again, then the plain sink, a few times over ten seconds.
String loadedAs = null;
for (int attempt = 1; attempt <= 5 && loadedAs == null && pid > 1; attempt++) {
try { Thread.sleep(2000L); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; }
if (pactl("load-module", "module-directaudio-native-sink", "sink_name=DirectAudio", "performance_mode=1",
"adaptive=1", "decay=0", "limiter=1", "watchdog=1", "volume=1.0", "control_dir=" + deviceControls) != null
&& pactl("set-default-sink", "DirectAudio") != null) {
loadedAs = "DirectAudio on attempt " + attempt;
} else if (pactl("load-module", "module-aaudio-sink", "sink_name=AAudioSink",
"performance_mode=" + (attempt == 1 ? "1" : "0"), "adaptive=1", "volume=1.0", "control_dir=" + deviceControls) != null
&& pactl("set-default-sink", "AAudioSink") != null) {
loadedAs = "the plain AAudio sink (AAudioSink) on attempt " + attempt;
}
}
line = loadedAs != null ? "== client sink: DirectAudio did not come up at start; retried and got " + loadedAs
: "== client sink: DirectAudio did not come up and the AAudio fallback failed too after 5 retries - no client sound."
+ " If audio.log shows AAUDIO_ERROR_DISCONNECTED, Android refused the audio output: rebooting the phone usually fixes this";
Log.w(TAG, line);
}
Log.i(TAG, line);
@@ -152,6 +179,14 @@ public class PulseAudioComponent extends SessionPart {
}
}
/** A timestamped line in the session's audio log. */
private void note(java.io.PrintWriter out, String what) {
String line = "== " + new java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.US).format(new java.util.Date()) + " " + what;
Log.i(TAG, line);
if (out != null) synchronized (out) { out.println(line); out.flush(); }
}
/** Runs pactl against this daemon; its stdout, or null when it failed or timed out. */
private String pactl(String... args) {
File pactl = new File(workingDir, "pactl");
if (!pactl.isFile() || pid <= 1) return null;
@@ -13,15 +13,10 @@ import kotlinx.coroutines.launch
import java.io.File
import java.io.FileInputStream
/**
* A pack built by CI alongside this APK (tools/packs). Main and release builds publish it to
* DroidDeck-Components under a content-addressed tag; a PR test build attaches packs that are not there
* yet to its pr-N release on DroidDeck-CI. The APK pins the pack's SHA-256, so only the bytes this build
* was made with install, whichever host serves them.
*/
internal class AddonPack(
val name: String, private val sha256: String, val size: Long, private val required: List<String>,
private val reuseDeviceCopy: Boolean = sha256.isEmpty() && !BuildConfig.CI_BUILD,
private val bundle: (Context) -> java.io.InputStream = { it.assets.open("packs/$name.tar.zst") },
) {
sealed interface State {
data object Missing : State
@@ -42,13 +37,11 @@ internal class AddonPack(
)
private fun root(context: Context) = File(context.filesDir, "addons/$name")
/** App-private and written only after the archive matched the pinned digest. */
fun dir(context: Context) = File(root(context), version(context))
fun file(context: Context, relative: String) = File(dir(context), relative)
fun installed(context: Context): Boolean = version(context).isNotEmpty() && File(dir(context), COMPLETE).isFile
// A build made outside CI pins no packs, so it runs the newest one a CI build already verified and installed.
private fun version(context: Context): String = if (!reuseDeviceCopy) sha256 else deviceCopy(context).orEmpty()
private fun deviceCopy(context: Context): String? = root(context).listFiles().orEmpty()
@@ -65,7 +58,6 @@ internal class AddonPack(
.also { mutableState.value = it }
}
/** Starts the download in the background unless the pack is already present or on its way. */
fun ensure(context: Context, onReady: (Context) -> Unit = {}) {
val app = context.applicationContext
synchronized(lock) {
@@ -80,7 +72,6 @@ internal class AddonPack(
}
}
/** Blocks a worker thread until the pack is installed, starting the download if needed. */
@JvmOverloads
fun await(context: Context, running: () -> Boolean = { true }, onState: (State) -> Unit = {}): Boolean {
if (installed(context)) return true
@@ -104,7 +95,7 @@ internal class AddonPack(
}
val root = root(context).apply { mkdirs() }
val archive = File(root, "$sha256.tar.zst")
val downloaded = urls.any { url ->
val downloaded = bundled(context, archive) || urls.any { url ->
Downloader.downloadFile(url, archive, true, sha256) { p -> mutableState.value = State.Downloading(p.coerceIn(0f, 1f)) }
.also { if (!it) archive.delete() }
}
@@ -126,6 +117,16 @@ internal class AddonPack(
return true
}
internal fun bundled(context: Context, archive: File): Boolean {
val copied = runCatching {
bundle(context).use { input -> archive.outputStream().use { input.copyTo(it) } }
}.isSuccess
if (copied && Hashes.sha256(archive).equals(sha256, ignoreCase = true)) return true
if (copied) Log.w(TAG, "$name: the bundled pack does not match the pinned digest")
archive.delete()
return false
}
companion object {
private const val TAG = "AddonPack"
private const val REPO = "Droid-Deck/DroidDeck-Components"
@@ -146,5 +147,10 @@ internal class AddonPack(
listOf("version", "xodus-cli", "xodus-service"))
val COMET = AddonPack("comet", BuildConfig.PACK_COMET_SHA256, BuildConfig.PACK_COMET_SIZE,
listOf("version", "comet"))
val BUTLER = AddonPack("butler", BuildConfig.PACK_BUTLER_SHA256, BuildConfig.PACK_BUTLER_SIZE,
listOf("version", "butler", "7z.so", "libc7zip.so"))
@JvmField
val POWERVR = AddonPack("powervr", BuildConfig.PACK_POWERVR_SHA256, BuildConfig.PACK_POWERVR_SIZE,
listOf("runtime.tzst", "version.txt"))
}
}
@@ -1,11 +1,32 @@
package com.droiddeck.launcher.core
import android.content.Context
import android.content.pm.ActivityInfo
import com.droiddeck.launcher.R
object AppUiPrefs {
const val DEFAULT_SCALE = 100
val scales = listOf(75, 85, 100, 115, 125, 150)
val orientationOptions = listOf(
ActivityInfo.SCREEN_ORIENTATION_USER to R.string.setup_launcher_orientation_auto,
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT to R.string.setup_orientation_portrait,
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE to R.string.setup_orientation_auto,
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE to R.string.setup_orientation_landscape,
ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE to R.string.setup_orientation_reverse,
)
fun orientation(context: Context): Int =
context.getSharedPreferences("app-ui", Context.MODE_PRIVATE)
.getInt("orientation", ActivityInfo.SCREEN_ORIENTATION_USER)
.takeIf { value -> orientationOptions.any { it.first == value } }
?: ActivityInfo.SCREEN_ORIENTATION_USER
fun setOrientation(context: Context, value: Int) {
require(orientationOptions.any { it.first == value })
context.getSharedPreferences("app-ui", Context.MODE_PRIVATE).edit().putInt("orientation", value).apply()
}
internal fun normalizeScale(percent: Int): Int = percent.takeIf { it in scales } ?: DEFAULT_SCALE
fun scale(context: Context): Int = normalizeScale(
@@ -0,0 +1,59 @@
package com.droiddeck.launcher.core
import android.content.Context
import android.util.Log
import java.util.concurrent.Executors
object BackgroundAppLimit {
private const val TAG = "BackgroundAppLimit"
const val OFF = -1
val CHOICES = listOf(OFF, 4, 1, 0)
private const val KEY = "max_cached_processes"
private const val STATE = "background-app-limit"
private const val APPLIED = "applied"
private const val PREVIOUS = "previous"
private val worker = Executors.newSingleThreadExecutor { Thread(it, "background-app-limit").apply { isDaemon = true } }
internal fun readCommand() = "device_config get activity_manager $KEY"
internal fun setCommand(limit: Int): String {
require(limit in 0..1024)
return "device_config put activity_manager $KEY $limit"
}
internal fun restoreCommand(previous: String?) =
if (previous == null) "device_config delete activity_manager $KEY" else "device_config put activity_manager $KEY $previous"
internal fun parse(output: String): String? = output.trim().takeIf { it.toIntOrNull() != null }
private fun state(context: Context) = context.applicationContext.getSharedPreferences(STATE, Context.MODE_PRIVATE)
fun applied(context: Context): Boolean = state(context).getBoolean(APPLIED, false)
fun applyAsync(context: Context, limit: Int) {
val app = context.applicationContext
worker.execute {
runCatching {
if (!applied(app)) {
val previous = parse(WirelessAdbFix.shell(app, readCommand()))
check(state(app).edit().putBoolean(APPLIED, true).putString(PREVIOUS, previous).commit()) { "previous value not saved" }
}
WirelessAdbFix.shell(app, setCommand(limit))
}.onSuccess { Log.i(TAG, "at most $limit cached app(s) while the session runs") }
.onFailure { Log.w(TAG, "limit not set: ${it.message}") }
}
}
fun restoreAsync(context: Context) {
val app = context.applicationContext
worker.execute {
if (!applied(app)) return@execute
val previous = state(app).getString(PREVIOUS, null)
runCatching { WirelessAdbFix.shell(app, restoreCommand(previous)) }
.onSuccess {
state(app).edit().putBoolean(APPLIED, false).remove(PREVIOUS).commit()
Log.i(TAG, "cached-app limit put back (${previous ?: "Android's own"})")
}
.onFailure { Log.w(TAG, "limit not put back yet: ${it.message}") }
}
}
}
@@ -1,27 +0,0 @@
package com.droiddeck.launcher.core
import com.droiddeck.launcher.gpu.GpuInfo
import com.droiddeck.launcher.gpu.SocNames
object CpuAllocationDefaults {
data class Allocation(val client: String, val game: String)
val device: Allocation? by lazy {
forDevice(SocNames.model(), GpuInfo.systemProperty("ro.board.platform"),
CpuCores.all.associateWith(CpuCores::maxGhz))
}
fun forDevice(model: String, platform: String, frequencies: Map<Int, Double?>): Allocation? {
val chip = model.trim().uppercase().substringBefore('-')
val gen2 = chip in setOf("SM8550", "QCS8550") ||
(chip.isEmpty() && platform.trim().equals("kalama", ignoreCase = true))
if (!gen2 || frequencies.size != 8 || frequencies.keys.any { it < 0 } ||
frequencies.values.any { it == null || !it.isFinite() || it <= 0.0 }) return null
val clusters = frequencies.entries.groupBy { it.value }.toSortedMap(compareBy { it ?: 0.0 }).values.toList()
if (clusters.map { it.size } != listOf(3, 4, 1)) return null
return Allocation(
client = CpuCores.format(clusters[2].map { it.key }.toSet()),
game = CpuCores.format(clusters[1].map { it.key }.toSet()),
)
}
}
Loaded 100 of 1774 files, more files were not shown because too many files have changed in this diff. Show more