a73x

52933806

Honor wl_output buffer scale in text-compare mode

a73x   2026-04-09 13:17

runTextCoverageCompare now:
- Rasterizes the font at px_size * buffer_scale when entering a scaled
  output, via ScaledGeometry + rebuildFaceForScale.
- Calls wl_surface.set_buffer_scale() on scale changes and recreates the
  Vulkan swapchain at buffer-pixel dimensions.
- Respects xdg_toplevel.configure dimensions rather than overriding them.
  Previously it forced window.width = scene_cols * cell_w, which made
  sway non-integer-scale the buffer to fit its tile — that was the actual
  cause of the fuzz on the 2x Studio Display. Now we render the scene at
  its natural buffer-pixel cell size into whatever logical window size
  the compositor tells us to use.
- Bumps the atlas from 1024x1024 to 2048x2048 to fit 2x glyphs.

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

diff --git a/src/main.zig b/src/main.zig
index 959be9f..cc30caa 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -16,6 +16,35 @@ const GridSize = struct {
    rows: u16,
};

const ScaledGeometry = struct {
    buffer_scale: i32,
    px_size: u32,
    cell_w_px: u32, // buffer pixels
    cell_h_px: u32, // buffer pixels
    baseline_px: u32,
};

fn rebuildFaceForScale(
    face: *font.Face,
    atlas: *font.Atlas,
    font_path: [:0]const u8,
    font_index: c_int,
    base_px_size: u32,
    buffer_scale: i32,
) !ScaledGeometry {
    const scale: u32 = @intCast(@max(@as(i32, 1), buffer_scale));
    const new_px = base_px_size * scale;
    try face.reinit(font_path, font_index, new_px);
    atlas.reset();
    return .{
        .buffer_scale = @intCast(scale),
        .px_size = new_px,
        .cell_w_px = face.cellWidth(),
        .cell_h_px = face.cellHeight(),
        .baseline_px = face.baseline(),
    };
}

fn writePtyFromTerminal(_: *vt.Terminal, ctx: ?*anyopaque, data: []const u8) void {
    const p: *pty.Pty = @ptrCast(@alignCast(ctx orelse return));
    _ = p.write(data) catch |err| {
@@ -1771,23 +1800,35 @@ fn runTextCoverageCompare(alloc: std.mem.Allocator) !void {
    var face = try font.Face.init(alloc, font_lookup.path, font_lookup.index, config.font_size_px);
    defer face.deinit();

    var atlas = try font.Atlas.init(alloc, 1024, 1024);
    var atlas = try font.Atlas.init(alloc, 2048, 2048);
    defer atlas.deinit();

    var geom: ScaledGeometry = .{
        .buffer_scale = 1,
        .px_size = config.font_size_px,
        .cell_w_px = face.cellWidth(),
        .cell_h_px = face.cellHeight(),
        .baseline_px = face.baseline(),
    };

    var scene = try buildTextCoverageCompareScene(alloc, &face, &atlas);
    defer scene.deinit(alloc);

    const cell_w = face.cellWidth();
    const cell_h = face.cellHeight();
    window.width = scene.window_cols * cell_w;
    window.height = scene.window_rows * cell_h;
    // Initial surface-coordinate window size. Prefer whatever the compositor
    // configured us at (already stored on window by xdgToplevelListener). If
    // the initial configure was (0, 0) — meaning "client chooses" — use a
    // size that fits the scene exactly at scale=1.
    if (window.width == 800 and window.height == 600) {
        window.width = scene.window_cols * geom.cell_w_px;
        window.height = scene.window_rows * geom.cell_h_px;
    }

    var ctx = try renderer.Context.init(
        alloc,
        @ptrCast(conn.display),
        @ptrCast(window.surface),
        window.width,
        window.height,
        window.width * @as(u32, @intCast(geom.buffer_scale)),
        window.height * @as(u32, @intCast(geom.buffer_scale)),
    );
    defer ctx.deinit();

@@ -1801,6 +1842,7 @@ fn runTextCoverageCompare(alloc: std.mem.Allocator) !void {
    };
    var last_window_w = window.width;
    var last_window_h = window.height;
    var last_scale: i32 = geom.buffer_scale;

    while (!window.should_close) {
        _ = conn.display.flush();
@@ -1815,9 +1857,42 @@ fn runTextCoverageCompare(alloc: std.mem.Allocator) !void {
        }
        _ = conn.display.dispatchPending();

        if (window.width != last_window_w or window.height != last_window_h) {
        const current_scale = window.bufferScale();
        const scale_changed = current_scale != last_scale;
        const size_changed = window.width != last_window_w or window.height != last_window_h;

        if (scale_changed or size_changed) {
            _ = try ctx.vkd.deviceWaitIdle(ctx.device);
            try ctx.recreateSwapchain(window.width, window.height);

            if (scale_changed) {
                geom = try rebuildFaceForScale(
                    &face,
                    &atlas,
                    font_lookup.path,
                    font_lookup.index,
                    config.font_size_px,
                    current_scale,
                );
                // Rebuild the scene against the fresh atlas.
                scene.deinit(alloc);
                scene = try buildTextCoverageCompareScene(alloc, &face, &atlas);

                // Do NOT touch window.width/window.height here — those reflect the
                // compositor's configured surface size (from xdg_toplevel.configure).
                // Overwriting them forced sway to non-integer-scale our buffer to fit
                // its tile, which was the actual cause of the residual fuzz.

                window.surface.setBufferScale(geom.buffer_scale);
                try ctx.uploadAtlas(atlas.pixels);
                atlas.dirty = false;
                try ctx.uploadInstances(scene.instances.items);
                last_scale = current_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_window_w = window.width;
            last_window_h = window.height;
        }
@@ -1825,13 +1900,15 @@ fn runTextCoverageCompare(alloc: std.mem.Allocator) !void {
        drawTextCoverageCompareFrame(
            &ctx,
            &scene,
            cell_w,
            cell_h,
            geom.cell_w_px,
            geom.cell_h_px,
            .{ 0.0, 0.0, 0.0, 1.0 },
        ) 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);
                last_window_w = window.width;
                last_window_h = window.height;
                continue;