mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-11 16:31:32 -06:00
LR-IMP-PREVIEW-READ: an unedited raw's grid thumbnail is its embedded JPEG
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>
This commit is contained in:
co-authored by
DeepSeek V3.2
parent
ab438b05ca
commit
bc2cbf41db
@@ -174,6 +174,8 @@ pub struct Renderer {
|
||||
/// Last thumbnail inputs; weak identities do not retain photo histories or force deep clones.
|
||||
/// (photo, size bucket, job key, whether the request went through an embedded stand-in)
|
||||
thumb_inputs: HashMap<PhotoId, (std::sync::Weak<lightcraft_catalog::Photo>, usize, u64, bool)>,
|
||||
/// Photos whose grid thumbnail is the camera's embedded preview itself (no render behind it).
|
||||
embedded_thumbs: HashMap<PhotoId, (std::sync::Weak<lightcraft_catalog::Photo>, usize, u64)>,
|
||||
pool: JobPool<Slot, RenderResult>,
|
||||
/// Jobs run elsewhere (see [`RenderOffload`]); `queue` holds the ones not started yet.
|
||||
offload: Option<Box<dyn RenderOffload>>,
|
||||
@@ -251,6 +253,7 @@ impl Renderer {
|
||||
preview_generation: None,
|
||||
pool: JobPool::new(threads),
|
||||
thumb_inputs: HashMap::new(),
|
||||
embedded_thumbs: HashMap::new(),
|
||||
offload: None,
|
||||
queue: Vec::new(),
|
||||
seq: 0,
|
||||
@@ -295,6 +298,30 @@ impl Renderer {
|
||||
}
|
||||
|
||||
/// Remember the inputs of the thumbnail just requested (`quick`: through a stand-in).
|
||||
/// Whether the camera's embedded preview *is* this photo's thumbnail: its quick render arrived
|
||||
/// (so `Slot::ThumbQuick` holds the picture) and no edit has moved the photo on to a settings
|
||||
/// hash of its own. The grid stops here instead of decoding the raw behind a thumbnail the
|
||||
/// embedded JPEG already covers — see `grid::skips_the_real_render`.
|
||||
pub fn thumb_quick_is_final(&self, photo: &Arc<lightcraft_catalog::Photo>, bucket: usize, priority: u32) -> bool {
|
||||
let Some((old, size, key)) = self.embedded_thumbs.get(&photo.id) else { return false };
|
||||
old.as_ptr() == Arc::as_ptr(photo) && *size == bucket && !self.request_needed(Slot::ThumbQuick(photo.id), *key, priority)
|
||||
}
|
||||
|
||||
/// Whether this photo's embedded-preview thumbnail is being fetched right now: the grid has
|
||||
/// asked, and neither the picture nor a render behind it is owed.
|
||||
pub fn thumb_quick_pending(&self, photo: &Arc<lightcraft_catalog::Photo>, bucket: usize) -> bool {
|
||||
let Some((old, size, _, quick)) = self.thumb_inputs.get(&photo.id) else { return false };
|
||||
old.as_ptr() == Arc::as_ptr(photo) && *size == bucket && *quick && self.pending.contains_key(&Slot::ThumbQuick(photo.id))
|
||||
}
|
||||
|
||||
/// Remember that the camera's embedded preview is this photo's final thumbnail, so the grid's
|
||||
/// per-frame requests stop at it (the quick render's key, which [`Self::thumb_quick_is_final`]
|
||||
/// compares against).
|
||||
pub fn remember_embedded_thumb(&mut self, photo: &Arc<lightcraft_catalog::Photo>, bucket: usize, key: u64) {
|
||||
self.remember_thumb(photo, bucket, key, true);
|
||||
self.embedded_thumbs.insert(photo.id, (Arc::downgrade(photo), bucket, key));
|
||||
}
|
||||
|
||||
pub fn remember_thumb(&mut self, photo: &Arc<lightcraft_catalog::Photo>, bucket: usize, key: u64, quick: bool) {
|
||||
if self.thumb_inputs.len() >= 1024 && !self.thumb_inputs.contains_key(&photo.id) {
|
||||
self.thumb_inputs.retain(|_, (p, _, _, _)| p.strong_count() > 0);
|
||||
@@ -1026,8 +1053,9 @@ mod thumbnail_tests {
|
||||
|
||||
#[test]
|
||||
fn raw_quick_path_frames_build_no_jobs() {
|
||||
// an unedited raw: the grid shows its embedded preview first (held here until released),
|
||||
// then queues the real thumbnail behind everything on screen
|
||||
// an unedited raw: the grid shows its embedded preview (held here until released) and stops
|
||||
// there — the camera JPEG at 4 416 × 2 944 already covers a 512 px thumbnail, so decoding the
|
||||
// whole raw behind it only costs the file read (45 s a photo on a 1.2 MB/s share)
|
||||
let gate = Arc::new(std::sync::Mutex::new(()));
|
||||
let held = gate.lock().unwrap();
|
||||
let mut s = Session::new();
|
||||
@@ -1055,14 +1083,30 @@ mod thumbnail_tests {
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
app.renderer.poll(&ctx, &mut app.session);
|
||||
}
|
||||
// the stand-in is shown: one more job queues the real thumbnail at background priority,
|
||||
// and the grid asking at its own (higher) priority every frame does not rebuild it
|
||||
// the embedded preview is the thumbnail: no real render is queued behind it, and the grid
|
||||
// asking every frame neither rebuilds the job nor re-requests the preview
|
||||
for _ in 0..180 {
|
||||
request(&mut app);
|
||||
}
|
||||
assert_eq!(app.renderer.thumb_jobs_built, 2);
|
||||
assert_eq!(app.renderer.queue.len(), 1);
|
||||
assert_eq!(app.renderer.pending.get(&Slot::Thumb(PhotoId(1))).map(|p| p.1), Some(crate::panels::grid::BACKGROUND_THUMB_PRIORITY));
|
||||
assert_eq!(app.renderer.thumb_jobs_built, 1, "an unedited raw's embedded preview is the thumbnail");
|
||||
assert!(app.renderer.queue.is_empty(), "nothing is decoded behind it");
|
||||
assert!(!app.renderer.is_pending(Slot::Thumb(PhotoId(1))));
|
||||
|
||||
// an edit takes the embedded preview out of the picture: the real thumbnail is rendered then
|
||||
let mut edited = (*photo()).clone();
|
||||
edited.kind = lightcraft_catalog::MediaKind::Raw;
|
||||
edited.develop = Arc::new({
|
||||
let mut d = edited.camera_defaults();
|
||||
d.light.exposure = 0.5;
|
||||
d
|
||||
});
|
||||
let (mut app, _, _ctx) = fixture_with(Session::new(), edited);
|
||||
app.renderer.set_offload(Box::new(Blocked));
|
||||
for _ in 0..180 {
|
||||
request(&mut app);
|
||||
}
|
||||
assert_eq!(app.renderer.thumb_jobs_built, 1);
|
||||
assert!(app.renderer.is_pending(Slot::Thumb(PhotoId(1))) || !app.renderer.queue.is_empty(), "an edited raw renders its thumbnail");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user