a73x

ce21fd10

Honor wl_output buffer scale in terminal render loop

a73x   2026-04-09 13:24

Commit message
Honor wl_output buffer scale in terminal render loop

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>

src/main.zig
Old New
@@ -102,9 +102,19 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
102 var face = try font.Face.init(alloc, font_lookup.path, font_lookup.index, font_size); 102 var face = try font.Face.init(alloc, font_lookup.path, font_lookup.index, font_size);
103 defer face.deinit(); 103 defer face.deinit();
104 104
105 const cell_w = face.cellWidth(); 105 var geom: ScaledGeometry = .{
106 const cell_h = face.cellHeight(); 106 .buffer_scale = 1,
107 const baseline = face.baseline(); 107 .px_size = font_size,
108 .cell_w_px = face.cellWidth(),
109 .cell_h_px = face.cellHeight(),
110 .baseline_px = face.baseline(),
111 };
112 // Mutable aliases so the scale-change path inside the main loop can
113 // refresh them after a rebuildFaceForScale call without renaming every
114 // downstream reference.
115 var cell_w = geom.cell_w_px;
116 var cell_h = geom.cell_h_px;
117 var baseline = geom.baseline_px;
108 118
109 // === grid size === 119 // === grid size ===
110 const initial_grid: GridSize = .{ .cols = 80, .rows = 24 }; 120 const initial_grid: GridSize = .{ .cols = 80, .rows = 24 };
@@ -196,6 +206,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
196 var key_buf: [32]u8 = undefined; 206 var key_buf: [32]u8 = undefined;
197 var last_window_w = window.width; 207 var last_window_w = window.width;
198 var last_window_h = window.height; 208 var last_window_h = window.height;
209 var last_scale: i32 = geom.buffer_scale;
199 var render_pending = true; 210 var render_pending = true;
200 211
201 while (!window.should_close and p.isChildAlive()) { 212 while (!window.should_close and p.isChildAlive()) {
@@ -252,11 +263,48 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
252 } 263 }
253 keyboard.event_queue.clearRetainingCapacity(); 264 keyboard.event_queue.clearRetainingCapacity();
254 265
266 const current_scale = window.bufferScale();
267 if (current_scale != last_scale) {
268 _ = try ctx.vkd.deviceWaitIdle(ctx.device);
269
270 geom = try rebuildFaceForScale(
271 &face,
272 &atlas,
273 font_lookup.path,
274 font_lookup.index,
275 font_size,
276 current_scale,
277 );
278 cell_w = geom.cell_w_px;
279 cell_h = geom.cell_h_px;
280 baseline = geom.baseline_px;
281
282 // Wipe all cached row instances + mark the terminal fully dirty so that
283 // every glyph gets re-inserted into the freshly reset atlas next frame.
284 render_cache.invalidateAfterResize();
285 term.render_state.dirty = .full;
286
287 window.surface.setBufferScale(geom.buffer_scale);
288
289 const buf_w = window.width * @as(u32, @intCast(geom.buffer_scale));
290 const buf_h = window.height * @as(u32, @intCast(geom.buffer_scale));
291 try ctx.recreateSwapchain(buf_w, buf_h);
292
293 last_scale = current_scale;
294 render_pending = true;
295 }
296
255 if (window.width != last_window_w or window.height != last_window_h) { 297 if (window.width != last_window_w or window.height != last_window_h) {
256 const new_grid = gridSizeForWindow(window.width, window.height, cell_w, cell_h); 298 // Grid is sized in surface coordinates. cell_w/cell_h are in buffer
299 // pixels, so divide by buffer_scale to get surface-pixel cell dims.
300 const surf_cell_w = cell_w / @as(u32, @intCast(geom.buffer_scale));
301 const surf_cell_h = cell_h / @as(u32, @intCast(geom.buffer_scale));
302 const new_grid = gridSizeForWindow(window.width, window.height, surf_cell_w, surf_cell_h);
303 const buf_w = window.width * @as(u32, @intCast(geom.buffer_scale));
304 const buf_h = window.height * @as(u32, @intCast(geom.buffer_scale));
257 if (new_grid.cols != cols or new_grid.rows != rows) { 305 if (new_grid.cols != cols or new_grid.rows != rows) {
258 _ = try ctx.vkd.deviceWaitIdle(ctx.device); 306 _ = try ctx.vkd.deviceWaitIdle(ctx.device);
259 try ctx.recreateSwapchain(window.width, window.height); 307 try ctx.recreateSwapchain(buf_w, buf_h);
260 try term.resize(new_grid.cols, new_grid.rows); 308 try term.resize(new_grid.cols, new_grid.rows);
261 try p.resize(new_grid.cols, new_grid.rows); 309 try p.resize(new_grid.cols, new_grid.rows);
262 cols = new_grid.cols; 310 cols = new_grid.cols;
@@ -269,7 +317,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
269 }); 317 });
270 } else { 318 } else {
271 _ = try ctx.vkd.deviceWaitIdle(ctx.device); 319 _ = try ctx.vkd.deviceWaitIdle(ctx.device);
272 try ctx.recreateSwapchain(window.width, window.height); 320 try ctx.recreateSwapchain(buf_w, buf_h);
273 } 321 }
274 last_window_w = window.width; 322 last_window_w = window.width;
275 last_window_h = window.height; 323 last_window_h = window.height;
@@ -445,7 +493,9 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
445 ) catch |err| switch (err) { 493 ) catch |err| switch (err) {
446 error.OutOfDateKHR => { 494 error.OutOfDateKHR => {
447 _ = try ctx.vkd.deviceWaitIdle(ctx.device); 495 _ = try ctx.vkd.deviceWaitIdle(ctx.device);
448 try ctx.recreateSwapchain(window.width, window.height); 496 const buf_w = window.width * @as(u32, @intCast(geom.buffer_scale));
497 const buf_h = window.height * @as(u32, @intCast(geom.buffer_scale));
498 try ctx.recreateSwapchain(buf_w, buf_h);
449 render_pending = true; 499 render_pending = true;
450 continue; 500 continue;
451 }, 501 },