722 lines · 25.5 KB · Rust
RawDownload
// MARK: Render apiuse std::collections::VecDeque; use crate::cmd::render::file_writer::OutputFormatExt;use crate::{Output, Scene, SceneConfig, SceneConstructor};use file_writer::{FileWriter, FileWriterBuilder};use indicatif::{ProgressState, ProgressStyle};use ranim_core::color::{self, LinearSrgb};use ranim_core::{SceneEvaluator, TimeMark};use ranim_render::resource::RenderTextures;use ranim_render::{Renderer, utils::WgpuContext, world::RenderFrame};use std::path::{Path, PathBuf};use std::time::Duration;use std::time::Instant;use tracing::{Span, info, instrument, trace};use tracing_indicatif::span_ext::IndicatifSpanExt; pub(crate) mod audio;pub(crate) mod file_writer; /// Render the output basename template by replacing placeholders.////// Supported placeholders:/// - `{name}`: the scene/output name/// - `{width}`: output width in pixels/// - `{height}`: output height in pixels/// - `{fps}`: output frame ratefn render_output_basename(template: &str, name: &str, width: u32, height: u32, fps: u32) -> String {    template        .replace("{name}", name)        .replace("{width}", &width.to_string())        .replace("{height}", &height.to_string())        .replace("{fps}", &fps.to_string())} /// Render a scene with all its outputspub fn render_scene(scene: &Scene, buffer_count: usize) {    for (i, output) in scene.outputs.iter().enumerate() {        info!(            "Rendering output {}/{} ({})",            i + 1,            scene.outputs.len(),            output.format        );        render_scene_output(            scene.constructor,            scene.name.to_string(),            &scene.config,            output,            buffer_count,        );    }} /// Render a scene outputpub fn render_scene_output(    constructor: impl SceneConstructor,    name: String,    scene_config: &SceneConfig,    output: &Output,    buffer_count: usize,) {    render_scene_output_with_progress(constructor, name, scene_config, output, buffer_count, None);} /// A single rendering job: one [`Output`] plus whether capture marks should/// be processed after the main pass.////// The CLI currently only creates full-scene jobs. Time-range and still-frame/// rendering will extend this type together with the debug rendering commands.pub struct RenderJob {    /// Output settings used for this job.    pub output: Output,    /// Render and save every [`TimeMark::Capture`] after the main pass.    ///    /// This is enabled by `ranim output` (declared outputs) and disabled by    /// the ad-hoc `ranim render` command.    pub capture_marks: bool,} impl RenderJob {    /// Creates a job that renders the whole scene.    pub fn full(output: Output, capture_marks: bool) -> Self {        Self {            output,            capture_marks,        }    }} /// Render one output with optional progress callback.////// The callback receives `(current_frame, total_frames)` each frame.pub fn render_scene_output_with_progress(    constructor: impl SceneConstructor,    name: String,    scene_config: &SceneConfig,    output: &Output,    buffer_count: usize,    on_progress: Option<Box<dyn Fn(u64, u64) + Send>>,) {    render_scene_job(        constructor,        name,        scene_config,        RenderJob::full(output.clone(), true),        buffer_count,        on_progress,    );} /// Render a single ad-hoc output for a scene.////// This is the low-level rendering entry point used by `ranim render`: it/// renders the whole scene once with the given [`Output`], but unlike/// [`render_scene_output_with_progress`] it does not process capture marks.pub fn render_scene_once(    constructor: impl SceneConstructor,    name: String,    scene_config: &SceneConfig,    output: &Output,    buffer_count: usize,) {    render_scene_job(        constructor,        name,        scene_config,        RenderJob::full(output.clone(), false),        buffer_count,        None,    );} /// Build the scene and run a [`RenderJob`].pub fn render_scene_job(    constructor: impl SceneConstructor,    name: String,    scene_config: &SceneConfig,    job: RenderJob,    buffer_count: usize,    on_progress: Option<Box<dyn Fn(u64, u64) + Send>>,) {    use std::time::Instant;     let RenderJob {        output,        capture_marks,    } = job;     info!(        "Output: {}x{} {}fps {} dir={:?} save_frames={}",        output.width, output.height, output.fps, output.format, output.dir, output.save_frames    );     let t = Instant::now();    let scene = constructor.build_scene();    trace!("Build timeline cost: {:?}", t.elapsed());     // Default logic grid 120 Hz (per the time model design); render fps only    // decides which logic states are sampled.    let mut evaluator = scene.into_evaluator(DEFAULT_LOGIC_FPS);     let mut app = RanimRenderApp::new(name, scene_config, &output, buffer_count);    app.render_scene_with_progress(&mut evaluator, on_progress);     if capture_marks && !evaluator.time_marks().is_empty() {        app.render_capture_marks(&mut evaluator);    }     // The audio plane was baked in one offline pass at seal: mux the baked    // buffer into the finished file.    if evaluator.has_audio() {        match audio::audio_codec(output.format) {            Some(_) => {                let pcm = evaluator.audio();                // Finalize the encoder before reading the file back for muxing.                app.finish_video();                let video_path = app.video_path();                if !video_path.exists() {                    tracing::warn!("no video file was written, skipping audio muxing");                } else if let Err(err) = audio::mux_audio_into_video(video_path, pcm, output.format)                {                    tracing::error!("failed to mux audio into {}: {err:#}", video_path.display());                }            }            None => tracing::warn!(                "the scene has audio, but the {:?} format cannot store it; rendering silently",                output.format            ),        }    }} /// Default logic grid resolution (Hz), per the time model design.const DEFAULT_LOGIC_FPS: f64 = 120.0; /// Handle to the background render thread, used to submit frame data from the main thread.////// Dropping this handle closes the submission channel and terminates the worker thread loop.pub struct RenderThreadHandle {    submit_frame_tx: async_channel::Sender<RenderFrame>,    back_rx: async_channel::Receiver<RenderFrame>,    worker_rx: async_channel::Receiver<RenderWorker>,} impl RenderThreadHandle {    /// Reuses a [`RenderFrame`], applies `f` to it, and submits it to the background render thread.    pub fn sync_and_submit(&self, f: impl FnOnce(&mut RenderFrame)) {        let mut store = self.get_store();        f(&mut store);        self.submit_frame_tx.send_blocking(store).unwrap();    }     /// Retrieves a previously rendered [`RenderFrame`] from the background thread for reuse.    pub fn get_store(&self) -> RenderFrame {        self.back_rx.recv_blocking().unwrap()    }     /// Closes the submission channel, waits for the background thread to finish, and returns the [`RenderWorker`].    pub fn retrive(&self) -> RenderWorker {        self.submit_frame_tx.close(); // This terminates the worker thread loop        self.worker_rx.recv_blocking().unwrap()    }} /// Background rendering worker that owns the wgpu context, renderer, output textures, and video/image writers.pub struct RenderWorker {    ctx: WgpuContext,    renderer: Renderer,    render_textures: Vec<RenderTextures>,    clear_color: wgpu::Color,    // video writer    video_writer: Option<FileWriter>,    video_writer_builder: Option<FileWriterBuilder>,    video_path: PathBuf,    save_frames: bool,    output_dir: PathBuf,    scene_name: String,    width: u32,    height: u32,    fps: u32,} impl RenderWorker {    /// Creates a new [`RenderWorker`].    ///    /// This checks for or downloads `ffmpeg`, initializes the wgpu context and renderer, and configures the video writer from [`Output`].    ///    /// # Panics    ///    /// Panics if `buffer_count < 1`.    pub fn new(        scene_name: String,        scene_config: &SceneConfig,        output: &Output,        buffer_count: usize,    ) -> Self {        assert!(buffer_count >= 1, "buffer_count must be at least 1");        info!("Checking ffmpeg...");        let t = Instant::now();        if let Ok(ffmpeg_path) = which::which("ffmpeg") {            info!("ffmpeg found at {ffmpeg_path:?}");        } else {            use std::path::Path;             info!(                "ffmpeg not found from path env, searching in {:?}...",                Path::new("./").canonicalize().unwrap()            );            if Path::new("./ffmpeg").exists() {                info!("ffmpeg found at current working directory")            } else {                info!("ffmpeg not found at current working directory, downloading...");                download_ffmpeg("./").expect("failed to download ffmpeg");            }        }        trace!("Check ffmmpeg cost: {:?}", t.elapsed());         let t = Instant::now();        info!("Creating wgpu context...");        let ctx = pollster::block_on(WgpuContext::new());        trace!("Create wgpu context cost: {:?}", t.elapsed());         let mut output_dir = PathBuf::from(&output.dir);        if !output_dir.is_absolute() {            output_dir = std::env::current_dir().unwrap().join(output_dir);        }        let renderer = Renderer::new(&ctx, output.width, output.height, 8);        let render_textures: Vec<RenderTextures> = (0..buffer_count)            .map(|_| renderer.new_render_textures(&ctx))            .collect();        let clear_color = color::try_color(&scene_config.clear_color)            .unwrap_or(color::color("#333333ff"))            .convert::<LinearSrgb>();        let [r, g, b, a] = clear_color.components.map(|x| x as f64);        let clear_color = wgpu::Color { r, g, b, a };        let (_, _, ext) = output.format.encoding_params();        let video_path = output_dir.join({            let template = output                .name_template                .as_deref()                .unwrap_or("{name}_{width}x{height}_{fps}");            let base_name = render_output_basename(                template,                output.name.as_deref().unwrap_or(&scene_name),                output.width,                output.height,                output.fps,            );            format!("{base_name}.{ext}")        });        Self {            ctx,            renderer,            render_textures,            clear_color,            video_writer: None,            video_writer_builder: Some(                FileWriterBuilder::default()                    .with_fps(output.fps)                    .with_size(output.width, output.height)                    .with_file_path(video_path.clone())                    .with_output_format(output.format),            ),            video_path,            save_frames: output.save_frames,            output_dir,            scene_name,            width: output.width,            height: output.height,            fps: output.fps,        }    }     /// Returns the directory path used to save individual frame images.    pub fn save_frame_dir(&self) -> PathBuf {        self.output_dir.join(format!(            "{}_{}x{}_{}-frames",            self.scene_name, self.width, self.height, self.fps        ))    }     /// Returns the path of the video file being written.    pub fn video_path(&self) -> &Path {        &self.video_path    }     /// Finalize the video file: flush the encoder and wait for it to exit.    ///    /// After this call the file on disk is complete (valid container index)    /// and no further frames can be written.    pub fn finish_video(&mut self) {        self.video_writer = None;    }     /// Moves this [`RenderWorker`] into a newly spawned background thread and returns a [`RenderThreadHandle`].    ///    /// The background thread loops over incoming [`RenderFrame`]s, rendering and outputting frames using multi-buffered async readback.    pub fn yeet(self) -> RenderThreadHandle {        let (submit_frame_tx, submit_frame_rx) = async_channel::bounded(1);        let (back_tx, back_rx) = async_channel::bounded(1);        let (worker_tx, worker_rx) = async_channel::bounded(1);         back_tx.send_blocking(RenderFrame::default()).unwrap();        std::thread::spawn(move || {            let mut worker = self;            let n = worker.render_textures.len();            let mut frame_count = 0u64;            let mut cur = 0usize;            let mut pending: VecDeque<(usize, u64)> = VecDeque::new();             while let Ok(store) = submit_frame_rx.recv_blocking() {                // Drain oldest pending readback if all targets are occupied                if pending.len() >= n {                    let (prev, prev_fc) = pending.pop_front().unwrap();                    worker.render_textures[prev].finish_readback(&worker.ctx);                    worker.output_frame_from(prev, prev_fc);                }                 // Render current frame and start async readback                worker.renderer.render_frame(                    &mut worker.render_textures[cur],                    worker.clear_color,                    &store,                );                worker.render_textures[cur].start_readback(&worker.ctx);                 pending.push_back((cur, frame_count));                frame_count += 1;                cur = (cur + 1) % n;                 // Return store early so main thread can eval next frame                // while GPU processes the readback                back_tx.send_blocking(store).unwrap();                 // Now try to drain any completed readbacks while we wait                // for the next frame from the main thread                while let Some(&(prev, _)) = pending.front() {                    // Non-blocking: check if the oldest readback is ready                    if !worker.render_textures[prev].try_finish_readback(&worker.ctx) {                        break;                    }                    let (prev, prev_fc) = pending.pop_front().unwrap();                    worker.output_frame_from(prev, prev_fc);                }            }             // Flush all remaining pending frames            while let Some((prev, prev_fc)) = pending.pop_front() {                worker.render_textures[prev].finish_readback(&worker.ctx);                worker.output_frame_from(prev, prev_fc);            }             worker_tx.send_blocking(worker).unwrap();        });        RenderThreadHandle {            submit_frame_tx,            back_rx,            worker_rx,        }    }     /// Renders a single frame synchronously on this worker (typically used for captures).    pub fn render_store(&mut self, store: &RenderFrame) {        self.renderer            .render_frame(&mut self.render_textures[0], self.clear_color, store);    }     /// Write and save (if [`Self::save_frames`] is true)    pub fn output_frame_from(&mut self, target_idx: usize, frame_number: u64) {        self.write_frame_from(target_idx);        if self.save_frames {            self.save_frame_from(target_idx, frame_number);        }    }     /// Write frame data from the given target to the video file.    pub fn write_frame_from(&mut self, target_idx: usize) {        let data = self.render_textures[target_idx]            .render_texture            .texture_data();        if let Some(video_writer) = self.video_writer.as_mut() {            video_writer.write_frame(data);        } else if let Some(builder) = self.video_writer_builder.as_ref() {            self.video_writer                .get_or_insert(builder.clone().build())                .write_frame(data);        }    }     /// Save frame from the given target as a PNG image.    pub fn save_frame_from(&mut self, target_idx: usize, frame_number: u64) {        let path = self.save_frame_dir().join(format!("{frame_number:04}.png"));        let dir = path.parent().unwrap();        if !dir.exists() || !dir.is_dir() {            std::fs::create_dir_all(dir).unwrap();        }        // Data is already in cpu buffer after finish_readback, this won't trigger GPU work        let buffer = self.render_textures[target_idx].get_rendered_texture_img_buffer(&self.ctx);        buffer.save(path).unwrap();    }     /// Capture frame to image file (sync path, uses target 0).    pub fn capture_frame(&mut self, path: impl AsRef<Path>) {        let path = path.as_ref();        let path = if !path.is_absolute() {            self.output_dir                .join(format!(                    "{}_{}x{}_{}",                    self.scene_name, self.width, self.height, self.fps                ))                .join(path)        } else {            path.to_path_buf()        };        let dir = path.parent().unwrap();        if !dir.exists() || !dir.is_dir() {            std::fs::create_dir_all(dir).unwrap();        }        let buffer = self.render_textures[0].get_rendered_texture_img_buffer(&self.ctx);        buffer.save(path).unwrap();    }} /// MARK: RanimRenderApp/// Application wrapper that drives frame-by-frame scene rendering, video output, and capture screenshots.pub struct RanimRenderApp {    render_worker: Option<RenderWorker>,    fps: u32,    store: RenderFrame,} impl RanimRenderApp {    /// Creates the application and initializes the internal [`RenderWorker`].    pub fn new(        scene_name: String,        scene_config: &SceneConfig,        output: &Output,        buffer_count: usize,    ) -> Self {

Lines 1–500 of 722.