Skip to content

Commit b8ca28f

Browse files
committed
v1.1.0
1 parent 188e188 commit b8ca28f

9 files changed

Lines changed: 217 additions & 35 deletions

File tree

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<picture><img align="center" src="/Img/Banner.gif"/></picture>
2-
<h4>Powered by ryzen_smu and Python</h4>
2+
<h4>Powered by AMD SMU and Python</h4>
33

44
[![GitHub Downloads](https://img.shields.io/github/downloads/HorizonUnix/UXTU4Linux/total?style=flat-square&color=blue)](https://github.com/HorizonUnix/UXTU4Linux/releases)
55
[![Python](https://img.shields.io/badge/Python-3.10%2B-yellow?style=flat-square)](https://www.python.org/)
@@ -8,7 +8,7 @@
88

99
## Overview
1010

11-
UXTU4Linux is a power management tool for **AMD Ryzen APUs and desktop CPUs** on Linux. It talks to the CPU directly through the [ryzen_smu](https://github.com/amkillam/ryzen_smu) kernel module, so you can change power limits, temperature limits and more without touching the BIOS. The interactive terminal UI runs as your normal user, and a small background daemon (systemd service) does the privileged work and auto-switches presets for you.
11+
UXTU4Linux is a power management tool for **AMD Ryzen APUs and desktop CPUs** on Linux. It talks to the CPU directly through PCI register access — no kernel module required on most systems. When Secure Boot is enabled, it uses the [ryzen_smu](https://github.com/amkillam/ryzen_smu) kernel module instead. Either way, you can change power limits, temperature limits and more without touching the BIOS. The interactive terminal UI runs as your normal user, and a small background daemon (systemd service) does the privileged work and auto-switches presets for you.
1212

1313
**What it can do:**
1414
- Built-in Eco / Balance / Performance / Extreme presets for a wide range of Ryzen APUs, desktop CPUs and Framework Laptops
@@ -29,11 +29,11 @@ UXTU4Linux is a power management tool for **AMD Ryzen APUs and desktop CPUs** on
2929
| Platform | Status |
3030
|----------|--------|
3131
| Linux with systemd, Python 3.10+ | Actively supported |
32-
| Linux without systemd (OpenRC, runit, etc.) | Works, but you start the daemon yourself |
32+
| Linux without systemd (OpenRC, runit, etc.) | Supported — installer sets everything up, you start the daemon manually |
3333
| Intel | Not supported |
3434

35-
> [!IMPORTANT]
36-
> Requires the **ryzen_smu** kernel module, version 0.1.7 or newer. The [Wiki](../../wiki) has build instructions for each distro.
35+
> [!NOTE]
36+
> **ryzen_smu is only needed when Secure Boot is enabled.** On most systems (Secure Boot off), UXTU4Linux uses PCI direct access and works out of the box. If Secure Boot is on, install ryzen_smu ≥ 0.1.7 and enroll the signing key — see the [Wiki](../../wiki) for per-distro instructions.
3737
3838
---
3939

UXTU4Linux/Assets/amd/smu.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,129 @@ def _send_pci(table: dict, default: tuple, family: str, op: int, arg0: int) -> i
238238
return SMU_FAILED
239239

240240

241+
def _smn_send_pci_get_args(fd: int, msg_addr: int, rsp_addr: int, args_addr: int,
242+
op: int, arg0: int) -> tuple[int, int, int]:
243+
_smn_write_pci(fd, rsp_addr, 0)
244+
_smn_write_pci(fd, args_addr, arg0)
245+
for i in range(1, _NARGS):
246+
_smn_write_pci(fd, args_addr + i * 4, 0)
247+
_smn_write_pci(fd, msg_addr, op)
248+
rsp = 0
249+
for _ in range(_POLL_FAST):
250+
rsp = _smn_read_pci(fd, rsp_addr)
251+
if rsp:
252+
break
253+
if not rsp:
254+
deadline = time.monotonic() + _POLL_DEADLINE_S
255+
while time.monotonic() < deadline:
256+
time.sleep(_POLL_SLEEP_S)
257+
rsp = _smn_read_pci(fd, rsp_addr)
258+
if rsp:
259+
break
260+
if not rsp:
261+
return SMU_FAILED, 0, 0
262+
out0 = _smn_read_pci(fd, args_addr)
263+
out1 = _smn_read_pci(fd, args_addr + 4)
264+
return rsp, out0, out1
265+
266+
267+
def _send_pci_rsmu_get_args(family: str, op: int, arg0: int = 0) -> tuple[int, int, int]:
268+
msg, rsp, args = _RSMU.get(family, _RSMU_DEFAULT)
269+
try:
270+
fd = os.open(_PCI_CONFIG, os.O_RDWR)
271+
try:
272+
with _lock:
273+
return _smn_send_pci_get_args(fd, msg, rsp, args, op, arg0)
274+
finally:
275+
os.close(fd)
276+
except OSError:
277+
return SMU_FAILED, 0, 0
278+
279+
280+
_PM_TABLE_CMDS: dict[str, tuple[int, int, int, bool, bool]] = {
281+
"RavenRidge": (0xC, 0xB, 0x3D, False, True),
282+
"Picasso": (0xC, 0xB, 0x3D, False, True),
283+
"Dali": (0xC, 0xB, 0x3D, False, True),
284+
"Pollock": (0xC, 0xB, 0x3D, False, True),
285+
"Renoir": (0x6, 0x66, 0x65, False, False),
286+
"Lucienne": (0x6, 0x66, 0x65, False, False),
287+
"Cezanne_Barcelo": (0x6, 0x66, 0x65, False, False),
288+
"VanGogh": (0x6, 0x66, 0x65, False, False),
289+
"Mendocino": (0x6, 0x66, 0x65, False, False),
290+
"Rembrandt": (0x6, 0x66, 0x65, True, False),
291+
"PhoenixPoint": (0x6, 0x66, 0x65, True, False),
292+
"PhoenixPoint2": (0x6, 0x66, 0x65, True, False),
293+
"HawkPoint": (0x6, 0x66, 0x65, True, False),
294+
"HawkPoint2": (0x6, 0x66, 0x65, True, False),
295+
"SonomaValley": (0x6, 0x66, 0x65, True, False),
296+
"StrixPoint": (0x6, 0x66, 0x65, True, False),
297+
"KrackanPoint": (0x6, 0x66, 0x65, True, False),
298+
"KrackanPoint2": (0x6, 0x66, 0x65, True, False),
299+
"StrixHalo": (0x6, 0x66, 0x65, True, False),
300+
}
301+
302+
_PM_TABLE_SIZES: dict[int, int] = {
303+
0x1E0001: 0x568, 0x1E0002: 0x580, 0x1E0003: 0x578,
304+
0x1E0004: 0x608, 0x1E0005: 0x608, 0x1E000A: 0x608, 0x1E0101: 0x608,
305+
0x370000: 0x794, 0x370001: 0x884, 0x370002: 0x88C,
306+
0x370003: 0x8AC, 0x370004: 0x8AC, 0x370005: 0x8C8,
307+
0x3F0000: 0x7AC,
308+
0x400001: 0x910, 0x400002: 0x928, 0x400003: 0x94C,
309+
0x400004: 0x944, 0x400005: 0x944,
310+
0x450004: 0xAA4, 0x450005: 0xAB0,
311+
0x4C0003: 0xB18, 0x4C0004: 0xB1C, 0x4C0005: 0xAF8,
312+
0x4C0006: 0xAFC, 0x4C0007: 0xB00, 0x4C0008: 0xAF0, 0x4C0009: 0xB00,
313+
0x5D0008: 0xD54, 0x5D0009: 0xD54, 0x5D000B: 0xD54,
314+
0x64020C: 0xE50,
315+
}
316+
317+
318+
def read_pm_table_pci(family: str) -> tuple[bytes, int] | None:
319+
cmds = _PM_TABLE_CMDS.get(family)
320+
if cmds is None:
321+
return None
322+
ver_cmd, addr_cmd, transfer_cmd, addr_64bit, needs_arg3 = cmds
323+
extra = 3 if needs_arg3 else 0
324+
325+
rsp, ver, _ = _send_pci_rsmu_get_args(family, ver_cmd, 0)
326+
if rsp != SMU_OK or not ver:
327+
return None
328+
329+
rsp, addr_lo, addr_hi = _send_pci_rsmu_get_args(family, addr_cmd, extra)
330+
if rsp != SMU_OK:
331+
return None
332+
phys = (addr_hi << 32 | addr_lo) if addr_64bit else addr_lo
333+
if not phys:
334+
return None
335+
336+
rsp, _, _ = _send_pci_rsmu_get_args(family, transfer_cmd, extra)
337+
if rsp == SMU_REJECTED_PREREQ:
338+
time.sleep(0.01)
339+
rsp, _, _ = _send_pci_rsmu_get_args(family, transfer_cmd, extra)
340+
if rsp != SMU_OK:
341+
return None
342+
343+
table_size = _PM_TABLE_SIZES.get(ver, 0x1000)
344+
try:
345+
import mmap as _mmap
346+
page = _mmap.PAGESIZE
347+
page_off = phys & ~(page - 1)
348+
inner = phys - page_off
349+
fd = os.open("/dev/mem", os.O_RDONLY | os.O_SYNC)
350+
try:
351+
mm = _mmap.mmap(fd, inner + table_size, _mmap.MAP_SHARED,
352+
_mmap.PROT_READ, offset=page_off)
353+
try:
354+
mm.seek(inner)
355+
return mm.read(table_size), ver
356+
finally:
357+
mm.close()
358+
finally:
359+
os.close(fd)
360+
except OSError:
361+
return None
362+
363+
241364
def status_name(code: int) -> str:
242365
return {
243366
SMU_OK: "OK",

UXTU4Linux/Assets/core/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010

1111
GITHUB_API_URL = "https://api.github.com/repos/HorizonUnix/UXTU4Linux/releases/latest"
1212
LATEST_VER_URL = "https://github.com/HorizonUnix/UXTU4Linux/releases/latest"
13-
RYZEN_SMU_WIKI_URL = "https://github.com/HorizonUnix/UXTU4Linux/wiki/Linux-Installation#2-install-the-ryzen_smu-kernel-module"
14-
INSTALL_WIKI_URL = "https://github.com/HorizonUnix/UXTU4Linux/wiki/Linux-Installation#3-install-uxtu4linux"
13+
RYZEN_SMU_WIKI_URL = "https://github.com/HorizonUnix/UXTU4Linux/wiki/Linux-Installation#2-install-ryzen_smu-secure-boot-only"
14+
DMIDECODE_WIKI_URL = "https://github.com/HorizonUnix/UXTU4Linux/wiki/Linux-Installation#3-install-uxtu4linux"
1515

1616
_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
1717
ASSETS_DIR = os.path.join(_ROOT, "Assets")

UXTU4Linux/Assets/core/hardware.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def check_binaries() -> str | None:
2222
"Fedora/RHEL : sudo dnf install dmidecode\n"
2323
"Arch : sudo pacman -S dmidecode\n"
2424
"openSUSE : sudo zypper install dmidecode\n\n"
25-
f"Install guide: {cfg.INSTALL_WIKI_URL}"
25+
f"Install guide: {cfg.DMIDECODE_WIKI_URL}"
2626
)
2727
cfg.DMIDECODE = dmi
2828
return None

UXTU4Linux/Assets/daemon/daemon.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
import json
43
import logging
54
import os
65
import signal

UXTU4Linux/Assets/system/pmtable.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,16 +122,32 @@ def _value(data, offset):
122122
return result
123123

124124

125+
def _parse(data: bytes, version: int) -> PmSample:
126+
sample = PmSample(version)
127+
for name, offset in _FIXED.items():
128+
setattr(sample, name, _value(data, offset))
129+
for name, version_map in _VERSION_MAPS.items():
130+
setattr(sample, name, _value(data, version_map.get(version)))
131+
return sample
132+
133+
125134
def read(table_path=PM_TABLE_PATH, version_path=PM_TABLE_VERSION_PATH):
126135
version = _read_version(version_path)
127136
if version is None:
128137
return None
129138
data = _read_bytes(table_path)
130139
if data is None:
131140
return None
132-
sample = PmSample(version)
133-
for name, offset in _FIXED.items():
134-
setattr(sample, name, _value(data, offset))
135-
for name, version_map in _VERSION_MAPS.items():
136-
setattr(sample, name, _value(data, version_map.get(version)))
137-
return sample
141+
return _parse(data, version)
142+
143+
144+
def read_pci(family: str) -> PmSample | None:
145+
try:
146+
from Assets.amd.smu import read_pm_table_pci
147+
result = read_pm_table_pci(family)
148+
except Exception:
149+
return None
150+
if result is None:
151+
return None
152+
data, version = result
153+
return _parse(data, version)

UXTU4Linux/Assets/system/sensors.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,12 @@ def _reconcile(trusted, secondary, tolerance, label):
201201

202202

203203
def sample():
204-
pm = pmtable.read()
204+
from Assets.amd import smu as _smu
205+
if _smu.active_backend() == "pci":
206+
from Assets.core import config as _cfg
207+
pm = pmtable.read_pci(_cfg.get("Info", "Family", ""))
208+
else:
209+
pm = pmtable.read()
205210
temp = _reconcile(_cpu_temp(), pm.tctl_temp if pm else None, 5.0, "cpu_temp")
206211
load = _reconcile(_cpu_load(), pm.cclk_busy if pm else None, 15.0, "cpu_load")
207212
pm_power = None

UXTU4Linux/Assets/tui/app.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,20 @@ def _deferred_startup(self) -> None:
105105
path_stale = not dep_error and service_path_stale()
106106

107107
client = get_client()
108-
if not client.status().get("mode"):
108+
st = client.status()
109+
if not st.get("mode"):
109110
client.apply_saved()
110111

112+
daemon_ver = st.get("version", "")
113+
if daemon_ver and daemon_ver != cfg.LOCAL_VERSION:
114+
self.call_from_thread(
115+
self.notify,
116+
f"TUI is v{cfg.LOCAL_VERSION}, daemon is v{daemon_ver}. Restart the daemon to sync.",
117+
title="Version mismatch",
118+
severity="warning",
119+
timeout=12,
120+
)
121+
111122
self.call_from_thread(self._post_startup, dep_error, path_stale)
112123

113124
def _post_startup(self, dep_error: str | None, path_stale: bool) -> None:

install.sh

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ trap '[[ -n "$TMP_DIR" && ( "$TMP_DIR" == /tmp/* || "$TMP_DIR" == /var/tmp/* ) ]
2525

2626
CURRENT_USER="$(whoami)"
2727
CURRENT_GROUP="$(id -gn)"
28+
HAS_SYSTEMD=false
29+
command -v systemctl &>/dev/null && HAS_SYSTEMD=true
2830

2931
resolve_release_tag() {
3032
local tag=""
@@ -46,23 +48,10 @@ detect_pm() {
4648
elif command -v yum &>/dev/null; then echo "yum"
4749
elif command -v pacman &>/dev/null; then echo "pacman"
4850
elif command -v zypper &>/dev/null; then echo "zypper"
49-
else die "Unsupported distro — see manual installation: https://github.com/HorizonUnix/UXTU4Linux/wiki/Linux-Installation"
51+
else echo "unknown"
5052
fi
5153
}
5254

53-
check_systemd() {
54-
if ! command -v systemctl &>/dev/null; then
55-
echo ""
56-
warn "systemd is not available on this system."
57-
info "This installer requires systemd to manage the background daemon."
58-
info "For non-systemd systems, see the manual installation guide:"
59-
info "https://github.com/HorizonUnix/UXTU4Linux/wiki/Linux-Installation"
60-
echo ""
61-
die "Unsupported init system."
62-
fi
63-
ok "systemd detected."
64-
}
65-
6655
ensure_python310() {
6756
local py=""
6857
for candidate in python3.14 python3.13 python3.12 python3.11 python3.10 python3; do
@@ -113,6 +102,9 @@ ensure_python310() {
113102
sudo zypper install -y --quiet python3 python3-pip &>/dev/null \
114103
|| die "Failed to install Python via zypper."
115104
;;
105+
unknown)
106+
die "Python 3.10+ is required but not found and cannot be installed automatically.\nInstall it with your distro's package manager and re-run."
107+
;;
116108
esac
117109

118110
for candidate in python3.14 python3.13 python3.12 python3.11 python3.10 python3; do
@@ -157,6 +149,21 @@ install_deps() {
157149
python3 python3-pip \
158150
dmidecode wget unzip curl &>/dev/null
159151
;;
152+
unknown)
153+
local missing=()
154+
command -v dmidecode &>/dev/null || missing+=("dmidecode")
155+
command -v unzip &>/dev/null || missing+=("unzip")
156+
{ command -v wget &>/dev/null || command -v curl &>/dev/null; } \
157+
|| missing+=("wget or curl")
158+
if [[ ${#missing[@]} -gt 0 ]]; then
159+
echo ""
160+
warn "No supported package manager found. Please install the following and re-run:"
161+
for pkg in "${missing[@]}"; do info " · $pkg"; done
162+
echo ""
163+
die "Missing required tools."
164+
fi
165+
ok "Required tools already present."
166+
;;
160167
esac
161168
ok "Dependencies installed."
162169
}
@@ -262,10 +269,11 @@ EOF
262269
}
263270

264271
daemon_is_installed() {
265-
[[ -f "$SERVICE_FILE" ]]
272+
$HAS_SYSTEMD && [[ -f "$SERVICE_FILE" ]]
266273
}
267274

268275
restart_daemon() {
276+
$HAS_SYSTEMD || return 0
269277
info "Restarting daemon..."
270278
sudo systemctl daemon-reload
271279
sudo systemctl restart "$SERVICE_NAME" \
@@ -320,7 +328,7 @@ uninstall() {
320328
hr
321329
echo ""
322330

323-
if command -v systemctl &>/dev/null && [[ -f "$SERVICE_FILE" ]]; then
331+
if $HAS_SYSTEMD && [[ -f "$SERVICE_FILE" ]]; then
324332
info "Removing daemon service..."
325333
sudo systemctl stop "$SERVICE_NAME" 2>/dev/null || true
326334
sudo systemctl disable "$SERVICE_NAME" 2>/dev/null || true
@@ -371,6 +379,17 @@ run_setup() {
371379
echo ""
372380
echo -e " ${_B}uxtu4linux${_R}"
373381
echo ""
382+
383+
if ! $HAS_SYSTEMD; then
384+
warn "No systemd detected — the daemon must be started manually."
385+
info "Start the daemon (needs root) before running the app:"
386+
echo ""
387+
echo -e " ${_B}sudo $VENV_PYTHON $SRC_DIR/Assets/daemon/daemon.py${_R}"
388+
echo ""
389+
info "For OpenRC / runit / s6 service examples, see the wiki:"
390+
info "https://github.com/HorizonUnix/UXTU4Linux/wiki/Linux-Installation"
391+
echo ""
392+
fi
374393
}
375394

376395
main() {
@@ -393,8 +412,17 @@ main() {
393412

394413
local pm
395414
pm="$(detect_pm)"
396-
info "Package manager: $pm"
397-
check_systemd
415+
416+
if [[ "$pm" == "unknown" ]]; then
417+
warn "No supported package manager found — checking for required tools."
418+
else
419+
info "Package manager: $pm"
420+
fi
421+
422+
if ! $HAS_SYSTEMD; then
423+
warn "systemd not found — daemon will need to be started manually after install."
424+
echo ""
425+
fi
398426

399427
if daemon_is_installed; then
400428
echo ""

0 commit comments

Comments
 (0)