eb4ff88c
Polish frame-loop visibility handling
a73x 2026-04-17 16:45
Commit message
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( |