Commit Graph
104 Commits
Author SHA1 Message Date
dtourolle 2e7f14dafe Develop every raw through the AI denoise by default, with a strength, cached
The learned demosaic was an option under Detail, off by default. It is
now how a Bayer raw is developed: on by default at full strength on
every device — which hardware runs it is the inference engine's choice
— and first in the Adjust panel, since it decides what every control
below is applied to.

Strength (0-100, default 100) replaces Keep grain: grain = 100 -
strength, the same luminance-only blend, so moving it is one GPU pass
and never a re-run. 0.21.0's sidecars stored grain; it is still read,
as the inverse, and never written.

With it on for every photograph, the result is now kept on disk
(denoise.md §7.1, §12): the network's output as half floats, keyed on
a SHA-256 of the file's bytes and the model, oldest first past a 5 GB
budget, beside the inference engine's cache. A reopened photograph and
an export of one already developed read it back instead of running the
network again; a damaged entry is a miss.
2026-10-03 22:16:36 -04:00
dtourolle 1a03cb52b4 Release 0.21.0
Benchmarks / CPU and I/O (per commit) (push) Successful in 8m38s
Benchmarks / Frame budget (on demand) (push) Skipped
Traceability / Requirement traces (push) Successful in 1m27s
Build and test / Android (aarch64) (push) Failing after 29m52s
Build and test / android-image (push) Successful in 2s
🐳 Android image / Build and push (push) Successful in 2s
Build and test / Desktop (Linux) (push) Successful in 1h2m28s
Build and test / windows-image (push) Successful in 2s
🐳 Windows image / Build and push (push) Successful in 2s
Build and test / Layer separation (push) Successful in 30s
Build and test / Windows (x86_64, cross) (push) Failing after 48m48s
Build and test / Publish the release (push) Skipped
2026-10-03 17:06:49 -04:00
dtourolle dd43f498fb Run the learned denoise in develop, and export with it
A Bayer photograph keeps its mosaic in the session and is offered the AI
Denoise switch. Asked for, the network runs on the decode executor from a
hot-pixel-repaired copy — the app's own pass — with the frame's noise from
its best source, and its progress in the activity bar; the classical
demosaic shows until the result lands, and the finished job says where the
noise figures came from. Keep grain is a GrainBlend of the two, made once
per value; the render draws it as its source and the adjust pass never
knows. demosaiced stays the classical result, so the raw histogram, the
white balance picker, masks and segmentation still read the sensor.

The develop view reconciles on a 250 ms poll rather than on each way an
edit can change (slider, undo, preset, version, a sidecar from another
device): two comparisons when nothing changed, and no path that can forget.
A failure is not retried until the switch is toggled. An export of a
photograph that asks for it waits for a running job or computes it.
2026-10-03 11:51:00 -04:00
dtourolle d8304d7c82 Add dr-denoise: the learned demosaic and denoise, without the UI
The noise model takes the best source the frame has: the body's measured
table (the Canon EOS 6D's, from the library), the DNG's NoiseProfile, or
the frame itself — read, row and column noise from its masked border, and
only the shot gain estimated, from the quietest flat patches. Checked on
130 6D frames, the estimate is within 10 % from ISO 1000 up; the network
loses under 0.3 dB for a sigma off by 15-20 %, so every Bayer body is
eligible.

Tiles of 1408 keep their central 1024 behind a 192-photosite halo, past the
185-photosite receptive field, and the frame is extended by reflection,
which keeps every photosite's colour; a pattern that starts on another
colour is read from one photosite up or left so the network sees RGGB, and
nothing is cropped. The tests run every Bayer phase, tiled against whole,
with a stand-in network of known reach.

The model ships as models/denoise/mosaic-1408.onnx (LFS), trained in
darkroom-denoise on the maintainer's own photographs, GPL like the code.
denoise_raw runs a file end to end: on a 6D frame at ISO 8000 the result
matches the training repository's own path to 2.5e-4 at worst, and takes
3.1 s on TensorRT fp16 (75 dB from f32) or 14.4 s on the CPU.
2026-10-03 11:15:50 -04:00
dtourolle 2fad846cd1 Release 0.20.0
Benchmarks / CPU and I/O (per commit) (push) Successful in 9m1s
Benchmarks / Frame budget (on demand) (push) Skipped
Traceability / Requirement traces (push) Successful in 1m25s
Build and test / Android (aarch64) (push) Successful in 48m24s
Build and test / android-image (push) Successful in 4s
🐳 Android image / Build and push (push) Successful in 3s
Build and test / Desktop (Linux) (push) Successful in 1h22m17s
Build and test / windows-image (push) Successful in 3s
🐳 Windows image / Build and push (push) Successful in 2s
Build and test / Layer separation (push) Successful in 37s
Build and test / Windows (x86_64, cross) (push) Successful in 55m30s
Build and test / Publish the release (push) Successful in 1m27s
2026-10-02 23:07:52 -04:00
dtourolle 825c5af20a Release 0.19.4
Benchmarks / Frame budget (on demand) (push) Canceled after 0s
Benchmarks / CPU and I/O (per commit) (push) Canceled after 5m49s
Traceability / Requirement traces (push) Canceled after 0s
Build and test / android-image (push) Canceled after 0s
🐳 Android image / Build and push (push) Canceled after 0s
Build and test / Android (aarch64) (push) Canceled after 0s
Build and test / windows-image (push) Canceled after 0s
🐳 Windows image / Build and push (push) Canceled after 0s
Build and test / Windows (x86_64, cross) (push) Canceled after 0s
Build and test / Layer separation (push) Canceled after 0s
Build and test / Publish the release (push) Canceled after 0s
Build and test / Desktop (Linux) (push) Canceled after 17s
2026-10-02 20:40:01 -04:00
dtourolle 2c947430e6 Release 0.19.3
Benchmarks / CPU and I/O (per commit) (push) Successful in 5m26s
Benchmarks / Frame budget (on demand) (push) Skipped
Traceability / Requirement traces (push) Successful in 1m9s
Build and test / Android (aarch64) (push) Successful in 30m44s
Build and test / android-image (push) Successful in 3s
🐳 Android image / Build and push (push) Successful in 2s
Build and test / Desktop (Linux) (push) Successful in 49m45s
Build and test / windows-image (push) Successful in 1s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / Layer separation (push) Successful in 35s
Build and test / Windows (x86_64, cross) (push) Successful in 36m12s
Build and test / Publish the release (push) Successful in 1m12s
2026-09-30 22:46:17 -04:00
dtourolle fcccc2c2e0 Release 0.19.2
Benchmarks / CPU and I/O (per commit) (push) Successful in 5m29s
Benchmarks / Frame budget (on demand) (push) Skipped
Traceability / Requirement traces (push) Successful in 1m4s
Build and test / Android (aarch64) (push) Successful in 31m8s
Build and test / android-image (push) Successful in 2s
🐳 Android image / Build and push (push) Successful in 2s
Build and test / Desktop (Linux) (push) Successful in 50m33s
Build and test / windows-image (push) Successful in 3s
🐳 Windows image / Build and push (push) Successful in 2s
Build and test / Layer separation (push) Successful in 33s
Build and test / Windows (x86_64, cross) (push) Successful in 36m27s
Build and test / Publish the release (push) Successful in 59s
2026-09-29 21:31:58 -04:00
dtourolle 5ae742816d Release 0.19.1
Benchmarks / CPU and I/O (per commit) (push) Successful in 5m29s
Benchmarks / Frame budget (on demand) (push) Skipped
Traceability / Requirement traces (push) Successful in 1m8s
Build and test / Android (aarch64) (push) Successful in 30m34s
Build and test / android-image (push) Successful in 3s
🐳 Android image / Build and push (push) Successful in 3s
Build and test / Desktop (Linux) (push) Successful in 48m59s
Build and test / windows-image (push) Successful in 2s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / Layer separation (push) Successful in 48s
Build and test / Windows (x86_64, cross) (push) Successful in 35m51s
Build and test / Publish the release (push) Successful in 51s
2026-09-28 19:58:04 -04:00
dtourolle 883b4aca10 Release 0.19.0
Benchmarks / CPU and I/O (per commit) (push) Successful in 5m27s
Benchmarks / Frame budget (on demand) (push) Skipped
Traceability / Requirement traces (push) Successful in 1m2s
Build and test / Android (aarch64) (push) Successful in 29m53s
Build and test / android-image (push) Successful in 2s
🐳 Android image / Build and push (push) Successful in 2s
Build and test / Desktop (Linux) (push) Successful in 50m11s
Build and test / windows-image (push) Successful in 2s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / Layer separation (push) Successful in 38s
Build and test / Windows (x86_64, cross) (push) Successful in 35m28s
Build and test / Publish the release (push) Successful in 1m10s
2026-09-27 19:50:46 -04:00
dtourolle 72aa7e98bf Let rawler decode a linear DNG wider than 16 700 pixels
rawler's allocation guard is sized in samples but worded in pixels, and
a linear DNG passes width × 3. A 22927 × 8966 Lightroom panorama was
refused as ">50000 px wide", and develop fell back silently to the
embedded preview. Route rawler through third_party with the guard at
1.5 G samples and 200 000 per axis.
2026-09-27 17:33:19 -04:00
dtourolle e22128c16b Release 0.18.2
Benchmarks / CPU and I/O (per commit) (push) Successful in 5m34s
Benchmarks / Frame budget (on demand) (push) Skipped
Traceability / Requirement traces (push) Successful in 1m0s
Build and test / Android (aarch64) (push) Successful in 30m28s
Build and test / android-image (push) Successful in 1s
🐳 Android image / Build and push (push) Successful in 1s
Build and test / Desktop (Linux) (push) Successful in 47m53s
Build and test / windows-image (push) Successful in 1s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / Layer separation (push) Successful in 34s
Build and test / Windows (x86_64, cross) (push) Successful in 36m29s
Build and test / Publish the release (push) Successful in 1m10s
2026-09-27 08:23:04 -04:00
dtourolle 2cde287440 Hold every verdict write to a reviewed list of user actions
FR-CULL-13 says evidence never writes a rating, flag, label or trash
membership, and nothing enforced it. tools/traceability/src/verdicts.rs
parses the shipped code with syn and enumerates every write: calls to
the catalog setters and trash recorders, SQL that assigns those columns,
sidecar Amendment::Judgement, and fields named rating/flag/label. Each
site must be in ALLOWED with a reason, as Input (inside a Slint on_*
closure, checked structurally), Relay (its callers are checked in
turn), Carried (a verdict made elsewhere: sidecar and XMP pulls, sync
merge, catalog mirrored to file, duplicates consolidation) or
NotAVerdict. Unlisted sites and stale entries both fail
`cargo test -p traceability`; `traces verdicts` prints the list.

syn and proc-macro2 were already in the lockfile as proc-macro
dependencies; this adds the edges, no new crate and no version change.
2026-09-27 07:20:38 -04:00
dtourolle 48c5e74fa8 Release 0.18.1
Benchmarks / CPU and I/O (per commit) (push) Successful in 5m46s
Benchmarks / Frame budget (on demand) (push) Skipped
Traceability / Requirement traces (push) Successful in 49s
Build and test / Android (aarch64) (push) Successful in 17m27s
Build and test / android-image (push) Successful in 3s
🐳 Android image / Build and push (push) Successful in 3s
Build and test / Desktop (Linux) (push) Failing after 25m32s
Build and test / windows-image (push) Successful in 1s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / Layer separation (push) Successful in 29s
Build and test / Windows (x86_64, cross) (push) Successful in 37m19s
Build and test / Publish the release (push) Skipped
2026-09-26 21:29:26 -04:00
dtourolle 515d4eb59e Release 0.18.0
Benchmarks / CPU and I/O (per commit) (push) Successful in 5m56s
Benchmarks / Frame budget (on demand) (push) Skipped
Traceability / Requirement traces (push) Successful in 45s
Build and test / Android (aarch64) (push) Successful in 17m8s
Build and test / android-image (push) Successful in 2s
🐳 Android image / Build and push (push) Successful in 2s
Build and test / Desktop (Linux) (push) Successful in 48m1s
Build and test / windows-image (push) Successful in 2s
🐳 Windows image / Build and push (push) Successful in 2s
Build and test / Layer separation (push) Successful in 36s
Build and test / Windows (x86_64, cross) (push) Successful in 21m35s
Build and test / Publish the release (push) Successful in 1m12s
2026-09-26 19:13:06 -04:00
dtourolle 5baaf9bac2 Release 0.17.0
Benchmarks / CPU and I/O (per commit) (push) Successful in 5m22s
Benchmarks / Frame budget (on demand) (push) Skipped
Traceability / Requirement traces (push) Successful in 1m1s
Build and test / Android (aarch64) (push) Successful in 32m18s
Build and test / android-image (push) Successful in 1s
🐳 Android image / Build and push (push) Successful in 1s
Build and test / Desktop (Linux) (push) Successful in 50m25s
Build and test / windows-image (push) Successful in 1s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / Layer separation (push) Successful in 31s
Build and test / Windows (x86_64, cross) (push) Successful in 38m0s
Build and test / Publish the release (push) Successful in 50s
2026-09-26 16:18:18 -04:00
dtourolle 6683c14b40 Choose folders in the platform's dialogue, not by typing a path
Every folder the desktop asked for was a text field: the library folder
at launch, an import's source and second copy, a preset folder brought
over from Lightroom. A typed path is how a destination silently becomes
a new folder nobody meant — one wrong letter three levels down and the
write succeeds somewhere the photographer will never look — and a field
cannot make the folder that is not there yet.

They now open the platform's own dialogue through rfd: the XDG desktop
portal on Linux, the common item dialogue on Windows. The portal rather
than GTK because it reaches the user's files from inside the Flatpak and
needs no GTK in a Slint application, and it draws whichever desktop's
chooser is running, "New folder" included. It is awaited on Slint's
event loop (spawn_local), so the window keeps drawing while it is open,
and parented to the window so it opens over it.

PathRow shows what is chosen, read-only, beside the button. Android has
no filesystem dialogue — only SAF, which returns document trees, not
paths — so there the same rows stay typed fields (Pickers.local-paths).

The launch screen keeps the folder used last on screen with "Open
folder" beside it, so reopening is one press. Presets get two buttons,
a folder and a single .xmp file, because no platform dialogue picks
"a file or a folder" in one go.
2026-09-26 14:13:53 -04:00
dtourolle 5794f8c6ea Release 0.16.0
Benchmarks / CPU and I/O (per commit) (push) Successful in 5m57s
Benchmarks / Frame budget (on demand) (push) Skipped
Traceability / Requirement traces (push) Successful in 47s
Build and test / Android (aarch64) (push) Successful in 31m41s
Build and test / android-image (push) Successful in 4s
🐳 Android image / Build and push (push) Successful in 4s
Build and test / Desktop (Linux) (push) Successful in 47m44s
Build and test / windows-image (push) Successful in 4s
🐳 Windows image / Build and push (push) Successful in 3s
Build and test / Layer separation (push) Successful in 45s
Build and test / Windows (x86_64, cross) (push) Successful in 36m16s
Build and test / Publish the release (push) Successful in 1m35s
2026-09-26 09:46:32 -04:00
dtourolle 8d4ecb75c1 Prove duplicate originals the same and consolidate them on workers
dr_ui::duplicates is the half of #67 that touches files. The check reads
each copy's first and last megabyte by range through the backend and
hashes them (or compares stored content hashes where every copy has one),
keeps the probes in the catalog, and reads each copy's sidecar: a group
whose bytes differ, whose copies cannot be read, or whose develop edits
differ is left out and the review says why.

Consolidating a group carries the one edit onto the survivor's sidecar
where it has none, moves the other copies into the trash, and then
commits dr_catalog::duplicates::consolidate. A failure after the first
move puts the files and the sidecar back; a run that died between the
moves and the commit is finished by the next one, which finds each moved
file at its trash path.

Tested end to end on a folder library of real files: the copies land in
.darkroom-trash, the skipped groups are untouched, the edit reaches the
survivor, a catalog failure moves everything back, and restore returns
the copies byte for byte.
2026-09-26 07:19:13 -04:00
dtourolle cf84cec96f Release 0.15.0
Benchmarks / CPU and I/O (per commit) (push) Successful in 5m19s
Benchmarks / Frame budget (on demand) (push) Skipped
Traceability / Requirement traces (push) Successful in 49s
Build and test / Android (aarch64) (push) Successful in 31m25s
Build and test / android-image (push) Successful in 1s
🐳 Android image / Build and push (push) Successful in 1s
Build and test / Desktop (Linux) (push) Successful in 48m26s
Build and test / windows-image (push) Successful in 5s
🐳 Windows image / Build and push (push) Successful in 4s
Build and test / Layer separation (push) Successful in 34s
Build and test / Windows (x86_64, cross) (push) Successful in 19m57s
Build and test / Publish the release (push) Successful in 1m6s
2026-09-25 07:51:31 -04:00
dtourolle a6ea6ba83f Let the manual's scripts find a control by its name
Every scene in tools/manual aimed at window pixels written in by hand, so
a panel that gained a row moved every slider under it and the recording
went on dragging where the slider used to be. The develop column has
already moved that way (Compose now sits above Adjust), and nothing said.

A build with the `automation` feature listens on the Unix socket named
by DR_AUTOMATION and answers where an element is: by its accessible
label, the name a screen reader reads, or by its markup id for the few
things that are not controls (the canvas, the crop rectangle). It uses
Slint's element queries, which need the compiler's debug tables, so the
feature also turns those on in build.rs. It only answers questions; the
input is still xdotool's real pointer. No default build has the feature,
and one that has it listens only when the variable is set.

drive.py gains click-on, drag-on, hold-on, wait-for, wait-gone, labels
and ids. The grid's cells are now named by their file, each rating star
by its value, the sidebar's + as "New collection", and the Adjust
heading's reset as "Reset all adjustments" - controls a screen reader
could not reach before either.
2026-09-25 07:26:36 -04:00
dtourolle dc1add9dbb Vendor wgpu-hal 29.0.4 and i-slint-renderer-skia 1.17.1, unmodified
The Android develop view reads its frame back through memory (TD-1)
because wgpu's Vulkan swapchain never pre-rotates, and a portrait window
on this tablet's landscape panel then tears. The fix is a small patch to
each of these two crates, and this commit is only the ground it lands on:
both are byte-for-byte the crates.io sources the lockfile already
resolved, so the commits that follow are the patch and nothing else.

third_party/ is excluded from the workspace, or every path dependency
under the root would become a member and `--workspace` would test and
lint upstream code as ours. The README says how to carry the patches
across a Slint or wgpu bump, which matters because a stale version here
does not fail the build — cargo just warns and uses the unpatched crate.
2026-09-25 04:18:39 -04:00
dtourolle 352e59498b Open the bundled manual from Help and from Settings
The packages now carry the manual, but nothing in the application opened
it: the help sheet listed gestures and stopped there.

The help sheet gains a Manual button beside Done, and Settings a Manual
row under About beside the version. Both go through dr_ui::manual, which
finds the installed page through dr_plat::system_data_dirs (the package's
share directory on Linux, the executable's directory on Windows), and a
development build also in the checkout it was compiled from. A copy with
no manual says so on the status line rather than doing nothing.

On the desktop the page goes to the system browser. A section is a URL
fragment, and xdg-open's generic mode and Windows' FileProtocolHandler
both turn a file: URL into a path and drop the fragment, so a section is
opened through a one-line redirect page written to the data directory:
the opener gets a plain path, which every opener keeps, and the browser
follows the redirect to index.html#section itself. The launcher behind
the sign-in's open_in_browser is split out so both share it; the https
check stays with the sign-in.

Android has no path to give a browser: an asset is not a file, a copy in
private storage is unreadable to other apps, a file: URI across apps is
refused, and a content: URI leaves the browser resolving every picture
against the provider. So ManualActivity, a WebView reading
file:///android_asset/manual/index.html straight out of the APK, shows
it, started by class name with the section as an extra. JavaScript is
off, links off the page go to the browser, and the theme is day-night so
the page's own light and dark follow the system. A test checks that the
manifest, the Java class and dr_ui agree on the name and the extra.
2026-09-24 22:56:09 -04:00
dtourolle 10216355c1 Render the manual as one HTML page the application can carry
The manual existed only as docs/manual/README.md, which the forge renders
and nothing else does. An installed copy of the application, on a laptop
with no network or on a tablet, had no manual it could open.

`traces manual` renders the README to docs/manual/index.html with
pulldown-cmark (already in the tree as Slint's Markdown parser, so this
adds a dependency edge and no crate). The page is one file with an inline
stylesheet that follows the system's light or dark preference, a
contents list of every section and subsection, and the pictures by their
relative media/ paths. Each heading carries the id the forge gives it, so
README.md#rating-and-flagging and index.html#rating-and-flagging are the
same link. A picture alone in its paragraph becomes a figure whose alt
text is shown as the caption, and every picture reserves its 16:11 box
before it loads, so a jump into the middle of the page lands where it
aimed rather than a screenful above. Links to design documents, which the
installed page has no copy of, point at the forge.

The page is committed rather than rendered at build time, as the gesture
book is: it is user-facing text reviewed in the diff, and the three
packagers then only copy it. `traces manual-check` fails in CI when the
committed page is not the render of the README, and the pre-commit hook
regenerates it when the README is staged.
2026-09-24 22:32:30 -04:00
dtourolle 317a2f40bd Release 0.14.1
Benchmarks / CPU and I/O (per commit) (push) Successful in 5m19s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Successful in 43m6s
Build and test / Layer separation (push) Successful in 37s
🐳 Android image / Build and push (push) Successful in 2s
Build and test / android-image (push) Successful in 3s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / windows-image (push) Successful in 1s
Traceability / Requirement traces (push) Successful in 46s
Build and test / Android (aarch64) (push) Successful in 29m7s
Build and test / Windows (x86_64, cross) (push) Successful in 34m31s
2026-09-23 19:07:22 -04:00
dtourolle 7fa3176f88 Release 0.14.0
Benchmarks / Frame budget (on demand) (push) Skipped
Benchmarks / CPU and I/O (per commit) (push) Successful in 8m25s
Build and test / Desktop (Linux) (push) Failing after 26m11s
Build and test / Layer separation (push) Successful in 33s
🐳 Android image / Build and push (push) Successful in 10m41s
Build and test / android-image (push) Successful in 10m42s
🐳 Windows image / Build and push (push) Successful in 4m17s
Build and test / windows-image (push) Successful in 4m17s
Traceability / Requirement traces (push) Successful in 59s
Build and test / Android (aarch64) (push) Successful in 40m53s
Build and test / Windows (x86_64, cross) (push) Successful in 46m51s
2026-09-21 11:32:41 +02:00
dtourolle 681486196e Release 0.13.6
Benchmarks / CPU and I/O (per commit) (push) Failing after 6m17s
Benchmarks / Frame budget (on demand) (push) Skipped
🐳 Android image / Build and push (push) Successful in 1s
Build and test / android-image (push) Successful in 2s
Build and test / Desktop (Linux) (push) Failing after 29s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / windows-image (push) Successful in 1s
Build and test / Layer separation (push) Successful in 26s
Traceability / Requirement traces (push) Failing after 42s
Build and test / Android (aarch64) (push) Failing after 2m48s
Build and test / Windows (x86_64, cross) (push) Failing after 4m22s
2026-09-20 20:19:48 +02:00
dtourolleandClaude Opus 5 39a22875b1 Add the MIGraphX rung for AMD GPUs
Benchmarks / CPU and I/O (per commit) (push) Failing after 6m20s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Failing after 45s
Build and test / Layer separation (push) Successful in 26s
Traceability / Requirement traces (push) Failing after 46s
🐳 Android image / Build and push (push) Successful in 1s
Build and test / android-image (push) Successful in 1s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / windows-image (push) Successful in 1s
Build and test / Android (aarch64) (push) Failing after 2m19s
Build and test / Windows (x86_64, cross) (push) Failing after 3m2s
Measured on a Radeon RX 7900 XT against Arch's onnxruntime-rocm 1.29
(docs/inference.md §1.3): MIGraphX fp16 runs the detectors at 2.4–3.4 ms
against 10–58 ms on the CPU provider, the inpainter at 8 ms against 514,
with a 15–135 s compile per graph the first time and under a second from
its cache after. A compiling rung on TensorRT's terms, wired the same way.

The ROCm execution provider is gone (removed in ONNX Runtime 1.23), so the
AMD ladder is MIGraphX then the CPU, with no non-compiling rung between.

MIGraphX is registered through the runtime's generic key/value entry
point rather than ort's builder: 1.29 reads the legacy options struct for
its precision flags only, and the compiled-program cache directory
(`migraphx_model_cache_dir`) only travels the generic way. The provider's
cache key omits the precision, so f32 and fp16 programs get their own
directories. The probe fingerprint now includes the provider libraries
beside the runtime and the ROCm version, since a distribution's CPU and
ROCm builds are the same file at the same path.

`status().failed` reports only the rungs above the selection, so an AMD
desktop's About line says why MIGraphX won rather than that the NVIDIA
providers are not in the build.

Two examples: `ep_probe` times each provider cold and from cache, and
`ladder` drives `init` as the app does to watch the first-run sequence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 19:23:00 +02:00
dtourolle 6507593715 Hand the drag ghost to the renderer through a file, so it draws
The bitmap under the cursor was a solid red rectangle. Slint's drag
overlay uploads the image as a texture, draws it and drops the texture in
one call; with the wgpu FemtoVG renderer the drop is immediate and the
draw is deferred to the flush, so the frame binds femtovg's placeholder —
which is red. An image with a cache key survives in the texture cache
until after the flush, and only a path gives one. So the composite goes
to the data directory's scratch as a PNG and comes back through
load_from_path; one file per drag, removed when the drag ends. A
workaround for Slint 1.17.1, written up as one beside the code.
2026-09-20 15:58:44 +02:00
dtourolle 08727cff5a Release 0.13.5
Benchmarks / CPU and I/O (per commit) (push) Failing after 6m16s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Failing after 45s
Build and test / Layer separation (push) Successful in 25s
Traceability / Requirement traces (push) Failing after 38s
🐳 Android image / Build and push (push) Successful in 1s
Build and test / android-image (push) Successful in 2s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / windows-image (push) Successful in 1s
Build and test / Android (aarch64) (push) Failing after 2m20s
Build and test / Windows (x86_64, cross) (push) Failing after 3m1s
2026-09-20 15:24:47 +02:00
dtourolle 8a897bbc01 Release 0.13.4
Benchmarks / CPU and I/O (per commit) (push) Failing after 6m22s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Failing after 46s
Build and test / Layer separation (push) Successful in 29s
Traceability / Requirement traces (push) Failing after 40s
🐳 Android image / Build and push (push) Successful in 1s
Build and test / android-image (push) Successful in 1s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / windows-image (push) Successful in 1s
Build and test / Android (aarch64) (push) Failing after 2m22s
Build and test / Windows (x86_64, cross) (push) Failing after 3m5s
2026-09-20 14:03:58 +02:00
dtourolle 301e6f3828 Release 0.13.3
Benchmarks / CPU and I/O (per commit) (push) Failing after 6m21s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Failing after 46s
Build and test / Layer separation (push) Successful in 26s
Traceability / Requirement traces (push) Failing after 39s
🐳 Android image / Build and push (push) Successful in 1s
Build and test / android-image (push) Successful in 1s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / windows-image (push) Successful in 1s
Build and test / Android (aarch64) (push) Failing after 2m21s
Build and test / Windows (x86_64, cross) (push) Failing after 3m8s
2026-09-20 13:01:41 +02:00
dtourolle ef1afc254d Release 0.13.2
Benchmarks / CPU and I/O (per commit) (push) Failing after 6m36s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Failing after 46s
Build and test / Layer separation (push) Successful in 34s
Traceability / Requirement traces (push) Failing after 40s
🐳 Android image / Build and push (push) Successful in 2s
Build and test / android-image (push) Successful in 2s
🐳 Windows image / Build and push (push) Successful in 2s
Build and test / windows-image (push) Successful in 2s
Build and test / Android (aarch64) (push) Failing after 2m26s
Build and test / Windows (x86_64, cross) (push) Failing after 3m7s
2026-09-20 11:06:54 +02:00
dtourolle 9d04ff2154 Release 0.13.1
Benchmarks / CPU and I/O (per commit) (push) Successful in 12m24s
Benchmarks / Frame budget (on demand) (push) Skipped
🐳 Android image / Build and push (push) Successful in 4s
Build and test / android-image (push) Successful in 5s
Build and test / Desktop (Linux) (push) Failing after 31m25s
🐳 Windows image / Build and push (push) Successful in 4s
Build and test / windows-image (push) Successful in 4s
Build and test / Layer separation (push) Successful in 46s
Build and test / Android (aarch64) (push) Failing after 6h25m37s
Build and test / Windows (x86_64, cross) (push) Failing after 3m17s
Traceability / Requirement traces (push) Failing after 48s
2026-09-19 22:06:06 +02:00
dtourolle b502a8ef90 Release 0.13.0
Benchmarks / CPU and I/O (per commit) (push) Successful in 12m2s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Successful in 1h41m17s
Build and test / Layer separation (push) Successful in 50s
Traceability / Requirement traces (push) Failing after 1m6s
🐳 Android image / Build and push (push) Successful in 16m7s
Build and test / android-image (push) Successful in 16m9s
🐳 Windows image / Build and push (push) Successful in 6m11s
Build and test / windows-image (push) Successful in 6m13s
Build and test / Android (aarch64) (push) Failing after 40m26s
Build and test / Windows (x86_64, cross) (push) Failing after 1h4m8s
2026-09-19 20:43:27 +02:00
dtourolle 7a436e2549 Move the panorama keypoint detector onto the engine, and probe with a detector
XFeat's two exports are a Keypoints role now; the crate no longer names
tract, and the app compiles TensorRT engines for both ahead of the
first merge. The probe picks the smallest *detector* rather than the
smallest file: the tablet's first run chose the 112 KB eye classifier,
which has no int8 form, and reported the Hexagon as failed for want of
one.
2026-09-19 16:05:08 +02:00
dtourolle 05508741af Start the inference engine from both apps and show its choice in Settings
The desktop names where a package may have put libonnxruntime — an
override variable, beside the executable, the package's own library
directory, the Flatpak prefix, the system library directory — and
Android points at the APK's native library directory, which is also
what Qualcomm's DSP loader must be told for the Hexagon skel. Android
starts the engine at the end of the model unpack rather than at launch,
because the probe fingerprints the model files and a first launch has
none until then.

The About panel gains an Inference row beside Graphics, re-read every
two seconds while the probe runs and engines land, and faces.model_id
carries the detector's form: an int8 detector finds a different set of
faces and is a different population (docs/inference.md §7). A
low-memory signal drops every idle session with the GPU caches.

The APK assembly bundles ONNX Runtime and the Qualcomm HTP libraries
from Maven, fetched by tools/fetch-android-runtime.sh with their
published checksums; RUNTIME_DIR=none builds the tract-only APK, which
is a slower app and not a broken one. The desktop packages carry no
runtime yet.

Two probe fixes from the first desktop run: the floor must not be
built with CPU fallback disabled, and a versioned libonnxruntime.so is
a runtime too. On the reference desktop the probe now loads ONNX
Runtime 1.30, measures 30 ms on the CPU provider, and selects TensorRT
at 1.5 ms.
2026-09-19 16:02:37 +02:00
dtourolle d15c41e699 Add dr-inference-engine and route every model session through it
One crate names the runtime, the providers and the devices; dr-face and
dr-segment ask it for a session by role. It hands ort an API table once
per process — from a libonnxruntime it dlopens when the app names a
directory holding one, otherwise from tract — so the Rust build stays
free of C on every target and a package can install the runtime as a
file (docs/inference.md §3).

Sessions live in a registry behind a Model handle that holds the bytes,
not the session: every use refreshes a timestamp and a reaper unloads
whatever sat idle past the decay. A scan that runs the detector on each
image never lets it go idle; a click in the develop view lets the
segmenter go after thirty seconds; a handle used after that reloads,
and reloads on a higher rung if a compiled engine has landed meanwhile.

The probe walks the platform's ladder by building strict sessions and
timing them against the CPU provider, caches the choice against a
fingerprint of the runtime, driver, hardware and models, and compiles
engines for the selected rung in the background, smallest model first.
Nothing in this commit turns the native path on: the apps still run on
tract until they call init with a runtime directory.
2026-09-19 16:02:37 +02:00
dtourolle acab0d7abb A linear DNG in and out: the writer, and a three-sample RawImage
dr-export gains write_linear_dng — LinearRaw, DNG 1.4, u16 samples at
the sensor's scale, the body's matrices with their illuminants, the
as-shot neutral, the EXIF block an export writes — streamed strip by
strip through a closure so the composite is never held (FR-MRG-11). The
tiff crate's directory is a map, so PhotometricInterpretation is written
over what new_image set, which is the trick the S15.1 spike thought it
had to hand-roll around. The test reads the file back through rawler.

dr-decode's RawImage carries samples_per_pixel (a linear DNG is 3), the
body's profile with its calibrations mapped back to EXIF illuminant
codes, and the cleaned make and model. The GPU uploads a three-sample
image as it is, normalised by black and white like a photosite, through
a full f16 conversion — subnormals kept, because a 14-bit LSB sits at
f16's smallest normal and rounding it to zero would crush exactly the
shadows the file was written to keep.
2026-09-19 15:24:12 +02:00
dtourolle 231b4a54ab dr-pano: the geometry, from features to cameras
A new crate holding the CPU half of a merge (FR-MRG-10): the grayscale
proxy with orientation, the XFeat decoder ported step for step from the
reference detectAndCompute, mutual-nearest-neighbour matching, a robust
pairwise homography with the focal length read off it, a hand-rolled
Levenberg–Marquardt bundle adjustment over every rotation and the focal,
the three output projections, and align(), which chains it all and names
the frames it could not place rather than guessing (FR-MRG-5).

Dependency-free without the xfeat feature — linalg.rs says why the dense
algebra is hand-rolled — and tested on synthetic sweeps whose answer is
known exactly. The noise test records the single-row degeneracy: one
pixel of noise is a tenth of a percent of focal, which is a uniform
stretch of the sweep, not a misalignment.
2026-09-19 15:24:12 +02:00
dtourolle 2917b7427d Release 0.12.2
Benchmarks / CPU and I/O (per commit) (push) Successful in 12m51s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Successful in 1h44m42s
Build and test / Layer separation (push) Successful in 1m4s
🐳 Android image / Build and push (push) Successful in 6s
Build and test / android-image (push) Successful in 7s
🐳 Windows image / Build and push (push) Successful in 3s
Build and test / windows-image (push) Successful in 3s
Traceability / Requirement traces (push) Successful in 2m15s
Build and test / Android (aarch64) (push) Successful in 1h4m7s
Build and test / Windows (x86_64, cross) (push) Successful in 1h12m17s
2026-09-19 10:15:46 +02:00
dtourolle 8012979a1e Release 0.12.1
Benchmarks / CPU and I/O (per commit) (push) Successful in 10m58s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Successful in 1h40m20s
Build and test / Layer separation (push) Successful in 1m5s
🐳 Android image / Build and push (push) Successful in 4s
Build and test / android-image (push) Successful in 5s
🐳 Windows image / Build and push (push) Successful in 3s
Build and test / windows-image (push) Successful in 4s
Traceability / Requirement traces (push) Successful in 1m18s
Build and test / Android (aarch64) (push) Successful in 1h0m2s
Build and test / Windows (x86_64, cross) (push) Failing after 56m14s
2026-09-13 20:10:03 +02:00
dtourolle 896188a489 Read the sidecars other editors write, and write them back on request
Benchmarks / CPU and I/O (per commit) (push) Successful in 10m59s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Successful in 1h33m36s
Build and test / Layer separation (push) Successful in 1m2s
Traceability / Requirement traces (push) Successful in 1m25s
🐳 Android image / Build and push (push) Successful in 9s
Build and test / android-image (push) Successful in 9s
Build and test / Android (aarch64) (push) Successful in 56m59s
FR-CAT-13 asked for standard XMP and `core/dr-xmp` answered the file: it
has read and written `dc:subject`, `xmp:Rating`, `xmp:Label` and the IPTC
core since 5fa4c07, under an ownership rule that leaves everything else in
the document untouched. What nothing did was call it. No scan found an
`.xmp` beside a raw, no catalog row was filled from one, no judgement
wrote one back, and the "external modification detected, reload offered"
clause had no mechanism. A library imported from Lightroom came in and
could not go back out.

The scan collects `.xmp` beside `.drsc` from the listings it was already
paying for, and the pull reads each one whose ETag has moved. Both
namings resolve: darktable's `IMG_0001.CR3.xmp` names its file exactly,
Lightroom's `IMG_0001.xmp` names the stem, and under the stem the JPEG
beside a RAW is the same photograph and takes the same document, as
DarkRoom's own sidecar already does. Each is reconciled with the catalog
winning — keywords union, a rating or label taken only where the catalog
has none — because a standard XMP carries nothing that could say whether
its value is newer. A genuine disagreement is not resolved; it is written
to a table, and the settings page offers the sidecars' values against it.
That button is the reload the requirement asks to be offered, and the
ETag that moved is the detection it asks for: an `.xmp` edited elsewhere
is exactly a file the pull's ordinary incrementality re-reads.

Writing goes the other way behind a setting that starts off, since NFR-R4
makes writes beside somebody's originals theirs to switch on. With it on,
a judgement or a keyword rewrites the sidecar of whichever spelling
exists, or creates Lightroom's. The record is read from the catalog
whole at that moment rather than carried from the gesture, so a rating
and a keyword a second apart are two writes of one file that agree. And
the file's own title, caption, copyright and hierarchy come through the
rewrite: the catalog has no columns for them, `rewrite` replaces the
owned set wholesale, and a record that said nothing about them would have
deleted them from a Lightroom sidecar on every star.

The rating's two axes cross the format's one field both ways: a
rejection is Adobe's `-1` and stars are stars, and stars arriving on a
rejected frame lift the rejection, since the file said it was worth a
number. An unrated file says nothing and clears nothing, on the rule the
`.drsc` merge keeps. `versions.label` finally has a reader and a writer,
with the code table moved out of the query so the two cannot drift.
2026-09-12 01:08:11 +02:00
dtourolle fa4dca327f Give the desktop executable a version flag and a Windows identity
Three things the Windows build showed the entry point was missing, and
that a Linux build never asks for.

`--version`, answered before the logger and the crash hook install: a
binary built on a machine that cannot run the application — the Linux
CI producing the Windows executable, checked under Wine — needs an
exit that proves it starts without opening a window or touching the
user's directories. It is the smoke test in docs/windows.md §6.

A GUI-subsystem executable in release, or Windows keeps a console
window open behind the application for the life of the process. Debug
builds keep the console, which is where their log goes.

A resource block, or Explorer, the Start Menu and the taskbar show the
generic executable icon and the Details tab is empty. build.rs wraps
the PNG every other platform uses into an .ico at build time — an ICO
entry may be a PNG, so the wrapper is a 22-byte header — and hands it
to winresource with the version cargo already knows. The crate is an
unconditional build-dependency because a cfg(windows) on one is
evaluated against the host, which here is Linux; the script itself
returns before touching it on any other target.
2026-09-12 00:54:11 +02:00
dtourolle 936490880b Release 0.12.0
Benchmarks / CPU and I/O (per commit) (push) Successful in 12m48s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Failing after 37m37s
Build and test / Layer separation (push) Successful in 46s
🐳 Android image / Build and push (push) Successful in 2s
Build and test / android-image (push) Successful in 3s
Traceability / Requirement traces (push) Successful in 1m4s
Build and test / Android (aarch64) (push) Successful in 53m59s
2026-09-11 09:33:36 +02:00
dtourolle d920716a2b Release 0.11.0
Benchmarks / CPU and I/O (per commit) (push) Successful in 11m47s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Failing after 37s
Build and test / Layer separation (push) Successful in 38s
Traceability / Requirement traces (push) Successful in 1m0s
🐳 Android image / Build and push (push) Successful in 2s
Build and test / android-image (push) Successful in 2s
Build and test / Android (aarch64) (push) Successful in 55m56s
2026-09-07 22:31:53 +02:00
dtourolle 5fa4c0772b Speak the sidecar format every other editor already reads
FR-CAT-13 asked for standard XMP and nothing in the tree parsed or wrote a
byte of it. `keywords.rs` mentioned `dc:subject` in a comment about what a
keyword's text is for, `dr-export`'s metadata module said "neither is read by
`dr-decode` today" about its own half, and `dr-preset-xmp` reads a different
file for a different requirement. So a library imported from Lightroom could
come in and never go back out: a one-way door, which is not a thing a
photographer walks their archive through.

`core/dr-xmp` reads and writes the properties the requirement names —
`dc:subject`, `lr:hierarchicalSubject`, `xmp:Rating`, `xmp:Label` and the IPTC
core fields — from whichever shape the file happens to use. A property may
arrive as an attribute or as an element, inside a Bag, a Seq, an Alt or no
container at all, because the specification is not what wrote the file; so one
collector takes whatever is in a property and the declared shape decides only
how many values survive. `xmp:Rating="-1"` is modelled as Adobe's rejection
rather than folded into zero stars, since DarkRoom keeps those on two axes and
the mapping belongs where both are visible.

Writing is a rewrite rather than a serialisation, and that is the whole design.
An XMP sidecar is a shared document: the file beside a raw carries somebody
else's `crs:` settings and comments and namespaces, and rendering our record
over it would be data loss on every photograph but the first. The rule is
stated once, in the crate documentation and in `PROPERTIES`: DarkRoom owns
exactly those properties, identified by namespace URI and never by prefix, and
nothing else in the document. Everything unowned is copied through byte for
byte. A `Description` left empty once our properties come out of it is
withdrawn, which is what keeps a rewrite idempotent instead of adding a husk to
the file on every save.

Precedence is settled conservatively, because a standard XMP carries no
revision and no device and there is nothing in it to order two edits by.
Keywords union, following the rule `dr_catalog::merge` already makes for
assignments; every other field is taken only where DarkRoom holds none,
following `Version::merge`'s judgement rule, and a genuine disagreement is
reported rather than resolved so a caller can offer the reload the requirement
asks for. What is deliberately left open — when a reload may happen without
asking — is written down in the module rather than picked silently.

No new dependency: quick-xml was already in the tree for WebDAV and for
Lightroom presets. Nothing above the crate calls it yet, and `outstanding.md`
now says so along with the two smaller gaps, GPS and the filename convention.
2026-09-06 19:01:48 +02:00
dtourolleandClaude Opus 5 c4ddcbe0f7 Look the lens up and say plainly whether one was found
`dr-lens` has held a complete Lensfun lookup — distortion, TCA and vignetting
coefficients from a lens name, a focal length and an aperture — with no
dependents anywhere in the workspace. The three corrections it feeds now
exist in the graph, so this connects the two and finishes the chain.

The coefficient structs stay duplicated. `dr-pipeline` is organised around
having no dependencies so its codegen is testable without a device or a
database (ARCH §6.5a), and `dr-lens` carries an XML parser and 5.5 MB of
profile data. Neither crate can convert to the other, so the conversion goes
above both, in `develop.rs`, which is the only place that sees them together.

Both traits grow the same defaulted door. The optical corrections do not sit
on the same side of the fetch — distortion and CA rewrite coordinates and are
`Warp`s, vignetting applies a gain to the pixel already there and is an
ordinary node — and fanning a profile out by which trait each happens to
implement would make the caller reason about that distinction. Each correction
takes its own share of the whole profile instead, and `set_lens_profile` walks
both lists identically.

The lookup happens in `set_source_metadata` rather than in its caller, because
that is the one place a session is told which file it came from. Doing it
there makes it unforgettable, in the shape `FilmRebake` already uses for the
other derived thing — and, more to the point, makes *clearing* unforgettable:
a session that opened a second photograph while still holding the first one's
profile would correct it for the wrong optics, invisibly, in a way that looks
exactly like the lens.

It needs the whole shot and not just a name. Distortion is interpolated across
a zoom's focal range and vignetting depends strongly on aperture — a fast
prime can be two stops down in the corners wide open and clean by f/8 — so a
lookup missing either returns coefficients measured for a shot nobody took.
Missing any of the three refuses rather than guesses.

A profile is derived, not persisted: it comes from the file's EXIF and a
database, so it is not a parameter, not in the sidecar and not undoable. What
is an edit is the manual trim beside it, which each correction composes with
the measurement — so a photographer can lean on it, override it, or work
without one.

`InfoPanel` gains a lens line, and it distinguishes three cases rather than
two. `dr-lens` states the rule it exists for: an automatic correction that
silently did nothing is worse than one the user can see is unavailable. A
session with no header draws nothing, a header naming no lens reads "Lens not
recorded", and a lens the database has never heard of reads "· no profile".
Collapsing the last two would send somebody hunting for a profile that was
never missing — which, for third-party and adapted glass, is the ordinary case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 15:11:32 +02:00
dtourolleandClaude Opus 5 c1069ce07e Release 0.10.1
Benchmarks / CPU and I/O (per commit) (push) Successful in 13m30s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / android-image (push) Successful in 3s
🐳 Android image / Build and push (push) Successful in 3s
Build and test / Layer separation (push) Successful in 46s
Traceability / Requirement traces (push) Failing after 1m36s
Build and test / Desktop (Linux) (push) Failing after 24h17m53s
Build and test / Android (aarch64) (push) Canceled after 0s
A bug fix release. Nothing here changes what DarkRoom is for since 0.10.0;
it changes how often it does the thing it already claimed to do.

The edits that went missing. A subject or category layer was stored as
identity alone, on the reasoning that the pixels were reproducible by
re-running the model — true, but nothing re-runs one except a photographer
pressing "find subjects". So a reopened photograph rendered without its
local adjustments and then saved that state back, and a batch export wrote
three hundred files without the edits their photographer had made, over a
log warning. The coverage now travels in the sidecar. Export also learned
to write the photograph rather than the canvas, and to carry the
photograph's header when the export is made from develop.

Face indexing was reading previews. It ran against a proxy and then
recorded the result as though it had seen the photograph, so faces smaller
than the proxy could resolve were not missed, they were *concluded absent*.
Indexing now runs on the native render, runs made against proxies too small
to find a face are forgotten rather than trusted, and a sweep that fails
everything says so instead of reporting a clean pass.

Where you were. The photo roll opens on the frame it opened with, develop
returns you to the photograph you were editing, the grid keeps its place
when another screen covers it, and the photographer's position now travels
between devices rather than being rediscovered on each.

Startup. The catalog opens on a worker and the bundled models unpack on
one, so a launch is no longer a page-by-page read on the way to the first
frame; the app says it is starting before there is anything to say it with.
The People rail builds the rows you can see, keeps portraits off the
blocking path, and withholds the empty groups that used to fill it.

Segmentation gained the half it was missing: the colour gate decided what
belonged to a category and had no way to decide where its edge fell, so a
refined sky kept the model's blocky outline no matter how the control was
set. A marker-based watershed now puts each contour onto a real edge, and
the refinement is a per-layer slider.

Coverage 70.4% -> 70.6% (127/180).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 21:33:16 +02:00
dtourolleandClaude Opus 5 23c3155128 Release 0.10.0
Benchmarks / CPU and I/O (per commit) (push) Successful in 11m46s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Failing after 1h15m53s
Build and test / Layer separation (push) Successful in 40s
🐳 Android image / Build and push (push) Successful in 8s
Build and test / android-image (push) Successful in 8s
Traceability / Requirement traces (push) Successful in 1m32s
Build and test / Android (aarch64) (push) Successful in 1h3m1s
Three waves of work since 0.9.0.

Culling gained the two instruments FR-CULL-3 asked for and never had:
focus peaking, and a histogram that reads the sensor rather than the
frame about to be displayed. Bursts group themselves. Masks can cover a
whole category rather than one instance.

The catalog now notices when it has been damaged and offers a way back --
restore a backup, or rebuild from the photographs, which were never at
risk. A crash leaves a record. A log survives the process, on a path a
tablet will hand back to adb, which is what makes the Android work
debuggable at all.

The APK compiles its own Java for the first time, so the app can receive
a photograph from another application and hand one back. Memory pressure
is answered in a stated order. A lost library root is reported rather
than reported as an empty library.

There is a benchmark suite now, so §8's promise that a regression fails
the build is a mechanism rather than a sentence. Its first run says the
catalog opens in 70ms against a 2s budget, and that thumbnail throughput
does not obviously reach its target.

Coverage 59.8% -> 69.8%, and it means more than it did: five requirements
that were tagged on code that did not implement them are no longer, and
the tool no longer counts its own test fixtures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 16:52:05 +02:00