use std::collections::HashMap;
use std::sync::Arc;
use vello_common::paint::ImageSource;
use vello_hybrid::{Pixmap, RenderSize, Resources, Scene, WebGlRenderer, WebGlTextureBindings};
use web_sys::{HtmlCanvasElement, WebGl2RenderingContext};
use super::{
dimension, image_key, recorded_images, HybridScene, Pass, Writer, PICK_ALIASING_THRESHOLD,
};
use crate::backend::BackendError;
use crate::color::Color;
use crate::geometry::Affine;
use crate::pick;
pub struct HybridWebGlRenderer {
renderer: WebGlRenderer,
resources: Resources,
scene: HybridScene,
display: Scene,
pick: Option<Scene>,
images: HashMap<u64, ImageSource>,
width: u32,
height: u32,
hitmap: Option<Vec<u32>>,
hitmap_dims: Option<(u32, u32)>,
refresh_pick: bool,
}
impl HybridWebGlRenderer {
pub fn new(
canvas: &HtmlCanvasElement,
width: u32,
height: u32,
picking: bool,
) -> Result<Self, BackendError> {
let (w, h) = (dimension(width)?, dimension(height)?);
let (renderer, resources) = WebGlRenderer::new(canvas);
Ok(Self {
renderer,
resources,
scene: HybridScene::new(),
display: Scene::new(w, h),
pick: picking.then(|| Scene::new(w, h)),
images: HashMap::new(),
width,
height,
hitmap: None,
hitmap_dims: None,
refresh_pick: true,
})
}
pub fn scene(&mut self) -> &mut HybridScene {
&mut self.scene
}
pub fn resize(&mut self, width: u32, height: u32) -> Result<(), BackendError> {
if (width, height) == (self.width, self.height) {
return Ok(());
}
let (w, h) = (dimension(width)?, dimension(height)?);
self.display.reset_and_resize(w, h);
if let Some(pick) = self.pick.as_mut() {
pick.reset_and_resize(w, h);
}
self.images.clear();
self.width = width;
self.height = height;
Ok(())
}
pub fn set_refresh_pick(&mut self, refresh: bool) {
self.refresh_pick = refresh;
}
pub fn refreshes_pick(&self) -> bool {
self.pick.is_some() && self.refresh_pick
}
pub fn pick_at(&self, x: u32, y: u32) -> Option<u32> {
let (w, h) = self.hitmap_dims?;
if x >= w || y >= h {
return None;
}
pick::decode(self.hitmap.as_ref()?[(y * w + x) as usize])
}
pub fn size(&self) -> (u32, u32) {
(self.width, self.height)
}
pub fn hitmap(&self) -> Option<&[u32]> {
self.hitmap.as_deref()
}
pub fn present(&mut self, background: Color) -> Result<(), BackendError> {
self.upload_images();
let refresh = self.refreshes_pick();
self.replay(background, refresh);
let size = RenderSize {
width: self.width,
height: self.height,
};
let bindings = WebGlTextureBindings::new();
if refresh {
let pick = self.pick.as_ref().expect("pick scene present");
self.renderer
.render(pick, &mut self.resources, &size, &bindings)
.map_err(|e| BackendError::Other(format!("hybrid webgl pick render: {e}")))?;
self.read_hitmap();
}
self.renderer
.render(&self.display, &mut self.resources, &size, &bindings)
.map_err(|e| BackendError::Other(format!("hybrid webgl render: {e}")))
}
fn upload_images(&mut self) {
for image in recorded_images(&self.scene.ops) {
let key = image_key(image);
if self.images.contains_key(&key) {
continue;
}
let ImageSource::Pixmap(pixmap) = ImageSource::from_peniko_image_data(image) else {
continue;
};
let transparency = pixmap.may_have_transparency();
let id = self
.renderer
.upload_image::<Arc<Pixmap>>(&mut self.resources, &pixmap);
self.images.insert(
key,
ImageSource::opaque_id_with_transparency_hint(id, transparency),
);
}
}
fn replay(&mut self, background: Color, refresh_pick: bool) {
let frame = crate::geometry::Rect::new(0.0, 0.0, self.width.into(), self.height.into());
self.display.reset();
self.display.set_aliasing_threshold(None);
self.display.set_transform(Affine::IDENTITY);
self.display.set_paint(background);
self.display.fill_rect(&frame);
let mut writer = Writer {
scene: &mut self.display,
resources: &mut self.resources,
pass: Pass::Display,
images: &self.images,
};
self.scene.ops.replay(&mut writer);
if !refresh_pick {
return;
}
let pick = self.pick.as_mut().expect("pick scene present");
pick.reset();
pick.set_aliasing_threshold(Some(PICK_ALIASING_THRESHOLD));
let mut writer = Writer {
scene: pick,
resources: &mut self.resources,
pass: Pass::Pick,
images: &self.images,
};
self.scene.ops.replay(&mut writer);
}
fn read_hitmap(&mut self) {
let (w, h) = (self.width as usize, self.height as usize);
let mut raw = vec![0u8; w * h * 4];
let gl = self.renderer.gl_context();
if gl
.read_pixels_with_opt_u8_array(
0,
0,
self.width as i32,
self.height as i32,
WebGl2RenderingContext::RGBA,
WebGl2RenderingContext::UNSIGNED_BYTE,
Some(&mut raw),
)
.is_err()
{
return;
}
let hitmap = self.hitmap.get_or_insert_with(Vec::new);
hitmap.clear();
hitmap.resize(w * h, 0);
for y in 0..h {
let src = (h - 1 - y) * w * 4;
let dst: &mut [u8] = bytemuck_cast(&mut hitmap[y * w..(y + 1) * w]);
dst.copy_from_slice(&raw[src..src + w * 4]);
}
self.hitmap_dims = Some((self.width, self.height));
}
}
fn bytemuck_cast(row: &mut [u32]) -> &mut [u8] {
unsafe { core::slice::from_raw_parts_mut(row.as_mut_ptr().cast::<u8>(), row.len() * 4) }
}