mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-30 04:57:22 -06:00
Photos imported before focus points were read got one only through Reload from Disk, which
hashes whole files on the UI thread. Now:
- lightcraft_meta::focus::read_header reads only what the decoder looks at: IFD0 (Make,
Model), the Exif IFD, the maker note's IFD and the focus tags' values (a JPEG: its segments
up to the Exif one), walked round by round and decoded with the whole-file code from a
buffer of the file's length holding just those ranges. Bounded rounds, bytes (2 MB) and
size (1 GB). One 32 KB read per α1 / A7S III ARW; on 109 real files (83 with a point)
it gives the whole-file read's point for every one.
- A background job (crates/engine/src/focus_scan.rs) reads photos with no point from a
supported body (by the stored camera; nothing else is opened), ARW or JPEG, one at a
time in path order, with a "Reading focus points" activity row (Cancel::Flag). The UI's
per-frame focus.pump gives it the face scan's pace (paused while dragging or typing) and
starts it once per library opening. Commits every 200 photos or 3 s as system ops (no
undo step, no sidecar write), only meta.focus, only on photos that still have none at the
same path. "Looked, none" goes to focus-scanned.bin beside the library, headed by the
decoder's tag so a decoder knowing more bodies starts afresh; unreadable files are retried
at the next opening.
- photo.readFocusPoints {ids?, wait?} (Photo > Read Focus Points) runs it on demand.
Measured cold on a 5400 rpm USB drive: 200 α1 / A7S III ARWs in 1.9 s (~100 photos/s,
32 KB each), so 58,796 raws take about 10 minutes instead of re-reading 2.25 TB.
70 lines
4.6 KiB
Markdown
70 lines
4.6 KiB
Markdown
# Long-running tasks and the activity stack
|
|
|
|
Everything that takes a while runs off the UI thread and shows up in **one** place: the activity stack in the
|
|
top-left corner under the top bar (issue #345). Each task has a row with its name, a progress bar (a sweep while
|
|
the amount of work isn't known), the count and what it's working on, and ✕ if it can stop. Quitting asks first while
|
|
a task that can stop is running. Agents see the same list through `activity.list` / `activity.cancel`, and
|
|
`ui.inspect` reports it as `activity`.
|
|
|
|
The registry is `lightcraft_engine::activity` (`crates/engine/src/activity.rs`, `Session::activity`). The stack is
|
|
`crates/ui-egui/src/panels/activity.rs`. Every job that can last more than a moment uses it; **don't add a progress
|
|
window, panel or progress toast of your own**. A dialog may still show progress inline (the Synchronize Folder
|
|
dialog counts files while it scans), and a toast at the end saying what happened is fine. A view saying its pixels
|
|
aren't final isn't progress either: the loupe's "Rendering…" pill (`loupe:rendering`) marks a stand-in or an
|
|
out-of-date render on the photo itself, and the render behind it holds no row (a slider drag replaces it many times a
|
|
second).
|
|
|
|
## Adding a task
|
|
|
|
```rust
|
|
use lightcraft_engine::activity::{Cancel, Unit};
|
|
|
|
// when the job starts, with the cancel flag the worker already checks between files
|
|
let guard = app.session.activity.start("export", "Exporting", Cancel::Flag(cancel.clone()));
|
|
guard.progress(0, total as u64);
|
|
|
|
// every frame (or from the worker, through `guard.handle()`)
|
|
guard.progress(done as u64, total as u64);
|
|
guard.detail(&file_name);
|
|
```
|
|
|
|
- **Keep the guard for exactly as long as the job runs.** Put it in the struct the UI polls (`ExportTask`, `ScanTask`,
|
|
`SyncRun`…), or move it into the worker thread. Dropping it removes the row, also when the worker panics, so a
|
|
dead job never leaves a stuck bar. A worker that only reports progress takes a `TaskHandle` (`guard.handle()`,
|
|
`Clone + Send`).
|
|
- **`kind`** is a stable camelCase id that agents filter by (`export`, `import`, `scan`, `sync`, `previews`,
|
|
`download`, `faces`, `focus`…). **`label`** is a short English name in sentence case ("Building previews"). The stack shows it
|
|
translated, so add it to every catalog in `crates/ui-egui/locales/`.
|
|
- **Progress:** `progress(done, total)`. A `total` of 0 means the amount of work isn't known yet, and the bar sweeps.
|
|
`set_unit(Unit::Bytes)` makes the count read "12 of 340 MB"; `Unit::Percent` reads "40 %". The default `Unit::Count`
|
|
reads "3 of 25".
|
|
- **Detail:** what the job is on (a file name, a model's name). Detail set by engine code is a name, never a sentence.
|
|
UI code may set translated text.
|
|
- **Cancel:**
|
|
- `Cancel::Flag(Arc<AtomicBool>)` adopts the flag the job already checks. The row's ✕, `activity.cancel` and the
|
|
job's own Cancel then all set the same atomic.
|
|
- `Cancel::Yes`: the job polls `guard.is_cancelled()` itself.
|
|
- `Cancel::No`: the job can't stop; it gets no ✕ and doesn't make Quit ask.
|
|
- `set_cancellable(false)` for a phase that has to finish once started (a Lightroom import adding its photos).
|
|
- **Quiet periodic work** shows no row: the auto-import folder listing runs every few seconds through
|
|
`tasks::spawn(.., None, ..)`.
|
|
|
|
## Examples in the code
|
|
|
|
| Job | Where | Shows how to |
|
|
|---|---|---|
|
|
| Export, contact sheet | `crates/ui-egui/src/export_task.rs` | adopt the job's flag; update the row each frame |
|
|
| Build Previews | `crates/engine/src/cmd/previews.rs` | move the guard into the worker thread |
|
|
| Model downloads | `crates/engine/src/face_download.rs` | report progress through a `TaskHandle` in shared state |
|
|
| Lightroom catalog import | `crates/ui-egui/src/lightroom_import.rs` | stop being cancellable for a phase that must finish |
|
|
| Face scan, AI Denoise | `crates/engine/src/cmd/face_recognize.rs`, `crates/engine/src/denoise.rs` | a continuous job with no ✕, kept in session state |
|
|
| Find Missing Photos | `crates/ui-egui/src/tasks.rs` | a generic background task with a row (`kind: Some(..)`) |
|
|
| Reading focus points | `crates/engine/src/focus_scan.rs` | a worker paced by the UI's per-frame pump (`focus.pump {pace}`); `Cancel::Flag`; commits in batches without undo steps |
|
|
|
|
## Testing
|
|
|
|
Assert the row (`session.activity.list()`: kind, label, total, `cancellable`), that `activity.cancel(id)` stops the
|
|
job, and that the row is gone once the job ends. `crates/ui-egui/src/tests_activity.rs` has the helpers, and the
|
|
job-specific tests show the pattern (`tests_sync.rs`, `export_task.rs`, `tests_denoise.rs`…). A row only appears
|
|
on screen once its task is 0.5 s old, so tests that click its ✕ wait for `activity:cancel:<id>` first.
|