From b0a9034f0d5c89c7e9ff4cf1e08991d05dedd00b Mon Sep 17 00:00:00 2001 From: Trycky Date: Sun, 8 Nov 2026 20:41:28 +0100 Subject: [PATCH 1/2] fix(vulkan): bind only bytes accessed by buffer-image copies --- gpu/vk/vk_xfer.cpp | 3 +- tests/run_all.py | 1 + tests/test_vulkan_copy_span.py | 104 ++++++++++++++++++++++++ tests/vulkan_copy_span_gpu.cpp | 140 +++++++++++++++++++++++++++++++++ 4 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 tests/test_vulkan_copy_span.py create mode 100644 tests/vulkan_copy_span_gpu.cpp diff --git a/gpu/vk/vk_xfer.cpp b/gpu/vk/vk_xfer.cpp index 18143e62..b139e1d4 100644 --- a/gpu/vk/vk_xfer.cpp +++ b/gpu/vk/vk_xfer.cpp @@ -136,7 +136,8 @@ bool buffer_side(const NvnState& st, const Sub& s, uint64_t addr, uint64_t& size warn(3, "copy row stride does not fit the level's blocks: skipped"); return false; } - size = row * hb * s.layers * s.d; + // The last row ends at its last block: its trailing stride padding is not accessed by Vulkan. + size = row * ((uint64_t)hb * s.layers * s.d - 1) + (uint64_t)wb * s.bpb; span = gpu_buffer_bind(addr, size, written); if (!addr || !span) { warn(3, "copy buffer is not one mapped pool span: skipped"); diff --git a/tests/run_all.py b/tests/run_all.py index 8d5d3bc1..099f6d8f 100644 --- a/tests/run_all.py +++ b/tests/run_all.py @@ -34,6 +34,7 @@ "tests/test_system_header_cache.py": ["--cc", "clang"], "tests/test_precompile_compute_guard.py": ["--cxx", "clang++"], "tests/test_vulkan_clear_mip.py": ["--cxx", "{cxx}", "--headers", "{vulkan_headers}"], + "tests/test_vulkan_copy_span.py": ["--cxx", "{cxx}", "--headers", "{vulkan_headers}"], "tests/test_input_background.py": ["--cc", "gcc"], "tests/test_oot_wide_save_picture.py": ["--cc", "gcc"], "nx2/test_import_windows.py": ["--cxx", "g++"], diff --git a/tests/test_vulkan_copy_span.py b/tests/test_vulkan_copy_span.py new file mode 100644 index 00000000..7affd1a1 --- /dev/null +++ b/tests/test_vulkan_copy_span.py @@ -0,0 +1,104 @@ +"""Run the production buffer/image copy layout against synthetic mapped pool spans.""" +import argparse +import os +from pathlib import Path +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[1] + + +def block(source, marker): + start = source.index(marker) + end = source.index("{", start) + 1 + depth = 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[start:end] + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--cxx", default="clang++") + parser.add_argument("--headers", type=Path, required=True) + parser.add_argument("--source", type=Path, default=ROOT / "gpu/vk/vk_xfer.cpp") + parser.add_argument("--sanitize", action="store_true") + parser.add_argument("--gpu", action="store_true", help="opt-in bounded upload/readback on an AMD RX 9070") + args = parser.parse_args() + source = args.source.read_text(encoding="utf-8") + fixture = r''' +#define VK_NO_PROTOTYPES +#include +#include +#include +#include +#include +#define CHECK(x) do { if (!(x)) { std::fprintf(stderr,"FAIL:%d %s\n",__LINE__,#x); std::exit(1); } } while (0) +struct Texture {}; +struct NvnState { int64_t copy_stride = 0; }; +struct BufferRef { VkBuffer buffer{}; uint64_t offset=0, size=0; explicit operator bool() const { return buffer != VK_NULL_HANDLE; } }; +uint64_t pool_size, requested; +unsigned binds; +bool expect_written; +void warn(int, const char*) {} +BufferRef gpu_buffer_bind(uint64_t addr, uint64_t size, bool written) { + ++binds; requested=size; + CHECK(written == expect_written); + return addr && size <= pool_size ? BufferRef{(VkBuffer)1, 0, size} : BufferRef{}; +} +''' + block(source, "struct Sub {") + ";\n" + "\n".join( + block(source, marker) for marker in ("VkImageSubresourceLayers layers(", "VkOffset3D offset(", + "VkExtent3D extent(", "bool buffer_side(")) + r''' +void run(uint32_t w, uint32_t h, uint32_t depth, uint32_t layers_n, int bw, int bh, + uint32_t bpb, int64_t stride, uint64_t available, uint64_t expected, bool valid) { + Sub s; s.w=w; s.h=h; s.d=depth; s.layers=layers_n; s.bw=bw; s.bh=bh; s.bpb=bpb; + s.aspect=VK_IMAGE_ASPECT_COLOR_BIT; s.level=2; s.layer=3; s.x=4; s.y=8; + for (bool written : {false,true}) { + NvnState state{stride}; BufferRef span; VkBufferImageCopy copy{}; uint64_t size=0; + pool_size=available; requested=0; binds=0; expect_written=written; + const bool ok=buffer_side(state,s,0x1000,size,copy,span,written); + if(ok!=valid) std::fprintf(stderr,"copy %ux%u: requested %llu bytes, pool has %llu, expected span %llu\n", + w,h,(unsigned long long)requested,(unsigned long long)available,(unsigned long long)expected); + CHECK(ok == valid); + if (!valid) continue; + CHECK(binds==1 && requested==expected && size==expected); + CHECK(copy.bufferOffset==0 && copy.bufferRowLength==(stride ? stride/bpb*bw : ((w+bw-1)/bw)*bw)); + CHECK(copy.bufferImageHeight==((h+bh-1)/bh)*bh); + CHECK(copy.imageSubresource.mipLevel==2 && copy.imageSubresource.baseArrayLayer==3 && copy.imageSubresource.layerCount==layers_n); + CHECK(copy.imageOffset.x==4 && copy.imageOffset.y==8 && copy.imageExtent.width==w && copy.imageExtent.height==h && copy.imageExtent.depth==depth); + } +} +int main() { + run(2,2,1,1,1,1,4,16,24,24,true); // no padding is accessed after the final row + run(2,1,1,1,1,1,4,16,8,8,true); + run(2,2,1,3,1,1,4,16,88,88,true); // intermediate layers retain their padding + run(2,2,3,1,1,1,4,16,88,88,true); + run(5,5,1,1,4,4,16,48,80,80,true); // partial BC edge blocks + run(2,2,1,1,1,1,4,0,16,16,true); + run(2,2,1,1,1,1,4,16,23,24,false); // genuinely truncated pool + run(2,2,1,1,1,1,4,7,32,0,false); // row too short/misaligned + std::puts("PASS: 8 copy layouts, upload/readback, exact spans and truncated pools"); +} +''' + if args.gpu: + production = block(source, "struct Sub {") + ";\n" + "\n".join( + block(source, marker) for marker in ("VkImageSubresourceLayers layers(", "VkOffset3D offset(", + "VkExtent3D extent(", "bool buffer_side(")) + fixture = (ROOT / "tests/vulkan_copy_span_gpu.cpp").read_text(encoding="utf-8").replace( + "// PRODUCTION_COPY_LAYOUT", production) + with tempfile.TemporaryDirectory(prefix="hookshot-copy-span-") as directory: + cpp, exe = Path(directory) / "test.cpp", Path(directory) / "test.exe" + cpp.write_text(fixture, encoding="utf-8") + flags = ["-fsanitize=address,undefined", "-fno-sanitize-recover=all"] if args.sanitize else [] + if args.gpu: + flags += ["-Wno-missing-field-initializers"] + flags += ["-static"] if os.name == "nt" and "mingw" in args.cxx.lower() else [] + libraries = ["-ldl"] if args.gpu and os.name != "nt" else [] + subprocess.run([args.cxx, "-std=c++20", "-O1", "-Wall", "-Wextra", "-Werror", *flags, + "-I" + str(args.headers), str(cpp), "-o", str(exe), *libraries], check=True) + subprocess.run([str(exe)], check=True, timeout=20 if args.gpu else 10) + + +if __name__ == "__main__": + main() diff --git a/tests/vulkan_copy_span_gpu.cpp b/tests/vulkan_copy_span_gpu.cpp new file mode 100644 index 00000000..bfa0e99f --- /dev/null +++ b/tests/vulkan_copy_span_gpu.cpp @@ -0,0 +1,140 @@ +// Optional bounded hardware check. The Python runner inserts the actual copy-layout functions at the marker. +#define VK_NO_PROTOTYPES +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#endif +#define CHECK(x) do { if (!(x)) { std::fprintf(stderr,"FAIL:%d %s\n",__LINE__,#x); std::exit(1); } } while (0) +#define APIS(X) X(vkCreateInstance) X(vkDestroyInstance) X(vkEnumerateInstanceLayerProperties) \ +X(vkEnumeratePhysicalDevices) X(vkGetPhysicalDeviceProperties) X(vkGetPhysicalDeviceQueueFamilyProperties) \ +X(vkGetPhysicalDeviceMemoryProperties) X(vkCreateDevice) X(vkDestroyDevice) X(vkGetDeviceQueue) \ +X(vkCreateBuffer) X(vkDestroyBuffer) X(vkGetBufferMemoryRequirements) X(vkBindBufferMemory) \ +X(vkCreateImage) X(vkDestroyImage) X(vkGetImageMemoryRequirements) X(vkBindImageMemory) \ +X(vkAllocateMemory) X(vkFreeMemory) X(vkMapMemory) X(vkUnmapMemory) X(vkCreateCommandPool) \ +X(vkDestroyCommandPool) X(vkAllocateCommandBuffers) X(vkBeginCommandBuffer) X(vkEndCommandBuffer) \ +X(vkCmdPipelineBarrier) X(vkCmdCopyBufferToImage) X(vkCmdCopyImageToBuffer) X(vkCmdFillBuffer) \ +X(vkCreateFence) X(vkDestroyFence) X(vkQueueSubmit) X(vkWaitForFences) +#define DECLARE(n) PFN_##n n; +APIS(DECLARE) +struct Texture {}; +struct NvnState { int64_t copy_stride=16; }; +struct BufferRef { VkBuffer buffer{}; uint64_t offset=0,size=0; explicit operator bool() const { return buffer!=VK_NULL_HANDLE; } }; +VkBuffer copy_buffer; +void warn(int,const char* text) { std::fprintf(stderr,"%s\n",text); } +BufferRef gpu_buffer_bind(uint64_t addr,uint64_t size,bool) { + return addr==0x1000 && size<=24 ? BufferRef{copy_buffer,0,size} : BufferRef{}; +} +// PRODUCTION_COPY_LAYOUT + +uint32_t memory_type(const VkPhysicalDeviceMemoryProperties& props,uint32_t bits,VkMemoryPropertyFlags required) { + for(uint32_t i=0;ipMessage); + } + return VK_FALSE; +} +int main() { +#ifdef _WIN32 + auto loader=LoadLibraryW(L"vulkan-1.dll"); CHECK(loader); + auto symbol=[&](const char* name) { return GetProcAddress(loader,name); }; +#else + auto loader=dlopen("libvulkan.so.1",RTLD_NOW); CHECK(loader); + auto symbol=[&](const char* name) { return dlsym(loader,name); }; +#endif +#define LOAD(n) n=reinterpret_cast(symbol(#n)); CHECK(n); + APIS(LOAD) + auto gip=reinterpret_cast(symbol("vkGetInstanceProcAddr")); CHECK(gip); + uint32_t nl=0; CHECK(vkEnumerateInstanceLayerProperties(&nl,nullptr)==VK_SUCCESS); + std::vector layers_list(nl); CHECK(vkEnumerateInstanceLayerProperties(&nl,layers_list.data())==VK_SUCCESS); + bool validation=false; + for(const auto& layer:layers_list) if(!std::strcmp(layer.layerName,"VK_LAYER_KHRONOS_validation")) validation=true; + const char* layer="VK_LAYER_KHRONOS_validation"; + const char* extension=VK_EXT_DEBUG_UTILS_EXTENSION_NAME; + VkDebugUtilsMessengerCreateInfoEXT db{VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT}; + db.messageSeverity=VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; + db.messageType=VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT|VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT|VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; + db.pfnUserCallback=debug; + VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO}; app.apiVersion=VK_API_VERSION_1_1; + VkInstanceCreateInfo ici{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; ici.pApplicationInfo=&app; + if(validation) { ici.enabledLayerCount=1;ici.ppEnabledLayerNames=&layer;ici.enabledExtensionCount=1;ici.ppEnabledExtensionNames=&extension;ici.pNext=&db; } + VkInstance instance; CHECK(vkCreateInstance(&ici,nullptr,&instance)==VK_SUCCESS); + VkDebugUtilsMessengerEXT messenger{}; + PFN_vkDestroyDebugUtilsMessengerEXT destroy_debug=nullptr; + if(validation) { + auto create_debug=reinterpret_cast(gip(instance,"vkCreateDebugUtilsMessengerEXT")); + destroy_debug=reinterpret_cast(gip(instance,"vkDestroyDebugUtilsMessengerEXT")); + CHECK(create_debug && destroy_debug && create_debug(instance,&db,nullptr,&messenger)==VK_SUCCESS); + } + std::printf("Khronos validation: %s\n",validation ? "enabled" : "unavailable"); + uint32_t np=0; CHECK(vkEnumeratePhysicalDevices(instance,&np,nullptr)==VK_SUCCESS); + std::vector devices(np); CHECK(vkEnumeratePhysicalDevices(instance,&np,devices.data())==VK_SUCCESS); + VkPhysicalDevice physical{}; VkPhysicalDeviceProperties properties{}; + for(auto candidate:devices) { + vkGetPhysicalDeviceProperties(candidate,&properties); + if(properties.vendorID==0x1002 && std::strstr(properties.deviceName,"RX 9070")) { physical=candidate;break; } + } + CHECK(physical); + std::printf("GPU: %s; Vulkan %u.%u.%u; driver %#x\n",properties.deviceName,VK_VERSION_MAJOR(properties.apiVersion),VK_VERSION_MINOR(properties.apiVersion),VK_VERSION_PATCH(properties.apiVersion),properties.driverVersion); + uint32_t nq=0; vkGetPhysicalDeviceQueueFamilyProperties(physical,&nq,nullptr); + std::vector queues(nq); vkGetPhysicalDeviceQueueFamilyProperties(physical,&nq,queues.data()); + uint32_t family=0; for(;family Date: Mon, 9 Nov 2026 20:25:24 +0100 Subject: [PATCH 2/2] test(vulkan): make copy-span GPU probe portable --- tests/run_all.py | 1 + tests/test_vulkan_copy_span.py | 9 +- tests/test_vulkan_copy_span_gpu_compile.py | 58 ++++++ tests/vulkan_copy_span_gpu.cpp | 211 ++++++++++++++++----- 4 files changed, 232 insertions(+), 47 deletions(-) create mode 100644 tests/test_vulkan_copy_span_gpu_compile.py diff --git a/tests/run_all.py b/tests/run_all.py index 099f6d8f..364d44de 100644 --- a/tests/run_all.py +++ b/tests/run_all.py @@ -35,6 +35,7 @@ "tests/test_precompile_compute_guard.py": ["--cxx", "clang++"], "tests/test_vulkan_clear_mip.py": ["--cxx", "{cxx}", "--headers", "{vulkan_headers}"], "tests/test_vulkan_copy_span.py": ["--cxx", "{cxx}", "--headers", "{vulkan_headers}"], + "tests/test_vulkan_copy_span_gpu_compile.py": ["--cxx", "{cxx}", "--headers", "{vulkan_headers}"], "tests/test_input_background.py": ["--cc", "gcc"], "tests/test_oot_wide_save_picture.py": ["--cc", "gcc"], "nx2/test_import_windows.py": ["--cxx", "g++"], diff --git a/tests/test_vulkan_copy_span.py b/tests/test_vulkan_copy_span.py index 7affd1a1..de7ce559 100644 --- a/tests/test_vulkan_copy_span.py +++ b/tests/test_vulkan_copy_span.py @@ -24,8 +24,10 @@ def main(): parser.add_argument("--headers", type=Path, required=True) parser.add_argument("--source", type=Path, default=ROOT / "gpu/vk/vk_xfer.cpp") parser.add_argument("--sanitize", action="store_true") - parser.add_argument("--gpu", action="store_true", help="opt-in bounded upload/readback on an AMD RX 9070") + parser.add_argument("--gpu", action="store_true", help="opt-in bounded Vulkan upload/readback on a compatible physical device") + parser.add_argument("--gpu-device", help="case-insensitive device-name substring for the --gpu probe") args = parser.parse_args() + args.gpu = args.gpu or bool(args.gpu_device) source = args.source.read_text(encoding="utf-8") fixture = r''' #define VK_NO_PROTOTYPES @@ -97,7 +99,10 @@ def main(): libraries = ["-ldl"] if args.gpu and os.name != "nt" else [] subprocess.run([args.cxx, "-std=c++20", "-O1", "-Wall", "-Wextra", "-Werror", *flags, "-I" + str(args.headers), str(cpp), "-o", str(exe), *libraries], check=True) - subprocess.run([str(exe)], check=True, timeout=20 if args.gpu else 10) + command = [str(exe)] + if args.gpu_device: + command += ["--device", args.gpu_device] + subprocess.run(command, check=True, timeout=20 if args.gpu else 10) if __name__ == "__main__": diff --git a/tests/test_vulkan_copy_span_gpu_compile.py b/tests/test_vulkan_copy_span_gpu_compile.py new file mode 100644 index 00000000..a7794e56 --- /dev/null +++ b/tests/test_vulkan_copy_span_gpu_compile.py @@ -0,0 +1,58 @@ +"""Compile the opt-in Vulkan hardware fixture without loading Vulkan or executing GPU work.""" +import argparse +import os +from pathlib import Path +import shutil +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[1] + + +def block(source, marker): + start = source.index(marker) + end = source.index("{", start) + 1 + depth = 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[start:end] + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--cxx", default="clang++") + parser.add_argument("--headers", type=Path) + parser.add_argument("--source", type=Path, default=ROOT / "gpu/vk/vk_xfer.cpp") + args = parser.parse_args() + compiler = shutil.which(args.cxx) or (args.cxx if Path(args.cxx).is_file() else None) + if not compiler: + print(f"SKIP: C++ compiler unavailable: {args.cxx}") + return 0 + if not args.headers or not (args.headers / "vulkan/vulkan_core.h").is_file(): + print("SKIP: Vulkan headers unavailable") + return 0 + source = args.source.read_text(encoding="utf-8") + production = block(source, "struct Sub {") + ";\n" + "\n".join( + block(source, marker) for marker in ( + "VkImageSubresourceLayers layers(", "VkOffset3D offset(", + "VkExtent3D extent(", "bool buffer_side(" + ) + ) + fixture = (ROOT / "tests/vulkan_copy_span_gpu.cpp").read_text(encoding="utf-8").replace( + "// PRODUCTION_COPY_LAYOUT", production + ) + with tempfile.TemporaryDirectory(prefix="hookshot-copy-span-gpu-compile-") as directory: + cpp, exe = Path(directory) / "fixture.cpp", Path(directory) / "fixture.exe" + cpp.write_text(fixture, encoding="utf-8") + flags = ["-std=c++20", "-O1", "-Wall", "-Wextra", "-Werror", "-Wno-missing-field-initializers"] + if os.name == "nt" and "mingw" in compiler.lower(): + flags.append("-static") + libraries = ["-ldl"] if os.name != "nt" else [] + subprocess.run([compiler, *flags, "-I" + str(args.headers), str(cpp), "-o", str(exe), *libraries], + check=True, timeout=120) + print("PASS: Vulkan hardware fixture compiled; no Vulkan loader or GPU work executed") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/vulkan_copy_span_gpu.cpp b/tests/vulkan_copy_span_gpu.cpp index bfa0e99f..326e2506 100644 --- a/tests/vulkan_copy_span_gpu.cpp +++ b/tests/vulkan_copy_span_gpu.cpp @@ -2,10 +2,13 @@ #define VK_NO_PROTOTYPES #include #include +#include +#include #include #include #include #include +#include #include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN @@ -15,7 +18,8 @@ #endif #define CHECK(x) do { if (!(x)) { std::fprintf(stderr,"FAIL:%d %s\n",__LINE__,#x); std::exit(1); } } while (0) #define APIS(X) X(vkCreateInstance) X(vkDestroyInstance) X(vkEnumerateInstanceLayerProperties) \ -X(vkEnumeratePhysicalDevices) X(vkGetPhysicalDeviceProperties) X(vkGetPhysicalDeviceQueueFamilyProperties) \ +X(vkEnumerateInstanceExtensionProperties) X(vkEnumeratePhysicalDevices) X(vkGetPhysicalDeviceProperties) \ +X(vkGetPhysicalDeviceFormatProperties) X(vkGetPhysicalDeviceQueueFamilyProperties) \ X(vkGetPhysicalDeviceMemoryProperties) X(vkCreateDevice) X(vkDestroyDevice) X(vkGetDeviceQueue) \ X(vkCreateBuffer) X(vkDestroyBuffer) X(vkGetBufferMemoryRequirements) X(vkBindBufferMemory) \ X(vkCreateImage) X(vkDestroyImage) X(vkGetImageMemoryRequirements) X(vkBindImageMemory) \ @@ -29,6 +33,7 @@ struct Texture {}; struct NvnState { int64_t copy_stride=16; }; struct BufferRef { VkBuffer buffer{}; uint64_t offset=0,size=0; explicit operator bool() const { return buffer!=VK_NULL_HANDLE; } }; VkBuffer copy_buffer; +std::atomic validation_errors{0}; void warn(int,const char* text) { std::fprintf(stderr,"%s\n",text); } BufferRef gpu_buffer_bind(uint64_t addr,uint64_t size,bool) { return addr==0x1000 && size<=24 ? BufferRef{copy_buffer,0,size} : BufferRef{}; @@ -38,78 +43,192 @@ BufferRef gpu_buffer_bind(uint64_t addr,uint64_t size,bool) { uint32_t memory_type(const VkPhysicalDeviceMemoryProperties& props,uint32_t bits,VkMemoryPropertyFlags required) { for(uint32_t i=0;ipMessage); - } - return VK_FALSE; +int skip(const char* reason) { + std::printf("SKIP: %s\n",reason); + return 0; +} +std::string lower(std::string s) { + std::transform(s.begin(),s.end(),s.begin(),[](unsigned char c){return (char)std::tolower(c);}); + return s; } -int main() { +int main(int argc,char** argv) { + const char* requested_device=nullptr; + for(int i=1;i(symbol(#n)); CHECK(n); + if(!loader) return skip("Vulkan loader unavailable"); + bool api_ready=true; +#define LOAD(n) n=reinterpret_cast(symbol(#n)); api_ready=api_ready && n; APIS(LOAD) - auto gip=reinterpret_cast(symbol("vkGetInstanceProcAddr")); CHECK(gip); +#undef LOAD + auto gip=reinterpret_cast(symbol("vkGetInstanceProcAddr")); + if(!api_ready || !gip) { +#ifdef _WIN32 + FreeLibrary(loader); +#else + dlclose(loader); +#endif + return skip("required Vulkan entry points unavailable"); + } uint32_t nl=0; CHECK(vkEnumerateInstanceLayerProperties(&nl,nullptr)==VK_SUCCESS); - std::vector layers_list(nl); CHECK(vkEnumerateInstanceLayerProperties(&nl,layers_list.data())==VK_SUCCESS); - bool validation=false; + std::vector layers_list(nl); + CHECK(vkEnumerateInstanceLayerProperties(&nl,layers_list.data())==VK_SUCCESS); + uint32_t ne=0; CHECK(vkEnumerateInstanceExtensionProperties(nullptr,&ne,nullptr)==VK_SUCCESS); + std::vector extensions(ne); + CHECK(vkEnumerateInstanceExtensionProperties(nullptr,&ne,extensions.data())==VK_SUCCESS); + bool validation=false,debug_utils=false; for(const auto& layer:layers_list) if(!std::strcmp(layer.layerName,"VK_LAYER_KHRONOS_validation")) validation=true; + for(const auto& ext:extensions) if(!std::strcmp(ext.extensionName,VK_EXT_DEBUG_UTILS_EXTENSION_NAME)) debug_utils=true; const char* layer="VK_LAYER_KHRONOS_validation"; const char* extension=VK_EXT_DEBUG_UTILS_EXTENSION_NAME; VkDebugUtilsMessengerCreateInfoEXT db{VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT}; db.messageSeverity=VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; db.messageType=VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT|VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT|VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; - db.pfnUserCallback=debug; + db.pfnUserCallback=[](VkDebugUtilsMessageSeverityFlagBitsEXT severity,VkDebugUtilsMessageTypeFlagsEXT, + const VkDebugUtilsMessengerCallbackDataEXT* data,void*) -> VkBool32 { + if(severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) { + validation_errors.fetch_add(1,std::memory_order_relaxed); + std::fprintf(stderr,"validation: %s\n",data->pMessage); + } + return VK_FALSE; + }; VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO}; app.apiVersion=VK_API_VERSION_1_1; VkInstanceCreateInfo ici{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; ici.pApplicationInfo=&app; - if(validation) { ici.enabledLayerCount=1;ici.ppEnabledLayerNames=&layer;ici.enabledExtensionCount=1;ici.ppEnabledExtensionNames=&extension;ici.pNext=&db; } - VkInstance instance; CHECK(vkCreateInstance(&ici,nullptr,&instance)==VK_SUCCESS); + if(validation) { ici.enabledLayerCount=1;ici.ppEnabledLayerNames=&layer; } + if(validation && debug_utils) { ici.enabledExtensionCount=1;ici.ppEnabledExtensionNames=&extension;ici.pNext=&db; } + VkInstance instance{}; + const VkResult instance_result=vkCreateInstance(&ici,nullptr,&instance); + if(instance_result==VK_ERROR_INCOMPATIBLE_DRIVER || instance_result==VK_ERROR_INITIALIZATION_FAILED) { +#ifdef _WIN32 + FreeLibrary(loader); +#else + dlclose(loader); +#endif + return skip("Vulkan instance or compatible driver unavailable"); + } + CHECK(instance_result==VK_SUCCESS); VkDebugUtilsMessengerEXT messenger{}; PFN_vkDestroyDebugUtilsMessengerEXT destroy_debug=nullptr; - if(validation) { + if(validation && debug_utils) { auto create_debug=reinterpret_cast(gip(instance,"vkCreateDebugUtilsMessengerEXT")); destroy_debug=reinterpret_cast(gip(instance,"vkDestroyDebugUtilsMessengerEXT")); CHECK(create_debug && destroy_debug && create_debug(instance,&db,nullptr,&messenger)==VK_SUCCESS); } - std::printf("Khronos validation: %s\n",validation ? "enabled" : "unavailable"); - uint32_t np=0; CHECK(vkEnumeratePhysicalDevices(instance,&np,nullptr)==VK_SUCCESS); - std::vector devices(np); CHECK(vkEnumeratePhysicalDevices(instance,&np,devices.data())==VK_SUCCESS); + std::printf("Khronos validation: %s\n",validation ? (debug_utils ? "enabled" : "enabled; debug utils unavailable") : "unavailable"); + uint32_t np=0; + const VkResult enumerate_result=vkEnumeratePhysicalDevices(instance,&np,nullptr); + if(enumerate_result==VK_ERROR_INITIALIZATION_FAILED) { + if(messenger) { destroy_debug(instance,messenger,nullptr); } + vkDestroyInstance(instance,nullptr); +#ifdef _WIN32 + FreeLibrary(loader); +#else + dlclose(loader); +#endif + return skip("Vulkan physical-device enumeration unavailable"); + } + CHECK(enumerate_result==VK_SUCCESS); + if(!np) { + if(messenger) destroy_debug(instance,messenger,nullptr); + vkDestroyInstance(instance,nullptr); +#ifdef _WIN32 + FreeLibrary(loader); +#else + dlclose(loader); +#endif + return skip("no Vulkan physical devices found"); + } + std::vector devices(np); + CHECK(vkEnumeratePhysicalDevices(instance,&np,devices.data())==VK_SUCCESS); VkPhysicalDevice physical{}; VkPhysicalDeviceProperties properties{}; + VkPhysicalDeviceMemoryProperties memory{}; uint32_t family=UINT32_MAX; + const std::string wanted=requested_device ? lower(requested_device) : std::string{}; for(auto candidate:devices) { - vkGetPhysicalDeviceProperties(candidate,&properties); - if(properties.vendorID==0x1002 && std::strstr(properties.deviceName,"RX 9070")) { physical=candidate;break; } + VkPhysicalDeviceProperties candidate_properties{}; + vkGetPhysicalDeviceProperties(candidate,&candidate_properties); + if(candidate_properties.apiVersion < VK_API_VERSION_1_1) continue; + if(!wanted.empty() && lower(candidate_properties.deviceName).find(wanted)==std::string::npos) continue; + VkFormatProperties format{}; + vkGetPhysicalDeviceFormatProperties(candidate,VK_FORMAT_R8G8B8A8_UNORM,&format); + const VkFormatFeatureFlags needed=VK_FORMAT_FEATURE_TRANSFER_SRC_BIT|VK_FORMAT_FEATURE_TRANSFER_DST_BIT; + if((format.optimalTilingFeatures&needed)!=needed) continue; + uint32_t nq=0; vkGetPhysicalDeviceQueueFamilyProperties(candidate,&nq,nullptr); + std::vector queues(nq); + vkGetPhysicalDeviceQueueFamilyProperties(candidate,&nq,queues.data()); + uint32_t candidate_family=UINT32_MAX; + for(uint32_t q=0;q queues(nq); vkGetPhysicalDeviceQueueFamilyProperties(physical,&nq,queues.data()); - uint32_t family=0; for(;family