#![cfg(all(feature = "wgpu", feature = "png"))]
use crate::core::composite::LayeredChart;
use crate::core::conversion::IntoLayered;
use crate::core::layer::{RenderBackend, TextConfig};
use crate::error::ChartonError;
use crate::render::backend::raster::RasterBackend;
use crate::render::backend::wgpu::WgpuBackend;
use std::sync::mpsc;
pub struct WgpuRenderer {
device: wgpu::Device,
queue: wgpu::Queue,
}
impl WgpuRenderer {
pub fn new() -> Self {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
flags: wgpu::InstanceFlags::default(),
backend_options: wgpu::BackendOptions::default(),
memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
display: None,
});
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: None,
force_fallback_adapter: false,
}))
.expect("charton: no suitable GPU adapter found");
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("charton WgpuRenderer"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
..Default::default()
}))
.expect("charton: failed to create wgpu device");
Self { device, queue }
}
pub fn render<C>(
&mut self,
chart: &C,
width: u32,
height: u32,
scale_factor: f32,
) -> Result<Vec<u8>, ChartonError>
where
C: IntoLayered + Clone,
{
let layered: LayeredChart = chart.clone().into();
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("charton render target"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let text_ledger =
pollster::block_on(self.render_geometry(&layered, &view, width, height, scale_factor))?;
let mut pixels = self.readback_pixels(&texture, width, height)?;
if !text_ledger.is_empty() {
self.composite_text(&mut pixels, width, height, scale_factor, text_ledger)?;
}
Ok(pixels)
}
async fn render_geometry(
&mut self,
chart: &LayeredChart,
view: &wgpu::TextureView,
width: u32,
height: u32,
scale_factor: f32,
) -> Result<Vec<TextConfig>, ChartonError> {
let mut backend = WgpuBackend::new(
self.device.clone(),
self.queue.clone(),
width,
height,
scale_factor,
)
.await;
chart.render_to_surface(&mut backend, view).await
}
fn readback_pixels(
&self,
texture: &wgpu::Texture,
width: u32,
height: u32,
) -> Result<Vec<u8>, ChartonError> {
let bytes_per_pixel = 4usize;
let unpadded_bytes = (width as usize) * bytes_per_pixel;
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
let padded_bytes = unpadded_bytes.div_ceil(align) * align;
let buffer_size = (padded_bytes * height as usize) as u64;
let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("charton readback"),
size: buffer_size,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut encoder = self.device.create_command_encoder(&Default::default());
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded_bytes as u32),
rows_per_image: Some(height),
},
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
self.queue.submit(std::iter::once(encoder.finish()));
let slice = buffer.slice(..);
let (tx, rx) = mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |r| {
let _ = tx.send(r);
});
let _ = self.device.poll(wgpu::PollType::Wait {
submission_index: None,
timeout: None,
});
rx.recv()
.map_err(|_| ChartonError::Render("readback channel closed".into()))?
.map_err(|e| ChartonError::Render(format!("buffer map failed: {e:?}")))?;
let data = slice.get_mapped_range();
let mut pixels = Vec::with_capacity(unpadded_bytes * height as usize);
for row in 0..height as usize {
let start = row * padded_bytes;
pixels.extend_from_slice(&data[start..start + unpadded_bytes]);
}
drop(data);
buffer.unmap();
Ok(pixels)
}
fn composite_text(
&self,
rgba: &mut [u8],
width: u32,
height: u32,
scale_factor: f32,
ledger: Vec<TextConfig>,
) -> Result<(), ChartonError> {
let mut pixmap = tiny_skia::Pixmap::from_vec(
rgba.to_vec(),
tiny_skia::IntSize::from_wh(width, height)
.ok_or_else(|| ChartonError::Render("invalid pixmap size".into()))?,
)
.ok_or_else(|| ChartonError::Render("failed to create pixmap".into()))?;
let mut backend = RasterBackend::new(&mut pixmap, scale_factor);
for config in ledger {
backend.draw_text(config);
}
rgba.copy_from_slice(pixmap.data());
Ok(())
}
}
impl Default for WgpuRenderer {
fn default() -> Self {
Self::new()
}
}