Commit Graph
132 Commits
Author SHA1 Message Date
d729c80a5a Detail: Remove Hot Pixels, on for newly imported raws (#1345) (#1350)
* WIP: suppress isolated hot photosites in the raw mosaic (#820)

Noise model fitted to the image, candidates 5 sigma above every same-colour neighbour, kept when two or
more direct neighbours are lit (real light); adjacent hot pairs are replaced too. LIGHTCRAFT_HOT_PIXELS=0
turns it off, LIGHTCRAFT_HP_DEBUG=1 prints statistics. Renders are cached apart from older ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: hot pixel suppression off by default (LIGHTCRAFT_HOT_PIXELS=1 turns it on)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #1345: mosaic outlier filter: cache key no longer names the #858 comparison switch (merged upstream)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #1345: mosaic outlier filter: fixed thresholds (no LIGHTCRAFT_HP_K / _SPOT knobs), statistics under LIGHTCRAFT_PROFILE

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #1345: Remove Hot Pixels, a box in Detail ▸ Noise Reduction, on for newly imported raws

`Detail::nr_outliers` replaces single outlier photosites in the raw mosaic before demosaicing. It is
true in `DevelopSettings::for_raw` and false by default, so edits saved before the box existed keep
their look. The loaders take the flag (no more `LIGHTCRAFT_HOT_PIXELS`), a decoded source records
it, and a cached raw source decoded with the other setting is decoded again. The box shows for raws
only (`check:detail.nrOutliers`).

* #1345: Remove Hot Pixels off keeps old settings' JSON; smart previews fit either setting

- `detail.nr_outliers` is left out of the stored JSON when off, so settings saved
  before it keep their hash and cached thumbnails. Copy / paste, presets (also
  at an amount), preset updates and Auto Sync work on
  `DevelopSettings::to_json_for_merge`, which states it, so turning the box off
  still carries over.
- `DecodedSource::fits_outliers` accepts a smart preview (`from_proxy`): it has
  no mosaic, so an offline raw with the box on keeps reusing the decoded proxy.

(review of #1350)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #1345: the Detail eye and Reset Detail switch Remove Hot Pixels off

`DevelopSettings::removes_hot_pixels` (the box and the Detail section's
eye) decides the raw decode in the app, the web worker and the thumbnail
bench, so a Detail section that is off contributes nothing here either.
Reset Detail resets the box to the general default (off), like the
sliders it resets.

(review of #1350)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #1345: the hot pixel limit counts only replaced photosites, and can be set

The 0.2 % limit was checked against the candidates before real light was
filtered out, so a dense star field reached it with its stars alone and
kept its hot pixels too. It now counts the photosites that would be
replaced, as the module docs say.

The limit is now a parameter (RawImage::suppress_hot_pixels_with, default
HOT_PIXEL_MAX_SHARE) and a setting: Settings > Performance > Hot pixel
limit, applied with the new command app.hotPixelLimit. Changing it forgets
the decoded sources and changes the render cache keys of photos that
remove hot pixels; at the default every key stays as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #1345: the app's default hot pixel limit is the default

The setting is an f32: 0.2 % read back as 0.2000000029… % was a new limit,
so every start forgot the decoded sources and changed the render keys of
photos that remove hot pixels. A value within a millionth of the default
now is the default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Format the imports

* Web loader test states what it decodes with

* Format the media exports

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-05 09:26:47 +01:00
8972565249 LR-PROF-IMPORT: import the user's own DNG camera profiles (.dcp) (#1063)
* 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>
2026-11-05 07:09:30 +01:00
OneSlipandClaude Opus 5.5 077f4a507f GPU-FULL: more of the render on the GPU, loupe drafts from GPU textures (#1065)
* 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>
2026-11-05 05:10:32 +01:00
Dylan M. Taylor ab39f7dbbc web: a restore plans its writes first, each storage key once and within the file's bytes; workers are capped (#3669) 2026-11-03 22:58:42 -05:00
Dylan M. Taylor ac1799db38 web: ?bench runs on demo photos in an unsaved library, never the stored one (#3646)
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.
2026-11-02 22:50:58 -05:00
dotCooCoo d826e79fa2 Skip offscreen Local rows and share hidden-folder checks
Reuse cached folder listings through Arc and allow tests to inject Local places.
Show browsed folders below hidden ancestors as session roots. Apply the same
hidden-entry checks to folder walks, browsing, import scanning, and Local.
2026-10-26 22:36:11 -07:00
skylar taylor-barrick 662df66f1c Preserve queued file drops across dialogs and restore Flatpak sources (#2975) 2026-10-27 03:50:41 +01:00
aiongg 672baaea46 Merge lightcraft#2913 (egui 0.37) into docking-adoption; craft-ui and craft-docpath at craft-libs main a091680 2026-10-25 17:36:59 +00:00
skylar taylor-barrick 3401286b5c egui037: preserve modal focus and first-pass text selection 2026-10-25 14:15:30 +01:00
aiongg 4bc5d2b587 Docking: the web app resets a saved split-frame layout once too 2026-10-25 12:17:42 +00:00
skylar taylor-barrick c627ce7210 egui-037: preserve modal input ownership and packaged font notices 2026-10-25 13:14:34 +01:00
skylartaylor e7144adc1f M0.37: reconcile current LightCraft main and qualify the migration 2026-10-25 07:47:22 +01:00
aiongg 5c46163e66 Merge PR #2778: LRC-PRINT-TEMPLATES: print templates export to a file and import from one 2026-10-25 02:23:20 +00:00
skylartaylor c057ccc23f Migrate LightCraft to egui 0.37 and reconcile dependency and font locks
Preserve current main, adapt frame/input and IME handling, and keep library
check results stable across layout passes. Include the matching Nix font lock
and reviewed attribution changes.
2026-10-25 02:54:42 +01:00
dotCooCoo 59e9a6a4f3 LRC-PRINT-TEMPLATES: print templates export to a file and import from one
The Print dialog's templates could be saved, applied and deleted, but
not moved to another library or another computer. Each saved template
now has an Export… button that writes it to a .lcprint file, and
Import… beside Save adds templates from such files.

print.template.export {name, path} writes {format, version, name,
settings}. print.template.import {path} reads at most 256 KB, checks
the format, version, name and every setting as a saved template's
settings are checked, drops cell photos and guides, and adds the
template under its name, or under "name (2)" and so on when the name is
in use. The buttons appear where the host provides open and save
dialogs: the desktop app does, and the web build does not.
2026-10-23 23:18:35 -07:00
skylartaylor 50df646183 M0.37: port LightCraft UI, input and capture lifecycle to egui 0.37 2026-10-24 08:02:12 +02:00
francoisgn 007047a3bb Merge PR #2205: LR-IMP-DRAGDROP: files dragged over the window show what a d
# Conflicts:
#	crates/ui-egui/locales/cs.json
#	crates/ui-egui/locales/de.json
#	crates/ui-egui/locales/es.json
#	crates/ui-egui/locales/fi.json
#	crates/ui-egui/locales/fr.json
#	crates/ui-egui/locales/id.json
#	crates/ui-egui/locales/it.json
#	crates/ui-egui/locales/ja.json
#	crates/ui-egui/locales/ka.json
#	crates/ui-egui/locales/pl.json
#	crates/ui-egui/locales/pt-br.json
#	crates/ui-egui/locales/ru.json
#	crates/ui-egui/locales/sv.json
#	crates/ui-egui/locales/uk.json
#	crates/ui-egui/locales/vi.json
#	crates/ui-egui/locales/zh-hans.json
#	crates/ui-egui/locales/zh-hant.json
#	crates/ui-egui/src/state.rs
#	docs/parity-checklist.md
#	docs/whats-new.md
2026-10-22 22:30:24 +03:00
francoisgn 927e071873 Merge PR #2453: Web: the page title names the open photo and unsaved changes
# Conflicts:
#	docs/web.md
2026-10-22 22:28:50 +03:00
francoisgn cbedf7b5b1 Merge PR #2455: Web: lightcraft.command refuses params that aren't a JSON ob 2026-10-22 22:28:42 +03:00
francoisgn 4abb3bc860 Merge PR #2430: fix(web): import picker accept list misses many raw formats 2026-10-22 22:28:42 +03:00
Nathaniel LedfordandClaude 434da7ae68 MENU-BAR: menu switches are named for what they show, and no two commands share a name
Switches read "Show Focus Point ✓" next to "Filmstrip ✓"; now every switch
is named for the thing and the check mark says whether it is on (Focus
Point, Clipping, Photo Counts, Mask Overlay, All Mask Overlays, Mask Pins,
Layout Image). Show Original stays: it is a view, not a switch.

Names that meant two things: View ▸ Detail and the Edit section Detail
(Detail View now, like Reference View), Window ▸ Edit and the Edit menu
(Edit Panel), Compare and Compare Before and After (Before/After Side by
Side, next to Split and Top/Bottom), and Clear/Discard Preview Cache (one
command, now Discard Preview Cache everywhere). New labels in all 13
catalogs. Tests: no "Show …" switch; no two menu commands share a label or
a menu's name.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-22 02:00:54 -04:00
Nathaniel LedfordandClaude Opus 5.5 d4753ae787 LR-IMP-DRAGDROP: the web drop notice is made from the import report, with a test of the mapping
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 01:58:51 -04:00
Nathaniel LedfordandClaude Opus 5.5 6881a8c585 LR-IMP-DRAGDROP: the web drop's notice names every outcome of the drop
Review fix: a drop that adds some photos and fails others showed only the first outcome.
`i18n::dropped_photos_outcome` joins the translated added, restored and not-readable parts with a
plain " · " and returns nothing for a drop that only found duplicates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 01:58:51 -04:00
Nathaniel LedfordandClaude Opus 5.5 222925c7d5 LR-IMP-DRAGDROP: a web drop whose files could not be read says so
Review follow-up: after counting what the import did, a drop where every file failed showed no
notice at all. It now says how many photos were not readable (existing translated key).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 01:58:51 -04:00
Nathaniel LedfordandClaude Opus 5.5 c1e3f20f5b LR-IMP-DRAGDROP: align the pl Lightroom wording and stop the web drop toasting "Added 0"
Review fixes:
- pl "Finish Lightroom catalog import before adding photos" uses "Lightrooma", as the other pl Lightroom strings do
- the web drop's toast counts imported photos, says "Restored n photos from Recently Deleted" (already translated) when it only restored some, and says nothing when it only found duplicates

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 01:58:50 -04:00
Nathaniel LedfordandClaude Opus 5.5 34cfc9cd3b LR-IMP-DRAGDROP: count what the web drop added, reject bad ui.dropFiles paths, and say the drop is refused during a Lightroom import
Review fixes:
- the web build's "Added n photos" toast counts the photos `library.import` reports, not the paths submitted
- `ui.dropFiles` rejects a `paths` list with a non-string entry instead of dropping part of it
- the hover card says a drop will be refused while a Lightroom catalog import is pending, and that refusal text is now translated in all 13 catalogs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 01:58:50 -04:00
Nathaniel LedfordandClaude f84347a88e LR-IMP-DRAGDROP: review follow-ups
A finished scan waits for an open dialog before opening its review; drops are refused while an import runs (and, on the web, while a dialog is open); orphaned doc comment restored to builtin_label.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-22 01:58:50 -04:00
Nathaniel LedfordandClaude 266f623480 LR-IMP-DRAGDROP: show what a file drop will do and open the import review
While files are dragged over the window a veil and a card say what dropping
does (review N items, import N presets, open the face model; close the open
dialog first when one is open). Dropped photos and folders open the import
review like File > Import; Settings > Import can import them at once. A drop
while a dialog is open is refused with a toast. The web build now reports
unreadable and failed drops. Adds the control request ui.dropFiles.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-22 01:58:44 -04:00
aiongg 21903294bf Merge track r1-rawcat into next
crates/ui-egui/locales/{cs,it,pl}-formats.json: kept the translations and added the track's new keys.
docs/parity-checklist.md: newer header line; revision rows from both sides.
docs/whats-new.md: both tracks' 2026-10-15 entries.
2026-10-21 14:58:55 +00:00
aiongg a1455fa7cd Merge PR #1988: #1507: web start-up treats only NotFoundError as a missing file
apps/lightcraft-web/src/files.rs: keeps #1987's and #1988's tests
docs/whats-new.md: keeps both 2026-10-15 entries
2026-10-21 14:56:12 +00:00
Bruno-Alumn 45f61ef5ef Web: a finished save repaints, so the title drops its unsaved mark (#2397) 2026-10-21 11:58:16 +00:00
Bruno-Alumn 79f11e098c Web: lightcraft.command refuses params that aren't a JSON object
The page's automation call parsed its params with a fallback to {}, so a
typo such as lightcraft.command("photo.delete", "{ids: [3]}") ran
photo.delete with no params, which acts on the selection. Params must
now be a JSON object (blank text or null is {}); anything else rejects
the promise and runs nothing, as the control channel and MCP already do.
2026-10-21 01:05:03 +00:00
joblonfk001 b8e335370d fix(web): derive file picker accept list from engine extensions
The web import picker hard-coded a 13-extension accept list, so raws the engine imports (CR3, NRW, RAF, ORF, RW2, RWL, RAW, PEF, SRW, …) were silently filtered out of the dialog on Windows/Edge, which matches accept case-sensitively. Generate the list from EXTENSIONS, each extension in both cases. Fixes #2212.
2026-10-21 08:09:19 +08:00
Bruno-Alumn 8ef46aa088 Web: the page title names the open photo and unsaved changes (#2397)
eframe's web runner doesn't handle ViewportCommand::Title, so the web
build never named what was open, and a host embedding the page could
only guess whether a dropped file had opened. The page title is now
"<file name> — LightCraft" for the active photo, with " •" after the
name while changes are not yet written to browser storage, and the
window title when no photo is active. It is set only when it changes.
2026-10-21 00:02:23 +00:00
eliets2 01495684d9 web: Auto language follows the browser's languages (#2085)
default_language() — what the Auto setting resolves to — only read LIGHTCRAFT_LANGUAGE,
which does not exist in a browser, so the web build always started in English. ui-egui
gains an installable default (set_default_language + a pure locale_from_tags that picks
the first shippable language from BCP-47 tags via Locale::parse_tag), and the web app
installs it at startup from navigator.languages (falling back to navigator.language),
read fallibly through js_sys::Reflect so it also works from a worker. Desktop never
installs the override and is unchanged; a language stored in ui.json still wins, which is
exactly Auto semantics.

Tests: locale_from_tags takes the first shippable language (it-IT -> Italiano,
unsupported-first skips ahead, zh-TW -> zh-hant, ja_JP.UTF-8 -> Japanese, pt-BR region
variant), empty/unsupported -> En fallback.
2026-10-17 22:30:14 +03:00
Claude 37a3ab4177 #1507: web start-up treats only NotFoundError as a missing file
OPFS lookups turned any exception into "absent", and start-up only logged
other read errors, so a library with an unreadable file opened incomplete
and the next save could overwrite the stored copy. Only NotFoundError now
means absent; when a stored library file or the active-library pointer
can't be read, the library isn't opened, nothing is saved and the window
names the file. An unreadable thumbnail index no longer deletes every
stored thumbnail as unindexed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
2026-10-15 23:08:54 +00:00
Claude abf29ea392 #1508: web backup refuses to start while changes are unsaved
The browser backup copies the stored library files, so while a save to
browser storage is failing it reported success but left out the queued
changes. It now refuses with the number of unsaved changes and why
(Session::unsaved), and is allowed again once they are written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
2026-10-15 21:53:46 +00:00
Brandon Thomas de5ec99f61 Merge PR #1489
# Conflicts:
#	docs/parity-checklist.md
#	docs/whats-new.md
2026-10-14 22:32:24 -04:00
Jason Baldwin d747c9e0f6 Merge slideshow-titles into slideshow-templates (origin/main: batches 32–33) 2026-10-13 17:01:24 +02:00
Jason Baldwin a021ad2c56 Merge slideshow-playback into slideshow-composer (origin/main: batches 32–33) 2026-10-13 16:55:11 +02:00
Atmik Shetty 21861c2f14 LR-EDIT-DETAIL-NR: noise reduction works in the original's pixels, so previews match the full-size render scaled down (fixes #960)
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).
2026-10-13 16:14:19 +05:30
bflatastic ed1d641f22 Merge PR #1174
# Conflicts:
#	crates/catalog/src/lock.rs
#	crates/pipeline/src/spots.rs
#	docs/parity-checklist.md
2026-10-13 11:06:26 +07:00
dev-null-undefinedandClaude a99c084805 M5.6: a slider drag no longer decodes the edited photo's thumbnail
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>
2026-10-13 00:29:45 +02:00
ChippedWandClaude Opus 5.5 17aafcd5bf LR-EXP-PSD: Photo ▸ Edit In ▸ Open Original in Other Editor (RAW, no copy)
Hands each selected photo's original file itself (a raw stays a raw: no
render, no copy, nothing added to the library) to the editor set in
Settings ▸ External Editing ▸ Other editor, e.g. DxO PhotoLab. Also on a
photo's right-click menu (Edit In).

- `photo.openOriginalInExternal {ids?, app?}` (UI command, menus.rs): up to
  50 photos; demo photos and missing files are skipped with the reason; when
  none has a file it fails saying why (toast + error, no panic). Enabled when
  the active photo has a file and the host can start programs (not the web).
- Several photos go to one start of the editor (`Services::open_all_with`,
  editors.rs `open_all_with`): a single-window editor such as DxO PhotoLab
  drops the files that arrive while it is still starting when started once
  per file. The system default ("" editor) still opens them one by one.
- Settings hint, translations of the two new toasts, parity rows
  LR-EXP-PSD / MENU-FILE-PHOTOSHOP.

Tested by hand on Windows with DxO PhotoLab 8 (one and several raws).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 04:31:34 +02:00
Jason Baldwin 54c1048b57 Merge slideshow-titles into slideshow-templates (origin/main: batch 31; open_session keeps both the slide templates dir and main's apple_photos_background, formats locales merged, whats-new keeps main's Reliability entry, parity summary refreshed) 2026-10-11 17:16:36 +02:00
Jason Baldwin 2138175826 Merge slideshow-playback into slideshow-composer (origin/main: batch 31; locales merged keeping main's values, whats-new keeps main's Reliability entry, parity summary refreshed) 2026-10-11 17:11:12 +02:00
Jason Baldwin b061563383 Merge slideshow-titles into slideshow-templates (origin/main: headless sessions load main's SAM3 model and the slide templates; format-locale keys from both; whats-new and tracker summary) 2026-10-11 16:30:20 +02:00
Jason Baldwin efaa39b8b5 Merge slideshow-playback into slideshow-composer (origin/main: geocode + slideshow crates side by side in the manifests, locale keys from both, tracker summary refreshed) 2026-10-11 16:22:50 +02:00
Claude 4ebb934c56 Merge PR #677
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).
2026-10-11 13:09:34 +00:00
Jason Baldwin 7e4ff5fddf Merge slideshow-titles into slideshow-templates (origin/main: integration batch 26, #975; no conflicts) 2026-10-11 15:05:04 +02:00