Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 221 additions & 25 deletions apps/desktop-gpui/src/editor_timeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,96 @@ impl PartialEq for WaveformGeometry {
}
}

/// The span of a segment (segment-local output seconds) its cached waveform
/// path covers. The visible slice is grown to a window anchored on a grid of
/// a quarter of the viewport, so a timeline that scrolls -- playback following
/// the playhead while zoomed in -- reuses one path for a quarter-viewport of
/// travel instead of rebuilding it every frame. A segment that is entirely
/// visible covers exactly that, as before.
pub fn waveform_window(visible: (f64, f64), zoom: f64, duration: f64) -> (f64, f64) {
if !zoom.is_finite() || zoom <= 0. || !duration.is_finite() || duration <= 0. {
return visible;
}
let grid = zoom / 4.;
let step = (visible.0 / grid).floor() - 1.;
let start = (step * grid).max(0.).min(visible.0);
let end = (start + zoom * 1.5).max(visible.1).min(duration);
(start, end)
}

/// Where a segment's waveform path sits inside its canvas. Everything is
/// canvas-local, so a timeline that only scrolls keeps hitting the cache; the
/// path is moved into place when painted.
#[derive(Debug, Clone, Copy, PartialEq)]
struct WaveformLayout {
range: (f64, f64),
origin: gpui::Point<Pixels>,
size: gpui::Size<Pixels>,
samples: usize,
clip_bounds: gpui::Bounds<Pixels>,
}

/// `view` is the timeline's (position, zoom), `segment` its (start, output
/// duration) and `canvas` the (canvas height, lane width, lane height, wave
/// height). `None` when the segment is off screen.
fn waveform_layout(
view: (f64, f64),
segment: (f64, f64),
full_width: f64,
canvas: (Pixels, Pixels, Pixels, Pixels),
) -> Option<WaveformLayout> {
let (view_start, zoom) = view;
let (timeline_start, output_duration) = segment;
let (canvas_height, lane_width, lane_height, wave_height) = canvas;
// The visible slice, in segment-local output seconds
// (`TL/ClipTrack.tsx:202-245`).
let view_end = view_start + zoom;
let visible_start = view_start.max(timeline_start) - timeline_start;
let visible_end = view_end.min(timeline_start + output_duration) - timeline_start;
if visible_end <= visible_start {
return None;
}
let px_per_sec = full_width / output_duration;
let range = waveform_window((visible_start, visible_end), zoom, output_duration);
let window_width = (range.1 - range.0) * px_per_sec;
Some(WaveformLayout {
range,
origin: gpui::point(px((range.0 * px_per_sec) as f32), px(0.)),
size: gpui::size(px(window_width as f32), wave_height),
samples: waveform_window_sample_count(window_width, zoom * px_per_sec),
clip_bounds: gpui::Bounds {
origin: gpui::point(px(0.), canvas_height - lane_height),
size: gpui::size(lane_width, lane_height),
},
})
}

/// `waveform_sample_count` for a window wider than the viewport: the sample
/// cap grows with the window, so the slice on screen keeps the density it had
/// when only the visible slice was built.
fn waveform_window_sample_count(window_width: f64, viewport_width: f64) -> usize {
let scale = if viewport_width.is_finite() && viewport_width > 0. {
(window_width / viewport_width).max(1.)
} else {
1.
};
let cap = (MAX_WAVEFORM_SAMPLES as f64 * scale).ceil() as usize;
((window_width * WAVEFORM_SAMPLES_PER_PIXEL).ceil() as usize).min(cap)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wider-window sample cap is truncated by waveform_path's MAX clamp

Medium Severity

The defect lives in the unchanged waveform_path (apps/desktop-gpui/src/editor_timeline.rs:599-601), which computes num_samples = target_samples.clamp(50, MAX_WAVEFORM_SAMPLES).min(native_samples); the changed helper at line 789 only affects the result through that clamp. Whenever the scaled cap actually matters — a window wider than 3000px, i.e. a zoomed-out long recording where window_width * 2 > 6000, e.g. viewport 6000px and window 9000px — waveform_window_sample_count returns up to 9000, and waveform_path silently truncates it to 6000. The rendered sample count is therefore still min(window_width * 2, MAX_WAVEFORM_SAMPLES), byte-for-byte the old waveform_sample_count(window_width) behavior, so the commit's stated goal (the on-screen slice keeping its pre-scrolling density in a wider cached window) is never realized and the on-screen density still drops by up to 1/3 in exactly the scenario the commit targets. The new test the_wider_window_keeps_the_on_screen_sample_density_at_the_cap passes only because it exercises waveform_window_sample_count in isolation and never builds a path with its result. Fix by raising the cap in waveform_path to the same scaled bound (e.g. pass the scaled cap alongside target_samples, or avoid down-truncating before the clamp) and cover the fix with a test that round-trips through waveform_path.

Open in Capy

}

/// `path` moved by `offset`: cached waveform paths are built relative to their
/// segment and placed where the segment is drawn this frame.
fn translated_path(
mut path: gpui::Path<Pixels>,
offset: gpui::Point<Pixels>,
) -> gpui::Path<Pixels> {
for vertex in &mut path.vertices {
vertex.xy_position = vertex.xy_position + offset;
}
path.bounds.origin = path.bounds.origin + offset;
path
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WaveformTrack {
Mic,
Expand Down Expand Up @@ -3217,27 +3307,13 @@ fn render_waveform(
gpui::canvas(
|bounds, _window, _cx| bounds,
move |_, bounds, window, _cx| {
// The visible slice, in segment-local output seconds
// (`TL/ClipTrack.tsx:202-245`). Off screen entirely: nothing.
let view_start = transform.position;
let view_end = view_start + transform.zoom;
let visible_start = view_start.max(timeline_start) - timeline_start;
let visible_end = view_end.min(timeline_start + output_duration) - timeline_start;
if visible_end <= visible_start {
let Some(layout) = waveform_layout(
(transform.position, transform.zoom),
(timeline_start, output_duration),
full_width,
(bounds.size.height, px(width), px(height), px(wave_height)),
) else {
return;
}
let px_per_sec = full_width / output_duration;
let origin = gpui::point(
bounds.origin.x + px((visible_start * px_per_sec) as f32),
bounds.origin.y,
);
let slice_width = ((visible_end - visible_start) * px_per_sec) as f32;
let size = gpui::size(px(slice_width), px(wave_height));
let samples = waveform_sample_count(slice_width as f64);

let clip_bounds = gpui::Bounds {
origin: gpui::point(bounds.origin.x, bounds.bottom() - px(height)),
size: gpui::size(px(width), px(height)),
};

for (track, peaks, scale) in [
Expand All @@ -3246,16 +3322,17 @@ fn render_waveform(
] {
let geometry = WaveformGeometry {
peaks: peaks.clone(),
range: (visible_start, visible_end),
target_samples: samples,
range: layout.range,
target_samples: layout.samples,
holds: holds.clone(),
source_start: segment_start,
origin,
size,
origin: layout.origin,
size: layout.size,
scale,
clip_bounds,
clip_bounds: layout.clip_bounds,
};
if let Some(path) = paths.get_or_build((index, track), geometry) {
let path = translated_path(path, bounds.origin);
window.with_content_mask(Some(gpui::ContentMask { bounds }), |window| {
window.paint_path(path, wave_color)
});
Expand Down Expand Up @@ -4659,6 +4736,125 @@ mod tests {
}
}

#[test]
fn the_waveform_window_always_covers_the_visible_slice() {
let duration = 120.;
let zoom = 20.;
let mut start = 0.;
while start + zoom <= duration {
let visible = (start, start + zoom);
let window = waveform_window(visible, zoom, duration);
assert!(
window.0 <= visible.0 && window.1 >= visible.1,
"{window:?} {visible:?}"
);
assert!(window.1 - window.0 <= zoom * 1.5 + 1e-9);
start += 0.37;
}
}

#[test]
fn a_fully_visible_segment_covers_exactly_its_visible_slice() {
assert_eq!(waveform_window((0., 8.), 30., 8.), (0., 8.));
assert_eq!(waveform_window((0., 30.), 30., 30.), (0., 30.));
}

#[test]
fn scrolling_reuses_the_window_for_a_quarter_viewport() {
let zoom = 20.;
let duration = 600.;
let mut windows = std::collections::HashSet::new();
// 60 s of follow-mode playback at 60 fps.
for frame in 0..3600 {
let start = 100. + frame as f64 / 60.;
let window = waveform_window((start, start + zoom), zoom, duration);
windows.insert((window.0.to_bits(), window.1.to_bits()));
}
assert!(windows.len() <= 13, "{} distinct windows", windows.len());
}

#[test]
fn a_translated_relative_path_matches_one_built_in_place() {
let peaks = speech_like_peaks(400, 7.);
let offset = gpui::point(px(250.5), px(312.25));
let relative = waveform_geometry(peaks.clone());
let path = waveform_path(&relative).unwrap();
let moved = translated_path(path.clone(), offset);
for (before, after) in path.vertices.iter().zip(&moved.vertices) {
assert_eq!(after.xy_position, before.xy_position + offset);
}
assert_eq!(moved.bounds.origin, path.bounds.origin + offset);
assert_eq!(moved.bounds.size, path.bounds.size);

// Building at the absolute position gives the same shape; the
// tessellator's triangle order can differ with float rounding, so the
// comparison is on the vertex count and the bounds.
let absolute = WaveformGeometry {
origin: relative.origin + offset,
clip_bounds: gpui::Bounds {
origin: relative.clip_bounds.origin + offset,
size: relative.clip_bounds.size,
},
..relative.clone()
};
let built = waveform_path(&absolute).unwrap();
assert_eq!(built.vertices.len(), moved.vertices.len());
let close = |a: Pixels, b: Pixels| (f32::from(a) - f32::from(b)).abs() < 0.01;
assert!(close(built.bounds.origin.x, moved.bounds.origin.x));
assert!(close(built.bounds.origin.y, moved.bounds.origin.y));
assert!(close(built.bounds.size.width, moved.bounds.size.width));
assert!(close(built.bounds.size.height, moved.bounds.size.height));
}

#[test]
fn follow_mode_paths_are_built_once_per_window() {
let paths = WaveformPaths::default();
let slot = (0, WaveformTrack::Mic);
let peaks = speech_like_peaks(2000, 9.);
let zoom = 20.;
let segment = (12., 190.);
let full_width = 190. * 60.;
let canvas = (px(44.), px(full_width as f32), px(44.), px(18.));
for frame in 0..600 {
let position = 40. + frame as f64 / 60.;
let layout = waveform_layout((position, zoom), segment, full_width, canvas).unwrap();
let geometry = WaveformGeometry {
range: layout.range,
target_samples: layout.samples,
origin: layout.origin,
size: layout.size,
clip_bounds: layout.clip_bounds,
..waveform_geometry(peaks.clone())
};
Comment on lines +4821 to +4828

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Scrolling test misses changing positions

follow_mode_paths_are_built_once_per_window changes only range; origin, size, and clip_bounds stay fixed in the helper. Those fields are part of the cache key, and changing screen coordinates caused the original cache misses. This test would still pass if render_waveform accidentally used screen coordinates again.

Build the test geometry through the same code as render_waveform, vary the canvas position, and check that scrolling within one window still hits the cache.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/editor_timeline.rs
Line: 4766-4769

Comment:
**Scrolling test misses changing positions**

`follow_mode_paths_are_built_once_per_window` changes only `range`; `origin`, `size`, and `clip_bounds` stay fixed in the helper. Those fields are part of the cache key, and changing screen coordinates caused the original cache misses. This test would still pass if `render_waveform` accidentally used screen coordinates again.

Build the test geometry through the same code as `render_waveform`, vary the canvas position, and check that scrolling within one window still hits the cache.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

paths.get_or_build(slot, geometry).unwrap();
}
let stats = paths.stats();
assert!(stats.builds <= 3, "{stats:?}");
assert_eq!(stats.builds + stats.hits, 600);
}

#[test]
fn an_off_screen_segment_has_no_waveform_layout() {
let canvas = (px(44.), px(600.), px(44.), px(18.));
assert!(waveform_layout((0., 20.), (30., 10.), 600., canvas).is_none());
assert!(waveform_layout((50., 20.), (30., 10.), 600., canvas).is_none());
}

#[test]
fn the_wider_window_keeps_the_on_screen_sample_density_at_the_cap() {
for (window, viewport) in [(15_000., 10_000.), (4_500., 3_000.), (900., 600.)] {
let samples = waveform_window_sample_count(window, viewport);
let window_density = samples as f64 / window;
let visible_density = waveform_sample_count(viewport) as f64 / viewport;
assert!(
(window_density - visible_density).abs() < 1e-3,
"{window} {viewport}: {window_density} vs {visible_density}"
);
}
assert_eq!(waveform_window_sample_count(300., 600.), 600);
assert_eq!(waveform_window_sample_count(300., 0.), 600);
}

#[test]
fn cached_waveform_matches_an_uncached_build_after_every_input_change() {
let paths = WaveformPaths::default();
Expand Down
Loading