const std = @import("std"); const glfw = @import("zglfw"); const vk = @import("vulkan"); // The build script compiles these GLSL files to SPIR-V and exposes them as // anonymous imports. They are currently only loaded and printed; the program // does not create shader modules or a graphics pipeline yet. const square_vert_spv = @embedFile("square_vertex_shader"); const square_frag_spv = @embedFile("square_fragment_shader"); const VulkanContext = struct { base: vk.BaseWrapper, instance: vk.Instance, vki: vk.InstanceWrapper, fn destroy(self: *const VulkanContext) void { self.vki.destroyInstance(self.instance, null); } }; const LogicalDeviceContext = struct { physical_device: vk.PhysicalDevice, graphics_queue_family_index: u32, device: vk.Device, vkd: vk.DeviceWrapper, graphics_queue: vk.Queue, fn destroy(self: *const LogicalDeviceContext) void { self.vkd.destroyDevice(self.device, null); } }; const SwapchainContext = struct { swapchain: vk.SwapchainKHR, images: []vk.Image, image_views: []vk.ImageView, format: vk.SurfaceFormatKHR, present_mode: vk.PresentModeKHR, extent: vk.Extent2D, image_count: u32, allocator: std.mem.Allocator, fn destroy(self: *const SwapchainContext, ldc: *const LogicalDeviceContext) void { for (self.image_views) |image_view| { ldc.vkd.destroyImageView(ldc.device, image_view, null); } self.allocator.free(self.image_views); self.allocator.free(self.images); ldc.vkd.destroySwapchainKHR(ldc.device, self.swapchain, null); } }; pub fn main() !void { std.debug.print("zig-chess bootstrap\n", .{}); std.debug.print("vertex shader bytes: {}\n", .{square_vert_spv.len}); std.debug.print("fragment shader bytes: {}\n", .{square_frag_spv.len}); // --------------------------------------------------------------------- // Window bootstrap // --------------------------------------------------------------------- const window = try initWindow(800, 600, "zig-chess"); defer glfw.terminate(); defer window.destroy(); window.show(); window.requestAttention(); // --------------------------------------------------------------------- // Vulkan instance setup // --------------------------------------------------------------------- const vc = try initInstance("zig-chess"); defer vc.destroy(); // --------------------------------------------------------------------- // Window surface // // This connects the platform window to Vulkan presentation. Swapchain // support and present-capable queue families are queried against this // surface. // --------------------------------------------------------------------- var surface: vk.SurfaceKHR = undefined; try glfw.createWindowSurface(vc.instance, window, null, &surface); defer vc.vki.destroySurfaceKHR(vc.instance, surface, null); std.debug.print("Created Vulkan surface\n", .{}); // --------------------------------------------------------------------- // Physical device and queue-family discovery // --------------------------------------------------------------------- const physical_devices = try vc.vki.enumeratePhysicalDevicesAlloc(vc.instance, std.heap.page_allocator); defer std.heap.page_allocator.free(physical_devices); //try debugPhysicalGPUs(vc, physical_devices, surface); // TODO(refactor): this is intentionally temporary and machine-specific. // Replace it with selection logic that searches for a device/queue pair // where queue_flags.graphics_bit is true and surface present support is // true. Hardcoding physical_devices[1] will fail on many systems. const selected_physical_device = physical_devices[1]; const graphics_queue_family_index: u32 = 0; const selected_props = vc.vki.getPhysicalDeviceProperties(selected_physical_device); std.debug.print( "selected device: {s}, queue family {}\n", .{ std.mem.sliceTo(&selected_props.device_name, 0), graphics_queue_family_index, }, ); // --------------------------------------------------------------------- // Logical device and queue // --------------------------------------------------------------------- const ldc = try initLogicalDevice(vc, selected_physical_device, graphics_queue_family_index); defer ldc.destroy(); // --------------------------------------------------------------------- // Swapchain setup // --------------------------------------------------------------------- const swapchain_context = try initSwapchain(vc, ldc, surface, window, std.heap.page_allocator); defer swapchain_context.destroy(&ldc); // --------------------------------------------------------------------- // Render pass // // This render pass has one color attachment: the current swapchain image. // load_op=.clear means each frame starts by clearing the image; final // layout present_src_khr means the image is ready for presentation. // // Refactor direction: this can become createRenderPass(device, format). // Later, when drawing pieces or UI, this may gain depth/stencil or change // if we move to dynamic rendering. // --------------------------------------------------------------------- const color_attachment = vk.AttachmentDescription{ .format = swapchain_context.format.format, .samples = .{ .@"1_bit" = true }, .load_op = .clear, .store_op = .store, .stencil_load_op = .dont_care, .stencil_store_op = .dont_care, .initial_layout = .undefined, .final_layout = .present_src_khr, }; const color_attachment_ref = vk.AttachmentReference{ .attachment = 0, .layout = .color_attachment_optimal, }; const subpass = vk.SubpassDescription{ .pipeline_bind_point = .graphics, .color_attachment_count = 1, .p_color_attachments = @ptrCast(&color_attachment_ref), }; const subpass_dependency = vk.SubpassDependency{ .src_subpass = vk.SUBPASS_EXTERNAL, .dst_subpass = 0, .src_stage_mask = .{ .color_attachment_output_bit = true, }, .src_access_mask = .{}, .dst_stage_mask = .{ .color_attachment_output_bit = true, }, .dst_access_mask = .{ .color_attachment_write_bit = true, }, }; const render_pass_create_info = vk.RenderPassCreateInfo{ .attachment_count = 1, .p_attachments = @ptrCast(&color_attachment), .subpass_count = 1, .p_subpasses = @ptrCast(&subpass), .dependency_count = 1, .p_dependencies = @ptrCast(&subpass_dependency), }; const render_pass = try ldc.vkd.createRenderPass(ldc.device, &render_pass_create_info, null); defer ldc.vkd.destroyRenderPass(ldc.device, render_pass, null); std.debug.print("created render pass\n", .{}); // --------------------------------------------------------------------- // Framebuffers // // A framebuffer binds the render pass attachment description to a concrete // image view. We need one framebuffer for each swapchain image view. // --------------------------------------------------------------------- const framebuffers = try std.heap.page_allocator.alloc( vk.Framebuffer, swapchain_context.image_views.len, ); defer std.heap.page_allocator.free(framebuffers); for (swapchain_context.image_views, 0..) |image_view, i| { const attachments = [_]vk.ImageView{image_view}; const framebuffer_create_info = vk.FramebufferCreateInfo{ .render_pass = render_pass, .attachment_count = attachments.len, .p_attachments = &attachments, .width = swapchain_context.extent.width, .height = swapchain_context.extent.height, .layers = 1, }; framebuffers[i] = try ldc.vkd.createFramebuffer( ldc.device, &framebuffer_create_info, null, ); } defer { for (framebuffers) |framebuffer| { ldc.vkd.destroyFramebuffer(ldc.device, framebuffer, null); } } std.debug.print("created framebuffers: {}\n", .{framebuffers.len}); // --------------------------------------------------------------------- // Command pool and command buffers // // Command buffers record GPU work. Right now each swapchain image gets one // pre-recorded command buffer that only clears the image. // // Refactor direction: for frame generation, introduce recordCommandBuffer() // and call it per frame after acquiring the image. That will make dynamic // board drawing, highlights, and resize handling easier to reason about. // --------------------------------------------------------------------- const command_pool_create_info = vk.CommandPoolCreateInfo{ .flags = .{ .reset_command_buffer_bit = true, }, .queue_family_index = ldc.graphics_queue_family_index, }; const command_pool = try ldc.vkd.createCommandPool( ldc.device, &command_pool_create_info, null, ); defer ldc.vkd.destroyCommandPool(ldc.device, command_pool, null); std.debug.print("created command pool\n", .{}); const command_buffers = try std.heap.page_allocator.alloc( vk.CommandBuffer, framebuffers.len, ); defer std.heap.page_allocator.free(command_buffers); const command_buffer_allocate_info = vk.CommandBufferAllocateInfo{ .command_pool = command_pool, .level = .primary, .command_buffer_count = @intCast(command_buffers.len), }; try ldc.vkd.allocateCommandBuffers( ldc.device, &command_buffer_allocate_info, command_buffers.ptr, ); std.debug.print("allocated command buffers: {}\n", .{command_buffers.len}); for (command_buffers, 0..) |command_buffer, i| { const begin_info = vk.CommandBufferBeginInfo{}; try ldc.vkd.beginCommandBuffer(command_buffer, &begin_info); // This is the only "drawing" currently happening: begin a render pass // and clear the swapchain image. The embedded shaders are not used yet. const clear_color = vk.ClearValue{ .color = .{ .float_32 = .{ 0.02, 0.02, 0.08, 1.0 }, }, }; const render_pass_begin_info = vk.RenderPassBeginInfo{ .render_pass = render_pass, .framebuffer = framebuffers[i], .render_area = .{ .offset = .{ .x = 0, .y = 0 }, .extent = swapchain_context.extent, }, .clear_value_count = 1, .p_clear_values = @ptrCast(&clear_color), }; ldc.vkd.cmdBeginRenderPass( command_buffer, &render_pass_begin_info, .@"inline", ); ldc.vkd.cmdEndRenderPass(command_buffer); try ldc.vkd.endCommandBuffer(command_buffer); } std.debug.print("recorded command buffers\n", .{}); // --------------------------------------------------------------------- // Synchronization objects // // The image-available semaphore is signaled when acquireNextImageKHR has a // swapchain image ready. The render-finished semaphore is signaled when GPU // rendering completes and presentation may wait on it. The fence lets the // CPU wait until submitted GPU work for this frame is done. // // Refactor direction: use arrays for 2 frames in flight, e.g. // image_available[2], render_finished[2], in_flight_fences[2]. // --------------------------------------------------------------------- const semaphore_create_info = vk.SemaphoreCreateInfo{}; const image_available_semaphore = try ldc.vkd.createSemaphore( ldc.device, &semaphore_create_info, null, ); defer ldc.vkd.destroySemaphore(ldc.device, image_available_semaphore, null); const render_finished_semaphore = try ldc.vkd.createSemaphore( ldc.device, &semaphore_create_info, null, ); defer ldc.vkd.destroySemaphore(ldc.device, render_finished_semaphore, null); const fence_create_info = vk.FenceCreateInfo{ .flags = .{ .signaled_bit = true, }, }; const in_flight_fence = try ldc.vkd.createFence( ldc.device, &fence_create_info, null, ); defer ldc.vkd.destroyFence(ldc.device, in_flight_fence, null); std.debug.print("created synchronization objects\n", .{}); // --------------------------------------------------------------------- // Single-frame acquire/submit/present // // This renders exactly one frame before entering the event loop. To turn // this into frame generation, move this whole block into drawFrame() and // call it from the window loop below. // // Per-frame shape: // 1. wait/reset the in-flight fence // 2. acquire the next swapchain image // 3. submit the command buffer for that image // 4. present that image // // Later this block must handle out-of-date/suboptimal swapchains and call // recreateSwapchainResources(). // --------------------------------------------------------------------- const wait_fences = [_]vk.Fence{in_flight_fence}; _ = try ldc.vkd.waitForFences(ldc.device, &wait_fences, .true, std.math.maxInt(u64)); try ldc.vkd.resetFences(ldc.device, &wait_fences); const acquire_result = try ldc.vkd.acquireNextImageKHR( ldc.device, swapchain_context.swapchain, std.math.maxInt(u64), image_available_semaphore, .null_handle, ); const image_index = acquire_result.image_index; std.debug.print("acquired swapchain image: {}\n", .{image_index}); const wait_semaphores = [_]vk.Semaphore{image_available_semaphore}; const wait_stages = [_]vk.PipelineStageFlags{ .{ .color_attachment_output_bit = true, }, }; const signal_semaphores = [_]vk.Semaphore{render_finished_semaphore}; const submit_command_buffers = [_]vk.CommandBuffer{ command_buffers[image_index], }; const submit_info = vk.SubmitInfo{ .wait_semaphore_count = wait_semaphores.len, .p_wait_semaphores = &wait_semaphores, .p_wait_dst_stage_mask = &wait_stages, .command_buffer_count = submit_command_buffers.len, .p_command_buffers = &submit_command_buffers, .signal_semaphore_count = signal_semaphores.len, .p_signal_semaphores = &signal_semaphores, }; try ldc.vkd.queueSubmit(ldc.graphics_queue, &[_]vk.SubmitInfo{submit_info}, in_flight_fence); const present_swapchains = [_]vk.SwapchainKHR{swapchain_context.swapchain}; const present_image_indices = [_]u32{image_index}; const present_info = vk.PresentInfoKHR{ .wait_semaphore_count = signal_semaphores.len, .p_wait_semaphores = &signal_semaphores, .swapchain_count = present_swapchains.len, .p_swapchains = &present_swapchains, .p_image_indices = &present_image_indices, }; _ = try ldc.vkd.queuePresentKHR(ldc.graphics_queue, &present_info); std.debug.print("presented one frame\n", .{}); // --------------------------------------------------------------------- // Event loop // // Currently this only keeps the window alive after the one presented frame. // Next rendering milestone: call drawFrame() each iteration after polling // events, then wait for the device to be idle before cleanup on exit. // --------------------------------------------------------------------- while (!window.shouldClose()) { glfw.pollEvents(); } } fn initWindow(x: c_int, y: c_int, name: [:0]const u8) !*glfw.Window { try glfw.init(); errdefer glfw.terminate(); glfw.windowHint(.client_api, .no_api); const window = try glfw.Window.create( x, y, name, null, null, ); std.debug.print("GLFW platform: {any}\n", .{glfw.getPlatform()}); std.debug.print("Vulkan supported by GLFW: {}\n", .{glfw.isVulkanSupported()}); const size = window.getSize(); const fb_size = window.getFramebufferSize(); std.debug.print("Window size: {}x{}\n", .{ size[0], size[1] }); std.debug.print("Framebuffer size: {}x{}\n", .{ fb_size[0], fb_size[1] }); std.debug.print("Window visible: {}\n", .{window.getAttribute(.visible)}); return window; } fn initInstance(name: [:0]const u8) !VulkanContext { const base = vk.BaseWrapper.load(glfw.getInstanceProcAddress); const required_extensions = try glfw.getRequiredInstanceExtensions(); std.debug.print("Required instance extensions:\n", .{}); for (required_extensions) |extension| { std.debug.print(" {s}\n", .{extension}); } const app_info = vk.ApplicationInfo{ .p_application_name = name, .application_version = 1, .p_engine_name = name, .engine_version = 1, .api_version = @bitCast(vk.makeApiVersion(0, 1, 2, 0)), }; const instance_create_info = vk.InstanceCreateInfo{ .p_application_info = &app_info, .enabled_extension_count = @intCast(required_extensions.len), .pp_enabled_extension_names = required_extensions.ptr, }; const instance = try base.createInstance(&instance_create_info, null); std.debug.print("Created Vulkan Instance", .{}); const vki = vk.InstanceWrapper.load(instance, base.dispatch.vkGetInstanceProcAddr.?); return .{ .instance = instance, .base = base, .vki = vki, }; } fn initLogicalDevice( vc: VulkanContext, physical_device: vk.PhysicalDevice, graphics_queue_family_index: u32, ) !LogicalDeviceContext { const queue_priority: f32 = 1.0; const queue_create_info = vk.DeviceQueueCreateInfo{ .queue_family_index = graphics_queue_family_index, .queue_count = 1, .p_queue_priorities = @ptrCast(&queue_priority), }; const device_extensions = [_][*:0]const u8{ "VK_KHR_swapchain", }; const device_create_info = vk.DeviceCreateInfo{ .queue_create_info_count = 1, .p_queue_create_infos = @ptrCast(&queue_create_info), .enabled_extension_count = device_extensions.len, .pp_enabled_extension_names = &device_extensions, }; const device = try vc.vki.createDevice(physical_device, &device_create_info, null); errdefer vc.vki.destroyDevice(device, null); std.debug.print("created logical device\n", .{}); const vkd = vk.DeviceWrapper.load(device, vc.vki.dispatch.vkGetDeviceProcAddr.?); const graphics_queue = vkd.getDeviceQueue(device, graphics_queue_family_index, 0); std.debug.print("retrieved graphics queue\n", .{}); return .{ .physical_device = physical_device, .graphics_queue_family_index = graphics_queue_family_index, .device = device, .vkd = vkd, .graphics_queue = graphics_queue, }; } fn initSwapchain( vc: VulkanContext, ldc: LogicalDeviceContext, surface: vk.SurfaceKHR, window: *glfw.Window, allocator: std.mem.Allocator, ) !SwapchainContext { const surface_caps = try vc.vki.getPhysicalDeviceSurfaceCapabilitiesKHR( ldc.physical_device, surface, ); std.debug.print( "surface current extent: {}x{}\n", .{ surface_caps.current_extent.width, surface_caps.current_extent.height, }, ); std.debug.print( "surface min/max image count: {}/{}\n", .{ surface_caps.min_image_count, surface_caps.max_image_count, }, ); const surface_formats = try vc.vki.getPhysicalDeviceSurfaceFormatsAllocKHR( ldc.physical_device, surface, allocator, ); defer allocator.free(surface_formats); std.debug.print("surface formats: {}\n", .{surface_formats.len}); for (surface_formats, 0..) |format, i| { std.debug.print( " format {}: format={any}, color_space={any}\n", .{ i, format.format, format.color_space }, ); } const present_modes = try vc.vki.getPhysicalDeviceSurfacePresentModesAllocKHR( ldc.physical_device, surface, allocator, ); defer allocator.free(present_modes); std.debug.print("present modes: {}\n", .{present_modes.len}); for (present_modes, 0..) |mode, i| { std.debug.print(" present mode {}: {any}\n", .{ i, mode }); } var chosen_surface_format = surface_formats[0]; for (surface_formats) |format| { if (format.format == .b8g8r8a8_srgb and format.color_space == .srgb_nonlinear_khr) { chosen_surface_format = format; break; } } var chosen_present_mode: vk.PresentModeKHR = .fifo_khr; for (present_modes) |mode| { if (mode == .fifo_khr) { chosen_present_mode = mode; break; } } const framebuffer_size = window.getFramebufferSize(); const chosen_extent = if (surface_caps.current_extent.width != std.math.maxInt(u32)) surface_caps.current_extent else vk.Extent2D{ .width = @intCast(framebuffer_size[0]), .height = @intCast(framebuffer_size[1]), }; var chosen_image_count = surface_caps.min_image_count + 1; if (surface_caps.max_image_count != 0 and chosen_image_count > surface_caps.max_image_count) { chosen_image_count = surface_caps.max_image_count; } std.debug.print( "chosen swapchain format={any}, color_space={any}\n", .{ chosen_surface_format.format, chosen_surface_format.color_space }, ); std.debug.print("chosen present mode={any}\n", .{chosen_present_mode}); std.debug.print( "chosen extent={}x{}\n", .{ chosen_extent.width, chosen_extent.height }, ); std.debug.print("chosen image count={}\n", .{chosen_image_count}); const swapchain_create_info = vk.SwapchainCreateInfoKHR{ .surface = surface, .min_image_count = chosen_image_count, .image_format = chosen_surface_format.format, .image_color_space = chosen_surface_format.color_space, .image_extent = chosen_extent, .image_array_layers = 1, .image_usage = .{ .color_attachment_bit = true, }, .image_sharing_mode = .exclusive, .pre_transform = surface_caps.current_transform, .composite_alpha = .{ .opaque_bit_khr = true, }, .present_mode = chosen_present_mode, .clipped = .true, }; const swapchain = try ldc.vkd.createSwapchainKHR(ldc.device, &swapchain_create_info, null); errdefer ldc.vkd.destroySwapchainKHR(ldc.device, swapchain, null); std.debug.print("created swapchain\n", .{}); const swapchain_images = try ldc.vkd.getSwapchainImagesAllocKHR( ldc.device, swapchain, allocator, ); errdefer allocator.free(swapchain_images); std.debug.print("swapchain images: {}\n", .{swapchain_images.len}); const swapchain_image_views = try allocator.alloc(vk.ImageView, swapchain_images.len); errdefer allocator.free(swapchain_image_views); var created_image_view_count: usize = 0; errdefer { for (swapchain_image_views[0..created_image_view_count]) |image_view| { ldc.vkd.destroyImageView(ldc.device, image_view, null); } } for (swapchain_images, 0..) |image, i| { const image_view_create_info = vk.ImageViewCreateInfo{ .image = image, .view_type = .@"2d", .format = chosen_surface_format.format, .components = .{ .r = .identity, .g = .identity, .b = .identity, .a = .identity, }, .subresource_range = .{ .aspect_mask = .{ .color_bit = true, }, .base_mip_level = 0, .level_count = 1, .base_array_layer = 0, .layer_count = 1, }, }; swapchain_image_views[i] = try ldc.vkd.createImageView( ldc.device, &image_view_create_info, null, ); created_image_view_count += 1; } std.debug.print("created swapchain image views: {}\n", .{swapchain_image_views.len}); return .{ .swapchain = swapchain, .images = swapchain_images, .image_views = swapchain_image_views, .format = chosen_surface_format, .present_mode = chosen_present_mode, .extent = chosen_extent, .image_count = chosen_image_count, .allocator = allocator, }; } fn debugPhysicalGPUs(vc: VulkanContext, physical_devices: []vk.PhysicalDevice, surface: vk.SurfaceKHR) !void { std.debug.print("physical devices: {}\n", .{physical_devices.len}); for (physical_devices, 0..) |physical_device, i| { const props = vc.vki.getPhysicalDeviceProperties(physical_device); std.debug.print("device {}: {s}\n", .{ i, std.mem.sliceTo(&props.device_name, 0) }); const queue_families = try vc.vki.getPhysicalDeviceQueueFamilyPropertiesAlloc( physical_device, std.heap.page_allocator, ); defer std.heap.page_allocator.free(queue_families); for (queue_families, 0..) |queue_family, queue_index| { const supports_graphics = queue_family.queue_flags.graphics_bit; const supports_compute = queue_family.queue_flags.compute_bit; const supports_transfer = queue_family.queue_flags.transfer_bit; const supports_present = try vc.vki.getPhysicalDeviceSurfaceSupportKHR( physical_device, @intCast(queue_index), surface, ); std.debug.print( " queue {}: count={}, graphics={}, compute={}, transfer={}, present={}\n", .{ queue_index, queue_family.queue_count, supports_graphics, supports_compute, supports_transfer, supports_present, }, ); } } }