Commit Graph
15 Commits
Author SHA1 Message Date
Brandon Thomas 59140d8265 Merge branch 'worktree-agent-aada35d252224c5cd'
# Conflicts:
#	crates/engine/src/media.rs
#	crates/ui-egui/src/state.rs
2026-10-01 11:11:49 -04:00
Brandon ThomasandClaude Opus 5.5 64dfa8adbd Merge edit-panel parity (Point Color, Calibration, curve refine saturation + targeted adjustment, red/pet eye, visualize spots)
Fix-ups: diagnostic overlays never populate the view-preview cache; Photo Merge's RenderRequest gets
the new overlay field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:02:11 -04:00
Brandon ThomasandClaude Opus 5.5 5f401d2eef Merge main (straighten, vignette style) into the polish branch
Also: GPU retired-buffer list leaks its buffers at thread exit instead of
dropping them (wgpu's thread-locals may already be gone; the drop aborted
the test process intermittently), and headless UI tests wait up to 120 s
for renders (they timed out on a loaded machine).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:52:17 -04:00
Brandon ThomasandClaude Opus 5.5 82cecec366 LR-REM-VISUALIZE: Visualize Spots — high-pass black/white view with threshold
Overlay::Spots(threshold): luminance minus its Gaussian blur (sigma relative to the long edge,
so the same specks show at any preview size), thresholded to white/black; the threshold slider
(0..100) raises sensitivity. Runs after either renderer like the other overlays (equivalence:
share of differing pixels, since the view is binary). Remove panel: Visualize Spots checkbox +
Threshold slider; UI command view.visualizeSpots (A) opens the Remove tool with the view on or
toggles it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:52:06 -04:00
Brandon ThomasandClaude Opus 5.5 06d30a0e59 LR-EYE-PET: pet eye — glow detection, dark pupil, catchlight
Pet eyes are found by brightness (the tapetum glows in any colour) and the disc is replaced by
a dark neutral pupil (Darken scales it); redeye.catchlight {index, on, offset} adds a soft
white catchlight at an offset from the pupil centre in pupil radii. Red Eye panel: Red Eye /
Pet Eye mode for new corrections and an Add Catchlight checkbox. CPU/GPU twins updated
together (equivalence covers both modes and the catchlight).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:43:57 -04:00
Brandon ThomasandClaude Opus 5.5 ebcc41825b LR-EYE-RED: red eye correction with automatic pupil detection
pipeline::redeye: inside the user's ellipse the pupil is found on the source image (a fixed
grid in normalized coordinates, so previews and exports agree): weighted centroid and area of
red-dominant pixels (or, for pet eyes, of the brightest ones). Pupil Size scales the detected
disc; rendering in scene-linear light after white balance neutralizes the red where it is red
(iris and catchlights stay) and darkens it. CPU (lin stage) and GPU kernel redeye_k with an
equivalence test (both the GPU and the CPU scene-linear paths). Indexed controls
redEye.<i>.pupilSize/darken, redeye.add/delete commands, the Red Eye panel (selected eye's
sliders, delete) and the loupe tool (drag an ellipse, click to select).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:37:02 -04:00
Brandon ThomasandClaude Opus 5.5 490fe943cd LR-EDIT-COLOR-POINTCOLOR: Point Color — sampled colours with shift, variance, range and visualize
Up to 8 OkLCh samples (pointColor.pick samples the colour after the mixer on a proxy render);
per sample hue/saturation/luminance shift, variance (compress/expand the spread around the
sample) and an overall + per-axis range with soft edges, applied after the colour mixer in
ColorOps and its WGSL twin (equivalence case). Indexed control specs
(pointColor.<i>.hueShift …) so develop.set/controls/MCP address each sample. Visualize range
is a render overlay (RenderRequest::overlay) drawn after either renderer: the sample's own
edit is left out and colours outside the range turn grey. Point Color flyout in the Color
section with swatches, eyedropper, sliders, visualize and delete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:18:28 -04:00
Brandon ThomasandClaude Opus 5.5 37d9ecf0e9 LR-EDIT-LIGHT-CURVE-REFINESAT: curve Refine Saturation
curve.refineSaturation (0..100, default 100 = the curve's own saturation): after the tone
curves, the chroma around luma is rescaled so saturation moves from the curve's towards the
pre-curve value, keeping the curve's tone. CPU (finish::refine_saturation) and GPU twin,
equivalence case, slider under the curve editor, crs:CurveRefineSaturation import.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:06:14 -04:00
Brandon ThomasandClaude Opus 5.5 48efc6df52 LR-EDIT-CALIB: Calibration — shadows tint and R/G/B primary hue/saturation
White-preserving primaries matrix (OkLCh hue rotation / chroma scale of each Rec.2020
primary) plus a luminance-preserving green/magenta shadows tint, applied in scene-linear
light before the tone map. CPU (colorops::calibrate) and GPU (finish.wgsl) with an
equivalence case; seven control specs (calibration.*), a settings group, crs: import
(ShadowTint, Red/Green/BlueHue/Saturation) and a Calibration section at the bottom of the
Edit panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:01:48 -04:00
Brandon ThomasandClaude Opus 5.5 570d03e202 M5.4c: prefetch filmstrip neighbours; GPU device created at startup off the UI thread
- Once the current photo's loupe render is done, the next and previous photos in filmstrip
  order get a background loupe job (Slot::Prefetch, priority below on-screen thumbnails,
  once per settings): the decoded 2560 px source is kept in the engine's bounded preview
  LRU and the view render is cached, so stepping to a neighbour shows its developed image
  in ~50 ms and then re-renders only on the GPU.
- lightcraft_gpu::warm_up (called when the app is created) builds the device and kernels on
  a background thread; gpu::ready() answers without blocking and ui.inspect uses it, so the
  UI thread never waits for device creation.
- docs/gpu-pipeline.md: opening-a-photo section.

Stepping through 24 MP raws in the loupe (real app): full render 220–580 ms → 45–62 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 09:23:36 -04:00
Brandon ThomasandClaude Opus 5.5 30aac43fab M5.2e: renders and exports use the GPU; render_bench GPU column
`media::develop` renders on the GPU when available (views, render_now, exports;
thumbnails stay on the CPU) and falls back to the CPU otherwise. `app.gpu
{enabled?}` toggles it at runtime, `LIGHTCRAFT_GPU=0` disables it for the process,
`ui.inspect` reports the adapter under perf.gpu.

Performance: dropped device buffers are recycled once their thread has submitted
(fresh 100-300 MB allocations per pass cost as much as the passes), Gaussian
passes ping-pong between two buffers and read all channels of a pixel at once,
and long box radii slide over longer chunks. LIGHTCRAFT_PROFILE prints real GPU
stage timings. render_bench prints CPU and GPU columns and the CPU/GPU difference.

24 MP ARW, loaded machine, min of 7: cold loupe 415 -> 32 ms, exposure drag
22-42 -> 3.9 ms, NR drag (1920) 301+ -> 18 ms, export render 1037+ -> 291 ms;
max 1 LSB difference. docs/gpu-pipeline.md describes the implementation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 08:19:44 -04:00
Brandon ThomasandClaude Opus 5.5 07c3f0261c M5.2d: geometry resample on the GPU
The source is uploaded once per view (kept with the GPU stages) and resampled on
the device: orientation as an integer pixel map, the Mitchell area prefilter
with the CPU's exact taps, then one bilinear resample through the
crop/straighten/flip affine or through the full warp (inverse perspective,
manual distortion, embedded DNG WarpRectilinear per plane, lateral CA, manual
and embedded vignetting gain, blank outside the source). `Frame::sample_plan`
(new) decides prefilter size and mode for both renderers; sources larger than
the device's buffer limit are still resampled on the CPU.

Equivalence: all 8 orientations with and without crop/straighten/flip, embedded
lens + perspective: max 1 LSB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 02:34:40 -04:00
Brandon ThomasandClaude Opus 5.5 2dfd65b39a M5.2c: mask evaluation on the GPU
Linear and radial gradients, luminance and colour ranges and brush strokes are
evaluated by a WGSL kernel per component, combined (add/subtract/intersect,
component and mask invert, amount) into one device buffer of alpha planes that
the per-pixel stage reads directly. Brushes use the CPU's dabs
(`masks::brush_dabs`, shared with the rasterizer) and the closed form of the
flow accumulation (1 − Π(1 − a·flow), capped at density). Sky / Subject /
Background and other heuristic shapes, and brushes of more than 4096 dabs, are
evaluated on the CPU inside the same render and uploaded.

New equivalence case with CPU-evaluated shapes, subtract/intersect and amount;
all cases stay at max 1 LSB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 02:32:50 -04:00
Brandon ThomasandClaude Opus 5.5 9ec34d774f M5.2b: spatial planes, white balance and noise reduction on the GPU
Gaussian blur (three box passes each way, running sums per 16-pixel chunk), the
separable resample with the CPU's exact taps (uploaded tables), the guided filter
and the fast guided filter (coefficients on the subsampled grid), log luminance,
dark channel + airlight (percentile of every 7th value read back), white balance
and luminance/colour noise reduction now run as WGSL kernels. A render records
everything into one command encoder and reads back only the output (and the
airlight samples); defringe and spot removal still run on the CPU when used.

Equivalence unchanged: max 1 LSB, mean < 0.001 LSB over all test cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 02:31:19 -04:00
Brandon ThomasandClaude Opus 5.5 c7a1e3e2f6 M5.2a: lightcraft-gpu crate (wgpu compute) with the per-pixel stage on the GPU
New L3 crate `lightcraft-gpu`: a wgpu device (Metal/Vulkan/DX12; none on wasm32),
WGSL kernels compiled once, buffer/dispatch/readback helpers, and a renderer that
runs the per-pixel stage (tone map, highlights/shadows, clarity/texture/sharpen,
dehaze, local mask terms, OkLCh colour ops, grading, vignette, gamut map, sRGB
encode, curves, grain) on the device. Earlier stages still run on the CPU and are
uploaded (per-stage hybrid). Stage results are cached per view next to the CPU
StageCache (new `StageCache::extension`).

The CPU pipeline stays the oracle and shares its resolved parameters with the
kernels: `finish::FinishParams` / `mask_terms`, `pipeline::plan` (stage-cache keys),
`local::plane_sigmas` / `nr_params` / `wb_matrix_for`, exact LUTs and OkLab
matrices exported for the shader prelude. Equivalence tests (skip without an
adapter): mean |d| < 0.5 LSB, max <= 3 LSB; measured max 1 LSB on Apple M4 Pro.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 02:29:54 -04:00