1

In the following minimal code example, a MutexGuard is used for accessing a BindGroup that is sure to exist longer than the lifetime of the RenderPass. However, this is obviously not known to the Rust compiler, resulting in a lifetime error.

For context: both RenderPass and BindGroup stem from the wgpu crate. Thus, it is not possible for me to simply adjust their method parameters.
The RenderPass is created every frame, whereas the TextureManager is only created once the program starts.
The TextureManager keeps the reference to the BindGroup as Arc<Mutex<…>> in order to manipulate it from multiple threads.

use core::marker::PhantomData;
use std::ops::Deref;
use std::sync::{Arc, Mutex};

fn main() {
    let texture_manager = TextureManager {
        active_bind_group: Arc::new(Mutex::new(None)),
    };
    texture_manager.activate_texture(0);

    let mut render_pass = RenderPass::new();

    step(&mut render_pass, &texture_manager);
}

fn step<'pass>(render_pass: &mut RenderPass<'pass>, texture_manager: &'pass TextureManager) {
    let guard = texture_manager.active_bind_group.lock().unwrap();
    if let Some(bind_group) = guard.deref() {
        render_pass.set_bind_group(bind_group);
    }
}

struct TextureManager {
    active_bind_group: Arc<Mutex<Option<BindGroup>>>,
    // textures: Arc<Mutex<Vec<Texture>>>,
}

impl TextureManager {
    // Only uses &self instead of &mut self for providing immutable interface
    // so that a `Arc<TextureManager>` suffices for use in multithreaded code
    fn activate_texture(&self, index: usize) {
        // let texture = textures.lock().unwrap()[index];
        // update bind group using the texture
        *self.active_bind_group.lock().unwrap() = Some(BindGroup);
    }
}

struct BindGroup;

struct RenderPass<'pass> {
    phantom: PhantomData<&'pass ()>,
}

impl<'pass> RenderPass<'pass> {
    fn new() -> Self {
        Self {
            phantom: PhantomData,
        }
    }

    fn set_bind_group(&mut self, bind_group: &'pass BindGroup) {}
}

Rust Playground

The resulting error:

error[E0597]: `guard` does not live long enough
  --> src/main.rs:18:31
   |
16 | fn step<'pass>(render_pass: &mut RenderPass<'pass>, texture_manager: &'pass TextureManager) {
   |         ----- lifetime `'pass` defined here
17 |     let guard = texture_manager.active_bind_group.lock().unwrap();
   |         ----- binding `guard` declared here
18 |     if let Some(bind_group) = guard.deref() {
   |                               ^^^^^^^^^^^^^
   |                               |
   |                               borrowed value does not live long enough
   |                               argument requires that `guard` is borrowed for `'pass`
...
21 | }
   | - `guard` dropped here while still borrowed

I understand the origin of the error: the MutexGuard only lives for the scope of the step method. As soon as it is dropped, a reference to the original value can no longer be ensured.
Still, I haven't found any solution as to how to solve this problem.
Are there potentially different constructs than Mutex to use in this scenario?

KingOfDog
  • 65
  • 1
  • 12
  • Why the `Mutex`? If the value is never modified, like in this example, then there is no reason for it and you can just drop it. If the value is modified, then the compiler is correct: it may be modified while it is borrowed, resulting in a dangling reference. – Chayim Friedman Aug 09 '23 at 09:53
  • @ChayimFriedman I didn't include any of the code that's modifying the `Mutex` value in the example. All the actual functionality of the `TextureManager` is omitted. I'll update the example to clarify the intention of the struct. – KingOfDog Aug 09 '23 at 10:04
  • Is the only usage of the `Mutex` to be lazily-initialized? That is, you set it once and never change it? – Chayim Friedman Aug 09 '23 at 12:23
  • @ChayimFriedman No, it can also be changed later on to change the active texture. – KingOfDog Aug 09 '23 at 12:47
  • Then there's nothing you can do (besides unsafe code, which I wouldn't recommend). What would you do when it is changing? You will have a dangling reference. – Chayim Friedman Aug 09 '23 at 12:58
  • @ChayimFriedman That's the response I feared. But I feel like there should be a solution to ensure the current value of the `Mutex` lives for the duration of the frame / render pass. – KingOfDog Aug 09 '23 at 12:59
  • 1
    I don't know any language that has such feature. In C++ you would have UB if you forget to update the `RenderPass` when the `BindGroup` changes; in GC languages you would have a silent bug of using the wrong `BindGroup`; Rust is not GC and does not allow for UB in safe code, so you just cannot do that. – Chayim Friedman Aug 09 '23 at 13:08

1 Answers1

2

Fundamentally, what you have here is not just a matter of writing down the right lifetime; you have to demonstrate to the compiler that the bind group you're borrowing will not be dropped or mutated while it is in use by the render pass.

One way to do this is to move the MutexGuard into the same scope as the RenderPass (instead of step) so that it is known to live long enough. This basically means inlining step() into main().

Another way, that is more composable, is to make use of shared ownership. Put the BindGroup into an Arc, and clone it, so that even if the value in the mutex is changed, the old value is still available — immutable and not dropped — to the render pass. However, you'll still need a place to stash the cloned Arc to hold it alive for the desired period. The straightforward and efficient way to do this is to set up the bind groups you want to use in some variable before creating the render pass:

fn main() {
    let texture_manager = TextureManager {
        active_bind_group: Arc::new(Mutex::new(None)),
    };
    texture_manager.activate_texture(0);

    let bind_group_to_use = get_bind_group(&texture_manager); // before pass
    let mut render_pass = RenderPass::new();
    if let Some(g) = bind_group_to_use {                      // within pass
        render_pass.set_bind_group(&g);
    }
}

/// This is what used to be the step() function
fn get_bind_group(texture_manager: &TextureManager) -> Option<Arc<BindGroup>> {
    Option::clone(&texture_manager.active_bind_group.lock().unwrap())
}

struct TextureManager {
    active_bind_group: Arc<Mutex<Option<Arc<BindGroup>>>>,
}

impl TextureManager {
    fn activate_texture(&self, index: usize) {
        // creates the Arc
        *self.active_bind_group.lock().unwrap() = Some(Arc::new(BindGroup));
    }
}

Playground

But perhaps this won't do. Perhaps you actually need to potentially create many bind groups, and do it while you're building the RenderPass. In that case, typed_arena::Arena can help you — it gives you a place to stash borrowable things that all have the same lifetime even if some of them didn't exist at the time you start borrowing other ones.

The disadvantage of using an arena is that the arena must allocate memory for its elements.

use typed_arena::Arena;

pub fn main() {
    let texture_manager = TextureManager {
        active_bind_group: Arc::new(Mutex::new(None)),
    };
    texture_manager.activate_texture(0);

    let arena = Arena::new();
    let mut render_pass = RenderPass::new();

    step(&arena, &mut render_pass, &texture_manager);
}

fn step<'mutex: 'pass, 'pass>(
    arena: &'pass Arena<MutexGuard<'mutex, Option<BindGroup>>>,
    render_pass: &mut RenderPass<'pass>,
    texture_manager: &'mutex TextureManager,
) {
    let guard = texture_manager.active_bind_group.lock().unwrap();
    let guard = arena.alloc(guard);
    if let Some(bind_group) = &**guard {
        render_pass.set_bind_group(bind_group);
    }
}

You could also combine both of the above, by storing Arc<BindGroup>s in the Arena instead of MutexGuards. This would give even more flexibility — you can use render passes from any source (as long as they are Arced), and the arena won't have the 'mutex lifetime in its type.

Kevin Reid
  • 37,492
  • 13
  • 80
  • 108