engine/src/GraphicsContext.zig
2024-09-25 10:59:06 +04:00

444 lines
17 KiB
Zig

const std = @import("std");
const vk = @import("vk");
const c = @import("sdl.zig");
pub const GraphicsContext = @This();
const apis: []const vk.ApiInfo = &.{
vk.features.version_1_0,
vk.features.version_1_1,
vk.features.version_1_2,
vk.features.version_1_3,
vk.extensions.khr_surface,
vk.extensions.khr_swapchain,
};
pub const Instance = vk.InstanceProxy(apis);
pub const Device = vk.DeviceProxy(apis);
pub const CommandBuffer = vk.CommandBufferProxy(apis);
const BaseDispatch = vk.BaseWrapper(apis);
const InstanceDispatch = vk.InstanceWrapper(apis);
const DeviceDispatch = Device.Wrapper;
const device_extensions = [_][:0]const u8{
vk.extensions.khr_swapchain.name,
};
const vk_layers = [_][:0]const u8{"VK_LAYER_KHRONOS_validation"};
allocator: std.mem.Allocator = undefined,
window: *c.SDL_Window = undefined,
vkb: BaseDispatch = undefined,
vki: InstanceDispatch = undefined,
vkd: DeviceDispatch = undefined,
device_info: SelectedPhysicalDevice = undefined,
instance: Instance = undefined,
device: Device = undefined,
queues: DeviceQueues = undefined,
surface: vk.SurfaceKHR = .null_handle,
swapchain: vk.SwapchainKHR = .null_handle,
swapchain_extent: vk.Extent2D = .{ .width = 0, .height = 0 },
swapchain_images: []vk.Image = &.{},
pipeline_cache: vk.PipelineCache = .null_handle,
pub const CommandPool = struct {
device: Device,
handle: vk.CommandPool,
pub fn allocateCommandBuffer(self: *const CommandPool) !CommandBuffer {
var cmd_bufs = [_]vk.CommandBuffer{.null_handle};
try self.device.allocateCommandBuffers(&.{
.command_pool = self.handle,
.level = .primary,
.command_buffer_count = cmd_bufs.len,
}, &cmd_bufs);
return CommandBuffer.init(cmd_bufs[0], self.device.wrapper);
}
};
pub fn init(self: *GraphicsContext, allocator: std.mem.Allocator, window: *c.SDL_Window) !void {
self.allocator = allocator;
self.window = window;
var scratch: [4096]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&scratch);
const vkGetInstanceProcAddr: vk.PfnGetInstanceProcAddr = @ptrCast(c.SDL_Vulkan_GetVkGetInstanceProcAddr());
var sdl_instance_ext_count: c_uint = 0;
if (c.SDL_Vulkan_GetInstanceExtensions(window, &sdl_instance_ext_count, null) == c.SDL_FALSE) {
std.debug.print("SDL_Vulkan_GetInstanceExtensions: get count {s}\n", .{c.SDL_GetError()});
return error.GetSDLExtensions;
}
const sdl_instance_ext_names = try fba.allocator().alloc([*:0]const u8, sdl_instance_ext_count);
if (c.SDL_Vulkan_GetInstanceExtensions(window, &sdl_instance_ext_count, @ptrCast(sdl_instance_ext_names.ptr)) == c.SDL_FALSE) {
std.debug.print("SDL_Vulkan_GetInstanceExtensions: get names {s}\n", .{c.SDL_GetError()});
return error.GetSDLExtensions;
}
std.debug.print("SDL Extensions: {s}\n", .{sdl_instance_ext_names});
self.vkb = try BaseDispatch.load(vkGetInstanceProcAddr);
const instance_handle = try self.vkb.createInstance(&vk.InstanceCreateInfo{
.p_application_info = &vk.ApplicationInfo{
.api_version = vk.API_VERSION_1_3,
.application_version = 0,
.engine_version = 0,
},
.pp_enabled_layer_names = @ptrCast((&vk_layers).ptr),
.enabled_layer_count = @intCast(vk_layers.len),
.enabled_extension_count = @intCast(sdl_instance_ext_names.len),
.pp_enabled_extension_names = sdl_instance_ext_names.ptr,
}, null);
self.vki = try InstanceDispatch.load(instance_handle, vkGetInstanceProcAddr);
errdefer self.vki.destroyInstance(instance_handle, null);
self.instance = Instance.init(instance_handle, &self.vki);
var sdl_vksurface: c.VkSurfaceKHR = null;
if (c.SDL_Vulkan_CreateSurface(window, @as(*c.VkInstance, @ptrCast(&self.instance.handle)).*, &sdl_vksurface) == c.SDL_FALSE) {
std.debug.print("SDL_Vulkan_CreateSurface: {s}\n", .{c.SDL_GetError()});
return error.SDLVulkanCreateSurface;
}
std.debug.assert(sdl_vksurface != null);
self.surface = @as(*vk.SurfaceKHR, @ptrCast(&sdl_vksurface)).*;
const physical_devices = try self.instance.enumeratePhysicalDevicesAlloc(fba.allocator());
self.device_info = try selectPhysicalDevice(self.instance, self.surface, physical_devices);
const queue_config = try selectQueues(self.instance, self.device_info.physical_device);
const device_create_config = vk.DeviceCreateInfo{
.p_next = &vk.PhysicalDeviceVulkan13Features{
.dynamic_rendering = vk.TRUE,
.synchronization_2 = vk.TRUE,
},
.p_queue_create_infos = &queue_config.queue_create_info,
.queue_create_info_count = queue_config.queue_count,
.p_enabled_features = &self.device_info.features,
.pp_enabled_layer_names = @ptrCast((&vk_layers).ptr),
.enabled_layer_count = @intCast(vk_layers.len),
.pp_enabled_extension_names = @ptrCast((&device_extensions).ptr),
.enabled_extension_count = @intCast(device_extensions.len),
};
const device_handle = try self.instance.createDevice(self.device_info.physical_device, &device_create_config, null);
self.vkd = try DeviceDispatch.load(device_handle, self.instance.wrapper.dispatch.vkGetDeviceProcAddr);
errdefer self.vkd.destroyDevice(device_handle, null);
self.device = Device.init(device_handle, &self.vkd);
try self.maybeResizeSwapchain();
errdefer self.device.destroySwapchainKHR(self.swapchain, null);
// TODO: handle the case when different queue instance map to the same queue
const graphics_queue = QueueInstance{
.device = self.device,
.family = queue_config.graphics.family,
.handle = self.device.getDeviceQueue(queue_config.graphics.family, queue_config.graphics.index),
};
const compute_queue = QueueInstance{
.device = self.device,
.family = queue_config.compute.family,
.handle = self.device.getDeviceQueue(queue_config.graphics.family, queue_config.compute.index),
};
const host_to_device_queue = QueueInstance{
.device = self.device,
.family = queue_config.host_to_device.family,
.handle = self.device.getDeviceQueue(queue_config.graphics.family, queue_config.host_to_device.index),
};
const device_to_host_queue = QueueInstance{
.device = self.device,
.family = queue_config.device_to_host.family,
.handle = self.device.getDeviceQueue(queue_config.graphics.family, queue_config.device_to_host.index),
};
self.queues = DeviceQueues{
.graphics = graphics_queue,
.compute = compute_queue,
.host_to_device = host_to_device_queue,
.device_to_host = device_to_host_queue,
};
self.pipeline_cache = try self.device.createPipelineCache(&.{}, null);
}
pub fn acquireSwapchainImage(self: *GraphicsContext, acuire_semaphore: vk.Semaphore) !u32 {
var found = false;
var swapchain_img: u32 = 0;
try self.maybeResizeSwapchain();
while (!found) {
const acquire_result = try self.device.acquireNextImageKHR(self.swapchain, std.math.maxInt(u64), acuire_semaphore, .null_handle);
switch (acquire_result.result) {
.success, .suboptimal_khr => {
swapchain_img = acquire_result.image_index;
found = true;
},
.error_out_of_date_khr => {
// TODO: resize swapchain
std.debug.print("Out of date swapchain\n", .{});
try self.maybeResizeSwapchain();
},
.error_surface_lost_khr => {
// TODO: recreate surface
return error.SurfaceLost;
},
.not_ready => return error.SwapchainImageNotReady,
.timeout => return error.SwapchainImageTimeout,
else => {
std.debug.print("Unexpected value: {}\n", .{acquire_result.result});
@panic("Unexpected");
},
}
}
return swapchain_img;
}
fn maybeResizeSwapchain(self: *GraphicsContext) !void {
var width: c_int = 0;
var height: c_int = 0;
c.SDL_Vulkan_GetDrawableSize(self.window, &width, &height);
const new_extent = vk.Extent2D{ .width = @intCast(width), .height = @intCast(height) };
if (self.swapchain_extent.width == new_extent.width and self.swapchain_extent.height == new_extent.height) {
return;
}
if (self.swapchain_images.len > 0) {
self.allocator.free(self.swapchain_images);
self.swapchain_images = &.{};
}
self.swapchain_extent = new_extent;
const surface_caps = self.device_info.surface_capabilities;
self.swapchain = try self.device.createSwapchainKHR(&.{
.surface = self.surface,
.min_image_count = std.math.clamp(3, surface_caps.min_image_count, if (surface_caps.max_image_count == 0) std.math.maxInt(u32) else surface_caps.max_image_count),
.image_format = .r8g8b8a8_unorm, // tonemapping handles srgb
.image_color_space = .srgb_nonlinear_khr,
.image_extent = self.swapchain_extent,
.image_array_layers = 1,
.image_usage = .{
.color_attachment_bit = true,
.transfer_dst_bit = true,
},
.image_sharing_mode = .exclusive,
.present_mode = .fifo_khr, // required to be supported
.pre_transform = surface_caps.current_transform,
.composite_alpha = .{ .opaque_bit_khr = true },
.clipped = vk.TRUE,
.old_swapchain = self.swapchain,
}, null);
self.swapchain_images = try self.device.getSwapchainImagesAllocKHR(self.swapchain, self.allocator);
}
pub const DeviceQueues = struct {
graphics: QueueInstance,
compute: QueueInstance,
host_to_device: QueueInstance,
device_to_host: QueueInstance,
};
pub const SubmitInfo = struct {
wait_semaphores: []const vk.Semaphore = &.{},
wait_dst_stage_mask: []const vk.PipelineStageFlags = &.{},
command_buffers: []const vk.CommandBuffer = &.{},
signal_semaphores: []const vk.Semaphore = &.{},
};
pub const QueueInstance = struct {
const Self = @This();
mu: std.Thread.Mutex = .{},
device: Device,
handle: vk.Queue,
family: u32,
pub fn createCommandPool(self: *Self, flags: vk.CommandPoolCreateFlags) !CommandPool {
return .{
.handle = try self.device.createCommandPool(&.{
.flags = flags,
.queue_family_index = self.family,
}, null),
.device = self.device,
};
}
pub fn submit(self: *Self, info: *const SubmitInfo, fence: vk.Fence) Device.QueueSubmitError!void {
std.debug.assert(info.wait_semaphores.len == info.wait_dst_stage_mask.len);
var vk_submit_info = vk.SubmitInfo{};
if (info.wait_semaphores.len > 0) {
vk_submit_info.p_wait_semaphores = info.wait_semaphores.ptr;
vk_submit_info.p_wait_dst_stage_mask = info.wait_dst_stage_mask.ptr;
vk_submit_info.wait_semaphore_count = @intCast(info.wait_semaphores.len);
}
if (info.command_buffers.len > 0) {
vk_submit_info.p_command_buffers = info.command_buffers.ptr;
vk_submit_info.command_buffer_count = @intCast(info.command_buffers.len);
}
if (info.signal_semaphores.len > 0) {
vk_submit_info.p_signal_semaphores = info.signal_semaphores.ptr;
vk_submit_info.signal_semaphore_count = @intCast(info.signal_semaphores.len);
}
try self.submitVK(&.{vk_submit_info}, fence);
}
pub fn submitVK(self: *Self, infos: []const vk.SubmitInfo, fence: vk.Fence) Device.QueueSubmitError!void {
self.mu.lock();
defer self.mu.unlock();
try self.device.queueSubmit(self.handle, @intCast(infos.len), infos.ptr, fence);
}
};
const SelectedPhysicalDevice = struct {
physical_device: vk.PhysicalDevice,
properties: vk.PhysicalDeviceProperties,
features: vk.PhysicalDeviceFeatures,
surface_capabilities: vk.SurfaceCapabilitiesKHR,
};
fn selectPhysicalDevice(vki: Instance, surface: vk.SurfaceKHR, devices: []vk.PhysicalDevice) !SelectedPhysicalDevice {
// TODO: select suitable physical device, allow overriding using some user config
for (devices) |device| {
const props = vki.getPhysicalDeviceProperties(device);
const features = vki.getPhysicalDeviceFeatures(device);
const surface_caps = try vki.getPhysicalDeviceSurfaceCapabilitiesKHR(device, surface);
return SelectedPhysicalDevice{
.physical_device = device,
.properties = props,
.features = features,
.surface_capabilities = surface_caps,
};
}
return error.NoDeviceFound;
}
const DeviceQueueConfig = struct {
const Config = struct {
family: u32,
index: u32,
};
queue_create_info: [4]vk.DeviceQueueCreateInfo = undefined,
queue_count: u32 = 0,
graphics: Config,
compute: Config,
host_to_device: Config,
device_to_host: Config,
};
// Hardcode queue priorities, no idea why I would need to use them
const queue_priorities = [_]f32{ 1.0, 1.0, 1.0, 1.0, 1.0 };
fn selectQueues(instance: Instance, device: vk.PhysicalDevice) !DeviceQueueConfig {
var scratch: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&scratch);
const queue_family_props = try instance.getPhysicalDeviceQueueFamilyPropertiesAlloc(device, fba.allocator());
if (queue_family_props.len == 0) {
return error.NoQueues;
}
var queue_create_info: [4]vk.DeviceQueueCreateInfo = undefined;
var queue_count: u32 = 0;
var graphics: ?DeviceQueueConfig.Config = null;
var compute: ?DeviceQueueConfig.Config = null;
var host_to_device: ?DeviceQueueConfig.Config = null;
var device_to_host: ?DeviceQueueConfig.Config = null;
// We're on Intel most likely, just a single queue for everything :(
if (queue_family_props.len == 1) {
if (!queue_family_props[0].queue_flags.contains(.{ .graphics_bit = true, .compute_bit = true, .transfer_bit = true })) {
return error.InvalidQueue;
}
graphics = .{ .family = 0, .index = 0 };
compute = graphics;
device_to_host = graphics;
host_to_device = graphics;
queue_create_info[0] = .{
.queue_family_index = 0,
.queue_count = 1,
.p_queue_priorities = &queue_priorities,
};
queue_count = 1;
} else {
for (queue_family_props, 0..) |props, family_idx| {
// Jackpot, generous Jensen provided us with an all powerfull queue family, use it for everything
// TODO: actually, still need to use the dedicated transfer queue for CPU->GPU transfers to be async for sure
if (props.queue_flags.contains(.{ .graphics_bit = true, .compute_bit = true, .transfer_bit = true }) and props.queue_count >= 4) {
graphics = .{
.family = @intCast(family_idx),
.index = 0,
};
compute = .{
.family = @intCast(family_idx),
.index = 1,
};
host_to_device = .{
.family = @intCast(family_idx),
.index = 2,
};
device_to_host = .{
.family = @intCast(family_idx),
.index = 3,
};
queue_create_info[0] = .{
.queue_family_index = 0,
.queue_count = 4,
.p_queue_priorities = &queue_priorities,
};
queue_count = 1;
break;
}
// TODO: make queue create info for AMD
// Probably AMD, one graphics+compute queue, 2 separate compute queues, one pure transfer queue
if (props.queue_flags.graphics_bit) {
graphics = .{
.family = @intCast(family_idx),
.index = 0,
};
}
if (props.queue_flags.compute_bit and (compute == null or !props.queue_flags.graphics_bit)) {
compute = .{
.family = @intCast(family_idx),
.index = 0,
};
}
if (props.queue_flags.transfer_bit and (host_to_device == null or !props.queue_flags.graphics_bit or !props.queue_flags.compute_bit)) {
device_to_host = .{
.family = @intCast(family_idx),
.index = 0,
};
host_to_device = .{
.family = @intCast(family_idx),
.index = 0,
};
}
}
}
if (graphics == null or compute == null or device_to_host == null or host_to_device == null) {
return error.MissingQueueFeatures;
}
return .{
.queue_create_info = queue_create_info,
.queue_count = queue_count,
.graphics = graphics.?,
.compute = compute.?,
.host_to_device = host_to_device.?,
.device_to_host = device_to_host.?,
};
}