5686b7ab
feat(renderer): vulkan instance + surface + device + swapchain
a73x 2026-04-08 09:01
Commit message
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -175,8 +175,10 @@ pub fn build(b: *std.Build) void { | |||
| 175 | .root_source_file = renderer_zig_path, | 175 | .root_source_file = renderer_zig_path, |
| 176 | .target = target, | 176 | .target = target, |
| 177 | .optimize = optimize, | 177 | .optimize = optimize, |
| 178 | .link_libc = true, | ||
| 178 | }); | 179 | }); |
| 179 | renderer_mod.addImport("vulkan", vulkan_module); | 180 | renderer_mod.addImport("vulkan", vulkan_module); |
| 181 | renderer_mod.linkSystemLibrary("dl", .{}); | ||
| 180 | exe_mod.addImport("renderer", renderer_mod); | 182 | exe_mod.addImport("renderer", renderer_mod); |
| 181 | 183 | ||
| 182 | // Test renderer.zig | 184 | // Test renderer.zig |
| @@ -184,8 +186,10 @@ pub fn build(b: *std.Build) void { | |||
| 184 | .root_source_file = renderer_zig_path, | 186 | .root_source_file = renderer_zig_path, |
| 185 | .target = target, | 187 | .target = target, |
| 186 | .optimize = optimize, | 188 | .optimize = optimize, |
| 189 | .link_libc = true, | ||
| 187 | }); | 190 | }); |
| 188 | renderer_test_mod.addImport("vulkan", vulkan_module); | 191 | renderer_test_mod.addImport("vulkan", vulkan_module); |
| 192 | renderer_test_mod.linkSystemLibrary("dl", .{}); | ||
| 189 | const renderer_tests = b.addTest(.{ | 193 | const renderer_tests = b.addTest(.{ |
| 190 | .root_module = renderer_test_mod, | 194 | .root_module = renderer_test_mod, |
| 191 | }); | 195 | }); |
src/main.zig
| Old | New | ||
|---|---|---|---|
| @@ -2,6 +2,7 @@ const std = @import("std"); | |||
| 2 | const vt = @import("vt"); | 2 | const vt = @import("vt"); |
| 3 | const pty = @import("pty"); | 3 | const pty = @import("pty"); |
| 4 | const wayland_client = @import("wayland-client"); | 4 | const wayland_client = @import("wayland-client"); |
| 5 | const renderer = @import("renderer"); | ||
| 5 | 6 | ||
| 6 | pub fn main() !void { | 7 | pub fn main() !void { |
| 7 | var gpa: std.heap.DebugAllocator(.{}) = .init; | 8 | var gpa: std.heap.DebugAllocator(.{}) = .init; |
| @@ -19,9 +20,40 @@ pub fn main() !void { | |||
| 19 | return runWaylandSmokeTest(alloc); | 20 | return runWaylandSmokeTest(alloc); |
| 20 | } | 21 | } |
| 21 | 22 | ||
| 23 | if (args.len >= 2 and std.mem.eql(u8, args[1], "--vulkan-smoke-test")) { | ||
| 24 | return runVulkanSmokeTest(alloc); | ||
| 25 | } | ||
| 26 | |||
| 22 | std.debug.print("waystty (run with --headless for CLI dump mode)\n", .{}); | 27 | std.debug.print("waystty (run with --headless for CLI dump mode)\n", .{}); |
| 23 | } | 28 | } |
| 24 | 29 | ||
| 30 | fn runVulkanSmokeTest(alloc: std.mem.Allocator) !void { | ||
| 31 | var conn = try wayland_client.Connection.init(); | ||
| 32 | defer conn.deinit(); | ||
| 33 | std.debug.print("wayland connected\n", .{}); | ||
| 34 | |||
| 35 | const window = try conn.createWindow(alloc, "waystty-vulkan-smoke"); | ||
| 36 | defer window.deinit(); | ||
| 37 | std.debug.print("window created (w={d} h={d})\n", .{ window.width, window.height }); | ||
| 38 | |||
| 39 | // Roundtrip to ensure configure events have arrived before Vulkan touches the surface | ||
| 40 | _ = conn.display.roundtrip(); | ||
| 41 | |||
| 42 | var ctx = try renderer.Context.init( | ||
| 43 | alloc, | ||
| 44 | @ptrCast(conn.display), | ||
| 45 | @ptrCast(window.surface), | ||
| 46 | window.width, | ||
| 47 | window.height, | ||
| 48 | ); | ||
| 49 | defer ctx.deinit(); | ||
| 50 | |||
| 51 | std.debug.print("vulkan ok\n", .{}); | ||
| 52 | std.debug.print(" format: {any}\n", .{ctx.swapchain_format}); | ||
| 53 | std.debug.print(" extent: {d}x{d}\n", .{ ctx.swapchain_extent.width, ctx.swapchain_extent.height }); | ||
| 54 | std.debug.print(" image count: {d}\n", .{ctx.swapchain_images.len}); | ||
| 55 | } | ||
| 56 | |||
| 25 | fn runWaylandSmokeTest(alloc: std.mem.Allocator) !void { | 57 | fn runWaylandSmokeTest(alloc: std.mem.Allocator) !void { |
| 26 | var conn = try wayland_client.Connection.init(); | 58 | var conn = try wayland_client.Connection.init(); |
| 27 | defer conn.deinit(); | 59 | defer conn.deinit(); |
src/renderer.zig
| Old | New | ||
|---|---|---|---|
| @@ -1,9 +1,325 @@ | |||
| 1 | const std = @import("std"); | 1 | const std = @import("std"); |
| 2 | const vk = @import("vulkan"); | 2 | const vk = @import("vulkan"); |
| 3 | 3 | ||
| 4 | const dl = @cImport({ | ||
| 5 | @cInclude("dlfcn.h"); | ||
| 6 | }); | ||
| 7 | |||
| 4 | pub const cell_vert_spv: []const u8 = @embedFile("cell.vert.spv"); | 8 | pub const cell_vert_spv: []const u8 = @embedFile("cell.vert.spv"); |
| 5 | pub const cell_frag_spv: []const u8 = @embedFile("cell.frag.spv"); | 9 | pub const cell_frag_spv: []const u8 = @embedFile("cell.frag.spv"); |
| 6 | 10 | ||
| 11 | var vk_lib_handle: ?*anyopaque = null; | ||
| 12 | |||
| 13 | fn getVkGetInstanceProcAddr() !vk.PfnGetInstanceProcAddr { | ||
| 14 | if (vk_lib_handle == null) { | ||
| 15 | vk_lib_handle = dl.dlopen("libvulkan.so.1", dl.RTLD_NOW); | ||
| 16 | } | ||
| 17 | const handle = vk_lib_handle orelse return error.VulkanLibraryNotFound; | ||
| 18 | const sym = dl.dlsym(handle, "vkGetInstanceProcAddr") orelse return error.NoVkGetInstanceProcAddr; | ||
| 19 | return @ptrCast(@alignCast(sym)); | ||
| 20 | } | ||
| 21 | |||
| 22 | // Wrap the raw PfnGetInstanceProcAddr so it matches the anytype loader signature | ||
| 23 | // expected by BaseWrapper.load (accepts instance + name, returns optional fn ptr). | ||
| 24 | fn makeBaseLoader(pfn: vk.PfnGetInstanceProcAddr) vk.PfnGetInstanceProcAddr { | ||
| 25 | return pfn; | ||
| 26 | } | ||
| 27 | |||
| 28 | const PhysicalDeviceInfo = struct { | ||
| 29 | physical: vk.PhysicalDevice, | ||
| 30 | graphics_queue_family: u32, | ||
| 31 | present_queue_family: u32, | ||
| 32 | }; | ||
| 33 | |||
| 34 | const SwapchainResult = struct { | ||
| 35 | swapchain: vk.SwapchainKHR, | ||
| 36 | format: vk.Format, | ||
| 37 | extent: vk.Extent2D, | ||
| 38 | images: []vk.Image, | ||
| 39 | image_views: []vk.ImageView, | ||
| 40 | }; | ||
| 41 | |||
| 42 | fn pickPhysicalDevice( | ||
| 43 | alloc: std.mem.Allocator, | ||
| 44 | vki: vk.InstanceWrapper, | ||
| 45 | instance: vk.Instance, | ||
| 46 | surface: vk.SurfaceKHR, | ||
| 47 | ) !PhysicalDeviceInfo { | ||
| 48 | var count: u32 = 0; | ||
| 49 | _ = try vki.enumeratePhysicalDevices(instance, &count, null); | ||
| 50 | if (count == 0) return error.NoVulkanDevices; | ||
| 51 | |||
| 52 | const devices = try alloc.alloc(vk.PhysicalDevice, count); | ||
| 53 | defer alloc.free(devices); | ||
| 54 | _ = try vki.enumeratePhysicalDevices(instance, &count, devices.ptr); | ||
| 55 | |||
| 56 | for (devices[0..count]) |pd| { | ||
| 57 | var qf_count: u32 = 0; | ||
| 58 | vki.getPhysicalDeviceQueueFamilyProperties(pd, &qf_count, null); | ||
| 59 | const qfs = try alloc.alloc(vk.QueueFamilyProperties, qf_count); | ||
| 60 | defer alloc.free(qfs); | ||
| 61 | vki.getPhysicalDeviceQueueFamilyProperties(pd, &qf_count, qfs.ptr); | ||
| 62 | |||
| 63 | var graphics_idx: ?u32 = null; | ||
| 64 | var present_idx: ?u32 = null; | ||
| 65 | for (qfs[0..qf_count], 0..) |qf, i| { | ||
| 66 | if (qf.queue_flags.graphics_bit) graphics_idx = @intCast(i); | ||
| 67 | |||
| 68 | const supported = try vki.getPhysicalDeviceSurfaceSupportKHR(pd, @intCast(i), surface); | ||
| 69 | if (supported == vk.Bool32.true) present_idx = @intCast(i); | ||
| 70 | |||
| 71 | if (graphics_idx != null and present_idx != null) break; | ||
| 72 | } | ||
| 73 | |||
| 74 | if (graphics_idx != null and present_idx != null) { | ||
| 75 | return .{ | ||
| 76 | .physical = pd, | ||
| 77 | .graphics_queue_family = graphics_idx.?, | ||
| 78 | .present_queue_family = present_idx.?, | ||
| 79 | }; | ||
| 80 | } | ||
| 81 | } | ||
| 82 | return error.NoSuitableDevice; | ||
| 83 | } | ||
| 84 | |||
| 85 | fn createSwapchain( | ||
| 86 | alloc: std.mem.Allocator, | ||
| 87 | vki: vk.InstanceWrapper, | ||
| 88 | vkd: vk.DeviceWrapper, | ||
| 89 | pd_info: PhysicalDeviceInfo, | ||
| 90 | surface: vk.SurfaceKHR, | ||
| 91 | device: vk.Device, | ||
| 92 | width: u32, | ||
| 93 | height: u32, | ||
| 94 | ) !SwapchainResult { | ||
| 95 | const caps = try vki.getPhysicalDeviceSurfaceCapabilitiesKHR(pd_info.physical, surface); | ||
| 96 | |||
| 97 | var fmt_count: u32 = 0; | ||
| 98 | _ = try vki.getPhysicalDeviceSurfaceFormatsKHR(pd_info.physical, surface, &fmt_count, null); | ||
| 99 | if (fmt_count == 0) return error.NoSurfaceFormats; | ||
| 100 | const formats = try alloc.alloc(vk.SurfaceFormatKHR, fmt_count); | ||
| 101 | defer alloc.free(formats); | ||
| 102 | _ = try vki.getPhysicalDeviceSurfaceFormatsKHR(pd_info.physical, surface, &fmt_count, formats.ptr); | ||
| 103 | |||
| 104 | var chosen = formats[0]; | ||
| 105 | for (formats[0..fmt_count]) |f| { | ||
| 106 | if (f.format == .b8g8r8a8_unorm and f.color_space == .srgb_nonlinear_khr) { | ||
| 107 | chosen = f; | ||
| 108 | break; | ||
| 109 | } | ||
| 110 | } | ||
| 111 | |||
| 112 | var extent = caps.current_extent; | ||
| 113 | if (extent.width == 0xFFFFFFFF) { | ||
| 114 | extent = .{ .width = width, .height = height }; | ||
| 115 | } | ||
| 116 | |||
| 117 | var image_count: u32 = caps.min_image_count + 1; | ||
| 118 | if (caps.max_image_count > 0 and image_count > caps.max_image_count) { | ||
| 119 | image_count = caps.max_image_count; | ||
| 120 | } | ||
| 121 | |||
| 122 | const same_family = pd_info.graphics_queue_family == pd_info.present_queue_family; | ||
| 123 | const families = [_]u32{ pd_info.graphics_queue_family, pd_info.present_queue_family }; | ||
| 124 | |||
| 125 | const swapchain = try vkd.createSwapchainKHR(device, &vk.SwapchainCreateInfoKHR{ | ||
| 126 | .surface = surface, | ||
| 127 | .min_image_count = image_count, | ||
| 128 | .image_format = chosen.format, | ||
| 129 | .image_color_space = chosen.color_space, | ||
| 130 | .image_extent = extent, | ||
| 131 | .image_array_layers = 1, | ||
| 132 | .image_usage = .{ .color_attachment_bit = true }, | ||
| 133 | .image_sharing_mode = if (same_family) .exclusive else .concurrent, | ||
| 134 | .queue_family_index_count = if (same_family) 0 else 2, | ||
| 135 | .p_queue_family_indices = if (same_family) null else &families, | ||
| 136 | .pre_transform = caps.current_transform, | ||
| 137 | .composite_alpha = .{ .opaque_bit_khr = true }, | ||
| 138 | .present_mode = .fifo_khr, | ||
| 139 | .clipped = .true, | ||
| 140 | }, null); | ||
| 141 | |||
| 142 | var sc_count: u32 = 0; | ||
| 143 | _ = try vkd.getSwapchainImagesKHR(device, swapchain, &sc_count, null); | ||
| 144 | const images = try alloc.alloc(vk.Image, sc_count); | ||
| 145 | errdefer alloc.free(images); | ||
| 146 | _ = try vkd.getSwapchainImagesKHR(device, swapchain, &sc_count, images.ptr); | ||
| 147 | |||
| 148 | const image_views = try alloc.alloc(vk.ImageView, sc_count); | ||
| 149 | errdefer { | ||
| 150 | // Clean up any views created before failure | ||
| 151 | alloc.free(image_views); | ||
| 152 | } | ||
| 153 | var views_created: usize = 0; | ||
| 154 | errdefer { | ||
| 155 | for (image_views[0..views_created]) |view| vkd.destroyImageView(device, view, null); | ||
| 156 | } | ||
| 157 | |||
| 158 | for (images[0..sc_count], 0..) |img, i| { | ||
| 159 | image_views[i] = try vkd.createImageView(device, &vk.ImageViewCreateInfo{ | ||
| 160 | .image = img, | ||
| 161 | .view_type = .@"2d", | ||
| 162 | .format = chosen.format, | ||
| 163 | .components = .{ .r = .identity, .g = .identity, .b = .identity, .a = .identity }, | ||
| 164 | .subresource_range = .{ | ||
| 165 | .aspect_mask = .{ .color_bit = true }, | ||
| 166 | .base_mip_level = 0, | ||
| 167 | .level_count = 1, | ||
| 168 | .base_array_layer = 0, | ||
| 169 | .layer_count = 1, | ||
| 170 | }, | ||
| 171 | }, null); | ||
| 172 | views_created += 1; | ||
| 173 | } | ||
| 174 | |||
| 175 | return .{ | ||
| 176 | .swapchain = swapchain, | ||
| 177 | .format = chosen.format, | ||
| 178 | .extent = extent, | ||
| 179 | .images = images, | ||
| 180 | .image_views = image_views, | ||
| 181 | }; | ||
| 182 | } | ||
| 183 | |||
| 184 | pub const Context = struct { | ||
| 185 | alloc: std.mem.Allocator, | ||
| 186 | vkb: vk.BaseWrapper, | ||
| 187 | instance: vk.Instance, | ||
| 188 | vki: vk.InstanceWrapper, | ||
| 189 | surface: vk.SurfaceKHR, | ||
| 190 | physical_device: vk.PhysicalDevice, | ||
| 191 | graphics_queue_family: u32, | ||
| 192 | present_queue_family: u32, | ||
| 193 | device: vk.Device, | ||
| 194 | vkd: vk.DeviceWrapper, | ||
| 195 | graphics_queue: vk.Queue, | ||
| 196 | present_queue: vk.Queue, | ||
| 197 | swapchain: vk.SwapchainKHR, | ||
| 198 | swapchain_format: vk.Format, | ||
| 199 | swapchain_extent: vk.Extent2D, | ||
| 200 | swapchain_images: []vk.Image, | ||
| 201 | swapchain_image_views: []vk.ImageView, | ||
| 202 | |||
| 203 | pub fn init( | ||
| 204 | alloc: std.mem.Allocator, | ||
| 205 | wl_display: *anyopaque, | ||
| 206 | wl_surface: *anyopaque, | ||
| 207 | width: u32, | ||
| 208 | height: u32, | ||
| 209 | ) !Context { | ||
| 210 | const get_proc_addr = try getVkGetInstanceProcAddr(); | ||
| 211 | const vkb = vk.BaseWrapper.load(get_proc_addr); | ||
| 212 | |||
| 213 | // Create instance | ||
| 214 | const app_info = vk.ApplicationInfo{ | ||
| 215 | .p_application_name = "waystty", | ||
| 216 | .application_version = @bitCast(vk.makeApiVersion(0, 0, 0, 1)), | ||
| 217 | .p_engine_name = "waystty", | ||
| 218 | .engine_version = @bitCast(vk.makeApiVersion(0, 0, 0, 1)), | ||
| 219 | .api_version = @bitCast(vk.API_VERSION_1_2), | ||
| 220 | }; | ||
| 221 | |||
| 222 | const instance_exts = [_][*:0]const u8{ | ||
| 223 | vk.extensions.khr_surface.name, | ||
| 224 | vk.extensions.khr_wayland_surface.name, | ||
| 225 | }; | ||
| 226 | |||
| 227 | const instance = try vkb.createInstance(&vk.InstanceCreateInfo{ | ||
| 228 | .p_application_info = &app_info, | ||
| 229 | .enabled_extension_count = instance_exts.len, | ||
| 230 | .pp_enabled_extension_names = &instance_exts, | ||
| 231 | }, null); | ||
| 232 | |||
| 233 | const vki = vk.InstanceWrapper.load(instance, vkb.dispatch.vkGetInstanceProcAddr.?); | ||
| 234 | errdefer vki.destroyInstance(instance, null); | ||
| 235 | |||
| 236 | // Create wayland surface | ||
| 237 | const surface = try vki.createWaylandSurfaceKHR(instance, &vk.WaylandSurfaceCreateInfoKHR{ | ||
| 238 | .display = @ptrCast(wl_display), | ||
| 239 | .surface = @ptrCast(wl_surface), | ||
| 240 | }, null); | ||
| 241 | errdefer vki.destroySurfaceKHR(instance, surface, null); | ||
| 242 | |||
| 243 | // Pick physical device + queue families | ||
| 244 | const pd_info = try pickPhysicalDevice(alloc, vki, instance, surface); | ||
| 245 | |||
| 246 | // Create logical device | ||
| 247 | const priority: f32 = 1.0; | ||
| 248 | var queue_create_infos: [2]vk.DeviceQueueCreateInfo = undefined; | ||
| 249 | var queue_count: u32 = 1; | ||
| 250 | queue_create_infos[0] = .{ | ||
| 251 | .queue_family_index = pd_info.graphics_queue_family, | ||
| 252 | .queue_count = 1, | ||
| 253 | .p_queue_priorities = @ptrCast(&priority), | ||
| 254 | }; | ||
| 255 | if (pd_info.graphics_queue_family != pd_info.present_queue_family) { | ||
| 256 | queue_create_infos[1] = .{ | ||
| 257 | .queue_family_index = pd_info.present_queue_family, | ||
| 258 | .queue_count = 1, | ||
| 259 | .p_queue_priorities = @ptrCast(&priority), | ||
| 260 | }; | ||
| 261 | queue_count = 2; | ||
| 262 | } | ||
| 263 | |||
| 264 | const device_exts = [_][*:0]const u8{vk.extensions.khr_swapchain.name}; | ||
| 265 | |||
| 266 | const empty_layer_name: *const u8 = @ptrFromInt(1); // dummy, count=0 so never dereferenced | ||
| 267 | const device = try vki.createDevice(pd_info.physical, &vk.DeviceCreateInfo{ | ||
| 268 | .queue_create_info_count = queue_count, | ||
| 269 | .p_queue_create_infos = &queue_create_infos, | ||
| 270 | .enabled_layer_count = 0, | ||
| 271 | .pp_enabled_layer_names = &empty_layer_name, | ||
| 272 | .enabled_extension_count = device_exts.len, | ||
| 273 | .pp_enabled_extension_names = &device_exts, | ||
| 274 | }, null); | ||
| 275 | |||
| 276 | const vkd = vk.DeviceWrapper.load(device, vki.dispatch.vkGetDeviceProcAddr.?); | ||
| 277 | errdefer vkd.destroyDevice(device, null); | ||
| 278 | |||
| 279 | const graphics_queue = vkd.getDeviceQueue(device, pd_info.graphics_queue_family, 0); | ||
| 280 | const present_queue = vkd.getDeviceQueue(device, pd_info.present_queue_family, 0); | ||
| 281 | |||
| 282 | // Create swapchain | ||
| 283 | const sc = try createSwapchain(alloc, vki, vkd, pd_info, surface, device, width, height); | ||
| 284 | errdefer { | ||
| 285 | for (sc.image_views) |view| vkd.destroyImageView(device, view, null); | ||
| 286 | alloc.free(sc.image_views); | ||
| 287 | alloc.free(sc.images); | ||
| 288 | vkd.destroySwapchainKHR(device, sc.swapchain, null); | ||
| 289 | } | ||
| 290 | |||
| 291 | return .{ | ||
| 292 | .alloc = alloc, | ||
| 293 | .vkb = vkb, | ||
| 294 | .instance = instance, | ||
| 295 | .vki = vki, | ||
| 296 | .surface = surface, | ||
| 297 | .physical_device = pd_info.physical, | ||
| 298 | .graphics_queue_family = pd_info.graphics_queue_family, | ||
| 299 | .present_queue_family = pd_info.present_queue_family, | ||
| 300 | .device = device, | ||
| 301 | .vkd = vkd, | ||
| 302 | .graphics_queue = graphics_queue, | ||
| 303 | .present_queue = present_queue, | ||
| 304 | .swapchain = sc.swapchain, | ||
| 305 | .swapchain_format = sc.format, | ||
| 306 | .swapchain_extent = sc.extent, | ||
| 307 | .swapchain_images = sc.images, | ||
| 308 | .swapchain_image_views = sc.image_views, | ||
| 309 | }; | ||
| 310 | } | ||
| 311 | |||
| 312 | pub fn deinit(self: *Context) void { | ||
| 313 | for (self.swapchain_image_views) |view| self.vkd.destroyImageView(self.device, view, null); | ||
| 314 | self.alloc.free(self.swapchain_image_views); | ||
| 315 | self.alloc.free(self.swapchain_images); | ||
| 316 | self.vkd.destroySwapchainKHR(self.device, self.swapchain, null); | ||
| 317 | self.vkd.destroyDevice(self.device, null); | ||
| 318 | self.vki.destroySurfaceKHR(self.instance, self.surface, null); | ||
| 319 | self.vki.destroyInstance(self.instance, null); | ||
| 320 | } | ||
| 321 | }; | ||
| 322 | |||
| 7 | test "vulkan module imports" { | 323 | test "vulkan module imports" { |
| 8 | _ = vk; | 324 | _ = vk; |
| 9 | } | 325 | } |