A command whose journal append fails now returns EngineError::NotSaved
("saved in memory but not written to disk: <reason>; LightCraft will retry")
instead of OK. The change stays applied and undoable, its ops stay queued and
are written by the next save (any command, or the frame loop with a 2 s
backoff). Queries and commands that change nothing are unaffected; a failed
compaction is reported in lastError, not as a failed command (and a failed
synchronous snapshot now backs off like a background one).
Session::unsaved(), library.info unsavedOps/unsavedError, ui.inspect unsaved;
the desktop UI toasts the failure and shows a warning on the top bar's cloud
icon until a save succeeds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
preset.import (File ▸ Import Presets…, Presets panel ⋯, drag & drop, CLI, MCP) now reads
everything people actually download or carry over from other editors:
- classic .lrtemplate files (Lua table literal parser; value.settings uses crs: field names),
including old process-version fields (Exposure, Contrast, FillLight, HighlightRecovery,
Shadows, Brightness, Clarity, ToneCurve) approximated with today's sliders;
- photos carrying their edits in XMP ("DNG presets" from mobile apps, edited JPEG/TIFF): the
look only, without the photo's crop, geometry or custom white balance;
- .zip bundles (stored + deflate) of any of these, skipping __MACOSX / hidden files;
- folders: the pack's folder name becomes the group, skipping app/format container folders
("Lightroom Classic", "XMP", "Mobile (DNG)"…).
Each imported preset reports the settings that had no counterpart (`unmapped`: profiles,
looks, local masks); the app's toast names them. dryRun previews, group overrides.
Dropped .lcpreset/.lrtemplate/.xmp/.zip files import as presets instead of photos.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- engine: `merge.hdr`, `merge.panorama`, `merge.hdrPanorama` (bracket size from
EXIF); MergeJob (Send) with progress/cancel, preview renders through the develop
pipeline (deghost overlay, auto-crop frame); finish writes <name>-HDR/-Pano.dng
next to the first source, imports, selects, applies Auto Settings / auto crop
- ui: Photo Merge dialog (HDR / Panorama / HDR Panorama) with live preview on a
worker thread, background final merge with progress toast; context menu and
Ctrl+H / Ctrl+M (Ctrl now means the physical Control key on macOS)
- cli: `merge hdr|panorama|hdr-panorama`, `synth-merge` for synthetic inputs
- merge::synth: procedural brackets (DNG) and panorama views; docs/merge.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`media::develop` renders on the GPU when available (views, render_now, exports;
thumbnails stay on the CPU) and falls back to the CPU otherwise. `app.gpu
{enabled?}` toggles it at runtime, `LIGHTCRAFT_GPU=0` disables it for the process,
`ui.inspect` reports the adapter under perf.gpu.
Performance: dropped device buffers are recycled once their thread has submitted
(fresh 100-300 MB allocations per pass cost as much as the passes), Gaussian
passes ping-pong between two buffers and read all channels of a pixel at once,
and long box radii slide over longer chunks. LIGHTCRAFT_PROFILE prints real GPU
stage timings. render_bench prints CPU and GPU columns and the CPU/GPU difference.
24 MP ARW, loaded machine, min of 7: cold loupe 415 -> 32 ms, exposure drag
22-42 -> 3.9 ms, NR drag (1920) 301+ -> 18 ms, export render 1037+ -> 291 ms;
max 1 LSB difference. docs/gpu-pipeline.md describes the implementation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- pipeline: StageCache + render_cached: resampled source, white-balanced/retouched/denoised image
and each spatial plane (log L, guided base, clarity, texture, dark channel + airlight) are keyed
by exactly their inputs; keeps a draft-size and a full-size entry warm.
- Exposure moved after the spatial filters (applied per pixel; planes shifted by EV): identical up
to rounding (test: <= 2 LSB vs. exposing the source), so exposure drags hit the cache too.
- engine: RenderJob::draft / with_stages; ui: loupe and before views own a StageCache, loupe
renders at Draft quality while interacting.
- engine example render_bench (min-of-N wall + process CPU time).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- new crate lightcraft-preview (L3): 128-bit content hashing (SipHash-1-3),
cost-bounded Lru, DiskCache (JPEG q90 files keyed by content hash +
settings hash + size + RENDER_CACHE_VERSION; temp+rename writes; LRU prune
by mtime to 80% of budget; corrupt files dropped), PreviewCache (memory
LRU over disk), JobPool (per-slot dedupe, priorities, reprioritize/drop,
inline mode for wasm).
- engine: Session::thumb_job buckets thumbnails to 128/256/384/512 and serves
them from the cache without decoding the source; decoded thumbnail
sources now in a 384 MB LRU (was unbounded); library attaches
<library>/thumbs; library.info cache stats; library.clearPreviews.
- ui-egui: Renderer runs on JobPool; thumbnails scrolled out of the
prefetch area are dropped from the queue; on-screen thumbnails get
re-prioritized; ui.inspect reports in-flight renders + thumb textures.
- Demo library (24 photos): grid fully drawn 4.4 s cold -> 0.21 s warm
(24/24 disk hits, no source decoded).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>