blob: ad3b7d0eb94d9ad731ebcdcda4a8f355d6ed497d [file]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Native WebGL2 rendering module for the sparse strips CPU/GPU rendering engine.
//!
//! This module provides identical functionality as the [`render_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."
)]
use crate::render::common::IMAGE_PADDING;
use crate::{
GpuStrip, RenderError, RenderSettings, RenderSize, Resources,
filter::{FilterContext, FilterInstanceData, FilterPassState, FilterPassTarget},
gradient_cache::GradientRampCache,
render::{
Config,
common::{
GPU_ENCODED_IMAGE_SIZE_TEXELS, GPU_LINEAR_GRADIENT_SIZE_TEXELS,
GPU_RADIAL_GRADIENT_SIZE_TEXELS, GPU_SWEEP_GRADIENT_SIZE_TEXELS, GpuEncodedImage,
GpuEncodedPaint, GpuLinearGradient, GpuRadialGradient, GpuSweepGradient,
normalize_atlas_config, 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::{
LoadOp, RendererBackend, RootRenderTarget, Scheduler, SchedulerState, StripPassRenderTarget,
},
};
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
use bytemuck::{Pod, Zeroable};
use core::fmt::Debug;
#[cfg(feature = "text")]
use glifo::{GLYPH_PADDING, PendingClearRect};
use vello_common::image_cache::{ImageCache, ImageResource};
#[cfg(feature = "probe")]
use vello_common::multi_atlas::AllocationStrategy;
use vello_common::multi_atlas::{AtlasConfig, AtlasId};
#[cfg(feature = "probe")]
use vello_common::probe::Probe;
use vello_common::render_graph::LayerId;
use vello_common::{
coarse::WideTile,
encode::{EncodedGradient, EncodedKind, EncodedPaint, MAX_GRADIENT_LUT_SIZE, RadialKind},
paint::{ImageId, ImageSource},
peniko::{self},
pixmap::Pixmap,
tile::Tile,
};
use vello_sparse_shaders::{
clear_slots, filters, opaque_gradient, opaque_image, opaque_image_untinted, opaque_solid,
render_strips,
};
use web_sys::wasm_bindgen::{JsCast, JsValue};
use web_sys::{
HtmlCanvasElement, WebGl2RenderingContext, WebGlBuffer, WebGlFramebuffer, WebGlProgram,
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],
});
/// Query the WebGL context for the max texture size.
fn get_max_texture_dimension_2d(gl: &WebGl2RenderingContext) -> u32 {
gl.get_parameter(WebGl2RenderingContext::MAX_TEXTURE_SIZE)
.unwrap()
.as_f64()
.unwrap() as u32
}
fn get_max_texture_array_layers(gl: &WebGl2RenderingContext) -> u32 {
gl.get_parameter(WebGl2RenderingContext::MAX_ARRAY_TEXTURE_LAYERS)
.unwrap()
.as_f64()
.unwrap() as u32
}
/// Vello Hybrid's WebGL2 Renderer.
#[derive(Debug)]
pub struct WebGlRenderer {
/// Programs for rendering.
programs: WebGlPrograms,
/// Scheduler for scheduling draws.
scheduler: Scheduler,
/// The state used by the scheduler.
scheduler_state: SchedulerState,
/// WebGL context.
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,
/// Context for GPU filter effects.
filter_context: FilterContext,
/// State used for constructing filter passes.
filter_pass_state: FilterPassState,
dummy_image_cache: Option<ImageCache>,
}
impl WebGlRenderer {
/// Creates a new WebGL2 renderer
pub fn new(canvas: &HtmlCanvasElement) -> Self {
Self::new_with(canvas, RenderSettings::default())
}
/// Creates a new WebGL2 renderer with specific settings.
pub fn new_with(canvas: &HtmlCanvasElement, settings: RenderSettings) -> Self {
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).unwrap();
// Vello only supports 24+ bit depth buffers. 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.
js_sys::Reflect::set(&context_options, &"depth".into(), &JsValue::TRUE).unwrap();
let gl = canvas
.get_context_with_context_options("webgl2", &context_options)
.expect("WebGL2 context to be available")
.unwrap()
.dyn_into::<WebGl2RenderingContext>()
.expect("Context to be a WebGL2 context");
let cloned_gl = gl.clone();
let _state_guard = WebGlStateGuard::with_config(
&cloned_gl,
WebGlStateConfig {
framebuffer: true,
..Default::default()
},
);
// 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().unwrap();
let antialias = js_sys::Reflect::get(&context_attributes, &"antialias".into())
.unwrap()
.as_bool()
.unwrap();
debug_assert!(
!antialias,
"WebGL context must be created with `antialias: false` for vello_hybrid to work correctly."
);
}
let mut settings = settings;
let max_texture_dimension_2d = get_max_texture_dimension_2d(&gl);
normalize_atlas_config(
&mut settings.atlas_config,
max_texture_dimension_2d,
get_max_texture_array_layers(&gl),
1,
);
let total_slots: usize = (max_texture_dimension_2d / u32::from(Tile::HEIGHT)) as usize;
assert!(
gl.get_parameter(WebGl2RenderingContext::DEPTH_BITS)
.unwrap()
.as_f64()
.unwrap()
>= 24.0,
"Depth buffer must be at least 24 bits"
);
let image_cache = ImageCache::new_with_config(settings.atlas_config);
// Estimate the maximum number of gradient cache entries based on the max texture dimension
// and the maximum gradient LUT size - worst case scenario.
let max_gradient_cache_size =
max_texture_dimension_2d * max_texture_dimension_2d / MAX_GRADIENT_LUT_SIZE as u32;
let gradient_cache = GradientRampCache::new(max_gradient_cache_size, settings.level);
let filter_context = FilterContext::new(settings.atlas_config);
Self {
programs: WebGlPrograms::new(gl.clone(), &image_cache, &filter_context, total_slots),
scheduler: Scheduler::new(total_slots),
scheduler_state: SchedulerState::default(),
gl,
encoded_paints: Vec::new(),
paint_idxs: Vec::new(),
gradient_cache,
filter_context,
filter_pass_state: FilterPassState::default(),
dummy_image_cache: Some(ImageCache::new_dummy()),
}
}
/// Render `scene` using WebGL2
///
/// This method creates GPU resources as needed and schedules potentially multiple draw calls.
pub fn render(
&mut self,
scene: &Scene,
resources: &mut Resources,
render_size: &RenderSize,
) -> Result<(), RenderError> {
debug_assert_eq!(
RenderSize {
width: self.gl.drawing_buffer_width() as u32,
height: self.gl.drawing_buffer_height() as u32
},
*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)
.expect("Failed to render glyphs to atlas");
},
|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,
&mut resources.image_cache,
render_size,
true,
RootRenderTarget::UserSurface,
)?;
#[cfg(feature = "text")]
{
resources.after_render(self, |renderer, rect| {
clear_atlas_region(renderer, rect);
});
}
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`).
#[doc(hidden)]
pub fn render_to_atlas(
&mut self,
scene: &Scene,
atlas_count: u32,
atlas_config: AtlasConfig,
atlas_id: AtlasId,
) -> Result<(), RenderError> {
self.programs
.maybe_resize_atlas_texture_array(&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 atlas_framebuffer = self
.programs
.resources
.atlas_render_framebuffer
.take()
.unwrap_or_else(|| self.gl.create_framebuffer().unwrap());
self.gl.bind_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
Some(&atlas_framebuffer),
);
self.gl.framebuffer_texture_layer(
WebGl2RenderingContext::FRAMEBUFFER,
WebGl2RenderingContext::COLOR_ATTACHMENT0,
Some(&self.programs.resources.atlas_texture_array.texture),
0,
atlas_id.as_u32() as i32,
);
// Set the view framebuffer override so the scheduler renders to the
// atlas layer instead of the default framebuffer.
self.programs.resources.view_framebuffer_override = Some(atlas_framebuffer);
// Swap in the stub atlas texture array to avoid binding the real atlas
// texture as a shader input while it is also the render target.
core::mem::swap(
&mut self.programs.resources.atlas_texture_array,
&mut self.programs.resources.stub_atlas_texture_array,
);
// TODO: Explore using an option instead of a dummy image cache.
let mut dummy_image_cache = self
.dummy_image_cache
.take()
.expect("dummy image cache must exist");
let result = self.render_scene(
scene,
&mut dummy_image_cache,
&atlas_render_size,
false,
RootRenderTarget::AtlasLayer,
);
self.dummy_image_cache = Some(dummy_image_cache);
// Restore the real atlas texture array.
core::mem::swap(
&mut self.programs.resources.atlas_texture_array,
&mut self.programs.resources.stub_atlas_texture_array,
);
// Restore the default view framebuffer and cache the atlas FBO for reuse.
self.programs.resources.atlas_render_framebuffer =
self.programs.resources.view_framebuffer_override.take();
result
}
/// Conduct a probing operation.
///
/// The WebGL drivers of certain devices are known to be buggy and might therefore not work correctly
/// with Vello Hybrid. In the best case, it will simply result in a program crash, but in the worst
/// case it can instead result in a silent failure, meaning that no explicit error is
/// thrown, but the rendered contents of Vello Hybrid will either be completely empty or look glitchy.
///
/// The purpose of this method is to run a sanity check to ensure that running Vello Hybrid on this
/// device actually results in visible and correct output. How this achieved is by drawing a selection
/// of small elements into a small canvas, and comparing the final output against a reference image.
///
/// If certain pixels in the final output deviate too much from the expected image, an error result
/// will be returned, containing the expected image and the actual image. If probe rendering itself
/// fails, [`Probe::RenderError`] will be returned with the corresponding [`RenderError`]. Otherwise,
/// [`Probe::Success`] will be returned, indicating that the device seems to be compatible with
/// Vello Hybrid.
///
/// **Important:** Note that this method can be expensive to call, as it performs a small rendering
/// operation and also does readback of the pixels to the CPU. Expect it to take anywhere from 10ms
/// to 100ms+. This should be taken into consideration when deciding when and how to call this method,
/// to prevent noticeable stalls on the main thread.
#[cfg(feature = "probe")]
pub fn probe(&mut self) -> Probe<RenderError> {
match self.probe_inner() {
Ok(actual) => Probe::from_actual(actual),
Err(error) => Probe::RenderError(error),
}
}
#[cfg(feature = "probe")]
fn probe_inner(&mut self) -> Result<Pixmap, RenderError> {
let (width, height) = vello_common::probe::canvas_size();
let render_size = RenderSize {
width: u32::from(width),
height: u32::from(height),
};
let probe_texture = create_texture(&self.gl);
self.gl
.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view(
WebGl2RenderingContext::TEXTURE_2D,
0,
WebGl2RenderingContext::RGBA8 as i32,
render_size.width as i32,
render_size.height as i32,
0,
WebGl2RenderingContext::RGBA,
WebGl2RenderingContext::UNSIGNED_BYTE,
None,
)
.unwrap();
let probe_framebuffer = create_framebuffer_for_texture(&self.gl, &probe_texture);
let atlas_config = AtlasConfig {
initial_atlas_count: 1,
// These should be large enough for the probe scene.
atlas_size: (256, 256),
max_atlases: 1,
auto_grow: true,
allocation_strategy: AllocationStrategy::FirstFit,
};
let (atlas_width, atlas_height) = atlas_config.atlas_size;
let mut probe_image_cache = ImageCache::new_with_config(atlas_config);
let mut probe_atlas_texture_array =
create_atlas_texture_array(&self.gl, atlas_width, atlas_height, 1);
core::mem::swap(
&mut self.programs.resources.atlas_texture_array,
&mut probe_atlas_texture_array,
);
let probe_image = Arc::new(vello_common::probe::probe_image_pixmap());
// Note: No need to destroy the image explicitly in the end, because we discard the image
// cache anyway.
let probe_image_id =
self.upload_image_with(&mut probe_image_cache, &probe_image, IMAGE_PADDING);
let mut scene = Scene::new(width, height);
vello_common::probe::draw_scene(
&mut scene,
ImageSource::opaque_id_with_opacity_hint(
probe_image_id,
probe_image.may_have_opacities(),
),
);
let previous_view_framebuffer = self
.programs
.resources
.view_framebuffer_override
.replace(probe_framebuffer.clone());
let render_result = self.render_scene(
&scene,
&mut probe_image_cache,
&render_size,
true,
RootRenderTarget::AtlasLayer,
);
self.programs.resources.view_framebuffer_override = previous_view_framebuffer;
core::mem::swap(
&mut self.programs.resources.atlas_texture_array,
&mut probe_atlas_texture_array,
);
self.gl
.delete_texture(Some(&probe_atlas_texture_array.texture));
let pixels = match render_result {
Ok(()) => read_framebuffer_rgba8(&self.gl, &probe_framebuffer, width, height),
Err(error) => {
self.gl.delete_framebuffer(Some(&probe_framebuffer));
self.gl.delete_texture(Some(&probe_texture));
return Err(error);
}
};
self.gl.delete_framebuffer(Some(&probe_framebuffer));
self.gl.delete_texture(Some(&probe_texture));
Ok(pixels)
}
/// Shared render pipeline: prepares GPU resources, runs the scheduler, and
/// maintains caches.
///
/// When `clear` is true the view framebuffer is cleared to transparent black
/// before drawing. This must happen *after* `prepare` (which may create/resize
/// the framebuffer attachment). Atlas renders skip the clear so previously
/// rendered atlas content is preserved.
fn render_scene(
&mut self,
scene: &Scene,
image_cache: &mut ImageCache,
render_size: &RenderSize,
clear: bool,
root_output_target: RootRenderTarget,
) -> Result<(), RenderError> {
if !self.filter_context.filter_textures.is_empty() {
self.programs.clear_filter_atlas_textures(&self.gl);
}
self.filter_context
.deallocate_all_and_clear_context(image_cache);
let mut encoded_paints = scene.encoded_paints.borrow_mut();
let original_scene_paint_count = encoded_paints.len();
self.filter_context
.prepare(&scene.render_graph, image_cache, &mut encoded_paints)?;
self.prepare_gpu_encoded_paints(&encoded_paints, image_cache);
self.programs
.maybe_resize_atlas_texture_array(&self.gl, image_cache.atlas_count() as u32);
self.programs.maybe_resize_filter_atlas_textures(
&self.gl,
self.filter_context.image_cache.atlas_count() as u32,
);
// 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.filter_context,
);
if clear {
self.programs.clear_view_framebuffer(&self.gl);
}
self.programs.resources.depth_cleared_this_frame = false;
let mut ctx = WebGlRendererContext {
programs: &mut self.programs,
gl: &self.gl,
image_cache,
encoded_paints: &encoded_paints,
paint_idxs: &self.paint_idxs,
filter_context: &self.filter_context,
filter_pass_state: &mut self.filter_pass_state,
};
self.scheduler.do_scene(
&mut self.scheduler_state,
&mut ctx,
scene,
root_output_target,
&self.paint_idxs,
&self.filter_context,
&encoded_paints,
)?;
// 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.
if self.programs.resources.depth_cleared_this_frame {
self.gl.bind_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
self.programs.resources.view_framebuffer_override.as_ref(),
);
self.gl
.invalidate_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
&self.programs.resources.depth_attachment_array,
)
.unwrap();
}
encoded_paints.truncate(original_scene_paint_count);
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,
) -> ImageId {
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,
) -> ImageId {
let width = writer.width();
let height = writer.height();
let image_id = image_cache.allocate(width, height, padding).unwrap();
self.write_to_atlas(image_cache, image_id, writer, None);
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<[u32; 2]>,
) {
let image_resource = image_cache.get(image_id).expect("Image resource not found");
self.programs
.maybe_resize_atlas_texture_array(&self.gl, image_cache.atlas_count() as u32);
let offset = offset_override.unwrap_or([
image_resource.offset[0] as u32,
image_resource.offset[1] as u32,
]);
writer.write_to_atlas_layer(
&self.gl,
&self.programs.resources.atlas_texture_array.texture,
image_resource.atlas_id.as_u32(),
offset,
writer.width(),
writer.height(),
);
}
/// 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) {
if let Some(image_resource) = resources.image_cache.deallocate(image_id) {
let padding = image_resource.padding as u32;
self.clear_atlas_region(
image_resource.atlas_id,
[
image_resource.offset[0] as u32 - padding,
image_resource.offset[1] as u32 - padding,
],
image_resource.width as u32 + padding * 2,
image_resource.height as u32 + padding * 2,
);
}
}
/// Returns a reference to the underlying atlas texture array.
///
/// This is a 2D array texture (`TextureViewDimension::D2Array`) containing all
/// atlas layers used by the image cache. Each layer holds cached image data
/// (e.g., rasterised glyphs) that the renderer samples during draw calls.
pub fn atlas_texture(&self) -> &WebGlTexture {
&self.programs.resources.atlas_texture_array.texture
}
/// Clear a specific region of the atlas texture array.
fn clear_atlas_region(&mut self, atlas_id: AtlasId, offset: [u32; 2], width: u32, height: u32) {
let _state_guard = WebGlStateGuard::for_clear_atlas_region(&self.gl);
let temp_framebuffer = self.gl.create_framebuffer().unwrap();
// Bind our temporary framebuffer
self.gl
.bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(&temp_framebuffer));
// Attach the specific atlas layer to the framebuffer
self.gl.framebuffer_texture_layer(
WebGl2RenderingContext::FRAMEBUFFER,
WebGl2RenderingContext::COLOR_ATTACHMENT0,
Some(&self.programs.resources.atlas_texture_array.texture),
0,
atlas_id.as_u32() as i32,
);
// Set viewport to match the atlas texture dimensions
let atlas_size = &self.programs.resources.atlas_texture_array.size;
self.gl
.viewport(0, 0, atlas_size.width as i32, atlas_size.height as i32);
// 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);
// Clean up temporary framebuffer
self.gl.delete_framebuffer(Some(&temp_framebuffer));
}
fn prepare_gpu_encoded_paints(
&mut self,
encoded_paints: &[EncodedPaint],
image_cache: &ImageCache,
) {
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;
match paint {
EncodedPaint::Image(img) => {
if let ImageSource::OpaqueId { id: image_id, .. } = img.source {
let image_resource: Option<&ImageResource> = image_cache.get(image_id);
if let Some(image_resource) = image_resource {
let gpu_image = self.encode_image_paint(img, image_resource);
self.encoded_paints[encoded_paint_idx] = gpu_image;
current_idx += GPU_ENCODED_IMAGE_SIZE_TEXELS;
}
}
}
EncodedPaint::Gradient(gradient) => {
let (gradient_start, gradient_width) =
self.gradient_cache.get_or_create_ramp(gradient);
let gpu_gradient =
self.encode_gradient_paint(gradient, gradient_width, gradient_start);
let gradient_size_texels = match &gpu_gradient {
GpuEncodedPaint::LinearGradient(_) => GPU_LINEAR_GRADIENT_SIZE_TEXELS,
GpuEncodedPaint::RadialGradient(_) => GPU_RADIAL_GRADIENT_SIZE_TEXELS,
GpuEncodedPaint::SweepGradient(_) => GPU_SWEEP_GRADIENT_SIZE_TEXELS,
_ => unreachable!("encode_gradient_for_gpu only returns gradient types"),
};
self.encoded_paints[encoded_paint_idx] = gpu_gradient;
current_idx += gradient_size_texels;
}
EncodedPaint::BlurredRoundedRect(_blurred_rect) => {
// TODO: Blurred rounded rectangles are not yet supported
log::warn!(
"Blurred rounded rectangles are not yet supported in sparse strips hybrid renderer"
);
}
}
}
self.paint_idxs[encoded_paints.len()] = current_idx;
}
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,
image_resource.atlas_id.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_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],
}),
}
}
}
#[cfg(feature = "text")]
fn clear_atlas_region(renderer: &mut WebGlRenderer, rect: &PendingClearRect) {
// TODO: Similarly to wgpu, maybe this can be done in a more effective
// way?
let padding = u32::from(GLYPH_PADDING);
let offset = [
u32::from(rect.x).saturating_sub(padding),
u32::from(rect.y).saturating_sub(padding),
];
let width = u32::from(rect.width) + padding * 2;
let height = u32::from(rect.height) + padding * 2;
renderer.clear_atlas_region(AtlasId::new(rect.page_index), offset, width, height);
}
/// Contains the WebGL programs and resources for rendering.
#[derive(Debug)]
struct WebGlPrograms {
/// Program for rendering wide tile commands.
strip_program: WebGlProgram,
/// Uniform locations for the strip program
strip_uniforms: StripUniforms,
/// Program for opaque solid strips on the final view.
opaque_solid_program: WebGlProgram,
/// Uniform locations for the opaque solid program.
opaque_solid_uniforms: StripUniforms,
/// Program for opaque tinted image strips on the final view.
opaque_image_program: WebGlProgram,
/// Uniform locations for the opaque tinted image program.
opaque_image_uniforms: StripUniforms,
/// Program for opaque untinted image strips on the final view.
opaque_image_untinted_program: WebGlProgram,
/// Uniform locations for the opaque untinted image program.
opaque_image_untinted_uniforms: StripUniforms,
/// Program for opaque gradient strips on the final view.
opaque_gradient_program: WebGlProgram,
/// Uniform locations for the opaque gradient program.
opaque_gradient_uniforms: StripUniforms,
/// Program for clearing slots in slot textures.
clear_program: WebGlProgram,
/// Uniform locations for the `clear_program`.
clear_uniforms: ClearUniforms,
/// Program for filter passes.
filter_program: WebGlProgram,
/// Uniform locations for the filter program.
filter_uniforms: FilterPassUniforms,
/// WebGL resources for rendering.
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,
in_tex: WebGlUniformLocation,
original_tex: WebGlUniformLocation,
}
/// Uniform locations for `strip_program`.
#[derive(Debug)]
struct StripUniforms {
/// Alphas texture location.
alphas_texture: Option<WebGlUniformLocation>,
/// Clip input texture location.
clip_input_texture: Option<WebGlUniformLocation>,
/// Atlas texture location.
atlas_texture_array: Option<WebGlUniformLocation>,
/// Encoded paints texture location for fragment shader.
encoded_paints_texture_fs: Option<WebGlUniformLocation>,
/// Encoded paints texture location for vertex shader.
encoded_paints_texture_vs: Option<WebGlUniformLocation>,
/// Gradient texture location.
gradient_texture: Option<WebGlUniformLocation>,
}
/// Uniform locations for `clear_program`.
#[derive(Debug)]
struct ClearUniforms {
/// Config uniform block index.
config_block_index: u32,
}
/// Contains all WebGL resources needed for rendering.
#[derive(Debug)]
struct WebGlResources {
/// VAO for strip rendering.
strip_vao: WebGlVertexArrayObject,
/// Buffer for [`GpuStrip`] data.
strips_buffer: WebGlBuffer,
/// Texture for alpha values (used by both view and slot rendering).
alphas_texture: WebGlTexture,
/// Height of alpha texture.
alpha_texture_height: u32,
/// Texture array for atlas data (multiple atlases supported)
atlas_texture_array: WebGlTextureArray,
/// Encoded paints texture for image metadata.
encoded_paints_texture: WebGlTexture,
/// Height of encoded paints texture.
encoded_paints_texture_height: u32,
/// Gradient texture for gradient ramp data.
gradient_texture: WebGlTexture,
/// Height of gradient texture.
gradient_texture_height: u32,
/// Config buffer for rendering wide tile commands into the view texture.
view_config_buffer: WebGlBuffer,
/// Config buffer for rendering wide tile commands into a slot texture.
slot_config_buffer: WebGlBuffer,
/// Buffer for slot indices used in `clear_slots`.
clear_slot_indices_buffer: WebGlBuffer,
/// VAO for clear slots program.
clear_vao: WebGlVertexArrayObject,
/// Config buffer for clear program.
clear_config_buffer: WebGlBuffer,
view_framebuffer_override: Option<WebGlFramebuffer>,
/// Whether the depth buffer has been cleared this frame.
depth_cleared_this_frame: bool,
/// Pre-allocated JS array for `invalidateFramebuffer` calls.
depth_attachment_array: js_sys::Array,
/// Slot textures.
slot_textures: [WebGlTexture; 2],
/// Framebuffers for slot textures.
slot_framebuffers: [WebGlFramebuffer; 2],
/// Cached result from querying `WebGl2RenderingContext::MAX_TEXTURE_SIZE` which is a blocking
/// WebGL call.
max_texture_dimension_2d: u32,
/// Placeholder 1x1 atlas texture array, used during `render_to_atlas` to avoid
/// binding the real atlas texture while it is also the render target.
stub_atlas_texture_array: WebGlTextureArray,
/// Cached framebuffer for rendering into an atlas layer in `render_to_atlas`.
/// Reused to avoid create/delete overhead on every call.
atlas_render_framebuffer: Option<WebGlFramebuffer>,
/// Cached framebuffer for filter passes that write back to the main atlas.
/// Reused to avoid create/delete overhead on every filter application.
filter_main_atlas_framebuffer: Option<WebGlFramebuffer>,
/// Individual 2D textures for filter intermediate results.
filter_atlas_textures: Vec<WebGlTexture>,
/// Framebuffers for each filter atlas texture.
filter_atlas_framebuffers: Vec<WebGlFramebuffer>,
/// RGBA32UI texture storing filter parameters.
filter_data_texture: WebGlTexture,
/// Current height of filter data texture.
filter_data_texture_height: u32,
/// Per-instance vertex data buffer for filter draws.
filter_instance_buffer: WebGlBuffer,
/// VAO for filter rendering.
filter_vao: WebGlVertexArrayObject,
/// Config buffer for rendering filter layers.
filter_config_buffer: WebGlBuffer,
/// Cached atlas width for creating new filter atlas textures.
filter_atlas_width: u32,
/// Cached atlas height for creating new filter atlas textures.
filter_atlas_height: u32,
}
/// Config for the clear slots pipeline.
#[repr(C)]
#[derive(Debug, Copy, Clone, Pod, Zeroable)]
struct ClearSlotsConfig {
/// Width of a slot.
pub slot_width: u32,
/// Height of a slot.
pub slot_height: u32,
/// Total height of the texture.
pub texture_height: u32,
/// Padding for alignment.
pub _padding: u32,
}
impl WebGlPrograms {
/// Creates programs and initializes resources.
fn new(
gl: WebGl2RenderingContext,
image_cache: &ImageCache,
filter_context: &FilterContext,
slot_count: usize,
) -> Self {
let strip_program = create_shader_program(
&gl,
render_strips::VERTEX_SOURCE,
render_strips::FRAGMENT_SOURCE,
);
let opaque_solid_program = create_shader_program(
&gl,
opaque_solid::VERTEX_SOURCE,
opaque_solid::FRAGMENT_SOURCE,
);
let opaque_image_program = create_shader_program(
&gl,
opaque_image::VERTEX_SOURCE,
opaque_image::FRAGMENT_SOURCE,
);
let opaque_image_untinted_program = create_shader_program(
&gl,
opaque_image_untinted::VERTEX_SOURCE,
opaque_image_untinted::FRAGMENT_SOURCE,
);
let opaque_gradient_program = create_shader_program(
&gl,
opaque_gradient::VERTEX_SOURCE,
opaque_gradient::FRAGMENT_SOURCE,
);
let clear_program = create_shader_program(
&gl,
clear_slots::VERTEX_SOURCE,
clear_slots::FRAGMENT_SOURCE,
);
let filter_program =
create_shader_program(&gl, filters::VERTEX_SOURCE, filters::FRAGMENT_SOURCE);
let filter_uniforms = get_filter_pass_uniforms(&gl, &filter_program);
let strip_uniforms = get_strip_uniforms(&gl, &strip_program);
let opaque_solid_uniforms = get_opaque_solid_uniforms(&gl, &opaque_solid_program);
let opaque_image_uniforms = get_opaque_image_uniforms(&gl, &opaque_image_program);
let opaque_image_untinted_uniforms =
get_opaque_image_untinted_uniforms(&gl, &opaque_image_untinted_program);
let opaque_gradient_uniforms = get_opaque_gradient_uniforms(&gl, &opaque_gradient_program);
let clear_uniforms = get_clear_uniforms(&gl, &clear_program);
let resources = create_webgl_resources(&gl, image_cache, filter_context, slot_count);
initialize_strip_vao(&gl, &resources);
initialize_clear_vao(&gl, &resources);
initialize_filter_vao(&gl, &resources);
let encoded_paints_data = vec![0; (resources.max_texture_dimension_2d << 4) as usize];
gl.enable(WebGl2RenderingContext::BLEND);
gl.blend_func(
WebGl2RenderingContext::ONE,
WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA,
);
Self {
strip_program,
clear_program,
filter_program,
filter_uniforms,
strip_uniforms,
opaque_solid_program,
opaque_solid_uniforms,
opaque_image_program,
opaque_image_uniforms,
opaque_image_untinted_program,
opaque_image_untinted_uniforms,
opaque_gradient_program,
opaque_gradient_uniforms,
clear_uniforms,
resources,
render_size: RenderSize {
width: 0,
height: 0,
},
negate_ndc: false,
encoded_paints_data,
filter_data: Vec::new(),
}
}
/// 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,
) {
let max_texture_dimension_2d = self.resources.max_texture_dimension_2d;
self.maybe_resize_alphas_tex(max_texture_dimension_2d, alphas.len());
self.maybe_resize_encoded_paints_tex(max_texture_dimension_2d, paint_idxs);
self.maybe_resize_filter_data_tex(filter_context);
self.maybe_update_config_buffer(gl, max_texture_dimension_2d, render_size);
self.upload_alpha_texture(gl, alphas);
self.upload_encoded_paints_texture(gl, encoded_paints);
self.upload_filter_data_texture(gl, filter_context);
if gradient_cache.has_changed() {
self.maybe_resize_gradient_tex(gl, max_texture_dimension_2d, gradient_cache);
self.upload_gradient_texture(gl, gradient_cache);
gradient_cache.mark_synced();
}
}
/// Resize atlas texture array to accommodate more atlases.
fn maybe_resize_atlas_texture_array(
&mut self,
gl: &WebGl2RenderingContext,
required_atlas_count: u32,
) {
let WebGlTextureSize {
width,
height,
depth_or_array_layers: current_atlas_count,
} = self.resources.atlas_texture_array.size();
if required_atlas_count > current_atlas_count {
// Create new texture array with more layers
let new_atlas_texture_array =
create_atlas_texture_array(gl, width, height, required_atlas_count);
// Copy existing atlas data from old texture array to new one
self.copy_atlas_texture_data(gl, &new_atlas_texture_array, current_atlas_count);
// Replace the old resources
self.resources.atlas_texture_array = new_atlas_texture_array;
// Cached FBOs were attached to the old texture; drop them so we recreate on next use.
if let Some(fb) = self.resources.atlas_render_framebuffer.take() {
gl.delete_framebuffer(Some(&fb));
}
if let Some(fb) = self.resources.filter_main_atlas_framebuffer.take() {
gl.delete_framebuffer(Some(&fb));
}
}
}
/// Copy texture data from the old atlas texture array to a new one.
/// This is necessary when resizing the texture array to preserve existing atlas data.
fn copy_atlas_texture_data(
&self,
gl: &WebGl2RenderingContext,
new_atlas_texture_array: &WebGlTextureArray,
layer_count_to_copy: u32,
) {
let WebGlTextureSize { width, height, .. } = self.resources.atlas_texture_array.size();
// Copy each layer from the old atlas to the new one
for layer in 0..layer_count_to_copy {
copy_to_texture_array_layer(
gl,
|gl| {
// Attach source layer to READ framebuffer
gl.framebuffer_texture_layer(
WebGl2RenderingContext::READ_FRAMEBUFFER,
WebGl2RenderingContext::COLOR_ATTACHMENT0,
Some(&self.resources.atlas_texture_array.texture),
0,
layer as i32,
);
},
&new_atlas_texture_array.texture,
layer,
[0, 0],
[width, height],
);
}
}
fn maybe_resize_filter_data_tex(&mut self, filter_context: &FilterContext) {
let max_texture_dimension_2d = self.resources.max_texture_dimension_2d;
let Some(required_height) =
filter_context.required_filter_data_height(max_texture_dimension_2d)
else {
return;
};
if required_height > self.resources.filter_data_texture_height {
let required_size = (max_texture_dimension_2d * required_height) << 4;
self.filter_data.resize(required_size as usize, 0);
self.resources.filter_data_texture_height = required_height;
}
}
fn upload_filter_data_texture(
&mut self,
gl: &WebGl2RenderingContext,
filter_context: &FilterContext,
) {
if filter_context.is_empty() {
return;
}
let width = self.resources.max_texture_dimension_2d;
let height = self.resources.filter_data_texture_height;
filter_context.serialize_to_buffer(&mut self.filter_data);
gl.active_texture(WebGl2RenderingContext::TEXTURE0);
gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(&self.resources.filter_data_texture),
);
let data_as_u32 = bytemuck::cast_slice::<u8, u32>(&self.filter_data);
let packed_array = js_sys::Uint32Array::from(data_as_u32);
gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view(
WebGl2RenderingContext::TEXTURE_2D,
0,
WebGl2RenderingContext::RGBA32UI as i32,
width as i32,
height as i32,
0,
WebGl2RenderingContext::RGBA_INTEGER,
WebGl2RenderingContext::UNSIGNED_INT,
Some(&packed_array),
)
.unwrap();
}
fn maybe_resize_filter_atlas_textures(
&mut self,
gl: &WebGl2RenderingContext,
required_count: u32,
) {
let current_count = self.resources.filter_atlas_textures.len() as u32;
// TODO: Same as wgpu, should we be destroying
// textures if they aren't needed anymore?
if required_count > current_count {
let width = self.resources.filter_atlas_width;
let height = self.resources.filter_atlas_height;
for _ in current_count..required_count {
let tex = create_filter_atlas_texture(gl, width, height);
let fb = create_framebuffer_for_texture(gl, &tex);
self.resources.filter_atlas_textures.push(tex);
self.resources.filter_atlas_framebuffers.push(fb);
}
}
}
fn clear_filter_atlas_textures(&self, gl: &WebGl2RenderingContext) {
let _state_guard = WebGlStateGuard::with_config(
gl,
WebGlStateConfig {
framebuffer: true,
viewport: true,
..Default::default()
},
);
let width = self.resources.filter_atlas_width;
let height = self.resources.filter_atlas_height;
for fb in &self.resources.filter_atlas_framebuffers {
gl.bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(fb));
gl.viewport(0, 0, width as i32, height as i32);
gl.clear_color(0.0, 0.0, 0.0, 0.0);
gl.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT);
}
}
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,
);
}
/// Update the alpha texture size if needed.
fn maybe_resize_alphas_tex(&mut self, max_texture_dimension_2d: u32, alphas_len: usize) {
let required_alpha_height = (alphas_len as u32)
// There are 16 1-byte alpha values per texel.
.div_ceil(max_texture_dimension_2d << 4);
let current_alpha_height = self.resources.alpha_texture_height;
if required_alpha_height > current_alpha_height {
// We need to resize the alpha texture to fit the new alpha data.
assert!(
required_alpha_height <= max_texture_dimension_2d,
"Alpha texture height exceeds max texture dimensions"
);
// Track the new height.
self.resources.alpha_texture_height = required_alpha_height;
}
}
/// Update the encoded paints texture size if needed.
fn maybe_resize_encoded_paints_tex(
&mut self,
max_texture_dimension_2d: u32,
paint_idxs: &[u32],
) {
let required_texels = paint_idxs.last().unwrap();
let required_encoded_paints_height = required_texels.div_ceil(max_texture_dimension_2d);
let current_encoded_paints_height = self.resources.encoded_paints_texture_height;
if required_encoded_paints_height > current_encoded_paints_height {
assert!(
required_encoded_paints_height <= max_texture_dimension_2d,
"Encoded paints texture height exceeds max texture dimensions"
);
let required_encoded_paints_size =
(max_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_height = required_encoded_paints_height;
}
}
/// Update the gradient texture size if needed.
fn maybe_resize_gradient_tex(
&mut self,
_gl: &WebGl2RenderingContext,
max_texture_dimension_2d: u32,
gradient_cache: &GradientRampCache,
) {
if gradient_cache.is_empty() {
return;
}
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(max_texture_dimension_2d * 4);
let current_gradient_height = self.resources.gradient_texture_height;
if required_gradient_height > current_gradient_height {
assert!(
required_gradient_height <= max_texture_dimension_2d,
"Gradient texture height exceeds max texture dimensions"
);
self.resources.gradient_texture_height = required_gradient_height;
}
}
/// Update config buffer if dimensions changed.
fn maybe_update_config_buffer(
&mut self,
gl: &WebGl2RenderingContext,
max_texture_dimension_2d: u32,
new_render_size: &RenderSize,
) {
// Only negate if we are rendering to the main frame buffer.
let negate_ndc = self.resources.view_framebuffer_override.is_none();
// 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 {
// Update view config buffer
{
let config = Config {
width: new_render_size.width,
height: new_render_size.height,
strip_height: u32::from(Tile::HEIGHT),
alphas_tex_width_bits: max_texture_dimension_2d.trailing_zeros(),
encoded_paints_tex_width_bits: max_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),
);
let config_data = bytemuck::bytes_of(&config);
gl.buffer_data_with_u8_array(
WebGl2RenderingContext::UNIFORM_BUFFER,
config_data,
WebGl2RenderingContext::STATIC_DRAW,
);
}
let total_slots = max_texture_dimension_2d / u32::from(Tile::HEIGHT);
// Update slot config buffer.
{
let slot_config = Config {
width: u32::from(WideTile::WIDTH),
height: u32::from(Tile::HEIGHT) * total_slots,
strip_height: u32::from(Tile::HEIGHT),
alphas_tex_width_bits: max_texture_dimension_2d.trailing_zeros(),
encoded_paints_tex_width_bits: max_texture_dimension_2d.trailing_zeros(),
strip_offset_x: 0,
strip_offset_y: 0,
// Always use y-down when rendering to slots.
negate_ndc: 0,
};
gl.bind_buffer(
WebGl2RenderingContext::UNIFORM_BUFFER,
Some(&self.resources.slot_config_buffer),
);
let slot_config_data = bytemuck::bytes_of(&slot_config);
gl.buffer_data_with_u8_array(
WebGl2RenderingContext::UNIFORM_BUFFER,
slot_config_data,
WebGl2RenderingContext::STATIC_DRAW,
);
}
// Update clear config buffer.
// TODO: This can be done once, and doesn't need to be done on every `prepare` call.
{
let clear_config = ClearSlotsConfig {
slot_width: u32::from(WideTile::WIDTH),
slot_height: u32::from(Tile::HEIGHT),
texture_height: u32::from(Tile::HEIGHT) * total_slots,
_padding: 0,
};
gl.bind_buffer(
WebGl2RenderingContext::UNIFORM_BUFFER,
Some(&self.resources.clear_config_buffer),
);
let clear_config_data = bytemuck::bytes_of(&clear_config);
gl.buffer_data_with_u8_array(
WebGl2RenderingContext::UNIFORM_BUFFER,
clear_config_data,
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>) {
if alphas.is_empty() {
return;
}
let alpha_texture_width = self.resources.max_texture_dimension_2d;
let alpha_texture_height = self.resources.alpha_texture_height;
let total_size = alpha_texture_width as usize * alpha_texture_height as usize * 16;
let original_len = alphas.len();
// Temporarily pad the length of the alphas to the texture size before uploading.
alphas.resize(total_size, 0);
gl.active_texture(WebGl2RenderingContext::TEXTURE0);
gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(&self.resources.alphas_texture),
);
upload_data_to_rgba32_texture(
gl,
bytemuck::cast_slice::<u8, u32>(alphas),
alpha_texture_width,
alpha_texture_height,
);
// Truncate back to the original size.
alphas.truncate(original_len);
}
/// Upload encoded paints to the texture.
fn upload_encoded_paints_texture(
&mut self,
gl: &WebGl2RenderingContext,
encoded_paints: &[GpuEncodedPaint],
) {
if !encoded_paints.is_empty() {
let encoded_paints_texture_width = self.resources.max_texture_dimension_2d;
let encoded_paints_texture_height = self.resources.encoded_paints_texture_height;
GpuEncodedPaint::serialize_to_buffer(encoded_paints, &mut self.encoded_paints_data);
gl.active_texture(WebGl2RenderingContext::TEXTURE0);
gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(&self.resources.encoded_paints_texture),
);
upload_data_to_rgba32_texture(
gl,
bytemuck::cast_slice::<u8, u32>(&self.encoded_paints_data),
encoded_paints_texture_width,
encoded_paints_texture_height,
);
}
}
/// Upload gradient data to the texture.
fn upload_gradient_texture(
&mut self,
gl: &WebGl2RenderingContext,
gradient_cache: &mut GradientRampCache,
) {
if gradient_cache.is_empty() {
return;
}
let gradient_texture_width = self.resources.max_texture_dimension_2d;
let gradient_texture_height = self.resources.gradient_texture_height;
let total_capacity = (gradient_texture_width * gradient_texture_height * 4) as usize;
// Take ownership of the luts to avoid copying, then resize for texture padding.
let mut luts = gradient_cache.take_luts();
luts.resize(total_capacity, 0);
gl.active_texture(WebGl2RenderingContext::TEXTURE0);
gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(&self.resources.gradient_texture),
);
gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
WebGl2RenderingContext::TEXTURE_2D,
0,
WebGl2RenderingContext::RGBA8 as i32,
gradient_texture_width as i32,
gradient_texture_height as i32,
0,
WebGl2RenderingContext::RGBA,
WebGl2RenderingContext::UNSIGNED_BYTE,
Some(&luts),
)
.unwrap();
// Restore the luts back to the cache.
gradient_cache.restore_luts(luts);
}
/// Clear the view framebuffer.
// TODO: Investigate adding tests for the clear_view behavior.
fn clear_view_framebuffer(&mut self, gl: &WebGl2RenderingContext) {
gl.bind_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
self.resources.view_framebuffer_override.as_ref(),
);
gl.clear_color(0.0, 0.0, 0.0, 0.0);
gl.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT);
}
/// Uploads a strip slice into the GPU instance buffer.
fn upload_strips(&mut self, gl: &WebGl2RenderingContext, strips: &[GpuStrip]) {
if strips.is_empty() {
return;
}
gl.bind_buffer(
WebGl2RenderingContext::ARRAY_BUFFER,
Some(&self.resources.strips_buffer),
);
let strip_bytes: &[u8] = bytemuck::cast_slice(strips);
gl.buffer_data_with_i32(
WebGl2RenderingContext::ARRAY_BUFFER,
strip_bytes.len() as i32,
WebGl2RenderingContext::DYNAMIC_DRAW,
);
gl.buffer_sub_data_with_i32_and_u8_array(
WebGl2RenderingContext::ARRAY_BUFFER,
0,
strip_bytes,
);
}
}
/// RAII guard for WebGL state management.
/// Automatically saves state on creation and restores it on drop.
/// Only saves/restores the state specified in the configuration.
struct WebGlStateGuard<'a> {
gl: &'a WebGl2RenderingContext,
config: WebGlStateConfig,
original_framebuffer: Option<WebGlFramebuffer>,
original_read_framebuffer: Option<WebGlFramebuffer>,
original_texture_2d_array: Option<WebGlTexture>,
scissor_enabled: bool,
viewport: [i32; 4],
}
impl<'a> WebGlStateGuard<'a> {
/// Create a new state guard with custom configuration.
fn with_config(gl: &'a WebGl2RenderingContext, config: WebGlStateConfig) -> Self {
// Save current framebuffer binding if requested
let original_framebuffer = if config.framebuffer {
gl.get_parameter(WebGl2RenderingContext::FRAMEBUFFER_BINDING)
.ok()
.and_then(|v| v.dyn_into::<WebGlFramebuffer>().ok())
} else {
None
};
// Save current read framebuffer binding if requested
let original_read_framebuffer = if config.read_framebuffer {
gl.get_parameter(WebGl2RenderingContext::READ_FRAMEBUFFER_BINDING)
.ok()
.and_then(|v| v.dyn_into::<WebGlFramebuffer>().ok())
} else {
None
};
// Save current 2D array texture binding if requested
let original_texture_2d_array = if config.texture_2d_array {
gl.get_parameter(WebGl2RenderingContext::TEXTURE_BINDING_2D_ARRAY)
.ok()
.and_then(|v| v.dyn_into::<WebGlTexture>().ok())
} else {
None
};
// Save current scissor test state if requested
let scissor_enabled = if config.scissor {
gl.get_parameter(WebGl2RenderingContext::SCISSOR_TEST)
.unwrap()
.as_bool()
.unwrap_or(false)
} else {
false
};
// Save current viewport if requested
let viewport = if config.viewport {
let viewport_js = gl
.get_parameter(WebGl2RenderingContext::VIEWPORT)
.unwrap()
.dyn_into::<js_sys::Int32Array>()
.unwrap();
let viewport_vec = viewport_js.to_vec();
[
viewport_vec[0],
viewport_vec[1],
viewport_vec[2],
viewport_vec[3],
]
} else {
[0, 0, 0, 0]
};
Self {
gl,
config,
original_framebuffer,
original_read_framebuffer,
original_texture_2d_array,
scissor_enabled,
viewport,
}
}
/// Create a state guard for clearing an atlas region operations.
fn for_clear_atlas_region(gl: &'a WebGl2RenderingContext) -> Self {
Self::with_config(
gl,
WebGlStateConfig {
framebuffer: true,
scissor: true,
viewport: true,
..Default::default()
},
)
}
/// Create a state guard for texture copying operations.
fn for_texture_copy(gl: &'a WebGl2RenderingContext) -> Self {
Self::with_config(
gl,
WebGlStateConfig {
read_framebuffer: true,
texture_2d_array: true,
..Default::default()
},
)
}
}
impl Drop for WebGlStateGuard<'_> {
/// Restore WebGL state when the guard goes out of scope.
/// Only restores state that was configured to be saved.
fn drop(&mut self) {
// Restore scissor test state if it was saved
if self.config.scissor {
if self.scissor_enabled {
self.gl.enable(WebGl2RenderingContext::SCISSOR_TEST);
} else {
self.gl.disable(WebGl2RenderingContext::SCISSOR_TEST);
}
}
// Restore viewport if it was saved
if self.config.viewport {
self.gl.viewport(
self.viewport[0],
self.viewport[1],
self.viewport[2],
self.viewport[3],
);
}
// Restore original framebuffer if it was saved
if self.config.framebuffer {
self.gl.bind_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
self.original_framebuffer.as_ref(),
);
}
// Restore original read framebuffer if it was saved
if self.config.read_framebuffer {
self.gl.bind_framebuffer(
WebGl2RenderingContext::READ_FRAMEBUFFER,
self.original_read_framebuffer.as_ref(),
);
}
// Restore original 2D array texture binding if it was saved
if self.config.texture_2d_array {
self.gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D_ARRAY,
self.original_texture_2d_array.as_ref(),
);
}
}
}
/// Configuration for which WebGL state to save/restore.
#[derive(Debug, Default)]
struct WebGlStateConfig {
/// Save/restore framebuffer binding (`FRAMEBUFFER_BINDING`)
framebuffer: bool,
/// Save/restore read framebuffer binding (`READ_FRAMEBUFFER_BINDING`)
read_framebuffer: bool,
/// Save/restore 2D array texture binding (`TEXTURE_BINDING_2D_ARRAY`)
texture_2d_array: bool,
/// Save/restore scissor test state
scissor: bool,
/// Save/restore viewport
viewport: bool,
}
#[cfg(feature = "probe")]
fn read_framebuffer_rgba8(
gl: &WebGl2RenderingContext,
framebuffer: &WebGlFramebuffer,
width: u16,
height: u16,
) -> Pixmap {
let _state_guard = WebGlStateGuard::with_config(
gl,
WebGlStateConfig {
framebuffer: true,
..Default::default()
},
);
gl.bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(framebuffer));
let mut pixmap = Pixmap::new(width, height);
gl.read_pixels_with_opt_u8_array(
0,
0,
i32::from(width),
i32::from(height),
WebGl2RenderingContext::RGBA,
WebGl2RenderingContext::UNSIGNED_BYTE,
Some(pixmap.data_as_u8_slice_mut()),
)
.unwrap();
pixmap.recompute_may_have_opacities();
pixmap
}
/// Create a WebGL shader program from vertex and fragment sources.
fn create_shader_program(
gl: &WebGl2RenderingContext,
vertex_src: &str,
fragment_src: &str,
) -> WebGlProgram {
// Compile vertex shader.
let vertex_shader = gl
.create_shader(WebGl2RenderingContext::VERTEX_SHADER)
.unwrap();
gl.shader_source(&vertex_shader, vertex_src);
gl.compile_shader(&vertex_shader);
if !gl
.get_shader_parameter(&vertex_shader, WebGl2RenderingContext::COMPILE_STATUS)
.as_bool()
.unwrap_or(false)
{
let info = gl
.get_shader_info_log(&vertex_shader)
.unwrap_or_else(|| "Unknown error creating vertex shader".into());
panic!("Failed to compile vertex shader: {info}");
}
// Compile fragment shader.
let fragment_shader = gl
.create_shader(WebGl2RenderingContext::FRAGMENT_SHADER)
.unwrap();
gl.shader_source(&fragment_shader, fragment_src);
gl.compile_shader(&fragment_shader);
if !gl
.get_shader_parameter(&fragment_shader, WebGl2RenderingContext::COMPILE_STATUS)
.as_bool()
.unwrap_or(false)
{
let info = gl
.get_shader_info_log(&fragment_shader)
.unwrap_or_else(|| "Unknown error creating fragment shader".into());
panic!("Failed to compile fragment shader: {info}");
}
// Create and link the program.
let program = gl.create_program().unwrap();
gl.attach_shader(&program, &vertex_shader);
gl.attach_shader(&program, &fragment_shader);
gl.link_program(&program);
if !gl
.get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS)
.as_bool()
.unwrap_or(false)
{
let info = gl
.get_program_info_log(&program)
.unwrap_or_else(|| "Unknown error creating program".into());
panic!("Failed to link program: {info}");
}
gl.delete_shader(Some(&vertex_shader));
gl.delete_shader(Some(&fragment_shader));
program
}
/// Get the uniform locations for the `render_strips` program.
fn get_strip_uniforms(gl: &WebGl2RenderingContext, program: &WebGlProgram) -> StripUniforms {
get_optional_strip_uniforms(
gl,
program,
Some(render_strips::vertex::CONFIG),
Some(render_strips::fragment::CONFIG),
Some(render_strips::fragment::ALPHAS_TEXTURE),
Some(render_strips::fragment::CLIP_INPUT_TEXTURE),
Some(render_strips::fragment::ATLAS_TEXTURE_ARRAY),
Some(render_strips::fragment::ENCODED_PAINTS_TEXTURE),
Some(render_strips::vertex::ENCODED_PAINTS_TEXTURE),
Some(render_strips::fragment::GRADIENT_TEXTURE),
)
}
fn get_opaque_solid_uniforms(gl: &WebGl2RenderingContext, program: &WebGlProgram) -> StripUniforms {
get_optional_strip_uniforms(
gl,
program,
Some(opaque_solid::vertex::CONFIG),
None,
None,
None,
None,
None,
None,
None,
)
}
fn get_opaque_image_uniforms(gl: &WebGl2RenderingContext, program: &WebGlProgram) -> StripUniforms {
get_optional_strip_uniforms(
gl,
program,
Some(opaque_image::vertex::CONFIG),
Some(opaque_image::fragment::CONFIG),
None,
None,
Some(opaque_image::fragment::ATLAS_TEXTURE_ARRAY),
Some(opaque_image::fragment::ENCODED_PAINTS_TEXTURE),
Some(opaque_image::vertex::ENCODED_PAINTS_TEXTURE),
None,
)
}
fn get_opaque_image_untinted_uniforms(
gl: &WebGl2RenderingContext,
program: &WebGlProgram,
) -> StripUniforms {
get_optional_strip_uniforms(
gl,
program,
Some(opaque_image_untinted::vertex::CONFIG),
Some(opaque_image_untinted::fragment::CONFIG),
None,
None,
Some(opaque_image_untinted::fragment::ATLAS_TEXTURE_ARRAY),
Some(opaque_image_untinted::fragment::ENCODED_PAINTS_TEXTURE),
Some(opaque_image_untinted::vertex::ENCODED_PAINTS_TEXTURE),
None,
)
}
fn get_opaque_gradient_uniforms(
gl: &WebGl2RenderingContext,
program: &WebGlProgram,
) -> StripUniforms {
get_optional_strip_uniforms(
gl,
program,
Some(opaque_gradient::vertex::CONFIG),
Some(opaque_gradient::fragment::CONFIG),
None,
None,
None,
Some(opaque_gradient::fragment::ENCODED_PAINTS_TEXTURE),
None,
Some(opaque_gradient::fragment::GRADIENT_TEXTURE),
)
}
#[expect(
clippy::too_many_arguments,
reason = "Uniform metadata is naturally wide here."
)]
fn get_optional_strip_uniforms(
gl: &WebGl2RenderingContext,
program: &WebGlProgram,
config_vs_name: Option<&str>,
config_fs_name: Option<&str>,
alphas_texture_name: Option<&str>,
clip_input_texture_name: Option<&str>,
atlas_texture_array_name: Option<&str>,
encoded_paints_texture_fs_name: Option<&str>,
encoded_paints_texture_vs_name: Option<&str>,
gradient_texture_name: Option<&str>,
) -> StripUniforms {
let _config_vs_block_index = config_vs_name.and_then(|name| {
let idx = gl.get_uniform_block_index(program, name);
if idx == WebGl2RenderingContext::INVALID_INDEX {
None
} else {
gl.uniform_block_binding(program, idx, 0);
Some(idx)
}
});
let _config_fs_block_index = config_fs_name.and_then(|name| {
let idx = gl.get_uniform_block_index(program, name);
if idx == WebGl2RenderingContext::INVALID_INDEX {
None
} else {
gl.uniform_block_binding(program, idx, 0);
Some(idx)
}
});
StripUniforms {
alphas_texture: alphas_texture_name.and_then(|name| gl.get_uniform_location(program, name)),
clip_input_texture: clip_input_texture_name
.and_then(|name| gl.get_uniform_location(program, name)),
atlas_texture_array: atlas_texture_array_name
.and_then(|name| gl.get_uniform_location(program, name)),
encoded_paints_texture_fs: encoded_paints_texture_fs_name
.and_then(|name| gl.get_uniform_location(program, name)),
encoded_paints_texture_vs: encoded_paints_texture_vs_name
.and_then(|name| gl.get_uniform_location(program, name)),
gradient_texture: gradient_texture_name
.and_then(|name| gl.get_uniform_location(program, name)),
}
}
/// Get the uniform locations for the `clear_slots` program.
fn get_clear_uniforms(gl: &WebGl2RenderingContext, program: &WebGlProgram) -> ClearUniforms {
let config_name = clear_slots::vertex::CONFIG;
let config_block_index = gl.get_uniform_block_index(program, config_name);
debug_assert_ne!(
config_block_index,
WebGl2RenderingContext::INVALID_INDEX,
"invalid uniform index"
);
// Bind uniform block to binding point.
gl.uniform_block_binding(program, config_block_index, 0);
ClearUniforms { config_block_index }
}
fn get_filter_pass_uniforms(
gl: &WebGl2RenderingContext,
program: &WebGlProgram,
) -> FilterPassUniforms {
let filter_data = gl
.get_uniform_location(program, filters::fragment::FILTER_DATA)
.unwrap();
let in_tex = gl
.get_uniform_location(program, filters::fragment::IN_TEX)
.unwrap();
let original_tex = gl
.get_uniform_location(program, filters::fragment::ORIGINAL_TEX)
.unwrap();
FilterPassUniforms {
filter_data,
in_tex,
original_tex,
}
}
fn create_filter_atlas_texture(
gl: &WebGl2RenderingContext,
width: u32,
height: u32,
) -> WebGlTexture {
let texture = gl.create_texture().unwrap();
gl.active_texture(WebGl2RenderingContext::TEXTURE0);
gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&texture));
gl.tex_parameteri(
WebGl2RenderingContext::TEXTURE_2D,
WebGl2RenderingContext::TEXTURE_MIN_FILTER,
WebGl2RenderingContext::LINEAR as i32,
);
gl.tex_parameteri(
WebGl2RenderingContext::TEXTURE_2D,
WebGl2RenderingContext::TEXTURE_MAG_FILTER,
WebGl2RenderingContext::LINEAR as i32,
);
gl.tex_parameteri(
WebGl2RenderingContext::TEXTURE_2D,
WebGl2RenderingContext::TEXTURE_WRAP_S,
WebGl2RenderingContext::CLAMP_TO_EDGE as i32,
);
gl.tex_parameteri(
WebGl2RenderingContext::TEXTURE_2D,
WebGl2RenderingContext::TEXTURE_WRAP_T,
WebGl2RenderingContext::CLAMP_TO_EDGE as i32,
);
gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view(
WebGl2RenderingContext::TEXTURE_2D,
0,
WebGl2RenderingContext::RGBA8 as i32,
width as i32,
height as i32,
0,
WebGl2RenderingContext::RGBA,
WebGl2RenderingContext::UNSIGNED_BYTE,
None,
)
.unwrap();
texture
}
/// Vertex attribute layout for [`FilterInstanceData`].
const FILTER_ATTRIBS: [(i32, i32); 9] = [
(2, 0), // src_offset
(2, 8), // src_size
(2, 16), // dest_offset
(2, 24), // dest_size
(2, 32), // dest_atlas_size
(1, 40), // filter_data_offset
(2, 44), // original_offset
(2, 52), // original_size
(1, 60), // pass_kind
];
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::ARRAY_BUFFER,
Some(&resources.filter_instance_buffer),
);
for (loc, &(components, offset)) in FILTER_ATTRIBS.iter().enumerate() {
let loc = loc as u32;
gl.enable_vertex_attrib_array(loc);
gl.vertex_attrib_i_pointer_with_i32(
loc,
components,
WebGl2RenderingContext::UNSIGNED_INT,
FILTER_INSTANCE_STRIDE,
offset,
);
gl.vertex_attrib_divisor(loc, 1);
}
gl.bind_vertex_array(None);
}
/// Create a texture with nearest neighbor sampling and clamp-to-edge wrapping.
fn create_texture(gl: &WebGl2RenderingContext) -> WebGlTexture {
create_texture_inner(gl, WebGl2RenderingContext::TEXTURE_2D)
}
/// Create a texture array with nearest neighbor sampling and
/// clamp-to-edge wrapping.
fn create_texture_array(gl: &WebGl2RenderingContext) -> WebGlTexture {
create_texture_inner(gl, WebGl2RenderingContext::TEXTURE_2D_ARRAY)
}
fn create_texture_inner(gl: &WebGl2RenderingContext, target: u32) -> WebGlTexture {
let texture = gl.create_texture().unwrap();
gl.active_texture(WebGl2RenderingContext::TEXTURE0);
gl.bind_texture(target, Some(&texture));
// The filter and wrap modes are irrelevant because the shader
// (`render_strips.wgsl`) exclusively uses `textureLoad`, which bypasses
// the sampler entirely.
gl.tex_parameteri(
target,
WebGl2RenderingContext::TEXTURE_MIN_FILTER,
WebGl2RenderingContext::NEAREST as i32,
);
gl.tex_parameteri(
target,
WebGl2RenderingContext::TEXTURE_MAG_FILTER,
WebGl2RenderingContext::NEAREST 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,
);
// Also only to be defensive, in theory this shouldn't be necessary since we use
// `NEAREST` for both filters.
gl.tex_parameteri(target, WebGl2RenderingContext::TEXTURE_MAX_LEVEL, 0);
texture
}
/// Create all WebGL resources needed for rendering.
fn create_webgl_resources(
gl: &WebGl2RenderingContext,
image_cache: &ImageCache,
filter_context: &FilterContext,
slot_count: usize,
) -> WebGlResources {
let strip_vao = gl.create_vertex_array().unwrap();
let clear_vao = gl.create_vertex_array().unwrap();
let filter_vao = gl.create_vertex_array().unwrap();
let filter_instance_buffer = gl.create_buffer().unwrap();
let strips_buffer = gl.create_buffer().unwrap();
let view_config_buffer = gl.create_buffer().unwrap();
let slot_config_buffer = gl.create_buffer().unwrap();
let clear_slot_indices_buffer = gl.create_buffer().unwrap();
let clear_config_buffer = gl.create_buffer().unwrap();
// Create and configure alpha texture.
let alphas_texture = create_texture(gl);
let AtlasConfig {
atlas_size: (atlas_width, atlas_height),
initial_atlas_count,
..
} = image_cache.atlas_manager().config();
let atlas_texture_array =
create_atlas_texture_array(gl, *atlas_width, *atlas_height, *initial_atlas_count as u32);
// Create a 1x1 stub atlas texture array for use during render_to_atlas.
// This avoids binding the real atlas as a shader input while it is the render target.
let stub_atlas_texture_array = create_atlas_texture_array(gl, 1, 1, 1);
// Create and configure encoded paints texture.
let encoded_paints_texture = create_texture(gl);
// Create and configure gradient texture.
let gradient_texture = create_texture(gl);
// Create slot textures and framebuffers.
let slot_textures: [WebGlTexture; 2] = [
create_slot_texture(gl, slot_count),
create_slot_texture(gl, slot_count),
];
let slot_framebuffers: [WebGlFramebuffer; 2] = [
create_framebuffer_for_texture(gl, &slot_textures[0]),
create_framebuffer_for_texture(gl, &slot_textures[1]),
];
let max_texture_dimension_2d = get_max_texture_dimension_2d(gl);
let filter_data_texture = create_texture(gl);
let filter_config_buffer = gl.create_buffer().unwrap();
let AtlasConfig {
atlas_size: (filter_atlas_width, filter_atlas_height),
..
} = filter_context.image_cache.atlas_manager().config();
WebGlResources {
strip_vao,
strips_buffer,
alphas_texture,
alpha_texture_height: 0,
atlas_texture_array,
encoded_paints_texture,
encoded_paints_texture_height: 0,
gradient_texture,
gradient_texture_height: 0,
view_config_buffer,
slot_config_buffer,
clear_slot_indices_buffer,
clear_vao,
clear_config_buffer,
slot_textures,
slot_framebuffers,
view_framebuffer_override: None,
depth_cleared_this_frame: false,
// Note: we use DEPTH (not DEPTH_ATTACHMENT) because we render to the default
// framebuffer. If we ever support non-default framebuffers, this must change
// to DEPTH_ATTACHMENT.
depth_attachment_array: js_sys::Array::of1(&WebGl2RenderingContext::DEPTH.into()),
max_texture_dimension_2d,
stub_atlas_texture_array,
atlas_render_framebuffer: None,
filter_main_atlas_framebuffer: None,
filter_atlas_textures: Vec::new(),
filter_atlas_framebuffers: Vec::new(),
filter_data_texture,
filter_data_texture_height: 0,
filter_instance_buffer,
filter_vao,
filter_config_buffer,
filter_atlas_width: *filter_atlas_width,
filter_atlas_height: *filter_atlas_height,
}
}
/// Create an atlas texture array.
fn create_atlas_texture_array(
gl: &WebGl2RenderingContext,
width: u32,
height: u32,
layer_count: u32,
) -> WebGlTextureArray {
let atlas_texture = create_texture_array(gl);
// Initialize with empty texture array data
gl.tex_image_3d_with_opt_u8_array(
WebGl2RenderingContext::TEXTURE_2D_ARRAY,
0,
WebGl2RenderingContext::RGBA8 as i32,
width as i32,
height as i32,
layer_count as i32,
0,
WebGl2RenderingContext::RGBA,
WebGl2RenderingContext::UNSIGNED_BYTE,
None,
)
.unwrap();
WebGlTextureArray::new(atlas_texture, width, height, layer_count)
}
/// Create a texture for slot rendering.
fn create_slot_texture(gl: &WebGl2RenderingContext, slot_count: usize) -> WebGlTexture {
let texture = create_texture(gl);
gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view(
WebGl2RenderingContext::TEXTURE_2D,
0,
WebGl2RenderingContext::RGBA8 as i32,
u32::from(WideTile::WIDTH) as i32,
(u32::from(Tile::HEIGHT) * slot_count as u32) as i32,
0,
WebGl2RenderingContext::RGBA,
WebGl2RenderingContext::UNSIGNED_BYTE,
None,
)
.unwrap();
texture
}
/// Create a framebuffer for a texture.
fn create_framebuffer_for_texture(
gl: &WebGl2RenderingContext,
texture: &WebGlTexture,
) -> WebGlFramebuffer {
let framebuffer = gl.create_framebuffer().unwrap();
gl.bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(&framebuffer));
gl.framebuffer_texture_2d(
WebGl2RenderingContext::FRAMEBUFFER,
WebGl2RenderingContext::COLOR_ATTACHMENT0,
WebGl2RenderingContext::TEXTURE_2D,
Some(texture),
0,
);
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::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);
}
/// Initialize clear VAO.
fn initialize_clear_vao(gl: &WebGl2RenderingContext, resources: &WebGlResources) {
gl.bind_vertex_array(Some(&resources.clear_vao));
gl.bind_buffer(
WebGl2RenderingContext::ARRAY_BUFFER,
Some(&resources.clear_slot_indices_buffer),
);
// Configure attributes.
let slot_idx_loc = 0;
gl.enable_vertex_attrib_array(slot_idx_loc);
gl.vertex_attrib_i_pointer_with_i32(
slot_idx_loc,
1,
WebGl2RenderingContext::UNSIGNED_INT,
4,
0,
);
gl.vertex_attrib_divisor(slot_idx_loc, 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,
image_cache: &'a ImageCache,
encoded_paints: &'a [EncodedPaint],
paint_idxs: &'a [u32],
filter_context: &'a FilterContext,
filter_pass_state: &'a mut FilterPassState,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum OpaqueBucket {
ImageUntinted,
ImageTinted,
Solid,
Gradient,
}
impl OpaqueBucket {
fn from_strip(strip: &GpuStrip, encoded_paints: &[EncodedPaint], paint_idxs: &[u32]) -> Self {
const COLOR_SOURCE_PAYLOAD: u32 = 0;
const PAINT_TYPE_SOLID: u32 = 0;
const PAINT_TYPE_IMAGE: u32 = 1;
const PAINT_TYPE_LINEAR_GRADIENT: u32 = 2;
const PAINT_TYPE_RADIAL_GRADIENT: u32 = 3;
const PAINT_TYPE_SWEEP_GRADIENT: u32 = 4;
let paint_and_rect_flag = strip.paint_and_rect_flag;
let color_source = (paint_and_rect_flag >> 29) & 0x3;
debug_assert_eq!(
color_source, COLOR_SOURCE_PAYLOAD,
"only payload-backed strips may be emitted as opaque"
);
match (paint_and_rect_flag >> 26) & 0x7 {
PAINT_TYPE_SOLID => Self::Solid,
PAINT_TYPE_IMAGE => {
let paint_tex_idx = paint_and_rect_flag & 0x03FF_FFFF;
let encoded_paint_idx = paint_idxs[..encoded_paints.len()]
.binary_search(&paint_tex_idx)
.expect("opaque image strip must reference a valid encoded paint");
let EncodedPaint::Image(img) = &encoded_paints[encoded_paint_idx] else {
panic!("opaque image strip must reference an encoded image paint");
};
if img.tint.is_some() {
Self::ImageTinted
} else {
Self::ImageUntinted
}
}
PAINT_TYPE_LINEAR_GRADIENT | PAINT_TYPE_RADIAL_GRADIENT | PAINT_TYPE_SWEEP_GRADIENT => {
Self::Gradient
}
paint_type => panic!("unexpected opaque paint type {paint_type}"),
}
}
}
fn bind_strip_program(
gl: &WebGl2RenderingContext,
program: &WebGlProgram,
uniforms: &StripUniforms,
resources: &WebGlResources,
clip_texture_idx: usize,
) {
gl.use_program(Some(program));
if let Some(location) = &uniforms.alphas_texture {
gl.active_texture(WebGl2RenderingContext::TEXTURE0);
gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(&resources.alphas_texture),
);
gl.uniform1i(Some(location), 0);
}
if let Some(location) = &uniforms.clip_input_texture {
gl.active_texture(WebGl2RenderingContext::TEXTURE1);
gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(&resources.slot_textures[clip_texture_idx]),
);
gl.uniform1i(Some(location), 1);
}
if let Some(location) = &uniforms.atlas_texture_array {
gl.active_texture(WebGl2RenderingContext::TEXTURE2);
gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D_ARRAY,
Some(&resources.atlas_texture_array.texture),
);
gl.uniform1i(Some(location), 2);
}
if let Some(location) = &uniforms.encoded_paints_texture_fs {
gl.active_texture(WebGl2RenderingContext::TEXTURE3);
gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(&resources.encoded_paints_texture),
);
gl.uniform1i(Some(location), 3);
}
if let Some(location) = &uniforms.encoded_paints_texture_vs {
gl.active_texture(WebGl2RenderingContext::TEXTURE3);
gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(&resources.encoded_paints_texture),
);
gl.uniform1i(Some(location), 3);
}
if let Some(location) = &uniforms.gradient_texture {
gl.active_texture(WebGl2RenderingContext::TEXTURE4);
gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(&resources.gradient_texture),
);
gl.uniform1i(Some(location), 4);
}
}
fn set_strip_instance_offset(gl: &WebGl2RenderingContext, base_instance: i32) {
let base_byte_offset = base_instance * STRIP_STRIDE;
for i in 0..STRIP_ATTR_COUNT {
gl.vertex_attrib_i_pointer_with_i32(
i as u32,
1,
WebGl2RenderingContext::UNSIGNED_INT,
STRIP_STRIDE,
i * 4 + base_byte_offset,
);
}
}
impl WebGlRendererContext<'_> {
/// Render strips to the specified render target.
fn do_strip_render_pass(
&mut self,
opaque_strips: &[GpuStrip],
alpha_strips: &[GpuStrip],
target: StripPassRenderTarget,
load: LoadOp,
) {
if opaque_strips.is_empty() && alpha_strips.is_empty() {
return;
}
let scissor_rect = match &target {
StripPassRenderTarget::FilterLayer(layer_id) => {
let image_id = self
.filter_context
.filter_textures
.get(layer_id)
.unwrap()
.initial_image_id;
let resources = self.filter_context.image_cache.get(image_id).unwrap();
let atlas_idx = resources.atlas_id.as_u32() as usize;
let fb = &self.programs.resources.filter_atlas_framebuffers[atlas_idx];
self.gl
.bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(fb));
let atlas_width = self.programs.resources.filter_atlas_width;
let atlas_height = self.programs.resources.filter_atlas_height;
self.gl
.viewport(0, 0, atlas_width as i32, atlas_height as i32);
let filter_textures = self.filter_context.filter_textures.get(layer_id).unwrap();
let strip_offset_x = resources.offset[0] as i32
- (filter_textures.bbox.x0() * WideTile::WIDTH) as i32;
let strip_offset_y =
resources.offset[1] as i32 - (filter_textures.bbox.y0() * Tile::HEIGHT) as i32;
let config = Config {
width: atlas_width,
height: atlas_height,
strip_height: u32::from(Tile::HEIGHT),
alphas_tex_width_bits: self
.programs
.resources
.max_texture_dimension_2d
.trailing_zeros(),
encoded_paints_tex_width_bits: self
.programs
.resources
.max_texture_dimension_2d
.trailing_zeros(),
strip_offset_x,
strip_offset_y,
negate_ndc: 0,
};
let buf = &self.programs.resources.filter_config_buffer;
self.gl
.bind_buffer(WebGl2RenderingContext::UNIFORM_BUFFER, Some(buf));
self.gl.buffer_data_with_u8_array(
WebGl2RenderingContext::UNIFORM_BUFFER,
bytemuck::bytes_of(&config),
WebGl2RenderingContext::DYNAMIC_DRAW,
);
self.gl
.bind_buffer_base(WebGl2RenderingContext::UNIFORM_BUFFER, 0, Some(buf));
Some([
resources.offset[0] as i32,
resources.offset[1] as i32,
resources.width as i32,
resources.height as i32,
])
}
StripPassRenderTarget::Root(_) => {
self.gl.bind_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
self.programs.resources.view_framebuffer_override.as_ref(),
);
let width = self.programs.render_size.width;
let height = self.programs.render_size.height;
self.gl.viewport(0, 0, width as i32, height as i32);
self.gl.bind_buffer_base(
WebGl2RenderingContext::UNIFORM_BUFFER,
0,
Some(&self.programs.resources.view_config_buffer),
);
None
}
StripPassRenderTarget::SlotTexture(ix) => {
self.gl.bind_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
Some(&self.programs.resources.slot_framebuffers[*ix as usize]),
);
// Set viewport to match slot framebuffer.
// TODO: Remove the slot height texture calculation.
let total_slots: usize = (self.programs.resources.max_texture_dimension_2d
/ u32::from(Tile::HEIGHT)) as usize;
// Set viewport to match slot texture.
let height = u32::from(Tile::HEIGHT) * total_slots as u32;
self.gl
.viewport(0, 0, i32::from(WideTile::WIDTH), height as i32);
// Use slot config buffer for rendering to a slot texture.
self.gl.bind_buffer_base(
WebGl2RenderingContext::UNIFORM_BUFFER,
0,
Some(&self.programs.resources.slot_config_buffer),
);
None
}
};
if let Some([x, y, width, height]) = scissor_rect {
self.gl.enable(WebGl2RenderingContext::SCISSOR_TEST);
self.gl.scissor(x, y, width, height);
} else {
self.gl.disable(WebGl2RenderingContext::SCISSOR_TEST);
}
// Clear framebuffer if requested.
if matches!(load, LoadOp::Clear) {
self.gl.clear_color(0.0, 0.0, 0.0, 0.0);
self.gl.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT);
}
let clip_texture_idx = match &target {
StripPassRenderTarget::SlotTexture(1) => 0,
_ => 1,
};
// Set up attributes.
self.gl
.bind_vertex_array(Some(&self.programs.resources.strip_vao));
self.gl.bind_buffer(
WebGl2RenderingContext::ARRAY_BUFFER,
Some(&self.programs.resources.strips_buffer),
);
// 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 is_final_view = matches!(
target,
StripPassRenderTarget::Root(RootRenderTarget::UserSurface)
);
let mut image_untinted_opaque = Vec::new();
let mut image_tinted_opaque = Vec::new();
let mut solid_opaque = Vec::new();
let mut gradient_opaque = Vec::new();
let mut uploaded_strips = Vec::new();
let (alpha_start, alpha_count) = if is_final_view {
image_untinted_opaque.reserve(opaque_strips.len());
image_tinted_opaque.reserve(opaque_strips.len());
solid_opaque.reserve(opaque_strips.len());
gradient_opaque.reserve(opaque_strips.len());
for strip in opaque_strips {
match OpaqueBucket::from_strip(strip, self.encoded_paints, self.paint_idxs) {
OpaqueBucket::ImageUntinted => image_untinted_opaque.push(*strip),
OpaqueBucket::ImageTinted => image_tinted_opaque.push(*strip),
OpaqueBucket::Solid => solid_opaque.push(*strip),
OpaqueBucket::Gradient => gradient_opaque.push(*strip),
}
}
uploaded_strips.reserve(
image_untinted_opaque.len()
+ image_tinted_opaque.len()
+ solid_opaque.len()
+ gradient_opaque.len()
+ alpha_strips.len(),
);
uploaded_strips.extend_from_slice(&image_untinted_opaque);
uploaded_strips.extend_from_slice(&image_tinted_opaque);
uploaded_strips.extend_from_slice(&solid_opaque);
uploaded_strips.extend_from_slice(&gradient_opaque);
let alpha_start = uploaded_strips.len() as i32;
uploaded_strips.extend_from_slice(alpha_strips);
(alpha_start, alpha_strips.len() as i32)
} else {
debug_assert!(
opaque_strips.is_empty(),
"opaque strips should only be emitted for the user surface"
);
uploaded_strips.extend_from_slice(alpha_strips);
(0, alpha_strips.len() as i32)
};
let image_untinted_start = 0_i32;
let image_untinted_count = image_untinted_opaque.len() as i32;
let image_tinted_start = image_untinted_count;
let image_tinted_count = image_tinted_opaque.len() as i32;
let solid_start = image_tinted_start + image_tinted_count;
let solid_count = solid_opaque.len() as i32;
let gradient_start = solid_start + solid_count;
let gradient_count = gradient_opaque.len() as i32;
self.programs.upload_strips(self.gl, &uploaded_strips);
set_strip_instance_offset(self.gl, 0);
if is_final_view {
self.gl.enable(WebGl2RenderingContext::DEPTH_TEST);
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 image_untinted_count + image_tinted_count + solid_count + gradient_count > 0 {
self.gl.depth_mask(true);
self.gl.disable(WebGl2RenderingContext::BLEND);
}
if image_untinted_count > 0 {
bind_strip_program(
self.gl,
&self.programs.opaque_image_untinted_program,
&self.programs.opaque_image_untinted_uniforms,
&self.programs.resources,
clip_texture_idx,
);
set_strip_instance_offset(self.gl, image_untinted_start);
self.gl.draw_arrays_instanced(
WebGl2RenderingContext::TRIANGLE_STRIP,
0,
4,
image_untinted_count,
);
}
if image_tinted_count > 0 {
bind_strip_program(
self.gl,
&self.programs.opaque_image_program,
&self.programs.opaque_image_uniforms,
&self.programs.resources,
clip_texture_idx,
);
set_strip_instance_offset(self.gl, image_tinted_start);
self.gl.draw_arrays_instanced(
WebGl2RenderingContext::TRIANGLE_STRIP,
0,
4,
image_tinted_count,
);
}
if solid_count > 0 {
bind_strip_program(
self.gl,
&self.programs.opaque_solid_program,
&self.programs.opaque_solid_uniforms,
&self.programs.resources,
clip_texture_idx,
);
set_strip_instance_offset(self.gl, solid_start);
self.gl.draw_arrays_instanced(
WebGl2RenderingContext::TRIANGLE_STRIP,
0,
4,
solid_count,
);
}
if gradient_count > 0 {
bind_strip_program(
self.gl,
&self.programs.opaque_gradient_program,
&self.programs.opaque_gradient_uniforms,
&self.programs.resources,
clip_texture_idx,
);
set_strip_instance_offset(self.gl, gradient_start);
self.gl.draw_arrays_instanced(
WebGl2RenderingContext::TRIANGLE_STRIP,
0,
4,
gradient_count,
);
}
// Alpha pass: back-to-front, depth test ON, depth write OFF, blend ON.
if alpha_count > 0 {
bind_strip_program(
self.gl,
&self.programs.strip_program,
&self.programs.strip_uniforms,
&self.programs.resources,
clip_texture_idx,
);
set_strip_instance_offset(self.gl, alpha_start);
self.gl.depth_mask(false);
self.gl.enable(WebGl2RenderingContext::BLEND);
self.gl.draw_arrays_instanced(
WebGl2RenderingContext::TRIANGLE_STRIP,
0,
4,
alpha_count,
);
}
// Restore state.
self.gl.disable(WebGl2RenderingContext::DEPTH_TEST);
self.gl.depth_mask(true);
self.gl.enable(WebGl2RenderingContext::BLEND);
set_strip_instance_offset(self.gl, 0);
} else {
// Slot texture / intermediate: single draw with blending, no depth.
bind_strip_program(
self.gl,
&self.programs.strip_program,
&self.programs.strip_uniforms,
&self.programs.resources,
clip_texture_idx,
);
self.gl.draw_arrays_instanced(
WebGl2RenderingContext::TRIANGLE_STRIP,
0,
4,
alpha_count,
);
}
// Clean up.
self.gl.bind_vertex_array(None);
}
/// Clear specific slots from a slot texture.
fn do_clear_slots_render_pass(&mut self, ix: usize, slot_indices: &[u32]) {
if slot_indices.is_empty() {
return;
}
// No blending needed for clearing: we want to completely overwrite existing slot data
// (matches wgpu implementation)
self.gl.disable(WebGl2RenderingContext::BLEND);
// Upload slot indices.
self.gl.bind_buffer(
WebGl2RenderingContext::ARRAY_BUFFER,
Some(&self.programs.resources.clear_slot_indices_buffer),
);
let slot_indices_data = bytemuck::cast_slice(slot_indices);
self.gl.buffer_data_with_u8_array(
WebGl2RenderingContext::ARRAY_BUFFER,
slot_indices_data,
WebGl2RenderingContext::STATIC_DRAW,
);
// Bind framebuffer and setup viewport.
self.gl.bind_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
Some(&self.programs.resources.slot_framebuffers[ix]),
);
// TODO: Remove the slot height texture calculation.
let total_slots: usize =
(self.programs.resources.max_texture_dimension_2d / u32::from(Tile::HEIGHT)) as usize;
let height = u32::from(Tile::HEIGHT) * total_slots as u32;
self.gl
.viewport(0, 0, i32::from(WideTile::WIDTH), height as i32);
// Setup clear program.
self.gl.use_program(Some(&self.programs.clear_program));
// Set up attributes.
self.gl
.bind_vertex_array(Some(&self.programs.resources.clear_vao));
// Set up clear config.
self.gl.bind_buffer_base(
WebGl2RenderingContext::UNIFORM_BUFFER,
self.programs.clear_uniforms.config_block_index,
Some(&self.programs.resources.clear_config_buffer),
);
// Draw.
self.gl.draw_arrays_instanced(
WebGl2RenderingContext::TRIANGLE_STRIP,
0,
4,
slot_indices.len() as i32,
);
self.gl.enable(WebGl2RenderingContext::BLEND);
// Clean up.
self.gl.bind_vertex_array(None);
}
}
impl RendererBackend for WebGlRendererContext<'_> {
/// Clear specific slots in a texture
fn clear_slots(&mut self, texture_index: usize, slots: &[u32]) {
self.do_clear_slots_render_pass(texture_index, slots);
}
/// Execute a render pass for strips.
fn render_strips(
&mut self,
opaque_strips: &[GpuStrip],
alpha_strips: &[GpuStrip],
target: StripPassRenderTarget,
load_op: LoadOp,
) {
self.do_strip_render_pass(opaque_strips, alpha_strips, target, load_op);
}
fn apply_filter(&mut self, layer_id: LayerId) {
let filter_atlas_width = self.programs.resources.filter_atlas_width;
let filter_atlas_height = self.programs.resources.filter_atlas_height;
self.filter_context.build_filter_passes(
self.filter_pass_state,
&layer_id,
self.image_cache,
|_atlas_idx| [filter_atlas_width, filter_atlas_height],
|| {
[
self.programs.resources.atlas_texture_array.size.width,
self.programs.resources.atlas_texture_array.size.height,
]
},
);
let filter_passes = self.filter_pass_state.filter_passes();
if filter_passes.is_empty() {
return;
}
self.gl.disable(WebGl2RenderingContext::BLEND);
self.gl.enable(WebGl2RenderingContext::SCISSOR_TEST);
let instances = self.filter_pass_state.instances();
self.programs.upload_filter_instances(self.gl, instances);
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);
for (i, pass) in filter_passes.iter().enumerate() {
// Base points to the correct offset for that specific filter pass.
let base = (i as i32) * FILTER_INSTANCE_STRIDE;
for (loc, &(components, offset)) in FILTER_ATTRIBS.iter().enumerate() {
self.gl.vertex_attrib_i_pointer_with_i32(
loc as u32,
components,
WebGl2RenderingContext::UNSIGNED_INT,
FILTER_INSTANCE_STRIDE,
base + offset,
);
}
let (target_width, target_height) = match &pass.output {
FilterPassTarget::FilterAtlas(idx) => {
let fb = &self.programs.resources.filter_atlas_framebuffers[*idx as usize];
self.gl
.bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(fb));
self.gl
.viewport(0, 0, filter_atlas_width as i32, filter_atlas_height as i32);
(filter_atlas_width, filter_atlas_height)
}
FilterPassTarget::MainAtlas(idx) => {
let fb = self
.programs
.resources
.filter_main_atlas_framebuffer
.get_or_insert_with(|| self.gl.create_framebuffer().unwrap());
self.gl
.bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(fb));
self.gl.framebuffer_texture_layer(
WebGl2RenderingContext::FRAMEBUFFER,
WebGl2RenderingContext::COLOR_ATTACHMENT0,
Some(&self.programs.resources.atlas_texture_array.texture),
0,
*idx as i32,
);
let width = self.programs.resources.atlas_texture_array.size.width;
let height = self.programs.resources.atlas_texture_array.size.height;
self.gl.viewport(0, 0, width as i32, height as i32);
(width, height)
}
};
let instance = &instances[i];
let [x, y, width, height] = instance.scissor_rect([target_width, target_height]);
self.gl
.scissor(x as i32, y as i32, width as i32, height as i32);
let input_tex =
&self.programs.resources.filter_atlas_textures[pass.input_atlas_idx as usize];
self.gl.active_texture(WebGl2RenderingContext::TEXTURE1);
self.gl
.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(input_tex));
self.gl
.uniform1i(Some(&self.programs.filter_uniforms.in_tex), 1);
let original_tex_idx = pass.original_atlas_idx.unwrap_or(pass.input_atlas_idx);
let original_tex =
&self.programs.resources.filter_atlas_textures[original_tex_idx as usize];
self.gl.active_texture(WebGl2RenderingContext::TEXTURE2);
self.gl
.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(original_tex));
self.gl
.uniform1i(Some(&self.programs.filter_uniforms.original_tex), 2);
self.gl
.draw_arrays_instanced(WebGl2RenderingContext::TRIANGLE_STRIP, 0, 4, 1);
}
self.gl.bind_vertex_array(None);
self.gl.disable(WebGl2RenderingContext::SCISSOR_TEST);
self.gl.enable(WebGl2RenderingContext::BLEND);
}
}
/// 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) -> u32;
/// Get the height of the image.
fn height(&self) -> u32;
/// Write image data to a specific layer of an atlas texture array at the specified offset.
fn write_to_atlas_layer(
&self,
gl: &WebGl2RenderingContext,
atlas_texture_array: &WebGlTexture,
layer: u32,
offset: [u32; 2],
width: u32,
height: u32,
);
}
/// Implementation for `Pixmap` - direct upload using raw pixel data.
impl WebGlAtlasWriter for Pixmap {
fn width(&self) -> u32 {
self.width() as u32
}
fn height(&self) -> u32 {
self.height() as u32
}
fn write_to_atlas_layer(
&self,
gl: &WebGl2RenderingContext,
atlas_texture_array: &WebGlTexture,
layer: u32,
offset: [u32; 2],
width: u32,
height: u32,
) {
// Bind the atlas texture array
gl.active_texture(WebGl2RenderingContext::TEXTURE0);
gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D_ARRAY,
Some(atlas_texture_array),
);
// Convert pixmap data to the format expected by WebGL
let rgba_data = self.data_as_u8_slice();
// Upload the image data to the specific layer and region of the atlas texture array
gl.tex_sub_image_3d_with_opt_u8_array(
WebGl2RenderingContext::TEXTURE_2D_ARRAY,
0,
offset[0] as i32,
offset[1] as i32,
layer as i32,
width as i32,
height as i32,
1,
WebGl2RenderingContext::RGBA,
WebGl2RenderingContext::UNSIGNED_BYTE,
Some(rgba_data),
)
.unwrap();
}
}
/// Implementation for `Arc<Pixmap>`.
impl WebGlAtlasWriter for Arc<Pixmap> {
fn width(&self) -> u32 {
self.as_ref().width() as u32
}
fn height(&self) -> u32 {
self.as_ref().height() as u32
}
fn write_to_atlas_layer(
&self,
gl: &WebGl2RenderingContext,
atlas_texture_array: &WebGlTexture,
layer: u32,
offset: [u32; 2],
width: u32,
height: u32,
) {
self.as_ref()
.write_to_atlas_layer(gl, atlas_texture_array, layer, offset, width, height);
}
}
/// Implementation for `WebGlTexture` - texture-to-texture copy.
impl WebGlAtlasWriter for WebGlTexture {
fn width(&self) -> u32 {
// 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) -> u32 {
// 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_layer(
&self,
gl: &WebGl2RenderingContext,
atlas_texture_array: &WebGlTexture,
layer: u32,
offset: [u32; 2],
width: u32,
height: u32,
) {
copy_to_texture_array_layer(
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_array,
layer,
offset,
[width, 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: u32,
/// The height of the texture.
pub height: u32,
}
impl WebGlAtlasWriter for WebGlTextureWithDimensions {
fn width(&self) -> u32 {
self.width
}
fn height(&self) -> u32 {
self.height
}
fn write_to_atlas_layer(
&self,
gl: &WebGl2RenderingContext,
atlas_texture_array: &WebGlTexture,
layer: u32,
offset: [u32; 2],
width: u32,
height: u32,
) {
self.texture
.write_to_atlas_layer(gl, atlas_texture_array, layer, offset, width, height);
}
}
/// Wrapper for `WebGlTexture` array with known dimensions.
#[derive(Debug)]
struct WebGlTextureArray {
/// The WebGL texture array.
texture: WebGlTexture,
/// The size of the texture array.
size: WebGlTextureSize,
}
impl WebGlTextureArray {
/// Create a new WebGL texture array wrapper.
fn new(texture: WebGlTexture, width: u32, height: u32, depth_or_array_layers: u32) -> Self {
Self {
texture,
size: WebGlTextureSize {
width,
height,
depth_or_array_layers,
},
}
}
/// Get the size of the texture array, similar to WGPU's `texture.size()`.
fn size(&self) -> WebGlTextureSize {
self.size
}
}
/// Size information for WebGL texture arrays, similar to WGPU's `Extent3d`.
#[derive(Debug, Clone, Copy)]
struct WebGlTextureSize {
/// The width of the texture.
width: u32,
/// The height of the texture.
height: u32,
/// The number of layers in the texture array.
depth_or_array_layers: u32,
}
/// Helper function to copy from a source texture/framebuffer to a destination texture array layer.
fn copy_to_texture_array_layer(
gl: &WebGl2RenderingContext,
source_setup: impl FnOnce(&WebGl2RenderingContext),
dest_texture_array: &WebGlTexture,
dest_layer: u32,
dest_offset: [u32; 2],
copy_size: [u32; 2],
) {
let _state_guard = WebGlStateGuard::for_texture_copy(gl);
let read_framebuffer = gl.create_framebuffer().unwrap();
// Bind destination texture array
gl.active_texture(WebGl2RenderingContext::TEXTURE0);
gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D_ARRAY,
Some(dest_texture_array),
);
// 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);
// Copy from READ framebuffer to destination array layer
gl.copy_tex_sub_image_3d(
WebGl2RenderingContext::TEXTURE_2D_ARRAY,
0,
dest_offset[0] as i32,
dest_offset[1] as i32,
dest_layer as i32,
0,
0,
copy_size[0] as i32,
copy_size[1] as i32,
);
// Clean up
gl.delete_framebuffer(Some(&read_framebuffer));
}
// Upload the data to the currently bound texture assuming a RGBA32UI format.
fn upload_data_to_rgba32_texture(
gl: &WebGl2RenderingContext,
data: &[u32],
texture_width: u32,
texture_height: u32,
) {
// 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_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_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view(
WebGl2RenderingContext::TEXTURE_2D,
0,
WebGl2RenderingContext::RGBA32UI as i32,
texture_width as i32,
texture_height as i32,
0,
WebGl2RenderingContext::RGBA_INTEGER,
WebGl2RenderingContext::UNSIGNED_INT,
Some(&packed_array),
)
.unwrap();
}