-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
ZenMaster is a small, flat package. This page is the map for contributors and anyone who wants to understand how a --name=value argument turns into an SMU write.
| Module | Responsibility |
|---|---|
hardware.py |
CPU detection → CpuInfo (family codename, arch, type, family/model/stepping ints) |
runner.py |
Static data: family→socket map, per-socket opcode tables, arg lookup |
mailbox.py |
The shared mailbox send/query/poll/retry protocol, plus MP1/RSMU register addresses per family |
pmtable.py |
PM-table opcode sets and table-size metadata per family |
apply.py |
apply(): parse a preset string, look up opcodes, send, collect results |
smu.py |
Platform-agnostic SMU facade: picks the backend, status codes, exceptions |
linux.py |
Linux backend: ryzen_smu sysfs and PCI direct access |
windows.py |
Windows backend: the PawnIO driver via DeviceIoControl
|
macos.py |
macOS backend: DirectHW.kext, or the kext-free IOPCIBridge fallback |
iokit.py |
IOKit user-client plumbing for the IOPCIBridge fallback path |
directhw.py |
IOKit user-client plumbing for DirectHW.kext |
table.py |
Decode raw PM-table bytes into labeled limit/value rows, or a named PmSensors
|
cli.py |
The zenmaster command: arg parsing, help, output formatting |
update.py |
PyPI version check |
errors.py |
Exception hierarchy |
There are no comments in the source by design; the names carry the meaning.
zenmaster --stapm-limit=15000
│
cli.main() parse options, detect CPU, gate on root/admin
│
apply.apply() tokenize → normalize name → look up opcodes
│
runner.lookup() "stapm-limit" on PhoenixPoint → [(True, 0x14), ...]
│
smu.send_mp1() facade → whichever backend this OS uses
│
backend._send() require init, run the mailbox sequence
│
SMU returns 0x01 (OK)
smu.py is a thin dispatcher: at import time it picks linux, windows, or macos based on platform.system(), and every facade function (send_mp1, read_pm_table_full, close, and so on) just forwards to whichever module that turned out to be. Nothing above smu.py needs to know which OS it's running on.
-
Linux reads
/proc/cpuinfoforcpu family,model,stepping, andmodel name. -
Windows parses the
PROCESSOR_IDENTIFIERenvironment variable ("AMD64 Family 25 Model 116 Stepping 1"), then reads the marketing name from the registry. -
macOS reads
machdep.cpu.family,machdep.cpu.model,machdep.cpu.stepping, andmachdep.cpu.brand_stringviasysctlbyname. This comes straight off CPUID, so it's just as trustworthy on a Hackintosh as the other two paths are on real hardware, unlike SMBIOS, which OpenCore can spoof.
(family, model) maps to a codename (Renoir, PhoenixPoint, Raphael, and so on) through _resolve_codename, and that codename, info.family, is the key used everywhere downstream. Detection never needs privileges. detect() is memoized (@lru_cache) since CPU identity can't change mid-process; resolve(name, family_int, model_int, stepping_int=0) runs the same logic on values you supply yourself and is deliberately not cached, since it exists precisely for feeding in something other than the live-detected chip.
The type field (Amd_Apu, Amd_Desktop_Cpu, Intel, Unknown) gates support: the CLI refuses to run on anything that isn't an AMD APU or desktop CPU.
The mapping is:
codename → socket group → command table → (arg_name, is_mp1, opcode)
Socket groups: AM4_V1, FT5_FP5_AM4, AM4_V2, FP6_AM4, FF3, FT6_FP7_FP8, AM5_V1.
Each table entry is (arg_name: str, is_mp1: bool, opcode: int):
-
is_mp1=Truetargets the MP1 mailbox. -
is_mp1=Falsetargets the RSMU mailbox. - Duplicate entries for one arg across different mailboxes mean ZenMaster tries each and reports a result per attempt.
lookup(family, arg) returns every (is_mp1, opcode) pair for that arg on that family, from a lookup table precomputed once at import time. get_supported_args and all_known_args come from the same data, and is_flag_arg tells you whether an argument takes a value at all.
The mailbox handshake itself, and how each of Linux/Windows/macOS reaches the hardware underneath it, has its own page: How ZenMaster Talks to the SMU. The short version: the protocol (mailbox_send, mailbox_query, poll_response, transfer_with_retry) lives once in mailbox.py and is shared by all three backends; only the low-level register read/write primitive differs per OS.
The SMU's PM table layout changes between firmware revisions, identified by a version word (e.g. 0x00450005). read_table(data, ver) selects the right field offsets for that version and returns labeled (label, value, arg_flag) rows: the limits you can set, paired with their live values. read_sensors(data, ver) decodes the same bytes into a PmSensors dataclass instead, the compact named form behind --sensors and smu.read_pm_sensors(). Fields absent from a given version come back as None/omitted either way.
This is a RyzenAdj-style decoder focused on tunable limits and the handful of sensors RyzenAdj itself exposes. It's intentionally not a full sensor decoder; consumers wanting more (GPU/memory/socket-power fields beyond what's here) read the raw bytes from smu.read_pm_table() and decode against their own maps.
smu.py doesn't hold any hardware state itself; every function either dispatches to the active backend or is a small piece of shared logic (ensure_backend, read_pm_sensors, send_arg). Two things worth knowing:
-
send_arg(family, name, value)is the primitiveapply()builds on: it looks up every mailbox an argument maps to and sends the already-encoded value to each, catching onlyOSError,struct.error, andctypes.ArgumentErroras a hardware rejection. Anything else (a real bug in a backend) propagates instead of getting silently swallowed as "the SMU said no." -
close()releases whatever the active backend is holding, the Windows PawnIO handle, macOS's DirectHW/IOKit connection, or the cached Linux file descriptor, and resets state so a laterinit()starts clean. Useful for a long-running process that wants to drop and reacquire access rather than holding it for its whole lifetime.
The exception hierarchy:
ZenMasterError(RuntimeError)
├── BackendUnavailable # init() couldn't bring up a backend
├── SMUNotInitialized # an SMU call before init()
└── UnsupportedCPU # a family with no socket/opcode mapping
Every backend's _require_init() guard has the same shape (a bare check-and-raise, called first thing inside send_mp1/send_rsmu/query_mp1/query_rsmu), so the point where "not initialized" gets caught is consistent across platforms even though the underlying state each one guards (_backend, _handle, whatever) differs.
ZenMaster merges three implementations. When changing data tables, cross-check all three:
| Concern | Reference |
|---|---|
| Argument names, SMU opcode semantics | RyzenAdj |
| SMU opcode tables, Linux backends, detection | UXTU4Linux |
Windows PawnIO path, PROCESSOR_IDENTIFIER detection |
Universal x86 Tuning Utility |
| macOS DirectHW/IOPCIBridge access | DirectHW (joevt), pciutils' darwin2.c
|
-
pip install "zenmaster[dev]"thenpytest. Tests cover apply parsing, the runner tables, detection on all three platforms, each backend's mailbox logic, PM-table metadata, and the update check. - The data tables can be validated by diffing against the reference projects.
- The one thing the test suite can't cover is a real SMU round-trip. Validate that on real AMD hardware and confirm the SMU returns
0x01(OK) for each applied argument.
Pragmatic, human-written Python: no docstring walls, no redundant comments, no AI verbosity. Names explain themselves. Standard library only, zero mandatory third-party dependencies. Python 3.10+ (match statements are used for model dispatch).
PyPI · Report an issue · Releases · By HorizonUnix under GPL-3.0
Getting started
Monitoring
Developers
Help