diff --git a/nixos/doc/manual/release-notes/rl-2505.section.md b/nixos/doc/manual/release-notes/rl-2505.section.md index 1caa762d4ebf1d..a8ff65836eb886 100644 --- a/nixos/doc/manual/release-notes/rl-2505.section.md +++ b/nixos/doc/manual/release-notes/rl-2505.section.md @@ -71,6 +71,8 @@ - [nvidia-gpu](https://github.com/utkuozdemir/nvidia_gpu_exporter), a Prometheus exporter that scrapes `nvidia-smi` for GPU metrics. Available as [services.prometheus.exporters.nvidia-gpu](#opt-services.prometheus.exporters.nvidia-gpu.enable). +- [OpenGamepadUI](https://github.com/ShadowBlip/OpenGamepadUI/), an open source gamepad-native game launcher and overlay for Linux. Available as [programs.opengamepadui](#opt-programs.opengamepadui.enable). + - [InputPlumber](https://github.com/ShadowBlip/InputPlumber/), an open source input router and remapper daemon for Linux. Available as [services.inputplumber](#opt-services.inputplumber.enable). - [Buffyboard](https://gitlab.postmarketos.org/postmarketOS/buffybox/-/tree/master/buffyboard), a framebuffer on-screen keyboard. Available as [services.buffyboard](option.html#opt-services.buffyboard). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 9fe8e3be6477d3..ae46bce174f702 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -267,6 +267,7 @@ ./programs/ns-usbloader.nix ./programs/oblogout.nix ./programs/oddjobd.nix + ./programs/opengamepadui.nix ./programs/openvpn3.nix ./programs/obs-studio.nix ./programs/partition-manager.nix diff --git a/nixos/modules/programs/opengamepadui.nix b/nixos/modules/programs/opengamepadui.nix new file mode 100644 index 00000000000000..095476f4cab207 --- /dev/null +++ b/nixos/modules/programs/opengamepadui.nix @@ -0,0 +1,295 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.programs.opengamepadui; + gamescopeCfg = config.programs.gamescope; + + opengamepadui-gamescope = + let + exports = builtins.attrValues ( + builtins.mapAttrs (n: v: "export ${n}=${v}") cfg.gamescopeSession.env + ); + in + # Based on gamescope-session-plus from ChimeraOS + pkgs.writeShellScriptBin "opengamepadui-gamescope" '' + ${builtins.concatStringsSep "\n" exports} + + # Enable Mangoapp + export MANGOHUD_CONFIGFILE=$(mktemp /tmp/mangohud.XXXXXXXX) + export RADV_FORCE_VRS_CONFIG_FILE=$(mktemp /tmp/radv_vrs.XXXXXXXX) + + # Plop GAMESCOPE_MODE_SAVE_FILE into $XDG_CONFIG_HOME (defaults to ~/.config). + export GAMESCOPE_MODE_SAVE_FILE="''${XDG_CONFIG_HOME:-$HOME/.config}/gamescope/modes.cfg" + export GAMESCOPE_PATCHED_EDID_FILE="''${XDG_CONFIG_HOME:-$HOME/.config}/gamescope/edid.bin" + + # Make path to gamescope mode save file. + mkdir -p "$(dirname "$GAMESCOPE_MODE_SAVE_FILE")" + touch "$GAMESCOPE_MODE_SAVE_FILE" + + # Make path to Gamescope edid patched file. + mkdir -p "$(dirname "$GAMESCOPE_PATCHED_EDID_FILE")" + touch "$GAMESCOPE_PATCHED_EDID_FILE" + + # Initially write no_display to our config file + # so we don't get mangoapp showing up before OpenGamepadUI initializes + # on OOBE and stuff. + mkdir -p "$(dirname "$MANGOHUD_CONFIGFILE")" + echo "no_display" >"$MANGOHUD_CONFIGFILE" + + # Prepare our initial VRS config file + # for dynamic VRS in Mesa. + mkdir -p "$(dirname "$RADV_FORCE_VRS_CONFIG_FILE")" + echo "1x1" >"$RADV_FORCE_VRS_CONFIG_FILE" + + # To play nice with the short term callback-based limiter for now + export GAMESCOPE_LIMITER_FILE=$(mktemp /tmp/gamescope-limiter.XXXXXXXX) + + ulimit -n 524288 + + # Setup socket for gamescope + # Create run directory file for startup and stats sockets + tmpdir="$([[ -n ''${XDG_RUNTIME_DIR+x} ]] && mktemp -p "$XDG_RUNTIME_DIR" -d -t gamescope.XXXXXXX)" + socket="''${tmpdir:+$tmpdir/startup.socket}" + stats="''${tmpdir:+$tmpdir/stats.pipe}" + + # Fail early if we don't have a proper runtime directory setup + if [[ -z $tmpdir || -z ''${XDG_RUNTIME_DIR+x} ]]; then + echo >&2 "!! Failed to find run directory in which to create stats session sockets (is \$XDG_RUNTIME_DIR set?)" + exit 0 + fi + + export GAMESCOPE_STATS="$stats" + mkfifo -- "$stats" + mkfifo -- "$socket" + + # Start gamescope compositor, log it's output and background it + echo gamescope ${builtins.toString cfg.gamescopeSession.args} -R $socket -T $stats >"$HOME"/.gamescope-cmd.log + gamescope ${builtins.toString cfg.gamescopeSession.args} -R $socket -T $stats >"$HOME"/.gamescope-stdout.log 2>&1 & + gamescope_pid="$!" + + if read -r -t 3 response_x_display response_wl_display <>"$socket"; then + export DISPLAY="$response_x_display" + export GAMESCOPE_WAYLAND_DISPLAY="$response_wl_display" + # We're done! + else + echo "gamescope failed" + kill -9 "$gamescope_pid" + wait -n "$gamescope_pid" + exit 1 + # Systemd or Session manager will have to restart session + fi + + # If we have mangoapp binary start it + if command -v mangoapp >/dev/null; then + (while true; do + sleep 1 + mangoapp >"$HOME"/.mangoapp-stdout.log 2>&1 + done) & + fi + + # Start OpenGamepadUI + opengamepadui ${builtins.toString cfg.args} + + # When the client exits, kill gamescope nicely + kill $gamescope_pid + ''; + + gamescopeSessionFile = + (pkgs.writeTextDir "share/wayland-sessions/opengamepadui.desktop" '' + [Desktop Entry] + Name=opengamepadui + Comment=OpenGamepadUI Session + Exec=${opengamepadui-gamescope}/bin/opengamepadui-gamescope + Type=Application + '').overrideAttrs + (_: { + passthru.providedSessions = [ "opengamepadui" ]; + }); +in +{ + options.programs.opengamepadui = { + enable = lib.mkEnableOption "opengamepadui"; + + args = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + Arguments to be passed to OpenGamepadUI + ''; + }; + + package = lib.mkOption { + type = lib.types.package; + default = pkgs.opengamepadui; + defaultText = lib.literalExpression "pkgs.opengamepadui"; + example = lib.literalExpression '' + pkgs.opengamepadui.override { + extraEnv = { + MANGOHUD = true; + OBS_VKCAPTURE = true; + RADV_TEX_ANISO = 16; + }; + extraLibraries = p: with p; [ + atk + ]; + } + ''; + + description = '' + The OpenGamepadUI package to use. Additional libraries are added from the system + configuration to ensure graphics work properly. + + Use this option to customise the OpenGamepadUI package rather than adding your + custom opengamepadui to {option}`environment.systemPackages` yourself. + ''; + }; + + extraPackages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + example = lib.literalExpression '' + with pkgs; [ + gamescope + ] + ''; + description = '' + Additional packages to add to the OpenGamepadUI environment. + ''; + }; + + fontPackages = lib.mkOption { + type = lib.types.listOf lib.types.package; + # `fonts.packages` is a list of paths now, filter out which are not packages + default = builtins.filter lib.types.package.check config.fonts.packages; + defaultText = lib.literalExpression "builtins.filter lib.types.package.check config.fonts.packages"; + example = lib.literalExpression "with pkgs; [ source-han-sans ]"; + description = '' + Font packages to use in OpenGamepadUI. + + 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. + ''; + }; + + gamescopeSession = lib.mkOption { + description = "Run a GameScope driven OpenGamepadUI session from your display-manager"; + default = { }; + type = lib.types.submodule { + options = { + enable = lib.mkEnableOption "GameScope Session"; + args = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ + "--prefer-output" + "*,eDP-1" + "--xwayland-count" + "2" + "--default-touch-mode" + "4" + "--hide-cursor-delay" + "3000" + "--fade-out-duration" + "200" + "--steam" + ]; + description = '' + Arguments to be passed to GameScope for the session. + ''; + }; + + env = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { + # Fix intel color corruption + # might come with some performance degradation but is better than a corrupted + # color image + INTEL_DEBUG = "norbc"; + mesa_glthread = "true"; + # This should be used by default by gamescope. Cannot hurt to force it anyway. + # Reported better framelimiting with this enabled + ENABLE_GAMESCOPE_WSI = "1"; + # Force Qt applications to run under xwayland + QT_QPA_PLATFORM = "xcb"; + # Some environment variables by default (taken from Deck session) + SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS = "0"; + # There is no way to set a color space for an NV12 + # buffer in Wayland. And the color management protocol that is + # meant to let this happen is missing the color range... + # So just workaround this with an ENV var that Remote Play Together + # and Gamescope will use for now. + GAMESCOPE_NV12_COLORSPACE = "k_EStreamColorspace_BT601"; + # Workaround older versions of vkd3d-proton setting this + # too low (desc.BufferCount), resulting in symptoms that are potentially like + # swapchain starvation. + VKD3D_SWAPCHAIN_LATENCY_FRAMES = "3"; + # To expose vram info from radv + WINEDLLOVERRIDES = "dxgi=n"; + # Don't wait for buffers to idle on the client side before sending them to gamescope + vk_xwayland_wait_ready = "false"; + # Temporary crutch until dummy plane interactions / etc are figured out + GAMESCOPE_DISABLE_ASYNC_FLIPS = "1"; + }; + description = '' + Environmental variables to be passed to GameScope for the session. + ''; + }; + }; + }; + }; + + inputplumber.enable = lib.mkEnableOption '' + Run InputPlumber service for input management and gamepad configuration. + ''; + + powerstation.enable = lib.mkEnableOption '' + Run PowerStation service for TDP and performance control. + ''; + }; + + config = lib.mkIf cfg.enable { + hardware.graphics = { + # this fixes the "glXChooseVisual failed" bug, context: https://github.com/NixOS/nixpkgs/issues/47932 + enable = true; + enable32Bit = true; + }; + + security.wrappers = lib.mkIf (cfg.gamescopeSession.enable && gamescopeCfg.capSysNice) { + # needed or steam plugin fails + bwrap = { + owner = "root"; + group = "root"; + source = "${pkgs.bubblewrap}/bin/bwrap"; + setuid = true; + }; + }; + + programs.opengamepadui.extraPackages = cfg.fontPackages; + + programs.gamescope.enable = lib.mkForce cfg.gamescopeSession.enable; + services.displayManager.sessionPackages = lib.mkIf cfg.gamescopeSession.enable [ + gamescopeSessionFile + ]; + + # optionally enable 32bit pulseaudio support if pulseaudio is enabled + hardware.pulseaudio.support32Bit = config.hardware.pulseaudio.enable; + services.pipewire.alsa.support32Bit = config.services.pipewire.alsa.enable; + + hardware.steam-hardware.enable = true; + + services.inputplumber.enable = lib.mkDefault cfg.inputplumber.enable; + services.powerstation.enable = lib.mkDefault cfg.powerstation.enable; + + environment.pathsToLink = [ "/share" ]; + + environment.systemPackages = [ + cfg.package + cfg.package.run + ] ++ lib.optional cfg.gamescopeSession.enable opengamepadui-gamescope; + }; + + meta.maintainers = with lib.maintainers; [ shadowapex ]; +} diff --git a/pkgs/by-name/op/opengamepadui/package.nix b/pkgs/by-name/op/opengamepadui/package.nix new file mode 100644 index 00000000000000..95e39479c8827e --- /dev/null +++ b/pkgs/by-name/op/opengamepadui/package.nix @@ -0,0 +1,154 @@ +{ + lib, + stdenv, + rustPlatform, + cargo, + fetchFromGitHub, + alsa-lib, + autoPatchelfHook, + dbus, + fontconfig, + gamescope, + gnumake, + godot_4, + godot_4-export-templates, + libGL, + libX11, + libXcursor, + libXext, + libXfixes, + libXi, + libXinerama, + libXrandr, + libXrender, + libxkbcommon, + openssl, + pkg-config, + vulkan-loader, + xorg, + udev, + libpulseaudio, + hwdata, + glxinfo, + upower, + withDebug ? false, +}: + +stdenv.mkDerivation rec { + pname = "opengamepadui"; + version = "0.35.1"; + + buildType = "${if withDebug then "debug" else "release"}"; + + src = fetchFromGitHub { + owner = "ShadowBlip"; + repo = "OpenGamepadUI"; + tag = "v${version}"; + hash = "sha256-741itXn39MxVMTNN9Azx2n5L1PRo5UsRsm6JSeY9nSI="; + }; + + cargoDeps = rustPlatform.fetchCargoVendor { + inherit src; + sourceRoot = "source/${cargoRoot}"; + hash = "sha256-sTzMewIfKHbmVhSPZgUIzFFz1ahK+PMoQ5oB4GEt8nY="; + }; + cargoRoot = "extensions"; + + nativeBuildInputs = [ + autoPatchelfHook + cargo + dbus.dev + gnumake + godot_4 + godot_4-export-templates + openssl + pkg-config + rustPlatform.cargoSetupHook + xorg.libX11.dev + xorg.libXi.dev + xorg.libXres.dev + xorg.libXtst + ]; + + buildInputs = [ + dbus + dbus.lib + fontconfig + fontconfig.lib + stdenv.cc.cc.lib + ]; + + runtimeDependencies = [ + alsa-lib + dbus + dbus.lib + gamescope + fontconfig + fontconfig.lib + glxinfo + hwdata + libGL + libX11 + libXcursor + libXext + libXfixes + libXi + libXinerama + libXrandr + libXrender + libpulseaudio + libxkbcommon + udev + upower + vulkan-loader + xorg.libX11 + xorg.libXi + xorg.libXres + xorg.libXtst + ]; + + dontStrip = withDebug; + + buildPhase = '' + runHook preBuild + + export GODOT="${godot_4}/bin/godot4" + export GODOT_VERSION=$($GODOT --version | grep -o '[0-9].*[0-9]\.' | sed 's/\.$//') + export GODOT_RELEASE=$($GODOT --version | grep -oP '^[0-9].*?[a-z]\.' | grep -oP '[a-z]+') + + # Godot looks for export templates in HOME + export HOME=$(mktemp -d) + mkdir -p $HOME/.local/share/godot/export_templates + ln -s "${godot_4-export-templates}" "$HOME/.local/share/godot/export_templates/$GODOT_VERSION.$GODOT_RELEASE" + + export BUILD_TYPE=${buildType} + make clean + make build + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + mkdir -p $out + export EXPORT_TEMPLATE="${godot_4-export-templates}" + export BUILD_TYPE=${buildType} + make GODOT="${godot_4}/bin/godot4" install PREFIX=$out + + # The Godot binary looks in "../lib" for gdextensions + mkdir -p $out/share/lib + mv $out/share/opengamepadui/*.so $out/share/lib + + runHook postInstall + ''; + + meta = { + description = "Open source gamepad-native game launcher and overlay"; + homepage = "https://github.com/ShadowBlip/OpenGamepadUI"; + license = lib.licenses.gpl3Only; + changelog = "https://github.com/ShadowBlip/OpenGamepadUI/releases/tag/v${version}"; + maintainers = with lib.maintainers; [ shadowapex ]; + mainProgram = "opengamepadui"; + }; +}