1use std::collections::VecDeque;
3
4use crate::cmd::render::file_writer::OutputFormatExt;
5use crate::{Output, Scene, SceneConfig, SceneConstructor};
6use file_writer::{FileWriter, FileWriterBuilder};
7use indicatif::{ProgressState, ProgressStyle};
8use ranim_core::color::{self, LinearSrgb};
9use ranim_core::{SceneEvaluator, TimeMark};
10use ranim_render::resource::RenderTextures;
11use ranim_render::{Renderer, utils::WgpuContext, world::RenderFrame};
12use std::path::{Path, PathBuf};
13use std::time::Duration;
14use std::time::Instant;
15use tracing::{Span, info, instrument, trace};
16use tracing_indicatif::span_ext::IndicatifSpanExt;
17
18pub(crate) mod audio;
19pub(crate) mod file_writer;
20
21fn render_output_basename(template: &str, name: &str, width: u32, height: u32, fps: u32) -> String {
29 template
30 .replace("{name}", name)
31 .replace("{width}", &width.to_string())
32 .replace("{height}", &height.to_string())
33 .replace("{fps}", &fps.to_string())
34}
35
36pub fn render_scene(scene: &Scene, buffer_count: usize) {
38 for (i, output) in scene.outputs.iter().enumerate() {
39 info!(
40 "Rendering output {}/{} ({})",
41 i + 1,
42 scene.outputs.len(),
43 output.format
44 );
45 render_scene_output(
46 scene.constructor,
47 scene.name.to_string(),
48 &scene.config,
49 output,
50 buffer_count,
51 );
52 }
53}
54
55pub fn render_scene_output(
57 constructor: impl SceneConstructor,
58 name: String,
59 scene_config: &SceneConfig,
60 output: &Output,
61 buffer_count: usize,
62) {
63 render_scene_output_with_progress(constructor, name, scene_config, output, buffer_count, None);
64}
65
66pub struct RenderJob {
72 pub output: Output,
74 pub capture_marks: bool,
79}
80
81impl RenderJob {
82 pub fn full(output: Output, capture_marks: bool) -> Self {
84 Self {
85 output,
86 capture_marks,
87 }
88 }
89}
90
91pub fn render_scene_output_with_progress(
95 constructor: impl SceneConstructor,
96 name: String,
97 scene_config: &SceneConfig,
98 output: &Output,
99 buffer_count: usize,
100 on_progress: Option<Box<dyn Fn(u64, u64) + Send>>,
101) {
102 render_scene_job(
103 constructor,
104 name,
105 scene_config,
106 RenderJob::full(output.clone(), true),
107 buffer_count,
108 on_progress,
109 );
110}
111
112pub fn render_scene_once(
118 constructor: impl SceneConstructor,
119 name: String,
120 scene_config: &SceneConfig,
121 output: &Output,
122 buffer_count: usize,
123) {
124 render_scene_job(
125 constructor,
126 name,
127 scene_config,
128 RenderJob::full(output.clone(), false),
129 buffer_count,
130 None,
131 );
132}
133
134pub fn render_scene_job(
136 constructor: impl SceneConstructor,
137 name: String,
138 scene_config: &SceneConfig,
139 job: RenderJob,
140 buffer_count: usize,
141 on_progress: Option<Box<dyn Fn(u64, u64) + Send>>,
142) {
143 use std::time::Instant;
144
145 let RenderJob {
146 output,
147 capture_marks,
148 } = job;
149
150 info!(
151 "Output: {}x{} {}fps {} dir={:?} save_frames={}",
152 output.width, output.height, output.fps, output.format, output.dir, output.save_frames
153 );
154
155 let t = Instant::now();
156 let scene = constructor.build_scene();
157 trace!("Build timeline cost: {:?}", t.elapsed());
158
159 let mut evaluator = scene.into_evaluator(DEFAULT_LOGIC_FPS);
162
163 let mut app = RanimRenderApp::new(name, scene_config, &output, buffer_count);
164 app.render_scene_with_progress(&mut evaluator, on_progress);
165
166 if capture_marks && !evaluator.time_marks().is_empty() {
167 app.render_capture_marks(&mut evaluator);
168 }
169
170 if evaluator.has_audio() {
173 match audio::audio_codec(output.format) {
174 Some(_) => {
175 let pcm = evaluator.audio();
176 app.finish_video();
178 let video_path = app.video_path();
179 if !video_path.exists() {
180 tracing::warn!("no video file was written, skipping audio muxing");
181 } else if let Err(err) = audio::mux_audio_into_video(video_path, pcm, output.format)
182 {
183 tracing::error!("failed to mux audio into {}: {err:#}", video_path.display());
184 }
185 }
186 None => tracing::warn!(
187 "the scene has audio, but the {:?} format cannot store it; rendering silently",
188 output.format
189 ),
190 }
191 }
192}
193
194const DEFAULT_LOGIC_FPS: f64 = 120.0;
196
197pub struct RenderThreadHandle {
201 submit_frame_tx: async_channel::Sender<RenderFrame>,
202 back_rx: async_channel::Receiver<RenderFrame>,
203 worker_rx: async_channel::Receiver<RenderWorker>,
204}
205
206impl RenderThreadHandle {
207 pub fn sync_and_submit(&self, f: impl FnOnce(&mut RenderFrame)) {
209 let mut store = self.get_store();
210 f(&mut store);
211 self.submit_frame_tx.send_blocking(store).unwrap();
212 }
213
214 pub fn get_store(&self) -> RenderFrame {
216 self.back_rx.recv_blocking().unwrap()
217 }
218
219 pub fn retrive(&self) -> RenderWorker {
221 self.submit_frame_tx.close(); self.worker_rx.recv_blocking().unwrap()
223 }
224}
225
226pub struct RenderWorker {
228 ctx: WgpuContext,
229 renderer: Renderer,
230 render_textures: Vec<RenderTextures>,
231 clear_color: wgpu::Color,
232 video_writer: Option<FileWriter>,
234 video_writer_builder: Option<FileWriterBuilder>,
235 video_path: PathBuf,
236 save_frames: bool,
237 output_dir: PathBuf,
238 scene_name: String,
239 width: u32,
240 height: u32,
241 fps: u32,
242}
243
244impl RenderWorker {
245 pub fn new(
253 scene_name: String,
254 scene_config: &SceneConfig,
255 output: &Output,
256 buffer_count: usize,
257 ) -> Self {
258 assert!(buffer_count >= 1, "buffer_count must be at least 1");
259 info!("Checking ffmpeg...");
260 let t = Instant::now();
261 if let Ok(ffmpeg_path) = which::which("ffmpeg") {
262 info!("ffmpeg found at {ffmpeg_path:?}");
263 } else {
264 use std::path::Path;
265
266 info!(
267 "ffmpeg not found from path env, searching in {:?}...",
268 Path::new("./").canonicalize().unwrap()
269 );
270 if Path::new("./ffmpeg").exists() {
271 info!("ffmpeg found at current working directory")
272 } else {
273 info!("ffmpeg not found at current working directory, downloading...");
274 download_ffmpeg("./").expect("failed to download ffmpeg");
275 }
276 }
277 trace!("Check ffmmpeg cost: {:?}", t.elapsed());
278
279 let t = Instant::now();
280 info!("Creating wgpu context...");
281 let ctx = pollster::block_on(WgpuContext::new());
282 trace!("Create wgpu context cost: {:?}", t.elapsed());
283
284 let mut output_dir = PathBuf::from(&output.dir);
285 if !output_dir.is_absolute() {
286 output_dir = std::env::current_dir().unwrap().join(output_dir);
287 }
288 let renderer = Renderer::new(&ctx, output.width, output.height, 8);
289 let render_textures: Vec<RenderTextures> = (0..buffer_count)
290 .map(|_| renderer.new_render_textures(&ctx))
291 .collect();
292 let clear_color = color::try_color(&scene_config.clear_color)
293 .unwrap_or(color::color("#333333ff"))
294 .convert::<LinearSrgb>();
295 let [r, g, b, a] = clear_color.components.map(|x| x as f64);
296 let clear_color = wgpu::Color { r, g, b, a };
297 let (_, _, ext) = output.format.encoding_params();
298 let video_path = output_dir.join({
299 let template = output
300 .name_template
301 .as_deref()
302 .unwrap_or("{name}_{width}x{height}_{fps}");
303 let base_name = render_output_basename(
304 template,
305 output.name.as_deref().unwrap_or(&scene_name),
306 output.width,
307 output.height,
308 output.fps,
309 );
310 format!("{base_name}.{ext}")
311 });
312 Self {
313 ctx,
314 renderer,
315 render_textures,
316 clear_color,
317 video_writer: None,
318 video_writer_builder: Some(
319 FileWriterBuilder::default()
320 .with_fps(output.fps)
321 .with_size(output.width, output.height)
322 .with_file_path(video_path.clone())
323 .with_output_format(output.format),
324 ),
325 video_path,
326 save_frames: output.save_frames,
327 output_dir,
328 scene_name,
329 width: output.width,
330 height: output.height,
331 fps: output.fps,
332 }
333 }
334
335 pub fn save_frame_dir(&self) -> PathBuf {
337 self.output_dir.join(format!(
338 "{}_{}x{}_{}-frames",
339 self.scene_name, self.width, self.height, self.fps
340 ))
341 }
342
343 pub fn video_path(&self) -> &Path {
345 &self.video_path
346 }
347
348 pub fn finish_video(&mut self) {
353 self.video_writer = None;
354 }
355
356 pub fn yeet(self) -> RenderThreadHandle {
360 let (submit_frame_tx, submit_frame_rx) = async_channel::bounded(1);
361 let (back_tx, back_rx) = async_channel::bounded(1);
362 let (worker_tx, worker_rx) = async_channel::bounded(1);
363
364 back_tx.send_blocking(RenderFrame::default()).unwrap();
365 std::thread::spawn(move || {
366 let mut worker = self;
367 let n = worker.render_textures.len();
368 let mut frame_count = 0u64;
369 let mut cur = 0usize;
370 let mut pending: VecDeque<(usize, u64)> = VecDeque::new();
371
372 while let Ok(store) = submit_frame_rx.recv_blocking() {
373 if pending.len() >= n {
375 let (prev, prev_fc) = pending.pop_front().unwrap();
376 worker.render_textures[prev].finish_readback(&worker.ctx);
377 worker.output_frame_from(prev, prev_fc);
378 }
379
380 worker.renderer.render_frame(
382 &mut worker.render_textures[cur],
383 worker.clear_color,
384 &store,
385 );
386 worker.render_textures[cur].start_readback(&worker.ctx);
387
388 pending.push_back((cur, frame_count));
389 frame_count += 1;
390 cur = (cur + 1) % n;
391
392 back_tx.send_blocking(store).unwrap();
395
396 while let Some(&(prev, _)) = pending.front() {
399 if !worker.render_textures[prev].try_finish_readback(&worker.ctx) {
401 break;
402 }
403 let (prev, prev_fc) = pending.pop_front().unwrap();
404 worker.output_frame_from(prev, prev_fc);
405 }
406 }
407
408 while let Some((prev, prev_fc)) = pending.pop_front() {
410 worker.render_textures[prev].finish_readback(&worker.ctx);
411 worker.output_frame_from(prev, prev_fc);
412 }
413
414 worker_tx.send_blocking(worker).unwrap();
415 });
416 RenderThreadHandle {
417 submit_frame_tx,
418 back_rx,
419 worker_rx,
420 }
421 }
422
423 pub fn render_store(&mut self, store: &RenderFrame) {
425 self.renderer
426 .render_frame(&mut self.render_textures[0], self.clear_color, store);
427 }
428
429 pub fn output_frame_from(&mut self, target_idx: usize, frame_number: u64) {
431 self.write_frame_from(target_idx);
432 if self.save_frames {
433 self.save_frame_from(target_idx, frame_number);
434 }
435 }
436
437 pub fn write_frame_from(&mut self, target_idx: usize) {
439 let data = self.render_textures[target_idx]
440 .render_texture
441 .texture_data();
442 if let Some(video_writer) = self.video_writer.as_mut() {
443 video_writer.write_frame(data);
444 } else if let Some(builder) = self.video_writer_builder.as_ref() {
445 self.video_writer
446 .get_or_insert(builder.clone().build())
447 .write_frame(data);
448 }
449 }
450
451 pub fn save_frame_from(&mut self, target_idx: usize, frame_number: u64) {
453 let path = self.save_frame_dir().join(format!("{frame_number:04}.png"));
454 let dir = path.parent().unwrap();
455 if !dir.exists() || !dir.is_dir() {
456 std::fs::create_dir_all(dir).unwrap();
457 }
458 let buffer = self.render_textures[target_idx].get_rendered_texture_img_buffer(&self.ctx);
460 buffer.save(path).unwrap();
461 }
462
463 pub fn capture_frame(&mut self, path: impl AsRef<Path>) {
465 let path = path.as_ref();
466 let path = if !path.is_absolute() {
467 self.output_dir
468 .join(format!(
469 "{}_{}x{}_{}",
470 self.scene_name, self.width, self.height, self.fps
471 ))
472 .join(path)
473 } else {
474 path.to_path_buf()
475 };
476 let dir = path.parent().unwrap();
477 if !dir.exists() || !dir.is_dir() {
478 std::fs::create_dir_all(dir).unwrap();
479 }
480 let buffer = self.render_textures[0].get_rendered_texture_img_buffer(&self.ctx);
481 buffer.save(path).unwrap();
482 }
483}
484
485pub struct RanimRenderApp {
488 render_worker: Option<RenderWorker>,
489 fps: u32,
490 store: RenderFrame,
491}
492
493impl RanimRenderApp {
494 pub fn new(
496 scene_name: String,
497 scene_config: &SceneConfig,
498 output: &Output,
499 buffer_count: usize,
500 ) -> Self {
501 let render_worker = RenderWorker::new(scene_name, scene_config, output, buffer_count);
502 Self {
503 render_worker: Some(render_worker),
504 fps: output.fps,
505 store: RenderFrame::default(),
506 }
507 }
508
509 pub fn video_path(&self) -> &Path {
511 self.render_worker
512 .as_ref()
513 .expect("render worker must exist outside rendering")
514 .video_path()
515 }
516
517 pub fn finish_video(&mut self) {
519 if let Some(worker) = self.render_worker.as_mut() {
520 worker.finish_video();
521 }
522 }
523
524 #[instrument(skip_all)]
528 pub fn render_scene_with_progress(
529 &mut self,
530 evaluator: &mut SceneEvaluator,
531 on_progress: Option<Box<dyn Fn(u64, u64) + Send>>,
532 ) {
533 let start = Instant::now();
534 let worker_thread = self.render_worker.take().unwrap().yeet();
535
536 let total_secs = evaluator.total_secs();
537 let fps = self.fps as f64;
538 let raw_frames = total_secs * fps;
539 let n = raw_frames.ceil() as u64;
542 let num_frames = if (raw_frames - raw_frames.round()).abs() < 1e-9 {
543 n
544 } else {
545 n + 1
546 };
547 let style = ProgressStyle::with_template(
548 "[{elapsed_precise}] [{wide_bar:.cyan/blue}] frame {human_pos}/{human_len} (eta {eta}) {msg}",
549 )
550 .unwrap()
551 .with_key("eta", |state: &ProgressState, w: &mut dyn std::fmt::Write| {
552 write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()
553 })
554 .progress_chars("#>-");
555
556 let span = Span::current();
557 span.pb_set_style(&style);
558 span.pb_set_length(num_frames);
559
560 (0..num_frames)
561 .map(|f| (f as f64 / fps).min(total_secs))
562 .enumerate()
563 .for_each(|(i, sec)| {
564 let mut frame_items = Vec::new();
565 evaluator.sample_at(sec, &mut frame_items);
566 worker_thread.sync_and_submit(move |store| {
567 store.update(frame_items.into_iter());
568 });
569
570 span.pb_inc(1);
571 if let Some(cb) = &on_progress {
572 cb(i as u64 + 1, num_frames);
573 }
574 span.pb_set_message(
575 format!(
576 "rendering {:.1?}/{:.1?}",
577 Duration::from_secs_f64(sec),
578 Duration::from_secs_f64(total_secs)
579 )
580 .as_str(),
581 );
582 });
583 self.render_worker.replace(worker_thread.retrive());
584
585 info!(
586 "rendered {} frames({:?}) in {:?}",
587 num_frames,
588 Duration::from_secs_f64(evaluator.total_secs()),
589 start.elapsed(),
590 );
591 trace!("render timeline cost: {:?}", start.elapsed());
592 }
593
594 #[instrument(skip_all)]
596 pub fn render_capture_marks(&mut self, evaluator: &mut SceneEvaluator) {
597 let start = Instant::now();
598 let timemarks = evaluator
599 .time_marks()
600 .iter()
601 .filter(|mark| matches!(mark.1, TimeMark::Capture(_)))
602 .map(|(sec, mark)| (*sec, mark.clone()))
603 .collect::<Vec<_>>();
604
605 let style = ProgressStyle::with_template(
606 "[{elapsed_precise}] [{wide_bar:.cyan/blue}] frame {human_pos}/{human_len} (eta {eta}) {msg}",
607 )
608 .unwrap()
609 .with_key("eta", |state: &ProgressState, w: &mut dyn std::fmt::Write| {
610 write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()
611 })
612 .progress_chars("#>-");
613
614 let span = Span::current();
615 span.pb_set_style(&style);
616 span.pb_set_length(timemarks.len() as u64);
617 let _enter = span.enter();
618
619 let mut captured = 0usize;
620 for (sec, TimeMark::Capture(filename)) in timemarks {
621 let mut frame_items = Vec::new();
624 evaluator.sample_at(sec, &mut frame_items);
625 self.store.update(frame_items.into_iter());
626 let worker = self.render_worker.as_mut().unwrap();
627 worker.render_store(&self.store);
628 worker.capture_frame(filename);
629 span.pb_inc(1);
630 captured += 1;
631 }
632 info!("saved {} capture frames from time marks", captured);
633
634 trace!("save capture frames cost: {:?}", start.elapsed());
635 }
636}
637
638const FFMPEG_RELEASE_URL: &str = "https://github.com/eugeneware/ffmpeg-static/releases/latest";
640
641#[allow(unused)]
643pub(crate) fn exe_dir() -> PathBuf {
644 std::env::current_exe()
645 .unwrap()
646 .parent()
647 .unwrap()
648 .to_path_buf()
649}
650
651pub fn download_ffmpeg(target_dir: impl AsRef<Path>) -> Result<PathBuf, anyhow::Error> {
653 use anyhow::Context;
654 use itertools::Itertools;
655 use std::io::Read;
656 use tracing::info;
657
658 let target_dir = target_dir.as_ref();
659
660 let res = reqwest::blocking::get(FFMPEG_RELEASE_URL).context("failed to get release url")?;
661 let url = res.url().to_string();
662 let url = url.split("tag").collect_array::<2>().unwrap();
663 let url = format!("{}/download/{}", url[0], url[1]);
664 info!("ffmpeg release url: {url:?}");
665
666 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
667 let url = format!("{url}/ffmpeg-win32-x64.gz");
668 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
669 let url = format!("{url}/ffmpeg-linux-x64.gz");
670 #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
671 let url = format!("{url}/ffmpeg-linux-arm64.gz");
672 #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
673 let url = format!("{url}/ffmpeg-darwin-x64.gz");
674 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
675 let url = format!("{url}/ffmpeg-darwin-arm64.gz");
676
677 info!("downloading ffmpeg from {url:?}...");
678
679 let res = reqwest::blocking::get(&url).context("get err")?;
680 let mut decoder = flate2::bufread::GzDecoder::new(std::io::BufReader::new(
681 std::io::Cursor::new(res.bytes().unwrap()),
682 ));
683 let mut bytes = Vec::new();
684 decoder
685 .read_to_end(&mut bytes)
686 .context("GzDecoder decode err")?;
687 let ffmpeg_path = target_dir.join("ffmpeg");
688 std::fs::write(&ffmpeg_path, bytes).unwrap();
689
690 #[cfg(target_family = "unix")]
691 {
692 use std::os::unix::fs::PermissionsExt;
693
694 std::fs::set_permissions(&ffmpeg_path, std::fs::Permissions::from_mode(0o755))?;
695 }
696 info!("ffmpeg downloaded to {target_dir:?}");
697 Ok(ffmpeg_path)
698}
699
700#[cfg(test)]
701mod tests {
702 use super::render_output_basename;
703
704 #[test]
705 fn render_output_basename_substitutes_template_placeholders() {
706 let cases = [
707 ("{name}_{width}x{height}_{fps}", "my_scene_1920x1080_60"),
708 (
709 "{name}_{fps}fps_{width}x{height}",
710 "my_scene_60fps_1920x1080",
711 ),
712 ("{name}", "my_scene"),
713 ];
714 for (template, expected) in cases {
715 assert_eq!(
716 render_output_basename(template, "my_scene", 1920, 1080, 60),
717 expected,
718 "template {template}"
719 );
720 }
721 }
722}