mirror of
https://github.com/saphid/chromium-webxr-steam-frame.git
synced 2026-10-06 02:00:18 +02:00
gamescope turns the laser into mouse clicks for apps Steam launches, so dragging selected text. When Steam starts it, the launcher now runs Chromium as a systemd user service outside Steam's process tree: its windows get their own gamescope panel and real touches. Steam still tracks it and Stop closes it. CHROMIUM_XR_STEAM_PANEL=1 keeps the old behaviour. --touch-events=enabled because Xwayland labels its touch device as a pointer. Before each start (when Chromium isn't running) the launcher also makes Allow the default VR permission and drops per-site Blocks, so there's no "Allow VR?" prompt. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
142 lines
5.9 KiB
Bash
Executable File
142 lines
5.9 KiB
Bash
Executable File
#!/bin/bash
|
|
# Start the WebXR Chromium build. frame/install.sh copies this to
|
|
# ~/.local/bin/chromium-xr; the Steam library shortcut and the desktop entry
|
|
# both run it. Arguments go to Chromium, so `chromium-xr URL` opens a page.
|
|
set -euo pipefail
|
|
|
|
CHROME="${CHROMIUM_XR_HOME:-$HOME/chromium-xr}/chrome"
|
|
[[ -x "$CHROME" ]] || { echo "chromium-xr: no build at $CHROME (run frame/install.sh)" >&2; exit 1; }
|
|
|
|
# Steam preloads its in-game overlay (gameoverlayrenderer.so) into anything it
|
|
# launches. It segfaults in Chromium's zygote, the GPU process then can't
|
|
# start, and Chromium quits with "GPU process isn't usable". Drop it and keep
|
|
# anything else that was preloaded.
|
|
if [[ -n "${LD_PRELOAD:-}" ]]; then
|
|
keep=()
|
|
# Split on spaces and colons only, like ld.so, with no glob expansion.
|
|
# read -d '' takes the whole value, newlines included (it returns 1 at EOF).
|
|
IFS=' :' read -r -d '' -a libs < <(printf '%s' "$LD_PRELOAD") || true
|
|
for lib in ${libs[@]+"${libs[@]}"}; do
|
|
[[ -z "$lib" || "$lib" == *gameoverlayrenderer.so ]] || keep+=("$lib")
|
|
done
|
|
if (( ${#keep[@]} )); then
|
|
LD_PRELOAD=${keep[0]}
|
|
for lib in "${keep[@]:1}"; do LD_PRELOAD+=":$lib"; done
|
|
export LD_PRELOAD
|
|
else
|
|
unset LD_PRELOAD
|
|
fi
|
|
fi
|
|
|
|
# --enable-features=OpenXR: the Linux OpenXR device is off by default.
|
|
# --ozone-platform=x11: gamescope's X display, where each app is a panel.
|
|
# --no-first-run and --password-store=basic: otherwise startup can stop at a
|
|
# first-run or keyring prompt you can't see.
|
|
# --disable-seccomp-filter-sandbox: with the XR process's seccomp policy on,
|
|
# SteamVR sees the wrong process ID and refuses the session (see
|
|
# docs/technical-notes.md). The namespace sandbox stays on.
|
|
# --touch-events=enabled: Xwayland labels its touch device as a pointer, so
|
|
# Chromium doesn't offer pages the touch API on its own.
|
|
profile=$HOME/.config/chromium-xr
|
|
|
|
# Let every site enter VR without the "Allow VR?" prompt: make Allow the
|
|
# default for the VR permission, and drop per-site Block exceptions. There's
|
|
# no policy or switch for this, and Chromium only reads Preferences at start,
|
|
# so skip it while Chromium is running. flock stops two launches editing the
|
|
# file at once; the running check is inside it, so it's fresh for each edit.
|
|
mkdir -p "$profile"
|
|
flock "$profile/.chromium-xr-prefs.lock" python3 - "$profile" <<'EOF' ||
|
|
import json, os, sys, tempfile
|
|
profile = sys.argv[1]
|
|
try:
|
|
# SingletonLock is a symlink to "<host>-<pid>" while Chromium runs.
|
|
os.kill(int(os.readlink(os.path.join(profile, 'SingletonLock')).rsplit('-', 1)[1]), 0)
|
|
sys.exit(0) # Chromium is running
|
|
except (OSError, ValueError, IndexError):
|
|
pass # no lock, or a stale one: not running
|
|
path = os.path.join(profile, 'Default', 'Preferences')
|
|
try:
|
|
with open(path) as f:
|
|
prefs = json.load(f)
|
|
except FileNotFoundError:
|
|
prefs = {}
|
|
settings = prefs.setdefault('profile', {})
|
|
settings.setdefault('default_content_setting_values', {})['vr'] = 1 # Allow
|
|
vr = settings.get('content_settings', {}).get('exceptions', {}).get('vr', {})
|
|
for pattern in [p for p, v in vr.items() if isinstance(v, dict) and v.get('setting') == 2]:
|
|
del vr[pattern] # Block
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
# Private (0600) and unique, so the file never widens its permissions and two
|
|
# launches can't write into each other's copy.
|
|
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path), prefix='.Preferences.')
|
|
try:
|
|
with os.fdopen(fd, 'w') as f:
|
|
json.dump(prefs, f)
|
|
os.replace(tmp, path)
|
|
except BaseException:
|
|
os.unlink(tmp)
|
|
raise
|
|
EOF
|
|
echo "chromium-xr: couldn't set the VR permission to Allow" >&2
|
|
|
|
cmd=("$CHROME"
|
|
--user-data-dir="$profile"
|
|
--enable-features=OpenXR
|
|
--ozone-platform=x11
|
|
--no-first-run --no-default-browser-check --password-store=basic
|
|
--disable-seccomp-filter-sandbox
|
|
--touch-events=enabled
|
|
"$@")
|
|
|
|
# gamescope turns the controller's laser into mouse clicks for apps that Steam
|
|
# launched, so dragging selects text instead of scrolling. Windows outside
|
|
# Steam's process tree still get their own panel, but the laser reaches them
|
|
# as a touchscreen, as in Desktop Mode. So when Steam starts us, run Chromium
|
|
# as its own systemd user service and wait for it, which keeps it "running"
|
|
# in Steam. CHROMIUM_XR_STEAM_PANEL=1 keeps the old behaviour.
|
|
if [[ -z "${SteamAppId:-}" || "${CHROMIUM_XR_STEAM_PANEL:-}" == 1 ]] ||
|
|
! command -v systemd-run >/dev/null; then
|
|
exec "${cmd[@]}"
|
|
fi
|
|
|
|
# Pass our environment through by name (-E NAME copies its value), minus
|
|
# systemd's per-service bookkeeping.
|
|
env_args=()
|
|
for name in $(compgen -e); do
|
|
case $name in
|
|
INVOCATION_ID|JOURNAL_STREAM|MANAGERPID|SYSTEMD_EXEC_PID|NOTIFY_SOCKET|MEMORY_PRESSURE_*|LISTEN_*) ;;
|
|
*) env_args+=(-E "$name") ;;
|
|
esac
|
|
done
|
|
unit=chromium-xr-$$
|
|
# Steam's Stop, or the reaper ending us, closes Chromium too. Set before
|
|
# starting it, so a Stop during startup isn't lost (bash runs the trap once
|
|
# systemd-run returns).
|
|
stopping=
|
|
trap 'stopping=1; systemctl --user stop --no-block "$unit" 2>/dev/null' TERM INT HUP
|
|
if ! systemd-run --user --quiet --collect --same-dir --unit="$unit" \
|
|
--service-type=exec --expand-environment=no "${env_args[@]}" "${cmd[@]}"; then
|
|
# A Stop can make systemd-run fail too; don't start Chromium again then.
|
|
[[ -z "$stopping" ]] || exit 0
|
|
echo "chromium-xr: systemd-run failed; starting in Steam's panel instead" >&2
|
|
exec "${cmd[@]}"
|
|
fi
|
|
# MainPID is 0 once the service has ended (and been collected), e.g. when
|
|
# Chromium passed the URL to a window that was already open. A failed query
|
|
# is retried rather than taken to mean Chromium has gone.
|
|
pid=
|
|
for _ in 1 2 3 4 5; do
|
|
pid=$(systemctl --user show -p MainPID --value "$unit") && break
|
|
pid=
|
|
sleep 1
|
|
done
|
|
if [[ -z "$pid" ]]; then
|
|
echo "chromium-xr: can't query $unit; Chromium keeps running, but Steam won't track it" >&2
|
|
exit 1
|
|
fi
|
|
[[ "$pid" != 0 ]] || exit 0
|
|
while kill -0 "$pid" 2>/dev/null; do
|
|
tail --pid="$pid" -f /dev/null &
|
|
wait $! || true
|
|
done
|