The grid already showed the camera's embedded preview first, then queued a render of the whole raw
to replace it. For a 512 px thumbnail that render decodes 50 of a 56 MB Fuji RAF's bytes for a
picture the 4 416 × 2 944 embedded JPEG already covers — 45 s a photo on the 1.2 MB/s share this was
measured on, 8.3 GB of traffic for a 149-photo folder, and a queue that delayed the render the user
was actually waiting for (measured: the first loupe render of a cold photo finished 174 s after the
click, most of it spent behind thumbnails). Idle for a minute afterwards, the same folder read 12
previews (26.9 MB) and decoded zero raws for thumbnails, against 638 MB of whole-file reads before.
- `Session::embedded_thumb_is_final` answers whether a photo is an unedited raw whose camera JPEG the
viewer is showing — the same `has_import_look` gate the preview path uses.
- `grid::skips_the_real_render` is the decision (unit-tested): a cached render as stand-in, an edited
photo and a raw without an embedded JPEG all still render, because their thumbnail must show the
pipeline's output rather than the camera's.
- `Renderer::remember_embedded_thumb` records the quick render's key and the bucket, and
`thumb_quick_is_final` / `thumb_quick_pending` let the per-frame grid request stop at the embedded
preview instead of rebuilding the render job (and its cache key) every frame.
- The thumbnail key still follows the settings hash, so editing a photo re-renders its thumbnail
through the ordinary background path.
`docs/raw-preview-reads.md` and the `LR-IMP-PREVIEW-READ` parity row record the behaviour and the
numbers.
Co-Authored-By: DeepSeek V3.2 <noreply@deepseek.com>
A grid thumbnail and a loupe preview of an unedited raw show the camera's embedded JPEG, which
sits near the start of the file — for a 24 MP Fuji RAF, 5.6 MB of a 56 MB file (measured:
DSCF9492.RAF). The loader read the file whole and threw the sensor data away, so every preview
paid for 50 MB nobody looked at. On a local disk that is merely wasteful; on a network share,
where photographers keep libraries, it is the wait: the same preview took 10 s over a GVFS/SMB
Wi-Fi share against 0.96 s after this change.
- `lightcraft_raw::bounded_preview(bytes)` locates the preview from the header alone and reports
its byte range; `embedded_preview_from_range` extracts it, byte for byte what `embedded_preview`
returns for the whole file (the pointer is read from the RAF header's fixed u32s at 84/88).
- `engine::files::preview_window` reads a 4 KiB prefix, then — because the container reader clamps
a slice to the buffer it was given, so a short prefix reports a plausible but truncated 3948
bytes — re-reads exactly the extent the header declares, and takes the window out of the prefix
rather than reading those bytes twice. `LIGHTCRAFT_PROFILE=1` prints what each preview cost.
- Formats whose preview only the whole file reveals (CR3 boxes, TIFF pointer tags past the prefix,
CRW/MRW/X3F scans), a lying pointer, a short read and a file that changes underneath us all fall
back to the whole-file path: a damaged header never costs the user their preview.
- Tests: the header range matches `embedded_preview` byte for byte, with a clamped prefix, a padded
extent, a late pointer, a lying pointer and a non-JPEG window covered; on a synthetic RAF with a
200 KB tail the quick path and the whole-file path produce identical pixels and the read stays
under half the file; `crates/engine/tests/preview_read.rs` (opt-in, `#[ignore]`) measures a real
file on a share. No raw fixture is committed.
- `docs/raw-preview-reads.md` records the mechanism and the numbers.
Co-Authored-By: DeepSeek V3.2 <noreply@deepseek.com>