Skip to content
rsb.sh

Architecture

RSB ships Lab — a free, professional photo editor for the desktop — and this is how Lab is built. The architecture rests on the four pillars every decision is tested against; the decisions themselves are recorded as ADRs at adrs.rsb.sh, and the rules those decisions imply are written down as standards at standards.rsb.sh.

What follows is the working architecture: the crate map as resolved through the design arcs, each crate's ruling, and its relation to the north star — Blender, the standing proof that free creative software can be professional-grade. Where Blender's design is adopted, that is recorded; where it is corrected or consciously rejected, the reason the fence was there is stated first. One rule of the house runs through all of it: move responsibility from author discipline into structure.

RSB Lab Architecture Diagram

Last updated 2026-06-17 · pre-ADR working architecture — the decisions below are blessed but not yet promoted to the decision record.

Code in RSB is organized on a single axis: generality. The most general articulation sits at the top; the deeper you go, the more specific the code becomes. References travel in exactly one direction — upward, from specific to general; a child may reference its parent, the parent may never reference its child. The relation is a DAG, not a tree: a crate may reference more-general crates across branches and may have multiple parents. Peers — crates of equal generality — never reference each other; they meet up, in a contract crate more general than both, or down, in a more-specific crate that references both.

most general — the rootreferences point up ▲
most specific — the leafchildren ▼

Every box links to its ruling below. An edge being legal never makes it granted: every crate's dependency cone is individually designed, declared, and asserted in CI, so an edge's absence is a checkable fact rather than a hope.

The universal base

lab-core

The root. It depends on nothing; everything else may depend on it — that asymmetry is the definition of the layer. The crate root holds the error contract: trait Fail: std::error::Error (an honest source() chain, a grammar-conformant Display, and deliberately no blanket impl — implementing the trait is the opt-in act of accepting its obligations), with Chain walking the cause chain and Rendered producing the Go-style outer: middle: leaf reading. mod report is the one child namespace, re-derived contract-first — the prototype's app-shaped domain enum does not survive. The annex pattern (feature-fenced namespaces for subject-generic machinery) is available within the crate and currently has zero occupants.

The contract

lab-shell — the pump

The contract crate; the pump's home. Its clause census is complete: View<Ctx> (event / listener / draw, generic over an app-defined Context type — subject-neutrality by type parameter, checked at compile time); Operator<Ctx> with a registry the app populates and typed argument structs; pointer capture and the step machinery — load-bearing, because undo granularity hangs off it; opaque notifier topics — the shell routes what it cannot read; the job door, with completed and canceled as distinct anticipated outcomes; the per-iteration phase hook; and the six-phase loop order.

Realizations, the recipe family, and the domains

lab-shell-sync

The desktop realization: winit/wgpu boundary modules, the loop, windows and areas, the rendering protocol, the job supervisor (eight callbacks in current Blender — completed and canceled kept as distinct paths), dispatch execution, and the tripwire — a debug assertion that no generation moves while no exec or capture is open.

lab-graph

Generic graph machinery: nodes, sockets, edges, evaluation, a type-parametric connection API — it knows nothing of photography. The design arcs grew its mandate three ways: generation counters and dirty marks on every mutation (the substrate of pull-based invalidation); efficient clone, restore-with-diff-and-mark, and shared-when-unchanged snapshots (undo as just another mutation); and all operations — delete, duplicate-with-remap, connect-with-validation — as the engine's mutation API. Named verbs never live here.

lab-pixel

Pixel-buffer value types: LinearBuffer and RgbBuffer — distinct per stage, so the develop/composite boundary is enforced by the payload itself. Doubly load-bearing: the types are both the seam enforcement and the signature vocabulary. The crate plausibly references nothing — another root, legal on principle.

The name is the naming law's fourth application. RSB already holds other "image" concepts — the image file, the evaluated display image, the image the user means — so lab-image wore a name more general than its contents. lab-pixel passes from both directions: the contents are exactly the pixel vocabulary, and the bare general name is held by the genuinely general artifact.

lab-photo-nodes

The photo node vocabulary: one crate, with develop/, composite/, and transition/ as peer modules. Each defines node kinds — typed evaluation code plus socket signatures registered as data through app admission. The develop/composite seam is held twice: lab-pixel's distinct payload types (compile-time, layout-independent) and module fences asserted by tooling — develop/ never references composite/ and vice versa. Cross-stage connection is refused at the single mutation door and unrepresentable in the derived typed graph.

Why one crate: the transition module references neither family — socket kinds are shared value types, so its signature spans payloads (Linear → Rgb), not crates — and no cut in the tier is forced by the crate-justification ladder. transition/ is a peer of both because its subject is the boundary itself; burying it in composite/ would let one stage silently own the seam.

lab-photo-recipe

The authored model — the graph, source references, parameters; what the user makes. It never holds a pixel buffer. It is what save writes, what undo snapshots, what navigate replaces. It implements nothing shell-shaped, is pumped by nobody, and is consulted through Ctx.

Its representation is a description: node kinds by name, edges, and typed parameter values — never typed node code. Every typed executable structure is the evaluation object's derived, cached artifact, rebuilt via generations. Unknown kinds are preserved, not understood — a recipe survives node kinds its reader doesn't know, so .rsb longevity is structural rather than a format promise. The cone is engine + lab-core only, which keeps the crate near the family's root and cheap for views, verbs, session, and evaluation to reference.

The name carries the naming law twice: "document" was renamed when the .rsb was understood to encode a recipe — a complete, reproducible description of how to get from this RAW to this result — and "recipe" alone over-claims, so the crate is lab-photo-recipe, with bare lab-recipe reserved, unbuilt, until a second recipe kind exists.

lab-photo-session

The session crate: the selection set, the active node, in-flight gesture ephemera, and the undo-stack instance — with the step-stack machinery private to this crate. The stack was never forced into a shared home: dispatch pushes steps through Ctx obligations and never needs the concrete type. If a second family ever arrives, the promotion target is a fenced namespace in lab-core, not a new crate.

The cone is engine + lab-core. The recipe is deliberately not in the cone — pruning on recipe-changed takes an iterator of extant node IDs, not the recipe; the crate's whole vocabulary is engine IDs.

lab-photo-ops — the verbs

All operators of the photo family: named verbs executing over recipe + session, with typed argument structs. The operations/operators cut decides what lives where: operations (delete, duplicate-with-remap, connect-with-validation) are functions on the graph and belong to the engine's mutation API; operators — even node.delete — take their operands from selection and wear names in the family's error grammar, so every one lives here. There is no engine-side verb tier and no verb-trait crate: lab-photo-ops exports plain functions, argument structs, and name constants; lab-shell defines the registry plumbing; the app composes the two. The cone provably excludes lab-shell, view-support, and the GPU — "verbs are headless" is a CI edge assertion, not a hope.

lab-raw

The RAW decode domain: a safe wrapper over the libraw FFI repository — which is its own repo for entirely intrinsic reasons: a C++ build profile, heavy CI, and a genuine external audience. Whether domain crates and node-family crates are distinct or the same crates is still open.

lab-post

The post-processing domain: the algorithms composite nodes invoke.

lab-color

Color management, invoked on the way to the shell's surface — one of the three pieces the dissolved "render layer" decomposed into (viewport transform is view behavior; inspect is a view feature; "render" is the traffic across the pump seam, not a layer).

The Context census

The pump's Ctx type parameter has a closed census of four members — recipe, evaluation, session, and the report stream — plus whatever the app adds. Every member is generation-tracked; no code anywhere fires a notification by hand. Once per loop iteration, the phase flush diffs all generations and emits coalesced topics.

recipe

The authored member: the lab-photo-recipe instance. Mutated only through the single door — verbs, or capture-scoped view mutation during a gesture — and snapshotted per undo step.

evaluation

Two internally distinct tiers: sourced (decoded sensor data, EXIF — invalidated by the file changing, seconds to rebuild) and derived (evaluated buffers — invalidated by edits, milliseconds to rebuild). Keeping them distinct is what guarantees editing and undo never re-decode. The evaluation object owns cache policy, is flushed in the phase hook, and bumps generations from worker completions. Its crate home is still to be decided.

session

Shared ephemera: the selection set and active node, in-flight gesture state, and the undo-stack instance. The membership test: state multiple views must agree on that does not change the output. Fires selection-changed via generations; subscribes to recipe-changed to prune dangling selection; never saved. Its crate home is lab-photo-session.

report stream

From lab-core. Verbs return Result<_, E: Fail>; the dispatcher reports failures in grammar form — the verb registry and the failure vocabulary are one taxonomy. A report-console view's subject is this stream: a view crate referencing lab-shell and lab-core only, the view rule generalizing cleanly.

App extensions — the census is a floor, not a ceiling

The four members are what the family requires. The app's concrete Ctx — its one piece of real code — may add members: layout state, app preferences. UI-state verbs (toggle panel, switch workspace) are app-tier operators over those extensions, dispatched by the same machinery.

The view tier and the app

view-support and the editor crates

A view is the editor. One crate per editor; editors never reference each other — coordination flows through the document and topics, and the isolation is mechanical, held by Cargo's acyclicity rather than review. Each editor implements View<Ctx> and consults its subject through Ctx; view-support holds the shared widgets at the top of the tier. Editor crates ship their default keymaps as data, binding keys to verb-name constants imported from the verbs crate — a typo is a compile error; the app composes keymaps and validates resolution.

Lab — the leaf

The app is composition only: main; the concrete Ctx type (census plus extensions); admission of view kinds and operator sets; keymap composition and resolution validation; failure policy — what abort, retry, and skip mean for this product; packaging. Bootstrap is inversion: the app assembles and calls run(…), and the shell is sovereign thereafter. The app is the only place where a shell realization and the document meet.