At fit, every output pixel of the fused pass loads one texel from a source three or four times its width, on a stride. The memory system fetches the texels it skips along with the one it wanted, so on a 60 MP rgba16float source that gather was most of what the fused pass cost: 10.6 ms of a 2560x1600 frame against 3.8 ms for the same shader reading a contiguous window (the 1:1 view). At 3840x2160 it was 21.1 ms. Those are the laptop RTX 3050 with its clocks held at 420/810 MHz by the power cap; unthrottled the same frames were about 2.0 and 3.2 ms, and the gather is the same share of them. Which texel an output pixel reads depends only on the framing prologue, the framing and warp uniforms, the source and the render size. None of those move during a slider drag, so the gather is the same work every frame. The fused shader now takes a render-sized rgba16float cache of it (bindings 6 and 7, declared in every generated shader like the masks) and a pair of uniform flags: write what was gathered, or read it back at the pixel's own coordinate. AdjustPass keeps the cache and decides per dispatch. The composer supplies `ComposedShader::sample_key`, a hash of the prologue and those uniforms, and AdjustPass adds the image and the size; an image gets a process-unique id for this rather than being held alive by the key. The picture is bit-for-bit the same. The source is rgba16float and so is the cache, so the stored texel is the texel, and only the path that reads a texel whole takes part: an interpolated sample (straightening, lens warps, CA) is a blend that f16 could not hold exactly, so the composer gives it no key and it reads directly as before. The cache is written on the second frame with a given key, not the first: a crop or zoom drag changes the key every frame, and writing then would add a render-sized write to exactly the gestures that can afford it least. It is kept only up to 3840x2400, so an export never parks a full-frame copy on the device, and `release_caches` drops it. Measured with a scratch probe rendering the synthetic 60 MP frame from examples/frame_budget.rs, forty frames per run after six warm-up, five runs of each binary alternated, median of the per-run p50 (GPU idle apart from the power cap): scene before after neutral 2560x1600 fit 10.62 ms 3.88 ms exposure 2560x1600 fit 10.83 ms 3.87 ms nr chroma 2560x1600 fit 19.84 ms 12.69 ms neutral 3840x2160 fit 21.05 ms 7.11 ms exposure 3840x2160 fit 21.08 ms 6.94 ms clarity 3840x2160 fit 42.20 ms 27.88 ms neutral 2560x1600 1:1 3.83 ms 3.84 ms (control: nothing to gain) The rgba8 output of every scene hashed identically before and after, in isolated runs and across all 38 scene/size/view combinations of the probe. New tests walk a pass through direct, write and read frames, a slider move, a neighbourhood operation and a framing change, and compare every frame with a fresh pass that can only have read directly.
Documentation
Two audiences, two folders. Most people want the first table and never the second.
Using DarkRoom
| The manual | Every feature, pictured from the application itself — opening a library, rating and filing, developing, local masks, repair, film, panoramas, export |
| How it is driven | Every gesture and shortcut, by screen. Generated from the code, so it cannot describe one the application does not have |
The top-level README says what DarkRoom is, how to get it on each platform, and what is still missing.
Changing DarkRoom
Everything under dev/ is for someone working on the code. Start with
CONTRIBUTING.md, which says how to land a first change
without reading the rest.
The register and the record. What must be built, how it is built, and how far along it is.
| requirements.md | What the software must do — the numbered register, the decisions (D-numbers) and the spikes (S-numbers) |
| architecture.md | How it is built — crates, the GPU pipeline, the data model, sync |
| traceability.md | Generated: which requirement is claimed by which file. Never edited by hand |
| outstanding.md | What is specified and not built, and whether that is a decision or a gap |
| technical-debt.md | Compromises taken deliberately, each with the condition that retires it |
| code-health.md | What a contribution costs, per seam, measured |
Designs, one per subsystem. Each is the specification the code was built to, kept current as the code moved.
| catalog.md | The index, the library view, incremental scan, the job queue |
| storage.md | Storage backends: the seam a folder, a sync client and a Nextcloud account share |
| faces.md | Face detection, identity, clustering and the eye-state models |
| segmentation.md | How the application finds the regions a local mask snaps to |
| mask-editing.md | Painting, erasing and combining masks |
| spot-removal.md | Clone and heal as parameters in the edit graph |
| panorama.md | Alignment, projection, the chunked composite and the border fill |
| inference.md | The neural runtime and model chosen per device, with the measurements |
| display-and-extension.md | The display contract, and why the fused pipeline is already most of a plugin format |
| view-composition.md | A controller for the display layer |
| ui-navigation.md | Finding things in the interface once there are many |
Measurements. Numbers committed so a regression is a diff rather than a recollection.
| benchmarks.md | The per-commit suite: what it covers, what it does not, how to read a failure |
| bench-baseline.json | The committed numbers the suite checks against |
| frame-budget.md | What a frame costs on each device, and the decision those figures settled |
Platforms and distribution.
| distribution.md | Which channels v1 targets and what each one constrains |
| windows.md | The Windows installer, cross-built from the Linux CI |
| android-signing.md | Which key signs the APK, and keeping it |
Archive. Kept as the record of what was asked for, not as plans.
| milestone-v0.1.md | The first milestone, delivered 2026-08-30 and superseded |
| ui-refinement.md | How the interface should look; succeeded by ui-navigation.md |
Conventions
Two files here are generated and must not be edited by hand:
gestures.md and dev/traceability.md. Both come from
cargo run -p traceability and the pre-commit hook keeps them in step with
the tree. The manual's pictures are recorded by
tools/manual and live in LFS.
A design document links to the requirements it satisfies and to the code
that satisfies them. When the code moves, the link moves with it; a document
that has stopped being true goes to dev/archive/ with a note saying what
replaced it, rather than being deleted.