Skip to content

Instantly share code, notes, and snippets.

@terickson001
Last active December 5, 2025 13:48
Show Gist options
  • Select an option

  • Save terickson001/bdaa52ce621a6c7f4120abba8959ffe6 to your computer and use it in GitHub Desktop.

Select an option

Save terickson001/bdaa52ce621a6c7f4120abba8959ffe6 to your computer and use it in GitHub Desktop.
vulkan-tutorial example in Odin
import "shared:shaderc"
import "vendor:glfw"
import vk "vendor:vulkan"
MAX_FRAMES_IN_FLIGHT :: 2
Context :: struct
{
instance: vk.Instance,
device: vk.Device,
physical_device: vk.PhysicalDevice,
swap_chain: Swapchain,
pipeline: Pipeline,
queue_indices: [QueueFamily]int,
queues: [QueueFamily]vk.Queue,
surface: vk.SurfaceKHR,
window: glfw.WindowHandle,
command_pool: vk.CommandPool,
command_buffers: [MAX_FRAMES_IN_FLIGHT]vk.CommandBuffer,
vertex_buffer: Buffer,
index_buffer: Buffer,
image_available: [MAX_FRAMES_IN_FLIGHT]vk.Semaphore,
render_finished: [MAX_FRAMES_IN_FLIGHT]vk.Semaphore,
in_flight: [MAX_FRAMES_IN_FLIGHT]vk.Fence,
curr_frame: u32,
framebuffer_resized: bool,
}
Buffer :: struct
{
buffer: vk.Buffer,
memory: vk.DeviceMemory,
length: int,
size: vk.DeviceSize,
}
Pipeline :: struct
{
handle: vk.Pipeline,
render_pass: vk.RenderPass,
layout: vk.PipelineLayout,
}
QueueFamily :: enum
{
Graphics,
Present,
}
Swapchain :: struct
{
handle: vk.SwapchainKHR,
images: []vk.Image,
image_views: []vk.ImageView,
format: vk.SurfaceFormatKHR,
extent: vk.Extent2D,
present_mode: vk.PresentModeKHR,
image_count: u32,
support: SwapChainDetails,
framebuffers: []vk.Framebuffer,
}
SwapChainDetails :: struct
{
capabilities: vk.SurfaceCapabilitiesKHR,
formats: []vk.SurfaceFormatKHR,
present_modes: []vk.PresentModeKHR,
}
Vertex :: struct
{
pos: [2]f32,
color: [3]f32,
}
DEVICE_EXTENSIONS := [?]cstring{
"VK_KHR_swapchain",
};
VALIDATION_LAYERS := [?]cstring{"VK_LAYER_KHRONOS_validation"};
main :: proc()
{
using ctx: Context;
init_window(&ctx);
for q in &queue_indices do q = -1;
vertices := [?]Vertex{
{{-0.5, -0.5}, {0.0, 0.0, 1.0}},
{{ 0.5, -0.5}, {1.0, 0.0, 0.0}},
{{ 0.5, 0.5}, {0.0, 1.0, 0.0}},
{{-0.5, 0.5}, {1.0, 0.0, 0.0}},
};
indices := [?]u16{
0, 1, 2,
2, 3, 0,
};
init_vulkan(&ctx, vertices[:], indices[:]);
for !glfw.WindowShouldClose(window)
{
glfw.PollEvents();
draw_frame(&ctx, vertices[:], indices[:]);
}
vk.DeviceWaitIdle(device);
deinit_vulkan(&ctx);
glfw.DestroyWindow(window);
glfw.Terminate();
}
VERTEX_BINDING := vk.VertexInputBindingDescription{
binding = 0,
stride = size_of(Vertex),
inputRate = .VERTEX,
};
VERTEX_ATTRIBUTES := [?]vk.VertexInputAttributeDescription{
{
binding = 0,
location = 0,
format = .R32G32_SFLOAT,
offset = cast(u32)offset_of(Vertex, pos),
},
{
binding = 0,
location = 1,
format = .R32G32B32_SFLOAT,
offset = cast(u32)offset_of(Vertex, color),
},
};
draw_frame :: proc(using ctx: ^Context, vertices: []Vertex, indices: []u16)
{
vk.WaitForFences(device, 1, &in_flight[curr_frame], true, max(u64));
image_index: u32;
res := vk.AcquireNextImageKHR(device, swap_chain.handle, max(u64), image_available[curr_frame], {}, &image_index);
if res == .ERROR_OUT_OF_DATE_KHR || res == .SUBOPTIMAL_KHR || framebuffer_resized
{
framebuffer_resized = false;
recreate_swap_chain(ctx);
return;
}
else if res != .SUCCESS
{
fmt.eprintf("Error: Failed tp acquire swap chain image!\n");
os.exit(1);
}
vk.ResetFences(device, 1, &in_flight[curr_frame]);
vk.ResetCommandBuffer(command_buffers[curr_frame], {});
record_command_buffer(ctx, command_buffers[curr_frame], image_index);
submit_info: vk.SubmitInfo;
submit_info.sType = .SUBMIT_INFO;
wait_semaphores := [?]vk.Semaphore{image_available[curr_frame]};
wait_stages := [?]vk.PipelineStageFlags{{.COLOR_ATTACHMENT_OUTPUT}};
submit_info.waitSemaphoreCount = 1;
submit_info.pWaitSemaphores = &wait_semaphores[0];
submit_info.pWaitDstStageMask = &wait_stages[0];
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &command_buffers[curr_frame];
signal_semaphores := [?]vk.Semaphore{render_finished[curr_frame]};
submit_info.signalSemaphoreCount = 1;
submit_info.pSignalSemaphores = &signal_semaphores[0];
if res := vk.QueueSubmit(queues[.Graphics], 1, &submit_info, in_flight[curr_frame]); res != .SUCCESS
{
fmt.eprintf("Error: Failed to submit draw command buffer!\n");
os.exit(1);
}
present_info: vk.PresentInfoKHR;
present_info.sType = .PRESENT_INFO_KHR;
present_info.waitSemaphoreCount = 1;
present_info.pWaitSemaphores = &signal_semaphores[0];
swap_chains := [?]vk.SwapchainKHR{swap_chain.handle};
present_info.swapchainCount = 1;
present_info.pSwapchains = &swap_chains[0];
present_info.pImageIndices = &image_index;
present_info.pResults = nil;
vk.QueuePresentKHR(queues[.Present], &present_info);
curr_frame = (curr_frame + 1) % MAX_FRAMES_IN_FLIGHT;
}
init_window :: proc(using ctx: ^Context)
{
glfw.Init();
glfw.WindowHint(glfw.CLIENT_API, glfw.NO_API);
glfw.WindowHint(glfw.RESIZABLE, 0);
window = glfw.CreateWindow(800, 600, "Vulkan", nil, nil);
glfw.SetWindowUserPointer(window, ctx);
glfw.SetFramebufferSizeCallback(window, framebuffer_size_callback);
}
framebuffer_size_callback :: proc "c" (window: glfw.WindowHandle, width, height: i32)
{
using ctx := cast(^Context)glfw.GetWindowUserPointer(window);
framebuffer_resized = true;
}
init_vulkan :: proc(using ctx: ^Context, vertices: []Vertex, indices: []u16)
{
context.user_ptr = &instance;
get_proc_address :: proc(p: rawptr, name: cstring)
{
(cast(^rawptr)p)^ = glfw.GetInstanceProcAddress((^vk.Instance)(context.user_ptr)^, name);
}
vk.load_proc_addresses(get_proc_address);
create_instance(ctx);
vk.load_proc_addresses(get_proc_address);
extensions := get_extensions();
for ext in &extensions do fmt.println(cstring(&ext.extensionName[0]));
create_surface(ctx);
get_suitable_device(ctx);
find_queue_families(ctx);
fmt.println("Queue Indices:");
for q, f in queue_indices do fmt.printf(" %v: %d\n", f, q);
create_device(ctx);
for q, f in &queues
{
vk.GetDeviceQueue(device, u32(queue_indices[f]), 0, &q);
}
create_swap_chain(ctx);
create_image_views(ctx);
create_graphics_pipeline(ctx, "shader.vert", "shader.frag");
create_framebuffers(ctx);
create_command_pool(ctx);
create_vertex_buffer(ctx, vertices);
create_index_buffer(ctx, indices);
create_command_buffers(ctx);
create_sync_objects(ctx);
return;
}
deinit_vulkan :: proc(using ctx: ^Context)
{
cleanup_swap_chain(ctx);
vk.FreeMemory(device, index_buffer.memory, nil);
vk.DestroyBuffer(device, index_buffer.buffer, nil);
vk.FreeMemory(device, vertex_buffer.memory, nil);
vk.DestroyBuffer(device, vertex_buffer.buffer, nil);
vk.DestroyPipeline(device, pipeline.handle, nil);
vk.DestroyPipelineLayout(device, pipeline.layout, nil);
vk.DestroyRenderPass(device, pipeline.render_pass, nil);
for i in 0..<MAX_FRAMES_IN_FLIGHT
{
vk.DestroySemaphore(device, image_available[i], nil);
vk.DestroySemaphore(device, render_finished[i], nil);
vk.DestroyFence(device, in_flight[i], nil);
}
vk.DestroyCommandPool(device, command_pool, nil);
vk.DestroyDevice(device, nil);
vk.DestroySurfaceKHR(instance, surface, nil);
vk.DestroyInstance(instance, nil);
}
compile_shader :: proc(name: string, kind: shaderc.shader_kind) -> []u8
{
src_path := fmt.tprintf("./shaders/%s", name);
cmp_path := fmt.tprintf("./shaders/compiled/%s.spv", name);
src_time, src_err := os.last_write_time_by_name(src_path);
if (src_err != os.ERROR_NONE)
{
fmt.eprintf("Failed to open shader %q\n", src_path);
return nil;
}
cmp_time, cmp_err := os.last_write_time_by_name(cmp_path);
if cmp_err == os.ERROR_NONE && cmp_time >= src_time
{
code, _ := os.read_entire_file(cmp_path);
return code;
}
comp := shaderc.compiler_initialize();
options := shaderc.compile_options_initialize();
defer
{
shaderc.compiler_release(comp);
shaderc.compile_options_release(options);
}
shaderc.compile_options_set_optimization_level(options, .performance);
code, _ := os.read_entire_file(src_path);
c_path := strings.clone_to_cstring(src_path, context.temp_allocator);
res := shaderc.compile_into_spv(comp, cstring(raw_data(code)), len(code), kind, c_path, cstring("main"), options);
defer shaderc.result_release(res);
status := shaderc.result_get_compilation_status(res);
if status != .success
{
fmt.printf("%s: Error: %s\n", name, shaderc.result_get_error_message(res));
return nil;
}
length := shaderc.result_get_length(res);
out := make([]u8, length);
c_out := shaderc.result_get_bytes(res);
mem.copy(raw_data(out), c_out, int(length));
os.write_entire_file(cmp_path, out);
return out;
}
/*
compile_shader :: proc(path: string) -> []u8
{
code, ok := os.read_entire_file(path);
if !ok
{
fmt.eprintf("Failed to open shader %q\n", path);
return nil;
}
defer delete(code);
input := glslang.input_t{
language = glslang.SOURCE_GLSL,
stage = glslang.STAGE_VERTEX,
client = glslang.CLIENT_VULKAN,
client_version = glslang.TARGET_VULKAN_1_0,
target_language = glslang.TARGET_SPV,
target_language_version = glslang.TARGET_SPV_1_5,
code = cstring(&code[0]),
default_version = 100,
default_profile = glslang.CORE_PROFILE,
force_default_version_and_profile = 0,
forward_compatible = 0,
messages = glslang.MSG_DEFAULT_BIT,
};
shader := glslang.shader_create(&input);
defer glslang.shader_delete(shader);
if res := glslang.shader_preprocess(shader, &input); res == 0
{
fmt.eprintf("GLSL preprocessing failed: %s\n", path);
fmt.eprintf("%s\n", glslang.shader_get_info_log(shader));
fmt.eprintf("%s\n", glslang.shader_get_info_debug_log(shader));
fmt.eprintf("%s\n", code);
}
return nil;
}
*/
create_instance :: proc(using ctx: ^Context)
{
app_info: vk.ApplicationInfo;
app_info.sType = .APPLICATION_INFO;
app_info.pApplicationName = "Hello Triangle";
app_info.applicationVersion = vk.MAKE_VERSION(0, 0, 1);
app_info.pEngineName = "No Engine";
app_info.engineVersion = vk.MAKE_VERSION(1, 0, 0);
app_info.apiVersion = vk.API_VERSION_1_0;
create_info: vk.InstanceCreateInfo;
create_info.sType = .INSTANCE_CREATE_INFO;
create_info.pApplicationInfo = &app_info;
glfw_ext := glfw.GetRequiredInstanceExtensions();
create_info.ppEnabledExtensionNames = raw_data(glfw_ext);
create_info.enabledExtensionCount = cast(u32)len(glfw_ext);
when ODIN_DEBUG
{
layer_count: u32;
vk.EnumerateInstanceLayerProperties(&layer_count, nil);
layers := make([]vk.LayerProperties, layer_count);
vk.EnumerateInstanceLayerProperties(&layer_count, raw_data(layers));
outer: for name in VALIDATION_LAYERS
{
for layer in &layers
{
if name == cstring(&layer.layerName[0]) do continue outer;
}
fmt.eprintf("ERROR: validation layer %q not available\n", name);
os.exit(1);
}
create_info.ppEnabledLayerNames = &VALIDATION_LAYERS[0];
create_info.enabledLayerCount = len(VALIDATION_LAYERS);
fmt.println("Validation Layers Loaded");
}
else
{
create_info.enabledLayerCount = 0;
}
if (vk.CreateInstance(&create_info, nil, &instance) != .SUCCESS)
{
fmt.eprintf("ERROR: Failed to create instance\n");
return;
}
fmt.println("Instance Created");
}
get_extensions :: proc() -> []vk.ExtensionProperties
{
n_ext: u32;
vk.EnumerateInstanceExtensionProperties(nil, &n_ext, nil);
extensions := make([]vk.ExtensionProperties, n_ext);
vk.EnumerateInstanceExtensionProperties(nil, &n_ext, raw_data(extensions));
return extensions;
}
create_surface :: proc(using ctx: ^Context)
{
surface_create_info := vk.Win32SurfaceCreateInfoKHR{};
surface_create_info.sType= .WIN32_SURFACE_CREATE_INFO_KHR;
surface_create_info.hwnd = glfw.GetWin32Window(window);
surface_create_info.hinstance = cast(vk.HANDLE)windows.GetModuleHandleA(nil);
if res := glfw.CreateWindowSurface(instance, window, nil, &surface); res != .SUCCESS
{
fmt.eprintf("ERROR: Failed to create window surface\n");
os.exit(1);
}
}
check_device_extension_support :: proc(physical_device: vk.PhysicalDevice) -> bool
{
ext_count: u32;
vk.EnumerateDeviceExtensionProperties(physical_device, nil, &ext_count, nil);
available_extensions := make([]vk.ExtensionProperties, ext_count);
vk.EnumerateDeviceExtensionProperties(physical_device, nil, &ext_count, raw_data(available_extensions));
for ext in DEVICE_EXTENSIONS
{
found: b32;
for available in &available_extensions
{
if cstring(&available.extensionName[0]) == ext
{
found = true;
break;
}
}
if !found do return false;
}
return true;
}
get_suitable_device :: proc(using ctx: ^Context)
{
device_count: u32;
vk.EnumeratePhysicalDevices(instance, &device_count, nil);
if device_count == 0
{
fmt.eprintf("ERROR: Failed to find GPUs with Vulkan support\n");
os.exit(1);
}
devices := make([]vk.PhysicalDevice, device_count);
vk.EnumeratePhysicalDevices(instance, &device_count, raw_data(devices));
suitability :: proc(using ctx: ^Context, dev: vk.PhysicalDevice) -> int
{
props: vk.PhysicalDeviceProperties;
features: vk.PhysicalDeviceFeatures;
vk.GetPhysicalDeviceProperties(dev, &props);
vk.GetPhysicalDeviceFeatures(dev, &features);
score := 0;
if props.deviceType == .DISCRETE_GPU do score += 1000;
score += cast(int)props.limits.maxImageDimension2D;
if !features.geometryShader do return 0;
if !check_device_extension_support(dev) do return 0;
query_swap_chain_details(ctx, dev);
if len(swap_chain.support.formats) == 0 || len(swap_chain.support.present_modes) == 0 do return 0;
return score;
}
hiscore := 0;
for dev in devices
{
score := suitability(ctx, dev);
if score > hiscore
{
physical_device = dev;
hiscore = score;
}
}
if (hiscore == 0)
{
fmt.eprintf("ERROR: Failed to find a suitable GPU\n");
os.exit(1);
}
}
find_queue_families :: proc(using ctx: ^Context)
{
queue_count: u32;
vk.GetPhysicalDeviceQueueFamilyProperties(physical_device, &queue_count, nil);
available_queues := make([]vk.QueueFamilyProperties, queue_count);
vk.GetPhysicalDeviceQueueFamilyProperties(physical_device, &queue_count, raw_data(available_queues));
for v, i in available_queues
{
if .GRAPHICS in v.queueFlags && queue_indices[.Graphics] == -1 do queue_indices[.Graphics] = i;
present_support: b32;
vk.GetPhysicalDeviceSurfaceSupportKHR(physical_device, u32(i), surface, &present_support);
if present_support && queue_indices[.Present] == -1 do queue_indices[.Present] = i;
for q in queue_indices do if q == -1 do continue;
break;
}
}
create_device :: proc(using ctx: ^Context)
{
unique_indices: map[int]b8;
defer delete(unique_indices);
for i in queue_indices do unique_indices[i] = true;
queue_priority := f32(1.0);
queue_create_infos: [dynamic]vk.DeviceQueueCreateInfo;
defer delete(queue_create_infos);
for k, _ in unique_indices
{
queue_create_info: vk.DeviceQueueCreateInfo;
queue_create_info.sType = .DEVICE_QUEUE_CREATE_INFO;
queue_create_info.queueFamilyIndex = u32(queue_indices[.Graphics]);
queue_create_info.queueCount = 1;
queue_create_info.pQueuePriorities = &queue_priority;
append(&queue_create_infos, queue_create_info);
}
device_features: vk.PhysicalDeviceFeatures;
device_create_info: vk.DeviceCreateInfo;
device_create_info.sType = .DEVICE_CREATE_INFO;
device_create_info.enabledExtensionCount = u32(len(DEVICE_EXTENSIONS));
device_create_info.ppEnabledExtensionNames = &DEVICE_EXTENSIONS[0];
device_create_info.pQueueCreateInfos = raw_data(queue_create_infos);
device_create_info.queueCreateInfoCount = u32(len(queue_create_infos));
device_create_info.pEnabledFeatures = &device_features;
device_create_info.enabledLayerCount = 0;
if vk.CreateDevice(physical_device, &device_create_info, nil, &device) != .SUCCESS
{
fmt.eprintf("ERROR: Failed to create logical device\n");
os.exit(1);
}
}
query_swap_chain_details :: proc(using ctx: ^Context, dev: vk.PhysicalDevice)
{
vk.GetPhysicalDeviceSurfaceCapabilitiesKHR(dev, surface, &swap_chain.support.capabilities);
format_count: u32;
vk.GetPhysicalDeviceSurfaceFormatsKHR(dev, surface, &format_count, nil);
if format_count > 0
{
swap_chain.support.formats = make([]vk.SurfaceFormatKHR, format_count);
vk.GetPhysicalDeviceSurfaceFormatsKHR(dev, surface, &format_count, raw_data(swap_chain.support.formats));
}
present_mode_count: u32;
vk.GetPhysicalDeviceSurfacePresentModesKHR(dev, surface, &present_mode_count, nil);
if present_mode_count > 0
{
swap_chain.support.present_modes = make([]vk.PresentModeKHR, present_mode_count);
vk.GetPhysicalDeviceSurfacePresentModesKHR(dev, surface, &present_mode_count, raw_data(swap_chain.support.present_modes));
}
}
choose_surface_format :: proc(using ctx: ^Context) -> vk.SurfaceFormatKHR
{
for v in swap_chain.support.formats
{
if v.format == .B8G8R8A8_SRGB && v.colorSpace == .SRGB_NONLINEAR do return v;
}
return swap_chain.support.formats[0];
}
choose_present_mode :: proc(using ctx: ^Context) -> vk.PresentModeKHR
{
for v in swap_chain.support.present_modes
{
if v == .MAILBOX do return v;
}
return .FIFO;
}
choose_swap_extent :: proc(using ctx: ^Context) -> vk.Extent2D
{
if (swap_chain.support.capabilities.currentExtent.width != max(u32))
{
return swap_chain.support.capabilities.currentExtent;
}
else
{
width, height := glfw.GetFramebufferSize(window);
extent := vk.Extent2D{u32(width), u32(height)};
extent.width = clamp(extent.width, swap_chain.support.capabilities.minImageExtent.width, swap_chain.support.capabilities.maxImageExtent.width);
extent.height = clamp(extent.height, swap_chain.support.capabilities.minImageExtent.height, swap_chain.support.capabilities.maxImageExtent.height);
return extent;
}
}
create_swap_chain :: proc(using ctx: ^Context)
{
using ctx.swap_chain.support;
swap_chain.format = choose_surface_format(ctx);
swap_chain.present_mode = choose_present_mode(ctx);
swap_chain.extent = choose_swap_extent(ctx);
swap_chain.image_count = capabilities.minImageCount + 1;
if capabilities.maxImageCount > 0 && swap_chain.image_count > capabilities.maxImageCount
{
swap_chain.image_count = capabilities.maxImageCount;
}
create_info: vk.SwapchainCreateInfoKHR;
create_info.sType = .SWAPCHAIN_CREATE_INFO_KHR;
create_info.surface = surface;
create_info.minImageCount = swap_chain.image_count;
create_info.imageFormat = swap_chain.format.format;
create_info.imageColorSpace = swap_chain.format.colorSpace;
create_info.imageExtent = swap_chain.extent;
create_info.imageArrayLayers = 1;
create_info.imageUsage = {.COLOR_ATTACHMENT};
queue_family_indices := [len(QueueFamily)]u32{u32(queue_indices[.Graphics]), u32(queue_indices[.Present])};
if queue_indices[.Graphics] != queue_indices[.Present]
{
create_info.imageSharingMode = .CONCURRENT;
create_info.queueFamilyIndexCount = 2;
create_info.pQueueFamilyIndices = &queue_family_indices[0];
}
else
{
create_info.imageSharingMode = .EXCLUSIVE;
create_info.queueFamilyIndexCount = 0;
create_info.pQueueFamilyIndices = nil;
}
create_info.preTransform = capabilities.currentTransform;
create_info.compositeAlpha = {.OPAQUE};
create_info.presentMode = swap_chain.present_mode;
create_info.clipped = true;
create_info.oldSwapchain = vk.SwapchainKHR{};
if res := vk.CreateSwapchainKHR(device, &create_info, nil, &swap_chain.handle); res != .SUCCESS
{
fmt.eprintf("Error: failed to create swap chain!\n");
os.exit(1);
}
vk.GetSwapchainImagesKHR(device, swap_chain.handle, &swap_chain.image_count, nil);
swap_chain.images = make([]vk.Image, swap_chain.image_count);
vk.GetSwapchainImagesKHR(device, swap_chain.handle, &swap_chain.image_count, raw_data(swap_chain.images));
}
create_image_views :: proc(using ctx: ^Context)
{
using ctx.swap_chain;
image_views = make([]vk.ImageView, len(images));
for _, i in images
{
create_info: vk.ImageViewCreateInfo;
create_info.sType = .IMAGE_VIEW_CREATE_INFO;
create_info.image = images[i];
create_info.viewType = .D2;
create_info.format = format.format;
create_info.components.r = .IDENTITY;
create_info.components.g = .IDENTITY;
create_info.components.b = .IDENTITY;
create_info.components.a = .IDENTITY;
create_info.subresourceRange.aspectMask = {.COLOR};
create_info.subresourceRange.baseMipLevel = 0;
create_info.subresourceRange.levelCount = 1;
create_info.subresourceRange.baseArrayLayer = 0;
create_info.subresourceRange.layerCount = 1;
if res := vk.CreateImageView(device, &create_info, nil, &image_views[i]); res != .SUCCESS
{
fmt.eprintf("Error: failed to create image view!");
os.exit(1);
}
}
}
create_graphics_pipeline :: proc(using ctx: ^Context, vs_name: string, fs_name: string)
{
vs_code := compile_shader(vs_name, .vertex_shader);
fs_code := compile_shader(fs_name, .fragment_shader);
/*
vs_code, vs_ok := os.read_entire_file(vs_path);
fs_code, fs_ok := os.read_entire_file(fs_path);
if !vs_ok
{
fmt.eprintf("Error: could not load vertex shader %q\n", vs_path);
os.exit(1);
}
if !fs_ok
{
fmt.eprintf("Error: could not load fragment shader %q\n", fs_path);
os.exit(1);
}
*/
defer
{
delete(vs_code);
delete(fs_code);
}
vs_shader := create_shader_module(ctx, vs_code);
fs_shader := create_shader_module(ctx, fs_code);
defer
{
vk.DestroyShaderModule(device, vs_shader, nil);
vk.DestroyShaderModule(device, fs_shader, nil);
}
vs_info: vk.PipelineShaderStageCreateInfo;
vs_info.sType = .PIPELINE_SHADER_STAGE_CREATE_INFO;
vs_info.stage = {.VERTEX};
vs_info.module = vs_shader;
vs_info.pName = "main";
fs_info: vk.PipelineShaderStageCreateInfo;
fs_info.sType = .PIPELINE_SHADER_STAGE_CREATE_INFO;
fs_info.stage = {.FRAGMENT};
fs_info.module = fs_shader;
fs_info.pName = "main";
shader_stages := [?]vk.PipelineShaderStageCreateInfo{vs_info, fs_info};
dynamic_states := [?]vk.DynamicState{.VIEWPORT, .SCISSOR};
dynamic_state: vk.PipelineDynamicStateCreateInfo;
dynamic_state.sType = .PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamic_state.dynamicStateCount = len(dynamic_states);
dynamic_state.pDynamicStates = &dynamic_states[0];
vertex_input: vk.PipelineVertexInputStateCreateInfo;
vertex_input.sType = .PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertex_input.vertexBindingDescriptionCount = 1;
vertex_input.pVertexBindingDescriptions = &VERTEX_BINDING;
vertex_input.vertexAttributeDescriptionCount = len(VERTEX_ATTRIBUTES);
vertex_input.pVertexAttributeDescriptions = &VERTEX_ATTRIBUTES[0];
input_assembly: vk.PipelineInputAssemblyStateCreateInfo;
input_assembly.sType = .PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
input_assembly.topology = .TRIANGLE_LIST;
input_assembly.primitiveRestartEnable = false;
viewport: vk.Viewport;
viewport.x = 0.0;
viewport.y = 0.0;
viewport.width = cast(f32)swap_chain.extent.width;
viewport.height = cast(f32)swap_chain.extent.height;
viewport.minDepth = 0.0;
viewport.maxDepth = 1.0;
scissor: vk.Rect2D;
scissor.offset = {0, 0};
scissor.extent = swap_chain.extent;
viewport_state: vk.PipelineViewportStateCreateInfo;
viewport_state.sType = .PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewport_state.viewportCount = 1;
viewport_state.scissorCount = 1;
rasterizer: vk.PipelineRasterizationStateCreateInfo;
rasterizer.sType = .PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rasterizer.depthClampEnable = false;
rasterizer.rasterizerDiscardEnable = false;
rasterizer.polygonMode = .FILL;
rasterizer.lineWidth = 1.0;
rasterizer.cullMode = {.BACK};
rasterizer.frontFace = .CLOCKWISE;
rasterizer.depthBiasEnable = false;
rasterizer.depthBiasConstantFactor = 0.0;
rasterizer.depthBiasClamp = 0.0;
rasterizer.depthBiasSlopeFactor = 0.0;
multisampling: vk.PipelineMultisampleStateCreateInfo;
multisampling.sType = .PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisampling.sampleShadingEnable = false;
multisampling.rasterizationSamples = {._1};
multisampling.minSampleShading = 1.0;
multisampling.pSampleMask = nil;
multisampling.alphaToCoverageEnable = false;
multisampling.alphaToOneEnable = false;
color_blend_attachment: vk.PipelineColorBlendAttachmentState;
color_blend_attachment.colorWriteMask = {.R, .G, .B, .A};
color_blend_attachment.blendEnable = true;
color_blend_attachment.srcColorBlendFactor = .SRC_ALPHA;
color_blend_attachment.dstColorBlendFactor = .ONE_MINUS_SRC_ALPHA;
color_blend_attachment.colorBlendOp = .ADD;
color_blend_attachment.srcAlphaBlendFactor = .ONE;
color_blend_attachment.dstAlphaBlendFactor = .ZERO;
color_blend_attachment.alphaBlendOp = .ADD;
color_blending: vk.PipelineColorBlendStateCreateInfo;
color_blending.sType = .PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
color_blending.logicOpEnable = false;
color_blending.logicOp = .COPY;
color_blending.attachmentCount = 1;
color_blending.pAttachments = &color_blend_attachment;
color_blending.blendConstants[0] = 0.0;
color_blending.blendConstants[1] = 0.0;
color_blending.blendConstants[2] = 0.0;
color_blending.blendConstants[3] = 0.0;
pipeline_layout_info: vk.PipelineLayoutCreateInfo;
pipeline_layout_info.sType = .PIPELINE_LAYOUT_CREATE_INFO;
pipeline_layout_info.setLayoutCount = 0;
pipeline_layout_info.pSetLayouts = nil;
pipeline_layout_info.pushConstantRangeCount = 0;
pipeline_layout_info.pPushConstantRanges = nil;
if res := vk.CreatePipelineLayout(device, &pipeline_layout_info, nil, &pipeline.layout); res != .SUCCESS
{
fmt.eprintf("Error: Failed to create pipeline layout!\n");
os.exit(1);
}
create_render_pass(ctx);
pipeline_info: vk.GraphicsPipelineCreateInfo;
pipeline_info.sType = .GRAPHICS_PIPELINE_CREATE_INFO;
pipeline_info.stageCount = 2;
pipeline_info.pStages = &shader_stages[0];
pipeline_info.pVertexInputState = &vertex_input;
pipeline_info.pInputAssemblyState = &input_assembly;
pipeline_info.pViewportState = &viewport_state;
pipeline_info.pRasterizationState = &rasterizer;
pipeline_info.pMultisampleState = &multisampling;
pipeline_info.pDepthStencilState = nil;
pipeline_info.pColorBlendState = &color_blending;
pipeline_info.pDynamicState = &dynamic_state;
pipeline_info.layout = pipeline.layout;
pipeline_info.renderPass = pipeline.render_pass;
pipeline_info.subpass = 0;
pipeline_info.basePipelineHandle = vk.Pipeline{};
pipeline_info.basePipelineIndex = -1;
if res := vk.CreateGraphicsPipelines(device, 0, 1, &pipeline_info, nil, &pipeline.handle); res != .SUCCESS
{
fmt.eprintf("Error: Failed to create graphics pipeline!\n");
os.exit(1);
}
}
create_shader_module :: proc(using ctx: ^Context, code: []u8) -> vk.ShaderModule
{
create_info: vk.ShaderModuleCreateInfo;
create_info.sType = .SHADER_MODULE_CREATE_INFO;
create_info.codeSize = len(code);
create_info.pCode = cast(^u32)raw_data(code);
shader: vk.ShaderModule;
if res := vk.CreateShaderModule(device, &create_info, nil, &shader); res != .SUCCESS
{
fmt.eprintf("Error: Could not create shader module!\n");
os.exit(1);
}
return shader;
}
create_render_pass :: proc(using ctx: ^Context)
{
color_attachment: vk.AttachmentDescription;
color_attachment.format = swap_chain.format.format;
color_attachment.samples = {._1};
color_attachment.loadOp = .CLEAR;
color_attachment.storeOp = .STORE;
color_attachment.stencilLoadOp = .DONT_CARE;
color_attachment.stencilStoreOp = .DONT_CARE;
color_attachment.initialLayout = .UNDEFINED;
color_attachment.finalLayout = .PRESENT_SRC_KHR;
color_attachment_ref: vk.AttachmentReference;
color_attachment_ref.attachment = 0;
color_attachment_ref.layout = .COLOR_ATTACHMENT_OPTIMAL;
subpass: vk.SubpassDescription;
subpass.pipelineBindPoint = .GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &color_attachment_ref;
dependency: vk.SubpassDependency;
dependency.srcSubpass = vk.SUBPASS_EXTERNAL;
dependency.dstSubpass = 0;
dependency.srcStageMask = {.COLOR_ATTACHMENT_OUTPUT};
dependency.srcAccessMask = {};
dependency.dstStageMask = {.COLOR_ATTACHMENT_OUTPUT};
dependency.dstAccessMask = {.COLOR_ATTACHMENT_WRITE};
render_pass_info: vk.RenderPassCreateInfo;
render_pass_info.sType = .RENDER_PASS_CREATE_INFO;
render_pass_info.attachmentCount = 1;
render_pass_info.pAttachments = &color_attachment;
render_pass_info.subpassCount = 1;
render_pass_info.pSubpasses = &subpass;
render_pass_info.dependencyCount = 1;
render_pass_info.pDependencies = &dependency;
if res := vk.CreateRenderPass(device, &render_pass_info, nil, &pipeline.render_pass); res != .SUCCESS
{
fmt.eprintf("Error: Failed to create render pass!\n");
os.exit(1);
}
}
create_framebuffers :: proc(using ctx: ^Context)
{
swap_chain.framebuffers = make([]vk.Framebuffer, len(swap_chain.image_views));
for v, i in swap_chain.image_views
{
attachments := [?]vk.ImageView{v};
framebuffer_info: vk.FramebufferCreateInfo;
framebuffer_info.sType = .FRAMEBUFFER_CREATE_INFO;
framebuffer_info.renderPass = pipeline.render_pass;
framebuffer_info.attachmentCount = 1;
framebuffer_info.pAttachments = &attachments[0];
framebuffer_info.width = swap_chain.extent.width;
framebuffer_info.height = swap_chain.extent.height;
framebuffer_info.layers = 1;
if res := vk.CreateFramebuffer(device, &framebuffer_info, nil, &swap_chain.framebuffers[i]); res != .SUCCESS
{
fmt.eprintf("Error: Failed to create framebuffer #%d!\n", i);
os.exit(1);
}
}
}
create_command_pool :: proc(using ctx: ^Context)
{
pool_info: vk.CommandPoolCreateInfo;
pool_info.sType = .COMMAND_POOL_CREATE_INFO;
pool_info.flags = {.RESET_COMMAND_BUFFER};
pool_info.queueFamilyIndex = u32(queue_indices[.Graphics]);
if res := vk.CreateCommandPool(device, &pool_info, nil, &command_pool); res != .SUCCESS
{
fmt.eprintf("Error: Failed to create command pool!\n");
os.exit(1);
}
}
create_command_buffers :: proc(using ctx: ^Context)
{
alloc_info: vk.CommandBufferAllocateInfo;
alloc_info.sType = .COMMAND_BUFFER_ALLOCATE_INFO;
alloc_info.commandPool = command_pool;
alloc_info.level = .PRIMARY;
alloc_info.commandBufferCount = len(command_buffers);
if res := vk.AllocateCommandBuffers(device, &alloc_info, &command_buffers[0]); res != .SUCCESS
{
fmt.eprintf("Error: Failed to allocate command buffers!\n");
os.exit(1);
}
}
record_command_buffer :: proc(using ctx: ^Context, buffer: vk.CommandBuffer, image_index: u32)
{
begin_info: vk.CommandBufferBeginInfo;
begin_info.sType = .COMMAND_BUFFER_BEGIN_INFO;
begin_info.flags = {};
begin_info.pInheritanceInfo = nil;
if res := vk.BeginCommandBuffer(buffer, &begin_info); res != .SUCCESS
{
fmt.eprintf("Error: Failed to begin recording command buffer!\n");
os.exit(1);
}
render_pass_info: vk.RenderPassBeginInfo;
render_pass_info.sType = .RENDER_PASS_BEGIN_INFO;
render_pass_info.renderPass = pipeline.render_pass;
render_pass_info.framebuffer = swap_chain.framebuffers[image_index];
render_pass_info.renderArea.offset = {0, 0};
render_pass_info.renderArea.extent = swap_chain.extent;
clear_color: vk.ClearValue;
clear_color.color.float32 = [4]f32{0.0, 0.0, 0.0, 1.0};
render_pass_info.clearValueCount = 1;
render_pass_info.pClearValues = &clear_color;
vk.CmdBeginRenderPass(buffer, &render_pass_info, .INLINE);
vk.CmdBindPipeline(buffer, .GRAPHICS, pipeline.handle);
vertex_buffers := [?]vk.Buffer{vertex_buffer.buffer};
offsets := [?]vk.DeviceSize{0};
vk.CmdBindVertexBuffers(buffer, 0, 1, &vertex_buffers[0], &offsets[0]);
vk.CmdBindIndexBuffer(buffer, index_buffer.buffer, 0, .UINT16);
viewport: vk.Viewport;
viewport.x = 0.0;
viewport.y = 0.0;
viewport.width = f32(swap_chain.extent.width);
viewport.height = f32(swap_chain.extent.height);
viewport.minDepth = 0.0;
viewport.maxDepth = 1.0;
vk.CmdSetViewport(buffer, 0, 1, &viewport);
scissor: vk.Rect2D;
scissor.offset = {0, 0};
scissor.extent = swap_chain.extent;
vk.CmdSetScissor(buffer, 0, 1, &scissor);
vk.CmdDrawIndexed(buffer, cast(u32)index_buffer.length, 1, 0, 0, 0);
vk.CmdEndRenderPass(buffer);
if res := vk.EndCommandBuffer(buffer); res != .SUCCESS
{
fmt.eprintf("Error: Failed to record command buffer!\n");
os.exit(1);
}
}
create_sync_objects :: proc(using ctx: ^Context)
{
semaphore_info: vk.SemaphoreCreateInfo;
semaphore_info.sType = .SEMAPHORE_CREATE_INFO;
fence_info: vk.FenceCreateInfo;
fence_info.sType = .FENCE_CREATE_INFO;
fence_info.flags = {.SIGNALED}
for i in 0..<MAX_FRAMES_IN_FLIGHT
{
res := vk.CreateSemaphore(device, &semaphore_info, nil, &image_available[i]);
if res != .SUCCESS
{
fmt.eprintf("Error: Failed to create \"image_available\" semaphore\n");
os.exit(1);
}
res = vk.CreateSemaphore(device, &semaphore_info, nil, &render_finished[i]);
if res != .SUCCESS
{
fmt.eprintf("Error: Failed to create \"render_finished\" semaphore\n");
os.exit(1);
}
res = vk.CreateFence(device, &fence_info, nil, &in_flight[i]);
if res != .SUCCESS
{
fmt.eprintf("Error: Failed to create \"in_flight\" fence\n");
os.exit(1);
}
}
}
recreate_swap_chain :: proc(using ctx: ^Context)
{
width, height := glfw.GetFramebufferSize(window);
for width == 0 && height == 0
{
width, height = glfw.GetFramebufferSize(window);
glfw.WaitEvents();
}
vk.DeviceWaitIdle(device);
cleanup_swap_chain(ctx);
create_swap_chain(ctx);
create_image_views(ctx);
create_framebuffers(ctx);
}
cleanup_swap_chain :: proc(using ctx: ^Context)
{
for f in swap_chain.framebuffers
{
vk.DestroyFramebuffer(device, f, nil);
}
for view in swap_chain.image_views
{
vk.DestroyImageView(device, view, nil);
}
vk.DestroySwapchainKHR(device, swap_chain.handle, nil);
}
create_vertex_buffer :: proc(using ctx: ^Context, vertices: []Vertex)
{
vertex_buffer.length = len(vertices);
vertex_buffer.size = cast(vk.DeviceSize)(len(vertices) * size_of(Vertex));
staging: Buffer;
create_buffer(ctx, size_of(Vertex), len(vertices), {.TRANSFER_SRC}, {.HOST_VISIBLE, .HOST_COHERENT}, &staging);
data: rawptr;
vk.MapMemory(device, staging.memory, 0, vertex_buffer.size, {}, &data);
mem.copy(data, raw_data(vertices), cast(int)vertex_buffer.size);
vk.UnmapMemory(device, staging.memory);
create_buffer(ctx, size_of(Vertex), len(vertices), {.VERTEX_BUFFER, .TRANSFER_DST}, {.DEVICE_LOCAL}, &vertex_buffer);
copy_buffer(ctx, staging, vertex_buffer, vertex_buffer.size);
vk.FreeMemory(device, staging.memory, nil);
vk.DestroyBuffer(device, staging.buffer, nil);
}
create_index_buffer :: proc(using ctx: ^Context, indices: []u16)
{
index_buffer.length = len(indices);
index_buffer.size = cast(vk.DeviceSize)(len(indices) * size_of(indices[0]));
staging: Buffer;
create_buffer(ctx, size_of(indices[0]), len(indices), {.TRANSFER_SRC}, {.HOST_VISIBLE, .HOST_COHERENT}, &staging);
data: rawptr;
vk.MapMemory(device, staging.memory, 0, index_buffer.size, {}, &data);
mem.copy(data, raw_data(indices), cast(int)index_buffer.size);
vk.UnmapMemory(device, staging.memory);
create_buffer(ctx, size_of(Vertex), len(indices), {.INDEX_BUFFER, .TRANSFER_DST}, {.DEVICE_LOCAL}, &index_buffer);
copy_buffer(ctx, staging, index_buffer, index_buffer.size);
vk.FreeMemory(device, staging.memory, nil);
vk.DestroyBuffer(device, staging.buffer, nil);
}
copy_buffer :: proc(using ctx: ^Context, src, dst: Buffer, size: vk.DeviceSize)
{
alloc_info := vk.CommandBufferAllocateInfo{
sType = .COMMAND_BUFFER_ALLOCATE_INFO,
level = .PRIMARY,
commandPool = command_pool,
commandBufferCount = 1,
};
cmd_buffer: vk.CommandBuffer;
vk.AllocateCommandBuffers(device, &alloc_info, &cmd_buffer);
begin_info := vk.CommandBufferBeginInfo{
sType = .COMMAND_BUFFER_BEGIN_INFO,
flags = {.ONE_TIME_SUBMIT},
}
vk.BeginCommandBuffer(cmd_buffer, &begin_info);
copy_region := vk.BufferCopy{
srcOffset = 0,
dstOffset = 0,
size = size,
}
vk.CmdCopyBuffer(cmd_buffer, src.buffer, dst.buffer, 1, &copy_region);
vk.EndCommandBuffer(cmd_buffer);
submit_info := vk.SubmitInfo{
sType = .SUBMIT_INFO,
commandBufferCount = 1,
pCommandBuffers = &cmd_buffer,
};
vk.QueueSubmit(queues[.Graphics], 1, &submit_info, {});
vk.QueueWaitIdle(queues[.Graphics]);
vk.FreeCommandBuffers(device, command_pool, 1, &cmd_buffer);
}
find_memory_type :: proc(using ctx: ^Context, type_filter: u32, properties: vk.MemoryPropertyFlags) -> u32
{
mem_properties: vk.PhysicalDeviceMemoryProperties;
vk.GetPhysicalDeviceMemoryProperties(physical_device, &mem_properties);
for i in 0..<mem_properties.memoryTypeCount
{
if (type_filter & (1 << i) != 0) && (mem_properties.memoryTypes[i].propertyFlags & properties) == properties
{
return i;
}
}
fmt.eprintf("Error: Failed to find suitable memory type!\n");
os.exit(1);
}
create_buffer :: proc(using ctx: ^Context, member_size: int, count: int, usage: vk.BufferUsageFlags, properties: vk.MemoryPropertyFlags, buffer: ^Buffer)
{
buffer_info := vk.BufferCreateInfo{
sType = .BUFFER_CREATE_INFO,
size = cast(vk.DeviceSize)(member_size * count),
usage = usage,
sharingMode = .EXCLUSIVE,
};
if res := vk.CreateBuffer(device, &buffer_info, nil, &buffer.buffer); res != .SUCCESS
{
fmt.eprintf("Error: failed to create buffer\n");
os.exit(1);
}
mem_requirements: vk.MemoryRequirements;
vk.GetBufferMemoryRequirements(device, buffer.buffer, &mem_requirements);
alloc_info := vk.MemoryAllocateInfo{
sType = .MEMORY_ALLOCATE_INFO,
allocationSize = mem_requirements.size,
memoryTypeIndex = find_memory_type(ctx, mem_requirements.memoryTypeBits, {.HOST_VISIBLE, .HOST_COHERENT})
};
if res := vk.AllocateMemory(device, &alloc_info, nil, &buffer.memory); res != .SUCCESS
{
fmt.eprintf("Error: Failed to allocate buffer memory!\n");
os.exit(1);
}
vk.BindBufferMemory(device, buffer.buffer, buffer.memory, 0);
}
#version 450
layout(location = 0) in vec3 fragColor;
layout(location = 0) out vec4 outColor;
void main()
{
outColor = vec4(fragColor, 1.0);
}
#version 450
layout(location = 0) in vec2 in_position;
layout(location = 1) in vec3 in_color;
layout(location = 0) out vec3 fragColor;
void main()
{
gl_Position = vec4(in_position, 0.0, 1.0);
fragColor = in_color;
}
@LeonardoTemperanza
Copy link

Thanks for the example. Just a heads up, you're not actually using "properties" in your "create_buffer" procedure, which means that all created buffers will be .HOST_VISIBLE and .HOST_COHERENT, even vertex and index buffers. This will significantly slow down your renderer.

@IRPotato
Copy link

Hey, did you bind shaderc for use with Odin ? if yes could you share the lib ?

Thank you!

@LeonardoTemperanza
Copy link

The bindings are right there in my "example repo", in that same file. They might be a bit old and not work anymore, might have to check. There are not too many functions so they're easy to write manually.

@IRPotato
Copy link

The bindings are right there in my "example repo", in that same file. They might be a bit old and not work anymore, might have to check. There are not too many functions so they're easy to write manually.

can't find it to save my life can you link it please :)

@LeonardoTemperanza
Copy link

@LeonardoTemperanza
Copy link

Just realized you weren't talking about shadercross, but shaderc which is a different thing lol. Now it makes sense why you'd ask that here

@IRPotato
Copy link

Just realized you weren't talking about shadercross, but shaderc which is a different thing lol. Now it makes sense why you'd ask that here

Yeah I am trying to make a binding for it but it's not looking good.

@terickson001
Copy link
Author

@A1-Forge I have some old bindings for shaderc here along with some other libraries: https://github.com/terickson001/odin-bindings

These bindings are auto-generated with https://github.com/terickson001/bind-odin-port, a forever WIP bindings generator of mine. Just a heads up as the quality may not be ideal, but they're completely functional.

As for the first comment from @LeonardoTemperanza , this is some very beginner code of mine from following one of the various VK guides out there. Someone on the Odin discord asked for some example code and I happened to have just finished this. This code is by no means a great example, and I have much more correct code for this nowadays.

@IRPotato
Copy link

IRPotato commented Oct 6, 2025

@terickson001 Thank you for the reply and links. I will look into the bind repo as I want to bind Tracy, as the current bindings are outdated, even in the creator's repo about the other bindings. I am going to update them next week as I am a bit busy at the moment and those will be of great help for what I am working on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment