diff --git a/src/engine.zig b/src/engine.zig deleted file mode 100644 index e28c222..0000000 --- a/src/engine.zig +++ /dev/null @@ -1,250 +0,0 @@ -const std = @import("std"); - -const glfw = @import("zglfw"); -const vk = @import("vulkan"); - -const c = @import("const.zig"); -const main = @import("main.zig"); - -pub const required_instance_layers = [_][*:0]const u8{ - "VK_LAYER_KHRONOS_validation", -}; - -pub const required_device_extensions = [_][*:0]const u8{ - vk.extensions.khr_swapchain.name, -}; - -pub const vk_log_scope = .vulkan; -pub const log = std.log.scoped(vk_log_scope); - -pub var vkb: vk.BaseWrapper = undefined; -pub var vki: vk.InstanceProxy = undefined; -pub var vkd: vk.DeviceProxy = undefined; - -pub var dum: vk.DebugUtilsMessengerEXT = undefined; -pub var surface: vk.SurfaceKHR = undefined; -pub var graphics_queue: vk.Queue = undefined; -pub var present_queue: vk.Queue = undefined; -pub var memory_properties: vk.PhysicalDeviceMemoryProperties = undefined; - -pub fn init() !void { - vkb = vk.BaseWrapper.load(glfw.getInstanceProcAddress); - - var vk_instance_extensions = blk: { - var ret: std.ArrayList([*:0]const u8) = .empty; - try ret.append(main.allocator, vk.extensions.ext_debug_utils.name); - try ret.append(main.allocator, vk.extensions.khr_portability_enumeration.name); - try ret.append(main.allocator, vk.extensions.khr_get_physical_device_properties_2.name); - - const glfw_instance_extensions = try glfw.getRequiredInstanceExtensions(); - try ret.appendSlice(main.allocator, glfw_instance_extensions); - - break :blk ret; - }; - defer vk_instance_extensions.deinit(main.allocator); - - const dum_create_info: vk.DebugUtilsMessengerCreateInfoEXT = .{ - .message_severity = .{ - .verbose_bit_ext = false, - .info_bit_ext = false, - .warning_bit_ext = true, - .error_bit_ext = true, - }, - .message_type = .{ - .general_bit_ext = true, - .validation_bit_ext = true, - .performance_bit_ext = true, - }, - .pfn_user_callback = &dumCallback, - .p_user_data = null, - }; - - const version = vk.makeApiVersion( - 0, - c.app_version.major, - c.app_version.minor, - c.app_version.patch, - ); - - const instance = vkb.createInstance(&.{ - .p_application_info = &.{ - .p_application_name = c.app_name, - .application_version = @bitCast(version), - .p_engine_name = c.app_name, - .engine_version = @bitCast(version), - .api_version = @bitCast(vk.API_VERSION_1_2), - }, - .enabled_layer_count = required_instance_layers.len, - .pp_enabled_layer_names = &required_instance_layers, - .enabled_extension_count = @intCast(vk_instance_extensions.items.len), - .pp_enabled_extension_names = vk_instance_extensions.items.ptr, - .flags = .{ .enumerate_portability_bit_khr = true }, - .p_next = &dum_create_info, - }, null) catch |err| { - std.log.err("Could not create Vulkan Instance", .{}); - return err; - }; - - const instance_wrapper_ptr = try main.allocator.create(vk.InstanceWrapper); - errdefer main.allocator.destroy(instance_wrapper_ptr); - - instance_wrapper_ptr.* = vk.InstanceWrapper.load(instance, vkb.dispatch.vkGetInstanceProcAddr.?); - vki = vk.InstanceProxy.init(instance, instance_wrapper_ptr); - errdefer vki.destroyInstance(null); - - dum = try vki.createDebugUtilsMessengerEXT(&dum_create_info, null); - errdefer vki.destroyDebugUtilsMessengerEXT(dum, null); - - try glfw.createWindowSurface(instance, main.window, null, &surface); - errdefer vki.destroySurfaceKHR(surface, null); - - const physical_devices = try vki.enumeratePhysicalDevicesAlloc(main.allocator); - defer main.allocator.free(physical_devices); - - const physical_device_data = physical_device_blk: { - loop: for (physical_devices) |physical_device_candidate| { - const device_properties = vki.getPhysicalDeviceProperties(physical_device_candidate); - const device_name = std.mem.sliceTo(&device_properties.device_name, 0); - - const device_extensions = try vki.enumerateDeviceExtensionPropertiesAlloc(physical_device_candidate, null, main.allocator); - defer main.allocator.free(device_extensions); - - for (required_device_extensions) |a| { - const a_name = std.mem.span(a); - for (device_extensions) |b| { - const b_name = std.mem.sliceTo(&b.extension_name, 0); - if (std.mem.eql(u8, a_name, b_name)) { - break; - } - } else { - std.log.debug("Device \"{s}\" does not support Vulkan Device Extension \"{s}\" and is not suitable for this application", .{ device_name, a_name }); - continue :loop; - } - } - - var format_count: u32 = undefined; - _ = try vki.getPhysicalDeviceSurfaceFormatsKHR(physical_device_candidate, surface, &format_count, null); - if (format_count == 0) { - std.log.debug("Device \"{s}\" has zero Vulkan Surface formats and is not suitable for this application", .{device_name}); - continue :loop; - } - - var present_mode_count: u32 = undefined; - _ = try vki.getPhysicalDeviceSurfacePresentModesKHR(physical_device_candidate, surface, &present_mode_count, null); - if (present_mode_count == 0) { - std.log.debug("Device \"{s}\" has zero Vulkan Present Modes and is not suitable for this application", .{device_name}); - continue :loop; - } - - const device_queue_families = try vki.getPhysicalDeviceQueueFamilyPropertiesAlloc(physical_device_candidate, main.allocator); - defer main.allocator.free(device_queue_families); - - const graphics_family_id = blk: { - var family_id: u32 = 0; - while (family_id < device_queue_families.len) : (family_id += 1) { - const family = device_queue_families[family_id]; - if (family.queue_flags.graphics_bit) { - break :blk family_id; - } - } - - std.log.debug("Device \"{s}\" has no Vulkan Queue Family with graphics flag and is not suitable for this application", .{device_name}); - continue :loop; - }; - - const present_family_id = blk: { - var family_id: u32 = 0; - while (family_id < device_queue_families.len) : (family_id += 1) { - if (try vki.getPhysicalDeviceSurfaceSupportKHR(physical_device_candidate, family_id, surface) == .true) { - break :blk family_id; - } - } - - std.log.debug("Device \"{s}\" has no Vulkan Queue Family with Vulkan Surface Support and is not suitable for this application", .{device_name}); - continue :loop; - }; - - break :physical_device_blk .{ - .handle = physical_device_candidate, - .name = device_name, - .properties = device_properties, - .graphics_family_id = graphics_family_id, - .present_family_id = present_family_id, - }; - } - - std.log.err("Could not find suitable Vulkan Physical Device", .{}); - return error.NoSuitableDevice; - }; - - std.log.debug("Picked Vulkan Physical Device \"{s}\"", .{physical_device_data.name}); - - const device = try vki.createDevice(physical_device_data.handle, &.{ - .queue_create_info_count = if (physical_device_data.graphics_family_id == physical_device_data.present_family_id) 1 else 2, - .p_queue_create_infos = &.{ - .{ .queue_family_index = physical_device_data.graphics_family_id, .queue_count = 1, .p_queue_priorities = &.{1.0} }, - .{ .queue_family_index = physical_device_data.present_family_id, .queue_count = 1, .p_queue_priorities = &.{1.0} }, - }, - .enabled_extension_count = required_device_extensions.len, - .pp_enabled_extension_names = &required_device_extensions, - }, null); - - const device_wrapper_ptr = try main.allocator.create(vk.DeviceWrapper); - errdefer main.allocator.destroy(device_wrapper_ptr); - - device_wrapper_ptr.* = vk.DeviceWrapper.load(device, vki.wrapper.dispatch.vkGetDeviceProcAddr.?); - vkd = vk.DeviceProxy.init(device, device_wrapper_ptr); - errdefer vkd.destroyDevice(null); - - graphics_queue = vkd.getDeviceQueue(physical_device_data.graphics_family_id, 0); - present_queue = vkd.getDeviceQueue(physical_device_data.present_family_id, 0); - - memory_properties = vki.getPhysicalDeviceMemoryProperties(physical_device_data.handle); - - const framebuffer_width, const framebuffer_height = blk: { - const w, const h = main.window.getFramebufferSize(); - break :blk [_]u32{ @intCast(w), @intCast(h) }; - }; - - const vk_extent: vk.Extent2D = .{ - .width = framebuffer_width, - .height = framebuffer_height, - }; - - _ = vk_extent; -} - -pub fn deinit() void { - vkd.destroyDevice(null); - vki.destroySurfaceKHR(surface, null); - vki.destroyDebugUtilsMessengerEXT(dum, null); - vki.destroyInstance(null); - - main.allocator.destroy(vki.wrapper); - main.allocator.destroy(vkd.wrapper); -} - -fn dumCallback( - severity: vk.DebugUtilsMessageSeverityFlagsEXT, - message_type: vk.DebugUtilsMessageTypeFlagsEXT, - maybe_callback_data: ?*const vk.DebugUtilsMessengerCallbackDataEXT, - _: ?*anyopaque, -) callconv(.c) vk.Bool32 { - const message = if (maybe_callback_data) |callback_data| callback_data.p_message orelse return .false else return .false; - const type_name = if (message_type.general_bit_ext) "general" else if (message_type.validation_bit_ext) "validation" else if (message_type.performance_bit_ext) "performance" else if (message_type.device_address_binding_bit_ext) "device_address_binding" else "unknown"; - - const format = "[{s}] {s}"; - const args = .{ type_name, message }; - - if (severity.error_bit_ext) { - log.err(format, args); - } else if (severity.warning_bit_ext) { - log.warn(format, args); - } else if (severity.info_bit_ext) { - log.info(format, args); - } else if (severity.verbose_bit_ext) { - log.debug(format, args); - } - - return .false; -} diff --git a/src/engine/Engine.zig b/src/engine/Engine.zig new file mode 100644 index 0000000..8df2d90 --- /dev/null +++ b/src/engine/Engine.zig @@ -0,0 +1,463 @@ +const Engine = @This(); +const std = @import("std"); + +const c = @import("../const.zig"); +const glfw = @import("zglfw"); +const vk = @import("vulkan"); + +const Queue = @import("Queue.zig"); +const VkAllocator = @import("VkAllocator.zig"); + +pub const Mode = union(enum) { + headless: void, + surface: struct { + window: *glfw.Window, + surface: vk.SurfaceKHR, + presentation_queue: Queue, + }, + + pub fn deinit(self: *Mode, instance: vk.InstanceProxy, vk_allocator: *VkAllocator) void { + switch (self.*) { + .headless => {}, + .surface => |x| { + instance.destroySurfaceKHR(x.surface, &vk_allocator.interface); + }, + } + + self.* = undefined; + } +}; + +pub const ModeTag = std.meta.Tag(Mode); + +const QueueAllocator = struct { + queue_families_properties_buffer: [max_queue_families]vk.QueueFamilyProperties, + queue_families_in_use: [max_queue_families]u32, + + queue_families_count: u32, + + const max_queue_families = 16; + + pub fn init(instance: vk.InstanceProxy, physical_device: vk.PhysicalDevice) QueueAllocator { + var queue_families_properties_buffer: [max_queue_families]vk.QueueFamilyProperties = undefined; + var queue_families_count: u32 = max_queue_families; + + instance.getPhysicalDeviceQueueFamilyProperties(physical_device, &queue_families_count, &queue_families_properties_buffer); + + var queue_families_in_use: [max_queue_families]u32 = undefined; + @memset(queue_families_in_use[0..queue_families_count], 0); + + return .{ + .queue_families_properties_buffer = queue_families_properties_buffer, + .queue_families_in_use = queue_families_in_use, + .queue_families_count = queue_families_count, + }; + } + + pub fn allocateQueueIndex(self: *QueueAllocator, queue_family: u32) u32 { + std.debug.assert(queue_family < self.queue_families_count); + if (self.queue_families_in_use[queue_family] < self.queue_families_properties_buffer[queue_family].queue_count) { + const queue_index = self.queue_families_in_use[queue_family]; + self.queue_families_in_use[queue_family] += 1; + return queue_index; + } else { + return 0; + } + } + + pub fn queueFamiliesProperties(self: *const QueueAllocator) []const vk.QueueFamilyProperties { + return self.queue_families_properties_buffer[0..self.queue_families_count]; + } + + pub fn queueInUse(self: *const QueueAllocator) []const u32 { + return self.queue_families_in_use[0..self.queue_families_count]; + } +}; + +const QueueAllocations = struct { + graphics_queue: Queue.Allocation, + compute_queue: Queue.Allocation, + transfer_queue: Queue.Allocation, + /// Defined only in `surface` mode, `undefined` otherwise. + presentation_queue: Queue.Allocation, +}; + +const DebugUtilsMessenger = if (debug) vk.DebugUtilsMessengerEXT else void; + +mode: Mode, +vk_allocator: *VkAllocator, + +base: vk.BaseWrapper, +instance: vk.InstanceProxy, +device: vk.DeviceProxy, + +debug_utils_messenger: DebugUtilsMessenger, +physical_device: vk.PhysicalDevice, +memory_types: std.ArrayList(vk.MemoryType), +memory_heaps: std.ArrayList(vk.MemoryHeap), + +graphics_queue: Queue, +compute_queue: Queue, +transfer_queue: Queue, + +const debug = @import("builtin").mode == .Debug; + +const vk_application_info: vk.ApplicationInfo = .{ + .p_application_name = c.app_name, + .application_version = @bitCast(vk_version), + .p_engine_name = c.app_name, + .engine_version = @bitCast(vk_version), + .api_version = @bitCast(vk.API_VERSION_1_2), +}; + +const vk_debug_utils_messenger_create_info: vk.DebugUtilsMessengerCreateInfoEXT = .{ + .message_severity = .{ + .verbose_bit_ext = false, + .info_bit_ext = false, + .warning_bit_ext = true, + .error_bit_ext = true, + }, + .message_type = .{ + .general_bit_ext = true, + .validation_bit_ext = true, + .performance_bit_ext = true, + }, + .pfn_user_callback = &debugUtilsMessengerCallback, + .p_user_data = null, +}; + +const vk_version: vk.Version = vk.makeApiVersion( + 0, + c.app_version.major, + c.app_version.minor, + c.app_version.patch, +); + +pub fn init(allocator: std.mem.Allocator, maybe_window: ?*glfw.Window) !Engine { + const vk_allocator = try VkAllocator.init(allocator); + errdefer vk_allocator.deinit(); + + // --- LOAD BASE DISPATCH -------------------------------------------------- + + const base = vk.BaseWrapper.load(glfw.getInstanceProcAddress); + + // --- CREATE INSTANCE, ITS WRAPPER AND PROXY ------------------------------ + + const instance_handle = blk: { + var enabled_extensions_buffer: [16][*:0]const u8 = undefined; + var enabled_extensions: std.ArrayList([*:0]const u8) = .initBuffer(&enabled_extensions_buffer); + + if (debug) { + try enabled_extensions.appendBounded(vk.extensions.ext_debug_utils.name); + } + + if (maybe_window != null) { + const glfw_instance_extensions = try glfw.getRequiredInstanceExtensions(); + try enabled_extensions.appendSliceBounded(glfw_instance_extensions); + } + + const enabled_layers: []const [*:0]const u8 = if (debug) &.{"VK_LAYER_KHRONOS_validation"} else &.{}; + + break :blk try base.createInstance(&.{ + .p_application_info = &vk_application_info, + .enabled_layer_count = enabled_layers.len, + .pp_enabled_layer_names = enabled_layers.ptr, + .enabled_extension_count = @intCast(enabled_extensions.items.len), + .pp_enabled_extension_names = enabled_extensions.items.ptr, + .p_next = if (debug) &vk_debug_utils_messenger_create_info else null, + }, &vk_allocator.interface); + }; + + const instance_wrapper_ptr = try allocator.create(vk.InstanceWrapper); + errdefer allocator.destroy(instance_wrapper_ptr); + + instance_wrapper_ptr.* = .load(instance_handle, base.dispatch.vkGetInstanceProcAddr.?); + const instance = vk.InstanceProxy.init(instance_handle, instance_wrapper_ptr); + errdefer instance.destroyInstance(&vk_allocator.interface); + + // --- CREATE DEBUG UTILS MESSENGER ---------------------------------------- + + const debug_utils_messenger: DebugUtilsMessenger = if (debug) try instance.createDebugUtilsMessengerEXT(&vk_debug_utils_messenger_create_info, &vk_allocator.interface) else {}; + errdefer { + if (debug) instance.destroyDebugUtilsMessengerEXT(debug_utils_messenger, &vk_allocator.interface); + } + + // --- CHOOSE PHYSICAL DEVICE, GET ITS MEMORY PROPERTIES ------------------- + + const physical_device = blk: { + var physical_devices_buffer: [16]vk.PhysicalDevice = undefined; + var physical_devices_count: u32 = physical_devices_buffer.len; + _ = try instance.enumeratePhysicalDevices(&physical_devices_count, &physical_devices_buffer); + + // Look for the first GPU with its `device_type` equal to + // `.discrete_gpu`. + + for (physical_devices_buffer[0..physical_devices_count]) |physical_device_candidate| { + const physical_device_properties = instance.getPhysicalDeviceProperties(physical_device_candidate); + if (physical_device_properties.device_type == .discrete_gpu) { + break :blk physical_device_candidate; + } + } + + // Look for the first GPU. + + if (physical_devices_count > 0) { + break :blk physical_devices_buffer[0]; + } + + return error.NoDevice; + }; + + const physical_device_memory_properties = instance.getPhysicalDeviceMemoryProperties(physical_device); + + var memory_types = try std.ArrayList(vk.MemoryType).initCapacity(allocator, vk.MAX_MEMORY_TYPES); + errdefer memory_types.deinit(allocator); + memory_types.appendSliceAssumeCapacity(physical_device_memory_properties.memory_types[0..physical_device_memory_properties.memory_type_count]); + + var memory_heaps = try std.ArrayList(vk.MemoryHeap).initCapacity(allocator, vk.MAX_MEMORY_HEAPS); + errdefer memory_heaps.deinit(allocator); + memory_heaps.appendSliceAssumeCapacity(physical_device_memory_properties.memory_heaps[0..physical_device_memory_properties.memory_heap_count]); + + // --- CREATE SURFACE ------------------------------------------------------ + + var surface: vk.SurfaceKHR = undefined; + if (maybe_window) |window| try glfw.createWindowSurface(instance_handle, window, &vk_allocator.interface, &surface); + errdefer { + if (maybe_window != null) instance.destroySurfaceKHR(surface, &vk_allocator.interface); + } + + // --- ALLOCATE QUEUES ----------------------------------------------------- + + const max_queues = 4; + + var queue_create_info_buffer: [max_queues]vk.DeviceQueueCreateInfo = undefined; + var queue_create_info: std.ArrayList(vk.DeviceQueueCreateInfo) = .initBuffer(&queue_create_info_buffer); + + const queue_allocations: QueueAllocations = blk: { + var queue_allocator: QueueAllocator = .init(instance, physical_device); + const queue_families_properties = queue_allocator.queueFamiliesProperties(); + + const graphics_queue_family = findGraphicsQueueFamily(queue_families_properties) orelse return error.NoGraphicsQueue; + const compute_queue_family = findComputeQueueFamily(queue_families_properties) orelse return error.NoComputeQueue; + const transfer_queue_family = findTransferQueueFamily(queue_families_properties) orelse return error.NoTransferQueue; + const presentation_queue_family = if (maybe_window != null) (try findPresentationQueueFamily(queue_families_properties, instance, physical_device, surface) orelse return error.NoPresentationQueue) else undefined; + + const graphics_queue_index = queue_allocator.allocateQueueIndex(graphics_queue_family); + const compute_queue_index = queue_allocator.allocateQueueIndex(compute_queue_family); + const transfer_queue_index = queue_allocator.allocateQueueIndex(transfer_queue_family); + const presentation_queue_index = if (maybe_window != null) queue_allocator.allocateQueueIndex(presentation_queue_family) else undefined; + + const queue_priorities = [_]f32{1.0} ** max_queues; + + for (queue_allocator.queueInUse(), 0..) |count, queue_family| { + if (count == 0) continue; + + queue_create_info.appendBounded(.{ + .queue_family_index = @intCast(queue_family), + .queue_count = count, + .p_queue_priorities = &queue_priorities, + }) catch unreachable; + } + + break :blk .{ + .graphics_queue = .{ .family = graphics_queue_family, .index = graphics_queue_index }, + .compute_queue = .{ .family = compute_queue_family, .index = compute_queue_index }, + .transfer_queue = .{ .family = transfer_queue_family, .index = transfer_queue_index }, + .presentation_queue = if (maybe_window != null) .{ .family = presentation_queue_family, .index = presentation_queue_index } else undefined, + }; + }; + + // --- CREATE (LOGICAL) DEVICE, ITS WRAPPER AND PROXY ---------------------- + + const device_handle = blk: { + var enabled_extensions_buffer: [16][*:0]const u8 = undefined; + var enabled_extensions: std.ArrayList([*:0]const u8) = .initBuffer(&enabled_extensions_buffer); + + if (maybe_window != null) { + try enabled_extensions.appendBounded(vk.extensions.khr_swapchain.name); + } + + break :blk try instance.createDevice(physical_device, &.{ + .queue_create_info_count = @intCast(queue_create_info.items.len), + .p_queue_create_infos = queue_create_info.items.ptr, + .enabled_extension_count = @intCast(enabled_extensions.items.len), + .pp_enabled_extension_names = enabled_extensions.items.ptr, + }, &vk_allocator.interface); + }; + + const device_wrapper_ptr = try allocator.create(vk.DeviceWrapper); + errdefer allocator.destroy(device_wrapper_ptr); + + device_wrapper_ptr.* = .load(device_handle, instance.wrapper.dispatch.vkGetDeviceProcAddr.?); + const device = vk.DeviceProxy.init(device_handle, device_wrapper_ptr); + errdefer device.destroyDevice(vk_allocator.interface); + + // ------------------------------------------------------------------------- + + return .{ + .mode = if (maybe_window) |window| .{ .surface = .{ + .window = window, + .surface = surface, + .presentation_queue = .initAllocation(device, queue_allocations.presentation_queue), + } } else .{ .headless = {} }, + .vk_allocator = vk_allocator, + + .base = base, + .instance = instance, + .device = device, + + .debug_utils_messenger = debug_utils_messenger, + .physical_device = physical_device, + .memory_types = memory_types, + .memory_heaps = memory_heaps, + + .graphics_queue = .initAllocation(device, queue_allocations.graphics_queue), + .compute_queue = .initAllocation(device, queue_allocations.compute_queue), + .transfer_queue = .initAllocation(device, queue_allocations.transfer_queue), + }; +} + +pub fn deinit(self: *Engine) void { + const allocator = self.vk_allocator.allocator; + + self.device.destroyDevice(&self.vk_allocator.interface); + allocator.destroy(self.device.wrapper); + + if (std.meta.activeTag(self.mode) == .surface) { + self.instance.destroySurfaceKHR(self.mode.surface.surface, &self.vk_allocator.interface); + } + + self.memory_heaps.deinit(allocator); + self.memory_types.deinit(allocator); + + if (debug) self.instance.destroyDebugUtilsMessengerEXT(self.debug_utils_messenger, &self.vk_allocator.interface); + + self.instance.destroyInstance(&self.vk_allocator.interface); + allocator.destroy(self.instance.wrapper); + + self.vk_allocator.deinit(); + + self.* = undefined; +} + +pub fn allocate(self: *const Engine, memory_requirements: vk.MemoryRequirements, memory_property_flags: vk.MemoryPropertyFlags) !vk.DeviceMemory { + for (self.memory_types.items, 0..) |memory_type, i| { + const is_type = memory_requirements.memory_type_bits & (@as(u32, 1) << @truncate(i)) != 0; + const has_flags = memory_type.property_flags.contains(memory_property_flags); + if (is_type and has_flags) { + return try self.device.allocateMemory(&.{ + .allocation_size = memory_requirements.size, + .memory_type_index = @truncate(i), + }); + } + } + + return error.NoSuitableMemoryType; +} + +fn debugUtilsMessengerCallback( + severity: vk.DebugUtilsMessageSeverityFlagsEXT, + message_type: vk.DebugUtilsMessageTypeFlagsEXT, + maybe_callback_data: ?*const vk.DebugUtilsMessengerCallbackDataEXT, + _: ?*anyopaque, +) callconv(vk.vulkan_call_conv) vk.Bool32 { + const log = std.log.scoped(.vulkan); + + const message = if (maybe_callback_data) |callback_data| callback_data.p_message orelse return .false else return .false; + const type_name = if (message_type.general_bit_ext) "general" else if (message_type.validation_bit_ext) "validation" else if (message_type.performance_bit_ext) "performance" else if (message_type.device_address_binding_bit_ext) "device_address_binding" else "unknown"; + + const format = "[{s}] {s}"; + const args = .{ type_name, message }; + + if (severity.error_bit_ext) { + log.err(format, args); + } else if (severity.warning_bit_ext) { + log.warn(format, args); + } else if (severity.info_bit_ext) { + log.info(format, args); + } else if (severity.verbose_bit_ext) { + log.debug(format, args); + } + + return .false; +} + +fn findGraphicsQueueFamily(queue_families_properties: []const vk.QueueFamilyProperties) ?u32 { + // Look for the first family that has the `graphics_bit` set. + + for (queue_families_properties, 0..) |queue_family_properties, i| { + const graphics_bit = queue_family_properties.queue_flags.graphics_bit; + if (graphics_bit) { + return @intCast(i); + } + } + + return null; +} + +fn findComputeQueueFamily(queue_families_properties: []const vk.QueueFamilyProperties) ?u32 { + // Look for the first family that has the `compute_bit` set, but not + // `graphics_bit`. + + for (queue_families_properties, 0..) |queue_family_properties, i| { + const compute_bit = queue_family_properties.queue_flags.compute_bit; + const graphics_bit = queue_family_properties.queue_flags.graphics_bit; + if (compute_bit and !graphics_bit) { + return @intCast(i); + } + } + + // Look for the first family that has the `compute_bit` set. + + for (queue_families_properties, 0..) |queue_family_properties, i| { + const compute_bit = queue_family_properties.queue_flags.compute_bit; + if (compute_bit) { + return @intCast(i); + } + } + + return null; +} + +fn findTransferQueueFamily(queue_families_properties: []const vk.QueueFamilyProperties) ?u32 { + // Look for the first family that has the `transfer_bit` set, but neither + // `graphics_bit` or `compute_bit`. + + for (queue_families_properties, 0..) |queue_family_properties, i| { + const transfer_bit = queue_family_properties.queue_flags.transfer_bit; + const graphics_bit = queue_family_properties.queue_flags.graphics_bit; + const compute_bit = queue_family_properties.queue_flags.compute_bit; + if (transfer_bit and !graphics_bit and !compute_bit) { + return @intCast(i); + } + } + + // Look for the first family that has either `transfer_bit`, `graphics_bit` + // or `compute_bit` set. + + // NOTE `graphics_bit` or `compute_bit` imply `transfer_bit`, even if not + // set explicitly. + + for (queue_families_properties, 0..) |queue_family_properties, i| { + const transfer_bit = queue_family_properties.queue_flags.transfer_bit; + const graphics_bit = queue_family_properties.queue_flags.graphics_bit; + const compute_bit = queue_family_properties.queue_flags.compute_bit; + if (transfer_bit or graphics_bit or compute_bit) { + return @intCast(i); + } + } + + return null; +} + +fn findPresentationQueueFamily(queue_families_properties: []const vk.QueueFamilyProperties, instance: vk.InstanceProxy, physical_device: vk.PhysicalDevice, surface: vk.SurfaceKHR) !?u32 { + // Look for the first family that supports presentation to a given surface. + + for (queue_families_properties, 0..) |_, i| { + if (try instance.getPhysicalDeviceSurfaceSupportKHR(physical_device, @intCast(i), surface) == .true) { + return @intCast(i); + } + } + + return null; +} diff --git a/src/engine/Queue.zig b/src/engine/Queue.zig new file mode 100644 index 0000000..7d5b36a --- /dev/null +++ b/src/engine/Queue.zig @@ -0,0 +1,19 @@ +const Queue = @This(); +const std = @import("std"); + +const vk = @import("vulkan"); + +pub const Allocation = struct { + family: u32, + index: u32, +}; + +handle: vk.Queue, +allocation: Allocation, + +pub fn initAllocation(device: vk.DeviceProxy, allocation: Allocation) Queue { + return .{ + .handle = device.getDeviceQueue(allocation.family, allocation.index), + .allocation = allocation, + }; +} diff --git a/src/engine/Swapchain.zig b/src/engine/Swapchain.zig new file mode 100644 index 0000000..de21028 --- /dev/null +++ b/src/engine/Swapchain.zig @@ -0,0 +1,319 @@ +const Swapchain = @This(); +const std = @import("std"); + +const vk = @import("vulkan"); + +const Engine = @import("Engine.zig"); + +engine: *Engine, +params: Params, +render_pass: vk.RenderPass, +swapchain: vk.SwapchainKHR = .null_handle, +swapchain_images: []SwapchainImage = &.{}, + +pub fn init(engine: *Engine) !Swapchain { + const params: Params = try .init(engine); + + const render_pass = blk: { + const attachments = [_]vk.AttachmentDescription{ + .{ + .format = params.surface_format.format, + .samples = .{ .@"1_bit" = true }, + .load_op = .dont_care, + .store_op = .store, + .stencil_load_op = .dont_care, + .stencil_store_op = .dont_care, + .initial_layout = .undefined, + .final_layout = .present_src_khr, + }, + }; + + const color_attachments = [_]vk.AttachmentReference{ + .{ + .attachment = 0, + .layout = .color_attachment_optimal, + }, + }; + + const subpasses = [_]vk.SubpassDescription{ + .{ + .pipeline_bind_point = .graphics, + .color_attachment_count = color_attachments.len, + .p_color_attachments = &color_attachments, + }, + }; + + break :blk try engine.device.createRenderPass(&.{ + .attachment_count = attachments.len, + .p_attachments = &attachments, + .subpass_count = subpasses.len, + .p_subpasses = &subpasses, + }, &engine.vk_allocator.interface); + }; + errdefer engine.device.destroyRenderPass(render_pass, &engine.vk_allocator.interface); + + var swapchain: Swapchain = .{ + .engine = engine, + .params = params, + .render_pass = render_pass, + }; + + try recreate(&swapchain); + return swapchain; +} + +pub fn deinit(self: *Swapchain) void { + const allocator = self.engine.vk_allocator.allocator; + + for (self.swapchain_images) |swapchain_image| { + swapchain_image.deinit(self.engine); + } + allocator.free(self.swapchain_images); + + if (self.swapchain != .null_handle) { + self.engine.device.destroySwapchainKHR(self.swapchain, &self.engine.vk_allocator.interface); + } + + self.engine.device.destroyRenderPass(self.render_pass, &self.engine.vk_allocator.interface); + + self.* = undefined; +} + +pub fn recreate(self: *Swapchain) !void { + const mode = &self.engine.mode.surface; + const allocator = self.engine.vk_allocator.allocator; + + const old_swapchain = self.swapchain; + const old_swapchain_images = self.swapchain_images; + + const extent = try getCurrentExtent(self.engine); + + // --- CREATE NEW SWAPCHAIN ------------------------------------------------ + + const surface_capabilities = try self.engine.instance.getPhysicalDeviceSurfaceCapabilitiesKHR(self.engine.physical_device, mode.surface); + + const graphics_queue_family = self.engine.graphics_queue.allocation.family; + const presentation_queue_family = mode.presentation_queue.allocation.family; + + const single_queue_family = graphics_queue_family == presentation_queue_family; + const queue_family_indices: []const u32 = if (single_queue_family) &.{} else &.{ graphics_queue_family, presentation_queue_family }; + + const new_swapchain = try self.engine.device.createSwapchainKHR(&.{ + .surface = mode.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 }, + .image_sharing_mode = if (single_queue_family) .exclusive else .concurrent, + .queue_family_index_count = @intCast(queue_family_indices.len), + .p_queue_family_indices = queue_family_indices.ptr, + .pre_transform = surface_capabilities.current_transform, + .composite_alpha = .{ .opaque_bit_khr = true }, + .present_mode = .fifo_khr, + .clipped = .true, + .old_swapchain = old_swapchain, + }, &self.engine.vk_allocator.interface); + errdefer self.engine.device.destroySwapchainKHR(new_swapchain, &self.engine.vk_allocator.interface); + + // --- 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(self.engine); + } + allocator.free(self.swapchain_images); + self.swapchain_images = &.{}; + + if (old_swapchain != .null_handle) { + self.engine.device.destroySwapchainKHR(old_swapchain, &self.engine.vk_allocator.interface); + self.swapchain = .null_handle; + } + + // --- CREATE NEW SWAPCHAIN IMAGES ----------------------------------------- + + const new_swapchain_images = blk: { + const images = try self.engine.device.getSwapchainImagesAllocKHR(new_swapchain, allocator); + defer allocator.free(images); + + var swapchain_images: std.ArrayList(SwapchainImage) = try .initCapacity(allocator, images.len); + errdefer swapchain_images.deinit(allocator); + + errdefer for (swapchain_images.items) |x| x.deinit(self.engine); + + for (images) |image| { + swapchain_images.appendAssumeCapacity(try SwapchainImage.init(self.engine, .{ + .image = image, + .format = self.params.surface_format.format, + .render_pass = self.render_pass, + .extent = extent, + })); + } + + break :blk try swapchain_images.toOwnedSlice(allocator); + }; + errdefer { + for (new_swapchain_images) |swapchain_image| { + swapchain_image.deinit(self.engine); + } + allocator.free(new_swapchain_images); + } + + // --- COMMIT -------------------------------------------------------------- + + self.swapchain = new_swapchain; + self.swapchain_images = new_swapchain_images; +} + +fn getCurrentExtent(self: *Engine) !vk.Extent2D { + const mode = &self.mode.surface; + const surface_capabilities = try self.instance.getPhysicalDeviceSurfaceCapabilitiesKHR(self.physical_device, mode.surface); + + 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 = mode.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(engine: *Engine) !Params { + const mode = &engine.mode.surface; + const allocator = engine.vk_allocator.allocator; + + const surface_capabilities = try engine.instance.getPhysicalDeviceSurfaceCapabilitiesKHR(engine.physical_device, mode.surface); + + const surface_format = blk: { + const surface_formats = try engine.instance.getPhysicalDeviceSurfaceFormatsAllocKHR(engine.physical_device, mode.surface, allocator); + defer allocator.free(surface_formats); + + // 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), + ); + + 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, + fence: vk.Fence, + framebuffer: vk.Framebuffer, + + const InitProps = struct { + image: vk.Image, + format: vk.Format, + render_pass: vk.RenderPass, + extent: vk.Extent2D, + }; + + fn init(engine: *Engine, props: InitProps) !SwapchainImage { + const image_view = try engine.device.createImageView(&.{ + .image = props.image, + .view_type = .@"2d", + .format = props.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, + }, + }, &engine.vk_allocator.interface); + errdefer engine.device.destroyImageView(image_view, &engine.vk_allocator.interface); + + const semaphore_image_acquired = try engine.device.createSemaphore(&.{}, &engine.vk_allocator.interface); + errdefer engine.device.destroySemaphore(semaphore_image_acquired, &engine.vk_allocator.interface); + + const semaphore_render_finished = try engine.device.createSemaphore(&.{}, &engine.vk_allocator.interface); + errdefer engine.device.destroySemaphore(semaphore_render_finished, &engine.vk_allocator.interface); + + const fence = try engine.device.createFence(&.{ + .flags = .{ .signaled_bit = true }, + }, &engine.vk_allocator.interface); + errdefer engine.device.destroyFence(fence, &engine.vk_allocator.interface); + + const attachments = [_]vk.ImageView{image_view}; + + const framebuffer = try engine.device.createFramebuffer(&.{ + .render_pass = props.render_pass, + .attachment_count = attachments.len, + .p_attachments = &attachments, + .width = props.extent.width, + .height = props.extent.height, + .layers = 1, + }, &engine.vk_allocator.interface); + errdefer engine.device.destroyFramebuffer(framebuffer, &engine.vk_allocator.interface); + + return .{ + .image = props.image, + .image_view = image_view, + .semaphore_image_acquired = semaphore_image_acquired, + .semaphore_render_finished = semaphore_render_finished, + .fence = fence, + .framebuffer = framebuffer, + }; + } + + fn deinit(self: SwapchainImage, engine: *Engine) void { + self.waitForFence(engine) catch return; + engine.device.destroyFramebuffer(self.framebuffer, &engine.vk_allocator.interface); + engine.device.destroyFence(self.fence, &engine.vk_allocator.interface); + engine.device.destroySemaphore(self.semaphore_render_finished, &engine.vk_allocator.interface); + engine.device.destroySemaphore(self.semaphore_image_acquired, &engine.vk_allocator.interface); + engine.device.destroyImageView(self.image_view, &engine.vk_allocator.interface); + } + + fn waitForFence(self: SwapchainImage, engine: *Engine) !void { + const fences = [_]vk.Fence{self.fence}; + _ = try engine.device.waitForFences(fences.len, &fences, .true, std.math.maxInt(u64)); + } +}; diff --git a/src/engine/VkAllocator.zig b/src/engine/VkAllocator.zig new file mode 100644 index 0000000..d6abeea --- /dev/null +++ b/src/engine/VkAllocator.zig @@ -0,0 +1,119 @@ +const VkAllocator = @This(); +const std = @import("std"); + +const vk = @import("vulkan"); + +allocator: std.mem.Allocator, +allocations: std.AutoHashMapUnmanaged(?*anyopaque, usize) = .empty, +mutex: std.Thread.Mutex = .{}, + +interface: vk.AllocationCallbacks, + +// Allocation in Zig's standard library relies heavily on the alignment being +// comptime-known, but we only know the alignment at runtime. We could work +// around it, but it would add much complexity. Instead, we allocate everything +// with the same, relatively big alignment and hope for the best. +const actual_alignment: std.mem.Alignment = .@"16"; + +pub fn init(allocator: std.mem.Allocator) !*VkAllocator { + // NOTE We allocate the structure to pin its address. + const self = try allocator.create(VkAllocator); + + self.* = .{ + .allocator = allocator, + .interface = .{ + .p_user_data = self, + .pfn_allocation = &allocationFunction, + .pfn_reallocation = &reallocationFunction, + .pfn_free = &freeFunction, + }, + }; + return self; +} + +pub fn deinit(self: *VkAllocator) void { + const allocator = self.allocator; + + self.allocations.deinit(allocator); + + self.* = undefined; + allocator.destroy(self); +} + +fn allocationFunction( + p_user_data: ?*anyopaque, + size: usize, + alignment: usize, + _: vk.SystemAllocationScope, +) callconv(vk.vulkan_call_conv) ?*anyopaque { + const self: *VkAllocator = @ptrCast(@alignCast(p_user_data.?)); + + const desired_alignment: std.mem.Alignment = .fromByteUnits(alignment); + std.debug.assert(std.mem.Alignment.compare(actual_alignment, .gte, desired_alignment)); + + self.mutex.lock(); + defer self.mutex.unlock(); + + self.allocations.ensureUnusedCapacity(self.allocator, 1) catch return null; + const memory = self.allocator.alignedAlloc(u8, actual_alignment, size) catch return null; + + self.allocations.putAssumeCapacity(memory.ptr, size); + + return memory.ptr; +} + +fn reallocationFunction( + p_user_data: ?*anyopaque, + maybe_p_original: ?*anyopaque, + size: usize, + alignment: usize, + _: vk.SystemAllocationScope, +) callconv(vk.vulkan_call_conv) ?*anyopaque { + const self: *VkAllocator = @ptrCast(@alignCast(p_user_data.?)); + + const desired_alignment: std.mem.Alignment = .fromByteUnits(alignment); + std.debug.assert(std.mem.Alignment.compare(actual_alignment, .gte, desired_alignment)); + + self.mutex.lock(); + defer self.mutex.unlock(); + + // NOTE If we were pedantic, we would consider the fact that we might not + // need unused capacity if the memory doesn't get relocated. + self.allocations.ensureUnusedCapacity(self.allocator, 1) catch return null; + + const old_memory = if (maybe_p_original) |p_original| blk_then: { + const old_size = self.allocations.get(p_original).?; + break :blk_then @as([*]align(actual_alignment.toByteUnits()) u8, @ptrCast(@alignCast(p_original)))[0..old_size]; + } else blk_else: { + break :blk_else @as([]align(actual_alignment.toByteUnits()) u8, &.{}); + }; + + const memory = self.allocator.realloc(old_memory, size) catch return null; + std.debug.assert(std.mem.isAligned(@intFromPtr(memory.ptr), alignment)); + + if (maybe_p_original) |p_original| { + const removed = self.allocations.remove(p_original); + std.debug.assert(removed); + } + + self.allocations.putAssumeCapacityNoClobber(memory.ptr, size); + + return memory.ptr; +} + +fn freeFunction( + p_user_data: ?*anyopaque, + maybe_p_memory: ?*anyopaque, +) callconv(vk.vulkan_call_conv) void { + const self: *VkAllocator = @ptrCast(@alignCast(p_user_data.?)); + + if (maybe_p_memory) |p_memory| { + self.mutex.lock(); + defer self.mutex.unlock(); + + const size = self.allocations.fetchRemove(p_memory).?.value; + const memory = @as([*]align(actual_alignment.toByteUnits()) u8, @ptrCast(@alignCast(p_memory)))[0..size]; + + self.allocator.free(memory); + } +} diff --git a/src/main.zig b/src/main.zig index 2044898..0524810 100644 --- a/src/main.zig +++ b/src/main.zig @@ -5,9 +5,11 @@ const stbi = @import("zstbi"); const vk = @import("vulkan"); const c = @import("const.zig"); -const engine = @import("engine.zig"); const game = @import("game.zig"); +const Engine = @import("engine/Engine.zig"); +const Swapchain = @import("engine/Swapchain.zig"); + pub var allocator: std.mem.Allocator = undefined; pub var temp_allocator: std.mem.Allocator = undefined; pub var window: *glfw.Window = undefined; @@ -46,18 +48,18 @@ pub fn main() !void { window.setSizeLimits(c.min_window_width, c.min_window_height, -1, -1); - engine.init() catch |err| { - std.log.err("Could not initialize engine", .{}); - return err; - }; + var engine = try Engine.init(allocator, window); defer engine.deinit(); + var swapchain = try Swapchain.init(&engine); + defer swapchain.deinit(); + //game.init(); //defer game.deinit(); while (!window.shouldClose()) { glfw.pollEvents(); //game.update(dt); - window.swapBuffers(); + std.Thread.sleep(1 * std.time.ns_per_ms); } }