Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
eb32568
feat: GPU NV12 export path + export-optimized encoder settings
cursoragent Feb 15, 2026
989eb3e
fix: handle RGBA fallback in NV12 export path
cursoragent Feb 15, 2026
c6b81be
fix: CPU RGBA→NV12 fallback when GPU conversion fails
cursoragent Feb 15, 2026
aea1455
fix: flush pending NV12 frame at end of export render loop
cursoragent Feb 15, 2026
8e0417b
improve: robust RGBA fallback + encoder timing instrumentation
cursoragent Feb 15, 2026
fc4d7bc
improve: zero-alloc NV12 frame encoding via reusable frame buffer
cursoragent Feb 15, 2026
7732fcd
improve: use SIMD-optimized swscale for RGBA→NV12 CPU fallback
cursoragent Feb 15, 2026
182d5ec
fix: ensure screenshot always receives NV12 data
cursoragent Feb 15, 2026
16b1705
test: add unit tests for NV12 export path helpers
cursoragent Feb 15, 2026
05fef9f
refactor: extract shared decode_segment_frames_with_retry helper
cursoragent Feb 15, 2026
7bef34c
clippy
richiemcilroy Feb 15, 2026
cf4c170
perf(video-decode): enable slice threading and increase decode thread…
richiemcilroy Feb 15, 2026
7aa4f7e
perf(export): remove unnecessary 200ms sleep before export
richiemcilroy Feb 15, 2026
a7e73c8
style(desktop): collapse nested if into let-chain
richiemcilroy Feb 15, 2026
f89c780
refactor(rendering): wrap frame data in Arc<Vec<u8>>
richiemcilroy Feb 15, 2026
a4cac0a
perf(rendering): cache NV12 converter bind groups across frames
richiemcilroy Feb 15, 2026
937e30b
perf(rendering): pipeline render with decode prefetch and latency hiding
richiemcilroy Feb 15, 2026
0d0cf47
refactor(desktop): use render_immediate for non-export render calls
richiemcilroy Feb 15, 2026
f078f5c
perf(export): always use NV12 GPU path with dimension alignment
richiemcilroy Feb 15, 2026
981d6d7
perf(export): increase pipeline channel buffer sizes to 32
richiemcilroy Feb 15, 2026
acdc1be
refactor(export): use into_data for Arc frame extraction
richiemcilroy Feb 15, 2026
e9e01ed
clippy
richiemcilroy Feb 15, 2026
5db2514
clippy
richiemcilroy Feb 15, 2026
1fe56b3
clippy
richiemcilroy Feb 15, 2026
3f25623
clippy
richiemcilroy Feb 15, 2026
a451f83
Fix AVAssetReader unwraps and add session notes
richiemcilroy Feb 15, 2026
4186535
Sync system audio start_time; minor rust refactors
richiemcilroy Feb 15, 2026
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
54 changes: 1 addition & 53 deletions apps/desktop/src-tauri/src/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,50 +151,6 @@ impl CameraPreviewManager {
}
}

// Resumes a paused camera preview. Uses window.show() which is safe, unlike
// panel.order_front_regardless() which causes crashes after repeated use.
pub fn resume(&mut self, window: &WebviewWindow) {
if let Some(preview) = &mut self.preview
&& preview.is_paused
{
preview.is_paused = false;
preview
.reconfigure
.send(ReconfigureEvent::Resume)
.map_err(|err| error!("Error sending camera preview resume event: {err}"))
.ok();
window
.run_on_main_thread({
let window = window.clone();
move || {
let _ = window.show();
}
})
.ok();
}
}

pub fn is_paused(&self) -> bool {
self.preview.as_ref().is_some_and(|p| p.is_paused)
}

pub fn begin_shutdown_for_session(
&mut self,
expected_session_id: u64,
) -> Option<oneshot::Receiver<()>> {
if let Some(preview) = &self.preview
&& preview.session_id != expected_session_id
{
info!(
"Skipping camera preview close: session mismatch (expected {}, current {})",
expected_session_id, preview.session_id
);
return None;
}

self.begin_shutdown()
}

pub fn begin_shutdown(&mut self) -> Option<oneshot::Receiver<()>> {
let preview = self.preview.take()?;
info!(
Expand Down Expand Up @@ -304,14 +260,6 @@ impl CameraPreviewManager {

Ok(())
}

pub fn on_window_close_for_session(&mut self, expected_session_id: u64) {
let _ = self.begin_shutdown_for_session(expected_session_id);
}

pub fn on_window_close(&mut self) {
let _ = self.begin_shutdown();
}
}

// Internal events for the persistent camera renderer architecture.
Expand Down Expand Up @@ -874,7 +822,7 @@ impl Renderer {
let _ = self.device.poll(wgpu::PollType::Wait);

drop(std::mem::take(&mut self.texture));
drop(std::mem::take(&mut self.aspect_ratio));
self.aspect_ratio = Cached::default();

let surface = self.surface.take();
let (drop_tx, drop_rx) = oneshot::channel();
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/src/camera_legacy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ pub async fn create_camera_preview_ws() -> (Sender<FFmpegVideoFrame>, u16, Cance

frame_tx_clone
.send(WSFrame {
data: frame.data(0).to_vec(),
data: std::sync::Arc::new(frame.data(0).to_vec()),
width: frame.width(),
height: frame.height(),
stride: frame.stride(0) as u32,
Expand Down
5 changes: 2 additions & 3 deletions apps/desktop/src-tauri/src/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ pub async fn export_video(
let _guard = if let Some(ref ed) = *editor {
ed.export_active.store(true, Ordering::Release);
tracing::info!("Pausing editor preview during export");
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
Some(ExportActiveGuard(&ed.export_active))
} else {
None
Expand Down Expand Up @@ -367,7 +366,7 @@ pub async fn generate_export_preview(
);

let frame = frame_renderer
.render(
.render_immediate(
segment_frames,
uniforms,
&render_segment.cursor,
Expand Down Expand Up @@ -510,7 +509,7 @@ pub async fn generate_export_preview_fast(
);

let frame = frame_renderer
.render(segment_frames, uniforms, &segment_media.cursor, &mut layers)
.render_immediate(segment_frames, uniforms, &segment_media.cursor, &mut layers)
.await
.map_err(|e| format!("Failed to render frame: {e}"))?;

Expand Down
14 changes: 7 additions & 7 deletions apps/desktop/src-tauri/src/frame_ws.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ pub enum WSFrameFormat {

#[derive(Clone)]
pub struct WSFrame {
pub data: Vec<u8>,
pub data: std::sync::Arc<Vec<u8>>,
pub width: u32,
pub height: u32,
pub stride: u32,
Expand Down Expand Up @@ -138,11 +138,11 @@ pub async fn create_watch_frame_ws(
let borrowed = camera_rx.borrow();
borrowed.as_deref().map(pack_ws_frame_ref)
};
if let Some(packed) = packed {
if let Err(e) = socket.send(Message::Binary(packed)).await {
tracing::error!("Failed to send initial frame to socket: {:?}", e);
return;
}
if let Some(packed) = packed
&& let Err(e) = socket.send(Message::Binary(packed)).await
{
tracing::error!("Failed to send initial frame to socket: {:?}", e);
return;
}
}

Expand Down Expand Up @@ -278,7 +278,7 @@ pub async fn create_frame_ws(frame_tx: broadcast::Sender<WSFrame>) -> (u16, Canc
match incoming_frame {
Ok(frame) => {
let packed = pack_frame_data(
frame.data,
std::sync::Arc::unwrap_or_clone(frame.data),
frame.stride,
frame.height,
frame.width,
Expand Down
121 changes: 2 additions & 119 deletions apps/desktop/src-tauri/src/panel_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,26 @@ use tokio::sync::RwLock;
use tracing::{debug, info, trace, warn};

#[cfg(target_os = "macos")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum PanelState {
#[default]
None,
Creating,
Ready,
Destroying,
}

#[cfg(target_os = "macos")]
impl Default for PanelState {
fn default() -> Self {
Self::None
}
}

#[cfg(target_os = "macos")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PanelWindowType {
Camera,
Main,
TargetSelectOverlay,
InProgressRecording,
}

#[cfg(target_os = "macos")]
impl std::fmt::Display for PanelWindowType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Camera => write!(f, "Camera"),
Self::Main => write!(f, "Main"),
Self::TargetSelectOverlay => write!(f, "TargetSelectOverlay"),
Self::InProgressRecording => write!(f, "InProgressRecording"),
}
}
}
Expand Down Expand Up @@ -107,7 +95,6 @@ impl PanelManager {
window_type, op_id
);
Some(PanelOperationGuard {
window_type,
operation_id: op_id,
completed: false,
})
Expand Down Expand Up @@ -136,87 +123,6 @@ impl PanelManager {
}
}

pub async fn try_begin_show(
&self,
window_type: PanelWindowType,
) -> Option<PanelOperationGuard> {
let mut panels = self.panels.write().await;
let entry = panels.entry(window_type).or_default();

match entry.state {
PanelState::Ready => {
let op_id = self
.operation_counter
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
debug!(
"Panel {}: beginning show operation (op_id={})",
window_type, op_id
);
Some(PanelOperationGuard {
window_type,
operation_id: op_id,
completed: true,
})
}
PanelState::None => {
debug!("Panel {}: show blocked - window doesn't exist", window_type);
None
}
PanelState::Creating => {
debug!(
"Panel {}: show blocked - currently creating (op_id={})",
window_type, entry.operation_id
);
None
}
PanelState::Destroying => {
debug!(
"Panel {}: show blocked - currently destroying (op_id={})",
window_type, entry.operation_id
);
None
}
}
}

pub async fn try_begin_destroy(
&self,
window_type: PanelWindowType,
) -> Option<PanelOperationGuard> {
let mut panels = self.panels.write().await;
let entry = panels.entry(window_type).or_default();

match entry.state {
PanelState::Ready | PanelState::Creating => {
let op_id = self
.operation_counter
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
entry.state = PanelState::Destroying;
entry.operation_id = op_id;
debug!(
"Panel {}: beginning destroy operation (op_id={})",
window_type, op_id
);
Some(PanelOperationGuard {
window_type,
operation_id: op_id,
completed: false,
})
}
PanelState::None => {
debug!("Panel {}: destroy skipped - already destroyed", window_type);
None
}
PanelState::Destroying => {
debug!(
"Panel {}: destroy blocked - already destroying (op_id={})",
window_type, entry.operation_id
);
None
}
}
}

pub async fn mark_ready(&self, window_type: PanelWindowType, operation_id: u64) {
let mut panels = self.panels.write().await;
if let Some(entry) = panels.get_mut(&window_type) {
Expand All @@ -235,24 +141,6 @@ impl PanelManager {
}
}

pub async fn mark_destroyed(&self, window_type: PanelWindowType, operation_id: u64) {
let mut panels = self.panels.write().await;
if let Some(entry) = panels.get_mut(&window_type) {
if entry.operation_id == operation_id && entry.state == PanelState::Destroying {
entry.state = PanelState::None;
info!(
"Panel {}: marked destroyed (op_id={})",
window_type, operation_id
);
} else {
warn!(
"Panel {}: mark_destroyed ignored - state mismatch (current state={:?}, current op={}, requested op={})",
window_type, entry.state, entry.operation_id, operation_id
);
}
}
}

pub async fn force_reset(&self, window_type: PanelWindowType) {
let mut panels = self.panels.write().await;
if let Some(entry) = panels.get_mut(&window_type) {
Expand Down Expand Up @@ -293,7 +181,6 @@ impl PanelManager {

#[cfg(target_os = "macos")]
pub struct PanelOperationGuard {
pub window_type: PanelWindowType,
pub operation_id: u64,
completed: bool,
}
Expand All @@ -303,10 +190,6 @@ impl PanelOperationGuard {
pub fn mark_completed(&mut self) {
self.completed = true;
}

pub fn is_completed(&self) -> bool {
self.completed
}
}

#[cfg(target_os = "macos")]
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/src/screenshot_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ impl ScreenshotEditorInstances {
);

let rendered_frame = frame_renderer
.render(
.render_immediate(
segment_frames,
uniforms,
&cap_project::CursorEvents::default(),
Expand Down
32 changes: 13 additions & 19 deletions apps/desktop/src-tauri/src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,9 @@ fn is_system_dark_mode() -> bool {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
if let Ok(key) =
hkcu.open_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize")
&& let Ok(value) = key.get_value::<u32, _>("AppsUseLightTheme")
{
if let Ok(value) = key.get_value::<u32, _>("AppsUseLightTheme") {
return value == 0;
}
return value == 0;
}
false
}
Expand Down Expand Up @@ -858,17 +857,17 @@ impl ShowCapWindow {
}

#[cfg(not(target_os = "macos"))]
if let Self::InProgressRecording { .. } = self {
if let Some(window) = self.id(app).get(app) {
let width = 320.0;
let height = 150.0;
let recording_monitor = CursorMonitorInfo::get();
let (pos_x, pos_y) = recording_monitor.bottom_center_position(width, height, 120.0);
let _ = window.set_position(tauri::LogicalPosition::new(pos_x, pos_y));
window.show().ok();
window.set_focus().ok();
return Ok(window);
}
if let Self::InProgressRecording { .. } = self
&& let Some(window) = self.id(app).get(app)
{
let width = 320.0;
let height = 150.0;
let recording_monitor = CursorMonitorInfo::get();
let (pos_x, pos_y) = recording_monitor.bottom_center_position(width, height, 120.0);
let _ = window.set_position(tauri::LogicalPosition::new(pos_x, pos_y));
window.show().ok();
window.set_focus().ok();
return Ok(window);
}

if !matches!(self, Self::Camera { .. } | Self::InProgressRecording { .. })
Expand All @@ -886,11 +885,6 @@ impl ShowCapWindow {
None
};

match self {
Self::Main { .. } => {}
_ => {}
}

if let Self::Main {
init_target_mode: Some(target_mode),
} = self
Expand Down
Loading
Loading