render: fix 4K playback memory growth and decode-to-target-size
Root causes found for the 4K stalls and the second-footage memory blowup (audit + code review): - ticket bookkeeping leaked unbounded: the procpool ticket table and the arena slot map only ever grew (50-100 tickets/sec during playback, each pinning montage params and shm region views). Completed/cancelled/superseded/crashed entries are now removed, and the arena reaps fire-and-forget tickets once finished; the sync poll path reaps via a terminal result() read. InFlight duplicate submits now answer State immediately instead of sitting in the map forever. - decode ran a full-resolution swscale to F32 RGBA (~132 MB at 4K) plus a second full-res copy before downscaling to the 480px proxy: RetrieveVideoParams.target_size lets swscale convert AND resize in one pass (bilinear, matching the old Rust resampler), so a 4K preview frame costs ~1 MB instead of ~260 MB of churn. This applies to proxy AND full-res requests alike. - per-process decoder cache was unbounded (each session pins an FFmpeg context + 2 native decoded frames): LRU-capped at 16, eviction drops the map entry (in-flight renders keep their Arc; Drop releases FFmpeg). - playback window completions were not generation-gated: a stale render from before an edit landed in the rebuilt window (wrong frame displayed, fresh request blocked). Stale completions now return their shm slot credit instead. - async audio prefetch used the polling ticket submit without ever polling: switched to the fire-and-forget submit so entries reap.
This commit is contained in:
@@ -136,6 +136,13 @@ pub struct RetrieveVideoParams {
|
||||
pub mode: RenderMode,
|
||||
/// Frame alpha channel is premultiplied.
|
||||
pub alpha_is_premultiplied: bool,
|
||||
/// Target output size for the scaled frame: `Some((w, h))` lets the
|
||||
/// decoder's swscale pass convert AND resize in one step (native
|
||||
/// yuv → RGBA/F32 at the target size), skipping the full-resolution
|
||||
/// float intermediate (a 4K frame is ~132 MB as F32 RGBA — decoding
|
||||
/// to a 480px preview through it costs ~260 MB of churn per frame).
|
||||
/// `None` keeps the native size (the old behavior).
|
||||
pub target_size: Option<(u32, u32)>,
|
||||
}
|
||||
|
||||
/// `Decoder::RetrieveAudioStatus` — outcome of an audio retrieve.
|
||||
@@ -708,6 +715,7 @@ mod tests_unimplemented {
|
||||
image_sequence_number: 0,
|
||||
mode: RenderMode::Offline,
|
||||
alpha_is_premultiplied: false,
|
||||
target_size: None,
|
||||
};
|
||||
assert!(d.retrieve_video_frame(&p).is_err());
|
||||
assert!(d.retrieve_video(&p).is_err());
|
||||
|
||||
@@ -332,7 +332,7 @@ impl Decoder for FFmpegDecoder {
|
||||
let f = decoded?
|
||||
.ok_or_else(|| fail("no video frame available at the requested time"))?;
|
||||
|
||||
let (w, h, bytes) = state.scale_video_to_f32(f, p.force_range)?;
|
||||
let (w, h, bytes) = state.scale_video_to_f32(f, p.force_range, p.target_size)?;
|
||||
let frame = copy_rgba_f32_to_frame(w, h, &bytes, p.time)?;
|
||||
Ok(Arc::new(frame))
|
||||
}
|
||||
@@ -908,10 +908,15 @@ impl DecoderState {
|
||||
/// Scale a decoded frame to float RGBA (F32, 4 channels), returning the
|
||||
/// raw pixel bytes plus dimensions. Mirrors `pre_process_frame` +
|
||||
/// `retrieve_video_frame_internal` scaling with the color-range forcing.
|
||||
/// `target_size` resizes in the same swscale pass (native → RGBA/F32 at
|
||||
/// the target size) instead of converting at native size first — the
|
||||
/// caller's downscale then degenerates to a plain copy, and no
|
||||
/// full-resolution float intermediate (~132 MB at 4K) ever exists.
|
||||
fn scale_video_to_f32(
|
||||
&mut self,
|
||||
f: ffmpeg::frame::Video,
|
||||
force_range: i32,
|
||||
target_size: Option<(u32, u32)>,
|
||||
) -> crate::error::Result<(u32, u32, Vec<u8>)> {
|
||||
let video = self
|
||||
.video
|
||||
@@ -929,7 +934,12 @@ impl DecoderState {
|
||||
ffmpeg::color::Range::MPEG
|
||||
});
|
||||
|
||||
let (w, h) = (f.width(), f.height());
|
||||
let (src_w, src_h) = (f.width(), f.height());
|
||||
// 目标尺寸(None = 原生;0 边回退原生,swscale 不接受 0)。
|
||||
let (w, h) = match target_size {
|
||||
Some((tw, th)) if tw > 0 && th > 0 => (tw, th),
|
||||
_ => (src_w, src_h),
|
||||
};
|
||||
|
||||
// swscale cannot reliably output float RGBA on every build:
|
||||
// float output is missing from some static FFmpeg swscale
|
||||
@@ -945,7 +955,7 @@ impl DecoderState {
|
||||
} else {
|
||||
(Pixel::RGBA, false)
|
||||
};
|
||||
let ctx = get_or_create_scaler(&mut video.scaler, src_format, w, h, out_fmt, w, h)?;
|
||||
let ctx = get_or_create_scaler(&mut video.scaler, src_format, src_w, src_h, out_fmt, w, h)?;
|
||||
let mut out = ffmpeg::frame::Video::empty();
|
||||
ctx.run(&f, &mut out).map_err(ffmpeg_err)?;
|
||||
let stride = out.stride(0);
|
||||
@@ -1389,6 +1399,8 @@ fn get_or_create_scaler(
|
||||
None => true,
|
||||
};
|
||||
if recreate {
|
||||
// BILINEAR:与渲染侧原 Rust 双线性重采样器等效(POINT 会
|
||||
// 明显锯齿);同尺寸纯格式转换时滤波器不参与运算。
|
||||
let ctx = scaling::Context::get(
|
||||
src_format,
|
||||
src_width,
|
||||
@@ -1396,7 +1408,7 @@ fn get_or_create_scaler(
|
||||
dst_format,
|
||||
dst_width,
|
||||
dst_height,
|
||||
scaling::Flags::POINT,
|
||||
scaling::Flags::BILINEAR,
|
||||
)
|
||||
.map_err(ffmpeg_err)?;
|
||||
*cache = Some(ScalingCache {
|
||||
@@ -2448,6 +2460,7 @@ mod tests {
|
||||
image_sequence_number: 0,
|
||||
mode: crate::decoder::RenderMode::Offline,
|
||||
alpha_is_premultiplied: false,
|
||||
target_size: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -242,6 +242,7 @@ mod tests {
|
||||
image_sequence_number: 0,
|
||||
mode: crate::decoder::RenderMode::Offline,
|
||||
alpha_is_premultiplied: false,
|
||||
target_size: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ fn video_params(stream: CodecStream, time: Rational) -> RetrieveVideoParams {
|
||||
image_sequence_number: 0,
|
||||
mode: RenderMode::Offline,
|
||||
alpha_is_premultiplied: false,
|
||||
target_size: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user