a73x

e4c26d38

Handle window resize

a73x   2026-04-08 11:40


diff --git a/src/main.zig b/src/main.zig
index 0b97c28..2096ccc 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -9,6 +9,11 @@ const c = @cImport({
    @cInclude("xkbcommon/xkbcommon-keysyms.h");
});

const GridSize = struct {
    cols: u16,
    rows: u16,
};

pub fn main() !void {
    var gpa: std.heap.DebugAllocator(.{}) = .init;
    defer _ = gpa.deinit();
@@ -53,8 +58,9 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
    const cell_h = face.cellHeight();

    // === grid size ===
    const cols: u16 = 80;
    const rows: u16 = 24;
    const initial_grid: GridSize = .{ .cols = 80, .rows = 24 };
    var cols: u16 = initial_grid.cols;
    var rows: u16 = initial_grid.rows;
    const initial_w: u32 = @as(u32, cols) * cell_w;
    const initial_h: u32 = @as(u32, rows) * cell_h;

@@ -124,6 +130,8 @@ fn runTerminal(alloc: std.mem.Allocator) !void {

    var read_buf: [8192]u8 = undefined;
    var key_buf: [32]u8 = undefined;
    var last_window_w = window.width;
    var last_window_h = window.height;

    while (!window.should_close and p.isChildAlive()) {
        // Flush any pending wayland requests
@@ -165,6 +173,23 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
        }
        keyboard.event_queue.clearRetainingCapacity();

        if (window.width != last_window_w or window.height != last_window_h) {
            const new_grid = gridSizeForWindow(window.width, window.height, cell_w, cell_h);
            if (new_grid.cols != cols or new_grid.rows != rows) {
                _ = try ctx.vkd.deviceWaitIdle(ctx.device);
                try ctx.recreateSwapchain(window.width, window.height);
                try term.resize(new_grid.cols, new_grid.rows);
                try p.resize(new_grid.cols, new_grid.rows);
                cols = new_grid.cols;
                rows = new_grid.rows;
            } else {
                _ = try ctx.vkd.deviceWaitIdle(ctx.device);
                try ctx.recreateSwapchain(window.width, window.height);
            }
            last_window_w = window.width;
            last_window_h = window.height;
        }

        // === render ===
        try term.snapshot();

@@ -216,6 +241,15 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
    _ = try ctx.vkd.deviceWaitIdle(ctx.device);
}

fn gridSizeForWindow(window_w: u32, window_h: u32, cell_w: u32, cell_h: u32) GridSize {
    const cols = @max(@as(u32, 1), if (cell_w == 0) 1 else window_w / cell_w);
    const rows = @max(@as(u32, 1), if (cell_h == 0) 1 else window_h / cell_h);
    return .{
        .cols = @intCast(cols),
        .rows = @intCast(rows),
    };
}

fn encodeKeyboardEvent(
    term: *const vt.Terminal,
    ev: wayland_client.KeyboardEvent,
@@ -374,6 +408,12 @@ test "encodeKeyboardEvent encodes left arrow" {
    try std.testing.expectEqualStrings("\x1b[D", encoded);
}

test "gridSizeForWindow clamps to at least one cell" {
    const grid = gridSizeForWindow(10, 5, 16, 20);
    try std.testing.expectEqual(@as(u16, 1), grid.cols);
    try std.testing.expectEqual(@as(u16, 1), grid.rows);
}

fn runRenderSmokeTest(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 fc2def4..12003ef 100644
--- a/src/renderer.zig
+++ b/src/renderer.zig
@@ -198,6 +198,36 @@ fn createSwapchain(
    };
}

fn createFramebuffers(
    alloc: std.mem.Allocator,
    vkd: vk.DeviceWrapper,
    device: vk.Device,
    render_pass: vk.RenderPass,
    extent: vk.Extent2D,
    image_views: []vk.ImageView,
) ![]vk.Framebuffer {
    const framebuffers = try alloc.alloc(vk.Framebuffer, image_views.len);
    errdefer alloc.free(framebuffers);
    var created: usize = 0;
    errdefer {
        for (framebuffers[0..created]) |fb| vkd.destroyFramebuffer(device, fb, null);
    }

    for (image_views, 0..) |view, i| {
        framebuffers[i] = try vkd.createFramebuffer(device, &vk.FramebufferCreateInfo{
            .render_pass = render_pass,
            .attachment_count = 1,
            .p_attachments = @ptrCast(&view),
            .width = extent.width,
            .height = extent.height,
            .layers = 1,
        }, null);
        created += 1;
    }

    return framebuffers;
}

/// Push constants layout matching cell.vert
pub const PushConstants = extern struct {
    viewport_size: [2]f32,
@@ -454,23 +484,14 @@ pub const Context = struct {
        errdefer vkd.destroyRenderPass(device, render_pass, null);

        // Create framebuffers (one per swapchain image view)
        const framebuffers = try alloc.alloc(vk.Framebuffer, sc.image_views.len);
        errdefer alloc.free(framebuffers);
        var fbs_created: usize = 0;
        errdefer {
            for (framebuffers[0..fbs_created]) |fb| vkd.destroyFramebuffer(device, fb, null);
        }
        for (sc.image_views, 0..) |view, i| {
            framebuffers[i] = try vkd.createFramebuffer(device, &vk.FramebufferCreateInfo{
                .render_pass = render_pass,
                .attachment_count = 1,
                .p_attachments = @ptrCast(&view),
                .width = sc.extent.width,
                .height = sc.extent.height,
                .layers = 1,
            }, null);
            fbs_created += 1;
        }
        const framebuffers = try createFramebuffers(
            alloc,
            vkd,
            device,
            render_pass,
            sc.extent,
            sc.image_views,
        );

        // Create descriptor set layout (single combined image sampler for glyph atlas)
        const dsl_binding = vk.DescriptorSetLayoutBinding{
@@ -856,22 +877,69 @@ pub const Context = struct {
        self.vkd.destroyDescriptorPool(self.device, self.descriptor_pool, null);
        self.vkd.destroyDescriptorSetLayout(self.device, self.descriptor_set_layout, null);

        // Framebuffers
        for (self.framebuffers) |fb| self.vkd.destroyFramebuffer(self.device, fb, null);
        self.alloc.free(self.framebuffers);
        self.destroySwapchainResources();

        // Render pass
        self.vkd.destroyRenderPass(self.device, self.render_pass, null);

        // Swapchain
        self.vkd.destroyDevice(self.device, null);
        self.vki.destroySurfaceKHR(self.instance, self.surface, null);
        self.vki.destroyInstance(self.instance, null);
    }

    fn destroySwapchainResources(self: *Context) void {
        for (self.framebuffers) |fb| self.vkd.destroyFramebuffer(self.device, fb, null);
        self.alloc.free(self.framebuffers);

        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);
    pub fn recreateSwapchain(self: *Context, width: u32, height: u32) !void {
        self.destroySwapchainResources();

        const sc = try createSwapchain(
            self.alloc,
            self.vki,
            self.vkd,
            .{
                .physical = self.physical_device,
                .graphics_queue_family = self.graphics_queue_family,
                .present_queue_family = self.present_queue_family,
            },
            self.surface,
            self.device,
            width,
            height,
        );
        errdefer {
            for (sc.image_views) |view| self.vkd.destroyImageView(self.device, view, null);
            self.alloc.free(sc.image_views);
            self.alloc.free(sc.images);
            self.vkd.destroySwapchainKHR(self.device, sc.swapchain, null);
        }

        const framebuffers = try createFramebuffers(
            self.alloc,
            self.vkd,
            self.device,
            self.render_pass,
            sc.extent,
            sc.image_views,
        );
        errdefer {
            for (framebuffers) |fb| self.vkd.destroyFramebuffer(self.device, fb, null);
            self.alloc.free(framebuffers);
        }

        self.swapchain = sc.swapchain;
        self.swapchain_format = sc.format;
        self.swapchain_extent = sc.extent;
        self.swapchain_images = sc.images;
        self.swapchain_image_views = sc.image_views;
        self.framebuffers = framebuffers;
    }

    /// Record a command buffer that begins the render pass with the given clear color and presents.