From 25cf0167c122552d0ffc99c80adfc32f2f9a326a Mon Sep 17 00:00:00 2001 From: OlivierChiasson Date: Fri, 10 Jul 2026 17:32:37 -0300 Subject: [PATCH] Enhance 14900k configuration with streaming display support and virtual EDID generation - Added a new `streaming-display.nix` module to configure a headless 4K virtual output for Sunshine and Moonlight. - Introduced a Python script `generate-virtual-edid.py` to create a virtual display EDID with 4K capabilities. - Updated `configuration.nix` to integrate the new streaming display module and enable related services. - Modified `displays.nix` to accommodate the new streaming display settings and ensure compatibility with existing GPU passthrough configurations. --- flake.lock | 169 +++++++++++----- modules/hosts/14900k/_private/displays.nix | 27 ++- .../14900k/_private/generate-virtual-edid.py | 191 ++++++++++++++++++ .../14900k/_private/streaming-display.nix | 126 ++++++++++++ modules/hosts/14900k/configuration.nix | 4 + 5 files changed, 458 insertions(+), 59 deletions(-) create mode 100644 modules/hosts/14900k/_private/generate-virtual-edid.py create mode 100644 modules/hosts/14900k/_private/streaming-display.nix diff --git a/flake.lock b/flake.lock index fb3a552..db0ad90 100644 --- a/flake.lock +++ b/flake.lock @@ -38,11 +38,11 @@ ] }, "locked": { - "lastModified": 1780452866, - "narHash": "sha256-Fq5cR/qGYcGwOOZf6sShtM+kljwecgntgcKXnIBrQDE=", + "lastModified": 1782389081, + "narHash": "sha256-60aWPXGfLy3fVDTgwd1noQ2W+gryGy0EnNB0jLjfsUY=", "ref": "refs/heads/main", - "rev": "344f4065e69455adbcdfb4db1cd8d07a73ae7140", - "revCount": 114, + "rev": "0feb09ad82ab1800ce5727bb94191deb500ad184", + "revCount": 123, "type": "git", "url": "https://git.chiasson.cloud/Olivier/cursor-nixos-flake" }, @@ -79,11 +79,11 @@ ] }, "locked": { - "lastModified": 1778679554, - "narHash": "sha256-zoPgnxIlDja91/4TmnCui+Fzc/xU/1jdJFu9bovtOk8=", + "lastModified": 1782185186, + "narHash": "sha256-URMC6ppMqe9WQOdH/oTeOqUcJggJGJaNOrhdzwSKyYo=", "owner": "AvengeMedia", "repo": "dgop", - "rev": "06574b54fa4878a93d8605962d50b13e9528a4ca", + "rev": "28558a7e79ab5c81b6d832fe48f40ea499866496", "type": "github" }, "original": { @@ -172,11 +172,11 @@ "dms-plugin-calculator": { "flake": false, "locked": { - "lastModified": 1778511624, - "narHash": "sha256-hiqrO8WkzmWGVlUrzxmffUZBs4t1QM2mMTBUxZqCIyU=", + "lastModified": 1782146268, + "narHash": "sha256-j8C62+sevr6b+akzVSAqUVysIhb6Vbr8jnWcTXeOtE8=", "owner": "rochacbruno", "repo": "DankCalculator", - "rev": "73073d051d08254633f28f89d2609344c8289813", + "rev": "1db5865419a40a33171a475855a59e0b8bf7187f", "type": "github" }, "original": { @@ -513,6 +513,28 @@ } }, "flake-parts_4": { + "inputs": { + "nixpkgs-lib": [ + "navi", + "tofunix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1736143030, + "narHash": "sha256-+hu54pAoLDEZT9pjHlqL9DNzWz0NbUn8NEAHP7PQPzU=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "b905f6fc23a9051a6e1b741e1438dbfc0634c6de", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_5": { "inputs": { "nixpkgs-lib": "nixpkgs-lib_3" }, @@ -530,7 +552,7 @@ "type": "github" } }, - "flake-parts_5": { + "flake-parts_6": { "inputs": { "nixpkgs-lib": [ "nur", @@ -553,7 +575,7 @@ }, "flake-utils": { "inputs": { - "systems": "systems" + "systems": "systems_2" }, "locked": { "lastModified": 1731533236, @@ -571,7 +593,7 @@ }, "flake-utils_2": { "inputs": { - "systems": "systems_3" + "systems": "systems_4" }, "locked": { "lastModified": 1731533236, @@ -594,11 +616,11 @@ ] }, "locked": { - "lastModified": 1780679734, - "narHash": "sha256-KmRNvpNOb7QEORa06bVgjW9kITcx0VhsI7w0vhmZyD8=", + "lastModified": 1782423922, + "narHash": "sha256-qPNd6lUohHP5gcJhqQ7rLV87RwIx0xYR2A4Frb9Zjc4=", "owner": "nix-community", "repo": "home-manager", - "rev": "b2b7db486e06e098711dc291bb25db82850e1d16", + "rev": "5d320ab301cfaaca7d32514f13815d19d109f5f4", "type": "github" }, "original": { @@ -615,11 +637,11 @@ ] }, "locked": { - "lastModified": 1780515920, - "narHash": "sha256-8KX2hEeOX6KP3hBBJJI8dGWVrzbOOf1rBPmg/GUG24U=", + "lastModified": 1781497404, + "narHash": "sha256-9GAF8sSsnkyCVCWkomXR0T+zdSxyUlfPt6neQidimdg=", "owner": "nix-community", "repo": "home-manager", - "rev": "4c5c1e8ba14f1c7475fa31ff11bc1c19cd220974", + "rev": "1285cd3d6882a9847f2d56ed5541b3350c8a6162", "type": "github" }, "original": { @@ -666,14 +688,15 @@ "nix-github-actions": "nix-github-actions", "nixos-anywhere": "nixos-anywhere", "nixpkgs": "nixpkgs", - "stable": "stable" + "stable": "stable", + "tofunix": "tofunix" }, "locked": { - "lastModified": 1777283737, - "narHash": "sha256-sVwTOYwxzUUqAEr5kjOZooJ/v8H2hcuZ42f17MgCqX8=", + "lastModified": 1781964794, + "narHash": "sha256-DpmkR7ifgSLj7890ayIsri3eioB7V8KhA6ys3ivXAPw=", "owner": "cafkafk", "repo": "navi", - "rev": "b060683e4ca2bd37a1c3bcb4e04abd2296190e9d", + "rev": "f6e15457cebefb3a8aa6e30e1961288676c9c7f1", "type": "github" }, "original": { @@ -744,18 +767,18 @@ "nixcord": { "inputs": { "flake-compat": "flake-compat", - "flake-parts": "flake-parts_4", + "flake-parts": "flake-parts_5", "nixpkgs": [ "nixpkgs" ], "nixpkgs-nixcord": "nixpkgs-nixcord" }, "locked": { - "lastModified": 1780751320, - "narHash": "sha256-3PlWrZkSwElqYHMnWi4d0A+GxlvCXk0JaMsTGghmQGE=", + "lastModified": 1782436508, + "narHash": "sha256-F91/VTVJmeERWwiJMHfEjkCEXWcNECSOiBsil3TlX9k=", "owner": "KaylorBen", "repo": "nixcord", - "rev": "2b70dbed679d2705696deb36f0778c9ef9f1ded2", + "rev": "9c530d99735c236db405f6a1ed58de0872ff9a19", "type": "github" }, "original": { @@ -964,11 +987,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1780243769, - "narHash": "sha256-x5UQuRsH3MqI0U9afaXSNqzTPSeZlRLvFAav2Ux1pNw=", + "lastModified": 1781577229, + "narHash": "sha256-lrp67w8AulE9Ks53n27I45ADSzbOCn4H+CNW1Ck8B+8=", "owner": "nixos", "repo": "nixpkgs", - "rev": "331800de5053fcebacf6813adb5db9c9dca22a0c", + "rev": "567a49d1913ce81ac6e9582e3553dd90a955875f", "type": "github" }, "original": { @@ -1015,17 +1038,17 @@ }, "nur": { "inputs": { - "flake-parts": "flake-parts_5", + "flake-parts": "flake-parts_6", "nixpkgs": [ "nixpkgs" ] }, "locked": { - "lastModified": 1780771425, - "narHash": "sha256-qxX2a/MnwrIkMUZ7nLafJkDvaDaLLsPRC0NK2VXW+sc=", + "lastModified": 1782441318, + "narHash": "sha256-e+EWubnfN/CqogQ3qFL7i5Yg8ST7hg/19WS+BAOYFb8=", "owner": "nix-community", "repo": "NUR", - "rev": "a144829f0faf14576d4faa88a42cd62bebb7428b", + "rev": "548388638a8f389437bc81e14122667244a36912", "type": "github" }, "original": { @@ -1066,11 +1089,11 @@ ] }, "locked": { - "lastModified": 1779906441, - "narHash": "sha256-py8KJJMi4awjZHi5FWBPYfbRPvk3Rg9SeFkPJydsG2E=", + "lastModified": 1780803198, + "narHash": "sha256-H7wxhe9HJKUvPSDuBKHh/COx+YTZDaQlb6g8b+ZGtE8=", "ref": "refs/heads/main", - "rev": "339a9ba1ef79dc9976af77f1fea0302de8a696d0", - "revCount": 16, + "rev": "46fe1e10e650e078799d0a35ffc05a5bba0733ed", + "revCount": 17, "type": "git", "url": "https://git.chiasson.cloud/Olivier/Personal-Website" }, @@ -1155,11 +1178,11 @@ ] }, "locked": { - "lastModified": 1780547341, - "narHash": "sha256-Gq8KNx5A7hBB3uGJaj6eQfLDIz5YdLu92gqBcvHvoUo=", + "lastModified": 1782165805, + "narHash": "sha256-478kKQBvK6SYTOdN2h9jhKJv94nbXRbFMfuL1WshErg=", "owner": "Mic92", "repo": "sops-nix", - "rev": "9ed65852b6257fbeae4355bc24ecfea307ca759a", + "rev": "56b24064fdcaedca53553b1a6d607fd23b613a24", "type": "github" }, "original": { @@ -1173,14 +1196,14 @@ "nixpkgs": [ "nixpkgs" ], - "systems": "systems_2" + "systems": "systems_3" }, "locked": { - "lastModified": 1780422259, - "narHash": "sha256-dWGk4SEdI189kQW5cE4Uo1Mc+P+kQEdgMcyMgTtmQOA=", + "lastModified": 1782031037, + "narHash": "sha256-a7oWSyS7SN81UOqVt481yIEMDsMpaJ7GNdV6Eaz5Yqg=", "owner": "Gerg-L", "repo": "spicetify-nix", - "rev": "8414bbf2fcc7bc0a22c675e498e3c7365c1aec0a", + "rev": "9cb27462cfd20edac174353f1e95bc03aa888863", "type": "github" }, "original": { @@ -1271,6 +1294,21 @@ "type": "github" } }, + "systems_4": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, "t2fanrd": { "inputs": { "nixpkgs": [ @@ -1294,11 +1332,11 @@ "t2linux-patches": { "flake": false, "locked": { - "lastModified": 1779914035, - "narHash": "sha256-VsHuI2CbQ8gFplW+51gUJvCqo1Ts10Ueks9aTtkAOiw=", + "lastModified": 1782419564, + "narHash": "sha256-Rz6BsjpS9SETgcSkTz/QYuncR5CB0TM61SCzTiIcRI0=", "owner": "t2linux", "repo": "linux-t2-patches", - "rev": "7ee7d19c38e5df31a386b2a0c35ca8f064003960", + "rev": "ff59395cc61769dc255b5eab6faac27e8ce441f7", "type": "github" }, "original": { @@ -1307,6 +1345,29 @@ "type": "github" } }, + "tofunix": { + "inputs": { + "flake-parts": "flake-parts_4", + "nixpkgs": [ + "navi", + "nixpkgs" + ], + "systems": "systems" + }, + "locked": { + "lastModified": 1769620829, + "narHash": "sha256-OGZn7OKNm3wuLvtynXAk5C1GUd0jKHyfirhkbdbMIgo=", + "owner": "cafkafk", + "repo": "tofunix", + "rev": "472fe73ec92e3c21c470361d7531c67c4ced2861", + "type": "github" + }, + "original": { + "owner": "cafkafk", + "repo": "tofunix", + "type": "github" + } + }, "treefmt-nix": { "inputs": { "nixpkgs": [ @@ -1350,11 +1411,11 @@ "nixpkgs": "nixpkgs_3" }, "locked": { - "lastModified": 1780661205, - "narHash": "sha256-3F5DixT3Gk91lBI9E+TGMm0ko5HrRbDiL23di16TJGA=", + "lastModified": 1782135443, + "narHash": "sha256-vAmbArdCyjqpVW+37aCy/PMBOLIqukUXLQuEKLwUhA4=", "owner": "BirdeeHub", "repo": "nix-wrapper-modules", - "rev": "8dd304c3582ddd339217e1cc5fb53f50acb63c2d", + "rev": "6e7f66fa2cdf4d63162580b438f7fcf87c28a46f", "type": "github" }, "original": { @@ -1371,11 +1432,11 @@ ] }, "locked": { - "lastModified": 1780567926, - "narHash": "sha256-LVaiAnBwgr2YotaIlrcwCgmbwHsE2ccegRztLjur/d4=", + "lastModified": 1782144240, + "narHash": "sha256-RgCWSv7AJZCwPhCzz+J0lvwp1WBz9ouvCnnlmvu0xfw=", "owner": "0xc000022070", "repo": "zen-browser-flake", - "rev": "eea9ae34eb9011aee9b8ce8ee2bc2dd111ee8285", + "rev": "d1693556428967f8b4eef128feb090421ddcaf15", "type": "github" }, "original": { diff --git a/modules/hosts/14900k/_private/displays.nix b/modules/hosts/14900k/_private/displays.nix index 22529cc..b0f4fcd 100644 --- a/modules/hosts/14900k/_private/displays.nix +++ b/modules/hosts/14900k/_private/displays.nix @@ -1,5 +1,5 @@ # Monitor layout for 14900k. -# NVIDIA (default): DP-2 ultrawide, HDMI-A-3 + DP-4 side/top stack. +# NVIDIA (default): DP-3 ultrawide, HDMI-A-3 + DP-4 side/top stack; DP-2 virtual 4K when streamingDisplay enabled. # Intel iGPU (gpu passthru): DP-1 ultrawide, HDMI-A-2 Samsung to the left. # Niri: `extraSettings` (KDL via `extraConfig`, other wrapper-modules keys) + `extraBinds` (merged keybinds). # Hyprland: `chiasson.desktop.hyprland.settings` (merged in HM when `chiasson.desktop.hyprland.enable`). @@ -8,9 +8,20 @@ { config, lib, ... }: let gpuPassthru = config.chiasson.system.gpuPassthru.enable; + streamingDisplay = config.chiasson.system.streamingDisplay.enable or false; + streamingCfg = config.chiasson.system.streamingDisplay or { }; + + virtualOutputNiri = + lib.optionalString streamingDisplay '' + output "${streamingCfg.connector}" { + mode "${streamingCfg.mode}" + scale ${toString (streamingCfg.niriScale or 1.0)} + position ${streamingCfg.niriPosition} + } + ''; niriOutputs = - if gpuPassthru then + (if gpuPassthru then '' output "DP-1" { mode "2560x1080@144" @@ -42,10 +53,15 @@ let scale 1.0 position x=0 y=-1080 } - ''; + '') + + virtualOutputNiri; + + virtualOutputHyprland = + lib.optionalString streamingDisplay + "${streamingCfg.connector}, ${streamingCfg.mode}, 7680x0, ${toString (streamingCfg.niriScale or 1.0)}"; hyprlandMonitors = - if gpuPassthru then + (if gpuPassthru then [ "DP-1, 2560x1080@144, 0x0, 1" "HDMI-A-2, 1920x1080@60, -1920x0, 1" @@ -55,7 +71,8 @@ let "DP-3, 2560x1080@144, 0x0, 1" "DP-4, 1920x1080@144, 0x-1080, 1" "HDMI-A-3, 1920x1080@60, -1920x0, 1" - ]; + ]) + ++ lib.optional streamingDisplay virtualOutputHyprland; hyprlandWorkspaces = if gpuPassthru then diff --git a/modules/hosts/14900k/_private/generate-virtual-edid.py b/modules/hosts/14900k/_private/generate-virtual-edid.py new file mode 100644 index 0000000..ded6c7c --- /dev/null +++ b/modules/hosts/14900k/_private/generate-virtual-edid.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""Generate a virtual-display EDID with 4K CTA VICs (trimmed from EnriqueWood POC).""" + +import math +import sys + + +def cvt_rb(w, h, refresh): + hblank, hfront, hsync = 160, 48, 32 + htotal = w + hblank + vfront, vsync = 3, 8 + rb_min_vblank = 460e-6 + vblank = math.ceil(rb_min_vblank * refresh * h / (1.0 - rb_min_vblank * refresh)) + vblank = max(vblank, vfront + vsync + 6) + vtotal = h + vblank + pixclk_khz = int(round(htotal * vtotal * refresh / 10000.0)) * 10 + return dict( + w=w, + h=h, + refresh=refresh, + htotal=htotal, + vtotal=vtotal, + hblank=hblank, + hfront=hfront, + hsync=hsync, + vblank=vblank, + vfront=vfront, + vsync=vsync, + pixclk_khz=pixclk_khz, + ) + + +def make_dtd(t, hmm=600, vmm=340): + w, h = t["w"], t["h"] + hblank, vblank = t["hblank"], t["vblank"] + hfront, hsync = t["hfront"], t["hsync"] + vfront, vsync = t["vfront"], t["vsync"] + pc = t["pixclk_khz"] // 10 + d = bytearray(18) + d[0] = pc & 0xFF + d[1] = (pc >> 8) & 0xFF + d[2] = w & 0xFF + d[3] = hblank & 0xFF + d[4] = ((w >> 4) & 0xF0) | ((hblank >> 8) & 0x0F) + d[5] = h & 0xFF + d[6] = vblank & 0xFF + d[7] = ((h >> 4) & 0xF0) | ((vblank >> 8) & 0x0F) + d[8] = hfront & 0xFF + d[9] = hsync & 0xFF + d[10] = ((vfront & 0x0F) << 4) | (vsync & 0x0F) + d[11] = ( + (((hfront >> 8) & 3) << 6) + | (((hsync >> 8) & 3) << 4) + | (((vfront >> 4) & 3) << 2) + | ((vsync >> 4) & 3) + ) + d[12] = hmm & 0xFF + d[13] = vmm & 0xFF + d[14] = ((hmm >> 4) & 0xF0) | ((vmm >> 8) & 0x0F) + d[17] = 0x1E + return bytes(d) + + +def make_name(name): + d = bytearray(18) + d[3] = 0xFC + nb = name.encode("ascii")[:13] + d[5 : 5 + len(nb)] = nb + pos = 5 + len(nb) + if pos < 18: + d[pos] = 0x0A + pos += 1 + for i in range(pos, 18): + d[i] = 0x20 + return bytes(d) + + +def make_range(min_v, max_v, min_h, max_h, max_pixclk_mhz): + d = bytearray(18) + d[3] = 0xFD + flags = 0 + mv, mh = max_v, max_h + if mh > 255: + flags |= 0x08 + mh -= 255 + if mv > 255: + flags |= 0x02 + mv -= 255 + d[4] = flags + d[5] = min_v + d[6] = mv + d[7] = min_h + d[8] = mh + d[9] = max_pixclk_mhz // 10 + d[10] = 0x01 + for i in range(11, 18): + d[i] = 0x0A if i == 11 else 0x20 + return bytes(d) + + +def checksum(block): + return (256 - (sum(block[:-1]) % 256)) % 256 + + +VICS = [97, 118, 96, 95, 63, 16, 4] + + +def build_base(base_dtds, name, max_pixclk_mhz): + b = bytearray(128) + b[0:8] = b"\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00" + b[8:10] = b"\x50\x74" + b[10:12] = b"\x70\x02" + b[16] = 22 + b[17] = 34 + b[18] = 1 + b[19] = 4 + b[20] = 0xA5 + b[21] = 60 + b[22] = 34 + b[23] = 120 + b[24] = 0x2E + b[25:35] = bytes([0x35, 0x85, 0xA6, 0x56, 0x48, 0x9A, 0x24, 0x12, 0x50, 0x54]) + for i in range(8): + b[38 + i * 2] = 0x01 + b[39 + i * 2] = 0x01 + b[54:72] = make_dtd(base_dtds[0]) + b[72:90] = make_dtd(base_dtds[1]) + b[90:108] = make_range(24, 240, 30, 510, max_pixclk_mhz) + b[108:126] = make_name(name) + b[126] = 1 + b[127] = checksum(b) + return b + + +def build_cta_primary(dtds): + b = bytearray(128) + b[0] = 0x02 + b[1] = 0x03 + pos = 4 + b[pos] = (2 << 5) | len(VICS) + for i, v in enumerate(VICS): + b[pos + 1 + i] = v + pos += 1 + len(VICS) + b[pos] = (1 << 5) | 3 + b[pos + 1], b[pos + 2], b[pos + 3] = 0x17, 0x7F, 0x07 + pos += 4 + b[pos] = (4 << 5) | 3 + b[pos + 1] = 0x4F + pos += 4 + b[2] = pos + b[3] = 0x40 + used, p = [], pos + for t in dtds: + if p + 18 > 127: + break + b[p : p + 18] = make_dtd(t) + used.append(t) + p += 18 + b[127] = checksum(b) + return b, used + + +def build(custom, name, max_pixclk_mhz=1200): + if len(custom) < 2: + custom = custom + custom[:1] * (2 - len(custom)) + base = build_base(custom[0:2], name, max_pixclk_mhz) + rest = custom[2:] + cta1, _used = build_cta_primary(rest) + base[126] = 1 + base[127] = checksum(base) + return bytes(base + cta1) + + +def main(): + output = sys.argv[1] + custom = [ + cvt_rb(3840, 2160, 60), + cvt_rb(2560, 1440, 60), + cvt_rb(1920, 1080, 60), + ] + edid = build(custom, "SUN-4K") + with open(output, "wb") as f: + f.write(edid) + assert len(edid) % 128 == 0 + for i in range(len(edid) // 128): + block = edid[i * 128 : (i + 1) * 128] + assert sum(block) % 256 == 0 + + +if __name__ == "__main__": + main() diff --git a/modules/hosts/14900k/_private/streaming-display.nix b/modules/hosts/14900k/_private/streaming-display.nix new file mode 100644 index 0000000..e968b18 --- /dev/null +++ b/modules/hosts/14900k/_private/streaming-display.nix @@ -0,0 +1,126 @@ +# Headless 4K virtual output for Sunshine → Moonlight on a client TV. +# +# NVIDIA proprietary driver: both kernel params are required — +# video=:e force-enables the connector +# drm.edid_firmware=:… loads modes from the EDID blob +# +# Pick a connector that is disconnected on the host (check after boot): +# grep -H . /sys/class/drm/card*-*/status +# +# Reboot after changing connector or EDID. 4K@60 is realistic on RTX 2070; +# 4K@120 on a sink-less connector needs driver patches (see NVIDIA issue #1184). +{ config, lib, pkgs, ... }: +let + cfg = config.chiasson.system.streamingDisplay; + + edidFirmware = pkgs.runCommand "sunshine-virtual-4k-edid" { + nativeBuildInputs = [ pkgs.python3 ]; + } '' + ${pkgs.python3}/bin/python3 ${./generate-virtual-edid.py} $TMPDIR/${cfg.edidFileName} + mkdir -p $out/lib/firmware/edid + cp $TMPDIR/${cfg.edidFileName} $out/lib/firmware/edid/${cfg.edidFileName} + ''; + + # Sunshine 2025.924 wlr capture only accepts a numeric monitor index (not "DP-2"). + # If the index drifts after hotplug, check: + # journalctl --user -u sunshine -b | rg 'Monitor [0-9]+ is' + streamDisplayUp = pkgs.writeShellScriptBin "stream-display-up" '' + ${pkgs.niri}/bin/niri msg output ${cfg.connector} scale ${toString cfg.niriScale} + ${pkgs.niri}/bin/niri msg action focus-monitor ${cfg.connector} + ${pkgs.niri}/bin/niri msg action move-window-to-monitor ${cfg.connector} || true + ''; + + streamDisplayDown = pkgs.writeShellScriptBin "stream-display-down" '' + ${pkgs.niri}/bin/niri msg action focus-monitor ${cfg.focusMonitorOnEnd} + ''; +in +{ + options.chiasson.system.streamingDisplay = { + enable = lib.mkEnableOption '' + Force-enable a spare NVIDIA DRM connector with a 4K EDID for Sunshine/Moonlight. + Requires reboot. Pair with `chiasson.desktop.niri` output config in `displays.nix`. + ''; + + connector = lib.mkOption { + type = lib.types.str; + default = "DP-2"; + example = "HDMI-A-1"; + description = '' + DRM connector name (e.g. `DP-2`, `HDMI-A-1`). Must be unused on the host — + 14900k desk uses DP-3, DP-4, and HDMI-A-3 by default. + ''; + }; + + edidFileName = lib.mkOption { + type = lib.types.str; + default = "virtual-4k.bin"; + description = "Filename under `/lib/firmware/edid/` referenced by `drm.edid_firmware`."; + }; + + mode = lib.mkOption { + type = lib.types.str; + default = "3840x2160@60"; + description = "Mode Niri should use on the virtual output (must match EDID)."; + }; + + niriPosition = lib.mkOption { + type = lib.types.str; + default = "x=7680 y=0"; + description = '' + Niri `position` for the virtual panel — keep it clear of desk monitors + so streamed windows do not overlap your ultrawide layout. + ''; + }; + + niriScale = lib.mkOption { + type = lib.types.float; + default = 1.5; + example = 1.5; + description = '' + Niri `scale` on the virtual panel. Values above 1.0 enlarge UI and text + on the 4K framebuffer (helpful when viewing on a TV from the couch). + ''; + }; + + focusMonitorOnEnd = lib.mkOption { + type = lib.types.str; + default = "DP-3"; + description = "Niri monitor to refocus when a Moonlight session ends."; + }; + + sunshineOutputIndex = lib.mkOption { + type = lib.types.str; + default = "3"; + example = "3"; + description = '' + Wayland/wlr monitor index for Sunshine `output_name`. Sunshine 2025.924 ignores + connector strings like `DP-2` and falls back to monitor 0 (your primary). + Find the index with: + `journalctl --user -u sunshine -b | rg "Monitor [0-9]+ is ${cfg.connector}"` + ''; + }; + }; + + config = lib.mkIf cfg.enable (lib.mkMerge [ + { + hardware.firmware = [ edidFirmware ]; + + boot.kernelParams = [ + "video=${cfg.connector}:e" + "drm.edid_firmware=${cfg.connector}:edid/${cfg.edidFileName}" + ]; + + environment.systemPackages = [ + streamDisplayUp + streamDisplayDown + ]; + } + (lib.mkIf config.chiasson.system.gaming.sunshine.enable { + services.sunshine.settings = { + capture = "wlr"; + output_name = cfg.sunshineOutputIndex; + global_prep_cmd = "[{\"do\":\"${streamDisplayUp}/bin/stream-display-up\",\"undo\":\"${streamDisplayDown}/bin/stream-display-down\"}]"; + }; + }) + ]); +} diff --git a/modules/hosts/14900k/configuration.nix b/modules/hosts/14900k/configuration.nix index 40e2354..26b63c9 100644 --- a/modules/hosts/14900k/configuration.nix +++ b/modules/hosts/14900k/configuration.nix @@ -76,6 +76,10 @@ gaming.sunshine.enable = true; gaming.sunshine.cudaSupport = true; + streamingDisplay.enable = true; + # Spare NVIDIA port — verify with: grep -H . /sys/class/drm/card*-*/status + streamingDisplay.connector = "DP-2"; + monitorInput.enable = true; flatpak.enable = true;