gpui_wgpu: Fall back when color atlas formats are unavailable

Oleksiy Syvokon created

Avoid crashes on devices that cannot use the preferred color atlas texture format by selecting a supported fallback during renderer setup and recovery.

Change summary

crates/gpui_wgpu/src/wgpu_atlas.rs    | 68 +++++++++++++++++++++++-----
crates/gpui_wgpu/src/wgpu_renderer.rs | 46 ++++++++++++++++++
2 files changed, 100 insertions(+), 14 deletions(-)

Detailed changes

crates/gpui_wgpu/src/wgpu_atlas.rs 🔗

@@ -31,6 +31,7 @@ struct WgpuAtlasState {
     device: Arc<wgpu::Device>,
     queue: Arc<wgpu::Queue>,
     max_texture_size: u32,
+    color_texture_format: wgpu::TextureFormat,
     storage: WgpuAtlasStorage,
     tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
     pending_uploads: Vec<PendingUpload>,
@@ -41,12 +42,17 @@ pub struct WgpuTextureInfo {
 }
 
 impl WgpuAtlas {
-    pub fn new(device: Arc<wgpu::Device>, queue: Arc<wgpu::Queue>) -> Self {
+    pub fn new(
+        device: Arc<wgpu::Device>,
+        queue: Arc<wgpu::Queue>,
+        color_texture_format: wgpu::TextureFormat,
+    ) -> Self {
         let max_texture_size = device.limits().max_texture_dimension_2d;
         WgpuAtlas(Mutex::new(WgpuAtlasState {
             device,
             queue,
             max_texture_size,
+            color_texture_format,
             storage: WgpuAtlasStorage::default(),
             tiles_by_key: Default::default(),
             pending_uploads: Vec::new(),
@@ -68,10 +74,16 @@ impl WgpuAtlas {
 
     /// Handles device lost by clearing all textures and cached tiles.
     /// The atlas will lazily recreate textures as needed on subsequent frames.
-    pub fn handle_device_lost(&self, device: Arc<wgpu::Device>, queue: Arc<wgpu::Queue>) {
+    pub fn handle_device_lost(
+        &self,
+        device: Arc<wgpu::Device>,
+        queue: Arc<wgpu::Queue>,
+        color_texture_format: wgpu::TextureFormat,
+    ) {
         let mut lock = self.0.lock();
         lock.device = device;
         lock.queue = queue;
+        lock.color_texture_format = color_texture_format;
         lock.storage = WgpuAtlasStorage::default();
         lock.tiles_by_key.clear();
         lock.pending_uploads.clear();
@@ -167,8 +179,7 @@ impl WgpuAtlasState {
         let size = min_size.min(&max_atlas_size).max(&DEFAULT_ATLAS_SIZE);
         let format = match kind {
             AtlasTextureKind::Monochrome => wgpu::TextureFormat::R8Unorm,
-            AtlasTextureKind::Subpixel => wgpu::TextureFormat::Bgra8Unorm,
-            AtlasTextureKind::Polychrome => wgpu::TextureFormat::Bgra8Unorm,
+            AtlasTextureKind::Subpixel | AtlasTextureKind::Polychrome => self.color_texture_format,
         };
 
         let texture = self.device.create_texture(&wgpu::TextureDescriptor {
@@ -221,11 +232,14 @@ impl WgpuAtlasState {
     }
 
     fn upload_texture(&mut self, id: AtlasTextureId, bounds: Bounds<DevicePixels>, bytes: &[u8]) {
-        self.pending_uploads.push(PendingUpload {
-            id,
-            bounds,
-            data: bytes.to_vec(),
-        });
+        let data = self
+            .storage
+            .get(id)
+            .map(|texture| swizzle_upload_data(bytes, texture.format))
+            .unwrap_or_else(|| bytes.to_vec());
+
+        self.pending_uploads
+            .push(PendingUpload { id, bounds, data });
     }
 
     fn flush_uploads(&mut self) {
@@ -341,7 +355,7 @@ impl WgpuAtlasTexture {
     fn bytes_per_pixel(&self) -> u8 {
         match self.format {
             wgpu::TextureFormat::R8Unorm => 1,
-            wgpu::TextureFormat::Bgra8Unorm => 4,
+            wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Rgba8Unorm => 4,
             _ => 4,
         }
     }
@@ -355,6 +369,19 @@ impl WgpuAtlasTexture {
     }
 }
 
+fn swizzle_upload_data(bytes: &[u8], format: wgpu::TextureFormat) -> Vec<u8> {
+    match format {
+        wgpu::TextureFormat::Rgba8Unorm => {
+            let mut data = bytes.to_vec();
+            for pixel in data.chunks_exact_mut(4) {
+                pixel.swap(0, 2);
+            }
+            data
+        }
+        _ => bytes.to_vec(),
+    }
+}
+
 #[cfg(all(test, not(target_family = "wasm")))]
 mod tests {
     use super::*;
@@ -400,7 +427,7 @@ mod tests {
     fn before_frame_skips_uploads_for_removed_texture() -> anyhow::Result<()> {
         let (device, queue) = test_device_and_queue()?;
 
-        let atlas = WgpuAtlas::new(device, queue);
+        let atlas = WgpuAtlas::new(device, queue, wgpu::TextureFormat::Bgra8Unorm);
         let key = AtlasKey::Image(RenderImageParams {
             image_id: ImageId(1),
             frame_index: 0,
@@ -417,7 +444,24 @@ mod tests {
             .expect("tile should be created");
         atlas.remove(&key);
         atlas.before_frame();
-
         Ok(())
     }
+
+    #[test]
+    fn swizzle_upload_data_preserves_bgra_uploads() {
+        let input = vec![0x10, 0x20, 0x30, 0x40];
+        assert_eq!(
+            swizzle_upload_data(&input, wgpu::TextureFormat::Bgra8Unorm),
+            input
+        );
+    }
+
+    #[test]
+    fn swizzle_upload_data_converts_bgra_to_rgba() {
+        let input = vec![0x10, 0x20, 0x30, 0x40, 0xAA, 0xBB, 0xCC, 0xDD];
+        assert_eq!(
+            swizzle_upload_data(&input, wgpu::TextureFormat::Rgba8Unorm),
+            vec![0x30, 0x20, 0x10, 0x40, 0xCC, 0xBB, 0xAA, 0xDD]
+        );
+    }
 }

crates/gpui_wgpu/src/wgpu_renderer.rs 🔗

@@ -161,6 +161,40 @@ impl WgpuRenderer {
             .expect("GPU resources not available")
     }
 
+    fn select_color_atlas_texture_format(
+        adapter: &wgpu::Adapter,
+    ) -> anyhow::Result<wgpu::TextureFormat> {
+        let required_usages = wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST;
+        let bgra_features = adapter.get_texture_format_features(wgpu::TextureFormat::Bgra8Unorm);
+        if bgra_features.allowed_usages.contains(required_usages) {
+            return Ok(wgpu::TextureFormat::Bgra8Unorm);
+        }
+
+        let rgba_features = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba8Unorm);
+        if rgba_features.allowed_usages.contains(required_usages) {
+            let info = adapter.get_info();
+            warn!(
+                "Adapter {} ({:?}) does not support Bgra8Unorm atlas textures with usages {:?}; \
+                 falling back to Rgba8Unorm atlas textures.",
+                info.name, info.backend, required_usages,
+            );
+            return Ok(wgpu::TextureFormat::Rgba8Unorm);
+        }
+
+        let info = adapter.get_info();
+        Err(anyhow::anyhow!(
+            "Adapter {} ({:?}, device={:#06x}) does not support a usable color atlas texture \
+             format with usages {:?}. Bgra8Unorm allowed usages: {:?}; \
+             Rgba8Unorm allowed usages: {:?}.",
+            info.name,
+            info.backend,
+            info.device,
+            required_usages,
+            bgra_features.allowed_usages,
+            rgba_features.allowed_usages,
+        ))
+    }
+
     /// Creates a new WgpuRenderer from raw window handles.
     ///
     /// The `gpu_context` is a shared reference that coordinates GPU context across
@@ -217,9 +251,11 @@ impl WgpuRenderer {
             None => ctx_ref.insert(WgpuContext::new(instance, &surface, compositor_gpu)?),
         };
 
+        let color_atlas_texture_format = Self::select_color_atlas_texture_format(&context.adapter)?;
         let atlas = Arc::new(WgpuAtlas::new(
             Arc::clone(&context.device),
             Arc::clone(&context.queue),
+            color_atlas_texture_format,
         ));
 
         Self::new_internal(
@@ -243,9 +279,11 @@ impl WgpuRenderer {
             .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
             .map_err(|e| anyhow::anyhow!("Failed to create surface: {e}"))?;
 
+        let color_atlas_texture_format = Self::select_color_atlas_texture_format(&context.adapter)?;
         let atlas = Arc::new(WgpuAtlas::new(
             Arc::clone(&context.device),
             Arc::clone(&context.queue),
+            color_atlas_texture_format,
         ));
 
         Self::new_internal(None, context, surface, config, None, atlas)
@@ -1805,10 +1843,14 @@ impl WgpuRenderer {
         let gpu_context = Rc::clone(gpu_context);
         let ctx_ref = gpu_context.borrow();
         let context = ctx_ref.as_ref().expect("context should exist");
+        let color_atlas_texture_format = Self::select_color_atlas_texture_format(&context.adapter)?;
 
         self.resources = None;
-        self.atlas
-            .handle_device_lost(Arc::clone(&context.device), Arc::clone(&context.queue));
+        self.atlas.handle_device_lost(
+            Arc::clone(&context.device),
+            Arc::clone(&context.queue),
+            color_atlas_texture_format,
+        );
 
         *self = Self::new_internal(
             Some(gpu_context.clone()),