eb4ff88c
Polish frame-loop visibility handling
a73x 2026-04-17 16:45
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>
diff --git a/src/main.zig b/src/main.zig index 083552b..b25e71e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -202,24 +202,22 @@ fn runTerminal(alloc: std.mem.Allocator) !void { }); // === pty === const is_bench = std.posix.getenv("WAYSTTY_BENCH") != null; const bench_unthrottled = is_bench and std.posix.getenv("WAYSTTY_BENCH_UNTHROTTLED") != null; const shell: [:0]const u8 = blk: { if (std.posix.getenv("WAYSTTY_BENCH") != null) { break :blk try alloc.dupeZ(u8, "/bin/sh"); } if (is_bench) break :blk try alloc.dupeZ(u8, "/bin/sh"); const shell_env = std.posix.getenv("SHELL") orelse "/bin/sh"; break :blk try alloc.dupeZ(u8, shell_env); }; defer alloc.free(shell); const bench_script: ?[:0]const u8 = if (std.posix.getenv("WAYSTTY_BENCH") != null) const bench_script: ?[:0]const u8 = if (is_bench) "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" else null; const bench_unthrottled = std.posix.getenv("WAYSTTY_BENCH") != null and std.posix.getenv("WAYSTTY_BENCH_UNTHROTTLED") != null; if (std.posix.getenv("WAYSTTY_BENCH") != null) { if (is_bench) { if (bench_unthrottled) { std.debug.print("[bench] mode: UNTHROTTLED (not freeze-safe)\n", .{}); } else { @@ -349,9 +347,8 @@ fn runTerminal(alloc: std.mem.Allocator) !void { printFrameStats(computeFrameStats(&frame_ring)); } if (!shouldRenderFrame(render_pending, false, false)) continue; if (!render_pending) continue; // applyPendingScale — Vulkan work, gated on canRender(). if (scale_pending) { _ = try ctx.vkd.deviceWaitIdle(ctx.device); @@ -361,7 +358,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void { font_lookup.path, font_lookup.index, font_size, window.bufferScale(), current_scale, ); cell_w = geom.cell_w_px; cell_h = geom.cell_h_px; @@ -382,7 +379,6 @@ fn runTerminal(alloc: std.mem.Allocator) !void { scale_pending = false; } // applyPendingResize — Vulkan work, gated on canRender(). if (resize_pending) { // Grid is sized in surface coordinates. cell_w/cell_h are in buffer // pixels, so divide by buffer_scale to get surface-pixel cell dims. @@ -624,7 +620,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void { frame_ring.push(frame_timing); clearConsumedDirtyFlags(&term.render_state.dirty, dirty_rows, refresh_plan); if (!bench_unthrottled and frame_loop.canRender()) try frame_loop.commitRender(); if (!bench_unthrottled) frame_loop.commitRender() catch {}; render_pending = false; } @@ -692,10 +688,6 @@ fn computePollTimeoutMs(next_repeat_in_ms: ?i32, render_pending: bool) i32 { return next_repeat_in_ms orelse -1; } fn shouldRenderFrame(terminal_dirty: bool, window_dirty: bool, forced: bool) bool { return terminal_dirty or window_dirty or forced; } fn extractSelectedText( alloc: std.mem.Allocator, row_data: anytype, @@ -1708,13 +1700,6 @@ test "event loop waits indefinitely when idle and wakes for imminent repeat" { try std.testing.expectEqual(@as(i32, 17), computePollTimeoutMs(17, false)); } test "event loop redraws only when terminal or window state changed" { try std.testing.expect(shouldRenderFrame(true, false, false)); try std.testing.expect(shouldRenderFrame(false, true, false)); try std.testing.expect(shouldRenderFrame(false, false, true)); try std.testing.expect(!shouldRenderFrame(false, false, false)); } test "planRowRefresh requests full rebuild for full dirty state" { const plan = planRowRefresh(.full, &.{ false, true, false }, .{ .cursor = .{ @@ -3388,7 +3373,7 @@ fn runHiddenFreezeRegression(alloc: std.mem.Allocator) !void { \\ ); var stdin_file = std.fs.File.stdin(); const stdin_file = std.fs.File.stdin(); var buf: [1]u8 = undefined; _ = try stdin_file.read(&buf); diff --git a/src/renderer.zig b/src/renderer.zig index 3ee0f2b..9f5f458 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -19,12 +19,6 @@ fn getVkGetInstanceProcAddr() !vk.PfnGetInstanceProcAddr { return @ptrCast(@alignCast(sym)); } // Wrap the raw PfnGetInstanceProcAddr so it matches the anytype loader signature // expected by BaseWrapper.load (accepts instance + name, returns optional fn ptr). fn makeBaseLoader(pfn: vk.PfnGetInstanceProcAddr) vk.PfnGetInstanceProcAddr { return pfn; } const PhysicalDeviceInfo = struct { physical: vk.PhysicalDevice, graphics_queue_family: u32, diff --git a/src/wayland.zig b/src/wayland.zig index fef2858..51a2c05 100644 --- a/src/wayland.zig +++ b/src/wayland.zig @@ -686,6 +686,20 @@ pub const Window = struct { .tracker = self.tracker, }; } /// Call before dispatching an event that may change visibility, then call /// notifyVisibilityChange with the returned value after the event is handled. pub fn captureVisibility(self: *const Window) bool { return self.state.visible(); } pub fn notifyVisibilityChange(self: *Window, was_visible: bool) void { const now_visible = self.state.visible(); if (self.frame_loop) |loop| { if (was_visible and !now_visible) loop.onSurfaceHidden(); if (!was_visible and now_visible) loop.onSurfaceShown(); } } }; pub const Connection = struct { @@ -1138,22 +1152,18 @@ fn wmBaseListener(wm_base: *xdg.WmBase, event: xdg.WmBase.Event, _: *xdg.WmBase) } fn xdgSurfaceListener(surface: *xdg.Surface, event: xdg.Surface.Event, window: *Window) void { const was_visible = window.state.visible(); const was_visible = window.captureVisibility(); switch (event) { .configure => |cfg| { surface.ackConfigure(cfg.serial); window.state.configured = true; }, } const now_visible = window.state.visible(); if (window.frame_loop) |loop| { if (was_visible and !now_visible) loop.onSurfaceHidden(); if (!was_visible and now_visible) loop.onSurfaceShown(); } window.notifyVisibilityChange(was_visible); } fn surfaceListener(_: *wl.Surface, event: wl.Surface.Event, window: *Window) void { const was_visible = window.state.visible(); const was_visible = window.captureVisibility(); switch (event) { .enter => |e| { if (e.output) |wl_out| { @@ -1170,11 +1180,7 @@ fn surfaceListener(_: *wl.Surface, event: wl.Surface.Event, window: *Window) voi .preferred_buffer_scale => {}, .preferred_buffer_transform => {}, } const now_visible = window.state.visible(); if (window.frame_loop) |loop| { if (was_visible and !now_visible) loop.onSurfaceHidden(); if (!was_visible and now_visible) loop.onSurfaceShown(); } window.notifyVisibilityChange(was_visible); } fn applyToplevelStates(state: *SurfaceState, states: []const u32) void { @@ -1189,7 +1195,7 @@ fn applyToplevelStates(state: *SurfaceState, states: []const u32) void { } fn xdgToplevelListener(_: *xdg.Toplevel, event: xdg.Toplevel.Event, window: *Window) void { const was_visible = window.state.visible(); const was_visible = window.captureVisibility(); switch (event) { .configure => |cfg| { if (cfg.width > 0) window.width = @intCast(cfg.width); @@ -1200,11 +1206,7 @@ fn xdgToplevelListener(_: *xdg.Toplevel, event: xdg.Toplevel.Event, window: *Win .configure_bounds => {}, .wm_capabilities => {}, } const now_visible = window.state.visible(); if (window.frame_loop) |loop| { if (was_visible and !now_visible) loop.onSurfaceHidden(); if (!was_visible and now_visible) loop.onSurfaceShown(); } window.notifyVisibilityChange(was_visible); } fn registryListener(