Fix Windows Vulkan driver discovery and add fast graphics-data checks
This commit is contained in:
@@ -19,6 +19,32 @@ jobs:
|
||||
- name: Windows compiler environment
|
||||
if: runner.os == 'Windows'
|
||||
uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
|
||||
- name: Identify pinned Windows Vulkan SDK cache
|
||||
if: runner.os == 'Windows'
|
||||
id: vulkan-identity
|
||||
run: python tools/ci/prepare_windows_vulkan.py --cache-key
|
||||
- name: Restore Windows Vulkan SDK
|
||||
if: runner.os == 'Windows'
|
||||
id: vulkan-sdk-cache
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830
|
||||
with:
|
||||
key: ${{ steps.vulkan-identity.outputs.key }}-loader-only
|
||||
restore-keys: |
|
||||
${{ steps.vulkan-identity.outputs.key }}
|
||||
${{ steps.vulkan-identity.outputs.legacy }}
|
||||
path: .cache/windows-graphics/sdk
|
||||
- name: Build Windows Vulkan headers and loader without a GPU driver
|
||||
if: runner.os == 'Windows'
|
||||
run: python tools/ci/prepare_windows_vulkan.py --loader-only
|
||||
- name: Save successfully built Windows Vulkan SDK
|
||||
if: runner.os == 'Windows' && steps.vulkan-sdk-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830
|
||||
with:
|
||||
key: ${{ steps.vulkan-sdk-cache.outputs.cache-primary-key }}
|
||||
path: .cache/windows-graphics/sdk
|
||||
- name: Fetch Windows Slang compiler
|
||||
if: runner.os == 'Windows'
|
||||
run: python tools/fetch_slang.py
|
||||
- name: Linux build tools
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get update && sudo apt-get install -y clang ninja-build
|
||||
@@ -27,7 +53,7 @@ jobs:
|
||||
run: cmake --preset linux-debug -DFASET_BUILD_RENDERER=OFF -DFASET_BUILD_EDITOR=OFF
|
||||
- name: Configure Windows
|
||||
if: runner.os == 'Windows'
|
||||
run: cmake --preset windows-debug -DFASET_BUILD_RENDERER=OFF -DFASET_BUILD_EDITOR=OFF
|
||||
run: cmake --preset windows-debug -DFASET_BUILD_RENDERER=ON -DFASET_BUILD_EDITOR=OFF
|
||||
- name: Build Linux
|
||||
if: runner.os == 'Linux'
|
||||
run: cmake --build --preset linux-debug --parallel 2
|
||||
@@ -39,7 +65,17 @@ jobs:
|
||||
run: ctest --preset linux-debug
|
||||
- name: Test Windows
|
||||
if: runner.os == 'Windows'
|
||||
run: ctest --preset windows-debug
|
||||
run: ctest --preset windows-debug -LE gpu
|
||||
- name: Preserve Windows CPU test evidence
|
||||
if: always() && runner.os == 'Windows'
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: windows-cpu-evidence
|
||||
if-no-files-found: warn
|
||||
include-hidden-files: true
|
||||
path: |
|
||||
.cache/windows-graphics/toolchain.json
|
||||
build/windows-debug/Testing/Temporary/LastTest.log
|
||||
manual:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
|
||||
@@ -21,11 +21,15 @@ jobs:
|
||||
uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
|
||||
with:
|
||||
arch: x64
|
||||
- name: Identify pinned Vulkan source cache
|
||||
id: vulkan-identity
|
||||
run: python tools/ci/prepare_windows_vulkan.py --cache-key
|
||||
- name: Restore pinned Vulkan test tools
|
||||
id: vulkan-cache
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830
|
||||
with:
|
||||
key: windows-2025-vulkan-${{ hashFiles('tools/ci/prepare_windows_vulkan.py') }}
|
||||
key: ${{ steps.vulkan-identity.outputs.key }}
|
||||
restore-keys: ${{ steps.vulkan-identity.outputs.legacy }}
|
||||
path: |
|
||||
.cache/windows-graphics/sdk
|
||||
.cache/windows-graphics/driver
|
||||
@@ -39,6 +43,10 @@ jobs:
|
||||
path: |
|
||||
.cache/windows-graphics/sdk
|
||||
.cache/windows-graphics/driver
|
||||
- name: Probe Vulkan loader and SwiftShader before compiling the engine
|
||||
env:
|
||||
VK_LOADER_DEBUG: all
|
||||
run: python tools/ci/probe_windows_vulkan.py
|
||||
- name: Fetch checksum-verified Slang compiler
|
||||
run: python tools/fetch_slang.py
|
||||
- name: Configure full editor and Player
|
||||
@@ -46,6 +54,8 @@ jobs:
|
||||
- name: Build full editor and tests
|
||||
run: cmake --build --preset windows-debug --parallel 2
|
||||
- name: CPU contracts and software GPU pixel tests
|
||||
env:
|
||||
VK_LOADER_DEBUG: error,warn,driver
|
||||
run: ctest --preset windows-debug --timeout 180
|
||||
- name: Real Release exports, 2D and 3D execution, incremental Debug rebuild
|
||||
run: build/windows-debug/faset_build_service_tests.exe --integration .cache/windows-export-e2e
|
||||
@@ -61,6 +71,9 @@ jobs:
|
||||
include-hidden-files: true
|
||||
path: |
|
||||
.cache/windows-graphics/toolchain.json
|
||||
.cache/windows-graphics/probe.json
|
||||
.cache/windows-graphics/driver/*.json
|
||||
.cache/windows-graphics/sdk/source-identity.json
|
||||
build/windows-debug/Testing/Temporary/LastTest.log
|
||||
build/windows-debug/*test.ppm
|
||||
.cache/windows-export-e2e/result-*.json
|
||||
|
||||
@@ -264,6 +264,8 @@ void run() {
|
||||
auto importedScene = scene;
|
||||
importedScene["entities"][1]["components"][1]["fields"]["asset"] = imported.asset_id;
|
||||
snapshot = view.build(importedScene, 1);
|
||||
for (const auto& diagnostic : view.diagnostics())
|
||||
std::cerr << "Cooked fixture: " << diagnostic << '\n';
|
||||
check(view.diagnostics().empty(), "valid cooked texture/material produces no error");
|
||||
check(snapshot.draws.size() == 1 && snapshot.draws[0].mesh->vertices.size() == 3,
|
||||
"cooked mesh reaches render snapshot");
|
||||
|
||||
@@ -25,6 +25,23 @@ SOURCES = {
|
||||
"swiftshader": ("google/swiftshader", "1e80438d2b93ef36a7c05f8d2b81233bac0e3d16", "1c3a1afc397c7aa4d3275790bc9b451e80b144a1db665135d5519838bacf44a8"),
|
||||
}
|
||||
|
||||
# This exact previous helper cache used the same verified source pins. Allow a
|
||||
# one-time migration without rebuilding LLVM, but never restore it after pin changes.
|
||||
LEGACY_SOURCE_IDENTITY = "38d3f6735736a2d0cf25cec2d9edef8b1220f6d10d13a694efb633aa85c3b8fc"
|
||||
LEGACY_CACHE_KEY = "windows-2025-vulkan-0ca023e20dd35e447b4e13537e95c70859cc7604367979f32c45705b3f370568"
|
||||
|
||||
|
||||
def cache_identity() -> str:
|
||||
return hashlib.sha256(json.dumps(SOURCES, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
|
||||
|
||||
|
||||
def stamp(directory: Path, names: tuple[str, ...]) -> None:
|
||||
identity = {name: list(SOURCES[name]) for name in names}
|
||||
path = directory / "source-identity.json"
|
||||
if path.exists() and json.loads(path.read_text(encoding="utf-8")) != identity:
|
||||
raise RuntimeError(f"Cached tool source identity differs from the pins: {path}")
|
||||
path.write_text(json.dumps(identity, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def run(*arguments: str | Path) -> None:
|
||||
command = [str(argument) for argument in arguments]
|
||||
@@ -67,7 +84,18 @@ def configure(source_path: Path, build: Path, *arguments: str) -> None:
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--cache", type=Path, default=Path(".cache/windows-graphics"))
|
||||
parser.add_argument("--loader-only", action="store_true", help="Build headers/loader only for CPU renderer-linked tests")
|
||||
parser.add_argument("--cache-key", action="store_true", help="Print source identity and expose Actions cache outputs without building")
|
||||
args = parser.parse_args()
|
||||
if args.cache_key:
|
||||
identity = cache_identity()
|
||||
key = "windows-2025-vulkan-v2-" + identity
|
||||
legacy = LEGACY_CACHE_KEY if identity == LEGACY_SOURCE_IDENTITY else ""
|
||||
print(key)
|
||||
if "GITHUB_OUTPUT" in os.environ:
|
||||
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
|
||||
output.write(f"key={key}\nlegacy={legacy}\n")
|
||||
return 0
|
||||
if sys.platform != "win32":
|
||||
parser.error("Run this helper on Windows in a Visual Studio x64 developer environment")
|
||||
for program in ("cmake", "ninja", "cl"):
|
||||
@@ -83,7 +111,8 @@ def main() -> int:
|
||||
configure(loader, cache / "loader-build", f"-DCMAKE_INSTALL_PREFIX={sdk}", f"-DCMAKE_PREFIX_PATH={sdk}", "-DBUILD_TESTS=OFF", "-DBUILD_WERROR=OFF")
|
||||
run("cmake", "--build", cache / "loader-build", "--parallel", "2")
|
||||
run("cmake", "--install", cache / "loader-build")
|
||||
if not (driver / "vk_swiftshader_icd.json").is_file():
|
||||
stamp(sdk, ("headers", "loader"))
|
||||
if not args.loader_only and not (driver / "vk_swiftshader_icd.json").is_file():
|
||||
swift = source(cache, "swiftshader")
|
||||
build = cache / "swiftshader-build"
|
||||
configure(swift, build, "-DSWIFTSHADER_BUILD_TESTS=OFF", "-DSWIFTSHADER_BUILD_BENCHMARKS=OFF", "-DSWIFTSHADER_BUILD_PVR=OFF", "-DSWIFTSHADER_WARNINGS_AS_ERRORS=OFF")
|
||||
@@ -93,14 +122,29 @@ def main() -> int:
|
||||
library = (manifest_directory / manifest["ICD"]["library_path"]).resolve()
|
||||
driver.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(library, driver / library.name)
|
||||
manifest["ICD"]["library_path"] = "./" + library.name
|
||||
# Khronos' Windows loader identifies relative paths by a backslash, not
|
||||
# a forward slash. './name.dll' otherwise remains relative to the cwd.
|
||||
manifest["ICD"]["library_path"] = ".\\" + library.name
|
||||
(driver / "vk_swiftshader_icd.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
||||
report = {"sources": {name: {"repository": repository, "commit": commit, "sha256": digest} for name, (repository, commit, digest) in SOURCES.items()}, "validation_layer": False, "vulkan_sdk": str(sdk), "driver": str(driver / "vk_swiftshader_icd.json")}
|
||||
if not args.loader_only:
|
||||
# Repair the old separator on cache hits as well as newly built bundles.
|
||||
manifest_path = driver / "vk_swiftshader_icd.json"
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
library_name = Path(manifest["ICD"]["library_path"]).name
|
||||
if not (driver / library_name).is_file():
|
||||
raise RuntimeError("Cached SwiftShader manifest points to a missing DLL")
|
||||
manifest["ICD"]["library_path"] = ".\\" + library_name
|
||||
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
||||
stamp(driver, ("swiftshader",))
|
||||
used_sources = {name: source for name, source in SOURCES.items() if not args.loader_only or name != "swiftshader"}
|
||||
report = {"sources": {name: {"repository": repository, "commit": commit, "sha256": digest} for name, (repository, commit, digest) in used_sources.items()}, "validation_layer": False, "vulkan_sdk": str(sdk), "driver": None if args.loader_only else str(driver / "vk_swiftshader_icd.json"), "loader_only": args.loader_only}
|
||||
cache.mkdir(parents=True, exist_ok=True)
|
||||
(cache / "toolchain.json").write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
|
||||
if "GITHUB_ENV" in os.environ:
|
||||
with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as output:
|
||||
output.write(f"VULKAN_SDK={sdk}\nVK_DRIVER_FILES={report['driver']}\nVK_ICD_FILENAMES={report['driver']}\n")
|
||||
output.write(f"VULKAN_SDK={sdk}\n")
|
||||
if report["driver"]:
|
||||
output.write(f"VK_DRIVER_FILES={report['driver']}\nVK_ICD_FILENAMES={report['driver']}\n")
|
||||
with open(os.environ["GITHUB_PATH"], "a", encoding="utf-8") as output:
|
||||
output.write(str(sdk / "bin") + "\n")
|
||||
print(json.dumps(report, indent=2))
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user