| // Copyright 2025 the Vello Authors |
| // SPDX-License-Identifier: Apache-2.0 OR MIT |
| |
| #[cfg(not(all(target_arch = "wasm32", feature = "webgl")))] |
| use std::collections::HashMap; |
| use std::sync::Arc; |
| |
| use glifo::GlyphRunBackend; |
| use vello_common::filter_effects::Filter; |
| use vello_common::kurbo::{Affine, BezPath, Rect, Stroke}; |
| use vello_common::mask::Mask; |
| use vello_common::paint::{ImageId, ImageSource, PaintType, Tint}; |
| use vello_common::peniko::{BlendMode, Fill, FontData, ImageQuality}; |
| use vello_common::pixmap::Pixmap; |
| use vello_cpu::{Level, RasterizerSettings, RenderContext, RenderMode, RenderSettings, Resources}; |
| use vello_hybrid::{ |
| RenderSettings as HybridRenderSettings, Resources as HybridResources, SampleRect, Scene, |
| TextureId, |
| }; |
| #[cfg(all(target_arch = "wasm32", feature = "webgl"))] |
| use web_sys::WebGl2RenderingContext; |
| |
| pub(crate) trait Renderer: Sized { |
| type GlyphRunBackend<'a>: GlyphRunBackend<'a> |
| where |
| Self: 'a; |
| |
| fn new( |
| width: u16, |
| height: u16, |
| num_threads: u16, |
| level: Level, |
| render_mode: RenderMode, |
| ) -> Self; |
| fn fill_path(&mut self, path: &BezPath); |
| fn stroke_path(&mut self, path: &BezPath); |
| fn fill_rect(&mut self, rect: &Rect); |
| fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32, invert: bool); |
| fn stroke_rect(&mut self, rect: &Rect); |
| fn glyph_run( |
| &mut self, |
| font: &FontData, |
| ) -> glifo::GlyphRunBuilder<'_, Self::GlyphRunBackend<'_>>; |
| fn push_layer( |
| &mut self, |
| clip_path: Option<&BezPath>, |
| blend_mode: Option<BlendMode>, |
| opacity: Option<f32>, |
| mask: Option<Mask>, |
| filter: Option<Filter>, |
| ); |
| fn flush(&mut self); |
| fn push_clip_layer(&mut self, path: &BezPath); |
| fn push_clip_path(&mut self, path: &BezPath); |
| fn push_blend_layer(&mut self, blend_mode: BlendMode); |
| fn push_opacity_layer(&mut self, opacity: f32); |
| fn push_mask_layer(&mut self, mask: Mask); |
| fn push_filter_layer(&mut self, filter: Filter); |
| fn pop_layer(&mut self); |
| fn pop_clip_path(&mut self); |
| fn set_stroke(&mut self, stroke: Stroke); |
| fn set_mask(&mut self, mask: Mask); |
| fn set_paint(&mut self, paint: impl Into<PaintType>); |
| fn set_tint(&mut self, tint: Option<Tint>); |
| fn set_paint_transform(&mut self, affine: Affine); |
| fn set_fill_rule(&mut self, fill_rule: Fill); |
| fn set_transform(&mut self, transform: Affine); |
| fn set_aliasing_threshold(&mut self, aliasing_threshold: Option<u8>); |
| fn set_blend_mode(&mut self, blend_mode: BlendMode); |
| fn set_filter_effect(&mut self, filter: Filter); |
| fn reset_filter_effect(&mut self); |
| fn reset(&mut self); |
| fn render_to_pixmap(&mut self, pixmap: &mut Pixmap); |
| fn width(&self) -> u16; |
| fn height(&self) -> u16; |
| #[cfg_attr( |
| all(target_arch = "wasm32", feature = "webgl"), |
| expect( |
| dead_code, |
| reason = "external textures are not wired up for the WebGL backend" |
| ) |
| )] |
| fn register_external_texture(&mut self, pixmap: Arc<Pixmap>) -> TextureId; |
| #[cfg_attr( |
| all(target_arch = "wasm32", feature = "webgl"), |
| expect( |
| dead_code, |
| reason = "external textures are not wired up for the WebGL backend" |
| ) |
| )] |
| fn draw_texture_rects( |
| &mut self, |
| texture_id: TextureId, |
| quality: ImageQuality, |
| rects: impl IntoIterator<Item = SampleRect>, |
| ); |
| fn get_image_source(&mut self, pixmap: Arc<Pixmap>) -> ImageSource; |
| fn register_image(&mut self, pixmap: Arc<Pixmap>) -> ImageId; |
| } |
| |
| pub(crate) struct CpuRenderer { |
| ctx: RenderContext, |
| resources: Resources, |
| render_mode: RenderMode, |
| } |
| |
| impl Renderer for CpuRenderer { |
| type GlyphRunBackend<'a> = vello_cpu::CpuGlyphRunBackend<'a>; |
| |
| fn new( |
| width: u16, |
| height: u16, |
| num_threads: u16, |
| level: Level, |
| render_mode: RenderMode, |
| ) -> Self { |
| let settings = RenderSettings { level, num_threads }; |
| Self { |
| ctx: RenderContext::new_with(width, height, settings), |
| resources: Resources::new(), |
| render_mode, |
| } |
| } |
| |
| fn fill_path(&mut self, path: &BezPath) { |
| self.ctx.fill_path(path); |
| } |
| |
| fn stroke_path(&mut self, path: &BezPath) { |
| self.ctx.stroke_path(path); |
| } |
| |
| fn fill_rect(&mut self, rect: &Rect) { |
| self.ctx.fill_rect(rect); |
| } |
| |
| fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32, invert: bool) { |
| self.ctx |
| .fill_blurred_rounded_rect(rect, radius, std_dev, invert); |
| } |
| |
| fn stroke_rect(&mut self, rect: &Rect) { |
| self.ctx.stroke_rect(rect); |
| } |
| |
| fn glyph_run( |
| &mut self, |
| font: &FontData, |
| ) -> glifo::GlyphRunBuilder<'_, Self::GlyphRunBackend<'_>> { |
| self.ctx.glyph_run(&mut self.resources, font) |
| } |
| |
| fn push_layer( |
| &mut self, |
| clip_path: Option<&BezPath>, |
| blend_mode: Option<BlendMode>, |
| opacity: Option<f32>, |
| mask: Option<Mask>, |
| filter: Option<Filter>, |
| ) { |
| self.ctx |
| .push_layer(clip_path, blend_mode, opacity, mask, filter); |
| } |
| |
| fn flush(&mut self) { |
| self.ctx.flush(); |
| } |
| |
| fn push_clip_layer(&mut self, path: &BezPath) { |
| self.ctx.push_clip_layer(path); |
| } |
| |
| fn push_clip_path(&mut self, path: &BezPath) { |
| self.ctx.push_clip_path(path); |
| } |
| |
| fn push_blend_layer(&mut self, blend_mode: BlendMode) { |
| self.ctx.push_blend_layer(blend_mode); |
| } |
| |
| fn push_opacity_layer(&mut self, opacity: f32) { |
| self.ctx.push_opacity_layer(opacity); |
| } |
| |
| fn push_mask_layer(&mut self, mask: Mask) { |
| self.ctx.push_mask_layer(mask); |
| } |
| |
| fn push_filter_layer(&mut self, filter: Filter) { |
| self.ctx.push_filter_layer(filter); |
| } |
| |
| fn pop_layer(&mut self) { |
| self.ctx.pop_layer(); |
| } |
| |
| fn pop_clip_path(&mut self) { |
| self.ctx.pop_clip_path(); |
| } |
| |
| fn set_stroke(&mut self, stroke: Stroke) { |
| self.ctx.set_stroke(stroke); |
| } |
| |
| fn set_mask(&mut self, mask: Mask) { |
| self.ctx.set_mask(mask); |
| } |
| |
| fn set_paint(&mut self, paint: impl Into<PaintType>) { |
| self.ctx.set_paint(paint); |
| } |
| |
| fn set_tint(&mut self, tint: Option<Tint>) { |
| self.ctx.set_tint(tint); |
| } |
| |
| fn set_paint_transform(&mut self, affine: Affine) { |
| self.ctx.set_paint_transform(affine); |
| } |
| |
| fn set_fill_rule(&mut self, fill_rule: Fill) { |
| self.ctx.set_fill_rule(fill_rule); |
| } |
| |
| fn set_transform(&mut self, transform: Affine) { |
| self.ctx.set_transform(transform); |
| } |
| |
| fn set_aliasing_threshold(&mut self, aliasing_threshold: Option<u8>) { |
| self.ctx.set_aliasing_threshold(aliasing_threshold); |
| } |
| |
| fn set_blend_mode(&mut self, blend_mode: BlendMode) { |
| self.ctx.set_blend_mode(blend_mode); |
| } |
| |
| fn set_filter_effect(&mut self, filter: Filter) { |
| self.ctx.set_filter_effect(filter); |
| } |
| |
| fn reset_filter_effect(&mut self) { |
| self.ctx.reset_filter_effect(); |
| } |
| |
| fn reset(&mut self) { |
| self.ctx.reset(); |
| } |
| |
| fn render_to_pixmap(&mut self, pixmap: &mut Pixmap) { |
| self.ctx.render_with( |
| pixmap, |
| &mut self.resources, |
| RasterizerSettings { |
| render_mode: self.render_mode, |
| ..Default::default() |
| }, |
| ); |
| } |
| |
| fn width(&self) -> u16 { |
| self.ctx.width() |
| } |
| |
| fn height(&self) -> u16 { |
| self.ctx.height() |
| } |
| |
| fn register_external_texture(&mut self, _: Arc<Pixmap>) -> TextureId { |
| unimplemented!("external textures are only supported by hybrid renderer tests") |
| } |
| |
| fn draw_texture_rects( |
| &mut self, |
| _: TextureId, |
| _: ImageQuality, |
| _: impl IntoIterator<Item = SampleRect>, |
| ) { |
| unimplemented!("external textures are only supported by hybrid renderer tests") |
| } |
| |
| fn get_image_source(&mut self, pixmap: Arc<Pixmap>) -> ImageSource { |
| let id = self.resources.register_image(Arc::clone(&pixmap)); |
| ImageSource::opaque_id_with_transparency_hint(id, pixmap.may_have_transparency()) |
| } |
| |
| fn register_image(&mut self, pixmap: Arc<Pixmap>) -> ImageId { |
| self.resources.register_image(pixmap) |
| } |
| } |
| |
| #[cfg(not(all(target_arch = "wasm32", feature = "webgl")))] |
| pub(crate) struct HybridRenderer { |
| scene: Scene, |
| resources: HybridResources, |
| device: wgpu::Device, |
| queue: wgpu::Queue, |
| texture: wgpu::Texture, |
| texture_view: wgpu::TextureView, |
| renderer: vello_hybrid::Renderer, |
| external_textures: HashMap<TextureId, wgpu::TextureView>, |
| #[allow( |
| dead_code, |
| reason = "external texture snapshot scaffolding is not enabled yet" |
| )] |
| next_external_texture_id: u64, |
| } |
| |
| #[cfg(not(all(target_arch = "wasm32", feature = "webgl")))] |
| impl HybridRenderer { |
| fn new_with_settings(width: u16, height: u16, settings: HybridRenderSettings) -> Self { |
| let scene = Scene::new_with(width, height, settings); |
| // Initialize wgpu device and queue for GPU rendering |
| let instance = wgpu::Instance::default(); |
| let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { |
| power_preference: wgpu::PowerPreference::default(), |
| force_fallback_adapter: false, |
| compatible_surface: None, |
| })) |
| .expect("Failed to find an appropriate adapter"); |
| let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor { |
| label: Some("Device"), |
| required_features: wgpu::Features::empty(), |
| ..Default::default() |
| })) |
| .expect("Failed to create device"); |
| |
| // Create a render target texture |
| let texture = device.create_texture(&wgpu::TextureDescriptor { |
| label: Some("Render Target"), |
| size: wgpu::Extent3d { |
| width: width.into(), |
| height: height.into(), |
| 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: &[], |
| }); |
| #[cfg(not(all(target_arch = "wasm32", feature = "webgl")))] |
| let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); |
| |
| // Create renderer and render the scene to the texture |
| let renderer = vello_hybrid::Renderer::new_with( |
| &device, |
| &vello_hybrid::RenderTargetConfig { |
| format: texture.format(), |
| width: width.into(), |
| height: height.into(), |
| }, |
| settings, |
| ); |
| |
| Self { |
| scene, |
| resources: HybridResources::new(), |
| device, |
| queue, |
| texture, |
| texture_view, |
| renderer, |
| external_textures: HashMap::new(), |
| next_external_texture_id: 1, |
| } |
| } |
| |
| fn upload_image_with_resources( |
| &mut self, |
| pixmap: &Arc<Pixmap>, |
| label: &'static str, |
| ) -> ImageId { |
| let mut encoder = self |
| .device |
| .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some(label) }); |
| let image_id = self.renderer.upload_image( |
| &mut self.resources, |
| &self.device, |
| &self.queue, |
| &mut encoder, |
| pixmap, |
| ); |
| self.queue.submit([encoder.finish()]); |
| image_id |
| } |
| } |
| |
| #[cfg(not(all(target_arch = "wasm32", feature = "webgl")))] |
| impl Renderer for HybridRenderer { |
| type GlyphRunBackend<'a> = vello_hybrid::HybridGlyphRunBackend<'a>; |
| |
| fn new(width: u16, height: u16, num_threads: u16, level: Level, _: RenderMode) -> Self { |
| if num_threads != 0 { |
| panic!("hybrid renderer doesn't support multi-threading"); |
| } |
| if !level.is_fallback() { |
| panic!("hybrid renderer doesn't support SIMD"); |
| } |
| let mut settings = HybridRenderSettings::default(); |
| // Most of the tests are 100x100 by default, and we want to make sure that some visual |
| // tests have the chance to cover more complex parts of the Vello Hybrid scheduler |
| // (for example situations where we need to spill to a new page, etc.). Therefore, |
| // we make the minimum size smaller than the default. |
| settings.memory_settings.layers_config.min_texture_size = vello_hybrid::SizeU16::new(100); |
| Self::new_with_settings(width, height, settings) |
| } |
| |
| fn fill_path(&mut self, path: &BezPath) { |
| self.scene.fill_path(path); |
| } |
| |
| fn stroke_path(&mut self, path: &BezPath) { |
| self.scene.stroke_path(path); |
| } |
| |
| fn fill_rect(&mut self, rect: &Rect) { |
| self.scene.fill_rect(rect); |
| } |
| |
| fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32, invert: bool) { |
| self.scene |
| .fill_blurred_rounded_rect(rect, radius, std_dev, invert); |
| } |
| |
| fn stroke_rect(&mut self, rect: &Rect) { |
| self.scene.stroke_rect(rect); |
| } |
| |
| fn glyph_run( |
| &mut self, |
| font: &FontData, |
| ) -> glifo::GlyphRunBuilder<'_, Self::GlyphRunBackend<'_>> { |
| self.scene.glyph_run(&mut self.resources, font) |
| } |
| |
| fn push_layer( |
| &mut self, |
| clip: Option<&BezPath>, |
| blend_mode: Option<BlendMode>, |
| opacity: Option<f32>, |
| mask: Option<Mask>, |
| filter: Option<Filter>, |
| ) { |
| self.scene |
| .push_layer(clip, blend_mode, opacity, mask, filter); |
| } |
| |
| fn flush(&mut self) {} |
| |
| fn push_clip_layer(&mut self, path: &BezPath) { |
| self.scene.push_clip_layer(path); |
| } |
| |
| fn push_clip_path(&mut self, path: &BezPath) { |
| self.scene.push_clip_path(path); |
| } |
| |
| fn push_blend_layer(&mut self, blend_mode: BlendMode) { |
| self.scene |
| .push_layer(None, Some(blend_mode), None, None, None); |
| } |
| |
| fn push_opacity_layer(&mut self, opacity: f32) { |
| self.scene.push_layer(None, None, Some(opacity), None, None); |
| } |
| |
| fn push_mask_layer(&mut self, mask: Mask) { |
| self.scene.push_mask_layer(mask); |
| } |
| |
| fn push_filter_layer(&mut self, filter: Filter) { |
| self.scene.push_filter_layer(filter); |
| } |
| |
| fn pop_layer(&mut self) { |
| self.scene.pop_layer(); |
| } |
| |
| fn pop_clip_path(&mut self) { |
| self.scene.pop_clip_path(); |
| } |
| |
| fn set_stroke(&mut self, stroke: Stroke) { |
| self.scene.set_stroke(stroke); |
| } |
| |
| fn set_mask(&mut self, _: Mask) { |
| unimplemented!() |
| } |
| |
| fn set_paint(&mut self, paint: impl Into<PaintType>) { |
| self.scene.set_paint(paint); |
| } |
| |
| fn set_tint(&mut self, tint: Option<Tint>) { |
| self.scene.set_tint(tint); |
| } |
| |
| fn set_paint_transform(&mut self, affine: Affine) { |
| self.scene.set_paint_transform(affine); |
| } |
| |
| fn set_fill_rule(&mut self, fill_rule: Fill) { |
| self.scene.set_fill_rule(fill_rule); |
| } |
| |
| fn set_transform(&mut self, transform: Affine) { |
| self.scene.set_transform(transform); |
| } |
| |
| fn set_blend_mode(&mut self, blend_mode: BlendMode) { |
| self.scene.set_blend_mode(blend_mode); |
| } |
| |
| fn set_aliasing_threshold(&mut self, aliasing_threshold: Option<u8>) { |
| self.scene.set_aliasing_threshold(aliasing_threshold); |
| } |
| |
| fn set_filter_effect(&mut self, filter: Filter) { |
| self.scene.set_filter_effect(filter); |
| } |
| |
| fn reset_filter_effect(&mut self) { |
| self.scene.reset_filter_effect(); |
| } |
| |
| fn reset(&mut self) { |
| self.scene.reset(); |
| } |
| |
| // This method creates device resources every time it is called. This does not matter much for |
| // testing, but should not be used as a basis for implementing something real. This would be a |
| // very bad example for that. |
| fn render_to_pixmap(&mut self, pixmap: &mut Pixmap) { |
| // On some platforms using `cargo test` triggers segmentation faults in wgpu when the GPU |
| // tests are run in parallel (likely related to the number of device resources being |
| // requested simultaneously). This is "fixed" by putting a mutex around this method, |
| // ensuring only one set of device resources is alive at the same time. This slows down |
| // testing when `cargo test` is used. |
| // |
| // Testing with `cargo nextest` (as on CI) is not meaningfully slowed down. `nextest` runs |
| // each test in its own process (<https://nexte.st/docs/design/why-process-per-test/>), |
| // meaning there is no contention on this mutex. |
| let _guard = { |
| use std::sync::Mutex; |
| static M: Mutex<()> = Mutex::new(()); |
| M.lock().unwrap() |
| }; |
| |
| let width = self.scene.width(); |
| let height = self.scene.height(); |
| |
| let render_size = vello_hybrid::RenderSize { |
| width: width.into(), |
| height: height.into(), |
| }; |
| |
| let mut texture_bindings = vello_hybrid::TextureBindings::new(); |
| for (texture_id, texture) in &self.external_textures { |
| texture_bindings.insert(*texture_id, texture.clone()); |
| } |
| // Copy texture to buffer |
| let mut encoder = self |
| .device |
| .create_command_encoder(&wgpu::CommandEncoderDescriptor { |
| label: Some("Vello Render To Buffer"), |
| }); |
| self.renderer |
| .render( |
| &self.scene, |
| &mut self.resources, |
| &self.device, |
| &self.queue, |
| &mut encoder, |
| &render_size, |
| &self.texture_view, |
| &texture_bindings, |
| ) |
| .unwrap(); |
| |
| // Create a buffer to copy the texture data |
| let bytes_per_row = (u32::from(width) * 4).next_multiple_of(256); |
| let texture_copy_buffer = self.device.create_buffer(&wgpu::BufferDescriptor { |
| label: Some("Output Buffer"), |
| size: u64::from(bytes_per_row) * u64::from(height), |
| usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, |
| mapped_at_creation: false, |
| }); |
| |
| encoder.copy_texture_to_buffer( |
| wgpu::TexelCopyTextureInfo { |
| texture: &self.texture, |
| mip_level: 0, |
| origin: wgpu::Origin3d::ZERO, |
| aspect: wgpu::TextureAspect::All, |
| }, |
| wgpu::TexelCopyBufferInfo { |
| buffer: &texture_copy_buffer, |
| layout: wgpu::TexelCopyBufferLayout { |
| offset: 0, |
| bytes_per_row: Some(bytes_per_row), |
| rows_per_image: None, |
| }, |
| }, |
| wgpu::Extent3d { |
| width: width.into(), |
| height: height.into(), |
| depth_or_array_layers: 1, |
| }, |
| ); |
| |
| self.queue.submit([encoder.finish()]); |
| |
| // Map the buffer for reading |
| texture_copy_buffer |
| .slice(..) |
| .map_async(wgpu::MapMode::Read, move |result| { |
| if result.is_err() { |
| panic!("Failed to map texture for reading"); |
| } |
| }); |
| self.device |
| .poll(wgpu::PollType::wait_indefinitely()) |
| .unwrap(); |
| |
| // Read back the pixel data |
| for (row, buf) in texture_copy_buffer |
| .slice(..) |
| .get_mapped_range() |
| .chunks_exact(bytes_per_row as usize) |
| .zip( |
| pixmap |
| .data_as_u8_slice_mut() |
| .chunks_exact_mut(width as usize * 4), |
| ) |
| { |
| buf.copy_from_slice(&row[0..width as usize * 4]); |
| } |
| texture_copy_buffer.unmap(); |
| } |
| |
| fn width(&self) -> u16 { |
| self.scene.width() |
| } |
| |
| fn height(&self) -> u16 { |
| self.scene.height() |
| } |
| |
| fn register_external_texture(&mut self, pixmap: Arc<Pixmap>) -> TextureId { |
| let texture_id = TextureId(self.next_external_texture_id); |
| self.next_external_texture_id += 1; |
| |
| let width = u32::from(pixmap.width()); |
| let height = u32::from(pixmap.height()); |
| let texture = self.device.create_texture(&wgpu::TextureDescriptor { |
| label: Some("Test External Texture"), |
| 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::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, |
| view_formats: &[], |
| }); |
| self.queue.write_texture( |
| wgpu::TexelCopyTextureInfo { |
| texture: &texture, |
| mip_level: 0, |
| origin: wgpu::Origin3d::ZERO, |
| aspect: wgpu::TextureAspect::All, |
| }, |
| pixmap.data_as_u8_slice(), |
| wgpu::TexelCopyBufferLayout { |
| offset: 0, |
| bytes_per_row: Some(width * 4), |
| rows_per_image: None, |
| }, |
| wgpu::Extent3d { |
| width, |
| height, |
| depth_or_array_layers: 1, |
| }, |
| ); |
| let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); |
| self.external_textures.insert(texture_id, view); |
| texture_id |
| } |
| |
| fn draw_texture_rects( |
| &mut self, |
| texture_id: TextureId, |
| quality: ImageQuality, |
| rects: impl IntoIterator<Item = SampleRect>, |
| ) { |
| self.scene.draw_texture_rects(texture_id, quality, rects); |
| } |
| |
| fn get_image_source(&mut self, pixmap: Arc<Pixmap>) -> ImageSource { |
| let image_id = self.upload_image_with_resources(&pixmap, "Upload Test Image"); |
| ImageSource::opaque_id_with_transparency_hint(image_id, pixmap.may_have_transparency()) |
| } |
| |
| fn register_image(&mut self, pixmap: Arc<Pixmap>) -> ImageId { |
| self.upload_image_with_resources(&pixmap, "Register Test Image") |
| } |
| } |
| |
| #[cfg(all(target_arch = "wasm32", feature = "webgl"))] |
| pub(crate) struct HybridRenderer { |
| scene: Scene, |
| resources: HybridResources, |
| renderer: vello_hybrid::WebGlRenderer, |
| gl: WebGl2RenderingContext, |
| } |
| |
| #[cfg(all(target_arch = "wasm32", feature = "webgl"))] |
| impl HybridRenderer { |
| fn upload_image(&mut self, pixmap: &Arc<Pixmap>) -> ImageId { |
| self.renderer.upload_image(&mut self.resources, pixmap) |
| } |
| } |
| |
| #[cfg(all(target_arch = "wasm32", feature = "webgl"))] |
| impl Renderer for HybridRenderer { |
| type GlyphRunBackend<'a> = vello_hybrid::HybridGlyphRunBackend<'a>; |
| |
| fn new(width: u16, height: u16, num_threads: u16, level: Level, _: RenderMode) -> Self { |
| use wasm_bindgen::JsCast; |
| use web_sys::HtmlCanvasElement; |
| |
| if num_threads != 0 { |
| panic!("hybrid renderer doesn't support multi-threading"); |
| } |
| |
| if !level.is_fallback() { |
| panic!("hybrid renderer doesn't support SIMD"); |
| } |
| |
| let mut settings = HybridRenderSettings::default(); |
| // See the comment above for why we change the `min_texture_size`. |
| settings.memory_settings.layers_config.min_texture_size = vello_hybrid::SizeU16::new(100); |
| let scene = Scene::new_with(width, height, settings); |
| // Create an offscreen HTMLCanvasElement, render the test image to it, and finally read off |
| // the pixmap for diff checking. |
| let document = web_sys::window().unwrap().document().unwrap(); |
| let canvas = document |
| .create_element("canvas") |
| .unwrap() |
| .dyn_into::<HtmlCanvasElement>() |
| .unwrap(); |
| canvas.set_width(width.into()); |
| canvas.set_height(height.into()); |
| let renderer = vello_hybrid::WebGlRenderer::new_with(&canvas, settings); |
| let gl = canvas |
| .get_context("webgl2") |
| .unwrap() |
| .unwrap() |
| .dyn_into::<WebGl2RenderingContext>() |
| .unwrap(); |
| Self { |
| scene, |
| resources: HybridResources::new(), |
| renderer, |
| gl, |
| } |
| } |
| |
| fn fill_path(&mut self, path: &BezPath) { |
| self.scene.fill_path(path); |
| } |
| |
| fn set_blend_mode(&mut self, blend_mode: BlendMode) { |
| self.scene.set_blend_mode(blend_mode); |
| } |
| |
| fn stroke_path(&mut self, path: &BezPath) { |
| self.scene.stroke_path(path); |
| } |
| |
| fn fill_rect(&mut self, rect: &Rect) { |
| self.scene.fill_rect(rect); |
| } |
| |
| fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32, invert: bool) { |
| self.scene |
| .fill_blurred_rounded_rect(rect, radius, std_dev, invert); |
| } |
| |
| fn stroke_rect(&mut self, rect: &Rect) { |
| self.scene.stroke_rect(rect); |
| } |
| |
| fn glyph_run( |
| &mut self, |
| font: &FontData, |
| ) -> glifo::GlyphRunBuilder<'_, Self::GlyphRunBackend<'_>> { |
| self.scene.glyph_run(&mut self.resources, font) |
| } |
| |
| fn push_clip_path(&mut self, path: &BezPath) { |
| self.scene.push_clip_path(path); |
| } |
| |
| fn push_layer( |
| &mut self, |
| clip: Option<&BezPath>, |
| blend_mode: Option<BlendMode>, |
| opacity: Option<f32>, |
| mask: Option<Mask>, |
| filter: Option<Filter>, |
| ) { |
| self.scene |
| .push_layer(clip, blend_mode, opacity, mask, filter); |
| } |
| |
| fn flush(&mut self) {} |
| |
| fn push_clip_layer(&mut self, path: &BezPath) { |
| self.scene.push_clip_layer(path); |
| } |
| |
| fn push_blend_layer(&mut self, mode: BlendMode) { |
| self.scene.push_layer(None, Some(mode), None, None, None); |
| } |
| |
| fn push_opacity_layer(&mut self, opacity: f32) { |
| self.scene.push_layer(None, None, Some(opacity), None, None); |
| } |
| |
| fn push_mask_layer(&mut self, _: Mask) { |
| unimplemented!() |
| } |
| |
| fn push_filter_layer(&mut self, filter: Filter) { |
| self.scene.push_filter_layer(filter); |
| } |
| |
| fn pop_layer(&mut self) { |
| self.scene.pop_layer(); |
| } |
| |
| fn pop_clip_path(&mut self) { |
| self.scene.pop_clip_path(); |
| } |
| |
| fn set_stroke(&mut self, stroke: Stroke) { |
| self.scene.set_stroke(stroke); |
| } |
| |
| fn set_mask(&mut self, _: Mask) { |
| unimplemented!() |
| } |
| |
| fn set_paint(&mut self, paint: impl Into<PaintType>) { |
| self.scene.set_paint(paint); |
| } |
| |
| fn set_tint(&mut self, tint: Option<Tint>) { |
| self.scene.set_tint(tint); |
| } |
| |
| fn set_paint_transform(&mut self, affine: Affine) { |
| self.scene.set_paint_transform(affine); |
| } |
| |
| fn set_fill_rule(&mut self, fill_rule: Fill) { |
| self.scene.set_fill_rule(fill_rule); |
| } |
| |
| fn set_transform(&mut self, transform: Affine) { |
| self.scene.set_transform(transform); |
| } |
| |
| fn set_aliasing_threshold(&mut self, aliasing_threshold: Option<u8>) { |
| self.scene.set_aliasing_threshold(aliasing_threshold); |
| } |
| |
| fn set_filter_effect(&mut self, filter: Filter) { |
| self.scene.set_filter_effect(filter); |
| } |
| |
| fn reset_filter_effect(&mut self) { |
| self.scene.reset_filter_effect(); |
| } |
| |
| fn reset(&mut self) { |
| self.scene.reset(); |
| } |
| |
| // vello_hybrid WebGL renderer backend. |
| fn render_to_pixmap(&mut self, pixmap: &mut Pixmap) { |
| use web_sys::WebGl2RenderingContext; |
| |
| let width = self.scene.width(); |
| let height = self.scene.height(); |
| |
| let render_size = vello_hybrid::RenderSize { |
| width: width.into(), |
| height: height.into(), |
| }; |
| self.renderer |
| .render(&self.scene, &mut self.resources, &render_size) |
| .unwrap(); |
| let mut pixels = vec![0_u8; (width as usize) * (height as usize) * 4]; |
| self.gl |
| .read_pixels_with_opt_u8_array( |
| 0, |
| 0, |
| width.into(), |
| height.into(), |
| WebGl2RenderingContext::RGBA, |
| WebGl2RenderingContext::UNSIGNED_BYTE, |
| Some(&mut pixels), |
| ) |
| .unwrap(); |
| |
| // WebGL framebuffers are y-up, so we need to invert the rows. |
| let row_bytes = width as usize * 4; |
| let height = height as usize; |
| for y in 0..height / 2 { |
| let (a, b) = pixels.split_at_mut((height - 1 - y) * row_bytes); |
| a[y * row_bytes..(y + 1) * row_bytes].swap_with_slice(&mut b[..row_bytes]); |
| } |
| |
| let pixmap_data = pixmap.data_as_u8_slice_mut(); |
| pixmap_data.copy_from_slice(&pixels); |
| } |
| |
| fn width(&self) -> u16 { |
| self.scene.width() |
| } |
| |
| fn height(&self) -> u16 { |
| self.scene.height() |
| } |
| |
| fn register_external_texture(&mut self, _: Arc<Pixmap>) -> TextureId { |
| unimplemented!("external textures are not wired up for the WebGL test backend") |
| } |
| |
| fn draw_texture_rects( |
| &mut self, |
| _: TextureId, |
| _: ImageQuality, |
| _: impl IntoIterator<Item = SampleRect>, |
| ) { |
| unimplemented!("external textures are not wired up for the WebGL test backend") |
| } |
| |
| fn get_image_source(&mut self, pixmap: Arc<Pixmap>) -> ImageSource { |
| let image_id = self.upload_image(&pixmap); |
| ImageSource::opaque_id_with_transparency_hint(image_id, pixmap.may_have_transparency()) |
| } |
| |
| fn register_image(&mut self, pixmap: Arc<Pixmap>) -> ImageId { |
| self.upload_image(&pixmap) |
| } |
| } |