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>
* CI: dev profile keeps line tables for workspace crates and no debug info for dependencies
* Control server tests: persistent_requests_and_slow_replies_outlive_the_idle_read_timeout no longer races the idle timeout
Each request (and the closing half-shutdown) reaches the socket before the
worker reads, so on a slow machine only the reply waits outlast the 100 ms
idle timeout, as the test intends. The idle test sends its input the same
way, and the no-frames test allows 5 s instead of 2 s to give up.
* CI: one integration-test binary per crate
Each crate's tests/*.rs become modules of tests/it/main.rs (51 test files
into 13 binaries, plus 6 that stay their own). Inner #![cfg] attributes
move to the mod lines. Files that change state for the whole process keep
a binary of their own: codecs jxl_tail_heap and raw sr2_heap (dhat global
allocator), gpu shutdown, shutdown_first and memory, ui-egui shutdown.
Docs, comments and the GPU probe name the new paths and targets
(--test it <module>::); the CI report names an it binary's skips after
the crate.
* CI: tests under nextest, doctests apart, one wasm check
xtask ci runs the tests with cargo nextest run --lib --bins --tests when
cargo-nextest is installed, else cargo test with the same targets and a
one-line notice; doctests run after them with cargo test --workspace --doc.
The heif and hardware GPU steps use the same runner. Examples stay out of
the test build (clippy --all-targets still checks them). Concurrent tests
under nextest follow the RUST_TEST_THREADS cap (NEXTEST_TEST_THREADS).
The wasm check is one cargo check --keep-going over the same crates, and
clippy runs with --keep-going.
* CI: tests run under cargo test by default, nextest with LIGHTCRAFT_NEXTEST=1 (per-process scene cache)
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>
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>
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.
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>
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>
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>
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>
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>
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>
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
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>
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>
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>
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).
For Overlay::AllMasks, the masks buffer already holds each visible mask's
alpha back-to-back in the same order the single-mask overlay uses
(cx.slice(all, mi*n, n)). Read the whole buffer once with cx.read instead
of one synchronous read_plane per mask, then split it into per-mask Planes
on the CPU. Same bytes, but the pipeline stalls once instead of N times.
- view.maskOverlayAll: Cmd+O clashed with library.open; rebind to Cmd+Shift+O
- Masking panel tooltip now derives the key label from the binding (platform
aware: Cmd vs Ctrl) instead of hardcoding Ctrl+O; base string localized
- gpu/render.rs: rewrite the AllMasks alpha gather as a for-loop so it passes
cargo fmt --check (max_width 150)
- docs/parity-checklist.md: add LR-MASK-OVERLAYALL row
- locales: add 'Show All Mask Overlays' and 'Show all overlays' to de, es, fr,
ja, pt-br, ru, uk, zh-hans, zh-hant
`cargo xtask ci` points each test step at target/ci/skips-<step>.jsonl,
then probes the machine: OS, CPUs, RAM, toolchain, corpus and craft-fonts,
and the GPU through the ignored `gpu_probe` test of lightcraft-gpu (run
right after the workspace tests with --workspace, so it reuses their build;
xtask stays without wgpu). The run ends with the skips grouped by need and
reason and writes target/ci/report.json (schema 1: commit, dirty flag,
machine, capabilities, step results and times, skips) and
target/ci/summary.md for a PR (`--summary-md` prints it). `--hardware`
also runs the lightcraft-gpu tests once per backend that has a hardware
adapter. Nothing is uploaded; reasons are cut free of the workspace root
and home directory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
libtest has no skipped status and hides the output of passing tests, so a
test that returns early without a GPU, a corpus file, a craft-fonts face or
a model passed silently. `lightcraft-testkit` (the testkit slot the layering
table already reserves, dev-dependency only, std only) adds
`skip!(Need, "reason")`: it prints the note as before and, when
LIGHTCRAFT_SKIP_LOG names a file, appends one JSON line (need, reason, test,
binary, file:line). Plain `cargo test` is unchanged. The 61 existing skip
sites use it.
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>
The horizontal box pass gave each thread 16+ consecutive pixels to slide
a running sum over, so a workgroup's threads read pixels 16 apart. On a
Radeon 780M (DX12) that pass took 9-15x as long as the vertical one, and
it sits under every GPU Gaussian and guided filter (NR, texture, dehaze,
clarity/base). Each thread now sums one pixel's 2r+1 window: adjacent
threads read adjacent pixels. Same 64x4 workgroups, parameters and
bindings; box_v unchanged.
Measured on the Radeon 780M (6000x4000): guided filter at sigma 1.75
828 -> 146 ms, colour-NR Gaussian 495 -> 104 ms. Full-size heavy-edit
export of the CC0 Sony ARW: GPU 3.6-4.0 s -> 1.9-2.0 s (CPU 1.9-2.1 s);
GPU-vs-CPU output distance unchanged.
Adds gaussian_matches_cpu (GPU blur vs lightcraft_raster::blur::gaussian,
1-3 channels, partial workgroups, windows wider than the image) and
per-pass rows to bench_kernels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The loupe sized its slider-drag drafts by its own copy of try_render's
"CPU only" list (soft proof, HDR range). A new CPU-only overlay added to
try_render (e.g. #995's sharpening mask) would have left the loupe
drafting full size on the CPU for its first drag. lightcraft_gpu::handles
is now the single rule: try_render returns NotTried by it and the loupe
asks it, so the two can't drift apart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>