Merge pull request #25 from The412Banner/feat/desktop-gpu

Desktop on the GPU (Vulkan by default), emulator controllers, PC keyboard, PS3 shader fixes
This commit is contained in:
The412Banner authored and GitHub committed 2026-09-23 23:16:51 -04:00
commit f6f7470385
20 files changed
+1834 -42

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
name: Build wlroots (patched)
# The desktop's wlroots (Arch Linux ARM's wlroots0.20, what the desktop package's labwc links)
# rebuilt with tools/wlroots/patches: a Vulkan DMA-BUF allocator for the Adreno node, which is not
# a DRM device, so labwc's vulkan and gles2 renderers can start. Built in an Arch Linux ARM
# container on an arm64 runner and published as a release asset the apk build fetches
# (tools/wlroots/release.env names the tag and sha). The hosted desktop package is never changed.
on:
push:
paths:
- 'tools/wlroots/**'
- '.github/workflows/build-wlroots.yml'
workflow_dispatch:
inputs:
tag:
description: 'Release tag to publish as (e.g. wlroots-0.20.2-p1); empty = artifact only'
required: false
default: ''
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-24.04-arm
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
- name: Build in Arch Linux ARM
run: |
set -euo pipefail
docker run --rm -v "$PWD:/work" -w /work menci/archlinuxarm:base-devel bash tools/wlroots/build-in-arch.sh
sudo chown -R "$(id -u):$(id -g)" .
- uses: actions/upload-artifact@v4
with:
name: wlroots-patched
path: |
wlroots.tzst
wlroots.tzst.sha256
- name: Publish
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh release create "${{ inputs.tag }}" --title "${{ inputs.tag }}" --notes "wlroots 0.20.2 with tools/wlroots/patches (Vulkan DMA-BUF allocator for non-DRM GPU nodes); staged by the apk at /usr/local/lib/droiddeck-wlroots for the desktop's GPU renderers. sha256 $(cut -d' ' -f1 wlroots.tzst.sha256)" wlroots.tzst wlroots.tzst.sha256 || gh release upload "${{ inputs.tag }}" wlroots.tzst wlroots.tzst.sha256 --clobber
+13 -2
View File
@@ -150,7 +150,7 @@ jobs:
uses: actions/cache@v4
with:
path: app/src/main/assets/linuxfs
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/mangoapp/*') }}
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/wlroots/release.env', 'tools/mangoapp/*') }}
- name: Build libfakeinput.so and libblsession.so (glibc aarch64)
if: steps.preloads.outputs.cache-hit != 'true'
@@ -191,6 +191,15 @@ jobs:
test -f "$D/usr/local/bin/gamescope"
rm gamescope.tzst
fi
# The patched wlroots for the desktop's GPU renderers (tools/wlroots), the same way.
if [ -f tools/wlroots/release.env ]; then
. tools/wlroots/release.env
gh release download "${WLROOTS_TAG}" -R "${GITHUB_REPOSITORY}" -p wlroots.tzst -O wlroots.tzst
echo "${WLROOTS_SHA256} wlroots.tzst" | sha256sum -c -
tar --use-compress-program=unzstd -xf wlroots.tzst -C "$D"
test -f "$D/usr/local/lib/droiddeck-wlroots/libwlroots-0.20.so"
rm wlroots.tzst
fi
# Valve's aarch64 mangoapp for Deck mode's performance overlay, plus the five libraries the
# runtime lacks, pinned by sha256 in tools/mangoapp/packages.txt. Stored under the SONAME
# each is loaded by, as real files: assets cannot carry symlinks.
@@ -212,6 +221,8 @@ jobs:
ls -l "$M"
# And the desktop's launcher and labwc defaults, staged over the desktop package's copies.
install -Dm644 tools/linuxfs/desktop/droiddeck-desktop "$D/usr/local/bin/droiddeck-desktop"
install -Dm644 tools/linuxfs/desktop/droiddeck-gpu "$D/usr/local/bin/droiddeck-gpu"
install -Dm644 tools/linuxfs/desktop/droiddeck-desktop-gpu "$D/usr/local/bin/droiddeck-desktop-gpu"
install -Dm644 tools/linuxfs/desktop/autostart "$D/etc/xdg/labwc/autostart"
install -Dm644 tools/linuxfs/desktop/rc.xml "$D/etc/xdg/labwc/rc.xml"
install -Dm644 tools/linuxfs/desktop/panel.conf "$D/etc/xdg/lxqt/panel.conf"
@@ -306,7 +317,7 @@ jobs:
echo "apk: $APK"
ls -l "$APK"
sha256sum "$APK"
unzip -l "$APK" | grep -E 'lib/arm64-v8a|assets/linuxfs' | head -30
unzip -l "$APK" | grep -E 'lib/arm64-v8a|assets/linuxfs' | awk 'NR <= 30' # awk reads to the end: head would break the pipe under pipefail
# Every NEEDED of every library we ship must resolve inside the apk or on the device. The
# first build shipped libwayland-server.so without libffi.so, and the app died on
+72 -3
View File
@@ -275,6 +275,7 @@ struct surface {
int sub_x, sub_y, sub_pending_x, sub_pending_y, sub_pending;
int below_parent;
int fullscreen; /* xdg_toplevel.set_fullscreen: no client decorations */
int toplevel_committed; /* the toplevel's initial commit was answered */
char *title; /* xdg_toplevel title, for the session log */
int announced_vulkan; /* logged its first dmabuf frame */
@@ -589,6 +590,10 @@ static void map_toplevel(struct surface *s) {
s->placed ? "" : " (no desktop position yet)");
}
wl_list_insert(g_toplevels.prev, &s->toplevel_link); /* new windows start on top */
/* Without a Wine desktop the clients are whole programs (the Linux desktop, a gamescope a
* program on it started over it): a window that opens on top takes the keys too, as it would
* on any desktop, rather than the one clicked last that is now underneath it. */
if (!g_desktop) g_key_target = s;
apply_zorder();
}
@@ -884,10 +889,21 @@ static void surface_set_opaque(struct wl_client *c, struct wl_resource *r,
static void surface_set_input(struct wl_client *c, struct wl_resource *r,
struct wl_resource *region) {}
static void send_toplevel_configure(struct surface *s);
static void surface_commit(struct wl_client *c, struct wl_resource *r) {
struct surface *s = wl_resource_get_user_data(r);
struct surface *child;
/* xdg-shell: the initial commit of a toplevel is answered with a configure. One is also sent
* at get_toplevel, which Wine's driver has always had; a client that waits for the reply to its
* commit on a queue of its own (wlroots' Wayland backend - the desktop's labwc) can read that
* early one onto the wrong queue and would wait for ever. */
if (s->xdg_toplevel && !s->toplevel_committed) {
s->toplevel_committed = 1;
send_toplevel_configure(s);
}
if (s->pending_src_set) {
s->src_set = s->pending_src[2] > 0;
memcpy(s->src, s->pending_src, sizeof(s->src));
@@ -1298,6 +1314,7 @@ static void xdg_toplevel_resource_destroy(struct wl_resource *r) {
struct surface *s = wl_resource_get_user_data(r);
if (!s) return;
s->xdg_toplevel = NULL;
s->toplevel_committed = 0;
unmap_toplevel(s);
s->placed = 0;
s->hwnd = 0;
@@ -1315,8 +1332,23 @@ static void xdg_surface_get_toplevel(struct wl_client *c, struct wl_resource *r,
s->xdg_toplevel = tl;
send_toplevel_configure(s);
}
/* A popup is dismissed as soon as it is made (see the positioner below). */
static void popup_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static void popup_grab(struct wl_client *c, struct wl_resource *r, struct wl_resource *seat, uint32_t serial) {}
static void popup_reposition(struct wl_client *c, struct wl_resource *r, struct wl_resource *positioner,
uint32_t token) {}
static const struct xdg_popup_interface popup_impl = {
.destroy = popup_destroy,
.grab = popup_grab,
.reposition = popup_reposition,
};
static void xdg_surface_get_popup(struct wl_client *c, struct wl_resource *r, uint32_t id,
struct wl_resource *parent, struct wl_resource *positioner) {}
struct wl_resource *parent, struct wl_resource *positioner) {
struct wl_resource *p = wl_resource_create(c, &xdg_popup_interface, wl_resource_get_version(r), id);
if (!p) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(p, &popup_impl, NULL, NULL);
xdg_popup_send_popup_done(p);
}
static void xdg_surface_set_geometry(struct wl_client *c, struct wl_resource *r,
int32_t x, int32_t y, int32_t w, int32_t h) {}
static void xdg_surface_ack_configure(struct wl_client *c, struct wl_resource *r, uint32_t serial) {}
@@ -1333,9 +1365,40 @@ static void xdg_surface_resource_destroy(struct wl_resource *r) {
}
static void xdg_wm_base_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
/* Popups are not drawn here, but their requests must still land somewhere: a resource without an
* implementation makes libwayland abort on the first request to it - the whole compositor, and the
* app with it. A positioner takes every request and keeps nothing; a popup is dismissed at once
* (popup_done), which is what a client does with a popup it cannot show. Seen with a gamescope
* started from the Linux desktop beside labwc. */
static void positioner_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static void positioner_set_size(struct wl_client *c, struct wl_resource *r, int32_t w, int32_t h) {}
static void positioner_set_anchor_rect(struct wl_client *c, struct wl_resource *r, int32_t x, int32_t y,
int32_t w, int32_t h) {}
static void positioner_set_anchor(struct wl_client *c, struct wl_resource *r, uint32_t a) {}
static void positioner_set_gravity(struct wl_client *c, struct wl_resource *r, uint32_t g) {}
static void positioner_set_constraint_adjustment(struct wl_client *c, struct wl_resource *r, uint32_t a) {}
static void positioner_set_offset(struct wl_client *c, struct wl_resource *r, int32_t x, int32_t y) {}
static void positioner_set_reactive(struct wl_client *c, struct wl_resource *r) {}
static void positioner_set_parent_size(struct wl_client *c, struct wl_resource *r, int32_t w, int32_t h) {}
static void positioner_set_parent_configure(struct wl_client *c, struct wl_resource *r, uint32_t serial) {}
static const struct xdg_positioner_interface positioner_impl = {
.destroy = positioner_destroy,
.set_size = positioner_set_size,
.set_anchor_rect = positioner_set_anchor_rect,
.set_anchor = positioner_set_anchor,
.set_gravity = positioner_set_gravity,
.set_constraint_adjustment = positioner_set_constraint_adjustment,
.set_offset = positioner_set_offset,
.set_reactive = positioner_set_reactive,
.set_parent_size = positioner_set_parent_size,
.set_parent_configure = positioner_set_parent_configure,
};
static void xdg_wm_base_create_positioner(struct wl_client *c, struct wl_resource *r, uint32_t id) {
struct wl_resource *p = wl_resource_create(c, &xdg_positioner_interface, wl_resource_get_version(r), id);
if (p) wl_resource_set_implementation(p, NULL, NULL, NULL);
if (!p) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(p, &positioner_impl, NULL, NULL);
}
static void xdg_wm_base_get_xdg_surface(struct wl_client *c, struct wl_resource *r, uint32_t id,
struct wl_resource *surf) {
@@ -1721,9 +1784,15 @@ static void bind_dmabuf(struct wl_client *c, void *data, uint32_t ver, uint32_t
/* ------------------------------------------------------------------ wl_output */
/* wl_output v3+ has a request (release); without an implementation the first client to send it
* aborted the compositor. */
static void output_release(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static const struct wl_output_interface output_impl = { .release = output_release };
static void bind_output(struct wl_client *c, void *data, uint32_t ver, uint32_t id) {
struct wl_resource *r = wl_resource_create(c, &wl_output_interface, ver, id);
wl_resource_set_implementation(r, NULL, NULL, NULL);
if (!r) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(r, &output_impl, NULL, NULL);
wl_output_send_geometry(r, 0, 0, 340, 190, WL_OUTPUT_SUBPIXEL_UNKNOWN,
"Bannerlator", "Wayland", WL_OUTPUT_TRANSFORM_NORMAL);
wl_output_send_mode(r, WL_OUTPUT_MODE_CURRENT | WL_OUTPUT_MODE_PREFERRED,
@@ -169,7 +169,7 @@ class MainActivity : ComponentActivity() {
private var touchMode by mutableStateOf(SessionPrefs.TOUCH_AUTO)
private var suspendPolicy by mutableStateOf(SessionPrefs.SUSPEND_MANUAL)
private var oscMode by mutableStateOf(SessionPrefs.OSC_AUTO)
private var renderer by mutableStateOf("pixman")
private var renderer by mutableStateOf("vulkan")
private var gameStorage by mutableStateOf("")
private var storageOptions by mutableStateOf<List<Pair<String, String>>>(emptyList())
private val pickGameStorage = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r ->
@@ -88,6 +88,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
// Compose reads these; the activity writes them.
private var drawerOpen by mutableStateOf(false)
/** The on-screen PC keyboard (ui/PcKeyboard): real key presses, Esc and F1 included. */
private var pcKeyboardOpen by mutableStateOf(false)
private var hudOn by mutableStateOf(true)
private var frameGenLabel by mutableStateOf("Off")
private var frameGenEngine by mutableStateOf(FrameGen.ENGINE_OFF)
@@ -181,6 +183,13 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
if (loading.visible) LoadingOverlay(loading.step, loading.percent, loading.elapsed, loading.hint, loading.ended)
// Opening the drawer takes the controller away from the game: release its pad.
androidx.compose.runtime.LaunchedEffect(drawerOpen) { if (drawerOpen) padBridge?.releaseAll() }
// The PC keyboard takes it too, for moving over the keys; the drawer opens over it.
androidx.compose.runtime.LaunchedEffect(pcKeyboardOpen) { if (pcKeyboardOpen) padBridge?.releaseAll() }
if (pcKeyboardOpen) com.droiddeck.launcher.ui.PcKeyboard(
sendKey = { code, down -> if (CompositorHost.isStarted) WaylandCompositor.nativeSendKey(code, if (down) 1 else 0) },
onAndroidKeyboard = { pcKeyboardOpen = false; keyboard?.toggle() },
onClose = { pcKeyboardOpen = false },
)
SessionDrawer(drawerOpen, DrawerActions(
steam = SessionState.mode == SessionService.MODE_STEAM,
isHomeApp = isHomeApp,
@@ -200,7 +209,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
readPrefs()
applyFrameGen()
},
onKeyboard = { drawerOpen = false; keyboard?.toggle() },
onKeyboard = { drawerOpen = false; pcKeyboardOpen = false; keyboard?.toggle() },
onHardwareKeyboard = { drawerOpen = false; pcKeyboardOpen = !pcKeyboardOpen },
onSteamMenu = if (SessionState.mode == SessionService.MODE_STEAM) ({ sendSteamGuide() }) else null,
onQam = if (SessionState.mode == SessionService.MODE_STEAM) ({
drawerOpen = false
@@ -247,7 +257,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
// from a button a game might also be reading.
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
drawerOpen = !drawerOpen
// The PC keyboard closes first (B on a controller), then Back is the drawer again.
if (pcKeyboardOpen && !drawerOpen) pcKeyboardOpen = false else drawerOpen = !drawerOpen
}
})
watchSession()
@@ -612,7 +623,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
// With the drawer open the controller drives the drawer, not the game: Android's own
// handling moves focus with the d-pad and stick, and its fallbacks make A select and B Back
// (which closes the drawer) - the same as on the app's main screen.
if (drawerOpen && event.device != null && PadBridge.isFromController(event.device)) return super.dispatchKeyEvent(event)
if ((drawerOpen || pcKeyboardOpen) && event.device != null && PadBridge.isFromController(event.device)) return super.dispatchKeyEvent(event)
if (event.keyCode != KeyEvent.KEYCODE_BACK && padBridge?.onKeyEvent(event) == true) return true
// A hardware keyboard, forwarded to the compositor's wl_keyboard. Back is left to the
// activity, which opens the drawer.
@@ -654,7 +665,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
}
override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
if (drawerOpen && event.device != null && PadBridge.isFromController(event.device)) return super.dispatchGenericMotionEvent(event)
if ((drawerOpen || pcKeyboardOpen) && event.device != null && PadBridge.isFromController(event.device)) return super.dispatchGenericMotionEvent(event)
if (padBridge?.onMotionEvent(event) == true) return true
if (event.isFromSource(android.view.InputDevice.SOURCE_MOUSE) && !drawerOpen && onMouse(event)) return true
return super.dispatchGenericMotionEvent(event)
@@ -38,6 +38,7 @@ object SessionFiles {
"usr/local/bin/bannerlator-desktop-games" to "usr/local/bin/bannerlator-desktop-games",
"usr/local/bin/bannerlator-steam-shim" to "usr/local/bin/bannerlator-steam-shim",
"usr/local/bin/bannerlator-steam-shortcuts" to "usr/local/bin/bannerlator-steam-shortcuts",
"usr/local/bin/bannerlator-pad-defaults" to "usr/local/bin/bannerlator-pad-defaults",
// The SteamOS helpers the client calls in Deck mode: the two Armada found it needs, plus
// the three under /usr/bin, all no-ops that answer "nothing to do" (see each file).
"usr/bin/steamos-update" to "usr/bin/steamos-update",
@@ -56,6 +57,9 @@ object SessionFiles {
// staging them into a runtime without it would make the desktop look present when it is not.
val desktop = arrayOf(
"usr/local/bin/droiddeck-desktop" to "usr/local/bin/droiddeck-desktop",
// Games and emulators from the menu, full screen in a gamescope of their own.
"usr/local/bin/droiddeck-gpu" to "usr/local/bin/droiddeck-gpu",
"usr/local/bin/droiddeck-desktop-gpu" to "usr/local/bin/droiddeck-desktop-gpu",
"etc/xdg/labwc/autostart" to "etc/xdg/labwc/autostart",
"etc/xdg/labwc/rc.xml" to "etc/xdg/labwc/rc.xml",
"etc/xdg/lxqt/panel.conf" to "etc/xdg/lxqt/panel.conf",
@@ -75,9 +79,13 @@ object SessionFiles {
"usr/local/lib/mangoapp/libtraceevent.so.1",
"usr/local/lib/mangoapp/libtracefs.so.1",
).map { it to it }
// The patched wlroots (tools/wlroots) the desktop loads for its vulkan / gles2 renderers.
val wlroots = if (File(root, "usr/bin/labwc").isFile) {
arrayOf("usr/local/lib/droiddeck-wlroots/libwlroots-0.20.so" to "usr/local/lib/droiddeck-wlroots/libwlroots-0.20.so")
} else emptyArray()
val optional = (arrayOf(
"usr/local/bin/gamescope" to "usr/local/bin/gamescope",
) + mangoapp).filter { (asset, _) ->
) + wlroots + mangoapp).filter { (asset, _) ->
val dir = asset.substringBeforeLast('/')
runCatching { context.assets.list("linuxfs/$dir")?.contains(asset.substringAfterLast('/')) == true }.getOrDefault(false)
}
@@ -330,14 +330,18 @@ object SessionPrefs {
}
/**
* What the desktop shell composites with: pixman (software, the default - the Adreno stand-in
* is not a DRM render node, so labwc's gbm allocator cannot use it), or gles2 / vulkan for a
* device that has a real node. `Download/droiddeck-wlr-renderer` still overrides it.
* What the desktop shell composites with: vulkan (the default) or gles2 on the GPU, through the
* app's patched wlroots (tools/wlroots) - the Adreno stand-in is not a DRM device and stock
* wlroots cannot allocate on it - or pixman in software. A GPU renderer a device cannot start
* falls back to pixman by itself (droiddeck-desktop). `Download/droiddeck-wlr-renderer` still
* overrides it.
*/
fun desktopRenderer(context: Context): String = prefs(context).getString("desktopRenderer", "pixman") ?: "pixman"
fun desktopRenderer(context: Context): String = prefs(context).getString("desktopRenderer", "vulkan") ?: "vulkan"
fun setDesktopRenderer(context: Context, renderer: String) {
prefs(context).edit().putString("desktopRenderer", renderer).apply()
// A choice made again is a retry: forget that a renderer failed to start here before.
java.io.File(com.droiddeck.launcher.runtime.LinuxRuntime.rootDir(context), "root/.droiddeck-renderer-failed").delete()
}
/**
@@ -280,7 +280,12 @@ class SessionService : Service() {
// An imported glibc Turnip for this mode, when the user chose one: the session script checks
// the manifest and its library from inside and points the loader at it with VK_DRIVER_FILES,
// so the runtime's own driver above stays untouched and is what a bad import falls back to.
val linuxDriverId = SessionPrefs.linuxDriver(this, SessionPrefs.prefMode(SessionState.mode))
// A program from the rail is one of the desktop's emulators, so it draws with the desktop's
// Linux driver, not the Steam session's: the driver's shader cache is keyed on the driver
// build, and with two drivers every emulator compiled its shaders twice - once per way of
// starting it (RPCS3's 6650 interpreter variants on each first boot).
val driverMode = if (SessionState.mode == MODE_RUN) MODE_DESKTOP else SessionPrefs.prefMode(SessionState.mode)
val linuxDriverId = SessionPrefs.linuxDriver(this, driverMode)
LinuxVulkanDriver.resolveIcdPath(this, linuxDriverId)
?.let { guest.add(LinuxVulkanDriver.ENV + "=" + it) }
// Turnip's own debug switches, for the runtime's driver and everything on it. The file in
@@ -321,13 +326,13 @@ class SessionService : Service() {
// on, even naming every core: it exists to undo the pin Steam applies to its own interface
// renderer, and the scheduler's default is exactly what that pin takes away. A game's is
// sent only when it is a real restriction - a game has no pin of its own to undo.
if (SessionState.mode == MODE_STEAM) {
if (SessionPrefs.clientCpusOverride(this)) {
guest.add("BL_CLIENT_CPUS=" + CpuCores.listOrAll(SessionPrefs.clientCpus(this)))
}
CpuCores.restrictionOrEmpty(SessionPrefs.gameCpus(this))
.takeIf { it.isNotEmpty() }?.let { guest.add("BL_GAME_CPUS=$it") }
if (SessionState.mode == MODE_STEAM && SessionPrefs.clientCpusOverride(this)) {
guest.add("BL_CLIENT_CPUS=" + CpuCores.listOrAll(SessionPrefs.clientCpus(this)))
}
// The game cores also pin the desktop and a program from the rail; without a choice the
// session script picks every core but the slowest cluster for those (program_cores).
CpuCores.restrictionOrEmpty(SessionPrefs.gameCpus(this))
.takeIf { it.isNotEmpty() }?.let { guest.add("BL_GAME_CPUS=$it") }
// PulseAudio always: the client is a native Linux program and has no other way to make a
// sound - its menus, its music and its voice chat all go through here. DirectAudio is not
@@ -398,7 +403,11 @@ class SessionService : Service() {
// identity gets the standard layout without the user configuring the pad by hand.
guest.add("FAKE_EVDEV_IDENTITY=xbox360")
guest.add("FAKE_EVDEV_VIBRATION=1")
guest.add("FAKE_EVDEV_STEAM_VIRTUAL=1")
// Steam Input's virtual-gamepad identity is for games the client starts, which are
// meant to see that pad. Everywhere else (the desktop, a program from the rail) it
// hides the pad: SDL ignores a Steam virtual gamepad unless it runs under Steam, so
// every SDL emulator came up with no controller. There it is a plain Xbox 360 pad.
if (SessionState.mode == MODE_STEAM) guest.add("FAKE_EVDEV_STEAM_VIRTUAL=1")
guest.add("SDL_JOYSTICK_DISABLE_UDEV=1")
guest.add("SDL_HIDAPI_JOYSTICK_DISABLE_UDEV=1")
guest.add("SDL_JOYSTICK_HIDAPI=0")
@@ -407,12 +416,11 @@ class SessionService : Service() {
}
SessionState.fakeInputDir = fakeInputDir
}
// The desktop is wlroots (labwc), and wlroots allocates its buffers through gbm on a real
// DRM render node. Ours is a KGSL stand-in that gbm cannot use - labwc dies at "unable to
// create allocator" - so the desktop shell is composited by pixman (software, a shm
// allocator, no DRM). Accelerated clients on it pay a CPU copy; a 2D emulator does not
// notice, a demanding one does. droiddeck-wlr-renderer in Downloads (pixman/vulkan/gles2)
// overrides it, for trying acceleration on a device that has a real node.
// The desktop is wlroots (labwc). Stock wlroots allocates through gbm on a real DRM render
// node, and ours is a KGSL stand-in - labwc died at "unable to create allocator" - so the
// default is pixman (software, shm), where a Vulkan program cannot draw at all; those run
// through droiddeck-gpu instead. vulkan / gles2 use the app's patched wlroots and fall back
// to pixman by themselves. droiddeck-wlr-renderer in Downloads overrides the choice.
if (SessionState.mode == MODE_DESKTOP) {
val override = File(Environment.getExternalStorageDirectory(), "Download/droiddeck-wlr-renderer")
.takeIf { it.isFile }?.let { FileUtils.readString(it)?.trim() }
@@ -322,8 +322,11 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
if (!steam && s.renderer != null) SettingsGroup("Renderer") {
ChoiceRow(
host, "renderer", "Desktop renderer", "Composites the desktop.",
listOf("pixman" to "pixman - software", "gles2" to "gles2", "vulkan" to "vulkan"), s.renderer,
note = "GLES2 and Vulkan require a DRM render node, unavailable on most devices.", onPick = a.onRenderer,
listOf("vulkan" to "vulkan - GPU", "gles2" to "gles2 - GPU (experimental)", "pixman" to "pixman - software"), s.renderer,
note = "On Vulkan, programs on the desktop draw with the GPU in their own windows; if it cannot start, " +
"the desktop comes up on pixman. On pixman, games and emulators from the menu open full screen " +
"on the GPU instead (right-click one for a desktop window).",
onPick = a.onRenderer,
)
}
}
@@ -0,0 +1,251 @@
package com.droiddeck.launcher.ui
import android.view.KeyEvent
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
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.width
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.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.nativeKeyCode
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
/**
* A PC keyboard on screen: every key is a physical key (a Linux evdev code) pressed and released
* on the session's wl_keyboard, as a USB keyboard would - Esc, the function row, Ctrl, Alt, Super,
* the arrows and the editing block included, none of which the Android keyboard can send.
*
* A key goes down when a finger lands on it and up when it lifts, so holding one repeats it the way
* the program's own key repeat does. Ctrl, Shift, Alt and Super are sticky: one tap holds the
* modifier for the next key, a second tap locks it, a third lets it go. A controller moves over the
* keys with the d-pad and presses with A.
*/
private data class PcKey(val label: String, val code: Int, val weight: Float = 1f, val shifted: String? = null)
private fun k(label: String, code: Int, weight: Float = 1f, shifted: String? = null) = PcKey(label, code, weight, shifted)
private val ROWS: List<List<PcKey>> = listOf(
listOf(
k("Esc", 1), k("F1", 59), k("F2", 60), k("F3", 61), k("F4", 62), k("F5", 63), k("F6", 64),
k("F7", 65), k("F8", 66), k("F9", 67), k("F10", 68), k("F11", 87), k("F12", 88),
k("Ins", 110), k("Del", 111), k("Home", 102), k("End", 107), k("PgUp", 104), k("PgDn", 109),
),
listOf(
k("`", 41, shifted = "~"), k("1", 2, shifted = "!"), k("2", 3, shifted = "@"), k("3", 4, shifted = "#"),
k("4", 5, shifted = "$"), k("5", 6, shifted = "%"), k("6", 7, shifted = "^"), k("7", 8, shifted = "&"),
k("8", 9, shifted = "*"), k("9", 10, shifted = "("), k("0", 11, shifted = ")"), k("-", 12, shifted = "_"),
k("=", 13, shifted = "+"), k("⌫ Backspace", 14, 2f),
),
listOf(
k("Tab ⇥", 15, 1.5f), k("q", 16), k("w", 17), k("e", 18), k("r", 19), k("t", 20), k("y", 21),
k("u", 22), k("i", 23), k("o", 24), k("p", 25), k("[", 26, shifted = "{"), k("]", 27, shifted = "}"),
k("\\", 43, 1.5f, shifted = "|"),
),
listOf(
k("Caps", 58, 1.75f), k("a", 30), k("s", 31), k("d", 32), k("f", 33), k("g", 34), k("h", 35),
k("j", 36), k("k", 37), k("l", 38), k(";", 39, shifted = ":"), k("'", 40, shifted = "\""),
k("Enter ⏎", 28, 2.25f),
),
listOf(
k("⇧ Shift", 42, 2.25f), k("z", 44), k("x", 45), k("c", 46), k("v", 47), k("b", 48), k("n", 49),
k("m", 50), k(",", 51, shifted = "<"), k(".", 52, shifted = ">"), k("/", 53, shifted = "?"),
k("⇧ Shift", 54, 1.75f), k("↑", 103),
),
listOf(
k("Ctrl", 29, 1.5f), k("Super", 125, 1.25f), k("Alt", 56, 1.25f), k("Space", 57, 6.25f),
k("AltGr", 100, 1.25f), k("Ctrl", 97, 1.25f), k("←", 105), k("↓", 108), k("→", 106),
),
)
private val MODIFIERS = setOf(42, 54, 29, 97, 56, 100, 125)
private const val MOD_OFF = 0
private const val MOD_ONCE = 1
private const val MOD_LOCKED = 2
@Composable
fun PcKeyboard(
/** evdev code, pressed. */
sendKey: (Int, Boolean) -> Unit,
onAndroidKeyboard: () -> Unit,
onClose: () -> Unit,
) {
val pal = LocalPalette.current
val colors = MaterialTheme.colorScheme
val mods = remember { mutableStateMapOf<Int, Int>() }
var capsOn by remember { mutableStateOf(false) }
val firstKey = remember { FocusRequester() }
val shifted = (mods[42] ?: MOD_OFF) != MOD_OFF || (mods[54] ?: MOD_OFF) != MOD_OFF
fun releaseOnceModifiers() {
for ((code, state) in mods.toMap()) if (state == MOD_ONCE) { sendKey(code, false); mods[code] = MOD_OFF }
}
fun press(key: PcKey) {
if (key.code in MODIFIERS) {
when (mods[key.code] ?: MOD_OFF) {
MOD_OFF -> { sendKey(key.code, true); mods[key.code] = MOD_ONCE }
MOD_ONCE -> mods[key.code] = MOD_LOCKED
else -> { sendKey(key.code, false); mods[key.code] = MOD_OFF }
}
return
}
sendKey(key.code, true)
}
fun release(key: PcKey) {
if (key.code in MODIFIERS) return
sendKey(key.code, false)
if (key.code == 58) capsOn = !capsOn
releaseOnceModifiers()
}
// Nothing is left held when the keyboard goes away.
androidx.compose.runtime.DisposableEffect(Unit) {
onDispose { for ((code, state) in mods.toMap()) if (state != MOD_OFF) sendKey(code, false) }
}
LaunchedEffect(Unit) { runCatching { firstKey.requestFocus() } }
BoxWithConstraints(Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) {
// Slightly under half the screen, header included, so the program stays in view above it:
// six rows share what is left of 47% after the header, the gaps and the padding.
val rowHeight = ((maxHeight * 0.47f - 24.dp - 3.dp * 6 - 10.dp) / 6).coerceIn(24.dp, 44.dp)
Box(
Modifier.fillMaxWidth()
.background(Color(0xE6101418), RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp))
.border(1.dp, pal.line2, RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp))
// Touches on the keyboard stop here; the program underneath never sees them.
.pointerInput(Unit) { awaitEachGesture { awaitFirstDown().consume(); waitForUpOrCancellation()?.consume() } }
.padding(horizontal = 8.dp, vertical = 5.dp),
) {
Column(verticalArrangement = Arrangement.spacedBy(3.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().height(24.dp)) {
Text("PC keyboard", fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = colors.onSurfaceVariant)
Spacer(Modifier.weight(1f))
HeaderButton("Android keyboard", onAndroidKeyboard)
Spacer(Modifier.width(8.dp))
HeaderButton("Hide ✕", onClose)
}
ROWS.forEachIndexed { r, row ->
Row(horizontalArrangement = Arrangement.spacedBy(3.dp), modifier = Modifier.fillMaxWidth().height(rowHeight)) {
row.forEachIndexed { i, key ->
val state = mods[key.code] ?: MOD_OFF
val lit = state != MOD_OFF || (key.code == 58 && capsOn)
val label = when {
shifted && key.shifted != null -> key.shifted
key.label.length == 1 && key.label[0].isLetter() && (shifted xor capsOn) -> key.label.uppercase()
else -> key.label
}
KeyCap(
label, key.weight, lit, locked = state == MOD_LOCKED,
modifier = if (r == 0 && i == 0) Modifier.focusRequester(firstKey) else Modifier,
onDown = { press(key) }, onUp = { release(key) },
)
}
}
}
}
}
}
}
@Composable
private fun RowScope.KeyCap(
label: String, weight: Float, lit: Boolean, locked: Boolean,
modifier: Modifier, onDown: () -> Unit, onUp: () -> Unit,
) {
val pal = LocalPalette.current
val src = remember { MutableInteractionSource() }
val focused by src.collectIsFocusedAsState()
var down by remember { mutableStateOf(false) }
val fill by animateColorAsState(
when {
down -> pal.signal.copy(alpha = 0.55f)
lit -> pal.signal.copy(alpha = if (locked) 0.45f else 0.28f)
else -> Color.White.copy(alpha = 0.07f)
}, label = "keyFill",
)
val edge = if (focused) pal.signal else if (lit) pal.signal.copy(alpha = 0.7f) else Color.White.copy(alpha = 0.12f)
Box(
contentAlignment = Alignment.Center,
modifier = modifier.weight(weight).fillMaxHeight()
.background(fill, RoundedCornerShape(7.dp))
.border(if (focused) 2.dp else 1.dp, edge, RoundedCornerShape(7.dp))
.pointerInput(Unit) {
awaitEachGesture {
awaitFirstDown().consume()
down = true; onDown()
waitForUpOrCancellation()?.consume()
down = false; onUp()
}
}
// A controller: A (or the d-pad's centre) is the finger.
.onPreviewKeyEvent { e ->
val kc = e.key.nativeKeyCode
if (kc != KeyEvent.KEYCODE_BUTTON_A && kc != KeyEvent.KEYCODE_DPAD_CENTER && kc != KeyEvent.KEYCODE_ENTER) return@onPreviewKeyEvent false
when (e.type) {
KeyEventType.KeyDown -> if (!down) { down = true; onDown() }
KeyEventType.KeyUp -> if (down) { down = false; onUp() }
}
true
}
.focusable(interactionSource = src),
) {
Text(
label, fontSize = if (label.length > 2) 11.sp else 15.sp, fontWeight = FontWeight.SemiBold,
color = Color.White, textAlign = TextAlign.Center, maxLines = 1,
)
}
}
@Composable
private fun HeaderButton(text: String, onClick: () -> Unit) {
val pal = LocalPalette.current
val src = remember { MutableInteractionSource() }
val focused by src.collectIsFocusedAsState()
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxHeight()
.background(Color.White.copy(alpha = 0.08f), RoundedCornerShape(7.dp))
.border(if (focused) 2.dp else 1.dp, if (focused) pal.signal else Color.White.copy(alpha = 0.14f), RoundedCornerShape(7.dp))
.clickable(interactionSource = src, indication = LocalIndication.current, onClick = onClick)
.padding(horizontal = 12.dp),
) { Text(text, fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = Color.White) }
}
@@ -158,7 +158,10 @@ class DrawerActions(
val selectedSecondScreenDisplay: Int,
val onHud: (Boolean) -> Unit,
val onFrameGenPick: (engine: String, multiplier: Int) -> Unit,
/** The Android keyboard (text, turned into key presses). */
val onKeyboard: () -> Unit,
/** The on-screen PC keyboard: real keys, Esc, F1-F12, Ctrl, Alt... */
val onHardwareKeyboard: () -> Unit,
val onSteamMenu: (() -> Unit)?,
val onQam: (() -> Unit)?,
val onOsc: (String) -> Unit,
@@ -320,7 +323,13 @@ fun SessionDrawer(open: Boolean, a: DrawerActions) {
listOf(SessionPrefs.OSC_AUTO to "Auto", SessionPrefs.OSC_ALWAYS to "Always", SessionPrefs.OSC_STEAM_QAM to "Steam + QAM", SessionPrefs.OSC_NEVER to "Never"), a.oscMode,
onPick = a.onOsc,
)
ActionRow("Keyboard", null, "Show") { host.open = null; a.onKeyboard() }
SettingsRow("Keyboard", null) {
Row(verticalAlignment = Alignment.CenterVertically) {
SecondaryButton("Hardware") { host.open = null; a.onHardwareKeyboard() }
Spacer(Modifier.width(8.dp))
SecondaryButton("Android") { host.open = null; a.onKeyboard() }
}
}
}
if (a.steam && a.secondScreenDisplays.isNotEmpty()) SettingsGroup("Second screen") {
+15
View File
@@ -36,3 +36,18 @@ swaybg -m fill -i /usr/share/backgrounds/steamdeck.png &
run pcmanfm-qt pcmanfm-qt --desktop --profile lxqt
run lxqt-panel lxqt-panel
/usr/lib/xdg-desktop-portal-wlr >/dev/null 2>&1 &
# Remote runs, for driving the desktop from a debugger: with ~/.droiddeck-remote-run present (only
# the app or root can create it), each script dropped into ~/.cache/droiddeck/run/ is run once,
# inside the session with its environment, its output tagged into the desktop log.
if [ -f "$HOME/.droiddeck-remote-run" ]; then
mkdir -p "$HOME/.cache/droiddeck/run"
(
while sleep 1; do
for f in "$HOME"/.cache/droiddeck/run/*.sh; do
[ -f "$f" ] || continue
mv -f "$f" "$f.running" || continue
( echo "[run] $(basename "$f") started"; bash "$f.running" 2>&1 | sed "s/^/[run] /"; echo "[run] $(basename "$f") exited"; rm -f "$f.running" ) &
done
done
) &
fi
+112 -10
View File
@@ -14,9 +14,37 @@ export QT_QPA_PLATFORMTHEME=lxqt
# The prefs for this are only advisory and a profile can override them; the environment is not.
export MOZ_DISABLE_CONTENT_SANDBOX=1 MOZ_DISABLE_GMP_SANDBOX=1 MOZ_DISABLE_RDD_SANDBOX=1
export MOZ_DISABLE_SOCKET_PROCESS_SANDBOX=1 MOZ_DISABLE_UTILITY_SANDBOX=1
# wlroots allocates through gbm on a real DRM node, which the KGSL stand-in is not: pixman
# (software, shm) is what boots here. BL_WLR_RENDERER=vulkan/gles2 tries acceleration.
export WLR_RENDERER=${BL_WLR_RENDERER:-pixman}
# pixman composites in software and gives programs shm buffers only, so a Vulkan program on it
# fails at its swapchain. vulkan (or gles2) composite on the GPU and give programs DMA-BUFs, but
# stock wlroots allocates through gbm on a real DRM node and the KGSL stand-in is not one ("unable
# to create allocator"): they need the app's patched wlroots (tools/wlroots), whose Vulkan
# allocator exports the buffers instead. BL_WLR_RENDERER picks; see the fallback at the end.
export WLR_RENDERER=${BL_WLR_RENDERER:-vulkan}
WLROOTS_DIR=/usr/local/lib/droiddeck-wlroots
BASE_LD_LIBRARY_PATH=${LD_LIBRARY_PATH:-}
use_renderer() {
export WLR_RENDERER=$1
if [ "$1" != pixman ] && [ -f "$WLROOTS_DIR/libwlroots-0.20.so" ]; then
export LD_LIBRARY_PATH=$WLROOTS_DIR${BASE_LD_LIBRARY_PATH:+:$BASE_LD_LIBRARY_PATH}
elif [ -n "$BASE_LD_LIBRARY_PATH" ]; then
export LD_LIBRARY_PATH=$BASE_LD_LIBRARY_PATH
else
unset LD_LIBRARY_PATH
fi
}
use_renderer "$WLR_RENDERER"
# A GPU renderer that failed to start here is not tried again on every desktop start (each try costs
# up to 20 s): the failure is kept with what it failed on - the renderer, the patched wlroots and the
# Linux driver - so an app update or a different driver retries, and so does picking a renderer
# again in the app's settings (which deletes the file).
FAILED_FILE=$HOME/.droiddeck-renderer-failed
renderer_key() {
echo "$WLR_RENDERER $(md5sum "$WLROOTS_DIR/libwlroots-0.20.so" 2>/dev/null | cut -d' ' -f1) ${BL_VK_DRIVER:-runtime}"
}
if [ "$WLR_RENDERER" != pixman ] && [ "$(cat "$FAILED_FILE" 2>/dev/null)" = "$(renderer_key)" ]; then
echo "== droiddeck-desktop: $WLR_RENDERER failed to start here before; using pixman (choose a renderer in the settings to retry)"
use_renderer pixman
fi
# The desktop's own log, beside the session's, so labwc and the programs on it can be read apart
# from the session that started them. The session log keeps the milestones either way.
if [ -n "${BL_DEBUG_DIR:-}" ]; then
@@ -85,12 +113,86 @@ export BL_DESKTOP=1
ln -sf /usr/local/bin/bannerlator-steam-launch /usr/local/bin/steamdeck-steam 2>/dev/null
ln -sf /usr/local/bin/bannerlator-steam-shim /usr/local/bin/steam 2>/dev/null
python3 /usr/local/bin/bannerlator-desktop-games 2>&1 | sed 's/^/[games] /'
# Player 1 on the controller in every emulator that has no profile of its own yet - now, and again
# every half minute: an emulator started for the first time writes its own config, which is then
# given the controller for its next start without waiting for the next desktop.
python3 /usr/local/bin/bannerlator-pad-defaults 2>&1 | sed 's/^/[pads] /'
( while sleep 30; do python3 /usr/local/bin/bannerlator-pad-defaults 2>&1 | sed 's/^/[pads] /'; done ) &
# Games and emulators from the menu run through droiddeck-gpu (their own gamescope on the app's
# compositor) while the desktop is on pixman, where they could not draw with the GPU.
# BL_DESKTOP_GPU=on/off overrides; each entry keeps "Run in a desktop window" either way.
gpu_entries() { python3 /usr/local/bin/droiddeck-desktop-gpu 2>&1 | sed 's/^/[gpu] /'; }
gpu_entries
echo "== droiddeck-desktop: labwc ($WLR_RENDERER) on $WAYLAND_DISPLAY"
# ~/.droiddeck-desktop-debug: labwc's own debug log (views and layer surfaces mapping) too.
DEBUG=; [ -f "$HOME/.droiddeck-desktop-debug" ] && DEBUG=-d
# A session bus: the panel's menus, the file manager and the portal all expect one.
if command -v dbus-run-session >/dev/null 2>&1; then
exec dbus-run-session -- labwc $DEBUG -C "$HOME/.config/labwc"
# ~/.droiddeck-desktop-debug: labwc's own debug log (views and layer surfaces mapping) too. The GPU
# renderers are experimental and always log in full, so a desktop that does not come up says why.
DEBUG=; { [ -f "$HOME/.droiddeck-desktop-debug" ] || [ "$WLR_RENDERER" != pixman ]; } && DEBUG=-d
run_labwc() {
echo "== droiddeck-desktop: labwc ($WLR_RENDERER${LD_LIBRARY_PATH:+, wlroots from $WLROOTS_DIR}) on $WAYLAND_DISPLAY"
# A session bus: the panel's menus, the file manager and the portal all expect one.
if command -v dbus-run-session >/dev/null 2>&1; then
dbus-run-session -- labwc $DEBUG -C "$HOME/.config/labwc"
else
labwc $DEBUG -C "$HOME/.config/labwc"
fi
}
pids_named() {
local p
for p in /proc/[0-9]*; do
[ "$(cat "$p/comm" 2>/dev/null)" = "$1" ] && echo "${p#/proc/}"
done
}
# labwc answers on its own socket (the one beside the app's wayland-0) once its event loop runs; a
# labwc stuck on the GPU never does.
labwc_answers() {
local sock
for sock in "$XDG_RUNTIME_DIR"/wayland-*; do
case $sock in *.lock|*/wayland-0) continue ;; esac
[ -S "$sock" ] || continue
WAYLAND_DISPLAY=${sock##*/} timeout 3 wayland-info >/dev/null 2>&1 && return 0
done
return 1
}
if [ "$WLR_RENDERER" = pixman ]; then
run_labwc
exit $?
fi
exec labwc $DEBUG -C "$HOME/.config/labwc"
# A GPU renderer this device cannot start either ends the desktop at once or leaves it hanging on
# the loading screen. Either way the desktop comes up on pixman instead, and the log says why.
started=$(date +%s)
run_labwc &
labwc_job=$!
answered=
while kill -0 "$labwc_job" 2>/dev/null; do
if labwc_answers; then answered=1; break; fi
[ $(( $(date +%s) - started )) -ge 20 ] && break
sleep 1
done
if [ -n "$answered" ]; then
echo "== droiddeck-desktop: labwc on $WLR_RENDERER is up after $(( $(date +%s) - started )) s"
wait "$labwc_job"
exit $?
fi
if kill -0 "$labwc_job" 2>/dev/null; then
echo "== droiddeck-desktop: labwc on $WLR_RENDERER did not answer in 20 s; stopping it"
kill -TERM $(pids_named labwc) 2>/dev/null
sleep 2
kill -KILL $(pids_named labwc) 2>/dev/null
for prog in lxqt-panel pcmanfm-qt swaybg Xwayland; do kill -KILL $(pids_named "$prog") 2>/dev/null; done
wait "$labwc_job" 2>/dev/null
rc=124
else
wait "$labwc_job"
rc=$?
fi
if [ "$rc" -ne 0 ] && [ $(( $(date +%s) - started )) -lt 40 ]; then
echo "== droiddeck-desktop: labwc on $WLR_RENDERER failed at start (exit $rc); starting on pixman instead"
renderer_key > "$FAILED_FILE"
use_renderer pixman
DEBUG=; [ -f "$HOME/.droiddeck-desktop-debug" ] && DEBUG=-d
find "$XDG_RUNTIME_DIR" -mindepth 1 -maxdepth 1 ! -name 'wayland-0*' ! -name 'keymap.xkb' -exec rm -rf {} + 2>/dev/null
gpu_entries
run_labwc
rc=$?
fi
exit $rc
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/python3
# The desktop's games and emulators, started on the GPU. Every menu entry whose categories say
# Game or Emulator gets a copy of its own in ~/.local/share/applications (which wins over
# /usr/share/applications) whose Exec runs it through droiddeck-gpu: full screen on the app's
# compositor, where Vulkan and OpenGL work. The entry keeps the old way as a second action, "Run in
# a desktop window". Run at every desktop start; copies whose original is gone, or all of them when
# the switch is off, are removed.
#
# BL_DESKTOP_GPU=auto (default: on while the desktop is composited by pixman) | on | off
import os
import re
HOME = os.environ.get("HOME", "/root")
SYSTEM_DIR = "/usr/share/applications"
USER_DIR = os.path.join(HOME, ".local/share/applications")
MARK = "X-DroidDeck-GPU=1"
# The client and its games already hand themselves to the app (bannerlator-steam-launch).
SKIP = re.compile(r"^(steam|droiddeck-game-|droiddeck-steam|steamdeck-)", re.I)
def enabled():
mode = os.environ.get("BL_DESKTOP_GPU", "auto").strip().lower()
if mode in ("on", "1", "yes", "true"):
return True
if mode in ("off", "0", "no", "false"):
return False
return os.environ.get("WLR_RENDERER", "pixman") == "pixman"
def main_group(text):
"""The [Desktop Entry] group's lines, and everything after it."""
lines = text.splitlines()
start = next((i for i, l in enumerate(lines) if l.strip() == "[Desktop Entry]"), None)
if start is None:
return None, None, None
end = next((i for i in range(start + 1, len(lines)) if lines[i].startswith("[")), len(lines))
return lines[:start + 1], lines[start + 1:end], lines[end:]
def wrap(text):
head, group, rest = main_group(text)
if group is None:
return None
keys = {}
for l in group:
if "=" in l and not l.lstrip().startswith("#"):
k, v = l.split("=", 1)
keys.setdefault(k.strip(), v)
cats = set(filter(None, keys.get("Categories", "").split(";")))
if not cats & {"Game", "Emulator"}:
return None
if keys.get("Type", "Application") != "Application" or keys.get("NoDisplay", "").lower() == "true":
return None
exec_line = keys.get("Exec", "").strip()
if not exec_line or exec_line.startswith("droiddeck-gpu "):
return None
out_group = []
actions = keys.get("Actions", "")
for l in group:
k = l.split("=", 1)[0].strip() if "=" in l else None
if k == "Exec":
out_group.append("Exec=droiddeck-gpu " + exec_line)
elif k == "Actions":
continue
else:
out_group.append(l)
while out_group and not out_group[-1].strip():
out_group.pop()
out_group.append("Actions=" + ("droiddeck-window;" + actions if actions else "droiddeck-window;"))
out_group.append(MARK)
window = ["", "[Desktop Action droiddeck-window]", "Name=Run in a desktop window", "Exec=" + exec_line]
# The entry's own actions (new window, a different mode...) run on the GPU as well.
wrapped_rest = []
for l in rest:
if l.startswith("Exec=") and not l[5:].startswith("droiddeck-gpu "):
l = "Exec=droiddeck-gpu " + l[5:]
wrapped_rest.append(l)
if wrapped_rest and wrapped_rest[0].strip():
wrapped_rest.insert(0, "")
return "\n".join(head + out_group + window + wrapped_rest) + "\n"
def ours(path):
try:
with open(path, encoding="utf-8", errors="replace") as f:
return MARK in f.read()
except OSError:
return False
def main():
on = enabled()
os.makedirs(USER_DIR, exist_ok=True)
written = set()
if on and os.path.isdir(SYSTEM_DIR):
for name in sorted(os.listdir(SYSTEM_DIR)):
if not name.endswith(".desktop") or SKIP.match(name):
continue
target = os.path.join(USER_DIR, name)
# A copy the user made or edited themselves is theirs.
if os.path.exists(target) and not ours(target):
continue
try:
with open(os.path.join(SYSTEM_DIR, name), encoding="utf-8", errors="replace") as f:
text = wrap(f.read())
except OSError:
continue
if text is None:
continue
with open(target + ".tmp", "w", encoding="utf-8") as f:
f.write(text)
os.replace(target + ".tmp", target)
written.add(name)
removed = 0
for name in os.listdir(USER_DIR):
path = os.path.join(USER_DIR, name)
if name.endswith(".desktop") and name not in written and ours(path):
os.remove(path)
removed += 1
print(f"droiddeck-desktop-gpu: {'on' if on else 'off'}, {len(written)} entries on the GPU, {removed} removed")
main()
+32
View File
@@ -0,0 +1,32 @@
#!/bin/bash
# Runs a program from the desktop on the GPU: in a gamescope of its own that draws straight into
# the app's compositor (wayland-0), not inside labwc. The desktop is composited by pixman, and
# pixman gives its windows no DMA-BUFs - a Vulkan program there fails at its swapchain
# (VK_ERROR_SURFACE_LOST, RPCS3's error). Here it gets the same path as a program started from the
# app's rail: full screen over the desktop until it exits, then the desktop is back.
# droiddeck-gpu <program> [args...]
set -u
[ $# -gt 0 ] || { echo "usage: droiddeck-gpu <program> [args...]" >&2; exit 64; }
if [ -n "${BL_DEBUG_DIR:-}" ]; then
exec >>"$BL_DEBUG_DIR/desktop.log" 2>&1
fi
width=${BL_WIDTH:-1280}
height=${BL_HEIGHT:-720}
args=(--backend wayland --expose-wayland -f -W "$width" -H "$height" -w "$width" -h "$height")
if [ "${BL_FPS:-0}" -gt 0 ]; then
args+=(-r "$BL_FPS")
elif [ "${BL_REFRESH:-0}" -gt 0 ]; then
args+=(-r "$BL_REFRESH")
fi
[ "${BL_HDR:-0}" = 1 ] && args+=(--hdr-enabled)
echo "== droiddeck-gpu $(date -u +%H:%M:%S): $* (gamescope ${width}x${height} on wayland-0)"
# gamescope is a client of the app's compositor, beside labwc; the program inside it reaches it
# through gamescope's own Xwayland, as in a Steam session.
export WAYLAND_DISPLAY=wayland-0
export GDK_BACKEND=x11 QT_QPA_PLATFORM=xcb SDL_VIDEODRIVER=x11
# AppImages cannot mount under proot; the menu entries already say so, a hand-typed one may not.
export APPIMAGE_EXTRACT_AND_RUN=1
gamescope "${args[@]}" -- "$@"
rc=$?
echo "== droiddeck-gpu $(date -u +%H:%M:%S): exited $rc"
exit $rc
@@ -0,0 +1,457 @@
#!/usr/bin/python3
# Player 1 on the session's controller, for every emulator that would otherwise start on the
# keyboard. The session's pad is an Xbox 360 controller (libfakeinput's identity outside Steam) that
# SDL maps by itself; what an emulator lacks is a profile that points Player 1 at it. One is
# written only where the emulator has none yet - a profile the user made is never touched.
# Run at every desktop start and before every program from the rail.
import os
HOME = os.environ.get("HOME", "/root")
CONFIG = os.environ.get("XDG_CONFIG_HOME", os.path.join(HOME, ".config"))
def seed(path, text, what, done="Player 1 on the controller"):
if os.path.exists(path):
return False
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path + ".tmp", "w", encoding="utf-8") as f:
f.write(text)
os.replace(path + ".tmp", path)
print(f"bannerlator-pad-defaults: {what}: {done} ({path})")
return True
# RPCS3: its global input configuration (rpcs3::utils::get_input_config_dir() + "Default.yml").
# Without one it adds the keyboard pad handler (pad_config.cpp, cfg_input::load). SDL handler
# names from rpcs3/Input/sdl_pad_handler.cpp (button_list, init_config); the device is
# "<SDL gamepad name> <n>" (sdl_pad_handler::enumerate_devices).
RPCS3_PLAYER1 = """Player 1 Input:
Handler: SDL
Device: Xbox 360 Controller 1
Config:
Left Stick Left: LS X-
Left Stick Down: LS Y-
Left Stick Right: LS X+
Left Stick Up: LS Y+
Right Stick Left: RS X-
Right Stick Down: RS Y-
Right Stick Right: RS X+
Right Stick Up: RS Y+
Start: Start
Select: Back
PS Button: Guide
Square: West
Cross: South
Circle: East
Triangle: North
Left: Left
Down: Down
Right: Right
Up: Up
R1: RB
R2: RT
R3: RS
L1: LB
L2: LT
L3: LS
Left Stick Deadzone: 8000
Right Stick Deadzone: 8000
Left Stick Anti-Deadzone: 4259
Right Stick Anti-Deadzone: 4259
Left Trigger Threshold: 0
Right Trigger Threshold: 0
Color Value B: 20
LED battery indicator brightness: 10
Buddy Device: ""
"""
def rpcs3():
seed(os.path.join(CONFIG, "rpcs3/input_configs/global/Default.yml"), RPCS3_PLAYER1, "RPCS3")
rpcs3_shader_mode()
# RPCS3's default shader mode (Async Recompiler with Shader Interpreter) builds 6650 interpreter
# pipelines at the start of every boot, minutes on an Adreno. The async recompiler compiles only
# what a game draws, in the background, once. A new install starts with it (a config.yml that
# holds only this is merged over RPCS3's defaults); an existing one still on the default is moved
# once, and a marker keeps any later choice the user makes in RPCS3's own settings.
RPCS3_INTERPRETER = "Async Recompiler with Shader Interpreter"
RPCS3_RECOMPILER = "Async Recompiler (multi-threaded)" # the GUI says "Async Shader Recompiler"; this is the value (system_config_types.cpp)
def rpcs3_shader_mode():
d = os.path.join(CONFIG, "rpcs3")
marker = os.path.join(d, ".droiddeck-shader-mode")
if os.path.exists(marker):
return
cfg = os.path.join(d, "config.yml")
if not os.path.isfile(cfg):
if seed(cfg, f"Video:\n Shader Mode: {RPCS3_RECOMPILER}\n", "RPCS3", f"shader mode {RPCS3_RECOMPILER}"):
open(marker, "w").close()
return
with open(cfg, encoding="utf-8", errors="replace") as f:
text = f.read()
if f"Shader Mode: {RPCS3_INTERPRETER}" in text:
text = text.replace(f"Shader Mode: {RPCS3_INTERPRETER}", f"Shader Mode: {RPCS3_RECOMPILER}", 1)
with open(cfg + ".tmp", "w", encoding="utf-8") as f:
f.write(text)
os.replace(cfg + ".tmp", cfg)
print(f"bannerlator-pad-defaults: RPCS3: shader mode {RPCS3_RECOMPILER} ({cfg})")
open(marker, "w").close()
# ---- ini helpers (DuckStation and PCSX2 share one ini dialect, keys may repeat) -------------
def ini_section(lines, name):
"""(start, end) line indices of [name]'s body, or None."""
start = None
for i, l in enumerate(lines):
t = l.strip()
if t.startswith("[") and t.endswith("]"):
if start is not None:
return start, i
if t[1:-1].strip() == name:
start = i + 1
return (start, len(lines)) if start is not None else None
def patch_ini_section(path, section, body, what, needle="SDL-", require=None):
"""Add body's lines to [section] of an ini the emulator wrote itself, unless the section
already binds a controller (needle). Missing file: nothing - the emulator writes its own
defaults on first run and this runs again."""
if not os.path.isfile(path):
return
with open(path, encoding="utf-8", errors="replace") as f:
text = f.read()
if require and require not in text:
return
lines = text.splitlines()
span = ini_section(lines, section)
if span and any(needle in l for l in lines[span[0]:span[1]]):
return
new = body.strip("\n").splitlines()
if span:
# The controller type the emulator already wrote stays; only bindings are added.
have = {l.split("=", 1)[0].strip() for l in lines[span[0]:span[1]] if "=" in l}
new = [l for l in new if not (l.split("=", 1)[0].strip() == "Type" and "Type" in have)]
end = span[1]
while end > span[0] and not lines[end - 1].strip():
end -= 1
lines[end:end] = new
else:
lines += ["", f"[{section}]"] + new
with open(path + ".tmp", "w", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
os.replace(path + ".tmp", path)
print(f"bannerlator-pad-defaults: {what}: Player 1 on the controller ({path})")
# DuckStation: SDL-<n>/<name>, axes signed (src/util/sdl_input_source.cpp); Pad1 keys from
# src/core/analog_controller.cpp. Its defaults bind the keyboard only (settings.cpp).
DUCKSTATION_PAD1 = """Type = AnalogController
Up = SDL-0/DPadUp
Down = SDL-0/DPadDown
Left = SDL-0/DPadLeft
Right = SDL-0/DPadRight
Cross = SDL-0/A
Circle = SDL-0/B
Square = SDL-0/X
Triangle = SDL-0/Y
Select = SDL-0/Back
Start = SDL-0/Start
Analog = SDL-0/Guide
L1 = SDL-0/LeftShoulder
R1 = SDL-0/RightShoulder
L2 = SDL-0/+LeftTrigger
R2 = SDL-0/+RightTrigger
L3 = SDL-0/LeftStick
R3 = SDL-0/RightStick
LLeft = SDL-0/-LeftX
LRight = SDL-0/+LeftX
LUp = SDL-0/-LeftY
LDown = SDL-0/+LeftY
RLeft = SDL-0/-RightX
RRight = SDL-0/+RightX
RUp = SDL-0/-RightY
RDown = SDL-0/+RightY
LargeMotor = SDL-0/LargeMotor
SmallMotor = SDL-0/SmallMotor
"""
def duckstation():
for d in (os.path.join(CONFIG, "duckstation"), os.path.join(HOME, ".local/share/duckstation")):
patch_ini_section(os.path.join(d, "settings.ini"), "Pad1", DUCKSTATION_PAD1, "DuckStation")
# PCSX2: FaceSouth/... names (pcsx2/Input/SDLInputSource.cpp), keys from SIO/Pad/PadDualshock2.cpp.
# An ini without [UI] SettingsVersion is thrown away and rebuilt, so only a complete one is patched.
PCSX2_PAD1 = """Type = DualShock2
Up = SDL-0/DPadUp
Right = SDL-0/DPadRight
Down = SDL-0/DPadDown
Left = SDL-0/DPadLeft
Triangle = SDL-0/FaceNorth
Circle = SDL-0/FaceEast
Cross = SDL-0/FaceSouth
Square = SDL-0/FaceWest
Select = SDL-0/Back
Start = SDL-0/Start
L1 = SDL-0/LeftShoulder
R1 = SDL-0/RightShoulder
L2 = SDL-0/+LeftTrigger
R2 = SDL-0/+RightTrigger
L3 = SDL-0/LeftStick
R3 = SDL-0/RightStick
Analog = SDL-0/Guide
LUp = SDL-0/-LeftY
LRight = SDL-0/+LeftX
LDown = SDL-0/+LeftY
LLeft = SDL-0/-LeftX
RUp = SDL-0/-RightY
RRight = SDL-0/+RightX
RDown = SDL-0/+RightY
RLeft = SDL-0/-RightX
LargeMotor = SDL-0/LargeMotor
SmallMotor = SDL-0/SmallMotor
"""
def pcsx2():
patch_ini_section(os.path.join(CONFIG, "PCSX2/inis/PCSX2.ini"), "Pad1", PCSX2_PAD1, "PCSX2",
require="SettingsVersion")
# Dolphin: SDL/<id>/<SDL gamepad name>; control names from InputCommon/.../SDL/SDLGamepad.h (Left Y+
# is up). A section that exists is taken whole - keys it lacks are unbound - so both are complete.
DOLPHIN_GC = """[GCPad1]
Device = SDL/0/Xbox 360 Controller
Buttons/A = `Button S`
Buttons/B = `Button W`
Buttons/X = `Button E`
Buttons/Y = `Button N`
Buttons/Z = `Shoulder R`
Buttons/Start = `Start`
Main Stick/Up = `Left Y+`
Main Stick/Down = `Left Y-`
Main Stick/Left = `Left X-`
Main Stick/Right = `Left X+`
C-Stick/Up = `Right Y+`
C-Stick/Down = `Right Y-`
C-Stick/Left = `Right X-`
C-Stick/Right = `Right X+`
Triggers/L = `Trigger L`
Triggers/R = `Trigger R`
Triggers/L-Analog = `Trigger L`
Triggers/R-Analog = `Trigger R`
D-Pad/Up = `Pad N`
D-Pad/Down = `Pad S`
D-Pad/Left = `Pad W`
D-Pad/Right = `Pad E`
Rumble/Motor = `Motor`
"""
DOLPHIN_WII = """[Wiimote1]
Device = SDL/0/Xbox 360 Controller
Source = 1
Buttons/A = `Button S`
Buttons/B = `Trigger R`
Buttons/1 = `Button W`
Buttons/2 = `Button N`
Buttons/- = `Back`
Buttons/+ = `Start`
Buttons/Home = `Guide`
D-Pad/Up = `Pad N`
D-Pad/Down = `Pad S`
D-Pad/Left = `Pad W`
D-Pad/Right = `Pad E`
IR/Up = `Right Y+`
IR/Down = `Right Y-`
IR/Left = `Right X-`
IR/Right = `Right X+`
Shake/X = `Button E`
Shake/Y = `Button E`
Shake/Z = `Button E`
Rumble/Motor = `Motor`
Extension = Nunchuk
Nunchuk/Buttons/C = `Shoulder L`
Nunchuk/Buttons/Z = `Trigger L`
Nunchuk/Stick/Up = `Left Y+`
Nunchuk/Stick/Down = `Left Y-`
Nunchuk/Stick/Left = `Left X-`
Nunchuk/Stick/Right = `Left X+`
"""
def dolphin():
d = os.path.join(CONFIG, "dolphin-emu")
seed(os.path.join(d, "GCPadNew.ini"), DOLPHIN_GC, "Dolphin (GameCube)")
seed(os.path.join(d, "WiimoteNew.ini"), DOLPHIN_WII, "Dolphin (Wii)")
# Cemu: an SDLController profile for the GamePad. The uuid is "<index>_<SDL GUID>" and SDL's GUID
# carries a CRC of the device name; this is the one SDL reports for the session's pad
# ("Xbox 360 Controller (0)", 045e:028e - read from RPCS3's SDL log on the device). Mapping ids
# from VPADController.h, values from Controller.h, Cemu's own SDL default layout plus Home.
CEMU_MAP = [(1, 1), (2, 0), (3, 3), (4, 2), (5, 9), (6, 10), (7, 42), (8, 43), (9, 6), (10, 4),
(11, 11), (12, 12), (13, 13), (14, 14), (15, 7), (16, 8), (17, 45), (18, 39), (19, 44),
(20, 38), (21, 47), (22, 41), (23, 46), (24, 40), (27, 5)]
CEMU_XML = """<?xml version="1.0" encoding="UTF-8"?>
<emulated_controller>
\t<type>Wii U GamePad</type>
\t<toggle_display>0</toggle_display>
\t<controller>
\t\t<api>SDLController</api>
\t\t<uuid>0_03005e615e0400008e02000010010000</uuid>
\t\t<display_name>Xbox 360 Controller</display_name>
\t\t<rumble>0.5</rumble>
\t\t<mappings>
""" + "".join(f"\t\t\t<entry>\n\t\t\t\t<mapping>{m}</mapping>\n\t\t\t\t<button>{b}</button>\n\t\t\t</entry>\n" for m, b in CEMU_MAP) + """\t\t</mappings>
\t</controller>
</emulated_controller>
"""
def cemu():
seed(os.path.join(CONFIG, "Cemu/controllerProfiles/controller0.xml"), CEMU_XML, "Cemu")
# melonDS 1.x: raw SDL joystick indices. The session's pad has the kernel's Xbox 360 layout
# (buttons A B X Y LB RB Back Start Guide LS RS = 0..10, hat 0 for the d-pad); the d-pad values
# also take the left stick (EmuInstanceInput.cpp; encoding InputConfig/MapButton.h).
MELONDS_JOY = """[Instance0.Joystick]
A = 1
B = 0
X = 3
Y = 2
L = 4
R = 5
Select = 6
Start = 7
Up = 17891585
Down = 16843012
Left = 1114376
Right = 65794
"""
def melonds():
path = os.path.join(CONFIG, "melonDS/melonDS.toml")
if not os.path.isfile(path):
seed(path, "[Instance0]\nJoystickID = 0\n\n" + MELONDS_JOY, "melonDS")
return
with open(path, encoding="utf-8", errors="replace") as f:
text = f.read()
try:
import tomllib
joy = tomllib.loads(text).get("Instance0", {}).get("Joystick", {})
except Exception:
return
if any(isinstance(v, int) and v != -1 for v in joy.values()):
return # something is bound already
# Replace melonDS's own all-unbound table with ours (a TOML table can be defined only once).
out, skipping = [], False
for line in text.splitlines():
t = line.strip()
if t.startswith("["):
skipping = t == "[Instance0.Joystick]"
if not skipping:
out.append(line)
with open(path + ".tmp", "w", encoding="utf-8") as f:
f.write("\n".join(out).rstrip("\n") + "\n\n" + MELONDS_JOY)
os.replace(path + ".tmp", path)
print(f"bannerlator-pad-defaults: melonDS: Player 1 on the controller ({path})")
# PPSSPP (SDL): its pad defaults leave the shoulders, the right stick and the triggers unbound on
# SDL builds (SDLJoystick.cpp codes vs KeyMapDefaults.cpp); a [ControlMapping] that exists replaces
# every default, so this one is complete, keyboard included (device 1 keyboard, 10 pad).
PPSSPP_CONTROLS = """[ControlMapping]
Up = 1-19,10-19
Down = 1-20,10-20
Left = 1-21,10-21
Right = 1-22,10-22
Cross = 1-54,10-189
Circle = 1-52,10-190
Square = 1-29,10-191
Triangle = 1-47,10-188
Start = 1-62,10-197
Select = 1-66,10-196
L = 1-45,10-193
R = 1-51,10-192
An.Up = 1-37,10-4003
An.Down = 1-39,10-4002
An.Left = 1-38,10-4001
An.Right = 1-40,10-4000
RightAn.Up = 10-4007
RightAn.Down = 10-4006
RightAn.Left = 10-4005
RightAn.Right = 10-4004
Pause = 1-111,10-4
"""
def ppsspp():
seed(os.path.join(CONFIG, "ppsspp/PSP/SYSTEM/controls.ini"), PPSSPP_CONTROLS, "PPSSPP")
# RetroArch: the udev joypad driver finds nothing here (no udev database in the runtime); sdl2
# does, and matches this profile by vendor/product (task_autodetect.c).
RETROARCH_PROFILE = """input_driver = "sdl2"
input_device = "Xbox 360 Controller"
input_vendor_id = "1118"
input_product_id = "654"
input_b_btn = "0"
input_a_btn = "1"
input_y_btn = "2"
input_x_btn = "3"
input_select_btn = "4"
input_menu_toggle_btn = "5"
input_start_btn = "6"
input_l3_btn = "7"
input_r3_btn = "8"
input_l_btn = "9"
input_r_btn = "10"
input_up_btn = "11"
input_down_btn = "12"
input_left_btn = "13"
input_right_btn = "14"
input_l2_axis = "+4"
input_r2_axis = "+5"
input_l_x_plus_axis = "+0"
input_l_x_minus_axis = "-0"
input_l_y_plus_axis = "+1"
input_l_y_minus_axis = "-1"
input_r_x_plus_axis = "+2"
input_r_x_minus_axis = "-2"
input_r_y_plus_axis = "+3"
input_r_y_minus_axis = "-3"
"""
def retroarch():
d = os.path.join(CONFIG, "retroarch")
seed(os.path.join(d, "autoconfig/sdl2/Xbox 360 Controller.cfg"), RETROARCH_PROFILE, "RetroArch (profile)")
cfg = os.path.join(d, "retroarch.cfg")
if not os.path.isfile(cfg):
seed(cfg, 'input_joypad_driver = "sdl2"\ninput_autodetect_enable = "true"\n', "RetroArch")
return
with open(cfg, encoding="utf-8", errors="replace") as f:
lines = f.read().splitlines()
changed = False
for i, l in enumerate(lines):
if l.replace(" ", "").startswith('input_joypad_driver="udev"'):
lines[i] = 'input_joypad_driver = "sdl2"'
changed = True
if changed:
with open(cfg + ".tmp", "w", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
os.replace(cfg + ".tmp", cfg)
print(f"bannerlator-pad-defaults: RetroArch: joypad driver udev -> sdl2 ({cfg})")
for step in (rpcs3, duckstation, pcsx2, dolphin, cemu, melonds, ppsspp, retroarch):
try:
step()
except Exception as e:
print(f"bannerlator-pad-defaults: {step.__name__}: {e}")
@@ -79,6 +79,28 @@ export XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config}
export XDG_CACHE_HOME=${XDG_CACHE_HOME:-$HOME/.cache}
mkdir -p "$XDG_DATA_HOME" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME"
# The cores a program runs on outside Steam (the desktop and everything started from it, a
# program from the rail): the user's game cores when set (BL_GAME_CPUS), else every core but the
# slowest cluster - Android is free to park an emulator's threads on the little cores, and those
# are what compile shaders and recompile PPU/SPU code. BL_PIN_CORES=0 leaves the scheduler alone.
program_cores() {
[ "${BL_PIN_CORES:-1}" = 0 ] && return 0
command -v taskset >/dev/null 2>&1 || return 0
if [ -n "${BL_GAME_CPUS:-}" ]; then echo "$BL_GAME_CPUS"; return 0; fi
local f n max min= list=
for f in /sys/devices/system/cpu/cpu[0-9]*/cpufreq/cpuinfo_max_freq; do
max=$(cat "$f" 2>/dev/null) || continue
[ -z "$min" ] || [ "$max" -lt "$min" ] && min=$max
done
[ -n "$min" ] || return 0
for f in /sys/devices/system/cpu/cpu[0-9]*/cpufreq/cpuinfo_max_freq; do
max=$(cat "$f" 2>/dev/null) || continue
n=${f#/sys/devices/system/cpu/cpu}; n=${n%%/*}
[ "$max" -gt "$min" ] && list="$list${list:+,}$n"
done
echo "$list"
}
if [ -z "${BL_INSIDE:-}" ]; then
# The compositor may still be starting, and a socket left by an earlier session is not
# a compositor: probe until a connection succeeds.
@@ -91,6 +113,11 @@ if [ -z "${BL_INSIDE:-}" ]; then
if [ "$mode" = lxqt ]; then
step "starting the desktop"
[ -x /usr/local/bin/droiddeck-desktop ] || { echo "== the desktop package is not installed"; exit 66; }
cores=$(program_cores)
if [ -n "$cores" ]; then
echo "== desktop cores: $cores (the desktop and every program started from it)"
exec taskset -c "$cores" /usr/local/bin/droiddeck-desktop
fi
exec /usr/local/bin/droiddeck-desktop
fi
width=${BL_WIDTH:-1280}
@@ -162,6 +189,13 @@ case $mode in
run)
target=${1:?program path}
shift
cores=$(program_cores)
if [ -n "$cores" ]; then
echo "== program cores: $cores"
taskset -cp "$cores" $$ >/dev/null 2>&1 || true
fi
# Player 1 on the controller in an emulator that has no profile of its own yet.
python3 /usr/local/bin/bannerlator-pad-defaults 2>&1 || true
case ${target,,} in
*.appimage)
export APPIMAGE_EXTRACT_AND_RUN=1
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/bash
# Runs INSIDE an Arch Linux ARM container (menci/archlinuxarm:base-devel) on an arm64 runner.
# Builds Arch's own wlroots0.20 package - the wlroots the desktop package's labwc links - with the
# patches in ./patches added, keeps only the library, and packs it as wlroots.tzst
# (usr/local/lib/droiddeck-wlroots/libwlroots-0.20.so) for the apk to stage. The desktop launcher
# puts that directory first on labwc's library path for the GPU renderers only.
set -euxo pipefail
VERSION=${WLROOTS_VERSION:-0.20.2}
WORK=/work
cd "$WORK"
# Same container workarounds as tools/gamescope/build-in-arch.sh.
grep -q '^DisableSandbox' /etc/pacman.conf || sed -i 's/^\[options\]/[options]\nDisableSandbox/' /etc/pacman.conf
{ for m in https://ca.us.mirror.archlinuxarm.org https://fl.us.mirror.archlinuxarm.org https://de3.mirror.archlinuxarm.org https://nl.mirror.archlinuxarm.org; do echo "Server = $m/\$arch/\$repo"; done; cat /etc/pacman.d/mirrorlist; } > /etc/pacman.d/mirrorlist.new
mv /etc/pacman.d/mirrorlist.new /etc/pacman.d/mirrorlist
pacman -Syu --noconfirm --needed git sudo zstd binutils
id builder >/dev/null 2>&1 || useradd -m builder
echo 'builder ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/builder
rm -rf pkg && mkdir pkg && cp -r tools/wlroots/patches pkg/ && chown -R builder pkg
sudo -u builder bash -c "
set -euxo pipefail
cd pkg
for rel in 1 2 3 4; do
if git clone -q --depth 1 --branch ${VERSION}-\$rel https://gitlab.archlinux.org/archlinux/packaging/packages/wlroots0.20.git arch 2>/dev/null; then break; fi
done
test -f arch/PKGBUILD
cd arch
grep -q '^pkgver=${VERSION}$' PKGBUILD
cp ../patches/*.patch .
PATCHES=\$(ls *.patch | sort)
{ echo; echo 'source+=('; for p in \$PATCHES; do echo \" \$p\"; done; echo ')'; for p in \$PATCHES; do echo \"sha256sums+=('SKIP')\"; done; } >> PKGBUILD
# Arch's PKGBUILD has no prepare(); ours applies the patches to the checkout.
cat >> PKGBUILD <<'PREP'
prepare() {
cd \"\$srcdir/\$pkgname\"
for p in \$(ls \"\$srcdir\"/*.patch | sort); do
echo \"applying \$(basename \"\$p\")\"
patch -p1 --no-backup-if-mismatch < \"\$p\"
done
}
PREP
makepkg -A -s --noconfirm --skipchecksums --skippgpcheck
ls -l *.pkg.tar.*
"
cd "$WORK"
rm -rf out && mkdir -p out/usr/local/lib/droiddeck-wlroots
PKG=$(ls pkg/arch/wlroots0.20-*.pkg.tar.* | grep -v -- '-debug-' | head -1)
mkdir -p x && tar --use-compress-program=unzstd -xf "$PKG" -C x
SO=x/usr/lib/libwlroots-0.20.so
test -f "$SO"
cp -L "$SO" out/usr/local/lib/droiddeck-wlroots/libwlroots-0.20.so
strip --strip-unneeded out/usr/local/lib/droiddeck-wlroots/libwlroots-0.20.so || true
# The patch is in: the allocator's symbol and its log line.
grep -q 'Created Vulkan DMA-BUF allocator' out/usr/local/lib/droiddeck-wlroots/libwlroots-0.20.so
readelf -d out/usr/local/lib/droiddeck-wlroots/libwlroots-0.20.so | grep -E 'NEEDED|SONAME'
(cd out && tar --use-compress-program='zstd -19' -cf ../wlroots.tzst usr)
sha256sum wlroots.tzst | tee wlroots.tzst.sha256
ls -l wlroots.tzst out/usr/local/lib/droiddeck-wlroots/
@@ -0,0 +1,549 @@
diff --git a/include/render/vulkan.h b/include/render/vulkan.h
index f96619f2..9f945f87 100644
--- a/include/render/vulkan.h
+++ b/include/render/vulkan.h
@@ -461,6 +461,10 @@ struct wlr_vk_descriptor_pool *vulkan_alloc_blend_ds(
// Frees the given descriptor set from the pool its pool.
void vulkan_free_ds(struct wlr_vk_renderer *renderer,
struct wlr_vk_descriptor_pool *pool, VkDescriptorSet ds);
+// A DMA-BUF allocator on a Vulkan device of its own, for a DRM fd gbm cannot use (see
+// render/vulkan/allocator.c).
+struct wlr_allocator *vulkan_allocator_create(int drm_fd);
+
struct wlr_vk_format_props *vulkan_format_props_from_drm(
struct wlr_vk_device *dev, uint32_t drm_format);
struct wlr_vk_renderer *vulkan_get_renderer(struct wlr_renderer *r);
diff --git a/render/allocator/allocator.c b/render/allocator/allocator.c
index b7dbf3d6..f3162d47 100644
--- a/render/allocator/allocator.c
+++ b/render/allocator/allocator.c
@@ -1,6 +1,7 @@
#include <assert.h>
#include <fcntl.h>
#include <stdlib.h>
+#include <string.h>
#include <unistd.h>
#include <wlr/backend.h>
#include <wlr/config.h>
@@ -13,6 +14,10 @@
#include "render/allocator/shm.h"
#include "render/wlr_renderer.h"
+#if WLR_HAS_VULKAN_RENDERER
+#include "render/vulkan.h"
+#endif
+
#if WLR_HAS_GBM_ALLOCATOR
#include "render/allocator/gbm.h"
#endif
@@ -32,6 +37,29 @@ void wlr_allocator_init(struct wlr_allocator *alloc,
wl_signal_init(&alloc->events.destroy);
}
+/* Whether the fd is a DRM device at all. The GPU node on Android (KGSL, for Adreno) stands in for
+ * a render node so that Vulkan and libdrm's device lookup can name the GPU, but it answers no DRM
+ * ioctl: drmIsMaster() takes that for mastership, and gbm cannot allocate on it. */
+static bool is_drm_device(int fd) {
+ drmVersion *version = drmGetVersion(fd);
+ if (version == NULL) {
+ return false;
+ }
+ drmFreeVersion(version);
+ return true;
+}
+
+#if WLR_HAS_VULKAN_RENDERER
+static struct wlr_allocator *try_vulkan_allocator(int drm_fd) {
+ wlr_log(WLR_DEBUG, "Trying to create Vulkan allocator");
+ struct wlr_allocator *alloc = vulkan_allocator_create(drm_fd);
+ if (alloc == NULL) {
+ wlr_log(WLR_DEBUG, "Failed to create Vulkan allocator");
+ }
+ return alloc;
+}
+#endif
+
/* Re-open the DRM node to avoid GEM handle ref'counting issues. See:
* https://gitlab.freedesktop.org/mesa/drm/-/merge_requests/110
*/
@@ -109,8 +137,20 @@ struct wlr_allocator *wlr_allocator_autocreate(struct wlr_backend *backend,
struct wlr_allocator *alloc = NULL;
uint32_t gbm_caps = WLR_BUFFER_CAP_DMABUF;
+ bool drm_device = drm_fd >= 0 && is_drm_device(drm_fd);
+ const char *forced = getenv("WLR_ALLOCATOR");
+ bool vk_first = forced != NULL && strcmp(forced, "vulkan") == 0;
+#if WLR_HAS_VULKAN_RENDERER
+ // A GPU node that is not a DRM device (and WLR_ALLOCATOR=vulkan): buffers come from Vulkan.
+ if ((backend_caps & gbm_caps) && (renderer_caps & gbm_caps) && drm_fd >= 0
+ && (vk_first || !drm_device)) {
+ if ((alloc = try_vulkan_allocator(drm_fd)) != NULL) {
+ return alloc;
+ }
+ }
+#endif
if ((backend_caps & gbm_caps) && (renderer_caps & gbm_caps)
- && drm_fd >= 0) {
+ && drm_fd >= 0 && drm_device) {
#if WLR_HAS_GBM_ALLOCATOR
wlr_log(WLR_DEBUG, "Trying to create gbm allocator");
int gbm_fd = reopen_drm_node(drm_fd, true);
@@ -124,6 +164,11 @@ struct wlr_allocator *wlr_allocator_autocreate(struct wlr_backend *backend,
wlr_log(WLR_DEBUG, "Failed to create gbm allocator");
#else
wlr_log(WLR_DEBUG, "Skipping gbm allocator: disabled at compile-time");
+#endif
+#if WLR_HAS_VULKAN_RENDERER
+ if (!vk_first && (alloc = try_vulkan_allocator(drm_fd)) != NULL) {
+ return alloc;
+ }
#endif
}
@@ -138,7 +183,7 @@ struct wlr_allocator *wlr_allocator_autocreate(struct wlr_backend *backend,
uint32_t drm_caps = WLR_BUFFER_CAP_DMABUF | WLR_BUFFER_CAP_DATA_PTR;
if ((backend_caps & drm_caps) && (renderer_caps & drm_caps)
- && drm_fd >= 0 && drmIsMaster(drm_fd)) {
+ && drm_fd >= 0 && drm_device && drmIsMaster(drm_fd)) {
wlr_log(WLR_DEBUG, "Trying to create drm dumb allocator");
int dumb_fd = reopen_drm_node(drm_fd, false);
if (dumb_fd < 0) {
diff --git a/render/vulkan/allocator.c b/render/vulkan/allocator.c
new file mode 100644
index 00000000..4c00d361
--- /dev/null
+++ b/render/vulkan/allocator.c
@@ -0,0 +1,392 @@
+#include <assert.h>
+#include <drm_fourcc.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <wlr/interfaces/wlr_buffer.h>
+#include <wlr/render/allocator.h>
+#include <wlr/render/drm_format_set.h>
+#include <wlr/util/log.h>
+#include "render/vulkan.h"
+
+/*
+ * DMA-BUFs allocated by Vulkan, for a GPU whose device node is not a DRM device and so cannot be
+ * given to gbm - Adreno through KGSL on Android, where the node Turnip opens stands in for a
+ * render node. Each buffer is an exportable image with an explicit DRM format modifier, created
+ * the way vulkan_import_dmabuf() imports one for rendering, so the renderer and the parent
+ * compositor see the same layout. The device is our own, on the physical device the DRM fd names:
+ * this works under the gles2 renderer too, as long as its EGL can import the result.
+ */
+
+struct wlr_vk_allocator {
+ struct wlr_allocator base;
+ struct wlr_vk_device *dev;
+ PFN_vkGetMemoryFdKHR get_memory_fd;
+ PFN_vkGetImageDrmFormatModifierPropertiesEXT get_modifier_props;
+ struct wl_list buffers; // wlr_vk_allocator_buffer.link
+};
+
+struct wlr_vk_allocator_buffer {
+ struct wlr_buffer base;
+ struct wlr_vk_allocator *alloc; // NULL once the allocator is gone
+ struct wl_list link;
+ VkImage image;
+ VkDeviceMemory memory;
+ struct wlr_dmabuf_attributes dmabuf;
+};
+
+static const struct wlr_buffer_impl buffer_impl;
+
+static struct wlr_vk_allocator_buffer *buffer_from_buffer(struct wlr_buffer *wlr_buffer) {
+ assert(wlr_buffer->impl == &buffer_impl);
+ struct wlr_vk_allocator_buffer *buffer = wl_container_of(wlr_buffer, buffer, base);
+ return buffer;
+}
+
+static void buffer_release_vk(struct wlr_vk_allocator_buffer *buffer) {
+ if (buffer->alloc == NULL) {
+ return;
+ }
+ VkDevice dev = buffer->alloc->dev->dev;
+ // The exported DMA-BUF keeps the memory alive for whoever imported it.
+ if (buffer->image != VK_NULL_HANDLE) {
+ vkDestroyImage(dev, buffer->image, NULL);
+ }
+ if (buffer->memory != VK_NULL_HANDLE) {
+ vkFreeMemory(dev, buffer->memory, NULL);
+ }
+ buffer->image = VK_NULL_HANDLE;
+ buffer->memory = VK_NULL_HANDLE;
+ wl_list_remove(&buffer->link);
+ buffer->alloc = NULL;
+}
+
+static void buffer_destroy(struct wlr_buffer *wlr_buffer) {
+ struct wlr_vk_allocator_buffer *buffer = buffer_from_buffer(wlr_buffer);
+ wlr_buffer_finish(wlr_buffer);
+ buffer_release_vk(buffer);
+ wlr_dmabuf_attributes_finish(&buffer->dmabuf);
+ free(buffer);
+}
+
+static bool buffer_get_dmabuf(struct wlr_buffer *wlr_buffer,
+ struct wlr_dmabuf_attributes *dmabuf) {
+ struct wlr_vk_allocator_buffer *buffer = buffer_from_buffer(wlr_buffer);
+ *dmabuf = buffer->dmabuf;
+ return true;
+}
+
+static const struct wlr_buffer_impl buffer_impl = {
+ .destroy = buffer_destroy,
+ .get_dmabuf = buffer_get_dmabuf,
+};
+
+// One vkCreateImage over the modifiers that share the same mutable-sRGB setting, the one thing
+// in the import that depends on the modifier. The driver picks the modifier.
+static VkImage create_image(struct wlr_vk_allocator *alloc,
+ const struct wlr_vk_format_props *props, int width, int height,
+ const uint64_t *mods, uint32_t mods_len, bool mutable_srgb) {
+ VkImageCreateInfo img_info = {
+ .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
+ .imageType = VK_IMAGE_TYPE_2D,
+ .format = props->format.vk,
+ .mipLevels = 1,
+ .arrayLayers = 1,
+ .samples = VK_SAMPLE_COUNT_1_BIT,
+ .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
+ .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
+ .extent = (VkExtent3D){ width, height, 1 },
+ .usage = vulkan_render_usage,
+ .tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT,
+ };
+ if (mutable_srgb) {
+ img_info.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
+ }
+
+ VkExternalMemoryImageCreateInfo eimg = {
+ .sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO,
+ .handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT,
+ };
+ img_info.pNext = &eimg;
+
+ VkImageDrmFormatModifierListCreateInfoEXT mod_list = {
+ .sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_LIST_CREATE_INFO_EXT,
+ .drmFormatModifierCount = mods_len,
+ .pDrmFormatModifiers = mods,
+ };
+ eimg.pNext = &mod_list;
+
+ VkFormat view_formats[] = {
+ props->format.vk,
+ props->format.vk_srgb,
+ };
+ VkImageFormatListCreateInfoKHR list_info = {
+ .sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO_KHR,
+ .pViewFormats = view_formats,
+ .viewFormatCount = sizeof(view_formats) / sizeof(view_formats[0]),
+ };
+ if (mutable_srgb) {
+ mod_list.pNext = &list_info;
+ }
+
+ VkImage image = VK_NULL_HANDLE;
+ VkResult res = vkCreateImage(alloc->dev->dev, &img_info, NULL, &image);
+ if (res != VK_SUCCESS) {
+ wlr_vk_error("vkCreateImage", res);
+ return VK_NULL_HANDLE;
+ }
+ return image;
+}
+
+static struct wlr_buffer *allocator_create_buffer(struct wlr_allocator *wlr_allocator,
+ int width, int height, const struct wlr_drm_format *format) {
+ struct wlr_vk_allocator *alloc = wl_container_of(wlr_allocator, alloc, base);
+ VkDevice dev = alloc->dev->dev;
+
+ const struct wlr_vk_format_props *props =
+ vulkan_format_props_from_drm(alloc->dev, format->format);
+ if (props == NULL) {
+ wlr_log(WLR_ERROR, "Vulkan allocator: format 0x%08"PRIX32" not supported",
+ format->format);
+ return NULL;
+ }
+
+ // The modifiers asked for that this device can render to at this size, split by whether the
+ // import will make the image mutable-format. INVALID (implicit) is not something Vulkan can
+ // allocate; LINEAR stands in for it when it is all there is.
+ uint64_t *mutable_mods = calloc(format->len + 1, sizeof(uint64_t));
+ uint64_t *plain_mods = calloc(format->len + 1, sizeof(uint64_t));
+ if (mutable_mods == NULL || plain_mods == NULL) {
+ free(mutable_mods);
+ free(plain_mods);
+ return NULL;
+ }
+ uint32_t mutable_len = 0, plain_len = 0;
+ bool implicit_only = format->len > 0;
+ for (size_t i = 0; i <= format->len; i++) {
+ uint64_t mod;
+ if (i < format->len) {
+ mod = format->modifiers[i];
+ if (mod == DRM_FORMAT_MOD_INVALID) {
+ continue;
+ }
+ implicit_only = false;
+ } else if (implicit_only) {
+ mod = DRM_FORMAT_MOD_LINEAR;
+ } else {
+ break;
+ }
+ const struct wlr_vk_format_modifier_props *mp =
+ vulkan_format_props_find_modifier(props, mod, true);
+ if (mp == NULL || (uint32_t)width > mp->max_extent.width ||
+ (uint32_t)height > mp->max_extent.height) {
+ continue;
+ }
+ if (mp->has_mutable_srgb) {
+ mutable_mods[mutable_len++] = mod;
+ } else {
+ plain_mods[plain_len++] = mod;
+ }
+ }
+
+ VkImage image = VK_NULL_HANDLE;
+ if (mutable_len > 0) {
+ image = create_image(alloc, props, width, height, mutable_mods, mutable_len, true);
+ }
+ if (image == VK_NULL_HANDLE && plain_len > 0) {
+ image = create_image(alloc, props, width, height, plain_mods, plain_len, false);
+ }
+ free(mutable_mods);
+ free(plain_mods);
+ if (image == VK_NULL_HANDLE) {
+ wlr_log(WLR_ERROR, "Vulkan allocator: no usable modifier for %dx%d format 0x%08"PRIX32,
+ width, height, format->format);
+ return NULL;
+ }
+
+ struct wlr_vk_allocator_buffer *buffer = calloc(1, sizeof(*buffer));
+ if (buffer == NULL) {
+ vkDestroyImage(dev, image, NULL);
+ return NULL;
+ }
+ wlr_buffer_init(&buffer->base, &buffer_impl, width, height);
+ buffer->alloc = alloc;
+ buffer->image = image;
+ wl_list_insert(&alloc->buffers, &buffer->link);
+ for (int i = 0; i < WLR_DMABUF_MAX_PLANES; i++) {
+ buffer->dmabuf.fd[i] = -1;
+ }
+
+ VkMemoryRequirements reqs;
+ vkGetImageMemoryRequirements(dev, image, &reqs);
+ int mem_type = vulkan_find_mem_type(alloc->dev,
+ VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, reqs.memoryTypeBits);
+ if (mem_type < 0) {
+ mem_type = vulkan_find_mem_type(alloc->dev, 0, reqs.memoryTypeBits);
+ }
+ if (mem_type < 0) {
+ wlr_log(WLR_ERROR, "Vulkan allocator: no memory type for the image");
+ goto error;
+ }
+
+ VkMemoryDedicatedAllocateInfo dedicated = {
+ .sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO,
+ .image = image,
+ };
+ VkExportMemoryAllocateInfo export = {
+ .sType = VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO,
+ .pNext = &dedicated,
+ .handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT,
+ };
+ VkMemoryAllocateInfo mem_info = {
+ .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
+ .pNext = &export,
+ .allocationSize = reqs.size,
+ .memoryTypeIndex = mem_type,
+ };
+ VkResult res = vkAllocateMemory(dev, &mem_info, NULL, &buffer->memory);
+ if (res != VK_SUCCESS) {
+ wlr_vk_error("vkAllocateMemory", res);
+ goto error;
+ }
+ res = vkBindImageMemory(dev, image, buffer->memory, 0);
+ if (res != VK_SUCCESS) {
+ wlr_vk_error("vkBindImageMemory", res);
+ goto error;
+ }
+
+ VkImageDrmFormatModifierPropertiesEXT mod_props = {
+ .sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_PROPERTIES_EXT,
+ };
+ res = alloc->get_modifier_props(dev, image, &mod_props);
+ if (res != VK_SUCCESS) {
+ wlr_vk_error("vkGetImageDrmFormatModifierPropertiesEXT", res);
+ goto error;
+ }
+ const struct wlr_vk_format_modifier_props *mp =
+ vulkan_format_props_find_modifier(props, mod_props.drmFormatModifier, true);
+ if (mp == NULL) {
+ wlr_log(WLR_ERROR, "Vulkan allocator: driver chose modifier 0x%016"PRIX64
+ " which it cannot render to", mod_props.drmFormatModifier);
+ goto error;
+ }
+ uint32_t n_planes = mp->props.drmFormatModifierPlaneCount;
+ if (n_planes == 0 || n_planes > WLR_DMABUF_MAX_PLANES) {
+ wlr_log(WLR_ERROR, "Vulkan allocator: %"PRIu32" memory planes", n_planes);
+ goto error;
+ }
+
+ VkMemoryGetFdInfoKHR fd_info = {
+ .sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR,
+ .memory = buffer->memory,
+ .handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT,
+ };
+ int fd = -1;
+ res = alloc->get_memory_fd(dev, &fd_info, &fd);
+ if (res != VK_SUCCESS || fd < 0) {
+ wlr_vk_error("vkGetMemoryFdKHR", res);
+ goto error;
+ }
+
+ buffer->dmabuf = (struct wlr_dmabuf_attributes){
+ .width = width,
+ .height = height,
+ .format = format->format,
+ .modifier = mod_props.drmFormatModifier,
+ .n_planes = n_planes,
+ };
+ for (int i = 0; i < WLR_DMABUF_MAX_PLANES; i++) {
+ buffer->dmabuf.fd[i] = -1;
+ }
+ static const VkImageAspectFlagBits plane_aspects[] = {
+ VK_IMAGE_ASPECT_MEMORY_PLANE_0_BIT_EXT,
+ VK_IMAGE_ASPECT_MEMORY_PLANE_1_BIT_EXT,
+ VK_IMAGE_ASPECT_MEMORY_PLANE_2_BIT_EXT,
+ VK_IMAGE_ASPECT_MEMORY_PLANE_3_BIT_EXT,
+ };
+ for (uint32_t i = 0; i < n_planes; i++) {
+ VkImageSubresource sub = { .aspectMask = plane_aspects[i] };
+ VkSubresourceLayout layout;
+ vkGetImageSubresourceLayout(dev, image, &sub, &layout);
+ buffer->dmabuf.offset[i] = layout.offset;
+ buffer->dmabuf.stride[i] = layout.rowPitch;
+ // Every plane lives in the one allocation; each gets its own fd, as
+ // wlr_dmabuf_attributes_finish() closes them one by one.
+ buffer->dmabuf.fd[i] = i == 0 ? fd : fcntl(fd, F_DUPFD_CLOEXEC, 0);
+ if (buffer->dmabuf.fd[i] < 0) {
+ wlr_log_errno(WLR_ERROR, "Vulkan allocator: dup failed");
+ goto error;
+ }
+ }
+
+ wlr_log(WLR_DEBUG, "Vulkan allocator: %dx%d format 0x%08"PRIX32" modifier 0x%016"PRIX64
+ ", %"PRIu32" plane(s)", width, height, format->format, mod_props.drmFormatModifier, n_planes);
+ return &buffer->base;
+
+error:
+ buffer_destroy(&buffer->base);
+ return NULL;
+}
+
+static void allocator_destroy(struct wlr_allocator *wlr_allocator) {
+ struct wlr_vk_allocator *alloc = wl_container_of(wlr_allocator, alloc, base);
+ // Buffers may outlive their allocator; their DMA-BUF fds stay valid without the device.
+ struct wlr_vk_allocator_buffer *buffer, *tmp;
+ wl_list_for_each_safe(buffer, tmp, &alloc->buffers, link) {
+ buffer_release_vk(buffer);
+ }
+ struct wlr_vk_instance *ini = alloc->dev->instance;
+ vulkan_device_destroy(alloc->dev);
+ vulkan_instance_destroy(ini);
+ free(alloc);
+}
+
+static const struct wlr_allocator_interface allocator_impl = {
+ .destroy = allocator_destroy,
+ .create_buffer = allocator_create_buffer,
+};
+
+struct wlr_allocator *vulkan_allocator_create(int drm_fd) {
+ struct wlr_vk_instance *ini = vulkan_instance_create(false);
+ if (ini == NULL) {
+ wlr_log(WLR_ERROR, "Vulkan allocator: could not create an instance");
+ return NULL;
+ }
+ VkPhysicalDevice phdev = vulkan_find_drm_phdev(ini, drm_fd);
+ if (phdev == VK_NULL_HANDLE) {
+ wlr_log(WLR_ERROR, "Vulkan allocator: no Vulkan device for the DRM fd");
+ vulkan_instance_destroy(ini);
+ return NULL;
+ }
+ struct wlr_vk_device *dev = vulkan_device_create(ini, phdev);
+ if (dev == NULL) {
+ wlr_log(WLR_ERROR, "Vulkan allocator: could not create a device");
+ vulkan_instance_destroy(ini);
+ return NULL;
+ }
+
+ struct wlr_vk_allocator *alloc = calloc(1, sizeof(*alloc));
+ if (alloc == NULL) {
+ vulkan_device_destroy(dev);
+ vulkan_instance_destroy(ini);
+ return NULL;
+ }
+ alloc->dev = dev;
+ wl_list_init(&alloc->buffers);
+ alloc->get_memory_fd = (PFN_vkGetMemoryFdKHR)
+ vkGetDeviceProcAddr(dev->dev, "vkGetMemoryFdKHR");
+ alloc->get_modifier_props = (PFN_vkGetImageDrmFormatModifierPropertiesEXT)
+ vkGetDeviceProcAddr(dev->dev, "vkGetImageDrmFormatModifierPropertiesEXT");
+ if (alloc->get_memory_fd == NULL || alloc->get_modifier_props == NULL) {
+ wlr_log(WLR_ERROR, "Vulkan allocator: the device lacks DMA-BUF export");
+ free(alloc);
+ vulkan_device_destroy(dev);
+ vulkan_instance_destroy(ini);
+ return NULL;
+ }
+
+ wlr_allocator_init(&alloc->base, &allocator_impl, WLR_BUFFER_CAP_DMABUF);
+ wlr_log(WLR_INFO, "Created Vulkan DMA-BUF allocator");
+ return &alloc->base;
+}
diff --git a/render/vulkan/meson.build b/render/vulkan/meson.build
index 36a822f9..abeb5337 100644
--- a/render/vulkan/meson.build
+++ b/render/vulkan/meson.build
@@ -37,6 +37,7 @@ glslang_version_info = run_command(glslang, '--version', check: true).stdout()
glslang_version = glslang_version_info.split('\n')[0].split(':')[-1]
wlr_files += files(
+ 'allocator.c',
'pass.c',
'renderer.c',
'texture.c',
diff --git a/types/wlr_linux_dmabuf_v1.c b/types/wlr_linux_dmabuf_v1.c
index 3165e880..a7fe060c 100644
--- a/types/wlr_linux_dmabuf_v1.c
+++ b/types/wlr_linux_dmabuf_v1.c
@@ -901,11 +901,22 @@ static bool set_default_feedback(struct wlr_linux_dmabuf_v1 *linux_dmabuf,
if (device->available_nodes & (1 << DRM_NODE_RENDER)) {
const char *name = device->nodes[DRM_NODE_RENDER];
main_device_fd = open(name, O_RDWR | O_CLOEXEC);
- drmFreeDevice(&device);
if (main_device_fd < 0) {
wlr_log_errno(WLR_ERROR, "Failed to open DRM device %s", name);
+ drmFreeDevice(&device);
goto error_compiled;
}
+ // A GPU node that stands in for a render node without being a DRM device (KGSL on
+ // Android) cannot import a buffer as a GEM handle; every client's would be refused.
+ drmVersion *version = drmGetVersion(main_device_fd);
+ if (version == NULL) {
+ wlr_log(WLR_INFO, "%s is not a DRM device, skipping DMA-BUF import checks", name);
+ close(main_device_fd);
+ main_device_fd = -1;
+ } else {
+ drmFreeVersion(version);
+ }
+ drmFreeDevice(&device);
} else {
// Likely a split display/render setup. Unfortunately we have no way to
// get back the proper render node used by the renderer under-the-hood.
+2
View File
@@ -0,0 +1,2 @@
WLROOTS_TAG=wlroots-0.20.2-p1
WLROOTS_SHA256=b4dcb08b46e69dbfaa004597844e843cdf938591ba5dbc7079b91626cdd4591d