GtkGLArea had numerous downsides that forced us to invent unsightly
hacks in our renderer to work around them, most chiefly the fact that it
holds its own GdkGLContext on the main thread (GL contexts are not at
all thread-safe), forcing us to keep our GL calls on the main thread. It
also does not interact well with triple-buffering and initialization is
forced to be this sort of deferred song-and-dance since we need to wait
for the GLArea to initialize its GL context before we can initialize the
renderer, the core surface, and then most things in the GTK surface.
We instead invent our own custom widget named RenderSurface that takes
simple DMABUFs and displays them. The task of obtaining a GL context
falls to manual EGL bindings, since we also need EGL to export OpenGL
textures into DMABUFs. We keep the EGL context solely on the render
thread meaning that the main thread never concerns itself with rendering
except when being notified that the renderer has pushed a new frame.
What makes this extra significant is that now the entire GTK apprt no
longer depends on OpenGL in any way, shape or form. As long as it is
being fed DMABUFs, it can render from whichever graphics API you want.
This means we can add more backends based on OpenGL ES or more likely
Vulkan rather painlessly in the future.
**AI disclosure**: I came up with the idea and let Pi implement most of
the nitty-gritty details around EGL, as well as replumbing the renderer
and cleaning up all the GTK-specific workarounds there. I then carefully
vetted every line of code and spent roughly as much time reviewing as
coding. Most of the documentation and all commit messages are in my own
words.
1. Speeds up runs by ~3x for projects with a large number of
dependencies by downloading in parallel (~55s → 16s on my workstation).
2. Adds some workarounds for Zig 0.16's package management. The changes
don't affect Ghostty itself, but do affect downstream projects that
embed libghostty-vt and want to create Nix packages of their own.
AI disclosure: Claude was used to diagnose CI failures and suggest
fixes.
Repeated builds were penalized for three reasons:
1. The gresource XML embedded the absolute cache paths of the compiled
.ui files. It now uses relative paths, resolved against a `--sourcedir`
passed to glib-compile-resources.
2. Blueprints were compiled through a small Zig wrapper, and a Run step
hashes the bytes of the executable it runs. A Zig binary does not relink
to the same bytes, so after a branch switch that touched the wrapper
every .ui moved, the gresource compiler re-ran and the whole app
recompiled for identical output. blueprint-compiler is now run directly
and the wrapper is reduced to a single version check whose output
nothing reads.
3. The gresource pipeline was built once per artifact. It is now
memoized on the *std.Build.
Cold builds and builds after real blueprint changes are unaffected. A
build after the wrapper relinks goes from ~43s to ~7s on my system.
AI disclosure: Claude Opus 5 was used to pinpoint the source of the
cache poisoning and to prepare a patch. Claude Fable 5.1 measured it,
traced the remaining churn to the wrapper's relinks, and replaced a
custom build step with stock ones. The commit message is human-written
based on the author's understanding of the changes.
Fixes https://github.com/ghostty-org/ghostty/issues/14135.
<img width="1398" height="652" alt="image"
src="https://github.com/user-attachments/assets/6901a05e-9d23-4e25-89a7-c16c1694a0f9"
/>
> The #9168 fix is no longer needed, since the frame is now higher than
the actual glyph.
The frame change observation only affects those who have a custom window
title font set in their config. I asked Claude to run some main thread
benchmarking compared to `main`; it will gain some delays for rapid
title changes and window resizing. The additional cost is brought by the
frequent frame updates which are done by AppKit. But that's necessary
for updating the title to the correct style.
> I tried to do some diffing and removing duplicates, but it will add
too many changes too, and I didn't think it's worth doing so.
The amount looks ok to me.
### `window-title-font-family = PT Mono`
| Phase | Metric | base | branch | Δ | ratio |
|---|---|---:|---:|---:|---:|
| Idle, 3 s | main-thread CPU | 2.8 ms | 2.8 ms | -0.0 | 1.00 |
| | process CPU | 11.3 ms | 11.2 ms | -0.1 | 0.99 |
| Paced title updates, 150 × 100 ms | main-thread CPU | 878.5 ms |
**946.7 ms** | **+68.3** | **1.08** |
| | process CPU | 1219.9 ms | 1335.0 ms | +115.1 | 1.09 |
| | wall | 17.24 s | 17.34 s | +0.1 | 1.01 |
| Title burst, 5000 back-to-back | main-thread CPU | 180.7 ms | 181.4 ms
| +0.7 | 1.00 |
| | process CPU | 254.2 ms | 254.8 ms | +0.6 | 1.00 |
| | wall | 2.31 s | 2.32 s | +0.0 | 1.00 |
| `toggle_maximize` × 16 (animated resize) | main-thread CPU | 1947.8 ms
| **2178.0 ms** | **+230.2** | **1.12** |
| | process CPU | 4166.8 ms | 4403.2 ms | +236.4 | 1.06 |
| | wall | 16.68 s | 16.72 s | +0.0 | 1.00 |
| Native fullscreen enter/exit × 2 | main-thread CPU | 196.8 ms | 195.9
ms | -0.9 | 1.00 |
| | process CPU | 360.8 ms | 361.5 ms | +0.7 | 1.00 |
| | wall | 8.47 s | 8.47 s | +0.0 | 1.00 |
### AI Disclosure
Asked Claude to generate the harness to run the benchmark and review my
changes. I did the changes myself.
Closes#2630 for macOS. #13621 added the `move_tab_to_new_window` action
and the GTK side; this does the same on macOS with AppKit.
With native tabs a tab is already a window, so the action forwards to
AppKit's own `moveTabToNewWindow:`. It's a no-op when the window is
alone in its tab group, same as the Window menu item. The "only
implemented on Linux" note comes off the doc comment in `Binding.zig`.
Tested on macOS 26, from a keybind and from the command palette. I have
no macOS 13-15 machine.
Full disclosure, written with Claude Code, I directed it, read every
line, and tested the result.
Repeated builds were penalized for three reasons:
1. The gresource XML embedded the absolute cache paths of the compiled
.ui files. It now uses relative paths, resolved against a `--sourcedir`
passed to glib-compile-resources.
2. Blueprints were compiled through a small Zig wrapper, and a Run step
hashes the bytes of the executable it runs. A Zig binary does not relink
to the same bytes, so after a branch switch that touched the wrapper
every .ui moved, the gresource compiler re-ran and the whole app
recompiled for identical output. blueprint-compiler is now run directly
and the wrapper is reduced to a single version check whose output
nothing reads.
3. The gresource pipeline was built once per artifact. It is now
memoized on the *std.Build.
Cold builds and builds after real blueprint changes are unaffected. A
build after the wrapper relinks goes from ~43s to ~7s on my system.
AI disclosure: Claude Opus 5 was used to pinpoint the source of the
cache poisoning and to prepare a patch. Claude Fable 5.1 measured it,
traced the remaining churn to the wrapper's relinks, and replaced a
custom build step with stock ones. The commit message is human-written
based on the author's understanding of the changes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RsfB3eoSTLfnKa6zi25teN
This commit refactors our use of translate-c, in preparation for larger
removal of `cImport` and better co-ordination between building of C
dependencies and translation of headers.
The major update is the creation of an internal helper package that
wraps our use of the external translate-c library. This allows us to not
only have better shorthand and a data-driven, declarative approach to C
translation (versus the otherwise more imperative approach), it also
funnels the external dependency into a single package instead of
spreading it out among what will be an increasingly larger amount of
places as dependencies in `pkg/` get updated.
It also includes some refactors, namely to the harfbuzz package, which
has had its individual settings refactored into helpers to allow for the
settings to be better shared between translation and the build of the
c-based static library.
Wuffs has also had a bit of a refactor too so that we don't generate a
file with all of the macro defines in it - we just send these in as `-D`
flags now.
The action and its keybind exist, and GTK implements them, but macOS had no
handler so the binding did nothing there. AppKit already has the command for
window tabs, so this forwards to it.
A window that isn't in a tab group, or is alone in one, is already a window of
its own, so there is nothing to move and the action reports it did nothing.
Implements the remaining macOS half of #2630.
This commit refactors our use of translate-c, in preparation for larger
removal of cImport and better co-ordination between building of C
dependencies and translation of headers.
The major update is the creation of an internal helper package that
wraps our use of the external translate-c library. This allows us to not
only have better shorthand and a data-driven, declarative approach to C
translation (versus the otherwise more imperative approach), it also
funnels the external dependency into a single package instead of
spreading it out among what will be an increasingly larger amount of
places as dependencies in "pkg/" get updated.
It also includes some refactors, namely to the harfbuzz package, which
has had its individual settings refactored into helpers to allow for the
settings to be better shared between translation and the build of the
c-based static library.
Wuffs has also had a bit of a refactor too so that we don't generate a
file with all of the macro defines in it - we just send these in as "-D"
flags now.
Implements TinyIO for Windows which is used to save binary and runtime
costs. As a reminder, binary costs are saved because `std.Io` uses a
vtable so compilers can't prune any unused functions, so you pay for the
full cost. We can noop unused functions to save. Runtime is saved
because there is less state to carry for unused functionality like
concurrency primitives.
The impl itself is mostly taken from Zig directly. I ran tests on
Windows (arm64) and verified everything works as expected so far!
Binary size measurements before/after:
| Mode | Io owner | ghostty-vt.dll | vs. Threaded |
|--------------|-----------------|---------------:|-------------:|
| ReleaseFast | std.Io.Threaded | 2,209,280 | |
| ReleaseFast | TinyIo | 1,826,816 | -382,464 |
| ReleaseSmall | std.Io.Threaded | 1,541,632 | |
| ReleaseSmall | TinyIo | 1,199,616 | -342,016 |
The runtime savings are relatively small, but 1KB per terminal ain't
nothing:
| Io owner | Private, +100 terminals | Private, startup |
|-----------------|------------------------:|-----------------:|
| std.Io.Threaded | +161,845,248 | 782,336 |
| TinyIo | +161,742,848 | 729,088 |
Implements TinyIO for Windows which is used to save binary and runtime
costs. As a reminder, binary costs are saved because `std.Io` uses a
vtable so compilers can't prune any unused functions, so you pay for the
full cost. We can noop unused functions to save. Runtime is saved
because there is less state to carry for unused functionality like
concurrency primitives.
The impl itself is mostly taken from Zig directly. I ran tests on
Windows (arm64) and verified everything works as expected so far!
Binary size measurements before/after:
| Mode | Io owner | ghostty-vt.dll | vs. Threaded |
|--------------|-----------------|---------------:|-------------:|
| ReleaseFast | std.Io.Threaded | 2,209,280 | |
| ReleaseFast | std.Io.failing | 1,815,552 | -393,728 |
| ReleaseFast | TinyIo | 1,826,816 | -382,464 |
| ReleaseSmall | std.Io.Threaded | 1,541,632 | |
| ReleaseSmall | std.Io.failing | 1,190,912 | -350,720 |
| ReleaseSmall | TinyIo | 1,199,616 | -342,016 |
The runtime savings are relatively small, but 1KB per terminal ain't nothing:
| Io owner | Private, +100 terminals | Private, startup |
|-----------------|------------------------:|-----------------:|
| std.Io.Threaded | +161,845,248 | 782,336 |
| TinyIo | +161,742,848 | 729,088 |
1. Speeds up runs by ~3x for projects with a large number of
dependencies by downloading in parallel (~55s → 16s on my workstation).
2. Adds some workarounds for Zig 0.16's package management. The changes
don't affect Ghostty itself, but do affect downstream projects that
embed libghostty-vt and want to create Nix packages of their own.
Two small patches that don't directly affect Ghostty, but do affect
programs that embed `libghostty-vt` and `libvaxis`, or
any other combination that also uses `uucode`.
AI disclosure: these bugs were discovered/fixed by Claude, but I've
rewritten parts of the patches and the comments.
CC @rockorager
This adds memory decommit/recommit support to Windows via
DiscardVirtualMemory. This allows unused page memory to be reclaimed the
same way it is already today on Linux and macOS.
DiscardVirtualMemory releases the physical pages behind a committed
range but leaves it committed, so a later access finds a zero page or
the old contents rather than faulting, and nothing has to be committed
again before reuse. That keeps recommit a no-op and, more importantly,
keeps restoring a compressed page infallible.
Windows has an alternative `VirtualFree(MEM_DECOMMIT)` followed by
`VirtualAlloc(MEM_COMMIT)` which releases the commit charge as well, but
Windows has no overcommit, so the commit can be refused on restore and
our restore path doesn't support OOM.
https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-discardvirtualmemory
Now that our core surface does not depend on any GTK-sided OpenGL
initialization, we can initialize it a lot earlier than before, during
the first resize event right after GTK allocates the size for the widget.
GtkGLArea had numerous downsides that forced us to invent unsightly hacks
in our renderer to work around them, most chiefly the fact that it holds
its own GdkGLContext on the main thread (GL contexts are not at all
thread-safe), forcing us to keep our GL calls on the main thread.
It also does not interact well with triple-buffering and initialization
is forced to be this sort of deferred song-and-dance since we need to
wait for the GLArea to initialize its GL context before we can initialize
the renderer, the core surface, and then most things in the GTK surface.
We instead invent our own custom widget named RenderSurface that takes
simple DMABUFs and displays them. The task of obtaining a GL context
falls to manual EGL bindings, since we also need EGL to export OpenGL
textures into DMABUFs. We keep the EGL context solely on the render
thread meaning that the main thread never concerns itself with rendering
except when being notified that the renderer has pushed a new frame.
What makes this extra significant is that now the entire GTK apprt no
longer depends on OpenGL in any way, shape or form. As long as it is
being fed DMABUFs, it can render from whichever graphics API you want.
This means we can add more backends based on OpenGL ES or more likely
Vulkan rather painlessly in the future.
**AI disclosure**: I came up with the idea and let Pi implement most of
the nitty-gritty details around EGL, as well as replumbing the renderer
and cleaning up all the GTK-specific workarounds there. I then carefully
vetted every line of code and spent roughly as much time reviewing as
coding. Most of the documentation and all commit messages are in my
own words.
This adds memory decommit/recommit support to Windows via
DiscardVirtualMemory. This allows unused page memory to be reclaimed
the same way it is already today on Linux and macOS.
DiscardVirtualMemory releases the physical pages behind a committed
range but leaves it committed, so a later access finds a zero page or
the old contents rather than faulting, and nothing has to be committed
again before reuse. That keeps recommit a no-op and, more importantly,
keeps restoring a compressed page infallible.
Windows has an alternative `VirtualFree(MEM_DECOMMIT)` followed by
`VirtualAlloc(MEM_COMMIT)` which releases the commit charge as well, but
Windows has no overcommit, so the commit can be refused on restore and our
restore path doesn't support OOM.
https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-discardvirtualmemory
The Kitty graphics file and temporary file mediums open a
client-supplied path and the Kitty specification only specifies the
blocklist for Unix-style machines.
Windows has various unsafe paths as well that we should very obviously
block. This diverges from the Kitty specification for now (I plan on
reporting this upstream and asking for feedback) but I think its the
right move for security.
Windows dangerous namespaces:
- A UNC path (`\\server\share\x`, also `//server/share/x`) makes the
process resolve the host and authenticate to it over SMB.
- The device namespaces (`\\.\`, `\\?\`, `\??\`) reach raw volumes and
named pipes, where the open connects to something or blocks.
- Reserved DOS device names (CON, NUL, COM1, ...) resolve to devices
from inside any directory.
These are now blocked.
This commit also heap allocates the path buffer because max path on
windows is around 100KB. :)\
**AI usage:** Fable and Astra both helped with validation, edge cases.
The Kitty graphics file and temporary file mediums open a
client-supplied path and the Kitty specification only specifies the
blocklist for Unix-style machines.
Windows has various unsafe paths as well that we should very obviously
block. This diverges from the Kitty specification for now (I plan
on reporting this upstream and asking for feedback) but I think its the
right move for security.
Windows dangerous namespaces:
- A UNC path (`\\server\share\x`, also `//server/share/x`) makes the
process resolve the host and authenticate to it over SMB.
- The device namespaces (`\\.\`, `\\?\`, `\??\`) reach raw volumes and
named pipes, where the open connects to something or blocks.
- Reserved DOS device names (CON, NUL, COM1, ...) resolve to devices
from inside any directory.
These are now blocked.
This commit also heap allocates the path buffer because max path on
windows is around 100KB. :)