Commit Graph
198 Commits
Author SHA1 Message Date
Brandon Thomas 5242f02838 Merge PR #1741 2026-10-14 22:29:38 -04:00
Brandon Thomas a8f0f5d60b Merge PR #1715
# Conflicts:
#	docs/gaps.md
#	docs/parity-checklist.md
#	docs/target-app-parity.md
2026-10-14 22:29:33 -04:00
Brandon Thomas aa384d62f7 Merge PR #1466
# Conflicts:
#	docs/camera-preview-colour.md
2026-10-14 22:29:14 -04:00
Brandon Thomas aba04aaf5c Merge PR #1087
# Conflicts:
#	ROADMAP.md
#	docs/gaps.md
#	docs/parity-checklist.md
#	docs/raw-parity.md
#	docs/raw/coverage.tsv
#	docs/raw/evidence.tsv
#	xtask/src/main.rs
2026-10-14 21:59:31 -04:00
Brandon Thomas 0bef63b6eb Merge PR #974
# Conflicts:
#	crates/engine/src/cmd/mod.rs
#	crates/ui-egui/locales/de-formats.json
#	crates/ui-egui/locales/de.json
#	crates/ui-egui/locales/es-formats.json
#	crates/ui-egui/locales/es.json
#	crates/ui-egui/locales/fr-formats.json
#	crates/ui-egui/locales/fr.json
#	crates/ui-egui/locales/ja-formats.json
#	crates/ui-egui/locales/ja.json
#	crates/ui-egui/locales/pt-br-formats.json
#	crates/ui-egui/locales/pt-br.json
#	crates/ui-egui/locales/ru-formats.json
#	crates/ui-egui/locales/ru.json
#	crates/ui-egui/locales/uk-formats.json
#	crates/ui-egui/locales/uk.json
#	crates/ui-egui/locales/zh-hans-formats.json
#	crates/ui-egui/locales/zh-hans.json
#	crates/ui-egui/locales/zh-hant-formats.json
#	crates/ui-egui/locales/zh-hant.json
#	docs/mcp.md
#	docs/parity-checklist.md
2026-10-14 21:55:01 -04:00
midasmatterhorn 14d321c107 catalog: slice captured dates on char boundaries so non-ASCII dates don't panic 2026-10-14 18:20:34 +03:00
Brandon ThomasandClaude Opus 5.5 3b4279fc24 FreeBSD CI: three more flaky tests made deterministic
import_reads_files_off_the_ui_thread_and_can_be_cancelled: the probe was
a 150 ms sleep judged by a 300 ms wall-clock frame budget, which the
loaded FreeBSD QEMU VM can miss. It now waits on the sleeping-NAS gate of
tests_offline, which can also let a counted number of reads through:
frames must go on while every read is held (a watchdog records a frame
that waited), one batch is let through and joins the catalog, then
Cancel and wake: the files being read finish and nothing new starts.

compare_at_one_to_one_gets_a_sharp_window_per_pane ("both originals stay
decoded"): the media cache takes its budget from the process-wide memory
budget, which settings_dialog_by_keyboard_and_clicks sets to 1 GB for a
moment (and left there when it failed on FreeBSD). That leaves a 512 MiB
share, too small for two 24 MP originals (288 MB each), so one pane's
was evicted. compare_at now gives its session the default 768 MiB share;
LIGHTCRAFT_MEMORY_MB=1024 reproduces the failure without this.

dropping_the_lock_frees_the_library_while_another_thread_spawns_processes:
"the spawning thread never got a child off the ground" when a spawn took
longer than the 2 s of lock cycles. The cycles now go on until at least
5 children were spawned during them (at most a minute).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:36:53 -04:00
dev-null-undefinedandClaude Opus 5.5 b52762797f LR-MERGE-HEADLESS: HDR for Each Bracket Set
Photo ▸ Photo Merge ▸ HDR for Each Bracket Set (`merge.hdrBatch`) merges many brackets in one go with the last HDR
options, as Lightroom merges a selection of stacks: one HDR per stack among the photos, then per bracket among the
unstacked ones (`Catalog::bracket_sets`: photos taken less than about a second after the previous one's shutter
closed, at exposures at least ½ EV apart; bursts at one exposure and earlier merge results are left out). The app runs
the merges one after the other in the activity stack (cancelling one stops the rest; a failed set doesn't); the
command runs them inline for the CLI and MCP (`lightcraft-cli merge hdr-batch FILES…`) and reports merged and failed
sets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:37:13 +02:00
Andrew FrahnandClaude Opus 5.5 50cb797872 Merge upstream/main into pr/raw-erf
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 18:48:38 +11:00
Val EnfysandClaude Opus 5.5 5773533e23 LR-IMP-FORMATS: preview-only photos record the app version that decided it (#662)
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>
2026-10-14 08:19:17 +01:00
Andrew FrahnandClaude Opus 5.5 a3071fe0f3 Merge upstream/main into pr/raw-erf
Docs conflicts resolved on upstream's newer state (Leica Q3, Hasselblad Lunar);
ERF counts recomputed with docs/raw/coverage.py: 240 verified, 229 preview.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 07:55:22 +11:00
Brandon Thomas f478b000de Merge PR #1580 2026-10-13 11:51:51 -04:00
Brandon Thomas 2294329b0c Merge PR #1565
# Conflicts:
#	docs/parity-checklist.md
#	docs/whats-new.md
2026-10-13 11:49:15 -04:00
Brandon Thomas 7505017ea3 Merge PR #1432 2026-10-13 11:48:32 -04:00
Michael Anderson 271bc41842 Never crash: Repair damaged catalog snapshots on open
Fix ids, stacks, album folders and counters that don't agree, and stop album trees at 64 deep.
2026-10-13 10:46:47 -05:00
MarekSpodBiedry 51ce34d78c Reserve the name with an exclusive create when hard links are unavailable (#1503) 2026-10-13 16:36:09 +02:00
Claude 5cb57ef4ea LR-PROF-CAMERACOLOR: Canon EOS R7 / R6 Mark II calibrate measurements
Held-out and library-wide calibrate runs on real CR3s (calibrate's CR3 /
CR2 input is #1073): profiles change little (R7 2.52 -> 2.57, R6 Mark II
6.12 -> 6.09 mean dE), so none is bundled; notes the CRX adaptive QP
172/175 files that stay preview-only. The catalog test pins that an EOS
R6 Mark II (Exif "Canon EOS R6m2") edits in Kelvin through its measured
matrices and an EOS R7 stays relative.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzWyUn77ZWLe3q3qMiQbVt
2026-10-13 09:10:47 +00:00
Claudio 0a565d308a Recently Deleted shows deleted Local (browsed) photos (Fixes #1430)
A photo browsed in a Local folder is flagged deleted by photo.delete, but
Filter::matches_in dropped every browsed photo from views without a folder,
so it vanished from the folder and never reached Recently Deleted: it could
not be restored and sat in the catalog invisibly (a deleted record is also
never forgotten as an untouched Local one).

Deleted browsed photos now match the Recently Deleted filter, so they can be
restored; the library view still excludes browsed photos.
2026-10-12 23:15:38 -06:00
bflatastic 963d4f327a Merge PR #1114 2026-10-13 11:05:45 +07:00
bflatastic 8df468bfc6 Merge PR #1246
# Conflicts:
#	crates/pipeline/src/spots.rs
#	docs/parity-checklist.md
2026-10-13 11:05:44 +07:00
bflatastic 14b111f6f5 Merge PR #1344
# Conflicts:
#	docs/parity-checklist.md
#	docs/whats-new.md
2026-10-13 11:01:49 +07:00
bflatastic ac19325c57 Merge PR #978 2026-10-13 11:01:01 +07:00
bflatastic 035c378758 Merge PR #1052 2026-10-13 10:39:12 +07:00
PiMinusMaxandClaude Opus 5.5 d9006c96f6 LR-EDIT-COLOR-TEMP: Kelvin white balance from Sony's white-balance presets
Sony ARWs of cameras without measured colour matrices were edited on the
relative scale (6500 K / 0 = as shot). Their SR2SubIFD stores the
multipliers of the camera's presets at 2500, 3200, 4500, 6000 and 8500 K;
lightcraft_raw::kelvin_presets reads the as-shot multipliers on the curve
through them and they take the spectral matrices' place in the #730
design (as-shot Kelvin and tint only; look, Bradford adaptation and the
pixel-identical legacy upgrade unchanged).

Covered: ILCA-99M2, ILCA-77M2, SLT-A33, DSC-RX10, ILCE-3500, ILCE-7S.
Kelvin-dial shots read back 3400 -> 3396, 5200 -> 5193, 4700 -> 4708,
5600 -> 5594 K; Shade 7493 K. Off-curve constants (isotherm tilt 0.74,
tint spacing 1) measured from the 14 Sony bodies with both spectral
matrices and presets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:37:22 +02:00
Nathaniel LedfordandClaude Opus 5.5 f0862221c1 Issue #989: format the lock test (the shorter command fits on one line)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:48:35 -04:00
Nathaniel LedfordandClaude Opus 5.5 ef3e50bb2d Issue #989: the lock test finds true on the PATH (macOS has no /bin/true)
The spawning thread ran /bin/true, which is /usr/bin/true on macOS, so
it never spawned a child and the test failed there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:48:34 -04:00
Andrew FrahnandClaude Opus 5.5 139e9633cf LRC-DEV-HISTORY: History cursor — going to a step adds no step
Go to History Step (and new Previous/Next History Step commands) loads the
step's settings and moves a per-photo cursor (`Photo.history_cursor`,
`Op::SetHistoryCursor`, catalog format v8; older catalogs open at the latest
step) instead of appending "History: …". Later steps stay listed, greyed in
the History panel, and can be returned to; a new edit made behind the end
drops them first. Each move is one undo step; undoing an edit restores the
dropped steps and the cursor. `history.list` returns `cursor`.

Also: spots.rs uses as_chunks (clippy chunks_exact_to_as_chunks).

Sources: own design (Lightroom Classic / Photoshop History panel behaviour, observed).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 00:58:05 +11:00
Matthew James SivakandClaude Opus 5.5 6797c6b483 fix: share the sidebar's off-screen row skip, keep widths stable (#1011)
Review follow-up on the keyword sidebar fix:
- The skip is a shared helper, `out_of_sight`, that Keywords and Folders
  call before a row's work; a row out of sight still takes its room, is
  listed for the control channel (agents and tests find it as before) and
  asks for its measured width, so the content no longer widens and
  narrows as rows scroll by.
- The scroll to a keyword made from the header is a per-frame request
  for any row id (`SCROLL_TO_ROW`), set before the sections are drawn and
  cleared after: no frame stamp, nothing left behind.
- keyword_rows/folder_rows are skip-or-draw then recurse (the drawn row
  is keyword_row/folder_row); leaves no longer look up an open state.
- keyword_tree builds children lists as levels are made and sorts by the
  lower-case name it already computed; empty_hint uses ROW_HEIGHT; the
  test reuses tests_panels::scroll_sidebar.

58,000 photos / 4,000 keywords, release: 1.6 ms per unchanged frame
(7.4 ms before the fix), keyword_tree 14 ms (156 ms).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:11:02 +02:00
Matthew James SivakandClaude Opus 5.5 aa7e0bbc34 fix: left panel slow with a large keyword list (#1011)
Cause: the sidebar's Keywords section drew every keyword row on every
frame (layout, text measuring, widget registration, hover text, context
menu), even the thousands out of sight, and every catalog edit rebuilt
the keyword tree from all photos with several String allocations per
keyword level. On a 58,000-photo library with 4,000 keywords an
unchanged frame took 7.4 ms in the sidebar alone and the frame after a
rating 220 ms (156 ms of it the tree), release build on Apple Silicon.

Fix:
- Keywords rows out of sight only take their height (open children are
  still walked, so the layout is unchanged), as the grid already does
  (#35). A keyword made from the header's + is always drawn and the
  sidebar scrolls to it, so it is in sight even at the end of a long list.
- Catalog::keyword_tree works out each distinct keyword string's levels
  once and then only counts (a per-level "last photo" stamp replaces
  the per-photo HashSet of Strings).

Now: 0.55 ms per unchanged frame (7 rows drawn instead of 4,000),
keyword_tree 17 ms, the frame after a rating 44 ms.

Test: tests_keyword_sidebar::keywords_section_draws_only_the_rows_near_
the_screen (failed before: "2000 of 2000 keyword rows drawn"; now the
rows near the screen, and scrolling reaches the last keyword), an
ignored timing probe at the report's library size, a catalog test for a
keyword repeated on one photo, and the_keywords_header_creates_a_keyword
now checks the new keyword is in sight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:54:42 +02:00
Andrew FrahnandClaude Opus 5.5 a6d048be8d LR-IMP-FORMATS: decode Epson ERF (12-bit 16-byte blocks; WB from the preview's neutrals)
Sources: TIFF 6.0 and own black-box analysis of the three CC0 samples;
white-balance estimate from the embedded preview: own design.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 05:43:30 +11:00
OneSlipsandClaude Opus 5.5 6c79be9130 Windows: desktop entry paths, toolchain lints
desktop_exec treats a freedesktop Exec path as absolute when it starts
with '/' on every platform (Path::is_absolute wants a drive letter on
Windows); fill_box uses as_chunks (clippy::chunks_exact_to_as_chunks);
LibraryLock::descriptor_copy is cfg(all(test, unix)) like its only test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 02:32:08 +10:00
Fabio LussiandClaude Opus 5.5 15e308c3eb Sort ties by file name, then id
Photos that tie on the sort key (equal ratings, one import batch, a
burst within a second) now sort by file name before id, in
Catalog::query, Recently Added and auto-stack. Split out of #641.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:26:13 +02:00
Claude a0e422ce4a Merge PR #752
# Conflicts:
#	crates/engine/src/cmd/develop.rs
#	crates/engine/src/files.rs
#	docs/mcp.md
#	docs/parity-checklist.md
#	docs/whats-new.md
2026-10-11 12:02:46 +00:00
Claude 8339028a80 Merge PR #794
# Conflicts:
#	crates/engine/src/media.rs
#	crates/pipeline/src/optics.rs
#	docs/parity-checklist.md
2026-10-11 12:00:57 +00:00
Claude 8d319563ca Merge PR #710
# Conflicts:
#	docs/parity-checklist.md
#	docs/whats-new.md
2026-10-11 11:48:05 +00:00
Claude ae2d936c4d Merge PR #679 2026-10-11 11:47:27 +00:00
skylar taylor-barrick 9bfabc98b1 UI.9: integrate shared docking and preserve panel interactions 2026-10-10 16:22:21 -06:00
radialmonster 0041aef955 LR-EDIT-OPTICS-PROFILE: apply DNG WarpRectilinear2 lens corrections (Pixel DNGs, #635) 2026-10-10 14:21:13 -04:00
Jason Baldwin 76e633c9e1 LR-EDIT-COLOR-TEMP: Kelvin white balance for raws whose camera has colour matrices (#730)
Raws without matrices in the file (ARW, NEF, RW2, RAF, CR2, CR3, PEF, SRW,
ORF) were all edited on the relative white-balance scale (6500 K / 0 = as
shot), even when lightcraft_raw::spectral has measured matrices for the
camera. Those 52 bodies (ILCE-7M3, ILCE-7M4, D850, EOS 5D Mark III...) now
edit in Kelvin like a DNG and like Lightroom.

Design:
- One decision on both sides: spectral::find_camera("Make Model") backs
  catalog::relative_wb_camera (Photo::relative_wb); files::WbScaleOf decides
  the same from the decoded file. A corpus test asserts they agree.
  Photo::fitted_look keeps the old predicate for smart previews.
- The matrices only convert: As Shot is the file's multipliers read through
  the model's ColorMatrix1/2 (spectral::as_shot_white_through). The look
  (JPEG fit / profile / spectral fallback) is unchanged, and the slider
  adapts from that white with Bradford. The camera-space rebalance stays
  reserved for files with their own matrices.
- Presets resolve to their Kelvin values (WbMode::preset, sources cited).
- The Color panel shows the white balance in effect (effective_wb).
- XMP: crs::Target::for_file takes the camera; matrix cameras read
  crs:Temperature as Kelvin (marked "scale": "kelvin"), others keep the
  #510 as-shot shift.

Migration (no look changes):
- WhiteBalance.scale: WbScale { Legacy (absent in JSON), Kelvin }. A
  white balance without a scale keeps its old meaning. On a spectral-Kelvin
  raw (SourceInfo.legacy_relative_wb) the pipeline composes the old
  adaptation (6500 -> value) with the new one (as shot -> ...) in Bradford
  cone space (local::legacy_white). Von Kries scalings compose exactly, so
  the matrix matches to f32 and the pixels are identical
  (pipeline legacy_relative_white_balance_renders_as_before_on_a_kelvin_raw,
  media old_relative_white_balance_on_a_kelvin_camera_keeps_its_look...).
- Every white-balance writer (develop.set/adjust/quickAdjust/wb/wbPick/
  reset) first runs local::normalize_wb: a legacy value becomes the Custom
  Kelvin value with the same look and is marked Kelvin. Partials without a
  scale (old presets, develop.merge) stay legacy.
- Catalog format 7: Op::SetAsShotWb replaces the 6500/0 as-shot recorded
  by older imports when the file is next decoded or reloaded (system op,
  not an undo step). Format-version test updated.
- The web wire carries legacy_relative_wb.

Docs: parity (LR-EDIT-COLOR-TEMP), camera-preview-colour, xmp-interop,
lightroom-catalog-import, control-protocol, mcp, whats-new.
2026-10-10 16:09:31 +02:00
Jason Baldwin 8166185a0f LR-LIB-LOCK: release the library lock explicitly on drop (#680)
LibraryLock freed the library by letting its File close. On Unix the
flock lock belongs to the open file description and lasts while any
descriptor on it is open. When another thread spawns a process,
posix_spawn copies the whole descriptor table into the child and
O_CLOEXEC only closes the copies at execve, so a LibraryLock dropped in
that window stayed locked until the child exec'd. Re-acquiring the
library then failed with LockError::InUse(None) ("already open in
another LightCraft program") although nobody had it open. That is the
flake in tests_lock::second_opener_is_refused_until_the_first_lets_go,
whose neighbour lock_held_by_another_process_is_released_when_it_dies
spawns the test binary on a parallel libtest thread.

Drop now removes the owner note while still holding the lock (so it
can't delete the next holder's), then calls File::unlock(): LOCK_UN
through our descriptor frees the lock whatever copies a child still
holds. An unlock error is logged, never a panic; the handle still closes
right after, which releases the lock once every copy is gone, as before.

Tests: the_library_is_free_once_dropped_even_if_a_child_holds_a_copy
holds a try_clone of the lock's descriptor across the drop (the same
open file description a child gets between fork and exec) and
re-acquires; dropping_the_lock_frees_the_library_while_another_thread_
spawns_processes runs a thread spawning /bin/true against two seconds of
acquire/drop cycles. Both fail every run without the fix (the first
deterministically) and pass with it; the flaky pair went from 3/100 to
0/100 failed runs here. Both are cfg(unix): Windows handles aren't
inherited by CreateProcess.
2026-10-10 12:36:16 +02:00
Alex ChambersandClaude Opus 5.5 04f8146d10 Issue #99: lock tests: keep the child-process test from overlapping the others
tests_lock::second_opener_is_refused_until_the_first_lets_go failed now and
then on macOS with InUse(None): the child process that
lock_held_by_another_process_is_released_when_it_dies spawns briefly holds
copies of the test process's file descriptors, lock files included, until its
exec closes them, so a lock dropped in that moment stays held. The three tests
that hold a lock in the test process now take one mutex. Under parallel load:
6 of 400 runs failed before, 0 of 400 after. Test-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 09:59:22 +02:00
Brandon Thomas b785023073 Merge PR #597
# Conflicts:
#	crates/catalog/src/journal.rs
#	crates/catalog/src/lib.rs
#	crates/catalog/src/local.rs
#	crates/catalog/src/tests_format_version.rs
#	crates/catalog/src/tests_journal.rs
#	crates/ui-egui/src/panels/left.rs
#	docs/whats-new.md
2026-10-09 20:56:33 -04:00
Brandon Thomas 2421f2223b Merge PR #592
# Conflicts:
#	crates/ui-egui/src/tests_panels.rs
#	docs/whats-new.md
2026-10-09 20:55:50 -04:00
Brandon Thomas 4372750986 Merge PR #585
# Conflicts:
#	docs/whats-new.md
2026-10-09 20:55:49 -04:00
Brandon Thomas d0f042c269 Merge PR #578
# Conflicts:
#	crates/catalog/src/journal.rs
#	crates/catalog/src/tests_format_version.rs
#	crates/ui-egui/locales/de.json
#	crates/ui-egui/locales/es.json
#	crates/ui-egui/locales/ja.json
#	crates/ui-egui/locales/pt-br.json
#	crates/ui-egui/locales/ru.json
#	crates/ui-egui/locales/zh-hans.json
#	crates/ui-egui/locales/zh-hant.json
#	docs/whats-new.md
2026-10-09 20:55:40 -04:00
Brandon Thomas 244d279091 Merge PR #536
# Conflicts:
#	crates/catalog/src/tests.rs
#	crates/ui-egui/locales/de.json
#	crates/ui-egui/locales/es.json
#	crates/ui-egui/locales/ja.json
#	crates/ui-egui/locales/pt-br.json
#	crates/ui-egui/locales/ru.json
#	crates/ui-egui/locales/zh-hans.json
#	crates/ui-egui/locales/zh-hant.json
#	crates/ui-egui/src/lib.rs
#	docs/parity.md
2026-10-09 20:54:53 -04:00
Brandon Thomas f4f7d5c401 Merge PR #616
# Conflicts:
#	crates/catalog/src/model.rs
#	crates/engine/src/media.rs
#	docs/parity.md
2026-10-09 20:51:49 -04:00
Brandon Thomas 6f323e01d9 Merge PR #533
# Conflicts:
#	docs/parity.md
2026-10-09 20:50:41 -04:00
Brandon Thomas 6fcc1e5e28 Merge PR #571 2026-10-09 20:36:41 -04:00
Exalted5373andClaude Opus 5.5 c21c305e55 LR-PROF-CAMERACOLOR: Olympus ORF starting look fitted to its own JPEG
The ORF decoder supplies the as-shot white balance (ImageProcessing
WB_RBLevels) and no colour matrix, like ARW, NEF, RW2, RAF, CR2/CR3, PEF
and SRW, but ORF was left out of camera_preview::file_local_look and the
catalog's relative_wb, so it rendered with the neutral fallback matrix:
flat, grey and dark. It now gets the same guarded fit to its camera JPEG
and white balance relative to the as-shot look. Corpus (held-out RMS
against the fallback, all accepted): E-1 0.126 -> 0.014, E-400 0.087 ->
0.022, XZ-2 0.061 -> 0.023; compressed ORFs still open from their preview.

Against Lightroom Classic (CC0 E-1 kit, 2000 px, ΔE00): unedited render
11.25 -> 8.30; mean over 86 variants abs 12.10 -> 9.48, effect 3.44 ->
2.80. Every other kit's unedited render is byte-identical.
RENDER_CACHE_VERSION 23.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:04:18 -07:00