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;