Skip to content

GPU context cleanup, exit guards, and error handling fixes#1661

Merged
richiemcilroy merged 19 commits intomainfrom
misc-fixes-2
Mar 17, 2026
Merged

GPU context cleanup, exit guards, and error handling fixes#1661
richiemcilroy merged 19 commits intomainfrom
misc-fixes-2

Conversation

@richiemcilroy
Copy link
Member

@richiemcilroy richiemcilroy commented Mar 16, 2026

  • Centralizes wgpu instance creation and software adapter detection into cap_rendering, eliminating duplicated GPU init logic across desktop and rendering crates
    • Adds app_is_exiting guards to background task loops and window event handlers to prevent spurious errors during shutdown
    • Replaces unwrap() calls with proper error propagation in editor IPC commands and dock icon visibility checks
    • Uses request_app_exit for tray quit instead of immediate app.exit(0)
    • Adds window content protection diagnostics on Windows (display affinity verification)
    • Skips performance test suite on Windows CI when only a software adapter is available

Greptile Summary

This PR delivers three tightly-scoped improvements: (1) centralizing wgpu instance creation and software-adapter detection into cap_rendering so all crates share the same Windows DX12-probe logic, (2) adding app_is_exiting guards to background task loops and window event handlers to prevent spurious errors during shutdown, and (3) a collection of error-handling hardening changes (replacing unwrap() with proper propagation, using request_app_exit from the tray, adding Windows display-affinity diagnostics, and skipping the performance suite on CI when only a software adapter is present).

Key changes:

  • crates/rendering/src/lib.rs: New create_wgpu_instance, is_software_wgpu_adapter, and probe_software_adapter public functions extract all GPU-selection logic from desktop/rendering consumers.
  • apps/desktop/src-tauri/src/gpu_context.rs: Drops 26 lines of Windows/non-Windows #[cfg] branching; now a one-liner call to cap_rendering::create_wgpu_instance().
  • apps/desktop/src-tauri/src/lib.rs: app_is_exiting helper queries AppExitState safely via try_state (no panic before state registration). Guards added at loop tops in four background tasks; CloseRequested/Moved/Focused/Destroyed window events short-circuit during exit. Two unwrap() calls in IPC handlers replaced with map_err(|e| e.to_string())?. The dock-icon predicate now uses unwrap_or(false) instead of panicking on unknown window labels.
  • apps/desktop/src-tauri/src/tray.rs: Tray quit now spawns request_app_exit asynchronously; the compare_exchange in AppExitState::begin() makes repeated clicks safe.
  • apps/desktop/src-tauri/src/windows.rs: Windows-only log_window_content_protection diagnostic logs the actual vs. expected WDA_* affinity value and warns on OS builds below 19041 where WDA_EXCLUDEFROMCAPTURE is unsupported.
  • crates/cap-test/src/suites/performance.rs: probe_windows_ci_software_adapter early-returns pre-built "skipped" results when GITHUB_ACTIONS=true and only a software renderer is available, avoiding false CI failures.
  • apps/web/…: TypeScript type-safety fixes across 20+ test files (! non-null assertions, optional chaining on IntersectionObserver entries, ! guards on array-index accesses), S3Buckets.getBucketAccess(Option.none()) call-site updates, and branded-type ID construction in DB queries.

Confidence Score: 4/5

  • This PR is safe to merge; changes are well-scoped refactors with no regressions identified.
  • The core Rust changes are behaviorally equivalent to or strictly better than what they replace (centralized GPU init, graceful shutdown guards, ? over unwrap). The tray-quit path and Windows diagnostic code add new surface area but are correctly guarded and do not affect the happy path on macOS. The TypeScript changes are pure type-safety improvements. The one point deducted is for the minor inconsistency in exit-guard placement (top-only vs. top-and-bottom) across the watcher loops, and for the Windows content-protection diagnostic path which exercises unsafe Win32 APIs in a new way that cannot be fully tested in CI.
  • No files require special attention beyond the minor guard inconsistency noted in apps/desktop/src-tauri/src/lib.rs.

Important Files Changed

Filename Overview
crates/rendering/src/lib.rs Adds three new public functions (create_wgpu_instance, is_software_wgpu_adapter, probe_software_adapter) that centralize GPU initialization logic previously duplicated across crates. Also adds from_shared_device constructor and adapter_name() accessor to RenderVideoConstants, and introduces the private adapter_name field to avoid repeated get_info() calls. Changes are well-structured with no issues found.
apps/desktop/src-tauri/src/gpu_context.rs Removes the duplicated Windows DX12-vs-default backend selection logic and replaces it with a call to the centralized cap_rendering::create_wgpu_instance(). Also improves software adapter detection by using the shared is_software_wgpu_adapter helper and logs the device_type field for richer diagnostics. Clean improvement with no issues.
apps/desktop/src-tauri/src/lib.rs Adds app_is_exiting helper that safely queries AppExitState via try_state (returning false when not yet registered). Guards are added at the top of four background task loops and inside WindowEvent::Destroyed. The CloseRequested/Moved/Focused events are short-circuited at the event-handler entry. Two unwrap() calls in editor IPC commands are replaced with proper map_err+? propagation. Note: spawn_devices_snapshot_emitter has guards at both the top and bottom of its loop (good), while spawn_microphone_watcher and spawn_camera_watcher only guard at the top — this is not a bug but is slightly inconsistent.
apps/desktop/src-tauri/src/screenshot_editor.rs Fallback GPU path (when shared GPU context is unavailable) now uses cap_rendering::create_wgpu_instance() instead of the old bare wgpu::InstanceDescriptor::default(), completing the centralization goal. GPU resources are wrapped into cap_rendering::SharedWgpuDevice and passed to the new RenderVideoConstants::from_shared_device constructor. The is_software_adapter flag is now correctly computed via is_software_wgpu_adapter. Clean refactor, no issues.
apps/desktop/src-tauri/src/tray.rs Replaces synchronous app.exit(0) on tray-quit with a spawned async task calling request_app_exit. This ensures the graceful shutdown path (cleanup, exit watchdog, etc.) is invoked, and double-clicks are safe because AppExitState::begin() uses compare_exchange.
apps/desktop/src-tauri/src/windows.rs Adds Windows-only log_window_content_protection diagnostic function that calls GetWindowDisplayAffinity and logs a warning on mismatch. Hoists should_protect computation before window builder to allow reuse. Also adds a Windows-build version check (build < 19041) warning for WDA_EXCLUDEFROMCAPTURE unsupported on older builds. The display_affinity_name helper uses hardcoded integer literals for the match arms, which is acceptable since the Windows API constants cannot be used as const pattern values in Rust. The dock-icon fix (unwrap_or(false) vs panic) is correct and safe.
crates/cap-test/src/suites/performance.rs Adds probe_windows_ci_software_adapter (no-op on non-Windows, only active when GITHUB_ACTIONS=true on Windows) that calls cap_rendering::probe_software_adapter() and returns pre-built "skipped" results if a software adapter is detected, avoiding meaningless performance gate failures on CI runners without hardware GPUs. Well structured; the non-Windows stub uses _recording_path correctly to suppress unused-variable warnings.
apps/web/app/api/developer/sdk/v1/[...route]/upload.ts Updates four call-sites of S3Buckets.getBucketAccess() to pass Option.none(), matching a changed function signature in the backend layer. Adds Option import from effect. Routine API-compatibility update.

Sequence Diagram

sequenceDiagram
    participant App as Desktop App
    participant GpuCtx as gpu_context.rs
    participant Rendering as cap_rendering
    participant WGPU as wgpu

    Note over App,WGPU: Shared GPU Context Initialization (centralized)
    App->>GpuCtx: init_gpu_inner()
    GpuCtx->>Rendering: create_wgpu_instance()
    alt Windows
        Rendering->>WGPU: Instance::new(DX12)
        WGPU-->>Rendering: dx12_instance
        Rendering->>WGPU: request_adapter(DX12, high-perf)
        alt DX12 available
            WGPU-->>Rendering: Ok(adapter)
            Rendering-->>GpuCtx: dx12_instance
        else DX12 not available
            WGPU-->>Rendering: Err
            Rendering->>WGPU: Instance::new(default)
            WGPU-->>Rendering: default_instance
            Rendering-->>GpuCtx: default_instance
        end
    else Non-Windows
        Rendering->>WGPU: Instance::new(default)
        WGPU-->>Rendering: instance
        Rendering-->>GpuCtx: instance
    end
    GpuCtx->>WGPU: request_adapter(high-perf, no-fallback)
    alt Hardware adapter found
        WGPU-->>GpuCtx: Ok(hardware_adapter)
        GpuCtx->>Rendering: is_software_wgpu_adapter(info)
        Rendering-->>GpuCtx: bool (DeviceType::Cpu or MBRD name)
        GpuCtx-->>GpuCtx: (adapter, is_software)
    else No hardware adapter
        WGPU-->>GpuCtx: Err
        GpuCtx->>WGPU: request_adapter(low-power, force-fallback)
        WGPU-->>GpuCtx: Ok(software_adapter)
        GpuCtx-->>GpuCtx: (software_adapter, true)
    end
    GpuCtx->>WGPU: request_device(adapter)
    WGPU-->>GpuCtx: (device, queue)
    GpuCtx-->>App: SharedGpuContext

    Note over App,WGPU: Graceful Shutdown via Tray Quit
    App->>App: TrayItem::Quit clicked
    App->>App: tokio::spawn(request_app_exit)
    App->>App: AppExitState::begin() → compare_exchange
    App->>App: spawn_exit_watchdog (force-exit after 8s)
    App->>App: cleanup_app_resources_for_exit
    loop Background tasks
        App->>App: app_is_exiting() → true → break
    end
    App->>App: app.exit(0)
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/lib.rs
Line: 884-956

Comment:
**Inconsistent exit-guard placement in watcher loops**

`spawn_devices_snapshot_emitter` correctly places exit guards at both the **top** and **bottom** of its loop (so a long sleep can be interrupted promptly), but `spawn_microphone_watcher` (line ~1147) and `spawn_camera_watcher` (line ~1208) only guard at the **top**. For the microphone and camera watchers that sleep 1 second per iteration this is harmless, but it creates a subtle inconsistency: during shutdown those loops can run one extra full sleep before exiting.

Consider adding a guard after the sleep in those watchers as well, mirroring the pattern used here:

```rust
tokio::time::sleep(Duration::from_secs(1)).await;

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

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: acc8ada

@paragon-review
Copy link

Paragon Summary

This pull request review identified 1 issue across 1 category in 8 files. The review analyzed code changes, potential bugs, security vulnerabilities, performance issues, and code quality concerns using automated analysis tools.

This PR centralizes GPU context initialization into cap_rendering and adds comprehensive exit guards and error handling improvements to prevent spurious errors during app shutdown, while also adding Windows-specific display affinity diagnostics and CI test skipping for software-only adapters.

Key changes:

  • Centralizes wgpu GPU init logic into cap_rendering crate
  • Adds app_is_exiting guards to prevent shutdown errors
  • Replaces unwrap() with proper error propagation in editor IPC and dock icon code
  • Uses request_app_exit for tray quit instead of immediate exit
  • Skips Windows CI performance tests when only software adapter available

Confidence score: 4/5

  • This PR has low-moderate risk with 1 medium-priority issue identified
  • Score reflects code quality concerns and maintainability issues
  • Consider addressing medium-priority findings to improve code quality

8 files reviewed, 1 comment

Severity breakdown: Medium: 1

Dashboard

@richiemcilroy
Copy link
Member Author

@greptileai please re-review this pr.

@richiemcilroy richiemcilroy merged commit a42edb3 into main Mar 17, 2026
17 checks passed
return `/s/${notification.videoId}`;
}

return `/s/${(notification as APINotification).videoId}`;
Copy link

Choose a reason for hiding this comment

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

The (notification as APINotification) cast is a bit of a smell here (usually comes from notification being never after an exhaustive switch). Might be clearer to keep the fallback via a default arm and drop the post-switch return.

Suggested change
return `/s/${(notification as APINotification).videoId}`;
function getLink(notification: APINotification) {
switch (notification.type) {
case "reply":
return `/s/${notification.videoId}/?reply=${notification.comment.id}`;
case "comment":
case "reaction":
return `/s/${notification.videoId}/?comment=${notification.comment.id}`;
case "view":
case "anon_view":
return `/s/${notification.videoId}`;
default:
return `/s/${notification.videoId}`;
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant