mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
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:
20 files changed
+1834
-42
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
Executable
+58
@@ -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.
|
||||
@@ -0,0 +1,2 @@
|
||||
WLROOTS_TAG=wlroots-0.20.2-p1
|
||||
WLROOTS_SHA256=b4dcb08b46e69dbfaa004597844e843cdf938591ba5dbc7079b91626cdd4591d
|
||||
Reference in new issue
Block a user