a73x

52933806

Honor wl_output buffer scale in text-compare mode

a73x   2026-04-09 13:17

Commit message
Honor wl_output buffer scale in text-compare mode

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>

src/main.zig
Old New
@@ -16,6 +16,35 @@ const GridSize = struct {
16 rows: u16, 16 rows: u16,
17 }; 17 };
18 18
19 const ScaledGeometry = struct {
20 buffer_scale: i32,
21 px_size: u32,
22 cell_w_px: u32, // buffer pixels
23 cell_h_px: u32, // buffer pixels
24 baseline_px: u32,
25 };
26
27 fn rebuildFaceForScale(
28 face: *font.Face,
29 atlas: *font.Atlas,
30 font_path: [:0]const u8,
31 font_index: c_int,
32 base_px_size: u32,
33 buffer_scale: i32,
34 ) !ScaledGeometry {
35 const scale: u32 = @intCast(@max(@as(i32, 1), buffer_scale));
36 const new_px = base_px_size * scale;
37 try face.reinit(font_path, font_index, new_px);
38 atlas.reset();
39 return .{
40 .buffer_scale = @intCast(scale),
41 .px_size = new_px,
42 .cell_w_px = face.cellWidth(),
43 .cell_h_px = face.cellHeight(),
44 .baseline_px = face.baseline(),
45 };
46 }
47
19 fn writePtyFromTerminal(_: *vt.Terminal, ctx: ?*anyopaque, data: []const u8) void { 48 fn writePtyFromTerminal(_: *vt.Terminal, ctx: ?*anyopaque, data: []const u8) void {
20 const p: *pty.Pty = @ptrCast(@alignCast(ctx orelse return)); 49 const p: *pty.Pty = @ptrCast(@alignCast(ctx orelse return));
21 _ = p.write(data) catch |err| { 50 _ = p.write(data) catch |err| {
@@ -1771,23 +1800,35 @@ fn runTextCoverageCompare(alloc: std.mem.Allocator) !void {
1771 var face = try font.Face.init(alloc, font_lookup.path, font_lookup.index, config.font_size_px); 1800 var face = try font.Face.init(alloc, font_lookup.path, font_lookup.index, config.font_size_px);
1772 defer face.deinit(); 1801 defer face.deinit();
1773 1802
1774 var atlas = try font.Atlas.init(alloc, 1024, 1024); 1803 var atlas = try font.Atlas.init(alloc, 2048, 2048);
1775 defer atlas.deinit(); 1804 defer atlas.deinit();
1776 1805
1806 var geom: ScaledGeometry = .{
1807 .buffer_scale = 1,
1808 .px_size = config.font_size_px,
1809 .cell_w_px = face.cellWidth(),
1810 .cell_h_px = face.cellHeight(),
1811 .baseline_px = face.baseline(),
1812 };
1813
1777 var scene = try buildTextCoverageCompareScene(alloc, &face, &atlas); 1814 var scene = try buildTextCoverageCompareScene(alloc, &face, &atlas);
1778 defer scene.deinit(alloc); 1815 defer scene.deinit(alloc);
1779 1816
1780 const cell_w = face.cellWidth(); 1817 // Initial surface-coordinate window size. Prefer whatever the compositor
1781 const cell_h = face.cellHeight(); 1818 // configured us at (already stored on window by xdgToplevelListener). If
1782 window.width = scene.window_cols * cell_w; 1819 // the initial configure was (0, 0) — meaning "client chooses" — use a
1783 window.height = scene.window_rows * cell_h; 1820 // size that fits the scene exactly at scale=1.
1821 if (window.width == 800 and window.height == 600) {
1822 window.width = scene.window_cols * geom.cell_w_px;
1823 window.height = scene.window_rows * geom.cell_h_px;
1824 }
1784 1825
1785 var ctx = try renderer.Context.init( 1826 var ctx = try renderer.Context.init(
1786 alloc, 1827 alloc,
1787 @ptrCast(conn.display), 1828 @ptrCast(conn.display),
1788 @ptrCast(window.surface), 1829 @ptrCast(window.surface),
1789 window.width, 1830 window.width * @as(u32, @intCast(geom.buffer_scale)),
1790 window.height, 1831 window.height * @as(u32, @intCast(geom.buffer_scale)),
1791 ); 1832 );
1792 defer ctx.deinit(); 1833 defer ctx.deinit();
1793 1834
@@ -1801,6 +1842,7 @@ fn runTextCoverageCompare(alloc: std.mem.Allocator) !void {
1801 }; 1842 };
1802 var last_window_w = window.width; 1843 var last_window_w = window.width;
1803 var last_window_h = window.height; 1844 var last_window_h = window.height;
1845 var last_scale: i32 = geom.buffer_scale;
1804 1846
1805 while (!window.should_close) { 1847 while (!window.should_close) {
1806 _ = conn.display.flush(); 1848 _ = conn.display.flush();
@@ -1815,9 +1857,42 @@ fn runTextCoverageCompare(alloc: std.mem.Allocator) !void {
1815 } 1857 }
1816 _ = conn.display.dispatchPending(); 1858 _ = conn.display.dispatchPending();
1817 1859
1818 if (window.width != last_window_w or window.height != last_window_h) { 1860 const current_scale = window.bufferScale();
1861 const scale_changed = current_scale != last_scale;
1862 const size_changed = window.width != last_window_w or window.height != last_window_h;
1863
1864 if (scale_changed or size_changed) {
1819 _ = try ctx.vkd.deviceWaitIdle(ctx.device); 1865 _ = try ctx.vkd.deviceWaitIdle(ctx.device);
1820 try ctx.recreateSwapchain(window.width, window.height); 1866
1867 if (scale_changed) {
1868 geom = try rebuildFaceForScale(
1869 &face,
1870 &atlas,
1871 font_lookup.path,
1872 font_lookup.index,
1873 config.font_size_px,
1874 current_scale,
1875 );
1876 // Rebuild the scene against the fresh atlas.
1877 scene.deinit(alloc);
1878 scene = try buildTextCoverageCompareScene(alloc, &face, &atlas);
1879
1880 // Do NOT touch window.width/window.height here — those reflect the
1881 // compositor's configured surface size (from xdg_toplevel.configure).
1882 // Overwriting them forced sway to non-integer-scale our buffer to fit
1883 // its tile, which was the actual cause of the residual fuzz.
1884
1885 window.surface.setBufferScale(geom.buffer_scale);
1886 try ctx.uploadAtlas(atlas.pixels);
1887 atlas.dirty = false;
1888 try ctx.uploadInstances(scene.instances.items);
1889 last_scale = current_scale;
1890 }
1891
1892 const buf_w = window.width * @as(u32, @intCast(geom.buffer_scale));
1893 const buf_h = window.height * @as(u32, @intCast(geom.buffer_scale));
1894 try ctx.recreateSwapchain(buf_w, buf_h);
1895
1821 last_window_w = window.width; 1896 last_window_w = window.width;
1822 last_window_h = window.height; 1897 last_window_h = window.height;
1823 } 1898 }
@@ -1825,13 +1900,15 @@ fn runTextCoverageCompare(alloc: std.mem.Allocator) !void {
1825 drawTextCoverageCompareFrame( 1900 drawTextCoverageCompareFrame(
1826 &ctx, 1901 &ctx,
1827 &scene, 1902 &scene,
1828 cell_w, 1903 geom.cell_w_px,
1829 cell_h, 1904 geom.cell_h_px,
1830 .{ 0.0, 0.0, 0.0, 1.0 }, 1905 .{ 0.0, 0.0, 0.0, 1.0 },
1831 ) catch |err| switch (err) { 1906 ) catch |err| switch (err) {
1832 error.OutOfDateKHR => { 1907 error.OutOfDateKHR => {
1833 _ = try ctx.vkd.deviceWaitIdle(ctx.device); 1908 _ = try ctx.vkd.deviceWaitIdle(ctx.device);
1834 try ctx.recreateSwapchain(window.width, window.height); 1909 const buf_w = window.width * @as(u32, @intCast(geom.buffer_scale));
1910 const buf_h = window.height * @as(u32, @intCast(geom.buffer_scale));
1911 try ctx.recreateSwapchain(buf_w, buf_h);
1835 last_window_w = window.width; 1912 last_window_w = window.width;
1836 last_window_h = window.height; 1913 last_window_h = window.height;
1837 continue; 1914 continue;