diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-21 23:43:31 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | e714bbfa8b7cbf9970053cfbabbb9b1f02a2290e (patch) | |
| tree | d1112abb074f739cb2d70f1117b91f064d9875b7 /src/pardes.zig | |
| parent | 8fc6bb0633d7992dad6b28a2d36730689226e096 (diff) | |
| download | pardes-e714bbfa8b7cbf9970053cfbabbb9b1f02a2290e.tar.gz pardes-e714bbfa8b7cbf9970053cfbabbb9b1f02a2290e.zip | |
Draw a frame only when there is one worth drawing
A 9P round trip on a local Unix socket cost 9.7 ms against the SDL shell and
0.758 ms against the terminal one. The server was not slow and the wake was
not broken: instrumenting the path showed every request waking the loop early
(wakes=210, woke_early=212, timed_out=38 over 250 ticks) and being answered on
that same pass. The cost was that `pump` answers 9P at one point in a loop
that then renders and presents unconditionally, so a client's next request
landed while the main thread was blocked on the display, and each round trip
therefore cost a whole frame. The frame rate was governing something that has
nothing to do with drawing.
`Pardes.needs_frame` starts true, is set by every event except a tick with
nothing animating and a filesystem request that only reads, and is cleared
once a frame is presented. `pump` returns before render and present when it is
false and nothing is animating. An idle editor answering reads now draws
nothing at all.
9P read_fid, one RPC: gui 9.7 ms -> 0.056 ms (173x)
tty 0.758 ms -> 0.062 ms (12x)
Verified the shells still paint rather than going quiet: the rendered screen
carries the opened file, a write through 9P redraws within the frame, and
`fs-discovery-test` passes over the real wire. Suite unchanged at 778/783 with
the two pre-existing crashes.
Also from the adversarial review of the previous commits:
`pardes --tty FILE` silently discarded the file, and `--tty MISSING` silently
discarded the error pane. main.zig named the boot layout before the positional
was resolved, and naming one short-circuits `Boot.of`. The choice now happens
after the argument is known, and only when there is no file and no missing
word. macos.zig names the same layout, so the app no longer boots a different
one from the terminal and SDL shells.
`pre_close_last_pane_tail` was transcribed from the NEW default rather than the
old one, so the upgrade path it was added for did not exist: a workspace dumped
before the tagline reorder came back with the old default welded on as a custom
tail. It is now the string it claims to be.
A pane two rows tall lost its message and, worse, its prompt and the cursor
with it. The notice cap keeps the LAST notices now, because the prompt is last
and a prompt you cannot see is one you type into blind.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 69 |
1 files changed, 66 insertions, 3 deletions
diff --git a/src/pardes.zig b/src/pardes.zig index 51442034..e9908d65 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -304,7 +304,7 @@ const pre_tty_first_terminal_pane_tail = " Save Tty Del Togglettymode Filter Col const pre_mode_terminal_pane_tail = " Tty Save Del Togglettymode Filter Collapse"; // ...and the defaults from before the closing word moved to the end of every // tagline, where a click cannot land on it by overshooting the one before. -const pre_close_last_pane_tail = " Tty Collapse Del"; +const pre_close_last_pane_tail = " Tty Del Collapse"; const pre_close_last_file_pane_tail = " Save Tty Del Collapse"; const pre_close_last_pdf_pane_tail = " Tty Del PdfSections PdfTint Collapse"; const pre_close_last_terminal_pane_tail = " Tty Save Del Mode Filter Collapse"; @@ -6099,6 +6099,9 @@ pub const Pardes = struct { fs: filesystem.Namespace = .{}, boot: Boot = .shell, + /// Is there a frame worth drawing? Cleared once one is presented, set by + /// anything that can change what is on screen. The first frame always is. + needs_frame: bool = true, /// Builtins announce themselves on the message row. Off while startup /// configuration is applied: those lines are nobody clicking, and should /// not leave the row holding the name of the last one. @@ -7137,13 +7140,28 @@ pub const Pardes = struct { // A quitting frame has already freed what it would draw. if (p.quit) return; if (v.poll_frame) |f| f(h.ctx); + // Nothing to draw: do not spend a frame, and above all do not block on + // the display for one. This is what keeps the frame rate governing + // rendering and nothing else. + if (!p.needs_frame and !p.animationActive()) return; _ = p.frame_arena.reset(.retain_capacity); const surface = try p.render(p.frame_arena.allocator()); if (v.present) |f| f(h.ctx, surface); if (v.post_present) |f| f(h.ctx); + p.needs_frame = false; } pub fn update(p: *Pardes, ev: Event) void { + // Only two events cannot change the screen: a tick with nothing + // animating, and a filesystem request that only reads. The second is + // what 9P traffic overwhelmingly is, and answering it used to drag a + // whole vsync-blocked frame behind it -- which is why a round trip on + // a local socket cost a frame instead of a few microseconds. + p.needs_frame = p.needs_frame or switch (ev) { + .tick => false, + .fs_req => |req| ctlfs.changesPane(req), + else => true, + }; p.shell_rows.sweep(p.gpa); switch (ev) { .tick => {}, @@ -13953,8 +13971,17 @@ pub const Pardes = struct { if (pane.promptAt()) |at| { if (pane.tagSlice()[@min(at, pane.tag_tail_len)..].len > 0) pane.notices.push(.prompt); } - // Never claim so many rows that the body has none left. - pane.notices.len = @min(pane.notices.len, r.h - BOX_H -| 1); + // A pane only has so many rows to give. When they do not all fit the + // LAST ones survive, because the prompt is last and it is the one + // carrying the cursor: a prompt you cannot see is a prompt you type + // into blind. A two-row pane spends its only body row on one notice, + // which is what the single shared row used to do anyway. + const room: u8 = @intCast(@min(@as(u16, Pane.Notices.max), r.h - BOX_H)); + if (pane.notices.len > room) { + const drop = pane.notices.len - room; + for (0..room) |i| pane.notices.kinds[i] = pane.notices.kinds[i + drop]; + pane.notices.len = room; + } } fn renderBodyLayer(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect, id: usize) !void { @@ -14603,6 +14630,42 @@ test "Esc back into a tty leaves its view at the prompt" { try std.testing.expectEqual(live, sp.terminal.?.vt.screens.active.pages.scrollbar().offset); } +test "closing the last pane in a column leaves an empty one in its place" { + if (platform == .web) return; + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 120, .rows = 24 }); + defer p.deinit(); + const shell = p.active; + p.newScratchColumn(shell); + const lone = p.active; + try std.testing.expect(lone != shell); + try std.testing.expectEqual(@as(usize, 2), p.ncol); + const at = layout.findPane(p, lone).?; + try std.testing.expectEqual(@as(usize, 1), p.col_n[at.col]); + const had = try std.testing.allocator.dupe(u8, p.panes[lone].?.cwdSlice()); + defer std.testing.allocator.free(had); + + try p.removePane(lone); + p.sync(); + + // The column outlives the pane: still two columns, and the one that was + // emptied holds a fresh empty pane in the same directory. + try std.testing.expectEqual(@as(usize, 2), p.ncol); + try std.testing.expectEqual(@as(usize, 1), p.col_n[at.col]); + const heir = p.col_panes[at.col][0]; + try std.testing.expect(heir != lone); + try std.testing.expect(p.panes[lone] == null); + const hp = p.panes[heir].?; + try std.testing.expectEqualStrings("", hp.file.?.content); + try std.testing.expectEqualStrings(had, hp.cwdSlice()); + try std.testing.expectEqual(heir, p.active); + try std.testing.expect(!p.quit); + + // ...and Delcol is what actually takes the column away. + try p.removeColumn(heir); + p.sync(); + try std.testing.expectEqual(@as(usize, 1), p.ncol); +} + test "the bare tty startup opens a text pane under the shell, unfocused" { if (platform == .web) return; const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .boot = .tty }); |
