From d7c1ced08d5d78fcb239550404db4e67d4357a19 Mon Sep 17 00:00:00 2001 From: hachem Date: Tue, 22 Sep 2026 06:57:16 +0200 Subject: fix: windows support --- src/platform/opengl/shader.cpp | 13 +++++- src/platform/opengl/texture.cpp | 14 +++--- src/platform/vulkan/vulkan_common.h | 7 ++- src/platform/vulkan/vulkan_cubemap.cpp | 33 ++++++++++---- src/platform/vulkan/vulkan_device.cpp | 46 +++++++++++++++++-- src/platform/vulkan/vulkan_device.h | 4 ++ src/platform/vulkan/vulkan_resources.cpp | 5 ++- src/platform/vulkan/vulkan_swapchain.cpp | 77 ++++++++++++++++++++++++++------ 8 files changed, 162 insertions(+), 37 deletions(-) (limited to 'src/platform') diff --git a/src/platform/opengl/shader.cpp b/src/platform/opengl/shader.cpp index ca63c14..7319f8b 100644 --- a/src/platform/opengl/shader.cpp +++ b/src/platform/opengl/shader.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include "core/log.h" namespace Donut { @@ -93,6 +95,13 @@ namespace Donut in.read(&result[0], size); } } + else + { + std::error_code ec; // u8string: msvc's string() throws on a cwd outside the ANSI code page + const auto cwd = std::filesystem::current_path(ec).u8string(); + DONUT_ERROR("shader file not found: {} (cwd {}; generated shaders live in assets/shaders/generated)", + filepath, std::string(cwd.begin(), cwd.end())); + } return result; } @@ -140,7 +149,7 @@ namespace Donut int max_length = 0; glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &max_length); std::vector info_log(max_length); - glGetShaderInfoLog(shader, max_length, &max_length, &info_log[0]); + if (max_length > 0) glGetShaderInfoLog(shader, max_length, &max_length, info_log.data()); // [0] on an empty vector trips msvc's debug STL glDeleteShader(shader); for (auto id : glShaderIDs) glDeleteShader(id); @@ -167,7 +176,7 @@ namespace Donut int max_length = 0; glGetProgramiv(m_renderer_id, GL_INFO_LOG_LENGTH, &max_length); std::vector info_log(max_length); - glGetProgramInfoLog(m_renderer_id, max_length, &max_length, &info_log[0]); + if (max_length > 0) glGetProgramInfoLog(m_renderer_id, max_length, &max_length, info_log.data()); glDeleteProgram(m_renderer_id); for (auto id : glShaderIDs) glDeleteShader(id); diff --git a/src/platform/opengl/texture.cpp b/src/platform/opengl/texture.cpp index 5036ca6..41d14d5 100644 --- a/src/platform/opengl/texture.cpp +++ b/src/platform/opengl/texture.cpp @@ -188,15 +188,15 @@ namespace Donut glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - auto equirect_shader = Shader::create("assets/shaders/EquirectToCubemap.glsl"); - if (!equirect_shader) + // the slang-generated capture shader, the same source the vulkan path uses + auto equirect_shader = Shader::create("assets/shaders/generated/equirect_to_cubemap.glsl"); + uint32_t shader_program = equirect_shader ? equirect_shader->get_renderer_id() : 0; + if (!shader_program) { - DONUT_ERROR("Failed to create equirectangular to cubemap shader"); + DONUT_ERROR("equirect_to_cubemap shader missing or failed to compile; the sky stays dark"); return; } - uint32_t shader_program = equirect_shader->get_renderer_id(); - float vertices[] = { -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, @@ -231,7 +231,7 @@ namespace Donut glUniform1i(glGetUniformLocation(shader_program, "u_EquirectangularMap"), 0); // EquirectToCubemap is authored in Slang (row-major); transpose glm's // column-major matrices on upload (GL_TRUE) to match. - glUniformMatrix4fv(glGetUniformLocation(shader_program, "u_Projection"), 1, GL_TRUE, &capture_projection[0][0]); + glUniformMatrix4fv(glGetUniformLocation(shader_program, "u_projection"), 1, GL_TRUE, &capture_projection[0][0]); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, hdr_texture); @@ -239,7 +239,7 @@ namespace Donut glBindFramebuffer(GL_FRAMEBUFFER, capture_fbo); for (unsigned int i = 0; i < 6; ++i) { - glUniformMatrix4fv(glGetUniformLocation(shader_program, "u_View"), 1, GL_TRUE, &capture_views[i][0][0]); + glUniformMatrix4fv(glGetUniformLocation(shader_program, "u_view"), 1, GL_TRUE, &capture_views[i][0][0]); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, m_renderer_id, 0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glBindVertexArray(cube_vao); diff --git a/src/platform/vulkan/vulkan_common.h b/src/platform/vulkan/vulkan_common.h index 22d389c..ead4d1f 100644 --- a/src/platform/vulkan/vulkan_common.h +++ b/src/platform/vulkan/vulkan_common.h @@ -187,6 +187,9 @@ namespace Donut::RHI auto bind_pipeline(Pipeline* p) -> void override { m_pipe = static_cast(p); + // a pipeline whose shaders never loaded has no vk object; binding a null + // handle is driver roulette, so skip it and let draw() skip too + if (!m_pipe || m_pipe->m_pipeline == VK_NULL_HANDLE) { m_pipe = nullptr; return; } vkCmdBindPipeline(m_cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipe->m_pipeline); } auto set_viewport(int x, int y, int w, int h, bool flip_y) -> void override @@ -216,9 +219,9 @@ namespace Donut::RHI auto bind_index_buffer(Buffer* ib) -> void override { vkCmdBindIndexBuffer(m_cmd, static_cast(ib)->m_buf, 0, VK_INDEX_TYPE_UINT32); } auto draw(uint32_t vertex_count) -> void override - { flush_descriptors(); vkCmdDraw(m_cmd, vertex_count, 1, 0, 0); } + { if (!m_pipe) return; flush_descriptors(); vkCmdDraw(m_cmd, vertex_count, 1, 0, 0); } auto draw_indexed(uint32_t index_count) -> void override - { flush_descriptors(); vkCmdDrawIndexed(m_cmd, index_count, 1, 0, 0, 0); } + { if (!m_pipe) return; flush_descriptors(); vkCmdDrawIndexed(m_cmd, index_count, 1, 0, 0, 0); } // allocate + write + bind a descriptor set for the current pipeline's // declared resources, using whatever was bound since bind_pipeline. diff --git a/src/platform/vulkan/vulkan_cubemap.cpp b/src/platform/vulkan/vulkan_cubemap.cpp index 3ffc496..de0116e 100644 --- a/src/platform/vulkan/vulkan_cubemap.cpp +++ b/src/platform/vulkan/vulkan_cubemap.cpp @@ -2,6 +2,7 @@ #include "stb_image.h" #include +#include namespace Donut::RHI { @@ -36,10 +37,22 @@ namespace Donut::RHI csm.addressModeU = csm.addressModeV = csm.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; vkCreateSampler(m_device, &csm, nullptr, &tex->m_sampler); + // the capture shaders come first: if they're missing (no generated/ dir) + // take the same dark road as a missing hdr, rather than handing the driver + // two garbage module handles. + VkShaderModule vmod = VK_NULL_HANDLE, fmod = VK_NULL_HANDLE; + const bool shaders_ok = create_shader_module("assets/shaders/generated/equirect_to_cubemap.vertexMain.spv", vmod) + && create_shader_module("assets/shaders/generated/equirect_to_cubemap.fragmentMain.spv", fmod); + // bottom row first, same as the GL loader: SampleSphericalMap sends "up" to + // v = 1, so the top of the panorama has to be the last row. without this the + // whole sky is upside down (the old projection flip was half-hiding that). int w = 0, h = 0, ch = 0; - float* pixels = stbi_loadf(path.c_str(), &w, &h, &ch, 4); + stbi_set_flip_vertically_on_load(true); + float* pixels = shaders_ok ? stbi_loadf(path.c_str(), &w, &h, &ch, 4) : nullptr; if (!pixels) { + if (vmod) vkDestroyShaderModule(m_device, vmod, nullptr); + if (fmod) vkDestroyShaderModule(m_device, fmod, nullptr); DONUT_WARN("Vulkan RHI: HDRI '{}' could not be loaded; using a dark background", path); VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; cbai.commandPool = m_command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; @@ -65,13 +78,14 @@ namespace Donut::RHI } const VkFormat eq_fmt = VK_FORMAT_R16G16B16A16_SFLOAT; - size_t texel_count = (size_t)w * h * 4; + size_t texel_count = (size_t)w * (size_t)h * 4; VkDeviceSize eq_size = (VkDeviceSize)texel_count * sizeof(uint16_t); VkBuffer eq_staging; VkDeviceMemory eq_staging_mem; create_buffer_raw(eq_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, host_vis, eq_staging, eq_staging_mem); void* mp = nullptr; vkMapMemory(m_device, eq_staging_mem, 0, eq_size, 0, &mp); uint16_t* dst = (uint16_t*)mp; - for (size_t i = 0; i < texel_count; ++i) { __fp16 hf = (__fp16)pixels[i]; std::memcpy(&dst[i], &hf, sizeof(uint16_t)); } + // glm's half packing rather than __fp16: that one is a clang/ARM extension and MSVC has never heard of it + for (size_t i = 0; i < texel_count; ++i) dst[i] = glm::packHalf1x16(pixels[i]); vkUnmapMemory(m_device, eq_staging_mem); stbi_image_free(pixels); @@ -141,9 +155,6 @@ namespace Donut::RHI VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; dpci.maxSets = 6; dpci.poolSizeCount = 2; dpci.pPoolSizes = psizes; vkCreateDescriptorPool(m_device, &dpci, nullptr, &pool); - VkShaderModule vmod, fmod; - create_shader_module("assets/shaders/generated/equirect_to_cubemap.vertexMain.spv", vmod); - create_shader_module("assets/shaders/generated/equirect_to_cubemap.fragmentMain.spv", fmod); VkPipelineLayout playout; VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; plci.setLayoutCount = 1; plci.pSetLayouts = &set_layout; vkCreatePipelineLayout(m_device, &plci, nullptr, &playout); @@ -180,8 +191,11 @@ namespace Donut::RHI create_buffer_raw(sizeof(cube_verts), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, cube_vb, cube_vb_mem); vkMapMemory(m_device, cube_vb_mem, 0, sizeof(cube_verts), 0, &mp); std::memcpy(mp, cube_verts, sizeof(cube_verts)); vkUnmapMemory(m_device, cube_vb_mem); + // no vulkan y-flip here. rendering into a texture and sampling it back is + // the same in both APIs (clip-space bottom lands on row 0 either way), so + // these are the exact GL capture matrices. the flip that used to sit here + // mirrored every face top to bottom and scrambled the sky. glm::mat4 proj = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f); - proj[1][1] *= -1.0f; glm::mat4 views[6] = { glm::lookAt(glm::vec3(0), glm::vec3( 1, 0, 0), glm::vec3(0, -1, 0)), glm::lookAt(glm::vec3(0), glm::vec3(-1, 0, 0), glm::vec3(0, -1, 0)), @@ -194,7 +208,10 @@ namespace Donut::RHI for (uint32_t i = 0; i < 6; ++i) { create_buffer_raw(128, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, ubo[i], ubo_mem[i]); - glm::mat4 mats[2] = { glm::transpose(proj), glm::transpose(views[i]) }; + // straight glm, no transpose: the SPIR-V marks these RowMajor and does + // v * M, which reads glm's column-major memory as-is (same as every + // other UBO here). only GL's flattened loose uniforms need the transpose. + glm::mat4 mats[2] = { proj, views[i] }; vkMapMemory(m_device, ubo_mem[i], 0, 128, 0, &mp); std::memcpy(mp, mats, 128); vkUnmapMemory(m_device, ubo_mem[i]); VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; dsai.descriptorPool = pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &set_layout; vkAllocateDescriptorSets(m_device, &dsai, &sets[i]); diff --git a/src/platform/vulkan/vulkan_device.cpp b/src/platform/vulkan/vulkan_device.cpp index f780f6f..54a4a3c 100644 --- a/src/platform/vulkan/vulkan_device.cpp +++ b/src/platform/vulkan/vulkan_device.cpp @@ -1,5 +1,15 @@ #include "vulkan_common.h" +#include +#include + +#if defined(_WIN32) + #ifndef NOMINMAX + #define NOMINMAX + #endif + #include // LoadLibraryExW, for the runtime probe +#endif + #include #include #include @@ -44,7 +54,16 @@ namespace Donut::RHI auto VulkanDevice::create_shader_module(const std::string& path, VkShaderModule& out) const -> bool { auto spv = load_spirv(path); - if (spv.empty()) { DONUT_ERROR("Vulkan RHI: failed to load SPIR-V {}", path); return false; } + if (spv.empty()) + { + // u8string, not string(): msvc's narrow conversion throws on a cwd the + // ANSI code page can't spell, which would turn this log into a crash + std::error_code ec; + const auto cwd = std::filesystem::current_path(ec).u8string(); + DONUT_ERROR("Vulkan RHI: failed to load SPIR-V {} (cwd {}; shaders live in assets/shaders/generated, see tools/compile-shaders.sh)", + path, std::string(cwd.begin(), cwd.end())); + return false; + } VkShaderModuleCreateInfo ci{ VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO }; ci.codeSize = spv.size() * 4; ci.pCode = spv.data(); return vkCreateShaderModule(m_device, &ci, nullptr, &out) == VK_SUCCESS; @@ -190,8 +209,15 @@ namespace Donut::RHI vkQueueSubmit(m_graphics_queue, 1, &si, VK_NULL_HANDLE); vkQueueWaitIdle(m_graphics_queue); void* mp = nullptr; vkMapMemory(m_device, mem, 0, sz, 0, &mp); - std::memcpy(out.data(), mp, sz); // RGBA8_UNORM, top-down (top-left origin) + std::memcpy(out.data(), mp, sz); vkUnmapMemory(m_device, mem); + // off-screen targets hold the same rows as on GL: clip-space bottom lands on + // row 0 in both APIs, and the present pass flips it upright. so flip here + // too, exactly like the GL readback, or the export comes out upside down. + const size_t row = (size_t)w * 4; + for (uint32_t y = 0; y < h / 2; ++y) + std::swap_ranges(out.begin() + (std::ptrdiff_t)(y * row), out.begin() + (std::ptrdiff_t)((y + 1) * row), + out.begin() + (std::ptrdiff_t)((h - 1 - y) * row)); vkFreeCommandBuffers(m_device, m_command_pool, 1, &cmd); vkDestroyBuffer(m_device, buf, nullptr); vkFreeMemory(m_device, mem, nullptr); } @@ -227,8 +253,12 @@ namespace Donut::RHI vkQueueSubmit(m_graphics_queue, 1, &si, VK_NULL_HANDLE); vkQueueWaitIdle(m_graphics_queue); void* mp = nullptr; vkMapMemory(m_device, mem, 0, sz, 0, &mp); - std::memcpy(out.data(), mp, sz); // R32G32B32A32_SFLOAT, top-down + std::memcpy(out.data(), mp, sz); // R32G32B32A32_SFLOAT vkUnmapMemory(m_device, mem); + const size_t row = (size_t)w * 4; // same flip as the RGBA8 readback above + for (uint32_t y = 0; y < h / 2; ++y) + std::swap_ranges(out.begin() + (std::ptrdiff_t)(y * row), out.begin() + (std::ptrdiff_t)((y + 1) * row), + out.begin() + (std::ptrdiff_t)((h - 1 - y) * row)); vkFreeCommandBuffers(m_device, m_command_pool, 1, &cmd); vkDestroyBuffer(m_device, buf, nullptr); vkFreeMemory(m_device, mem, nullptr); } @@ -299,6 +329,16 @@ namespace Donut::RHI auto create_vulkan_device() -> Scope { return create_scope(); } + auto vulkan_runtime_available() -> bool + { +#if defined(_WIN32) + // stays loaded on purpose: the delay-load thunks bind to this module later + return LoadLibraryExW(L"vulkan-1.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32) != nullptr; +#else + return true; // macOS links the loader directly, and there's no Windows-style delay-load +#endif + } + auto vulkan_prepare_glfw() -> void { #ifdef __APPLE__ diff --git a/src/platform/vulkan/vulkan_device.h b/src/platform/vulkan/vulkan_device.h index 85cc5ca..31f5842 100644 --- a/src/platform/vulkan/vulkan_device.h +++ b/src/platform/vulkan/vulkan_device.h @@ -14,4 +14,8 @@ namespace Donut::RHI // the loader the app links against (its own dlopen fails on macOS/Homebrew) // and configures the MoltenVK ICD / layer paths. auto vulkan_prepare_glfw() -> void; + + // whether a Vulkan loader exists on this machine at all. on windows vulkan-1.dll + // is delay-loaded, so this has to say yes before any vk* call; true elsewhere. + auto vulkan_runtime_available() -> bool; } diff --git a/src/platform/vulkan/vulkan_resources.cpp b/src/platform/vulkan/vulkan_resources.cpp index ee2091b..05cb1be 100644 --- a/src/platform/vulkan/vulkan_resources.cpp +++ b/src/platform/vulkan/vulkan_resources.cpp @@ -77,7 +77,10 @@ namespace Donut::RHI VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; ici.imageType = VK_IMAGE_TYPE_2D; ici.format = cfmt; ici.extent = { (uint32_t)w, (uint32_t)h, 1 }; ici.mipLevels = 1; ici.arrayLayers = 1; ici.samples = VK_SAMPLE_COUNT_1_BIT; - ici.tiling = VK_IMAGE_TILING_OPTIMAL; ici.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + // TRANSFER_SRC because export reads targets back with a copy. MoltenVK never + // minded it missing; a desktop driver is within its rights to hand back garbage. + ici.tiling = VK_IMAGE_TILING_OPTIMAL; + ici.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; vkCreateImage(m_device, &ici, nullptr, &rt->m_image); VkMemoryRequirements req{}; vkGetImageMemoryRequirements(m_device, rt->m_image, &req); VkMemoryAllocateInfo ai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; diff --git a/src/platform/vulkan/vulkan_swapchain.cpp b/src/platform/vulkan/vulkan_swapchain.cpp index cbd4512..65b510c 100644 --- a/src/platform/vulkan/vulkan_swapchain.cpp +++ b/src/platform/vulkan/vulkan_swapchain.cpp @@ -11,19 +11,44 @@ namespace Donut::RHI const char** glfwExts = glfwGetRequiredInstanceExtensions(&glfwExtCount); if (!glfwExts) { DONUT_ERROR("Vulkan RHI: GLFW reports no surface support"); return false; } std::vector exts(glfwExts, glfwExts + glfwExtCount); - exts.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); - exts.push_back(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME); + // the portability bits are how MoltenVK gets listed at all (it's a + // "portability" driver, not a conformant one). a desktop loader may not + // offer the extension, and asking for one it doesn't have fails the whole + // instance, so only ask when it's on the menu. + uint32_t iec = 0; vkEnumerateInstanceExtensionProperties(nullptr, &iec, nullptr); + std::vector iexts(iec); + vkEnumerateInstanceExtensionProperties(nullptr, &iec, iexts.data()); + auto has_ext = [&](const char* name) + { + for (const auto& e : iexts) if (std::strcmp(e.extensionName, name) == 0) return true; + return false; + }; + VkInstanceCreateFlags flags = 0; +#ifdef VK_KHR_portability_enumeration // older SDK headers don't have it at all + if (has_ext(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME)) + { + exts.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); + flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; + } +#endif + if (has_ext(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME)) + exts.push_back(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME); + + // validation only in debug builds: it's slow, and with the SDK installed on + // windows it would otherwise load for every release run too. std::vector layers; +#ifdef DONUT_DEBUG uint32_t layer_count = 0; vkEnumerateInstanceLayerProperties(&layer_count, nullptr); std::vector avail(layer_count); vkEnumerateInstanceLayerProperties(&layer_count, avail.data()); for (const auto& l : avail) if (std::strcmp(l.layerName, "VK_LAYER_KHRONOS_validation") == 0) layers.push_back("VK_LAYER_KHRONOS_validation"); +#endif VkInstanceCreateInfo ici{ VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO }; - ici.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; + ici.flags = flags; ici.pApplicationInfo = &app; ici.enabledExtensionCount = (uint32_t)exts.size(); ici.ppEnabledExtensionNames = exts.data(); ici.enabledLayerCount = (uint32_t)layers.size(); ici.ppEnabledLayerNames = layers.data(); @@ -31,6 +56,7 @@ namespace Donut::RHI if (r != VK_SUCCESS && !layers.empty()) { DONUT_WARN("Vulkan RHI: validation layer unavailable, continuing without it"); + layers.clear(); ici.enabledLayerCount = 0; ici.ppEnabledLayerNames = nullptr; r = vkCreateInstance(&ici, nullptr, &m_instance); } @@ -46,19 +72,30 @@ namespace Donut::RHI if (count == 0) { DONUT_ERROR("Vulkan RHI: no physical devices"); return false; } std::vector devices(count); vkEnumeratePhysicalDevices(m_instance, &count, devices.data()); - m_physical = devices[0]; - uint32_t q = 0; vkGetPhysicalDeviceQueueFamilyProperties(m_physical, &q, nullptr); - std::vector qfams(q); - vkGetPhysicalDeviceQueueFamilyProperties(m_physical, &q, qfams.data()); - bool fg = false, fp = false; - for (uint32_t i = 0; i < q; ++i) + // pick the best gpu that can both draw and present: a discrete card over an + // integrated one (laptops tend to list the igpu first), never one that can't + // present to our surface. + int best = -1; + for (VkPhysicalDevice pd : devices) { - if (!fg && (qfams[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)) { m_graphics_family = i; fg = true; } - VkBool32 present = VK_FALSE; vkGetPhysicalDeviceSurfaceSupportKHR(m_physical, i, m_surface, &present); - if (!fp && present) { m_present_family = i; fp = true; } + uint32_t q = 0; vkGetPhysicalDeviceQueueFamilyProperties(pd, &q, nullptr); + std::vector qfams(q); + vkGetPhysicalDeviceQueueFamilyProperties(pd, &q, qfams.data()); + uint32_t gfam = UINT32_MAX, pfam = UINT32_MAX; + for (uint32_t i = 0; i < q; ++i) + { + if (gfam == UINT32_MAX && (qfams[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)) gfam = i; + VkBool32 present = VK_FALSE; vkGetPhysicalDeviceSurfaceSupportKHR(pd, i, m_surface, &present); + if (pfam == UINT32_MAX && present) pfam = i; + } + if (gfam == UINT32_MAX || pfam == UINT32_MAX) continue; + VkPhysicalDeviceProperties pp{}; vkGetPhysicalDeviceProperties(pd, &pp); + const int score = pp.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU ? 3 + : pp.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU ? 2 : 1; + if (score > best) { best = score; m_physical = pd; m_graphics_family = gfam; m_present_family = pfam; } } - if (!fg || !fp) { DONUT_ERROR("Vulkan RHI: no graphics/present queue"); return false; } + if (best < 0) { DONUT_ERROR("Vulkan RHI: no gpu with both a graphics and a present queue"); return false; } std::vector dev_exts = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; uint32_t dec = 0; vkEnumerateDeviceExtensionProperties(m_physical, nullptr, &dec, nullptr); @@ -295,14 +332,26 @@ namespace Donut::RHI auto VulkanDevice::recreate_swapchain() -> bool { + // minimized (0x0 on windows): wait it out, but not through a close request, + // or closing a minimized window from the taskbar hangs the process int w = 0, h = 0; glfwGetFramebufferSize(m_window, &w, &h); - while (w == 0 || h == 0) { glfwGetFramebufferSize(m_window, &w, &h); glfwWaitEvents(); } + while ((w == 0 || h == 0) && !glfwWindowShouldClose(m_window)) { glfwWaitEvents(); glfwGetFramebufferSize(m_window, &w, &h); } + if (w == 0 || h == 0) return false; // closing; keep the old swapchain until shutdown m_width = w; m_height = h; vkDeviceWaitIdle(m_device); cleanup_swapchain(); if (!create_swapchain()) return false; if (!create_image_views()) return false; if (!create_depth_and_framebuffers())return false; + // a new swapchain can come back with a different image count, and the + // present semaphores are per image + if (m_render_finished.size() != m_images.size()) + { + for (auto s : m_render_finished) vkDestroySemaphore(m_device, s, nullptr); + m_render_finished.assign(m_images.size(), VK_NULL_HANDLE); + VkSemaphoreCreateInfo sci{ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO }; + for (auto& s : m_render_finished) VKD_CHECK(vkCreateSemaphore(m_device, &sci, nullptr, &s)); + } m_images_in_flight.assign(m_images.size(), VK_NULL_HANDLE); return true; } -- cgit v1.3