Skip to content

Commit d7ca65f

Browse files
committed
perf(studio): preview scale selector and prefetch worker pool
Heavy scenarios (glass, camera, parallax) cost ~150ms CPU per frame at full resolution — 4.5x the 33ms budget of 30fps playback — and the single prefetch thread rendered at scale 1.0, so the cache drained and playback stalled while the exported MP4 (parallel, offline) was smooth. - render preview frames at a UI-selected scale (100/75/50/25%, default 50%); the scale is part of the frame cache key and a scale switch purges stale-scale entries on first insert - replace the single prefetch thread with a worker pool (cores-2, clamped to [2,6]); workers spread over the window through a shared claim set so no frame is rendered twice concurrently - the transport bar gains a preview-quality select; export still renders at 100%
1 parent 11bd69e commit d7ca65f

4 files changed

Lines changed: 269 additions & 41 deletions

File tree

crates/rustmotion-studio/src/editor/frames.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,15 @@ mod tests {
185185
assert_eq!(&jpeg[0..2], &[0xFF, 0xD8], "must be a JPEG");
186186
}
187187

188+
#[test]
189+
fn renders_frame_at_reduced_scale() {
190+
let scenario = rustmotion::loader::load_scenario_from_source(None, Some(SCENARIO)).unwrap();
191+
let tasks = rustmotion::encode::build_frame_tasks(&scenario);
192+
let jpeg = render_frame(&scenario, &tasks, 0, 0.5);
193+
let img = image::load_from_memory(&jpeg).expect("decodable JPEG");
194+
assert_eq!((img.width(), img.height()), (640, 360), "half of 1280x720");
195+
}
196+
188197
#[test]
189198
fn frame_hits_are_in_percent_and_have_kind() {
190199
let json = r##"{ "video": { "width": 800, "height": 600, "background": "#101418" }, "scenes": [ { "duration": 1.0, "children": [ { "type": "text", "content": "Hi", "style": { "font-size": 40 } } ] } ] }"##;

crates/rustmotion-studio/src/editor/playback.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ use dioxus::prelude::*;
44
use dioxus_icons::lucide::{Pause, Play};
55

66
use crate::components::button::{Button, ButtonSize, ButtonVariant};
7+
use crate::components::select::{Select, SelectOption};
78
use crate::scenario::Shared;
89

10+
use super::prefetch::{set_preview_scale_pct, PREVIEW_SCALE_CHOICES};
11+
912
/// What a playback keyboard shortcut does (see [`playback_action`]).
1013
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1114
pub enum PlaybackAction {
@@ -93,6 +96,7 @@ pub fn PlaybackBar(
9396
total: u32,
9497
diff_active: Signal<bool>,
9598
mut diff_side: Signal<super::diff_panel::DiffSide>,
99+
mut preview_scale: Signal<u16>,
96100
) -> Element {
97101
use super::diff_panel::DiffSide;
98102

@@ -162,6 +166,34 @@ pub fn PlaybackBar(
162166
}
163167
},
164168
}
169+
// Preview quality: render scale of the preview frames only (the
170+
// export always renders at 100%). Lower = smoother playback on
171+
// heavy scenarios (glass, camera, parallax).
172+
div {
173+
title: "Preview quality (export is always 100%)",
174+
style: "width:96px; flex:none;",
175+
Select::<u16> {
176+
default_value: Some(preview_scale()),
177+
on_value_change: move |v: Option<u16>| {
178+
if let Some(pct) = v {
179+
// Atomic first: the render threads and the asset
180+
// handler must see the new scale before the signal
181+
// change triggers the <img> refetch.
182+
set_preview_scale_pct(pct);
183+
preview_scale.set(pct);
184+
}
185+
},
186+
for (i, pct) in PREVIEW_SCALE_CHOICES.iter().enumerate() {
187+
SelectOption::<u16> {
188+
key: "{pct}",
189+
index: i,
190+
value: *pct,
191+
text_value: "{pct}%",
192+
"{pct}%"
193+
}
194+
}
195+
}
196+
}
165197
div { style: "min-width:120px; text-align:right; color:var(--rm-text-muted);",
166198
"{cur} / {max}"
167199
}

0 commit comments

Comments
 (0)