commit 619ca99bc33edf90feecb096e798a4425ffae974
Author: MikoĊaj Lenczewski <mikolaj@lenczewski.org>
Date: Mon, 12 Oct 2026 00:35:36 +0100
Initial commit
Diffstat:
18 files changed, 2247 insertions(+), 0 deletions(-)
diff --git a/README b/README
@@ -0,0 +1,3 @@
+wintoys
+==============================================================================
+A collection of older pieces of code I had written for windows.
diff --git a/gldemo/build.bat b/gldemo/build.bat
@@ -0,0 +1,23 @@
+@echo off
+
+REM -- remember to call setenv.bat before running this, to ensure that MSVC is
+REM set up correctly for the build environment (x64)
+
+REM -- SDK location, set as appropriate
+set GLSDK=C:\OpenGL_ES_SDK\xxx
+
+REM -- point these at the correct directories
+set GLINCDIR=%GLSDK%\yyy
+set GLLIBDIR=%GLSDK%\zzz
+
+set INCS=/I%GLINCDIR%
+set LIBS=user32.lib gdi32.lib /LIBPATH:%GLLIBDIR% www.lib
+
+set CFLAGS=/std:c11 /TC /WX /W4 /Od /Oi /Zi
+set CPPFLAGS=/I %VKINCDIR%
+set LDFLAGS=/SUBSYSTEM:WINDOWS,5.2 /INCREMENTAL:NO /OPT:REF /WX
+
+set SOURCE=win32_gldemo.c
+set TARGET=win32_gldemo.exe
+
+cl -nologo %CFLAGS% %CPPFLAGS% %INCS% %SOURCE% /link %LDFLAGS% %LIBS% /OUT:%TARGET%
diff --git a/gldemo/setenv.bat b/gldemo/setenv.bat
@@ -0,0 +1,6 @@
+@echo off
+
+REM -- location of MSVC toolchain setup file, set this as required
+set VCVARS="C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat"
+
+%VCVARS% x64
diff --git a/gldemo/win32_gldemo.c b/gldemo/win32_gldemo.c
@@ -0,0 +1,214 @@
+#define WIN32_LEAN_AND_MEAN 1
+#include <windows.h>
+
+#include <EGL/egl.h>
+// #include <EGL/eglplatform.h>
+// #include <GLES2/gl2.h>
+
+static void
+win32_process_messages(void);
+
+LRESULT CALLBACK
+win32_window_proc(HWND window, UINT msg, WPARAM wparam, LPARAM lparam);
+
+static void
+render_init(void);
+
+static void
+render(UINT width, UINT height, EGLDisplay egl_display, EGLSurface egl_surface);
+
+static bool win32_running;
+static HWND win32_window;
+
+int WINAPI
+WinMain(HINSTANCE instance, HINSTANCE prev_instance, PSTR cmdline, int cmdshown)
+{
+ (void) instance;
+ (void) prev_instance;
+ (void) cmdline;
+ (void) cmdshown;
+
+ const char classname[] = "gldemo_wndcls";
+
+ // https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa
+ WNDCLASSEXA cls = {
+ .cbSize = sizeof(WNDCLASSEXA),
+ .style = CS_OWNDC | CS_VREDRAW | CS_HREDRAW,
+ .lpfnWndProc = win32_window_proc,
+ .hInstance = instance,
+ .lpszClassName = classname,
+ };
+
+ // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclassexa
+ RegisterClassExA(&cls);
+
+ // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexa
+ win32_window = CreateWindowExA(
+ 0,
+ classname,
+ "gldemo",
+ WS_OVERLAPPEDWINDOW,
+ CW_USEDEFAULT,
+ CW_USEDEFAULT,
+ CW_USEDEFAULT,
+ CW_USEDEFAULT,
+ NULL,
+ NULL,
+ instance,
+ NULL);
+
+ if (!win32_window) {
+ fprintf(stderr, "Failed to create Win32 window!\n");
+ return 1;
+ }
+
+ ShowWindow(win32_window, 1);
+
+ // get a handle to the win32 display context
+ HDC egl_hdc = GetDC(win32_window);
+
+ // wrap the HDC on the EGL side
+ EGLDisplay egl_display = eglGetDisplay(egl_hdc);
+
+ // find out what version of EGL we support
+ EGLint major, minor;
+ eglInitialize(egl_display, &major, &minor);
+ fprintf(stderr, "EGL version: %d.%d\n", major, minor);
+
+ // initialise the OpenGL ES version of the GL api
+ // NOTE: this is an alternative version of the OpenGL API that is more
+ // limited, but runs on more devices. consider using this only if
+ // learning OpenGL as it is more transferrable and closer to bare metal
+ // programming! If you would want to move to Vulkan later, it might be
+ // easier to do so having learnt OpenGL ES
+ eglBindAPI(EGL_OPENGL_ES_API);
+
+ // NOTE: if you want the original OpenGL api, use this. it may require
+ // changing some of the context attrs we define later as well to avoid
+ // errors
+ // eglBindAPI(EGL_OPENGL_API);
+
+ // setup some parameters for the API that we expect to hold
+ EGLint config_attrs[] = {
+ EGL_RED_SIZE, 8,
+ EGL_GREEN_SIZE, 8,
+ EGL_BLUE_SIZE, 8,
+ EGL_ALPHA_SIZE, 8,
+ EGL_NONE,
+ };
+
+ // select an API config that matches the parameters we wanted (if any)
+ EGLConfig config;
+ EGLint num_config;
+ eglChooseConfig(egl_display, config_attrs, &config, 1, &num_config);
+
+ // create a surface that allows egl to write to our win32 window
+ EGLint surface_attrs[] = {
+ EGL_NONE,
+ };
+ EGLSurfaec egl_surface = eglCreateWindowSurface(egl_display, config, win32_window, surface_attrs);
+
+ // create an egl context that our GL commands will operate on
+ EGLint context_attrs[] = {
+ EGL_CONTEXT_CLIENT_VERSION, 2,
+ EGL_NONE,
+ };
+ EGLContext egl_context = eglCreateContext(egl_display, config, EGL_NO_CONTEXT, context_attrs);
+
+ // set our current egl display, surface, and context as the ones all
+ // GL commands from the current thread should operate on
+ eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context);
+
+ render_init();
+
+ LARGE_INTEGER perf_frequency, start_perf_count, end_perf_count;
+ QueryPerformanceFrequency(&perf_frequency);
+
+ win32_running = true;
+ while (win32_running) {
+ QueryPerformanceCounter(&start_perf_count);
+ uint64_t start_cycles = __rdtsc();
+
+ win32_process_messages();
+
+ GetClientRect(win32_window, &rect);
+ render(rect.right - rect.left, recv.bottom - rect.top);
+
+ uint64_t end_cycles = __rdtsc();
+ QueryPerformanceCounter(&end_perf_count);
+
+ const uint64_t micros = 1000 * 1000;
+ uint64_t elapsed_micros = (end_perf_count.QuadPart - start_perf_count.QuadPart) * micros / perf_frequency.QuadPart;
+ uint64_t elapsed_cycles = end_cycles - start_cycles;
+
+ char buf[256];
+ snprintf(buf, sizeof buf, "Frame Timings: %" PRIu64 "us, %" PRIu64 " Cy\n", elapsed_micros, elapsed_cycles);
+ OutputDebugString(buf);
+ }
+
+ return 0;
+}
+
+static void
+win32_process_messages(void)
+{
+ MSG msg;
+ while (PeekMessageA(&msg, win32_window, 0, 0, PM_REMOVE)) {
+ switch (msg.message) {
+ default:
+ break;
+ }
+
+ TranslateMessage(&msg);
+ DispatchMessageA(&msg);
+ }
+}
+
+LRESULT CALLBACK
+win32_window_proc(HWND window, UINT msg, WPARAM wparam, LPARAM lparam)
+{
+ switch (msg) {
+ case WM_QUIT:
+ case WM_DESTROY: {
+ win32_running = false;
+ } break;
+
+ default:
+ return DefWindowProcA(window, msg, wparam, lparam);
+ }
+
+ return 0;
+}
+
+// actual rendering implementation
+// ===========================================================================
+
+// this performs one-time setup for our GL render pipeline
+static void
+render_init(void)
+{
+ glClearColor(0.0f, 0.0f, 0.2f, 1.0f);
+}
+
+// this is called for every frame that we want to render with our pipeline
+static void
+render(UINT width, UINT height, EGLDisplay egl_display, EGLSurface egl_surface)
+{
+ // select our entire window as our render viewport
+ glViewport(0, 0, width, height);
+
+ // clear the current viewport to the previously set glClearColor()
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+ // log any errors encountered during rendering this frame
+ GLuint error = glGetError();
+ if (error != GL_NO_ERROR) {
+ fprintf(stderr, "OpenGL Error : %d\n", error);
+ }
+
+ // finish rendering, and wait for vsync before drawing this frame
+ eglSwapBuffer(egl_display, egl_surface);
+
+ fprintf(stderr, "Rendered frame!\n");
+}
+
diff --git a/vkdemo/build.bat b/vkdemo/build.bat
@@ -0,0 +1,28 @@
+@echo off
+
+REM -- remember to call setenv.bat before running this, to ensure that MSVC is
+REM set up correctly for the build environment (x64)
+
+REM -- vulkan SDK location, set as appropriate
+set VKSDK=C:\VulkanSDK\1.3.268.0
+
+set VKINCDIR=%VKSDK%\Include
+set VKLIBDIR=%VKSDK%\Lib
+
+set SHADERC=%VKSDK%\Bin\glslc.exe
+set SHADERCFLAGS=-g -mfmt=c
+
+%SHADERC% shader.vert %SHADERCFLAGS% -o shader.vert.spv.inc
+%SHADERC% shader.frag %SHADERCFLAGS% -o shader.frag.spv.inc
+
+set INCS=/I%VKINCDIR%
+set LIBS=user32.lib gdi32.lib /LIBPATH:%VKLIBDIR% vulkan-1.lib
+
+set CFLAGS=/std:c11 /TC /WX /W4 /Od /Oi /Zi
+set CPPFLAGS=/I %VKINCDIR%
+set LDFLAGS=/SUBSYSTEM:WINDOWS,5.2 /INCREMENTAL:NO /OPT:REF /WX
+
+set SOURCE=win32_vkdemo.c
+set TARGET=win32_vkdemo.exe
+
+cl -nologo %CFLAGS% %CPPFLAGS% %INCS% %SOURCE% /link %LDFLAGS% %LIBS% /OUT:%TARGET%
diff --git a/vkdemo/setenv.bat b/vkdemo/setenv.bat
@@ -0,0 +1,6 @@
+@echo off
+
+REM -- location of MSVC toolchain setup file, set this as required
+set VCVARS="C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat"
+
+%VCVARS% x64
diff --git a/vkdemo/shader.frag b/vkdemo/shader.frag
@@ -0,0 +1,8 @@
+#version 450
+
+layout(location = 0) in vec3 fragColor;
+layout(location = 0) out vec4 outColor;
+
+void main() {
+ outColor = vec4(fragColor, 1.0);
+}
diff --git a/vkdemo/shader.vert b/vkdemo/shader.vert
@@ -0,0 +1,20 @@
+#version 450
+
+layout(location = 0) out vec3 fragColor;
+
+vec2 positions[3] = vec2[](
+ vec2(0.0, -0.5),
+ vec2(0.5, 0.5),
+ vec2(-0.5, 0.5)
+);
+
+vec3 colors[3] = vec3[](
+ vec3(1.0, 0.0, 0.0),
+ vec3(0.0, 1.0, 0.0),
+ vec3(0.0, 0.0, 1.0)
+);
+
+void main() {
+ gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
+ fragColor = colors[gl_VertexIndex];
+}
diff --git a/vkdemo/vkdemo.c b/vkdemo/vkdemo.c
@@ -0,0 +1,775 @@
+#include "vkdemo.h"
+
+#define MAX_FRAMES_IN_FLIGHT 2
+
+static uint32_t vert_shader[] =
+#include "shader.vert.spv.inc"
+;
+
+static uint32_t frag_shader[] =
+#include "shader.frag.spv.inc"
+;
+
+static inline void *
+vkdemo_memory_arena_alloc(struct vkdemo_memory_arena *self, uint64_t size, uint64_t align)
+{
+ assert(align);
+ assert(align % 2 == 0);
+
+ uint64_t off = align - 1;
+ uint64_t mask = ~off;
+
+ uintptr_t aligned_len = (self->len + off) & mask;
+ if (self->cap < aligned_len + size)
+ return NULL;
+
+ void *aligned_ptr = (uint8_t *) self->ptr + aligned_len;
+ self->len = aligned_len;
+
+ return aligned_ptr;
+}
+
+struct vkdemo_state {
+ struct {
+ VkInstance instance;
+ VkSurfaceKHR surface;
+
+ VkPhysicalDevice physical_device;
+ VkDevice device;
+
+ VkQueue graphics_queue;
+ VkQueue present_queue;
+
+ VkSwapchainKHR swapchain;
+ VkFormat swapchain_image_format;
+ VkExtent2D swapchain_extent;
+
+ struct {
+ VkImage *ptr;
+ uint32_t len;
+ } swapchain_images;
+
+ struct {
+ VkImageView *ptr;
+ uint32_t len;
+ } swapchain_image_views;
+
+ struct {
+ VkFramebuffer *ptr;
+ uint32_t len;
+ } swapchain_framebuffers;
+
+ VkRenderPass render_pass;
+ VkPipelineLayout pipeline_layout;
+ VkPipeline pipeline;
+
+ VkCommandPool command_pool;
+ VkCommandBuffer command_buffers[MAX_FRAMES_IN_FLIGHT];
+
+ struct {
+ VkSemaphore image_available[MAX_FRAMES_IN_FLIGHT];
+ VkSemaphore render_finished[MAX_FRAMES_IN_FLIGHT];
+ VkFence in_flight[MAX_FRAMES_IN_FLIGHT];
+ } sync_objects;
+
+ uint32_t frame_index;
+ } vk;
+};
+
+struct queue_family_indices {
+ uint32_t graphics_family;
+ bool has_graphics_family;
+
+ uint32_t present_family;
+ bool has_present_family;
+};
+
+static struct queue_family_indices
+get_device_queue_families(VkPhysicalDevice device, VkSurfaceKHR surface)
+{
+ struct queue_family_indices result = {0};
+
+ uint32_t queue_family_count;
+ vkGetPhysicalDeviceQueueFamilyProperties(device, &queue_family_count, NULL);
+
+ VkQueueFamilyProperties *queue_families = _alloca(queue_family_count * sizeof *queue_families);
+ vkGetPhysicalDeviceQueueFamilyProperties(device, &queue_family_count, queue_families);
+
+ for (uint32_t i = 0; i < queue_family_count; i++) {
+ if (queue_families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
+ result.has_graphics_family = true;
+ result.graphics_family = i;
+ }
+
+ VkBool32 present_support = false;
+ vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &present_support);
+ if (present_support) {
+ result.has_present_family = true;
+ result.present_family = i;
+ }
+
+ if (result.has_graphics_family && result.has_present_family)
+ break;
+ }
+
+ return result;
+}
+
+static bool
+is_device_suitable(VkPhysicalDevice device, VkSurfaceKHR surface, const char **device_extensions, uint32_t device_extensions_len)
+{
+ struct queue_family_indices indices = get_device_queue_families(device, surface);
+
+ // NOTE: we require a device to have both graphics and present capability for its queue
+ if (!indices.has_graphics_family || !indices.has_present_family)
+ return false;
+
+ uint32_t extension_count;
+ vkEnumerateDeviceExtensionProperties(device, NULL, &extension_count, NULL);
+
+ VkExtensionProperties *extensions = _alloca(extension_count * sizeof *extensions);
+ vkEnumerateDeviceExtensionProperties(device, NULL, &extension_count, extensions);
+
+ for (uint32_t i = 0; i < device_extensions_len; i++) {
+ for (uint32_t j = 0; j < extension_count; j++) {
+ if (strcmp(device_extensions[i], extensions[j].extensionName) == 0)
+ goto extension_found;
+ }
+
+ // NOTE: device does not support a required extension
+ return false;
+
+extension_found:
+ continue;
+ }
+
+ uint32_t format_count;
+ vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &format_count, NULL);
+ if (!format_count) return false;
+
+ uint32_t present_mode_count;
+ vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &present_mode_count, NULL);
+ if (!present_mode_count) return false;
+
+ return true;
+}
+
+static VkExtent2D
+get_swapchain_extent(VkSurfaceCapabilitiesKHR *surface_caps, uint32_t width, uint32_t height)
+{
+ if (surface_caps->currentExtent.width == UINT32_MAX && surface_caps->currentExtent.height == UINT32_MAX) {
+ return (VkExtent2D) {
+ .width = CLAMP(width, surface_caps->minImageExtent.width, surface_caps->maxImageExtent.width),
+ .height = CLAMP(height, surface_caps->minImageExtent.height, surface_caps->maxImageExtent.height),
+ };
+ } else {
+ return surface_caps->currentExtent;
+ }
+}
+
+static VkSurfaceFormatKHR
+get_swapchain_format(VkSurfaceFormatKHR *formats, uint64_t len)
+{
+ for (uint64_t i = 0; i < len; i++) {
+ if (formats[i].format == VK_FORMAT_B8G8R8A8_SRGB && formats[i].colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)
+ return formats[i];
+ }
+
+ return formats[0];
+}
+
+static VkPresentModeKHR
+get_swapchain_present_mode(VkPresentModeKHR *present_modes, uint64_t len)
+{
+ for (uint64_t i = 0; i < len; i++) {
+ if (present_modes[i] == VK_PRESENT_MODE_MAILBOX_KHR)
+ return present_modes[i];
+ }
+
+ return VK_PRESENT_MODE_FIFO_KHR;
+}
+
+static bool
+recreate_swapchain(struct vkdemo_memory *memory, struct vkdemo_state *state, uint32_t width, uint32_t height, VkSwapchainKHR old_swapchain)
+{
+ struct queue_family_indices indices = get_device_queue_families(state->vk.physical_device, state->vk.surface);
+ assert(indices.has_graphics_family && indices.has_present_family);
+
+ VkSurfaceCapabilitiesKHR surface_caps;
+ vkGetPhysicalDeviceSurfaceCapabilitiesKHR(state->vk.physical_device, state->vk.surface, &surface_caps);
+
+ uint32_t format_count;
+ vkGetPhysicalDeviceSurfaceFormatsKHR(state->vk.physical_device, state->vk.surface, &format_count, NULL);
+
+ assert(format_count);
+ VkSurfaceFormatKHR *formats = _alloca(format_count * sizeof *formats);
+ vkGetPhysicalDeviceSurfaceFormatsKHR(state->vk.physical_device, state->vk.surface, &format_count, formats);
+
+ uint32_t present_mode_count;
+ vkGetPhysicalDeviceSurfacePresentModesKHR(state->vk.physical_device, state->vk.surface, &present_mode_count, NULL);
+
+ assert(present_mode_count);
+ VkPresentModeKHR *present_modes = _alloca(present_mode_count * sizeof *present_modes);
+ vkGetPhysicalDeviceSurfacePresentModesKHR(state->vk.physical_device, state->vk.surface, &present_mode_count, present_modes);
+
+ VkExtent2D extent = get_swapchain_extent(&surface_caps, width, height);
+ VkSurfaceFormatKHR format = get_swapchain_format(formats, format_count);
+ VkPresentModeKHR present_mode = get_swapchain_present_mode(present_modes, present_mode_count);
+
+ uint32_t image_count = surface_caps.minImageCount + 1;
+ if (surface_caps.maxImageCount)
+ image_count = MIN(image_count, surface_caps.maxImageCount);
+
+ assert(indices.graphics_family == indices.present_family);
+ VkSwapchainCreateInfoKHR swapchain_info = {
+ .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
+ .surface = state->vk.surface,
+ .minImageCount = image_count,
+ .imageFormat = format.format,
+ .imageColorSpace = format.colorSpace,
+ .imageExtent = extent,
+ .imageArrayLayers = 1,
+ .imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
+ .imageSharingMode = VK_SHARING_MODE_EXCLUSIVE,
+ .preTransform = surface_caps.currentTransform,
+ .compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
+ .presentMode = present_mode,
+ .clipped = VK_TRUE,
+ .oldSwapchain = old_swapchain,
+ };
+
+ state->vk.swapchain_image_format = format.format;
+ state->vk.swapchain_extent = extent;
+
+ if (vkCreateSwapchainKHR(state->vk.device, &swapchain_info, NULL, &state->vk.swapchain) != VK_SUCCESS)
+ return false;
+
+ vkGetSwapchainImagesKHR(state->vk.device, state->vk.swapchain, &image_count, NULL);
+
+ state->vk.swapchain_images.len = state->vk.swapchain_image_views.len = state->vk.swapchain_framebuffers.len = image_count;
+
+ state->vk.swapchain_images.ptr = vkdemo_memory_arena_alloc(&memory->permanent, state->vk.swapchain_images.len * sizeof *state->vk.swapchain_images.ptr, alignof(VkImage));
+ assert(state->vk.swapchain_images.ptr);
+
+ vkGetSwapchainImagesKHR(state->vk.device, state->vk.swapchain, &state->vk.swapchain_images.len, state->vk.swapchain_images.ptr);
+
+ state->vk.swapchain_image_views.ptr = vkdemo_memory_arena_alloc(&memory->permanent, state->vk.swapchain_image_views.len * sizeof *state->vk.swapchain_image_views.ptr, alignof(VkImageView));
+ assert(state->vk.swapchain_image_views.ptr);
+
+ for (uint32_t i = 0; i < state->vk.swapchain_images.len; i++) {
+ VkImageViewCreateInfo image_view_info = {
+ .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
+ .image = state->vk.swapchain_images.ptr[i],
+ .viewType = VK_IMAGE_VIEW_TYPE_2D,
+ .format = state->vk.swapchain_image_format,
+ .components.r = VK_COMPONENT_SWIZZLE_IDENTITY,
+ .components.g = VK_COMPONENT_SWIZZLE_IDENTITY,
+ .components.b = VK_COMPONENT_SWIZZLE_IDENTITY,
+ .components.a = VK_COMPONENT_SWIZZLE_IDENTITY,
+ .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .subresourceRange.baseMipLevel = 0,
+ .subresourceRange.levelCount = 1,
+ .subresourceRange.baseArrayLayer = 0,
+ .subresourceRange.layerCount = 1,
+ };
+
+ if (vkCreateImageView(state->vk.device, &image_view_info, NULL, &state->vk.swapchain_image_views.ptr[i]) != VK_SUCCESS)
+ return false;
+ }
+
+ VkAttachmentDescription color_attachment = {
+ .format = state->vk.swapchain_image_format,
+ .samples = VK_SAMPLE_COUNT_1_BIT,
+ .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
+ .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
+ .stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE,
+ .stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
+ .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
+ .finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
+ };
+
+ VkAttachmentReference color_attachment_ref = {
+ .attachment = 0,
+ .layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
+ };
+
+ VkSubpassDescription subpass = {
+ .pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
+ .colorAttachmentCount = 1,
+ .pColorAttachments = &color_attachment_ref,
+ };
+
+ VkSubpassDependency dependency = {
+ .srcSubpass = VK_SUBPASS_EXTERNAL,
+ .srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
+ .srcAccessMask = 0,
+ .dstSubpass = 0,
+ .dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
+ .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
+ };
+
+ VkRenderPassCreateInfo render_pass_info = {
+ .sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
+ .attachmentCount = 1,
+ .pAttachments = &color_attachment,
+ .subpassCount = 1,
+ .pSubpasses = &subpass,
+ .dependencyCount = 1,
+ .pDependencies = &dependency,
+ };
+
+ if (vkCreateRenderPass(state->vk.device, &render_pass_info, NULL, &state->vk.render_pass) != VK_SUCCESS)
+ return false;
+
+ state->vk.swapchain_framebuffers.ptr = vkdemo_memory_arena_alloc(&memory->permanent, state->vk.swapchain_framebuffers.len * sizeof *state->vk.swapchain_framebuffers.ptr, alignof(VkFramebuffer));
+ assert(state->vk.swapchain_framebuffers.ptr);
+
+ for (uint32_t i = 0; i < state->vk.swapchain_framebuffers.len; i++) {
+ VkImageView attachments[] = { state->vk.swapchain_image_views.ptr[i], };
+
+ VkFramebufferCreateInfo framebuffer_info = {
+ .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
+ .renderPass = state->vk.render_pass,
+ .attachmentCount = ARRLEN(attachments),
+ .pAttachments = attachments,
+ .width = state->vk.swapchain_extent.width,
+ .height = state->vk.swapchain_extent.height,
+ .layers = 1,
+ };
+
+ if (vkCreateFramebuffer(state->vk.device, &framebuffer_info, NULL, &state->vk.swapchain_framebuffers.ptr[i]) != VK_SUCCESS)
+ return false;
+ }
+
+ return true;
+}
+
+static void
+destroy_swapchain(struct vkdemo_memory *memory, struct vkdemo_state *state)
+{
+ (void) memory;
+
+ // TODO: can this cause deadlock?
+ vkDeviceWaitIdle(state->vk.device);
+
+ for (uint32_t i = 0; i < state->vk.swapchain_framebuffers.len; i++) {
+ vkDestroyFramebuffer(state->vk.device, state->vk.swapchain_framebuffers.ptr[i], NULL);
+ }
+
+ vkDestroyRenderPass(state->vk.device, state->vk.render_pass, NULL);
+
+ for (uint32_t i = 0; i < state->vk.swapchain_image_views.len; i++) {
+ vkDestroyImageView(state->vk.device, state->vk.swapchain_image_views.ptr[i], NULL);
+ }
+
+ vkDestroySwapchainKHR(state->vk.device, state->vk.swapchain, NULL);
+}
+
+static bool
+create_shader_module(VkDevice device, uint32_t *code, uint64_t size, VkShaderModule *out)
+{
+ VkShaderModuleCreateInfo module_info = {
+ .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
+ .codeSize = size,
+ .pCode = code,
+ };
+
+ return vkCreateShaderModule(device, &module_info, NULL, out) == VK_SUCCESS;
+}
+
+static bool
+create_pipeline(struct vkdemo_state *state)
+{
+ VkShaderModule vert_shader_module;
+ if (!create_shader_module(state->vk.device, vert_shader, sizeof vert_shader, &vert_shader_module)) {
+ fprintf(stderr, "Failed to create vertex shader module!\n");
+ return false;
+ }
+
+ VkShaderModule frag_shader_module;
+ if (!create_shader_module(state->vk.device, frag_shader, sizeof frag_shader, &frag_shader_module)) {
+ fprintf(stderr, "Failed to create fragment shader module!\n");
+ return false;
+ }
+
+ VkPipelineShaderStageCreateInfo vert_stage_info = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
+ .stage = VK_SHADER_STAGE_VERTEX_BIT,
+ .module = vert_shader_module,
+ .pName = "main",
+ };
+
+ VkPipelineShaderStageCreateInfo frag_stage_info = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
+ .stage = VK_SHADER_STAGE_FRAGMENT_BIT,
+ .module = frag_shader_module,
+ .pName = "main",
+ };
+
+ VkPipelineShaderStageCreateInfo shader_stages[] = {
+ vert_stage_info,
+ frag_stage_info,
+ };
+
+ VkDynamicState dynamic_states[] = {
+ VK_DYNAMIC_STATE_VIEWPORT,
+ VK_DYNAMIC_STATE_SCISSOR,
+ };
+
+ VkPipelineDynamicStateCreateInfo dynamic_state_info = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
+ .dynamicStateCount = ARRLEN(dynamic_states),
+ .pDynamicStates = dynamic_states,
+ };
+
+ VkPipelineVertexInputStateCreateInfo vertex_input_info = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
+ .vertexBindingDescriptionCount = 0,
+ .pVertexBindingDescriptions = NULL,
+ .vertexAttributeDescriptionCount = 0,
+ .pVertexAttributeDescriptions = NULL,
+ };
+
+ VkPipelineInputAssemblyStateCreateInfo input_assembly_info = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
+ .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
+ .primitiveRestartEnable = VK_FALSE,
+ };
+
+ VkPipelineViewportStateCreateInfo viewport_info = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
+ .viewportCount = 1,
+ .scissorCount = 1,
+ };
+
+ VkPipelineRasterizationStateCreateInfo rasteriser_info = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
+ .depthClampEnable = VK_FALSE,
+ .rasterizerDiscardEnable = VK_FALSE,
+ .polygonMode = VK_POLYGON_MODE_FILL,
+ .lineWidth = 1.0f,
+ .cullMode = VK_CULL_MODE_BACK_BIT,
+ .frontFace = VK_FRONT_FACE_CLOCKWISE,
+ .depthBiasEnable = VK_FALSE,
+ };
+
+ VkPipelineMultisampleStateCreateInfo multisample_info = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
+ .sampleShadingEnable = VK_FALSE,
+ .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT,
+ };
+
+ VkPipelineColorBlendAttachmentState color_blend_attachment = {
+ .colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT,
+ .blendEnable = VK_FALSE,
+ };
+
+ VkPipelineColorBlendStateCreateInfo color_blend_info = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
+ .logicOpEnable = VK_FALSE,
+ .attachmentCount = 1,
+ .pAttachments = &color_blend_attachment,
+ };
+
+ VkPipelineLayoutCreateInfo pipeline_layout_info = {
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
+ };
+
+ if (vkCreatePipelineLayout(state->vk.device, &pipeline_layout_info, NULL, &state->vk.pipeline_layout) != VK_SUCCESS) {
+ fprintf(stderr, "Failed to create pipeline layout!\n");
+ return false;
+ }
+
+ VkGraphicsPipelineCreateInfo pipeline_info = {
+ .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
+ .stageCount = 2,
+ .pStages = shader_stages,
+ .pVertexInputState = &vertex_input_info,
+ .pInputAssemblyState = &input_assembly_info,
+ .pViewportState = &viewport_info,
+ .pRasterizationState = &rasteriser_info,
+ .pMultisampleState = &multisample_info,
+ .pColorBlendState = &color_blend_info,
+ .pDynamicState = &dynamic_state_info,
+ .layout = state->vk.pipeline_layout,
+ .renderPass = state->vk.render_pass,
+ .subpass = 0,
+ .basePipelineHandle = VK_NULL_HANDLE,
+ .basePipelineIndex = -1,
+ };
+
+ VkPipeline pipeline;
+ if (vkCreateGraphicsPipelines(state->vk.device, VK_NULL_HANDLE, 1, &pipeline_info, NULL, &pipeline) != VK_SUCCESS) {
+ fprintf(stderr, "Failed to create pipeline!\n");
+ return false;
+ }
+
+ state->vk.pipeline = pipeline;
+
+ vkDestroyShaderModule(state->vk.device, frag_shader_module, NULL);
+ vkDestroyShaderModule(state->vk.device, vert_shader_module, NULL);
+
+ return true;
+}
+
+static struct vkdemo_state *state;
+
+bool
+vkdemo_init(struct vkdemo_memory *memory, VkInstance vk_instance, VkSurfaceKHR vk_surface, uint32_t width, uint32_t height)
+{
+ state = vkdemo_memory_arena_alloc(&memory->permanent, sizeof *state, alignof(struct vkdemo_state));
+ assert(state);
+
+ state->vk.instance = vk_instance;
+ state->vk.surface = vk_surface;
+ state->vk.frame_index = 0;
+
+ uint32_t physical_device_count;
+ vkEnumeratePhysicalDevices(vk_instance, &physical_device_count, NULL);
+
+ if (!physical_device_count) {
+ fprintf(stderr, "Failed to find physical devices supporting Vulkan 1.0!\n");
+ return false;
+ }
+
+ VkPhysicalDevice *physical_devices = _alloca(physical_device_count * sizeof *physical_devices);
+ vkEnumeratePhysicalDevices(vk_instance, &physical_device_count, physical_devices);
+
+ const char *device_extensions[] = {
+ VK_KHR_SWAPCHAIN_EXTENSION_NAME,
+ };
+
+ state->vk.physical_device = VK_NULL_HANDLE;
+ for (uint32_t i = 0; i < physical_device_count; i++) {
+ if (is_device_suitable(physical_devices[i], vk_surface, device_extensions, ARRLEN(device_extensions)))
+ state->vk.physical_device = physical_devices[i];
+ }
+
+ if (state->vk.physical_device == VK_NULL_HANDLE) {
+ fprintf(stderr, "Failed to find suitable physical device!\n");
+ return false;
+ }
+
+ struct queue_family_indices device_queue_families = get_device_queue_families(state->vk.physical_device, vk_surface);
+
+ float queue_priority = 1.0f;
+
+ // TODO: handle case where graphics queue and present queue are separate
+ VkDeviceQueueCreateInfo queue_info = {
+ .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
+ .queueFamilyIndex = device_queue_families.graphics_family,
+ .queueCount = 1,
+ .pQueuePriorities = &queue_priority,
+ };
+
+ VkPhysicalDeviceFeatures device_features = {0};
+
+ VkDeviceCreateInfo device_info = {
+ .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
+ .pQueueCreateInfos = &queue_info,
+ .queueCreateInfoCount = 1,
+ .pEnabledFeatures = &device_features,
+ .enabledExtensionCount = ARRLEN(device_extensions),
+ .ppEnabledExtensionNames = device_extensions,
+ };
+
+ if (vkCreateDevice(state->vk.physical_device, &device_info, NULL, &state->vk.device) != VK_SUCCESS) {
+ fprintf(stderr, "Failed to create logical device!\n");
+ return false;
+ }
+
+ vkGetDeviceQueue(state->vk.device, device_queue_families.graphics_family, 0, &state->vk.graphics_queue);
+ vkGetDeviceQueue(state->vk.device, device_queue_families.present_family, 0, &state->vk.present_queue);
+
+ if (!recreate_swapchain(memory, state, width, height, VK_NULL_HANDLE)) {
+ fprintf(stderr, "Failed to create swapchain!\n");
+ return false;
+ }
+
+ if (!create_pipeline(state)) {
+ fprintf(stderr, "Failed to create graphics pipeline!\n");
+ return false;
+ }
+
+ VkCommandPoolCreateInfo command_pool_info = {
+ .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
+ .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,
+ .queueFamilyIndex = device_queue_families.graphics_family,
+ };
+
+ if (vkCreateCommandPool(state->vk.device, &command_pool_info, NULL, &state->vk.command_pool) != VK_SUCCESS) {
+ fprintf(stderr, "Failed to create command pool!\n");
+ return false;
+ }
+
+ VkCommandBufferAllocateInfo command_buffer_alloc_info = {
+ .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
+ .commandPool = state->vk.command_pool,
+ .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
+ .commandBufferCount = ARRLEN(state->vk.command_buffers),
+ };
+
+ if (vkAllocateCommandBuffers(state->vk.device, &command_buffer_alloc_info, state->vk.command_buffers) != VK_SUCCESS) {
+ fprintf(stderr, "Failed to allocate command buffers!\n");
+ return false;
+ }
+
+ VkSemaphoreCreateInfo semaphore_info = {
+ .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
+ };
+
+ VkFenceCreateInfo fence_info = {
+ .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
+ .flags = VK_FENCE_CREATE_SIGNALED_BIT,
+ };
+
+ for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
+ if (vkCreateSemaphore(state->vk.device, &semaphore_info, NULL, &state->vk.sync_objects.image_available[i]) != VK_SUCCESS ||
+ vkCreateSemaphore(state->vk.device, &semaphore_info, NULL, &state->vk.sync_objects.render_finished[i]) != VK_SUCCESS ||
+ vkCreateFence(state->vk.device, &fence_info, NULL, &state->vk.sync_objects.in_flight[i]) != VK_SUCCESS) {
+ fprintf(stderr, "Failed to create sync objects!\n");
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void
+vkdemo_update(struct vkdemo_memory *memory)
+{
+ (void) memory;
+}
+
+void
+vkdemo_render(struct vkdemo_memory *memory, uint32_t width, uint32_t height)
+{
+ (void) memory;
+
+ uint32_t frame = state->vk.frame_index;
+ state->vk.frame_index = (frame + 1) % MAX_FRAMES_IN_FLIGHT;
+
+ vkWaitForFences(state->vk.device, 1, &state->vk.sync_objects.in_flight[frame], VK_TRUE, UINT64_MAX);
+
+ uint32_t image_index;
+ VkResult result = vkAcquireNextImageKHR(state->vk.device, state->vk.swapchain, UINT64_MAX, state->vk.sync_objects.image_available[frame], VK_NULL_HANDLE, &image_index);
+
+ if (result == VK_ERROR_OUT_OF_DATE_KHR) {
+ destroy_swapchain(memory, state);
+ if (!recreate_swapchain(memory, state, width, height, VK_NULL_HANDLE))
+ fprintf(stderr, "Failed to recreate swapchain!\n");
+ return;
+ } else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
+ fprintf(stderr, "Failed to aqcuire swapchain image!\n");
+ return;
+ }
+
+ vkResetFences(state->vk.device, 1, &state->vk.sync_objects.in_flight[frame]);
+
+ {
+ VkCommandBuffer buffer = state->vk.command_buffers[frame];
+ vkResetCommandBuffer(buffer, 0);
+
+ VkCommandBufferBeginInfo begin_info = {
+ .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
+ };
+
+ if (vkBeginCommandBuffer(buffer, &begin_info) != VK_SUCCESS) {
+ fprintf(stderr, "Failed to begin recording command buffer!\n");
+ return;
+ }
+
+ VkClearValue clear_color = {{{0.0f, 0.0f, 0.0f, 1.0f}}};
+ VkRenderPassBeginInfo render_pass_begin_info = {
+ .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
+ .renderPass = state->vk.render_pass,
+ .framebuffer = state->vk.swapchain_framebuffers.ptr[image_index],
+ .renderArea.offset = {0, 0},
+ .renderArea.extent = state->vk.swapchain_extent,
+ .clearValueCount = 1,
+ .pClearValues = &clear_color,
+ };
+
+ vkCmdBeginRenderPass(buffer, &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE);
+
+ vkCmdBindPipeline(buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, state->vk.pipeline);
+
+ VkViewport viewport = {
+ .x = 0.0f,
+ .y = 0.0f,
+ .width = (float) state->vk.swapchain_extent.width,
+ .height = (float) state->vk.swapchain_extent.height,
+ .minDepth = 0.0f,
+ .maxDepth = 1.0f,
+ };
+
+ vkCmdSetViewport(buffer, 0, 1, &viewport);
+
+ VkRect2D scissor = {
+ .offset = {0, 0},
+ .extent = state->vk.swapchain_extent,
+ };
+
+ vkCmdSetScissor(buffer, 0, 1, &scissor);
+
+ uint32_t vertex_count = 3, instance_count = 1, first_vertex = 0, first_instance = 0;
+ vkCmdDraw(buffer, vertex_count, instance_count, first_vertex, first_instance);
+
+ vkCmdEndRenderPass(buffer);
+
+ if (vkEndCommandBuffer(buffer) != VK_SUCCESS) {
+ fprintf(stderr, "Failed to end recording command buffer!\n");
+ return;
+ }
+ }
+
+ VkPipelineStageFlags wait_stages[] = { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT };
+
+ VkSubmitInfo submit_info = {
+ .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
+ .waitSemaphoreCount = 1,
+ .pWaitSemaphores = &state->vk.sync_objects.image_available[frame],
+ .pWaitDstStageMask = wait_stages,
+ .signalSemaphoreCount = 1,
+ .pSignalSemaphores = &state->vk.sync_objects.render_finished[frame],
+ .commandBufferCount = 1,
+ .pCommandBuffers = &state->vk.command_buffers[frame],
+ };
+
+ if (vkQueueSubmit(state->vk.graphics_queue, 1, &submit_info, state->vk.sync_objects.in_flight[frame]) != VK_SUCCESS) {
+ fprintf(stderr, "Failed to submit command buffer!\n");
+ return;
+ }
+
+ VkPresentInfoKHR present_info = {
+ .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
+ .waitSemaphoreCount = 1,
+ .pWaitSemaphores = &state->vk.sync_objects.render_finished[frame],
+ .swapchainCount = 1,
+ .pSwapchains = &state->vk.swapchain,
+ .pImageIndices = &image_index,
+ };
+
+ vkQueuePresentKHR(state->vk.present_queue, &present_info);
+}
+
+void
+vkdemo_free(struct vkdemo_memory *memory)
+{
+ vkDeviceWaitIdle(state->vk.device);
+
+ destroy_swapchain(memory, state);
+
+ vkDestroyPipeline(state->vk.device, state->vk.pipeline, NULL);
+ vkDestroyPipelineLayout(state->vk.device, state->vk.pipeline_layout, NULL);
+
+ for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
+ vkDestroySemaphore(state->vk.device, state->vk.sync_objects.render_finished[i], NULL);
+ vkDestroySemaphore(state->vk.device, state->vk.sync_objects.image_available[i], NULL);
+ vkDestroyFence(state->vk.device, state->vk.sync_objects.in_flight[i], NULL);
+ }
+
+ vkDestroyCommandPool(state->vk.device, state->vk.command_pool, NULL);
+
+ vkDestroyDevice(state->vk.device, NULL);
+}
diff --git a/vkdemo/vkdemo.h b/vkdemo/vkdemo.h
@@ -0,0 +1,50 @@
+#ifndef VKDEMO_H
+#define VKDEMO_H
+
+#include <assert.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <stdalign.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <vulkan/vulkan.h>
+
+// NOTE: MSVC UCRT does not support alloca() (or VLAs), so we need to include this header :/
+#include <malloc.h>
+
+#define KiB 1024ULL
+#define MiB (1024 * KiB)
+#define GiB (1024 * MiB)
+#define TiB (1024 * GiB)
+
+#define ARRLEN(arr) (sizeof (arr) / sizeof (arr)[0])
+
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+#define CLAMP(v, min, max) (MIN(MAX(v, min), max))
+
+struct vkdemo_memory_arena {
+ void *ptr;
+ uint64_t cap, len;
+};
+
+struct vkdemo_memory {
+ struct vkdemo_memory_arena permanent, temporary;
+};
+
+extern bool
+vkdemo_init(struct vkdemo_memory *memory, VkInstance vk_instance, VkSurfaceKHR vk_surface, uint32_t width, uint32_t height);
+
+extern void
+vkdemo_update(struct vkdemo_memory *memory);
+
+extern void
+vkdemo_render(struct vkdemo_memory *memory, uint32_t width, uint32_t height);
+
+extern void
+vkdemo_free(struct vkdemo_memory *memory);
+
+#endif /* VKDEMO_H */
diff --git a/vkdemo/win32_vkdemo.c b/vkdemo/win32_vkdemo.c
@@ -0,0 +1,290 @@
+#include "vkdemo.h"
+
+#include "vkdemo.c"
+
+#define WIN32_LEAN_AND_MEAN 1
+#include <windows.h>
+
+#include <vulkan/vulkan_win32.h>
+
+static void
+win32_process_messages(void);
+
+LRESULT CALLBACK
+win32_window_proc(HWND window, UINT msg, WPARAM wparam, LPARAM lparam);
+
+static VKAPI_ATTR VkBool32 VKAPI_CALL
+vk_debug_callback(VkDebugUtilsMessageSeverityFlagBitsEXT severity,
+ VkDebugUtilsMessageTypeFlagsEXT type,
+ const VkDebugUtilsMessengerCallbackDataEXT *callback_data,
+ void *user_data);
+
+static bool win32_running;
+static HWND win32_window;
+
+int WINAPI
+WinMain(HINSTANCE instance, HINSTANCE prev_instance, PSTR cmdline, int cmdshown)
+{
+ (void) instance;
+ (void) prev_instance;
+ (void) cmdline;
+ (void) cmdshown;
+
+ const char classname[] = "vkdemo_wndcls";
+
+ // https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa
+ WNDCLASSEXA cls = {
+ .cbSize = sizeof(WNDCLASSEXA),
+ .style = CS_OWNDC | CS_VREDRAW | CS_HREDRAW,
+ .lpfnWndProc = win32_window_proc,
+ .hInstance = instance,
+ .lpszClassName = classname,
+ };
+
+ // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclassexa
+ RegisterClassExA(&cls);
+
+ // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexa
+ win32_window = CreateWindowExA(
+ 0,
+ classname,
+ "vkdemo",
+ WS_OVERLAPPEDWINDOW,
+ CW_USEDEFAULT,
+ CW_USEDEFAULT,
+ CW_USEDEFAULT,
+ CW_USEDEFAULT,
+ NULL,
+ NULL,
+ instance,
+ NULL);
+
+ if (!win32_window) {
+ fprintf(stderr, "Failed to create Win32 window!\n");
+ return 1;
+ }
+
+ const uint64_t vkdemo_permanent_memory_cap = 128 * MiB;
+ const uint64_t vkdemo_temporary_memory_cap = 128 * MiB;
+
+ uint64_t alloc_size = vkdemo_permanent_memory_cap + vkdemo_temporary_memory_cap;
+ void *vkdemo_memory_alloc = VirtualAlloc(0, alloc_size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
+ if (!vkdemo_memory_alloc) {
+ fprintf(stderr, "Failed to allocate memory for vkdemo!\n");
+ return 1;
+ }
+
+ struct vkdemo_memory vkdemo_memory = {
+ .permanent = {
+ .ptr = vkdemo_memory_alloc,
+ .cap = vkdemo_permanent_memory_cap,
+ },
+
+ .temporary = {
+ .ptr = (uint8_t *) vkdemo_memory_alloc + vkdemo_permanent_memory_cap,
+ .cap = vkdemo_temporary_memory_cap,
+ },
+ };
+
+ {
+ // NOTE: debug code to display available extensions and layers
+ uint32_t extension_count;
+ vkEnumerateInstanceExtensionProperties(NULL, &extension_count, NULL);
+
+ fprintf(stderr, "Extension count: %" PRIu32 "\n", extension_count);
+
+ VkExtensionProperties *extensions = _alloca(extension_count * sizeof *extensions);
+ vkEnumerateInstanceExtensionProperties(NULL, &extension_count, extensions);
+
+ for (uint32_t i = 0; i < extension_count; i++)
+ fprintf(stderr, "\t%s\n", extensions[i].extensionName);
+
+ uint32_t layer_count;
+ vkEnumerateInstanceLayerProperties(&layer_count, NULL);
+
+ fprintf(stderr, "Layer count: %" PRIu32 "\n", layer_count);
+
+ VkLayerProperties *layers = _alloca(layer_count * sizeof *layers);
+ vkEnumerateInstanceLayerProperties(&layer_count, layers);
+
+ for (uint32_t i = 0; i < layer_count; i++)
+ fprintf(stderr, "\t%s\n", layers[i].layerName);
+ }
+
+ VkApplicationInfo app_info = {
+ .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
+ .pApplicationName = "vkdemo",
+ .applicationVersion = VK_MAKE_VERSION(1, 0, 0),
+ .pEngineName = "No Engine",
+ .engineVersion = VK_MAKE_VERSION(1, 0, 0),
+ .apiVersion = VK_API_VERSION_1_0,
+ };
+
+ const char *instance_extensions[] = {
+ VK_KHR_SURFACE_EXTENSION_NAME,
+ VK_KHR_WIN32_SURFACE_EXTENSION_NAME,
+ VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME,
+ VK_EXT_DEBUG_UTILS_EXTENSION_NAME,
+ };
+
+ const char *instance_layers[] = {
+ "VK_LAYER_KHRONOS_validation",
+ };
+
+ uint32_t debug_severity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT
+ | VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT
+ | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT
+ | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
+
+ uint32_t debug_type = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT
+ | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT
+ | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
+
+ VkDebugUtilsMessengerCreateInfoEXT debug_info = {
+ .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT,
+ .messageSeverity = debug_severity,
+ .messageType = debug_type,
+ .pfnUserCallback = vk_debug_callback,
+ .pUserData = NULL,
+ };
+
+ VkInstanceCreateInfo instance_info = {
+ .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
+ .pApplicationInfo = &app_info,
+ .enabledExtensionCount = ARRLEN(instance_extensions),
+ .ppEnabledExtensionNames = instance_extensions,
+ .enabledLayerCount = ARRLEN(instance_layers),
+ .ppEnabledLayerNames = instance_layers,
+ .pNext = &debug_info,
+ };
+
+ VkInstance vk_instance;
+ if (vkCreateInstance(&instance_info, NULL, &vk_instance) != VK_SUCCESS) {
+ fprintf(stderr, "Failed to create Vulkan instance!\n");
+ return 1;
+ }
+
+ PFN_vkCreateDebugUtilsMessengerEXT vk_create_debug_utils_messenger_ext =
+ (PFN_vkCreateDebugUtilsMessengerEXT) vkGetInstanceProcAddr(vk_instance, "vkCreateDebugUtilsMessengerEXT");
+ if (!vk_create_debug_utils_messenger_ext) {
+ fprintf(stderr, "Failed to get proc address of vkCreateDebugUtilsMessengerEXT!\n");
+ return 1;
+ }
+
+ VkDebugUtilsMessengerEXT vk_debug_messenger;
+ if (vk_create_debug_utils_messenger_ext(vk_instance, &debug_info, NULL, &vk_debug_messenger) != VK_SUCCESS) {
+ fprintf(stderr, "Failed to create Vulkan debug messenger!\n");
+ return 1;
+ }
+
+ VkWin32SurfaceCreateInfoKHR win32_surface_info = {
+ .sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR,
+ .hwnd = win32_window,
+ .hinstance = instance,
+ };
+
+ VkSurfaceKHR vk_surface;
+ if (vkCreateWin32SurfaceKHR(vk_instance, &win32_surface_info, NULL, &vk_surface) != VK_SUCCESS) {
+ fprintf(stderr, "Failed to create Vulkan win32 surface!\n");
+ return 1;
+ }
+
+ RECT rect;
+ GetClientRect(win32_window, &rect);
+ if (!vkdemo_init(&vkdemo_memory, vk_instance, vk_surface, rect.right - rect.left, rect.bottom - rect.top)) {
+ fprintf(stderr, "Failed to initialise vkdemo!\n");
+ return 1;
+ }
+
+ ShowWindow(win32_window, 1);
+
+ LARGE_INTEGER perf_frequency, start_perf_count, end_perf_count;
+ QueryPerformanceFrequency(&perf_frequency);
+
+ win32_running = true;
+ while (win32_running) {
+ QueryPerformanceCounter(&start_perf_count);
+ uint64_t start_cycles = __rdtsc();
+
+ win32_process_messages();
+
+ vkdemo_update(&vkdemo_memory);
+
+ GetClientRect(win32_window, &rect);
+ vkdemo_render(&vkdemo_memory, rect.right - rect.left, rect.bottom - rect.top);
+
+ uint64_t end_cycles = __rdtsc();
+ QueryPerformanceCounter(&end_perf_count);
+
+ const uint64_t micros = 1000 * 1000;
+ uint64_t elapsed_micros = (end_perf_count.QuadPart - start_perf_count.QuadPart) * micros / perf_frequency.QuadPart;
+ uint64_t elapsed_cycles = end_cycles - start_cycles;
+
+ char buf[256];
+ snprintf(buf, sizeof buf, "Frame Timings: %" PRIu64 "us, %" PRIu64 " Cy\n", elapsed_micros, elapsed_cycles);
+ OutputDebugString(buf);
+ }
+
+ vkdemo_free(&vkdemo_memory);
+
+ vkDestroySurfaceKHR(vk_instance, vk_surface, NULL);
+
+ PFN_vkDestroyDebugUtilsMessengerEXT vk_destroy_debug_utils_messenger_ext =
+ (PFN_vkDestroyDebugUtilsMessengerEXT) vkGetInstanceProcAddr(vk_instance, "vkDestroyDebugUtilsMessengerEXT");
+ if (!vk_destroy_debug_utils_messenger_ext) {
+ fprintf(stderr, "Failed to get proc address of vkDestroyDebugUtilsMessengerEXT!\n");
+ return 1;
+ }
+
+ vk_destroy_debug_utils_messenger_ext(vk_instance, vk_debug_messenger, NULL);
+
+ vkDestroyInstance(vk_instance, NULL);
+
+ return 0;
+}
+
+static void
+win32_process_messages(void)
+{
+ MSG msg;
+ while (PeekMessageA(&msg, win32_window, 0, 0, PM_REMOVE)) {
+ switch (msg.message) {
+ default:
+ break;
+ }
+
+ TranslateMessage(&msg);
+ DispatchMessageA(&msg);
+ }
+}
+
+LRESULT CALLBACK
+win32_window_proc(HWND window, UINT msg, WPARAM wparam, LPARAM lparam)
+{
+ switch (msg) {
+ case WM_QUIT:
+ case WM_DESTROY: {
+ win32_running = false;
+ } break;
+
+ default:
+ return DefWindowProcA(window, msg, wparam, lparam);
+ }
+
+ return 0;
+}
+
+static VKAPI_ATTR VkBool32 VKAPI_CALL
+vk_debug_callback(VkDebugUtilsMessageSeverityFlagBitsEXT severity,
+ VkDebugUtilsMessageTypeFlagsEXT type,
+ const VkDebugUtilsMessengerCallbackDataEXT *callback_data,
+ void *user_data)
+{
+ (void) severity;
+ (void) type;
+ (void) user_data;
+
+ fprintf(stderr, "validation layer: %s\n", callback_data->pMessage);
+
+ return VK_FALSE;
+}
diff --git a/wasapidemo/.editorconfig b/wasapidemo/.editorconfig
@@ -0,0 +1,16 @@
+root = true
+
+guidelines = 80 120 160
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+
+[{Makefile,*.mk}]
+indent_style = tab
+indent_size = 8
+
+[*.{c,h}]
+indent_style = tab
+indent_size = 8
diff --git a/wasapidemo/build.sh b/wasapidemo/build.sh
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+set -ex
+
+CC="clang"
+
+WARNINGS="-Wall -Wextra -Wpedantic -Werror -Wno-unused-function -Wno-unused-variable -Wno-unused-but-set-variable"
+
+# NOTE: clang only emits working pdbs with the following options:
+# - '-fuse-ld=lld' : only LLD supports PDB file generation
+# - '-O0' : prevents optimisations from breaking the debug info
+# - '-g -gcodeview' : generates CodeView debug informations
+# - '-fmsc-version=xxxx' : defines the MSVC toolchain version
+# - '-Wl,--pdb=' : outputs a PDB file
+# SEEALSO: https://learn.microsoft.com/en-us/cpp/preprocessor/predefined-macros?view=msvc-170
+
+CFLAGS="-std=c11 -O0 -g -gcodeview $WARNINGS"
+#CPPFLAGS="-fmsc-version=1935"
+LDFLAGS="-fuse-ld=lld -Wl,/debug:full,/pdb:"
+
+WIN32_LIBS="-luser32 -lgdi32 -lwinmm -lavrt -lole32"
+
+$CC -static -o wasapidemo.exe src/wasapidemo.c $CFLAGS $CPPFLAGS $LDFLAGS $WIN32_LIBS
diff --git a/wasapidemo/clean.sh b/wasapidemo/clean.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+set -ex
+
+rm -rf *wasapidemo*
diff --git a/wasapidemo/debug.sh b/wasapidemo/debug.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+set -ex
+
+/c/Users/mikolaj/remedybg.exe misc/wasapidemo.rdbg &
diff --git a/wasapidemo/misc/wasapidemo.rdbg b/wasapidemo/misc/wasapidemo.rdbg
Binary files differ.
diff --git a/wasapidemo/src/wasapidemo.c b/wasapidemo/src/wasapidemo.c
@@ -0,0 +1,598 @@
+#include "wasapidemo.h"
+
+global_variable b32 win32_running;
+global_variable b32 win32_granular_sleep;
+global_variable u64 win32_perf_frequency;
+
+global_variable struct win32_dsound_buffer win32_dsound_buffer;
+global_variable struct win32_wasapi_buffer win32_wasapi_buffer;
+
+global_variable struct wasapidemo_mixer win32_mixer;
+
+int WINAPI
+WinMain(HINSTANCE instance, HINSTANCE prev_instance, LPSTR cmdline, int cmdshow)
+{
+ (void) prev_instance;
+ (void) cmdline;
+ (void) cmdshow;
+
+ LARGE_INTEGER perf_frequency_result;
+ QueryPerformanceFrequency(&perf_frequency_result);
+ win32_perf_frequency = perf_frequency_result.QuadPart;
+
+ const UINT scheduler_granularity_ms = 1;
+ win32_granular_sleep = timeBeginPeriod(scheduler_granularity_ms) == TIMERR_NOERROR;
+
+ // NOTE: set high priority for thread to reduce audio glitches :/
+ DWORD task_index = 0;
+ AvSetMmThreadCharacteristics("Pro Audio", &task_index);
+ (void) task_index;
+
+ const char window_class_name[] = "wasapidemo_window";
+
+ WNDCLASSEXA window_class = {
+ .cbSize = sizeof(WNDCLASSEX),
+ .style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW,
+ .lpfnWndProc = win32_window_proc,
+ .hInstance = instance,
+ .lpszClassName = window_class_name,
+ };
+
+ RegisterClassExA(&window_class);
+
+ HWND window = CreateWindowExA(
+ 0,
+ window_class_name,
+ "wasapidemo",
+ WS_OVERLAPPEDWINDOW | WS_VISIBLE,
+ CW_USEDEFAULT,
+ CW_USEDEFAULT,
+ CW_USEDEFAULT,
+ CW_USEDEFAULT,
+ 0,
+ 0,
+ instance,
+ 0);
+
+ if (!window) {
+ fprintf(stderr, "Failed to create window\n");
+ exit(EXIT_FAILURE);
+ }
+
+ u32 target_render_rate = 60;
+
+ {
+ HDC dc = GetDC(window);
+ int actual_refresh_rate = GetDeviceCaps(dc, VREFRESH);
+ if (actual_refresh_rate > 1)
+ target_render_rate = actual_refresh_rate;
+ ReleaseDC(window, dc);
+ }
+
+ u64 target_us_per_frame = (u64) (USECS / (f32) target_render_rate);
+
+ win32_mixer = (struct wasapidemo_mixer) {
+ .tone_hz = WASAPIDEMO_MIXER_FUNDAMENTAL,
+ .tone_volume = 4000,
+ };
+
+#if 1
+ // TODO: investigate WAVEFORMATEXTENSIBLE instead for higher bitrate sound
+ WAVEFORMATEX sound_format = {
+ .wFormatTag = WAVE_FORMAT_PCM,
+ .nChannels = SOUND_CHANNELS,
+ .nSamplesPerSec = SOUND_SAMPLE_RATE,
+ .wBitsPerSample = SOUND_SAMPLE_BITS,
+ .nBlockAlign = SOUND_FRAME_BYTES,
+ .nAvgBytesPerSec = SOUND_SAMPLE_RATE * SOUND_FRAME_BYTES,
+ };
+#else
+ WAVEFORMATEXTENSIBLE sound_format = {
+ .Format = {
+ .wFormatTag = WAVE_FORMAT_EXTENSIBLE,
+ .cbSize = sizeof(WAVEFORMATEX),
+ .nChannels = SOUND_CHANNELS,
+ .nSamplesPerSec = SOUND_SAMPLE_RATE,
+ .wBitsPerSample = SOUND_SAMPLE_BITES,
+ .nBlockAlign = SOUND_FRAME_BYTES,
+ .nAvgBytesPerSec = SOUND_SAMPLE_RATE * SOUND_FRAME_BYTES,
+ },
+ .Samples.wValidBitsPerSample = sample_bits,
+ .SubFormat = KSDATAFORMAT_SUBTYPE_PCM,
+ };
+#endif
+
+ u32 sound_buffer_size = ((WAVEFORMATEX *) &sound_format)->nAvgBytesPerSec;
+
+#if USE_WASAPI
+ // u32 frame_sound_buffer_ms = 3 * ((f32) MSECS / target_render_rate);
+ u32 buffer_frames = ((WAVEFORMATEX *) &sound_format)->nSamplesPerSec / (f32) target_render_rate;
+
+ win32_init_wasapi((WAVEFORMATEX *) &sound_format, buffer_frames);
+#else
+ win32_init_dsound(window, (WAVEFORMATEX *) &sound_format, sound_buffer_size);
+#endif
+ init_sound(&win32_mixer);
+
+ s16 *sound_buffer = VirtualAlloc(NULL, sound_buffer_size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
+
+ b32 sound_is_valid = false;
+
+ u64 frame_flip_micros = win32_perf_micros();
+
+ win32_running = true;
+
+#if 0
+ win32_granular_sleep = false;
+#endif
+
+ f32 dt = 1.0f / target_render_rate;
+ while (win32_running) {
+ u64 start_perf_micros = win32_perf_micros();
+ u64 start_perf_cycles = __rdtsc();
+
+ win32_process_pending_messages();
+
+ u64 calc_audio_micros = win32_perf_micros();
+
+#if USE_WASAPI
+ UINT32 queued_frames;
+ if (!IAudioClient_GetCurrentPadding(win32_wasapi_buffer.audio_client, &queued_frames)) {
+ u32 required_frames = win32_wasapi_buffer.buffer_frame_count - queued_frames;
+
+ char buf[256];
+ snprintf(buf, ARRLEN(buf), "WASAPI: total buffered frames: %" PRIu32 ", %" PRIu32 " queued frames, %" PRIu32 " new frames to generate\n", win32_wasapi_buffer.buffer_frame_count, queued_frames, required_frames);
+ OutputDebugStringA(buf);
+
+ struct wasapidemo_audio_buffer audio_buffer = {
+ .buffer = sound_buffer,
+ .expected_samples = required_frames,
+ .sample_rate = SOUND_SAMPLE_RATE,
+ .channels = SOUND_CHANNELS,
+ };
+
+ render_sound(&audio_buffer, &win32_mixer, dt);
+
+ win32_fill_wasapi_buffer(required_frames, &audio_buffer);
+ }
+#else
+ DWORD play_cursor, write_cursor;
+ if (SUCCEEDED(IDirectSoundBuffer_GetCurrentPosition(win32_dsound_buffer.buffer, &play_cursor, &write_cursor))) {
+ if (!sound_is_valid) {
+ win32_dsound_buffer.buffer_cursor = write_cursor;
+ sound_is_valid = true;
+ }
+
+ u32 target_sample_bytes_per_frame = (win32_dsound_buffer.sample_rate * win32_dsound_buffer.block_size) / target_render_rate;
+ u32 frame_flip_latency_safety_margin_bytes = target_sample_bytes_per_frame / 2.0f;
+
+ u64 audio_micros = calc_audio_micros - frame_flip_micros;
+ f32 frame_time_remaining = (f32) (target_us_per_frame - audio_micros) / (f32) target_us_per_frame;
+ DWORD expected_bytes_until_next_frame_flip = (DWORD) (frame_time_remaining * target_sample_bytes_per_frame);
+ DWORD next_frame_flip_cursor = play_cursor + expected_bytes_until_next_frame_flip;
+
+ DWORD unwrapped_write_cursor = write_cursor;
+ if (unwrapped_write_cursor < play_cursor)
+ unwrapped_write_cursor += win32_dsound_buffer.buffer_size;
+ ASSERT(play_cursor <= unwrapped_write_cursor);
+ unwrapped_write_cursor += frame_flip_latency_safety_margin_bytes;
+
+ b32 audio_card_is_low_latency = unwrapped_write_cursor < next_frame_flip_cursor;
+
+ DWORD target_cursor;
+ if (audio_card_is_low_latency) {
+ target_cursor = next_frame_flip_cursor + target_sample_bytes_per_frame;
+ } else {
+ target_cursor = unwrapped_write_cursor + target_sample_bytes_per_frame + frame_flip_latency_safety_margin_bytes;
+ }
+ target_cursor %= win32_dsound_buffer.buffer_size;
+
+ DWORD expected_bytes = 0;
+ if (win32_dsound_buffer.buffer_cursor > target_cursor) {
+ expected_bytes = (win32_dsound_buffer.buffer_size - win32_dsound_buffer.buffer_cursor) + target_cursor;
+ } else /* win32_dsound_buffer.buffer_cursor <= target_cursor */ {
+ expected_bytes = target_cursor - win32_dsound_buffer.buffer_cursor;
+ }
+
+ struct wasapidemo_audio_buffer audio_buffer = {
+ .buffer = sound_buffer,
+ .expected_samples = expected_bytes / win32_dsound_buffer.block_size,
+ .sample_rate = win32_dsound_buffer.sample_rate,
+ .channels = win32_dsound_buffer.channels,
+ };
+
+ render_sound(&audio_buffer, &win32_mixer, dt);
+
+ win32_fill_dsound_buffer(win32_dsound_buffer.buffer_cursor, expected_bytes, &audio_buffer);
+ } else {
+ sound_is_valid = false;
+ }
+#endif
+
+ frame_flip_micros = win32_perf_micros();
+
+ u64 elapsed_cycles = __rdtsc() - start_perf_cycles;
+ u64 elapsed_us = win32_perf_micros() - start_perf_micros;
+
+#if 1
+ if (elapsed_us < target_us_per_frame) {
+ if (win32_granular_sleep) {
+ DWORD sleep_ms = (DWORD) ((f32) (target_us_per_frame - elapsed_us) / MSECS);
+ if (sleep_ms) Sleep(sleep_ms);
+ elapsed_us = win32_perf_micros() - start_perf_micros;
+ } else {
+ while (elapsed_us < target_us_per_frame)
+ elapsed_us = win32_perf_micros() - start_perf_micros;
+ }
+ } else if (elapsed_us > target_us_per_frame) {
+ // TODO: missed frame, log me!
+ }
+
+#endif
+ dt = (f32) elapsed_us / USECS;
+
+ char buf[256];
+ snprintf(buf, ARRLEN(buf), "Timings: %" PRIu64 " us / %" PRIu64 " Cy\n", elapsed_us, elapsed_cycles);
+ OutputDebugStringA(buf);
+ }
+
+ exit(EXIT_SUCCESS);
+}
+
+internal u64
+win32_perf_micros(void)
+{
+ LARGE_INTEGER result;
+ QueryPerformanceCounter(&result);
+
+ return ((u64) result.QuadPart * 1000 * 1000) / win32_perf_frequency;
+}
+
+internal void
+win32_process_pending_messages(void)
+{
+ MSG msg;
+ while (PeekMessageA(&msg, 0, 0, 0, PM_REMOVE)) {
+ switch (msg.message) {
+ case WM_QUIT: {
+ win32_running = false;
+
+#if USE_WASAPI
+ win32_wasapi_stop();
+#else
+ win32_dsound_stop();
+#endif
+ } break;
+
+ case WM_SYSKEYDOWN:
+ case WM_SYSKEYUP:
+ case WM_KEYDOWN:
+ case WM_KEYUP: {
+ u32 vkeycode = (u32) msg.wParam;
+ b32 was_down = (msg.lParam & (1 << 30));
+ b32 is_down = (msg.lParam & (1 << 31)) == 0;
+
+ win32_mixer.tone_hz = WASAPIDEMO_MIXER_FUNDAMENTAL;
+
+ if (is_down != was_down) {
+ switch (vkeycode) {
+ case 'L':
+ if (is_down) win32_mixer.tone_hz = WASAPIDEMO_MIXER_FUNDAMENTAL / 2;
+ break;
+
+ case 'H':
+ if (is_down) win32_mixer.tone_hz = 2 * WASAPIDEMO_MIXER_FUNDAMENTAL;
+ break;
+ }
+ }
+ } break;
+
+ default: {
+ TranslateMessage(&msg);
+ DispatchMessageA(&msg);
+ } break;
+ }
+ }
+}
+
+LRESULT CALLBACK
+win32_window_proc(HWND window, UINT msg, WPARAM wparam, LPARAM lparam)
+{
+ LRESULT res = 0;
+
+ switch (msg) {
+ case WM_DESTROY:
+ case WM_CLOSE: {
+ win32_running = false;
+
+#if USE_WASAPI
+ win32_wasapi_stop();
+#else
+ win32_dsound_stop();
+#endif
+ } break;
+
+ default: {
+ res = DefWindowProc(window, msg, wparam, lparam);
+ } break;
+ }
+
+ return res;
+}
+
+#define DIRECT_SOUND_CREATE(name) HRESULT WINAPI name(LPCGUID pcGuidDevice, LPDIRECTSOUND *ppDS, LPUNKNOWN pUnkOuter)
+typedef DIRECT_SOUND_CREATE(direct_sound_create_t);
+
+internal void
+win32_init_dsound(HWND window, WAVEFORMATEX *sound_format, u32 buffer_size)
+{
+ HMODULE lib = LoadLibraryA("dsound.dll");
+ if (!lib) {
+ fprintf(stderr, "Failed to load DirectSound!\n");
+ return;
+ }
+
+ direct_sound_create_t *direct_sound_create_fn = (direct_sound_create_t *) GetProcAddress(lib, "DirectSoundCreate");
+ if (!direct_sound_create_fn) {
+ fprintf(stderr, "Failed to get handle to DirectSoundCreate!\n");
+ return;
+ }
+
+ LPDIRECTSOUND direct_sound;
+ if (!SUCCEEDED(direct_sound_create_fn(0, &direct_sound, 0))) {
+ fprintf(stderr, "Failed to create DirectSound instance!\n");
+ return;
+ }
+
+ if (!SUCCEEDED(IDirectSound_SetCooperativeLevel(direct_sound, window, DSSCL_PRIORITY))) {
+ fprintf(stderr, "Failed to set DirectSound cooperative level!\n");
+ return;
+ }
+
+ win32_dsound_buffer.buffer_size = buffer_size;
+ win32_dsound_buffer.buffer_cursor = 0;
+ win32_dsound_buffer.sample_rate = sound_format->nSamplesPerSec;
+ win32_dsound_buffer.sample_bits = sound_format->wBitsPerSample;
+ win32_dsound_buffer.channels = sound_format->nChannels;
+ win32_dsound_buffer.block_size = sound_format->nBlockAlign;
+
+ DSBUFFERDESC primary_buffer_desc = {
+ .dwSize = sizeof(DSBUFFERDESC),
+ .dwFlags = DSBCAPS_PRIMARYBUFFER,
+ };
+
+ LPDIRECTSOUNDBUFFER primary_buffer;
+ if (!SUCCEEDED(IDirectSound_CreateSoundBuffer(direct_sound, &primary_buffer_desc, &primary_buffer, 0))) {
+ fprintf(stderr, "Failed to create DirectSound primary buffer!\n");
+ return;
+ }
+
+ if (!SUCCEEDED(IDirectSoundBuffer_SetFormat(primary_buffer, sound_format))) {
+ fprintf(stderr, "Failed to set format for DirectSound primary buffer!\n");
+ return;
+ }
+
+ DSBUFFERDESC secondary_buffer_desc = {
+ .dwSize = sizeof(DSBUFFERDESC),
+ .dwBufferBytes = win32_dsound_buffer.buffer_size,
+ .lpwfxFormat = sound_format,
+ };
+
+ if (!SUCCEEDED(IDirectSound_CreateSoundBuffer(direct_sound, &secondary_buffer_desc, &win32_dsound_buffer.buffer, 0))) {
+ fprintf(stderr, "Failed to create DirectSound secondary buffer!\n");
+ return;
+ }
+
+ win32_clear_dsound_buffer();
+
+ IDirectSoundBuffer_Play(win32_dsound_buffer.buffer, 0, 0, DSBPLAY_LOOPING);
+}
+
+internal void
+win32_clear_dsound_buffer(void)
+{
+ void *region1, *region2;
+ DWORD region1_size, region2_size;
+ if (SUCCEEDED(IDirectSoundBuffer_Lock(win32_dsound_buffer.buffer, 0, win32_dsound_buffer.buffer_size, ®ion1, ®ion1_size, ®ion2, ®ion2_size, 0))) {
+ u8 *dst;
+
+ dst = region1;
+ for (u32 i = 0; i < region1_size; i++)
+ *dst++ = 0;
+
+ dst = region2;
+ for (u8 i = 0; i < region2_size; i++)
+ *dst++ = 0;
+
+ IDirectSoundBuffer_Unlock(win32_dsound_buffer.buffer, region1, region1_size, region2, region2_size);
+ }
+}
+
+internal void
+win32_fill_dsound_buffer(DWORD ptr, DWORD size, struct wasapidemo_audio_buffer const *source)
+{
+ ASSERT(win32_dsound_buffer.channels == source->channels);
+
+ void *region1, *region2;
+ DWORD region1_size, region2_size;
+ if (SUCCEEDED(IDirectSoundBuffer_Lock(win32_dsound_buffer.buffer, ptr, size, ®ion1, ®ion1_size, ®ion2, ®ion2_size, 0))) {
+ s16 *dst, *src = source->buffer;
+
+ DWORD region1_samples = region1_size / win32_dsound_buffer.block_size;
+ dst = region1;
+ for (DWORD i = 0; i < region1_samples; i++) {
+ for (u32 j = 0; j < win32_dsound_buffer.channels; j++)
+ *dst++ = *src++;
+ }
+
+ DWORD region2_samples = region2_size / win32_dsound_buffer.block_size;
+ dst = region2;
+ for (DWORD i = 0; i < region2_samples; i++) {
+ for (u32 j = 0; j < win32_dsound_buffer.channels; j++)
+ *dst++ = *src++;
+ }
+
+ win32_dsound_buffer.buffer_cursor += region1_size + region2_size;
+ win32_dsound_buffer.buffer_cursor %= win32_dsound_buffer.buffer_size;
+
+ IDirectSoundBuffer_Unlock(win32_dsound_buffer.buffer, region1, region1_size, region2, region2_size);
+ }
+}
+
+internal void
+win32_dsound_stop(void)
+{
+ IDirectSoundBuffer_Stop(win32_dsound_buffer.buffer);
+}
+
+internal void
+win32_init_wasapi(WAVEFORMATEX *sound_format, u32 buffer_frames)
+{
+ HRESULT hr;
+
+ CoInitialize(0);
+
+ IMMDeviceEnumerator *device_enumerator;
+ if (CoCreateInstance(&CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, &IID_IMMDeviceEnumerator, (LPVOID *) &device_enumerator)) {
+ fprintf(stderr, "Failed to create IMMDeviceEnumerator instance.\n");
+ return;
+ }
+
+ IMMDevice *device;
+ if (IMMDeviceEnumerator_GetDefaultAudioEndpoint(device_enumerator, eRender, eMultimedia, &device)) {
+ fprintf(stderr, "Failed to get default render audio endpoint.\n");
+ IMMDeviceEnumerator_Release(device_enumerator);
+ return;
+ }
+
+ IMMDeviceEnumerator_Release(device_enumerator);
+
+ IAudioClient *audio_client;
+ if (IMMDevice_Activate(device, &IID_IAudioClient, CLSCTX_ALL, NULL, (LPVOID *) &audio_client)) {
+ fprintf(stderr, "Failed to activate default render audio endpoint.\n");
+ goto device_error;
+ }
+
+ if (IAudioClient_QueryInterface(audio_client, &IID_IAudioClient3, (LPVOID *) &win32_wasapi_buffer.audio_client)) {
+ fprintf(stderr, "Failed to activate audio client 3 interface.\n");
+ IAudioClient_Release(audio_client);
+ goto device_error;
+ }
+
+ IAudioClient_Release(audio_client);
+
+ AudioClientProperties props = {
+ .cbSize = sizeof props,
+ .eCategory = AudioCategory_Media,
+ .Options = AUDCLNT_STREAMOPTIONS_MATCH_FORMAT,
+ };
+
+ IAudioClient2_SetClientProperties(win32_wasapi_buffer.audio_client, &props);
+
+ if (IAudioClient_IsFormatSupported(win32_wasapi_buffer.audio_client, AUDCLNT_SHAREMODE_EXCLUSIVE, sound_format, NULL)) {
+ fprintf(stderr, "Desired format not supported.\n");
+ goto audio_client_error;
+ }
+
+ UINT32 default_period, fundamental_period, min_period, max_period;
+ if (IAudioClient3_GetSharedModeEnginePeriod(win32_wasapi_buffer.audio_client, sound_format, &default_period, &fundamental_period, &min_period, &max_period)) {
+ fprintf(stderr, "Cannot fetch shared engine periods.\n");
+ goto audio_client_error;
+ }
+
+ UINT32 period_in_frames = CLAMP(buffer_frames, min_period, max_period);
+ if (IAudioClient3_InitializeSharedAudioStream(win32_wasapi_buffer.audio_client, 0, period_in_frames, sound_format, 0)) {
+ fprintf(stderr, "Failed to initialise shared audio stream.\n");
+ goto audio_client_error;
+ }
+
+ IAudioClient_GetBufferSize(win32_wasapi_buffer.audio_client, &win32_wasapi_buffer.buffer_frame_count);
+
+ if (IAudioClient_GetService(win32_wasapi_buffer.audio_client, &IID_IAudioRenderClient, (LPVOID *) &win32_wasapi_buffer.render_client)) {
+ fprintf(stderr, "Failed to get audio render client.\n");
+ goto audio_client_error;
+ }
+
+ BYTE *buffer;
+ if (IAudioRenderClient_GetBuffer(win32_wasapi_buffer.render_client, win32_wasapi_buffer.buffer_frame_count, &buffer)) {
+ fprintf(stderr, "Failed to get audio render client buffer.\n");
+ goto audio_render_client_error;
+ }
+
+ if (IAudioRenderClient_ReleaseBuffer(win32_wasapi_buffer.render_client, win32_wasapi_buffer.buffer_frame_count, AUDCLNT_BUFFERFLAGS_SILENT)) {
+ fprintf(stderr, "Failed to release audio render client buffer.\n");
+ goto audio_render_client_error;
+ }
+
+ if (IAudioClient_Start(win32_wasapi_buffer.audio_client)) {
+ fprintf(stderr, "Failed to start audio client playback.\n");
+ goto audio_render_client_error;
+ }
+
+ goto end;
+
+audio_render_client_error:
+ IAudioRenderClient_Release(win32_wasapi_buffer.render_client);
+
+audio_client_error:
+ IAudioClient_Release(win32_wasapi_buffer.audio_client);
+
+device_error:
+end:
+ IMMDevice_Release(device);
+
+ CoUninitialize();
+}
+
+internal void
+win32_fill_wasapi_buffer(UINT32 frames, struct wasapidemo_audio_buffer const *source)
+{
+ BYTE *data;
+ if (!IAudioRenderClient_GetBuffer(win32_wasapi_buffer.render_client, frames, &data)) {
+ s16 *dst = (s16 *) data, *src = source->buffer;
+ for (u32 i = 0; i < frames; i++) {
+ for (u32 j = 0; j < source->channels; j++)
+ *dst++ = *src++;
+ }
+
+ IAudioRenderClient_ReleaseBuffer(win32_wasapi_buffer.render_client, frames, 0);
+ }
+}
+
+internal void
+win32_wasapi_stop(void)
+{
+ IAudioClient_Stop(win32_wasapi_buffer.audio_client);
+}
+
+#define _USE_MATH_DEFINES
+#include <math.h>
+
+void
+init_sound(struct wasapidemo_mixer *mixer)
+{
+ (void) mixer;
+}
+
+void
+render_sound(struct wasapidemo_audio_buffer *buffer, struct wasapidemo_mixer *mixer, f32 dt)
+{
+ (void) dt;
+
+ const u32 wave_period = buffer->sample_rate / mixer->tone_hz;
+
+ s16 *sample = buffer->buffer;
+ for (u32 i = 0; i < buffer->expected_samples; i++) {
+ f32 v = sinf(mixer->tone_t);
+
+ s16 sample_value = (s16) (v * mixer->tone_volume);
+ for (u32 j = 0; j < buffer->channels; j++)
+ *sample++ = sample_value;
+
+ const f32 PI = M_PI;
+ mixer->tone_t += 2 * PI / wave_period;
+ if (mixer->tone_t >= 2 * PI)
+ mixer->tone_t -= 2 * PI;
+ }
+}
+
diff --git a/wasapidemo/src/wasapidemo.h b/wasapidemo/src/wasapidemo.h
@@ -0,0 +1,177 @@
+#ifndef WSAPIDEMO_H
+#define WSAPIDEMO_H
+
+#include <inttypes.h>
+#include <limits.h>
+#include <stdalign.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+typedef unsigned char c8;
+
+typedef int32_t b32;
+
+typedef uint8_t u8;
+typedef uint16_t u16;
+typedef uint32_t u32;
+typedef uint64_t u64;
+
+typedef int8_t s8;
+typedef int16_t s16;
+typedef int32_t s32;
+typedef int64_t s64;
+
+typedef float f32;
+typedef double f64;
+
+#define internal static
+#define global_variable static
+#define function_local static
+
+#define KiB 1024ULL
+#define MiB (KiB * 1024)
+#define GiB (MiB * 1024)
+#define TiB (GiB * 1024)
+
+#define MSECS 1000ULL
+#define USECS (MSECS * 1000)
+#define NSECS (USECS * 1000)
+
+#define ARRLEN(arr) (sizeof (arr) / sizeof (arr)[0])
+
+#define TESTBIT(v, mask) (((v) & (mask)) == (mask))
+
+#define MIN(a, b) (((a) < (b)) ? (a) : (b))
+#define MAX(a, b) (((a) > (b)) ? (a) : (b))
+#define CLAMP(v, lo, hi) MAX(MIN((v), (hi)), (lo))
+
+#if HANDMADE_INTERNAL
+ #define ASSERT(condition) if (!(condition)) { *(volatile u32 *)0 = 0; }
+#else
+ #define ASSERT(condition)
+#endif
+
+#define IS_ALIGNED(addr, align) (((addr) & (align - 1)) == 0)
+#define ALIGN_NEXT(addr, align) (((addr) + (align - 1)) & ~(align - 1))
+#define ALIGN_PREV(addr, align) ((addr) & ~(align - 1))
+
+#include <immintrin.h>
+
+#define SOUND_CHANNELS 2
+#define SOUND_SAMPLE_RATE 48000
+#define SOUND_SAMPLE_BITS 16
+#define SOUND_FRAME_BYTES ((SOUND_SAMPLE_BITS / 8) * SOUND_CHANNELS)
+
+struct wasapidemo_audio_buffer {
+ void *buffer;
+ u32 expected_samples;
+ u32 sample_rate;
+ u32 channels;
+};
+
+#define WASAPIDEMO_MIXER_FUNDAMENTAL 256
+
+struct wasapidemo_mixer {
+ f32 tone_hz;
+ f32 tone_volume;
+ f32 tone_t;
+};
+
+void
+init_sound(struct wasapidemo_mixer *mixer);
+
+void
+render_sound(struct wasapidemo_audio_buffer *buffer, struct wasapidemo_mixer *mixer, f32 dt);
+
+#define USE_WASAPI 1
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+// TODO: remove me!
+#include <stdio.h>
+
+#define WIN32_LEAN_AND_MEAN 1
+#include <windows.h>
+#include <timeapi.h>
+
+// NOTE: multimedia thread characteristics header
+#include <avrt.h>
+
+// NOTE: shared audio format headers
+#include <mmreg.h>
+//#include <ksmedia.h> // TODO: find how to get KSDATAFORMAT_SUBTYPE_PCM
+
+// NOTE: wasapi headers
+#define COBJMACROS 1 // TODO: required to expose c-style macros for WASAPI, why?
+#define CONST_VTABLE 1 // TODO: taken from vlc/wasapi.c, what does this do?
+#include <initguid.h>
+#include <mmdeviceapi.h>
+#include <audioclient.h>
+
+// NOTE: dsound headers
+#include <dsound.h>
+
+#if HANDMADE_INTERNAL
+ #define WIN32_MEMORY_BASE ((LPVOID) (TiB * 2))
+#else
+ #define WIN32_MEMORY_BASE NULL
+#endif
+
+#define WIN32_FILE_PATH_MAX 256
+
+internal u64
+win32_perf_micros(void);
+
+internal void
+win32_process_pending_messages(void);
+
+LRESULT CALLBACK
+win32_window_proc(HWND window, UINT msg, WPARAM wparam, LPARAM lparam);
+
+struct win32_dsound_buffer {
+ LPDIRECTSOUNDBUFFER buffer;
+ u32 buffer_size;
+ u32 buffer_cursor;
+ u32 sample_rate;
+ u32 sample_bits;
+ u32 channels;
+ u32 block_size;
+};
+
+internal void
+win32_init_dsound(HWND window, WAVEFORMATEX *sound_format, u32 buffer_size);
+
+internal void
+win32_clear_dsound_buffer(void);
+
+internal void
+win32_fill_dsound_buffer(DWORD ptr, DWORD size, struct wasapidemo_audio_buffer const *source);
+
+internal void
+win32_dsound_stop(void);
+
+struct win32_wasapi_buffer {
+ IAudioClient3 *audio_client;
+ IAudioRenderClient *render_client;
+ u32 buffer_frame_count;
+};
+
+#define REFTIMES_PER_SEC 10000000
+#define REFTIMES_PER_MSEC 10000
+
+internal void
+win32_init_wasapi(WAVEFORMATEX *sound_format, u32 buffer_frames);
+
+internal void
+win32_fill_wasapi_buffer(UINT32 frames, struct wasapidemo_audio_buffer const *source);
+
+internal void
+win32_wasapi_stop(void);
+
+#endif