const std = @import("std"); const sdl = @import("sdl2"); const vk = @import("vulkan"); const builtin = @import("builtin"); const shaders = @import("shaders"); const zm = @import("zmath"); const Utilities = @import("utilities.zig"); const QueueFamilyIndices = Utilities.QueueFamilyIndices; const SwapchainDetails = Utilities.SwapchainDetails; const SwapchainImage = Utilities.SwapchainImage; const Vertex = Utilities.Vertex; const Mesh = @import("Mesh.zig"); const enable_validation_layers = builtin.mode == .Debug; const validation_layers = [_][*:0]const u8{"VK_LAYER_KHRONOS_validation"}; const MAX_FRAME_DRAWS: u32 = 2; const MAX_OBJECTS: u32 = 2; const apis: []const vk.ApiInfo = &.{ vk.features.version_1_0, vk.features.version_1_1, vk.features.version_1_2, vk.features.version_1_3, vk.extensions.khr_surface, vk.extensions.khr_swapchain, vk.extensions.ext_debug_utils, }; const BaseDispatch = vk.BaseWrapper(apis); const InstanceDispatch = vk.InstanceWrapper(apis); const DeviceDispatch = vk.DeviceWrapper(apis); pub const Instance = vk.InstanceProxy(apis); pub const Device = vk.DeviceProxy(apis); pub const Queue = vk.QueueProxy(apis); pub const CommandBuffer = vk.CommandBufferProxy(apis); const UboViewProjection = struct { projection: zm.Mat align(16), view: zm.Mat align(16), }; pub const Model = struct { model: zm.Mat align(16), }; pub const VulkanRenderer = struct { const Self = @This(); allocator: std.mem.Allocator, vkb: BaseDispatch, window: sdl.Window, current_frame: u32 = 0, // Scene objects meshes: [2]Mesh, // Scene settings ubo_view_projection: UboViewProjection, // Main instance: Instance, physical_device: vk.PhysicalDevice, device: Device, graphics_queue: Queue, presentation_queue: Queue, surface: vk.SurfaceKHR, swapchain: vk.SwapchainKHR, viewport: vk.Viewport, scissor: vk.Rect2D, swapchain_images: []SwapchainImage, swapchain_framebuffers: []vk.Framebuffer, command_buffers: []CommandBuffer, depth_buffer_image: vk.Image, depth_buffer_image_memory: vk.DeviceMemory, depth_buffer_image_view: vk.ImageView, // Descriptors descriptor_set_layout: vk.DescriptorSetLayout, push_constant_range: vk.PushConstantRange, descriptor_pool: vk.DescriptorPool, descriptor_sets: []vk.DescriptorSet, vp_uniform_buffer: []vk.Buffer, vp_uniform_buffer_memory: []vk.DeviceMemory, // Pipeline graphics_pipeline: vk.Pipeline, pipeline_layout: vk.PipelineLayout, render_pass: vk.RenderPass, // Pools graphics_command_pool: vk.CommandPool, // Utilities swapchain_image_format: vk.Format, depth_format: vk.Format, extent: vk.Extent2D, // Synchronisation image_available: [MAX_FRAME_DRAWS]vk.Semaphore, render_finished: [MAX_FRAME_DRAWS]vk.Semaphore, draw_fences: [MAX_FRAME_DRAWS]vk.Fence, debug_utils: ?vk.DebugUtilsMessengerEXT, pub fn init(window: sdl.Window, allocator: std.mem.Allocator) !Self { var self: Self = undefined; self.window = window; self.current_frame = 0; self.allocator = allocator; self.vkb = try BaseDispatch.load(try sdl.vulkan.getVkGetInstanceProcAddr()); try self.createInstance(); try self.createSurface(); if (enable_validation_layers) { self.debug_utils = try createDebugMessenger(self.instance); } try self.getPhysicalDevice(); try self.createLogicalDevice(); try self.createSwapchain(); try self.createDepthBufferImage(); try self.createRenderPass(); try self.createDescriptorSetLayout(); try self.createPushConstantRange(); try self.createGraphicsPipeline(); try self.createFramebuffers(); try self.createCommandPool(); const aspect: f32 = @as(f32, @floatFromInt(self.extent.width)) / @as(f32, @floatFromInt(self.extent.height)); self.ubo_view_projection.projection = zm.perspectiveFovRh( std.math.degreesToRadians(45.0), aspect, 0.1, 100.0, ); self.ubo_view_projection.view = zm.lookAtRh( zm.Vec{ 0.0, 0.0, 2.0, 0.0 }, zm.Vec{ 0.0, 0.0, 0.0, 0.0 }, zm.Vec{ 0.0, 1.0, 0.0, 0.0 }, ); // Invert y scale self.ubo_view_projection.projection[1][1] *= -1; // Create meshes // Vertex Data var mesh_vertices = [_]Vertex{ .{ .pos = .{ -0.4, 0.4, 0.0 }, .col = .{ 1.0, 0.0, 0.0 } }, // 0 .{ .pos = .{ -0.4, -0.4, 0.0 }, .col = .{ 1.0, 0.0, 0.0 } }, // 1 .{ .pos = .{ 0.4, -0.4, 0.0 }, .col = .{ 1.0, 0.0, 0.0 } }, // 2 .{ .pos = .{ 0.4, 0.4, 0.0 }, .col = .{ 1.0, 0.0, 0.0 } }, // 3 }; var mesh_vertices2 = [_]Vertex{ .{ .pos = .{ -0.25, 0.6, 0.0 }, .col = .{ 0.0, 0.0, 1.0 } }, // 0 .{ .pos = .{ -0.25, -0.6, 0.0 }, .col = .{ 0.0, 0.0, 1.0 } }, // 1 .{ .pos = .{ 0.25, -0.6, 0.0 }, .col = .{ 0.0, 0.0, 1.0 } }, // 2 .{ .pos = .{ 0.25, 0.6, 0.0 }, .col = .{ 0.0, 0.0, 1.0 } }, // 3 }; // Index Data const mesh_indices = [_]u32{ 0, 1, 2, 2, 3, 0, }; const first_mesh = try Mesh.new( self.instance, self.physical_device, self.device, self.graphics_queue.handle, self.graphics_command_pool, &mesh_vertices, &mesh_indices, self.allocator, ); const second_mesh = try Mesh.new( self.instance, self.physical_device, self.device, self.graphics_queue.handle, self.graphics_command_pool, &mesh_vertices2, &mesh_indices, self.allocator, ); self.meshes = [_]Mesh{ first_mesh, second_mesh }; try self.createCommandBuffers(); try self.createUniformBuffers(); try self.createDescriptorPool(); try self.createDescriptorSets(); try self.createSynchronisation(); return self; } pub fn updateModel(self: *Self, model_id: u32, new_model: zm.Mat) !void { if (model_id < self.meshes.len) { self.meshes[model_id].ubo_model.model = new_model; } } pub fn draw(self: *Self) !void { // Wait for given fence to signal (open) from last draw before continuing _ = try self.device.waitForFences( 1, @ptrCast(&self.draw_fences[self.current_frame]), vk.TRUE, std.math.maxInt(u64), ); // Manually reset (close) fences try self.device.resetFences(1, @ptrCast(&self.draw_fences[self.current_frame])); // -- Get next image // Get index of next image to be drawn to, and signal semaphore when ready to be drawn to const image_index_result = try self.device.acquireNextImageKHR( self.swapchain, std.math.maxInt(u64), self.image_available[self.current_frame], .null_handle, ); try self.recordCommands(image_index_result.image_index); try self.updateUniformBuffers(image_index_result.image_index); // -- Submit command buffer to render // Queue submission information const wait_stages = [_]vk.PipelineStageFlags{.{ .color_attachment_output_bit = true }}; const submit_info: vk.SubmitInfo = .{ .wait_semaphore_count = 1, // Number of semaphores to wait on .p_wait_semaphores = @ptrCast(&self.image_available[self.current_frame]), // List of semaphores to wait on .p_wait_dst_stage_mask = &wait_stages, // Stages to check semaphores at .command_buffer_count = 1, // Number of command buffers to submit .p_command_buffers = @ptrCast(&self.command_buffers[image_index_result.image_index]), // Command buffer to submit .signal_semaphore_count = 1, // Number of semaphores to signal .p_signal_semaphores = @ptrCast(&self.render_finished[self.current_frame]), // List of semaphores to signal when command buffer finishes }; // Submit command buffer to queue try self.device.queueSubmit(self.graphics_queue.handle, 1, @ptrCast(&submit_info), self.draw_fences[self.current_frame]); // -- Present rendered image to screen const present_info: vk.PresentInfoKHR = .{ .wait_semaphore_count = 1, // Number of semaphores to wait on .p_wait_semaphores = @ptrCast(&self.render_finished[self.current_frame]), // Semaphores to wait on .swapchain_count = 1, // Number of swapchains to present to .p_swapchains = @ptrCast(&self.swapchain), // Swapchains to present images to .p_image_indices = @ptrCast(&image_index_result.image_index), // Index of images in swapchains to present }; // Present image _ = try self.device.queuePresentKHR(self.presentation_queue.handle, &present_info); // Get next frame (use % to keep the current frame below MAX_FRAME_DRAWS) self.current_frame = (self.current_frame + 1) % MAX_FRAME_DRAWS; } pub fn deinit(self: *Self) void { self.device.deviceWaitIdle() catch undefined; if (enable_validation_layers) { self.instance.destroyDebugUtilsMessengerEXT(self.debug_utils.?, null); } self.device.destroyImageView(self.depth_buffer_image_view, null); self.device.destroyImage(self.depth_buffer_image, null); self.device.freeMemory(self.depth_buffer_image_memory, null); self.device.destroyDescriptorPool(self.descriptor_pool, null); self.device.destroyDescriptorSetLayout(self.descriptor_set_layout, null); for (0..self.swapchain_images.len) |i| { self.device.destroyBuffer(self.vp_uniform_buffer[i], null); self.device.freeMemory(self.vp_uniform_buffer_memory[i], null); } self.allocator.free(self.vp_uniform_buffer); self.allocator.free(self.vp_uniform_buffer_memory); self.allocator.free(self.descriptor_sets); for (self.meshes) |mesh| { mesh.destroyBuffers(); } for (0..MAX_FRAME_DRAWS) |i| { self.device.destroySemaphore(self.render_finished[i], null); self.device.destroySemaphore(self.image_available[i], null); self.device.destroyFence(self.draw_fences[i], null); } self.allocator.free(self.command_buffers); self.device.destroyCommandPool(self.graphics_command_pool, null); for (self.swapchain_framebuffers) |framebuffer| { self.device.destroyFramebuffer(framebuffer, null); } self.allocator.free(self.swapchain_framebuffers); self.device.destroyPipeline(self.graphics_pipeline, null); self.device.destroyPipelineLayout(self.pipeline_layout, null); self.device.destroyRenderPass(self.render_pass, null); for (self.swapchain_images) |swapchain_image| { self.device.destroyImageView(swapchain_image.image_view, null); } self.allocator.free(self.swapchain_images); self.device.destroySwapchainKHR(self.swapchain, null); self.device.destroyDevice(null); self.instance.destroySurfaceKHR(self.surface, null); self.instance.destroyInstance(null); self.allocator.destroy(self.device.wrapper); self.allocator.destroy(self.instance.wrapper); } fn createInstance(self: *Self) !void { if (enable_validation_layers and !self.checkValidationLayersSupport()) { // TODO Better error return error.LayerNotPresent; } const extensions = try self.getRequiredExtensions(); defer self.allocator.free(extensions); std.debug.print("[Required instance extensions]\n", .{}); for (extensions) |ext| { std.debug.print("\t- {s}\n", .{ext}); } if (!try self.checkInstanceExtensions(&extensions)) { return error.ExtensionNotPresent; } const app_info = vk.ApplicationInfo{ .p_application_name = "Vulkan SDL Test", .application_version = vk.makeApiVersion(0, 0, 1, 0), .p_engine_name = "Vulkan SDL Test", .engine_version = vk.makeApiVersion(0, 0, 1, 0), .api_version = vk.API_VERSION_1_3, }; var instance_create_info: vk.InstanceCreateInfo = .{ .p_application_info = &app_info, .enabled_extension_count = @intCast(extensions.len), .pp_enabled_extension_names = @ptrCast(extensions), }; if (enable_validation_layers) { const debug_create_info = getDebugUtilsCreateInfo(); instance_create_info.enabled_layer_count = @intCast(validation_layers.len); instance_create_info.pp_enabled_layer_names = &validation_layers; instance_create_info.p_next = &debug_create_info; } const instance_handle = try self.vkb.createInstance(&instance_create_info, null); const vki = try self.allocator.create(InstanceDispatch); errdefer self.allocator.destroy(vki); vki.* = try InstanceDispatch.load(instance_handle, self.vkb.dispatch.vkGetInstanceProcAddr); self.instance = Instance.init(instance_handle, vki); } fn createSurface(self: *Self) !void { self.surface = try sdl.vulkan.createSurface(self.window, self.instance.handle); } fn createLogicalDevice(self: *Self) !void { const indices = try self.getQueueFamilies(self.physical_device); // 1 is the highest priority const priority = [_]f32{1}; const qci = [_]vk.DeviceQueueCreateInfo{ .{ .queue_family_index = indices.graphics_family.?, .queue_count = 1, .p_queue_priorities = &priority, }, .{ .queue_family_index = indices.presentation_family.?, .queue_count = 1, .p_queue_priorities = &priority, }, }; const queue_count: u32 = if (indices.graphics_family.? == indices.presentation_family.?) 1 else 2; const device_create_info: vk.DeviceCreateInfo = .{ .queue_create_info_count = queue_count, .p_queue_create_infos = &qci, .pp_enabled_extension_names = &Utilities.device_extensions, .enabled_extension_count = @intCast(Utilities.device_extensions.len), }; const device_handle = try self.instance.createDevice(self.physical_device, &device_create_info, null); const vkd = try self.allocator.create(DeviceDispatch); errdefer self.allocator.destroy(vkd); vkd.* = try DeviceDispatch.load(device_handle, self.instance.wrapper.dispatch.vkGetDeviceProcAddr); self.device = Device.init(device_handle, vkd); const queues = try self.getDeviceQueues(); self.graphics_queue = Queue.init(queues[0], self.device.wrapper); self.presentation_queue = Queue.init(queues[1], self.device.wrapper); } fn createSwapchain(self: *Self) !void { const swapchain_details = try self.getSwapchainDetails(self.physical_device); defer self.allocator.free(swapchain_details.formats); defer self.allocator.free(swapchain_details.presentation_modes); // 1. Choose best surface format const surface_format = chooseBestSurfaceFormat(swapchain_details.formats); // 2. Choose best presentation mode const present_mode = chooseBestPresentationMode(swapchain_details.presentation_modes); // 3. Choose swapchain image resolution const extent = chooseSwapExtent(&self.window, swapchain_details.surface_capabilities); // How many images are in the swapchain? Get 1 more than the minimum to allow triple buffering var image_count: u32 = swapchain_details.surface_capabilities.min_image_count + 1; const max_image_count = swapchain_details.surface_capabilities.max_image_count; // Clamp down if higher // If 0, it means it's limitless if (max_image_count != 0 and image_count > max_image_count) { image_count = max_image_count; } var swapchain_create_info: vk.SwapchainCreateInfoKHR = .{ .image_format = surface_format.format, .image_color_space = surface_format.color_space, .present_mode = present_mode, .image_extent = extent, .min_image_count = image_count, .image_array_layers = 1, // Number of layers for each image .image_usage = .{ .color_attachment_bit = true }, // What attachment will images be used as .pre_transform = swapchain_details.surface_capabilities.current_transform, // Transform to perform on swapchain images .composite_alpha = .{ .opaque_bit_khr = true }, // How to handle blending images with external graphics (e.g.: other windows) .clipped = vk.TRUE, // Whether to clip parts of images not in view (e.g.: behind another window, off-screen, etc...) .old_swapchain = .null_handle, // Links old one to quickly share responsibilities in case it's been destroyed and replaced .surface = self.surface, .image_sharing_mode = .exclusive, }; // Get queue family indices const family_indices = try self.getQueueFamilies(self.physical_device); // If graphic and presentation families are different, then swapchain must let images be shared between families if (family_indices.graphics_family.? != family_indices.presentation_family.?) { const qfi = [_]u32{ family_indices.graphics_family.?, family_indices.presentation_family.?, }; swapchain_create_info.image_sharing_mode = .concurrent; swapchain_create_info.queue_family_index_count = @intCast(qfi.len); // Number of queues to share images between swapchain_create_info.p_queue_family_indices = &qfi; } self.swapchain = try self.device.createSwapchainKHR(&swapchain_create_info, null); self.swapchain_image_format = surface_format.format; self.extent = extent; // Swapchain images var swapchain_image_count: u32 = 0; _ = try self.device.getSwapchainImagesKHR(self.swapchain, &swapchain_image_count, null); const images = try self.allocator.alloc(vk.Image, swapchain_image_count); defer self.allocator.free(images); _ = try self.device.getSwapchainImagesKHR(self.swapchain, &swapchain_image_count, images.ptr); self.swapchain_images = try self.allocator.alloc(SwapchainImage, swapchain_image_count); for (images, 0..) |image, i| { self.swapchain_images[i] = .{ .image = image, .image_view = try self.createImageView(image, self.swapchain_image_format, .{ .color_bit = true }), }; } } fn createRenderPass(self: *Self) !void { // -- Attachments -- // Colour attachment of the render pass const colour_attachment: vk.AttachmentDescription = .{ .format = self.swapchain_image_format, // Format to use for attachment .samples = .{ .@"1_bit" = true }, // Number of samples to write for multisampling .load_op = .clear, // Describes what to do with attachment before rendering .store_op = .store, // Describes what to do with attachment after rendering .stencil_load_op = .dont_care, // Describes what to do with stencil before rendering .stencil_store_op = .dont_care, // Describes what to do with stencil after rendering // Framebuffer data will be stored as an image, but images can be given different data layouts // to give optimal use for certain operations .initial_layout = vk.ImageLayout.undefined, // Image data layout before render pass starts .final_layout = vk.ImageLayout.present_src_khr, // Image data layout after render pass (to change to) }; // Depth attachment of render pass const depth_attachment: vk.AttachmentDescription = .{ .format = self.depth_format, .samples = .{ .@"1_bit" = true }, .load_op = .clear, .store_op = .dont_care, .stencil_load_op = .dont_care, .stencil_store_op = .dont_care, .initial_layout = .undefined, .final_layout = .depth_stencil_attachment_optimal, }; // -- References -- // Attachment reference uses an attachment index that refers to index in the attachment list passed to render pass create info const colour_attachment_reference: vk.AttachmentReference = .{ .attachment = 0, .layout = vk.ImageLayout.color_attachment_optimal, }; const depth_attachment_reference: vk.AttachmentReference = .{ .attachment = 1, .layout = vk.ImageLayout.depth_stencil_attachment_optimal, }; // Information about a particular subpass the render pass is using const subpass: vk.SubpassDescription = .{ .pipeline_bind_point = .graphics, // Pipeline type subpass is to be bound to .color_attachment_count = 1, .p_color_attachments = @ptrCast(&colour_attachment_reference), .p_depth_stencil_attachment = &depth_attachment_reference, }; // Need to determine when layout transitions occur using subpass dependencies const subpass_dependencies = [2]vk.SubpassDependency{ // Conversion from VK_IMAGE_LAYOUT_UNDEFINED to VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL vk.SubpassDependency{ // Transition must happen after... .src_subpass = vk.SUBPASS_EXTERNAL, // Subpass index (VK_SUBPASS_EXTERNAL = outside of renderpass) .src_stage_mask = .{ .bottom_of_pipe_bit = true }, // Pipeline stage .src_access_mask = .{ .memory_read_bit = true }, // Stage access mask (memory access) // But must happen before... .dst_subpass = 0, .dst_stage_mask = .{ .color_attachment_output_bit = true }, .dst_access_mask = .{ .memory_read_bit = true, .memory_write_bit = true }, }, // Conversion from VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL to VK_IMAGE_LAYOUT_PRESENT_SRC_KHR vk.SubpassDependency{ // Transition must happen after... .src_subpass = 0, .src_stage_mask = .{ .color_attachment_output_bit = true }, .src_access_mask = .{ .memory_read_bit = true, .memory_write_bit = true }, // But must happen before... .dst_subpass = vk.SUBPASS_EXTERNAL, .dst_stage_mask = .{ .bottom_of_pipe_bit = true }, .dst_access_mask = .{ .memory_read_bit = true }, }, }; // Order matters const render_pass_attachments = [_]vk.AttachmentDescription{ colour_attachment, depth_attachment }; const render_pass_create_info: vk.RenderPassCreateInfo = .{ .attachment_count = @intCast(render_pass_attachments.len), .p_attachments = &render_pass_attachments, .subpass_count = 1, .p_subpasses = @ptrCast(&subpass), .dependency_count = @intCast(subpass_dependencies.len), .p_dependencies = &subpass_dependencies, }; self.render_pass = try self.device.createRenderPass(&render_pass_create_info, null); } fn createDescriptorSetLayout(self: *Self) !void { // UboViewProjection binding info const vp_layout_binding: vk.DescriptorSetLayoutBinding = .{ .binding = 0, // Binding point in shader (designated by binding number in shader) .descriptor_type = .uniform_buffer, // Type of descriptor (unifor, dynamic uniform, image sampler, etc) .descriptor_count = 1, // Number of descriptors for binding .stage_flags = .{ .vertex_bit = true }, // Shader stage to bind to .p_immutable_samplers = null, // For texture: can make smapler data immutable by specifying in layout }; const layout_bindings = [_]vk.DescriptorSetLayoutBinding{vp_layout_binding}; // Create descriptor set layout with given bindings const layout_create_info: vk.DescriptorSetLayoutCreateInfo = .{ .binding_count = @intCast(layout_bindings.len), // Number of binding infos .p_bindings = &layout_bindings, // Array of binding infos }; // Create descriptor set layout self.descriptor_set_layout = try self.device.createDescriptorSetLayout(&layout_create_info, null); } fn createPushConstantRange(self: *Self) !void { // Define push constant values (no 'create' needed) self.push_constant_range = .{ .stage_flags = .{ .vertex_bit = true }, // Shader stage push constant will go to .offset = 0, // Offset into given data to pass to push constant .size = @sizeOf(Model), // Size of data being passed }; } fn createDepthBufferImage(self: *Self) !void { // Get supported depth buffer format const formats = [_]vk.Format{ .d32_sfloat_s8_uint, .d32_sfloat, .d24_unorm_s8_uint }; self.depth_format = chooseSupportedFormat( self.physical_device, self.instance, &formats, .optimal, .{ .depth_stencil_attachment_bit = true }, ) orelse return error.UnsupportedDepthBufferFormat; // Create depth buffer image self.depth_buffer_image = try self.createImage( self.extent.width, self.extent.height, self.depth_format, .optimal, .{ .depth_stencil_attachment_bit = true }, .{ .device_local_bit = true }, &self.depth_buffer_image_memory, ); // Create depth buffer image view self.depth_buffer_image_view = try self.createImageView(self.depth_buffer_image, self.depth_format, .{ .depth_bit = true }); } fn createGraphicsPipeline(self: *Self) !void { // Create shader modules const vert = try self.device.createShaderModule(&.{ .code_size = shaders.shader_vert.len, .p_code = @ptrCast(&shaders.shader_vert), }, null); defer self.device.destroyShaderModule(vert, null); const frag = try self.device.createShaderModule(&.{ .code_size = shaders.shader_frag.len, .p_code = @ptrCast(&shaders.shader_frag), }, null); defer self.device.destroyShaderModule(frag, null); // -- Shader stage creation information -- // Vertex stage creation information const vertex_shader_create_info: vk.PipelineShaderStageCreateInfo = .{ .stage = .{ .vertex_bit = true }, .module = vert, .p_name = "main", }; // Fragment stage creation information const fragment_shader_create_info: vk.PipelineShaderStageCreateInfo = .{ .stage = .{ .fragment_bit = true }, .module = frag, .p_name = "main", }; const shader_create_infos = [_]vk.PipelineShaderStageCreateInfo{ vertex_shader_create_info, fragment_shader_create_info, }; // How the data for a single vertex (including info such as position, colour, texture coords, normals, etc...) is as a whole const binding_description: vk.VertexInputBindingDescription = .{ .binding = 0, // Can bind multiple streams of data, this defines which one .stride = @sizeOf(Vertex), // Size of simple vertex object .input_rate = .vertex, // How to move between data after each vertex // vertex: move to the next vertex // instance: move to a vertex for the next instance }; // How the data for an attribute is defined within the vertex const attribute_descriptions = [_]vk.VertexInputAttributeDescription{ // Position attribute .{ .binding = 0, // Which binding the data is at (should be same as above) .location = 0, // Location in shader where data will be read from .format = vk.Format.r32g32b32_sfloat, // Format the data will take (also helps define size of data) .offset = @offsetOf(Vertex, "pos"), // Where this attribute is defined in data for a single vertex }, // Colour attribute .{ .binding = 0, .location = 1, .format = vk.Format.r32g32b32_sfloat, .offset = @offsetOf(Vertex, "col"), }, }; // -- Vertex input -- const vertex_input_create_info: vk.PipelineVertexInputStateCreateInfo = .{ .vertex_binding_description_count = 1, .p_vertex_binding_descriptions = @ptrCast(&binding_description), // List of vertex binding descriptions (data spacing, stride info) .vertex_attribute_description_count = @intCast(attribute_descriptions.len), .p_vertex_attribute_descriptions = &attribute_descriptions, // List of vertex attribute descriptions (data format and where to bind to/from) }; // -- Input assembly -- const assembly_create_info: vk.PipelineInputAssemblyStateCreateInfo = .{ .topology = .triangle_list, // Primitive type to assemble vertices as .primitive_restart_enable = vk.FALSE, // Allow overrinding of strip topology to start new primitives }; // -- Viewport & scissor -- self.viewport = .{ .x = 0.0, .y = 0.0, .width = @floatFromInt(self.extent.width), .height = @floatFromInt(self.extent.height), .min_depth = 0.0, .max_depth = 1.0, }; self.scissor = .{ .offset = .{ .x = 0, .y = 0 }, .extent = self.extent, }; const viewport_state_create_info: vk.PipelineViewportStateCreateInfo = .{ .viewport_count = 1, .p_viewports = @ptrCast(&self.viewport), .scissor_count = 1, .p_scissors = @ptrCast(&self.scissor), }; // -- Dynamic states -- // Dynamic states to enable (TODO: To investigate later) const dynamic_states = [_]vk.DynamicState{ .viewport, .scissor }; const dynamic_state_create_info: vk.PipelineDynamicStateCreateInfo = .{ .dynamic_state_count = @intCast(dynamic_states.len), .p_dynamic_states = &dynamic_states, }; // -- Rasterizer -- const rasterizer_create_info: vk.PipelineRasterizationStateCreateInfo = .{ .depth_clamp_enable = vk.FALSE, // Change if fragments beyond near/far planes are clipped (default) or clamped to plane .rasterizer_discard_enable = vk.FALSE, // Whether to discard data and skip rasterizer (never creates fragments) .polygon_mode = .fill, // How to handle filling points between vertices .line_width = 1.0, // How thick the lines should be when drawn .cull_mode = .{ .back_bit = true }, // Which face of a triangle to cull .front_face = .counter_clockwise, // Winding to determine which side is front .depth_bias_enable = vk.FALSE, // Whether to add depth bias to fragments (good for stopping "shadow acne" in shadow mapping) .depth_bias_constant_factor = 0, .depth_bias_clamp = 0, .depth_bias_slope_factor = 0, }; // -- Multisampling -- const multisampling_create_info: vk.PipelineMultisampleStateCreateInfo = .{ .sample_shading_enable = vk.FALSE, // Enable multisample shading or not .rasterization_samples = .{ .@"1_bit" = true }, // Number of samples to use per fragment .min_sample_shading = 1, .alpha_to_coverage_enable = vk.FALSE, .alpha_to_one_enable = vk.FALSE, }; // -- Blending -- // Blend attachment state (how blending is handled) const colour_state: vk.PipelineColorBlendAttachmentState = .{ .color_write_mask = .{ .r_bit = true, .g_bit = true, .b_bit = true, .a_bit = true }, // Colours to apply blending to .blend_enable = vk.TRUE, // Enable blending .src_color_blend_factor = vk.BlendFactor.src_alpha, .dst_color_blend_factor = vk.BlendFactor.one_minus_src_alpha, .color_blend_op = vk.BlendOp.add, // Summary: (VK_BLEND_FACTOR_SRC_ALPHA * new colour) + (VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA * old colour) .src_alpha_blend_factor = vk.BlendFactor.one, .dst_alpha_blend_factor = vk.BlendFactor.zero, .alpha_blend_op = vk.BlendOp.add, // Summary (1 * new alpha) + (0 * old alpha) = new alpha }; // Blending uses equation: (srcColorBlendFactor * new colour) colorBlendOp (dstColorBlendFactor * old colour) const colour_blending_create_info: vk.PipelineColorBlendStateCreateInfo = .{ .logic_op_enable = vk.FALSE, // Alternative to calculations is to use logical operations .logic_op = .copy, .attachment_count = 1, .p_attachments = @ptrCast(&colour_state), .blend_constants = [_]f32{ 0, 0, 0, 0 }, }; // -- Pipeline layout -- const pipeline_layout_create_info: vk.PipelineLayoutCreateInfo = .{ .set_layout_count = 1, .p_set_layouts = @ptrCast(&self.descriptor_set_layout), .push_constant_range_count = 1, .p_push_constant_ranges = @ptrCast(&self.push_constant_range), }; self.pipeline_layout = try self.device.createPipelineLayout(&pipeline_layout_create_info, null); // -- Depth stencil testing -- const depth_stencil_create_info: vk.PipelineDepthStencilStateCreateInfo = .{ .depth_test_enable = vk.TRUE, // Enable checking depth to determine fragment write .depth_write_enable = vk.TRUE, // Enable writing to depth buffer to replace all values .depth_compare_op = .less, // Comparison operation that allows an overwrite (is in front) .depth_bounds_test_enable = vk.FALSE, // Depth bounds test: does the depth value exist between two bounds .stencil_test_enable = vk.FALSE, // Enable stencil test .front = undefined, .back = undefined, .min_depth_bounds = undefined, .max_depth_bounds = undefined, }; // -- Graphics pipeline creation -- const pipeline_create_info: vk.GraphicsPipelineCreateInfo = .{ .stage_count = @intCast(shader_create_infos.len), // Number of shader stages .p_stages = &shader_create_infos, // List of shader stages .p_vertex_input_state = &vertex_input_create_info, .p_input_assembly_state = &assembly_create_info, .p_viewport_state = &viewport_state_create_info, .p_dynamic_state = &dynamic_state_create_info, .p_rasterization_state = &rasterizer_create_info, .p_multisample_state = &multisampling_create_info, .p_color_blend_state = &colour_blending_create_info, .p_depth_stencil_state = &depth_stencil_create_info, .layout = self.pipeline_layout, // Pipeline layout the pipeline should use .render_pass = self.render_pass, // Renderpass description the pipeline is compatible with .subpass = 0, // Subpass of renderpass to use with pipeline // Pipeline derivatives: can create multiple pipelines that derive from one another for optimisation .base_pipeline_handle = .null_handle, // Existing pipeline to derive from... .base_pipeline_index = -1, // Or index of pipeline being created to derive from (in case creating multiple at once) }; _ = try self.device.createGraphicsPipelines( .null_handle, 1, @ptrCast(&pipeline_create_info), null, @ptrCast(&self.graphics_pipeline), ); } fn createFramebuffers(self: *Self) !void { self.swapchain_framebuffers = try self.allocator.alloc(vk.Framebuffer, self.swapchain_images.len); // Create a frammebuffer for each swapchain image for (self.swapchain_images, 0..) |swapchain_image, i| { // Order matters const attachments = [_]vk.ImageView{ swapchain_image.image_view, self.depth_buffer_image_view }; const framebuffer_create_info: vk.FramebufferCreateInfo = .{ .render_pass = self.render_pass, // Render pass layout the frambuffer will be used with .attachment_count = @intCast(attachments.len), .p_attachments = &attachments, // List of attachments (1:1 with render pass) .width = self.extent.width, // Framebuffer width .height = self.extent.height, // Framebuffer height .layers = 1, // Framebuffer layers }; self.swapchain_framebuffers[i] = try self.device.createFramebuffer(&framebuffer_create_info, null); } } fn createCommandPool(self: *Self) !void { // Get indices of queue families from device const queue_family_indices = try self.getQueueFamilies(self.physical_device); const pool_create_info: vk.CommandPoolCreateInfo = .{ // Queue family type that buffers from this command pool will use .queue_family_index = queue_family_indices.graphics_family.?, .flags = .{ .reset_command_buffer_bit = true }, }; // Create a graphics queue family command pool self.graphics_command_pool = try self.device.createCommandPool(&pool_create_info, null); } fn createCommandBuffers(self: *Self) !void { // Allocate one command buffer for each framebuffer const command_buffer_handles = try self.allocator.alloc(vk.CommandBuffer, self.swapchain_framebuffers.len); defer self.allocator.free(command_buffer_handles); self.command_buffers = try self.allocator.alloc(CommandBuffer, command_buffer_handles.len); const command_buffer_allocate_info: vk.CommandBufferAllocateInfo = .{ .command_pool = self.graphics_command_pool, .level = .primary, // primary: buffer you submit directly to queue. Can't be called by other buffers .command_buffer_count = @intCast(command_buffer_handles.len), }; // Allocate command buffers and place handles in array of buffers try self.device.allocateCommandBuffers(&command_buffer_allocate_info, command_buffer_handles.ptr); for (command_buffer_handles, 0..) |command_buffer_handle, i| { self.command_buffers[i] = CommandBuffer.init(command_buffer_handle, self.device.wrapper); } } fn createSynchronisation(self: *Self) !void { // Fence create information const fence_create_info: vk.FenceCreateInfo = .{ .flags = .{ .signaled_bit = true } }; // Semaphore creation information for (0..MAX_FRAME_DRAWS) |i| { self.image_available[i] = try self.device.createSemaphore(&.{}, null); self.render_finished[i] = try self.device.createSemaphore(&.{}, null); self.draw_fences[i] = try self.device.createFence(&fence_create_info, null); } } fn createUniformBuffers(self: *Self) !void { // View projection buffer size const vp_buffer_size: vk.DeviceSize = @sizeOf(UboViewProjection); // One uniform buffer for each image (and by extension, command buffer) self.vp_uniform_buffer = try self.allocator.alloc(vk.Buffer, self.swapchain_images.len); self.vp_uniform_buffer_memory = try self.allocator.alloc(vk.DeviceMemory, self.swapchain_images.len); // Create the uniform buffers for (0..self.vp_uniform_buffer.len) |i| { try Utilities.createBuffer( self.physical_device, self.instance, self.device, vp_buffer_size, .{ .uniform_buffer_bit = true }, .{ .host_visible_bit = true, .host_coherent_bit = true }, &self.vp_uniform_buffer[i], &self.vp_uniform_buffer_memory[i], ); } } fn createDescriptorPool(self: *Self) !void { // Type of descriptors + how many descriptors (!= descriptor sets) (combined makes the pool size) // View projection pool const vp_pool_size: vk.DescriptorPoolSize = .{ .type = .uniform_buffer, .descriptor_count = @intCast(self.vp_uniform_buffer.len), }; // List of pool sizes const descriptor_pool_sizes = [_]vk.DescriptorPoolSize{vp_pool_size}; // Data to create descriptor pool const pool_create_info: vk.DescriptorPoolCreateInfo = .{ .max_sets = @intCast(self.swapchain_images.len), // Maximum number of descriptor sets that can be created from pool .pool_size_count = @intCast(descriptor_pool_sizes.len), // Amount of pool sizes being passed .p_pool_sizes = &descriptor_pool_sizes, // Pool sizes to create pool with }; // Create descriptor pool self.descriptor_pool = try self.device.createDescriptorPool(&pool_create_info, null); } fn createDescriptorSets(self: *Self) !void { // One descriptor set for every buffer self.descriptor_sets = try self.allocator.alloc(vk.DescriptorSet, self.swapchain_images.len); var set_layouts = try self.allocator.alloc(vk.DescriptorSetLayout, self.swapchain_images.len); defer self.allocator.free(set_layouts); for (0..set_layouts.len) |i| { set_layouts[i] = self.descriptor_set_layout; } // Descriptor set allocation info const set_alloc_info: vk.DescriptorSetAllocateInfo = .{ .descriptor_pool = self.descriptor_pool, // Pool to allocate descriptor set from .descriptor_set_count = @intCast(self.swapchain_images.len), // Number of sets to allocate .p_set_layouts = set_layouts.ptr, // Layouts to use to allocate sets (1:1 relationship) }; // Allocate descriptor sets (multiple) try self.device.allocateDescriptorSets(&set_alloc_info, self.descriptor_sets.ptr); // Update all of descriptor set buffer bindings for (0..self.swapchain_images.len) |i| { // -- View projection descriptor // Buffer info and data offset info const vp_buffer_info: vk.DescriptorBufferInfo = .{ .buffer = self.vp_uniform_buffer[i], // Bufer to get data from .offset = 0, // Position of start of data .range = @sizeOf(UboViewProjection), // Size of data }; // Data about connection between binding and buffer const vp_set_write: vk.WriteDescriptorSet = .{ .dst_set = self.descriptor_sets[i], // Descriptor set to update .dst_binding = 0, // Binding to update (matches with binding on layout/shader) .dst_array_element = 0, // Index in array to update .descriptor_type = .uniform_buffer, // Type of descriptor .descriptor_count = 1, // Amount to update .p_buffer_info = @ptrCast(&vp_buffer_info), // Information about buffer data to bind .p_image_info = undefined, .p_texel_buffer_view = undefined, }; // List of descriptor set writes const set_writes = [_]vk.WriteDescriptorSet{vp_set_write}; // Update the descriptor sets with new buffer/binding info self.device.updateDescriptorSets(@intCast(set_writes.len), &set_writes, 0, null); } } fn updateUniformBuffers(self: *Self, image_index: u32) !void { // Copy VP data const data = try self.device.mapMemory( self.vp_uniform_buffer_memory[image_index], 0, @sizeOf(UboViewProjection), .{}, ); const vp_data: *UboViewProjection = @ptrCast(@alignCast(data)); vp_data.* = self.ubo_view_projection; self.device.unmapMemory(self.vp_uniform_buffer_memory[image_index]); } fn recordCommands(self: *Self, current_image: u32) !void { // Information about how to begin each command const buffer_begin_info: vk.CommandBufferBeginInfo = .{ // Buffer can be resubmitted when it has already been submitted and is awaiting execution .flags = .{ .simultaneous_use_bit = true }, }; const clear_values = [_]vk.ClearValue{ .{ .color = .{ .float_32 = [4]f32{ 0.6, 0.65, 0.4, 1.0 } } }, .{ .depth_stencil = .{ .depth = 1.0, .stencil = 1 } }, }; // Information about how to begin a render pass (only needed for graphical application) var render_pass_begin_info: vk.RenderPassBeginInfo = .{ .render_pass = self.render_pass, // Render pass to begin .render_area = .{ .offset = .{ .x = 0, .y = 0 }, // Start point of render pass in pixels .extent = self.extent, // Size of region to run render pass on (starting at offset) }, .p_clear_values = &clear_values, // List of clear values .clear_value_count = @intCast(clear_values.len), .framebuffer = undefined, }; render_pass_begin_info.framebuffer = self.swapchain_framebuffers[current_image]; const command_buffer = self.command_buffers[current_image]; // Start recording commands to command buffer try command_buffer.beginCommandBuffer(&buffer_begin_info); { // Begin render pass command_buffer.beginRenderPass(&render_pass_begin_info, vk.SubpassContents.@"inline"); // Needed when using dynamic state command_buffer.setViewport(0, 1, @ptrCast(&self.viewport)); command_buffer.setScissor(0, 1, @ptrCast(&self.scissor)); for (self.meshes) |mesh| { // Bind pipeline to be used in render pass command_buffer.bindPipeline(.graphics, self.graphics_pipeline); // Buffers to bind const vertex_buffers = [_]vk.Buffer{mesh.vertex_buffer}; // Offsets into buffers being bound const offsets = [_]vk.DeviceSize{0}; // Command to bind vertex buffer before drawing with them command_buffer.bindVertexBuffers(0, 1, &vertex_buffers, &offsets); // Bind mesh index buffer, with 0 offset and using the uint32 type command_buffer.bindIndexBuffer(mesh.index_buffer, 0, .uint32); // Push constants to given shader stage directly (no buffer) command_buffer.pushConstants( self.pipeline_layout, .{ .vertex_bit = true }, // Stage to push constants to 0, // Offset of push constants to update @sizeOf(Model), // Size of data being pushed @ptrCast(&mesh.ubo_model.model), // Actual data being pushed (can be array) ); // Bind descriptor sets command_buffer.bindDescriptorSets( .graphics, self.pipeline_layout, 0, 1, @ptrCast(&self.descriptor_sets[current_image]), 0, null, ); // Execute a pipeline command_buffer.drawIndexed(mesh.index_count, 1, 0, 0, 0); } // End render pass command_buffer.endRenderPass(); } // Stop recording to command buffer try command_buffer.endCommandBuffer(); } fn getPhysicalDevice(self: *Self) !void { var pdev_count: u32 = 0; _ = try self.instance.enumeratePhysicalDevices(&pdev_count, null); const pdevs = try self.allocator.alloc(vk.PhysicalDevice, pdev_count); defer self.allocator.free(pdevs); _ = try self.instance.enumeratePhysicalDevices(&pdev_count, pdevs.ptr); for (pdevs) |pdev| { if (self.checkDeviceSuitable(pdev)) { self.physical_device = pdev; break; } } else { // TODO Obviously needs to be something else unreachable; } } fn getRequiredExtensions(self: Self) ![][*:0]const u8 { var ext_count = sdl.vulkan.getInstanceExtensionsCount(self.window); if (enable_validation_layers) { ext_count += 1; } var extensions = try self.allocator.alloc([*:0]const u8, ext_count); _ = try sdl.vulkan.getInstanceExtensions(self.window, extensions); if (enable_validation_layers) { extensions[extensions.len - 1] = vk.extensions.ext_debug_utils.name; } return extensions; } fn getQueueFamilies(self: Self, pdev: vk.PhysicalDevice) !QueueFamilyIndices { var indices: QueueFamilyIndices = .{ .graphics_family = null }; var queue_family_count: u32 = 0; self.instance.getPhysicalDeviceQueueFamilyProperties(pdev, &queue_family_count, null); const queue_family_list = try self.allocator.alloc(vk.QueueFamilyProperties, queue_family_count); defer self.allocator.free(queue_family_list); self.instance.getPhysicalDeviceQueueFamilyProperties(pdev, &queue_family_count, queue_family_list.ptr); for (queue_family_list, 0..) |queue_family, i| { if (queue_family.queue_count > 0 and queue_family.queue_flags.graphics_bit) { indices.graphics_family = @intCast(i); } const presentation_support = try self.instance.getPhysicalDeviceSurfaceSupportKHR(pdev, @intCast(i), self.surface); if (queue_family.queue_count > 0 and presentation_support == vk.TRUE) { indices.presentation_family = @intCast(i); } if (indices.isValid()) { return indices; } } unreachable; } fn getDeviceQueues(self: Self) ![2]vk.Queue { const indices = try self.getQueueFamilies(self.physical_device); const graphics_queue = self.device.getDeviceQueue(indices.graphics_family.?, 0); const presentation_queue = self.device.getDeviceQueue(indices.presentation_family.?, 0); return .{ graphics_queue, presentation_queue }; } fn checkInstanceExtensions(self: Self, required_extensions: *const [][*:0]const u8) !bool { var prop_count: u32 = 0; _ = try self.vkb.enumerateInstanceExtensionProperties(null, &prop_count, null); const props = try self.allocator.alloc(vk.ExtensionProperties, prop_count); defer self.allocator.free(props); _ = try self.vkb.enumerateInstanceExtensionProperties(null, &prop_count, props.ptr); for (required_extensions.*) |required_extension| { for (props) |prop| { if (std.mem.eql(u8, std.mem.sliceTo(&prop.extension_name, 0), std.mem.span(required_extension))) { break; } } else { return false; } } return true; } fn checkDeviceExtensions(self: Self, pdev: vk.PhysicalDevice) !bool { var prop_count: u32 = 0; _ = try self.instance.enumerateDeviceExtensionProperties(pdev, null, &prop_count, null); if (prop_count == 0) { return false; } const props = try self.allocator.alloc(vk.ExtensionProperties, prop_count); defer self.allocator.free(props); _ = try self.instance.enumerateDeviceExtensionProperties(pdev, null, &prop_count, props.ptr); for (Utilities.device_extensions) |device_extension| { for (props) |prop| { if (std.mem.eql(u8, std.mem.sliceTo(&prop.extension_name, 0), std.mem.span(device_extension))) { break; } } else { return false; } } return true; } fn checkDeviceSuitable(self: Self, pdev: vk.PhysicalDevice) bool { const pdev_properties = self.instance.getPhysicalDeviceProperties(pdev); if (pdev_properties.device_type == .cpu) { return false; } const queue_family_indices = self.getQueueFamilies(pdev) catch return false; const extension_support = self.checkDeviceExtensions(pdev) catch return false; const swapchain_details = self.getSwapchainDetails(pdev) catch return false; defer self.allocator.free(swapchain_details.formats); defer self.allocator.free(swapchain_details.presentation_modes); const swapchain_valid = swapchain_details.formats.len != 0 and swapchain_details.formats.len != 0; return queue_family_indices.isValid() and extension_support and swapchain_valid; } fn checkValidationLayersSupport(self: Self) bool { var layer_count: u32 = undefined; _ = self.vkb.enumerateInstanceLayerProperties(&layer_count, null) catch return false; const available_layers = self.allocator.alloc(vk.LayerProperties, layer_count) catch unreachable; defer self.allocator.free(available_layers); _ = self.vkb.enumerateInstanceLayerProperties(&layer_count, available_layers.ptr) catch return false; for (validation_layers) |validation_layer| { for (available_layers) |available_layer| { if (std.mem.eql(u8, std.mem.span(validation_layer), std.mem.sliceTo(&available_layer.layer_name, 0))) { return true; } } } return false; } fn getSwapchainDetails(self: Self, pdev: vk.PhysicalDevice) !SwapchainDetails { // Capabilities const surface_capabilities = try self.instance.getPhysicalDeviceSurfaceCapabilitiesKHR(pdev, self.surface); // Formats var format_count: u32 = 0; _ = try self.instance.getPhysicalDeviceSurfaceFormatsKHR(pdev, self.surface, &format_count, null); const formats = try self.allocator.alloc(vk.SurfaceFormatKHR, format_count); _ = try self.instance.getPhysicalDeviceSurfaceFormatsKHR(pdev, self.surface, &format_count, formats.ptr); // Presentation modes var present_mode_count: u32 = 0; _ = try self.instance.getPhysicalDeviceSurfacePresentModesKHR(pdev, self.surface, &present_mode_count, null); const presentation_modes = try self.allocator.alloc(vk.PresentModeKHR, format_count); _ = try self.instance.getPhysicalDeviceSurfacePresentModesKHR(pdev, self.surface, &present_mode_count, presentation_modes.ptr); return .{ .surface_capabilities = surface_capabilities, .formats = formats, .presentation_modes = presentation_modes, }; } fn createImage( self: *Self, width: u32, height: u32, format: vk.Format, tiling: vk.ImageTiling, use_flags: vk.ImageUsageFlags, prop_flags: vk.MemoryPropertyFlags, image_memory: *vk.DeviceMemory, ) !vk.Image { // -- Create Image -- const image_create_info: vk.ImageCreateInfo = .{ .image_type = .@"2d", // Type of image (1D, 2D or 3D) .extent = .{ .width = width, // Width of image extent .height = height, // Height of image extent .depth = 1, // Depth of image (just 1, no 3D aspecct) }, .mip_levels = 1, // Number of mipmap levels .array_layers = 1, // Number of level in image array .format = format, // Format type of image .tiling = tiling, // How image data should be tiled (arranged for optimal reading) .initial_layout = .undefined, // Layout of image data on creation .usage = use_flags, // Bit flags defining what image will be used for .samples = .{ .@"1_bit" = true }, // Number of samples for multi-sampling .sharing_mode = .exclusive, // Whether image can be shared between queues }; const image = try self.device.createImage(&image_create_info, null); // -- Create memory for image -- // Get memory requirements for a type of image const memory_requirements = self.device.getImageMemoryRequirements(image); // Allocate memory using image requirements and user-defined properties const memory_alloc_info: vk.MemoryAllocateInfo = .{ .allocation_size = memory_requirements.size, .memory_type_index = Utilities.findMemoryTypeIndex(self.physical_device, self.instance, memory_requirements.memory_type_bits, prop_flags), }; image_memory.* = try self.device.allocateMemory(&memory_alloc_info, null); // Connect memory to image try self.device.bindImageMemory(image, image_memory.*, 0); return image; } fn createImageView(self: Self, image: vk.Image, format: vk.Format, aspect_flags: vk.ImageAspectFlags) !vk.ImageView { const image_view_create_info: vk.ImageViewCreateInfo = .{ .image = image, .format = format, .view_type = .@"2d", .components = .{ // Used for remapping rgba values to other rgba values .r = .identity, .g = .identity, .b = .identity, .a = .identity, }, .subresource_range = .{ .aspect_mask = aspect_flags, // Which aspect of image to view (e.g.: colour, depth, stencil, etc...) .base_mip_level = 0, // Start mipmap level to view from .level_count = 1, // Number of mipmap levels to view .base_array_layer = 0, // Start array level to view from .layer_count = 1, // Number of array levels to view }, }; return try self.device.createImageView(&image_view_create_info, null); } }; // Format: VK_FORMAT_R8G8B8A8_UNORM (VK_FORMAT_B8G8R8A8_UNORM as backup) // Color space: VK_COLOR_SPACE_SRGB_NONLINEAR_KHR fn chooseBestSurfaceFormat(formats: []vk.SurfaceFormatKHR) vk.SurfaceFormatKHR { // If only one format available and is undefined, then this means all formats are available if (formats.len == 1 and formats[0].format == vk.Format.undefined) { return .{ .format = vk.Format.r8g8b8a8_unorm, .color_space = vk.ColorSpaceKHR.srgb_nonlinear_khr, }; } for (formats) |format| { if ((format.format == vk.Format.r8g8b8a8_unorm or format.format == vk.Format.b8g8r8a8_unorm) and format.color_space == vk.ColorSpaceKHR.srgb_nonlinear_khr) { return format; } } return formats[0]; } fn chooseBestPresentationMode(presentation_modes: []vk.PresentModeKHR) vk.PresentModeKHR { for (presentation_modes) |presentation_mode| { if (presentation_mode == vk.PresentModeKHR.mailbox_khr) { return presentation_mode; } } // Use FIFO as Vulkan spec says it must be present return vk.PresentModeKHR.fifo_khr; } fn chooseSwapExtent(window: *sdl.Window, surface_capabilities: vk.SurfaceCapabilitiesKHR) vk.Extent2D { // If the current extent is at max value, the extent can vary. Otherwise it's the size of the window if (surface_capabilities.current_extent.width != std.math.maxInt(u32)) { return surface_capabilities.current_extent; } // If value can very, need to set the extent manually const framebuffer_size = sdl.vulkan.getDrawableSize(window); var extent: vk.Extent2D = .{ .width = @intCast(framebuffer_size.width), .height = @intCast(framebuffer_size.height), }; // Surface also defines max and min, so make sure it's within boundaries by clamping values extent.width = @max(surface_capabilities.min_image_extent.width, @min(surface_capabilities.max_image_extent.width, extent.width)); extent.height = @max(surface_capabilities.min_image_extent.height, @min(surface_capabilities.max_image_extent.height, extent.height)); return extent; } fn chooseSupportedFormat( pdev: vk.PhysicalDevice, instance: Instance, formats: []const vk.Format, tiling: vk.ImageTiling, feature_flags: vk.FormatFeatureFlags, ) ?vk.Format { // Loop through the options and find a compatible one // Depending on tiling choice. Need to check for different bit flag for (formats) |format| { // Get properties for given format on this device const properties = instance.getPhysicalDeviceFormatProperties(pdev, format); if (tiling == .linear and properties.linear_tiling_features.contains(feature_flags)) { return format; } else if (tiling == .optimal and properties.optimal_tiling_features.contains(feature_flags)) { return format; } } return null; } // Validation layers stuff fn createDebugMessenger(instance: Instance) !vk.DebugUtilsMessengerEXT { const debug_create_info = getDebugUtilsCreateInfo(); return try instance.createDebugUtilsMessengerEXT(&debug_create_info, null); } fn getDebugUtilsCreateInfo() vk.DebugUtilsMessengerCreateInfoEXT { return vk.DebugUtilsMessengerCreateInfoEXT{ .message_severity = .{ .verbose_bit_ext = true, .warning_bit_ext = true, .error_bit_ext = true }, .message_type = .{ .general_bit_ext = true, .validation_bit_ext = true, .performance_bit_ext = true }, .pfn_user_callback = debugCallback, }; } fn debugCallback( message_severity: vk.DebugUtilsMessageSeverityFlagsEXT, message_types: vk.DebugUtilsMessageTypeFlagsEXT, p_callback_data: ?*const vk.DebugUtilsMessengerCallbackDataEXT, p_user_data: ?*anyopaque, ) callconv(vk.vulkan_call_conv) vk.Bool32 { _ = p_user_data; const severity = getMessageSeverityLabel(message_severity); const message_type = getMessageTypeLabel(message_types); std.debug.print("[{s}] ({s}): {s}\n", .{ severity, message_type, p_callback_data.?.p_message.? }); return vk.TRUE; } inline fn getMessageSeverityLabel(message_severity: vk.DebugUtilsMessageSeverityFlagsEXT) []const u8 { if (message_severity.verbose_bit_ext) { return "VERBOSE"; } else if (message_severity.info_bit_ext) { return "INFO"; } else if (message_severity.warning_bit_ext) { return "WARNING"; } else if (message_severity.error_bit_ext) { return "ERROR"; } else { unreachable; } } inline fn getMessageTypeLabel(message_types: vk.DebugUtilsMessageTypeFlagsEXT) []const u8 { if (message_types.general_bit_ext) { return "general"; } else if (message_types.validation_bit_ext) { return "validation"; } else if (message_types.performance_bit_ext) { return "performance"; } else if (message_types.device_address_binding_bit_ext) { return "device_address_binding"; } else { return "unknown"; } }