From 75fae5535f2c4033fd52c1ac5843b9ee6d31e7b8 Mon Sep 17 00:00:00 2001 From: grovebit <337789029+grovebit@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:31:02 +0100 Subject: [PATCH 1/2] Share the session with the UI instead of copying it every frame The Edit window copied the whole session once per frame for the rulers and twice for every visible track (track_row and lane_interaction), plus once more for each track shown as playlists and once for the Timecode 2 ruler. The MIDI editor copied it once per frame, and every Edit row and Mix strip copied its track. A copy includes every plugin's parameters and saved state: in a 23-track session with three plugins per track (200 parameters and 64 KB of state each), one copy took 0.2 ms and each Edit window frame 15.6 ms, which is paid on every frame of playback, dragging and scrolling. These now hold the engine's Arc (session_arc). It is still a snapshot: a command run later in the frame copies on write, as it already does to keep its undo step. timebase_ruler takes the frame rate as a parameter, so the Timecode 2 ruler no longer needs a copy with another frame rate. Median frame times in a release build, with 21 of 23 tracks on screen: - The demo session with its tracks tripled: Edit window from 1.25 to 0.68 ms. - Plus 5,000 volume automation points per audio track: from 2.03 to 0.68 ms. - Plus three plugins per track: Edit window from 15.6 to 0.69 ms, Mix window from 1.14 to 0.71 ms. The Edit window (every ruler, a playlist lane and an automation lane), the MIDI editor and the Mix window render byte-identical images before and after. --- crates/ui-egui/src/edit_window.rs | 54 ++++++++++++++++++------------- crates/ui-egui/src/midi_editor.rs | 2 +- crates/ui-egui/src/mix_window.rs | 13 ++++---- 3 files changed, 39 insertions(+), 30 deletions(-) diff --git a/crates/ui-egui/src/edit_window.rs b/crates/ui-egui/src/edit_window.rs index f13bc78a..42e72f22 100644 --- a/crates/ui-egui/src/edit_window.rs +++ b/crates/ui-egui/src/edit_window.rs @@ -417,7 +417,7 @@ fn nice_step(spp: f64, sr: f64, min_px: f64) -> i64 { fn draw_rulers(app: &mut SoundApp, ui: &mut Ui, area: Rect, tl: Rect, rulers: &[String]) { let t = Tokens::current(); - let s = app.engine.session().clone(); + let s = app.engine.session_arc(); ui.painter().rect_filled(area, 0.0, t.ruler_bg); let view = Range::new(sample_at(&s, tl, tl.min.x), sample_at(&s, tl, tl.max.x)); let sr = s.sample_rate; @@ -502,9 +502,7 @@ fn draw_rulers(app: &mut SoundApp, ui: &mut Ui, area: Rect, tl: Rect, rulers: &[ } else { soundcraft_time::FrameRate::Fps25 }; - let mut s2 = s.clone(); - s2.frame_rate = alt; - timebase_ruler(ui, &painter, &s2, tl, row, view, TimeFormat::Timecode, i + 1 == rulers.len()); + timebase_ruler(ui, &painter, &s, tl, row, view, TimeFormat::Timecode, alt, i + 1 == rulers.len()); } "markers" | "markers2" | "markers3" | "markers4" | "markers5" | "chords" => { let lane: u8 = match id.as_str() { @@ -554,7 +552,7 @@ fn draw_rulers(app: &mut SoundApp, ui: &mut Ui, area: Rect, tl: Rect, rulers: &[ } _ => { if let Some(f) = fmt { - timebase_ruler(ui, &painter, &s, tl, row, view, f, i + 1 == rulers.len()); + timebase_ruler(ui, &painter, &s, tl, row, view, f, s.frame_rate, i + 1 == rulers.len()); // Clicking a timebase ruler locates. let resp = ui.interact(row, ui.id().with(("ruler", id.as_str())), Sense::click_and_drag()); if (resp.clicked() || resp.dragged()) @@ -576,7 +574,17 @@ fn draw_rulers(app: &mut SoundApp, ui: &mut Ui, area: Rect, tl: Rect, rulers: &[ ui.painter().line_segment([pos2(area.min.x, area.max.y - 1.0), pos2(area.max.x, area.max.y - 1.0)], Stroke::new(1.0, t.border)); } -fn timebase_ruler(_ui: &Ui, painter: &egui::Painter, s: &Session, tl: Rect, row: Rect, view: Range, f: TimeFormat, bottom: bool) { +fn timebase_ruler( + _ui: &Ui, + painter: &egui::Painter, + s: &Session, + tl: Rect, + row: Rect, + view: Range, + f: TimeFormat, + rate: soundcraft_time::FrameRate, + bottom: bool, +) { let t = Tokens::current(); let sr = s.sample_rate; let spp = s.edit.zoom.samples_per_px; @@ -586,7 +594,7 @@ fn timebase_ruler(_ui: &Ui, painter: &egui::Painter, s: &Session, tl: Rect, row: // Bars every n so labels are ≥ 60 px apart. let bar_px = { let b = GridValue::Note { value: NoteValue::Bar, dotted: false, triplet: false }; - b.step_samples(view.start.max(0), sr, &s.tempo, s.frame_rate) as f64 / spp + b.step_samples(view.start.max(0), sr, &s.tempo, rate) as f64 / spp }; let every = [1i64, 2, 4, 8, 16, 32, 64, 128].into_iter().find(|n| bar_px * *n as f64 >= 60.0).unwrap_or(256); let first = s.tempo.bar_beat_at(view.start.max(0), sr).bar; @@ -658,10 +666,10 @@ fn timebase_ruler(_ui: &Ui, painter: &egui::Painter, s: &Session, tl: Rect, row: if secs.fract().abs() < 1e-9 { format!("{}:{:02}", (secs / 60.0) as i64, (secs as i64) % 60) } else { - format_position(at, f, sr, &s.tempo, s.frame_rate, s.timecode_start) + format_position(at, f, sr, &s.tempo, rate, s.timecode_start) } } - _ => format_position(at, f, sr, &s.tempo, s.frame_rate, s.timecode_start), + _ => format_position(at, f, sr, &s.tempo, rate, s.timecode_start), }; painter.text(pos2(x + 3.0, row.center().y), Align2::LEFT_CENTER, label, regular(11.0), t.ruler_text); } else { @@ -677,26 +685,26 @@ fn timebase_ruler(_ui: &Ui, painter: &egui::Painter, s: &Session, tl: Rect, row: fn track_row(app: &mut SoundApp, ui: &mut Ui, id: TrackId, row: Rect, tl: Rect) { let t = Tokens::current(); - let s = app.engine.session(); - let Some(track) = s.track(id).cloned() else { return }; + let s = app.engine.session_arc(); + let Some(track) = s.track(id) else { return }; let selected = s.edit.selected_tracks.contains(&id); let head = Rect::from_min_max(row.min, pos2(row.min.x + HEADER_W, row.max.y)); let cols = edit_columns(app.engine.session()); for (i, c) in cols.iter().enumerate() { let cr = Rect::from_min_size(pos2(head.max.x + i as f32 * COLUMN_W, row.min.y), vec2(COLUMN_W, row.height())); - header_column(app, ui, &track, c, cr); + header_column(app, ui, track, c, cr); } let main_h = track.height.points(); - let lane = lane_rect(&track, row, tl); + let lane = lane_rect(track, row, tl); // Header background. ui.painter().rect_filled(head, 0.0, if selected { t.header_selected } else { t.panel_bg2 }); ui.painter().rect_filled(Rect::from_min_size(head.min, vec2(5.0, head.height())), 0.0, rgb(track.color)); ui.painter().line_segment([pos2(head.min.x, row.max.y + 0.5), pos2(tl.max.x, row.max.y + 0.5)], Stroke::new(1.0, t.border)); - track_header(app, ui, &track, head, selected); + track_header(app, ui, track, head, selected); // Lane. let painter = ui.painter().with_clip_rect(lane.intersect(ui.clip_rect())); painter.rect_filled(lane, 0.0, if selected { t.playlist_alt } else { t.playlist_bg }); - let s = app.engine.session().clone(); + let s = app.engine.session_arc(); grid_lines(&painter, &s, tl, lane); let view = Range::new(sample_at(&s, tl, tl.min.x), sample_at(&s, tl, tl.max.x)); let auto_view = AutoParam::parse(&track.view); @@ -704,15 +712,15 @@ fn track_row(app: &mut SoundApp, ui: &mut Ui, id: TrackId, row: Rect, tl: Rect) if !clip.range().overlaps(&view) { continue; } - draw_clip(app, &painter, &s, &track, clip, tl, lane, auto_view.is_some()); + draw_clip(app, &painter, &s, track, clip, tl, lane, auto_view.is_some()); } - crate::video_track::draw_lane(app, &painter, &s, &track, tl, lane); + crate::video_track::draw_lane(app, &painter, &s, track, tl, lane); if let Some(p) = &auto_view { - draw_automation(&painter, &s, &track, p, tl, lane); + draw_automation(&painter, &s, track, p, tl, lane); } if track.kind == TrackKind::Master || track.kind == TrackKind::Aux || track.kind == TrackKind::Vca { let p = auto_view.clone().unwrap_or(AutoParam::Volume); - draw_automation(&painter, &s, &track, &p, tl, lane); + draw_automation(&painter, &s, track, &p, tl, lane); } // Selection highlight on this track. let sel = s.edit.selection; @@ -723,16 +731,16 @@ fn track_row(app: &mut SoundApp, ui: &mut Ui, id: TrackId, row: Rect, tl: Rect) painter.rect_filled(Rect::from_min_max(pos2(x0, lane.min.y), pos2(x1, lane.max.y)), 0.0, t.selection); } } - lane_interaction(app, ui, &track, lane, tl); + lane_interaction(app, ui, track, lane, tl); if track.view == "playlists" { - playlist_lanes(app, ui, &track, row, main_h, tl); + playlist_lanes(app, ui, track, row, main_h, tl); } } /// Alternate playlists as lanes under the main one; clicking a clip promotes it (comping). fn playlist_lanes(app: &mut SoundApp, ui: &mut Ui, track: &Track, row: Rect, main_h: f32, tl: Rect) { let t = Tokens::current(); - let s = app.engine.session().clone(); + let s = app.engine.session_arc(); let mut y = row.min.y + main_h; for (i, pl) in track.playlists.iter().enumerate() { if i == track.active_playlist { @@ -1486,7 +1494,7 @@ fn lane_interaction(app: &mut SoundApp, ui: &mut Ui, track: &Track, lane: Rect, if resp.dnd_hover_payload::().is_some() { ui.painter().rect_stroke(lane, 0.0, Stroke::new(2.0, Tokens::current().accent), StrokeKind::Inside); } - let s = app.engine.session().clone(); + let s = app.engine.session_arc(); let tool = s.edit.tool; let mods = ui.input(|i| i.modifiers); let Some(p) = resp.interact_pointer_pos().or_else(|| resp.hover_pos()) else { return }; diff --git a/crates/ui-egui/src/midi_editor.rs b/crates/ui-egui/src/midi_editor.rs index e69d6255..e229d397 100644 --- a/crates/ui-egui/src/midi_editor.rs +++ b/crates/ui-egui/src/midi_editor.rs @@ -105,7 +105,7 @@ pub fn show(app: &mut SoundApp, ui: &mut Ui) { p["clips"] = json!([cid.0]); let _ = app.run(id, p); } - let s = app.engine.session().clone(); + let s = app.engine.session_arc(); let sr = s.sample_rate; let base_tick = s.tempo.samples_to_ticks(clip.start, sr); let len_ticks = (s.tempo.samples_to_ticks(clip.end(), sr) - base_tick).max(1); diff --git a/crates/ui-egui/src/mix_window.rs b/crates/ui-egui/src/mix_window.rs index 314267a8..1ff28512 100644 --- a/crates/ui-egui/src/mix_window.rs +++ b/crates/ui-egui/src/mix_window.rs @@ -90,7 +90,8 @@ fn route_name(app: &SoundApp, r: &Route) -> String { fn strip(app: &mut SoundApp, ui: &mut Ui, id: TrackId, snap: Option<&MeterSnapshot>) { let t = Tokens::current(); - let Some(track) = app.engine.session().track(id).cloned() else { return }; + let session = app.engine.session_arc(); + let Some(track) = session.track(id) else { return }; let narrow = app.ui.narrow_mix; let w = strip_width(narrow); let h = ui.available_height().max(560.0); @@ -132,8 +133,8 @@ fn strip(app: &mut SoundApp, ui: &mut Ui, id: TrackId, snap: Option<&MeterSnapsh let sr = Rect::from_min_size(pos2(x0 + 2.0, sec.min.y + 16.0), vec2(inner_w - 4.0, 15.0)); let slot = track.mixer.inserts.iter().position(|i| i.as_ref().is_some_and(|i| plugin_info(&i.plugin).is_some_and(|p| p.is_instrument))); match slot { - _ if track.kind == TrackKind::Instrument => instrument_slot(app, ui, &track, sr), - Some(k) => insert_slot(app, ui, &track, k, sr), + _ if track.kind == TrackKind::Instrument => instrument_slot(app, ui, track, sr), + Some(k) => insert_slot(app, ui, track, k, sr), None if track.kind == TrackKind::Midi => { ui.painter().text(sr.center(), Align2::CENTER_CENTER, "MIDI in: all", regular(9.0), t.text_dim); } @@ -168,9 +169,9 @@ fn strip(app: &mut SoundApp, ui: &mut Ui, id: TrackId, snap: Option<&MeterSnapsh for k in 0..5 { let sr = Rect::from_min_size(pos2(x0 + 2.0, sec.min.y + 15.0 + k as f32 * 17.0), vec2(inner_w - 4.0, 15.0)); if is_send { - send_slot(app, ui, &track, off + k, sr); + send_slot(app, ui, track, off + k, sr); } else { - insert_slot(app, ui, &track, off + k, sr); + insert_slot(app, ui, track, off + k, sr); } } y = sec.max.y + 4.0; @@ -264,7 +265,7 @@ fn strip(app: &mut SoundApp, ui: &mut Ui, id: TrackId, snap: Option<&MeterSnapsh let _ = selector_box(&mut c, gr.width(), gr.height(), &gname, t.text_dim); y += 22.0; // Pan: a surround panner when the output is multichannel, else the stereo knobs. - let out_fmt = output_format(app, &track); + let out_fmt = output_format(app, track); let surround_out = out_fmt.channels() > 2 && !out_fmt.is_ambisonic() && !matches!(track.kind, TrackKind::Master | TrackKind::Vca) && track.channels() <= 2; if surround_out { From e927e295bb034cea174ad9f9516fe35b4eaedec5 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 13 Oct 2026 05:24:58 -0400 Subject: [PATCH 2/2] Fix build after merging main into #360 Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/edit_window.rs | 4 ++-- crates/ui-egui/src/mix_window.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/edit_window.rs b/crates/ui-egui/src/edit_window.rs index 08ede709..7d1813fd 100644 --- a/crates/ui-egui/src/edit_window.rs +++ b/crates/ui-egui/src/edit_window.rs @@ -805,8 +805,8 @@ fn track_row(app: &mut SoundApp, ui: &mut Ui, id: TrackId, row: Rect, tl: Rect, } draw_clip(app, &painter, &s, track, clip, tl, lane, auto_view.is_some()); } - draw_live_take(app, &painter, &s, &track, tl, lane); - crate::video_track::draw_lane(app, &painter, &s, &track, tl, lane); + draw_live_take(app, &painter, &s, track, tl, lane); + crate::video_track::draw_lane(app, &painter, &s, track, tl, lane); if s.edit.grid_lines { grid_lines(&painter, &s, tl, lane); } diff --git a/crates/ui-egui/src/mix_window.rs b/crates/ui-egui/src/mix_window.rs index 92261ea6..5622f745 100644 --- a/crates/ui-egui/src/mix_window.rs +++ b/crates/ui-egui/src/mix_window.rs @@ -256,7 +256,7 @@ fn strip(app: &mut SoundApp, ui: &mut Ui, id: TrackId, snap: Option<&MeterSnapsh let in_r = Rect::from_min_size(pos2(x0 + 2.0, sec.min.y + 15.0), vec2(inner_w - 4.0, 16.0)); let out_r = Rect::from_min_size(pos2(x0 + 2.0, sec.min.y + 33.0), vec2(inner_w - 4.0, 16.0)); let mut c = ui.new_child(egui::UiBuilder::new().id_salt(("mix-input", id.0)).max_rect(in_r)); - let resp = selector_box(&mut c, in_r.width(), in_r.height(), &input_name(app, &track), t.text); + let resp = selector_box(&mut c, in_r.width(), in_r.height(), &input_name(app, track), t.text); route_popup(app, &resp, id, true); let mut c = ui.new_child(egui::UiBuilder::new().id_salt(("mix-output", id.0)).max_rect(out_r)); let resp = selector_box(&mut c, out_r.width(), out_r.height(), &route_name(app, &track.mixer.output), t.text);