Repository: gfx-rs/wgpu
Pull request #6229 (2024-09-06 14:25:19)

Invalidate the device when we encounter driver-induced device loss or on unexpected errors
Fixes https://github.com/gfx-rs/wgpu/issues/1624.
Fixes https://github.com/gfx-rs/wgpu/issues/5132.


ErichDonGubler: Tentatively assigning and requesting review from @jimblandy.

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9762a8ecf..802c8bd75 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -619,7 +619,7 @@ jobs:
           cargo fmt --manifest-path xtask/Cargo.toml -- --check
 
       - name: Check for typos
-        uses: crate-ci/typos@v1.24.1
+        uses: crate-ci/typos@v1.24.5
 
   check-cts-runner:
     # runtime is normally 2 minutes
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index b1c83e53b..9e5dc2716 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -41,7 +41,7 @@ jobs:
         if: ${{ failure() }}
 
       - name: Deploy the docs
-        uses: JamesIves/github-pages-deploy-action@v4.6.3
+        uses: JamesIves/github-pages-deploy-action@v4.6.4
         if: github.ref == 'refs/heads/trunk'
         with:
           token: ${{ secrets.WEB_DEPLOY }}
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index e8a630024..266d4e7f5 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -41,7 +41,7 @@ jobs:
         run: cargo xtask run-wasm --no-serve
 
       - name: Deploy WebGPU examples
-        uses: JamesIves/github-pages-deploy-action@v4.6.3
+        uses: JamesIves/github-pages-deploy-action@v4.6.4
         if: github.ref == 'refs/heads/trunk'
         with:
           token: ${{ secrets.WEB_DEPLOY }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9c1dbdabe..96b823a2f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -65,6 +65,15 @@ traits that have now been implemented for `wgpu` resources.
 
 By @teoxoy [#6134](https://github.com/gfx-rs/wgpu/pull/6134).
 
+#### `set_bind_group` now takes an `Option` for the bind group argument.
+
+https://gpuweb.github.io/gpuweb/#programmable-passes-bind-groups specifies that bindGroup
+is nullable. This change is the start of implementing this part of the spec. Callers that
+specify a `Some()` value should have unchanged behavior. Handling of `None` values still
+needs to be implemented by backends.
+
+By @bradwerth [#6216](https://github.com/gfx-rs/wgpu/pull/6216).
+
 ### New Features
 
 #### Naga
diff --git a/benches/benches/computepass.rs b/benches/benches/computepass.rs
index 9fbd3df09..719f84696 100644
--- a/benches/benches/computepass.rs
+++ b/benches/benches/computepass.rs
@@ -389,7 +389,7 @@ impl ComputepassState {
         let end_idx = start_idx + dispatch_per_pass;
         for dispatch_idx in start_idx..end_idx {
             compute_pass.set_pipeline(&self.pipeline);
-            compute_pass.set_bind_group(0, &self.bind_groups[dispatch_idx], &[]);
+            compute_pass.set_bind_group(0, Some(&self.bind_groups[dispatch_idx]), &[]);
             compute_pass.dispatch_workgroups(1, 1, 1);
         }
 
@@ -412,7 +412,7 @@ impl ComputepassState {
         });
 
         compute_pass.set_pipeline(self.bindless_pipeline.as_ref().unwrap());
-        compute_pass.set_bind_group(0, self.bindless_bind_group.as_ref().unwrap(), &[]);
+        compute_pass.set_bind_group(0, Some(self.bindless_bind_group.as_ref().unwrap()), &[]);
         for _ in 0..dispatch_count_bindless {
             compute_pass.dispatch_workgroups(1, 1, 1);
         }
diff --git a/benches/benches/renderpass.rs b/benches/benches/renderpass.rs
index 34007a39d..fe23aa62b 100644
--- a/benches/benches/renderpass.rs
+++ b/benches/benches/renderpass.rs
@@ -367,7 +367,7 @@ impl RenderpassState {
         let end_idx = start_idx + draws_per_pass;
         for draw_idx in start_idx..end_idx {
             render_pass.set_pipeline(&self.pipeline);
-            render_pass.set_bind_group(0, &self.bind_groups[draw_idx], &[]);
+            render_pass.set_bind_group(0, Some(&self.bind_groups[draw_idx]), &[]);
             for i in 0..VERTEX_BUFFERS_PER_DRAW {
                 render_pass.set_vertex_buffer(
                     i as u32,
@@ -410,7 +410,7 @@ impl RenderpassState {
         });
 
         render_pass.set_pipeline(self.bindless_pipeline.as_ref().unwrap());
-        render_pass.set_bind_group(0, self.bindless_bind_group.as_ref().unwrap(), &[]);
+        render_pass.set_bind_group(0, Some(self.bindless_bind_group.as_ref().unwrap()), &[]);
         for i in 0..VERTEX_BUFFERS_PER_DRAW {
             render_pass.set_vertex_buffer(i as u32, self.vertex_buffers[0].slice(..));
         }
diff --git a/deno_webgpu/bundle.rs b/deno_webgpu/bundle.rs
index 0d1421d20..58d179051 100644
--- a/deno_webgpu/bundle.rs
+++ b/deno_webgpu/bundle.rs
@@ -150,7 +150,7 @@ pub fn op_webgpu_render_bundle_encoder_set_bind_group(
         wgpu_core::command::bundle_ffi::wgpu_render_bundle_set_bind_group(
             &mut render_bundle_encoder_resource.0.borrow_mut(),
             index,
-            bind_group_resource.1,
+            Some(bind_group_resource.1),
             dynamic_offsets_data.as_ptr(),
             dynamic_offsets_data.len(),
         );
diff --git a/deno_webgpu/compute_pass.rs b/deno_webgpu/compute_pass.rs
index e3e69860a..6755e84a6 100644
--- a/deno_webgpu/compute_pass.rs
+++ b/deno_webgpu/compute_pass.rs
@@ -136,7 +136,7 @@ pub fn op_webgpu_compute_pass_set_bind_group(
         .compute_pass_set_bind_group(
             &mut compute_pass_resource.0.borrow_mut(),
             index,
-            bind_group_resource.1,
+            Some(bind_group_resource.1),
             dynamic_offsets_data,
         )?;
 
diff --git a/deno_webgpu/render_pass.rs b/deno_webgpu/render_pass.rs
index 2d4557cf0..4929fbbe9 100644
--- a/deno_webgpu/render_pass.rs
+++ b/deno_webgpu/render_pass.rs
@@ -231,7 +231,7 @@ pub fn op_webgpu_render_pass_set_bind_group(
         .render_pass_set_bind_group(
             &mut render_pass_resource.0.borrow_mut(),
             index,
-            bind_group_resource.1,
+            Some(bind_group_resource.1),
             dynamic_offsets_data,
         )?;
 
diff --git a/examples/src/boids/mod.rs b/examples/src/boids/mod.rs
index f7875ed76..c527be96d 100644
--- a/examples/src/boids/mod.rs
+++ b/examples/src/boids/mod.rs
@@ -298,7 +298,7 @@ impl crate::framework::Example for Example {
                 timestamp_writes: None,
             });
             cpass.set_pipeline(&self.compute_pipeline);
-            cpass.set_bind_group(0, &self.particle_bind_groups[self.frame_num % 2], &[]);
+            cpass.set_bind_group(0, Some(&self.particle_bind_groups[self.frame_num % 2]), &[]);
             cpass.dispatch_workgroups(self.work_group_count, 1, 1);
         }
         command_encoder.pop_debug_group();
diff --git a/examples/src/bunnymark/mod.rs b/examples/src/bunnymark/mod.rs
index 54bdc2a94..0c242c713 100644
--- a/examples/src/bunnymark/mod.rs
+++ b/examples/src/bunnymark/mod.rs
@@ -34,19 +34,29 @@ impl Bunny {
         self.position[0] += self.velocity[0] * delta;
         self.position[1] += self.velocity[1] * delta;
         self.velocity[1] += GRAVITY * delta;
+
         if (self.velocity[0] > 0.0 && self.position[0] + 0.5 * BUNNY_SIZE > extent[0] as f32)
             || (self.velocity[0] < 0.0 && self.position[0] - 0.5 * BUNNY_SIZE < 0.0)
         {
             self.velocity[0] *= -1.0;
         }
+
         if self.velocity[1] < 0.0 && self.position[1] < 0.5 * BUNNY_SIZE {
             self.velocity[1] *= -1.0;
         }
+
+        // Top boundary check
+        if self.velocity[1] > 0.0 && self.position[1] + 0.5 * BUNNY_SIZE > extent[1] as f32 {
+            self.velocity[1] *= -1.0;
+        }
     }
 }
 
 /// Example struct holds references to wgpu resources and frame persistent data
 struct Example {
+    view: wgpu::TextureView,
+    sampler: wgpu::Sampler,
+    global_bind_group_layout: wgpu::BindGroupLayout,
     global_group: wgpu::BindGroup,
     local_group: wgpu::BindGroup,
     pipeline: wgpu::RenderPipeline,
@@ -118,11 +128,11 @@ impl Example {
                 occlusion_query_set: None,
             });
             rpass.set_pipeline(&self.pipeline);
-            rpass.set_bind_group(0, &self.global_group, &[]);
+            rpass.set_bind_group(0, Some(&self.global_group), &[]);
             for i in 0..self.bunnies.len() {
                 let offset =
                     (i as wgpu::DynamicOffset) * (uniform_alignment as wgpu::DynamicOffset);
-                rpass.set_bind_group(1, &self.local_group, &[offset]);
+                rpass.set_bind_group(1, Some(&self.local_group), &[offset]);
                 rpass.draw(0..4, 0..1);
             }
         }
@@ -286,6 +296,7 @@ impl crate::framework::Example for Example {
             size: [BUNNY_SIZE; 2],
             pad: [0.0; 2],
         };
+
         let global_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
             label: Some("global"),
             contents: bytemuck::bytes_of(&globals),
@@ -335,6 +346,9 @@ impl crate::framework::Example for Example {
         let rng = WyRand::new_seed(42);
 
         let mut ex = Example {
+            view,
+            sampler,
+            global_bind_group_layout,
             pipeline,
             global_group,
             local_group,
@@ -366,11 +380,51 @@ impl crate::framework::Example for Example {
 
     fn resize(
         &mut self,
-        _sc_desc: &wgpu::SurfaceConfiguration,
-        _device: &wgpu::Device,
+        sc_desc: &wgpu::SurfaceConfiguration,
+        device: &wgpu::Device,
         _queue: &wgpu::Queue,
     ) {
-        //empty
+        self.extent = [sc_desc.width, sc_desc.height];
+
+        let globals = Globals {
+            mvp: glam::Mat4::orthographic_rh(
+                0.0,
+                sc_desc.width as f32,
+                0.0,
+                sc_desc.height as f32,
+                -1.0,
+                1.0,
+            )
+            .to_cols_array_2d(),
+            size: [BUNNY_SIZE; 2],
+            pad: [0.0; 2],
+        };
+
+        let global_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+            label: Some("global"),
+            contents: bytemuck::bytes_of(&globals),
+            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM,
+        });
+
+        let global_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+            layout: &self.global_bind_group_layout,
+            entries: &[
+                wgpu::BindGroupEntry {
+                    binding: 0,
+                    resource: global_buffer.as_entire_binding(),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 1,
+                    resource: wgpu::BindingResource::TextureView(&self.view),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 2,
+                    resource: wgpu::BindingResource::Sampler(&self.sampler),
+                },
+            ],
+            label: None,
+        });
+        self.global_group = global_group;
     }
 
     fn render(&mut self, view: &wgpu::TextureView, device: &wgpu::Device, queue: &wgpu::Queue) {
diff --git a/examples/src/conservative_raster/mod.rs b/examples/src/conservative_raster/mod.rs
index d02913475..46fb8742a 100644
--- a/examples/src/conservative_raster/mod.rs
+++ b/examples/src/conservative_raster/mod.rs
@@ -305,7 +305,7 @@ impl crate::framework::Example for Example {
             });
 
             rpass.set_pipeline(&self.pipeline_upscale);
-            rpass.set_bind_group(0, &self.bind_group_upscale, &[]);
+            rpass.set_bind_group(0, Some(&self.bind_group_upscale), &[]);
             rpass.draw(0..3, 0..1);
 
             if let Some(pipeline_lines) = &self.pipeline_lines {
diff --git a/examples/src/cube/mod.rs b/examples/src/cube/mod.rs
index 00e2ffcfc..78dc06e06 100644
--- a/examples/src/cube/mod.rs
+++ b/examples/src/cube/mod.rs
@@ -361,7 +361,7 @@ impl crate::framework::Example for Example {
             });
             rpass.push_debug_group("Prepare data for draw.");
             rpass.set_pipeline(&self.pipeline);
-            rpass.set_bind_group(0, &self.bind_group, &[]);
+            rpass.set_bind_group(0, Some(&self.bind_group), &[]);
             rpass.set_index_buffer(self.index_buf.slice(..), wgpu::IndexFormat::Uint16);
             rpass.set_vertex_buffer(0, self.vertex_buf.slice(..));
             rpass.pop_debug_group();
diff --git a/examples/src/hello_compute/mod.rs b/examples/src/hello_compute/mod.rs
index a5257c07f..e53f49fa4 100644
--- a/examples/src/hello_compute/mod.rs
+++ b/examples/src/hello_compute/mod.rs
@@ -135,7 +135,7 @@ async fn execute_gpu_inner(
             timestamp_writes: None,
         });
         cpass.set_pipeline(&compute_pipeline);
-        cpass.set_bind_group(0, &bind_group, &[]);
+        cpass.set_bind_group(0, Some(&bind_group), &[]);
         cpass.insert_debug_marker("compute collatz iterations");
         cpass.dispatch_workgroups(numbers.len() as u32, 1, 1); // Number of cells to run, the (x,y,z) size of item being processed
     }
diff --git a/examples/src/hello_synchronization/mod.rs b/examples/src/hello_synchronization/mod.rs
index 20743549f..fad5d7a9d 100644
--- a/examples/src/hello_synchronization/mod.rs
+++ b/examples/src/hello_synchronization/mod.rs
@@ -128,7 +128,7 @@ async fn execute(
             timestamp_writes: None,
         });
         compute_pass.set_pipeline(&patient_pipeline);
-        compute_pass.set_bind_group(0, &bind_group, &[]);
+        compute_pass.set_bind_group(0, Some(&bind_group), &[]);
         compute_pass.dispatch_workgroups(local_patient_workgroup_results.len() as u32, 1, 1);
     }
     queue.submit(Some(command_encoder.finish()));
@@ -150,7 +150,7 @@ async fn execute(
             timestamp_writes: None,
         });
         compute_pass.set_pipeline(&hasty_pipeline);
-        compute_pass.set_bind_group(0, &bind_group, &[]);
+        compute_pass.set_bind_group(0, Some(&bind_group), &[]);
         compute_pass.dispatch_workgroups(local_patient_workgroup_results.len() as u32, 1, 1);
     }
     queue.submit(Some(command_encoder.finish()));
diff --git a/examples/src/hello_workgroups/mod.rs b/examples/src/hello_workgroups/mod.rs
index d140bc5b0..7a653cf3e 100644
--- a/examples/src/hello_workgroups/mod.rs
+++ b/examples/src/hello_workgroups/mod.rs
@@ -127,7 +127,7 @@ async fn run() {
             timestamp_writes: None,
         });
         compute_pass.set_pipeline(&pipeline);
-        compute_pass.set_bind_group(0, &bind_group, &[]);
+        compute_pass.set_bind_group(0, Some(&bind_group), &[]);
         /* Note that since each workgroup will cover both arrays, we only need to
         cover the length of one array. */
         compute_pass.dispatch_workgroups(local_a.len() as u32, 1, 1);
diff --git a/examples/src/mipmap/mod.rs b/examples/src/mipmap/mod.rs
index a3f7833b4..179970ad7 100644
--- a/examples/src/mipmap/mod.rs
+++ b/examples/src/mipmap/mod.rs
@@ -180,7 +180,7 @@ impl Example {
                 );
             }
             rpass.set_pipeline(&pipeline);
-            rpass.set_bind_group(0, &bind_group, &[]);
+            rpass.set_bind_group(0, Some(&bind_group), &[]);
             rpass.draw(0..3, 0..1);
             if let Some(ref query_sets) = query_sets {
                 rpass.write_timestamp(&query_sets.timestamp, timestamp_query_index_base + 1);
@@ -497,7 +497,7 @@ impl crate::framework::Example for Example {
                 occlusion_query_set: None,
             });
             rpass.set_pipeline(&self.draw_pipeline);
-            rpass.set_bind_group(0, &self.bind_group, &[]);
+            rpass.set_bind_group(0, Some(&self.bind_group), &[]);
             rpass.draw(0..4, 0..1);
         }
 
diff --git a/examples/src/repeated_compute/mod.rs b/examples/src/repeated_compute/mod.rs
index 5dac9ce7c..83dcd4099 100644
--- a/examples/src/repeated_compute/mod.rs
+++ b/examples/src/repeated_compute/mod.rs
@@ -59,7 +59,7 @@ async fn compute(local_buffer: &mut [u32], context: &WgpuContext) {
             timestamp_writes: None,
         });
         compute_pass.set_pipeline(&context.pipeline);
-        compute_pass.set_bind_group(0, &context.bind_group, &[]);
+        compute_pass.set_bind_group(0, Some(&context.bind_group), &[]);
         compute_pass.dispatch_workgroups(local_buffer.len() as u32, 1, 1);
     }
     // We finish the compute pass by dropping it.
diff --git a/examples/src/shadow/mod.rs b/examples/src/shadow/mod.rs
index 5e3511ed8..a7edcce7e 100644
--- a/examples/src/shadow/mod.rs
+++ b/examples/src/shadow/mod.rs
@@ -779,10 +779,10 @@ impl crate::framework::Example for Example {
                     occlusion_query_set: None,
                 });
                 pass.set_pipeline(&self.shadow_pass.pipeline);
-                pass.set_bind_group(0, &self.shadow_pass.bind_group, &[]);
+                pass.set_bind_group(0, Some(&self.shadow_pass.bind_group), &[]);
 
                 for entity in &self.entities {
-                    pass.set_bind_group(1, &self.entity_bind_group, &[entity.uniform_offset]);
+                    pass.set_bind_group(1, Some(&self.entity_bind_group), &[entity.uniform_offset]);
                     pass.set_index_buffer(entity.index_buf.slice(..), entity.index_format);
                     pass.set_vertex_buffer(0, entity.vertex_buf.slice(..));
                     pass.draw_indexed(0..entity.index_count as u32, 0, 0..1);
@@ -823,10 +823,10 @@ impl crate::framework::Example for Example {
                 occlusion_query_set: None,
             });
             pass.set_pipeline(&self.forward_pass.pipeline);
-            pass.set_bind_group(0, &self.forward_pass.bind_group, &[]);
+            pass.set_bind_group(0, Some(&self.forward_pass.bind_group), &[]);
 
             for entity in &self.entities {
-                pass.set_bind_group(1, &self.entity_bind_group, &[entity.uniform_offset]);
+                pass.set_bind_group(1, Some(&self.entity_bind_group), &[entity.uniform_offset]);
                 pass.set_index_buffer(entity.index_buf.slice(..), entity.index_format);
                 pass.set_vertex_buffer(0, entity.vertex_buf.slice(..));
                 pass.draw_indexed(0..entity.index_count as u32, 0, 0..1);
diff --git a/examples/src/skybox/mod.rs b/examples/src/skybox/mod.rs
index 7397d8846..82e58ef6d 100644
--- a/examples/src/skybox/mod.rs
+++ b/examples/src/skybox/mod.rs
@@ -451,7 +451,7 @@ impl crate::framework::Example for Example {
                 occlusion_query_set: None,
             });
 
-            rpass.set_bind_group(0, &self.bind_group, &[]);
+            rpass.set_bind_group(0, Some(&self.bind_group), &[]);
             rpass.set_pipeline(&self.entity_pipeline);
 
             for entity in self.entities.iter() {
diff --git a/examples/src/srgb_blend/mod.rs b/examples/src/srgb_blend/mod.rs
index 63e5e79cb..822d95d3c 100644
--- a/examples/src/srgb_blend/mod.rs
+++ b/examples/src/srgb_blend/mod.rs
@@ -202,7 +202,7 @@ impl<const SRGB: bool> crate::framework::Example for Example<SRGB> {
             });
             rpass.push_debug_group("Prepare data for draw.");
             rpass.set_pipeline(&self.pipeline);
-            rpass.set_bind_group(0, &self.bind_group, &[]);
+            rpass.set_bind_group(0, Some(&self.bind_group), &[]);
             rpass.set_index_buffer(self.index_buf.slice(..), wgpu::IndexFormat::Uint16);
             rpass.set_vertex_buffer(0, self.vertex_buf.slice(..));
             rpass.pop_debug_group();
diff --git a/examples/src/storage_texture/mod.rs b/examples/src/storage_texture/mod.rs
index 904a721f0..a68758419 100644
--- a/examples/src/storage_texture/mod.rs
+++ b/examples/src/storage_texture/mod.rs
@@ -117,7 +117,7 @@ async fn run(_path: Option<String>) {
             label: None,
             timestamp_writes: None,
         });
-        compute_pass.set_bind_group(0, &bind_group, &[]);
+        compute_pass.set_bind_group(0, Some(&bind_group), &[]);
         compute_pass.set_pipeline(&pipeline);
         compute_pass.dispatch_workgroups(TEXTURE_DIMS.0 as u32, TEXTURE_DIMS.1 as u32, 1);
     }
diff --git a/examples/src/texture_arrays/mod.rs b/examples/src/texture_arrays/mod.rs
index 00b90603f..8c81950e9 100644
--- a/examples/src/texture_arrays/mod.rs
+++ b/examples/src/texture_arrays/mod.rs
@@ -391,12 +391,12 @@ impl crate::framework::Example for Example {
         rpass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
         rpass.set_index_buffer(self.index_buffer.slice(..), self.index_format);
         if self.uniform_workaround {
-            rpass.set_bind_group(0, &self.bind_group, &[0]);
+            rpass.set_bind_group(0, Some(&self.bind_group), &[0]);
             rpass.draw_indexed(0..6, 0, 0..1);
-            rpass.set_bind_group(0, &self.bind_group, &[256]);
+            rpass.set_bind_group(0, Some(&self.bind_group), &[256]);
             rpass.draw_indexed(6..12, 0, 0..1);
         } else {
-            rpass.set_bind_group(0, &self.bind_group, &[0]);
+            rpass.set_bind_group(0, Some(&self.bind_group), &[0]);
             rpass.draw_indexed(0..12, 0, 0..1);
         }
 
diff --git a/examples/src/timestamp_queries/mod.rs b/examples/src/timestamp_queries/mod.rs
index 0daa71c0e..2921ae4c8 100644
--- a/examples/src/timestamp_queries/mod.rs
+++ b/examples/src/timestamp_queries/mod.rs
@@ -324,7 +324,7 @@ fn compute_pass(
     });
     *next_unused_query += 2;
     cpass.set_pipeline(&compute_pipeline);
-    cpass.set_bind_group(0, &bind_group, &[]);
+    cpass.set_bind_group(0, Some(&bind_group), &[]);
     cpass.dispatch_workgroups(1, 1, 1);
     if device
         .features()
diff --git a/examples/src/uniform_values/mod.rs b/examples/src/uniform_values/mod.rs
index 770fcd1aa..1ef58de09 100644
--- a/examples/src/uniform_values/mod.rs
+++ b/examples/src/uniform_values/mod.rs
@@ -327,7 +327,11 @@ async fn run(event_loop: EventLoop<()>, window: Arc<Window>) {
                                     });
                                 render_pass.set_pipeline(&wgpu_context_ref.pipeline);
                                 // (9)
-                                render_pass.set_bind_group(0, &wgpu_context_ref.bind_group, &[]);
+                                render_pass.set_bind_group(
+                                    0,
+                                    Some(&wgpu_context_ref.bind_group),
+                                    &[],
+                                );
                                 render_pass.draw(0..3, 0..1);
                             }
                             wgpu_context_ref.queue.submit(Some(encoder.finish()));
diff --git a/examples/src/water/mod.rs b/examples/src/water/mod.rs
index 7f766034a..505f5707e 100644
--- a/examples/src/water/mod.rs
+++ b/examples/src/water/mod.rs
@@ -630,7 +630,7 @@ impl crate::framework::Example for Example {
                     multiview: None,
                 });
             encoder.set_pipeline(&terrain_pipeline);
-            encoder.set_bind_group(0, &terrain_flipped_bind_group, &[]);
+            encoder.set_bind_group(0, Some(&terrain_flipped_bind_group), &[]);
             encoder.set_vertex_buffer(0, terrain_vertex_buf.slice(..));
             encoder.draw(0..terrain_vertices.len() as u32, 0..1);
             encoder.finish(&wgpu::RenderBundleDescriptor::default())
@@ -784,7 +784,7 @@ impl crate::framework::Example for Example {
                 occlusion_query_set: None,
             });
             rpass.set_pipeline(&self.terrain_pipeline);
-            rpass.set_bind_group(0, &self.terrain_normal_bind_group, &[]);
+            rpass.set_bind_group(0, Some(&self.terrain_normal_bind_group), &[]);
             rpass.set_vertex_buffer(0, self.terrain_vertex_buf.slice(..));
             rpass.draw(0..self.terrain_vertex_count as u32, 0..1);
         }
@@ -811,7 +811,7 @@ impl crate::framework::Example for Example {
             });
 
             rpass.set_pipeline(&self.water_pipeline);
-            rpass.set_bind_group(0, &self.water_bind_group, &[]);
+            rpass.set_bind_group(0, Some(&self.water_bind_group), &[]);
             rpass.set_vertex_buffer(0, self.water_vertex_buf.slice(..));
             rpass.draw(0..self.water_vertex_count as u32, 0..1);
         }
diff --git a/wgpu-core/src/command/bundle.rs b/wgpu-core/src/command/bundle.rs
index a7a43e1e2..775bd5f82 100644
--- a/wgpu-core/src/command/bundle.rs
+++ b/wgpu-core/src/command/bundle.rs
@@ -582,8 +582,15 @@ fn set_bind_group(
     dynamic_offsets: &[u32],
     index: u32,
     num_dynamic_offsets: usize,
-    bind_group_id: id::Id<id::markers::BindGroup>,
+    bind_group_id: Option<id::Id<id::markers::BindGroup>>,
 ) -> Result<(), RenderBundleErrorInner> {
+    if bind_group_id.is_none() {
+        // TODO: do appropriate cleanup for null bind_group.
+        return Ok(());
+    }
+
+    let bind_group_id = bind_group_id.unwrap();
+
     let bind_group = bind_group_guard
         .get_owned(bind_group_id)
         .map_err(|_| RenderCommandError::InvalidBindGroupId(bind_group_id))?;
@@ -981,12 +988,17 @@ impl RenderBundle {
                     num_dynamic_offsets,
                     bind_group,
                 } => {
-                    let raw_bg = bind_group.try_raw(snatch_guard)?;
+                    let mut bg = None;
+                    if bind_group.is_some() {
+                        let bind_group = bind_group.as_ref().unwrap();
+                        let raw_bg = bind_group.try_raw(snatch_guard)?;
+                        bg = Some(raw_bg);
+                    }
                     unsafe {
                         raw.set_bind_group(
                             pipeline_layout.as_ref().unwrap().raw(),
                             *index,
-                            raw_bg,
+                            bg,
                             &offsets[..*num_dynamic_offsets],
                         )
                     };
@@ -1501,7 +1513,7 @@ impl State {
                         let offsets = &contents.dynamic_offsets;
                         return Some(ArcRenderCommand::SetBindGroup {
                             index: i.try_into().unwrap(),
-                            bind_group: contents.bind_group.clone(),
+                            bind_group: Some(contents.bind_group.clone()),
                             num_dynamic_offsets: offsets.end - offsets.start,
                         });
                     }
@@ -1581,7 +1593,7 @@ pub mod bundle_ffi {
     pub unsafe extern "C" fn wgpu_render_bundle_set_bind_group(
         bundle: &mut RenderBundleEncoder,
         index: u32,
-        bind_group_id: id::BindGroupId,
+        bind_group_id: Option<id::BindGroupId>,
         offsets: *const DynamicOffset,
         offset_length: usize,
     ) {
diff --git a/wgpu-core/src/command/clear.rs b/wgpu-core/src/command/clear.rs
index 944dd40af..7a16f6db3 100644
--- a/wgpu-core/src/command/clear.rs
+++ b/wgpu-core/src/command/clear.rs
@@ -163,7 +163,7 @@ impl Global {
 
         // actual hal barrier & operation
         let dst_barrier = dst_pending.map(|pending| pending.into_hal(&dst_buffer, &snatch_guard));
-        let cmd_buf_raw = cmd_buf_data.encoder.open()?;
+        let cmd_buf_raw = cmd_buf_data.encoder.open(&cmd_buf.device)?;
         unsafe {
             cmd_buf_raw.transition_buffers(dst_barrier.as_slice());
             cmd_buf_raw.clear_buffer(dst_raw, offset..end_offset);
@@ -249,7 +249,7 @@ impl Global {
 
         let device = &cmd_buf.device;
         device.check_is_valid()?;
-        let (encoder, tracker) = cmd_buf_data.open_encoder_and_tracker()?;
+        let (encoder, tracker) = cmd_buf_data.open_encoder_and_tracker(&cmd_buf.device)?;
 
         let snatch_guard = device.snatchable_lock.read();
         clear_texture(
diff --git a/wgpu-core/src/command/compute.rs b/wgpu-core/src/command/compute.rs
index eb929740c..8dbd6efa1 100644
--- a/wgpu-core/src/command/compute.rs
+++ b/wgpu-core/src/command/compute.rs
@@ -433,10 +433,10 @@ impl Global {
         // We automatically keep extending command buffers over time, and because
         // we want to insert a command buffer _before_ what we're about to record,
         // we need to make sure to close the previous one.
-        encoder.close().map_pass_err(pass_scope)?;
+        encoder.close(&cmd_buf.device).map_pass_err(pass_scope)?;
         // will be reset to true if recording is done without errors
         *status = CommandEncoderStatus::Error;
-        let raw_encoder = encoder.open().map_pass_err(pass_scope)?;
+        let raw_encoder = encoder.open(&cmd_buf.device).map_pass_err(pass_scope)?;
 
         let mut state = State {
             binder: Binder::new(),
@@ -617,12 +617,12 @@ impl Global {
         } = state;
 
         // Stop the current command buffer.
-        encoder.close().map_pass_err(pass_scope)?;
+        encoder.close(&cmd_buf.device).map_pass_err(pass_scope)?;
 
         // Create a new command buffer, which we will insert _before_ the body of the compute pass.
         //
         // Use that buffer to insert barriers and clear discarded images.
-        let transit = encoder.open().map_pass_err(pass_scope)?;
+        let transit = encoder.open(&cmd_buf.device).map_pass_err(pass_scope)?;
         fixup_discarded_surfaces(
             pending_discard_init_fixups.into_iter(),
             transit,
@@ -637,7 +637,9 @@ impl Global {
             &snatch_guard,
         );
         // Close the command buffer, and swap it with the previous.
-        encoder.close_and_swap().map_pass_err(pass_scope)?;
+        encoder
+            .close_and_swap(&cmd_buf.device)
+            .map_pass_err(pass_scope)?;
 
         Ok(())
     }
@@ -649,10 +651,8 @@ fn set_bind_group(
     dynamic_offsets: &[DynamicOffset],
     index: u32,
     num_dynamic_offsets: usize,
-    bind_group: Arc<BindGroup>,
+    bind_group: Option<Arc<BindGroup>>,
 ) -> Result<(), ComputePassErrorInner> {
-    bind_group.same_device_as(cmd_buf)?;
-
     let max_bind_groups = state.device.limits.max_bind_groups;
     if index >= max_bind_groups {
         return Err(ComputePassErrorInner::BindGroupIndexOutOfRange {
@@ -668,7 +668,16 @@ fn set_bind_group(
     );
     state.dynamic_offset_count += num_dynamic_offsets;
 
+    if bind_group.is_none() {
+        // TODO: Handle bind_group None.
+        return Ok(());
+    }
+
+    let bind_group = bind_group.unwrap();
     let bind_group = state.tracker.bind_groups.insert_single(bind_group);
+
+    bind_group.same_device_as(cmd_buf)?;
+
     bind_group.validate_dynamic_bindings(index, &state.temp_offsets)?;
 
     state
@@ -700,7 +709,7 @@ fn set_bind_group(
                     state.raw_encoder.set_bind_group(
                         pipeline_layout,
                         index + i as u32,
-                        raw_bg,
+                        Some(raw_bg),
                         &e.dynamic_offsets,
                     );
                 }
@@ -745,7 +754,7 @@ fn set_pipeline(
                         state.raw_encoder.set_bind_group(
                             pipeline.layout.raw(),
                             start_index as u32 + i as u32,
-                            raw_bg,
+                            Some(raw_bg),
                             &e.dynamic_offsets,
                         );
                     }
@@ -952,7 +961,7 @@ impl Global {
         &self,
         pass: &mut ComputePass,
         index: u32,
-        bind_group_id: id::BindGroupId,
+        bind_group_id: Option<id::BindGroupId>,
         offsets: &[DynamicOffset],
     ) -> Result<(), ComputePassError> {
         let scope = PassErrorScope::SetBindGroup;
@@ -973,12 +982,18 @@ impl Global {
             return Ok(());
         }
 
-        let hub = &self.hub;
-        let bind_group = hub
-            .bind_groups
-            .get(bind_group_id)
-            .map_err(|_| ComputePassErrorInner::InvalidBindGroupId(bind_group_id))
-            .map_pass_err(scope)?;
+        let mut bind_group = None;
+        if bind_group_id.is_some() {
+            let bind_group_id = bind_group_id.unwrap();
+
+            let hub = &self.hub;
+            let bg = hub
+                .bind_groups
+                .get(bind_group_id)
+                .map_err(|_| ComputePassErrorInner::InvalidBindGroupId(bind_group_id))
+                .map_pass_err(scope)?;
+            bind_group = Some(bg);
+        }
 
         base.commands.push(ArcComputeCommand::SetBindGroup {
             index,
diff --git a/wgpu-core/src/command/compute_command.rs b/wgpu-core/src/command/compute_command.rs
index 07bceafc5..6596b8e6a 100644
--- a/wgpu-core/src/command/compute_command.rs
+++ b/wgpu-core/src/command/compute_command.rs
@@ -13,7 +13,7 @@ pub enum ComputeCommand {
     SetBindGroup {
         index: u32,
         num_dynamic_offsets: usize,
-        bind_group_id: id::BindGroupId,
+        bind_group_id: Option<id::BindGroupId>,
     },
 
     SetPipeline(id::ComputePipelineId),
@@ -89,16 +89,29 @@ impl ComputeCommand {
                         index,
                         num_dynamic_offsets,
                         bind_group_id,
-                    } => ArcComputeCommand::SetBindGroup {
-                        index,
-                        num_dynamic_offsets,
-                        bind_group: bind_group_guard.get_owned(bind_group_id).map_err(|_| {
+                    } => {
+                        if bind_group_id.is_none() {
+                            return Ok(ArcComputeCommand::SetBindGroup {
+                                index,
+                                num_dynamic_offsets,
+                                bind_group: None,
+                            });
+                        }
+
+                        let bind_group_id = bind_group_id.unwrap();
+                        let bg = bind_group_guard.get_owned(bind_group_id).map_err(|_| {
                             ComputePassError {
                                 scope: PassErrorScope::SetBindGroup,
                                 inner: ComputePassErrorInner::InvalidBindGroupId(bind_group_id),
                             }
-                        })?,
-                    },
+                        })?;
+
+                        ArcComputeCommand::SetBindGroup {
+                            index,
+                            num_dynamic_offsets,
+                            bind_group: Some(bg),
+                        }
+                    }
 
                     ComputeCommand::SetPipeline(pipeline_id) => ArcComputeCommand::SetPipeline(
                         pipelines_guard
@@ -185,7 +198,7 @@ pub enum ArcComputeCommand {
     SetBindGroup {
         index: u32,
         num_dynamic_offsets: usize,
-        bind_group: Arc<BindGroup>,
+        bind_group: Option<Arc<BindGroup>>,
     },
 
     SetPipeline(Arc<ComputePipeline>),
diff --git a/wgpu-core/src/command/mod.rs b/wgpu-core/src/command/mod.rs
index d58748ed9..91af31662 100644
--- a/wgpu-core/src/command/mod.rs
+++ b/wgpu-core/src/command/mod.rs
@@ -172,10 +172,10 @@ impl CommandEncoder {
     /// [l]: CommandEncoder::list
     /// [`transition_buffers`]: hal::CommandEncoder::transition_buffers
     /// [`transition_textures`]: hal::CommandEncoder::transition_textures
-    fn close_and_swap(&mut self) -> Result<(), DeviceError> {
+    fn close_and_swap(&mut self, device: &Device) -> Result<(), DeviceError> {
         if self.is_open {
             self.is_open = false;
-            let new = unsafe { self.raw.end_encoding()? };
+            let new = unsafe { self.raw.end_encoding() }.map_err(|e| device.handle_hal_error(e))?;
             self.list.insert(self.list.len() - 1, new);
         }
 
@@ -192,10 +192,11 @@ impl CommandEncoder {
     /// On return, the underlying hal encoder is closed.
     ///
     /// [l]: CommandEncoder::list
-    fn close(&mut self) -> Result<(), DeviceError> {
+    fn close(&mut self, device: &Device) -> Result<(), DeviceError> {
         if self.is_open {
             self.is_open = false;
-            let cmd_buf = unsafe { self.raw.end_encoding()? };
+            let cmd_buf =
+                unsafe { self.raw.end_encoding() }.map_err(|e| device.handle_hal_error(e))?;
             self.list.push(cmd_buf);
         }
 
@@ -215,11 +216,15 @@ impl CommandEncoder {
     /// Begin recording a new command buffer, if we haven't already.
     ///
     /// The underlying hal encoder is put in the "recording" state.
-    pub(crate) fn open(&mut self) -> Result<&mut dyn hal::DynCommandEncoder, DeviceError> {
+    pub(crate) fn open(
+        &mut self,
+        device: &Device,
+    ) -> Result<&mut dyn hal::DynCommandEncoder, DeviceError> {
         if !self.is_open {
             self.is_open = true;
             let hal_label = self.hal_label.as_deref();
-            unsafe { self.raw.begin_encoding(hal_label)? };
+            unsafe { self.raw.begin_encoding(hal_label) }
+                .map_err(|e| device.handle_hal_error(e))?;
         }
 
         Ok(self.raw.as_mut())
@@ -229,9 +234,9 @@ impl CommandEncoder {
     /// its own label.
     ///
     /// The underlying hal encoder is put in the "recording" state.
-    fn open_pass(&mut self, hal_label: Option<&str>) -> Result<(), DeviceError> {
+    fn open_pass(&mut self, hal_label: Option<&str>, device: &Device) -> Result<(), DeviceError> {
         self.is_open = true;
-        unsafe { self.raw.begin_encoding(hal_label)? };
+        unsafe { self.raw.begin_encoding(hal_label) }.map_err(|e| device.handle_hal_error(e))?;
 
         Ok(())
     }
@@ -276,8 +281,9 @@ pub struct CommandBufferMutable {
 impl CommandBufferMutable {
     pub(crate) fn open_encoder_and_tracker(
         &mut self,
+        device: &Device,
     ) -> Result<(&mut dyn hal::DynCommandEncoder, &mut Tracker), DeviceError> {
-        let encoder = self.encoder.open()?;
+        let encoder = self.encoder.open(device)?;
         let tracker = &mut self.trackers;
 
         Ok((encoder, tracker))
@@ -621,7 +627,7 @@ impl Global {
                 let cmd_buf_data = cmd_buf_data.as_mut().unwrap();
                 match cmd_buf_data.status {
                     CommandEncoderStatus::Recording => {
-                        if let Err(e) = cmd_buf_data.encoder.close() {
+                        if let Err(e) = cmd_buf_data.encoder.close(&cmd_buf.device) {
                             Some(e.into())
                         } else {
                             cmd_buf_data.status = CommandEncoderStatus::Finished;
@@ -671,7 +677,7 @@ impl Global {
             list.push(TraceCommand::PushDebugGroup(label.to_string()));
         }
 
-        let cmd_buf_raw = cmd_buf_data.encoder.open()?;
+        let cmd_buf_raw = cmd_buf_data.encoder.open(&cmd_buf.device)?;
         if !self
             .instance
             .flags
@@ -713,7 +719,7 @@ impl Global {
             .flags
             .contains(wgt::InstanceFlags::DISCARD_HAL_LABELS)
         {
-            let cmd_buf_raw = cmd_buf_data.encoder.open()?;
+            let cmd_buf_raw = cmd_buf_data.encoder.open(&cmd_buf.device)?;
             unsafe {
                 cmd_buf_raw.insert_debug_marker(label);
             }
@@ -744,7 +750,7 @@ impl Global {
             list.push(TraceCommand::PopDebugGroup);
         }
 
-        let cmd_buf_raw = cmd_buf_data.encoder.open()?;
+        let cmd_buf_raw = cmd_buf_data.encoder.open(&cmd_buf.device)?;
         if !self
             .instance
             .flags
@@ -805,7 +811,7 @@ impl<T: Copy + PartialEq> Default for StateChange<T> {
 
 #[derive(Debug)]
 struct BindGroupStateChange {
-    last_states: [StateChange<id::BindGroupId>; hal::MAX_BIND_GROUPS],
+    last_states: [StateChange<Option<id::BindGroupId>>; hal::MAX_BIND_GROUPS],
 }
 
 impl BindGroupStateChange {
@@ -817,7 +823,7 @@ impl BindGroupStateChange {
 
     fn set_and_check_redundant(
         &mut self,
-        bind_group_id: id::BindGroupId,
+        bind_group_id: Option<id::BindGroupId>,
         index: u32,
         dynamic_offsets: &mut Vec<u32>,
         offsets: &[wgt::DynamicOffset],
diff --git a/wgpu-core/src/command/query.rs b/wgpu-core/src/command/query.rs
index de5103ac8..6d9fa8021 100644
--- a/wgpu-core/src/command/query.rs
+++ b/wgpu-core/src/command/query.rs
@@ -346,7 +346,7 @@ impl Global {
         let encoder = &mut cmd_buf_data.encoder;
         let tracker = &mut cmd_buf_data.trackers;
 
-        let raw_encoder = encoder.open()?;
+        let raw_encoder = encoder.open(&cmd_buf.device)?;
 
         let query_set = hub
             .query_sets
@@ -397,7 +397,7 @@ impl Global {
         let encoder = &mut cmd_buf_data.encoder;
         let tracker = &mut cmd_buf_data.trackers;
         let buffer_memory_init_actions = &mut cmd_buf_data.buffer_memory_init_actions;
-        let raw_encoder = encoder.open()?;
+        let raw_encoder = encoder.open(&cmd_buf.device)?;
 
         if destination_offset % wgt::QUERY_RESOLVE_BUFFER_ALIGNMENT != 0 {
             return Err(QueryError::Resolve(ResolveError::BufferOffsetAlignment));
diff --git a/wgpu-core/src/command/render.rs b/wgpu-core/src/command/render.rs
index f85696ced..17e04d14c 100644
--- a/wgpu-core/src/command/render.rs
+++ b/wgpu-core/src/command/render.rs
@@ -635,8 +635,8 @@ pub enum RenderPassErrorInner {
     SurfaceTextureDropped,
     #[error("Not enough memory left for render pass")]
     OutOfMemory,
-    #[error("The bind group at index {0:?} is invalid")]
-    InvalidBindGroup(u32),
+    #[error("BindGroupId {0:?} is invalid")]
+    InvalidBindGroupId(id::BindGroupId),
     #[error("Unable to clear non-present/read-only depth")]
     InvalidDepthOps,
     #[error("Unable to clear non-present/read-only stencil")]
@@ -1588,10 +1588,12 @@ impl Global {
             // We automatically keep extending command buffers over time, and because
             // we want to insert a command buffer _before_ what we're about to record,
             // we need to make sure to close the previous one.
-            encoder.close().map_pass_err(pass_scope)?;
+            encoder.close(&cmd_buf.device).map_pass_err(pass_scope)?;
             // We will reset this to `Recording` if we succeed, acts as a fail-safe.
             *status = CommandEncoderStatus::Error;
-            encoder.open_pass(hal_label).map_pass_err(pass_scope)?;
+            encoder
+                .open_pass(hal_label, &cmd_buf.device)
+                .map_pass_err(pass_scope)?;
 
             let info = RenderPassInfo::start(
                 device,
@@ -1894,7 +1896,7 @@ impl Global {
                 .finish(state.raw_encoder, state.snatch_guard)
                 .map_pass_err(pass_scope)?;
 
-            encoder.close().map_pass_err(pass_scope)?;
+            encoder.close(&cmd_buf.device).map_pass_err(pass_scope)?;
             (trackers, pending_discard_init_fixups)
         };
 
@@ -1906,7 +1908,7 @@ impl Global {
         let tracker = &mut cmd_buf_data.trackers;
 
         {
-            let transit = encoder.open().map_pass_err(pass_scope)?;
+            let transit = encoder.open(&cmd_buf.device).map_pass_err(pass_scope)?;
 
             fixup_discarded_surfaces(
                 pending_discard_init_fixups.into_iter(),
@@ -1922,7 +1924,9 @@ impl Global {
         }
 
         *status = CommandEncoderStatus::Recording;
-        encoder.close_and_swap().map_pass_err(pass_scope)?;
+        encoder
+            .close_and_swap(&cmd_buf.device)
+            .map_pass_err(pass_scope)?;
 
         Ok(())
     }
@@ -1934,12 +1938,16 @@ fn set_bind_group(
     dynamic_offsets: &[DynamicOffset],
     index: u32,
     num_dynamic_offsets: usize,
-    bind_group: Arc<BindGroup>,
+    bind_group: Option<Arc<BindGroup>>,
 ) -> Result<(), RenderPassErrorInner> {
-    api_log!(
-        "RenderPass::set_bind_group {index} {}",
-        bind_group.error_ident()
-    );
+    if bind_group.is_none() {
+        api_log!("RenderPass::set_bind_group {index} None");
+    } else {
+        api_log!(
+            "RenderPass::set_bind_group {index} {}",
+            bind_group.as_ref().unwrap().error_ident()
+        );
+    }
 
     let max_bind_groups = state.device.limits.max_bind_groups;
     if index >= max_bind_groups {
@@ -1957,6 +1965,12 @@ fn set_bind_group(
     );
     state.dynamic_offset_count += num_dynamic_offsets;
 
+    if bind_group.is_none() {
+        // TODO: Handle bind_group None.
+        return Ok(());
+    }
+
+    let bind_group = bind_group.unwrap();
     let bind_group = state.tracker.bind_groups.insert_single(bind_group);
 
     bind_group.same_device_as(cmd_buf.as_ref())?;
@@ -1999,7 +2013,7 @@ fn set_bind_group(
                     state.raw_encoder.set_bind_group(
                         pipeline_layout,
                         index + i as u32,
-                        raw_bg,
+                        Some(raw_bg),
                         &e.dynamic_offsets,
                     );
                 }
@@ -2073,7 +2087,7 @@ fn set_pipeline(
                         state.raw_encoder.set_bind_group(
                             pipeline.layout.raw(),
                             start_index as u32 + i as u32,
-                            raw_bg,
+                            Some(raw_bg),
                             &e.dynamic_offsets,
                         );
                     }
@@ -2788,7 +2802,7 @@ impl Global {
         &self,
         pass: &mut RenderPass,
         index: u32,
-        bind_group_id: id::BindGroupId,
+        bind_group_id: Option<id::BindGroupId>,
         offsets: &[DynamicOffset],
     ) -> Result<(), RenderPassError> {
         let scope = PassErrorScope::SetBindGroup;
@@ -2808,12 +2822,18 @@ impl Global {
             return Ok(());
         }
 
-        let hub = &self.hub;
-        let bind_group = hub
-            .bind_groups
-            .get(bind_group_id)
-            .map_err(|_| RenderPassErrorInner::InvalidBindGroup(index))
-            .map_pass_err(scope)?;
+        let mut bind_group = None;
+        if bind_group_id.is_some() {
+            let bind_group_id = bind_group_id.unwrap();
+
+            let hub = &self.hub;
+            let bg = hub
+                .bind_groups
+                .get(bind_group_id)
+                .map_err(|_| RenderPassErrorInner::InvalidBindGroupId(bind_group_id))
+                .map_pass_err(scope)?;
+            bind_group = Some(bg);
+        }
 
         base.commands.push(ArcRenderCommand::SetBindGroup {
             index,
diff --git a/wgpu-core/src/command/render_command.rs b/wgpu-core/src/command/render_command.rs
index ea82284d1..7b8a2f283 100644
--- a/wgpu-core/src/command/render_command.rs
+++ b/wgpu-core/src/command/render_command.rs
@@ -17,7 +17,7 @@ pub enum RenderCommand {
     SetBindGroup {
         index: u32,
         num_dynamic_offsets: usize,
-        bind_group_id: id::BindGroupId,
+        bind_group_id: Option<id::BindGroupId>,
     },
     SetPipeline(id::RenderPipelineId),
     SetIndexBuffer {
@@ -147,16 +147,29 @@ impl RenderCommand {
                         index,
                         num_dynamic_offsets,
                         bind_group_id,
-                    } => ArcRenderCommand::SetBindGroup {
-                        index,
-                        num_dynamic_offsets,
-                        bind_group: bind_group_guard.get_owned(bind_group_id).map_err(|_| {
+                    } => {
+                        if bind_group_id.is_none() {
+                            return Ok(ArcRenderCommand::SetBindGroup {
+                                index,
+                                num_dynamic_offsets,
+                                bind_group: None,
+                            });
+                        }
+
+                        let bind_group_id = bind_group_id.unwrap();
+                        let bg = bind_group_guard.get_owned(bind_group_id).map_err(|_| {
                             RenderPassError {
                                 scope: PassErrorScope::SetBindGroup,
-                                inner: RenderPassErrorInner::InvalidBindGroup(index),
+                                inner: RenderPassErrorInner::InvalidBindGroupId(bind_group_id),
                             }
-                        })?,
-                    },
+                        })?;
+
+                        ArcRenderCommand::SetBindGroup {
+                            index,
+                            num_dynamic_offsets,
+                            bind_group: Some(bg),
+                        }
+                    }
 
                     RenderCommand::SetPipeline(pipeline_id) => ArcRenderCommand::SetPipeline(
                         pipelines_guard
@@ -384,7 +397,7 @@ pub enum ArcRenderCommand {
     SetBindGroup {
         index: u32,
         num_dynamic_offsets: usize,
-        bind_group: Arc<BindGroup>,
+        bind_group: Option<Arc<BindGroup>>,
     },
     SetPipeline(Arc<RenderPipeline>),
     SetIndexBuffer {
diff --git a/wgpu-core/src/command/transfer.rs b/wgpu-core/src/command/transfer.rs
index de5ef9ed8..aeceb29af 100644
--- a/wgpu-core/src/command/transfer.rs
+++ b/wgpu-core/src/command/transfer.rs
@@ -432,7 +432,7 @@ fn handle_texture_init(
 
     // In rare cases we may need to insert an init operation immediately onto the command buffer.
     if !immediate_inits.is_empty() {
-        let cmd_buf_raw = encoder.open()?;
+        let cmd_buf_raw = encoder.open(device)?;
         for init in immediate_inits {
             clear_texture(
                 &init.texture,
@@ -684,7 +684,7 @@ impl Global {
             dst_offset: destination_offset,
             size: wgt::BufferSize::new(size).unwrap(),
         };
-        let cmd_buf_raw = cmd_buf_data.encoder.open()?;
+        let cmd_buf_raw = cmd_buf_data.encoder.open(&cmd_buf.device)?;
         let barriers = src_barrier
             .into_iter()
             .chain(dst_barrier)
@@ -855,7 +855,7 @@ impl Global {
             })
             .collect::<Vec<_>>();
 
-        let cmd_buf_raw = encoder.open()?;
+        let cmd_buf_raw = encoder.open(&cmd_buf.device)?;
         unsafe {
             cmd_buf_raw.transition_textures(&dst_barrier);
             cmd_buf_raw.transition_buffers(src_barrier.as_slice());
@@ -1030,7 +1030,7 @@ impl Global {
                 }
             })
             .collect::<Vec<_>>();
-        let cmd_buf_raw = encoder.open()?;
+        let cmd_buf_raw = encoder.open(&cmd_buf.device)?;
         unsafe {
             cmd_buf_raw.transition_buffers(dst_barrier.as_slice());
             cmd_buf_raw.transition_textures(&src_barrier);
@@ -1209,7 +1209,7 @@ impl Global {
                 }
             })
             .collect::<Vec<_>>();
-        let cmd_buf_raw = cmd_buf_data.encoder.open()?;
+        let cmd_buf_raw = cmd_buf_data.encoder.open(&cmd_buf.device)?;
         unsafe {
             cmd_buf_raw.transition_textures(&barriers);
             cmd_buf_raw.copy_texture_to_texture(
diff --git a/wgpu-core/src/device/global.rs b/wgpu-core/src/device/global.rs
index dad7d96db..a86651d87 100644
--- a/wgpu-core/src/device/global.rs
+++ b/wgpu-core/src/device/global.rs
@@ -270,21 +270,27 @@ impl Global {
 
         let snatch_guard = device.snatchable_lock.read();
         let raw_buf = buffer.try_raw(&snatch_guard)?;
-        unsafe {
-            let mapping = device
+
+        let mapping = unsafe {
+            device
                 .raw()
                 .map_buffer(raw_buf, offset..offset + data.len() as u64)
-                .map_err(DeviceError::from)?;
-            std::ptr::copy_nonoverlapping(data.as_ptr(), mapping.ptr.as_ptr(), data.len());
-            if !mapping.is_coherent {
-                #[allow(clippy::single_range_in_vec_init)]
+        }
+        .map_err(|e| device.handle_hal_error(e))?;
+
+        unsafe { std::ptr::copy_nonoverlapping(data.as_ptr(), mapping.ptr.as_ptr(), data.len()) };
+
+        if !mapping.is_coherent {
+            #[allow(clippy::single_range_in_vec_init)]
+            unsafe {
                 device
                     .raw()
-                    .flush_mapped_ranges(raw_buf, &[offset..offset + data.len() as u64]);
-            }
-            device.raw().unmap_buffer(raw_buf);
+                    .flush_mapped_ranges(raw_buf, &[offset..offset + data.len() as u64])
+            };
         }
 
+        unsafe { device.raw().unmap_buffer(raw_buf) };
+
         Ok(())
     }
 
@@ -2006,7 +2012,9 @@ impl Global {
                             hal::SurfaceError::Outdated | hal::SurfaceError::Lost => {
                                 E::InvalidSurface
                             }
-                            hal::SurfaceError::Device(error) => E::Device(error.into()),
+                            hal::SurfaceError::Device(error) => {
+                                E::Device(device.handle_hal_error(error))
+                            }
                             hal::SurfaceError::Other(message) => {
                                 log::error!("surface configuration failed: {}", message);
                                 E::InvalidSurface
@@ -2285,17 +2293,7 @@ impl Global {
             // check for empty queues and a DeviceLostClosure. At that time,
             // the DeviceLostClosure will be called with "destroyed" as the
             // reason.
-            device.valid.store(false, Ordering::Relaxed);
-        }
-    }
-
-    pub fn device_mark_lost(&self, device_id: DeviceId, message: &str) {
-        api_log!("Device::mark_lost {device_id:?}");
-
-        let hub = &self.hub;
-
-        if let Ok(device) = hub.devices.get(device_id) {
-            device.lose(message);
+            device.valid.store(false, Ordering::Release);
         }
     }
 
diff --git a/wgpu-core/src/device/mod.rs b/wgpu-core/src/device/mod.rs
index b9c346775..71bbce19c 100644
--- a/wgpu-core/src/device/mod.rs
+++ b/wgpu-core/src/device/mod.rs
@@ -308,7 +308,7 @@ fn map_buffer(
     let raw_buffer = buffer.try_raw(snatch_guard)?;
     let mapping = unsafe {
         raw.map_buffer(raw_buffer, offset..offset + size)
-            .map_err(DeviceError::from)?
+            .map_err(|e| buffer.device.handle_hal_error(e))?
     };
 
     if !mapping.is_coherent && kind == HostMap::Read {
@@ -420,13 +420,16 @@ pub enum DeviceError {
     DeviceMismatch(#[from] Box<DeviceMismatch>),
 }
 
-impl From<hal::DeviceError> for DeviceError {
-    fn from(error: hal::DeviceError) -> Self {
+impl DeviceError {
+    /// Only use this function in contexts where there is no `Device`.
+    ///
+    /// Use [`Device::handle_hal_error`] otherwise.
+    pub fn from_hal(error: hal::DeviceError) -> Self {
         match error {
-            hal::DeviceError::Lost => DeviceError::Lost,
-            hal::DeviceError::OutOfMemory => DeviceError::OutOfMemory,
-            hal::DeviceError::ResourceCreationFailed => DeviceError::ResourceCreationFailed,
-            hal::DeviceError::Unexpected => DeviceError::Lost,
+            hal::DeviceError::Lost => Self::Lost,
+            hal::DeviceError::OutOfMemory => Self::OutOfMemory,
+            hal::DeviceError::ResourceCreationFailed => Self::ResourceCreationFailed,
+            hal::DeviceError::Unexpected => Self::Lost,
         }
     }
 }
diff --git a/wgpu-core/src/device/queue.rs b/wgpu-core/src/device/queue.rs
index e516e0dac..7058799a0 100644
--- a/wgpu-core/src/device/queue.rs
+++ b/wgpu-core/src/device/queue.rs
@@ -269,17 +269,20 @@ impl PendingWrites {
     fn pre_submit(
         &mut self,
         command_allocator: &CommandAllocator,
-        device: &dyn hal::DynDevice,
-        queue: &dyn hal::DynQueue,
+        device: &Device,
+        queue: &Queue,
     ) -> Result<Option<EncoderInFlight>, DeviceError> {
         if self.is_recording {
             let pending_buffers = mem::take(&mut self.dst_buffers);
             let pending_textures = mem::take(&mut self.dst_textures);
 
-            let cmd_buf = unsafe { self.command_encoder.end_encoding()? };
+            let cmd_buf = unsafe { self.command_encoder.end_encoding() }
+                .map_err(|e| device.handle_hal_error(e))?;
             self.is_recording = false;
 
-            let new_encoder = command_allocator.acquire_encoder(device, queue)?;
+            let new_encoder = command_allocator
+                .acquire_encoder(device.raw(), queue.raw())
+                .map_err(|e| device.handle_hal_error(e))?;
 
             let encoder = EncoderInFlight {
                 raw: mem::replace(&mut self.command_encoder, new_encoder),
@@ -1194,14 +1197,12 @@ impl Global {
 
                         // execute resource transitions
                         unsafe {
-                            baked
-                                .encoder
-                                .begin_encoding(hal_label(
-                                    Some("(wgpu internal) Transit"),
-                                    device.instance_flags,
-                                ))
-                                .map_err(DeviceError::from)?
-                        };
+                            baked.encoder.begin_encoding(hal_label(
+                                Some("(wgpu internal) Transit"),
+                                device.instance_flags,
+                            ))
+                        }
+                        .map_err(|e| device.handle_hal_error(e))?;
 
                         //Note: locking the trackers has to be done after the storages
                         let mut trackers = device.trackers.lock();
@@ -1224,14 +1225,12 @@ impl Global {
                         // but here we have a command encoder by hand, so it's easier to use it.
                         if !used_surface_textures.is_empty() {
                             unsafe {
-                                baked
-                                    .encoder
-                                    .begin_encoding(hal_label(
-                                        Some("(wgpu internal) Present"),
-                                        device.instance_flags,
-                                    ))
-                                    .map_err(DeviceError::from)?
-                            };
+                                baked.encoder.begin_encoding(hal_label(
+                                    Some("(wgpu internal) Present"),
+                                    device.instance_flags,
+                                ))
+                            }
+                            .map_err(|e| device.handle_hal_error(e))?;
                             let texture_barriers = trackers
                                 .textures
                                 .set_from_usage_scope_and_drain_transitions(
@@ -1299,7 +1298,7 @@ impl Global {
             }
 
             if let Some(pending_execution) =
-                pending_writes.pre_submit(&device.command_allocator, device.raw(), queue.raw())?
+                pending_writes.pre_submit(&device.command_allocator, device, &queue)?
             {
                 active_executions.insert(0, pending_execution);
             }
@@ -1324,15 +1323,13 @@ impl Global {
                 }
 
                 unsafe {
-                    queue
-                        .raw()
-                        .submit(
-                            &hal_command_buffers,
-                            &submit_surface_textures,
-                            (fence.as_mut(), submit_index),
-                        )
-                        .map_err(DeviceError::from)?;
+                    queue.raw().submit(
+                        &hal_command_buffers,
+                        &submit_surface_textures,
+                        (fence.as_mut(), submit_index),
+                    )
                 }
+                .map_err(|e| device.handle_hal_error(e))?;
 
                 // Advance the successful submission index.
                 device
diff --git a/wgpu-core/src/device/resource.rs b/wgpu-core/src/device/resource.rs
index cfa0a460e..253f2ca94 100644
--- a/wgpu-core/src/device/resource.rs
+++ b/wgpu-core/src/device/resource.rs
@@ -227,31 +227,29 @@ impl Device {
         desc: &DeviceDescriptor,
         trace_path: Option<&std::path::Path>,
         instance_flags: wgt::InstanceFlags,
-    ) -> Result<Self, CreateDeviceError> {
+    ) -> Result<Self, DeviceError> {
         #[cfg(not(feature = "trace"))]
         if let Some(_) = trace_path {
             log::error!("Feature 'trace' is not enabled");
         }
-        let fence =
-            unsafe { raw_device.create_fence() }.map_err(|_| CreateDeviceError::OutOfMemory)?;
+        let fence = unsafe { raw_device.create_fence() }.map_err(DeviceError::from_hal)?;
 
         let command_allocator = command::CommandAllocator::new();
         let pending_encoder = command_allocator
             .acquire_encoder(raw_device.as_ref(), raw_queue)
-            .map_err(|_| CreateDeviceError::OutOfMemory)?;
+            .map_err(DeviceError::from_hal)?;
         let mut pending_writes = PendingWrites::new(pending_encoder);
 
         // Create zeroed buffer used for texture clears.
         let zero_buffer = unsafe {
-            raw_device
-                .create_buffer(&hal::BufferDescriptor {
-                    label: hal_label(Some("(wgpu internal) zero init buffer"), instance_flags),
-                    size: ZERO_BUFFER_SIZE,
-                    usage: hal::BufferUses::COPY_SRC | hal::BufferUses::COPY_DST,
-                    memory_flags: hal::MemoryFlags::empty(),
-                })
-                .map_err(DeviceError::from)?
-        };
+            raw_device.create_buffer(&hal::BufferDescriptor {
+                label: hal_label(Some("(wgpu internal) zero init buffer"), instance_flags),
+                size: ZERO_BUFFER_SIZE,
+                usage: hal::BufferUses::COPY_SRC | hal::BufferUses::COPY_DST,
+                memory_flags: hal::MemoryFlags::empty(),
+            })
+        }
+        .map_err(DeviceError::from_hal)?;
         pending_writes.activate();
         unsafe {
             pending_writes
@@ -339,6 +337,18 @@ impl Device {
         }
     }
 
+    pub fn handle_hal_error(&self, error: hal::DeviceError) -> DeviceError {
+        match error {
+            hal::DeviceError::OutOfMemory => {}
+            hal::DeviceError::Lost
+            | hal::DeviceError::ResourceCreationFailed
+            | hal::DeviceError::Unexpected => {
+                self.lose(&error.to_string());
+            }
+        }
+        DeviceError::from_hal(error)
+    }
+
     pub(crate) fn release_queue(&self, queue: Box<dyn hal::DynQueue>) {
         assert!(self.queue_to_drop.set(queue).is_ok());
     }
@@ -441,11 +451,8 @@ impl Device {
             wgt::Maintain::Wait => self
                 .last_successful_submission_index
                 .load(Ordering::Acquire),
-            wgt::Maintain::Poll => unsafe {
-                self.raw()
-                    .get_fence_value(fence.as_ref())
-                    .map_err(DeviceError::from)?
-            },
+            wgt::Maintain::Poll => unsafe { self.raw().get_fence_value(fence.as_ref()) }
+                .map_err(|e| self.handle_hal_error(e))?,
         };
 
         // If necessary, wait for that submission to complete.
@@ -453,8 +460,8 @@ impl Device {
             unsafe {
                 self.raw()
                     .wait(fence.as_ref(), submission_index, CLEANUP_WAIT_MS)
-                    .map_err(DeviceError::from)?
-            };
+            }
+            .map_err(|e| self.handle_hal_error(e))?;
         }
         log::trace!("Device::maintain: waiting for submission index {submission_index}");
 
@@ -588,7 +595,8 @@ impl Device {
             usage,
             memory_flags: hal::MemoryFlags::empty(),
         };
-        let buffer = unsafe { self.raw().create_buffer(&hal_desc) }.map_err(DeviceError::from)?;
+        let buffer =
+            unsafe { self.raw().create_buffer(&hal_desc) }.map_err(|e| self.handle_hal_error(e))?;
 
         let buffer = Buffer {
             raw: Snatchable::new(buffer),
@@ -935,11 +943,8 @@ impl Device {
             view_formats: hal_view_formats,
         };
 
-        let raw_texture = unsafe {
-            self.raw()
-                .create_texture(&hal_desc)
-                .map_err(DeviceError::from)?
-        };
+        let raw_texture = unsafe { self.raw().create_texture(&hal_desc) }
+            .map_err(|e| self.handle_hal_error(e))?;
 
         let clear_mode = if hal_usage
             .intersects(hal::TextureUses::DEPTH_STENCIL_WRITE | hal::TextureUses::COLOR_TARGET)
@@ -982,7 +987,7 @@ impl Device {
                                 unsafe {
                                     self.raw().create_texture_view(raw_texture.as_ref(), &desc)
                                 }
-                                .map_err(DeviceError::from)?,
+                                .map_err(|e| self.handle_hal_error(e))?,
                             ));
                         };
                     }
@@ -1288,11 +1293,8 @@ impl Device {
             range: resolved_range,
         };
 
-        let raw = unsafe {
-            self.raw()
-                .create_texture_view(texture_raw, &hal_desc)
-                .map_err(|_| resource::CreateTextureViewError::OutOfMemory)?
-        };
+        let raw = unsafe { self.raw().create_texture_view(texture_raw, &hal_desc) }
+            .map_err(|e| self.handle_hal_error(e))?;
 
         let selector = TextureSelector {
             mips: desc.range.base_mip_level..mip_level_end,
@@ -1423,11 +1425,8 @@ impl Device {
             border_color: desc.border_color,
         };
 
-        let raw = unsafe {
-            self.raw()
-                .create_sampler(&hal_desc)
-                .map_err(DeviceError::from)?
-        };
+        let raw = unsafe { self.raw().create_sampler(&hal_desc) }
+            .map_err(|e| self.handle_hal_error(e))?;
 
         let sampler = Sampler {
             raw: ManuallyDrop::new(raw),
@@ -1551,7 +1550,7 @@ impl Device {
             Err(error) => {
                 return Err(match error {
                     hal::ShaderError::Device(error) => {
-                        pipeline::CreateShaderModuleError::Device(error.into())
+                        pipeline::CreateShaderModuleError::Device(self.handle_hal_error(error))
                     }
                     hal::ShaderError::Compilation(ref msg) => {
                         log::error!("Shader error: {}", msg);
@@ -1592,7 +1591,7 @@ impl Device {
             Err(error) => {
                 return Err(match error {
                     hal::ShaderError::Device(error) => {
-                        pipeline::CreateShaderModuleError::Device(error.into())
+                        pipeline::CreateShaderModuleError::Device(self.handle_hal_error(error))
                     }
                     hal::ShaderError::Compilation(ref msg) => {
                         log::error!("Shader error: {}", msg);
@@ -1624,7 +1623,8 @@ impl Device {
 
         let encoder = self
             .command_allocator
-            .acquire_encoder(self.raw(), queue.raw())?;
+            .acquire_encoder(self.raw(), queue.raw())
+            .map_err(|e| self.handle_hal_error(e))?;
 
         let command_buffer = command::CommandBuffer::new(encoder, self, label);
 
@@ -1856,11 +1856,9 @@ impl Device {
             flags: bgl_flags,
             entries: &hal_bindings,
         };
-        let raw = unsafe {
-            self.raw()
-                .create_bind_group_layout(&hal_desc)
-                .map_err(DeviceError::from)?
-        };
+
+        let raw = unsafe { self.raw().create_bind_group_layout(&hal_desc) }
+            .map_err(|e| self.handle_hal_error(e))?;
 
         let mut count_validator = binding_model::BindingTypeMaxCountValidator::default();
         for entry in entry_map.values() {
@@ -2290,11 +2288,8 @@ impl Device {
             textures: &hal_textures,
             acceleration_structures: &[],
         };
-        let raw = unsafe {
-            self.raw()
-                .create_bind_group(&hal_desc)
-                .map_err(DeviceError::from)?
-        };
+        let raw = unsafe { self.raw().create_bind_group(&hal_desc) }
+            .map_err(|e| self.handle_hal_error(e))?;
 
         // collect in the order of BGL iteration
         let late_buffer_binding_sizes = layout
@@ -2588,11 +2583,8 @@ impl Device {
             push_constant_ranges: desc.push_constant_ranges.as_ref(),
         };
 
-        let raw = unsafe {
-            self.raw()
-                .create_pipeline_layout(&hal_desc)
-                .map_err(DeviceError::from)?
-        };
+        let raw = unsafe { self.raw().create_pipeline_layout(&hal_desc) }
+            .map_err(|e| self.handle_hal_error(e))?;
 
         drop(raw_bind_group_layouts);
 
@@ -2746,7 +2738,7 @@ impl Device {
             unsafe { self.raw().create_compute_pipeline(&pipeline_desc) }.map_err(
                 |err| match err {
                     hal::PipelineError::Device(error) => {
-                        pipeline::CreateComputePipelineError::Device(error.into())
+                        pipeline::CreateComputePipelineError::Device(self.handle_hal_error(error))
                     }
                     hal::PipelineError::Linkage(_stages, msg) => {
                         pipeline::CreateComputePipelineError::Internal(msg)
@@ -3326,7 +3318,7 @@ impl Device {
             unsafe { self.raw().create_render_pipeline(&pipeline_desc) }.map_err(
                 |err| match err {
                     hal::PipelineError::Device(error) => {
-                        pipeline::CreateRenderPipelineError::Device(error.into())
+                        pipeline::CreateRenderPipelineError::Device(self.handle_hal_error(error))
                     }
                     hal::PipelineError::Linkage(stage, msg) => {
                         pipeline::CreateRenderPipelineError::Internal { stage, error: msg }
@@ -3449,7 +3441,9 @@ impl Device {
         };
         let raw = match unsafe { self.raw().create_pipeline_cache(&cache_desc) } {
             Ok(raw) => raw,
-            Err(e) => return Err(e.into()),
+            Err(e) => match e {
+                hal::PipelineCacheError::Device(e) => return Err(self.handle_hal_error(e).into()),
+            },
         };
         let cache = pipeline::PipelineCache {
             device: self.clone(),
@@ -3506,9 +3500,11 @@ impl Device {
         submission_index: crate::SubmissionIndex,
     ) -> Result<(), DeviceError> {
         let fence = self.fence.read();
-        let last_done_index = unsafe { self.raw().get_fence_value(fence.as_ref())? };
+        let last_done_index = unsafe { self.raw().get_fence_value(fence.as_ref()) }
+            .map_err(|e| self.handle_hal_error(e))?;
         if last_done_index < submission_index {
-            unsafe { self.raw().wait(fence.as_ref(), submission_index, !0)? };
+            unsafe { self.raw().wait(fence.as_ref(), submission_index, !0) }
+                .map_err(|e| self.handle_hal_error(e))?;
             drop(fence);
             let closures = self
                 .lock_life()
@@ -3567,7 +3563,7 @@ impl Device {
         Ok(query_set)
     }
 
-    pub(crate) fn lose(&self, message: &str) {
+    fn lose(&self, message: &str) {
         // Follow the steps at https://gpuweb.github.io/gpuweb/#lose-the-device.
 
         // Mark the device explicitly as invalid. This is checked in various
diff --git a/wgpu-core/src/instance.rs b/wgpu-core/src/instance.rs
index b3ce11fd1..ca19d3c53 100644
--- a/wgpu-core/src/instance.rs
+++ b/wgpu-core/src/instance.rs
@@ -4,7 +4,7 @@ use std::{borrow::Cow, collections::HashMap};
 use crate::hub::Hub;
 use crate::{
     api_log,
-    device::{queue::Queue, resource::Device, DeviceDescriptor},
+    device::{queue::Queue, resource::Device, DeviceDescriptor, DeviceError},
     global::Global,
     hal_api::HalApi,
     id::{markers, AdapterId, DeviceId, Id, Marker, QueueId, SurfaceId},
@@ -272,20 +272,19 @@ impl Adapter {
     ) -> Result<(Arc<Device>, Arc<Queue>), RequestDeviceError> {
         api_log!("Adapter::create_device");
 
-        if let Ok(device) = Device::new(
+        let device = Device::new(
             hal_device.device,
             hal_device.queue.as_ref(),
             self,
             desc,
             trace_path,
             instance_flags,
-        ) {
-            let device = Arc::new(device);
-            let queue = Arc::new(Queue::new(device.clone(), hal_device.queue));
-            device.set_queue(&queue);
-            return Ok((device, queue));
-        }
-        Err(RequestDeviceError::OutOfMemory)
+        )?;
+
+        let device = Arc::new(device);
+        let queue = Arc::new(Queue::new(device.clone(), hal_device.queue));
+        device.set_queue(&queue);
+        Ok((device, queue))
     }
 
     #[allow(clippy::type_complexity)]
@@ -338,12 +337,7 @@ impl Adapter {
                 &desc.memory_hints,
             )
         }
-        .map_err(|err| match err {
-            hal::DeviceError::Lost => RequestDeviceError::DeviceLost,
-            hal::DeviceError::OutOfMemory => RequestDeviceError::OutOfMemory,
-            hal::DeviceError::ResourceCreationFailed => RequestDeviceError::Internal,
-            hal::DeviceError::Unexpected => RequestDeviceError::DeviceLost,
-        })?;
+        .map_err(DeviceError::from_hal)?;
 
         self.create_device_and_queue_from_hal(open, desc, instance_flags, trace_path)
     }
@@ -377,18 +371,14 @@ pub enum GetSurfaceSupportError {
 /// Error when requesting a device from the adaptor
 #[non_exhaustive]
 pub enum RequestDeviceError {
+    #[error(transparent)]
+    Device(#[from] DeviceError),
     #[error("Parent adapter is invalid")]
     InvalidAdapter,
-    #[error("Connection to device was lost during initialization")]
-    DeviceLost,
-    #[error("Device initialization failed due to implementation specific errors")]
-    Internal,
     #[error(transparent)]
     LimitsExceeded(#[from] FailedLimit),
     #[error("Device has no queue supporting graphics")]
     NoGraphicsQueue,
-    #[error("Not enough memory left to request device")]
-    OutOfMemory,
     #[error("Unsupported features were requested: {0:?}")]
     UnsupportedFeature(wgt::Features),
 }
diff --git a/wgpu-core/src/pipeline.rs b/wgpu-core/src/pipeline.rs
index db1c1ba76..e5eb9e555 100644
--- a/wgpu-core/src/pipeline.rs
+++ b/wgpu-core/src/pipeline.rs
@@ -286,16 +286,6 @@ pub enum CreatePipelineCacheError {
     Internal(String),
 }
 
-impl From<hal::PipelineCacheError> for CreatePipelineCacheError {
-    fn from(value: hal::PipelineCacheError) -> Self {
-        match value {
-            hal::PipelineCacheError::Device(device) => {
-                CreatePipelineCacheError::Device(device.into())
-            }
-        }
-    }
-}
-
 #[derive(Debug)]
 pub struct PipelineCache {
     pub(crate) raw: ManuallyDrop<Box<dyn hal::DynPipelineCache>>,
diff --git a/wgpu-core/src/present.rs b/wgpu-core/src/present.rs
index 697156b35..b4fe0bbfb 100644
--- a/wgpu-core/src/present.rs
+++ b/wgpu-core/src/present.rs
@@ -191,7 +191,7 @@ impl Global {
                         .raw()
                         .create_texture_view(ast.texture.as_ref().borrow(), &clear_view_desc)
                 }
-                .map_err(DeviceError::from)?;
+                .map_err(|e| device.handle_hal_error(e))?;
 
                 let mut presentation = surface.presentation.lock();
                 let present = presentation.as_mut().unwrap();
@@ -238,7 +238,7 @@ impl Global {
                 match err {
                     hal::SurfaceError::Lost => Status::Lost,
                     hal::SurfaceError::Device(err) => {
-                        return Err(DeviceError::from(err).into());
+                        return Err(device.handle_hal_error(err).into());
                     }
                     hal::SurfaceError::Outdated => Status::Outdated,
                     hal::SurfaceError::Other(msg) => {
@@ -315,7 +315,9 @@ impl Global {
             Ok(()) => Ok(Status::Good),
             Err(err) => match err {
                 hal::SurfaceError::Lost => Ok(Status::Lost),
-                hal::SurfaceError::Device(err) => Err(SurfaceError::from(DeviceError::from(err))),
+                hal::SurfaceError::Device(err) => {
+                    Err(SurfaceError::from(device.handle_hal_error(err)))
+                }
                 hal::SurfaceError::Outdated => Ok(Status::Outdated),
                 hal::SurfaceError::Other(msg) => {
                     log::error!("acquire error: {}", msg);
diff --git a/wgpu-core/src/resource.rs b/wgpu-core/src/resource.rs
index 4c69266ae..dc0792cb3 100644
--- a/wgpu-core/src/resource.rs
+++ b/wgpu-core/src/resource.rs
@@ -832,8 +832,10 @@ impl StagingBuffer {
             memory_flags: hal::MemoryFlags::TRANSIENT,
         };
 
-        let raw = unsafe { device.raw().create_buffer(&stage_desc)? };
-        let mapping = unsafe { device.raw().map_buffer(raw.as_ref(), 0..size.get()) }?;
+        let raw = unsafe { device.raw().create_buffer(&stage_desc) }
+            .map_err(|e| device.handle_hal_error(e))?;
+        let mapping = unsafe { device.raw().map_buffer(raw.as_ref(), 0..size.get()) }
+            .map_err(|e| device.handle_hal_error(e))?;
 
         let staging_buffer = StagingBuffer {
             raw,
@@ -1358,7 +1360,7 @@ impl Global {
             let cmd_buf_data = cmd_buf_data.as_mut().unwrap();
             let cmd_buf_raw = cmd_buf_data
                 .encoder
-                .open()
+                .open(&cmd_buf.device)
                 .ok()
                 .and_then(|encoder| encoder.as_any_mut().downcast_mut());
             hal_command_encoder_callback(cmd_buf_raw)
diff --git a/wgpu-hal/src/dx12/adapter.rs b/wgpu-hal/src/dx12/adapter.rs
index 7d604654b..45d69f558 100644
--- a/wgpu-hal/src/dx12/adapter.rs
+++ b/wgpu-hal/src/dx12/adapter.rs
@@ -66,7 +66,7 @@ impl super::Adapter {
             profiling::scope!("ID3D12Device::create_device");
             library
                 .create_device(&adapter, Direct3D::D3D_FEATURE_LEVEL_11_0)
-                .ok()?
+                .ok()??
         };
 
         profiling::scope!("feature queries");
diff --git a/wgpu-hal/src/dx12/mod.rs b/wgpu-hal/src/dx12/mod.rs
index b871e1011..defeab1ad 100644
--- a/wgpu-hal/src/dx12/mod.rs
+++ b/wgpu-hal/src/dx12/mod.rs
@@ -108,7 +108,7 @@ impl D3D12Lib {
         &self,
         adapter: &DxgiAdapter,
         feature_level: Direct3D::D3D_FEATURE_LEVEL,
-    ) -> Result<Direct3D12::ID3D12Device, crate::DeviceError> {
+    ) -> Result<Option<Direct3D12::ID3D12Device>, crate::DeviceError> {
         // Calls windows::Win32::Graphics::Direct3D12::D3D12CreateDevice on d3d12.dll
         type Fun = extern "system" fn(
             padapter: *mut core::ffi::c_void,
@@ -118,19 +118,28 @@ impl D3D12Lib {
         ) -> windows_core::HRESULT;
         let func: libloading::Symbol<Fun> = unsafe { self.lib.get(b"D3D12CreateDevice\0") }?;
 
-        let mut result__ = None;
+        let mut result__: Option<Direct3D12::ID3D12Device> = None;
 
-        (func)(
+        let res = (func)(
             adapter.as_raw(),
             feature_level,
             // TODO: Generic?
             &Direct3D12::ID3D12Device::IID,
             <*mut _>::cast(&mut result__),
         )
-        .ok()
-        .into_device_result("Device creation")?;
+        .ok();
 
-        result__.ok_or(crate::DeviceError::Unexpected)
+        if let Err(ref err) = res {
+            match err.code() {
+                Dxgi::DXGI_ERROR_UNSUPPORTED => return Ok(None),
+                Dxgi::DXGI_ERROR_DRIVER_INTERNAL_ERROR => return Err(crate::DeviceError::Lost),
+                _ => {}
+            }
+        }
+
+        res.into_device_result("Device creation")?;
+
+        result__.ok_or(crate::DeviceError::Unexpected).map(Some)
     }
 
     fn serialize_root_signature(
diff --git a/wgpu-hal/src/dynamic/command.rs b/wgpu-hal/src/dynamic/command.rs
index 6c0f1cb02..4ecdf7472 100644
--- a/wgpu-hal/src/dynamic/command.rs
+++ b/wgpu-hal/src/dynamic/command.rs
@@ -61,7 +61,7 @@ pub trait DynCommandEncoder: DynResource + std::fmt::Debug {
         &mut self,
         layout: &dyn DynPipelineLayout,
         index: u32,
-        group: &dyn DynBindGroup,
+        group: Option<&dyn DynBindGroup>,
         dynamic_offsets: &[wgt::DynamicOffset],
     );
 
@@ -282,9 +282,15 @@ impl<C: CommandEncoder + DynResource> DynCommandEncoder for C {
         &mut self,
         layout: &dyn DynPipelineLayout,
         index: u32,
-        group: &dyn DynBindGroup,
+        group: Option<&dyn DynBindGroup>,
         dynamic_offsets: &[wgt::DynamicOffset],
     ) {
+        if group.is_none() {
+            // TODO: Handle group None correctly.
+            return;
+        }
+        let group = group.unwrap();
+
         let layout = layout.expect_downcast_ref();
         let group = group.expect_downcast_ref();
         unsafe { C::set_bind_group(self, layout, index, group, dynamic_offsets) };
diff --git a/wgpu-hal/src/metal/mod.rs b/wgpu-hal/src/metal/mod.rs
index 62d409a8f..1935e843e 100644
--- a/wgpu-hal/src/metal/mod.rs
+++ b/wgpu-hal/src/metal/mod.rs
@@ -96,9 +96,7 @@ crate::impl_dyn_resource!(
     TextureView
 );
 
-pub struct Instance {
-    managed_metal_layer_delegate: surface::HalManagedMetalLayerDelegate,
-}
+pub struct Instance {}
 
 impl Instance {
     pub fn create_surface_from_layer(&self, layer: &metal::MetalLayerRef) -> Surface {
@@ -113,9 +111,7 @@ impl crate::Instance for Instance {
         profiling::scope!("Init Metal Backend");
         // We do not enable metal validation based on the validation flags as it affects the entire
         // process. Instead, we enable the validation inside the test harness itself in tests/src/native.rs.
-        Ok(Instance {
-            managed_metal_layer_delegate: surface::HalManagedMetalLayerDelegate::new(),
-        })
+        Ok(Instance {})
     }
 
     unsafe fn create_surface(
@@ -126,16 +122,12 @@ impl crate::Instance for Instance {
         match window_handle {
             #[cfg(target_os = "ios")]
             raw_window_handle::RawWindowHandle::UiKit(handle) => {
-                let _ = &self.managed_metal_layer_delegate;
-                Ok(unsafe { Surface::from_view(handle.ui_view.as_ptr(), None) })
+                Ok(unsafe { Surface::from_view(handle.ui_view.cast()) })
             }
             #[cfg(target_os = "macos")]
-            raw_window_handle::RawWindowHandle::AppKit(handle) => Ok(unsafe {
-                Surface::from_view(
-                    handle.ns_view.as_ptr(),
-                    Some(&self.managed_metal_layer_delegate),
-                )
-            }),
+            raw_window_handle::RawWindowHandle::AppKit(handle) => {
+                Ok(unsafe { Surface::from_view(handle.ns_view.cast()) })
+            }
             _ => Err(crate::InstanceError::new(format!(
                 "window handle {window_handle:?} is not a Metal-compatible handle"
             ))),
@@ -367,7 +359,6 @@ pub struct Device {
 }
 
 pub struct Surface {
-    view: Option<NonNull<objc::runtime::Object>>,
     render_layer: Mutex<metal::MetalLayer>,
     swapchain_format: RwLock<Option<wgt::TextureFormat>>,
     extent: RwLock<wgt::Extent3d>,
diff --git a/wgpu-hal/src/metal/surface.rs b/wgpu-hal/src/metal/surface.rs
index 115e4208a..668b60247 100644
--- a/wgpu-hal/src/metal/surface.rs
+++ b/wgpu-hal/src/metal/surface.rs
@@ -1,26 +1,30 @@
 #![allow(clippy::let_unit_value)] // `let () =` being used to constrain result type
 
-use std::{os::raw::c_void, ptr::NonNull, sync::Once, thread};
+use std::ffi::c_uint;
+use std::mem::ManuallyDrop;
+use std::ptr::NonNull;
+use std::sync::Once;
+use std::thread;
 
 use core_graphics_types::{
     base::CGFloat,
     geometry::{CGRect, CGSize},
 };
+use metal::foreign_types::ForeignType;
 use objc::{
     class,
     declare::ClassDecl,
     msg_send,
-    rc::autoreleasepool,
+    rc::{autoreleasepool, StrongPtr},
     runtime::{Class, Object, Sel, BOOL, NO, YES},
     sel, sel_impl,
 };
 use parking_lot::{Mutex, RwLock};
 
-#[cfg(target_os = "macos")]
 #[link(name = "QuartzCore", kind = "framework")]
 extern "C" {
     #[allow(non_upper_case_globals)]
-    static kCAGravityTopLeft: *mut Object;
+    static kCAGravityResize: *mut Object;
 }
 
 extern "C" fn layer_should_inherit_contents_scale_from_window(
@@ -46,6 +50,7 @@ impl HalManagedMetalLayerDelegate {
             type Fun = extern "C" fn(&Class, Sel, *mut Object, CGFloat, *mut Object) -> BOOL;
             let mut decl = ClassDecl::new(&class_name, class!(NSObject)).unwrap();
             unsafe {
+                // <https://developer.apple.com/documentation/appkit/nsviewlayercontentscaledelegate/3005294-layer?language=objc>
                 decl.add_class_method::<Fun>(
                     sel!(layer:shouldInheritContentsScale:fromWindow:),
                     layer_should_inherit_contents_scale_from_window,
@@ -58,9 +63,8 @@ impl HalManagedMetalLayerDelegate {
 }
 
 impl super::Surface {
-    fn new(view: Option<NonNull<Object>>, layer: metal::MetalLayer) -> Self {
+    fn new(layer: metal::MetalLayer) -> Self {
         Self {
-            view,
             render_layer: Mutex::new(layer),
             swapchain_format: RwLock::new(None),
             extent: RwLock::new(wgt::Extent3d::default()),
@@ -71,86 +75,183 @@ impl super::Surface {
 
     /// If not called on the main thread, this will panic.
     #[allow(clippy::transmute_ptr_to_ref)]
-    pub unsafe fn from_view(
-        view: *mut c_void,
-        delegate: Option<&HalManagedMetalLayerDelegate>,
-    ) -> Self {
-        let view = view.cast::<Object>();
-        let render_layer = {
-            let layer = unsafe { Self::get_metal_layer(view, delegate) };
-            let layer = layer.cast::<metal::MetalLayerRef>();
-            // SAFETY: This pointer…
-            //
-            // - …is properly aligned.
-            // - …is dereferenceable to a `MetalLayerRef` as an invariant of the `metal`
-            //   field.
-            // - …points to an _initialized_ `MetalLayerRef`.
-            // - …is only ever aliased via an immutable reference that lives within this
-            //   lexical scope.
-            unsafe { &*layer }
-        }
-        .to_owned();
-        let _: *mut c_void = msg_send![view, retain];
-        Self::new(NonNull::new(view), render_layer)
+    pub unsafe fn from_view(view: NonNull<Object>) -> Self {
+        let layer = unsafe { Self::get_metal_layer(view) };
+        let layer = ManuallyDrop::new(layer);
+        // SAFETY: The layer is an initialized instance of `CAMetalLayer`, and
+        // we transfer the retain count to `MetalLayer` using `ManuallyDrop`.
+        let layer = unsafe { metal::MetalLayer::from_ptr(layer.cast()) };
+        Self::new(layer)
     }
 
     pub unsafe fn from_layer(layer: &metal::MetalLayerRef) -> Self {
         let class = class!(CAMetalLayer);
         let proper_kind: BOOL = msg_send![layer, isKindOfClass: class];
         assert_eq!(proper_kind, YES);
-        Self::new(None, layer.to_owned())
+        Self::new(layer.to_owned())
     }
 
-    /// If not called on the main thread, this will panic.
-    pub(crate) unsafe fn get_metal_layer(
-        view: *mut Object,
-        delegate: Option<&HalManagedMetalLayerDelegate>,
-    ) -> *mut Object {
-        if view.is_null() {
-            panic!("window does not have a valid contentView");
-        }
-
+    /// Get or create a new `CAMetalLayer` associated with the given `NSView`
+    /// or `UIView`.
+    ///
+    /// # Panics
+    ///
+    /// If called from a thread that is not the main thread, this will panic.
+    ///
+    /// # Safety
+    ///
+    /// The `view` must be a valid instance of `NSView` or `UIView`.
+    pub(crate) unsafe fn get_metal_layer(view: NonNull<Object>) -> StrongPtr {
         let is_main_thread: BOOL = msg_send![class!(NSThread), isMainThread];
         if is_main_thread == NO {
             panic!("get_metal_layer cannot be called in non-ui thread.");
         }
 
-        let main_layer: *mut Object = msg_send![view, layer];
-        let class = class!(CAMetalLayer);
-        let is_valid_layer: BOOL = msg_send![main_layer, isKindOfClass: class];
+        // Ensure that the view is layer-backed.
+        // Views are always layer-backed in UIKit.
+        #[cfg(target_os = "macos")]
+        let () = msg_send![view.as_ptr(), setWantsLayer: YES];
 
-        if is_valid_layer == YES {
-            main_layer
+        let root_layer: *mut Object = msg_send![view.as_ptr(), layer];
+        // `-[NSView layer]` can return `NULL`, while `-[UIView layer]` should
+        // always be available.
+        assert!(!root_layer.is_null(), "failed making the view layer-backed");
+
+        // NOTE: We explicitly do not touch properties such as
+        // `layerContentsPlacement`, `needsDisplayOnBoundsChange` and
+        // `contentsGravity` etc. on the root layer, both since we would like
+        // to give the user full control over them, and because the default
+        // values suit us pretty well (especially the contents placement being
+        // `NSViewLayerContentsRedrawDuringViewResize`, which allows the view
+        // to receive `drawRect:`/`updateLayer` calls).
+
+        let is_metal_layer: BOOL = msg_send![root_layer, isKindOfClass: class!(CAMetalLayer)];
+        if is_metal_layer == YES {
+            // The view has a `CAMetalLayer` as the root layer, which can
+            // happen for example if user overwrote `-[NSView layerClass]` or
+            // the view is `MTKView`.
+            //
+            // This is easily handled: We take "ownership" over the layer, and
+            // render directly into that; after all, the user passed a view
+            // with an explicit Metal layer to us, so this is very likely what
+            // they expect us to do.
+            unsafe { StrongPtr::retain(root_layer) }
         } else {
-            // If the main layer is not a CAMetalLayer, we create a CAMetalLayer and use it.
-            let new_layer: *mut Object = msg_send![class, new];
-            let frame: CGRect = msg_send![main_layer, bounds];
+            // The view does not have a `CAMetalLayer` as the root layer (this
+            // is the default for most views).
+            //
+            // This case is trickier! We cannot use the existing layer with
+            // Metal, so we must do something else. There are a few options:
+            //
+            // 1. Panic here, and require the user to pass a view with a
+            //    `CAMetalLayer` layer.
+            //
+            //    While this would "work", it doesn't solve the problem, and
+            //    instead passes the ball onwards to the user and ecosystem to
+            //    figure it out.
+            //
+            // 2. Override the existing layer with a newly created layer.
+            //
+            //    If we overlook that this does not work in UIKit since
+            //    `UIView`'s `layer` is `readonly`, and that as such we will
+            //    need to do something different there anyhow, this is
+            //    actually a fairly good solution, and was what the original
+            //    implementation did.
+            //
+            //    It has some problems though, due to:
+            //
+            //    a. `wgpu` in our API design choosing not to register a
+            //       callback with `-[CALayerDelegate displayLayer:]`, but
+            //       instead leaves it up to the user to figure out when to
+            //       redraw. That is, we rely on other libraries' callbacks
+            //       telling us when to render.
+            //
+            //       (If this were an API only for Metal, we would probably
+            //       make the user provide a `render` closure that we'd call
+            //       in the right situations. But alas, we have to be
+            //       cross-platform here).
+            //
+            //    b. Overwriting the `layer` on `NSView` makes the view
+            //       "layer-hosting", see [wantsLayer], which disables drawing
+            //       functionality on the view like `drawRect:`/`updateLayer`.
+            //
+            //    These two in combination makes it basically impossible for
+            //    crates like Winit to provide a robust rendering callback
+            //    that integrates with the system's built-in mechanisms for
+            //    redrawing, exactly because overwriting the layer would be
+            //    implicitly disabling those mechanisms!
+            //
+            //    [wantsLayer]: https://developer.apple.com/documentation/appkit/nsview/1483695-wantslayer?language=objc
+            //
+            // 3. Create a sublayer.
+            //
+            //    `CALayer` has the concept of "sublayers", which we can use
+            //    instead of overriding the layer.
+            //
+            //    This is also the recommended solution on UIKit, so it's nice
+            //    that we can use (almost) the same implementation for these.
+            //
+            //    It _might_, however, perform ever so slightly worse than
+            //    overriding the layer directly.
+            //
+            // 4. Create a new `MTKView` (or a custom view), and add it as a
+            //    subview.
+            //
+            //    Similar to creating a sublayer (see above), but also
+            //    provides a bunch of event handling that we don't need.
+            //
+            // Option 3 seems like the most robust solution, so this is what
+            // we're going to do.
+
+            // Create a new sublayer.
+            let new_layer: *mut Object = msg_send![class!(CAMetalLayer), new];
+            let () = msg_send![root_layer, addSublayer: new_layer];
+
+            // Automatically resize the sublayer's frame to match the
+            // superlayer's bounds.
+            //
+            // Note that there is a somewhat hidden design decision in this:
+            // We define the `width` and `height` in `configure` to control
+            // the `drawableSize` of the layer, while `bounds` and `frame` are
+            // outside of the user's direct control - instead, though, they
+            // can control the size of the view (or root layer), and get the
+            // desired effect that way.
+            //
+            // We _could_ also let `configure` set the `bounds` size, however
+            // that would be inconsistent with using the root layer directly
+            // (as we may do, see above).
+            let width_sizable = 1 << 1; // kCALayerWidthSizable
+            let height_sizable = 1 << 4; // kCALayerHeightSizable
+            let mask: c_uint = width_sizable | height_sizable;
+            let () = msg_send![new_layer, setAutoresizingMask: mask];
+
+            // Specify the relative size that the auto resizing mask above
+            // will keep (i.e. tell it to fill out its superlayer).
+            let frame: CGRect = msg_send![root_layer, bounds];
             let () = msg_send![new_layer, setFrame: frame];
-            #[cfg(target_os = "ios")]
-            {
-                // Unlike NSView, UIView does not allow to replace main layer.
-                let () = msg_send![main_layer, addSublayer: new_layer];
-                // On iOS, "from_view" may be called before the application initialization is complete,
-                // `msg_send![view, window]` and `msg_send![window, screen]` will get null.
-                let screen: *mut Object = msg_send![class!(UIScreen), mainScreen];
-                let scale_factor: CGFloat = msg_send![screen, nativeScale];
-                let () = msg_send![view, setContentScaleFactor: scale_factor];
-            };
-            #[cfg(target_os = "macos")]
-            {
-                let () = msg_send![view, setLayer: new_layer];
-                let () = msg_send![view, setWantsLayer: YES];
-                let () = msg_send![new_layer, setContentsGravity: unsafe { kCAGravityTopLeft }];
-                let window: *mut Object = msg_send![view, window];
-                if !window.is_null() {
-                    let scale_factor: CGFloat = msg_send![window, backingScaleFactor];
-                    let () = msg_send![new_layer, setContentsScale: scale_factor];
-                }
-            };
-            if let Some(delegate) = delegate {
-                let () = msg_send![new_layer, setDelegate: delegate.0];
-            }
-            new_layer
+
+            // The gravity to use when the layer's `drawableSize` isn't the
+            // same as the bounds rectangle.
+            //
+            // The desired content gravity is `kCAGravityResize`, because it
+            // masks / alleviates issues with resizing when
+            // `present_with_transaction` is disabled, and behaves better when
+            // moving the window between monitors.
+            //
+            // Unfortunately, it also makes it harder to see changes to
+            // `width` and `height` in `configure`. When debugging resize
+            // issues, swap this for `kCAGravityTopLeft` instead.
+            let _: () = msg_send![new_layer, setContentsGravity: unsafe { kCAGravityResize }];
+
+            // Set initial scale factor of the layer. This is kept in sync by
+            // `configure` (on UIKit), and the delegate below (on AppKit).
+            let scale_factor: CGFloat = msg_send![root_layer, contentsScale];
+            let () = msg_send![new_layer, setContentsScale: scale_factor];
+
+            let delegate = HalManagedMetalLayerDelegate::new();
+            let () = msg_send![new_layer, setDelegate: delegate.0];
+
+            unsafe { StrongPtr::new(new_layer) }
         }
     }
 
@@ -171,16 +272,6 @@ impl super::Surface {
     }
 }
 
-impl Drop for super::Surface {
-    fn drop(&mut self) {
-        if let Some(view) = self.view {
-            unsafe {
-                let () = msg_send![view.as_ptr(), release];
-            }
-        }
-    }
-}
-
 impl crate::Surface for super::Surface {
     type A = super::Api;
 
@@ -210,19 +301,30 @@ impl crate::Surface for super::Surface {
             _ => (),
         }
 
-        let device_raw = device.shared.device.lock();
-        // On iOS, unless the user supplies a view with a CAMetalLayer, we
-        // create one as a sublayer. However, when the view changes size,
-        // its sublayers are not automatically resized, and we must resize
-        // it here. The drawable size and the layer size don't correlate
-        #[cfg(target_os = "ios")]
+        // AppKit / UIKit automatically sets the correct scale factor for
+        // layers attached to a view. Our layer, however, may not be directly
+        // attached to a view; in those cases, we need to set the scale
+        // factor ourselves.
+        //
+        // For AppKit, we do so by adding a delegate on the layer with the
+        // `layer:shouldInheritContentsScale:fromWindow:` method returning
+        // `true` - this tells the system to automatically update the scale
+        // factor when it changes.
+        //
+        // For UIKit, we manually update the scale factor from the super layer
+        // here, if there is one.
+        //
+        // TODO: Is there a way that we could listen to such changes instead?
+        #[cfg(not(target_os = "macos"))]
         {
-            if let Some(view) = self.view {
-                let main_layer: *mut Object = msg_send![view.as_ptr(), layer];
-                let bounds: CGRect = msg_send![main_layer, bounds];
-                let () = msg_send![*render_layer, setFrame: bounds];
+            let superlayer: *mut Object = msg_send![render_layer.as_ptr(), superlayer];
+            if !superlayer.is_null() {
+                let scale_factor: CGFloat = msg_send![superlayer, contentsScale];
+                let () = msg_send![render_layer.as_ptr(), setContentsScale: scale_factor];
             }
         }
+
+        let device_raw = device.shared.device.lock();
         render_layer.set_device(&device_raw);
         render_layer.set_pixel_format(caps.map_format(config.format));
         render_layer.set_framebuffer_only(framebuffer_only);
diff --git a/wgpu-hal/src/vulkan/instance.rs b/wgpu-hal/src/vulkan/instance.rs
index f44c4fa30..5673859e4 100644
--- a/wgpu-hal/src/vulkan/instance.rs
+++ b/wgpu-hal/src/vulkan/instance.rs
@@ -514,7 +514,7 @@ impl super::Instance {
     #[cfg(metal)]
     fn create_surface_from_view(
         &self,
-        view: *mut c_void,
+        view: std::ptr::NonNull<c_void>,
     ) -> Result<super::Surface, crate::InstanceError> {
         if !self.shared.extensions.contains(&ext::metal_surface::NAME) {
             return Err(crate::InstanceError::new(String::from(
@@ -522,16 +522,17 @@ impl super::Instance {
             )));
         }
 
-        let layer = unsafe {
-            crate::metal::Surface::get_metal_layer(view.cast::<objc::runtime::Object>(), None)
-        };
+        let layer = unsafe { crate::metal::Surface::get_metal_layer(view.cast()) };
+        // NOTE: The layer is retained by Vulkan's `vkCreateMetalSurfaceEXT`,
+        // so no need to retain it beyond the scope of this function.
+        let layer_ptr = (*layer).cast();
 
         let surface = {
             let metal_loader =
                 ext::metal_surface::Instance::new(&self.shared.entry, &self.shared.raw);
             let vk_info = vk::MetalSurfaceCreateInfoEXT::default()
                 .flags(vk::MetalSurfaceCreateFlagsEXT::empty())
-                .layer(layer.cast());
+                .layer(layer_ptr);
 
             unsafe { metal_loader.create_metal_surface(&vk_info, None).unwrap() }
         };
@@ -873,13 +874,13 @@ impl crate::Instance for super::Instance {
             (Rwh::AppKit(handle), _)
                 if self.shared.extensions.contains(&ext::metal_surface::NAME) =>
             {
-                self.create_surface_from_view(handle.ns_view.as_ptr())
+                self.create_surface_from_view(handle.ns_view)
             }
             #[cfg(all(target_os = "ios", feature = "metal"))]
             (Rwh::UiKit(handle), _)
                 if self.shared.extensions.contains(&ext::metal_surface::NAME) =>
             {
-                self.create_surface_from_view(handle.ui_view.as_ptr())
+                self.create_surface_from_view(handle.ui_view)
             }
             (_, _) => Err(crate::InstanceError::new(format!(
                 "window handle {window_handle:?} is not a Vulkan-compatible handle"
diff --git a/wgpu-types/src/lib.rs b/wgpu-types/src/lib.rs
index e44f91665..cc0318eda 100644
--- a/wgpu-types/src/lib.rs
+++ b/wgpu-types/src/lib.rs
@@ -5547,8 +5547,18 @@ pub struct SurfaceConfiguration<V> {
     /// `Bgra8Unorm` and `Bgra8UnormSrgb`
     pub format: TextureFormat,
     /// Width of the swap chain. Must be the same size as the surface, and nonzero.
+    ///
+    /// If this is not the same size as the underlying surface (e.g. if it is
+    /// set once, and the window is later resized), the behaviour is defined
+    /// but platform-specific, and may change in the future (currently macOS
+    /// scales the surface, other platforms may do something else).
     pub width: u32,
     /// Height of the swap chain. Must be the same size as the surface, and nonzero.
+    ///
+    /// If this is not the same size as the underlying surface (e.g. if it is
+    /// set once, and the window is later resized), the behaviour is defined
+    /// but platform-specific, and may change in the future (currently macOS
+    /// scales the surface, other platforms may do something else).
     pub height: u32,
     /// Presentation mode of the swap chain. Fifo is the only mode guaranteed to be supported.
     /// FifoRelaxed, Immediate, and Mailbox will crash if unsupported, while AutoVsync and
diff --git a/wgpu/src/api/compute_pass.rs b/wgpu/src/api/compute_pass.rs
index bd0575bbd..306a9f0b4 100644
--- a/wgpu/src/api/compute_pass.rs
+++ b/wgpu/src/api/compute_pass.rs
@@ -48,14 +48,15 @@ impl<'encoder> ComputePass<'encoder> {
     pub fn set_bind_group(
         &mut self,
         index: u32,
-        bind_group: &BindGroup,
+        bind_group: Option<&BindGroup>,
         offsets: &[DynamicOffset],
     ) {
+        let bg = bind_group.map(|x| x.data.as_ref());
         DynContext::compute_pass_set_bind_group(
             &*self.inner.context,
             self.inner.data.as_mut(),
             index,
-            bind_group.data.as_ref(),
+            bg,
             offsets,
         );
     }
diff --git a/wgpu/src/api/render_bundle_encoder.rs b/wgpu/src/api/render_bundle_encoder.rs
index 446b4cbf8..d69548cdf 100644
--- a/wgpu/src/api/render_bundle_encoder.rs
+++ b/wgpu/src/api/render_bundle_encoder.rs
@@ -66,14 +66,15 @@ impl<'a> RenderBundleEncoder<'a> {
     pub fn set_bind_group(
         &mut self,
         index: u32,
-        bind_group: &'a BindGroup,
+        bind_group: Option<&'a BindGroup>,
         offsets: &[DynamicOffset],
     ) {
+        let bg = bind_group.map(|x| x.data.as_ref());
         DynContext::render_bundle_encoder_set_bind_group(
             &*self.parent.context,
             self.data.as_mut(),
             index,
-            bind_group.data.as_ref(),
+            bg,
             offsets,
         )
     }
diff --git a/wgpu/src/api/render_pass.rs b/wgpu/src/api/render_pass.rs
index 8fddfd009..7cdbc3135 100644
--- a/wgpu/src/api/render_pass.rs
+++ b/wgpu/src/api/render_pass.rs
@@ -77,14 +77,15 @@ impl<'encoder> RenderPass<'encoder> {
     pub fn set_bind_group(
         &mut self,
         index: u32,
-        bind_group: &BindGroup,
+        bind_group: Option<&BindGroup>,
         offsets: &[DynamicOffset],
     ) {
+        let bg = bind_group.map(|x| x.data.as_ref());
         DynContext::render_pass_set_bind_group(
             &*self.inner.context,
             self.inner.data.as_mut(),
             index,
-            bind_group.data.as_ref(),
+            bg,
             offsets,
         )
     }
diff --git a/wgpu/src/backend/webgpu.rs b/wgpu/src/backend/webgpu.rs
index ef6bbebe2..30ad22f73 100644
--- a/wgpu/src/backend/webgpu.rs
+++ b/wgpu/src/backend/webgpu.rs
@@ -2757,9 +2757,14 @@ impl crate::context::Context for ContextWebGpu {
         &self,
         pass_data: &mut Self::ComputePassData,
         index: u32,
-        bind_group_data: &Self::BindGroupData,
+        bind_group_data: Option<&Self::BindGroupData>,
         offsets: &[wgt::DynamicOffset],
     ) {
+        if bind_group_data.is_none() {
+            // TODO: Handle the None case.
+            return;
+        }
+        let bind_group_data = bind_group_data.unwrap();
         if offsets.is_empty() {
             pass_data.0.set_bind_group(index, Some(&bind_group_data.0));
         } else {
@@ -2869,9 +2874,14 @@ impl crate::context::Context for ContextWebGpu {
         &self,
         encoder_data: &mut Self::RenderBundleEncoderData,
         index: u32,
-        bind_group_data: &Self::BindGroupData,
+        bind_group_data: Option<&Self::BindGroupData>,
         offsets: &[wgt::DynamicOffset],
     ) {
+        if bind_group_data.is_none() {
+            // TODO: Handle the None case.
+            return;
+        }
+        let bind_group_data = bind_group_data.unwrap();
         if offsets.is_empty() {
             encoder_data
                 .0
@@ -3021,9 +3031,14 @@ impl crate::context::Context for ContextWebGpu {
         &self,
         pass_data: &mut Self::RenderPassData,
         index: u32,
-        bind_group_data: &Self::BindGroupData,
+        bind_group_data: Option<&Self::BindGroupData>,
         offsets: &[wgt::DynamicOffset],
     ) {
+        if bind_group_data.is_none() {
+            // TODO: Handle the None case.
+            return;
+        }
+        let bind_group_data = bind_group_data.unwrap();
         if offsets.is_empty() {
             pass_data.0.set_bind_group(index, Some(&bind_group_data.0));
         } else {
diff --git a/wgpu/src/backend/wgpu_core.rs b/wgpu/src/backend/wgpu_core.rs
index d6f2da0fa..9b2597cb4 100644
--- a/wgpu/src/backend/wgpu_core.rs
+++ b/wgpu/src/backend/wgpu_core.rs
@@ -2168,15 +2168,14 @@ impl crate::Context for ContextWgpuCore {
         &self,
         pass_data: &mut Self::ComputePassData,
         index: u32,
-        bind_group_data: &Self::BindGroupData,
+        bind_group_data: Option<&Self::BindGroupData>,
         offsets: &[wgt::DynamicOffset],
     ) {
-        if let Err(cause) = self.0.compute_pass_set_bind_group(
-            &mut pass_data.pass,
-            index,
-            *bind_group_data,
-            offsets,
-        ) {
+        let bg = bind_group_data.cloned();
+        if let Err(cause) =
+            self.0
+                .compute_pass_set_bind_group(&mut pass_data.pass, index, bg, offsets)
+        {
             self.handle_error(
                 &pass_data.error_sink,
                 cause,
@@ -2364,14 +2363,15 @@ impl crate::Context for ContextWgpuCore {
         &self,
         encoder_data: &mut Self::RenderBundleEncoderData,
         index: u32,
-        bind_group_data: &Self::BindGroupData,
+        bind_group_data: Option<&Self::BindGroupData>,
         offsets: &[wgt::DynamicOffset],
     ) {
+        let bg = bind_group_data.cloned();
         unsafe {
             wgpu_render_bundle_set_bind_group(
                 encoder_data,
                 index,
-                *bind_group_data,
+                bg,
                 offsets.as_ptr(),
                 offsets.len(),
             )
@@ -2494,12 +2494,13 @@ impl crate::Context for ContextWgpuCore {
         &self,
         pass_data: &mut Self::RenderPassData,
         index: u32,
-        bind_group_data: &Self::BindGroupData,
+        bind_group_data: Option<&Self::BindGroupData>,
         offsets: &[wgt::DynamicOffset],
     ) {
+        let bg = bind_group_data.cloned();
         if let Err(cause) =
             self.0
-                .render_pass_set_bind_group(&mut pass_data.pass, index, *bind_group_data, offsets)
+                .render_pass_set_bind_group(&mut pass_data.pass, index, bg, offsets)
         {
             self.handle_error(
                 &pass_data.error_sink,
diff --git a/wgpu/src/context.rs b/wgpu/src/context.rs
index c090548ce..dd989ddd2 100644
--- a/wgpu/src/context.rs
+++ b/wgpu/src/context.rs
@@ -441,7 +441,7 @@ pub trait Context: Debug + WasmNotSendSync + Sized {
         &self,
         pass_data: &mut Self::ComputePassData,
         index: u32,
-        bind_group_data: &Self::BindGroupData,
+        bind_group_data: Option<&Self::BindGroupData>,
         offsets: &[DynamicOffset],
     );
     fn compute_pass_set_push_constants(
@@ -494,7 +494,7 @@ pub trait Context: Debug + WasmNotSendSync + Sized {
         &self,
         encoder_data: &mut Self::RenderBundleEncoderData,
         index: u32,
-        bind_group_data: &Self::BindGroupData,
+        bind_group_data: Option<&Self::BindGroupData>,
         offsets: &[DynamicOffset],
     );
     #[allow(clippy::too_many_arguments)]
@@ -557,7 +557,7 @@ pub trait Context: Debug + WasmNotSendSync + Sized {
         &self,
         pass_data: &mut Self::RenderPassData,
         index: u32,
-        bind_group_data: &Self::BindGroupData,
+        bind_group_data: Option<&Self::BindGroupData>,
         offsets: &[DynamicOffset],
     );
     #[allow(clippy::too_many_arguments)]
@@ -1112,7 +1112,7 @@ pub(crate) trait DynContext: Debug + WasmNotSendSync {
         &self,
         pass_data: &mut crate::Data,
         index: u32,
-        bind_group_data: &crate::Data,
+        bind_group_data: Option<&crate::Data>,
         offsets: &[DynamicOffset],
     );
     fn compute_pass_set_push_constants(
@@ -1155,7 +1155,7 @@ pub(crate) trait DynContext: Debug + WasmNotSendSync {
         &self,
         encoder_data: &mut crate::Data,
         index: u32,
-        bind_group_data: &crate::Data,
+        bind_group_data: Option<&crate::Data>,
         offsets: &[DynamicOffset],
     );
     #[allow(clippy::too_many_arguments)]
@@ -1214,7 +1214,7 @@ pub(crate) trait DynContext: Debug + WasmNotSendSync {
         &self,
         pass_data: &mut crate::Data,
         index: u32,
-        bind_group_data: &crate::Data,
+        bind_group_data: Option<&crate::Data>,
         offsets: &[DynamicOffset],
     );
     #[allow(clippy::too_many_arguments)]
@@ -2161,12 +2161,12 @@ where
         &self,
         pass_data: &mut crate::Data,
         index: u32,
-        bind_group_data: &crate::Data,
+        bind_group_data: Option<&crate::Data>,
         offsets: &[DynamicOffset],
     ) {
         let pass_data = downcast_mut::<T::ComputePassData>(pass_data);
-        let bind_group_data = downcast_ref(bind_group_data);
-        Context::compute_pass_set_bind_group(self, pass_data, index, bind_group_data, offsets)
+        let bg = bind_group_data.map(downcast_ref);
+        Context::compute_pass_set_bind_group(self, pass_data, index, bg, offsets)
     }
 
     fn compute_pass_set_push_constants(
@@ -2272,18 +2272,12 @@ where
         &self,
         encoder_data: &mut crate::Data,
         index: u32,
-        bind_group_data: &crate::Data,
+        bind_group_data: Option<&crate::Data>,
         offsets: &[DynamicOffset],
     ) {
         let encoder_data = downcast_mut::<T::RenderBundleEncoderData>(encoder_data);
-        let bind_group_data = downcast_ref(bind_group_data);
-        Context::render_bundle_encoder_set_bind_group(
-            self,
-            encoder_data,
-            index,
-            bind_group_data,
-            offsets,
-        )
+        let bg = bind_group_data.map(downcast_ref);
+        Context::render_bundle_encoder_set_bind_group(self, encoder_data, index, bg, offsets)
     }
 
     fn render_bundle_encoder_set_index_buffer(
@@ -2406,12 +2400,12 @@ where
         &self,
         pass_data: &mut crate::Data,
         index: u32,
-        bind_group_data: &crate::Data,
+        bind_group_data: Option<&crate::Data>,
         offsets: &[DynamicOffset],
     ) {
         let pass_data = downcast_mut::<T::RenderPassData>(pass_data);
-        let bind_group_data = downcast_ref(bind_group_data);
-        Context::render_pass_set_bind_group(self, pass_data, index, bind_group_data, offsets)
+        let bg = bind_group_data.map(downcast_ref);
+        Context::render_pass_set_bind_group(self, pass_data, index, bg, offsets)
     }
 
     fn render_pass_set_index_buffer(
diff --git a/wgpu/src/util/encoder.rs b/wgpu/src/util/encoder.rs
index bef8fe950..c6d42e3eb 100644
--- a/wgpu/src/util/encoder.rs
+++ b/wgpu/src/util/encoder.rs
@@ -10,7 +10,12 @@ pub trait RenderEncoder<'a> {
     /// in the active pipeline when any `draw()` function is called must match the layout of this bind group.
     ///
     /// If the bind group have dynamic offsets, provide them in order of their declaration.
-    fn set_bind_group(&mut self, index: u32, bind_group: &'a BindGroup, offsets: &[DynamicOffset]);
+    fn set_bind_group(
+        &mut self,
+        index: u32,
+        bind_group: Option<&'a BindGroup>,
+        offsets: &[DynamicOffset],
+    );
 
     /// Sets the active render pipeline.
     ///
@@ -101,7 +106,12 @@ pub trait RenderEncoder<'a> {
 
 impl<'a> RenderEncoder<'a> for RenderPass<'a> {
     #[inline(always)]
-    fn set_bind_group(&mut self, index: u32, bind_group: &'a BindGroup, offsets: &[DynamicOffset]) {
+    fn set_bind_group(
+        &mut self,
+        index: u32,
+        bind_group: Option<&'a BindGroup>,
+        offsets: &[DynamicOffset],
+    ) {
         Self::set_bind_group(self, index, bind_group, offsets);
     }
 
@@ -152,7 +162,12 @@ impl<'a> RenderEncoder<'a> for RenderPass<'a> {
 
 impl<'a> RenderEncoder<'a> for RenderBundleEncoder<'a> {
     #[inline(always)]
-    fn set_bind_group(&mut self, index: u32, bind_group: &'a BindGroup, offsets: &[DynamicOffset]) {
+    fn set_bind_group(
+        &mut self,
+        index: u32,
+        bind_group: Option<&'a BindGroup>,
+        offsets: &[DynamicOffset],
+    ) {
         Self::set_bind_group(self, index, bind_group, offsets);
     }
 


Test patch:
diff --git a/naga/tests/snapshots.rs b/naga/tests/snapshots.rs
index 69e5922b3..0e285e7b0 100644
--- a/naga/tests/snapshots.rs
+++ b/naga/tests/snapshots.rs
@@ -14,8 +14,12 @@ const BASE_DIR_OUT: &str = "tests/out";
 bitflags::bitflags! {
     #[derive(Clone, Copy)]
     struct Targets: u32 {
+        /// A serialization of the `naga::Module`, in RON format.
         const IR = 1;
+
+        /// A serialization of the `naga::valid::ModuleInfo`, in RON format.
         const ANALYSIS = 1 << 1;
+
         const SPIRV = 1 << 2;
         const METAL = 1 << 3;
         const GLSL = 1 << 4;
diff --git a/player/tests/data/bind-group.ron b/player/tests/data/bind-group.ron
index a53a77b16..87b3a1c72 100644
--- a/player/tests/data/bind-group.ron
+++ b/player/tests/data/bind-group.ron
@@ -70,7 +70,7 @@
                         SetBindGroup(
                             index: 0,
                             num_dynamic_offsets: 0,
-                            bind_group_id: Id(0, 1, Empty),
+                            bind_group_id: Some(Id(0, 1, Empty)),
                         ),
                         SetPipeline(Id(0, 1, Empty)),
                     ],
diff --git a/player/tests/data/zero-init-buffer.ron b/player/tests/data/zero-init-buffer.ron
index b13786e26..2b3f4a890 100644
--- a/player/tests/data/zero-init-buffer.ron
+++ b/player/tests/data/zero-init-buffer.ron
@@ -149,7 +149,7 @@
                         SetBindGroup(
                             index: 0,
                             num_dynamic_offsets: 0,
-                            bind_group_id: Id(0, 1, Empty),
+                            bind_group_id: Some(Id(0, 1, Empty)),
                         ),
                         Dispatch((4, 1, 1)),
                     ],
diff --git a/player/tests/data/zero-init-texture-binding.ron b/player/tests/data/zero-init-texture-binding.ron
index ba4951c19..e828b6c13 100644
--- a/player/tests/data/zero-init-texture-binding.ron
+++ b/player/tests/data/zero-init-texture-binding.ron
@@ -150,7 +150,7 @@
                         SetBindGroup(
                             index: 0,
                             num_dynamic_offsets: 0,
-                            bind_group_id: Id(0, 1, Empty),
+                            bind_group_id: Some(Id(0, 1, Empty)),
                         ),
                         Dispatch((4, 1, 1)),
                     ],
diff --git a/tests/src/image.rs b/tests/src/image.rs
index e72d3ee44..602d93c4e 100644
--- a/tests/src/image.rs
+++ b/tests/src/image.rs
@@ -377,7 +377,7 @@ fn copy_via_compute(
         let mut pass = encoder.begin_compute_pass(&ComputePassDescriptor::default());
 
         pass.set_pipeline(&pipeline_copy);
-        pass.set_bind_group(0, &bg, &[]);
+        pass.set_bind_group(0, Some(&bg), &[]);
         pass.dispatch_workgroups(1, 1, 1);
     }
 
diff --git a/tests/tests/bgra8unorm_storage.rs b/tests/tests/bgra8unorm_storage.rs
index 0859473b2..fa8310c7e 100644
--- a/tests/tests/bgra8unorm_storage.rs
+++ b/tests/tests/bgra8unorm_storage.rs
@@ -110,7 +110,7 @@ static BGRA8_UNORM_STORAGE: GpuTestConfiguration = GpuTestConfiguration::new()
                 timestamp_writes: None,
             });
 
-            pass.set_bind_group(0, &bg, &[]);
+            pass.set_bind_group(0, Some(&bg), &[]);
             pass.set_pipeline(&pipeline);
             pass.dispatch_workgroups(256, 256, 1);
         }
diff --git a/tests/tests/bind_group_layout_dedup.rs b/tests/tests/bind_group_layout_dedup.rs
index b322b019b..f81360288 100644
--- a/tests/tests/bind_group_layout_dedup.rs
+++ b/tests/tests/bind_group_layout_dedup.rs
@@ -92,11 +92,11 @@ async fn bgl_dedupe(ctx: TestingContext) {
         timestamp_writes: None,
     });
 
-    pass.set_bind_group(0, &bg_1b, &[]);
+    pass.set_bind_group(0, Some(&bg_1b), &[]);
     pass.set_pipeline(&pipeline);
     pass.dispatch_workgroups(1, 1, 1);
 
-    pass.set_bind_group(0, &bg_1a, &[]);
+    pass.set_bind_group(0, Some(&bg_1a), &[]);
     pass.dispatch_workgroups(1, 1, 1);
 
     drop(pass);
@@ -179,7 +179,7 @@ fn bgl_dedupe_with_dropped_user_handle(ctx: TestingContext) {
         timestamp_writes: None,
     });
 
-    pass.set_bind_group(0, &bg, &[]);
+    pass.set_bind_group(0, Some(&bg), &[]);
     pass.set_pipeline(&pipeline);
     pass.dispatch_workgroups(1, 1, 1);
 
@@ -250,10 +250,10 @@ fn get_derived_bgl(ctx: TestingContext) {
 
     pass.set_pipeline(&pipeline);
 
-    pass.set_bind_group(0, &bg1, &[]);
+    pass.set_bind_group(0, Some(&bg1), &[]);
     pass.dispatch_workgroups(1, 1, 1);
 
-    pass.set_bind_group(0, &bg2, &[]);
+    pass.set_bind_group(0, Some(&bg2), &[]);
     pass.dispatch_workgroups(1, 1, 1);
 
     drop(pass);
@@ -313,7 +313,7 @@ fn separate_pipelines_have_incompatible_derived_bgls(ctx: TestingContext) {
     pass.set_pipeline(&pipeline1);
 
     // We use the wrong bind group for this pipeline here. This should fail.
-    pass.set_bind_group(0, &bg2, &[]);
+    pass.set_bind_group(0, Some(&bg2), &[]);
     pass.dispatch_workgroups(1, 1, 1);
 
     fail(
@@ -385,7 +385,7 @@ fn derived_bgls_incompatible_with_regular_bgls(ctx: TestingContext) {
 
     pass.set_pipeline(&pipeline);
 
-    pass.set_bind_group(0, &bg, &[]);
+    pass.set_bind_group(0, Some(&bg), &[]);
     pass.dispatch_workgroups(1, 1, 1);
 
     fail(
@@ -476,8 +476,8 @@ fn bgl_dedupe_derived(ctx: TestingContext) {
         timestamp_writes: None,
     });
     pass.set_pipeline(&pipeline);
-    pass.set_bind_group(0, &bind_group_0, &[]);
-    pass.set_bind_group(1, &bind_group_1, &[]);
+    pass.set_bind_group(0, Some(&bind_group_0), &[]);
+    pass.set_bind_group(1, Some(&bind_group_1), &[]);
     pass.dispatch_workgroups(1, 1, 1);
 
     drop(pass);
diff --git a/tests/tests/buffer.rs b/tests/tests/buffer.rs
index b3a48f178..f76a9fc35 100644
--- a/tests/tests/buffer.rs
+++ b/tests/tests/buffer.rs
@@ -328,7 +328,7 @@ static MINIMUM_BUFFER_BINDING_SIZE_DISPATCH: GpuTestConfiguration = GpuTestConfi
                     timestamp_writes: None,
                 });
 
-                pass.set_bind_group(0, &bind_group, &[]);
+                pass.set_bind_group(0, Some(&bind_group), &[]);
                 pass.set_pipeline(&pipeline);
                 pass.dispatch_workgroups(1, 1, 1);
 
diff --git a/tests/tests/compute_pass_ownership.rs b/tests/tests/compute_pass_ownership.rs
index df50f51e0..6b5bad8cf 100644
--- a/tests/tests/compute_pass_ownership.rs
+++ b/tests/tests/compute_pass_ownership.rs
@@ -45,7 +45,7 @@ async fn compute_pass_resource_ownership(ctx: TestingContext) {
     {
         let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor::default());
         cpass.set_pipeline(&pipeline);
-        cpass.set_bind_group(0, &bind_group, &[]);
+        cpass.set_bind_group(0, Some(&bind_group), &[]);
         cpass.dispatch_workgroups_indirect(&indirect_buffer, 0);
 
         // Now drop all resources we set. Then do a device poll to make sure the resources are really not dropped too early, no matter what.
@@ -95,7 +95,7 @@ async fn compute_pass_query_set_ownership_pipeline_statistics(ctx: TestingContex
     {
         let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor::default());
         cpass.set_pipeline(&pipeline);
-        cpass.set_bind_group(0, &bind_group, &[]);
+        cpass.set_bind_group(0, Some(&bind_group), &[]);
         cpass.begin_pipeline_statistics_query(&query_set, 0);
         cpass.dispatch_workgroups(1, 1, 1);
         cpass.end_pipeline_statistics_query();
@@ -153,7 +153,7 @@ async fn compute_pass_query_set_ownership_timestamps(ctx: TestingContext) {
             }),
         });
         cpass.set_pipeline(&pipeline);
-        cpass.set_bind_group(0, &bind_group, &[]);
+        cpass.set_bind_group(0, Some(&bind_group), &[]);
         cpass.write_timestamp(&query_set_write_timestamp, 0);
         cpass.dispatch_workgroups(1, 1, 1);
 
@@ -203,7 +203,7 @@ async fn compute_pass_keep_encoder_alive(ctx: TestingContext) {
 
     // Record some draw commands.
     cpass.set_pipeline(&pipeline);
-    cpass.set_bind_group(0, &bind_group, &[]);
+    cpass.set_bind_group(0, Some(&bind_group), &[]);
     cpass.dispatch_workgroups_indirect(&indirect_buffer, 0);
 
     // Dropping the pass will still execute the pass, even though there's no way to submit it.
diff --git a/tests/tests/mem_leaks.rs b/tests/tests/mem_leaks.rs
index 75de0776e..84879efda 100644
--- a/tests/tests/mem_leaks.rs
+++ b/tests/tests/mem_leaks.rs
@@ -194,7 +194,7 @@ async fn draw_test_with_reports(
     });
 
     rpass.set_pipeline(&pipeline);
-    rpass.set_bind_group(0, &bg, &[]);
+    rpass.set_bind_group(0, Some(&bg), &[]);
 
     let global_report = ctx.instance.generate_report().unwrap();
     let report = global_report.hub_report();
diff --git a/tests/tests/nv12_texture/mod.rs b/tests/tests/nv12_texture/mod.rs
index 6ded163a3..d6af8496f 100644
--- a/tests/tests/nv12_texture/mod.rs
+++ b/tests/tests/nv12_texture/mod.rs
@@ -115,7 +115,7 @@ static NV12_TEXTURE_CREATION_SAMPLING: GpuTestConfiguration = GpuTestConfigurati
             occlusion_query_set: None,
         });
         rpass.set_pipeline(&pipeline);
-        rpass.set_bind_group(0, &bind_group, &[]);
+        rpass.set_bind_group(0, Some(&bind_group), &[]);
         rpass.draw(0..4, 0..1);
         drop(rpass);
         ctx.queue.submit(Some(encoder.finish()));
diff --git a/tests/tests/partially_bounded_arrays/mod.rs b/tests/tests/partially_bounded_arrays/mod.rs
index 195fd88dd..4e6d6fc09 100644
--- a/tests/tests/partially_bounded_arrays/mod.rs
+++ b/tests/tests/partially_bounded_arrays/mod.rs
@@ -90,7 +90,7 @@ static PARTIALLY_BOUNDED_ARRAY: GpuTestConfiguration = GpuTestConfiguration::new
                 timestamp_writes: None,
             });
             cpass.set_pipeline(&compute_pipeline);
-            cpass.set_bind_group(0, &bind_group, &[]);
+            cpass.set_bind_group(0, Some(&bind_group), &[]);
             cpass.dispatch_workgroups(1, 1, 1);
         }
 
diff --git a/tests/tests/pipeline_cache.rs b/tests/tests/pipeline_cache.rs
index 67e9e6827..c88a871c7 100644
--- a/tests/tests/pipeline_cache.rs
+++ b/tests/tests/pipeline_cache.rs
@@ -32,7 +32,7 @@ fn shader() -> String {
         r#"
         @group(0) @binding(0)
         var<storage, read_write> output: array<u32>;
-    
+
         @compute @workgroup_size(1)
         fn main() {{
         {body}
@@ -167,7 +167,7 @@ async fn validate_pipeline(
             timestamp_writes: None,
         });
         cpass.set_pipeline(&pipeline);
-        cpass.set_bind_group(0, bind_group, &[]);
+        cpass.set_bind_group(0, Some(bind_group), &[]);
 
         cpass.dispatch_workgroups(1, 1, 1);
     }
diff --git a/tests/tests/poll.rs b/tests/tests/poll.rs
index 7e99cbcd7..aeea2617f 100644
--- a/tests/tests/poll.rs
+++ b/tests/tests/poll.rs
@@ -46,7 +46,7 @@ fn generate_dummy_work(ctx: &TestingContext) -> CommandBuffer {
         .create_command_encoder(&CommandEncoderDescriptor::default());
 
     let mut cpass = cmd_buf.begin_compute_pass(&ComputePassDescriptor::default());
-    cpass.set_bind_group(0, &bind_group, &[]);
+    cpass.set_bind_group(0, Some(&bind_group), &[]);
     drop(cpass);
 
     cmd_buf.finish()
diff --git a/tests/tests/push_constants.rs b/tests/tests/push_constants.rs
index 905578d53..047fe5c8f 100644
--- a/tests/tests/push_constants.rs
+++ b/tests/tests/push_constants.rs
@@ -119,7 +119,7 @@ async fn partial_update_test(ctx: TestingContext) {
             timestamp_writes: None,
         });
         cpass.set_pipeline(&pipeline);
-        cpass.set_bind_group(0, &bind_group, &[]);
+        cpass.set_bind_group(0, Some(&bind_group), &[]);
 
         // -- Dispatch 0 --
 
diff --git a/tests/tests/regression/issue_3349.rs b/tests/tests/regression/issue_3349.rs
index 21929bd9b..b1361722f 100644
--- a/tests/tests/regression/issue_3349.rs
+++ b/tests/tests/regression/issue_3349.rs
@@ -163,7 +163,7 @@ async fn multi_stage_data_binding_test(ctx: TestingContext) {
         });
 
         rpass.set_pipeline(&pipeline);
-        rpass.set_bind_group(0, &bg, &[]);
+        rpass.set_bind_group(0, Some(&bg), &[]);
         rpass.set_push_constants(
             wgpu::ShaderStages::VERTEX_FRAGMENT,
             0,
diff --git a/tests/tests/render_pass_ownership.rs b/tests/tests/render_pass_ownership.rs
index 7c41c8591..5086d38d9 100644
--- a/tests/tests/render_pass_ownership.rs
+++ b/tests/tests/render_pass_ownership.rs
@@ -87,7 +87,7 @@ async fn render_pass_resource_ownership(ctx: TestingContext) {
         drop(depth_stencil_view);
 
         rpass.set_pipeline(&pipeline);
-        rpass.set_bind_group(0, &bind_group, &[]);
+        rpass.set_bind_group(0, Some(&bind_group), &[]);
         rpass.set_vertex_buffer(0, vertex_buffer.slice(..));
         rpass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
         rpass.begin_occlusion_query(0);
@@ -163,7 +163,7 @@ async fn render_pass_query_set_ownership_pipeline_statistics(ctx: TestingContext
             ..Default::default()
         });
         rpass.set_pipeline(&pipeline);
-        rpass.set_bind_group(0, &bind_group, &[]);
+        rpass.set_bind_group(0, Some(&bind_group), &[]);
         rpass.set_vertex_buffer(0, vertex_buffer.slice(..));
         rpass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
         rpass.begin_pipeline_statistics_query(&query_set, 0);
@@ -242,7 +242,7 @@ async fn render_pass_query_set_ownership_timestamps(ctx: TestingContext) {
         rpass.write_timestamp(&query_set_write_timestamp, 0);
 
         rpass.set_pipeline(&pipeline);
-        rpass.set_bind_group(0, &bind_group, &[]);
+        rpass.set_bind_group(0, Some(&bind_group), &[]);
         rpass.set_vertex_buffer(0, vertex_buffer.slice(..));
         rpass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
         rpass.draw(0..3, 0..1);
@@ -305,7 +305,7 @@ async fn render_pass_keep_encoder_alive(ctx: TestingContext) {
 
     // Record some a draw command.
     rpass.set_pipeline(&pipeline);
-    rpass.set_bind_group(0, &bind_group, &[]);
+    rpass.set_bind_group(0, Some(&bind_group), &[]);
     rpass.set_vertex_buffer(0, vertex_buffer.slice(..));
     rpass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
     rpass.draw(0..3, 0..1);
diff --git a/tests/tests/shader/mod.rs b/tests/tests/shader/mod.rs
index 7d6ed7aaa..2e7dfd942 100644
--- a/tests/tests/shader/mod.rs
+++ b/tests/tests/shader/mod.rs
@@ -349,7 +349,7 @@ async fn shader_input_output_test(
             timestamp_writes: None,
         });
         cpass.set_pipeline(&pipeline);
-        cpass.set_bind_group(0, &bg, &[]);
+        cpass.set_bind_group(0, Some(&bg), &[]);
 
         if let InputStorageType::PushConstant = storage_type {
             cpass.set_push_constants(0, bytemuck::cast_slice(&test.input_values))
diff --git a/tests/tests/shader/zero_init_workgroup_mem.rs b/tests/tests/shader/zero_init_workgroup_mem.rs
index beacb4fcc..084498770 100644
--- a/tests/tests/shader/zero_init_workgroup_mem.rs
+++ b/tests/tests/shader/zero_init_workgroup_mem.rs
@@ -119,7 +119,7 @@ static ZERO_INIT_WORKGROUP_MEMORY: GpuTestConfiguration = GpuTestConfiguration::
 
         cpass.set_pipeline(&pipeline_read);
         for i in 0..NR_OF_DISPATCHES {
-            cpass.set_bind_group(0, &bg, &[i * BUFFER_BINDING_SIZE]);
+            cpass.set_bind_group(0, Some(&bg), &[i * BUFFER_BINDING_SIZE]);
             cpass.dispatch_workgroups(DISPATCH_SIZE.0, DISPATCH_SIZE.1, DISPATCH_SIZE.2);
         }
         drop(cpass);
diff --git a/tests/tests/shader_view_format/mod.rs b/tests/tests/shader_view_format/mod.rs
index b2bc0426e..052573bc0 100644
--- a/tests/tests/shader_view_format/mod.rs
+++ b/tests/tests/shader_view_format/mod.rs
@@ -148,7 +148,7 @@ async fn reinterpret(
         occlusion_query_set: None,
     });
     rpass.set_pipeline(&pipeline);
-    rpass.set_bind_group(0, &bind_group, &[]);
+    rpass.set_bind_group(0, Some(&bind_group), &[]);
     rpass.draw(0..3, 0..1);
     drop(rpass);
     ctx.queue.submit(Some(encoder.finish()));
diff --git a/tests/tests/subgroup_operations/mod.rs b/tests/tests/subgroup_operations/mod.rs
index b11dd7f6a..ecf8adfd7 100644
--- a/tests/tests/subgroup_operations/mod.rs
+++ b/tests/tests/subgroup_operations/mod.rs
@@ -93,7 +93,7 @@ static SUBGROUP_OPERATIONS: GpuTestConfiguration = GpuTestConfiguration::new()
                 timestamp_writes: None,
             });
             cpass.set_pipeline(&compute_pipeline);
-            cpass.set_bind_group(0, &bind_group, &[]);
+            cpass.set_bind_group(0, Some(&bind_group), &[]);
             cpass.dispatch_workgroups(1, 1, 1);
         }
         ctx.queue.submit(Some(encoder.finish()));
diff --git a/tests/tests/vertex_formats/mod.rs b/tests/tests/vertex_formats/mod.rs
index d2ff09447..d447ac8f7 100644
--- a/tests/tests/vertex_formats/mod.rs
+++ b/tests/tests/vertex_formats/mod.rs
@@ -315,7 +315,7 @@ async fn vertex_formats_common(ctx: TestingContext, tests: &[Test<'_>]) {
 
         rpass.set_vertex_buffer(0, buffer_input.slice(..));
         rpass.set_pipeline(&pipeline);
-        rpass.set_bind_group(0, &bg, &[]);
+        rpass.set_bind_group(0, Some(&bg), &[]);
 
         // Draw three vertices and no instance, which is enough to generate the
         // checksums.
diff --git a/tests/tests/vertex_indices/mod.rs b/tests/tests/vertex_indices/mod.rs
index d2b6a874f..5c5ce8a20 100644
--- a/tests/tests/vertex_indices/mod.rs
+++ b/tests/tests/vertex_indices/mod.rs
@@ -409,7 +409,7 @@ async fn vertex_index_common(ctx: TestingContext) {
             render_encoder.set_vertex_buffer(1, identity_buffer.slice(..));
             render_encoder.set_index_buffer(identity_buffer.slice(..), wgpu::IndexFormat::Uint32);
             render_encoder.set_pipeline(pipeline);
-            render_encoder.set_bind_group(0, &bg, &[]);
+            render_encoder.set_bind_group(0, Some(&bg), &[]);
 
             let draws = test.case.draws();