// A complete presentation checkpoint, not a complete Signal Garden engine. #define GLFW_INCLUDE_VULKAN #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { void check(VkResult result, std::string_view operation) { if (result != VK_SUCCESS) throw std::runtime_error(std::string(operation) + " failed: VkResult=" + std::to_string(result)); } template std::vector enumerate(Query query) { for (int attempt = 0; attempt < 8; ++attempt) { std::uint32_t count{}; check(query(&count, static_cast(nullptr)), "enumeration count"); if (count > 65536) throw std::runtime_error("unreasonable enumeration count"); std::vector values(count); if (!count) return values; const auto result = query(&count, values.data()); if (result == VK_INCOMPLETE) continue; check(result, "enumeration values"); values.resize(count); return values; } throw std::runtime_error("enumeration did not stabilize"); } bool has(const std::vector& extensions, const char* name) { return std::ranges::any_of(extensions, [name](const auto& extension) { return std::string_view(extension.extensionName) == name; }); } // Each child handle borrows its parent; member declaration order keeps the // parent alive. Creating directly into an empty owner is exception-safe. template class Owner { public: Owner() = default; explicit Owner(Parent parent) noexcept : parent_(parent) {} ~Owner() { reset(); } Owner(const Owner&) = delete; Owner& operator=(const Owner&) = delete; Owner(Owner&& other) noexcept : parent_(other.parent_), value_(std::exchange(other.value_, {})) {} Owner& operator=(Owner&& other) noexcept { if (this != &other) { reset(); parent_ = other.parent_; value_ = std::exchange(other.value_, {}); } return *this; } [[nodiscard]] Handle get() const noexcept { return value_; } Handle* put() { if (value_) throw std::logic_error("creating into a nonempty handle owner"); return &value_; } void reset() noexcept { if (value_) Destroy(parent_, std::exchange(value_, {}), nullptr); } private: Parent parent_{}; Handle value_{}; }; using Surface = Owner; using Swapchain = Owner; using ImageView = Owner; using Semaphore = Owner; using Fence = Owner; using CommandPool = Owner; struct Instance { VkInstance value{}; Instance() = default; Instance(const Instance&) = delete; Instance& operator=(const Instance&) = delete; ~Instance() { if (value) vkDestroyInstance(value, nullptr); } }; struct Device { VkDevice value{}; Device() = default; Device(const Device&) = delete; Device& operator=(const Device&) = delete; ~Device() { if (value) vkDestroyDevice(value, nullptr); } }; struct Debug { VkInstance instance{}; VkDebugUtilsMessengerEXT value{}; PFN_vkDestroyDebugUtilsMessengerEXT destroy{}; std::atomic_uint errors{}; ~Debug() { if (value) destroy(instance, value, nullptr); } static VKAPI_ATTR VkBool32 VKAPI_CALL callback(VkDebugUtilsMessageSeverityFlagBitsEXT severity, VkDebugUtilsMessageTypeFlagsEXT, const VkDebugUtilsMessengerCallbackDataEXT* data, void* user) noexcept { if (severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) static_cast(user)->errors.fetch_add(1, std::memory_order_relaxed); // C stdio cannot propagate a C++ exception across this API callback. std::fprintf(stderr, "validation: %s\n", data && data->pMessage ? data->pMessage : "no message"); return VK_FALSE; } }; struct Frame { CommandPool pool; VkCommandBuffer commands{}; // Borrowed from pool; destruction frees it. Semaphore acquired; Fence submitted; explicit Frame(VkDevice device) : pool(device), acquired(device), submitted(device) {} }; struct Image { VkImage value{}; // Borrowed from swapchain; never vkDestroyImage. ImageView view; Semaphore rendered; // One per swapchain image, not one per CPU frame. Fence presented; bool presentation_pending{}; Image(VkDevice device, VkImage image) : value(image), view(device), rendered(device), presented(device) {} }; struct Options { std::optional frames; bool resize{}; bool validation{}; bool sync_validation{}; }; Options parse(int argc, const char* const* argv) { Options options; for (int i = 1; i < argc; ++i) { const std::string_view arg{argv[i]}; if (arg == "--frames" && i + 1 < argc) { const std::string_view text{argv[++i]}; std::size_t count{}; const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), count); if (error != std::errc{} || end != text.data() + text.size() || count < 1 || count > 10000) throw std::runtime_error("--frames requires an integer from 1 to 10000"); options.frames = count; } else if (arg == "--resize") options.resize = true; else if (arg == "--validation") options.validation = true; else if (arg == "--sync-validation") { options.validation = true; options.sync_validation = true; } else throw std::runtime_error("usage: signal-window [--frames N] [--resize] [--validation|--sync-validation]"); } return options; } class Renderer { public: Renderer() = default; Renderer(const Renderer&) = delete; Renderer& operator=(const Renderer&) = delete; ~Renderer() { if (window_) { glfwSetFramebufferSizeCallback(window_, nullptr); glfwSetWindowUserPointer(window_, nullptr); } // initialize() may throw halfway through; every nonempty member still owns // its resource. Drain before member destruction, including during unwind. if (device_.value) { try { drain(); } catch (const std::exception& error) { std::cerr << "shutdown: " << error.what() << '\n'; // Device loss permits child destruction. Other wait failures do // not prove completion; fail fast instead of freeing live work. if (!device_lost_) std::terminate(); } } } void initialize(GLFWwindow* window, const Options& options) { window_ = window; create_instance(options); surface_ = Surface(instance_.value); check(glfwCreateWindowSurface(instance_.value, window, nullptr, surface_.put()), "glfwCreateWindowSurface"); choose_device(); create_device(); frames_.reserve(2); for (int i = 0; i < 2; ++i) { frames_.emplace_back(device_.value); auto& frame = frames_.back(); VkCommandPoolCreateInfo pool{}; pool.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; pool.queueFamilyIndex = family_; check(vkCreateCommandPool(device_.value, &pool, nullptr, frame.pool.put()), "vkCreateCommandPool"); VkCommandBufferAllocateInfo commands{}; commands.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; commands.commandPool = frame.pool.get(); commands.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; commands.commandBufferCount = 1; check(vkAllocateCommandBuffers(device_.value, &commands, &frame.commands), "vkAllocateCommandBuffers"); create_semaphore(frame.acquired); create_fence(frame.submitted, VK_FENCE_CREATE_SIGNALED_BIT); } } bool draw() { int width{}, height{}; glfwGetFramebufferSize(window_, &width, &height); if (width <= 0 || height <= 0) return false; // Minimize: no acquire and no fence reset. if ((rebuild_ || !swapchain_.get()) && !recreate(width, height)) return false; // Surface reports zero extent even with nonzero window size on some platforms. if (!swapchain_.get()) return false; auto& frame = frames_[submitted_ % frames_.size()]; const auto frame_fence = frame.submitted.get(); check_completion(vkWaitForFences(device_.value, 1, &frame_fence, VK_TRUE, UINT64_MAX), "frame completion"); std::uint32_t image_index{}; // Finite timeout keeps the event pump responsive when no image is ready. const auto acquired = vkAcquireNextImageKHR(device_.value, swapchain_.get(), 100'000'000, frame.acquired.get(), VK_NULL_HANDLE, &image_index); if (acquired == VK_TIMEOUT || acquired == VK_NOT_READY) return false; if (acquired == VK_ERROR_OUT_OF_DATE_KHR) { rebuild_ = true; return false; } if (acquired != VK_SUCCESS && acquired != VK_SUBOPTIMAL_KHR) check(acquired, "vkAcquireNextImageKHR"); auto& image = images_.at(image_index); wait_presentation(image); check(vkResetCommandPool(device_.value, frame.pool.get(), 0), "vkResetCommandPool"); record(frame.commands, image); VkSemaphoreSubmitInfo wait{}; wait.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; wait.semaphore = frame.acquired.get(); wait.stageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT; VkSemaphoreSubmitInfo signal{}; signal.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; signal.semaphore = image.rendered.get(); signal.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; VkCommandBufferSubmitInfo commands{}; commands.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; commands.commandBuffer = frame.commands; VkSubmitInfo2 submit{}; submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; submit.waitSemaphoreInfoCount = 1; submit.pWaitSemaphoreInfos = &wait; submit.commandBufferInfoCount = 1; submit.pCommandBufferInfos = &commands; submit.signalSemaphoreInfoCount = 1; submit.pSignalSemaphoreInfos = &signal; // Reset only after successful acquire/record, immediately before submit. // A submission failure exits; cleanup never waits this unsignaled frame fence. check(vkResetFences(device_.value, 1, &frame_fence), "vkResetFences(frame)"); check(vkQueueSubmit2(queue_, 1, &submit, frame_fence), "vkQueueSubmit2"); ++submitted_; const auto present_fence = image.presented.get(); check(vkResetFences(device_.value, 1, &present_fence), "vkResetFences(presentation)"); VkSwapchainPresentFenceInfoEXT completion{}; completion.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_PRESENT_FENCE_INFO_EXT; completion.swapchainCount = 1; completion.pFences = &present_fence; const auto rendered = image.rendered.get(); const auto swapchain = swapchain_.get(); VkPresentInfoKHR present{}; present.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; present.pNext = &completion; present.waitSemaphoreCount = 1; present.pWaitSemaphores = &rendered; present.swapchainCount = 1; present.pSwapchains = &swapchain; present.pImageIndices = &image_index; const auto result = vkQueuePresentKHR(queue_, &present); // These rejected presentations still enqueue their semaphore waits. OOM // does not enqueue; device loss is terminal and waits report that loss. image.presentation_pending = result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR || result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_ERROR_SURFACE_LOST_KHR; if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || acquired == VK_SUBOPTIMAL_KHR) rebuild_ = true; if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR && result != VK_ERROR_OUT_OF_DATE_KHR) check(result, "vkQueuePresentKHR"); return true; } void resized() noexcept { rebuild_ = true; } [[nodiscard]] std::size_t submitted() const noexcept { return submitted_; } void finish() { drain(); // Normal shutdown checks validation after destroying device/surface // resources, while the debug messenger and its counter are still alive. // Exceptional shutdown continues to use the member RAII owners. images_.clear(); swapchain_.reset(); frames_.clear(); if (device_.value) vkDestroyDevice(std::exchange(device_.value, VK_NULL_HANDLE), nullptr); surface_.reset(); if (debug_.errors.load(std::memory_order_relaxed) != 0) throw std::runtime_error("validation reported errors; inspect the messages above"); std::cout << "submitted=" << submitted_ << " swapchain-generations=" << generations_ << " shutdown=GPU-and-presentation-drained\n"; } private: Instance instance_; Debug debug_; Surface surface_; Device device_; std::vector frames_; Swapchain swapchain_; // Declared before images so image views die first. std::vector images_; GLFWwindow* window_{}; // Borrowed; main destroys window after this renderer. VkPhysicalDevice physical_{}; VkQueue queue_{}; std::uint32_t family_{}; VkExtent2D extent_{}; VkFormat format_{}; bool ext_surface_{}; bool khr_surface_{}; bool portability_{}; const char* maintenance_{}; bool rebuild_{true}; bool device_lost_{}; std::size_t submitted_{}; std::size_t generations_{}; void create_instance(const Options& options) { std::uint32_t api = VK_API_VERSION_1_0; const auto version = reinterpret_cast(vkGetInstanceProcAddr(nullptr, "vkEnumerateInstanceVersion")); if (version) check(version(&api), "vkEnumerateInstanceVersion"); if (api < VK_API_VERSION_1_3) throw std::runtime_error("Vulkan loader API 1.3 is required"); const auto available = enumerate([](auto* count, auto* data) { return vkEnumerateInstanceExtensionProperties(nullptr, count, data); }); std::uint32_t required_count{}; const auto* required = glfwGetRequiredInstanceExtensions(&required_count); if (!required) throw std::runtime_error("GLFW found no Vulkan window surface extensions"); std::vector extensions(required, required + required_count); const auto add = [&](const char* name) { if (!has(available, name)) throw std::runtime_error(std::string("missing instance extension: ") + name); if (std::ranges::none_of(extensions, [name](const char* other) { return std::string_view(other) == name; })) extensions.push_back(name); }; add(VK_KHR_GET_SURFACE_CAPABILITIES_2_EXTENSION_NAME); // KHR promotion aliases the EXT feature/fence structures used below. khr_surface_ = has(available, "VK_KHR_surface_maintenance1"); ext_surface_ = has(available, VK_EXT_SURFACE_MAINTENANCE_1_EXTENSION_NAME); if (khr_surface_) add("VK_KHR_surface_maintenance1"); if (ext_surface_) add(VK_EXT_SURFACE_MAINTENANCE_1_EXTENSION_NAME); if (!khr_surface_ && !ext_surface_) throw std::runtime_error("surface maintenance1 is required for this checkpoint's presentation-fence lifetime policy"); const bool enumerate_portability = has(available, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); if (enumerate_portability) add(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); constexpr const char* layer = "VK_LAYER_KHRONOS_validation"; VkValidationFeatureEnableEXT sync_feature = VK_VALIDATION_FEATURE_ENABLE_SYNCHRONIZATION_VALIDATION_EXT; VkValidationFeaturesEXT validation{}; validation.sType = VK_STRUCTURE_TYPE_VALIDATION_FEATURES_EXT; validation.enabledValidationFeatureCount = 1; validation.pEnabledValidationFeatures = &sync_feature; if (options.validation) { const auto layers = enumerate([](auto* count, auto* data) { return vkEnumerateInstanceLayerProperties(count, data); }); if (std::ranges::none_of(layers, [](const auto& item) { return std::string_view(item.layerName) == layer; })) throw std::runtime_error("requested VK_LAYER_KHRONOS_validation is not installed/discoverable"); add(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); if (options.sync_validation) { const auto layer_extensions = enumerate([&](auto* count, auto* data) { return vkEnumerateInstanceExtensionProperties(layer, count, data); }); if (!has(available, VK_EXT_VALIDATION_FEATURES_EXTENSION_NAME) && !has(layer_extensions, VK_EXT_VALIDATION_FEATURES_EXTENSION_NAME)) throw std::runtime_error("synchronization validation requires VK_EXT_validation_features"); extensions.push_back(VK_EXT_VALIDATION_FEATURES_EXTENSION_NAME); } } VkApplicationInfo application{}; application.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; application.pApplicationName = "Parallax Signal Garden checkpoint"; application.apiVersion = VK_API_VERSION_1_3; VkInstanceCreateInfo create{}; create.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; create.pApplicationInfo = &application; create.enabledExtensionCount = static_cast(extensions.size()); create.ppEnabledExtensionNames = extensions.data(); if (enumerate_portability) create.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; if (options.validation) { create.enabledLayerCount = 1; create.ppEnabledLayerNames = &layer; } if (options.sync_validation) create.pNext = &validation; check(vkCreateInstance(&create, nullptr, &instance_.value), "vkCreateInstance"); if (options.validation) { debug_.instance = instance_.value; const auto make = reinterpret_cast(vkGetInstanceProcAddr(instance_.value, "vkCreateDebugUtilsMessengerEXT")); debug_.destroy = reinterpret_cast(vkGetInstanceProcAddr(instance_.value, "vkDestroyDebugUtilsMessengerEXT")); if (!make || !debug_.destroy) throw std::runtime_error("debug-utils entry points are unavailable"); VkDebugUtilsMessengerCreateInfoEXT info{}; info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; info.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; info.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; info.pfnUserCallback = Debug::callback; info.pUserData = &debug_; check(make(instance_.value, &info, nullptr, &debug_.value), "vkCreateDebugUtilsMessengerEXT"); } std::cout << "validation=" << (options.sync_validation ? "core+sync" : options.validation ? "core" : "off (not checked)") << '\n'; } void choose_device() { const auto devices = enumerate([&](auto* count, auto* data) { return vkEnumeratePhysicalDevices(instance_.value, count, data); }); for (auto candidate : devices) { VkPhysicalDeviceProperties properties{}; vkGetPhysicalDeviceProperties(candidate, &properties); const auto extensions = enumerate([&](auto* count, auto* data) { return vkEnumerateDeviceExtensionProperties(candidate, nullptr, count, data); }); const char* maintenance = khr_surface_ && has(extensions, "VK_KHR_swapchain_maintenance1") ? "VK_KHR_swapchain_maintenance1" : ext_surface_ && has(extensions, VK_EXT_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME) ? VK_EXT_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME : nullptr; if (properties.apiVersion < VK_API_VERSION_1_3 || !has(extensions, VK_KHR_SWAPCHAIN_EXTENSION_NAME) || !maintenance) { std::cout << "rejected " << properties.deviceName << ": need API 1.3, swapchain, and maintenance1\n"; continue; } VkPhysicalDeviceSwapchainMaintenance1FeaturesEXT maintenance_features{}; maintenance_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_EXT; VkPhysicalDeviceVulkan13Features features13{}; features13.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES; features13.pNext = &maintenance_features; VkPhysicalDeviceFeatures2 features{}; features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; features.pNext = &features13; vkGetPhysicalDeviceFeatures2(candidate, &features); if (!features13.dynamicRendering || !features13.synchronization2 || !maintenance_features.swapchainMaintenance1) { std::cout << "rejected " << properties.deviceName << ": missing required rendering/sync/presentation feature\n"; continue; } std::uint32_t count{}; vkGetPhysicalDeviceQueueFamilyProperties(candidate, &count, nullptr); std::vector families(count); vkGetPhysicalDeviceQueueFamilyProperties(candidate, &count, families.data()); for (std::uint32_t i = 0; i < count; ++i) { VkBool32 present{}; check(vkGetPhysicalDeviceSurfaceSupportKHR(candidate, i, surface_.get(), &present), "surface support"); if (families[i].queueCount && (families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) && present) { physical_ = candidate; family_ = i; maintenance_ = maintenance; portability_ = has(extensions, "VK_KHR_portability_subset"); std::cout << "selected=" << properties.deviceName << " queue-family=" << family_ << " completion=" << maintenance_ << '\n'; return; } } std::cout << "rejected " << properties.deviceName << ": this checkpoint requires one graphics+present family\n"; } throw std::runtime_error("no suitable device; read the rejected requirements above"); } void create_device() { const float priority = 1.0f; VkDeviceQueueCreateInfo queue{}; queue.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; queue.queueFamilyIndex = family_; queue.queueCount = 1; queue.pQueuePriorities = &priority; VkPhysicalDeviceSwapchainMaintenance1FeaturesEXT maintenance{}; maintenance.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_EXT; maintenance.swapchainMaintenance1 = VK_TRUE; VkPhysicalDeviceVulkan13Features features{}; features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES; features.pNext = &maintenance; features.dynamicRendering = VK_TRUE; features.synchronization2 = VK_TRUE; std::vector extensions{VK_KHR_SWAPCHAIN_EXTENSION_NAME, maintenance_}; if (portability_) extensions.push_back("VK_KHR_portability_subset"); VkDeviceCreateInfo create{}; create.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; create.pNext = &features; create.queueCreateInfoCount = 1; create.pQueueCreateInfos = &queue; create.enabledExtensionCount = static_cast(extensions.size()); create.ppEnabledExtensionNames = extensions.data(); check(vkCreateDevice(physical_, &create, nullptr, &device_.value), "vkCreateDevice"); vkGetDeviceQueue(device_.value, family_, 0, &queue_); } void create_semaphore(Semaphore& owner) { VkSemaphoreCreateInfo create{}; create.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; check(vkCreateSemaphore(device_.value, &create, nullptr, owner.put()), "vkCreateSemaphore"); } void create_fence(Fence& owner, VkFenceCreateFlags flags = 0) { VkFenceCreateInfo create{}; create.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; create.flags = flags; check(vkCreateFence(device_.value, &create, nullptr, owner.put()), "vkCreateFence"); } void check_completion(VkResult result, std::string_view operation) { if (result == VK_ERROR_DEVICE_LOST) device_lost_ = true; check(result, operation); } void wait_presentation(Image& image) { if (!image.presentation_pending) return; const auto fence = image.presented.get(); check_completion(vkWaitForFences(device_.value, 1, &fence, VK_TRUE, UINT64_MAX), "presentation resource completion"); image.presentation_pending = false; } void drain() { if (!device_.value) return; // Device idle covers command work, but is NOT the presentation completion // proof. The maintenance1 fences supply that distinct proof. check_completion(vkDeviceWaitIdle(device_.value), "vkDeviceWaitIdle"); for (auto& image : images_) wait_presentation(image); } bool recreate(int width, int height) { VkSurfaceCapabilitiesKHR capabilities{}; check(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical_, surface_.get(), &capabilities), "surface capabilities"); auto size = capabilities.currentExtent; if (size.width == std::numeric_limits::max()) { size.width = std::clamp(static_cast(width), capabilities.minImageExtent.width, capabilities.maxImageExtent.width); size.height = std::clamp(static_cast(height), capabilities.minImageExtent.height, capabilities.maxImageExtent.height); } if (size.width == 0 || size.height == 0) return false; const auto formats = enumerate([&](auto* count, auto* data) { return vkGetPhysicalDeviceSurfaceFormatsKHR(physical_, surface_.get(), count, data); }); if (formats.empty()) throw std::runtime_error("surface reports no formats"); auto format = formats.front(); for (const auto& candidate : formats) if (candidate.format == VK_FORMAT_B8G8R8A8_SRGB && candidate.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) format = candidate; if (format.format == VK_FORMAT_UNDEFINED) format.format = VK_FORMAT_B8G8R8A8_SRGB; if (!(capabilities.supportedUsageFlags & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT)) throw std::runtime_error("surface images cannot be color attachments"); VkCompositeAlphaFlagBitsKHR alpha{}; for (const auto candidate : {VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR, VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR, VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR}) if (!alpha && (capabilities.supportedCompositeAlpha & candidate)) alpha = candidate; if (!alpha) throw std::runtime_error("surface reports no composite alpha mode"); auto count = capabilities.minImageCount; if (count < std::numeric_limits::max()) ++count; if (capabilities.maxImageCount) count = std::min(count, capabilities.maxImageCount); drain(); images_.clear(); swapchain_.reset(); // A deliberately blocking resize policy: release the old generation before // creating the next. No oldSwapchain retirement or deferred-release list. swapchain_ = Swapchain(device_.value); VkSwapchainCreateInfoKHR create{}; create.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; create.surface = surface_.get(); create.minImageCount = count; create.imageFormat = format.format; create.imageColorSpace = format.colorSpace; create.imageExtent = size; create.imageArrayLayers = 1; create.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; create.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; create.preTransform = capabilities.currentTransform; create.compositeAlpha = alpha; create.presentMode = VK_PRESENT_MODE_FIFO_KHR; create.clipped = VK_TRUE; check(vkCreateSwapchainKHR(device_.value, &create, nullptr, swapchain_.put()), "vkCreateSwapchainKHR"); const auto borrowed = enumerate([&](auto* image_count, auto* data) { return vkGetSwapchainImagesKHR(device_.value, swapchain_.get(), image_count, data); }); images_.reserve(borrowed.size()); for (const auto image : borrowed) { images_.emplace_back(device_.value, image); auto& owned = images_.back(); VkImageViewCreateInfo view{}; view.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; view.image = image; view.viewType = VK_IMAGE_VIEW_TYPE_2D; view.format = format.format; view.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; check(vkCreateImageView(device_.value, &view, nullptr, owned.view.put()), "vkCreateImageView"); create_semaphore(owned.rendered); create_fence(owned.presented); } extent_ = size; format_ = format.format; rebuild_ = false; ++generations_; std::cout << "swapchain " << generations_ << ": " << size.width << 'x' << size.height << " images=" << images_.size() << " format=" << format_ << " present=FIFO\n"; return true; } void record(VkCommandBuffer commands, const Image& image) const { VkCommandBufferBeginInfo begin{}; begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; check(vkBeginCommandBuffer(commands, &begin), "vkBeginCommandBuffer"); VkImageMemoryBarrier2 barrier{}; barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2; // Chain the acquire wait to this transition at the same stage. barrier.srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT; barrier.dstStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT; barrier.dstAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT; barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; // Discard previous clear; do not read it. barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.image = image.value; barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; VkDependencyInfo dependency{}; dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.imageMemoryBarrierCount = 1; dependency.pImageMemoryBarriers = &barrier; vkCmdPipelineBarrier2(commands, &dependency); VkRenderingAttachmentInfo color{}; color.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; color.imageView = image.view.get(); color.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE; color.clearValue.color = {{0.08f, 0.22f, 0.32f, 1.0f}}; VkRenderingInfo rendering{}; rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; rendering.renderArea.extent = extent_; rendering.layerCount = 1; rendering.colorAttachmentCount = 1; rendering.pColorAttachments = &color; vkCmdBeginRendering(commands, &rendering); vkCmdEndRendering(commands); barrier.srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT; barrier.srcAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT; barrier.dstStageMask = VK_PIPELINE_STAGE_2_NONE; barrier.dstAccessMask = VK_ACCESS_2_NONE; barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; vkCmdPipelineBarrier2(commands, &dependency); check(vkEndCommandBuffer(commands), "vkEndCommandBuffer"); } }; struct Glfw { Glfw() { glfwSetErrorCallback([](int error, const char* message) noexcept { std::fprintf(stderr, "GLFW %d: %s\n", error, message ? message : "no message"); }); // Keep GLFW's surface creation on the same loader as our linked Vulkan. glfwInitVulkanLoader(vkGetInstanceProcAddr); if (!glfwInit()) throw std::runtime_error("glfwInit failed; a graphical session is required"); } ~Glfw() { glfwTerminate(); } Glfw(const Glfw&) = delete; Glfw& operator=(const Glfw&) = delete; }; } int main(int argc, const char* argv[]) { try { const auto options = parse(argc, argv); const Glfw glfw; glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); std::unique_ptr window( glfwCreateWindow(800, 500, "Parallax · Signal Garden · clear-color checkpoint", nullptr, nullptr), glfwDestroyWindow); if (!window) throw std::runtime_error("window creation failed"); Renderer renderer; renderer.initialize(window.get(), options); glfwSetWindowUserPointer(window.get(), &renderer); glfwSetFramebufferSizeCallback(window.get(), [](GLFWwindow* resized, int, int) { static_cast(glfwGetWindowUserPointer(resized))->resized(); }); const auto start = std::chrono::steady_clock::now(); bool resized{}; while (!glfwWindowShouldClose(window.get())) { glfwPollEvents(); if (options.frames && renderer.submitted() >= *options.frames) break; if (options.resize && !resized && renderer.submitted() >= 2) { glfwSetWindowSize(window.get(), 960, 540); renderer.resized(); resized = true; } if (!renderer.draw()) glfwWaitEventsTimeout(0.01); if (options.frames && std::chrono::steady_clock::now() - start > std::chrono::seconds(30)) throw std::runtime_error("smoke run timed out without requested frames"); } glfwSetFramebufferSizeCallback(window.get(), nullptr); renderer.finish(); if (options.frames && renderer.submitted() != *options.frames) throw std::runtime_error("window closed before requested frame count"); return 0; } catch (const std::exception& error) { std::cerr << "signal-window: " << error.what() << '\n'; return 1; } }