A drag tick requested the loupe's draft and the frame ended; the draft,
done in 1.5-3 ms on the GPU, was picked up at the start of the next frame,
a frame after the slider moved. Now, in a drag, the loupe waits up to 40 %
of a frame for its draft when the last loupe render took well under that
(not with a render offload, nor zoomed in, where what changes on screen is
the window's draft). A CPU render, which would only make the frame late, is
not waited for.
JobPool::wait waits for the next finished job, which try_recv then hands
out; Renderer::poll's collection of finished jobs is split out so the wait
can turn the draft into the texture.
Co-Authored-By: Claude <noreply@anthropic.com>
The semibold family had a semibold Inter but, for other scripts, the same
regular craft-fonts faces as regular text, so a heading or the chosen tab
in Arabic was drawn at regular weight beside bold Latin.
A fallback face with a weight axis (today Noto Sans Arabic, 100 to 900)
is now registered a second time for the semibold family at weight 600,
through epaint's FontTweak::coords. The bytes are borrowed from the
embedded font, not copied. Faces with one fixed weight, regular and
monospace text, the order of fallbacks and Latin are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With GPU rendering on and no graphics adapter at all (wgpu: "no GPU adapter among …", e.g. no
Vulkan loader: a `cargo run` outside `nix develop` on NixOS), every render ran on the CPU, much
slower, and only Settings ▸ Performance, Help ▸ System Info and `perf.gpuReason` said why.
The desktop app now queues one start-up notice per session once the device has been tried
(`gpu::ready()`, never blocking): "No graphics processor found: LightCraft renders on the CPU, which
is much slower. Details in Help → System Info.", on Linux with "On NixOS, start it from `nix
develop`." (→, not ▸: no installed font paints ▸.) `panels::notices::gpu_notice` decides from the
reason: nothing when the GPU was turned off (preference, LIGHTCRAFT_GPU, LIGHTCRAFT_GPU_BACKEND=off),
only a software adapter was skipped, or it failed later. Headless sessions and tests never show it
(`app.gpu_notice` is set by the desktop app only). Translated in all nine catalogs; one README
sentence in the Nix section.
Co-Authored-By: Claude <noreply@anthropic.com>
The A7 III's raw-IFD 0x7037 tables were refused until their geometry was checked. Measured against
the camera's own JPEG of the same exposure (Lens Comp. Distortion: Auto) on 27 RAW+JPEG pairs from
one body with the FE 16-35 F4 ZA, FE 28-70 and FE 50 F1.8 (strong barrel to strong pincushion,
compressed and uncompressed, TV and monitor checkerboards): corrected board corners sit 0.06-0.44 px
(median, 6000 px frame) from the JPEG's, against 0.8-26 px uncorrected; LightCraft applies the
camera's amount of correction within 2 %; corrected rows and columns are as straight as the JPEG's.
Sony's crop and the default crop agree on this body, so the warp is centred on the image.
A unit test pins the warp from three of these tables to the camera's fitted geometry; the model
boundary tests now accept ILCE-7M3 and still refuse its neighbours. RENDER_CACHE_VERSION is bumped.
The measurement script handles screen moiré (X-corner check, duplicate corners, consensus lattice
vectors). The corpus test's two ILCE-7M3 samples now expect one warp; raw.pixls.us is not reachable
from this environment, so that test was skipped here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015gRtdWF1E9358FfzhzYqCS
MediaCache::enforce_budget never evicts the original of the photo on screen (set_on_screen), but it still
counted it. A 64 MP original (Sony A7R VI: 9984×6656 linear float, 760 MiB) nearly fills the default cache
share (768 MiB) alone, so the loop evicted everything else instead: the photo's preview and thumbnail sources,
the rendered previews and the prefetched neighbours. Every edit at 1:1 (the whole-frame render and the
thumbnail), every zoom out and every step to the next photo then decoded the raw again.
The pinned original is now left out of what is compared with the budget. The rest still fits the share; the
original sits over it, as the first commit of this PR already allowed.
Co-Authored-By: Claude <noreply@anthropic.com>
Each drag tick changed the edited photo's settings, so the filmstrip asked
for a new thumbnail render every frame. For an unedited raw shown with its
embedded preview, the thumbnail-level source had never been decoded, so each
of those renders decoded the whole raw (a 64 MP ARW: over a second), several
side by side, and the loupe's drafts queued behind them.
- The edited photo's thumbnail is not re-rendered during a slider drag while
it has one; it follows once the drag ends.
- A thumbnail of a photo whose preview-level source is decoded (the one in
the loupe) is cut from that source (SourceRef::Fit) instead of decoding the
file again; not when the photo has a denoised picture.
Co-Authored-By: Claude <noreply@anthropic.com>
The D90 writes 4352 x 2868 samples whose last 42 columns (from 4310) are
filler at a flat ~254, above its dark image samples (black 0). The
masked-column test only trims columns near zero, so they stayed in the
image and developed as a pink band on the right. Measured on 10 of 550
NEFs from one body: the camera's 4288 x 2848 JPEG sits at (10, 10), and
green saturates at 3840-3851 while red and blue run on to 4095, so one
white level of 3830 keeps clipped highlights white. The D90 joins the D4
in MEASURED_BODIES; other bodies are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sony ARWs of cameras without measured colour matrices were edited on the
relative scale (6500 K / 0 = as shot). Their SR2SubIFD stores the
multipliers of the camera's presets at 2500, 3200, 4500, 6000 and 8500 K;
lightcraft_raw::kelvin_presets reads the as-shot multipliers on the curve
through them and they take the spectral matrices' place in the #730
design (as-shot Kelvin and tint only; look, Bradford adaptation and the
pixel-identical legacy upgrade unchanged).
Covered: ILCA-99M2, ILCA-77M2, SLT-A33, DSC-RX10, ILCE-3500, ILCE-7S.
Kelvin-dial shots read back 3400 -> 3396, 5200 -> 5193, 4700 -> 4708,
5600 -> 5594 K; Shade 7493 K. Off-curve constants (isotherm tilt 0.74,
tint spacing 1) measured from the 14 Sony bodies with both spectral
matrices and presets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Document polling of selection/settings/control ranges and shared edit
interactions. Clarify explicit-photo reads, empty selection and navigation,
and correct photo-tool ID descriptions and MCP initialization guidance.
Partial documentation work for issue 251;
hardware feedback remains outside this change.
Co-authored-by: Codex <codex@openai.com>
Add correlated dark holdouts that reject misleading training and accept
matching training. Check finite, monotone curves in both cases. Confirm
the regression fails with the error gate removed and passes when restored.
Qualify the improvement claim when no original curve can be fitted, and
explain the source-look process policy and offline smart preview refresh.
Co-Authored-By: Codex <noreply@openai.com>
Use separate luminance pairs for tone fitting so the camera look follows
its JPEG into deep shadows. Keep colour pairs and holdouts unchanged.
Require enough correlated shadow samples and lower held-out error, with
noise and compression floors and the existing median curve fit.
Add synthetic regressions, bump look and render cache versions, and
record the public multi-make and private numeric validation in the docs.
Fixes#1307
Co-Authored-By: Codex <noreply@openai.com>
A debugging toggle (`view.frameRate`, `{show?}`, a checked item in a new Help ▸ Debug
submenu; LIGHTCRAFT_FPS=1 starts with it on; never saved). In the window's top-right
corner it draws frames per second and the worst over the last second, each frame's UI
CPU time against its time spent waiting (vsync, or the GPU), the latest whole-frame
and zoom-window render times, the render queue, renders thrown away as superseded
(Renderer::discarded), the GPU adapter and its memory, and a graph of the last frames
with a tick where a render arrived. It keeps the window repainting while shown and
logs the same numbers once a second (`fps-hud:`), so scripted drags can be measured.
Perf::host_frame_ms (the frame's CPU time as eframe reports it) is added as #1009
adds it, identically.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An uncompressed Leaf Aptus 22 MOS keeps its 16-bit mosaic in IFD0
(compression 1, BlackIsZero, one sample per pixel) with no Make tag and
no CFA photometric, so `probe` returned None and the file imported as a
greyscale image of the mosaic. Leaf's data block, tag 0x8606 (LeafData
in the ExifTool EXIF tag-name documentation), now marks such an IFD0 as
a raw: OtherTiff, preview only with the reason, as the lossless-JPEG MOS
already is. A rendered RGB image that kept the block stays an image.
The Hasselblad Lunar is a Sony NEX-7 that writes an ARW under Make
HASSELBLAD; only SONY files reached the ARW reader, so it opened as its
preview. A HASSELBLAD file with Sony's raw coding (an IFD with
compression 32767 or the tone-curve tag 0x7010, which no 3FR / FFF has)
now goes there and decodes like a NEX-7: black 512, the SR2SubIFD white
balance. The Tag2010 white-balance fallback, the one table arw.rs keys
by model, takes the body from the maker note's SonyModelID (0xb001)
when the file is not Sony-branded.
Docs: the Lunar is verified in docs/raw/evidence.tsv (236 verified),
raw-parity.md, parity-checklist.md, gaps.md and whats-new.md.
RENDER_CACHE_VERSION 61 -> 62: a Lunar's thumbnails were rendered from
its preview.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With "Leave them", the review refused every click on a file whose photo is in
Recently Deleted, and its "In Recently Deleted" badge was drawn over the
checkbox. Leave them now only keeps such files out of the default selection
(Check All and the file-type toggles skip them); a click checks one and it is
restored, as the import does by default. The badge goes under the checkbox
when it doesn't fit beside it.
One widget drew the tab bar and every row of choices: on General the
languages read as a second tab bar. What was chosen differed from what was
not by its fill alone, 1.35 to 1.56:1 in the four themes, and a tab or choice
reached with Tab showed nothing (the frame with and without the focus was
the same, pixel for pixel).
- widgets::tab_button: a tab is its name on the window, a size up from the
headings inside a page, the open one in the text colour over a 2 pt line
of that colour; no border, no fill. Each line of tabs stands on a 1 pt
line the width of the window, so the open tab's line never hangs over the
names below when the tabs take two lines (a narrow window, or Russian at
full width).
- widgets::choice_button, used by settings::choices: the chosen option keeps
its fill and is outlined in the label colour (5.5:1 or more against its
fill, a button and the window in every theme), and is a radio button to a
screen reader. The Export dialog's own rows of choices are as they were.
- Both, and text_button wherever it is used, show a ring while they have the
keyboard focus: 2 pt in the text colour with a 1 pt line of the surface's
colour inside it, within their rect. A double line, where a chosen option
has a single thin one, so the two are told apart by shape. Tab, the arrow
keys, Space and Return already worked.
Widget ids are as they were (button:settingsTab-{id}, button:{id}-{index}).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>