a73x

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(