Commit Graph
5163 Commits
Author SHA1 Message Date
Dylan M. Taylor 76b30801bd security(xtask): web --serve serves only plain files inside the site; linear asset globs; exclusive lock owner file (#3682)
* security(xtask): web --serve serves only plain files inside the site; linear asset globs; exclusive lock owner file

* address review: say so when the ci lock owner file cannot be written
2026-11-10 18:06:23 -05:00
Dylan M. Taylor 7e409a3706 security(engine): one unpack budget across a preset zip's packs; linear comment skipping; small zip errors (#3675)
* security(engine): one unpack budget across a preset zip's packs; linear comment skipping; small zip errors

* address review: charge a failed pack's unpacked bytes to the shared zip budget

* address review: charge a corrupt deflate entry's partial output to the shared budget

* address review: an entry past its stated size spends only what it unpacked
2026-11-10 18:06:04 -05:00
FredApps b188295ee4 Merge pull request #4048 from storytold/sonar/release-job-permissions
CI: release workflow grants write permissions to the release job only
2026-11-10 20:59:36 +01:00
FredApps 202021ddf5 Merge branch 'main' into sonar/release-job-permissions 2026-11-10 19:33:54 +01:00
FredApps e60a725133 Merge pull request #3632 from Yeetjof/feat/survey-layout
LR-VIEW-SURVEY: the survey fits its tiles to the photos' shapes, and tiles glide to their new places
2026-11-10 18:00:44 +01:00
Fridtjof MichelsandClaude Opus 5.5 e2d45b8818 LR-VIEW-SURVEY: merge main (#3842: photo_tile also returns the painted rect; the survey ignores it)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 16:03:14 +01:00
FredApps 993af1c343 ci(release): grant write permissions to the release job only 2026-11-10 11:11:01 +01:00
FredApps e9c29e2c5c Merge pull request #2599 from francoisgn/feat/issue-2592-lightroom-backup-zip
LRC-LIB-CATALOG-IMPORT: import Lightroom catalog backup zips directly (#2592)
2026-11-09 23:25:54 +01:00
FredApps 4ecbd194fd LRC-LIB-CATALOG-IMPORT: reconcile backup import with main
Preserve collection ownership, check extraction space, and resolve the upstream merge.
2026-11-09 22:14:17 +01:00
FredApps f6b57b0b59 Merge pull request #3630 from jeremyh/crystal-grain
LR-EDIT-EFFECTS-GRAIN: film grain from crystal layers (process V4), resolution independent, no GPU cost
2026-11-09 11:39:02 +01:00
Dylan M. Taylor d73c849ed1 Merge pull request #3996 from dotCooCoo/chore/craft-update-1b34eba41
Updates: take craft-update from craft-libs main at 1b34eba41
2026-11-08 23:45:33 -05:00
dotCooCoo c31f91c8fe Updates: take craft-update from craft-libs main at 1b34eba41 2026-11-08 19:47:39 -08:00
Dylan M. Taylor 2f4d095903 Merge pull request #3995 from dotCooCoo/fix/updater-starts-at-exit
Updates: the updater reports installing, ready, installed and failed, and installs at exit
2026-11-08 22:09:45 -05:00
dotCooCoo e62efc0af2 Updates: craft-update comes from storytold/craft-libs
The pin stays at the head of storytold/craft-libs#696. The commit is reachable from storytold, so the dependency no longer names the fork.
2026-11-08 18:33:46 -08:00
dotCooCoo 9e8a7b7c88 Updates: the updater reports installing, ready, installed and failed, and installs at exit
craft-update moves from a231e517 to the head of storytold/craft-libs#696.
The check result window now shows for every status but idle, so the first
start after an update reports that it installed or why it failed. Restart
to Update on a ready update asks the app to close.

The install helper now starts when the app exits. on_exit calls
Updater::start_pending_update through LightcraftApp on macOS, because the
process ends there without dropping the app.
2026-11-08 18:26:40 -08:00
FredApps 7e2b8a6b27 Merge pull request #3833 from ZIDAN44/fix/3776-one-selected-count
Issue #3776: one selected photo shows "1 selected" in the header
2026-11-08 17:06:27 +01:00
Luis b3b49987d4 Release: refresh contributor credits for each release (#2254) (#3898)
About > Contributors still showed an old snapshot, because nothing
regenerated contributors/contributors.json before a release. Opt in to
the shared refresh from craft-common-actions: a read-only credits job
regenerates the snapshot for the release commit, and the shared release
workflow installs it before every source build. The snapshot is not
committed back.
2026-11-08 02:12:52 +01:00
FredApps de096566ff Merge pull request #3887 from eliets2/fix/issue-2028-kelvin-white-balance-missing-for-dxo-dem
Cover DxO demosaiced Fujifilm DNGs for Kelvin white balance
2026-11-07 16:21:33 +01:00
FredApps 10db03b091 LR-IMP-FORMATS: withdraw compressed ORF decoding pending provenance review (#3916)
* LR-IMP-FORMATS: withdraw compressed ORF decoding

Remove the decoder introduced in #3775 pending provenance review while preserving uncompressed ORF layouts and embedded previews. Add unsupported probe/decode regressions, remove the prior provenance narrative, and restore camera coverage records to preview-only.

* LR-IMP-FORMATS: refresh ORF support status and render caches

* LR-IMP-FORMATS: document ORF withdrawal regressions
2026-11-07 15:56:03 +01:00
FredApps daa83c3a43 Merge pull request #3862 from eliets2/fix/issue-2475-lightroom-import-mask-issue
Honor inverted Lightroom masks when importing catalog edits
2026-11-07 15:38:42 +01:00
Jeremy HookeandClaude Opus 5.5 622c288621 LR-EDIT-EFFECTS-GRAIN: review fix (a draft waits when nothing can stand in)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 11:06:06 +11:00
Jeremy HookeandClaude Opus 5.5 ed10d4486c Merge main into crystal-grain; the crystal grain moves to process V7
v0.19.0 shipped V4 to V6 with value-noise grain, so those stay frozen and
the crystal grain becomes a process of its own. V7 is V6 plus the grain:
without grain it renders as V6 (`without_grain_v7_renders_as_v6`), and the
GPU equivalence cases now cover V6's value noise.

Merged with main's float output (`deep`, `hdr`): the value noise keeps its
clamped luminance; the crystal grain already clamps, and fades out at and
above SDR white. The finish kernel's failed-render check stays ahead of
the aux upload.

The test of unknown process numbers now takes one past the latest: 7 was
hard-coded, and is a known process now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 10:53:18 +11:00
Jeremy HookeandClaude Opus 5.5 a850d05fe4 LR-EDIT-EFFECTS-GRAIN: review fixes (crystal grain unit tests)
Direct tests for what was only covered through whole renders, or not at
all: slider clamping and NaN fallbacks, interp, normal_quantile, the
Poisson draws, the emulsion's mix of sizes per seed, the summed-area
table and box sums against brute force (seams, negative coordinates,
other periods) and the footprint limit.

A tile redrawn is now compared with the first one directly:
seed_picks_the_pattern compared a cached tile with itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 10:31:14 +11:00
eliets2 3b209a40b7 Cover DxO demosaiced Fujifilm DNGs for Kelvin white balance
DxO demosaiced DNGs from Fujifilm RAF files should offer the same Kelvin white-balance editing as the original RAF and Adobe demosaiced DNGs, instead of only a relative slider.

Treat a DxO make on a DNG as transparent when checking Fujifilm (and other listed) models in covers, so the Kelvin scale is offered for those files.

Test: dxo_dng_fujifilm_model_is_covered_for_kelvin failed before the change and passes after.
2026-11-06 23:58:35 +02:00
8a0b91a95c GPU float output for the HDR render (F32Hdr) (#3856)
* Render HDR rendition on GPU for HDR display drafts

HDR edits on HDR displays currently pay for a second CPU float render on every full render and show only SDR during drags, making slider drags several times slower.

Allow F32Hdr requests through the GPU path with a float output target matching the CPU HDR rendition, and let draft renders carry HDR again instead of being excluded.

Test: gpu_handles_f32hdr_renders_for_hdr_display failed before the change and passes after.

* Issue #2297: float output from the GPU's finish kernel for the HDR render

The kernel wrote 8 bits only, so `handles` taking F32Hdr left no float samples.
It now keeps values up to the edit's peak (highlight desaturation, vignette,
gamut map, curves above SDR white, grain, LUT profiles, as finish_with does),
encodes with the exact sRGB curve as a float render does on the CPU, and writes
three linear floats per pixel into `out`. Written words carry the sign bit, so
work that never ran still fails the render instead of reading as black.

hdr_histogram keeps its Quality::Draft exclusion: it renders on the CPU, and the
UI already runs a small render beside the drafts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:59:03 +01:00
lostwaldandFredApps 1b5b766513 WEB-WORKER-PARITY: compare serialized jobs with the CPU reference (#3782)
Fix #3778: native thumbnail jobs can use an already initialized GPU, making exact worker serialization assertions depend on test order. Keep exact image and histogram assertions against the engine CPU pipeline. The separate native window equivalence test is unchanged.

Validation: reproduced on clean main 56d1cc28 using serial web tests (37 pass, 1 fail), then all 38 web tests pass both serially and in parallel. Full cargo xtask ci passes all eight gates on parent 2e66a00d with craft-fonts and Metal (152 external-data skips). Latest main has separate dithering/cutoff-test failures tracked as #3780; no GPU disabling or assertion weakening used.

Assisted-by: OpenAI GPT-6.1 Sol

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-06 17:46:43 +01:00
FredAppsandFredApps 097dac4566 Stabilize sidebar month-name test fixture (#3859)
Inject an empty local folder list so asynchronous host discovery cannot move rows between queued clicks. Preserve both clicks and all accessibility assertions.

Fixes #3858

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-06 17:03:17 +01:00
92adb04782 Issue #3768: dialog_role tests draw glyphs only craft-fonts has as boxes when built without it (#3769)
Settings lists each language by its own name, and Georgian has glyphs only in
the craft-fonts faces, so egui_kittest's missing-glyph panic failed
settings_is_a_dialog_too in every build without CRAFT_FONTS_DIR. Allow missing
glyphs there, as tests/it/accessibility.rs does: these tests check roles and
titles, not glyphs.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-06 16:17:31 +01:00
e354de502b Use adapter texture limit for probed window device (#3853)
* Use adapter texture limit for probed window device

Mixed-DPI dragging can grow the window past 8192 px; the probed device should allow the adapter's full texture size so the window keeps rendering.

Change probe to request the adapter's max_texture_dimension_2d and add a GPU test asserting the probed device matches the adapter limit.

Test: probed_device_uses_adapter_max_texture_size failed before the change and passes after.

* Window device: the adapter's texture limit on GL too, which the conservative defaults would cap at 2048

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 15:15:23 +01:00
FredApps 45ba859f88 Merge pull request #3857 from storytold/house-rules
Copy the shared house rules into AGENTS.md
2026-11-06 14:58:43 +01:00
8f638b33bd Treat a first intersect mask component as its base selection (#3854)
* Treat a first intersect mask component as its base selection

A mask created with Intersect as its first component currently renders as empty, so the overlay and local adjustments never appear. Using that component as the base matches how Add and Subtract first components already behave.

Change evaluate_one so the first component always sets the base alpha regardless of its combine op. Add a regression test with a single Intersect linear-gradient component asserting the start side is selected.

Test: intersect_first_component_selects_its_shape failed before the change and passes after.

* Masks: the GPU treats a first Intersect as the base too, and the process doc says so

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Docs: what changes for masks that start with Intersect

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:46:11 +01:00
eliets2andeliets2 081190c92f Match renamed missing files by content when size is unknown (#3852)
Libraries created on another OS may have no recorded file size for some photos. After moving OS and renaming the file, Find Missing should still relink it by its content hash instead of leaving it missing.

Allow plan_find_missing to match renamed files by content hash when size is unknown by hashing unused candidate files, and add a regression test with a Windows-style path, unknown size, known hash and a renamed file in the searched folder.

Test: find_missing_matches_renamed_file_by_content_without_size failed before the change and passes after.

Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
2026-11-06 13:05:15 +01:00
eliets2 ba8caf8fa6 Honor inverted Lightroom masks when importing catalog edits
Users importing a Lightroom catalog see masks that darken or lighten the wrong part of the photo, even though the mask exposure value itself looks correct.

Carry the inverted flag through catalog mask orientation handling for develop and XMP masks, and add a regression test with an inverted radial mask.

Test: inverted_radial_mask_keeps_its_inversion_on_import failed before the change and passes after.
2026-11-06 13:49:40 +02:00
FredApps 52134ffc95 docs: preserve local guidance in house-rules rollout 2026-11-06 12:37:13 +01:00
a941d706c3 Picture Package and Custom Package pages can print cut guides at each cell's corners (#3843)
* Picture Package and Custom Package pages can print cut guides at each cell's corners

Picture Package and Custom Package pages can print cut guides at each cell's corners

Picture Package and Custom Package pages can print cut guides at each cell's corners

Test: picture_package_and_custom_package_pages_can_print_cut_guide failed before the change and passes after.

* Print: Cut Guides in the package preview, page panel in its own function, tests follow the guides

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Print: Cut Guides sit at each cell's corners, outside any border

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Print: shorten the scroll position comment to what the code does

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Print: back to one scroll position for the settings column

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 12:31:38 +01:00
FredApps 0b23cc6384 Copy the shared house rules into AGENTS.md.
The blocks come from craft-contrib at dde09ea31e2e457f5b4653a6e76f7cdf55944bdf. CLAUDE.md imports AGENTS.md. The house-rules workflow checks the copy.
2026-11-06 11:50:01 +01:00
d2bf0810bd Fix mismatched source/destination rects in before/after zoom window renders (#3842)
* Fix mismatched source/destination rects in before/after zoom window renders

Both panes of Before/After cut the zoom window from one rect but paste it over another, which is exactly the inconsistent-offset and artifact behavior reported in issue #1035; a headless test exercising click-zoom in both panes with a fixed camera raw would catch the regression.

Detail view: compute `visible` relative to `v.img` instead of `v.target` so the window's source and destination share the animated rect. Compare/Reference view: compute `visible` relative to `display` (the rect actually drawn) instead of `img` so texture-aspect fits land in the right place.

Test: compare_zoom_window_tracks_the_zoomed_pane failed before the change and passes after.

* Compare: keep before_target, return both rects so Survey's remove button stays on the photo

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Format

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Compare: the early return gives both rects

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 10:56:57 +01:00
a856df5f42 Write camera make and lens make to EXIF on export (#3828)
* Write camera make and lens make to EXIF on export

PTGui and similar stitching tools read EXIF Make/LensMake to identify camera and lens; without them, exported JPEGs from CR2 files are not recognized the same way as Adobe's export.

Add make and lens_make fields to catalog Meta, populate them from the raw's EXIF on import in meta_of, and write them as EXIF Make and LensMake tags in export_metadata.

Test: export_writes_camera_make_and_lens_make_to_exif failed before the change and passes after.

* Export: read Make and LensMake on import and write them, with Model on its own

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Export: strip the maker from Model only as a separate word

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 10:56:51 +01:00
9c07f427f0 Arm the window renderer marker before probing the driver (#3817)
* Arm the window renderer marker before probing the driver

The issue's own table says the window renderer is created without a crash marker, so a launch that dies in the driver writes nothing and repeats itself. The window does have a marker, but it is armed only after the risky probe has already run, which is exactly the case the reporter's Windows launch hit.

Arm the window-init.marker with the renderer name before each probe in pick_probed, so the marker on disk names the renderer being tried while the driver is touched, and re-arm it for the next renderer when a probe fails. The next launch then reads that renderer via take_failed and reports it (the existing 'the window could not start with its ... renderer last time' notice) instead of failing silently again.

Test: the_window_marker_is_written_while_the_renderer_is_probed failed before the change and passes after.

* Window marker: fix the test's closing brace, skip it under forced glow, sync the marker to disk

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Format

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 10:46:24 +01:00
09f0550751 Face boxes: drag a box's body to move it (#3846)
* Add test that dragging a face box body reports a move edit

The test exercises region_overlay through the existing RegionEdit::Resize enumeration and CanvasMap/Photo types, asserting that a drag inside a box body reports a move. It fails on current main because region_overlay returns None for such drags.

Add a #[cfg(test)] mod move_tests with a single test that builds a CanvasMap and Photo with one face region, simulates a pointer drag from the box body centre to a new position via egui RawInput, calls region_overlay, and asserts it returns Some(RegionEdit::Resize(_, moved)) with size preserved and rect clamped into [0,1].

Test: a_moved_face_box_keeps_its_size_and_stays_inside_the_photo failed before the change and passes after.

* Face boxes: body drag in its own function, no edit for a box that ends where it began, tools keep the drag

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 10:29:52 +01:00
b2007e06a9 Remove the dead Cloud button from the top bar (#3832)
* Remove the dead Cloud button from the top bar

A clickable top-bar button that does nothing confuses users and contradicts LightCraft's no-cloud design; removing it ends the confusion while save status stays visible through toasts (lib.rs save_status) and library.info.

Drop the cloud entry from the top bar's right-hand icon loop in topbar.rs, which also removes its tied warning badge; nothing else in the repo references it.

Test: top_bar_has_no_cloud_widget failed before the change and passes after.

* Top bar: the cloud icon only shows as the warning that saving is failing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 10:16:52 +01:00
2a35db6bcf Add flash, exposure bias and serial rules to smart albums (#3821)
* Add flash, exposure bias and serial rules to smart albums

Smart albums cannot filter by flash fired, exposure bias or camera serial even though the file metadata is already parsed.

Add flash, exposure_bias and camera_serial to Meta, copy them in meta_of and on reload, and add flash, exposureBias and cameraSerial rule fields evaluated in Rule::matches.

Test: flash_exposure_bias_and_serial_rules failed before the change and passes after.

* Rules editor: Exposure Bias takes negative values

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 10:14:19 +01:00
3943279105 Colour noise reduction: joint linear chroma filter with speck brake, CPU and GPU (#815) (#1348)
* WIP: colour NR v2 prototype (env LIGHTCRAFT_NR_V2, CPU only)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: colour NR v2 on by default (LIGHTCRAFT_NR_V2=0 turns it off), GPU renders read back for it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: cache renders with the new colour NR apart from older ones

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: colour NR v2 on the GPU (fused down / apply kernels), coefficient grid sigma / 2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: colour NR v2 on the CPU without full-size intermediates (row-streamed passes)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: report peak device-buffer use per GPU stage with LIGHTCRAFT_PROFILE

* WIP: colour NR v2 linear mode (NRV2_LINEAR=1: c - Y, additive, noise model), Jensen term, mean-drift probe

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: colour NR v2 noise model with equal-count luminance bands and a non-negative fit; probe asserts the mean over 3 noise realisations

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: colour NR v2 linear mode by default (per channel, K 64, coarse pass smooths only the coarse part); CPU on GPU renders; real-colour probe

Known: tests_window::noise_reduction_does_not_depend_on_the_window fails (the noise is measured in the
rendered window; the square-root mode failed it too).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: colour NR v2 joint guide (NRV2_JOINT=1, opt-in), luminance guide knobs, leaf-next-to-black probe, camera-look guard from 858

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: colour NR v2 reads its env knobs once (per-pixel env reads made the linear mode 11x slower); joint stage timings with LIGHTCRAFT_PROFILE

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: joint colour NR without redundant passes (fused paths + next weights, none after the last scale, delta-1 weights from the path), two-component buffers, parallel reconstruction

Bit-identical to 8b89526d (test joint_matches_the_reference_bit_for_bit against a test-only copy).
Cat 32.7 MP, CPU: 6.2 -> 5.0 s at Color 25, 7.0 -> 5.6 s at 100; working buffers 2.36 -> 1.44 GB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: joint colour NR with separate H/V slice loops and zero-difference shortcuts (no division, weight 1 without exp)

Still bit-identical to 8b89526d. Interleaved 24 MP bench: 2.72 s vs 4.52 s for the reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: joint colour NR on the GPU (nrcj.wgsl), default on; whole-image noise model from samples, halo bands above 2 GB

Kernels mirror nrc_joint step for step (same scale plan, noise model fitted on the CPU from GPU samples).
Cat 32.7 MP on a Quadro M2200: wb/nr 1.2-1.3 s vs 4-5 s CPU; against the CPU max 1 LSB; bands = single.
Waits after dropping large buffers (freed memory otherwise piled up to the whole 4 GB). Cache key includes joint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: GPU joint NR plans its bands from a budget that includes the output buffer and the device buffer limit; halo 2*sum(delta) + max delta + 8; a band that cannot fit runs only this stage on the CPU

Bands vs one piece: at most 1 LSB in a few dozen pixels (running-sum boxes round differently per band start).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: GPU joint NR budgets its bands from the free device memory where it can be asked without unsafe (nvidia-smi, amdgpu sysfs), cached 3 s; fixed 2 GB otherwise

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: docs/nr-color-v2-history.md: every approach tried, measured and dropped, speed numbers, known limits (no outlier brake yet)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: history doc: speck behaviour stated qualitatively until measured on full-size renders

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: joint colour NR speck brake: compact colour specks (chroma far past the noise, small luminance step, shift not reaching the 5x5 border) lose their colour before the guide is built; weak at Color 25, full at 100; CPU and GPU

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: joint NR CPU 5.2 -> 3.5 s (33 MP), bit for bit: guide as planes with branch-free distance rows, exponentiated paths, pilot straight into planes, speck brake in place with a min/max pre-test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: joint NR GPU: path and weight pairs instead of quads (48 instead of 60 bytes per pixel, 33 MP in one piece), speck and pilot kernels without loop-indexed private arrays, noise fit cached for identical samples; Quadro M2200 2.15 -> 1.47 s, HD 630 5.66 -> 4.26 s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: speck brake: compactness from the second largest border share (a line or edge crosses the 5x5 border twice; one noisy border pixel no longer protects a speck)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #815: colour noise reduction as process V3

The joint method (linear R−Y / B−Y, edge-aware guide, speck brake) is process V3 and the latest; edits on V1 and V2 keep main's blurred chromaticity. The prototype switches (LIGHTCRAFT_NR_V2, NRV2_*) and the square-root and per-channel variants they selected are gone, with their GPU kernels; nrc_params uses the former defaults as constants, so V3 renders as the prototype did. The GPU falls back to the CPU joint filter when no band fits. vram is native only (wasm build). Docs: process-versions, nr-color-joint, parity checklist, what's new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #815: process routing test initialises its settings in place (clippy field_reassign_with_default)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #815: one GPU band plan at a time, with a reserve for every render on the device

An export batch renders two photos side by side (`export_parallelism`). Each joint colour NR planned
its bands from the device memory free at that moment and counted the shared buffer pool as its own,
so two plans made at once could together ask for more than there is. Measured on a Quadro M2200
(4 GB): two 33 MP exports side by side peaked at 3761 of 4096 MiB.

The plan and its bands now run under one lock (`BANDS`), and the budget leaves `VRAM_RESERVE` for
each render on the device (`RENDERS`), not just for this one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #815: the joint colour NR's noise fit and pilot no longer switch at a hard bound

With main's preview-scaled luminance NR (#960) the CPU and GPU renders of a 640-px preview differed
by up to 10 levels (gpu equivalence `output_spaces_match`). Their images after luminance NR differ by
9e-7 rms, but one luminance band of the noise fit sat at exactly 4 times the first fit: the CPU's
samples dropped it, the GPU's kept it, and the fitted noise came out 2.6 % apart. The pilot's axis
choice then switched at a few pixels, and the edge-aware weights carried that on.

- robust_line: a band's weight fades to 0 between 4 and 5 times the fit instead of dropping at 4
  (identical below 4).
- pilot_pick: the smoother-axis mean fades in between 0.8 and 1.25 times its bound instead of
  switching at it (identical outside), on the CPU, in nrcj.wgsl and in the test reference.

All 19 CPU/GPU comparisons of the equivalence suite are within 1 level again. The new test
noise_line_does_not_jump_when_a_band_crosses_the_outlier_bound uses the bands of that preview and
fails on the old fit (5.6 % jump).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #815: the joint colour NR measures the whole frame's noise and is keyed by process

A window measured the noise model on the pixels it held, so a loupe window
could smooth differently from the same pixels of the full render or the
export. The model now comes from fixed tiles of the whole frame
(`lightcraft_pipeline::colour_noise`, `nrc_joint::tiles`), the same for every
window, for the CPU and the GPU, and is kept in the stage cache.

The linear stage's cache key now includes the colour filter in use, so a
process change (Update to Current Process, undo) no longer reuses the other
filter's denoised image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M-NR: deep windows match the whole render; the VRAM query no longer stalls renders

- A U16 / F32 window with the joint colour NR is grown by the filter's reach and
  cut back, like an 8-bit one (DeepImage::crop).
- The free device memory is refreshed in the background once the cached answer is
  stale; only the first question for an adapter waits. The joint NR asks before it
  takes the BANDS lock.
- nvidia-smi starts without a console window on Windows (CREATE_NO_WINDOW).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M-NR: the VRAM query finds the right card and keeps one answer per adapter

- A card is found by its PCI address (wgpu device_pci_bus_id, nvidia-smi pci.bus_id,
  the amdgpu sysfs directory) where both sides report one; else by a device id that
  only one card has. Two cards of one model without an address give no answer, and
  the fixed band budget applies.
- Each adapter keeps its own cached answer, so a late background refresh cannot
  replace another adapter's answer. A refresh not done after 10 s is retried.
- The cache tests wait on signals instead of fixed sleeps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M-NR: the VRAM cache tests no longer depend on timing

The two cache tests used a 20 ms TTL: on a slow machine a refreshed answer could go
stale again between assertions and start an extra query. They now use a 30 s TTL and
age an answer explicitly (Cache::expire, test only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU: allow joint_band's eight arguments, as color_nr_joint does

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-06 09:44:58 +01:00
FredAppsandClaude Opus 5.5 472560b309 Tests: clipping warnings crush the shadows with Blacks (#3839)
Main fails this test: Exposure -5 alone no longer leaves a channel at 0 in the
demo photo.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 02:36:08 +01:00
68b464011f Import review: an optional batch-edit recipe for newly imported photos (#2940)
* Import: an optional batch-edit recipe for newly imported photos

library.import takes a recipe, a list of develop control ids and values.
Each control is set, clamped to its range like a slider's, on every photo
the import actually adds, after the XMP sidecar, the camera default and
the chosen preset, as one more History step named Import adjustments.
Photos that are skipped as duplicates, restored from Recently Deleted or
joined from a browsed folder are not touched, as the preset and keywords
never touch them.

A recipe names plain adjustments only. White balance is refused, because
a temperature means Kelvin on a raw with colour matrices and a shift from
the as-shot white on a rendered photo, so one number cannot name the same
look for a whole batch, and the as-shot white balance of a raw is never
touched. Geometry and crop controls are refused, because the slider
refits the crop around a new angle and a batch write cannot. Per-photo
controls such as pointColor.0.hueShift are refused, because a write to a
photo without that instance is dropped. The rule is on the control's
spec (a control of the shared list, outside the Geometry section, not
white balance, and one that set accepts), so new controls follow it. An
id named twice counts once, the last value winning, and a list longer
than the control list is refused. ImportJob::new checks a recipe built in
code the same way, before any file is read or placed.

preset.saveRecipe saves a recipe as a user preset that stores only the
settings the recipe names, including a control the recipe sets to its
default, and refuses a recipe that stores nothing. The preset gets its id
the way imported presets do (user.<group>-<name>, then -2, -3 ...), so no
two presets share one: an id from the list length repeats once a preset
is deleted, and the first preset with an id is the one that gets applied.
A name or group over 100 characters is refused, and saving the same
recipe under the same name and group again returns the preset that
exists.

* Import recipe: tests

Cover a recipe on import (the values, the Import adjustments step, a
render, one undo step), the refusals (unknown ids, malformed items,
per-photo controls, geometry and crop, white balance, too many entries,
by the command and by ImportJob::new), clamping and a repeated id, a
raw's as-shot white balance staying as imported, a saved recipe preset
giving the same settings as the recipe (including a reset to a default),
the photos the recipe leaves alone (duplicates by path and by content, a
restored photo, a browsed photo that joins; a photo imported afresh gets
it), preset ids staying distinct after a delete and surviving a reopened
library, and both commands being listed with their parameters.

* Import review: a Batch edits section

The import review gets a collapsible Batch edits section: sliders for
the everyday adjustments (exposure, contrast, highlights, shadows,
whites, blacks, vibrance, saturation, clarity, texture, dehaze,
sharpening, grain, vignette). The controls moved off their default go to
library.import as its recipe, only when there are any, and a summary
beside the section counts them. The recipe stays in the dialog until the
import runs, so cancelling changes neither the catalog nor the presets.

Save as Preset, which needs a name and a recipe, saves the recipe with
preset.saveRecipe. It leaves the review's own preset choice as it is, so
a preset picked for the import is never replaced by the saved one. The
name goes in a TextField, as new text boxes do.

The summary line is a new tr_format message, added with a translation to
every formats catalog; the other new messages are plain and show in
English until a catalog translates them.

* Import review recipe: tests

Unit tests hold the dialog to the engine: every control it offers is one
a recipe may name, a slider moved back to its default leaves the recipe,
and start passes a recipe to library.import only when the dialog has one.
Driving the real review, Save as Preset saves nothing without a name or
without a recipe and with both saves exactly the recipe (leaving the
review's preset choice and the catalog alone), cancelling the review
discards the recipe, and confirming imports the photos with it as one
undo step, or without an Import adjustments step when there is none.

* Import recipe: tests for the disabled Save button and MCP reachability

The disabled state of Save as Preset was not pinned: the engine refuses
a nameless or empty recipe anyway, so the preset count stayed the same
even with the button always on. The test now also expects no error
toast, which only an enabled button that asked the engine would raise.

A new MCP end-to-end test drives both commands through the public
tools: list_commands documents them, run_command takes library.import
with a recipe (a recipe naming white balance is a tool error that
imports nothing), get_develop shows the values on the imported photo,
and the generated cmd_preset_saveRecipe tool saves a recipe.

* Import recipe: follow main's register(ui) and Id::unique after the rebase

Upstream main changed widgets::register to take &egui::Ui (every other
call site passes ui) and deprecated egui::Id::new in favour of Id::unique
(same hash, so the stored open/closed state is unchanged). Adapt the
batch-edit recipe section so lightcraft-ui-egui compiles again.

* docs: parity row LR-IMP-RECIPE for batch edits during import

* Keep HDR resets when a recipe is saved as a preset

A recipe that resets an HDR control to its default was dropped when it was saved as a preset, because the HDR subtree is left out of the settings JSON while it is the default. The recipe's settings are now compared with the HDR subtree always present, so the reset is stored alone and next to other adjustments. Round-trip tests apply the saved preset to settings with a non-default headroom.

* Address review findings

* Address review findings

* Import review: the imports the merge left in body.rs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 01:23:04 +01:00
NetterSilverandClaude Opus 5.5 826875470a Paint clipped pixels red/blue when clipping warnings (J) are on (#3638)
Fixes #3620

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 00:35:26 +01:00
0429844cd9 never-crash: deny the panic lints in every crate root; GPU asserts become fail paths (#2137) (#2170)
* never-crash: deny the panic lints in every crate root; GPU asserts become fail paths (#2137)

The never-crash policy was documented but not machine-checked for the two largest crates:
engine and meta carried only #![forbid(unsafe_code)], so a future unwrap/expect/panic would
pass cargo xtask ci. Both crate roots now also carry
as does xtask (previously un-annotated); the guarded expects in meta/src/xmp.rs and xtask's
parity.rs / root() now use the documented #[allow(clippy::expect_used)] + invariant form.

crates/gpu: the three production asserts become fail(FailKind::Fatal, ..) + return, so a
binding-count mismatch, an over-long readback, or a wrong-length curve fails the render over
to the CPU instead of crashing (finish_and_read also early-returns on len == 0, which would
have issued a zero-size copy_buffer_to_buffer that wgpu rejects).

xtask: new roots check (in ci) verifies every workspace lib root and every binary main.rs
carries both attributes; crates/sysmem is the documented exception for forbid(unsafe_code)
only. A panic in placement or readback is a fail, not a crash.

* audit fixes: roots covers binary and cdylib/rlib roots; render gates on failed() after finish_block (#2137)

Two reviewers audited the first commit; both highs it found are fixed here:

- roots scanner: the by_ref().any()/any() pair consumed the kind iterator hunting for
  "lib", so every bin-only root (both app mains, the sam3-worker bin, xtask itself) and the
  cdylib/rlib web root was silently unchecked — the check covered 29 of the 33 real roots.
  Kind detection now collects the kinds once and matches lib/rlib/dylib/cdylib/staticlib,
  with a root_paths regression test over bin, lib and cdylib/rlib targets; unreadable root
  files now fail with the I/O error instead of a fake policy violation.
- render.rs: the caller gates on failed() right after finish_block, so the malformed-curve
  fail path can no longer reach the band loop's p[y0_at] index on the empty block.
- finish_and_read: the len == 0 path now submits the encoder before returning zeros, so a
  failure staged in the same render stays detectable (the old zero-size copy failed loudly
  via the error scopes; a bare return would have hidden it).
- docs: finish_block and finish_and_read state their failure contracts; USAGE and AGENTS.md
  name the new roots step in the ci enumeration.

* xtask: check a library root at the source path that cargo metadata reports

The roots check read src/lib.rs for every library target, so a package that sets its library path was checked on the wrong file. Libraries now use the reported source path, with the same default as binaries when none is given.

* xtask: match the crate-root deny lints by whole name, ignoring a trailing comment

A lint named only in a trailing comment, or a lint whose name contains another (clippy::panic inside clippy::panic_in_result_fn), satisfied the deny check. Lint names are now compared whole and a trailing comment is ignored.

* xtask: read the crate-root attributes from the header, not a fixed 30 lines

The roots check read only the first 30 lines of each crate root, so a root whose attributes came after a longer doc header was reported as missing them. The check now reads the inner attributes at the top of the file: it skips blank lines and comments, reads each attribute whole, and stops at the first item. A multi-line list is read in full and a commented-out attribute does not count.

* xtask: reword two comments in the roots check

Comments only: the lint-name match and the source-path default are described without naming a lint or a build tool. No code changes.

* xtask: report a root attribute that overrides the required denies

* xtask: accept a runtime manifest dir only when it has a parent

* xtask: render the overridden deny attribute without literal braces

The write! escaped the braces around the substituted attribute and lint,
so the violation printed `#![{allow}({clippy::panic})]`; assert the
rendered message in the allow-override test.

* Tests: new test helpers meet the crate-root panic lints

Main added test helpers after this branch; the deny at the crate root now
applies to them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* roots: crates/wincolor is the second crate allowed unsafe

Main added it for the Windows colour APIs after this branch; its module docs
already say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 00:29:41 +01:00
FredAppsandClaude Opus 5.5 96f3fd2d61 Tests: the next folder drop gets 20 seconds to land (#3834)
It checked the file on the frame after the drop, which failed on busy Windows
runners.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 23:45:53 +01:00
dcc8a62910 ui: first-run onboarding tour and What's New after an update (#2206)
* ui: first-run onboarding tour and What's New after an update

The very first launch now opens a short Welcome tour (six pages over the
real views: import, the Photo Grid, Detail, Assisted Culling, export and
print, and driving the app with agents over MCP), and after the app has
been updated the existing What's New dialog opens once on its own. Both
are skipped once the running version has been seen.

- UiState.seen_version: the last version whose startup dialog was shown
  (serde default/skip, so older ui.json files read as None = first run).
- new startup module: parse_version, startup_dialog (FirstRun only when
  nothing was ever seen; WhatsNew when the version changed, comparing
  through parse_version and falling back to string difference when either
  side is unparseable), and open_at_startup — headless hosts (snapshots,
  scripted runs, tests: session-local state) never gain a modal.
- UiState::record_startup_dialog_seen records on show, so a crash
  mid-dialog does not re-trigger it on every launch; ui.json saves every
  few seconds and at exit, as before.
- Dialog::Welcome renders the tour with Back / Skip / Next (Get Started on
  the last page); every string goes through tr(), every menu path names a
  real command (file.addPhotos, view.photoGrid, view.detail, dialog.cull,
  dialog.export, dialog.print).
- the decision runs once on the first frame (ui-egui, so desktop and web
  both get it); headless hosts and already-seen versions open nothing.

* ui: the integration harnesses are headless hosts

The accessibility and docking tests build the app directly and drive it
through logic() and ui(). On a fresh UI state the first frame therefore
opened the Welcome tour: its modal took the Tab focus the accessibility
tests walk (canvas_tool_sliders_are_reachable_with_tab,
tab_walks_the_right_panels_top_to_bottom) and the pointer the docking
drag aims at (real_drag_float_close_reopen_and_state_reload_keep_photo_tool_mode).

Mark both harnesses as headless hosts, as the Headless harness already
does: a headless host never gains a first-run modal.

* ui: translate the first-run tour into the 13 catalogs

The 21 strings of the first-run tour (its six pages, its window title and
its Next and Get Started labels) were in no catalog, so every language
showed the tour in English. They are now in each of the 13 catalogs under
crates/ui-egui/locales/, with the placeholders and punctuation of the
English kept: the ellipses, the menu separators, the middle dots, the
parentheses around shortcuts and the final full stop. ka.json stays
key-sorted; the other catalogs get the keys appended.

WELCOME_TITLE, WELCOME_NEXT and WELCOME_START name the three labels that
are not part of WELCOME_PAGES, so one test can check all 21 keys:
the_welcome_tour_is_translated_in_every_catalog. It fails with the catalogs
as they were.

The translations were written without a native speaker's review and need
one for each language before release.

* ui: translate the first-run tour into the four new catalogs

main added sv, fi, id and vi after this branch was cut, and
the_welcome_tour_is_translated_in_every_catalog iterates Locale::ALL, so
the tour's 21 keys were missing there. Same block, same place (after
"Visualize HDR"), in each catalog's own terms.

* ui: the lifecycle gesture test is a headless host

main's egui-037 lifecycle tests (added after this branch was cut) build
the app directly and drive it through logic() and ui(), so the first
frame opened the Welcome tour there: its modal took the dialog the
delivered gesture under test opens
(a_delivered_gesture_may_change_the_state_it_acted_on saw the tour where
it expected View Options).

Mark that one test's app as a headless host, as the accessibility and
docking harnesses already are: a headless host never gains a first-run
modal. The flag stays scoped to this test because the file's screenshot
tests exercise the headless-capture fallback, which a headless host
short-circuits.

* ui: build the tour's shortcut chords from the platform bindings

WELCOME_PAGES hard-coded Cmd+Shift+I, D and Cmd+P in its prose, and the
renderer translated those sentences verbatim, so Windows, Linux and
FreeBSD users were shown keys the app does not bind. The three pages
that name a shortcut now take the command's live binding at render time
(shortcuts::shortcut_of) and paint it the way the menus paint theirs
(menubar::shortcut_text: a symbol chord on macOS, Ctrl+... elsewhere);
no key is written in the source strings any more. A command whose
binding was removed shows the menu path alone.

The chorded sentences move from the message catalogs into tr_format!
messages (placeholders are format!-checked in every language), translated
in all 17 catalogs' -formats.json; the catalog keys stay, chordless, as
the no-binding fallback, and the_tour_is_translated_in_every_catalog
pins both per language.

* ui: the tour's Develop page names the Before/After command, not the Y key

The page's prose still hard-coded Y as the before/after key: remap or remove
that binding and the tour sends users to a command that no longer runs. The
body now names View › Before/After Side by Side the way the other pages name
their commands, and every catalog translates the new sentence with its own
menu label.

* ui: record the startup dialog once its input is not blocked

* Keep headless UI tests independent of onboarding

Assisted-by: OpenAI Codex

* Tests: integration harnesses mark the app as a headless host

Services has no headless field; the app does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tests: Find Similar waits for the worker to set the row total

It lost that race twice on the Windows gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 23:40:00 +01:00
Zinadin Zidan c97fee7b2f Grid: the library header's selection count is cut, not clipped, when it is wider than the header (#3776)
In the library view the count was painted right-aligned at any width, so at
the narrowest pane a long count started outside the header and lost its
beginning. It now truncates with "..." and shows the full text on hover, as
the folder header's count already did; the title still gives way first.
The header moved into its own function so a test can render it at a set
width, and the folder view's one-photo count is tested too.
2026-11-06 04:09:48 +06:00