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>
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>
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)
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
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>
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).
`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
Resize::apply rounds the size, then the render fit the frame into that box
again: on a frame a hair under 3:2 the refit gave 1333 x 1.4999999 = 1999.49
and lost a pixel of the long edge. Frame::fit now keeps a box that already has
the frame's aspect up to rounding, and output_size rounds the native size to
whole pixels first so float noise can't decide a half-pixel tie.
A drag renders drafts, which have no HDR histogram, so an HDR edit's histogram dropped to the
SDR layout until the drag ended. Now, during a drag of an HDR edit, a small full-quality render
(Slot::HdrHistogram, 192 px, its own stage cache) runs beside the drafts and feeds the panel at
its own pace: the drafts don't wait for it. The panel also keeps an HDR edit's last HDR histogram
when a render without HDR bins arrives (a draft, a cached thumbnail or stand-in), so it never
swaps to the SDR layout mid-drag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rust 1.99's clippy rejects `chunks_exact(3)` with a constant size
(`chunks_exact_to_as_chunks`, -D warnings), which failed `cargo xtask ci` on
main. Same behaviour: the completed pixels are copied as [f32; 3].
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Visualize Spots, Point Color range and Visualize HDR are drawn on the
loupe's 1:1-8:1 window like mask overlays: the range and HDR views act
per pixel, the spots high pass reads within the window's margin. New
pipeline test: each overlay's window equals the whole-frame render's
pixels. The GPU finish kernel now soft-proofs output-space proofs
(destination and display gamut warnings) like the CPU; an output-device
profile's table still renders on the CPU. New GPU equivalence test.
Stacked on pr/zoom-detail (#1027).
Sources: own design.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brush strokes record a pen pressure (0..1) per point from egui touch
force while Brush > Pen Pressure is on (default). Each dab's radius goes
from a tenth to all of the brush, and its flow is proportional to the
pressure, in the committed stroke and the live preview alike. A mouse
stroke stores no pressure, so documents and renders are unchanged. The
GPU mask kernel leaves pressure strokes to the CPU. mask.brushStroke takes
`pressure`; bad lists are refused. Also satisfies clippy 1.99's
chunks_exact_to_as_chunks in spots.rs.
Sources: own design.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Item 4 of #989. Same elements and the same ignored remainder as chunks_exact(3),
checked on lengths 0-9 in a scratch file; the 13 spots tests pass and clippy over
the workspace without the desktop app package is clean under -D warnings. Items
1-3 of #989 (the macOS app build, the /bin/true lock test, eleven engine tests)
are not touched here.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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).