25cf0167c1
- 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.
192 lines
4.5 KiB
Python
192 lines
4.5 KiB
Python
#!/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()
|