diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 221a568..b5f18d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -302,6 +302,19 @@ jobs: SUDO_USER=tester SEGLINUX_SOURCE="$PWD" SEGLINUX_VERSION=0.1.0.999999 \ bash data/system/seglinux-upgrade --noconfirm pacman -Q seglinux | grep -q ' 0\.1\.0\.999999-' + # seg (package segctl) comes with it, with its group, sudoers rule + # and services (pacman -U ran the package's install script). + pacman -Q segctl + seg --version + getent group gamemode + test "$(stat -c %a /etc/sudoers.d/seg-game)" = 440 + visudo -cf /etc/sudoers.d/seg-game + test -x /etc/init.d/seg-game && test -f /etc/runit/sv/seg-game/run && test -f /etc/dinit.d/seg-game + ls /etc/dinit.d/boot.d /etc/runlevels/default 2>/dev/null | grep -E '^seg-' || true + # seg game works on a machine with no GPU: it reports, it does not crash. + seg game check > /tmp/game-check.txt || test $? -eq 1 + grep -q 'max_map_count' /tmp/game-check.txt + seg game run --dry-run -- true test -x /usr/bin/seglinux-upgrade && test -L /usr/bin/seglinux-update test ! -e /usr/local/bin/seglinux-upgrade seglinux-shell --version diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c1c678..57e8325 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,9 @@ Measurements and the reasons behind each removal are in - Setup installs the **other kernel** (`linux` beside `linux-zen`, or the reverse) as a boot menu entry to fall back on when online (`[performance] fallback_kernel`); the installed kernel is never removed. +- Transparent hugepages go from `always` to `madvise` at boot (`seg-game` + service), so the live image and upgraded systems get it too, not only + fresh installs. - Kernel command line: `transparent_hugepage=madvise` everywhere and `amd_pstate=active` on AMD processors (an answer file's parameter of the same name replaces the default). @@ -62,6 +65,9 @@ Measurements and the reasons behind each removal are in `gamemode` group may use `nice -10`. - Shader caches: Mesa 4 GB -> 10 GB; NVIDIA's shader cache set to 10 GB and no longer deleted when full (`/etc/profile.d`, so on every init). +- `sudo seglinux-upgrade` (the from-source path) now also builds and + installs `segctl`, so an older install gets `seg`, `seg game`, the + services and the sudoers rule; CI checks this on an Artix container. - `makepkg` builds with every thread; the I/O scheduler rule skips partitions and covers virtual disks. diff --git a/data/system/seglinux-upgrade b/data/system/seglinux-upgrade index 41f465e..662d15d 100755 --- a/data/system/seglinux-upgrade +++ b/data/system/seglinux-upgrade @@ -178,6 +178,7 @@ latest() { # makepkg does not build as root, so it builds as the account that ran sudo. build_seglinux() { local user home version sha installed work top deps packages + local segctl_dir segctl_version segctl_deps segctl_packages=() user=${SUDO_USER:-} if [[ -z $user || $user == root ]]; then warn "SegLinux itself is built from your account: run sudo seglinux-upgrade as yourself, not as root." @@ -218,10 +219,27 @@ build_seglinux() { tar -C "$work/x" -czf "$work/seglinux-$version.tar.gz" "seglinux-$version" fi cp "$work/x/seglinux-$version/packaging/seglinux/"{PKGBUILD,seglinux.install} "$work/" + # seg, the system tool (package segctl: seg game, the zram and tuning + # services), is built from the same source: its recipe, and its folder as + # the source tarball the recipe names. + segctl_dir= + if [[ -f $work/x/seglinux-$version/pkg/segctl/PKGBUILD ]]; then + segctl_dir=$work/segctl + mkdir "$segctl_dir" + cp "$work/x/seglinux-$version/pkg/segctl/"{PKGBUILD,segctl.install} "$segctl_dir/" + segctl_version=$(sed -n 's/^pkgver=//p' "$segctl_dir/PKGBUILD") + tar -C "$work/x/seglinux-$version" --transform "s,^\./tools/seg,segctl-$segctl_version/tools/seg," \ + -czf "$segctl_dir/segctl-$segctl_version.tar.gz" ./tools/seg + fi rm -rf "$work/x" "$work/source.tar.gz" sed -i "s/^pkgver=.*/pkgver=$version/" "$work/PKGBUILD" chown -R "$user:" "$work" - as_user() { (cd "$work" && runuser -u "$user" -- env HOME="$home" CARGO_HOME="$home/.cache/seglinux/cargo" "$@"); } + as_user_in() { + local dir=$1 + shift + (cd "$dir" && runuser -u "$user" -- env HOME="$home" CARGO_HOME="$home/.cache/seglinux/cargo" "$@") + } + as_user() { as_user_in "$work" "$@"; } # What it needs to build and run, as dependencies (pacman -Qdt lists # them once nothing needs them). deps=$(as_user makepkg --printsrcinfo | sed -n 's/^\t\(make\)\{0,1\}depends = //p') || { @@ -229,8 +247,12 @@ build_seglinux() { warn "SegLinux's build recipe could not be read; SegLinux itself is not upgraded this time." return 0 } + segctl_deps= + if [[ -n $segctl_dir ]]; then + segctl_deps=$(as_user_in "$segctl_dir" makepkg --printsrcinfo | sed -n 's/^\t\(make\)\{0,1\}depends = //p') || segctl_dir= + fi # shellcheck disable=SC2086 # one package name per word - pacman -S --needed --noconfirm --asdeps base-devel $deps || { + pacman -S --needed --noconfirm --asdeps base-devel $deps $segctl_deps || { rm -rf "$work" warn "The tools to build SegLinux could not be installed; SegLinux itself is not upgraded this time." return 0 @@ -240,8 +262,16 @@ build_seglinux() { warn "SegLinux $version did not build; your SegLinux stays as it is. Please report it: $source_url/issues" return 0 fi + # seg is a smaller extra: if it does not build, SegLinux is still upgraded. + if [[ -n $segctl_dir ]]; then + if as_user_in "$segctl_dir" makepkg -f --nocheck --noconfirm; then + segctl_packages=("$segctl_dir"/segctl-*.pkg.tar.zst) + else + warn "seg (the segctl package) did not build; it is not upgraded this time." + fi + fi packages=("$work"/seglinux-"$version"-*.pkg.tar.zst) - pacman -U "$@" "${packages[0]}" || warn "SegLinux $version was not installed." + pacman -U "$@" "${packages[0]}" "${segctl_packages[@]}" || warn "SegLinux $version was not installed." rm -rf "$work" } diff --git a/docs/gaming.md b/docs/gaming.md index 33ffaac..0539507 100644 --- a/docs/gaming.md +++ b/docs/gaming.md @@ -226,7 +226,8 @@ a build container with no GPU and the stock kernel, to show the format: `--games` adds Steam, gamescope, MangoHud (64-bit only), Wine Staging and winetricks; `--lutris` adds Lutris. It also tells you when `[lib32]` is not enabled in `/etc/pacman.conf`. -* `sudo seg game apply` applies the gaming sysctls and makes `/dev/ntsync` +* `sudo seg game apply` applies the gaming sysctls, sets transparent + hugepages to `madvise` (from `always` only) and makes `/dev/ntsync` usable now; the `seg-game` service runs it at boot on every init. * `seg game audio on|off|status` lowers PipeWire's buffer to 256 samples at 48 kHz (about 5 ms), for you only (`~/.config/pipewire/...conf.d`). **Off by @@ -345,7 +346,7 @@ leave it off. | `nmi_watchdog=0`, `nowatchdog`, watchdog modules blacklisted | kernel command line (Setup), `/usr/lib/modprobe.d/seglinux-nowatchdog.conf` | Fewer periodic interrupts | | `vm.compaction_proactiveness = 0` | same | No background memory defragmentation while a game allocates (the default is 20); a program that needs a contiguous block still gets it by compacting on demand. What SteamOS sets. | | `fs.inotify.max_user_watches` | sysctl | Launchers watching many files | -| `transparent_hugepage=madvise` | kernel command line (Setup) | Huge pages only for programs that ask (games, databases); `always` can stall on compaction | +| `transparent_hugepage=madvise` | kernel command line (Setup), and `seg game apply` at boot | Huge pages only for programs that ask (games, databases); `always` can stall on compaction. linux-zen starts with `always`, so the live image and a system upgraded with `seglinux-upgrade` get `madvise` from the `seg-game` service (it changes `always` only; `never` is left alone). Found by booting the Lite image: the check said `[always]` before this. | | `amd_pstate=active` | kernel command line (Setup, AMD processors only) | The CPU picks its frequency itself, guided by the energy hint `seg game run` sets. Kernels or CPUs without it ignore it. | | I/O schedulers: NVMe none, SSD mq-deadline, HDD BFQ | `/usr/lib/udev/rules.d/60-seglinux-ioschedulers.rules` | Low latency while games stream assets | | NTSYNC | `/etc/modules-load.d/10-ntsync.conf`, udev rule | Windows synchronisation in the kernel, used by Wine 11 and Proton | diff --git a/docs/lean-audit.md b/docs/lean-audit.md index 9e0c70e..2f921e7 100644 --- a/docs/lean-audit.md +++ b/docs/lean-audit.md @@ -176,8 +176,10 @@ Found, not changed: the udev rule starts it whatever the answer says. * The two greetd `config.toml` and `pacman.conf` copies in the root overlays are identical to each other (and the first to `data/greeter/greetd.toml`). -* `seglinux-upgrade` updates only the `seglinux` package, so a system - installed before this work does not get `segctl` that way yet. +* `seglinux-upgrade` updated only the `seglinux` package, so a system + installed before this work did not get `segctl`. Fixed in the gaming work: + the from-source upgrade also builds and installs `segctl` (tested in CI on + an Artix container); the signed repository still carries `seglinux` only. ## Decisions for the maintainers diff --git a/docs/updating.md b/docs/updating.md index 9addded..c3201a0 100644 --- a/docs/updating.md +++ b/docs/updating.md @@ -83,7 +83,13 @@ each marked `# Added by seglinux-upgrade.`. It keeps the previous file as When SegLinux is built from source, the source comes over HTTPS from SegLinux's GitHub repository, as the ISO and this command do; pacman installs -the package it builds as a local file, like any AUR package. +the package it builds as a local file, like any AUR package. The same build +also makes `segctl` from the `tools/seg` folder (the `seg` tool: `seg game`, +the zram, tuning and game services, the sudoers rule for the `gamemode` +group) and installs the two together; if only `segctl` fails to build, +SegLinux is still upgraded. SegLinux's signed repository does not carry +`segctl` yet (`.github/workflows/update.yml` builds `seglinux` only), so once +packages are published `seg` still needs `--from-source` or the ISO. ## When an update fails diff --git a/tools/seg/README.md b/tools/seg/README.md index 3000af6..a3cb695 100644 --- a/tools/seg/README.md +++ b/tools/seg/README.md @@ -121,7 +121,7 @@ safety notes are in [docs/gaming.md](../../docs/gaming.md#seg-game). | `seg game check` | PASS/WARN/FAIL/SKIP for Vulkan, GPU driver, lib32 drivers, ntsync, `vm.max_map_count`, open files, kernel, fallback kernel, governor, boost permission, hugepages, split lock, compaction, mitigations; the fix under each; exit 1 if any FAIL | | `seg game setup [--yes] [--games] [--lutris]` | the driver packages for the GPUs in this computer (from `/sys/class/drm` and `lspci`) and the command that installs them; `--yes` runs it | | `seg game profiles [FILE]` | check `~/.config/seg/games.toml` and list what it holds | -| `seg game apply` | the gaming sysctls and `/dev/ntsync` permissions now (the `seg-game` service) | +| `seg game apply` | the gaming sysctls, hugepages `always` -> `madvise`, and `/dev/ntsync` permissions now (the `seg-game` service) | | `seg game audio on\|off\|status` | low-latency PipeWire for the current user | | `seg game unsafe-boost on\|off\|status` | `mitigations=off`; needs `--i-understand` after a warning | diff --git a/tools/seg/segctl/src/game/apply.rs b/tools/seg/segctl/src/game/apply.rs index 37f2327..539068b 100644 --- a/tools/seg/segctl/src/game/apply.rs +++ b/tools/seg/segctl/src/game/apply.rs @@ -1,7 +1,7 @@ //! `seg game apply`, run at boot by the `seg-game` service of every init: -//! the gaming sysctls and the permissions of /dev/ntsync. OpenRC and udev do -//! both too; runit, dinit and s6 images may not, and applying twice is -//! harmless. +//! the gaming sysctls, transparent hugepages on `madvise` and the permissions +//! of /dev/ntsync. OpenRC and udev do the sysctls and the device too; runit, +//! dinit and s6 images may not, and applying twice is harmless. use std::fs; use std::os::unix::fs::PermissionsExt; @@ -15,12 +15,23 @@ pub const HELP: &str = "\ Usage: seg game apply Applies /etc/sysctl.d/80-seglinux-gaming.conf now (vm.max_map_count, -kernel.split_lock_mitigate, vm.compaction_proactiveness, ...) and makes -/dev/ntsync usable by every user, so Wine and Proton can use NTSYNC. Needs -root. The seg-game service runs it at boot. +kernel.split_lock_mitigate, vm.compaction_proactiveness, ...), changes +transparent hugepages from `always` to `madvise` (linux-zen starts with +`always`, which can stall a game on memory compaction; `never` is left as it +is) and makes /dev/ntsync usable by every user, so Wine and Proton can use +NTSYNC. Needs root. The seg-game service runs it at boot. "; pub const SYSCTL_FILE: &str = "etc/sysctl.d/80-seglinux-gaming.conf"; +const THP_FILE: &str = "sys/kernel/mm/transparent_hugepage/enabled"; + +/// The word in brackets of `always [madvise] never`. +fn thp_selected(text: &str) -> Option<&str> { + text.split_once('[')? + .1 + .split_once(']') + .map(|(word, _)| word) +} /// Apply the sysctls of the gaming file below `root`; then fix /dev/ntsync. pub fn apply(root: &Path) -> Result> { @@ -45,6 +56,13 @@ pub fn apply(root: &Path) -> Result> { )); } } + let thp = root.join(THP_FILE); + if let Ok(text) = fs::read_to_string(&thp) + && thp_selected(&text) == Some("always") + { + fs::write(&thp, "madvise").context(format!("writing {}", thp.display()))?; + lines.push("transparent hugepages: always -> madvise".into()); + } let ntsync = root.join("dev/ntsync"); if let Ok(meta) = fs::metadata(&ntsync) { let mode = meta.permissions().mode() & 0o777; @@ -117,6 +135,42 @@ mod tests { assert!(again.iter().all(|l| l.starts_with("note:")), "{again:?}"); } + #[test] + fn hugepages_go_from_always_to_madvise_and_nothing_else_changes() { + let t = TempDir::new(); + t.put(THP_FILE, "[always] madvise never\n"); + let lines = apply(t.path()).unwrap(); + assert!( + lines + .iter() + .any(|l| l == "transparent hugepages: always -> madvise"), + "{lines:?}" + ); + assert_eq!( + fs::read_to_string(t.path().join(THP_FILE)).unwrap(), + "madvise" + ); + // madvise is left alone, and so is never (someone chose it). + for already in ["always [madvise] never\n", "always madvise [never]\n"] { + let t = TempDir::new(); + t.put(THP_FILE, already); + let lines = apply(t.path()).unwrap(); + assert!(lines.iter().all(|l| !l.contains("hugepages")), "{lines:?}"); + assert_eq!( + fs::read_to_string(t.path().join(THP_FILE)).unwrap(), + already + ); + } + } + + #[test] + fn the_hugepage_setting_is_read_from_the_brackets() { + assert_eq!(thp_selected("[always] madvise never"), Some("always")); + assert_eq!(thp_selected("always [madvise] never"), Some("madvise")); + assert_eq!(thp_selected("always madvise never"), None); + assert_eq!(thp_selected(""), None); + } + #[test] fn no_file_and_no_device_is_not_an_error() { let t = TempDir::new();