"Delete" parsed as egui's forward-Delete key, so the Mac ⌫ key (Backspace) didn't delete. In the
Masking panel ⌫ now deletes the active mask; while retouching it does nothing. ⇧⌘N no longer
fires album.create (which needs a name).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- engine: QuickJob — what to show the moment a photo opens: the loupe's last render for the
current settings (kept by Session::loupe_job under a size-independent "view" key in the
preview cache, memory + disk with a library; the disk JPEG is written off the render
thread via PreviewCache::put_deferred), else the embedded camera JPEG of a raw that still
has its import look (import::has_import_look), else a thumbnail-level render.
- Grid / filmstrip: an unedited raw without a thumbnail shows its cached thumbnail, else its
embedded preview (Slot::ThumbQuick) at once; the real rendered thumbnail follows at
background priority (same cache keys, so it still ends up matching the develop render;
edited photos render directly).
- Loupe (detail, compare, filmstrip selection): Slot::Preview holds the stand-in until
Slot::Main has the photo; the full render then replaces it in place (no blank frame).
- files::embedded_preview_srgb + PreviewLoader hook (desktop/CLI/MCP and web).
- ui.inspect → loupe {photo, source: render|cached|embedded|small|thumb|none, pending}.
Real app (24 MP raws, loaded machine): first image 15–90 ms (thumbnail, then embedded
preview), full render 0.2–0.5 s (was 1.0–2.4 s with nothing shown before it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- lightcraft-raw: RawImage::develop_binned(k) — k×k superpixel binning of the CFA
(Bayer k even, X-Trans k multiple of 3), black/white normalised row by row, default
crop only, clipping preserved for highlight reconstruction. 2560 px previews of a
24 MP raw bin 2×, thumbnails 4–8×; exports / 1:1 still demosaic (AHD) at full size.
- highlight::reconstruct and its pyramid fill are rayon-parallel (bit-exact).
- X-Trans directional demosaic: precomputed per-phase neighbour offsets and no
reflection in the interior, no per-pixel allocation (bit-exact, tested against the
reference formulation): 2.9 s → 0.75 s CPU at 24 MP.
- files::load_bytes runs on a rayon worker so its short parallel loops don't each
wait for the pool to wake (dominant cost on a loaded machine); LIGHTCRAFT_PROFILE
prints per-stage source timings; files::bin_factor picks the block size.
- render_bench ONLY=source: per-file decode / normalise / demosaic / binning / load.
- Fix: u64 overflow on hostile IFD sizes in DNG/ARW/NEF raw-IFD selection (proptest).
load_bytes(2560) CPU time, ARW a7 III 24 MP: 2347 → 240 ms; X-T20: 3120 → 1220 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Renders run in dedicated module Web Workers, each a second instance of the app's wasm module
(index.html compiles it once and posts the WebAssembly.Module to worker.js). A job crosses as
JSON (WireJob: source, settings, request); the worker reads originals and cached thumbnails from
browser storage itself, keeps decoded sources and per-view stage caches, and transfers RGBA back.
Jobs follow the worker that decoded the photo; the loupe waits for it. No wasm threads or
SharedArrayBuffer needed; falls back to inline rendering if no worker starts (?workers=0).
Thumbnail disk cache on the web: thumbs/<key>.jpg in OPFS/IndexedDB with an index on the main
thread, pruned to the desktop budget (2 GB → 80 %, LRU).
ui-egui: RenderOffload hook in the Renderer (queue/priorities/dedup stay in the UI).
engine: RenderJob.origin + MediaCache::origin_ref; preview: encode_jpeg/decode_jpeg public.
xtask web: copies worker.js; --serve sends COOP/COEP/CORP (documented in docs/web.md).
Headless Chrome, cold load + grid scroll, 24 demo photos: main-thread busy 12.4 s → 0.7–1.1 s,
long tasks 23 (max 1.1 s) → 0; grid filled in 6.3–8.4 s vs 13–16 s; slider job 4 ms.
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>
The catalog Store on the web is an in-memory mirror (apps/lightcraft-web/src/files.rs) loaded
from browser storage at start-up and flushed in the background in modification order (so a
snapshot always lands before the log reset that follows it). Storage is OPFS when writable from
the main thread (createWritable), else IndexedDB (?store=idb forces it); ?reset clears it.
Imported originals are stored as originals/<content hash> and referenced as
web/<hash>/<name>; the main thread keeps a bounded memory cache and loads misses (and the
active photo) asynchronously. view.json and ui.json are saved every second when changed.
Engine: Session::open_library_in(LibraryStores) opens a library from any Store (presets, view
and prefs go through it too); Session::save_view; Copy imports into a non-disk library add.
JS automation API: lightcraft.command(id, paramsJson) (+ web.stats).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Integrated with M4 optics/Upright: render_impl resolves Upright and uses frame_for (lens + auto CA)
on the cached source; defringe runs in the cached linear stage and optics settings are part of its key.
Stage-cache equivalence test extended with optics/geometry steps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The guided base, clarity band, texture band and dark channel (and the paired blurs inside the
guided filter) are independent; each alone scales poorly (subsampled grids, short rows), so they now
run under raster::par_join. Same arithmetic, identical pixels. render_bench gains a per-feature
breakdown of the per-pixel stage (ONLY=finish).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- pipeline::upright: own gradient-based line-segment detector (level-line region growing seeded from the
strongest gradients, weighted principal-axis fit, elongation test); vanishing points by RANSAC over
segment pairs + least-squares refinement (smallest eigenvector of Σ w·l·lᵀ)
- corrections as a virtual camera rotation K·R·K⁻¹ (vertical VP → y axis; Full also horizontal VP → x axis
with focal length from VP orthogonality when plausible); Level = rotation; Auto = Full/Vertical when
moderate, else Level; all re-centred
- Guided: guides stored in normalized lens-corrected coordinates; 2+ vertical/horizontal guides define the
vanishing points, a single guide levels
- the analysis runs on the lens-corrected image at 1024 px and the result is stored in
geometry.upright_transform (identical preview/export); settings naming a mode without a transform
(XMP, CLI) are resolved at render time
- commands geometry.upright {mode} (Update = run again) and geometry.guides {guides, add}
- UI: Upright mode buttons, Update, Draw/Clear Guides in the crop panel; on-canvas guide drawing that
follows the warp
- tests: segment detector on known lines; Vertical/Auto/Full fix converging verticals; Level fixes a tilted
horizon; Full fixes both families; guided from two guides; analysis resolution independence; lazy vs
stored transform; engine commands
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Exports above 2560 px were rendered from the 2560 px preview proxy and upsampled. SourceLevel::Full
(native size; procedural demo scenes at their nominal size) is now selected for outputs larger than
a preview; the media cache keeps only the last full-size original. render_bench gains a batch-export
scenario (decode + render + JPEG per file).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- pipeline::optics::Warp: one inverse map (lens-corrected → source) inside the single geometry resample:
manual radial distortion, embedded DNG WarpRectilinear (per plane), per-channel lateral CA scale,
radial vignetting gain (manual + embedded FixVignetteRadial); all in normalized radii
- automatic lateral CA estimate: radial edge profiles matched sub-pixel per plane, robust LSQ fit of
displacement = α·r (cached per source)
- defringe: purple/green hue ranges (OkLab hue) desaturated next to high-contrast edges, width relative
to the image long edge
- "Enable Lens Corrections" applies only lens data embedded in DNGs (opcodes are no longer baked at decode;
stored on the photo record, on by default at import when present). No LCP data anywhere.
- new controls: optics.caRed/caBlue, defringe hue ranges; Optics panel: Manual sub-section, Defringe flyout
- tests: barrel grid straightened, CA removal (auto + manual), embedded corrections toggle/scale, preview vs
export resolution independence, CA estimator accuracy, orientation of embedded lens data
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- finish: sRGB encode through an interpolated 4096-entry table (< 2e-5 from the exact curve,
tested), exp2 instead of powf(2, x).
- guided / guided_fast: coefficient and output passes run row-parallel (were serial loops);
Image::zip_map.
- blur_bench reports process CPU time next to wall-clock (meaningful on a loaded machine).
- RENDER_CACHE_VERSION 3 (exposure-after-filters and the table change pixels by <= 2 LSB).
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>
- embedded_preview: CR3 PRVW/THMB boxes (plus RAF, RW2, ORF, PEF from the
format commits); stops at the first valid box of each kind
- engine: raw variants that report Unsupported are probed and shown from
their embedded JPEG (orientation from the JPEG or the raw), not an error
- crates/raw/tests/corpus.rs: every corpus/raw file is recognised, has a
JPEG preview (DNG optional) and decodes or is a known-unsupported
variant; prints decode timings
- xtask corpus --download: 37 CC0 raw.pixls.us samples (matched by SHA-256)
- ROADMAP/crate docs: format coverage, NEF Huffman / RW2 format 4 /
compressed ORF+RAF gaps, neutral colour-matrix fallback
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- engine::import: expand files/folders recursively (sorted; hidden entries
and the library folder skipped), skip known paths, probe + hash in
parallel (std:🧵:scope), skip duplicates by content hash (against
the library and within the batch), add in place or copy into
<library>/Originals/YYYY/YYYY-MM-DD/ with unique names; one undoable op.
- library.import {paths, mode: add|copy, album?} returns
{imported, duplicates[{path, existing, reason}], failed, scanned}.
- probe_bytes records the content hash (SipHash-1-3 128) on every photo.
- system clock for import/edit times on native (Session::with_system_clock).
- app: command-line folders are expanded by the engine (re-importing the
same folder is a no-op); example gen_photos writes procedural test JPEGs
(never committed).
- Measured (200 procedural 2000 px JPEGs, M-series): import 2.25 s; reopen:
library load 1.7 ms, control ready 0.12 s, first thumbnail 0.17 s,
visible grid drawn 0.48 s (thumbnails from the disk cache).
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>
- catalog: Store trait (FsStore: temp+fsync+rename+dir fsync; MemStore),
Journal: CRC-checked JSON-lines op log with sequence numbers + atomic
snapshots; torn final record dropped and truncated, stale records after a
snapshot skipped, mid-log damage preserved as catalog.log.damaged-N.
- PushHistory op so each edit logs one history step (not the full history).
- engine: Session::open_library/persist/close_library; every top-level
command is fsynced before returning; presets.json + view.json;
library.info / library.compact commands.
- app: default library ~/Pictures/LightCraft Library (LIGHTCRAFT_LIBRARY or
--library DIR override), --memory for throwaway sessions, snapshot on quit.
- tests: proptest replay(log)==state with random snapshots/reopens, every
truncation of the last record, stale log, mid-log damage, FsStore, engine
restart without close.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Export encoding lives in lightcraft-engine::export (pure bytes, shared by app/CLI/MCP/web).
Embeds our own sRGB ICC; writer creates the output folder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>