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::store::CoreItemStore;
10use ranim_core::{SealedRanimScene, TimeMark};
11use ranim_render::resource::{RenderPool, RenderTextures};
12use ranim_render::{Renderer, utils::WgpuContext};
13use std::path::{Path, PathBuf};
14use std::time::Duration;
15use std::time::Instant;
16use tracing::{Span, info, instrument, trace};
17use tracing_indicatif::span_ext::IndicatifSpanExt;
18
19pub(crate) mod file_writer;
20
21#[cfg(feature = "profiling")]
22use ranim_render::PUFFIN_GPU_PROFILER;
23
24pub fn render_scene(scene: &Scene, buffer_count: usize) {
26 for (i, output) in scene.outputs.iter().enumerate() {
27 info!(
28 "Rendering output {}/{} ({})",
29 i + 1,
30 scene.outputs.len(),
31 output.format
32 );
33 render_scene_output(
34 scene.constructor,
35 scene.name.to_string(),
36 &scene.config,
37 output,
38 buffer_count,
39 );
40 }
41}
42
43pub fn render_scene_output(
45 constructor: impl SceneConstructor,
46 name: String,
47 scene_config: &SceneConfig,
48 output: &Output,
49 buffer_count: usize,
50) {
51 render_scene_output_with_progress(constructor, name, scene_config, output, buffer_count, None);
52}
53
54pub fn render_scene_output_with_progress(
58 constructor: impl SceneConstructor,
59 name: String,
60 scene_config: &SceneConfig,
61 output: &Output,
62 buffer_count: usize,
63 on_progress: Option<Box<dyn Fn(u64, u64) + Send>>,
64) {
65 use std::time::Instant;
66
67 info!(
68 "Output: {}x{} {}fps {} dir={:?} save_frames={}",
69 output.width, output.height, output.fps, output.format, output.dir, output.save_frames
70 );
71
72 let t = Instant::now();
73 let scene = constructor.build_scene();
74 trace!("Build timeline cost: {:?}", t.elapsed());
75
76 let mut app = RanimRenderApp::new(name, scene_config, output, buffer_count);
77 app.render_scene_with_progress(&scene, on_progress);
78 if !scene.time_marks().is_empty() {
79 app.render_capture_marks(&scene);
80 }
81}
82
83struct RenderThreadHandle {
85 submit_frame_tx: async_channel::Sender<CoreItemStore>,
86 back_rx: async_channel::Receiver<CoreItemStore>,
87 worker_rx: async_channel::Receiver<RenderWorker>,
88}
89
90impl RenderThreadHandle {
91 fn sync_and_submit(&self, f: impl FnOnce(&mut CoreItemStore)) {
92 let mut store = self.get_store();
93 f(&mut store);
94 self.submit_frame_tx.send_blocking(store).unwrap();
95 }
96 fn get_store(&self) -> CoreItemStore {
97 self.back_rx.recv_blocking().unwrap()
98 }
99 fn retrive(&self) -> RenderWorker {
100 self.submit_frame_tx.close(); self.worker_rx.recv_blocking().unwrap()
102 }
103}
104
105struct RenderWorker {
106 ctx: WgpuContext,
107 renderer: Renderer,
108 render_textures: Vec<RenderTextures>,
109 pool: RenderPool,
110 clear_color: wgpu::Color,
111 video_writer: Option<FileWriter>,
113 video_writer_builder: Option<FileWriterBuilder>,
114 save_frames: bool,
115 output_dir: PathBuf,
116 scene_name: String,
117 width: u32,
118 height: u32,
119 fps: u32,
120}
121
122impl RenderWorker {
123 fn new(
124 scene_name: String,
125 scene_config: &SceneConfig,
126 output: &Output,
127 buffer_count: usize,
128 ) -> Self {
129 assert!(buffer_count >= 1, "buffer_count must be at least 1");
130 info!("Checking ffmpeg...");
131 let t = Instant::now();
132 if let Ok(ffmpeg_path) = which::which("ffmpeg") {
133 info!("ffmpeg found at {ffmpeg_path:?}");
134 } else {
135 use std::path::Path;
136
137 info!(
138 "ffmpeg not found from path env, searching in {:?}...",
139 Path::new("./").canonicalize().unwrap()
140 );
141 if Path::new("./ffmpeg").exists() {
142 info!("ffmpeg found at current working directory")
143 } else {
144 info!("ffmpeg not found at current working directory, downloading...");
145 download_ffmpeg("./").expect("failed to download ffmpeg");
146 }
147 }
148 trace!("Check ffmmpeg cost: {:?}", t.elapsed());
149
150 let t = Instant::now();
151 info!("Creating wgpu context...");
152 let ctx = pollster::block_on(WgpuContext::new());
153 trace!("Create wgpu context cost: {:?}", t.elapsed());
154
155 let mut output_dir = PathBuf::from(&output.dir);
156 if !output_dir.is_absolute() {
157 output_dir = std::env::current_dir().unwrap().join(output_dir);
158 }
159 let renderer = Renderer::new(&ctx, output.width, output.height, 8);
160 let render_textures: Vec<RenderTextures> = (0..buffer_count)
161 .map(|_| renderer.new_render_textures(&ctx))
162 .collect();
163 let clear_color = color::try_color(&scene_config.clear_color)
164 .unwrap_or(color::color("#333333ff"))
165 .convert::<LinearSrgb>();
166 let [r, g, b, a] = clear_color.components.map(|x| x as f64);
167 let clear_color = wgpu::Color { r, g, b, a };
168 let (_, _, ext) = output.format.encoding_params();
169 Self {
170 ctx,
171 renderer,
172 render_textures,
173 pool: RenderPool::new(),
174 clear_color,
175 video_writer: None,
176 video_writer_builder: Some(
177 FileWriterBuilder::default()
178 .with_fps(output.fps)
179 .with_size(output.width, output.height)
180 .with_file_path(output_dir.join(format!(
181 "{}_{}x{}_{}.{ext}",
182 output.name.clone().unwrap_or(scene_name.clone()),
183 output.width,
184 output.height,
185 output.fps
186 )))
187 .with_output_format(output.format),
188 ),
189 save_frames: output.save_frames,
190 output_dir,
191 scene_name,
192 width: output.width,
193 height: output.height,
194 fps: output.fps,
195 }
196 }
197
198 fn save_frame_dir(&self) -> PathBuf {
199 self.output_dir.join(format!(
200 "{}_{}x{}_{}-frames",
201 self.scene_name, self.width, self.height, self.fps
202 ))
203 }
204
205 fn yeet(self) -> RenderThreadHandle {
206 let (submit_frame_tx, submit_frame_rx) = async_channel::bounded(1);
207 let (back_tx, back_rx) = async_channel::bounded(1);
208 let (worker_tx, worker_rx) = async_channel::bounded(1);
209
210 back_tx.send_blocking(CoreItemStore::default()).unwrap();
211 std::thread::spawn(move || {
212 let mut worker = self;
213 let n = worker.render_textures.len();
214 let mut frame_count = 0u64;
215 let mut cur = 0usize;
216 let mut pending: VecDeque<(usize, u64)> = VecDeque::new();
217
218 while let Ok(store) = submit_frame_rx.recv_blocking() {
219 if pending.len() >= n {
221 let (prev, prev_fc) = pending.pop_front().unwrap();
222 worker.render_textures[prev].finish_readback(&worker.ctx);
223 worker.output_frame_from(prev, prev_fc);
224 }
225
226 worker.renderer.render_store_with_pool(
228 &worker.ctx,
229 &mut worker.render_textures[cur],
230 worker.clear_color,
231 &store,
232 &mut worker.pool,
233 );
234 worker.render_textures[cur].start_readback(&worker.ctx);
235 worker.pool.clean();
236
237 pending.push_back((cur, frame_count));
238 frame_count += 1;
239 cur = (cur + 1) % n;
240
241 back_tx.send_blocking(store).unwrap();
244
245 while let Some(&(prev, _)) = pending.front() {
248 if !worker.render_textures[prev].try_finish_readback(&worker.ctx) {
250 break;
251 }
252 let (prev, prev_fc) = pending.pop_front().unwrap();
253 worker.output_frame_from(prev, prev_fc);
254 }
255 }
256
257 while let Some((prev, prev_fc)) = pending.pop_front() {
259 worker.render_textures[prev].finish_readback(&worker.ctx);
260 worker.output_frame_from(prev, prev_fc);
261 }
262
263 worker_tx.send_blocking(worker).unwrap();
264 });
265 RenderThreadHandle {
266 submit_frame_tx,
267 back_rx,
268 worker_rx,
269 }
270 }
271
272 fn render_store(&mut self, store: &CoreItemStore) {
273 #[cfg(feature = "profiling")]
274 profiling::scope!("frame");
275
276 {
277 #[cfg(feature = "profiling")]
278 profiling::scope!("render");
279
280 self.renderer.render_store_with_pool(
281 &self.ctx,
282 &mut self.render_textures[0],
283 self.clear_color,
284 store,
285 &mut self.pool,
286 );
287 }
288 self.pool.clean();
289
290 #[cfg(feature = "profiling")]
291 profiling::finish_frame!();
292 }
293
294 fn output_frame_from(&mut self, target_idx: usize, frame_number: u64) {
296 self.write_frame_from(target_idx);
297 if self.save_frames {
298 self.save_frame_from(target_idx, frame_number);
299 }
300 }
301
302 fn write_frame_from(&mut self, target_idx: usize) {
304 let data = self.render_textures[target_idx]
305 .render_texture
306 .texture_data();
307 if let Some(video_writer) = self.video_writer.as_mut() {
308 video_writer.write_frame(data);
309 } else if let Some(builder) = self.video_writer_builder.as_ref() {
310 self.video_writer
311 .get_or_insert(builder.clone().build())
312 .write_frame(data);
313 }
314 }
315
316 fn save_frame_from(&mut self, target_idx: usize, frame_number: u64) {
318 let path = self.save_frame_dir().join(format!("{frame_number:04}.png"));
319 let dir = path.parent().unwrap();
320 if !dir.exists() || !dir.is_dir() {
321 std::fs::create_dir_all(dir).unwrap();
322 }
323 let buffer = self.render_textures[target_idx].get_rendered_texture_img_buffer(&self.ctx);
325 buffer.save(path).unwrap();
326 }
327
328 pub fn capture_frame(&mut self, path: impl AsRef<Path>) {
330 let path = path.as_ref();
331 let path = if !path.is_absolute() {
332 self.output_dir
333 .join(format!(
334 "{}_{}x{}_{}",
335 self.scene_name, self.width, self.height, self.fps
336 ))
337 .join(path)
338 } else {
339 path.to_path_buf()
340 };
341 let dir = path.parent().unwrap();
342 if !dir.exists() || !dir.is_dir() {
343 std::fs::create_dir_all(dir).unwrap();
344 }
345 let buffer = self.render_textures[0].get_rendered_texture_img_buffer(&self.ctx);
346 buffer.save(path).unwrap();
347 }
348}
349
350struct RanimRenderApp {
352 render_worker: Option<RenderWorker>,
353 fps: u32,
354 store: CoreItemStore,
355}
356
357impl RanimRenderApp {
358 fn new(
359 scene_name: String,
360 scene_config: &SceneConfig,
361 output: &Output,
362 buffer_count: usize,
363 ) -> Self {
364 let render_worker = RenderWorker::new(scene_name, scene_config, output, buffer_count);
365 Self {
366 render_worker: Some(render_worker),
367 fps: output.fps,
368 store: CoreItemStore::default(),
369 }
370 }
371
372 #[instrument(skip_all)]
373 pub fn render_scene_with_progress(
374 &mut self,
375 timeline: &SealedRanimScene,
376 on_progress: Option<Box<dyn Fn(u64, u64) + Send>>,
377 ) {
378 let start = Instant::now();
379 #[cfg(feature = "profiling")]
380 let (_cpu_server, _gpu_server) = {
381 puffin::set_scopes_on(true);
382 let cpu_server =
384 puffin_http::Server::new(&format!("0.0.0.0:{}", puffin_http::DEFAULT_PORT))
385 .unwrap();
386 let gpu_server = puffin_http::Server::new_custom(
388 &format!("0.0.0.0:{}", puffin_http::DEFAULT_PORT + 1),
389 |sink| PUFFIN_GPU_PROFILER.lock().unwrap().add_sink(sink),
390 |id| _ = PUFFIN_GPU_PROFILER.lock().unwrap().remove_sink(id),
391 )
392 .unwrap();
393 (cpu_server, gpu_server)
394 };
395
396 let worker_thread = self.render_worker.take().unwrap().yeet();
397
398 let total_secs = timeline.total_secs();
399 let fps = self.fps as f64;
400 let raw_frames = total_secs * fps;
401 let n = raw_frames.ceil() as u64;
404 let num_frames = if (raw_frames - raw_frames.round()).abs() < 1e-9 {
405 n
406 } else {
407 n + 1
408 };
409 let style = ProgressStyle::with_template(
410 "[{elapsed_precise}] [{wide_bar:.cyan/blue}] frame {human_pos}/{human_len} (eta {eta}) {msg}",
411 )
412 .unwrap()
413 .with_key("eta", |state: &ProgressState, w: &mut dyn std::fmt::Write| {
414 write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()
415 })
416 .progress_chars("#>-");
417
418 let span = Span::current();
419 span.pb_set_style(&style);
420 span.pb_set_length(num_frames);
421
422 (0..num_frames)
423 .map(|f| (f as f64 / fps).min(total_secs))
424 .enumerate()
425 .for_each(|(i, sec)| {
426 worker_thread.sync_and_submit(|store| {
427 store.update(timeline.eval_at_sec(sec));
428 });
429
430 span.pb_inc(1);
431 if let Some(cb) = &on_progress {
432 cb(i as u64 + 1, num_frames);
433 }
434 span.pb_set_message(
435 format!(
436 "rendering {:.1?}/{:.1?}",
437 Duration::from_secs_f64(sec),
438 Duration::from_secs_f64(total_secs)
439 )
440 .as_str(),
441 );
442 });
443 self.render_worker.replace(worker_thread.retrive());
444
445 info!(
446 "rendered {} frames({:?}) in {:?}",
447 num_frames,
448 Duration::from_secs_f64(timeline.total_secs()),
449 start.elapsed(),
450 );
451 trace!("render timeline cost: {:?}", start.elapsed());
452 }
453
454 #[instrument(skip_all)]
455 fn render_capture_marks(&mut self, timeline: &SealedRanimScene) {
456 let start = Instant::now();
457 let timemarks = timeline
458 .time_marks()
459 .iter()
460 .filter(|mark| matches!(mark.1, TimeMark::Capture(_)))
461 .collect::<Vec<_>>();
462
463 let style = ProgressStyle::with_template(
464 "[{elapsed_precise}] [{wide_bar:.cyan/blue}] frame {human_pos}/{human_len} (eta {eta}) {msg}",
465 )
466 .unwrap()
467 .with_key("eta", |state: &ProgressState, w: &mut dyn std::fmt::Write| {
468 write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()
469 })
470 .progress_chars("#>-");
471
472 let span = Span::current();
473 span.pb_set_style(&style);
474 span.pb_set_length(timemarks.len() as u64);
475 let _enter = span.enter();
476
477 for (sec, TimeMark::Capture(filename)) in &timemarks {
478 let alpha = *sec / timeline.total_secs();
479
480 self.store.update(timeline.eval_at_alpha(alpha));
481 let worker = self.render_worker.as_mut().unwrap();
482 worker.render_store(&self.store);
483 worker.capture_frame(filename);
484 span.pb_inc(1);
485 }
486 info!("saved {} capture frames from time marks", timemarks.len());
487 trace!("save capture frames cost: {:?}", start.elapsed());
488 }
489}
490
491const FFMPEG_RELEASE_URL: &str = "https://github.com/eugeneware/ffmpeg-static/releases/latest";
493
494#[allow(unused)]
495pub(crate) fn exe_dir() -> PathBuf {
496 std::env::current_exe()
497 .unwrap()
498 .parent()
499 .unwrap()
500 .to_path_buf()
501}
502
503pub fn download_ffmpeg(target_dir: impl AsRef<Path>) -> Result<PathBuf, anyhow::Error> {
505 use anyhow::Context;
506 use itertools::Itertools;
507 use std::io::Read;
508 use tracing::info;
509
510 let target_dir = target_dir.as_ref();
511
512 let res = reqwest::blocking::get(FFMPEG_RELEASE_URL).context("failed to get release url")?;
513 let url = res.url().to_string();
514 let url = url.split("tag").collect_array::<2>().unwrap();
515 let url = format!("{}/download/{}", url[0], url[1]);
516 info!("ffmpeg release url: {url:?}");
517
518 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
519 let url = format!("{url}/ffmpeg-win32-x64.gz");
520 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
521 let url = format!("{url}/ffmpeg-linux-x64.gz");
522 #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
523 let url = format!("{url}/ffmpeg-linux-arm64.gz");
524 #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
525 let url = format!("{url}/ffmpeg-darwin-x64.gz");
526 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
527 let url = format!("{url}/ffmpeg-darwin-arm64.gz");
528
529 info!("downloading ffmpeg from {url:?}...");
530
531 let res = reqwest::blocking::get(&url).context("get err")?;
532 let mut decoder = flate2::bufread::GzDecoder::new(std::io::BufReader::new(
533 std::io::Cursor::new(res.bytes().unwrap()),
534 ));
535 let mut bytes = Vec::new();
536 decoder
537 .read_to_end(&mut bytes)
538 .context("GzDecoder decode err")?;
539 let ffmpeg_path = target_dir.join("ffmpeg");
540 std::fs::write(&ffmpeg_path, bytes).unwrap();
541
542 #[cfg(target_family = "unix")]
543 {
544 use std::os::unix::fs::PermissionsExt;
545
546 std::fs::set_permissions(&ffmpeg_path, std::fs::Permissions::from_mode(0o755))?;
547 }
548 info!("ffmpeg downloaded to {target_dir:?}");
549 Ok(ffmpeg_path)
550}