diff options
| -rw-r--r-- | README | 26 | ||||
| -rwxr-xr-x | vk-cube/build.sh | 6 | ||||
| -rw-r--r-- | vk-cube/vk-cube-fs.glsl | 47 | ||||
| -rw-r--r-- | vk-cube/vk-cube-vs.glsl | 42 | ||||
| -rw-r--r-- | vk-cube/vk-cube.c | 1285 | ||||
| -rw-r--r-- | win-resize-mt/build.bat | 3 | ||||
| -rw-r--r-- | win-resize-mt/build.sh | 3 | ||||
| -rw-r--r-- | win-resize-mt/win-resize-mt.c | 276 | ||||
| -rw-r--r-- | win-resize-st/build.bat | 3 | ||||
| -rw-r--r-- | win-resize-st/build.sh | 3 | ||||
| -rw-r--r-- | win-resize-st/win-resize-st.c (renamed from win-resize/win-resize.c) | 456 | ||||
| -rw-r--r-- | win-resize/build.bat | 3 | ||||
| -rwxr-xr-x | win-resize/build.sh | 3 |
13 files changed, 1908 insertions, 248 deletions
@@ -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 |