Skip to content

fix(windows): prevent DirectComposition ghost artifacts on recording start/stop#1971

Merged
richiemcilroy merged 1 commit into
CapSoftware:mainfrom
ManthanNimodiya:fix/windows-ghost-overlay-close
Jul 2, 2026
Merged

fix(windows): prevent DirectComposition ghost artifacts on recording start/stop#1971
richiemcilroy merged 1 commit into
CapSoftware:mainfrom
ManthanNimodiya:fix/windows-ghost-overlay-close

Conversation

@ManthanNimodiya

@ManthanNimodiya ManthanNimodiya commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

On Windows, window.hide() doesn't release the DirectComposition surface, leaving a ghost composited on screen.

  • TargetSelectOverlay: call window.close() instead of hide() on Windows to fully release the DComp surface
  • Main window: call window.minimize() instead of hide() on Windows — minimized windows have no DComp surface, and the existing unminimize() in handle_recording_end restores it correctly after recording ends

Fixes the transparent overlay ghost and white blank box reported on Windows.

Greptile Summary

This PR fixes DirectComposition ghost artifacts on Windows by replacing hide() calls with close() for overlay windows and minimize() for the main window, preventing the DComp surface from persisting on screen during recording start/stop.

  • general_settings.rs: Self::Close now calls minimize() on Windows so the main window disappears from view without leaving a DComp surface; the existing unminimize() at recording end restores it correctly.
  • recording.rs / target_select_overlay.rs: TargetSelectOverlay windows are now fully destroyed (close()) on Windows instead of just hidden, releasing the transparent DComp surface that was compositing as a ghost overlay.

Confidence Score: 4/5

The fix is safe to merge and correctly addresses the reported ghost artifact on Windows recording start/stop.

The three changed sites correctly use close()/minimize() on Windows, and the camera-cleanup side-effect triggered by the new Destroyed event path is guarded against harm by existing checks. One overlay hide_overlay() call in the display-disconnect path of target_select_overlay.rs is left unfixed, so the DComp artifact can still appear in that less common scenario.

apps/desktop/src-tauri/src/target_select_overlay.rs — the hide_overlay() call around line 101 (stale-display cleanup inside open_target_select_overlay_windows) was not updated with the Windows fix.

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/general_settings.rs Self::Close now calls minimize() instead of hide() on Windows to avoid DComp ghost artifact; on non-Windows behavior is unchanged. The two variants (Close and Minimise) are now identical on Windows, and window.unminimize() at recording end already handles restoration.
apps/desktop/src-tauri/src/recording.rs TargetSelectOverlay windows are now closed (not hidden) on Windows during recording end; explicit fm.destroy() call is redundant with the Destroyed event handler but is idempotent. Triggers an unintended cleanup_camera_after_overlay_close spawn, which is guarded against harm.
apps/desktop/src-tauri/src/target_select_overlay.rs close_target_select_overlay_windows is fixed for Windows, but the parallel hide_overlay() call inside open_target_select_overlay_windows (~line 101) for stale displays is missing the same fix, leaving a residual DComp artifact path on Windows.

Comments Outside Diff (2)

  1. apps/desktop/src-tauri/src/target_select_overlay.rs, line 93-103 (link)

    P2 Incomplete Windows fix — DComp artifact still present on display disconnect

    This hide_overlay() call (reached when an overlay's display disappears while the overlay is open) skips the Windows close() fix applied in the other three sites. On Windows, hiding instead of closing leaves the ghost DComp surface on screen for the stale display's overlay. The fix should mirror the pattern used in close_target_select_overlay_windows below: call window.close() on Windows and hide_overlay() elsewhere.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src-tauri/src/target_select_overlay.rs
    Line: 93-103
    
    Comment:
    **Incomplete Windows fix — DComp artifact still present on display disconnect**
    
    This `hide_overlay()` call (reached when an overlay's display disappears while the overlay is open) skips the Windows `close()` fix applied in the other three sites. On Windows, hiding instead of closing leaves the ghost DComp surface on screen for the stale display's overlay. The fix should mirror the pattern used in `close_target_select_overlay_windows` below: call `window.close()` on Windows and `hide_overlay()` elsewhere.
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. apps/desktop/src-tauri/src/recording.rs, line 3020-3026 (link)

    P2 close() triggers the Destroyed event handler, causing a redundant fm.destroy() and an unintended cleanup_camera_after_overlay_close spawn

    The Destroyed event handler in lib.rs (~line 5373) fires when window.close() is called on Windows, which both calls focus_manager.destroy() again (redundant but idempotent) and spawns cleanup_camera_after_overlay_close. That function wasn't designed to run during recording-end cleanup: it checks is_recording_active_or_pending() (false at this point) and then proceeds unless a camera window is still present. The camera window exists-but-hidden at this point (line 3031 hides it), so CapWindowId::Camera.get(&app).is_some() returns true and prevents actual harm — but this is a fragile guard that may not hold if the camera-window lifecycle changes later. The explicit fm.destroy() call after window.close() is also now redundant: the Destroyed event handler already handles it.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src-tauri/src/recording.rs
    Line: 3020-3026
    
    Comment:
    **`close()` triggers the `Destroyed` event handler, causing a redundant `fm.destroy()` and an unintended `cleanup_camera_after_overlay_close` spawn**
    
    The `Destroyed` event handler in `lib.rs` (~line 5373) fires when `window.close()` is called on Windows, which both calls `focus_manager.destroy()` again (redundant but idempotent) and spawns `cleanup_camera_after_overlay_close`. That function wasn't designed to run during recording-end cleanup: it checks `is_recording_active_or_pending()` (false at this point) and then proceeds unless a camera window is still present. The camera window exists-but-hidden at this point (line 3031 hides it), so `CapWindowId::Camera.get(&app).is_some()` returns `true` and prevents actual harm — but this is a fragile guard that may not hold if the camera-window lifecycle changes later. The explicit `fm.destroy()` call after `window.close()` is also now redundant: the Destroyed event handler already handles it.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
apps/desktop/src-tauri/src/target_select_overlay.rs:93-103
**Incomplete Windows fix — DComp artifact still present on display disconnect**

This `hide_overlay()` call (reached when an overlay's display disappears while the overlay is open) skips the Windows `close()` fix applied in the other three sites. On Windows, hiding instead of closing leaves the ghost DComp surface on screen for the stale display's overlay. The fix should mirror the pattern used in `close_target_select_overlay_windows` below: call `window.close()` on Windows and `hide_overlay()` elsewhere.

### Issue 2 of 2
apps/desktop/src-tauri/src/recording.rs:3020-3026
**`close()` triggers the `Destroyed` event handler, causing a redundant `fm.destroy()` and an unintended `cleanup_camera_after_overlay_close` spawn**

The `Destroyed` event handler in `lib.rs` (~line 5373) fires when `window.close()` is called on Windows, which both calls `focus_manager.destroy()` again (redundant but idempotent) and spawns `cleanup_camera_after_overlay_close`. That function wasn't designed to run during recording-end cleanup: it checks `is_recording_active_or_pending()` (false at this point) and then proceeds unless a camera window is still present. The camera window exists-but-hidden at this point (line 3031 hides it), so `CapWindowId::Camera.get(&app).is_some()` returns `true` and prevents actual harm — but this is a fragile guard that may not hold if the camera-window lifecycle changes later. The explicit `fm.destroy()` call after `window.close()` is also now redundant: the Destroyed event handler already handles it.

Reviews (1): Last reviewed commit: "fix(windows): use close/minimize instead..." | Re-trigger Greptile

Comment on lines +91 to +94
#[cfg(windows)]
return window.minimize();
#[cfg(not(windows))]
window.hide()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Small thing: you can avoid the early return here so this match arm stays expression-based (makes it harder to accidentally skip future logic added after the match).

Suggested change
#[cfg(windows)]
return window.minimize();
#[cfg(not(windows))]
window.hide()
#[cfg(windows)]
window.minimize()
#[cfg(not(windows))]
window.hide()

Comment on lines +330 to +331
#[cfg(windows)]
let _ = window.close();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Since this is a Windows-only compositor issue, it might be worth logging close() failures so we have signal if this ever regresses (same pattern appears in handle_recording_end).

Suggested change
#[cfg(windows)]
let _ = window.close();
#[cfg(windows)]
if let Err(err) = window.close() {
debug!(?err, "Failed to close target-select overlay window");
}

@richiemcilroy richiemcilroy merged commit 7f3892e into CapSoftware:main Jul 2, 2026
12 of 17 checks passed
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.

2 participants