a73x

35c92e24

Add synthetic hidden-freeze regression test

a73x   2026-04-16 11:21

Exercises the hide → pty-flood → show sequence through the FrameLoop
state machine. Verifies no canRender() call returns true while hidden,
and that normal pacing resumes on show.

diff --git a/src/frame_loop.zig b/src/frame_loop.zig
index 66e5bdf..46a2c21 100644
--- a/src/frame_loop.zig
+++ b/src/frame_loop.zig
@@ -369,3 +369,44 @@ test "deinit destroys any pending callback" {

    try std.testing.expectEqual(@as(usize, 1), mock.callbacks_destroyed);
}

test "hidden-freeze regression: pty flood while hidden never blocks" {
    var mock = try Mock.init();
    defer mock.deinit();
    var ts = try TestState.init(std.testing.allocator);
    defer ts.deinit();

    var loop = FrameLoop.init(mock.ops(), ts.view());
    defer loop.deinit();

    // Initial render.
    try loop.commitRender();
    const first_tok = loop.pending_token.?;
    loop.onFrameCallbackDone(first_tok);

    // Hide the surface: tracker leaves all outputs.
    ts.tracker.leaveOutput(1);
    loop.onSurfaceHidden();
    try std.testing.expect(!loop.canRender());

    // Simulate 100 iterations of "pty wrote more data, we want to render".
    // Under the gate, canRender is false every iteration and we never call
    // commitRender — so no Vulkan work, no blocking.
    var i: usize = 0;
    while (i < 100) : (i += 1) {
        try std.testing.expect(!loop.canRender());
    }

    // Show again: tracker re-enters an output.
    try ts.tracker.enterOutput(1);
    loop.onSurfaceShown();

    try std.testing.expect(loop.canRender());

    // Normal render resumes.
    try loop.commitRender();
    try std.testing.expect(loop.pending_token != null);
    const tok = loop.pending_token.?;
    loop.onFrameCallbackDone(tok);
    try std.testing.expect(loop.armed);
}