2025-01-21 21:30:36 -08:00
{
config ,
lib ,
pkgs ,
. . .
} :
let
cfg = config . programs . opengamepadui ;
gamescopeCfg = config . programs . gamescope ;
opengamepadui-gamescope =
let
exports = lib . mapAttrsToList ( n : v : " e x p o r t ${ n } = ${ v } " ) cfg . gamescopeSession . env ;
in
# Based on gamescope-session-plus from ChimeraOS
pkgs . writeShellScriptBin " o p e n g a m e p a d u i - g a m e s c o p e " ''
$ { 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 } / g a m e s c o p e / m o d e s . c f g "
export GAMESCOPE_PATCHED_EDID_FILE = " ' ' ${ XDG_CONFIG_HOME : - $ HOME/.config } / g a m e s c o p e / e d i d . b i n "
# Make path to gamescope mode save file.
mkdir - p " $ ( d i r n a m e " $ GAMESCOPE_MODE_SAVE_FILE " ) "
touch " $ G A M E S C O P E _ M O D E _ S A V E _ F I L E "
# Make path to Gamescope edid patched file.
mkdir - p " $ ( d i r n a m e " $ GAMESCOPE_PATCHED_EDID_FILE " ) "
touch " $ G A M E S C O P E _ P A T C H E D _ E D I D _ F I L E "
# 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 " $ ( d i r n a m e " $ MANGOHUD_CONFIGFILE " ) "
echo " n o _ d i s p l a y " > " $ M A N G O H U D _ C O N F I G F I L E "
# Prepare our initial VRS config file
# for dynamic VRS in Mesa.
mkdir - p " $ ( d i r n a m e " $ RADV_FORCE_VRS_CONFIG_FILE " ) "
echo " 1 x 1 " > " $ R A D V _ F O R C E _ V R S _ C O N F I G _ F I L E "
# 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 } ] ] & & m k t e m p - p " $ XDG_RUNTIME_DIR " - d - t g a m e s c o p e . X X X X X X X ) "
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 } ] ] ; t h e n
echo > & 2 " ! ! F a i l e d t o f i n d r u n d i r e c t o r y i n w h i c h t o c r e a t e s t a t s s e s s i o n s o c k e t s ( i s \$ X D G _ R U N T I M E _ D I R s e t ? ) "
exit 0
fi
export GAMESCOPE_STATS = " $ s t a t s "
mkfifo - - " $ s t a t s "
mkfifo - - " $ s o c k e t "
# Start gamescope compositor, log it's output and background it
echo gamescope $ { lib . escapeShellArgs cfg . gamescopeSession . args } - R $ socket - T $ stats > " $ H O M E " /.gamescope-cmd.log
gamescope $ { lib . escapeShellArgs cfg . gamescopeSession . args } - R $ socket - T $ stats > " $ H O M E " /.gamescope-stdout.log 2 > & 1 &
gamescope_pid = " $ ! "
if read - r - t 3 response_x_display response_wl_display < > " $ s o c k e t " ; then
export DISPLAY = " $ r e s p o n s e _ x _ d i s p l a y "
export GAMESCOPE_WAYLAND_DISPLAY = " $ r e s p o n s e _ w l _ d i s p l a y "
# We're done!
else
echo " g a m e s c o p e f a i l e d "
kill -9 " $ g a m e s c o p e _ p i d "
wait - n " $ g a m e s c o p e _ p i d "
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 > " $ H O M E " /.mangoapp-stdout.log 2 > & 1
done ) &
fi
# Start OpenGamepadUI
opengamepadui $ { lib . escapeShellArgs cfg . args }
# When the client exits, kill gamescope nicely
kill $ gamescope_pid
'' ;
gamescopeSessionFile =
( pkgs . writeTextDir " s h a r e / w a y l a n d - s e s s i o n s / o p e n g a m e p a d u i . d e s k t o p " ''
[ Desktop Entry ]
Name = opengamepadui
Comment = OpenGamepadUI Session
Exec = $ { opengamepadui-gamescope } /bin/opengamepadui-gamescope
Type = Application
'' ) . o v e r r i d e A t t r s
( _ : {
passthru . providedSessions = [ " o p e n g a m e p a d u i " ] ;
} ) ;
in
{
options . programs . opengamepadui = {
enable = lib . mkEnableOption " o p e n g a m e p a d u i " ;
args = lib . mkOption {
type = lib . types . listOf lib . types . str ;
default = [ ] ;
description = ''
Arguments to be passed to OpenGamepadUI
'' ;
} ;
package = lib . mkPackageOption pkgs " O p e n G a m e p a d U I " {
default = [ " o p e n g a m e p a d u i " ] ;
} ;
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 ;
default = config . fonts . packages ;
defaultText = lib . literalExpression " b u i l t i n s . f i l t e r l i b . t y p e s . p a c k a g e . c h e c k c o n f i g . f o n t s . p a c k a g e s " ;
example = lib . literalExpression " w i t h p k g s ; [ s o u r c e - h a n - s a n s ] " ;
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 = " R u n a G a m e S c o p e d r i v e n O p e n G a m e p a d U I s e s s i o n f r o m y o u r d i s p l a y - m a n a g e r " ;
default = { } ;
type = lib . types . submodule {
options = {
enable = lib . mkEnableOption " G a m e S c o p e S e s s i o n " ;
args = lib . mkOption {
type = lib . types . listOf lib . types . str ;
default = [
" - - p r e f e r - o u t p u t "
" * , e D P - 1 "
" - - x w a y l a n d - c o u n t "
" 2 "
" - - d e f a u l t - t o u c h - m o d e "
" 4 "
" - - h i d e - c u r s o r - d e l a y "
" 3 0 0 0 "
" - - f a d e - o u t - d u r a t i o n "
" 2 0 0 "
" - - s t e a m "
] ;
description = ''
Arguments to be passed to GameScope for the session .
'' ;
} ;
env = lib . mkOption {
type = lib . types . attrsOf lib . types . str ;
default = { } ;
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 control and performance settings .
'' ;
} ;
config = lib . mkIf cfg . enable {
hardware . graphics = {
# this fixes the "glXChooseVisual failed" bug, context: https://github.com/NixOS/nixpkgs/issues/47932
enable = true ;
enable32Bit = pkgs . stdenv . hostPlatform . isx86_64 ;
} ;
security . wrappers = lib . mkIf ( cfg . gamescopeSession . enable && gamescopeCfg . capSysNice ) {
# needed or steam plugin fails
bwrap = {
owner = " r o o t " ;
group = " r o o t " ;
source = lib . getExe pkgs . bubblewrap ;
setuid = true ;
} ;
} ;
programs . opengamepadui . extraPackages = cfg . fontPackages ;
programs . gamescope . enable = true ;
services . displayManager . sessionPackages = lib . mkIf cfg . gamescopeSession . enable [
gamescopeSessionFile
] ;
programs . opengamepadui . gamescopeSession . env = {
# Fix intel color corruption
# might come with some performance degradation but is better than a corrupted
# color image
INTEL_DEBUG = " n o r b c " ;
mesa_glthread = " t r u e " ;
# 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 = " x c b " ;
# 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 _ E S t r e a m C o l o r s p a c e _ B T 6 0 1 " ;
# 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 = " d x g i = n " ;
# Don't wait for buffers to idle on the client side before sending them to gamescope
vk_xwayland_wait_ready = " f a l s e " ;
# Temporary crutch until dummy plane interactions / etc are figured out
GAMESCOPE_DISABLE_ASYNC_FLIPS = " 1 " ;
} ;
# optionally enable 32bit pulseaudio support if pulseaudio is enabled
services . pulseaudio . support32Bit = config . services . 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 = [ " / s h a r e " ] ;
environment . systemPackages = [
cfg . package
2025-07-22 15:19:36 +02:00
]
++ lib . optional cfg . gamescopeSession . enable opengamepadui-gamescope ;
2025-01-21 21:30:36 -08:00
} ;
meta . maintainers = with lib . maintainers ; [ shadowapex ] ;
}