From 4d77c8b8144fb2420b6bdb6609485def3121a43f Mon Sep 17 00:00:00 2001 From: Aditya Garud <153842990+yashranaway@users.noreply.github.com> Date: Tue, 13 Oct 2026 10:49:28 +0000 Subject: [PATCH 1/3] perf(rendering): share the project config across frame uniforms ProjectUniforms owned a full ProjectConfiguration, so every rendered frame deep-cloned the project, including every caption word twice, once in captions and once in the timeline's caption segments. Playback, scrubbing and export all build uniforms per frame, and export can clone again per render attempt. ProjectUniforms now holds an Arc. The constructors take the caller's Arc and only allocate a new config when a style segment overrides it at that frame. Export builds one Arc per export, and playback keeps one until the project changes. --- apps/desktop-gpui/src/screenshot_editor.rs | 2 +- apps/desktop/src-tauri/src/export.rs | 8 +- .../src-tauri/src/screenshot_editor.rs | 6 +- crates/cap-test/src/suites/performance.rs | 4 +- .../examples/playback-pipeline-benchmark.rs | 2 + crates/editor/src/playback.rs | 8 +- crates/editor/src/preparing_preview.rs | 1 + .../src/preparing_preview/native_tests.rs | 4 +- crates/editor/src/thumbnail.rs | 4 +- crates/export/src/preview.rs | 5 +- .../export/tests/real_recording_regression.rs | 2 +- .../examples/background-benchmark.rs | 1 + .../rendering/src/layers/animated_gradient.rs | 2 +- crates/rendering/src/lib.rs | 174 +++++++++++++++--- .../src/overlay_layers/native_tests.rs | 4 +- 15 files changed, 181 insertions(+), 46 deletions(-) diff --git a/apps/desktop-gpui/src/screenshot_editor.rs b/apps/desktop-gpui/src/screenshot_editor.rs index 7e83655f2a9..117a8e10d35 100644 --- a/apps/desktop-gpui/src/screenshot_editor.rs +++ b/apps/desktop-gpui/src/screenshot_editor.rs @@ -319,7 +319,7 @@ async fn render_still( let uniforms = ProjectUniforms::new( constants, - config, + &Arc::new(config.clone()), 0, 30, resolution_base, diff --git a/apps/desktop/src-tauri/src/export.rs b/apps/desktop/src-tauri/src/export.rs index ad1d2379871..6384881dc71 100644 --- a/apps/desktop/src-tauri/src/export.rs +++ b/apps/desktop/src-tauri/src/export.rs @@ -2038,7 +2038,7 @@ async fn generate_export_preview_inner( let uniforms = ProjectUniforms::new( &render_constants, - &project_config, + &Arc::new(project_config.clone()), frame_number, settings.fps, settings.resolution_base, @@ -2083,7 +2083,7 @@ async fn generate_export_preview_inner( outgoing_zoom.ensure_precomputed_until((frame_number as f32 + 1.0) / settings.fps as f32); let outgoing_uniforms = ProjectUniforms::new( &render_constants, - &project_config, + &Arc::new(project_config.clone()), frame_number, settings.fps, settings.resolution_base, @@ -2602,7 +2602,7 @@ async fn generate_export_preview_fast_inner( let uniforms = ProjectUniforms::new( &editor.render_constants, - &project_config, + &Arc::new(project_config.clone()), frame_number, settings.fps, settings.resolution_base, @@ -2649,7 +2649,7 @@ async fn generate_export_preview_fast_inner( outgoing_zoom.ensure_precomputed_until((frame_number as f32 + 1.0) / settings.fps as f32); let outgoing_uniforms = ProjectUniforms::new( &editor.render_constants, - &project_config, + &Arc::new(project_config.clone()), frame_number, settings.fps, settings.resolution_base, diff --git a/apps/desktop/src-tauri/src/screenshot_editor.rs b/apps/desktop/src-tauri/src/screenshot_editor.rs index 1138336b1e7..2992b142426 100644 --- a/apps/desktop/src-tauri/src/screenshot_editor.rs +++ b/apps/desktop/src-tauri/src/screenshot_editor.rs @@ -507,7 +507,7 @@ impl ScreenshotEditorInstances { let uniforms = ProjectUniforms::new( &constants, - ¤t_config, + &std::sync::Arc::new(current_config.clone()), 0, 30, cap_project::XY::new(base_w, base_h), @@ -1023,7 +1023,7 @@ pub async fn prewarm_screenshot_renderer() { zoom_timeline.ensure_precomputed_until(1.0 / 30.0); let uniforms = ProjectUniforms::new( &constants, - &config, + &std::sync::Arc::new(config.clone()), 0, 30, cap_project::XY::new(base_w, base_h), @@ -1825,7 +1825,7 @@ pub async fn render_screenshot_png(instance: &ScreenshotEditorInstance) -> Resul zoom_timeline.ensure_precomputed_until(1.0 / 30.0); let uniforms = ProjectUniforms::new( &constants, - &config, + &std::sync::Arc::new(config.clone()), 0, 30, resolution_base, diff --git a/crates/cap-test/src/suites/performance.rs b/crates/cap-test/src/suites/performance.rs index d7ac1a74cf4..615553bf2a6 100644 --- a/crates/cap-test/src/suites/performance.rs +++ b/crates/cap-test/src/suites/performance.rs @@ -148,7 +148,7 @@ fn skipped_result( } struct FixtureContext { - project: ProjectConfiguration, + project: Arc, render_constants: Arc, segments: Vec, } @@ -642,7 +642,7 @@ async fn load_fixture(recording_path: &Path) -> Result { } Ok(FixtureContext { - project, + project: Arc::new(project), render_constants, segments, }) diff --git a/crates/editor/examples/playback-pipeline-benchmark.rs b/crates/editor/examples/playback-pipeline-benchmark.rs index 8e772d10372..fd77e746970 100644 --- a/crates/editor/examples/playback-pipeline-benchmark.rs +++ b/crates/editor/examples/playback-pipeline-benchmark.rs @@ -423,6 +423,7 @@ async fn run_full_pipeline_benchmark( recordings: &ProjectRecordingsMeta, config: PipelineBenchmarkConfig, ) -> PipelineTimings { + let project = &Arc::new(project.clone()); let PipelineBenchmarkConfig { fps, frame_count, @@ -617,6 +618,7 @@ async fn run_scrubbing_benchmark( resolution_base: XY, force_ffmpeg: bool, ) -> PipelineTimings { + let project = &Arc::new(project.clone()); let mut timings = PipelineTimings::default(); let render_constants = match RenderVideoConstants::new( diff --git a/crates/editor/src/playback.rs b/crates/editor/src/playback.rs index 4a93c6e844e..692bda6a54b 100644 --- a/crates/editor/src/playback.rs +++ b/crates/editor/src/playback.rs @@ -1090,7 +1090,7 @@ impl Playback { .sort_by_key(|p| p.frame_number); let playback_start_frame = frame_number; - let mut cached_project = self.project.borrow().clone(); + let mut cached_project = Arc::new(self.project.borrow().clone()); let build_cursor_timelines = |project: &ProjectConfiguration| -> Vec> { @@ -1387,7 +1387,7 @@ impl Playback { drop(adopted.take()); preparing_audio_released.store(true, Ordering::Release); let project = self.project.borrow().clone(); - cached_project.audio = project.audio.clone(); + Arc::make_mut(&mut cached_project).audio = project.audio.clone(); let ticket = self.audio_output.prepare_playback(PlaySpec { segments: audio_segments.clone(), music: self.music.clone(), @@ -1460,7 +1460,7 @@ impl Playback { || clip_audio_changed(&cached_project, &next_project); let cursor_changed = cursor_timeline_settings(&cached_project.cursor) != cursor_timeline_settings(&next_project.cursor); - cached_project = next_project; + cached_project = Arc::new(next_project); if adopted.is_none() && audio_changed && let Some(generation) = audio_generation @@ -1469,7 +1469,7 @@ impl Playback { PlaySpec { segments: audio_segments.clone(), music: self.music.clone(), - project: cached_project.clone(), + project: (*cached_project).clone(), duration_secs: duration, start_playhead_secs: frame_number as f64 / fps_f64, playhead_rx: audio_playhead_rx.clone(), diff --git a/crates/editor/src/preparing_preview.rs b/crates/editor/src/preparing_preview.rs index 6a66454b3b4..f13bc342481 100644 --- a/crates/editor/src/preparing_preview.rs +++ b/crates/editor/src/preparing_preview.rs @@ -519,6 +519,7 @@ async fn run_preview( segments, cursor_assets, } = input; + let project = Arc::new(project); let metadata = recording_meta .studio_meta() .expect("Validated Studio metadata") diff --git a/crates/editor/src/preparing_preview/native_tests.rs b/crates/editor/src/preparing_preview/native_tests.rs index 578745b8df8..eb62cbb134e 100644 --- a/crates/editor/src/preparing_preview/native_tests.rs +++ b/crates/editor/src/preparing_preview/native_tests.rs @@ -382,7 +382,7 @@ async fn ordinary_composed_frames( let uniforms = match timeline { Some(timeline) => ProjectUniforms::new_with_precomputed_cursor( &constants, - &fixture.project, + &Arc::new(fixture.project.clone()), frame_number, 30, XY::new(320, 240), @@ -394,7 +394,7 @@ async fn ordinary_composed_frames( ), None => ProjectUniforms::new( &constants, - &fixture.project, + &Arc::new(fixture.project.clone()), frame_number, 30, XY::new(320, 240), diff --git a/crates/editor/src/thumbnail.rs b/crates/editor/src/thumbnail.rs index ac3534278a8..2c078355fec 100644 --- a/crates/editor/src/thumbnail.rs +++ b/crates/editor/src/thumbnail.rs @@ -1,7 +1,7 @@ use crate::{EditorInstance, editor::RendererTransitionInput}; use cap_project::{ProjectConfiguration, XY}; use cap_rendering::{ProjectUniforms, RenderedFrame, ZoomTransformTimeline}; -use std::{io::Write, path::Path}; +use std::{io::Write, path::Path, sync::Arc}; const THUMBNAIL_SIZE: XY = XY { x: 640, y: 640 }; const THUMBNAIL_FPS: u32 = 30; @@ -53,6 +53,7 @@ impl EditorInstance { let Some((project, signature)) = pending else { return Ok(None); }; + let project = Arc::new(project); let (source_time, segment) = project .get_segment_time(0.0) .ok_or("No first frame in the edited timeline")?; @@ -165,7 +166,6 @@ fn save_thumbnail( #[cfg(test)] mod tests { use super::*; - use std::sync::Arc; fn project() -> (tempfile::TempDir, Vec) { let root = tempfile::tempdir().unwrap(); diff --git a/crates/export/src/preview.rs b/crates/export/src/preview.rs index d406dce7deb..c092ec0fdf4 100644 --- a/crates/export/src/preview.rs +++ b/crates/export/src/preview.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; use base64::{Engine, engine::general_purpose::STANDARD}; use cap_editor::EditorInstance; @@ -220,6 +220,7 @@ async fn render_preview_frame( segments, total_duration, } = source; + let project_config = &Arc::new(project_config.clone()); let transition_mapping = project_config.timeline.as_ref().and_then(|timeline| { if timeline.transitions.is_empty() { return None; @@ -465,8 +466,6 @@ fn encode_preview_jpeg(frame: &RenderedFrame, quality: u8) -> Result, Ex #[cfg(test)] mod tests { - use std::sync::Arc; - use super::*; fn frame(width: u32, height: u32, stride: usize, mut state: u64) -> RenderedFrame { diff --git a/crates/export/tests/real_recording_regression.rs b/crates/export/tests/real_recording_regression.rs index 54050064193..b3267eb64e9 100644 --- a/crates/export/tests/real_recording_regression.rs +++ b/crates/export/tests/real_recording_regression.rs @@ -129,7 +129,7 @@ async fn export_sequence_render_matches_editor_reference_for_real_recording() return Err("Recording is not a studio recording".into()); }; - let project_config = recording_meta.project_config(); + let project_config = Arc::new(recording_meta.project_config()); let recordings = Arc::new( ProjectRecordingsMeta::new(&recording_meta.project_path, studio_meta) .map_err(std::io::Error::other)?, diff --git a/crates/rendering/examples/background-benchmark.rs b/crates/rendering/examples/background-benchmark.rs index c14db8d2251..785eb47f117 100644 --- a/crates/rendering/examples/background-benchmark.rs +++ b/crates/rendering/examples/background-benchmark.rs @@ -176,6 +176,7 @@ async fn main() -> Result<()> { }; project.background.source = case.source.clone(); project.background.padding = 20.0; + let project = Arc::new(project); let duration = (args.frames + args.warmup) as f64 / 60.0; let mut zoom = ZoomTransformTimeline::from_project(&project, &cursor, duration, screen_size); diff --git a/crates/rendering/src/layers/animated_gradient.rs b/crates/rendering/src/layers/animated_gradient.rs index 5890f99f90c..e44a0b2df43 100644 --- a/crates/rendering/src/layers/animated_gradient.rs +++ b/crates/rendering/src/layers/animated_gradient.rs @@ -398,7 +398,7 @@ mod tests { } fn test_uniforms(constants: &RenderVideoConstants, frame: u32) -> ProjectUniforms { - let project = ProjectConfiguration::default(); + let project = std::sync::Arc::new(ProjectConfiguration::default()); let cursor = CursorEvents::default(); let frames = DecodedSegmentFrames { screen_size: constants.options.screen_size, diff --git a/crates/rendering/src/lib.rs b/crates/rendering/src/lib.rs index dfe7030cf43..c526bb136fd 100644 --- a/crates/rendering/src/lib.rs +++ b/crates/rendering/src/lib.rs @@ -29,7 +29,7 @@ use std::sync::{ Arc, Mutex, atomic::{AtomicBool, Ordering}, }; -use std::{path::PathBuf, time::Instant}; +use std::{borrow::Cow, path::PathBuf, time::Instant}; use tokio::sync::mpsc; pub mod camera3d; @@ -770,6 +770,7 @@ pub async fn render_video_to_channel( ) -> Result<(), RenderingError> { ffmpeg::init().unwrap(); + let project = &Arc::new(project.clone()); let start_time = Instant::now(); let duration = get_duration(recordings, recording_meta, meta, project); @@ -1346,6 +1347,7 @@ pub async fn render_video_to_channel_nv12( stop_after_frames_sent: Option, startup_breakdown_ms: Option>>>, ) -> Result<(), RenderingError> { + let project = &Arc::new(project.clone()); let ffmpeg_init_start = Instant::now(); ffmpeg::init().unwrap(); let ffmpeg_init_ms = ffmpeg_init_start.elapsed().as_millis() as u64; @@ -2091,7 +2093,7 @@ impl InFlightFrame { struct TransitionExportContext<'a> { constants: &'a RenderVideoConstants, - project: &'a ProjectConfiguration, + project: &'a Arc, render_segments: &'a [RenderSegment], outgoing_zoom_timelines: &'a mut [ZoomTransformTimeline], precomputed_cursor_timelines: &'a [Arc], @@ -2898,7 +2900,7 @@ pub struct ProjectUniforms { /// their edges); the cursor and click-ripple layers scale by it. pub cursor_clip_visibility: f32, pub click_ripples: Vec, - pub project: ProjectConfiguration, + pub project: Arc, pub zoom: InterpolatedZoom, pub scene: InterpolatedScene, pub split: Option, @@ -3915,7 +3917,7 @@ impl ProjectUniforms { #[allow(clippy::too_many_arguments)] pub fn new( constants: &RenderVideoConstants, - project: &ProjectConfiguration, + project: &Arc, frame_number: u32, fps: u32, resolution_base: XY, @@ -3961,7 +3963,7 @@ impl ProjectUniforms { #[allow(clippy::too_many_arguments)] pub fn new_with_precomputed_cursor( constants: &RenderVideoConstants, - project: &ProjectConfiguration, + project: &Arc, frame_number: u32, fps: u32, resolution_base: XY, @@ -4024,7 +4026,7 @@ impl ProjectUniforms { #[allow(clippy::too_many_arguments)] fn new_inner( constants: &RenderVideoConstants, - project: &ProjectConfiguration, + shared_project: &Arc, frame_number: u32, fps: u32, resolution_base: XY, @@ -4034,6 +4036,7 @@ impl ProjectUniforms { zoom_timeline: &ZoomTransformTimeline, cursor_interp_fn: &dyn Fn(f32) -> Option, ) -> Self { + let project = shared_project.as_ref(); let options = &constants.options; let output_size = Self::get_output_size(options, project, resolution_base); let fps_f32 = fps.max(1) as f32; @@ -5108,7 +5111,10 @@ impl ProjectUniforms { frame_chrome, notch, display_outer_bounds, - project: styled_project.into_owned(), + project: match styled_project { + Cow::Borrowed(_) => Arc::clone(shared_project), + Cow::Owned(styled) => Arc::new(styled), + }, zoom, scene, split: split_layout, @@ -5907,7 +5913,15 @@ mod style_image_tests { let zoom = ZoomTransformTimeline::from_project(&project, &cursor, 6.0, size); let at = |project: &ProjectConfiguration, frame| { ProjectUniforms::new( - &constants, project, frame, 60, size, &cursor, &frames, 6.0, &zoom, + &constants, + &Arc::new(project.clone()), + frame, + 60, + size, + &cursor, + &frames, + 6.0, + &zoom, ) }; let uniforms = at(&project, 180); @@ -6133,7 +6147,7 @@ mod style_image_tests { for frame_number in [41, 42, 60, 83, 84] { let uniforms = ProjectUniforms::new( &constants, - &project, + &Arc::new(project.clone()), frame_number, 60, XY::new(160, 90), @@ -6206,7 +6220,7 @@ mod style_image_tests { ); let uniforms = ProjectUniforms::new( &constants, - &project, + &Arc::new(project.clone()), 60, 60, XY::new(160, 90), @@ -6233,7 +6247,7 @@ mod style_image_tests { let visibility_at = |frame_number: u32| { ProjectUniforms::new( &constants, - &project, + &Arc::new(project.clone()), frame_number, 60, XY::new(160, 90), @@ -6606,7 +6620,7 @@ mod nv12_flush_tests { for frame_number in 0..5u32 { let uniforms = ProjectUniforms::new( &constants, - &project, + &Arc::new(project.clone()), frame_number, 60, XY::new(4, 4), @@ -6723,7 +6737,7 @@ mod nv12_flush_tests { let zoom = ZoomTransformTimeline::from_project(&project, &cursor, 1.0, XY::new(4, 4)); let uniforms = ProjectUniforms::new( &constants, - &project, + &Arc::new(project.clone()), 7, 60, XY::new(4, 4), @@ -9285,7 +9299,7 @@ mod project_uniforms_tests { } let uniforms = ProjectUniforms::new( &constants, - &project, + &Arc::new(project.clone()), 0, 30, screen_size, @@ -9592,7 +9606,7 @@ mod project_uniforms_tests { ); let uniforms = ProjectUniforms::new( &constants, - &project, + &Arc::new(project.clone()), frame_number, 60, XY::new(800, 450), @@ -9621,7 +9635,7 @@ mod project_uniforms_tests { assert!((opacity - expected).abs() < 0.001); } let mut display_uniforms = uniforms.clone(); - display_uniforms.project.cursor.hide = true; + Arc::make_mut(&mut display_uniforms.project).cursor.hide = true; let display = renderer .render_immediate(frames.clone(), display_uniforms, &events, true, &mut layers) .await @@ -9633,7 +9647,7 @@ mod project_uniforms_tests { for ripple_only in [false, true] { let mut overlay_uniforms = uniforms.clone(); if ripple_only { - overlay_uniforms.project.cursor.hide = true; + Arc::make_mut(&mut overlay_uniforms.project).cursor.hide = true; overlay_uniforms.click_ripples = vec![ClickRipple { position: Coord::new(position), progress: 0.35, @@ -9657,7 +9671,7 @@ mod project_uniforms_tests { cursor_only_frames.camera_frame = None; let mut cursor_only_uniforms = ProjectUniforms::new( &constants, - &project, + &Arc::new(project.clone()), frame_number, 60, XY::new(800, 450), @@ -9666,12 +9680,14 @@ mod project_uniforms_tests { 4.0, &zoom_timeline, ); - cursor_only_uniforms.project.background.source = BackgroundSource::Color { + Arc::make_mut(&mut cursor_only_uniforms.project) + .background + .source = BackgroundSource::Color { value: [0, 0, 0], alpha: 0, }; if ripple_only { - cursor_only_uniforms.project.cursor.hide = true; + Arc::make_mut(&mut cursor_only_uniforms.project).cursor.hide = true; cursor_only_uniforms.click_ripples = vec![ClickRipple { position: Coord::new(position), progress: 0.35, @@ -9714,7 +9730,7 @@ mod project_uniforms_tests { ); report_pixel_changes("overlay vs original", &overlay, &display, marker_center); let mut hidden_uniforms = uniforms.clone(); - hidden_uniforms.project.cursor.hide = true; + Arc::make_mut(&mut hidden_uniforms.project).cursor.hide = true; let hidden_repeat = renderer .render_immediate( frames.clone(), @@ -9844,6 +9860,122 @@ mod project_uniforms_tests { assert!(delta < 0.2, "focus moved too far ahead: {delta}"); assert!(delta > -0.25, "focus lagged too far behind: {delta}"); } + + fn uniforms_at( + constants: &RenderVideoConstants, + project: &Arc, + frame_number: u32, + ) -> ProjectUniforms { + let screen_size = constants.options.screen_size; + let events = CursorEvents::default(); + let mut zoom_timeline = + ZoomTransformTimeline::from_project(project, &events, 60.0, screen_size); + zoom_timeline.ensure_precomputed_until(60.0); + let frames = DecodedSegmentFrames { + screen_size, + screen_frame: Some(DecodedFrame::new(Vec::new(), screen_size.x, screen_size.y)), + camera_frame: None, + segment_time: frame_number as f32 / 30.0, + recording_time: frame_number as f32 / 30.0, + segment_has_camera: false, + }; + ProjectUniforms::new( + constants, + project, + frame_number, + 30, + screen_size, + &events, + &frames, + 60.0, + &zoom_timeline, + ) + } + + fn captioned_project(segments: u32) -> ProjectConfiguration { + let captions: Vec<_> = (0..segments) + .map(|i| { + let start = f64::from(i) * 3.0; + serde_json::json!({ + "id": format!("caption-{i}"), + "start": start, + "end": start + 2.5, + "text": "the quick brown fox jumps over the lazy dog", + "words": (0..8).map(|w| serde_json::json!({ + "text": format!("word{w}"), + "start": start + f64::from(w) * 0.3, + "end": start + f64::from(w) * 0.3 + 0.25, + })).collect::>(), + }) + }) + .collect(); + serde_json::from_value(serde_json::json!({ + "timeline": { + "segments": [{"recordingClip": 0, "start": 0.0, "end": f64::from(segments) * 3.0, "timescale": 1.0}], + "zoomSegments": [], + "captionSegments": captions, + }, + "captions": {"segments": captions, "settings": {}}, + })) + .unwrap() + } + + #[tokio::test] + async fn uniforms_share_the_project_unless_a_style_segment_is_active() { + let constants = cursor_test_constants().await; + let mut project = captioned_project(10); + project.timeline.as_mut().unwrap().style_segments = serde_json::from_value( + serde_json::json!([{"start": 1.0, "end": 2.0, "overrides": {"background": {"padding": 30.0}}}]), + ) + .unwrap(); + let project = Arc::new(project); + + let before = uniforms_at(&constants, &project, 15); + let styled = uniforms_at(&constants, &project, 45); + let after = uniforms_at(&constants, &project, 75); + + assert!(Arc::ptr_eq(&before.project, &project)); + assert!(Arc::ptr_eq(&after.project, &project)); + assert!(!Arc::ptr_eq(&styled.project, &project)); + assert_eq!(styled.project.background.padding, 30.0); + assert_eq!( + styled.project.captions.as_ref().unwrap().segments.len(), + project.captions.as_ref().unwrap().segments.len() + ); + } + + #[tokio::test] + #[ignore] + async fn benchmark_project_uniforms() { + use std::hint::black_box; + + let constants = cursor_test_constants().await; + for (transcript, segments) in [ + ("none", 0), + ("10 min", 200), + ("30 min", 600), + ("60 min", 1200), + ] { + let project = Arc::new(captioned_project(segments)); + let mut runs: Vec = (0..400) + .map(|frame| { + let start = Instant::now(); + black_box(uniforms_at(&constants, &project, frame % 1800)); + start.elapsed().as_secs_f64() * 1_000_000.0 + }) + .collect(); + runs.sort_by(f64::total_cmp); + println!( + "{}", + serde_json::json!({ + "transcript": transcript, + "caption_segments": segments, + "median_us": runs[runs.len() / 2], + "p95_us": runs[runs.len() * 95 / 100], + }) + ); + } + } } #[cfg(test)] diff --git a/crates/rendering/src/overlay_layers/native_tests.rs b/crates/rendering/src/overlay_layers/native_tests.rs index db1695c6b31..f137c0ae24b 100644 --- a/crates/rendering/src/overlay_layers/native_tests.rs +++ b/crates/rendering/src/overlay_layers/native_tests.rs @@ -88,7 +88,7 @@ fn uniforms( ZoomTransformTimeline::from_project(project, &cursor, 3.0, constants.options.screen_size); ProjectUniforms::new( constants, - project, + &std::sync::Arc::new(project.clone()), frame, 30, XY::new(320, 240), @@ -289,7 +289,7 @@ async fn restricted_late_uniforms_fail_before_prepare_or_direct_render_work() { let late_project = uniforms(&constants, &with_overlay("image", "must-not-open.png"), 30); let mut late_text = uniforms(&constants, &with_overlay("text", ""), 30); assert!(!late_text.texts.is_empty()); - late_text.project = project; + late_text.project = std::sync::Arc::new(project); let mut session = RenderSession::new(&constants.device, clean.output_size.0, clean.output_size.1); let mut encoder = constants.device.create_command_encoder(&Default::default()); From d24584291a25a6ba79d33c9cd5941940d20c9f6f Mon Sep 17 00:00:00 2001 From: Aditya Garud <153842990+yashranaway@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:24:08 +0000 Subject: [PATCH 2/3] perf(rendering): share one Arc per Tauri preview and time only the constructor The Tauri export previews and screenshot editor wrapped a fresh clone of the project for every set of uniforms. They now move their owned config into one Arc. benchmark_project_uniforms builds its zoom timeline and frame inputs outside the timed region and times ProjectUniforms::new alone. --- apps/desktop/src-tauri/src/export.rs | 10 ++++--- .../src-tauri/src/screenshot_editor.rs | 14 ++++----- crates/rendering/src/lib.rs | 29 +++++++++++++++++-- 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/apps/desktop/src-tauri/src/export.rs b/apps/desktop/src-tauri/src/export.rs index 6384881dc71..662fe679128 100644 --- a/apps/desktop/src-tauri/src/export.rs +++ b/apps/desktop/src-tauri/src/export.rs @@ -1950,6 +1950,7 @@ async fn generate_export_preview_inner( .map(|segment| segment.display.duration) .collect::>(), ); + let project_config = Arc::new(project_config); let render_constants = Arc::new( RenderVideoConstants::new( @@ -2038,7 +2039,7 @@ async fn generate_export_preview_inner( let uniforms = ProjectUniforms::new( &render_constants, - &Arc::new(project_config.clone()), + &project_config, frame_number, settings.fps, settings.resolution_base, @@ -2083,7 +2084,7 @@ async fn generate_export_preview_inner( outgoing_zoom.ensure_precomputed_until((frame_number as f32 + 1.0) / settings.fps as f32); let outgoing_uniforms = ProjectUniforms::new( &render_constants, - &Arc::new(project_config.clone()), + &project_config, frame_number, settings.fps, settings.resolution_base, @@ -2546,6 +2547,7 @@ async fn generate_export_preview_fast_inner( }) .await .map_err(|error| format!("Failed to synchronize export preview timing: {error}"))?; + let project_config = Arc::new(project_config); let transition_mapping = project_config.timeline.as_ref().and_then(|timeline| { if timeline.transitions.is_empty() { return None; @@ -2602,7 +2604,7 @@ async fn generate_export_preview_fast_inner( let uniforms = ProjectUniforms::new( &editor.render_constants, - &Arc::new(project_config.clone()), + &project_config, frame_number, settings.fps, settings.resolution_base, @@ -2649,7 +2651,7 @@ async fn generate_export_preview_fast_inner( outgoing_zoom.ensure_precomputed_until((frame_number as f32 + 1.0) / settings.fps as f32); let outgoing_uniforms = ProjectUniforms::new( &editor.render_constants, - &Arc::new(project_config.clone()), + &project_config, frame_number, settings.fps, settings.resolution_base, diff --git a/apps/desktop/src-tauri/src/screenshot_editor.rs b/apps/desktop/src-tauri/src/screenshot_editor.rs index 2992b142426..0d903fdc4ca 100644 --- a/apps/desktop/src-tauri/src/screenshot_editor.rs +++ b/apps/desktop/src-tauri/src/screenshot_editor.rs @@ -469,7 +469,7 @@ impl ScreenshotEditorInstances { ); let shutdown_token = render_shutdown_token; let mut current_update = config_rx.borrow().clone(); - let mut current_config = current_update.config.clone(); + let mut current_config = std::sync::Arc::new(current_update.config.clone()); let mut current_revision = current_update.revision; let mut first_frame_logged = false; @@ -507,7 +507,7 @@ impl ScreenshotEditorInstances { let uniforms = ProjectUniforms::new( &constants, - &std::sync::Arc::new(current_config.clone()), + ¤t_config, 0, 30, cap_project::XY::new(base_w, base_h), @@ -564,7 +564,7 @@ impl ScreenshotEditorInstances { } current_update = config_rx.borrow().clone(); current_revision = current_update.revision; - current_config = current_update.config.clone(); + current_config = std::sync::Arc::new(current_update.config.clone()); } _ = shutdown_token.cancelled() => { break; @@ -989,7 +989,7 @@ pub async fn prewarm_screenshot_renderer() { Arc::new(cap_rendering::BackgroundTextureCache::default()), ); - let config = ProjectConfiguration::default(); + let config = std::sync::Arc::new(ProjectConfiguration::default()); let mut frame_renderer = FrameRenderer::new(&constants); let mut layers = RendererLayers::new_with_options( &constants.device, @@ -1023,7 +1023,7 @@ pub async fn prewarm_screenshot_renderer() { zoom_timeline.ensure_precomputed_until(1.0 / 30.0); let uniforms = ProjectUniforms::new( &constants, - &std::sync::Arc::new(config.clone()), + &config, 0, 30, cap_project::XY::new(base_w, base_h), @@ -1637,7 +1637,7 @@ pub async fn render_screenshot_project_for_export( pub async fn render_screenshot_png(instance: &ScreenshotEditorInstance) -> Result, String> { let path = instance.path.clone(); - let config = instance.config_tx.borrow().config.clone(); + let config = std::sync::Arc::new(instance.config_tx.borrow().config.clone()); let width = instance.image_width; let height = instance.image_height; @@ -1825,7 +1825,7 @@ pub async fn render_screenshot_png(instance: &ScreenshotEditorInstance) -> Resul zoom_timeline.ensure_precomputed_until(1.0 / 30.0); let uniforms = ProjectUniforms::new( &constants, - &std::sync::Arc::new(config.clone()), + &config, 0, 30, resolution_base, diff --git a/crates/rendering/src/lib.rs b/crates/rendering/src/lib.rs index c526bb136fd..583eba9ccc7 100644 --- a/crates/rendering/src/lib.rs +++ b/crates/rendering/src/lib.rs @@ -9950,6 +9950,16 @@ mod project_uniforms_tests { use std::hint::black_box; let constants = cursor_test_constants().await; + let screen_size = constants.options.screen_size; + let events = CursorEvents::default(); + let frames = DecodedSegmentFrames { + screen_size, + screen_frame: Some(DecodedFrame::new(Vec::new(), screen_size.x, screen_size.y)), + camera_frame: None, + segment_time: 0.0, + recording_time: 0.0, + segment_has_camera: false, + }; for (transcript, segments) in [ ("none", 0), ("10 min", 200), @@ -9957,11 +9967,26 @@ mod project_uniforms_tests { ("60 min", 1200), ] { let project = Arc::new(captioned_project(segments)); + let mut zoom_timeline = + ZoomTransformTimeline::from_project(&project, &events, 60.0, screen_size); + zoom_timeline.ensure_precomputed_until(60.0); let mut runs: Vec = (0..400) .map(|frame| { let start = Instant::now(); - black_box(uniforms_at(&constants, &project, frame % 1800)); - start.elapsed().as_secs_f64() * 1_000_000.0 + let uniforms = ProjectUniforms::new( + &constants, + &project, + frame % 1800, + 30, + screen_size, + &events, + &frames, + 60.0, + &zoom_timeline, + ); + let elapsed = start.elapsed().as_secs_f64() * 1_000_000.0; + black_box(uniforms); + elapsed }) .collect(); runs.sort_by(f64::total_cmp); From e442f888a14cc717369bddcac441532bc8be6205 Mon Sep 17 00:00:00 2001 From: Aditya Garud <153842990+yashranaway@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:46:23 +0000 Subject: [PATCH 3/3] perf(rendering): pass the shared project to the new uniform call sites --- crates/rendering/examples/camera-texture-benchmark.rs | 1 + crates/rendering/src/layers/camera.rs | 2 +- crates/rendering/src/lib.rs | 3 ++- crates/rendering/tests/background_resize.rs | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/rendering/examples/camera-texture-benchmark.rs b/crates/rendering/examples/camera-texture-benchmark.rs index 629d1b2cfc5..47cf76524e5 100644 --- a/crates/rendering/examples/camera-texture-benchmark.rs +++ b/crates/rendering/examples/camera-texture-benchmark.rs @@ -138,6 +138,7 @@ async fn main() -> Result<()> { }))?); } let zoom = ZoomTransformTimeline::from_project(&project, &cursor, 6.0, size); + let project = std::sync::Arc::new(project); let mut renderer = FrameRenderer::new(&constants); let mut frame_ms = Vec::new(); let mut frame_memory = Vec::new(); diff --git a/crates/rendering/src/layers/camera.rs b/crates/rendering/src/layers/camera.rs index d8ef387e5fe..073b533d1b5 100644 --- a/crates/rendering/src/layers/camera.rs +++ b/crates/rendering/src/layers/camera.rs @@ -766,7 +766,7 @@ mod tests { let zoom = ZoomTransformTimeline::from_project(project, &cursor, 6.0, size); let uniforms = ProjectUniforms::new( constants, - project, + &std::sync::Arc::new(project.clone()), (frames.segment_time * 60.0).round() as u32, 60, size, diff --git a/crates/rendering/src/lib.rs b/crates/rendering/src/lib.rs index 583eba9ccc7..9d176947d6f 100644 --- a/crates/rendering/src/lib.rs +++ b/crates/rendering/src/lib.rs @@ -9109,6 +9109,7 @@ mod project_uniforms_tests { Some(project.cursor.click_spring_config()), ); let zoom = ZoomTransformTimeline::from_project(&project, &events, 2.0, size); + let project = Arc::new(project); let mut legacy_renderer = FrameRenderer::new(&constants); let mut indexed_renderer = FrameRenderer::new(&constants); let mut hidden_renderer = FrameRenderer::new(&constants); @@ -9172,7 +9173,7 @@ mod project_uniforms_tests { 2.0, &zoom, ); - hidden.project.cursor.hide = true; + Arc::make_mut(&mut hidden.project).cursor.hide = true; let hidden = hidden_renderer .render_immediate(frames, hidden, &events, true, &mut hidden_layers) .await diff --git a/crates/rendering/tests/background_resize.rs b/crates/rendering/tests/background_resize.rs index e52e297b136..28996302fb3 100644 --- a/crates/rendering/tests/background_resize.rs +++ b/crates/rendering/tests/background_resize.rs @@ -75,7 +75,7 @@ async fn image_background_resize_matches_fresh_layers() { let zoom = ZoomTransformTimeline::from_project(&project, &cursor, 1.0, screen_size); let uniforms = ProjectUniforms::new( &constants, - &project, + &std::sync::Arc::new(project.clone()), 0, 30, XY::new(128, 128),