On an Intel Mac with an AMD Radeon Pro 5300M the Metal compiler crashes
on candle's mlx_gemm kernel, candle-metal-kernels panics on the compile
error, and the worker reported "the AI model failed unexpectedly" and
loaded the model on Metal again for every next job, so AI masks never
worked there.
The worker now retries a job that fails on the GPU (a panic or a candle
error) once on the CPU, after freeing the model. When that works, the
model stays on the CPU for the session and the job's outcome carries a
notice that segment_poll shows once; when the CPU fails too, the GPU
was not the cause and the next job uses it again. Missing or damaged
model files, render and remote-worker errors are not retried. Only
builds with a GPU path (macOS) retry; LIGHTCRAFT_GPU=0 and
LIGHTCRAFT_GPU_BACKEND=off now keep SAM 3 on the CPU from the start
(lightcraft_gpu::env_off_reason), like AI Denoise and the face models.
lightcraft-segment: Sam3::load_cpu, GPU_BUILD; text_logits returns the
crate's Error so Describe failures are classified like clicks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Partial scope only: catalog-backed renderer diagnostics. Broader import/export paths remain open. Preserve already-merged workflow/package changes; the exact candidate tree passed both diagnostic regressions, renderer tests and all eight local CI gates.
Manual metadata reread now finds standard embedded JPEG XMP when no sidecar exists. Existing sidecars remain authoritative, metadata updates remain undoable, and rereading does not write a sidecar. The reader retains its bounded header policy; Extended XMP assembly is unsupported.
Co-authored-by: Codex <codex@openai.com>
Found while testing crates/engine/src/rename.rs:919 (start parsed at crates/engine/src/cmd/manage.rs:12)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflict only in docs/parity-checklist.md: the generated summary is
refreshed with `cargo xtask parity --write`, and the Last updated line
keeps main's format with this PR's change note (today's date) instead of
the duplicated note the branch carried. ⌥-drag preview still wins over
AllMasks and the selected mask in view_overlay; overlay kinds 6–9 unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As in the Profile browser, starred presets are listed in a Favorites
section above the groups, and stay in their own group too. The section
has its own open state and widget ids, so a user group named
"Favorites" and the same preset listed twice don't clash.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
Review round 1: with no active photo saved (an empty Missing view leaves
none), open_stores worked the view out to pick its first photo, with the
same synchronous check of every original. Skip that on Missing Photos;
the test now opens both an empty and a full Missing view.
Forgetting untouched Local records on open kept the visible photos, and on
Missing Photos working them out stats every original on the UI thread
before the availability worker runs. A 37k-photo library on a sleeping
SMB share froze the window for minutes. Missing Photos lists library
photos only, never Local records, so it can keep nothing: skip it.
Export Keywords wrote with std::fs::write to any path, replacing a library
original or its XMP sidecar. It now goes through check_write_target and
write_file_durable like preset.export and curve.exportPresets.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Rendered at the drawn size, the pipeline itself shrinks the 2560 px preview
source (Mitchell prefilter, then the bilinear sampling pass) before the display
fit shrinks it again. At rest the whole-frame render now asks for no less than
the preview level (the photo's own size if smaller, a user's explicit Preview
size if lower, the texture limit at most): the source is copied 1:1 and shrunk
once, by the Lanczos display fit. Drags still render drafts at the planned size.
On a line-grating chart at Fit this takes 1.8 px bars from 0.83 to 1.00 of their
contrast (1920 x 1200 window), at the cost of a larger settled render.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At Fit the loupe was softer than a good downscale of the same file. Four
resamples stacked up between the source and the screen:
- the render edge is rounded up to a 64 px step, so the texture was drawn up
to ~6 % smaller than its size and the GPU shrank it with 2-tap bilinear
sampling;
- the fitted texture sat on a fractional pixel position, so even at the right
size every screen pixel was averaged with its neighbour (#1831 fixed the
same for 100 %);
- the render box truncated its short side (2560 x 1706 for a 2560 x 1707
source), a slightly anisotropic resample that blurred one axis only;
- the codec's decode-time shrink at 2:1 and more, and SourceRef::Fit, used a
box filter, which halves the contrast of detail near the output's limit.
RenderJob::display_fit shrinks a full-quality render that is larger than its
drawn size to exactly that size with Lanczos-3 on the worker (after the caches
have the full render; drafts untouched; part of the key). The Detail view sets
it for the After and Before renders at rest, and snap_fit_rect draws a texture
that is its drawn size on whole physical pixels. The render boxes round their
short side, and the 2:1-and-more shrinks use Lanczos-3.
Refs #1472, #1042.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Canon's maker-note ColorData lists the levels of each white-balance preset
with Canon's colour temperature for it. For versions -4 and 13-15 the
Daylight, Shade, Cloudy, Tungsten and Flash rows now give a Kelvin curve
(kelvin_presets, generalised from five fixed points to any number, with a
from_unordered constructor that sorts and drops points that would turn the
curve back). Fluorescent (off the locus) and the Kelvin row (the dial's last
setting) are left out. Enabled for the PowerShot G1 X Mark III, which has no
colour matrices: its nine as-shot white balances read within 4 % of the
camera's own ColorTempAsShot.
Sources: own design on the existing kelvin_presets model; ColorData row
positions from the ExifTool Canon tag-name documentation, confirmed with
ExifTool run as a black-box tool on sample files.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Z5II has no colour matrices and its NEFs carry no preset table, so its Temp slider
showed the relative -100..+100 scale. A small per-model table (crates/raw/src/kelvin_builtin.rs)
gives five (Kelvin, multipliers) points that feed the existing KelvinPresets curve: the NEF
decoder attaches them to ColorData::kelvin_presets and kelvin_presets::covers accepts the model.
Points: as-shot WB_RBLevels of the body's own shots at Incandescent, Direct sunlight, Kelvin
5500 K, Cloudy and Shade, placed at the temperature the camera itself writes for each preset
(maker note ColorTemperatureAuto: 3000, 5210, 5500, 5990, 8000 K). Fluorescent and flash are
left out. Natural Auto (camera: 5030 K) reads back at 5036 K.
Sources: own design on the existing kelvin_presets model; the camera's own stated temperatures;
ExifTool Nikon tag-name documentation (prose), exiftool used as a black-box tool.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With no aspect ever chosen, every crop started free, so the Lock had to
be switched on photo after photo. Nothing chosen yet now means locked to
Original, as Lightroom starts; switching the Lock off once is remembered
as free, and an explicit ratio is remembered as before.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011K1peyrh5MdZ6ttHkVq8Gh
The Pick flag on thumbnails, in Compare, in the loupe and on the Pick
button was the same outline as the button's idle state, so a pick was
hard to tell at a glance. A new filled pennant (`Icon::FlagPickFilled`)
marks a pick wherever a photo's own flag is shown; buttons, filters and
the reject flag keep their outlines.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011K1peyrh5MdZ6ttHkVq8Gh
`[workspace.package] rust-version` said 1.90 while `jixel` needs 1.94,
`lightcraft-ui-egui` declared 1.95 on its own and the README asks for
1.95. One declaration now, inherited by every crate; the flake's note
says the same.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011K1peyrh5MdZ6ttHkVq8Gh
A DNG whose profile has a LookTable but no ProfileToneCurve (Adobe Standard in
Lightroom-written DNGs) was not tone-fitted and rendered with the near-linear
default tone: flat and ~0.8 EV darker than Lightroom. A look table defines colour,
not tone, and the fit already applies the tables to its proxy. The preview may now
be 10% of the raw's long edge (and 384 px), so Lightroom's 1024 px previews
qualify. Two lossy ILCE-7CM2 DNGs now match Lightroom's mean linear RGB within 3%.
Render cache 65, look version 10.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lightroom Classic's lossy LinearRaw DNGs linearise the sRGB-coded samples with a
cubic MapPolynomial in OpcodeList2; measured against Lightroom's preview, the
declared-curve reading tracks its rendering and the linear one is 3.6 EV dark.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lightroom Classic 15.1 writes lossy LinearRaw DNGs (e.g. Denoise output) whose
JPEG XL tiles declare Rec. 2100 primaries with the sRGB transfer curve. These
were rejected ("declared encoding that is not linear RGB") and fell back to the
embedded preview. The tile's samples mean what they would in a lossless tile with
the same header, so the declared curve (sRGB, BT.709, DCI, gamma) is now applied
after the primaries matrix, mirrored and unclipped. PQ / HLG / unknown stay
unsupported.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests_folder_drop built widget ids from OS paths (C:\...), but the Folders
tree names folders with '/' on every platform (libfolder:C:/Users/...), so
8 tests found no row on Windows. tests_offline asserted that the background
probe had run without waiting for the worker thread; it now waits up to 5 s.
Found by running the tests on windows-latest (gap 9). Test-only changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
same_folder_as_original_exports_next_to_each_photo compared path strings.
The export joins the subfolder with '/', so on Windows it reports
C:\...\A/Exports/IMG_1.jpg (a valid path) while the test expected
C:\...\A\Exports/IMG_1.jpg. Found by running the tests on windows-latest
(gap 9). Test-only change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
The template was copied from a sibling app: it exported a "LightCraft Document"
UTI for PhotoCraft's `.pcraft` extension (MIME `application/x-lightcraft`) and
imported `.psb` (Photoshop Large Document) and `.qoi`. LightCraft reads none of
them, so the declarations only made Launch Services type PhotoCraft documents as
LightCraft's and left `.pcraft` with three claimant apps among the Craft suite.
Remove both `UTExportedTypeDeclarations` and `UTImportedTypeDeclarations`. No
CFBundleDocumentTypes is added: the formats LightCraft opens are already declared
by the system or another app, so adding them is out of scope here.
`packaging/check-app-specific.sh` now parses the template as a property list and
fails if it declares any of those types again, while still checking the required
bundle keys.
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
serve() read through stream.try_clone(). On Windows a cloned socket handle is
inheritable (std's accept/connect sockets are not), so every program the app
started while a control client was connected (the browser for a link,
Explorer for Show in Explorer, an external editor) got a copy of the
connection. When the server then closed it, the client saw nothing until that
program exited, and then a reset instead of a clean close. Read and write
through the one stream (&TcpStream is Read and Write) instead.
Regression test (Windows): a program started while a client is connected no
longer delays or resets the close; it failed with ConnectionReset before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once a Vulkan or DX12 device is lost, wgpu 30 panics in `Device::poll`
instead of returning an error: the device error is not a `PollError`, so
`wgpu_core.rs` hands it to `handle_error_fatal`. The readback assumed the
wait would fail. A render whose readback found the device lost panicked
(caught by `try_render`), and while it unwound, a buffer going back to
the pool polled again from a destructor (`Gpu::trim`). That second panic
aborted the app ("panic in a destructor during cleanup"), as in the
reporter's logs on an Intel UHD 620 under Vulkan, each time a merge lost
the device.
The render device's three polls (the readback, `trim`, `sync`) now go
through `Gpu::poll`, which stops polling a lost device (the device-lost
callback, or a poll that panicked) and turns a panicking poll into an
error that retires the GPU. The render then fails over to the CPU and the
app keeps running. A GPU retired for another reason (a validation error)
keeps a live device and is still polled, so its pooled buffers are freed.
Regression tests run in a child process (they lose the device):
- `a_poll_on_a_lost_device_neither_panics_nor_aborts`: polls panic as
wgpu's do on a lost Vulkan device; a readback, then an unwinding render
whose destructor trims the pool. Before the fix the child aborts with
SIGABRT and the log line above.
- `a_really_lost_device_is_not_polled_again`: a Metal device really lost
(4200 unsubmitted command buffers, past wgpu-hal's 4096) is not polled
again. Before the fix the poll still reaches wgpu.
- `a_retired_but_live_device_is_still_polled`: a GPU retired by a
validation error still polls (and releases its buffers).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RbXK4njy12cpm4PG12Xrqt
On Windows 11, `cargo test --workspace` on main fails 11 tests:
- tests_export::same_folder_as_original_exports_next_to_each_photo built its
expected paths with Path::join (`\`), but export joins the subfolder and
file name with `/` (join_path) on every platform. Expect what export writes.
- tests_folder_drop (8 tests) looked rows up as
`source:libfolder:C:\...`, while the Folders section names them with `/`
(`source:libfolder:C:/...`), so no row was found. Look them up the same way.
- mcp backend::tests::{valid_replies_and_errors_are_preserved,
malformed_replies_are_rejected_without_resending} failed only when the whole
crate ran: the shutdown and one-shot-process tests start a copy of the test
binary, the child can inherit a socket a fake server just accepted, and when
the child exits Windows closes its copy abortively, resetting the test's
connection. A test-only lock keeps socket tests and child-process tests from
overlapping (passes 6 of 6 whole-crate runs, failed 4 of 4 before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SDR shoulder in ToneMap::display_hdr ran at neutral Contrast, Whites
and Blacks too, so an unedited JPEG or PNG exported neutral 255, 254 and
253 one level darker and a white background never came out as 255. The
shoulder now shapes edited curves only; neutral sliders are the identity
up to white and clip above it. Edited curves render as before (V1).
Tests: the identity test runs to 1.0, a new pipeline test renders the
issue's neutral greys back exactly, the sRGB ramp round trip is exact,
and a guard keeps the shoulder on edited curves.
`tests_staging` holds one `#[cfg(unix)]` test (#1502), so on Windows its
`use super::*` is unused and `cargo clippy --workspace --all-targets -- -D
warnings` (the clippy step of `cargo xtask ci`) fails in lightcraft-catalog.
Gate the module itself on Unix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Constrain Crop only applied at render time, and the Crop tool shows the
uncropped view with the stored crop, so in the Crop panel (where the
checkbox is) ticking it or moving a Geometry slider changed nothing.
Session::set_develop now shrinks the stored crop about its centre until it
fits the warped photo when Constrain Crop is turned on, or when an edit
changes the warp (Geometry, Upright, lens corrections, section eyes) while
it is on. A crop edit itself is kept as drawn.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
Describe merged the detector's per-instance masks. Those stay near 0 even well inside what they
found: a sky came out about 87 % selected, a volleyball net about 70 %, a person about 94 %.
Two instances that touch both fade out where they meet, so a line showed between people standing
side by side.
- The detector's semantic head is now ported: semantic_projection over the pixel decoder, the
transformers implementation's semantic_seg. It matches the reference to about 1e-3, like the
instance masks. tools/sam3_reference.py writes it and the reference test checks it; older
reference files still pass.
- Each instance found (score above the threshold, as before) takes the semantic logits where it
is the strongest instance and near (above -4). The semantic mask is sure inside and runs on
across the boundary between two instances. Far from every instance nothing changes, so
Describe selects nothing it didn't find before.
On 16 prompts over three photos (a mountain landscape, two gym photos), the median coverage
inside each selection rises, e.g.:
- sky 0.87 -> 1.00
- mountains 0.81 -> 0.99
- net 0.71 -> 0.99
- floor 0.92 -> 1.00
- person 0.94 -> 0.99
Each selection gains 0 to 1.9 % of the photo at its soft rim and loses none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OPFS lookups turned any exception into "absent", and start-up only logged
other read errors, so a library with an unreadable file opened incomplete
and the next save could overwrite the stored copy. Only NotFoundError now
means absent; when a stored library file or the active-library pointer
can't be read, the library isn't opened, nothing is saved and the window
names the file. An unreadable thumbnail index no longer deletes every
stored thumbnail as unindexed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK