e4c26d38
Handle window resize
a73x 2026-04-08 11:40
Commit message
src/main.zig
| Old | New | ||
|---|---|---|---|
| @@ -9,6 +9,11 @@ const c = @cImport({ | |||
| 9 | @cInclude("xkbcommon/xkbcommon-keysyms.h"); | 9 | @cInclude("xkbcommon/xkbcommon-keysyms.h"); |
| 10 | }); | 10 | }); |
| 11 | 11 | ||
| 12 | const GridSize = struct { | ||
| 13 | cols: u16, | ||
| 14 | rows: u16, | ||
| 15 | }; | ||
| 16 | |||
| 12 | pub fn main() !void { | 17 | pub fn main() !void { |
| 13 | var gpa: std.heap.DebugAllocator(.{}) = .init; | 18 | var gpa: std.heap.DebugAllocator(.{}) = .init; |
| 14 | defer _ = gpa.deinit(); | 19 | defer _ = gpa.deinit(); |
| @@ -53,8 +58,9 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 53 | const cell_h = face.cellHeight(); | 58 | const cell_h = face.cellHeight(); |
| 54 | 59 | ||
| 55 | // === grid size === | 60 | // === grid size === |
| 56 | const cols: u16 = 80; | 61 | const initial_grid: GridSize = .{ .cols = 80, .rows = 24 }; |
| 57 | const rows: u16 = 24; | 62 | var cols: u16 = initial_grid.cols; |
| 63 | var rows: u16 = initial_grid.rows; | ||
| 58 | const initial_w: u32 = @as(u32, cols) * cell_w; | 64 | const initial_w: u32 = @as(u32, cols) * cell_w; |
| 59 | const initial_h: u32 = @as(u32, rows) * cell_h; | 65 | const initial_h: u32 = @as(u32, rows) * cell_h; |
| 60 | 66 | ||
| @@ -124,6 +130,8 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 124 | 130 | ||
| 125 | var read_buf: [8192]u8 = undefined; | 131 | var read_buf: [8192]u8 = undefined; |
| 126 | var key_buf: [32]u8 = undefined; | 132 | var key_buf: [32]u8 = undefined; |
| 133 | var last_window_w = window.width; | ||
| 134 | var last_window_h = window.height; | ||
| 127 | 135 | ||
| 128 | while (!window.should_close and p.isChildAlive()) { | 136 | while (!window.should_close and p.isChildAlive()) { |
| 129 | // Flush any pending wayland requests | 137 | // Flush any pending wayland requests |
| @@ -165,6 +173,23 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 165 | } | 173 | } |
| 166 | keyboard.event_queue.clearRetainingCapacity(); | 174 | keyboard.event_queue.clearRetainingCapacity(); |
| 167 | 175 | ||
| 176 | if (window.width != last_window_w or window.height != last_window_h) { | ||
| 177 | const new_grid = gridSizeForWindow(window.width, window.height, cell_w, cell_h); | ||
| 178 | if (new_grid.cols != cols or new_grid.rows != rows) { | ||
| 179 | _ = try ctx.vkd.deviceWaitIdle(ctx.device); | ||
| 180 | try ctx.recreateSwapchain(window.width, window.height); | ||
| 181 | try term.resize(new_grid.cols, new_grid.rows); | ||
| 182 | try p.resize(new_grid.cols, new_grid.rows); | ||
| 183 | cols = new_grid.cols; | ||
| 184 | rows = new_grid.rows; | ||
| 185 | } else { | ||
| 186 | _ = try ctx.vkd.deviceWaitIdle(ctx.device); | ||
| 187 | try ctx.recreateSwapchain(window.width, window.height); | ||
| 188 | } | ||
| 189 | last_window_w = window.width; | ||
| 190 | last_window_h = window.height; | ||
| 191 | } | ||
| 192 | |||
| 168 | // === render === | 193 | // === render === |
| 169 | try term.snapshot(); | 194 | try term.snapshot(); |
| 170 | 195 | ||
| @@ -216,6 +241,15 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 216 | _ = try ctx.vkd.deviceWaitIdle(ctx.device); | 241 | _ = try ctx.vkd.deviceWaitIdle(ctx.device); |
| 217 | } | 242 | } |
| 218 | 243 | ||
| 244 | fn gridSizeForWindow(window_w: u32, window_h: u32, cell_w: u32, cell_h: u32) GridSize { | ||
| 245 | const cols = @max(@as(u32, 1), if (cell_w == 0) 1 else window_w / cell_w); | ||
| 246 | const rows = @max(@as(u32, 1), if (cell_h == 0) 1 else window_h / cell_h); | ||
| 247 | return .{ | ||
| 248 | .cols = @intCast(cols), | ||
| 249 | .rows = @intCast(rows), | ||
| 250 | }; | ||
| 251 | } | ||
| 252 | |||
| 219 | fn encodeKeyboardEvent( | 253 | fn encodeKeyboardEvent( |
| 220 | term: *const vt.Terminal, | 254 | term: *const vt.Terminal, |
| 221 | ev: wayland_client.KeyboardEvent, | 255 | ev: wayland_client.KeyboardEvent, |
| @@ -374,6 +408,12 @@ test "encodeKeyboardEvent encodes left arrow" { | |||
| 374 | try std.testing.expectEqualStrings("\x1b[D", encoded); | 408 | try std.testing.expectEqualStrings("\x1b[D", encoded); |
| 375 | } | 409 | } |
| 376 | 410 | ||
| 411 | test "gridSizeForWindow clamps to at least one cell" { | ||
| 412 | const grid = gridSizeForWindow(10, 5, 16, 20); | ||
| 413 | try std.testing.expectEqual(@as(u16, 1), grid.cols); | ||
| 414 | try std.testing.expectEqual(@as(u16, 1), grid.rows); | ||
| 415 | } | ||
| 416 | |||
| 377 | fn runRenderSmokeTest(alloc: std.mem.Allocator) !void { | 417 | fn runRenderSmokeTest(alloc: std.mem.Allocator) !void { |
| 378 | var conn = try wayland_client.Connection.init(); | 418 | var conn = try wayland_client.Connection.init(); |
| 379 | defer conn.deinit(); | 419 | defer conn.deinit(); |
src/renderer.zig
| Old | New | ||
|---|---|---|---|
| @@ -198,6 +198,36 @@ fn createSwapchain( | |||
| 198 | }; | 198 | }; |
| 199 | } | 199 | } |
| 200 | 200 | ||
| 201 | fn createFramebuffers( | ||
| 202 | alloc: std.mem.Allocator, | ||
| 203 | vkd: vk.DeviceWrapper, | ||
| 204 | device: vk.Device, | ||
| 205 | render_pass: vk.RenderPass, | ||
| 206 | extent: vk.Extent2D, | ||
| 207 | image_views: []vk.ImageView, | ||
| 208 | ) ![]vk.Framebuffer { | ||
| 209 | const framebuffers = try alloc.alloc(vk.Framebuffer, image_views.len); | ||
| 210 | errdefer alloc.free(framebuffers); | ||
| 211 | var created: usize = 0; | ||
| 212 | errdefer { | ||
| 213 | for (framebuffers[0..created]) |fb| vkd.destroyFramebuffer(device, fb, null); | ||
| 214 | } | ||
| 215 | |||
| 216 | for (image_views, 0..) |view, i| { | ||
| 217 | framebuffers[i] = try vkd.createFramebuffer(device, &vk.FramebufferCreateInfo{ | ||
| 218 | .render_pass = render_pass, | ||
| 219 | .attachment_count = 1, | ||
| 220 | .p_attachments = @ptrCast(&view), | ||
| 221 | .width = extent.width, | ||
| 222 | .height = extent.height, | ||
| 223 | .layers = 1, | ||
| 224 | }, null); | ||
| 225 | created += 1; | ||
| 226 | } | ||
| 227 | |||
| 228 | return framebuffers; | ||
| 229 | } | ||
| 230 | |||
| 201 | /// Push constants layout matching cell.vert | 231 | /// Push constants layout matching cell.vert |
| 202 | pub const PushConstants = extern struct { | 232 | pub const PushConstants = extern struct { |
| 203 | viewport_size: [2]f32, | 233 | viewport_size: [2]f32, |
| @@ -454,23 +484,14 @@ pub const Context = struct { | |||
| 454 | errdefer vkd.destroyRenderPass(device, render_pass, null); | 484 | errdefer vkd.destroyRenderPass(device, render_pass, null); |
| 455 | 485 | ||
| 456 | // Create framebuffers (one per swapchain image view) | 486 | // Create framebuffers (one per swapchain image view) |
| 457 | const framebuffers = try alloc.alloc(vk.Framebuffer, sc.image_views.len); | 487 | const framebuffers = try createFramebuffers( |
| 458 | errdefer alloc.free(framebuffers); | 488 | alloc, |
| 459 | var fbs_created: usize = 0; | 489 | vkd, |
| 460 | errdefer { | 490 | device, |
| 461 | for (framebuffers[0..fbs_created]) |fb| vkd.destroyFramebuffer(device, fb, null); | 491 | render_pass, |
| 462 | } | 492 | sc.extent, |
| 463 | for (sc.image_views, 0..) |view, i| { | 493 | sc.image_views, |
| 464 | framebuffers[i] = try vkd.createFramebuffer(device, &vk.FramebufferCreateInfo{ | 494 | ); |
| 465 | .render_pass = render_pass, | ||
| 466 | .attachment_count = 1, | ||
| 467 | .p_attachments = @ptrCast(&view), | ||
| 468 | .width = sc.extent.width, | ||
| 469 | .height = sc.extent.height, | ||
| 470 | .layers = 1, | ||
| 471 | }, null); | ||
| 472 | fbs_created += 1; | ||
| 473 | } | ||
| 474 | 495 | ||
| 475 | // Create descriptor set layout (single combined image sampler for glyph atlas) | 496 | // Create descriptor set layout (single combined image sampler for glyph atlas) |
| 476 | const dsl_binding = vk.DescriptorSetLayoutBinding{ | 497 | const dsl_binding = vk.DescriptorSetLayoutBinding{ |
| @@ -856,22 +877,69 @@ pub const Context = struct { | |||
| 856 | self.vkd.destroyDescriptorPool(self.device, self.descriptor_pool, null); | 877 | self.vkd.destroyDescriptorPool(self.device, self.descriptor_pool, null); |
| 857 | self.vkd.destroyDescriptorSetLayout(self.device, self.descriptor_set_layout, null); | 878 | self.vkd.destroyDescriptorSetLayout(self.device, self.descriptor_set_layout, null); |
| 858 | 879 | ||
| 859 | // Framebuffers | 880 | self.destroySwapchainResources(); |
| 860 | for (self.framebuffers) |fb| self.vkd.destroyFramebuffer(self.device, fb, null); | ||
| 861 | self.alloc.free(self.framebuffers); | ||
| 862 | 881 | ||
| 863 | // Render pass | 882 | // Render pass |
| 864 | self.vkd.destroyRenderPass(self.device, self.render_pass, null); | 883 | self.vkd.destroyRenderPass(self.device, self.render_pass, null); |
| 865 | 884 | ||
| 866 | // Swapchain | 885 | self.vkd.destroyDevice(self.device, null); |
| 886 | self.vki.destroySurfaceKHR(self.instance, self.surface, null); | ||
| 887 | self.vki.destroyInstance(self.instance, null); | ||
| 888 | } | ||
| 889 | |||
| 890 | fn destroySwapchainResources(self: *Context) void { | ||
| 891 | for (self.framebuffers) |fb| self.vkd.destroyFramebuffer(self.device, fb, null); | ||
| 892 | self.alloc.free(self.framebuffers); | ||
| 893 | |||
| 867 | for (self.swapchain_image_views) |view| self.vkd.destroyImageView(self.device, view, null); | 894 | for (self.swapchain_image_views) |view| self.vkd.destroyImageView(self.device, view, null); |
| 868 | self.alloc.free(self.swapchain_image_views); | 895 | self.alloc.free(self.swapchain_image_views); |
| 869 | self.alloc.free(self.swapchain_images); | 896 | self.alloc.free(self.swapchain_images); |
| 870 | self.vkd.destroySwapchainKHR(self.device, self.swapchain, null); | 897 | self.vkd.destroySwapchainKHR(self.device, self.swapchain, null); |
| 898 | } | ||
| 871 | 899 | ||
| 872 | self.vkd.destroyDevice(self.device, null); | 900 | pub fn recreateSwapchain(self: *Context, width: u32, height: u32) !void { |
| 873 | self.vki.destroySurfaceKHR(self.instance, self.surface, null); | 901 | self.destroySwapchainResources(); |
| 874 | self.vki.destroyInstance(self.instance, null); | 902 | |
| 903 | const sc = try createSwapchain( | ||
| 904 | self.alloc, | ||
| 905 | self.vki, | ||
| 906 | self.vkd, | ||
| 907 | .{ | ||
| 908 | .physical = self.physical_device, | ||
| 909 | .graphics_queue_family = self.graphics_queue_family, | ||
| 910 | .present_queue_family = self.present_queue_family, | ||
| 911 | }, | ||
| 912 | self.surface, | ||
| 913 | self.device, | ||
| 914 | width, | ||
| 915 | height, | ||
| 916 | ); | ||
| 917 | errdefer { | ||
| 918 | for (sc.image_views) |view| self.vkd.destroyImageView(self.device, view, null); | ||
| 919 | self.alloc.free(sc.image_views); | ||
| 920 | self.alloc.free(sc.images); | ||
| 921 | self.vkd.destroySwapchainKHR(self.device, sc.swapchain, null); | ||
| 922 | } | ||
| 923 | |||
| 924 | const framebuffers = try createFramebuffers( | ||
| 925 | self.alloc, | ||
| 926 | self.vkd, | ||
| 927 | self.device, | ||
| 928 | self.render_pass, | ||
| 929 | sc.extent, | ||
| 930 | sc.image_views, | ||
| 931 | ); | ||
| 932 | errdefer { | ||
| 933 | for (framebuffers) |fb| self.vkd.destroyFramebuffer(self.device, fb, null); | ||
| 934 | self.alloc.free(framebuffers); | ||
| 935 | } | ||
| 936 | |||
| 937 | self.swapchain = sc.swapchain; | ||
| 938 | self.swapchain_format = sc.format; | ||
| 939 | self.swapchain_extent = sc.extent; | ||
| 940 | self.swapchain_images = sc.images; | ||
| 941 | self.swapchain_image_views = sc.image_views; | ||
| 942 | self.framebuffers = framebuffers; | ||
| 875 | } | 943 | } |
| 876 | 944 | ||
| 877 | /// Record a command buffer that begins the render pass with the given clear color and presents. | 945 | /// Record a command buffer that begins the render pass with the given clear color and presents. |