a73x

eb4ff88c

Polish frame-loop visibility handling

a73x   2026-04-17 16:45

Commit message
Polish frame-loop visibility handling

wayland: extract Window.captureVisibility / notifyVisibilityChange
helpers to consolidate the before/after visibility-change detection
pattern shared by xdgSurfaceListener, surfaceListener, and
xdgToplevelListener.

main: drop unused shouldRenderFrame helper + its test; hoist
is_bench and bench_unthrottled into locals (one env lookup each);
use current_scale in the scale-pending path instead of re-reading
window.bufferScale(); tolerate commitRender errors rather than
gating on frame_loop.canRender(); fix var->const on unused stdin
binding.

renderer: remove makeBaseLoader (unused pass-through).

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

src/main.zig
Old New
@@ -202,24 +202,22 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
202 }); 202 });
203 203
204 // === pty === 204 // === pty ===
205 const is_bench = std.posix.getenv("WAYSTTY_BENCH") != null;
206 const bench_unthrottled = is_bench and std.posix.getenv("WAYSTTY_BENCH_UNTHROTTLED") != null;
207
205 const shell: [:0]const u8 = blk: { 208 const shell: [:0]const u8 = blk: {
206 if (std.posix.getenv("WAYSTTY_BENCH") != null) { 209 if (is_bench) break :blk try alloc.dupeZ(u8, "/bin/sh");
207 break :blk try alloc.dupeZ(u8, "/bin/sh");
208 }
209 const shell_env = std.posix.getenv("SHELL") orelse "/bin/sh"; 210 const shell_env = std.posix.getenv("SHELL") orelse "/bin/sh";
210 break :blk try alloc.dupeZ(u8, shell_env); 211 break :blk try alloc.dupeZ(u8, shell_env);
211 }; 212 };
212 defer alloc.free(shell); 213 defer alloc.free(shell);
213 214
214 const bench_script: ?[:0]const u8 = if (std.posix.getenv("WAYSTTY_BENCH") != null) 215 const bench_script: ?[:0]const u8 = if (is_bench)
215 "echo warmup; sleep 0.2; seq 1 50000; find /usr/lib -name '*.so' 2>/dev/null | head -500; yes 'hello world' | head -2000; exit 0" 216 "echo warmup; sleep 0.2; seq 1 50000; find /usr/lib -name '*.so' 2>/dev/null | head -500; yes 'hello world' | head -2000; exit 0"
216 else 217 else
217 null; 218 null;
218 219
219 const bench_unthrottled = std.posix.getenv("WAYSTTY_BENCH") != null 220 if (is_bench) {
220 and std.posix.getenv("WAYSTTY_BENCH_UNTHROTTLED") != null;
221
222 if (std.posix.getenv("WAYSTTY_BENCH") != null) {
223 if (bench_unthrottled) { 221 if (bench_unthrottled) {
224 std.debug.print("[bench] mode: UNTHROTTLED (not freeze-safe)\n", .{}); 222 std.debug.print("[bench] mode: UNTHROTTLED (not freeze-safe)\n", .{});
225 } else { 223 } else {
@@ -349,9 +347,8 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
349 printFrameStats(computeFrameStats(&frame_ring)); 347 printFrameStats(computeFrameStats(&frame_ring));
350 } 348 }
351 349
352 if (!shouldRenderFrame(render_pending, false, false)) continue; 350 if (!render_pending) continue;
353 351
354 // applyPendingScale — Vulkan work, gated on canRender().
355 if (scale_pending) { 352 if (scale_pending) {
356 _ = try ctx.vkd.deviceWaitIdle(ctx.device); 353 _ = try ctx.vkd.deviceWaitIdle(ctx.device);
357 354
@@ -361,7 +358,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
361 font_lookup.path, 358 font_lookup.path,
362 font_lookup.index, 359 font_lookup.index,
363 font_size, 360 font_size,
364 window.bufferScale(), 361 current_scale,
365 ); 362 );
366 cell_w = geom.cell_w_px; 363 cell_w = geom.cell_w_px;
367 cell_h = geom.cell_h_px; 364 cell_h = geom.cell_h_px;
@@ -382,7 +379,6 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
382 scale_pending = false; 379 scale_pending = false;
383 } 380 }
384 381
385 // applyPendingResize — Vulkan work, gated on canRender().
386 if (resize_pending) { 382 if (resize_pending) {
387 // Grid is sized in surface coordinates. cell_w/cell_h are in buffer 383 // Grid is sized in surface coordinates. cell_w/cell_h are in buffer
388 // pixels, so divide by buffer_scale to get surface-pixel cell dims. 384 // pixels, so divide by buffer_scale to get surface-pixel cell dims.
@@ -624,7 +620,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
624 frame_ring.push(frame_timing); 620 frame_ring.push(frame_timing);
625 621
626 clearConsumedDirtyFlags(&term.render_state.dirty, dirty_rows, refresh_plan); 622 clearConsumedDirtyFlags(&term.render_state.dirty, dirty_rows, refresh_plan);
627 if (!bench_unthrottled and frame_loop.canRender()) try frame_loop.commitRender(); 623 if (!bench_unthrottled) frame_loop.commitRender() catch {};
628 render_pending = false; 624 render_pending = false;
629 } 625 }
630 626
@@ -692,10 +688,6 @@ fn computePollTimeoutMs(next_repeat_in_ms: ?i32, render_pending: bool) i32 {
692 return next_repeat_in_ms orelse -1; 688 return next_repeat_in_ms orelse -1;
693 } 689 }
694 690
695 fn shouldRenderFrame(terminal_dirty: bool, window_dirty: bool, forced: bool) bool {
696 return terminal_dirty or window_dirty or forced;
697 }
698
699 fn extractSelectedText( 691 fn extractSelectedText(
700 alloc: std.mem.Allocator, 692 alloc: std.mem.Allocator,
701 row_data: anytype, 693 row_data: anytype,
@@ -1708,13 +1700,6 @@ test "event loop waits indefinitely when idle and wakes for imminent repeat" {
1708 try std.testing.expectEqual(@as(i32, 17), computePollTimeoutMs(17, false)); 1700 try std.testing.expectEqual(@as(i32, 17), computePollTimeoutMs(17, false));
1709 } 1701 }
1710 1702
1711 test "event loop redraws only when terminal or window state changed" {
1712 try std.testing.expect(shouldRenderFrame(true, false, false));
1713 try std.testing.expect(shouldRenderFrame(false, true, false));
1714 try std.testing.expect(shouldRenderFrame(false, false, true));
1715 try std.testing.expect(!shouldRenderFrame(false, false, false));
1716 }
1717
1718 test "planRowRefresh requests full rebuild for full dirty state" { 1703 test "planRowRefresh requests full rebuild for full dirty state" {
1719 const plan = planRowRefresh(.full, &.{ false, true, false }, .{ 1704 const plan = planRowRefresh(.full, &.{ false, true, false }, .{
1720 .cursor = .{ 1705 .cursor = .{
@@ -3388,7 +3373,7 @@ fn runHiddenFreezeRegression(alloc: std.mem.Allocator) !void {
3388 \\ 3373 \\
3389 ); 3374 );
3390 3375
3391 var stdin_file = std.fs.File.stdin(); 3376 const stdin_file = std.fs.File.stdin();
3392 var buf: [1]u8 = undefined; 3377 var buf: [1]u8 = undefined;
3393 _ = try stdin_file.read(&buf); 3378 _ = try stdin_file.read(&buf);
3394 3379
src/renderer.zig
Old New
@@ -19,12 +19,6 @@ fn getVkGetInstanceProcAddr() !vk.PfnGetInstanceProcAddr {
19 return @ptrCast(@alignCast(sym)); 19 return @ptrCast(@alignCast(sym));
20 } 20 }
21 21
22 // Wrap the raw PfnGetInstanceProcAddr so it matches the anytype loader signature
23 // expected by BaseWrapper.load (accepts instance + name, returns optional fn ptr).
24 fn makeBaseLoader(pfn: vk.PfnGetInstanceProcAddr) vk.PfnGetInstanceProcAddr {
25 return pfn;
26 }
27
28 const PhysicalDeviceInfo = struct { 22 const PhysicalDeviceInfo = struct {
29 physical: vk.PhysicalDevice, 23 physical: vk.PhysicalDevice,
30 graphics_queue_family: u32, 24 graphics_queue_family: u32,
src/wayland.zig
Old New
@@ -686,6 +686,20 @@ pub const Window = struct {
686 .tracker = self.tracker, 686 .tracker = self.tracker,
687 }; 687 };
688 } 688 }
689
690 /// Call before dispatching an event that may change visibility, then call
691 /// notifyVisibilityChange with the returned value after the event is handled.
692 pub fn captureVisibility(self: *const Window) bool {
693 return self.state.visible();
694 }
695
696 pub fn notifyVisibilityChange(self: *Window, was_visible: bool) void {
697 const now_visible = self.state.visible();
698 if (self.frame_loop) |loop| {
699 if (was_visible and !now_visible) loop.onSurfaceHidden();
700 if (!was_visible and now_visible) loop.onSurfaceShown();
701 }
702 }
689 }; 703 };
690 704
691 pub const Connection = struct { 705 pub const Connection = struct {
@@ -1138,22 +1152,18 @@ fn wmBaseListener(wm_base: *xdg.WmBase, event: xdg.WmBase.Event, _: *xdg.WmBase)
1138 } 1152 }
1139 1153
1140 fn xdgSurfaceListener(surface: *xdg.Surface, event: xdg.Surface.Event, window: *Window) void { 1154 fn xdgSurfaceListener(surface: *xdg.Surface, event: xdg.Surface.Event, window: *Window) void {
1141 const was_visible = window.state.visible(); 1155 const was_visible = window.captureVisibility();
1142 switch (event) { 1156 switch (event) {
1143 .configure => |cfg| { 1157 .configure => |cfg| {
1144 surface.ackConfigure(cfg.serial); 1158 surface.ackConfigure(cfg.serial);
1145 window.state.configured = true; 1159 window.state.configured = true;
1146 }, 1160 },
1147 } 1161 }
1148 const now_visible = window.state.visible(); 1162 window.notifyVisibilityChange(was_visible);
1149 if (window.frame_loop) |loop| {
1150 if (was_visible and !now_visible) loop.onSurfaceHidden();
1151 if (!was_visible and now_visible) loop.onSurfaceShown();
1152 }
1153 } 1163 }
1154 1164
1155 fn surfaceListener(_: *wl.Surface, event: wl.Surface.Event, window: *Window) void { 1165 fn surfaceListener(_: *wl.Surface, event: wl.Surface.Event, window: *Window) void {
1156 const was_visible = window.state.visible(); 1166 const was_visible = window.captureVisibility();
1157 switch (event) { 1167 switch (event) {
1158 .enter => |e| { 1168 .enter => |e| {
1159 if (e.output) |wl_out| { 1169 if (e.output) |wl_out| {
@@ -1170,11 +1180,7 @@ fn surfaceListener(_: *wl.Surface, event: wl.Surface.Event, window: *Window) voi
1170 .preferred_buffer_scale => {}, 1180 .preferred_buffer_scale => {},
1171 .preferred_buffer_transform => {}, 1181 .preferred_buffer_transform => {},
1172 } 1182 }
1173 const now_visible = window.state.visible(); 1183 window.notifyVisibilityChange(was_visible);
1174 if (window.frame_loop) |loop| {
1175 if (was_visible and !now_visible) loop.onSurfaceHidden();
1176 if (!was_visible and now_visible) loop.onSurfaceShown();
1177 }
1178 } 1184 }
1179 1185
1180 fn applyToplevelStates(state: *SurfaceState, states: []const u32) void { 1186 fn applyToplevelStates(state: *SurfaceState, states: []const u32) void {
@@ -1189,7 +1195,7 @@ fn applyToplevelStates(state: *SurfaceState, states: []const u32) void {
1189 } 1195 }
1190 1196
1191 fn xdgToplevelListener(_: *xdg.Toplevel, event: xdg.Toplevel.Event, window: *Window) void { 1197 fn xdgToplevelListener(_: *xdg.Toplevel, event: xdg.Toplevel.Event, window: *Window) void {
1192 const was_visible = window.state.visible(); 1198 const was_visible = window.captureVisibility();
1193 switch (event) { 1199 switch (event) {
1194 .configure => |cfg| { 1200 .configure => |cfg| {
1195 if (cfg.width > 0) window.width = @intCast(cfg.width); 1201 if (cfg.width > 0) window.width = @intCast(cfg.width);
@@ -1200,11 +1206,7 @@ fn xdgToplevelListener(_: *xdg.Toplevel, event: xdg.Toplevel.Event, window: *Win
1200 .configure_bounds => {}, 1206 .configure_bounds => {},
1201 .wm_capabilities => {}, 1207 .wm_capabilities => {},
1202 } 1208 }
1203 const now_visible = window.state.visible(); 1209 window.notifyVisibilityChange(was_visible);
1204 if (window.frame_loop) |loop| {
1205 if (was_visible and !now_visible) loop.onSurfaceHidden();
1206 if (!was_visible and now_visible) loop.onSurfaceShown();
1207 }
1208 } 1210 }
1209 1211
1210 fn registryListener( 1212 fn registryListener(