Put the developer docs under docs/dev and index the folder for users first

docs/ had 26 developer documents flat beside the manual, and the two
audiences are very differently sized: most readers want the manual and
the gesture reference, a few want the register, the designs and the
measurements. The manual and gestures.md stay at the top; everything for
someone changing the code moves to docs/dev/, and the two documents that
name their own successors — the v0.1 milestone and the UI-refinement plan
— go to docs/dev/archive/ rather than being deleted, since both are still
cited. docs/README.md is the index, users first.

Every reference follows: code comments, Cargo manifests, the workflows,
the pre-commit hook, the bench and traceability tools (which locate the
repo root by docs/dev/requirements.md now), packaging, the Docker READMEs,
CLAUDE.md, CONTRIBUTING.md and the README. The matrix links one level
deeper and is regenerated. Links out of the moved documents into the tree
gain a level; a link checker over every Markdown file finds none broken.
This commit is contained in:
2026-09-20 16:20:15 +02:00
parent 08727cff5a
commit 6b1aac477d
137 changed files with 658 additions and 572 deletions
+3 -3
View File
@@ -1,6 +1,6 @@
name: Benchmarks name: Benchmarks
# The suite docs/requirements.md §8 has been promising since it was written: # The suite docs/dev/requirements.md §8 has been promising since it was written:
# "an automated benchmark suite against a synthetic 50k catalog, run per-commit # "an automated benchmark suite against a synthetic 50k catalog, run per-commit
# … A regression beyond stated tolerance fails the build." # … A regression beyond stated tolerance fails the build."
# #
@@ -29,7 +29,7 @@ name: Benchmarks
# every commit to establish, every time, that this runner has no GPU. It # every commit to establish, every time, that this runner has no GPU. It
# runs on demand (Actions → Run workflow) so that a runner that *does* # runs on demand (Actions → Run workflow) so that a runner that *does*
# have one can be pointed at it, and the numbers it produces belong in # have one can be pointed at it, and the numbers it produces belong in
# docs/frame-budget.md by hand, as they already are. # docs/dev/frame-budget.md by hand, as they already are.
on: on:
push: push:
@@ -183,7 +183,7 @@ jobs:
- name: Frame budget (FR-DSP-3) - name: Frame budget (FR-DSP-3)
run: cargo test --release -p dr-gpu --test frame_budget -- --nocapture run: cargo test --release -p dr-gpu --test frame_budget -- --nocapture
# The instrument behind docs/frame-budget.md. It exits non-zero with no # The instrument behind docs/dev/frame-budget.md. It exits non-zero with no
# adapter, which is right for a tool a person runs deliberately and wrong # adapter, which is right for a tool a person runs deliberately and wrong
# for a job that usually has none — hence continue-on-error. Its table is # for a job that usually has none — hence continue-on-error. Its table is
# in the log for whoever asked for this run; the committed numbers are # in the log for whoever asked for this run; the committed numbers are
+2 -2
View File
@@ -322,7 +322,7 @@ jobs:
env: env:
CARGO_TARGET_DIR: target-android CARGO_TARGET_DIR: target-android
# Absent secrets mean a debug signature, which is what a fork or a # Absent secrets mean a debug signature, which is what a fork or a
# branch build should get. Set all three (see docs/android-signing.md) # branch build should get. Set all three (see docs/dev/android-signing.md)
# and the same job produces a release-signed APK instead. # and the same job produces a release-signed APK instead.
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
KEYSTORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} KEYSTORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
@@ -370,7 +370,7 @@ jobs:
# TRACES: FR-PLAT-WIN-3 # TRACES: FR-PLAT-WIN-3
# The Windows executable and its installer, cross-built from Linux # The Windows executable and its installer, cross-built from Linux
# (docs/windows.md §7). No Windows machine anywhere in this job: what it # (docs/dev/windows.md §7). No Windows machine anywhere in this job: what it
# can prove is that the binary links, is a Windows executable with no # can prove is that the binary links, is a Windows executable with no
# MinGW runtime imports, starts under Wine, and that the installer installs # MinGW runtime imports, starts under Wine, and that the installer installs
# and uninstalls under Wine. What it cannot prove — a Vulkan device, a # and uninstalls under Wine. What it cannot prove — a Vulkan device, a
+5 -5
View File
@@ -7,7 +7,7 @@ name: Traceability
# fail its own threshold. Two rules follow, and the extractor's own tests # fail its own threshold. Two rules follow, and the extractor's own tests
# enforce both: # enforce both:
# #
# 1. Denominators are parsed from docs/requirements.md at run time. # 1. Denominators are parsed from docs/dev/requirements.md at run time.
# 2. Coverage is |traced ∩ defined| / |defined|, never a raw traced count. # 2. Coverage is |traced ∩ defined| / |defined|, never a raw traced count.
# #
# This job is static analysis of source comments plus markdown parsing, so it # This job is static analysis of source comments plus markdown parsing, so it
@@ -74,11 +74,11 @@ jobs:
run: | run: |
set -e set -e
cargo run -q -p traceability -- report cargo run -q -p traceability -- report
if ! git diff --quiet docs/traceability.md; then if ! git diff --quiet docs/dev/traceability.md; then
echo "" echo ""
echo "docs/traceability.md is out of date." echo "docs/dev/traceability.md is out of date."
echo "Run: cargo run -p traceability -- report" echo "Run: cargo run -p traceability -- report"
git diff --stat docs/traceability.md git diff --stat docs/dev/traceability.md
exit 1 exit 1
fi fi
@@ -127,4 +127,4 @@ jobs:
- name: Summary - name: Summary
if: always() if: always()
run: head -30 docs/traceability.md || true run: head -30 docs/dev/traceability.md || true
+4 -4
View File
@@ -26,7 +26,7 @@ fi
# The artefacts are generated from the tree, so regenerating them because one # The artefacts are generated from the tree, so regenerating them because one
# was itself edited would be circular. # was itself edited would be circular.
case "$(tr -d '[:space:]' <<< "${staged}")" in case "$(tr -d '[:space:]' <<< "${staged}")" in
docs/traceability.md | docs/gestures.md | ui/dr-ui/src/gesture_book.rs) docs/dev/traceability.md | docs/gestures.md | ui/dr-ui/src/gesture_book.rs)
exit 0 exit 0
;; ;;
esac esac
@@ -41,9 +41,9 @@ if ! cargo run -q -p traceability -- report >/dev/null 2>&1; then
exit 0 exit 0
fi fi
if ! git diff --quiet -- docs/traceability.md; then if ! git diff --quiet -- docs/dev/traceability.md; then
git add docs/traceability.md git add docs/dev/traceability.md
echo "pre-commit: regenerated docs/traceability.md and staged it" echo "pre-commit: regenerated docs/dev/traceability.md and staged it"
fi fi
# The gesture vocabulary, same discipline. # The gesture vocabulary, same discipline.
+2 -2
View File
@@ -2,12 +2,12 @@
Notes for anyone — person or agent — changing this code. They record what Notes for anyone — person or agent — changing this code. They record what
went wrong once and what the fix looked like, so the same shape is not went wrong once and what the fix looked like, so the same shape is not
written again. Requirements live in `docs/requirements.md`; this file is written again. Requirements live in `docs/dev/requirements.md`; this file is
about habits, not features. about habits, not features.
## Catalog reads: work is proportional to what changed, never to library size ## Catalog reads: work is proportional to what changed, never to library size
`docs/catalog.md §1` states the rule. These are the ways it was broken on `docs/dev/catalog.md §1` states the rule. These are the ways it was broken on
the Identity screen, found when every confirm click cost half a second on a the Identity screen, found when every confirm click cost half a second on a
24k-image library (2026-09-19), and what each fix looked like. 24k-image library (2026-09-19), and what each fix looked like.
+11 -11
View File
@@ -90,18 +90,18 @@ break it by accident:
cargo run --release -p dr-bench -- check cargo run --release -p dr-bench -- check
``` ```
That is the benchmark suite (`docs/requirements.md` §8), which builds a That is the benchmark suite (`docs/dev/requirements.md` §8), which builds a
synthetic 50,000-image catalog and fails the build if a performance target is synthetic 50,000-image catalog and fails the build if a performance target is
missed or a measurement has drifted past its tolerance. It runs on every push in missed or a measurement has drifted past its tolerance. It runs on every push in
its own workflow. [`docs/benchmarks.md`](docs/benchmarks.md) says what it its own workflow. [`docs/dev/benchmarks.md`](docs/dev/benchmarks.md) says what it
measures, what it deliberately does not, and how to read a failure. If you have measures, what it deliberately does not, and how to read a failure. If you have
touched the catalog, the decoder, the thumbnail store or the exporter, run it touched the catalog, the decoder, the thumbnail store or the exporter, run it
before you send. before you send.
## Requirements and traceability ## Requirements and traceability
[`requirements.md`](docs/requirements.md) is the register of record. [`requirements.md`](docs/dev/requirements.md) is the register of record.
[`traceability.md`](docs/traceability.md) is generated from `TRACES:` tags in [`traceability.md`](docs/dev/traceability.md) is generated from `TRACES:` tags in
the source and must never be hand-edited: the source and must never be hand-edited:
```rust ```rust
@@ -124,7 +124,7 @@ Note that it tracks line numbers, so a change that only moves code still moves
the matrix. Never regenerate it with a stale prebuilt binary. the matrix. Never regenerate it with a stale prebuilt binary.
**One convention that the tooling cannot enforce.** A tag proves that a tag **One convention that the tooling cannot enforce.** A tag proves that a tag
exists, not that the code under it does the thing — `docs/code-health.md` exists, not that the code under it does the thing — `docs/dev/code-health.md`
CH-4 has the details, and two requirements currently read as covered on the CH-4 has the details, and two requirements currently read as covered on the
strength of plumbing a future feature would use. So: **close a requirement strength of plumbing a future feature would use. So: **close a requirement
with a test that would fail if the behaviour were removed.** Coverage that with a test that would fail if the behaviour were removed.** Coverage that
@@ -163,12 +163,12 @@ One commit per change. If you fixed two things, that is two commits.
| Document | Read it when | | Document | Read it when |
|---|---| |---|---|
| [`core/dr-pipeline/ops/README.md`](core/dr-pipeline/ops/README.md) | Adding or changing a develop operation — start here regardless | | [`core/dr-pipeline/ops/README.md`](core/dr-pipeline/ops/README.md) | Adding or changing a develop operation — start here regardless |
| [`docs/architecture.md`](docs/architecture.md) | Anything touching the render path, catalog or sync | | [`docs/dev/architecture.md`](docs/dev/architecture.md) | Anything touching the render path, catalog or sync |
| [`docs/code-health.md`](docs/code-health.md) | Deciding what to work on; grades each seam by what it costs | | [`docs/dev/code-health.md`](docs/dev/code-health.md) | Deciding what to work on; grades each seam by what it costs |
| [`docs/benchmarks.md`](docs/benchmarks.md) | A change that could plausibly cost time or memory | | [`docs/dev/benchmarks.md`](docs/dev/benchmarks.md) | A change that could plausibly cost time or memory |
| [`docs/technical-debt.md`](docs/technical-debt.md) | Something looks wrong — check it was not chosen | | [`docs/dev/technical-debt.md`](docs/dev/technical-debt.md) | Something looks wrong — check it was not chosen |
| [`docs/distribution.md`](docs/distribution.md) | Packaging a build, or adding a permission to one | | [`docs/dev/distribution.md`](docs/dev/distribution.md) | Packaging a build, or adding a permission to one |
| [`docs/requirements.md`](docs/requirements.md) | Reference, not reading | | [`docs/dev/requirements.md`](docs/dev/requirements.md) | Reference, not reading |
`technical-debt.md` is the one to check before "fixing" anything surprising. `technical-debt.md` is the one to check before "fixing" anything surprising.
It records compromises that were deliberate, each with the reasoning and a It records compromises that were deliberate, each with the reasoning and a
+1 -1
View File
@@ -48,7 +48,7 @@ dr-export = { path = "core/dr-export" }
dr-face = { path = "core/dr-face", default-features = false } dr-face = { path = "core/dr-face", default-features = false }
dr-film = { path = "core/dr-film" } dr-film = { path = "core/dr-film" }
# `tract` on by default so a test binary can open a session with nothing # `tract` on by default so a test binary can open a session with nothing
# installed; the apps add `native` to look for a runtime file (docs/inference.md §3). # installed; the apps add `native` to look for a runtime file (docs/dev/inference.md §3).
dr-inference-engine = { path = "core/dr-inference-engine" } dr-inference-engine = { path = "core/dr-inference-engine" }
dr-ingest = { path = "core/dr-ingest" } dr-ingest = { path = "core/dr-ingest" }
dr-gpu = { path = "core/dr-gpu" } dr-gpu = { path = "core/dr-gpu" }
+19 -19
View File
@@ -52,7 +52,7 @@ texture directly — no readback between the GPU and the screen.
|---|---|---| |---|---|---|
| Arch Linux | [`packaging/PKGBUILD`](packaging/PKGBUILD) — `makepkg -si` | Built from every release | | Arch Linux | [`packaging/PKGBUILD`](packaging/PKGBUILD) — `makepkg -si` | Built from every release |
| Android | The APK from each CI run, or `./docker/android/package.sh --install` | Runs on a tablet; F-Droid not yet submitted | | Android | The APK from each CI run, or `./docker/android/package.sh --install` | Runs on a tablet; F-Droid not yet submitted |
| Windows | `DarkRoom-<version>-x86_64-setup.exe`, cross-built by CI ([windows.md](docs/windows.md)) | Verified under Wine only; unsigned | | Windows | `DarkRoom-<version>-x86_64-setup.exe`, cross-built by CI ([windows.md](docs/dev/windows.md)) | Verified under Wine only; unsigned |
| Flatpak | [`packaging/flatpak/`](packaging/flatpak/) | Manifest in tree; choosing a library does not yet work in the sandbox | | Flatpak | [`packaging/flatpak/`](packaging/flatpak/) | Manifest in tree; choosing a library does not yet work in the sandbox |
Or build it. Git LFS is required for the model weights, and the toolchain Or build it. Git LFS is required for the model weights, and the toolchain
@@ -77,29 +77,29 @@ controls, its place in the chain and its tests.
## Where it stands ## Where it stands
**0.13.5**, nineteen tagged releases in. 184 numbered requirements in **0.13.5**, nineteen tagged releases in. 184 numbered requirements in
scope, 84% of them claimed by code and [traced to it](docs/traceability.md); scope, 84% of them claimed by code and [traced to it](docs/dev/traceability.md);
the rest are written down rather than merely absent. the rest are written down rather than merely absent.
**Not built:** plugins (post-v1, [D12](docs/requirements.md)), compare and **Not built:** plugins (post-v1, [D12](docs/dev/requirements.md)), compare and
survey culling, AI denoise, tiled and progressive rendering, HDR merge and survey culling, AI denoise, tiled and progressive rendering, HDR merge and
focus stacking, most of the Android platform integration beyond running, focus stacking, most of the Android platform integration beyond running,
and the Flatpak's library chooser. The performance targets are half and the Flatpak's library chooser. The performance targets are half
verified: the per-commit benchmark suite §8 requires exists for everything verified: the per-commit benchmark suite §8 requires exists for everything
that does not need a frame — the catalog, the scan, the thumbnails — and that does not need a frame — the catalog, the scan, the thumbnails — and
not yet for the render path, so a regression there fails nothing. not yet for the render path, so a regression there fails nothing.
[outstanding.md](docs/outstanding.md) is the list, with the reasoning for [outstanding.md](docs/dev/outstanding.md) is the list, with the reasoning for
each. each.
**The one deliberate compromise worth knowing about before reading **The one deliberate compromise worth knowing about before reading
anything else:** the Android develop view reads its frame back through the anything else:** the Android develop view reads its frame back through the
CPU, because zero-copy there needs wgpu's Vulkan swapchain and that tears a CPU, because zero-copy there needs wgpu's Vulkan swapchain and that tears a
portrait window on a tablet whose panel is mounted landscape. It is debt, portrait window on a tablet whose panel is mounted landscape. It is debt,
not a revision of the rule — [technical-debt.md TD-1](docs/technical-debt.md) not a revision of the rule — [technical-debt.md TD-1](docs/dev/technical-debt.md)
has the measurements and the three things any one of which would remove it. has the measurements and the three things any one of which would remove it.
## Documentation ## Documentation
For someone using it: [docs/README.md](docs/README.md) is the index. The short version, for someone using it:
| | | | | |
|---|---| |---|---|
@@ -111,21 +111,21 @@ For someone changing it:
| | | | | |
|---|---| |---|---|
| [CONTRIBUTING.md](CONTRIBUTING.md) | How to land a first change without reading the rest | | [CONTRIBUTING.md](CONTRIBUTING.md) | How to land a first change without reading the rest |
| [requirements.md](docs/requirements.md) | What the software must do — the numbered register, and the decisions | | [requirements.md](docs/dev/requirements.md) | What the software must do — the numbered register, and the decisions |
| [architecture.md](docs/architecture.md) | How it is built — crates, the GPU pipeline, the data model, sync | | [architecture.md](docs/dev/architecture.md) | How it is built — crates, the GPU pipeline, the data model, sync |
| [technical-debt.md](docs/technical-debt.md) | Compromises taken deliberately, each with the condition that retires it | | [technical-debt.md](docs/dev/technical-debt.md) | Compromises taken deliberately, each with the condition that retires it |
| [outstanding.md](docs/outstanding.md) | What is not built, and whether that is a decision or a gap | | [outstanding.md](docs/dev/outstanding.md) | What is not built, and whether that is a decision or a gap |
| [code-health.md](docs/code-health.md) | What a contribution costs, per seam, measured | | [code-health.md](docs/dev/code-health.md) | What a contribution costs, per seam, measured |
| [traceability.md](docs/traceability.md) | Generated: which requirement is claimed by which file | | [traceability.md](docs/dev/traceability.md) | Generated: which requirement is claimed by which file |
Designs, one per subsystem: Designs, one per subsystem:
[segmentation](docs/segmentation.md) and [mask editing](docs/mask-editing.md) · [segmentation](docs/dev/segmentation.md) and [mask editing](docs/dev/mask-editing.md) ·
[spot removal](docs/spot-removal.md) · [panorama](docs/panorama.md) · [spot removal](docs/dev/spot-removal.md) · [panorama](docs/dev/panorama.md) ·
[faces](docs/faces.md) · [inference](docs/inference.md) · [faces](docs/dev/faces.md) · [inference](docs/dev/inference.md) ·
[storage and sync](docs/storage.md) · [catalog](docs/catalog.md) · [storage and sync](docs/dev/storage.md) · [catalog](docs/dev/catalog.md) ·
[display and extension](docs/display-and-extension.md) · [display and extension](docs/dev/display-and-extension.md) ·
[navigation](docs/ui-navigation.md) · [distribution](docs/distribution.md) · [navigation](docs/dev/ui-navigation.md) · [distribution](docs/dev/distribution.md) ·
[windows](docs/windows.md) · [benchmarks](docs/benchmarks.md). [windows](docs/dev/windows.md) · [benchmarks](docs/dev/benchmarks.md).
## Licence ## Licence
+5 -5
View File
@@ -240,7 +240,7 @@ fn android_main(app: slint::android::AndroidApp) {
/// ///
/// **Face weights are absent from the repository by design.** The InsightFace /// **Face weights are absent from the repository by design.** The InsightFace
/// grant is research-only and incompatible with this project's licence /// grant is research-only and incompatible with this project's licence
/// (docs/faces.md §2), so a desktop user fetches them, runs /// (docs/dev/faces.md §2), so a desktop user fetches them, runs
/// `tools/fix-face-model-shapes.sh` over them, and drops the result in. A build /// `tools/fix-face-model-shapes.sh` over them, and drops the result in. A build
/// that carries none is the ordinary case and face indexing simply stays off. /// that carries none is the ordinary case and face indexing simply stays off.
/// ///
@@ -321,17 +321,17 @@ fn unpack_bundled_models(app: &slint::android::AndroidApp) {
// before it reports the tab available. // before it reports the tab available.
// //
// Three detectors, because which one runs is a setting // Three detectors, because which one runs is a setting
// (`FaceDetector`, docs/faces.md §12.3) and a tablet has no other way to // (`FaceDetector`, docs/dev/faces.md §12.3) and a tablet has no other way to
// obtain the one it was not shipped with. Twenty megabytes of APK for // obtain the one it was not shipped with. Twenty megabytes of APK for
// the choice; the embedder is the same for all three. // the choice; the embedder is the same for all three.
// //
// Then the three eye-state models (docs/faces.md §17): landmarks, open // Then the three eye-state models (docs/dev/faces.md §17): landmarks, open
// or closed, sunglasses. The app indexes without them; with them the // or closed, sunglasses. The app indexes without them; with them the
// eyes-open filter has something to read, and a tablet has no other way // eyes-open filter has something to read, and a tablet has no other way
// to get them either. // to get them either.
// //
// The int8 forms beside the three detectors are what the Hexagon runs // The int8 forms beside the three detectors are what the Hexagon runs
// (docs/inference.md §5); the engine loads the sibling when the probe // (docs/dev/inference.md §5); the engine loads the sibling when the probe
// chose that rung and ignores it otherwise. // chose that rung and ignores it otherwise.
const BUNDLED: [(&std::ffi::CStr, &str); 14] = [ const BUNDLED: [(&std::ffi::CStr, &str); 14] = [
(c"models/scrfd_500m_640.onnx", "scrfd_500m_640.onnx"), (c"models/scrfd_500m_640.onnx", "scrfd_500m_640.onnx"),
@@ -420,7 +420,7 @@ fn unpack_bundled_models(app: &slint::android::AndroidApp) {
// on a first launch they were not on disk until this line. The runtime // on a first launch they were not on disk until this line. The runtime
// is in the APK's native library directory beside `libdarkroom.so`, // is in the APK's native library directory beside `libdarkroom.so`,
// which is also where Qualcomm's DSP loader has to be pointed for the // which is also where Qualcomm's DSP loader has to be pointed for the
// Hexagon skel (docs/inference.md §3, §8). // Hexagon skel (docs/dev/inference.md §3, §8).
dr_ui::inference::init(native_library_dir().into_iter().collect()); dr_ui::inference::init(native_library_dir().into_iter().collect());
} }
+3 -3
View File
@@ -21,7 +21,7 @@ fn main() -> anyhow::Result<()> {
// build made on a machine that cannot run the application — the Linux CI // build made on a machine that cannot run the application — the Linux CI
// producing the Windows binary, checked under Wine — has an exit that // producing the Windows binary, checked under Wine — has an exit that
// proves the executable starts without opening a window or touching the // proves the executable starts without opening a window or touching the
// user's directories (docs/windows.md §6). // user's directories (docs/dev/windows.md §6).
if std::env::args().nth(1).as_deref() == Some("--version") { if std::env::args().nth(1).as_deref() == Some("--version") {
println!("darkroom-desktop {}", env!("CARGO_PKG_VERSION")); println!("darkroom-desktop {}", env!("CARGO_PKG_VERSION"));
return Ok(()); return Ok(());
@@ -63,7 +63,7 @@ fn main() -> anyhow::Result<()> {
// Before the window: the probe runs on its own thread and the first // Before the window: the probe runs on its own thread and the first
// frame does not wait for it, but the models a background job asks for // frame does not wait for it, but the models a background job asks for
// should already know where the runtime is (docs/inference.md §4). // should already know where the runtime is (docs/dev/inference.md §4).
dr_ui::inference::init(runtime_dirs()); dr_ui::inference::init(runtime_dirs());
dr_ui::run(paths)?; dr_ui::run(paths)?;
@@ -78,7 +78,7 @@ fn main() -> anyhow::Result<()> {
/// Where a desktop package may have put `libonnxruntime`, most specific /// Where a desktop package may have put `libonnxruntime`, most specific
/// first. None of these existing is the tract build, which is a complete /// first. None of these existing is the tract build, which is a complete
/// application and not an error (docs/inference.md §3). /// application and not an error (docs/dev/inference.md §3).
/// ///
/// `DARKROOM_ORT_DIR` is for a developer pointing at a runtime that is not /// `DARKROOM_ORT_DIR` is for a developer pointing at a runtime that is not
/// installed — the wheel's `capi` directory, say. Then beside the executable /// installed — the wheel's `capi` directory, say. Then beside the executable
+1 -1
View File
@@ -315,7 +315,7 @@ fn full_library(
// The three phases, separately, because "a regroup takes n seconds" does // The three phases, separately, because "a regroup takes n seconds" does
// not tell anyone which half to optimise — and the answer differs between // not tell anyone which half to optimise — and the answer differs between
// a desktop and a tablet (docs/faces.md §9). // a desktop and a tablet (docs/dev/faces.md §9).
{ {
let dim = candidates.first().map(|c| c.embedding.len()).unwrap_or(0); let dim = candidates.first().map(|c| c.embedding.len()).unwrap_or(0);
let flat: Vec<f32> = candidates let flat: Vec<f32> = candidates
+1 -1
View File
@@ -82,7 +82,7 @@
//! Grouping has no natural `subject_id`: it is a property of a *run* of frames, //! Grouping has no natural `subject_id`: it is a property of a *run* of frames,
//! so a per-image job would rebuild the world once per photograph. It is //! so a per-image job would rebuild the world once per photograph. It is
//! therefore a debounced library-level pass, for exactly the reasons //! therefore a debounced library-level pass, for exactly the reasons
//! docs/catalog.md §10.2 gives for face clustering, and [`regroup`] is the whole //! docs/dev/catalog.md §10.2 gives for face clustering, and [`regroup`] is the whole
//! of it — one ordered walk, no per-pair comparison beyond adjacent frames. //! of it — one ordered walk, no per-pair comparison beyond adjacent frames.
//! //!
//! # Grouping is not hiding //! # Grouping is not hiding
+1 -1
View File
@@ -2,7 +2,7 @@
//! Face data as sealed shards, so a second device does not re-index the library. //! Face data as sealed shards, so a second device does not re-index the library.
//! //!
//! Indexing a 23,500-image library is on the order of two hours of CPU //! Indexing a 23,500-image library is on the order of two hours of CPU
//! (docs/faces.md §12.2). It is also **byte-identical on every device**: the //! (docs/dev/faces.md §12.2). It is also **byte-identical on every device**: the
//! same model over the same proxy produces the same embedding. Paying for it //! same model over the same proxy produces the same embedding. Paying for it
//! once per account rather than once per device is the whole point of this //! once per account rather than once per device is the whole point of this
//! module, and it is the same bargain the thumbnail store already makes. //! module, and it is the same bargain the thumbnail store already makes.
+4 -4
View File
@@ -1,7 +1,7 @@
//! TRACES: FR-CULL-8 | FR-CULL-9 | FR-CULL-10 | FR-CULL-11 | FR-CULL-12 | NFR-SEC-5 //! TRACES: FR-CULL-8 | FR-CULL-9 | FR-CULL-10 | FR-CULL-11 | FR-CULL-12 | NFR-SEC-5
//! People and faces: what was detected, who it is, and who said so. //! People and faces: what was detected, who it is, and who said so.
//! //!
//! The storage half of docs/faces.md. `dr-face` finds faces and turns them into //! The storage half of docs/dev/faces.md. `dr-face` finds faces and turns them into
//! 512 numbers; this module is where those numbers acquire an identity, and //! 512 numbers; this module is where those numbers acquire an identity, and
//! where the user's corrections outrank the model's guesses. //! where the user's corrections outrank the model's guesses.
//! //!
@@ -110,7 +110,7 @@ pub struct DetectedFace {
/// Raw rather than unit length, so the length ([`Self::quality`]) is in /// Raw rather than unit length, so the length ([`Self::quality`]) is in
/// the blob and not only beside it. Readers re-normalise on load. /// the blob and not only beside it. Readers re-normalise on load.
pub embedding: Vec<u8>, pub embedding: Vec<u8>,
/// Source pixels across the aligned crop (docs/faces.md §7). /// Source pixels across the aligned crop (docs/dev/faces.md §7).
pub crop_px: f32, pub crop_px: f32,
/// Length of the raw embedding before normalisation — the model's own /// Length of the raw embedding before normalisation — the model's own
/// reading of how recognisable the crop was, and the gate on whether /// reading of how recognisable the crop was, and the gate on whether
@@ -211,7 +211,7 @@ pub use dr_face::Calibration;
/// the same face in the same photograph, for carrying an identity across a /// the same face in the same photograph, for carrying an identity across a
/// re-detection. /// re-detection.
/// ///
/// Set at the reference library's P≈0.95 line (docs/faces.md §9's table: /// Set at the reference library's P≈0.95 line (docs/dev/faces.md §9's table:
/// 0.449), which is far above anything two different people in one frame /// 0.449), which is far above anything two different people in one frame
/// reach and below what one face re-embedded from a better crop of itself /// reach and below what one face re-embedded from a better crop of itself
/// does. The number is only ever asked about *overlapping* boxes on *one* /// does. The number is only ever asked about *overlapping* boxes on *one*
@@ -2469,7 +2469,7 @@ mod tests {
} }
/// The reference implementation's fitted MBF curve puts the P=0.5 boundary /// The reference implementation's fitted MBF curve puts the P=0.5 boundary
/// at cosine 0.267 (docs/faces.md §1). Our own first end-to-end run scored /// at cosine 0.267 (docs/dev/faces.md §1). Our own first end-to-end run scored
/// 0.596 between distinct photographs of one person and 0.05 between /// 0.596 between distinct photographs of one person and 0.05 between
/// different people, so those two must land either side. /// different people, so those two must land either side.
#[test] #[test]
+1 -1
View File
@@ -729,7 +729,7 @@ fn attached_has_table(conn: &Connection, schema: &str, table: &str) -> Result<bo
/// # What travels, and what is recomputed /// # What travels, and what is recomputed
/// ///
/// The rule this module already follows for the rest of the catalog: user /// The rule this module already follows for the rest of the catalog: user
/// judgements travel, inference is rebuilt. Concretely (docs/faces.md, and the /// judgements travel, inference is rebuilt. Concretely (docs/dev/faces.md, and the
/// asymmetry `crate::faces` opens with): /// asymmetry `crate::faces` opens with):
/// ///
/// - **People** — uuid, name, and whether the user set them aside. Merged by /// - **People** — uuid, name, and whether the user set them aside. Merged by
+2 -2
View File
@@ -16,7 +16,7 @@
//! # The one thing a rebuild does not recover //! # The one thing a rebuild does not recover
//! //!
//! **Collections.** A manual collection is a set of images the user assembled //! **Collections.** A manual collection is a set of images the user assembled
//! by hand and nothing in the filesystem records it (`docs/catalog.md` §8.1) — //! by hand and nothing in the filesystem records it (`docs/dev/catalog.md` §8.1) —
//! which is the whole reason the catalog file itself syncs. So the two offers //! which is the whole reason the catalog file itself syncs. So the two offers
//! are not interchangeable, and the interface must not present them as if they //! are not interchangeable, and the interface must not present them as if they
//! were: a restore keeps the user's collections, a rebuild does not. //! were: a restore keeps the user's collections, a rebuild does not.
@@ -570,7 +570,7 @@ mod tests {
// The first NFR-R6 branch, asserted on the thing that distinguishes it // The first NFR-R6 branch, asserted on the thing that distinguishes it
// from the second: a collection exists nowhere but the catalog, so it // from the second: a collection exists nowhere but the catalog, so it
// is the evidence that the *contents* came back and not merely a // is the evidence that the *contents* came back and not merely a
// readable file (docs/catalog.md §8.1). // readable file (docs/dev/catalog.md §8.1).
let dir = tempdir("restore"); let dir = tempdir("restore");
let path = dir.join("catalog.sqlite"); let path = dir.join("catalog.sqlite");
fixture(&path, 500); fixture(&path, 500);
+2 -2
View File
@@ -1009,7 +1009,7 @@ CREATE INDEX face_index_model ON face_index(model_id);
const V8: &str = r#" const V8: &str = r#"
-- TRACES: FR-CULL-8 | FR-CULL-9 | FR-CULL-10 | FR-CULL-11 | FR-CULL-12 | NFR-SEC-5 -- TRACES: FR-CULL-8 | FR-CULL-9 | FR-CULL-10 | FR-CULL-11 | FR-CULL-12 | NFR-SEC-5
-- People and faces (docs/faces.md, docs/catalog.md §10). -- People and faces (docs/dev/faces.md, docs/dev/catalog.md §10).
-- --
-- Everything here is **derived data** except one column. Faces, landmarks, -- Everything here is **derived data** except one column. Faces, landmarks,
-- embeddings, cluster assignments and suggestions are all reproducible by -- embeddings, cluster assignments and suggestions are all reproducible by
@@ -1046,7 +1046,7 @@ CREATE TABLE faces (
landmarks BLOB NOT NULL, -- 5 x (x, y) f32, normalised likewise landmarks BLOB NOT NULL, -- 5 x (x, y) f32, normalised likewise
detector_confidence REAL NOT NULL, detector_confidence REAL NOT NULL,
embedding BLOB NOT NULL, -- 512 x f16; unit length until V14, raw since embedding BLOB NOT NULL, -- 512 x f16; unit length until V14, raw since
-- Source pixels across the aligned 112x112 crop (docs/faces.md §7). -- Source pixels across the aligned 112x112 crop (docs/dev/faces.md §7).
-- --
-- Not cosmetic: it is the honest quality signal for the UI, a feature in -- Not cosmetic: it is the honest quality signal for the UI, a feature in
-- the §8 calibration -- FR-CULL-9 names face size as an axis along which an -- the §8 calibration -- FR-CULL-9 names face size as an axis along which an
+2 -2
View File
@@ -11,7 +11,7 @@ log.workspace = true
# Inference. `ort` is the API; **what runs it is `dr-inference-engine`'s # Inference. `ort` is the API; **what runs it is `dr-inference-engine`'s
# business** — tract, or an ONNX Runtime the app found on disk, on whichever # business** — tract, or an ONNX Runtime the app found on disk, on whichever
# provider the device has (docs/inference.md). This crate never names either. # provider the device has (docs/dev/inference.md). This crate never names either.
ort = { workspace = true, optional = true } ort = { workspace = true, optional = true }
dr-inference-engine = { workspace = true, optional = true } dr-inference-engine = { workspace = true, optional = true }
ndarray = { workspace = true, optional = true } ndarray = { workspace = true, optional = true }
@@ -37,7 +37,7 @@ required-features = ["inference"]
[features] [features]
# Nothing on by default, and in particular **no `embedded-model`**: the weights # Nothing on by default, and in particular **no `embedded-model`**: the weights
# are not a build input and never become one (docs/faces.md §2.2). A feature # are not a build input and never become one (docs/dev/faces.md §2.2). A feature
# flag that *could* embed them is a flag someone eventually sets in a packaging # flag that *could* embed them is a flag someone eventually sets in a packaging
# script, and the InsightFace grant does not survive that. # script, and the InsightFace grant does not survive that.
default = [] default = []
+1 -1
View File
@@ -1,4 +1,4 @@
//! Detect the faces in a JPEG and read each one's eyes (docs/faces.md §17). //! Detect the faces in a JPEG and read each one's eyes (docs/dev/faces.md §17).
//! //!
//! The thing worth looking at is whether the eye boxes land on eyes and //! The thing worth looking at is whether the eye boxes land on eyes and
//! whether soft ones are refused — so with `--dump DIR` the crops the //! whether soft ones are refused — so with `--dump DIR` the crops the
+1 -1
View File
@@ -7,7 +7,7 @@
//! DET.onnx EMB.onnx photo.jpg [photo.jpg ...] //! DET.onnx EMB.onnx photo.jpg [photo.jpg ...]
//! //!
//! The models must have had their input dims frozen first; see //! The models must have had their input dims frozen first; see
//! `tools/fix-face-model-shapes.sh` and docs/faces.md §12 M1. //! `tools/fix-face-model-shapes.sh` and docs/dev/faces.md §12 M1.
use std::time::Instant; use std::time::Instant;
+1 -1
View File
@@ -1,4 +1,4 @@
//! M1 (docs/faces.md §12) — will tract load these graphs at all? //! M1 (docs/dev/faces.md §12) — will tract load these graphs at all?
//! //!
//! The one measurement everything else in the face subsystem is conditional //! The one measurement everything else in the face subsystem is conditional
//! on. `det_500m.onnx` has a dynamic H/W input, which is exactly what tract //! on. `det_500m.onnx` has a dynamic H/W input, which is exactly what tract
+1 -1
View File
@@ -9,7 +9,7 @@
//! //!
//! # What it is for //! # What it is for
//! //!
//! docs/faces.md §9 has the desktop numbers and the question they leave open: //! docs/dev/faces.md §9 has the desktop numbers and the question they leave open:
//! a GPU GEMM is worth roughly 1.5× of a regroup on a twenty-core desktop, //! a GPU GEMM is worth roughly 1.5× of a regroup on a twenty-core desktop,
//! because the scan is under a third of the pass there. On a tablet the CPU is //! because the scan is under a third of the pass there. On a tablet the CPU is
//! several times slower and the GPU is not, so the same optimisation is worth //! several times slower and the GPU is not, so the same optimisation is worth
+5 -5
View File
@@ -1,4 +1,4 @@
//! Five-point face alignment (docs/faces.md §5). //! Five-point face alignment (docs/dev/faces.md §5).
//! //!
//! ArcFace embeddings are trained on faces warped to a canonical 112×112 //! ArcFace embeddings are trained on faces warped to a canonical 112×112
//! arrangement. Feeding the model a plain bounding-box crop *works* — it //! arrangement. Feeding the model a plain bounding-box crop *works* — it
@@ -208,7 +208,7 @@ impl Similarity {
/// ///
/// # Why least squares and not RANSAC /// # Why least squares and not RANSAC
/// ///
/// The reference C++ implementation (docs/faces.md §1.1) fits this with /// The reference C++ implementation (docs/dev/faces.md §1.1) fits this with
/// OpenCV's `estimateAffinePartial2D` under RANSAC. RANSAC over five points is /// OpenCV's `estimateAffinePartial2D` under RANSAC. RANSAC over five points is
/// a strange fit: the minimal sample for a similarity is two, so it can discard /// a strange fit: the minimal sample for a similarity is two, so it can discard
/// landmarks it judges outliers and solve from a subset — and on a profile face /// landmarks it judges outliers and solve from a subset — and on a profile face
@@ -427,7 +427,7 @@ fn sample_window(
// ── eyes ────────────────────────────────────────────────────────────────── // ── eyes ──────────────────────────────────────────────────────────────────
/// Width of an eye crop as the classifier reads it, in pixels. Fixed by the /// Width of an eye crop as the classifier reads it, in pixels. Fixed by the
/// OCEC input (`docs/faces.md` §17): 40 wide, 24 high. /// OCEC input (`docs/dev/faces.md` §17): 40 wide, 24 high.
pub const EYE_PATCH_WIDTH: usize = 40; pub const EYE_PATCH_WIDTH: usize = 40;
/// Height of an eye crop as the classifier reads it, in pixels. /// Height of an eye crop as the classifier reads it, in pixels.
pub const EYE_PATCH_HEIGHT: usize = 24; pub const EYE_PATCH_HEIGHT: usize = 24;
@@ -438,7 +438,7 @@ pub const EYE_PATCH_HEIGHT: usize = 24;
/// The classifier was trained on a whole-body detector's *eye* boxes — tight /// The classifier was trained on a whole-body detector's *eye* boxes — tight
/// round the palpebral fissure — and measured on 25 open-eyed faces from the /// round the palpebral fissure — and measured on 25 open-eyed faces from the
/// reference library, a tight box is what it wants: 22 of 25 read open at /// reference library, a tight box is what it wants: 22 of 25 read open at
/// 0 and 0.1, 18 at 0.4, 14 at 0.6 (docs/faces.md §17.2). A tenth, so a /// 0 and 0.1, 18 at 0.4, 14 at 0.6 (docs/dev/faces.md §17.2). A tenth, so a
/// contour landing a pixel short of the lashes still holds them. /// contour landing a pixel short of the lashes still holds them.
pub const EYE_BOX_MARGIN: f32 = 0.1; pub const EYE_BOX_MARGIN: f32 = 0.1;
@@ -571,7 +571,7 @@ pub const SUNGLASSES_EDGE: usize = 48;
/// clear glasses, at 0.68. Erring towards "sunglasses" is the safe direction /// clear glasses, at 0.68. Erring towards "sunglasses" is the safe direction
/// for what this feeds: a face called sunglasses is left alone by the /// for what this feeds: a face called sunglasses is left alone by the
/// eyes-open filter, where a pair of sunglasses missed hands the eye /// eyes-open filter, where a pair of sunglasses missed hands the eye
/// classifier a lens to guess at (docs/faces.md §17). /// classifier a lens to guess at (docs/dev/faces.md §17).
pub const SUNGLASSES_WINDOWS: [(f32, f32, f32, f32); 2] = pub const SUNGLASSES_WINDOWS: [(f32, f32, f32, f32); 2] =
[(0.0, 0.0, 112.0, 112.0), (-5.0, -14.0, 122.0, 122.0)]; [(0.0, 0.0, 112.0, 112.0), (-5.0, -14.0, 122.0, 122.0)];
+3 -3
View File
@@ -1,4 +1,4 @@
//! Cosine to probability (docs/faces.md §8, FR-CULL-9). //! Cosine to probability (docs/dev/faces.md §8, FR-CULL-9).
//! //!
//! FR-CULL-9 is a hard requirement rather than an implementation detail: no //! FR-CULL-9 is a hard requirement rather than an implementation detail: no
//! code path may threshold a bare cosine, every threshold in the subsystem is //! code path may threshold a bare cosine, every threshold in the subsystem is
@@ -28,7 +28,7 @@
//! calibration to the belief it was supposed to test — and that is the whole //! calibration to the belief it was supposed to test — and that is the whole
//! of the alternative. //! of the alternative.
//! //!
//! docs/faces.md §8.1 names one more that would cost no labelling at all: two //! docs/dev/faces.md §8.1 names one more that would cost no labelling at all: two
//! faces in adjacent frames of one burst are near-certainly the same person, //! faces in adjacent frames of one burst are near-certainly the same person,
//! and FR-CULL-5's grouping is sitting there. Nothing draws on it. This crate //! and FR-CULL-5's grouping is sitting there. Nothing draws on it. This crate
//! cannot see a catalog, let alone the bursts in one — it is handed cosines by //! cannot see a catalog, let alone the bursts in one — it is handed cosines by
@@ -83,7 +83,7 @@ pub struct Calibration {
} }
impl Default for Calibration { impl Default for Calibration {
/// The reference implementation's fitted MBF curve (docs/faces.md §1): /// The reference implementation's fitted MBF curve (docs/dev/faces.md §1):
/// steepness 16.2, P=0.5 at cosine 0.267. /// steepness 16.2, P=0.5 at cosine 0.267.
/// ///
/// **`valid` is false**, and that is the point. It is a documented /// **`valid` is false**, and that is the point. It is a documented
+1 -1
View File
@@ -1,5 +1,5 @@
//! TRACES: FR-CULL-8a //! TRACES: FR-CULL-8a
//! The two small classifiers behind a face's eye state (docs/faces.md §17). //! The two small classifiers behind a face's eye state (docs/dev/faces.md §17).
//! //!
//! **OCEC** — *open closed eyes classification*, Hyodo 2025 — reads one //! **OCEC** — *open closed eyes classification*, Hyodo 2025 — reads one
//! 40×24 eye and answers P(open). **SGC** — *sunglasses classification*, //! 40×24 eye and answers P(open). **SGC** — *sunglasses classification*,
+1 -1
View File
@@ -1,4 +1,4 @@
//! Grouping faces into people (docs/faces.md §9, FR-CULL-10). //! Grouping faces into people (docs/dev/faces.md §9, FR-CULL-10).
//! //!
//! Model-free: this is arithmetic over embeddings, and it is where the //! Model-free: this is arithmetic over embeddings, and it is where the
//! subsystem's accuracy actually lives, so it is testable with no weights on //! subsystem's accuracy actually lives, so it is testable with no weights on
+4 -4
View File
@@ -1,4 +1,4 @@
//! SCRFD face detection (docs/faces.md §4). //! SCRFD face detection (docs/dev/faces.md §4).
//! //!
//! One forward pass produces a box, a confidence and **five landmarks** per //! One forward pass produces a box, a confidence and **five landmarks** per
//! face — the landmarks being the reason for this detector rather than a //! face — the landmarks being the reason for this detector rather than a
@@ -138,7 +138,7 @@ impl Detection {
pub struct Detector { pub struct Detector {
session: Model, session: Model,
/// f32 or int8 — the int8 form finds a different set of faces and is a /// f32 or int8 — the int8 form finds a different set of faces and is a
/// different detector in `model_id` (docs/inference.md §7). /// different detector in `model_id` (docs/dev/inference.md §7).
form: Form, form: Form,
/// Feature-map count: 3 for strides {8,16,32}, 4 for {8,16,32,64}. /// Feature-map count: 3 for strides {8,16,32}, 4 for {8,16,32,64}.
/// ///
@@ -346,7 +346,7 @@ fn iou(a: &(f32, f32, f32, f32), b: &(f32, f32, f32, f32)) -> f32 {
/// How the image is fitted into the graph's fixed square input. /// How the image is fitted into the graph's fixed square input.
/// ///
/// The forward and inverse mappings live in one struct on purpose: /// The forward and inverse mappings live in one struct on purpose:
/// docs/faces.md §4.1 notes that what matters is not *where* the padding goes /// docs/dev/faces.md §4.1 notes that what matters is not *where* the padding goes
/// but that the two agree. A mismatch offsets every box and landmark by the /// but that the two agree. A mismatch offsets every box and landmark by the
/// padding, producing detections that look plausible and embeddings that /// padding, producing detections that look plausible and embeddings that
/// quietly cluster badly three stages later. /// quietly cluster badly three stages later.
@@ -372,7 +372,7 @@ impl Letterbox {
/// ///
/// `(x·255 − 127.5) / 128` — note `/128`, not `/127.5`. The reference /// `(x·255 − 127.5) / 128` — note `/128`, not `/127.5`. The reference
/// implementation this is ported from uses `/128` for both models, and /// implementation this is ported from uses `/128` for both models, and
/// every measured number in docs/faces.md §1 came from it. /// every measured number in docs/dev/faces.md §1 came from it.
/// ///
/// Padding is grey, matching the reference's `114`: the value the network /// Padding is grey, matching the reference's `114`: the value the network
/// reads least as an edge, where black would draw a hard border across the /// reads least as an edge, where black would draw a hard border across the
+2 -2
View File
@@ -1,4 +1,4 @@
//! ArcFace / MobileFaceNet inference (docs/faces.md §6). //! ArcFace / MobileFaceNet inference (docs/dev/faces.md §6).
//! //!
//! Takes an aligned crop and returns 512 L2-normalised floats. The alignment is //! Takes an aligned crop and returns 512 L2-normalised floats. The alignment is
//! not optional and cannot be skipped by accident: [`Embedder::embed`] takes an //! not optional and cannot be skipped by accident: [`Embedder::embed`] takes an
@@ -66,7 +66,7 @@ impl Embedder {
pub fn from_bytes(bytes: &[u8], model: ModelId) -> Result<Self, FaceError> { pub fn from_bytes(bytes: &[u8], model: ModelId) -> Result<Self, FaceError> {
// Always the f32 form: an embedding must compare across devices // Always the f32 form: an embedding must compare across devices
// (docs/inference.md §7), and the engine pins this role to it. // (docs/dev/inference.md §7), and the engine pins this role to it.
let loaded = dr_inference_engine::open(Role::Embedder, Form::F32, bytes)?; let loaded = dr_inference_engine::open(Role::Embedder, Form::F32, bytes)?;
let acquired = loaded.acquire()?; let acquired = loaded.acquire()?;
let session = acquired.lock(); let session = acquired.lock();
+2 -2
View File
@@ -1,4 +1,4 @@
//! What an embedder produces, and how it is stored (docs/faces.md §6). //! What an embedder produces, and how it is stored (docs/dev/faces.md §6).
//! //!
//! Deliberately **model-free**: the vector, its identity, its comparison and //! Deliberately **model-free**: the vector, its identity, its comparison and
//! its storage encoding are arithmetic, and `calibrate` and `cluster` are built //! its storage encoding are arithmetic, and `calibrate` and `cluster` are built
@@ -273,7 +273,7 @@ mod tests {
); );
} }
/// The claim docs/faces.md §6 makes about the storage format: the f16 /// The claim docs/dev/faces.md §6 makes about the storage format: the f16
/// round-trip costs ~1e-3 of cosine, three orders below the separation /// round-trip costs ~1e-3 of cosine, three orders below the separation
/// between a match and a non-match. /// between a match and a non-match.
#[test] #[test]
+3 -3
View File
@@ -67,14 +67,14 @@ pub const SUNGLASSES_THRESHOLD: f32 = 0.5;
/// The classifier was trained on eyes down to about a dozen pixels wide /// The classifier was trained on eyes down to about a dozen pixels wide
/// (its reference footage averaged 15–21); below that the 40-pixel patch is /// (its reference footage averaged 15–21); below that the 40-pixel patch is
/// an interpolation of nothing, and the answer is noise that reads as /// an interpolation of nothing, and the answer is noise that reads as
/// "closed". docs/faces.md §17.3 has the measurement behind the number. /// "closed". docs/dev/faces.md §17.3 has the measurement behind the number.
pub const MIN_EYE_PX: f32 = 12.0; pub const MIN_EYE_PX: f32 = 12.0;
/// Least [`Eye::sharpness`] for the eye to be read. /// Least [`Eye::sharpness`] for the eye to be read.
/// ///
/// The same measure as the face's `min_sharpness`, over the eye patch, and /// The same measure as the face's `min_sharpness`, over the eye patch, and
/// chosen the same way: the value under which the open-eyed faces of the /// chosen the same way: the value under which the open-eyed faces of the
/// reference sample were being called closed. docs/faces.md §17.3. /// reference sample were being called closed. docs/dev/faces.md §17.3.
pub const MIN_EYE_SHARPNESS: f32 = 0.02; pub const MIN_EYE_SHARPNESS: f32 = 0.02;
/// An eye narrower than this fraction of its partner is the far eye of a /// An eye narrower than this fraction of its partner is the far eye of a
@@ -82,7 +82,7 @@ pub const MIN_EYE_SHARPNESS: f32 = 0.02;
/// ///
/// A landmark model's contour for a hidden eye collapses towards the nose. /// A landmark model's contour for a hidden eye collapses towards the nose.
/// Measured on twenty native renders of the reference library /// Measured on twenty native renders of the reference library
/// (docs/faces.md §17.4): profiles put the far eye at 0.02–0.43 of the near /// (docs/dev/faces.md §17.4): profiles put the far eye at 0.02–0.43 of the near
/// one, two three-quarter faces whose far eye read closed sat at 0.54, and /// one, two three-quarter faces whose far eye read closed sat at 0.54, and
/// every face looking at the camera — winks included, since a shut eye's /// every face looking at the camera — winks included, since a shut eye's
/// box keeps its width — sat at 0.78 or more. 0.6 splits the gap. /// box keeps its width — sat at 0.78 or more. 0.6 splits the gap.
+1 -1
View File
@@ -1,5 +1,5 @@
//! TRACES: FR-CULL-8a //! TRACES: FR-CULL-8a
//! Dense facial landmarks — InsightFace's `2d106det` (docs/faces.md §17.2). //! Dense facial landmarks — InsightFace's `2d106det` (docs/dev/faces.md §17.2).
//! //!
//! SCRFD's five points place a face; they do not place an eye. Its eye //! SCRFD's five points place a face; they do not place an eye. Its eye
//! point is loose enough that a window centred on it left the eye in a //! point is loose enough that a window centred on it left the eye in a
+4 -4
View File
@@ -1,4 +1,4 @@
//! Faces and identity (S14, docs/faces.md). //! Faces and identity (S14, docs/dev/faces.md).
//! //!
//! Two models, run over the native render, producing per face a box, five //! Two models, run over the native render, producing per face a box, five
//! landmarks, a confidence and a 512-d embedding (FR-CULL-8) — and then the //! landmarks, a confidence and a 512-d embedding (FR-CULL-8) — and then the
@@ -21,9 +21,9 @@
//! for a packaging script to switch on. The application obtains a model at //! for a packaging script to switch on. The application obtains a model at
//! runtime; this crate takes bytes and never fetches anything. //! runtime; this crate takes bytes and never fetches anything.
//! //!
//! docs/faces.md §2 is the full reading, including what would have to change //! docs/dev/faces.md §2 is the full reading, including what would have to change
//! for that to stop being true. The eye-state models are the exception: MIT, //! for that to stop being true. The eye-state models are the exception: MIT,
//! weights and all, and shipped in `models/face/` (docs/faces.md §17). //! weights and all, and shipped in `models/face/` (docs/dev/faces.md §17).
//! //!
//! # Why the runtime is split behind a feature //! # Why the runtime is split behind a feature
//! //!
@@ -55,7 +55,7 @@ pub mod references;
/// Smallest long edge a face crop may be sampled from. /// Smallest long edge a face crop may be sampled from.
/// ///
/// **A floor on the crop source, not on the detector input.** The distinction /// **A floor on the crop source, not on the detector input.** The distinction
/// is the whole of FR-CULL-8 and `docs/faces.md` §7: detection letterboxes /// is the whole of FR-CULL-8 and `docs/dev/faces.md` §7: detection letterboxes
/// every buffer into 640×640, so its input resolution decides nothing, while /// every buffer into 640×640, so its input resolution decides nothing, while
/// [`warp`] samples the 112×112 the embedder sees and so converts source /// [`warp`] samples the 112×112 the embedder sees and so converts source
/// resolution directly into embedding quality. FR-CULL-8 requires that crop to /// resolution directly into embedding quality. FR-CULL-8 requires that crop to
+1 -1
View File
@@ -115,7 +115,7 @@ pub struct Faces<'a> {
/// Source pixels across the aligned crop, for the calibration's size term. /// Source pixels across the aligned crop, for the calibration's size term.
pub crop_px: &'a [f32], pub crop_px: &'a [f32],
/// Which photograph each face came from. Two faces in one frame are not /// Which photograph each face came from. Two faces in one frame are not
/// the same person, so those pairs are never returned (docs/faces.md §9). /// the same person, so those pairs are never returned (docs/dev/faces.md §9).
pub images: &'a [u64], pub images: &'a [u64],
/// Which faces may be compared *against* — the gallery /// Which faces may be compared *against* — the gallery
/// ([`crate::embedding::MIN_GALLERY_QUALITY`]). /// ([`crate::embedding::MIN_GALLERY_QUALITY`]).
+2 -2
View File
@@ -1,6 +1,6 @@
//! What a frame actually costs — the measurement FR-DSP-2 is waiting on. //! What a frame actually costs — the measurement FR-DSP-2 is waiting on.
//! //!
//! `docs/display-and-extension.md` §2 argues that tiled computation predates //! `docs/dev/display-and-extension.md` §2 argues that tiled computation predates
//! the fused-shader design and may not need to exist: the composer folds every //! the fused-shader design and may not need to exist: the composer folds every
//! active operation into **one dispatch over a viewport-sized target**, so the //! active operation into **one dispatch over a viewport-sized target**, so the
//! problem tiles were invented to solve may already be solved. That argument //! problem tiles were invented to solve may already be solved. That argument
@@ -28,7 +28,7 @@
//! the per-frame CPU half is dominated by shader-source assembly, which is //! the per-frame CPU half is dominated by shader-source assembly, which is
//! string formatting and is several times slower unoptimised. //! string formatting and is several times slower unoptimised.
//! //!
//! The committed numbers live in `docs/frame-budget.md`. Rerun this and diff //! The committed numbers live in `docs/dev/frame-budget.md`. Rerun this and diff
//! that file; a regression should be a diff rather than somebody's memory. //! that file; a regression should be a diff rather than somebody's memory.
//! //!
//! # Why the 99th percentile and not the mean //! # Why the 99th percentile and not the mean
+1 -1
View File
@@ -1,6 +1,6 @@
//! Segment an image and write the granularity ladder as false-coloured PPMs. //! Segment an image and write the granularity ladder as false-coloured PPMs.
//! //!
//! The whole point of S15 step 2 (docs/segmentation.md §11): look at the //! The whole point of S15 step 2 (docs/dev/segmentation.md §11): look at the
//! ladder and decide whether clicking through it would land on the things a //! ladder and decide whether clicking through it would land on the things a
//! person means. No amount of design settles that — the pictures do. //! person means. No amount of design settles that — the pictures do.
//! //!
+7 -7
View File
@@ -1,4 +1,4 @@
//! Watershed segmentation — arm A's GPU half (S15, docs/segmentation.md). //! Watershed segmentation — arm A's GPU half (S15, docs/dev/segmentation.md).
//! //!
//! Runs the five passes in `shaders/watershed.wgsl` over a demosaiced image //! Runs the five passes in `shaders/watershed.wgsl` over a demosaiced image
//! and leaves a basin label per pixel on the GPU. The hierarchy built from //! and leaves a basin label per pixel on the GPU. The hierarchy built from
@@ -20,7 +20,7 @@
//! one AC-8 forbids is per frame in the render loop, and sharing a switch //! one AC-8 forbids is per frame in the render loop, and sharing a switch
//! would force a build wanting local masking to unlock the other. //! would force a build wanting local masking to unlock the other.
//! //!
//! It is still a real cost and still unfinished. F3 in docs/segmentation.md //! It is still a real cost and still unfinished. F3 in docs/dev/segmentation.md
//! §12 stands: the adjacency accumulation belongs GPU-side with atomics, and //! §12 stands: the adjacency accumulation belongs GPU-side with atomics, and
//! until it moves there every segmentation pays a full-resolution transfer. //! until it moves there every segmentation pays a full-resolution transfer.
//! Read the feature name as a description of a known gap rather than as //! Read the feature name as a description of a known gap rather than as
@@ -36,7 +36,7 @@ pub struct SegmentOptions {
/// Longest proxy edge. The segmentation runs here, not at sensor /// Longest proxy edge. The segmentation runs here, not at sensor
/// resolution: a 24 MP watershed costs 12× the memory to place boundaries /// resolution: a 24 MP watershed costs 12× the memory to place boundaries
/// a person cannot see, and the boundary refinement that matters at 1:1 /// a person cannot see, and the boundary refinement that matters at 1:1
/// is a separate stage (docs/segmentation.md §4). /// is a separate stage (docs/dev/segmentation.md §4).
pub max_edge: u32, pub max_edge: u32,
/// Pre-smoothing radius in proxy pixels. The caller's to raise with ISO — /// Pre-smoothing radius in proxy pixels. The caller's to raise with ISO —
/// this is the single knob that decides whether a noisy file segments /// this is the single knob that decides whether a noisy file segments
@@ -69,7 +69,7 @@ impl Default for SegmentOptions {
w_chroma: 0.5, w_chroma: 0.5,
// **Zero: the pass is off.** It is implemented, dispatched // **Zero: the pass is off.** It is implemented, dispatched
// correctly and measurably changes nothing — see the ignored test // correctly and measurably changes nothing — see the ignored test
// below and §12 of docs/segmentation.md. Until that is understood, // below and §12 of docs/dev/segmentation.md. Until that is understood,
// running it would buy 64 dispatches per segmentation and no // running it would buy 64 dispatches per segmentation and no
// improvement, so the default declines to pay. // improvement, so the default declines to pay.
plateau_iterations: 0, plateau_iterations: 0,
@@ -486,7 +486,7 @@ impl Segmentation {
/// a region graph of a few thousand nodes that every later interaction /// a region graph of a few thousand nodes that every later interaction
/// reads from the CPU anyway. /// reads from the CPU anyway.
/// ///
/// What it is *not* is finished. F3 in docs/segmentation.md §12 stands: /// What it is *not* is finished. F3 in docs/dev/segmentation.md §12 stands:
/// the adjacency accumulation belongs on the GPU with atomics, and until /// the adjacency accumulation belongs on the GPU with atomics, and until
/// it moves there a segmentation costs one full-resolution transfer of the /// it moves there a segmentation costs one full-resolution transfer of the
/// label and gradient buffers. That is a real cost on a phone and the /// label and gradient buffers. That is a real cost on a phone and the
@@ -724,7 +724,7 @@ mod tests {
px px
} }
#[test] #[test]
#[ignore = "the plateau pass is a measured no-op; see docs/segmentation.md §12"] #[ignore = "the plateau pass is a measured no-op; see docs/dev/segmentation.md §12"]
fn lower_completion_drains_a_plateau_instead_of_shattering_it() { fn lower_completion_drains_a_plateau_instead_of_shattering_it() {
// F1, asserted rather than eyeballed, and asserted at the level where // F1, asserted rather than eyeballed, and asserted at the level where
// it matters. // it matters.
@@ -741,7 +741,7 @@ mod tests {
// with no exit anywhere — cannot be drained by a distance that has // with no exit anywhere — cannot be drained by a distance that has
// nowhere to descend to, and collapsing it fully would need connected // nowhere to descend to, and collapsing it fully would need connected
// component labelling rather than a local rule. It is not worth it: // component labelling rather than a local rule. It is not worth it:
// see docs/segmentation.md §12. // see docs/dev/segmentation.md §12.
let Some(ctx) = ctx() else { return }; let Some(ctx) = ctx() else { return };
let (w, h) = (96u32, 96u32); let (w, h) = (96u32, 96u32);
let src = DemosaicedImage::from_rgba8(&ctx, &ramp(w, h), w, h).expect("source"); let src = DemosaicedImage::from_rgba8(&ctx, &ramp(w, h), w, h).expect("source");
+2 -2
View File
@@ -1,4 +1,4 @@
// Watershed segmentation — the passes behind arm A of S15 (docs/segmentation.md). // Watershed segmentation — the passes behind arm A of S15 (docs/dev/segmentation.md).
// //
// Seven entry points forming one chain: // Seven entry points forming one chain:
// //
@@ -197,7 +197,7 @@ fn gradient(@builtin(global_invocation_id) gid: vec3<u32>) {
// lowest-indexed neighbour, which is up and to the left. Each pixel therefore // lowest-indexed neighbour, which is up and to the left. Each pixel therefore
// walks diagonally until it falls off the plateau, and one flat region becomes // walks diagonally until it falls off the plateau, and one flat region becomes
// a fan of diagonal chains rather than one basin — visible as hatching across // a fan of diagonal chains rather than one basin — visible as hatching across
// what should be a single area (docs/segmentation.md §12, F1). // what should be a single area (docs/dev/segmentation.md §12, F1).
// //
// The fix is the standard lower-completion: give each plateau pixel its // The fix is the standard lower-completion: give each plateau pixel its
// geodesic distance to the nearest pixel that *does* have a lower neighbour, // geodesic distance to the nearest pixel that *does* have a lower neighbour,
+5 -5
View File
@@ -2,11 +2,11 @@
//! //!
//! FR-DSP-3 says a slider updates the visible region within one frame budget at //! FR-DSP-3 says a slider updates the visible region within one frame budget at
//! proxy resolution. Until this file existed nothing checked it, which made it //! proxy resolution. Until this file existed nothing checked it, which made it
//! a wish — `docs/display-and-extension.md` §3 is blunt about that, and §7 is //! a wish — `docs/dev/display-and-extension.md` §3 is blunt about that, and §7 is
//! blunt about what tagging an unchecked requirement does to the coverage //! blunt about what tagging an unchecked requirement does to the coverage
//! figure. //! figure.
//! //!
//! The measurements this guards are in [`docs/frame-budget.md`], produced by //! The measurements this guards are in [`docs/dev/frame-budget.md`], produced by
//! `examples/frame_budget.rs`. This file is the part of them that has to keep //! `examples/frame_budget.rs`. This file is the part of them that has to keep
//! being true: it renders the **whole point-operation chain** through the real //! being true: it renders the **whole point-operation chain** through the real
//! `render_detailed` for a hundred frames, moving a slider between each, and //! `render_detailed` for a hundred frames, moving a slider between each, and
@@ -17,7 +17,7 @@
//! **The neighbourhood stage is deliberately not in the asserted chain.** It is //! **The neighbourhood stage is deliberately not in the asserted chain.** It is
//! over the budget today — clarity alone is 34 ms at 4K, because its kernel is //! over the budget today — clarity alone is 34 ms at 4K, because its kernel is
//! a fraction of the frame and reaches a 52-pixel radius there — and //! a fraction of the frame and reaches a 52-pixel radius there — and
//! `docs/frame-budget.md` records that, names the fix (a base computed at //! `docs/dev/frame-budget.md` records that, names the fix (a base computed at
//! reduced resolution) and does not pretend otherwise. Asserting a budget the //! reduced resolution) and does not pretend otherwise. Asserting a budget the
//! code does not meet would produce a red suite that everyone learns to ignore; //! code does not meet would produce a red suite that everyone learns to ignore;
//! asserting it on a chain that quietly excluded the expensive stage *without //! asserting it on a chain that quietly excluded the expensive stage *without
@@ -81,7 +81,7 @@ const SOURCE: (u32, u32) = (6000, 4000);
/// The viewport the budget is asserted at: a 16:10 desktop display. /// The viewport the budget is asserted at: a 16:10 desktop display.
/// ///
/// Not 4K, and the reason is worth stating. At 4K the fused chain still passes /// Not 4K, and the reason is worth stating. At 4K the fused chain still passes
/// with room to spare (4.5 ms of GPU; see `docs/frame-budget.md`), but a test /// with room to spare (4.5 ms of GPU; see `docs/dev/frame-budget.md`), but a test
/// that renders 8.3 M pixels a hundred times twice over is four seconds of /// that renders 8.3 M pixels a hundred times twice over is four seconds of
/// suite time to re-establish a conclusion 4.1 M pixels already establishes. /// suite time to re-establish a conclusion 4.1 M pixels already establishes.
const VIEWPORT: (u32, u32) = (2560, 1600); const VIEWPORT: (u32, u32) = (2560, 1600);
@@ -166,7 +166,7 @@ impl Run {
judged <= BUDGET_MS, judged <= BUDGET_MS,
"{case} at {}x{}: p99 of {FRAMES} frames was {judged:.2} ms, over the \ "{case} at {}x{}: p99 of {FRAMES} frames was {judged:.2} ms, over the \
{BUDGET_MS:.0} ms budget (cpu {:.2} ms, gpu {:.2} ms, total {:.2} ms). \ {BUDGET_MS:.0} ms budget (cpu {:.2} ms, gpu {:.2} ms, total {:.2} ms). \
FR-DSP-3 is what this violates; docs/frame-budget.md holds the \ FR-DSP-3 is what this violates; docs/dev/frame-budget.md holds the \
numbers it used to be.", numbers it used to be.",
viewport.0, viewport.0,
viewport.1, viewport.1,
+1 -1
View File
@@ -326,7 +326,7 @@ fn a_proxy_and_an_export_agree_about_the_effect() {
#[test] #[test]
fn crossing_the_reduction_threshold_does_not_change_the_picture() { fn crossing_the_reduction_threshold_does_not_change_the_picture() {
// TRACES: FR-DSP-3 — `docs/technical-debt.md` TD-4, held in pixels. // TRACES: FR-DSP-3 — `docs/dev/technical-debt.md` TD-4, held in pixels.
// //
// Clarity's base is computed on a reduced grid, and how reduced depends on // Clarity's base is computed on a reduced grid, and how reduced depends on
// the viewport: `LocalContrast::reduction` steps 4 -> 2 -> 1 as sigma // the viewport: `LocalContrast::reduction` steps 4 -> 2 -> 1 as sigma
+1 -1
View File
@@ -10,7 +10,7 @@
//! code path — the zoom is the full-resolution path — which is why the //! code path — the zoom is the full-resolution path — which is why the
//! requirement has been satisfied for some time without anyone tagging it. //! requirement has been satisfied for some time without anyone tagging it.
//! //!
//! `docs/display-and-extension.md` §7 is the reason this file exists rather //! `docs/dev/display-and-extension.md` §7 is the reason this file exists rather
//! than a tag on `framing.rs`: a requirement counts as covered when a `TRACES` //! than a tag on `framing.rs`: a requirement counts as covered when a `TRACES`
//! comment names it, and nothing checks that the code under the tag does the //! comment names it, and nothing checks that the code under the tag does the
//! thing. `FR-DEV-8` is tagged against plumbing a future operation would use. //! thing. `FR-DEV-8` is tagged against plumbing a future operation would use.
+1 -1
View File
@@ -6,7 +6,7 @@ rust-version.workspace = true
license.workspace = true license.workspace = true
# The one crate that names a runtime, a provider, a vendor library or a # The one crate that names a runtime, a provider, a vendor library or a
# device (docs/inference.md §8). `dr-face` and `dr-segment` ask it for a # device (docs/dev/inference.md §8). `dr-face` and `dr-segment` ask it for a
# session by role and never see which of these answered. # session by role and never see which of these answered.
[dependencies] [dependencies]
+1 -1
View File
@@ -1,4 +1,4 @@
//! The API table `ort` runs on, chosen once (docs/inference.md §3). //! The API table `ort` runs on, chosen once (docs/dev/inference.md §3).
//! //!
//! `ort` with `alternative-backend` links no runtime and asks, on first use, //! `ort` with `alternative-backend` links no runtime and asks, on first use,
//! for an `OrtApi` — a struct of function pointers. Two things can fill it: //! for an `OrtApi` — a struct of function pointers. Two things can fill it:
+1 -1
View File
@@ -1,5 +1,5 @@
//! Compiled engines: what a rung builds once per device, and the thread that //! Compiled engines: what a rung builds once per device, and the thread that
//! builds them before anyone asks (docs/inference.md §5, §6). //! builds them before anyone asks (docs/dev/inference.md §5, §6).
//! //!
//! TensorRT keeps its own engine cache keyed by graph hash; QNN writes a //! TensorRT keeps its own engine cache keyed by graph hash; QNN writes a
//! context model. Both are opaque to this crate, which tracks only *that* a //! context model. Both are opaque to this crate, which tracks only *that* a
+5 -5
View File
@@ -1,5 +1,5 @@
//! Which runtime, which provider and which model form — decided once per //! Which runtime, which provider and which model form — decided once per
//! device, and the only crate that knows the answer (docs/inference.md). //! device, and the only crate that knows the answer (docs/dev/inference.md).
//! //!
//! Consumers ask for a session by [`Role`] and get `ort`'s `Session` back; //! Consumers ask for a session by [`Role`] and get `ort`'s `Session` back;
//! what built it — tract on one core, ONNX Runtime's CPU pool, a TensorRT //! what built it — tract on one core, ONNX Runtime's CPU pool, a TensorRT
@@ -35,13 +35,13 @@ pub enum Role {
Embedder, Embedder,
Segmenter, Segmenter,
Scene, Scene,
/// The dense landmark model behind the eye reading (docs/faces.md §7c). /// The dense landmark model behind the eye reading (docs/dev/faces.md §7c).
Landmarks, Landmarks,
/// The eye-state and sunglasses classifiers, a few hundred kilobytes. /// The eye-state and sunglasses classifiers, a few hundred kilobytes.
EyeClassifier, EyeClassifier,
/// XFeat, the panorama keypoint detector (docs/panorama.md). /// XFeat, the panorama keypoint detector (docs/dev/panorama.md).
Keypoints, Keypoints,
/// MI-GAN, the panorama border filler (docs/panorama.md §12). Plain /// MI-GAN, the panorama border filler (docs/dev/panorama.md §12). Plain
/// convolutions, so any rung serves it; fp16 on TensorRT and int8 on /// convolutions, so any rung serves it; fp16 on TensorRT and int8 on
/// the Hexagon are the point of it. /// the Hexagon are the point of it.
Inpainter, Inpainter,
@@ -499,7 +499,7 @@ mod tests {
/// The smallest shipped graph, if this checkout has the weights; a test /// The smallest shipped graph, if this checkout has the weights; a test
/// suite that needs a research-licensed download is one that does not /// suite that needs a research-licensed download is one that does not
/// run in CI (docs/faces.md §3), so absence is a skip. /// run in CI (docs/dev/faces.md §3), so absence is a skip.
fn probe_bytes() -> Option<Vec<u8>> { fn probe_bytes() -> Option<Vec<u8>> {
let path = concat!( let path = concat!(
env!("CARGO_MANIFEST_DIR"), env!("CARGO_MANIFEST_DIR"),
+1 -1
View File
@@ -1,4 +1,4 @@
//! Walk the ladder, once, by building real sessions (docs/inference.md §4). //! Walk the ladder, once, by building real sessions (docs/dev/inference.md §4).
//! //!
//! A rung is taken when a session builds on it, runs, and is faster than //! A rung is taken when a session builds on it, runs, and is faster than
//! the floor. Both halves matter: a provider can register and then fail at //! the floor. Both halves matter: a provider can register and then fail at
+1 -1
View File
@@ -1,4 +1,4 @@
//! One session builder per rung (docs/inference.md §2, §7, §9). //! One session builder per rung (docs/dev/inference.md §2, §7, §9).
use ort::session::Session; use ort::session::Session;
+1 -1
View File
@@ -13,7 +13,7 @@ log.workspace = true
# Inference for the learned keypoint detector, on the same footing as # Inference for the learned keypoint detector, on the same footing as
# `dr-segment`: `ort` is the API, `dr-inference-engine` decides what runs # `dr-segment`: `ort` is the API, `dr-inference-engine` decides what runs
# it (docs/inference.md), and both are optional so that the geometry — # it (docs/dev/inference.md), and both are optional so that the geometry —
# matching, the rotation solve, the projections — is a dependency-free crate # matching, the rotation solve, the projections — is a dependency-free crate
# that tests without a model. # that tests without a model.
ort = { workspace = true, optional = true } ort = { workspace = true, optional = true }
+1 -1
View File
@@ -95,7 +95,7 @@ pub struct Params {
/// and, beyond the band being filled, still unknown. That is what the /// and, beyond the band being filled, still unknown. That is what the
/// shipped model was trained on (a fine-tune of MI-GAN on voids cut /// shipped model was trained on (a fine-tune of MI-GAN on voids cut
/// from photographs the way a cylindrical merge cuts them, see /// from photographs the way a cylindrical merge cuts them, see
/// `docs/panorama.md` §14); a ring would give it a fold to continue. /// `docs/dev/panorama.md` §14); a ring would give it a fold to continue.
/// ///
/// Non-zero is the stock model's crutch: a plain reflection of a deep /// Non-zero is the stock model's crutch: a plain reflection of a deep
/// hole pulls in whatever is that far from the edge — a ridge, a peak — /// hole pulls in whatever is that far from the edge — a ridge, a peak —
+1 -1
View File
@@ -9,7 +9,7 @@
//! on every rung, and what they cost is the whole story of whether a fill //! on every rung, and what they cost is the whole story of whether a fill
//! is interactive: 7.4 s a tile under tract, 0.4 s under ONNX Runtime's //! is interactive: 7.4 s a tile under tract, 0.4 s under ONNX Runtime's
//! CPU pool, 23 ms in fp16 and 13 ms in int8 on a laptop's TensorRT //! CPU pool, 23 ms in fp16 and 13 ms in int8 on a laptop's TensorRT
//! (2026-09-19, docs/panorama.md §12). //! (2026-09-19, docs/dev/panorama.md §12).
//! //!
//! The model's contract, from the reference `export_inference_model.py`: //! The model's contract, from the reference `export_inference_model.py`:
//! input `1×4×512×512` float — channel 0 is `mask − 0.5` with 1 where the //! input `1×4×512×512` float — channel 0 is `mask − 0.5` with 1 where the
+2 -2
View File
@@ -4,7 +4,7 @@
//! Apache-2.0 weights (`models/LICENCE.md`), exported at a fixed shape by //! Apache-2.0 weights (`models/LICENCE.md`), exported at a fixed shape by
//! `tools/export-xfeat.sh` and loaded through the same `dr-inference-engine` //! `tools/export-xfeat.sh` and loaded through the same `dr-inference-engine`
//! `dr-segment` and `dr-face` use, so this adds no runtime and no C to the //! `dr-segment` and `dr-face` use, so this adds no runtime and no C to the
//! tree; what runs it is the device's business (docs/inference.md). ~300 ms //! tree; what runs it is the device's business (docs/dev/inference.md). ~300 ms
//! per frame on tract on the reference desktop, ~400 ms on the tablet //! per frame on tract on the reference desktop, ~400 ms on the tablet
//! (S15.2, S15.4). //! (S15.2, S15.4).
@@ -37,7 +37,7 @@ pub struct XFeat {
} }
/// The bytes of both exports compiled into the binary, for whoever compiles /// The bytes of both exports compiled into the binary, for whoever compiles
/// engines ahead of the first request (docs/inference.md §6). /// engines ahead of the first request (docs/dev/inference.md §6).
#[cfg(feature = "embedded-model")] #[cfg(feature = "embedded-model")]
pub fn embedded_model_bytes() -> [&'static [u8]; 2] { pub fn embedded_model_bytes() -> [&'static [u8]; 2] {
[EMBEDDED_LANDSCAPE, EMBEDDED_PORTRAIT] [EMBEDDED_LANDSCAPE, EMBEDDED_PORTRAIT]
+2 -2
View File
@@ -315,7 +315,7 @@ pub struct DetailPass {
/// 52 render pixels at 4K — holds no spatial frequency a quarter-scale /// 52 render pixels at 4K — holds no spatial frequency a quarter-scale
/// grid cannot represent. Computing it at the render size therefore buys /// grid cannot represent. Computing it at the render size therefore buys
/// nothing and costs everything: 105 taps over 8.3 M pixels, twice, which /// nothing and costs everything: 105 taps over 8.3 M pixels, twice, which
/// measured at 34 ms and is where `docs/technical-debt.md` TD-4 came from. /// measured at 34 ms and is where `docs/dev/technical-debt.md` TD-4 came from.
/// At a quarter it is a sixteenth of the pixels at a quarter of the /// At a quarter it is a sixteenth of the pixels at a quarter of the
/// radius, and the result is not an approximation of the full-resolution /// radius, and the result is not an approximation of the full-resolution
/// base — it is the same band-limited function, sampled where it is still /// base — it is the same band-limited function, sampled where it is still
@@ -568,7 +568,7 @@ pub fn compose_detail(
/// photograph the photographer thinks they are sharpening. /// photograph the photographer thinks they are sharpening.
/// ///
/// It also means ARCH §5.2's stage list, which draws spot removal after /// It also means ARCH §5.2's stage list, which draws spot removal after
/// texture and clarity, is not what this does — see `docs/spot-removal.md` /// texture and clarity, is not what this does — see `docs/dev/spot-removal.md`
/// §5.1, which is where the disagreement is written down. /// §5.1, which is where the disagreement is written down.
pub fn compose_detail_with( pub fn compose_detail_with(
ops: &[Box<dyn Operation>], ops: &[Box<dyn Operation>],
+2 -2
View File
@@ -113,7 +113,7 @@ pub struct EditGraph {
/// a sidecar comes to name one stock while the shader draws another. /// a sidecar comes to name one stock while the shader draws another.
film: Option<Film>, film: Option<Film>,
/// TRACES: FR-DEV-8 /// TRACES: FR-DEV-8
/// The repairs (`docs/spot-removal.md`). /// The repairs (`docs/dev/spot-removal.md`).
/// ///
/// Apart from `ops` for the third time and the same reason: a spot is not /// Apart from `ops` for the third time and the same reason: a spot is not
/// a scalar, and a list of them is not a slider. It sits beside the masks /// a scalar, and a list of them is not a slider. It sits beside the masks
@@ -122,7 +122,7 @@ pub struct EditGraph {
/// photograph comes from. /// photograph comes from.
spots: SpotSet, spots: SpotSet,
/// The lens corrections that rewrite coordinates: distortion and lateral /// The lens corrections that rewrite coordinates: distortion and lateral
/// chromatic aberration (`docs/architecture.md` §5.2). /// chromatic aberration (`docs/dev/architecture.md` §5.2).
/// ///
/// Apart from `ops` for the fourth time, and this one is not about shape /// Apart from `ops` for the fourth time, and this one is not about shape
/// but about direction. Every [`Operation`] is a function from colour to /// but about direction. Every [`Operation`] is a function from colour to
+5 -5
View File
@@ -27,7 +27,7 @@
//! [`MaskSource::Regions`] stores integers naming regions in the segmentation //! [`MaskSource::Regions`] stores integers naming regions in the segmentation
//! hierarchy (`dr-segment`). That choice is what makes a mask diffable, cheap //! hierarchy (`dr-segment`). That choice is what makes a mask diffable, cheap
//! in a sidecar, and mergeable per-field under FR-NC-9 — three properties a //! in a sidecar, and mergeable per-field under FR-NC-9 — three properties a
//! stored raster has none of (docs/segmentation.md §1). Two devices that //! stored raster has none of (docs/dev/segmentation.md §1). Two devices that
//! select the same subject produce the same small sorted list, and a sync //! select the same subject produce the same small sorted list, and a sync
//! conflict between them is resolvable rather than a binary blob fight. //! conflict between them is resolvable rather than a binary blob fight.
//! //!
@@ -564,7 +564,7 @@ pub enum MaskSource {
/// This is what the watershed and the semantic model exist to produce. /// This is what the watershed and the semantic model exist to produce.
/// Selecting a subject means "the regions the model's instance covers", /// Selecting a subject means "the regions the model's instance covers",
/// and the resulting edge is the watershed's, which is to say the image's /// and the resulting edge is the watershed's, which is to say the image's
/// own (docs/segmentation.md §5). /// own (docs/dev/segmentation.md §5).
Regions { Regions {
/// Which segmentation these ids index into. /// Which segmentation these ids index into.
/// ///
@@ -590,7 +590,7 @@ pub enum MaskSource {
/// **The primary way a local adjustment is made.** The watershed hierarchy /// **The primary way a local adjustment is made.** The watershed hierarchy
/// this crate was first built around does not survive a photograph: its /// this crate was first built around does not survive a photograph: its
/// saddles are near zero almost everywhere, so a global cut collapses the /// saddles are near zero almost everywhere, so a global cut collapses the
/// frame into one region plus noise (docs/segmentation.md §15). A model /// frame into one region plus noise (docs/dev/segmentation.md §15). A model
/// instance is a whole object, found as one thing, and needs no ladder. /// instance is a whole object, found as one thing, and needs no ladder.
/// ///
/// The trade is that the boundary is the model's — a quarter-resolution /// The trade is that the boundary is the model's — a quarter-resolution
@@ -625,7 +625,7 @@ pub enum MaskSource {
/// reason both exist. A subject is *one* instance — this dog, not that one /// reason both exist. A subject is *one* instance — this dog, not that one
/// — found by a COCO-trained instance model. A category is *all* the sky, /// — found by a COCO-trained instance model. A category is *all* the sky,
/// or all the foliage, from an ADE20K-trained semantic model that has no /// or all the foliage, from an ADE20K-trained semantic model that has no
/// notion of instances at all (docs/segmentation.md §16). /// notion of instances at all (docs/dev/segmentation.md §16).
/// ///
/// So this is what a global grade attaches to: lift the sky, desaturate /// So this is what a global grade attaches to: lift the sky, desaturate
/// the vegetation, warm the architecture. Asking it for "that person /// the vegetation, warm the architecture. Asking it for "that person
@@ -2231,7 +2231,7 @@ fn reveal_block(slot: usize, layer: &MaskLayer, style: RevealStyle, colour: [f32
/// **not** a hash of the label field: that would be a readback on a path that /// **not** a hash of the label field: that would be a readback on a path that
/// must not have one (ARCH §6.1), and would also make the signature depend on /// must not have one (ARCH §6.1), and would also make the signature depend on
/// float arithmetic whose cross-vendor determinism is exactly the open /// float arithmetic whose cross-vendor determinism is exactly the open
/// question (docs/segmentation.md §6, M5). /// question (docs/dev/segmentation.md §6, M5).
pub fn segmentation_signature(width: u32, height: u32, regions: u32, tuning: u64) -> u64 { pub fn segmentation_signature(width: u32, height: u32, regions: u32, tuning: u64) -> u64 {
// FNV-1a over the four fields. Small, dependency-free, and adequate: this // FNV-1a over the four fields. Small, dependency-free, and adequate: this
// guards against accidental mismatch, not against a forged sidecar. // guards against accidental mismatch, not against a forged sidecar.
+1 -1
View File
@@ -54,7 +54,7 @@ pub enum Affects {
/// A pixel's *neighbourhood* — sharpening, noise reduction, clarity, /// A pixel's *neighbourhood* — sharpening, noise reduction, clarity,
/// texture, dehaze, spot removal. /// texture, dehaze, spot removal.
/// ///
/// The seam `docs/requirements.md` §3.3 designed and nothing cut until /// The seam `docs/dev/requirements.md` §3.3 designed and nothing cut until
/// [`crate::detail`] existed. It is a separate variant rather than a flavour /// [`crate::detail`] existed. It is a separate variant rather than a flavour
/// of `Colour` because it is a separate *dispatch*: a fragment in the fused /// of `Colour` because it is a separate *dispatch*: a fragment in the fused
/// pass is handed a colour and has no way back to a coordinate, so a /// pass is handed a colour and has no way back to a coordinate, so a
+1 -1
View File
@@ -99,7 +99,7 @@
//! A minimum over a patch is separable, as a Gaussian is: minimum along x, //! A minimum over a patch is separable, as a Gaussian is: minimum along x,
//! then along y. That alone is not enough. The patch is 1% of the shorter edge //! then along y. That alone is not enough. The patch is 1% of the shorter edge
//! — 61 taps across at 4K — and two passes of 61 taps is the arithmetic that //! — 61 taps across at 4K — and two passes of 61 taps is the arithmetic that
//! measured 34 ms for clarity and became `docs/technical-debt.md` TD-4. //! measured 34 ms for clarity and became `docs/dev/technical-debt.md` TD-4.
//! //!
//! A minimum has a property a Gaussian does not: **erosions compose by adding //! A minimum has a property a Gaussian does not: **erosions compose by adding
//! their structuring elements**. The minimum over a contiguous run of `d` //! their structuring elements**. The minimum over a contiguous run of `d`
+2 -2
View File
@@ -150,7 +150,7 @@
//! the artefact this control must not have. //! the artefact this control must not have.
//! //!
//! Run at the render size, that measured **34 ms at 4K** — seven times the //! Run at the render size, that measured **34 ms at 4K** — seven times the
//! entire fused point chain, for one slider — which is `docs/technical-debt.md` //! entire fused point chain, for one slider — which is `docs/dev/technical-debt.md`
//! TD-4 and is what [`Recipe::base_scale`] now answers. The base is computed on //! TD-4 and is what [`Recipe::base_scale`] now answers. The base is computed on
//! a grid a quarter the size on each axis: a sixteenth of the pixels at a //! a grid a quarter the size on each axis: a sixteenth of the pixels at a
//! quarter of the radius. //! quarter of the radius.
@@ -271,7 +271,7 @@ impl Band for Coarse {
threshold: 0.35, threshold: 0.35,
gain: 1.0, gain: 1.0,
midtone_taper: true, midtone_taper: true,
// A quarter, which is what `docs/technical-debt.md` TD-4 bought back. // A quarter, which is what `docs/dev/technical-debt.md` TD-4 bought back.
// //
// σ is 1.2% of the shorter edge — 26 px at 4K — so the base holds no // σ is 1.2% of the shorter edge — 26 px at 4K — so the base holds no
// spatial frequency anywhere near the quarter-scale Nyquist of one // spatial frequency anywhere near the quarter-scale Nyquist of one
+1 -1
View File
@@ -217,7 +217,7 @@ pub struct Version {
/// graph's film cleared and the caller re-bakes — see `EditGraph::set_film`. /// graph's film cleared and the caller re-bakes — see `EditGraph::set_film`.
pub film: Option<FilmRef>, pub film: Option<FilmRef>,
/// TRACES: FR-DEV-8 | FR-NC-9 /// TRACES: FR-DEV-8 | FR-NC-9
/// The repairs (`docs/spot-removal.md`). /// The repairs (`docs/dev/spot-removal.md`).
/// ///
/// A line per spot, keyed `spot.<id>`, rather than a block per spot as a /// A line per spot, keyed `spot.<id>`, rather than a block per spot as a
/// mask gets: a spot is eight numbers, and sixty-four blocks would bury the /// mask gets: a spot is eight numbers, and sixty-four blocks would bury the
+3 -3
View File
@@ -6,7 +6,7 @@
//! are blended. No pixels are stored, here or anywhere: the shader draws the //! are blended. No pixels are stored, here or anywhere: the shader draws the
//! repair from these numbers every time the photograph is rendered, which is //! repair from these numbers every time the photograph is rendered, which is
//! what makes it non-destructive, cheap to sync, and undoable //! what makes it non-destructive, cheap to sync, and undoable
//! (`docs/spot-removal.md`). //! (`docs/dev/spot-removal.md`).
//! //!
//! # Why this is not an operation //! # Why this is not an operation
//! //!
@@ -61,7 +61,7 @@ pub const MAX_SPOTS: usize = 64;
/// bounds something that is otherwise unbounded: a detail pass declares how far /// bounds something that is otherwise unbounded: a detail pass declares how far
/// it reads from the pixel it writes, and for a spot that is the offset plus /// it reads from the pixel it writes, and for a spot that is the offset plus
/// the radius. An unbounded offset is an unbounded halo, which is a pass the /// the radius. An unbounded offset is an unbounded halo, which is a pass the
/// tile scheduler cannot plan (ARCH §5.3, `docs/spot-removal.md` §5.3). /// tile scheduler cannot plan (ARCH §5.3, `docs/dev/spot-removal.md` §5.3).
pub const MAX_SOURCE_DISTANCE: f32 = 0.5; pub const MAX_SOURCE_DISTANCE: f32 = 0.5;
/// The radius a new spot starts at, in frame units. /// The radius a new spot starts at, in frame units.
@@ -210,7 +210,7 @@ impl Spot {
/// **FR-DEV-8 asks for automatic source placement, and this is the cheap /// **FR-DEV-8 asks for automatic source placement, and this is the cheap
/// half of it.** The good half searches the photograph for a patch whose /// half of it.** The good half searches the photograph for a patch whose
/// surroundings match — a compute dispatch scoring candidate offsets, and /// surroundings match — a compute dispatch scoring candidate offsets, and
/// one small readback when the spot is created (`docs/spot-removal.md` /// one small readback when the spot is created (`docs/dev/spot-removal.md`
/// §8). This is what stands in for it, and it is worth having on its own /// §8). This is what stands in for it, and it is worth having on its own
/// terms rather than as a placeholder: dust sits on skies, skies are /// terms rather than as a placeholder: dust sits on skies, skies are
/// smooth, and a patch two and a half radii away is nearly always the same /// smooth, and a patch two and a half radii away is nearly always the same
+1 -1
View File
@@ -13,7 +13,7 @@ log.workspace = true
# Inference. `ort` is the API; **what runs it is `dr-inference-engine`'s # Inference. `ort` is the API; **what runs it is `dr-inference-engine`'s
# business** — tract, or an ONNX Runtime the app found on disk, on whichever # business** — tract, or an ONNX Runtime the app found on disk, on whichever
# provider the device has (docs/inference.md). This crate never names either. # provider the device has (docs/dev/inference.md). This crate never names either.
ort = { workspace = true, optional = true } ort = { workspace = true, optional = true }
dr-inference-engine = { workspace = true, optional = true } dr-inference-engine = { workspace = true, optional = true }
ndarray = { workspace = true, optional = true } ndarray = { workspace = true, optional = true }
+1 -1
View File
@@ -34,7 +34,7 @@
//! And doing it here buys two things a shader could not. It is **exactly //! And doing it here buys two things a shader could not. It is **exactly
//! deterministic**, which matters because masks reach the sidecar as indices //! deterministic**, which matters because masks reach the sidecar as indices
//! and a field that varied by vendor would mean a mask meaning one thing on //! and a field that varied by vendor would mean a mask meaning one thing on
//! the desktop and another on the phone (docs/segmentation.md §6, M5). And it //! the desktop and another on the phone (docs/dev/segmentation.md §6, M5). And it
//! is testable against hand-computed distances with no adapter present. //! is testable against hand-computed distances with no adapter present.
//! //!
//! # The transform //! # The transform
+1 -1
View File
@@ -40,7 +40,7 @@ pub struct Edge {
/// A partition of the image into labelled regions, plus how they adjoin. /// A partition of the image into labelled regions, plus how they adjoin.
/// ///
/// The shared interface from docs/segmentation.md §2: arm A produces this /// The shared interface from docs/dev/segmentation.md §2: arm A produces this
/// from a watershed, arm B would produce it from a class map, and the /// from a watershed, arm B would produce it from a class map, and the
/// consumers above cannot tell which. /// consumers above cannot tell which.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
+1 -1
View File
@@ -1,5 +1,5 @@
//! TRACES: FR-DEV-3i //! TRACES: FR-DEV-3i
//! Region segmentation for local masking (S15, docs/segmentation.md). //! Region segmentation for local masking (S15, docs/dev/segmentation.md).
//! //!
//! Local adjustments need to know where the image's regions are before they //! Local adjustments need to know where the image's regions are before they
//! can snap a mask to one. This crate is that map, and it is deliberately //! can snap a mask to one. This crate is that map, and it is deliberately
+2 -2
View File
@@ -1,6 +1,6 @@
//! Arm C — semantic instances as a prior over the watershed merge order. //! Arm C — semantic instances as a prior over the watershed merge order.
//! //!
//! docs/segmentation.md §5. The spec calls this the expected winner and it is //! docs/dev/segmentation.md §5. The spec calls this the expected winner and it is
//! what ships, for a reason that survives the model turning out to be narrower //! what ships, for a reason that survives the model turning out to be narrower
//! than §4 assumed: the two arms fail in *opposite* directions, so each one //! than §4 assumed: the two arms fail in *opposite* directions, so each one
//! covers the other's failure. //! covers the other's failure.
@@ -233,7 +233,7 @@ pub fn apply_semantic_prior(
/// This is the interaction the whole spike exists to enable, and the reason it /// This is the interaction the whole spike exists to enable, and the reason it
/// returns *region ids* rather than a raster: a mask that is a set of integers /// returns *region ids* rather than a raster: a mask that is a set of integers
/// is diffable, mergeable at node level under FR-NC-9, and cheap in a sidecar /// is diffable, mergeable at node level under FR-NC-9, and cheap in a sidecar
/// (docs/segmentation.md §1). A raster is none of those. /// (docs/dev/segmentation.md §1). A raster is none of those.
/// ///
/// The returned ids are sorted, so the same click always produces the same /// The returned ids are sorted, so the same click always produces the same
/// mask — which is what lets it be a cache key. /// mask — which is what lets it be a cache key.
+5 -5
View File
@@ -60,7 +60,7 @@
//! So the last step is a **marker-based watershed**. The mask is eroded to //! So the last step is a **marker-based watershed**. The mask is eroded to
//! give two markers — confidently inside, confidently outside — and the flood //! give two markers — confidently inside, confidently outside — and the flood
//! runs in the ribbon left between them, meeting along the most expensive line //! runs in the ribbon left between them, meeting along the most expensive line
//! it can find. The cost is a sum of terms, as docs/segmentation.md §2 says it //! it can find. The cost is a sum of terms, as docs/dev/segmentation.md §2 says it
//! should be: the photograph's own edges, and the colour model's disagreement. //! should be: the photograph's own edges, and the colour model's disagreement.
//! //!
//! Markers are what make this the right shape rather than the watershed §15 //! Markers are what make this the right shape rather than the watershed §15
@@ -244,7 +244,7 @@ pub struct RefineOptions {
/// How much the photograph's own edges count against the colour model in /// How much the photograph's own edges count against the colour model in
/// the flood's cost, `0.0..=1.0`. /// the flood's cost, `0.0..=1.0`.
/// ///
/// docs/segmentation.md §2 specifies the cost as *a sum of terms* — image /// docs/dev/segmentation.md §2 specifies the cost as *a sum of terms* — image
/// gradient always available, semantic evidence added when a model is /// gradient always available, semantic evidence added when a model is
/// present — and this is the mix. At one the boundary lands purely on the /// present — and this is the mix. At one the boundary lands purely on the
/// strongest edge in the band; at zero purely where the colour verdict /// strongest edge in the band; at zero purely where the colour verdict
@@ -378,7 +378,7 @@ const MAX_SAMPLES: usize = 20_000;
/// floating-point comparison is a stopping rule that can differ between /// floating-point comparison is a stopping rule that can differ between
/// machines, and a mask that differs between machines reaches the sidecar as /// machines, and a mask that differs between machines reaches the sidecar as
/// indices meaning one thing on the desktop and another on the phone /// indices meaning one thing on the desktop and another on the phone
/// (docs/segmentation.md §6). /// (docs/dev/segmentation.md §6).
const ITERATIONS: usize = 12; const ITERATIONS: usize = 12;
/// Half-width of the verdict scale, in nats. /// Half-width of the verdict scale, in nats.
@@ -676,7 +676,7 @@ impl Refinement {
/// fronts meet along the most expensive line in the ribbon — which is the /// fronts meet along the most expensive line in the ribbon — which is the
/// watershed, and which is where the boundary belongs. /// watershed, and which is where the boundary belongs.
/// ///
/// The cost is a sum of terms, as docs/segmentation.md §2 says it should /// The cost is a sum of terms, as docs/dev/segmentation.md §2 says it should
/// be: the photograph's own edges, and the colour model's disagreement. /// be: the photograph's own edges, and the colour model's disagreement.
/// Neither alone is right. An edge with no colour meaning is a texture, /// Neither alone is right. An edge with no colour meaning is a texture,
/// and a colour change with no edge is a gradient. /// and a colour change with no edge is a gradient.
@@ -889,7 +889,7 @@ fn neighbours(p: usize, w: usize, h: usize) -> impl Iterator<Item = usize> {
/// Edge strength over the opponent features, as one byte per pixel. /// Edge strength over the opponent features, as one byte per pixel.
/// ///
/// Sobel over the same three numbers the colour model is fitted on, rather /// Sobel over the same three numbers the colour model is fitted on, rather
/// than over plain luma — docs/segmentation.md §3 is explicit that a /// than over plain luma — docs/dev/segmentation.md §3 is explicit that a
/// channel-weighted RGB gradient reads a saturated red edge as weaker than it /// channel-weighted RGB gradient reads a saturated red edge as weaker than it
/// looks, and a flag against sky is exactly that edge. /// looks, and a flag against sky is exactly that edge.
/// ///
+3 -3
View File
@@ -1,4 +1,4 @@
//! Semantic segmentation — arm B (S15, docs/segmentation.md §4). //! Semantic segmentation — arm B (S15, docs/dev/segmentation.md §4).
//! //!
//! Runs a YOLO instance-segmentation graph over a proxy-resolution image and //! Runs a YOLO instance-segmentation graph over a proxy-resolution image and
//! returns the instances it found: a class, a score, a box, and a soft mask //! returns the instances it found: a class, a score, a box, and a soft mask
@@ -209,7 +209,7 @@ const EMBEDDED_MODEL: &[u8] = include_bytes!("../../../models/segment/yolo26n-se
const EMBEDDED_CLASSES: &str = include_str!("../../../models/segment/yolo26n-seg.classes.json"); const EMBEDDED_CLASSES: &str = include_str!("../../../models/segment/yolo26n-seg.classes.json");
/// The bytes of the model that ships with this crate, for whoever compiles /// The bytes of the model that ships with this crate, for whoever compiles
/// engines ahead of the first request (docs/inference.md §6). /// engines ahead of the first request (docs/dev/inference.md §6).
#[cfg(feature = "embedded-model")] #[cfg(feature = "embedded-model")]
pub fn embedded_model_bytes() -> &'static [u8] { pub fn embedded_model_bytes() -> &'static [u8] {
EMBEDDED_MODEL EMBEDDED_MODEL
@@ -237,7 +237,7 @@ impl SemanticModel {
pub fn from_bytes(bytes: &[u8], classes: Vec<Arc<str>>) -> Result<Self, SegmentError> { pub fn from_bytes(bytes: &[u8], classes: Vec<Arc<str>>) -> Result<Self, SegmentError> {
// The f32 graph on whatever the device's backend is. An int8 form // The f32 graph on whatever the device's backend is. An int8 form
// for the Hexagon waits on docs/inference.md §10 M7 — the mask // for the Hexagon waits on docs/dev/inference.md §10 M7 — the mask
// boundary has to be measured before it moves. // boundary has to be measured before it moves.
let session = dr_inference_engine::open( let session = dr_inference_engine::open(
dr_inference_engine::Role::Segmenter, dr_inference_engine::Role::Segmenter,
+2 -2
View File
@@ -177,7 +177,7 @@ pub struct FaceSettings {
/// Which SCRFD graph the indexing pass detects with. /// Which SCRFD graph the indexing pass detects with.
/// ///
/// Three exports of one architecture, differing only in how much computation /// Three exports of one architecture, differing only in how much computation
/// they spend, and docs/faces.md §12.3 is the measurement that made this a /// they spend, and docs/dev/faces.md §12.3 is the measurement that made this a
/// choice rather than a constant: over the same photographs the cheapest one /// choice rather than a constant: over the same photographs the cheapest one
/// misses the small faces in a group and reports a dog a dozen times, the /// misses the small faces in a group and reports a dog a dozen times, the
/// middle one finds 14% more faces for 12% more time, and the largest a /// middle one finds 14% more faces for 12% more time, and the largest a
@@ -261,7 +261,7 @@ impl FaceDetector {
} }
} }
/// The id when the detector runs in its int8 form (docs/inference.md §7). /// The id when the detector runs in its int8 form (docs/dev/inference.md §7).
/// ///
/// A different detector: it finds a different set of faces, so it is a /// A different detector: it finds a different set of faces, so it is a
/// different population of detections. The embedder half is unchanged, /// different population of detections. The embedder half is unchanged,
+1 -1
View File
@@ -48,7 +48,7 @@ pointers there; the build detects that and stops rather than shipping them.
This exists because Android offers no other route to a model: the directory the app reads from is This exists because Android offers no other route to a model: the directory the app reads from is
inside app-private storage, `run-as` needs a debuggable build, and the app has no picker and no inside app-private storage, `run-as` needs a debuggable build, and the app has no picker and no
fetch. docs/faces.md §2.2a is the decision and its limits — these files come back out before anything fetch. docs/dev/faces.md §2.2a is the decision and its limits — these files come back out before anything
is published. is published.
## Java in the APK ## Java in the APK
+2 -2
View File
@@ -258,7 +258,7 @@ fi
cp "${SO}" "${OUT}/staging/lib/${ABI}/libdarkroom.so" cp "${SO}" "${OUT}/staging/lib/${ABI}/libdarkroom.so"
cp "${DEX}" "${OUT}/staging/classes.dex" cp "${DEX}" "${OUT}/staging/classes.dex"
# The inference runtime (docs/inference.md §3): ONNX Runtime and Qualcomm's # The inference runtime (docs/dev/inference.md §3): ONNX Runtime and Qualcomm's
# Hexagon backend, beside libdarkroom.so so the app finds them in its own # Hexagon backend, beside libdarkroom.so so the app finds them in its own
# native library directory. The build links none of it — the app dlopens # native library directory. The build links none of it — the app dlopens
# `libonnxruntime.so` at launch and runs on tract if it is not there — so an # `libonnxruntime.so` at launch and runs on tract if it is not there — so an
@@ -278,7 +278,7 @@ else
fi fi
# The models. Android has no other route to one — app-private storage is not # The models. Android has no other route to one — app-private storage is not
# user-reachable and the in-app fetch is unbuilt (docs/faces.md §2.2a) — so # user-reachable and the in-app fetch is unbuilt (docs/dev/faces.md §2.2a) — so
# they go in the APK and `android_main` unpacks them on first launch. The # they go in the APK and `android_main` unpacks them on first launch. The
# sources are `models/face/` and `models/scene/`, shared with the Arch package # sources are `models/face/` and `models/scene/`, shared with the Arch package
# rather than living under this one platform's directory. # rather than living under this one platform's directory.
+1 -1
View File
@@ -73,7 +73,7 @@ SO="${CACHE}/target/jniLibs/${ABI}/libdarkroom.so"
# of KEYSTORE_PASS (see its header), and the keystore has to be reachable from # of KEYSTORE_PASS (see its header), and the keystore has to be reachable from
# inside the container, so a host path in KEYSTORE is copied under the mounted # inside the container, so a host path in KEYSTORE is copied under the mounted
# target directory for the duration of the build and removed after. The # target directory for the duration of the build and removed after. The
# passwords travel as environment, never as arguments -- docs/android-signing.md # passwords travel as environment, never as arguments -- docs/dev/android-signing.md
# has the incantation. # has the incantation.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
echo "==> packaging APK" echo "==> packaging APK"
+2 -2
View File
@@ -1,6 +1,6 @@
# DarkRoom — reproducible Windows cross-build environment # DarkRoom — reproducible Windows cross-build environment
# #
# Everything docs/windows.md §2 names: Rust with the GNU Windows target, the # Everything docs/dev/windows.md §2 names: Rust with the GNU Windows target, the
# MinGW-w64 cross compiler it links with, NSIS to build the installer, and Wine # MinGW-w64 cross compiler it links with, NSIS to build the installer, and Wine
# to smoke-test the result. Both CI and local builds use this image, so "works # to smoke-test the result. Both CI and local builds use this image, so "works
# on my machine" and "works in CI" are the same machine — the same argument # on my machine" and "works in CI" are the same machine — the same argument
@@ -77,7 +77,7 @@ RUN curl -fsSL https://sh.rustup.rs | sh -s -- \
# libwinpthread the Rust target's own MinGW pieces were built against, and # libwinpthread the Rust target's own MinGW pieces were built against, and
# picking the other produces link errors that read as if std were missing. # picking the other produces link errors that read as if std were missing.
# #
# The runtime is linked statically (docs/windows.md §2) so the installer # The runtime is linked statically (docs/dev/windows.md §2) so the installer
# carries one file. `-static-libgcc` is all it takes: rustc's windows-gnu # carries one file. `-static-libgcc` is all it takes: rustc's windows-gnu
# target links its own copy of winpthread in self-contained mode, so nothing # target links its own copy of winpthread in self-contained mode, so nothing
# imports libwinpthread-1.dll — the smoke test's objdump step is what checks # imports libwinpthread-1.dll — the smoke test's objdump step is what checks
+2 -2
View File
@@ -1,7 +1,7 @@
# Windows cross-build environment # Windows cross-build environment
Reproducible container for building the Windows executable and its installer from Linux. The Reproducible container for building the Windows executable and its installer from Linux. The
specification is [docs/windows.md](../../docs/windows.md); this directory is what it turned into, specification is [docs/dev/windows.md](../../docs/dev/windows.md); this directory is what it turned into,
and every departure from the spec's first draft is recorded in the Dockerfile's comments. and every departure from the spec's first draft is recorded in the Dockerfile's comments.
## Use ## Use
@@ -16,7 +16,7 @@ and every departure from the spec's first draft is recorded in the Dockerfile's
# Build the installer from that binary # Build the installer from that binary
./docker/windows/build.sh docker/windows/package.sh ./docker/windows/build.sh docker/windows/package.sh
# Smoke-test under Wine (docs/windows.md §6) # Smoke-test under Wine (docs/dev/windows.md §6)
./docker/windows/build.sh wine target-windows/x86_64-pc-windows-gnu/release/darkroom-desktop.exe --version ./docker/windows/build.sh wine target-windows/x86_64-pc-windows-gnu/release/darkroom-desktop.exe --version
./docker/windows/build.sh wine target-windows/installer/DarkRoom-0.12.0-x86_64-setup.exe /S ./docker/windows/build.sh wine target-windows/installer/DarkRoom-0.12.0-x86_64-setup.exe /S
+1 -1
View File
@@ -7,7 +7,7 @@
# to have run in the same target directory. Produces # to have run in the same target directory. Produces
# DarkRoom-<version>-x86_64-setup.exe in $OUT (default: target-windows/installer). # DarkRoom-<version>-x86_64-setup.exe in $OUT (default: target-windows/installer).
# #
# Runs inside the container, where makensis is; docs/windows.md §5 is the # Runs inside the container, where makensis is; docs/dev/windows.md §5 is the
# specification this implements. # specification this implements.
set -euo pipefail set -euo pipefail
+86
View File
@@ -0,0 +1,86 @@
# Documentation
Two audiences, two folders. Most people want the first table and never the
second.
## Using DarkRoom
| | |
|---|---|
| [The manual](manual/README.md) | Every feature, pictured from the application itself — opening a library, rating and filing, developing, local masks, repair, film, panoramas, export |
| [How it is driven](gestures.md) | Every gesture and shortcut, by screen. Generated from the code, so it cannot describe one the application does not have |
The [top-level README](../README.md) says what DarkRoom is, how to get it on
each platform, and what is still missing.
## Changing DarkRoom
Everything under [`dev/`](dev/) is for someone working on the code. Start with
[CONTRIBUTING.md](../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](dev/requirements.md) | What the software must do — the numbered register, the decisions (D-numbers) and the spikes (S-numbers) |
| [architecture.md](dev/architecture.md) | How it is built — crates, the GPU pipeline, the data model, sync |
| [traceability.md](dev/traceability.md) | Generated: which requirement is claimed by which file. Never edited by hand |
| [outstanding.md](dev/outstanding.md) | What is specified and not built, and whether that is a decision or a gap |
| [technical-debt.md](dev/technical-debt.md) | Compromises taken deliberately, each with the condition that retires it |
| [code-health.md](dev/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](dev/catalog.md) | The index, the library view, incremental scan, the job queue |
| [storage.md](dev/storage.md) | Storage backends: the seam a folder, a sync client and a Nextcloud account share |
| [faces.md](dev/faces.md) | Face detection, identity, clustering and the eye-state models |
| [segmentation.md](dev/segmentation.md) | How the application finds the regions a local mask snaps to |
| [mask-editing.md](dev/mask-editing.md) | Painting, erasing and combining masks |
| [spot-removal.md](dev/spot-removal.md) | Clone and heal as parameters in the edit graph |
| [panorama.md](dev/panorama.md) | Alignment, projection, the chunked composite and the border fill |
| [inference.md](dev/inference.md) | The neural runtime and model chosen per device, with the measurements |
| [display-and-extension.md](dev/display-and-extension.md) | The display contract, and why the fused pipeline is already most of a plugin format |
| [view-composition.md](dev/view-composition.md) | A controller for the display layer |
| [ui-navigation.md](dev/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](dev/benchmarks.md) | The per-commit suite: what it covers, what it does not, how to read a failure |
| [bench-baseline.json](dev/bench-baseline.json) | The committed numbers the suite checks against |
| [frame-budget.md](dev/frame-budget.md) | What a frame costs on each device, and the decision those figures settled |
**Platforms and distribution.**
| | |
|---|---|
| [distribution.md](dev/distribution.md) | Which channels v1 targets and what each one constrains |
| [windows.md](dev/windows.md) | The Windows installer, cross-built from the Linux CI |
| [android-signing.md](dev/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](dev/archive/milestone-v0.1.md) | The first milestone, delivered 2026-08-30 and superseded |
| [ui-refinement.md](dev/archive/ui-refinement.md) | How the interface should look; succeeded by [ui-navigation.md](dev/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`](../tools/manual/README.md) 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.
@@ -4,8 +4,8 @@
> Kept as the record of what the first milestone asked for, not as a plan. > Kept as the record of what the first milestone asked for, not as a plan.
> Everything below shipped, and the application went well past it — see > Everything below shipped, and the application went well past it — see
> [outstanding.md](outstanding.md) for what is still missing at 0.9.0. > [outstanding.md](../outstanding.md) for what is still missing at 0.9.0.
**Companion to:** [requirements.md](requirements.md) · [architecture.md](architecture.md) **Companion to:** [requirements.md](../requirements.md) · [architecture.md](../architecture.md)
The first buildable milestone: connect to a Nextcloud folder, index it locally, and display RAW The first buildable milestone: connect to a Nextcloud folder, index it locally, and display RAW
previews on both Linux and Android. previews on both Linux and Android.
@@ -39,7 +39,7 @@ building on sand.
## 2. The four assumptions under test ## 2. The four assumptions under test
Each maps to a spike in [requirements.md §9](requirements.md). Each maps to a spike in [requirements.md §9](../requirements.md).
| # | Assumption | If wrong | Spike | | # | Assumption | If wrong | Spike |
|---|---|---|---| |---|---|---|---|
@@ -55,7 +55,7 @@ all, so it should be proven in the first week, before catalog or sync work begin
## 3. Functional scope ## 3. Functional scope
Requirement IDs reference [requirements.md](requirements.md); a v0.1 suffix marks a reduced subset Requirement IDs reference [requirements.md](../requirements.md); a v0.1 suffix marks a reduced subset
of the full requirement. of the full requirement.
### 3.1 Account and connection ### 3.1 Account and connection
@@ -2,9 +2,9 @@
**Status:** Built, not yet recorded · 2026-08-30 **Status:** Built, not yet recorded · 2026-08-30
**Companion to:** [requirements.md](requirements.md) §4.1 (performance targets) · §8 (verification) **Companion to:** [requirements.md](requirements.md) §4.1 (performance targets) · §8 (verification)
**Instrument:** [`tools/bench`](../tools/bench) — `cargo run --release -p dr-bench -- check` **Instrument:** [`tools/bench`](../../tools/bench) — `cargo run --release -p dr-bench -- check`
**Committed numbers:** [`bench-baseline.json`](bench-baseline.json) **Committed numbers:** [`bench-baseline.json`](bench-baseline.json)
**GPU half:** [`core/dr-gpu/tests/frame_budget.rs`](../core/dr-gpu/tests/frame_budget.rs) · **GPU half:** [`core/dr-gpu/tests/frame_budget.rs`](../../core/dr-gpu/tests/frame_budget.rs) ·
[frame-budget.md](frame-budget.md) [frame-budget.md](frame-budget.md)
§8 has said since it was written that performance is verified by *"an automated §8 has said since it was written that performance is verified by *"an automated
@@ -60,7 +60,7 @@ Two of those rows carry a qualifier, and the qualifiers are the point.
library view cannot paint without: `Catalog::open` (which connects, migrates and library view cannot paint without: `Catalog::open` (which connects, migrates and
**backfills**, and the backfill is three passes over the images table on every **backfills**, and the backfill is three passes over the images table on every
open), `count`, the first 400-row `window`, and the monthly `timeline`. Tagged open), `count`, the first 400-row `window`, and the monthly `timeline`. Tagged
`TRACES: NFR-P1` in [`tools/bench/src/catalog_open.rs`](../tools/bench/src/catalog_open.rs), `TRACES: NFR-P1` in [`tools/bench/src/catalog_open.rs`](../../tools/bench/src/catalog_open.rs),
because a build that breaks it fails this gate. because a build that breaks it fails this gate.
**NFR-P3 — ≥ 100 images per second on the embedded preview path.** The **NFR-P3 — ≥ 100 images per second on the embedded preview path.** The
@@ -69,7 +69,7 @@ per-image work is exactly what `spawn_thumbnail_sweep` does — `decode_jpeg`,
`ThumbStore::put` — arranged in the same shape: chunks of 96, lanes owning `ThumbStore::put` — arranged in the same shape: chunks of 96, lanes owning
disjoint slices, and the single thread that owns the store writing the finished disjoint slices, and the single thread that owns the store writing the finished
chunk. Tagged `TRACES: NFR-P3` in chunk. Tagged `TRACES: NFR-P3` in
[`tools/bench/src/thumbnails.rs`](../tools/bench/src/thumbnails.rs). [`tools/bench/src/thumbnails.rs`](../../tools/bench/src/thumbnails.rs).
### Requirements this can only half-answer, and is not tagged for ### Requirements this can only half-answer, and is not tagged for
@@ -114,7 +114,7 @@ instead of ~2 TB, and neither half is flattered by that.
| Rows | 50,000 images, 50,000 default versions, 400 folders, one root | | Rows | 50,000 images, 50,000 default versions, 400 folders, one root |
| Capture times | Twelve years from a fixed epoch, so the timeline has ~144 monthly buckets | | Capture times | Twelve years from a fixed epoch, so the timeline has ~144 monthly buckets |
| Sources | 12 synthesised JPEGs at 1620 × 1080 — the size `dr-decode` records a CR2 carrying in IFD2 | | Sources | 12 synthesised JPEGs at 1620 × 1080 — the size `dr-decode` records a CR2 carrying in IFD2 |
| Seed | 20260829, in [`tools/bench/src/main.rs`](../tools/bench/src/main.rs) | | Seed | 20260829, in [`tools/bench/src/main.rs`](../../tools/bench/src/main.rs) |
| Location | `$DR_BENCH_DIR`, else the system temporary directory | | Location | `$DR_BENCH_DIR`, else the system temporary directory |
It is reproducible from the seed, and a `stamp.json` beside it records what it It is reproducible from the seed, and a `stamp.json` beside it records what it
@@ -17,8 +17,8 @@ permission to a package rather than after.
| Platform | Channel | State | What it constrains | | Platform | Channel | State | What it constrains |
|---|---|---|---| |---|---|---|---|
| Linux | Arch source package — [`packaging/PKGBUILD`](../packaging/PKGBUILD) | Built, in tree | Nothing. Full filesystem access, system Vulkan, system secret daemon | | Linux | Arch source package — [`packaging/PKGBUILD`](../../packaging/PKGBUILD) | Built, in tree | Nothing. Full filesystem access, system Vulkan, system secret daemon |
| Linux | Flatpak — [`packaging/flatpak/`](../packaging/flatpak/) | Manifest in tree, **library selection does not work** (§4) | Portals only. No `--filesystem=`, no host mount table, no typed paths | | Linux | Flatpak — [`packaging/flatpak/`](../../packaging/flatpak/) | Manifest in tree, **library selection does not work** (§4) | Portals only. No `--filesystem=`, no host mount table, no typed paths |
| Linux | AppImage | v1 channel, **recipe not yet written** (§5) | Oldest supported glibc, and no sandbox at all | | Linux | AppImage | v1 channel, **recipe not yet written** (§5) | Oldest supported glibc, and no sandbox at all |
| Android | F-Droid | v1 channel, not yet submitted | GPLv3-clean build, reproducible, no proprietary blobs | | Android | F-Droid | v1 channel, not yet submitted | GPLv3-clean build, reproducible, no proprietary blobs |
| Android | Play Store | **Not v1** (§6) | Would make ARCH §6.9 binding as policy rather than as engineering | | Android | Play Store | **Not v1** (§6) | Would make ARCH §6.9 binding as policy rather than as engineering |
@@ -39,7 +39,7 @@ somewhere:
that misses one of them costs the icon in the shell or the association in the that misses one of them costs the icon in the shell or the association in the
software centre, and neither failure announces itself. software centre, and neither failure announces itself.
- **The metainfo, not just the desktop entry.** - **The metainfo, not just the desktop entry.**
[`packaging/paris.tourolle.darkroom.metainfo.xml`](../packaging/paris.tourolle.darkroom.metainfo.xml) [`packaging/paris.tourolle.darkroom.metainfo.xml`](../../packaging/paris.tourolle.darkroom.metainfo.xml)
is the single description of the application, installed by every channel that is the single description of the application, installed by every channel that
has somewhere to put it. Its `metadata_license` is CC0-1.0 and its has somewhere to put it. Its `metadata_license` is CC0-1.0 and its
`project_license` is GPL-3.0-or-later; those differ on purpose — see the `project_license` is GPL-3.0-or-later; those differ on purpose — see the
@@ -173,7 +173,7 @@ Two changes, in this order:
chooser instead of a volume list. chooser instead of a volume list.
**Done when:** a Flatpak built from **Done when:** a Flatpak built from
[`packaging/flatpak/paris.tourolle.darkroom.yml`](../packaging/flatpak/paris.tourolle.darkroom.yml), [`packaging/flatpak/paris.tourolle.darkroom.yml`](../../packaging/flatpak/paris.tourolle.darkroom.yml),
with its `finish-args` unchanged and no `flatpak override` applied, can select a with its `finish-args` unchanged and no `flatpak override` applied, can select a
library root, scan it, and write a sidecar back into it. library root, scan it, and write a sidecar back into it.
View File
@@ -3,8 +3,8 @@
**Status:** Measured · 2026-08-27 **Status:** Measured · 2026-08-27
**Companion to:** [display-and-extension.md](display-and-extension.md) §2–3 · **Companion to:** [display-and-extension.md](display-and-extension.md) §2–3 ·
[requirements.md](requirements.md) §3.4 FR-DSP-2, FR-DSP-3, FR-DSP-4 [requirements.md](requirements.md) §3.4 FR-DSP-2, FR-DSP-3, FR-DSP-4
**Instrument:** [`core/dr-gpu/examples/frame_budget.rs`](../core/dr-gpu/examples/frame_budget.rs) **Instrument:** [`core/dr-gpu/examples/frame_budget.rs`](../../core/dr-gpu/examples/frame_budget.rs)
**Guard:** [`core/dr-gpu/tests/frame_budget.rs`](../core/dr-gpu/tests/frame_budget.rs) **Guard:** [`core/dr-gpu/tests/frame_budget.rs`](../../core/dr-gpu/tests/frame_budget.rs)
[display-and-extension.md](display-and-extension.md) §2 fixed a decision rule in [display-and-extension.md](display-and-extension.md) §2 fixed a decision rule in
advance and made three measurements the thing that settles it. This file is advance and made three measurements the thing that settles it. This file is
@@ -18,11 +18,11 @@ The core is further along than the interface, and it is worth being exact
about which half is missing, because it changes the size of the work. about which half is missing, because it changes the size of the work.
**Painting exists everywhere except where a finger is.** **Painting exists everywhere except where a finger is.**
[`MaskSource::Brush`](../core/dr-pipeline/src/mask.rs), [`Stroke`], the [`MaskSource::Brush`](../../core/dr-pipeline/src/mask.rs), [`Stroke`], the
simplification and the point budgets, the sidecar's `stroke = …` line and its simplification and the point budgets, the sidecar's `stroke = …` line and its
parser, the GPU's per-stroke bounding-box draw with add and erase blend parser, the GPU's per-stroke bounding-box draw with add and erase blend
states — all of it is written, tested, and reachable from no control in the states — all of it is written, tested, and reachable from no control in the
application. [`toolrail.slint:138`](../ui/dr-ui/ui/toolrail.slint#L138) says so application. [`toolrail.slint:138`](../../ui/dr-ui/ui/toolrail.slint#L138) says so
in as many words: *"what is missing is the canvas interaction"*. in as many words: *"what is missing is the canvas interaction"*.
**A mask has exactly one source.** A layer is one `MaskSource` and a shaping **A mask has exactly one source.** A layer is one `MaskSource` and a shaping
@@ -37,7 +37,7 @@ shoulder and leaks four pixels into the hair, no global number fixes both, and
that is the ordinary case rather than a corner one. that is the ordinary case rather than a corner one.
**And you cannot see the mask.** *(Built — see §6.)* The overlay on the canvas **And you cannot see the mask.** *(Built — see §6.)* The overlay on the canvas
was [`overlay_rgba`](../ui/dr-ui/src/segmentation.rs) and nothing else — a was [`overlay_rgba`](../../ui/dr-ui/src/segmentation.rs) and nothing else — a
CPU-built false-colour picture of *what the model detected*, at proxy CPU-built false-colour picture of *what the model detected*, at proxy
resolution. It is not the layer's alpha: it knows nothing of the layer's resolution. It is not the layer's alpha: it knows nothing of the layer's
feather, its falloff, its morphology, its invert, or its opacity. Nobody can feather, its falloff, its morphology, its invert, or its opacity. Nobody can
@@ -234,7 +234,7 @@ writes with one part is byte-identical to what it writes today**:
3. `stroke = …` lines keep their position and their meaning. The mode token 3. `stroke = …` lines keep their position and their meaning. The mode token
gains `push`, and `cling` is a fourth number written only when non-zero. gains `push`, and `cling` is a fourth number written only when non-zero.
An older build meeting either drops *that stroke and only that stroke*, An older build meeting either drops *that stroke and only that stroke*,
which is the rule [`parse_stroke`](../core/dr-pipeline/src/sidecar.rs) which is the rule [`parse_stroke`](../../core/dr-pipeline/src/sidecar.rs)
already documents and already implements. already documents and already implements.
**Merge (FR-NC-9).** A part is a block with an id, so two devices that added **Merge (FR-NC-9).** A part is a block with an id, so two devices that added
@@ -272,7 +272,7 @@ The set operations are already expressible in fixed-function blending over
| Intersect | `Zero`, `Src`, `Add` | `dst · src` | M2 | | Intersect | `Zero`, `Src`, `Add` | `dst · src` | M2 |
The middle row is `brush_erase`, already constructed in The middle row is `brush_erase`, already constructed in
[`MaskPass::new`](../core/dr-gpu/src/mask.rs). The other two are the same [`MaskPass::new`](../../core/dr-gpu/src/mask.rs). The other two are the same
three vertices with a different `BlendState`, and nothing is read back. three vertices with a different `BlendState`, and nothing is read back.
**One correction to the first draft of this section, found in the building.** **One correction to the first draft of this section, found in the building.**
@@ -289,7 +289,7 @@ the first time any layer has more than one part) and blended from there. A
layer of one part still takes the old path exactly — straight into its slice, layer of one part still takes the old path exactly — straight into its slice,
no scratch, no combine pass — which is what keeps every existing mask no scratch, no combine pass — which is what keeps every existing mask
rendering as it did. `an_erase_stroke_holes_its_own_part_and_not_the_mask` in rendering as it did. `an_erase_stroke_holes_its_own_part_and_not_the_mask` in
[`local_adjustments.rs`](../core/dr-gpu/tests/local_adjustments.rs) is the test [`local_adjustments.rs`](../../core/dr-gpu/tests/local_adjustments.rs) is the test
that holds this in place. that holds this in place.
`Max` blending on `r8unorm` is core WGPU and universally supported on the `Max` blending on `r8unorm` is core WGPU and universally supported on the
@@ -342,7 +342,7 @@ Three honest costs:
### 5.4 Painting has to be incremental ### 5.4 Painting has to be incremental
Today [`MaskPass::render`](../core/dr-gpu/src/mask.rs) clears each slice and Today [`MaskPass::render`](../../core/dr-gpu/src/mask.rs) clears each slice and
redraws every stroke of the layer. That is right when a shape changes and redraws every stroke of the layer. That is right when a shape changes and
wrong while a finger is down: at 120 reports a second, a layer holding 4096 wrong while a finger is down: at 120 reports a second, a layer holding 4096
points redraws all of them per dab, and the cost of a stroke grows as it is points redraws all of them per dab, and the cost of a stroke grows as it is
@@ -359,14 +359,14 @@ changing falls back to the full rebuild it does now.
This is required, not an optimisation to schedule later: it is what decides This is required, not an optimisation to schedule later: it is what decides
whether painting is usable on the phone, and it is the specific failure whether painting is usable on the phone, and it is the specific failure
[`mask.rs`'s module docs](../core/dr-pipeline/src/mask.rs) say this whole [`mask.rs`'s module docs](../../core/dr-pipeline/src/mask.rs) say this whole
design exists to avoid. design exists to avoid.
### 5.5 Distance fields become per part ### 5.5 Distance fields become per part
[`SubjectMasks`](../core/dr-gpu/src/mask.rs) uploads one signed distance field [`SubjectMasks`](../../core/dr-gpu/src/mask.rs) uploads one signed distance field
**per active layer, in stack order**, and **per active layer, in stack order**, and
[`DevelopSession`](../ui/dr-ui/src/develop.rs) builds them on the same [`DevelopSession`](../../ui/dr-ui/src/develop.rs) builds them on the same
indexing. With parts, a field belongs to the part that shaped it: the upload indexing. With parts, a field belongs to the part that shaped it: the upload
becomes one field per *model-backed part*, flattened in `(layer, part)` order, becomes one field per *model-backed part*, flattened in `(layer, part)` order,
and the rasteriser indexes it by a running counter rather than by `slot`. and the rasteriser indexes it by a running counter rather than by `slot`.
@@ -540,7 +540,7 @@ layer's.
or painted. One code path, three joins. or painted. One code path, three joins.
**Folding two layers into one** uses the multi-selection **Folding two layers into one** uses the multi-selection
[`masks_ui.rs`](../ui/dr-ui/src/masks_ui.rs) already supports: with two layers [`masks_ui.rs`](../../ui/dr-ui/src/masks_ui.rs) already supports: with two layers
selected, "Combine" appends the second's parts to the first and removes it. selected, "Combine" appends the second's parts to the first and removes it.
Offered only when the second layer's adjustments are neutral, and otherwise Offered only when the second layer's adjustments are neutral, and otherwise
offered with a warning that names what will be lost — quietly discarding an offered with a warning that names what will be lost — quietly discarding an
@@ -560,7 +560,7 @@ who sets a small eraser expects it to still be small the next time they erase.
### 7.4 Gestures ### 7.4 Gestures
Each of these needs a `GESTURE:` block beside its implementation — that is the Each of these needs a `GESTURE:` block beside its implementation — that is the
only place [gestures.md](gestures.md) can be written from. only place [gestures.md](../gestures.md) can be written from.
| Gesture | Touch | Pointer | Keyboard | | Gesture | Touch | Pointer | Keyboard |
|---------|-------|---------|----------| |---------|-------|---------|----------|
@@ -611,7 +611,7 @@ removing or re-joining a part.
The mask stack is already snapshotted per step and shared by `Arc` when a step The mask stack is already snapshotted per step and shared by `Arc` when a step
does not touch it, so the cost of an undoable stroke is a clone of one layer's does not touch it, so the cost of an undoable stroke is a clone of one layer's
parts, not of the picture. New keys in parts, not of the picture. New keys in
[`labels.rs`](../ui/dr-ui/src/labels.rs): [`labels.rs`](../../ui/dr-ui/src/labels.rs):
``` ```
history.mask_painted "Paint Mask" history.mask_painted "Paint Mask"
@@ -196,7 +196,7 @@ inside `#[cfg(test)]` — `LocalStorage::open` refuses it, and the test that pro
`dr_plat::imports_supported`, which *returns false on Android* and whose own documentation says it `dr_plat::imports_supported`, which *returns false on Android* and whose own documentation says it
"stops being false when a SAF implementation lands". The second tag documented the absence of the "stops being false when a SAF implementation lands". The second tag documented the absence of the
thing it was counted as evidence for. Both have been removed; this is the "plumbing a future feature thing it was counted as evidence for. Both have been removed; this is the "plumbing a future feature
would use" case [CONTRIBUTING.md](../CONTRIBUTING.md) and [code-health.md CH-4](code-health.md) both would use" case [CONTRIBUTING.md](../../CONTRIBUTING.md) and [code-health.md CH-4](code-health.md) both
warn about. Android reaches a library through a Nextcloud account or a folder, over paths, like the warn about. Android reaches a library through a Nextcloud account or a folder, over paths, like the
desktop. desktop.
@@ -355,10 +355,10 @@ synthetic 50k catalog, run per commit, where **"a regression beyond a stated tol
failure, not a notification."** For most of this project's life it did not exist — no `benches/`, no failure, not a notification."** For most of this project's life it did not exist — no `benches/`, no
criterion, no synthetic catalog, and three CI workflows that between them measured nothing. criterion, no synthetic catalog, and three CI workflows that between them measured nothing.
**It exists now, for everything that does not need a frame.** [`tools/bench`](../tools/bench) builds **It exists now, for everything that does not need a frame.** [`tools/bench`](../../tools/bench) builds
a deterministic 50,000-row catalog over a pool of a dozen real files, measures against it, and fails a deterministic 50,000-row catalog over a pool of a dozen real files, measures against it, and fails
the build on a violated budget or a drift past tolerance; the build on a violated budget or a drift past tolerance;
[`.gitea/workflows/benchmark.yml`](../.gitea/workflows/benchmark.yml) runs it on every push, and [`.gitea/workflows/benchmark.yml`](../../.gitea/workflows/benchmark.yml) runs it on every push, and
[benchmarks.md](benchmarks.md) is the account of what it does and does not cover. **NFR-P1** and [benchmarks.md](benchmarks.md) is the account of what it does and does not cover. **NFR-P1** and
**NFR-P3** are now genuinely gated, and R2's "catalog opens in under 2s" clause with them. **NFR-P3** are now genuinely gated, and R2's "catalog opens in under 2s" clause with them.
@@ -404,7 +404,7 @@ tag anything against.
NFR-OPS-1 was covered by tags that were real rather than fixtures, which is the worse case of the NFR-OPS-1 was covered by tags that were real rather than fixtures, which is the worse case of the
two: one on `compute_coverage` and one on the gesture extractor, both on the traceability tool. A two: one on `compute_coverage` and one on the gesture extractor, both on the traceability tool. A
coverage calculation and a documentation generator are not diagnostics under any reading, so both coverage calculation and a documentation generator are not diagnostics under any reading, so both
tags were removed. It is the case [CONTRIBUTING.md](../CONTRIBUTING.md) warns about in its own words: tags were removed. It is the case [CONTRIBUTING.md](../../CONTRIBUTING.md) warns about in its own words:
a tag proves a tag exists. The requirement has since been built where it says: the rotating, a tag proves a tag exists. The requirement has since been built where it says: the rotating,
size-capped log and its redaction in `platform/dr-plat/src/diagnostics.rs` (2026-08-30), and the size-capped log and its redaction in `platform/dr-plat/src/diagnostics.rs` (2026-08-30), and the
bundle in `diagnostics/bundle.rs` (2026-09-12) — the log, the crash records, the version, the schema bundle in `diagnostics/bundle.rs` (2026-09-12) — the log, the crash records, the version, the schema
+2 -2
View File
@@ -179,7 +179,7 @@ exports the network alone at 768×1024 — thirteen operator types, all
standard: `Conv`, `InstanceNormalization`, `AveragePool`, `Resize`, `Slice`, standard: `Conv`, `InstanceNormalization`, `AveragePool`, `Resize`, `Slice`,
`Transpose`, `Reshape`, `Concat`, `Add`, `Relu`, `Sigmoid`, `ReduceMean`, `Transpose`, `Reshape`, `Concat`, `Add`, `Relu`, `Sigmoid`, `ReduceMean`,
`Unsqueeze` — and `Unsqueeze` — and
[`examples/onnx_probe.rs`](../core/dr-segment/examples/onnx_probe.rs) loads [`examples/onnx_probe.rs`](../../core/dr-segment/examples/onnx_probe.rs) loads
the 2.8 MB file through the app's own `ort`-over-tract backend with nothing the 2.8 MB file through the app's own `ort`-over-tract backend with nothing
unsupported, in 28 ms, and runs it in **~300 ms on the reference desktop's unsupported, in 28 ms, and runs it in **~300 ms on the reference desktop's
CPU**. The weights ship as `models/keypoints/xfeat-1024.onnx`, recorded in CPU**. The weights ship as `models/keypoints/xfeat-1024.onnx`, recorded in
@@ -255,7 +255,7 @@ Two containers were candidates and S15.1 decided, on 2026-09-19:
the existing encoder with a different sample type, and nothing about it is the existing encoder with a different sample type, and nothing about it is
uncertain. uncertain.
**Linear DNG.** [`examples/linear_dng.rs`](../core/dr-decode/examples/linear_dng.rs) **Linear DNG.** [`examples/linear_dng.rs`](../../core/dr-decode/examples/linear_dng.rs)
hand-rolls a 64 × 48 `LinearRaw` DNG — one IFD, uncompressed 16-bit RGB, hand-rolls a 64 × 48 `LinearRaw` DNG — one IFD, uncompressed 16-bit RGB,
`DNGVersion`, `ColorMatrix1`, `AsShotNeutral`, `CalibrationIlluminant1` — and `DNGVersion`, `ColorMatrix1`, `AsShotNeutral`, `CalibrationIlluminant1` — and
rawler 0.7 reads it back: `cpp 3`, the samples interleaved as written, the rawler 0.7 reads it back: `cpp 3`, the samples interleaved as written, the
@@ -2140,7 +2140,7 @@ Rationale, evidence, and the eliminated alternatives are recorded in
|---|---|---| |---|---|---|
| D1 | Language and UI framework | Rust + Slint, rendering through wgpu | | D1 | Language and UI framework | Rust + Slint, rendering through wgpu |
| D2 | RAW decoder | rawler; LibRaw fallback behind a trait | | D2 | RAW decoder | rawler; LibRaw fallback behind a trait |
| D3 | First milestone | Delivered — [milestone-v0.1.md](milestone-v0.1.md), closed 2026-08-30 | | D3 | First milestone | Delivered — [milestone-v0.1.md](archive/milestone-v0.1.md), closed 2026-08-30 |
| D4 | Nextcloud sync mechanism | ETag pruning, chunked upload v2, Login Flow v2 | | D4 | Nextcloud sync mechanism | ETag pruning, chunked upload v2, Login Flow v2 |
| D5 | Colour management | lcms2 + GPU-side matrix/LUT transforms | | D5 | Colour management | lcms2 + GPU-side matrix/LUT transforms |
| D6 | Shader authoring | Hand-written WGSL | | D6 | Shader authoring | Hand-written WGSL |
@@ -21,11 +21,11 @@ one in its class and the workflow still breaks at the first frame with a mark on
the sky. the sky.
It is also, unusually, a feature whose cost has already been paid twice over. It is also, unusually, a feature whose cost has already been paid twice over.
The neighbourhood stage exists ([`crate::detail`](../core/dr-pipeline/src/detail.rs)), The neighbourhood stage exists ([`crate::detail`](../../core/dr-pipeline/src/detail.rs)),
the convention for storing geometry in normalised source coordinates exists the convention for storing geometry in normalised source coordinates exists
([`mask.rs`](../core/dr-pipeline/src/mask.rs)), the canvas-drag pattern exists ([`mask.rs`](../../core/dr-pipeline/src/mask.rs)), the canvas-drag pattern exists
([`gradient.rs`](../ui/dr-ui/src/gradient.rs)), and the merge-by-id rule exists ([`gradient.rs`](../../ui/dr-ui/src/gradient.rs)), and the merge-by-id rule exists
([`sidecar.rs`](../core/dr-pipeline/src/sidecar.rs)). What is genuinely new is ([`sidecar.rs`](../../core/dr-pipeline/src/sidecar.rs)). What is genuinely new is
small and is named in §3. small and is named in §3.
## 2. Non-goals ## 2. Non-goals
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
TRACES: FR-UI-1 | FR-UI-3 | FR-UI-5 | FR-DEV-3a | FR-DEV-3c TRACES: FR-UI-1 | FR-UI-3 | FR-UI-5 | FR-DEV-3a | FR-DEV-3c
Successor to `ui-refinement.md`, which asked how the interface should *look*. Successor to [`ui-refinement.md`](archive/ui-refinement.md), which asked how the interface should *look*.
This asks how someone finds anything in it. The two are sequenced together at This asks how someone finds anything in it. The two are sequenced together at
the end. the end.
@@ -3,7 +3,7 @@
TRACES: FR-UI-1 | FR-UI-6 | FR-UI-8 | FR-DEV-3a | NFR-P9 TRACES: FR-UI-1 | FR-UI-6 | FR-UI-8 | FR-DEV-3a | NFR-P9
**Status:** Draft · 2026-08-09 **Status:** Draft · 2026-08-09
**Companion to:** [architecture.md](architecture.md) §4.3a, [ui-refinement.md](ui-refinement.md) **Companion to:** [architecture.md](architecture.md) §4.3a, [ui-refinement.md](archive/ui-refinement.md)
## Why ## Why
+9 -9
View File
@@ -11,8 +11,8 @@ and — because there is no Windows hardware on the runner — exactly how much
verified before a person double-clicks it. verified before a person double-clicks it.
**Written as a spec; §10 is the report.** Every step of §9 has since been run — **Written as a spec; §10 is the report.** Every step of §9 has since been run —
[`docker/windows/`](../docker/windows/) is the container, [`packaging/windows/darkroom.nsi`](../packaging/windows/darkroom.nsi) [`docker/windows/`](../../docker/windows/) is the container, [`packaging/windows/darkroom.nsi`](../../packaging/windows/darkroom.nsi)
the installer, [`.gitea/workflows/windows-image.yml`](../.gitea/workflows/windows-image.yml) and the installer, [`.gitea/workflows/windows-image.yml`](../../.gitea/workflows/windows-image.yml) and
the `windows` job in `build-and-test.yml` the CI leg, and §6's gate passes through row 4 under the `windows` job in `build-and-test.yml` the CI leg, and §6's gate passes through row 4 under
Wine. Four claims in the first draft were wrong and are corrected in place with a note; §10 lists Wine. Four claims in the first draft were wrong and are corrected in place with a note; §10 lists
them. Where a claim still rests on reading rather than running, it says so. them. Where a claim still rests on reading rather than running, it says so.
@@ -105,7 +105,7 @@ keeps DX12 out here. It is one flag away if that turns out to be wrong.
The same shape as the Android leg: a job container built from a Dockerfile in the tree and pushed The same shape as the Android leg: a job container built from a Dockerfile in the tree and pushed
to the Gitea registry, tagged by the tree id of its directory so an unrelated push reuses it to the Gitea registry, tagged by the tree id of its directory so an unrelated push reuses it
([`android-image.yml`](../.gitea/workflows/android-image.yml) already does this and the comment ([`android-image.yml`](../../.gitea/workflows/android-image.yml) already does this and the comment
there explains why). there explains why).
``` ```
@@ -157,7 +157,7 @@ Ordered by what blocks a first sign-in. **All four are done**; each item says ho
`is_available`'s probe always succeeds on Windows, which is correct — Credential Manager is `is_available`'s probe always succeeds on Windows, which is correct — Credential Manager is
always present, so FR-NC-2's degraded mode does not arise. always present, so FR-NC-2's degraded mode does not arise.
2. **Paths.** *Done* — [`platform/dr-plat/src/dirs.rs`](../platform/dr-plat/src/dirs.rs). FR-PLAT-LIN-1 2. **Paths.** *Done* — [`platform/dr-plat/src/dirs.rs`](../../platform/dr-plat/src/dirs.rs). FR-PLAT-LIN-1
says XDG, and the code said it in five places by reading `XDG_*_HOME` and falling back to says XDG, and the code said it in five places by reading `XDG_*_HOME` and falling back to
`$HOME/.local/...`. On Windows `HOME` is normally unset, so every one of these degraded to a `$HOME/.local/...`. On Windows `HOME` is normally unset, so every one of these degraded to a
relative path from the working directory — which for a Start Menu launch is relative path from the working directory — which for a Start Menu launch is
@@ -198,15 +198,15 @@ Ordered by what blocks a first sign-in. **All four are done**; each item says ho
6. **The executable's identity.** *Done.* Windows takes the icon and the version block from a 6. **The executable's identity.** *Done.* Windows takes the icon and the version block from a
resource compiled into the `.exe`, not from a `.desktop` file. resource compiled into the `.exe`, not from a `.desktop` file.
[`apps/darkroom-desktop/build.rs`](../apps/darkroom-desktop/build.rs) uses `winresource` [`apps/darkroom-desktop/build.rs`](../../apps/darkroom-desktop/build.rs) uses `winresource`
(which invokes MinGW's `windres` when cross-compiling) to embed (which invokes MinGW's `windres` when cross-compiling) to embed
[`ui/dr-ui/ui/app-icon.png`](../ui/dr-ui/ui/app-icon.png) — wrapped into an `.ico` in [`ui/dr-ui/ui/app-icon.png`](../../ui/dr-ui/ui/app-icon.png) — wrapped into an `.ico` in
`OUT_DIR` at build time, since an ICO entry may be a PNG, so no generated binary is committed — `OUT_DIR` at build time, since an ICO entry may be a PNG, so no generated binary is committed —
plus the version from `CARGO_PKG_VERSION` and the product name. The script returns before plus the version from `CARGO_PKG_VERSION` and the product name. The script returns before
touching the crate on every other target, and `winresource` is an unconditional touching the crate on every other target, and `winresource` is an unconditional
build-dependency because **a `cfg(windows)` on a build-dependency is evaluated against the build-dependency because **a `cfg(windows)` on a build-dependency is evaluated against the
host**, which is Linux. This is the fifth place the identifier lives, and host**, which is Linux. This is the fifth place the identifier lives, and
[`tools/set-version.sh`](../tools/set-version.sh) does not need to learn it: the resource [`tools/set-version.sh`](../../tools/set-version.sh) does not need to learn it: the resource
reads the version cargo already knows. The same commit made the release binary a GUI-subsystem reads the version cargo already knows. The same commit made the release binary a GUI-subsystem
executable (`windows_subsystem = "windows"`), or Windows keeps a console window open behind executable (`windows_subsystem = "windows"`), or Windows keeps a console window open behind
the application. the application.
@@ -257,7 +257,7 @@ and running them means Wine. §6 does that for exactly one binary, deliberately.
## 5. The installer ## 5. The installer
[`packaging/windows/darkroom.nsi`](../packaging/windows/darkroom.nsi), compiled by `makensis` on [`packaging/windows/darkroom.nsi`](../../packaging/windows/darkroom.nsi), compiled by `makensis` on
the runner into `DarkRoom-<version>-x86_64-setup.exe`. `package.sh` passes the version in the runner into `DarkRoom-<version>-x86_64-setup.exe`. `package.sh` passes the version in
(`/DVERSION=…`, from `tools/set-version.sh`'s single source, the workspace `Cargo.toml`) and refuses (`/DVERSION=…`, from `tools/set-version.sh`'s single source, the workspace `Cargo.toml`) and refuses
to run if any `models/face/*.onnx` is smaller than 100 KB — the LFS-pointer guard every other to run if any `models/face/*.onnx` is smaller than 100 KB — the LFS-pointer guard every other
@@ -370,7 +370,7 @@ say which of these were checked and on what.
## 7. The CI job ## 7. The CI job
A fourth leg of [`build-and-test.yml`](../.gitea/workflows/build-and-test.yml), beside desktop, A fourth leg of [`build-and-test.yml`](../../.gitea/workflows/build-and-test.yml), beside desktop,
Android and traceability: Android and traceability:
```yaml ```yaml
+8 -8
View File
@@ -5,7 +5,7 @@ this page was captured from the desktop build driving itself — nothing is a
mock-up, and nothing has been retouched outside DarkRoom. Where a feature is mock-up, and nothing has been retouched outside DarkRoom. Where a feature is
better seen moving, it moves. better seen moving, it moves.
The requirements behind each feature are in [requirements.md](../requirements.md); The requirements behind each feature are in [requirements.md](../dev/requirements.md);
the reasoning is in the design documents linked from each section. This page the reasoning is in the design documents linked from each section. This page
is only about what you see. is only about what you see.
@@ -197,19 +197,19 @@ sidecars, and a diagnostics bundle for a bug report.
Face detection and identity run over the library and group faces by person; Face detection and identity run over the library and group faces by person;
the `Identity` page is where suggestions are confirmed, rejected and split, the `Identity` page is where suggestions are confirmed, rejected and split,
and `People` on the filter bar narrows the grid to someone. Not pictured and `People` on the filter bar narrows the grid to someone. Not pictured
here, for the obvious reason — [faces.md](../faces.md) has the design. here, for the obvious reason — [faces.md](../dev/faces.md) has the design.
## Where things are written down ## Where things are written down
| Feature | Design | | Feature | Design |
|---|---| |---|---|
| Local masks and segmentation | [segmentation.md](../segmentation.md), [mask-editing.md](../mask-editing.md) | | Local masks and segmentation | [segmentation.md](../dev/segmentation.md), [mask-editing.md](../dev/mask-editing.md) |
| Repair | [spot-removal.md](../spot-removal.md) | | Repair | [spot-removal.md](../dev/spot-removal.md) |
| Panorama | [panorama.md](../panorama.md) | | Panorama | [panorama.md](../dev/panorama.md) |
| Faces and identity | [faces.md](../faces.md) | | Faces and identity | [faces.md](../dev/faces.md) |
| Gestures, generated from the code | [gestures.md](../gestures.md) | | Gestures, generated from the code | [gestures.md](../gestures.md) |
| Navigation and layout | [ui-navigation.md](../ui-navigation.md) | | Navigation and layout | [ui-navigation.md](../dev/ui-navigation.md) |
| Sync and storage | [storage.md](../storage.md) | | Sync and storage | [storage.md](../dev/storage.md) |
## How this page is made ## How this page is made
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More