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.
// 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
