Move nested conditions into match guards and simplify equivalent Boolean
expressions without changing validation or short-circuit behavior. Keep
the explicit permitted Testkit dev-dependency arm in the layer checker.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
Catalog format v9 adds Photo.preview_checked and Op::SetContent.preview_checked:
the app version (CARGO_PKG_VERSION) whose decoder set preview_only, empty when
it isn't set. Import and content_op (Reload, Relink) stamp it; undo swaps it
back. Catalogs from before v9 read an empty stamp, so their preview-only photos
count as stale (Photo::preview_only_stale) and will be rechecked once.
The version guard test now also accepts a format version that only adds fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New vendor::arw_lens_rm6 reads the raw IFD's lens tables (0x7037 distortion,
0x7032 vignetting, 0x7035 lateral CA, their flags) and turns the distortion
table into a one-plane WarpRectilinear in OpcodeList3, with the conventions
measured on 54 ILCE-7RM6 photos: knots at i/15 of the full-frame crop half
diagonal, S(r) = r (1 + v(r)/15000), frame-filling zoom, centred on the
default crop. Only the ILCE-7RM6 is enabled; vignetting is read, not applied.
The radial least-squares fit is split out as vendor::fit_radial_warp (RW2
results unchanged); its error gate can be limited to the radii the crop
shows, so a padded active area (lossless files) doesn't refuse a fit over
pixels that are never rendered.
It sits next to main's vendor::arw_lens (#498), which holds the ILCE-7RM4A
conventions measured against Sony's exports (unit 16384, table indexed at the
output radius); the two are selected by camera model (disjoint allowlists).
arw::decode chains this module's opcodes after arw_lens::distortion, with the
same active area and the same crop (the DNG-style default crop when the file
has one, which on the ILCE-7RM6 equals Sony's crop tags), and like it only
for CFA data (not the downsized linear-RGB lossless sizes, where the knot
spacing in full-frame pixels would not hold).
The corpus test checks the conversion on the two lossless RawDB files (CC0).
The APS-C one is pinned in xtask RAW_SAMPLES with its sha256; the full-frame
one is not (at most two pins per PR, and #625 pins two Compressed ARW 6.0
files), so its case runs only where the file is present.
RENDER_CACHE_VERSION 62 -> 63.
Co-Authored-By: Claude <noreply@anthropic.com>
Grid thumbnails looked much more denoised than the loupe. `nr_params` sized
the radii from the output's scale relative to the source buffer it was given,
not the original: a 256-px cell drawn from the 512-px thumbnail level used
half the full-size radius on pixels that each average ~23 sensor pixels, and
below 0.4x (0.35x for colour) the radius stopped shrinking at all. The
guided filter's eps (the variance treated as noise) also stayed at its
full-size value although downscaling divides the noise variance by 1/scale²,
so texture in small previews was flattened as noise.
- `SourceInfo::native_long` (filled by the engine from the catalog's photo
size, kept when the decoder supplies its own facts, carried to web
workers) and `Plan::detail_scale` = output pixels per original pixel
(from `px_per_long`, so a crop no longer changes it).
- `nr_params(s, scale)`: radii x scale with no floor, eps x scale². Below
1 px the smallest blur the box Gaussian makes is blended in by the ratio
of variances (`blur::box_sigma`); a fade under 1% skips the filter.
At scale 1 the parameters are unchanged: 1:1 views and full-size exports
render as before. The GPU takes the same parameters.
- RENDER_CACHE_VERSION 63: cached thumbnails are redrawn.
Measured on a 1536x1024 noisy synthetic scene (tests_nr_scale.rs), error
of the preview against the full-size render scaled down, as a fraction of
leaving NR off, luminance NR 50 at preview scales 0.5 / 0.33 / 0.25 /
0.167 / 0.1 / 0.05: before 0.53 / 0.70 / 1.04 / 2.26 / 3.87 / 6.07, after
0.28 / 0.37 / 0.66 / 0.90 / 0.99 / 1.00. A 256-px render from the 512-px
level now matches one from the original (mean 0.07 LSB vs 0.71 before).
The grid queues no render behind an unedited raw's quick thumbnail, since the embedded JPEG is
meant to be the thumbnail. But whether that JPEG can be read is only known when the quick job
runs, and `quick_thumb_job` had no fallback: when the preview loader returned nothing the job
failed with "no quick preview", the failure was remembered for that key, and the cell stayed
empty until an edit changed it. The quick job now carries the real thumbnail job as its last
resort, as `quick_view_job` already does for the loupe.
An unedited raw's face tile is cut from its largest embedded preview, read
the planned way (header and JPEG, never the sensor data), when the preview
holds at least 3/4 of the tile's pixels across the face; otherwise, and for
edited photos and other files, it is developed as before. Tiles keep going
to the memory + on-disk thumbnail cache, now at 16 px size steps so the
thumbnail slider reuses them. The planned preview path carries an Adobe RGB
preview colour space and applies it instead of declining to the whole-file
read (Sony alpha 1 previews).
MediaCache::enforce_budget never evicts the original of the photo on screen (set_on_screen), but it still
counted it. A 64 MP original (Sony A7R VI: 9984×6656 linear float, 760 MiB) nearly fills the default cache
share (768 MiB) alone, so the loop evicted everything else instead: the photo's preview and thumbnail sources,
the rendered previews and the prefetched neighbours. Every edit at 1:1 (the whole-frame render and the
thumbnail), every zoom out and every step to the next photo then decoded the raw again.
The pinned original is now left out of what is compared with the budget. The rest still fits the share; the
original sits over it, as the first commit of this PR already allowed.
Co-Authored-By: Claude <noreply@anthropic.com>
Each drag tick changed the edited photo's settings, so the filmstrip asked
for a new thumbnail render every frame. For an unedited raw shown with its
embedded preview, the thumbnail-level source had never been decoded, so each
of those renders decoded the whole raw (a 64 MP ARW: over a second), several
side by side, and the loupe's drafts queued behind them.
- The edited photo's thumbnail is not re-rendered during a slider drag while
it has one; it follows once the drag ends.
- A thumbnail of a photo whose preview-level source is decoded (the one in
the loupe) is cut from that source (SourceRef::Fit) instead of decoding the
file again; not when the photo has a denoised picture.
Co-Authored-By: Claude <noreply@anthropic.com>
For a DNG with its own colour matrix, an identity or absent ProfileToneCurve, no
LookTable and no ProfileGainTableMap, fit only the tone and chroma curves to the
camera JPEG, with the file's colour model untouched (profile tables and embedded
vignetting applied to the sensor proxy as the loader renders it). The fit is used
only when the preview is at least 90% of the raw's long edge, shows the same
picture (structure gate), passes the existing held-out acceptance, and does not
put more of the photo near white than the plain render (highlight gate, at 2x
proxy luminance). Otherwise the render is unchanged. Other formats and
catalogued photos are untouched. Render cache version 62.
A decoded 64 MP original (Sony A7R VI: 9984×6656 linear float, 797 MB) is bigger than the
default cache share (half of 1.5 GiB). MediaCache::enforce_budget evicted it as soon as the
render handed it over, so every pan of a 1:1 view and every slider move while zoomed decoded
the raw again (and lost the shared GPU copy, keyed by the CPU Arc).
The frontend now tells the cache which photo Detail shows (MediaCache::set_on_screen, set per
frame in panels::compare::prepare); that photo's original is not evicted for the budget and is
let go as soon as another photo or view is shown. Keying it on "no newer preview of another
photo" instead does not work: the neighbour prefetch stores previews after the original.
- Files with no capture date in their metadata or sidecar (e.g. exports saved without
metadata) take the file's modification time, as Lightroom does, instead of all landing
under "Unknown Date" (the loupe overlay showed the import date for them). The import
review and its folder/rename templates use the same date.
- The import review counted files with the same bytes as another file in the source as
"already in the library" (8 on a brand-new, empty library); they are now shown apart as
"identical copies".
- View > Grid Info captions are drawn on Square Grid only; choosing one from Photo Grid now
switches to Square Grid instead of seeming to do nothing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A panic inside RenderJob::run or QuickJob::run (a raw decoder or pipeline bug) unwound the
job pool's worker without sending a result. The slot stayed pending and its key was never
asked for again, so the loupe or the thumbnail rendered forever; and the pool never
replaces a dead worker, so after as many such panics as it has workers (2–6) no photo
rendered until LightCraft was restarted.
Both now catch the panic (guard::catch) and return it as the job's failure, answering the
request it came from, so the renderer records it like any failed render: no retry until
the catalog changes, and the workers live on. run() is the one place every render goes
through (the pool, export, Build Previews, contact sheets, face and AI-mask workers);
callers that had their own guard keep working, and the watermark preview's, now
redundant, is gone. The web build aborts on panic, so nothing changes there.
Co-Authored-By: Claude <noreply@anthropic.com>
Soft proof is per-pixel, so the After window carries the proof like the
whole-frame render under it (the proof key is part of the tile's look).
New tests: a 1:1 window of a 24 MP photo equals the full-size export to
within one level; soft proofing at 1:1 is a canvas-sized frame plus a
native window. profile_slider_drag gains a soft-proof case. Gap 4 closed,
checklist row to done with measured Linux latencies.
Sources: own design.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On merge: the HDR histogram is applied in develop_reporting (#712 split develop), is skipped for
draft renders (a second render per drag tick), and HdrBins::bin clamps a hand-built split so it
can't underflow (test hdr_bins_with_any_split_stay_in_range).
On merge: the quick preview path orients like the whole-file path (preview_orientation with the
raw's header prefix, so a portrait raw whose embedded JPEG carries no orientation is not sideways),
and its doc links point at preview_window.