Commit Graph

6 Commits

Author SHA1 Message Date
Brandon Thomas 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 Thomas 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 Thomas 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 Thomas 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 Thomas 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 Thomas 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