From f2a7652abab5d03f846f3150f9cc1b2dc23bb3dd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 20 Jul 2026 20:33:23 -0700 Subject: [PATCH] mitchell's touchups - benchmark: avoid buffers to avoid a memcpy - build: keep frame pointers on macOS. There was some debug changes from Zig 0.15 and this helps. Also, Apple actually requires/expects x29 to always be a frame pointer. - build/macos: force libSystem symbols instead of compiler-rt - global: add InitOpts.tool so that ghostty-gen/bench can parse their own actions in `+action` - quirks: provide our own vectorized memset. see the comment for more details why. - synthetic: fix UB by accessing global.io before it was initialized - terminal/hash_map: force inline for unique repr types. Zig 0.15 inlined and 0.16 doesn't, measured a huge slowdown in hyperlink benchmarks. - terminal: add explicit `@Vector` usage for storing a run of identical cells as well as for scanning printable cells. This auto-vectorized in Zig 0.15 but not in Zig 0.16. This produces the same assembly. - unicode: properties and LUT need power-of-two backing integer to avoid bad LLVM codegen --- .gitignore | 2 +- build.zig.zon | 22 +- build.zig.zon.json | 32 +-- build.zig.zon.nix | 40 ++-- build.zig.zon.txt | 24 +-- flatpak/zig-packages.json | 34 +-- nix/build-support/update-mirror.nu | 59 +++-- pkg/dcimgui/build.zig.zon | 2 +- pkg/gtk4-layer-shell/build.zig.zon | 2 +- pkg/harfbuzz/build.zig.zon | 2 +- pkg/macos/build.zig.zon | 2 +- pkg/wuffs/build.zig.zon | 2 +- src/benchmark/TerminalStream.zig | 6 +- src/benchmark/cli.zig | 2 +- src/build/Config.zig | 11 + src/build/GhosttyExe.zig | 2 +- src/build/GhosttyLib.zig | 20 +- src/build/GhosttyLibVt.zig | 31 ++- src/build/LibsystemOverrideStep.zig | 51 +++++ src/build/libsystem_override.sh | 123 +++++++++++ src/config/path.zig | 4 +- src/global.zig | 27 ++- src/lib_vt.zig | 6 + src/main.zig | 7 + src/main_bench.zig | 6 + src/main_c.zig | 4 + src/main_gen.zig | 6 + src/quirks_memset.zig | 322 ++++++++++++++++++++++++++++ src/simd/main.zig | 12 ++ src/synthetic/cli.zig | 3 +- src/terminal/Terminal.zig | 155 +++++++++++-- src/terminal/hash_map.zig | 18 +- src/terminal/stream.zig | 24 ++- src/unicode/grapheme.zig | 12 +- src/unicode/props.zig | 12 +- 35 files changed, 936 insertions(+), 151 deletions(-) create mode 100644 src/build/LibsystemOverrideStep.zig create mode 100755 src/build/libsystem_override.sh create mode 100644 src/quirks_memset.zig diff --git a/.gitignore b/.gitignore index 51b929544..c60d3bb15 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,7 @@ zig-pkg/ build-cmake/ CMakeCache.txt CMakeFiles/ -/build.zig.zon.bak +build.zig.zon.bak /result* /.nixos-test-history example/*.wasm diff --git a/build.zig.zon b/build.zig.zon index 8d1553199..6f8287505 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -11,62 +11,62 @@ // Aro/translate-c itself. .translate_c = .{ .lazy = true, - .url = "https://codeberg.org/vancluever/translate-c/archive/a916baf225a3cfc93a982225462db612604ae290.tar.gz", + .url = "https://deps.files.ghostty.org/translate_c-a916baf225a3cfc93a982225462db612604ae290.tar.gz", .hash = "translate_c-0.0.0-Q_BUWj8_BwCr4na219OimiKKj4IVSq0aKtMXCQmIk4AB", }, // Zig libs .libxev = .{ // mitchellh/libxev - .url = "https://github.com/mitchellh/libxev/archive/9ce8e8e6ff89e583258a7f8e7adeeeaeae8611bf.tar.gz", + .url = "https://deps.files.ghostty.org/libxev-9ce8e8e6ff89e583258a7f8e7adeeeaeae8611bf.tar.gz", .hash = "libxev-0.0.0-86vtcwIRFADbH4hk-EjROXxlrKIRPQdA41XiTSytYO-F", .lazy = true, }, .vaxis = .{ // rockorager/libvaxis - .url = "https://github.com/rockorager/libvaxis/archive/1dbbe575dff4586fe51e3217aa5c3fecdcbb6089.tar.gz", + .url = "https://deps.files.ghostty.org/vaxis-1dbbe575dff4586fe51e3217aa5c3fecdcbb6089.tar.gz", .hash = "vaxis-0.6.0-BWNV_CrbCQCscGpzsAlR402rYQ_tV3aAl081c2iRRkka", .lazy = true, }, .z2d = .{ // vancluever/z2d - .url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.11.0.tar.gz", + .url = "https://deps.files.ghostty.org/z2d-0.11.0-j5P_HtLzDwBGyQt49DrT0v4BuVqI_SRs6CXsuj7eBVhR.tar.gz", .hash = "z2d-0.11.0-j5P_HtLzDwBGyQt49DrT0v4BuVqI_SRs6CXsuj7eBVhR", .lazy = true, }, .zig_objc = .{ // mitchellh/zig-objc - .url = "https://github.com/mitchellh/zig-objc/archive/c8de82ff80281215ad92900866dab7103a8efa8b.tar.gz", + .url = "https://deps.files.ghostty.org/zig_objc-c8de82ff80281215ad92900866dab7103a8efa8b.tar.gz", .hash = "zig_objc-0.0.0-Ir_Sp9gsAQCPAJc0oF5xoWePHWP6Y6tCphDeyNUThJoi", .lazy = true, }, .zig_js = .{ // mitchellh/zig-js - .url = "https://github.com/mitchellh/zig-js/archive/3c23860e47fdcdc5af805efb7fd0bdac5fd3e9bc.tar.gz", + .url = "https://deps.files.ghostty.org/zig_js-3c23860e47fdcdc5af805efb7fd0bdac5fd3e9bc.tar.gz", .hash = "zig_js-0.0.0-rjCAV7-GAADvMTBL7lPMuvDk7xgS9PCMIZWiOUXLZSlj", .lazy = true, }, .uucode = .{ // jacobsandlund/uucode - .url = "https://github.com/jacobsandlund/uucode/archive/2826a37a4562284fdacd8fa029d49509cc9bffcd.tar.gz", + .url = "https://deps.files.ghostty.org/uucode-2826a37a4562284fdacd8fa029d49509cc9bffcd.tar.gz", .hash = "uucode-0.2.0-ZZjBPlK5VADj7fdoq7G8LIHzD5o6FSkcBXXrRWr4jnrA", }, .zig_wayland = .{ // codeberg ifreund/zig-wayland - .url = "https://codeberg.org/ifreund/zig-wayland/archive/v0.6.0.tar.gz", + .url = "https://deps.files.ghostty.org/wayland-0.6.0-lQa1kqz8AQADQmdNJsNhLoNHcnEGEUjrOaPV-dtEnEmX.tar.gz", .hash = "wayland-0.6.0-lQa1kqz8AQADQmdNJsNhLoNHcnEGEUjrOaPV-dtEnEmX", .lazy = true, }, .zf = .{ // natecraddock/zf - .url = "https://github.com/natecraddock/zf/archive/c35c421f84895193246db06c40683c1a30e616ef.tar.gz", + .url = "https://deps.files.ghostty.org/zf-c35c421f84895193246db06c40683c1a30e616ef.tar.gz", .hash = "zf-0.11.0-OIRy8X-RAAAwaRXHMYpj2uvBnuGTZWEE_3V7acqHQNtW", .lazy = true, }, .gobject = .{ // https://github.com/ghostty-org/zig-gobject based on zig_gobject // Temporary until we generate them at build time automatically. - .url = "https://github.com/ghostty-org/zig-gobject/releases/download/0.8.0-2026-04-23-26-1/ghostty-gobject-0.8.0-2026-04-23-26-1.tar.zst", + .url = "https://deps.files.ghostty.org/gobject-2026-04-23-26-1.tar.zst", .hash = "gobject-0.3.1-Skun7E1KnwBGMX5nslHYG1yWHaSevywxQO8oM7tTOgIp", .lazy = true, }, @@ -99,7 +99,7 @@ .lazy = true, }, .wayland_protocols = .{ - .url = "https://gitlab.freedesktop.org/wayland/wayland-protocols/-/archive/1.47/wayland-protocols-1.47.tar.gz", + .url = "https://deps.files.ghostty.org/N-V-__8AAFdWDwA0ktbNUi9pFBHCRN4weXIgIfCrVjfGxqgA.tar.gz", .hash = "N-V-__8AAFdWDwA0ktbNUi9pFBHCRN4weXIgIfCrVjfGxqgA", .lazy = true, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index 2ca7b1034..e23b6f873 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -36,7 +36,7 @@ }, "gobject-0.3.1-Skun7E1KnwBGMX5nslHYG1yWHaSevywxQO8oM7tTOgIp": { "name": "gobject", - "url": "https://github.com/ghostty-org/zig-gobject/releases/download/0.8.0-2026-04-23-26-1/ghostty-gobject-0.8.0-2026-04-23-26-1.tar.zst", + "url": "https://deps.files.ghostty.org/gobject-2026-04-23-26-1.tar.zst", "hash": "sha256-cZGf9a1+sDeORKWYIdzQ/5KLT7l/9pgPc2cIbqfBc4o=" }, "N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr": { @@ -56,7 +56,7 @@ }, "N-V-__8AAEbOfQBnvcFcCX2W5z7tDaN8vaNZGamEQtNOe0UI": { "name": "imgui", - "url": "https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz", + "url": "https://deps.files.ghostty.org/N-V-__8AAEbOfQBnvcFcCX2W5z7tDaN8vaNZGamEQtNOe0UI.tar.gz", "hash": "sha256-yBbCDox18+Fa6Gc1DnmSVQLRpqhZOLsac7iSfl8x+cs=" }, "N-V-__8AALZGBAAS5NLVH-c8eC-6VtCdcH-9nUvVfUSkWS__": { @@ -76,7 +76,7 @@ }, "libxev-0.0.0-86vtcwIRFADbH4hk-EjROXxlrKIRPQdA41XiTSytYO-F": { "name": "libxev", - "url": "https://github.com/mitchellh/libxev/archive/9ce8e8e6ff89e583258a7f8e7adeeeaeae8611bf.tar.gz", + "url": "https://deps.files.ghostty.org/libxev-9ce8e8e6ff89e583258a7f8e7adeeeaeae8611bf.tar.gz", "hash": "sha256-fOU1oxIxfoEgoLuWz7fVX6M+zmqpo7gqZObWiH/aDE0=" }, "N-V-__8AAG3RoQEyRC2Vw7Qoro5SYBf62IHn3HjqtNVY6aWK": { @@ -116,12 +116,12 @@ }, "translate_c-0.0.0-Q_BUWj8_BwCr4na219OimiKKj4IVSq0aKtMXCQmIk4AB": { "name": "translate_c", - "url": "https://codeberg.org/vancluever/translate-c/archive/a916baf225a3cfc93a982225462db612604ae290.tar.gz", + "url": "https://deps.files.ghostty.org/translate_c-a916baf225a3cfc93a982225462db612604ae290.tar.gz", "hash": "sha256-i7TSSiLU9YGtLtEjgVqy2jSnKLDTeKHX+EFuXEMXJ+U=" }, "uucode-0.2.0-ZZjBPlK5VADj7fdoq7G8LIHzD5o6FSkcBXXrRWr4jnrA": { "name": "uucode", - "url": "https://github.com/jacobsandlund/uucode/archive/2826a37a4562284fdacd8fa029d49509cc9bffcd.tar.gz", + "url": "https://deps.files.ghostty.org/uucode-2826a37a4562284fdacd8fa029d49509cc9bffcd.tar.gz", "hash": "sha256-R5RXW5tWIaDq5JOF2+oWd5YOYOyns6WH7f687WE+b20=" }, "vaxis-0.6.0-BWNV_MjFCQCs9UDHiRkrgw_ayeiPkzOe4xVbaAqXkUWW": { @@ -131,7 +131,7 @@ }, "vaxis-0.6.0-BWNV_CrbCQCscGpzsAlR402rYQ_tV3aAl081c2iRRkka": { "name": "vaxis", - "url": "https://github.com/rockorager/libvaxis/archive/1dbbe575dff4586fe51e3217aa5c3fecdcbb6089.tar.gz", + "url": "https://deps.files.ghostty.org/vaxis-1dbbe575dff4586fe51e3217aa5c3fecdcbb6089.tar.gz", "hash": "sha256-z3J3w+oYapfAZZR+MvWXwg+th1hePDA1TptnKFO4r+M=" }, "N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t": { @@ -139,16 +139,16 @@ "url": "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz", "hash": "sha256-6kGR1o5DdnflHzqs3ieCmBAUTpMdOXoyfcYDXiw5xQ0=" }, + "N-V-__8AAFdWDwA0ktbNUi9pFBHCRN4weXIgIfCrVjfGxqgA": { + "name": "wayland_protocols", + "url": "https://deps.files.ghostty.org/N-V-__8AAFdWDwA0ktbNUi9pFBHCRN4weXIgIfCrVjfGxqgA.tar.gz", + "hash": "sha256-3S3xSrX0EDgleq7cxLX7msDuAY8/D5SvkJcCjmDTMiM=" + }, "N-V-__8AAKw-DAAaV8bOAAGqA0-oD7o-HNIlPFYKRXSPT03S": { "name": "wayland_protocols", "url": "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz", "hash": "sha256-XO3K3egbdeYPI+XoO13SuOtO+5+Peb16NH0UiusFMPg=" }, - "N-V-__8AAFdWDwA0ktbNUi9pFBHCRN4weXIgIfCrVjfGxqgA": { - "name": "wayland_protocols", - "url": "https://gitlab.freedesktop.org/wayland/wayland-protocols/-/archive/1.47/wayland-protocols-1.47.tar.gz", - "hash": "sha256-3S3xSrX0EDgleq7cxLX7msDuAY8/D5SvkJcCjmDTMiM=" - }, "N-V-__8AAAzZywE3s51XfsLbP9eyEw57ae9swYB9aGB6fCMs": { "name": "wuffs", "url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz", @@ -156,27 +156,27 @@ }, "z2d-0.11.0-j5P_HtLzDwBGyQt49DrT0v4BuVqI_SRs6CXsuj7eBVhR": { "name": "z2d", - "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.11.0.tar.gz", + "url": "https://deps.files.ghostty.org/z2d-0.11.0-j5P_HtLzDwBGyQt49DrT0v4BuVqI_SRs6CXsuj7eBVhR.tar.gz", "hash": "sha256-/Yje2P+ro7IrPodQ26YKk7RsPSYj1Cqx6bdrmykXEKA=" }, "zf-0.11.0-OIRy8X-RAAAwaRXHMYpj2uvBnuGTZWEE_3V7acqHQNtW": { "name": "zf", - "url": "https://github.com/natecraddock/zf/archive/c35c421f84895193246db06c40683c1a30e616ef.tar.gz", + "url": "https://deps.files.ghostty.org/zf-c35c421f84895193246db06c40683c1a30e616ef.tar.gz", "hash": "sha256-LHurWwqK8jhP8AZd/CXrMJNeVsKftRCTN31/EgEgKaA=" }, "zig_js-0.0.0-rjCAV7-GAADvMTBL7lPMuvDk7xgS9PCMIZWiOUXLZSlj": { "name": "zig_js", - "url": "https://github.com/mitchellh/zig-js/archive/3c23860e47fdcdc5af805efb7fd0bdac5fd3e9bc.tar.gz", + "url": "https://deps.files.ghostty.org/zig_js-3c23860e47fdcdc5af805efb7fd0bdac5fd3e9bc.tar.gz", "hash": "sha256-Q1643wb/MCX0QkPa9QEY9xeQbqpC2IOrjER0ZyL3mH0=" }, "zig_objc-0.0.0-Ir_Sp9gsAQCPAJc0oF5xoWePHWP6Y6tCphDeyNUThJoi": { "name": "zig_objc", - "url": "https://github.com/mitchellh/zig-objc/archive/c8de82ff80281215ad92900866dab7103a8efa8b.tar.gz", + "url": "https://deps.files.ghostty.org/zig_objc-c8de82ff80281215ad92900866dab7103a8efa8b.tar.gz", "hash": "sha256-t8cTTJm0crcoGAyOqXO0wB2w7Yit80J2Orwr6prd/G0=" }, "wayland-0.6.0-lQa1kqz8AQADQmdNJsNhLoNHcnEGEUjrOaPV-dtEnEmX": { "name": "zig_wayland", - "url": "https://codeberg.org/ifreund/zig-wayland/archive/v0.6.0.tar.gz", + "url": "https://deps.files.ghostty.org/wayland-0.6.0-lQa1kqz8AQADQmdNJsNhLoNHcnEGEUjrOaPV-dtEnEmX.tar.gz", "hash": "sha256-3m/ITNhZUJ/5uD/Tqm+0uZSktGoYgWF5oldOqOCUkIE=" }, "zigimg-0.1.0-8_eo2oyaFwBZwJpmqPkCfVXWBrHcqbYwmrp1I6bTD3lI": { diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 2dec71387..cfda6af29 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -164,7 +164,7 @@ in name = "gobject-0.3.1-Skun7E1KnwBGMX5nslHYG1yWHaSevywxQO8oM7tTOgIp"; path = fetchZigArtifact { name = "gobject"; - url = "https://github.com/ghostty-org/zig-gobject/releases/download/0.8.0-2026-04-23-26-1/ghostty-gobject-0.8.0-2026-04-23-26-1.tar.zst"; + url = "https://deps.files.ghostty.org/gobject-2026-04-23-26-1.tar.zst"; hash = "sha256-cZGf9a1+sDeORKWYIdzQ/5KLT7l/9pgPc2cIbqfBc4o="; unpack = true; }; @@ -200,7 +200,7 @@ in name = "N-V-__8AAEbOfQBnvcFcCX2W5z7tDaN8vaNZGamEQtNOe0UI"; path = fetchZigArtifact { name = "imgui"; - url = "https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz"; + url = "https://deps.files.ghostty.org/N-V-__8AAEbOfQBnvcFcCX2W5z7tDaN8vaNZGamEQtNOe0UI.tar.gz"; hash = "sha256-yBbCDox18+Fa6Gc1DnmSVQLRpqhZOLsac7iSfl8x+cs="; unpack = false; }; @@ -236,7 +236,7 @@ in name = "libxev-0.0.0-86vtcwIRFADbH4hk-EjROXxlrKIRPQdA41XiTSytYO-F"; path = fetchZigArtifact { name = "libxev"; - url = "https://github.com/mitchellh/libxev/archive/9ce8e8e6ff89e583258a7f8e7adeeeaeae8611bf.tar.gz"; + url = "https://deps.files.ghostty.org/libxev-9ce8e8e6ff89e583258a7f8e7adeeeaeae8611bf.tar.gz"; hash = "sha256-fOU1oxIxfoEgoLuWz7fVX6M+zmqpo7gqZObWiH/aDE0="; unpack = true; }; @@ -308,7 +308,7 @@ in name = "translate_c-0.0.0-Q_BUWj8_BwCr4na219OimiKKj4IVSq0aKtMXCQmIk4AB"; path = fetchZigArtifact { name = "translate_c"; - url = "https://codeberg.org/vancluever/translate-c/archive/a916baf225a3cfc93a982225462db612604ae290.tar.gz"; + url = "https://deps.files.ghostty.org/translate_c-a916baf225a3cfc93a982225462db612604ae290.tar.gz"; hash = "sha256-i7TSSiLU9YGtLtEjgVqy2jSnKLDTeKHX+EFuXEMXJ+U="; unpack = true; }; @@ -317,7 +317,7 @@ in name = "uucode-0.2.0-ZZjBPlK5VADj7fdoq7G8LIHzD5o6FSkcBXXrRWr4jnrA"; path = fetchZigArtifact { name = "uucode"; - url = "https://github.com/jacobsandlund/uucode/archive/2826a37a4562284fdacd8fa029d49509cc9bffcd.tar.gz"; + url = "https://deps.files.ghostty.org/uucode-2826a37a4562284fdacd8fa029d49509cc9bffcd.tar.gz"; hash = "sha256-R5RXW5tWIaDq5JOF2+oWd5YOYOyns6WH7f687WE+b20="; unpack = true; }; @@ -335,7 +335,7 @@ in name = "vaxis-0.6.0-BWNV_CrbCQCscGpzsAlR402rYQ_tV3aAl081c2iRRkka"; path = fetchZigArtifact { name = "vaxis"; - url = "https://github.com/rockorager/libvaxis/archive/1dbbe575dff4586fe51e3217aa5c3fecdcbb6089.tar.gz"; + url = "https://deps.files.ghostty.org/vaxis-1dbbe575dff4586fe51e3217aa5c3fecdcbb6089.tar.gz"; hash = "sha256-z3J3w+oYapfAZZR+MvWXwg+th1hePDA1TptnKFO4r+M="; unpack = true; }; @@ -349,6 +349,15 @@ in unpack = false; }; } + { + name = "N-V-__8AAFdWDwA0ktbNUi9pFBHCRN4weXIgIfCrVjfGxqgA"; + path = fetchZigArtifact { + name = "wayland_protocols"; + url = "https://deps.files.ghostty.org/N-V-__8AAFdWDwA0ktbNUi9pFBHCRN4weXIgIfCrVjfGxqgA.tar.gz"; + hash = "sha256-3S3xSrX0EDgleq7cxLX7msDuAY8/D5SvkJcCjmDTMiM="; + unpack = false; + }; + } { name = "N-V-__8AAKw-DAAaV8bOAAGqA0-oD7o-HNIlPFYKRXSPT03S"; path = fetchZigArtifact { @@ -358,15 +367,6 @@ in unpack = false; }; } - { - name = "N-V-__8AAFdWDwA0ktbNUi9pFBHCRN4weXIgIfCrVjfGxqgA"; - path = fetchZigArtifact { - name = "wayland_protocols"; - url = "https://gitlab.freedesktop.org/wayland/wayland-protocols/-/archive/1.47/wayland-protocols-1.47.tar.gz"; - hash = "sha256-3S3xSrX0EDgleq7cxLX7msDuAY8/D5SvkJcCjmDTMiM="; - unpack = false; - }; - } { name = "N-V-__8AAAzZywE3s51XfsLbP9eyEw57ae9swYB9aGB6fCMs"; path = fetchZigArtifact { @@ -380,7 +380,7 @@ in name = "z2d-0.11.0-j5P_HtLzDwBGyQt49DrT0v4BuVqI_SRs6CXsuj7eBVhR"; path = fetchZigArtifact { name = "z2d"; - url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.11.0.tar.gz"; + url = "https://deps.files.ghostty.org/z2d-0.11.0-j5P_HtLzDwBGyQt49DrT0v4BuVqI_SRs6CXsuj7eBVhR.tar.gz"; hash = "sha256-/Yje2P+ro7IrPodQ26YKk7RsPSYj1Cqx6bdrmykXEKA="; unpack = true; }; @@ -389,7 +389,7 @@ in name = "zf-0.11.0-OIRy8X-RAAAwaRXHMYpj2uvBnuGTZWEE_3V7acqHQNtW"; path = fetchZigArtifact { name = "zf"; - url = "https://github.com/natecraddock/zf/archive/c35c421f84895193246db06c40683c1a30e616ef.tar.gz"; + url = "https://deps.files.ghostty.org/zf-c35c421f84895193246db06c40683c1a30e616ef.tar.gz"; hash = "sha256-LHurWwqK8jhP8AZd/CXrMJNeVsKftRCTN31/EgEgKaA="; unpack = true; }; @@ -398,7 +398,7 @@ in name = "zig_js-0.0.0-rjCAV7-GAADvMTBL7lPMuvDk7xgS9PCMIZWiOUXLZSlj"; path = fetchZigArtifact { name = "zig_js"; - url = "https://github.com/mitchellh/zig-js/archive/3c23860e47fdcdc5af805efb7fd0bdac5fd3e9bc.tar.gz"; + url = "https://deps.files.ghostty.org/zig_js-3c23860e47fdcdc5af805efb7fd0bdac5fd3e9bc.tar.gz"; hash = "sha256-Q1643wb/MCX0QkPa9QEY9xeQbqpC2IOrjER0ZyL3mH0="; unpack = true; }; @@ -407,7 +407,7 @@ in name = "zig_objc-0.0.0-Ir_Sp9gsAQCPAJc0oF5xoWePHWP6Y6tCphDeyNUThJoi"; path = fetchZigArtifact { name = "zig_objc"; - url = "https://github.com/mitchellh/zig-objc/archive/c8de82ff80281215ad92900866dab7103a8efa8b.tar.gz"; + url = "https://deps.files.ghostty.org/zig_objc-c8de82ff80281215ad92900866dab7103a8efa8b.tar.gz"; hash = "sha256-t8cTTJm0crcoGAyOqXO0wB2w7Yit80J2Orwr6prd/G0="; unpack = true; }; @@ -416,7 +416,7 @@ in name = "wayland-0.6.0-lQa1kqz8AQADQmdNJsNhLoNHcnEGEUjrOaPV-dtEnEmX"; path = fetchZigArtifact { name = "zig_wayland"; - url = "https://codeberg.org/ifreund/zig-wayland/archive/v0.6.0.tar.gz"; + url = "https://deps.files.ghostty.org/wayland-0.6.0-lQa1kqz8AQADQmdNJsNhLoNHcnEGEUjrOaPV-dtEnEmX.tar.gz"; hash = "sha256-3m/ITNhZUJ/5uD/Tqm+0uZSktGoYgWF5oldOqOCUkIE="; unpack = true; }; diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 1e9505feb..dc0010aab 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -1,9 +1,9 @@ git+https://github.com/rockorager/libvaxis.git#c1e1f23be38951c425cdf31af455ba23ef178940 git+https://github.com/zigimg/zigimg#d695acd97c02e57bb151e8f659d1280f5cd6ca70 -https://codeberg.org/ifreund/zig-wayland/archive/v0.6.0.tar.gz -https://codeberg.org/vancluever/translate-c/archive/a916baf225a3cfc93a982225462db612604ae290.tar.gz https://deps.files.ghostty.org/DearBindings_v0.17_ImGui_v1.92.5-docking.tar.gz https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz +https://deps.files.ghostty.org/N-V-__8AAEbOfQBnvcFcCX2W5z7tDaN8vaNZGamEQtNOe0UI.tar.gz +https://deps.files.ghostty.org/N-V-__8AAFdWDwA0ktbNUi9pFBHCRN4weXIgIfCrVjfGxqgA.tar.gz https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz @@ -11,28 +11,28 @@ https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab0 https://deps.files.ghostty.org/gettext-0.24.tar.gz https://deps.files.ghostty.org/ghostty-themes-release-20260713-155359-c3968b3.tgz https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz +https://deps.files.ghostty.org/gobject-2026-04-23-26-1.tar.zst https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz https://deps.files.ghostty.org/harfbuzz-11.0.0.tar.xz https://deps.files.ghostty.org/highway-66486a10623fa0d72fe91260f96c892e41aceb06.tar.gz https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz +https://deps.files.ghostty.org/libxev-9ce8e8e6ff89e583258a7f8e7adeeeaeae8611bf.tar.gz https://deps.files.ghostty.org/libxml2-2.11.5.tar.gz https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz https://deps.files.ghostty.org/pixels-12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806.tar.gz https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566.tar.gz https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz +https://deps.files.ghostty.org/translate_c-a916baf225a3cfc93a982225462db612604ae290.tar.gz +https://deps.files.ghostty.org/uucode-2826a37a4562284fdacd8fa029d49509cc9bffcd.tar.gz +https://deps.files.ghostty.org/vaxis-1dbbe575dff4586fe51e3217aa5c3fecdcbb6089.tar.gz +https://deps.files.ghostty.org/wayland-0.6.0-lQa1kqz8AQADQmdNJsNhLoNHcnEGEUjrOaPV-dtEnEmX.tar.gz https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz +https://deps.files.ghostty.org/z2d-0.11.0-j5P_HtLzDwBGyQt49DrT0v4BuVqI_SRs6CXsuj7eBVhR.tar.gz +https://deps.files.ghostty.org/zf-c35c421f84895193246db06c40683c1a30e616ef.tar.gz +https://deps.files.ghostty.org/zig_js-3c23860e47fdcdc5af805efb7fd0bdac5fd3e9bc.tar.gz +https://deps.files.ghostty.org/zig_objc-c8de82ff80281215ad92900866dab7103a8efa8b.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz -https://github.com/ghostty-org/zig-gobject/releases/download/0.8.0-2026-04-23-26-1/ghostty-gobject-0.8.0-2026-04-23-26-1.tar.zst -https://github.com/jacobsandlund/uucode/archive/2826a37a4562284fdacd8fa029d49509cc9bffcd.tar.gz -https://github.com/mitchellh/libxev/archive/9ce8e8e6ff89e583258a7f8e7adeeeaeae8611bf.tar.gz -https://github.com/mitchellh/zig-js/archive/3c23860e47fdcdc5af805efb7fd0bdac5fd3e9bc.tar.gz -https://github.com/mitchellh/zig-objc/archive/c8de82ff80281215ad92900866dab7103a8efa8b.tar.gz -https://github.com/natecraddock/zf/archive/c35c421f84895193246db06c40683c1a30e616ef.tar.gz -https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz -https://github.com/rockorager/libvaxis/archive/1dbbe575dff4586fe51e3217aa5c3fecdcbb6089.tar.gz https://github.com/vancluever/arocc/archive/ecbc5c799574e0da2758a961b12efa586007f03c.tar.gz -https://github.com/vancluever/z2d/archive/refs/tags/v0.11.0.tar.gz -https://gitlab.freedesktop.org/wayland/wayland-protocols/-/archive/1.47/wayland-protocols-1.47.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 01f949fbc..598e64e1a 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -43,7 +43,7 @@ }, { "type": "archive", - "url": "https://github.com/ghostty-org/zig-gobject/releases/download/0.8.0-2026-04-23-26-1/ghostty-gobject-0.8.0-2026-04-23-26-1.tar.zst", + "url": "https://deps.files.ghostty.org/gobject-2026-04-23-26-1.tar.zst", "dest": "vendor/p/gobject-0.3.1-Skun7E1KnwBGMX5nslHYG1yWHaSevywxQO8oM7tTOgIp", "sha256": "642e324b3644c00464a2161e4af497f73cbbe8ce02a4c0edb92aacb365748047" }, @@ -67,7 +67,7 @@ }, { "type": "archive", - "url": "https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz", + "url": "https://deps.files.ghostty.org/N-V-__8AAEbOfQBnvcFcCX2W5z7tDaN8vaNZGamEQtNOe0UI.tar.gz", "dest": "vendor/p/N-V-__8AAEbOfQBnvcFcCX2W5z7tDaN8vaNZGamEQtNOe0UI", "sha256": "c816c20e8c75f3e15ae867350e79925502d1a6a85938bb1a73b8927e5f31f9cb" }, @@ -91,7 +91,7 @@ }, { "type": "archive", - "url": "https://github.com/mitchellh/libxev/archive/9ce8e8e6ff89e583258a7f8e7adeeeaeae8611bf.tar.gz", + "url": "https://deps.files.ghostty.org/libxev-9ce8e8e6ff89e583258a7f8e7adeeeaeae8611bf.tar.gz", "dest": "vendor/p/libxev-0.0.0-86vtcwIRFADbH4hk-EjROXxlrKIRPQdA41XiTSytYO-F", "sha256": "e2b9b224cd78101156abb8f46479a51f6cce66c4815c632856e20a1440dd02b7" }, @@ -139,13 +139,13 @@ }, { "type": "archive", - "url": "https://codeberg.org/vancluever/translate-c/archive/a916baf225a3cfc93a982225462db612604ae290.tar.gz", + "url": "https://deps.files.ghostty.org/translate_c-a916baf225a3cfc93a982225462db612604ae290.tar.gz", "dest": "vendor/p/translate_c-0.0.0-Q_BUWj8_BwCr4na219OimiKKj4IVSq0aKtMXCQmIk4AB", "sha256": "4fd2a2b21dbc49c22bea6854a59a7568dc77183760531ace3cf8da1267696afd" }, { "type": "archive", - "url": "https://github.com/jacobsandlund/uucode/archive/2826a37a4562284fdacd8fa029d49509cc9bffcd.tar.gz", + "url": "https://deps.files.ghostty.org/uucode-2826a37a4562284fdacd8fa029d49509cc9bffcd.tar.gz", "dest": "vendor/p/uucode-0.2.0-ZZjBPlK5VADj7fdoq7G8LIHzD5o6FSkcBXXrRWr4jnrA", "sha256": "7e76fc7fab1e7ac728c52b35bbb3e5b8c639841abfc7fe1a4bcb13050594bc9e" }, @@ -157,7 +157,7 @@ }, { "type": "archive", - "url": "https://github.com/rockorager/libvaxis/archive/1dbbe575dff4586fe51e3217aa5c3fecdcbb6089.tar.gz", + "url": "https://deps.files.ghostty.org/vaxis-1dbbe575dff4586fe51e3217aa5c3fecdcbb6089.tar.gz", "dest": "vendor/p/vaxis-0.6.0-BWNV_CrbCQCscGpzsAlR402rYQ_tV3aAl081c2iRRkka", "sha256": "3ea050e905887c46bca1a2c18e41d6edf574c1aa6ebfbc5cf3c52846275e4fa8" }, @@ -169,15 +169,15 @@ }, { "type": "archive", - "url": "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz", - "dest": "vendor/p/N-V-__8AAKw-DAAaV8bOAAGqA0-oD7o-HNIlPFYKRXSPT03S", - "sha256": "5cedcadde81b75e60f23e5e83b5dd2b8eb4efb9f8f79bd7a347d148aeb0530f8" + "url": "https://deps.files.ghostty.org/N-V-__8AAFdWDwA0ktbNUi9pFBHCRN4weXIgIfCrVjfGxqgA.tar.gz", + "dest": "vendor/p/N-V-__8AAFdWDwA0ktbNUi9pFBHCRN4weXIgIfCrVjfGxqgA", + "sha256": "dd2df14ab5f41038257aaedcc4b5fb9ac0ee018f3f0f94af9097028e60d33223" }, { "type": "archive", - "url": "https://gitlab.freedesktop.org/wayland/wayland-protocols/-/archive/1.47/wayland-protocols-1.47.tar.gz", - "dest": "vendor/p/N-V-__8AAFdWDwA0ktbNUi9pFBHCRN4weXIgIfCrVjfGxqgA", - "sha256": "dd2df14ab5f41038257aaedcc4b5fb9ac0ee018f3f0f94af9097028e60d33223" + "url": "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz", + "dest": "vendor/p/N-V-__8AAKw-DAAaV8bOAAGqA0-oD7o-HNIlPFYKRXSPT03S", + "sha256": "5cedcadde81b75e60f23e5e83b5dd2b8eb4efb9f8f79bd7a347d148aeb0530f8" }, { "type": "archive", @@ -187,31 +187,31 @@ }, { "type": "archive", - "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.11.0.tar.gz", + "url": "https://deps.files.ghostty.org/z2d-0.11.0-j5P_HtLzDwBGyQt49DrT0v4BuVqI_SRs6CXsuj7eBVhR.tar.gz", "dest": "vendor/p/z2d-0.11.0-j5P_HtLzDwBGyQt49DrT0v4BuVqI_SRs6CXsuj7eBVhR", "sha256": "d48b117307f767ca379bf31bd171880e66dcafb89eca4a8f923222619992fcb4" }, { "type": "archive", - "url": "https://github.com/natecraddock/zf/archive/c35c421f84895193246db06c40683c1a30e616ef.tar.gz", + "url": "https://deps.files.ghostty.org/zf-c35c421f84895193246db06c40683c1a30e616ef.tar.gz", "dest": "vendor/p/zf-0.11.0-OIRy8X-RAAAwaRXHMYpj2uvBnuGTZWEE_3V7acqHQNtW", "sha256": "ca4bc109fd9d97cedae2f5bdb879cc78c346d43c14a34e3a2abd2ee9208c8547" }, { "type": "archive", - "url": "https://github.com/mitchellh/zig-js/archive/3c23860e47fdcdc5af805efb7fd0bdac5fd3e9bc.tar.gz", + "url": "https://deps.files.ghostty.org/zig_js-3c23860e47fdcdc5af805efb7fd0bdac5fd3e9bc.tar.gz", "dest": "vendor/p/zig_js-0.0.0-rjCAV7-GAADvMTBL7lPMuvDk7xgS9PCMIZWiOUXLZSlj", "sha256": "a2ff488a244301f4e59469a5232aad6869b01acc50f7d7ed2647c029a57d4b3b" }, { "type": "archive", - "url": "https://github.com/mitchellh/zig-objc/archive/c8de82ff80281215ad92900866dab7103a8efa8b.tar.gz", + "url": "https://deps.files.ghostty.org/zig_objc-c8de82ff80281215ad92900866dab7103a8efa8b.tar.gz", "dest": "vendor/p/zig_objc-0.0.0-Ir_Sp9gsAQCPAJc0oF5xoWePHWP6Y6tCphDeyNUThJoi", "sha256": "831eaec554bb23ef20229f1e705d8f53e8a29c829840bb8d0f5edb05c76d6df0" }, { "type": "archive", - "url": "https://codeberg.org/ifreund/zig-wayland/archive/v0.6.0.tar.gz", + "url": "https://deps.files.ghostty.org/wayland-0.6.0-lQa1kqz8AQADQmdNJsNhLoNHcnEGEUjrOaPV-dtEnEmX.tar.gz", "dest": "vendor/p/wayland-0.6.0-lQa1kqz8AQADQmdNJsNhLoNHcnEGEUjrOaPV-dtEnEmX", "sha256": "759a632e36a948e0e412d2d74a43a69d2f34e65d40289a16af7aa3725852ef25" }, diff --git a/nix/build-support/update-mirror.nu b/nix/build-support/update-mirror.nu index 8571ddea6..b234172bf 100755 --- a/nix/build-support/update-mirror.nu +++ b/nix/build-support/update-mirror.nu @@ -2,23 +2,34 @@ # This script downloads external dependencies from build.zig.zon.json that # are not already mirrored at deps.files.ghostty.org, saves them to a local -# directory, and updates build.zig.zon to point to the new mirror URLs. +# directory, and updates the build.zig.zon files (the root one as well as +# every pkg/*/build.zig.zon) to point to the new mirror URLs. # # The downloaded files are unmodified so their checksums and content hashes # will match the originals. # # After running this script, the files in the output directory can be uploaded -# to blob storage, and build.zig.zon will already be updated with the new URLs. +# to blob storage, and the build.zig.zon files will already be updated with +# the new URLs. def main [ --output: string = "tmp-mirror", # Output directory for the mirrored files --prefix: string = "https://deps.files.ghostty.org/", # Final URL prefix to ignore --dry-run, # Print what would be downloaded without downloading ] { let script_dir = ($env.CURRENT_FILE | path dirname) - let input_file = ($script_dir | path join ".." ".." "build.zig.zon.json") - let zon_file = ($script_dir | path join ".." ".." "build.zig.zon") + let root_dir = ($script_dir | path join ".." ".." | path expand) + let input_file = ($root_dir | path join "build.zig.zon.json") let output_dir = $output + # All build.zig.zon files that may reference external URLs: the root + # one plus every package under pkg/. build.zig.zon.json is generated + # from the full dependency tree so it covers all of these. + let zon_files = ( + [($root_dir | path join "build.zig.zon")] + | append (glob ($root_dir | path join "pkg" "*" "build.zig.zon")) + | sort + ) + # Ensure the output directory exists mkdir $output_dir @@ -75,22 +86,34 @@ def main [ if $dry_run { print "Dry run complete - no files were downloaded\n" - print $"Would update ($url_replacements | length) URLs in build.zig.zon" } else { print "All dependencies downloaded successfully\n" - print $"Updating ($zon_file)..." - - # Backup the old file - let backup_file = $"($zon_file).bak" - cp $zon_file $backup_file - print $"Backed up to ($backup_file)" - - mut zon_content = (open $zon_file) - for replacement in $url_replacements { - $zon_content = ($zon_content | str replace $replacement.old $replacement.new) - } - $zon_content | save -f $zon_file + } - print $"Updated ($url_replacements | length) URLs in build.zig.zon" + # Apply the URL replacements to every build.zig.zon that references them. + for zon_file in $zon_files { + mut zon_content = (open --raw $zon_file) + mut count = 0 + for replacement in $url_replacements { + if ($zon_content | str contains $replacement.old) { + $zon_content = ($zon_content | str replace --all $replacement.old $replacement.new) + $count = $count + 1 + } + } + + if $count == 0 { + continue + } + + let relative_path = ($zon_file | path relative-to $root_dir) + if $dry_run { + print $"Would update ($count) URLs in ($relative_path)" + } else { + # Backup the old file + let backup_file = $"($zon_file).bak" + cp $zon_file $backup_file + $zon_content | save -f $zon_file + print $"Updated ($count) URLs in ($relative_path), backup at ($relative_path).bak" + } } } diff --git a/pkg/dcimgui/build.zig.zon b/pkg/dcimgui/build.zig.zon index 95d0120e1..c488e6122 100644 --- a/pkg/dcimgui/build.zig.zon +++ b/pkg/dcimgui/build.zig.zon @@ -15,7 +15,7 @@ .imgui = .{ // https://github.com/ocornut/imgui - .url = "https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz", + .url = "https://deps.files.ghostty.org/N-V-__8AAEbOfQBnvcFcCX2W5z7tDaN8vaNZGamEQtNOe0UI.tar.gz", .hash = "N-V-__8AAEbOfQBnvcFcCX2W5z7tDaN8vaNZGamEQtNOe0UI", .lazy = true, }, diff --git a/pkg/gtk4-layer-shell/build.zig.zon b/pkg/gtk4-layer-shell/build.zig.zon index 042ff96fe..32c7adfc1 100644 --- a/pkg/gtk4-layer-shell/build.zig.zon +++ b/pkg/gtk4-layer-shell/build.zig.zon @@ -15,7 +15,7 @@ .lazy = true, }, .translate_c = .{ - .url = "https://codeberg.org/vancluever/translate-c/archive/a916baf225a3cfc93a982225462db612604ae290.tar.gz", + .url = "https://deps.files.ghostty.org/translate_c-a916baf225a3cfc93a982225462db612604ae290.tar.gz", .hash = "translate_c-0.0.0-Q_BUWj8_BwCr4na219OimiKKj4IVSq0aKtMXCQmIk4AB", .lazy = true, }, diff --git a/pkg/harfbuzz/build.zig.zon b/pkg/harfbuzz/build.zig.zon index de5ebcf94..c4ee2bdfe 100644 --- a/pkg/harfbuzz/build.zig.zon +++ b/pkg/harfbuzz/build.zig.zon @@ -5,7 +5,7 @@ .paths = .{""}, .dependencies = .{ .translate_c = .{ - .url = "https://codeberg.org/vancluever/translate-c/archive/a916baf225a3cfc93a982225462db612604ae290.tar.gz", + .url = "https://deps.files.ghostty.org/translate_c-a916baf225a3cfc93a982225462db612604ae290.tar.gz", .hash = "translate_c-0.0.0-Q_BUWj8_BwCr4na219OimiKKj4IVSq0aKtMXCQmIk4AB", .lazy = true, }, diff --git a/pkg/macos/build.zig.zon b/pkg/macos/build.zig.zon index 3c2f4448e..58b008c33 100644 --- a/pkg/macos/build.zig.zon +++ b/pkg/macos/build.zig.zon @@ -6,7 +6,7 @@ .dependencies = .{ .apple_sdk = .{ .path = "../apple-sdk" }, .translate_c = .{ - .url = "https://codeberg.org/vancluever/translate-c/archive/a916baf225a3cfc93a982225462db612604ae290.tar.gz", + .url = "https://deps.files.ghostty.org/translate_c-a916baf225a3cfc93a982225462db612604ae290.tar.gz", .hash = "translate_c-0.0.0-Q_BUWj8_BwCr4na219OimiKKj4IVSq0aKtMXCQmIk4AB", .lazy = true, }, diff --git a/pkg/wuffs/build.zig.zon b/pkg/wuffs/build.zig.zon index 338f4f27f..1cf438e8e 100644 --- a/pkg/wuffs/build.zig.zon +++ b/pkg/wuffs/build.zig.zon @@ -5,7 +5,7 @@ .dependencies = .{ .translate_c = .{ .lazy = true, - .url = "https://codeberg.org/vancluever/translate-c/archive/a916baf225a3cfc93a982225462db612604ae290.tar.gz", + .url = "https://deps.files.ghostty.org/translate_c-a916baf225a3cfc93a982225462db612604ae290.tar.gz", .hash = "translate_c-0.0.0-Q_BUWj8_BwCr4na219OimiKKj4IVSq0aKtMXCQmIk4AB", }, diff --git a/src/benchmark/TerminalStream.zig b/src/benchmark/TerminalStream.zig index 1e5c8aeec..a521c5b7a 100644 --- a/src/benchmark/TerminalStream.zig +++ b/src/benchmark/TerminalStream.zig @@ -113,8 +113,10 @@ fn step(ptr: *anyopaque) Benchmark.Error!void { // aren't currently IO bound. const f = self.data_f orelse return; - var read_buf: [64 * 1024]u8 align(std.atomic.cache_line) = undefined; - var f_reader = f.reader(global.io(), &read_buf); + // Unbuffered: readSliceShort below reads directly into `buf`, + // avoiding a per-chunk memcpy through an intermediate reader + // buffer that would pollute the measurement. + var f_reader = f.reader(global.io(), &.{}); const r = &f_reader.interface; // This buffer size matches the read buffer size used by the diff --git a/src/benchmark/cli.zig b/src/benchmark/cli.zig index 4c31ea8c4..6cab79025 100644 --- a/src/benchmark/cli.zig +++ b/src/benchmark/cli.zig @@ -46,7 +46,7 @@ pub const Action = enum { /// An entrypoint for the benchmark CLI. pub fn main(init: std.process.Init) !void { - try global.init(.{ .main = init }); + try global.init(.{ .tool = init }); const alloc = std.heap.c_allocator; const action_ = try cli.action.detectArgs(Action, alloc, init.minimal.args); const action = action_ orelse return error.NoAction; diff --git a/src/build/Config.zig b/src/build/Config.zig index 20a355cb5..43db20971 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -669,6 +669,17 @@ pub fn fromOptions() Config { }; } +/// Whether release artifacts should omit frame pointers. +/// +/// Stripped release builds omit them in general, but we always keep frame +/// pointers on Apple platforms: the Apple arm64 ABI expects x29 to +/// be a valid frame pointer and Apple's profiling and crash reporting +/// tools rely on it for backtraces. +pub fn omitFramePointer(self: *const Config) bool { + if (self.target.result.os.tag.isDarwin()) return false; + return self.strip; +} + /// Returns the minimum OS version for the given OS tag. This shouldn't /// be used generally, it should only be used for Darwin-based OS currently. pub fn osVersionMin(tag: std.Target.Os.Tag) ?std.Target.Query.OsVersion { diff --git a/src/build/GhosttyExe.zig b/src/build/GhosttyExe.zig index 680ac856e..22f39b7b3 100644 --- a/src/build/GhosttyExe.zig +++ b/src/build/GhosttyExe.zig @@ -18,7 +18,7 @@ pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !Ghostty .target = cfg.target, .optimize = cfg.optimize, .strip = cfg.strip, - .omit_frame_pointer = cfg.strip, + .omit_frame_pointer = cfg.omitFramePointer(), .unwind_tables = if (cfg.strip) .none else .sync, }), // Crashes on x86_64 self-hosted on 0.15.1 diff --git a/src/build/GhosttyLib.zig b/src/build/GhosttyLib.zig index 6fe28370c..529bb2821 100644 --- a/src/build/GhosttyLib.zig +++ b/src/build/GhosttyLib.zig @@ -1,9 +1,11 @@ const GhosttyLib = @This(); const std = @import("std"); +const builtin = @import("builtin"); const RunStep = std.Build.Step.Run; const CombineArchivesStep = @import("CombineArchivesStep.zig"); const Config = @import("Config.zig"); +const LibsystemOverrideStep = @import("LibsystemOverrideStep.zig"); const SharedDeps = @import("SharedDeps.zig"); const LipoStep = @import("LipoStep.zig"); @@ -27,7 +29,7 @@ pub fn initStatic( .target = deps.config.target, .optimize = deps.config.optimize, .strip = deps.config.strip, - .omit_frame_pointer = deps.config.strip, + .omit_frame_pointer = deps.config.omitFramePointer(), .unwind_tables = if (deps.config.strip) .none else .sync, .link_libc = true, }), @@ -57,9 +59,19 @@ pub fn initStatic( const combined = CombineArchivesStep.create(b, deps.config.target, "ghostty-internal", lib_list.items); combined.step.dependOn(&lib.step); + // On Darwin, prefer libSystem's libc/libm over the bundled + // compiler-rt for consumers of this archive. See + // libsystem_override.sh for details. This is a no-op elsewhere. + const override = LibsystemOverrideStep.create( + b, + deps.config.target, + combined.output, + "libghostty-internal.a", + ); + return .{ - .step = combined.step, - .output = combined.output, + .step = override.step orelse combined.step, + .output = override.output, // Static libraries cannot have dSYMs because they aren't linked. .dsym = null, @@ -88,7 +100,7 @@ pub fn initShared( .target = deps.config.target, .optimize = deps.config.optimize, .strip = deps.config.strip, - .omit_frame_pointer = deps.config.strip, + .omit_frame_pointer = deps.config.omitFramePointer(), .unwind_tables = if (deps.config.strip) .none else .sync, }), diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index 8ff25f503..32ed2aa72 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -5,6 +5,7 @@ const builtin = @import("builtin"); const assert = std.debug.assert; const RunStep = std.Build.Step.Run; const CombineArchivesStep = @import("CombineArchivesStep.zig"); +const LibsystemOverrideStep = @import("LibsystemOverrideStep.zig"); const Config = @import("Config.zig"); const GhosttyZig = @import("GhosttyZig.zig"); const LipoStep = @import("LipoStep.zig"); @@ -295,22 +296,44 @@ fn initLib( const combined = CombineArchivesStep.create(b, target, "ghostty-vt", sources.items); combined.step.dependOn(&lib.step); + // On Darwin, prefer libSystem's libc/libm over the bundled + // compiler-rt for consumers of this archive. See GhosttyLib + // and libsystem_override.sh for details. + const override = LibsystemOverrideStep.create( + b, + target, + combined.output, + "libghostty-vt-fat.a", + ); + return .{ - .step = combined.step, + .step = override.step orelse combined.step, .artifact = &b.addInstallArtifact(lib, .{}).step, .kind = kind, - .output = combined.output, + .output = override.output, .dsym = dsymutil, .pkg_config = if (pcs) |v| v.shared else null, .pkg_config_static = if (pcs) |v| v.static else null, }; } + // Same libSystem preference for the plain (no vendored SIMD) + // static archive; a no-op off Darwin. + const override: LibsystemOverrideStep.Result = if (kind == .static) LibsystemOverrideStep.create( + b, + target, + lib.getEmittedBin(), + "libghostty-vt-static.a", + ) else .{ + .step = null, + .output = lib.getEmittedBin(), + }; + return .{ - .step = &lib.step, + .step = override.step orelse &lib.step, .artifact = &b.addInstallArtifact(lib, .{}).step, .kind = kind, - .output = lib.getEmittedBin(), + .output = override.output, .dsym = dsymutil, .pkg_config = if (pcs) |v| v.shared else null, .pkg_config_static = if (pcs) |v| v.static else null, diff --git a/src/build/LibsystemOverrideStep.zig b/src/build/LibsystemOverrideStep.zig new file mode 100644 index 000000000..3e1cafa5e --- /dev/null +++ b/src/build/LibsystemOverrideStep.zig @@ -0,0 +1,51 @@ +//! Wraps a Darwin static archive with the libsystem_override.sh +//! post-processing step so that consumers linking the archive bind +//! well-known libc/libm symbols (memcpy, memmove, memset, cos, sin, +//! ...) to Apple's libSystem instead of the bundled Zig compiler-rt. +//! See src/build/libsystem_override.sh for the full rationale. +//! +//! This applies to every static archive we produce, public +//! (e.g. libghostty-vt) and app-internal alike: the compiler-rt +//! bundle must stay because the few symbols libSystem does not +//! provide (f128 conversions, *q math, sincos*, ___zig_probe_stack) +//! have live references, e.g. from the bundled ubsan runtime and +//! x86_64 stack probing in Debug builds. +//! +//! Note shared libraries can't use either approach: Zig 0.16 links +//! compiler-rt as an eagerly-included object, so its definitions are +//! already bound into the dylib at link time and cannot be repaired +//! post-hoc. That needs an upstream Zig fix. +const std = @import("std"); +const builtin = @import("builtin"); + +pub const Result = struct { + /// The step performing the override, or null if the override + /// doesn't apply to this target/host (in which case `output` is + /// the unmodified input). + step: ?*std.Build.Step, + output: std.Build.LazyPath, +}; + +/// Post-process the given static archive on Darwin. Requires a Darwin +/// host for the Apple toolchain (nmedit); in all other configurations +/// this is a no-op and the input is returned unmodified, which is +/// functional (the bundled compiler-rt symbols are used), just slower. +pub fn create( + b: *std.Build, + target: std.Build.ResolvedTarget, + input: std.Build.LazyPath, + out_name: []const u8, +) Result { + if (!target.result.os.tag.isDarwin() or + comptime !builtin.os.tag.isDarwin()) + { + return .{ .step = null, .output = input }; + } + + const run = b.addSystemCommand(&.{"/bin/sh"}); + run.setName("libsystem override"); + run.addFileArg(b.path("src/build/libsystem_override.sh")); + run.addFileArg(input); + const output = run.addOutputFileArg(out_name); + return .{ .step = &run.step, .output = output }; +} diff --git a/src/build/libsystem_override.sh b/src/build/libsystem_override.sh new file mode 100755 index 000000000..14ed191b8 --- /dev/null +++ b/src/build/libsystem_override.sh @@ -0,0 +1,123 @@ +#!/bin/sh +# +# Darwin-only: rewrite a static archive so that consumers linking it bind +# libc/libm symbols to Apple's libSystem instead of the bundled Zig +# compiler-rt. This uses the Apple toolchain (xcrun, nmedit) and libSystem +# semantics, so it must run on a Darwin host for a Darwin target. +# +# Background: our static library bundles Zig's compiler-rt, which defines +# strong global implementations of libc/libm functions (memcpy, memmove, +# memset, cos, sin, ...). When a consumer (e.g. the macOS app via the +# XCFramework) links this archive, ld64 resolves those symbols from the +# archive instead of libSystem, silently replacing Apple's highly +# optimized implementations (_platform_memmove and friends, vectorized +# libm) with compiler-rt's generic ones. We measured Zig 0.16 (LLVM 21) +# compiler-rt memmove costing several percent of PTY throughput on +# scroll-region workloads versus libSystem's. +# +# The mechanism is to localize (make non-external) the libSystem-provided +# symbols defined by the compiler_rt.o archive member. This keeps +# compiler_rt.o functional for the intrinsics that libSystem does NOT +# provide (e.g. the f128 conversions ___extenddftf2/___extendxftf2, *q +# math, sincos*) while letting every other archive member bind the +# well-known libc/libm symbols to libSystem at final link. +# +# usage: libsystem_override.sh +set -eu + +in="$1" +out="$2" + +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +cp -f "$in" "$out" +chmod u+w "$out" + +# The symbols to prefer from libSystem. Everything listed here is a +# stable macOS export (verified against libSystem's link surface). +# Notably NOT listed (not exported by libSystem): the *q (f128) +# variants, sincos/sincosf/sincosl, and all ___-prefixed compiler +# intrinsics except the fortify _chk wrappers below. +cat >"$tmp/localize.txt" <<'EOF' +_bcmp +_memcmp +_memcpy +_memmove +_memset +_strlen +___memcpy_chk +___memmove_chk +___memset_chk +___strcat_chk +___strcpy_chk +_ceil +_ceilf +_ceill +_cos +_cosf +_cosl +_exp +_exp2 +_exp2f +_exp2l +_expf +_expl +_fabs +_fabsf +_fabsl +_floor +_floorf +_floorl +_fma +_fmaf +_fmal +_fmax +_fmaxf +_fmaxl +_fmin +_fminf +_fminl +_fmod +_fmodf +_fmodl +_log +_log10 +_log10f +_log10l +_log2 +_log2f +_log2l +_logf +_logl +_round +_roundf +_roundl +_sin +_sinf +_sinl +_sqrt +_sqrtf +_sqrtl +_tan +_tanf +_tanl +_trunc +_truncf +_truncl +EOF + +cd "$tmp" + +xcrun ar x "$out" compiler_rt.o +chmod 644 compiler_rt.o + +# nmedit takes a keep-list (-s): all current global definitions except +# the ones we want to localize. +xcrun nm -g compiler_rt.o | awk '$2 ~ /^[A-TV-Z]$/ {print $3}' | sort -u >all.txt +sort -u localize.txt >loc.txt +comm -23 all.txt loc.txt >keep.txt +xcrun nmedit -s keep.txt compiler_rt.o + +xcrun ar r "$out" compiler_rt.o +xcrun ranlib "$out" 2>/dev/null || true diff --git a/src/config/path.zig b/src/config/path.zig index 5bf0bff39..4d9c8586a 100644 --- a/src/config/path.zig +++ b/src/config/path.zig @@ -204,7 +204,7 @@ pub const Path = union(enum) { defer dir.close(global.io()); const abs = abs: { - const dir_len = dir.realPathFile(global.io(), ".", &buf) catch |err| { + const abs_len = dir.realPathFile(global.io(), path, &buf) catch |err| { if (err == error.FileNotFound) { // The file doesn't exist. Try to resolve the relative path // another way. @@ -229,7 +229,7 @@ pub const Path = union(enum) { return; }; - break :abs buf[0..dir_len]; + break :abs buf[0..abs_len]; }; log.debug( diff --git a/src/global.zig b/src/global.zig index e61f21264..dd9264f3a 100644 --- a/src/global.zig +++ b/src/global.zig @@ -35,6 +35,14 @@ var state: ?GlobalState = undefined; pub const InitOpts = union(enum) { main: std.process.Init, + + /// Same as `main` but for auxiliary tool binaries (e.g. ghostty-bench + /// and ghostty-gen) that have their own CLI action namespace. This + /// skips detection of ghostty app CLI actions, since tool actions + /// (e.g. `+terminal-stream`) are not valid app actions and would + /// otherwise cause init to fail with InvalidAction. + tool: std.process.Init, + c: struct { argc: usize, argv: [*][*:0]u8, @@ -52,11 +60,11 @@ pub fn init(opts: InitOpts) !void { .gpa = null, .alloc = undefined, .environ = switch (opts) { - .main => |m| m.minimal.environ, + .main, .tool => |m| m.minimal.environ, .c => |c| c.environ, }, .args = switch (opts) { - .main => |m| m.minimal.args, + .main, .tool => |m| m.minimal.args, // TODO: Using the C API from Windows is unsupported at this time. // // When do we plan on supporting Windows, it's recommended to @@ -116,11 +124,16 @@ pub fn init(opts: InitOpts) !void { errdefer freeTmpDir(self.alloc, self.tmp_dir_path.?); // We first try to parse any action that we may be executing. - self.action = try cli.action.detectArgs( - cli.ghostty.Action, - self.alloc, - self.args, - ); + // Tool binaries (ghostty-bench, ghostty-gen) have their own action + // namespace and detect their own actions, so we skip detection here. + self.action = switch (opts) { + .main, .c => try cli.action.detectArgs( + cli.ghostty.Action, + self.alloc, + self.args, + ), + .tool => null, + }; // If we have an action executing, we disable logging by default // since we write to stderr we don't want logs messing up our diff --git a/src/lib_vt.zig b/src/lib_vt.zig index c6ac93743..2475ff786 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -145,6 +145,12 @@ comptime { // If we're building the C library (vs. the Zig module) then // we want to reference the C API so that it gets exported. if (@import("root") == lib) { + // Force-reference our memset override so its export is + // emitted. This must stay inside the root guard so that + // downstream Zig module consumers don't get the override + // injected into their binaries. See quirks_memset.zig. + _ = @import("quirks_memset.zig"); + const c = terminal.c_api; @export(&c.key_event_new, .{ .name = "ghostty_key_event_new" }); @export(&c.key_event_free, .{ .name = "ghostty_key_event_free" }); diff --git a/src/main.zig b/src/main.zig index a88de728e..fe41640ab 100644 --- a/src/main.zig +++ b/src/main.zig @@ -21,6 +21,12 @@ pub const std_options: std.Options = if (@hasDecl(entrypoint, "std_options")) else .{}; +comptime { + // Force-reference our memset override so its export is emitted. + // See quirks_memset.zig for details on why this exists. + _ = @import("quirks_memset.zig"); +} + test { // Zig 0.16.0 has made test logging more strict. Now, *anything* that gets // printed to stderr results in a "failed command" message, even if the @@ -28,4 +34,5 @@ test { // log spam in general), we bump the default testing log level to error. std.testing.log_level = std.log.Level.err; _ = entrypoint; + _ = @import("quirks_memset.zig"); } diff --git a/src/main_bench.zig b/src/main_bench.zig index 9804f51ef..c48724903 100644 --- a/src/main_bench.zig +++ b/src/main_bench.zig @@ -1,3 +1,9 @@ const benchmark = @import("benchmark/main.zig"); +comptime { + // Force-reference our memset override so its export is emitted. + // See quirks_memset.zig for details on why this exists. + _ = @import("quirks_memset.zig"); +} + pub const main = benchmark.cli.main; diff --git a/src/main_c.zig b/src/main_c.zig index d14a455b4..3e331e022 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -44,6 +44,10 @@ comptime { // Our benchmark API. We probably want to gate this on a build // config in the future but for now we always just export it. _ = @import("benchmark/main.zig").CApi; + + // Force-reference our memset override so its export is emitted. + // See quirks_memset.zig for details on why this exists. + _ = @import("quirks_memset.zig"); } /// ghostty_info_s diff --git a/src/main_gen.zig b/src/main_gen.zig index 3342bc2e9..d4df3b9ec 100644 --- a/src/main_gen.zig +++ b/src/main_gen.zig @@ -1,3 +1,9 @@ const synthetic = @import("synthetic/main.zig"); +comptime { + // Force-reference our memset override so its export is emitted. + // See quirks_memset.zig for details on why this exists. + _ = @import("quirks_memset.zig"); +} + pub const main = synthetic.cli.main; diff --git a/src/quirks_memset.zig b/src/quirks_memset.zig new file mode 100644 index 000000000..6293b1c4f --- /dev/null +++ b/src/quirks_memset.zig @@ -0,0 +1,322 @@ +//! Provides a fast `memset` symbol that overrides the slow scalar +//! implementation provided by Zig 0.16's compiler_rt. +//! +//! Consider deleting when upstream ships an optimized compiler_rt +//! memset (tracked by ziglang/zig#32091, maybe fixed at +//! ziglang/zig#35754). To verify it is safe to delete, check that +//! the disassembly of `memset` in a ReleaseFast binary is vectorized +//! (or at least not a byte-at-a-time loop). +//! +//! ## Background +//! +//! Zig 0.16.0 globally disabled LLVM loop auto-vectorization to work +//! around an LLVM 21 miscompilation (llvm/llvm-project#186922, see +//! the 0.16.0 release notes). compiler_rt's memset is a naive +//! byte-at-a-time loop that relied entirely on auto-vectorization, +//! so it now compiles to a scalar loop roughly 24x slower than what +//! Zig 0.15 produced. +//! +//! In benchmarks this made `ghostty-bench +terminal-stream` 2.8x slower on +//! ASCII input (memset was 63% of all executed instructions!). +//! +//! Because compiler_rt's symbol is weak, exporting a strong `memset` +//! from our own code transparently overrides it everywhere. The +//! implementation below is manually vectorized with `@Vector`, which +//! does not depend on the disabled loop vectorizer. +//! +//! Other mem functions deliberately not overridden: +//! +//! - memcpy/memmove: compiler_rt's implementations are manually +//! vectorized upstream ("memcpyFast") and remain fast. +//! - memcmp/bcmp/strlen: also scalar in 0.16, but they never showed +//! up in our profiles. +//! +//! This file has no effect unless it is referenced from an artifact +//! root (e.g. `comptime { _ = @import("quirks_memset.zig"); }`). +//! It must NOT be imported from shared code, otherwise downstream +//! consumers of our Zig modules would get this export injected into +//! their binaries. +//! +//! ## References +//! +//! I referenced the musl asm + c memset implementation (MIT licensed). +//! The primary thing I took away from that is the dc zva trick for +//! aarch64. The remainder is fairly obvious memset work. + +const std = @import("std"); +const builtin = @import("builtin"); + +comptime { + // Strong linkage when we control the final link (executables, + // shared libraries), weak otherwise. + const linkage: std.builtin.GlobalLinkage = switch (builtin.output_mode) { + .Exe => .strong, + .Lib => switch (builtin.link_mode) { + .dynamic => .strong, + .static => .weak, + }, + .Obj => .weak, + }; + + // Whether the override is emitted: + // + // 1. On targets without SIMD we disable, since compiler_rt's + // scalar operations are going to be just as good. + // 2. The C object format target uses strong linkage which + // conflits with ours and errors. + // 3. Weak COFF builds fatally error because MSVC's linker + // errors when two identical linked symbols exist. MSVC has + // CRT which links so we don't need this there anyways. + const enabled = + std.simd.suggestVectorLength(u8) != null and + builtin.object_format != .c and + !(linkage == .weak and builtin.object_format == .coff); + + if (enabled) @export(&memset, .{ + .name = "memset", + .linkage = linkage, + + // Hidden so that shared library builds (libghostty, + // libghostty-vt) resolve this internally without exporting + // it to their host applications. + .visibility = .hidden, + }); +} + +/// Bytes stored per loop iteration. Twice the native vector size so +/// each iteration issues two vector stores (e.g. `stp q0, q0` on +/// aarch64). Capped at 128 both because the small path below relies +/// on len < 128 once the loop is skipped, and because wider single +/// iterations stop paying off anyway (e.g. on scalable-vector +/// targets that suggest very large lengths). +const vec_bytes = @min(128, 2 * (std.simd.suggestVectorLength(u8) orelse 8)); + +/// Whether the `dc zva` fast path for large zero fills is available. +/// `dc zva` zeroes a whole cacheline per instruction without moving data +/// through the store pipeline. Based on musl. +const zva_enabled = builtin.cpu.arch == .aarch64 and + builtin.os.tag != .freestanding; + +/// Only use `dc zva` at or above this many bytes. Below this our +/// plain vector loop measures faster. Empirically mesaured. +const zva_threshold = 16384; + +/// Matches compiler_rt's memset signature (lib/compiler_rt.zig). +fn memset(dest: ?[*]u8, c: u8, len: usize) callconv(.c) ?[*]u8 { + @setRuntimeSafety(false); + + if (len == 0) return dest; + const d = dest.?; + + // Large path: full-width vector stores. + if (len >= vec_bytes) { + // Very large zero fills: zero whole cachelines with `dc zva` + // instead. Only implemented for the (universal in practice) + // 64-byte block size; anything else falls through to the + // vector loop. + if (comptime zva_enabled) { + if (len >= zva_threshold and c == 0) zva: { + if (zvaSize() != 64) break :zva; + const splat64: @Vector(64, u8) = @splat(0); + + // Head: one unaligned store covering every byte up + // to the first 64-aligned address (and usually a bit + // beyond it; the overlap is fine). + const addr = @intFromPtr(d); + const aligned = std.mem.alignForward(usize, addr, 64); + d[0..64].* = splat64; + + // Zero whole aligned cachelines while at least one + // full line remains in range. `dc zva` requires the + // WHOLE line to be inside the buffer: it always + // zeroes all 64 bytes. + var p: [*]u8 = @ptrFromInt(aligned); + const end_addr = addr + len; + while (@intFromPtr(p) + 64 <= end_addr) : (p += 64) { + asm volatile ("dc zva, %[ptr]" + : + : [ptr] "r" (p), + : .{ .memory = true }); + } + + // Tail: overlapping unaligned store anchored to the + // end of the buffer, covering whatever the line loop + // could not. len >= zva_threshold >= 64 so this + // cannot underflow. + d[len - 64 ..][0..64].* = splat64; + return dest; + } + } + + const splat: @Vector(vec_bytes, u8) = @splat(c); + + // Fill [0, N) where N is len rounded down to a multiple of + // vec_bytes. + var i: usize = 0; + while (i + vec_bytes <= len) : (i += vec_bytes) { + d[i..][0..vec_bytes].* = splat; + + // This empty asm statement prevents LLVM's + // LoopIdiomRecognize pass from replacing this loop with + // a call to memset, which would be infinite recursion + // since we ARE memset. compiler_rt is protected from + // this by being built with -fno-builtin; Ghostty is not. + asm volatile ("" ::: .{ .memory = true }); + } + + // Fill the remaining [N, len) tail, if any, with one more + // full-width store anchored to the END of the buffer. It + // overlaps up to vec_bytes-1 bytes that the loop already + // wrote, which is fine (same value), and cannot underflow + // because len >= vec_bytes in this path. + if (i != len) d[len - vec_bytes ..][0..vec_bytes].* = splat; + return dest; + } + + // Small path (len < vec_bytes): pairs of narrower stores, one + // anchored to the start of the buffer and one to the end. + // + // The invariant for each branch below: a store of width w at + // [0..w] plus a store at [len-w..len] covers every byte exactly + // when w <= len <= 2*w. Smaller lens are handled by a later + // branch; larger lens by an earlier one. + + comptime std.debug.assert(vec_bytes <= 128); + if (comptime vec_bytes > 64) { + if (len >= 64) { + // 64 <= len < vec_bytes <= 128 (asserted above): one + // pair of 64-byte stores covers len <= 128. Only emitted + // when vec_bytes > 64 (e.g. AVX-512), otherwise the + // vector loop above already handled these lengths. + const splat64: @Vector(64, u8) = @splat(c); + d[0..64].* = splat64; + d[len - 64 ..][0..64].* = splat64; + return dest; + } + } + if (len >= 16) { + // 16 <= len < @min(vec_bytes, 64), so at most 63. One pair + // of 16-byte stores covers len <= 32. For 32 < len < 64, a + // second pair extends the covered prefix to [0..32] and the + // covered suffix to [len-32..len], which meet or overlap in + // the middle. + const splat16: @Vector(16, u8) = @splat(c); + d[0..16].* = splat16; + d[len - 16 ..][0..16].* = splat16; + if (len > 32) { + d[16..32].* = splat16; + d[len - 32 ..][0..16].* = splat16; + } + return dest; + } + if (len >= 8) { + // 8 <= len <= 15: one 8-byte pair (covers len <= 16). + const splat8: @Vector(8, u8) = @splat(c); + d[0..8].* = splat8; + d[len - 8 ..][0..8].* = splat8; + return dest; + } + if (len >= 4) { + // 4 <= len <= 7: one 4-byte pair (covers len <= 8). + const splat4: @Vector(4, u8) = @splat(c); + d[0..4].* = splat4; + d[len - 4 ..][0..4].* = splat4; + return dest; + } + + // 1 <= len <= 3: too short for the pair trick; write bytes one + // at a time. The asm statement again keeps LoopIdiomRecognize + // from turning this into a recursive memset call; LLVM will do + // that even for a loop this short because the trip count is not + // statically known. + var i: usize = 0; + while (i < len) : (i += 1) { + d[i] = c; + asm volatile ("" ::: .{ .memory = true }); + } + return dest; +} + +/// Cached `dc zva` block size: 0 = not yet queried, 1 = unavailable +/// or prohibited, otherwise the block size in bytes. Racing threads +/// store the same value so the memory ordering is irrelevant. +var zva_size: std.atomic.Value(usize) = .init(0); + +fn zvaSize() usize { + const cached = zva_size.load(.monotonic); + if (cached != 0) return cached; + const dczid = asm ("mrs %[out], dczid_el0" + : [out] "=r" (-> u64), + ); + // Bit 4 (DZP) prohibits DC ZVA; bits 0-3 are log2 of the block + // size in 4-byte words. + const size: usize = if (dczid & 0x10 != 0) + 1 + else + @as(usize, 4) << @intCast(dczid & 0xF); + zva_size.store(size, .monotonic); + return size; +} + +test memset { + const testing = std.testing; + + // A buffer larger than anything the small/vector paths special + // case, with room for offset (alignment) variations. Guard bytes + // around the fill region must remain untouched. + var buf: [4 * vec_bytes + 33]u8 = undefined; + for (0..vec_bytes + 1) |offset| { + for (0..buf.len - offset) |len| { + for ([_]u8{ 0x00, 0x5C, 0xFF }) |c| { + @memset(&buf, 0xAA); + const region = buf[offset..][0..len]; + const ret = memset(region.ptr, c, len); + try testing.expectEqual(region.ptr, ret.?); + for (buf[0..offset]) |b| try testing.expectEqual(0xAA, b); + for (region) |b| try testing.expectEqual(c, b); + for (buf[offset + len ..]) |b| try testing.expectEqual(0xAA, b); + } + } + } + + // Zero length must not touch memory and must tolerate null. + try testing.expectEqual(null, memset(null, 0x11, 0)); + var one: [1]u8 = .{0xAA}; + _ = memset(&one, 0x11, 0); + try testing.expectEqual(0xAA, one[0]); +} + +test "memset large zero fills (dc zva path)" { + const testing = std.testing; + const alloc = testing.allocator; + + // Sizes at and around the ZVA threshold, with a guard byte on + // each side and every offset within a cacheline so the head and + // tail alignment handling is fully exercised. + const buf = try alloc.alloc(u8, 2 * zva_threshold + 66); + defer alloc.free(buf); + + for ([_]usize{ + zva_threshold - 1, + zva_threshold, + zva_threshold + 63, + 2 * zva_threshold, + }) |len| { + for (0..65) |offset| { + @memset(buf, 0xAA); + const region = buf[1 + offset ..][0..len]; + _ = memset(region.ptr, 0x00, len); + for (buf[0 .. 1 + offset]) |b| try testing.expectEqual(0xAA, b); + for (region) |b| try testing.expectEqual(0x00, b); + for (buf[1 + offset + len ..]) |b| try testing.expectEqual(0xAA, b); + } + } + + // Nonzero fills of ZVA-eligible sizes must not take the ZVA path + // (it can only write zeroes). + @memset(buf, 0xAA); + _ = memset(buf.ptr + 1, 0x5C, zva_threshold); + try testing.expectEqual(0xAA, buf[0]); + for (buf[1 .. 1 + zva_threshold]) |b| try testing.expectEqual(0x5C, b); + try testing.expectEqual(0xAA, buf[1 + zva_threshold]); +} diff --git a/src/simd/main.zig b/src/simd/main.zig index aabdd21d1..8a3f00670 100644 --- a/src/simd/main.zig +++ b/src/simd/main.zig @@ -9,6 +9,18 @@ pub const index_of = @import("index_of.zig"); pub const vt = @import("vt.zig"); pub const codepointWidth = codepoint_width.codepointWidth; +/// The number of vector lanes to use for manually vectorized hot +/// loops operating on elements of type T, or null if the target has +/// no usable SIMD support and a plain scalar loop should be used +/// instead (e.g. wasm32 without simd128). +/// +/// This is twice the native vector width so that each iteration works +/// two vector registers, giving better instruction-level parallelism. +pub fn lanes(comptime T: type) ?comptime_int { + const native = std.simd.suggestVectorLength(T) orelse return null; + return native * 2; +} + test { @import("std").testing.refAllDecls(@This()); } diff --git a/src/synthetic/cli.zig b/src/synthetic/cli.zig index 12acfaa1f..dcac0d0dc 100644 --- a/src/synthetic/cli.zig +++ b/src/synthetic/cli.zig @@ -1,7 +1,6 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const cli = @import("../cli.zig"); -const global = @import("../global.zig"); /// The available actions for the CLI. This is the list of available /// synthetic generators. View docs for each individual one in the @@ -93,7 +92,7 @@ fn mainActionImpl( // Our output always goes to stdout. var buffer: [2048]u8 = undefined; - var stdout_writer = std.Io.File.stdout().writer(global.io(), &buffer); + var stdout_writer = std.Io.File.stdout().writer(io, &buffer); const writer = &stdout_writer.interface; // Create our implementation diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 98a64ef83..6ec768f4f 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -10,6 +10,7 @@ const assert = @import("../quirks.zig").inlineAssert; const tripwire = @import("../tripwire.zig"); const testing = std.testing; const Allocator = std.mem.Allocator; +const simd = @import("../simd/main.zig"); const unicode = @import("../unicode/main.zig"); const uucode = @import("uucode"); @@ -630,6 +631,85 @@ inline fn printSliceEligible(cp: u32, comptime width: PrintSliceWidth) bool { }); } +/// Store a run of narrow codepoint cells built from a bit template: +/// for each `idx` in `[from, to)`, `cells[idx]` is assigned the bits +/// `template_bits | (cps[idx] << cp_shift)`. +/// +/// The template is a complete Cell (content tag, style, wide state, +/// etc. already baked in by the caller) whose codepoint content bits +/// are zero. Since Cell is a packed struct(u64), OR-ing a codepoint +/// into the content field's bit position yields a finished cell as a +/// single integer, keeping the loop pure data movement: no per-cell +/// field assignments and no branches. +/// +/// This loop is manually vectorized: Zig 0.16 (LLVM 21) no longer +/// auto-vectorizes the scalar form the way Zig 0.15 (LLVM 20) did. +inline fn printSliceStoreRun( + cells: [*]Cell, + cps: [*]const u32, + from: usize, + to: usize, + template_bits: u64, +) void { + // The bit position of the `content` field within the packed + // Cell. A codepoint occupies the low bits of `content`, so + // shifting a codepoint left by this amount places it exactly + // where `.content = .{ .codepoint = .{ .data = cp } }` would. + const cp_shift = @bitOffsetOf(Cell, "content"); + + // Since codepoints are OR'd into the content field rather than + // assigned, any nonzero content bits in the template would + // corrupt the stored codepoints. + const content_mask: u64 = comptime mask: { + const bits = @bitSizeOf(@FieldType(Cell, "content")); + break :mask ((1 << bits) - 1) << cp_shift; + }; + assert(template_bits & content_mask == 0); + + var idx = from; + + // Vectorized bulk of the run. + if (simd.lanes(u64)) |lanes| { + // u64 due to backing integer of Cell + const V = @Vector(lanes, u64); + + // The template and shift amount are loop-invariant, so + // broadcast them to every lane once up front. + const template: V = @splat(template_bits); + const shift: @Vector( + lanes, + std.math.Log2Int(u64), + ) = @splat(cp_shift); + + while (idx + lanes <= to) : (idx += lanes) { + // Load `lanes` decoded codepoints... + const narrow: @Vector(lanes, u32) = cps[idx..][0..lanes].*; + + // ...widen each u32 lane to the u64 cell size... + const wide: V = @intCast(narrow); + + // ...shift each codepoint into the content field's bit + // position and merge with the template, producing + // `lanes` finished cells (coerced from vector to array + // so they can be bitcast for the store below)... + const bits: [lanes]u64 = template | (wide << shift); + + // ...and store them contiguously. Cell is a packed + // struct(u64) so an array of u64 bit patterns has + // identical layout to an array of cells. + cells[idx..][0..lanes].* = @bitCast(bits); + } + } + + // Scalar tail: the final `< lanes` cells of the run, or the + // entire run on targets without SIMD. Note `idx` carries over + // from the vector loop above. This is the same computation as + // the vector body, one cell at a time. + while (idx < to) : (idx += 1) { + cells[idx] = @bitCast(template_bits | (@as(u64, cps[idx]) << cp_shift)); + } +} + /// The row-filling portion of the printSlice fast path, specialized by /// width class. The first codepoint must already be validated by the /// caller (printSliceFast). @@ -671,19 +751,20 @@ fn printSliceFill( // Anything else (including eligible unicode) proceeds via // the scalar loop below. if (comptime width == .narrow) { - const lanes = 8; - const V = @Vector(lanes, u32); - const lo: V = @splat(0x10); - const hi: V = @splat(0xFF); - while (idx + lanes <= cps.len) { - const v: V = cps[idx..][0..lanes].*; - const in_range = (v >= lo) & (v <= hi); - if (!@reduce(.And, in_range)) { - const bits: std.meta.Int(.unsigned, lanes) = @bitCast(in_range); - idx += @ctz(~bits); - break; + if (simd.lanes(u32)) |lanes| { + const V = @Vector(lanes, u32); + const lo: V = @splat(0x10); + const hi: V = @splat(0xFF); + while (idx + lanes <= cps.len) { + const v: V = cps[idx..][0..lanes].*; + const in_range = (v >= lo) & (v <= hi); + if (!@reduce(.And, in_range)) { + const bits: std.meta.Int(.unsigned, lanes) = @bitCast(in_range); + idx += @ctz(~bits); + break; + } + idx += lanes; } - idx += lanes; } } @@ -817,12 +898,38 @@ fn printSliceFill( // We can only write whole (wide, spacer) pairs. const pair_end = k + (simple - k) / 2 * 2; var idx = k; + + // Manually vectorized for the same reason as + // printSliceStoreRun: build a vector of wide cells, + // interleave with spacer tails, and store (wide, + // spacer) pairs several at a time. + if (simd.lanes(u64)) |lanes| { + const pair_lanes = lanes / 2; + const Vp = @Vector(pair_lanes, u64); + const wide_v: Vp = @splat(wide_bits); + const spacer_v: Vp = @splat(spacer_bits); + const shift_v: @Vector( + pair_lanes, + std.math.Log2Int(u64), + ) = @splat(cp_shift); + while (idx + 2 * pair_lanes <= pair_end) : (idx += 2 * pair_lanes) { + const narrow: @Vector(pair_lanes, u32) = + cps[printed + idx / 2 ..][0..pair_lanes].*; + const wides: Vp = wide_v | (@as(Vp, @intCast(narrow)) << shift_v); + const inter: [2 * pair_lanes]u64 = std.simd.interlace(.{ + wides, + spacer_v, + }); + cells[idx..][0 .. 2 * pair_lanes].* = @bitCast(inter); + } + } while (idx < pair_end) : (idx += 2) { cells[idx] = @bitCast( wide_bits | (@as(u64, cps[printed + idx / 2]) << cp_shift), ); cells[idx + 1] = @bitCast(spacer_bits); } + // If the simple run ended mid-pair we stop at the pair // boundary and handle the offending cell below. k = pair_end; @@ -832,11 +939,13 @@ fn printSliceFill( simple = pair_end; } } else { - for (k..simple) |idx| { - cells[idx] = @bitCast( - template_bits | (@as(u64, cps[printed + idx]) << cp_shift), - ); - } + printSliceStoreRun( + cells, + cps.ptr + printed, + k, + simple, + template_bits, + ); k = simple; } if (k >= cell_count) break; @@ -888,11 +997,13 @@ fn printSliceFill( page.styles.useMultiple(page.memory, style_id, @intCast(n)); } - for (k..m) |idx| { - cells[idx] = @bitCast( - template_bits | (@as(u64, cps[printed + idx]) << cp_shift), - ); - } + printSliceStoreRun( + cells, + cps.ptr + printed, + k, + m, + template_bits, + ); k = m; continue :fill; } diff --git a/src/terminal/hash_map.zig b/src/terminal/hash_map.zig index d8feddcd1..abef87e3b 100644 --- a/src/terminal/hash_map.zig +++ b/src/terminal/hash_map.zig @@ -70,8 +70,24 @@ fn AutoHashMapUnmanaged( fn AutoContext(comptime K: type) type { return struct { - pub const hash = std.hash_map.getAutoHashFn(K, @This()); pub const eql = std.hash_map.getAutoEqlFn(K, @This()); + + pub fn hash(_: @This(), key: K) u64 { + if (comptime std.meta.hasUniqueRepresentation(K)) { + // LLVM 21 (Zig 0.16) failed to inline this which resulted + // in a measurable almost 2x slowdown on our hyperlink map + // benchmark. So, force it. + return @call( + .always_inline, + std.hash.Wyhash.hash, + .{ 0, std.mem.asBytes(&key) }, + ); + } + + var hasher = std.hash.Wyhash.init(0); + std.hash.autoHash(&hasher, key); + return hasher.final(); + } }; } diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 31769df03..791d0c55d 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -579,8 +579,30 @@ pub fn Stream(comptime H: type) type { continue; } + // Find the end of the printable run. This is an + // early-exit search loop that LLVM won't + // auto-vectorize, and printable runs dominate real + // input, so scan several codepoints at a time + // manually (same idiom as the printSliceFill run + // scan). var end = i + 1; - while (end < cps.len and cps[end] > 0xF) end += 1; + scan: { + if (simd.lanes(u32)) |lanes| { + const V = @Vector(lanes, u32); + const threshold: V = @splat(0xF); + while (end + lanes <= cps.len) { + const v: V = cps[end..][0..lanes].*; + const gt = v > threshold; + if (!@reduce(.And, gt)) { + const bits: std.meta.Int(.unsigned, lanes) = @bitCast(gt); + end += @ctz(~bits); + break :scan; + } + end += lanes; + } + } + while (end < cps.len and cps[end] > 0xF) end += 1; + } self.handler.vt(.print_slice, .{ .cps = cps[i..end] }); i = end; } diff --git a/src/unicode/grapheme.zig b/src/unicode/grapheme.zig index eed5e6e75..799689357 100644 --- a/src/unicode/grapheme.zig +++ b/src/unicode/grapheme.zig @@ -141,8 +141,8 @@ inline fn invalidCodepoint(cp: anytype) bool { /// This is all the structures and data for the precomputed lookup table /// for all possible permutations of state and grapheme break properties. -/// Precomputation requires 2^13 keys of 4 bit values so the whole table is -/// 8KB. +/// Precomputation requires 2^13 keys of byte-sized values so the whole +/// table is 8KB. const Precompute = struct { const Key = packed struct(u13) { state: uucode.grapheme.BreakState, @@ -154,9 +154,15 @@ const Precompute = struct { } }; - const Value = packed struct(u4) { + const Value = packed struct(u8) { result: bool, state: uucode.grapheme.BreakState, + + /// Explicit padding to give the struct a power-of-two backing + /// integer, for the same reason as `Properties._padding` (avoids + /// exotic-width LLVM integer accesses that defeat register + /// promotion). @sizeOf was already 1, so the table stays 8KB. + _padding: u4 = 0, }; const data = precompute: { diff --git a/src/unicode/props.zig b/src/unicode/props.zig index 6a4169747..dd6a4dbb1 100644 --- a/src/unicode/props.zig +++ b/src/unicode/props.zig @@ -7,7 +7,7 @@ const std = @import("std"); const uucode = @import("uucode"); -pub const Properties = packed struct { +pub const Properties = packed struct(u16) { /// Codepoint width. We clamp to [0, 2] since Ghostty handles control /// characters and we max out at 2 for wide characters (i.e. 3-em dash /// becomes a 2-em dash). @@ -23,6 +23,16 @@ pub const Properties = packed struct { /// Emoji VS compatibility emoji_vs_base: bool = false, + /// Explicit padding to give the struct a power-of-two backing integer. + /// This is a performance workaround: with a non-power-of-two backing + /// integer (u9), Zig 0.16 lowers loads/stores of this struct through + /// mismatched exotic-width LLVM integer types (store i9 / load i16), + /// which blocks LLVM from promoting temporaries to registers and forces + /// stack round-trips in hot paths like `graphemeBreak`. See + /// https://github.com/ziglang/zig/issues/21891 for the upstream issue. + /// The size of the lookup table is unchanged (@sizeOf was already 2). + _padding: u7 = 0, + // Needed for lut.Generator pub fn eql(a: Properties, b: Properties) bool { return a.width == b.width and