diff options
Diffstat (limited to 'docs/macos.md')
| -rw-r--r-- | docs/macos.md | 176 |
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 |
