diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-16 15:49:12 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-18 23:44:42 -0300 |
| commit | 1551e409c31992437cb2fa864f576d45c8433801 (patch) | |
| tree | e2fae8451f87b735a1360c7c2e383fdc40165789 /build.zig | |
| parent | be2a9957708cbf0c478ca861c4a1f0f227bbfe10 (diff) | |
| download | pardes-1551e409c31992437cb2fa864f576d45c8433801.tar.gz pardes-1551e409c31992437cb2fa864f576d45c8433801.zip | |
big slow change: prebuilt shaders (SPIR-V/Metal), core gui reflow, docs, web + snapshot refresh
Diffstat (limited to 'build.zig')
| -rw-r--r-- | build.zig | 163 |
1 files changed, 94 insertions, 69 deletions
@@ -21,12 +21,12 @@ pub const macos_min_version: std.SemanticVersion = .{ .major = 13, .minor = 0, . const zls_version = "0.16.1-dev+3e0d0820"; pub const TreeSitterGrammars = enum { disabled, zig, minimal, full }; -/// The GUI shell's shaders, spelled ONCE. Three things read this list: the -/// eight SPIR-V imports the gui module embeds, the glslc runs that produce -/// them, and `zig build shaders`, which writes the same set into the tracked -/// shaders/prebuilt/. Each name is `shaders/<name>.glsl`, and the `.vert`/ -/// `.frag` in it is also the glslc shader stage — adding a shader is adding a -/// name here plus the @embedFile in src/gui/gui.zig. +/// The GUI shell's shaders, spelled ONCE. The runtime SPIR-V imports, their +/// EffectCode source imports, and `zig build shaders` all read this list. That +/// last step writes BOTH the bytecode and its exact source snapshot into the +/// tracked shaders/prebuilt/ pair. Each name is `shaders/<name>.glsl`, and the +/// `.vert`/`.frag` in it is also the glslc shader stage — adding a shader is +/// adding a name here plus the @embedFile in src/gui/gui.zig. const gui_shaders = [_][]const u8{ "ui.vert", "ui.frag", "overlay.vert", "overlay.frag", @@ -34,6 +34,10 @@ const gui_shaders = [_][]const u8{ "crt.vert", "crt.frag", }; +/// Every GUI shader is also a source import for EffectCode. Live builds import +/// shaders/*.glsl; prebuilt builds import the source snapshot paired with the +/// committed SPIR-V, so the builtin cannot print code other than what produced +/// the bytecode that build executes. pub fn build(b: *std.Build) void { const platform = b.option(Platform, "platform", "which shell to build (tty, gui, web, macos)") orelse .tty; // Default target is the Steam Deck (deckcap's trick): x86_64 linux-gnu @@ -105,16 +109,17 @@ pub fn build(b: *std.Build) void { // here needs: glslc, which ships with the Vulkan SDK / shaderc and is not // on a stock machine. It is also the input that changes least often, so // -Dprebuilt-shaders decouples the two: the SPIR-V compiled from - // shaders/*.glsl is committed under shaders/prebuilt/, and this flag - // embeds that copy instead of shelling out. `-Dplatform=gui` then builds - // with nothing but a C toolchain. + // shaders/*.glsl and its exact GLSL snapshot are committed together under + // shaders/prebuilt/, and this flag embeds that pair instead of shelling + // out. `-Dplatform=gui` then builds with nothing but a C toolchain. // // Off by default because it trades a dependency for a freshness problem: - // with the flag on, the .glsl sources are NOT build inputs, so editing one - // changes nothing until someone runs `zig build shaders` (see below). The - // default therefore stays the honest one — compile the shaders that are - // actually in the tree. + // with the flag on, the live .glsl sources are NOT build inputs, so editing + // one changes neither runtime bytecode nor EffectCode until someone runs + // `zig build shaders` (see below). The default therefore stays the honest + // one — compile the shaders that are actually in the tree. const prebuilt_shaders = b.option(bool, "prebuilt-shaders", "embed the committed shaders/prebuilt/*.spv instead of running glslc (-Dplatform=gui without a Vulkan SDK)") orelse false; + const gui_shader_sources_prebuilt = platform == .gui and prebuilt_shaders; // The browser shell is a freestanding wasm core plus ordinary web files. // JavaScript owns the loop and IO; HTML/CSS own rendering. @@ -132,23 +137,28 @@ pub fn build(b: *std.Build) void { if (!enable_mupdf) pdf_scroll_bench_step.dependOn(&b.addFail("pdf-scroll-bench is unavailable with -Dmupdf=false").step); - // The other half of -Dprebuilt-shaders: recompile every shader and write - // the SPIR-V back into shaders/prebuilt/, which is TRACKED rather than - // generated-and-gitignored, because a machine without glslc cannot - // regenerate it. Deliberately independent of -Dplatform: whoever changes a - // shader runs `zig build shaders` on a machine that has the compiler and - // commits the diff (`jj diff shaders/prebuilt` says whether the cache had - // drifted), and no gui build is involved either way. - const shaders_step = b.step("shaders", "recompile shaders/*.glsl into the tracked shaders/prebuilt/*.spv (glslc; refreshes what -Dprebuilt-shaders embeds)"); + // The other half of -Dprebuilt-shaders: recompile every shader and update + // BOTH tracked artifacts — SPIR-V plus the exact GLSL EffectCode will print. + // A machine without glslc can then consume the pair without consulting the + // live source. Deliberately independent of -Dplatform: whoever changes a + // shader runs this on a machine that has the compiler and commits the diff + // (`jj diff shaders/prebuilt` says whether the pair drifted). + const shaders_step = b.step("shaders", "refresh paired shaders/prebuilt/*.spv + *.glsl snapshots (glslc)"); const update_shaders = b.addUpdateSourceFiles(); - for (gui_shaders) |name| + for (gui_shaders) |name| { update_shaders.addCopyFileToSource(compileGlsl(b, name), b.fmt("shaders/prebuilt/{s}.spv", .{name})); + update_shaders.addCopyFileToSource( + b.path(b.fmt("shaders/{s}.glsl", .{name})), + b.fmt("shaders/prebuilt/{s}.glsl", .{name}), + ); + } shaders_step.dependOn(&update_shaders.step); // Web LOOK has a tiny read-only source filesystem. Generate one Zig module - // on the host from every tracked .zig path + its current working-tree - // contents. The generator prefers Git as the tracked-file filter and has a - // conservative filesystem fallback for source archives without `.git`. + // on the host from every tracked or new/nonignored .zig path + its current + // working-tree contents. The generator prefers Git as the ignore-aware + // path filter and has a conservative filesystem fallback for source + // archives without `.git`. const source_gen = b.addExecutable(.{ .name = "pardes-embed-zig-sources", .root_module = b.createModule(.{ @@ -164,7 +174,7 @@ pub fn build(b: *std.Build) void { // discovery intentionally runs whenever a consumer builds it. run_source_gen.has_side_effects = true; const inspect_sources = b.addInstallFileWithDir(embedded_sources, .{ .custom = "web" }, "embedded_sources.zig"); - b.step("embed-web-sources", "generate zig-out/web/embedded_sources.zig from tracked .zig files").dependOn(&inspect_sources.step); + b.step("embed-web-sources", "generate zig-out/web/embedded_sources.zig from working-tree .zig files").dependOn(&inspect_sources.step); snap_build.addWeb(b); @@ -192,7 +202,7 @@ pub fn build(b: *std.Build) void { .link_libc = !is_web, }); // helix differential harness (test/hxdiff.zig): a second compilation of - // the sans-IO core, driven headlessly. Mirrors root_mod's wiring for + // the core, driven headlessly. Mirrors root_mod's wiring for // everything src/pardes.zig pulls in (ghostty-vt, tree-sitter, zstbi, the // option modules); imported as "pardes" by the harness exe below. const hx_core_mod = b.createModule(.{ @@ -256,6 +266,9 @@ pub fn build(b: *std.Build) void { opts.addOption(bool, "enable_tracy", tracy != null); opts.addOption(bool, "zls_backend", zls_backend); opts.addOption(bool, "mupdf", enable_mupdf); + // Meaningful only for the SDL shell. Keeping the platform condition here + // prevents Config/EffectCode from describing tty/macOS/web as "prebuilt". + opts.addOption(bool, "gui_shader_sources_prebuilt", gui_shader_sources_prebuilt); // Which ZLS is compiled in, for `SPC l i`. Kept next to the dependency it // names: the .zon pins a commit, and a status screen that cannot say WHICH // analyser answered is not worth opening. @@ -648,6 +661,16 @@ pub fn build(b: *std.Build) void { root_mod.addOptions("pardes_config", opts); hx_core_mod.addOptions("pardes_config", opts); + const gui_effect_source_dir = if (gui_shader_sources_prebuilt) "shaders/prebuilt" else "shaders"; + for ([_]*std.Build.Module{ root_mod, hx_core_mod }) |mod| { + if (platform == .gui) for (gui_shaders) |name| mod.addAnonymousImport( + b.fmt("effect-source-{s}.glsl", .{name}), + .{ .root_source_file = b.path(b.fmt("{s}/{s}.glsl", .{ gui_effect_source_dir, name })) }, + ); + if (platform == .macos) mod.addAnonymousImport("effect-source-crt.ci.metal", .{ + .root_source_file = b.path("shaders/crt.ci.metal"), + }); + } // SDL3 shell wiring (-Dplatform=gui). SDL3 and FreeType are both built // from source as static libraries; gui.zig @cImports their headers plus @@ -733,7 +756,7 @@ pub fn build(b: *std.Build) void { // one. Deliberately NOT here: an xcframework, lipo and an Xcode // project. The first two are for a UNIVERSAL binary and this ships // arm64; the third is a second build system to keep in step for what - // a plist and three install steps already do. ghostty's + // a plist and a few install steps already do. ghostty's // src/build/GhosttyXCFramework.zig is the map if that day comes. const header = b.addTranslateC(.{ .root_source_file = b.path("src/macos/pardes.h"), @@ -781,7 +804,7 @@ pub fn build(b: *std.Build) void { // // A bundle is a directory with a plist, a binary and an icon in it, so // it is assembled HERE rather than by a script the build shells out to. - // Every input is a file the graph knows — the archive, the three Swift + // Every input is a file the graph knows — the archive, the four Swift // sources, the header, the plist, the icon generator — which is what // makes the app rebuild when one of them moves and stay untouched when // none does. The script this replaced took an install PREFIX and @@ -824,10 +847,28 @@ pub fn build(b: *std.Build) void { link.addFileArg(b.path("src/macos/pardes.h")); link.addArg("-o"); const app_bin = link.addOutputFileArg("pardes"); - for ([_][]const u8{ "main.swift", "AppDelegate.swift", "PardesView.swift" }) |src| + for ([_][]const u8{ + "main.swift", + "AppDelegate.swift", + "PardesView.swift", + "ScenePostprocessor.swift", + "FileWatcher.swift", + }) |src| link.addFileArg(b.path(b.fmt("src/macos/Sources/{s}", .{src}))); link.addFileArg(lib_archive); - link.addArgs(&.{ "-lc++", "-framework", "AppKit", "-framework", "CoreText", "-framework", "CoreGraphics" }); + link.addArgs(&.{ + "-lc++", + "-framework", + "AppKit", + "-framework", + "CoreText", + "-framework", + "CoreGraphics", + "-framework", + "CoreImage", + "-framework", + "Metal", + }); // The icon is generated, not committed: the mark is drawn out of // the same palette PardesView.swift renders cells with, so a colour @@ -854,6 +895,14 @@ pub fn build(b: *std.Build) void { const install_app_bin = b.addInstallFileWithDir(app_bin, .{ .custom = "pardes.app/Contents/MacOS" }, "pardes"); const install_plist = b.addInstallFileWithDir(plist_out, .{ .custom = "pardes.app/Contents" }, "Info.plist"); + // Runtime Metal compilation keeps this exact source shared with + // EffectCode's build-time embedding; no generated Swift literal + // or second shader copy can drift from what the app executes. + const install_scene_kernel = b.addInstallFileWithDir( + b.path("shaders/crt.ci.metal"), + .{ .custom = "pardes.app/Contents/Resources" }, + "crt.ci.metal", + ); // CFBundleIconFile names this without its extension; without both // halves the Dock shows the generic blank page. const install_icon = b.addInstallFileWithDir(icon_dir.path(b, "pardes.icns"), .{ .custom = "pardes.app/Contents/Resources" }, "pardes.icns"); @@ -865,7 +914,7 @@ pub fn build(b: *std.Build) void { // asking for — both are notarization's requirements rather than a // signature's, and --timestamp on an ad-hoc signature is an error. // - // It signs the DIRECTORY, so it must follow all three installs: a + // It signs the DIRECTORY, so it must follow all four installs: a // signature taken before the icon lands is a signature the icon // then breaks. Always runs, because the bundle it edits lives // outside the cache and zig cannot know what is in it. @@ -876,6 +925,7 @@ pub fn build(b: *std.Build) void { sign.step.dependOn(&install_app_bin.step); sign.step.dependOn(&install_plist.step); sign.step.dependOn(&install_icon.step); + sign.step.dependOn(&install_scene_kernel.step); app_step.dependOn(&sign.step); // ...and the thing you hand someone. UDZO is the compressed @@ -935,11 +985,10 @@ pub fn build(b: *std.Build) void { e2e_step.dependOn(&b.addFail("macos-e2e needs a Darwin target; drop -Dtarget= or pass -Dtarget=native").step); // Same step name the other native platforms use, because in this - // configuration theirs is not declared: the ABI guard and the core's - // own inline tests are what `-Dplatform=macos` has to keep green. - const unit_step = b.step("unit-test", "run the C-ABI guard and the core unit tests"); + // configuration theirs is not declared. macos.zig imports the core, so + // this one aggregate reaches both the C-ABI guard and the core tests. + const unit_step = b.step("unit-test", "run the C-ABI guard and core unit tests"); unit_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = root_mod })).step); - unit_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = hx_core_mod })).step); web_step.dependOn(&b.addFail("web needs -Dplatform=web -Dtarget=wasm32-freestanding -Ddump=<dump.zon>").step); } else { // binary name: the native-shaped linux-x86_64 tty build stays `pardes` @@ -1010,7 +1059,7 @@ pub fn build(b: *std.Build) void { pdf_harness_step.dependOn(&b.addFail("pdf-harness is unavailable with -Dmupdf=false").step); } - // helix differential harness: drive the sans-IO core headlessly over + // helix differential harness: drive the core headlessly over // JSON-Lines cases, one contract result line per case on stdout — // the pardes half of the pardes-vs-helix diff (helix's hx-harness on // its pardes-harness branch speaks the same contract). @@ -1101,23 +1150,7 @@ pub fn build(b: *std.Build) void { run_perf.setCwd(b.path(".")); b.step("perf", "large-file / long-line latency table (-- [--json] [--reps N] [--base old.json])").dependOn(&run_perf.step); - // modal.zig is pure std — its inline unit tests run here - const unit = b.addTest(.{ .root_module = b.createModule(.{ - .target = target, - .optimize = optimize, - .root_source_file = b.path("src/modal.zig"), - }) }); - const unit_step = b.step("unit-test", "run the modal.zig + crt.zig + deck replay + core unit tests"); - unit_step.dependOn(&b.addRunArtifact(unit).step); - // Native config path/loading is std-only and has its own hermetic - // filesystem/environment tests. The core receives bytes through - // Options, so this IO module stays outside the core module graph. - const user_config_test = b.addTest(.{ .root_module = b.createModule(.{ - .target = target, - .optimize = optimize, - .root_source_file = b.path("src/user_config.zig"), - }) }); - unit_step.dependOn(&b.addRunArtifact(user_config_test).step); + const unit_step = b.step("unit-test", "run native shell and module unit tests"); // Secure tempfile creation is native-shell IO, isolated from the core // and tested as its own libc-linked module. const temp_file_test = b.addTest(.{ .root_module = b.createModule(.{ @@ -1180,21 +1213,13 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("src/gui/deck.zig"), })); unit_step.dependOn(&b.addRunArtifact(deck_test).step); - // the core's own inline tests (config.zig's wheel drift guard). It - // links the SAME module hxdiff does rather than a fourth hand-wired - // copy of ghostty-vt/tree-sitter/zstbi — a second wiring of that graph - // is a second thing to keep in step. - const core_test = b.addTest(.{ .root_module = hx_core_mod }); - unit_step.dependOn(&b.addRunArtifact(core_test).step); - // gui.zig's own inline tests (the fractional-scroll motion). - // Only reachable with -Dplatform=gui, and it links the SAME module - // the gui exe does - // for the same reason core_test does: SDL, the shaders and the font - // are already wired onto it, and a second wiring is a second thing to - // keep in step. - if (platform == .gui) { - const gui_test = b.addTest(.{ .root_module = root_mod }); - unit_step.dependOn(&b.addRunArtifact(gui_test).step); + // Shell-specific inline tests. main() imports a shell inside its + // runtime switch, which test analysis never enters. main.zig's test + // block names the selected shell, user config, and platform-only + // helpers; its ordinary core import reaches pardes.zig and modal.zig. + if (platform == .tty or platform == .gui) { + const shell_test = b.addTest(.{ .root_module = root_mod }); + unit_step.dependOn(&b.addRunArtifact(shell_test).step); } } } |
