Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
031315bdb6 | ||
|
|
00c028c8c8 | ||
|
|
bddf3250c5 | ||
|
|
41486bd59b | ||
|
|
d6d27fb062 | ||
|
|
317a2f40bd | ||
|
|
949fe40d5b | ||
|
|
2be80d4203 | ||
|
|
9ebaa15099 | ||
|
|
aee355fada | ||
|
|
7fa3176f88 | ||
|
|
af162dd010 | ||
|
|
f5300a9f43 | ||
|
|
b19d470189 | ||
|
|
bfadd9c409 | ||
|
|
050b2a3914 | ||
|
|
e166b64ee9 | ||
|
|
a773ad5c27 | ||
|
|
1e472fd251 | ||
|
|
6bf67cefc4 | ||
|
|
00e2fe6aaf | ||
|
|
402dcdc24c | ||
|
|
94b39410bc | ||
|
|
2014c80e62 | ||
|
|
fbfa891296 | ||
|
|
aee62dc7f2 | ||
|
|
84fade99ec | ||
|
|
3bfa73d1e1 | ||
|
|
4616cb0a23 | ||
|
|
cc73ea3153 | ||
|
|
f6ff5eabd9 | ||
|
|
14ed1dc410 | ||
|
|
38819da222 | ||
|
|
d6e9c7dc94 | ||
|
|
9c8f21b754 | ||
|
|
bd7d75522d | ||
|
|
4ef1b74f2f | ||
|
|
e86edef47c | ||
|
|
0aff5e6c8c | ||
|
|
5458314083 | ||
|
|
cb7b9d71c4 | ||
|
|
beb7a5eac0 | ||
|
|
262ed2553c | ||
|
|
8d08ffd7b7 | ||
|
|
8540518022 | ||
|
|
b952f5976a | ||
|
|
a1d511fd4b | ||
|
|
050c2c9d16 | ||
|
|
bd3b993b90 | ||
|
|
59605f9fbb | ||
|
|
04949741c1 | ||
|
|
6b1aac477d |
@@ -1,6 +1,6 @@
|
||||
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
|
||||
# … 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
|
||||
# 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
|
||||
# docs/frame-budget.md by hand, as they already are.
|
||||
# docs/dev/frame-budget.md by hand, as they already are.
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -183,7 +183,7 @@ jobs:
|
||||
- name: Frame budget (FR-DSP-3)
|
||||
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
|
||||
# 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
|
||||
|
||||
@@ -7,6 +7,10 @@ name: Build and test
|
||||
on:
|
||||
push:
|
||||
branches: [main, master, develop]
|
||||
# A release tag builds again and publishes what it built (the `release`
|
||||
# job at the end). The master push of the same commit has usually filled
|
||||
# the caches, so the second run is the warm one.
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
branches: [main, master, develop]
|
||||
|
||||
@@ -154,6 +158,16 @@ jobs:
|
||||
- name: Build
|
||||
run: cargo build --workspace --release
|
||||
|
||||
# Only on a release tag: the binary is 150 MB and nothing but the
|
||||
# release job wants it.
|
||||
- name: Upload the desktop binary
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: darkroom-desktop-x86_64-linux
|
||||
path: target/release/darkroom-desktop
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Disk after
|
||||
if: always()
|
||||
run: df -h /workspace 2>/dev/null || df -h .
|
||||
@@ -322,7 +336,7 @@ jobs:
|
||||
env:
|
||||
CARGO_TARGET_DIR: target-android
|
||||
# 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.
|
||||
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||
KEYSTORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
@@ -370,7 +384,7 @@ jobs:
|
||||
|
||||
# TRACES: FR-PLAT-WIN-3
|
||||
# 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
|
||||
# MinGW runtime imports, starts under Wine, and that the installer installs
|
||||
# and uninstalls under Wine. What it cannot prove — a Vulkan device, a
|
||||
@@ -454,7 +468,12 @@ jobs:
|
||||
wine "$SETUP" /S 2>/dev/null
|
||||
INST=$(echo "$HOME"/.wine/drive_c/users/*/AppData/Local/Programs/DarkRoom)
|
||||
ls "$INST"
|
||||
[ "$(ls "$INST/models" | wc -l)" = 7 ] || { echo "FAIL: expected 7 model files"; exit 1; }
|
||||
# As many files as package.sh stages: everything but the READMEs in
|
||||
# the directories it copies. A literal here went stale the first
|
||||
# time a model was added.
|
||||
WANT=$(find models/face models/scene models/inpaint -maxdepth 1 -type f ! -name README.md | wc -l)
|
||||
GOT=$(ls "$INST/models" | wc -l)
|
||||
[ "$GOT" = "$WANT" ] || { echo "FAIL: expected $WANT model files, installed $GOT"; exit 1; }
|
||||
wine reg query 'HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\DarkRoom' 2>/dev/null \
|
||||
| grep -q DisplayVersion || { echo "FAIL: no uninstall registry key"; exit 1; }
|
||||
wine "$INST/darkroom.exe" --version 2>/dev/null | grep -q '^darkroom-desktop ' \
|
||||
@@ -514,3 +533,47 @@ jobs:
|
||||
fi
|
||||
done
|
||||
exit $FAILED
|
||||
|
||||
# A v* tag becomes a Gitea Release carrying the three builds and their
|
||||
# SHA256SUMS, titled and described by the tag's message. Until this job
|
||||
# existed every release was made by hand, and most tags never got one.
|
||||
#
|
||||
# It needs all three platform jobs, so a tag whose tests fail publishes
|
||||
# nothing; re-run the failed job and this one follows. The work is
|
||||
# tools/publish-release.sh, which is also how a release is finished by hand.
|
||||
release:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [desktop, android, windows]
|
||||
runs-on: linux/amd64
|
||||
name: Publish the release
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Fetch the builds
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: dist
|
||||
|
||||
# Named for the download page, with the version in each name the way
|
||||
# the hand-made releases had them. The installer already carries its
|
||||
# version from package.sh.
|
||||
- name: Publish
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN || github.token }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -e
|
||||
V="${TAG#v}"
|
||||
ls -lR dist
|
||||
mkdir -p out
|
||||
cp dist/darkroom-arm64-v8a-apk/darkroom.apk "out/darkroom-${V}-arm64-v8a.apk"
|
||||
cp dist/darkroom-desktop-x86_64-linux/darkroom-desktop "out/darkroom-desktop-${V}-x86_64-linux"
|
||||
chmod +x "out/darkroom-desktop-${V}-x86_64-linux"
|
||||
cp dist/darkroom-windows-x86_64-setup/DarkRoom-${V}-x86_64-setup.exe out/
|
||||
bash tools/publish-release.sh "$TAG" out/*
|
||||
|
||||
@@ -7,7 +7,7 @@ name: Traceability
|
||||
# fail its own threshold. Two rules follow, and the extractor's own tests
|
||||
# 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.
|
||||
#
|
||||
# This job is static analysis of source comments plus markdown parsing, so it
|
||||
@@ -74,11 +74,11 @@ jobs:
|
||||
run: |
|
||||
set -e
|
||||
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 "docs/traceability.md is out of date."
|
||||
echo "docs/dev/traceability.md is out of date."
|
||||
echo "Run: cargo run -p traceability -- report"
|
||||
git diff --stat docs/traceability.md
|
||||
git diff --stat docs/dev/traceability.md
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -127,4 +127,4 @@ jobs:
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: head -30 docs/traceability.md || true
|
||||
run: head -30 docs/dev/traceability.md || true
|
||||
|
||||
@@ -26,7 +26,7 @@ fi
|
||||
# The artefacts are generated from the tree, so regenerating them because one
|
||||
# was itself edited would be circular.
|
||||
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
|
||||
;;
|
||||
esac
|
||||
@@ -41,9 +41,9 @@ if ! cargo run -q -p traceability -- report >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! git diff --quiet -- docs/traceability.md; then
|
||||
git add docs/traceability.md
|
||||
echo "pre-commit: regenerated docs/traceability.md and staged it"
|
||||
if ! git diff --quiet -- docs/dev/traceability.md; then
|
||||
git add docs/dev/traceability.md
|
||||
echo "pre-commit: regenerated docs/dev/traceability.md and staged it"
|
||||
fi
|
||||
|
||||
# The gesture vocabulary, same discipline.
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
## 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
|
||||
24k-image library (2026-09-19), and what each fix looked like.
|
||||
|
||||
|
||||
+11
-11
@@ -90,18 +90,18 @@ break it by accident:
|
||||
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
|
||||
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
|
||||
touched the catalog, the decoder, the thumbnail store or the exporter, run it
|
||||
before you send.
|
||||
|
||||
## Requirements and traceability
|
||||
|
||||
[`requirements.md`](docs/requirements.md) is the register of record.
|
||||
[`traceability.md`](docs/traceability.md) is generated from `TRACES:` tags in
|
||||
[`requirements.md`](docs/dev/requirements.md) is the register of record.
|
||||
[`traceability.md`](docs/dev/traceability.md) is generated from `TRACES:` tags in
|
||||
the source and must never be hand-edited:
|
||||
|
||||
```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.
|
||||
|
||||
**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
|
||||
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
|
||||
@@ -163,12 +163,12 @@ One commit per change. If you fixed two things, that is two commits.
|
||||
| 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 |
|
||||
| [`docs/architecture.md`](docs/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/benchmarks.md`](docs/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/distribution.md`](docs/distribution.md) | Packaging a build, or adding a permission to one |
|
||||
| [`docs/requirements.md`](docs/requirements.md) | Reference, not reading |
|
||||
| [`docs/dev/architecture.md`](docs/dev/architecture.md) | Anything touching the render path, catalog or sync |
|
||||
| [`docs/dev/code-health.md`](docs/dev/code-health.md) | Deciding what to work on; grades each seam by what it costs |
|
||||
| [`docs/dev/benchmarks.md`](docs/dev/benchmarks.md) | A change that could plausibly cost time or memory |
|
||||
| [`docs/dev/technical-debt.md`](docs/dev/technical-debt.md) | Something looks wrong — check it was not chosen |
|
||||
| [`docs/dev/distribution.md`](docs/dev/distribution.md) | Packaging a build, or adding a permission to one |
|
||||
| [`docs/dev/requirements.md`](docs/dev/requirements.md) | Reference, not reading |
|
||||
|
||||
`technical-debt.md` is the one to check before "fixing" anything surprising.
|
||||
It records compromises that were deliberate, each with the reasoning and a
|
||||
|
||||
Generated
+25
-25
@@ -1221,7 +1221,7 @@ checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
|
||||
|
||||
[[package]]
|
||||
name = "darkroom-android"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"android_logger",
|
||||
"dr-plat",
|
||||
@@ -1234,7 +1234,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "darkroom-desktop"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dr-plat",
|
||||
@@ -1408,7 +1408,7 @@ checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
||||
|
||||
[[package]]
|
||||
name = "dr-bench"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dr-catalog",
|
||||
@@ -1425,7 +1425,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-catalog"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"dr-face",
|
||||
"dr-plat",
|
||||
@@ -1440,7 +1440,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-decode"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"dr-types",
|
||||
"env_logger",
|
||||
@@ -1454,7 +1454,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-export"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"dr-decode",
|
||||
"dr-gpu",
|
||||
@@ -1473,7 +1473,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-face"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"dr-inference-engine",
|
||||
"env_logger",
|
||||
@@ -1486,7 +1486,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-film"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -1495,7 +1495,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-gpu"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"dr-decode",
|
||||
@@ -1513,7 +1513,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-inference-engine"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"env_logger",
|
||||
"libloading",
|
||||
@@ -1528,7 +1528,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-ingest"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"dr-plat",
|
||||
"dr-types",
|
||||
@@ -1540,7 +1540,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-lens"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"lensfun",
|
||||
"log",
|
||||
@@ -1548,7 +1548,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-pano"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"dr-decode",
|
||||
"dr-inference-engine",
|
||||
@@ -1562,7 +1562,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-pipeline"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"dr-types",
|
||||
"log",
|
||||
@@ -1571,7 +1571,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-plat"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"android-native-keyring-store",
|
||||
"dr-types",
|
||||
@@ -1587,7 +1587,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-preset-xmp"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"dr-pipeline",
|
||||
"log",
|
||||
@@ -1597,7 +1597,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-segment"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"dr-inference-engine",
|
||||
"env_logger",
|
||||
@@ -1610,7 +1610,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-sync"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"dr-plat",
|
||||
@@ -1624,7 +1624,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-sync-folder"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"dr-sync",
|
||||
@@ -1636,7 +1636,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-sync-nextcloud"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"dr-decode",
|
||||
@@ -1658,7 +1658,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-thumbs"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"dr-types",
|
||||
"jpeg-encoder",
|
||||
@@ -1670,7 +1670,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-types"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1679,7 +1679,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-ui"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1722,7 +1722,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dr-xmp"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"dr-types",
|
||||
"log",
|
||||
@@ -7023,7 +7023,7 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||
|
||||
[[package]]
|
||||
name = "traceability"
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
|
||||
+2
-2
@@ -29,7 +29,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.13.6"
|
||||
version = "0.14.1"
|
||||
edition = "2021"
|
||||
rust-version = "1.92"
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -48,7 +48,7 @@ dr-export = { path = "core/dr-export" }
|
||||
dr-face = { path = "core/dr-face", default-features = false }
|
||||
dr-film = { path = "core/dr-film" }
|
||||
# `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-ingest = { path = "core/dr-ingest" }
|
||||
dr-gpu = { path = "core/dr-gpu" }
|
||||
|
||||
@@ -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 |
|
||||
| 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 |
|
||||
|
||||
Or build it. Git LFS is required for the model weights, and the toolchain
|
||||
@@ -76,30 +76,30 @@ controls, its place in the chain and its tests.
|
||||
|
||||
## Where it stands
|
||||
|
||||
**0.13.6**, twenty tagged releases in. 188 numbered requirements in
|
||||
scope, 82% of them claimed by code and [traced to it](docs/traceability.md);
|
||||
**0.14.1**, twenty-two tagged releases in. 188 numbered requirements in
|
||||
scope, 82% of them claimed by code and [traced to it](docs/dev/traceability.md);
|
||||
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
|
||||
focus stacking, most of the Android platform integration beyond running,
|
||||
and the Flatpak's library chooser. The performance targets are half
|
||||
verified: the per-commit benchmark suite §8 requires exists for everything
|
||||
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.
|
||||
[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.
|
||||
|
||||
**The one deliberate compromise worth knowing about before reading
|
||||
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
|
||||
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.
|
||||
|
||||
## 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 |
|
||||
| [requirements.md](docs/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 |
|
||||
| [technical-debt.md](docs/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 |
|
||||
| [code-health.md](docs/code-health.md) | What a contribution costs, per seam, measured |
|
||||
| [traceability.md](docs/traceability.md) | Generated: which requirement is claimed by which file |
|
||||
| [requirements.md](docs/dev/requirements.md) | What the software must do — the numbered register, and the decisions |
|
||||
| [architecture.md](docs/dev/architecture.md) | How it is built — crates, the GPU pipeline, the data model, sync |
|
||||
| [technical-debt.md](docs/dev/technical-debt.md) | Compromises taken deliberately, each with the condition that retires it |
|
||||
| [outstanding.md](docs/dev/outstanding.md) | What is not built, and whether that is a decision or a gap |
|
||||
| [code-health.md](docs/dev/code-health.md) | What a contribution costs, per seam, measured |
|
||||
| [traceability.md](docs/dev/traceability.md) | Generated: which requirement is claimed by which file |
|
||||
|
||||
Designs, one per subsystem:
|
||||
[segmentation](docs/segmentation.md) and [mask editing](docs/mask-editing.md) ·
|
||||
[spot removal](docs/spot-removal.md) · [panorama](docs/panorama.md) ·
|
||||
[faces](docs/faces.md) · [inference](docs/inference.md) ·
|
||||
[storage and sync](docs/storage.md) · [catalog](docs/catalog.md) ·
|
||||
[display and extension](docs/display-and-extension.md) ·
|
||||
[navigation](docs/ui-navigation.md) · [distribution](docs/distribution.md) ·
|
||||
[windows](docs/windows.md) · [benchmarks](docs/benchmarks.md).
|
||||
[segmentation](docs/dev/segmentation.md) and [mask editing](docs/dev/mask-editing.md) ·
|
||||
[spot removal](docs/dev/spot-removal.md) · [panorama](docs/dev/panorama.md) ·
|
||||
[faces](docs/dev/faces.md) · [inference](docs/dev/inference.md) ·
|
||||
[storage and sync](docs/dev/storage.md) · [catalog](docs/dev/catalog.md) ·
|
||||
[display and extension](docs/dev/display-and-extension.md) ·
|
||||
[navigation](docs/dev/ui-navigation.md) · [distribution](docs/dev/distribution.md) ·
|
||||
[windows](docs/dev/windows.md) · [benchmarks](docs/dev/benchmarks.md).
|
||||
|
||||
## Licence
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ fn android_main(app: slint::android::AndroidApp) {
|
||||
///
|
||||
/// **Face weights are absent from the repository by design.** The InsightFace
|
||||
/// 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
|
||||
/// 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.
|
||||
//
|
||||
// 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
|
||||
// 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
|
||||
// eyes-open filter has something to read, and a tablet has no other way
|
||||
// to get them either.
|
||||
//
|
||||
// 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.
|
||||
const BUNDLED: [(&std::ffi::CStr, &str); 14] = [
|
||||
(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
|
||||
// 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
|
||||
// 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());
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ fn main() -> anyhow::Result<()> {
|
||||
// build made on a machine that cannot run the application — the Linux CI
|
||||
// producing the Windows binary, checked under Wine — has an exit that
|
||||
// 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") {
|
||||
println!("darkroom-desktop {}", env!("CARGO_PKG_VERSION"));
|
||||
return Ok(());
|
||||
@@ -63,7 +63,7 @@ fn main() -> anyhow::Result<()> {
|
||||
|
||||
// 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
|
||||
// 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::run(paths)?;
|
||||
@@ -78,7 +78,7 @@ fn main() -> anyhow::Result<()> {
|
||||
|
||||
/// Where a desktop package may have put `libonnxruntime`, most specific
|
||||
/// 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
|
||||
/// installed — the wheel's `capi` directory, say. Then beside the executable
|
||||
|
||||
@@ -315,7 +315,7 @@ fn full_library(
|
||||
|
||||
// The three phases, separately, because "a regroup takes n seconds" does
|
||||
// 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 flat: Vec<f32> = candidates
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
//! 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
|
||||
//! 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.
|
||||
//!
|
||||
//! # Grouping is not hiding
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! 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
|
||||
//! (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
|
||||
//! 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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! 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.
|
||||
//!
|
||||
//! 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
|
||||
//! 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
|
||||
/// the blob and not only beside it. Readers re-normalise on load.
|
||||
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,
|
||||
/// Length of the raw embedding before normalisation — the model's own
|
||||
/// 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
|
||||
/// 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
|
||||
/// 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*
|
||||
@@ -2486,7 +2486,7 @@ mod tests {
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// different people, so those two must land either side.
|
||||
#[test]
|
||||
|
||||
@@ -789,7 +789,7 @@ fn attached_has_table(conn: &Connection, schema: &str, table: &str) -> Result<bo
|
||||
/// # What travels, and what is recomputed
|
||||
///
|
||||
/// 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):
|
||||
///
|
||||
/// - **People** — uuid, name, and whether the user set them aside. Merged by
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
//! # The one thing a rebuild does not recover
|
||||
//!
|
||||
//! **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
|
||||
//! 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.
|
||||
@@ -570,7 +570,7 @@ mod tests {
|
||||
// The first NFR-R6 branch, asserted on the thing that distinguishes 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
|
||||
// readable file (docs/catalog.md §8.1).
|
||||
// readable file (docs/dev/catalog.md §8.1).
|
||||
let dir = tempdir("restore");
|
||||
let path = dir.join("catalog.sqlite");
|
||||
fixture(&path, 500);
|
||||
|
||||
@@ -1009,7 +1009,7 @@ CREATE INDEX face_index_model ON face_index(model_id);
|
||||
|
||||
const V8: &str = r#"
|
||||
-- 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,
|
||||
-- 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
|
||||
detector_confidence REAL NOT NULL,
|
||||
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
|
||||
-- the §8 calibration -- FR-CULL-9 names face size as an axis along which an
|
||||
|
||||
@@ -11,7 +11,7 @@ log.workspace = true
|
||||
|
||||
# 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
|
||||
# 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 }
|
||||
dr-inference-engine = { workspace = true, optional = true }
|
||||
ndarray = { workspace = true, optional = true }
|
||||
@@ -37,7 +37,7 @@ required-features = ["inference"]
|
||||
|
||||
[features]
|
||||
# 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
|
||||
# script, and the InsightFace grant does not survive that.
|
||||
default = []
|
||||
|
||||
@@ -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
|
||||
//! whether soft ones are refused — so with `--dump DIR` the crops the
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! DET.onnx EMB.onnx photo.jpg [photo.jpg ...]
|
||||
//!
|
||||
//! 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;
|
||||
|
||||
|
||||
@@ -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
|
||||
//! on. `det_500m.onnx` has a dynamic H/W input, which is exactly what tract
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
//!
|
||||
//! # 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,
|
||||
//! 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
|
||||
|
||||
@@ -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
|
||||
//! arrangement. Feeding the model a plain bounding-box crop *works* — it
|
||||
@@ -208,7 +208,7 @@ impl Similarity {
|
||||
///
|
||||
/// # 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
|
||||
/// 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
|
||||
@@ -427,7 +427,7 @@ fn sample_window(
|
||||
// ── eyes ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/// 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;
|
||||
/// Height of an eye crop as the classifier reads it, in pixels.
|
||||
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
|
||||
/// 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
|
||||
/// 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.
|
||||
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
|
||||
/// 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
|
||||
/// 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] =
|
||||
[(0.0, 0.0, 112.0, 112.0), (-5.0, -14.0, 122.0, 122.0)];
|
||||
|
||||
|
||||
@@ -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
|
||||
//! 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
|
||||
//! 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,
|
||||
//! 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
|
||||
@@ -83,7 +83,7 @@ pub struct 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.
|
||||
///
|
||||
/// **`valid` is false**, and that is the point. It is a documented
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! 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
|
||||
//! 40×24 eye and answers P(open). **SGC** — *sunglasses classification*,
|
||||
|
||||
@@ -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
|
||||
//! subsystem's accuracy actually lives, so it is testable with no weights on
|
||||
|
||||
@@ -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
|
||||
//! face — the landmarks being the reason for this detector rather than a
|
||||
@@ -138,7 +138,7 @@ impl Detection {
|
||||
pub struct Detector {
|
||||
session: Model,
|
||||
/// 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,
|
||||
/// 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.
|
||||
///
|
||||
/// 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
|
||||
/// padding, producing detections that look plausible and embeddings that
|
||||
/// quietly cluster badly three stages later.
|
||||
@@ -372,7 +372,7 @@ impl Letterbox {
|
||||
///
|
||||
/// `(x·255 − 127.5) / 128` — note `/128`, not `/127.5`. The reference
|
||||
/// 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
|
||||
/// reads least as an edge, where black would draw a hard border across the
|
||||
|
||||
@@ -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
|
||||
//! 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> {
|
||||
// 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 acquired = loaded.acquire()?;
|
||||
let session = acquired.lock();
|
||||
|
||||
@@ -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
|
||||
//! 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
|
||||
/// between a match and a non-match.
|
||||
#[test]
|
||||
|
||||
@@ -67,14 +67,14 @@ pub const SUNGLASSES_THRESHOLD: f32 = 0.5;
|
||||
/// 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
|
||||
/// 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;
|
||||
|
||||
/// Least [`Eye::sharpness`] for the eye to be read.
|
||||
///
|
||||
/// 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
|
||||
/// 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;
|
||||
|
||||
/// 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.
|
||||
/// 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
|
||||
/// 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! 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
|
||||
//! point is loose enough that a window centred on it left the eye in a
|
||||
|
||||
@@ -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
|
||||
//! 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
|
||||
//! 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,
|
||||
//! 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
|
||||
//!
|
||||
@@ -55,7 +55,7 @@ pub mod references;
|
||||
/// Smallest long edge a face crop may be sampled from.
|
||||
///
|
||||
/// **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
|
||||
/// [`warp`] samples the 112×112 the embedder sees and so converts source
|
||||
/// resolution directly into embedding quality. FR-CULL-8 requires that crop to
|
||||
|
||||
@@ -115,7 +115,7 @@ pub struct Faces<'a> {
|
||||
/// Source pixels across the aligned crop, for the calibration's size term.
|
||||
pub crop_px: &'a [f32],
|
||||
/// 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],
|
||||
/// Which faces may be compared *against* — the gallery
|
||||
/// ([`crate::embedding::MIN_GALLERY_QUALITY`]).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! 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
|
||||
//! active operation into **one dispatch over a viewport-sized target**, so the
|
||||
//! 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
|
||||
//! 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.
|
||||
//!
|
||||
//! # Why the 99th percentile and not the mean
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! 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
|
||||
//! person means. No amount of design settles that — the pictures do.
|
||||
//!
|
||||
|
||||
@@ -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
|
||||
//! 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
|
||||
//! 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
|
||||
//! 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
|
||||
@@ -36,7 +36,7 @@ pub struct SegmentOptions {
|
||||
/// Longest proxy edge. The segmentation runs here, not at sensor
|
||||
/// 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
|
||||
/// is a separate stage (docs/segmentation.md §4).
|
||||
/// is a separate stage (docs/dev/segmentation.md §4).
|
||||
pub max_edge: u32,
|
||||
/// 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
|
||||
@@ -69,7 +69,7 @@ impl Default for SegmentOptions {
|
||||
w_chroma: 0.5,
|
||||
// **Zero: the pass is off.** It is implemented, dispatched
|
||||
// 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
|
||||
// improvement, so the default declines to pay.
|
||||
plateau_iterations: 0,
|
||||
@@ -486,7 +486,7 @@ impl Segmentation {
|
||||
/// a region graph of a few thousand nodes that every later interaction
|
||||
/// 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
|
||||
/// 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
|
||||
@@ -724,7 +724,7 @@ mod tests {
|
||||
px
|
||||
}
|
||||
#[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() {
|
||||
// F1, asserted rather than eyeballed, and asserted at the level where
|
||||
// it matters.
|
||||
@@ -741,7 +741,7 @@ mod tests {
|
||||
// with no exit anywhere — cannot be drained by a distance that has
|
||||
// nowhere to descend to, and collapsing it fully would need connected
|
||||
// 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 (w, h) = (96u32, 96u32);
|
||||
let src = DemosaicedImage::from_rgba8(&ctx, &ramp(w, h), w, h).expect("source");
|
||||
|
||||
@@ -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:
|
||||
//
|
||||
@@ -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
|
||||
// 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
|
||||
// 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
|
||||
// geodesic distance to the nearest pixel that *does* have a lower neighbour,
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
//!
|
||||
//! 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
|
||||
//! 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
|
||||
//! 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
|
||||
//! being true: it renders the **whole point-operation chain** through the real
|
||||
//! `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
|
||||
//! 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
|
||||
//! `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
|
||||
//! 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
|
||||
@@ -81,7 +81,7 @@ const SOURCE: (u32, u32) = (6000, 4000);
|
||||
/// 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
|
||||
/// 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
|
||||
/// suite time to re-establish a conclusion 4.1 M pixels already establishes.
|
||||
const VIEWPORT: (u32, u32) = (2560, 1600);
|
||||
@@ -166,7 +166,7 @@ impl Run {
|
||||
judged <= BUDGET_MS,
|
||||
"{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). \
|
||||
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.",
|
||||
viewport.0,
|
||||
viewport.1,
|
||||
|
||||
@@ -326,7 +326,7 @@ fn a_proxy_and_an_export_agree_about_the_effect() {
|
||||
|
||||
#[test]
|
||||
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
|
||||
// the viewport: `LocalContrast::reduction` steps 4 -> 2 -> 1 as sigma
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
//! code path — the zoom is the full-resolution path — which is why the
|
||||
//! 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`
|
||||
//! 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.
|
||||
|
||||
@@ -6,7 +6,7 @@ rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
# 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.
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -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,
|
||||
//! for an `OrtApi` — a struct of function pointers. Two things can fill it:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! 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
|
||||
//! context model. Both are opaque to this crate, which tracks only *that* a
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! 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;
|
||||
//! what built it — tract on one core, ONNX Runtime's CPU pool, a TensorRT
|
||||
@@ -35,13 +35,13 @@ pub enum Role {
|
||||
Embedder,
|
||||
Segmenter,
|
||||
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,
|
||||
/// The eye-state and sunglasses classifiers, a few hundred kilobytes.
|
||||
EyeClassifier,
|
||||
/// XFeat, the panorama keypoint detector (docs/panorama.md).
|
||||
/// XFeat, the panorama keypoint detector (docs/dev/panorama.md).
|
||||
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
|
||||
/// the Hexagon are the point of it.
|
||||
Inpainter,
|
||||
@@ -516,7 +516,7 @@ mod tests {
|
||||
|
||||
/// The smallest shipped graph, if this checkout has the weights; a test
|
||||
/// 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>> {
|
||||
let path = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
|
||||
@@ -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
|
||||
//! the floor. Both halves matter: a provider can register and then fail at
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ log.workspace = true
|
||||
|
||||
# Inference for the learned keypoint detector, on the same footing as
|
||||
# `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
|
||||
# that tests without a model.
|
||||
ort = { workspace = true, optional = true }
|
||||
|
||||
@@ -95,7 +95,7 @@ pub struct Params {
|
||||
/// 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
|
||||
/// 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
|
||||
/// hole pulls in whatever is that far from the edge — a ridge, a peak —
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
//! 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
|
||||
//! 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`:
|
||||
//! input `1×4×512×512` float — channel 0 is `mask − 0.5` with 1 where the
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! 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`
|
||||
//! `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
|
||||
//! (S15.2, S15.4).
|
||||
|
||||
@@ -37,7 +37,7 @@ pub struct XFeat {
|
||||
}
|
||||
|
||||
/// 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")]
|
||||
pub fn embedded_model_bytes() -> [&'static [u8]; 2] {
|
||||
[EMBEDDED_LANDSCAPE, EMBEDDED_PORTRAIT]
|
||||
|
||||
@@ -315,7 +315,7 @@ pub struct DetailPass {
|
||||
/// 52 render pixels at 4K — holds no spatial frequency a quarter-scale
|
||||
/// grid cannot represent. Computing it at the render size therefore buys
|
||||
/// 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
|
||||
/// 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
|
||||
@@ -568,7 +568,7 @@ pub fn compose_detail(
|
||||
/// photograph the photographer thinks they are sharpening.
|
||||
///
|
||||
/// 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.
|
||||
pub fn compose_detail_with(
|
||||
ops: &[Box<dyn Operation>],
|
||||
|
||||
@@ -113,7 +113,7 @@ pub struct EditGraph {
|
||||
/// a sidecar comes to name one stock while the shader draws another.
|
||||
film: Option<Film>,
|
||||
/// 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
|
||||
/// 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.
|
||||
spots: SpotSet,
|
||||
/// 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
|
||||
/// but about direction. Every [`Operation`] is a function from colour to
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
//! [`MaskSource::Regions`] stores integers naming regions in the segmentation
|
||||
//! 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
|
||||
//! 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
|
||||
//! 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.
|
||||
/// 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
|
||||
/// own (docs/segmentation.md §5).
|
||||
/// own (docs/dev/segmentation.md §5).
|
||||
Regions {
|
||||
/// Which segmentation these ids index into.
|
||||
///
|
||||
@@ -590,7 +590,7 @@ pub enum MaskSource {
|
||||
/// **The primary way a local adjustment is made.** The watershed hierarchy
|
||||
/// this crate was first built around does not survive a photograph: its
|
||||
/// 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.
|
||||
///
|
||||
/// 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
|
||||
/// — 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
|
||||
/// 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
|
||||
/// 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
|
||||
/// 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
|
||||
/// 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 {
|
||||
// FNV-1a over the four fields. Small, dependency-free, and adequate: this
|
||||
// guards against accidental mismatch, not against a forged sidecar.
|
||||
|
||||
@@ -54,7 +54,7 @@ pub enum Affects {
|
||||
/// A pixel's *neighbourhood* — sharpening, noise reduction, clarity,
|
||||
/// 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
|
||||
/// 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
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
//! 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
|
||||
//! — 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
|
||||
//! their structuring elements**. The minimum over a contiguous run of `d`
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
//! the artefact this control must not have.
|
||||
//!
|
||||
//! 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
|
||||
//! a grid a quarter the size on each axis: a sixteenth of the pixels at a
|
||||
//! quarter of the radius.
|
||||
@@ -271,7 +271,7 @@ impl Band for Coarse {
|
||||
threshold: 0.35,
|
||||
gain: 1.0,
|
||||
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
|
||||
// spatial frequency anywhere near the quarter-scale Nyquist of one
|
||||
|
||||
@@ -217,7 +217,7 @@ pub struct Version {
|
||||
/// graph's film cleared and the caller re-bakes — see `EditGraph::set_film`.
|
||||
pub film: Option<FilmRef>,
|
||||
/// 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
|
||||
/// mask gets: a spot is eight numbers, and sixty-four blocks would bury the
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//! 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
|
||||
//! 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
|
||||
//!
|
||||
@@ -61,7 +61,7 @@ pub const MAX_SPOTS: usize = 64;
|
||||
/// 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
|
||||
/// 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;
|
||||
|
||||
/// 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
|
||||
/// half of it.** The good half searches the photograph for a patch whose
|
||||
/// 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
|
||||
/// 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
|
||||
|
||||
@@ -13,7 +13,7 @@ log.workspace = true
|
||||
|
||||
# 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
|
||||
# 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 }
|
||||
dr-inference-engine = { workspace = true, optional = true }
|
||||
ndarray = { workspace = true, optional = true }
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
//! And doing it here buys two things a shader could not. It is **exactly
|
||||
//! 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
|
||||
//! 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.
|
||||
//!
|
||||
//! # The transform
|
||||
|
||||
@@ -40,7 +40,7 @@ pub struct Edge {
|
||||
|
||||
/// 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
|
||||
/// consumers above cannot tell which.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! 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
|
||||
//! can snap a mask to one. This crate is that map, and it is deliberately
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! 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
|
||||
//! than §4 assumed: the two arms fail in *opposite* directions, so each one
|
||||
//! 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
|
||||
/// 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
|
||||
/// (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
|
||||
/// mask — which is what lets it be a cache key.
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
//! So the last step is a **marker-based watershed**. The mask is eroded to
|
||||
//! give two markers — confidently inside, confidently outside — and the flood
|
||||
//! 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.
|
||||
//!
|
||||
//! 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
|
||||
/// 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
|
||||
/// 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
|
||||
@@ -378,7 +378,7 @@ const MAX_SAMPLES: usize = 20_000;
|
||||
/// floating-point comparison is a stopping rule that can differ between
|
||||
/// machines, and a mask that differs between machines reaches the sidecar as
|
||||
/// 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;
|
||||
|
||||
/// 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
|
||||
/// 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.
|
||||
/// Neither alone is right. An edge with no colour meaning is a texture,
|
||||
/// 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.
|
||||
///
|
||||
/// 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
|
||||
/// looks, and a flag against sky is exactly that edge.
|
||||
///
|
||||
|
||||
@@ -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
|
||||
//! 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");
|
||||
|
||||
/// 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")]
|
||||
pub fn embedded_model_bytes() -> &'static [u8] {
|
||||
EMBEDDED_MODEL
|
||||
@@ -237,7 +237,7 @@ impl SemanticModel {
|
||||
|
||||
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
|
||||
// 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.
|
||||
let session = dr_inference_engine::open(
|
||||
dr_inference_engine::Role::Segmenter,
|
||||
|
||||
@@ -115,6 +115,10 @@ pub enum PlaceScope {
|
||||
#[serde(default)]
|
||||
pub struct StoredFilter {
|
||||
pub min_rating: u8,
|
||||
/// TRACES: FR-UI-5
|
||||
/// The top of a star range. A record from before ranges existed has none,
|
||||
/// which reads as no ceiling — what it meant when it was written.
|
||||
pub max_rating: Option<u8>,
|
||||
pub unjudged: bool,
|
||||
pub flag: Option<FlagState>,
|
||||
pub local_only: bool,
|
||||
|
||||
@@ -177,7 +177,7 @@ pub struct FaceSettings {
|
||||
/// Which SCRFD graph the indexing pass detects with.
|
||||
///
|
||||
/// 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
|
||||
/// 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
|
||||
@@ -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
|
||||
/// different population of detections. The embedder half is unchanged,
|
||||
|
||||
@@ -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
|
||||
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.
|
||||
|
||||
## Java in the APK
|
||||
|
||||
@@ -258,7 +258,7 @@ fi
|
||||
cp "${SO}" "${OUT}/staging/lib/${ABI}/libdarkroom.so"
|
||||
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
|
||||
# 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
|
||||
@@ -278,7 +278,7 @@ else
|
||||
fi
|
||||
|
||||
# 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
|
||||
# sources are `models/face/` and `models/scene/`, shared with the Arch package
|
||||
# rather than living under this one platform's directory.
|
||||
|
||||
@@ -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
|
||||
# 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
|
||||
# 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.
|
||||
# ---------------------------------------------------------------------------
|
||||
echo "==> packaging APK"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
# 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
|
||||
@@ -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
|
||||
# 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
|
||||
# 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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Windows cross-build environment
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
./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/installer/DarkRoom-0.12.0-x86_64-setup.exe /S
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# to have run in the same target directory. Produces
|
||||
# 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.
|
||||
set -euo pipefail
|
||||
|
||||
@@ -48,7 +48,13 @@ sed 's/$/\r/' "${REPO}/LICENSE" > "${STAGE}/LICENSE"
|
||||
# tract on the user's machine with a message about a broken graph rather than
|
||||
# a checkout that needed `git lfs pull`. Only the weights are checked; the
|
||||
# scene model's vocabulary and category descriptor are legitimately small.
|
||||
for dir in face scene; do
|
||||
#
|
||||
# The directories are the ones the APK stages (assemble-apk.sh) and the Arch
|
||||
# package installs: the face pair and its eye-state models, the scene model
|
||||
# with its two descriptors, and the panorama border filler. The installer
|
||||
# smoke test counts the same directories, so a model added here is expected
|
||||
# there without a number to update.
|
||||
for dir in face scene inpaint; do
|
||||
for f in "${REPO}/models/${dir}"/*; do
|
||||
case "$(basename "${f}")" in
|
||||
README.md) continue ;;
|
||||
|
||||
@@ -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.
|
||||
> Everything below shipped, and the application went well past it — see
|
||||
> [outstanding.md](outstanding.md) for what is still missing at 0.9.0.
|
||||
**Companion to:** [requirements.md](requirements.md) · [architecture.md](architecture.md)
|
||||
> [outstanding.md](../outstanding.md) for what is still missing at 0.9.0.
|
||||
**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
|
||||
previews on both Linux and Android.
|
||||
@@ -39,7 +39,7 @@ building on sand.
|
||||
|
||||
## 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 |
|
||||
|---|---|---|---|
|
||||
@@ -55,7 +55,7 @@ all, so it should be proven in the first week, before catalog or sync work begin
|
||||
|
||||
## 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.
|
||||
|
||||
### 3.1 Account and connection
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
**Status:** Built, not yet recorded · 2026-08-30
|
||||
**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)
|
||||
**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)
|
||||
|
||||
§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
|
||||
**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
|
||||
`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.
|
||||
|
||||
**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
|
||||
disjoint slices, and the single thread that owns the store writing the finished
|
||||
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
|
||||
|
||||
@@ -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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
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 |
|
||||
|---|---|---|---|
|
||||
| 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 | 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 | 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 | 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
|
||||
software centre, and neither failure announces itself.
|
||||
- **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
|
||||
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
|
||||
@@ -173,7 +173,7 @@ Two changes, in this order:
|
||||
chooser instead of a volume list.
|
||||
|
||||
**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
|
||||
library root, scan it, and write a sidecar back into it.
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
**Status:** Measured · 2026-08-27
|
||||
**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
|
||||
**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)
|
||||
**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)
|
||||
|
||||
[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
|
||||
@@ -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.
|
||||
|
||||
**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
|
||||
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
|
||||
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"*.
|
||||
|
||||
**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.
|
||||
|
||||
**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
|
||||
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
|
||||
@@ -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
|
||||
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*,
|
||||
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.
|
||||
|
||||
**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 |
|
||||
|
||||
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.
|
||||
|
||||
**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,
|
||||
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
|
||||
[`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.
|
||||
|
||||
`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
|
||||
|
||||
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
|
||||
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
|
||||
@@ -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
|
||||
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.
|
||||
|
||||
### 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
|
||||
[`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
|
||||
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`.
|
||||
@@ -540,7 +540,7 @@ layer's.
|
||||
or painted. One code path, three joins.
|
||||
|
||||
**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.
|
||||
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
|
||||
@@ -560,7 +560,7 @@ who sets a small eraser expects it to still be small the next time they erase.
|
||||
### 7.4 Gestures
|
||||
|
||||
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 |
|
||||
|---------|-------|---------|----------|
|
||||
@@ -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
|
||||
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
|
||||
[`labels.rs`](../ui/dr-ui/src/labels.rs):
|
||||
[`labels.rs`](../../ui/dr-ui/src/labels.rs):
|
||||
|
||||
```
|
||||
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
|
||||
"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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
**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
|
||||
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
|
||||
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,
|
||||
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
|
||||
@@ -179,7 +179,7 @@ exports the network alone at 768×1024 — thirteen operator types, all
|
||||
standard: `Conv`, `InstanceNormalization`, `AveragePool`, `Resize`, `Slice`,
|
||||
`Transpose`, `Reshape`, `Concat`, `Add`, `Relu`, `Sigmoid`, `ReduceMean`,
|
||||
`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
|
||||
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
|
||||
@@ -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
|
||||
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,
|
||||
`DNGVersion`, `ColorMatrix1`, `AsShotNeutral`, `CalibrationIlluminant1` — and
|
||||
rawler 0.7 reads it back: `cpp 3`, the samples interleaved as written, the
|
||||
@@ -2169,7 +2169,7 @@ Rationale, evidence, and the eliminated alternatives are recorded in
|
||||
|---|---|---|
|
||||
| D1 | Language and UI framework | Rust + Slint, rendering through wgpu |
|
||||
| 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 |
|
||||
| D5 | Colour management | lcms2 + GPU-side matrix/LUT transforms |
|
||||
| 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.
|
||||
|
||||
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
|
||||
([`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
|
||||
([`sidecar.rs`](../core/dr-pipeline/src/sidecar.rs)). What is genuinely new is
|
||||
([`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
|
||||
([`sidecar.rs`](../../core/dr-pipeline/src/sidecar.rs)). What is genuinely new is
|
||||
small and is named in §3.
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
the end.
|
||||
|
||||
@@ -684,7 +684,8 @@ three columns of equal width side by side, each its own scroll:
|
||||
with them because it is not in their column.
|
||||
2. **Sliders** — `GroupStrip` above `AdjustPanel`, exactly as in the column.
|
||||
With a group selected this is one screen of sliders; with All it scrolls.
|
||||
3. **The mode's panels** — `ComposePanel` and `TransferPanel` in photo mode,
|
||||
3. **The mode's panels** — `ComposePanel` in photo mode (copy and paste
|
||||
moved to the top bar, 2026-09-22),
|
||||
`SpotPanel` in repair, `MaskPanel` in local. Empty otherwise, which is
|
||||
a signal of its own about which mode the view is in (§1.1).
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
TRACES: FR-UI-1 | FR-UI-6 | FR-UI-8 | FR-DEV-3a | NFR-P9
|
||||
|
||||
**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
|
||||
|
||||
@@ -11,8 +11,8 @@ and — because there is no Windows hardware on the runner — exactly how much
|
||||
verified before a person double-clicks it.
|
||||
|
||||
**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)
|
||||
the installer, [`.gitea/workflows/windows-image.yml`](../.gitea/workflows/windows-image.yml) and
|
||||
[`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 `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
|
||||
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
|
||||
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).
|
||||
|
||||
```
|
||||
@@ -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
|
||||
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
|
||||
`$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
|
||||
@@ -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
|
||||
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
|
||||
[`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 —
|
||||
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
|
||||
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
|
||||
[`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
|
||||
executable (`windows_subsystem = "windows"`), or Windows keeps a console window open behind
|
||||
the application.
|
||||
@@ -257,7 +257,7 @@ and running them means Wine. §6 does that for exactly one binary, deliberately.
|
||||
|
||||
## 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
|
||||
(`/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
|
||||
@@ -282,7 +282,9 @@ $LOCALAPPDATA\Programs\DarkRoom\
|
||||
models\
|
||||
scrfd_500m_640.onnx scrfd_2.5g_640.onnx scrfd_10g_640.onnx arcface_mbf_b1.onnx
|
||||
2d106det_b1.onnx ocec_s_b1.onnx sgc_l_48_b1.onnx
|
||||
scrfd_500m_640.int8.onnx scrfd_2.5g_640.int8.onnx scrfd_10g_640.int8.onnx
|
||||
yolo26s-sem-ade20k.onnx yolo26s-sem-ade20k.classes.json categories.txt
|
||||
migan-512.onnx
|
||||
LICENSE
|
||||
uninstall.exe
|
||||
```
|
||||
@@ -370,7 +372,7 @@ say which of these were checked and on what.
|
||||
|
||||
## 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:
|
||||
|
||||
```yaml
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user