blob: e648e900822cceaa0fae403c796c9ac4276d31b9 [file]
// Copyright 2026 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use crate::render::webgl::resource::{Buffer, Framebuffer, Renderbuffer, SyncFence};
use crate::render::webgl::{
ViewFramebuffer, WebGlOperation, WebGlProbeOperation, WebGlResultExt, WebGlTextureBindings,
create_framebuffer_for_texture, create_texture_storage,
};
use crate::target::RootTarget;
use crate::{ClearSettings, RenderError, RenderSize, Scene, TargetInit, WebGlError, WebGlRenderer};
use alloc::{borrow::Cow, format, vec::Vec};
use core::ops::Deref;
use thiserror::Error;
use vello_common::TextureId;
use vello_common::color::palette::css;
use vello_common::filter_effects::Filter;
use vello_common::geometry::RectU16;
use vello_common::image_cache::ImageCache;
use vello_common::kurbo::{Affine, BezPath, Rect};
use vello_common::paint::{ImageSource, PaintType};
use vello_common::peniko::BlendMode;
use vello_common::pixmap::Pixmap;
use vello_common::probe::{Probe, ProbeFeature};
use web_sys::WebGl2RenderingContext;
/// A WebGL probe whose pixel readback has been queued but not completed.
#[derive(Debug)]
pub struct WebGlPendingProbe {
gl: WebGl2RenderingContext,
sync: SyncFence,
buffer: Buffer,
width: u16,
height: u16,
elements: Vec<ProbeFeature>,
}
/// Error returned while running a WebGL probe.
#[derive(Debug, Clone, Error)]
pub enum WebGlProbeError {
/// The probe was requested without any elements.
#[error("probe requires at least one element")]
NoElements,
/// Finishing the probe failed.
#[error("probe failed to finish: {}", webgl_error_name(*.0))]
FinishFailed(u32),
/// A WebGL operation failed.
#[error(transparent)]
WebGl(#[from] WebGlError),
}
/// Result of polling the WebGL probe.
#[derive(Debug)]
pub enum WebGlProbeStatus {
/// The probe is still pending.
Pending(WebGlPendingProbe),
/// The probe has finished and the result is available.
Complete(Probe<RenderError>),
}
impl WebGlRenderer {
/// Conduct a probing operation.
///
/// The WebGL drivers of certain devices are known to be buggy and might therefore not work correctly
/// with Vello GPU. In the best case, it will simply result in a program crash, but in the worst
/// case it can instead result in a silent failure, meaning that no explicit error is
/// thrown, but the rendered contents of Vello GPU will either be completely empty or look glitchy.
///
/// The purpose of this method is to run a sanity check to ensure that running Vello GPU on this
/// device actually results in visible and correct output. How this achieved is by drawing a selection
/// of small elements into a small canvas, and comparing the final output against a reference image.
///
/// This method will return a handle that allows inspecting the results of the probe once the
/// results of the probe scene can be copied back from GPU to CPU. For performance reasons,
/// anything in-between mostly happens asynchronously.
pub fn probe(
&mut self,
elements: &[ProbeFeature],
) -> Result<WebGlPendingProbe, WebGlProbeError> {
if elements.is_empty() {
return Err(WebGlProbeError::NoElements);
}
self.probe_inner(elements).map_err(WebGlProbeError::WebGl)
}
fn probe_inner(&mut self, elements: &[ProbeFeature]) -> Result<WebGlPendingProbe, WebGlError> {
// Whenever making changes here, make sure to unignore the `webgl_probe_succeeds_` and
// run them locally!
let (width, height) = vello_common::probe::canvas_size(elements);
let render_size = RenderSize { width, height };
// Check whether the canvas framebuffer was configured by the user to
// hold a depth buffer, and apply the same to the probe.
let use_depth_buffer = self.programs.resources.view_framebuffer.use_depth_buffer();
let probe_texture = create_texture_storage(
&self.gl,
WebGl2RenderingContext::RGBA8,
u32::from(render_size.width),
u32::from(render_size.height),
WebGl2RenderingContext::NEAREST,
WebGl2RenderingContext::NEAREST,
)?;
let probe_framebuffer = create_framebuffer_for_texture(&self.gl, &probe_texture)?;
// We need to keep the renderbuffer around until we finished rendering!
let _probe_depth = if use_depth_buffer {
let probe_depth = Renderbuffer::new(&self.gl)?;
self.gl
.bind_renderbuffer(WebGl2RenderingContext::RENDERBUFFER, Some(&probe_depth));
self.gl.renderbuffer_storage(
WebGl2RenderingContext::RENDERBUFFER,
WebGl2RenderingContext::DEPTH_COMPONENT24,
i32::from(width),
i32::from(height),
);
self.gl.framebuffer_renderbuffer(
WebGl2RenderingContext::FRAMEBUFFER,
WebGl2RenderingContext::DEPTH_ATTACHMENT,
WebGl2RenderingContext::RENDERBUFFER,
Some(&probe_depth),
);
Some(probe_depth)
} else {
None
};
let probe_image = vello_common::probe::probe_image_pixmap();
let probe_image_texture = create_texture_storage(
&self.gl,
WebGl2RenderingContext::RGBA8,
u32::from(probe_image.width()),
u32::from(probe_image.height()),
WebGl2RenderingContext::NEAREST,
WebGl2RenderingContext::NEAREST,
)?;
self.gl
.tex_sub_image_2d_with_i32_and_i32_and_u32_and_type_and_opt_u8_array(
WebGl2RenderingContext::TEXTURE_2D,
0,
0,
0,
i32::from(probe_image.width()),
i32::from(probe_image.height()),
WebGl2RenderingContext::RGBA,
WebGl2RenderingContext::UNSIGNED_BYTE,
Some(probe_image.data_as_u8_slice()),
)
.map_js_error(WebGlOperation::Probe(WebGlProbeOperation::ImageUpload))?;
let probe_texture_id = TextureId(0);
let mut texture_bindings = WebGlTextureBindings::new();
texture_bindings.insert(probe_texture_id, probe_image_texture.deref().clone());
let mut scene = Scene::new(width, height);
vello_common::probe::draw_scene(
&mut scene,
ImageSource::external_texture(
probe_texture_id,
RectU16::new(0, 0, probe_image.width(), probe_image.height()),
probe_image.may_have_transparency(),
),
elements,
);
let previous_view_framebuffer = core::mem::replace(
&mut self.programs.resources.view_framebuffer,
ViewFramebuffer::offscreen(probe_framebuffer, use_depth_buffer),
);
let render_result = self.render_scene(
&scene,
&ImageCache::new_dummy(),
&render_size,
TargetInit::Clear(ClearSettings::Viewport { color: css::WHITE }),
RootTarget::UserSurface,
&texture_bindings,
Some(&probe_texture),
);
let probe_framebuffer = core::mem::replace(
&mut self.programs.resources.view_framebuffer,
previous_view_framebuffer,
)
.into_framebuffer()
.unwrap();
// Propagate render failures only after restoring the previous framebuffer.
render_result?;
let pending = launch_probe(&self.gl, &probe_framebuffer, width, height, elements)?;
Ok(pending)
}
}
#[cfg(feature = "probe")]
impl WebGlPendingProbe {
/// Try to finish the probe.
///
/// In case the result is not available yet, a new pending probe object will be returned
/// which can be checked again in the future. Otherwise, the probe result or an error will be
/// returned.
pub fn try_finish(mut self) -> Result<WebGlProbeStatus, WebGlProbeError> {
let status = self.gl.client_wait_sync_with_u32(&self.sync, 0, 0);
if status == WebGl2RenderingContext::TIMEOUT_EXPIRED {
return Ok(WebGlProbeStatus::Pending(self));
}
if status == WebGl2RenderingContext::ALREADY_SIGNALED
|| status == WebGl2RenderingContext::CONDITION_SATISFIED
{
Ok(WebGlProbeStatus::Complete(self.finish_success()))
} else {
Err(self.finish_failure())
}
}
fn finish_success(&mut self) -> Probe<RenderError> {
let mut pixmap = Pixmap::new(self.width, self.height);
self.gl.bind_buffer(
WebGl2RenderingContext::PIXEL_PACK_BUFFER,
Some(&self.buffer),
);
// Safari 15 crashes the tab when attempting to read from a pixel pack buffer directly
// into WASM-allocated memory. Therefore, we first read it into a JS-allocated buffer and
// only then transfer it into the buffer backing the pixmap in WASM memory.
let readback =
js_sys::Uint8Array::new_with_length(u32::from(self.width) * u32::from(self.height) * 4);
self.gl.get_buffer_sub_data_with_i32_and_js_u8_array(
WebGl2RenderingContext::PIXEL_PACK_BUFFER,
0,
&readback,
);
readback.copy_to(pixmap.data_as_u8_slice_mut());
// Need to flip the resulting image upside down.
let row_len = usize::from(self.width) * 4;
let height = usize::from(self.height);
let pixels = pixmap.data_as_u8_slice_mut();
for row in 0..height / 2 {
let opposite_row = height - 1 - row;
let (top, bottom) = pixels.split_at_mut(opposite_row * row_len);
top[row * row_len..(row + 1) * row_len].swap_with_slice(&mut bottom[..row_len]);
}
Probe::from_actual(pixmap, &self.elements)
}
fn finish_failure(&self) -> WebGlProbeError {
WebGlProbeError::FinishFailed(self.gl.get_error())
}
}
fn webgl_error_name(error: u32) -> Cow<'static, str> {
let name = match error {
WebGl2RenderingContext::NO_ERROR => "NO_ERROR",
WebGl2RenderingContext::INVALID_ENUM => "INVALID_ENUM",
WebGl2RenderingContext::INVALID_VALUE => "INVALID_VALUE",
WebGl2RenderingContext::INVALID_OPERATION => "INVALID_OPERATION",
WebGl2RenderingContext::INVALID_FRAMEBUFFER_OPERATION => "INVALID_FRAMEBUFFER_OPERATION",
WebGl2RenderingContext::OUT_OF_MEMORY => "OUT_OF_MEMORY",
WebGl2RenderingContext::CONTEXT_LOST_WEBGL => "CONTEXT_LOST_WEBGL",
_ => return Cow::Owned(format!("UNKNOWN_WEBGL_ERROR ({error:#06x})")),
};
Cow::Borrowed(name)
}
#[cfg(feature = "probe")]
fn launch_probe(
gl: &WebGl2RenderingContext,
framebuffer: &Framebuffer,
width: u16,
height: u16,
elements: &[ProbeFeature],
) -> Result<WebGlPendingProbe, WebGlError> {
let pixel_pack_buffer = Buffer::new(gl)?;
let byte_len = i32::from(width) * i32::from(height) * 4;
gl.bind_buffer(
WebGl2RenderingContext::PIXEL_PACK_BUFFER,
Some(&pixel_pack_buffer),
);
gl.buffer_data_with_i32(
WebGl2RenderingContext::PIXEL_PACK_BUFFER,
byte_len,
WebGl2RenderingContext::STREAM_READ,
);
gl.bind_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
Some(framebuffer.deref()),
);
gl.read_pixels_with_i32(
0,
0,
i32::from(width),
i32::from(height),
WebGl2RenderingContext::RGBA,
WebGl2RenderingContext::UNSIGNED_BYTE,
0,
)
.map_js_error(WebGlOperation::Probe(WebGlProbeOperation::Readback))?;
// Create a fence that notifies us once rendering is complete and the contents have been
// transferred from the framebuffer to the pixel pack buffer.
let sync = SyncFence::new(gl)?;
// https://wikis.khronos.org/opengl/Sync_Object
// "It is important that syncs are properly flushed into the GPU's command queue. Without
// proper flushing, the sync object may never be signaled."
gl.flush();
gl.bind_buffer(WebGl2RenderingContext::PIXEL_PACK_BUFFER, None);
Ok(WebGlPendingProbe {
gl: gl.clone(),
sync,
buffer: pixel_pack_buffer,
width,
height,
elements: elements.to_vec(),
})
}
impl vello_common::probe::ProbeRenderer for Scene {
fn set_transform(&mut self, transform: Affine) {
Self::set_transform(self, transform);
}
fn set_paint(&mut self, paint: PaintType) {
Self::set_paint(self, paint);
}
fn fill_path(&mut self, path: &BezPath) {
Self::fill_path(self, path);
}
fn fill_rect(&mut self, rect: &Rect) {
Self::fill_rect(self, rect);
}
fn push_layer(&mut self, blend_mode: Option<BlendMode>, opacity: Option<f32>) {
Self::push_layer(self, None, blend_mode, opacity, None, None);
}
fn push_filter_layer(&mut self, filter: Filter) {
Self::push_filter_layer(self, filter);
}
fn pop_layer(&mut self) {
Self::pop_layer(self);
}
fn set_paint_transform(&mut self, paint_transform: Affine) {
Self::set_paint_transform(self, paint_transform);
}
fn reset_paint_transform(&mut self) {
Self::reset_paint_transform(self);
}
}