diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 16:26:08 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | be09d1ec7abd71f4e52f05942a941d76b6d420f2 (patch) | |
| tree | 463ddfbaeec7529635a7730422faf22591254d2a /src/gui/Post.zig | |
| parent | c6a5aaf5c4a020eaefe24943ecb6cf9d4eb26836 (diff) | |
| download | pardes-be09d1ec7abd71f4e52f05942a941d76b6d420f2.tar.gz pardes-be09d1ec7abd71f4e52f05942a941d76b6d420f2.zip | |
Post chain review fixes: glslc on stdin, level A at the display's refresh
Stage 9 review: a user's Shadertoy file goes to glslc on its stdin, so no
source file is written anywhere (the old predictable name under
XDG_RUNTIME_DIR or /tmp was a symlink-clobber hazard). std's spawn does
all its allocation before fork and searches PATH on the stack after it,
said at the call. Level A redraws are paced by the display's refresh rate
(SDL_GetCurrentDisplayMode), not a fixed 16 ms.
Shared files touched: gui.zig.
Diffstat (limited to 'src/gui/Post.zig')
| -rw-r--r-- | src/gui/Post.zig | 83 |
1 files changed, 50 insertions, 33 deletions
diff --git a/src/gui/Post.zig b/src/gui/Post.zig index d92623d7..c529efc0 100644 --- a/src/gui/Post.zig +++ b/src/gui/Post.zig @@ -15,7 +15,6 @@ const libc = std.c; const ghostty_vt = @import("ghostty-vt"); const pardes = @import("../pardes.zig"); const filesystem = @import("../fs.zig"); -const host_io = @import("../host_io.zig"); const gui = @import("gui.zig"); const c = gui.c; @@ -191,8 +190,9 @@ fn collect(post: *Post, gpa: std.mem.Allocator, device: *c.SDL_GPUDevice, format } } -/// The compile thread: each file behind the prefix, through glslc exactly as -/// the build calls it (build.zig compileGlsl), its SPIR-V on stdout. +/// The compile thread: each file behind the prefix, through glslc with the +/// build's own flags (build.zig compileGlsl), source on stdin, SPIR-V on +/// stdout. fn compile(job: *Job) void { defer job.done.store(true, .release); defer { @@ -200,10 +200,10 @@ fn compile(job: *Job) void { event.type = c.SDL_EVENT_USER; _ = c.SDL_PushEvent(&event); } - for (job.paths[0..job.len], 0..) |path, i| job.results[i] = compileOne(job.gpa, job.io, path, i); + for (job.paths[0..job.len], 0..) |path, i| job.results[i] = compileOne(job.gpa, job.io, path); } -fn compileOne(gpa: std.mem.Allocator, io: std.Io, path: []const u8, index: usize) Job.Result { +fn compileOne(gpa: std.mem.Allocator, io: std.Io, path: []const u8) Job.Result { var home_buf: [4096]u8 = undefined; const expanded = if (std.mem.startsWith(u8, path, "~/")) if (libc.getenv("HOME")) |home| std.fmt.bufPrint(&home_buf, "{s}/{s}", .{ std.mem.span(home), path[2..] }) catch path @@ -212,37 +212,54 @@ fn compileOne(gpa: std.mem.Allocator, io: std.Io, path: []const u8, index: usize const body = filesystem.readFile(gpa, expanded) catch |err| return .{ .failed = std.fmt.allocPrint(gpa, "cannot read it ({t})", .{err}) catch return .none }; defer gpa.free(body); - const dir = if (libc.getenv("XDG_RUNTIME_DIR")) |d| std.mem.span(d) else "/tmp"; - var source_buf: [4096]u8 = undefined; - const source = std.fmt.bufPrintSentinel(&source_buf, "{s}/pardes-post-{d}-{d}.frag.glsl", .{ dir, libc.getpid(), index }, 0) catch return .none; - defer _ = libc.unlink(source); - { - const fd = libc.open(source, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, @as(libc.mode_t, 0o600)); - if (fd < 0) return .{ .failed = gpa.dupe(u8, "cannot write its source for glslc") catch return .none }; - defer _ = libc.close(fd); - if (!host_io.writeFd(fd, prefix) or !host_io.writeFd(fd, body)) - return .{ .failed = gpa.dupe(u8, "cannot write its source for glslc") catch return .none }; - } - const run = std.process.run(gpa, io, .{ - .argv = &.{ "glslc", "-fshader-stage=fragment", source, "-o", "-" }, - .stdout_limit = .limited(16 << 20), - .stderr_limit = .limited(64 << 10), + // The prefix and the file go to glslc on its stdin: no file of ours is + // written anywhere. std's spawn does every allocation before fork and + // searches PATH on the stack after it (Io/Threaded.zig spawnPosix, + // posixExecv), so a child forked from this thread cannot wedge on a + // lock another thread held (the forkShell hazard). + var child = std.process.spawn(io, .{ + .argv = &.{ "glslc", "-fshader-stage=fragment", "-o", "-", "-" }, + .stdin = .pipe, + .stdout = .pipe, + .stderr = .pipe, }) catch |err| return switch (err) { error.FileNotFound => .missing, else => .{ .failed = std.fmt.allocPrint(gpa, "glslc did not run ({t})", .{err}) catch return .none }, }; - const clean = switch (run.term) { + defer child.kill(io); + // glslc reads all of its input before it writes a byte, so writing it + // first cannot deadlock against a full output pipe. + { + var stdin = child.stdin.?; + child.stdin = null; + defer stdin.close(io); + var buf: [4096]u8 = undefined; + var writer = stdin.writer(io, &buf); + writer.interface.writeAll(prefix) catch {}; + writer.interface.writeAll(body) catch {}; + writer.interface.flush() catch {}; + } + var streams: std.Io.File.MultiReader.Buffer(2) = undefined; + var reader: std.Io.File.MultiReader = undefined; + reader.init(gpa, io, streams.toStreams(), &.{ child.stdout.?, child.stderr.? }); + defer reader.deinit(); + while (reader.fill(64, .none)) |_| { + if (reader.reader(0).buffered().len > 16 << 20 or reader.reader(1).buffered().len > 64 << 10) + return .{ .failed = gpa.dupe(u8, "glslc said too much") catch return .none }; + } else |err| switch (err) { + error.EndOfStream => {}, + else => return .{ .failed = gpa.dupe(u8, "reading glslc failed") catch return .none }, + } + const term = child.wait(io) catch return .{ .failed = gpa.dupe(u8, "glslc did not finish") catch return .none }; + const clean = switch (term) { .exited => |code| code == 0, else => false, }; - if (clean) { - gpa.free(run.stderr); - return .{ .spirv = run.stdout }; - } - gpa.free(run.stdout); - // glslc names the temporary source; the file is what a person knows. - defer gpa.free(run.stderr); - const named = std.mem.replaceOwned(u8, gpa, run.stderr, source, path) catch return .{ .failed = gpa.dupe(u8, "glslc failed") catch return .none }; + if (clean) return .{ .spirv = reader.toOwnedSlice(0) catch return .none }; + const said = reader.toOwnedSlice(1) catch return .none; + // glslc calls its input <stdin>; the file is what a person knows. + defer gpa.free(said); + const named = std.mem.replaceOwned(u8, gpa, said, "<stdin>", path) catch return .{ .failed = gpa.dupe(u8, "glslc failed") catch return .none }; return .{ .failed = named }; } @@ -535,9 +552,9 @@ test "ghostty's test shaders compile behind the prefix, and its invalid one fail if (std.mem.startsWith(u8, entry.name, "ghostty-")) break entry.name; } else return error.SkipZigTest; var buf: [512]u8 = undefined; - for ([_][]const u8{ "crt", "focus", "invalid" }, 0..) |name, i| { + for ([_][]const u8{ "crt", "focus", "invalid" }) |name| { const path = try std.fmt.bufPrint(&buf, "zig-pkg/{s}/src/renderer/shaders/test_shadertoy_{s}.glsl", .{ ghostty, name }); - const result = compileOne(gpa, io, path, i); + const result = compileOne(gpa, io, path); switch (result) { .missing => return error.SkipZigTest, // no glslc here .spirv => |spirv| { @@ -549,9 +566,9 @@ test "ghostty's test shaders compile behind the prefix, and its invalid one fail .failed => |text| { defer gpa.free(text); try std.testing.expectEqualStrings("invalid", name); - // Named by the file, not by the temporary source glslc read. + // Named by the file, not by glslc's <stdin>. try std.testing.expect(std.mem.indexOf(u8, text, path) != null); - try std.testing.expect(std.mem.indexOf(u8, text, "pardes-post-") == null); + try std.testing.expect(std.mem.indexOf(u8, text, "<stdin>") == null); }, .none => return error.TestUnexpectedResult, } |
