commit d5a7f9feeb101dc21848cd03f3e15e15c7ea7415 Author: Dawnsorrow Date: Wed Jun 24 23:30:48 2026 -0500 Initial release: z13Profiler GUI for unified memory on GZ302. Adds zenity profile picker (gaming / LLM / reset) with safe RAM guardrails, pkexec-based apply, and install.sh for Nobara/Fedora. Co-authored-by: Cursor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f147c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.tar.gz +__pycache__/ +*.pyc +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..12ae8be --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# z13Profiler + +GUI profile switcher for **ROG Flow Z13 (GZ302)** unified memory on **Nobara / Fedora**. + +Repository: [git.hisora.dev/Dawnsorrow/z13Profiler](https://git.hisora.dev/Dawnsorrow/z13Profiler) + +Switches between: + +| Profile | Fixed VRAM (carveout) | Dynamic GPU pool (TTM) | +|---------|----------------------|-------------------------| +| **Gaming** | 8 GB | Kernel default (removes custom TTM) | +| **LLM / AI** | 512 MB (minimum available) | 48 GB | +| **Reset** | 4 GB (ASUS default) | Kernel default | + +All changes require a **reboot**. The GUI warns and confirms before applying. + +## Quick install (Z13 / Nobara) + +```bash +git clone https://git.hisora.dev/Dawnsorrow/z13Profiler.git +cd z13Profiler +./install.sh +z13-memory-profiles +``` + +One-liner (clone + install): + +```bash +git clone https://git.hisora.dev/Dawnsorrow/z13Profiler.git /tmp/z13Profiler && /tmp/z13Profiler/install.sh +``` + +Dependencies (install first if missing): + +```bash +sudo dnf install -y git zenity python3 polkit +``` + +## Update from git + +If installed from this repo, pull and re-run the installer: + +```bash +cd ~/z13Profiler # or wherever you cloned +git pull +./install.sh +``` + +Or update the installed copy directly: + +```bash +cd ~/.local/share/z13-memory-profiles +git pull +./install.sh +``` + +## Requirements + +- ROG Flow Z13 2025 (GZ302), 64 GB RAM (defaults in `config/profiles.conf`) +- Nobara / Fedora with `zenity`, `python3`, `polkit` (`pkexec`) +- Root via `pkexec` when applying profiles + +Optional: `pipx install amd-debug-tools` for standalone `amd-ttm` CLI. + +## Usage + +**GUI (Application menu → “Z13 Memory Profiles”, or terminal):** + +```bash +z13-memory-profiles +# alias after install: +z13profiler +``` + +**Check current settings:** + +```bash +z13-memory-profiles --status +``` + +**Apply from terminal (reboot manually after):** + +```bash +sudo z13-memory-profiles --apply gaming +sudo z13-memory-profiles --apply llm +sudo z13-memory-profiles --apply reset +``` + +## Safeguards + +- Refuses profiles that would leave less than **8 GB** for the OS (configurable). +- Warns if TTM exceeds **90%** of installed RAM. +- Only writes `/etc/modprobe.d/ttm.conf` and UMA sysfs index (no random kernel hacks). +- Uses `pkexec` for privileged operations from the GUI. + +## If fixed VRAM (carveout) does not change + +Some setups do not expose `/sys/class/drm/card0/device/uma/` yet. The tool still adjusts TTM for LLM mode. For gaming VRAM: + +1. Boot **Windows** once → **Armoury Crate → System Settings → Free up memory** → set **8 GB** → reboot. +2. Or update Nobara/kernel and re-check `z13-memory-profiles --status`. + +## Customizing for your RAM size + +Edit `~/.local/share/z13-memory-profiles/config/profiles.conf` after install: + +```ini +[llm] +ttm_gb=48 # lower on 32 GB systems (e.g. 24) +min_os_reserve_gb=8 +``` + +## Uninstall + +```bash +rm -f ~/.local/bin/z13-memory-profiles ~/.local/bin/z13profiler +rm -rf ~/.local/share/z13-memory-profiles +rm -f ~/.local/share/applications/z13-memory-profiles.desktop +sudo rm -f /etc/modprobe.d/ttm.conf +sudo dracut --force --regenerate-all 2>/dev/null || true +``` + +Then reboot. + +## Build tarball (optional) + +```bash +./package.sh +``` + +Creates `z13Profiler-1.0.0.tar.gz` in the parent directory for USB transfer. diff --git a/bin/z13-memory-profiles b/bin/z13-memory-profiles new file mode 100755 index 0000000..699d4a8 --- /dev/null +++ b/bin/z13-memory-profiles @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +# Z13 Memory Profiles — GUI picker for unified RAM (GZ302 / Nobara) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +INSTALL_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +export INSTALL_ROOT CONFIG_FILE="${INSTALL_ROOT}/config/profiles.conf" + +# shellcheck source=../lib/memory-profiles.sh +source "${INSTALL_ROOT}/lib/memory-profiles.sh" + +SELF="${INSTALL_ROOT}/bin/$(basename "$0")" + +die_gui() { + zenity --error --width=420 --title="Z13 Memory Profiles" --text="$1" 2>/dev/null || { + echo "ERROR: $1" >&2 + exit 1 + } + exit 1 +} + +need_zenity() { + command -v zenity >/dev/null 2>&1 || die_gui "zenity is required.\n\nInstall: sudo dnf install zenity" +} + +run_privileged() { + if [[ "${EUID}" -eq 0 ]]; then + "$@" + elif command -v pkexec >/dev/null 2>&1; then + pkexec "$@" + else + die_gui "pkexec not found.\n\nInstall: sudo dnf install polkit\nOr run: sudo ${SELF} --apply " + fi +} + +profile_summary() { + local profile="$1" + local label desc carveout ttm_action ttm_gb + label="$(read_profile_value "${profile}" label)" + desc="$(read_profile_value "${profile}" description)" + carveout="$(read_profile_value "${profile}" carveout_target_mb)" + ttm_action="$(read_profile_value "${profile}" ttm_action)" + ttm_gb="$(read_profile_value "${profile}" ttm_gb)" + + local ttm_text + if [[ "${ttm_action}" == "clear" ]]; then + ttm_text="default (clear custom TTM)" + else + ttm_text="${ttm_gb} GB dynamic" + fi + + printf '%s\n\n%s\n\nFixed VRAM target: %s MB\nDynamic GPU pool: %s\n\nRequires reboot.' \ + "${label}" "${desc}" "${carveout}" "${ttm_text}" +} + +gui_main() { + need_zenity + + local total_gb uma_note + total_gb="$(total_ram_gb)" + if find_uma_base >/dev/null 2>&1; then + uma_note="UMA sysfs: available" + else + uma_note="UMA sysfs: not found — fixed VRAM may need Windows Armoury Crate" + fi + + local choice + choice="$(zenity --list --title="Z13 Memory Profiles" --width=520 --height=360 \ + --text="ROG Flow Z13 unified memory (~${total_gb} GB RAM)\n${uma_note}\n\nPick a profile (reboot required):" \ + --column="Profile" --column="Use case" \ + "gaming" "Gaming — 8 GB fixed VRAM, default dynamic pool" \ + "llm" "LLM / AI — min fixed VRAM, 48 GB dynamic pool" \ + "reset" "Reset — 4 GB fixed VRAM, default dynamic pool" \ + "status" "Show current settings (no changes)" \ + "discover" "Print full status to terminal" \ + 2>/dev/null)" || exit 0 + + case "${choice}" in + status) + local info + info="$(show_status 2>&1)" + zenity --text-info --title="Z13 Memory Status" --width=620 --height=480 --font=monospace <<<"${info}" + gui_main + ;; + discover) + xterm -e "bash -lc '${SELF} --status; echo; read -p \"Press Enter...\"'" 2>/dev/null \ + || konsole -e bash -lc "${SELF} --status; read -p 'Press Enter...'" 2>/dev/null \ + || { + show_status + read -r -p "Press Enter..." + } + gui_main + ;; + gaming|llm|reset) + local summary confirm + summary="$(profile_summary "${choice}")" + if ! validate_msg="$(validate_profile "${choice}" 2>&1)"; then + die_gui "${validate_msg}\n\nEdit ${CONFIG_FILE} if your RAM size differs." + fi + confirm="$(zenity --question --title="Confirm reboot" --width=480 \ + --text="${summary}\n\nApply this profile and reboot now?" \ + --ok-label="Apply & Reboot" --cancel-label="Cancel" 2>/dev/null && echo yes || echo no)" + [[ "${confirm}" == "yes" ]] || exit 0 + + local tmpout + tmpout="$(mktemp)" + if ! run_privileged "${SELF}" --apply "${choice}" >"${tmpout}" 2>&1; then + die_gui "Failed to apply profile:\n\n$(cat "${tmpout}")" + fi + + zenity --info --title="Rebooting" --width=420 \ + --text="Profile applied.\n\n$(cat "${tmpout}")\n\nRebooting in 5 seconds..." 2>/dev/null || true + sleep 5 + run_privileged systemctl reboot + ;; + esac +} + +usage() { + cat <&2; exit 1; } + apply_profile "$2" + ;; + --validate) + [[ -n "${2:-}" ]] || { echo "Missing profile name" >&2; exit 1; } + validate_profile "$2" + ;; + -h|--help) + usage + ;; + *) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + esac +} + +main "$@" diff --git a/config/profiles.conf b/config/profiles.conf new file mode 100644 index 0000000..6da7895 --- /dev/null +++ b/config/profiles.conf @@ -0,0 +1,27 @@ +# Z13 Memory Profiles — defaults for 64 GB ROG Flow Z13 (GZ302) +# Edit after running: z13-memory-profiles --discover +# +# carveout_target_mb: fixed GPU reservation (what many games read as "VRAM") +# ttm_gb: dynamic GPU pool (LLM / compute). Empty or "clear" removes custom TTM. +# min_os_reserve_gb: refuse settings that leave less than this for the OS + +[gaming] +label=Gaming +description=8 GB fixed VRAM for game compatibility. Clears custom TTM (kernel default ~half RAM). +carveout_target_mb=8192 +ttm_action=clear +min_os_reserve_gb=8 + +[llm] +label=LLM / AI +description=Minimum fixed VRAM + 48 GB dynamic GPU pool for local models. +carveout_target_mb=512 +ttm_gb=48 +min_os_reserve_gb=8 + +[reset] +label=Reset to defaults +description=4 GB fixed VRAM (ASUS default) and default TTM settings. +carveout_target_mb=4096 +ttm_action=clear +min_os_reserve_gb=8 diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..9d8aa41 --- /dev/null +++ b/install.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Install Z13 Memory Profiles for the current user + +set -euo pipefail + +SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PREFIX="${PREFIX:-${HOME}/.local}" +INSTALL_DIR="${PREFIX}/share/z13-memory-profiles" +BIN_DIR="${PREFIX}/bin" +APP_DIR="${PREFIX}/share/applications" + +echo "==> z13Profiler installer" +echo " Source: ${SRC_DIR}" +echo " Target: ${INSTALL_DIR}" + +missing=() +for cmd in zenity python3; do + command -v "${cmd}" >/dev/null 2>&1 || missing+=("${cmd}") +done + +if ((${#missing[@]})); then + echo "Missing dependencies: ${missing[*]}" + echo "Install with: sudo dnf install ${missing[*]} polkit" + exit 1 +fi + +if ! command -v pkexec >/dev/null 2>&1; then + echo "Note: pkexec not found — GUI apply will fall back to sudo." + echo " Recommended: sudo dnf install polkit" +fi + +mkdir -p "${INSTALL_DIR}" "${BIN_DIR}" "${APP_DIR}" + +rsync -a --delete \ + --exclude='.git' \ + --exclude='*.tar.gz' \ + --exclude='package.sh' \ + "${SRC_DIR}/" "${INSTALL_DIR}/" + +chmod +x "${INSTALL_DIR}/bin/z13-memory-profiles" +chmod +x "${INSTALL_DIR}/install.sh" 2>/dev/null || true + +ln -sf "${INSTALL_DIR}/bin/z13-memory-profiles" "${BIN_DIR}/z13-memory-profiles" +ln -sf "${INSTALL_DIR}/bin/z13-memory-profiles" "${BIN_DIR}/z13profiler" + +cat > "${APP_DIR}/z13-memory-profiles.desktop" <&2; } + +require_root() { + if [[ "${EUID}" -ne 0 ]]; then + echo "Root required. Re-run with pkexec or sudo." >&2 + exit 1 + fi +} + +total_ram_mb() { + awk '/^MemTotal:/ { printf "%d\n", $2 / 1024 }' /proc/meminfo +} + +total_ram_gb() { + awk '/^MemTotal:/ { printf "%.1f\n", $2 / 1024 / 1024 }' /proc/meminfo +} + +gb_to_pages() { + python3 -c "print(int(float('${1}') * (1024 ** 3) / 4096))" +} + +pages_to_gb() { + python3 -c "print(f'{int(${1}) * 4096 / (1024 ** 3):.2f}')" +} + +find_uma_base() { + local card + for card in /sys/class/drm/card*/device; do + [[ -d "${card}/uma" ]] || continue + if [[ -r "${card}/uma/carveout_options" ]]; then + echo "${card}/uma" + return 0 + fi + done + return 1 +} + +parse_carveout_options() { + local uma_base="$1" + cat "${uma_base}/carveout_options" +} + +# Pick carveout index closest to target_mb without exceeding max_mb (when set). +find_carveout_index() { + local uma_base="$1" target_mb="$2" max_mb="${3:-}" + python3 - "${uma_base}/carveout_options" "${target_mb}" "${max_mb}" <<'PY' +import re, sys +path, target_s, max_s = sys.argv[1:4] +target = float(target_s) +max_mb = float(max_s) if max_s else None +options = [] +with open(path, encoding="utf-8", errors="replace") as f: + for line in f: + m = re.match(r"\s*(\d+):\s*(.*)$", line.strip()) + if not m: + continue + idx, rest = int(m.group(1)), m.group(2).lower() + mb = None + g = re.search(r"(\d+(?:\.\d+)?)\s*gb", rest) + if g: + mb = float(g.group(1)) * 1024 + else: + m2 = re.search(r"(\d+(?:\.\d+)?)\s*mb", rest) + if m2: + mb = float(m2.group(1)) + if mb is None: + continue + if max_mb is not None and mb > max_mb: + continue + options.append((idx, mb, line.strip())) +if not options: + sys.exit(1) +# Prefer smallest option >= target; else largest below target. +at_or_above = [o for o in options if o[1] >= target - 0.5] +if at_or_above: + best = min(at_or_above, key=lambda o: o[1]) +else: + best = max(options, key=lambda o: o[1]) +print(best[0]) +print(best[2]) +PY +} + +read_profile_value() { + local section="$1" key="$2" + awk -v section="[$section]" -v key="$key" ' + $0 == section { found=1; next } + /^\[/ { found=0 } + found && $0 ~ "^[[:space:]]*" key "=" { + sub(/^[^=]*=/, "") + gsub(/^[[:space:]]+|[[:space:]]+$/, "") + print + exit + } + ' "${CONFIG_FILE}" +} + +current_ttm_gb() { + if [[ ! -r "${TTM_PARAM}" ]]; then + echo "unknown" + return + fi + pages_to_gb "$(cat "${TTM_PARAM}")" +} + +current_carveout_info() { + local uma_base + if ! uma_base="$(find_uma_base)"; then + echo "UMA sysfs: not available" + return + fi + echo "Current index: $(cat "${uma_base}/carveout")" + echo "Options:" + parse_carveout_options "${uma_base}" +} + +validate_profile() { + local profile="$1" + local carveout_mb ttm_action ttm_gb min_reserve total_mb total_gb max_ttm_gb + + carveout_mb="$(read_profile_value "${profile}" carveout_target_mb)" + ttm_action="$(read_profile_value "${profile}" ttm_action)" + ttm_gb="$(read_profile_value "${profile}" ttm_gb)" + min_reserve="$(read_profile_value "${profile}" min_os_reserve_gb)" + + [[ -n "${carveout_mb}" ]] || { echo "Missing carveout_target_mb for ${profile}"; return 1; } + + total_mb="$(total_ram_mb)" + total_gb="$(total_ram_gb)" + max_ttm_gb="$(python3 -c "print(${total_gb} * ${MAX_TTM_PERCENT} / 100)")" + + if [[ "${ttm_action}" == "clear" ]]; then + ttm_gb="0" + elif [[ -n "${ttm_gb}" ]]; then + : + else + ttm_gb="0" + fi + + local needed_mb + needed_mb="$(python3 -c " +c = ${carveout_mb} +t = float('${ttm_gb}') * 1024 if ${ttm_gb} else 0 +r = float('${min_reserve}') * 1024 +print(int(c + t + r)) +")" + + if (( needed_mb > total_mb )); then + echo "Refusing: profile needs ~$(( needed_mb / 1024 )) GB but only ~${total_gb} GB RAM installed." + echo " carveout ${carveout_mb} MB + TTM ${ttm_gb} GB + OS reserve ${min_reserve} GB" + return 1 + fi + + if [[ "${ttm_gb}" != "0" ]] && python3 -c "exit(0 if float('${ttm_gb}') > float('${max_ttm_gb}') else 1)"; then + echo "Warning: TTM ${ttm_gb} GB exceeds ${MAX_TTM_PERCENT}% of RAM (~${max_ttm_gb} GB)." + fi + + return 0 +} + +write_ttm_config() { + local gb="$1" + local pages + pages="$(gb_to_pages "${gb}")" + cat > "${TTM_CONF}" < ${pages} pages)" +} + +clear_ttm_config() { + if [[ -f "${TTM_CONF}" ]]; then + rm -f "${TTM_CONF}" + log "Removed ${TTM_CONF}" + else + log "No ${TTM_CONF} to remove (already default)" + fi +} + +regenerate_initramfs() { + if command -v dracut >/dev/null 2>&1; then + log "Regenerating initramfs (dracut)..." + dracut --force --regenerate-all 2>/dev/null || dracut --force + return + fi + if command -v update-initramfs >/dev/null 2>&1; then + update-initramfs -u -k all + return + fi + if command -v mkinitcpio >/dev/null 2>&1; then + mkinitcpio -P + return + fi + log "No initramfs tool found; reboot may still apply modprobe.d on next boot." +} + +set_carveout_index() { + local index="$1" + local uma_base + uma_base="$(find_uma_base)" + echo "${index}" > "${uma_base}/carveout" + log "Set ${uma_base}/carveout -> ${index} (applies next boot)" +} + +apply_profile() { + local profile="$1" + require_root + + local carveout_mb ttm_action ttm_gb label desc + + label="$(read_profile_value "${profile}" label)" + desc="$(read_profile_value "${profile}" description)" + carveout_mb="$(read_profile_value "${profile}" carveout_target_mb)" + ttm_action="$(read_profile_value "${profile}" ttm_action)" + ttm_gb="$(read_profile_value "${profile}" ttm_gb)" + + validate_profile "${profile}" || return 1 + + log "Applying profile: ${label} (${profile})" + log "${desc}" + + local uma_base carveout_line carveout_index + if uma_base="$(find_uma_base)"; then + mapfile -t carveout_pick < <(find_carveout_index "${uma_base}" "${carveout_mb}") + carveout_index="${carveout_pick[0]}" + carveout_line="${carveout_pick[1]:-}" + set_carveout_index "${carveout_index}" + log "Carveout: ${carveout_line}" + else + log "WARN: UMA sysfs missing — fixed VRAM unchanged." + log " Use Armoury Crate in Windows once, or update kernel/firmware." + fi + + if [[ "${ttm_action}" == "clear" ]]; then + clear_ttm_config + elif [[ -n "${ttm_gb}" ]]; then + write_ttm_config "${ttm_gb}" + fi + + regenerate_initramfs + + cat </dev/null || true +EOF +} + +show_status() { + local total_gb uma_line ttm_gb + total_gb="$(total_ram_gb)" + echo "=== Z13 Memory Status ===" + echo "Total RAM: ~${total_gb} GB" + echo "" + + if uma_base="$(find_uma_base)"; then + echo "UMA carveout (fixed VRAM):" + echo " Path: ${uma_base}" + echo " Active index: $(cat "${uma_base}/carveout" 2>/dev/null || echo '?')" + echo " Options:" + sed 's/^/ /' "${uma_base}/carveout_options" + else + echo "UMA carveout: not exposed on this kernel (use Windows Armoury Crate for fixed VRAM)" + fi + echo "" + + echo "TTM dynamic pool:" + if [[ -r "${TTM_PARAM}" ]]; then + ttm_gb="$(current_ttm_gb)" + echo " Active limit: ~${ttm_gb} GB" + else + echo " ${TTM_PARAM} not readable" + fi + if [[ -f "${TTM_CONF}" ]]; then + echo " Pending config: ${TTM_CONF}" + sed 's/^/ /' "${TTM_CONF}" + else + echo " No custom ${TTM_CONF}" + fi + echo "" + + if command -v amd-ttm >/dev/null 2>&1; then + echo "amd-ttm:" + amd-ttm 2>/dev/null | sed 's/^/ /' || true + fi +} diff --git a/package.sh b/package.sh new file mode 100755 index 0000000..e144ed1 --- /dev/null +++ b/package.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Build a transferable tarball of z13-memory-profiles + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +NAME="z13Profiler" +VERSION="1.0.0" +OUT="$(cd "${ROOT}/.." && pwd)/${NAME}-${VERSION}.tar.gz" + +cd "${ROOT}/.." +tar -czf "${OUT}" \ + --exclude='*.tar.gz' \ + --exclude='.git' \ + "${NAME}/" + +echo "Created: ${OUT}" +echo "" +echo "Transfer to Z13 (USB, scp, etc.), then on the tablet:" +echo " tar -xzf ${NAME}-${VERSION}.tar.gz" +echo " cd ${NAME}" +echo " ./install.sh" +echo " z13-memory-profiles"