From 31cb659ded4cf50af5903fc107f8c868ee3c7311 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 22 Sep 2026 11:15:46 -0300 Subject: Answer 9P on the connection's task, so a session can open its own tree The editor's loop was the only thing that could answer a 9P request, which made the editor's own syscalls through a mount of its own tree -- a Look at /mnt/9p/pardes//anything under a `9ns --mntgen` view, a Save into it -- requests only the blocked loop could serve. The name-based refusal that followed (ownMountSuffix) and the in-process routing of a mount of oneself (Client.sameSession) were patches over that, and both are gone, with the mailbox that shipped every request to the editor's thread. One rule replaces them, `pardes.turn`: the core is single-threaded, the editor's thread has the turn by default and gives it up in two kinds of gap -- while it waits for input and while a step of it is out in a host syscall -- and a cloud9 connection task takes it in those gaps to answer. `out` counts the steps that are out, from any thread: while one is, the core reads consistently but that step still holds pointers into it, so a request that would change a pane (a write, a truncation, an rmdir) is parked in the engine and retried when the turn is next given up with nothing out, and the editor's own wake waits for the count to reach zero. It is never a write of its own that a step waits on out there -- writes come from a shell performing a save between steps -- so a parked request is never the syscall's own, and making a pane or rendering a screen need not park: every yield sits before its step's mutation, so the layout and the surface are whole under it. A changing request that queued effects is answered once the editor has performed them (`echo Save > exec` returns with the file written, as acme's `put` does), and it settles the way a step does, because without that a /log reader waited for the user's next keystroke. Every host syscall on a user path has to give the turn up, not fs.zig's alone: the first end-to-end run hung in `inotify_add_watch` performing the new pane's watch effect. PDFs and images are read whole at open, so no draw goes out into the host. The core's allocator takes its fixed buffer through the lock-free interface, since a connection task allocates while the editor's thread is out in a syscall that allocates too. A Restore puts the replacement in first and releases every task waiting on the old core. cloud9 (pinned at eb1a104) parks an open, a truncating wstat, a clunk and a remove on `again`, not only reads and writes, and answers a parked job whose fid was clunked without asking the backend. Verified: test/selfmount.py runs the editor under `9ns --mntgen` and Looks at, reads and Saves its own tree through the mount; a unit test pins that a change parks while the editor is out mid-step and lands when it rests, while a read is answered in the window. 9P over the Unix socket against a tty session, same machine, Debug builds: a read of /index 278us -> 61us, a truncating body write 1184us -> 609us, exec Save 718us -> 583us; the gesture benchmark is unchanged (geometric mean 0.997 over 53 cells). Also from the reviews: a notice chip over an image or PDF pane was painted out by the picture drawn after the cells, so pictures give up the rows; in the GUI a tree-sitter context band painted over the chip, so body layers are emitted first; a message is one row of printable text, its 256-byte cut never leaves half a glyph, and one wider than its pane keeps its tail (the file name, the reason) rather than its head. Co-Authored-By: Claude Fable 5.1 --- src/panes.zig | 45 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 10 deletions(-) (limited to 'src/panes.zig') diff --git a/src/panes.zig b/src/panes.zig index f995986b..b3424385 100644 --- a/src/panes.zig +++ b/src/panes.zig @@ -3324,11 +3324,28 @@ pub const Image = struct { /// Construct an image pane from a path and optionally transferred dump bytes. /// `raw` must be image_gpa-owned and ownership transfers only on success. + /// Without them the file is read here, not at the first draw: a draw + /// must not go out into the host (`pardes.turn`), and the path may be a + /// mount this editor serves. A file that cannot be read draws blank. pub fn create(p: *pardes.Pardes, id: usize, path: []const u8, raw: []u8) !*pardes.Pane { + var bytes = raw; + var read_here = false; + if (bytes.len == 0) { + // The slot stays ours across the read: another request may + // make a pane meanwhile. + p.reserved_slots[id] = true; + defer p.reserved_slots[id] = false; + if (filesystem.read(p, path)) |from_disk| { + defer p.gpa.free(from_disk); + bytes = try p.image_gpa.dupe(u8, from_disk); + read_here = true; + } else |_| {} + } + errdefer if (read_here) p.image_gpa.free(bytes); const path_copy = try p.image_gpa.dupe(u8, path); errdefer p.image_gpa.free(path_copy); const pane = try p.newDocPane(id); - pane.image = .{ .path = path_copy, .raw = raw }; + pane.image = .{ .path = path_copy, .raw = bytes }; return pane; } @@ -3427,12 +3444,8 @@ pub const Image = struct { fn ensureDecoded(p: *pardes.Pardes, state: *State) void { if (state.tried) return; state.tried = true; - const bytes: []const u8 = if (state.raw.len > 0) - state.raw - else - filesystem.read(p, state.path) catch &.{}; - defer if (state.raw.len == 0) p.gpa.free(bytes); - if (image.decode(p.image_gpa, bytes)) |decoded| { + if (state.raw.len == 0) return; // nothing was readable at open + if (image.decode(p.image_gpa, state.raw)) |decoded| { state.rgba = decoded.rgba; state.iw = decoded.w; state.ih = decoded.h; @@ -3771,9 +3784,15 @@ pub const Pdf = struct { sections_output: ?SectionsOutput = null, outline_reveal_pending: ?pdf.OutlineInternalDestination = null, + /// Read whole and opened from memory (the bridge copies), never from + /// the file itself: MuPDF reads a file lazily at every page, and the + /// path may be a mount this editor serves, which only a read that + /// gives the turn up can come back from (`filesystem.readFile`). pub fn open(gpa: std.mem.Allocator, path: []const u8, page_one_based: usize) !@This() { const local = filesystem.localPath(path) orelse return error.NonLocalPath; - return initDocument(gpa, path, try Document.open(local), page_one_based); + const bytes = try filesystem.readFile(gpa, local); + defer gpa.free(bytes); + return initDocument(gpa, path, try Document.openBytes(bytes), page_one_based); } pub fn openBytes(gpa: std.mem.Allocator, path: []const u8, bytes: []const u8, page_one_based: usize) !@This() { @@ -5219,6 +5238,10 @@ pub const Pdf = struct { page_one_based: usize, ) !*pardes.Pane { if (comptime !enabled) return error.PdfDisabled; + // The slot stays ours across the read: another request may make a + // pane meanwhile. + core.reserved_slots[id] = true; + defer core.reserved_slots[id] = false; var state = if (filesystem.localPath(path) != null) try State.open(core.pdf_gpa, path, page_one_based) else virtual: { @@ -5903,9 +5926,11 @@ pub const Pdf = struct { null, }, .x = text_x, - .y = core.bodyTop(rect), + // Below the notice chips, like an image: a placed page is + // drawn after the cells and would paint a chip out. + .y = core.bodyTop(rect) + pane.notices.len, .w = text_width, - .h = rect.h -| pardes.BOX_H, + .h = (rect.h -| pardes.BOX_H) -| pane.notices.len, .rgba = placed.rgba, .iw = placed.width, // The texture contains the retained band, not the full page. -- cgit v1.3