From 826ec1fac3c012e71ab34207328faaac5b5827b7 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 09:43:05 -0300 Subject: Shader files compile again on save; an attached GUI runs the session's post chain shader_build.zig takes the Shadertoy compile out of the GUI's Post: the process that holds the core compiles each chain file behind the prefix on a thread, watches its directory (file_watch's new shader slots), and on a change reads and hashes each file, compiling only one whose bytes moved. A failed compile keeps the last good SPIR-V and is said once; the same bytes are never compiled or reported twice. A file is read with std, not fs.zig's readFile, whose turn hand-off a worker thread does not hold. A detached session compiles for its frontends and sends them the chain (wire post: each pass's scene and level or the file's SPIR-V, and ShaderAnimation) on attach and on every change, so an attached GUI runs the same passes, levels and animation as a local one while still reading no disk and running no program. The attached GUI describes its frames to the chain from the session's chrome and redraws an animating chain on its own. --- src/detached/server.zig | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) (limited to 'src/detached/server.zig') diff --git a/src/detached/server.zig b/src/detached/server.zig index 13cf4488..99a2afc2 100644 --- a/src/detached/server.zig +++ b/src/detached/server.zig @@ -11,6 +11,7 @@ const host_io = @import("../host_io.zig"); const selection_pipe = @import("../selection_pipe.zig"); const file_watch = @import("../file_watch.zig"); +const shader_build = @import("../shader_build.zig"); const ninep_io = @import("../9p_io.zig"); @@ -63,6 +64,8 @@ const Client = struct { mirror: std.ArrayListUnmanaged(pardes.Cell) = .empty, need_full: bool = true, accepted_ms: i64 = 0, + /// The post chain last sent (Session.sendPost); null: none yet. + post_sent: ?u64 = null, }; const Pty = struct { @@ -173,6 +176,8 @@ pub const Session = struct { /// the next poll returns at once, to draw it. ninep_wake: std.atomic.Value(bool) = .init(false), watches: file_watch.Table = @splat(null), + /// The post chain's files, compiled here for the GUIs attached. + shaders: shader_build = .{}, check_files: bool = false, in_loop: bool = false, greet_deadline_ms: u32 = greet_deadline_default_ms, @@ -254,6 +259,8 @@ pub const Session = struct { for (0..pardes.MAX_PANES) |pane| file_watch.watchPane(s.inotify_fd, &s.watches, @intCast(pane), null, 0, .{ .text = 0 }); _ = file_watch.applyThemeEffect(s.core, s.gpa, s.inotify_fd, &s.watches, 0, false, false); + // The new core's chain generations are its own. + s.shaders.generation = null; s.check_files = false; if (s.ninep) |listener| listener.reset(replacement); s.ninep_pending = false; @@ -267,6 +274,8 @@ pub const Session = struct { } pub fn deinit(s: *Session) void { + // First: its compile thread wakes the mailbox, closed below. + s.shaders.deinit(s.gpa, s.inotify_fd, &s.watches); if (s.mailbox.wake[0] >= 0) pardes.lsp.setStatusSink(null, null); if (s.mailbox.wake[0] >= 0) host_io.setExitWake(null, null); s.cancelWorkers(); @@ -766,6 +775,7 @@ pub const Session = struct { fn reloadWatched(s: *Session) void { if (!s.check_files) return; s.check_files = false; + s.shaders.recheck(); for (0..reload_retries) |_| { if (!file_watch.reloadChanged(s.core, s.io, s.gpa, &s.watches)) return; } @@ -823,6 +833,8 @@ pub const Session = struct { const completed = s.ninep_wake.swap(false, .acq_rel) or drained; for (&s.clients) |*c| if (c.fd >= 0) s.flush(c); const regridded = s.reconcile(); + // After the greeting reconcile sends: a chain before it is refused. + const compiled = s.sendPost(); // Nobody attached, nobody to dismiss a message: the loop expires // them itself, between the editor's steps (expireUnattended). s.core.unattended = s.primary() == null; @@ -891,7 +903,7 @@ pub const Session = struct { } var timeout: c_int = if (timeout_ms == 0) -1 else @intCast(@min(timeout_ms, std.math.maxInt(c_int))); if (s.nextWake(now)) |due| timeout = if (timeout < 0) due else @min(timeout, due); - if (completed or s.check_files or regridded or s.ninep_pending) timeout = 0; + if (completed or compiled or s.check_files or regridded or s.ninep_pending) timeout = 0; pardes.turn.rest(); const ready = libc.poll(&fds, @intCast(n), timeout); pardes.turn.wake(); @@ -1102,6 +1114,28 @@ pub const Session = struct { return regridded; } + /// Compiles the post chain's files (a finished compile wakes the loop + /// through the mailbox) and sends each attached frontend the chain it + /// has not seen: on attach, and when it or a file's SPIR-V changes. True + /// when a compile was taken in, so the loop draws what it said. + /// ponytail: a terminal frontend is sent it too and drops it; a hello + /// saying which frontend it is would spare those bytes. + fn sendPost(s: *Session) bool { + const files = for (s.core.settings.post.list()) |entry| { + if (entry.scene == null) break true; + } else false; + const compiled = s.shaders.sync(s.gpa, s.io, s.core, if (files) s.inotify() else s.inotify_fd, &s.watches, .{ .ctx = &s.mailbox, .call = wakeForExit }); + const key = @as(u64, s.shaders.revision) << 8 | @intFromEnum(s.core.settings.shader_animation); + var chain: wire.Post = .{ .animation = s.core.settings.shader_animation, .len = s.core.settings.post.len }; + _ = s.shaders.view(&s.core.settings.post, &chain.passes); + for (&s.clients) |*c| { + if (!c.attached or c.greet or c.post_sent == key) continue; + c.post_sent = key; + s.send(c, .{ .post = chain }); + } + return compiled; + } + fn send(s: *Session, c: *Client, msg: wire.ServerMsg) void { const want = wire.serverBound(msg); s.scratch.ensureTotalCapacity(s.gpa, want) catch return s.close(c, .oom); -- cgit v1.3