Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
80e8710
refactor(rendering): centralize wgpu instance creation and software a…
richiemcilroy Mar 16, 2026
de1985c
refactor(desktop): use centralized wgpu utilities in gpu_context
richiemcilroy Mar 16, 2026
6a5cac6
refactor(desktop): use SharedWgpuDevice and from_shared_device in scr…
richiemcilroy Mar 16, 2026
7ac489b
refactor(cap-test): use adapter_name accessor in performance tests
richiemcilroy Mar 16, 2026
395ac3f
refactor(editor): use adapter_name accessor in playback benchmark
richiemcilroy Mar 16, 2026
eeabea6
feat(cap-test): skip performance suite on Windows CI with software ad…
richiemcilroy Mar 16, 2026
abc5fc1
feat(desktop): guard background tasks and window events during app exit
richiemcilroy Mar 16, 2026
8601e08
fix(desktop): replace unwrap with error propagation in editor commands
richiemcilroy Mar 16, 2026
dea74c1
fix(desktop): handle CapWindowId parse failure in dock icon visibilit…
richiemcilroy Mar 16, 2026
64ff8f2
refactor(desktop): use request_app_exit for tray quit action
richiemcilroy Mar 16, 2026
58a9018
feat(desktop): add window content protection diagnostics on Windows
richiemcilroy Mar 16, 2026
7b9b7ba
Use Option.map to simplify shared device creation
richiemcilroy Mar 16, 2026
0a55be9
fmt
richiemcilroy Mar 16, 2026
9ba0f6d
clippy
richiemcilroy Mar 16, 2026
1dce18c
Add null-safety and typing fixes across web
richiemcilroy Mar 16, 2026
6b1308b
Address PR review comments for wgpu instance, camera protection, and …
richiemcilroy Mar 17, 2026
07c97ac
Add fallback return in getLink for forward-compatible notification types
richiemcilroy Mar 17, 2026
f11755c
Add fallback return in getLink for forward-compatible notification types
richiemcilroy Mar 17, 2026
acc8ada
Merge branch 'misc-fixes-2' of https://github.com/CapSoftware/Cap int…
richiemcilroy Mar 17, 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
59 changes: 26 additions & 33 deletions apps/desktop/src-tauri/src/gpu_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,31 +49,7 @@ pub struct SharedGpuContext {
static GPU: OnceCell<Option<SharedGpuContext>> = OnceCell::const_new();

async fn init_gpu_inner() -> Option<SharedGpuContext> {
#[cfg(not(target_os = "windows"))]
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());

#[cfg(target_os = "windows")]
let instance = {
let dx12_instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::DX12,
..Default::default()
});
let has_dx12 = dx12_instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: None,
})
.await
.is_ok();
if has_dx12 {
tracing::info!("Using DX12 backend for shared GPU context");
dx12_instance
} else {
tracing::info!("DX12 not available for shared context, falling back to all backends");
wgpu::Instance::new(&wgpu::InstanceDescriptor::default())
}
};
let instance = cap_rendering::create_wgpu_instance().await;

let hardware_adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
Expand All @@ -85,12 +61,26 @@ async fn init_gpu_inner() -> Option<SharedGpuContext> {
.ok();

let (adapter, is_software_adapter) = if let Some(adapter) = hardware_adapter {
tracing::info!(
adapter_name = adapter.get_info().name,
adapter_backend = ?adapter.get_info().backend,
"Using hardware GPU adapter for shared context"
);
(adapter, false)
let adapter_info = adapter.get_info();
let is_software_adapter = cap_rendering::is_software_wgpu_adapter(&adapter_info);

if is_software_adapter {
tracing::warn!(
adapter_name = adapter_info.name,
adapter_backend = ?adapter_info.backend,
adapter_device_type = ?adapter_info.device_type,
"Selected shared-context adapter behaves like a software renderer"
);
} else {
tracing::info!(
adapter_name = adapter_info.name,
adapter_backend = ?adapter_info.backend,
adapter_device_type = ?adapter_info.device_type,
"Using hardware GPU adapter for shared context"
);
}

(adapter, is_software_adapter)
} else {
tracing::warn!(
"No hardware GPU adapter found, attempting software fallback for shared context"
Expand All @@ -104,9 +94,12 @@ async fn init_gpu_inner() -> Option<SharedGpuContext> {
.await
.ok()?;

let adapter_info = software_adapter.get_info();

tracing::info!(
adapter_name = software_adapter.get_info().name,
adapter_backend = ?software_adapter.get_info().backend,
adapter_name = adapter_info.name,
adapter_backend = ?adapter_info.backend,
adapter_device_type = ?adapter_info.device_type,
"Using software adapter for shared context (CPU rendering - performance may be reduced)"
);
(software_adapter, true)
Expand Down
78 changes: 61 additions & 17 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,13 @@ fn spawn_exit_watchdog() {
});
}

fn app_is_exiting(app: &AppHandle) -> bool {
match app.try_state::<AppExitState>() {
Some(state) => state.is_exiting(),
None => false,
}
}

fn now_millis() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
Expand Down Expand Up @@ -799,6 +806,10 @@ fn spawn_mic_error_handler(app_handle: AppHandle, error_rx: flume::Receiver<Stre
let error_rx = error_rx;

while let Ok(err) = error_rx.recv_async().await {
if app_is_exiting(&app_handle) {
break;
}

error!("Mic feed actor error: {err}");

{
Expand Down Expand Up @@ -873,6 +884,10 @@ fn spawn_devices_snapshot_emitter(app_handle: AppHandle) {
let mut last_mics: Vec<String> = Vec::new();
let mut fast_loops = 0u32;
loop {
if app_is_exiting(&app_handle) {
break;
}

let permissions = permissions::do_permissions_check(false);
let cameras = if permissions.camera.permitted() {
cap_camera::list_cameras().collect::<Vec<_>>()
Expand Down Expand Up @@ -935,6 +950,10 @@ fn spawn_devices_snapshot_emitter(app_handle: AppHandle) {
};
fast_loops = fast_loops.saturating_add(1);
tokio::time::sleep(dur).await;

if app_is_exiting(&app_handle) {
break;
}
}
});
}
Expand Down Expand Up @@ -1128,6 +1147,10 @@ fn spawn_microphone_watcher(app_handle: AppHandle) {
let state = state.inner().clone();

loop {
if app_is_exiting(&app_handle) {
break;
}

let (should_check, label, is_marked) = {
let guard = state.read().await;
(
Expand Down Expand Up @@ -1185,6 +1208,10 @@ fn spawn_camera_watcher(app_handle: AppHandle) {
let state = state.inner().clone();

loop {
if app_is_exiting(&app_handle) {
break;
}

let (should_check, camera_id, is_marked) = {
let guard = state.read().await;
(
Expand Down Expand Up @@ -1808,7 +1835,9 @@ struct SerializedEditorInstance {
#[specta::specta]
#[instrument(skip(window))]
async fn create_editor_instance(window: Window) -> Result<SerializedEditorInstance, String> {
let CapWindowId::Editor { id } = CapWindowId::from_str(window.label()).unwrap() else {
let CapWindowId::Editor { id } =
CapWindowId::from_str(window.label()).map_err(|e| e.to_string())?
else {
return Err("Invalid window".to_string());
};

Expand Down Expand Up @@ -1844,7 +1873,9 @@ async fn create_editor_instance(window: Window) -> Result<SerializedEditorInstan
#[specta::specta]
#[instrument(skip(window))]
async fn get_editor_project_path(window: Window) -> Result<PathBuf, String> {
let CapWindowId::Editor { id } = CapWindowId::from_str(window.label()).unwrap() else {
let CapWindowId::Editor { id } =
CapWindowId::from_str(window.label()).map_err(|e| e.to_string())?
else {
return Err("Invalid window".to_string());
};

Expand Down Expand Up @@ -3521,6 +3552,16 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
let label = window.label();
let app = window.app_handle();

if matches!(
event,
WindowEvent::CloseRequested { .. }
| WindowEvent::Moved(_)
| WindowEvent::Focused(_)
) && app_is_exiting(app)
{
return;
}

match event {
WindowEvent::CloseRequested { api, .. } => {
if let Ok(window_id) = CapWindowId::from_str(label) {
Expand Down Expand Up @@ -3582,6 +3623,9 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
}
}
WindowEvent::Destroyed => {
if app_is_exiting(app) {
return;
}
if let Ok(window_id) = CapWindowId::from_str(label) {
if matches!(window_id, CapWindowId::Camera) {
tracing::warn!("Camera window Destroyed event received!");
Expand Down Expand Up @@ -3702,10 +3746,11 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {

if let Some(settings) = GeneralSettingsStore::get(app).unwrap_or(None)
&& settings.hide_dock_icon
&& app
.webview_windows()
.keys()
.all(|label| !CapWindowId::from_str(label).unwrap().activates_dock())
&& app.webview_windows().keys().all(|label| {
CapWindowId::from_str(label)
.map(|id| !id.activates_dock())
.unwrap_or(false)
})
{
#[cfg(target_os = "macos")]
app.set_activation_policy(tauri::ActivationPolicy::Accessory)
Expand Down Expand Up @@ -4032,17 +4077,16 @@ async fn create_editor_instance_impl(

wait_for_recording_ready(&app, &path).await?;

let shared_device = if let Some(shared) = gpu_context::get_shared_gpu().await {
Some(cap_rendering::SharedWgpuDevice {
instance: (*shared.instance).clone(),
adapter: (*shared.adapter).clone(),
device: (*shared.device).clone(),
queue: (*shared.queue).clone(),
is_software_adapter: shared.is_software_adapter,
})
} else {
None
};
let shared_device =
gpu_context::get_shared_gpu()
.await
.map(|shared| cap_rendering::SharedWgpuDevice {
instance: (*shared.instance).clone(),
adapter: (*shared.adapter).clone(),
device: (*shared.device).clone(),
queue: (*shared.queue).clone(),
is_software_adapter: shared.is_software_adapter,
});

let instance = {
let app = app.clone();
Expand Down
87 changes: 43 additions & 44 deletions apps/desktop/src-tauri/src/screenshot_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,62 +249,61 @@ impl ScreenshotEditorInstances {
}
};

let (instance, adapter, device, queue, is_software_adapter) =
if let Some(shared) = gpu_context::get_shared_gpu().await {
(
shared.instance.clone(),
shared.adapter.clone(),
shared.device.clone(),
shared.queue.clone(),
shared.is_software_adapter,
)
} else {
let instance =
Arc::new(wgpu::Instance::new(&wgpu::InstanceDescriptor::default()));
let adapter = Arc::new(
instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: None,
})
.await
.map_err(|_| "No GPU adapter found".to_string())?,
);

let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("cap-rendering-device"),
required_features: wgpu::Features::empty(),
..Default::default()
})
.await
.map_err(|e| e.to_string())?;
(instance, adapter, Arc::new(device), Arc::new(queue), false)
};
let shared = if let Some(gpu) = gpu_context::get_shared_gpu().await {
cap_rendering::SharedWgpuDevice {
instance: (*gpu.instance).clone(),
adapter: (*gpu.adapter).clone(),
device: (*gpu.device).clone(),
queue: (*gpu.queue).clone(),
is_software_adapter: gpu.is_software_adapter,
}
} else {
let instance = cap_rendering::create_wgpu_instance().await;
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: None,
})
.await
.map_err(|_| "No GPU adapter found".to_string())?;
let adapter_info = adapter.get_info();
let is_software_adapter =
cap_rendering::is_software_wgpu_adapter(&adapter_info);

let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("cap-rendering-device"),
required_features: wgpu::Features::empty(),
..Default::default()
})
.await
.map_err(|e| e.to_string())?;
cap_rendering::SharedWgpuDevice {
instance,
adapter,
device,
queue,
is_software_adapter,
}
};

let options = cap_rendering::RenderOptions {
screen_size: cap_project::XY::new(width, height),
camera_size: None,
};

// We need to extract the studio meta from the recording meta
let studio_meta = match &recording_meta.inner {
RecordingMetaInner::Studio(meta) => meta.clone(),
_ => return Err("Invalid recording meta for screenshot".to_string()),
};

let constants = RenderVideoConstants {
_instance: (*instance).clone(),
_adapter: (*adapter).clone(),
queue: (*queue).clone(),
device: (*device).clone(),
let constants = RenderVideoConstants::from_shared_device(
shared,
options,
meta: *studio_meta,
recording_meta: recording_meta.clone(),
background_textures: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
is_software_adapter,
};
*studio_meta,
recording_meta.clone(),
);

let (config_tx, mut config_rx) = watch::channel(loaded_config.unwrap_or_default());

Expand Down
5 changes: 4 additions & 1 deletion apps/desktop/src-tauri/src/tray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,10 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> {
});
}
Ok(TrayItem::Quit) => {
app.exit(0);
let app = app.clone();
tokio::spawn(async move {
crate::request_app_exit(app).await;
});
}
Ok(TrayItem::PreviousItem(path)) => {
handle_previous_item_click(app, &path);
Expand Down
Loading
Loading