summaryrefslogtreecommitdiff
path: root/src/detached
diff options
context:
space:
mode:
Diffstat (limited to 'src/detached')
-rw-r--r--src/detached/client.zig33
-rw-r--r--src/detached/server.zig36
-rw-r--r--src/detached/wire.zig77
3 files changed, 139 insertions, 7 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 4d2d3882..a08a0342 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();
@@ -769,6 +778,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;
}
@@ -826,6 +836,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;
@@ -894,7 +906,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();
@@ -1105,6 +1117,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 3326277d..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
@@ -13,7 +16,7 @@ pub const version: u16 = 8;
/// The Chrome palette is carried field by field as its declaration lists
/// them, so any change to it changes the wire: bump `version`, then this.
-const chrome_layout: u64 = 0x7f982e44ef87de72;
+const chrome_layout: u64 = 0x5e758b47fb0cf8da;
comptime {
@setEvalBranchQuota(10_000);
var hash = std.hash.Fnv1a_64.init();
@@ -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
@@ -270,6 +276,7 @@ pub const Frame = struct {
region.notice = try r.getByte();
region.clip = try r.getU16();
region.lift = @bitCast(try r.getU32());
+ region.line = @bitCast(try r.getU32());
// A springy flavour overshoots, and a wind-up dips below.
if (!(region.lift >= -1 and region.lift <= 2)) return error.BadValue;
if (region.rect.x > f.cols or region.rect.w > f.cols - region.rect.x or
@@ -352,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];
+ }
};
// ---------------------------------------------------------------------------
@@ -754,7 +777,7 @@ pub fn layersBound(layers: []const pardes.Layer, tags: []const pardes.Layer, reg
return n + regions.len * region_len + chrome_len;
}
-const region_len = 1 + 1 + 4 + 8 + 1 + 2 + 2 + 1 + 2 + 4;
+const region_len = 1 + 1 + 4 + 8 + 1 + 2 + 2 + 1 + 2 + 4 + 4;
/// The palette: each colour three bytes, a theme's optional ones a flag
/// before them.
const chrome_len = @typeInfo(pardes.Chrome).@"struct".fields.len * 4 + 8 * 4;
@@ -839,6 +862,7 @@ pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, point
try w.putByte(region.notice);
try w.putU16(region.clip);
try w.putU32(@bitCast(region.lift));
+ try w.putU32(@bitCast(region.line));
}
inline for (@typeInfo(pardes.Chrome).@"struct".fields) |field| {
const rgb = @field(chrome, field.name);
@@ -1147,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();
@@ -1168,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;
+ },
};
}
@@ -1230,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;
@@ -1347,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" {
@@ -1355,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 => {},
};
}