Coverage for src / lilbee / providers / llama_cpp / vulkan_icd_discovery.py: 100%
115 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-05-15 20:55 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-05-15 20:55 +0000
1"""Cross-platform discovery of installed Vulkan ICD manifests.
3Mirrors the Vulkan loader's own discovery so callers can identify which
4vendors are installed without calling ``vkCreateInstance`` (which would
5pre-load every vendor's ICD into the process before any disable env
6var can take effect). Windows reads the registry; Linux walks the XDG
7``vulkan/icd.d`` hierarchy; macOS yields nothing. See
8https://github.com/KhronosGroup/Vulkan-Loader/blob/main/docs/LoaderDriverInterface.md
9for the loader-side spec.
10"""
12from __future__ import annotations
14import logging
15import os
16import sys
17from pathlib import Path
18from typing import TYPE_CHECKING, Any
20if TYPE_CHECKING:
21 from collections.abc import Iterator
23log = logging.getLogger(__name__)
26def iter_vulkan_manifest_paths() -> Iterator[str]:
27 """Yield absolute ``.json`` manifest paths the Vulkan loader would discover.
29 Returns an empty iterator on macOS (Metal-only build, no Vulkan loader).
30 """
31 if sys.platform == "win32":
32 yield from _iter_windows_vulkan_manifest_paths()
33 elif sys.platform.startswith("linux"):
34 yield from _iter_linux_vulkan_manifest_paths()
35 else:
36 yield from ()
39# Microsoft-defined PnP device-setup class GUIDs that host Vulkan ICD manifests.
40# Both GUIDs and the Khronos software-driver key are documented in
41# https://github.com/KhronosGroup/Vulkan-Loader/blob/main/docs/LoaderDriverInterface.md#driver-discovery-on-windows
42# (the GUIDs themselves are the public Windows
43# https://learn.microsoft.com/en-us/windows-hardware/drivers/install/system-defined-device-setup-classes-available-to-vendors).
44_PNP_DISPLAY_ADAPTER_CLASS_GUID = "{4d36e968-e325-11ce-bfc1-08002be10318}"
45_PNP_SOFTWARE_COMPONENT_CLASS_GUID = "{5c4c3332-344d-483c-8739-259e934c9cc8}"
47# Legacy software-driver paths (HKLM + WOW6432Node mirror for 32-bit ICDs).
48# Each value name is a manifest path; the DWORD value is 0=enabled.
49_KHRONOS_DRIVERS_KEYS = (
50 r"SOFTWARE\Khronos\Vulkan\Drivers",
51 r"SOFTWARE\WOW6432Node\Khronos\Vulkan\Drivers",
52)
54_PNP_VULKAN_VALUE_NAMES = ("VulkanDriverName", "VulkanDriverNameWow")
57def _iter_windows_vulkan_manifest_paths() -> Iterator[str]:
58 """Yield manifest paths from the four Windows ICD-discovery locations."""
59 try:
60 import winreg
61 except ImportError: # pragma: no cover - winreg ships with CPython on Windows
62 return
63 yield from _iter_khronos_software_manifests(winreg)
64 yield from _iter_pnp_class_manifests(winreg, _PNP_DISPLAY_ADAPTER_CLASS_GUID)
65 yield from _iter_pnp_class_manifests(winreg, _PNP_SOFTWARE_COMPONENT_CLASS_GUID)
68def _iter_khronos_software_manifests(winreg: Any) -> Iterator[str]:
69 """Yield enabled-flag (DWORD=0) manifest paths from the Khronos software keys."""
70 hklm = winreg.HKEY_LOCAL_MACHINE
71 for sub_key in _KHRONOS_DRIVERS_KEYS:
72 try:
73 key = winreg.OpenKey(hklm, sub_key)
74 except OSError:
75 continue
76 try:
77 i = 0
78 while True:
79 try:
80 name, value, _value_type = winreg.EnumValue(key, i)
81 except OSError:
82 break
83 i += 1
84 if value == 0 and name:
85 yield name
86 finally:
87 winreg.CloseKey(key)
90def _iter_pnp_class_manifests(winreg: Any, class_guid: str) -> Iterator[str]:
91 """Yield manifest paths from PnP keys under one device-class GUID.
93 Walks ``HKLM\\SYSTEM\\CurrentControlSet\\Control\\Class\\{GUID}\\NNNN``,
94 reading each subkey's ``VulkanDriverName`` and ``VulkanDriverNameWow``
95 values. Both ``REG_SZ`` (single path) and ``REG_MULTI_SZ`` (path list)
96 are honoured because the loader spec allows both.
97 """
98 hklm = winreg.HKEY_LOCAL_MACHINE
99 class_root_path = rf"SYSTEM\CurrentControlSet\Control\Class\{class_guid}"
100 try:
101 class_root = winreg.OpenKey(hklm, class_root_path)
102 except OSError:
103 return
104 try:
105 i = 0
106 while True:
107 try:
108 subkey_name = winreg.EnumKey(class_root, i)
109 except OSError:
110 break
111 i += 1
112 try:
113 subkey = winreg.OpenKey(class_root, subkey_name)
114 except OSError:
115 continue
116 try:
117 yield from _read_vulkan_driver_name_values(winreg, subkey)
118 finally:
119 winreg.CloseKey(subkey)
120 finally:
121 winreg.CloseKey(class_root)
124def _read_vulkan_driver_name_values(winreg: Any, subkey: Any) -> Iterator[str]:
125 """Yield non-empty paths from one PnP subkey's ``VulkanDriverName*`` values.
127 Handles both REG_SZ (single string) and REG_MULTI_SZ (list of strings)
128 that the loader spec allows.
129 """
130 for value_name in _PNP_VULKAN_VALUE_NAMES:
131 try:
132 value, _value_type = winreg.QueryValueEx(subkey, value_name)
133 except OSError:
134 continue
135 if isinstance(value, str):
136 if value:
137 yield value
138 elif isinstance(value, list):
139 for entry in value:
140 if isinstance(entry, str) and entry:
141 yield entry
144# Linux ICD-discovery search-path config. Defaults follow the XDG basedir
145# spec (https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html);
146# SYSCONFDIR / EXTRASYSCONFDIR are loader build-time constants that expand
147# to /usr/local/etc and /etc on the distros lilbee ships against. The
148# Flatpak export trees aren't in the Khronos spec but the loader picks them
149# up via XDG_DATA_DIRS inside a Flatpak runtime; we walk them defensively
150# in case lilbee runs outside the sandbox.
151_VULKAN_ICD_SUBPATH = "vulkan/icd.d"
152_LINUX_FIXED_ETC_ICD_DIRS: tuple[str, ...] = (
153 "/usr/local/etc/vulkan/icd.d",
154 "/etc/vulkan/icd.d",
155)
156_LINUX_FLATPAK_ICD_DIRS: tuple[str, ...] = (
157 "~/.local/share/flatpak/exports/share/vulkan/icd.d",
158 "/var/lib/flatpak/exports/share/vulkan/icd.d",
159)
162def _iter_linux_vulkan_manifest_paths() -> Iterator[str]:
163 """Glob ``*.json`` across the Linux ICD-discovery directories, deduping."""
164 seen_dirs: set[Path] = set()
165 seen_files: set[Path] = set()
166 for directory in _linux_vulkan_icd_directories():
167 try:
168 resolved = directory.expanduser()
169 except RuntimeError:
170 # PosixPath.expanduser raises when HOME is unset; skip.
171 continue
172 if resolved in seen_dirs:
173 continue
174 seen_dirs.add(resolved)
175 if not resolved.is_dir():
176 continue
177 try:
178 entries = sorted(resolved.glob("*.json"))
179 except OSError:
180 log.debug("Vulkan ICD dir %s could not be read", resolved, exc_info=True)
181 continue
182 for entry in entries:
183 if entry in seen_files or not entry.is_file():
184 continue
185 seen_files.add(entry)
186 yield str(entry)
189def _linux_vulkan_icd_directories() -> Iterator[Path]:
190 """Yield each Linux ICD search directory in loader-spec order."""
191 yield from _xdg_dirs("XDG_CONFIG_HOME", "~/.config", _VULKAN_ICD_SUBPATH)
192 yield from _xdg_dirs("XDG_CONFIG_DIRS", "/etc/xdg", _VULKAN_ICD_SUBPATH)
193 for fixed in _LINUX_FIXED_ETC_ICD_DIRS:
194 yield Path(fixed)
195 yield from _xdg_dirs("XDG_DATA_HOME", "~/.local/share", _VULKAN_ICD_SUBPATH)
196 yield from _xdg_dirs("XDG_DATA_DIRS", "/usr/local/share:/usr/share", _VULKAN_ICD_SUBPATH)
197 for flatpak in _LINUX_FLATPAK_ICD_DIRS:
198 yield Path(flatpak)
201def _xdg_dirs(env_var: str, default: str, subpath: str) -> Iterator[Path]:
202 """Split *env_var* (or *default*) on ``:``, append *subpath* to each.
204 Empty components are dropped (the "extra slash in XDG_DATA_DIRS" loader
205 quirk, Vulkan-Loader#2331) and appends *subpath* to each remaining
206 entry. Falls back to *default* when the env var is unset.
207 """
208 raw = os.environ.get(env_var) or default
209 for component in raw.split(":"):
210 stripped = component.strip()
211 if not stripped:
212 continue
213 yield Path(stripped) / subpath