const std = @import("std"); const sdl = @import("sdl"); const vk = @import("vulkan"); const builtin = @import("builtin"); const shaders = @import("shaders"); const zm = @import("zmath"); const img = @import("zstbi"); const ai = @import("assimp.zig").c; const QueueUtils = @import("queue_utils.zig"); const StringUtils = @import("string_utils.zig"); const Utilities = @import("utilities.zig"); const Vertex = Utilities.Vertex; const Vector3 = Utilities.Vector3; const Context = @import("Context.zig"); const Instance = Context.Instance; const Swapchain = @import("Swapchain.zig"); const Texture = @import("Texture.zig"); const Image = @import("image.zig"); const Mesh = @import("Mesh.zig"); const MeshModel = @import("MeshModel.zig"); const MAX_FRAME_DRAWS: u32 = 2; const MAX_OBJECTS: u32 = 20; pub const CommandBuffer = vk.CommandBufferProxy(Context.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, current_frame: u32 = 0, ctx: Context, swapchain: Swapchain, // Scene settings ubo_view_projection: UboViewProjection, // Main viewport: vk.Viewport, scissor: vk.Rect2D, texture_sampler: vk.Sampler, depth_buffer_image: []vk.Image, depth_buffer_image_memory: []vk.DeviceMemory, depth_buffer_image_view: []vk.ImageView, colour_buffer_image: []vk.Image, colour_buffer_image_memory: []vk.DeviceMemory, colour_buffer_image_view: []vk.ImageView, // Descriptors descriptor_set_layout: vk.DescriptorSetLayout, sampler_set_layout: vk.DescriptorSetLayout, input_set_layout: vk.DescriptorSetLayout, push_constant_range: vk.PushConstantRange, descriptor_pool: vk.DescriptorPool, sampler_descriptor_pool: vk.DescriptorPool, input_descriptor_pool: vk.DescriptorPool, descriptor_sets: []vk.DescriptorSet, // sampler_descriptor_sets: std.ArrayList(vk.DescriptorSet), input_descriptor_sets: []vk.DescriptorSet, vp_uniform_buffer: []vk.Buffer, vp_uniform_buffer_memory: []vk.DeviceMemory, // TODO command_buffers: []CommandBuffer, // Assets textures: std.ArrayList(Texture), model_list: std.ArrayList(MeshModel), // Pipeline graphics_pipeline: vk.Pipeline, pipeline_layout: vk.PipelineLayout, second_pipeline: vk.Pipeline, second_pipeline_layout: vk.PipelineLayout, render_pass: vk.RenderPass, // Pools graphics_command_pool: vk.CommandPool, // Utilities depth_format: vk.Format, // Synchronisation image_available: [MAX_FRAME_DRAWS]vk.Semaphore, render_finished: [MAX_FRAME_DRAWS]vk.Semaphore, draw_fences: [MAX_FRAME_DRAWS]vk.Fence, pub fn init(allocator: std.mem.Allocator, window: sdl.Window) !Self { var self: Self = undefined; self.allocator = allocator; self.ctx = try Context.init(allocator, window); self.current_frame = 0; self.swapchain = try Swapchain.create(allocator, self.ctx); try self.createColourBufferImage(); try self.createDepthBufferImage(); try self.createRenderPass(); try self.createDescriptorSetLayout(); try self.createPushConstantRange(); try self.createGraphicsPipeline(); try self.createFramebuffers(); try self.createCommandPool(); self.sampler_descriptor_sets = try std.ArrayList(vk.DescriptorSet).initCapacity(self.allocator, self.swapchain.swapchain_images.len); try self.createCommandBuffers(); try self.createTextureSampler(); try self.createUniformBuffers(); try self.createDescriptorPool(); try self.createDescriptorSets(); try self.createInputDescriptorSets(); try self.createSynchronisation(); self.image_files = std.ArrayList(img.Image).init(self.allocator); self.textures = std.ArrayList(Texture).init(self.allocator); self.model_list = std.ArrayList(MeshModel).init(allocator); const aspect: f32 = @as(f32, @floatFromInt(self.swapchain.extent.width)) / @as(f32, @floatFromInt(self.swapchain.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, 2.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; _ = try self.createTexture("giraffe.png"); return self; } pub fn updateModel(self: *Self, model_id: usize, new_model: zm.Mat) !void { if (model_id < self.model_list.items.len) { self.model_list.items[model_id].model = new_model; } } pub fn updateCamera(self: *Self, movement: zm.Mat) void { self.ubo_view_projection.view = zm.mul(self.ubo_view_projection.view, movement); } pub fn draw(self: *Self) !void { // Wait for given fence to signal (open) from last draw before continuing _ = try self.ctx.device.waitForFences( 1, @ptrCast(&self.draw_fences[self.current_frame]), vk.TRUE, std.math.maxInt(u64), ); // Manually reset (close) fences try self.ctx.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.ctx.device.acquireNextImageKHR( self.swapchain.handle, 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.ctx.device.queueSubmit(self.ctx.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.handle), // 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.ctx.device.queuePresentKHR(self.ctx.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.ctx.device.deviceWaitIdle() catch undefined; for (0..self.model_list.items.len) |i| { self.model_list.items[i].destroy(); } self.model_list.deinit(); for (0..self.image_files.items.len) |i| { self.image_files.items[i].deinit(); } self.image_files.deinit(); self.ctx.device.destroySampler(self.texture_sampler, null); for ( self.texture_images.items, self.texture_image_memory.items, self.texture_image_views.items, ) |tex_image, tex_image_memory, tex_image_view| { self.ctx.device.destroyImage(tex_image, null); self.ctx.device.freeMemory(tex_image_memory, null); self.ctx.device.destroyImageView(tex_image_view, null); } self.texture_images.deinit(); self.texture_image_memory.deinit(); self.texture_image_views.deinit(); for (0..self.depth_buffer_image.len) |i| { self.ctx.device.destroyImageView(self.depth_buffer_image_view[i], null); self.ctx.device.destroyImage(self.depth_buffer_image[i], null); self.ctx.device.freeMemory(self.depth_buffer_image_memory[i], null); } self.allocator.free(self.depth_buffer_image); self.allocator.free(self.depth_buffer_image_memory); self.allocator.free(self.depth_buffer_image_view); for (0..self.colour_buffer_image.len) |i| { self.ctx.device.destroyImageView(self.colour_buffer_image_view[i], null); self.ctx.device.destroyImage(self.colour_buffer_image[i], null); self.ctx.device.freeMemory(self.colour_buffer_image_memory[i], null); } self.allocator.free(self.colour_buffer_image); self.allocator.free(self.colour_buffer_image_memory); self.allocator.free(self.colour_buffer_image_view); self.ctx.device.destroyDescriptorPool(self.input_descriptor_pool, null); self.ctx.device.destroyDescriptorPool(self.descriptor_pool, null); self.ctx.device.destroyDescriptorSetLayout(self.descriptor_set_layout, null); self.ctx.device.destroyDescriptorPool(self.sampler_descriptor_pool, null); self.ctx.device.destroyDescriptorSetLayout(self.sampler_set_layout, null); self.ctx.device.destroyDescriptorSetLayout(self.input_set_layout, null); self.sampler_descriptor_sets.deinit(); self.allocator.free(self.input_descriptor_sets); for (0..self.swapchain.swapchain_images.len) |i| { self.ctx.device.destroyBuffer(self.vp_uniform_buffer[i], null); self.ctx.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 (0..MAX_FRAME_DRAWS) |i| { self.ctx.device.destroySemaphore(self.render_finished[i], null); self.ctx.device.destroySemaphore(self.image_available[i], null); self.ctx.device.destroyFence(self.draw_fences[i], null); } self.allocator.free(self.command_buffers); self.ctx.device.destroyCommandPool(self.graphics_command_pool, null); self.ctx.device.destroyPipeline(self.second_pipeline, null); self.ctx.device.destroyPipelineLayout(self.second_pipeline_layout, null); self.ctx.device.destroyPipeline(self.graphics_pipeline, null); self.ctx.device.destroyPipelineLayout(self.pipeline_layout, null); self.ctx.device.destroyRenderPass(self.render_pass, null); self.swapchain.deinit(); self.ctx.deinit(); } fn createRenderPass(self: *Self) !void { // -- Attachments -- var subpasses: [2]vk.SubpassDescription = undefined; // Subpass 1 attachments and references (input attachments) // Colour attachment (input) const colour_format = chooseSupportedFormat( self.ctx.physical_device, self.ctx.instance, &[_]vk.Format{.r8g8b8a8_srgb}, .optimal, .{ .color_attachment_bit = true }, ); const colour_attachment: vk.AttachmentDescription = .{ .format = colour_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 = .color_attachment_optimal, }; // Depth attachment (input) 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, }; // Colour attachment (input) reference const colour_attachment_reference: vk.AttachmentReference = .{ .attachment = 1, .layout = .color_attachment_optimal, }; // Depth attachment (input) reference const depth_attachment_reference: vk.AttachmentReference = .{ .attachment = 2, .layout = .depth_stencil_attachment_optimal, }; subpasses[0] = .{ .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, }; // Subpass 2 attachments and references // Colour attachment of the render pass const swapchain_colour_attachment: vk.AttachmentDescription = .{ .format = self.swapchain.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) }; // Attachment reference uses an attachment index that refers to index in the attachment list passed to render pass create info const swapchain_colour_attachment_reference: vk.AttachmentReference = .{ .attachment = 0, .layout = vk.ImageLayout.color_attachment_optimal, }; // References to attachments that subpass will take input from const input_references = [_]vk.AttachmentReference{ .{ .attachment = 1, // Colour attachment .layout = .shader_read_only_optimal, }, .{ .attachment = 2, // Depth attachment .layout = .shader_read_only_optimal, }, }; subpasses[1] = .{ .pipeline_bind_point = .graphics, .color_attachment_count = 1, .p_color_attachments = @ptrCast(&swapchain_colour_attachment_reference), .input_attachment_count = @intCast(input_references.len), .p_input_attachments = &input_references, }; // -- Subpass dependencies // Need to determine when layout transitions occur using subpass dependencies const subpass_dependencies = [_]vk.SubpassDependency{ // Conversion from VK_IMAGE_LAYOUT_UNDEFINED to VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL .{ // 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 = .{ .color_attachment_read_bit = true, .color_attachment_write_bit = true }, }, // Subpass 1 layout (colour/depth) to subpass 2 layout (shader read) .{ .src_subpass = 0, .src_stage_mask = .{ .color_attachment_output_bit = true }, .src_access_mask = .{ .color_attachment_write_bit = true }, .dst_subpass = 1, .dst_stage_mask = .{ .fragment_shader_bit = true }, .dst_access_mask = .{ .shader_read_bit = true }, }, // Conversion from VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL to VK_IMAGE_LAYOUT_PRESENT_SRC_KHR .{ // Transition must happen after... .src_subpass = 0, .src_stage_mask = .{ .color_attachment_output_bit = true }, .src_access_mask = .{ .color_attachment_read_bit = true, .color_attachment_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{ swapchain_colour_attachment, 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 = @intCast(subpasses.len), .p_subpasses = &subpasses, .dependency_count = @intCast(subpass_dependencies.len), .p_dependencies = &subpass_dependencies, }; self.render_pass = try self.ctx.device.createRenderPass(&render_pass_create_info, null); } fn createDescriptorSetLayout(self: *Self) !void { // -- Uniform values descriptor set layout -- // 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.ctx.device.createDescriptorSetLayout(&layout_create_info, null); // -- Texture sampler descriptor set layout -- // Texture binding info const sampler_layout_binding: vk.DescriptorSetLayoutBinding = .{ .binding = 0, .descriptor_type = .combined_image_sampler, .descriptor_count = 1, .stage_flags = .{ .fragment_bit = true }, .p_immutable_samplers = null, }; // Create a descriptor set layout with given bindings for texture const texture_layout_info: vk.DescriptorSetLayoutCreateInfo = .{ .binding_count = 1, .p_bindings = @ptrCast(&sampler_layout_binding), }; self.sampler_set_layout = try self.ctx.device.createDescriptorSetLayout(&texture_layout_info, null); // -- Create input attachment image descriptor set layout // Colour input binding const colour_input_layout_binding: vk.DescriptorSetLayoutBinding = .{ .binding = 0, .descriptor_type = .input_attachment, .descriptor_count = 1, .stage_flags = .{ .fragment_bit = true }, }; // Depth input binding const depth_input_layout_binding: vk.DescriptorSetLayoutBinding = .{ .binding = 1, .descriptor_type = .input_attachment, .descriptor_count = 1, .stage_flags = .{ .fragment_bit = true }, }; // Array of input attachment bindings const input_bindings = [_]vk.DescriptorSetLayoutBinding{ colour_input_layout_binding, depth_input_layout_binding }; // Create a descriptor set layout for input attachments const input_layout_create_info: vk.DescriptorSetLayoutCreateInfo = .{ .binding_count = @intCast(input_bindings.len), .p_bindings = &input_bindings, }; self.input_set_layout = try self.ctx.device.createDescriptorSetLayout(&input_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 createColourBufferImage(self: *Self) !void { self.colour_buffer_image = try self.allocator.alloc(vk.Image, self.swapchain.swapchain_images.len); self.colour_buffer_image_memory = try self.allocator.alloc(vk.DeviceMemory, self.swapchain.swapchain_images.len); self.colour_buffer_image_view = try self.allocator.alloc(vk.ImageView, self.swapchain.swapchain_images.len); // Get supported format for colour attachment const colour_format = chooseSupportedFormat( self.ctx.physical_device, self.ctx.instance, &[_]vk.Format{.r8g8b8a8_srgb}, .optimal, .{ .color_attachment_bit = true }, ) orelse return error.FormatNotSupported; // Create colour buffers for (0..self.colour_buffer_image.len) |i| { self.colour_buffer_image[i] = try Image.createImage( self.ctx, self.swapchain.extent.width, self.swapchain.extent.height, colour_format, .optimal, .{ .color_attachment_bit = true, .input_attachment_bit = true }, .{ .device_local_bit = true }, &self.colour_buffer_image_memory[i], ); self.colour_buffer_image_view[i] = try Image.createImageView( self.ctx, self.colour_buffer_image[i], colour_format, .{ .color_bit = true }, ); } } fn createDepthBufferImage(self: *Self) !void { self.depth_buffer_image = try self.allocator.alloc(vk.Image, self.swapchain.swapchain_images.len); self.depth_buffer_image_memory = try self.allocator.alloc(vk.DeviceMemory, self.swapchain.swapchain_images.len); self.depth_buffer_image_view = try self.allocator.alloc(vk.ImageView, self.swapchain.swapchain_images.len); // Get supported depth buffer format const formats = [_]vk.Format{ .d32_sfloat_s8_uint, .d32_sfloat, .d24_unorm_s8_uint }; self.depth_format = chooseSupportedFormat( self.ctx.physical_device, self.ctx.instance, &formats, .optimal, .{ .depth_stencil_attachment_bit = true }, ) orelse return error.UnsupportedDepthBufferFormat; for (0..self.depth_buffer_image.len) |i| { // Create depth buffer image self.depth_buffer_image[i] = try Image.createImage( self.ctx, self.swapchain.extent.width, self.swapchain.extent.height, self.depth_format, .optimal, .{ .depth_stencil_attachment_bit = true, .input_attachment_bit = true }, .{ .device_local_bit = true }, &self.depth_buffer_image_memory[i], ); // Create depth buffer image view self.depth_buffer_image_view[i] = try Image.createImageView(self.ctx, self.depth_buffer_image[i], self.depth_format, .{ .depth_bit = true }); } } fn createGraphicsPipeline(self: *Self) !void { // Create shader modules const vert = try self.ctx.device.createShaderModule(&.{ .code_size = shaders.shader_vert.len, .p_code = @ptrCast(&shaders.shader_vert), }, null); defer self.ctx.device.destroyShaderModule(vert, null); const frag = try self.ctx.device.createShaderModule(&.{ .code_size = shaders.shader_frag.len, .p_code = @ptrCast(&shaders.shader_frag), }, null); defer self.ctx.device.destroyShaderModule(frag, null); // -- Shader stage creation information -- // Vertex stage creation information var vertex_shader_create_info: vk.PipelineShaderStageCreateInfo = .{ .stage = .{ .vertex_bit = true }, .module = vert, .p_name = "main", }; // Fragment stage creation information var 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"), }, // Texture attribute .{ .binding = 0, .location = 2, .format = vk.Format.r32g32_sfloat, .offset = @offsetOf(Vertex, "tex"), }, }; // -- Vertex input -- var 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.swapchain.extent.width), .height = @floatFromInt(self.swapchain.extent.height), .min_depth = 0.0, .max_depth = 1.0, }; self.scissor = .{ .offset = .{ .x = 0, .y = 0 }, .extent = self.swapchain.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 = false }, // 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 descriptor_set_layouts = [_]vk.DescriptorSetLayout{ self.descriptor_set_layout, self.sampler_set_layout }; const pipeline_layout_create_info: vk.PipelineLayoutCreateInfo = .{ .set_layout_count = @intCast(descriptor_set_layouts.len), .p_set_layouts = &descriptor_set_layouts, .push_constant_range_count = 1, .p_push_constant_ranges = @ptrCast(&self.push_constant_range), }; self.pipeline_layout = try self.ctx.device.createPipelineLayout(&pipeline_layout_create_info, null); // -- Depth stencil testing -- var 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 -- var 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.ctx.device.createGraphicsPipelines( .null_handle, 1, @ptrCast(&pipeline_create_info), null, @ptrCast(&self.graphics_pipeline), ); // -- Create second pass pipeline // Second pass shaders const second_vert_shader_module = try self.ctx.device.createShaderModule(&.{ .code_size = shaders.second_vert.len, .p_code = @ptrCast(&shaders.second_vert), }, null); defer self.ctx.device.destroyShaderModule(second_vert_shader_module, null); const second_frag_shader_module = try self.ctx.device.createShaderModule(&.{ .code_size = shaders.second_frag.len, .p_code = @ptrCast(&shaders.second_frag), }, null); defer self.ctx.device.destroyShaderModule(second_frag_shader_module, null); // Set new shaders vertex_shader_create_info.module = second_vert_shader_module; fragment_shader_create_info.module = second_frag_shader_module; const second_shader_stages = [_]vk.PipelineShaderStageCreateInfo{ vertex_shader_create_info, fragment_shader_create_info }; // No vertex data for second pass vertex_input_create_info.vertex_binding_description_count = 0; vertex_input_create_info.p_vertex_binding_descriptions = null; vertex_input_create_info.vertex_attribute_description_count = 0; vertex_input_create_info.p_vertex_attribute_descriptions = null; // Don't want to write to depth buffer depth_stencil_create_info.depth_write_enable = vk.FALSE; // Create new pipeline layout const second_pipeline_layout_create_info: vk.PipelineLayoutCreateInfo = .{ .set_layout_count = 1, .p_set_layouts = @ptrCast(&self.input_set_layout), }; self.second_pipeline_layout = try self.ctx.device.createPipelineLayout(&second_pipeline_layout_create_info, null); pipeline_create_info.stage_count = @intCast(second_shader_stages.len); pipeline_create_info.p_stages = &second_shader_stages; pipeline_create_info.layout = self.second_pipeline_layout; pipeline_create_info.subpass = 1; // Create second pipeline _ = try self.ctx.device.createGraphicsPipelines( .null_handle, 1, @ptrCast(&pipeline_create_info), null, @ptrCast(&self.second_pipeline), ); } fn createFramebuffers(self: *Self) !void { self.swapchain.swapchain_framebuffers = try self.allocator.alloc(vk.Framebuffer, self.swapchain.swapchain_images.len); // Create a frammebuffer for each swapchain image for (self.swapchain.swapchain_images, 0..) |swapchain_image, i| { // Order matters const attachments = [_]vk.ImageView{ swapchain_image.image_view, self.colour_buffer_image_view[i], self.depth_buffer_image_view[i], }; 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.swapchain.extent.width, // Framebuffer width .height = self.swapchain.extent.height, // Framebuffer height .layers = 1, // Framebuffer layers }; self.swapchain.swapchain_framebuffers[i] = try self.ctx.device.createFramebuffer(&framebuffer_create_info, null); } } fn createCommandPool(self: *Self) !void { // Get indices of queue families from device const queue_family_indices = try QueueUtils.getQueueFamilies(self.ctx, self.ctx.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.ctx.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.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.ctx.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.ctx.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.ctx.device.createSemaphore(&.{}, null); self.render_finished[i] = try self.ctx.device.createSemaphore(&.{}, null); self.draw_fences[i] = try self.ctx.device.createFence(&fence_create_info, null); } } fn createTextureSampler(self: *Self) !void { // Sampler create info const sampler_create_info: vk.SamplerCreateInfo = .{ .mag_filter = .linear, // How to render when image is magnified on screen .min_filter = .linear, // How to render when image is minified on screen .address_mode_u = .repeat, // How to handle texture wrap in U (x direction) .address_mode_v = .repeat, // How to handle texture wrap in U (y direction) .address_mode_w = .repeat, // How to handle texture wrap in U (z direction) .border_color = .int_opaque_black, // Border beyond texture (only works for border clamp) .unnormalized_coordinates = vk.FALSE, // Whether coords should be normalized (between 0 and 1) .mipmap_mode = .linear, // Mipmap interpolation mode .mip_lod_bias = 0.0, // Level of detail bias for mip level .min_lod = 0.0, // Minimum lod to pick mip level .max_lod = 0.0, // Maximum lod to pick mip level .anisotropy_enable = vk.TRUE, // Enable anisotropy .max_anisotropy = 16.0, // Anisotropy sample level .compare_enable = vk.FALSE, .compare_op = .never, }; self.texture_sampler = try self.ctx.device.createSampler(&sampler_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.swapchain_images.len); self.vp_uniform_buffer_memory = try self.allocator.alloc(vk.DeviceMemory, self.swapchain.swapchain_images.len); // Create the uniform buffers for (0..self.vp_uniform_buffer.len) |i| { try Utilities.createBuffer( self.ctx.physical_device, self.ctx.instance, self.ctx.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 { // -- Create uniform descriptor pool -- // 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.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.ctx.device.createDescriptorPool(&pool_create_info, null); // -- Create sampler descriptor pool -- // Texture sampler pool const sampler_pool_size: vk.DescriptorPoolSize = .{ .type = .combined_image_sampler, .descriptor_count = MAX_OBJECTS, }; // FIXME Not the best (look into array layers) const sampler_pool_create_info: vk.DescriptorPoolCreateInfo = .{ .max_sets = MAX_OBJECTS, .pool_size_count = 1, .p_pool_sizes = @ptrCast(&sampler_pool_size), }; self.sampler_descriptor_pool = try self.ctx.device.createDescriptorPool(&sampler_pool_create_info, null); // -- Create input attachment descriptor pool // Colour attachment pool size const colour_input_pool_size: vk.DescriptorPoolSize = .{ .type = .input_attachment, .descriptor_count = @intCast(self.colour_buffer_image_view.len), }; // Depth attachment pool size const depth_input_pool_size: vk.DescriptorPoolSize = .{ .type = .input_attachment, .descriptor_count = @intCast(self.depth_buffer_image_view.len), }; const input_pool_sizes = [_]vk.DescriptorPoolSize{ colour_input_pool_size, depth_input_pool_size }; // Create input attachment pool const input_pool_create_info: vk.DescriptorPoolCreateInfo = .{ .max_sets = @intCast(self.swapchain.swapchain_images.len), .pool_size_count = @intCast(input_pool_sizes.len), .p_pool_sizes = &input_pool_sizes, }; self.input_descriptor_pool = try self.ctx.device.createDescriptorPool(&input_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.swapchain_images.len); var set_layouts = try self.allocator.alloc(vk.DescriptorSetLayout, self.swapchain.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.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.ctx.device.allocateDescriptorSets(&set_alloc_info, self.descriptor_sets.ptr); // Update all of descriptor set buffer bindings for (0..self.swapchain.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.ctx.device.updateDescriptorSets(@intCast(set_writes.len), &set_writes, 0, null); } } fn createInputDescriptorSets(self: *Self) !void { self.input_descriptor_sets = try self.allocator.alloc(vk.DescriptorSet, self.swapchain.swapchain_images.len); // Fill array of layouts ready for set creation var set_layouts = try self.allocator.alloc(vk.DescriptorSetLayout, self.swapchain.swapchain_images.len); defer self.allocator.free(set_layouts); for (0..set_layouts.len) |i| { set_layouts[i] = self.input_set_layout; } // Input attachment descriptor set allocation info const set_alloc_info: vk.DescriptorSetAllocateInfo = .{ .descriptor_pool = self.input_descriptor_pool, .descriptor_set_count = @intCast(self.swapchain.swapchain_images.len), .p_set_layouts = set_layouts.ptr, }; // Allocate descriptor sets try self.ctx.device.allocateDescriptorSets(&set_alloc_info, self.input_descriptor_sets.ptr); // Update each descriptor set with input attachment for (0..self.swapchain.swapchain_images.len) |i| { // Colour attachment descriptor const colour_attachment_descriptor: vk.DescriptorImageInfo = .{ .image_layout = .shader_read_only_optimal, .image_view = self.colour_buffer_image_view[i], .sampler = .null_handle, }; // Colour attachment descriptor write const colour_write: vk.WriteDescriptorSet = .{ .dst_set = self.input_descriptor_sets[i], .dst_binding = 0, .dst_array_element = 0, .descriptor_type = .input_attachment, .descriptor_count = 1, .p_image_info = @ptrCast(&colour_attachment_descriptor), .p_buffer_info = undefined, .p_texel_buffer_view = undefined, }; // Depth attachment descriptor const depth_attachment_descriptor: vk.DescriptorImageInfo = .{ .image_layout = .shader_read_only_optimal, .image_view = self.depth_buffer_image_view[i], .sampler = .null_handle, }; // Depth attachment descriptor write const depth_write: vk.WriteDescriptorSet = .{ .dst_set = self.input_descriptor_sets[i], .dst_binding = 1, .dst_array_element = 0, .descriptor_type = .input_attachment, .descriptor_count = 1, .p_image_info = @ptrCast(&depth_attachment_descriptor), .p_buffer_info = undefined, .p_texel_buffer_view = undefined, }; // List of input descriptor set writes const set_writes = [_]vk.WriteDescriptorSet{ colour_write, depth_write }; // Update descriptor sets self.ctx.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.ctx.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.ctx.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 = .{ 0.0, 0.0, 0.0, 0.0 } } }, .{ .color = .{ .float_32 = .{ 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.swapchain.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 = self.swapchain.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)); // Bind pipeline to be used in render pass command_buffer.bindPipeline(.graphics, self.graphics_pipeline); for (self.model_list.items) |model| { // 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(&model.model), // Actual data being pushed (can be array) ); for (model.mesh_list.items) |mesh| { // 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); const descriptor_set_group = [_]vk.DescriptorSet{ self.descriptor_sets[current_image], self.sampler_descriptor_sets.items[mesh.tex_id], }; // Bind descriptor sets command_buffer.bindDescriptorSets( .graphics, self.pipeline_layout, 0, @intCast(descriptor_set_group.len), &descriptor_set_group, 0, null, ); // Execute a pipeline command_buffer.drawIndexed(mesh.index_count, 1, 0, 0, 0); } } // Start second subpass command_buffer.nextSubpass(.@"inline"); command_buffer.bindPipeline(.graphics, self.second_pipeline); command_buffer.bindDescriptorSets( .graphics, self.second_pipeline_layout, 0, 1, @ptrCast(&self.input_descriptor_sets[current_image]), 0, null, ); command_buffer.draw(3, 1, 0, 0); // End render pass command_buffer.endRenderPass(); } // Stop recording to command buffer try command_buffer.endCommandBuffer(); } pub fn createMeshModel(self: *Self, model_file: []const u8) !usize { // Pass tex smapler MeshModel.new( self.allocator, self.ctx, self.graphics_command_pool, self.texture_sampler, model_file, ); } }; 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; }