Compare commits

..
13 Commits
Author SHA1 Message Date
theupriser febdab5b53 Merge pull request #28 from theupriser/docs/readme-screenshot
2.4.1: Screenshot of the app's menu in the README
2026-09-27 13:53:41 +02:00
theupriser 90cdfd6666 chore: Version 2.4.1 2026-09-27 13:53:11 +02:00
theupriser 6f598d18fd docs: README screenshot shows every option 2026-09-27 13:52:59 +02:00
theupriser 9bbe5fa602 docs: Screenshot of the app's menu in the README 2026-09-27 13:52:59 +02:00
theupriser ceeb949e6b Merge pull request #27 from theupriser/refactor/qml-screens
2.4.0: One QML file per screen, HDMI setup removed
2026-09-27 13:49:43 +02:00
theupriser 03a8b8c4ba chore: Version 2.4.0 2026-09-27 13:48:13 +02:00
theupriser e699a3f8eb refactor(ui): One QML file per screen; the HDMI boost's setup screens and backend commands removed 2026-09-27 13:48:02 +02:00
theupriser 7bef834b16 Merge pull request #26 from theupriser/feat/vram-booster
2.3.0: VRAM booster, New badge
2026-09-27 13:05:19 +02:00
theupriser 86b9744227 feat: VRAM booster greyed out with an NVIDIA card, with why 2026-09-27 13:02:13 +02:00
theupriser b0947d7a2f feat: VRAM booster only for GPUs with at least 2 GB of their own VRAM 2026-09-27 12:55:40 +02:00
theupriser 3f97c85c3c feat: VRAM booster for every supported GPU, not only the Steam Machine 2026-09-27 12:51:47 +02:00
theupriser fd162c1e2e feat: New badge for default sub-options added in this version 2026-09-27 12:49:49 +02:00
theupriser 54ebddfe98 feat: VRAM booster for the Steam Machine 2026-09-27 12:48:01 +02:00
34 changed files with 1226 additions and 1839 deletions

No files matched your search

+21 -21
View File
@@ -19,6 +19,13 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
apply, never inline in the shell code. Read them with `patch_file apply, never inline in the shell code. Read them with `patch_file
<name>` (`lib/common.sh`, from the checkout); the bundle embeds every file <name>` (`lib/common.sh`, from the checkout); the bundle embeds every file
in `patches/` and overrides `patch_file`. Add new ones to its README. in `patches/` and overrides `patch_file`. Add new ones to its README.
- `ui/` - the app: `steamify-ui` (PySide6; runs `steamify.sh --backend`,
`lib/backend.sh`) and `ui/qml/`: `Main.qml` (window, header, which screen
shows), `AppState.qml` (all state and logic, input actions), one
`*Screen.qml` per screen, small widgets (`Btn`, `Chip`, `Badge`, ...), and
the singletons `Theme` (colours, fonts), `Texts` (item texts) and `Input`
(controller/keyboard/remote and its button names), listed in `qmldir`.
Screens get the `AppState` as `app` and only show it or call its functions.
- `lib/*.sh` - one file per responsibility, each defining functions only - `lib/*.sh` - one file per responsibility, each defining functions only
(no top-level side effects besides constants). See the table in (no top-level side effects besides constants). See the table in
`TECHNICAL.md`. `TECHNICAL.md`.
@@ -33,7 +40,8 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
`VERSION` whenever what `<id>_enable` sets up changes (a new component gets `VERSION` whenever what `<id>_enable` sets up changes (a new component gets
one too): installs recorded with an older version one too): installs recorded with an older version
are ticked and re-applied ("update" in the plan and the app). New default are ticked and re-applied ("update" in the plan and the app). New default
sub-options are ticked for installs whose parent is on (`feature_new`). options are ticked for installs whose parent (top-level: `gaming`) is on
(`feature_new`, shown as "new").
Status still comes from the system; don't add ad-hoc `<id>_repair` checks Status still comes from the system; don't add ad-hoc `<id>_repair` checks
for new changes. for new changes.
- **Reversibility.** Every per-user KDE setting a component changes goes - **Reversibility.** Every per-user KDE setting a component changes goes
@@ -187,26 +195,10 @@ 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`): a sub-option of - HDMI refresh boost (`hdmi`, `lib/hdmi-refresh.sh`): retired in 2.2.0
`machine` (like `kpin` and `bios`), only on Fremont with the pinned kernel (it needed the pinned kernel); only removal is left (see the power-off
(stays visible while on); unticking `kpin` unticks it. It needs someone at fix below). `hdmi_enable` refuses. debugfs is root-only (glob it under
the screen. Terminal: every step needs a "y" within 15 s sudo), and `edid_override` takes exactly `reset` with no newline.
(`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
newline. Build the EDID from the DDC read, not from sysfs: a live override
replaces the kernel's copy. EDIDs are saved per display
(`steamify-<id>.bin`, id = EDID bytes 8-17, listed in
`/etc/steamify/hdmi-edid.conf`), never on the kernel command line (that
applied to any display on the port): `steamify-edid-hotplug` (boot unit +
udev drm hotplug rule) loads the connected display's file, else resets. It
records what's loaded in `/run/steamify-edid` before its own
`trigger_hotplug`, whose event runs it again. `hdmi_status` is on only
while the connected display is boosted; disable forgets only that display
(`kpin_disable` forgets all). The app manages the list (`hdmi-forget`,
`hdmiDisplays` in status). Pre-2.1.0 setups are moved by `hdmi_migrate`.
- Steam Machine CEC driver (`cec_driver_enable`, `lib/cec.sh`): mainline - Steam Machine CEC driver (`cec_driver_enable`, `lib/cec.sh`): mainline
`cros_ec_cec` lacks Fremont, so HDMI-CEC builds Valve's copy (evlaV `cros_ec_cec` lacks Fremont, so HDMI-CEC builds Valve's copy (evlaV
`linux-integration`, pinned commit + SHA-256) with DKMS for every kernel. `linux-integration`, pinned commit + SHA-256) with DKMS for every kernel.
@@ -231,6 +223,14 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
`linux-cachyos` is clang-built, `-bore` GCC-built: let DKMS pick the `linux-cachyos` is clang-built, `-bore` GCC-built: let DKMS pick the
compiler, never pass `LLVM=1`. Test shutdown on the real machine for every compiler, never pass `LLVM=1`. Test shutdown on the real machine for every
new major kernel. new major kernel.
- VRAM booster (`vram`, `lib/vram-booster.sh`, top-level, ticked by
default, 2.3.0; only available when `/sys/fs/cgroup/dmem.capacity` lists a
VRAM region of at least 2 GB; greyed out with an NVIDIA card,
`vram_selectable`, faked with `WIZARD_VRAM_FAKE_NVIDIA=1`): CachyOS's `dmemcg-booster` (system + user service) and
`plasma-foreground-booster`, like SteamOS 3.9's VRAM management. The
latter only starts with `kcgroupsrc [Foreground Booster] autostart=true`
(set with `kset`). Disable removes only the packages it installed
(`state_get vram installed_pkgs`).
- `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.
+20 -1
View File
@@ -5,9 +5,28 @@ 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.
## 2.4.1 - 2026-09-27
- **chore: Version 2.4.1**
- `6f598d1` **docs: README screenshot shows every option**
- `9bbe5fa` **docs: Screenshot of the app's menu in the README**
## 2.4.0 - 2026-09-27
- `03a8b8c` **chore: Version 2.4.0**
- `e699a3f` **refactor(ui): One QML file per screen; the HDMI boost's setup screens and backend commands removed**
## 2.3.0 - 2026-09-27
- `86b9744` **feat: VRAM booster greyed out with an NVIDIA card, with why**
- `b0947d7` **feat: VRAM booster only for GPUs with at least 2 GB of their own VRAM**
- `3f97c85` **feat: VRAM booster for every supported GPU, not only the Steam Machine**
- `fd162c1` **feat: New badge for default sub-options added in this version**
- `54ebddf` **feat: VRAM booster for the Steam Machine**
## 2.2.0 - 2026-09-27 ## 2.2.0 - 2026-09-27
- **docs: Changelog entries per commit for 2.2.0** - `ef585c0` **docs: Changelog entries per commit for 2.2.0**
- `32795bd` **feat(ui): Update badge on the progress screen** - `32795bd` **feat(ui): Update badge on the progress screen**
- `3c8ca65` **fix(ui): Never write the sudo password to a file; Update label on the progress screen** - `3c8ca65` **fix(ui): Never write the sudo password to a file; Update label on the progress screen**
- The app gave `sudo` the password through a short-lived file; the helper - The app gave `sudo` the password through a short-lived file; the helper
+8 -1
View File
@@ -1,5 +1,7 @@
# Steamify CachyOS # Steamify CachyOS
![The Steamify app's menu on a Steam Machine](assets/screenshot-menu.png)
Turn a CachyOS desktop PC into a SteamOS-style console: it boots straight Turn a CachyOS desktop PC into a SteamOS-style console: it boots straight
into Steam's Big Picture (gamescope), and you can switch to the KDE Plasma into Steam's Big Picture (gamescope), and you can switch to the KDE Plasma
desktop and back whenever you like - just like on a Steam Deck or Valve's desktop and back whenever you like - just like on a Steam Deck or Valve's
@@ -28,7 +30,12 @@ Steam Machine.
5. **Steamify shortcut** - "Steamify CachyOS" on the desktop and in the 5. **Steamify shortcut** - "Steamify CachyOS" on the desktop and in the
launcher opens the newest Steamify app, so you don't need the install launcher opens the newest Steamify app, so you don't need the install
command again. "Steamify Terminal" in the launcher opens the terminal menu. command again. "Steamify Terminal" in the launcher opens the terminal menu.
6. **HDMI-CEC** (experimental; off by default, on by default on a Steam 6. **VRAM booster** - on by default: the game you're playing keeps its video
memory and background apps make room, like SteamOS 3.9. Shown for GPUs
whose driver supports it (AMD, Intel) on kernel 7.2 or newer; greyed out
with an NVIDIA card until NVIDIA's driver supports it
([details](TECHNICAL.md#vram-booster)).
7. **HDMI-CEC** (experimental; off by default, on by default on a Steam
Machine) - use Steam with your TV's Machine) - use Steam with your TV's
remote, and the TV turns on and off with the PC. Needs a PC with CEC (like remote, and the TV turns on and off with the PC. Needs a PC with CEC (like
the Steam Machine) or a USB CEC adapter; most graphics cards, NVIDIA the Steam Machine) or a USB CEC adapter; most graphics cards, NVIDIA
+34 -3
View File
@@ -236,6 +236,34 @@ Where Steam Machine support was set up before 2.2.0, the menu ticks the
power-off fix as a new default sub-option (see feature versions below), so a power-off fix as a new default sub-option (see feature versions below), so a
normal run adds it. normal run adds it.
## VRAM booster
SteamOS 3.9 manages the dGPU's VRAM per cgroup: the game in front is
protected, background apps are evicted first. Without it a game that needs
most of the VRAM (8 GB on the Steam Machine) can be pushed into system RAM
by the desktop and other apps, and stutter. The kernel side is the `dmem`
cgroup controller (7.2); amdgpu and Intel's xe register their VRAM with it,
NVIDIA's driver doesn't: with an NVIDIA card the option is shown greyed
out, with why (`WIZARD_VRAM_FAKE_NVIDIA=1` fakes that for tests). Otherwise
the **VRAM booster** option is only shown when
`/sys/fs/cgroup/dmem.capacity` lists a VRAM region of at least 2 GB (a
dedicated GPU; an integrated one registers a small carve-out) or it's already on, and is
ticked by default; new in 2.3.0, so setups with the SteamOS conversion on
get it ticked. CachyOS packages the userspace side, which it installs:
- `dmemcg-booster`: `dmemcg-booster-system.service` enables the `dmem`
controller and sets `dmem.low` for the cgroup in front;
`dmemcg-booster-user.service` runs in the user session.
- `plasma-foreground-booster`: tells it which window is in front on the
desktop. It only starts with `autostart=true` under `[Foreground Booster]`
in `kcgroupsrc`, which the wizard sets (and reverts).
Check it with `cat /sys/fs/cgroup/cgroup.subtree_control` (lists `dmem`) and
the `dmem.low` of the slices under `user@<uid>.service`. Turning it off
disables the services and removes the packages the wizard installed.
Not added: a fixed `dmem.max` cap for `app.slice` (as some guides do); only
worth it if the booster itself turns out to cause stutter.
## Feature versions (updates) ## Feature versions (updates)
Each component has a feature version (`FEATURE_VERSION` in `lib/menu.sh`): Each component has a feature version (`FEATURE_VERSION` in `lib/menu.sh`):
@@ -250,9 +278,11 @@ on the system itself; the version only decides about updates:
"update". A setup from before versions were recorded counts as 2.1.0, "update". A setup from before versions were recorded counts as 2.1.0,
so on a machine set up with 2.1.0, Steam Machine support (2.2.0) is so on a machine set up with 2.1.0, Steam Machine support (2.2.0) is
updated. updated.
- **A new default sub-option** (not in `NO_PRESELECT`) whose parent is on - **A new default option** (not in `NO_PRESELECT`) whose parent is on (for
and that was never turned on or off: ticked, so a normal run adds it. a top-level one: the SteamOS conversion) and that was never turned on or
Turning it off once records `off`, so it isn't ticked again. off: ticked, so a normal run adds it. Turning it off once records `off`,
so it isn't ticked again. The menu and the app show it with a **New**
badge, and the plan says "new in this version".
Set a component's entry to the new `VERSION` whenever what its Set a component's entry to the new `VERSION` whenever what its
`<id>_enable` sets up changes. `<id>_enable` sets up changes.
@@ -327,6 +357,7 @@ immediately, which can turn into a loop - see
| `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 | | `lib/steam-machine.sh` | Steam Machine support: LED driver, LED access, steamos-manager |
| `lib/fremont-poweroff.sh` | Steam Machine support: the power-off fix (DKMS module from `patches/`) | | `lib/fremont-poweroff.sh` | Steam Machine support: the power-off fix (DKMS module from `patches/`) |
| `lib/vram-booster.sh` | VRAM booster (`dmemcg-booster`, `plasma-foreground-booster`) |
| `patches/` | Module sources and patches the scripts build or apply (`patch_file`); see its README | | `patches/` | Module sources and patches the scripts build or apply (`patch_file`); see its README |
| `.github/tools/bundle.sh` | Builds the single-file version (`dist/steamify.sh`), with `patches/` embedded | | `.github/tools/bundle.sh` | Builds the single-file version (`dist/steamify.sh`), with `patches/` embedded |
| `.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` |
Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

+5 -48
View File
@@ -8,16 +8,6 @@
# 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>
# steamify.sh --backend hdmi-forget <display id>
# removes a saved display (hdmi-forgotten event); status lists them.
# 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).
@@ -63,6 +53,7 @@ backend_status() {
items+=",\"on\":$( [[ "$now" == 1 ]] && echo true || echo false)" items+=",\"on\":$( [[ "$now" == 1 ]] && echo true || echo false)"
items+=",\"wanted\":$( [[ "${WANTED[$c]:-0}" == 1 ]] && echo true || echo false)" items+=",\"wanted\":$( [[ "${WANTED[$c]:-0}" == 1 ]] && echo true || echo false)"
items+=",\"update\":$(feature_outdated "$c" && echo true || echo false)" items+=",\"update\":$(feature_outdated "$c" && echo true || echo false)"
items+=",\"new\":$(feature_new "$c" && echo true || echo false)"
items+=",\"selectable\":$(component_selectable "$c" && echo true || echo false)}" items+=",\"selectable\":$(component_selectable "$c" && echo true || echo false)}"
done done
local cec="" f local cec="" f
@@ -71,20 +62,13 @@ backend_status() {
if bios_available; then if bios_available; then
bios="{\"current\":$(json_str "$(bios_current)"),\"newest\":$(json_str "${BIOS_NEWEST:-}"),\"selectable\":$(bios_selectable && echo true || echo false),\"dryRun\":$([[ -n "$BIOS_DRY_RUN" ]] && echo true || echo false)}" bios="{\"current\":$(json_str "$(bios_current)"),\"newest\":$(json_str "${BIOS_NEWEST:-}"),\"selectable\":$(bios_selectable && echo true || echo false),\"dryRun\":$([[ -n "$BIOS_DRY_RUN" ]] && echo true || echo false)}"
fi fi
# HDMI refresh boost's saved displays; "active" = connected and boosted. printf '{"version":%s,"firstRun":%s,"steamMachine":%s,"kernel":%s,"pinnedKernel":%s,"cecDevices":%s,"leds":%s,"bios":%s,"items":[%s]}\n' \
local hdmi_list="" active hid hname hmode hrates
active=" $(hdmi_active_ids | tr '\n' ' ') "
while IFS=$'\t' read -r hid hname hmode hrates; do
[[ -n "$hid" ]] || continue
hdmi_list+="${hdmi_list:+,}{\"id\":$(json_str "$hid"),\"name\":$(json_str "$hname"),\"mode\":$(json_str "$hmode"),\"rates\":$(json_str "$hrates"),\"active\":$([[ "$active" == *" $hid "* ]] && echo true || echo false)}"
done < <(hdmi_saved)
printf '{"version":%s,"firstRun":%s,"steamMachine":%s,"kernel":%s,"pinnedKernel":%s,"cecDevices":%s,"leds":%s,"bios":%s,"hdmiDisplays":[%s],"items":[%s]}\n' \
"$(json_str "$VERSION")" "$first_run" \ "$(json_str "$VERSION")" "$first_run" \
"$(detect_valve_fremont && echo true || echo false)" \ "$(detect_valve_fremont && echo true || echo false)" \
"$(json_str "$(uname -r)")" "$(json_str "${PINNED_KERNEL_VER:-}")" \ "$(json_str "$(uname -r)")" "$(json_str "${PINNED_KERNEL_VER:-}")" \
"$(json_str "$cec")" \ "$(json_str "$cec")" \
"$(compgen -G '/sys/class/leds/valve-leds*' | wc -l)" \ "$(compgen -G '/sys/class/leds/valve-leds*' | wc -l)" \
"$bios" "$hdmi_list" "$items" "$bios" "$items"
} }
backend_run_component() { backend_run_component() {
@@ -145,7 +129,6 @@ 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
@@ -165,8 +148,8 @@ backend_apply() {
[[ "${WANTED[machine]:-0}" == 0 ]] && WANTED[poweroff]=0 [[ "${WANTED[machine]:-0}" == 0 ]] && WANTED[poweroff]=0
[[ "${WANTED[kpin]:-0}" == 1 ]] && WANTED[machine]=1 [[ "${WANTED[kpin]:-0}" == 1 ]] && WANTED[machine]=1
[[ "${WANTED[machine]:-0}" == 0 ]] && WANTED[kpin]=0 [[ "${WANTED[machine]:-0}" == 0 ]] && WANTED[kpin]=0
[[ "${WANTED[hdmi]:-0}" == 1 ]] && { WANTED[machine]=1; WANTED[kpin]=1; } # Retired: only ever removed.
[[ "${WANTED[kpin]:-0}" == 0 ]] && WANTED[hdmi]=0 WANTED[hdmi]=0
case "$boot" in desktop) WANTED[boot]=1; WANTED[gaming]=1 ;; gamescope) WANTED[boot]=0 ;; esac case "$boot" in desktop) WANTED[boot]=1; WANTED[gaming]=1 ;; gamescope) WANTED[boot]=0 ;; esac
REAPPLY=$reapply REAPPLY=$reapply
@@ -192,38 +175,12 @@ 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|forget [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 ;;
forget)
[[ "${1:-}" =~ ^[0-9a-f]{20}$ ]] && hdmi_forget "$1" >/dev/null 2>&1; rc=$?
backend_event hdmi-forgotten "\"id\":$(json_str "${1:-}"),\"ok\":$([[ $rc -eq 0 ]] && echo true || echo false)" ;;
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 "$@" ;;
hdmi-forget) shift; backend_hdmi forget "$@" ;;
*) 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
} }
+28 -621
View File
@@ -1,37 +1,22 @@
#!/bin/bash #!/bin/bash
# "HDMI refresh boost" menu item, Steam Machine (Fremont) only: finds the # "HDMI refresh boost" menu item (hdmi), retired in 2.2.0: it saved an EDID
# highest refresh rate the display runs at the desktop resolution over HDMI, # override per display (with a hotplug script loading it) so the pinned
# tests it live and saves an EDID override per display, loaded only while # 7.1.6 kernel ran HDMI displays above 60 Hz. Newer kernels read the whole
# that display is connected (see hdmi_install_hotplug). Two things keep displays below what they can do: # EDID themselves, and the kernel pin is gone with the power-off fix, so
# - Monitors put their fast modes in an extra EDID block (HDMI Forum EEODB); # only its removal is left: shown while anything of it is on the system,
# the pinned kernel (7.1.6) only reads the first extension. Newer kernels # always unticked, so a normal run removes it.
# do, and do HDMI 2.1, so it's only offered with the pinned kernel. # Sourced by steamify.sh; not meant to be run on its own.
# - 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_FW_DIR=/usr/lib/firmware/edid
# Before 2.1.0 the override was on the kernel command line; only removed now. # Before 2.1.0 the override was on the kernel command line.
HDMI_INITRAMFS_CONF=/etc/mkinitcpio.conf.d/90-steamify-edid.conf HDMI_INITRAMFS_CONF=/etc/mkinitcpio.conf.d/90-steamify-edid.conf
# The saved displays, one per line: <id> TAB <name> TAB <w>x<h> TAB <rates>. # The saved displays; their EDIDs are $HDMI_FW_DIR/steamify-<id>.bin.
# Their EDIDs are $HDMI_FW_DIR/steamify-<id>.bin.
HDMI_MAP=/etc/steamify/hdmi-edid.conf HDMI_MAP=/etc/steamify/hdmi-edid.conf
# Per output, the ID of the display whose EDID is loaded, or "reset".
HDMI_RUN=/run/steamify-edid HDMI_RUN=/run/steamify-edid
HDMI_HOTPLUG=/usr/local/bin/steamify-edid-hotplug HDMI_HOTPLUG=/usr/local/bin/steamify-edid-hotplug
HDMI_UNIT_NAME=steamify-edid.service HDMI_UNIT_NAME=steamify-edid.service
HDMI_UNIT="/etc/systemd/system/$HDMI_UNIT_NAME" HDMI_UNIT="/etc/systemd/system/$HDMI_UNIT_NAME"
HDMI_UDEV_RULE=/etc/udev/rules.d/90-steamify-edid.rules HDMI_UDEV_RULE=/etc/udev/rules.d/90-steamify-edid.rules
# 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
# 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).
@@ -55,307 +40,29 @@ hdmi_cmdline_param() {
[[ -n "$f" ]] && grep -oE 'drm\.edid_firmware=[^ "]*steamify-[^ "]*' "$f" 2>/dev/null | head -n 1 [[ -n "$f" ]] && grep -oE 'drm\.edid_firmware=[^ "]*steamify-[^ "]*' "$f" 2>/dev/null | head -n 1
} }
hdmi_available() { hdmi_available() { detect_valve_fremont && hdmi_status; }
# No longer offered (it needed the pinned kernel): only shown while
# anything of it is left, so a normal run removes it.
detect_valve_fremont && hdmi_status
}
hdmi_status() { hdmi_status() {
# On while anything of it is left: saved displays (connected or not), the # On while anything of it is left: saved displays, the hotplug script,
# hotplug script, an EDID file, or the kernel parameter of versions # an EDID file, or the kernel parameter of versions before 2.1.0.
# before 2.1.0. [[ -s "$HDMI_MAP" || -f "$HDMI_UNIT" || -n "$(hdmi_cmdline_param)" ]] ||
[[ -n "$(hdmi_saved)" || -f "$HDMI_UNIT" || -n "$(hdmi_cmdline_param)" ]] ||
compgen -G "$HDMI_FW_DIR/steamify-*.bin" >/dev/null compgen -G "$HDMI_FW_DIR/steamify-*.bin" >/dev/null
} }
hdmi_saved() { [[ -f "$HDMI_MAP" ]] && grep -v '^#' "$HDMI_MAP"; } hdmi_reset_live() {
# Puts a connected display back on its own EDID right away. debugfs is
hdmi_active_ids() { # root-only, so the glob runs under sudo; the kernel takes exactly
# IDs of the connected displays whose saved EDID is loaded. # "reset", without a newline.
local f
for f in "$HDMI_RUN"/*; do [[ -f "$f" ]] && grep -vx reset "$f"; done
return 0
}
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 == 'name':
e = load(args[0])
for o in range(54, 126, 18):
if e[o:o + 3] == b'\0\0\0' and e[o + 3] == 0xfc:
print(e[o + 5:o + 18].split(b'\n')[0].decode('ascii', 'replace').strip())
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 == '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':
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 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*-}")" 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" [[ -n "$d" ]] || return 1
} printf reset | sudo tee "$d/edid_override" >/dev/null &&
echo 1 | sudo tee "$d/trigger_hotplug" >/dev/null
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)"
# Whole hertz: the rate asked for can be e.g. 59.97.
[[ "${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_remove_boot_param() { hdmi_remove_boot_param() {
# Removes the drm.edid_firmware= older versions put on the kernel command # Removes the drm.edid_firmware= older versions put on the kernel command
# line (it applied to whatever display was on that port), then rebuilds # line, then rebuilds the initramfs and boot entries.
# the initramfs and boot entries.
local f local f
[[ -n "$(hdmi_cmdline_param)" ]] || { sudo rm -f "$HDMI_INITRAMFS_CONF"; return 0; } [[ -n "$(hdmi_cmdline_param)" ]] || { sudo rm -f "$HDMI_INITRAMFS_CONF"; return 0; }
f="$(hdmi_boot_file)" f="$(hdmi_boot_file)"
@@ -370,326 +77,26 @@ hdmi_remove_boot_param() {
esac esac
} }
hdmi_write_map() { hdmi_forget_all() {
# hdmi_write_map <lines>: the saved displays list. # Every saved display and the hotplug script that loaded their EDIDs.
sudo mkdir -p "$(dirname "$HDMI_MAP")"
{ printf '# Written by steamify: HDMI refresh boost, one display per line:\n'
printf '# <id>\t<name>\t<w>x<h>\t<rates>\n'
[[ -n "$1" ]] && printf '%s\n' "$1"; } | sudo tee "$HDMI_MAP" >/dev/null
}
hdmi_save() {
# hdmi_save <edid> <w>x<h> <rates>: keep an EDID for the display it was
# built from (block 0 is the display's own, so is its ID), replacing
# what was saved for that display before.
local id name
id="$(hdmi_edid_tool id "$1")" && [[ -n "$id" ]] || return 1
name="$(hdmi_edid_tool name "$1")"
sudo install -Dm644 "$1" "$HDMI_FW_DIR/steamify-$id.bin" || return 1
hdmi_write_map "$(hdmi_saved | awk -F'\t' -v i="$id" '$1 != i'
printf '%s\t%s\t%s\t%s' "$id" "${name:-HDMI display}" "$2" "$3")"
}
hdmi_forget() {
# hdmi_forget <id>|all: remove saved displays; the hotplug script puts
# a connected one back on its own EDID, and goes when none are left.
local rest=""
if [[ "$1" == all ]]; then
sudo rm -f "$HDMI_FW_DIR"/steamify-*.bin
else
rest="$(hdmi_saved | awk -F'\t' -v i="$1" '$1 != i')"
sudo rm -f "$HDMI_FW_DIR/steamify-$1.bin"
fi
if [[ -z "$rest" ]]; then hdmi_remove_hotplug; return; fi
hdmi_write_map "$rest"
sudo systemctl restart "$HDMI_UNIT_NAME"
}
hdmi_migrate() {
# Setups before 2.1.0: steamify-<output>.bin on the kernel command line,
# the display's ID and mode in the state file. Saved per display now.
local f out
local -a smode
for f in "$HDMI_FW_DIR"/steamify-*-*.bin; do
[[ -e "$f" ]] || continue
out="${f##*/steamify-}"; out="${out%.bin}"
read -ra smode <<< "$(state_get hdmi "$out-mode")"
[[ ${#smode[@]} -ge 2 ]] && hdmi_save "$f" "${smode[0]}x${smode[1]}" "${smode[*]:2}"
sudo rm -f "$f"
done
state_clear hdmi
hdmi_remove_boot_param
}
hdmi_install_hotplug() {
# A saved EDID is only loaded while its display is connected: a udev
# rule runs the script at every hotplug (and a unit at boot, before the
# login manager). The script reads the display's ID over DDC, which
# shows the real display even while an override is loaded, and loads
# that display's file or resets to its own EDID. Unplugging resets too,
# so the next display never starts on the wrong one.
sudo tee "$HDMI_HOTPLUG" > /dev/null << 'EOF'
#!/bin/bash
# Steamify CachyOS, HDMI refresh boost: load a display's saved EDID only
# while it is connected. Runs as root (debugfs).
FW=/usr/lib/firmware/edid RUN=/run/steamify-edid
modprobe i2c-dev 2>/dev/null
mkdir -p "$RUN"
# Hotplug events come in bursts, and the link needs a moment before DDC works.
sleep 1
for c in /sys/class/drm/card*-HDMI-A-*; do
[ -e "$c" ] || continue
out="${c##*/}"; out="${out#card*-}"
new=reset id=""
if [ "$(cat "$c/status")" = connected ]; then
bus="$(basename "$(readlink -f "$c/ddc")")"
for i in 1 2 3 4 5; do
# Bytes 8-17 of block 0: manufacturer, product, serial, date.
id="$(i2ctransfer -y "${bus#i2c-}" w1@0x50 0 r18@0x50 2>/dev/null | tr -d ' ' | sed 's/0x//g')"
[ ${#id} -eq 36 ] && break
sleep 1
done
[ ${#id} -eq 36 ] && [ -f "$FW/steamify-${id:16}.bin" ] && new="${id:16}"
fi
[ "$(cat "$RUN/$out" 2>/dev/null || echo reset)" = "$new" ] && continue
for d in /sys/kernel/debug/dri/*/"$out"; do [ -e "$d/edid_override" ] && break; done
[ -e "$d/edid_override" ] || continue
if [ "$new" = reset ]; then printf reset > "$d/edid_override"
else cat "$FW/steamify-$new.bin" > "$d/edid_override"; fi
# Before the hotplug below, whose own event runs this again.
echo "$new" > "$RUN/$out"
echo 1 > "$d/trigger_hotplug"
done
exit 0
EOF
sudo chmod 755 "$HDMI_HOTPLUG"
# StartLimitIntervalSec=0: a burst of hotplugs must not get it rate-limited.
sudo tee "$HDMI_UNIT" > /dev/null << EOF
[Unit]
Description=Steamify CachyOS: HDMI refresh boost EDID for the connected display
StartLimitIntervalSec=0
Wants=sys-kernel-debug.mount
After=sys-kernel-debug.mount
Before=display-manager.service sddm.service plasmalogin.service
[Service]
Type=oneshot
ExecStart=$HDMI_HOTPLUG
[Install]
WantedBy=graphical.target
EOF
# restart, not start: a hotplug during a run must still be looked at.
printf '%s\n' '# Written by steamify: HDMI refresh boost, check the display at every hotplug.' \
"ACTION==\"change\", SUBSYSTEM==\"drm\", ENV{HOTPLUG}==\"1\", RUN+=\"/usr/bin/systemctl --no-block restart $HDMI_UNIT_NAME\"" |
sudo tee "$HDMI_UDEV_RULE" >/dev/null || return 1
sudo systemctl daemon-reload
sudo udevadm control --reload
sudo systemctl enable "$HDMI_UNIT_NAME" >/dev/null 2>&1 || { err "Enabling $HDMI_UNIT_NAME failed."; return 1; }
sudo systemctl restart "$HDMI_UNIT_NAME"
}
hdmi_remove_hotplug() {
local conn local conn
sudo rm -f "$HDMI_FW_DIR"/steamify-*.bin
sudo systemctl disable "$HDMI_UNIT_NAME" >/dev/null 2>&1 sudo systemctl disable "$HDMI_UNIT_NAME" >/dev/null 2>&1
sudo rm -f "$HDMI_UDEV_RULE" "$HDMI_UNIT" "$HDMI_HOTPLUG" "$HDMI_MAP" sudo rm -f "$HDMI_UDEV_RULE" "$HDMI_UNIT" "$HDMI_HOTPLUG" "$HDMI_MAP"
sudo rm -rf "$HDMI_RUN" sudo rm -rf "$HDMI_RUN"
sudo systemctl daemon-reload sudo systemctl daemon-reload
sudo udevadm control --reload sudo udevadm control --reload
for conn in $(hdmi_connectors); do hdmi_override_live "$conn" reset 2>/dev/null; done for conn in $(hdmi_connectors); do hdmi_reset_live "$conn" 2>/dev/null; done
return 0 return 0
} }
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[*]}"
}
# --- 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() {
local -a HDMI_RESULTS=() err "HDMI refresh boost is no longer offered: current kernels run HDMI displays at their full rate."
if [[ -n "$(hdmi_cmdline_param)" ]]; then return 1
info "Moving the EDID override off the kernel command line, so it only applies to its display..."
hdmi_migrate && hdmi_install_hotplug || { err "Moving it failed."; return 1; }
ok "HDMI refresh boost now only applies to the display it was set up for."
return 0
fi
if hdmi_status; then
ok "Already set up for the connected display. Untick and tick it again to re-test."
return 0
fi
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
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
sudo pacman -S --needed --noconfirm i2c-tools python >/dev/null 2>&1 || { err "Installing i2c-tools failed."; return 1; }
local tmp conn out res saved=false rc=0
local -a smode
tmp="$(mktemp -d)"
if [[ ${#HDMI_CHOICE[@]} -gt 0 ]]; then
# Picked and tested in the app.
hdmi_choose "$tmp" || rc=1
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%%|*}"; read -ra smode <<< "${res#*|}"
hdmi_save "$tmp/$out.final" "${smode[0]}x${smode[1]}" "${smode[*]:2}" && saved=true || rc=1
done
rm -rf "$tmp" "$HDMI_CACHE"
[[ "$saved" == true ]] || return $rc
if ! hdmi_install_hotplug; then
# Nothing half-done left behind; the live EDID lasts until a restart.
hdmi_remove_hotplug
err "Making it permanent failed; the display is back on its own EDID."
return 1
fi
ok "HDMI refresh boost is saved for this display; other displays keep their own settings."
return $rc
} }
hdmi_disable() { hdmi_disable() {
# Everything, for every saved display: newer kernels read the whole EDID hdmi_forget_all
# themselves, and an old one loaded at the next hotplug would be wrong.
hdmi_forget all
hdmi_remove_boot_param || return 1 hdmi_remove_boot_param || return 1
state_clear hdmi state_clear hdmi
ok "HDMI refresh boost removed; displays use their own EDID again." ok "HDMI refresh boost removed; displays use their own EDID again."
+23 -18
View File
@@ -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 poweroff kpin hdmi bios) COMPONENTS=(gaming boot theme glyphs single launcher vram cec machine poweroff 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)
@@ -26,7 +26,7 @@ NO_PRESELECT=(boot cec kpin hdmi)
FEATURE_BASELINE=2.1.0 FEATURE_BASELINE=2.1.0
declare -A FEATURE_VERSION=( declare -A FEATURE_VERSION=(
[gaming]=2.1.0 [boot]=2.1.0 [theme]=2.1.0 [glyphs]=2.1.0 [single]=2.1.0 [gaming]=2.1.0 [boot]=2.1.0 [theme]=2.1.0 [glyphs]=2.1.0 [single]=2.1.0
[launcher]=2.1.0 [cec]=2.1.0 [machine]=2.2.0 [poweroff]=2.2.0 [launcher]=2.1.0 [cec]=2.1.0 [machine]=2.2.0 [poweroff]=2.2.0 [vram]=2.3.0
[kpin]=2.1.0 [hdmi]=2.1.0 [kpin]=2.1.0 [hdmi]=2.1.0
) )
@@ -37,6 +37,7 @@ declare -A LABEL=(
[glyphs]="Install Steam Deck/Machine icons: Deck button icons in gaming mode" [glyphs]="Install Steam Deck/Machine icons: Deck button icons in gaming mode"
[single]="Single user mode: no password, lock screen or log out (SDDM)" [single]="Single user mode: no password, lock screen or log out (SDDM)"
[launcher]="Steamify shortcut: the app on the desktop, Steamify Terminal in the launcher" [launcher]="Steamify shortcut: the app on the desktop, Steamify Terminal in the launcher"
[vram]="VRAM booster: the game in front keeps its VRAM, background apps make room"
[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"
[poweroff]="Power-off fix: the Steam Machine stays off after shutting down" [poweroff]="Power-off fix: the Steam Machine stays off after shutting down"
@@ -49,6 +50,7 @@ declare -A CURRENT WANTED
component_available() { component_available() {
case "$1" in case "$1" in
machine|poweroff) machine_available ;; machine|poweroff) machine_available ;;
vram) vram_available ;;
kpin) kpin_available ;; kpin) kpin_available ;;
hdmi) hdmi_available ;; hdmi) hdmi_available ;;
bios) bios_available ;; bios) bios_available ;;
@@ -89,20 +91,25 @@ feature_outdated() {
menu_label() { menu_label() {
# The label, with "(update)" after the name when a newer version of it # The label, with "(update)" after the name when a newer version of it
# will be applied. # will be applied, "(new)" for a default sub-option added since the
local l="${LABEL[$1]}" # last run.
if feature_outdated "$1"; then local l="${LABEL[$1]}" b=""
if [[ "$l" == *:* ]]; then l="${l%%:*} ${c_yellow}(update)${c_reset}:${l#*:}" if feature_outdated "$1"; then b="${c_yellow}(update)${c_reset}"
else l+=" ${c_yellow}(update)${c_reset}"; fi elif feature_new "$1"; then b="${c_green}(new)${c_reset}"; fi
if [[ -n "$b" ]]; then
if [[ "$l" == *:* ]]; then l="${l%%:*} $b:${l#*:}"
else l+=" $b"; fi
fi fi
printf '%s' "$l" printf '%s' "$l"
} }
feature_new() { feature_new() {
# A default sub-option added after its parent was set up (e.g. the # A default option added after its parent was set up (e.g. the power-off
# power-off fix under Steam Machine support): never turned on or off. # fix under Steam Machine support; a top-level one counts the SteamOS
[[ -n "${PARENT[$1]:-}" && "${CURRENT[${PARENT[$1]}]:-0}" == 1 && "${CURRENT[$1]:-0}" == 0 ]] && # conversion as its parent): never turned on or off.
! is_action "$1" && [[ " ${NO_PRESELECT[*]} " != *" $1 "* ]] && local p="${PARENT[$1]:-gaming}"
[[ "$1" != gaming && "${CURRENT[$p]:-0}" == 1 && "${CURRENT[$1]:-0}" == 0 ]] &&
! is_action "$1" && component_selectable "$1" && [[ " ${NO_PRESELECT[*]} " != *" $1 "* ]] &&
[[ -z "$(state_get features "$1")" ]] [[ -z "$(state_get features "$1")" ]]
} }
@@ -110,6 +117,7 @@ component_selectable() {
# Greyed out and not tickable when it has nothing to do. # Greyed out and not tickable when it has nothing to do.
case "$1" in case "$1" in
bios) bios_selectable ;; bios) bios_selectable ;;
vram) vram_selectable ;;
esac esac
} }
@@ -138,10 +146,12 @@ detect_components() {
launcher_repair && WANTED[launcher]=1 launcher_repair && WANTED[launcher]=1
# Shows the current and newest BIOS version. # Shows the current and newest BIOS version.
bios_available && { bios_lookup_newest; LABEL[bios]="$(bios_label)"; } bios_available && { bios_lookup_newest; LABEL[bios]="$(bios_label)"; }
# Greyed out with an NVIDIA card: say why.
component_available vram && ! vram_selectable && LABEL[vram]="VRAM booster: not supported by NVIDIA's driver yet"
# First run: preselect the full SteamOS experience (never an action). # First run: preselect the full SteamOS experience (never an action).
if [[ "$any" == false ]]; then if [[ "$any" == false ]]; then
for c in "${COMPONENTS[@]}"; do for c in "${COMPONENTS[@]}"; do
component_available "$c" && ! is_action "$c" && component_available "$c" && component_selectable "$c" && ! is_action "$c" &&
[[ " ${NO_PRESELECT[*]} " != *" $c "* ]] && WANTED[$c]=1 [[ " ${NO_PRESELECT[*]} " != *" $c "* ]] && WANTED[$c]=1
done done
machine_available && WANTED[cec]=1 machine_available && WANTED[cec]=1
@@ -160,13 +170,8 @@ toggle_component() {
# The power-off fix is opt-out: ticked along with Steam Machine support. # The power-off fix is opt-out: ticked along with Steam Machine support.
if [[ "$c" == machine ]]; then WANTED[poweroff]=${WANTED[machine]}; fi if [[ "$c" == machine ]]; then WANTED[poweroff]=${WANTED[machine]}; fi
if [[ "$c" == poweroff && "${WANTED[poweroff]}" == 1 ]]; then WANTED[machine]=1; fi if [[ "$c" == poweroff && "${WANTED[poweroff]}" == 1 ]]; then WANTED[machine]=1; fi
if [[ "$c" == machine && "${WANTED[machine]}" == 0 ]]; then WANTED[kpin]=0; fi if [[ "$c" == machine && "${WANTED[machine]}" == 0 ]]; then WANTED[kpin]=0; WANTED[bios]=0; fi
if [[ "$c" == kpin && "${WANTED[kpin]}" == 1 ]]; then WANTED[machine]=1; fi if [[ "$c" == kpin && "${WANTED[kpin]}" == 1 ]]; then WANTED[machine]=1; fi
# HDMI refresh boost and the BIOS update sit under Steam Machine support;
# the boost also needs the pinned kernel (newer kernels don't need it).
if [[ "$c" == machine && "${WANTED[machine]}" == 0 ]]; then WANTED[hdmi]=0; WANTED[bios]=0; fi
if [[ "$c" == kpin && "${WANTED[kpin]}" == 0 ]]; then WANTED[hdmi]=0; fi
if [[ "$c" == hdmi && "${WANTED[hdmi]}" == 1 ]]; then WANTED[machine]=1; WANTED[kpin]=1; fi
if [[ "$c" == bios && "${WANTED[bios]}" == 1 ]]; then WANTED[machine]=1; fi if [[ "$c" == bios && "${WANTED[bios]}" == 1 ]]; then WANTED[machine]=1; fi
} }
+2 -2
View File
@@ -167,9 +167,9 @@ kpin_enable() {
} }
kpin_disable() { kpin_disable() {
# Saved HDMI refresh boost EDIDs are for the pinned kernel only (newer # Saved HDMI refresh boost EDIDs were for the pinned kernel only (newer
# ones read the whole EDID), also those of displays not connected now. # ones read the whole EDID), also those of displays not connected now.
[[ -n "$(hdmi_saved)" ]] && hdmi_forget all hdmi_status && hdmi_forget_all
remove_kernel_pin remove_kernel_pin
} }
+78
View File
@@ -0,0 +1,78 @@
#!/bin/bash
# "VRAM booster" menu item (vram), ticked by default, like SteamOS 3.9's
# dGPU VRAM management: the game in front keeps its VRAM, background apps
# are evicted first, so a game that needs most of it doesn't spill into
# system RAM and stutter. The kernel side is the dmem cgroup controller
# (7.2; amdgpu and xe register their VRAM, NVIDIA's driver doesn't), so it's
# only offered when the kernel lists a VRAM region of at least 2 GB. CachyOS packages the userspace
# side: dmemcg-booster (a system service that enables the controller and
# sets the limits, plus a user service) and plasma-foreground-booster, which
# tells it which window is in front on the desktop. The latter only starts
# with "autostart" set in kcgroupsrc.
# Sourced by steamify.sh; not meant to be run on its own.
VRAM_PKGS=(dmemcg-booster plasma-foreground-booster)
VRAM_MIN_BYTES=$((2 * 1024 * 1024 * 1024))
vram_supported() {
# WIZARD_VRAM_FAKE_NVIDIA=1 (tests): as with an NVIDIA card.
[[ -n "${WIZARD_VRAM_FAKE_NVIDIA:-}" ]] && return 1
# Only for a GPU with its own VRAM: an integrated GPU registers a small
# carve-out too, but mostly uses system RAM, so there's little to boost.
awk -v min="$VRAM_MIN_BYTES" '$1 ~ /\/vram$/ && $2 >= min { found = 1 } END { exit !found }' \
/sys/fs/cgroup/dmem.capacity 2>/dev/null
}
vram_nvidia() {
# An NVIDIA display controller (PCI vendor 10de, class 03xxxx).
local d
[[ -n "${WIZARD_VRAM_FAKE_NVIDIA:-}" ]] && return 0
for d in /sys/bus/pci/devices/*; do
[[ "$(cat "$d/vendor" 2>/dev/null)" == 0x10de && "$(cat "$d/class" 2>/dev/null)" == 0x03* ]] && return 0
done
return 1
}
# Shown where it works, and greyed out with an NVIDIA card, whose driver
# doesn't register its VRAM with the kernel (yet): so it's clear why it
# can't be turned on. Hidden elsewhere (integrated graphics, older kernel).
vram_available() { vram_supported || vram_status || vram_nvidia; }
vram_selectable() { vram_supported || vram_status; }
vram_status() {
pacman -Q "${VRAM_PKGS[@]}" >/dev/null 2>&1 &&
systemctl is-enabled -q dmemcg-booster-system.service 2>/dev/null &&
systemctl --user is-enabled -q dmemcg-booster-user.service 2>/dev/null
}
vram_enable() {
local p missing=()
info "Installing the VRAM booster (the game in front keeps its VRAM)..."
for p in "${VRAM_PKGS[@]}"; do pacman -Q "$p" >/dev/null 2>&1 || missing+=("$p"); done
if ((${#missing[@]})); then
sudo pacman -S --needed --noconfirm "${missing[@]}" ||
{ err "Installing ${missing[*]} failed."; return 1; }
state_set vram installed_pkgs "${missing[*]}"
fi
sudo systemctl enable --now dmemcg-booster-system.service >/dev/null 2>&1 ||
{ err "Starting dmemcg-booster failed."; return 1; }
systemctl --user enable --now dmemcg-booster-user.service >/dev/null 2>&1 ||
{ err "Starting dmemcg-booster for $USER failed."; return 1; }
kset vram kcgroupsrc "Foreground Booster" autostart true
# Started by the Plasma session from now on; start it in this one too.
[[ "${XDG_CURRENT_DESKTOP:-}" == KDE ]] &&
systemctl --user start plasma-foreground-booster.service >/dev/null 2>&1
ok "VRAM booster on: the game in front keeps its VRAM."
}
vram_disable() {
local pkgs
systemctl --user disable --now plasma-foreground-booster.service dmemcg-booster-user.service >/dev/null 2>&1
sudo systemctl disable --now dmemcg-booster-system.service >/dev/null 2>&1
krevert vram
pkgs="$(state_get vram installed_pkgs)"
# shellcheck disable=SC2086 # a list of package names
[[ -n "$pkgs" ]] && sudo pacman -Rns --noconfirm $pkgs >/dev/null 2>&1
state_clear vram
ok "VRAM booster off."
}
+5 -3
View File
@@ -15,11 +15,11 @@
set -uo pipefail set -uo pipefail
# Release version, see CHANGELOG.md. # Release version, see CHANGELOG.md.
VERSION=2.2.0 VERSION=2.4.1
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 fremont-poweroff hdmi-refresh cec boot-session vapor-theme steamos-extras bios desktop-shortcut wizard-shortcut menu backend; do for lib in common state packages login-manager single-user steam-desktop steam-machine fremont-poweroff vram-booster hdmi-refresh cec boot-session vapor-theme steamos-extras bios desktop-shortcut wizard-shortcut menu backend; do
# shellcheck source=/dev/null # shellcheck source=/dev/null
source "$SCRIPT_DIR/lib/$lib.sh" source "$SCRIPT_DIR/lib/$lib.sh"
done done
@@ -140,7 +140,9 @@ while true; do
if [[ "$c" == boot ]]; then echo " - boot into: desktop (from the next boot)" if [[ "$c" == boot ]]; then echo " - boot into: desktop (from the next boot)"
elif is_action "$c"; then echo " - run: ${LABEL[$c]%%:*} at your own risk (checks, then asks twice more)" elif is_action "$c"; then echo " - run: ${LABEL[$c]%%:*} at your own risk (checks, then asks twice more)"
elif feature_outdated "$c"; then echo " - update: ${LABEL[$c]} (changed in this version)" elif feature_outdated "$c"; then echo " - update: ${LABEL[$c]} (changed in this version)"
elif [[ "${CURRENT[$c]}" == 1 ]]; then echo " - re-apply: ${LABEL[$c]}"; else echo " - turn on: ${LABEL[$c]}"; fi elif [[ "${CURRENT[$c]}" == 1 ]]; then echo " - re-apply: ${LABEL[$c]}"
elif feature_new "$c"; then echo " - turn on: ${LABEL[$c]} (new in this version)"
else echo " - turn on: ${LABEL[$c]}"; fi
done done
ask_yn "Go ahead?" y || { info "Nothing changed."; continue; } ask_yn "Go ahead?" y || { info "Nothing changed."; continue; }
+265
View File
@@ -0,0 +1,265 @@
// State and logic of the app: menu -> review -> password -> applying -> done,
// and the BIOS update (check -> warning 1 -> warning 2 -> flash). The
// screens only show this and call its functions.
import QtQuick
Item {
id: app
required property var backend
required property var gamepad
property string screen: "menu" // menu review password applying done bios1 bios2
property double remoteAt: 0
property var want: ({})
property string boot: "gamescope"
property int sel: 0 // row index; rows.length = the Apply button
property bool reapply: false
property var plan: [] // [{id, action, isNew}]
property var steps: ({}) // id -> "wait"|"run"|"ok"|"fail"
property var failed: []
property bool restartNeeded: false
property string runError: ""
property bool wrongPassword: false
property bool typing: false // a text field has the keys
property int doneCount: 0
property string pending: "apply" // what the password is for: apply | bios
property string sessionPassword: "" // kept between the BIOS steps only
property var biosInfo: ({})
property bool biosRun: false
property bool biosChecking: false // the invisible check before the warnings
property int biosFocus: 0 // warning 1: 0 = Cancel, 1 = continue
property real holdProgress: 0 // warning 2: hold A/OK for 5 s
property bool holding: false
readonly property alias log: logModel
readonly property var status: backend.status || ({})
readonly property var items: status.items || []
readonly property var rows: items.filter(function (i) { return !i.parent || want[i.parent]; })
readonly property var bios: status.bios || null
signal passwordRequested()
signal passwordRejected()
signal passwordSubmitRequested() // A/OK on the password screen
ListModel { id: logModel }
function nowOn(id) {
for (var i = 0; i < items.length; i++) if (items[i].id === id) return items[i].on;
return false;
}
function label(item) { return Texts.label(item.id, item.label); }
function biosHint() {
if (!bios) return "";
if (!bios.newest) return "Now " + bios.current + ", newest unknown (offline?)";
if (bios.current === bios.newest) return bios.current + " is up to date";
return "Now " + bios.current + ", newest " + bios.newest + " · at your own risk";
}
function syncFromStatus() {
reapply = false;
var w = {};
for (var i = 0; i < items.length; i++) w[items[i].id] = items[i].kind === "action" ? false : items[i].wanted;
want = w;
boot = nowOn("boot") ? "desktop" : "gamescope";
if (sel > rows.length) sel = 0;
}
Connections { target: backend; function onStatusChanged() { if (app.screen === "menu" || app.screen === "done") app.syncFromStatus(); } }
// --- Menu ---
function toggle(id) {
var cur = items.find(function (i) { return i.id === id; });
if (cur && cur.kind === "toggle" && cur.selectable === false) return;
var w = Object.assign({}, want);
w[id] = !w[id];
// The same rules as the terminal menu (toggle_component).
if (id === "gaming" && !w.gaming) w.single = false;
if (id === "single" && w.single) w.gaming = true;
if (id === "machine") w.poweroff = w.machine;
if (id === "poweroff" && w.poweroff) w.machine = true;
if (id === "machine" && !w.machine) w.kpin = false;
if (id === "kpin" && w.kpin) w.machine = true;
want = w;
}
function computePlan() {
var p = [];
for (var i = 0; i < items.length; i++) {
var it = items[i];
if (it.kind !== "toggle") continue;
if (it.parent && !want[it.parent]) { if (it.on) p.push({ id: it.id, action: "off" }); continue; }
if (want[it.id] && !it.on) p.push({ id: it.id, action: "on", isNew: !!it["new"] });
else if (!want[it.id] && it.on) p.push({ id: it.id, action: "off" });
else if (want[it.id] && (reapply || it.update)) p.push({ id: it.id, action: reapply ? "again" : "update" });
}
var bootNow = nowOn("boot") ? "desktop" : "gamescope";
if (want.gaming && boot !== bootNow) p.push({ id: "boot", action: boot === "desktop" ? "desktop" : "gaming" });
return p;
}
function goReview(again) { reapply = again; plan = computePlan(); screen = "review"; }
function wantedIds() {
var ids = [];
for (var i = 0; i < items.length; i++)
if (items[i].kind === "toggle" && want[items[i].id] && (!items[i].parent || want[items[i].parent])) ids.push(items[i].id);
return ids;
}
// --- Password and apply ---
function resetRun() { steps = {}; failed = []; restartNeeded = false; runError = ""; doneCount = 0; logModel.clear(); }
function startApply(password) {
resetRun();
var s = {};
for (var i = 0; i < plan.length; i++) s[plan[i].id] = "wait";
steps = s;
screen = "applying";
backend.apply(wantedIds(), want.gaming ? boot : "", reapply, password || "");
}
function askPassword(forWhat) {
pending = forWhat;
if (!backend.needsPassword()) return false;
wrongPassword = false; screen = "password"; passwordRequested();
return true;
}
function onApplyPressed() {
if (plan.length === 0) return;
if (!askPassword("apply")) startApply("");
}
function submitPassword(p) {
if (!backend.checkPassword(p)) { wrongPassword = true; passwordRejected(); return; }
if (pending === "bios") { sessionPassword = p; biosCheck(); }
else startApply(p);
}
function passwordBack() { screen = pending === "bios" ? "menu" : "review"; }
// --- BIOS ---
function startBios() {
if (!bios || !bios.selectable) return;
biosInfo = {}; sessionPassword = "";
if (!askPassword("bios")) biosCheck();
}
function biosCheck() {
// Invisible: stay on the menu (the BIOS row says "Checking…"),
// then the first warning.
failed = []; runError = ""; logModel.clear();
plan = [{ id: "bios", action: "check" }];
biosRun = true; biosChecking = true; screen = "menu";
backend.biosPrepare(sessionPassword);
}
function biosCancel() { sessionPassword = ""; holdProgress = 0; holding = false; biosRun = false; syncFromStatus(); screen = "menu"; }
function biosContinue() { holdProgress = 0; screen = "bios2"; }
function biosFlash() {
holdProgress = 0; holding = false;
resetRun();
plan = [{ id: "bios", action: "flash" }];
steps = { bios: "wait" };
biosRun = true; screen = "applying";
backend.biosFlash(sessionPassword);
}
Timer {
interval: 50; repeat: true; running: app.screen === "bios2" && app.holding
onTriggered: { app.holdProgress = Math.min(1, app.holdProgress + 0.01); if (app.holdProgress >= 1) app.biosFlash(); }
}
function backToMenu() { biosRun = false; syncFromStatus(); screen = "menu"; }
// --- Backend events ---
function errorText(e) {
return e === "wrong-password" ? "The password didn't work." : (e === "start" ? "Steamify couldn't start the changes." : (e ? "sudo isn't available." : ""));
}
Connections {
target: app.backend
function onEvent(ev) {
var s = Object.assign({}, app.steps);
if (ev.event === "start") { s[ev.id] = "run"; app.steps = s; }
else if (ev.event === "done") { s[ev.id] = ev.ok ? "ok" : "fail"; app.steps = s; app.doneCount++; }
else if (ev.event === "log") { logModel.append({ line: ev.line }); if (logModel.count > 400) logModel.remove(0); }
else if (ev.event === "bios-ready") { app.biosChecking = false; app.biosInfo = ev; app.biosFocus = 0; app.holdProgress = 0; app.screen = "bios1"; }
else if (ev.event === "finished" && app.biosChecking) {
// The check found nothing to do or a problem: say so.
app.biosChecking = false; app.sessionPassword = "";
app.failed = ev.failed || []; app.restartNeeded = false;
app.runError = ev.nothing && !ev.error ? "The BIOS is already the newest version." : app.errorText(ev.error);
app.screen = "done";
}
else if (ev.event === "finished") {
// A successful BIOS check ends with bios-ready instead, keeping
// the password for the flash.
app.sessionPassword = "";
app.failed = ev.failed || []; app.restartNeeded = !!ev.restart;
app.runError = app.errorText(ev.error);
app.screen = "done";
}
}
function onRemotePressed() { app.remoteAt = Date.now(); Input.type = "remote"; }
}
// --- Input: one set of actions for controller, keyboard and remote ---
function act(a) {
if (screen === "menu") {
if (biosChecking) return;
if (a === "up") sel = Math.max(0, sel - 1);
else if (a === "down") sel = Math.min(rows.length, sel + 1);
else if (a === "accept") {
if (sel === rows.length) { goReview(false); return; }
var r = rows[sel];
if (r.kind === "action" && r.id === "bios") startBios();
else if (r.kind === "choice") boot = boot === "gamescope" ? "desktop" : "gamescope";
else if (r.kind === "toggle") toggle(r.id);
}
else if ((a === "left" || a === "right") && sel < rows.length && rows[sel].kind === "choice")
boot = a === "left" ? "gamescope" : "desktop";
else if (a === "apply") goReview(false);
else if (a === "reapply") goReview(true);
else if (a === "back") Qt.quit();
} else if (screen === "review") {
if (a === "accept" || a === "apply") onApplyPressed();
else if (a === "back") { reapply = false; screen = "menu"; }
} else if (screen === "password") {
if (a === "accept" || a === "apply") passwordSubmitRequested();
else if (a === "back") passwordBack();
} else if (screen === "bios1") {
if (a === "left") biosFocus = 0;
else if (a === "right") biosFocus = 1;
else if (a === "accept") { if (biosFocus === 1) biosContinue(); else biosCancel(); }
else if (a === "back") biosCancel();
} else if (screen === "bios2") {
if (a === "back") biosCancel();
else if (a === "hold") holding = true;
else if (a === "release") { holding = false; if (holdProgress < 1) holdProgress = 0; }
} else if (screen === "done") {
if (a === "accept" && restartNeeded) backend.restart();
else if (a === "back" || a === "accept") backToMenu();
}
}
Connections {
target: app.gamepad
function onButton(b) {
if (b === "a_up") { if (app.screen === "bios2") app.act("release"); return; }
if (b.slice(-3) === "_up") return;
Input.type = "controller";
if (b === "a" && app.screen === "bios2") { app.act("hold"); return; }
var map = { a: "accept", b: "back", x: "apply", y: "reapply", start: "apply", up: "up", down: "down", left: "left", right: "right" };
if (map[b]) app.act(map[b]);
}
}
function keyPressed(e) {
// A text field handles its own keys, except Escape and Enter.
if (typing && e.key !== Qt.Key_Escape && e.key !== Qt.Key_Return && e.key !== Qt.Key_Enter) return;
var fromRemote = Date.now() - remoteAt < 400;
if (screen === "bios2" && Input.type !== "keyboard" && (e.key === Qt.Key_Return || e.key === Qt.Key_Enter)) {
if (!e.isAutoRepeat) act("hold"); e.accepted = true; return;
}
if (!fromRemote) Input.type = "keyboard";
var k = e.key;
if (k === Qt.Key_Up) act("up");
else if (k === Qt.Key_Down) act("down");
else if (k === Qt.Key_Left) act("left");
else if (k === Qt.Key_Right) act("right");
else if (k === Qt.Key_Space) act("accept");
else if (k === Qt.Key_Return || k === Qt.Key_Enter) act(Input.type === "remote" || screen !== "menu" ? "accept" : "apply");
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 return;
e.accepted = true;
}
function keyReleased(e) {
if (screen === "bios2" && !e.isAutoRepeat && (e.key === Qt.Key_Return || e.key === Qt.Key_Enter)) act("release");
}
}
+60
View File
@@ -0,0 +1,60 @@
// Progress: every step of the plan on the left, the log on the right.
import QtQuick
import QtQuick.Controls.Basic
Item {
required property AppState app
readonly property bool checking: app.biosRun && app.plan.length > 0 && app.plan[0].action === "check"
Column {
x: 40; y: 32; width: 560; spacing: 18
Text { text: app.biosRun ? (checking ? "Checking the BIOS update" : "Installing the BIOS update") : "Applying your changes"
color: Theme.text; font.family: Theme.display; font.pixelSize: 34; font.weight: Font.DemiBold }
Rectangle { width: 560; height: 10; radius: 5; color: Theme.line
Rectangle { height: 10; radius: 5; color: Theme.accent; width: app.plan.length ? parent.width * Math.min(1, (app.doneCount + 0.3) / app.plan.length) : 0
Behavior on width { NumberAnimation { duration: 300 } } } }
Row { width: 560
Text { text: "Step " + Math.min(app.plan.length, app.doneCount + 1) + " of " + app.plan.length; color: Theme.mute; font.family: Theme.body; font.pixelSize: 14; width: 280 }
Text { text: "Don't turn off the PC"; color: Theme.mute; font.family: Theme.body; font.pixelSize: 14; width: 280; horizontalAlignment: Text.AlignRight } }
ListView {
width: 560; clip: true; spacing: 8
height: parent.parent.height - 32 - 156 - 32
model: app.plan
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
// Follow the progress: the running step stays in view.
readonly property int running: app.plan.findIndex(function (p) { return app.steps[p.id] === "run"; })
onRunningChanged: if (running >= 0) positionViewAtIndex(running, ListView.Contain)
delegate: Rectangle {
id: step
required property var modelData
readonly property string st: app.steps[modelData.id] || "wait"
width: 560; height: 54; radius: 12; color: st === "run" ? Theme.cardSel : Theme.card
border.width: st === "run" ? 2 : 0; border.color: Theme.accent
Row { anchors.fill: parent; anchors.leftMargin: 18; anchors.rightMargin: 18; spacing: 14
Rectangle { width: 22; height: 22; radius: 11; anchors.verticalCenter: parent.verticalCenter
color: step.st === "ok" ? Theme.goodBg : (step.st === "fail" ? Theme.badBg : "transparent")
border.width: step.st === "wait" || step.st === "run" ? 2 : 0; border.color: step.st === "run" ? Theme.accent : "#343f50"
Text { anchors.centerIn: parent; text: step.st === "ok" ? "✓" : (step.st === "fail" ? "!" : ""); color: step.st === "ok" ? Theme.good : Theme.bad
font.pixelSize: 13; font.weight: Font.Bold }
RotationAnimator on rotation { running: step.st === "run"; from: 0; to: 360; duration: 1000; loops: Animation.Infinite } }
Row { width: 380; anchors.verticalCenter: parent.verticalCenter; spacing: 10
Text { text: (Texts.actions[step.modelData.action] || [])[3] + Texts.label(step.modelData.id)
color: step.st === "wait" ? Theme.faint : Theme.textHi; font.family: Theme.body; font.pixelSize: 16; font.weight: Font.DemiBold
anchors.verticalCenter: parent.verticalCenter; elide: Text.ElideRight
width: Math.min(implicitWidth, parent.width - (badge.visible ? badge.width + parent.spacing : 0)) }
Badge { id: badge; update: step.modelData.action === "update"; isNew: !!step.modelData.isNew; anchors.verticalCenter: parent.verticalCenter } }
Text { text: ({ wait: "Waiting", run: "Working…", ok: "Done", fail: "Problem" })[step.st]
color: step.st === "ok" ? Theme.good : (step.st === "fail" ? Theme.bad : Theme.label); font.family: Theme.body; font.pixelSize: 13
anchors.verticalCenter: parent.verticalCenter }
}
}
}
}
Column {
x: 632; y: 32; width: parent.width - 672; height: parent.height - 64; spacing: 10
Text { text: "DETAILS"; color: Theme.faint; font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.8 }
Rectangle { width: parent.width; height: parent.height - 30; radius: 16; color: "#0b0e13"; border.width: 1; border.color: "#1d2531"
ListView { id: logView; anchors.fill: parent; anchors.margins: 18; clip: true; model: app.log
onCountChanged: positionViewAtEnd()
delegate: Text { required property string line; width: logView.width; text: line; color: "#aab5c4"; font.family: Theme.mono; font.pixelSize: 13; wrapMode: Text.WrapAnywhere } } }
}
}
+13
View File
@@ -0,0 +1,13 @@
// "Update" (a newer version of it will be applied) or "New" (a default
// option added since the last run), after an item's name.
import QtQuick
Chip {
property bool update: false
property bool isNew: false
visible: update || isNew
height: 20
text: update ? "Update" : "New"
fg: update ? Theme.warn : Theme.good
bgc: update ? Theme.warnBg : Theme.goodBg
}
+7
View File
@@ -0,0 +1,7 @@
// The bar at the bottom of a screen, for its buttons.
import QtQuick
Rectangle {
anchors.bottom: parent.bottom; width: parent.width; height: 64; color: Theme.bar
Rectangle { width: parent.width; height: 1; color: Theme.line }
}
+36
View File
@@ -0,0 +1,36 @@
// BIOS update, warning 2 (last chance): type UPDATE on a keyboard, or hold
// A/OK for 5 seconds on a controller or remote.
import QtQuick
import QtQuick.Controls.Basic
Item {
id: screen
required property AppState app
onVisibleChanged: if (visible) { typed.text = ""; if (Input.type === "keyboard") typed.forceActiveFocus(); }
Binding { target: screen.app; property: "typing"; value: typed.activeFocus; when: screen.visible }
Rectangle {
anchors.centerIn: parent; width: 760; height: col.implicitHeight + 64; radius: 20; color: Theme.dangerBg; border.width: 2; border.color: Theme.dangerLine
Column { id: col; anchors.fill: parent; anchors.margins: 32; spacing: 18
Text { text: "Last chance: this flashes BIOS " + (app.biosInfo.newest || ""); color: "#ffd9d3"; font.family: Theme.display; font.pixelSize: 32; font.weight: Font.DemiBold }
Text { width: parent.width; wrapMode: Text.WordWrap; color: Theme.soft; font.family: Theme.body; font.pixelSize: 16; lineHeight: 1.3
text: "After this, keep the power on until the machine has fully started again. Don't touch it, even if the screen is black." }
Column { visible: Input.type === "keyboard"; width: parent.width; spacing: 8
Text { text: "Type UPDATE (in capitals) and press Enter to flash the BIOS:"; color: Theme.text; font.family: Theme.body; font.pixelSize: 15; font.weight: Font.DemiBold }
TextField { id: typed; width: 320; height: 50; color: Theme.textHi; font.family: Theme.mono; font.pixelSize: 20; leftPadding: 14
placeholderText: "UPDATE"; placeholderTextColor: "#6b5a5a"
background: Rectangle { radius: 10; color: Theme.bg; border.width: 2; border.color: typed.text === "UPDATE" ? Theme.bad : "#5a3a3a" }
Keys.onReturnPressed: if (text === "UPDATE") app.biosFlash()
Keys.onEnterPressed: if (text === "UPDATE") app.biosFlash()
Keys.onEscapePressed: app.biosCancel() } }
Column { visible: Input.type !== "keyboard"; width: parent.width; spacing: 10
Text { text: "Hold " + Input.g.ok + " for 5 seconds to flash the BIOS. Let go to stop."; color: Theme.text; font.family: Theme.body; font.pixelSize: 15; font.weight: Font.DemiBold }
Rectangle { width: parent.width; height: 14; radius: 7; color: "#3a2224"
Rectangle { height: 14; radius: 7; color: Theme.bad; width: parent.width * app.holdProgress } } }
Row { spacing: 12
Btn { k: Input.g.back; text: "Cancel"; onClicked: app.biosCancel() }
Text { visible: app.bios && app.bios.dryRun; text: "Dry run: nothing will be flashed."; color: Theme.warn; font.family: Theme.body; font.pixelSize: 14
anchors.verticalCenter: parent.verticalCenter } }
}
}
}
+37
View File
@@ -0,0 +1,37 @@
// BIOS update, warning 1: the risks and what was checked; Cancel is
// selected first.
import QtQuick
Item {
required property AppState app
Rectangle {
x: 40; y: 28; width: parent.width - 80; height: parent.height - 28 - 64 - 24; radius: 16; color: Theme.dangerBg; border.width: 2; border.color: Theme.dangerLine
Row { anchors.fill: parent; anchors.margins: 32; spacing: 40
Column { width: 640; spacing: 16
Row { spacing: 12
Rectangle { width: 44; height: 44; radius: 12; color: "#3a1c1f"
Text { anchors.centerIn: parent; text: "!"; color: Theme.bad; font.pixelSize: 26; font.weight: Font.Bold } }
Text { text: "BIOS update – entirely at your own risk"; color: "#ffd9d3"; font.family: Theme.display; font.pixelSize: 30; font.weight: Font.DemiBold
anchors.verticalCenter: parent.verticalCenter } }
RiskList { width: 640 }
}
Rectangle { width: 440; height: 250; radius: 14; color: Theme.card
Column { anchors.fill: parent; anchors.margins: 22; spacing: 12
Repeater { model: [["Current BIOS", app.biosInfo.current || ""], ["New BIOS", app.biosInfo.newest || ""], ["Checksum", "OK (Valve's package)"],
["Compatible", app.biosInfo.compatible === "yes" ? "Yes (checked by fwupd)" : "Not checked (dry run)"]]
Row { required property var modelData; width: 396
Text { text: parent.modelData[0]; color: Theme.faint; font.family: Theme.body; font.pixelSize: 15; width: 150 }
Text { text: parent.modelData[1]; color: parent.modelData[1].indexOf("Not") === 0 ? Theme.warn : Theme.text; font.family: Theme.mono; font.pixelSize: 15
width: 246; horizontalAlignment: Text.AlignRight; elide: Text.ElideLeft } } }
} }
}
}
Bar {
Text { anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; text: "Do you understand the risks and want to continue?"
color: Theme.soft; font.family: Theme.body; font.pixelSize: 15 }
Row { anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 12
Btn { k: app.biosFocus === 0 ? Input.g.ok : "◀"; text: "Cancel"; focusRing: app.biosFocus === 0; onClicked: app.biosCancel() }
Btn { k: app.biosFocus === 1 ? Input.g.ok : "▶"; text: "I understand, continue"; focusRing: app.biosFocus === 1; color: Theme.danger
onClicked: app.biosContinue() } }
}
}
+20
View File
@@ -0,0 +1,20 @@
// A button with the input's button name in front.
import QtQuick
Rectangle {
id: btn
property string k
property string text
property bool primary: false
property bool focusRing: false
signal clicked
height: 48; radius: 12; width: row.implicitWidth + 36
color: primary ? Theme.accent : Theme.button
border.width: focusRing ? 2 : 0; border.color: Theme.textHi
Row { id: row; anchors.centerIn: parent; spacing: 10
Glyph { k: btn.k; visible: btn.k !== ""; dark: btn.primary; anchors.verticalCenter: parent.verticalCenter }
Text { text: btn.text; color: btn.primary ? Theme.ink : Theme.text; font.family: Theme.body; font.pixelSize: 15; font.weight: Font.DemiBold
anchors.verticalCenter: parent.verticalCenter }
}
MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: btn.clicked() }
}
+12
View File
@@ -0,0 +1,12 @@
// A small label in capitals.
import QtQuick
Rectangle {
id: chip
property string text
property color fg: Theme.good
property color bgc: Theme.goodBg
height: 24; radius: 6; width: ct.implicitWidth + 16; color: bgc
Text { id: ct; anchors.centerIn: parent; text: chip.text.toUpperCase(); color: chip.fg
font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.6 }
}
+45
View File
@@ -0,0 +1,45 @@
// Right of the menu: what the selected item does, or a nudge to review
// when the Apply button is selected.
import QtQuick
import QtQuick.Controls.Basic
Rectangle {
id: detail
required property AppState app
readonly property var it: app.sel < app.rows.length ? app.rows[app.sel] : null
readonly property var tx: it ? Texts.of(it) : ({})
radius: 16; color: Theme.card
// Long texts scroll instead of running out of the card; each item
// starts at the top.
onItChanged: flick.contentY = 0
Flickable {
id: flick
anchors.fill: parent; anchors.margins: 24; anchors.rightMargin: 12
visible: detail.it !== null; clip: true
contentHeight: col.implicitHeight; boundsBehavior: Flickable.StopAtBounds
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
Column {
id: col; width: flick.width - 12; spacing: 14
Row { spacing: 10
Chip { readonly property var it: detail.it
text: !it ? "" : it.kind === "choice" ? (app.boot === "desktop" ? "Desktop" : "Gaming") : it.kind === "action" ? "Opt-in" : (it.on ? "On" : "Off")
fg: it && it.on ? Theme.good : Theme.label; bgc: it && it.on ? Theme.goodBg : Theme.line }
Chip { visible: !!detail.tx.experimental; text: "Experimental"; fg: Theme.warn; bgc: Theme.warnBg }
}
Text { text: detail.it ? app.label(detail.it) : ""; color: Theme.text; font.family: Theme.display; font.pixelSize: 26; font.weight: Font.DemiBold
wrapMode: Text.WordWrap; width: parent.width }
Text { text: detail.tx.body || ""; color: Theme.soft; font.family: Theme.body; font.pixelSize: 15; lineHeight: 1.4; wrapMode: Text.WordWrap; width: parent.width }
Text { text: "WHAT IT CHANGES"; color: Theme.faint; font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.8; topPadding: 4 }
Repeater { model: detail.tx.changes || []
Text { required property string modelData; text: "• " + modelData; color: Theme.soft; font.family: Theme.body; font.pixelSize: 14
wrapMode: Text.WordWrap; width: col.width } }
}
}
Column {
anchors.centerIn: parent; spacing: 8; visible: detail.it === null
Text { text: app.computePlan().length === 0 ? "Everything is the way you want it" : "Review what will change"; color: Theme.text
font.family: Theme.display; font.pixelSize: 24; anchors.horizontalCenter: parent.horizontalCenter }
Text { text: "then apply it"; color: Theme.mute; font.family: Theme.body; font.pixelSize: 15; anchors.horizontalCenter: parent.horizontalCenter }
}
}
+28
View File
@@ -0,0 +1,28 @@
// The result of a run: all done, a problem, or nothing changed.
import QtQuick
Item {
required property AppState app
readonly property bool problem: app.failed.length > 0 || app.runError !== ""
readonly property string message: {
if (app.runError) return app.runError;
if (!app.biosRun) return app.plan.length + (app.plan.length === 1 ? " change" : " changes") + " applied." + (app.restartNeeded ? " Some take effect after a restart." : "");
if (app.failed.length) return "The BIOS was not changed.";
return (app.bios && app.bios.dryRun ? "Dry run: nothing was flashed. " : "BIOS " + (app.biosInfo.newest || "") + " is staged and is written during the restart. ")
+ "Keep the power on and don't touch the machine until it has fully started again, even if the screen stays black.";
}
Column {
anchors.centerIn: parent; width: 760; spacing: 20
Rectangle { width: 72; height: 72; radius: 36; anchors.horizontalCenter: parent.horizontalCenter; color: problem ? Theme.warnBg : Theme.goodBg
Text { anchors.centerIn: parent; text: problem ? "!" : "✓"; color: problem ? Theme.warn : Theme.good; font.pixelSize: 36; font.weight: Font.Bold } }
Text { text: app.runError ? "Nothing changed" : (app.biosRun && app.failed.length ? "The BIOS update stopped" : (app.failed.length ? "Done, with a problem" : "All done"))
color: Theme.text; font.family: Theme.display; font.pixelSize: 40; font.weight: Font.DemiBold; anchors.horizontalCenter: parent.horizontalCenter }
Text { width: parent.width; horizontalAlignment: Text.AlignHCenter; wrapMode: Text.WordWrap; color: Theme.soft; font.family: Theme.body; font.pixelSize: 17; text: message }
Rectangle { visible: app.failed.length > 0; anchors.horizontalCenter: parent.horizontalCenter; width: ft.implicitWidth + 32; height: 44; radius: 12; color: Theme.warnBg
Text { id: ft; anchors.centerIn: parent; color: "#f2c27a"; font.family: Theme.body; font.pixelSize: 14
text: "Had a problem: " + app.failed.map(function (id) { return Texts.label(id); }).join(", ") + ". See the details." } }
Row { anchors.horizontalCenter: parent.horizontalCenter; spacing: 14; topPadding: 8
Btn { k: Input.g.back; text: "Back to the menu"; height: 52; onClicked: app.act("back") }
Btn { visible: app.restartNeeded; k: Input.g.ok; text: "Restart now"; primary: true; height: 52; onClicked: app.backend.restart() } }
}
}
+13
View File
@@ -0,0 +1,13 @@
// A button name (A, Enter, OK...), round for a controller's face buttons.
import QtQuick
Rectangle {
id: glyph
property string k
property bool dark: false
height: 26; width: Math.max(26, gt.implicitWidth + 14)
radius: Input.type === "controller" && k.length === 1 ? 13 : 7
color: dark ? Theme.ink : Theme.key
Text { id: gt; anchors.centerIn: parent; text: glyph.k; color: glyph.dark ? Theme.accent : Theme.textHi
font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold }
}
+25
View File
@@ -0,0 +1,25 @@
// Logo, the screen's name (the version on the menu) and the machine.
import QtQuick
Rectangle {
required property AppState app
required property string iconUrl
readonly property var titles: ({ review: "Review", password: "Password", applying: app.biosRun ? "BIOS update" : "Applying",
done: "Done", bios1: "BIOS update", bios2: "BIOS update" })
width: parent.width; height: 72; color: Theme.bar
Rectangle { anchors.bottom: parent.bottom; width: parent.width; height: 1; color: Theme.line }
Row {
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 }
Text { text: "Steamify"; color: Theme.text; font.family: Theme.display; font.pixelSize: 28; font.weight: Font.Bold; anchors.verticalCenter: parent.verticalCenter }
Text { readonly property bool menu: app.screen === "menu"
text: menu ? "v" + (app.status.version || "") : "/ " + titles[app.screen]
color: Theme.faint; font.family: menu ? Theme.mono : Theme.body; font.pixelSize: menu ? 13 : 15; anchors.verticalCenter: parent.verticalCenter }
}
Rectangle {
visible: app.status.steamMachine === true
anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter
height: 34; radius: 17; color: "#1b2330"; width: mt.implicitWidth + 28
Text { id: mt; anchors.centerIn: parent; text: "Steam Machine · CachyOS"; color: Theme.label; font.family: Theme.body; font.pixelSize: 14 }
}
}
+14
View File
@@ -0,0 +1,14 @@
// The input in use (controller, keyboard or remote) and the button names
// shown for it.
pragma Singleton
import QtQuick
QtObject {
property string type: "keyboard"
readonly property var glyphs: ({
controller: { toggle: "A", choose: "◀ ▶", reapply: "Y", quit: "B", apply: "X", ok: "A", back: "B" },
keyboard: { toggle: "Space", choose: "← →", reapply: "R", quit: "Esc", apply: "Enter", ok: "Enter", back: "Esc" },
remote: { toggle: "OK", choose: "◀ ▶", reapply: "Red", quit: "Back", apply: "↓ Apply", ok: "OK", back: "Back" }
})
readonly property var g: glyphs[type]
}
+24 -1097
View File
File diff suppressed because it is too large. Load diff
+80
View File
@@ -0,0 +1,80 @@
// One menu item: name, badge and hint on the left; its control (switch,
// Gaming/Desktop choice or action button) on the right. Sub-items fold in
// and out with their parent.
import QtQuick
Item {
id: row
required property var modelData
required property int index
required property AppState app
readonly property var it: modelData
readonly property bool shown: !it.parent || !!app.want[it.parent]
readonly property int rowIndex: app.rows.findIndex(function (r) { return r.id === it.id; })
readonly property bool selected: shown && rowIndex === app.sel
readonly property bool on: !!app.want[it.id]
// Greyed out: an action with nothing to do, or an option this machine
// can't turn on.
readonly property bool unavailable: (it.kind === "action" && !(app.bios && app.bios.selectable)) || (it.kind === "toggle" && it.selectable === false)
readonly property string hint: it.id === "bios" ? app.biosHint()
: it.selectable === false ? it.hint
: ((Texts.items[it.id] || {}).hint || it.hint)
height: shown ? 62 : 0
opacity: shown ? 1 : 0
clip: true
Behavior on height { NumberAnimation { duration: 160; easing.type: Easing.OutCubic } }
Behavior on opacity { NumberAnimation { duration: 160 } }
Rectangle {
width: parent.width; height: 56; radius: 12
color: row.selected ? Theme.cardSel : Theme.card
border.width: row.selected ? 2 : 0; border.color: Theme.accent
MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor
onClicked: { Input.type = "keyboard"; if (row.selected) app.act("accept"); else app.sel = row.rowIndex; } }
Text { visible: !!row.it.parent; x: 20; anchors.verticalCenter: parent.verticalCenter; text: "└"; color: "#56627a"; font.pixelSize: 18 }
Column {
anchors.verticalCenter: parent.verticalCenter; spacing: 2
x: row.it.parent ? 46 : 16
width: controls.x - x - 16
opacity: row.unavailable ? 0.6 : 1
Row { width: parent.width; spacing: 10
Text { text: app.label(row.it); color: Theme.textHi; font.family: Theme.body; font.pixelSize: 17; font.weight: Font.DemiBold; elide: Text.ElideRight
width: Math.min(implicitWidth, parent.width - (badge.visible ? badge.width + parent.spacing : 0)) }
Badge { id: badge; update: !!row.it.update; isNew: !!row.it["new"]; anchors.verticalCenter: parent.verticalCenter } }
Text { text: row.hint; color: Theme.mute; font.family: Theme.body; font.pixelSize: 13; elide: Text.ElideRight; width: parent.width }
}
// Every control ends on the same edge.
Item {
id: controls
opacity: row.it.kind === "toggle" && row.it.selectable === false ? 0.4 : 1
anchors.right: parent.right; anchors.rightMargin: 16; anchors.verticalCenter: parent.verticalCenter
width: 180; height: 32
// choice
Rectangle { visible: row.it.kind === "choice"; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter
width: 172; height: 32; radius: 10; color: Theme.bg
Row { anchors.centerIn: parent; spacing: 4
Repeater { model: [["gamescope", "Gaming"], ["desktop", "Desktop"]]
Rectangle { required property var modelData
readonly property bool picked: app.boot === modelData[0]
width: 80; height: 26; radius: 7; color: picked ? Theme.accent : "transparent"
Text { anchors.centerIn: parent; text: parent.modelData[1]; color: parent.picked ? Theme.ink : Theme.mute
font.family: Theme.body; font.pixelSize: 13; font.weight: Font.DemiBold }
MouseArea { anchors.fill: parent; onClicked: app.boot = parent.modelData[0] } } } } }
// toggle: "now on/off" left of the switch
Rectangle { id: sw; visible: row.it.kind === "toggle"; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter
width: 46; height: 26; radius: 13; color: row.on ? Theme.accent : "#343f50"
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 } } } }
Text { visible: row.it.kind === "toggle"; anchors.right: sw.left; anchors.rightMargin: 12; anchors.verticalCenter: parent.verticalCenter
text: row.it.on ? "now on" : "now off"; color: row.it.on ? Theme.good : Theme.faint; font.family: Theme.mono; font.pixelSize: 12 }
// action (Update BIOS)
Rectangle { visible: row.it.kind === "action"; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter
width: at.implicitWidth + 24; height: 30; radius: 8; color: Theme.warnBg
Text { id: at; anchors.centerIn: parent; color: Theme.warn; font.family: Theme.body; font.pixelSize: 13; font.weight: Font.DemiBold
text: app.biosChecking ? "Checking…" : app.bios && app.bios.selectable ? "Update…" : (app.bios && app.bios.newest ? "Up to date" : "Unavailable") } }
}
}
}
+76
View File
@@ -0,0 +1,76 @@
// The menu: every item on the left, what the selected one does and the
// system on the right, Review & apply at the bottom.
import QtQuick
import QtQuick.Controls.Basic
Item {
id: screen
required property AppState app
readonly property var g: Input.g
Text {
visible: app.items.length === 0
anchors.centerIn: parent; color: Theme.mute; font.family: Theme.body; font.pixelSize: 18
text: app.status.error ? "Couldn't read the current state:\n" + app.status.error : "Checking what's on…"
horizontalAlignment: Text.AlignHCenter
}
Item {
visible: app.items.length > 0
x: 40; y: 28; width: parent.width - 80; height: parent.height - 28 - 64 - 20
Column {
width: 740; height: parent.height; spacing: 10
Text { text: "What do you want?"; color: Theme.text; font.family: Theme.display; font.pixelSize: 30; font.weight: Font.DemiBold }
ListView {
id: list; width: parent.width; height: parent.height - 50; clip: true
// A fixed model: sub-items fold in and out instead of the
// list being rebuilt (and jumping) on every tick.
model: app.items
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
delegate: MenuRow {
app: screen.app
width: list.width - 12; x: 2
onSelectedChanged: if (selected) Qt.callLater(list.positionViewAtIndex, index, ListView.Contain)
}
}
}
Column {
x: 772; width: parent.width - 772; height: parent.height; spacing: 16
DetailPanel { app: screen.app; width: parent.width; height: parent.height - sys.height - 16 }
Rectangle {
id: sys; width: parent.width; height: 82; radius: 16; color: Theme.card
Grid { anchors.fill: parent; anchors.margins: 16; anchors.leftMargin: 20; anchors.rightMargin: 20; columns: 2; columnSpacing: 20; rowSpacing: 10
Repeater {
model: [["Kernel", (app.status.kernel || "").replace("-cachyos", "")],
["LED bar", app.status.steamMachine ? (app.status.leds || 0) + " LEDs" : "—"],
["HDMI-CEC", app.status.cecDevices ? "/dev/" + app.status.cecDevices.split(" ")[0] : "none"],
["Version", app.status.version || ""]]
Row { required property var modelData; width: (sys.width - 60) / 2
Text { text: parent.modelData[0]; color: Theme.faint; font.family: Theme.body; font.pixelSize: 13; width: parent.width / 2 }
Text { text: parent.modelData[1]; color: Theme.text; font.family: Theme.mono; font.pixelSize: 13; width: parent.width / 2
horizontalAlignment: Text.AlignRight; elide: Text.ElideLeft } }
}
}
}
}
}
Bar {
Row {
anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 28
Repeater {
model: [[screen.g.toggle, "Toggle"], [screen.g.choose, "Choose"], [screen.g.reapply, "Re-apply what's on"], [screen.g.quit, "Quit"]]
Row { required property var modelData; spacing: 8
Glyph { k: parent.modelData[0]; anchors.verticalCenter: parent.verticalCenter }
Text { text: parent.modelData[1]; color: Theme.label; font.family: Theme.body; font.pixelSize: 14; anchors.verticalCenter: parent.verticalCenter } }
}
}
Row {
anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 16
Text { readonly property int n: app.computePlan().length
text: n === 0 ? "Everything is the way you want it" : n + (n === 1 ? " change" : " changes")
color: Theme.faint; font.family: Theme.body; font.pixelSize: 14; anchors.verticalCenter: parent.verticalCenter }
Btn { k: screen.g.apply; text: "Review & apply"; primary: true; focusRing: app.sel === app.rows.length; height: 44; onClicked: app.goReview(false) }
}
}
}
+57
View File
@@ -0,0 +1,57 @@
// The sudo password, asked once per run; never stored.
import QtQuick
import QtQuick.Controls.Basic
Rectangle {
id: screen
required property AppState app
color: "#0c1016"
function submit() { var p = pw.text; pw.text = ""; app.submitPassword(p); }
Connections {
target: screen.app
function onPasswordRequested() { pw.text = ""; pw.shown = false; pw.forceActiveFocus(); }
function onPasswordRejected() { pw.forceActiveFocus(); }
function onPasswordSubmitRequested() { screen.submit(); }
}
Binding { target: screen.app; property: "typing"; value: pw.activeFocus; when: screen.visible }
Rectangle {
anchors.centerIn: parent; width: 560; height: col.implicitHeight + 72; radius: 20; color: Theme.card; border.width: 1; border.color: Theme.line
Column {
id: col; anchors.fill: parent; anchors.margins: 36; anchors.leftMargin: 40; anchors.rightMargin: 40; spacing: 18
Row { spacing: 14
Rectangle { width: 48; height: 48; radius: 14; color: Theme.infoBg
// A padlock.
Canvas { anchors.centerIn: parent; width: 24; height: 24
onPaint: { var c = getContext("2d"); c.strokeStyle = Theme.info; c.lineWidth = 1.8; c.beginPath(); c.roundedRect(4.5, 10.5, 15, 10, 2.5, 2.5); c.stroke();
c.beginPath(); c.moveTo(8, 10.5); c.lineTo(8, 8); c.arc(12, 8, 4, Math.PI, 0); c.lineTo(16, 10.5); c.stroke(); } } }
Column { spacing: 2; anchors.verticalCenter: parent.verticalCenter
Text { text: "Your password, once"; color: Theme.text; font.family: Theme.display; font.pixelSize: 30; font.weight: Font.DemiBold }
Text { text: "Steamify needs it to change system settings."; color: Theme.mute; font.family: Theme.body; font.pixelSize: 14 } }
}
Text { text: "Password for " + app.backend.user(); color: Theme.soft; font.family: Theme.body; font.pixelSize: 14; font.weight: Font.DemiBold }
TextField {
id: pw; width: parent.width; height: 52; echoMode: shown ? TextInput.Normal : TextInput.Password
property bool shown: false
color: Theme.textHi; font.family: Theme.body; font.pixelSize: 18; leftPadding: 16; rightPadding: 56
placeholderText: "Password"; placeholderTextColor: "#6b778a"
background: Rectangle { radius: 12; color: Theme.bg; border.width: 2; border.color: app.wrongPassword ? Theme.bad : Theme.accent }
Keys.onReturnPressed: screen.submit()
Keys.onEnterPressed: screen.submit()
Keys.onEscapePressed: app.act("back")
Text { anchors.right: parent.right; anchors.rightMargin: 16; anchors.verticalCenter: parent.verticalCenter; text: pw.shown ? "Hide" : "Show"
color: Theme.label; font.family: Theme.body; font.pixelSize: 13
MouseArea { anchors.fill: parent; anchors.margins: -10; onClicked: pw.shown = !pw.shown } }
}
Text { visible: app.wrongPassword; text: "That password didn't work. Try again."; color: Theme.bad; font.family: Theme.body; font.pixelSize: 14 }
Rectangle { width: parent.width; height: 46; radius: 12; color: Theme.note
Text { anchors.verticalCenter: parent.verticalCenter; x: 14; color: Theme.soft; font.family: Theme.body; font.pixelSize: 14
text: ({ controller: "Press Steam + X for the on-screen keyboard.", keyboard: "Type your password and press Enter.", remote: "Select the field for the on-screen keyboard." })[Input.type] } }
Item { width: parent.width; height: 48
Btn { k: Input.g.back; text: "Back"; onClicked: app.act("back") }
Btn { anchors.right: parent.right; k: Input.g.ok; text: "Apply"; primary: true; onClicked: screen.submit() } }
Text { text: "Only used for this run. Steamify never stores it."; color: Theme.faint; font.family: Theme.body; font.pixelSize: 12; anchors.horizontalCenter: parent.horizontalCenter }
}
}
}
+49
View File
@@ -0,0 +1,49 @@
// What a run will change, with a summary; Apply asks for the password.
import QtQuick
import QtQuick.Controls.Basic
Item {
required property AppState app
Column {
x: 40; y: 32; width: 740; spacing: 14
Text { text: app.plan.length === 0 ? "Nothing to change" : "This will"; color: Theme.text; font.family: Theme.display; font.pixelSize: 34; font.weight: Font.DemiBold }
ListView {
width: 740; clip: true; spacing: 8
height: Math.min(contentHeight, parent.parent.height - 32 - 50 - 14 - 66 - 64 - 32)
model: app.plan
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
delegate: Rectangle {
id: step
required property var modelData
readonly property var st: Texts.actions[modelData.action] || ["", Theme.text, Theme.card]
width: 740; height: 56; radius: 12; color: Theme.card
Row { anchors.fill: parent; anchors.leftMargin: 18; spacing: 14
Chip { text: step.st[0]; fg: step.st[1]; bgc: step.st[2]; width: 84; anchors.verticalCenter: parent.verticalCenter }
Text { text: Texts.label(step.modelData.id); color: Theme.textHi; font.family: Theme.body; font.pixelSize: 17; font.weight: Font.DemiBold
anchors.verticalCenter: parent.verticalCenter }
Badge { isNew: !!step.modelData.isNew; anchors.verticalCenter: parent.verticalCenter }
}
}
}
Rectangle { width: 740; height: 52; radius: 12; color: Theme.note
Text { anchors.verticalCenter: parent.verticalCenter; x: 18; text: "Your password is asked once. Changes to how the PC starts need a restart."
color: Theme.soft; font.family: Theme.body; font.pixelSize: 14 } }
}
Rectangle {
x: parent.width - 468; y: 32; width: 428; height: 250; radius: 16; color: Theme.card
Column { anchors.fill: parent; anchors.margins: 24; spacing: 12
Text { text: "SUMMARY"; color: Theme.faint; font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.8 }
Repeater { model: [["Turn on", "on"], ["Update", "update"], ["Re-apply", "again"], ["Turn off", "off"]]
Row { required property var modelData; width: 380
Text { text: parent.modelData[0]; color: Theme.soft; font.family: Theme.body; font.pixelSize: 15; width: 300 }
Text { readonly property string action: parent.modelData[1]
text: app.plan.filter(function (p) { return p.action === action; }).length
color: Theme.text; font.family: Theme.mono; font.pixelSize: 15; width: 80; horizontalAlignment: Text.AlignRight } } }
}
}
Bar {
Btn { anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; k: Input.g.back; text: "Back to the menu"; onClicked: app.act("back") }
Btn { anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter; k: Input.g.ok; text: "Apply"; primary: true
visible: app.plan.length > 0; onClicked: app.onApplyPressed() }
}
}
+17
View File
@@ -0,0 +1,17 @@
// The BIOS update's risks.
import QtQuick
Column {
id: list
spacing: 8
Repeater {
model: ["A failed or interrupted BIOS update can leave the machine unable to start (bricked). Steamify, CachyOS and Valve take no responsibility for that.",
"NEVER turn off the power, unplug the machine or press the power button while it updates, including the restart(s) afterwards.",
"The screen can stay black for several minutes. Wait.",
"Only on mains power, with all other programs closed."]
Row { required property string modelData; spacing: 10; width: list.width
Rectangle { width: 6; height: 6; radius: 3; color: Theme.bad; y: 9 }
Text { text: parent.modelData; width: parent.width - 16; wrapMode: Text.WordWrap; color: Theme.soft
font.family: Theme.body; font.pixelSize: 15; lineHeight: 1.3 } }
}
}
+66
View File
@@ -0,0 +1,66 @@
// Texts per item; the backend's labels are the fallback.
pragma Singleton
import QtQuick
QtObject {
readonly property var items: ({
gaming: { label: "SteamOS conversion", hint: "Boot into gaming mode, Steam on the desktop",
body: "Boots straight into gaming mode. Switch to Desktop in Steam works, and Return to Gaming Mode on the desktop brings you back.",
changes: ["Autologin into gamescope", "Return to Gaming Mode shortcut", "Steam starts silently on the desktop"] },
boot: { label: "Boot into", hint: "Where the PC starts",
body: "Where the PC starts after a restart. Switching back and forth works the same either way.",
changes: ["Desktop: the Plasma session is set before login", "Gaming: the SteamOS default"] },
theme: { label: "SteamOS theme", hint: "Vapor look for the desktop, Add to Steam",
body: "The Vapor look of SteamOS for the desktop, with its panel, launcher icon and wallpaper.",
changes: ["cachyos-vapor global theme", "Add to Steam in right-click menus", "Nested Desktop, Steam keyboard window rule"] },
glyphs: { label: "Steam Deck/Machine icons", hint: "Deck button icons in gaming mode",
body: "Steam shows Steam Deck button icons in gaming mode.", changes: ["STEAM_GAMEPADUI_ARGS -steamos3"] },
single: { label: "Single user mode", hint: "No password, lock screen or wallet prompts",
body: "Like SteamOS: never a login or lock screen, and no KDE wallet password prompts. Typing a password with a controller is no fun.",
changes: ["SDDM autologin, no lock screen or user switching", "Valve's empty wallet; your own is kept aside", "Launcher shows Sleep, Restart, Shut Down only"] },
launcher: { label: "Steamify shortcut", hint: "The app on the desktop and in the launcher",
body: "Opens the newest Steamify app, so you never need the install command again. Steamify Terminal in the launcher opens the terminal menu.",
changes: ["Steamify CachyOS on the desktop and in the launcher (the app)", "Steamify Terminal in the launcher"] },
vram: { label: "VRAM booster", hint: "The game in front keeps its VRAM",
body: "Like SteamOS 3.9: the game you're playing keeps its video memory, and background apps are moved out first. Without it a game that needs most of the video memory can spill into system RAM and stutter. Shown for GPUs whose driver supports it (AMD, Intel) on kernel 7.2 or newer.",
changes: ["dmemcg-booster (system and user service)", "plasma-foreground-booster, which tells it which window is in front", "Off: removed again, unless you had installed them yourself"] },
cec: { label: "HDMI-CEC", hint: "Use Steam with the TV remote, TV on/off with the PC", experimental: true,
body: "Use Steam with your TV remote, and the TV turns on and off with the PC. Turn on CEC on the TV too (Sony: BRAVIA Sync, Samsung: Anynet+, LG: SimpLink).",
changes: ["Valve's cecd and cec-audio-control", "HDMI CEC section in Steam's Display settings", "Volume buttons for the TV in the Quick Access menu"] },
machine: { label: "Steam Machine support", hint: "LED bar, fan and performance settings in Steam",
body: "The front LED bar works, Steam's hardware settings work, and the power button puts it to sleep like a console.",
changes: ["leds-valve driver for every kernel (DKMS)", "steamos-manager for Steam's settings", "Console-like power handling"] },
poweroff: { label: "Power-off fix", hint: "Stays off after shutting down",
body: "With recent kernels the Steam Machine starts again right after shutting down: the firmware leaves a wake bit set, and newer kernels (7.2, and updates of 6.x, 7.0 and 7.1) no longer clear it. Valve's own kernel clears it; this small module does the same right before power-off.",
changes: ["steamify-fremont-poweroff module for each installed kernel (DKMS)", "Only on a Steam Machine, only touches that one wake bit", "Off: recent kernels may start it again after shutting down"] },
kpin: { label: "Pin the kernel", hint: "No longer needed: untick for CachyOS's current kernel",
body: "Kept the Steam Machine on CachyOS kernel 7.1.6, which powered off properly. The power-off fix does that on every kernel now, so the pin is removed and CachyOS's current kernel comes back.",
changes: ["Removed from IgnorePkg", "linux-cachyos updated to CachyOS's current version"] },
hdmi: { label: "HDMI refresh boost", hint: "No longer needed: removed",
body: "Made HDMI displays run above 60 Hz on the pinned kernel. Current kernels do that themselves, so what's left of it is removed and every display uses its own EDID again.",
changes: ["Saved display EDIDs and the hotplug service removed", "The old kernel parameter removed, if it's still there"] },
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.",
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"] }
})
// Replaces the explanation of an option that can't be turned on here.
readonly property var unsupported: ({
vram: { body: "Not available with your NVIDIA graphics card: NVIDIA's driver doesn't tell Linux how its video memory is used yet, so there's nothing to steer. It works with AMD and Intel graphics cards, and shows up here as soon as NVIDIA's driver supports it.",
changes: ["Nothing: can't be turned on with NVIDIA's driver"] }
})
// Per plan action: the review's chip [text, colour, background] and the
// progress screen's verb.
readonly property var actions: ({
on: ["Turn on", Theme.good, Theme.goodBg, "Turn on "], off: ["Turn off", Theme.bad, Theme.badBg, "Turn off "],
again: ["Re-apply", Theme.info, Theme.infoBg, "Re-apply "], update: ["Update", Theme.warn, Theme.warnBg, ""],
desktop: ["Desktop", Theme.info, Theme.infoBg, "Boot into "], gaming: ["Gaming", Theme.info, Theme.infoBg, "Boot into "],
check: ["Check", Theme.warn, Theme.warnBg, "Download and check the "], flash: ["Flash", Theme.bad, Theme.badBg, "Hand to fwupd: the "]
})
function of(item) {
var tx = items[item.id] || {};
return item.selectable === false && unsupported[item.id] ? Object.assign({}, tx, unsupported[item.id]) : tx;
}
function label(id, fallback) { return (items[id] || {}).label || fallback || id; }
}
+36
View File
@@ -0,0 +1,36 @@
// Colours and fonts (from the design).
pragma Singleton
import QtQuick
QtObject {
readonly property color bg: "#0f1319"
readonly property color bar: "#141a22"
readonly property color line: "#232c38"
readonly property color card: "#161d27"
readonly property color cardSel: "#1f2a3a"
readonly property color text: "#e6ebf2"
readonly property color textHi: "#eef2f7"
readonly property color soft: "#c3ccd8"
readonly property color mute: "#97a3b4"
readonly property color faint: "#8b97a8"
readonly property color label: "#b8c3d1"
readonly property color accent: "#3ea6ff"
readonly property color ink: "#06121f"
readonly property color good: "#7fd1a8"
readonly property color goodBg: "#173226"
readonly property color warn: "#f2a33a"
readonly property color warnBg: "#2a2116"
readonly property color bad: "#ff9a8a"
readonly property color badBg: "#2a1c1c"
readonly property color info: "#7cc4ff"
readonly property color infoBg: "#1b2b40"
readonly property color note: "#1a2230"
readonly property color key: "#2a3444"
readonly property color button: "#232c38"
readonly property color danger: "#8a2c2c"
readonly property color dangerBg: "#1d1416"
readonly property color dangerLine: "#b33a3a"
readonly property string display: "Barlow Semi Condensed"
readonly property string body: "IBM Plex Sans"
readonly property string mono: "IBM Plex Mono"
}
+20
View File
@@ -0,0 +1,20 @@
singleton Theme 1.0 Theme.qml
singleton Texts 1.0 Texts.qml
singleton Input 1.0 Input.qml
AppState 1.0 AppState.qml
Glyph 1.0 Glyph.qml
Btn 1.0 Btn.qml
Chip 1.0 Chip.qml
Badge 1.0 Badge.qml
Bar 1.0 Bar.qml
RiskList 1.0 RiskList.qml
Header 1.0 Header.qml
MenuRow 1.0 MenuRow.qml
DetailPanel 1.0 DetailPanel.qml
MenuScreen 1.0 MenuScreen.qml
ReviewScreen 1.0 ReviewScreen.qml
PasswordScreen 1.0 PasswordScreen.qml
ApplyingScreen 1.0 ApplyingScreen.qml
BiosWarningScreen 1.0 BiosWarningScreen.qml
BiosConfirmScreen 1.0 BiosConfirmScreen.qml
DoneScreen 1.0 DoneScreen.qml
+2 -24
View File
@@ -271,8 +271,8 @@ class Backend(QObject):
self.busyChanged.emit() self.busyChanged.emit()
p.start() p.start()
@Slot("QVariant", str, bool, str, str) @Slot("QVariant", str, bool, str)
def apply(self, ids, boot, reapply, password, hdmi): def apply(self, ids, boot, reapply, password):
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()
@@ -281,8 +281,6 @@ 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.
@@ -300,26 +298,6 @@ 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)
@Slot(str, str)
def hdmiForget(self, password, display):
"""Removes a saved display (hdmi-forgotten event)."""
self._start(["hdmi-forget", display], 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: