| // 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::color::{AlphaColor, Srgb}; | |
| 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}; | |
| use vello_common::pixmap::Pixmap; | |
| use vello_cpu::{ | |
| Level, RasterizerSettings, RenderContext, RenderMode, RenderSettings, Resources, | |
| TargetInit as CpuTargetInit, | |
| }; | |
| use vello_gpu::{ | |
| ClearSettings, RectU16, RenderSettings as GpuRenderSettings, Resources as GpuResources, Scene, | |
| TargetInit as GpuTargetInit, TextureId, | |
| }; | |
| #[cfg(all(target_arch = "wasm32", feature = "webgl"))] | |
| use web_sys::WebGl2RenderingContext; | |
| pub 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 new_with_depth_buffer( | |
| width: u16, | |
| height: u16, | |
| num_threads: u16, | |
| level: Level, | |
| render_mode: RenderMode, | |
| use_depth_buffer: bool, | |
| ) -> Self { | |
| assert!( | |
| use_depth_buffer, | |
| "this test renderer does not support disabling the depth buffer" | |
| ); | |
| Self::new(width, height, num_threads, level, render_mode) | |
| } | |
| 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_clip_rect(&mut self, rect: &Rect); | |
| 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(&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 set_target_init(&mut self, target_init: GpuTargetInit<'static>); | |
| fn render(&mut self); | |
| fn snapshot(&mut self) -> Pixmap; | |
| fn register_external_texture(&mut self, pixmap: Arc<Pixmap>) -> TextureId; | |
| fn get_image_source(&mut self, pixmap: Arc<Pixmap>) -> ImageSource; | |
| fn register_image(&mut self, pixmap: Arc<Pixmap>) -> ImageId; | |
| } | |
| pub struct CpuRenderer { | |
| ctx: RenderContext, | |
| resources: Resources, | |
| render_mode: RenderMode, | |
| target: Pixmap, | |
| target_init: GpuTargetInit<'static>, | |
| } | |
| 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, | |
| target: Pixmap::new(width, height), | |
| target_init: GpuTargetInit::Clear(ClearSettings::default()), | |
| } | |
| } | |
| 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_clip_rect(&mut self, rect: &Rect) { | |
| self.ctx.push_clip_rect(rect); | |
| } | |
| 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(&mut self) { | |
| self.ctx.pop_clip(); | |
| } | |
| 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 set_target_init(&mut self, target_init: GpuTargetInit<'static>) { | |
| self.target_init = target_init; | |
| } | |
| fn render(&mut self) { | |
| let target_init = match self.target_init { | |
| GpuTargetInit::SrcOver => CpuTargetInit::SrcOver, | |
| GpuTargetInit::Clear(ClearSettings::Viewport { color }) => CpuTargetInit::Clear(color), | |
| GpuTargetInit::Clear(ClearSettings::Rects { color, rects }) => { | |
| apply_rect_clear(&mut self.target, color, rects); | |
| CpuTargetInit::SrcOver | |
| } | |
| }; | |
| self.ctx.render_with( | |
| &mut self.target, | |
| &mut self.resources, | |
| RasterizerSettings { | |
| render_mode: self.render_mode, | |
| target_init, | |
| ..Default::default() | |
| }, | |
| ); | |
| } | |
| fn snapshot(&mut self) -> Pixmap { | |
| self.target.clone() | |
| } | |
| fn register_external_texture(&mut self, _: Arc<Pixmap>) -> TextureId { | |
| unimplemented!("external textures are only supported by GPU 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")))] | |
| static WGPU_TEST_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(()); | |
| #[cfg(not(all(target_arch = "wasm32", feature = "webgl")))] | |
| fn create_wgpu_device_queue() -> (wgpu::Device, wgpu::Queue) { | |
| 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, | |
| ..Default::default() | |
| })) | |
| .expect("Failed to find an appropriate adapter"); | |
| pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor { | |
| label: Some("Device"), | |
| required_features: wgpu::Features::empty(), | |
| ..Default::default() | |
| })) | |
| .expect("Failed to create device") | |
| } | |
| #[cfg(not(target_arch = "wasm32"))] | |
| static WGPU_DEVICE_QUEUE: std::sync::LazyLock<(wgpu::Device, wgpu::Queue)> = | |
| std::sync::LazyLock::new(create_wgpu_device_queue); | |
| #[cfg(not(all(target_arch = "wasm32", feature = "webgl")))] | |
| pub struct GpuRenderer { | |
| scene: Scene, | |
| resources: GpuResources, | |
| device: wgpu::Device, | |
| queue: wgpu::Queue, | |
| texture: wgpu::Texture, | |
| texture_view: wgpu::TextureView, | |
| depth_texture_view: Option<wgpu::TextureView>, | |
| renderer: vello_gpu::Renderer, | |
| external_textures: HashMap<TextureId, wgpu::TextureView>, | |
| next_external_texture_id: u64, | |
| target_init: GpuTargetInit<'static>, | |
| gpu_test_guard: Option<std::sync::MutexGuard<'static, ()>>, | |
| } | |
| #[cfg(not(all(target_arch = "wasm32", feature = "webgl")))] | |
| impl GpuRenderer { | |
| fn new_with_settings( | |
| width: u16, | |
| height: u16, | |
| settings: GpuRenderSettings, | |
| use_depth_buffer: bool, | |
| ) -> Self { | |
| let scene = Scene::new_with(width, height, settings.level); | |
| #[cfg(not(target_arch = "wasm32"))] | |
| let (device, queue) = &*WGPU_DEVICE_QUEUE; | |
| #[cfg(not(target_arch = "wasm32"))] | |
| let device = device.clone(); | |
| #[cfg(not(target_arch = "wasm32"))] | |
| let queue = queue.clone(); | |
| #[cfg(target_arch = "wasm32")] | |
| let (device, queue) = create_wgpu_device_queue(); | |
| // 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, resources) = vello_gpu::Renderer::new_with( | |
| &device, | |
| &vello_gpu::RenderTargetConfig { | |
| format: texture.format(), | |
| width, | |
| height, | |
| }, | |
| settings, | |
| ); | |
| let render_size = vello_gpu::RenderSize { width, height }; | |
| let depth_texture_view = use_depth_buffer | |
| .then(|| vello_gpu::Renderer::create_depth_texture_view(&device, &render_size)); | |
| Self { | |
| scene, | |
| resources, | |
| device, | |
| queue, | |
| texture, | |
| texture_view, | |
| depth_texture_view, | |
| renderer, | |
| external_textures: HashMap::new(), | |
| next_external_texture_id: 1, | |
| target_init: GpuTargetInit::Clear(ClearSettings::default()), | |
| gpu_test_guard: None, | |
| } | |
| } | |
| fn lock_gpu_test(&mut self) { | |
| if self.gpu_test_guard.is_none() { | |
| self.gpu_test_guard = Some(WGPU_TEST_MUTEX.lock().unwrap()); | |
| } | |
| } | |
| 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 GpuRenderer { | |
| type GlyphRunBackend<'a> = vello_gpu::GpuGlyphRunBackend<'a>; | |
| fn new(width: u16, height: u16, num_threads: u16, level: Level, _: RenderMode) -> Self { | |
| Self::new_with_depth_buffer( | |
| width, | |
| height, | |
| num_threads, | |
| level, | |
| RenderMode::OptimizeSpeed, | |
| true, | |
| ) | |
| } | |
| fn new_with_depth_buffer( | |
| width: u16, | |
| height: u16, | |
| num_threads: u16, | |
| _level: Level, | |
| _: RenderMode, | |
| use_depth_buffer: bool, | |
| ) -> Self { | |
| if num_threads != 0 { | |
| panic!("GPU renderer doesn't support multi-threading"); | |
| } | |
| let mut settings = GpuRenderSettings::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 GPU 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_gpu::SizeU16::new(100); | |
| Self::new_with_settings(width, height, settings, use_depth_buffer) | |
| } | |
| 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_clip_rect(&mut self, rect: &Rect) { | |
| self.scene.push_clip_rect(rect); | |
| } | |
| 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(&mut self) { | |
| self.scene.pop_clip(); | |
| } | |
| 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(); | |
| } | |
| fn set_target_init(&mut self, target_init: GpuTargetInit<'static>) { | |
| self.target_init = target_init; | |
| } | |
| fn render(&mut self) { | |
| // 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 GPU rendering and | |
| // readback. CI uses `cargo test`, so this intentionally trades some throughput for | |
| // stability by serializing the GPU portion of otherwise parallel tests. | |
| self.lock_gpu_test(); | |
| let width = self.scene.width(); | |
| let height = self.scene.height(); | |
| let render_size = vello_gpu::RenderSize { width, height }; | |
| let mut texture_bindings = vello_gpu::TextureBindings::new(); | |
| for (texture_id, texture) in &self.external_textures { | |
| texture_bindings.insert(*texture_id, texture.clone()); | |
| } | |
| let mut encoder = self | |
| .device | |
| .create_command_encoder(&wgpu::CommandEncoderDescriptor { | |
| label: Some("Vello Render"), | |
| }); | |
| self.renderer | |
| .render( | |
| &self.scene, | |
| &mut self.resources, | |
| &self.device, | |
| &self.queue, | |
| &mut encoder, | |
| &render_size, | |
| &self.texture_view, | |
| self.depth_texture_view.as_ref(), | |
| &texture_bindings, | |
| self.target_init, | |
| ) | |
| .unwrap(); | |
| self.queue.submit([encoder.finish()]); | |
| } | |
| // 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 snapshot(&mut self) -> Pixmap { | |
| self.lock_gpu_test(); | |
| let width = self.scene.width(); | |
| let height = self.scene.height(); | |
| let mut encoder = self | |
| .device | |
| .create_command_encoder(&wgpu::CommandEncoderDescriptor { | |
| label: Some("Vello Readback"), | |
| }); | |
| // 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 | |
| let mut pixmap = Pixmap::new(width, height); | |
| for (row, buf) in texture_copy_buffer | |
| .slice(..) | |
| .get_mapped_range() | |
| .unwrap() | |
| .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(); | |
| drop(texture_copy_buffer); | |
| self.gpu_test_guard = None; | |
| pixmap | |
| } | |
| 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 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 struct GpuRenderer { | |
| scene: Scene, | |
| resources: GpuResources, | |
| renderer: vello_gpu::WebGlRenderer, | |
| gl: WebGl2RenderingContext, | |
| external_textures: vello_gpu::WebGlTextureBindings, | |
| next_external_texture_id: u64, | |
| clear_color: AlphaColor<Srgb>, | |
| } | |
| #[cfg(all(target_arch = "wasm32", feature = "webgl"))] | |
| impl GpuRenderer { | |
| fn upload_image(&mut self, pixmap: &Arc<Pixmap>) -> ImageId { | |
| self.renderer | |
| .upload_image(&mut self.resources, pixmap) | |
| .unwrap() | |
| } | |
| } | |
| #[cfg(all(target_arch = "wasm32", feature = "webgl"))] | |
| impl Renderer for GpuRenderer { | |
| type GlyphRunBackend<'a> = vello_gpu::GpuGlyphRunBackend<'a>; | |
| fn new(width: u16, height: u16, num_threads: u16, level: Level, _: RenderMode) -> Self { | |
| Self::new_with_depth_buffer( | |
| width, | |
| height, | |
| num_threads, | |
| level, | |
| RenderMode::OptimizeSpeed, | |
| true, | |
| ) | |
| } | |
| fn new_with_depth_buffer( | |
| width: u16, | |
| height: u16, | |
| num_threads: u16, | |
| _level: Level, | |
| _: RenderMode, | |
| use_depth_buffer: bool, | |
| ) -> Self { | |
| use wasm_bindgen::JsCast; | |
| use web_sys::HtmlCanvasElement; | |
| if num_threads != 0 { | |
| panic!("GPU renderer doesn't support multi-threading"); | |
| } | |
| let mut settings = GpuRenderSettings::default(); | |
| // See the comment above for why we change the `min_texture_size`. | |
| settings.memory_settings.layers_config.min_texture_size = vello_gpu::SizeU16::new(100); | |
| let scene = Scene::new_with(width, height, settings.level); | |
| // 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, resources) = | |
| vello_gpu::WebGlRenderer::new_with(&canvas, settings, use_depth_buffer).unwrap(); | |
| let gl = canvas | |
| .get_context("webgl2") | |
| .unwrap() | |
| .unwrap() | |
| .dyn_into::<WebGl2RenderingContext>() | |
| .unwrap(); | |
| Self { | |
| scene, | |
| resources, | |
| renderer, | |
| gl, | |
| external_textures: vello_gpu::WebGlTextureBindings::new(), | |
| next_external_texture_id: 1, | |
| clear_color: AlphaColor::TRANSPARENT, | |
| } | |
| } | |
| 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_clip_rect(&mut self, rect: &Rect) { | |
| self.scene.push_clip_rect(rect); | |
| } | |
| 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(&mut self) { | |
| self.scene.pop_clip(); | |
| } | |
| 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(); | |
| } | |
| fn set_target_init(&mut self, target_init: GpuTargetInit<'static>) { | |
| self.clear_color = match target_init { | |
| GpuTargetInit::Clear(ClearSettings::Viewport { color }) => color, | |
| GpuTargetInit::SrcOver | GpuTargetInit::Clear(ClearSettings::Rects { .. }) => { | |
| panic!("WebGL only supports clearing the complete viewport") | |
| } | |
| }; | |
| } | |
| fn render(&mut self) { | |
| let width = self.scene.width(); | |
| let height = self.scene.height(); | |
| let render_size = vello_gpu::RenderSize { width, height }; | |
| self.renderer | |
| .render( | |
| &self.scene, | |
| &mut self.resources, | |
| &render_size, | |
| &self.external_textures, | |
| self.clear_color, | |
| ) | |
| .unwrap(); | |
| } | |
| fn snapshot(&mut self) -> Pixmap { | |
| use vello_common::peniko::ImageAlphaType; | |
| use vello_common::pixmap::PixelMetadata; | |
| use web_sys::WebGl2RenderingContext; | |
| let width = self.scene.width(); | |
| let height = self.scene.height(); | |
| 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 = usize::from(width) * 4; | |
| let height_usize = usize::from(height); | |
| for y in 0..height_usize / 2 { | |
| let (a, b) = pixels.split_at_mut((height_usize - 1 - y) * row_bytes); | |
| a[y * row_bytes..(y + 1) * row_bytes].swap_with_slice(&mut b[..row_bytes]); | |
| } | |
| Pixmap::from_parts( | |
| pixels, | |
| width, | |
| height, | |
| PixelMetadata::new(ImageAlphaType::AlphaPremultiplied, true), | |
| ) | |
| } | |
| 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 texture = self.gl.create_texture().unwrap(); | |
| self.gl | |
| .bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&texture)); | |
| self.gl | |
| .tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( | |
| WebGl2RenderingContext::TEXTURE_2D, | |
| 0, | |
| WebGl2RenderingContext::RGBA8 as i32, | |
| pixmap.width().into(), | |
| pixmap.height().into(), | |
| 0, | |
| WebGl2RenderingContext::RGBA, | |
| WebGl2RenderingContext::UNSIGNED_BYTE, | |
| Some(pixmap.data_as_u8_slice()), | |
| ) | |
| .unwrap(); | |
| // `texelFetch` requires a complete texture, which a texture without mipmaps only is once | |
| // its minification filter no longer samples mipmaps. | |
| for (param, value) in [ | |
| ( | |
| WebGl2RenderingContext::TEXTURE_MIN_FILTER, | |
| WebGl2RenderingContext::NEAREST, | |
| ), | |
| ( | |
| WebGl2RenderingContext::TEXTURE_MAG_FILTER, | |
| WebGl2RenderingContext::NEAREST, | |
| ), | |
| ( | |
| WebGl2RenderingContext::TEXTURE_WRAP_S, | |
| WebGl2RenderingContext::CLAMP_TO_EDGE, | |
| ), | |
| ( | |
| WebGl2RenderingContext::TEXTURE_WRAP_T, | |
| WebGl2RenderingContext::CLAMP_TO_EDGE, | |
| ), | |
| ] { | |
| self.gl | |
| .tex_parameteri(WebGl2RenderingContext::TEXTURE_2D, param, value as i32); | |
| } | |
| self.gl | |
| .bind_texture(WebGl2RenderingContext::TEXTURE_2D, None); | |
| self.external_textures.insert(texture_id, texture); | |
| texture_id | |
| } | |
| 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) | |
| } | |
| } | |
| // Vello CPU does not expose rectangle clears (because it would be pretty inefficient), | |
| // so we simulate them here for the snapshot tests. | |
| fn apply_rect_clear(pixmap: &mut Pixmap, color: AlphaColor<Srgb>, rects: &[RectU16]) { | |
| let color = color.premultiply().to_rgba8(); | |
| let width = usize::from(pixmap.width()); | |
| let bounds = RectU16::new(0, 0, pixmap.width(), pixmap.height()); | |
| let data = pixmap.data_mut(); | |
| for rect in rects | |
| .iter() | |
| .map(|rect| rect.intersect(bounds)) | |
| .filter(|rect| !rect.is_empty()) | |
| { | |
| for y in usize::from(rect.y0)..usize::from(rect.y1) { | |
| let start = y * width + usize::from(rect.x0); | |
| let end = y * width + usize::from(rect.x1); | |
| data[start..end].fill(color); | |
| } | |
| } | |
| } |