summaryrefslogtreecommitdiff
path: root/vk-cube/vk-cube.c
diff options
context:
space:
mode:
Diffstat (limited to 'vk-cube/vk-cube.c')
-rw-r--r--vk-cube/vk-cube.c1285
1 files changed, 1285 insertions, 0 deletions
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;
+}
+