5686b7ab
feat(renderer): vulkan instance + surface + device + swapchain
a73x 2026-04-08 09:01
Implements Context.init/deinit in renderer.zig: loads libvulkan.so.1 via dlopen, creates Vulkan instance with KHR_surface + KHR_wayland_surface, picks a physical device + queue families, creates the logical device + swapchain + image views. Adds --vulkan-smoke-test to main.zig that exercises the full init path and prints diagnostics. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
diff --git a/build.zig b/build.zig index 06dde99..ffd6045 100644 --- a/build.zig +++ b/build.zig @@ -175,8 +175,10 @@ pub fn build(b: *std.Build) void { .root_source_file = renderer_zig_path, .target = target, .optimize = optimize, .link_libc = true, }); renderer_mod.addImport("vulkan", vulkan_module); renderer_mod.linkSystemLibrary("dl", .{}); exe_mod.addImport("renderer", renderer_mod); // Test renderer.zig @@ -184,8 +186,10 @@ pub fn build(b: *std.Build) void { .root_source_file = renderer_zig_path, .target = target, .optimize = optimize, .link_libc = true, }); renderer_test_mod.addImport("vulkan", vulkan_module); renderer_test_mod.linkSystemLibrary("dl", .{}); const renderer_tests = b.addTest(.{ .root_module = renderer_test_mod, }); diff --git a/src/main.zig b/src/main.zig index f413a5e..e644bdc 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2,6 +2,7 @@ const std = @import("std"); const vt = @import("vt"); const pty = @import("pty"); const wayland_client = @import("wayland-client"); const renderer = @import("renderer"); pub fn main() !void { var gpa: std.heap.DebugAllocator(.{}) = .init; @@ -19,9 +20,40 @@ pub fn main() !void { return runWaylandSmokeTest(alloc); } if (args.len >= 2 and std.mem.eql(u8, args[1], "--vulkan-smoke-test")) { return runVulkanSmokeTest(alloc); } std.debug.print("waystty (run with --headless for CLI dump mode)\n", .{}); } fn runVulkanSmokeTest(alloc: std.mem.Allocator) !void { var conn = try wayland_client.Connection.init(); defer conn.deinit(); std.debug.print("wayland connected\n", .{}); const window = try conn.createWindow(alloc, "waystty-vulkan-smoke"); defer window.deinit(); std.debug.print("window created (w={d} h={d})\n", .{ window.width, window.height }); // Roundtrip to ensure configure events have arrived before Vulkan touches the surface _ = conn.display.roundtrip(); var ctx = try renderer.Context.init( alloc, @ptrCast(conn.display), @ptrCast(window.surface), window.width, window.height, ); defer ctx.deinit(); std.debug.print("vulkan ok\n", .{}); std.debug.print(" format: {any}\n", .{ctx.swapchain_format}); std.debug.print(" extent: {d}x{d}\n", .{ ctx.swapchain_extent.width, ctx.swapchain_extent.height }); std.debug.print(" image count: {d}\n", .{ctx.swapchain_images.len}); } fn runWaylandSmokeTest(alloc: std.mem.Allocator) !void { var conn = try wayland_client.Connection.init(); defer conn.deinit(); diff --git a/src/renderer.zig b/src/renderer.zig index 5942c5c..e21751d 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -1,9 +1,325 @@ const std = @import("std"); const vk = @import("vulkan"); const dl = @cImport({ @cInclude("dlfcn.h"); }); pub const cell_vert_spv: []const u8 = @embedFile("cell.vert.spv"); pub const cell_frag_spv: []const u8 = @embedFile("cell.frag.spv"); var vk_lib_handle: ?*anyopaque = null; fn getVkGetInstanceProcAddr() !vk.PfnGetInstanceProcAddr { if (vk_lib_handle == null) { vk_lib_handle = dl.dlopen("libvulkan.so.1", dl.RTLD_NOW); } const handle = vk_lib_handle orelse return error.VulkanLibraryNotFound; const sym = dl.dlsym(handle, "vkGetInstanceProcAddr") orelse return error.NoVkGetInstanceProcAddr; return @ptrCast(@alignCast(sym)); } // Wrap the raw PfnGetInstanceProcAddr so it matches the anytype loader signature // expected by BaseWrapper.load (accepts instance + name, returns optional fn ptr). fn makeBaseLoader(pfn: vk.PfnGetInstanceProcAddr) vk.PfnGetInstanceProcAddr { return pfn; } const PhysicalDeviceInfo = struct { physical: vk.PhysicalDevice, graphics_queue_family: u32, present_queue_family: u32, }; const SwapchainResult = struct { swapchain: vk.SwapchainKHR, format: vk.Format, extent: vk.Extent2D, images: []vk.Image, image_views: []vk.ImageView, }; fn pickPhysicalDevice( alloc: std.mem.Allocator, vki: vk.InstanceWrapper, instance: vk.Instance, surface: vk.SurfaceKHR, ) !PhysicalDeviceInfo { var count: u32 = 0; _ = try vki.enumeratePhysicalDevices(instance, &count, null); if (count == 0) return error.NoVulkanDevices; const devices = try alloc.alloc(vk.PhysicalDevice, count); defer alloc.free(devices); _ = try vki.enumeratePhysicalDevices(instance, &count, devices.ptr); for (devices[0..count]) |pd| { var qf_count: u32 = 0; vki.getPhysicalDeviceQueueFamilyProperties(pd, &qf_count, null); const qfs = try alloc.alloc(vk.QueueFamilyProperties, qf_count); defer alloc.free(qfs); vki.getPhysicalDeviceQueueFamilyProperties(pd, &qf_count, qfs.ptr); var graphics_idx: ?u32 = null; var present_idx: ?u32 = null; for (qfs[0..qf_count], 0..) |qf, i| { if (qf.queue_flags.graphics_bit) graphics_idx = @intCast(i); const supported = try vki.getPhysicalDeviceSurfaceSupportKHR(pd, @intCast(i), surface); if (supported == vk.Bool32.true) present_idx = @intCast(i); if (graphics_idx != null and present_idx != null) break; } if (graphics_idx != null and present_idx != null) { return .{ .physical = pd, .graphics_queue_family = graphics_idx.?, .present_queue_family = present_idx.?, }; } } return error.NoSuitableDevice; } fn createSwapchain( alloc: std.mem.Allocator, vki: vk.InstanceWrapper, vkd: vk.DeviceWrapper, pd_info: PhysicalDeviceInfo, surface: vk.SurfaceKHR, device: vk.Device, width: u32, height: u32, ) !SwapchainResult { const caps = try vki.getPhysicalDeviceSurfaceCapabilitiesKHR(pd_info.physical, surface); var fmt_count: u32 = 0; _ = try vki.getPhysicalDeviceSurfaceFormatsKHR(pd_info.physical, surface, &fmt_count, null); if (fmt_count == 0) return error.NoSurfaceFormats; const formats = try alloc.alloc(vk.SurfaceFormatKHR, fmt_count); defer alloc.free(formats); _ = try vki.getPhysicalDeviceSurfaceFormatsKHR(pd_info.physical, surface, &fmt_count, formats.ptr); var chosen = formats[0]; for (formats[0..fmt_count]) |f| { if (f.format == .b8g8r8a8_unorm and f.color_space == .srgb_nonlinear_khr) { chosen = f; break; } } var extent = caps.current_extent; if (extent.width == 0xFFFFFFFF) { extent = .{ .width = width, .height = height }; } var image_count: u32 = caps.min_image_count + 1; if (caps.max_image_count > 0 and image_count > caps.max_image_count) { image_count = caps.max_image_count; } const same_family = pd_info.graphics_queue_family == pd_info.present_queue_family; const families = [_]u32{ pd_info.graphics_queue_family, pd_info.present_queue_family }; const swapchain = try vkd.createSwapchainKHR(device, &vk.SwapchainCreateInfoKHR{ .surface = surface, .min_image_count = image_count, .image_format = chosen.format, .image_color_space = chosen.color_space, .image_extent = extent, .image_array_layers = 1, .image_usage = .{ .color_attachment_bit = true }, .image_sharing_mode = if (same_family) .exclusive else .concurrent, .queue_family_index_count = if (same_family) 0 else 2, .p_queue_family_indices = if (same_family) null else &families, .pre_transform = caps.current_transform, .composite_alpha = .{ .opaque_bit_khr = true }, .present_mode = .fifo_khr, .clipped = .true, }, null); var sc_count: u32 = 0; _ = try vkd.getSwapchainImagesKHR(device, swapchain, &sc_count, null); const images = try alloc.alloc(vk.Image, sc_count); errdefer alloc.free(images); _ = try vkd.getSwapchainImagesKHR(device, swapchain, &sc_count, images.ptr); const image_views = try alloc.alloc(vk.ImageView, sc_count); errdefer { // Clean up any views created before failure alloc.free(image_views); } var views_created: usize = 0; errdefer { for (image_views[0..views_created]) |view| vkd.destroyImageView(device, view, null); } for (images[0..sc_count], 0..) |img, i| { image_views[i] = try vkd.createImageView(device, &vk.ImageViewCreateInfo{ .image = img, .view_type = .@"2d", .format = chosen.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); views_created += 1; } return .{ .swapchain = swapchain, .format = chosen.format, .extent = extent, .images = images, .image_views = image_views, }; } pub const Context = struct { alloc: std.mem.Allocator, vkb: vk.BaseWrapper, instance: vk.Instance, vki: vk.InstanceWrapper, surface: vk.SurfaceKHR, physical_device: vk.PhysicalDevice, graphics_queue_family: u32, present_queue_family: u32, device: vk.Device, vkd: vk.DeviceWrapper, graphics_queue: vk.Queue, present_queue: vk.Queue, swapchain: vk.SwapchainKHR, swapchain_format: vk.Format, swapchain_extent: vk.Extent2D, swapchain_images: []vk.Image, swapchain_image_views: []vk.ImageView, pub fn init( alloc: std.mem.Allocator, wl_display: *anyopaque, wl_surface: *anyopaque, width: u32, height: u32, ) !Context { const get_proc_addr = try getVkGetInstanceProcAddr(); const vkb = vk.BaseWrapper.load(get_proc_addr); // Create instance const app_info = vk.ApplicationInfo{ .p_application_name = "waystty", .application_version = @bitCast(vk.makeApiVersion(0, 0, 0, 1)), .p_engine_name = "waystty", .engine_version = @bitCast(vk.makeApiVersion(0, 0, 0, 1)), .api_version = @bitCast(vk.API_VERSION_1_2), }; const instance_exts = [_][*:0]const u8{ vk.extensions.khr_surface.name, vk.extensions.khr_wayland_surface.name, }; const instance = try vkb.createInstance(&vk.InstanceCreateInfo{ .p_application_info = &app_info, .enabled_extension_count = instance_exts.len, .pp_enabled_extension_names = &instance_exts, }, null); const vki = vk.InstanceWrapper.load(instance, vkb.dispatch.vkGetInstanceProcAddr.?); errdefer vki.destroyInstance(instance, null); // Create wayland surface const surface = try vki.createWaylandSurfaceKHR(instance, &vk.WaylandSurfaceCreateInfoKHR{ .display = @ptrCast(wl_display), .surface = @ptrCast(wl_surface), }, null); errdefer vki.destroySurfaceKHR(instance, surface, null); // Pick physical device + queue families const pd_info = try pickPhysicalDevice(alloc, vki, instance, surface); // Create logical device const priority: f32 = 1.0; var queue_create_infos: [2]vk.DeviceQueueCreateInfo = undefined; var queue_count: u32 = 1; queue_create_infos[0] = .{ .queue_family_index = pd_info.graphics_queue_family, .queue_count = 1, .p_queue_priorities = @ptrCast(&priority), }; if (pd_info.graphics_queue_family != pd_info.present_queue_family) { queue_create_infos[1] = .{ .queue_family_index = pd_info.present_queue_family, .queue_count = 1, .p_queue_priorities = @ptrCast(&priority), }; queue_count = 2; } const device_exts = [_][*:0]const u8{vk.extensions.khr_swapchain.name}; const empty_layer_name: *const u8 = @ptrFromInt(1); // dummy, count=0 so never dereferenced const device = try vki.createDevice(pd_info.physical, &vk.DeviceCreateInfo{ .queue_create_info_count = queue_count, .p_queue_create_infos = &queue_create_infos, .enabled_layer_count = 0, .pp_enabled_layer_names = &empty_layer_name, .enabled_extension_count = device_exts.len, .pp_enabled_extension_names = &device_exts, }, null); const vkd = vk.DeviceWrapper.load(device, vki.dispatch.vkGetDeviceProcAddr.?); errdefer vkd.destroyDevice(device, null); const graphics_queue = vkd.getDeviceQueue(device, pd_info.graphics_queue_family, 0); const present_queue = vkd.getDeviceQueue(device, pd_info.present_queue_family, 0); // Create swapchain const sc = try createSwapchain(alloc, vki, vkd, pd_info, surface, device, width, height); errdefer { for (sc.image_views) |view| vkd.destroyImageView(device, view, null); alloc.free(sc.image_views); alloc.free(sc.images); vkd.destroySwapchainKHR(device, sc.swapchain, null); } return .{ .alloc = alloc, .vkb = vkb, .instance = instance, .vki = vki, .surface = surface, .physical_device = pd_info.physical, .graphics_queue_family = pd_info.graphics_queue_family, .present_queue_family = pd_info.present_queue_family, .device = device, .vkd = vkd, .graphics_queue = graphics_queue, .present_queue = present_queue, .swapchain = sc.swapchain, .swapchain_format = sc.format, .swapchain_extent = sc.extent, .swapchain_images = sc.images, .swapchain_image_views = sc.image_views, }; } pub fn deinit(self: *Context) void { for (self.swapchain_image_views) |view| self.vkd.destroyImageView(self.device, view, null); self.alloc.free(self.swapchain_image_views); self.alloc.free(self.swapchain_images); self.vkd.destroySwapchainKHR(self.device, self.swapchain, null); self.vkd.destroyDevice(self.device, null); self.vki.destroySurfaceKHR(self.instance, self.surface, null); self.vki.destroyInstance(self.instance, null); } }; test "vulkan module imports" { _ = vk; }