mirror of
https://github.com/ryan4yin/nix-config.git
synced 2026-09-09 19:41:46 +02:00
fix(sunshine): use Intel rendering and recover remote Niri sessions (#268)
- render Niri on the Intel iGPU while retaining NVIDIA-connected physical outputs - encode Sunshine streams with Intel VAAPI and wait for a Niri output before startup - load VKMS persistently for headless sessions and configure `Virtual-1` at 2560x1600@60 - let Moonlight request the stream bitrate instead of enforcing a 20 Mbps host cap - remove unsupported Dynamic Boost and global NVIDIA session overrides - grant Sunshine access to virtual input devices - add a Nushell recovery helper that starts a temporary greetd/Niri session from SSH - identify and safely replace normal, standalone, or previous transient Niri sessions - launch the transient service in the background and verify greetd, Niri, and Sunshine before returning - ignore local `.worktrees` directories and keep eval regression coverage focused on structural behavior
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
result
|
result
|
||||||
result/
|
result/
|
||||||
.direnv/
|
.direnv/
|
||||||
|
.worktrees/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.pre-commit-config.yaml
|
.pre-commit-config.yaml
|
||||||
logs/
|
logs/
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ in
|
|||||||
|
|
||||||
modules.desktop.gaming.enable = false;
|
modules.desktop.gaming.enable = false;
|
||||||
modules.desktop.niri.enable = true;
|
modules.desktop.niri.enable = true;
|
||||||
modules.desktop.nvidia.enable = false;
|
|
||||||
|
|
||||||
xdg.configFile."niri/niri-hardware.kdl".source =
|
xdg.configFile."niri/niri-hardware.kdl".source =
|
||||||
mkSymlink "${config.home.homeDirectory}/nix-config/hosts/12kingdoms-shoukei/niri-hardware.kdl";
|
mkSymlink "${config.home.homeDirectory}/nix-config/hosts/12kingdoms-shoukei/niri-hardware.kdl";
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ in
|
|||||||
|
|
||||||
modules.desktop.gaming.enable = true;
|
modules.desktop.gaming.enable = true;
|
||||||
modules.desktop.niri.enable = true;
|
modules.desktop.niri.enable = true;
|
||||||
modules.desktop.nvidia.enable = true;
|
|
||||||
|
|
||||||
programs.zed-editor.userSettings = {
|
programs.zed-editor.userSettings = {
|
||||||
ui_font_size = 18.0;
|
ui_font_size = 18.0;
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
with lib;
|
|
||||||
let
|
|
||||||
cfg = config.modules.desktop.nvidia;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options.modules.desktop.nvidia = {
|
|
||||||
enable = mkEnableOption "whether nvidia GPU is used";
|
|
||||||
};
|
|
||||||
|
|
||||||
config = mkIf (cfg.enable && cfg.enable) {
|
|
||||||
home.sessionVariables = {
|
|
||||||
# for hyprland with nvidia gpu" = " ref https://wiki.hyprland.org/Nvidia/
|
|
||||||
"LIBVA_DRIVER_NAME" = "nvidia";
|
|
||||||
"__GLX_VENDOR_LIBRARY_NAME" = "nvidia";
|
|
||||||
# VA-API hardware video acceleration
|
|
||||||
"NVD_BACKEND" = "direct";
|
|
||||||
|
|
||||||
"GBM_BACKEND" = "nvidia-drm";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -14,8 +14,17 @@
|
|||||||
|
|
||||||
# kvm virtualization support
|
# kvm virtualization support
|
||||||
boot.initrd.kernelModules = [ ];
|
boot.initrd.kernelModules = [ ];
|
||||||
boot.kernelModules = [ "kvm-intel" ];
|
boot.kernelModules = [
|
||||||
boot.extraModprobeConfig = "options kvm_intel nested=1";
|
"kvm-intel"
|
||||||
|
# Provide a connected DRM output when the NVIDIA-attached monitors are off.
|
||||||
|
# Niri exposes it as Virtual-1 for headless Sunshine sessions.
|
||||||
|
"vkms"
|
||||||
|
];
|
||||||
|
boot.extraModprobeConfig = ''
|
||||||
|
options kvm_intel nested=1
|
||||||
|
# Create Virtual-1 as soon as vkms loads, including during normal physical sessions.
|
||||||
|
options vkms create_default_dev=1
|
||||||
|
'';
|
||||||
boot.extraModulePackages = [ ];
|
boot.extraModulePackages = [ ];
|
||||||
|
|
||||||
# Intel Graphics
|
# Intel Graphics
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
|
||||||
pkgs,
|
pkgs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
@@ -42,8 +41,6 @@
|
|||||||
# required by most wayland compositors!
|
# required by most wayland compositors!
|
||||||
modesetting.enable = true;
|
modesetting.enable = true;
|
||||||
powerManagement.enable = true;
|
powerManagement.enable = true;
|
||||||
|
|
||||||
dynamicBoost.enable = lib.mkForce true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
hardware.nvidia-container-toolkit.enable = true;
|
hardware.nvidia-container-toolkit.enable = true;
|
||||||
@@ -54,9 +51,23 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
services.sunshine.settings = {
|
services.sunshine.settings = {
|
||||||
max_bitrate = 20000; # in Kbps
|
adapter_name = "/dev/dri/by-path/pci-0000:00:02.0-render"; # Intel iGPU
|
||||||
# NVIDIA NVENC Encoder
|
encoder = "vaapi";
|
||||||
nvenc_preset = 3; # 1(fastest + worst quality) - 7(slowest + best quality)
|
};
|
||||||
nvenc_twopass = "full_res"; # quarter_res / full_res.
|
|
||||||
|
systemd.user.services.sunshine = {
|
||||||
|
after = [ "niri.service" ];
|
||||||
|
environment.LIBVA_DRIVER_NAME = "iHD";
|
||||||
|
preStart = ''
|
||||||
|
for _ in {1..20}; do
|
||||||
|
if ${config.programs.niri.package}/bin/niri msg outputs | ${pkgs.gnugrep}/bin/grep -q '^Output '; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 0.5
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Sunshine requires at least one Niri output" >&2
|
||||||
|
exit 1
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
// running `niri msg outputs` to find outputs
|
// running `niri msg outputs` to find outputs
|
||||||
|
debug {
|
||||||
|
// Keep desktop rendering and ordinary applications on the Intel iGPU.
|
||||||
|
// Physical outputs remain on the NVIDIA GPU and use cross-GPU scanout.
|
||||||
|
render-drm-device "/dev/dri/by-path/pci-0000:00:02.0-render"
|
||||||
|
}
|
||||||
|
|
||||||
output "DP-1" {
|
output "DP-1" {
|
||||||
// Uncomment this line to disable this output.
|
// Uncomment this line to disable this output.
|
||||||
// off
|
// off
|
||||||
@@ -37,6 +43,16 @@ output "HDMI-A-2" {
|
|||||||
position x=2560 y=0 // on the right of DP-2
|
position x=2560 y=0 // on the right of DP-2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output "Virtual-1" {
|
||||||
|
// Created by vkms in hardware-intel.nix. Sunshine captures this output when
|
||||||
|
// the NVIDIA-attached physical monitors are off. It remains available in
|
||||||
|
// normal physical sessions, with minor idle GPU and framebuffer overhead.
|
||||||
|
scale 1
|
||||||
|
transform "normal"
|
||||||
|
mode "2560x1600@60"
|
||||||
|
position x=0 y=0
|
||||||
|
}
|
||||||
|
|
||||||
// ============= Named Workspaces =============
|
// ============= Named Workspaces =============
|
||||||
workspace "4entertainment" { open-on-output "DP-1"; }
|
workspace "4entertainment" { open-on-output "DP-1"; }
|
||||||
workspace "2browser" { open-on-output "DP-1"; }
|
workspace "2browser" { open-on-output "DP-1"; }
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
{ lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
myvars,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
{
|
{
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
moonlight-qt # moonlight client, for streaming games/desktop from a PC
|
moonlight-qt # moonlight client, for streaming games/desktop from a PC
|
||||||
@@ -39,4 +45,6 @@
|
|||||||
wan_encryption_mode = 2;
|
wan_encryption_mode = 2;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
users.users."${myvars.username}".extraGroups = lib.mkIf config.services.sunshine.enable [ "input" ];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Temporary remote session recovery
|
||||||
|
|
||||||
|
When a remote-only machine is stuck at the `greetd` TUI, run the Nushell helper from an
|
||||||
|
SSH session to start one complete PAM/logind Wayland session without changing
|
||||||
|
the persistent NixOS configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nu ./remote-session.nu
|
||||||
|
```
|
||||||
|
|
||||||
|
The helper validates its dependencies before changing the current session. It
|
||||||
|
then stops the normal `greetd` service, starts a transient systemd-managed
|
||||||
|
`greetd` configuration on `/dev/tty1`, and runs Niri directly. It returns after
|
||||||
|
greetd, Niri, and Sunshine are ready. Tailscale is not affected.
|
||||||
|
|
||||||
|
The `Virtual-1` output is created persistently by the `vkms` configuration in
|
||||||
|
`hosts/idols-ai/hardware-intel.nix` and configured by
|
||||||
|
`hosts/idols-ai/niri-hardware.kdl`. It gives Niri and Sunshine a capture target
|
||||||
|
when both physical monitors attached to the NVIDIA GPU are off. The output also
|
||||||
|
exists during normal physical sessions; keeping an idle virtual framebuffer has
|
||||||
|
minor GPU and memory overhead.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
```text
|
||||||
|
-u USER session user (defaults to SUDO_USER)
|
||||||
|
-t TTY tty device, for example /dev/tty2
|
||||||
|
-c COMMAND Wayland session command (defaults to niri --session)
|
||||||
|
-f, --force replace an existing Niri session without confirmation
|
||||||
|
--check validate prerequisites without changing service state
|
||||||
|
```
|
||||||
|
|
||||||
|
A reboot or a manual restart of `greetd.service` restores the normal TUI
|
||||||
|
configuration.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{ }
|
||||||
+151
@@ -0,0 +1,151 @@
|
|||||||
|
#!/usr/bin/env nu
|
||||||
|
|
||||||
|
def user-machine [user: string] {
|
||||||
|
$'($user)@.host'
|
||||||
|
}
|
||||||
|
|
||||||
|
def niri-source [user: string] {
|
||||||
|
let process = (^pgrep -o -u $user -x niri | complete)
|
||||||
|
if $process.exit_code != 0 { return 'none' }
|
||||||
|
|
||||||
|
let pid = ($process.stdout | str trim)
|
||||||
|
let cgroup = (open --raw $'/proc/($pid)/cgroup')
|
||||||
|
if ($cgroup | str contains '/greetd-sunshine.service/') { return 'remote session' }
|
||||||
|
if ($cgroup | str contains '/niri.service') { return 'user niri.service' }
|
||||||
|
'standalone process'
|
||||||
|
}
|
||||||
|
|
||||||
|
def stop-niri [user: string, machine: string, source: string] {
|
||||||
|
match $source {
|
||||||
|
'remote session' => { stop-transient-session }
|
||||||
|
'user niri.service' => { ^systemctl --machine $machine --user stop niri.service }
|
||||||
|
'standalone process' => { ^pkill -TERM -u $user -x niri }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def stop-transient-session [] {
|
||||||
|
let loaded = ((^systemctl show greetd-sunshine.service -p LoadState --value | complete).stdout | str trim)
|
||||||
|
if $loaded == 'not-found' or $loaded == '' { return }
|
||||||
|
|
||||||
|
^systemctl stop greetd-sunshine.service
|
||||||
|
let _ = (^systemctl reset-failed greetd-sunshine.service | complete)
|
||||||
|
for _ in 1..50 {
|
||||||
|
let state = ((^systemctl show greetd-sunshine.service -p LoadState --value | complete).stdout | str trim)
|
||||||
|
if $state == 'not-found' or $state == '' { return }
|
||||||
|
sleep 100ms
|
||||||
|
}
|
||||||
|
error make { msg: 'Previous greetd-sunshine.service was stopped but not unloaded' }
|
||||||
|
}
|
||||||
|
|
||||||
|
def confirm-replacement [source: string] {
|
||||||
|
(input $'Niri is already running via ($source). Terminate it and start a remote session? [y/N] '
|
||||||
|
| str lowercase) == 'y'
|
||||||
|
}
|
||||||
|
|
||||||
|
def preflight [tty: string, command: string] {
|
||||||
|
let command_binary = ($command | split row ' ' | first)
|
||||||
|
if not ($command_binary | path exists) {
|
||||||
|
error make { msg: $'Wayland session command not found: ($command_binary)' }
|
||||||
|
}
|
||||||
|
if not ($tty | path exists) {
|
||||||
|
error make { msg: $'TTY not found: ($tty)' }
|
||||||
|
}
|
||||||
|
|
||||||
|
let exec_start = (^systemctl show greetd.service -p ExecStart --value)
|
||||||
|
let greetd = ($exec_start
|
||||||
|
| parse --regex 'path=(?<path>[^ ;]+)'
|
||||||
|
| get path
|
||||||
|
| first)
|
||||||
|
let system_config = ($exec_start
|
||||||
|
| parse --regex '--config (?<path>[^ ;]+)'
|
||||||
|
| get path
|
||||||
|
| first)
|
||||||
|
let default_session = (open $system_config | get default_session)
|
||||||
|
{ greetd: $greetd, default_session: $default_session }
|
||||||
|
}
|
||||||
|
|
||||||
|
def write-greetd-config [path: string, user: string, tty: string, command: string, default_session: record] {
|
||||||
|
{
|
||||||
|
general: { runfile: $'($path).run' }
|
||||||
|
initial_session: { command: $command, user: $user }
|
||||||
|
default_session: $default_session
|
||||||
|
terminal: { vt: ($tty | path basename | str replace 'tty' '') }
|
||||||
|
} | to toml | save --force $path
|
||||||
|
chmod 600 $path
|
||||||
|
}
|
||||||
|
|
||||||
|
def wait-ready [user: string, machine: string] {
|
||||||
|
for _ in 1..30 {
|
||||||
|
let greetd = ((^systemctl is-active greetd-sunshine.service | complete).stdout | str trim) == 'active'
|
||||||
|
let niri = ((^pgrep -u $user -x niri | complete).exit_code) == 0
|
||||||
|
let sunshine = ((^systemctl --machine $machine --user is-active sunshine.service | complete).stdout | str trim) == 'active'
|
||||||
|
if $greetd and $niri and $sunshine { return }
|
||||||
|
sleep 1sec
|
||||||
|
}
|
||||||
|
error make { msg: 'Remote session did not become ready within 30 seconds; inspect journalctl -u greetd-sunshine.service' }
|
||||||
|
}
|
||||||
|
|
||||||
|
def wait-niri [user: string] {
|
||||||
|
for _ in 1..30 {
|
||||||
|
let greetd = ((^systemctl is-active greetd-sunshine.service | complete).stdout | str trim) == 'active'
|
||||||
|
let niri = ((^pgrep -u $user -x niri | complete).exit_code) == 0
|
||||||
|
if $greetd and $niri { return }
|
||||||
|
sleep 1sec
|
||||||
|
}
|
||||||
|
error make { msg: 'Niri did not become ready within 30 seconds; inspect journalctl -u greetd-sunshine.service' }
|
||||||
|
}
|
||||||
|
|
||||||
|
def start-sunshine [machine: string] {
|
||||||
|
^systemctl --machine $machine --user reset-failed sunshine.service
|
||||||
|
^systemctl --machine $machine --user start sunshine.service
|
||||||
|
}
|
||||||
|
|
||||||
|
def main [
|
||||||
|
--user (-u): string = 'ryan'
|
||||||
|
--tty (-t): string = '/dev/tty1'
|
||||||
|
--command (-c): string = ''
|
||||||
|
--force (-f)
|
||||||
|
--check
|
||||||
|
] {
|
||||||
|
let session_user = ($env.SUDO_USER? | default $user)
|
||||||
|
let user_machine = (user-machine $session_user)
|
||||||
|
# niri-session starts niri.service through the user manager. Run the
|
||||||
|
# compositor directly so greetd owns this temporary session.
|
||||||
|
let command = if $command == '' { '/run/current-system/sw/bin/niri --session' } else { $command }
|
||||||
|
let runtime = (preflight $tty $command)
|
||||||
|
if $check {
|
||||||
|
print 'Remote session prerequisites are valid.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let source = (niri-source $session_user)
|
||||||
|
if $source != 'none' {
|
||||||
|
if not $force and not (confirm-replacement $source) {
|
||||||
|
print 'Aborted.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
^systemctl --machine $user_machine --user stop sunshine.service
|
||||||
|
stop-niri $session_user $user_machine $source
|
||||||
|
}
|
||||||
|
# A previous helper may have exited after Niri but left its greeter running.
|
||||||
|
stop-transient-session
|
||||||
|
|
||||||
|
let config = (mktemp --tmpdir-path /run greetd-sunshine.XXXXXX.toml)
|
||||||
|
write-greetd-config $config $session_user $tty $command $runtime.default_session
|
||||||
|
systemctl stop greetd.service
|
||||||
|
(^systemd-run
|
||||||
|
--unit greetd-sunshine
|
||||||
|
--collect
|
||||||
|
--property $'TTYPath=($tty)'
|
||||||
|
--property StandardInput=tty
|
||||||
|
--property StandardOutput=tty
|
||||||
|
--property StandardError=journal
|
||||||
|
--
|
||||||
|
$runtime.greetd
|
||||||
|
--config $config)
|
||||||
|
|
||||||
|
wait-niri $session_user
|
||||||
|
start-sunshine $user_machine
|
||||||
|
wait-ready $session_user $user_machine
|
||||||
|
print 'Remote Niri and Sunshine session is ready.'
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
loadsVirtualDisplay = true;
|
||||||
|
createsVirtualDisplay = true;
|
||||||
|
niriUsesIntelRenderer = true;
|
||||||
|
sunshineUserHasInputAccess = true;
|
||||||
|
sunshineStartsAfterNiri = true;
|
||||||
|
sunshineWaitsForNiriOutput = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
lib,
|
||||||
|
outputs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = outputs.nixosConfigurations.ai-niri.config;
|
||||||
|
niriHardware = builtins.readFile ../../../../hosts/idols-ai/niri-hardware.kdl;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
loadsVirtualDisplay = builtins.elem "vkms" cfg.boot.kernelModules;
|
||||||
|
createsVirtualDisplay = lib.hasInfix "options vkms create_default_dev=1" cfg.boot.extraModprobeConfig;
|
||||||
|
niriUsesIntelRenderer = lib.hasInfix "/dev/dri/by-path/pci-0000:00:02.0-render" niriHardware;
|
||||||
|
sunshineUserHasInputAccess = builtins.elem "input" cfg.users.users.ryan.extraGroups;
|
||||||
|
sunshineStartsAfterNiri = builtins.elem "niri.service" cfg.systemd.user.services.sunshine.after;
|
||||||
|
sunshineWaitsForNiriOutput = lib.hasInfix "niri msg outputs" cfg.systemd.user.services.sunshine.preStart;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user