Recover the unpublished denoise work against current upstream. Preserve later opt-in model setup, shared Rust HTTPS downloads, pending-photo resume and an Amount-independent switch. Keep original RAWs unchanged and share the current color finish between ordinary and denoised sources.
Bound public tiling inputs, refuse corrupt-cache exports, and retain synthetic/real-model coverage. Include two Windows path spelling fixes in existing tests. No model weights, face code, media or Tract runtime are included.
Validation: cargo xtask ci (all seven gates); installed RawNIND CPU/GPU agreement 1.38e-6; native CPU preview/export on a public 24 MP Sony RAW, unchanged original SHA-256.
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Generated with Codex.
- With no window (a mask or point-colour overlay, Visualize Spots, soft
proofing, a window refused for a far read) the loupe renders the whole
frame at the size it is drawn. While a slider drags that meant the
photo's every pixel each draft frame at 1:1 and beyond (24 MP per tick
where 2.5 MP was rendered before #351; mask overlays are on by
default). A drag now drafts at most the canvas size, as the windowed
path does; at rest the zoomed frame is rendered as before.
- The Preview size hint in German, Spanish and Russian; the replaced
'Largest long edge…' text removed from those catalogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
The desktop app keeps logs/lightcraft.log in its settings folder (#286)
and a failed start names it (#370), but a user still had to find the
folder by hand. The host now hands the UI its log file (Services
.log_file) and Help ▸ Open Log Folder (app.openLogFolder) reveals it
through the existing reveal service: Finder and Explorer select the
file, Linux opens the folder. Off where no log is kept (the web,
--memory, LIGHTCRAFT_NO_PREFS). Translated in de, es, ja, pt-br, ru,
zh-hans and zh-hant; parity row MENU-HELP-LOGFOLDER; What's New and
README note it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The desktop showed rfd's synchronous dialogs on the UI thread, so the
window stopped answering the compositor while one was up: GNOME marked
LightCraft "not responding", the File menu stayed drawn and the control
channel went quiet. Services.picker lets the host start a dialog
elsewhere and answer through a channel; a command that needs a path
asks for one and returns, and runs again with the answer when the
dialog closes (pick.rs). Import Photos, Import from Folder, Open
Library, Import Lightroom Catalog, Find Missing Photos, the preset and
curve-preset imports and exports, Auto-Tag from Tracklog and Locate
Missing File use it; hosts without a picker (the web, tests) keep the
synchronous dialogs. ui.inspect lists the waiting commands as
fileDialogs. Same cause as storytold/photocraft#574.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A full-size load asks the decoder for the largest box (`SourceLevel::Full` is
`usize::MAX`, cast to `u32::MAX`). `jpeg::decode` then computed `2 * u32::MAX` to
decide on DCT scaling: an overflow panic in debug builds (caught, so every
full-size JPEG load failed with "decoder panicked"), and a wrap that happened to
give the right answer in release. The comparison saturates now, and the engine
converts its `usize` edge with `u32::try_from` instead of a truncating `as`.
Found decoding camera JPEGs (no JFIF/EXIF header) at full size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lightroom Classic reads `ToneCurvePV2012Red/Green/Blue` only together with
`ToneCurvePV2012`: a packet with a channel curve and no master curve leaves the
photo unchanged there (rendered on a chart: the channel curve alone has no
effect, with an identity master curve it applies). Imports now do the same.
The preset-packet test helper uses made-up as-shot values.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
XMP presets usually say LensProfileEnable=0, which turned optics.lens_profile
off and with it the DNG-embedded WarpRectilinear / FixVignetteRadial
corrections (iPhone ProRAW and other files that carry them). Lightroom
applies a file's built-in corrections whatever that switch says (our
inference), and LightCraft's profile corrections are only those embedded
ones, so crs import now carries over only an enabling switch. The Enable
Lens Corrections checkbox and the amount sliders keep working as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
XMP presets carry fields that describe the preset or the photo rather than
an adjustment (Cluster, SupportsAmount2, RequiresRGBTables, SortName,
Description, CropConstrainToWarp, OverrideLookVignette, AsShotTemperature /
AsShotTint). preset.import listed them as settings that couldn't be carried
over; so did an HDREditMode of 0 and PointColors slots that are all -1.
They are now treated as bookkeeping; an HDR edit, a used Point Color slot and
unknown adjustments are still reported.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
clippy 1.98's manual_chunks lint fails `cargo xtask ci` on main at psd.rs:170/191/201
(chunks_exact with a constant size). The fixed-size chunks also drop the per-sample
indexing. No behaviour change; the codecs tests pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CR2 and PEF decode without a camera colour matrix, like ARW, NEF, RW2,
RAF and CR3, but camera_preview::file_local_look (and the catalog's
relative white balance) left them out, so they rendered with the neutral
fallback matrix: the camera preview showed first, then a flat,
desaturated render replaced it. They now use the same guarded fit and
relative WB; a fit that fails the gates keeps the fallback, so a file can
only stay as it was or move closer to its camera JPEG. Not measured on
the corpus here (raw.pixls.us is blocked from this environment).
RENDER_CACHE_VERSION 15 re-renders cached CR2/PEF thumbnails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
egui keeps a popup inside the window, so a menu taller than the room
under the bar (Photo, on a short window) was slid up over the bar: the
titles vanished under it, and the pointer left on the title rested on
the first row and opened its submenu by itself. Every level now gets
the room it has below the bar (a submenu: from its row down, or up to
the bar when egui opens it upward) and scrolls inside it, with wheel,
drag and ▲/▼ strips that scroll while the pointer rests on them, as in
PhotoCraft's menu bar (storytold/photocraft#337). Menu levels and
submenu rows are registered for the control channel and tests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A review measured 1 GB of GPU stages for a Before/After at 1:1 on a 2800 px
canvas against a 400 MB budget, with nothing bounding it. The open photo's window
and before caches are now cut (the before side first) past four times the budget,
the whole-frame view's never; idle caches still go at the budget. The one shared
device copy of a big original is reported (sharedSourceBytes in the renderer's
memory report) and a misleading comment about it is corrected.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
At first open of Before/After the two window jobs were asked for together, each
decoded its own 24 MP copy of the original (a review saw two decodes and three
uploads for one photo) and the shared device copy was swapped between them. The
Before's window now waits until the After's job has handed the original to the
session (region::defer_before_window, Session media has_source); later windows
of both share the one copy.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The window of the Before view alone was clipped to the wipe's left half while the
picture drawn was the Before on the whole canvas, leaving the right half the
low-resolution stand-in. The clip follows the effective mode.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A review showed the wipe test asserted on a hand-written list of labels (deleting
the stand-in draw calls still passed) and nothing tested the clip at the call
site (replacing it with the canvas still passed). The tests now read the meshes
the frame painted: the Before window comes after the stand-in and stays inside
its side of the line, and side by side each window is painted inside its pane.
Both mutations of the code now fail them.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A review measured the drag at 100 % on a 2800 px canvas taking 87 ms a tick
against 24 ms with a bigger budget: the open photo's caches were over the budget
(a 5 MP window alone is 230 MB) and a "hard" budget cleared them every frame, so
every tick rebuilt them. The whole-frame, zoom window and before caches are what a
drag reuses; only idle caches (hover, compare tiles, second window) are trimmed
now, the least useful first, and the open photo's are counted and reported. The
drag test no longer waits on a tick count.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Keeping a 288 MB upload with each view's stages made them thrash against the stage
budget; not keeping it made every new window (a pan crossing a snap, the first
window of the Before side, any control that invalidates the sampled pixels) upload
and convert the source again, about 64 ms against 6-12 ms. A source too big for a
view's stages is now kept once for all views of the photo (shared_source), outside
the views' stages, and given back when the app idles (trim_pool(0)).
source_uploads and shared_source_bytes expose it for tests and diagnostics.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Review findings on region::plan:
- a drag at fit on a 4K or 5K display switched the window off (the whole-frame
draft shrinks, the preview-source cap does not): the window decision now
compares against the undrafted whole-frame render;
- overlays, soft proofing and windows refused for reading too far (a spot or Auto
Mask stroke) left the 1:1 view a canvas-sized upscale: those modes render the
whole frame at the size it is drawn, as before windows (a refusal is remembered
per photo, look and frame, and retried when any changes);
- the 3840 and 5120 preview sizes had become no-ops: a limit above the preview
source level is honoured;
- windows were limited to 8192 px frames: they now reach the photo's own pixels
(65536 px at most), so a medium-format photo is sharp at 1:1;
- a pinch out from a deep zoom asked for an ever wider window every frame: windows
already there are kept, magnified, until the gesture has been quiet.
The unused region::needed is gone.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An adversarial review found that (1) in a wipe the Before window was drawn
before, so under, the low-resolution Before stand-in; (2) the windows of side by
side and stacked views were clipped to the canvas, so their margins spilled into
the neighbouring pane; (3) with identical Before and After looks (a fresh,
unedited photo) both windows had the same job key and the After tile replaced the
Before one in the tile table. Windows are now drawn over the stand-ins and under
the wipe line, clipped to their pane or their side of the line (region::window_clip,
pure and tested), and tiles are keyed by side and job key.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
profile_slider_drag prints the milliseconds from a slider tick to the loupe
showing it at fit, 100 % and 400 % of the demo library's 24 MP photos.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A drag supersedes every request before it finishes, and the source a superseded
draft had decoded was thrown away with it, so a window cut from a full-size
original decoded it again for every tick. A view slot's superseded draft that is
newer than what the view shows now also hands its source to the session (late
results of other slots stay ignored, as before). Render results become textures
with a plain copy for the opaque case instead of egui's per-pixel premultiplying
conversion, on the UI thread.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Profiling a slider drag at 100 % and 400 % found each tick of the window render
taking about 60 ms: the 24 MP source (288 MB) uploaded to the device was kept with
the view's stages, counted against the stage budget, and the stages cleared every
frame, so every tick uploaded and resampled again. A source over 96 MB is now
uploaded for the render only; the stages keep the window's own sampled pixels.
A tick costs 7-12 ms instead. equivalence.rs covers it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A continuous trackpad pinch changes the zoom every frame, and with it the sizes
the loupe plans, so it would have been a stream of re-renders of every size in
between. SizeHold (pure, tested) keeps the plan from before the gesture while it
runs and for a quarter of a second after; the view is drawn from the renders it
has, magnified by the GPU, and planned afresh when the gesture is quiet (a repaint
is asked for at that moment). Another photo resets the hold.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Before image was a stand-in capped at 2560 px beside an After at full
resolution, so a 1:1 comparison misled. Both sides now get the same treatment:
the same whole-frame size and, once zoomed, a window each (Slot::RegionBefore
beside Slot::Region) cut from the same frame, in side by side, top/bottom, the
\ original view and both wipes (the Before window is clipped to its side of the
line). Each side's windows and tiles are tracked apart, a side is freed when it
is no longer shown, and ui.inspect reports regionBefore.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The loupe's, the zoom window's and the before / hover renders' stage caches hold
decoded-image sized buffers that the engine's budget did not count. A quarter of
the memory budget is now theirs: each frame the renderer clears what exceeds it,
the least reusable first (hover, before, compare, second window, the zoom window)
and the open photo's own view last (stage_trim_order, pure and tested). The
inspector's memory report shows the budget and how often caches were trimmed.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
At 8:1 on a 24 MP photo every drag frame rendered the whole 24 MP frame, and a
Retina fit view (about 2800 px) decoded the original of every photo stepped to.
A new render plan (region::plan, pure and tested) makes the cost follow the
canvas, not the zoom:
- the whole-frame render is canvas-sized and never above the preview source
level, the preview size limit or the texture side (a drag drafts that);
- once the view is zoomed past it (by more than 25 %) the visible window is
rendered, cut from a frame that is at most the photo's own pixels, so beyond
100 % the GPU magnifies it instead of a bigger render being made; this also
ends the duplicated second render above 100 %;
- a window is drafted during a drag and a draft of an older value still draws;
- a user who capped the preview size keeps the whole-frame render at fit;
- no window where the picture on screen isn't the frame's own aspect (an
unsupported raw's embedded JPEG cropped differently, #280).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>