summaryrefslogtreecommitdiff
path: root/docs/macos.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/macos.md')
-rw-r--r--docs/macos.md176
1 files changed, 142 insertions, 34 deletions
diff --git a/docs/macos.md b/docs/macos.md
index 12e598b5..b800513f 100644
--- a/docs/macos.md
+++ b/docs/macos.md
@@ -44,6 +44,11 @@ the view and drives its own frame clock, and the ABI grows a `platform` pointer
field carrying the `NSView*` — one field, because the rest of the seam does not
change. Nothing here is designed to make that harder.
+That does not rule out a *postprocess*. CoreText is still the renderer and the
+cell ABI is unchanged, but shader effects draw that same frame into a retained
+bitmap and feed it through Core Image kernels on one Metal context.
+There is no second glyph atlas, pane renderer, or view pointer in the ABI.
+
## Why the ABI mirrors src/web.zig
The browser and a Cocoa app are the same host, and the browser proved the shape
@@ -78,6 +83,11 @@ asking for a thin insert caret instead of a block. `Surface.images` crosses
separately as `pardes_frame_images` / `pardes_frame_image_list` — see "Pixel
attachments" below.
+`pardes_scene` is the matching full-window snapshot: a bit each for CRT,
+ripple, and glitch, plus the 60 Hz time/frame that animates them. It is returned
+by value, so the renderer never holds a pointer into live core state. A zero
+flags word is also the fast-path contract: draw the CoreText frame directly.
+
**Input.** `pardes_key` takes a codepoint and the host's already-composed text,
because composition is AppKit's job and the core only ever wants finished
characters. Keys that carry no text are the four ASCII controls (enter, escape,
@@ -123,15 +133,19 @@ because it owns the filesystem side too.
**Lifecycle.** `pardes_init` returns 0 or an opaque nonzero code,
`pardes_deinit` tears down, `pardes_tick` drains pty output plus the effect
queue and returns whether anything changed, `pardes_should_quit` reports the
-Exit builtin or the last pane closing, and `pardes_animating` says a theme
-transition wants ~60 Hz ticks until it settles — the one thing in an otherwise
-event-driven frontend that redraws on a clock, handled in `tty.zig` by sleeping
-the loop and forcing a tick.
+Exit builtin or the last pane closing, and `pardes_animating` says
+`pardes_animation_tick` wants a ~60 Hz call until it settles — the one thing in
+an otherwise event-driven frontend that redraws on a clock. A persistent scene
+effect deliberately keeps that clock armed until its builtin turns it off.
+Ordinary input and pty pumps never count as elapsed animation frames.
The ordering contract is the part a header cannot enforce. **Init with the real
-grid size.** The core defers each shell's greeting until it has seen a resize:
-`sync()` in `src/pardes.zig` only emits the opening `ls` once `resize_count > 0`
-and the pty has produced its first prompt. Setting `Options.cols`/`rows` alone
+grid size.** The core defers each integrated shell's greeting until it has seen
+a resize: `sync()` in `src/pardes.zig` only emits the opening `ls` once
+`resize_count > 0` and parsed OSC 133 B says the prompt has handed the cursor
+to input. An unintegrated shell has no reliable readiness signal, so it skips
+the cosmetic greeting; an explicit command still releases after its successful
+fork. Setting `Options.cols`/`rows` alone
never bumps that counter, so init must turn its arguments into an actual resize
event the way `src/web.zig` does immediately after construction — and the size
must be true, because the first `forkpty` takes its winsize from the core's
@@ -252,12 +266,12 @@ most deliberate twist there is and the one a stale velocity sample would fling
hardest. And a finger back down (`pardes_rotate(0)`) catches a coast in
progress, the way a hand catches a dial.
-The coast itself is spent by `pardes_tick`, one fixed 1/60 step per tick with a
-0.94 decay, and it makes `pardes_animating` true for as long as it lasts — so
-it rides the same 16 ms re-pump a theme transition does and needs no clock of
-its own. Fixed rather than measured on purpose: one fling then spends the same
-travel every time, which is what lets `rotate.snap` assert it instead of
-asserting the machine's timer jitter.
+The coast itself is spent by `pardes_animation_tick`, one fixed 1/60 step per
+scheduled frame with a 0.94 decay, and it makes `pardes_animating` true for as
+long as it lasts — so it rides the same 16 ms re-pump a theme transition does
+and needs no clock of its own. Fixed rather than measured on purpose: one fling
+then spends the same travel every time, which is what lets `rotate.snap` assert
+it instead of asserting the machine's timer jitter.
Both halves are goldens. `rotate.snap` turns the dial at 4° per 100 ms (40°/s,
under the floor) and asserts the screen is byte-identical across the release,
@@ -471,14 +485,18 @@ can disagree. `pardes_font_take` hands it over once, the same take-and-clear
shape as the haptic, and the view loads it with
`CTFontManagerCreateFontDescriptorsFromURL`, picks the untraited cut out of a
collection, derives bold and italic from it, and re-measures. A file it cannot
-wear leaves the screen exactly as it was — a terminal that cannot draw has no
-way back out of itself.
+wear crosses back through `pardes_font_reject` and leaves the old metrics
+exactly as they were. Success crosses through `pardes_font_ack` with the
+effective PostScript name and point size. Requested and effective values are
+therefore distinct, queryable facts rather than a request being mistaken for
+what is on screen.
-Zoom does not touch the core at all. Cmd+=, Cmd- and Cmd+0 change the point size,
-`Metrics` is rebuilt, and the new cell is reported through the same resize path
-a window drag uses; the core reflows to a different number of columns and knows
-nothing about points. Cmd+= rather than Cmd++ because AppKit matches the
-character and `=` is what is under the finger.
+Cmd+=, Cmd- and Cmd+0 change the point size, rebuild `Metrics`, and report the
+new cell through the same resize path a window drag uses. They also observe the
+effective face/point tuple through `pardes_font_observe`, so the single `Config`
+report follows a host-owned zoom without resolving a pending face request. The
+initial system face is observed the same way. Cmd+= rather than Cmd++ because
+AppKit matches the character and `=` is what is under the finger.
Both are machine-checked in `test/macos-snapshots/font.snap`, which needs two
different kinds of assertion because a snapshot is the core's cell buffer and
@@ -551,18 +569,40 @@ Turning this on also changes what an IMAGE pane is here: it was the PETSCII
glyph-art fallback, the same one a terminal without kitty graphics gets, and it
is now the real pixels.
+## Live file reload
+
+`FileWatcher.swift` gives each watched pane a vnode source on the file and one
+on its parent directory. The file catches in-place writes; the parent is
+essential because editors commonly save by renaming a fresh inode over the
+path. After the 45 ms debounce the file source is rearmed on the current inode,
+then the event returns to Zig with the pane's watch generation. Replacing or
+closing a pane advances that generation, so a callback already queued for the
+old occupant cannot reload the new one.
+
+The callback only wakes the ordinary main-thread pump. `pardes_tick` checks the
+exact owned path, filtering unrelated changes in the same directory, duplicate
+vnode events, and Pardes's own saves. Text panes compare and adopt a bounded
+byte snapshot by content hash. PDFs can be much larger than that bound, so they
+compare inode/size/time metadata and let MuPDF reopen the pathname directly.
+That identity is committed only when equal stats bracket a successful
+transactional reopen; a mismatch receives one self-scheduled retry, so it does
+not depend on a second vnode edge and cannot spin forever on a malformed stable
+file. Reading settings survive and derived page data is regenerated. A
+malformed PDF therefore leaves the last good document usable and remains
+retryable after the next real write.
+
## Themes, live
Two bugs lived here, and they were the same bug.
`ChromeTheme` fades between themes over ten 16 ms steps, advanced by a `.tick`
-event. The tty and SDL loops call `core.update(.tick)` on their own clocks;
-this host has no loop of its own, so nothing advanced it — `pardes_tick`
-drained ptys and reported `themeAnimationActive()` back without ever stepping
-the transition. The fade therefore never moved and never ended: every tagline
-kept the *previous* theme's colours until the next launch, and the 16 ms
-re-pump in `AppDelegate.pump` spun at 60 Hz for the rest of the session. The
-pump is the clock, so `pardes_tick` steps it.
+event. The tty and SDL loops call `core.update(.tick)` on their own clocks; the
+native host schedules the same clock explicitly through
+`pardes_animation_tick`. `pardes_tick` only drains work: if every input pump
+also advanced the transition, a burst of key or pty events could collapse a
+ten-frame fade into one display frame. The scheduled callback advances once,
+then pumps effects and redraws, and re-arms itself only while
+`pardes_animating` remains true.
`pardes_theme_bg` is the other half. The window background behind the titlebar
and behind a live resize was a hand-agreed `#121212` in two files; it is now
@@ -571,6 +611,65 @@ chrome's, because document backgrounds switch the instant the theme does while
chrome fades. `window.appearance` follows its luminance, so wearing `acme` no
longer leaves a dark titlebar over a cream grid.
+## Scene effects
+
+`Crt`, `Ripple`, and `Glitch` are three switches over one postprocess, not three
+stacked filters. Their canonical macOS source is `shaders/crt.ci.metal`, which
+the build installs as `pardes.app/Contents/Resources/crt.ci.metal`. The Swift
+shell loads that exact asset with `CIKernel.kernels(withMetalString:)` and runs
+it through a `CIContext` created from the system Metal device. The source is
+also a Zig build import, so `EffectCode` can print what the app executes when
+the source checkout is absent.
+
+The ordinary CoreText/attachment/cursor pass is one function. With any scene
+bit or panel track it targets a retained, backing-scale bitmap; kernels then
+sample the complete frame into the flipped view. PDFs, image panes, taglines,
+rules, and the caret therefore receive the same effect. With every bit off and
+no panel track the bitmap and Core Image context are bypassed entirely. Ripple
+and glitch alter sample coordinates only, preserving theme colors; CRT works
+in linear light with restrained bloom, scan/mask, vignette, hum, and noise
+instead of applying a broad color remap.
+
+The scene kernel alone receives a `clampedToExtent` image and the final output
+is cropped back to the original finite extent. Coordinate tears and chroma
+samples therefore clamp to the edge exactly like SDL's scene sampler instead
+of acquiring transparent-black seams from Core Image's finite source image.
+
+The Zig side owns the clock. `pardes_animation_tick` increments its wrapped
+60 Hz frame only while a scene bit is active, and `pardes_scene` derives seconds
+from that integer. Input bursts cannot accelerate the shader.
+
+Pointer input follows the same destination-to-source transform as the last
+presented scene frame before it is divided by the cell metrics. The view keeps
+that exact `pardes_scene_s` snapshot and mirrors the Metal barrel, ripple and
+glitch sampling arithmetic in `ScenePostprocessor.sourcePoint`; pixels outside
+the CRT tube have no cell. The resulting displayed-grid cell then reaches the
+core, whose panel-track mapping resolves it to canonical pane content.
+
+## Panel transitions
+
+`pardes_frame_panel_track_list` publishes the core's plain `Track` records
+without a host-side animation model: pane serial/id, phase, effect, frame, and
+logical-cell `from`/`to` boxes. The C layout and every field offset are asserted
+against the Zig extern struct on Linux. The exported list is already stable
+paint order—moving panes by slot, then opening panes by slot—so Swift only
+consumes it.
+
+CoreText still renders one canonical complete frame. When tracks exist, the
+same retained bitmap used by scene effects is fed through two kernels in
+`shaders/crt.ci.metal`: `pardesPanelClear` removes every final target first,
+then `pardesPanel` samples each target into its eased presented box. Slide uses
+ease-out cubic, zoom uses ease-out-back, dissolve uses stable pane/cell noise,
+and ASCII materialization replaces not-yet-revealed cells with procedural
+punctuation. Because the input is the finished bitmap rather than a glyph-only
+batch, backgrounds, glyphs, taglines, rules, the caret, PDF pages, and image
+panes move and dissolve together. Scene CRT/ripple/glitch runs once after the
+panel composition.
+
+With no scene bit and no panel track the retained bitmap, Core Image context,
+and Metal passes are bypassed. `EffectCode Panel*` embeds this actual Metal
+file plus its direct Swift owner, the same files the app executes.
+
### Transparent themes
A theme with `bg = null` — the curated `dark`, and every vendored
@@ -794,7 +893,12 @@ vocabulary (`start`, `wait`, `stable`, `text`, `key`, `snap`, `command`,
exists here: `fingers <n>`, `force`, `rotate <degrees> [gap_ms]`, `rotate_end`,
`drop <path> <col> <row>`, `scroll <rows> <col> <row>`,
`haptic <none|exec|look>`, `nsclick` (the AppKit-event path, as opposed to
-`click`'s direct entry-point call), `font <name>`, `zoom` and `clipboard`.
+`click`'s direct entry-point call), `font <name>`, `font-size <points>`, `zoom`,
+`tracks <phase:effect>...`, `draw-effect`, and `clipboard`. The boot script uses
+`tracks` followed by `draw-effect` to assert the moving/opening ABI order and
+that the runtime Metal owner actually accepted the panel frame (a raw fallback
+fails); the font script checks every initial/adopted/zoomed effective point
+size without changing its grid goldens.
The dial's two extras are what
make momentum testable at all: the optional gap is a real sleep before the
event, so a script can say how FAST the dial is being turned, and `rotate_end`
@@ -815,6 +919,11 @@ every pixel comes out identical, which is what keeps `draw(_:)` honest — `snap
reads the core's cell buffer and would be perfectly happy with a `draw` that
returned on its first line.
+The Linux loop proves the new C layout, flag encoding, clock wrap, embedded
+kernel source, header syntax, and static library. Compiling Swift, runtime Metal
+kernel compilation, and comparing processed pixels remain `macos-e2e` work on
+a Darwin host; Linux has neither AppKit nor Apple's Metal runtime.
+
Goldens are hermetic: a fake `$HOME` with a pinned `PS1`, `Shell bash` in the
config (fish's prompt carries a hostname), `LC_ALL=C`, `PARDES_NOTIME=1`, and
`TMPDIR` inside the per-script world so that `New`'s document has a reproducible
@@ -851,6 +960,11 @@ this matter, are both in `pardes_frame` rather than in the view — it re-render
every cell of the grid on every frame, and `draw(_:)` ignores its `dirtyRect`
for exactly that reason.
+Those figures are the direct path with all scene bits off. An enabled scene
+adds an offscreen CoreGraphics frame, one Core Image/Metal kernel, and
+presentation of its result. That opt-in cost has not been measured on the M2
+used for the table and is not folded into the direct-path claim.
+
## Not implemented
- **The `lsp` effect.** Needs a worker plus a snapshot of the pane's path and
@@ -862,12 +976,6 @@ for exactly that reason.
- **The `pipe` effect.** Selection filters need `pipeRequest(id)` copied into a
job, a worker to run the command, and a `pipe_resp` event back. Same shape as
`lsp`, one more response type.
-- **The `watch` effect.** `watchPane` is inotify and returns silently off Linux
- (`ponytail:` at `src/tty/tty.zig:1183`). macOS wants the FSEvents half of
- `std.Build.Watch`, which the standard library already has as
- `Build/Watch/FsEvents.zig`. Without it the core simply never receives
- `file_changed`, a state it tolerates because the browser has no filesystem
- either.
- **IME and marked text.** Only finished characters reach `pardes_key`, so a
dead key composes nothing and Option is Alt rather than a compose modifier.
Real composition means implementing `NSTextInputClient` *and* giving the core