From 701b57bfe8abe5ee2818b46bdff0d6fd1cb355c5 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 26 Jun 2026 05:39:35 -0400 Subject: [PATCH] video: thumbnails force CPU frames (fix black thumbs + decoder thrash) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The asset-library thumbnail called VideoManager::get_frame, which honors the render-pass hardware flag — left ON by the preview, so the thumbnail got a GPU frame with empty rgba_data → an all-black thumbnail. The thumbnail is also the only consumer that requests a fixed low timestamp (1.0s) on the shared per-clip decoder, so when it re-decodes during playback it yanks the decoder back to ~1s; the next playback frame (6.x) is then ">2s forward" and re-seeks to the keyframe + re-decodes the whole GOP (the jerk: per-frame decode is ~5ms, but these seeks cost 40ms + N-frame catch-up). Add VideoManager::get_frame_cpu (forces want_gpu=false regardless of the render flag); the thumbnail uses it. Now it produces a real RGBA thumbnail that the editor texture-caches once, instead of re-decoding black frames. Co-Authored-By: Claude Opus 4.8 --- lightningbeam-ui/lightningbeam-core/src/video.rs | 11 +++++++++++ .../lightningbeam-editor/src/panes/asset_library.rs | 6 ++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-core/src/video.rs b/lightningbeam-ui/lightningbeam-core/src/video.rs index 8fb8552..67770d4 100644 --- a/lightningbeam-ui/lightningbeam-core/src/video.rs +++ b/lightningbeam-ui/lightningbeam-core/src/video.rs @@ -940,6 +940,17 @@ impl VideoManager { // Whether this pass wants (and can produce) a GPU frame. Gated on HW being configured at all // so that with software-only decode preview and export share one cache entry (no double-cache). let want_gpu = self.render_hardware_ok && self.hw_device.is_some(); + self.get_frame_inner(clip_id, timestamp, target_w, target_h, want_gpu) + } + + /// Like [`get_frame`](Self::get_frame) but always returns a CPU (RGBA) frame, ignoring the + /// render-pass hardware flag. For consumers that need pixel bytes (thumbnails, image readback) + /// regardless of whether a render pass last enabled GPU frames. + pub fn get_frame_cpu(&mut self, clip_id: &Uuid, timestamp: f64, target_w: u32, target_h: u32) -> Option> { + self.get_frame_inner(clip_id, timestamp, target_w, target_h, false) + } + + fn get_frame_inner(&mut self, clip_id: &Uuid, timestamp: f64, target_w: u32, target_h: u32, want_gpu: bool) -> Option> { // The cache key includes (target size, want_gpu): preview (GPU, preview res) and export // (CPU, export res) request the same clip/time and must not collide or cross representation. let timestamp_ms = (timestamp * 1000.0) as i64; diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/asset_library.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/asset_library.rs index a76fc80..0132c8f 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/asset_library.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/asset_library.rs @@ -316,8 +316,10 @@ fn generate_video_thumbnail( let frame = { let mut video_mgr = video_manager.lock().ok()?; - // Small frame for the asset thumbnail (capped to native, aspect preserved). - video_mgr.get_frame(clip_id, timestamp, THUMBNAIL_SIZE, THUMBNAIL_SIZE)? + // Small CPU frame for the asset thumbnail (capped to native, aspect preserved). Force CPU: + // the render-pass hardware flag may be on, but the thumbnail needs RGBA bytes (a GPU frame + // has empty rgba_data → an all-black thumbnail). + video_mgr.get_frame_cpu(clip_id, timestamp, THUMBNAIL_SIZE, THUMBNAIL_SIZE)? }; let src_width = frame.width as usize;