Commit Graph
179 Commits
Author SHA1 Message Date
francoisgn 0304d6acfe Merge PR #2550: #2550 c21714a37 Dehaze no longer crushes dark pixels next to bright ones (#2504), spli 2026-10-23 16:38:37 +03:00
francoisgn 5b4c0e79c2 Merge PR #1976: GUM-3201: Reduce guided-filter coefficient allocations 2026-10-22 22:31:15 +03:00
francoisgn b32b4383f5 Merge PR #1074: LR-VIEW-ZOOM: diagnostic overlays render zoom windows; soft
# Conflicts:
#	docs/gaps.md
#	docs/parity-checklist.md
#	docs/ui-parity.md
2026-10-22 22:28:28 +03:00
Claude c21714a379 #2504: the dehaze regression test also fails when the whole dark field renders black
Review follow-up (CodeRabbit): an all-black field made every comparison pass, so the field's own level must stay visible first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
2026-10-22 11:42:59 +00:00
Matt MarkhamandCopilot App 9122443f22 pipeline: bound dehaze veil by the pixel's own minimum channel (#2504)
The dark channel is a Gaussian blur, not a patch minimum, so near a
bright neighbour it can credit a dark pixel with more veil than its own
minimum channel holds; (c - air*(1-t))/t then went negative and was
clamped to black - the blotches on CR3 raws. Bound the credited veil by
the pixel's own minimum channel (d = min(dark/air_pre, c_min/air)),
which provably keeps every channel non-negative for any gain, airlight
estimate or windowed fixed_air, in both the CPU and GPU paths. Also
zero-guards the divisions, fixing a 0/0 NaN on an all-black frame.

Regression test: a uniform dark field beside a bright haze must render
uniformly; pre-fix the bleed zone crushed to black.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-22 11:21:40 +00:00
aiongg 3300e6dad8 Merge track r2-t01 into next
# Conflicts:
#	docs/gaps.md
#	docs/parity-checklist.md
#	docs/raw/coverage.tsv
#	docs/whats-new.md
2026-10-22 02:49:52 +00:00
aiongg 90eee2208f Integration: #1258 restore the pipeline local tests the PR head truncated
Drops the unfinished nr_contrast_keeps_fine_texture test and restores exposure_doubles and
fast_tests::fast_guided_close_to_reference; keeps the hidden/empty mask test.
2026-10-22 02:38:57 +00:00
aiongg bf2f3df0ac Merge PR #1809: engine: keep the decoder facts a smart preview's offline edits need
crates/engine/src/cmd/previews.rs: the opening refresh takes every photo on disk (#1809) with the preview_fitted_look flag of #1545 (vendor raws and DNGs); SmartJob and refresh docs name both.
2026-10-22 02:38:42 +00:00
aiongg b242cd6bfc Merge PR #1972: Tone: neutral display tone map is the identity up to white (#1945) 2026-10-22 02:37:23 +00:00
aiongg b1c4cb7df9 Merge PR #1258: Ignore hidden and empty masks when choosing develop planes. 2026-10-22 02:37:22 +00:00
aiongg 69fcc25535 Revert "Merge PR #995: LR-VIEW-CLIPPING: ⌥-drag previews for Blacks, Whites, Exposure, Sharpening and NR (#937)"
Held for the owner: #995 and #999 overlap, and #995's author offered on 10-12 to
close #995 once his commit was in #999. docs/parity-checklist.md: header keeps the
#1485 note and drops the #995 part.
2026-10-21 14:56:17 +00:00
aiongg a44a2eeb1d Merge track r1-ui into next
docs/whats-new.md: kept both tracks' entries, newest first, one 2026-10-18 heading.
2026-10-21 14:54:06 +00:00
Matthew James SivakandClaude Opus 5.5 30fea8d34c LR-VIEW-CLIPPING: profiled diagnostic views keep the sRGB histogram; GPU equivalence for the clipping overlays
On a profiled display the Grey and Sharpening-mask views rendered in the display's primaries, so
their histogram counted display-primary pixels, not the photo's sRGB ones. They now render in sRGB
like the clipping views (`Overlay::renders_in_srgb`) and are converted for the display after; they
show no photo colour, so nothing is lost.

The GPU equivalence test now covers Grey (mean/max LSB limits) and the shadow/highlight clipping
views (share of differing pixels, as for Visualize Spots: both renderers draw them with the same
`visualize::apply`, and a 1-LSB difference at 0/255 flips a pixel).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 13:14:16 +02:00
Andrew FrahnandClaude Sonnet 5.5 131de8cccd Merge upstream/main into pr/overlay-windows (#1027 landed)
GPU handles(): only output-device proofs stay on the CPU; Show All Masks now renders zoom windows (upstream); proof-boundary tolerance in the GPU equivalence test; device-profile draft test.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 10:57:07 +11:00
adrdsouzaandCodex 4d8f93585a Keep live brush dab budget across frames (#1520)
Pitch-Impact: none

Co-authored-by: Codex <noreply@openai.com>
2026-10-17 21:14:43 +05:30
Ted Mao 8b069bf87e Reduce CPU guided-filter coefficient allocations
Generated by an AI coding agent.
2026-10-15 20:26:21 -07:00
Gert Pellin 27974fac0f Tone: test the edited-curve shoulder at white, not past its end (#1945) 2026-10-16 04:46:51 +02:00
Gert Pellin c5cebdef39 Tone: neutral display tone map is the identity up to white (#1945)
The SDR shoulder in ToneMap::display_hdr ran at neutral Contrast, Whites
and Blacks too, so an unedited JPEG or PNG exported neutral 255, 254 and
253 one level darker and a white background never came out as 255. The
shoulder now shapes edited curves only; neutral sliders are the identity
up to white and clip above it. Edited curves render as before (V1).

Tests: the identity test runs to 1.0, a new pipeline test renders the
issue's neutral greys back exactly, the sRGB ramp round trip is exact,
and a guard keeps the shoulder on edited curves.
2026-10-16 04:27:35 +02:00
Adrian D'souzaandClaude Opus 5.5 d770c52313 Masks: bound the total dabs of one brush stroke (#1520)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 17:23:18 +05:30
Matthew James SivakandClaude Opus 5.5 9a0f076105 LR-VIEW-CLIPPING: ⌥-drag previews for Blacks, Whites, Exposure, Sharpening and NR (#937)
Holding ⌥ (Option) while dragging a slider shows a diagnostic view in the loupe, as in
Lightroom; releasing ⌥ or the slider brings the photo back and nothing is saved:

- Blacks: shadow clipping (white; black where every channel is 0, the colour of the
  unclipped channels where only some are).
- Whites / Exposure: highlight clipping (black; white where every channel is 255, the
  clipped channels' colour where only some are).
- Sharpening Masking: the weight sharpening gets at each pixel, white on black.
- Sharpening Amount / Radius / Detail, luminance NR: the image in grey.

New `Overlay` variants (ShadowClip, HighlightClip, SharpenMask, Grey) are drawn by the shared
`visualize::apply`, so CPU and GPU agree; the sharpening mask needs the detail planes and
renders on the CPU (like HdrRange). The mask uses the same weight function as the render
(`finish::sharpen_weight`). With a display profile the clipping views are tested in sRGB, like
the histogram, and diagnostic views keep the photo's histogram instead of the view's.

Tracker: LR-VIEW-CLIPPING ✅ → 🟡 (J only frames the photo, it doesn't paint clipped pixels)
and LR-EDIT-DETAIL-SHARPEN ✅ → 🟡 (Radius and Detail don't reach the render).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:41:44 +02:00
Brandon Thomas 1d006f7703 Merge PR #1538
# Conflicts:
#	docs/parity-checklist.md
2026-10-14 22:32:30 -04:00
Brandon Thomas de5ec99f61 Merge PR #1489
# Conflicts:
#	docs/parity-checklist.md
#	docs/whats-new.md
2026-10-14 22:32:24 -04:00
Brandon Thomas 75e0a6dd2a Merge PR #1835 2026-10-14 22:32:07 -04:00
Brandon Thomas 7ab2f2bfb4 Merge PR #1808 2026-10-14 22:31:56 -04:00
MyrarcandOpenAI Codex cebaade144 CI-RUST195: keep strict Clippy clean on Rust 1.95
Move nested conditions into match guards and simplify equivalent Boolean
expressions without changing validation or short-circuit behavior. Keep
the explicit permitted Testkit dev-dependency arm in the layer checker.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
2026-10-15 08:50:22 +07:00
midasmatterhorn 8a8fe7e098 engine: keep the decoder facts a smart preview's offline edits need
A proxy stored the pixels and the camera tone curve only. An edit that
reads the file's own facts — a Kelvin white balance through the camera
colour model, Camera local tone mapping's gain table map, an AI mask's
semantic matte — rendered differently once the original went offline,
because the catalog's header facts cannot carry them.

The facts now ride in a tail after the JPEG (a length-prefixed JSON blob,
then one byte plane per matte), counted from the file's end so a header
rewritten in place keeps pointing at it; is_valid requires the JPEG to
end right before the tail. ENCODE_VERSION becomes 3, so proxies an older
build wrote are rebuilt when their original is readable (offline ones
keep their older look, as before). smart::encode now takes the source's
SourceInfo and decode returns the restored one.

Tests:
- tests_import::smart_previews_keep_the_decoder_facts_offline_edits_need:
  a proxy's own pixels and facts render bit-identically offline for a
  Kelvin white balance (6144/6144 pixels differ before), a stored sky
  matte, and a gain table map
- smart::tests::facts_and_mattes_travel_with_the_proxy: the facts and
  mattes round-trip; a proxy cut inside its tail is invalid; an old
  proxy, and one whose header lies about the tail, read as no facts
2026-10-15 00:42:03 +03:00
midasmatterhorn 0366116c76 ui: render the Show All Masks overlay in zoom windows
The loupe's window rendering already evaluates every visible mask's alpha
for an `Overlay::AllMasks` (its union is composed from the same alphas the
selected-mask overlay uses), but `overlay_supports_window` refused it, so
at 1:1 and closer "Show All Masks" fell back to a whole-frame render at
the zoomed size: slower, and its detail stopped at the frame render's
resolution. Allowing it makes the zoomed view a canvas-sized frame plus a
native window, as a single mask's overlay already is.

Tests:
- tests_preview_limit::an_all_masks_overlay_at_one_to_one_is_a_window
  (fails before: "native 6000: the whole-frame render is 6000")
- tests_window::an_all_masks_overlay_window_matches_the_whole_render
  (a window of the union matches the whole render's pixels, for every view)
2026-10-15 00:05:03 +03:00
Andrew FrahnandClaude Opus 5.5 78b014fb79 Merge upstream/main into pr/pen-pressure
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 20:04:19 +11:00
Andrew FrahnandClaude Opus 5.5 1fc6fd24a7 Merge pr/zoom-detail (with upstream/main) into pr/overlay-windows
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 19:30:48 +11:00
Brandon Thomas 9085eb4b23 Merge PR #1653
# Conflicts:
#	docs/parity-checklist.md
2026-10-14 01:39:50 -04:00
Brandon Thomas 4d0e11651a Merge PR #1546
# Conflicts:
#	crates/engine/src/tests.rs
2026-10-14 01:38:55 -04:00
eliets2 3ad5eadab2 LR-REM-HEAL: checkpoint zero-feather spot edge correction
Treat a zero-width feather transition as a hard edge so exact-radius pixels remain unchanged, including at zero opacity. Add actual image regressions for all spot modes, image corners and two clone output sizes.

Validation: both new regressions failed on the original implementation and pass here. Formatting, workspace Clippy, HEIF and a generated-photo app smoke test pass. The unfiltered workspace gate remains blocked by the existing all-masks overlay test tracked by #1599; later parity, layers, assets and WASM stages are pending. This is a source checkpoint, not a green release candidate.

Contributor: @eliets2
2026-10-14 00:51:56 +03:00
Andrew FrahnandClaude Opus 5.5 c594c7fdc1 Merge pr/zoom-detail into pr/overlay-windows
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 08:03:06 +11:00
Andrew FrahnandClaude Opus 5.5 83d388002d Merge upstream/main into pr/pen-pressure
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 07:58:20 +11:00
Brandon Thomas 5ece09aa6c Merge PR #1562 2026-10-13 11:49:13 -04:00
Brandon Thomas 2fd0530beb Merge PR #1561
# Conflicts:
#	docs/whats-new.md
2026-10-13 11:49:13 -04:00
Brandon Thomas 8bcbcd918b Merge PR #1560
# Conflicts:
#	docs/whats-new.md
2026-10-13 11:49:12 -04:00
Brandon Thomas 2341132b35 Merge PR #1558
# Conflicts:
#	docs/whats-new.md
2026-10-13 11:49:11 -04:00
Brandon Thomas f0493fa11b Merge PR #1419 2026-10-13 11:48:36 -04:00
Michael Anderson 4a07254e06 Never crash: Read the edge pixel for bilinear samples outside the picture
Infinite coordinates from a zero-size crop overflowed the sampler.
2026-10-13 10:46:42 -05:00
Michael Anderson f753778f45 Never crash: Fix blur overflow and hangs on sliver crops
Cap the sigma at twice the picture's long side and square the box width in i64.
2026-10-13 10:46:37 -05:00
Michael Anderson 4c088c3284 Never crash: Give the loupe a finite aspect for a zero-size crop
Count each side as at least one pixel.
2026-10-13 10:46:33 -05:00
Michael Anderson 307843b2ad Never crash: Fix defringe radius overflow for amounts past the sliders
Treat the amount as the slider's end and keep the radius within the picture.
2026-10-13 10:46:23 -05:00
Michael Anderson 57487875b5 Never crash: Cap the spot radius so huge spot sizes render (#1519)
Sidecar spot sizes aren't limited to the slider range. Count at most two long edges.
2026-10-13 10:45:50 -05:00
nevicia2010-hub f86459db58 LR-MASK: fix range handle drags and color sampling settings 2026-10-13 14:29:16 +02:00
dev-null-undefinedandClaude e7cd00ef56 M5.6: a zoomed view with Dehaze keeps its airlight proxy (each pan and slider step resampled the whole original)
A windowed render dehazes with the whole frame's airlight, estimated on a 384 px proxy of the frame
(`local::frame_airlight`). Every window asked for that proxy again, so every pan of a 1:1 view and every
slider step while zoomed sampled the whole original down to it on the CPU: ~200 ms of CPU on a 64 MP raw.

The proxy (taken before white balance) is now kept for the last 4 (source fingerprint, frame, size); white
balance, the dark channel and the percentile still run on it every time. Same pixels: the GPU window of
`render_bench` is byte-identical.

`optics::fingerprint` indexed the first pixel of an empty image; it now hashes only the dimensions then.

render_bench ONLY=window GPU_ONLY=1 on a 64 MP Sony ILCE-7RM6 raw (RTX 4070 Ti SUPER, 4-CPU quota), CPU time:
pan 208-212 -> 9-11 ms, exposure change 200-204 -> 4 ms, NR change 211-217 -> 13 ms.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 13:07:34 +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
wstockandClaude Fable 5.1 5e9a1b6ec7 Visualize: the all-masks overlay shows the union of the masks, not the last one
`Overlay::AllMasks` painted each mask's plane in turn, and every mask view
rewrites the whole image from its alpha (White on Black writes `255·a`,
Image on Black multiplies by `a`…), so each plane wiped the ones before it
and only the last visible mask showed. The planes are now merged (per-pixel
max) and painted once; the GPU path goes through the same `apply`.

The test added with #919 could not pass: besides the above, its hidden-mask
check expected the plain pixel under White on Black, where everything
outside the masks is black by definition (as the single-mask test asserts).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NH3itd85jPuM6wNjgcGfEH
2026-10-13 07:26:18 +00:00
Brandon Thomas d8f10a1ef7 Merge PR #919 2026-10-13 02:01:30 -04:00
bflatastic fa3dfbbc4a Merge PR #1238
# Conflicts:
#	crates/pipeline/src/spots.rs
#	crates/ui-egui/src/panels/edit.rs
#	docs/parity-checklist.md
2026-10-13 11:06:46 +07:00