wgpu_context 0.4.0

Context for managing WGPU surfaces
Documentation
use crate::{DeviceHandle, WgpuContextError, util::create_texture};
use wgpu::{
    CommandEncoderDescriptor, CompositeAlphaMode, Device, PresentMode, Queue, Surface,
    SurfaceConfiguration, SurfaceError, SurfaceTexture, TextureFormat, TextureUsages, TextureView,
    TextureViewDescriptor, util::TextureBlitter,
};

#[derive(Clone)]
pub struct TextureConfiguration {
    pub usage: TextureUsages,
}

#[derive(Clone)]
pub struct SurfaceRendererConfiguration {
    /// The usage of the swap chain. The only usage guaranteed to be supported is [`TextureUsages::RENDER_ATTACHMENT`].
    pub usage: TextureUsages,
    /// The texture format of the swap chain. The only formats that are guaranteed are
    /// [`TextureFormat::Bgra8Unorm`] and [`TextureFormat::Bgra8UnormSrgb`].
    pub formats: Vec<TextureFormat>,
    /// Width of the swap chain. Must be the same size as the surface, and nonzero.
    ///
    /// If this is not the same size as the underlying surface (e.g. if it is
    /// set once, and the window is later resized), the behaviour is defined
    /// but platform-specific, and may change in the future (currently macOS
    /// scales the surface, other platforms may do something else).
    pub width: u32,
    /// Height of the swap chain. Must be the same size as the surface, and nonzero.
    ///
    /// If this is not the same size as the underlying surface (e.g. if it is
    /// set once, and the window is later resized), the behaviour is defined
    /// but platform-specific, and may change in the future (currently macOS
    /// scales the surface, other platforms may do something else).
    pub height: u32,
    /// Presentation mode of the swap chain. Fifo is the only mode guaranteed to be supported.
    /// `FifoRelaxed`, `Immediate`, and `Mailbox` will crash if unsupported, while `AutoVsync` and
    /// `AutoNoVsync` will gracefully do a designed sets of fallbacks if their primary modes are
    /// unsupported.
    pub present_mode: PresentMode,
    /// Desired maximum number of frames that the presentation engine should queue in advance.
    ///
    /// This is a hint to the backend implementation and will always be clamped to the supported range.
    /// As a consequence, either the maximum frame latency is set directly on the swap chain,
    /// or waits on present are scheduled to avoid exceeding the maximum frame latency if supported,
    /// or the swap chain size is set to (max-latency + 1).
    ///
    /// Defaults to 2 when created via `Surface::get_default_config`.
    ///
    /// Typical values range from 3 to 1, but higher values are possible:
    /// * Choose 2 or higher for potentially smoother frame display, as it allows to be at least one frame
    ///   to be queued up. This typically avoids starving the GPU's work queue.
    ///   Higher values are useful for achieving a constant flow of frames to the display under varying load.
    /// * Choose 1 for low latency from frame recording to frame display.
    ///   ⚠️ If the backend does not support waiting on present, this will cause the CPU to wait for the GPU
    ///   to finish all work related to the previous frame when calling `Surface::get_current_texture`,
    ///   causing CPU-GPU serialization (i.e. when `Surface::get_current_texture` returns, the GPU might be idle).
    ///   It is currently not possible to query this. See <https://github.com/gfx-rs/wgpu/issues/2869>.
    /// * A value of 0 is generally not supported and always clamped to a higher value.
    pub desired_maximum_frame_latency: u32,
    /// Specifies how the alpha channel of the textures should be handled during compositing.
    pub alpha_mode: CompositeAlphaMode,
    /// Specifies what view formats will be allowed when calling `Texture::create_view` on the texture returned by `Surface::get_current_texture`.
    ///
    /// View formats of the same format as the texture are always allowed.
    ///
    /// Note: currently, only the srgb-ness is allowed to change. (ex: `Rgba8Unorm` texture + `Rgba8UnormSrgb` view)
    pub view_formats: Vec<TextureFormat>,
}

struct IntermediateTextureStuff {
    pub config: TextureConfiguration,
    // TextureView for the intermediate Texture which we sometimes render to because compute shaders
    // cannot always render directly to surfaces. Since WGPU 26, the underlying Texture can be accessed
    // from the TextureView so we don't need to store both.
    pub texture_view: TextureView,
    // Blitter for blitting from the intermediate texture to the surface.
    pub blitter: TextureBlitter,
}

/// Combination of surface and its configuration.
pub struct SurfaceRenderer<'s> {
    // The device and queue for rendering to the surface
    pub dev_id: usize,
    pub device_handle: DeviceHandle,

    // The surface and it's configuration
    pub surface: Surface<'s>,
    pub config: SurfaceConfiguration,

    current_surface_texture: Option<Result<SurfaceTexture, SurfaceError>>,
    intermediate_texture: Option<Box<IntermediateTextureStuff>>,
}

impl std::fmt::Debug for SurfaceRenderer<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("SurfaceRenderer")
            .field("dev_id", &self.dev_id)
            .field("surface_config", &self.config)
            .field("has_intermediate_texture", &true)
            .finish()
    }
}

impl<'s> SurfaceRenderer<'s> {
    /// Creates a new render surface for the specified window and dimensions.
    pub async fn new<'w>(
        surface: Surface<'w>,
        surface_renderer_config: SurfaceRendererConfiguration,
        intermediate_texture_config: Option<TextureConfiguration>,
        device_handle: DeviceHandle,
        dev_id: usize,
    ) -> Result<SurfaceRenderer<'w>, WgpuContextError> {
        // Convert SurfaceRendererConfiguration to SurfaceConfiguration.
        // The difference is that `format` is a Vec in SurfaceRendererConfiguration and a single value in SurfaceConfiguration
        let surface_config = SurfaceConfiguration {
            usage: surface_renderer_config.usage,
            format: surface
                .get_capabilities(&device_handle.adapter)
                .formats
                .into_iter()
                .find(|it| surface_renderer_config.formats.contains(it))
                .ok_or(WgpuContextError::UnsupportedSurfaceFormat)?,
            width: surface_renderer_config.width,
            height: surface_renderer_config.height,
            present_mode: surface_renderer_config.present_mode,
            desired_maximum_frame_latency: surface_renderer_config.desired_maximum_frame_latency,
            alpha_mode: surface_renderer_config.alpha_mode,
            view_formats: surface_renderer_config.view_formats,
        };

        let intermediate_texture = intermediate_texture_config.map(|texture_config| {
            Box::new(IntermediateTextureStuff {
                config: texture_config.clone(),
                texture_view: create_texture(
                    surface_renderer_config.width,
                    surface_renderer_config.height,
                    TextureFormat::Rgba8Unorm,
                    texture_config.usage,
                    &device_handle.device,
                ),
                blitter: TextureBlitter::new(&device_handle.device, surface_config.format),
            })
        });

        let surface = SurfaceRenderer {
            dev_id,
            device_handle,
            surface,
            config: surface_config,
            current_surface_texture: None,
            intermediate_texture,
        };
        surface.configure();
        Ok(surface)
    }

    pub fn device(&self) -> &Device {
        &self.device_handle.device
    }

    pub fn queue(&self) -> &Queue {
        &self.device_handle.queue
    }

    /// Resizes the surface to the new dimensions.
    pub fn resize(&mut self, width: u32, height: u32) {
        // TODO: Use clever resize semantics to avoid thrashing the memory allocator during a resize
        // especially important on metal.
        if let Some(intermediate_texture_stuff) = &mut self.intermediate_texture {
            intermediate_texture_stuff.texture_view = create_texture(
                width,
                height,
                TextureFormat::Rgba8Unorm,
                intermediate_texture_stuff.config.usage,
                &self.device_handle.device,
            );
        }
        self.config.width = width;
        self.config.height = height;
        self.configure();
    }

    pub fn set_present_mode(&mut self, present_mode: wgpu::PresentMode) {
        self.config.present_mode = present_mode;
        self.configure();
    }

    fn configure(&self) {
        self.surface
            .configure(&self.device_handle.device, &self.config);
    }

    pub fn clear_surface_texture(&mut self) {
        self.current_surface_texture = None;
    }

    pub fn ensure_current_surface_texture(&mut self) -> Result<(), SurfaceError> {
        if self.current_surface_texture.is_none() {
            let tex = self.surface.get_current_texture();
            if let Err(SurfaceError::Lost | SurfaceError::Outdated) = &tex {
                self.surface
                    .configure(&self.device_handle.device, &self.config);
            }

            self.current_surface_texture = Some(tex);
        }

        self.current_surface_texture
            .as_ref()
            .unwrap()
            .as_ref()
            .map(|_| ())
            .map_err(|err| err.clone())
    }

    /// Get a target texture view to render to.
    ///
    /// If there is an intermediate texture, this is a view of that intermediate texture, otherwise
    /// it is a view of the surface texture.
    pub fn target_texture_view(&mut self) -> Result<TextureView, SurfaceError> {
        match &self.intermediate_texture {
            Some(intermediate_texture) => Ok(intermediate_texture.texture_view.clone()),
            None => {
                self.ensure_current_surface_texture()?;
                Ok(self
                    .current_surface_texture
                    .as_ref()
                    .unwrap()
                    .as_ref()
                    .unwrap()
                    .texture
                    .create_view(&TextureViewDescriptor::default()))
            }
        }
    }

    /// Present the texture to the surface. If there is an intermediate texture, this first blits
    /// from the intermediate texture to the surface texture.
    ///
    /// Prior to calling this, [`Self::target_texture_view`] must have been called and some
    /// rendering work must have been scheduled to the resulting view.
    pub fn maybe_blit_and_present(&mut self) -> Result<(), SurfaceError> {
        self.ensure_current_surface_texture()?;
        let surface_texture = self.current_surface_texture.take().unwrap().unwrap();

        if let Some(its) = &self.intermediate_texture {
            self.blit_from_intermediate_texture_to_surface(&surface_texture, its);
        }

        surface_texture.present();

        Ok(())
    }

    /// Blit from the intermediate texture to the surface texture
    fn blit_from_intermediate_texture_to_surface(
        &self,
        surface_texture: &SurfaceTexture,
        intermediate_texture_stuff: &IntermediateTextureStuff,
    ) {
        // TODO: verify that handling of SurfaceError::Outdated is no longer required
        //
        // let surface_texture = match state.surface.surface.get_current_texture() {
        //     Ok(surface) => surface,
        //     // When resizing too aggresively, the surface can get outdated (another resize) before being rendered into
        //     Err(SurfaceError::Outdated) => return,
        //     Err(_) => panic!("failed to get surface texture"),
        // };

        // Perform the copy
        // (TODO: Does it improve throughput to acquire the surface after the previous texture render has happened?)
        let mut encoder =
            self.device_handle
                .device
                .create_command_encoder(&CommandEncoderDescriptor {
                    label: Some("Surface Blit"),
                });

        intermediate_texture_stuff.blitter.copy(
            &self.device_handle.device,
            &mut encoder,
            &intermediate_texture_stuff.texture_view,
            &surface_texture
                .texture
                .create_view(&TextureViewDescriptor::default()),
        );
        self.device_handle.queue.submit([encoder.finish()]);
    }
}