Files
Faset_Engine/tools/ci/probe_windows_vulkan.py
T

128 lines
6.4 KiB
Python

#!/usr/bin/env python3
"""Verify the prepared Windows Vulkan DLL/ICD before spending time on engine builds.
Use VK_LOADER_DEBUG=all to retain the loader's own driver-discovery diagnostics.
This probe uses ctypes and the Vulkan ABI, and needs neither the engine nor Slang.
"""
from __future__ import annotations
import argparse
import ctypes as ct
import json
import os
from pathlib import Path
import sys
class Application(ct.Structure):
_fields_ = [("sType", ct.c_uint32), ("pNext", ct.c_void_p), ("pApplicationName", ct.c_char_p),
("applicationVersion", ct.c_uint32), ("pEngineName", ct.c_char_p),
("engineVersion", ct.c_uint32), ("apiVersion", ct.c_uint32)]
class InstanceCreate(ct.Structure):
_fields_ = [("sType", ct.c_uint32), ("pNext", ct.c_void_p), ("flags", ct.c_uint32),
("pApplicationInfo", ct.POINTER(Application)), ("enabledLayerCount", ct.c_uint32),
("ppEnabledLayerNames", ct.POINTER(ct.c_char_p)), ("enabledExtensionCount", ct.c_uint32),
("ppEnabledExtensionNames", ct.POINTER(ct.c_char_p))]
def version(value: int) -> str:
return f"{value >> 22}.{(value >> 12) & 1023}.{value & 4095}"
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--cache", type=Path, default=Path(".cache/windows-graphics"))
args = parser.parse_args()
if sys.platform != "win32":
parser.error("This probe verifies the native Windows DLL loader")
cache = args.cache.resolve()
manifest_path = cache / "driver/vk_swiftshader_icd.json"
loader_path = cache / "sdk/bin/vulkan-1.dll"
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
library = (manifest_path.parent / manifest["ICD"]["library_path"]).resolve()
report = {"format": "faset.windows-vulkan-probe", "version": 1, "status": "running",
"loader": str(loader_path), "driver_manifest": str(manifest_path),
"driver_library": str(library), "manifest": manifest,
"loader_debug": os.environ.get("VK_LOADER_DEBUG"), "pointer_bits": ct.sizeof(ct.c_void_p) * 8}
instance = ct.c_void_p()
loader = None
try:
if not loader_path.is_file() or not library.is_file():
raise RuntimeError("Prepared Vulkan loader or driver DLL is missing")
if not manifest["ICD"]["library_path"].startswith(".\\"):
raise RuntimeError("Windows ICD relative library path must use a native backslash")
os.environ["VK_DRIVER_FILES"] = str(manifest_path)
os.environ["VK_ICD_FILENAMES"] = str(manifest_path)
# Direct DLL loading separates Windows dependency/architecture errors from
# manifest discovery and Vulkan API compatibility errors in the next step.
driver = ct.WinDLL(str(library))
negotiate = driver.vk_icdNegotiateLoaderICDInterfaceVersion
negotiate.argtypes = [ct.POINTER(ct.c_uint32)]
negotiate.restype = ct.c_int32
interface_version = ct.c_uint32(7)
report["icd_negotiate_result"] = negotiate(ct.byref(interface_version))
report["icd_interface_version"] = interface_version.value
if report["icd_negotiate_result"] != 0:
raise RuntimeError("SwiftShader rejected the loader interface negotiation")
loader = ct.WinDLL(str(loader_path))
enumerate_version = loader.vkEnumerateInstanceVersion
enumerate_version.argtypes = [ct.POINTER(ct.c_uint32)]
enumerate_version.restype = ct.c_int32
api = ct.c_uint32()
report["enumerate_version_result"] = enumerate_version(ct.byref(api))
report["loader_api_version"] = version(api.value)
create = loader.vkCreateInstance
create.argtypes = [ct.POINTER(InstanceCreate), ct.c_void_p, ct.POINTER(ct.c_void_p)]
create.restype = ct.c_int32
app = Application(0, None, b"Faset Windows Vulkan probe", 1, b"Faset", 1, (1 << 22) | (3 << 12))
info = InstanceCreate(1, None, 0, ct.pointer(app), 0, None, 0, None)
result = create(ct.byref(info), None, ct.byref(instance))
report["create_instance_result"] = result
if result != 0:
raise RuntimeError(f"vkCreateInstance for Vulkan 1.3 failed: {result}")
enumerate_devices = loader.vkEnumeratePhysicalDevices
enumerate_devices.argtypes = [ct.c_void_p, ct.POINTER(ct.c_uint32), ct.POINTER(ct.c_void_p)]
enumerate_devices.restype = ct.c_int32
count = ct.c_uint32()
if enumerate_devices(instance, ct.byref(count), None) != 0 or count.value == 0:
raise RuntimeError("The loader found no usable Vulkan physical device")
devices = (ct.c_void_p * count.value)()
if enumerate_devices(instance, ct.byref(count), devices) != 0:
raise RuntimeError("Cannot enumerate Vulkan physical devices")
properties = loader.vkGetPhysicalDeviceProperties
properties.argtypes = [ct.c_void_p, ct.c_void_p]
properties.restype = None
report["devices"] = []
for device in devices:
# VkPhysicalDeviceProperties starts with five uint32 values and a
# 256-byte deviceName. Reserve ample room for the limits that follow.
storage = (ct.c_uint64 * 512)()
properties(device, ct.byref(storage))
raw = bytes(storage)
values = [int.from_bytes(raw[index:index + 4], "little") for index in range(0, 20, 4)]
name = raw[20:276].split(b"\0", 1)[0].decode("utf-8")
report["devices"].append({"name": name, "api_version": version(values[0]),
"driver_version": values[1], "vendor_id": values[2],
"device_id": values[3], "device_type": values[4]})
if not any("SwiftShader" in device["name"] for device in report["devices"]):
raise RuntimeError("Pinned SwiftShader was not among the enumerated devices")
report["status"] = "passed"
except Exception as error:
report.update({"status": "failed", "error": str(error)})
raise
finally:
if loader is not None and instance.value:
destroy = loader.vkDestroyInstance
destroy.argtypes = [ct.c_void_p, ct.c_void_p]
destroy.restype = None
destroy(instance, None)
(cache / "probe.json").write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
print(json.dumps(report, indent=2), flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())