This repository has no description
11 kB
253 lines
1{
2 config,
3 pkgs,
4 ...
5}:
6let
7 # Run the patched launcher from the on-disk strom checkout at
8 # /home/strom/strom (it carries STROM_NO_GAMESCOPE support until that lands
9 # upstream). With the kiosk itself being a gamescope session (below), games
10 # are launched as <slug>.no-gamescope so they render directly in the session
11 # compositor instead of nesting a per-game gamescope.
12 strom-session = pkgs.writeShellScript "strom-session" ''
13 set -u
14 # xpadneo owns the Xbox pad's hidraw node, so force SDL (pygame launcher and
15 # the games) to read the pad through evdev instead of its HIDAPI backend -
16 # same fix as 2configs/steam.nix, without it SDL finds no controller.
17 export SDL_JOYSTICK_HIDAPI_XBOX=0
18 # SDL's udev-monitor hotplug does not deliver in this kiosk session, so a pad
19 # powered on after the launcher starts is never seen. Force SDL to poll
20 # /dev/input instead, which reliably picks up pads as they connect/disconnect.
21 export SDL_JOYSTICK_DISABLE_UDEV=1
22 # Under cage the launcher window never reports input focus, and SDL suppresses
23 # joystick (not keyboard) events while unfocused - so keyboard works but the
24 # pad does not. Allow joystick events regardless of focus.
25 export SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS=1
26 # The kiosk compositor is already gamescope on DRM (see services.greetd
27 # below), so tell the launcher to run games directly in it rather than
28 # nesting a per-game gamescope (which double-nests and, on this GPU, storms
29 # the Vulkan swapchain and crashes games like Baba Is You).
30 export STROM_NO_GAMESCOPE=1
31 # strom-run (the per-game supervisor) needs a delegated cgroup v2 so it can
32 # mkdir a child cgroup and cgroup.kill the game tree atomically. The greetd
33 # PAM session drops us into a root-owned logind session scope
34 # (user-1011.slice/session-cN.scope) that is NOT delegated, so strom-run dies
35 # with "could not create cgroup (Permission denied)" and the game never
36 # launches. Re-parent into a delegated transient scope under the user manager
37 # (user@UID.service, which logind DOES delegate). Wait for the user bus first:
38 # on boot the PAM session opens slightly before the user manager's bus socket.
39 rd="''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
40 for _ in $(seq 1 50); do
41 [ -S "$rd/bus" ] && break
42 sleep 0.2
43 done
44 # Relaunch whenever the couch launcher exits (gamepad B / Esc) so the kiosk
45 # never drops to a blank screen. The loop runs inside the delegated scope so
46 # every game inherits a writable cgroup; the gamescope compositor stays up
47 # (it is the session's other child) across relaunches.
48 #
49 # setpriv --ambient-caps=-all: the login session may carry an ambient
50 # capability (e.g. cap_wake_alarm) that every child inherits. bwrap refuses
51 # to start when it sees capabilities in its permitted set without being
52 # setuid ("Unexpected capabilities but not setuid, old file caps config?"),
53 # which instant-crashes every sandboxed game. Drop the ambient set for the
54 # whole launcher tree so bwrap (and thus each game) starts clean.
55 exec ${pkgs.systemd}/bin/systemd-run --user --scope -p Delegate=yes \
56 --quiet --collect -- \
57 ${pkgs.util-linux}/bin/setpriv --ambient-caps=-all \
58 ${pkgs.bash}/bin/bash -c 'while true; do
59 ${config.nix.package}/bin/nix run /home/strom/strom#scripts.launcher
60 sleep 2
61 done'
62 '';
63
64 # Bluetooth pairing helper for Xbox Wireless Controllers. Run `strom-pair`
65 # once per controller (as root / on the console, e.g.
66 # `ssh root@coaxmetal.r strom-pair`): put the pad in pairing mode (hold the
67 # Xbox button to power on, then hold the small round sync button on the top
68 # edge until the light flashes rapidly) and the script blocks until it pairs
69 # that one pad, then exits. A pad currently paired to another device is
70 # re-paired here (its stale bond is cleared); a pad already connected and
71 # working is left alone. Run once per controller. Bonded+trusted pads
72 # reconnect on their own on every later boot. Pass a MAC to pair just one.
73 strom-pair = pkgs.writeShellApplication {
74 name = "strom-pair";
75 runtimeInputs = [ pkgs.bluez ];
76 text = ''
77 # Find one Xbox pad that is here right now (advertising in pairing mode)
78 # and not already connected; echo its MAC (empty if none). We key off what
79 # actually shows up in THIS scan, not the bonded-device list: a pad that
80 # was since re-paired to another host still lingers as "bonded" here but
81 # only appears in a scan when it is genuinely present and pairable.
82 find_target() {
83 local sl m
84 local -a bonded present
85 sl=$(mktemp)
86 bluetoothctl --timeout 12 scan on > "$sl" 2>&1 || true
87 mapfile -t bonded < <(bluetoothctl devices 2>/dev/null | grep -i xbox | awk '{print $2}')
88 # MACs of xbox pads that showed up in this scan (present + advertising).
89 mapfile -t present < <(
90 {
91 # freshly discovered pads announce their name
92 grep -i xbox "$sl" | grep -oiE '([0-9a-f]{2}:){5}[0-9a-f]{2}'
93 # known/bonded pads emit nameless RSSI lines; match their MAC in-scan
94 for m in "''${bonded[@]:-}"; do
95 [ -n "$m" ] && grep -qiF "$m" "$sl" && printf '%s\n' "$m"
96 done
97 } | sort -uf
98 )
99 rm -f "$sl"
100 for m in "''${present[@]:-}"; do
101 [ -n "$m" ] || continue
102 bluetoothctl info "$m" 2>/dev/null | grep -q "Connected: yes" && continue
103 printf '%s\n' "$m"
104 return 0
105 done
106 return 0
107 }
108
109 # Pair one pad. Xbox controllers use JustWorks pairing, which needs a
110 # NoInputNoOutput agent held in the SAME bluetoothctl session as `pair`
111 # (plus disable_ertm, set system-wide below). Remove any existing bond
112 # first: a pad re-paired to another host still shows bonded here but never
113 # reconnects, and bluez refuses a fresh pair while that stale bond exists.
114 # `connect` succeeds even when pairing fails, so success is judged by
115 # "Bonded: yes", not by connect.
116 pair_one() {
117 local mac="$1"
118 bluetoothctl remove "$mac" >/dev/null 2>&1 || true
119 {
120 echo "power on"
121 echo "agent NoInputNoOutput"
122 echo "default-agent"
123 echo "scan on"
124 sleep 14
125 echo "pair $mac"
126 sleep 12
127 echo "trust $mac"
128 echo "connect $mac"
129 sleep 4
130 echo "quit"
131 } | bluetoothctl >/dev/null 2>&1 || true
132 # Bonding can finalize a moment after the session exits; poll a few
133 # seconds before deciding it failed.
134 for _ in 1 2 3 4 5; do
135 bluetoothctl info "$mac" 2>/dev/null | grep -q "Bonded: yes" && return 0
136 sleep 1
137 done
138 return 1
139 }
140
141 bluetoothctl power on >/dev/null 2>&1 || true
142
143 # Single-pad mode: (re)pair exactly the given MAC and exit.
144 if [ -n "''${1:-}" ]; then
145 if pair_one "$1"; then
146 echo ">> paired, trusted, connected: $1"
147 exit 0
148 fi
149 echo "!! could not pair $1." >&2
150 exit 1
151 fi
152
153 # Default: wait for a pad that is present and in pairing mode, (re)pair it,
154 # then exit. Run once per controller. A pad that was paired to another
155 # device in the meantime is re-paired here (its stale bond is cleared)
156 # rather than skipped; a pad already connected and working is left alone.
157 echo ">> put a controller in pairing mode: hold the Xbox button to power"
158 echo " on, then hold the small round sync button on the top edge until"
159 echo " the light flashes rapidly. Waiting... (Ctrl-C to abort)"
160 while true; do
161 echo ">> scanning..."
162 mac=$(find_target)
163 if [ -n "$mac" ]; then
164 printf '>> pairing %s ... ' "$mac"
165 if pair_one "$mac"; then
166 echo "OK"
167 echo ">> paired, trusted, connected: $mac"
168 exit 0
169 fi
170 echo "failed, retrying (keep it in pairing mode)"
171 fi
172 sleep 1
173 done
174 '';
175 };
176in
177{
178 # Dedicated, unprivileged kiosk user. Gamepad input comes through evdev
179 # (input group), audio through the system-wide pipewire socket (pipewire
180 # group), and the games need video/audio like any local player.
181 users.users.strom = {
182 isNormalUser = true;
183 extraGroups = [
184 "video"
185 "audio"
186 "input"
187 "pipewire"
188 ];
189 };
190
191 # Kiosk autostart: a gamescope session on DRM (via greetd autologin),
192 # replacing cage. gamescope presents straight to the display, so there is no
193 # nested Wayland swapchain for RADV to storm on this Vega iGPU. greetd gives
194 # the strom user a real seat/VT (DRM master) and a delegated user manager,
195 # which the launcher's systemd-run --user --scope relies on for game cgroups.
196 services.greetd = {
197 enable = true;
198 settings =
199 let
200 session = {
201 command = "${pkgs.gamescope}/bin/gamescope -- ${strom-session}";
202 user = "strom";
203 };
204 in
205 {
206 # initial_session autologins on boot with no prompt; default_session
207 # re-runs it (also autologin) if the gamescope session ever exits.
208 initial_session = session;
209 default_session = session;
210 };
211 };
212
213 # The panel comes up at ~1% backlight, unreadable for a couch setup. Ship
214 # brightnessctl + its udev rules (chgrp backlight to the video group, g+w) so
215 # any video-group user (strom, lass) can adjust it without root, e.g.
216 # `brightnessctl set 100%` / `brightnessctl set 50%`.
217 environment.systemPackages = [
218 pkgs.brightnessctl
219 strom-pair
220 ];
221 services.udev.packages = [ pkgs.brightnessctl ];
222
223 # Force the backlight to maximum on every boot. Ordered after
224 # systemd-backlight, which would otherwise restore the last (dim) saved level.
225 systemd.services.max-brightness = {
226 wantedBy = [ "multi-user.target" ];
227 after = [ "systemd-backlight@backlight:amdgpu_bl1.service" ];
228 serviceConfig = {
229 Type = "oneshot";
230 ExecStart = "${pkgs.brightnessctl}/bin/brightnessctl set 100%";
231 };
232 };
233
234 # Xbox Wireless Controllers (Bluetooth). xpadneo for correct rumble/axis
235 # mapping; the two bluez tweaks + disable_ertm are what make Xbox pads
236 # actually bond (without disable_ertm they fail with AuthenticationFailed).
237 # Bluetooth itself (adapter + powerOnBoot) already comes from
238 # 2configs/pipewire.nix. Pair each pad once with `strom-pair` (see above).
239 hardware.xpadneo.enable = true;
240 # Xbox One/Series controllers pair over Bluetooth only with L2CAP ERTM off.
241 boot.extraModprobeConfig = "options bluetooth disable_ertm=1";
242 hardware.bluetooth.settings.General = {
243 # JustWorksRepairing lets bluez accept fresh pairings without a manual
244 # remove; Privacy = device pins a static host address so the controller's
245 # bond key survives RPA rotation.
246 JustWorksRepairing = "always";
247 Privacy = "device";
248 # Keep page scan always on so a bonded pad reconnects promptly when powered
249 # back on (otherwise bonded pads bond but never re-link). Fine on a
250 # mains-powered kiosk.
251 FastConnectable = true;
252 };
253}