Repository: gfx-rs/wgpu
Pull request #7267: Allow the `"gles"` backend to (theoretically) compile on `no_std` targets.
Author: login: kpreid
Created: 2025-03-05T05:02:29Z | Merged: 2025-03-05T10:48:43Z

**Connections**
Part of #6826.

**Description**
Removes `std` usage from the `gles` backend in `wgpu-hal`, allowing `wgpu-hal` to build on `no_std` targets like `wasm32v1-none` (once its dependencies, notably `naga`, are all `no_std` compatible too).

This entirely consists of conditionally replacing `Mutex` with `RefCell`, then making sure that this doesn’t accidentally happen if we are also exposing `Send + Sync`.

I’m not very confident in the design of this change, but I tried to do it in a way which at least doesn’t add any unsound configurations.

**Testing**
Confirmed the backend still builds on native and wasm, and ran the test suite.

**Squash or Rebase?**
Rebase

**Checklist**

- [X] Run `cargo fmt`.
- [ ] Run `taplo format`.
- [X] Run `cargo clippy`. If applicable, add:
  - [X] `--target wasm32-unknown-unknown`
- [X] Run `cargo xtask test` to run tests.
- [ ] If this contains user-facing changes, add a `CHANGELOG.md` entry. <!-- See instructions at the top of `CHANGELOG.md`. -->



Diff:
diff --git a/wgpu-hal/Cargo.toml b/wgpu-hal/Cargo.toml
index be41164c31c..098d9ecb4c9 100644
--- a/wgpu-hal/Cargo.toml
+++ b/wgpu-hal/Cargo.toml
@@ -104,6 +104,7 @@ gles = [
     "naga/glsl-out",
     "dep:arrayvec",
     "dep:bytemuck",
+    "dep:cfg-if",
     "dep:glow",
     "dep:glutin_wgl_sys",
     "dep:hashbrown",
@@ -216,6 +217,7 @@ rustc-hash = { workspace = true, optional = true }
 # Backend: GLES
 bytemuck = { workspace = true, optional = true }
 glow = { workspace = true, optional = true }
+cfg-if = { workspace = true, optional = true }
 
 ########################
 ### Platform: Native ###
diff --git a/wgpu-hal/build.rs b/wgpu-hal/build.rs
index d67321a72d8..960f6453008 100644
--- a/wgpu-hal/build.rs
+++ b/wgpu-hal/build.rs
@@ -9,6 +9,18 @@ fn main() {
         Emscripten: { all(target_os = "emscripten", gles) },
         dx12: { all(target_os = "windows", feature = "dx12") },
         gles: { all(feature = "gles") },
+        // Within the GL ES backend, use `std` and be Send + Sync only if we are using a target
+        // that, among the ones where the GL ES backend is supported, has `std`.
+        gles_with_std: { all(
+            feature = "gles",
+            any(
+                not(target_arch = "wasm32"),
+                // Accept wasm32-unknown-unknown, which uniquely has a stub `std`
+                all(target_vendor = "unknown", target_os = "unknown"),
+                // Accept wasm32-unknown-emscripten and similar, which has a real `std`
+                target_os = "emscripten"
+            )
+        ) },
         metal: { all(target_vendor = "apple", feature = "metal") },
         vulkan: { all(not(target_arch = "wasm32"), feature = "vulkan") },
         // ⚠️ Keep in sync with target.cfg() definition in Cargo.toml and cfg_alias in `wgpu` crate ⚠️
diff --git a/wgpu-hal/src/gles/device.rs b/wgpu-hal/src/gles/device.rs
index 123d87f8f36..2cf1de9cdf4 100644
--- a/wgpu-hal/src/gles/device.rs
+++ b/wgpu-hal/src/gles/device.rs
@@ -2,13 +2,12 @@ use alloc::{
     borrow::ToOwned, format, string::String, string::ToString as _, sync::Arc, vec, vec::Vec,
 };
 use core::{cmp::max, convert::TryInto, num::NonZeroU32, ptr, sync::atomic::Ordering};
-use std::sync::Mutex;
 
 use arrayvec::ArrayVec;
 use glow::HasContext;
 use naga::FastHashMap;
 
-use super::{conv, PrivateCapabilities};
+use super::{conv, lock, MaybeMutex, PrivateCapabilities};
 use crate::auxil::map_naga_stage;
 use crate::TlasInstance;
 
@@ -526,8 +525,8 @@ impl crate::Device for super::Device {
                 target,
                 size: desc.size,
                 map_flags: 0,
-                data: Some(Arc::new(Mutex::new(vec![0; desc.size as usize]))),
-                offset_of_current_mapping: Arc::new(Mutex::new(0)),
+                data: Some(Arc::new(MaybeMutex::new(vec![0; desc.size as usize]))),
+                offset_of_current_mapping: Arc::new(MaybeMutex::new(0)),
             });
         }
 
@@ -614,7 +613,7 @@ impl crate::Device for super::Device {
         }
 
         let data = if emulate_map && desc.usage.contains(wgt::BufferUses::MAP_READ) {
-            Some(Arc::new(Mutex::new(vec![0; desc.size as usize])))
+            Some(Arc::new(MaybeMutex::new(vec![0; desc.size as usize])))
         } else {
             None
         };
@@ -627,7 +626,7 @@ impl crate::Device for super::Device {
             size: desc.size,
             map_flags,
             data,
-            offset_of_current_mapping: Arc::new(Mutex::new(0)),
+            offset_of_current_mapping: Arc::new(MaybeMutex::new(0)),
         })
     }
 
@@ -652,7 +651,7 @@ impl crate::Device for super::Device {
         let is_coherent = buffer.map_flags & glow::MAP_COHERENT_BIT != 0;
         let ptr = match buffer.raw {
             None => {
-                let mut vec = buffer.data.as_ref().unwrap().lock().unwrap();
+                let mut vec = lock(buffer.data.as_ref().unwrap());
                 let slice = &mut vec.as_mut_slice()[range.start as usize..range.end as usize];
                 slice.as_mut_ptr()
             }
@@ -660,12 +659,12 @@ impl crate::Device for super::Device {
                 let gl = &self.shared.context.lock();
                 unsafe { gl.bind_buffer(buffer.target, Some(raw)) };
                 let ptr = if let Some(ref map_read_allocation) = buffer.data {
-                    let mut guard = map_read_allocation.lock().unwrap();
+                    let mut guard = lock(map_read_allocation);
                     let slice = guard.as_mut_slice();
                     unsafe { self.shared.get_buffer_sub_data(gl, buffer.target, 0, slice) };
                     slice.as_mut_ptr()
                 } else {
-                    *buffer.offset_of_current_mapping.lock().unwrap() = range.start;
+                    *lock(&buffer.offset_of_current_mapping) = range.start;
                     unsafe {
                         gl.map_buffer_range(
                             buffer.target,
@@ -691,7 +690,7 @@ impl crate::Device for super::Device {
                 unsafe { gl.bind_buffer(buffer.target, Some(raw)) };
                 unsafe { gl.unmap_buffer(buffer.target) };
                 unsafe { gl.bind_buffer(buffer.target, None) };
-                *buffer.offset_of_current_mapping.lock().unwrap() = 0;
+                *lock(&buffer.offset_of_current_mapping) = 0;
             }
         }
     }
@@ -704,8 +703,7 @@ impl crate::Device for super::Device {
                 let gl = &self.shared.context.lock();
                 unsafe { gl.bind_buffer(buffer.target, Some(raw)) };
                 for range in ranges {
-                    let offset_of_current_mapping =
-                        *buffer.offset_of_current_mapping.lock().unwrap();
+                    let offset_of_current_mapping = *lock(&buffer.offset_of_current_mapping);
                     unsafe {
                         gl.flush_mapped_buffer_range(
                             buffer.target,
diff --git a/wgpu-hal/src/gles/mod.rs b/wgpu-hal/src/gles/mod.rs
index 756838d776d..ab434b90dd1 100644
--- a/wgpu-hal/src/gles/mod.rs
+++ b/wgpu-hal/src/gles/mod.rs
@@ -346,8 +346,8 @@ pub struct Buffer {
     target: BindTarget,
     size: wgt::BufferAddress,
     map_flags: u32,
-    data: Option<Arc<std::sync::Mutex<Vec<u8>>>>,
-    offset_of_current_mapping: Arc<std::sync::Mutex<wgt::BufferAddress>>,
+    data: Option<Arc<MaybeMutex<Vec<u8>>>>,
+    offset_of_current_mapping: Arc<MaybeMutex<wgt::BufferAddress>>,
 }
 
 #[cfg(send_sync)]
@@ -1093,3 +1093,26 @@ fn gl_debug_message_callback(source: u32, gltype: u32, id: u32, severity: u32, m
         crate::VALIDATION_CANARY.add(message.to_string());
     }
 }
+
+// If we are using `std`, then use `Mutex` to provide `Send` and `Sync`
+cfg_if::cfg_if! {
+    if #[cfg(gles_with_std)] {
+        type MaybeMutex<T> = std::sync::Mutex<T>;
+
+        fn lock<T>(mutex: &MaybeMutex<T>) -> std::sync::MutexGuard<'_, T> {
+            mutex.lock().unwrap()
+        }
+    } else {
+        // It should be impossible for any build configuration to trigger this error
+        // It is intended only as a guard against changes elsewhere causing the use of
+        // `RefCell` here to become unsound.
+        #[cfg(all(send_sync, not(feature = "fragile-send-sync-non-atomic-wasm")))]
+        compile_error!("cannot provide non-fragile Send+Sync without std");
+
+        type MaybeMutex<T> = core::cell::RefCell<T>;
+
+        fn lock<T>(mutex: &MaybeMutex<T>) -> core::cell::RefMut<'_, T> {
+            mutex.borrow_mut()
+        }
+    }
+}
diff --git a/wgpu-hal/src/gles/queue.rs b/wgpu-hal/src/gles/queue.rs
index da7bc1c1938..1d391f7bded 100644
--- a/wgpu-hal/src/gles/queue.rs
+++ b/wgpu-hal/src/gles/queue.rs
@@ -1,9 +1,12 @@
-use super::{conv::is_layered_target, Command as C, PrivateCapabilities};
 use alloc::sync::Arc;
-use arrayvec::ArrayVec;
+use alloc::vec;
 use core::{slice, sync::atomic::Ordering};
+
+use arrayvec::ArrayVec;
 use glow::HasContext;
 
+use super::{conv::is_layered_target, lock, Command as C, PrivateCapabilities};
+
 const DEBUG_ID: u32 = 0;
 
 fn extract_marker<'a>(data: &'a [u8], range: &core::ops::Range<u32>) -> &'a str {
@@ -340,7 +343,7 @@ impl super::Queue {
                     }
                 }
                 None => {
-                    dst.data.as_ref().unwrap().lock().unwrap().as_mut_slice()
+                    lock(dst.data.as_ref().unwrap()).as_mut_slice()
                         [range.start as usize..range.end as usize]
                         .fill(0);
                 }
@@ -382,7 +385,7 @@ impl super::Queue {
                         };
                     }
                     (Some(src), None) => {
-                        let mut data = dst.data.as_ref().unwrap().lock().unwrap();
+                        let mut data = lock(dst.data.as_ref().unwrap());
                         let dst_data = &mut data.as_mut_slice()
                             [copy.dst_offset as usize..copy.dst_offset as usize + size];
 
@@ -397,7 +400,7 @@ impl super::Queue {
                         };
                     }
                     (None, Some(dst)) => {
-                        let data = src.data.as_ref().unwrap().lock().unwrap();
+                        let data = lock(src.data.as_ref().unwrap());
                         let src_data = &data.as_slice()
                             [copy.src_offset as usize..copy.src_offset as usize + size];
                         unsafe { gl.bind_buffer(copy_dst_target, Some(dst)) };
@@ -738,7 +741,7 @@ impl super::Queue {
                             glow::PixelUnpackData::BufferOffset(copy.buffer_layout.offset as u32)
                         }
                         None => {
-                            buffer_data = src.data.as_ref().unwrap().lock().unwrap();
+                            buffer_data = lock(src.data.as_ref().unwrap());
                             let src_data =
                                 &buffer_data.as_slice()[copy.buffer_layout.offset as usize..];
                             glow::PixelUnpackData::Slice(Some(src_data))
@@ -802,7 +805,7 @@ impl super::Queue {
                             )
                         }
                         None => {
-                            buffer_data = src.data.as_ref().unwrap().lock().unwrap();
+                            buffer_data = lock(src.data.as_ref().unwrap());
                             let src_data = &buffer_data.as_slice()
                                 [(offset as usize)..(offset + bytes_in_upload) as usize];
                             glow::CompressedPixelUnpackData::Slice(src_data)
@@ -883,7 +886,7 @@ impl super::Queue {
                             glow::PixelPackData::BufferOffset(offset as u32)
                         }
                         None => {
-                            buffer_data = dst.data.as_ref().unwrap().lock().unwrap();
+                            buffer_data = lock(dst.data.as_ref().unwrap());
                             let dst_data = &mut buffer_data.as_mut_slice()[offset as usize..];
                             glow::PixelPackData::Slice(Some(dst_data))
                         }
@@ -1054,7 +1057,7 @@ impl super::Queue {
                             };
                         }
                         None => {
-                            let data = &mut dst.data.as_ref().unwrap().lock().unwrap();
+                            let data = &mut lock(dst.data.as_ref().unwrap());
                             let len = query_data.len().min(data.len());
                             data[..len].copy_from_slice(&query_data[..len]);
                         }
diff --git a/wgpu-hal/src/lib.rs b/wgpu-hal/src/lib.rs
index 88f6e07fa6b..aa997a2d9dd 100644
--- a/wgpu-hal/src/lib.rs
+++ b/wgpu-hal/src/lib.rs
@@ -241,8 +241,7 @@
 extern crate alloc;
 extern crate wgpu_types as wgt;
 // Each of these backends needs `std` in some fashion; usually `std::thread` functions.
-// TODO(https://github.com/gfx-rs/wgpu/issues/6826): gles-WebGL backend should be made no-std
-#[cfg(any(dx12, gles, metal, vulkan))]
+#[cfg(any(dx12, gles_with_std, metal, vulkan))]
 #[macro_use]
 extern crate std;
 


Reviews:
user: login: kpreid
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T05:04:01Z
user: login: Wumpf
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T10:45:50Z
user: login: Wumpf
body: looks good to me. Changes are non-invasive enough imho
We should follow-up though with a way to check this configuration on ci, lest someone adds e.g. a new mutex or similar again
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-05T10:48:09Z

Review comments:
diff_hunk: @@ -104,6 +104,7 @@ gles = [
     "naga/glsl-out",
     "dep:arrayvec",
     "dep:bytemuck",
+    "dep:cfg-if",
path: wgpu-hal/Cargo.toml
user: login: kpreid
body: I notice that before this change, `cfg-if` is only a dev-dependency, never a library dependency. I assume there is no strong reason for that; I can rewrite the code to use only `#[cfg]` if desired.
created_at: 2025-03-05T05:04:01Z
author_association: COLLABORATOR
diff_hunk: @@ -104,6 +104,7 @@ gles = [
     "naga/glsl-out",
     "dep:arrayvec",
     "dep:bytemuck",
+    "dep:cfg-if",
path: wgpu-hal/Cargo.toml
user: login: Wumpf
body: it's an absolutely tiny dependency and makes code look nicer here, don't mind it :)
created_at: 2025-03-05T10:45:50Z
author_association: MEMBER

PR comments:
user: login: kpreid
created_at: 2025-03-05T05:07:01Z
body: Ah, I see I broke Emscripten. It’s late, so I will think about that another day.
author_association: COLLABORATOR
user: login: kpreid
created_at: 2025-03-05T06:07:24Z
body: There we go. I just forgot that `cfg(target = "...")` isn’t a thing.
author_association: COLLABORATOR
user: login: kpreid
created_at: 2025-03-05T14:45:06Z
body: A sufficient CI test would be

`cargo (check|build|clippy) -p wgpu-hal --features gles`

but once #6826 is closer to done, we can and should run builds on actual doesn’t-have-std targets, which will be even stronger.

author_association: COLLABORATOR