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/wire.zig | 71 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 2 deletions(-) (limited to 'src/detached/wire.zig') 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 => {}, }; } -- cgit v1.3