video: thumbnails force CPU frames (fix black thumbs + decoder thrash)

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 <noreply@anthropic.com>
This commit is contained in:
Skyler Lehmkuhl 2026-06-26 05:39:35 -04:00
parent ca9a70e10a
commit 701b57bfe8
2 changed files with 15 additions and 2 deletions

View File

@ -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<Arc<VideoFrame>> {
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<Arc<VideoFrame>> {
// 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;

View File

@ -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;