gfx-rs / wgpu

A cross-platform, safe, pure-Rust graphics API.
https://wgpu.rs
Apache License 2.0
12.01k stars 871 forks source link

Make wgpu-core less generic (brainstorm) #5124

Closed nical closed 1 week ago

nical commented 7 months ago

I'll use this issue to explore various ways we could make wgpu-core less generic.

The motivations include reducing the clutter of overly generic code and more importantly to reduce binary size, compile times.

Factor out non-generic code into separate functions

There is some amount of code that could be easily factored out, for example some of the validation. Isolating the validation also has the advantage that it can be laid out to match the specification so that we can more easily check that the validation is correct. There is only a limited amount of bang for buck we can get

Making the resources not generic

We'll get the biggest wins by far if we can erase the type of the resources. most of wgpu-core is generic only because something needs to hold on to a generic resource.

Downcasting

Assuming we can accept the cost of downcasting every resource at the top of every hal functions, we could use trait objects and downcast via Any.

// in wgpu-hal:

/// Implemented by most hal types
pub trait Resource: 'static {
    fn as_any(&self) -> &dyn std::any::Any;
}

// Helper for downcasting. Failure to downcast is always an internal error
pub fn downcast<T:'static>(resource: &dyn Resource) -> &T {
    resource.as_any().downcast_ref::<T>().unwrap()
}

The gpu-hal APIs would take &dyn Resource parameters instead of some associated types, and the first thing they would do is downcast these dyn traits into the concrete types they are expecting.

Note that we call also break it down into plenty of traits like TextureResource, BufferResource, etc. That's a rather small detail once we have overcome the other obstacles.

Boxed resources

The simple approach (which I find very unsatisfying) is to box all hal resources:

// in wgpu-core:

pub struct BindGroupLayout {
    // ...

    raw: Box<dyn hal::Resource>,
}

The wgpu-core and wgpu-hal structures are no longer next to each other in memory, that most likely has a cost but it is hard to estimate it without doing all of the work.

Dynamically sized types

The last member of a struct can be a DST, which turns the structure into a dst as well:

pub struct BindGroupLayoutResource<Hal: ?Sized> {
    // ...
    pub raw: Hal
}

// The alias that is used in most places
pub type BindGroupLayout = BindGroupLayoutResource<dyn hal::Resource>;

pub fn create_bind_group_layout(device: &Device) -> Result<Arc<BindGroupLayout>, Error> {
    let typed_bgl = Arc::new(BindGroupLayoutResource {
        label: String::new(),
        // TODO: Here we need the concrete hal structure before we can erase its type. Ideally
        // we would like device_create_* functions to not be generic (to allow out of tree backends).
        raw: hal::vulkan::BindGroupLayout {

        },
    });

    // BindGroupLayoutResource<hal::BindGroupLayout> can be converted into a BindGroupLayoutReosurce<dyn hal::Resource>
    Ok(typed_bgl)
}

It's quite nice, however there are a few constraints:

Code of the DST experiment: https://gist.github.com/nical/c5d88aaf97f20815756a36dc5c94b5a3 it also shows how snatchable resources would work.

Can we do it without Any/downcast?

Maybe, with a copious amount of unsafe code which might be fine. Suggestions are welcome!