zenmaster 1.2.0__tar.gz → 1.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {zenmaster-1.2.0/zenmaster.egg-info → zenmaster-1.2.2}/PKG-INFO +2 -2
  2. {zenmaster-1.2.0 → zenmaster-1.2.2}/README.md +1 -1
  3. {zenmaster-1.2.0 → zenmaster-1.2.2}/pyproject.toml +1 -1
  4. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster/RyzenSMU.bin +0 -0
  5. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster/__init__.py +1 -1
  6. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster/cli.py +9 -1
  7. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster/hardware.py +12 -1
  8. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster/linux.py +81 -26
  9. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster/macos.py +121 -18
  10. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster/mailbox.py +20 -11
  11. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster/smu.py +23 -10
  12. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster/windows.py +171 -40
  13. {zenmaster-1.2.0 → zenmaster-1.2.2/zenmaster.egg-info}/PKG-INFO +2 -2
  14. {zenmaster-1.2.0 → zenmaster-1.2.2}/LICENSE +0 -0
  15. {zenmaster-1.2.0 → zenmaster-1.2.2}/setup.cfg +0 -0
  16. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster/__main__.py +0 -0
  17. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster/apply.py +0 -0
  18. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster/directhw.py +0 -0
  19. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster/errors.py +0 -0
  20. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster/iokit.py +0 -0
  21. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster/iopci.py +0 -0
  22. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster/pmtable.py +0 -0
  23. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster/py.typed +0 -0
  24. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster/runner.py +0 -0
  25. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster/sensors.py +0 -0
  26. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster/update.py +0 -0
  27. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster.egg-info/SOURCES.txt +0 -0
  28. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster.egg-info/dependency_links.txt +0 -0
  29. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster.egg-info/entry_points.txt +0 -0
  30. {zenmaster-1.2.0 → zenmaster-1.2.2}/zenmaster.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zenmaster
3
- Version: 1.2.0
3
+ Version: 1.2.2
4
4
  Summary: Adjust power management settings for Ryzen CPUs on Linux, Windows, and macOS
5
5
  Author-email: oxGorou <lekhanhbinh2901@gmail.com>
6
6
  License-Expression: GPL-3.0-only
@@ -193,7 +193,7 @@ print(f"SMN 0x0005A380 = 0x{value:08X}")
193
193
 
194
194
  ## Technical Comparison: ZenMaster vs. RyzenAdj
195
195
 
196
- | Feature | RyzenAdj | ZenMaster v1.2.0 |
196
+ | Feature | RyzenAdj | ZenMaster v1.2.2 |
197
197
  |---|---|---|
198
198
  | **Installation** | Compilation from source (`cmake`, `make`, `libpci-dev`) | `pip install zenmaster` (pure Python package) |
199
199
  | **Supported Families** | ~15 APU and desktop families | 44 families (Zen 1 to Zen 5/6, Desktop, Mobile, Threadripper, Server EPYC via HSMP) |
@@ -162,7 +162,7 @@ print(f"SMN 0x0005A380 = 0x{value:08X}")
162
162
 
163
163
  ## Technical Comparison: ZenMaster vs. RyzenAdj
164
164
 
165
- | Feature | RyzenAdj | ZenMaster v1.2.0 |
165
+ | Feature | RyzenAdj | ZenMaster v1.2.2 |
166
166
  |---|---|---|
167
167
  | **Installation** | Compilation from source (`cmake`, `make`, `libpci-dev`) | `pip install zenmaster` (pure Python package) |
168
168
  | **Supported Families** | ~15 APU and desktop families | 44 families (Zen 1 to Zen 5/6, Desktop, Mobile, Threadripper, Server EPYC via HSMP) |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "zenmaster"
7
- version = "1.2.0"
7
+ version = "1.2.2"
8
8
  requires-python = ">=3.10"
9
9
  description = "Adjust power management settings for Ryzen CPUs on Linux, Windows, and macOS"
10
10
  readme = "README.md"
@@ -26,7 +26,7 @@ from zenmaster.errors import (
26
26
  try:
27
27
  __version__ = version("zenmaster")
28
28
  except PackageNotFoundError:
29
- __version__ = "1.2.0"
29
+ __version__ = "1.2.2"
30
30
 
31
31
  __all__ = [
32
32
  "CpuInfo", "detect", "resolve", "apply", "ApplyResult", "runner", "smu", "sensors", "table",
@@ -490,9 +490,17 @@ _SENSOR_ROWS = [
490
490
 
491
491
 
492
492
  def _show_sensors(json_out: bool, family: str = "") -> None:
493
+ if not smu.pm_table_supported(family):
494
+ msg = _pm_unavailable_msg(family)
495
+ if json_out:
496
+ print(json.dumps({"error": msg}))
497
+ else:
498
+ print(f"ZenMaster: {msg}", file=sys.stderr)
499
+ sys.exit(1)
500
+
493
501
  sensors = smu.read_pm_sensors(family)
494
502
  if sensors is None:
495
- msg = _pm_unavailable_msg(family)
503
+ msg = "Failed to read PM table"
496
504
  if json_out:
497
505
  print(json.dumps({"error": msg}))
498
506
  else:
@@ -97,7 +97,16 @@ def _libSystem() -> ctypes.CDLL | None:
97
97
  global _libc
98
98
  if _libc is None:
99
99
  try:
100
- _libc = ctypes.CDLL("/usr/lib/libSystem.B.dylib")
100
+ libc = ctypes.CDLL("/usr/lib/libSystem.B.dylib")
101
+ libc.sysctlbyname.restype = ctypes.c_int
102
+ libc.sysctlbyname.argtypes = [
103
+ ctypes.c_char_p,
104
+ ctypes.c_void_p,
105
+ ctypes.POINTER(ctypes.c_size_t),
106
+ ctypes.c_void_p,
107
+ ctypes.c_size_t,
108
+ ]
109
+ _libc = libc
101
110
  except (OSError, Exception):
102
111
  return None
103
112
  return _libc
@@ -128,6 +137,8 @@ def _sysctl_int(name: str) -> int:
128
137
  size = ctypes.c_size_t(ctypes.sizeof(val))
129
138
  if libc.sysctlbyname(name.encode(), ctypes.byref(val), ctypes.byref(size), None, 0) != 0:
130
139
  return 0
140
+ if size.value == 4:
141
+ return int(ctypes.c_int32(val.value & 0xFFFFFFFF).value)
131
142
  return int(val.value)
132
143
  except (OSError, Exception):
133
144
  return 0
@@ -11,7 +11,10 @@ from typing import Any
11
11
 
12
12
  from zenmaster.errors import BackendUnavailable, SMUNotInitialized
13
13
  from zenmaster.pmtable import PM_TABLE_CMDS, TABLE_SIZES, DEFAULT_TABLE_SIZE
14
- from zenmaster.mailbox import MP1, MP1_DEFAULT, RSMU, RSMU_DEFAULT, HSMP, HSMP_DEFAULT, NARGS
14
+ from zenmaster.mailbox import (
15
+ MP1, MP1_DEFAULT, RSMU, RSMU_DEFAULT, HSMP, HSMP_DEFAULT, NARGS,
16
+ DEFAULT_TIMEOUT, poll_response, transfer_with_retry,
17
+ )
15
18
  from zenmaster.smu import SMU_OK, SMU_FAILED, SMU_REJECTED_PREREQ, ModuleStatus
16
19
 
17
20
  DRIVER_NAME = "ryzen_smu"
@@ -27,7 +30,7 @@ _PCI_MUTEX_FALLBACK: str = "/tmp/access_pci.lock"
27
30
  _PCI_MUTEX_TIMEOUT_MS: int = 5000
28
31
 
29
32
 
30
- def acquire_pci_mutex(timeout_ms: int = _PCI_MUTEX_TIMEOUT_MS) -> int | None:
33
+ def acquire_pci_mutex(timeout_ms: int = _PCI_MUTEX_TIMEOUT_MS) -> Any:
31
34
  path = PCI_MUTEX_NAME
32
35
  fd = None
33
36
  try:
@@ -87,10 +90,11 @@ MIN_VERSION = (0, 1, 7)
87
90
 
88
91
  _POLL_N = 100
89
92
  _SLEEP_S = 0.0001
90
- _DEADLINE = 1.0
93
+ _DEADLINE = DEFAULT_TIMEOUT
91
94
  _lock = threading.Lock()
92
95
  _backend: str | None = None
93
96
  _fd: int | None = None
97
+ _cached_pm_ver: int | None = None
94
98
 
95
99
 
96
100
  def version_str(v: tuple[int, ...]) -> str:
@@ -159,7 +163,7 @@ def secure_boot_enabled() -> bool:
159
163
  try:
160
164
  out = subprocess.run(
161
165
  ["mokutil", "--sb-state"],
162
- capture_output=True, text=True, timeout=3,
166
+ capture_output=True, text=True, timeout=5,
163
167
  ).stdout.lower()
164
168
  return "enabled" in out
165
169
  except Exception:
@@ -185,6 +189,17 @@ def _pci_writable() -> bool:
185
189
  return False
186
190
 
187
191
 
192
+ def _ryzen_smu_writable() -> bool:
193
+ if not (os.path.isdir(DRIVER_PATH) and module_version_ok() and os.path.exists(SMN_PATH)):
194
+ return False
195
+ try:
196
+ fd = os.open(SMN_PATH, os.O_RDWR)
197
+ os.close(fd)
198
+ return True
199
+ except OSError:
200
+ return False
201
+
202
+
188
203
  def init() -> str:
189
204
  global _backend
190
205
 
@@ -215,6 +230,10 @@ def init() -> str:
215
230
  _backend = "pci"
216
231
  return _backend
217
232
 
233
+ if _ryzen_smu_writable():
234
+ _backend = "ryzen_smu"
235
+ return _backend
236
+
218
237
  if os.path.exists(PCI_CONFIG):
219
238
  raise BackendUnavailable(
220
239
  "Secure Boot is off, so ZenMaster uses PCI direct access, but the PCI "
@@ -256,7 +275,7 @@ def _pci_read(fd: int, addr: int) -> int:
256
275
 
257
276
  def _pci_write(fd: int, addr: int, value: int) -> None:
258
277
  os.lseek(fd, NB_ADDR, os.SEEK_SET)
259
- os.write(fd, struct.pack("<I", addr))
278
+ os.write(fd, struct.pack("<I", addr & ~3))
260
279
  os.lseek(fd, NB_DATA, os.SEEK_SET)
261
280
  os.write(fd, struct.pack("<I", value))
262
281
 
@@ -267,17 +286,7 @@ def _send_seq(write: Any, read: Any, fd: int, msg: int, rsp: int, args: int, op:
267
286
  for i in range(1, NARGS):
268
287
  write(fd, args + i * 4, 0)
269
288
  write(fd, msg, op)
270
- for _ in range(_POLL_N):
271
- r = read(fd, rsp)
272
- if r:
273
- return r
274
- deadline = time.monotonic() + _DEADLINE
275
- while time.monotonic() < deadline:
276
- time.sleep(_SLEEP_S)
277
- r = read(fd, rsp)
278
- if r:
279
- return r
280
- return SMU_FAILED
289
+ return poll_response(lambda a: read(fd, a), rsp, _POLL_N, _SLEEP_S, _DEADLINE) or SMU_FAILED
281
290
 
282
291
 
283
292
  def _require_init() -> None:
@@ -378,6 +387,8 @@ def smu_command(msg_id: int, arg: int = 0) -> int:
378
387
 
379
388
 
380
389
  def pm_table_supported(family: str = "") -> bool:
390
+ if _cached_pm_ver:
391
+ return True
381
392
  if os.path.exists(DRIVER_PATH + "/pm_table"):
382
393
  return True
383
394
  return family in PM_TABLE_CMDS
@@ -406,6 +417,9 @@ def _read_devmem(phys: int, size: int) -> bytes | None:
406
417
 
407
418
 
408
419
  def _read_pm_table_pci(family: str) -> tuple[bytes, int] | None:
420
+ global _cached_pm_ver
421
+ if _backend is None:
422
+ return None
409
423
  cmds = PM_TABLE_CMDS.get(family)
410
424
  if cmds is None:
411
425
  return None
@@ -414,7 +428,8 @@ def _read_pm_table_pci(family: str) -> tuple[bytes, int] | None:
414
428
  status, out = query_rsmu(family, ver_op, 0)
415
429
  if status != SMU_OK or not out[0]:
416
430
  return None
417
- ver = out[0]
431
+ ver = out[0] & 0xFFFFFFFF
432
+ _cached_pm_ver = ver
418
433
  size = TABLE_SIZES.get(ver, DEFAULT_TABLE_SIZE)
419
434
 
420
435
  status, out = query_rsmu(family, addr_op, extra)
@@ -424,10 +439,7 @@ def _read_pm_table_pci(family: str) -> tuple[bytes, int] | None:
424
439
  if not phys:
425
440
  return None
426
441
 
427
- status, _ = query_rsmu(family, transfer_op, extra)
428
- if status == SMU_REJECTED_PREREQ:
429
- time.sleep(0.01)
430
- status, _ = query_rsmu(family, transfer_op, extra)
442
+ status = transfer_with_retry(lambda: query_rsmu(family, transfer_op, extra)[0], (0.1, 0.5))
431
443
  if status != SMU_OK:
432
444
  return None
433
445
 
@@ -435,6 +447,19 @@ def _read_pm_table_pci(family: str) -> tuple[bytes, int] | None:
435
447
  return (data, ver) if data is not None else None
436
448
 
437
449
 
450
+ def get_smu_version(family: str = "") -> int:
451
+ if _backend in ("ryzen_smu", None):
452
+ try:
453
+ with open(DRIVER_PATH + "/version", "r") as f:
454
+ return int(f.read().strip(), 16) & 0xFFFFFFFF
455
+ except (OSError, ValueError):
456
+ pass
457
+ if _backend is None:
458
+ return 0
459
+ status, out = query_mp1(family, 0x02, 1)
460
+ return (out[0] & 0xFFFFFFFF) if status == SMU_OK and out[0] else 0
461
+
462
+
438
463
  def read_pm_table_full(family: str = "") -> tuple[bytes, int] | None:
439
464
  if _backend == "pci":
440
465
  return _read_pm_table_pci(family)
@@ -445,16 +470,45 @@ def read_pm_table_full(family: str = "") -> tuple[bytes, int] | None:
445
470
 
446
471
 
447
472
  def read_pm_table_version(family: str = "") -> int:
473
+ global _cached_pm_ver
474
+ if _cached_pm_ver:
475
+ return _cached_pm_ver
476
+
448
477
  if _backend == "pci":
478
+ if family in PM_TABLE_CMDS:
479
+ ver_op = PM_TABLE_CMDS[family][0]
480
+ status, out = query_rsmu(family, ver_op, 0)
481
+ if status == SMU_OK and out[0]:
482
+ _cached_pm_ver = out[0] & 0xFFFFFFFF
483
+ return _cached_pm_ver
449
484
  r = _read_pm_table_pci(family)
450
- return r[1] if r else 0
485
+ if r:
486
+ _cached_pm_ver = r[1] & 0xFFFFFFFF
487
+ return _cached_pm_ver
488
+ return 0
489
+
451
490
  try:
452
491
  with open(DRIVER_PATH + "/pm_table_version", "rb") as f:
453
492
  raw = f.read(4)
454
- return struct.unpack("<I", raw)[0] if len(raw) >= 4 else 0
493
+ ver = struct.unpack("<I", raw)[0] & 0xFFFFFFFF if len(raw) >= 4 else 0
494
+ if ver:
495
+ _cached_pm_ver = ver
496
+ return _cached_pm_ver
455
497
  except OSError:
456
- r = _read_pm_table_pci(family)
457
- return r[1] if r else 0
498
+ pass
499
+
500
+ if _backend is not None and family in PM_TABLE_CMDS:
501
+ ver_op = PM_TABLE_CMDS[family][0]
502
+ status, out = query_rsmu(family, ver_op, 0)
503
+ if status == SMU_OK and out[0]:
504
+ _cached_pm_ver = out[0] & 0xFFFFFFFF
505
+ return _cached_pm_ver
506
+
507
+ r = _read_pm_table_pci(family)
508
+ if r:
509
+ _cached_pm_ver = r[1] & 0xFFFFFFFF
510
+ return _cached_pm_ver
511
+ return 0
458
512
 
459
513
 
460
514
  def read_pm_table(family: str = "") -> bytes | None:
@@ -477,7 +531,7 @@ def read_pm_table(family: str = "") -> bytes | None:
477
531
 
478
532
 
479
533
  def close() -> None:
480
- global _backend, _fd
534
+ global _backend, _fd, _cached_pm_ver
481
535
  if _fd is not None:
482
536
  try:
483
537
  os.close(_fd)
@@ -485,3 +539,4 @@ def close() -> None:
485
539
  pass
486
540
  _fd = None
487
541
  _backend = None
542
+ _cached_pm_ver = None
@@ -1,5 +1,10 @@
1
1
  from __future__ import annotations
2
+ from contextlib import contextmanager
3
+ import fcntl
4
+ import os
2
5
  import threading
6
+ import time
7
+ from typing import Any
3
8
 
4
9
  from zenmaster import directhw, iopci
5
10
  from zenmaster.hardware import _sysctl_str
@@ -7,29 +12,84 @@ from zenmaster.errors import BackendUnavailable, SMUNotInitialized
7
12
  from zenmaster.pmtable import PM_TABLE_CMDS, TABLE_SIZES, DEFAULT_TABLE_SIZE
8
13
  from zenmaster.mailbox import (
9
14
  MP1, MP1_DEFAULT, RSMU, RSMU_DEFAULT, HSMP, HSMP_DEFAULT,
10
- mailbox_send, mailbox_query, transfer_with_retry,
15
+ DEFAULT_TIMEOUT, mailbox_send, mailbox_query, transfer_with_retry,
11
16
  )
12
17
  from zenmaster.smu import SMU_OK, ModuleStatus
13
18
 
14
19
  DRIVER_NAME = "DirectHW"
20
+ PCI_MUTEX_NAME: str = "/tmp/access_pci.lock"
21
+ _PCI_MUTEX_TIMEOUT_MS: int = 5000
15
22
 
16
23
  NB_ADDR = 0xB8
17
24
  NB_DATA = 0xBC
18
25
  PCI_ADDR = 0xCF8
19
26
  PCI_DATA = 0xCFC
20
27
 
21
- _POLL_N = 100
22
28
  _FAST_POLL = 64
23
29
  _POLL_SLEEP = 0.0005
30
+ _TIMEOUT = DEFAULT_TIMEOUT
24
31
  _lock = threading.Lock()
25
32
  _backend: str | None = None
26
33
  _force: str | None = None
34
+ _cached_pm_ver: int | None = None
27
35
 
28
36
  _KEXT_URL = "https://github.com/joevt/directhw"
29
37
 
30
38
  _IOPCI_DEBUG_MSG = "The IOPCIBridge path requires the debug=0x144 boot-arg."
31
39
 
32
40
 
41
+ def acquire_pci_mutex(timeout_ms: int = _PCI_MUTEX_TIMEOUT_MS) -> Any:
42
+ path = PCI_MUTEX_NAME
43
+ try:
44
+ fd = os.open(path, os.O_CREAT | os.O_RDWR, 0o666)
45
+ except OSError:
46
+ return None
47
+ deadline = time.monotonic() + (timeout_ms / 1000.0)
48
+ while True:
49
+ try:
50
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
51
+ return fd
52
+ except (BlockingIOError, OSError):
53
+ if time.monotonic() >= deadline:
54
+ try:
55
+ os.close(fd)
56
+ except OSError:
57
+ pass
58
+ return None
59
+ time.sleep(0.005)
60
+
61
+
62
+ def release_pci_mutex(handle: Any) -> bool:
63
+ if handle is None:
64
+ return False
65
+ try:
66
+ fcntl.flock(handle, fcntl.LOCK_UN)
67
+ return True
68
+ except OSError:
69
+ return False
70
+
71
+
72
+ def close_pci_mutex(handle: Any) -> bool:
73
+ if handle is None:
74
+ return False
75
+ try:
76
+ os.close(handle)
77
+ return True
78
+ except OSError:
79
+ return False
80
+
81
+
82
+ @contextmanager
83
+ def pci_mutex_guard(timeout_ms: int = _PCI_MUTEX_TIMEOUT_MS) -> Any:
84
+ h = acquire_pci_mutex(timeout_ms)
85
+ try:
86
+ yield h
87
+ finally:
88
+ if h is not None:
89
+ release_pci_mutex(h)
90
+ close_pci_mutex(h)
91
+
92
+
33
93
  def force_iopci() -> None:
34
94
  global _force
35
95
  _force = "iopci"
@@ -52,13 +112,17 @@ def module_version_ok() -> bool:
52
112
 
53
113
 
54
114
  def module_status() -> ModuleStatus:
55
- if not is_available():
115
+ if _backend == "iopci":
116
+ return ModuleStatus(True, "loaded", "", None)
117
+ if not directhw.is_loaded():
118
+ if _debug_boot_arg_ok() and iopci.is_available():
119
+ return ModuleStatus(True, "loaded", "", None)
56
120
  return ModuleStatus(False, "unknown", "", "not_loaded")
57
121
  return ModuleStatus(True, "loaded", "", None)
58
122
 
59
123
 
60
124
  def is_available() -> bool:
61
- return directhw.is_loaded() or iopci.is_available()
125
+ return directhw.is_loaded() or (_debug_boot_arg_ok() and iopci.is_available())
62
126
 
63
127
 
64
128
  def init() -> str:
@@ -140,12 +204,12 @@ def _smn_write(addr: int, value: int) -> None:
140
204
 
141
205
  def _mailbox_send(msg: int, rsp: int, args_addr: int, op: int, arg0: int) -> int:
142
206
  return mailbox_send(_smn_write, _smn_read, msg, rsp, args_addr, op, arg0,
143
- _POLL_N, _FAST_POLL, _POLL_SLEEP)
207
+ _FAST_POLL, _POLL_SLEEP, _TIMEOUT)
144
208
 
145
209
 
146
210
  def _mailbox_query(msg: int, rsp: int, args_base: int, op: int, arg0: int = 0) -> tuple[int, list[int]]:
147
211
  return mailbox_query(_smn_write, _smn_read, msg, rsp, args_base, op, arg0,
148
- _POLL_N, _FAST_POLL, _POLL_SLEEP)
212
+ _FAST_POLL, _POLL_SLEEP, _TIMEOUT)
149
213
 
150
214
 
151
215
  def _require_init() -> None:
@@ -157,14 +221,16 @@ def _send(table: dict, default: tuple, family: str, op: int, arg0: int) -> int:
157
221
  _require_init()
158
222
  msg, rsp, args = table.get(family, default)
159
223
  with _lock:
160
- return _mailbox_send(msg, rsp, args, op, arg0)
224
+ with pci_mutex_guard():
225
+ return _mailbox_send(msg, rsp, args, op, arg0)
161
226
 
162
227
 
163
228
  def _query(table: dict, default: tuple, family: str, op: int, arg0: int) -> tuple[int, list[int]]:
164
229
  _require_init()
165
230
  msg, rsp, args = table.get(family, default)
166
231
  with _lock:
167
- return _mailbox_query(msg, rsp, args, op, arg0)
232
+ with pci_mutex_guard():
233
+ return _mailbox_query(msg, rsp, args, op, arg0)
168
234
 
169
235
 
170
236
  def send_mp1(family: str, op: int, arg0: int = 0) -> int:
@@ -194,49 +260,66 @@ def query_hsmp(family: str, op: int, arg0: int = 0) -> tuple[int, list[int]]:
194
260
  def read_smn(addr: int) -> int:
195
261
  _require_init()
196
262
  with _lock:
197
- return _smn_read(addr)
263
+ with pci_mutex_guard():
264
+ return _smn_read(addr)
198
265
 
199
266
 
200
267
  def write_smn(addr: int, value: int) -> None:
201
268
  _require_init()
202
269
  with _lock:
203
- _smn_write(addr, value)
270
+ with pci_mutex_guard():
271
+ _smn_write(addr, value)
204
272
 
205
273
 
206
274
  def smu_command(msg_id: int, arg: int = 0) -> int:
207
275
  _require_init()
208
276
  msg, rsp, args = MP1_DEFAULT
209
277
  with _lock:
210
- return _mailbox_send(msg, rsp, args, msg_id, arg)
278
+ with pci_mutex_guard():
279
+ return _mailbox_send(msg, rsp, args, msg_id, arg)
280
+
281
+
282
+ def get_smu_version(family: str = "") -> int:
283
+ if _backend is None:
284
+ return 0
285
+ status, out = query_mp1(family, 0x02, 1)
286
+ return (out[0] & 0xFFFFFFFF) if status == SMU_OK and out[0] else 0
211
287
 
212
288
 
213
289
  def pm_table_supported(family: str = "") -> bool:
290
+ if _cached_pm_ver:
291
+ return True
214
292
  return _backend != "iopci" and family in PM_TABLE_CMDS
215
293
 
216
294
 
217
295
  def _transfer_with_retry(msg: int, rsp: int, args_base: int, op: int, arg0: int = 0,
218
- delays: tuple[float, ...] = (0.01, 0.1)) -> int:
296
+ delays: tuple[float, ...] = (0.1, 0.5)) -> int:
219
297
  def once() -> int:
220
298
  with _lock:
221
- return _mailbox_send(msg, rsp, args_base, op, arg0)
299
+ with pci_mutex_guard():
300
+ return _mailbox_send(msg, rsp, args_base, op, arg0)
222
301
  return transfer_with_retry(once, delays)
223
302
 
224
303
 
225
304
  def read_pm_table_full(family: str = "") -> tuple[bytes, int] | None:
305
+ global _cached_pm_ver
226
306
  if _backend in (None, "iopci") or family not in PM_TABLE_CMDS:
227
307
  return None
228
308
  ver_op, addr_op, transfer_op, addr_64bit, extra = PM_TABLE_CMDS[family]
229
309
  msg, rsp, args_base = RSMU.get(family, RSMU_DEFAULT)
230
310
 
231
311
  with _lock:
232
- status, out = _mailbox_query(msg, rsp, args_base, ver_op)
312
+ with pci_mutex_guard():
313
+ status, out = _mailbox_query(msg, rsp, args_base, ver_op)
233
314
  if status != SMU_OK or not out[0]:
234
315
  return None
235
- ver = out[0]
316
+ ver = out[0] & 0xFFFFFFFF
317
+ _cached_pm_ver = ver
236
318
  size = TABLE_SIZES.get(ver, DEFAULT_TABLE_SIZE)
237
319
 
238
320
  with _lock:
239
- status, out = _mailbox_query(msg, rsp, args_base, addr_op, extra)
321
+ with pci_mutex_guard():
322
+ status, out = _mailbox_query(msg, rsp, args_base, addr_op, extra)
240
323
  if status != SMU_OK:
241
324
  return None
242
325
  phys_addr = (out[1] << 32) | out[0] if addr_64bit else out[0]
@@ -251,8 +334,26 @@ def read_pm_table_full(family: str = "") -> tuple[bytes, int] | None:
251
334
 
252
335
 
253
336
  def read_pm_table_version(family: str = "") -> int:
337
+ global _cached_pm_ver
338
+ if _backend in (None, "iopci"):
339
+ return 0
340
+ if _cached_pm_ver:
341
+ return _cached_pm_ver
342
+
343
+ if family in PM_TABLE_CMDS:
344
+ ver_op = PM_TABLE_CMDS[family][0]
345
+ msg, rsp, args_base = RSMU.get(family, RSMU_DEFAULT)
346
+ with _lock:
347
+ with pci_mutex_guard():
348
+ status, out = _mailbox_query(msg, rsp, args_base, ver_op)
349
+ if status == SMU_OK and out[0]:
350
+ _cached_pm_ver = out[0] & 0xFFFFFFFF
351
+ return _cached_pm_ver
254
352
  r = read_pm_table_full(family)
255
- return r[1] if r else 0
353
+ if r:
354
+ _cached_pm_ver = r[1] & 0xFFFFFFFF
355
+ return _cached_pm_ver
356
+ return 0
256
357
 
257
358
 
258
359
  def read_pm_table(family: str = "") -> bytes | None:
@@ -261,7 +362,9 @@ def read_pm_table(family: str = "") -> bytes | None:
261
362
 
262
363
 
263
364
  def close() -> None:
264
- global _backend
365
+ global _backend, _cached_pm_ver, DRIVER_NAME
265
366
  directhw.close()
266
367
  iopci.close()
368
+ DRIVER_NAME = "DirectHW"
267
369
  _backend = None
370
+ _cached_pm_ver = None
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
  import time
3
+ from typing import Any
3
4
 
4
5
  from zenmaster.smu import SMU_FAILED, SMU_REJECTED_PREREQ
5
6
 
6
7
  NARGS = 6
8
+ DEFAULT_TIMEOUT: float = 5.0
7
9
 
8
10
  MP1: dict[str, tuple[int, int, int]] = {
9
11
  "SummitRidge": (0x3B10528, 0x3B10564, 0x3B10598),
@@ -49,41 +51,48 @@ HSMP: dict[str, tuple[int, int, int]] = {
49
51
  HSMP_DEFAULT = (0x3B10A8C, 0x3B10A90, 0x3B10A94)
50
52
 
51
53
 
52
- def poll_response(smn_read, rsp: int, poll_n: int, fast_poll: int, poll_sleep: float) -> int:
53
- for i in range(poll_n):
54
+ def poll_response(smn_read: Any, rsp: int, fast_poll: int = 64, poll_sleep: float = 0.0005,
55
+ timeout: float = DEFAULT_TIMEOUT) -> int:
56
+ for _ in range(fast_poll):
57
+ r = smn_read(rsp)
58
+ if r:
59
+ return r
60
+ deadline = time.monotonic() + timeout
61
+ while time.monotonic() < deadline:
62
+ time.sleep(poll_sleep)
54
63
  r = smn_read(rsp)
55
64
  if r:
56
65
  return r
57
- if i >= fast_poll:
58
- time.sleep(poll_sleep)
59
66
  return 0
60
67
 
61
68
 
62
- def mailbox_send(smn_write, smn_read, msg: int, rsp: int, args_addr: int, op: int, arg0: int,
63
- poll_n: int, fast_poll: int, poll_sleep: float) -> int:
69
+ def mailbox_send(smn_write: Any, smn_read: Any, msg: int, rsp: int, args_addr: int, op: int, arg0: int,
70
+ fast_poll: int = 64, poll_sleep: float = 0.0005,
71
+ timeout: float = DEFAULT_TIMEOUT) -> int:
64
72
  smn_write(rsp, 0)
65
73
  smn_write(args_addr, arg0)
66
74
  for i in range(1, NARGS):
67
75
  smn_write(args_addr + i * 4, 0)
68
76
  smn_write(msg, op)
69
- return poll_response(smn_read, rsp, poll_n, fast_poll, poll_sleep) or SMU_FAILED
77
+ return poll_response(smn_read, rsp, fast_poll, poll_sleep, timeout) or SMU_FAILED
70
78
 
71
79
 
72
- def mailbox_query(smn_write, smn_read, msg: int, rsp: int, args_base: int, op: int, arg0: int,
73
- poll_n: int, fast_poll: int, poll_sleep: float) -> tuple[int, list[int]]:
80
+ def mailbox_query(smn_write: Any, smn_read: Any, msg: int, rsp: int, args_base: int, op: int, arg0: int,
81
+ fast_poll: int = 64, poll_sleep: float = 0.0005,
82
+ timeout: float = DEFAULT_TIMEOUT) -> tuple[int, list[int]]:
74
83
  smn_write(rsp, 0)
75
84
  for i in range(NARGS):
76
85
  smn_write(args_base + i * 4, 0)
77
86
  if arg0:
78
87
  smn_write(args_base, arg0)
79
88
  smn_write(msg, op)
80
- r = poll_response(smn_read, rsp, poll_n, fast_poll, poll_sleep)
89
+ r = poll_response(smn_read, rsp, fast_poll, poll_sleep, timeout)
81
90
  if r:
82
91
  return r, [smn_read(args_base + i * 4) for i in range(NARGS)]
83
92
  return SMU_FAILED, [0] * NARGS
84
93
 
85
94
 
86
- def transfer_with_retry(send_once, delays: tuple[float, ...] = (0.01, 0.1)) -> int:
95
+ def transfer_with_retry(send_once, delays: tuple[float, ...] = (0.1, 0.5)) -> int:
87
96
  status = send_once()
88
97
  for delay in delays:
89
98
  if status != SMU_REJECTED_PREREQ:
@@ -58,10 +58,8 @@ def _backend() -> Any:
58
58
 
59
59
  def smu_command(msg_id: int, arg: int = 0) -> int:
60
60
  b = _backend() if callable(_backend) else _backend
61
- fn = getattr(b, "smu_command", None)
62
- if fn is not None:
63
- return fn(msg_id, arg)
64
- ensure_backend()
61
+ if getattr(b, "active_backend", None) is not None and not ensure_backend():
62
+ return SMU_FAILED
65
63
  b = _backend() if callable(_backend) else _backend
66
64
  fn = getattr(b, "smu_command", None)
67
65
  if fn is not None:
@@ -116,18 +114,21 @@ def pm_table_supported(family: str = "") -> bool:
116
114
 
117
115
 
118
116
  def read_pm_table(family: str = "") -> bytes | None:
117
+ ensure_backend()
119
118
  b = _backend()
120
119
  fn = getattr(b, "read_pm_table", None)
121
120
  return fn(family) if fn else None
122
121
 
123
122
 
124
123
  def read_pm_table_version(family: str = "") -> int:
124
+ ensure_backend()
125
125
  b = _backend()
126
126
  fn = getattr(b, "read_pm_table_version", None)
127
127
  return fn(family) if fn else 0
128
128
 
129
129
 
130
130
  def read_pm_table_full(family: str = "") -> tuple[bytes, int] | None:
131
+ ensure_backend()
131
132
  b = _backend()
132
133
  fn = getattr(b, "read_pm_table_full", None)
133
134
  if fn:
@@ -191,7 +192,9 @@ def send_arg(family: str, name: str, value: int) -> list[tuple[str, int, int]]:
191
192
  from zenmaster.errors import UnsupportedCPU
192
193
  if not runner.is_supported(family):
193
194
  raise UnsupportedCPU(f"'{family}' is not a supported CPU family")
194
- ensure_backend()
195
+ b = _backend() if callable(_backend) else _backend
196
+ if getattr(b, "active_backend", None) is not None and active_backend() is None:
197
+ init()
195
198
  out: list[tuple[str, int, int]] = []
196
199
  use_hsmp = runner.is_hsmp(family)
197
200
  for is_mp1, op in runner.lookup(family, name):
@@ -225,16 +228,26 @@ def unavailable_reason() -> str | None:
225
228
 
226
229
 
227
230
  def get_bios_if_ver(family: str) -> int:
228
- ensure_backend()
231
+ if not ensure_backend():
232
+ return 0
229
233
  status, out = query_mp1(family, 0x03, 0)
230
234
  return out[0] if status == SMU_OK else 0
231
235
 
232
236
 
233
237
  def get_smu_version(family: str) -> int:
234
- ensure_backend()
235
- status, out = query_mp1(family, 0x02, 1)
236
- if status == SMU_OK and out[0]:
237
- return out[0]
238
+ if ensure_backend():
239
+ b = _backend()
240
+ fn = getattr(b, "get_smu_version", None)
241
+ if fn is not None:
242
+ try:
243
+ ver = fn(family)
244
+ if ver:
245
+ return ver
246
+ except Exception:
247
+ pass
248
+ status, out = query_mp1(family, 0x02, 1)
249
+ if status == SMU_OK and out[0]:
250
+ return out[0]
238
251
  if platform.system() == "Linux":
239
252
  for path in ("/sys/kernel/ryzen_smu_drv/version", "/sys/kernel/ryzen_smu_drv/drv_version"):
240
253
  try:
@@ -11,7 +11,7 @@ from zenmaster.errors import BackendUnavailable, SMUNotInitialized
11
11
  from zenmaster.pmtable import PM_TABLE_CMDS, TABLE_SIZES, DEFAULT_TABLE_SIZE
12
12
  from zenmaster.mailbox import (
13
13
  MP1, MP1_DEFAULT, RSMU, RSMU_DEFAULT, HSMP, HSMP_DEFAULT,
14
- mailbox_send, mailbox_query, transfer_with_retry,
14
+ DEFAULT_TIMEOUT, mailbox_send, mailbox_query, transfer_with_retry,
15
15
  )
16
16
  from zenmaster.smu import SMU_OK, ModuleStatus
17
17
 
@@ -24,9 +24,9 @@ _DEVICE_PATHS = [
24
24
  ]
25
25
  _IOCTL_LOAD = 0xA1B22084
26
26
  _IOCTL_EXEC = 0xA1B22104
27
- _POLL_N = 8192
28
27
  _FAST_POLL = 64
29
28
  _POLL_SLEEP = 0.0005
29
+ _TIMEOUT = DEFAULT_TIMEOUT
30
30
  _lock = threading.Lock()
31
31
  _handle = None
32
32
  _k32 = None
@@ -56,6 +56,8 @@ def _query_pawnio_info() -> str | None:
56
56
  return winreg.QueryValueEx(key, "DisplayVersion")[0]
57
57
  except OSError:
58
58
  return ""
59
+ finally:
60
+ winreg.CloseKey(key)
59
61
  except OSError:
60
62
  return None
61
63
 
@@ -81,7 +83,7 @@ def module_status() -> ModuleStatus:
81
83
 
82
84
 
83
85
  def is_available() -> bool:
84
- return _pawnio_info() is not None
86
+ return _handle is not None or _pawnio_info() is not None
85
87
 
86
88
 
87
89
  def acquire_pci_mutex(timeout_ms: int = _PCI_MUTEX_TIMEOUT_MS) -> Any:
@@ -186,16 +188,18 @@ def init() -> str:
186
188
  raise BackendUnavailable(f"PawnIO module not found: {module_path}")
187
189
 
188
190
  ver = _pawnio_info()
189
- if ver is None:
190
- raise BackendUnavailable(
191
- "PawnIO driver is not installed.\n"
192
- f"Download and run the installer: {_PAWNIO_INSTALLER_URL}\n"
193
- "After installation, reboot and try again."
194
- )
195
-
196
- k32 = _make_k32()
191
+ try:
192
+ k32 = _make_k32()
193
+ except AttributeError as e:
194
+ raise BackendUnavailable("PawnIO driver is only supported on Windows.") from e
197
195
  handle = _open_device(k32)
198
196
  if handle is None:
197
+ if ver is None:
198
+ raise BackendUnavailable(
199
+ "PawnIO driver is not installed.\n"
200
+ f"Download and run the installer: {_PAWNIO_INSTALLER_URL}\n"
201
+ "After installation, reboot and try again."
202
+ )
199
203
  raise BackendUnavailable(
200
204
  "PawnIO device not found. The driver may need a reboot to activate.\n"
201
205
  f"If not installed: {_PAWNIO_INSTALLER_URL}"
@@ -240,6 +244,10 @@ def _require_init() -> None:
240
244
  raise SMUNotInitialized("PawnIO not initialized, call smu.init() first")
241
245
 
242
246
 
247
+ _cached_pm_ver: int | None = None
248
+ _cached_pm_size: int | None = None
249
+
250
+
243
251
  def _execute(fn_name: str, in_args: list[int], out_count: int) -> list[int]:
244
252
  fn_bytes = fn_name.encode("ascii")[:31]
245
253
  name_buf = struct.pack("32s", fn_bytes)
@@ -265,6 +273,27 @@ def _execute(fn_name: str, in_args: list[int], out_count: int) -> list[int]:
265
273
  return list(struct.unpack(f"<{count}q", out_buf.raw[: count * 8]))
266
274
 
267
275
 
276
+ def _execute_cmd(fn_name: str, in_args: list[int] | None = None) -> bool:
277
+ if in_args is None:
278
+ in_args = []
279
+ fn_bytes = fn_name.encode("ascii")[:31]
280
+ name_buf = struct.pack("32s", fn_bytes)
281
+ args_buf = struct.pack(f"<{len(in_args)}q", *in_args) if in_args else b""
282
+ payload = name_buf + args_buf
283
+ in_buf = ctypes.create_string_buffer(payload)
284
+ ret = ctypes.wintypes.DWORD(0)
285
+
286
+ ok = _k32.DeviceIoControl(
287
+ ctypes.wintypes.HANDLE(_handle),
288
+ ctypes.wintypes.DWORD(_IOCTL_EXEC),
289
+ ctypes.cast(in_buf, ctypes.c_void_p),
290
+ ctypes.wintypes.DWORD(len(payload)),
291
+ None, 0,
292
+ ctypes.byref(ret), None,
293
+ )
294
+ return bool(ok)
295
+
296
+
268
297
  def _smn_read(addr: int) -> int:
269
298
  result = _execute("ioctl_read_smu_register", [addr], 1)
270
299
  return result[0] & 0xFFFFFFFF if result else 0
@@ -276,24 +305,61 @@ def _smn_write(addr: int, value: int) -> None:
276
305
 
277
306
  def _mailbox_send(msg: int, rsp: int, args_addr: int, op: int, arg0: int) -> int:
278
307
  return mailbox_send(_smn_write, _smn_read, msg, rsp, args_addr, op, arg0,
279
- _POLL_N, _FAST_POLL, _POLL_SLEEP)
308
+ _FAST_POLL, _POLL_SLEEP, _TIMEOUT)
280
309
 
281
310
 
282
311
  def _mailbox_query(msg: int, rsp: int, args_base: int, op: int, arg0: int = 0) -> tuple[int, list[int]]:
283
312
  return mailbox_query(_smn_write, _smn_read, msg, rsp, args_base, op, arg0,
284
- _POLL_N, _FAST_POLL, _POLL_SLEEP)
313
+ _FAST_POLL, _POLL_SLEEP, _TIMEOUT)
314
+
315
+
316
+ def _resolve_pm_table() -> tuple[int, int] | None:
317
+ _require_init()
318
+ import time
319
+ for delay in (0.0, 0.1, 0.5):
320
+ if delay:
321
+ time.sleep(delay)
322
+ with _lock:
323
+ with pci_mutex_guard():
324
+ out = _execute("ioctl_resolve_pm_table", [], 2)
325
+ if len(out) == 2 and out[0] != 0:
326
+ return (out[0] & 0xFFFFFFFF), out[1]
327
+ return None
328
+
329
+
330
+ def _update_pm_table(family: str = "") -> bool:
331
+ _require_init()
332
+ import time
333
+ for delay in (0.0, 0.1, 0.5):
334
+ if delay:
335
+ time.sleep(delay)
336
+ with _lock:
337
+ with pci_mutex_guard():
338
+ ok = _execute_cmd("ioctl_update_pm_table")
339
+ if ok:
340
+ return True
341
+ if family in PM_TABLE_CMDS:
342
+ _ver_op, _addr_op, transfer_op, _addr_64bit, extra = PM_TABLE_CMDS[family]
343
+ msg, rsp, args_base = RSMU.get(family, RSMU_DEFAULT)
344
+ status = _transfer_with_retry(msg, rsp, args_base, transfer_op, extra)
345
+ return status == SMU_OK
346
+ return False
285
347
 
286
348
 
287
349
  def _read_physical_memory(phys_addr: int, size: int) -> bytes | None:
288
- n = (size + 7) // 8
289
- raw = _execute("ioctl_read_pm_table", [phys_addr, n], n)
290
- if len(raw) == n:
291
- return struct.pack(f"<{n}q", *raw)[:size]
350
+ n = min((size + 7) // 8, 512)
351
+ with _lock:
352
+ raw = _execute("ioctl_read_pm_table", [], n)
353
+ if raw:
354
+ data = struct.pack(f"<{len(raw)}q", *raw)[:size]
355
+ if len(data) < size:
356
+ data = data + b"\x00" * (size - len(data))
357
+ return data
292
358
  return None
293
359
 
294
360
 
295
361
  def _transfer_with_retry(msg: int, rsp: int, args_base: int, op: int, arg0: int = 0,
296
- delays: tuple[float, ...] = (0.01, 0.1)) -> int:
362
+ delays: tuple[float, ...] = (0.1, 0.5)) -> int:
297
363
  def once() -> int:
298
364
  with _lock:
299
365
  with pci_mutex_guard():
@@ -363,43 +429,105 @@ def smu_command(msg_id: int, arg: int = 0) -> int:
363
429
  return _mailbox_send(msg, rsp, args, msg_id, arg)
364
430
 
365
431
 
432
+ def get_smu_version(family: str = "") -> int:
433
+ if _handle is None or _k32 is None:
434
+ return 0
435
+ with _lock:
436
+ with pci_mutex_guard():
437
+ out = _execute("ioctl_get_smu_version", [], 1)
438
+ if out and out[0]:
439
+ return out[0] & 0xFFFFFFFF
440
+ msg, rsp, args = MP1.get(family, MP1_DEFAULT)
441
+ with _lock:
442
+ with pci_mutex_guard():
443
+ status, res = _mailbox_query(msg, rsp, args, 0x02, 1)
444
+ return (res[0] & 0xFFFFFFFF) if status == SMU_OK and res[0] else 0
445
+
446
+
366
447
  def pm_table_supported(family: str = "") -> bool:
448
+ if _cached_pm_ver:
449
+ return True
367
450
  return family in PM_TABLE_CMDS
368
451
 
369
452
 
370
- def read_pm_table_full(family: str = "") -> tuple[bytes, int] | None:
371
- if not _handle or family not in PM_TABLE_CMDS:
372
- return None
373
- ver_op, addr_op, transfer_op, addr_64bit, extra = PM_TABLE_CMDS[family]
374
- msg, rsp, args_base = RSMU.get(family, RSMU_DEFAULT)
453
+ def _cache_pm_ver(ver: int) -> int:
454
+ global _cached_pm_ver, _cached_pm_size
455
+ _cached_pm_ver = ver & 0xFFFFFFFF
456
+ _cached_pm_size = TABLE_SIZES.get(_cached_pm_ver, DEFAULT_TABLE_SIZE)
457
+ return _cached_pm_ver
375
458
 
459
+
460
+ def _read_pm_ver_rsmu(family: str = "") -> int:
461
+ if _handle is None or _k32 is None or family not in PM_TABLE_CMDS:
462
+ return 0
463
+ ver_op = PM_TABLE_CMDS[family][0]
464
+ msg, rsp, args_base = RSMU.get(family, RSMU_DEFAULT)
376
465
  with _lock:
377
466
  with pci_mutex_guard():
378
467
  status, out = _mailbox_query(msg, rsp, args_base, ver_op)
379
- if status != SMU_OK or not out[0]:
380
- return None
381
- ver = out[0]
382
- size = TABLE_SIZES.get(ver, DEFAULT_TABLE_SIZE)
468
+ return (out[0] & 0xFFFFFFFF) if status == SMU_OK and out[0] else 0
383
469
 
384
- with _lock:
385
- with pci_mutex_guard():
386
- status, out = _mailbox_query(msg, rsp, args_base, addr_op, extra)
387
- if status != SMU_OK:
470
+
471
+ def read_pm_table_full(family: str = "") -> tuple[bytes, int] | None:
472
+ if _handle is None or _k32 is None:
388
473
  return None
389
- phys_addr = (out[1] << 32) | out[0] if addr_64bit else out[0]
390
- if not phys_addr:
474
+
475
+ global _cached_pm_ver, _cached_pm_size
476
+
477
+ if _cached_pm_ver is None:
478
+ resolved = _resolve_pm_table()
479
+ if resolved is not None and resolved[0]:
480
+ _cache_pm_ver(resolved[0])
481
+ elif family in PM_TABLE_CMDS:
482
+ ver = _read_pm_ver_rsmu(family)
483
+ if ver:
484
+ _cache_pm_ver(ver)
485
+
486
+ if _cached_pm_ver is None or not _cached_pm_ver:
391
487
  return None
392
488
 
393
- status = _transfer_with_retry(msg, rsp, args_base, transfer_op, extra)
394
- if status != SMU_OK:
489
+ ver = _cached_pm_ver & 0xFFFFFFFF
490
+ if _cached_pm_size is None:
491
+ _cached_pm_size = TABLE_SIZES.get(ver, DEFAULT_TABLE_SIZE)
492
+ size = _cached_pm_size
493
+
494
+ _update_pm_table(family)
495
+
496
+ n = min((size + 7) // 8, 512)
497
+ with _lock:
498
+ raw = _execute("ioctl_read_pm_table", [], n)
499
+
500
+ if not raw:
501
+ resolved = _resolve_pm_table()
502
+ if resolved is not None and resolved[0]:
503
+ ver = _cache_pm_ver(resolved[0])
504
+ size = _cached_pm_size or DEFAULT_TABLE_SIZE
505
+ n = min((size + 7) // 8, 512)
506
+ _update_pm_table(family)
507
+ with _lock:
508
+ raw = _execute("ioctl_read_pm_table", [], n)
509
+
510
+ if not raw:
395
511
  return None
396
- data = _read_physical_memory(phys_addr, size)
397
- return (data, ver) if data is not None else None
512
+
513
+ data = struct.pack(f"<{len(raw)}q", *raw)[:size]
514
+ if len(data) < size:
515
+ data = data + b"\x00" * (size - len(data))
516
+ return (data, ver)
398
517
 
399
518
 
400
519
  def read_pm_table_version(family: str = "") -> int:
401
- r = read_pm_table_full(family)
402
- return r[1] if r else 0
520
+ if _cached_pm_ver:
521
+ return _cached_pm_ver
522
+ if _handle is None or _k32 is None:
523
+ return 0
524
+ resolved = _resolve_pm_table()
525
+ if resolved and resolved[0]:
526
+ return _cache_pm_ver(resolved[0])
527
+ ver = _read_pm_ver_rsmu(family)
528
+ if ver:
529
+ return _cache_pm_ver(ver)
530
+ return 0
403
531
 
404
532
 
405
533
  def read_pm_table(family: str = "") -> bytes | None:
@@ -408,9 +536,12 @@ def read_pm_table(family: str = "") -> bytes | None:
408
536
 
409
537
 
410
538
  def close() -> None:
411
- global _handle, _k32, _pawnio_info_cache
539
+ global _handle, _k32, _pawnio_info_cache, _cached_pm_ver, _cached_pm_size
412
540
  if _handle is not None and _k32 is not None:
413
541
  _k32.CloseHandle(ctypes.wintypes.HANDLE(_handle))
414
542
  _handle = None
415
543
  _k32 = None
416
544
  _pawnio_info_cache = _UNSET
545
+ _cached_pm_ver = None
546
+ _cached_pm_size = None
547
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zenmaster
3
- Version: 1.2.0
3
+ Version: 1.2.2
4
4
  Summary: Adjust power management settings for Ryzen CPUs on Linux, Windows, and macOS
5
5
  Author-email: oxGorou <lekhanhbinh2901@gmail.com>
6
6
  License-Expression: GPL-3.0-only
@@ -193,7 +193,7 @@ print(f"SMN 0x0005A380 = 0x{value:08X}")
193
193
 
194
194
  ## Technical Comparison: ZenMaster vs. RyzenAdj
195
195
 
196
- | Feature | RyzenAdj | ZenMaster v1.2.0 |
196
+ | Feature | RyzenAdj | ZenMaster v1.2.2 |
197
197
  |---|---|---|
198
198
  | **Installation** | Compilation from source (`cmake`, `make`, `libpci-dev`) | `pip install zenmaster` (pure Python package) |
199
199
  | **Supported Families** | ~15 APU and desktop families | 44 families (Zen 1 to Zen 5/6, Desktop, Mobile, Threadripper, Server EPYC via HSMP) |
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes