blob: 52fb657a3c23d6d68f167c7bf3f9f993a71fa46d [file]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Native WebGL2 backend for Vello GPU's Sparse Strips renderer.
//!
//! This module provides identical functionality as the [`wgpu`] module, however the graphics
//! context is the browser's native [`WebGl2RenderingContext`]. Hence, this module is only available
//! when targeting `wasm32` with the "webgl" feature flag active.
//!
//! The main benefit of this module, is binary size. Omitting `wgpu` saves approximately 3mb of
//! binary size (when targeting WebGL2).
//!
//! Maintaining this backend should be continually re-evaluated, as once the majority of users can
//! leverage WebGPU, we can remove this backend without the binary size increasing.
//! - WebGPU usage: <https://caniuse.com/webgpu>
#![expect(
clippy::cast_possible_truncation,
reason = "We temporarily ignore those because the casts\
only break in edge cases, and some of them are also only related to conversions from f64 to f32."
)]
mod error;
#[cfg(feature = "probe")]
pub(crate) mod probe;
pub(crate) mod resource;
#[cfg(feature = "probe")]
pub use error::WebGlProbeOperation;
pub(crate) use error::WebGlResultExt;
pub use error::{
IncompatibleContextReason, WebGlContextOperation, WebGlDataTransferOperation, WebGlError,
WebGlOperation, WebGlResourceKind, WebGlShaderInterfaceOperation, WebGlShaderProgramOperation,
WebGlShaderStage,
};
use crate::draw::{EXTERNAL_TEXTURE_SLOT_COUNT, ExternalTextureBindings, ExternalTextureRun};
use crate::render::common::IMAGE_PADDING;
use crate::util::RangedSlice;
use crate::{
ClearSettings, GpuStrip, LayersConfig, RenderError, RenderSettings, RenderSize, Resources,
TargetInit,
blend::{BlendStrip, GpuBlendInstance},
copy::GpuCopyInstance,
filter::{FilterContext, FilterInstanceData, FilterPassPlan},
gradient_cache::GradientRampCache,
paint::{PaintResolver, TextureSourceId},
render::{
Config,
common::{
DeviceLimits, GpuBlurredRoundedRect, GpuEncodedImage, GpuEncodedPaint,
GpuLinearGradient, GpuRadialGradient, GpuSweepGradient, ScratchBuffers, ScratchTexture,
pack_image_offset, pack_image_params, pack_image_size, pack_radial_kind_and_swapped,
pack_texture_width_and_extend_mode, pack_tint,
},
},
scene::Scene,
schedule::{
Backend, IntermediateTextureAllocations, Schedule, ScheduleStorage, round::BlendOp,
},
target::{
BlendPassBindings, DrawPassBindings, DrawPassTarget, FilterPassBindings, LayerTextureId,
RootTarget, TextureParity,
},
};
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
#[cfg(feature = "text")]
use glifo::PendingClearRect;
use hashbrown::HashMap;
use resource::{Buffer, FragmentShader, Framebuffer, Program, Texture, VertexArray, VertexShader};
use vello_common::color::{AlphaColor, Srgb};
use vello_common::image_cache::{ImageCache, ImageResource};
use vello_common::multi_atlas::{AtlasConfig, AtlasId};
use vello_common::{
TextureId,
encode::{
EncodedBlurredRoundedRectangle, EncodedGradient, EncodedKind, EncodedPaint,
MAX_GRADIENT_LUT_SIZE, RadialKind,
},
geometry::{RectU16, SizeU16},
paint::{ImageId, ImageSource},
peniko::{self},
pixmap::Pixmap,
tile::Tile,
};
use vello_gpu_shaders::{blend, copy, filter as filter_shader, render};
use web_sys::wasm_bindgen::{JsCast, JsValue};
use web_sys::{
HtmlCanvasElement, WebGl2RenderingContext, WebGlBuffer, WebGlFramebuffer, WebGlProgram,
WebGlShader, WebGlTexture, WebGlUniformLocation, WebGlVertexArrayObject,
};
/// Placeholder value for uninitialized GPU encoded paints.
const GPU_PAINT_PLACEHOLDER: GpuEncodedPaint = GpuEncodedPaint::LinearGradient(GpuLinearGradient {
texture_width_and_extend_mode: 0,
gradient_start: 0,
transform: [0.0; 6],
});
/// First texture unit from which the strip program samples external textures.
const EXTERNAL_TEXTURE_UNIT_START: u32 = 4;
/// Depth precision required to preserve painter order during opaque-strip rendering.
const REQUIRED_DEPTH_BITS: u32 = 24;
/// Query the WebGL context for the max texture size.
fn get_max_texture_dimension_2d(gl: &WebGl2RenderingContext) -> Result<u32, WebGlError> {
Ok(gl
.get_parameter(WebGl2RenderingContext::MAX_TEXTURE_SIZE)
.map_js_error(WebGlOperation::Context(WebGlContextOperation::Query))?
.as_f64()
.unwrap() as u32)
}
/// Result of polling WebGL renderer initialization.
#[derive(Debug)]
#[expect(
clippy::large_enum_variant,
reason = "both variants are one-shot ownership transfers; boxing would add unnecessary allocation"
)]
pub enum WebGlRendererInitStatus {
/// Shader compilation or program linking is still in progress.
Pending(WebGlRendererInit),
/// Initialization has completed and the renderer is ready to use.
Complete(WebGlRenderer),
}
/// Vello GPU's WebGL2 renderer.
///
/// Note that any operation of [`WebGlRenderer`] might modify the global state of the underlying
/// WebGL context; state like the active texture unit and texture bindings are therefore not
/// guaranteed to be restored to their original value after each method call.
///
/// # Context loss
/// Methods on [`WebGlRenderer`] are **not** guaranteed to return an error if the WebGL context
/// has been lost; therefore, if not handled properly, rendering or other operations might silently
/// fail. To prevent this, clients should create their own listener for the WebGL context lost
/// event and react to it appropriately.
#[derive(Debug)]
pub struct WebGlRenderer {
/// Programs for rendering.
pub(super) programs: WebGlPrograms,
/// WebGL context.
pub(crate) gl: WebGl2RenderingContext,
/// Encoded paints for storing encoded paints.
encoded_paints: Vec<GpuEncodedPaint>,
/// Stores the index (offset) of the encoded paints in the encoded paints texture.
paint_idxs: Vec<u32>,
/// Gradient cache for storing gradient ramps.
gradient_cache: GradientRampCache,
dummy_image_cache: Option<ImageCache>,
schedule_storage: ScheduleStorage,
scratch_buffers: ScratchBuffers,
layers_config: LayersConfig,
}
/// WebGL renderer initialization that may still be compiling shader programs.
///
/// Poll this with [`WebGlRendererInit::try_finish`] from successive animation frames to avoid
/// blocking the browser's main thread when `KHR_parallel_shader_compile` is available. On browsers
/// without the extension, the first poll falls back to synchronously finishing initialization.
#[derive(Debug)]
pub struct WebGlRendererInit {
programs: PendingWebGlPrograms,
gl: WebGl2RenderingContext,
gradient_cache: GradientRampCache,
layers_config: LayersConfig,
}
/// Runtime bindings for [externally owned textures](`TextureId`) sampled by image paints.
#[derive(Debug, Default, Clone)]
pub struct WebGlTextureBindings {
textures: HashMap<TextureId, WebGlTexture>,
}
impl WebGlTextureBindings {
/// Create an empty binding map.
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Insert or replace a texture binding.
///
/// The texture must satisfy the following requirements.
///
/// - It is sampled as a `sampler2D`, so its internal format must be float-sampleable (e.g.
/// `RGBA8`). Integer formats do not match the sampler's type.
/// - It is read with `texelFetch`, which requires the texture to be complete. For a texture
/// without mipmaps this means `TEXTURE_MIN_FILTER` must be set to `NEAREST` or `LINEAR`;
/// the WebGL default of `NEAREST_MIPMAP_LINEAR` leaves it incomplete and it will sample as
/// transparent black. Only mip level 0 is read.
/// - Its contents are treated as premultiplied alpha, and are sampled in texel space with
/// the origin at the first uploaded texel. A texture uploaded with `UNPACK_FLIP_Y_WEBGL`
/// enabled therefore samples vertically flipped.
#[inline]
pub fn insert(&mut self, texture_id: TextureId, texture: WebGlTexture) {
self.textures.insert(texture_id, texture);
}
/// Get a texture binding.
#[inline]
fn get(&self, texture_id: TextureId) -> Option<&WebGlTexture> {
self.textures.get(&texture_id)
}
/// Remove a texture binding.
///
/// This returns the removed texture binding if it existed.
#[inline]
pub fn remove(&mut self, texture_id: TextureId) -> Option<WebGlTexture> {
self.textures.remove(&texture_id)
}
}
/// Current allocation state of the WebGL image/glyph atlas textures.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AtlasTextureInfo {
/// Width of each atlas texture.
pub width: u16,
/// Height of each atlas texture.
pub height: u16,
/// Number of atlas textures available to the image cache.
pub texture_count: u32,
}
impl WebGlRenderer {
/// Creates a new WebGL2 renderer and its persistent resources.
///
/// This blocks until shader compilation and linking finish. Use [`WebGlRenderer::begin`] when
/// initialization must not block the browser's main thread.
pub fn new(canvas: &HtmlCanvasElement) -> Result<(Self, Resources), WebGlError> {
Self::new_with(canvas, RenderSettings::default(), true)
}
/// Creates a new WebGL2 renderer and its persistent resources with specific settings.
///
/// When `use_depth_buffer` is true, the WebGL context allocates a depth buffer and the renderer
/// uses it for opaque-strip occlusion. Disabling it reduces GPU memory usage, but can increase
/// overdraw.
pub fn new_with(
canvas: &HtmlCanvasElement,
settings: RenderSettings,
use_depth_buffer: bool,
) -> Result<(Self, Resources), WebGlError> {
let (init, resources) = Self::begin_with(canvas, settings, use_depth_buffer)?;
Ok((init.finish()?, resources))
}
/// Begins creating a WebGL2 renderer and its persistent resources.
///
/// When `KHR_parallel_shader_compile` is available, shader compilation and program linking can
/// continue while the caller yields to the browser. Poll the returned initialization with
/// [`WebGlRendererInit::try_finish`] on successive animation frames.
///
/// If the extension is unavailable, the first poll synchronously finishes shader compilation
/// and linking.
pub fn begin(canvas: &HtmlCanvasElement) -> Result<(WebGlRendererInit, Resources), WebGlError> {
Self::begin_with(canvas, RenderSettings::default(), true)
}
/// Begins creating a WebGL2 renderer with specific settings.
///
/// See [`WebGlRenderer::begin`] for polling and fallback behavior.
pub fn begin_with(
canvas: &HtmlCanvasElement,
mut settings: RenderSettings,
use_depth_buffer: bool,
) -> Result<(WebGlRendererInit, Resources), WebGlError> {
#[allow(
clippy::assertions_on_constants,
reason = "intentional guard against non-wasm32 use"
)]
{
debug_assert!(
cfg!(target_arch = "wasm32"),
"`WebGlRenderer` can only be constructed when targeting `wasm32`",
);
}
super::common::maybe_warn_about_webgl_feature_conflict();
// We do our own anti-aliasing, so no need to enable it in the WebGL
// context.
let context_options = js_sys::Object::new();
js_sys::Reflect::set(&context_options, &"antialias".into(), &JsValue::FALSE)
.map_js_error(WebGlOperation::Context(WebGlContextOperation::Configure))?;
// Avoid running Vello GPU if it's gonna be super slow.
js_sys::Reflect::set(
&context_options,
&"failIfMajorPerformanceCaveat".into(),
&JsValue::TRUE,
)
.map_js_error(WebGlOperation::Context(WebGlContextOperation::Configure))?;
// The opaque-strip optimization requires a 24+ bit depth buffer. If the hardware falls
// back to a 16 bit depth buffer, correctness issues will arise. For all intents and
// purposes, a device manufactured in the past 10 years should support 24+ bit depth
// buffers (certainly those within the realm of what we consider "supported" devices) but:
//
// Relevant code for default depth buffer behaviour can be found here:
// - Chromium defaults to 24 bit with no fallback: https://github.com/chromium/chromium/blob/86bafb3aab8e999690d310b201d0b5489f512b08/third_party/blink/renderer/platform/graphics/gpu/drawing_buffer.cc#L1376-L1400
// - Firefox defaults to 24 bit with no fallback: https://github.com/mozilla/gecko-dev/blob/5836a062726f715fda621338a17b51aff30d0a8c/gfx/gl/MozFramebuffer.cpp#L155-L161
// - Safari defaults to 24 bit _with 16 bit_ fallback: https://github.com/WebKit/WebKit/blob/a6d6c154bbee0643f5ad1e55c071558c0df9aef7/Source/WebCore/platform/graphics/angle/GraphicsContextGLANGLE.cpp#L393-L416
//
// TODO: The above understanding is encoded in a below assertion, but this should be encapsulated within a
// "this device can run Vello correctly" check function.
let depth = if use_depth_buffer {
&JsValue::TRUE
} else {
&JsValue::FALSE
};
js_sys::Reflect::set(&context_options, &"depth".into(), depth)
.map_js_error(WebGlOperation::Context(WebGlContextOperation::Configure))?;
let context = canvas
.get_context_with_context_options("webgl2", &context_options)
.map_js_error(WebGlOperation::Context(WebGlContextOperation::Create))?
.ok_or(WebGlError::ContextUnavailable)?;
let gl = context
.dyn_into::<WebGl2RenderingContext>()
.expect("Context to be a WebGL2 context");
// See https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#use_webgl_provoking_vertex_when_its_available
// for why we need to set this. This has been confirmed by benchmarks, where doing just a
// couple of draw calls with external textures tanks performance on any
// Apple device, especially low-tier iOS devices.
// IMPORTANT: This assumes that any use of flat parameters in shaders is the same across
// each vertex! In case this ever changes, we need to revisit this.
// https://web3dsurvey.com/webgl2/extensions/WEBGL_provoking_vertex
if let Some(ext) = gl
.get_extension("WEBGL_provoking_vertex")
.map_js_error(WebGlOperation::Context(WebGlContextOperation::Extension))?
{
// See https://registry.khronos.org/webgl/extensions/WEBGL_provoking_vertex
const FIRST_VERTEX_CONVENTION_WEBGL: u32 = 0x8E4D;
// In WebGL, the _last_ vertex supplies the value for flat shader varyings by default.
// According to the spec, implementations should only expose this flag if the _first_
// convention is faster by default. Therefore, if the extension is available, we always
// set it to _first_.
js_sys::Reflect::get(ext.as_ref(), &"provokingVertexWEBGL".into())
.map_js_error(WebGlOperation::Context(WebGlContextOperation::Extension))?
.unchecked_into::<js_sys::Function>()
.call1(ext.as_ref(), &JsValue::from(FIRST_VERTEX_CONVENTION_WEBGL))
.map_js_error(WebGlOperation::Context(WebGlContextOperation::Extension))?;
}
// Note: It is not entirely clear whether we really _have_ to ensure anti-aliasing is disabled.
// This code is inherited from a similar snippet in wgpu
// (https://github.com/gfx-rs/wgpu/blob/56e4a389ddd02403e232beef3d3ff305625e6485/wgpu-hal/src/gles/web.rs#L101-L106),
// which itself seems to have been copied from the older `gfx` crate, where it was first introduced
// in https://github.com/gfx-rs/gfx/pull/2554/changes#diff-a47711d61df7a43fe6dd99c39b936d17ff817cbc2238d7e3ae6698ffde9b88f7R79,
// without any comment on why.
// From my (Laurenz) testing, tests seem to work even when anti-aliasing is enabled,
// but Andrew previously got errors similar to the ones outlined in
// https://github.com/gfx-rs/wgpu/issues/5263. Therefore, we just leave it as is for now.
#[cfg(debug_assertions)]
{
// If a WebGL context already exists on this canvas, it will be returned instead of
// creating a new one with the correct context_options set.
// See this comment for why we still care about non-antialiased context:
// https://github.com/linebender/vello/pull/1546/changes#r3008692535
let context_attributes =
gl.get_context_attributes()
.ok_or(WebGlError::OperationFailed {
operation: WebGlOperation::Context(WebGlContextOperation::Query),
message: None,
})?;
let antialias = js_sys::Reflect::get(&context_attributes, &"antialias".into())
.map_js_error(WebGlOperation::Context(WebGlContextOperation::Query))?
.as_bool()
.unwrap();
if antialias {
return Err(WebGlError::IncompatibleContext(
IncompatibleContextReason::AntialiasingEnabled,
));
}
}
let device_limits = DeviceLimits {
max_texture_dimension_2d: u16::try_from(get_max_texture_dimension_2d(&gl)?)
.unwrap_or(u16::MAX),
};
settings.memory_settings.normalize(&device_limits);
if use_depth_buffer {
let actual_bits = gl
.get_parameter(WebGl2RenderingContext::DEPTH_BITS)
.map_js_error(WebGlOperation::Context(WebGlContextOperation::Query))?
.as_f64()
.unwrap() as u32;
if actual_bits < REQUIRED_DEPTH_BITS {
return Err(WebGlError::IncompatibleContext(
IncompatibleContextReason::InsufficientDepthBuffer {
actual_bits,
required_bits: REQUIRED_DEPTH_BITS,
},
));
}
}
let resources = Resources::new(settings.memory_settings.image_atlas_config);
let resource_texture_dimension_2d = device_limits.resource_texture_dimension_2d();
// Estimate the maximum number of gradient cache entries based on the resource texture
// dimension and the maximum gradient LUT size - worst case scenario.
let max_gradient_cache_size = resource_texture_dimension_2d * resource_texture_dimension_2d
/ MAX_GRADIENT_LUT_SIZE as u32;
let gradient_cache = GradientRampCache::new(max_gradient_cache_size, settings.level);
let layer_config = settings.memory_settings.layers_config;
let init = WebGlRendererInit {
programs: PendingWebGlPrograms::new(
gl.clone(),
&resources.image_cache,
layer_config,
resource_texture_dimension_2d,
use_depth_buffer,
)?,
gl,
gradient_cache,
layers_config: layer_config,
};
Ok((init, resources))
}
/// Render `scene` using WebGL2
///
/// This method creates GPU resources as needed and schedules potentially multiple draw calls.
///
/// Every [`TextureId`] referenced by the scene must have a binding; this returns
/// [`WebGlError::Render`] containing [`RenderError::MissingTextureBinding`] otherwise. See
/// [`WebGlTextureBindings::insert`] for the requirements on the bound textures.
///
/// To render without any texture bindings, you can pass an empty [`WebGlTextureBindings`].
pub fn render(
&mut self,
scene: &Scene,
resources: &mut Resources,
render_size: &RenderSize,
texture_bindings: &WebGlTextureBindings,
// Note that in WebGL, the contents of drawing buffers are not guaranteed to be preserved
// unless `preserveDrawingBuffer` has been set to `true` (which is highly discouraged).
// Therefore, for the WebGL backend we currently don't expose more configurable clear
// options.
clear_color: AlphaColor<Srgb>,
) -> Result<(), WebGlError> {
debug_assert_eq!(
RenderSize {
width: self.gl.drawing_buffer_width().try_into().unwrap(),
height: self.gl.drawing_buffer_height().try_into().unwrap()
},
*render_size,
"Render size must match drawing buffer size"
);
#[cfg(feature = "text")]
resources.before_render(
self,
|renderer, glyph_renderer, atlas_count, atlas_config, atlas_id| {
renderer.render_to_atlas(
glyph_renderer,
atlas_count,
atlas_config,
atlas_id,
texture_bindings,
)
},
|renderer, image_cache, upload, dst_x, dst_y| {
renderer.write_to_atlas(
image_cache,
upload.image_id,
&upload.pixmap,
Some([dst_x, dst_y]),
)
},
)?;
self.render_scene(
scene,
&resources.image_cache,
render_size,
TargetInit::Clear(ClearSettings::Viewport { color: clear_color }),
RootTarget::UserSurface,
texture_bindings,
None,
)?;
#[cfg(feature = "text")]
// TODO: We should sort the rectangles once by atlas
// page and then clear per atlas page instead of per rect.
resources.after_render(self, clear_atlas_region)?;
Ok(())
}
/// Render a `scene` directly into an atlas layer.
///
/// This renders the scene's content into the specified atlas layer, which can then
/// be sampled as an image in subsequent render passes. This is useful for rendering
/// vector content (e.g., glyphs) into the atlas for later use as cached images.
///
/// The scene should be sized to the atlas layer dimensions
/// ([`AtlasConfig::atlas_size`]), with content positioned at the allocated offset
/// coordinates from `ImageCache::allocate`.
///
/// This method creates its own command encoder and submits immediately,
/// ensuring atlas content is committed before any subsequent
/// [`render`](Self::render) call (the two methods share GPU resources that
/// are staged by `queue.write_*` and only applied on the next `queue.submit`).
///
/// `texture_bindings` provides [externally bound textures](`WebGlTextureBindings`)
/// referenced by the scene. Pass `&WebGlTextureBindings::new()` if the scene does
/// not use any.
#[doc(hidden)]
pub fn render_to_atlas(
&mut self,
scene: &Scene,
atlas_count: u32,
atlas_config: AtlasConfig,
atlas_id: AtlasId,
texture_bindings: &WebGlTextureBindings,
) -> Result<(), WebGlError> {
self.programs
.maybe_create_atlas_textures(&self.gl, atlas_count)?;
let (atlas_width, atlas_height) = atlas_config.atlas_size;
let atlas_render_size = RenderSize {
width: atlas_width,
height: atlas_height,
};
let render_target_texture = self.atlas_texture(atlas_id).clone();
let atlas_framebuffer =
if let Some(framebuffer) = self.programs.resources.atlas_render_framebuffer.take() {
framebuffer
} else {
Framebuffer::new(&self.gl)?
};
self.gl.bind_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
Some(&atlas_framebuffer),
);
self.gl.framebuffer_texture_2d(
WebGl2RenderingContext::FRAMEBUFFER,
WebGl2RenderingContext::COLOR_ATTACHMENT0,
WebGl2RenderingContext::TEXTURE_2D,
Some(&self.programs.resources.atlas_textures[atlas_id.as_u32() as usize]),
0,
);
let previous_view_framebuffer = core::mem::replace(
&mut self.programs.resources.view_framebuffer,
ViewFramebuffer::offscreen(atlas_framebuffer, false),
);
// TODO: Explore using an option instead of a dummy image cache.
let dummy_image_cache = self
.dummy_image_cache
.take()
.expect("dummy image cache must exist");
let result = self.render_scene(
scene,
&dummy_image_cache,
&atlas_render_size,
TargetInit::SrcOver,
RootTarget::AtlasLayer,
texture_bindings,
Some(&render_target_texture),
);
self.dummy_image_cache = Some(dummy_image_cache);
// Restore the previous view framebuffer and cache the atlas FBO for reuse.
let atlas_framebuffer = core::mem::replace(
&mut self.programs.resources.view_framebuffer,
previous_view_framebuffer,
)
.into_framebuffer()
.unwrap();
self.programs.resources.atlas_render_framebuffer = Some(atlas_framebuffer);
result
}
fn current_allocations(&self) -> IntermediateTextureAllocations {
IntermediateTextureAllocations {
layer_pages: self
.programs
.resources
.layer_textures
.each_ref()
.map(Vec::len),
scratch: self.programs.resources.scratch_texture.is_some(),
}
}
/// Shared render pipeline: prepares GPU resources, runs the scheduler, and
/// maintains caches.
///
pub(crate) fn render_scene(
&mut self,
scene: &Scene,
image_cache: &ImageCache,
render_size: &RenderSize,
target_init: TargetInit<'_>,
root_output_target: RootTarget,
texture_bindings: &WebGlTextureBindings,
render_target_texture: Option<&WebGlTexture>,
) -> Result<(), WebGlError> {
let encoded_paints = &scene.encoded_paints;
let use_depth_buffer = self.programs.resources.view_framebuffer.use_depth_buffer();
self.prepare_gpu_encoded_paints(
encoded_paints,
image_cache,
texture_bindings,
render_target_texture,
)?;
let mut required_texture_size = self
.layers_config
.required_intermediate_texture_size(&scene.recorder)
.map_err(RenderError::IntermediateTexture)?;
// We currently only grow and never shrink textures, so max it with whatever
// we had in the previous run.
required_texture_size = self
.programs
.resources
.texture_size
.max(required_texture_size);
let current_allocations = self.current_allocations();
let paint_resolver =
PaintResolver::new(encoded_paints, &self.paint_idxs).with_image_cache(image_cache);
let schedule = Schedule::try_new(
&mut self.schedule_storage,
scene,
root_output_target,
use_depth_buffer,
paint_resolver,
required_texture_size,
current_allocations,
self.layers_config.max_textures,
)?;
self.programs
.prepare_intermediate_textures(&self.gl, &schedule, required_texture_size)?;
// TODO: For the time being, we upload the entire alpha buffer as one big chunk. As a future
// refinement, we could have a bounded alpha buffer, and break draws when the alpha
// buffer fills.
self.programs.prepare(
&self.gl,
&mut self.gradient_cache,
&self.encoded_paints,
&mut scene.strip_storage.borrow_mut().alphas,
render_size,
&self.paint_idxs,
&self.schedule_storage.filter_context,
root_output_target,
)?;
self.programs.resources.depth_cleared_this_frame = false;
let mut ctx = WebGlRendererContext {
programs: &mut self.programs,
gl: &self.gl,
texture_bindings,
scratch_buffers: &mut self.scratch_buffers,
use_depth_buffer,
};
if let TargetInit::Clear(clear) = target_init {
ctx.clear_pass_inner(DrawPassTarget::Root(root_output_target), clear);
}
crate::schedule::execute(
&mut ctx,
&mut self.schedule_storage,
schedule,
root_output_target,
)?;
self.gl.bind_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
self.programs.resources.view_framebuffer.binding(),
);
if self.programs.resources.depth_cleared_this_frame {
// See: https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#use_invalidateframebuffer
// We want to indicate to the GPU driver that we won't read the depth buffer again
// until the next clear. This enables the GPU to avoid storing depth tiles back to VRAM.
self.gl
.invalidate_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
if self.programs.resources.view_framebuffer.is_offscreen() {
&self.programs.resources.offscreen_depth_attachment_array
} else {
&self.programs.resources.default_depth_attachment_array
},
)
.map_js_error(WebGlOperation::DataTransfer(
WebGlDataTransferOperation::FramebufferInvalidation,
))?;
}
self.gradient_cache.maintain();
Ok(())
}
/// Get a reference to the underlying WebGL context.
///
/// This allows direct access to WebGL operations for advanced use cases like texture creation.
pub fn gl_context(&self) -> &WebGl2RenderingContext {
&self.gl
}
/// Upload image to cache and atlas in one step. Returns the `ImageId`.
///
/// This is the WebGL analogue of the wgpu Renderer's `upload_image` method.
/// It allocates space in the image cache and uploads the image data to the atlas texture.
pub fn upload_image<T: WebGlAtlasWriter>(
&mut self,
resources: &mut Resources,
writer: &T,
) -> Result<ImageId, WebGlError> {
self.upload_image_with(&mut resources.image_cache, writer, IMAGE_PADDING)
}
pub(crate) fn upload_image_with<T: WebGlAtlasWriter>(
&mut self,
image_cache: &mut ImageCache,
writer: &T,
padding: u16,
) -> Result<ImageId, WebGlError> {
let width = writer.width();
let height = writer.height();
let image_id = image_cache
.allocate(width, height, padding)
.map_err(RenderError::AtlasError)?;
if let Err(err) = self.write_to_atlas(image_cache, image_id, writer, None) {
image_cache.deallocate(image_id);
return Err(err);
}
Ok(image_id)
}
/// Write pixel data to an existing atlas allocation.
///
/// Unlike [`upload_image`](Self::upload_image), this does not allocate space in the image
/// cache. The `image_id` must have been previously allocated (e.g. via
/// `ImageCache::allocate`). This is useful for uploading CPU-side pixel data (such as
/// bitmap font glyphs) to a pre-allocated atlas region.
///
/// If `offset_override` is `Some`, the provided offset is used instead of the
/// allocator-assigned position. Pass `None` to use the default atlas offset.
pub(crate) fn write_to_atlas<T: WebGlAtlasWriter>(
&mut self,
image_cache: &ImageCache,
image_id: ImageId,
writer: &T,
offset_override: Option<[u16; 2]>,
) -> Result<(), WebGlError> {
let image_resource = image_cache
.get(image_id)
.ok_or(RenderError::MissingImage(image_id))?;
self.programs
.maybe_create_atlas_textures(&self.gl, image_cache.atlas_count() as u32)?;
let offset = offset_override.unwrap_or(image_resource.offset);
writer.write_to_atlas(
&self.gl,
&self.programs.resources.atlas_textures[image_resource.atlas_id.as_u32() as usize],
offset,
writer.width(),
writer.height(),
)?;
Ok(())
}
/// Destroy an image from the cache and clear the allocated slot in the atlas.
pub fn destroy_image(
&mut self,
resources: &mut Resources,
image_id: ImageId,
) -> Result<(), WebGlError> {
if let Some(image_resource) = resources.image_cache.get(image_id) {
let padding = image_resource.padding;
self.clear_atlas_region(
image_resource.atlas_id,
[
image_resource.offset[0] - padding,
image_resource.offset[1] - padding,
],
image_resource.width + padding * 2,
image_resource.height + padding * 2,
)?;
let _ = resources.image_cache.deallocate(image_id);
}
Ok(())
}
/// Returns an individual image atlas texture.
pub fn atlas_texture(&self, atlas_id: AtlasId) -> &WebGlTexture {
self.programs
.resources
.atlas_textures
.get(atlas_id.as_u32() as usize)
.map(|texture| &**texture)
.unwrap()
}
/// Returns the current allocation state of the image/glyph atlas texture.
pub fn atlas_info(&self) -> AtlasTextureInfo {
let resources = &self.programs.resources;
AtlasTextureInfo {
width: resources.atlas_size.0,
height: resources.atlas_size.1,
texture_count: u32::try_from(resources.atlas_textures.len()).unwrap(),
}
}
/// Clear a specific region of an atlas texture.
fn clear_atlas_region(
&mut self,
atlas_id: AtlasId,
offset: [u16; 2],
width: u16,
height: u16,
) -> Result<(), WebGlError> {
let temp_framebuffer = Framebuffer::new(&self.gl)?;
// Bind our temporary framebuffer
self.gl
.bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(&temp_framebuffer));
self.gl.framebuffer_texture_2d(
WebGl2RenderingContext::FRAMEBUFFER,
WebGl2RenderingContext::COLOR_ATTACHMENT0,
WebGl2RenderingContext::TEXTURE_2D,
Some(&self.programs.resources.atlas_textures[atlas_id.as_u32() as usize]),
0,
);
// Set viewport to match the atlas texture dimensions
let (atlas_width, atlas_height) = self.programs.resources.atlas_size;
self.gl
.viewport(0, 0, i32::from(atlas_width), i32::from(atlas_height));
// Enable scissor test and set scissor rectangle to our region
self.gl.enable(WebGl2RenderingContext::SCISSOR_TEST);
self.gl.scissor(
offset[0] as i32,
offset[1] as i32,
width as i32,
height as i32,
);
// Clear the region to transparent (0, 0, 0, 0)
self.gl.clear_color(0.0, 0.0, 0.0, 0.0);
self.gl.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT);
Ok(())
}
fn prepare_gpu_encoded_paints(
&mut self,
encoded_paints: &[EncodedPaint],
image_cache: &ImageCache,
texture_bindings: &WebGlTextureBindings,
render_target_texture: Option<&WebGlTexture>,
) -> Result<(), WebGlError> {
self.encoded_paints
.resize_with(encoded_paints.len(), || GPU_PAINT_PLACEHOLDER);
self.paint_idxs.resize(encoded_paints.len() + 1, 0);
let mut current_idx = 0;
for (encoded_paint_idx, paint) in encoded_paints.iter().enumerate() {
self.paint_idxs[encoded_paint_idx] = current_idx;
let gpu_paint = match paint {
EncodedPaint::Image(img) => match &img.source {
ImageSource::OpaqueId { id, .. } => {
let image_resource =
image_cache.get(*id).ok_or(RenderError::MissingImage(*id))?;
self.encode_image_paint(img, image_resource)
}
ImageSource::ExternalTexture {
id, source_region, ..
} => {
let texture = texture_bindings
.get(*id)
.ok_or(RenderError::MissingTextureBinding(*id))?;
if render_target_texture.is_some_and(|target| target == texture) {
return Err(WebGlError::Render(RenderError::TextureFeedbackLoop(*id)));
}
Self::encode_external_texture_paint(img, *source_region)
}
ImageSource::Pixmap(_) => {
panic!("pixmap image sources are not supported by Vello GPU")
}
},
EncodedPaint::Gradient(gradient) => {
let (gradient_start, gradient_width) =
self.gradient_cache.get_or_create_ramp(gradient);
self.encode_gradient_paint(gradient, gradient_width, gradient_start)
}
EncodedPaint::BlurredRoundedRect(blurred_rect) => {
Self::encode_blurred_rounded_rect_paint(blurred_rect)
}
};
current_idx += gpu_paint.size_texels();
self.encoded_paints[encoded_paint_idx] = gpu_paint;
}
self.paint_idxs[encoded_paints.len()] = current_idx;
Ok(())
}
fn encode_image_paint(
&self,
image: &vello_common::encode::EncodedImage,
image_resource: &ImageResource,
) -> GpuEncodedPaint {
let transform = image.transform.as_coeffs().map(|x| x as f32);
let image_size = pack_image_size(image_resource.width, image_resource.height);
let image_offset = pack_image_offset(image_resource.offset[0], image_resource.offset[1]);
let image_params = pack_image_params(
image.sampler.quality as u32,
image.sampler.x_extend as u32,
image.sampler.y_extend as u32,
);
let (tint, tint_mode) = pack_tint(image.tint);
GpuEncodedPaint::Image(GpuEncodedImage {
image_params,
image_size,
image_offset,
transform,
tint,
tint_mode,
image_padding: image_resource.padding as u32,
})
}
fn encode_external_texture_paint(
image: &vello_common::encode::EncodedImage,
region: RectU16,
) -> GpuEncodedPaint {
let transform = image.transform.as_coeffs().map(|x| x as f32);
let image_size = pack_image_size(region.width(), region.height());
let image_offset = pack_image_offset(region.x0, region.y0);
let image_params = pack_image_params(
image.sampler.quality as u32,
image.sampler.x_extend as u32,
image.sampler.y_extend as u32,
);
let (tint, tint_mode) = pack_tint(image.tint);
GpuEncodedPaint::Image(GpuEncodedImage {
image_params,
image_size,
image_offset,
transform,
tint,
tint_mode,
image_padding: 0,
})
}
fn encode_gradient_paint(
&self,
gradient: &EncodedGradient,
gradient_width: u32,
gradient_start: u32,
) -> GpuEncodedPaint {
let transform = gradient.transform.as_coeffs().map(|x| x as f32);
let extend_mode = match gradient.extend {
peniko::Extend::Pad => 0,
peniko::Extend::Repeat => 1,
peniko::Extend::Reflect => 2,
};
let texture_width_and_extend_mode =
pack_texture_width_and_extend_mode(gradient_width, extend_mode);
match &gradient.kind {
EncodedKind::Linear(_) => GpuEncodedPaint::LinearGradient(GpuLinearGradient {
texture_width_and_extend_mode,
gradient_start,
transform,
}),
EncodedKind::Radial(radial) => {
let (kind, bias, scale, fp0, fp1, fr1, f_focal_x, f_is_swapped, scaled_r0_squared) =
match radial {
RadialKind::Radial { bias, scale } => {
(0, *bias, *scale, 0.0, 0.0, 0.0, 0.0, 0, 0.0)
}
RadialKind::Strip { scaled_r0_squared } => {
(1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, *scaled_r0_squared)
}
RadialKind::Focal {
focal_data,
fp0,
fp1,
} => (
2,
*fp0,
*fp1,
*fp0,
*fp1,
focal_data.fr1,
focal_data.f_focal_x,
focal_data.f_is_swapped as u32,
0.0,
),
};
GpuEncodedPaint::RadialGradient(GpuRadialGradient {
texture_width_and_extend_mode,
gradient_start,
transform,
kind_and_f_is_swapped: pack_radial_kind_and_swapped(kind, f_is_swapped),
bias,
scale,
fp0,
fp1,
fr1,
f_focal_x,
scaled_r0_squared,
})
}
EncodedKind::Sweep(sweep) => GpuEncodedPaint::SweepGradient(GpuSweepGradient {
texture_width_and_extend_mode,
gradient_start,
transform,
start_angle: sweep.start_angle,
inv_angle_delta: sweep.inv_angle_delta,
_padding: [0, 0],
}),
}
}
fn encode_blurred_rounded_rect_paint(rect: &EncodedBlurredRoundedRectangle) -> GpuEncodedPaint {
GpuEncodedPaint::BlurredRoundedRect(GpuBlurredRoundedRect {
transform: rect.transform.as_coeffs().map(|x| x as f32),
color: rect.color.as_premul_rgba8().to_u32(),
invert: u32::from(rect.invert),
params0: [
rect.exponent,
rect.recip_exponent,
rect.scale,
rect.std_dev_inv,
],
params1: [rect.min_edge, rect.w, rect.h, rect.r1],
size: [rect.width, rect.height],
_padding1: [0, 0],
})
}
}
impl WebGlRendererInit {
/// Polls shader compilation and program linking without blocking when
/// `KHR_parallel_shader_compile` is available.
///
/// On browsers without the extension, this synchronously finishes initialization and returns
/// [`WebGlRendererInitStatus::Complete`].
pub fn try_finish(self) -> Result<WebGlRendererInitStatus, WebGlError> {
if self.programs.is_complete(&self.gl) {
Ok(WebGlRendererInitStatus::Complete(self.finish()?))
} else {
Ok(WebGlRendererInitStatus::Pending(self))
}
}
/// Blocks until shader compilation and program linking finish.
///
/// Prefer [`WebGlRendererInit::try_finish`] when blocking the browser's main thread is
/// undesirable.
pub fn finish(self) -> Result<WebGlRenderer, WebGlError> {
let programs = self.programs.finish(&self.gl)?;
Ok(WebGlRenderer {
programs,
gl: self.gl,
encoded_paints: Vec::new(),
paint_idxs: Vec::new(),
gradient_cache: self.gradient_cache,
dummy_image_cache: Some(ImageCache::new_dummy()),
schedule_storage: ScheduleStorage::default(),
scratch_buffers: ScratchBuffers::default(),
layers_config: self.layers_config,
})
}
}
#[cfg(feature = "text")]
fn clear_atlas_region(
renderer: &mut WebGlRenderer,
rect: &PendingClearRect,
) -> Result<(), WebGlError> {
// TODO: Similarly to wgpu, maybe this can be done in a more effective
// way?
renderer.clear_atlas_region(
AtlasId::new(rect.page_index),
[rect.x, rect.y],
rect.width,
rect.height,
)?;
Ok(())
}
/// Contains the WebGL programs and resources for rendering.
#[derive(Debug)]
pub(crate) struct WebGlPrograms {
/// Program for rendering strips.
strip_program: Program,
/// Uniform locations for the strip program
strip_uniforms: StripUniforms,
/// Program for filter passes.
filter_program: Program,
/// Uniform locations for the filter program.
filter_uniforms: FilterPassUniforms,
/// Program for performing blending.
blend_program: Program,
/// Uniform locations for the blend program.
blend_uniforms: BlendUniforms,
/// Program for performing copies.
copy_program: Program,
/// Uniform locations for the copy program.
copy_uniforms: CopyUniforms,
/// WebGL resources for rendering.
pub(crate) resources: WebGlResources,
/// Dimensions of the rendering target.
render_size: RenderSize,
/// Whether the last config buffer upload had NDC Y negation enabled.
negate_ndc: bool,
/// Scratch buffer for staging encoded paints texture data.
encoded_paints_data: Vec<u8>,
/// Scratch buffer for staging filter data texture data.
filter_data: Vec<u8>,
}
#[derive(Debug)]
struct FilterPassUniforms {
filter_data: WebGlUniformLocation,
source_texture: WebGlUniformLocation,
original_texture: WebGlUniformLocation,
}
#[derive(Debug)]
struct BlendUniforms {
layer_texture_0: WebGlUniformLocation,
layer_texture_1: WebGlUniformLocation,
alphas_texture: WebGlUniformLocation,
}
#[derive(Debug)]
struct CopyUniforms {
source_texture: WebGlUniformLocation,
}
/// Uniform locations for `strip_program`.
#[derive(Debug)]
struct StripUniforms {
/// Config uniform block index for vertex shader.
config_vs_block_index: u32,
/// Config uniform block index for fragment shader.
config_fs_block_index: u32,
/// Alphas texture location.
alphas_texture: WebGlUniformLocation,
/// Layer input texture location.
layer_input_texture: WebGlUniformLocation,
/// Encoded paints texture location for fragment shader.
encoded_paints_texture_fs: WebGlUniformLocation,
/// Encoded paints texture location for vertex shader.
encoded_paints_texture_vs: WebGlUniformLocation,
/// Gradient texture location.
gradient_texture: WebGlUniformLocation,
/// External texture locations, indexed by shader slot.
external_textures: [WebGlUniformLocation; EXTERNAL_TEXTURE_SLOT_COUNT],
}
/// The framebuffer used for root-target rendering.
#[derive(Debug)]
pub(crate) enum ViewFramebuffer {
/// The canvas framebuffer created with the WebGL context.
Default {
/// Whether the context's depth buffer is enabled and should be used.
use_depth_buffer: bool,
},
/// An offscreen framebuffer object.
Offscreen {
/// The framebuffer object to bind.
framebuffer: Framebuffer,
/// Whether the framebuffer has a usable depth attachment.
use_depth_buffer: bool,
},
}
impl ViewFramebuffer {
pub(crate) const fn default(use_depth_buffer: bool) -> Self {
Self::Default { use_depth_buffer }
}
/// If `use_depth_buffer` is true, the framebuffer must have a valid depth attachment!
pub(crate) fn offscreen(framebuffer: Framebuffer, use_depth_buffer: bool) -> Self {
Self::Offscreen {
framebuffer,
use_depth_buffer,
}
}
fn binding(&self) -> Option<&WebGlFramebuffer> {
match self {
Self::Default { .. } => None,
Self::Offscreen { framebuffer, .. } => Some(framebuffer),
}
}
const fn use_depth_buffer(&self) -> bool {
match self {
Self::Default { use_depth_buffer }
| Self::Offscreen {
use_depth_buffer, ..
} => *use_depth_buffer,
}
}
const fn is_offscreen(&self) -> bool {
matches!(self, Self::Offscreen { .. })
}
pub(crate) fn into_framebuffer(self) -> Option<Framebuffer> {
match self {
Self::Default { .. } => None,
Self::Offscreen { framebuffer, .. } => Some(framebuffer),
}
}
}
/// Contains all WebGL resources needed for rendering.
#[derive(Debug)]
pub(crate) struct WebGlResources {
/// Shared indices for four-vertex triangle-strip draws.
quad_index_buffer: Buffer,
/// VAO for strip rendering.
strip_vao: VertexArray,
/// Buffer for [`GpuStrip`] data.
strips_buffer: Buffer,
/// Texture for alpha values.
alphas_texture: Texture,
/// Height of alpha texture.
alpha_texture_height: u32,
/// One 2D texture per image atlas.
pub(crate) atlas_textures: Vec<Texture>,
/// Configured atlas dimensions.
pub(crate) atlas_size: (u16, u16),
/// Encoded paints texture for image metadata.
encoded_paints_texture: Texture,
/// Height of encoded paints texture.
encoded_paints_texture_height: u32,
/// Gradient texture for gradient ramp data.
gradient_texture: Texture,
/// Placeholder texture bound to unoccupied external texture slots.
placeholder_external_texture: Texture,
/// Height of gradient texture.
gradient_texture_height: u32,
/// Config buffer for rendering strips into the root target.
view_config_buffer: Buffer,
/// The root framebuffer and whether its depth buffer may be used.
pub(crate) view_framebuffer: ViewFramebuffer,
/// Whether the depth buffer has been cleared this frame.
depth_cleared_this_frame: bool,
/// Pre-allocated depth attachment list for the default framebuffer.
default_depth_attachment_array: js_sys::Array,
/// Pre-allocated depth attachment list for offscreen framebuffers.
offscreen_depth_attachment_array: js_sys::Array,
/// Maximum width or height of packed resource textures.
resource_texture_dimension_2d: u32,
/// Dimensions of the intermediate layer and scratch textures.
texture_size: SizeU16,
/// Cached framebuffer for rendering into an atlas texture in `render_to_atlas`.
/// Reused to avoid create/delete overhead on every call.
atlas_render_framebuffer: Option<Framebuffer>,
/// RGBA32UI texture storing filter parameters.
filter_data_texture: Texture,
/// Current height of filter data texture.
filter_data_texture_height: u32,
/// Per-instance vertex data buffer for filter draws.
filter_instance_buffer: Buffer,
/// VAO for filter rendering.
filter_vao: VertexArray,
/// Per-instance vertex data buffer for blend draws.
blend_instance_buffer: Buffer,
/// VAO for blend rendering.
blend_vao: VertexArray,
/// Per-instance vertex data buffer for copy draws.
copy_instance_buffer: Buffer,
/// VAO for copy rendering.
copy_vao: VertexArray,
/// Scratch texture.
scratch_texture: Option<ScratchTexture<WebGlIntermediateTexture>>,
/// Config buffer for rendering strips into a layer texture.
layer_config_buffer: Buffer,
/// Layer texture pages grouped by parity.
layer_textures: [Vec<WebGlIntermediateTexture>; 2],
}
impl WebGlResources {
fn layer_texture(&self, id: LayerTextureId) -> &Texture {
self.layer_textures[id.texture_parity.get_parity()][usize::from(id.page_index)]
.binding_texture()
}
fn layer_framebuffer(&self, id: LayerTextureId) -> &Framebuffer {
self.layer_textures[id.texture_parity.get_parity()][usize::from(id.page_index)]
.framebuffer()
}
fn scratch_binding_texture(&self) -> &Texture {
self.scratch_texture
.as_ref()
.map_or(&self.placeholder_external_texture, |texture| {
texture.get().binding_texture()
})
}
fn scratch_framebuffer(&self) -> &Framebuffer {
self.scratch_texture.as_ref().unwrap().get().framebuffer()
}
}
#[derive(Debug)]
struct WebGlIntermediateTexture {
texture: Texture,
framebuffer: Framebuffer,
}
impl WebGlIntermediateTexture {
fn binding_texture(&self) -> &Texture {
&self.texture
}
fn framebuffer(&self) -> &Framebuffer {
&self.framebuffer
}
}
#[derive(Debug)]
struct PendingShaderProgram {
vertex_shader: VertexShader,
fragment_shader: FragmentShader,
program: Program,
}
impl PendingShaderProgram {
fn new(
gl: &WebGl2RenderingContext,
vertex_src: &str,
fragment_src: &str,
) -> Result<Self, WebGlError> {
let vertex_shader = VertexShader::new(gl)?;
gl.shader_source(&vertex_shader, vertex_src);
gl.compile_shader(&vertex_shader);
let fragment_shader = FragmentShader::new(gl)?;
gl.shader_source(&fragment_shader, fragment_src);
gl.compile_shader(&fragment_shader);
let program = Program::new(gl)?;
gl.attach_shader(&program, &vertex_shader);
gl.attach_shader(&program, &fragment_shader);
gl.link_program(&program);
Ok(Self {
vertex_shader,
fragment_shader,
program,
})
}
fn is_complete(&self, gl: &WebGl2RenderingContext, parallel_shader_compile: bool) -> bool {
/// `COMPLETION_STATUS_KHR` from the `KHR_parallel_shader_compile` specification.
const COMPLETION_STATUS_KHR: u32 = 0x91B1;
if !parallel_shader_compile {
return true;
}
gl.get_program_parameter(&self.program, COMPLETION_STATUS_KHR)
.as_bool()
.unwrap_or(false)
}
fn finish(self, gl: &WebGl2RenderingContext) -> Result<Program, WebGlError> {
if !gl
.get_program_parameter(&self.program, WebGl2RenderingContext::LINK_STATUS)
.as_bool()
.unwrap_or(false)
{
if !gl
.get_shader_parameter(&self.vertex_shader, WebGl2RenderingContext::COMPILE_STATUS)
.as_bool()
.unwrap_or(false)
{
let info = gl
.get_shader_info_log(&self.vertex_shader)
.unwrap_or_else(|| "Unknown error creating vertex shader".into());
return Err(WebGlError::OperationFailed {
operation: WebGlOperation::ShaderProgram(WebGlShaderProgramOperation::Compile(
WebGlShaderStage::Vertex,
)),
message: Some(info),
});
}
if !gl
.get_shader_parameter(
&self.fragment_shader,
WebGl2RenderingContext::COMPILE_STATUS,
)
.as_bool()
.unwrap_or(false)
{
let info = gl
.get_shader_info_log(&self.fragment_shader)
.unwrap_or_else(|| "Unknown error creating fragment shader".into());
return Err(WebGlError::OperationFailed {
operation: WebGlOperation::ShaderProgram(WebGlShaderProgramOperation::Compile(
WebGlShaderStage::Fragment,
)),
message: Some(info),
});
}
let info = gl
.get_program_info_log(&self.program)
.unwrap_or_else(|| "Unknown error creating program".into());
return Err(WebGlError::OperationFailed {
operation: WebGlOperation::ShaderProgram(WebGlShaderProgramOperation::Link),
message: Some(info),
});
}
Ok(self.program)
}
}
#[derive(Debug)]
struct PendingWebGlPrograms {
parallel_shader_compile: bool,
strip_program: PendingShaderProgram,
filter_program: PendingShaderProgram,
blend_program: PendingShaderProgram,
copy_program: PendingShaderProgram,
resources: WebGlResources,
encoded_paints_data: Vec<u8>,
}
impl PendingWebGlPrograms {
/// Starts compiling all programs before performing any blocking status queries.
fn new(
gl: WebGl2RenderingContext,
image_cache: &ImageCache,
layer_config: LayersConfig,
resource_texture_dimension_2d: u32,
use_depth_buffer: bool,
) -> Result<Self, WebGlError> {
let parallel_shader_compile = gl
.get_extension("KHR_parallel_shader_compile")
.map_js_error(WebGlOperation::Context(WebGlContextOperation::Extension))?
.is_some();
let strip_program =
PendingShaderProgram::new(&gl, render::VERTEX_SOURCE, render::FRAGMENT_SOURCE)?;
let filter_program = PendingShaderProgram::new(
&gl,
filter_shader::VERTEX_SOURCE,
filter_shader::FRAGMENT_SOURCE,
)?;
let blend_program =
PendingShaderProgram::new(&gl, blend::VERTEX_SOURCE, blend::FRAGMENT_SOURCE)?;
let copy_program =
PendingShaderProgram::new(&gl, copy::VERTEX_SOURCE, copy::FRAGMENT_SOURCE)?;
let resources = create_webgl_resources(
&gl,
image_cache,
layer_config,
resource_texture_dimension_2d,
use_depth_buffer,
)?;
initialize_strip_vao(&gl, &resources);
initialize_filter_vao(&gl, &resources);
initialize_blend_vao(&gl, &resources);
initialize_copy_vao(&gl, &resources);
let encoded_paints_data = vec![0; (resources.resource_texture_dimension_2d << 4) as usize];
Ok(Self {
parallel_shader_compile,
strip_program,
filter_program,
blend_program,
copy_program,
resources,
encoded_paints_data,
})
}
fn is_complete(&self, gl: &WebGl2RenderingContext) -> bool {
self.strip_program
.is_complete(gl, self.parallel_shader_compile)
&& self
.filter_program
.is_complete(gl, self.parallel_shader_compile)
&& self
.blend_program
.is_complete(gl, self.parallel_shader_compile)
&& self
.copy_program
.is_complete(gl, self.parallel_shader_compile)
}
fn finish(self, gl: &WebGl2RenderingContext) -> Result<WebGlPrograms, WebGlError> {
let strip_program = self.strip_program.finish(gl)?;
let filter_program = self.filter_program.finish(gl)?;
let blend_program = self.blend_program.finish(gl)?;
let copy_program = self.copy_program.finish(gl)?;
let filter_uniforms = get_filter_pass_uniforms(gl, &filter_program)?;
let blend_uniforms = get_blend_uniforms(gl, &blend_program)?;
let copy_uniforms = get_copy_uniforms(gl, &copy_program)?;
let strip_uniforms = get_strip_uniforms(gl, &strip_program)?;
gl.enable(WebGl2RenderingContext::BLEND);
gl.blend_func(
WebGl2RenderingContext::ONE,
WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA,
);
Ok(WebGlPrograms {
strip_program,
strip_uniforms,
filter_program,
filter_uniforms,
blend_program,
blend_uniforms,
copy_program,
copy_uniforms,
resources: self.resources,
render_size: RenderSize {
width: 0,
height: 0,
},
negate_ndc: false,
encoded_paints_data: self.encoded_paints_data,
filter_data: Vec::new(),
})
}
}
impl WebGlPrograms {
/// Prepare resources for rendering.
fn prepare(
&mut self,
gl: &WebGl2RenderingContext,
gradient_cache: &mut GradientRampCache,
encoded_paints: &[GpuEncodedPaint],
alphas: &mut Vec<u8>,
render_size: &RenderSize,
paint_idxs: &[u32],
filter_context: &FilterContext,
root_target: RootTarget,
) -> Result<(), WebGlError> {
let resource_texture_dimension_2d = self.resources.resource_texture_dimension_2d;
self.maybe_resize_alphas_tex(gl, resource_texture_dimension_2d, alphas.len())?;
self.maybe_resize_encoded_paints_tex(gl, resource_texture_dimension_2d, paint_idxs)?;
self.maybe_resize_filter_data_tex(gl, filter_context)?;
let negate_ndc = DrawPassTarget::Root(root_target).negate_ndc();
self.maybe_update_config_buffer(gl, resource_texture_dimension_2d, render_size, negate_ndc);
self.upload_alpha_texture(gl, alphas)?;
self.upload_encoded_paints_texture(gl, encoded_paints, paint_idxs)?;
self.upload_filter_data_texture(gl, filter_context)?;
if gradient_cache.has_changed() {
self.maybe_resize_gradient_tex(gl, resource_texture_dimension_2d, gradient_cache)?;
self.upload_gradient_texture(gl, gradient_cache)?;
gradient_cache.mark_synced();
}
Ok(())
}
fn prepare_intermediate_textures(
&mut self,
gl: &WebGl2RenderingContext,
schedule: &Schedule,
texture_size: SizeU16,
) -> Result<(), WebGlError> {
let current_size = self.resources.texture_size;
let allocations = schedule.intermediate_texture_requirements().allocations;
let layer_pages = allocations.layer_pages;
let scratch_required = allocations.scratch;
let size_changed = current_size != texture_size;
if size_changed {
upload_layer_config_buffer(
gl,
&self.resources.layer_config_buffer,
texture_size,
self.resources.resource_texture_dimension_2d,
);
}
for (index, textures) in self.resources.layer_textures.iter_mut().enumerate() {
let required_page_count = layer_pages[index];
// When resizing textures, we explicitly first destroy the old textures before creating
// the new ones. The hope is that this will allow the WebGL driver to recognize
// that the memory of the old texture can be deallocated before allocating the
// new one, reducing peak memory consumption. It has not been validated whether
// this actually makes any difference in practice, but it shouldn't hurt either.
// Note: Currently, `texture_size` only grows across frames, so if this condition
// is true it means that the new required size is larger than what we currently have.
if size_changed {
let page_count = textures.len().max(required_page_count);
textures.clear();
for _ in 0..page_count {
textures.push(create_intermediate_texture(gl, texture_size)?);
}
} else {
for _ in textures.len()..required_page_count {
textures.push(create_intermediate_texture(gl, texture_size)?);
}
}
}
let recreate_scratch = (self.resources.scratch_texture.is_some() && size_changed)
|| (self.resources.scratch_texture.is_none() && scratch_required);
if recreate_scratch {
// Make sure the old texture is destroyed first.
let _ = self.resources.scratch_texture.take();
self.resources.scratch_texture = Some(ScratchTexture::new(
create_intermediate_texture(gl, texture_size)?,
));
}
self.resources.texture_size = texture_size;
Ok(())
}
/// Create any newly allocated atlas textures.
fn maybe_create_atlas_textures(
&mut self,
gl: &WebGl2RenderingContext,
required_atlas_count: u32,
) -> Result<(), WebGlError> {
let (width, height) = self.resources.atlas_size;
while self.resources.atlas_textures.len() < required_atlas_count as usize {
self.resources
.atlas_textures
.push(create_atlas_texture(gl, width, height)?);
}
Ok(())
}
/// Grow the filter data texture if needed.
fn maybe_resize_filter_data_tex(
&mut self,
gl: &WebGl2RenderingContext,
filter_context: &FilterContext,
) -> Result<(), WebGlError> {
let resource_texture_dimension_2d = self.resources.resource_texture_dimension_2d;
let Some(required_height) =
filter_context.required_filter_data_height(resource_texture_dimension_2d)
else {
return Ok(());
};
if required_height > self.resources.filter_data_texture_height {
let required_size = (resource_texture_dimension_2d * required_height) << 4;
self.filter_data.resize(required_size as usize, 0);
self.resources.filter_data_texture = create_data_texture_storage(
gl,
WebGl2RenderingContext::RGBA32UI,
resource_texture_dimension_2d,
required_height,
)?;
self.resources.filter_data_texture_height = required_height;
}
Ok(())
}
/// Upload filter data to the texture.
fn upload_filter_data_texture(
&mut self,
gl: &WebGl2RenderingContext,
filter_context: &FilterContext,
) -> Result<(), WebGlError> {
if filter_context.is_empty() {
return Ok(());
}
let width = self.resources.resource_texture_dimension_2d;
let used_height = filter_context
.required_filter_data_height(width)
.expect("non-empty filter context must have a data height");
let used_size = ((width * used_height) << 4) as usize;
filter_context.serialize_to_buffer(&mut self.filter_data);
upload_rgba32ui_rows(
gl,
&self.resources.filter_data_texture,
bytemuck::cast_slice::<u8, u32>(&self.filter_data[..used_size]),
width,
used_height,
)?;
Ok(())
}
fn upload_filter_instances(
&self,
gl: &WebGl2RenderingContext,
instances: &[FilterInstanceData],
) {
gl.bind_buffer(
WebGl2RenderingContext::ARRAY_BUFFER,
Some(&self.resources.filter_instance_buffer),
);
gl.buffer_data_with_u8_array(
WebGl2RenderingContext::ARRAY_BUFFER,
bytemuck::cast_slice(instances),
WebGl2RenderingContext::DYNAMIC_DRAW,
);
}
fn upload_blend_instances(&self, gl: &WebGl2RenderingContext, instances: &[GpuBlendInstance]) {
gl.bind_buffer(
WebGl2RenderingContext::ARRAY_BUFFER,
Some(&self.resources.blend_instance_buffer),
);
gl.buffer_data_with_u8_array(
WebGl2RenderingContext::ARRAY_BUFFER,
bytemuck::cast_slice(instances),
WebGl2RenderingContext::DYNAMIC_DRAW,
);
}
fn upload_copy_instances(&self, gl: &WebGl2RenderingContext, instances: &[GpuCopyInstance]) {
gl.bind_buffer(
WebGl2RenderingContext::ARRAY_BUFFER,
Some(&self.resources.copy_instance_buffer),
);
gl.buffer_data_with_u8_array(
WebGl2RenderingContext::ARRAY_BUFFER,
bytemuck::cast_slice(instances),
WebGl2RenderingContext::DYNAMIC_DRAW,
);
}
/// Grow the alpha texture if needed.
fn maybe_resize_alphas_tex(
&mut self,
gl: &WebGl2RenderingContext,
resource_texture_dimension_2d: u32,
alphas_len: usize,
) -> Result<(), WebGlError> {
let required_alpha_height = (alphas_len as u32)
// There are 16 1-byte alpha values per texel.
.div_ceil(resource_texture_dimension_2d << 4);
let current_alpha_height = self.resources.alpha_texture_height;
if required_alpha_height > current_alpha_height {
validate_resource_texture_size(
resource_texture_dimension_2d,
required_alpha_height,
resource_texture_dimension_2d,
)?;
// We need to resize the alpha texture to fit the new alpha data.
self.resources.alphas_texture = create_data_texture_storage(
gl,
WebGl2RenderingContext::RGBA32UI,
resource_texture_dimension_2d,
required_alpha_height,
)?;
self.resources.alpha_texture_height = required_alpha_height;
}
Ok(())
}
/// Grow the encoded paints texture if needed.
fn maybe_resize_encoded_paints_tex(
&mut self,
gl: &WebGl2RenderingContext,
resource_texture_dimension_2d: u32,
paint_idxs: &[u32],
) -> Result<(), WebGlError> {
let required_texels = paint_idxs.last().unwrap();
let required_encoded_paints_height =
required_texels.div_ceil(resource_texture_dimension_2d);
let current_encoded_paints_height = self.resources.encoded_paints_texture_height;
if required_encoded_paints_height > current_encoded_paints_height {
validate_resource_texture_size(
resource_texture_dimension_2d,
required_encoded_paints_height,
resource_texture_dimension_2d,
)?;
let required_encoded_paints_size =
(resource_texture_dimension_2d * required_encoded_paints_height) << 4;
self.encoded_paints_data
.resize(required_encoded_paints_size as usize, 0);
self.resources.encoded_paints_texture = create_data_texture_storage(
gl,
WebGl2RenderingContext::RGBA32UI,
resource_texture_dimension_2d,
required_encoded_paints_height,
)?;
self.resources.encoded_paints_texture_height = required_encoded_paints_height;
}
Ok(())
}
/// Grow the gradient texture if needed.
fn maybe_resize_gradient_tex(
&mut self,
gl: &WebGl2RenderingContext,
resource_texture_dimension_2d: u32,
gradient_cache: &GradientRampCache,
) -> Result<(), WebGlError> {
if gradient_cache.is_empty() {
return Ok(());
}
let gradient_data_size = gradient_cache.luts_size();
// Each texel is RGBA8, so 4 bytes per texel
let required_gradient_height =
(gradient_data_size as u32).div_ceil(resource_texture_dimension_2d * 4);
let current_gradient_height = self.resources.gradient_texture_height;
if required_gradient_height > current_gradient_height {
validate_resource_texture_size(
resource_texture_dimension_2d,
required_gradient_height,
resource_texture_dimension_2d,
)?;
self.resources.gradient_texture = create_data_texture_storage(
gl,
WebGl2RenderingContext::RGBA8,
resource_texture_dimension_2d,
required_gradient_height,
)?;
self.resources.gradient_texture_height = required_gradient_height;
}
Ok(())
}
/// Update config buffer if dimensions changed.
fn maybe_update_config_buffer(
&mut self,
gl: &WebGl2RenderingContext,
resource_texture_dimension_2d: u32,
new_render_size: &RenderSize,
negate_ndc: bool,
) {
// TODO: Collect all attributes that influence the config buffer into a
// single struct and compare that, such that we cannot forget to update the
// condition in case we add new fields in the future.
if self.render_size != *new_render_size || self.negate_ndc != negate_ndc {
let config = Config {
width: u32::from(new_render_size.width),
height: u32::from(new_render_size.height),
strip_height: u32::from(Tile::HEIGHT),
alphas_tex_width_bits: resource_texture_dimension_2d.trailing_zeros(),
encoded_paints_tex_width_bits: resource_texture_dimension_2d.trailing_zeros(),
strip_offset_x: 0,
strip_offset_y: 0,
negate_ndc: u32::from(negate_ndc),
};
gl.bind_buffer(
WebGl2RenderingContext::UNIFORM_BUFFER,
Some(&self.resources.view_config_buffer),
);
gl.buffer_data_with_u8_array(
WebGl2RenderingContext::UNIFORM_BUFFER,
bytemuck::bytes_of(&config),
WebGl2RenderingContext::STATIC_DRAW,
);
self.render_size = new_render_size.clone();
self.negate_ndc = negate_ndc;
}
}
/// Upload alpha data to the texture.
fn upload_alpha_texture(
&mut self,
gl: &WebGl2RenderingContext,
alphas: &mut Vec<u8>,
) -> Result<(), WebGlError> {
if alphas.is_empty() {
return Ok(());
}
let alpha_texture_width = self.resources.resource_texture_dimension_2d;
// There are 16 1-byte alpha values per texel.
let used_height = (alphas.len() as u32).div_ceil(alpha_texture_width << 4);
let used_size = (alpha_texture_width as usize) * (used_height as usize) * 16;
let original_len = alphas.len();
// Temporarily pad the length of the alphas to the end of the last used row.
alphas.resize(used_size, 0);
let result = upload_rgba32ui_rows(
gl,
&self.resources.alphas_texture,
bytemuck::cast_slice::<u8, u32>(alphas),
alpha_texture_width,
used_height,
);
// Truncate back to the original size.
alphas.truncate(original_len);
result
}
/// Upload encoded paints to the texture.
fn upload_encoded_paints_texture(
&mut self,
gl: &WebGl2RenderingContext,
encoded_paints: &[GpuEncodedPaint],
paint_idxs: &[u32],
) -> Result<(), WebGlError> {
if !encoded_paints.is_empty() {
let encoded_paints_texture_width = self.resources.resource_texture_dimension_2d;
let used_texels = *paint_idxs.last().unwrap();
let used_height = used_texels.div_ceil(encoded_paints_texture_width);
let used_size = ((encoded_paints_texture_width * used_height) << 4) as usize;
GpuEncodedPaint::serialize_to_buffer(encoded_paints, &mut self.encoded_paints_data);
upload_rgba32ui_rows(
gl,
&self.resources.encoded_paints_texture,
bytemuck::cast_slice::<u8, u32>(&self.encoded_paints_data[..used_size]),
encoded_paints_texture_width,
used_height,
)?;
}
Ok(())
}
/// Upload gradient data to the texture.
fn upload_gradient_texture(
&mut self,
gl: &WebGl2RenderingContext,
gradient_cache: &mut GradientRampCache,
) -> Result<(), WebGlError> {
if gradient_cache.is_empty() {
return Ok(());
}
let gradient_texture_width = self.resources.resource_texture_dimension_2d;
// Each texel is RGBA8, so 4 bytes per texel. Only the rows covering the LUT cache are
// uploaded; see `upload_alpha_texture`.
let used_height = (gradient_cache.luts_size() as u32).div_ceil(gradient_texture_width * 4);
let used_size = (gradient_texture_width * used_height * 4) as usize;
// Take ownership of the luts to avoid copying, then pad to the end of the last used row.
let mut luts = gradient_cache.take_luts();
let old_luts_len = luts.len();
luts.resize(used_size, 0);
gl.active_texture(WebGl2RenderingContext::TEXTURE0);
gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(&self.resources.gradient_texture),
);
let result = gl
.tex_sub_image_2d_with_i32_and_i32_and_u32_and_type_and_opt_u8_array(
WebGl2RenderingContext::TEXTURE_2D,
0,
0,
0,
gradient_texture_width as i32,
used_height as i32,
WebGl2RenderingContext::RGBA,
WebGl2RenderingContext::UNSIGNED_BYTE,
Some(&luts),
)
.map_js_error(WebGlOperation::DataTransfer(
WebGlDataTransferOperation::TextureUpload,
));
// Restore the luts back to the cache.
luts.truncate(old_luts_len);
gradient_cache.restore_luts(luts);
result?;
Ok(())
}
/// Uploads two strip slices (opaque then alpha) into a single GPU buffer.
fn upload_strip_pair(
&mut self,
gl: &WebGl2RenderingContext,
opaque_strips: &[GpuStrip],
alpha_strips: RangedSlice<'_, GpuStrip>,
) {
if opaque_strips.is_empty() && alpha_strips.len() == 0 {
return;
}
gl.bind_buffer(
WebGl2RenderingContext::ARRAY_BUFFER,
Some(&self.resources.strips_buffer),
);
let opaque_len = size_of_val(opaque_strips);
let alpha_len = alpha_strips.len() * size_of::<GpuStrip>();
let total_len = opaque_len + alpha_len;
// Allocate buffer, then write both slices via bufferSubData.
// We don't want to pay for concatenating the two slices. It's better to
// simply write twice.
gl.buffer_data_with_i32(
WebGl2RenderingContext::ARRAY_BUFFER,
total_len as i32,
WebGl2RenderingContext::DYNAMIC_DRAW,
);
if !opaque_strips.is_empty() {
gl.buffer_sub_data_with_i32_and_u8_array(
WebGl2RenderingContext::ARRAY_BUFFER,
0,
bytemuck::cast_slice(opaque_strips),
);
}
let mut offset = opaque_len;
// TODO: Maybe concatenate first and upload once?
for strips in alpha_strips.slices() {
let bytes = bytemuck::cast_slice(strips);
gl.buffer_sub_data_with_i32_and_u8_array(
WebGl2RenderingContext::ARRAY_BUFFER,
offset as i32,
bytes,
);
offset += bytes.len();
}
}
}
fn required_uniform_location(
gl: &WebGl2RenderingContext,
program: &Program,
name: &'static str,
) -> Result<WebGlUniformLocation, WebGlError> {
gl.get_uniform_location(program, name)
.ok_or_else(|| WebGlError::OperationFailed {
operation: WebGlOperation::ShaderInterface(WebGlShaderInterfaceOperation::Uniform),
message: Some(name.into()),
})
}
/// Get the uniform locations for the `render_strips` program.
fn get_strip_uniforms(
gl: &WebGl2RenderingContext,
program: &Program,
) -> Result<StripUniforms, WebGlError> {
let config_vs_name = render::vertex::CONFIG;
let config_vs_block_index = gl.get_uniform_block_index(program, config_vs_name);
let config_fs_name = render::fragment::CONFIG;
let config_fs_block_index = gl.get_uniform_block_index(program, config_fs_name);
if config_vs_block_index == WebGl2RenderingContext::INVALID_INDEX {
return Err(WebGlError::OperationFailed {
operation: WebGlOperation::ShaderInterface(WebGlShaderInterfaceOperation::UniformBlock),
message: Some(config_vs_name.into()),
});
}
if config_fs_block_index == WebGl2RenderingContext::INVALID_INDEX {
return Err(WebGlError::OperationFailed {
operation: WebGlOperation::ShaderInterface(WebGlShaderInterfaceOperation::UniformBlock),
message: Some(config_fs_name.into()),
});
}
// Bind uniform blocks to binding points.
gl.uniform_block_binding(program, config_vs_block_index, 0);
gl.uniform_block_binding(program, config_fs_block_index, 0);
// Get texture uniform locations.
let alphas_texture_name = render::fragment::ALPHAS_TEXTURE;
let layer_input_texture_name = render::fragment::LAYER_INPUT_TEXTURE;
let encoded_paints_texture_fs_name = render::fragment::ENCODED_PAINTS_TEXTURE;
let encoded_paints_texture_vs_name = render::vertex::ENCODED_PAINTS_TEXTURE;
let gradient_texture_name = render::fragment::GRADIENT_TEXTURE;
// TODO: Change it so this is based on `EXTERNAL_TEXTURE_SLOT_COUNT`.
let external_texture_names = [render::fragment::EXTERNAL_TEXTURE_0];
Ok(StripUniforms {
config_vs_block_index,
config_fs_block_index,
alphas_texture: required_uniform_location(gl, program, alphas_texture_name)?,
layer_input_texture: required_uniform_location(gl, program, layer_input_texture_name)?,
encoded_paints_texture_fs: required_uniform_location(
gl,
program,
encoded_paints_texture_fs_name,
)?,
encoded_paints_texture_vs: required_uniform_location(
gl,
program,
encoded_paints_texture_vs_name,
)?,
gradient_texture: required_uniform_location(gl, program, gradient_texture_name)?,
external_textures: [required_uniform_location(
gl,
program,
external_texture_names[0],
)?],
})
}
fn get_filter_pass_uniforms(
gl: &WebGl2RenderingContext,
program: &Program,
) -> Result<FilterPassUniforms, WebGlError> {
let filter_data = required_uniform_location(gl, program, filter_shader::fragment::FILTER_DATA)?;
let source_texture =
required_uniform_location(gl, program, filter_shader::fragment::SOURCE_TEXTURE)?;
let original_texture =
required_uniform_location(gl, program, filter_shader::fragment::ORIGINAL_TEXTURE)?;
Ok(FilterPassUniforms {
filter_data,
source_texture,
original_texture,
})
}
fn get_blend_uniforms(
gl: &WebGl2RenderingContext,
program: &Program,
) -> Result<BlendUniforms, WebGlError> {
Ok(BlendUniforms {
layer_texture_0: required_uniform_location(gl, program, blend::fragment::LAYER_TEXTURE_0)?,
layer_texture_1: required_uniform_location(gl, program, blend::fragment::LAYER_TEXTURE_1)?,
alphas_texture: required_uniform_location(gl, program, blend::fragment::ALPHAS_TEXTURE)?,
})
}
fn get_copy_uniforms(
gl: &WebGl2RenderingContext,
program: &Program,
) -> Result<CopyUniforms, WebGlError> {
Ok(CopyUniforms {
source_texture: required_uniform_location(gl, program, copy::fragment::SOURCE_TEXTURE)?,
})
}
/// Derive this from the number of fields on [`FilterInstanceData`].
const FILTER_ATTRIB_COUNT: u32 = 9;
const FILTER_INSTANCE_STRIDE: i32 = size_of::<FilterInstanceData>() as i32;
fn initialize_filter_vao(gl: &WebGl2RenderingContext, resources: &WebGlResources) {
gl.bind_vertex_array(Some(&resources.filter_vao));
gl.bind_buffer(
WebGl2RenderingContext::ELEMENT_ARRAY_BUFFER,
Some(&resources.quad_index_buffer),
);
gl.bind_buffer(
WebGl2RenderingContext::ARRAY_BUFFER,
Some(&resources.filter_instance_buffer),
);
initialize_packed_u32_attribs(gl, FILTER_ATTRIB_COUNT, FILTER_INSTANCE_STRIDE);
gl.bind_vertex_array(None);
}
/// Derive this from the number of fields on [`GpuBlendInstance`].
const BLEND_ATTRIB_COUNT: u32 = 8;
const BLEND_INSTANCE_STRIDE: i32 = size_of::<GpuBlendInstance>() as i32;
fn initialize_blend_vao(gl: &WebGl2RenderingContext, resources: &WebGlResources) {
gl.bind_vertex_array(Some(&resources.blend_vao));
gl.bind_buffer(
WebGl2RenderingContext::ELEMENT_ARRAY_BUFFER,
Some(&resources.quad_index_buffer),
);
gl.bind_buffer(
WebGl2RenderingContext::ARRAY_BUFFER,
Some(&resources.blend_instance_buffer),
);
initialize_packed_u32_attribs(gl, BLEND_ATTRIB_COUNT, BLEND_INSTANCE_STRIDE);
gl.bind_vertex_array(None);
}
/// Derive this from the number of fields on [`GpuCopyInstance`].
const COPY_ATTRIB_COUNT: u32 = 4;
const COPY_INSTANCE_STRIDE: i32 = size_of::<GpuCopyInstance>() as i32;
fn initialize_copy_vao(gl: &WebGl2RenderingContext, resources: &WebGlResources) {
gl.bind_vertex_array(Some(&resources.copy_vao));
gl.bind_buffer(
WebGl2RenderingContext::ELEMENT_ARRAY_BUFFER,
Some(&resources.quad_index_buffer),
);
gl.bind_buffer(
WebGl2RenderingContext::ARRAY_BUFFER,
Some(&resources.copy_instance_buffer),
);
initialize_packed_u32_attribs(gl, COPY_ATTRIB_COUNT, COPY_INSTANCE_STRIDE);
gl.bind_vertex_array(None);
}
fn initialize_packed_u32_attribs(gl: &WebGl2RenderingContext, count: u32, stride: i32) {
for loc in 0..count {
gl.enable_vertex_attrib_array(loc);
gl.vertex_attrib_i_pointer_with_i32(
loc,
1,
WebGl2RenderingContext::UNSIGNED_INT,
stride,
i32::try_from(loc).unwrap() * size_of::<u32>() as i32,
);
gl.vertex_attrib_divisor(loc, 1);
}
}
/// Create a texture with the requested filtering and clamp-to-edge wrapping.
pub(crate) fn create_texture(
gl: &WebGl2RenderingContext,
min_filter: u32,
mag_filter: u32,
) -> Result<Texture, WebGlError> {
create_texture_inner(
gl,
WebGl2RenderingContext::TEXTURE_2D,
min_filter,
mag_filter,
)
}
fn create_texture_inner(
gl: &WebGl2RenderingContext,
target: u32,
min_filter: u32,
mag_filter: u32,
) -> Result<Texture, WebGlError> {
let texture = Texture::new(gl)?;
gl.active_texture(WebGl2RenderingContext::TEXTURE0);
gl.bind_texture(target, Some(&texture));
gl.tex_parameteri(
target,
WebGl2RenderingContext::TEXTURE_MIN_FILTER,
min_filter as i32,
);
gl.tex_parameteri(
target,
WebGl2RenderingContext::TEXTURE_MAG_FILTER,
mag_filter as i32,
);
gl.tex_parameteri(
target,
WebGl2RenderingContext::TEXTURE_WRAP_S,
WebGl2RenderingContext::CLAMP_TO_EDGE as i32,
);
gl.tex_parameteri(
target,
WebGl2RenderingContext::TEXTURE_WRAP_T,
WebGl2RenderingContext::CLAMP_TO_EDGE as i32,
);
// Strictly speaking this shouldn't be necessary since all our textures are created
// as immutable texture, but it seems like there exist drivers that have a bug here
// (https://stackoverflow.com/questions/13859061/does-an-immutable-texture-need-a-gl-texture-max-level).
// Also keep in mind the following bug: https://issues.chromium.org/issues/355605685
// We must ensure to never use mutable storage when changing this parameter.
gl.tex_parameteri(target, WebGl2RenderingContext::TEXTURE_MAX_LEVEL, 0);
Ok(texture)
}
/// Create a texture backed by immutable storage (`texStorage2D`).
pub(crate) fn create_texture_storage(
gl: &WebGl2RenderingContext,
internal_format: u32,
width: u32,
height: u32,
min_filter: u32,
mag_filter: u32,
) -> Result<Texture, WebGlError> {
let texture = create_texture(gl, min_filter, mag_filter)?;
// `create_texture` leaves the new texture bound on the active texture unit.
// Prefer `texStorage2D` over `texImage2D` for potentially better performance; see
// <https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#use_texstorage_to_create_textures>.
gl.tex_storage_2d(
WebGl2RenderingContext::TEXTURE_2D,
1,
internal_format,
width as i32,
height as i32,
);
Ok(texture)
}
/// Validate that a resource texture fits within its configured dimension limit.
fn validate_resource_texture_size(
width: u32,
height: u32,
max_dimension: u32,
) -> Result<(), WebGlError> {
if width > max_dimension || height > max_dimension {
return Err(WebGlError::ResourceTextureTooLarge {
width,
height,
max_dimension,
});
}
Ok(())
}
/// Create a zero-initialized data texture backed by immutable storage (`texStorage2D`).
fn create_data_texture_storage(
gl: &WebGl2RenderingContext,
internal_format: u32,
width: u32,
height: u32,
) -> Result<Texture, WebGlError> {
create_texture_storage(
gl,
internal_format,
width,
height,
WebGl2RenderingContext::NEAREST,
WebGl2RenderingContext::NEAREST,
)
}
/// Create a 1x1 RGBA32UI placeholder texture.
fn create_placeholder_rgba32ui_texture(gl: &WebGl2RenderingContext) -> Result<Texture, WebGlError> {
// Allocated storage keeps the placeholder texture complete when no data is available.
create_data_texture_storage(gl, WebGl2RenderingContext::RGBA32UI, 1, 1)
}
/// Create a 1x1 RGBA8 placeholder texture.
///
/// A texture with no allocated level is incomplete, and binding an incomplete texture to a sampler
/// makes the draw call non-renderable: the driver substitutes a black texture for every draw and
/// browsers log a render warning for each one. Data textures are only uploaded when a scene has
/// data for them, so allocate a minimal level 0 up front to keep them complete for scenes that
/// don't.
fn create_placeholder_rgba8_texture(gl: &WebGl2RenderingContext) -> Result<Texture, WebGlError> {
create_data_texture_storage(gl, WebGl2RenderingContext::RGBA8, 1, 1)
}
fn upload_layer_config_buffer(
gl: &WebGl2RenderingContext,
buffer: &Buffer,
size: SizeU16,
resource_texture_dimension_2d: u32,
) {
let config = Config {
width: u32::from(size.width()),
height: u32::from(size.height()),
strip_height: u32::from(Tile::HEIGHT),
alphas_tex_width_bits: resource_texture_dimension_2d.trailing_zeros(),
encoded_paints_tex_width_bits: resource_texture_dimension_2d.trailing_zeros(),
strip_offset_x: 0,
strip_offset_y: 0,
// Always use y-down when rendering to layer textures.
negate_ndc: 0,
};
gl.bind_buffer(WebGl2RenderingContext::UNIFORM_BUFFER, Some(buffer));
gl.buffer_data_with_u8_array(
WebGl2RenderingContext::UNIFORM_BUFFER,
bytemuck::bytes_of(&config),
WebGl2RenderingContext::STATIC_DRAW,
);
}
/// Create all WebGL resources needed for rendering.
fn create_webgl_resources(
gl: &WebGl2RenderingContext,
image_cache: &ImageCache,
layer_config: LayersConfig,
resource_texture_dimension_2d: u32,
use_depth_buffer: bool,
) -> Result<WebGlResources, WebGlError> {
let quad_index_buffer = Buffer::new(gl)?;
let strip_vao = VertexArray::new(gl)?;
// We use u16 for the indices instead of u8 due to better compatibility, to avoid overhead
// from any potential emulation that the driver might have to do for correctness reasons.
// D3D11 only supports u16/u32: https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-iasetindexbuffer
// Same for Metal: https://developer.apple.com/documentation/metal/mtlindextype
// In Vulkan u8 only was added in later versions: https://docs.vulkan.org/refpages/latest/refpages/source/VkIndexType.html
// ANGLE also seems to special-case on Metal, as additional evidence: https://chromium.googlesource.com/angle/angle/+/bfc764c553fa0613f858315bc6c0cc1ecee469a1/src/libANGLE/renderer/metal/VertexArrayMtl.mm#63
let quad_indices = js_sys::Uint16Array::from(&[0_u16, 1, 2, 3][..]);
// Just to avoid potentially modifying a VAO that might
// have been bound by a caller before calling Vello, since
// binding `ELEMENT_ARRAY_BUFFER` also updates VAO state.
gl.bind_vertex_array(Some(&strip_vao));
gl.bind_buffer(
WebGl2RenderingContext::ELEMENT_ARRAY_BUFFER,
Some(&quad_index_buffer),
);
gl.buffer_data_with_array_buffer_view(
WebGl2RenderingContext::ELEMENT_ARRAY_BUFFER,
&quad_indices,
WebGl2RenderingContext::STATIC_DRAW,
);
gl.bind_vertex_array(None);
let filter_vao = VertexArray::new(gl)?;
let blend_vao = VertexArray::new(gl)?;
let copy_vao = VertexArray::new(gl)?;
let filter_instance_buffer = Buffer::new(gl)?;
let blend_instance_buffer = Buffer::new(gl)?;
let copy_instance_buffer = Buffer::new(gl)?;
let strips_buffer = Buffer::new(gl)?;
let view_config_buffer = Buffer::new(gl)?;
let texture_size = layer_config.min_texture_size;
let layer_config_buffer = Buffer::new(gl)?;
upload_layer_config_buffer(
gl,
&layer_config_buffer,
texture_size,
resource_texture_dimension_2d,
);
// Create and configure alpha texture.
let alphas_texture = create_placeholder_rgba32ui_texture(gl)?;
let AtlasConfig {
atlas_size: (atlas_width, atlas_height),
initial_atlas_count,
..
} = *image_cache.atlas_manager().config();
let atlas_size = (atlas_width, atlas_height);
let atlas_textures = (0..initial_atlas_count)
.map(|_| create_atlas_texture(gl, atlas_width, atlas_height))
.collect::<Result<Vec<_>, _>>()?;
// Create and configure encoded paints texture.
let encoded_paints_texture = create_placeholder_rgba32ui_texture(gl)?;
// Create and configure gradient texture.
let gradient_texture = create_placeholder_rgba8_texture(gl)?;
let placeholder_external_texture = create_placeholder_rgba8_texture(gl)?;
let layer_textures: [Vec<WebGlIntermediateTexture>; 2] = core::array::from_fn(|_| Vec::new());
let scratch_texture = None;
let filter_data_texture = create_placeholder_rgba32ui_texture(gl)?;
Ok(WebGlResources {
quad_index_buffer,
strip_vao,
strips_buffer,
alphas_texture,
alpha_texture_height: 0,
atlas_textures,
atlas_size,
encoded_paints_texture,
encoded_paints_texture_height: 0,
gradient_texture,
placeholder_external_texture,
gradient_texture_height: 0,
view_config_buffer,
view_framebuffer: ViewFramebuffer::default(use_depth_buffer),
depth_cleared_this_frame: false,
default_depth_attachment_array: js_sys::Array::of1(&WebGl2RenderingContext::DEPTH.into()),
offscreen_depth_attachment_array: js_sys::Array::of1(
&WebGl2RenderingContext::DEPTH_ATTACHMENT.into(),
),
resource_texture_dimension_2d,
texture_size,
atlas_render_framebuffer: None,
filter_data_texture,
filter_data_texture_height: 0,
filter_instance_buffer,
filter_vao,
blend_instance_buffer,
blend_vao,
copy_instance_buffer,
copy_vao,
scratch_texture,
layer_config_buffer,
layer_textures,
})
}
fn create_intermediate_texture(
gl: &WebGl2RenderingContext,
size: SizeU16,
) -> Result<WebGlIntermediateTexture, WebGlError> {
let texture = create_texture_storage(
gl,
WebGl2RenderingContext::RGBA8,
u32::from(size.width()),
u32::from(size.height()),
WebGl2RenderingContext::LINEAR,
WebGl2RenderingContext::LINEAR,
)?;
let framebuffer = create_framebuffer_for_texture(gl, &texture)?;
Ok(WebGlIntermediateTexture {
texture,
framebuffer,
})
}
/// Create an image atlas texture.
pub(crate) fn create_atlas_texture(
gl: &WebGl2RenderingContext,
width: u16,
height: u16,
) -> Result<Texture, WebGlError> {
create_texture_storage(
gl,
WebGl2RenderingContext::RGBA8,
u32::from(width),
u32::from(height),
WebGl2RenderingContext::NEAREST,
WebGl2RenderingContext::NEAREST,
)
}
/// Create a framebuffer for a texture.
pub(crate) fn create_framebuffer_for_texture(
gl: &WebGl2RenderingContext,
texture: &WebGlTexture,
) -> Result<Framebuffer, WebGlError> {
let framebuffer = Framebuffer::new(gl)?;
gl.bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(&framebuffer));
gl.framebuffer_texture_2d(
WebGl2RenderingContext::FRAMEBUFFER,
WebGl2RenderingContext::COLOR_ATTACHMENT0,
WebGl2RenderingContext::TEXTURE_2D,
Some(texture),
0,
);
Ok(framebuffer)
}
const STRIP_STRIDE: i32 = size_of::<GpuStrip>() as i32;
const STRIP_ATTR_COUNT: i32 = STRIP_STRIDE / 4;
const _: () = assert!(
STRIP_STRIDE == 24,
"GpuStrip layout must match strip vertex stride"
);
/// Initialize strip VAO.
fn initialize_strip_vao(gl: &WebGl2RenderingContext, resources: &WebGlResources) {
gl.bind_vertex_array(Some(&resources.strip_vao));
gl.bind_buffer(
WebGl2RenderingContext::ELEMENT_ARRAY_BUFFER,
Some(&resources.quad_index_buffer),
);
gl.bind_buffer(
WebGl2RenderingContext::ARRAY_BUFFER,
Some(&resources.strips_buffer),
);
for i in 0..STRIP_ATTR_COUNT {
let location = i as u32;
let offset = i * 4;
gl.enable_vertex_attrib_array(location);
gl.vertex_attrib_i_pointer_with_i32(
location,
1,
WebGl2RenderingContext::UNSIGNED_INT,
STRIP_STRIDE,
offset,
);
gl.vertex_attrib_divisor(location, 1);
}
gl.bind_vertex_array(None);
}
/// Context for WebGL rendering operations.
// TODO: Improve buffer management. Currently a single buffer is used per resource, which means that
// the GPU must finish drawing before the next `upload_strip_pair` can be executed (effectively pausing
// execution). Investigate a buffer pool or creating a new buffer per pass.
struct WebGlRendererContext<'a> {
programs: &'a mut WebGlPrograms,
gl: &'a WebGl2RenderingContext,
texture_bindings: &'a WebGlTextureBindings,
scratch_buffers: &'a mut ScratchBuffers,
use_depth_buffer: bool,
}
fn checked_instance_count(value: usize) -> Result<i32, WebGlError> {
i32::try_from(value).map_err(|_| WebGlError::TooManyInstances)
}
impl WebGlRendererContext<'_> {
/// Draw `instance_count` quad instances using the currently bound VAO.
fn draw_instanced_quads(&self, instance_count: i32) {
// We use `drawElementsInstanced` instead of `drawArraysInstanced` due to a bug on
// older Chrome versions where integer vertex attributes aren't handled correctly.
// See https://github.com/linebender/vello/pull/1819 for more information.
self.gl.draw_elements_instanced_with_i32(
WebGl2RenderingContext::TRIANGLE_STRIP,
4,
WebGl2RenderingContext::UNSIGNED_SHORT,
0,
instance_count,
);
}
/// Draw `count` strip instances starting at `first_instance`, split into one draw per
/// external texture run.
///
/// Strip attribute offsets are restored to the start of the buffer before returning.
fn draw_strips(
&self,
external_texture_runs: &[ExternalTextureRun],
first_instance: i32,
count: i32,
) -> Result<(), WebGlError> {
if count == 0 {
return Ok(());
}
// We validate it here once so that we don't need to validate it
// again in each call of `draw_strip_range` and `set_strip_attrib_offset`.
first_instance
.checked_add(count)
.and_then(|end_instance| end_instance.checked_mul(STRIP_STRIDE))
.ok_or(WebGlError::TooManyInstances)?;
if external_texture_runs.is_empty() {
self.draw_strip_range(first_instance, count);
} else {
// A run covers the strips from its start up to the next run's start, or up to the
// end of the draw for the last run.
for (i, run) in external_texture_runs.iter().enumerate() {
let start = i32::try_from(run.strips_start).unwrap();
let end = external_texture_runs
.get(i + 1)
.map_or(count, |next| i32::try_from(next.strips_start).unwrap());
self.bind_external_textures(&run.bindings)?;
self.draw_strip_range(first_instance + start, end - start);
}
self.bind_external_textures(&ExternalTextureBindings::EMPTY)?;
}
self.set_strip_attrib_offset(0);
Ok(())
}
/// Draw `count` strip instances starting at `first_instance`.
///
/// WebGL2 has no base-instance argument, so the vertex attributes are offset to the first
/// requested instance before issuing the instanced draw.
fn draw_strip_range(&self, first_instance: i32, count: i32) {
debug_assert!(count >= 0, "strip runs must be ordered by start index");
if count == 0 {
return;
}
self.set_strip_attrib_offset(first_instance);
self.draw_instanced_quads(count);
}
/// Bind the external texture slots.
fn bind_external_textures(&self, bindings: &ExternalTextureBindings) -> Result<(), WebGlError> {
for (slot, texture_source) in bindings.as_array().into_iter().enumerate() {
self.gl.active_texture(
WebGl2RenderingContext::TEXTURE0
+ EXTERNAL_TEXTURE_UNIT_START
+ u32::try_from(slot).unwrap(),
);
let texture: &WebGlTexture = match texture_source {
Some(TextureSourceId::Atlas(atlas_id)) => {
&self.programs.resources.atlas_textures[atlas_id.as_u32() as usize]
}
Some(TextureSourceId::External(texture_id)) => self
.texture_bindings
.get(texture_id)
.ok_or(RenderError::MissingTextureBinding(texture_id))?,
None => &self.programs.resources.placeholder_external_texture,
};
self.gl
.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(texture));
}
Ok(())
}
/// Point the strip vertex attributes at the instance at `first_instance`.
fn set_strip_attrib_offset(&self, first_instance: i32) {
let byte_offset = first_instance * STRIP_STRIDE;
for i in 0..STRIP_ATTR_COUNT {
self.gl.vertex_attrib_i_pointer_with_i32(
i as u32,
1,
WebGl2RenderingContext::UNSIGNED_INT,
STRIP_STRIDE,
i * 4 + byte_offset,
);
}
}
/// Render strips to the specified render target.
fn strip_pass_inner(
&mut self,
opaque_strips: &[GpuStrip],
alpha_strips: RangedSlice<'_, GpuStrip>,
external_texture_runs: &[ExternalTextureRun],
target: DrawPassTarget,
child_layer_texture: Option<LayerTextureId>,
) -> Result<(), WebGlError> {
let opaque_count = opaque_strips.len();
let alpha_count = alpha_strips.len();
if opaque_count == 0 && alpha_count == 0 {
return Ok(());
}
match &target {
DrawPassTarget::Root(_) => {
self.gl.bind_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
self.programs.resources.view_framebuffer.binding(),
);
let width = self.programs.render_size.width;
let height = self.programs.render_size.height;
self.gl.viewport(0, 0, i32::from(width), i32::from(height));
self.gl.bind_buffer_base(
WebGl2RenderingContext::UNIFORM_BUFFER,
self.programs.strip_uniforms.config_vs_block_index,
Some(&self.programs.resources.view_config_buffer),
);
self.gl.bind_buffer_base(
WebGl2RenderingContext::UNIFORM_BUFFER,
self.programs.strip_uniforms.config_fs_block_index,
Some(&self.programs.resources.view_config_buffer),
);
}
DrawPassTarget::Layer(id) => {
self.gl.bind_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
Some(self.programs.resources.layer_framebuffer(*id)),
);
let size = self.texture_size();
self.gl
.viewport(0, 0, i32::from(size.width()), i32::from(size.height()));
let buf = &self.programs.resources.layer_config_buffer;
self.gl.bind_buffer_base(
WebGl2RenderingContext::UNIFORM_BUFFER,
self.programs.strip_uniforms.config_vs_block_index,
Some(buf),
);
self.gl.bind_buffer_base(
WebGl2RenderingContext::UNIFORM_BUFFER,
self.programs.strip_uniforms.config_fs_block_index,
Some(buf),
);
}
};
self.gl.disable(WebGl2RenderingContext::SCISSOR_TEST);
// Use the strip program.
self.gl.use_program(Some(&self.programs.strip_program));
// Set up attributes.
self.gl
.bind_vertex_array(Some(&self.programs.resources.strip_vao));
// Bind textures.
self.gl.active_texture(WebGl2RenderingContext::TEXTURE0);
self.gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(&self.programs.resources.alphas_texture),
);
self.gl
.uniform1i(Some(&self.programs.strip_uniforms.alphas_texture), 0);
self.gl.active_texture(WebGl2RenderingContext::TEXTURE1);
self.gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(child_layer_texture.map_or_else(
|| &self.programs.resources.placeholder_external_texture,
|id| self.programs.resources.layer_texture(id),
)),
);
self.gl
.uniform1i(Some(&self.programs.strip_uniforms.layer_input_texture), 1);
// Bind encoded paints texture for image metadata
self.gl.active_texture(WebGl2RenderingContext::TEXTURE2);
self.gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(&self.programs.resources.encoded_paints_texture),
);
self.gl.uniform1i(
Some(&self.programs.strip_uniforms.encoded_paints_texture_fs),
2,
);
self.gl.uniform1i(
Some(&self.programs.strip_uniforms.encoded_paints_texture_vs),
2,
);
// Bind gradient texture for gradient rendering
self.gl.active_texture(WebGl2RenderingContext::TEXTURE3);
self.gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(&self.programs.resources.gradient_texture),
);
self.gl
.uniform1i(Some(&self.programs.strip_uniforms.gradient_texture), 3);
// External textures are rebound per run while drawing. Start from the placeholder so the
// sampler is valid for draws that don't reference one.
self.bind_external_textures(&ExternalTextureBindings::EMPTY)?;
for (slot, uniform) in self
.programs
.strip_uniforms
.external_textures
.iter()
.enumerate()
{
self.gl.uniform1i(
Some(uniform),
i32::try_from(EXTERNAL_TEXTURE_UNIT_START).unwrap() + i32::try_from(slot).unwrap(),
);
}
// TODO: Today, we only support early-z rejection on the final view. If we wanted to support
// intermediate layers, we would require separate depth buffers for each target. We can explore
// that possibility in the future.
let enable_opaque = self.use_depth_buffer && target.enable_opaque();
self.programs
.upload_strip_pair(self.gl, opaque_strips, alpha_strips);
let opaque_count = checked_instance_count(opaque_count)?;
let alpha_count = checked_instance_count(alpha_count)?;
if enable_opaque {
self.gl.enable(WebGl2RenderingContext::DEPTH_TEST);
self.gl.depth_mask(true);
self.gl.depth_func(WebGl2RenderingContext::LEQUAL);
// Clear depth buffer on first use per frame.
if !self.programs.resources.depth_cleared_this_frame {
self.programs.resources.depth_cleared_this_frame = true;
self.gl.clear_depth(1.0);
self.gl.clear(WebGl2RenderingContext::DEPTH_BUFFER_BIT);
}
// Opaque pass: front-to-back, depth test ON, depth write ON, blend OFF.
if opaque_count > 0 {
self.gl.enable(WebGl2RenderingContext::DEPTH_TEST);
self.gl.depth_mask(true);
self.gl.disable(WebGl2RenderingContext::BLEND);
self.draw_strips(external_texture_runs, 0, opaque_count)?;
}
// Alpha pass: back-to-front, depth test ON, depth write OFF, blend ON.
if alpha_count > 0 {
self.gl.enable(WebGl2RenderingContext::DEPTH_TEST);
self.gl.depth_mask(false);
self.gl.enable(WebGl2RenderingContext::BLEND);
self.draw_strips(external_texture_runs, opaque_count, alpha_count)?;
}
// Restore state.
self.gl.disable(WebGl2RenderingContext::DEPTH_TEST);
self.gl.depth_mask(true);
self.gl.enable(WebGl2RenderingContext::BLEND);
} else {
// Intermediate target: single draw with blending, no depth.
debug_assert_eq!(
opaque_count, 0,
"opaque strips require the final view's depth attachment"
);
self.gl.disable(WebGl2RenderingContext::DEPTH_TEST);
self.gl.depth_mask(false);
self.gl.enable(WebGl2RenderingContext::BLEND);
let total_count = opaque_count
.checked_add(alpha_count)
.ok_or(WebGlError::TooManyInstances)?;
self.draw_strips(external_texture_runs, 0, total_count)?;
}
// Clean up.
self.gl.bind_vertex_array(None);
Ok(())
}
fn texture_size(&self) -> SizeU16 {
self.programs.resources.texture_size
}
fn blend_pass_inner(
&mut self,
blends: RangedSlice<'_, BlendOp>,
blend_strips: &[BlendStrip],
bindings: BlendPassBindings,
) -> Result<(), WebGlError> {
let texture_size = self.texture_size();
self.gl.disable(WebGl2RenderingContext::BLEND);
self.gl.disable(WebGl2RenderingContext::SCISSOR_TEST);
self.gl.disable(WebGl2RenderingContext::DEPTH_TEST);
self.gl.depth_mask(false);
self.gl
.bind_vertex_array(Some(&self.programs.resources.blend_vao));
self.scratch_buffers.blend_instances.clear();
for blend in blends.iter() {
if let Some(range) = &blend.clip_strips {
self.scratch_buffers.blend_instances.extend(
blend_strips[usize::try_from(range.start).unwrap()
..usize::try_from(range.end).unwrap()]
.iter()
.copied()
.map(|strip| GpuBlendInstance::new(blend, Some(strip), texture_size)),
);
} else {
self.scratch_buffers
.blend_instances
.push(GpuBlendInstance::new(blend, None, texture_size));
}
}
self.programs
.upload_blend_instances(self.gl, &self.scratch_buffers.blend_instances);
let instance_count = checked_instance_count(self.scratch_buffers.blend_instances.len())?;
self.gl.bind_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
Some(self.programs.resources.scratch_framebuffer()),
);
self.gl.viewport(
0,
0,
i32::from(texture_size.width()),
i32::from(texture_size.height()),
);
self.gl.use_program(Some(&self.programs.blend_program));
self.gl.active_texture(WebGl2RenderingContext::TEXTURE2);
self.gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(&self.programs.resources.alphas_texture),
);
self.gl
.uniform1i(Some(&self.programs.blend_uniforms.alphas_texture), 2);
self.gl.active_texture(WebGl2RenderingContext::TEXTURE0);
self.gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(
self.programs
.resources
.layer_texture(bindings.layer_id(TextureParity::Even)),
),
);
self.gl
.uniform1i(Some(&self.programs.blend_uniforms.layer_texture_0), 0);
self.gl.active_texture(WebGl2RenderingContext::TEXTURE1);
self.gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(
self.programs
.resources
.layer_texture(bindings.layer_id(TextureParity::Odd)),
),
);
self.gl
.uniform1i(Some(&self.programs.blend_uniforms.layer_texture_1), 1);
self.draw_instanced_quads(instance_count);
self.scratch_buffers.copy_instances.clear();
self.scratch_buffers.copy_instances.extend(
self.scratch_buffers
.blend_instances
.iter()
.copied()
.map(GpuBlendInstance::copy_from_scratch),
);
self.programs
.upload_copy_instances(self.gl, &self.scratch_buffers.copy_instances);
self.gl.disable(WebGl2RenderingContext::BLEND);
self.gl.disable(WebGl2RenderingContext::SCISSOR_TEST);
self.gl.disable(WebGl2RenderingContext::DEPTH_TEST);
self.gl.depth_mask(false);
self.gl.bind_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
Some(self.programs.resources.layer_framebuffer(bindings.target())),
);
self.gl.viewport(
0,
0,
i32::from(texture_size.width()),
i32::from(texture_size.height()),
);
self.gl
.bind_vertex_array(Some(&self.programs.resources.copy_vao));
self.gl.use_program(Some(&self.programs.copy_program));
self.gl.active_texture(WebGl2RenderingContext::TEXTURE0);
self.gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(self.programs.resources.scratch_binding_texture()),
);
self.gl
.uniform1i(Some(&self.programs.copy_uniforms.source_texture), 0);
self.draw_instanced_quads(instance_count);
self.gl.bind_vertex_array(None);
Ok(())
}
fn filter_pass_inner(
&mut self,
plan: &FilterPassPlan,
bindings: FilterPassBindings,
) -> Result<(), WebGlError> {
if let Some(copy_pass) = plan.copy_pass() {
self.programs.upload_copy_instances(self.gl, copy_pass);
self.gl.disable(WebGl2RenderingContext::BLEND);
self.gl.disable(WebGl2RenderingContext::SCISSOR_TEST);
self.gl.disable(WebGl2RenderingContext::DEPTH_TEST);
self.gl.depth_mask(false);
self.gl
.bind_vertex_array(Some(&self.programs.resources.copy_vao));
self.gl.use_program(Some(&self.programs.copy_program));
self.gl.bind_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
Some(self.programs.resources.scratch_framebuffer()),
);
let scratch_size = self.texture_size();
self.gl.viewport(
0,
0,
i32::from(scratch_size.width()),
i32::from(scratch_size.height()),
);
self.gl.active_texture(WebGl2RenderingContext::TEXTURE0);
self.gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(self.programs.resources.layer_texture(bindings.target())),
);
self.gl
.uniform1i(Some(&self.programs.copy_uniforms.source_texture), 0);
self.draw_instanced_quads(checked_instance_count(copy_pass.len())?);
}
self.gl.use_program(Some(&self.programs.filter_program));
self.gl
.bind_vertex_array(Some(&self.programs.resources.filter_vao));
self.gl.active_texture(WebGl2RenderingContext::TEXTURE0);
self.gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(&self.programs.resources.filter_data_texture),
);
self.gl
.uniform1i(Some(&self.programs.filter_uniforms.filter_data), 0);
self.gl.active_texture(WebGl2RenderingContext::TEXTURE2);
self.gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(self.programs.resources.scratch_binding_texture()),
);
self.gl
.uniform1i(Some(&self.programs.filter_uniforms.original_texture), 2);
for (step_index, instances) in plan.steps().enumerate() {
self.do_filter_instance_pass(
instances,
bindings.input(step_index),
bindings.output(step_index),
)?;
}
self.gl.bind_vertex_array(None);
Ok(())
}
fn do_filter_instance_pass(
&self,
instances: &[FilterInstanceData],
input: LayerTextureId,
output: LayerTextureId,
) -> Result<(), WebGlError> {
self.gl.disable(WebGl2RenderingContext::BLEND);
self.gl.disable(WebGl2RenderingContext::SCISSOR_TEST);
self.gl.disable(WebGl2RenderingContext::DEPTH_TEST);
self.gl.depth_mask(false);
self.gl.bind_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
Some(self.programs.resources.layer_framebuffer(output)),
);
let target_texture_size = self.texture_size();
self.gl.viewport(
0,
0,
i32::from(target_texture_size.width()),
i32::from(target_texture_size.height()),
);
self.gl.active_texture(WebGl2RenderingContext::TEXTURE1);
self.gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(self.programs.resources.layer_texture(input)),
);
self.gl
.uniform1i(Some(&self.programs.filter_uniforms.source_texture), 1);
// TODO: Filter instances ideally should be uploaded once for the whole round (or even once
// globally), not per pass.
self.programs.upload_filter_instances(self.gl, instances);
self.draw_instanced_quads(checked_instance_count(instances.len())?);
Ok(())
}
fn clear_pass_inner(&self, target: DrawPassTarget, settings: ClearSettings<'_>) {
let color = settings.clear_color();
let negate_ndc = target.negate_ndc();
let (width, height) = match target {
DrawPassTarget::Root(_) => {
self.gl.bind_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
self.programs.resources.view_framebuffer.binding(),
);
(
self.programs.render_size.width,
self.programs.render_size.height,
)
}
DrawPassTarget::Layer(target) => {
let size = self.texture_size();
let framebuffer = self.programs.resources.layer_framebuffer(target);
self.gl
.bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(framebuffer));
(size.width(), size.height())
}
};
let [r, g, b, a] = color.premultiply().components;
self.gl.clear_color(r, g, b, a);
match settings {
ClearSettings::Viewport { .. } => {
self.gl.disable(WebGl2RenderingContext::SCISSOR_TEST);
self.gl.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT);
}
ClearSettings::Rects { rects, .. } => {
self.gl.enable(WebGl2RenderingContext::SCISSOR_TEST);
let bounds = RectU16::new(0, 0, width, height);
// It's unclear whether it's better to scissor + clear repeatedly, or to do
// one instanced clear pass via a shader. But using this approach should give the
// GPU more semantic information about what we want to do, so it seems reasonable
// to assume that this is better.
for rect in rects
.iter()
.map(|rect| rect.intersect(bounds))
.filter(|rect| !rect.is_empty())
{
let y = if negate_ndc {
height - rect.y1
} else {
rect.y0
};
self.gl.scissor(
i32::from(rect.x0),
i32::from(y),
i32::from(rect.width()),
i32::from(rect.height()),
);
self.gl.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT);
}
self.gl.disable(WebGl2RenderingContext::SCISSOR_TEST);
}
}
}
}
impl Backend for WebGlRendererContext<'_> {
type Error = WebGlError;
fn opaque_draw_pass(
&mut self,
strips: &[GpuStrip],
external_texture_runs: &[ExternalTextureRun],
) -> Result<(), Self::Error> {
self.strip_pass_inner(
strips,
RangedSlice::empty(),
external_texture_runs,
DrawPassTarget::Root(RootTarget::UserSurface),
None,
)
}
fn draw_pass(
&mut self,
strips: RangedSlice<'_, GpuStrip>,
external_texture_runs: &[ExternalTextureRun],
bindings: DrawPassBindings,
) -> Result<(), Self::Error> {
self.strip_pass_inner(
&[],
strips,
external_texture_runs,
bindings.target,
bindings.child,
)
}
fn blend_pass(
&mut self,
blends: RangedSlice<'_, BlendOp>,
blend_strips: &[BlendStrip],
bindings: BlendPassBindings,
) -> Result<(), Self::Error> {
self.blend_pass_inner(blends, blend_strips, bindings)
}
fn filter_pass(
&mut self,
plan: &FilterPassPlan,
bindings: FilterPassBindings,
) -> Result<(), Self::Error> {
self.filter_pass_inner(plan, bindings)
}
fn clear_pass(&mut self, target: LayerTextureId, rects: &[RectU16]) -> Result<(), Self::Error> {
self.clear_pass_inner(
DrawPassTarget::Layer(target),
ClearSettings::Rects {
color: AlphaColor::TRANSPARENT,
rects,
},
);
Ok(())
}
}
/// Trait for types that can write image data directly to the atlas texture in WebGL.
///
/// This allows efficient uploading from different sources:
/// - `Pixmap`: Direct upload using raw pixel data
/// - `ImageData`: Browser `ImageData` objects
/// - Custom implementations for other image sources
pub trait WebGlAtlasWriter {
/// Get the width of the image.
fn width(&self) -> u16;
/// Get the height of the image.
fn height(&self) -> u16;
/// Write image data to an atlas texture at the specified offset.
fn write_to_atlas(
&self,
gl: &WebGl2RenderingContext,
atlas_texture: &WebGlTexture,
offset: [u16; 2],
width: u16,
height: u16,
) -> Result<(), WebGlError>;
}
/// Implementation for `Pixmap` - direct upload using raw pixel data.
impl WebGlAtlasWriter for Pixmap {
fn width(&self) -> u16 {
self.width()
}
fn height(&self) -> u16 {
self.height()
}
fn write_to_atlas(
&self,
gl: &WebGl2RenderingContext,
atlas_texture: &WebGlTexture,
offset: [u16; 2],
width: u16,
height: u16,
) -> Result<(), WebGlError> {
gl.active_texture(WebGl2RenderingContext::TEXTURE0);
gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(atlas_texture));
// Convert pixmap data to the format expected by WebGL
let rgba_data = self.data_as_u8_slice();
gl.tex_sub_image_2d_with_i32_and_i32_and_u32_and_type_and_opt_u8_array(
WebGl2RenderingContext::TEXTURE_2D,
0,
offset[0] as i32,
offset[1] as i32,
width as i32,
height as i32,
WebGl2RenderingContext::RGBA,
WebGl2RenderingContext::UNSIGNED_BYTE,
Some(rgba_data),
)
.map_js_error(WebGlOperation::DataTransfer(
WebGlDataTransferOperation::TextureUpload,
))?;
Ok(())
}
}
/// Implementation for `Arc<Pixmap>`.
impl WebGlAtlasWriter for Arc<Pixmap> {
fn width(&self) -> u16 {
self.as_ref().width()
}
fn height(&self) -> u16 {
self.as_ref().height()
}
fn write_to_atlas(
&self,
gl: &WebGl2RenderingContext,
atlas_texture: &WebGlTexture,
offset: [u16; 2],
width: u16,
height: u16,
) -> Result<(), WebGlError> {
self.as_ref()
.write_to_atlas(gl, atlas_texture, offset, width, height)
}
}
/// Implementation for `WebGlTexture` - texture-to-texture copy.
impl WebGlAtlasWriter for WebGlTexture {
fn width(&self) -> u16 {
// WebGL textures don't expose their dimensions directly
// This is a limitation - in practice, you'd need to track dimensions separately
// For now, we'll require the caller to provide correct width/height parameters
unreachable!("WebGlTexture width must be provided by caller")
}
fn height(&self) -> u16 {
// WebGL textures don't expose their dimensions directly
// This is a limitation - in practice, you'd need to track dimensions separately
// For now, we'll require the caller to provide correct width/height parameters
unreachable!("WebGlTexture height must be provided by caller")
}
fn write_to_atlas(
&self,
gl: &WebGl2RenderingContext,
atlas_texture: &WebGlTexture,
offset: [u16; 2],
width: u16,
height: u16,
) -> Result<(), WebGlError> {
copy_to_texture(
gl,
|gl| {
// Attach source texture to read framebuffer
gl.framebuffer_texture_2d(
WebGl2RenderingContext::READ_FRAMEBUFFER,
WebGl2RenderingContext::COLOR_ATTACHMENT0,
WebGl2RenderingContext::TEXTURE_2D,
Some(self),
0,
);
},
atlas_texture,
[u32::from(offset[0]), u32::from(offset[1])],
[u32::from(width), u32::from(height)],
)
}
}
/// Wrapper for `WebGlTexture` with known dimensions.
#[derive(Debug)]
pub struct WebGlTextureWithDimensions {
/// The WebGL texture.
pub texture: WebGlTexture,
/// The width of the texture.
pub width: u16,
/// The height of the texture.
pub height: u16,
}
impl WebGlAtlasWriter for WebGlTextureWithDimensions {
fn width(&self) -> u16 {
self.width
}
fn height(&self) -> u16 {
self.height
}
fn write_to_atlas(
&self,
gl: &WebGl2RenderingContext,
atlas_texture: &WebGlTexture,
offset: [u16; 2],
width: u16,
height: u16,
) -> Result<(), WebGlError> {
self.texture
.write_to_atlas(gl, atlas_texture, offset, width, height)
}
}
/// Copy from a source texture/framebuffer to an atlas texture.
fn copy_to_texture(
gl: &WebGl2RenderingContext,
source_setup: impl FnOnce(&WebGl2RenderingContext),
dest_texture: &WebGlTexture,
dest_offset: [u32; 2],
copy_size: [u32; 2],
) -> Result<(), WebGlError> {
gl.active_texture(WebGl2RenderingContext::TEXTURE0);
let read_framebuffer = Framebuffer::new(gl)?;
gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(dest_texture));
// Bind the READ framebuffer
gl.bind_framebuffer(
WebGl2RenderingContext::READ_FRAMEBUFFER,
Some(&read_framebuffer),
);
// Let the caller set up the source (attach texture/layer to read framebuffer)
source_setup(gl);
gl.copy_tex_sub_image_2d(
WebGl2RenderingContext::TEXTURE_2D,
0,
dest_offset[0] as i32,
dest_offset[1] as i32,
0,
0,
copy_size[0] as i32,
copy_size[1] as i32,
);
Ok(())
}
/// Upload full rows of RGBA32UI data into immutable texture storage.
///
/// `data` must contain exactly `width * rows * 4` `u32` values.
/// Texels outside the uploaded rows remain unchanged.
fn upload_rgba32ui_rows(
gl: &WebGl2RenderingContext,
texture: &WebGlTexture,
data: &[u32],
width: u32,
rows: u32,
) -> Result<(), WebGlError> {
debug_assert_eq!(
data.len(),
(width * rows * 4) as usize,
"row upload data must cover exactly the uploaded region"
);
gl.active_texture(WebGl2RenderingContext::TEXTURE0);
gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(texture));
// Safety: This calling `Uint32Array::view` is unsafe because it provides a view into
// WASM linear memory, and any additional allocations might invalidate that view.
// In our case, this is not an issue because we only use this view once for uploading
// data to the GPU below, and no allocations happen between that.
// The `tex_sub_image_2d` method is synchronous in the sense that once it returns, it is
// guaranteed that all necessary data has already been read, so any allocations that happen
// after this block don't affect this anymore.
//
// See also: https://wikis.khronos.org/opengl/Synchronization
// >> There are several OpenGL functions that can pull data directly from client-side memory,
// >> or push data directly into client-side memory. Functions like `glTexSubImage2D`,
// >> `glReadPixels`, `glBufferSubData` and so forth.
//
// >> Because OpenGL is defined to be synchronous, when any of these functions have
// >> returned, they must have finished with the client memory. When `glReadPixels` returns,
// >> the pixel data is in your client memory (unless you are reading into a buffer object).
// >> When `glBufferSubData` returns, you can immediately modify or delete whatever memory
// >> pointer you gave it, as OpenGL has already read as much as it wants.
let packed_array = unsafe { js_sys::Uint32Array::view(data) };
gl.tex_sub_image_2d_with_i32_and_i32_and_u32_and_type_and_opt_array_buffer_view(
WebGl2RenderingContext::TEXTURE_2D,
0,
0,
0,
width as i32,
rows as i32,
WebGl2RenderingContext::RGBA_INTEGER,
WebGl2RenderingContext::UNSIGNED_INT,
Some(&packed_array),
)
.map_js_error(WebGlOperation::DataTransfer(
WebGlDataTransferOperation::TextureUpload,
))?;
Ok(())
}
impl DrawPassTarget {
fn negate_ndc(self) -> bool {
// Only negate if we are rendering to the main frame buffer.
matches!(self, Self::Root(RootTarget::UserSurface))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resource_texture_size_must_fit_dimension_limit() {
assert!(validate_resource_texture_size(4096, 4096, 4096).is_ok());
assert!(matches!(
validate_resource_texture_size(4096, 4097, 4096),
Err(WebGlError::ResourceTextureTooLarge {
width: 4096,
height: 4097,
max_dimension: 4096,
})
));
}
}