fix lag spike when first displaying audio clip
This commit is contained in:
parent
b87e4325c2
commit
6bd400d353
|
|
@ -181,6 +181,10 @@ pub struct TimelinePane {
|
||||||
|
|
||||||
/// Whether to display time as seconds or measures
|
/// Whether to display time as seconds or measures
|
||||||
time_display_format: TimeDisplayFormat,
|
time_display_format: TimeDisplayFormat,
|
||||||
|
|
||||||
|
/// Waveform upload progress: pool_index -> frames uploaded so far.
|
||||||
|
/// Tracks chunked GPU uploads across frames to avoid hitches.
|
||||||
|
waveform_upload_progress: std::collections::HashMap<usize, usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a clip type can be dropped on a layer type
|
/// Check if a clip type can be dropped on a layer type
|
||||||
|
|
@ -367,6 +371,7 @@ impl TimelinePane {
|
||||||
layer_control_clicked: false,
|
layer_control_clicked: false,
|
||||||
context_menu_clip: None,
|
context_menu_clip: None,
|
||||||
time_display_format: TimeDisplayFormat::Seconds,
|
time_display_format: TimeDisplayFormat::Seconds,
|
||||||
|
waveform_upload_progress: std::collections::HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1497,7 +1502,7 @@ impl TimelinePane {
|
||||||
/// Render layer rows (timeline content area)
|
/// Render layer rows (timeline content area)
|
||||||
/// Returns video clip hover data for processing after input handling
|
/// Returns video clip hover data for processing after input handling
|
||||||
fn render_layers(
|
fn render_layers(
|
||||||
&self,
|
&mut self,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
rect: egui::Rect,
|
rect: egui::Rect,
|
||||||
theme: &crate::theme::Theme,
|
theme: &crate::theme::Theme,
|
||||||
|
|
@ -2128,11 +2133,27 @@ impl TimelinePane {
|
||||||
let screen_size = ui.ctx().content_rect().size();
|
let screen_size = ui.ctx().content_rect().size();
|
||||||
|
|
||||||
let pending_upload = if waveform_gpu_dirty.contains(audio_pool_index) {
|
let pending_upload = if waveform_gpu_dirty.contains(audio_pool_index) {
|
||||||
waveform_gpu_dirty.remove(audio_pool_index);
|
// Chunked upload: track progress across frames
|
||||||
|
let chunk = crate::waveform_gpu::UPLOAD_CHUNK_FRAMES;
|
||||||
|
let progress = self.waveform_upload_progress.get(audio_pool_index).copied().unwrap_or(0);
|
||||||
|
let next_end = (progress + chunk).min(total_frames);
|
||||||
|
let frame_limit = Some(next_end);
|
||||||
|
|
||||||
|
if next_end >= total_frames {
|
||||||
|
// Final chunk — done
|
||||||
|
waveform_gpu_dirty.remove(audio_pool_index);
|
||||||
|
self.waveform_upload_progress.remove(audio_pool_index);
|
||||||
|
} else {
|
||||||
|
// More chunks needed
|
||||||
|
self.waveform_upload_progress.insert(*audio_pool_index, next_end);
|
||||||
|
ui.ctx().request_repaint();
|
||||||
|
}
|
||||||
|
|
||||||
Some(crate::waveform_gpu::PendingUpload {
|
Some(crate::waveform_gpu::PendingUpload {
|
||||||
samples: samples.clone(),
|
samples: samples.clone(),
|
||||||
sample_rate: *sr,
|
sample_rate: *sr,
|
||||||
channels: *ch,
|
channels: *ch,
|
||||||
|
frame_limit,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
@ -2239,6 +2260,7 @@ impl TimelinePane {
|
||||||
samples: samples.clone(),
|
samples: samples.clone(),
|
||||||
sample_rate: *sr,
|
sample_rate: *sr,
|
||||||
channels: *ch,
|
channels: *ch,
|
||||||
|
frame_limit: None, // recording uses incremental path
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
|
||||||
|
|
@ -108,8 +108,16 @@ pub struct PendingUpload {
|
||||||
pub samples: std::sync::Arc<Vec<f32>>,
|
pub samples: std::sync::Arc<Vec<f32>>,
|
||||||
pub sample_rate: u32,
|
pub sample_rate: u32,
|
||||||
pub channels: u32,
|
pub channels: u32,
|
||||||
|
/// If set, only upload up to this many frames (for chunked uploads).
|
||||||
|
/// The texture is allocated at full size, but total_frames is set to
|
||||||
|
/// the limited count so subsequent calls use the incremental path.
|
||||||
|
pub frame_limit: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Maximum frames to convert and upload per frame (~250K frames ≈ 5.6s at 44.1kHz).
|
||||||
|
/// Keeps the CPU f32→f16 conversion under ~2-3ms per frame.
|
||||||
|
pub const UPLOAD_CHUNK_FRAMES: usize = 250_000;
|
||||||
|
|
||||||
impl WaveformGpuResources {
|
impl WaveformGpuResources {
|
||||||
pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
|
pub fn new(device: &wgpu::Device, target_format: wgpu::TextureFormat) -> Self {
|
||||||
// Render shader
|
// Render shader
|
||||||
|
|
@ -282,18 +290,22 @@ impl WaveformGpuResources {
|
||||||
samples: &[f32],
|
samples: &[f32],
|
||||||
sample_rate: u32,
|
sample_rate: u32,
|
||||||
channels: u32,
|
channels: u32,
|
||||||
|
frame_limit: Option<usize>,
|
||||||
) -> Vec<wgpu::CommandBuffer> {
|
) -> Vec<wgpu::CommandBuffer> {
|
||||||
let new_total_frames = samples.len() / channels.max(1) as usize;
|
let new_total_frames = samples.len() / channels.max(1) as usize;
|
||||||
if new_total_frames == 0 {
|
if new_total_frames == 0 {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For incremental path, also respect frame_limit
|
||||||
|
let effective_frames = frame_limit.map_or(new_total_frames, |lim| lim.min(new_total_frames));
|
||||||
|
|
||||||
// If entry exists and texture is large enough, do an incremental update
|
// If entry exists and texture is large enough, do an incremental update
|
||||||
let incremental = if let Some(entry) = self.entries.get(&pool_index) {
|
let incremental = if let Some(entry) = self.entries.get(&pool_index) {
|
||||||
let new_tex_height = (new_total_frames as u32 + TEX_WIDTH - 1) / TEX_WIDTH;
|
let new_tex_height = (new_total_frames as u32 + TEX_WIDTH - 1) / TEX_WIDTH;
|
||||||
if new_tex_height <= entry.tex_height && new_total_frames > entry.total_frames as usize {
|
if new_tex_height <= entry.tex_height && effective_frames > entry.total_frames as usize {
|
||||||
Some((entry.total_frames as usize, entry.tex_height))
|
Some((entry.total_frames as usize, entry.tex_height))
|
||||||
} else if new_total_frames <= entry.total_frames as usize {
|
} else if effective_frames <= entry.total_frames as usize {
|
||||||
return Vec::new(); // No new data
|
return Vec::new(); // No new data
|
||||||
} else {
|
} else {
|
||||||
None // Texture too small, need full recreate
|
None // Texture too small, need full recreate
|
||||||
|
|
@ -305,7 +317,7 @@ impl WaveformGpuResources {
|
||||||
if let Some((old_frames, tex_height)) = incremental {
|
if let Some((old_frames, tex_height)) = incremental {
|
||||||
// Write only the NEW rows into the existing texture
|
// Write only the NEW rows into the existing texture
|
||||||
let start_row = old_frames as u32 / TEX_WIDTH;
|
let start_row = old_frames as u32 / TEX_WIDTH;
|
||||||
let end_row = (new_total_frames as u32 + TEX_WIDTH - 1) / TEX_WIDTH;
|
let end_row = (effective_frames as u32 + TEX_WIDTH - 1) / TEX_WIDTH;
|
||||||
let rows_to_write = end_row - start_row;
|
let rows_to_write = end_row - start_row;
|
||||||
|
|
||||||
let row_texel_count = (TEX_WIDTH * rows_to_write) as usize;
|
let row_texel_count = (TEX_WIDTH * rows_to_write) as usize;
|
||||||
|
|
@ -314,7 +326,7 @@ impl WaveformGpuResources {
|
||||||
let row_start_frame = start_row as usize * TEX_WIDTH as usize;
|
let row_start_frame = start_row as usize * TEX_WIDTH as usize;
|
||||||
for frame in 0..(rows_to_write as usize * TEX_WIDTH as usize) {
|
for frame in 0..(rows_to_write as usize * TEX_WIDTH as usize) {
|
||||||
let global_frame = row_start_frame + frame;
|
let global_frame = row_start_frame + frame;
|
||||||
if global_frame >= new_total_frames {
|
if global_frame >= effective_frames {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let sample_offset = global_frame * channels as usize;
|
let sample_offset = global_frame * channels as usize;
|
||||||
|
|
@ -364,11 +376,11 @@ impl WaveformGpuResources {
|
||||||
TEX_WIDTH,
|
TEX_WIDTH,
|
||||||
tex_height,
|
tex_height,
|
||||||
mip_count,
|
mip_count,
|
||||||
new_total_frames as u32,
|
effective_frames as u32,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update total_frames after borrow of entry is done
|
// Update total_frames after borrow of entry is done
|
||||||
self.entries.get_mut(&pool_index).unwrap().total_frames = new_total_frames as u64;
|
self.entries.get_mut(&pool_index).unwrap().total_frames = effective_frames as u64;
|
||||||
return cmds;
|
return cmds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -378,12 +390,15 @@ impl WaveformGpuResources {
|
||||||
self.per_instance.retain(|&(pi, _), _| pi != pool_index);
|
self.per_instance.retain(|&(pi, _), _| pi != pool_index);
|
||||||
|
|
||||||
let total_frames = new_total_frames;
|
let total_frames = new_total_frames;
|
||||||
|
// Upload only effective_frames worth of data on this call
|
||||||
|
let upload_frames = effective_frames;
|
||||||
|
|
||||||
// For live recording (pool_index == usize::MAX), pre-allocate extra texture
|
// For live recording (pool_index == usize::MAX), pre-allocate extra texture
|
||||||
// height to avoid frequent full recreates as recording grows.
|
// height to avoid frequent full recreates as recording grows.
|
||||||
// Allocate 60 seconds ahead so incremental updates can fill without recreating.
|
// Allocate 60 seconds ahead so incremental updates can fill without recreating.
|
||||||
|
// When chunking, always allocate for the full total so incremental updates fit.
|
||||||
let alloc_frames = if pool_index == usize::MAX {
|
let alloc_frames = if pool_index == usize::MAX {
|
||||||
let extra = sample_rate as usize * 60; // 60s of mono frames (texture is per-frame, not per-sample)
|
let extra = sample_rate as usize * 60;
|
||||||
total_frames + extra
|
total_frames + extra
|
||||||
} else {
|
} else {
|
||||||
total_frames
|
total_frames
|
||||||
|
|
@ -411,12 +426,16 @@ impl WaveformGpuResources {
|
||||||
let seg_end_frame = ((seg + 1) as u64 * frames_per_segment as u64)
|
let seg_end_frame = ((seg + 1) as u64 * frames_per_segment as u64)
|
||||||
.min(total_frames as u64);
|
.min(total_frames as u64);
|
||||||
let seg_frame_count = (seg_end_frame - seg_start_frame) as u32;
|
let seg_frame_count = (seg_end_frame - seg_start_frame) as u32;
|
||||||
|
// Limit actual data processing to upload_frames (for chunked uploads)
|
||||||
|
let seg_upload_end = ((seg + 1) as u64 * frames_per_segment as u64)
|
||||||
|
.min(upload_frames as u64);
|
||||||
|
let seg_upload_count = seg_upload_end.saturating_sub(seg_start_frame) as u32;
|
||||||
|
|
||||||
// Allocate texture large enough for future growth (recording) or exact fit (normal)
|
// Allocate texture large enough for the FULL data (not just this chunk)
|
||||||
let alloc_seg_frames = if pool_index == usize::MAX {
|
let alloc_seg_frames = if pool_index == usize::MAX {
|
||||||
(alloc_frames as u32).min(seg_frame_count + sample_rate * 60)
|
(alloc_frames as u32).min(seg_frame_count + sample_rate * 60)
|
||||||
} else {
|
} else {
|
||||||
seg_frame_count
|
seg_frame_count // full size so incremental updates fit
|
||||||
};
|
};
|
||||||
let tex_height = (alloc_seg_frames + TEX_WIDTH - 1) / TEX_WIDTH;
|
let tex_height = (alloc_seg_frames + TEX_WIDTH - 1) / TEX_WIDTH;
|
||||||
let mip_count = compute_mip_count(TEX_WIDTH, tex_height);
|
let mip_count = compute_mip_count(TEX_WIDTH, tex_height);
|
||||||
|
|
@ -440,12 +459,12 @@ impl WaveformGpuResources {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pack raw samples into Rgba16Float data for mip 0
|
// Pack raw samples into Rgba16Float data for mip 0
|
||||||
// Only pack rows containing actual data (not the pre-allocated empty region)
|
// Only pack rows containing data uploaded this chunk
|
||||||
let data_height = (seg_frame_count + TEX_WIDTH - 1) / TEX_WIDTH;
|
let data_height = (seg_upload_count + TEX_WIDTH - 1) / TEX_WIDTH;
|
||||||
let data_texel_count = (TEX_WIDTH * data_height) as usize;
|
let data_texel_count = (TEX_WIDTH * data_height) as usize;
|
||||||
let mut mip0_data: Vec<half::f16> = vec![half::f16::ZERO; data_texel_count * 4];
|
let mut mip0_data: Vec<half::f16> = vec![half::f16::ZERO; data_texel_count * 4];
|
||||||
|
|
||||||
for frame in 0..seg_frame_count as usize {
|
for frame in 0..seg_upload_count as usize {
|
||||||
let global_frame = seg_start_frame as usize + frame;
|
let global_frame = seg_start_frame as usize + frame;
|
||||||
let sample_offset = global_frame * channels as usize;
|
let sample_offset = global_frame * channels as usize;
|
||||||
|
|
||||||
|
|
@ -490,14 +509,14 @@ impl WaveformGpuResources {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate mipmaps via compute shader
|
// Generate mipmaps via compute shader (only for uploaded data)
|
||||||
let cmds = self.generate_mipmaps(
|
let cmds = self.generate_mipmaps(
|
||||||
device,
|
device,
|
||||||
&texture,
|
&texture,
|
||||||
TEX_WIDTH,
|
TEX_WIDTH,
|
||||||
tex_height,
|
tex_height,
|
||||||
mip_count,
|
mip_count,
|
||||||
seg_frame_count,
|
seg_upload_count,
|
||||||
);
|
);
|
||||||
all_command_buffers.extend(cmds);
|
all_command_buffers.extend(cmds);
|
||||||
|
|
||||||
|
|
@ -549,7 +568,7 @@ impl WaveformGpuResources {
|
||||||
render_bind_groups,
|
render_bind_groups,
|
||||||
uniform_buffers,
|
uniform_buffers,
|
||||||
frames_per_segment,
|
frames_per_segment,
|
||||||
total_frames: total_frames as u64,
|
total_frames: upload_frames as u64, // only what was uploaded this chunk
|
||||||
tex_height: (alloc_frames as u32 + TEX_WIDTH - 1) / TEX_WIDTH,
|
tex_height: (alloc_frames as u32 + TEX_WIDTH - 1) / TEX_WIDTH,
|
||||||
sample_rate,
|
sample_rate,
|
||||||
channels,
|
channels,
|
||||||
|
|
@ -681,6 +700,7 @@ impl egui_wgpu::CallbackTrait for WaveformCallback {
|
||||||
&upload.samples,
|
&upload.samples,
|
||||||
upload.sample_rate,
|
upload.sample_rate,
|
||||||
upload.channels,
|
upload.channels,
|
||||||
|
upload.frame_limit,
|
||||||
);
|
);
|
||||||
cmds.extend(new_cmds);
|
cmds.extend(new_cmds);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue