From 34253e231bd9cee85db2b5485cad11f47ff5f1d0 Mon Sep 17 00:00:00 2001 From: Robin Voetter Date: Thu, 2 Jul 2020 17:09:34 +0200 Subject: [PATCH] Swapchain --- examples/graphics_context.zig | 21 +++ examples/main.zig | 35 ++-- examples/swapchain.zig | 306 ++++++++++++++++++++++++++++++++++ 3 files changed, 341 insertions(+), 21 deletions(-) create mode 100644 examples/swapchain.zig diff --git a/examples/graphics_context.zig b/examples/graphics_context.zig index df802a7..e2b2c74 100644 --- a/examples/graphics_context.zig +++ b/examples/graphics_context.zig @@ -21,6 +21,7 @@ const InstanceDispatch = struct { vkEnumerateDeviceExtensionProperties: vk.PfnEnumerateDeviceExtensionProperties, vkGetPhysicalDeviceSurfaceFormatsKHR: vk.PfnGetPhysicalDeviceSurfaceFormatsKHR, vkGetPhysicalDeviceSurfacePresentModesKHR: vk.PfnGetPhysicalDeviceSurfacePresentModesKHR, + vkGetPhysicalDeviceSurfaceCapabilitiesKHR: vk.PfnGetPhysicalDeviceSurfaceCapabilitiesKHR, vkGetPhysicalDeviceQueueFamilyProperties: vk.PfnGetPhysicalDeviceQueueFamilyProperties, vkGetPhysicalDeviceSurfaceSupportKHR: vk.PfnGetPhysicalDeviceSurfaceSupportKHR, vkGetDeviceProcAddr: vk.PfnGetDeviceProcAddr, @@ -30,6 +31,21 @@ const InstanceDispatch = struct { const DeviceDispatch = struct { vkDestroyDevice: vk.PfnDestroyDevice, vkGetDeviceQueue: vk.PfnGetDeviceQueue, + vkCreateSemaphore: vk.PfnCreateSemaphore, + vkCreateFence: vk.PfnCreateFence, + vkCreateImageView: vk.PfnCreateImageView, + vkDestroyImageView: vk.PfnDestroyImageView, + vkDestroySemaphore: vk.PfnDestroySemaphore, + vkDestroyFence: vk.PfnDestroyFence, + vkGetSwapchainImagesKHR: vk.PfnGetSwapchainImagesKHR, + vkCreateSwapchainKHR: vk.PfnCreateSwapchainKHR, + vkDestroySwapchainKHR: vk.PfnDestroySwapchainKHR, + vkAcquireNextImageKHR: vk.PfnAcquireNextImageKHR, + vkDeviceWaitIdle: vk.PfnDeviceWaitIdle, + vkWaitForFences: vk.PfnWaitForFences, + vkResetFences: vk.PfnResetFences, + vkQueueSubmit: vk.PfnQueueSubmit, + vkQueuePresentKHR: vk.PfnQueuePresentKHR, usingnamespace vk.DeviceWrapper(@This()); }; @@ -86,6 +102,11 @@ pub const GraphicsContext = struct { self.vki.destroySurfaceKHR(self.instance, self.surface, null); self.vki.destroyInstance(self.instance, null); } + + pub fn deviceName(self: GraphicsContext) []const u8 { + const len = std.mem.indexOfScalar(u8, &self.props.device_name, 0).?; + return self.props.device_name[0 .. len]; + } }; pub const Queue = struct { diff --git a/examples/main.zig b/examples/main.zig index 3ed2fe0..21a2bd9 100644 --- a/examples/main.zig +++ b/examples/main.zig @@ -2,26 +2,9 @@ const std = @import("std"); const vk = @import("vulkan"); const c = @import("c.zig"); const GraphicsContext = @import("graphics_context.zig").GraphicsContext; - -const BaseDispatch = struct { - vkCreateInstance: vk.PfnCreateInstance, - usingnamespace vk.BaseWrapper(@This()); -}; - -const InstanceDispatch = struct { - vkDestroyInstance: vk.PfnDestroyInstance, - vkCreateDevice: vk.PfnCreateDevice, - vkDestroySurfaceKHR: vk.PfnDestroySurfaceKHR, - usingnamespace vk.InstanceWrapper(@This()); -}; - -const DeviceDispatch = struct { - vkDestroyDevice: vk.PfnDestroyDevice, - usingnamespace vk.DeviceWrapper(@This()); -}; +const Swapchain = @import("swapchain.zig").Swapchain; const app_name = "vulkan-zig example"; - const app_info = vk.ApplicationInfo{ .p_application_name = app_name, .application_version = vk.makeVersion(0, 0, 0), @@ -34,12 +17,12 @@ pub fn main() !void { if (c.glfwInit() != c.GLFW_TRUE) return error.GlfwInitFailed; defer c.glfwTerminate(); - const dim = vk.Extent2D{.width = 800, .height = 600}; + const extent = vk.Extent2D{.width = 800, .height = 600}; c.glfwWindowHint(c.GLFW_CLIENT_API, c.GLFW_NO_API); const window = c.glfwCreateWindow( - dim.width, - dim.height, + extent.width, + extent.height, app_name, null, null @@ -48,4 +31,14 @@ pub fn main() !void { const gc = try GraphicsContext.init(std.heap.page_allocator, &app_info, window); defer gc.deinit(); + + std.debug.print("Using device: {}\n", .{gc.deviceName()}); + + const swapchain = try Swapchain.init(&gc, std.heap.page_allocator, extent); + defer swapchain.deinit(); + + while (c.glfwWindowShouldClose(window) == c.GLFW_FALSE) { + c.glfwSwapBuffers(window); + c.glfwPollEvents(); + } } diff --git a/examples/swapchain.zig b/examples/swapchain.zig new file mode 100644 index 0000000..37e1af9 --- /dev/null +++ b/examples/swapchain.zig @@ -0,0 +1,306 @@ +const std = @import("std"); +const vk = @import("vulkan"); +const c = @import("c.zig"); +const GraphicsContext = @import("graphics_context.zig").GraphicsContext; +const Allocator = std.mem.Allocator; + +pub const Swapchain = struct { + pub const PresentState = enum { + optimal, + suboptimal, + }; + + gc: *const GraphicsContext, + allocator: *Allocator, + + surface_format: vk.SurfaceFormatKHR, + present_mode: vk.PresentModeKHR, + extent: vk.Extent2D, + handle: vk.SwapchainKHR, + + swap_images: []SwapImage, + image_index: u32, + next_image_acquired: vk.Semaphore, + + pub fn init(gc: *const GraphicsContext, allocator: *Allocator, extent: vk.Extent2D) !Swapchain { + return try initRecycle(gc, allocator, extent, .null_handle); + } + + pub fn initRecycle(gc: *const GraphicsContext, allocator: *Allocator, extent: vk.Extent2D, old_handle: vk.SwapchainKHR) !Swapchain { + const caps = try gc.vki.getPhysicalDeviceSurfaceCapabilitiesKHR(gc.pdev, gc.surface); + const actual_extent = findActualExtent(caps, extent); + if (actual_extent.width == 0 or actual_extent.height == 0) { + return error.InvalidSurfaceDimensions; + } + + const surface_format = try findSurfaceFormat(gc, allocator); + const present_mode = try findPresentMode(gc, allocator); + + var image_count = caps.min_image_count + 1; + if (caps.max_image_count > 0) { + image_count = std.math.min(image_count, caps.max_image_count); + } + + const concurrent = gc.graphics_queue.family != gc.present_queue.family; + const qfi = [_]u32{gc.graphics_queue.family, gc.present_queue.family}; + + const handle = try gc.vkd.createSwapchainKHR(gc.dev, .{ + .flags = .{}, + .surface = gc.surface, + .min_image_count = image_count, + .image_format = surface_format.format, + .image_color_space = surface_format.color_space, + .image_extent = actual_extent, + .image_array_layers = 1, + .image_usage = .{.color_attachment_bit = true, .transfer_dst_bit = true}, + .image_sharing_mode = if (concurrent) .concurrent else .exclusive, + .queue_family_index_count = qfi.len, + .p_queue_family_indices = &qfi, + .pre_transform = caps.current_transform, + .composite_alpha = .{.opaque_bit_khr = true}, + .present_mode = present_mode, + .clipped = vk.TRUE, + .old_swapchain = old_handle, + }, null); + errdefer gc.vkd.destroySwapchainKHR(gc.dev, handle, null); + + const swap_images = try initSwapchainImages(gc, handle, surface_format.format, allocator); + errdefer for (swap_images) |si| si.deinit(gc); + + var next_image_acquired = try gc.vkd.createSemaphore(gc.dev, .{.flags = .{}}, null); + errdefer gc.vkd.destroySemaphore(gc.dev, next_image_acquired, null); + + const result = try gc.vkd.acquireNextImageKHR(gc.dev, handle, std.math.maxInt(u64), next_image_acquired, .null_handle); + if (result.result != .success) { + return error.ImageAcquireFailed; + } + + std.mem.swap(vk.Semaphore, &swap_images[result.image_index].image_acquired, &next_image_acquired); + return Swapchain{ + .gc = gc, + .allocator = allocator, + .surface_format = surface_format, + .present_mode = present_mode, + .extent = actual_extent, + .handle = handle, + .swap_images = swap_images, + .image_index = result.image_index, + .next_image_acquired = next_image_acquired, + }; + } + + fn deinitExceptSwapchain(self: Swapchain) void { + for (self.swap_images) |si| si.deinit(self.gc); + self.gc.vkd.destroySemaphore(self.gc.dev, self.next_image_acquired, null); + } + + pub fn deinit(self: Swapchain) void { + self.gc.vkd.deviceWaitIdle(self.gc.dev) catch return; + self.deinitExceptSwapchain(); + self.gc.vkd.destroySwapchainKHR(self.gc.dev, self.handle, null); + } + + pub fn recreate(self: *Swapchain, new_extent: vk.Extnet2D) !void { + const gc = self.gc; + const allocator = self.allocator; + const old_handle = self.handle; + self.deinitExceptSwapchain(); + self.* = try initRecycle(gc, allocator, new_extent, old_handle); + } + + pub fn currentImage(self: Swapchain) vk.Image { + return self.swap_images[self.image_index].image; + } + + pub fn currentSwapImage(self: Swapchain) *const SwapImage { + return &self.swap_images[self.image_index]; + } + + pub fn present(self: *Swapchain, cmdbuf: vk.CommandBuffer) !PresentState { + // Simple method: + // 1) Acquire next image + // 2) Wait for and reset fence of the acquired image + // 3) Submit command buffer with fence of acquired image, + // dependendent on the semaphore signalled by the first step. + // 4) Present current frame, dependent on semaphore signalled by previous step + // Problem: This way we can't reference the current image while rendering. + // Better method: Shuffle the steps around such that acquire next image is the last step, + // leaving the swapchain in a state with the current image. + // 1) Wait for and reset fence of current image + // 2) Submit command buffer, signalling fence of current image and dependent on + // the semaphore signalled by step 4. + // 3) Present current frame, dependent on semaphore signalled by the submit + // 4) Acquire next image, signalling its semaphore + // One problem that arises is that we can't know beforehand which semaphore to signal, + // so we keep an extra auxilery semaphore that is swapped around + + // Step 1: Make sure the current frame has finished rendering + const current = self.currentSwapImage(); + const fence_ptr = @ptrCast([*]const vk.Fence, ¤t.frame_fence); + _ = try self.gc.vkd.waitForFences(self.gc.dev, 1, fence_ptr, vk.TRUE, std.math.maxInt(u64)); + try self.gc.vkd.resetFences(self.gc.dev, 1, fence_ptr); + + // Step 2: Submit the command buffer + const wait_stage = [_]vk.PipelineStageFlags{.{.top_of_pipe_bit = true}}; + try self.gc.vkd.queueSubmit(self.gc.graphics_queue.handle, 1, .{ + .wait_semaphore_count = 1, + .p_wait_semaphores = @ptrCast([*]const vk.Semaphore, ¤t.image_acquired), + .p_wait_dst_stage_mask = &wait_stage, + .command_buffer_count = 1, + .p_command_buffers = @ptrCast([*]const CommandBuffer, &cmdbuf), + .signal_semaphore_count = 1, + .p_signal_semaphores = @ptrCast([*]const vk.Semaphore, ¤t.render_finished), + }, current_swap_image.frame_fence); + + // Step 3: Present the current frame + _ = try self.gc.vkd.queuePresentKHR(self.gc.present_queue.handle, .{ + .wait_semaphore_count = 1, + .p_signal_semaphores = @ptrCast([*]const vk.Semaphore, ¤t.render_finished), + .swapchain_count = 1, + .p_swapchains = @ptrCast([*]const vk.Swapchain, &self.handle), + .p_image_indices = @ptrCast([*]const u32, &self.image_index), + .p_results = null, + }); + + // Step 4: Acquire next frame + const result = try self.gc.vkd.acquireNextImageKHR( + self.gc.dev, + self.handle, + std.math.maxInt(u64), + self.next_image_acquired, + .null_handle, + ); + + std.mem.swap(vk.Semaphore, &self.swap_images[result.image_index].image_acquired, &self.next_image_acquired); + self.image_index = result.image_index; + + return switch (result) { + .success => .optimal, + .suboptimal_khr => .suboptimal, + }; + } +}; + +const SwapImage = struct { + image: vk.Image, + view: vk.ImageView, + image_acquired: vk.Semaphore, + render_finished: vk.Semaphore, + frame_fence: vk.Fence, + + fn init(gc: *const GraphicsContext, image: vk.Image, format: vk.Format) !SwapImage { + const view = try gc.vkd.createImageView(gc.dev, .{ + .flags = .{}, + .image = image, + .view_type = .@"2d", + .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, + }, + }, null); + errdefer gc.vkd.destroyImageView(gc.dev, view, null); + + const image_acquired = try gc.vkd.createSemaphore(gc.dev, .{.flags = .{}}, null); + errdefer gc.vkd.destroySemaphore(gc.dev, image_acquired, null); + + const render_finished = try gc.vkd.createSemaphore(gc.dev, .{.flags = .{}}, null); + errdefer gc.vkd.destroySemaphore(gc.dev, image_acquired, null); + + const frame_fence = try gc.vkd.createFence(gc.dev, .{.flags = .{.signaled_bit = true}}, null); + errdefer gc.vkd.destroyFence(gc.dev, frame_fence, null); + + return SwapImage{ + .image = image, + .view = view, + .image_acquired = image_acquired, + .render_finished = render_finished, + .frame_fence = frame_fence, + }; + } + + fn deinit(self: SwapImage, gc: *const GraphicsContext) void { + gc.vkd.destroyImageView(gc.dev, self.view, null); + gc.vkd.destroySemaphore(gc.dev, self.image_acquired, null); + gc.vkd.destroySemaphore(gc.dev, self.render_finished, null); + gc.vkd.destroyFence(gc.dev, self.frame_fence, null); + } +}; + +fn initSwapchainImages(gc: *const GraphicsContext, swapchain: vk.SwapchainKHR, format: vk.Format, allocator: *Allocator) ![]SwapImage { + var count: u32 = undefined; + _ = try gc.vkd.getSwapchainImagesKHR(gc.dev, swapchain, &count, null); + const images = try allocator.alloc(vk.Image, count); + defer allocator.free(images); + _ = try gc.vkd.getSwapchainImagesKHR(gc.dev, swapchain, &count, images.ptr); + + const swap_images = try allocator.alloc(SwapImage, count); + errdefer allocator.free(images); + + var i: usize = 0; + errdefer for (swap_images[0 .. i]) |si| si.deinit(gc); + + for (images) |image| { + swap_images[i] = try SwapImage.init(gc, image, format); + i += 1; + } + + return swap_images; +} + +fn findSurfaceFormat(gc: *const GraphicsContext, allocator: *Allocator) !vk.SurfaceFormatKHR { + const preferred = vk.SurfaceFormatKHR{ + .format = .b8g8r8a8_srgb, + .color_space = .srgb_nonlinear_khr, + }; + + var count: u32 = undefined; + _ = try gc.vki.getPhysicalDeviceSurfaceFormatsKHR(gc.pdev, gc.surface, &count, null); + const surface_formats = try allocator.alloc(vk.SurfaceFormatKHR, count); + defer allocator.free(surface_formats); + _ = try gc.vki.getPhysicalDeviceSurfaceFormatsKHR(gc.pdev, gc.surface, &count, surface_formats.ptr); + + for (surface_formats) |sfmt| { + if (std.meta.eql(sfmt, preferred)) { + return preferred; + } + } + + return surface_formats[0]; // There must always be at least one supported surface format +} + +fn findPresentMode(gc: *const GraphicsContext, allocator: *Allocator) !vk.PresentModeKHR { + var count: u32 = undefined; + _ = try gc.vki.getPhysicalDeviceSurfacePresentModesKHR(gc.pdev, gc.surface, &count, null); + const present_modes = try allocator.alloc(vk.PresentModeKHR, count); + defer allocator.free(present_modes); + _ = try gc.vki.getPhysicalDeviceSurfacePresentModesKHR(gc.pdev, gc.surface, &count, present_modes.ptr); + + const preferred = [_]vk.PresentModeKHR{ + .mailbox_khr, + .immediate_khr, + }; + + for (preferred) |mode| { + if (std.mem.indexOfScalar(vk.PresentModeKHR, present_modes, mode) != null) { + return mode; + } + } + + return .fifo_khr; +} + +fn findActualExtent(caps: vk.SurfaceCapabilitiesKHR, extent: vk.Extent2D) vk.Extent2D { + if (caps.current_extent.width != 0xFFFF_FFFF) { + return caps.current_extent; + } else { + return .{ + .width = std.math.clamp(extent.width, caps.min_image_extent.width, caps.max_image_extent.width), + .height = std.math.clamp(extent.height, caps.min_image_extent.height, caps.max_image_extent.height), + }; + } +}