gpu-aux 1.2.0__py3-none-any.whl
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.
- gpu_aux/__init__.py +20 -0
- gpu_aux/adl.py +370 -0
- gpu_aux/api.py +240 -0
- gpu_aux/intel.py +563 -0
- gpu_aux/nvapi.py +316 -0
- gpu_aux-1.2.0.dist-info/METADATA +119 -0
- gpu_aux-1.2.0.dist-info/RECORD +10 -0
- gpu_aux-1.2.0.dist-info/WHEEL +5 -0
- gpu_aux-1.2.0.dist-info/licenses/LICENSE +21 -0
- gpu_aux-1.2.0.dist-info/top_level.txt +1 -0
gpu_aux/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Direct Python access to DisplayPort AUX functions."""
|
|
2
|
+
|
|
3
|
+
from .adl import Adapter, AmdAux, AuxError, Port
|
|
4
|
+
from .api import AuxPort, GpuPorts, enumerate_gpus, enumerate_gpus_and_ports, enumerate_ports
|
|
5
|
+
from .intel import IntelAux
|
|
6
|
+
from .nvapi import NvidiaAux
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Adapter",
|
|
10
|
+
"AmdAux",
|
|
11
|
+
"AuxError",
|
|
12
|
+
"AuxPort",
|
|
13
|
+
"GpuPorts",
|
|
14
|
+
"IntelAux",
|
|
15
|
+
"NvidiaAux",
|
|
16
|
+
"Port",
|
|
17
|
+
"enumerate_gpus",
|
|
18
|
+
"enumerate_gpus_and_ports",
|
|
19
|
+
"enumerate_ports",
|
|
20
|
+
]
|
gpu_aux/adl.py
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from ctypes import POINTER, Structure, byref, c_char, c_int, c_uint8, c_uint32, c_void_p
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
ADL_OK = 0
|
|
11
|
+
ADL_DISPLAY_CONNECTED = 1
|
|
12
|
+
AMD_VENDOR_IDS = {1002, 0x1002}
|
|
13
|
+
ADL_CONNECTOR_DISPLAY_PORT = 15
|
|
14
|
+
ADL_CONNECTOR_EDP = 16
|
|
15
|
+
AUX_CONNECTORS = {ADL_CONNECTOR_DISPLAY_PORT, ADL_CONNECTOR_EDP}
|
|
16
|
+
AUX_CHUNK_SIZE = 16
|
|
17
|
+
RETRY_COUNT = 10
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AuxError(RuntimeError):
|
|
21
|
+
"""Raised when ADL or an AUX transaction fails."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _ADLAdapterInfo(Structure):
|
|
25
|
+
_fields_ = [
|
|
26
|
+
("iSize", c_int),
|
|
27
|
+
("iAdapterIndex", c_int),
|
|
28
|
+
("strUDID", c_char * 256),
|
|
29
|
+
("iBusNumber", c_int),
|
|
30
|
+
("iDeviceNumber", c_int),
|
|
31
|
+
("iFunctionNumber", c_int),
|
|
32
|
+
("iVendorID", c_int),
|
|
33
|
+
("strAdapterName", c_char * 256),
|
|
34
|
+
("strDisplayName", c_char * 256),
|
|
35
|
+
("iPresent", c_int),
|
|
36
|
+
("iExist", c_int),
|
|
37
|
+
("strDriverPath", c_char * 256),
|
|
38
|
+
("strDriverPathExt", c_char * 256),
|
|
39
|
+
("strPNPString", c_char * 256),
|
|
40
|
+
("iOSDisplayIndex", c_int),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _ADLDisplayID(Structure):
|
|
45
|
+
_fields_ = [
|
|
46
|
+
("iDisplayLogicalIndex", c_int),
|
|
47
|
+
("iDisplayPhysicalIndex", c_int),
|
|
48
|
+
("iDisplayLogicalAdapterIndex", c_int),
|
|
49
|
+
("iDisplayPhysicalAdapterIndex", c_int),
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class _ADLDisplayInfo(Structure):
|
|
54
|
+
_fields_ = [
|
|
55
|
+
("displayID", _ADLDisplayID),
|
|
56
|
+
("iDisplayControllerIndex", c_int),
|
|
57
|
+
("strDisplayName", c_char * 256),
|
|
58
|
+
("strDisplayManufacturerName", c_char * 256),
|
|
59
|
+
("iDisplayType", c_int),
|
|
60
|
+
("iDisplayOutputType", c_int),
|
|
61
|
+
("iDisplayConnector", c_int),
|
|
62
|
+
("iDisplayInfoMask", c_int),
|
|
63
|
+
("iDisplayInfoValue", c_int),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class _NativeAuxRequest(Structure):
|
|
68
|
+
_fields_ = [
|
|
69
|
+
("size", c_uint32),
|
|
70
|
+
("result", c_uint32),
|
|
71
|
+
("operation", c_uint32),
|
|
72
|
+
("address", c_uint32),
|
|
73
|
+
("data_size", c_uint32),
|
|
74
|
+
("data", c_uint8 * AUX_CHUNK_SIZE),
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
assert ctypes.sizeof(_ADLAdapterInfo) == 0x624
|
|
79
|
+
assert ctypes.sizeof(_ADLDisplayInfo) == 0x228
|
|
80
|
+
assert ctypes.sizeof(_NativeAuxRequest) == 0x24
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _text(value: bytes) -> str:
|
|
84
|
+
return value.split(b"\0", 1)[0].decode("utf-8", errors="replace")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class Adapter:
|
|
89
|
+
index: int
|
|
90
|
+
name: str
|
|
91
|
+
display_name: str
|
|
92
|
+
bus: int
|
|
93
|
+
device: int
|
|
94
|
+
function: int
|
|
95
|
+
backend: str = "AMD"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class Port:
|
|
100
|
+
adapter: Adapter
|
|
101
|
+
index: int
|
|
102
|
+
logical_display_index: int
|
|
103
|
+
physical_display_index: int
|
|
104
|
+
name: str
|
|
105
|
+
manufacturer: str
|
|
106
|
+
display_type: int
|
|
107
|
+
output_type: int
|
|
108
|
+
connector: int
|
|
109
|
+
connected: bool
|
|
110
|
+
backend: str = "adl"
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def identity(self) -> str:
|
|
114
|
+
return f"{self.backend}:{self.adapter.index}:{self.logical_display_index}"
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def kind(self) -> str:
|
|
118
|
+
return "eDP" if self.connector == ADL_CONNECTOR_EDP else "DP"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class AmdAux:
|
|
122
|
+
"""Direct binding to AMD ADL AUX and DDC functions.
|
|
123
|
+
|
|
124
|
+
ADL is process-global, so one instance serializes all transactions.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(self) -> None:
|
|
128
|
+
if ctypes.sizeof(c_void_p) != 8:
|
|
129
|
+
raise AuxError("gpu_aux requires 64-bit Python")
|
|
130
|
+
self._lock = threading.RLock()
|
|
131
|
+
self._closed = False
|
|
132
|
+
self._crt = ctypes.CDLL("msvcrt")
|
|
133
|
+
self._crt.malloc.argtypes = [ctypes.c_size_t]
|
|
134
|
+
self._crt.malloc.restype = c_void_p
|
|
135
|
+
self._crt.free.argtypes = [c_void_p]
|
|
136
|
+
self._alloc_type = ctypes.CFUNCTYPE(c_void_p, c_int)
|
|
137
|
+
self._allocator = self._alloc_type(self._allocate)
|
|
138
|
+
self._adl = self._load_adl()
|
|
139
|
+
self._bind()
|
|
140
|
+
self._check(self._adl.ADL_Main_Control_Create(self._allocator, 1), "ADL initialization")
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def _load_adl():
|
|
144
|
+
for name in ("atiadlxx.dll", "atiadlxy.dll"):
|
|
145
|
+
try:
|
|
146
|
+
return ctypes.CDLL(name)
|
|
147
|
+
except OSError:
|
|
148
|
+
pass
|
|
149
|
+
raise AuxError("AMD ADL library atiadlxx.dll was not found")
|
|
150
|
+
|
|
151
|
+
def _allocate(self, size: int):
|
|
152
|
+
return self._crt.malloc(size)
|
|
153
|
+
|
|
154
|
+
def _bind(self) -> None:
|
|
155
|
+
adl = self._adl
|
|
156
|
+
adl.ADL_Main_Control_Create.argtypes = [self._alloc_type, c_int]
|
|
157
|
+
adl.ADL_Main_Control_Create.restype = c_int
|
|
158
|
+
adl.ADL_Main_Control_Destroy.argtypes = []
|
|
159
|
+
adl.ADL_Main_Control_Destroy.restype = c_int
|
|
160
|
+
adl.ADL_Adapter_NumberOfAdapters_Get.argtypes = [POINTER(c_int)]
|
|
161
|
+
adl.ADL_Adapter_NumberOfAdapters_Get.restype = c_int
|
|
162
|
+
adl.ADL_Adapter_AdapterInfo_Get.argtypes = [c_void_p, c_int]
|
|
163
|
+
adl.ADL_Adapter_AdapterInfo_Get.restype = c_int
|
|
164
|
+
adl.ADL_Display_DisplayInfo_Get.argtypes = [c_int, POINTER(c_int), POINTER(c_void_p), c_int]
|
|
165
|
+
adl.ADL_Display_DisplayInfo_Get.restype = c_int
|
|
166
|
+
adl.ADL_Display_NativeAUXChannel_Access.argtypes = [c_int, c_int, POINTER(_NativeAuxRequest)]
|
|
167
|
+
adl.ADL_Display_NativeAUXChannel_Access.restype = c_int
|
|
168
|
+
adl.ADL_Display_DDCBlockAccess_Get.argtypes = [
|
|
169
|
+
c_int, c_int, c_int, c_int, c_int, c_void_p, POINTER(c_int), c_void_p
|
|
170
|
+
]
|
|
171
|
+
adl.ADL_Display_DDCBlockAccess_Get.restype = c_int
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _check(rc: int, operation: str) -> None:
|
|
175
|
+
if rc != ADL_OK:
|
|
176
|
+
raise AuxError(f"{operation} failed: ADL error {rc}")
|
|
177
|
+
|
|
178
|
+
def _ensure_open(self) -> None:
|
|
179
|
+
if self._closed:
|
|
180
|
+
raise AuxError("AmdAux is closed")
|
|
181
|
+
|
|
182
|
+
def close(self) -> None:
|
|
183
|
+
with self._lock:
|
|
184
|
+
if not self._closed:
|
|
185
|
+
self._adl.ADL_Main_Control_Destroy()
|
|
186
|
+
self._closed = True
|
|
187
|
+
|
|
188
|
+
def __enter__(self) -> "AmdAux":
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
|
192
|
+
self.close()
|
|
193
|
+
|
|
194
|
+
def adapters(self) -> list[Adapter]:
|
|
195
|
+
with self._lock:
|
|
196
|
+
self._ensure_open()
|
|
197
|
+
count = c_int()
|
|
198
|
+
self._check(self._adl.ADL_Adapter_NumberOfAdapters_Get(byref(count)), "adapter enumeration")
|
|
199
|
+
raw = (_ADLAdapterInfo * count.value)()
|
|
200
|
+
for item in raw:
|
|
201
|
+
item.iSize = ctypes.sizeof(_ADLAdapterInfo)
|
|
202
|
+
self._check(self._adl.ADL_Adapter_AdapterInfo_Get(raw, ctypes.sizeof(raw)), "adapter information")
|
|
203
|
+
adapters = [
|
|
204
|
+
Adapter(
|
|
205
|
+
index=item.iAdapterIndex,
|
|
206
|
+
name=_text(item.strAdapterName),
|
|
207
|
+
display_name=_text(item.strDisplayName),
|
|
208
|
+
bus=item.iBusNumber,
|
|
209
|
+
device=item.iDeviceNumber,
|
|
210
|
+
function=item.iFunctionNumber,
|
|
211
|
+
)
|
|
212
|
+
for item in raw
|
|
213
|
+
if item.iPresent == 1 and item.iVendorID in AMD_VENDOR_IDS
|
|
214
|
+
]
|
|
215
|
+
# ADL exposes one logical adapter per Windows DISPLAYx even when all
|
|
216
|
+
# entries refer to the same physical GPU. AUX ports belong to the
|
|
217
|
+
# physical PCI function, so retain one representative per BDF.
|
|
218
|
+
unique = {}
|
|
219
|
+
for adapter in adapters:
|
|
220
|
+
unique.setdefault((adapter.bus, adapter.device, adapter.function), adapter)
|
|
221
|
+
return list(unique.values())
|
|
222
|
+
|
|
223
|
+
def ports(self, adapter: Adapter, connected_only: bool = True) -> list[Port]:
|
|
224
|
+
with self._lock:
|
|
225
|
+
self._ensure_open()
|
|
226
|
+
count = c_int()
|
|
227
|
+
pointer = c_void_p()
|
|
228
|
+
self._check(
|
|
229
|
+
self._adl.ADL_Display_DisplayInfo_Get(adapter.index, byref(count), byref(pointer), 1),
|
|
230
|
+
"display enumeration",
|
|
231
|
+
)
|
|
232
|
+
try:
|
|
233
|
+
if not pointer.value or count.value <= 0:
|
|
234
|
+
return []
|
|
235
|
+
raw = ctypes.cast(pointer, POINTER(_ADLDisplayInfo))
|
|
236
|
+
result = []
|
|
237
|
+
for position in range(count.value):
|
|
238
|
+
item = raw[position]
|
|
239
|
+
connected = bool(item.iDisplayInfoValue & ADL_DISPLAY_CONNECTED)
|
|
240
|
+
if item.iDisplayConnector not in AUX_CONNECTORS:
|
|
241
|
+
continue
|
|
242
|
+
if connected_only and not connected:
|
|
243
|
+
continue
|
|
244
|
+
result.append(
|
|
245
|
+
Port(
|
|
246
|
+
adapter=adapter,
|
|
247
|
+
index=position,
|
|
248
|
+
logical_display_index=item.displayID.iDisplayLogicalIndex,
|
|
249
|
+
physical_display_index=item.displayID.iDisplayPhysicalIndex,
|
|
250
|
+
name=_text(item.strDisplayName),
|
|
251
|
+
manufacturer=_text(item.strDisplayManufacturerName),
|
|
252
|
+
display_type=item.iDisplayType,
|
|
253
|
+
output_type=item.iDisplayOutputType,
|
|
254
|
+
connector=item.iDisplayConnector,
|
|
255
|
+
connected=connected,
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
return result
|
|
259
|
+
finally:
|
|
260
|
+
if pointer.value:
|
|
261
|
+
self._crt.free(pointer)
|
|
262
|
+
|
|
263
|
+
@staticmethod
|
|
264
|
+
def _validate_port(port: Port) -> None:
|
|
265
|
+
if not port.connected:
|
|
266
|
+
raise ValueError("port is not connected")
|
|
267
|
+
|
|
268
|
+
def _native_aux(self, port: Port, operation: int, address: int, data: bytes) -> bytes:
|
|
269
|
+
self._validate_port(port)
|
|
270
|
+
if not 0 <= address <= 0xFFFFF:
|
|
271
|
+
raise ValueError("DPCD address must be in range 0x00000..0xFFFFF")
|
|
272
|
+
request = _NativeAuxRequest()
|
|
273
|
+
request.size = ctypes.sizeof(request)
|
|
274
|
+
request.operation = operation
|
|
275
|
+
request.address = address
|
|
276
|
+
request.data_size = len(data)
|
|
277
|
+
if operation == 1:
|
|
278
|
+
request.data[: len(data)] = data
|
|
279
|
+
rc = -1
|
|
280
|
+
for _ in range(RETRY_COUNT):
|
|
281
|
+
rc = self._adl.ADL_Display_NativeAUXChannel_Access(
|
|
282
|
+
port.adapter.index, port.logical_display_index, byref(request)
|
|
283
|
+
)
|
|
284
|
+
if rc == ADL_OK:
|
|
285
|
+
break
|
|
286
|
+
time.sleep(0.01)
|
|
287
|
+
self._check(rc, "DPCD read" if operation == 0 else "DPCD write")
|
|
288
|
+
if operation == 0 and request.data_size < len(data):
|
|
289
|
+
raise AuxError(f"DPCD read returned {request.data_size} of {len(data)} bytes")
|
|
290
|
+
return bytes(request.data[: len(data)])
|
|
291
|
+
|
|
292
|
+
def read_dpcd(self, port: Port, address: int, length: int) -> bytes:
|
|
293
|
+
if length <= 0:
|
|
294
|
+
raise ValueError("length must be positive")
|
|
295
|
+
with self._lock:
|
|
296
|
+
self._ensure_open()
|
|
297
|
+
output = bytearray()
|
|
298
|
+
while len(output) < length:
|
|
299
|
+
size = min(AUX_CHUNK_SIZE, length - len(output))
|
|
300
|
+
output += self._native_aux(port, 0, address + len(output), bytes(size))
|
|
301
|
+
return bytes(output)
|
|
302
|
+
|
|
303
|
+
def write_dpcd(self, port: Port, address: int, data: bytes) -> None:
|
|
304
|
+
data = bytes(data)
|
|
305
|
+
if not data:
|
|
306
|
+
raise ValueError("data must not be empty")
|
|
307
|
+
with self._lock:
|
|
308
|
+
self._ensure_open()
|
|
309
|
+
for offset in range(0, len(data), AUX_CHUNK_SIZE):
|
|
310
|
+
self._native_aux(port, 1, address + offset, data[offset : offset + AUX_CHUNK_SIZE])
|
|
311
|
+
|
|
312
|
+
@staticmethod
|
|
313
|
+
def _dev7(address: int) -> int:
|
|
314
|
+
if not 0 <= address <= 0xFF:
|
|
315
|
+
raise ValueError("I2C address must fit in one byte")
|
|
316
|
+
return address >> 1 if address >= 0x80 else address & 0x7F
|
|
317
|
+
|
|
318
|
+
def _ddc(self, port: Port, send: bytes, receive_length: int) -> bytes:
|
|
319
|
+
self._validate_port(port)
|
|
320
|
+
send_buffer = (c_uint8 * len(send)).from_buffer_copy(send)
|
|
321
|
+
receive_buffer = (c_uint8 * receive_length)() if receive_length else None
|
|
322
|
+
receive_size = c_int(receive_length)
|
|
323
|
+
rc = -1
|
|
324
|
+
for _ in range(RETRY_COUNT):
|
|
325
|
+
receive_size.value = receive_length
|
|
326
|
+
rc = self._adl.ADL_Display_DDCBlockAccess_Get(
|
|
327
|
+
port.adapter.index,
|
|
328
|
+
port.logical_display_index,
|
|
329
|
+
0,
|
|
330
|
+
0,
|
|
331
|
+
len(send),
|
|
332
|
+
send_buffer,
|
|
333
|
+
byref(receive_size),
|
|
334
|
+
receive_buffer,
|
|
335
|
+
)
|
|
336
|
+
if rc == ADL_OK:
|
|
337
|
+
break
|
|
338
|
+
time.sleep(0.01)
|
|
339
|
+
self._check(rc, "I2C-over-AUX transaction")
|
|
340
|
+
if receive_size.value < receive_length:
|
|
341
|
+
raise AuxError(f"I2C-over-AUX returned {receive_size.value} of {receive_length} bytes")
|
|
342
|
+
return bytes(receive_buffer) if receive_buffer is not None else b""
|
|
343
|
+
|
|
344
|
+
def i2c_read(self, port: Port, device: int, register: int, length: int) -> bytes:
|
|
345
|
+
if length <= 0 or not 0 <= register <= 0xFF:
|
|
346
|
+
raise ValueError("length must be positive and register must fit in one byte")
|
|
347
|
+
with self._lock:
|
|
348
|
+
self._ensure_open()
|
|
349
|
+
output = bytearray()
|
|
350
|
+
read_address = (self._dev7(device) << 1) | 1
|
|
351
|
+
while len(output) < length:
|
|
352
|
+
size = min(AUX_CHUNK_SIZE, length - len(output))
|
|
353
|
+
output += self._ddc(port, bytes((read_address, (register + len(output)) & 0xFF)), size)
|
|
354
|
+
return bytes(output)
|
|
355
|
+
|
|
356
|
+
def i2c_write(self, port: Port, device: int, register: int, data: bytes) -> None:
|
|
357
|
+
data = bytes(data)
|
|
358
|
+
if not data or not 0 <= register <= 0xFF:
|
|
359
|
+
raise ValueError("data must not be empty and register must fit in one byte")
|
|
360
|
+
with self._lock:
|
|
361
|
+
self._ensure_open()
|
|
362
|
+
write_address = self._dev7(device) << 1
|
|
363
|
+
if self._dev7(device) == 0x30:
|
|
364
|
+
if len(data) != 1:
|
|
365
|
+
raise ValueError("EDID segment-pointer writes require exactly one byte")
|
|
366
|
+
self._ddc(port, bytes((write_address, data[0])), 0)
|
|
367
|
+
return
|
|
368
|
+
for offset in range(0, len(data), AUX_CHUNK_SIZE):
|
|
369
|
+
chunk = data[offset : offset + AUX_CHUNK_SIZE]
|
|
370
|
+
self._ddc(port, bytes((write_address, (register + offset) & 0xFF)) + chunk, 0)
|
gpu_aux/api.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Object-oriented public API for selecting and using one AUX port."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from .adl import Adapter, AmdAux, AuxError, Port
|
|
9
|
+
from .intel import IntelAux
|
|
10
|
+
from .nvapi import NvidiaAux
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_context_lock = threading.RLock()
|
|
14
|
+
_contexts = {}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _normalize_backend(backend: str) -> str:
|
|
18
|
+
if not isinstance(backend, str):
|
|
19
|
+
raise TypeError("backend must be 'AMD', 'NVIDIA', or 'INTEL'")
|
|
20
|
+
normalized = backend.strip().upper()
|
|
21
|
+
if normalized in {"AMD", "NVIDIA", "INTEL"}:
|
|
22
|
+
return normalized
|
|
23
|
+
raise ValueError("backend must be 'AMD', 'NVIDIA', or 'INTEL'")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _create_aux(backend: str):
|
|
27
|
+
if backend == "AMD":
|
|
28
|
+
return AmdAux()
|
|
29
|
+
if backend == "NVIDIA":
|
|
30
|
+
return NvidiaAux()
|
|
31
|
+
if backend == "INTEL":
|
|
32
|
+
return IntelAux()
|
|
33
|
+
raise ValueError("backend must be 'AMD', 'NVIDIA', or 'INTEL'")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _acquire_aux(backend: str):
|
|
37
|
+
normalized_backend = _normalize_backend(backend)
|
|
38
|
+
with _context_lock:
|
|
39
|
+
context = _contexts.get(normalized_backend)
|
|
40
|
+
if context is None:
|
|
41
|
+
context = [_create_aux(normalized_backend), 0]
|
|
42
|
+
_contexts[normalized_backend] = context
|
|
43
|
+
context[1] += 1
|
|
44
|
+
return context[0]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _release_aux(backend: str, aux) -> None:
|
|
48
|
+
normalized_backend = _normalize_backend(backend)
|
|
49
|
+
with _context_lock:
|
|
50
|
+
context = _contexts.get(normalized_backend)
|
|
51
|
+
if context is None or aux is not context[0] or context[1] <= 0:
|
|
52
|
+
return
|
|
53
|
+
context[1] -= 1
|
|
54
|
+
if context[1] == 0:
|
|
55
|
+
aux.close()
|
|
56
|
+
del _contexts[normalized_backend]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class GpuPorts:
|
|
61
|
+
"""A physical GPU and a snapshot of its connected DP/eDP ports."""
|
|
62
|
+
|
|
63
|
+
gpu_index: int
|
|
64
|
+
adapter: Adapter
|
|
65
|
+
ports: tuple[Port, ...]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def enumerate_gpus(backend: str) -> list[Adapter]:
|
|
69
|
+
"""Return physical GPUs for one backend without creating an ``AuxPort``."""
|
|
70
|
+
|
|
71
|
+
normalized_backend = _normalize_backend(backend)
|
|
72
|
+
aux = _acquire_aux(normalized_backend)
|
|
73
|
+
try:
|
|
74
|
+
return aux.adapters()
|
|
75
|
+
finally:
|
|
76
|
+
_release_aux(normalized_backend, aux)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def enumerate_ports(backend: str, gpu_index: int = 0) -> list[Port]:
|
|
80
|
+
"""Return connected DP/eDP ports on one GPU without creating an ``AuxPort``."""
|
|
81
|
+
|
|
82
|
+
if gpu_index < 0:
|
|
83
|
+
raise ValueError("gpu_index must not be negative")
|
|
84
|
+
normalized_backend = _normalize_backend(backend)
|
|
85
|
+
aux = _acquire_aux(normalized_backend)
|
|
86
|
+
try:
|
|
87
|
+
adapters = aux.adapters()
|
|
88
|
+
if gpu_index >= len(adapters):
|
|
89
|
+
raise AuxError(
|
|
90
|
+
f"GPU index {gpu_index} is unavailable for {normalized_backend}; "
|
|
91
|
+
f"found {len(adapters)} GPU(s)"
|
|
92
|
+
)
|
|
93
|
+
return aux.ports(adapters[gpu_index])
|
|
94
|
+
finally:
|
|
95
|
+
_release_aux(normalized_backend, aux)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def enumerate_gpus_and_ports(backend: str) -> list[GpuPorts]:
|
|
99
|
+
"""Return a combined GPU/port snapshot without creating an ``AuxPort``."""
|
|
100
|
+
|
|
101
|
+
normalized_backend = _normalize_backend(backend)
|
|
102
|
+
aux = _acquire_aux(normalized_backend)
|
|
103
|
+
try:
|
|
104
|
+
result = []
|
|
105
|
+
for gpu_index, adapter in enumerate(aux.adapters()):
|
|
106
|
+
result.append(
|
|
107
|
+
GpuPorts(
|
|
108
|
+
gpu_index=gpu_index,
|
|
109
|
+
adapter=adapter,
|
|
110
|
+
ports=tuple(aux.ports(adapter)),
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
return result
|
|
114
|
+
finally:
|
|
115
|
+
_release_aux(normalized_backend, aux)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class AuxPort:
|
|
119
|
+
"""An opened DP/eDP AUX endpoint.
|
|
120
|
+
|
|
121
|
+
``index`` is counted within ports of the requested kind on one physical
|
|
122
|
+
GPU. For example, ``AuxPort("DP", index=1, backend="NVIDIA")`` selects the
|
|
123
|
+
second external DP port on the first NVIDIA GPU.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
def __init__(self, kind: str, index: int = 0, gpu_index: int = 0, *, backend: str) -> None:
|
|
127
|
+
self._aux = None
|
|
128
|
+
self._port = None
|
|
129
|
+
|
|
130
|
+
normalized_backend = _normalize_backend(backend)
|
|
131
|
+
normalized_kind = self._normalize_kind(kind)
|
|
132
|
+
if index < 0 or gpu_index < 0:
|
|
133
|
+
raise ValueError("index and gpu_index must not be negative")
|
|
134
|
+
|
|
135
|
+
aux = _acquire_aux(normalized_backend)
|
|
136
|
+
try:
|
|
137
|
+
adapters = aux.adapters()
|
|
138
|
+
if gpu_index >= len(adapters):
|
|
139
|
+
raise AuxError(
|
|
140
|
+
f"GPU index {gpu_index} is unavailable for {normalized_backend}; "
|
|
141
|
+
f"found {len(adapters)} GPU(s)"
|
|
142
|
+
)
|
|
143
|
+
adapter = adapters[gpu_index]
|
|
144
|
+
matching_ports = [
|
|
145
|
+
port for port in aux.ports(adapter) if port.kind == normalized_kind
|
|
146
|
+
]
|
|
147
|
+
if index >= len(matching_ports):
|
|
148
|
+
raise AuxError(
|
|
149
|
+
f"{normalized_kind} port index {index} is unavailable on GPU {gpu_index}; "
|
|
150
|
+
f"found {len(matching_ports)} matching port(s)"
|
|
151
|
+
)
|
|
152
|
+
self._aux = aux
|
|
153
|
+
self._port = matching_ports[index]
|
|
154
|
+
self._backend = normalized_backend
|
|
155
|
+
self._gpu_index = gpu_index
|
|
156
|
+
self._kind_index = index
|
|
157
|
+
except Exception:
|
|
158
|
+
_release_aux(normalized_backend, aux)
|
|
159
|
+
raise
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def _normalize_kind(kind: str) -> str:
|
|
163
|
+
if not isinstance(kind, str):
|
|
164
|
+
raise TypeError("kind must be 'eDP' or 'DP'")
|
|
165
|
+
normalized = kind.strip().lower()
|
|
166
|
+
if normalized == "edp":
|
|
167
|
+
return "eDP"
|
|
168
|
+
if normalized == "dp":
|
|
169
|
+
return "DP"
|
|
170
|
+
raise ValueError("kind must be 'eDP' or 'DP'")
|
|
171
|
+
|
|
172
|
+
def _require_open(self) -> tuple[AmdAux, Port]:
|
|
173
|
+
if self._aux is None or self._port is None:
|
|
174
|
+
raise AuxError("AuxPort is closed")
|
|
175
|
+
return self._aux, self._port
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def info(self) -> Port:
|
|
179
|
+
"""Return the immutable port information."""
|
|
180
|
+
|
|
181
|
+
return self._require_open()[1]
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def identity(self) -> str:
|
|
185
|
+
return self.info.identity
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def kind(self) -> str:
|
|
189
|
+
return self.info.kind
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def gpu_index(self) -> int:
|
|
193
|
+
self._require_open()
|
|
194
|
+
return self._gpu_index
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def backend(self) -> str:
|
|
198
|
+
self._require_open()
|
|
199
|
+
return self._backend
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def index(self) -> int:
|
|
203
|
+
self._require_open()
|
|
204
|
+
return self._kind_index
|
|
205
|
+
|
|
206
|
+
def read_dpcd(self, address: int, length: int) -> bytes:
|
|
207
|
+
aux, port = self._require_open()
|
|
208
|
+
return aux.read_dpcd(port, address, length)
|
|
209
|
+
|
|
210
|
+
def write_dpcd(self, address: int, data: bytes) -> None:
|
|
211
|
+
aux, port = self._require_open()
|
|
212
|
+
aux.write_dpcd(port, address, data)
|
|
213
|
+
|
|
214
|
+
def i2c_read(self, device: int, register: int, length: int) -> bytes:
|
|
215
|
+
aux, port = self._require_open()
|
|
216
|
+
return aux.i2c_read(port, device, register, length)
|
|
217
|
+
|
|
218
|
+
def i2c_write(self, device: int, register: int, data: bytes) -> None:
|
|
219
|
+
aux, port = self._require_open()
|
|
220
|
+
aux.i2c_write(port, device, register, data)
|
|
221
|
+
|
|
222
|
+
def close(self) -> None:
|
|
223
|
+
if self._aux is not None:
|
|
224
|
+
_release_aux(self._backend, self._aux)
|
|
225
|
+
self._aux = None
|
|
226
|
+
self._port = None
|
|
227
|
+
|
|
228
|
+
def __enter__(self) -> "AuxPort":
|
|
229
|
+
self._require_open()
|
|
230
|
+
return self
|
|
231
|
+
|
|
232
|
+
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
|
233
|
+
self.close()
|
|
234
|
+
|
|
235
|
+
def __del__(self) -> None:
|
|
236
|
+
try:
|
|
237
|
+
self.close()
|
|
238
|
+
except Exception:
|
|
239
|
+
# Interpreter shutdown may have already released module globals.
|
|
240
|
+
pass
|