- 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>
`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>
New L3 crate `lightcraft-gpu`: a wgpu device (Metal/Vulkan/DX12; none on wasm32),
WGSL kernels compiled once, buffer/dispatch/readback helpers, and a renderer that
runs the per-pixel stage (tone map, highlights/shadows, clarity/texture/sharpen,
dehaze, local mask terms, OkLCh colour ops, grading, vignette, gamut map, sRGB
encode, curves, grain) on the device. Earlier stages still run on the CPU and are
uploaded (per-stage hybrid). Stage results are cached per view next to the CPU
StageCache (new `StageCache::extension`).
The CPU pipeline stays the oracle and shares its resolved parameters with the
kernels: `finish::FinishParams` / `mask_terms`, `pipeline::plan` (stage-cache keys),
`local::plane_sigmas` / `nr_params` / `wb_matrix_for`, exact LUTs and OkLab
matrices exported for the shader prelude. Equivalence tests (skip without an
adapter): mean |d| < 0.5 LSB, max <= 3 LSB; measured max 1 LSB on Apple M4 Pro.
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>
- ui-egui/softpaint: pure-Rust rasterizer for tessellated egui output (gamma-space premultiplied
blending like egui-wgpu, bilinear texel-centre sampling, scissor clips, tie-break rule so shared
edges blend once, parallel row bands, deterministic) + CPU texture mirror from TexturesDelta.
- ui-egui/headless: HeadlessView (offscreen context with our fonts/theme) and Headless (windowless
app session: logic+ui per frame, answers viewport Screenshot/InnerSize/Close, settle loop,
control requests through the shared handler).
- ui.screenshot {headless: true} in the running app renders from the logic tick (works while the
window is occluded / display asleep); windowed captures fall back to headless after 2 s.
Screenshots now wait for in-flight renders, not just queued ones.
- lightcraft-cli snapshot [--demo|--library DIR] [--script f.jsonl] -o out.png --size WxH --scale S.
- Fixes found with it: dialog backdrop was painted over the dialog itself (now an area below the
window); dialog content padding; crop overlay options use our buttons (registered widget ids).
- Docs: control-protocol.md (headless section), AGENTS.md (prefer headless when unattended).
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>
apps/lightcraft-web runs the egui UI in the browser (eframe web, glow/WebGL2):
procedural demo library on load; in-memory imports via file picker
(Cmd+Shift+I) and drag-and-drop (MemStore + bytes-based engine hooks);
export goes through app.export and downloads in the browser; `?bench` logs
first-frame and slider latency. `cargo xtask web [--serve [port]] [--dev]`
builds with the matching wasm-bindgen CLI into <target>/web/; the web app is
part of the CI wasm check. Wasm fixes: lazy profiling Instant in pipeline,
web-time clocks in ui-egui (render timings were 0 on wasm). Docs: docs/web.md.
No persistence on web yet.
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>
crates/mcp (lightcraft-mcp, L5): hand-written MCP over stdio (JSON-RPC 2.0,
protocol 2025-06-18 with 2025-03-26/2024-11-05 negotiation): initialize,
ping, tools/list, tools/call, resources/list/read. Two backends behind one
trait: Headless (in-process engine Session; answers engine.execute,
engine.commands, ui.render, app.export) and Remote (the desktop app's
loopback JSON-lines control channel; lazy connect + reconnect).
Tools: helpers (list_commands, run_command, import, query_photos,
select_photos, list_controls, get/set_develop, apply_preset, crop,
render_photo -> image content, export), UI tools when connected
(screenshot -> image content, inspect_ui, set_ui, list_widgets, click,
press_key, type_text, pointer_gesture), plus one cmd_<id> tool generated
per command in the registry (--compact omits them).
apps/lightcraft-cli: `mcp [--connect [ADDR]] [--demo] [--compact] [FILES]`,
`render <in> -o <out> [--set control=value]... [--settings] [--preset]
[--size] [--quality]`, `commands`, `controls`.
Acceptance e2e: crates/mcp/tests/e2e.rs sets exposure over MCP and renders
(headless and through the TCP transport); apps/lightcraft-cli/tests/cli.rs
does the same through the real binary's stdio. Docs: docs/mcp.md,
docs/control-protocol.md. Also rustfmt of one line in pipeline/finish.rs
(main was not fmt-clean).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decoders (JPEG via zune-jpeg + jpeg-decoder for DCT scaling/CMYK, PNG,
TIFF, WebP, GIF, BMP, PSD composite, JPEG XL via jxl-oxide) produce linear
Rgb32f in source primaries with SourceSpace; ICC matrix/TRC applied exactly,
LUT/CMYK profiles through moxcms; to_working() to linear Rec.2020 D65.
Encoders: JPEG, PNG 8/16, TIFF 8/16/32f, lossless WebP, AVIF (native).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Classic + BigTIFF, both byte orders, all field types, SubIFDs and Exif/GPS/
Interop pointer IFDs, strips/tiles layout, maker-note offset-base helpers,
IFD writer; hardened against malformed input (proptests). Placeholder
manifests for lightcraft-meta and lightcraft-raw.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>