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 file_writer;
19
20#[cfg(feature = "profiling")]
21use ranim_render::PUFFIN_GPU_PROFILER;
22
23fn render_output_basename(template: &str, name: &str, width: u32, height: u32, fps: u32) -> String {
31 template
32 .replace("{name}", name)
33 .replace("{width}", &width.to_string())
34 .replace("{height}", &height.to_string())
35 .replace("{fps}", &fps.to_string())
36}
37
38pub fn render_scene(scene: &Scene, buffer_count: usize) {
40 for (i, output) in scene.outputs.iter().enumerate() {
41 info!(
42 "Rendering output {}/{} ({})",
43 i + 1,
44 scene.outputs.len(),
45 output.format
46 );
47 render_scene_output(
48 scene.constructor,
49 scene.name.to_string(),
50 &scene.config,
51 output,
52 buffer_count,
53 );
54 }
55}
56
57pub fn render_scene_output(
59 constructor: impl SceneConstructor,
60 name: String,
61 scene_config: &SceneConfig,
62 output: &Output,
63 buffer_count: usize,
64) {
65 render_scene_output_with_progress(constructor, name, scene_config, output, buffer_count, None);
66}
67
68pub fn render_scene_output_with_progress(
72 constructor: impl SceneConstructor,
73 name: String,
74 scene_config: &SceneConfig,
75 output: &Output,
76 buffer_count: usize,
77 on_progress: Option<Box<dyn Fn(u64, u64) + Send>>,
78) {
79 use std::time::Instant;
80
81 info!(
82 "Output: {}x{} {}fps {} dir={:?} save_frames={}",
83 output.width, output.height, output.fps, output.format, output.dir, output.save_frames
84 );
85
86 let t = Instant::now();
87 let scene = constructor.build_scene();
88 trace!("Build timeline cost: {:?}", t.elapsed());
89
90 let mut evaluator = scene.into_evaluator(DEFAULT_LOGIC_FPS);
93
94 let mut app = RanimRenderApp::new(name, scene_config, output, buffer_count);
95 app.render_scene_with_progress(&mut evaluator, on_progress);
96 if !evaluator.time_marks().is_empty() {
97 app.render_capture_marks(&mut evaluator);
98 }
99}
100
101const DEFAULT_LOGIC_FPS: f64 = 120.0;
103
104pub struct RenderThreadHandle {
108 submit_frame_tx: async_channel::Sender<RenderFrame>,
109 back_rx: async_channel::Receiver<RenderFrame>,
110 worker_rx: async_channel::Receiver<RenderWorker>,
111}
112
113impl RenderThreadHandle {
114 pub fn sync_and_submit(&self, f: impl FnOnce(&mut RenderFrame)) {
116 let mut store = self.get_store();
117 f(&mut store);
118 self.submit_frame_tx.send_blocking(store).unwrap();
119 }
120
121 pub fn get_store(&self) -> RenderFrame {
123 self.back_rx.recv_blocking().unwrap()
124 }
125
126 pub fn retrive(&self) -> RenderWorker {
128 self.submit_frame_tx.close(); self.worker_rx.recv_blocking().unwrap()
130 }
131}
132
133pub struct RenderWorker {
135 ctx: WgpuContext,
136 renderer: Renderer,
137 render_textures: Vec<RenderTextures>,
138 clear_color: wgpu::Color,
139 video_writer: Option<FileWriter>,
141 video_writer_builder: Option<FileWriterBuilder>,
142 save_frames: bool,
143 output_dir: PathBuf,
144 scene_name: String,
145 width: u32,
146 height: u32,
147 fps: u32,
148}
149
150impl RenderWorker {
151 pub fn new(
159 scene_name: String,
160 scene_config: &SceneConfig,
161 output: &Output,
162 buffer_count: usize,
163 ) -> Self {
164 assert!(buffer_count >= 1, "buffer_count must be at least 1");
165 info!("Checking ffmpeg...");
166 let t = Instant::now();
167 if let Ok(ffmpeg_path) = which::which("ffmpeg") {
168 info!("ffmpeg found at {ffmpeg_path:?}");
169 } else {
170 use std::path::Path;
171
172 info!(
173 "ffmpeg not found from path env, searching in {:?}...",
174 Path::new("./").canonicalize().unwrap()
175 );
176 if Path::new("./ffmpeg").exists() {
177 info!("ffmpeg found at current working directory")
178 } else {
179 info!("ffmpeg not found at current working directory, downloading...");
180 download_ffmpeg("./").expect("failed to download ffmpeg");
181 }
182 }
183 trace!("Check ffmmpeg cost: {:?}", t.elapsed());
184
185 let t = Instant::now();
186 info!("Creating wgpu context...");
187 let ctx = pollster::block_on(WgpuContext::new());
188 trace!("Create wgpu context cost: {:?}", t.elapsed());
189
190 let mut output_dir = PathBuf::from(&output.dir);
191 if !output_dir.is_absolute() {
192 output_dir = std::env::current_dir().unwrap().join(output_dir);
193 }
194 let renderer = Renderer::new(&ctx, output.width, output.height, 8);
195 let render_textures: Vec<RenderTextures> = (0..buffer_count)
196 .map(|_| renderer.new_render_textures(&ctx))
197 .collect();
198 let clear_color = color::try_color(&scene_config.clear_color)
199 .unwrap_or(color::color("#333333ff"))
200 .convert::<LinearSrgb>();
201 let [r, g, b, a] = clear_color.components.map(|x| x as f64);
202 let clear_color = wgpu::Color { r, g, b, a };
203 let (_, _, ext) = output.format.encoding_params();
204 Self {
205 ctx,
206 renderer,
207 render_textures,
208 clear_color,
209 video_writer: None,
210 video_writer_builder: Some(
211 FileWriterBuilder::default()
212 .with_fps(output.fps)
213 .with_size(output.width, output.height)
214 .with_file_path(output_dir.join({
215 let template = output
216 .name_template
217 .as_deref()
218 .unwrap_or("{name}_{width}x{height}_{fps}");
219 let base_name = render_output_basename(
220 template,
221 output.name.as_deref().unwrap_or(&scene_name),
222 output.width,
223 output.height,
224 output.fps,
225 );
226 format!("{base_name}.{ext}")
227 }))
228 .with_output_format(output.format),
229 ),
230 save_frames: output.save_frames,
231 output_dir,
232 scene_name,
233 width: output.width,
234 height: output.height,
235 fps: output.fps,
236 }
237 }
238
239 pub fn save_frame_dir(&self) -> PathBuf {
241 self.output_dir.join(format!(
242 "{}_{}x{}_{}-frames",
243 self.scene_name, self.width, self.height, self.fps
244 ))
245 }
246
247 pub fn yeet(self) -> RenderThreadHandle {
251 let (submit_frame_tx, submit_frame_rx) = async_channel::bounded(1);
252 let (back_tx, back_rx) = async_channel::bounded(1);
253 let (worker_tx, worker_rx) = async_channel::bounded(1);
254
255 back_tx.send_blocking(RenderFrame::default()).unwrap();
256 std::thread::spawn(move || {
257 let mut worker = self;
258 let n = worker.render_textures.len();
259 let mut frame_count = 0u64;
260 let mut cur = 0usize;
261 let mut pending: VecDeque<(usize, u64)> = VecDeque::new();
262
263 while let Ok(store) = submit_frame_rx.recv_blocking() {
264 if pending.len() >= n {
266 let (prev, prev_fc) = pending.pop_front().unwrap();
267 worker.render_textures[prev].finish_readback(&worker.ctx);
268 worker.output_frame_from(prev, prev_fc);
269 }
270
271 worker.renderer.render_frame(
273 &mut worker.render_textures[cur],
274 worker.clear_color,
275 &store,
276 );
277 worker.render_textures[cur].start_readback(&worker.ctx);
278
279 pending.push_back((cur, frame_count));
280 frame_count += 1;
281 cur = (cur + 1) % n;
282
283 back_tx.send_blocking(store).unwrap();
286
287 while let Some(&(prev, _)) = pending.front() {
290 if !worker.render_textures[prev].try_finish_readback(&worker.ctx) {
292 break;
293 }
294 let (prev, prev_fc) = pending.pop_front().unwrap();
295 worker.output_frame_from(prev, prev_fc);
296 }
297 }
298
299 while let Some((prev, prev_fc)) = pending.pop_front() {
301 worker.render_textures[prev].finish_readback(&worker.ctx);
302 worker.output_frame_from(prev, prev_fc);
303 }
304
305 worker_tx.send_blocking(worker).unwrap();
306 });
307 RenderThreadHandle {
308 submit_frame_tx,
309 back_rx,
310 worker_rx,
311 }
312 }
313
314 pub fn render_store(&mut self, store: &RenderFrame) {
316 #[cfg(feature = "profiling")]
317 profiling::scope!("frame");
318
319 {
320 #[cfg(feature = "profiling")]
321 profiling::scope!("render");
322
323 self.renderer
324 .render_frame(&mut self.render_textures[0], self.clear_color, store);
325 }
326
327 #[cfg(feature = "profiling")]
328 profiling::finish_frame!();
329 }
330
331 pub fn output_frame_from(&mut self, target_idx: usize, frame_number: u64) {
333 self.write_frame_from(target_idx);
334 if self.save_frames {
335 self.save_frame_from(target_idx, frame_number);
336 }
337 }
338
339 pub fn write_frame_from(&mut self, target_idx: usize) {
341 let data = self.render_textures[target_idx]
342 .render_texture
343 .texture_data();
344 if let Some(video_writer) = self.video_writer.as_mut() {
345 video_writer.write_frame(data);
346 } else if let Some(builder) = self.video_writer_builder.as_ref() {
347 self.video_writer
348 .get_or_insert(builder.clone().build())
349 .write_frame(data);
350 }
351 }
352
353 pub fn save_frame_from(&mut self, target_idx: usize, frame_number: u64) {
355 let path = self.save_frame_dir().join(format!("{frame_number:04}.png"));
356 let dir = path.parent().unwrap();
357 if !dir.exists() || !dir.is_dir() {
358 std::fs::create_dir_all(dir).unwrap();
359 }
360 let buffer = self.render_textures[target_idx].get_rendered_texture_img_buffer(&self.ctx);
362 buffer.save(path).unwrap();
363 }
364
365 pub fn capture_frame(&mut self, path: impl AsRef<Path>) {
367 let path = path.as_ref();
368 let path = if !path.is_absolute() {
369 self.output_dir
370 .join(format!(
371 "{}_{}x{}_{}",
372 self.scene_name, self.width, self.height, self.fps
373 ))
374 .join(path)
375 } else {
376 path.to_path_buf()
377 };
378 let dir = path.parent().unwrap();
379 if !dir.exists() || !dir.is_dir() {
380 std::fs::create_dir_all(dir).unwrap();
381 }
382 let buffer = self.render_textures[0].get_rendered_texture_img_buffer(&self.ctx);
383 buffer.save(path).unwrap();
384 }
385}
386
387pub struct RanimRenderApp {
390 render_worker: Option<RenderWorker>,
391 fps: u32,
392 store: RenderFrame,
393}
394
395impl RanimRenderApp {
396 pub fn new(
398 scene_name: String,
399 scene_config: &SceneConfig,
400 output: &Output,
401 buffer_count: usize,
402 ) -> Self {
403 let render_worker = RenderWorker::new(scene_name, scene_config, output, buffer_count);
404 Self {
405 render_worker: Some(render_worker),
406 fps: output.fps,
407 store: RenderFrame::default(),
408 }
409 }
410
411 #[instrument(skip_all)]
415 pub fn render_scene_with_progress(
416 &mut self,
417 evaluator: &mut SceneEvaluator,
418 on_progress: Option<Box<dyn Fn(u64, u64) + Send>>,
419 ) {
420 let start = Instant::now();
421 #[cfg(feature = "profiling")]
422 let (_cpu_server, _gpu_server) = {
423 puffin::set_scopes_on(true);
424 let cpu_server =
426 puffin_http::Server::new(&format!("0.0.0.0:{}", puffin_http::DEFAULT_PORT))
427 .unwrap();
428 let gpu_server = puffin_http::Server::new_custom(
430 &format!("0.0.0.0:{}", puffin_http::DEFAULT_PORT + 1),
431 |sink| PUFFIN_GPU_PROFILER.lock().unwrap().add_sink(sink),
432 |id| _ = PUFFIN_GPU_PROFILER.lock().unwrap().remove_sink(id),
433 )
434 .unwrap();
435 (cpu_server, gpu_server)
436 };
437
438 let worker_thread = self.render_worker.take().unwrap().yeet();
439
440 let total_secs = evaluator.total_secs();
441 let fps = self.fps as f64;
442 let raw_frames = total_secs * fps;
443 let n = raw_frames.ceil() as u64;
446 let num_frames = if (raw_frames - raw_frames.round()).abs() < 1e-9 {
447 n
448 } else {
449 n + 1
450 };
451 let style = ProgressStyle::with_template(
452 "[{elapsed_precise}] [{wide_bar:.cyan/blue}] frame {human_pos}/{human_len} (eta {eta}) {msg}",
453 )
454 .unwrap()
455 .with_key("eta", |state: &ProgressState, w: &mut dyn std::fmt::Write| {
456 write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()
457 })
458 .progress_chars("#>-");
459
460 let span = Span::current();
461 span.pb_set_style(&style);
462 span.pb_set_length(num_frames);
463
464 (0..num_frames)
465 .map(|f| (f as f64 / fps).min(total_secs))
466 .enumerate()
467 .for_each(|(i, sec)| {
468 let mut frame_items = Vec::new();
469 evaluator.advance_to(sec);
470 evaluator.sample_into(&mut frame_items);
471 worker_thread.sync_and_submit(move |store| {
472 store.update(frame_items.into_iter());
473 });
474
475 span.pb_inc(1);
476 if let Some(cb) = &on_progress {
477 cb(i as u64 + 1, num_frames);
478 }
479 span.pb_set_message(
480 format!(
481 "rendering {:.1?}/{:.1?}",
482 Duration::from_secs_f64(sec),
483 Duration::from_secs_f64(total_secs)
484 )
485 .as_str(),
486 );
487 });
488 self.render_worker.replace(worker_thread.retrive());
489
490 info!(
491 "rendered {} frames({:?}) in {:?}",
492 num_frames,
493 Duration::from_secs_f64(evaluator.total_secs()),
494 start.elapsed(),
495 );
496 trace!("render timeline cost: {:?}", start.elapsed());
497 }
498
499 #[instrument(skip_all)]
501 pub fn render_capture_marks(&mut self, evaluator: &mut SceneEvaluator) {
502 let start = Instant::now();
503 let timemarks = evaluator
504 .time_marks()
505 .iter()
506 .filter(|mark| matches!(mark.1, TimeMark::Capture(_)))
507 .map(|(sec, mark)| (*sec, mark.clone()))
508 .collect::<Vec<_>>();
509
510 let style = ProgressStyle::with_template(
511 "[{elapsed_precise}] [{wide_bar:.cyan/blue}] frame {human_pos}/{human_len} (eta {eta}) {msg}",
512 )
513 .unwrap()
514 .with_key("eta", |state: &ProgressState, w: &mut dyn std::fmt::Write| {
515 write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()
516 })
517 .progress_chars("#>-");
518
519 let span = Span::current();
520 span.pb_set_style(&style);
521 span.pb_set_length(timemarks.len() as u64);
522 let _enter = span.enter();
523
524 let mut captured = 0usize;
525 for (sec, TimeMark::Capture(filename)) in timemarks {
526 evaluator.seek(sec);
529 let mut frame_items = Vec::new();
530 evaluator.sample_into(&mut frame_items);
531 self.store.update(frame_items.into_iter());
532 let worker = self.render_worker.as_mut().unwrap();
533 worker.render_store(&self.store);
534 worker.capture_frame(filename);
535 span.pb_inc(1);
536 captured += 1;
537 }
538 info!("saved {} capture frames from time marks", captured);
539
540 trace!("save capture frames cost: {:?}", start.elapsed());
541 }
542}
543
544const FFMPEG_RELEASE_URL: &str = "https://github.com/eugeneware/ffmpeg-static/releases/latest";
546
547#[allow(unused)]
549pub(crate) fn exe_dir() -> PathBuf {
550 std::env::current_exe()
551 .unwrap()
552 .parent()
553 .unwrap()
554 .to_path_buf()
555}
556
557pub fn download_ffmpeg(target_dir: impl AsRef<Path>) -> Result<PathBuf, anyhow::Error> {
559 use anyhow::Context;
560 use itertools::Itertools;
561 use std::io::Read;
562 use tracing::info;
563
564 let target_dir = target_dir.as_ref();
565
566 let res = reqwest::blocking::get(FFMPEG_RELEASE_URL).context("failed to get release url")?;
567 let url = res.url().to_string();
568 let url = url.split("tag").collect_array::<2>().unwrap();
569 let url = format!("{}/download/{}", url[0], url[1]);
570 info!("ffmpeg release url: {url:?}");
571
572 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
573 let url = format!("{url}/ffmpeg-win32-x64.gz");
574 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
575 let url = format!("{url}/ffmpeg-linux-x64.gz");
576 #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
577 let url = format!("{url}/ffmpeg-linux-arm64.gz");
578 #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
579 let url = format!("{url}/ffmpeg-darwin-x64.gz");
580 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
581 let url = format!("{url}/ffmpeg-darwin-arm64.gz");
582
583 info!("downloading ffmpeg from {url:?}...");
584
585 let res = reqwest::blocking::get(&url).context("get err")?;
586 let mut decoder = flate2::bufread::GzDecoder::new(std::io::BufReader::new(
587 std::io::Cursor::new(res.bytes().unwrap()),
588 ));
589 let mut bytes = Vec::new();
590 decoder
591 .read_to_end(&mut bytes)
592 .context("GzDecoder decode err")?;
593 let ffmpeg_path = target_dir.join("ffmpeg");
594 std::fs::write(&ffmpeg_path, bytes).unwrap();
595
596 #[cfg(target_family = "unix")]
597 {
598 use std::os::unix::fs::PermissionsExt;
599
600 std::fs::set_permissions(&ffmpeg_path, std::fs::Permissions::from_mode(0o755))?;
601 }
602 info!("ffmpeg downloaded to {target_dir:?}");
603 Ok(ffmpeg_path)
604}
605
606#[cfg(test)]
607mod tests {
608 use super::render_output_basename;
609
610 #[test]
611 fn test_render_output_basename_default_template() {
612 assert_eq!(
613 render_output_basename("{name}_{width}x{height}_{fps}", "my_scene", 1920, 1080, 60),
614 "my_scene_1920x1080_60"
615 );
616 }
617
618 #[test]
619 fn test_render_output_basename_custom_template() {
620 assert_eq!(
621 render_output_basename(
622 "{name}_{fps}fps_{width}x{height}",
623 "my_scene",
624 1920,
625 1080,
626 60
627 ),
628 "my_scene_60fps_1920x1080"
629 );
630 }
631
632 #[test]
633 fn test_render_output_basename_name_only() {
634 assert_eq!(
635 render_output_basename("{name}", "my_scene", 1920, 1080, 60),
636 "my_scene"
637 );
638 }
639}