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
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.
Universal base — unrelated roots permitted; currently one
Realizations · the recipe family · domains
Recipe family · data tier — references no shell crate
Context members (runtime census — not all are crates)
View tier · app
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.