Skip to content

Architecture

Le Khanh Binh edited this page Jul 2, 2026 · 1 revision

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.

Modules

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.


The flow of one apply

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.


CPU family detection (hardware.py)

  • Linux reads /proc/cpuinfo for cpu family, model, stepping, and model name.
  • Windows parses the PROCESSOR_IDENTIFIER environment 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, and machdep.cpu.brand_string via sysctlbyname. 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.


Opcode tables (runner.py)

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=True targets the MP1 mailbox.
  • is_mp1=False targets 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.


SMU mailbox protocol and the three backends

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.


PM-table decode (table.py)

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.


The facade and its exceptions (smu.py, errors.py)

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 primitive apply() builds on: it looks up every mailbox an argument maps to and sends the already-encoded value to each, catching only OSError, struct.error, and ctypes.ArgumentError as 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 later init() 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.


Reference projects

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

Testing & contributing

  • pip install "zenmaster[dev]" then pytest. 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.

Style

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).

Clone this wiki locally