mirror of
https://github.com/theupriser/steamify-cachyos.git
synced 2026-10-03 17:41:58 +02:00
feat: HDMI refresh boost (Steam Machine, pinned kernel)
The pinned 7.1.6 kernel doesn't read the extra EDID block (HDMI Forum EEODB) where monitors list their fast modes, and does no HDMI 2.1, so HDMI displays often stay at 60 Hz. The new menu item reads the display's EDID over DDC, calculates the highest rate that fits HDMI 2.0 at the desktop resolution (rounded down to ten, plus the hundred below as a safe option), tests each step live with a confirmation, and makes the confirmed ones permanent through drm.edid_firmware (Limine, sdboot-manage or GRUB).
This commit is contained in:
1 parent
224aede84b
commit
4dffcef60f
7 files changed
+514
-5
No files matched your search
@@ -171,6 +171,14 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
|
|||||||
The start script closes its window after a 10-second countdown on success
|
The start script closes its window after a 10-second countdown on success
|
||||||
and waits for Enter after an error; it uses `pipefail`, or a failed
|
and waits for Enter after an error; it uses `pipefail`, or a failed
|
||||||
download would run an empty script and count as success.
|
download would run an empty script and count as success.
|
||||||
|
- HDMI refresh boost (`hdmi`, `lib/hdmi-refresh.sh`): only on Fremont with
|
||||||
|
the pinned kernel (stays visible while on). It needs someone at the
|
||||||
|
screen: the graphical app's backend (`BACKEND=true`) must refuse it, and every step needs a "y" within 15 s
|
||||||
|
(`WIZARD_HDMI_CONFIRM_SECONDS` for scripted tests). debugfs is root-only
|
||||||
|
(glob it under sudo), and `edid_override` takes exactly `reset` with no
|
||||||
|
newline. Build the EDID from the DDC read, not from sysfs: a live override
|
||||||
|
replaces the kernel's copy. The kernel command-line value holds a path, so
|
||||||
|
`sed` uses `|` as its delimiter.
|
||||||
- `Relogin=true` means a gamescope that fails to start is relaunched in a
|
- `Relogin=true` means a gamescope that fails to start is relaunched in a
|
||||||
tight loop; keep that in mind when changing session handling.
|
tight loop; keep that in mind when changing session handling.
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,17 @@ All notable changes, per version and per commit. Versions follow
|
|||||||
`steamify.sh`. Versions before 0.7.0 were numbered afterwards,
|
`steamify.sh`. Versions before 0.7.0 were numbered afterwards,
|
||||||
one per merged pull request.
|
one per merged pull request.
|
||||||
|
|
||||||
|
## 1.2.0 - 2026-09-25
|
||||||
|
|
||||||
|
- **feat: HDMI refresh boost (Steam Machine, pinned kernel)**
|
||||||
|
- The pinned 7.1.6 kernel doesn't read the extra EDID block where monitors
|
||||||
|
list their fast modes, and does no HDMI 2.1, so HDMI displays often stay
|
||||||
|
at 60 Hz. The new menu item reads the display's EDID over DDC,
|
||||||
|
calculates the highest rate that fits HDMI 2.0 at the desktop resolution
|
||||||
|
(rounded down to ten, plus the hundred below as a safe option), tests
|
||||||
|
each step live with a confirmation, and makes the confirmed ones
|
||||||
|
permanent through `drm.edid_firmware` (Limine, sdboot-manage or GRUB).
|
||||||
|
|
||||||
## 1.1.4 - 2026-09-25
|
## 1.1.4 - 2026-09-25
|
||||||
|
|
||||||
- **fix: No lock screen in single user mode before a restart**
|
- **fix: No lock screen in single user mode before a restart**
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ What Steamify can do on a Valve Steam Machine:
|
|||||||
[release](https://github.com/theupriser/steamify-cachyos/releases/tag/kernel-7.1.6-1),
|
[release](https://github.com/theupriser/steamify-cachyos/releases/tag/kernel-7.1.6-1),
|
||||||
checked against CachyOS's signature
|
checked against CachyOS's signature
|
||||||
([details](TECHNICAL.md#kernel-pin-steam-machine)).
|
([details](TECHNICAL.md#kernel-pin-steam-machine)).
|
||||||
|
- **HDMI refresh boost** - only with the pinned kernel, never ticked by
|
||||||
|
default: gets the highest refresh rate your HDMI display runs at the
|
||||||
|
desktop resolution (e.g. 110 Hz instead of 60 on a 3440x1440 monitor).
|
||||||
|
Each step is shown and you confirm it; no answer within 15 s switches
|
||||||
|
back. For more (e.g. 175 Hz), use DisplayPort
|
||||||
|
([details](TECHNICAL.md#hdmi-refresh-boost-steam-machine)).
|
||||||
- **Update BIOS** - installs Valve's newest Steam Machine BIOS. Never ticked
|
- **Update BIOS** - installs Valve's newest Steam Machine BIOS. Never ticked
|
||||||
by default, at your own risk, and only after two warnings
|
by default, at your own risk, and only after two warnings
|
||||||
([details](TECHNICAL.md#bios-updates-steam-machine)).
|
([details](TECHNICAL.md#bios-updates-steam-machine)).
|
||||||
|
|||||||
@@ -229,6 +229,48 @@ into the kernel directory yourself.
|
|||||||
Unticking it removes the pin and runs `sudo pacman -Syu`, which brings the
|
Unticking it removes the pin and runs `sudo pacman -Syu`, which brings the
|
||||||
kernel back to CachyOS's current version; the files stay for next time.
|
kernel back to CachyOS's current version; the files stay for next time.
|
||||||
|
|
||||||
|
## HDMI refresh boost (Steam Machine)
|
||||||
|
|
||||||
|
With the pinned kernel (7.1.6), HDMI displays often stay at 60 Hz. Two
|
||||||
|
reasons:
|
||||||
|
|
||||||
|
- Monitors list their fast modes in an extra EDID block, announced by the
|
||||||
|
HDMI Forum EEODB data block. 7.1.6 only reads the first extension block,
|
||||||
|
so it never sees them. Newer kernels do.
|
||||||
|
- Their fastest modes need HDMI 2.1 (FRL). 7.1.6's amdgpu only does HDMI
|
||||||
|
2.0 (TMDS, at most 600 MHz), but a mode with the display's own shortest
|
||||||
|
blanking at a slightly lower rate often fits.
|
||||||
|
|
||||||
|
The menu item (only on a Steam Machine with the pinned kernel, never
|
||||||
|
ticked by default, run from the desktop in Konsole):
|
||||||
|
|
||||||
|
1. Takes the desktop resolution from KDE (`kscreen-doctor -j`) and reads the
|
||||||
|
display's complete EDID over DDC (`i2ctransfer`, segment pointer 0x30).
|
||||||
|
A live EDID left by an earlier test is cleared first.
|
||||||
|
2. Calculates the highest rate that fits: the display's TMDS limit (HDMI
|
||||||
|
Forum VSDB, capped at amdgpu's 600 MHz), its shortest blanking at that
|
||||||
|
resolution and its maximum refresh (range limits, VRR maximum). Steps:
|
||||||
|
that rate rounded down to ten, and the hundred below it as a safe option.
|
||||||
|
Rates the display already lists, or that aren't faster than what works
|
||||||
|
now, are left out.
|
||||||
|
3. Builds the EDID: all of the display's blocks, the block count and EEODB
|
||||||
|
fixed, plus a DisplayID block with the steps.
|
||||||
|
4. Loads it live (debugfs `edid_override`, `trigger_hotplug`) and switches
|
||||||
|
to each step, lowest first. Each one needs a "y" within 15 s
|
||||||
|
(`WIZARD_HDMI_CONFIRM_SECONDS` for tests); anything else switches back
|
||||||
|
and stops.
|
||||||
|
5. Makes it permanent with only the confirmed steps:
|
||||||
|
`/usr/lib/firmware/edid/steamify-<connector>.bin`, in the initramfs via
|
||||||
|
`/etc/mkinitcpio.conf.d/90-steamify-edid.conf` (amdgpu loads from there),
|
||||||
|
and `drm.edid_firmware=<connector>:edid/steamify-<connector>.bin` on the
|
||||||
|
kernel command line: `/etc/default/limine` (`limine-mkinitcpio`),
|
||||||
|
`/etc/sdboot-manage.conf` or `/etc/default/grub`.
|
||||||
|
|
||||||
|
The display's manufacturer, model and serial are stored; re-applying with
|
||||||
|
another display connected tests that one instead. Turning it off removes
|
||||||
|
the files and the parameter and rebuilds. Untick it before removing the
|
||||||
|
kernel pin: newer kernels read the EDID themselves and can do HDMI 2.1.
|
||||||
|
|
||||||
## BIOS updates (Steam Machine)
|
## BIOS updates (Steam Machine)
|
||||||
|
|
||||||
The **Update BIOS** item is only shown on a Steam Machine and is never ticked
|
The **Update BIOS** item is only shown on a Steam Machine and is never ticked
|
||||||
@@ -298,6 +340,7 @@ immediately, which can turn into a loop - see
|
|||||||
| `lib/bios.sh` | Update BIOS (Steam Machine, opt-in): current/newest version, double confirmation, fwupd |
|
| `lib/bios.sh` | Update BIOS (Steam Machine, opt-in): current/newest version, double confirmation, fwupd |
|
||||||
| `lib/cec.sh` | HDMI-CEC: Valve's `cecd` and friends from its `holo` repository |
|
| `lib/cec.sh` | HDMI-CEC: Valve's `cecd` and friends from its `holo` repository |
|
||||||
| `lib/steam-machine.sh` | Steam Machine support: LED driver, LED access, steamos-manager; kernel pin |
|
| `lib/steam-machine.sh` | Steam Machine support: LED driver, LED access, steamos-manager; kernel pin |
|
||||||
|
| `lib/hdmi-refresh.sh` | HDMI refresh boost (Steam Machine, pinned kernel): EDID over DDC, calculated steps, live test, `drm.edid_firmware` |
|
||||||
| `.github/tools/bundle.sh` | Builds the single-file version (`dist/steamify.sh`) |
|
| `.github/tools/bundle.sh` | Builds the single-file version (`dist/steamify.sh`) |
|
||||||
| `.github/workflows/bundle.yml` | Builds and checks it on every push; publishes it on `main` |
|
| `.github/workflows/bundle.yml` | Builds and checks it on every push; publishes it on `main` |
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,438 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# "HDMI refresh boost" menu item, Steam Machine (Fremont) only: finds the
|
||||||
|
# highest refresh rate the display runs at the desktop resolution over HDMI,
|
||||||
|
# tests it live and makes it permanent with an EDID override
|
||||||
|
# (drm.edid_firmware). Two things keep displays below what they can do:
|
||||||
|
# - Monitors put their fast modes in an extra EDID block (HDMI Forum EEODB);
|
||||||
|
# the pinned kernel (7.1.6) only reads the first extension. Newer kernels
|
||||||
|
# do, and do HDMI 2.1, so it's only offered with the pinned kernel.
|
||||||
|
# - Their fastest modes need HDMI 2.1 (FRL), which amdgpu doesn't do on
|
||||||
|
# every kernel; a mode with the display's shortest blanking at a slightly
|
||||||
|
# lower rate often still fits HDMI 2.0's TMDS limit (600 MHz).
|
||||||
|
# What a display claims isn't what it accepts: every step is shown and must
|
||||||
|
# be confirmed, and no answer within 15 s switches back (the screen may be
|
||||||
|
# black). Sourced by steamify.sh; not meant to be run on its own.
|
||||||
|
|
||||||
|
HDMI_FW_DIR=/usr/lib/firmware/edid
|
||||||
|
HDMI_INITRAMFS_CONF=/etc/mkinitcpio.conf.d/90-steamify-edid.conf
|
||||||
|
# No answer within this time switches back; longer for scripted tests.
|
||||||
|
HDMI_CONFIRM_SECONDS="${WIZARD_HDMI_CONFIRM_SECONDS:-15}"
|
||||||
|
# amdgpu's HDMI 2.0 limit; the display's own limit (from its EDID) may be lower.
|
||||||
|
HDMI_TMDS_MAX_KHZ=600000
|
||||||
|
|
||||||
|
hdmi_connectors() {
|
||||||
|
# Connected HDMI outputs, as "<card>-<connector>" (e.g. card1-HDMI-A-1).
|
||||||
|
local d
|
||||||
|
for d in /sys/class/drm/card*-HDMI-A-*; do
|
||||||
|
[[ "$(cat "$d/status" 2>/dev/null)" == connected ]] && basename "$d"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
hdmi_boot_file() {
|
||||||
|
# The file holding the kernel command line, per boot loader.
|
||||||
|
if [[ -f /etc/default/limine ]]; then echo /etc/default/limine
|
||||||
|
elif [[ -f /etc/sdboot-manage.conf ]] && command -v sdboot-manage >/dev/null; then echo /etc/sdboot-manage.conf
|
||||||
|
elif [[ -f /etc/default/grub ]]; then echo /etc/default/grub
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
hdmi_cmdline_param() {
|
||||||
|
# The drm.edid_firmware= value we set, or nothing.
|
||||||
|
local f; f="$(hdmi_boot_file)"
|
||||||
|
[[ -n "$f" ]] && grep -oE 'drm\.edid_firmware=[^ "]*steamify-[^ "]*' "$f" 2>/dev/null | head -n 1
|
||||||
|
}
|
||||||
|
|
||||||
|
hdmi_available() {
|
||||||
|
# Stays available while on, so it can be turned off after the pin is gone.
|
||||||
|
detect_valve_fremont || return 1
|
||||||
|
hdmi_status || { pinned_kernel_installed && [[ -n "$(hdmi_connectors)" && -n "$(hdmi_boot_file)" ]]; }
|
||||||
|
}
|
||||||
|
|
||||||
|
hdmi_status() { [[ -n "$(hdmi_cmdline_param)" ]] && compgen -G "$HDMI_FW_DIR/steamify-*.bin" >/dev/null; }
|
||||||
|
|
||||||
|
hdmi_edid_tool() {
|
||||||
|
# hdmi_edid_tool <command> ...: EDID parsing and building (python, as
|
||||||
|
# it's byte work). Commands:
|
||||||
|
# blocks <edid> number of blocks the display says it has
|
||||||
|
# id <edid> vendor/product/serial, to recognise it
|
||||||
|
# plan <edid> <w> <h> <cur_hz> "<limit_hz> <best> <step>..." for w x h
|
||||||
|
# build <in> <out> <w> <h> <hz>... full EDID plus a DisplayID block
|
||||||
|
# with those rates at the shortest blanking
|
||||||
|
python3 - "$HDMI_TMDS_MAX_KHZ" "$@" <<'PY'
|
||||||
|
import sys
|
||||||
|
|
||||||
|
src_khz = int(sys.argv[1]); cmd = sys.argv[2]; args = sys.argv[3:]
|
||||||
|
def load(p): return bytearray(open(p, 'rb').read())
|
||||||
|
def le(b): return int.from_bytes(b, 'little')
|
||||||
|
|
||||||
|
def cta_blocks(e):
|
||||||
|
for i in range(128, len(e), 128):
|
||||||
|
if e[i] == 0x02: yield i
|
||||||
|
|
||||||
|
def data_blocks(e, i):
|
||||||
|
# CTA data block collection: (tag, extended tag, offset, length)
|
||||||
|
end, p = e[i + 2], i + 4
|
||||||
|
while p < i + end and p < i + 127:
|
||||||
|
tag, n = e[p] >> 5, e[p] & 0x1f
|
||||||
|
yield tag, (e[p + 1] if tag == 7 and n else None), p, n
|
||||||
|
p += n + 1
|
||||||
|
|
||||||
|
def eeodb(e):
|
||||||
|
for i in cta_blocks(e):
|
||||||
|
for tag, ext, p, n in data_blocks(e, i):
|
||||||
|
if tag == 7 and ext == 0x78: return p + 2
|
||||||
|
return None
|
||||||
|
|
||||||
|
def nblocks(e):
|
||||||
|
p = eeodb(e)
|
||||||
|
return 1 + max(e[126], e[p] if p and p < len(e) else 0)
|
||||||
|
|
||||||
|
def timings(e):
|
||||||
|
# (hz, clk_khz, ha, hb, hf, hs, hpos, va, vb, vf, vs, vpos)
|
||||||
|
out = []
|
||||||
|
def dtd(b):
|
||||||
|
clk = le(b[0:2]) * 10
|
||||||
|
if not clk: return
|
||||||
|
ha = b[2] | (b[4] >> 4) << 8; hb = b[3] | (b[4] & 15) << 8
|
||||||
|
va = b[5] | (b[7] >> 4) << 8; vb = b[6] | (b[7] & 15) << 8
|
||||||
|
hf = b[8] | (b[11] >> 6 & 3) << 8; hs = b[9] | (b[11] >> 4 & 3) << 8
|
||||||
|
vf = (b[10] >> 4) | (b[11] >> 2 & 3) << 4; vs = (b[10] & 15) | (b[11] & 3) << 4
|
||||||
|
if b[17] & 0x80: return # interlaced
|
||||||
|
out.append((clk * 1000 / ((ha + hb) * (va + vb)), clk, ha, hb, hf, hs, b[17] >> 1 & 1,
|
||||||
|
va, vb, vf, vs, b[17] >> 2 & 1))
|
||||||
|
for o in range(54, 126, 18): dtd(e[o:o + 18])
|
||||||
|
for i in cta_blocks(e):
|
||||||
|
d = e[i + 2]
|
||||||
|
if d >= 4:
|
||||||
|
for o in range(i + d, i + 127 - 17, 18): dtd(e[o:o + 18])
|
||||||
|
for i in range(128, len(e), 128):
|
||||||
|
if e[i] != 0x70: continue
|
||||||
|
p, end = i + 5, i + 5 + e[i + 2]
|
||||||
|
while p + 3 <= end:
|
||||||
|
tag, n = e[p], e[p + 2]
|
||||||
|
if tag == 0 and n == 0: break
|
||||||
|
if tag == 0x03:
|
||||||
|
for o in range(p + 3, p + 3 + n - 19, 20):
|
||||||
|
b = e[o:o + 20]
|
||||||
|
clk = (le(b[0:3]) + 1) * 10
|
||||||
|
hf, vf = le(b[8:10]), le(b[16:18])
|
||||||
|
ha, hb, va, vb = le(b[4:6]) + 1, le(b[6:8]) + 1, le(b[12:14]) + 1, le(b[14:16]) + 1
|
||||||
|
out.append((clk * 1000 / ((ha + hb) * (va + vb)), clk, ha, hb, (hf & 0x7fff) + 1,
|
||||||
|
le(b[10:12]) + 1, hf >> 15, va, vb, (vf & 0x7fff) + 1, le(b[18:20]) + 1, vf >> 15))
|
||||||
|
p += 3 + n
|
||||||
|
return out
|
||||||
|
|
||||||
|
def tmds_khz(e):
|
||||||
|
# The display's own TMDS limit: HDMI Forum VSDB, else the HDMI 1.4 VSDB.
|
||||||
|
hf = h14 = 0
|
||||||
|
for i in cta_blocks(e):
|
||||||
|
for tag, ext, p, n in data_blocks(e, i):
|
||||||
|
if tag != 3 or n < 3: continue
|
||||||
|
oui = e[p + 3] << 16 | e[p + 2] << 8 | e[p + 1]
|
||||||
|
if oui == 0xC45DD8 and n >= 5: hf = e[p + 5] * 5000
|
||||||
|
if oui == 0x000C03 and n >= 7: h14 = e[p + 7] * 5000
|
||||||
|
lim = hf or h14 or 165000
|
||||||
|
return min(lim, src_khz)
|
||||||
|
|
||||||
|
def max_vrefresh(e):
|
||||||
|
# Range limits descriptor, the HDMI Forum / AMD VRR maximum.
|
||||||
|
best = 0
|
||||||
|
for o in range(54, 126, 18):
|
||||||
|
b = e[o:o + 18]
|
||||||
|
if b[0:3] == b'\0\0\0' and b[3] == 0xfd:
|
||||||
|
best = max(best, b[6] + (255 if b[4] & 2 else 0))
|
||||||
|
for i in cta_blocks(e):
|
||||||
|
for tag, ext, p, n in data_blocks(e, i):
|
||||||
|
if tag != 3 or n < 3: continue
|
||||||
|
oui = e[p + 3] << 16 | e[p + 2] << 8 | e[p + 1]
|
||||||
|
if oui == 0xC45DD8 and n >= 10: best = max(best, (e[p + 9] & 0xc0) << 2 | e[p + 10])
|
||||||
|
if oui == 0x00001A and n >= 7: best = max(best, e[p + 7])
|
||||||
|
return best
|
||||||
|
|
||||||
|
def shortest(e, w, h):
|
||||||
|
ts = [t for t in timings(e) if t[2] == w and t[7] == h]
|
||||||
|
return min(ts, key=lambda t: (t[2] + t[3]) * (t[7] + t[8])) if ts else None
|
||||||
|
|
||||||
|
if cmd == 'blocks':
|
||||||
|
print(nblocks(load(args[0])))
|
||||||
|
elif cmd == 'id':
|
||||||
|
print(load(args[0])[8:18].hex())
|
||||||
|
elif cmd == 'plan':
|
||||||
|
e = load(args[0]); w, h, cur = int(args[1]), int(args[2]), float(args[3])
|
||||||
|
lim, t = tmds_khz(e), shortest(e, w, h)
|
||||||
|
if not t: sys.exit(1)
|
||||||
|
listed = [x[0] for x in timings(e) if x[2] == w and x[7] == h and x[1] <= lim]
|
||||||
|
best = max(listed) if listed else cur
|
||||||
|
top = int(lim * 1000 // ((t[2] + t[3]) * (t[7] + t[8])))
|
||||||
|
vmax = max_vrefresh(e) or int(max(x[0] for x in timings(e) if x[2] == w and x[7] == h))
|
||||||
|
top = min(top, vmax)
|
||||||
|
# Not right at the limit: the top rounded down to ten, and the hundred
|
||||||
|
# below it as a safe option. Rates the display already lists are left
|
||||||
|
# out (its own timing is the safer one), and so is anything not faster
|
||||||
|
# than what already works.
|
||||||
|
steps = sorted({top // 10 * 10, top // 100 * 100})
|
||||||
|
steps = [s for s in steps if s > best + 1 and not any(abs(s - x) < 1 for x in listed)]
|
||||||
|
print(lim // 1000, round(best, 2), *steps)
|
||||||
|
elif cmd == 'build':
|
||||||
|
e = load(args[0]); w, h = int(args[2]), int(args[3]); rates = [int(r) for r in args[4:]]
|
||||||
|
t = shortest(e, w, h)
|
||||||
|
n = nblocks(e)
|
||||||
|
e = e[:n * 128]
|
||||||
|
out = b''
|
||||||
|
for hz in rates:
|
||||||
|
_, _, ha, hb, hf, hs, hp, va, vb, vf, vs, vp = t
|
||||||
|
clk = round((ha + hb) * (va + vb) * hz / 1e4)
|
||||||
|
x = (clk - 1).to_bytes(3, 'little') + bytes([0x08])
|
||||||
|
for v, pol in ((ha, 0), (hb, 0), (hf, hp), (hs, 0), (va, 0), (vb, 0), (vf, vp), (vs, 0)):
|
||||||
|
x += ((v - 1) | pol << 15).to_bytes(2, 'little')
|
||||||
|
out += x
|
||||||
|
if out:
|
||||||
|
blk = bytearray(128)
|
||||||
|
blk[0:5] = bytes([0x70, 0x12, 121, 0x00, 0x00])
|
||||||
|
blk[5:8] = bytes([0x03, 0x00, len(out)])
|
||||||
|
blk[8:8 + len(out)] = out
|
||||||
|
blk[126] = (-sum(blk[1:126])) % 256
|
||||||
|
blk[127] = (-sum(blk[:127])) % 256
|
||||||
|
e += blk
|
||||||
|
count = len(e) // 128 - 1
|
||||||
|
e[126] = count
|
||||||
|
e[127] = (-sum(e[:127])) % 256
|
||||||
|
p = eeodb(e)
|
||||||
|
if p:
|
||||||
|
e[p] = count
|
||||||
|
i = p - p % 128
|
||||||
|
e[i + 127] = (-sum(e[i:i + 127])) % 256
|
||||||
|
open(args[1], 'wb').write(e)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
hdmi_read_edid() {
|
||||||
|
# hdmi_read_edid <card-connector> <out>: the display's complete EDID,
|
||||||
|
# read over DDC block by block (the kernel's copy may miss blocks).
|
||||||
|
local conn="$1" out="$2" bus n i tmp
|
||||||
|
bus="$(basename "$(readlink -f "/sys/class/drm/$conn/ddc")")"; bus="${bus#i2c-}"
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
cat "/sys/class/drm/$conn/edid" > "$out"
|
||||||
|
if [[ -n "$bus" ]] && sudo modprobe i2c-dev 2>/dev/null; then
|
||||||
|
n="$(hdmi_edid_tool blocks "$out")"
|
||||||
|
: > "$tmp"
|
||||||
|
for (( i = 0; i < n; i++ )); do
|
||||||
|
# Segment pointer (0x30) selects each pair of blocks.
|
||||||
|
sudo i2ctransfer -y "$bus" w1@0x30 $(( i / 2 )) w1@0x50 $(( (i % 2) * 128 )) r128@0x50 2>/dev/null |
|
||||||
|
xxd -r -p >> "$tmp" 2>/dev/null ||
|
||||||
|
{ [[ $i -lt 2 ]] && { sudo i2ctransfer -y "$bus" w1@0x50 $(( i * 128 )) r128@0x50 | xxd -r -p >> "$tmp"; }; } || break
|
||||||
|
done
|
||||||
|
# Keep the DDC copy only when it's whole and the same display as the
|
||||||
|
# kernel's (not the whole block: a live override changes it).
|
||||||
|
if [[ "$(stat -c %s "$tmp")" == $(( n * 128 )) &&
|
||||||
|
"$(hdmi_edid_tool id "$tmp")" == "$(hdmi_edid_tool id "$out")" ]]; then
|
||||||
|
cat "$tmp" > "$out"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
rm -f "$tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
hdmi_debugfs() {
|
||||||
|
# debugfs directory of a connector (one of several paths to the same
|
||||||
|
# GPU). Only root can list debugfs, so the glob runs under sudo.
|
||||||
|
local d
|
||||||
|
d="$(sudo sh -c 'for d in /sys/kernel/debug/dri/*/"$1"; do [ -e "$d/edid_override" ] && { echo "$d"; break; }; done' _ "${1#card*-}")"
|
||||||
|
[[ -n "$d" ]] && echo "$d"
|
||||||
|
}
|
||||||
|
|
||||||
|
hdmi_override_live() {
|
||||||
|
# hdmi_override_live <card-connector> <edid|reset>: swap the EDID the
|
||||||
|
# kernel uses right now, without a restart.
|
||||||
|
local d; d="$(hdmi_debugfs "$1")" || return 1
|
||||||
|
# The kernel takes exactly "reset", without a newline.
|
||||||
|
if [[ "$2" == reset ]]; then printf reset | sudo tee "$d/edid_override" >/dev/null
|
||||||
|
else sudo tee "$d/edid_override" < "$2" >/dev/null; fi || return 1
|
||||||
|
echo 1 | sudo tee "$d/trigger_hotplug" >/dev/null
|
||||||
|
sleep 3
|
||||||
|
}
|
||||||
|
|
||||||
|
hdmi_kscreen() {
|
||||||
|
# hdmi_kscreen current|mode <output> [<w> <h> <hz>]: KDE's view of the
|
||||||
|
# output: "w h hz" of the current mode, or set the mode closest to hz.
|
||||||
|
kscreen-doctor -j 2>/dev/null | python3 -c '
|
||||||
|
import json, sys
|
||||||
|
what, name = sys.argv[1], sys.argv[2]
|
||||||
|
for o in json.load(sys.stdin)["outputs"]:
|
||||||
|
if o["name"] != name: continue
|
||||||
|
modes = {m["id"]: m for m in o["modes"]}
|
||||||
|
if what == "current":
|
||||||
|
m = modes[o["currentModeId"]]
|
||||||
|
print(m["size"]["width"], m["size"]["height"], round(m["refreshRate"], 2))
|
||||||
|
else:
|
||||||
|
w, h, hz = int(sys.argv[3]), int(sys.argv[4]), float(sys.argv[5])
|
||||||
|
ms = [m for m in o["modes"] if m["size"]["width"] == w and m["size"]["height"] == h]
|
||||||
|
if ms: print(min(ms, key=lambda m: abs(m["refreshRate"] - hz))["id"])
|
||||||
|
' "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
hdmi_set_mode() {
|
||||||
|
# hdmi_set_mode <output> <w> <h> <hz>: returns 1 if KDE has no such mode.
|
||||||
|
local id rate
|
||||||
|
id="$(hdmi_kscreen mode "$@")" || return 1
|
||||||
|
[[ -n "$id" ]] || return 1
|
||||||
|
kscreen-doctor "output.$1.mode.$id" >/dev/null 2>&1 || return 1
|
||||||
|
sleep 2
|
||||||
|
rate="$(hdmi_kscreen current "$1" | cut -d' ' -f3)"
|
||||||
|
[[ "${rate%.*}" -ge $(( $4 - 1 )) ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
hdmi_confirm() {
|
||||||
|
# No answer (black screen, nobody there) is a no.
|
||||||
|
local reply
|
||||||
|
read -rt "$HDMI_CONFIRM_SECONDS" -p "$(echo -e "${c_bold}$1${c_reset} [y/N, ${HDMI_CONFIRM_SECONDS}s] ")" reply || { echo; return 1; }
|
||||||
|
[[ "$reply" =~ ^[Yy]$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
hdmi_boot_param() {
|
||||||
|
# hdmi_boot_param <value|"">: set or remove our drm.edid_firmware= in
|
||||||
|
# the boot loader's command line, then rebuild the initramfs and entries.
|
||||||
|
local f value="$1"
|
||||||
|
f="$(hdmi_boot_file)" || return 1
|
||||||
|
[[ -n "$f" ]] || { err "No supported boot loader (Limine, systemd-boot with sdboot-manage, GRUB) found."; return 1; }
|
||||||
|
backup_file "$f"
|
||||||
|
sudo sed -i -E 's/drm\.edid_firmware=[^ "]*steamify-[^ "]* ?//' "$f"
|
||||||
|
if [[ -n "$value" ]]; then
|
||||||
|
case "$f" in
|
||||||
|
# | as delimiter: the value holds a path.
|
||||||
|
/etc/default/limine) sudo sed -i -E "0,\|^KERNEL_CMDLINE\[default\]\+?=\"|s||&$value |" "$f" ;;
|
||||||
|
/etc/sdboot-manage.conf) sudo sed -i -E "0,\|^LINUX_OPTIONS=\"|s||&$value |" "$f" ;;
|
||||||
|
/etc/default/grub) sudo sed -i -E "0,\|^GRUB_CMDLINE_LINUX_DEFAULT=\"|s||&$value |" "$f" ;;
|
||||||
|
esac
|
||||||
|
grep -qF "$value" "$f" || { err "Couldn't add $value to $f."; return 1; }
|
||||||
|
fi
|
||||||
|
info "Rebuilding the initramfs and boot entries..."
|
||||||
|
case "$f" in
|
||||||
|
/etc/default/limine) sudo limine-mkinitcpio ;;
|
||||||
|
/etc/sdboot-manage.conf) sudo /usr/bin/mkinitcpio -P && sudo sdboot-manage gen ;;
|
||||||
|
/etc/default/grub) sudo /usr/bin/mkinitcpio -P && sudo grub-mkconfig -o /boot/grub/grub.cfg ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
hdmi_tune() {
|
||||||
|
# hdmi_tune <card-connector> <tmpdir>: find and confirm the highest rate
|
||||||
|
# at the desktop resolution. Sets HDMI_RESULT to "<w> <h> <hz>..." (the
|
||||||
|
# confirmed new rates) or leaves it empty.
|
||||||
|
local conn="$1" tmp="$2" out="${1#card*-}" cur w h hz plan lim best steps_str
|
||||||
|
local -a steps ok_rates=()
|
||||||
|
HDMI_RESULT=""
|
||||||
|
# From the display's own EDID: a live one left by an earlier test would
|
||||||
|
# make its rates look like what the display already does.
|
||||||
|
hdmi_override_live "$conn" reset 2>/dev/null
|
||||||
|
cur="$(hdmi_kscreen current "$out")"
|
||||||
|
[[ -n "$cur" ]] || { warn "$out: KDE doesn't show this output; skipped."; return 0; }
|
||||||
|
read -r w h hz <<< "$cur"
|
||||||
|
info "$out: desktop resolution ${w}x${h} at ${hz} Hz. Reading the display's EDID..."
|
||||||
|
hdmi_read_edid "$conn" "$tmp/$out.orig" || return 1
|
||||||
|
if ! plan="$(hdmi_edid_tool plan "$tmp/$out.orig" "$w" "$h" "$hz")"; then
|
||||||
|
warn "$out: the display lists no timing for ${w}x${h}; nothing to calculate."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
read -r lim best steps_str <<< "$plan"
|
||||||
|
read -ra steps <<< "${steps_str:-}"
|
||||||
|
info "$out: HDMI limit ${lim} MHz, fastest mode that fits now: ${best} Hz."
|
||||||
|
if [[ ${#steps[@]} -eq 0 ]]; then
|
||||||
|
ok "$out: already at the highest rate that fits HDMI here; nothing to change."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
info "$out: steps to try: ${steps[*]} Hz. After each one you're asked whether the"
|
||||||
|
info "picture is OK; no answer within ${HDMI_CONFIRM_SECONDS} s (e.g. a black screen) switches back."
|
||||||
|
ask_yn "Start the test?" y || return 0
|
||||||
|
|
||||||
|
hdmi_edid_tool build "$tmp/$out.orig" "$tmp/$out.test" "$w" "$h" "${steps[@]}" &&
|
||||||
|
hdmi_override_live "$conn" "$tmp/$out.test" ||
|
||||||
|
{ err "$out: couldn't load the test EDID (debugfs)."; return 1; }
|
||||||
|
local s
|
||||||
|
for s in "${steps[@]}"; do
|
||||||
|
info "$out: switching to ${w}x${h} at $s Hz..."
|
||||||
|
if hdmi_set_mode "$out" "$w" "$h" "$s" && hdmi_confirm "Is the picture OK at $s Hz?"; then
|
||||||
|
ok "$out: $s Hz works."
|
||||||
|
ok_rates+=("$s")
|
||||||
|
else
|
||||||
|
warn "$out: $s Hz doesn't work; stopping here."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ ${#ok_rates[@]} -eq 0 ]]; then
|
||||||
|
hdmi_override_live "$conn" reset
|
||||||
|
hdmi_set_mode "$out" "$w" "$h" "$hz" >/dev/null
|
||||||
|
info "$out: back to ${hz} Hz; nothing changed."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
# Without the steps that failed, so KDE and gamescope never pick them.
|
||||||
|
hdmi_edid_tool build "$tmp/$out.orig" "$tmp/$out.final" "$w" "$h" "${ok_rates[@]}" &&
|
||||||
|
hdmi_override_live "$conn" "$tmp/$out.final"
|
||||||
|
hdmi_set_mode "$out" "$w" "$h" "${ok_rates[-1]}" >/dev/null
|
||||||
|
HDMI_RESULT="$w $h ${ok_rates[*]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
hdmi_enable() {
|
||||||
|
if [[ "${BACKEND:-false}" == true ]]; then
|
||||||
|
err "HDMI refresh boost needs you at the screen: run it from the Steamify menu in Konsole."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ "${XDG_CURRENT_DESKTOP:-}" != *KDE* ]] || ! command -v kscreen-doctor >/dev/null; then
|
||||||
|
err "Run this from the Plasma desktop: the test switches the display mode through KDE."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if hdmi_status; then
|
||||||
|
local conn stale=false
|
||||||
|
for conn in $(hdmi_connectors); do
|
||||||
|
[[ -f "$HDMI_FW_DIR/steamify-${conn#card*-}.bin" ]] || continue
|
||||||
|
[[ "$(state_get hdmi "${conn#card*-}")" == "$(hdmi_edid_tool id "/sys/class/drm/$conn/edid")" ]] || stale=true
|
||||||
|
done
|
||||||
|
if [[ "$stale" == false ]]; then
|
||||||
|
ok "Already set up for the connected display(s). Untick and tick it again to re-test."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
warn "Set up for another display; testing the one connected now."
|
||||||
|
hdmi_disable || return 1
|
||||||
|
fi
|
||||||
|
sudo pacman -S --needed --noconfirm i2c-tools python >/dev/null 2>&1 || { err "Installing i2c-tools failed."; return 1; }
|
||||||
|
|
||||||
|
local tmp conn out param="" files=() rc=0
|
||||||
|
tmp="$(mktemp -d)"
|
||||||
|
for conn in $(hdmi_connectors); do
|
||||||
|
out="${conn#card*-}"
|
||||||
|
hdmi_tune "$conn" "$tmp" || { rc=1; continue; }
|
||||||
|
[[ -n "$HDMI_RESULT" ]] || continue
|
||||||
|
sudo install -Dm644 "$tmp/$out.final" "$HDMI_FW_DIR/steamify-$out.bin" || { rc=1; continue; }
|
||||||
|
state_set hdmi "$out" "$(hdmi_edid_tool id "$tmp/$out.orig")"
|
||||||
|
state_set hdmi "$out-mode" "$HDMI_RESULT"
|
||||||
|
param+="${param:+,}$out:edid/steamify-$out.bin"
|
||||||
|
files+=("$HDMI_FW_DIR/steamify-$out.bin")
|
||||||
|
done
|
||||||
|
rm -rf "$tmp"
|
||||||
|
[[ -n "$param" ]] || return $rc
|
||||||
|
|
||||||
|
# amdgpu loads from the initramfs (the kms hook), before the root
|
||||||
|
# filesystem: the EDID file has to be in there too.
|
||||||
|
sudo mkdir -p "$(dirname "$HDMI_INITRAMFS_CONF")"
|
||||||
|
printf '# Written by steamify: EDID override for HDMI refresh boost.\nFILES+=(%s)\n' "${files[*]}" |
|
||||||
|
sudo tee "$HDMI_INITRAMFS_CONF" >/dev/null
|
||||||
|
if ! hdmi_boot_param "drm.edid_firmware=$param"; then
|
||||||
|
# Nothing half-done left behind; the live EDID lasts until a restart.
|
||||||
|
sudo rm -f "$HDMI_INITRAMFS_CONF" "${files[@]}"
|
||||||
|
for conn in $(hdmi_connectors); do hdmi_override_live "$conn" reset 2>/dev/null; done
|
||||||
|
state_clear hdmi
|
||||||
|
err "Making it permanent failed; the display is back on its own EDID."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
ok "HDMI refresh boost is set up; it's active now and after every restart."
|
||||||
|
return $rc
|
||||||
|
}
|
||||||
|
|
||||||
|
hdmi_disable() {
|
||||||
|
local f conn
|
||||||
|
# Drop-in and files first: the rebuild below leaves them out.
|
||||||
|
sudo rm -f "$HDMI_INITRAMFS_CONF"
|
||||||
|
for f in "$HDMI_FW_DIR"/steamify-*.bin; do [[ -e "$f" ]] && sudo rm -f "$f"; done
|
||||||
|
hdmi_boot_param "" || return 1
|
||||||
|
for conn in $(hdmi_connectors); do hdmi_override_live "$conn" reset 2>/dev/null; done
|
||||||
|
state_clear hdmi
|
||||||
|
ok "HDMI refresh boost removed; the display uses its own EDID again."
|
||||||
|
}
|
||||||
+6
-3
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
# Menu order. Components are turned on in this order and off in reverse;
|
# Menu order. Components are turned on in this order and off in reverse;
|
||||||
# gaming must come first (single user builds on it).
|
# gaming must come first (single user builds on it).
|
||||||
COMPONENTS=(gaming boot theme glyphs single launcher cec machine kpin bios)
|
COMPONENTS=(gaming boot theme glyphs single launcher cec machine kpin hdmi bios)
|
||||||
# One-off actions rather than on/off components: never preselected, never
|
# One-off actions rather than on/off components: never preselected, never
|
||||||
# re-applied, not listed as on or off.
|
# re-applied, not listed as on or off.
|
||||||
ACTIONS=(bios)
|
ACTIONS=(bios)
|
||||||
@@ -14,8 +14,9 @@ declare -A PARENT=([boot]=gaming [kpin]=machine)
|
|||||||
# Never preselected on a first run: booting into the desktop is a choice,
|
# Never preselected on a first run: booting into the desktop is a choice,
|
||||||
# gamescope is the default; HDMI-CEC is opt-in (it can wake the machine or
|
# gamescope is the default; HDMI-CEC is opt-in (it can wake the machine or
|
||||||
# upset other devices on the TV, even on SteamOS), except on a Steam Machine,
|
# upset other devices on the TV, even on SteamOS), except on a Steam Machine,
|
||||||
# which has CEC like on SteamOS.
|
# which has CEC like on SteamOS. HDMI refresh boost needs someone at the
|
||||||
NO_PRESELECT=(boot cec)
|
# screen to confirm each step.
|
||||||
|
NO_PRESELECT=(boot cec hdmi)
|
||||||
|
|
||||||
declare -A LABEL=(
|
declare -A LABEL=(
|
||||||
[gaming]="SteamOS conversion: boot into gaming mode, Steam on the desktop"
|
[gaming]="SteamOS conversion: boot into gaming mode, Steam on the desktop"
|
||||||
@@ -27,6 +28,7 @@ declare -A LABEL=(
|
|||||||
[cec]="HDMI-CEC: use Steam with the TV remote, TV on/off with the PC (experimental)"
|
[cec]="HDMI-CEC: use Steam with the TV remote, TV on/off with the PC (experimental)"
|
||||||
[machine]="Steam Machine support: LED bar driver, hardware settings in Steam"
|
[machine]="Steam Machine support: LED bar driver, hardware settings in Steam"
|
||||||
[kpin]="Pin the kernel to $PINNED_KERNEL_VER (fixes rebooting after shutdown)"
|
[kpin]="Pin the kernel to $PINNED_KERNEL_VER (fixes rebooting after shutdown)"
|
||||||
|
[hdmi]="HDMI refresh boost: highest refresh your HDMI display runs"
|
||||||
[bios]="Update BIOS"
|
[bios]="Update BIOS"
|
||||||
)
|
)
|
||||||
declare -A CURRENT WANTED
|
declare -A CURRENT WANTED
|
||||||
@@ -34,6 +36,7 @@ declare -A CURRENT WANTED
|
|||||||
component_available() {
|
component_available() {
|
||||||
case "$1" in
|
case "$1" in
|
||||||
machine|kpin) machine_available ;;
|
machine|kpin) machine_available ;;
|
||||||
|
hdmi) hdmi_available ;;
|
||||||
bios) bios_available ;;
|
bios) bios_available ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -15,11 +15,11 @@
|
|||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
# Release version, see CHANGELOG.md.
|
# Release version, see CHANGELOG.md.
|
||||||
VERSION=1.1.4
|
VERSION=1.2.0
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
for lib in common state packages login-manager single-user steam-desktop steam-machine cec boot-session vapor-theme steamos-extras bios desktop-shortcut wizard-shortcut menu; do
|
for lib in common state packages login-manager single-user steam-desktop steam-machine hdmi-refresh cec boot-session vapor-theme steamos-extras bios desktop-shortcut wizard-shortcut menu; do
|
||||||
# shellcheck source=/dev/null
|
# shellcheck source=/dev/null
|
||||||
source "$SCRIPT_DIR/lib/$lib.sh"
|
source "$SCRIPT_DIR/lib/$lib.sh"
|
||||||
done
|
done
|
||||||
|
|||||||
Reference in new issue
Block a user