diff options
Diffstat (limited to 'src/detached')
| -rw-r--r-- | src/detached/client.zig | 33 | ||||
| -rw-r--r-- | src/detached/server.zig | 36 | ||||
| -rw-r--r-- | src/detached/wire.zig | 71 |
3 files changed, 135 insertions, 5 deletions
diff --git a/src/detached/client.zig b/src/detached/client.zig index 8c9a2560..69ab2fdd 100644 --- a/src/detached/client.zig +++ b/src/detached/client.zig @@ -109,6 +109,10 @@ pub const Client = struct { regions: std.ArrayListUnmanaged(pardes.Region) = .empty, /// The palette its chrome is drawn in, as its last frame gave it. chrome: pardes.Chrome = .{}, + /// The post chain its frames are drawn through, as the session last + /// sent it; its paths and SPIR-V owned here. `post_key` moves with it. + post: wire.Post = .{}, + post_key: u32 = 0, in: std.ArrayListUnmanaged(u8) = .empty, out: std.ArrayListUnmanaged(u8) = .empty, /// Bytes of `in` belonging to the message `next` returned last. Compacted at @@ -187,6 +191,7 @@ pub const Client = struct { } for (&c.tag_layers) |*layer| c.gpa.free(layer.cells); c.regions.deinit(c.gpa); + c.freePost(); c.in.deinit(c.gpa); c.out.deinit(c.gpa); } @@ -356,10 +361,30 @@ pub const Client = struct { // open a URI on this display or read this display's selection back // over the socket. `Ungreeted` is the same refusal a frame gets. .set_clipboard, .read_clipboard, .open_link => if (!c.attached()) return error.Ungreeted, + // Kept, like the chrome: the frames that follow are drawn + // through it. + .post => |chain| { + if (!c.attached()) return error.Ungreeted; + c.freePost(); + c.post = chain; + for (c.post.passes[0..c.post.len]) |*pass| { + pass.path = c.gpa.dupe(u8, pass.path) catch return error.Closed; + pass.spirv = c.gpa.dupe(u8, pass.spirv) catch return error.Closed; + } + c.post_key +%= 1; + }, } return msg; } + fn freePost(c: *Client) void { + for (c.post.passes[0..c.post.len]) |pass| { + c.gpa.free(pass.path); + c.gpa.free(pass.spirv); + } + c.post = .{}; + } + // ---- internals -------------------------------------------------------- fn reshape(c: *Client, cols: u16, rows: u16) (Error || wire.Error)!void { @@ -1131,7 +1156,7 @@ test "detached session: the seam's own routing rules, per surviving effect" { test "detached session: a frontend is never asked to fork, write, or watch" { // THE INVARIANT OF THE WHOLE DETACHED DESIGN, pinned as a property of the // protocol rather than of one code path: a frontend is input and screen, so - // the set of messages that can reach it is exactly the eight below. If a + // the set of messages that can reach it is exactly the nine below. If a // machine-local effect is ever put back on the wire, a frontend becomes the // process that owns a pane's shell again — and a pane whose shell belongs to // a frontend dies when that frontend leaves, which is the bug this replaced. @@ -1149,6 +1174,10 @@ test "detached session: a frontend is never asked to fork, write, or watch" { // The three that need THIS human's display and cannot be done by a // daemon nobody is looking at. "set_clipboard", "read_clipboard", "open_link", + // Display state, like a frame's chrome: the passes this display + // draws its frames through. A file's SPIR-V arrives compiled, so the + // frontend still reads no disk and runs no program. + "post", }; // One: the union a frontend decodes into has no other variant. Named @@ -1183,7 +1212,7 @@ test "detached session: a frontend is never asked to fork, write, or watch" { if (@intFromEnum(want) == tag) break true; } else false; if (!named) { - std.debug.print("tag 0x{x:0>2} is decodable by a frontend and is not one of the seven\n", .{tag}); + std.debug.print("tag 0x{x:0>2} is decodable by a frontend and is not one of the nine\n", .{tag}); return error.MachineLocalEffectOnTheWire; } } 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); diff --git a/src/detached/wire.zig b/src/detached/wire.zig index 6903a1e8..ee8c72fb 100644 --- a/src/detached/wire.zig +++ b/src/detached/wire.zig @@ -3,6 +3,9 @@ const std = @import("std"); const pardes = @import("../pardes.zig"); const limits = @import("../memory.zig").limits; +const shader_build = @import("../shader_build.zig"); +const Scene = pardes.config.Runtime.Scene; +const ShaderAnimation = pardes.config.Runtime.ShaderAnimation; /// 7: tag layers gained the `.notice` kind, so a frame can carry a tag value /// an older frontend would reject mid-session. The greeting is where that has @@ -73,6 +76,9 @@ pub const ServerTag = enum(u8) { set_clipboard = 0x10, read_clipboard = 0x11, open_link = 0x12, + /// The post chain this frontend draws its frames through: sent on + /// attach and whenever it or a file's SPIR-V changes. + post = 0x13, }; /// Why the server hung up on a connect. Sent as a `refuse` and followed by a @@ -353,6 +359,22 @@ pub const ServerMsg = union(enum) { set_clipboard: []const u8, read_clipboard, open_link: []const u8, + post: Post, +}; + +/// The session's post chain, as a GUI attached to it draws it: its passes in +/// order, each a bundled scene at its level or a Shadertoy file's SPIR-V +/// (compiled by the session, shader_build.zig: a frontend reads no disk and +/// runs no program), and when it animates on its own. Slices borrow the +/// payload. +pub const Post = struct { + animation: ShaderAnimation = .on, + passes: [shader_build.max]shader_build.Pass = @splat(.{}), + len: u8 = 0, + + pub fn list(p: *const Post) []const shader_build.Pass { + return p.passes[0..p.len]; + } }; // --------------------------------------------------------------------------- @@ -1149,6 +1171,18 @@ pub fn encodeServer(out: []u8, msg: ServerMsg) Error![]const u8 { .set_clipboard => |t| try w.putSlice32(t), .read_clipboard => {}, .open_link => |u| try w.putSlice16(u), + .post => |p| { + try w.putByte(@intFromEnum(p.animation)); + try w.putByte(p.len); + for (p.list()) |pass| { + // 0 a file, else a bundled scene and one. + try w.putByte(if (pass.scene) |scene| @as(u8, @intFromEnum(scene)) + 1 else 0); + try w.putByte(pass.level); + try w.putU32(pass.revision); + try w.putSlice16(pass.path); + try w.putSlice32(pass.spirv); + } + }, } try finishMessage(&w, at); return w.written(); @@ -1170,6 +1204,11 @@ pub fn serverBound(msg: ServerMsg) usize { .frame => |f| frameBound(f.cols, f.rows), .set_clipboard => |t| t.len, .open_link => |u| u.len, + .post => |p| blk: { + var n: usize = 0; + for (p.list()) |pass| n += 12 + pass.path.len + pass.spirv.len; + break :blk n; + }, }; } @@ -1232,6 +1271,22 @@ pub fn decodeServer(tag: u8, payload: []const u8) Error!ServerMsg { .set_clipboard => .{ .set_clipboard = try r.getSlice32() }, .read_clipboard => .read_clipboard, .open_link => .{ .open_link = try r.getSlice16() }, + .post => blk: { + var p: Post = .{ .animation = try r.getTag(ShaderAnimation) }; + p.len = try r.getByte(); + if (p.len > shader_build.max) return error.Overlong; + for (p.passes[0..p.len]) |*pass| { + const kind = try r.getByte(); + pass.* = .{ + .scene = if (kind == 0) null else std.enums.fromInt(Scene, kind - 1) orelse return error.BadValue, + .level = try r.getByte(), + .revision = try r.getU32(), + .path = try r.getSlice16(), + .spirv = try r.getSlice32(), + }; + } + break :blk .{ .post = p }; + }, }; try r.end(); return msg; @@ -1349,6 +1404,18 @@ test "detached wire: every server message round-trips" { try testing.expectEqualStrings("yank", (try roundServer(&buf, .{ .set_clipboard = "yank" })).set_clipboard); try testing.expectEqual(ServerMsg.read_clipboard, try roundServer(&buf, .read_clipboard)); try testing.expectEqualStrings("https://x", (try roundServer(&buf, .{ .open_link = "https://x" })).open_link); + var chain: Post = .{ .animation = .always, .len = 2 }; + chain.passes[0] = .{ .scene = .bloom, .level = 3 }; + chain.passes[1] = .{ .path = "~/wave.glsl", .spirv = "\x03\x02\x23\x07spirv", .revision = 7 }; + const back = (try roundServer(&buf, .{ .post = chain })).post; + try testing.expectEqual(ShaderAnimation.always, back.animation); + try testing.expectEqual(@as(u8, 2), back.len); + try testing.expectEqual(Scene.bloom, back.passes[0].scene.?); + try testing.expectEqual(@as(u8, 3), back.passes[0].level); + try testing.expect(back.passes[1].scene == null); + try testing.expectEqualStrings("~/wave.glsl", back.passes[1].path); + try testing.expectEqualStrings("\x03\x02\x23\x07spirv", back.passes[1].spirv); + try testing.expectEqual(@as(u32, 7), back.passes[1].revision); } test "detached wire: only the display's own effects are on the wire" { @@ -1357,9 +1424,9 @@ test "detached wire: only the display's own effects are on the wire" { // is the direction that matters: it would hand a frontend work that must // outlive it. The count fails if one of the three is dropped, which would // silently leave a clipboard or a link unanswered on every frontend. - try testing.expectEqual(@as(usize, 8), std.enums.values(ServerTag).len); + try testing.expectEqual(@as(usize, 9), std.enums.values(ServerTag).len); for (std.enums.values(ServerTag)) |t| switch (t) { - .welcome, .refuse, .frame, .quit, .detach, .set_clipboard, .read_clipboard, .open_link => {}, + .welcome, .refuse, .frame, .quit, .detach, .set_clipboard, .read_clipboard, .open_link, .post => {}, }; } |
