Wgpu WriteOnly vs bytemuch vs encase

Ref: rend3-hp/rend3/src/util/scatter_copy.rs at 5480169a3dad0092b8b8da18429358177f3a0470 · John-Nagle/rend3-hp · GitHub

I'm trying to update rend3-hp (my fork of abandoned rend3) to wgpu 30, from wgpu 24. There were many breaking changes. I'm down from 150 compile errors to only 2. But I'm stuck on those.

error[E0308]: mismatched types
   --> rend3/src/util/scatter_copy.rs:103:65
    |
103 |         let mapped_slice: &mut [u32] = bytemuck::cast_slice_mut(&mut mapped_range);
    |                                        ------------------------ ^^^^^^^^^^^^^^^^^ expected `&mut [_]`, found `&mut Result<BufferViewMut, ...>`
    |                                        |
    |                                        arguments to this function are incorrect
    |
    = note: expected mutable reference `&mut [_]`
               found mutable reference `&mut std::result::Result<BufferViewMut, MapRangeError>`
note: function defined here
   --> /home/john/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytemuck-1.25.2/src/lib.rs:423:8
    |
423 | pub fn cast_slice_mut<
    |        ^^^^^^^^^^^^^^

This worked with WGPU 24.

Here's the trouble spot:

        let mut writer = encase::internal::Writer::new(
                &item.data,
                bytemuck::cast_slice_mut::<_, u8>(&mut mapped_slice[range_start + 1..range_end]),
                0,
            )
            .unwrap();
            item.data.write_into(&mut writer);

The first problem is just that mapped_range is now a Result. That's easy to fix. But after fixing that, we have a type mismatch.

A Writer from encase encapsulates a destination, which is then passed to write_into of the source data. That then calls a function of the Writer. This is kind of backwards, but that's the way it's set up. The Writer mechanism supports repeated writes, but here the code creates a Writer and discards it after one Write, so this is a lot more complex than it needs to be.

What this code is doing is writing directly to a part of the address space mapped as write-only on the CPU and read-only on the GPU. Wgpu recently introduced the trait WriteOnly, which is like MaybeUninit; you can write, but not read. Although WriteOnly means don't even try to read it; you'll get a memory protection fault.

Since WriteOnly was introduced in a high level crate, Wgpu, the bytemuck and encase low level crates don't know about it. But there are WGPU functions that need their parameter to be WriteOnly. There's no way I can add that trait to an existing data item. I can change rend3-hp, but not wgpu, bytemuck, or encase.

The functions in scatter_copy mostly take generic parameters. So we can't do much with them except use the very few available functions of their traits. All the function demands of the data is that it be ShaderSize + WriteInto They're not Pod, for example, which would help with conversion. (Also, bytemuck has its own Pod type. rather than using the one from the Pod crate, just to confuse things.) There was so much generic and trait cleverness in this area that adding WriteOnly broke too much.

All this really does is put the bytes of item.data into the mapped slice. Is there some straightforward way to do that?

there is a wrapper struct and an enum variant named WriteOnly, but I don't see a trait with the same name.

can you show an example of the compile error messages related to this?

You're right, WriteOnly is not a trait, but an enum.

Make this change to the code above, because get_mapped_range_mut now returns a Result:

    //////let mut mapped_range = source_buffer.slice(..).get_mapped_range_mut();
    let mut mapped_range = source_buffer.slice(..).get_mapped_range_mut().expect("Unable to map range of GPU memory");

Now the error is:

error[E0308]: mismatched types
   --> rend3/src/util/scatter_copy.rs:104:65
    |
104 |         let mapped_slice: &mut [u32] = bytemuck::cast_slice_mut(&mut mapped_range);
    |                                        ------------------------ ^^^^^^^^^^^^^^^^^ expected `&mut [_]`, found `&mut BufferViewMut`
    |                                        |
    |                                        arguments to this function are incorrect
    |
    = note: expected mutable reference `&mut [_]`
               found mutable reference `&mut BufferViewMut`
note: function defined here
   --> /home/john/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytemuck-1.25.2/src/lib.rs:423:8
    |
423 | pub fn cast_slice_mut<
    |        ^^^^^^^^^^^^^^

So the problem is that we can't get from BufferViewMut to &mut [_].

That's because all we can get out of BufferViewMut is a WriteOnly. See:

And that won't go into an &mut [_].

That's what the introduction of WriteOnly did. You can't get to regular &mut from there by any route I've been able to figure out. This is a legit restriction. We'd need something like bytemuck::cast_slice_write_only to make this work. But bytemuck is a crate below wgpu, which defines WriteOnly.

The correct fix is that WriteOnly ought to be in some place like std::mem, so wgpu, encase, and bytemuck could all use it. But I need a workaround. That's a year of politics to fix.

I believe you can make this work if you:

  • create a wrapper around WriteOnly<[u8]> (this is needed because we'll implement a foreign trait for it)
  • implement encase::internal::BufferMut for it (this should be doable since BufferMut only needs to know the capacity and the ability to write bytes into it; you should be able to copy most of the implementation for &mut [u8])
  • create a new encase::internal::Writer by passing this wrapper instead of a &mut [u8]

I'll have another go at that, but I went down that road once and got stuck into another typing corner. If WriteOnly was a trait, it wouldn't be too bad, but it's an enum.

Could you be a little more explicit about "Create a wrapper?". Thanks.

This usually means using a new-type pattern. If you have foreign type Foo and foreign trait Bar, you can do something like this:

struct MyFoo(Foo);

impl Bar for MyFoo {}

and that's the whole point of the WriteOnly wrapper: to not expose a regular mut reference.

the user is forced to either use the WriteOnly api, or use raw pointers and unsafe.

you don't need a slice, what you really need is a way to write data to the mapped address.

the Writer is parameterized over a buffer type. to construct a Writer, you need a BufferMut. the old version used bytemuck and a slice as the buffer, now you need to implement a custom BufferMut using the newer WriteOnly api.

I meant something like this:

struct WriteOnlyBuf<'a>(wgpu::WriteOnly<'a, [u8]>);

impl<'a> encase::internal::BufferMut for WriteOnlyBuf<'a> {
    #[inline]
    fn capacity(&self) -> usize {
        self.0.len()
    }

    #[inline]
    fn write<const N: usize>(&mut self, offset: usize, val: &[u8; N]) {
        self.write_slice(offset, val);
    }

    #[inline]
    fn write_slice(&mut self, offset: usize, val: &[u8]) {
        self.0.slice(offset..offset+val.len()).copy_from_slice(val);
    }
}

let mut mapped_range = source_buffer
    .get_mapped_range_mut()
    .expect("Unable to map range of GPU memory")
    .slice(..);

let mut writer = encase::internal::Writer::new(&item.data, WriteOnlyBuf(mapped_range), 0);
item.data.write_into(&mut writer);

Speaking as the author of the WriteOnly type, I certainly agree that moving it to a lower-level crate not part of wgpu, that has fewer major releases, would make things more convenient and be better for the ecosystem. However, it is not ready for this. It’s already needed one breaking change (moving from normal writes to volatile writes) and may have more in the future. Once the answer to “how should Rust programs access write combining memory” is more settled and tested, it may make sense for WriteOnly to be moved to a separate library crate.

For now, you can use wrappers like @SkiFire13 posted.

Yes. That fix does work, after some additional fixes. Or at least can be made to compile.

Looks like there are similar problems with UniformBuffer and QueueWriteBufferView. More later if necessary.

QueueWriteBufferView::slice() returns a WriteOnly, so you don’t need a separate wrapper dedicated to that case.

Thanks.

There are about a half dozen places where rend3 needs to write into a write-only buffer, and they're all a bit different. Here's one in unsafe code. With some of the new features of WriteOnly, I was able to rewrite this unsafe use of a raw pointer in Rend3:

    let joint_matrices_ptr = joint_matrices_data.as_mut_ptr() as *mut [[f32; 4]; 4];
    .....
    joint_matrices_ptr.add(joint_matrix_idx as usize)
       .write_unaligned(joint_matrix.to_cols_array_2d());

as this safe code:

//  Write only memory target for new joint matrices.
let mut joint_matrices_slice: wgpu::WriteOnly<'_, [u8]> = joint_matrices_data
    .expect("Can't get joint matrices data")
    .slice(..);
....
joint_matrices_slice.slice(joint_matrix_idx as usize .. (joint_matrix_idx as usize + 4 * 4 * std::mem::size_of::<f32>()))
    .copy_from_slice(bytemuck::cast_slice(&joint_matrix.to_cols_array_2d()));

and was then able to remove an unsafe around the block. I think this is right, but can't test until I've fixed every breaking change and can run "cargo test"

It's amusing to realize that most of that slicing and type conversions generate no code.

I now see what the intent is here. WriteOnly is an improvement. But it's a painful retrofit. This wgpu upgrade is taking me far too much effort. 690 lines of diffs so far to go from wgpu 24 to wgpu 30. About 80-90% done.

Has anyone tried using Claude Code on this kind of task? Going forward, that's probably going to be the industry metric for breaking changes: "Can Claude upgrade all the using code?"

The problem is then you're putting a "reasonable" dollar price on an upgrade. Better that wgpu simply gets to something they are happy to call 1.0, surely!

Now if we ever get to dumb local models being capable enough to one shot a port like this, I'm listening...