Files
voxel-game/src/engine/Swapchain.zig
2026-05-22 23:34:25 +02:00

390 lines
13 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const Swapchain = @This();
const std = @import("std");
const ctx = @import("../AppContext.zig");
const vk = @import("vulkan");
const CommandBuffer = @import("CommandBuffer.zig");
const Engine = @import("Engine.zig");
const Texture = @import("Texture.zig");
const WaitSemaphore = @import("WaitSemaphore.zig");
params: Params,
extent: vk.Extent2D = .{ .width = 0, .height = 0 },
swapchain: vk.SwapchainKHR = .null_handle,
/// Allocated with `allocator_general`.
swapchain_images: []SwapchainImage = &.{},
depth_texture: ?Texture = null,
image_index: ?u32 = null,
fence: vk.Fence = .null_handle,
semaphore_image_acquired: vk.Semaphore = .null_handle,
pub const PresentResult = enum {
optimal,
suboptimal,
};
pub fn init() !*Swapchain {
const allocator_persistent = ctx.allocator_persistent;
const swapchain = try allocator_persistent.create(Swapchain);
errdefer allocator_persistent.destroy(swapchain);
swapchain.* = .{
.params = try .init(),
};
try swapchain.recreate();
return swapchain;
}
pub fn deinit(self: *Swapchain) void {
const allocator_general = ctx.allocator_general;
const engine = ctx.engine;
std.log.scoped(.deinit).debug("Deinitializing {*}", .{self});
for (self.swapchain_images) |*swapchain_image| {
swapchain_image.deinit();
}
allocator_general.free(self.swapchain_images);
if (self.depth_texture) |*depth_texture| {
depth_texture.deinit();
}
if (self.swapchain != .null_handle) {
engine.destroySwapchain(self.swapchain);
}
engine.destroySemaphore(self.semaphore_image_acquired);
engine.destroyFence(self.fence);
self.* = undefined;
}
pub fn recreate(self: *Swapchain) !void {
const allocator_general = ctx.allocator_general;
const allocator_frame = ctx.allocator_frame;
const engine = ctx.engine;
// TODO This is very "LMAO just wait for all" way to synchronize. It might
// be the only viable option, though. Either way, we should research what
// is "the way".
try engine.deviceWaitIdle();
const old_swapchain = self.swapchain;
const old_swapchain_images = self.swapchain_images;
const old_depth_texture = &self.depth_texture;
const old_semaphore_image_acquired = self.semaphore_image_acquired;
const old_fence = self.fence;
const extent = try getCurrentExtent();
std.log.debug("Recreating swapchain with extent of {d}×{d}...", .{ extent.width, extent.height });
// --- CREATE NEW SWAPCHAIN ------------------------------------------------
const surface_capabilities = try engine.getPhysicalDeviceSurfaceCapabilities();
const new_swapchain = try engine.createSwapchain(.{
.surface = engine.surface,
.min_image_count = self.params.image_count,
.image_format = self.params.surface_format.format,
.image_color_space = self.params.surface_format.color_space,
.image_extent = extent,
.image_array_layers = 1,
.image_usage = .{ .color_attachment_bit = true },
.queue_family_indices = &.{
engine.graphics_queue.allocation.family,
engine.presentation_queue.allocation.family,
},
.pre_transform = surface_capabilities.current_transform,
.composite_alpha = .{ .opaque_bit_khr = true },
.present_mode = .fifo_khr,
.clipped = .true,
.old_swapchain = old_swapchain,
});
errdefer engine.destroySwapchain(new_swapchain);
engine.setObjectName(new_swapchain, "SC {d}×{d}", .{ extent.width, extent.height });
// --- DESTROY OLD SWAPCHAIN AND ITS IMAGES --------------------------------
// NOTE At this point, the old swapchain has been recycled. There is no way
// to revert on error. Instead, we set the current swapchain and images to
// null and deinit the old swapchain and images.
for (old_swapchain_images) |*swapchain_image| {
swapchain_image.deinit();
}
allocator_general.free(self.swapchain_images);
self.swapchain_images = &.{};
if (old_depth_texture.*) |*depth_texture| {
depth_texture.deinit();
}
if (old_swapchain != .null_handle) {
engine.destroySwapchain(old_swapchain);
self.swapchain = .null_handle;
}
self.extent = .{ .width = 0, .height = 0 };
self.image_index = null;
if (old_semaphore_image_acquired != .null_handle) {
engine.destroySemaphore(old_semaphore_image_acquired);
self.semaphore_image_acquired = .null_handle;
}
if (old_fence != .null_handle) {
engine.destroyFence(old_fence);
self.fence = .null_handle;
}
// --- CREATE DEPTH TEXTURE ------------------------------------------------
var new_depth_texture = try Texture.init(.{
.width = extent.width,
.height = extent.height,
.target_queue = .graphics,
.usage = .depth,
.name = "@Depth",
});
errdefer new_depth_texture.deinit();
// --- CREATE NEW SWAPCHAIN IMAGES -----------------------------------------
const new_swapchain_images = blk: {
const images = try engine.getSwapchainImagesAlloc(new_swapchain, allocator_frame);
var swapchain_images: std.ArrayList(SwapchainImage) = try .initCapacity(allocator_general, images.len);
errdefer swapchain_images.deinit(allocator_general);
errdefer for (swapchain_images.items) |*x| x.deinit();
for (images, 0..) |image, index| {
swapchain_images.appendAssumeCapacity(try SwapchainImage.init(.{
.image = image,
.format = self.params.surface_format.format,
.extent = extent,
.depth_stencil_image_view = new_depth_texture.image_view,
.index = index,
}));
}
break :blk try swapchain_images.toOwnedSlice(allocator_general);
};
errdefer {
for (new_swapchain_images) |*swapchain_image| {
swapchain_image.deinit();
}
allocator_general.free(new_swapchain_images);
}
// --- CREATE SEMAPHORES AND FENCES ----------------------------------------
const semaphore_image_acquired = try engine.createSemaphore();
errdefer engine.destroySemaphore(semaphore_image_acquired);
engine.setObjectName(semaphore_image_acquired, "S Acquired[E]", .{});
const fence = try engine.createFence(.{
.flags = .{ .signaled_bit = true },
});
errdefer engine.destroyFence(fence);
engine.setObjectName(fence, "F", .{});
// --- COMMIT --------------------------------------------------------------
self.extent = extent;
self.swapchain = new_swapchain;
self.swapchain_images = new_swapchain_images;
self.depth_texture = new_depth_texture;
self.image_index = null;
self.semaphore_image_acquired = semaphore_image_acquired;
self.fence = fence;
}
pub fn acquire(self: *Swapchain) !void {
const engine = ctx.engine;
try engine.waitForFence(self.fence);
try engine.resetFence(self.fence);
const res = try engine.acquireNextImage(self.swapchain, .{
.semaphore = self.semaphore_image_acquired,
});
std.mem.swap(vk.Semaphore, &self.swapchain_images[res.image_index].semaphore_image_acquired, &self.semaphore_image_acquired);
self.image_index = res.image_index;
}
pub fn getCurrentSwapchainImage(self: *Swapchain) *const SwapchainImage {
return &self.swapchain_images[self.image_index.?];
}
pub const PresentInfo = struct {
command_buffer: CommandBuffer,
wait_semaphores: []const WaitSemaphore = &.{},
};
pub fn present(self: *Swapchain, present_info: PresentInfo) !void {
const allocator_frame = ctx.allocator_frame;
const engine = ctx.engine;
const current_swapchain_image = self.getCurrentSwapchainImage();
// --- SUBMIT COMMAND BUFFER -----------------------------------------------
var wait_semaphores: std.ArrayList(WaitSemaphore) = try .initCapacity(allocator_frame, 1 + present_info.wait_semaphores.len);
wait_semaphores.appendAssumeCapacity(.{
.semaphore = current_swapchain_image.semaphore_image_acquired,
.stage_flags = .{ .top_of_pipe_bit = true },
});
wait_semaphores.appendSliceAssumeCapacity(present_info.wait_semaphores);
try present_info.command_buffer.submit(.{
.wait_semaphores = wait_semaphores.items,
.signal_semaphores = &.{current_swapchain_image.semaphore_render_finished},
.fence = self.fence,
});
// --- PRESENT CURRENT FRAME -----------------------------------------------
_ = try engine.queuePresent(.{
.wait_semaphores = &.{current_swapchain_image.semaphore_render_finished},
.swapchain = self.swapchain,
.image_index = self.image_index.?,
});
}
fn getCurrentExtent() !vk.Extent2D {
const window = ctx.window;
const engine = ctx.engine;
const surface_capabilities = try engine.getPhysicalDeviceSurfaceCapabilities();
if (surface_capabilities.current_extent.width != std.math.maxInt(u32) and surface_capabilities.current_extent.height != std.math.maxInt(u32)) {
return surface_capabilities.current_extent;
}
const framebuffer_width, const framebuffer_height = window.getFramebufferSize();
return .{
.width = std.math.clamp(
@as(u32, @intCast(framebuffer_width)),
surface_capabilities.min_image_extent.width,
surface_capabilities.max_image_extent.width,
),
.height = std.math.clamp(
@as(u32, @intCast(framebuffer_height)),
surface_capabilities.min_image_extent.height,
surface_capabilities.max_image_extent.height,
),
};
}
const Params = struct {
surface_format: vk.SurfaceFormatKHR,
image_count: u32,
pub fn init() !Params {
const allocator_frame = ctx.allocator_frame;
const engine = ctx.engine;
const surface_capabilities = try engine.getPhysicalDeviceSurfaceCapabilities();
const surface_format = blk: {
const surface_formats = try engine.getPhysicalDeviceSurfaceFormatsAlloc(allocator_frame);
// Look for 8-bit BGRA sRGB surface format.
for (surface_formats) |surface_format| {
if (surface_format.format == .b8g8r8a8_srgb and surface_format.color_space == .srgb_nonlinear_khr) {
break :blk surface_format;
}
}
if (surface_formats.len == 0) {
return error.NoSurfaceFormats;
}
break :blk surface_formats[0];
};
const image_count = @min(
surface_capabilities.min_image_count + 1,
if (surface_capabilities.max_image_count > 0) surface_capabilities.max_image_count else std.math.maxInt(u32),
);
std.log.debug("Using surface format \"{s}\" and color space \"{s}\" with {d} images.", .{ @tagName(surface_format.format), @tagName(surface_format.color_space), image_count });
return .{
.surface_format = surface_format,
.image_count = image_count,
};
}
};
const SwapchainImage = struct {
image: vk.Image,
image_view: vk.ImageView,
semaphore_image_acquired: vk.Semaphore,
semaphore_render_finished: vk.Semaphore,
const InitProps = struct {
image: vk.Image,
format: vk.Format,
extent: vk.Extent2D,
depth_stencil_image_view: vk.ImageView,
index: usize,
};
fn init(props: InitProps) !SwapchainImage {
const engine = ctx.engine;
const image_view = try engine.createImageView(.{
.image = props.image,
.view_type = .@"2d",
.format = props.format,
.subresource_range = .{
.aspect_mask = .{ .color_bit = true },
.base_mip_level = 0,
.level_count = 1,
.base_array_layer = 0,
.layer_count = 1,
},
});
errdefer engine.destroyImageView(image_view);
engine.setObjectName(image_view, "IV Swapchain[{d}]", .{props.index});
const semaphore_image_acquired = try engine.createSemaphore();
errdefer engine.destroySemaphore(semaphore_image_acquired);
engine.setObjectName(image_view, "S Acquired[{d}]", .{props.index});
const semaphore_render_finished = try engine.createSemaphore();
errdefer engine.destroySemaphore(semaphore_render_finished);
engine.setObjectName(image_view, "S Rendered[{d}]", .{props.index});
return .{
.image = props.image,
.image_view = image_view,
.semaphore_image_acquired = semaphore_image_acquired,
.semaphore_render_finished = semaphore_render_finished,
};
}
fn deinit(self: *SwapchainImage) void {
const engine = ctx.engine;
std.log.scoped(.deinit).debug("Deinitializing {*}", .{self});
engine.destroySemaphore(self.semaphore_render_finished);
engine.destroySemaphore(self.semaphore_image_acquired);
engine.destroyImageView(self.image_view);
self.* = undefined;
}
};