A lightweight, multi-platform game engine built in C using the Vulkan graphics API.
Koru is designed to be fast, portable, and easy to extend — providing a low-level foundation for game development across platforms including Windows, Linux, and soon consoles and mobile devices.
Koru is a C99-based game engine focused on performance, portability, and modularity. It's ideal for developers who want fine-grained control over rendering, input, memory, and platform-specific functionality without the overhead of high-level abstractions.
- stb-image.h - From STB by Sean Barrett - Lightweight way to handle and load images
- Written entirely in C99 – minimal dependencies
- Vulkan-based renderer for high-performance graphics
- Cross-platform: Windows, Linux, Mac (with future plans for consoles and mobile)
- Modular architecture for easy extension
- Custom memory allocators, logging, assertions, and data structures etc
The high-level architecture of Koru is divided into four main layers:
Handles OS-specific functionality:
- Windowing: Creates and manages windows on Windows/Linux/Mac.
- Input: Manages keyboard, mouse, and gamepad input.
- File I/O: Provides cross-platform file system access.
- Console Output: Handles logging and debugging output.
- Memory Management: Allocates and frees memory efficiently.
- Renderer API Extensions: Integrates with Vulkan for rendering.
Provides fundamental utilities and services:
- Logger: Logs messages for debugging and monitoring.
- Assertions: Ensures correctness during development.
- Data Structures: Efficient containers like dynamic arrays, linked lists, etc.
- Memory Allocators: Custom allocators for optimized memory usage.
- Math Library: Vector, matrix, and quaternion operations.
- Parsers: Parses formats like XML, CSV, JSON, etc.
- Engine Configuration: Loads and manages engine settings.
- Profiling: Measures performance bottlenecks.
- Async File I/O: Asynchronous file operations for non-blocking I/O.
- Localization: Supports internationalization.
- String Library: String manipulation utilities.
- Random Number Generator (RNG): Generates random numbers for simulations and AI.
Manages various assets:
- Images: Texture loading and caching.
- Materials: Shader and lighting configurations.
- Meshes: 3D model loading and processing.
- Animations: Skeletal animations and timeline systems.
- World Maps: Terrain and level data management.
Contains higher-level systems:
- Renderer Front End: GUI, camera management, post-processing effects.
- Renderer Back End: Scene graph, materials, shaders, meshes, textures.
- Audio: 2D/3D audio playback, effects, and spatial audio.
- Others: Animation/timelines, event system, ECS, state machines, collision/physics.
Here’s a summary of the features to be implemented in v0.1:
- Lightweight build system: Simple scripts for compiling on Windows and Linux.
- Low-level utilities:
- Dynamic arrays
- String handling
- Memory allocators
- Profiling tools
- Assertions
- Logging
- Platform layer:
- Window creation and management
- Input handling (keyboard, mouse, gamepad)
- File I/O
- Console output
- Renderer/API Abstraction Layer: Uses Vulkan for cross-platform rendering.
- Memory Management: Custom allocators for efficient memory use.
- Scenegraph/ECS: Hierarchical scene management and entity-component-system architecture.
- Profiling/Debugging utilities: Tools for measuring performance and identifying bottlenecks.
- "Scripting" support via hot reloading: Experimental support for scripting through hot-reloading mechanisms.
- Mobile and console support
- Full audio subsystem
- Physics and collision detection
- Advanced animation system
- Improved asset pipeline
Check out the diagrams included in the assets/ folder to understand how everything fits together:
| Diagram | Description |
|---|---|
![]() |
High-level overview of system modules and interactions |
![]() |
File structure and module organization |
![]() |
Summary of current capabilities and roadmap |
.
├── assets
│ ├── Koru Engine Architecture.png
│ ├── Koru Project Structure.png
│ ├── Koru v0.1 Feature List.png
│ └── shaders
│ ├── Builtin.ObjectShader.frag.glsl
│ └── Builtin.ObjectShader.vert.glsl
├── bin
│ ├── assets
│ │ ├── Koru Engine Architecture.png
│ │ ├── Koru Project Structure.png
│ │ ├── Koru v0.1 Feature List.png
│ │ └── shaders
│ │ ├── Builtin.ObjectShader.frag.glsl
│ │ ├── Builtin.ObjectShader.frag.spv
│ │ ├── Builtin.ObjectShader.vert.glsl
│ │ └── Builtin.ObjectShader.vert.spv
│ ├── console.log
│ ├── libengine.so
│ ├── testbed
│ └── tests
├── build-all.bat
├── build-all.sh
├── clean-all.bat
├── clean-all.sh
├── Doxyfile # Configuration for generating API documentation with Doxygen
├── engine
│ ├── build.bat # Windows: Builds engine DLL
│ ├── build.sh # Linux: Builds engine shared object (.so)
│ └── src
│ ├── containers
│ │ ├── darray.c # Implementation of dynamic array container
│ │ └── darray.h # Public interface for dynamic arrays
│ ├── core
│ │ ├── application.c # Application lifecycle implementation (init/run/shutdown)
│ │ ├── application.h # Public interface for the application system
│ │ ├── asserts.h # Custom assertion macros and debugging utilities
│ │ ├── clock.c
│ │ ├── clock.h
│ │ ├── event.c # Event system implementation (register/unregister/fire)
│ │ ├── event.h # Public interface for event handling system
│ │ ├── input.c
│ │ ├── input.h
│ │ ├── kmemory.c # Memory allocation system with tagging support
│ │ ├── kmemory.h # Interface for tagged memory management
│ │ ├── kstring.c
│ │ ├── kstring.h
│ │ ├── logger.c # Logging system with color-coded output
│ │ └── logger.h # Public interface for logging module
│ ├── defines.h # Common type definitions, macros, and platform detection
│ ├── entry.h # Entry point interface; declares create_game()
│ ├── game_types.h # Game-related types used across engine and game logic
│ ├── math
│ │ ├── kmath.c
│ │ ├── kmath.h
│ │ └── math_types.h
│ ├── memory
│ │ ├── linear_allocator.c
│ │ └── linear_allocator.h
│ ├── platform
│ │ ├── filesystem.c
│ │ ├── filesystem.h
│ │ ├── platform_android.c
│ | ├── platform.h # Platform abstraction layer interface
│ │ ├── platform_ios.c
│ │ ├── platform_linux.c # Linux-specific implementation of platform functions
│ │ ├── platform_macos.c
│ │ └── platform_win32.c # Windows-specific implementation of platform functions
│ └── renderer
│ ├── renderer_backend.c
│ ├── renderer_backend.h
│ ├── renderer_frontend.c
│ ├── renderer_frontend.h
│ ├── renderer_types.inl
│ └── vulkan
│ ├── shaders
│ │ ├── vulkan_object_shader.c
│ │ └── vulkan_object_shader.h
│ ├── vulkan_backend.c
│ ├── vulkan_backend.h
│ ├── vulkan_buffer.c
│ ├── vulkan_buffer.h
│ ├── vulkan_command_buffer.c
│ ├── vulkan_command_buffer.h
│ ├── vulkan_device.c
│ ├── vulkan_device.h
│ ├── vulkan_fence.c
│ ├── vulkan_fence.h
│ ├── vulkan_framebuffer.c
│ ├── vulkan_framebuffer.h
│ ├── vulkan_image.c
│ ├── vulkan_image.h
│ ├── vulkan_pipeline.c
│ ├── vulkan_pipeline.h
│ ├── vulkan_platform.h
│ ├── vulkan_renderpass.c
│ ├── vulkan_renderpass.h
│ ├── vulkan_shader_utils.c
│ ├── vulkan_shader_utils.h
│ ├── vulkan_swapchain.c
│ ├── vulkan_swapchain.h
│ ├── vulkan_types.inl
│ ├── vulkan_utils.c
│ └── vulkan_utils.h
├── LICENSE # MIT License file
├── Makefile.engine.linux.mak
├── Makefile.engine.windows.mak
├── Makefile.testbed.linux.mak
├── Makefile.testbed.windows.mak
├── post-build.bat
├── post-build.sh
├── README.md # This file - main project overview and guide
├── testbed
| ├── build.bat # Windows: Builds testbed application
| ├── build.sh # Linux: Builds testbed application
| └── src
| ├── entry.c # Entry point for testbed app; implements create_game()
| ├── game.c # Testbed game logic (initialize/update/render stubs)
| └── game.h # Interface for testbed game implementation
└── tests
├── build.bat
├── build.sh
└── src
├── expect.h
├── main.c
├── memory
│ ├── linear_allocator_tests.c
│ └── linear_allocator_tests.h
├── test_manager.c
└── test_manager.h
37 directories, 176 files
Check tasks.json for sequence:
| File | Purpose | Notes |
|---|---|---|
engine/build.sh |
Builds engine .so (shared lib) on Linux |
Uses Vulkan/XCB/X11 |
engine/build.bat |
Builds engine .dll on Windows |
Links with Vulkan SDK |
testbed/build.sh |
Builds testbed app using engine .so on Linux |
Sets RPATH for easy loading |
testbed/build.bat |
Builds testbed .exe using engine .dll on Windows |
Links .lib import library |
build-all.sh |
Runs all Linux builds sequentially | Checks for errors |
build-all.bat |
Runs all Windows builds sequentially | Same as above but for Windows |
Make sure Clang and the Vulkan SDK are installed. Alternatively install VSCode and run the debugger (Commonly F5, and run the engine)
# Give execution permissions
chmod +x build-all.sh
# Run the build
./build-all.shThe output will be placed in bin/:
libengine.so– The engine shared librarytestbed– The testbed executable
Ensure:
- Clang is available
- Vulkan SDK is installed and
VULKAN_SDKenvironment variable is set - VS Code makes it easy to run the tasks in the .vscode/tasks.json file.
Then run:
build-all.batTo generate our Doxygen documentation, run:
doxygen DoxyfileOutput files:
bin\engine.dllbin\testbed.exe
Plans to expand Koru include:
-
✅ Rendering Layer
- Vulkan renderer initialization
- Swapchain setup
- Command buffers, pipelines, shaders
-
✅ Input System
- Keyboard/mouse/gamepad support
- Input mapping and event system
-
✅ Asset Management
- Texture loader
- Mesh format and loader
- Material system
-
✅ Audio System
- Basic sound playback
- Cross-platform audio backend
-
✅ Multi-Platform Support
- Console (PlayStation, Xbox, Switch)
- Mobile (Android/iOS)
-
✅ Editor Tools
- Simple scene editor
- Asset pipeline tools
Contributions are welcome! Whether you're interested in:
- Writing documentation
- Implementing new features
- Fixing bugs
- Improving tooling or build scripts
Feel free to open an issue or PR!
This project is licensed under the MIT License – see the LICENSE file for details.
## 🚀 Next Steps
TO-DO: Add:
- ✅ A **Getting Started Guide** (`docs/getting-started.md`)
- ✅ A **Contribution Guide** (`CONTRIBUTING.md`)
- ✅ A **Code of Conduct** (`CODE_OF_CONDUCT.md`)
- ✅ GitHub Actions for CI builds (optional)
A deep dive into how Koru sets up its Vulkan-based rendering system — from instance creation to frame submission.
Koru is a low-level game engine written in C99 that uses the Vulkan graphics API for rendering. This document explains the entire rendering pipeline setup used in Koru, including:
- Instance creation
- Physical device selection
- Logical device creation
- Surface & swapchain setup
- Render passes
- Framebuffers
- Command buffers
This guide assumes some basic knowledge of C and Vulkan concepts. If you're new to Vulkan, this will help understand what’s happening behind the scenes as you run the testbed.
Vulkan gives developers explicit control over GPU operations. It allows fine-grained access to queues, memory, and rendering pipelines — which is great for performance, but comes at the cost of complexity.
Koru abstracts much of that complexity using a modular architecture, allowing you to build a rendering system step-by-step without needing to manage everything manually every time.
+------------------+
| Application |
| Logic |
| (game.c) |
+------------------+
|
v
+------------------+
| Renderer |
| Frontend |
| (renderer_frontend.h/c) |
+------------------+
|
v
+------------------+
| Platform |
| Abstraction |
| (platform_linux.c)|
+------------------+
|
v
+------------------+
| Vulkan |
| Device |
| (vulkan_device.c/h) |
+------------------+
|
v
+------------------+
| Swapchain |
| Management |
| (vulkan_swapchain.c/h) |
+------------------+
|
v
+------------------+
| Render Pass / |
| Framebuffer |
| (vulkan_renderpass.c/h) |
+------------------+
|
v
+------------------+
| Image |
| Management |
| (vulkan_image.c/h) |
+------------------+
|
v
+------------------+
| Command Buffer |
| Submission |
| (renderer_frontend.c) |
+------------------+
This shows how each module fits into the overall rendering flow.
Let’s walk through each stage of the rendering setup process, with real code examples from the engine.
The first thing to do is creating a VkInstance — this connects the app to the Vulkan library.
// From vulkan_renderer_backend_initialize()
VkApplicationInfo app_info = {VK_STRUCTURE_TYPE_APPLICATION_INFO};
app_info.apiVersion = VK_MAKE_API_VERSION(0, 1, 4, 0);
app_info.pApplicationName = application_name;
app_info.applicationVersion = VK_MAKE_API_VERSION(0, 0, 0, 1);
app_info.pEngineName = "Koru Engine";
app_info.engineVersion = VK_MAKE_API_VERSION(0, 0, 0, 1);
VkInstanceCreateInfo create_info = {VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
create_info.pApplicationInfo = &app_info;
const char** required_extensions = darray_create(const char*);
darray_push(required_extensions, &VK_KHR_SURFACE_EXTENSION_NAME);
platform_get_required_extension_names(&required_extensions);
create_info.enabledExtensionCount = darray_length(required_extensions);
create_info.ppEnabledExtensionNames = required_extensions;
// Enable validation layers if _DEBUG is defined
#if defined(_DEBUG)
create_info.enabledLayerCount = required_validation_layer_count;
create_info.ppEnabledLayerNames = required_validation_layer_names;
#endif
VK_CHECK(vkCreateInstance(&create_info, context.allocator, &context.instance));✅ This creates the main connection to Vulkan, enabling extensions and validation layers.
Next, to create a surface — this is what connects the window to the GPU.
In Linux:
// platform_linux.c
xcb_intern_atom_reply_t* wm_delete_reply = xcb_intern_atom_reply(...);
state->wm_delete_win = wm_delete_reply->atom;
// Create surface
VkXcbSurfaceCreateInfoKHR surface_create_info = {VK_STRUCTURE_TYPE_XCB_SURFACE_CREATE_INFO_KHR};
surface_create_info.connection = state->connection;
surface_create_info.window = state->window;
vkCreateXcbSurfaceKHR(context->instance, &surface_create_info, context->allocator, &context->surface);In Windows: The engine uses vkCreateWin32SurfaceKHR() instead.
✅ A surface is essential because it defines where rendered frames go usually the screen or a window.
Once an instance and a surface are ready, query available GPUs:
// vulkan_device.c
u32 physical_device_count = 0;
VK_CHECK(vkEnumeratePhysicalDevices(context->device.physical_device, &physical_device_count, 0));
VkPhysicalDevice physical_devices[physical_device_count];
VK_CHECK(vkEnumeratePhysicalDevices(context->instance, &physical_device_count, physical_devices));Then select one that meets requirements:
for (u32 i = 0; i < physical_device_count; ++i) {
VkPhysicalDeviceProperties properties;
vkGetPhysicalDeviceProperties(physical_devices[i], &properties);
VkPhysicalDeviceFeatures features;
vkGetPhysicalDeviceFeatures(physical_devices[i], &features);
// Check queue family support
vulkan_physical_device_queue_family_info queue_info = {};
vulkan_swapchain_support_info swapchain_support = {};
b8 result = physical_device_meets_requirements(
physical_devices[i],
context->surface,
&properties,
&features,
&requirements,
&queue_info,
&swapchain_support);
}🔍 Requirements include:
- Graphics/Compute/Present/Transfer queues
- Extensions like
VK_KHR_SWAPCHAIN_EXTENSION_NAME - Features like
samplerAnisotropy - Prefer discrete GPU (optional)
After selecting a suitable physical device, create a logical device to interface with the GPU.
// vulkan_device.c
b8 present_shares_graphics_queue = (context->device.graphics_queue_index == context->device.present_queue_index);
b8 transfer_shares_graphics_queue = (context->device.graphics_queue_index == context->device.transfer_queue_index);
u32 index_count = 1;
if (!present_shares_graphics_queue) index_count++;
if (!transfer_shares_graphics_queue) index_count++;
u32 indices[index_count];
indices[0] = context->device.graphics_queue_index;
...Define the queues as needed:
VkDeviceQueueCreateInfo queue_create_infos[index_count];
for (u32 i = 0; i < index_count; ++i) {
queue_create_infos[i].sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queue_create_infos[i].queueFamilyIndex = indices[i];
queue_create_infos[i].queueCount = 1;
f32 queue_priority = 1.0f;
queue_create_infos[i].pQueuePriorities = &queue_priority;
}Finally, create the logical device:
VkDeviceCreateInfo device_create_info = {VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
device_create_info.queueCreateInfoCount = index_count;
device_create_info.pQueueCreateInfos = queue_create_infos;
device_create_info.pEnabledFeatures = &device_features;
VK_CHECK(vkCreateDevice(context->device.physical_device, &device_create_info, context->allocator, &context->device.logical_device));✅ This creates a way to submit commands to the GPU.
Once the device is created, get handles to the actual queues:
vkGetDeviceQueue(context->device.logical_device, context->device.graphics_queue_index, 0, &context->device.graphics_queue);
vkGetDeviceQueue(context->device.logical_device, context->device.present_queue_index, 0, &context->device.present_queue);These are used later to:
- Submit draw commands (
graphics_queue) - Present frames (
present_queue) - Copy data (
transfer_queue)
Before creating a swapchain, check what formats and modes are supported:
// vulkan_device.c
void vulkan_device_query_swapchain_support(...) {
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &out_support->capabilities);
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &out_support->format_count, 0);
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &out_support->present_mode_count, 0);
}This helps choose the best image format and presentation mode.
Now create the swapchain based on what the device supports:
// vulkan_swapchain.c
VkSwapchainCreateInfoKHR create_info = {VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR};
create_info.surface = context->surface;
create_info.minImageCount = image_count;
create_info.imageFormat = swapchain->image_format.format;
create_info.imageExtent = swapchain_extent;
create_info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
if (context->device.graphics_queue_index != context->device.present_queue_index) {
u32 queueFamilyIndices[] = {
(u32)context->device.graphics_queue_index,
(u32)context->device.present_queue_index};
create_info.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
create_info.queueFamilyIndexCount = 2;
create_info.pQueueFamilyIndices = queueFamilyIndices;
} else {
create_info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
}
VK_CHECK(vkCreateSwapchainKHR(context->device.logical_device, &create_info, context->allocator, &swapchain->handle));🔄 The swapchain acts like a rotating carousel of images:
- One is being drawn
- One is being displayed
- One is waiting to be used next
This avoids tearing and ensures smooth rendering.
Each image in the swapchain needs a view so the GPU can interpret it:
for (u32 i = 0; i < swapchain->image_count; ++i) {
VkImageViewCreateInfo view_info = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
view_info.image = swapchain->images[i];
view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
view_info.format = swapchain->image_format.format;
view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
view_info.subresourceRange.baseMipLevel = 0;
view_info.subresourceRange.levelCount = 1;
view_info.subresourceRange.baseArrayLayer = 0;
view_info.layerCount = 1;
VK_CHECK(vkCreateImageView(context->device.logical_device, &view_info, context->allocator, &swapchain->views[i]));
}🖼 Each image view wraps a raw VkImage, defining how it's accessed (e.g., color attachment, depth buffer).
For depth testing, a compatible depth format is needed:
b8 vulkan_device_detect_depth_format(vulkan_device* device) {
const u64 candidate_count = 3;
VkFormat candidates[candidate_count] = {
VK_FORMAT_D32_SFLOAT,
VK_FORMAT_D32_SFLOAT_S8_UINT,
VK_FORMAT_D24_UNORM_S8_UINT};
for (u64 i = 0; i < candidate_count; ++i) {
VkFormatProperties props;
vkGetPhysicalDeviceFormatProperties(device->physical_device, candidates[i], &props);
if ((props.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT)) {
device->depth_format = candidates[i];
return True;
}
}
KERROR("No supported depth format found.");
return False;
}🧃 Once selected, create a depth/stencil image and its view.
The render pass defines how rendering works — what attachments to use, how they behave, and what subpasses exist.
// vulkan_renderpass.c
VkAttachmentDescription attachments[2] = {};
attachments[0].format = swapchain->image_format.format;
attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachments[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
attachments[1].format = context->device.depth_format;
attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkSubpassDescription subpass = {};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &color_ref;
subpass.pDepthStencilAttachment = &depth_stencil_ref;
VkRenderPassCreateInfo info = {};
info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
info.attachmentCount = 2;
info.pAttachments = attachments;
info.subpassCount = 1;
info.pSubpasses = &subpass;
VK_CHECK(vkCreateRenderPass(context->device.logical_device, &info, context->allocator, &renderpass->handle));🎨 The render pass is like a blueprint — it tells the GPU:
- What kind of data it will write to
- How to handle clearing and storing it
- What layout transitions happen between steps
Framebuffers combine images and render passes:
VkFramebufferCreateInfo fb_info = {VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
fb_info.renderPass = renderpass->handle;
fb_info.attachmentCount = 2;
fb_info.pAttachments = attachments;
fb_info.width = width;
fb_info.height = height;
fb_info.layers = 1;
VK_CHECK(vkCreateFramebuffer(device, &fb_info, 0, &framebuffer));🖼 Each framebuffer links:
- A color image from the swapchain
- A depth/stencil image
- The render pass that defines how to use them
Command buffers store drawing commands:
// Begin command recording
VkCommandBufferBeginInfo begin_info = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
vkBeginCommandBuffer(command_buffer, &begin_info);
// Record draw commands here...
vkEndCommandBuffer(command_buffer);Then submit to the GPU:
VkSubmitInfo submit_info = {VK_STRUCTURE_TYPE_SUBMIT_INFO};
submit_info.waitSemaphoreCount = 1;
submit_info.pWaitSemaphores = &image_available_semaphore;
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &command_buffer;
VK_CHECK(vkQueueSubmit(graphics_queue, 1, &submit_info, fence));And finally present:
VkPresentInfoKHR present_info = {VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
present_info.swapchainCount = 1;
present_info.pSwapchains = &swapchain->handle;
present_info.pImageIndices = &image_index;
VkResult result = vkQueuePresentKHR(present_queue, &present_info);+-------------------+
| Application |
| Entry Point |
| (application_run())|
+-------------------+
|
v
+-------------------+
| Create Instance |
| (vkCreateInstance)|
+-------------------+
|
v
+-------------------+
| Create Surface |
| (vkCreateXcbSurfaceKHR) |
+-------------------+
|
v
+-------------------+
| Enumerate Physical|
| Devices |
| (vkEnumeratePhysicalDevices) |
+-------------------+
|
v
+-------------------+
| Select Suitable |
| Physical Device |
| (select_physical_device) |
+-------------------+
|
v
+-------------------+
| Create Logical |
| Device |
| (vkCreateDevice) |
+-------------------+
|
v
+-------------------+
| Get Queue Handles |
| (vkGetDeviceQueue) |
+-------------------+
|
v
+-------------------+
| Query Swapchain |
| Support |
| (vkGetPhysicalDeviceSurface*) |
+-------------------+
|
v
+-------------------+
| Create Swapchain |
| (vkCreateSwapchainKHR) |
+-------------------+
|
v
+-------------------+
| Create Image Views|
| (vkCreateImageView) |
+-------------------+
|
v
+-------------------+
| Detect Depth |
| Format |
| (vulkan_device_detect_depth_format) |
+-------------------+
|
v
+-------------------+
| Create Depth Image|
| (vulkan_image_create) |
+-------------------+
|
v
+-------------------+
| Create Render Pass|
| (vkCreateRenderPass) |
+-------------------+
|
v
+-------------------+
| Create Framebuffer|
| (vkCreateFramebuffer) |
+-------------------+
|
v
+-------------------+
| Begin Main Loop |
| (vkAcquireNextImageKHR) |
+-------------------+
|
v
+-------------------+
| Begin Render Pass |
| (vkCmdBeginRenderPass) |
+-------------------+
|
v
+-------------------+
| Record Draw |
| Commands |
+-------------------+
|
v
+-------------------+
| End Render Pass |
| (vkCmdEndRenderPass) |
+-------------------+
|
v
+-------------------+
| Submit Command |
| Buffer to GPU |
| (vkQueueSubmit) |
+-------------------+
|
v
+-------------------+
| Present Frame |
| (vkQueuePresentKHR) |
+-------------------+
This is the full rendering loop that happens every frame.
Tracks global Vulkan state:
typedef struct vulkan_context {
VkInstance instance;
VkSurfaceKHR surface;
VkDebugUtilsMessengerEXT debug_messenger;
vulkan_device device;
vulkan_swapchain swapchain;
u32 image_index;
u32 current_frame;
b8 recreating_swapchain;
i32 (*find_memory_index)(...);
} vulkan_context;Holds physical/logical device and queue info:
typedef struct vulkan_device {
VkPhysicalDevice physical_device;
VkDevice logical_device;
vulkan_swapchain_support_info swapchain_support;
i32 graphics_queue_index;
i32 present_queue_index;
i32 transfer_queue_index;
VkQueue graphics_queue;
VkQueue present_queue;
VkQueue transfer_queue;
VkPhysicalDeviceProperties properties;
VkPhysicalDeviceFeatures features;
VkPhysicalDeviceMemoryProperties memory;
VkFormat depth_format;
} vulkan_device;Manages images and depth buffer:
typedef struct vulkan_swapchain {
VkSurfaceFormatKHR image_format;
u8 max_frames_in_flight;
VkSwapchainKHR handle;
u32 image_count;
VkImage* images;
VkImageView* views;
vulkan_image depth_attachment;
} vulkan_swapchain;Defines clear values and viewport settings:
typedef struct vulkan_renderpass {
VkRenderPass handle;
f32 clear_r, clear_g, clear_b, clear_a;
f32 clear_depth;
u32 clear_stencil;
f32 viewport_x, viewport_y, viewport_w, viewport_h;
f32 scissor_x, scissor_y, scissor_w, scissor_h;
} vulkan_renderpass;Vulkan is explicit by design. Every operation must be described precisely. Here's why each part matters:
| Component | Purpose |
|---|---|
| Instance | Connects app to Vulkan driver |
| Surface | Links window to GPU output |
| Physical Device | Represents the actual GPU |
| Logical Device | Interface to the GPU |
| Queues | Workers for graphics/present/transfer |
| Swapchain | Manages sequence of renderable images |
| Images | Raw GPU memory to draw into |
| Image Views | Define how to read/write those images |
| Render Pass | Defines rendering rules (clear/store ops, layout) |
| Framebuffers | Combine images into something you can draw into |
| Command Buffers | Store GPU commands to execute later |
| Module | Responsibility |
|---|---|
vulkan_renderer_backend_initialize() |
Creates instance, surface, and starts device setup |
vulkan_device_create() |
Selects physical device, creates logical device |
vulkan_device_query_swapchain_support() |
Queries device capabilities for swapchain |
vulkan_swapchain_create() |
Initializes the swapchain and image views |
vulkan_device_detect_depth_format() |
Finds a usable depth format |
vulkan_renderpass_create() |
Sets up how we render |
vulkan_command_buffer_begin() |
Starts recording draw commands |
vulkan_swapchain_present() |
Presents the final image to the screen |
b8 vulkan_swapchain_acquire_next_image_index(...) {
VkResult result = vkAcquireNextImageKHR(
context->device.logical_device,
swapchain->handle,
timeout_ns,
image_available_semaphore,
fence,
out_image_index);
if (result == VK_ERROR_OUT_OF_DATE_KHR) {
vulkan_swapchain_recreate(context, width, height, swapchain);
return False;
} else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
KFATAL("Failed to acquire swapchain image!");
return False;
}
return True;
}This function gets the next image from the swapchain — and triggers recreation if needed.
Not critical — just means the shader cache doesn't exist yet.


