Theme Catppuccin
Make yourself at homeCatppuccin

A softer light for long learning sessions.

Appearance

Loading your preference…

Dark palette

Choose a dark palette to try it.

Light uses Latte. Your choices stay in this browser.

Back to your source shelf

Inside the companion

Vulkan · Signal Garden

Query a device and study a clear-color window with explicit frame and presentation lifetimes.

Project files 6

Clear, present, and retire a frame

A complete clear-color window with frame slots, swapchain recreation, and tracked presentation.

634 lines clear_window.cpp
// A complete presentation checkpoint, not a complete Signal Garden engine.
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>

#include <algorithm>
#include <array>
#include <atomic>
#include <charconv>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <exception>
#include <iostream>
#include <limits>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

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<class T, class Query>
std::vector<T> enumerate(Query query) {
    for (int attempt = 0; attempt < 8; ++attempt) {
        std::uint32_t count{};
        check(query(&count, static_cast<T*>(nullptr)), "enumeration count");
        if (count > 65536) throw std::runtime_error("unreasonable enumeration count");
        std::vector<T> 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<VkExtensionProperties>& 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 Handle, class Parent, auto Destroy>
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<VkSurfaceKHR, VkInstance, vkDestroySurfaceKHR>;
using Swapchain = Owner<VkSwapchainKHR, VkDevice, vkDestroySwapchainKHR>;
using ImageView = Owner<VkImageView, VkDevice, vkDestroyImageView>;
using Semaphore = Owner<VkSemaphore, VkDevice, vkDestroySemaphore>;
using Fence = Owner<VkFence, VkDevice, vkDestroyFence>;
using CommandPool = Owner<VkCommandPool, VkDevice, vkDestroyCommandPool>;

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<Debug*>(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<std::size_t> 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<Frame> frames_;
    Swapchain swapchain_; // Declared before images so image views die first.
    std::vector<Image> 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<PFN_vkEnumerateInstanceVersion>(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<VkExtensionProperties>([](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<const char*> 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<VkLayerProperties>([](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<VkExtensionProperties>([&](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<std::uint32_t>(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<PFN_vkCreateDebugUtilsMessengerEXT>(vkGetInstanceProcAddr(instance_.value, "vkCreateDebugUtilsMessengerEXT"));
            debug_.destroy = reinterpret_cast<PFN_vkDestroyDebugUtilsMessengerEXT>(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<VkPhysicalDevice>([&](auto* count, auto* data) {
            return vkEnumeratePhysicalDevices(instance_.value, count, data);
        });
        for (auto candidate : devices) {
            VkPhysicalDeviceProperties properties{};
            vkGetPhysicalDeviceProperties(candidate, &properties);
            const auto extensions = enumerate<VkExtensionProperties>([&](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<VkQueueFamilyProperties> 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<const char*> 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<std::uint32_t>(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<std::uint32_t>::max()) {
            size.width = std::clamp(static_cast<std::uint32_t>(width), capabilities.minImageExtent.width, capabilities.maxImageExtent.width);
            size.height = std::clamp(static_cast<std::uint32_t>(height), capabilities.minImageExtent.height, capabilities.maxImageExtent.height);
        }
        if (size.width == 0 || size.height == 0) return false;
        const auto formats = enumerate<VkSurfaceFormatKHR>([&](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<std::uint32_t>::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<VkImage>([&](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<GLFWwindow, decltype(&glfwDestroyWindow)> 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<Renderer*>(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;
    }
}

This is the file included with the course. Copy and download give you its complete source.

Taking your work offline? Download all course companions