185553de
Migrate main terminal loop to FrameLoop; fix hidden-workspace freeze
a73x 2026-04-16 11:14
Commit message
src/main.zig
| Old | New | ||
|---|---|---|---|
| @@ -2,6 +2,7 @@ const std = @import("std"); | |||
| 2 | const vt = @import("vt"); | 2 | const vt = @import("vt"); |
| 3 | const pty = @import("pty"); | 3 | const pty = @import("pty"); |
| 4 | const wayland_client = @import("wayland-client"); | 4 | const wayland_client = @import("wayland-client"); |
| 5 | const frame_loop_mod = @import("frame_loop"); | ||
| 5 | const renderer = @import("renderer"); | 6 | const renderer = @import("renderer"); |
| 6 | const font = @import("font"); | 7 | const font = @import("font"); |
| 7 | const config = @import("config"); | 8 | const config = @import("config"); |
| @@ -229,10 +230,17 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 229 | var frame_ring = FrameTimingRing{}; | 230 | var frame_ring = FrameTimingRing{}; |
| 230 | installSigusr1Handler(); | 231 | installSigusr1Handler(); |
| 231 | 232 | ||
| 233 | // === frame loop === | ||
| 234 | var frame_loop = frame_loop_mod.FrameLoop.init( | ||
| 235 | window.displayOps(conn.display), | ||
| 236 | window.surfaceStateView(), | ||
| 237 | ); | ||
| 238 | defer frame_loop.deinit(); | ||
| 239 | window.frame_loop = &frame_loop; | ||
| 240 | defer window.frame_loop = null; | ||
| 241 | |||
| 232 | // === main loop === | 242 | // === main loop === |
| 233 | const wl_fd = conn.display.getFd(); | 243 | var pollfds_extra = [_]std.posix.pollfd{ |
| 234 | var pollfds = [_]std.posix.pollfd{ | ||
| 235 | .{ .fd = wl_fd, .events = std.posix.POLL.IN, .revents = 0 }, | ||
| 236 | .{ .fd = p.master_fd, .events = std.posix.POLL.IN, .revents = 0 }, | 244 | .{ .fd = p.master_fd, .events = std.posix.POLL.IN, .revents = 0 }, |
| 237 | }; | 245 | }; |
| 238 | 246 | ||
| @@ -242,24 +250,16 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 242 | var last_window_h = window.height; | 250 | var last_window_h = window.height; |
| 243 | var last_scale: i32 = geom.buffer_scale; | 251 | var last_scale: i32 = geom.buffer_scale; |
| 244 | var render_pending = true; | 252 | var render_pending = true; |
| 253 | var resize_pending = false; | ||
| 254 | var scale_pending = false; | ||
| 245 | 255 | ||
| 246 | while (!window.should_close and p.isChildAlive()) { | 256 | while (!window.should_close and p.isChildAlive()) { |
| 247 | // Flush any pending wayland requests | ||
| 248 | _ = conn.display.flush(); | ||
| 249 | |||
| 250 | const repeat_timeout_ms = remainingRepeatTimeoutMs(keyboard.nextRepeatDeadlineNs()); | 257 | const repeat_timeout_ms = remainingRepeatTimeoutMs(keyboard.nextRepeatDeadlineNs()); |
| 251 | _ = std.posix.poll(&pollfds, computePollTimeoutMs(repeat_timeout_ms, render_pending)) catch {}; | 258 | const timeout = computePollTimeoutMs(repeat_timeout_ms, render_pending and frame_loop.canRender()); |
| 252 | 259 | try frame_loop.waitForWork(&pollfds_extra, timeout); | |
| 253 | // Wayland events: prepare_read / read_events / dispatch_pending | ||
| 254 | if (pollfds[0].revents & std.posix.POLL.IN != 0) { | ||
| 255 | if (conn.display.prepareRead()) { | ||
| 256 | _ = conn.display.readEvents(); | ||
| 257 | } | ||
| 258 | } | ||
| 259 | _ = conn.display.dispatchPending(); | ||
| 260 | 260 | ||
| 261 | // PTY output | 261 | // PTY output |
| 262 | if (pollfds[1].revents & std.posix.POLL.IN != 0) { | 262 | if (pollfds_extra[0].revents & std.posix.POLL.IN != 0) { |
| 263 | while (true) { | 263 | while (true) { |
| 264 | const n = p.read(&read_buf) catch |err| switch (err) { | 264 | const n = p.read(&read_buf) catch |err| switch (err) { |
| 265 | error.WouldBlock => break, | 265 | error.WouldBlock => break, |
| @@ -314,8 +314,24 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 314 | } | 314 | } |
| 315 | keyboard.event_queue.clearRetainingCapacity(); | 315 | keyboard.event_queue.clearRetainingCapacity(); |
| 316 | 316 | ||
| 317 | // observeResize — detect scale/size changes. No Vulkan here; this runs | ||
| 318 | // every loop iteration so transitions are noticed even while hidden, | ||
| 319 | // and applyPendingScale/Resize below catches up once visible again. | ||
| 317 | const current_scale = window.bufferScale(); | 320 | const current_scale = window.bufferScale(); |
| 318 | if (current_scale != last_scale) { | 321 | if (current_scale != last_scale) { |
| 322 | scale_pending = true; | ||
| 323 | render_pending = true; | ||
| 324 | } | ||
| 325 | if (window.width != last_window_w or window.height != last_window_h) { | ||
| 326 | resize_pending = true; | ||
| 327 | render_pending = true; | ||
| 328 | } | ||
| 329 | |||
| 330 | if (!shouldRenderFrame(render_pending, false, false)) continue; | ||
| 331 | if (!frame_loop.canRender()) continue; // hidden — no Vulkan at all | ||
| 332 | |||
| 333 | // applyPendingScale — Vulkan work, gated on canRender(). | ||
| 334 | if (scale_pending) { | ||
| 319 | _ = try ctx.vkd.deviceWaitIdle(ctx.device); | 335 | _ = try ctx.vkd.deviceWaitIdle(ctx.device); |
| 320 | 336 | ||
| 321 | geom = try rebuildFaceForScale( | 337 | geom = try rebuildFaceForScale( |
| @@ -324,7 +340,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 324 | font_lookup.path, | 340 | font_lookup.path, |
| 325 | font_lookup.index, | 341 | font_lookup.index, |
| 326 | font_size, | 342 | font_size, |
| 327 | current_scale, | 343 | window.bufferScale(), |
| 328 | ); | 344 | ); |
| 329 | cell_w = geom.cell_w_px; | 345 | cell_w = geom.cell_w_px; |
| 330 | cell_h = geom.cell_h_px; | 346 | cell_h = geom.cell_h_px; |
| @@ -341,11 +357,12 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 341 | const buf_h = window.height * @as(u32, @intCast(geom.buffer_scale)); | 357 | const buf_h = window.height * @as(u32, @intCast(geom.buffer_scale)); |
| 342 | try ctx.recreateSwapchain(buf_w, buf_h); | 358 | try ctx.recreateSwapchain(buf_w, buf_h); |
| 343 | 359 | ||
| 344 | last_scale = current_scale; | 360 | last_scale = geom.buffer_scale; |
| 345 | render_pending = true; | 361 | scale_pending = false; |
| 346 | } | 362 | } |
| 347 | 363 | ||
| 348 | if (window.width != last_window_w or window.height != last_window_h) { | 364 | // applyPendingResize — Vulkan work, gated on canRender(). |
| 365 | if (resize_pending) { | ||
| 349 | // Grid is sized in surface coordinates. cell_w/cell_h are in buffer | 366 | // Grid is sized in surface coordinates. cell_w/cell_h are in buffer |
| 350 | // pixels, so divide by buffer_scale to get surface-pixel cell dims. | 367 | // pixels, so divide by buffer_scale to get surface-pixel cell dims. |
| 351 | const surf_cell_w = cell_w / @as(u32, @intCast(geom.buffer_scale)); | 368 | const surf_cell_w = cell_w / @as(u32, @intCast(geom.buffer_scale)); |
| @@ -376,11 +393,9 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 376 | } | 393 | } |
| 377 | last_window_w = window.width; | 394 | last_window_w = window.width; |
| 378 | last_window_h = window.height; | 395 | last_window_h = window.height; |
| 379 | render_pending = true; | 396 | resize_pending = false; |
| 380 | } | 397 | } |
| 381 | 398 | ||
| 382 | if (!shouldRenderFrame(render_pending, false, false)) continue; | ||
| 383 | |||
| 384 | var frame_timing: FrameTiming = .{}; | 399 | var frame_timing: FrameTiming = .{}; |
| 385 | 400 | ||
| 386 | // === render === | 401 | // === render === |
| @@ -577,6 +592,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 577 | const buf_w = window.width * @as(u32, @intCast(geom.buffer_scale)); | 592 | const buf_w = window.width * @as(u32, @intCast(geom.buffer_scale)); |
| 578 | const buf_h = window.height * @as(u32, @intCast(geom.buffer_scale)); | 593 | const buf_h = window.height * @as(u32, @intCast(geom.buffer_scale)); |
| 579 | try ctx.recreateSwapchain(buf_w, buf_h); | 594 | try ctx.recreateSwapchain(buf_w, buf_h); |
| 595 | frame_loop.forceArm(); | ||
| 580 | render_pending = true; | 596 | render_pending = true; |
| 581 | continue; | 597 | continue; |
| 582 | }, | 598 | }, |
| @@ -592,6 +608,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 592 | } | 608 | } |
| 593 | 609 | ||
| 594 | clearConsumedDirtyFlags(&term.render_state.dirty, dirty_rows, refresh_plan); | 610 | clearConsumedDirtyFlags(&term.render_state.dirty, dirty_rows, refresh_plan); |
| 611 | try frame_loop.commitRender(); | ||
| 595 | render_pending = false; | 612 | render_pending = false; |
| 596 | } | 613 | } |
| 597 | 614 | ||
src/wayland.zig
| Old | New | ||
|---|---|---|---|
| @@ -625,6 +625,7 @@ pub const Window = struct { | |||
| 625 | width: u32 = 800, | 625 | width: u32 = 800, |
| 626 | height: u32 = 600, | 626 | height: u32 = 600, |
| 627 | display_adapter: DisplayAdapter = undefined, | 627 | display_adapter: DisplayAdapter = undefined, |
| 628 | frame_loop: ?*FrameLoop = null, | ||
| 628 | 629 | ||
| 629 | pub fn deinit(self: *Window) void { | 630 | pub fn deinit(self: *Window) void { |
| 630 | self.xdg_toplevel.destroy(); | 631 | self.xdg_toplevel.destroy(); |
| @@ -1137,29 +1138,43 @@ fn wmBaseListener(wm_base: *xdg.WmBase, event: xdg.WmBase.Event, _: *xdg.WmBase) | |||
| 1137 | } | 1138 | } |
| 1138 | 1139 | ||
| 1139 | fn xdgSurfaceListener(surface: *xdg.Surface, event: xdg.Surface.Event, window: *Window) void { | 1140 | fn xdgSurfaceListener(surface: *xdg.Surface, event: xdg.Surface.Event, window: *Window) void { |
| 1141 | const was_visible = window.state.visible(); | ||
| 1140 | switch (event) { | 1142 | switch (event) { |
| 1141 | .configure => |cfg| { | 1143 | .configure => |cfg| { |
| 1142 | surface.ackConfigure(cfg.serial); | 1144 | surface.ackConfigure(cfg.serial); |
| 1143 | window.state.configured = true; | 1145 | window.state.configured = true; |
| 1144 | }, | 1146 | }, |
| 1145 | } | 1147 | } |
| 1148 | const now_visible = window.state.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 | } | ||
| 1146 | } | 1153 | } |
| 1147 | 1154 | ||
| 1148 | fn surfaceListener(_: *wl.Surface, event: wl.Surface.Event, window: *Window) void { | 1155 | fn surfaceListener(_: *wl.Surface, event: wl.Surface.Event, window: *Window) void { |
| 1156 | const was_visible = window.state.visible(); | ||
| 1149 | switch (event) { | 1157 | switch (event) { |
| 1150 | .enter => |e| { | 1158 | .enter => |e| { |
| 1151 | const wl_out = e.output orelse return; | 1159 | if (e.output) |wl_out| { |
| 1152 | window.handleSurfaceEnter(wl_out); | 1160 | window.handleSurfaceEnter(wl_out); |
| 1153 | window.scale_generation += 1; | 1161 | window.scale_generation += 1; |
| 1162 | } | ||
| 1154 | }, | 1163 | }, |
| 1155 | .leave => |e| { | 1164 | .leave => |e| { |
| 1156 | const wl_out = e.output orelse return; | 1165 | if (e.output) |wl_out| { |
| 1157 | window.handleSurfaceLeave(wl_out); | 1166 | window.handleSurfaceLeave(wl_out); |
| 1158 | window.scale_generation += 1; | 1167 | window.scale_generation += 1; |
| 1168 | } | ||
| 1159 | }, | 1169 | }, |
| 1160 | .preferred_buffer_scale => {}, | 1170 | .preferred_buffer_scale => {}, |
| 1161 | .preferred_buffer_transform => {}, | 1171 | .preferred_buffer_transform => {}, |
| 1162 | } | 1172 | } |
| 1173 | const now_visible = window.state.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 | } | ||
| 1163 | } | 1178 | } |
| 1164 | 1179 | ||
| 1165 | fn applyToplevelStates(state: *SurfaceState, states: []const u32) void { | 1180 | fn applyToplevelStates(state: *SurfaceState, states: []const u32) void { |
| @@ -1174,6 +1189,7 @@ fn applyToplevelStates(state: *SurfaceState, states: []const u32) void { | |||
| 1174 | } | 1189 | } |
| 1175 | 1190 | ||
| 1176 | fn xdgToplevelListener(_: *xdg.Toplevel, event: xdg.Toplevel.Event, window: *Window) void { | 1191 | fn xdgToplevelListener(_: *xdg.Toplevel, event: xdg.Toplevel.Event, window: *Window) void { |
| 1192 | const was_visible = window.state.visible(); | ||
| 1177 | switch (event) { | 1193 | switch (event) { |
| 1178 | .configure => |cfg| { | 1194 | .configure => |cfg| { |
| 1179 | if (cfg.width > 0) window.width = @intCast(cfg.width); | 1195 | if (cfg.width > 0) window.width = @intCast(cfg.width); |
| @@ -1184,6 +1200,11 @@ fn xdgToplevelListener(_: *xdg.Toplevel, event: xdg.Toplevel.Event, window: *Win | |||
| 1184 | .configure_bounds => {}, | 1200 | .configure_bounds => {}, |
| 1185 | .wm_capabilities => {}, | 1201 | .wm_capabilities => {}, |
| 1186 | } | 1202 | } |
| 1203 | const now_visible = window.state.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 | } | ||
| 1187 | } | 1208 | } |
| 1188 | 1209 | ||
| 1189 | fn registryListener( | 1210 | fn registryListener( |