Custom Views

Custom views are types that you can borrow (like View or UniqueView) but are not provided by shipyard.

Many types can become custom views, they'll fall into one of two categories: View Bundle or Wild View. View bundles only contain other views while wild views can contain other types.

Example of a View Bundle:

struct Hierarchy<'v> {
    entities: EntitiesViewMut<'v>,
    parents: ViewMut<'v, Parent>,
    children: ViewMut<'v, Child>,
}

Example of a Wild View:

struct RandomNumber(u64);

Concrete example

When creating a frame with any low level api there is always some boilerplate. We'll look at how custom views can help for wgpu.

The original code creates the frame in a system by borrowing Graphics which contains everything needed. The rendering part just clears the screen with a color.

The entire starting code for this chapter is available in this file. You can copy all of it in a fresh main.rs and edit the fresh Cargo.toml.

Original
#[derive(Unique)]
struct Graphics {
    surface: wgpu::Surface,
    device: wgpu::Device,
    queue: wgpu::Queue,
    config: wgpu::SurfaceConfiguration,
    size: winit::dpi::PhysicalSize<u32>,
}

fn render(graphics: UniqueView<Graphics>) -> Result<(), wgpu::SurfaceError> {
    // Get a few things from the GPU
    let output = graphics.surface.get_current_texture()?;
    let view = output
        .texture
        .create_view(&wgpu::TextureViewDescriptor::default());

    let mut encoder = graphics
        .device
        .create_command_encoder(&wgpu::CommandEncoderDescriptor {
            label: Some("Render Encoder"),
        });

    {
        // RenderPass borrows encoder for all its lifetime
        let mut _render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
            label: Some("Render Pass"),
            color_attachments: &[wgpu::RenderPassColorAttachment {
                view: &view,
                resolve_target: None,
                ops: wgpu::Operations {
                    load: wgpu::LoadOp::Clear(wgpu::Color {
                        r: 0.1,
                        g: 0.2,
                        b: 0.3,
                        a: 1.0,
                    }),
                    store: true,
                },
            }],
            depth_stencil_attachment: None,
        });
    }

    // encoder.finish() consumes `encoder`, so the RenderPass needs to disappear before that to release the borrow
    graphics.queue.submit(iter::once(encoder.finish()));
    output.present();

    Ok(())
}

We want to abstract the beginning and end of the system to get this version working. The error handling is going to move, we could keep it closer to the original by having a ResultRenderGraphicsViewMut for example.

fn render(mut graphics: RenderGraphicsViewMut) {
    let mut _render_pass = graphics
        .encoder
        .begin_render_pass(&wgpu::RenderPassDescriptor {
            label: Some("Render Pass"),
            color_attachments: &[wgpu::RenderPassColorAttachment {
                view: &graphics.view,
                resolve_target: None,
                ops: wgpu::Operations {
                    load: wgpu::LoadOp::Clear(wgpu::Color {
                        r: 0.1,
                        g: 0.2,
                        b: 0.3,
                        a: 1.0,
                    }),
                    store: true,
                },
            }],
            depth_stencil_attachment: None,
        });
}

We'll start by creating a struct to hold our init state.

struct RenderGraphicsViewMut {
    view: wgpu::TextureView,
    encoder: wgpu::CommandEncoder,
}

Now let's make this struct able to be borrowed and generate the initial state we need.

impl<'v> shipyard::Borrow<'v> for RenderGraphicsViewMut {
    type View = RenderGraphicsViewMut;

    fn borrow(
        world: &'v shipyard::World,
        last_run: Option<u32>,
        current: TrackingTimestamp,
    ) -> Result<Self::View, shipyard::error::GetStorage> {
        // Even if we don't use tracking for Graphics, it's good to build an habit of using last_run and current when creating custom views
        let graphics = <UniqueView<Graphics> as shipyard::IntoBorrow>::Borrow::borrow(
            world, last_run, current,
        )?;
        // This error will now be reported as an error during the view creation process and not the system but is still bubbled up
        let output = graphics
            .surface
            .get_current_texture()
            .map_err(shipyard::error::GetStorage::from_custom)?;
        let view = output
            .texture
            .create_view(&wgpu::TextureViewDescriptor::default());

        let encoder = graphics
            .device
            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
                label: Some("Render Encoder"),
            });

        Ok(RenderGraphicsViewMut {
            encoder,
            view,
        })
    }
}

We now have a custom view! We can't change our system just yet, we're missing output.

Let's add output and graphics to our custom view.

struct RenderGraphicsViewMut<'v> {
    encoder: wgpu::CommandEncoder,
    view: wgpu::TextureView,
    // New fields
    output: Option<wgpu::SurfaceTexture>,
    graphics: UniqueView<'v, Graphics>,
}

Since our view now has a lifetime we need a bit of boilerplate (explanation).

struct RenderGraphicsBorrower {}

impl shipyard::IntoBorrow for RenderGraphicsViewMut<'_> {
    type Borrow = RenderGraphicsBorrower;
}

With that our of the way we can revisit our Borrow implementation and add one for Drop.

impl<'v> shipyard::Borrow<'v> for RenderGraphicsBorrower {
    type View = RenderGraphicsViewMut<'v>;

    fn borrow(
        world: &'v shipyard::World,
        last_run: Option<u32>,
        current: TrackingTimestamp,
    ) -> Result<Self::View, shipyard::error::GetStorage> {
        let graphics = <UniqueView<Graphics> as shipyard::IntoBorrow>::Borrow::borrow(
            world, last_run, current,
        )?;
        let output = graphics
            .surface
            .get_current_texture()
            .map_err(shipyard::error::GetStorage::from_custom)?;
        let view = output
            .texture
            .create_view(&wgpu::TextureViewDescriptor::default());

        let encoder = graphics
            .device
            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
                label: Some("Render Encoder"),
            });

        Ok(RenderGraphicsViewMut {
            encoder,
            view,
            output: Some(output),
            graphics,
        })
    }
}

impl Drop for RenderGraphicsViewMut<'_> {
    fn drop(&mut self) {
        // I chose to swap here to not have to use an `Option<wgpu::CommandEncoder>` in a publicly accessible field
        let encoder = std::mem::replace(
            &mut self.encoder,
            self.graphics
                .device
                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
                    label: Some("Render Encoder"),
                }),
        );

        self.graphics.queue.submit(iter::once(encoder.finish()));
        // output on the other hand is only used here so an `Option` is good enough
        self.output.take().unwrap().present();
    }
}

Our custom view is now fully functional and we successfully moved code that would be duplicated out of the render system. You can remove the error handling in main.rs to see the result.

As a final touch we can implement BorrowInfo and AllStoragesBorrow. Respectively to make our view work with workloads and AllStorages.

// SAFE: All storages info is recorded.
unsafe impl shipyard::BorrowInfo for RenderGraphicsViewMut<'_> {
    fn borrow_info(info: &mut Vec<shipyard::info::TypeInfo>) {
        <UniqueView<Graphics>>::borrow_info(info);
    }
}

impl<'v> shipyard::AllStoragesBorrow<'v> for RenderGraphicsBorrower {
    fn all_borrow(
        all_storages: &'v shipyard::AllStorages,
        last_run: Option<u32>,
        current: TrackingTimestamp,
    ) -> Result<Self::View, shipyard::error::GetStorage> {
        let graphics = <UniqueView<Graphics> as shipyard::IntoBorrow>::Borrow::all_borrow(
            all_storages,
            last_run,
            current,
        )?;
        let output = graphics
            .surface
            .get_current_texture()
            .map_err(shipyard::error::GetStorage::from_custom)?;
        let view = output
            .texture
            .create_view(&wgpu::TextureViewDescriptor::default());

        let encoder = graphics
            .device
            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
                label: Some("Render Encoder"),
            });

        Ok(RenderGraphicsViewMut {
            encoder,
            view,
            output: Some(output),
            graphics,
        })
    }
}