Repository: gfx-rs/wgpu (Rust)

Issue:
Crash when creating multiple GL devices on wayland/surfaceless.
**Description**
If you hold multiple devices with a GL backend on a wayland or surfaceless platform, you'll get a crash when they drop, citing `BadDisplay`:
```
called `Result::unwrap()` on an `Err` value: BadDisplay
stack backtrace:
   0: rust_begin_unwind
             at /rustc/2c9c764110ebbaffa2e248628ef8da36b69727bf/library/std/src/panicking.rs:647:5
   1: core::panicking::panic_fmt
             at /rustc/2c9c764110ebbaffa2e248628ef8da36b69727bf/library/core/src/panicking.rs:72:14
   2: core::result::unwrap_failed
             at /rustc/2c9c764110ebbaffa2e248628ef8da36b69727bf/library/core/src/result.rs:1649:5
   3: core::result::Result<T,E>::unwrap
             at /rustc/2c9c764110ebbaffa2e248628ef8da36b69727bf/library/core/src/result.rs:1073:23
   4: wgpu_hal::gles::egl::EglContext::make_current
             at /home/ashdnazg/.cargo/registry/src/index.crates.io-6f17d22bba15001f/wgpu-hal-0.19.3/src/gles/egl.rs:293:9
   5: wgpu_hal::gles::egl::AdapterContext::lock::{{closure}}
             at /home/ashdnazg/.cargo/registry/src/index.crates.io-6f17d22bba15001f/wgpu-hal-0.19.3/src/gles/egl.rs:407:13
   6: core::option::Option<T>::map
             at /rustc/2c9c764110ebbaffa2e248628ef8da36b69727bf/library/core/src/option.rs:1072:29
   7: wgpu_hal::gles::egl::AdapterContext::lock
             at /home/ashdnazg/.cargo/registry/src/index.crates.io-6f17d22bba15001f/wgpu-hal-0.19.3/src/gles/egl.rs:406:19
   8: wgpu_hal::gles::device::<impl wgpu_hal::Device<wgpu_hal::gles::Api> for wgpu_hal::gles::Device>::destroy_buffer
             at /home/ashdnazg/.cargo/registry/src/index.crates.io-6f17d22bba15001f/wgpu-hal-0.19.3/src/gles/device.rs:622:23
   9: <wgpu_core::device::resource::Device<A> as core::ops::drop::Drop>::drop
             at /home/ashdnazg/.cargo/registry/src/index.crates.io-6f17d22bba15001f/wgpu-core-0.19.3/src/device/resource.rs:155:13
```

I already investigated this and found the underlying issue.

The GL backend's context gets a display using [eglGetPlatformDisplay](https://registry.khronos.org/EGL/sdk/docs/man/html/eglGetPlatformDisplay.xhtml), whose documentation specifies:
> Multiple calls made to eglGetPlatformDisplay with the same parameters will return the same EGLDisplay handle.

Later, it initialises it using [eglInitialize](https://registry.khronos.org/EGL/sdk/docs/man/html/eglInitialize.xhtml) which comes with the following fine print:
> Initializing an already initialized EGL display connection has no effect

When dropped, it calls [eglTerminate](https://registry.khronos.org/EGL/sdk/docs/man/html/eglTerminate.xhtml).

The first context to drop kills the display, causing all subsequent egl calls of the other contexts to fail, since they share the same display.

This doesn't happen in X11, since `eglGetPlatformDisplay` is called with a distinct `native_display` parameter for every new device.

**Repro steps**
```rust
    #[tokio::test]
    async fn it_crashes() {
        async fn default_device_and_queue() -> (wgpu::Device, wgpu::Queue) {
            let _ = env_logger::builder().is_test(true).try_init();
            let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
                backends: wgpu::Backends::GL,
                flags: wgpu::InstanceFlags::debugging(),
                ..Default::default()
            });

            let adapter = instance
                .request_adapter(&wgpu::RequestAdapterOptions::default())
                .await
                .unwrap();

            adapter.request_device(&wgpu::DeviceDescriptor::default(), None).await.unwrap()
        }

        let _ = vec![default_device_and_queue().await, default_device_and_queue().await];
        // The above line crashes.
    }
```

**Expected vs observed behavior**
AFAIK it should be allowed to hold multiple devices/adapters.

**Platform**
Ubuntu 22.04, wgpu 0.19.3



Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index be2adbc4e3a..69f1369318d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -41,7 +41,7 @@ Bottom level categories:
 
 ### Major Changes
 
-#### Vendored WebGPU Bindings from `web_sys` 
+#### Vendored WebGPU Bindings from `web_sys`
 
 **`--cfg=web_sys_unstable_apis` is no longer needed in your `RUSTFLAGS` to compile for WebGPU!!!**
 
@@ -164,6 +164,7 @@ By @cwfitzgerald in [#5325](https://github.com/gfx-rs/wgpu/pull/5325).
 
 - Fixes for being able to use an OpenGL 4.1 core context provided by macOS with wgpu. By @bes in [#5331](https://github.com/gfx-rs/wgpu/pull/5331).
 - Don't create a program for shader-clearing if that workaround isn't required. By @Dinnerbone in [#5348](https://github.com/gfx-rs/wgpu/pull/5348).
+- Fix crash when holding multiple devices on wayland/surfaceless. By @ashdnazg in [#5351](https://github.com/gfx-rs/wgpu/pull/5351).
 
 #### Vulkan
 
diff --git a/wgpu-hal/src/gles/egl.rs b/wgpu-hal/src/gles/egl.rs
index 874ad1729f7..f4bfcf5487e 100644
--- a/wgpu-hal/src/gles/egl.rs
+++ b/wgpu-hal/src/gles/egl.rs
@@ -1,7 +1,8 @@
 use glow::HasContext;
+use once_cell::sync::Lazy;
 use parking_lot::{Mutex, MutexGuard, RwLock};
 
-use std::{ffi, os::raw, ptr, rc::Rc, sync::Arc, time::Duration};
+use std::{collections::HashMap, ffi, os::raw, ptr, rc::Rc, sync::Arc, time::Duration};
 
 /// The amount of time to wait while trying to obtain a lock to the adapter context
 const CONTEXT_LOCK_TIMEOUT_SECS: u64 = 1;
@@ -432,6 +433,45 @@ struct Inner {
     srgb_kind: SrgbFrameBufferKind,
 }
 
+// Different calls to `eglGetPlatformDisplay` may return the same `Display`, making it a global
+// state of all our `EglContext`s. This forces us to track the number of such context to prevent
+// terminating the display if it's currently used by another `EglContext`.
+static DISPLAYS_REFERENCE_COUNT: Lazy<Mutex<HashMap<usize, usize>>> = Lazy::new(Default::default);
+
+fn initialize_display(
+    egl: &EglInstance,
+    display: khronos_egl::Display,
+) -> Result<(i32, i32), khronos_egl::Error> {
+    let mut guard = DISPLAYS_REFERENCE_COUNT.lock();
+    *guard.entry(display.as_ptr() as usize).or_default() += 1;
+
+    // We don't need to check the reference count here since according to the `eglInitialize`
+    // documentation, initializing an already initialized EGL display connection has no effect
+    // besides returning the version numbers.
+    egl.initialize(display)
+}
+
+fn terminate_display(
+    egl: &EglInstance,
+    display: khronos_egl::Display,
+) -> Result<(), khronos_egl::Error> {
+    let key = &(display.as_ptr() as usize);
+    let mut guard = DISPLAYS_REFERENCE_COUNT.lock();
+    let count_ref = guard
+        .get_mut(key)
+        .expect("Attempted to decref a display before incref was called");
+
+    if *count_ref > 1 {
+        *count_ref -= 1;
+
+        Ok(())
+    } else {
+        guard.remove(key);
+
+        egl.terminate(display)
+    }
+}
+
 impl Inner {
     fn create(
         flags: wgt::InstanceFlags,
@@ -439,7 +479,7 @@ impl Inner {
         display: khronos_egl::Display,
         force_gles_minor_version: wgt::Gles3MinorVersion,
     ) -> Result<Self, crate::InstanceError> {
-        let version = egl.initialize(display).map_err(|e| {
+        let version = initialize_display(&egl, display).map_err(|e| {
             crate::InstanceError::with_source(
                 String::from("failed to initialize EGL display connection"),
                 e,
@@ -608,7 +648,8 @@ impl Drop for Inner {
         {
             log::warn!("Error in destroy_context: {:?}", e);
         }
-        if let Err(e) = self.egl.instance.terminate(self.egl.display) {
+
+        if let Err(e) = terminate_display(&self.egl.instance, self.egl.display) {
             log::warn!("Error in terminate: {:?}", e);
         }
     }
@@ -778,7 +819,7 @@ impl crate::Instance<super::Api> for Instance {
                 let display = unsafe {
                     egl.get_platform_display(
                         EGL_PLATFORM_SURFACELESS_MESA,
-                        std::ptr::null_mut(),
+                        khronos_egl::DEFAULT_DISPLAY,
                         &[khronos_egl::ATTRIB_NONE],
                     )
                 }


Test patch:
diff --git a/tests/tests/device.rs b/tests/tests/device.rs
index f6c42736a77..02e2d1fb83a 100644
--- a/tests/tests/device.rs
+++ b/tests/tests/device.rs
@@ -29,39 +29,69 @@ static CROSS_DEVICE_BIND_GROUP_USAGE: GpuTestConfiguration = GpuTestConfiguratio
     });
 
 #[cfg(not(all(target_arch = "wasm32", not(target_os = "emscripten"))))]
-#[test]
-fn device_lifetime_check() {
-    use pollster::FutureExt as _;
-
-    env_logger::init();
-    let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
-        backends: wgpu::util::backend_bits_from_env().unwrap_or(wgpu::Backends::all()),
-        dx12_shader_compiler: wgpu::util::dx12_shader_compiler_from_env().unwrap_or_default(),
-        gles_minor_version: wgpu::util::gles_minor_version_from_env().unwrap_or_default(),
-        flags: wgpu::InstanceFlags::advanced_debugging().with_env(),
-    });
+#[gpu_test]
+static DEVICE_LIFETIME_CHECK: GpuTestConfiguration = GpuTestConfiguration::new()
+    .parameters(TestParameters::default())
+    .run_sync(|_| {
+        use pollster::FutureExt as _;
+
+        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
+            backends: wgpu::util::backend_bits_from_env().unwrap_or(wgpu::Backends::all()),
+            dx12_shader_compiler: wgpu::util::dx12_shader_compiler_from_env().unwrap_or_default(),
+            gles_minor_version: wgpu::util::gles_minor_version_from_env().unwrap_or_default(),
+            flags: wgpu::InstanceFlags::advanced_debugging().with_env(),
+        });
 
-    let adapter = wgpu::util::initialize_adapter_from_env_or_default(&instance, None)
-        .block_on()
-        .expect("failed to create adapter");
+        let adapter = wgpu::util::initialize_adapter_from_env_or_default(&instance, None)
+            .block_on()
+            .expect("failed to create adapter");
 
-    let (device, queue) = adapter
-        .request_device(&wgpu::DeviceDescriptor::default(), None)
-        .block_on()
-        .expect("failed to create device");
+        let (device, queue) = adapter
+            .request_device(&wgpu::DeviceDescriptor::default(), None)
+            .block_on()
+            .expect("failed to create device");
 
-    instance.poll_all(false);
+        instance.poll_all(false);
 
-    let pre_report = instance.generate_report().unwrap().unwrap();
+        let pre_report = instance.generate_report().unwrap();
 
-    drop(queue);
-    drop(device);
-    let post_report = instance.generate_report().unwrap().unwrap();
-    assert_ne!(
-        pre_report, post_report,
-        "Queue and Device has not been dropped as expected"
-    );
-}
+        drop(queue);
+        drop(device);
+        let post_report = instance.generate_report().unwrap();
+        assert_ne!(
+            pre_report, post_report,
+            "Queue and Device has not been dropped as expected"
+        );
+    });
+
+#[cfg(not(all(target_arch = "wasm32", not(target_os = "emscripten"))))]
+#[gpu_test]
+static MULTIPLE_DEVICES: GpuTestConfiguration = GpuTestConfiguration::new()
+    .parameters(TestParameters::default())
+    .run_sync(|_| {
+        use pollster::FutureExt as _;
+
+        fn create_device_and_queue() -> (wgpu::Device, wgpu::Queue) {
+            let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
+                backends: wgpu::util::backend_bits_from_env().unwrap_or(wgpu::Backends::all()),
+                dx12_shader_compiler: wgpu::util::dx12_shader_compiler_from_env()
+                    .unwrap_or_default(),
+                gles_minor_version: wgpu::util::gles_minor_version_from_env().unwrap_or_default(),
+                flags: wgpu::InstanceFlags::advanced_debugging().with_env(),
+            });
+
+            let adapter = wgpu::util::initialize_adapter_from_env_or_default(&instance, None)
+                .block_on()
+                .expect("failed to create adapter");
+
+            adapter
+                .request_device(&wgpu::DeviceDescriptor::default(), None)
+                .block_on()
+                .expect("failed to create device")
+        }
+
+        let _ = vec![create_device_and_queue(), create_device_and_queue()];
+    });
 
 #[cfg(not(all(target_arch = "wasm32", not(target_os = "emscripten"))))]
 #[gpu_test]