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