1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 cfg = config.programs.opengamepadui;
10 gamescopeCfg = config.programs.gamescope;
11
12 opengamepadui-gamescope =
13 let
14 exports = lib.mapAttrsToList (n: v: "export ${n}=${v}") cfg.gamescopeSession.env;
15 in
16 # Based on gamescope-session-plus from ChimeraOS
17 pkgs.writeShellScriptBin "opengamepadui-gamescope" ''
18 ${builtins.concatStringsSep "\n" exports}
19
20 # Enable Mangoapp
21 export MANGOHUD_CONFIGFILE=$(mktemp /tmp/mangohud.XXXXXXXX)
22 export RADV_FORCE_VRS_CONFIG_FILE=$(mktemp /tmp/radv_vrs.XXXXXXXX)
23
24 # Plop GAMESCOPE_MODE_SAVE_FILE into $XDG_CONFIG_HOME (defaults to ~/.config).
25 export GAMESCOPE_MODE_SAVE_FILE="''${XDG_CONFIG_HOME:-$HOME/.config}/gamescope/modes.cfg"
26 export GAMESCOPE_PATCHED_EDID_FILE="''${XDG_CONFIG_HOME:-$HOME/.config}/gamescope/edid.bin"
27
28 # Make path to gamescope mode save file.
29 mkdir -p "$(dirname "$GAMESCOPE_MODE_SAVE_FILE")"
30 touch "$GAMESCOPE_MODE_SAVE_FILE"
31
32 # Make path to Gamescope edid patched file.
33 mkdir -p "$(dirname "$GAMESCOPE_PATCHED_EDID_FILE")"
34 touch "$GAMESCOPE_PATCHED_EDID_FILE"
35
36 # Initially write no_display to our config file
37 # so we don't get mangoapp showing up before OpenGamepadUI initializes
38 # on OOBE and stuff.
39 mkdir -p "$(dirname "$MANGOHUD_CONFIGFILE")"
40 echo "no_display" >"$MANGOHUD_CONFIGFILE"
41
42 # Prepare our initial VRS config file
43 # for dynamic VRS in Mesa.
44 mkdir -p "$(dirname "$RADV_FORCE_VRS_CONFIG_FILE")"
45 echo "1x1" >"$RADV_FORCE_VRS_CONFIG_FILE"
46
47 # To play nice with the short term callback-based limiter for now
48 export GAMESCOPE_LIMITER_FILE=$(mktemp /tmp/gamescope-limiter.XXXXXXXX)
49
50 ulimit -n 524288
51
52 # Setup socket for gamescope
53 # Create run directory file for startup and stats sockets
54 tmpdir="$([[ -n ''${XDG_RUNTIME_DIR+x} ]] && mktemp -p "$XDG_RUNTIME_DIR" -d -t gamescope.XXXXXXX)"
55 socket="''${tmpdir:+$tmpdir/startup.socket}"
56 stats="''${tmpdir:+$tmpdir/stats.pipe}"
57
58 # Fail early if we don't have a proper runtime directory setup
59 if [[ -z $tmpdir || -z ''${XDG_RUNTIME_DIR+x} ]]; then
60 echo >&2 "!! Failed to find run directory in which to create stats session sockets (is \$XDG_RUNTIME_DIR set?)"
61 exit 0
62 fi
63
64 export GAMESCOPE_STATS="$stats"
65 mkfifo -- "$stats"
66 mkfifo -- "$socket"
67
68 # Start gamescope compositor, log it's output and background it
69 echo gamescope ${lib.escapeShellArgs cfg.gamescopeSession.args} -R $socket -T $stats >"$HOME"/.gamescope-cmd.log
70 gamescope ${lib.escapeShellArgs cfg.gamescopeSession.args} -R $socket -T $stats >"$HOME"/.gamescope-stdout.log 2>&1 &
71 gamescope_pid="$!"
72
73 if read -r -t 3 response_x_display response_wl_display <>"$socket"; then
74 export DISPLAY="$response_x_display"
75 export GAMESCOPE_WAYLAND_DISPLAY="$response_wl_display"
76 # We're done!
77 else
78 echo "gamescope failed"
79 kill -9 "$gamescope_pid"
80 wait -n "$gamescope_pid"
81 exit 1
82 # Systemd or Session manager will have to restart session
83 fi
84
85 # If we have mangoapp binary start it
86 if command -v mangoapp >/dev/null; then
87 (while true; do
88 sleep 1
89 mangoapp >"$HOME"/.mangoapp-stdout.log 2>&1
90 done) &
91 fi
92
93 # Start OpenGamepadUI
94 opengamepadui ${lib.escapeShellArgs cfg.args}
95
96 # When the client exits, kill gamescope nicely
97 kill $gamescope_pid
98 '';
99
100 gamescopeSessionFile =
101 (pkgs.writeTextDir "share/wayland-sessions/opengamepadui.desktop" ''
102 [Desktop Entry]
103 Name=opengamepadui
104 Comment=OpenGamepadUI Session
105 Exec=${opengamepadui-gamescope}/bin/opengamepadui-gamescope
106 Type=Application
107 '').overrideAttrs
108 (_: {
109 passthru.providedSessions = [ "opengamepadui" ];
110 });
111in
112{
113 options.programs.opengamepadui = {
114 enable = lib.mkEnableOption "opengamepadui";
115
116 args = lib.mkOption {
117 type = lib.types.listOf lib.types.str;
118 default = [ ];
119 description = ''
120 Arguments to be passed to OpenGamepadUI
121 '';
122 };
123
124 package = lib.mkPackageOption pkgs "OpenGamepadUI" {
125 default = [ "opengamepadui" ];
126 };
127
128 extraPackages = lib.mkOption {
129 type = lib.types.listOf lib.types.package;
130 default = [ ];
131 example = lib.literalExpression ''
132 with pkgs; [
133 gamescope
134 ]
135 '';
136 description = ''
137 Additional packages to add to the OpenGamepadUI environment.
138 '';
139 };
140
141 fontPackages = lib.mkOption {
142 type = lib.types.listOf lib.types.package;
143 default = config.fonts.packages;
144 defaultText = lib.literalExpression "builtins.filter lib.types.package.check config.fonts.packages";
145 example = lib.literalExpression "with pkgs; [ source-han-sans ]";
146 description = ''
147 Font packages to use in OpenGamepadUI.
148
149 Defaults to system fonts, but could be overridden to use other fonts — useful for users who would like to customize CJK fonts used in opengamepadui. According to the [upstream issue](https://github.com/ValveSoftware/opengamepadui-for-linux/issues/10422#issuecomment-1944396010), opengamepadui only follows the per-user fontconfig configuration.
150 '';
151 };
152
153 gamescopeSession = lib.mkOption {
154 description = "Run a GameScope driven OpenGamepadUI session from your display-manager";
155 default = { };
156 type = lib.types.submodule {
157 options = {
158 enable = lib.mkEnableOption "GameScope Session";
159 args = lib.mkOption {
160 type = lib.types.listOf lib.types.str;
161 default = [
162 "--prefer-output"
163 "*,eDP-1"
164 "--xwayland-count"
165 "2"
166 "--default-touch-mode"
167 "4"
168 "--hide-cursor-delay"
169 "3000"
170 "--fade-out-duration"
171 "200"
172 "--steam"
173 ];
174 description = ''
175 Arguments to be passed to GameScope for the session.
176 '';
177 };
178
179 env = lib.mkOption {
180 type = lib.types.attrsOf lib.types.str;
181 default = { };
182 description = ''
183 Environmental variables to be passed to GameScope for the session.
184 '';
185 };
186 };
187 };
188 };
189
190 inputplumber.enable = lib.mkEnableOption ''
191 Run InputPlumber service for input management and gamepad configuration.
192 '';
193
194 powerstation.enable = lib.mkEnableOption ''
195 Run PowerStation service for TDP control and performance settings.
196 '';
197 };
198
199 config = lib.mkIf cfg.enable {
200 hardware.graphics = {
201 # this fixes the "glXChooseVisual failed" bug, context: https://github.com/NixOS/nixpkgs/issues/47932
202 enable = true;
203 enable32Bit = pkgs.stdenv.hostPlatform.isx86_64;
204 };
205
206 security.wrappers = lib.mkIf (cfg.gamescopeSession.enable && gamescopeCfg.capSysNice) {
207 # needed or steam plugin fails
208 bwrap = {
209 owner = "root";
210 group = "root";
211 source = lib.getExe pkgs.bubblewrap;
212 setuid = true;
213 };
214 };
215
216 programs.opengamepadui.extraPackages = cfg.fontPackages;
217
218 programs.gamescope.enable = true;
219 services.displayManager.sessionPackages = lib.mkIf cfg.gamescopeSession.enable [
220 gamescopeSessionFile
221 ];
222
223 programs.opengamepadui.gamescopeSession.env = {
224 # Fix intel color corruption
225 # might come with some performance degradation but is better than a corrupted
226 # color image
227 INTEL_DEBUG = "norbc";
228 mesa_glthread = "true";
229 # This should be used by default by gamescope. Cannot hurt to force it anyway.
230 # Reported better framelimiting with this enabled
231 ENABLE_GAMESCOPE_WSI = "1";
232 # Force Qt applications to run under xwayland
233 QT_QPA_PLATFORM = "xcb";
234 # Some environment variables by default (taken from Deck session)
235 SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS = "0";
236 # There is no way to set a color space for an NV12
237 # buffer in Wayland. And the color management protocol that is
238 # meant to let this happen is missing the color range...
239 # So just workaround this with an ENV var that Remote Play Together
240 # and Gamescope will use for now.
241 GAMESCOPE_NV12_COLORSPACE = "k_EStreamColorspace_BT601";
242 # Workaround older versions of vkd3d-proton setting this
243 # too low (desc.BufferCount), resulting in symptoms that are potentially like
244 # swapchain starvation.
245 VKD3D_SWAPCHAIN_LATENCY_FRAMES = "3";
246 # To expose vram info from radv
247 WINEDLLOVERRIDES = "dxgi=n";
248 # Don't wait for buffers to idle on the client side before sending them to gamescope
249 vk_xwayland_wait_ready = "false";
250 # Temporary crutch until dummy plane interactions / etc are figured out
251 GAMESCOPE_DISABLE_ASYNC_FLIPS = "1";
252 };
253
254 # optionally enable 32bit pulseaudio support if pulseaudio is enabled
255 services.pulseaudio.support32Bit = config.services.pulseaudio.enable;
256 services.pipewire.alsa.support32Bit = config.services.pipewire.alsa.enable;
257
258 hardware.steam-hardware.enable = true;
259
260 services.inputplumber.enable = lib.mkDefault cfg.inputplumber.enable;
261 services.powerstation.enable = lib.mkDefault cfg.powerstation.enable;
262
263 environment.pathsToLink = [ "/share" ];
264
265 environment.systemPackages = [
266 cfg.package
267 ]
268 ++ lib.optional cfg.gamescopeSession.enable opengamepadui-gamescope;
269 };
270
271 meta.maintainers = with lib.maintainers; [ shadowapex ];
272}