use std::error::Error;
use std::fmt;
use smudge_core::app::FrameOutput;
use vello::RenderParams;
use vello::wgpu::util::TextureBlitter;
use vello::wgpu::{
Adapter, CommandEncoderDescriptor, CompositeAlphaMode, CurrentSurfaceTexture, Device, Extent3d,
PresentMode, Queue, Surface, SurfaceColorSpace, SurfaceConfiguration, SurfaceTexture, Texture,
TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureView,
TextureViewDescriptor,
};
use crate::encoder::{Encoder, peniko_color};
fn is_vello_format(format: &TextureFormat) -> bool {
matches!(
format,
TextureFormat::Rgba8Unorm | TextureFormat::Bgra8Unorm
)
}
fn pick_format(formats: &[TextureFormat]) -> Option<TextureFormat> {
formats.iter().copied().find(is_vello_format)
}
#[derive(Debug)]
pub enum GpuError {
UnsupportedFormat,
SurfaceTooLarge {
width: u32,
height: u32,
limit: u32,
},
SurfaceLost,
Render(vello::Error),
}
impl fmt::Display for GpuError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GpuError::UnsupportedFormat => write!(
formatter,
"the window surface offers no format the GPU renderer supports"
),
GpuError::SurfaceTooLarge {
width,
height,
limit,
} => write!(
formatter,
"a {width}x{height} surface exceeds the device's texture limit of {limit}"
),
GpuError::SurfaceLost => {
write!(formatter, "the window surface or the device was lost")
}
GpuError::Render(error) => write!(formatter, "the vello render failed: {error}"),
}
}
}
impl Error for GpuError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
GpuError::Render(error) => Some(error),
GpuError::UnsupportedFormat
| GpuError::SurfaceTooLarge { .. }
| GpuError::SurfaceLost => None,
}
}
}
pub struct GpuSurface {
device: Device,
queue: Queue,
surface: Surface<'static>,
config: SurfaceConfiguration,
renderer: vello::Renderer,
encoder: Encoder,
blitter: TextureBlitter,
target: (Texture, TextureView),
reconfigure: bool,
}
impl GpuSurface {
pub fn new(
surface: Surface<'static>,
adapter: &Adapter,
device: Device,
queue: Queue,
width: u32,
height: u32,
) -> Result<GpuSurface, GpuError> {
let limit = device.limits().max_texture_dimension_2d;
if width > limit || height > limit {
return Err(GpuError::SurfaceTooLarge {
width,
height,
limit,
});
}
let capabilities = surface.get_capabilities(adapter);
let format = pick_format(&capabilities.formats).ok_or(GpuError::UnsupportedFormat)?;
let config = SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format,
color_space: SurfaceColorSpace::Auto,
width,
height,
present_mode: PresentMode::Fifo,
desired_maximum_frame_latency: 2,
alpha_mode: CompositeAlphaMode::Auto,
view_formats: Vec::new(),
};
surface.configure(&device, &config);
let renderer = vello::Renderer::new(&device, vello::RendererOptions::default())
.expect("the device cannot create the vello pipelines");
let blitter = TextureBlitter::new(&device, format);
let target = create_target(&device, width, height);
Ok(GpuSurface {
device,
queue,
surface,
config,
renderer,
encoder: Encoder::new(),
blitter,
target,
reconfigure: false,
})
}
pub fn resize(&mut self, width: u32, height: u32) -> Result<(), GpuError> {
assert!(
width > 0 && height > 0,
"a resized surface must keep a positive extent"
);
let limit = self.device.limits().max_texture_dimension_2d;
if width > limit || height > limit {
return Err(GpuError::SurfaceTooLarge {
width,
height,
limit,
});
}
self.config.width = width;
self.config.height = height;
self.surface.configure(&self.device, &self.config);
self.reconfigure = false;
self.target = create_target(&self.device, width, height);
Ok(())
}
pub fn width(&self) -> u32 {
self.config.width
}
pub fn height(&self) -> u32 {
self.config.height
}
pub fn acquire(&mut self) -> Result<Option<SurfaceTexture>, GpuError> {
if self.reconfigure {
self.reconfigure = false;
self.surface.configure(&self.device, &self.config);
}
match self.surface.get_current_texture() {
CurrentSurfaceTexture::Outdated => {
self.surface.configure(&self.device, &self.config);
self.texture_from(self.surface.get_current_texture())
}
other => self.texture_from(other),
}
}
fn texture_from(
&mut self,
current: CurrentSurfaceTexture,
) -> Result<Option<SurfaceTexture>, GpuError> {
match current {
CurrentSurfaceTexture::Success(texture) => Ok(Some(texture)),
CurrentSurfaceTexture::Suboptimal(texture) => {
self.reconfigure = true;
Ok(Some(texture))
}
CurrentSurfaceTexture::Timeout
| CurrentSurfaceTexture::Occluded
| CurrentSurfaceTexture::Outdated => Ok(None),
_ => Err(GpuError::SurfaceLost),
}
}
pub fn draw(
&mut self,
frame: &FrameOutput<'_>,
texture: SurfaceTexture,
) -> Result<(), GpuError> {
let params = RenderParams {
base_color: peniko_color(frame.clear_color),
width: self.config.width,
height: self.config.height,
antialiasing_method: vello::AaConfig::Area,
};
let GpuSurface {
device,
queue,
renderer,
encoder,
blitter,
target,
..
} = self;
encoder.encode(frame);
renderer
.render_to_texture(device, queue, encoder.scene(), &target.1, ¶ms)
.map_err(GpuError::Render)?;
let surface_view = texture
.texture
.create_view(&TextureViewDescriptor::default());
let mut command = device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("smudge present"),
});
blitter.copy(device, &mut command, &target.1, &surface_view);
queue.submit([command.finish()]);
queue.present(texture);
Ok(())
}
}
fn create_target(device: &Device, width: u32, height: u32) -> (Texture, TextureView) {
let texture = device.create_texture(&TextureDescriptor {
label: Some("smudge gpu surface"),
size: Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let view = texture.create_view(&TextureViewDescriptor::default());
(texture, view)
}
#[cfg(test)]
#[path = "surface_tests.rs"]
mod tests;