mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-13 17:31:09 -06:00
c31f91c8fec7843cda6078754363b7de34fea02b
511
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
84a6f5674c |
Give the pipeline one exact output size instead of re-fitting (#3749)
* Give the pipeline one exact output size instead of re-fitting Export and the pipeline each compute the output size independently from two descriptions of the photo's shape, so an already-chosen export size can come back a pixel short when the pipeline re-fits against a reduced source whose aspect rounds differently. Add an exact output size to RenderRequest, make plan take it unchanged via an exact Frame::sized mode, drop the aspect-tolerance guard, and have export resolve one canonical size before calling the pipeline; add a unit test asserting plan returns the requested size unchanged when it matches the frame aspect. Test: plan_preserves_matching_box_size failed before the change and passes after. * Honor exact output dimensions for exports Assisted-by: OpenAI Codex * Export: an exact size does not stretch a source of another shape A raw whose decode failed can fall back to an embedded preview with a different aspect. The exact box now stands only against a proxy that differs by rounding. 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> |
||
|
|
e50db33453 |
Cut edited thumbnails from the decoded source, not the file (#3810)
* Cut edited thumbnails from the decoded source, not the file In Detail view the decoded source of the photo on screen is held at full size, but the thumbnail branch of `source_ref_plain` only looks for a preview-level source, so each edit re-queued a thumbnail that decoded the original again —1.4 GB of gate usage for one 489 MB file, matching the reported stall on the SMB share. When building a thumbnail job, reuse the photo's decoded source at whatever level it is cached (preview or full), fitting it to the thumbnail's edge, so an edited photo's thumbnail no longer re-reads the original file while its source is already in memory. Test: a_thumbnail_is_cut_from_the_decoded_full_source failed before the change and passes after. * Reuse decoded sources for thumbnail rendering Assisted-by: OpenAI Codex * Web worker: box the fitted source, as SourceRef::Fit holds it 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> |
||
|
|
326b48286e |
Merge the host bridge's cross-origin check into page mode
Bring #2315's current head onto the page-mode branch, including the check that a cross-origin parent gets no bridge. ?host=parent wins when a page also sets data-host="page". A page-mode event is typed by its name, listeners are Fn, and dirty state is recorded before the synchronous send. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
d73cf00ca8 |
Merge main into the host bridge
Keep the current changelog and record the embed under Unreleased. A cross-origin parent gets no bridge, so exports keep downloading; an opaque origin still gets none. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
bb0f0d5e11 | Merge storytold/main into feat/windows-monitor-profile | ||
|
|
d729c80a5a |
Detail: Remove Hot Pixels, on for newly imported raws (#1345) (#1350)
* WIP: suppress isolated hot photosites in the raw mosaic (#820) Noise model fitted to the image, candidates 5 sigma above every same-colour neighbour, kept when two or more direct neighbours are lit (real light); adjacent hot pairs are replaced too. LIGHTCRAFT_HOT_PIXELS=0 turns it off, LIGHTCRAFT_HP_DEBUG=1 prints statistics. Renders are cached apart from older ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP: hot pixel suppression off by default (LIGHTCRAFT_HOT_PIXELS=1 turns it on) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * #1345: mosaic outlier filter: cache key no longer names the #858 comparison switch (merged upstream) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * #1345: mosaic outlier filter: fixed thresholds (no LIGHTCRAFT_HP_K / _SPOT knobs), statistics under LIGHTCRAFT_PROFILE Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * #1345: Remove Hot Pixels, a box in Detail ▸ Noise Reduction, on for newly imported raws `Detail::nr_outliers` replaces single outlier photosites in the raw mosaic before demosaicing. It is true in `DevelopSettings::for_raw` and false by default, so edits saved before the box existed keep their look. The loaders take the flag (no more `LIGHTCRAFT_HOT_PIXELS`), a decoded source records it, and a cached raw source decoded with the other setting is decoded again. The box shows for raws only (`check:detail.nrOutliers`). * #1345: Remove Hot Pixels off keeps old settings' JSON; smart previews fit either setting - `detail.nr_outliers` is left out of the stored JSON when off, so settings saved before it keep their hash and cached thumbnails. Copy / paste, presets (also at an amount), preset updates and Auto Sync work on `DevelopSettings::to_json_for_merge`, which states it, so turning the box off still carries over. - `DecodedSource::fits_outliers` accepts a smart preview (`from_proxy`): it has no mosaic, so an offline raw with the box on keeps reusing the decoded proxy. (review of #1350) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * #1345: the Detail eye and Reset Detail switch Remove Hot Pixels off `DevelopSettings::removes_hot_pixels` (the box and the Detail section's eye) decides the raw decode in the app, the web worker and the thumbnail bench, so a Detail section that is off contributes nothing here either. Reset Detail resets the box to the general default (off), like the sliders it resets. (review of #1350) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * #1345: the hot pixel limit counts only replaced photosites, and can be set The 0.2 % limit was checked against the candidates before real light was filtered out, so a dense star field reached it with its stars alone and kept its hot pixels too. It now counts the photosites that would be replaced, as the module docs say. The limit is now a parameter (RawImage::suppress_hot_pixels_with, default HOT_PIXEL_MAX_SHARE) and a setting: Settings > Performance > Hot pixel limit, applied with the new command app.hotPixelLimit. Changing it forgets the decoded sources and changes the render cache keys of photos that remove hot pixels; at the default every key stays as it was. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * #1345: the app's default hot pixel limit is the default The setting is an f32: 0.2 % read back as 0.2000000029… % was a new limit, so every start forgot the decoded sources and changed the render keys of photos that remove hot pixels. A value within a millionth of the default now is the default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Format the imports * Web loader test states what it decodes with * Format the media exports --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> |
||
|
|
c18081f0b9 | Merge storytold/main into feat/windows-monitor-profile | ||
|
|
61005f3dc1 |
Culling: the loupe uses built previews; photos prepared ahead and in the background (#3077)
* PERF-CULL-0: ui.inspect reports how the loupe showed each photo it was stepped to
perf.loupeSteps records, per step, the ms from the step to the first frame
with anything of the photo, with a full-size picture (camera JPEG, cached
or rendered) and with LightCraft's own render, measured on the frames the
app draws, so culling benchmarks exclude the control channel's round trip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 95f0343848fc0a238f80f07ceb292eaf6779aeaa)
* PERF-CULL-1: browsing a folder shows its first photos within a second
The Local view read and hashed every file of the folder before showing any
of them (2,933 EOS R5 C CR3s: 30.6 s to the first photo). It now hands the
files the library lacks straight to the import, ordered the way the view
sorts them (file time for the date sorts), which adds them batch by batch:
first photo 0.9 s, twelve thumbnails 1.4 s, the whole folder in the same
~34 s as before, in the background.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit b766558e7ef4f802919a144da53e444f70e0186b)
* PERF-CULL-2: the loupe makes the next photos' camera previews ahead of the step
Culling steps through photos, and each step waited for the next raw's
embedded JPEG to be read and decoded (p50 187 ms, p95 332 ms on EOS R5 C
CR3s) and then for a full raw develop. Now:
- the loupe's stand-ins for the next five photos in the direction of
travel and the one behind are made ahead (Slot::QuickAhead, priority
just below the loupe's own render) and taken over at the step, so a
step shows the camera JPEG at once (p50 0 ms);
- a slot keeps its photo while the photo stays wanted, so photos that
shift places as the user steps are not made twice;
- decoded camera JPEGs stay in the memory LRU (never on disk), so going
back shows them again without a decode;
- while an arrow key is held (a photo left within 0.25 s), the raw
develop waits until the photo has stayed 0.12 s, and a render queued
for a photo already left is dropped, so the workers make the previews
instead of renders nobody sees.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 0430b95636d2b6adce8887af7b9464687c95549e)
* PERF-CULL-3: CR3 previews take the smallest embedded JPEG that covers the view; plain sRGB JPEGs decode straight to 8-bit
Upstream's embedded_preview_covering (
|
||
|
|
0b9ff64e83 | Merge storytold/main into feat/windows-monitor-profile | ||
|
|
8972565249 |
LR-PROF-IMPORT: import the user's own DNG camera profiles (.dcp) (#1063)
* LR-PROF-IMPORT: import the user's own DNG camera profiles (.dcp)
profile.import (File > Import Profiles & Presets, drag & drop) now also
reads DNG Camera Profiles: the user's own .dcp files, e.g. made with a
colour-checker tool. They appear in the profile browser and menu under
Camera Profiles with dcp:<camera>-<file> ids; profiles.camera lists them
and profile.deleteCamera / profile.deleteImported remove them. A library
on disk keeps a copy in Profiles/. No profile ships with the app and the
repository holds none: tests build theirs in memory (write_dcp).
lightcraft_raw::dcp parses the file per DNG spec ch. 6 (magic RC in
place of 42; IFD 0 read through the existing DNG colour builders) and
DcpProfile::applied_to puts its matrices, illuminants and look in place
of a raw file's, keeping the shot's white balance, analog balance and
baseline exposure.
Choosing one (develop.profile) replaces the raw file's colour model when
it is decoded, before the starting colour and the white-balance scale
(WbScaleOf) are worked out, so the matrix path takes over from the fitted
JPEG look (CR3/CR2/ARW...) and white balance follows the profile. A
profile without a tone curve of its own (most chart-made ones) keeps the
photo's contrast: the DNG's tone curve, else the tone of the camera-JPEG
fit. The profile travels with SourceRef::File into FileLoader /
PairLoader (new Option<&DcpProfile> argument); a decoded source records
the profile it was made with and is not reused for another. Smart
previews and the web render worker keep the file's own colour; rendered
(non-raw) photos ignore it.
This reverses the documented "Adobe profile formats deliberately
unsupported" stance for user-supplied files only; docs updated.
Split out of #1012.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: build against upstream main (kelvin_presets, 3-arg loaders)
DcpProfile::applied_to keeps the file's white-balance presets (new
ColorData::kelvin_presets upstream): they are camera facts like as-shot
neutral. Upstream's new FileLoader test closures take the profile argument.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: hover previews and browser thumbnails develop the hovered camera profile
The loupe's hover preview (preview_job) and the profile browser's thumbnails (variant_job) took
the photo's own decoded source, so hovering an imported camera profile showed the built-in colour
(and hovering a built-in profile while one was applied kept the camera profile's). Both now decode
as the hovered settings develop: the source follows the settings' camera profile, as the main
render does, and a variant's cache key already follows its settings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: build against upstream main (face-tile test loader and render_bench take the camera profile)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CI: the catalog's staging-link test module is Unix-only (unused import broke clippy on Windows)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CI: same-folder export test expects the '/'-joined subfolder (failed on Windows)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CI: folder-drop tests build widget ids with the Folders tree's '/' paths (failed on Windows)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: rustfmt after merging main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: a real D750 camera profile as a pinned test corpus, checked against Lightroom
dieli's CC0 Nikon D750 ColorChecker set (Calibrite PROFILER dual-illuminant DCP,
matrix-only; the two chart NEFs; Lightroom Classic 15.6 renders with the profile)
is fetched by cargo xtask corpus --download into corpus/d750-colorchecker/, pinned
to commit f37f4054 and by sha256; nothing is committed.
Tests (skip without the corpus): the profile parses as matrix-only with
Cool White Fluorescent + D55; the matrices blend by inverse temperature; the
matrix-only path inverts the colour matrix and adapts to D50; it takes over the
chart NEFs' colour; imported and applied end to end, the charts develop within
mean dE2000 3.84 (flash) / 3.35 (indoor) of Lightroom's renders.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: a single .dcp file is a camera profile only, not also an unreadable LUT
profile.import read every plain file that was neither a folder nor a .zip as a .cube LUT, so importing
one .dcp imported the camera profile and also reported it as failed ("stream did not contain valid
UTF-8"). Only .cube files are read as LUTs now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: review fixes for imported camera profiles
- A library moved or restored elsewhere finds its camera profiles in its own Profiles/ folder.
- A grid thumbnail is cut from the decoded preview only when that preview used the same camera profile.
- Render and source cache keys carry a fingerprint of the profile file, so a deleted or replaced profile never
has its old renders stand in.
- Camera profiles can be favourites, and favourites/recent entries survive reopening the library.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LR-PROF-IMPORT: smart previews without the camera profile, current-profile source facts, unsupported files reported
CodeRabbit review of
|
||
|
|
4da8573b55 | Merge storytold/main into feat/windows-monitor-profile | ||
|
|
077f4a507f |
GPU-FULL: more of the render on the GPU, loupe drafts from GPU textures (#1065)
* GPU-FULL-1: LUT profiles, Refine Edges and soft proofs render on the GPU These three sent the whole loupe / compare / export render back to the CPU pipeline (media::develop and lightcraft_gpu::render declined them). - LUT profiles: the .cube table rides in the finish kernel's aux buffer (LUT_* parameters); lut_apply mirrors Lut3d::apply (trilinear, domain), applied after grain and blended by the profile amount, as on the CPU. - Refine Edges: the cross guided filter Auto Mask already ran on the device, now with a combine mode (max for strokes, mix by sqrt(refine) for Refine), applied to the combined alpha before the mask's invert and amount. - Soft proofing: proof-space gamut mapping and the destination / display gamut warnings in the kernel (PROOF_* parameters, GAMUT_WARN and warning colours generated from the CPU constants), for output spaces and displays. Equivalence tests (RTX 5070, DX12): LUT profiles at three amounts and a non-default domain, Refine 15/60/100 on two masks, proofs in 16 combinations of space, output, display and warnings - all max 1 LSB, mean <= 0.0005 LSB. The tests also check that strong refine and the destination warning change the image (so they aren't comparing two unchanged renders). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * GPU-FULL-2: spot removal, defringe and the histogram on the GPU Spots and defringe made every render that had them (and every white balance or spot edit) read the resampled image back, run the scene-linear stage on the CPU and upload it again. Now only the ProRAW local tone map (off by default) does. - Defringe: OkLab pass, separable lightness min/max filter, per-pixel apply. Parameters come from the new optics::defringe_params, which the CPU stage now uses too. - Spots: new spot module (spot_prep / spot_apply over each spot's box, the Heal tone correction as device Gaussians), spot by spot in place so each reads what the earlier ones left, as on the CPU. Remove's automatic source: spot_score rates the 48 candidates on the device and only the scores come back. Layout, candidates, spoke directions, radii and ranking are shared helpers in pipeline::spots (spots::apply uses them; behaviour unchanged, pipeline tests green). - Histogram: counted with atomics on the device on Histogram::of_srgb8's sampling grid (exact), instead of a CPU pass over the readback. Tests: spots (clone, heal, auto remove incl. near the edge where no candidate fits, brushed strokes, feather 0, cut by the edge, overlapping) and defringe (purple/green, hue ranges, preview and full size) max 1 LSB; the device histogram equals the CPU count of the returned image for small, sampled (>1 MP) and windowed renders; the scoring kernel picks the CPU's candidate. (The edge case caught a real bug: "doesn't fit" was written as a decimal literal that rounds below f32::MAX.) Unit tests that create the device now take turns with the crash-sentinel test, which swaps a global. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * GPU-FULL-2: fix the spot kernels' build; GPU tests fail when kernels don't build 07d4680b's spot module declared `const F32_MAX = bitcast<f32>(...)`, which naga can't evaluate as a constant: the kernels failed to build, the device was unavailable, and the equivalence tests skipped as if there were no adapter - so that commit's GPU tests didn't actually run. f32::MAX is now a function, and the equivalence suite (and the device unit test) fail instead of skipping when an adapter exists but the kernels don't build. Re-verified on the RTX 5070 (DX12), all on the device: 21 equivalence tests incl. the 8 spot cases (max 1 LSB), defringe, the exact device histogram, and the auto-source unit test. Adds an ignored thumbnail throughput bench (tests/thumbs_bench.rs). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * GPU-FULL-3: thumbnails render on the GPU once the device is ready Every cached job (grid and filmstrip thumbnails, preset and profile previews, faces, quick stand-ins) was forced onto the CPU. They now use the GPU when lightcraft_gpu::ready() - so they never block on device creation at startup (1.8 s incl. kernels here) and render on the CPU until then. Views and exports keep waiting for the device as before. tests/thumbs_bench.rs (ignored): six workers rendering 256 px thumbnails of 512 px sources with preset-like settings (NR, sharpening, clarity, grading, a gradient mask): RTX 5070 3.6 ms each vs 4.3-5.2 ms on 32 CPU threads, with the cores left free for raw decoding. Engine and UI suites: unchanged (only the 12 failures that predate this branch on Windows: tests_sync paths, tests_model_setup). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * GPU-FULL-4: the loupe's drafts are drawn straight from the GPU Rendering and drawing now share one device: the desktop app creates the window's wgpu device with the adapter's limits and hands it to the GPU renderer (lightcraft_gpu::adopt_window_device, on a background thread at startup, under the crash sentinel; a device that doesn't suit falls back to one of the renderer's own, as before). A drag's drafts of the views are RenderJob::on_screen: the render ends in an Rgba8Unorm texture (lightcraft_gpu::render_display -> Output::Display, new `present` kernels: unwritten-pixel count, row padding for the copy) and egui-wgpu draws it as a native texture (ui-egui ScreenTextures, implemented by the app) - no readback, no conversion to egui pixels, no re-upload. The readback's checks run on the device (atomic unwritten count, device histogram). Renders that need pixels keep the readback: the settled full render (view cache, overlays, screenshots), thumbnails, exports, overlays, windows that spots grew, a monitor profile. - engine: RenderJob::on_screen, RenderResult::frame (ScreenFrame). - ui-egui: Tex::native / Tex::id() (21 draw sites), Renderer set_screen_textures, natives per slot (freed when pixels return), ui.inspect perf.gpuWindowDevice / screenTextures / mainOnGpu. - app: WindowTextures, window device limits, LIGHTCRAFT_GPU_WINDOW=0 to opt out. Tests: crates/gpu/tests/display.rs adopts a device in its own process and checks the texture holds exactly the readback's pixels (padded and unpadded widths, draft and full), the histogram matches, and overlays / grown windows come back as pixels. In the app (RTX 5070, a 45 MP CR3 from the user's library copy): all 90 drafts of a drag drawn from GPU textures, 1.69 ms vs 2.05 ms median per draft render with the readback; the frame screenshot shows the image correctly; pixels return after the drag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * GPU-FULL-2: the denoise runner tests take turns with the crash-sentinel tests Building a denoise runner creates a device and compiles kernels under the init marker. Running in parallel with backend::tests, it could arm the marker the test had just set, so "is removed afterwards" failed now and then. The runner tests now hold DEVICE_TEST_LOCK while they build, like the render tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * GPU-FULL-4: fix the build after merging upstream main Upstream's QuickJob panic guard builds a RenderResult (add `frame: None`), its idle-trim test builds a Tex (add `native: None`), and its on_exit test builds App (add the WindowTextures slot). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * GPU-FULL-2: upstream's blur tests take turns with the crash-sentinel tests gaussian_matches_cpu and bench_kernels create the device, so they hold DEVICE_TEST_LOCK like every other device test in lightcraft-gpu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tests: the same-folder export test compares paths separator-blind (Windows) The written path is the original's folder (`\` on Windows) joined with `Exports/IMG_1.jpg`; the expected one had `\Exports`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tests: the folder-drop tests find folder widgets separator-blind (Windows) The folder tree spells its paths with `/`; the tests built widget ids from Windows paths with `\`, so none was found and every test failed on Windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tests: the Windows path fixes take the same form as in the other PR branches So the branches merge into main without conflicts between their copies of the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * GPU-FULL: headless screenshots read back GPU-drawn views; LIGHTCRAFT_GPU_LIMITS caps the adopted device Review of PR #1065: - A headless `ui.screenshot` taken mid-drag showed the photo as a white rectangle: the GPU-resident draft left no CPU copy. With keep_pixels (control channel) the renderer now keeps each view's frame, and `cpu_textures` reads it back through `ScreenTextures::read` (`lightcraft_gpu::read_texture`). Drawing still never reads back, so drags keep the speed-up. - Adopting the window's device skipped LIGHTCRAFT_GPU_LIMITS; `Gpu::adopt` now caps its limits the same way `Gpu::new` does. Tests: a_headless_screenshot_reads_back_a_view_drawn_from_the_gpu (ui-egui), the_limits_override_caps_an_adopted_device (gpu); the display test uses read_texture and skips through lightcraft_testkit::skip!. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * GPU-FULL: review fixes (CodeRabbit 2026-10-21) and slideshow/print draw GPU-drawn views - the window-device adoption is announced before its thread starts: until it finishes, the settings' warm-up and renders don't create a device of their own (they raced it) - the device error / device-lost handlers go on only after the kernels build, so a declined window device keeps the window's own handler - releasing a slot frees its GPU-resident texture and kept frame - a soft proof into a colour space drafts full size on the GPU; only one to a device profile counts as proofed for `handles` - docs: Fill spots and device-profile soft proofs render on the CPU - slideshow, slide layout and print draw `Tex::id()` (the GPU texture when there is one) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * GPU-FULL: the display test skips when LIGHTCRAFT_GPU=0 switches the GPU off It panicked ("GPU rendering is off") instead of skipping like the other GPU tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * GPU-FULL: a view readback never panics on a lost device read_texture (headless screenshots of GPU-drawn views) polled the device directly. On a lost device wgpu 30 panics in Device::poll; it now goes through the same guard as the renderer's own polls (poll_device): a lost device returns an error, stops the GPU and is not polled again. Review of PR #1065. Also quiets a browser-build dead-code warning on OnScreen::Frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * GPU-FULL: an HDR edit's histogram shows the HDR range on the on-screen GPU path too develop_on_screen returned the GPU's SDR histogram for both its Display and Image arms; it now applies lightcraft_pipeline::hdr_histogram as develop_reporting does (None for drafts, windows, proofs and non-HDR edits, which keep the GPU's histogram). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * GPU-FULL: the Navigator draws the photo's texture as it is drawn in the loupe The Navigator falls back to the main view's texture before the photo has a thumbnail. It drew `t.tex`, which is stale (or the 1x1 placeholder) when the main view holds a GPU-resident draft; it now draws `Tex::id()`, like every other draw site. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * GPU-FULL: the egui 0.37 lifecycle test's texture names no native id Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * GPU-FULL: rustfmt the lifecycle test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * GPU-FULL: the second window fits the frame's size; freed GPU frames drop their views The second window sized its image from the texture handle, which is only a 1x1 placeholder for a GPU-resident render; it now uses the frame's size. When a GPU-resident render can't be shown and every native texture is freed, the views pointing at them (Before, Region, Compare tiles) are dropped too, so they render again as pixels instead of drawing a freed id. Test: freeing_gpu_frames_drops_the_views_drawn_from_them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(gpu): LIGHTCRAFT_GPU_ADAPTER picks the card for rendering too The variable chose the card only for AI Denoise and the face detector (two copies of the same enumerate-and-match code); photo rendering always took the first high-performance adapter, on a two-card machine the one driving the screens. One helper, backend::pick_adapter, now serves the render device (Gpu::new), the denoise network and the face detector: the first hardware card whose name contains the value (any case), else the high-performance one. A name that matches no card is logged and falls back instead of failing. backend::may_adopt_window_device says whether rendering may share the window's device (only for the window's card). Name matching is unit-tested without a GPU; the equivalence tests print the card they ran on. Documented in docs/gpu-pipeline.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * GPU-FULL: a view switching to a GPU texture releases its pixel image Review of #1065 (CodeRabbit nitpick): when a slot that had pixels switched to a GPU-resident texture, its egui texture kept the full-size image, never drawn while the native texture is set. Tex::show_native now swaps it for a 1x1 placeholder on the switch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(gpu): LIGHTCRAFT_GPU_ADAPTER picks among several matches in a defined order and logs once Review of #3428: - several cards match: the one whose whole name it is, then dedicated before built-in before virtual cards, then wgpu's order (backend::choose_named_adapter); the log names every match and the one used; - no match: the default choice, with a warning that lists the cards; - a name matching only a software renderer: never used, and the warning says so; - each line is logged once (backend::log_once), though the render device, AI Denoise and the face models each ask; - tests on fake adapter lists: a_fake_adapter_list_picks_in_a_defined_order, a_choice_is_logged_once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * GPU-FULL: the release note gives the GPU's documented bound against the CPU (3 levels, mean under half a level) Review of PR #3544. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * GPU-FULL: the window's device isn't adopted when LIGHTCRAFT_GPU_ADAPTER names another card With #3428 merged, Gpu::new already makes the render device on the named card; the window's device was still adopted first, so the app rendered on the screen's card whatever the variable said. backend::may_adopt_window_device (tested on names in backend.rs) now decides: another card named renders on a device of its own, and the window stays on the screen's card. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a9f766bf60 |
Merge storytold/main into feat/windows-monitor-profile
Conflicts only in the docs' "Last updated" header lines (main's newer entries are at the front too): main's line with this branch's entry kept in front. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvoftP5JSqAEJ9t7FSczTs |
||
|
|
36b244286a |
Merge remote-tracking branch 'origin/main' into pr-3073
# Conflicts: # docs/gaps.md |
||
|
|
ab39f7dbbc | web: a restore plans its writes first, each storage key once and within the file's bytes; workers are capped (#3669) | ||
|
|
5c96a70058 | security(logging): a log message cannot begin a record of its own (#3720) | ||
|
|
4d2fa1269b |
lightcraft-cli: calibrate walks folders with the engine's bounded walk (#3643)
* lightcraft-cli: calibrate walks folders with the engine's bounded walk * lightcraft-cli: calibrate keeps only regular raw files |
||
|
|
76f3972484 |
Merge storytold/main into feat/windows-monitor-profile
Conflicts only in the docs' "Last updated" header lines (main's #3503 added its entries at the front too): main's line with this branch's entry kept in front. Cargo.lock: lightcraft-wincolor follows the workspace version to 0.18.0 (main's release bump). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvoftKWm1wYSXdzRBwvFR3 |
||
|
|
ac1799db38 |
web: ?bench runs on demo photos in an unsaved library, never the stored one (#3646)
A navigation link with ?bench opened the first photo of the library in browser storage and committed Exposure edits to it, finishing at 0 instead of the photo's own value. ?bench now implies ?store=memory and loads the generated demo photos, so the benchmark cannot edit stored photos. |
||
|
|
06c1b9ae07 |
Use crw craftlib (#3503)
* CRW-LIB: decode Canon CRW through the shared craft-crw crate LightCraft decodes Canon CRW (the CIFF raws of the EOS D30, D60, 10D, 300D and the PowerShot G1/G6) through craft-libs' `craft-crw`, the clean-room decoder derived and verified on the `clean-room-crw-format-decode` branch and moved there with its frozen tables and derivation record. `crates/raw/src/vendor/crw.rs` is now the adapter: it maps the crate's header and samples onto `RawImage` (CFA, levels, active area, orientation, as-shot white balance, the make/model/time records a CRW keeps instead of Exif) and converts the capture time through chrono. The dependency is a local path (`../craft-libs/crates/crw`) until the craft-libs branch merges; then it becomes a git revision. Everything LightCraft-side from that branch comes along unchanged: `probe_bytes` fills metadata gaps from the decoder's records so CRW imports name their camera; the EOS D60 bundled camera profile and its chart-measured dual-illuminant matrices (`chart_table.rs`, `spectral::Camera` carries its illuminant pair) give the D60 a Kelvin white-balance scale; CRW joins the JPEG-fitted starting look, the catalog's raw formats and `lightcraft-cli calibrate`; `RENDER_CACHE_VERSION` 71; the CC0 CRW corpus entries for `cargo xtask corpus`; coverage, parity, gaps, raw-parity, target-app-parity and ROADMAP updates. The G5 stays preview-only (the crate refuses it with the reason) and the robustness test keeps its CRW sample. What does not come along: the decoder, its tables and `crw_oracle.rs` (now craft-crw's digest-pinned sample test) and the derivation record, which lives in craft-libs `crates/crw/clean-room/`. `clean-room/crw/` here keeps only the EOS D60 chart-calibration document and its capture list, with a README pointing at the record. Gates: fmt, clippy -D warnings (raw all targets; engine, catalog, cli), lightcraft-raw tests (475 + 134, the corpus files decoding through the adapter), catalog tests, engine lib tests (1,300 pass; the two failures, `tests_import::recursive_discovery_…` and `originals::tests::write_targets_pin_absent_paths_…`, are in files unchanged from main), `cargo xtask parity`, `assets`, `layers`, `coverage.py --check`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * no message * no message * Update documentation after moving code to craft-libs * Review coment fixes * CRW: pin craft-crw f3b62ec, a truncated entropy stream is Corrupt craft-libs #472 makes craft_crw::decode refuse a stream that ends more than Limits::max_stream_end_fill (64 samples, one block) before the frame does, instead of filling the rest with predictor values and returning success. The adapter already maps craft_crw::Error::Corrupt to RawError::Corrupt; a regression test proves a truncated CRW now fails at this boundary (it decoded as a flat frame on the old pin) while its header still reads. Only craft-crw moves; craft-ui/docpath/update/window stay on c5a1cbf. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Code review comment fix * Add some tests --------- Co-authored-by: Robert Woldberg <314159+rwoldberg@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
d16285bca9 |
feat(web): page host mode, so a page can host LightCraft in its own document (#2270)
data-host="page" on the canvas turns it on. The bridge gets a Transport (Parent or Page) with one send: in page mode the same messages travel as non-bubbling CustomEvents on the canvas, and the page sends lightcraft:open and lightcraft:cmd the same way. Parent mode is unchanged. |
||
|
|
77e289fc1f |
Merge upstream/main into feat/issue-2270-web-host-bridge
# Conflicts: # docs/whats-new.md |
||
|
|
29443e117c |
LR-EDIT-OPTICS-PROFILE: merge main (#3430 integration round 3, #3390)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fc2439dd81 |
Merge storytold/main into feat/windows-monitor-profile
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvoftM8ZqGRb62UWw9DAKP |
||
|
|
ee0a627536 |
Merge track r3-t10
# Conflicts: # crates/engine/src/cmd/masks.rs # crates/ui-egui/locales/de.json # crates/ui-egui/locales/es.json # crates/ui-egui/locales/fr.json # crates/ui-egui/locales/ja.json # crates/ui-egui/locales/ka.json # crates/ui-egui/locales/pt-br.json # crates/ui-egui/locales/ru.json # crates/ui-egui/locales/uk.json # crates/ui-egui/locales/zh-hant.json # docs/gaps.md # docs/parity-checklist.md # docs/whats-new.md |
||
|
|
e05c4f7e7c |
Merge track r3-t08
# Conflicts: # crates/meta/README.md # crates/ui-egui/locales/cs.json # crates/ui-egui/locales/de.json # crates/ui-egui/locales/es.json # crates/ui-egui/locales/fi.json # crates/ui-egui/locales/fr.json # crates/ui-egui/locales/id-formats.json # crates/ui-egui/locales/id.json # crates/ui-egui/locales/it.json # crates/ui-egui/locales/ja.json # crates/ui-egui/locales/ka-formats.json # crates/ui-egui/locales/ka.json # crates/ui-egui/locales/pl.json # crates/ui-egui/locales/pt-br.json # crates/ui-egui/locales/ru.json # crates/ui-egui/locales/sv-formats.json # crates/ui-egui/locales/sv.json # crates/ui-egui/locales/uk.json # crates/ui-egui/locales/vi.json # crates/ui-egui/locales/zh-hans.json # crates/ui-egui/locales/zh-hant.json # crates/ui-egui/src/panels/grid.rs # crates/ui-egui/tests/it/main.rs # docs/gaps.md # docs/library-shortcuts.md # docs/parity-checklist.md # docs/raw-parity.md # docs/user/guides/export-photos.md # docs/whats-new.md |
||
|
|
8415fd6a90 |
Merge track r3-t06
# Conflicts: # docs/parity-checklist.md # docs/whats-new.md |
||
|
|
057ef1fd27 | Integration: #3006 use binary() in the non-finite --set test | ||
|
|
d8f45ed5a5 |
Merge PR #3322: LRC-LIB-CATALOG-SWITCH: a library missing at launch is reported instead of being made anew with demo photos
docs/parity-checklist.md: the PR's edit to the LRC-LIB-CATALOG-SWITCH row applied to main's row. docs/whats-new.md: the PR's 2026-10-29 section added above main's sections. |
||
|
|
76a03de507 | Integration: #2421 adapt the window-surface test to choose(files, compose_window) | ||
|
|
c9c69ddd4c |
Merge PR #2919: PERF-WINDOW: the window queues one frame, not two (eframe's low-latency surface)
# Conflicts: # docs/whats-new.md |
||
|
|
78e70ab8f8 | Merge PR #2545: Issue #136: applying the GPU setting never waits for the device on the UI thread | ||
|
|
5062acc601 |
Merge PR #2421: fix(windows): add opt-in DirectComposition workaround for VRR flicker
apps/lightcraft/src/main.rs: kept main's split; hunks ported (clear_color, compose_window, with_transparent); test moved to tests.rs crates/ui-egui/src/panels/settings.rs: deleted on main; checkbox ported to dialogs/settings/performance.rs crates/ui-egui/src/state.rs: compose_window field and default ported to state/settings.rs crates/ui-egui/locales/*.json: main's file plus the new key docs/gaps.md, docs/parity-checklist.md: main's header and rows plus the PR's additions; duplicated header line and blank lines dropped |
||
|
|
9d4de0518a |
Merge PR #2979: LRC-LIB-CATALOG-SWITCH: a library's catalog.snap or folder given at launch opens that library
docs/parity-checklist.md: header keeps main's newer date plus the PR's item; revision rows union. |
||
|
|
b7970ca840 | Merge PR #3295: MENU-BAR: merge the native menu's identical quit/about/settings branches (clippy::if_same_then_else) | ||
|
|
9f587f69d8 |
Merge PR #3220: MENU-HELP-HELP: on macOS the Help menu is registered with AppKit after the menu bar is installed
Conflicts: apps/lightcraft/src/native_menu.rs (both added lines kept: RowIds and help_menu), docs unioned |
||
|
|
42fbec83dc | Merge PR #2725: LRC-LIB-CATALOG-LOCK: quitting on macOS removes the lock's owner note | ||
|
|
21a57ff79a |
Merge PR #3196: MENU-BAR: the macOS menu bar keeps every row when two rows run the same command
Conflicts: docs/whats-new.md (both entries kept), docs/parity-checklist.md (header change list and revision rows unioned) |
||
|
|
7e68f1a31e | Merge PR #3124: LC-003: reject non-Unicode CLI arguments without panicking | ||
|
|
b1f3e00f3c | Merge PR #3123: LC-001: stop commands after failed imports | ||
|
|
328e42373c | Merge PR #3122: LC-002: reject non-object script parameters | ||
|
|
15a046c537 | Merge PR #3098: Issue #3070: the CLI's refused-connection tests use a free port, not 18437 | ||
|
|
bc36137608 | Merge PR #3076: Issue #3069: --connect resends a command that crossed the app's idle-close notice | ||
|
|
6ad10a6bf4 | Merge PR #3006: CLI-HOSTILE: render --set refuses inf / NaN instead of dropping the value | ||
|
|
035564c828 | Merge PR #2963: Accept a UTF-8 BOM at the start of lightcraft-cli --script files |