use super::FFramesRendererRuntime;
use super::renderer_error::{FFramesRendererError, FFramesRendererResult};
use crate::diagnostics::{self, Diagnostic};
use crate::{
AudioTimelineSamples, Color, FFramesContext, Frame, RenderOptions, ResolvedRenderingTimeline,
TextCache, TimeBase, TimelineIndex, Video, VideoDecodersWorker, VideoSize, usvgr,
};
use serde::Serialize;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
#[derive(Clone)]
pub struct RgbaFrame {
pub width: u32,
pub height: u32,
pub pixels: Vec<u8>,
}
impl std::fmt::Debug for RgbaFrame {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "RgbaFrame({}x{})", self.width, self.height)
}
}
impl RgbaFrame {
pub fn from_premultiplied(width: u32, height: u32, mut pixels: Vec<u8>) -> Self {
for px in pixels.as_chunks_mut::<4>().0 {
let a = px[3];
if a != 255 && a != 0 {
for c in &mut px[..3] {
*c = ((u32::from(*c) * 255 + u32::from(a) / 2) / u32::from(a)).min(255) as u8;
}
}
}
Self {
width,
height,
pixels,
}
}
pub fn into_image(self) -> image::RgbaImage {
image::RgbaImage::from_raw(self.width, self.height, self.pixels)
.expect("RgbaFrame pixel buffer matches its size")
}
pub fn crop(&self, x: u32, y: u32, width: u32, height: u32) -> RgbaFrame {
let x = x.min(self.width.saturating_sub(1));
let y = y.min(self.height.saturating_sub(1));
let width = width.clamp(1, self.width - x);
let height = height.clamp(1, self.height - y);
let mut pixels = Vec::with_capacity((width * height * 4) as usize);
for row in y..y + height {
let start = ((row * self.width + x) * 4) as usize;
pixels.extend_from_slice(&self.pixels[start..start + (width * 4) as usize]);
}
RgbaFrame {
width,
height,
pixels,
}
}
pub fn save_png(&self, path: impl AsRef<Path>) -> FFramesRendererResult<()> {
let path = path.as_ref();
image::save_buffer(
path,
&self.pixels,
self.width,
self.height,
image::ExtendedColorType::Rgba8,
)
.map_err(|err| FFramesRendererError::ImageError((path.display().to_string(), err)))
}
}
pub trait FrameRenderer {
fn fast_shapes(&self) -> bool {
false
}
fn svg_text_cache_capacity(&self) -> Option<usize> {
None
}
fn render_tree(
&mut self,
tree: &usvgr::Tree,
background: Color,
width: u32,
height: u32,
) -> FFramesRendererResult<RgbaFrame>;
}
impl FrameRenderer for std::convert::Infallible {
fn render_tree(
&mut self,
_tree: &usvgr::Tree,
_background: Color,
_width: u32,
_height: u32,
) -> FFramesRendererResult<RgbaFrame> {
match *self {}
}
}
pub fn fit_transform(tree: &usvgr::Tree, width: u32, height: u32) -> usvgr::Transform {
let size = tree.size();
let sx = width as f32 / size.width();
let sy = height as f32 / size.height();
if (sx - 1.).abs() < f32::EPSILON && (sy - 1.).abs() < f32::EPSILON {
usvgr::Transform::default()
} else {
usvgr::Transform::from_scale(sx, sy)
}
}
#[cfg(feature = "cpu_renderer")]
pub struct CpuFrameRenderer {
pixmap: Option<svgr::tiny_skia::Pixmap>,
cache: svgr::SvgrCache,
pixmap_pool: svgr::PixmapPool,
}
#[cfg(feature = "cpu_renderer")]
impl CpuFrameRenderer {
pub fn new(cache_capacity: usize) -> Self {
Self {
pixmap: None,
cache: svgr::SvgrCache::new(cache_capacity),
pixmap_pool: svgr::PixmapPool::new(),
}
}
}
#[cfg(feature = "cpu_renderer")]
impl Default for CpuFrameRenderer {
fn default() -> Self {
Self::new(20)
}
}
#[cfg(feature = "cpu_renderer")]
impl FrameRenderer for CpuFrameRenderer {
fn render_tree(
&mut self,
tree: &usvgr::Tree,
background: Color,
width: u32,
height: u32,
) -> FFramesRendererResult<RgbaFrame> {
use svgr::tiny_skia;
let pixmap = match self.pixmap.take() {
Some(pixmap) if pixmap.width() == width && pixmap.height() == height => pixmap,
_ => tiny_skia::Pixmap::new(width, height).ok_or_else(|| {
FFramesRendererError::Internal(format!(
"can not allocate a {width}x{height} pixmap"
))
})?,
};
let mut pixmap = pixmap;
pixmap.fill(tiny_skia::Color::from_rgba8(
background.r,
background.g,
background.b,
background.a,
));
let ctx = svgr::Context::new_from_pixmap_unsafe(&pixmap);
svgr::render(
tree,
fit_transform(tree, width, height),
&mut pixmap.as_mut(),
&mut self.cache,
&self.pixmap_pool,
&ctx,
);
let frame = RgbaFrame::from_premultiplied(width, height, pixmap.data().to_vec());
self.pixmap = Some(pixmap);
Ok(frame)
}
}
#[derive(Debug, Clone)]
pub struct PreviewRequest {
pub start_frame: usize,
pub autoplay: bool,
pub looping: bool,
pub audio: bool,
pub backend: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct TimelineReport {
pub fps: usize,
pub width: usize,
pub height: usize,
pub duration_frames: usize,
pub duration_seconds: f32,
pub scenes: Vec<SceneReport>,
pub audio: Vec<AudioTrackReport>,
}
#[derive(Debug, Clone, Serialize)]
pub struct SceneReport {
pub index: usize,
pub name: String,
pub full_name: String,
pub start_frame: usize,
pub end_frame: usize,
pub start_seconds: f32,
pub end_seconds: f32,
}
#[derive(Debug, Clone, Serialize)]
pub struct AudioTrackReport {
pub file: String,
pub start_seconds: f64,
pub end_seconds: f64,
pub mix: crate::TrackMix,
}
#[derive(Debug, Clone, Serialize)]
pub struct FrameReport {
pub frame: usize,
pub seconds: f32,
pub scenes: Vec<String>,
pub diagnostics: Vec<ReportedDiagnostic>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ReportedDiagnostic {
pub severity: diagnostics::Severity,
pub key: String,
pub message: String,
#[serde(flatten)]
pub diagnostic: Diagnostic,
}
impl From<Diagnostic> for ReportedDiagnostic {
fn from(diagnostic: Diagnostic) -> Self {
Self {
severity: diagnostic.severity(),
key: diagnostic.group_key(),
message: diagnostic.to_string(),
diagnostic,
}
}
}
pub struct Previewer<'a, 'media, TVideo: Video> {
video: &'a TVideo,
runtime: FFramesRendererRuntime<'a>,
options: RenderOptions<'a, 'media>,
image_source: HashMap<String, Arc<usvgr::PreloadedImageData>>,
size: VideoSize,
timeline_index: TimelineIndex,
worker: Worker,
converter_text_cache_capacity: usize,
svgr_images: crate::SvgrImages<'a>,
}
impl<'a, 'media: 'a, TVideo: Video> Previewer<'a, 'media, TVideo> {
pub fn new(
video: &'a TVideo,
options: &RenderOptions<'a, 'media>,
) -> FFramesRendererResult<Self> {
let scenes = video.define_scenes();
let mut runtime = FFramesRendererRuntime::new(
TimeBase {
fps: TVideo::FPS,
sample_rate: options.audio_encoder_options.sample_rate,
},
video,
&scenes,
options.media,
)?;
if options.load_system_fonts {
runtime.font_source.fontdb.load_system_fonts();
}
let mut image_source = HashMap::new();
if let Some(media) = options.media {
media.populate_image_source(&mut image_source);
}
let timeline_index = TimelineIndex::new(
TVideo::FPS,
runtime.timeline.duration_in_frames,
runtime.timeline.scenes.as_ref(),
);
Ok(Self {
video,
size: VideoSize::new_scaled(TVideo::WIDTH, TVideo::HEIGHT, options.scale_resolution),
options: options.clone(),
runtime,
image_source,
timeline_index,
worker: Worker::new(10),
converter_text_cache_capacity: 10,
svgr_images: crate::SvgrImages::new(default_svgr_rasterizer()),
})
}
pub fn with_svgr_rasterizer(
mut self,
rasterizer: Option<Box<dyn crate::SvgrRasterizer + 'a>>,
) -> Self {
self.svgr_images = crate::SvgrImages::new(rasterizer);
self
}
pub fn timeline(&self) -> &TimelineIndex {
&self.timeline_index
}
pub fn resolved_timeline(&self) -> &ResolvedRenderingTimeline<'a, AudioTimelineSamples> {
&self.runtime.timeline
}
pub fn font_db(&self) -> &usvgr::fontdb::Database {
self.runtime.font_source.as_db_ref()
}
pub fn timeline_report(&self) -> TimelineReport {
let index = &self.timeline_index;
let time_base = self.runtime.time_base;
TimelineReport {
fps: TVideo::FPS,
width: TVideo::WIDTH,
height: TVideo::HEIGHT,
duration_frames: index.duration_in_frames,
duration_seconds: index.duration_in_seconds(),
scenes: index
.scenes
.iter()
.map(|scene| SceneReport {
index: scene.index,
name: scene.name.clone(),
full_name: scene.full_name.clone(),
start_frame: scene.frames.start,
end_frame: scene.frames.end,
start_seconds: index.frame_to_seconds(scene.frames.start),
end_seconds: index.frame_to_seconds(scene.frames.end),
})
.collect(),
audio: self
.runtime
.timeline
.audio_map
.as_ref()
.map(|map| {
use crate::AudioTimelineUnit;
map.tracks()
.iter()
.map(|track| AudioTrackReport {
file: track.file.clone(),
start_seconds: track.range.start.to_seconds(&time_base),
end_seconds: track.range.end.to_seconds(&time_base),
mix: track.mix.clone(),
})
.collect()
})
.unwrap_or_default(),
}
}
pub fn media(&self) -> Option<&'media dyn crate::MediaProvider<'media>> {
self.options.media
}
pub fn options(&self) -> &RenderOptions<'a, 'media> {
&self.options
}
pub fn size(&self) -> (u32, u32) {
(self.size.width as u32, self.size.height as u32)
}
pub fn set_scale(&mut self, scale: f64) {
self.size = VideoSize::new_scaled(TVideo::WIDTH, TVideo::HEIGHT, scale);
}
pub fn svg_tree(&mut self, frame: usize) -> FFramesRendererResult<usvgr::Tree> {
self.convert_frame(frame, false)
}
fn convert_frame(
&mut self,
frame: usize,
fast_shapes: bool,
) -> FFramesRendererResult<usvgr::Tree> {
Conversion::new(
self.video,
&self.runtime,
&self.options,
&self.image_source,
&self.size,
&self.svgr_images,
fast_shapes,
)
.convert(&mut self.worker, frame)
}
fn convert_each<T: Send>(
&mut self,
frames: &[usize],
batch: Batch,
keep: impl Fn(usvgr::Tree) -> T + Sync,
mut each: impl FnMut(
usize,
FFramesRendererResult<T>,
Vec<Diagnostic>,
) -> FFramesRendererResult<()>,
) -> FFramesRendererResult<()> {
let conversion = Conversion::new(
self.video,
&self.runtime,
&self.options,
&self.image_source,
&self.size,
&self.svgr_images,
batch.fast_shapes,
);
let convert = |worker: &mut Worker, frame: usize| {
if !batch.inspect {
return (conversion.convert(worker, frame).map(&keep), Vec::new());
}
let (tree, mut found) = diagnostics::collect(|| conversion.convert(worker, frame));
if let Ok(tree) = &tree {
found.extend(diagnostics::inspect_tree(
tree,
tree.size().width(),
tree.size().height(),
));
}
(tree.map(&keep), found)
};
let threads = batch.threads.min(frames.len());
if threads <= 1 {
for &frame in frames {
let (kept, found) = convert(&mut self.worker, frame);
each(frame, kept, found)?;
}
return Ok(());
}
let (run, window) = match batch.schedule {
Schedule::Ahead => (1, threads * AHEAD_PER_THREAD),
Schedule::Parts => (frames.len().div_ceil(threads), frames.len()),
};
let capacity = self.converter_text_cache_capacity;
in_order(
frames.len(),
threads,
run,
window,
|| Worker::new(capacity),
|worker, index| convert(worker, frames[index]),
|index, (kept, found)| each(frames[index], kept, found),
)
}
pub fn svg(&mut self, frame: usize) -> FFramesRendererResult<String> {
Ok(self
.svg_tree(frame)?
.to_string(&usvgr::WriteOptions::default()))
}
pub fn render(
&mut self,
frame: usize,
renderer: &mut dyn FrameRenderer,
) -> FFramesRendererResult<RgbaFrame> {
self.configure_renderer_cache(renderer);
let tree = self.convert_frame(frame, renderer.fast_shapes())?;
let (width, height) = self.size();
renderer.render_tree(&tree, TVideo::BACKGROUND_COLOR, width, height)
}
fn configure_renderer_cache(&mut self, renderer: &dyn FrameRenderer) {
if let Some(capacity) = renderer.svg_text_cache_capacity()
&& capacity != self.converter_text_cache_capacity
{
self.worker.converter_cache = usvgr::Cache::new_with_text_cache(capacity);
self.converter_text_cache_capacity = capacity;
}
}
pub fn render_inspected(
&mut self,
frame: usize,
renderer: &mut dyn FrameRenderer,
) -> FFramesRendererResult<(RgbaFrame, FrameReport)> {
let mut rendered = None;
self.render_inspected_on(&[frame], renderer, 1, |pixels, report| {
rendered = Some((pixels, report));
Ok(())
})?;
Ok(rendered.expect("the frame is rendered or an error is returned"))
}
pub fn render_each(
&mut self,
frames: &[usize],
renderer: &mut dyn FrameRenderer,
mut each: impl FnMut(usize, RgbaFrame) -> FFramesRendererResult<()>,
) -> FFramesRendererResult<()> {
self.configure_renderer_cache(renderer);
let (width, height) = self.size();
let batch = Batch {
fast_shapes: renderer.fast_shapes(),
inspect: false,
threads: convert_threads(),
schedule: Schedule::Ahead,
};
self.convert_each(
frames,
batch,
|tree| tree,
|frame, tree, _| {
let pixels =
renderer.render_tree(&tree?, TVideo::BACKGROUND_COLOR, width, height)?;
each(frame, pixels)
},
)
}
pub fn render_inspected_each(
&mut self,
frames: &[usize],
renderer: &mut dyn FrameRenderer,
each: impl FnMut(RgbaFrame, FrameReport) -> FFramesRendererResult<()>,
) -> FFramesRendererResult<()> {
self.render_inspected_on(frames, renderer, convert_threads(), each)
}
fn render_inspected_on(
&mut self,
frames: &[usize],
renderer: &mut dyn FrameRenderer,
threads: usize,
mut each: impl FnMut(RgbaFrame, FrameReport) -> FFramesRendererResult<()>,
) -> FFramesRendererResult<()> {
self.configure_renderer_cache(renderer);
let (width, height) = self.size();
let batch = Batch {
fast_shapes: renderer.fast_shapes(),
inspect: true,
threads,
schedule: Schedule::Ahead,
};
let index = self.timeline_index.clone();
self.convert_each(
frames,
batch,
|tree| tree,
|frame, tree, found| {
let pixels =
renderer.render_tree(&tree?, TVideo::BACKGROUND_COLOR, width, height)?;
each(pixels, report(&index, frame, found))
},
)
}
pub fn inspect(&mut self, frame: usize) -> FFramesRendererResult<FrameReport> {
let mut inspected = None;
self.inspect_on(&[frame], 1, |report| {
inspected = Some(report);
Ok(())
})?;
Ok(inspected.expect("the frame is inspected or an error is returned"))
}
pub fn inspect_each(
&mut self,
frames: &[usize],
each: impl FnMut(FrameReport) -> FFramesRendererResult<()>,
) -> FFramesRendererResult<()> {
self.inspect_on(frames, convert_threads(), each)
}
fn inspect_on(
&mut self,
frames: &[usize],
threads: usize,
mut each: impl FnMut(FrameReport) -> FFramesRendererResult<()>,
) -> FFramesRendererResult<()> {
let batch = Batch {
fast_shapes: false,
inspect: true,
threads,
schedule: Schedule::Parts,
};
let index = self.timeline_index.clone();
self.convert_each(frames, batch, drop, |frame, tree, found| {
match tree {
Ok(()) | Err(FFramesRendererError::FramePanicked(_)) => {}
Err(err) => return Err(err),
}
each(report(&index, frame, found))
})
}
}
fn report(index: &TimelineIndex, frame: usize, found: Vec<Diagnostic>) -> FrameReport {
FrameReport {
frame,
seconds: index.frame_to_seconds(frame),
scenes: index.scenes_at(frame).map(|s| s.name.clone()).collect(),
diagnostics: found.into_iter().map(ReportedDiagnostic::from).collect(),
}
}
struct Worker {
text_cache: Option<TextCache>,
decoders: VideoDecodersWorker,
converter_cache: usvgr::Cache,
}
impl Worker {
fn new(converter_text_cache_capacity: usize) -> Self {
Self {
text_cache: TextCache::new(10),
decoders: VideoDecodersWorker::new(2),
converter_cache: usvgr::Cache::new_with_text_cache(converter_text_cache_capacity),
}
}
}
struct Conversion<'p, 'media, TVideo> {
video: &'p TVideo,
ctx: FFramesContext<'p, 'media>,
options: usvgr::Options<'p>,
font_db: &'p usvgr::fontdb::Database,
svgr_images: &'p crate::SvgrImages<'p>,
}
impl<'p, 'media: 'p, TVideo: Video> Conversion<'p, 'media, TVideo> {
fn new(
video: &'p TVideo,
runtime: &'p FFramesRendererRuntime<'_>,
options: &RenderOptions<'_, 'media>,
image_source: &'p HashMap<String, Arc<usvgr::PreloadedImageData>>,
size: &VideoSize,
svgr_images: &'p crate::SvgrImages<'p>,
fast_shapes: bool,
) -> Self {
Self {
video,
ctx: FFramesContext {
time_base: runtime.time_base,
mode: crate::FFramesMode::Renderer,
media_source: options.media,
duration_in_frames: runtime.timeline.duration_in_frames,
scenes: runtime.timeline.scenes.as_ref(),
font_source: Some(&runtime.font_source),
abort_signal: None,
current_video_size: size.clone(),
},
options: usvgr::Options {
image_data: Some(image_source),
font_family: options.default_font.to_string(),
fast_shapes,
..Default::default()
},
font_db: runtime.font_source.as_db_ref(),
svgr_images,
}
}
fn convert(&self, worker: &mut Worker, frame: usize) -> FFramesRendererResult<usvgr::Tree> {
if frame >= self.ctx.duration_in_frames {
return Err(FFramesRendererError::Custom(format!(
"frame {frame} is outside the video (0..{})",
self.ctx.duration_in_frames
)));
}
let svgr = self.svgr_images.enter(&self.options, self.font_db, || {
super::render_frame_guarded(
self.video,
Frame::__internal_make_for_renderer(
frame,
frame,
TVideo::FPS,
worker.text_cache.clone(),
worker.decoders.clone(),
),
&self.ctx,
)
})?;
Ok(svgr.into_svg_tree(&self.options, &mut worker.converter_cache, self.font_db)?)
}
}
#[cfg_attr(feature = "cpu_renderer", allow(clippy::unnecessary_wraps))]
pub(super) fn default_svgr_rasterizer() -> Option<Box<dyn crate::SvgrRasterizer + 'static>> {
#[cfg(feature = "cpu_renderer")]
return Some(Box::new(super::cpu::CpuSvgrRasterizer));
#[cfg(not(feature = "cpu_renderer"))]
None
}
#[derive(Clone, Copy)]
struct Batch {
fast_shapes: bool,
inspect: bool,
threads: usize,
schedule: Schedule,
}
#[derive(Clone, Copy)]
enum Schedule {
Ahead,
Parts,
}
const AHEAD_PER_THREAD: usize = 2;
fn convert_threads() -> usize {
(super::get_thread_count() / 4).clamp(1, 4)
}
fn in_order<W, R: Send, E>(
count: usize,
threads: usize,
run: usize,
window: usize,
worker: impl Fn() -> W + Sync,
work: impl Fn(&mut W, usize) -> R + Sync,
mut done: impl FnMut(usize, R) -> Result<(), E>,
) -> Result<(), E> {
use std::sync::{Condvar, Mutex, MutexGuard, PoisonError, mpsc};
struct Progress {
next: usize,
done: usize,
stop: bool,
}
struct Shared {
progress: Mutex<Progress>,
moved: Condvar,
}
impl Shared {
fn lock(&self) -> MutexGuard<'_, Progress> {
self.progress.lock().unwrap_or_else(PoisonError::into_inner)
}
fn stop(&self) {
self.lock().stop = true;
self.moved.notify_all();
}
}
struct StopOnPanic<'s>(&'s Shared);
impl Drop for StopOnPanic<'_> {
fn drop(&mut self) {
if std::thread::panicking() {
self.0.stop();
}
}
}
let (run, window) = (run.max(1), window.max(1));
let shared = Shared {
progress: Mutex::new(Progress {
next: 0,
done: 0,
stop: false,
}),
moved: Condvar::new(),
};
let (sender, receiver) = mpsc::channel();
std::thread::scope(|scope| {
let _stop = StopOnPanic(&shared);
for _ in 0..threads.max(1) {
let sender = sender.clone();
let (shared, worker, work) = (&shared, &worker, &work);
scope.spawn(move || {
let _stop = StopOnPanic(shared);
let mut state = worker();
'runs: loop {
let items = {
let mut progress = shared.lock();
while !progress.stop
&& progress.next < count
&& progress.next >= progress.done + window
{
progress = shared
.moved
.wait(progress)
.unwrap_or_else(PoisonError::into_inner);
}
if progress.stop || progress.next >= count {
break;
}
let start = progress.next;
progress.next = (start + run).min(count);
start..progress.next
};
for index in items {
if shared.lock().stop
|| sender.send((index, work(&mut state, index))).is_err()
{
break 'runs;
}
}
}
});
}
drop(sender);
let mut waiting: HashMap<usize, R> = HashMap::new();
let mut next = 0;
for (index, result) in &receiver {
waiting.insert(index, result);
while let Some(result) = waiting.remove(&next) {
if let Err(err) = done(next, result) {
shared.stop();
return Err(err);
}
next += 1;
shared.lock().done = next;
shared.moved.notify_all();
}
}
Ok(())
})
}
#[cfg(test)]
mod batch_tests {
use super::*;
use crate::{AudioMap, Duration, Svgr};
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
struct Changing {
panic_at: Option<usize>,
}
impl Video for Changing {
const FPS: usize = 10;
const WIDTH: usize = 64;
const HEIGHT: usize = 32;
fn duration(&self) -> Duration<'_> {
Duration::Frames(24)
}
fn audio(&self) -> AudioMap<'_> {
AudioMap::none()
}
fn render_frame<'a>(&'a self, frame: Frame, ctx: &FFramesContext<'a, '_>) -> Svgr<'a> {
assert!(Some(frame.index) != self.panic_at, "boom");
if frame.index >= 20 {
let _ = ctx.get_image("missing.png");
}
let width = frame.index * 2 + 1;
crate::svgr!(
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="32">
<rect x="0" y="4" width={width} height="20" fill="#ff8800" />
<text x="2" y="30" font-family="No Such Font" font-size="8">"fframes"</text>
</svg>
)
}
}
const FRAMES: [usize; 9] = [23, 5, 0, 12, 12, 19, 20, 3, 8];
type Rendered = (Vec<(RgbaFrame, (String, bool))>, Option<String>);
fn without_fonts(mut report: FrameReport) -> (String, bool) {
let all = report.diagnostics.len();
report
.diagnostics
.retain(|d| !matches!(d.diagnostic, Diagnostic::MissingFont { .. }));
let font = report.diagnostics.len() != all;
(format!("{report:?}"), font)
}
fn assert_fonts(expected: &[bool], actual: &[bool]) {
assert_eq!(actual.len(), expected.len());
if cfg!(feature = "compile-time-svgtree") {
assert!(expected[0] && actual[0]);
assert!(
expected
.iter()
.zip(actual)
.all(|(&one, &threads)| !one || threads)
);
} else {
assert_eq!(actual, expected);
}
}
fn one_by_one(video: &Changing, frames: &[usize]) -> Rendered {
let mut previewer = Previewer::new(video, &RenderOptions::default()).unwrap();
let mut renderer = CpuFrameRenderer::default();
let mut rendered = Vec::new();
for &frame in frames {
match previewer.render_inspected(frame, &mut renderer) {
Ok((pixels, report)) => rendered.push((pixels, without_fonts(report))),
Err(err) => return (rendered, Some(err.to_string())),
}
}
(rendered, None)
}
fn batch(video: &Changing, frames: &[usize]) -> Rendered {
let mut previewer = Previewer::new(video, &RenderOptions::default()).unwrap();
let mut rendered = Vec::new();
let result = previewer.render_inspected_on(
frames,
&mut CpuFrameRenderer::default(),
4,
|pixels, report| {
rendered.push((pixels, without_fonts(report)));
Ok(())
},
);
(rendered, result.err().map(|err| err.to_string()))
}
fn assert_same((expected, expected_err): Rendered, (actual, actual_err): Rendered) {
assert_eq!(actual.len(), expected.len());
for ((pixels, (report, _)), (expected_pixels, (expected_report, _))) in
actual.iter().zip(&expected)
{
assert_eq!(report, expected_report);
assert_eq!(pixels.pixels, expected_pixels.pixels, "{report}");
}
let fonts = |rendered: &[(RgbaFrame, (String, bool))]| {
rendered
.iter()
.map(|(_, (_, font))| *font)
.collect::<Vec<_>>()
};
assert_fonts(&fonts(&expected), &fonts(&actual));
assert_eq!(actual_err, expected_err);
}
#[test]
fn several_threads_render_and_report_like_one_by_one() {
let video = Changing { panic_at: None };
let expected = one_by_one(&video, &FRAMES);
assert_eq!(expected.0.len(), FRAMES.len());
assert!(expected.0[0].1.0.contains("MissingMedia"));
assert!(expected.0[0].1.1);
assert_same(expected, batch(&video, &FRAMES));
let mut previewer = Previewer::new(&video, &RenderOptions::default()).unwrap();
let mut renderer = CpuFrameRenderer::default();
let mut rendered = Vec::new();
previewer
.render_each(&FRAMES, &mut renderer, |frame, pixels| {
rendered.push((frame, pixels.pixels));
Ok(())
})
.unwrap();
for (&frame, (rendered_frame, pixels)) in FRAMES.iter().zip(rendered) {
assert_eq!(rendered_frame, frame);
assert_eq!(
pixels,
previewer.render(frame, &mut renderer).unwrap().pixels
);
}
}
#[test]
fn several_threads_stop_at_the_first_error_like_one_by_one() {
let video = Changing { panic_at: Some(12) };
let expected = one_by_one(&video, &FRAMES);
assert_eq!(expected.0.len(), 3);
assert!(expected.1.as_ref().unwrap().contains("boom"));
assert_same(expected, batch(&video, &FRAMES));
let outside = [1, 2, 99, 3];
let expected = one_by_one(&video, &outside);
assert_eq!(expected.0.len(), 2);
assert_same(expected, batch(&video, &outside));
}
#[test]
fn several_threads_inspect_like_one_by_one() {
let video = Changing { panic_at: Some(12) };
let mut previewer = Previewer::new(&video, &RenderOptions::default()).unwrap();
let (expected, expected_fonts): (Vec<String>, Vec<bool>) = FRAMES
.iter()
.map(|&frame| without_fonts(previewer.inspect(frame).unwrap()))
.unzip();
assert!(expected[3].contains("Panic"));
let mut previewer = Previewer::new(&video, &RenderOptions::default()).unwrap();
let mut reports = Vec::new();
previewer
.inspect_on(&FRAMES, 4, |report| {
reports.push(without_fonts(report));
Ok(())
})
.unwrap();
let (reports, fonts): (Vec<String>, Vec<bool>) = reports.into_iter().unzip();
assert_eq!(reports, expected);
assert_fonts(&expected_fonts, &fonts);
}
#[test]
fn results_come_in_order_and_at_most_a_window_ahead() {
let done = AtomicUsize::new(0);
let ahead = AtomicUsize::new(0);
let mut next = 0;
in_order(
300,
6,
1,
8,
|| (),
|(), index| {
ahead.fetch_max(index + 1 - done.load(Ordering::SeqCst), Ordering::SeqCst);
std::thread::sleep(std::time::Duration::from_micros((index % 7) as u64 * 30));
index * 2
},
|index, result| {
assert_eq!((index, result), (next, next * 2));
next += 1;
std::thread::sleep(std::time::Duration::from_micros(100));
done.store(next, Ordering::SeqCst);
Ok::<_, ()>(())
},
)
.unwrap();
assert_eq!(next, 300);
let ahead = ahead.load(Ordering::SeqCst);
assert!((2..=8).contains(&ahead), "{ahead} items ahead");
}
#[test]
fn runs_of_items_come_in_order() {
let threads = Mutex::new(vec![None; 100]);
let mut next = 0;
in_order(
100,
4,
25,
100,
|| (),
|(), index| {
threads.lock().unwrap()[index] = Some(std::thread::current().id());
index
},
|index, result| {
assert_eq!((index, result), (next, next));
next += 1;
Ok::<_, ()>(())
},
)
.unwrap();
assert_eq!(next, 100);
let threads = threads.into_inner().unwrap();
for part in threads.chunks(25) {
assert!(part.iter().all(|thread| *thread == part[0]));
}
}
#[test]
fn an_error_stops_the_threads() {
let started = Mutex::new(Vec::new());
let mut delivered = Vec::new();
let result = in_order(
100,
4,
1,
8,
|| (),
|(), index| started.lock().unwrap().push(index),
|index, ()| {
if index == 10 {
return Err(index);
}
delivered.push(index);
Ok(())
},
);
assert_eq!(result, Err(10));
assert_eq!(delivered, (0..10).collect::<Vec<_>>());
let started = started.into_inner().unwrap();
assert!(started.iter().all(|&index| index < 11 + 8), "{started:?}");
}
#[test]
#[should_panic(expected = "a scoped thread panicked")]
fn a_panic_of_a_thread_does_not_hang() {
let _ = in_order(
100,
4,
1,
2,
|| (),
|(), index| assert!(index != 7, "boom"),
|_, ()| Ok::<_, ()>(()),
);
}
#[test]
#[should_panic(expected = "done panics")]
fn a_panic_of_done_does_not_hang() {
let _ = in_order(
100,
4,
1,
2,
|| (),
|(), _| std::thread::sleep(std::time::Duration::from_millis(1)),
|index, ()| {
assert!(index != 3, "done panics");
Ok::<_, ()>(())
},
);
}
}
#[cfg(test)]
mod cache_tests {
use super::*;
use crate::{AudioMap, Duration, Svgr};
struct EmptyVideo;
impl Video for EmptyVideo {
const FPS: usize = 30;
const WIDTH: usize = 1;
const HEIGHT: usize = 1;
fn duration(&self) -> Duration<'_> {
Duration::Frames(2)
}
fn audio(&self) -> AudioMap<'_> {
AudioMap::none()
}
fn render_frame<'a>(&'a self, _: Frame, _: &FFramesContext<'a, '_>) -> Svgr<'a> {
Svgr::empty()
}
}
struct Renderer(Option<usize>);
impl FrameRenderer for Renderer {
fn svg_text_cache_capacity(&self) -> Option<usize> {
self.0
}
fn render_tree(
&mut self,
_: &usvgr::Tree,
_: Color,
width: u32,
height: u32,
) -> FFramesRendererResult<RgbaFrame> {
Ok(RgbaFrame {
width,
height,
pixels: Vec::new(),
})
}
}
#[test]
fn renderer_text_capacity_is_applied_without_recreating_warm_cache() {
let mut preview = Previewer::new(&EmptyVideo, &RenderOptions::default()).unwrap();
preview.configure_renderer_cache(&Renderer(Some(100_000)));
assert_eq!(preview.converter_text_cache_capacity, 100_000);
preview.worker.converter_cache.paint.insert(
"warm".into(),
usvgr::Paint::Color(usvgr::Color::new_rgb(1, 2, 3)),
);
preview.configure_renderer_cache(&Renderer(Some(100_000)));
assert!(preview.worker.converter_cache.paint.contains_key("warm"));
preview.configure_renderer_cache(&Renderer(None));
assert_eq!(preview.converter_text_cache_capacity, 100_000);
preview.configure_renderer_cache(&Renderer(Some(0)));
assert_eq!(preview.converter_text_cache_capacity, 0);
assert!(preview.worker.converter_cache.usvgr_text_cache.is_none());
}
}