* LR-PROF-IMPORT: import the user's own DNG camera profiles (.dcp)
profile.import (File > Import Profiles & Presets, drag & drop) now also
reads DNG Camera Profiles: the user's own .dcp files, e.g. made with a
colour-checker tool. They appear in the profile browser and menu under
Camera Profiles with dcp:<camera>-<file> ids; profiles.camera lists them
and profile.deleteCamera / profile.deleteImported remove them. A library
on disk keeps a copy in Profiles/. No profile ships with the app and the
repository holds none: tests build theirs in memory (write_dcp).
lightcraft_raw::dcp parses the file per DNG spec ch. 6 (magic RC in
place of 42; IFD 0 read through the existing DNG colour builders) and
DcpProfile::applied_to puts its matrices, illuminants and look in place
of a raw file's, keeping the shot's white balance, analog balance and
baseline exposure.
Choosing one (develop.profile) replaces the raw file's colour model when
it is decoded, before the starting colour and the white-balance scale
(WbScaleOf) are worked out, so the matrix path takes over from the fitted
JPEG look (CR3/CR2/ARW...) and white balance follows the profile. A
profile without a tone curve of its own (most chart-made ones) keeps the
photo's contrast: the DNG's tone curve, else the tone of the camera-JPEG
fit. The profile travels with SourceRef::File into FileLoader /
PairLoader (new Option<&DcpProfile> argument); a decoded source records
the profile it was made with and is not reused for another. Smart
previews and the web render worker keep the file's own colour; rendered
(non-raw) photos ignore it.
This reverses the documented "Adobe profile formats deliberately
unsupported" stance for user-supplied files only; docs updated.
Split out of #1012.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: build against upstream main (kelvin_presets, 3-arg loaders)
DcpProfile::applied_to keeps the file's white-balance presets (new
ColorData::kelvin_presets upstream): they are camera facts like as-shot
neutral. Upstream's new FileLoader test closures take the profile argument.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: hover previews and browser thumbnails develop the hovered camera profile
The loupe's hover preview (preview_job) and the profile browser's thumbnails (variant_job) took
the photo's own decoded source, so hovering an imported camera profile showed the built-in colour
(and hovering a built-in profile while one was applied kept the camera profile's). Both now decode
as the hovered settings develop: the source follows the settings' camera profile, as the main
render does, and a variant's cache key already follows its settings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: build against upstream main (face-tile test loader and render_bench take the camera profile)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CI: the catalog's staging-link test module is Unix-only (unused import broke clippy on Windows)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CI: same-folder export test expects the '/'-joined subfolder (failed on Windows)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CI: folder-drop tests build widget ids with the Folders tree's '/' paths (failed on Windows)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: rustfmt after merging main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: a real D750 camera profile as a pinned test corpus, checked against Lightroom
dieli's CC0 Nikon D750 ColorChecker set (Calibrite PROFILER dual-illuminant DCP,
matrix-only; the two chart NEFs; Lightroom Classic 15.6 renders with the profile)
is fetched by cargo xtask corpus --download into corpus/d750-colorchecker/, pinned
to commit f37f4054 and by sha256; nothing is committed.
Tests (skip without the corpus): the profile parses as matrix-only with
Cool White Fluorescent + D55; the matrices blend by inverse temperature; the
matrix-only path inverts the colour matrix and adapts to D50; it takes over the
chart NEFs' colour; imported and applied end to end, the charts develop within
mean dE2000 3.84 (flash) / 3.35 (indoor) of Lightroom's renders.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: a single .dcp file is a camera profile only, not also an unreadable LUT
profile.import read every plain file that was neither a folder nor a .zip as a .cube LUT, so importing
one .dcp imported the camera profile and also reported it as failed ("stream did not contain valid
UTF-8"). Only .cube files are read as LUTs now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: review fixes for imported camera profiles
- A library moved or restored elsewhere finds its camera profiles in its own Profiles/ folder.
- A grid thumbnail is cut from the decoded preview only when that preview used the same camera profile.
- Render and source cache keys carry a fingerprint of the profile file, so a deleted or replaced profile never
has its old renders stand in.
- Camera profiles can be favourites, and favourites/recent entries survive reopening the library.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: smart previews without the camera profile, current-profile source facts, unsupported files reported
CodeRabbit review of 4ef025d9:
- Build Smart Previews develops the proxy without the photo's imported camera profile and never takes a cached
source decoded with it (the denoised picture is kept); before, only the stale-proxy refresh left it out.
- Session::source_info reports decoder facts only from a cached source developed with the camera profile the
photo uses now, else the catalog's.
- profile.import reports a file that is neither .cube, .dcp nor .zip as failed instead of passing over it.
Each with a regression test in tests_dcp.rs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: the Before side decodes with its own camera profile; a failed profile copy fails that file only
- build_job (and region_job_before through it) picks the Before render's source with the Before
settings, so it never reuses or decodes a source developed with the edit's camera profile.
- profile.import: a .dcp whose copy into the library fails is reported in `failed`; the profiles
imported with it stay imported and are saved, and nothing half-written is left.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: a cached smart preview stands in while the original is offline
A smart preview carries no camera profile, so with an imported profile the profile check
rejected the cached proxy and every render decoded it again (it gives the same picture). It
now stands in, for its own level and for the thumbnail cut from it, while the original is
known to be offline; otherwise the profile check is as before, so once the original is back
the photo is decoded from it with its profile. Photos without an imported profile keep main's
behaviour. Test `tests_dcp::a_cached_smart_preview_stands_in_while_the_original_is_offline`
(fails without the fix).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: deleting a camera profile removes the library's copy before forgetting it
profile.deleteCamera now removes the library's copy of the profile file
first. A copy that is already gone is fine; any other error (file in use,
no access) fails the command and keeps the profile listed, so the library
never holds a profile file it no longer lists. Test:
a_profile_whose_copy_cannot_be_deleted_stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: deleting a camera profile never follows `..` out of the profiles folder
A listed path with a `..` component can start with the profiles folder and
still lead out of it; such a path is not treated as the library's copy, so
deleting the profile leaves that file alone. Test:
deleting_a_profile_never_follows_dot_dot_out_of_the_profiles_folder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: both tone fits of a DNG see its lens opcodes; no side effect in the profile-tag comparison
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: a hover preview's key carries the camera profile file's fingerprint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: unusable camera profiles not offered; file's WB scale and the profile's tone kept
Review of #1063 (CodeRabbit, outside-diff findings):
- a camera profile whose file is missing or damaged when the library opens
is no longer offered in the profile browser or accepted by develop.profile
(it silently developed with the raw's own colour); the entry stays, so it is
offered again once the file is back.
- a raw on the relative white-balance scale stays relative with a camera
profile (its matrices turned it to Kelvin, so existing edits were read as
absolute temperatures and the catalog and quick previews disagreed).
- with a camera profile its tone curve, then the tone kept for a curve-less
profile, come before the fitted camera look's tone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: one Camera Profiles group; the web loader keeps a raw's own colour
Review of #1063 (CodeRabbit):
- a LUT group named "Camera Profiles" that didn't sort last made the profile
menu list that group twice; it is now one group, last, with both kinds.
- without web workers a render ran inline and the web loader applied the
selected camera profile, unlike the worker (the documented web behaviour
keeps the raw's own colour); the loader now leaves it out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: camera profile files are read at most to the 64 MiB limit
A .dcp larger than the parser accepts was read whole into memory before
being refused. One bounded read (lightcraft_raw::dcp::read_dcp) stops one
byte past the limit and is used for import, reopening a library and the
duplicate check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: a test that a hostile hue/saturation map header in a camera profile drops the map (FredApps, #1063)
Dimensions far past the data, past u32 products or zero, an entry count of 4 billion values and a
data offset past the end each drop the map while the profile's matrix still reads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Docs: parity checklist keeps both sides of the merge
* LR-PROF-IMPORT: tests added on main take the file loader's camera-profile argument
* Merge main; a thumbnail test takes the file loader's camera-profile argument
* Docs: one camera colour row, with main's notes and the import note
* Process key tests take the profile salt argument
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
a_failed_write_is_held_reported_and_retried raced the log thread: it
could fail the first append before the second arrived, which is then
refused by design. The test now hands both over in one append.
a_file_path_rule_allocates_nothing_per_photo counted heap blocks on every
thread, so the test harness's own thread could fail it (four blocks, on
PRs that don't touch this code). An allocation per photo is thousands, so
the bound stays far below that.
read_texture waited 10 s where the render path waits 60 s, so a view
could be dropped from a headless screenshot while earlier work ran.
The GPU guide now says what the code does about the window's card.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* LR-BEHAV-RENDER-FIDELITY: split V3 Contrast and Blacks
Port calibrated global Contrast and Blacks with shared CPU and GPU tables.
Preserve V2 Whites, local adjustments, other source paths, and all V1 and V2
renders and persistent cache keys.
Generate the constants from public Contrast and Blacks fits. Cover numeric
references, legacy renders, keys that include the resolved process, windows
and GPU parity. Document the measurements, limits and provisional number.
Co-Authored-By: Codex <noreply@openai.com>
* LR-BEHAV-RENDER-FIDELITY: reduce V3 preview cost and sanitize fits
Compose calibrated CPU preview operators over linear input and skip unused
mix endpoints. Keep high-bit-depth exports on the original operator path.
Require three distinct photos per published error group and band, regenerate
the fit hashes, and cover the sanitizer with synthetic tests. Document release
measurements and identify the separate measurement tool proposed in #1181.
Co-Authored-By: Codex <noreply@openai.com>
* LR-BEHAV-RENDER-FIDELITY: preserve V3 luminance evaluation
Retain the original operator evaluation for luminance mixes, including
preceding Contrast, so preview interpolation cannot change the cutoff.
Keep the fused per-channel path and exact deep exports.
Add CPU and GPU regressions for both uniform cutoff fixtures. Document
the measured CPU cost of positive Blacks and the retained fused path.
Co-Authored-By: Codex <noreply@openai.com>
* LR-BEHAV-RENDER-FIDELITY: finish split-module integration
Import the persistent process-key helper directly in the session module. Keep the existing V3 operator selection and evaluation order in an inline helper so the combined finish-stage closure stays within the complexity limit.
Refresh the V1/V2 golden hashes only after the exact fixture produces identical hashes on plain current main. Keep the cross-platform legacy samples unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-BEHAV-RENDER-FIDELITY: date the calibrated Light gap summary
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* LRC-DEV-AUTOSYNC: Auto Sync carries white balance as a mode or on each photo's own scale: As Shot and Auto per photo, a Temp move keeps each photo's tint (N-A3, note 18)
Auto Sync already sets every other setting to the active photo's number.
White balance is the exception: main stores the resolved Kelvin and tint
with the mode, and Auto Sync copied those numbers. As Shot then gave every
selected photo the active photo's camera white, Auto gave them the active
photo's Auto white, a Temp move from As Shot gave them the active photo's
tint too, and a JPEG beside a raw got the raw's Kelvin read on its own scale.
Now (sync_same.rs) the mode goes on each photo and each applies it to
itself: As Shot its own camera white, Auto its own white worked out on its
own pixels, a light preset its numbers. A typed or dragged Temp or Tint
sets the same Kelvin on every raw (an old relative-scale value converted
first) and only the slider that moved, and a photo on another scale moves
by as much as the active one did. One undo step, as before.
Tests (tests/autosync_wb.rs): As Shot / Tungsten / Auto per photo with
three different as-shot whites and undo; a typed and a dragged Temp keep
each photo's tint; the old relative scale; a JPEG beside a raw. Three of
the four fail on main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LRC-DEV-AUTOSYNC: Auto's per-photo pixels are read in parallel; the row says which sync does what
Review of #3571:
- When Auto is synced, the other selected photos' thumbnails that aren't
in memory are now decoded in parallel (one per core), not one after
another on the UI thread.
- The checklist row says the per-photo white balance belongs to Auto
Sync, while Sync Settings copies the active photo's stored one.
- The row and the test notes now say a Temp is the same number only on a
photo of the same scale, and that other photos move by as much as the
active photo did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LRC-DEV-AUTOSYNC: Auto's per-photo pixels are read a core's worth at a time, each with its decoder's facts
Review of #3571:
- A large selection no longer holds every decoded thumbnail at once. They
are decoded in batches of one per core (at most 16), and each batch's
white balance is worked out before the next is decoded.
- Each photo's Auto uses the facts its own decoder gave, read right after
its source is cached, so a photo pushed out of the cache by later ones
never falls back to catalog metadata.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LRC-DEV-AUTOSYNC: a light preset with a tint of its own on a JPEG carries that tint; Auto's facts are read before the cache can evict them (review of #3585)
- On the relative scale the stored numbers are what renders, so a preset
the active photo overrides (`develop.wb {mode, tint}`) travels as its
numbers, like Custom, not as the preset's tint.
- `sources_now` reads each photo's decoder facts before storing the
decoded source: the cache's budget may evict that entry at once, and
the header facts alone lack the camera colour Auto uses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* PERF-STEP-STALL: stepping and flagging never wait for the drive; the catalog log is written and synced by its own thread, and nothing handed to it is ever dropped
On Windows a write to the catalog log waited for a sync of the same file in
flight, and each flag, rating or step synced on the UI thread, so culling on
a busy drive froze the window. FsStore now hands log appends to a thread
that writes and syncs them in order, batching what arrives during a sync.
No change is lost: the thread keeps every byte it has not yet synced. A
write or sync that fails is held and retried (cutting any fragment first),
later appends are refused with the reason (the engine keeps them queued, as
for any failed save), the held records count as unsaved so the quit prompt
warns, and a snapshot is made due. Cuts, replaces and reads act on the
logical log (synced bytes plus held ones), so the log never has a gap.
A clean quit's closing snapshot holds everything; dropping the store waits
for the thread.
Tests drive a slow drive (a sync hook that sleeps) and a failing one
through test-only fault hooks (doc-hidden, keyed by library folder): with 300 ms syncs the slowest flag is
0.2 ms (305 ms with the log written on the caller, as before); every change
made during slow saves reopens after a flush, a drop and a close; a failed
write is reported, retried and lands with no gap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-STEP-STALL: review fixes: a reopen reads what the log thread was handed, the quit check reads the held status now, a covering snapshot clears it, a cut to the synced length reaches the file
- open_stores flushes the old journal's log thread before the same
library's files are read again (Settings, Libraries; a reopen).
- Session::refresh_unsaved, called by the quit check, reads what the log
thread holds at that moment, not as of the last frame.
- Journal::unwritten is None once a snapshot holds every op (the closing
snapshot after a failed log reset no longer makes the quit prompt say
changes would be lost); wants_snapshot follows it.
- A cut to exactly the synced length with bytes held cuts the file, so
bytes a failed sync left there never come back on the next open.
- A caller that waits over a minute for the drive gets a timed-out error,
not "the writer stopped".
Each fix has a test that fails without it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-STEP-STALL: a reopen and the quit check wait for what the log thread was handed
Review of #3557:
- Opening the same library again while its log thread can't write what
it was handed now fails and keeps the open library (with the held
change, retried). Before, the new catalog was read from files missing
it.
- The quit check flushes the log thread first instead of reading only
the failure it had already reported. A change accepted but not yet
written, then failed, is saved by the closing snapshot before the
window may close; if that fails too, the unsaved prompt shows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PERF-STEP-STALL: a reopen goes ahead when the snapshot already holds every change
Review of #3557: the reopen was refused whenever the old log's flush
failed. When a completed snapshot covers every op, nothing is missing
from the files, so the open now goes ahead and only logs the error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* LR-EDIT-DETAIL-SHARPEN: Radius and Detail work, and sharpening acts at pixel scale (#757)
* LR-EDIT-DETAIL-SHARPEN: an enlarging export keeps every Radius step (#757)
The sharpening blur's sigma was capped at 4 px, so a 2x export rendered
Radius 2 and 3 alike (both scaled past 4 px). The cap is now the largest
sigma the Gaussian taps support, GAUSS_SIGMA_MAX (64), which the CPU and
GPU blurs share. Test: enlarged_export_keeps_radius_steps_apart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ti8KemZbjE2ZgHgV1q1BM
* LR-EDIT-DETAIL-SHARPEN: preserve process-aware previews after rebase (#757)
Use the V3 pixel-scale blur and Masking weight for the Radius, Detail and
Masking previews on both renderers. Preserve the legacy texture-band previews
for V1/V2 and compute the same Gaussian when sharpening is disabled.
Cover process selection, radius changes, and prepared/fresh plane equivalence.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ti8KemZbjE2ZgHgV1q1BM
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* GPU-FULL-1: LUT profiles, Refine Edges and soft proofs render on the GPU
These three sent the whole loupe / compare / export render back to the CPU
pipeline (media::develop and lightcraft_gpu::render declined them).
- LUT profiles: the .cube table rides in the finish kernel's aux buffer
(LUT_* parameters); lut_apply mirrors Lut3d::apply (trilinear, domain),
applied after grain and blended by the profile amount, as on the CPU.
- Refine Edges: the cross guided filter Auto Mask already ran on the device,
now with a combine mode (max for strokes, mix by sqrt(refine) for Refine),
applied to the combined alpha before the mask's invert and amount.
- Soft proofing: proof-space gamut mapping and the destination / display
gamut warnings in the kernel (PROOF_* parameters, GAMUT_WARN and warning
colours generated from the CPU constants), for output spaces and displays.
Equivalence tests (RTX 5070, DX12): LUT profiles at three amounts and a
non-default domain, Refine 15/60/100 on two masks, proofs in 16 combinations
of space, output, display and warnings - all max 1 LSB, mean <= 0.0005 LSB.
The tests also check that strong refine and the destination warning change
the image (so they aren't comparing two unchanged renders).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU-FULL-2: spot removal, defringe and the histogram on the GPU
Spots and defringe made every render that had them (and every white
balance or spot edit) read the resampled image back, run the scene-linear
stage on the CPU and upload it again. Now only the ProRAW local tone map
(off by default) does.
- Defringe: OkLab pass, separable lightness min/max filter, per-pixel
apply. Parameters come from the new optics::defringe_params, which the
CPU stage now uses too.
- Spots: new spot module (spot_prep / spot_apply over each spot's box, the
Heal tone correction as device Gaussians), spot by spot in place so each
reads what the earlier ones left, as on the CPU. Remove's automatic
source: spot_score rates the 48 candidates on the device and only the
scores come back. Layout, candidates, spoke directions, radii and ranking
are shared helpers in pipeline::spots (spots::apply uses them; behaviour
unchanged, pipeline tests green).
- Histogram: counted with atomics on the device on Histogram::of_srgb8's
sampling grid (exact), instead of a CPU pass over the readback.
Tests: spots (clone, heal, auto remove incl. near the edge where no
candidate fits, brushed strokes, feather 0, cut by the edge, overlapping)
and defringe (purple/green, hue ranges, preview and full size) max 1 LSB;
the device histogram equals the CPU count of the returned image for small,
sampled (>1 MP) and windowed renders; the scoring kernel picks the CPU's
candidate. (The edge case caught a real bug: "doesn't fit" was written as a
decimal literal that rounds below f32::MAX.) Unit tests that create the
device now take turns with the crash-sentinel test, which swaps a global.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU-FULL-2: fix the spot kernels' build; GPU tests fail when kernels don't build
07d4680b's spot module declared `const F32_MAX = bitcast<f32>(...)`, which
naga can't evaluate as a constant: the kernels failed to build, the device
was unavailable, and the equivalence tests skipped as if there were no
adapter - so that commit's GPU tests didn't actually run. f32::MAX is now a
function, and the equivalence suite (and the device unit test) fail instead
of skipping when an adapter exists but the kernels don't build.
Re-verified on the RTX 5070 (DX12), all on the device: 21 equivalence tests
incl. the 8 spot cases (max 1 LSB), defringe, the exact device histogram,
and the auto-source unit test.
Adds an ignored thumbnail throughput bench (tests/thumbs_bench.rs).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU-FULL-3: thumbnails render on the GPU once the device is ready
Every cached job (grid and filmstrip thumbnails, preset and profile
previews, faces, quick stand-ins) was forced onto the CPU. They now use the
GPU when lightcraft_gpu::ready() - so they never block on device creation
at startup (1.8 s incl. kernels here) and render on the CPU until then.
Views and exports keep waiting for the device as before.
tests/thumbs_bench.rs (ignored): six workers rendering 256 px thumbnails of
512 px sources with preset-like settings (NR, sharpening, clarity, grading,
a gradient mask): RTX 5070 3.6 ms each vs 4.3-5.2 ms on 32 CPU threads,
with the cores left free for raw decoding.
Engine and UI suites: unchanged (only the 12 failures that predate this
branch on Windows: tests_sync paths, tests_model_setup).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU-FULL-4: the loupe's drafts are drawn straight from the GPU
Rendering and drawing now share one device: the desktop app creates the
window's wgpu device with the adapter's limits and hands it to the GPU
renderer (lightcraft_gpu::adopt_window_device, on a background thread at
startup, under the crash sentinel; a device that doesn't suit falls back to
one of the renderer's own, as before).
A drag's drafts of the views are RenderJob::on_screen: the render ends in an
Rgba8Unorm texture (lightcraft_gpu::render_display -> Output::Display, new
`present` kernels: unwritten-pixel count, row padding for the copy) and
egui-wgpu draws it as a native texture (ui-egui ScreenTextures, implemented
by the app) - no readback, no conversion to egui pixels, no re-upload. The
readback's checks run on the device (atomic unwritten count, device
histogram). Renders that need pixels keep the readback: the settled full
render (view cache, overlays, screenshots), thumbnails, exports, overlays,
windows that spots grew, a monitor profile.
- engine: RenderJob::on_screen, RenderResult::frame (ScreenFrame).
- ui-egui: Tex::native / Tex::id() (21 draw sites), Renderer
set_screen_textures, natives per slot (freed when pixels return),
ui.inspect perf.gpuWindowDevice / screenTextures / mainOnGpu.
- app: WindowTextures, window device limits, LIGHTCRAFT_GPU_WINDOW=0 to opt
out.
Tests: crates/gpu/tests/display.rs adopts a device in its own process and
checks the texture holds exactly the readback's pixels (padded and
unpadded widths, draft and full), the histogram matches, and overlays /
grown windows come back as pixels. In the app (RTX 5070, a 45 MP CR3 from
the user's library copy): all 90 drafts of a drag drawn from GPU textures,
1.69 ms vs 2.05 ms median per draft render with the readback; the frame
screenshot shows the image correctly; pixels return after the drag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU-FULL-2: the denoise runner tests take turns with the crash-sentinel tests
Building a denoise runner creates a device and compiles kernels under the
init marker. Running in parallel with backend::tests, it could arm the marker
the test had just set, so "is removed afterwards" failed now and then. The
runner tests now hold DEVICE_TEST_LOCK while they build, like the render tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU-FULL-4: fix the build after merging upstream main
Upstream's QuickJob panic guard builds a RenderResult (add `frame: None`),
its idle-trim test builds a Tex (add `native: None`), and its on_exit test
builds App (add the WindowTextures slot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU-FULL-2: upstream's blur tests take turns with the crash-sentinel tests
gaussian_matches_cpu and bench_kernels create the device, so they hold
DEVICE_TEST_LOCK like every other device test in lightcraft-gpu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tests: the same-folder export test compares paths separator-blind (Windows)
The written path is the original's folder (`\` on Windows) joined with `Exports/IMG_1.jpg`;
the expected one had `\Exports`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tests: the folder-drop tests find folder widgets separator-blind (Windows)
The folder tree spells its paths with `/`; the tests built widget ids from Windows paths with
`\`, so none was found and every test failed on Windows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tests: the Windows path fixes take the same form as in the other PR branches
So the branches merge into main without conflicts between their copies of the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU-FULL: headless screenshots read back GPU-drawn views; LIGHTCRAFT_GPU_LIMITS caps the adopted device
Review of PR #1065:
- A headless `ui.screenshot` taken mid-drag showed the photo as a white
rectangle: the GPU-resident draft left no CPU copy. With keep_pixels
(control channel) the renderer now keeps each view's frame, and
`cpu_textures` reads it back through `ScreenTextures::read`
(`lightcraft_gpu::read_texture`). Drawing still never reads back, so
drags keep the speed-up.
- Adopting the window's device skipped LIGHTCRAFT_GPU_LIMITS; `Gpu::adopt`
now caps its limits the same way `Gpu::new` does.
Tests: a_headless_screenshot_reads_back_a_view_drawn_from_the_gpu (ui-egui),
the_limits_override_caps_an_adopted_device (gpu); the display test uses
read_texture and skips through lightcraft_testkit::skip!.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU-FULL: review fixes (CodeRabbit 2026-10-21) and slideshow/print draw GPU-drawn views
- the window-device adoption is announced before its thread starts: until it finishes, the
settings' warm-up and renders don't create a device of their own (they raced it)
- the device error / device-lost handlers go on only after the kernels build, so a declined
window device keeps the window's own handler
- releasing a slot frees its GPU-resident texture and kept frame
- a soft proof into a colour space drafts full size on the GPU; only one to a device profile
counts as proofed for `handles`
- docs: Fill spots and device-profile soft proofs render on the CPU
- slideshow, slide layout and print draw `Tex::id()` (the GPU texture when there is one)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU-FULL: the display test skips when LIGHTCRAFT_GPU=0 switches the GPU off
It panicked ("GPU rendering is off") instead of skipping like the other GPU tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU-FULL: a view readback never panics on a lost device
read_texture (headless screenshots of GPU-drawn views) polled the device
directly. On a lost device wgpu 30 panics in Device::poll; it now goes
through the same guard as the renderer's own polls (poll_device): a lost
device returns an error, stops the GPU and is not polled again. Review of
PR #1065. Also quiets a browser-build dead-code warning on OnScreen::Frame.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU-FULL: an HDR edit's histogram shows the HDR range on the on-screen GPU path too
develop_on_screen returned the GPU's SDR histogram for both its Display and Image arms; it now
applies lightcraft_pipeline::hdr_histogram as develop_reporting does (None for drafts, windows,
proofs and non-HDR edits, which keep the GPU's histogram).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU-FULL: the Navigator draws the photo's texture as it is drawn in the loupe
The Navigator falls back to the main view's texture before the photo has a thumbnail. It drew
`t.tex`, which is stale (or the 1x1 placeholder) when the main view holds a GPU-resident draft;
it now draws `Tex::id()`, like every other draw site.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU-FULL: the egui 0.37 lifecycle test's texture names no native id
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU-FULL: rustfmt the lifecycle test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU-FULL: the second window fits the frame's size; freed GPU frames drop their views
The second window sized its image from the texture handle, which is only a
1x1 placeholder for a GPU-resident render; it now uses the frame's size.
When a GPU-resident render can't be shown and every native texture is
freed, the views pointing at them (Before, Region, Compare tiles) are
dropped too, so they render again as pixels instead of drawing a freed id.
Test: freeing_gpu_frames_drops_the_views_drawn_from_them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(gpu): LIGHTCRAFT_GPU_ADAPTER picks the card for rendering too
The variable chose the card only for AI Denoise and the face detector
(two copies of the same enumerate-and-match code); photo rendering
always took the first high-performance adapter, on a two-card machine
the one driving the screens.
One helper, backend::pick_adapter, now serves the render device
(Gpu::new), the denoise network and the face detector: the first
hardware card whose name contains the value (any case), else the
high-performance one. A name that matches no card is logged and falls
back instead of failing. backend::may_adopt_window_device says whether
rendering may share the window's device (only for the window's card).
Name matching is unit-tested without a GPU; the equivalence tests print
the card they ran on. Documented in docs/gpu-pipeline.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU-FULL: a view switching to a GPU texture releases its pixel image
Review of #1065 (CodeRabbit nitpick): when a slot that had pixels switched to
a GPU-resident texture, its egui texture kept the full-size image, never drawn
while the native texture is set. Tex::show_native now swaps it for a 1x1
placeholder on the switch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(gpu): LIGHTCRAFT_GPU_ADAPTER picks among several matches in a defined order and logs once
Review of #3428:
- several cards match: the one whose whole name it is, then dedicated before built-in before
virtual cards, then wgpu's order (backend::choose_named_adapter); the log names every match
and the one used;
- no match: the default choice, with a warning that lists the cards;
- a name matching only a software renderer: never used, and the warning says so;
- each line is logged once (backend::log_once), though the render device, AI Denoise and the
face models each ask;
- tests on fake adapter lists: a_fake_adapter_list_picks_in_a_defined_order,
a_choice_is_logged_once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU-FULL: the release note gives the GPU's documented bound against the CPU (3 levels, mean under half a level)
Review of PR #3544.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* GPU-FULL: the window's device isn't adopted when LIGHTCRAFT_GPU_ADAPTER names another card
With #3428 merged, Gpu::new already makes the render device on the named card; the window's
device was still adopted first, so the app rendered on the screen's card whatever the variable
said. backend::may_adopt_window_device (tested on names in backend.rs) now decides: another
card named renders on a device of its own, and the window stays on the screen's card.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Pressing HDR no longer swaps the layout in one frame. The SDR curve squeezes
into the HDR layout's SDR section as the HDR section and legend open, and
Visualize HDR and HDR Headroom slide open with it (LR-EDIT-HDR-MODE); turning
HDR off runs it back. An SDR edit never shows an HDR histogram.
The flow lives in its own module, panels/edit/hdr_flow.rs, with small hooks in
histogram.rs and sliders.rs.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* LR-EXP-WATERMARK: decomposed Korean and Vietnamese export text draws whole (#3480)
Export text (contact-sheet captions, watermarks, slideshow overlays, print
page text) laid each char out as its own glyph. macOS often stores Korean
file names in NFD, so a caption for 제주도.jpg drew as loose jamo, and NFD
Vietnamese letters lost their stacked marks (#3277).
TextLayout::new now composes the text to NFC first (unicode-normalization,
MIT OR Apache-2.0). Every Hangul syllable and Vietnamese letter has a
composed form; sequences without one draw as before.
* LR-EXP-WATERMARK: CJK compatibility ideographs keep their code points in export text (#3480)
NFC maps each CJK compatibility ideograph (U+F900-U+FAFF, U+2F800-U+2FA1F)
to its unified form, so a name spelled with 塚 (U+FA10) would draw as 塚
(U+585A). Compose the text around them and keep those characters as they
are.
* LR-EXP-WATERMARK: export text already in NFC stays as it is (#3480)
A navigation link with ?bench opened the first photo of the library in
browser storage and committed Exposure edits to it, finishing at 0 instead
of the photo's own value. ?bench now implies ?store=memory and loads the
generated demo photos, so the benchmark cannot edit stored photos.
* CRW-LIB: decode Canon CRW through the shared craft-crw crate
LightCraft decodes Canon CRW (the CIFF raws of the EOS D30, D60, 10D, 300D and the
PowerShot G1/G6) through craft-libs' `craft-crw`, the clean-room decoder derived and
verified on the `clean-room-crw-format-decode` branch and moved there with its frozen
tables and derivation record. `crates/raw/src/vendor/crw.rs` is now the adapter: it maps
the crate's header and samples onto `RawImage` (CFA, levels, active area, orientation,
as-shot white balance, the make/model/time records a CRW keeps instead of Exif) and
converts the capture time through chrono. The dependency is a local path
(`../craft-libs/crates/crw`) until the craft-libs branch merges; then it becomes a git
revision.
Everything LightCraft-side from that branch comes along unchanged: `probe_bytes` fills
metadata gaps from the decoder's records so CRW imports name their camera; the EOS D60
bundled camera profile and its chart-measured dual-illuminant matrices (`chart_table.rs`,
`spectral::Camera` carries its illuminant pair) give the D60 a Kelvin white-balance scale;
CRW joins the JPEG-fitted starting look, the catalog's raw formats and `lightcraft-cli
calibrate`; `RENDER_CACHE_VERSION` 71; the CC0 CRW corpus entries for `cargo xtask corpus`;
coverage, parity, gaps, raw-parity, target-app-parity and ROADMAP updates. The G5 stays
preview-only (the crate refuses it with the reason) and the robustness test keeps its CRW
sample.
What does not come along: the decoder, its tables and `crw_oracle.rs` (now craft-crw's
digest-pinned sample test) and the derivation record, which lives in craft-libs
`crates/crw/clean-room/`. `clean-room/crw/` here keeps only the EOS D60 chart-calibration
document and its capture list, with a README pointing at the record.
Gates: fmt, clippy -D warnings (raw all targets; engine, catalog, cli), lightcraft-raw tests
(475 + 134, the corpus files decoding through the adapter), catalog tests, engine lib tests
(1,300 pass; the two failures, `tests_import::recursive_discovery_…` and
`originals::tests::write_targets_pin_absent_paths_…`, are in files unchanged from main),
`cargo xtask parity`, `assets`, `layers`, `coverage.py --check`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* no message
* no message
* Update documentation after moving code to craft-libs
* Review coment fixes
* CRW: pin craft-crw f3b62ec, a truncated entropy stream is Corrupt
craft-libs #472 makes craft_crw::decode refuse a stream that ends more than
Limits::max_stream_end_fill (64 samples, one block) before the frame does,
instead of filling the rest with predictor values and returning success.
The adapter already maps craft_crw::Error::Corrupt to RawError::Corrupt;
a regression test proves a truncated CRW now fails at this boundary (it
decoded as a flat frame on the old pin) while its header still reads.
Only craft-crw moves; craft-ui/docpath/update/window stay on c5a1cbf.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Code review comment fix
* Add some tests
---------
Co-authored-by: Robert Woldberg <314159+rwoldberg@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Use the same exact-score known-crop preference as best_lens in the built-in fixed-lens fallback. Preserve higher-score priority and cover both profile orders through the public lookup API.
Validated with 33 lensdb and 5 engine lens-profile tests, plus full cargo xtask ci (8/8, Rust 1.95.0, Apple M5 Max Metal and WASM). The new tie regression fails against the previously published selector. No workflows or lockfile changes.
Co-authored-by: Codex <noreply@openai.com>
As in Lightroom Classic, a ⌘/Ctrl-click on a tile in Survey deselects that
photo (library.select, mode toggle), as the hover × does; a plain click still
only makes it active, and the last photo stays so the survey never empties.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge upstream main at 496524ced5 without rewriting published history. Preserve current upstream fixes and retain only the independent feature change in the resulting PR diff.
Co-authored-by: Codex <noreply@openai.com>
Mask coordinates lived on the corrected frame, so a Transform, Upright or lens-correction change
moved the photo under them and the masked look stayed behind, offset from the person it was drawn
on (4.3 px on a 240 px render under Vertical 25 + Rotate 4: about 100 px at 6000 px). Rotate Left /
Right turned the crop but not the masks.
A mask's coordinates are now on the photo before the warp (Mask::on_photo, catalog format 10):
the CPU render, the GPU render, Auto Mask brush windows and the on-photo tools map them through
the warp (Frame::for_mask, Warp::from_source). Rotate Left / Right turns masks with the photo
(mask_space::turn). The AI models see the photo without lens corrections, Upright, Transform,
crop or flips, so stored Object / Prompt selections stay on their content and changing those
doesn't ask the model again. Older masks (no on_photo) render where they were and move onto the
photo, without moving, at their next edit (mask_space::attach).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#3419 added a test that holds device memory until an allocation fails; on lavapipe the allocations
never fail, so it skips with the opt-in too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The check grepped for "no GPU adapter" only, so the ctx unit tests' "no
GPU device" skips (and nn's "no GPU runner here") went unseen. It now
lists every "need":"gpu" line except the expected device-loss one. On
the 2026-10-30 lavapipe logs it shows all 24 GPU skips without the
opt-in (the old grep showed 21) and nothing with it.
The log goes in a new mktemp -d directory instead of a fixed /tmp name
another local user could plant a symlink at.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>