185553de
Migrate main terminal loop to FrameLoop; fix hidden-workspace freeze
a73x 2026-04-16 11:14
Splits the scale/resize handler into observeResize (non-Vulkan, always runs) and applyPendingResize/Scale (Vulkan, gated on canRender). All Vulkan calls — deviceWaitIdle, recreateSwapchain, rebuildFaceForScale, drawCells — now happen only when the surface is visible. OUT_OF_DATE path calls forceArm() to retry without a callback. Wires FrameLoop hide/show hooks into surfaceListener, xdgToplevelListener, and xdgSurfaceListener: each now samples visible() before and after event handling and invokes onSurfaceHidden/onSurfaceShown on transitions. Fixes: waystty hanging when its window is moved to a hidden sway workspace. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
diff --git a/src/main.zig b/src/main.zig index 6ed0a50..057f19f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2,6 +2,7 @@ const std = @import("std"); const vt = @import("vt"); const pty = @import("pty"); const wayland_client = @import("wayland-client"); const frame_loop_mod = @import("frame_loop"); const renderer = @import("renderer"); const font = @import("font"); const config = @import("config"); @@ -229,10 +230,17 @@ fn runTerminal(alloc: std.mem.Allocator) !void { var frame_ring = FrameTimingRing{}; installSigusr1Handler(); // === frame loop === var frame_loop = frame_loop_mod.FrameLoop.init( window.displayOps(conn.display), window.surfaceStateView(), ); defer frame_loop.deinit(); window.frame_loop = &frame_loop; defer window.frame_loop = null; // === main loop === const wl_fd = conn.display.getFd(); var pollfds = [_]std.posix.pollfd{ .{ .fd = wl_fd, .events = std.posix.POLL.IN, .revents = 0 }, var pollfds_extra = [_]std.posix.pollfd{ .{ .fd = p.master_fd, .events = std.posix.POLL.IN, .revents = 0 }, }; @@ -242,24 +250,16 @@ fn runTerminal(alloc: std.mem.Allocator) !void { var last_window_h = window.height; var last_scale: i32 = geom.buffer_scale; var render_pending = true; var resize_pending = false; var scale_pending = false; while (!window.should_close and p.isChildAlive()) { // Flush any pending wayland requests _ = conn.display.flush(); const repeat_timeout_ms = remainingRepeatTimeoutMs(keyboard.nextRepeatDeadlineNs()); _ = std.posix.poll(&pollfds, computePollTimeoutMs(repeat_timeout_ms, render_pending)) catch {}; // Wayland events: prepare_read / read_events / dispatch_pending if (pollfds[0].revents & std.posix.POLL.IN != 0) { if (conn.display.prepareRead()) { _ = conn.display.readEvents(); } } _ = conn.display.dispatchPending(); const timeout = computePollTimeoutMs(repeat_timeout_ms, render_pending and frame_loop.canRender()); try frame_loop.waitForWork(&pollfds_extra, timeout); // PTY output if (pollfds[1].revents & std.posix.POLL.IN != 0) { if (pollfds_extra[0].revents & std.posix.POLL.IN != 0) { while (true) { const n = p.read(&read_buf) catch |err| switch (err) { error.WouldBlock => break, @@ -314,8 +314,24 @@ fn runTerminal(alloc: std.mem.Allocator) !void { } keyboard.event_queue.clearRetainingCapacity(); // observeResize — detect scale/size changes. No Vulkan here; this runs // every loop iteration so transitions are noticed even while hidden, // and applyPendingScale/Resize below catches up once visible again. const current_scale = window.bufferScale(); if (current_scale != last_scale) { scale_pending = true; render_pending = true; } if (window.width != last_window_w or window.height != last_window_h) { resize_pending = true; render_pending = true; } if (!shouldRenderFrame(render_pending, false, false)) continue; if (!frame_loop.canRender()) continue; // hidden — no Vulkan at all // applyPendingScale — Vulkan work, gated on canRender(). if (scale_pending) { _ = try ctx.vkd.deviceWaitIdle(ctx.device); geom = try rebuildFaceForScale( @@ -324,7 +340,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void { font_lookup.path, font_lookup.index, font_size, current_scale, window.bufferScale(), ); cell_w = geom.cell_w_px; cell_h = geom.cell_h_px; @@ -341,11 +357,12 @@ fn runTerminal(alloc: std.mem.Allocator) !void { const buf_h = window.height * @as(u32, @intCast(geom.buffer_scale)); try ctx.recreateSwapchain(buf_w, buf_h); last_scale = current_scale; render_pending = true; last_scale = geom.buffer_scale; scale_pending = false; } if (window.width != last_window_w or window.height != last_window_h) { // 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. const surf_cell_w = cell_w / @as(u32, @intCast(geom.buffer_scale)); @@ -376,11 +393,9 @@ fn runTerminal(alloc: std.mem.Allocator) !void { } last_window_w = window.width; last_window_h = window.height; render_pending = true; resize_pending = false; } if (!shouldRenderFrame(render_pending, false, false)) continue; var frame_timing: FrameTiming = .{}; // === render === @@ -577,6 +592,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void { const buf_w = window.width * @as(u32, @intCast(geom.buffer_scale)); const buf_h = window.height * @as(u32, @intCast(geom.buffer_scale)); try ctx.recreateSwapchain(buf_w, buf_h); frame_loop.forceArm(); render_pending = true; continue; }, @@ -592,6 +608,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void { } clearConsumedDirtyFlags(&term.render_state.dirty, dirty_rows, refresh_plan); try frame_loop.commitRender(); render_pending = false; } diff --git a/src/wayland.zig b/src/wayland.zig index e31224c..7afebf7 100644 --- a/src/wayland.zig +++ b/src/wayland.zig @@ -625,6 +625,7 @@ pub const Window = struct { width: u32 = 800, height: u32 = 600, display_adapter: DisplayAdapter = undefined, frame_loop: ?*FrameLoop = null, pub fn deinit(self: *Window) void { self.xdg_toplevel.destroy(); @@ -1137,29 +1138,43 @@ 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(); 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(); } } fn surfaceListener(_: *wl.Surface, event: wl.Surface.Event, window: *Window) void { const was_visible = window.state.visible(); switch (event) { .enter => |e| { const wl_out = e.output orelse return; window.handleSurfaceEnter(wl_out); window.scale_generation += 1; if (e.output) |wl_out| { window.handleSurfaceEnter(wl_out); window.scale_generation += 1; } }, .leave => |e| { const wl_out = e.output orelse return; window.handleSurfaceLeave(wl_out); window.scale_generation += 1; if (e.output) |wl_out| { window.handleSurfaceLeave(wl_out); window.scale_generation += 1; } }, .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(); } } fn applyToplevelStates(state: *SurfaceState, states: []const u32) void { @@ -1174,6 +1189,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(); switch (event) { .configure => |cfg| { if (cfg.width > 0) window.width = @intCast(cfg.width); @@ -1184,6 +1200,11 @@ 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(); } } fn registryListener(