Inspecting and importing a Lightroom catalog show a row instead of the
progress window; reading the catalog stops from its cross, adding the
photos and saving the index run to the end.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Build Previews, Build Smart Previews and Discard Smart Previews show a row
with the photo count and stop from its cross; the start toast goes, the
finish toast stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The import review's folder scan (and reading a folder for Local) is a
row in the activity stack: "Looking for photos…" while the folders are
listed, then the files read of the total. Its cross drops the scan at
once, without waiting for the file being read, as the old Cancel did.
The separate Scanning window is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cross sits at the right edge of its row (the name's area kept only
the text's width, so the cross followed short names). Seen in the
running app; a test pins it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An import (and Auto Import, and reading a folder for Local) registers in
the activity stack with the files done of the total; its cross and
activity.cancel set the import's cancel flag, so nothing new starts and
the result still reads "Import cancelled · N added" as one undo step.
The separate Importing window is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A background export registers in the activity stack with the photos done
of the total and the file in progress; its cross and activity.cancel set
the export's own cancel flag. The separate export panel at the bottom
left is gone; the result toast is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Quitting while an import, an export or another task that can be
cancelled is running asks first: Quit Anyway cancels them (then any
unsaved changes still get their own question), Cancel keeps working.
Tasks without a cross (the face scan, AI Denoise) pick up again on the
next launch and don't ask.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every task in the engine's activity registry floats as a row under the
top bar once it is half a second old: its name, a progress bar (a sweep
while the amount of work isn't known), the count and what it is working
on, and a cross for the ones that can be cancelled. Three rows, then
"+N more". ui.inspect reports the same list as activity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The registry is reachable as commands, so the UI, the control channel,
the CLI and MCP list and cancel background tasks the same way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A registry of the background tasks in flight on Session: a job holds a
TaskGuard while it runs (dropping it, also during a panic unwind, removes
the row), adopts its existing cancel flag, and reports done/total in a
unit the frontend words. Groundwork for one progress stack for imports,
exports, preview builds and the other long jobs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Testing the first version on ILCE-7RM4 photos found a partly clipped sky between
bare branches leaning cyan to teal under #523's edit, where Lightroom shows white.
Where only green clipped, red and blue say such a sky is a pale cyan-blue; the
rebuild kept that colour until halfway to full clipping, and darkening and a camera
profile showed it. Colour borrowed from another surface (bright foliage below the
sky) could also push green up, with a small step where green first clips.
- highlight::reconstruct_with: the colour of a pale partly clipped pixel
(saturation relative to the colour model's white below 0.2, easing off by 0.4)
fades to the clipped neutral from the first clip on instead of from halfway;
brightness and strongly coloured pixels keep the old schedule.
- Borrowed colour counts less and less as both the pixel's own channels and the
rim's colour contradict it (0.15 to 0.4 stop); either alone can be wrong.
- Right past the rim every estimate continues the rim's colour (it used to hold
the clipped channels at their clip, for the neutral estimate only), so the first
clip has no step whatever lent the estimate. The rim is built whenever there is
clipping.
- Nothing jumps where a later channel clips: in the rim distance and the
contradiction, a clipped channel counts as the lower bound it is, and a channel
goes over to that from 0.75 of its clip; a channel about to clip is a floor
capped like the clip floor it becomes.
- Tests: a pale sky above yellow-green foliage with thin dark branches in it (no
step where green clips, no green from the foliage, the same beside the branches,
neutral well past the clip); the fringe test compares the sky beside the branch
with the sky away from it, on its way to neutral; a band whose red clips second
next to a rim of another colour renders without a step, in six layouts.
- docs/parity.md: the Highlights note. Render cache version 28.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A render still running on a worker thread while the process exits crashes in the
driver (SIGSEGV in the NVIDIA Vulkan driver on Linux), where neither the panic
hook nor catch_unwind helps: pool workers were detached, the devices are statics
that are never dropped, and nothing on the way out waited for GPU work.
- gpu: every entry into a device (render, device creation, pool trim, denoise
runner and tiles) is counted as work in flight (exit.rs). begin_shutdown()
lets nothing new in; wait_idle(timeout) waits for what is inside.
- preview: JobPool keeps its workers. shutdown(timeout) and Drop discard the
queued jobs and wait, bounded, for the running ones (2 s on drop); never for
the calling thread, and not twice.
- engine: a render job caught by the shutdown gives up instead of rendering on
the CPU, so quitting doesn't wait for it.
- app: on_exit ends with LightcraftApp::shutdown (2 s for everything), after
settings and library are saved; the macOS menu-bar Quit goes through on_exit.
Not changed: the export, merge, denoise and import threads are still detached;
their GPU work goes through the gate, their CPU work is cut off at exit as before.
Tests: pool drop waits / is bounded / survives a panicked job and its own job
dropping it; the gate; GPU renders refused and the device not created after
begin_shutdown; the app's shutdown leaves no render worker. The crash itself
needs Linux + NVIDIA and was not reproduced here (Windows, DX12): 9 of 9 quits
of the app mid-render exit with code 0 in under 0.8 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Testing the first version on ILCE-7RM4 photos found a partly clipped sky between
bare branches leaning cyan to teal under #523's edit, where Lightroom shows white.
Where only green clipped, red and blue say such a sky is a pale cyan-blue; the
rebuild kept that colour until halfway to full clipping, and darkening and a camera
profile showed it. Colour borrowed from another surface (bright foliage below the
sky) could also push green up, with a small step where green first clips.
- highlight::reconstruct_with: the colour of a pale partly clipped pixel
(saturation relative to the colour model's white below 0.2, easing off by 0.4)
fades to the clipped neutral from the first clip on instead of from halfway;
brightness and strongly coloured pixels keep the old schedule.
- Borrowed colour counts less and less as both the pixel's own channels and the
rim's colour contradict it (0.15 to 0.4 stop); either alone can be wrong.
- Right past the rim every estimate continues the rim's colour (it used to hold
the clipped channels at their clip, for the neutral estimate only), so the first
clip has no step whatever lent the estimate. The rim is built whenever there is
clipping.
- Nothing jumps where a later channel clips: in the rim distance and the
contradiction, a clipped channel counts as the lower bound it is, and a channel
goes over to that from 0.75 of its clip; a channel about to clip is a floor
capped like the clip floor it becomes.
- Tests: a pale sky above yellow-green foliage with thin dark branches in it (no
step where green clips, no green from the foliage, the same beside the branches,
neutral well past the clip); the fringe test compares the sky beside the branch
with the sky away from it, on its way to neutral; a band whose red clips second
next to a rim of another colour renders without a step, in six layouts.
- docs/parity.md: the Highlights note. Render cache version 28.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The two new catalogs landed alongside PRs that added ~127 strings; the per-language
tests require them in every catalog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The loupe read the GPU's texture limit from the frame's raw input
(`i.raw.max_texture_side`). A native host (egui-winit) reports it in the
first frame's input only, so from the second frame on the loupe fell back
to 2048: every window render of a zoomed view was cut to 2048 x 2048 px,
and on a canvas wider than that (HiDPI, 4K, a large window) the strips
beside it showed the magnified whole-frame render. The whole-frame render
and the second-window and compare renders were capped the same way.
Read the limit egui keeps (`InputState::max_texture_side`) instead.
The headless host reported the limit in every frame, which is why no test
saw this: it now reports it once (and again when it changes), as a window
does. Two regression tests: the limit outlives the frame it was reported
in, and at 1:1 on a 2600 px canvas the window covers the canvas.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`encode_avif_pq`: linear Rec. 2020 with SDR white at 1.0 → PQ (SDR white at
203 cd/m², BT.2408), BT.2020 NCL YCbCr 4:4:4, 10-bit, with CICP nclx
(9/16/9, full range) and a clli box. ravif fixes the sRGB curve, so this
drives the rav1e + avif-serialize it already uses (no new crates).
Export: AVIF with `hdr` renders the HDR edit in Rec. 2020 and writes PQ.
Photos without an HDR edit export as before, in every format. CoreImage
decodes the file as HDR (same 3.1 % of the frame above SDR white as the gain
map JPEG of the same edit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Light panel: HDR switch, headroom limit and an SDR Rendition flyout
(shown with HDR on); Visualize HDR beside the switch.
- `Overlay::HdrRange` / `view.visualizeHdr`: the SDR render dimmed to grey,
with pixels the HDR rendition puts above SDR white painted in four bands by
stops above it. Rendered on the CPU from the float HDR render.
- Export dialog: "HDR output" for JPEG (gain map) and 32-bit TIFF.
- Parity (Q. HDR 2 ✅ 3 🟡, LR-EDIT-HDR-MODE ✅, LR-EXP-HDR 🟡), README, ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `ExportOptions.hdr` (`hdr` param): photos edited in HDR export as ISO
21496-1 gain map JPEGs (the SDR rendition as the base, the gain map measured
against it as written) or 32-bit float TIFF with highlights above 1.
Other formats and SDR edits export as before.
- Output sharpening and watermarks apply to both renditions.
- `develop.hdr {enabled?, maxEv?}` turns HDR editing on/off and sets the
headroom limit.
Checked on macOS: ImageIO reports both the ISO and the Apple gain map, and
CoreImage's HDR decode of a 3 EV edit reaches 2.95 EV.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sensor-clipped pixels are rebuilt at raw decode. The tone map rolls them off to
white at Neutral, so any colour they carry shows only when Highlights, a negative
Exposure or Whites darkens them. They took colour from the nearest unclipped pixel
(dark foliage, lens fringes along branches, pixels demosaiced from clipped samples),
left two clipped channels at their white-balance-scaled clip levels (red or pink),
and were neutral only in camera RGB, not after the colour model (a look fitted to
the camera's JPEG, a camera profile).
- highlight::reconstruct_with: chromaticity only from bright unclipped content away
from edges and from the clip, through a normalised push-pull pyramid; a neutral
estimate blended in where reliable content is scarce at every scale up to a
quarter of the long edge, applied only as far as the pixel lies past the nearest
near-clip unclipped pixels (so a surface still runs into clipping without a
step); clipped pairs keep the estimate's ratio at their clip floors; partly
clipped pixels fade into the clipped neutral on their way from the first channel
clipping to the last; fully clipped pixels become the colour model's neutral.
Pixels without a clipped channel are left exactly as they were.
- engine: the decode colour step as CameraColourModel, with clipped_white, which
keeps the camera's neutral for nearly singular or implausible models.
- Tests in raw (continuity along a ramp through every clip, also as lines 1 to 16
rows thick with no reliable colour, on canvases 32 to 1024 rows), engine
(pipeline renders at Highlights -100, #523's edit and Exposure -2 through the
first clip, over the same sweep; a CC0 ILCE-7RM4 corpus file added to
RAW_SAMPLES) and the GPU equivalence suite.
- Render cache version 27.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Part of #535 (item 4).
Pre-2017 bodies without Sony crop tags shot in the camera's 16:9 mode now get
the 16:9 crop of their camera JPEG: width from FullImageSize, anchored left as
before (drops the ILCE-7SM2's 32 padding columns); height from FullImageSize
(ILCE-7SM2) or from an Exif image size of the same width (DSLR-A580, whose
FullImageSize still says 3:2); centred vertically at an even offset, as measured
by registering each camera JPEG on its raw. The aspect now matches the JPEG, so
the camera-look fit runs: ILCE-7SM2 dE 15.2 -> 4.8, DSLR-A580 dE 26.7 -> 3.9.
Only these 2 of 140 Sony decodes change; render cache version 26 (23-25 are
taken by #568, #542, #575).
Tests: crop helper (3:2 unchanged, both 16:9 sources, even offsets, hostile
sizes) and a corpus test of the two crops against their camera JPEGs; both
samples added to the corpus list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`lightcraft_codecs::gainmap`:
- `compute` measures a gain map (luminance or RGB, log2, block-averaged)
from an SDR and an HDR rendition; `apply` rebuilds the HDR rendition.
- ISO 21496-1 metadata: writer (fixed denominator) and reader (also the
common-denominator form; refuses unknown versions, backward maps and zero
denominators). Adobe `hdrgm` XMP reader and writer, per-channel values as
rdf:Seq; Apple's HDRGainMap/HDRToneMap description of the same map.
- `encode_jpeg`: base with CIPA DC-007 MPF index (gain map typed 0x050000),
the ISO marker and Container XMP merged into the user's packet, followed by
the gain map JPEG. `read_jpeg` / `is_gain_map_jpeg` find and decode the map.
Also: gain map images are no longer picked as embedded thumbnails (an Ultra
HDR JPEG could show its greyscale gain map in the grid). The JPEG APP-segment
building is shared with `encode_jpeg`. Fuzzed with the other decoders.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `DevelopSettings.hdr`: enabled, headroom limit (`max_ev`) and SDR-rendition
offsets (brightness, contrast, highlights, shadows, whites, clarity), with
control specs (`hdr.maxEv`, `hdr.sdr*`) and copied with the Light group.
- `ToneMap::hdr` / `display_hdr`: the SDR curve through the midtones, with
highlights lifted up to `2^max_ev`; identical to the SDR tables at peak 1.
- `OutputDepth::F32Hdr`: unclamped linear output up to the peak. The finish
stage carries the peak through highlight desaturation, gamut mapping,
encoding, curves, grain and LUT profiles; with HDR off every branch is the
SDR code path, bit for bit.
- `settings_for`: every non-HDR render (CPU and GPU) uses the SDR rendition.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Uncompressed 16-bit words are read in the other byte order only when the
file's order leaves more than 1% of sampled words at or above
1 << max(bits, 14) and the other order leaves none. Measured on the CC0
DSC-R1 (raw.pixls.us 3221). Crop and white balance (SR2SubIFD) are not
addressed. RENDER_CACHE_VERSION 31 (adjust at merge).