diff options
Diffstat (limited to 'src/tty')
| -rw-r--r-- | src/tty/panel_compositor.zig | 27 | ||||
| -rw-r--r-- | src/tty/tty.zig | 110 |
2 files changed, 106 insertions, 31 deletions
diff --git a/src/tty/panel_compositor.zig b/src/tty/panel_compositor.zig index 9363841a..67d3a6e6 100644 --- a/src/tty/panel_compositor.zig +++ b/src/tty/panel_compositor.zig @@ -8,9 +8,10 @@ const std = @import("std"); const pardes = @import("../pardes.zig"); const layout = @import("../layout.zig"); +const animation = @import("../animation.zig"); -const Box = layout.Box; -const Track = layout.Track; +const Box = animation.Box; +const Track = animation.Track; /// Kitty placements cannot be resampled through the character-grid transform. /// Keep their transmitted pixels cached, but omit the placement while its pane @@ -77,7 +78,7 @@ pub fn compose( // Stable layout motion first, newly opening panels above it, and closing // tombstones last. A closing pane no longer owns input or canonical cells, // but its frozen old content remains the top visual until it slides out. - for ([_]layout.Phase{ .moving, .opening, .closing }) |phase| { + for ([_]animation.Phase{ .moving, .opening, .closing }) |phase| { for (tracks) |track| { if (!drawable(source, track) or track.phase != phase) continue; switch (track.effect) { @@ -139,7 +140,7 @@ fn dissolve(out: *pardes.Surface, source: *const pardes.Surface, track: Track) v var x = area.x0; while (x < area.x1) : (x += 1) { if (!source.panelCellChanged(x, y)) continue; - if (layout.dissolveRevealed( + if (animation.dissolveRevealed( track.serial, x - area.x0, y - area.y0, @@ -339,7 +340,7 @@ test "TTY content effects never touch cells outside the published diff" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); - for ([_]layout.Transition{ .ascii, .dissolve }) |effect| { + for ([_]animation.Transition{ .ascii, .dissolve }) |effect| { const track: Track = .{ .serial = 17, .effect = effect, @@ -366,7 +367,7 @@ test "content transition without a diff snaps to canonical surface" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); - for ([_]layout.Transition{ .dissolve, .ascii, .vertical }) |effect| { + for ([_]animation.Transition{ .dissolve, .ascii, .vertical }) |effect| { const track: Track = .{ .effect = effect, .phase = .opening, @@ -391,7 +392,7 @@ test "exact transition endpoint preserves the canonical cursor" { }; const track: Track = .{ .effect = .ascii, - .frame = layout.Transition.ascii.frames() - 1, + .frame = animation.Transition.ascii.frames() - 1, .from = .{ .w = 1, .h = 1 }, .to = .{ .w = 1, .h = 1 }, }; @@ -431,7 +432,7 @@ test "vertical opening rises through a fixed old-grid clip" { .serial = 5, .phase = .opening, .effect = .vertical, - .from = layout.openingBox(.vertical, target, 3), + .from = animation.openingBox(.vertical, target, 3), .to = target, }; @@ -485,7 +486,7 @@ test "vertical closing drops frozen content over canonical cells" { .phase = .closing, .effect = .vertical, .from = old_box, - .to = layout.closingBox(.vertical, old_box), + .to = animation.closingBox(.vertical, old_box), }; const first = try compose(arena.allocator(), &surface, &.{track}, null); @@ -530,13 +531,13 @@ test "closing content paints after opening content regardless of track order" { .phase = .closing, .effect = .vertical, .from = box, - .to = layout.closingBox(.vertical, box), + .to = animation.closingBox(.vertical, box), }; const opening: Track = .{ .serial = 2, .phase = .opening, .effect = .ascii, - .frame = layout.Transition.ascii.frames() - 1, + .frame = animation.Transition.ascii.frames() - 1, .from = box, .to = box, }; @@ -691,8 +692,8 @@ test "active panel transition hides only its own native attachment" { const tracks = [_]Track{ .{ .serial = 41, .effect = .slide, .frame = 0 }, .{ .serial = 42, .effect = .ascii, .frame = 0 }, - .{ .serial = 43, .effect = .zoom, .frame = layout.Transition.zoom.frames() - 1 }, - .{ .serial = 44, .effect = .zoom, .frame = layout.Transition.zoom.frames() }, + .{ .serial = 43, .effect = .zoom, .frame = animation.Transition.zoom.frames() - 1 }, + .{ .serial = 44, .effect = .zoom, .frame = animation.Transition.zoom.frames() }, .{ .serial = 45, .phase = .opening, .effect = .vertical, .frame = 0 }, }; diff --git a/src/tty/tty.zig b/src/tty/tty.zig index f8f8cdc5..822c2d7f 100644 --- a/src/tty/tty.zig +++ b/src/tty/tty.zig @@ -76,7 +76,8 @@ pub const Command = struct { pub var value: union(enum) { nop, quit, - tick, + /// A timed wait ran out: the core's next wake has come. + wake, key_press: vaxis.Key, pty_read: struct { id: usize, gen: u32, bytes: []u8 }, pty_eof: struct { id: usize, gen: u32 }, @@ -737,6 +738,7 @@ fn localSession( .paste_buf = paste_buf, .inotify_fd = file_watch.init(false), .fs = fs, + .test_clock = host_io.testClock(), }; pardes.lsp.setStatusSink(&sh, lspStatusSink); host_io.setExitWake(loop, wakeForExit); @@ -786,7 +788,7 @@ fn localSession( try startInput(loop, input_cache); defer if (attached.* == null) stopInput(loop); (try std.Thread.spawn(.{}, winchWatch, .{ loop, vx, tty })).detach(); - (try std.Thread.spawn(.{}, tickWatch, .{ io, loop })).detach(); + (try std.Thread.spawn(.{}, wakeWatch, .{ io, loop })).detach(); if (fs) |f| try f.watch(loop, wakeFs); try vx.queryTerminalSend(tty.writer()); @@ -911,7 +913,11 @@ const Shell = struct { caps_pending: bool = true, check_files: bool = false, in_paste: bool = false, - tracks: []const pardes.layout.Track = &.{}, + tracks: []const pardes.animation.Track = &.{}, + /// PARDES_TEST_CLOCK's virtual time (host_io.testClock), and where the + /// running-out wait moves it: the core's next wake. + test_clock: ?u64 = null, + wake_to: u64 = 0, fn of(ctx: ?*anyopaque) *Shell { return @ptrCast(@alignCast(ctx.?)); @@ -923,12 +929,14 @@ const Shell = struct { const isolated_vtable: host_io.Host.VTable = .{ .wait_input = waitInput, + .now = clockNow, .present = present, .post_present = postPresent, }; const vtable: host_io.Host.VTable = .{ .wait_input = waitInput, + .now = clockNow, .present = present, .post_present = postPresent, .poll_frame = pollFrame, @@ -951,12 +959,20 @@ const Shell = struct { .pipe = pipe, }; + fn clockNow(ctx: ?*anyopaque) u64 { + const s = of(ctx); + return s.test_clock orelse host_io.monotonicNs(); + } + fn waitInput(ctx: ?*anyopaque, timeout_ms: u32) void { const s = of(ctx); var batch: usize = 0; - // The wait is the 9P connections' turn with the core. Animating, the - // frame's tick arrives through the same queue (`tickWatch`). - if (timeout_ms != 0) tick_request.set(s.io); + // The wait is the 9P connections' turn with the core. A timeout (the + // core's next wake) arrives through the same queue (`wakeWatch`). + if (timeout_ms != 0) { + s.wake_to = s.core.nextWake() orelse 0; + requestWake(s.io, timeout_ms); + } pardes.turn.rest(); const first = s.loop.nextEvent() catch { pardes.turn.wake(); @@ -999,7 +1015,9 @@ const Shell = struct { defer tz_event.end(); switch (event) { .nop => {}, - .tick => core.update(.tick), + .wake => if (s.test_clock) |*virtual| { + virtual.* = @max(virtual.*, s.wake_to); + }, .quit => { core.quit = true; return true; @@ -1565,21 +1583,77 @@ fn winchWatch(loop: *Loop, vx: *vaxis.Vaxis, tty: *vaxis.Tty) void { } } -/// Set by an animating frame's wait: post one `.tick` a frame from now. The -/// tick joins the input queue, so a key or a click that lands inside the -/// frame is handled at once instead of after a blind sleep. -var tick_request: std.Io.Event = .unset; +/// A wait with a timeout: post one `.wake` when it runs out. The wake joins +/// the input queue, so a key or a click that lands first is handled at once +/// instead of after a blind sleep; vaxis's queue has no timed wait. Each +/// request bumps `wake_gen`, which cuts short a sleep still timing an +/// earlier, longer request (an 800 ms linger interrupted by a key that +/// starts a 16 ms fall) and times the new one instead. +var wake_gen: std.atomic.Value(u32) = .init(0); +var wake_ms: std.atomic.Value(u32) = .init(0); -fn tickWatch(io: std.Io, loop: *Loop) void { +fn requestWake(io: std.Io, ms: u32) void { + wake_ms.store(ms, .release); + _ = wake_gen.fetchAdd(1, .acq_rel); + io.futexWake(u32, &wake_gen.raw, 1); +} + +fn wakeWatch(io: std.Io, loop: *Loop) void { + var seen = wake_gen.load(.acquire); while (true) { - tick_request.waitUncancelable(io); - tick_request.reset(); - var ts: libc.timespec = .{ .sec = 0, .nsec = @as(isize, pardes.layout.Animation.frame_ms) * std.time.ns_per_ms }; - _ = libc.nanosleep(&ts, null); - loop.postEvent(.tick) catch {}; + // Asleep until a request. + while (wake_gen.load(.acquire) == seen) io.futexWaitUncancelable(u32, &wake_gen.raw, seen); + seen = wake_gen.load(.acquire); + if (timeWake(io, &seen)) loop.postEvent(.wake) catch {}; } } +/// Sleep out the latest request; true when it ran its course, false when +/// none is pending. A newer request met on the way restarts the timing. +fn timeWake(io: std.Io, seen: *u32) bool { + while (true) { + const ms = wake_ms.load(.acquire); + const deadline = host_io.monotonicNs() + @as(u64, ms) * std.time.ns_per_ms; + while (true) { + const now = host_io.monotonicNs(); + if (now >= deadline) return true; + io.futexWaitTimeout(u32, &wake_gen.raw, seen.*, .{ .duration = .{ + .raw = .fromNanoseconds(@intCast(deadline - now)), + .clock = .awake, + } }) catch {}; + const gen = wake_gen.load(.acquire); + if (gen != seen.*) { + seen.* = gen; + break; + } + } + } +} + +test "a short wake request cuts short a long one still sleeping" { + const io = std.testing.io; + var seen = wake_gen.load(.acquire); + requestWake(io, 800); + seen = wake_gen.load(.acquire); + const Timer = struct { + fn run(timer_io: std.Io, start_seen: u32, done: *std.atomic.Value(u64)) void { + var local = start_seen; + _ = timeWake(timer_io, &local); + done.store(host_io.monotonicNs(), .release); + } + }; + var done: std.atomic.Value(u64) = .init(0); + const start = host_io.monotonicNs(); + const thread = try std.Thread.spawn(.{}, Timer.run, .{ io, seen, &done }); + var pause: std.c.timespec = .{ .sec = 0, .nsec = 50 * std.time.ns_per_ms }; + _ = std.c.nanosleep(&pause, null); + requestWake(io, 16); + thread.join(); + // The 16 ms request lands about 66 ms in: well before the 800 ms one + // would have, however loaded the machine. + try std.testing.expect(done.load(.acquire) - start < 700 * std.time.ns_per_ms); +} + fn wakeFs(ctx: ?*anyopaque) void { const loop: *Loop = @ptrCast(@alignCast(ctx.?)); _ = loop.tryPostEvent(.fs_ready) catch {}; @@ -1864,7 +1938,7 @@ const Attach = struct { fn apply(a: *Attach, event: @TypeOf(Command.value)) ?AttachEnd { switch (event) { - .nop, .tick, .fs_ready, .lsp_done, .lsp_status, .pipe_done, .pty_read, .pty_eof, .files_changed => {}, + .nop, .wake, .fs_ready, .lsp_done, .lsp_status, .pipe_done, .pty_read, .pty_eof, .files_changed => {}, .quit => return .none, .focus_in => {}, .focus_out => return a.send(.pointer_leave), |
