Shub's logo

T-3: SwapChain and Image Views in  gfx-hal

06 Jun, 2020

5 min read

Before diving in, let's first understand what Double Buffering is. A great video on Double Buffering to get a fundamental understanding.

As you can see in the video, double buffering is nothing but a way to cache images in a buffer. One of the Buffer is used to draw on the Window Surface, while other acts as a hidden canvas to draw the next frame and keep it in the cache, to get better performance and framerate.

Swapchain to Surface to Window Draw

Swapchain to Surface to Window Draw

Swapchain is very similar. It can be used to achieve double buffering. In simple words, Swapchain is a collection Images (Stored in Image Buffers), that can be used to draw frames and present them on a screen when needed.

Quoting directly from falseidolfactory.com

Swapchain is a chain of images that we can render onto and then present to our window. While we’re showing one of them on screen, we can render it to a different one. Then once we’re done rendering, we can swap them.

This is one of the few places where gfx departs significantly from the Vulkan API. In Vulkan, you create and manage the swapchain yourself. In gfx, the surface mostly does it for you. You can read more about the decision behind that here.

The above decision, to handle swapchain internally in gfx-hal, was made to hide logical improvements done by gfx-hal to improve performance in MacOS Metal and DirectX 12 backends. Metal backend doesn't quite work well with Vulkan Swapchain API; thus, the team decided to manage the logic internally to provide a performant cross-platform GPU Driver framework.

Discussion on the above issue can be read here.

Thus, Swapchain in gfx-rs can be described in simple words:

The following diagram illustrates how Swapchain is created in Vulkan, and how we need to bind it to surface instance, to make it work. (We do not have to worry about all these details, in gfx-hal though)

gfx-hal Swapchain flow

gfx-hal Swapchain flow

Let's Code

First, we will discuss on how swapchain is created in Vulkan:

In gfx-hal, point-2 i.e., the creation of Swapchain is not something a developer needs to worry about. We need to use the Swapchain support details to configure_swapchain using the surface instance.

Let's Breakdown the above code in 3 sections:
Configuration
struct Renderer<B: Backend> {
    ...
    // Collection Swapchain Image, Empty buffer initially
    frame_count: usize,
    // Desired Format / Selected Format
    format: hal_format::Format,
}

impl<B: Backend> Renderer<B> {
    fn new(instance: B::Instance, surface: B::Surface) -> Self {
        ...

        // Configure Swapchain
        let (frame_count, format) = {
            let caps = surface.capabilities(&adapter.physical_device);

            let supported_formats = surface.supported_formats(&adapter.physical_device);
            // We need a supported format for the OS Window, so that Images drawn on
            // Swapchain are of that same format.
            let format = supported_formats.map_or(hal_format::Format::Rgba8Srgb, |formats| {
                formats
                    .iter()
                    .find(|format| format.base_format().1 == hal_format::ChannelType::Srgb)
                    .map(|format| *format)
                    .unwrap_or(formats[0])
            });

            let swap_config = hal_window::SwapchainConfig::from_caps(&caps, format, init_extent);
            let image_extent = swap_config.extent.to_extent();

            unsafe {
                surface
                    .configure_swapchain(&device, swap_config)
                    .expect("Can't configure swapchain");
            };

            (3, format)
        };

        Renderer {
            ...
            frame_count,
            format,
        }
    }
}
...

As discussed before, first, we need to query details for Swapchain support. Once we have the details, we will use them to configure our swapchain using gfx-hal surface instance.

We will keep a reference to format for later use, especially for re-configuring the swapchain when the window surface becomes invalidated, like on window resize and other related events.

frame_count is the number of frames we would be parallelly working with. I am not exactly sure right now, why 3 is the frame count used in most of the gfx-hal examples. My guess is, it's because it's the number of image_counts any GPU currently supports for buffering.

Re-Configuration

Some changes to be done to our code, before moving forward

impl<B: Backend> Renderer<B> {
fn new(
instance: B::Instance,
mut surface: B::Surface,
init_extent: hal_window::Extent2D,
) -> Self {
...
}
}
...
fn create_backend(
wb: window::WindowBuilder,
ev_loop: &event_loop::EventLoop<()>,
extent: hal_window::Extent2D,
) -> (back::Instance, back::Surface, window::Window) {
...
}
...
fn main() {
let (window_builder, extent) = build_window(&ev_loop);
let (instance, surface, window) = create_backend(window_builder, &ev_loop, extent);
let (instance, surface, window) = create_backend(window_builder, &ev_loop);
...
let renderer = Renderer::<back::Backend>::new(instance, surface);
let mut renderer = Renderer::<back::Backend>::new(instance, surface, extent);
...
}

Changes from above include:

Re-Configuration is an important part of making Swapchain work. It is required to notify the swapchain that the window is invalidated, and thus every image that it contains should be invalidated and re-created.

struct Renderer<B: Backend> {
    window_dims: hal_window::Extent2D,
    viewport: Viewport,
    ...
}

impl<B: Backend> Renderer<B> {
    fn new(
        ...
    ) -> Self {
        ...
        let viewport = Viewport {
            rect: Rect {
                x: 0,
                y: 0,
                w: init_extent.width as _,
                h: init_extent.height as _,
            },
            depth: 0.0..1.0,
        };

        Renderer {
            window_dims: init_extent,
            viewport,
            ...
        }
    }

    fn set_dims(&mut self, dims: PhysicalSize<u32>) {
        self.window_dims = hal_window::Extent2D {
            width: dims.width,
            height: dims.height,
        };
    }

    fn recreate_swapchain(&mut self) {
        let caps = self.surface.capabilities(&self.adapter.physical_device);
        let swap_config =
            hal_window::SwapchainConfig::from_caps(&caps, self.format, self.window_dims);
        let image_extent = swap_config.extent.to_extent();

        unsafe {
            self.surface
                .configure_swapchain(&self.device, swap_config)
                .expect("Can't create swapchain");
        }

        self.viewport.rect.w = image_extent.width as _;
        self.viewport.rect.h = image_extent.height as _;
    }
}

fn main() -> Result<(), &'static str> {
    ...
    ev_loop.run(move |event, _, control_flow| {
        ...
        event::WindowEvent::Resized(dims) => {
            // We need to re-configure our swapchain
            // with new window dimensions on every re-size
            renderer.set_dims(dims);
            renderer.recreate_swapchain();
        }
        ...
    }
}

We are re-configuring our swapchain on every resize event.

Similarly, we should also re-configure for other window events that will invalidate the Vulkan surface like winit's ScaleFactorChanged, which we will skip for now.

The viewport will be used later when we will render our images.

Destruction

A part where we need to memory manage our swapchain. Since we created (configured) swapchain using surface, we will have to use surface again to destroy it as well, before our struct Renderer gets destroyed.

impl<B: Backend> Drop for Renderer<B> {
    fn drop(&mut self) {
        unsafe {
            self.surface.unconfigure_swapchain(&self.device);
            ...
        }
    }
}

Once that is done, swapchain will automatically get dropped by surface, before surface itself is dropped.

Image Views

We won't be working with ImageViews in gfx-hal (at-least for this tutorial), but in Vulkan ImageView is a very integral part when it comes to SwapChain. It is just a representation (view) of the actual Image that SwapChain contains.

Quoting directly from Vulkan Tutorial

An image view is quite literally a view into an image. It describes how to access the image and which part of the image to access, for example if it should be treated as a 2D texture depth texture without any mipmapping levels.

Since we do not actually interact with Swapchain in gfx-hal, gfx-hal provides us APIs to get SwapchainImage, which can be rendered to the screen directly. Consider it as a frame per unit time.


You can find the full code for this Doc, here 003-swap_chain

© Copyright 2020 Subroto Biswas

Share