summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README26
-rwxr-xr-xvk-cube/build.sh6
-rw-r--r--vk-cube/vk-cube-fs.glsl47
-rw-r--r--vk-cube/vk-cube-vs.glsl42
-rw-r--r--vk-cube/vk-cube.c1285
-rw-r--r--win-resize-mt/build.bat3
-rw-r--r--win-resize-mt/build.sh3
-rw-r--r--win-resize-mt/win-resize-mt.c276
-rw-r--r--win-resize-st/build.bat3
-rw-r--r--win-resize-st/build.sh3
-rw-r--r--win-resize-st/win-resize-st.c (renamed from win-resize/win-resize.c)456
-rw-r--r--win-resize/build.bat3
-rwxr-xr-xwin-resize/build.sh3
13 files changed, 1908 insertions, 248 deletions
diff --git a/README b/README
index 6852f7b..45c58cb 100644
--- a/README
+++ b/README
@@ -1,13 +1,13 @@
--------------------------------------------------------------------------------
-kvog-git/demos
--------------------------------------------------------------------------------
-
-This repository contains a collection of small demo applications that don’t
-deserve their own repositories.
-
-These demos are for educational and experimental purposes only. They are not
-guaranteed to be correct, performant, safe, or (generally speaking) good.
-
--------------------------------------------------------------------------------
-win-resize | Drawing while resizing on Windows
--------------------------------------------------------------------------------
+-------------------------------------------------------------------------------
+kvog-git/demos
+-------------------------------------------------------------------------------
+
+This repository contains a collection of small demo applications that don’t
+deserve their own repositories.
+
+These demos are for educational and experimental purposes only. They are not
+guaranteed to be correct, performant, safe, or (generally speaking) good.
+
+vk-cube: Spinning cube Vulkan demo
+win-resize-mt: Drawing while resizing on Windows (multi threaded)
+win-resize-st: Drawing while resizing on Windows (single threaded)
diff --git a/vk-cube/build.sh b/vk-cube/build.sh
new file mode 100755
index 0000000..95fe2a4
--- /dev/null
+++ b/vk-cube/build.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+mkdir -p bin
+cc -o bin/vk-cube -Wall -Wextra -Wpedantic -O0 -g ./vk-cube.c \
+ $(pkg-config --cflags --libs sdl3 vulkan)
+glslc -o bin/vk-cube-vs.spv -fshader-stage=vertex ./vk-cube-vs.glsl
+glslc -o bin/vk-cube-fs.spv -fshader-stage=fragment ./vk-cube-fs.glsl
diff --git a/vk-cube/vk-cube-fs.glsl b/vk-cube/vk-cube-fs.glsl
new file mode 100644
index 0000000..622d2bd
--- /dev/null
+++ b/vk-cube/vk-cube-fs.glsl
@@ -0,0 +1,47 @@
+#version 450
+// ================================================================================================
+// Fragment shader, basic Phong shading
+//
+// Build:
+// $ glslc -o vk-cube-fs.spv -fshader-stage=fragment vk-cube-fs.glsl
+//
+// Changelog:
+// 5/31/2026: Initial release
+//
+// License:
+// Copyright (c) 2026 Hunter Kvalevog
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE.
+// ================================================================================================
+
+layout (location = 0) in vec3 f_c;
+layout (location = 1) in vec3 f_n;
+layout (location = 2) in vec3 f_p;
+
+layout (location = 0) out vec4 out_color;
+
+void main()
+{
+ vec3 light_pos = vec3(0.0f, 0.0f, -2.0f);
+ vec3 light_dir = normalize(light_pos - f_p);
+ vec3 view_dir = normalize(-f_p);
+
+ // ambient
+ float ambient = 0.15f;
+
+ // diffuse
+ float diffuse = max(dot(f_n, light_dir), 0.0f);
+
+ // specular
+ vec3 reflect_dir = reflect(-light_dir, f_n);
+ float spec = pow(max(dot(view_dir, reflect_dir), 0.0f), 32.0f);
+
+ vec3 result = (ambient + diffuse + 0.5 * spec) * f_c;
+
+ out_color = vec4(result, 1.0f);
+}
+
diff --git a/vk-cube/vk-cube-vs.glsl b/vk-cube/vk-cube-vs.glsl
new file mode 100644
index 0000000..eb51557
--- /dev/null
+++ b/vk-cube/vk-cube-vs.glsl
@@ -0,0 +1,42 @@
+#version 450
+// ================================================================================================
+// Vertex shader
+//
+// Build:
+// $ glslc -o vk-cube-vs.spv -fshader-stage=vertex vk-cube-vs.glsl
+//
+// Changelog:
+// 5/31/2026: Initial release
+//
+// License:
+// Copyright (c) 2026 Hunter Kvalevog
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE.
+// ================================================================================================
+
+layout (binding = 0) uniform UBO
+{
+ mat4 mvp;
+ mat4 model;
+};
+
+layout (location = 0) in vec3 v_p;
+layout (location = 1) in vec3 v_c;
+layout (location = 2) in vec3 v_n;
+
+layout (location = 0) out vec3 f_c;
+layout (location = 1) out vec3 f_n;
+layout (location = 2) out vec3 f_p;
+
+void main()
+{
+ f_c = v_c;
+ f_n = mat3(model) * v_n;
+ f_p = (model * vec4(v_p, 1.0f)).xyz;
+ gl_Position = mvp * vec4(v_p, 1.0f);
+}
+
diff --git a/vk-cube/vk-cube.c b/vk-cube/vk-cube.c
new file mode 100644
index 0000000..9be1eb7
--- /dev/null
+++ b/vk-cube/vk-cube.c
@@ -0,0 +1,1285 @@
+// ================================================================================================
+// This is a basic spinning cube that I wrote to learn Vulkan.
+//
+// This program could be structured better. I intentionally kept all the Vulkan API calls in the
+// main function so they can be read sequentially. It would be better to create helper functions
+// for swapchain creation, memory allocation, etc.
+//
+// ref: https://docs.vulkan.org
+// ref: https://github.com/KhronosGroup/Vulkan-Samples
+//
+// Changelog:
+// 5/31/2026: Initial release
+//
+// License:
+// Copyright (c) 2026 Hunter Kvalevog
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE.
+// ================================================================================================
+
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_vulkan.h>
+#include <vulkan/vulkan.h>
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#if defined(_WIN32)
+# include <windows.h>
+#endif
+
+#if defined(__APPLE__) || defined(__linux__)
+# include <unistd.h>
+#endif
+
+#ifdef __APPLE__
+# include <vulkan/vulkan_metal.h>
+#endif
+
+// ================================================================================================
+// Utility code
+// ================================================================================================
+
+#define ASSERT(X) assert(X)
+#define COUNTOF(ARR) (sizeof(ARR) / sizeof((ARR)[0]))
+#define DEG2RAD(DEG) ((DEG) * 3.14159265f / 180.0f)
+#define MAX(A, B) ((A) > (B) ? (A) : (B))
+#define MIN(A, B) ((A) < (B) ? (A) : (B))
+#define UNUSED(X) ((void)(X))
+
+// Find the index of the appropriate memory type
+static uint32_t find_mem_type(VkPhysicalDevice pdev, uint32_t filter, VkMemoryPropertyFlags flags)
+{
+ VkPhysicalDeviceMemoryProperties mem;
+ vkGetPhysicalDeviceMemoryProperties(pdev, &mem);
+ for (uint32_t i = 0; i < mem.memoryTypeCount; i++) {
+ if ((filter & (1 << i)) && (mem.memoryTypes[i].propertyFlags & flags) == flags) {
+ return i;
+ }
+ }
+ assert(0 && "failed to find memory type");
+ return 0;
+}
+
+// 4x4 identity matrix
+static inline void mat4ident(float dst[16])
+{
+ dst[ 0] = 1.0f; dst[ 1] = 0.0f; dst[ 2] = 0.0f; dst[ 3] = 0.0f;
+ dst[ 4] = 0.0f; dst[ 5] = 1.0f; dst[ 6] = 0.0f; dst[ 7] = 0.0f;
+ dst[ 8] = 0.0f; dst[ 9] = 0.0f; dst[10] = 1.0f; dst[11] = 0.0f;
+ dst[12] = 0.0f; dst[13] = 0.0f; dst[14] = 0.0f; dst[15] = 1.0f;
+}
+
+// 4x4 X rotation matrix
+static inline void mat4rotx(float dst[16], float rad)
+{
+ mat4ident(dst);
+ dst[ 5] = SDL_cosf(rad);
+ dst[ 9] = -SDL_sinf(rad);
+ dst[ 6] = SDL_sinf(rad);
+ dst[10] = SDL_cosf(rad);
+}
+
+// 4x4 Y rotation matrix
+static inline void mat4roty(float dst[16], float rad)
+{
+ mat4ident(dst);
+ dst[ 0] = SDL_cosf(rad);
+ dst[ 8] = SDL_sinf(rad);
+ dst[ 2] = -SDL_sinf(rad);
+ dst[10] = SDL_cosf(rad);
+}
+
+// 4x4 translation matrix
+static inline void mat4translate(float dst[16], float vec[3])
+{
+ mat4ident(dst);
+ dst[12] = vec[0];
+ dst[13] = vec[1];
+ dst[14] = vec[2];
+}
+
+// 4x4 matrix multiplication
+static inline void mat4mul(float dst[16], const float left[16], const float right[16])
+{
+ for (size_t col = 0; col < 4; ++col) {
+ for (size_t row = 0; row < 4; ++row) {
+ dst[col * 4 + row] =
+ left[0 * 4 + row] * right[col * 4 + 0] +
+ left[1 * 4 + row] * right[col * 4 + 1] +
+ left[2 * 4 + row] * right[col * 4 + 2] +
+ left[3 * 4 + row] * right[col * 4 + 3];
+ }
+ }
+}
+
+// 4x4 perspective projection matrix
+static inline void mat4perspective(float dst[16], float fov, float aspect, float z0, float z1)
+{
+ float f = 1.0f / SDL_tanf(fov / 2.0f);
+ float nmf = z0 - z1;
+ dst[ 0] = f / aspect; dst[ 1] = 0.0f; dst[ 2] = 0.0f; dst[ 3] = 0.0f;
+ dst[ 4] = 0.0f; dst[ 5] = -f; dst[ 6] = 0.0f; dst[ 7] = 0.0f;
+ dst[ 8] = 0.0f; dst[ 9] = 0.0f; dst[10] = z1 / nmf; dst[11] = -1.0f;
+ dst[12] = 0.0f; dst[13] = 0.0f; dst[14] = (z0 * z1) / nmf; dst[15] = 0.0f;
+}
+
+// ================================================================================================
+// Application code
+// ================================================================================================
+
+int main(int argc, const char **argv)
+{
+ UNUSED(argc); UNUSED(argv);
+
+ if (!SDL_Init(SDL_INIT_VIDEO)) {
+ printf("Failed to initialize SDL: %s", SDL_GetError());
+ return 0;
+ }
+
+ // Shader binaries should be in the same directory as the demo executable. Reset the working
+ // directory to make things reliable.
+ {
+ const char *exe_dir = SDL_GetBasePath();
+ printf("Setting working directory: %s\n", exe_dir);
+ // I wish the SDL devs were pragmatic enough to add SDL_SetCurrentDirectory():
+ // https://github.com/libsdl-org/SDL/issues/9110
+#if defined(_WIN32)
+ SetCurrentDirectory(exe_dir);
+#endif
+#if defined(__APPLE__) || defined(__linux__)
+ chdir(exe_dir);
+#endif
+ }
+
+ // Create VkInstance
+ VkInstance vki = 0;
+ {
+ // Instance extensions are essentially just extensions to the Vulkan spec. Without any
+ // extensions, Vulkan can't actually render anything because it doesn't know how to interop
+ // with the native OS window.
+ uint32_t num_exts = 0;
+ const char *exts[32] = { 0 };
+ #define REQUIRE_EXTENSION(NAME) ASSERT(num_exts < COUNTOF(exts)); exts[num_exts++] = NAME;
+
+ // SDL has a nice function that tells us what extensions are required for the given video
+ // backend.
+ uint32_t num_sdl_exts = 0;
+ const char *const *sdl_exts = SDL_Vulkan_GetInstanceExtensions(&num_sdl_exts);
+ for (uint32_t i = 0; i < num_sdl_exts; ++i) {
+ REQUIRE_EXTENSION(sdl_exts[i]);
+ }
+
+ // On macOS, we also need to activate the portability extension in order to use MoltenVK.
+ // This is currently the only extension we need that isn't mentioned by SDL.
+#ifdef __APPLE__
+ REQUIRE_EXTENSION(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
+#endif
+
+ // Tell the driver about this app. The only thing that relly matters is the API version.
+ VkApplicationInfo app_info = {
+ .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
+ .apiVersion = VK_API_VERSION_1_3,
+ };
+
+ // Bitwise flags that change the behavior of the VkInstance. It's basically pointless. The
+ // only accepted value in the spec is VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR.
+ VkInstanceCreateFlags flags = 0;
+
+ // ...which we need on macOS
+#ifdef __APPLE__
+ flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;
+#endif
+
+ printf("Requested instance extensions:\n");
+ for (uint32_t i = 0; i < num_exts; ++i) {
+ printf(" %s\n", exts[i]);
+ }
+
+ // The VK_LAYER_KHRONOS_validation validation layer helps detect incorrect API usage. It's
+ // extremely helpful in development, but not supported on every system. Enable it if it's
+ // available.
+
+ const char *validation_layer = "VK_LAYER_KHRONOS_validation";
+ bool has_validation_layer = false;
+ {
+ uint32_t num_layers = 0;
+ vkEnumerateInstanceLayerProperties(&num_layers, 0);
+
+ VkLayerProperties *layers = calloc(num_layers, sizeof(VkLayerProperties));
+ vkEnumerateInstanceLayerProperties(&num_layers, layers);
+
+ for (uint32_t i = 0; i < num_layers; ++i) {
+ if (!strcmp(layers[i].layerName, validation_layer)) {
+ has_validation_layer = true;
+ break;
+ }
+ }
+
+ free(layers);
+ }
+
+ // This function just passes info the vkCreateInstance. Specify required instance
+ // extensions and validation layers here.
+ VkInstanceCreateInfo create_info = {
+ .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
+ .flags = flags,
+ .pApplicationInfo = &app_info,
+ .enabledExtensionCount = num_exts,
+ .ppEnabledExtensionNames = exts,
+ .enabledLayerCount = has_validation_layer ? 1 : 0,
+ .ppEnabledLayerNames = &validation_layer,
+ };
+ VkResult vkr = vkCreateInstance(&create_info, 0, &vki);
+ if (vkr != VK_SUCCESS) {
+ printf("vkCreateInstance failed: %d", vkr);
+ return 0;
+ }
+
+ #undef REQUIRE_EXTENSION
+ }
+
+ // Create the window
+ const uint32_t wndflags = SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE;
+ SDL_Window *wnd = SDL_CreateWindow("vk-cube", 1024, 768, wndflags);
+ if (!wnd) {
+ printf("Failed to create window: %s\n", SDL_GetError());
+ return 0;
+ }
+
+ // Create the surface now so we can check if the physical device and queue families support
+ // drawing to it.
+ VkSurfaceKHR vksurf = 0;
+ if (!SDL_Vulkan_CreateSurface(wnd, vki, 0, &vksurf)) {
+ printf("Failed to create Vulkan surface: %s\n", SDL_GetError());
+ return 0;
+ }
+
+ // Image formats
+ VkFormat swapchain_format = VK_FORMAT_B8G8R8A8_SRGB;
+ VkFormat depth_format = VK_FORMAT_D32_SFLOAT;
+
+ // Select physical device and queue family
+ //
+ // The physical device is the literal GPU hardware unit that support Vulkan. I'm just selecting
+ // the first one with dynamic rendering support. In a real app, you might want to make it more
+ // complex and try to select the best GPU. Or better yet, allow the user to select the GPU and
+ // match the device UUID in VkPhysicalDeviceProperties.
+ //
+ // Queue families essentially just describe what operations a given device supports. This is
+ // important for nuanced things like compute or video, but this isn't really critical when we
+ // just want to draw basic 3D graphics. Like the device, just support the first queue family
+ // with VK_QUEUE_GRAPHICS_BIT support.
+ VkPhysicalDevice vkpdev = 0;
+ uint32_t vkqfi = UINT32_MAX;
+ {
+ // Enumerate physical devices
+ uint32_t num_devs = 0;
+ vkEnumeratePhysicalDevices(vki, &num_devs, 0);
+
+ VkPhysicalDevice *devs = calloc(num_devs, sizeof(VkPhysicalDevice));
+ vkEnumeratePhysicalDevices(vki, &num_devs, devs);
+
+ printf("Available GPUs:\n");
+ for (uint32_t i = 0; i < num_devs; ++i) {
+ // Get basic device properties (name)
+ VkPhysicalDeviceProperties2 properties = {
+ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
+ };
+ vkGetPhysicalDeviceProperties2(devs[i], &properties);
+
+ // Get dynamic rendering support
+ VkPhysicalDeviceDynamicRenderingFeatures dynamic_rendering_features = {
+ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES,
+ };
+ // and Synchronization2 support
+ VkPhysicalDeviceSynchronization2Features sync2_features = {
+ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SYNCHRONIZATION_2_FEATURES,
+ .pNext = &dynamic_rendering_features,
+ };
+ VkPhysicalDeviceFeatures2 features = {
+ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
+ .pNext = &sync2_features,
+ };
+ vkGetPhysicalDeviceFeatures2(devs[i], &features);
+
+ // Get device queue families
+ uint32_t num_qfams = 0;
+ vkGetPhysicalDeviceQueueFamilyProperties(devs[i], &num_qfams, 0);
+
+ VkQueueFamilyProperties *qfams = calloc(num_qfams, sizeof(VkQueueFamilyProperties));
+ vkGetPhysicalDeviceQueueFamilyProperties(devs[i], &num_qfams, qfams);
+
+ uint32_t dev_qfi = UINT32_MAX;
+ for (uint32_t j = 0; j < num_qfams; ++j) {
+ if (!(qfams[j].queueFlags & VK_QUEUE_GRAPHICS_BIT)) {
+ continue;
+ }
+
+ if (SDL_Vulkan_GetPresentationSupport(vki, devs[i], j)) {
+ dev_qfi = j;
+ }
+ }
+
+ free(qfams);
+
+ bool selected = !vkpdev && dev_qfi != UINT32_MAX &&
+ dynamic_rendering_features.dynamicRendering &&
+ sync2_features.synchronization2;
+
+ printf(" %s%s\n", properties.properties.deviceName, selected ? " (selected)" : "");
+
+ if (selected) {
+ vkpdev = devs[i];
+ vkqfi = dev_qfi;
+ }
+ }
+ free(devs);
+ }
+
+ // At this point our validation layers are loaded and I'm not going to check VkResult
+
+ // Create the device instance
+ VkDevice vkdev = 0;
+ {
+ const char *exts[] = {
+ "VK_KHR_swapchain", // required to present stuff to the screen
+#ifdef __APPLE__
+ "VK_KHR_portability_subset", // required for MoltenVK
+#endif
+ };
+ printf("Requested device extensions:\n");
+ for (uint32_t i = 0; i < COUNTOF(exts); ++i) {
+ printf(" %s\n", exts[i]);
+ }
+
+ // Ask for dynamic rendering support
+ VkPhysicalDeviceDynamicRenderingFeatures dynamic_rendering_features = {
+ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES,
+ .dynamicRendering = VK_TRUE,
+ };
+ // Ask for Synchronization2 support
+ VkPhysicalDeviceSynchronization2Features sync2_features = {
+ .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SYNCHRONIZATION_2_FEATURES,
+ .synchronization2 = VK_TRUE,
+ .pNext = &dynamic_rendering_features,
+ };
+
+ float queue_priority = 1.0f;
+ VkDeviceQueueCreateInfo queue_create_info = {
+ .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
+ .queueFamilyIndex = vkqfi,
+ .queueCount = 1,
+ .pQueuePriorities = &queue_priority,
+ };
+ VkDeviceCreateInfo create_info = {
+ .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
+ .queueCreateInfoCount = 1,
+ .pQueueCreateInfos = &queue_create_info,
+ .pNext = &sync2_features,
+ .enabledExtensionCount = COUNTOF(exts),
+ .ppEnabledExtensionNames = exts,
+ };
+ vkCreateDevice(vkpdev, &create_info, 0, &vkdev);
+
+ printf("Logical device created\n");
+ }
+
+ // Get handle to graphics queue for the logical device
+ VkQueue vkq = 0;
+ vkGetDeviceQueue(vkdev, vkqfi, 0, &vkq);
+
+ // Allow two frames in flight. This means we can start preparing the next CPU-side while
+ // waiting for the GPU to render the last frame;
+ const uint32_t max_frames_in_flight = 2;
+
+ // Create command pool and buffers.
+ //
+ // The command pool is simply a memory allocator for GPU commands.
+ //
+ // The command buffer is the actual list of commands that will later be queued for execution on
+ // the GPU. With max_frames_in_flight = 2, we will need 2 command buffers since we will be
+ // rendering two frames at the same time.
+ VkCommandPool vkcmdpool = 0;
+ VkCommandBuffer *vkcmdbufs = calloc(max_frames_in_flight, sizeof(VkCommandBuffer));
+ {
+ VkCommandPoolCreateInfo create_pool = {
+ .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
+ .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,
+ .queueFamilyIndex = vkqfi,
+ };
+ vkCreateCommandPool(vkdev, &create_pool, 0, &vkcmdpool);
+
+ VkCommandBufferAllocateInfo allocate_buffer = {
+ .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
+ .commandPool = vkcmdpool,
+ .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
+ .commandBufferCount = max_frames_in_flight,
+ };
+ vkAllocateCommandBuffers(vkdev, &allocate_buffer, vkcmdbufs);
+
+ printf("Command buffers created\n");
+ }
+
+ typedef struct Vertex Vertex;
+ struct Vertex { float p[3]; float c[3]; float n[3]; };
+
+ // Model data for a unit cube
+ const Vertex vdata[] = {
+ // front
+ { { -0.5f, -0.5f, 0.5f }, { 1.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 1.0f } },
+ { { 0.5f, -0.5f, 0.5f }, { 1.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 1.0f } },
+ { { 0.5f, 0.5f, 0.5f }, { 1.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 1.0f } },
+ { { -0.5f, 0.5f, 0.5f }, { 1.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 1.0f } },
+ // back
+ { { 0.5f, -0.5f, -0.5f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 0.0f, -1.0f } },
+ { { -0.5f, -0.5f, -0.5f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 0.0f, -1.0f } },
+ { { -0.5f, 0.5f, -0.5f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 0.0f, -1.0f } },
+ { { 0.5f, 0.5f, -0.5f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 0.0f, -1.0f } },
+ // left (blue)
+ { { -0.5f, -0.5f, -0.5f }, { 0.0f, 0.0f, 1.0f }, { -1.0f, 0.0f, 0.0f } },
+ { { -0.5f, -0.5f, 0.5f }, { 0.0f, 0.0f, 1.0f }, { -1.0f, 0.0f, 0.0f } },
+ { { -0.5f, 0.5f, 0.5f }, { 0.0f, 0.0f, 1.0f }, { -1.0f, 0.0f, 0.0f } },
+ { { -0.5f, 0.5f, -0.5f }, { 0.0f, 0.0f, 1.0f }, { -1.0f, 0.0f, 0.0f } },
+ // right (yellow)
+ { { 0.5f, -0.5f, 0.5f }, { 1.0f, 1.0f, 0.0f }, { 1.0f, 0.0f, 0.0f } },
+ { { 0.5f, -0.5f, -0.5f }, { 1.0f, 1.0f, 0.0f }, { 1.0f, 0.0f, 0.0f } },
+ { { 0.5f, 0.5f, -0.5f }, { 1.0f, 1.0f, 0.0f }, { 1.0f, 0.0f, 0.0f } },
+ { { 0.5f, 0.5f, 0.5f }, { 1.0f, 1.0f, 0.0f }, { 1.0f, 0.0f, 0.0f } },
+ // top (magenta)
+ { { -0.5f, 0.5f, 0.5f }, { 1.0f, 0.0f, 1.0f }, { 0.0f, 1.0f, 0.0f } },
+ { { 0.5f, 0.5f, 0.5f }, { 1.0f, 0.0f, 1.0f }, { 0.0f, 1.0f, 0.0f } },
+ { { 0.5f, 0.5f, -0.5f }, { 1.0f, 0.0f, 1.0f }, { 0.0f, 1.0f, 0.0f } },
+ { { -0.5f, 0.5f, -0.5f }, { 1.0f, 0.0f, 1.0f }, { 0.0f, 1.0f, 0.0f } },
+ // bottom (cyan)
+ { { -0.5f, -0.5f, -0.5f }, { 0.0f, 1.0f, 1.0f }, { 0.0f, -1.0f, 0.0f } },
+ { { 0.5f, -0.5f, -0.5f }, { 0.0f, 1.0f, 1.0f }, { 0.0f, -1.0f, 0.0f } },
+ { { 0.5f, -0.5f, 0.5f }, { 0.0f, 1.0f, 1.0f }, { 0.0f, -1.0f, 0.0f } },
+ { { -0.5f, -0.5f, 0.5f }, { 0.0f, 1.0f, 1.0f }, { 0.0f, -1.0f, 0.0f } }
+ };
+
+ const uint16_t idata[] = {
+ 0, 1, 2, 0, 2, 3, // front
+ 4, 5, 6, 4, 6, 7, // back
+ 8, 9, 10, 8, 10, 11, // left
+ 12, 13, 14, 12, 14, 15, // right
+ 16, 17, 18, 16, 18, 19, // top
+ 20, 21, 22, 20, 22, 23, // bottom
+ };
+
+ // Uniform data
+ typedef struct Uniforms Uniforms;
+ struct Uniforms
+ {
+ float mvp[16];
+ float model[16];
+ };
+
+ // Alllocate memory for vertex, index, and uniform data
+ //
+ // Note: vkAllocateMemory is very expensive, and there's a hard limit to how many times it can
+ // be called. In a real app, it's better to do bulk allocations and sub-allocate as needed.
+ // Theres'a a library called "vulkan memory allocator" that people really like. For this demo,
+ // allocating per buffer is fine.
+
+ VkBuffer vkvbuf = 0; // cube vertex buffer
+ VkDeviceMemory vkvmem = 0;
+ VkBuffer vkibuf = 0; // cube index buffer
+ VkDeviceMemory vkimem = 0;
+
+ VkBuffer *vkubufs = calloc(max_frames_in_flight, sizeof(VkBuffer));
+ VkDeviceMemory *vkumems = calloc(max_frames_in_flight, sizeof(VkDeviceMemory));
+
+ {
+ VkPhysicalDeviceMemoryProperties memprops = { 0 };
+ vkGetPhysicalDeviceMemoryProperties(vkpdev, &memprops);
+
+ // This code is super long for what it does, so make it data-driven. It would be cleaner
+ // as a function, but I want this demo to read sequentually.
+
+ typedef struct Alloc Alloc;
+ struct Alloc
+ {
+ VkBuffer *buf;
+ VkDeviceMemory *mem;
+ VkDeviceSize size;
+ VkBufferUsageFlags usage;
+ };
+
+ uint32_t num_allocs = 0;
+ Alloc allocs[32] = { 0 };
+
+ #define ALLOC(BUF, MEM, SIZE, USAGE) \
+ ASSERT(num_allocs< COUNTOF(allocs)); \
+ allocs[num_allocs++] = (Alloc){ BUF, MEM, SIZE, USAGE };
+
+ ALLOC(&vkvbuf, &vkvmem, sizeof(vdata), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
+ ALLOC(&vkibuf, &vkimem, sizeof(idata), VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
+ for (uint32_t i = 0; i < max_frames_in_flight; ++i) {
+ ALLOC(&vkubufs[i], &vkumems[i], sizeof(Uniforms), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT);
+ }
+
+ for (uint32_t i = 0; i < num_allocs; ++i) {
+ VkBufferCreateInfo create = {
+ .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
+ .size = allocs[i].size,
+ .usage = allocs[i].usage,
+ .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
+ };
+ vkCreateBuffer(vkdev, &create, 0, allocs[i].buf);
+
+ // Actual allocation size including padding and alignment
+ VkMemoryRequirements memreq = { 0 };
+ vkGetBufferMemoryRequirements(vkdev, *allocs[i].buf, &memreq);
+
+ // Find the appropriate device memory type for this allocation
+ VkMemoryPropertyFlagBits required_props = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
+ VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
+ uint32_t mem_type_idx = find_mem_type(vkpdev, memreq.memoryTypeBits, required_props);
+
+ VkMemoryAllocateInfo alloc = {
+ .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
+ .allocationSize = memreq.size,
+ .memoryTypeIndex = mem_type_idx,
+ };
+ vkAllocateMemory(vkdev, &alloc, 0, allocs[i].mem);
+ vkBindBufferMemory(vkdev, *allocs[i].buf, *allocs[i].mem, 0);
+ }
+
+ #undef ALLOC
+
+ printf("Geometry buffers created\n");
+ }
+
+ // Upload vertex data
+ {
+ void *map = 0;
+ vkMapMemory(vkdev, vkvmem, 0, sizeof(vdata), 0, &map);
+ memcpy(map, vdata, sizeof(vdata));
+ vkUnmapMemory(vkdev, vkvmem);
+ }
+
+ // Upload index data
+ {
+ void *map = 0;
+ vkMapMemory(vkdev, vkimem, 0, sizeof(idata), 0, &map);
+ memcpy(map, idata, sizeof(idata));
+ vkUnmapMemory(vkdev, vkimem);
+ }
+
+ // Map uniform buffers
+ Uniforms **ubufs = calloc(max_frames_in_flight, sizeof(Uniforms *));
+ for (uint32_t i = 0; i < max_frames_in_flight; ++i) {
+ vkMapMemory(vkdev, vkumems[i], 0, sizeof(Uniforms), 0, (void **)&ubufs[i]);
+ }
+
+ // Create descriptors
+ //
+ // Descriptors specify how a shader can access a resource. In this case, it only needs to
+ // know how to read uniforms in the vertex stage.
+ //
+ // VkDescriptorSetLayout defines how the binding is used
+ // VkDescriptorPool is an allocator for descriptor sets
+ // VkDescriptorSet defines the pointer to the actual block of GPU device memory is used
+ VkDescriptorSetLayout vksetlayout = 0;
+ VkDescriptorPool vkdescpool = 0;
+ VkDescriptorSet *vksets = calloc(max_frames_in_flight, sizeof(VkDescriptorSet));
+ {
+ VkDescriptorSetLayoutBinding descriptor_set_layout_binding = {
+ .binding = 0,
+ .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
+ .descriptorCount = 1,
+ .stageFlags = VK_SHADER_STAGE_VERTEX_BIT
+ };
+ VkDescriptorSetLayoutCreateInfo descriptor_set_layout_create = {
+ .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
+ .bindingCount = 1,
+ .pBindings = &descriptor_set_layout_binding
+ };
+ vkCreateDescriptorSetLayout(vkdev, &descriptor_set_layout_create, 0, &vksetlayout);
+
+ // Allocator for descriptor sets
+ VkDescriptorPoolSize pool_size = {
+ .type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
+ .descriptorCount = max_frames_in_flight
+ };
+ VkDescriptorPoolCreateInfo pool_create = {
+ .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
+ .maxSets = max_frames_in_flight,
+ .poolSizeCount = 1,
+ .pPoolSizes = &pool_size
+ };
+ vkCreateDescriptorPool(vkdev, &pool_create, 0, &vkdescpool);
+
+ VkDescriptorSetLayout *layouts = calloc(max_frames_in_flight,
+ sizeof(VkDescriptorSetLayout));
+ for (uint32_t i = 0; i < max_frames_in_flight; ++i) {
+ layouts[i] = vksetlayout;
+ }
+
+ VkDescriptorSetAllocateInfo set_alloc_info = {
+ .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO,
+ .descriptorPool = vkdescpool,
+ .descriptorSetCount = max_frames_in_flight,
+ .pSetLayouts = layouts
+ };
+ vkAllocateDescriptorSets(vkdev, &set_alloc_info, vksets);
+
+ // Point each descriptor set to its respective uniform buffer
+ for (uint32_t i = 0; i < max_frames_in_flight; ++i) {
+ VkDescriptorBufferInfo buffer_info = {
+ .buffer = vkubufs[i],
+ .offset = 0,
+ .range = sizeof(Uniforms)
+ };
+ VkWriteDescriptorSet write = {
+ .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
+ .dstSet = vksets[i],
+ .dstBinding = 0,
+ .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
+ .descriptorCount = 1,
+ .pBufferInfo = &buffer_info
+ };
+ vkUpdateDescriptorSets(vkdev, 1, &write, 0, 0);
+ }
+ printf("Descriptor sets created\n");
+ }
+
+ // Create pipeline
+ VkPipelineLayout vklayout = 0;
+ VkPipeline vkpl = 0;
+ {
+ // Vertex shader module
+ VkShaderModule vs_mod = 0;
+ VkShaderModuleCreateInfo vs_create = {
+ .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO
+ };
+ vs_create.pCode = SDL_LoadFile("vk-cube-vs.spv", &vs_create.codeSize);
+ if (!vs_create.pCode) {
+ printf("Failed to load vertex shader: %s\n", SDL_GetError());
+ return 0;
+ }
+ vkCreateShaderModule(vkdev, &vs_create, 0, &vs_mod);
+
+ VkPipelineShaderStageCreateInfo vs_stage = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
+ .stage = VK_SHADER_STAGE_VERTEX_BIT,
+ .module = vs_mod,
+ .pName = "main"
+ };
+
+ // Fragment shader module
+ VkShaderModule fs_mod = 0;
+ VkShaderModuleCreateInfo fs_create = {
+ .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
+ };
+ fs_create.pCode = SDL_LoadFile("vk-cube-fs.spv", &fs_create.codeSize);
+ if (!fs_create.pCode) {
+ printf("Failed to load fragment shader: %s\n", SDL_GetError());
+ return 0;
+ }
+ vkCreateShaderModule(vkdev, &fs_create, 0, &fs_mod);
+
+ VkPipelineShaderStageCreateInfo fs_stage = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
+ .stage = VK_SHADER_STAGE_FRAGMENT_BIT,
+ .module = fs_mod,
+ .pName = "main"
+ };
+
+ VkPipelineShaderStageCreateInfo stages[] = { vs_stage, fs_stage };
+
+ // Define vertex input
+ VkVertexInputBindingDescription vert_bind_desc = {
+ .binding = 0,
+ .stride = sizeof(Vertex),
+ .inputRate = VK_VERTEX_INPUT_RATE_VERTEX
+ };
+
+ // Vertex attribute: position
+ VkVertexInputAttributeDescription vert_attr_p = {
+ .binding = 0,
+ .location = 0,
+ .format = VK_FORMAT_R32G32B32_SFLOAT,
+ .offset = offsetof(Vertex, p)
+ };
+ // Vertex attribute: color
+ VkVertexInputAttributeDescription vert_attr_c = {
+ .binding = 0,
+ .location = 1,
+ .format = VK_FORMAT_R32G32B32_SFLOAT,
+ .offset = offsetof(Vertex, c)
+ };
+ // Vertex attribute: normal
+ VkVertexInputAttributeDescription vert_attr_n = {
+ .binding = 0,
+ .location = 2,
+ .format = VK_FORMAT_R32G32B32_SFLOAT,
+ .offset = offsetof(Vertex, n)
+ };
+
+ VkVertexInputAttributeDescription vert_attrs[] = {
+ vert_attr_p,
+ vert_attr_c,
+ vert_attr_n
+ };
+
+ VkPipelineVertexInputStateCreateInfo vert_create = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
+ .vertexBindingDescriptionCount = 1,
+ .pVertexBindingDescriptions = &vert_bind_desc,
+ .vertexAttributeDescriptionCount = COUNTOF(vert_attrs),
+ .pVertexAttributeDescriptions = vert_attrs
+ };
+
+ // Input geometry layout
+ VkPipelineInputAssemblyStateCreateInfo input_assembly_create = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
+ .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST
+ };
+
+ // Dynamic viewport and scissor state
+ VkDynamicState dynamic_states[] = {
+ VK_DYNAMIC_STATE_VIEWPORT,
+ VK_DYNAMIC_STATE_SCISSOR
+ };
+ VkPipelineDynamicStateCreateInfo dynamic_state_create = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
+ .dynamicStateCount = COUNTOF(dynamic_states),
+ .pDynamicStates = dynamic_states
+ };
+ VkPipelineViewportStateCreateInfo viewport_state_create = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
+ .viewportCount = 1,
+ .scissorCount = 1
+ };
+
+ // Rasterizer state
+ VkPipelineRasterizationStateCreateInfo rasterizer_state_create = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
+ .polygonMode = VK_POLYGON_MODE_FILL,
+ .cullMode = VK_CULL_MODE_BACK_BIT,
+ .frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE,
+ .lineWidth = 1.0f
+ };
+
+ // Multisample state
+ VkPipelineMultisampleStateCreateInfo multisample_state_create = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
+ .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT // disabled
+ };
+
+ // Depth stencil state
+ VkPipelineDepthStencilStateCreateInfo depth_stencil_state_create = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO,
+ .depthTestEnable = VK_TRUE,
+ .depthWriteEnable = VK_TRUE,
+ .depthCompareOp = VK_COMPARE_OP_LESS
+ };
+
+ // Color blending state
+ VkPipelineColorBlendAttachmentState color_blend_attachment_state = {
+ .colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
+ VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT,
+ };
+ VkPipelineColorBlendStateCreateInfo color_blend_state_create = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
+ .attachmentCount = 1,
+ .pAttachments = &color_blend_attachment_state
+ };
+
+ // Pipeline layout - basically just specifies descriptor set layout
+ VkPipelineLayoutCreateInfo layout_create = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
+ .setLayoutCount = 1,
+ .pSetLayouts = &vksetlayout
+ };
+ vkCreatePipelineLayout(vkdev, &layout_create, 0, &vklayout);
+
+ // Rendering state
+ VkPipelineRenderingCreateInfo rendering_create = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO,
+ .colorAttachmentCount = 1,
+ .pColorAttachmentFormats = &swapchain_format,
+ .depthAttachmentFormat = depth_format
+ };
+
+ // Assemble everything
+ VkGraphicsPipelineCreateInfo pipeline_create = {
+ .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
+ .pNext = &rendering_create,
+ .stageCount = COUNTOF(stages),
+ .pStages = stages,
+ .pVertexInputState = &vert_create,
+ .pInputAssemblyState = &input_assembly_create,
+ .pViewportState = &viewport_state_create,
+ .pRasterizationState = &rasterizer_state_create,
+ .pMultisampleState = &multisample_state_create,
+ .pDepthStencilState = &depth_stencil_state_create,
+ .pColorBlendState = &color_blend_state_create,
+ .pDynamicState = &dynamic_state_create,
+ .layout = vklayout
+ };
+ vkCreateGraphicsPipelines(vkdev, 0, 1, &pipeline_create, 0, &vkpl);
+
+ printf("Pipeline created\n");
+ }
+
+ // The swapchain needs to be recreated any time the window is resized
+ bool swapchain_dirty = true;
+ VkSwapchainKHR vkswapchain = 0;
+ uint32_t num_swapchain_images = 0;
+ VkImage *swapchain_images = 0;
+ VkImageView *swapchain_views = 0;
+ VkImage depth_image = 0;
+ VkDeviceMemory depth_mem = 0;
+ VkImageView depth_view = 0;
+ VkExtent2D extent2 = { 0 };
+ VkExtent3D extent3 = { 0 };
+
+ // Signaled when the swapchain has fresh image to render to
+ VkSemaphore *vk_image_available_sems = calloc(max_frames_in_flight, sizeof(VkSemaphore));
+
+ // Signaled when we are done drawing to an image and it should be presented to the user
+ VkSemaphore *vk_render_finished_sems = 0;
+ uint32_t num_render_finished_sems = 0;
+
+ // Signaled when the command buffer is done executing. Signaled by default to avoid deadlock
+ // on first frame.
+ VkFence *vk_in_flight_fences = calloc(max_frames_in_flight, sizeof(VkFence));
+
+ // Initial allocations for both
+ for (uint32_t i = 0; i < max_frames_in_flight; ++i) {
+ VkFenceCreateInfo fci = {
+ .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
+ .flags = VK_FENCE_CREATE_SIGNALED_BIT,
+ };
+ vkCreateFence(vkdev, &fci, 0, &vk_in_flight_fences[i]);
+
+ VkSemaphoreCreateInfo sci = {
+ .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
+ };
+ vkCreateSemaphore(vkdev, &sci, 0, &vk_image_available_sems[i]);
+ }
+
+ bool running = true;
+ while (running) {
+ SDL_Event evt;
+ while (SDL_PollEvent(&evt)) {
+ switch (evt.type) {
+ case SDL_EVENT_WINDOW_RESIZED:
+ swapchain_dirty = true;
+ break;
+ case SDL_EVENT_QUIT:
+ running = false;
+ break;
+ };
+ }
+
+ int wnd_w = 0;
+ int wnd_h = 0;
+ SDL_GetWindowSizeInPixels(wnd, &wnd_w, &wnd_h);
+
+ if (wnd_w <= 0 || wnd_h <= 0) {
+ SDL_Delay(10); // 10ms, idk
+ continue;
+ }
+
+ // Create swapchain if needed
+ if (swapchain_dirty) {
+ vkDeviceWaitIdle(vkdev);
+
+ VkSurfaceCapabilitiesKHR scaps;
+ vkGetPhysicalDeviceSurfaceCapabilitiesKHR(vkpdev, vksurf, &scaps);
+
+ assert(scaps.currentExtent.width > 0);
+ assert(scaps.currentExtent.height > 0);
+
+ if (vkswapchain) {
+ vkDestroyImageView(vkdev, depth_view, 0); depth_view = 0;
+ vkDestroyImage(vkdev, depth_image, 0); depth_image = 0;
+ vkFreeMemory(vkdev, depth_mem, 0); depth_mem = 0;
+ for (uint32_t i = 0; i < num_swapchain_images; ++i) {
+ vkDestroyImageView(vkdev, swapchain_views[i], 0);
+ swapchain_views[i] = 0;
+ }
+ free(swapchain_images); swapchain_images = 0;
+ free(swapchain_views); swapchain_views = 0;
+ vkDestroySwapchainKHR(vkdev, vkswapchain, 0); vkswapchain = 0;
+ }
+
+ // minImageCount is almost always 2
+ uint32_t image_count = scaps.minImageCount + 1;
+ if (scaps.maxImageCount > 0) {
+ image_count = MIN(image_count, scaps.maxImageCount);
+ }
+ assert(max_frames_in_flight <= image_count);
+
+ extent2.width = wnd_w;
+ extent2.height = wnd_h;
+ extent3.width = extent2.width;
+ extent3.height = extent2.height;
+ extent3.depth = 1;
+
+ VkSwapchainCreateInfoKHR swapchain_create = {
+ .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
+ .surface = vksurf,
+ .minImageCount = image_count,
+ .imageFormat = swapchain_format,
+ .imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR,
+ .imageExtent = extent2,
+ .imageArrayLayers = 1,
+ .imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
+ .imageSharingMode = VK_SHARING_MODE_EXCLUSIVE, // gfx and present queues are same
+ .preTransform = scaps.currentTransform,
+ .compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
+ .presentMode = VK_PRESENT_MODE_FIFO_KHR, // vsync
+ .clipped = VK_TRUE
+ };
+ vkCreateSwapchainKHR(vkdev, &swapchain_create, 0, &vkswapchain);
+
+ // Get swapchain image handles
+ vkGetSwapchainImagesKHR(vkdev, vkswapchain, &num_swapchain_images, 0);
+ swapchain_images = calloc(num_swapchain_images, sizeof(VkImage));
+ vkGetSwapchainImagesKHR(vkdev, vkswapchain, &num_swapchain_images, swapchain_images);
+
+ // Create swapchain image views
+ swapchain_views = calloc(num_swapchain_images, sizeof(VkImageView));
+ for (uint32_t i = 0; i < num_swapchain_images; ++i) {
+ VkImageViewCreateInfo view_create = {
+ .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
+ .image = swapchain_images[i],
+ .viewType = VK_IMAGE_VIEW_TYPE_2D,
+ .format = swapchain_format,
+ .subresourceRange = (VkImageSubresourceRange){
+ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .levelCount = 1,
+ .layerCount = 1
+ }
+ };
+ vkCreateImageView(vkdev, &view_create, 0, &swapchain_views[i]);
+ }
+
+ // Create depth image
+ VkImageCreateInfo depth_create = {
+ .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
+ .imageType = VK_IMAGE_TYPE_2D,
+ .format = depth_format,
+ .extent = extent3,
+ .mipLevels = 1,
+ .arrayLayers = 1,
+ .samples = VK_SAMPLE_COUNT_1_BIT,
+ .tiling = VK_IMAGE_TILING_OPTIMAL,
+ .usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT
+ };
+ vkCreateImage(vkdev, &depth_create, 0, &depth_image);
+
+ // Allocate depth image memory
+ VkMemoryRequirements memreq = { 0 };
+ vkGetImageMemoryRequirements(vkdev, depth_image, &memreq);
+ VkMemoryAllocateInfo alloc = {
+ .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
+ .allocationSize = memreq.size
+ };
+ alloc.memoryTypeIndex = find_mem_type(vkpdev, memreq.memoryTypeBits,
+ VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
+ vkAllocateMemory(vkdev, &alloc, 0, &depth_mem);
+ vkBindImageMemory(vkdev, depth_image, depth_mem, 0);
+
+ // Create depth image view
+ VkImageViewCreateInfo view_create = {
+ .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
+ .image = depth_image,
+ .viewType = VK_IMAGE_VIEW_TYPE_2D,
+ .format = depth_format,
+ .subresourceRange = (VkImageSubresourceRange){
+ .aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT,
+ .levelCount = 1,
+ .layerCount = 1
+ }
+ };
+ vkCreateImageView(vkdev, &view_create, 0, &depth_view);
+
+ // Create synchronization objects
+ //
+ // Semaphores are for GPU-GPU synchronization and fences are for CPU-GPU sync.
+ {
+ // The spec allows num_swapchain_images to vary per frame, but it probably won't.
+ // Deal with it anyway.
+ if (num_render_finished_sems < num_swapchain_images) {
+ vk_render_finished_sems = realloc(vk_render_finished_sems,
+ sizeof(VkSemaphore) * num_swapchain_images);
+ for (uint32_t i = num_render_finished_sems; i < num_swapchain_images; ++i) {
+ VkSemaphoreCreateInfo sci = {
+ .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
+ };
+ vkCreateSemaphore(vkdev, &sci, 0, &vk_render_finished_sems[i]);
+ }
+
+ num_render_finished_sems = num_swapchain_images;
+ }
+ }
+
+ printf("Swapchain created\n");
+
+ swapchain_dirty = false;
+ }
+
+ static int f = 0; // frame cycler, [0, max_frames_in_flight)
+
+ vkWaitForFences(vkdev, 1, &vk_in_flight_fences[f], VK_TRUE, UINT64_MAX);
+
+ uint32_t img_idx = 0;
+ VkResult vkr = vkAcquireNextImageKHR(vkdev, vkswapchain, UINT64_MAX,
+ vk_image_available_sems[f], VK_NULL_HANDLE, &img_idx);
+ if (vkr == VK_ERROR_OUT_OF_DATE_KHR) {
+ swapchain_dirty = true;
+ continue;
+ }
+
+ vkResetFences(vkdev, 1, &vk_in_flight_fences[f]);
+
+ // Update MVP
+ float *mvp = ubufs[f]->mvp;
+ float *model = ubufs[f]->model;
+ {
+ const float t = (float)SDL_GetTicks();
+
+ float xyz[3] = { SDL_cosf(t * 0.001f), SDL_sinf(t * 0.001f), -2.0f };
+ float translate[16];
+ mat4translate(translate, xyz);
+
+ float rotate_x[16];
+ mat4rotx(rotate_x, DEG2RAD(t * 0.08f));
+
+ float rotate_y[16];
+ mat4roty(rotate_y, DEG2RAD(t * 0.05f));
+
+ float tmp[16];
+ mat4mul(tmp, rotate_x, rotate_y);
+ mat4mul(model, translate, tmp);
+
+ float proj[16];
+ mat4perspective(proj, DEG2RAD(90.0f), (float)wnd_w / (float)wnd_h, 0.1f, 10.0f);
+
+ mat4mul(mvp, proj, model);
+ }
+
+ VkCommandBuffer cmd = vkcmdbufs[f];
+ vkResetCommandBuffer(cmd, 0);
+
+ VkCommandBufferBeginInfo cmd_begin = {
+ .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO
+ };
+ vkBeginCommandBuffer(cmd, &cmd_begin);
+
+ // Transition swapchain image: unknown -> color attachment
+ {
+ VkImageMemoryBarrier2 barrier = {
+ .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
+ .srcStageMask = VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT,
+ .srcAccessMask = 0,
+ .dstStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT,
+ .dstAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT,
+ .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
+ .newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
+ .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .image = swapchain_images[img_idx],
+ .subresourceRange = (VkImageSubresourceRange){
+ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .baseMipLevel = 0,
+ .levelCount = 1,
+ .baseArrayLayer = 0,
+ .layerCount = 1
+ }
+ };
+
+ VkDependencyInfo dep_info = {
+ .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
+ .imageMemoryBarrierCount = 1,
+ .pImageMemoryBarriers = &barrier
+ };
+ vkCmdPipelineBarrier2(cmd, &dep_info);
+ }
+
+ // Transition depth image: unknown -> depth attachment
+ {
+ VkImageMemoryBarrier2 barrier = {
+ .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
+ .srcStageMask = VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT,
+ .srcAccessMask = 0,
+ .dstStageMask = VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT,
+ .dstAccessMask = VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
+ .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
+ .newLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL,
+ .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .image = depth_image,
+ .subresourceRange = (VkImageSubresourceRange){
+ .aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT,
+ .baseMipLevel = 0,
+ .levelCount = 1,
+ .baseArrayLayer = 0,
+ .layerCount = 1
+ }
+ };
+
+ VkDependencyInfo dep_info = {
+ .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
+ .imageMemoryBarrierCount = 1,
+ .pImageMemoryBarriers = &barrier
+ };
+ vkCmdPipelineBarrier2(cmd, &dep_info);
+ }
+
+ // Begin dynamic rendering
+ {
+ VkRenderingAttachmentInfo color_attachment = {
+ .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO,
+ .imageView = swapchain_views[img_idx],
+ .imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
+ .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
+ .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
+ .clearValue.color = { { 0.1f, 0.1f, 0.1f, 1.0f } }
+ };
+
+ VkRenderingAttachmentInfo depth_attachment = {
+ .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO,
+ .imageView = depth_view,
+ .imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL,
+ .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
+ .storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
+ .clearValue.depthStencil = { 1.0f, 0 }
+ };
+
+ VkRenderingInfo render_info = {
+ .sType = VK_STRUCTURE_TYPE_RENDERING_INFO,
+ .renderArea = { { 0, 0 }, extent2 },
+ .layerCount = 1,
+ .colorAttachmentCount = 1,
+ .pColorAttachments = &color_attachment,
+ .pDepthAttachment = &depth_attachment
+ };
+
+ vkCmdBeginRendering(cmd, &render_info);
+ }
+
+ // Set dynamic viewport and scissor
+ {
+ VkViewport viewport = {
+ .width = extent2.width,
+ .height = extent2.height,
+ .minDepth = 0.0f,
+ .maxDepth = 1.0f,
+ };
+ vkCmdSetViewport(cmd, 0, 1, &viewport);
+
+ VkRect2D scissor = {
+ .extent = extent2,
+ };
+ vkCmdSetScissor(cmd, 0, 1, &scissor);
+ }
+
+ // Draw the cube
+ {
+ vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, vkpl);
+ vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, vklayout, 0, 1,
+ &vksets[f], 0, 0);
+ VkDeviceSize offset = 0;
+ vkCmdBindVertexBuffers(cmd, 0, 1, &vkvbuf, &offset);
+ vkCmdBindIndexBuffer(cmd, vkibuf, 0, VK_INDEX_TYPE_UINT16);
+ vkCmdDrawIndexed(cmd, COUNTOF(idata), 1, 0, 0, 0);
+ }
+
+ // End dynamic rendering
+ vkCmdEndRendering(cmd);
+
+ // Transition swapchain image: color attachment -> present
+ {
+ VkImageMemoryBarrier2 barrier = {
+ .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
+ .srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT,
+ .srcAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT,
+ .dstStageMask = VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT,
+ .dstAccessMask = 0,
+ .oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
+ .newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
+ .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .image = swapchain_images[img_idx],
+ .subresourceRange = (VkImageSubresourceRange){
+ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .baseMipLevel = 0,
+ .levelCount = 1,
+ .baseArrayLayer = 0,
+ .layerCount = 1
+ }
+ };
+
+ VkDependencyInfo dep_info = {
+ .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
+ .imageMemoryBarrierCount = 1,
+ .pImageMemoryBarriers = &barrier
+ };
+ vkCmdPipelineBarrier2(cmd, &dep_info);
+ }
+
+ // Done recording commands
+ vkEndCommandBuffer(cmd);
+
+ // Wait for these semaphores before swapping
+ VkSemaphore wait_sems[] = { vk_image_available_sems[f] };
+
+ // Signal these semaphores after swapping
+ VkSemaphore signal_sems[] = { vk_render_finished_sems[img_idx] };
+
+ // Where to wait for wait_sems
+ VkPipelineStageFlags wait_stages[] = {
+ VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT
+ };
+
+ // Submit
+ {
+ VkSubmitInfo submit_info = {
+ .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
+ .waitSemaphoreCount = COUNTOF(wait_sems),
+ .pWaitSemaphores = wait_sems,
+ .pWaitDstStageMask = wait_stages,
+ .commandBufferCount = 1,
+ .pCommandBuffers = &cmd,
+ .signalSemaphoreCount = COUNTOF(signal_sems),
+ .pSignalSemaphores = signal_sems
+ };
+ vkQueueSubmit(vkq, 1, &submit_info, vk_in_flight_fences[f]);
+ }
+
+ // Present
+ {
+ VkPresentInfoKHR present_info = {
+ .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
+ .waitSemaphoreCount = COUNTOF(signal_sems),
+ .pWaitSemaphores = signal_sems,
+ .swapchainCount = 1,
+ .pSwapchains = &vkswapchain,
+ .pImageIndices = &img_idx
+ };
+ VkResult vkr = vkQueuePresentKHR(vkq, &present_info);
+ if (vkr == VK_ERROR_OUT_OF_DATE_KHR || vkr == VK_SUBOPTIMAL_KHR) {
+ swapchain_dirty = true;
+ }
+ }
+
+ f = (f + 1) % max_frames_in_flight;
+ }
+
+ // the end is never the end is never the end is never the end is never the end is never the end
+
+ return 0;
+}
+
diff --git a/win-resize-mt/build.bat b/win-resize-mt/build.bat
new file mode 100644
index 0000000..45cfb7f
--- /dev/null
+++ b/win-resize-mt/build.bat
@@ -0,0 +1,3 @@
+@echo off
+if not exist bin mkdir bin
+cl /W4 /Od /Zi /Fe:bin\win-resize-mt.exe win-resize-mt.c /Fd:bin\ /Fo:bin\
diff --git a/win-resize-mt/build.sh b/win-resize-mt/build.sh
new file mode 100644
index 0000000..c7c4423
--- /dev/null
+++ b/win-resize-mt/build.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+mkdir -p bin
+cc -o bin/win-resize-mt -Wall -Wextra -Wpedantic -O0 -g ./win-resize-mt.c -ldwmapi -lgdi32
diff --git a/win-resize-mt/win-resize-mt.c b/win-resize-mt/win-resize-mt.c
new file mode 100644
index 0000000..06fb5d0
--- /dev/null
+++ b/win-resize-mt/win-resize-mt.c
@@ -0,0 +1,276 @@
+// ================================================================================================
+// Example of how to draw while resizing / moving a window on Windows in C.
+//
+// Window contents are rendered via GDI, but the behavior is the same no matter what graphics API
+// you use. The original version of this demo (on old-20260427 branch) used D3D11.
+//
+// This demo uses a multiple threads. There's a multi threaded version called win-resize-mt if
+// that's more your style.
+//
+// Build (MSVC):
+// > cl /W4 /Od /Zi win-resize-mt.c /Fe:win-resize-mt.exe
+// Build (GCC/clang):
+// $ cc -o win-resize-mt.exe -Wall -Wextra -Wpedantic -O0 -g win-resize-mt.c -ldwmapi -lgdi32
+//
+// Changelog:
+// 6/2/2026: Fixed potential race condition during resize
+// 5/21/2026: Initial release
+//
+// License:
+// Copyright (c) 2026 Hunter Kvalevog
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE.
+// ================================================================================================
+
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#include <dwmapi.h>
+
+#include <assert.h>
+#include <math.h>
+#include <stdio.h>
+
+#ifdef _MSC_VER
+# pragma comment(lib, "dwmapi.lib")
+# pragma comment(lib, "gdi32.lib")
+# pragma comment(lib, "user32.lib")
+#endif
+
+#define UNUSED(Var) ((void)(Var))
+
+static void GfxInit(HWND wnd);
+static void GfxResize(UINT vpw, UINT vph);
+static void GfxDraw(void);
+
+#define THREAD_BIT_QUIT (1 << 0)
+#define THREAD_BIT_SYNC (1 << 1)
+
+typedef struct ThreadData ThreadData;
+struct ThreadData
+{
+ HWND wnd;
+ HANDLE init_sig;
+ HANDLE sync_sig;
+ volatile LONG bits;
+ volatile LONG size;
+};
+
+//
+static DWORD WINAPI RenderThread(LPVOID opaque)
+{
+ ThreadData *td = opaque;
+ GfxInit(td->wnd);
+ GfxDraw();
+
+ SetEvent(td->init_sig);
+
+ while (TRUE) {
+ const LONG bits = InterlockedOr(&td->bits, 0);
+
+ if (bits & THREAD_BIT_QUIT) {
+ break;
+ }
+
+ const LONG size = InterlockedOr(&td->size, 0);
+ if (size > 0) {
+ GfxResize(LOWORD(size), HIWORD(size));
+ InterlockedExchange(&td->size, 0);
+ }
+
+ GfxDraw();
+
+ if (bits & THREAD_BIT_SYNC) {
+ SetEvent(td->sync_sig);
+ InterlockedAnd(&td->bits, ~THREAD_BIT_SYNC);
+ }
+ }
+
+ return 0;
+}
+
+//
+static LRESULT CALLBACK WndProc(HWND wnd, UINT msg, WPARAM wparam, LPARAM lparam)
+{
+ ThreadData *td = (ThreadData *)GetWindowLongPtrW(wnd, GWLP_USERDATA);
+ switch (msg)
+ {
+ case WM_DESTROY:
+ PostQuitMessage(0);
+ return 0;
+ case WM_SIZE:
+ // Resize
+ InterlockedExchange(&td->size, lparam);
+
+ // Sync
+ InterlockedOr(&td->bits, THREAD_BIT_SYNC);
+ WaitForSingleObject(td->sync_sig, INFINITE);
+ ResetEvent(td->sync_sig);
+ break;
+ }
+ return DefWindowProcW(wnd, msg, wparam, lparam);
+}
+
+//
+int WinMain(HINSTANCE instance, HINSTANCE previnstance, LPSTR cmdline, int cmdshow)
+{
+ UNUSED(previnstance); UNUSED(cmdline);
+
+ // Register window class
+ WNDCLASSEXW wc = { 0 };
+ wc.cbSize = sizeof(wc);
+ wc.hCursor = LoadCursor(0, IDC_ARROW);
+ wc.hInstance = instance;
+ wc.lpfnWndProc = WndProc;
+ wc.lpszClassName = L"win-resize-mt";
+
+ ATOM atom = RegisterClassExW(&wc);
+ assert(atom && "Failed to register window class");
+
+ // Create window
+ HWND wnd = CreateWindowExW(WS_EX_APPWINDOW, wc.lpszClassName, wc.lpszClassName,
+ WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
+ CW_USEDEFAULT, 0, 0, wc.hInstance, 0);
+ assert(wnd && "Failed to create window");
+
+ //
+ ThreadData td = { 0 };
+ td.wnd = wnd;
+ td.init_sig = CreateEvent(0, TRUE, FALSE, 0);
+ td.sync_sig = CreateEvent(0, TRUE, FALSE, 0);
+
+ //
+ HANDLE th = CreateThread(0, 0, RenderThread, &td, 0, 0);
+ assert(th && "Failed to create render thread");
+
+ //
+ SetWindowLongPtrW(wnd, GWLP_USERDATA, (LONG_PTR)&td);
+ WaitForSingleObject(td.init_sig, INFINITE);
+
+ //
+ ShowWindow(wnd, cmdshow);
+
+ //
+ MSG msg;
+ while (GetMessageW(&msg, 0, 0, 0) > 0) {
+ TranslateMessage(&msg);
+ DispatchMessage(&msg);
+ }
+
+ //
+ InterlockedOr(&td.bits, THREAD_BIT_QUIT);
+ WaitForSingleObject(th, INFINITE);
+
+ return 0;
+}
+
+// ================================================================================================
+
+static UINT g_vpw = 0;
+static UINT g_vph = 0;
+static HDC g_hdc = 0;
+static HDC g_hdc_back = 0;
+static HBITMAP g_bmp = 0;
+static HBRUSH g_bgbr = 0;
+static HBRUSH g_fgbr = 0;
+static HPEN g_pen = 0;
+
+static void GfxInit(HWND wnd)
+{
+ g_hdc = GetDC(wnd);
+ g_hdc_back = CreateCompatibleDC(g_hdc);
+ g_bgbr = CreateSolidBrush(RGB(0x00, 0x20, 0x20));
+ g_fgbr = CreateSolidBrush(RGB(0xE0, 0xE0, 0xE0));
+ g_pen = CreatePen(PS_SOLID, 2, RGB(0xFF, 0x10, 0x10));
+
+ RECT rc;
+ GetClientRect(wnd, &rc);
+
+ GfxResize(rc.right, rc.bottom);
+}
+
+static void GfxResize(UINT vpw, UINT vph)
+{
+ if (g_bmp)
+ {
+ DeleteObject(g_bmp);
+ g_bmp = 0;
+ }
+
+ if (vpw > 0 && vph > 0)
+ {
+ g_bmp = CreateCompatibleBitmap(g_hdc, vpw, vph);
+ SelectObject(g_hdc_back, g_bmp);
+ }
+
+ g_vpw = vpw;
+ g_vph = vph;
+}
+
+static void GfxDraw(void)
+{
+ static UINT frame = 0;
+ ++frame;
+
+ if (g_bmp && g_vpw > 0 && g_vph > 0)
+ {
+ HDC hdc = g_hdc_back;
+
+ // Clear
+ {
+ RECT rc = { 0, 0, g_vpw, g_vph };
+ FillRect(hdc, &rc, g_bgbr);
+ }
+
+ // Draw X
+ {
+ SelectObject(hdc, g_pen);
+ MoveToEx(hdc, 0, 0, 0);
+ LineTo(hdc, g_vpw, g_vph);
+ MoveToEx(hdc, 0, g_vph, 0);
+ LineTo(hdc, g_vpw, 0);
+ }
+
+ // Draw rotating triangle
+ {
+ int cx = g_vpw / 2;
+ int cy = g_vph / 2;
+ int r = (g_vpw < g_vph ? g_vpw : g_vph) / 3;
+
+ POINT verts[3];
+ for (int i = 0; i < 3; ++i)
+ {
+ float ang = ((float)frame / 100.0f) + (float)i * (2.0f * 3.14159265f / 3.0f);
+ verts[i].x = cx + (int)(cosf(ang) * r);
+ verts[i].y = cy + (int)(sinf(ang) * r);
+ }
+
+ SelectObject(hdc, g_pen);
+ SelectObject(hdc, g_fgbr);
+ Polygon(hdc, verts, ARRAYSIZE(verts));
+ }
+
+ // Draw text
+ {
+ WCHAR txtbuf[256];
+ _snwprintf_s(txtbuf, ARRAYSIZE(txtbuf), _TRUNCATE, L"win-resize-mt (frame %u)", frame);
+
+ SIZE sz;
+ GetTextExtentPoint32W(hdc, txtbuf, (int)wcslen(txtbuf), &sz);
+
+ SetBkMode(hdc, TRANSPARENT);
+ SetTextColor(hdc, RGB(0xE0, 0xE0, 0xE0));
+ TextOutW(hdc, (g_vpw - sz.cx) / 2, 19, txtbuf, (int)wcslen(txtbuf));
+ }
+
+ // Present
+ BitBlt(g_hdc, 0, 0, g_vpw, g_vph, g_hdc_back, 0, 0, SRCCOPY);
+
+ // Wait for dwm to display a frame
+ DwmFlush();
+ }
+}
+
diff --git a/win-resize-st/build.bat b/win-resize-st/build.bat
new file mode 100644
index 0000000..8568ca2
--- /dev/null
+++ b/win-resize-st/build.bat
@@ -0,0 +1,3 @@
+@echo off
+if not exist bin mkdir bin
+cl /W4 /Od /Zi /Fe:bin\win-resize-st.exe win-resize-st.c /Fd:bin\ /Fo:bin\
diff --git a/win-resize-st/build.sh b/win-resize-st/build.sh
new file mode 100644
index 0000000..d644837
--- /dev/null
+++ b/win-resize-st/build.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+mkdir -p bin
+cc -o bin/win-resize-st -Wall -Wextra -Wpedantic -O0 -g ./win-resize-st.c -ldwmapi -lgdi32
diff --git a/win-resize/win-resize.c b/win-resize-st/win-resize-st.c
index 275760e..fe6cf97 100644
--- a/win-resize/win-resize.c
+++ b/win-resize-st/win-resize-st.c
@@ -1,229 +1,227 @@
-// ================================================================================================
-// Example of how to draw while resizing / moving a window on Windows in C.
-//
-// Window contents are rendered via GDI, but the behavior is the same no matter what graphics API
-// you use. The original version of this demo (on old-20260427 branch) used D3D11.
-//
-// Pretty much all modern Direct3D Windows apps use a render thread and therefore don't have to
-// deal with this problem. That strategy doesn't work with APIs that have weird thread affinity
-// issues (OpenGL), so this is one way of doing it without multithreading.
-//
-// Build (MSVC):
-// > cl /W4 /Od /Zi win-resize.c /Fe:win-resize.exe
-// Build (GCC/clang):
-// $ cc -o win-resize.exe -Wall -Wextra -Wpedantic -O0 -g win-resize.c -ldwmapi -lgdi32
-//
-// Changelog:
-// 5/15/2026: Initial release
-//
-// License:
-// SPDX-License-Identifier: 0BSD
-// Copyright (c) 2026 Hunter Kvalevog
-//
-// Permission to use, copy, modify, and/or distribute this software for any
-// purpose with or without fee is hereby granted.
-//
-// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-// WITH REGARD TO THIS SOFTWARE.
-// ================================================================================================
-
-#define WIN32_LEAN_AND_MEAN
-#include <windows.h>
-#include <dwmapi.h>
-
-#include <assert.h>
-#include <math.h>
-#include <stdio.h>
-
-#ifdef _MSC_VER
-# pragma comment(lib, "dwmapi.lib")
-# pragma comment(lib, "gdi32.lib")
-# pragma comment(lib, "user32.lib")
-#endif
-
-#define UNUSED(Var) ((void)(Var))
-
-static void GfxInit(HWND wnd);
-static void GfxResize(UINT vpw, UINT vph);
-static void GfxDraw(void);
-
-//
-static LRESULT CALLBACK WndProc(HWND wnd, UINT msg, WPARAM wparam, LPARAM lparam)
-{
- switch (msg)
- {
- case WM_DESTROY:
- PostQuitMessage(0);
- return 0;
- case WM_SIZE:
- GfxResize(LOWORD(lparam), HIWORD(lparam));
- GfxDraw();
- break;
- case WM_ENTERSIZEMOVE:
- SetTimer(wnd, 1, USER_TIMER_MINIMUM, 0);
- break;
- case WM_EXITSIZEMOVE:
- KillTimer(wnd, 1);
- break;
- case WM_TIMER:
- GfxDraw();
- break;
- }
- return DefWindowProcW(wnd, msg, wparam, lparam);
-}
-
-//
-int WinMain(HINSTANCE instance, HINSTANCE previnstance, LPSTR cmdline, int cmdshow)
-{
- UNUSED(previnstance); UNUSED(cmdline);
-
- // Register window class
- WNDCLASSEXW wc = { };
- wc.cbSize = sizeof(wc);
- wc.hCursor = LoadCursor(0, IDC_ARROW);
- wc.hInstance = instance;
- wc.lpfnWndProc = WndProc;
- wc.lpszClassName = L"win-resize";
-
- ATOM atom = RegisterClassExW(&wc);
- assert(atom && "Failed to register window class");
-
- // Create window
- HWND wnd = CreateWindowExW(WS_EX_APPWINDOW, wc.lpszClassName, wc.lpszClassName,
- WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
- CW_USEDEFAULT, 0, 0, wc.hInstance, 0);
- assert(wnd && "Failed to create window");
-
- //
- GfxInit(wnd);
-
- //
- ShowWindow(wnd, cmdshow);
-
- // Run a typical D3D-style window loop
- BOOL quit = FALSE;
- while (!quit)
- {
- // Pump the message loop
- MSG msg;
- while (PeekMessageW(&msg, 0, 0, 0, PM_REMOVE))
- {
- quit |= (msg.message == WM_QUIT);
- TranslateMessage(&msg);
- DispatchMessageW(&msg);
- }
-
- // Update the screen
- GfxDraw();
- }
-
- return 0;
-}
-
-// ================================================================================================
-
-static UINT g_vpw = 0;
-static UINT g_vph = 0;
-static HDC g_hdc = 0;
-static HDC g_hdc_back = 0;
-static HBITMAP g_bmp = 0;
-static HBRUSH g_bgbr = 0;
-static HBRUSH g_fgbr = 0;
-static HPEN g_pen = 0;
-
-static void GfxInit(HWND wnd)
-{
- g_hdc = GetDC(wnd);
- g_hdc_back = CreateCompatibleDC(g_hdc);
- g_bgbr = CreateSolidBrush(RGB(0x00, 0x20, 0x20));
- g_fgbr = CreateSolidBrush(RGB(0xE0, 0xE0, 0xE0));
- g_pen = CreatePen(PS_SOLID, 2, RGB(0xFF, 0x10, 0x10));
-
- RECT rc;
- GetClientRect(wnd, &rc);
-
- GfxResize(rc.right, rc.bottom);
-}
-
-static void GfxResize(UINT vpw, UINT vph)
-{
- if (g_bmp)
- {
- DeleteObject(g_bmp);
- g_bmp = 0;
- }
-
- if (vpw > 0 && vph > 0)
- {
- g_bmp = CreateCompatibleBitmap(g_hdc, vpw, vph);
- SelectObject(g_hdc_back, g_bmp);
- }
-
- g_vpw = vpw;
- g_vph = vph;
-}
-
-static void GfxDraw(void)
-{
- static UINT frame = 0;
- ++frame;
-
- if (g_bmp && g_vpw > 0 && g_vph > 0)
- {
- HDC hdc = g_hdc_back;
-
- // Clear
- {
- RECT rc = { 0, 0, g_vpw, g_vph };
- FillRect(hdc, &rc, g_bgbr);
- }
-
- // Draw X
- {
- SelectObject(hdc, g_pen);
- MoveToEx(hdc, 0, 0, 0);
- LineTo(hdc, g_vpw, g_vph);
- MoveToEx(hdc, 0, g_vph, 0);
- LineTo(hdc, g_vpw, 0);
- }
-
- // Draw rotating triangle
- {
- int cx = g_vpw / 2;
- int cy = g_vph / 2;
- int r = (g_vpw < g_vph ? g_vpw : g_vph) / 3;
-
- POINT verts[3];
- for (int i = 0; i < 3; ++i)
- {
- float ang = ((float)frame / 100.0f) + (float)i * (2.0f * 3.14159265f / 3.0f);
- verts[i].x = cx + (int)(cosf(ang) * r);
- verts[i].y = cy + (int)(sinf(ang) * r);
- }
-
- SelectObject(hdc, g_pen);
- SelectObject(hdc, g_fgbr);
- Polygon(hdc, verts, ARRAYSIZE(verts));
- }
-
- // Draw text
- {
- WCHAR txtbuf[256];
- _snwprintf_s(txtbuf, ARRAYSIZE(txtbuf), _TRUNCATE, L"win-resize (frame %u)", frame);
-
- SIZE sz;
- GetTextExtentPoint32W(hdc, txtbuf, (int)wcslen(txtbuf), &sz);
-
- SetBkMode(hdc, TRANSPARENT);
- SetTextColor(hdc, RGB(0xE0, 0xE0, 0xE0));
- TextOutW(hdc, (g_vpw - sz.cx) / 2, 19, txtbuf, (int)wcslen(txtbuf));
- }
-
- // Present
- BitBlt(g_hdc, 0, 0, g_vpw, g_vph, g_hdc_back, 0, 0, SRCCOPY);
-
- // Wait for dwm to display a frame
- DwmFlush();
- }
-}
-
+// ================================================================================================
+// Example of how to draw while resizing / moving a window on Windows in C.
+//
+// Window contents are rendered via GDI, but the behavior is the same no matter what graphics API
+// you use. The original version of this demo (on old-20260427 branch) used D3D11.
+//
+// This demo uses a single thread. There's a multi threaded version called win-resize-mt if that's
+// more your style.
+//
+// Build (MSVC):
+// > cl /W4 /Od /Zi win-resize-st.c /Fe:win-resize-st.exe
+// Build (GCC/clang):
+// $ cc -o win-resize-st.exe -Wall -Wextra -Wpedantic -O0 -g win-resize-st.c -ldwmapi -lgdi32
+//
+// Changelog:
+// 5/15/2026: Initial release
+//
+// License:
+// Copyright (c) 2026 Hunter Kvalevog
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE.
+// ================================================================================================
+
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#include <dwmapi.h>
+
+#include <assert.h>
+#include <math.h>
+#include <stdio.h>
+
+#ifdef _MSC_VER
+# pragma comment(lib, "dwmapi.lib")
+# pragma comment(lib, "gdi32.lib")
+# pragma comment(lib, "user32.lib")
+#endif
+
+#define UNUSED(Var) ((void)(Var))
+
+static void GfxInit(HWND wnd);
+static void GfxResize(UINT vpw, UINT vph);
+static void GfxDraw(void);
+
+//
+static LRESULT CALLBACK WndProc(HWND wnd, UINT msg, WPARAM wparam, LPARAM lparam)
+{
+ switch (msg)
+ {
+ case WM_DESTROY:
+ PostQuitMessage(0);
+ return 0;
+ case WM_SIZE:
+ GfxResize(LOWORD(lparam), HIWORD(lparam));
+ GfxDraw();
+ break;
+ case WM_ENTERSIZEMOVE:
+ SetTimer(wnd, 1, USER_TIMER_MINIMUM, 0);
+ break;
+ case WM_EXITSIZEMOVE:
+ KillTimer(wnd, 1);
+ break;
+ case WM_TIMER:
+ GfxDraw();
+ break;
+ }
+ return DefWindowProcW(wnd, msg, wparam, lparam);
+}
+
+//
+int WinMain(HINSTANCE instance, HINSTANCE previnstance, LPSTR cmdline, int cmdshow)
+{
+ UNUSED(previnstance); UNUSED(cmdline);
+
+ // Register window class
+ WNDCLASSEXW wc = { 0 };
+ wc.cbSize = sizeof(wc);
+ wc.hCursor = LoadCursor(0, IDC_ARROW);
+ wc.hInstance = instance;
+ wc.lpfnWndProc = WndProc;
+ wc.lpszClassName = L"win-resize-st";
+
+ ATOM atom = RegisterClassExW(&wc);
+ assert(atom && "Failed to register window class");
+
+ // Create window
+ HWND wnd = CreateWindowExW(WS_EX_APPWINDOW, wc.lpszClassName, wc.lpszClassName,
+ WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
+ CW_USEDEFAULT, 0, 0, wc.hInstance, 0);
+ assert(wnd && "Failed to create window");
+
+ //
+ GfxInit(wnd);
+
+ //
+ ShowWindow(wnd, cmdshow);
+
+ // Run a typical D3D-style window loop
+ BOOL quit = FALSE;
+ while (!quit)
+ {
+ // Pump the message loop
+ MSG msg;
+ while (PeekMessageW(&msg, 0, 0, 0, PM_REMOVE))
+ {
+ quit |= (msg.message == WM_QUIT);
+ TranslateMessage(&msg);
+ DispatchMessageW(&msg);
+ }
+
+ // Update the screen
+ GfxDraw();
+ }
+
+ return 0;
+}
+
+// ================================================================================================
+
+static UINT g_vpw = 0;
+static UINT g_vph = 0;
+static HDC g_hdc = 0;
+static HDC g_hdc_back = 0;
+static HBITMAP g_bmp = 0;
+static HBRUSH g_bgbr = 0;
+static HBRUSH g_fgbr = 0;
+static HPEN g_pen = 0;
+
+static void GfxInit(HWND wnd)
+{
+ g_hdc = GetDC(wnd);
+ g_hdc_back = CreateCompatibleDC(g_hdc);
+ g_bgbr = CreateSolidBrush(RGB(0x00, 0x20, 0x20));
+ g_fgbr = CreateSolidBrush(RGB(0xE0, 0xE0, 0xE0));
+ g_pen = CreatePen(PS_SOLID, 2, RGB(0xFF, 0x10, 0x10));
+
+ RECT rc;
+ GetClientRect(wnd, &rc);
+
+ GfxResize(rc.right, rc.bottom);
+}
+
+static void GfxResize(UINT vpw, UINT vph)
+{
+ if (g_bmp)
+ {
+ DeleteObject(g_bmp);
+ g_bmp = 0;
+ }
+
+ if (vpw > 0 && vph > 0)
+ {
+ g_bmp = CreateCompatibleBitmap(g_hdc, vpw, vph);
+ SelectObject(g_hdc_back, g_bmp);
+ }
+
+ g_vpw = vpw;
+ g_vph = vph;
+}
+
+static void GfxDraw(void)
+{
+ static UINT frame = 0;
+ ++frame;
+
+ if (g_bmp && g_vpw > 0 && g_vph > 0)
+ {
+ HDC hdc = g_hdc_back;
+
+ // Clear
+ {
+ RECT rc = { 0, 0, g_vpw, g_vph };
+ FillRect(hdc, &rc, g_bgbr);
+ }
+
+ // Draw X
+ {
+ SelectObject(hdc, g_pen);
+ MoveToEx(hdc, 0, 0, 0);
+ LineTo(hdc, g_vpw, g_vph);
+ MoveToEx(hdc, 0, g_vph, 0);
+ LineTo(hdc, g_vpw, 0);
+ }
+
+ // Draw rotating triangle
+ {
+ int cx = g_vpw / 2;
+ int cy = g_vph / 2;
+ int r = (g_vpw < g_vph ? g_vpw : g_vph) / 3;
+
+ POINT verts[3];
+ for (int i = 0; i < 3; ++i)
+ {
+ float ang = ((float)frame / 100.0f) + (float)i * (2.0f * 3.14159265f / 3.0f);
+ verts[i].x = cx + (int)(cosf(ang) * r);
+ verts[i].y = cy + (int)(sinf(ang) * r);
+ }
+
+ SelectObject(hdc, g_pen);
+ SelectObject(hdc, g_fgbr);
+ Polygon(hdc, verts, ARRAYSIZE(verts));
+ }
+
+ // Draw text
+ {
+ WCHAR txtbuf[256];
+ _snwprintf_s(txtbuf, ARRAYSIZE(txtbuf), _TRUNCATE, L"win-resize-st (frame %u)", frame);
+
+ SIZE sz;
+ GetTextExtentPoint32W(hdc, txtbuf, (int)wcslen(txtbuf), &sz);
+
+ SetBkMode(hdc, TRANSPARENT);
+ SetTextColor(hdc, RGB(0xE0, 0xE0, 0xE0));
+ TextOutW(hdc, (g_vpw - sz.cx) / 2, 19, txtbuf, (int)wcslen(txtbuf));
+ }
+
+ // Present
+ BitBlt(g_hdc, 0, 0, g_vpw, g_vph, g_hdc_back, 0, 0, SRCCOPY);
+
+ // Wait for dwm to display a frame
+ DwmFlush();
+ }
+}
+
diff --git a/win-resize/build.bat b/win-resize/build.bat
deleted file mode 100644
index 14ca49c..0000000
--- a/win-resize/build.bat
+++ /dev/null
@@ -1,3 +0,0 @@
-@echo off
-if not exist bin mkdir bin
-cl /W4 /Od /Zi /Fe:bin\win-resize.exe win-resize.c /Fd:bin\ /Fo:bin\
diff --git a/win-resize/build.sh b/win-resize/build.sh
deleted file mode 100755
index 4d3a4ab..0000000
--- a/win-resize/build.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/bash
-mkdir -p bin
-cc -o bin/win-resize -Wall -Wextra -Wpedantic -O0 -g ./win-resize.c -ldwmapi -lgdi32