Commit Graph
127 Commits
Author SHA1 Message Date
aiongg c2155326d6 Merge PR #2942: Issue #2936: build the GPU kernels on parallel threads 2026-10-31 02:11:04 +00:00
aiongg f12012bc9f Merge PR #2844: GPU: black pixels take the colour tools and a light vignette as on the CPU 2026-10-31 02:10:48 +00:00
aiongg 4a50aacb8a Merge PR #2731: Brush dabs: build only as far as the caller will look (#1520)
crates/pipeline/src/masks.rs: main moved the tests to masks/tests.rs; the PR's test ported there
2026-10-31 02:10:46 +00:00
aiongg 5d08f8632f Merge PR #2631: LR-EDIT-DETAIL-DENOISE: GPU denoise doesn't hang when its tiles in flight fail (#2577) 2026-10-31 02:10:23 +00:00
aiongg 78e70ab8f8 Merge PR #2545: Issue #136: applying the GPU setting never waits for the device on the UI thread 2026-10-31 02:10:21 +00:00
Bruno-Alumn 06e2ec43ba LR-MASK-COLORRANGE: a colour-range mask keeps at most 64 samples however it is loaded (#2556)
Render cost is pixels x samples (CPU and GPU), and only mask.add / mask.update capped the list, so a
sidecar's lc:settings, a preset, develop.merge, render --settings or a catalog / history load could
carry thousands and stall a render or lose the GPU device. MAX_COLOR_RANGE_SAMPLES moves to
lightcraft-develop and ColorRange::samples deserializes through a capped visitor (the first 64 in
order, the rest skipped as IgnoredAny); the CPU and GPU renders also take at most 64 for settings
built in code.
2026-10-30 12:03:03 +01:00
radialmonsterandClaude Opus 5.5 1bfa617a5a GPU: black pixels take the colour tools and a light vignette as on the CPU
The GPU colour tools took the hue of every pixel with atan2(a, b). For a black pixel both are
0, and WGSL leaves atan2(0, 0) undefined: on an RTX 3090 (Vulkan) it is NaN, so with any
colour tool on (saturation, vibrance, the mixer) every exactly black pixel became NaN. NaN is
written as 0, so it stayed black and nothing showed, until a later step lightened it: with a
positive vignette the CPU lifts the black corners and the GPU left them black (up to 127
levels apart in 8-bit). A neutral pixel now gets hue 0, as Rust's atan2 gives on the CPU.

Regression test: black bars under a light vignette with saturation, vibrance or the mixer
(fails on main: mean 12.8, max 127 levels).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ti8KemZbjE2ZgHgV1q1BM
2026-10-28 13:58:46 -04:00
Dylan M. Taylor 9c1ea73116 refactor(gpu): extract neural plan validation 2026-10-28 03:24:18 +00:00
Dylan M. Taylor 2f42ff8944 refactor(pipeline): group pixel finish parameters 2026-10-28 03:24:18 +00:00
NetterSilverandClaude Opus 5.5 93487af3b3 Cut serial work from raw development and GPU transfers
- RawImage::develop demosaics only the default crop with AHD when there
  is no OpcodeList3 (ahd_window), instead of demosaicing the margin too
  and then copying the crop out of the full picture on one thread.
- Gpu::upload writes through queue.write_buffer_with and copies into
  the staging buffer in parallel parts. create_buffer_init zeroed a
  staging buffer and then copied over it, two single-threaded passes
  over a full-size photo's ~290 MB.
- finish_and_read copies the mapped readback out in parallel parts (one
  thread read the 91 MB of a 24 MP export at ~2 GB/s, ~45 ms).

Output is byte-identical (ahd_window_is_the_crop_of_the_whole,
uploads_read_back_exactly).

Export of a 24 MP ARW to full-size JPEG (examples/export_bench, 75
interleaved runs each on a shared 16-thread Ryzen):
main 802 ms median / 614 ms min -> this branch 647 / 525 (-19.3% / -14.5%).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:43:41 +01:00
Ingmar LübbeandClaude Opus 5.5 c9b123ab9e Issue #2936: the doc comment gives the kernel time, not the whole start-up
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018QWU3BMJMENFqFqxDHzV3b
2026-10-25 16:53:34 +01:00
Ingmar LübbeandClaude Opus 5.5 fa4bccff64 Issue #2936: build the GPU kernels on parallel threads
Gpu::new built the 27 compute pipelines one after another. On DX12 each one is compiled by FXC
(issue #471), single-threaded, inside create_compute_pipeline, so every process waited ~2.2 s for
them on an Intel Core Ultra 7 165U iGPU (app.gpu 2.4 s median) while the other cores idled.

build_kernels creates the shader modules and layouts on the calling thread and shares the pipelines
out to worker threads (std::thread::scope, an atomic job counter; the calling thread works too, so
every kernel is built even when no thread can be started). wgpu's error scopes are per thread, so
each worker pushes its own validation and internal scopes: a refused kernel or a panicked worker
still means no GPU, never a half-built one. app.gpu: 2.4 s -> 1.2 s median on that machine, exports
pixel-identical. The gpu: info line now reports the kernel count and build time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018QWU3BMJMENFqFqxDHzV3b
2026-10-25 16:38:12 +01:00
OneSlipsandClaude Opus 5.5 caba780e1e Issue #136: the GPU preference and the warm-up flag use one ordering
set_enabled now stores the preference with SeqCst, as the warm-up thread reads it after releasing
its start, so a switch back on made meanwhile is always seen there (CodeRabbit). The start flag
uses SeqCst throughout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:00:50 +10:00
OneSlipsandClaude Opus 5.5 8541131cd8 Issue #136: a GPU start lost to an off/on switch is retried
Found by radialmonster: turned off before `lc-gpu-init` created the device and back on,
`warm_up` saw the start still held and returned, then the thread released it with no device,
leaving Settings > Performance at "GPU: starting…". The thread now releases the flag first, then
reads the preference again and tries again itself when GPU rendering is back on (it stops when
another start took over, at shutdown or with the environment's off switch). Settings >
Performance also calls the idempotent `gpu::warm_up()` in its "starting" branch (their
suggestion), on the repaint it already asks for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:58:20 +10:00
OneSlipsandClaude Opus 5.5 9a6ac9aea7 Issue #136: a non-waiting GPU query starts the device; Settings repaints while it starts
- `app.gpu {wait: false}` with GPU rendering on starts creating the device in the background
  (`warm_up`), so a caller that never waits still gets an answer later. `warm_up` starts one
  thread at most, and may start again if GPU rendering was turned off before it ran.
- Settings > Performance asks for a repaint every 250 ms while it shows "GPU: starting…": the
  device is created on another thread, which doesn't wake the window.
- CLI test: `{enabled: true, wait: false}` answers enabled with available, adapter and reason
  null (skips with LIGHTCRAFT_GPU=0, since it starts creating a device).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 13:48:25 +10:00
Muhammad Saad a3dbe2ab91 Brush dabs: build only as far as the caller will look (#1520)
MAX_STROKE_DABS bounds one stroke at two million, but the two callers
that will not look at nearly that many check the length after the
vector exists, which bounds nothing: it only discards what was already
built.

  gpu/render.rs rejects a brush mask past MAX_DABS, 4096 across the
  whole mask, after collecting every stroke's dabs. Each stroke may
  construct two million first, so the sum that rejects them is reached
  only once they all exist.

  spots.rs skips an Auto Mask stroke past MAX_DABS_CONSIDERED, 200,000,
  after brush_dabs has built the whole thing.

brush_dabs_within takes the caller's own budget, and push_dabs_within
charges against it instead of MAX_STROKE_DABS. Both callers now ask for
one more than they accept, so a stroke past the threshold is still
recognised as past it without the rest being built: 4097 rather than
two million per stroke on the GPU path, 200,001 rather than two million
for the spot window.

Behaviour is unchanged for anything either caller keeps. A result
short of its budget was never truncated, so it is the whole stroke, and
a truncated one only ever reaches a caller that was going to reject it.
A truncated stroke is not a prefix: running out of room makes a segment
lay its remaining dabs further apart rather than stop mid-segment,
which is how push_dabs_counted already behaved at MAX_STROKE_DABS.
2026-10-23 23:49:37 +04:00
francoisgnandClaude Opus 5.5 f74178cdae Merge upstream/main (#2710 one test binary per crate, #2713) into round3a-integration
Conflicts:
- xtask/src/main.rs: upstream's runners (cargo test or nextest, no
  examples, doctests apart, one wasm `cargo check --keep-going`) with
  #2302's `ci --quick` on top: test_selected builds the same targets
  (--lib --bins --tests), the wasm check takes the changed crates in one
  call, the full test step is upstream's.
- apps/lightcraft/src/control_server.rs: upstream's `before` closure, with
  the batch's no-delay sockets kept.
- docs/parity-checklist.md: LR-BEHAV-ACCESS from main (tests/it path),
  LR-BEHAV-LOCALE / LR-BEHAV-LOCALIZE from the batch (Polish).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 21:06:33 +03:00
Dylan M. Taylor 6fb6815cd9 fix: document the deliberate buffer leak during thread teardown (SonarCloud)
Dropping a wgpu buffer after the device's thread-locals are gone can
abort; the device owns the memory either way.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-23 09:44:05 -04:00
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 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
Bruno-Alumn ff4dc74e5d LR-EDIT-DETAIL-DENOISE: GPU denoise doesn't hang when its tiles in flight fail (#2577)
The GPU runner's workspace pool kept its free list and its count of made
workspaces behind two mutexes, and forget() lowered the count without
the free list's lock that a waiting caller holds. If the tiles in flight
failed between a caller finding every workspace in use and that caller
starting to wait, the wakeup was lost and the denoise thread blocked
forever, out of reach of cancel. The free list and the count now share
one lock in a small Workspaces type, so a give or a forget can't slip in
between the check and the wait. A caller woken that way after the device
was lost returns the GPU's error instead of making a new workspace on it.
2026-10-22 20:20:55 +02: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 e705eb563c Merge PR #2380: LR-EDIT-DETAIL-DENOISE: GPU denoise gives the right result on DX12 with NVIDIA Pascal 2026-10-22 02:42:06 +00:00
aiongg 17b9bdefc7 Merge PR #1984: GPU: a lost device is not polled again, and a poll that panics fails the render (#1858) 2026-10-22 02:42:06 +00:00
Claude 55a094475d Merge main into fix/dx12-nn-denoise
The only conflict was the 2026-10-19 whats-new entries; both are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K93HTyCiwYvbRGaAkjPvXn
2026-10-21 13:01:31 -07: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
Claude 2c085e0ae5 LR-EDIT-DETAIL-DENOISE: check the transposed convolution's stores exactly too
A max-pool, a channel-copy 2x2 transposed convolution (mode 1) and the
channel-copy 1x1 layer give an exact answer, so a store fault in mode 1
can't hide in tolerance (CodeRabbit review of #2380).

On the GTX 1070 this test passes even without mode 1's stepped stores:
FXC merges them the same way, but the driver runs that store right, so
only mode 0 was shown wrong. Mode 1 keeps the stepped stores as a
precaution; the shader comment and docs/gpu-pipeline.md now say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:34:56 -07:00
Claude 75292874a3 LR-EDIT-DETAIL-DENOISE: GPU denoise gives the right result on DX12 with NVIDIA Pascal
On DX12, FXC merged the conv kernel's four output stores (modes 0 and 1)
into one 16-byte store_raw. The DXBC is valid and aligned, but NVIDIA's
driver ran it wrong on a GTX 1070: every second pixel's channels came
out shifted, so AI Denoise on the GPU was silently wrong on Windows.
DXC and Vulkan were right on the same card.

The stores now step by `1 + p.zero` (the always-0 uniform, renamed from
`pad`), which FXC cannot fold, so it keeps them apart. A two-layer
channel-copy network checks the stores exactly; it fails on that card
without the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:54:24 -07: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
helgehelge123andClaude Opus 5.5 53f57b9404 LR-MASK-OBJECTS: SAM 3 falls back to the CPU when the GPU can't run it (#1974)
On an Intel Mac with an AMD Radeon Pro 5300M the Metal compiler crashes
on candle's mlx_gemm kernel, candle-metal-kernels panics on the compile
error, and the worker reported "the AI model failed unexpectedly" and
loaded the model on Metal again for every next job, so AI masks never
worked there.

The worker now retries a job that fails on the GPU (a panic or a candle
error) once on the CPU, after freeing the model. When that works, the
model stays on the CPU for the session and the job's outcome carries a
notice that segment_poll shows once; when the CPU fails too, the GPU
was not the cause and the next job uses it again. Missing or damaged
model files, render and remote-worker errors are not retried. Only
builds with a GPU path (macOS) retry; LIGHTCRAFT_GPU=0 and
LIGHTCRAFT_GPU_BACKEND=off now keep SAM 3 on the CPU from the start
(lightcraft_gpu::env_off_reason), like AI Denoise and the face models.

lightcraft-segment: Sam3::load_cpu, GPU_BUILD; text_logits returns the
crate's Error so Describe failures are classified like clicks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:55:32 +02:00
Matthew James SivakandClaude Opus 5.5 0d30c99e87 LR-VIEW-CLIPPING: merge main into #937 (⌥-drag previews)
Conflict only in docs/parity-checklist.md: the generated summary is
refreshed with `cargo xtask parity --write`, and the Last updated line
keeps main's format with this PR's change note (today's date) instead of
the duplicated note the branch carried. ⌥-drag preview still wins over
AllMasks and the selected mask in view_overlay; overlay kinds 6–9 unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:52:08 +02:00
lincleejunandClaude Opus 5.5 389a2cdd65 GPU: a lost device is not polled again, and a poll that panics fails the render (#1858)
Once a Vulkan or DX12 device is lost, wgpu 30 panics in `Device::poll`
instead of returning an error: the device error is not a `PollError`, so
`wgpu_core.rs` hands it to `handle_error_fatal`. The readback assumed the
wait would fail. A render whose readback found the device lost panicked
(caught by `try_render`), and while it unwound, a buffer going back to
the pool polled again from a destructor (`Gpu::trim`). That second panic
aborted the app ("panic in a destructor during cleanup"), as in the
reporter's logs on an Intel UHD 620 under Vulkan, each time a merge lost
the device.

The render device's three polls (the readback, `trim`, `sync`) now go
through `Gpu::poll`, which stops polling a lost device (the device-lost
callback, or a poll that panicked) and turns a panicking poll into an
error that retires the GPU. The render then fails over to the CPU and the
app keeps running. A GPU retired for another reason (a validation error)
keeps a live device and is still polled, so its pooled buffers are freed.

Regression tests run in a child process (they lose the device):
- `a_poll_on_a_lost_device_neither_panics_nor_aborts`: polls panic as
  wgpu's do on a lost Vulkan device; a readback, then an unwinding render
  whose destructor trims the pool. Before the fix the child aborts with
  SIGABRT and the log line above.
- `a_really_lost_device_is_not_polled_again`: a Metal device really lost
  (4200 unsubmitted command buffers, past wgpu-hal's 4096) is not polled
  again. Before the fix the poll still reaches wgpu.
- `a_retired_but_live_device_is_still_polled`: a GPU retired by a
  validation error still polls (and releases its buffers).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RbXK4njy12cpm4PG12Xrqt
2026-10-15 21:40:29 -07:00
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 9ddb8d1824 Merge PR #1663 (rebased)
# Conflicts:
#	crates/ui-egui/locales/de.json
#	crates/ui-egui/locales/es.json
#	crates/ui-egui/locales/fr.json
#	crates/ui-egui/locales/ja.json
#	crates/ui-egui/locales/pt-br.json
#	crates/ui-egui/locales/ru.json
#	crates/ui-egui/locales/uk.json
#	crates/ui-egui/locales/zh-hans.json
#	crates/ui-egui/locales/zh-hant.json
2026-10-15 03:40:17 -04:00
Brandon ThomasandClaude Opus 5.5 2179473536 Merge PR #1663
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 03:24:51 -04:00
Brandon Thomas 9ca3cd0926 Merge PR #1135
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	README.md
#	crates/raw/Cargo.toml
#	crates/ui-egui/src/theme.rs
#	xtask/src/main.rs
2026-10-14 23:23:18 -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 b985090c90 Merge PR #1758 2026-10-14 22:29:45 -04:00
Brandon Thomas e0ed8c0270 Merge PR #1033
# Conflicts:
#	docs/gaps.md
#	docs/parity-checklist.md
2026-10-14 21:58:33 -04:00
Brandon Thomas 2138dc6e7d Merge PR #1009
# Conflicts:
#	docs/parity-checklist.md
2026-10-14 21:55:05 -04:00
MarekSpodBiedry 6e0880769a gpu: init-marker tests use their own scope counter so parallel device tests cannot join them (#1721) 2026-10-14 19:31:57 +02: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 f625c74c7a Faces: whole networks on the GPU, batched faces, CPU-only switch, paced GPU use
Activations stay on the device between layers (net/program.rs lowers the graph; every
operator is a WGSL kernel, one submission and read-back per run); faces of a photo go
in batches of up to 16; Settings > Faces shows each model's device and a CPU-only
switch (faces.gpu); the scan's pace limits GPU use; the GPU check is an activity row.
YuNet 58 -> 21 ms, SFace 40 -> 9.6 ms, AuraFace 295 -> 71 ms (heavily loaded machine).

Sources: own design.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 19:43:44 +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
Andrew FrahnandClaude Opus 5.5 0242a75b5a Faces: run the networks' convolutions on the GPU (wgpu) when it agrees with the CPU and is faster
An accelerator hook in the face interpreter takes Conv nodes; lightcraft-gpu implements it with a WGSL
implicit-GEMM convolution (any stride, padding, grouping; split-K for small maps; a direct kernel for depthwise
layers), each model's weights resident on a device of its own. After loading, detection and recognition check
the GPU in the background (agreement within 1e-3 and faster than the CPU) and otherwise stay on the CPU, which
remains the reference; the first GPU error drops it. Sources: own design.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 12:22:50 +11: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
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
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