Skip to content
This repository was archived by the owner on Jun 18, 2021. It is now read-only.

Safety of buffer mapping #119

Closed
wants to merge 2 commits into from
Closed

Conversation

Coder-256
Copy link
Contributor

Also removes unnecessary 'static lifetimes. LMK if you would like me to undo 62bbe5f (cargo fmt).

Copy link
Member

@kvark kvark left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got some questions about the chage

where
T: 'static + Copy,
{
) -> CreateBufferMapped<'a, T> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to at least have T: 'a (i.e. T type lives at least as long as 'a) unless it's inferred automatically

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized... the current function is actually unsafe:

let test_buf = device.create_buffer_mapped::<&u8>(8, wgpu::BufferUsage::VERTEX);
println!("{:?}", test_buf.data); // Segmentation fault

I'll need to think about how to fix this...

@@ -888,8 +885,8 @@ where
impl Buffer {
pub fn map_read_async<T, F>(&self, start: BufferAddress, size: BufferAddress, callback: F)
where
T: 'static + FromBytes,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is static removed from here? We don't know when the call back is going to be called, and for this time frame T has to be valid. static is a conservative bound to enforce that

@Coder-256
Copy link
Contributor Author

Ok, I have looked into this further and I realized that this is a much trickier issue than I thought at first. I was looking at Vulkano implemented buffer mapping, but I think that even their implementation is unsafe. It looks like a lot of this library and Vulkano might both need reworking for their mapped buffer implementations to be 100% safe due to the way Rust handles pointers (unallocated, unaligned, etc.). Let's say I have a 256-bit aligned structure or something like that, I don't think either implementation properly handles this. Certainly wgpu_device_create_buffer_mapped doesn't have any alignment information as one example (it just uses the minimum required alignment from the gfx backend). This likely hasn't come up yet because people probably aren't passing many things other than literals to the GPU and back, but currently, there is far too much freedom in what you can send (think references, even 'static references, anything that contains an address or is dependent on a particular instance of a process).

I ended up going wayyy down the rabbit hole and I found out that even ash doesn't handle pointers well! (here; even though [AFAIK] technically it is impossible to create an AlignIter in a safe way, there is no safe way to use it at all because it gives unallocated &mut T rather than *mut T). It's possible that I am wrong about this though.

It all stems from the problem that it's hard to properly manage allocating a buffer for a user-provided type. You have to consider:

  • You can only call ptr.write(...), never *ptr = ... on unallocated values or else you can call drop() by mistake
  • Some types are aligned weirdly, and if at any point in the process you forget that (e.g. casting to *mut u8), you can end up with unaligned pointers, which should not be accessed in any way
  • You need to figure out a good way to make sure that people can't pass any references. Even 'static doesn't fully stop this (const FOO: &'static str = "I'm static!"). This is the crux of what makes user-provided types tricky, they essentially have to be serializable in order to pass them to the GPU (treat it like you're uploading it online or something).
    • It violates Rust's safety guarantees unless you require that all types can be passed to the GPU in a safe way. Otherwise, imagine you decide to read data from the GPU into a user-provided struct. If a field has any kind of reference whatsoever (even 'static references, think multi-threading), then that's UB.
    • zerocopy::{FromBytes, AsBytes} is a good example of one possible generic bound to make sure that you are only passing things that are completely serializable.

I can see that this library is still in the early stages so I'm not too concerned about security yet, but this is definitely something that has to be taken into consideration in the design (see CreateBufferMapped 😕).

Please let me know if I am wrong about anything, and what you think about this issue.

@kvark
Copy link
Member

kvark commented Nov 10, 2019

Thank you for the detailed report on your mapping investigation! I'll try to answer some of the points, please ping me if something is missing.

First of all, it's not completely clear yet if the upstream API will even have mapping, and in what form. See gpuweb/gpuweb#491 for an alternative.

Alignment

You are correct that we aren't doing the right thing if the structure is big. We need to either never have references into mapped area, or take the T size into account when doing the mapping.

References

Previously, we tried to address this by introducing our own trait, see gfx-rs/gfx#999 . Now we use zerocopy traits instead, but it's missing on create_buffer_mapped specifically.

@Coder-256
Copy link
Contributor Author

Coder-256 commented Nov 11, 2019

I've been looking into this some more. These are the basic problems I've found:

  • Creating a reference to uninitialized memory
  • Creating a reference to unaligned memory
  • Unnecessary generic bounds
  • Code should be marked unsafe

For the first problem, right now I'm leaning towards making CreateBufferMapped.data typed as &'a mut [MaybeUninit<T>] to at least start to tackle this. As the docs say:

The compiler, in general, assumes that variables are properly initialized at their respective type. For example, a variable of reference type must be aligned and non-NULL. This is an invariant that must always be upheld, even in unsafe code. As a consequence, zero-initializing a variable of reference type causes instantaneous undefined behavior, no matter whether that reference ever gets used to access memory

Note that this is true for nearly all types, notably including u8 and other numbers. The current implementation violates this requirement by creating a reference to the uninitialized buffer. I think that it's safe to cast an uninitialized buffer as &'a mut [MaybeUninit<T>] (instead of &'a mut MaybeUninit<[T]>, then only finish() will be unsafe because it calls self.data.assume_init(). I also need to think about ZSTs...

create_buffer_mapped() will remain safe. Safe wrappers could also be created to hide the pattern of initializing and calling finish(). Maybe create_buffer_mapped_with_iter(), create_buffer_mapped_from_cloned_slice(), etc. but with better names 😄

About the "staging belt" proposal, I really like it (nit: maybe separate functions for providing data vs. requesting a buffer; also, will synchronously requesting a buffer really be fast enough?). As I understand it, from the perspective of the website, you are still mapping a buffer right? (even if that's not what actually happens behind the scenes).

I am still looking into ways to fix the 2nd and 3rd problems (alignment and generic bounds respectively).

For the last problem, there's a bit of an issue: what actually is the difference between safe and unsafe code when it comes to the GPU? The Nomicon says, in bold, "No matter what, Safe Rust can't cause Undefined Behavior". What actually is "undefined behavior"? Does that include invalid memory access on the GPU, or just the CPU? I think it really depends on if you treat the GPU as a remote black box or as something more directly integrated with your code's resources/execution. IMHO any code on the GPU should also have the same safety guarantees, e.g. never access unallocated/uninitialized/freed memory, among other things. In this case, a number of functions need to either have even safer wrappers created or be marked unsafe.

The majority of functions currently in wgpu-rs would need to be marked unsafe. Maybe I'm being a bit too liberal with the unsafes here, but unless there ends up being some kind of bindgen for whatever shader language is eventually chosen, then even specifying the binding group layout should be an unsafe operation. This means that these would all need to be unsafe:

  • copy_buffer_to_buffer(), copy_buffer_to_texture(), draw_indirect(), etc. since they require you to manually specify a size (and sometimes an offset)
  • create_bind_group_layout(), create_pipeline_layout(), create_render_pipeline() etc. (many, many others) because it is very easy to cause UB if the layout or sizes in your shaders' bindings don't match up
  • Possibly create_shader_module(), for similar reasons

I really think that a bindgen-like solution for shaders would be a good idea here (pass in an uncompiled shader, and it will compile to SPIR-V and generate a struct with binding and type information for each parameter). But that's clearly not going to happen any time soon. In the meantime I just think it's important to figure out what needs to be marked unsafe to hold up Rust's guarantee that it is basically impossible to cause undefined behavior without unsafe (aside from overflow and other edge cases for the time being).

EDIT: Also, I can't figure out for the life of me: how are you supposed to use map_read_async()?

@grovesNL
Copy link
Collaborator

The majority of functions currently in wgpu-rs would need to be marked unsafe.

I don't think this will be necessary, because we'll have validation in each function to avoid all unsafe behavior (including shader validation to prevent out-of-bounds access in a shader for example). So in general the API should (eventually) be fully safe and not require using the unsafe annotation anywhere.

We already have a few asserts to catch some common issues, but we'll keep expanding this to catch all invalid usages and return errors whenever this happens (not just assert/panic).

@kvark
Copy link
Member

kvark commented Nov 11, 2019

In addition to @grovesNL comments:

We are absolutely positive about making this a safe API in Rust sense: we are going to be validating all the state and either panicing or processing the errors carefully, without ever triggering an undefined behavior. We don't want the entry points to be unsafe, therefore, unless there is no other way.

Mapping is a tricky subject, but we shouldn't need MaybeUninit for it either: instead, all the resources should be zero-initialized by default, so when we return a slice to the user, we either know that it has some stuff wrote down by GPU, or our implementation initialized it. That's not yet implemented, but that's the idea that we agree in upstream WebGPU API.

@Coder-256
Copy link
Contributor Author

Coder-256 commented Nov 17, 2019

@grovesNL @kvark I've put some thought into this, and I agree that the wgpu-rs API should be fully safe (or at least have a safe option for everything). However, I've come to the conclusion that sadly it is actually impossible to do buffer mapping, or the staging belt/futures proposals for that matter, using generic user types without better bounds. Here are just a few reasons why, some coming from a list I keep of Rust memory/unsafety "gotchas":

  • For the time being, Rust's memory guarantees are extremely, extremely weak and are still poorly defined. Some examples:
    • There is not a single useful guarantee about the size or layout of a non-#[repr(C)] type. And you can't require a type to be #[repr(C)]. The problem for wgpu-rs is that these types have no defined memory layout. RIP generic uniform buffers. The only #[repr(Rust)] object that has a defined layout is arrays.
    • Not all types can be zeroed. References, for example, cannot be zeroed; that is instant UB. Also, there is no marker type for types that can be zeroed, you need to make your own trait.
    • Unsafe memory cannot be accessed or referenced. Ever. In any type. Seriously. This includes scalars. This is always instant UB. I'm not even kidding:

      Moreover, uninitialized memory is special in that the compiler knows that it does not have a fixed value. This makes it undefined behavior to have uninitialized data in a variable even if that variable has an integer type, which otherwise can hold any fixed bit pattern:

      use std::mem::{self, MaybeUninit};
      
      let x: i32 = unsafe { mem::uninitialized() }; // undefined behavior!
      // The equivalent code with `MaybeUninit<i32>`:
      let x: i32 = unsafe { MaybeUninit::uninit().assume_init() }; // undefined behavior!
      You have to initialize uninitialized memory before you read from it, this is an invariant.
    • Don't forget to make sure things are Send, Sync, 'static, etc. in order to be safe to send to the GPU.
  • There are countless weird quirks about how Rust handles memory and unsafe code, which makes it just unrealistic to take any type. Some random examples:
    • Pinning. As a whole.
    • Dropping. As a whole.
      • Including this:

        Storing through a raw pointer using *ptr = data calls drop on the old value, so write must be used if the type has drop glue and memory is not already initialized - otherwise drop would be called on the uninitialized memory.

    • ptr.offset() is UB if it doesn't point to the same allocation as the original pointer.
    • *const T is covariant, but *mut T is invariant, and NonNull<T> is covariant even though it acts like *mut T.
    • The most common pitfall: creating a reference to memory if that reference would be unsafe to read from (or write for &mut) is instant UB. Even if you don't actually read from it. It has to be initialized, aligned, and valid for its lifetime.
    • You cannot create a pointer to a field of a packed struct. You just can't, because of the above rule. I am not cruel enough to be kidding about this.
    • Pointers to unsized types are not the same size as regular pointers.
    • #[repr(C)] structs can be zero-sized which is not actually possible in C.
    • Zero-sized types can have an alignment that is greater than 1. For example, [u16; 0] can have an alignment of 2 (playground). I admit this makes a bit of sense, but only a bit.
    • (slightly off topic) Fun fact: when you write Rust code that is called from FFI, it is UB to unwind back into FFI. Fun fact #2: The default "panic runtime" behavior is to unwind, which means that if anything panics in a Rust function called from FFI, that's UB by default (and there are countless things that panic in the standard library alone).
  • Let's say you have some way to enforce a type is #[repr(C)] and has only reasonable types (ex. scalars, bools). How do you actually check that it actually has the right layout for the corresponding SPIR-V struct? You can read the SPIR-V to figure out what it's supposed to be, but you can't figure out what the layout of the Rust struct actually is, nevertheless the type of each field.

The point is: I feel very strongly that you cannot catch all invalid usages if you take any generic type at all. Even if you find the proper size and alignment of the type as a whole, there are just too many things that can go wrong. I know that this is kind of ironic to say considering the original goal of the PR 😅 but I have a much better understanding of how wgpu-rs works now. So far the best solution I have thought of is refactoring shader info (bindings, entry points, etc.) into its own unsafe trait so that users can implement it manually (marking it unsafe as a reminder) or eventually can use the bindgen-like solution I've described.

I have looked into the bindgen solution some more, and I really think it is achievable. I tried really hard to think of a theoretical way to just create a derive macro that could verify that the shader info matched a shader, but I think that would sadly be impossible for certain info such as binding groups (it's pretty hard to parse the layout of a struct using a procedural macro, especially accounting for const values). Instead, I believe it's possible to create a procedural macro that takes SPIR-V and uses spirv-cross, possibly aided by user-provided identifiers in the absence of debug info, to output a struct that lets you pass strongly-typed parameters (e.g. vertex buffers, uniform buffer structs, etc.) in/out of entry points, and generates an unsafe impl to show that it has verified that everything is laid out properly. The only other solution I can think of that will actually be fully safe and verifiable is individual functions like read_f32, write_u64_slice, etc. in order to avoid type confusion (IMO these functions actually wouldn't be too bad, just a pain for uniform buffers). Thoughts on this?

In the meantime, I think it's important that at least these functions be made unsafe. I think my solution of using MaybeUninit is a good compromise. It requires the minimum amount of unsafe code that I can think of, and that is mostly just to promise that you've initialized the provided buffer. I'm hesitant to provide a zeroed buffer because the generic type might hold references; although that would be a stupid thing to send to the GPU, it would cause memory unsafety in the host program which is unacceptable in safe Rust. Another possible solution which I prefer is an unsafe but safely derivable marker trait: Zeroable (derivable like Copy, requires its members to be Zeroable; or better yet a trait like GPUSafe that also enforces 'static, Send or whatever else); however I still consider this only a partial solution because it doesn't solve the problems of memory layout, type confusion, and alignment.

To sum it up, possible solutions I've thought of for mapping buffers are:

  1. bindgen-like solution (my favorite, but a lot of work, and possibly unrealistic). Completely safe, super easy for the client to use, but also lets you manually and unsafely specify the shader layout, bind groups, etc.
  2. Zeroed array of T: Zeroable or T: GPUSafe. finish() is still unsafe but in 99% of cases you don't have to worry about that and can just use the one-liner unsafe { buffer.finish() }
  3. Uninitialized array of MaybeUninit<T> with some light restrictions on T. finish() is unsafe.
  4. *mut T and a count. Definitely unsafe.

I think solution #2 is the best short-term, and solution #1 is best long-term. I hope I answered your concerns, and please let me know if I missed anything or got something wrong. Also, for the time being, treat this PR more like an issue.

@Coder-256 Coder-256 changed the title Remove unnecessary generic bounds Safety of buffer mapping Nov 17, 2019
@grovesNL
Copy link
Collaborator

Thanks for the detailed investigation!

However, I've come to the conclusion that sadly it is actually impossible to do buffer mapping, or the staging belt/futures proposals for that matter, using generic user types without better bounds.

If this turns out to be the case, we should still be able to safely expose buffer mappings as &[u8] (instead of &[T] and trying to handle the &[T]<->&[u8] for users).

For the time being, Rust's memory guarantees are extremely, extremely weak and are still poorly defined. There are countless weird quirks about how Rust handles memory and unsafe code, which makes it just unrealistic to take any type.

Could we use the traits from zerocopy to have better guarantees about byte-alignment as we're doing in a couple places? In general I think it's fine to require additional trait bounds here.

Let's say you have some way to enforce a type is #[repr(C)] and has only reasonable types (ex. scalars, bools). How do you actually check that it actually has the right layout for the corresponding SPIR-V struct? You can read the SPIR-V to figure out what it's supposed to be, but you can't figure out what the layout of the Rust struct actually is, nevertheless the type of each field.

We're converting &[T] to &[u8] (or the reverse), so for safety we should only need to ensure that this conversion is safe. The remainder of validation/safety checks are done at the &[u8] level (i.e. preventing reads of uninitialized memory, bounds checking). So I don't think we need to worry about comparing Rust struct members to SPIR-V, we just need to worry about ensuring we can serialize or deserialize &[T] into bytes.

To sum it up, possible solutions

I think solution 2 is closer to what we would prefer, but with a stronger trait bound that doesn't require unsafe in order to use it. For example, using zerocopy's AsBytes+FromBytes and deriving these (like #[derive(AsBytes)]) is similar to a combination of 1 and 2. This would give us the guarantee of being able to convert to/from bytes as a trait bound without having to run bindgen.

@kvark
Copy link
Member

kvark commented Nov 18, 2019

@Coder-256 The depth at which you are investigating this issue is incredible! I do have a feeling that maybe you are digging in a slightly wrong direction. You are trying to show that supporting generic T for mapping is impossible, but nobody claimed it is. Quite the opposite, I said above that we need the zerocopy trait bound on the mapping:

Now we use zerocopy traits instead, but it's missing on create_buffer_mapped specifically.

The API safety would stop right after we expose mapping as &[u8]. Conversion to and from it doesn't have to be safe, we don't care much even (since it's not affecting the implementation).

@Coder-256
Copy link
Contributor Author

@grovesNL @kvark Thank you both for helping with this!

If this turns out to be the case, we should still be able to safely expose buffer mappings as &[u8] (instead of &[T] and trying to handle the &[T]<->&[u8] for users).

Yes, that's true... although I think that leaving it up to users to convert a slice of bytes back into their struct wouldn't be the best UX. More on that below. I agree that it is safe though (with AsBytes or similar).

Could we use the traits from zerocopy to have better guarantees about byte-alignment as we're doing in a couple places? In general I think it's fine to require additional trait bounds here.

Still looking into this. However, while looking into the way zerocopy::AsBytes works, I realized that it requires packed structs, and after researching a bit, I have realized that we would need to do the same. The reason is that padding bytes are always undefined (aka 0xUU). This stinks but the good news is that if you want well-aligned, packed structs, you can always just manually insert padding with [u8; 3] or something like that. For example:

// Total alignment for a struct is the maximum alignment.
// Total size for a packed struct is the sum of sizes.
// `Foo` has an alignment of 4 and a size of 12.
// In this case, `packed(4)` is already implied, I'm just being explicit.
#[repr(C, packed(4))]
struct Foo {
    bar: u8,       // align: 1, size: 1
    _pad0: [u8; 3] // align: 1, size: 3
    baz: f32,      // align: 4, size: 4
    qux: u16,      // align: 2, size: 2
    _pad1: [u8; 2] // align: 1, size: 2
    // _pad1 makes the struct's size a multiple of its alignment
    // This is necessary in order to emulate a non-packed struct
}

Side note: I think the corresponding struct in SPIR-V would not need to have these padding bytes if the graphics API ensures that it is safe for the CPU to read from uninitialized padding bytes, but I haven't looked into the portability of this guarantee.

We're converting &[T] to &[u8] (or the reverse), so for safety we should only need to ensure that this conversion is safe. The remainder of validation/safety checks are done at the &[u8] level (i.e. preventing reads of uninitialized memory, bounds checking). So I don't think we need to worry about comparing Rust struct members to SPIR-V, we just need to worry about ensuring we can serialize or deserialize &[T] into bytes.

The API safety would stop right after we expose mapping as &[u8]. Conversion to and from it doesn't have to be safe, we don't care much even (since it's not affecting the implementation).

I respectfully disagree. I am not just trying to show that mapping T is impossible: I believe that &[u8] is impossible too.

With Rust's ridiculously complex padding/alignment issues I mentioned above, it is super easy for users to mess this all up. IMHO I feel that it is very important that if there is any way that wgpu-rs could verify the actual struct layout, it has an obligation to do so. Look at struct Foo from above. That is the only correct way to transfer a well-aligned, packed struct of a u8, f32, and u16 (in that order) while maintaining minimal size and alignment. It is simply unreasonable to expect users to get this right. Without verifying the struct layout you'd get floats in scalars, types in the wrong places, incorrectly aligned data, and all the things that type-safe languages like Rust are supposed to fix. Just passing around &[u8] slices is not a viable solution, plus for most structs, both wgpu-rs and the user would need to manually align it. For example, if they want to use zerocopy, well zerocopy::LayoutVerified::new will just return None if the slice happens to be unaligned. There is no way around this. See the UdpHeader example; there's a good chance that users would have to copy the buffer to an aligned address if they want u16s, because u16s are not Unaligned.

Again, this is just my opinion, but I do feel strongly that leaving users on their own with a &[u8] is not a good way to do this. There needs to be some way to generate the proper struct.

@Coder-256
Copy link
Contributor Author

Coder-256 commented Nov 21, 2019

Also, completely unrelated, are you sure that this is valid?:

wgpu-rs/src/lib.rs

Lines 548 to 563 in 95787f1

/// Retrieves an [`Adapter`] which matches the given options.
///
/// Some options are "soft", so treated as non-mandatory. Others are "hard".
///
/// If no adapters are found that suffice all the "hard" options, `None` is returned.
pub fn request(options: &RequestAdapterOptions, backends: BackendBit) -> Option<Self> {
unsafe extern "C" fn adapter_callback(id: core::id::AdapterId, user_data: *mut std::ffi::c_void) {
*(user_data as *mut core::id::AdapterId) = id;
}
let mut id = core::id::AdapterId::ERROR;
wgn::wgpu_request_adapter_async(Some(options), backends, adapter_callback, &mut id as *mut _ as *mut std::ffi::c_void);
Some(Adapter {
id,
})
}

Couldn't adapter_callback race with the user dropping the Adapter?

Edit: What I mean to say is: shouldn't Adapter::request block on the callback?

@grovesNL
Copy link
Collaborator

Again, this is just my opinion, but I do feel strongly that leaving users on their own with a &[u8] is not a good way to do this. There needs to be some way to generate the proper struct.

This is the way WebGPU works in general. The type of these mapped buffers is ArrayBuffer on the web (https://gpuweb.github.io/gpuweb/#typedefdef-gpumappedbuffer), which is basically just a byte slice.

If we can provide some safe helpers to convert between &[T] and &[u8] then that's useful, but if not then we would have to fallback on using &[u8] here. Similarly the safety guarantee is for &[u8] to be used safely, not providing any guarantees about T matching a type in SPIR-V (it might even be useful sometimes for T to be aliased and match multiple types in different shaders).

Couldn't adapter_callback race with the user dropping the Adapter?

Currently no because the callback fires immediately inline. The callback isn't scheduled/queued anywhere yet (see gfx-rs/wgpu#375 (comment) for an explanation). We're planning to change that soon, at which point this signature and implementation would change too.

bors bot added a commit that referenced this pull request Nov 21, 2019
126: Fix generic bounds on buffer mapping r=grovesNL a=kvark

Smaller (and incomplete!) alternative to #119 while it's still discussed.
One missing piece here is alignment. cc @Coder-256

Also, importing `wgpu-core` as just "core" wasn't the best idea 😅 : it collides with the actual `core`.

Co-authored-by: Dzmitry Malyshau <[email protected]>
@Coder-256
Copy link
Contributor Author

@grovesNL

This is the way WebGPU works in general. The type of these mapped buffers is ArrayBuffer on the web (gpuweb.github.io/gpuweb/#typedefdef-gpumappedbuffer), which is basically just a byte slice.

If we can provide some safe helpers to convert between &[T] and &[u8] then that's useful, but if not then we would have to fallback on using &[u8] here. Similarly the safety guarantee is for &[u8] to be used safely, not providing any guarantees about T matching a type in SPIR-V (it might even be useful sometimes for T to be aliased and match multiple types in different shaders).

I am aware that it is an ArrayBuffer in the WebIDL, and I guess I'm ok with just exposing &[u8] safely as long as it's clear that users need to take into account the issues I mentioned above. I'm also pasting my comment on #126:

I think this solves part of the issue, but why not just return a &[u8]? I'm not convinced that would be the best long-term solution, but it would partially fix the problem of alignment (since [u8] has an alignment of 1). Users can initialize their data from manual offsets with from_ne_bytes, which was recently stabilized for floats (so it should be available on 1.40).

That being said, I think it would be very helpful to be able to request an alignment so that you can use *zerocopy::LayoutVerified::new(bytes), and conversion helpers would be ideal. I'll start looking into a procedural macro to help with generating a struct.

I guess this just comes down to the intended level of safety guarantee of wgpu-core/wgpu-rs, which I admit I don't fully understand. The way I had seen it was that wgpu-rs would be a safe interface eventually meant for WASM, is that correct?

Full disclosure: I am not an expert on WebGPU, I am pretty new to the proposal. Anyway, my point has been that even though WebGPU is designed for JS, it is also designed to be very low-level like Vulkan. It seems like a natural consequence that a Rust wrapper should be safe. For example, it should offer a way to transmute to a custom struct while upholding type safety if doing so is possible, and I believe it is. Exposing a raw buffer of &[u8] as the only way to communicate with the GPU, which is the only safe thing to do without helpers, feels like it is just moving the burden of unsafety onto the users. It encourages them to manually create a properly packed type and use it with zerocopy::LayoutVerified with no guarantee that the layout matches the GPU. It is infeasible to catch all possible invalid uses of the GPU at compile-time, but this one stands out to me as something that we can do this for.

Bottom line: I'm ok with safely exposing a &[u8] since technically it is safe, but I feel that as the only safe solution, this stops short of the safety guarantees it could offer by verifying the data layout, and leaves it up to users to ensure that the last step of conversion is safe. I admit that this is the case for the WebGPU specification, but with Rust's data layout, stronger guarantees are possible.

@kvark
Copy link
Member

kvark commented Nov 21, 2019

(I rewrote the answer a couple of times before posting...)

The way I had seen it was that wgpu-rs would be a safe interface eventually meant for WASM, is that correct?

correct

For example, it should offer a way to transmute to a custom struct while upholding type safety if doing so is possible, and I believe it is.

The open question (that needs to be answered for this) is whether or not we can even check the layout of uniform data that the shader uses. If SPIR-V exposes this, and would be able to check if the layout matches CPU side, in theory. It feels like we are a looong way to there, possibly past the Javelin checkpoint.

It just feels like layout matching falls into a large category of "cool safety things we could do in the future", somewhere behind the generically bounds buffer and texture usages. It's not required for us to expose a safe API, it's nice, it's probably going to be complicated. So the best way would be for it to go into some sort of a wrapper crate, e.g. wgpu-safety or something.

@Coder-256
Copy link
Contributor Author

I've already looked into this a bit. SPIR-V definitely exposes this info (although possibly without field names, but that might be part of the debug info such as OpMemberName if not optimized away). spirv_cross already somewhat exposes this info. I honestly think it wouldn't be too hard to parse this! I'm thinking of playing around with it sometime soon.

bors bot added a commit that referenced this pull request Nov 22, 2019
127: Use u8 for buffer mapping r=kvark a=Coder-256

cc @kvark @grovesNL

This is a temporary solution for #119, and a follow-up for #126.

Co-authored-by: Jacob Greenfield <[email protected]>
@kvark
Copy link
Member

kvark commented Dec 18, 2019

Alright, it looks like the problem will need to be attacked from another angle. Closing this, please feel free to reopen!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants