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