use futures_executor::block_on;
use smudge_core::app::FrameOutput;
use smudge_core::renderer::Renderer;
use vello::RenderParams;
use vello::wgpu;
use vello::wgpu::{
Device, DeviceType, Extent3d, Instance, InstanceDescriptor, MapMode, PollType, Queue,
RequestAdapterOptions, TexelCopyBufferInfo, Texture, TextureDescriptor, TextureDimension,
TextureFormat, TextureUsages, TextureView, TextureViewDescriptor,
};
use crate::encoder::{Encoder, peniko_color};
const ROW_ALIGNMENT: u32 = 256;
pub struct GpuRenderer {
device: Device,
queue: Queue,
width: u32,
height: u32,
target: Option<(Texture, TextureView)>,
renderer: vello::Renderer,
encoder: Encoder,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AdapterSummary {
pub name: String,
pub backend: &'static str,
pub device_type: &'static str,
}
impl AdapterSummary {
pub fn of(info: &wgpu::AdapterInfo) -> AdapterSummary {
AdapterSummary {
name: info.name.clone(),
backend: info.backend.to_str(),
device_type: match info.device_type {
DeviceType::DiscreteGpu => "discrete-gpu",
DeviceType::IntegratedGpu => "integrated-gpu",
DeviceType::VirtualGpu => "virtual-gpu",
DeviceType::Cpu => "cpu",
DeviceType::Other => "other",
},
}
}
}
impl GpuRenderer {
pub fn new(device: Device, queue: Queue, width: u32, height: u32) -> GpuRenderer {
let limit = device.limits().max_texture_dimension_2d;
assert!(
width <= limit && height <= limit,
"a {width}x{height} surface exceeds the device's texture limit of {limit}"
);
let renderer = vello::Renderer::new(&device, vello::RendererOptions::default())
.expect("the device cannot create the vello pipelines");
let target = create_target(&device, width, height);
GpuRenderer {
device,
queue,
width,
height,
target,
renderer,
encoder: Encoder::new(),
}
}
pub fn headless(width: u32, height: u32) -> Option<GpuRenderer> {
let instance = Instance::new(InstanceDescriptor::new_without_display_handle_from_env());
let adapter = block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
let (device, queue) =
block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok()?;
Some(GpuRenderer::new(device, queue, width, height))
}
pub fn adapter_summary(&self) -> AdapterSummary {
AdapterSummary::of(&self.device.adapter_info())
}
pub fn read_pixels(&self) -> Vec<u8> {
let Some((texture, _)) = &self.target else {
return Vec::new();
};
let stride = self.stride();
let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("smudge readback"),
size: u64::from(stride) * u64::from(self.height),
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("smudge readback"),
});
encoder.copy_texture_to_buffer(
texture.as_image_copy(),
TexelCopyBufferInfo {
buffer: &buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(stride),
rows_per_image: Some(self.height),
},
},
Extent3d {
width: self.width,
height: self.height,
depth_or_array_layers: 1,
},
);
self.queue.submit([encoder.finish()]);
let (sender, receiver) = std::sync::mpsc::channel();
buffer.slice(..).map_async(MapMode::Read, move |result| {
let _ = sender.send(result);
});
self.device
.poll(PollType::wait_indefinitely())
.expect("the device was lost while reading pixels back");
receiver
.recv()
.expect("the readback callback ran during the poll")
.expect("the readback buffer could not be mapped");
let mapped = buffer
.slice(..)
.get_mapped_range()
.expect("the readback buffer was already mapped");
let mut pixels = Vec::with_capacity(self.width as usize * self.height as usize * 4);
for row in 0..self.height {
let start = row as usize * stride as usize;
pixels.extend_from_slice(&mapped[start..start + self.width as usize * 4]);
}
drop(mapped);
buffer.unmap();
pixels
}
pub fn wait(&self) {
self.device
.poll(PollType::wait_indefinitely())
.expect("the device was lost while waiting for renders");
}
fn stride(&self) -> u32 {
(self.width * 4).next_multiple_of(ROW_ALIGNMENT)
}
}
impl Renderer for GpuRenderer {
fn render(&mut self, frame: &FrameOutput<'_>) {
let Some((_, view)) = &self.target else {
return;
};
let params = RenderParams {
base_color: peniko_color(frame.clear_color),
width: self.width,
height: self.height,
antialiasing_method: vello::AaConfig::Area,
};
let GpuRenderer {
device,
queue,
renderer,
encoder,
..
} = self;
encoder.encode(frame);
renderer
.render_to_texture(device, queue, encoder.scene(), view, ¶ms)
.expect("the device rejected a vello render");
}
}
fn create_target(device: &Device, width: u32, height: u32) -> Option<(Texture, TextureView)> {
if width == 0 || height == 0 {
return None;
}
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::COPY_SRC,
view_formats: &[],
});
let view = texture.create_view(&TextureViewDescriptor::default());
Some((texture, view))
}