gpui: Fix crashes when losing devices while resizing on windows (#42740)

Lukas Wirth created

Fixes ZED-1HC

Release Notes:

- Fixed Zed panicking when moving Zed windows over different screens
associated with different gpu devices on windows

Change summary

crates/gpui/src/platform/windows/directx_atlas.rs    | 13 ++-
crates/gpui/src/platform/windows/directx_renderer.rs | 47 +++++++++++--
crates/gpui/src/platform/windows/events.rs           | 11 ++
crates/gpui/src/platform/windows/platform.rs         | 19 +++++
crates/gpui/src/platform/windows/window.rs           | 11 ++
5 files changed, 81 insertions(+), 20 deletions(-)

Detailed changes

crates/gpui/src/platform/windows/directx_atlas.rs 🔗

@@ -234,11 +234,14 @@ impl DirectXAtlasState {
     }
 
     fn texture(&self, id: AtlasTextureId) -> &DirectXAtlasTexture {
-        let textures = match id.kind {
-            crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
-            crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
-        };
-        textures[id.index as usize].as_ref().unwrap()
+        match id.kind {
+            crate::AtlasTextureKind::Monochrome => &self.monochrome_textures[id.index as usize]
+                .as_ref()
+                .unwrap(),
+            crate::AtlasTextureKind::Polychrome => &self.polychrome_textures[id.index as usize]
+                .as_ref()
+                .unwrap(),
+        }
     }
 }
 

crates/gpui/src/platform/windows/directx_renderer.rs 🔗

@@ -48,6 +48,12 @@ pub(crate) struct DirectXRenderer {
 
     width: u32,
     height: u32,
+
+    /// Whether we want to skip drwaing due to device lost events.
+    ///
+    /// In that case we want to discard the first frame that we draw as we got reset in the middle of a frame
+    /// meaning we lost all the allocated gpu textures and scene resources.
+    skip_draws: bool,
 }
 
 /// Direct3D objects
@@ -167,6 +173,7 @@ impl DirectXRenderer {
             font_info: Self::get_font_info(),
             width: 1,
             height: 1,
+            skip_draws: false,
         })
     }
 
@@ -192,8 +199,13 @@ impl DirectXRenderer {
             }],
         )?;
         unsafe {
-            device_context
-                .ClearRenderTargetView(resources.render_target_view.as_ref().unwrap(), &[0.0; 4]);
+            device_context.ClearRenderTargetView(
+                resources
+                    .render_target_view
+                    .as_ref()
+                    .context("missing render target view")?,
+                &[0.0; 4],
+            );
             device_context
                 .OMSetRenderTargets(Some(slice::from_ref(&resources.render_target_view)), None);
             device_context.RSSetViewports(Some(slice::from_ref(&resources.viewport)));
@@ -283,10 +295,16 @@ impl DirectXRenderer {
         self.globals = globals;
         self.pipelines = pipelines;
         self.direct_composition = direct_composition;
+        self.skip_draws = true;
         Ok(())
     }
 
     pub(crate) fn draw(&mut self, scene: &Scene) -> Result<()> {
+        if self.skip_draws {
+            // skip drawing this frame, we just recovered from a device lost event
+            // and so likely do not have the textures anymore that are required for drawing
+            return Ok(());
+        }
         self.pre_draw()?;
         for batch in scene.batches() {
             match batch {
@@ -306,14 +324,18 @@ impl DirectXRenderer {
                     sprites,
                 } => self.draw_polychrome_sprites(texture_id, sprites),
                 PrimitiveBatch::Surfaces(surfaces) => self.draw_surfaces(surfaces),
-            }.context(format!("scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
-                    scene.paths.len(),
-                    scene.shadows.len(),
-                    scene.quads.len(),
-                    scene.underlines.len(),
-                    scene.monochrome_sprites.len(),
-                    scene.polychrome_sprites.len(),
-                    scene.surfaces.len(),))?;
+            }
+            .context(format!(
+                "scene too large:\
+                {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
+                scene.paths.len(),
+                scene.shadows.len(),
+                scene.quads.len(),
+                scene.underlines.len(),
+                scene.monochrome_sprites.len(),
+                scene.polychrome_sprites.len(),
+                scene.surfaces.len(),
+            ))?;
         }
         self.present()
     }
@@ -352,6 +374,7 @@ impl DirectXRenderer {
         }
 
         resources.recreate_resources(devices, width, height)?;
+
         unsafe {
             devices
                 .device_context
@@ -647,6 +670,10 @@ impl DirectXRenderer {
             }
         })
     }
+
+    pub(crate) fn mark_drawable(&mut self) {
+        self.skip_draws = false;
+    }
 }
 
 impl DirectXResources {

crates/gpui/src/platform/windows/events.rs 🔗

@@ -201,8 +201,10 @@ impl WindowsWindowInner {
         let new_logical_size = device_size.to_pixels(scale_factor);
         let mut lock = self.state.borrow_mut();
         lock.logical_size = new_logical_size;
-        if should_resize_renderer {
-            lock.renderer.resize(device_size).log_err();
+        if should_resize_renderer && let Err(e) = lock.renderer.resize(device_size) {
+            log::error!("Failed to resize renderer, invalidating devices: {}", e);
+            lock.invalidate_devices
+                .store(true, std::sync::atomic::Ordering::Release);
         }
         if let Some(mut callback) = lock.callbacks.resize.take() {
             drop(lock);
@@ -1138,6 +1140,11 @@ impl WindowsWindowInner {
     #[inline]
     fn draw_window(&self, handle: HWND, force_render: bool) -> Option<isize> {
         let mut request_frame = self.state.borrow_mut().callbacks.request_frame.take()?;
+
+        // we are instructing gpui to force render a frame, this will
+        // re-populate all the gpu textures for us so we can resume drawing in
+        // case we disabled drawing earlier due to a device loss
+        self.state.borrow_mut().renderer.mark_drawable();
         request_frame(RequestFrameOptions {
             require_presentation: false,
             force_render,

crates/gpui/src/platform/windows/platform.rs 🔗

@@ -3,7 +3,10 @@ use std::{
     ffi::OsStr,
     path::{Path, PathBuf},
     rc::{Rc, Weak},
-    sync::{Arc, atomic::Ordering},
+    sync::{
+        Arc,
+        atomic::{AtomicBool, Ordering},
+    },
 };
 
 use ::util::{ResultExt, paths::SanitizedPath};
@@ -36,6 +39,9 @@ pub(crate) struct WindowsPlatform {
     text_system: Arc<DirectWriteTextSystem>,
     windows_version: WindowsVersion,
     drop_target_helper: IDropTargetHelper,
+    /// Flag to instruct the `VSyncProvider` thread to invalidate the directx devices
+    /// as resizing them has failed, causing us to have lost at least the render target.
+    invalidate_devices: Arc<AtomicBool>,
     handle: HWND,
     disable_direct_composition: bool,
 }
@@ -162,6 +168,7 @@ impl WindowsPlatform {
             disable_direct_composition,
             windows_version,
             drop_target_helper,
+            invalidate_devices: Arc::new(AtomicBool::new(false)),
         })
     }
 
@@ -195,6 +202,7 @@ impl WindowsPlatform {
             platform_window_handle: self.handle,
             disable_direct_composition: self.disable_direct_composition,
             directx_devices: self.inner.state.borrow().directx_devices.clone().unwrap(),
+            invalidate_devices: self.invalidate_devices.clone(),
         }
     }
 
@@ -247,13 +255,17 @@ impl WindowsPlatform {
         let validation_number = self.inner.validation_number;
         let all_windows = Arc::downgrade(&self.raw_window_handles);
         let text_system = Arc::downgrade(&self.text_system);
+        let invalidate_devices = self.invalidate_devices.clone();
+
         std::thread::Builder::new()
             .name("VSyncProvider".to_owned())
             .spawn(move || {
                 let vsync_provider = VSyncProvider::new();
                 loop {
                     vsync_provider.wait_for_vsync();
-                    if check_device_lost(&directx_device.device) {
+                    if check_device_lost(&directx_device.device)
+                        || invalidate_devices.fetch_and(false, Ordering::Acquire)
+                    {
                         if let Err(err) = handle_gpu_device_lost(
                             &mut directx_device,
                             platform_window.as_raw(),
@@ -877,6 +889,9 @@ pub(crate) struct WindowCreationInfo {
     pub(crate) platform_window_handle: HWND,
     pub(crate) disable_direct_composition: bool,
     pub(crate) directx_devices: DirectXDevices,
+    /// Flag to instruct the `VSyncProvider` thread to invalidate the directx devices
+    /// as resizing them has failed, causing us to have lost at least the render target.
+    pub(crate) invalidate_devices: Arc<AtomicBool>,
 }
 
 struct PlatformWindowCreateContext {

crates/gpui/src/platform/windows/window.rs 🔗

@@ -6,7 +6,7 @@ use std::{
     path::PathBuf,
     rc::{Rc, Weak},
     str::FromStr,
-    sync::{Arc, Once},
+    sync::{Arc, Once, atomic::AtomicBool},
     time::{Duration, Instant},
 };
 
@@ -53,6 +53,9 @@ pub struct WindowsWindowState {
     pub nc_button_pressed: Option<u32>,
 
     pub display: WindowsDisplay,
+    /// Flag to instruct the `VSyncProvider` thread to invalidate the directx devices
+    /// as resizing them has failed, causing us to have lost at least the render target.
+    pub invalidate_devices: Arc<AtomicBool>,
     fullscreen: Option<StyleAndBounds>,
     initial_placement: Option<WindowOpenStatus>,
     hwnd: HWND,
@@ -83,6 +86,7 @@ impl WindowsWindowState {
         min_size: Option<Size<Pixels>>,
         appearance: WindowAppearance,
         disable_direct_composition: bool,
+        invalidate_devices: Arc<AtomicBool>,
     ) -> Result<Self> {
         let scale_factor = {
             let monitor_dpi = unsafe { GetDpiForWindow(hwnd) } as f32;
@@ -138,6 +142,7 @@ impl WindowsWindowState {
             fullscreen,
             initial_placement,
             hwnd,
+            invalidate_devices,
         })
     }
 
@@ -211,6 +216,7 @@ impl WindowsWindowInner {
             context.min_size,
             context.appearance,
             context.disable_direct_composition,
+            context.invalidate_devices.clone(),
         )?);
 
         Ok(Rc::new(Self {
@@ -361,6 +367,7 @@ struct WindowCreateContext {
     appearance: WindowAppearance,
     disable_direct_composition: bool,
     directx_devices: DirectXDevices,
+    invalidate_devices: Arc<AtomicBool>,
 }
 
 impl WindowsWindow {
@@ -380,6 +387,7 @@ impl WindowsWindow {
             platform_window_handle,
             disable_direct_composition,
             directx_devices,
+            invalidate_devices,
         } = creation_info;
         register_window_class(icon);
         let hide_title_bar = params
@@ -440,6 +448,7 @@ impl WindowsWindow {
             appearance,
             disable_direct_composition,
             directx_devices,
+            invalidate_devices,
         };
         let creation_result = unsafe {
             CreateWindowExW(