Skip to content

Safely integrate Godot cross-thread calls (e.g. AudioStreamPlayback::mix) #1131

Open
@djcsdy

Description

@djcsdy

This is a rough proposal for how to safely implement functions that currently require experimental-threads. It's not completely fleshed out but I wanted to see if there's any interest before I expand on it further.

If you're not taking proposals on how to solve this problem at the moment, or you don't find this proposal useful, I won't be offended if you close the issue.

Take AudioStreamPlayback as an example:

unsafe fn mix(&mut self, buffer: * mut AudioFrame, rate_scale: f32, frames: i32,) -> i32;

Godot calls mix from a non-main thread, which means that access to self in this function is unsound. (Without experimental-threads this condition is checked at runtime by godot-rust, which will panic before mix is even called).

My proposal is to remove the self parameter from functions that are called from a non-main thread, and replace it with a user-defined state parameter, which must impl Clone + Send + 'static and can therefore be passed between threads safely. For example:

trait IAudioStreamPlayback<AudioState> /* : ... */
where
    AudioState: Clone + Send + 'static
{
    /* ... */
    unsafe fn mix(state: AudioState, buffer: * mut AudioFrame, rate_scale: f32, frames: i32) -> i32;
    /* ... */
}

A sensible choice for AudioState might be for example Arc<RwLock<S>>, where S is some arbitrary struct. The AudioState could then be safely read and mutated from multiple threads. But the idea is that the actual type of AudioState should be left up to the programmer as long as it implements Clone + Send + 'static.

The state object should be attached to the class struct, for example:

#[derive(GodotClass)]
#[class(base=AudioStreamPlayback)]
struct Example {
    #[audio_state]
    state: Arc<RwLock<ExampleState>>,
}

#[godot_api]
impl IAudioStreamPlayback<Arc<RwLock<ExampleState>> for Example {
    unsafe fn mix(state: Arc<RwLock<ExampleState>>, buffer: * mut AudioFrame, rate_scale: f32, frames: i32) -> i32 {
        / * ... */
        frames
    }
}

struct ExampleState {
    position: usize,
}

godot-rust would then have some glue code that takes care of cloning state and passing it through to IAudioStreamPlayback::mix when mix is called from Godot.

It would be illegal to mark a field #[export] if it is also marked #[audio_state].

This approach is roughly what I'm using in my own project, although I of course have to implement the glue manually each time I extend AudioStreamPlayback.

I think this general approach should be logically extensible to other cases where Godot calls functions from non-main threads.

If you're interested in something like this approach I would be interested in having a go at implementing it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    breaking-changeRequires SemVer bumpc: engineGodot classes (nodes, resources, ...)c: threadsRelated to multithreading in GodotfeatureAdds functionality to the library

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions