Skip to content

Latest commit

 

History

History
175 lines (149 loc) · 9.59 KB

File metadata and controls

175 lines (149 loc) · 9.59 KB

Architecture

Schist is a microkernel plus plugins. The kernel owns state and contracts; everything a user can see or click lives in a plugin.

crates/app              GPUI shell: window, canvas, panels, dialogs, keymap
├── crates/ui           widget kit: palette, buttons, rows, checkboxes, links
├── crates/plugin-api   the trait surface every feature implements
├── crates/core         kernel: document, COW tiles, layers, history, selection
├── crates/color        pixel/colour primitives, depth conversion
├── crates/pixel-ops    CPU reference blend modes (the semantic contract)
├── crates/compositor   tile compositor, viewport resampling, damage cache
├── crates/compositor-gpu  wgpu compute backend, parity-tested against the CPU
├── crates/fx           blur/warp/carve kernels, same CPU-reference seam
├── crates/adjustments  adjustment parameters, PSD payloads, LUT compilation
├── crates/vector       path building, Bézier flattening, AA rasterization
├── crates/text-engine  font discovery, layout, glyph rasterization
├── crates/colormgmt    ICC profiles, display transforms, dithering
├── crates/codec-psd    PSD/PSB reader and writer
├── crates/codec-raw    camera raw decoding and development, clean-room pure Rust
├── crates/plugin-host-wasm  sandboxed third-party plugins
├── crates/plugin-host-8bf   Photoshop .8bf filter plug-ins (unshipped)
└── crates/plugin-sdk   what plugin authors compile against

crates/quicklook        macOS Quick Look thumbnail and preview extensions
└── crates/preview      one windowless image of a file, as fast as the file allows
crates/mcp              MCP catalog + dispatch: headless sessions (schist-mcp) and the app's AI panel

plugins/                first-party features, each optional at compile time
├── tools-basic         move, eyedropper, hand, zoom
├── tools-paint         brush, pencil, eraser, clone, gradient, bucket, dodge…
├── tools-select        marquees, lasso, magic wand
├── tools-transform     free transform, crop, image/canvas resize
├── tools-vector        shapes, pen
├── tools-type          text layers
├── filters-core        blur, sharpen, noise
├── codecs-common       PNG/JPEG/WebP/TIFF/HEIC/camera raw
└── commands-core       menu commands and their keybindings

Chrome

GPUI ships no widget library, so crates/ui is the one the panels and dialogs share: the two palettes, and a small set of components (Button, IconButton, Checkbox, ListItem, Tab, Link, Swatch). They know nothing about the workspace; they take plain handlers, which is what Context::listener produces. The rule they enforce is when a control acts: a button, a row in a menu, a checkbox, a link all commit on release over the control (GPUI's on_click), the way native buttons do, and never on the press. What starts a gesture keeps the press: canvas tools, sliders, drags, click-to-focus fields, and the openers of menus and dropdowns, which native menus also open on press.

Why the kernel is small

crates/core contains no features. Delete every plugins/ entry and the app still builds and boots — to an empty workspace that can do nothing. That is the test of "everything is a plugin", and it keeps the contracts honest: a tool cannot reach around the API because there is nothing to reach for.

Pixels

All raster data — layer pixels, masks, selections — lives in 256×256 copy-on-write tiles. One decision buys a lot:

  • Undo is cheap. An edit stores Arcs of the tiles it replaced, so a history entry costs memory proportional to changed pixels, not document size.
  • Duplication is free until something is written.
  • Damage tracking falls out. Edits mark rectangles; the compositor and canvas recomposite only the tiles that intersect them.

Colour is straight-alpha f32 RGBA in the compositing pipeline; documents store 8/16/32-bit and convert at tile granularity.

Rendering

crates/pixel-ops is the semantic reference: all 27 PSD blend modes, per-pixel, tested against the spec's formulas. crates/compositor walks the layer tree bottom-up per tile — groups isolate unless pass-through, masks multiply source alpha, clipped layers are confined to their base's alpha, adjustment layers re-colour the backdrop beneath them.

Painting is deliberately not per tile. GPUI's sprite atlas has no padding between entries, so drawing one quad per tile let the sampler bleed past each tile's slot at fractional zoom and drew a dark line at every tile boundary. The canvas instead assembles the visible tiles into a single image — resampled (nearest when zoomed in, so pixels stay crisp; bilinear when zoomed out, to damp aliasing) and checkered — and paints that. Colour management stays cached per tile, since converting is the expensive part.

Interactivity comes first from doing less: only damaged, visible tiles recomposite. On a 100 MP document with three full-canvas blend layers plus a curves adjustment, a 1920×1080 viewport recomposites on the CPU in ~16 ms and a single dirty tile — what a brush stroke actually costs — in ~3 ms (cargo run --release -p schist-compositor --example bench).

On top of that sits a GPU backend (crates/compositor-gpu). The Compositor trait is the seam; set_backend routes every composite_* call — canvas cache, tools, exports — through whichever backend is active. The GPU implementation compiles the layer tree once per composite into a flat op program (a per-pixel stack machine: push layer, blend, clip-blend, adjust, mask), uploads the referenced tiles and masks as storage buffers, and executes the whole batch in one compute dispatch; composite.wgsl mirrors pixel-ops formula for formula and parity tests in compositor-gpu/tests hold it to ±1 RGBA8 step. Viewport resampling (compositor/src/viewport.rs — the crisp/bilinear/box zoom logic the canvas uses) has the same dual implementation. GPUI doesn't expose its render device, so this is a second wgpu instance and results come back over the bus — which batching amortizes, and which is why the damage-driven single-tile path stays cheap either way. Adjustment layers arrive as a per-channel LUT where one exists and as an explicit shader branch where none does — hue/saturation, black & white, threshold and posterize mix channels or step, so the plan sends their coefficients instead of a table. Layers mid-drag and nesting deeper than the shader's fixed stack fall back to the CPU reference per call, bit-identically.

A second seam (crates/fx) covers the pixel work that is not compositing: the separable box passes behind every Gaussian, the lens blur's disc, and the mesh resample Liquify and Puppet Warp re-run on every pointer move. Same shape as the compositor's — a FxBackend trait, set_backend, a CPU reference that is the contract — with two additions the compositor doesn't need. Jobs declare their arithmetic intensity and small ones stay home (worth_offloading), because a round trip costs the same bytes whatever happens in between. And a plane too big for one storage binding is blurred in horizontal bands overlapping by passes * radius rows, which is exactly the distance a vertical pass spreads information, so the rows a band keeps are the ones the whole-image pass would have produced. The warp can't do that — an arbitrary displacement may read anywhere in its source — so it declines instead, and in exchange keeps its source plane resident on the device for the length of a drag, since only the mesh changes between pointer moves.

Content-Aware Scale sits behind the same seam but is a different shape: not one sweep the caller repeats, but a loop the backend repeats. Finding the lowest-energy seam, cutting it and recomputing is hundreds of full-image passes for one command, so coming back between them would cost more than the work; fx_carve.wgsl holds every stage and one command runs the lot, with a single readback at the end. The awkward one is the cumulative-cost scan, which walks the rows in order: a dispatch per row would be tens of thousands per command, so a workgroup does a band of rows at once, loading a span wider than it owns and letting the valid region shrink by a column a row — exactly how far the ±1 dependency spreads. That band index rides on a dynamic uniform offset rather than a counter in a buffer, since bumping a counter would need its own dispatch between every band. compositor-gpu/examples/fxbench measures all of it.

Tools declare a group(), so related tools share one toolbar slot with a flyout and a shared shortcut letter — third-party tools can join an existing group or form their own, and unknown groups sort after the built-ins.

The GPUI boundary

The kernel and plugins never import GPUI. Tools receive PointerInput in document space and return Overlay primitives; crates/app translates between those and GPUI events/paint calls. Two consequences: tools are unit testable with no window, and a GPUI upgrade touches one crate.

PSD fidelity

Every block codec-psd doesn't interpret — layer effects, text engine data, smart objects, unknown image resources — is preserved verbatim on the layer or document and re-emitted on save. Unimplemented features therefore mean "untouched", never "corrupted", which is why files survive a round trip long before every feature exists.

Third-party plugins

Plugins are WebAssembly modules loaded by wasmtime with exactly one host import (schist::log) and a fuel budget. No filesystem, no network, no clock, no randomness: isolation comes from what the sandbox lacks. See plugin-guide.md.