fix(platform): an unreadable GPU classifies as unknown, so requires.gpu fails open as documented

When every DRM card's vendor file or every Windows display adapter's
MatchingDeviceId was unreadable, classify_gpu_vendors returned 'none' and
availability refused with unsupported_gpu, contradicting the documented
'unreadable GPU passes' rule. Windows now records an unreadable numbered
adapter as an empty id so the classifier sees it.

Also drops the type-narrowing assert in availability() and makes
_action's gpu_label required.
This commit is contained in:
teknium1
2026-10-03 10:11:41 -07:00
committed by Teknium
parent f1c79255fe
commit b412ee9206
4 changed files with 11 additions and 5 deletions

View File

@@ -233,7 +233,12 @@ _GPU_SCAN_LIMIT = 16
def classify_gpu_vendors(vendor_ids: list[str]) -> str:
"""Return the highest-priority GPU class for PCI vendor ids, or ``none``."""
"""Return the highest-priority GPU class for PCI vendor ids, or ``none``.
``unknown`` when devices were seen but none of their vendor ids could be read.
"""
if vendor_ids and not any(v.strip() for v in vendor_ids):
return "unknown"
found = {_GPU_PCI_VENDORS.get(v.strip().lower().removeprefix("0x")) for v in vendor_ids}
return next((name for name in _GPU_PRIORITY if name in found), "none")
@@ -263,6 +268,8 @@ def _windows_gpu_vendor_ids() -> list[str] | None:
device_id = _winreg_str(f"{_DISPLAY_CLASS_KEY}\\{subkey}", "MatchingDeviceId").upper()
if "VEN_" in device_id:
vendors.append(device_id.split("VEN_", 1)[1][:4])
elif subkey.isdigit(): # an adapter whose id could not be read
vendors.append("")
except OSError:
return None
return vendors

View File

@@ -89,9 +89,8 @@ def availability(entry: _HasRequirements, *, os_family: str | None = None) -> Av
# the machines that have the GPU, and the MCP connection check still applies.
if gpu is not None and facts.gpu_class() not in (gpu, "unknown"):
return Availability("unsupported_gpu", min_version=entry.min_version)
if not entry.requires_app:
if definition is None:
return Availability("no_requirements")
assert definition is not None
resolver = AppResolver(definition)
res = resolver.locate()
looked_at = res.command[0] if res.command else (res.candidates[0].value if res.candidates else None)

View File

@@ -220,7 +220,7 @@ def test_meminfo_total_is_parsed_in_bytes():
@pytest.mark.parametrize(("vendors", "gpu"), [
(["0x8086", "0x10de"], "nvidia"), (["0x8086", "0x1002"], "amd"), (["0x8086"], "intel"),
(["0x1af4"], "none"), ([], "none"), (["10DE"], "nvidia"),
(["0x1af4"], "none"), ([], "none"), (["10DE"], "nvidia"), ([""], "unknown"), (["", "0x10de"], "nvidia"),
])
def test_gpu_vendor_priority_prefers_the_discrete_card(vendors, gpu):
assert facts.classify_gpu_vendors(vendors) == gpu

View File

@@ -108,7 +108,7 @@ def liveness_for(server_name: str) -> Liveness:
return Liveness("static")
def _action(state: LivenessState, app_name: str, gpu_label: str = "") -> tuple[str, Retry]:
def _action(state: LivenessState, app_name: str, gpu_label: str) -> tuple[str, Retry]:
actions: dict[LivenessState, tuple[str, Retry]] = {
"app_not_running": (f"Start {app_name}, then try again.", "after_user_action"),
"hermes_not_connected": (f"Reconnect {app_name} in Hermes, then try again.", "after_user_action"),