feat(gpui): gpui render backend, macos-app target with native text layout, and the desktop benchmark - #293
feat(gpui): gpui render backend, macos-app target with native text layout, and the desktop benchmark#293doodlewind wants to merge 8 commits into
Conversation
The portable backend family (wgpu, software raster, PPA, GLES2) keeps executing the DrawList against compile-time baked font atlases. This adds a second backend class on the same DrawList contract: engine/backends/gpui paints through Zed's gpui/Metal renderer, and text measurement + shaping move to the host text system (CoreText) when an app opts in. Core (backend-neutral, no behavior change unless a host installs the hook): - text::MeasureFn — a pluggable native text measurer on Fonts; Ui::set_text_measure installs it before mount. measure_run, the taffy MeasureCtx and the measureText op all route through the one provider. - DRAW_OP.TEXT_RUN (9): translation-only tracking-0 runs emit the run string + style through a DrawList side table; a styleHash word keeps identical word streams pixel-identical (demand-render hashes stay truthful). Tracked, scaled and rotated runs keep the baked GLYPH_RUN pair on BOTH the measure and paint sides. Partially clipped runs are scissor-bracketed. - raster/damage/wgpu interpreters learn to skip the op (it never reaches fixed-function backends); 5 new core tests pin the gates. Registry: text.layout.native capability + the macos-app target profile (hostAbi 3, window form, acceptsFixed — the slot platforms.ts reserved), so every fixed-viewport console demo admits unchanged, size-locked. engine/backends/gpui (standalone, like esp32p4-ppa): the DrawList -> gpui interpreter (quads, gradients, glyph-cell blitting for baked apps, content- mask scissors, TEXT_RUN shaping via shape_line with kern/liga off so prefix-sum caret math stays exact) plus a pixel-exact escape hatch: gouraud TRI / TEX_TRI batches raster through pocketjs_core::raster into cached local images at target density. hosts/macos (standalone lone-bin, like pocketbook): gpui window host of the macos-app target. Fixed 60 Hz guest ticks from a foreground timer governor (one guest.frame + surface.tick per tick, never from paint), demand renders off the DrawList hash, speaks note-widget's svc editor protocol (keyboard, pointer, scroll, clipboard, IME through EntityInputHandler), letterboxes fixed-viewport apps. bun run macos <app> resolves the manifest against macos-app and derives every host flag from the plan. Also: host string ops now decode JS strings lossily (LossyString) — an app measuring an emoji prefix sliced between surrogate halves is legal JS and must never abort the frame transaction. apps/note enhances text.layout.native: the same unmodified JSX markdown editor now runs with CoreText metrics, full CJK + color emoji, no runtime atlas baking. Proof: bun run macos note --proof (scripted click + typing, debounced autosave round-trips through the gpui host). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tools/bench-desktop.ts measures the pocket note on the gpui host against byte-identical web editors shelled by Tauri v2 and Electron: cold start to each app's own first-painted-frame READY report, hands-off idle and a 120 chars/s typing storm through each stack's real edit path, ps process- tree medians (WebKit XPC helpers attributed by spawn-delta — Tauri's WebContent/GPU processes are launchd children), footprint for physical memory. The gpui renderer gains shaped-line and measured-width caches so a keystroke repaint reshapes one line, not the document. docs/BACKENDS.md names the backend split; .github/workflows/macos.yml is the first macOS CI lane (core tests + clippy + host build). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nch fixtures Final measured run (M3 Max, methodology + fairness caveats in the report): the pocket note on the gpui host takes 1 process / 84 MB idle RSS / 145 ms cold start / 10 MB disk against Tauri v2 (4 procs, 192 MB, 391 ms) and Electron (5 procs, 382 MB, 328 ms, 242 MB). Storm completion is now verified (STORM-DONE), the Tauri window is explicitly focused (an unfocused WKWebView throttles timers and reads fiction), and the footprint column is dropped from the table — Electron's hardened helpers refuse task inspection, so RSS is the uniform metric. The benchmark fixtures' cargo target/ was packing into the npm tarball through the wholesale "tools" files entry (the v0.8.0 E415 failure mode — the tests/npm-package.test.ts tripwire caught it): tools/bench-desktop is now a governed negation in the files map. Tauri codegen (gen/) untracked; gpui backend passes clippy -D warnings for the macOS CI lane. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The caret was animate-pulse — a continuous opacity sine that changes the DrawList every tick, so an idle editor repainted at 60 fps on every host (88% of ticks; ~3.6% CPU on both the gpui and wgpu hosts). Replace it with a browser-style square wave (app-local pocket.config.ts theme: 500 ms on / 500 ms off, hard edges via a same-frame twin keyframe) plus the browser input discipline: the caret is SOLID while typing or moving and resumes blinking from its ON phase after a 0.6 s rest (a <Show> swap remounts the animated node, restarting the baked timeline at frame 0). The square wave's constant segments keep the DrawList byte-stable between edges, so every demand-rendering host skips them: idle-in-edit repaints drop from 2115/2400 ticks to 83/2400 (~2 fps, exactly the edge count), CPU from 3.6% to 0.95% on the gpui host and 3.65% to 0.7% on the wgpu widget host. Same measurement protocol as docs/bench. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Double click selects the word under the pointer in BOTH modes, browser semantics throughout: whitespace and punctuation select as runs, a click just past a word's right half still selects the word (caretFromX rounds to the nearest boundary), CJK and surrogate pairs ride the non-ASCII word class, code blocks stay atomic in preview (rowSelSpan granularity), and a drag after the double click extends from the word start. Detection rides the virtual clock (0.4 s / 3 px on the svc press stream) so replays stay deterministic; wordRangeAt is regex-free QuickJS-portable math with unit coverage. The sample doc's 'same bytes as the PSP build' line predates the backend split — it now names the real contract (same core, same DrawList; wgpu paints baked atlases, gpui paints native text) and the two launch commands, and the charset-anchor comment drops the stale tofu caveat (runtime glyph baking and native shaping both exist now). Verified end-to-end on the gpui host: scripted double click on 'Pocket' + typing X autosaves '# X Note'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… svc-independent capabilities Three review blockers on #293, each fixed at the contract level: 1. TEXT_RUN carried a 32-bit content hash while the run string lived in a side table — colliding texts could leave identical word streams with different pixels, silently defeating demand-render hashes and damage word-diffs, and breaking the "DrawList is the complete Vec<u32>" contract. The op now packs the run string's UTF-8 bytes INTO the words (8 header words + payload; slot/align/lineHeight all in-stream) and the side table is gone: snapshots, hashes and diffs are exact by construction, not probabilistic. All interpreters skip the variable length; the gpui backend decodes from the stream. 2. Measurement and paint could disagree on rotated/scaled text (native- sized box, baked glyphs). The provider is now chosen ONCE at layout build (native iff a measurer is installed, tracking is 0 and the subtree declares no non-translation transform) and RECORDED on the node (Node::text_native); paint follows the record unconditionally, and the style-dirty restyle path can only flip it for a node-local tracking change. Rotated/transformed text takes the baked pair on BOTH sides — pinned by tests including a transformed-ancestor case and a paint-only-transform consistency case. 3. macos-app capabilities were conflated with the note's svc protocol. display.viewport.live is now host-generic: every dynamic app receives the framework's __pocketResizeViewport hook inside the tick transaction (the hero-resize regression); --editor derives from the app being the note companion, never from input.text; and the registry comment states the delivery paths plainly — buttons + live viewport host-generic, pointer/text/IME/clipboard via the companion adapter today (the macos-widget stock-host bar), with the host-generic pointer feed named as follow-up (the touch packing's 9-bit axes cannot carry a 4096-px window, so it needs new framework surface). Rot: hosts/macos passes clippy -D warnings and CI now runs it; the macOS workflow path filter covers contracts/spec, tools/macos.ts and apps/note; tools/macos.ts is negated out of the npm tarball (its build inputs are git-only — a published entry point that cannot run). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Tauri/Electron comparison apps, the runner and the results move to a stacked PR so the backend/host/contract surface reviews on its own boundary. The npm files negation stays — it guards the re-add. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
三个 P1 均已在 17e1881 修复,rot 项一并处理,benchmark 已拆分为 stacked #294。逐条回应: 1. TEXT_RUN 哈希碰撞 → 词流现在直接携带字节。 你的判断成立:短哈希不能替代精确内容,且违反了 "DrawList 是完整 Vec" 的契约。修复取消了 side table 和 styleHash:TEXT_RUN 现在把 run 字符串的 UTF-8 字节打包进词流(8 个头部词 + payload,slot/align/lineHeight 全部随流),所有 hash/damage/快照消费者天然精确——是字节比较,不是摘要。各解释器按变长跳过; 2. 测量/绘制 provider 不一致 → provider 在布局构建期决策一次并记录在节点上。 原实现确实矛盾(native 高度布局 + baked 图集绘制,测试还钉错了语义)。现在 3. 能力与 Note 协议解耦。
Rot 项: hosts/macos 过 验证:core 119 测试、 |
The DrawList grows a second backend class: gpui (Zed's Metal renderer) on macOS, beside the portable baked-text family (wgpu, software raster, GE, PPA, GLES2). Same JSX + Tailwind guest, same deterministic frame transactions and animation engine — the one capability that legitimately differs per backend, text layout, becomes an explicit contract. Full design:
docs/BACKENDS.md.The text capability contract
text.glyphs.baked(portable): compile-time Inter atlases, core-owned measurement,GLYPH_RUN, byte-deterministic pixels across hosts.text.layout.native(new): the host installs a core text measurer before the guest mounts (Ui::set_text_measure); taffy leaf sizes, themeasureTextop and painted glyphs all observe one provider — CoreText through gpui. Full OS font fallback (CJK, color emoji), no runtime atlas baking, no tofu. Pixels are deterministic per host, not across hosts — hence a different id, per the platforms.ts header rule.Core mechanics (backend-neutral; zero behavior change until a host installs the hook):
DRAW_OP.TEXT_RUN(9): the run string's UTF-8 bytes are packed into the word stream (8 header words — slot/align/origin/boxW/lineHeight/color/byteLen — plus payload), so the DrawList stays the completeVec<u32>pixel truth: snapshots, demand-render hashes and damage word-diffs are exact by construction, never a digest. The measurement provider is chosen ONCE at layout build and recorded on the node (Node::text_native): native iff a measurer is installed, tracking is 0 and the subtree declares no non-translation transform — paint follows the record unconditionally, so a node's box and glyphs always come from the same provider (rotated/tracked text = the baked pair on both sides). Partially clipped runs are scissor-bracketed (the origin words are f32 and exempt from the i16 clip guarantee).native_measure_routes_layout_and_emits_text_run, fallbacks, scissor bracketing, styleHash content tracking, hook clearing).The backend and host
engine/backends/gpui(standalone, likeesp32p4-ppa— gpui must not enter the ubuntu release workspace): the DrawList → gpui interpreter. Quads/gradients as vector geometry, baked glyph-cell blitting for portable-text apps, scissors as recursivewith_content_maskscopes, TEXT_RUN shaping with kern/liga/calt disabled so prefix-sum caret math stays exact. GouraudTRI/TEX_TRIbatches (rotated gradients/images, 3D subtrees) raster throughpocketjs_core::rasterinto cached bbox-local images at target density — the portable rasterizer as a pixel-exact sub-backend. Shaped-line + measured-width caches make a keystroke repaint reshape one line, not the document.hosts/macos(standalone lone-bin, likepocketbook): the stock host of the newmacos-apptarget (hostAbi 3 — the desktop wire generationmacos-widgetalready speaks;form: "window";acceptsFixed— the slot platforms.ts had reserved). Fixed 60 Hz guest ticks from a foreground timer governor (oneguest.frame()+surface.tick()per tick, never from paint; bounded catch-up, drop-backlog resync), demand renders off the DrawList hash, speaks note-widget's svc editor protocol (keyboard, pointer, scroll, clipboard, IME viaEntityInputHandler), letterboxes fixed-viewport apps. Governor receipt on exit:pocket-macos: N ticks, M frames rendered.bun run macos <app>(tools/macos.ts): resolves the manifest againstmacos-appand derives every host flag from the plan (--fixed,--native-text,--editor) — never from convention. Every fixed-viewport console demo admits unchanged (admission matrix now 4 targets wide).Flagship: the markdown editor
apps/note— the existing full markdown editor (soft wrap, caret math, selection, undo/redo, clipboard, IME) — runs unmodified on the gpui host; the only diff is one manifest line (enhances: ["text.layout.native"]). Same JSX, native text: proportional CoreText metrics, CJK and color emoji straight from input, resizable window.bun run macos note --proof— scripted pencil-toggle click + typing through the svc protocol, debounced autosave round-trips the file.LossyString) — a legal JS string can never abort a frame again.Benchmark vs Tauri and Electron
Split into the stacked #294 per review (the harness, comparison apps and results review on their own boundary). Headline, same machine and protocol:
Apple M3 Max, macOS 26.5.2, measured on the post-review TEXT_RUN code — one process, a quarter of Electron's memory, 2x faster cold start, 1/24th its disk; the storm confirmed complete on all three (~3600 chars). Idle pcpu at these magnitudes drifts ±1.5 points across runs (pocket and Tauri alike); the structural idle number is the governor receipt: 84 repaints / 2400 idle ticks (~2/s, the caret square wave's edges).
Honest caveats (in #294's report): the pocket storm CPU ramps with document length — the note re-parses the whole growing document per keystroke through the QuickJS interpreter (profiled:
JS_CallInternal+ regex, not rendering — incremental note layout is the follow-up).The caret itself became a measured fix:
animate-pulse(a continuous opacity sine) changed the DrawList every tick, so an idle editor repainted at 60 fps on EVERY host. It is now a browser-style square wave (500 ms on/off, solid while typing, ON-phase restart after a 0.6 s rest) whose constant segments demand rendering skips: idle-in-edit repaints fell from 88% of ticks to 3.5% (84/2400 — exactly the wave's edge count) on the gpui host, with the same word-stream skip on the wgpu widget host — one app-level fix, both backends benefit. (Repaint rate is the robust metric here; single-run pcpu medians at low single digits drift ±1.5 points.)CI, tests, validation
.github/workflows/macos.yml— the first macOS lane (core tests + clippy on the gpui crate + host build onmacos-latest, path-filtered like esp32p4.yml).bun run test: all 11 stages green.cargo testengine/core (118) + desktop workspace green. Goldens untouched by construction (no portable host installs the hook;clearing_native_measure_restores_baked_goldens_pathpins the escape).tests/platform-contracts.test.ts(macos-app column, note native-vs-widget feature split).Review round 1 — resolved
text_run_words_are_the_complete_pixel_truth).tracked_and_transformed_runs_use_the_baked_pair_on_both_sides,recorded_provider_survives_a_paint_only_transform).__pocketResizeViewportfires for every dynamic app inside the tick transaction (the hero-resize regression);--editor= the note companion adapter only, never derived frominput.text; the registry comment states each capability's delivery path plainly. A host-generic pointer feed is real follow-up work: the touch packing's 9-bit axes (≤511 px) cannot carry an app-frame window, so it needs new framework surface — pointer/text/IME/clipboard ride the companion adapter today, the same stock-host barmacos-widgetset in feat(pocket-widget): flat widget runtime — Pocket Note, a markdown sticky on the desktop #129.hosts/macospassesclippy -D warningsand CI runs it; the workflow path filter coverscontracts/spec/**,tools/macos.ts,apps/note/**;tools/macos.tsis negated out of the npm tarball; the benchmark is split into feat(bench): desktop editor benchmark — gpui vs Tauri v2 vs Electron #294.Known limitations / follow-ups
bun run gen).macos-appdoes not claimtext.glyphs.runtime(no runtime atlas extension in this host — native mode makes it unnecessary for the flagship).pocketCLI gets nomacos-appbackend yet;tools/macos.tsfollows thetools/note.tswrapper precedent. The host and gpui crate stay git-only; the files map gained exactly one negation (!tools/bench-desktop, with its governed-list twin) so the benchmark fixtures' cargotarget/can never pack — the tarball tripwire caught it.🤖 Generated with Claude Code