wintoys

wintoys.git
git clone git://git.lenczewski.org/wintoys.git
Log | Files | Refs | README

commit 619ca99bc33edf90feecb096e798a4425ffae974
Author: MikoĊ‚aj Lenczewski <mikolaj@lenczewski.org>
Date:   Mon, 12 Oct 2026 00:35:36 +0100

Initial commit

Diffstat:
AREADME | 3+++
Agldemo/build.bat | 23+++++++++++++++++++++++
Agldemo/setenv.bat | 6++++++
Agldemo/win32_gldemo.c | 214+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Avkdemo/build.bat | 28++++++++++++++++++++++++++++
Avkdemo/setenv.bat | 6++++++
Avkdemo/shader.frag | 8++++++++
Avkdemo/shader.vert | 20++++++++++++++++++++
Avkdemo/vkdemo.c | 775+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Avkdemo/vkdemo.h | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Avkdemo/win32_vkdemo.c | 290+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awasapidemo/.editorconfig | 16++++++++++++++++
Awasapidemo/build.sh | 23+++++++++++++++++++++++
Awasapidemo/clean.sh | 5+++++
Awasapidemo/debug.sh | 5+++++
Awasapidemo/misc/wasapidemo.rdbg | 0
Awasapidemo/src/wasapidemo.c | 598+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awasapidemo/src/wasapidemo.h | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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, &region1, &region1_size, &region2, &region2_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, &region1, &region1_size, &region2, &region2_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