Files
DroidDeck/tools/linuxfs/desktop/droiddeck-desktop
T
The412Banner 1a870c5c07 Desktop: Vulkan by default; a renderer that fails to start is remembered
vulkan is the default desktop renderer (settings list it first). When labwc
cannot start on a GPU renderer the desktop falls back to pixman as before, and
now records the renderer with the patched wlroots and Linux driver it failed
on, so later desktops go straight to pixman instead of waiting 20 s each time;
an app update, a different driver, or choosing a renderer again in the settings
(which deletes the record) retries.
2026-09-23 22:49:50 -04:00

199 lines
11 KiB
Bash

#!/bin/bash
# The desktop session: labwc nested as a client of the app's compositor, with LXQt's panel and
# file manager started from its autostart, and Xwayland for the X11 programs (the Steam client,
# most emulators). Run by bannerlator-session in place of gamescope for mode "lxqt".
# Staged by the app over the desktop package's copy on every launch, so a fix here reaches an
# installed desktop without re-downloading the package.
set -u
export XDG_SESSION_TYPE=wayland XDG_CURRENT_DESKTOP=LXQt XDG_SESSION_DESKTOP=lxqt
export QT_QPA_PLATFORM=wayland GDK_BACKEND=wayland,x11 SDL_VIDEODRIVER=wayland,x11
export QT_QPA_PLATFORMTHEME=lxqt
# Firefox's child processes sandbox themselves with seccomp and namespaces, neither of which
# behaves under proot: the content process ends up unable to read the font files its parent can,
# reports "no fonts" and dies - the tab crash, with the chrome around it still drawing text.
# 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
# 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
mkdir -p "$BL_DEBUG_DIR" 2>/dev/null || true
exec >>"$BL_DEBUG_DIR/desktop.log" 2>&1
echo "== droiddeck-desktop $(date -u '+%Y-%m-%d %H:%M:%S UTC') renderer=$WLR_RENDERER"
fi
export WLR_BACKENDS=wayland
mkdir -p "$HOME/.config/labwc" "$HOME/.config/lxqt"
# Our defaults for labwc and the panel, refreshed every session. What a user changes in the
# panel's own settings lands in panel.conf and is kept; only rc.xml and autostart are ours.
# The nested output takes the app's surface size; left alone, wlroots' Wayland backend opens
# a 1280x720 window and the whole desktop is upscaled from that.
sed "s/@MODE@/${BL_WIDTH:-1280}x${BL_HEIGHT:-720}/" /etc/xdg/labwc/rc.xml > "$HOME/.config/labwc/rc.xml"
cp -f /etc/xdg/labwc/autostart "$HOME/.config/labwc/autostart"
# panel.conf is the user's once the panel has written it, except the one our first package
# shipped: it never named its panel ([General] panels=), so lxqt-panel built an empty default
# one. That file is replaced; one that names its panels is kept.
if ! grep -q '^panels=' "$HOME/.config/lxqt/panel.conf" 2>/dev/null; then
cp -f /etc/xdg/lxqt/panel.conf "$HOME/.config/lxqt/panel.conf"
fi
[ -f "$HOME/.config/lxqt/lxqt.conf" ] || cp /etc/xdg/lxqt/lxqt.conf "$HOME/.config/lxqt/lxqt.conf"
# pcmanfm-qt paints the desktop; left unconfigured it paints it black, over swaybg's wallpaper.
if [ ! -f "$HOME/.config/pcmanfm-qt/lxqt/settings.conf" ]; then
mkdir -p "$HOME/.config/pcmanfm-qt/lxqt"
printf '[Desktop]\nWallpaper=/usr/share/backgrounds/steamdeck.png\nWallpaperMode=zoom\nBgColor=#1b1030\nShowHidden=false\n' \
> "$HOME/.config/pcmanfm-qt/lxqt/settings.conf"
fi
# The runtime directory outlives a session. lxqt-panel and pcmanfm-qt are single-instance
# programs that leave a lock and a socket there; finding last session's, they decide they are
# already running and exit without a word - a desktop with a wallpaper and nothing on it.
# Only the app's own compositor socket (wayland-0) is kept.
find "$XDG_RUNTIME_DIR" -mindepth 1 -maxdepth 1 ! -name 'wayland-0*' ! -name 'keymap.xkb' -exec rm -rf {} + 2>/dev/null
# fontconfig's rules (sans-serif -> DejaVu Sans and the rest) live as links in /etc/fonts/conf.d,
# and the rootfs has none: applications then find fonts but no default font. Firefox logs
# "no fonts" and draws pages without text.
if [ -d /usr/share/fontconfig/conf.default ] && ! ls /etc/fonts/conf.d/*.conf >/dev/null 2>&1; then
for f in /usr/share/fontconfig/conf.default/*.conf; do ln -sf "$f" /etc/fonts/conf.d/; done
echo "== fontconfig: linked $(ls /etc/fonts/conf.d/*.conf | wc -l) default rules"
fi
# Those rules map sans-serif and friends onto whatever a full distribution installs, and this
# rootfs has one family. Naming it outright is what gives every toolkit a default font: without
# one Gecko reports "no fonts" with families found, and draws pages with no text at all.
if [ ! -f /etc/fonts/local.conf ]; then
{ echo '<?xml version="1.0"?><!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd"><fontconfig>'
for generic in sans-serif sans serif monospace mono cursive fantasy system-ui ui-sans-serif; do
case $generic in
serif) face="DejaVu Serif" ;;
mono*|ui-mono) face="DejaVu Sans Mono" ;;
*) face="DejaVu Sans" ;;
esac
printf '<alias binding="weak"><family>%s</family><prefer><family>%s</family></prefer></alias>\n' "$generic" "$face"
done
echo '</fontconfig>'
} > /etc/fonts/local.conf
echo "== fontconfig: wrote local.conf (DejaVu as the default family)"
fi
# The cache in the image was built before those rules existed; rebuild it once, and only once.
if [ ! -f /var/cache/fontconfig/.droiddeck-built ] && command -v fc-cache >/dev/null 2>&1; then
fc-cache -f >/dev/null 2>&1 && touch /var/cache/fontconfig/.droiddeck-built
echo "== fontconfig: cache rebuilt, default sans is $(fc-match sans-serif 2>/dev/null)"
fi
# The menu's "Steam" / "Steam Big Picture" entries and one entry per installed game: all of them
# hand off to the app (bannerlator-steam-launch), since the client cannot draw on this desktop.
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
# ~/.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
# 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