mirror of
https://github.com/theupriser/steamify-cachyos.git
synced 2026-10-03 17:41:58 +02:00
feat(ui): HDMI refresh boost in the app
Set up… opens a screen with the display, its resolution and the rates on offer. The display's own faster mode is switched to right away; the other rates can each be tried live (wait for the picture, keep within 15 s or it switches back) and ticked; Install installs the ticked ones through apply --hdmi. All rates are loaded when the screen opens, so a try is only a mode switch. Also fixes switching back to a rate like 59.97 Hz.
This commit is contained in:
1 parent
881330c3a6
commit
5efe448a51
6 files changed
+444
-34
No files matched your search
@@ -173,9 +173,12 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
|
|||||||
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`): a sub-option of
|
- HDMI refresh boost (`hdmi`, `lib/hdmi-refresh.sh`): a sub-option of
|
||||||
`machine` (like `kpin` and `bios`), only on Fremont with the pinned kernel
|
`machine` (like `kpin` and `bios`), only on Fremont with the pinned kernel
|
||||||
(stays visible while on); unticking `kpin` unticks it. It needs someone at the
|
(stays visible while on); unticking `kpin` unticks it. It needs someone at
|
||||||
screen: the graphical app's backend (`BACKEND=true`) must refuse it, and every step needs a "y" within 15 s
|
the screen. Terminal: every step needs a "y" within 15 s
|
||||||
(`WIZARD_HDMI_CONFIRM_SECONDS` for scripted tests). debugfs is root-only
|
(`WIZARD_HDMI_CONFIRM_SECONDS` for scripted tests). App: its own screen
|
||||||
|
(`hdmi-options`, `hdmi-try`, `hdmi-reset` backend commands, the EDID cached
|
||||||
|
in `$XDG_RUNTIME_DIR/steamify-hdmi`), then `apply --hdmi
|
||||||
|
<output>=<w>x<h>:<rates>`; without `--hdmi` the backend refuses it. debugfs is root-only
|
||||||
(glob it under sudo), and `edid_override` takes exactly `reset` with no
|
(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
|
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
|
replaces the kernel's copy. The kernel command-line value holds a path, so
|
||||||
|
|||||||
+13
-4
@@ -11,10 +11,19 @@ one per merged pull request.
|
|||||||
- A graphical app for controller, keyboard and TV remote, driving the same
|
- A graphical app for controller, keyboard and TV remote, driving the same
|
||||||
components through `steamify.sh --backend`; the BIOS update with both
|
components through `steamify.sh --backend`; the BIOS update with both
|
||||||
warnings; built by GitHub and run with curl.
|
warnings; built by GitHub and run with curl.
|
||||||
- **feat(ui): HDMI refresh boost in the app, off only**
|
- **feat(ui): HDMI refresh boost in the app**
|
||||||
- Each step needs someone at the keyboard, so it's turned on from the
|
- "Set up…" opens its own screen: the display, its resolution and the
|
||||||
menu in Konsole; the app can only turn it off. Unticking Steam Machine
|
rates on offer (its own mode, calculated tens, the HDMI 2.0 limit). The
|
||||||
support or the kernel pin unticks it, as in the menu.
|
display's own faster mode is switched to right away; every other rate
|
||||||
|
has a Try button (switch live, wait for the picture, then keep it within
|
||||||
|
15 s or it switches back) and a tick box. Install installs the ticked
|
||||||
|
rates. All rates are loaded when the screen opens, so a try is only a
|
||||||
|
mode switch.
|
||||||
|
- Unticking Steam Machine support or the kernel pin unticks it, as in the
|
||||||
|
menu.
|
||||||
|
- **fix(ui): The details panel scrolls instead of overflowing**
|
||||||
|
- **fix: Switching back to a rate like 59.97 Hz**
|
||||||
|
- `hdmi_set_mode` compared it as a whole number and printed an error.
|
||||||
|
|
||||||
## 1.2.0 - 2026-09-25
|
## 1.2.0 - 2026-09-25
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,14 @@
|
|||||||
# turns on the listed components and off the others (like ticking them
|
# turns on the listed components and off the others (like ticking them
|
||||||
# in the menu), then streams one JSON object per line: plan, start,
|
# in the menu), then streams one JSON object per line: plan, start,
|
||||||
# log, done, finished. sudo asks through $SUDO_ASKPASS (no terminal).
|
# log, done, finished. sudo asks through $SUDO_ASKPASS (no terminal).
|
||||||
|
# steamify.sh --backend apply ... --hdmi <output>=<w>x<h>:<rate>,... hdmi
|
||||||
|
# HDMI refresh boost with the rates the app confirmed.
|
||||||
|
# steamify.sh --backend hdmi-options
|
||||||
|
# hdmi-options event: every HDMI output, its mode and rates to offer.
|
||||||
|
# steamify.sh --backend hdmi-try <connector> <w> <h> <hz> [<rate>...]
|
||||||
|
# steamify.sh --backend hdmi-reset <connector> <w> <h> <hz>
|
||||||
|
# switch to a rate live (hdmi-tried event), or back to the display's
|
||||||
|
# own EDID (hdmi-reset event); the app asks in between.
|
||||||
# steamify.sh --backend bios-prepare | bios-flash
|
# steamify.sh --backend bios-prepare | bios-flash
|
||||||
# the BIOS update in two steps, so the app shows both warnings between
|
# the BIOS update in two steps, so the app shows both warnings between
|
||||||
# them (bios-ready event: current, newest, checksum, compatible).
|
# them (bios-ready event: current, newest, checksum, compatible).
|
||||||
@@ -127,6 +135,7 @@ backend_apply() {
|
|||||||
case "$1" in
|
case "$1" in
|
||||||
--reapply) reapply=true ;;
|
--reapply) reapply=true ;;
|
||||||
--boot) boot="$2"; shift ;;
|
--boot) boot="$2"; shift ;;
|
||||||
|
--hdmi) HDMI_CHOICE+=("$2"); shift ;;
|
||||||
*) want+=("$1") ;;
|
*) want+=("$1") ;;
|
||||||
esac
|
esac
|
||||||
shift
|
shift
|
||||||
@@ -167,12 +176,34 @@ backend_apply() {
|
|||||||
backend_event finished "\"failed\":$(json_list "${failed[@]}"),\"restart\":$(restart_needed && echo true || echo false)"
|
backend_event finished "\"failed\":$(json_list "${failed[@]}"),\"restart\":$(restart_needed && echo true || echo false)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
backend_hdmi() {
|
||||||
|
# backend_hdmi options|try|reset [args]: the app's HDMI screen. Events
|
||||||
|
# only (no "finished"): the app goes on from its own screen.
|
||||||
|
local what="$1" rc
|
||||||
|
shift
|
||||||
|
backend_sudo || return 1
|
||||||
|
case "$what" in
|
||||||
|
options)
|
||||||
|
sudo pacman -S --needed --noconfirm i2c-tools python >/dev/null 2>&1
|
||||||
|
backend_event hdmi-options "\"outputs\":$(hdmi_options 2>/dev/null)" ;;
|
||||||
|
try)
|
||||||
|
hdmi_try "$@" >/dev/null 2>&1; rc=$?
|
||||||
|
backend_event hdmi-tried "\"hz\":$4,\"ok\":$([[ $rc -eq 0 ]] && echo true || echo false)" ;;
|
||||||
|
reset)
|
||||||
|
hdmi_reset "$@" >/dev/null 2>&1
|
||||||
|
backend_event hdmi-reset ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
backend_main() {
|
backend_main() {
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
status) backend_status ;;
|
status) backend_status ;;
|
||||||
apply) shift; backend_apply "$@" ;;
|
apply) shift; backend_apply "$@" ;;
|
||||||
bios-prepare) backend_bios_prepare ;;
|
bios-prepare) backend_bios_prepare ;;
|
||||||
bios-flash) backend_bios_flash ;;
|
bios-flash) backend_bios_flash ;;
|
||||||
|
hdmi-options) backend_hdmi options ;;
|
||||||
|
hdmi-try) shift; backend_hdmi try "$@" ;;
|
||||||
|
hdmi-reset) shift; backend_hdmi reset "$@" ;;
|
||||||
*) err "Usage: steamify.sh --backend status | apply [--reapply] [--boot gamescope|desktop] <id>..."; return 2 ;;
|
*) err "Usage: steamify.sh --backend status | apply [--reapply] [--boot gamescope|desktop] <id>..."; return 2 ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
+132
-11
@@ -19,6 +19,9 @@ HDMI_INITRAMFS_CONF=/etc/mkinitcpio.conf.d/90-steamify-edid.conf
|
|||||||
HDMI_CONFIRM_SECONDS="${WIZARD_HDMI_CONFIRM_SECONDS:-15}"
|
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.
|
# amdgpu's HDMI 2.0 limit; the display's own limit (from its EDID) may be lower.
|
||||||
HDMI_TMDS_MAX_KHZ=600000
|
HDMI_TMDS_MAX_KHZ=600000
|
||||||
|
# Rates picked and tested in the app ("<output>=<w>x<h>:<rate>,..."), set by
|
||||||
|
# the backend's apply --hdmi; empty in the terminal menu, which tests itself.
|
||||||
|
HDMI_CHOICE=()
|
||||||
|
|
||||||
hdmi_connectors() {
|
hdmi_connectors() {
|
||||||
# Connected HDMI outputs, as "<card>-<connector>" (e.g. card1-HDMI-A-1).
|
# Connected HDMI outputs, as "<card>-<connector>" (e.g. card1-HDMI-A-1).
|
||||||
@@ -173,6 +176,36 @@ elif cmd == 'plan':
|
|||||||
steps = sorted({top // 10 * 10, top // 100 * 100})
|
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)]
|
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)
|
print(lim // 1000, round(best, 2), *steps)
|
||||||
|
elif cmd == 'options':
|
||||||
|
# JSON for the app: the display's name, the HDMI limit and the rates to
|
||||||
|
# offer at w x h: its own modes the kernel skips ("monitor"), calculated
|
||||||
|
# tens up to the limit ("calculated", ticked) and the exact limit when
|
||||||
|
# that's no ten ("limit", not ticked).
|
||||||
|
import json
|
||||||
|
e, k = load(args[0]), load(args[1]); w, h, cur = int(args[2]), int(args[3]), float(args[4])
|
||||||
|
lim, t = tmds_khz(e), shortest(e, w, h)
|
||||||
|
name = ''
|
||||||
|
for o in range(54, 126, 18):
|
||||||
|
if e[o:o + 3] == b'\0\0\0' and e[o + 3] == 0xfc:
|
||||||
|
name = e[o + 5:o + 18].split(b'\n')[0].decode('ascii', 'replace').strip()
|
||||||
|
rates = []
|
||||||
|
if t:
|
||||||
|
fits = [x for x in timings(e) if x[2] == w and x[7] == h and x[1] <= lim]
|
||||||
|
known = [x[0] for x in timings(k) if x[2] == w and x[7] == h]
|
||||||
|
listed = [x[0] for x in fits]
|
||||||
|
best = max(listed + [cur])
|
||||||
|
for x in sorted(fits):
|
||||||
|
if x[0] > cur + 1 and not any(abs(x[0] - y) < 0.5 for y in known):
|
||||||
|
rates.append({'hz': round(x[0], 2), 'mhz': round(x[1] / 1000, 1), 'kind': 'monitor', 'pick': True})
|
||||||
|
size = (t[2] + t[3]) * (t[7] + t[8])
|
||||||
|
vmax = max_vrefresh(e) or int(max(listed + [cur]))
|
||||||
|
top = min(int(lim * 1000 // size), vmax)
|
||||||
|
tens = [r for r in range(10, top + 1, 10) if r > best + 1 and not any(abs(r - y) < 1 for y in listed)][-3:]
|
||||||
|
for r in tens:
|
||||||
|
rates.append({'hz': r, 'mhz': round(size * r / 1e6, 1), 'kind': 'calculated', 'pick': True})
|
||||||
|
if top % 10 and top > best + 1:
|
||||||
|
rates.append({'hz': top, 'mhz': round(size * top / 1e6, 1), 'kind': 'limit', 'pick': False})
|
||||||
|
print(json.dumps({'name': name, 'limit': lim // 1000, 'rates': rates}))
|
||||||
elif cmd == 'build':
|
elif cmd == 'build':
|
||||||
e = load(args[0]); w, h = int(args[2]), int(args[3]); rates = [int(r) for r in args[4:]]
|
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)
|
t = shortest(e, w, h)
|
||||||
@@ -278,7 +311,8 @@ hdmi_set_mode() {
|
|||||||
kscreen-doctor "output.$1.mode.$id" >/dev/null 2>&1 || return 1
|
kscreen-doctor "output.$1.mode.$id" >/dev/null 2>&1 || return 1
|
||||||
sleep 2
|
sleep 2
|
||||||
rate="$(hdmi_kscreen current "$1" | cut -d' ' -f3)"
|
rate="$(hdmi_kscreen current "$1" | cut -d' ' -f3)"
|
||||||
[[ "${rate%.*}" -ge $(( $4 - 1 )) ]]
|
# Whole hertz: the rate asked for can be e.g. 59.97.
|
||||||
|
[[ "${rate%.*}" -ge $(( ${4%.*} - 1 )) ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
hdmi_confirm() {
|
hdmi_confirm() {
|
||||||
@@ -370,9 +404,89 @@ hdmi_tune() {
|
|||||||
HDMI_RESULT="$w $h ${ok_rates[*]}"
|
HDMI_RESULT="$w $h ${ok_rates[*]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- For the app: options, live tries, and installing what was confirmed ---
|
||||||
|
# The EDIDs read by hdmi_options, reused by the tries and the install.
|
||||||
|
HDMI_CACHE="${XDG_RUNTIME_DIR:-/tmp}/steamify-hdmi"
|
||||||
|
|
||||||
|
hdmi_options() {
|
||||||
|
# JSON array: per connected HDMI output its name, mode and rate options.
|
||||||
|
local conn out cur w h hz opts sep=""
|
||||||
|
mkdir -p "$HDMI_CACHE"
|
||||||
|
printf '['
|
||||||
|
for conn in $(hdmi_connectors); do
|
||||||
|
out="${conn#card*-}"
|
||||||
|
# A live EDID left by an earlier try would look like the display's own.
|
||||||
|
hdmi_status || hdmi_override_live "$conn" reset 2>/dev/null
|
||||||
|
cur="$(hdmi_kscreen current "$out")"
|
||||||
|
[[ -n "$cur" ]] || continue
|
||||||
|
read -r w h hz <<< "$cur"
|
||||||
|
hdmi_read_edid "$conn" "$HDMI_CACHE/$out.orig" || continue
|
||||||
|
opts="$(hdmi_edid_tool options "$HDMI_CACHE/$out.orig" "/sys/class/drm/$conn/edid" "$w" "$h" "$hz")" || continue
|
||||||
|
# Every rate on offer loaded now, while the list is on screen: a try
|
||||||
|
# is then only a mode switch (the current mode stays in the EDID).
|
||||||
|
hdmi_status || hdmi_preload "$conn" "$w" "$h" "$opts"
|
||||||
|
printf '%s{"connector":"%s","output":"%s","width":%s,"height":%s,"hz":%s,"info":%s}' "$sep" "$conn" "$out" "$w" "$h" "$hz" "$opts"
|
||||||
|
sep=","
|
||||||
|
done
|
||||||
|
printf ']'
|
||||||
|
}
|
||||||
|
|
||||||
|
hdmi_preload() {
|
||||||
|
# hdmi_preload <card-connector> <w> <h> <options json>: load the EDID
|
||||||
|
# with the calculated rates, as hdmi_try would on the first try.
|
||||||
|
local conn="$1" out="${1#card*-}"
|
||||||
|
local -a rates
|
||||||
|
read -ra rates <<< "$(python3 -c 'import json, sys
|
||||||
|
print(" ".join(str(r["hz"]) for r in json.loads(sys.argv[1])["rates"] if r["kind"] != "monitor"))' "$4")"
|
||||||
|
hdmi_edid_tool build "$HDMI_CACHE/$out.orig" "$HDMI_CACHE/$out.test" "$2" "$3" "${rates[@]}" &&
|
||||||
|
hdmi_override_live "$conn" "$HDMI_CACHE/$out.test" &&
|
||||||
|
cp "$HDMI_CACHE/$out.test" "$HDMI_CACHE/$out.loaded"
|
||||||
|
}
|
||||||
|
|
||||||
|
hdmi_try() {
|
||||||
|
# hdmi_try <card-connector> <w> <h> <hz> [<rate>...]: switch to hz, with
|
||||||
|
# an EDID holding the given calculated rates (loaded once per set).
|
||||||
|
local conn="$1" w="$2" h="$3" hz="$4" out="${1#card*-}"
|
||||||
|
shift 4
|
||||||
|
[[ -f "$HDMI_CACHE/$out.orig" ]] || hdmi_read_edid "$conn" "$HDMI_CACHE/$out.orig"
|
||||||
|
hdmi_edid_tool build "$HDMI_CACHE/$out.orig" "$HDMI_CACHE/$out.test" "$w" "$h" "$@" || return 1
|
||||||
|
if ! cmp -s "$HDMI_CACHE/$out.test" "$HDMI_CACHE/$out.loaded" 2>/dev/null; then
|
||||||
|
hdmi_override_live "$conn" "$HDMI_CACHE/$out.test" || return 1
|
||||||
|
cp "$HDMI_CACHE/$out.test" "$HDMI_CACHE/$out.loaded"
|
||||||
|
fi
|
||||||
|
hdmi_set_mode "$out" "$w" "$h" "$hz"
|
||||||
|
}
|
||||||
|
|
||||||
|
hdmi_reset() {
|
||||||
|
# hdmi_reset <card-connector> <w> <h> <hz>: the display's own EDID again.
|
||||||
|
hdmi_override_live "$1" reset
|
||||||
|
rm -f "$HDMI_CACHE/${1#card*-}.loaded"
|
||||||
|
hdmi_set_mode "${1#card*-}" "$2" "$3" "$4" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
hdmi_choose() {
|
||||||
|
# For HDMI_CHOICE ("<output>=<w>x<h>:<rate>,..." from the app, rates it
|
||||||
|
# confirmed): builds the final EDIDs in <tmpdir> like hdmi_tune does.
|
||||||
|
local tmp="$1" item out mode rates w h
|
||||||
|
local -a r
|
||||||
|
for item in "${HDMI_CHOICE[@]}"; do
|
||||||
|
out="${item%%=*}"; mode="${item#*=}"; rates="${mode#*:}"; mode="${mode%%:*}"
|
||||||
|
w="${mode%x*}"; h="${mode#*x}"
|
||||||
|
IFS=',' read -ra r <<< "$rates"
|
||||||
|
HDMI_CHOSEN_CONN="$(basename /sys/class/drm/card*-"$out")"
|
||||||
|
if [[ -f "$HDMI_CACHE/$out.orig" ]]; then cp "$HDMI_CACHE/$out.orig" "$tmp/$out.orig"
|
||||||
|
else hdmi_read_edid "$HDMI_CHOSEN_CONN" "$tmp/$out.orig" || return 1; fi
|
||||||
|
hdmi_edid_tool build "$tmp/$out.orig" "$tmp/$out.final" "$w" "$h" "${r[@]}" &&
|
||||||
|
hdmi_override_live "$HDMI_CHOSEN_CONN" "$tmp/$out.final" || return 1
|
||||||
|
[[ ${#r[@]} -gt 0 ]] && hdmi_set_mode "$out" "$w" "$h" "${r[-1]}" >/dev/null
|
||||||
|
HDMI_RESULTS+=("$out|$w $h ${r[*]}")
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
hdmi_enable() {
|
hdmi_enable() {
|
||||||
if [[ "${BACKEND:-false}" == true ]]; then
|
local -a HDMI_RESULTS=()
|
||||||
err "HDMI refresh boost needs you at the screen: run it from the Steamify menu in Konsole."
|
if [[ "${BACKEND:-false}" == true && ${#HDMI_CHOICE[@]} -eq 0 ]]; then
|
||||||
|
err "HDMI refresh boost needs you at the screen: pick and test the rates in the app first."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
if [[ "${XDG_CURRENT_DESKTOP:-}" != *KDE* ]] || ! command -v kscreen-doctor >/dev/null; then
|
if [[ "${XDG_CURRENT_DESKTOP:-}" != *KDE* ]] || ! command -v kscreen-doctor >/dev/null; then
|
||||||
@@ -389,24 +503,31 @@ hdmi_enable() {
|
|||||||
ok "Already set up for the connected display(s). Untick and tick it again to re-test."
|
ok "Already set up for the connected display(s). Untick and tick it again to re-test."
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
warn "Set up for another display; testing the one connected now."
|
warn "Set up for another display; setting up the one connected now."
|
||||||
hdmi_disable || return 1
|
hdmi_disable || return 1
|
||||||
fi
|
fi
|
||||||
sudo pacman -S --needed --noconfirm i2c-tools python >/dev/null 2>&1 || { err "Installing i2c-tools failed."; return 1; }
|
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
|
local tmp conn out param="" files=() rc=0 res
|
||||||
tmp="$(mktemp -d)"
|
tmp="$(mktemp -d)"
|
||||||
for conn in $(hdmi_connectors); do
|
if [[ ${#HDMI_CHOICE[@]} -gt 0 ]]; then
|
||||||
out="${conn#card*-}"
|
# Picked and tested in the app.
|
||||||
hdmi_tune "$conn" "$tmp" || { rc=1; continue; }
|
hdmi_choose "$tmp" || rc=1
|
||||||
[[ -n "$HDMI_RESULT" ]] || continue
|
else
|
||||||
|
for conn in $(hdmi_connectors); do
|
||||||
|
hdmi_tune "$conn" "$tmp" || { rc=1; continue; }
|
||||||
|
[[ -n "$HDMI_RESULT" ]] && HDMI_RESULTS+=("${conn#card*-}|$HDMI_RESULT")
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
for res in "${HDMI_RESULTS[@]}"; do
|
||||||
|
out="${res%%|*}"
|
||||||
sudo install -Dm644 "$tmp/$out.final" "$HDMI_FW_DIR/steamify-$out.bin" || { rc=1; 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" "$(hdmi_edid_tool id "$tmp/$out.orig")"
|
||||||
state_set hdmi "$out-mode" "$HDMI_RESULT"
|
state_set hdmi "$out-mode" "${res#*|}"
|
||||||
param+="${param:+,}$out:edid/steamify-$out.bin"
|
param+="${param:+,}$out:edid/steamify-$out.bin"
|
||||||
files+=("$HDMI_FW_DIR/steamify-$out.bin")
|
files+=("$HDMI_FW_DIR/steamify-$out.bin")
|
||||||
done
|
done
|
||||||
rm -rf "$tmp"
|
rm -rf "$tmp" "$HDMI_CACHE"
|
||||||
[[ -n "$param" ]] || return $rc
|
[[ -n "$param" ]] || return $rc
|
||||||
|
|
||||||
# amdgpu loads from the initramfs (the kms hook), before the root
|
# amdgpu loads from the initramfs (the kms hook), before the root
|
||||||
|
|||||||
+243
-14
@@ -71,9 +71,9 @@ ApplicationWindow {
|
|||||||
kpin: { label: "Pin the kernel", hint: "Fixes rebooting after shutdown",
|
kpin: { label: "Pin the kernel", hint: "Fixes rebooting after shutdown",
|
||||||
body: "Newer CachyOS kernels make the Steam Machine reboot instead of shutting down. Untick once CachyOS fixes that.",
|
body: "Newer CachyOS kernels make the Steam Machine reboot instead of shutting down. Untick once CachyOS fixes that.",
|
||||||
changes: ["linux-cachyos from Steamify's release (signature checked)", "Kept in /var/cache/steamify/kernel", "Added to IgnorePkg"] },
|
changes: ["linux-cachyos from Steamify's release (signature checked)", "Kept in /var/cache/steamify/kernel", "Added to IgnorePkg"] },
|
||||||
hdmi: { label: "HDMI refresh boost", hint: "Higher refresh over HDMI · turn on from Steamify in Konsole",
|
hdmi: { label: "HDMI refresh boost", hint: "Higher refresh rates over HDMI",
|
||||||
body: "The pinned kernel keeps many HDMI displays at 60 Hz. This finds the highest refresh rate your display runs at the desktop resolution. Each step needs you to confirm the picture, so it's turned on from the Steamify menu in Konsole; here you can only turn it off.",
|
body: "The pinned kernel keeps many HDMI displays at 60 Hz. Turning this on shows which refresh rates your display can run at the desktop resolution; you pick them, and each one is tried for 15 seconds so you can check the picture before it's installed.",
|
||||||
changes: ["The display's EDID with the confirmed rates", "In the initramfs, drm.edid_firmware on the kernel command line", "Off: the display uses its own EDID again"] },
|
changes: ["The display's EDID with the rates you confirmed", "In the initramfs, drm.edid_firmware on the kernel command line", "Off: the display uses its own EDID again"] },
|
||||||
bios: { label: "Update BIOS", hint: "",
|
bios: { label: "Update BIOS", hint: "",
|
||||||
body: "Installs Valve's newest Steam Machine BIOS, at your own risk. It checks Valve's checksum and asks fwupd whether the file fits this machine, then warns you twice before anything is written.",
|
body: "Installs Valve's newest Steam Machine BIOS, at your own risk. It checks Valve's checksum and asks fwupd whether the file fits this machine, then warns you twice before anything is written.",
|
||||||
changes: ["Valve's fremont-hw-support package (checksum checked)", "fwupd writes the BIOS during the next restart", "Keep the power on until the machine has fully started again"] }
|
changes: ["Valve's fremont-hw-support package (checksum checked)", "fwupd writes the BIOS during the next restart", "Keep the power on until the machine has fully started again"] }
|
||||||
@@ -101,6 +101,85 @@ ApplicationWindow {
|
|||||||
property bool biosChecking: false // the invisible check before the warnings
|
property bool biosChecking: false // the invisible check before the warnings
|
||||||
property int biosFocus: 0 // warning 1: 0 = Cancel, 1 = continue
|
property int biosFocus: 0 // warning 1: 0 = Cancel, 1 = continue
|
||||||
property real holdProgress: 0 // warning 2: hold A/OK for 5 s
|
property real holdProgress: 0 // warning 2: hold A/OK for 5 s
|
||||||
|
// HDMI refresh boost: try rates one by one, then install the kept ones.
|
||||||
|
property var hdmi: null // the first HDMI output from hdmi-options
|
||||||
|
property bool hdmiLoading: false
|
||||||
|
property var hdmiResult: ({}) // hz -> "ok" | "fail" (tried)
|
||||||
|
property var hdmiPick: ({}) // hz -> ticked for Install
|
||||||
|
property int hdmiSel: 0 // rate row; rates.length = the Install button
|
||||||
|
property real hdmiHz: 0 // the rate being tried (e.g. 99.98)
|
||||||
|
property real hdmiNow: 0 // the rate on screen: the last one kept
|
||||||
|
property double hdmiBackAt: 0 // when a test returned to the list
|
||||||
|
property string hdmiPhase: "" // switching | settle | ask | back
|
||||||
|
property int hdmiSettle: 8 // seconds for the display to show a picture again
|
||||||
|
property int hdmiLeft: 15 // seconds to keep it
|
||||||
|
property string hdmiChoice: "" // for apply: <output>=<w>x<h>:<rates>
|
||||||
|
property string hdmiNote: ""
|
||||||
|
property var afterBusy: null // a backend call waiting for the last one
|
||||||
|
function whenIdle(fn) { if (backend.busy) afterBusy = fn; else fn(); }
|
||||||
|
Connections { target: backend; function onBusyChanged() { if (!backend.busy && afterBusy) { var f = afterBusy; afterBusy = null; f(); } } }
|
||||||
|
readonly property var hdmiRates: hdmi && hdmi.info ? hdmi.info.rates : []
|
||||||
|
function hdmiCalc() {
|
||||||
|
// The calculated rates (not the display's own): what the test EDID holds.
|
||||||
|
return hdmiRates.filter(function (r) { return r.kind !== "monitor"; }).map(function (r) { return r.hz; });
|
||||||
|
}
|
||||||
|
function hdmiKept() {
|
||||||
|
// The ticked calculated rates (the display's own modes always come along).
|
||||||
|
return hdmiRates.filter(function (r) { return r.kind !== "monitor" && hdmiPick[r.hz]; })
|
||||||
|
.map(function (r) { return r.hz; }).sort(function (a, b) { return a - b; });
|
||||||
|
}
|
||||||
|
function hdmiUntried() { return hdmiKept().filter(function (hz) { return hdmiResult[hz] !== "ok"; }); }
|
||||||
|
function hdmiTick(r) {
|
||||||
|
if (r.kind === "monitor") return;
|
||||||
|
var p = Object.assign({}, hdmiPick); p[r.hz] = !p[r.hz]; hdmiPick = p;
|
||||||
|
}
|
||||||
|
readonly property bool hdmiCanInstall: hdmiKept().length > 0 || hdmiRates.some(function (r) { return r.kind === "monitor"; })
|
||||||
|
function startHdmi() {
|
||||||
|
hdmi = null; hdmiChoice = ""; hdmiNote = ""; sessionPassword = "";
|
||||||
|
if (!askPassword("hdmi")) hdmiLoad();
|
||||||
|
}
|
||||||
|
function hdmiLoad() {
|
||||||
|
hdmiLoading = true; hdmi = null; hdmiSel = 0; hdmiResult = ({}); hdmiPick = ({}); screen = "hdmi";
|
||||||
|
whenIdle(function () { backend.hdmiOptions(sessionPassword); });
|
||||||
|
}
|
||||||
|
function hdmiSwitch(hz) {
|
||||||
|
whenIdle(function () { backend.hdmiTry(sessionPassword, hdmi.connector, hdmi.width, hdmi.height, hz, hdmiCalc().join(",")); });
|
||||||
|
}
|
||||||
|
function hdmiTry(hz) {
|
||||||
|
hdmiHz = hz; hdmiPhase = "switching"; screen = "hdmitest";
|
||||||
|
hdmiSwitch(hz);
|
||||||
|
}
|
||||||
|
function hdmiMark(result) {
|
||||||
|
var r = Object.assign({}, hdmiResult); r[hdmiHz] = result; hdmiResult = r;
|
||||||
|
// Kept: ticked for Install; didn't work: unticked.
|
||||||
|
var p = Object.assign({}, hdmiPick); p[hdmiHz] = result === "ok"; hdmiPick = p;
|
||||||
|
}
|
||||||
|
function hdmiKeep() { if (hdmiPhase !== "ask" && hdmiPhase !== "settle") return; hdmiMark("ok"); hdmiNow = hdmiHz; hdmiPhase = ""; hdmiBackAt = Date.now(); screen = "hdmi"; }
|
||||||
|
function hdmiReject() {
|
||||||
|
// Back to the last rate that worked (at first: the one from before).
|
||||||
|
if (hdmiPhase !== "ask" && hdmiPhase !== "settle" && hdmiPhase !== "failed") return;
|
||||||
|
hdmiMark("fail"); hdmiPhase = "back";
|
||||||
|
hdmiSwitch(hdmiNow);
|
||||||
|
}
|
||||||
|
function hdmiInstall() {
|
||||||
|
if (!hdmiCanInstall) return;
|
||||||
|
hdmiChoice = hdmi.output + "=" + hdmi.width + "x" + hdmi.height + ":" + hdmiKept().join(",");
|
||||||
|
var w = Object.assign({}, want); w.hdmi = true; w.machine = true; w.kpin = true; want = w;
|
||||||
|
screen = "menu"; goReview(false);
|
||||||
|
}
|
||||||
|
function hdmiCancel() {
|
||||||
|
var tried = Object.keys(hdmiResult).length > 0;
|
||||||
|
hdmiPhase = ""; hdmiChoice = "";
|
||||||
|
if (tried && hdmi) whenIdle(function () { backend.hdmiReset(sessionPassword, hdmi.connector, hdmi.width, hdmi.height, hdmi.hz); });
|
||||||
|
sessionPassword = ""; screen = "menu";
|
||||||
|
}
|
||||||
|
Timer {
|
||||||
|
interval: 1000; repeat: true; running: screen === "hdmitest" && (hdmiPhase === "settle" || hdmiPhase === "ask")
|
||||||
|
onTriggered: {
|
||||||
|
if (hdmiPhase === "settle") { hdmiSettle--; if (hdmiSettle <= 0) hdmiPhase = "ask"; return; }
|
||||||
|
hdmiLeft--; if (hdmiLeft <= 0) hdmiReject();
|
||||||
|
}
|
||||||
|
}
|
||||||
property bool holding: false
|
property bool holding: false
|
||||||
readonly property var bios: status.bios || null
|
readonly property var bios: status.bios || null
|
||||||
function biosHint() {
|
function biosHint() {
|
||||||
@@ -131,13 +210,15 @@ ApplicationWindow {
|
|||||||
Connections { target: backend; function onStatusChanged() { if (screen === "menu" || screen === "done") syncFromStatus(); } }
|
Connections { target: backend; function onStatusChanged() { if (screen === "menu" || screen === "done") syncFromStatus(); } }
|
||||||
|
|
||||||
function toggle(id) {
|
function toggle(id) {
|
||||||
|
hdmiNote = "";
|
||||||
var w = Object.assign({}, want);
|
var w = Object.assign({}, want);
|
||||||
w[id] = !w[id];
|
w[id] = !w[id];
|
||||||
if (id === "gaming" && !w.gaming) { w.single = false; }
|
if (id === "gaming" && !w.gaming) { w.single = false; }
|
||||||
if (id === "single" && w.single) w.gaming = true;
|
if (id === "single" && w.single) w.gaming = true;
|
||||||
// HDMI refresh boost needs someone at the keyboard to confirm each
|
// Turning HDMI refresh boost on goes through its own screen: pick
|
||||||
// step: it's only turned on from the terminal menu.
|
// the rates and try each one first.
|
||||||
if (id === "hdmi" && w.hdmi && !nowOn("hdmi")) return;
|
if (id === "hdmi" && w.hdmi && !nowOn("hdmi") && !hdmiChoice) { startHdmi(); return; }
|
||||||
|
if (id === "hdmi" && !w.hdmi) hdmiChoice = "";
|
||||||
if (id === "machine") w.kpin = w.machine;
|
if (id === "machine") w.kpin = w.machine;
|
||||||
if (id === "kpin" && w.kpin) w.machine = true;
|
if (id === "kpin" && w.kpin) w.machine = true;
|
||||||
if (id === "hdmi" && w.hdmi) { w.machine = true; w.kpin = true; }
|
if (id === "hdmi" && w.hdmi) { w.machine = true; w.kpin = true; }
|
||||||
@@ -173,7 +254,8 @@ ApplicationWindow {
|
|||||||
for (var i = 0; i < plan.length; i++) s[plan[i].id] = "wait";
|
for (var i = 0; i < plan.length; i++) s[plan[i].id] = "wait";
|
||||||
steps = s;
|
steps = s;
|
||||||
screen = "applying";
|
screen = "applying";
|
||||||
backend.apply(wantedIds(), want.gaming ? boot : "", reapply, password || "");
|
backend.apply(wantedIds(), want.gaming ? boot : "", reapply, password || "", want.hdmi ? hdmiChoice : "");
|
||||||
|
hdmiChoice = "";
|
||||||
}
|
}
|
||||||
function askPassword(forWhat) {
|
function askPassword(forWhat) {
|
||||||
pending = forWhat;
|
pending = forWhat;
|
||||||
@@ -182,12 +264,15 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
function onApplyPressed() {
|
function onApplyPressed() {
|
||||||
if (plan.length === 0) return;
|
if (plan.length === 0) return;
|
||||||
|
// Just typed for the HDMI screen: not asked again.
|
||||||
|
if (hdmiChoice && sessionPassword) { var p = sessionPassword; sessionPassword = ""; startApply(p); return; }
|
||||||
if (!askPassword("apply")) startApply("");
|
if (!askPassword("apply")) startApply("");
|
||||||
}
|
}
|
||||||
function submitPassword() {
|
function submitPassword() {
|
||||||
if (!backend.checkPassword(pw.text)) { wrongPassword = true; pw.selectAll(); return; }
|
if (!backend.checkPassword(pw.text)) { wrongPassword = true; pw.selectAll(); return; }
|
||||||
var p = pw.text; pw.text = "";
|
var p = pw.text; pw.text = "";
|
||||||
if (pending === "bios") { sessionPassword = p; biosCheck(); }
|
if (pending === "bios") { sessionPassword = p; biosCheck(); }
|
||||||
|
else if (pending === "hdmi") { sessionPassword = p; hdmiLoad(); }
|
||||||
else startApply(p);
|
else startApply(p);
|
||||||
}
|
}
|
||||||
// --- BIOS: check -> warning 1 -> warning 2 -> flash ---
|
// --- BIOS: check -> warning 1 -> warning 2 -> flash ---
|
||||||
@@ -223,6 +308,32 @@ ApplicationWindow {
|
|||||||
if (ev.event === "start") { s[ev.id] = "run"; steps = s; }
|
if (ev.event === "start") { s[ev.id] = "run"; steps = s; }
|
||||||
else if (ev.event === "done") { s[ev.id] = ev.ok ? "ok" : "fail"; steps = s; doneCount++; }
|
else if (ev.event === "done") { s[ev.id] = ev.ok ? "ok" : "fail"; steps = s; doneCount++; }
|
||||||
else if (ev.event === "log") { logModel.append({ line: ev.line }); if (logModel.count > 400) logModel.remove(0); }
|
else if (ev.event === "log") { logModel.append({ line: ev.line }); if (logModel.count > 400) logModel.remove(0); }
|
||||||
|
else if (ev.event === "hdmi-options") {
|
||||||
|
hdmiLoading = false; hdmiSel = 0;
|
||||||
|
hdmi = ev.outputs && ev.outputs.length ? ev.outputs[0] : null;
|
||||||
|
hdmiNow = hdmi ? hdmi.hz : 0;
|
||||||
|
// Straight to the display's own faster mode (the safe one), with
|
||||||
|
// the next rate's Try selected for after that.
|
||||||
|
var rs = hdmi && hdmi.info ? hdmi.info.rates : [];
|
||||||
|
var own = rs.findIndex(function (r) { return r.kind === "monitor"; });
|
||||||
|
if (own >= 0) { hdmiSel = Math.min(own + 1, rs.length - 1); hdmiTry(rs[own].hz); }
|
||||||
|
}
|
||||||
|
else if (ev.event === "hdmi-tried") {
|
||||||
|
if (hdmiPhase === "back") { hdmiPhase = ""; hdmiBackAt = Date.now(); screen = "hdmi"; return; }
|
||||||
|
// Displays take a few seconds to show a picture after a mode
|
||||||
|
// change: the countdown starts after that.
|
||||||
|
// The display's own mode always works: kept without asking.
|
||||||
|
var ownMode = hdmiRates.some(function (r) { return r.kind === "monitor" && Math.abs(r.hz - hdmiHz) < 0.01; });
|
||||||
|
if (ev.ok && ownMode) { hdmiPhase = "ask"; hdmiKeep(); }
|
||||||
|
else if (ev.ok) { hdmiPhase = "settle"; hdmiSettle = 8; hdmiLeft = 15; }
|
||||||
|
else { hdmiPhase = "failed"; hdmiReject(); }
|
||||||
|
}
|
||||||
|
else if (ev.event === "hdmi-reset") { }
|
||||||
|
else if (ev.event === "finished" && (screen === "hdmi" || screen === "hdmitest")) {
|
||||||
|
// Only on an error (sudo): the HDMI commands end without one.
|
||||||
|
runError = ev.error === "wrong-password" ? "The password didn't work." : "sudo isn't available.";
|
||||||
|
failed = []; restartNeeded = false; sessionPassword = ""; screen = "done";
|
||||||
|
}
|
||||||
else if (ev.event === "bios-ready") { biosChecking = false; biosInfo = ev; biosFocus = 0; holdProgress = 0; screen = "bios1"; }
|
else if (ev.event === "bios-ready") { biosChecking = false; biosInfo = ev; biosFocus = 0; holdProgress = 0; screen = "bios1"; }
|
||||||
else if (ev.event === "finished" && biosChecking) {
|
else if (ev.event === "finished" && biosChecking) {
|
||||||
// The check found nothing to do or a problem: say so.
|
// The check found nothing to do or a problem: say so.
|
||||||
@@ -264,7 +375,7 @@ ApplicationWindow {
|
|||||||
else if (a === "back") { reapply = false; screen = "menu"; }
|
else if (a === "back") { reapply = false; screen = "menu"; }
|
||||||
} else if (screen === "password") {
|
} else if (screen === "password") {
|
||||||
if (a === "accept" || a === "apply") submitPassword();
|
if (a === "accept" || a === "apply") submitPassword();
|
||||||
else if (a === "back") { pw.text = ""; screen = "review"; }
|
else if (a === "back") { pw.text = ""; if (pending === "hdmi") hdmiCancel(); else screen = "review"; }
|
||||||
} else if (screen === "bios1") {
|
} else if (screen === "bios1") {
|
||||||
if (a === "left") biosFocus = 0;
|
if (a === "left") biosFocus = 0;
|
||||||
else if (a === "right") biosFocus = 1;
|
else if (a === "right") biosFocus = 1;
|
||||||
@@ -274,6 +385,20 @@ ApplicationWindow {
|
|||||||
if (a === "back") biosCancel();
|
if (a === "back") biosCancel();
|
||||||
else if (a === "hold") holding = true;
|
else if (a === "hold") holding = true;
|
||||||
else if (a === "release") { holding = false; if (holdProgress < 1) holdProgress = 0; }
|
else if (a === "release") { holding = false; if (holdProgress < 1) holdProgress = 0; }
|
||||||
|
} else if (screen === "hdmi") {
|
||||||
|
if (hdmiLoading) { if (a === "back") hdmiCancel(); return; }
|
||||||
|
var n = hdmiRates.length;
|
||||||
|
if (a === "up") hdmiSel = Math.max(0, hdmiSel - 1);
|
||||||
|
else if (a === "down") hdmiSel = Math.min(n, hdmiSel + 1);
|
||||||
|
else if (a === "accept") { if (hdmiSel === n) hdmiInstall(); else hdmiTick(hdmiRates[hdmiSel]); }
|
||||||
|
else if (a === "reapply" && hdmiSel < n && Math.abs(hdmiNow - hdmiRates[hdmiSel].hz) >= 0.5) hdmiTry(hdmiRates[hdmiSel].hz);
|
||||||
|
else if (a === "apply") hdmiInstall();
|
||||||
|
// A second Back meant for the test (it takes a moment to switch
|
||||||
|
// back) would otherwise leave the whole screen.
|
||||||
|
else if (a === "back" && Date.now() - hdmiBackAt > 1500) hdmiCancel();
|
||||||
|
} else if (screen === "hdmitest") {
|
||||||
|
if (a === "accept" || a === "apply") hdmiKeep();
|
||||||
|
else if (a === "back") hdmiReject();
|
||||||
} else if (screen === "done") {
|
} else if (screen === "done") {
|
||||||
if (a === "accept" && restartNeeded) backend.restart();
|
if (a === "accept" && restartNeeded) backend.restart();
|
||||||
else if (a === "back" || a === "accept") { biosRun = false; syncFromStatus(); screen = "menu"; }
|
else if (a === "back" || a === "accept") { biosRun = false; syncFromStatus(); screen = "menu"; }
|
||||||
@@ -303,7 +428,7 @@ ApplicationWindow {
|
|||||||
else if (k === Qt.Key_Right) act("right");
|
else if (k === Qt.Key_Right) act("right");
|
||||||
else if (k === Qt.Key_Space) act("accept");
|
else if (k === Qt.Key_Space) act("accept");
|
||||||
else if (k === Qt.Key_Return || k === Qt.Key_Enter) act(inputType === "remote" || screen !== "menu" ? "accept" : "apply");
|
else if (k === Qt.Key_Return || k === Qt.Key_Enter) act(inputType === "remote" || screen !== "menu" ? "accept" : "apply");
|
||||||
else if (k === Qt.Key_Escape || k === Qt.Key_Back || k === Qt.Key_Backspace) act("back");
|
else if (k === Qt.Key_Escape || k === Qt.Key_Back || k === Qt.Key_Backspace) { if (!e.isAutoRepeat) act("back"); }
|
||||||
else if (k === Qt.Key_R) act("reapply");
|
else if (k === Qt.Key_R) act("reapply");
|
||||||
else return;
|
else return;
|
||||||
e.accepted = true;
|
e.accepted = true;
|
||||||
@@ -334,7 +459,7 @@ ApplicationWindow {
|
|||||||
color: primary ? t.accent : "#232c38"
|
color: primary ? t.accent : "#232c38"
|
||||||
border.width: focusRing ? 2 : 0; border.color: t.textHi
|
border.width: focusRing ? 2 : 0; border.color: t.textHi
|
||||||
Row { id: br; anchors.centerIn: parent; spacing: 10
|
Row { id: br; anchors.centerIn: parent; spacing: 10
|
||||||
Glyph { k: parent.parent.k; dark: parent.parent.primary; anchors.verticalCenter: parent.verticalCenter }
|
Glyph { k: parent.parent.k; visible: k !== ""; dark: parent.parent.primary; anchors.verticalCenter: parent.verticalCenter }
|
||||||
Text { text: parent.parent.text; color: parent.parent.primary ? t.ink : t.text; font.family: t.body; font.pixelSize: 15; font.weight: Font.DemiBold; anchors.verticalCenter: parent.verticalCenter }
|
Text { text: parent.parent.text; color: parent.parent.primary ? t.ink : t.text; font.family: t.body; font.pixelSize: 15; font.weight: Font.DemiBold; anchors.verticalCenter: parent.verticalCenter }
|
||||||
}
|
}
|
||||||
MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: parent.clicked() }
|
MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: parent.clicked() }
|
||||||
@@ -381,7 +506,7 @@ ApplicationWindow {
|
|||||||
anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 14
|
anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 14
|
||||||
Image { source: iconUrl; width: 40; height: 40; sourceSize: Qt.size(80, 80); anchors.verticalCenter: parent.verticalCenter }
|
Image { source: iconUrl; width: 40; height: 40; sourceSize: Qt.size(80, 80); anchors.verticalCenter: parent.verticalCenter }
|
||||||
Text { text: "Steamify"; color: t.text; font.family: t.display; font.pixelSize: 28; font.weight: Font.Bold; anchors.verticalCenter: parent.verticalCenter }
|
Text { text: "Steamify"; color: t.text; font.family: t.display; font.pixelSize: 28; font.weight: Font.Bold; anchors.verticalCenter: parent.verticalCenter }
|
||||||
Text { text: screen === "menu" ? "v" + (status.version || "") : "/ " + ({review: "Review", password: "Password", applying: biosRun ? "BIOS update" : "Applying", done: "Done", bios1: "BIOS update", bios2: "BIOS update"})[screen]
|
Text { text: screen === "menu" ? "v" + (status.version || "") : "/ " + ({review: "Review", password: "Password", applying: biosRun ? "BIOS update" : "Applying", done: "Done", bios1: "BIOS update", bios2: "BIOS update", hdmi: "HDMI refresh boost", hdmitest: "HDMI refresh boost"})[screen]
|
||||||
color: t.faint; font.family: screen === "menu" ? t.mono : t.body; font.pixelSize: screen === "menu" ? 13 : 15; anchors.verticalCenter: parent.verticalCenter }
|
color: t.faint; font.family: screen === "menu" ? t.mono : t.body; font.pixelSize: screen === "menu" ? 13 : 15; anchors.verticalCenter: parent.verticalCenter }
|
||||||
}
|
}
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -433,6 +558,8 @@ ApplicationWindow {
|
|||||||
readonly property int rowIndex: rows.findIndex(function (r) { return r.id === modelData.id; })
|
readonly property int rowIndex: rows.findIndex(function (r) { return r.id === modelData.id; })
|
||||||
readonly property bool selected: shown && rowIndex === sel
|
readonly property bool selected: shown && rowIndex === sel
|
||||||
readonly property bool on: modelData.kind === "choice" ? true : !!want[modelData.id]
|
readonly property bool on: modelData.kind === "choice" ? true : !!want[modelData.id]
|
||||||
|
// HDMI refresh boost is set up on its own screen: a button while off.
|
||||||
|
readonly property bool setup: modelData.id === "hdmi" && !modelData.on
|
||||||
onSelectedChanged: if (selected) Qt.callLater(list.showSelected)
|
onSelectedChanged: if (selected) Qt.callLater(list.showSelected)
|
||||||
width: list.width - 12; x: 2
|
width: list.width - 12; x: 2
|
||||||
height: shown ? 62 : 0
|
height: shown ? 62 : 0
|
||||||
@@ -473,12 +600,19 @@ ApplicationWindow {
|
|||||||
Text { anchors.centerIn: parent; text: parent.modelData[1]; color: boot === parent.modelData[0] ? t.ink : t.mute; font.family: t.body; font.pixelSize: 13; font.weight: Font.DemiBold }
|
Text { anchors.centerIn: parent; text: parent.modelData[1]; color: boot === parent.modelData[0] ? t.ink : t.mute; font.family: t.body; font.pixelSize: 13; font.weight: Font.DemiBold }
|
||||||
MouseArea { anchors.fill: parent; onClicked: boot = parent.modelData[0] } } } } }
|
MouseArea { anchors.fill: parent; onClicked: boot = parent.modelData[0] } } } } }
|
||||||
// toggle: "now on/off" left of the switch, switch on the edge
|
// toggle: "now on/off" left of the switch, switch on the edge
|
||||||
Rectangle { id: sw; visible: row.modelData.kind === "toggle"; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter
|
Rectangle { id: sw; visible: row.modelData.kind === "toggle" && !row.setup; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter
|
||||||
width: 46; height: 26; radius: 13; color: row.on ? t.accent : "#343f50"
|
width: 46; height: 26; radius: 13; color: row.on ? t.accent : "#343f50"
|
||||||
Behavior on color { ColorAnimation { duration: 120 } }
|
Behavior on color { ColorAnimation { duration: 120 } }
|
||||||
Rectangle { width: 20; height: 20; radius: 10; color: "#f4f7fb"; y: 3; x: row.on ? 23 : 3; Behavior on x { NumberAnimation { duration: 120 } } } }
|
Rectangle { width: 20; height: 20; radius: 10; color: "#f4f7fb"; y: 3; x: row.on ? 23 : 3; Behavior on x { NumberAnimation { duration: 120 } } } }
|
||||||
Text { visible: row.modelData.kind === "toggle"; anchors.right: sw.left; anchors.rightMargin: 12; anchors.verticalCenter: parent.verticalCenter
|
Text { visible: row.modelData.kind === "toggle" && !row.setup; anchors.right: sw.left; anchors.rightMargin: 12; anchors.verticalCenter: parent.verticalCenter
|
||||||
text: row.modelData.on ? "now on" : "now off"; color: row.modelData.on ? t.good : t.faint; font.family: t.mono; font.pixelSize: 12 }
|
text: row.modelData.on ? "now on" : "now off"; color: row.modelData.on ? t.good : t.faint; font.family: t.mono; font.pixelSize: 12 }
|
||||||
|
// HDMI refresh boost while off: opens its screen
|
||||||
|
Rectangle { visible: row.setup; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: sut.implicitWidth + 24; height: 30; radius: 8; color: hdmiChoice && want.hdmi ? t.goodBg : "#1b2b40"
|
||||||
|
Text { id: sut; anchors.centerIn: parent; font.family: t.body; font.pixelSize: 13; font.weight: Font.DemiBold
|
||||||
|
color: hdmiChoice && want.hdmi ? t.good : "#7cc4ff"
|
||||||
|
text: hdmiChoice && want.hdmi ? (hdmiChoice.split(":")[1] ? hdmiChoice.split(":")[1].split(",").join(", ") + " Hz ✓" : "Own modes ✓") : "Set up…" }
|
||||||
|
MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: { sel = row.rowIndex; startHdmi(); } } }
|
||||||
// action
|
// action
|
||||||
Rectangle { visible: row.modelData.kind === "action"; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter
|
Rectangle { visible: row.modelData.kind === "action"; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter
|
||||||
width: at.implicitWidth + 24; height: 30; radius: 8; color: t.warnBg
|
width: at.implicitWidth + 24; height: 30; radius: 8; color: t.warnBg
|
||||||
@@ -555,7 +689,7 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
Row {
|
Row {
|
||||||
anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 16
|
anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 16
|
||||||
Text { readonly property int n: computePlan().length; text: n === 0 ? "Everything is the way you want it" : n + (n === 1 ? " change" : " changes"); color: t.faint; font.family: t.body; font.pixelSize: 14; anchors.verticalCenter: parent.verticalCenter }
|
Text { readonly property int n: computePlan().length; text: hdmiNote ? hdmiNote : (n === 0 ? "Everything is the way you want it" : n + (n === 1 ? " change" : " changes")); color: t.faint; font.family: t.body; font.pixelSize: 14; anchors.verticalCenter: parent.verticalCenter }
|
||||||
Btn { k: g.apply; text: "Review & apply"; primary: true; focusRing: sel === rows.length; height: 44; onClicked: goReview(false) }
|
Btn { k: g.apply; text: "Review & apply"; primary: true; focusRing: sel === rows.length; height: 44; onClicked: goReview(false) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -757,6 +891,101 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================= HDMI: pick the rates =================
|
||||||
|
Item {
|
||||||
|
visible: screen === "hdmi"
|
||||||
|
anchors.top: header.bottom; anchors.bottom: parent.bottom; width: parent.width
|
||||||
|
Text { visible: hdmiLoading; anchors.centerIn: parent; text: "Reading your display…"; color: t.mute; font.family: t.body; font.pixelSize: 18 }
|
||||||
|
Text { visible: !hdmiLoading && !hdmi; anchors.centerIn: parent; horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: "No HDMI display found at the desktop.\nThis only works from the Plasma desktop, not in gaming mode."; color: t.mute; font.family: t.body; font.pixelSize: 18 }
|
||||||
|
Column {
|
||||||
|
visible: !hdmiLoading && !!hdmi
|
||||||
|
x: 40; y: 28; width: 740; spacing: 12
|
||||||
|
Text { text: hdmi ? (hdmi.info.name || "HDMI display") + " · " + hdmi.output : ""; color: t.text; font.family: t.display; font.pixelSize: 30; font.weight: Font.DemiBold }
|
||||||
|
Text { text: hdmi ? hdmi.width + "×" + hdmi.height : ""; color: t.mute; font.family: t.mono; font.pixelSize: 14 }
|
||||||
|
Text { visible: hdmiRates.length === 0; width: 740; wrapMode: Text.WordWrap; topPadding: 12; color: t.soft; font.family: t.body; font.pixelSize: 16
|
||||||
|
text: "This display already runs its fastest rate that fits HDMI at this resolution. Nothing to add." }
|
||||||
|
Repeater {
|
||||||
|
model: hdmiRates
|
||||||
|
Rectangle {
|
||||||
|
required property var modelData; required property int index
|
||||||
|
readonly property bool own: modelData.kind === "monitor"
|
||||||
|
readonly property string res: own ? "own" : (hdmiResult[modelData.hz] || "")
|
||||||
|
width: 740; height: 56; radius: 12
|
||||||
|
color: hdmiSel === index ? t.cardSel : t.card; border.width: hdmiSel === index ? 2 : 0; border.color: t.accent
|
||||||
|
readonly property bool ticked: own || !!hdmiPick[modelData.hz]
|
||||||
|
readonly property bool running: Math.abs(hdmiNow - modelData.hz) < 0.5
|
||||||
|
MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: { hdmiSel = parent.index; hdmiTick(parent.modelData); } }
|
||||||
|
Row { anchors.left: parent.left; anchors.leftMargin: 18; anchors.verticalCenter: parent.verticalCenter; spacing: 16
|
||||||
|
Rectangle { width: 24; height: 24; radius: 6; anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: parent.parent.ticked ? (parent.parent.own ? "#343f50" : t.accent) : "transparent"; border.width: parent.parent.ticked ? 0 : 2; border.color: "#56627a"
|
||||||
|
Text { anchors.centerIn: parent; text: parent.parent.parent.ticked ? "✓" : ""; color: parent.parent.parent.own ? t.soft : t.ink; font.pixelSize: 15; font.weight: Font.Bold } }
|
||||||
|
Text { text: parent.parent.modelData.hz + " Hz"; width: 100; color: t.textHi; font.family: t.body; font.pixelSize: 18; font.weight: Font.DemiBold; anchors.verticalCenter: parent.verticalCenter }
|
||||||
|
Text { width: 380; anchors.verticalCenter: parent.verticalCenter; color: t.soft; font.family: t.body; font.pixelSize: 14
|
||||||
|
text: ({ monitor: "The display's own mode · always included", calculated: "Calculated from the display's timing", limit: "Right at the HDMI 2.0 limit · not recommended" })[parent.parent.modelData.kind] }
|
||||||
|
}
|
||||||
|
Row { anchors.right: parent.right; anchors.rightMargin: 14; anchors.verticalCenter: parent.verticalCenter; spacing: 12
|
||||||
|
Chip { anchors.verticalCenter: parent.verticalCenter; visible: parent.parent.res === "own" || parent.parent.res === "fail"
|
||||||
|
text: ({ own: "Included", fail: "✗ Didn't work" })[parent.parent.res] || ""
|
||||||
|
fg: parent.parent.res === "fail" ? t.bad : (parent.parent.res === "ok" ? t.good : "#b8c3d1")
|
||||||
|
bgc: parent.parent.res === "fail" ? t.badBg : (parent.parent.res === "ok" ? t.goodBg : t.line) }
|
||||||
|
// The rate on screen now: a green marker instead of Try.
|
||||||
|
Rectangle { visible: parent.parent.running; height: 38; radius: 12; width: rt.implicitWidth + 32; color: t.goodBg
|
||||||
|
border.width: hdmiSel === parent.parent.index ? 2 : 0; border.color: t.good; anchors.verticalCenter: parent.verticalCenter
|
||||||
|
Text { id: rt; anchors.centerIn: parent; text: "Running now"; color: t.good; font.family: t.body; font.pixelSize: 15; font.weight: Font.DemiBold } }
|
||||||
|
Btn { visible: !parent.parent.running; height: 38; focusRing: hdmiSel === parent.parent.index; anchors.verticalCenter: parent.verticalCenter
|
||||||
|
k: hdmiSel === parent.parent.index ? g.reapply : ""; text: parent.parent.res === "" || parent.parent.res === "own" ? "Try" : "Try again"
|
||||||
|
onClicked: { hdmiSel = parent.parent.index; hdmiTry(parent.parent.modelData.hz); } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
visible: !hdmiLoading && !!hdmi
|
||||||
|
x: parent.width - 468; y: 28; width: 428; height: 250; radius: 16; color: t.card
|
||||||
|
Column { anchors.fill: parent; anchors.margins: 24; spacing: 12
|
||||||
|
Text { text: "HOW IT WORKS"; color: t.faint; font.family: t.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.8 }
|
||||||
|
Text { width: parent.width; wrapMode: Text.WordWrap; color: t.soft; font.family: t.body; font.pixelSize: 15; lineHeight: 1.35
|
||||||
|
text: "Tick the rates to install (" + g.ok + "). Try one first (" + g.reapply + "): it's shown, and you press " + g.ok + " to keep it. No answer, or a black screen, switches back.\n\nInstall adds the ticked rates and the display's own modes; they stay after a restart." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
anchors.bottom: parent.bottom; width: parent.width; height: 64; color: t.bar
|
||||||
|
Rectangle { width: parent.width; height: 1; color: t.line }
|
||||||
|
Btn { anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; k: g.back; text: "Back to the menu"; onClicked: hdmiCancel() }
|
||||||
|
Text { anchors.right: instBtn.left; anchors.rightMargin: 16; anchors.verticalCenter: parent.verticalCenter; visible: !!hdmi && (!hdmiCanInstall || hdmiUntried().length > 0)
|
||||||
|
text: !hdmiCanInstall ? "Tick a rate first" : "Not tried: " + hdmiUntried().join(", ") + " Hz"
|
||||||
|
color: hdmiCanInstall ? t.warn : t.faint; font.family: t.body; font.pixelSize: 14 }
|
||||||
|
Btn { id: instBtn; anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter; k: hdmiSel === hdmiRates.length ? g.ok : g.apply
|
||||||
|
visible: !hdmiLoading && !!hdmi && hdmiRates.length > 0; primary: hdmiCanInstall; opacity: hdmiCanInstall ? 1 : 0.5
|
||||||
|
focusRing: hdmiSel === hdmiRates.length
|
||||||
|
text: "Install" + (hdmiKept().length ? " " + hdmiKept().join(", ") + " Hz" : ""); onClicked: hdmiInstall() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= HDMI: keep this rate? =================
|
||||||
|
Item {
|
||||||
|
visible: screen === "hdmitest"
|
||||||
|
anchors.top: header.bottom; anchors.bottom: parent.bottom; width: parent.width
|
||||||
|
Rectangle {
|
||||||
|
anchors.centerIn: parent; width: 640; height: ht.implicitHeight + 64; radius: 20; color: t.card; border.width: 1; border.color: t.line
|
||||||
|
Column { id: ht; anchors.fill: parent; anchors.margins: 32; spacing: 18
|
||||||
|
Text { text: hdmiPhase === "ask" || hdmiPhase === "settle" ? "Keep " + hdmiHz + " Hz?" : (hdmiPhase === "back" ? "Switching back…" : "Switching to " + hdmiHz + " Hz…")
|
||||||
|
color: t.text; font.family: t.display; font.pixelSize: 36; font.weight: Font.DemiBold }
|
||||||
|
Text { width: parent.width; wrapMode: Text.WordWrap; color: t.soft; font.family: t.body; font.pixelSize: 16; lineHeight: 1.3
|
||||||
|
text: hdmiPhase === "ask" || hdmiPhase === "settle" ? "Check the picture: sharp, stable, no flicker or dropouts. Your display's menu can show the rate it gets."
|
||||||
|
: "The screen may go black for a moment." }
|
||||||
|
Column { visible: hdmiPhase === "ask" || hdmiPhase === "settle"; width: parent.width; spacing: 8
|
||||||
|
Text { text: hdmiPhase === "settle" ? "Waiting for the picture… the countdown starts in " + hdmiSettle + " s" : "Switching back in " + hdmiLeft + " s"; color: t.mute; font.family: t.body; font.pixelSize: 14 }
|
||||||
|
Rectangle { width: parent.width; height: 10; radius: 5; color: t.line
|
||||||
|
Rectangle { height: 10; radius: 5; color: t.accent; width: parent.width * hdmiLeft / 15; Behavior on width { NumberAnimation { duration: 900 } } } } }
|
||||||
|
Row { visible: hdmiPhase === "ask" || hdmiPhase === "settle"; spacing: 12
|
||||||
|
Btn { k: g.back; text: "Switch back"; onClicked: hdmiReject() }
|
||||||
|
Btn { k: g.ok; text: "Keep " + hdmiHz + " Hz"; primary: true; onClicked: hdmiKeep() } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ================= DONE =================
|
// ================= DONE =================
|
||||||
Item {
|
Item {
|
||||||
visible: screen === "done"
|
visible: screen === "done"
|
||||||
|
|||||||
+19
-2
@@ -245,8 +245,8 @@ class Backend(QObject):
|
|||||||
self.busyChanged.emit()
|
self.busyChanged.emit()
|
||||||
p.start()
|
p.start()
|
||||||
|
|
||||||
@Slot("QVariant", str, bool, str)
|
@Slot("QVariant", str, bool, str, str)
|
||||||
def apply(self, ids, boot, reapply, password):
|
def apply(self, ids, boot, reapply, password, hdmi):
|
||||||
try:
|
try:
|
||||||
if hasattr(ids, "toVariant"): # a JS array from QML
|
if hasattr(ids, "toVariant"): # a JS array from QML
|
||||||
ids = ids.toVariant()
|
ids = ids.toVariant()
|
||||||
@@ -255,6 +255,8 @@ class Backend(QObject):
|
|||||||
args.append("--reapply")
|
args.append("--reapply")
|
||||||
if boot:
|
if boot:
|
||||||
args += ["--boot", boot]
|
args += ["--boot", boot]
|
||||||
|
if hdmi: # rates confirmed on the HDMI screen
|
||||||
|
args += ["--hdmi", hdmi]
|
||||||
self._start(args + [str(i) for i in (ids or [])], password)
|
self._start(args + [str(i) for i in (ids or [])], password)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Never leave the app on the progress screen.
|
# Never leave the app on the progress screen.
|
||||||
@@ -272,6 +274,21 @@ class Backend(QObject):
|
|||||||
"""Only after both warnings in the app."""
|
"""Only after both warnings in the app."""
|
||||||
self._start(["bios-flash"], password)
|
self._start(["bios-flash"], password)
|
||||||
|
|
||||||
|
@Slot(str)
|
||||||
|
def hdmiOptions(self, password):
|
||||||
|
"""Reads the HDMI displays: ends with an hdmi-options event."""
|
||||||
|
self._start(["hdmi-options"], password)
|
||||||
|
|
||||||
|
@Slot(str, str, int, int, float, str)
|
||||||
|
def hdmiTry(self, password, conn, w, h, hz, rates):
|
||||||
|
"""Switches to hz live: an hdmi-tried event says whether it worked."""
|
||||||
|
self._start(["hdmi-try", conn, str(w), str(h), "%g" % hz] + [r for r in rates.split(",") if r], password)
|
||||||
|
|
||||||
|
@Slot(str, str, int, int, float)
|
||||||
|
def hdmiReset(self, password, conn, w, h, hz):
|
||||||
|
"""The display's own EDID and the old rate again (hdmi-reset event)."""
|
||||||
|
self._start(["hdmi-reset", conn, str(w), str(h), "%g" % hz], password)
|
||||||
|
|
||||||
def _read(self, p):
|
def _read(self, p):
|
||||||
self._buf += bytes(p.readAllStandardOutput())
|
self._buf += bytes(p.readAllStandardOutput())
|
||||||
while b"\n" in self._buf:
|
while b"\n" in self._buf:
|
||||||
|
|||||||
Reference in new issue
Block a user