Commit Graph

17236 Commits

Author SHA1 Message Date
Mitchell Hashimoto
a8e9b413f1 libghostty: simplify Wasm allocation API
Replace a bunch of type-specific Wasm allocation functions with a generic
byte allocator and reusable opaque out-parameters for pointers. This
makes it a lot more ergonomic (relatively) to use the Wasm interface
and removes a dozen or so exports.

This also updates the `ghostty_type_json` `abi` field with a maximum
alignment value that host sides can use to keep every allocation aligned
properly, easily, without hardcoding numbers.

This adds a test to verify this all works as intended and runs in CI.
2026-08-16 12:39:51 -07:00
Mitchell Hashimoto
0ba6250388 libghostty: ghostty_type_json expanded with more metadata, every enum member, packed layouts, etc. (#13856)
This PR makes `ghostty_type_json` contain more metadata necessary for
FFI without access to the C header to produce safe, ABI compliant field
access.

The existing `ghostty_type_json` didn't expose enough information:
embedders still had to hardcode enum values, tagged union values, and
packed bit layouts. For wasm this meant copying offsets and masks from
Zig internals and hoping they didn't drift. This isn't an ABI I want to
promise.

This also includes a formal JSON schema included in Doxygen docs and
used in CI to continuously validate our output structure. I'd like to
expand in the future comparing to actual C headers too.

## Examples

### Named References, Arrays

```json
"GhosttyRenderStateColors": {
  "kind": "struct",
  "size": 792,
  "align": 8,
  "fields": {
    "background": {
      "offset": 8,
      "size": 3,
      "type": "GhosttyColorRgb"
    },
    "palette": {
      "offset": 18,
      "size": 768,
      "type": "array",
      "elem": "GhosttyColorRgb",
      "count": 256
    }
  }
}
```

### Pointers

```json
"GhosttyCellsView": {
  "kind": "struct",
  "size": 16,
  "align": 8,
  "fields": {
    "ptr": {
      "offset": 0,
      "size": 8,
      "type": "pointer",
      "elem": "GhosttyCell",
      "const": true,
      "nullable": true
    },
    "len": {
      "offset": 8,
      "size": 8,
      "type": "u64"
    }
  }
}
```

### Enums

```json
"GhosttyStyleColorTag": {
  "kind": "enum",
  "size": 4,
  "align": 4,
  "underlying": "i32",
  "prefix": "GHOSTTY_STYLE_COLOR_",
  "values": {
    "NONE": 0,
    "PALETTE": 1,
    "RGB": 2,
    "TAG_MAX_VALUE": 2147483647
  }
}
```

### Packed Struct

```json
"GhosttyCell": {
  "kind": "packed",
  "size": 8,
  "align": 8,
  "underlying": "u64",
  "bits": {
    "content_tag": {
      "lsb": 0,
      "width": 2,
      "type": "GhosttyCellContentTag"
    },
    "content": {
      "lsb": 2,
      "width": 24,
      "kind": "union",
      "tag": "content_tag",
      "arms": {
        "BG_COLOR_RGB": {
          "kind": "packed",
          "width": 24,
          "bits": {
            "r": {"lsb": 0, "width": 8, "type": "u8"},
            "g": {"lsb": 8, "width": 8, "type": "u8"},
            "b": {"lsb": 16, "width": 8, "type": "u8"}
          }
        }
      }
    }
  }
}
```
2026-08-16 06:43:14 -07:00
trag1c
26df373ec8 i18n: update Hebrew translations for v1.4 (#13785)
Update the Hebrew translations in po/he.po with the 181 new strings for
v1.4, as requested in issue #13766 :)
2026-08-16 11:28:58 +02:00
Mitchell Hashimoto
0e8b7bea63 vt: expose packed cell layout
GhosttyCell was exposed as a raw integer while its manifest entry was only an alias, forcing bulk-read consumers to duplicate the internal cell bit layout.\n\nAdd reflection helpers for packed structs and tagged unions, and keep the C-facing layout metadata next to Cell itself. Extend the ABI manifest and schema with recursive bit descriptors so every content arm, including palette and RGB backgrounds, can be decoded without hardcoded masks.\n\nDocument manifest-driven cell decoding and test the metadata against Zig reflection and real cell values.
2026-08-15 21:16:34 -07:00
Mitchell Hashimoto
c75559589e libghostty: add ABI manifest schema
The ABI manifest previously had no machine-readable grammar or test that
the public export conformed to it.

Define a Draft 2020-12 schema and add a build check that executes
ghostty_type_json for native and wasm libraries before validation. Run
both forms in CI and publish the schema with the generated API docs.
2026-08-15 21:16:34 -07:00
Mitchell Hashimoto
9673a22b01 libghostty: expand ABI type metadata
The type metadata export only described extern struct layouts, leaving embedders to mirror enum values and tagged union relationships.

Describe every public C type in a versioned manifest with target and build metadata. Keep union field renames alongside their source tagged unions so the manifest uses public C names without changing Zig value layouts.
2026-08-15 21:16:34 -07:00
Mitchell Hashimoto
ad6e72ddc4 libghostty: add dedicated dirty row iteration + clear functions (#13852)
Add render state C APIs for iterating only rows that require a redraw
and for marking a completed frame clean in one call.

A one-row update in a 24-row viewport reduces dirty-row discovery from
50 calls to two, while cleanup becomes one call instead of O(N) of rows.

This lower call count is massive for environments where FFI is expensive
(Go, wasm).

The dirty next API outputs the viewport y because it jumps, unlike the
normal sequential next where its trivial for a caller to keep track.
2026-08-15 21:11:21 -07:00
Mitchell Hashimoto
16c833c5f1 libghostty: add render state structured cursor read (#13851)
A normal renderer would have to call `ghostty_render_state_get` _eight
times_ to reconstruct the cursor. In languages where FFI is expensive
(Go, wasm, etc.), this showed up in profiles of every frame.

Add a sized cursor snapshot and expose it. Also expose the existing
color snapshot through ghostty_render_state_get and remove the older
dedicated color getter.

Found during my normal Go/wasm adventures.
2026-08-15 21:08:27 -07:00
Mitchell Hashimoto
0d37f2d34d libghostty: add dedicated dirty row iteration + clear functions
Add render state C APIs for iterating only rows that require a redraw
and for marking a completed frame clean in one call.  

A one-row update in a 24-row viewport reduces dirty-row discovery from 
50 calls to two, while cleanup becomes one call instead of O(N) of rows.

This lower call count is massive for environments where FFI is expensive
(Go, wasm).

The dirty next API outputs the viewport y because it jumps, unlike the
normal sequential next where its trivial for a caller to keep track.
2026-08-15 20:59:01 -07:00
Mitchell Hashimoto
b4079f00c8 libghostty: add render state structured cursor read
A normal renderer would have to call `ghostty_render_state_get`
_eight times_ to reconstruct the cursor. In languages where FFI is
expensive (Go, wasm, etc.), this showed up in profiles of every frame.

Add a sized cursor snapshot and expose it. Also expose the existing color
snapshot through ghostty_render_state_get and remove the older
dedicated color getter.
2026-08-15 20:54:40 -07:00
Jeffrey C. Ollie
02436fd4eb Update iTerm2 colorschemes (#13850)
Upstream release:
https://github.com/mbadolato/iTerm2-Color-Schemes/releases/tag/release-20260810-152212-0173c3c
2026-08-15 20:52:04 -05:00
mitchellh
f2897f31de deps: Update iTerm2 color schemes 2026-08-16 00:25:02 +00:00
Mitchell Hashimoto
9009122953 macos: send all insertText commits as key events (#13817)
Partially addresses #13796. Extends #13222.

Previously, `insertText` commits without marked text were delivered via
`sendText`, which applies paste semantics and wraps the text in
bracketed
paste when the program enables it. macOS dictation and other input
methods often commit without marked text, so programs treated dictated
text as a paste: opencode collapsed it into a `"[Pasted ~N lines]"` chip
and Neovim applied paste-mode handling.

`insertText` is only invoked by input methods (IME, dictation, emoji
picker, character viewer); real paste operations use a separate path.
Every non-empty commit is now sent as a key event — the same path
already used for preedit commits since #13222 — so input method text
always arrives as typed input.

Typing is unaffected (the accumulator path returns earlier) and Cmd+V
pastes are unaffected. `committedPreeditTextAction` is renamed to
`committedTextAction` since it no longer only handles preedit commits.

Testing:

- 311 macOS unit tests pass.
- Manually verified on macOS 26: dictation into Opencode and Neovim
  arrives inline with no paste handling; emoji picker inserts inline;
  Chinese IME composition unchanged; dictation in Neovim normal mode now
  behaves as keystrokes, matching Terminal.app.

Notes:

- Dictated "new line" now matches Terminal.app behavior (no newline
  with typed-text semantics). The previous behavior came from the paste
  path preserving the newline; a follow-up could deliver it as an
  Enter keypress if desired.

AI usage: drafted with OMO + OpenCode + DeepSeek V4 Pro assistance;
reviewed, edited, and manually tested by the author.
2026-08-15 14:26:19 -07:00
Mitchell Hashimoto
cbe8a0ed4e cli: keep cached SSH terminfo installs current (#13844)
On a local cache miss, always send our embedded terminfo source to the
remote `tic` instead of accepting any existing entry reported by
`infocmp`.

We also version cache entries using a content-derived hash of our
embedded terminfo. Non-matching entries produce a cache miss and trigger
(re)installation.
2026-08-15 14:24:37 -07:00
Mitchell Hashimoto
e524df6c82 terminal/apc: limit glyf decode allocations (#13849)
Limit individual allocations made while decoding registered glyf
outlines to 64 KB.

Carefully crafted glyf outlines could expand into ~768KB of memory per
glossary entry, which adds up to hundreds of MB per terminal surface.
Across many terminals this could cause issues.

The 64KB number was chosen by inspecting every glyph across Apple
symbols and Noto emoji and the largest single glyph found was 40KB. So,
64KB is generous while limiting each terminal to ~68MB of RAM for max
glyph glossaries.

AI was used only to write initial tests, I rewrote em.
2026-08-15 14:24:05 -07:00
Mitchell Hashimoto
746a11c721 libghostty: much faster terminal snapshot encode and decode for wasm (#13848)
Snapshot encode is now 4-7x faster, decode is 3x faster for wasm builds.

Snapshot decode is particularly important for wasm builds because
libghostty is mainly used on web as a terminal _viewer_ and snapshots
are the best, most efficient way to ship down full terminal state.

The biggest change here is a totally custom software CRC32
implementation, which accounted for ~70% of total decode time. Native
builds on aarch64/x86_64 use dedicated hardware instructions that wasm
doesn't have. We've written a custom CRC32 impl (verified against Zig
stdlib through randomized unit tests) that goes from 0.3 GB/s to 5 GB/s
throughput in V8.

## Benchmarks

Wasm on V8:

| Workload | Encode Before | Encode After | Speedup | Decode Before |
Decode After | Speedup |
|---|---|---|---|---|---|---|
| ascii | 290 MB/s | 1182 MB/s | 4.1x | 318 MB/s | 946 MB/s | 3.0x |
| styled (sgr16) | 387 MB/s | 2771 MB/s | 7.2x | 284 MB/s | 758 MB/s |
2.7x |
| sgr-truecolor | 361 MB/s | 2382 MB/s | 6.6x | 252 MB/s | 766 MB/s |
3.0x |
| cjk | 411 MB/s | 2686 MB/s | 6.5x | 339 MB/s | 1100 MB/s | 3.2x |
| grapheme | 280 MB/s | 1117 MB/s | 4.0x | 287 MB/s | 839 MB/s | 2.9x |

Native on aarch64:

| Corpus | Mode | Before | After |
|---|---|---|---|
| ascii | encode | 40.6 ms | 24.8 ms |
| ascii | decode | 51.2 ms | 49.2 ms |
| utf8 | encode | 45.2 ms | 42.4 ms |
| utf8 | decode | 59.8 ms | 59.5 ms |

**AI usage:** Fable did everything here except write this PR and the
comments. It also wrote the commit messages in this case. I reviewed
everything.
2026-08-15 14:22:24 -07:00
Mitchell Hashimoto
433b16bcb1 terminal/apc: limit glyf decode allocations
Limit individual allocations made while decoding registered glyf
outlines to 64 KB.

Carefully crafted glyf outlines could expand into ~768KB of memory per
glossary entry, which adds up to hundreds of MB per terminal surface.
Across many terminals this could cause issues.

The 64KB number was chosen by inspecting every glyph across Apple
symbols and Noto emoji and the largest single glyph found was 40KB. So,
64KB is generous while limiting each terminal to ~68MB of RAM for max
glyph glossaries.
2026-08-15 14:18:24 -07:00
Mitchell Hashimoto
eb09bf8291 terminal/snapshot: interleave software CRC32C streams
Slicing tables removed the byte-at-a-time dependency chain, but each
16-byte fold still depends serially on the previous one, leaving the
software CRC latency-bound at roughly 2.5-3 GB/s in V8 while snapshot
payloads run through it once per direction. wasm has no carry-less
multiply, so wider tables are the only classic escape — and measuring
slicing-by-32 against interleaving showed the extra 16 KB of tables buys
nothing once the chain is hidden.

Instead, inputs of 4 KiB and up split into thirds processed as three
independent fold chains in one loop, then merge with the GF(2) zero-shift
operator: crc(A ++ B, s) = crc(B, 0) XOR zeroShift(crc(A, s), |B|). The
shift matrices are comptime, storing only even powers of two (an odd
power applies the preceding matrix twice), 4 KB total. Software CRC
throughput roughly doubles; hardware backends are untouched, so native
is unaffected (tables below are noise).

Benchmarks ("prev" is the parent commit):

| wasm      | encode prev | encode   | decode prev | decode   |
|-----------|------------:|---------:|------------:|---------:|
| ascii     |     2.12 ms |  1.73 ms |     2.59 ms |  2.17 ms |
| styled    |     2.23 ms |  1.47 ms |     6.43 ms |  5.38 ms |
| truecolor |     3.44 ms |  2.30 ms |     8.35 ms |  7.17 ms |
| cjk       |     5.99 ms |  3.89 ms |    11.39 ms |  9.50 ms |
| grapheme  |     8.33 ms |  6.94 ms |    10.89 ms |  9.24 ms |

| native | mode   | prev    | this    |
|--------|--------|--------:|--------:|
| ascii  | encode | 24.7 ms | 24.8 ms |
| ascii  | decode | 47.5 ms | 49.2 ms |
| utf8   | encode | 41.9 ms | 42.4 ms |
| utf8   | decode | 58.8 ms | 59.5 ms |
2026-08-15 14:08:19 -07:00
Mitchell Hashimoto
1359973aef terminal/snapshot: single-pass style entry codec
Style entries went through roughly five writer or reader vtable calls
each: encode wrote three 4-byte colors and two u16s separately, and
decode read 16 bytes into a stack buffer only to re-parse it through a
nested fixed reader, one small read per field. Style-heavy pages carry
hundreds of entries per page, so encode now assembles each entry in a
16-byte buffer with a single write, and decode parses the fixed-size
entry directly from a byte array. Entries additionally parse straight
from the buffered payload (ID and value together) when it is contiguous.

Inserting a decoded style also hashed twice: an explicit `lookup` before
`add`, even though `add` already returns the existing entry for repeated
values. Insert with `add` alone, taking one reference per accepted table
entry, and surrender those references through the encoded-ID remap after
grid decoding, the same scheme hyperlink entries already use. Refcount
outcomes are identical: each distinct style nets its cell references.

Benchmarks ("prev" is the parent commit):

| wasm      | encode prev | encode   | decode prev | decode   |
|-----------|------------:|---------:|------------:|---------:|
| ascii     |     2.13 ms |  2.12 ms |     2.54 ms |  2.59 ms |
| styled    |     2.29 ms |  2.23 ms |     6.75 ms |  6.43 ms |
| truecolor |     4.95 ms |  3.44 ms |    12.05 ms |  8.35 ms |
| cjk       |     6.23 ms |  5.99 ms |    11.39 ms | 11.39 ms |
| grapheme  |     8.72 ms |  8.33 ms |    11.27 ms | 10.89 ms |

| native | mode   | prev    | this    |
|--------|--------|--------:|--------:|
| ascii  | encode | 24.4 ms | 24.7 ms |
| ascii  | decode | 45.1 ms | 47.5 ms |
| utf8   | encode | 41.9 ms | 41.9 ms |
| utf8   | decode | 58.6 ms | 58.8 ms |
2026-08-15 14:04:05 -07:00
Mitchell Hashimoto
47a5182621 terminal/snapshot: skip remap tables for pages without styles
Decoding a page allocated and zeroed two full remap tables (a 128 KB
entries array plus an 8 KB seen bitmap each for styles and hyperlinks)
even when the page declared no table entries at all, which is every page
of plain scrollback. Empty tables now use a shared `.empty` remap that
allocates nothing; `get` reads it as all-unmapped through a length check.
Pages that do declare entries are unchanged.

(Leaving the entries array unzeroed behind a seen-bitmap-gated `get` was
also tried and measured no better than the plain memset, so the table
keeps its simple zero-means-unmapped representation.)

Benchmarks ("prev" is the parent commit):

| wasm      | encode prev | encode   | decode prev | decode   |
|-----------|------------:|---------:|------------:|---------:|
| ascii     |     2.06 ms |  2.13 ms |     2.70 ms |  2.54 ms |
| styled    |     2.41 ms |  2.29 ms |     6.56 ms |  6.75 ms |
| truecolor |     5.02 ms |  4.95 ms |    11.86 ms | 12.05 ms |
| cjk       |     6.03 ms |  6.23 ms |    11.60 ms | 11.39 ms |
| grapheme  |     8.36 ms |  8.72 ms |    11.39 ms | 11.27 ms |

| native | mode   | prev    | this    |
|--------|--------|--------:|--------:|
| ascii  | encode | 25.6 ms | 24.4 ms |
| ascii  | decode | 48.1 ms | 45.1 ms |
| utf8   | encode | 41.7 ms | 41.9 ms |
| utf8   | decode | 58.5 ms | 58.6 ms |
2026-08-15 14:04:05 -07:00
Mitchell Hashimoto
7c1014ef66 terminal/snapshot: resolve wide pairs in a per-row pass
Cell decoding ran wide-pair normalization inline for every decoded cell:
two neighbor loads and a switch per cell, even though the overwhelming
majority of rows contain no wide cells at all. Normalization is defined
against already-stored predecessors, so running it as an ordered pass
over the stored row afterward is exactly equivalent to interleaving it.

The word-cell decoders now accumulate the bitwise OR of the row's wire
words as they apply cells, and the pass is gated on it: rows without
wide bits are already normalized (every cell narrow), and width-four and
narrower transports cannot encode wide bits at all, so their rows skip
the check at comptime. That removes the per-cell neighbor traffic from
all styled text, which decodes through the four-byte width.

Benchmarks ("prev" is the parent commit):

| wasm      | encode prev | encode   | decode prev | decode   |
|-----------|------------:|---------:|------------:|---------:|
| ascii     |     2.18 ms |  2.06 ms |     2.68 ms |  2.70 ms |
| styled    |     2.28 ms |  2.41 ms |     6.93 ms |  6.56 ms |
| truecolor |     4.93 ms |  5.02 ms |    11.91 ms | 11.86 ms |
| cjk       |     6.11 ms |  6.03 ms |    11.72 ms | 11.60 ms |
| grapheme  |     8.28 ms |  8.36 ms |    11.38 ms | 11.39 ms |

| native | mode   | prev    | this    |
|--------|--------|--------:|--------:|
| ascii  | encode | 24.3 ms | 25.6 ms |
| ascii  | decode | 46.7 ms | 48.1 ms |
| utf8   | encode | 41.0 ms | 41.7 ms |
| utf8   | decode | 59.5 ms | 58.5 ms |
2026-08-15 14:04:05 -07:00
Mitchell Hashimoto
2aaad3ca99 terminal/snapshot: vectorize grid cell decoding
Decoding one- and two-byte cells widened them to their 8-byte words one
scalar store at a time. The bulk codec now widens sixteen transported
bytes per step with byte shuffles against a zero vector, degrading
width-two surrogate lanes to U+FFFD with a vector select, exactly
matching the scalar path. Short row tails reprocess the final full
window with overlapping stores that rewrite identical bytes.

This is a native win: the shuffles lower to NEON and take ascii decode
from 38.9 ms to 34.0 ms (measured by toggling this path at the tip of
this series). On wasm, V8 runs the scalar fallback at the same speed as
the shuffle version — the loop is store-bound either way — so the wasm
deltas below are flat.

Row decoding also drops per-field packed-struct read-modify-writes in
favor of one load and one store per row header, and rows that are fully
default (zero header byte, zero encoded cells) skip all work: decoded
pages start zeroed, which is exactly the default row and cell state.

Benchmarks ("prev" is the parent commit):

| wasm      | encode prev | encode   | decode prev | decode   |
|-----------|------------:|---------:|------------:|---------:|
| ascii     |     2.07 ms |  2.18 ms |     2.69 ms |  2.68 ms |
| styled    |     2.29 ms |  2.28 ms |     7.06 ms |  6.93 ms |
| truecolor |     4.91 ms |  4.93 ms |    11.99 ms | 11.91 ms |
| cjk       |     6.14 ms |  6.11 ms |    11.83 ms | 11.72 ms |
| grapheme  |     8.26 ms |  8.28 ms |    11.18 ms | 11.38 ms |

| native | mode   | prev    | this    |
|--------|--------|--------:|--------:|
| ascii  | encode | 24.3 ms | 24.3 ms |
| ascii  | decode | 50.9 ms | 46.7 ms |
| utf8   | encode | 41.2 ms | 41.0 ms |
| utf8   | decode | 59.5 ms | 59.5 ms |
2026-08-15 14:04:05 -07:00
Mitchell Hashimoto
593762cfa1 terminal/snapshot: batch grapheme suffix codec
Grapheme suffix encoding made two full passes over the grid (a counting
pass, then an emit pass) and issued three writer calls per entry plus one
per codepoint. Every grapheme cell owns exactly one entry in the page's
grapheme map, so the section count now comes straight from
page.graphemeCount() with no counting pass, and entries are batched
through a 4 KB buffer with one writer call per flush. The per-entry size
check moved into the emit loop; an error still cancels the whole record
before any of it is emitted, so error behavior is unchanged.

Decoding similarly parsed entry headers and codepoints with one reader
call per integer. Fully buffered payloads (the common case after the
borrowed-payload commit) now parse entry headers and codepoint runs
directly from the buffered bytes, and entries whose target cell cannot
carry a suffix discard their codepoints in bulk.

Benchmarks ("prev" is the parent commit):

| wasm      | encode prev | encode   | decode prev | decode   |
|-----------|------------:|---------:|------------:|---------:|
| ascii     |     2.09 ms |  2.07 ms |     2.67 ms |  2.69 ms |
| styled    |     2.23 ms |  2.29 ms |     7.00 ms |  7.06 ms |
| truecolor |     4.95 ms |  4.91 ms |    11.85 ms | 11.99 ms |
| cjk       |     6.03 ms |  6.14 ms |    11.67 ms | 11.83 ms |
| grapheme  |    13.08 ms |  8.26 ms |    13.28 ms | 11.18 ms |

| native | mode   | prev    | this    |
|--------|--------|--------:|--------:|
| ascii  | encode | 24.5 ms | 24.3 ms |
| ascii  | decode | 53.4 ms | 50.9 ms |
| utf8   | encode | 47.6 ms | 41.2 ms |
| utf8   | decode | 60.9 ms | 59.5 ms |
2026-08-15 14:04:05 -07:00
Mitchell Hashimoto
973f619a23 terminal/snapshot: vectorize grid row encoding
Row encoding previously made two scalar passes over every row (a backward
scan for the encoded cell count and a validation pass accumulating the
width-selection OR), then wrote a 3-byte header and per-width chunked
cells through separate writer calls.

Three changes, all bulk-codec only with the portable path unchanged:

  - scanRow computes the count and word-OR in @Vector(4, u64) strides.
    Trailing default cells are all-zero words, so the OR over the whole
    row equals the OR over the encoded prefix.
  - The per-cell wide-pair validation loop is skipped entirely when the
    OR carries no wide bits, which is every row of plain text.
  - Rows are emitted with a single reservation in the destination's spare
    buffer capacity (header plus cells, no writer calls), using explicit
    i8x16.shuffle truncation for the 1/2/4-byte cell widths. Zig 0.16
    disables loop auto-vectorization, so the previous "vectorizable"
    truncating loop was actually scalar. Destinations without buffered
    capacity (counting writers, a still-growing scratch) fall through to
    the streaming path.

Benchmarks ("prev" is the parent commit):

| wasm      | encode prev | encode   | decode prev | decode   |
|-----------|------------:|---------:|------------:|---------:|
| ascii     |     3.50 ms |  2.09 ms |     2.68 ms |  2.67 ms |
| styled    |     3.15 ms |  2.23 ms |     7.04 ms |  7.00 ms |
| truecolor |     5.27 ms |  4.95 ms |    12.04 ms | 11.85 ms |
| cjk       |     5.92 ms |  6.03 ms |    11.89 ms | 11.67 ms |
| grapheme  |    14.33 ms | 13.08 ms |    13.14 ms | 13.28 ms |

| native | mode   | prev    | this    |
|--------|--------|--------:|--------:|
| ascii  | encode | 41.8 ms | 24.5 ms |
| ascii  | decode | 52.4 ms | 53.4 ms |
| utf8   | encode | 47.4 ms | 47.6 ms |
| utf8   | decode | 61.2 ms | 60.9 ms |
2026-08-15 14:04:05 -07:00
Mitchell Hashimoto
c1a61fddda terminal/snapshot: borrow fully buffered record payloads
When the record source already has the complete payload buffered — always
the case for in-memory snapshots such as ghostty_snapshot_decoder_new_buf
— the record reader now borrows the payload straight out of the source
buffer instead of streaming it through the limited and hashing reader
adapters. Payload decoders parse a fixed reader over the borrowed bytes,
`finish` validates the CRC with a single bulk update, and the source
advances only after validation.

The page decoder takes a matching fast path: a fully buffered payload is
parsed in place, skipping the staging allocation and copy it previously
made per PAGE record. Streaming sources are unchanged.

This is a modest win on its own; it is also the foundation for later
commits whose buffered fast paths rely on the payload being contiguous.

Benchmarks (see the first commit in this series for methodology; "prev"
is the parent commit):

| wasm      | encode prev | encode   | decode prev | decode   |
|-----------|------------:|---------:|------------:|---------:|
| ascii     |     3.43 ms |  3.50 ms |     2.81 ms |  2.68 ms |
| styled    |     3.14 ms |  3.15 ms |     7.12 ms |  7.04 ms |
| truecolor |     5.33 ms |  5.27 ms |    12.11 ms | 12.04 ms |
| cjk       |     5.95 ms |  5.92 ms |    12.04 ms | 11.89 ms |
| grapheme  |    14.18 ms | 14.33 ms |    13.16 ms | 13.14 ms |

| native | mode   | prev    | this    |
|--------|--------|--------:|--------:|
| ascii  | encode | 41.7 ms | 41.8 ms |
| ascii  | decode | 53.2 ms | 52.4 ms |
| utf8   | encode | 47.2 ms | 47.4 ms |
| utf8   | decode | 61.6 ms | 61.2 ms |
2026-08-15 14:04:05 -07:00
Mitchell Hashimoto
36d8e3f777 terminal/snapshot: slicing-by-16 software CRC32C
The software CRC32C fallback (WebAssembly and any other target without a
dedicated instruction) was the std byte-at-a-time table walk, which
profiled at ~65% of snapshot encode and ~70% of decode self-time in V8.
Replace it with slicing-by-16: sixteen bytes fold per iteration through
comptime per-position tables, so the serial dependency advances one block
at a time instead of one byte.

The hardware backends (aarch64 CRC, x86_64 SSE4.2) are unchanged, so
native is expected to be unaffected; its deltas below are run-to-run
noise.

Benchmarks: wasm is V8 (node 25), ReleaseFast + wasm-opt -O3, 80x24
terminal, 2 MiB VT corpus per workload, complete snapshot including
scrollback, best-of-5. Native is aarch64 macOS, hyperfine mean,
ghostty-bench +terminal-snapshot --loops=20. "base" is the parent commit.

| wasm      | encode base | encode   | decode base | decode   |
|-----------|------------:|---------:|------------:|---------:|
| ascii     |     7.07 ms |  3.43 ms |     6.45 ms |  2.81 ms |
| styled    |    10.54 ms |  3.14 ms |    14.37 ms |  7.12 ms |
| truecolor |    15.23 ms |  5.33 ms |    21.76 ms | 12.11 ms |
| cjk       |    25.41 ms |  5.95 ms |    30.83 ms | 12.04 ms |
| grapheme  |    27.67 ms | 14.18 ms |    27.00 ms | 13.16 ms |

| native | mode   | base    | this    |
|--------|--------|--------:|--------:|
| ascii  | encode | 40.6 ms | 41.7 ms |
| ascii  | decode | 51.2 ms | 53.2 ms |
| utf8   | encode | 45.2 ms | 47.2 ms |
| utf8   | decode | 59.8 ms | 61.6 ms |
2026-08-15 14:04:05 -07:00
Jon Parise
69b9abf09e cli: version SSH terminfo cache entries
Derive a version from the encoded Ghostty terminfo and require callers
to pass it explicitly when reading or writing the SSH cache. Cache
entries created for older or different payloads no longer suppress a
required installation.
2026-08-15 12:18:56 -04:00
Sl (Shahaf Levi)
06b63c4859 i18n: update Hebrew translations for v1.4 2026-08-15 19:13:27 +03:00
Jon Parise
fff9f62f38 cli: always install embedded SSH terminfo
Remove the remote infocmp short-circuit from +ssh setup. The command
already sends Ghostty's embedded terminfo source to tic, so accepting
any existing entry leaves the payload unused and can preserve stale
data.
2026-08-15 12:12:46 -04:00
ghostty-vouch[bot]
cecf81678e Update VOUCHED list (#13843)
Triggered by [discussion
comment](https://github.com/ghostty-org/ghostty/discussions/13841#discussioncomment-18031969)
from @jcollie.

Vouch: @preiter93

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-15 16:07:28 +00:00
Mitchell Hashimoto
e3939d0f62 terminal: replace std.fmt.parseFloat with custom fraction parsing (#13839)
We have exactly two callers of `parseFloat` and they both have very
limited expect input shapes: values 0-1, simple decimals. parseFloat
brings in ~26KB of binary size, so replace it with a custom parser for
our exact shape.
2026-08-15 06:59:15 -07:00
Mitchell Hashimoto
946422dbc5 terminal: replace std.fmt.parseFloat with custom fraction parsing
We have exactly two callers of `parseFloat` and they both have very
limited expect input shapes: values 0-1, simple decimals. parseFloat
brings in ~26KB of binary size, so replace it with a custom parser for
our exact shape.
2026-08-15 06:51:18 -07:00
trag1c
b5aa8e7a07 i18n: complete Kazakh (kk) translation for v1.4 (#13778)
Translates the 180 remaining strings, mainly command palette entries
introduced for v1.4 localization.
2026-08-15 10:57:20 +02:00
Mitchell Hashimoto
794515ba60 libghostty: -Dvt-features to compile out unused features (#13834)
This introduces a `-Dvt-features` build option for libghostty-vt that
compiles out optional feature areas, primarily so size-conscious
embedders (e.g. wasm) can significantly trim the binary.

The flag is similar to `-Dcpu`, `+feature` or `feature` to enable it,
`-feature` to disable, magic word `all` to turn all features on or off.
Example: `-Dvt-features=-all,+render-state` builds only the render state
API.

Added CI to verify the lib and tests _compile_ (we don't run it) for
each individual feature.

### Sizes

wasm32, ReleaseFast:

| Build | Bytes | Brotli |
|---|---|---|
| default (all features) | 876,500 | 218,309 |
| web interactive
(`-all,+render-state,+input-encode,+selection,+color,+grid-introspection`)
| 661,119 | 168,994 |
| read-only viewer (`-all,+render-state`) | 537,441 | 132,858 | 
| bare VT core (`-all`) | 515,422 | 125,756 |
| xterm.js browser bundle (incl. renderers) | 488,663 | 99,311 | 
| @xterm/headless | 182,672 | 39,651 |

Note: xterm versions are stable as of this commit.

### C Header Note

I didn't do a `vt/features.h` style header that has macros to guard
symbols for the various features. This is something we should do in the
future. The way it is now, the C header always declares everything, and
its not a problem unless an unavailable function is referenced at link
time.
2026-08-14 22:35:38 -07:00
Mitchell Hashimoto
1fdbb8c912 libghostty: -Dvt-features to compile out unused features
This introduces a `-Dvt-features` build option for libghostty-vt that
compiles out optional feature areas, primarily so size-conscious
embedders (e.g. wasm) can significantly trim the binary.

The flag is similar to `-Dcpu`, `+feature` or `feature` to enable it,
`-feature` to disable, magic word `all` to turn all features on or off.
Example: `-Dvt-features=-all,+render-state` builds only the render
state API.

### Sizes

| Build | Bytes | Brotli |
|---|---|---|
| default (all features) | 876,500 | 218,309 |
| web interactive (`-all,+render-state,+input-encode,+selection,+color,+grid-introspection`) | 661,119 | 168,994 |
| read-only viewer (`-all,+render-state`) | 537,441 | 132,858 |
| bare VT core (`-all`) | 515,422 | 125,756 |
| xterm.js browser bundle (incl. renderers) | 488,663 | 99,311 |
| @xterm/headless | 182,672 | 39,651 |

Note: xterm versions are stable as of this commit.
2026-08-14 22:07:54 -07:00
ghostty-vouch[bot]
348f714ff9 Update VOUCHED list (#13833)
Triggered by [discussion
comment](https://github.com/ghostty-org/ghostty/discussions/13831#discussioncomment-18025282)
from @jcollie.

Vouch: @DiegoArmstrong

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-15 04:55:55 +00:00
Mitchell Hashimoto
0e0893adff libghostty: attacking wasm binary size (#13830)
This should release our ReleaseFast bundle from 1.1MB to ~800KB. 

The major win is disabling logging in ReleaseFast wasm builds (~200KB).

The next is removing aggressive inlining in paths that don't make sense
for performance. Verified with benchmarks on native to not affect
anything really.

The third was really dumb: `var buf: [4096]u32 = @splat(c)` in LLVM
releasefast for wasm was lowering to 4096 separate `i32.store`...
like... 30KB of code. Replacing this with a for loop reduced by 30KB and
made REP (a rare sequence) 11x faster lol.
2026-08-14 21:02:41 -07:00
ghostty-vouch[bot]
e84dd30155 Update VOUCHED list (#13829)
Triggered by [discussion
comment](https://github.com/ghostty-org/ghostty/discussions/13828#discussioncomment-18024891)
from @jcollie.

Vouch: @diego-moment

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-15 03:47:57 +00:00
Mitchell Hashimoto
51a4311ef1 terminal: clean up overzealous inlining 2026-08-14 20:44:25 -07:00
Mitchell Hashimoto
d61920d80e lib-vt: disable logging in wasm release builds 2026-08-14 20:26:06 -07:00
Mitchell Hashimoto
d760ee96e5 terminal: much faster wide-character reflow on resize (#13827)
Resizing a terminal whose buffer is heavy with wide characters (CJK,
emoji) is now 3-5x faster on the reflow path.

This was relatively simple work. We already have a bulk fast path for
same-style cells. We previously omitted ANY wide characters from this.
We relaxed this by making it work with complete wide pairs (wide
followed by spacer tail).

### Benchmarks

Resize dance (13 column resizes, 80 -> 40 -> 132 and back and forth
again) over a ~2,000-row scrollback buffer.

| Workload | Before | After | Speedup | 
|---|---|---|---|
| cjk | 0.938 | 0.296 | 3.2x | 
| emoji | 0.875 | 0.191 | 4.6x |
| mixed build-log | 0.306 | 0.155 | 2.0x |
| grapheme | 2.449 | 1.831 | 1.3x | 
| ascii-short | 0.115 | 0.117 | 1.0x |
| ascii-long | 0.109 | 0.112 | 1.0x |
| latin | 0.093 | 0.095 | 1.0x | 
| sgr-truecolor | 0.939 | 0.965 | 1.0x |

**AI usage:** Profiled, implemented, benchmarked, and written by Fable.
Plan validated by me before doing it, I wrote all the
comments/commits/blah.
2026-08-14 14:22:05 -07:00
Mitchell Hashimoto
88ed6bebf4 libghostty: much faster wide-character reflow on resize
Resizing a terminal whose buffer is heavy with wide characters (CJK,
emoji) is now 3-5x faster on the reflow path. 

This was relatively simple work. We already have a bulk fast path for
same-style cells. We previously omitted ANY wide characters from this.
We relaxed this by making it work with complete wide pairs (wide followed
by spacer tail).

### Benchmarks

Resize dance (13 column resizes, 80 -> 40 -> 132 and back and forth again) 
over a ~2,000-row scrollback buffer.

| Workload | Before | After | Speedup | 
|---|---|---|---|
| cjk | 0.938 | 0.296 | 3.2x | 
| emoji | 0.875 | 0.191 | 4.6x |
| mixed build-log | 0.306 | 0.155 | 2.0x |
| grapheme | 2.449 | 1.831 | 1.3x | 
| ascii-short | 0.115 | 0.117 | 1.0x |
| ascii-long | 0.109 | 0.112 | 1.0x |
| latin | 0.093 | 0.095 | 1.0x | 
| sgr-truecolor | 0.939 | 0.965 | 1.0x |

**AI usage:** Profiled, implemented, benchmarked, and written by Fable.
Plan validated by me before doing it, I wrote all the
comments/commits/blah.
2026-08-14 14:05:20 -07:00
Mitchell Hashimoto
6b22215c5d libghostty: much faster grapheme-heavy IO throughput (#13826)
Processing grapheme-heavy input (ZWJ sequences, emoji modifiers, flags,
combining marks) through is now almost 3x faster.

### Primary Change: PageList Capacity Projection

This workload was heavily bound by `PageList.increaseCapacity` because
pathological cases of single-dimensional growth cause repeated page
capacity doublings which get increasingly expensive because each time we
do a full allocation + clone.

So the major change is that for grapheme bytes in particular, when we
reach a capacity limit, we take the current usage for the current set of
rows and project it out to the remaining capacity of rows. Basically, we
assume that a similar workload will continue. So rather than doubling,
we're _guessing_ how much you're going to need.

In the real world, I'm not really sure if this matters at all. There are
no regressions on any regular corpus streams (asciinema, wikipedia
dumps, etc.).

### Other Changes

There are some other changes here, all found on the path to improving
grapheme IO throughput:

* The bitmap allocator now maintains `search_start` hint we update on
every allocation so that future free-scans are much faster. This is the
lowest possible place we don't have a full bitmap.

* For wasm32, we use an alternate hashing structure for small keys since
Wyhash's 64bit * 64bit multiplication is very very slow because wasm has
no widening instruction.

* Terminal `printSlice` now checks the fast path compatibility once up
front rather than on every fast-path attempt.

### Benchmarks

Data: ZWJ family/profession sequences, skin-tone modifiers, flags, and
combining marks streamed in 64 KiB chunks into an 80x24 terminal,
default modes.

| Benchmark | Before | After | Speedup |
|---|---|---|---|
| wasm, V8, 16 MiB stream | 52 MB/s | 151 MB/s | 2.9x | 
| native, terminal-stream, 64 MiB | 894 ms | 305 ms | 2.9x |

Sorry the native stuff is in ms, that's how our native `ghostty-bench`
does things versus the custom little V8 harness.

**AI usage:** Developed alongside Fable: profiling, implementation, and
benchmarks. All human language messages written myself. Validated
myself.
2026-08-14 13:18:18 -07:00
Mitchell Hashimoto
3d9b2b483c libghostty: much faster grapheme-heavy IO throughput
Processing grapheme-heavy input (ZWJ sequences, emoji modifiers, flags,
combining marks) through is now almost 3x faster.

### Primary Change: PageList Capacity Projection

This workload was heavily bound by `PageList.increaseCapacity` because
pathological cases of single-dimensional growth cause repeated page
capacity doublings which get increasingly expensive because each time we
do a full allocation + clone.

So the major change is that for grapheme bytes in particular, when we
reach a capacity limit, we take the current usage for the current set of
rows and project it out to the remaining capacity of rows. Basically, we
assume that a similar workload will continue. So rather than doubling,
we're _guessing_ how much you're going to need.

In the real world, I'm not really sure if this matters at all. There are
no regressions on any regular corpus streams (asciinema, wikipedia dumps, etc.).

### Other Changes

There are some other changes here, all found on the path to improving
grapheme IO throughput:

* The bitmap allocator now maintains `search_start` hint we update on
  every allocation so that future free-scans are much faster. This is
  the lowest possible place we don't have a full bitmap.

* For wasm32, we use an alternate hashing structure for small keys
  since Wyhash's 64bit * 64bit multiplication is very very slow because
  wasm has no widening instruction.

* Terminal `printSlice` now checks the fast path compatibility once up front
  rather than on every fast-path attempt.

### Benchmarks

Data: ZWJ family/profession sequences, skin-tone modifiers,
flags, and combining marks streamed in 64 KiB chunks into an 80x24
terminal, default modes.

| Benchmark | Before | After | Speedup |
|---|---|---|---|
| wasm, V8, 16 MiB stream | 52 MB/s | 151 MB/s | 2.9x |
| native, terminal-stream, 64 MiB | 894 ms | 305 ms | 2.9x |

Sorry the native stuff is in ms, that's how our native `ghostty-bench`
does things versus the custom little V8 harness.

**AI usage:** Developed alongside Fable: profiling, implementation, and
benchmarks. All human language messages written myself. Validated myself.
2026-08-14 13:10:21 -07:00
Mitchell Hashimoto
16833f5e5f libghostty: faster render state reads and updates on wasm targets (#13825)
This makes the `ghostty_render_state_*` C API significantly faster on
wasm32-freestanding, measured in V8 via Node for Chrome. Also verified
in `jsc` for Safari.

The major change is a new bulk row read API that makes full-screen cell
reads roughly 10x faster for wasm embedders. This should help any
embedder with high FFI overhead, such as Go, Python, etc. too.

Non-wasm performance is not impacted, all benchmarks were run on my mac
too w/ no regressions (two of the changes are native wins as well).

## Changes

* color: the "vectorized" palette conversion loop was silently
scalarized by LLVM into per-byte ops because it loaded/stored through
array-typed pointers. Zig 0.16 disables the LLVM loop vectorizer, so
manually vectorized loops must go through vector-typed pointers.
* C styles: major optimizations to converting Zig styles to C styles.
This is a heavy operation for render state.
* render: `endUpdate`'s style-run fill (`@memset` with a struct value)
re-loaded its source every iteration and stored field by field. Now
manually vectorized.
* render: new `GHOSTTY_RENDER_STATE_ROW_DATA_CELLS_RAW` returns a
borrowed `GhosttyCellsView` of the current row's raw cell values, valid
until the next update. One call per row instead of 3-6 calls per cell.

## Benchmarks

| Benchmark | Before | After | Speedup |
|---|---|---|---|
| colors_get | 114 ns | 35 ns | 3.3x |
| style get, per styled cell | 7.8 ns | 6.7 ns | 1.2x | 
| raw+style read, per cell | 8.6 ns | 7.7 ns | 1.1x |
 | full-screen text read, per cell | 7.5 ns | 0.7 ns | 10.7x |
 | full-screen text+style read, per cell | 8.6 ns | 1.7 ns | 5.1x | 
| render state update, styled full frame | 3.4 us | 2.6 us | 1.3x |

**AI usage:** Fable did the implementation and benchmarking and drafted
this message. Comments were partially rewritten by me.
2026-08-14 11:53:04 -07:00
Mitchell Hashimoto
74a233b543 libghostty: faster render state reads and updates on wasm targets
This makes the `ghostty_render_state_*` C API significantly faster on
wasm32-freestanding, measured in V8 via Node for Chrome. Also verified
in `jsc` for Safari.

The major change is a new bulk row read API that makes full-screen cell reads
roughly 10x faster for wasm embedders. This should help any embedder with
high FFI overhead, such as Go, Python, etc. too.

Non-wasm performance is not impacted, all benchmarks were run on my mac
too w/ no regressions (two of the changes are native wins as well).

## Changes

* color: the "vectorized" palette conversion loop was silently
  scalarized by LLVM into per-byte ops because it loaded/stored through
  array-typed pointers. Zig 0.16 disables the LLVM loop vectorizer, so
  manually vectorized loops must go through vector-typed pointers.
* C styles: major optimizations to converting Zig styles to C styles.
  This is a heavy operation for render state.
* render: `endUpdate`'s style-run fill (`@memset` with a struct value)
  re-loaded its source every iteration and stored field by field. Now
  manually vectorized.
* render: new `GHOSTTY_RENDER_STATE_ROW_DATA_CELLS_RAW` returns a
  borrowed `GhosttyCellsView` of the current row's raw cell values, valid
  until the next update. One call per row instead of 3-6 calls per cell.

## Benchmarks

| Benchmark | Before | After | Speedup |
|---|---|---|---|
| colors_get | 114 ns | 35 ns | 3.3x |
| style get, per styled cell | 7.8 ns | 6.7 ns | 1.2x |
| raw+style read, per cell | 8.6 ns | 7.7 ns | 1.1x |
| full-screen text read, per cell | 7.5 ns | 0.7 ns | 10.7x |
| full-screen text+style read, per cell | 8.6 ns | 1.7 ns | 5.1x |
| render state update, styled full frame | 3.4 us | 2.6 us | 1.3x |

**AI usage:** Fable did the implementation and benchmarking and drafted
this message. Comments were partially rewritten by me.
2026-08-14 11:40:01 -07:00
Mitchell Hashimoto
8f485a7f47 ci: publish wasm tip artifacts (#13822)
This builds and publishes `ghostty-vt.wasm` binaries into our tip GitHub
releases. These are built with the proper optimization, `simd128` CPU
feature set, and run through `wasm-opt`.

This allows wasm consumers to use libghostty without a Zig toolkit.

Published two: `ghostty-vt.wasm` and `ghostty-vt-small.wasm`. The latter
is ReleaseSmall, but is 10 to 20% slower. Users choice.
2026-08-14 11:00:13 -07:00
Mitchell Hashimoto
29a70bc367 ci: publish wasm tip artifacts
This builds and publishes `ghostty-vt.wasm` binaries into our tip
GitHub releases. These are built with the proper optimization, `simd128`
CPU feature set, and run through `wasm-opt`.

This allows wasm consumers to use libghostty without a Zig toolkit.

Published two: `ghostty-vt.wasm` and `ghostty-vt-small.wasm`. The latter
is ReleaseSmall, but is 10 to 20% slower. Users choice.
2026-08-14 10:47:22 -07:00
Mitchell Hashimoto
2f72b041f6 libghostty: much faster vt_write on wasm targets (#13821)
This makes `ghostty_terminal_vt_write` on wasm32-freestanding anywhere
from 1.4x to 13x faster depending on the input, measured in V8 via Node
for Chrome as well as `jsc` for Safari.

This changes the default Wasm build to default to enabling the `simd128`
CPU feature because baseline doesn't have that and every major browser
has supported it for years. This results in massive performance
improvements (like, 50%+ on all streams).

Non-wasm performance is not impacted, all benchmarks were run on my mac
too w/ no regressions.

## Changes

* stream: the batched parse path (bulk UTF-8 decode, print_slice runs)
is used even when `build_options.simd` is false. The per-byte loop is
now debug-only.
* simd/vt: the scalar `utf8DecodeUntilControlSeq` gets a vectorized
ASCII bulk path that is compatible with wasm simd128.
* style: on wasm, `Style.eql` compares canonical `PackedStyle` forms
which is faster by like 11%. On native its slower so we only do this for
wasm.
* build: wasm targets now default to the `simd128` CPU feature since
every browser engine has supported it for years. Opt out with
`-Dcpu=generic`.
* PACKAGING.md documents the wasm build, including `wasm-opt` notes.

## Benchmarks

| Workload | Before | After | Speedup |
|---|---|---|---|
| ascii | 85 MB/s | 1070 MB/s | 12.5x |
| ascii-wrap | 84 MB/s | 1103 MB/s | 13.1x |
| clear-redraw | 85 MB/s | 913 MB/s | 10.7x |
| scroll | 79 MB/s | 304 MB/s | 3.8x |
| cursor | 120 MB/s | 255 MB/s | 2.1x |
| utf8 | 99 MB/s | 169 MB/s | 1.7x |
| sgr16 | 81 MB/s | 133 MB/s | 1.6x |
| sgr-truecolor | 62 MB/s | 88 MB/s | 1.4x |

End result: wasm at roughly 50-85% of the native ReleaseFast+SIMD build
on the same workloads. Plain ASCII was at 6% of native before.

**AI usage:** Lots of Fable help. As always, the human language stuff
like this commit and comments were rewritten by me.
2026-08-14 10:37:58 -07:00
Mitchell Hashimoto
87f69a12ee libghostty: much faster vt_write on wasm targets
This makes `ghostty_terminal_vt_write` on wasm32-freestanding anywhere
from 1.4x to 13x faster depending on the input, measured in V8 via Node
for Chrome as well as `jsc` for Safari.

## Changes

* stream: the batched parse path (bulk UTF-8 decode, print_slice runs)
  is used even when `build_options.simd` is false. The per-byte loop
  is now debug-only.
* simd/vt: the scalar `utf8DecodeUntilControlSeq` gets a vectorized
  ASCII bulk path that is compatible with wasm simd128.
* style: on wasm, `Style.eql` compares canonical `PackedStyle` forms
  which is faster by like 11%. On native its slower so we only do this
  for wasm.
* build: wasm targets now default to the `simd128` CPU feature since
  every browser engine has supported it for years. Opt out with
  `-Dcpu=generic`.
* PACKAGING.md documents the wasm build, including `wasm-opt` notes.

## Benchmarks

| Workload | Before | After | Speedup |
|---|---|---|---|
| ascii | 85 MB/s | 1070 MB/s | 12.5x |
| ascii-wrap | 84 MB/s | 1103 MB/s | 13.1x |
| clear-redraw | 85 MB/s | 913 MB/s | 10.7x |
| scroll | 79 MB/s | 304 MB/s | 3.8x |
| cursor | 120 MB/s | 255 MB/s | 2.1x |
| utf8 | 99 MB/s | 169 MB/s | 1.7x |
| sgr16 | 81 MB/s | 133 MB/s | 1.6x |
| sgr-truecolor | 62 MB/s | 88 MB/s | 1.4x |

End result: wasm at roughly 50-85% of the native ReleaseFast+SIMD build
on the same workloads. Plain ASCII was at 6% of native before.

**AI usage:** Lots of Fable help. As always, the human language stuff
like this commit and comments were rewritten by me.
2026-08-14 10:25:30 -07:00