a73x

ce21fd10

Honor wl_output buffer scale in terminal render loop

a73x   2026-04-09 13:24

runTerminal now reacts to wl_surface.enter/leave scale changes by:
- Rebuilding font.Face at px_size * buffer_scale via rebuildFaceForScale
- Resetting the glyph atlas and marking the render cache + terminal
  fully dirty so all rows get re-rasterized against the fresh atlas
- Calling wl_surface.set_buffer_scale() and recreating the Vulkan
  swapchain at buffer-pixel dimensions
- Computing the terminal grid from surface-pixel cell sizes
  (cell_w / buffer_scale) so drag-between-monitors preserves cols/rows

cell_w/cell_h/baseline are now mutable aliases for geom fields, kept
in sync after each rebuildFaceForScale call.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

diff --git a/src/main.zig b/src/main.zig
index cc30caa..7baa943 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -102,9 +102,19 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
    var face = try font.Face.init(alloc, font_lookup.path, font_lookup.index, font_size);
    defer face.deinit();

    const cell_w = face.cellWidth();
    const cell_h = face.cellHeight();
    const baseline = face.baseline();
    var geom: ScaledGeometry = .{
        .buffer_scale = 1,
        .px_size = font_size,
        .cell_w_px = face.cellWidth(),
        .cell_h_px = face.cellHeight(),
        .baseline_px = face.baseline(),
    };
    // Mutable aliases so the scale-change path inside the main loop can
    // refresh them after a rebuildFaceForScale call without renaming every
    // downstream reference.
    var cell_w = geom.cell_w_px;
    var cell_h = geom.cell_h_px;
    var baseline = geom.baseline_px;

    // === grid size ===
    const initial_grid: GridSize = .{ .cols = 80, .rows = 24 };
@@ -196,6 +206,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
    var key_buf: [32]u8 = undefined;
    var last_window_w = window.width;
    var last_window_h = window.height;
    var last_scale: i32 = geom.buffer_scale;
    var render_pending = true;

    while (!window.should_close and p.isChildAlive()) {
@@ -252,11 +263,48 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
        }
        keyboard.event_queue.clearRetainingCapacity();

        const current_scale = window.bufferScale();
        if (current_scale != last_scale) {
            _ = try ctx.vkd.deviceWaitIdle(ctx.device);

            geom = try rebuildFaceForScale(
                &face,
                &atlas,
                font_lookup.path,
                font_lookup.index,
                font_size,
                current_scale,
            );
            cell_w = geom.cell_w_px;
            cell_h = geom.cell_h_px;
            baseline = geom.baseline_px;

            // Wipe all cached row instances + mark the terminal fully dirty so that
            // every glyph gets re-inserted into the freshly reset atlas next frame.
            render_cache.invalidateAfterResize();
            term.render_state.dirty = .full;

            window.surface.setBufferScale(geom.buffer_scale);

            const buf_w = window.width * @as(u32, @intCast(geom.buffer_scale));
            const buf_h = window.height * @as(u32, @intCast(geom.buffer_scale));
            try ctx.recreateSwapchain(buf_w, buf_h);

            last_scale = current_scale;
            render_pending = true;
        }

        if (window.width != last_window_w or window.height != last_window_h) {
            const new_grid = gridSizeForWindow(window.width, window.height, cell_w, cell_h);
            // Grid is sized in surface coordinates. cell_w/cell_h are in buffer
            // pixels, so divide by buffer_scale to get surface-pixel cell dims.
            const surf_cell_w = cell_w / @as(u32, @intCast(geom.buffer_scale));
            const surf_cell_h = cell_h / @as(u32, @intCast(geom.buffer_scale));
            const new_grid = gridSizeForWindow(window.width, window.height, surf_cell_w, surf_cell_h);
            const buf_w = window.width * @as(u32, @intCast(geom.buffer_scale));
            const buf_h = window.height * @as(u32, @intCast(geom.buffer_scale));
            if (new_grid.cols != cols or new_grid.rows != rows) {
                _ = try ctx.vkd.deviceWaitIdle(ctx.device);
                try ctx.recreateSwapchain(window.width, window.height);
                try ctx.recreateSwapchain(buf_w, buf_h);
                try term.resize(new_grid.cols, new_grid.rows);
                try p.resize(new_grid.cols, new_grid.rows);
                cols = new_grid.cols;
@@ -269,7 +317,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
                });
            } else {
                _ = try ctx.vkd.deviceWaitIdle(ctx.device);
                try ctx.recreateSwapchain(window.width, window.height);
                try ctx.recreateSwapchain(buf_w, buf_h);
            }
            last_window_w = window.width;
            last_window_h = window.height;
@@ -445,7 +493,9 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
        ) catch |err| switch (err) {
            error.OutOfDateKHR => {
                _ = try ctx.vkd.deviceWaitIdle(ctx.device);
                try ctx.recreateSwapchain(window.width, window.height);
                const buf_w = window.width * @as(u32, @intCast(geom.buffer_scale));
                const buf_h = window.height * @as(u32, @intCast(geom.buffer_scale));
                try ctx.recreateSwapchain(buf_w, buf_h);
                render_pending = true;
                continue;
            },